├── docs ├── .nojekyll └── GITHUB_PAGES_SETUP.md ├── doc ├── cryodune_worm.png ├── cryodune_chani.png ├── cryodune_harkonen.png └── cryodune_send_spice.png ├── dump ├── spice86dumpMemoryDump_E000_03EE_After_code_modification_1_fixed.bin ├── accessesToIntRelatedVars.txt └── CodeGeneratorConfig.json ├── .github ├── dependabot.yml ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── static.yml │ ├── pr-validation.yml │ └── release.yml └── copilot-instructions.md ├── NOTICE ├── src ├── Cryogenic │ ├── Globals │ │ ├── ExtraGlobalsOnCsSegment0x2538.cs │ │ └── ExtraGlobalsOnDs.cs │ ├── Generated │ │ ├── GlobalsOnEs.cs │ │ └── GlobalsOnCsSegment0x1ED.cs │ ├── Cryogenic.csproj │ ├── Program.cs │ ├── Overrides │ │ ├── InitCode.cs │ │ ├── TimerCode.cs │ │ ├── ScriptedSceneCode.cs │ │ ├── MT32DriverCode.cs │ │ ├── TimeCode.cs │ │ ├── DatastructuresCode.cs │ │ ├── VideoCode.cs │ │ ├── DialoguesCode.cs │ │ ├── HnmCode.cs │ │ ├── MapCode.cs │ │ ├── MenuCode.cs │ │ ├── DisplayCode.cs │ │ ├── UnknownCode.cs │ │ ├── StaticDefinitions.cs │ │ ├── Overrides.cs │ │ └── VgaDriverCode.cs │ ├── DuneCdOverrideSupplier.cs │ └── DriverLoadToolbox.cs ├── Cryogenic.sln └── .editorconfig ├── GitVersion.yml ├── .gitattributes ├── .gitignore ├── README.md ├── LICENSE └── CONTRIBUTING.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/cryodune_worm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRakis/Cryogenic/HEAD/doc/cryodune_worm.png -------------------------------------------------------------------------------- /doc/cryodune_chani.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRakis/Cryogenic/HEAD/doc/cryodune_chani.png -------------------------------------------------------------------------------- /doc/cryodune_harkonen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRakis/Cryogenic/HEAD/doc/cryodune_harkonen.png -------------------------------------------------------------------------------- /doc/cryodune_send_spice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRakis/Cryogenic/HEAD/doc/cryodune_send_spice.png -------------------------------------------------------------------------------- /dump/spice86dumpMemoryDump_E000_03EE_After_code_modification_1_fixed.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRakis/Cryogenic/HEAD/dump/spice86dumpMemoryDump_E000_03EE_After_code_modification_1_fixed.bin -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # generated by dependadotnet 2 | # https://github.com/dotnet/core/tree/master/samples/dependadotnet 3 | version: 2 4 | updates: 5 | - package-ecosystem: "nuget" 6 | directory: "/src" 7 | schedule: 8 | interval: "weekly" 9 | day: "wednesday" 10 | open-pull-requests-limit: 5 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description of Changes 2 | 3 | 4 | ### Rationale behind Changes 5 | 6 | 7 | ### Suggested Testing Steps 8 | 12 | 13 | 0.1.0 14 | 0.1.0.0 15 | 0.1.0.0 16 | 0.1.0 17 | 18 | 19 | 20 | True 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Cryogenic/Program.cs: -------------------------------------------------------------------------------- 1 | using Cryogenic; 2 | 3 | using Spice86.Core.CLI; 4 | 5 | // Configure command-line arguments for Spice86 execution 6 | List newArgs = args.ToList(); 7 | //newArgs.AddRange("-m /mnt/c/mt32-rom-data -d true -e /mnt/c/Jeux/ABWFR/DUNE_CD/C/DNCDPRG.EXE -a \"MID330 SBP2227\" --UseCodeOverride true".Split(" ")); 8 | 9 | // Inject the custom interrupt handler segment address into the configuration 10 | // This reserves segment 0x800 for BIOS/IRQ handlers, freeing 0xF000 for driver3 11 | newArgs.Add($"--{nameof(Configuration.ProvidedAsmHandlersSegment)}={DriverLoadToolbox.INTERRUPT_HANDLER_SEGMENT}"); 12 | 13 | // Launch Spice86 with the DuneCdOverrideSupplier and verify the DNCDPRG.EXE checksum 14 | // The SHA256 checksum ensures we're running against the expected version of the DOS executable 15 | global::Spice86.Program.RunWithOverrides(newArgs.ToArray(), "5F30AEB84D67CF2E053A83C09C2890F010F2E25EE877EBEC58EA15C5B30CFFF9"); 16 | 17 | /// 18 | /// Entry point class for the Cryogenic application. 19 | /// 20 | /// 21 | /// This partial class serves as the main entry point for running the Dune CD game 22 | /// through Spice86 with custom C# overrides. The top-level statements handle 23 | /// argument configuration and program launch. 24 | /// 25 | public partial class Program { 26 | } -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload docs directory 40 | path: 'docs' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/InitCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | using Spice86.Core.Emulator.ReverseEngineer; 4 | 5 | using System; 6 | 7 | /// 8 | /// Partial class containing game initialization related overrides. 9 | /// 10 | /// 11 | /// Method names contain underscores to separate segment, offset, and linear addresses 12 | /// for traceability back to the original DOS disassembly. 13 | /// 14 | public partial class Overrides { 15 | /// 16 | /// Registers game initialization function overrides with Spice86. 17 | /// 18 | public void DefineInitCodeOverrides() { 19 | DefineFunction(cs1, 0xDA53, VgaInitRelated_1000_DA53_01DA53); 20 | } 21 | 22 | /// 23 | /// Override for CS1:DA53 - Initializes VGA-related global variables to zero. 24 | /// 25 | /// Target address for potential jumps (unused in this override). 26 | /// A near return action to exit the function. 27 | /// 28 | /// Called during game initialization to clear VGA-related state variables. 29 | /// Sets DS:DC6A (16-bit) and DS:46D7 (8-bit) to zero. 30 | /// 31 | public Action VgaInitRelated_1000_DA53_01DA53(int gotoAddress) { 32 | this.globalsOnDs.Set1138_DC6A_Word16(0); 33 | this.globalsOnDs.Set1138_46D7_Byte8(0); 34 | return NearRet(); 35 | } 36 | } -------------------------------------------------------------------------------- /dump/accessesToIntRelatedVars.txt: -------------------------------------------------------------------------------- 1 | // Init timer 2 | 0xE8D4 3 | 1000_E86E 4 | 5 | // In game timer 6 | 0xCE7A 7 | 1000_D296 8 | 1000_D2A9 9 | 1000_D7B7 10 | 1000_D815 11 | 1000_D893 12 | 1000_D8DA 13 | 1000_D935 14 | 1000_D962 15 | 1000_D97A 16 | 1000_D9D5 17 | 1000_18B1 18 | 1000_1AF0 19 | 1000_2B47 20 | 1000_2E55 21 | 1000_4F27 22 | 1000_4F34 23 | 1000_5C0D 24 | 1000_A774 25 | 1000_A7D5 26 | 1000_A894 27 | 1000_A89D 28 | 1000_ABAE 29 | 1000_ABBC 30 | 1000_ACCD 31 | 1000_AD07 32 | 1000_AD0D 33 | 1000_C0DE 34 | 1000_C0EC 35 | 1000_C121 36 | 1000_DDCA 37 | 1000_DDDA 38 | 1000_DE93 39 | 1000_E05E 40 | 1000_E213 41 | 1000_E354 42 | 1000_E37B 43 | 1000_E38F 44 | 1000_E393 45 | 1000_E3A2 46 | 1000_E3A5 47 | 1000_E63D 48 | 1000_C85B 49 | 1000_C8C3 50 | 1000_C8ED 51 | 1000_CA59 52 | 1000_CADA 53 | 54 | 0x46DD 55 | 1000_0FEC 56 | 1000_1B23 57 | 58 | //in game Time 59 | 0x2 60 | 1000_395C 61 | 1000_917A 62 | 1000_950F 63 | 1000_9572 64 | 1000_957D 65 | 1000_0FF6 66 | 1000_1A42 67 | 1000_1AC5 68 | 1000_1AD1 69 | 1000_1AE0 70 | 1000_1B46 71 | 1000_1F6B 72 | 1000_1F79 73 | 1000_32C7 74 | 1000_B393 75 | 1000_6D7B 76 | 1000_705C 77 | 1000_7159 78 | 1000_66A3 79 | 1000_6B25 80 | 1000_BDCE 81 | 1000_BE3C 82 | 1000_BED7 83 | 1000_BF89 84 | 85 | // MIDI 86 | 0xDBCD 87 | 1000_ADCA 88 | 1000_AE10 89 | 1000_AE17 90 | 1000_ACF9 91 | 1000_AD3C 92 | 1000_AD6D 93 | 1000_DDF0 94 | 1000_DE0C 95 | 96 | 0xDBCE 97 | 1000_0635 98 | 1000_DE2F 99 | 100 | 0xDBD0 101 | 1000_DE23 102 | 103 | //Keyboard 104 | 0xCEE8 105 | 1000_D820 106 | 1000_DD6B 107 | 1000_DE59 108 | -------------------------------------------------------------------------------- /docs/GITHUB_PAGES_SETUP.md: -------------------------------------------------------------------------------- 1 | # GitHub Pages Setup Instructions 2 | 3 | This repository is configured to deploy documentation to GitHub Pages automatically using GitHub Actions. 4 | 5 | ## Prerequisites 6 | 7 | GitHub Pages must be manually enabled in the repository settings before the first deployment. 8 | 9 | ## Setup Steps 10 | 11 | 1. Navigate to your repository's **Settings** tab 12 | 2. In the left sidebar, click on **Pages** 13 | 3. Under **Source**, select **GitHub Actions** 14 | 4. Save the settings 15 | 16 | That's it! Once GitHub Pages is enabled, any push to the `main` branch that modifies files in the `docs/` directory will automatically trigger a deployment. 17 | 18 | ## Troubleshooting 19 | 20 | ### Error: "Resource not accessible by integration" 21 | 22 | This error occurs when trying to automatically create a GitHub Pages site using the workflow. The solution is to manually enable GitHub Pages in the repository settings as described above. 23 | 24 | ### Pages Not Updating 25 | 26 | If your changes aren't appearing on the GitHub Pages site: 27 | 28 | 1. Check the **Actions** tab to ensure the workflow ran successfully 29 | 2. Verify that GitHub Pages is enabled in repository settings 30 | 3. Wait a few minutes for changes to propagate 31 | 4. Clear your browser cache and try again 32 | 33 | ## Workflow Details 34 | 35 | The deployment workflow (`.github/workflows/deploy-pages.yml`) is triggered by: 36 | - Pushes to `main` branch that modify `docs/**` files 37 | - Changes to the workflow file itself 38 | - Manual workflow dispatch 39 | 40 | For more information about GitHub Pages, see the [official documentation](https://docs.github.com/en/pages). 41 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: ContinuousDeployment 2 | next-version: 0.1.0 3 | assembly-versioning-scheme: MajorMinorPatch 4 | assembly-file-versioning-scheme: MajorMinorPatch 5 | assembly-informational-format: '{InformationalVersion}' 6 | branches: 7 | main: 8 | regex: ^(main|master)$ 9 | mode: ContinuousDeployment 10 | label: '' 11 | increment: Patch 12 | prevent-increment: 13 | of-merged-branch: true 14 | track-merge-target: false 15 | tracks-release-branches: false 16 | is-release-branch: true 17 | pull-request: 18 | regex: ^(pull|pull\-requests|pr)[/-] 19 | mode: ContinuousDeployment 20 | label: PullRequest 21 | increment: Inherit 22 | label-number-pattern: '[/-](?\d+)[-/]' 23 | feature: 24 | regex: ^features?[/-] 25 | mode: ContinuousDeployment 26 | label: useBranchName 27 | increment: Inherit 28 | copilot: 29 | regex: ^copilot[/-] 30 | mode: ContinuousDeployment 31 | label: useBranchName 32 | increment: Inherit 33 | develop: 34 | regex: ^dev(elop)?(ment)?$ 35 | mode: ContinuousDeployment 36 | label: alpha 37 | increment: Minor 38 | prevent-increment: 39 | of-merged-branch: false 40 | track-merge-target: true 41 | release: 42 | regex: ^releases?[/-] 43 | mode: ContinuousDeployment 44 | label: beta 45 | increment: None 46 | prevent-increment: 47 | of-merged-branch: true 48 | track-merge-target: false 49 | tracks-release-branches: true 50 | is-release-branch: true 51 | hotfix: 52 | regex: ^hotfix(es)?[/-] 53 | mode: ContinuousDeployment 54 | label: beta 55 | increment: Patch 56 | ignore: 57 | sha: [] 58 | merge-message-formats: {} 59 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=crlf 5 | 6 | ############################################################################### 7 | # Set the merge driver for project and solution files 8 | # 9 | # Merging from the command prompt will add diff markers to the files if there 10 | # are conflicts (Merging from VS is not affected by the settings below, in VS 11 | # the diff markers are never inserted). Diff markers may cause the following 12 | # file extensions to fail to load in VS. An alternative would be to treat 13 | # these files as binary and thus will always conflict and require user 14 | # intervention with every merge. To do so, just comment the entries below and 15 | # uncomment the group further below 16 | ############################################################################### 17 | 18 | *.sln text eol=crlf 19 | *.xaml text eol=crlf 20 | *.cs text eol=crlf 21 | *.csproj text eol=crlf 22 | *.vbproj text eol=crlf 23 | *.vcxproj text eol=crlf 24 | *.vcproj text eol=crlf 25 | *.dbproj text eol=crlf 26 | *.fsproj text eol=crlf 27 | *.lsproj text eol=crlf 28 | *.wixproj text eol=crlf 29 | *.modelproj text eol=crlf 30 | *.sqlproj text eol=crlf 31 | *.wmaproj text eol=crlf 32 | 33 | *.xproj text eol=crlf 34 | *.props text eol=crlf 35 | *.filters text eol=crlf 36 | *.vcxitems text eol=crlf 37 | 38 | 39 | #*.sln merge=binary 40 | #*.csproj merge=binary 41 | #*.vbproj merge=binary 42 | #*.vcxproj merge=binary 43 | #*.vcproj merge=binary 44 | #*.dbproj merge=binary 45 | #*.fsproj merge=binary 46 | #*.lsproj merge=binary 47 | #*.wixproj merge=binary 48 | #*.modelproj merge=binary 49 | #*.sqlproj merge=binary 50 | #*.wwaproj merge=binary 51 | 52 | #*.xproj merge=binary 53 | #*.props merge=binary 54 | #*.filters merge=binary 55 | #*.vcxitems merge=binary 56 | -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/TimerCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | using Spice86.Core.Emulator.Devices.Timer; 4 | 5 | using System; 6 | 7 | /// 8 | /// Partial class containing hardware timer (PIT 8254) related overrides. 9 | /// 10 | /// 11 | /// Method names contain underscores to separate segment, offset, and linear addresses 12 | /// for traceability back to the original DOS disassembly. 13 | /// 14 | public partial class Overrides { 15 | /// 16 | /// Registers hardware timer function overrides with Spice86. 17 | /// 18 | public void DefineTimerCodeOverrides() { 19 | DefineFunction(cs1, 0xE8A8, SetPitTimerToAX_1000_E8A8_01E8A8); 20 | } 21 | 22 | /// 23 | /// Override for CS1:E8A8 - Configures the PIT (Programmable Interval Timer) 8254 counter 0. 24 | /// 25 | /// Target address for potential jumps (unused in this override). 26 | /// A near return action to exit the function. 27 | /// 28 | /// 29 | /// Sets up the system timer counter 0 with the value from AX register. 30 | /// The counter is configured in mode 3 (square wave generator) without BCD encoding. 31 | /// 32 | /// 33 | /// This function appears to be called primarily during game shutdown to restore 34 | /// the system timer to its default state. The PIT controls the system clock 35 | /// interrupt frequency (IRQ 0). 36 | /// 37 | /// 38 | public Action SetPitTimerToAX_1000_E8A8_01E8A8(int gotoAddress) { 39 | // Seems only called on quit 40 | ushort valueToSet = AX; 41 | _loggerService.Debug("Setting timer 0 value to {@ValueToSet}", valueToSet); 42 | Timer timer = Machine.Timer; 43 | Pit8254Counter counter = timer.GetCounter(0); 44 | counter.ReadWritePolicy = 0; 45 | counter.Mode = 3; 46 | counter.Bcd = false; 47 | counter.Configure(valueToSet); 48 | return NearRet(); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/ScriptedSceneCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | /// 4 | /// Partial class containing scripted scene sequence handling overrides. 5 | /// 6 | /// 7 | /// Method names contain underscores to separate segment, offset, and linear addresses 8 | /// for traceability back to the original DOS disassembly. This file handles the execution 9 | /// of scripted cutscenes and scene transitions through sequence data. 10 | /// 11 | public partial class Overrides { 12 | 13 | /// 14 | /// Registers scripted scene sequence function overrides with Spice86. 15 | /// 16 | public void DefineScriptedSceneCodeOverrides() { 17 | DefineFunction(cs1, 0x93F, LoadSceneSequenceDataIntoAXAndAdvanceSI_1000_093F_01093F); 18 | DefineFunction(cs1, 0x945, SetSceneSequenceOffsetToSi_1000_0945_010945); 19 | } 20 | 21 | /// 22 | /// Override for CS1:093F - Loads the next scene sequence command into AX and advances the sequence pointer. 23 | /// 24 | /// Target address for potential jumps (unused in this override). 25 | /// A near return action to exit the function. 26 | /// 27 | /// Reads a 16-bit command value from the current scene sequence offset in CS segment, 28 | /// stores it in AX, and advances the SI pointer to the next command. The sequence offset 29 | /// is maintained in global variable DS:4854. 30 | /// 31 | public Action LoadSceneSequenceDataIntoAXAndAdvanceSI_1000_093F_01093F(int gotoAddress) { 32 | ushort offset = globalsOnDs.Get1138_4854_Word16_SceneSequenceOffset(); 33 | ushort value = UInt16[CS, offset]; 34 | AX = value; 35 | _loggerService.Debug("loadSceneSequenceDataIntoAXAndAdvanceSI: offset:{@Offset},value:{@Value}", offset, value); 36 | 37 | // point to next value 38 | SI = (ushort)(offset + 2); 39 | 40 | // in asm this is done by continuing to setUnknownOffset4854ToSi_1ED_945_2815 41 | globalsOnDs.Set1138_4854_Word16_SceneSequenceOffset(SI); 42 | return NearRet(); 43 | } 44 | 45 | public Action SetSceneSequenceOffsetToSi_1000_0945_010945(int gotoAddress) { 46 | ushort offset = SI; 47 | _loggerService.Debug("setUnknownOffset4854ToSi: offset:{@Offset}", offset); 48 | globalsOnDs.Set1138_4854_Word16_SceneSequenceOffset(offset); 49 | return NearRet(); 50 | } 51 | } -------------------------------------------------------------------------------- /dump/CodeGeneratorConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "Namespace":"Cryogenic.Overrides", 3 | "CodeToInject": { 4 | "CheckExternalEvents({nextSegment}, {nextOffset});": [ 5 | "CS1:E86E", 6 | "CS1:D296","CS1:D2A9","CS1:D7B7","CS1:D815","CS1:D893","CS1:D8DA","CS1:D935","CS1:D962","CS1:D97A","CS1:D9D5","CS1:18B1","CS1:1AF0","CS1:2B47","CS1:2E55","CS1:4F27","CS1:4F34","CS1:5C0D","CS1:A774","CS1:A7D5","CS1:A894","CS1:A89D","CS1:ABAE","CS1:ABBC","CS1:ACCD","CS1:AD07","CS1:AD0D","CS1:C0DE","CS1:C0EC","CS1:C121","CS1:DDCA","CS1:DDDA","CS1:DE93","CS1:E05E","CS1:E213","CS1:E354","CS1:E37B","CS1:E38F","CS1:E393","CS1:E3A2","CS1:E3A5","CS1:E63D","CS1:C85B","CS1:C8C3","CS1:C8ED","CS1:CA59","CS1:CADA", 7 | "CS1:0FEC","CS1:1B23", 8 | "CS1:395C","CS1:917A","CS1:950F","CS1:9572","CS1:957D","CS1:0FF6","CS1:1A42","CS1:1AC5","CS1:1AD1","CS1:1AE0","CS1:1B46","CS1:1F6B","CS1:1F79","CS1:32C7","CS1:B393","CS1:6D7B","CS1:705C","CS1:7159","CS1:66A3","CS1:6B25","CS1:BDCE","CS1:BE3C","CS1:BED7","CS1:BF89", 9 | "CS1:ADCA","CS1:AE10","CS1:AE17","CS1:ACF9","CS1:AD3C","CS1:AD6D","CS1:DDF0","CS1:DE0C", 10 | "CS1:0635","CS1:DE2F", 11 | "CS1:DE23", 12 | "CS1:D820","CS1:DD6B","CS1:DE59" 13 | ], 14 | "DriverLoadToolbox.RemapDrivers(State, Memory);": [ 15 | "CS1:E57B" 16 | ], 17 | "DriverLoadToolbox.ResetAllocator(State, Memory);": [ 18 | "CS1:E593" 19 | ] 20 | }, 21 | "InstructionsToReplace": { 22 | "CS2:1E4A":"\/\/ Manual replacement of instruction that can be either jump or NOP (CS2:1E4A)\r\nFailIfValueIsNot(UInt16[cs2, 0x1E4A], 0x9090, 0x7DEB);\r\nswitch (UInt16[cs2, 0x1E4A]) {\r\n case 0x9090: break;\r\n case 0x7DEB: return split_C000_1EC9_C1EC9(0);\r\n}", 23 | "CS2:1E4B":"", 24 | "CS2:0C42":"\/\/ Manual replacement of instruction that can be either ADD or SUB (CS2:0C42)\r\nFailIfValueIsNot(UInt8[cs2, 0x0C43], 0xC7, 0xEF);\r\nswitch (UInt8[cs2, 0x0C43]) {\r\n case 0xC7: DI = Alu.Add16(DI, 0x140);break;\r\n case 0xEF: DI = Alu.Sub16(DI, 0x140);break;\r\n}", 25 | "CS2:0CDC":"\/\/ Manual replacement of instruction that can be either ADD or SUB (CS2:0CDC)\r\nFailIfValueIsNot(UInt8[cs2, 0x0CDD], 0xC7, 0xEF);\r\nswitch (UInt8[cs2, 0x0CDD]) {\r\n case 0xC7: DI = Alu.Add16(DI, 0x140);break;\r\n case 0xEF: DI = Alu.Sub16(DI, 0x140);break;\r\n}", 26 | "CS2:0E12":"\/\/ Manual replacement of instruction that can be either ADD or SUB (CS2:0E12)\r\nFailIfValueIsNot(UInt8[cs2, 0x0E13], 0xC7, 0xEF);\r\nswitch (UInt8[cs2, 0x0E13]) {\r\n case 0xC7: DI = Alu.Add16(DI, 0x140);break;\r\n case 0xEF: DI = Alu.Sub16(DI, 0x140);break;\r\n}" 27 | }, 28 | "GenerateCheckExternalEventsBeforeInstruction":false 29 | } -------------------------------------------------------------------------------- /src/Cryogenic/DuneCdOverrideSupplier.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic; 2 | 3 | using Spice86.Core.CLI; 4 | using Spice86.Core.Emulator.Function; 5 | using Spice86.Core.Emulator.ReverseEngineer; 6 | using Spice86.Core.Emulator.VM; 7 | using Spice86.Shared.Emulator.Memory; 8 | using Spice86.Shared.Interfaces; 9 | 10 | using System.Collections.Generic; 11 | 12 | /// 13 | /// Supplies C# override functions for the Dune CD (DNCDPRG.EXE) DOS program running in Spice86. 14 | /// 15 | /// 16 | /// This class implements to provide custom C# implementations 17 | /// that replace specific routines in the original DOS executable for improved performance, 18 | /// debugging capabilities, and modern platform compatibility. 19 | /// 20 | /// Example command line debug arguments: -e "C:\\Jeux\\ABWFR\\DUNE_CD\\C\\DNCDPRG.EXE" --UseCodeOverride true 21 | /// 22 | public class DuneCdOverrideSupplier : IOverrideSupplier { 23 | 24 | /// 25 | /// Generates function information mappings that tell Spice86 which addresses should be overridden 26 | /// with C# implementations. 27 | /// 28 | /// Service for logging debug information during override execution. 29 | /// Spice86 configuration containing runtime settings. 30 | /// The segment where the DOS program was loaded in memory. 31 | /// The emulated machine instance providing access to CPU, memory, and devices. 32 | /// A dictionary mapping segmented addresses to their function information and override implementations. 33 | public IDictionary GenerateFunctionInformations( 34 | ILoggerService loggerService, 35 | Configuration configuration, 36 | ushort programStartSegment, 37 | Machine machine) { 38 | Dictionary res = new(); 39 | CreateOverrides(loggerService, configuration, programStartSegment, machine, res); 40 | return res; 41 | } 42 | 43 | /// 44 | /// Creates and registers all override functions by instantiating the Overrides class. 45 | /// 46 | /// Service for logging debug information during override execution. 47 | /// Spice86 configuration containing runtime settings. 48 | /// The segment where the DOS program was loaded in memory. 49 | /// The emulated machine instance providing access to CPU, memory, and devices. 50 | /// The dictionary to populate with override function mappings. 51 | private void CreateOverrides(ILoggerService loggerService, Configuration configuration, ushort programStartSegment, 52 | Machine machine, Dictionary res) { 53 | new Overrides.Overrides(res, programStartSegment, machine, loggerService, configuration); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/MT32DriverCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | using System; 3 | 4 | /// 5 | /// Partial class containing Roland MT-32 MIDI driver overrides. 6 | /// 7 | /// 8 | /// Provides C# implementations for the DNMID MIDI driver functions. 9 | /// The MT-32 was a popular MIDI synthesizer module used for music in DOS games. 10 | /// 11 | public partial class Overrides { 12 | 13 | /// 14 | /// Registers MT-32 MIDI driver function overrides with Spice86. 15 | /// 16 | /// 17 | /// Defines function entry points at segment 0xF000 where the MIDI driver is loaded. 18 | /// 19 | public void DefineMT32DriverCodeOverrides() { 20 | DefineFunction(0xF000, 0x100, DNMID_entry_00, true, nameof(DNMID_entry_00)); 21 | DefineFunction(0xF000, 0x100 + 0x17b, DNMID_entry_01, true, nameof(DNMID_entry_01)); 22 | DefineFunction(0xF000, 0x1CB, DNMID_internal_function_00, true, nameof(DNMID_internal_function_00)); 23 | } 24 | 25 | /// 26 | /// MIDI driver entry point 01 - Handles MIDI output operations. 27 | /// 28 | /// Jump target argument (unused in this implementation). 29 | /// A near jump action based on the XOR result. 30 | /// 31 | /// Performs XOR on AX register with itself (clearing it) and conditionally outputs 32 | /// to MIDI port 0x330 based on the result. This likely handles MIDI reset or silence operations. 33 | /// 34 | private Action DNMID_entry_01(int arg) { 35 | var result = Alu16.Xor(AX, AX); 36 | if (result == 0) { 37 | return NearJump(0x04); 38 | } 39 | Cpu.Out16(0x330, AX); 40 | return NearJump(0x0); 41 | } 42 | 43 | /// 44 | /// MIDI driver entry point 00 - Primary entry delegating to entry point 01. 45 | /// 46 | /// Jump target argument (unused in this implementation). 47 | /// Action returned by DNMID_entry_01. 48 | /// 49 | /// Immediately delegates to DNMID_entry_01. The commented-out NearJump(0x17b) 50 | /// suggests this may have had alternative behavior in earlier implementations. 51 | /// 52 | private Action DNMID_entry_00(int arg) { 53 | return DNMID_entry_01(0); 54 | //return NearJump(0x17b); 55 | } 56 | 57 | /// 58 | /// MIDI driver internal function 00 - Placeholder that does nothing. 59 | /// 60 | /// Jump target argument (unused in this implementation). 61 | /// A near return action to exit immediately. 62 | /// 63 | /// This function appears to never be called during normal gameplay. 64 | /// It may be a stub for functionality that was planned but never implemented, 65 | /// or for error handling code that is rarely triggered. 66 | /// 67 | private Action DNMID_internal_function_00(int arg) { 68 | return NearRet(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/TimeCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | using Serilog.Events; 4 | 5 | using Spice86.Core.Emulator.ReverseEngineer; 6 | 7 | /// 8 | /// Partial class containing game time and day/night cycle related overrides. 9 | /// 10 | /// 11 | /// Method names contain underscores to separate segment, offset, and linear addresses 12 | /// for traceability back to the original DOS disassembly. 13 | /// 14 | public partial class Overrides { 15 | 16 | /// 17 | /// Registers time and day/night cycle function overrides with Spice86. 18 | /// 19 | public void DefineTimeCodeOverrides() { 20 | DefineFunction(cs1, 0x1AD1, GetSunlightDay_1000_1AD1_011AD1); 21 | DefineFunction(cs1, 0x1AE0, SetHourOfTheDayToAX_1000_1AE0_011AE0); 22 | } 23 | 24 | /// 25 | /// Gets the current hour of the day (0-15) from the game elapsed time. 26 | /// 27 | /// The hour of the day as a 4-bit value (0x0 to 0xF). 28 | /// 29 | /// Extracts the lower 4 bits of the game elapsed time counter, 30 | /// representing the hour within the current day. 31 | /// 32 | public ushort GetHourOfTheDay() { 33 | return (ushort)(globalsOnDs.Get1138_0002_Word16_GameElapsedTime() & 0xF); 34 | } 35 | 36 | /// 37 | /// Override for CS1:1AD1 - Calculates which day sunlight will be visible (current or next day). 38 | /// 39 | /// Target address for potential jumps (unused in this override). 40 | /// A near return action to exit the function. 41 | /// 42 | /// Puts into AX the day where the sunlight will be seen, either current day or next day. 43 | /// The calculation adds 3 hours to the current time and extracts the day component 44 | /// by right-shifting 4 bits (dividing by 16 hours per day). 45 | /// 46 | public Action GetSunlightDay_1000_1AD1_011AD1(int gotoAddress) { 47 | ushort elapsed = globalsOnDs.Get1138_0002_Word16_GameElapsedTime(); 48 | ushort in3Hours = (ushort)(elapsed + 3); 49 | ushort day = (ushort)(in3Hours >> 4); 50 | AX = day; 51 | return NearRet(); 52 | } 53 | 54 | /// 55 | /// Override for CS1:1AE0 - Sets the AX register to the current hour of the day. 56 | /// 57 | /// Target address for potential jumps (unused in this override). 58 | /// A near return action to exit the function. 59 | /// 60 | /// Retrieves the hour component from game elapsed time and stores it in AX. 61 | /// Logs the full game time and extracted hour value when debug logging is enabled. 62 | /// 63 | public Action SetHourOfTheDayToAX_1000_1AE0_011AE0(int gotoAddress) { 64 | AX = GetHourOfTheDay(); 65 | if (_loggerService.IsEnabled(LogEventLevel.Debug)) { 66 | _loggerService.Debug("setHourOfTheDayToAX:gameTime:{@GameTime}, gameHour:{@GameHour}", globalsOnDs.Get1138_0002_Word16_GameElapsedTime(), AX); 67 | } 68 | 69 | return NearRet(); 70 | } 71 | } -------------------------------------------------------------------------------- /.github/workflows/pr-validation.yml: -------------------------------------------------------------------------------- 1 | name: PR Validation 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - master 8 | - develop 9 | types: [opened, synchronize, reopened] 10 | 11 | permissions: 12 | contents: read 13 | security-events: write 14 | actions: read 15 | 16 | jobs: 17 | build-and-test: 18 | name: Build and Test 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 # Full history for GitVersion 26 | fetch-tags: true 27 | 28 | - name: Setup .NET 29 | uses: actions/setup-dotnet@v4 30 | with: 31 | dotnet-version: '8.0.x' 32 | 33 | - name: Install GitVersion 34 | uses: gittools/actions/gitversion/setup@v3.1.1 35 | with: 36 | versionSpec: '6.0.x' 37 | 38 | - name: Determine Version 39 | id: gitversion 40 | uses: gittools/actions/gitversion/execute@v3.1.1 41 | with: 42 | useConfigFile: true 43 | configFilePath: GitVersion.yml 44 | 45 | - name: Display GitVersion outputs 46 | run: | 47 | echo "SemVer: ${{ steps.gitversion.outputs.semVer }}" 48 | echo "AssemblySemVer: ${{ steps.gitversion.outputs.assemblySemVer }}" 49 | echo "InformationalVersion: ${{ steps.gitversion.outputs.informationalVersion }}" 50 | 51 | - name: Restore dependencies 52 | working-directory: ./src 53 | run: dotnet restore 54 | 55 | - name: Build 56 | working-directory: ./src 57 | run: | 58 | dotnet build --configuration Release --no-restore \ 59 | /p:Version=${{ steps.gitversion.outputs.semVer }} \ 60 | /p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \ 61 | /p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \ 62 | /p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} 63 | 64 | - name: Run tests 65 | working-directory: ./src 66 | run: dotnet test --configuration Release --no-build --verbosity normal 67 | continue-on-error: true # Continue if no tests exist 68 | 69 | codeql-analysis: 70 | name: CodeQL Security Scan 71 | runs-on: ubuntu-latest 72 | permissions: 73 | security-events: write 74 | contents: read 75 | actions: read 76 | 77 | steps: 78 | - name: Checkout code 79 | uses: actions/checkout@v4 80 | 81 | - name: Initialize CodeQL 82 | uses: github/codeql-action/init@v3 83 | with: 84 | languages: csharp 85 | queries: security-and-quality 86 | 87 | - name: Setup .NET 88 | uses: actions/setup-dotnet@v4 89 | with: 90 | dotnet-version: '8.0.x' 91 | 92 | - name: Restore dependencies 93 | working-directory: ./src 94 | run: dotnet restore 95 | 96 | - name: Build for CodeQL 97 | working-directory: ./src 98 | run: dotnet build --configuration Release --no-restore 99 | 100 | - name: Perform CodeQL Analysis 101 | uses: github/codeql-action/analyze@v3 102 | with: 103 | category: "/language:csharp" 104 | -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/DatastructuresCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | using Spice86.Core.Emulator.Memory; 4 | using Spice86.Core.Emulator.ReverseEngineer.DataStructure.Array; 5 | using Spice86.Shared.Emulator.Memory; 6 | 7 | using System; 8 | 9 | /// 10 | /// Partial class containing data structure manipulation overrides. 11 | /// 12 | /// 13 | /// Method names contain underscores to separate segment, offset, and linear addresses 14 | /// for traceability back to the original DOS disassembly. 15 | /// 16 | public partial class Overrides { 17 | /// 18 | /// Registers data structure manipulation function overrides with Spice86. 19 | /// 20 | public void DefineDataStructureOverrides() { 21 | DefineFunction(cs1, 0x98, ConvertIndexTableToPointerTable_1000_0098_010098); 22 | DefineFunction(cs1, 0xC1F4, GetEsSiPointerToUnknown_1000_C1F4_01C1F4); 23 | } 24 | 25 | /// 26 | /// Override for CS1:0098 - Converts an index table to a pointer table by adding an offset to each entry. 27 | /// 28 | /// Target address for potential jumps (unused in this override). 29 | /// A near return action to exit the function. 30 | /// 31 | /// Takes a table of indices at ES:DI and converts them to absolute pointers by adding 32 | /// the DI offset value to each entry. The first word in the table contains the total 33 | /// size in bytes (count = size / 2). This is used for relocating resource tables. 34 | /// 35 | public Action ConvertIndexTableToPointerTable_1000_0098_010098(int gotoAddress) { 36 | uint initialAddress = MemoryUtils.ToPhysicalAddress(ES, DI); 37 | 38 | // wtf 39 | int increment = DI; 40 | int count = UInt16[initialAddress] / 2; 41 | UInt16Array array = new UInt16Array(Memory, initialAddress, count); 42 | for (int i = 0; i < array.Count; i++) { 43 | array[i] = (ushort)(array[i] + increment); 44 | } 45 | 46 | return NearRet(); 47 | } 48 | 49 | /// 50 | /// Override for CS1:C1F4 - Retrieves a pointer from the sprite sheet resource table by index. 51 | /// 52 | /// Target address for potential jumps (unused in this override). 53 | /// A near return action to exit the function. 54 | /// 55 | /// Looks up an entry in the sprite sheet resource pointer table using the AX register as index. 56 | /// Returns the resulting pointer in ES:SI. This is used extensively for accessing sprite 57 | /// graphics data. TODO: Create a proper data structure with more organized accessors 58 | /// when the exact format is better understood. 59 | /// 60 | public Action GetEsSiPointerToUnknown_1000_C1F4_01C1F4(int gotoAddress) { 61 | // TODO: create a proper data structure with more organized accessors when what this is is known better. 62 | int index = AX; 63 | SegmentedAddress baseAddress = globalsOnDs.GetPtr1138_DBB0_Dword32_spriteSheetResourcePointer(); 64 | int resOffset = baseAddress.Offset + UInt16[(uint)(baseAddress.Linear + index * 2)]; 65 | ES = baseAddress.Segment; 66 | SI = (ushort)resOffset; 67 | return NearRet(); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/VideoCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | using Spice86.Core.Emulator.ReverseEngineer; 4 | 5 | /// 6 | /// Partial class containing video playback related overrides for HNM video format. 7 | /// 8 | /// 9 | /// Method names contain underscores to separate segment, offset, and linear addresses 10 | /// for traceability back to the original DOS disassembly. 11 | /// 12 | public partial class Overrides { 13 | /// 14 | /// Registers video playback related function overrides with Spice86. 15 | /// 16 | public void DefineVideoCodeOverrides() { 17 | DefineFunction(cs1, 0xC921, GetHnmResourceFlagNamePtrByIndexAXToBx_1000_C921_01C921); 18 | DefineFunction(cs1, 0xCA59, VideoPlayRelated_1000_CA59_01CA59); 19 | DefineFunction(cs1, 0xCC85, CheckIfHnmComplete_1000_CC85_01CC85); 20 | } 21 | 22 | /// 23 | /// Override for CS1:CC85 - Checks if the current HNM video has finished playing. 24 | /// 25 | /// Target address for potential jumps (unused in this override). 26 | /// A near return action to exit the function. 27 | /// 28 | /// Sets the Zero flag based on the HNM finished flag value (0 or 1 sets ZF, other values clear it). 29 | /// 30 | public Action CheckIfHnmComplete_1000_CC85_01CC85(int gotoAddress) { 31 | int value = globalsOnDs.Get1138_DBE7_Byte8_hnmFinishedFlag(); 32 | _loggerService.Debug("DBE7={@DBE7}", value); 33 | ZeroFlag = value is 0 or 1; 34 | return NearRet(); 35 | } 36 | 37 | /// 38 | /// Override for CS1:C921 - Reads an HNM resource flag name pointer by index and returns it in BX. 39 | /// 40 | /// Target address for potential jumps (unused in this override). 41 | /// A near return action to exit the function. 42 | /// 43 | /// Reads value at DS:(AX*2)+0x33A3 and returns it in BX register. 44 | /// Only executed when a video starts. The offset 0x33A3 points to a lookup table 45 | /// for HNM resource names or flags. 46 | /// 47 | public Action GetHnmResourceFlagNamePtrByIndexAXToBx_1000_C921_01C921(int gotoAddress) { 48 | // Only executed when a video starts 49 | ushort offset = (ushort)(AX * 2 + 0x33A3); 50 | ushort value = UInt16[DS, offset]; 51 | _loggerService.Debug("read33A3WithAxOffset {@Ax} {@Value}", AX, value); 52 | BX = value; 53 | return NearRet(); 54 | } 55 | 56 | /// 57 | /// Override for CS1:CA59 - Copies a video playback related index value between global variables. 58 | /// 59 | /// Target address for potential jumps (unused in this override). 60 | /// A near return action to exit the function. 61 | /// 62 | /// Only executed during video playback. The function appears to have minimal impact 63 | /// on actual video playback, likely serving as state synchronization. 64 | /// 65 | public Action VideoPlayRelated_1000_CA59_01CA59(int gotoAddress) { 66 | // seems to have no impact what so ever is done here. Only executed during videos 67 | ushort value = globalsOnDs.Get1138_CE7A_Word16_VideoPlayRelatedIndex(); 68 | globalsOnDs.Set1138_DC22_Word16_VideoPlayRelatedIndex(value); 69 | _loggerService.Debug("videoPlayRelated value:{@Value}", value); 70 | return NearRet(); 71 | } 72 | } -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/DialoguesCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | /// 4 | /// Partial class containing dialogue system related overrides. 5 | /// 6 | /// 7 | /// Method names contain underscores to separate segment, offset, and linear addresses 8 | /// for traceability back to the original DOS disassembly. 9 | /// 10 | public partial class Overrides { 11 | /// 12 | /// Registers dialogue system function overrides with Spice86. 13 | /// 14 | public void DefineDialoguesCodeOverrides() { 15 | DefineFunction(cs1, 0xA1E8, IncUnknown47A8_1000_A1E8_01A1E8); 16 | DefineFunction(cs1, 0xA8B1, Unknown_1000_A8B1_01A8B1); 17 | DefineFunction(cs1, 0xC85B, InitDialogue_1000_C85B_01C85B); 18 | } 19 | 20 | /// 21 | /// Override for CS1:A1E8 - Increments an unknown dialogue-related counter at DS:47A8. 22 | /// 23 | /// Target address for potential jumps (unused in this override). 24 | /// A near return action to exit the function. 25 | /// 26 | /// Called during dialogues, sometimes before the first text display and sometimes before 27 | /// the last text. The exact purpose of this counter is not yet fully understood. 28 | /// 29 | public Action IncUnknown47A8_1000_A1E8_01A1E8(int gotoAddress) { 30 | // Called in dialogues, sometimes before first text display, sometimes before last text 31 | globalsOnDs.Set1138_47A8_Byte8((byte)(globalsOnDs.Get1138_47A8_Byte8() + 1)); 32 | return NearRet(); 33 | } 34 | 35 | /// 36 | /// Override for CS1:C85B - Initializes dialogue state variables. 37 | /// 38 | /// Target address for potential jumps (unused in this override). 39 | /// A near return action to exit the function. 40 | /// 41 | /// Sets up initial dialogue state by copying a video-related index and configuring 42 | /// the time between face zoom animations (0x1770). This prepares the dialogue system 43 | /// for character conversations with animated portrait zooms. 44 | /// 45 | public Action InitDialogue_1000_C85B_01C85B(int gotoAddress) { 46 | ushort value = this.globalsOnDs.Get1138_CE7A_Word16_VideoPlayRelatedIndex(); 47 | this.globalsOnDs.Set1138_476E_Word16(value); 48 | this.globalsOnDs.Set1138_4772_Word16_TimeBetweenFaceZooms(0x1770); 49 | return NearRet(); 50 | } 51 | 52 | /// 53 | /// Override for CS1:A8B1 - Performs a character conversion operation (possibly hex digit formatting). 54 | /// 55 | /// Target address for potential jumps (unused in this override). 56 | /// A near return action to exit the function. 57 | /// 58 | /// Called when dialogue text changes (at the beginning and during dialogue), and when 59 | /// entering an ornithopter. Converts the lower 4 bits of AL to an ASCII character, 60 | /// likely for hex digit display (0-9, A-F). The resulting value doesn't appear to 61 | /// have a significant effect on gameplay. 62 | /// 63 | public Action Unknown_1000_A8B1_01A8B1(int gotoAddress) { 64 | // Called when a dialogue text changes (beginning and during dialogue), and when entering an orni 65 | // Value does not seem to have any effect 66 | byte value = AL; 67 | value &= 0xF; 68 | value += 0x30; 69 | if (value > 0x39) { 70 | value += 0x7; 71 | } 72 | 73 | AL = value; 74 | return NearRet(); 75 | } 76 | } -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/HnmCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | using Spice86.Core.Emulator.OperatingSystem; 4 | using Spice86.Core.Emulator.OperatingSystem.Structures; 5 | 6 | using System; 7 | 8 | /// 9 | /// Partial class containing HNM video file format handling overrides. 10 | /// 11 | /// 12 | /// 13 | /// HNM is a proprietary video format used by Cryo Interactive for full-motion video sequences. 14 | /// This file provides overrides for reading HNM file data from disk into memory buffers 15 | /// for decoding and playback. 16 | /// 17 | /// 18 | /// Method names contain underscores to separate segment, offset, and linear addresses 19 | /// for traceability back to the original DOS disassembly. 20 | /// 21 | /// 22 | public partial class Overrides { 23 | 24 | /// 25 | /// Registers HNM video file handling function overrides with Spice86. 26 | /// 27 | public void DefineHnmCodeOverrides() { 28 | DefineFunction(cs1, 0xCDBF, HnmReadFromFileHandle_1000_CDBF_01CDBF); 29 | } 30 | 31 | /// 32 | /// Override for CS1:CDBF - Reads data from an open HNM video file into a memory buffer. 33 | /// 34 | /// Target address for potential jumps (unused in this override). 35 | /// A near return action to exit the function. 36 | /// 37 | /// Thrown if the actual bytes read doesn't match the requested amount, indicating an untested code path 38 | /// where the original DOS code would loop to retry the read. 39 | /// 40 | /// 41 | /// 42 | /// Performs a sequential read from the HNM file using the DOS file manager. Updates global 43 | /// state tracking file position, remaining bytes, and buffer pointers. 44 | /// 45 | /// 46 | /// The function reads CX bytes from the file at the current offset into the target buffer, 47 | /// then advances all related pointers and counters. 48 | /// 49 | /// 50 | public Action HnmReadFromFileHandle_1000_CDBF_01CDBF(int gotoAddress) { 51 | DosFileManager dosFileManager = Machine.Dos.FileManager; 52 | ushort fileHandle = globalsOnDs.Get1138_35A6_Word16_IsAnimateMenuUnneeded(); 53 | if (fileHandle == 0) { 54 | return NearRet(); 55 | } 56 | 57 | ushort readLength = CX; 58 | uint offset = globalsOnDs.Get1138_DC04_DWord32_hnmFileOffset(); 59 | uint targetMemory = globalsOnDs.GetPtr1138_DC0C_Dword32_hnmFileReadBufferSegment().Linear; 60 | _loggerService.Debug("Read {@ReadLength} bytes from hnm file handle {@FileHandle} at offset {@Offset}", readLength, fileHandle, offset); 61 | dosFileManager.MoveFilePointerUsingHandle(0, fileHandle, (int)offset); 62 | DosFileOperationResult result = dosFileManager.ReadFileOrDevice(fileHandle, readLength, targetMemory); 63 | uint? actualReadLength = result.Value; 64 | if (actualReadLength != readLength) { 65 | throw this.FailAsUntested("The original code loops here when read bytes from hnm are not as expected."); 66 | } 67 | globalsOnDs.Set1138_DC08_DWord32_hnmFileRemain(globalsOnDs.Get1138_DC08_DWord32_hnmFileRemain() - actualReadLength.Value); 68 | globalsOnDs.Set1138_DC04_DWord32_hnmFileOffset(offset + actualReadLength.Value); 69 | globalsOnDs.Set1138_DC0C_Word16_hnmFileReadBufferSegment((ushort)(globalsOnDs.Get1138_DC0C_Word16_hnmFileReadBufferSegment() + actualReadLength.Value)); 70 | globalsOnDs.Set1138_DC1A_Word16((ushort)(globalsOnDs.Get1138_DC1A_Word16() + actualReadLength.Value)); 71 | return NearRet(); 72 | } 73 | } -------------------------------------------------------------------------------- /src/Cryogenic/Globals/ExtraGlobalsOnDs.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Globals; 2 | 3 | using Cryogenic.Generated; 4 | 5 | using Spice86.Core.Emulator.CPU.Registers; 6 | using Spice86.Core.Emulator.Memory.ReaderWriter; 7 | 8 | /// 9 | /// Extends the generated GlobalsOnDs class with additional manually-defined accessors 10 | /// for game state values that could not be automatically detected during runtime analysis. 11 | /// 12 | /// 13 | /// This class provides access to global variables stored in the DS (Data Segment). 14 | /// Values here represent game state that was not captured by the automated code generation tools, 15 | /// including HNM video playback state and menu system information. 16 | /// 17 | public class ExtraGlobalsOnDs : GlobalsOnDs { 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The memory reader/writer interface for accessing emulated memory. 23 | /// The CPU segment registers used to calculate absolute addresses. 24 | public ExtraGlobalsOnDs(IByteReaderWriter memory, SegmentRegisters segmentRegisters) : base(memory, segmentRegisters) { 25 | } 26 | 27 | /// 28 | /// Gets the current file offset for HNM video file reading. 29 | /// 30 | /// The 32-bit file offset into the currently open HNM video file. 31 | /// 32 | /// HNM files are video format used by the game. This offset tracks the current read position. 33 | /// Located at DS:DC04. 34 | /// 35 | public uint Get1138_DC04_DWord32_hnmFileOffset() { 36 | return UInt32[0xDC04]; 37 | } 38 | 39 | /// 40 | /// Gets the number of bytes remaining to read in the current HNM video file. 41 | /// 42 | /// The 32-bit count of remaining bytes in the HNM file. 43 | /// 44 | /// Used to track progress during HNM video playback. 45 | /// Located at DS:DC08. 46 | /// 47 | public uint Get1138_DC08_DWord32_hnmFileRemain() { 48 | return UInt32[0xDC08]; 49 | } 50 | 51 | /// 52 | /// Gets the current menu type identifier that determines which menu is displayed and what actions are available. 53 | /// 54 | /// The 16-bit menu type identifier. 55 | /// 56 | /// The menu displayed and associated user actions depend on this value. 57 | /// Different menu types include dialogue menus, map views, equipment screens, etc. 58 | /// The value is retrieved indirectly through an offset pointer. 59 | /// 60 | public ushort GetMenuType() { 61 | // menu displayed and associated actions depend on this value 62 | return this.UInt16[Get1138_21DA_Word16_OffsetToMenuType()]; 63 | } 64 | 65 | /// 66 | /// Sets the current file offset for HNM video file reading. 67 | /// 68 | /// The new 32-bit file offset to set. 69 | /// 70 | /// Updates the read position in the currently open HNM video file. 71 | /// Located at DS:DC04. 72 | /// 73 | public void Set1138_DC04_DWord32_hnmFileOffset(uint value) { 74 | UInt32[0xDC04] = value; 75 | } 76 | 77 | /// 78 | /// Sets the number of bytes remaining to read in the current HNM video file. 79 | /// 80 | /// The new 32-bit count of remaining bytes. 81 | /// 82 | /// Used to update progress tracking during HNM video playback. 83 | /// Located at DS:DC08. 84 | /// 85 | public void Set1138_DC08_DWord32_hnmFileRemain(uint value) { 86 | UInt32[0xDC08] = value; 87 | } 88 | } -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Cryogenic Copilot Instructions 2 | - Cryogenic runs the original DNCDPRG.EXE inside Spice86 and replaces key routines with C# overrides; always pass `--UseCodeOverride true` when launching or your code will never run. 3 | - Entry point `src/Cryogenic/Program.cs` only decorates the CLI args (injects `ProvidedAsmHandlersSegment`) before calling `Spice86.Program.RunWithOverrides`. 4 | - `DuneCdOverrideSupplier` just instantiates `Overrides.Overrides`; extend this partial class when adding new behavior so the override table stays centralized. 5 | - Segment fields in `Overrides` (`cs1=0x1000` main code, `cs2/3/4` mapped driver segments, `cs5=0xF000` BIOS/IRQ) are the anchors for every injected address; never change them without auditing all `DefineFunction`/`DoOnTopOfInstruction` calls. 6 | - Override registration happens in `DefineOverrides()` via helpers such as `DefineFunction` (replaces CALL targets) and `DoOnTopOfInstruction` (inject inline hooks); stick to those APIs so Spice86 can patch the emulated program (DNCDPRG.EXE) correctly. 7 | - Override methods live alongside the registration, keep the generated name pattern `{Segment}_{Offset}_{Linear}` (e.g. `SetBackBufferAsActiveFrameBuffer_1000_C085_01C085`) to preserve traceability with the DOS disassembly. 8 | - Return through `NearRet()` or `FarRet()` from `CSharpOverrideHelper` based on the original instruction (near/far RET); wrong choice will corrupt the stack. 9 | - Use the provided `globalsOnDs`/`globalsOnCsSegment0x2538` accessors for game state instead of manual pointer math; these wrappers synchronize with the DS/CS base registers automatically. 10 | - Anything under `src/Cryogenic/Generated/` is produced by Spice86 dump tooling (`dump/CodeGeneratorConfig.json`)—do not hand-edit; add manual accessors in `Globals/Extra*` instead. 11 | - `DriverLoadToolbox` remaps VGA/PCM/MIDI drivers to clean 0xD000/0xE000/0xF000 boundaries and resets the allocator; when touching driver loading ensure both `RemapDrivers` and `ResetAllocator` hooks stay paired at `CS1:E57B`/`CS1:E593`. 12 | - `DriverLoadToolbox.ReadDriverFunctionTable` auto-defines exported driver functions; let it run before introducing manual overrides inside driver segments to avoid duplicate registrations. 13 | - Memory dumps are triggered via `DefineMemoryDumpsMapping()` using `MemoryDataExporter`; copy that pattern if you need extra snapshots while debugging reverse-engineered routines. 14 | - Logging is done through `_loggerService.Debug` with structured templates—keep payloads terse so emulator logs remain readable during long play sessions. 15 | - `Overrides/MenuCode.cs` exposes canonical menu type constants; reuse them instead of magic numbers when routing DNCDPRG.EXE UI logic. 16 | - Video work happens in `Overrides/VgaDriverCode.cs`; reuse helpers like `SetDiFromXYCordsDxBx` and `VgaFunc08FillWithZeroFor64000AtES` to avoid duplicating pixel buffer math. 17 | - For map/dialogue/time systems, follow the existing partial file split (`MapCode`, `DialoguesCode`, etc.) so new overrides stay discoverable by domain. 18 | - Build inside `src`: `dotnet build` for validation, or `dotnet run --Exe C:/path/to/DNCDPRG.EXE --UseCodeOverride true` to execute against the DOS assets; running with audio requires the same `dotnet run` command plus `-a "ADL220 SBP2227"` or similar. 19 | - The expected DNCDPRG checksum is `5F30AEB84D67CF2E053A83C09C2890F010F2E25EE877EBEC58EA15C5B30CFFF9`; mismatches cause Spice86 to refuse to load overrides, so verify the SHA256 before debugging. 20 | - The repo has no automated tests—launch the game and exercise the scenario you touched to validate changes. 21 | - If you hit `FailAsUntested` exceptions, it means the override covers an unobserved code path; either reproduce that path in-game or guard the code before shipping. 22 | - Coordinate updates to `CodeGeneratorConfig.json` with regenerated dumps under `dump/` so address lists and injected snippets stay consistent. 23 | - Extensive documentation is encouraged to explain the purpose and functionality of overrides, aiding future developers in understanding the codebase. 24 | -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/MapCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | 4 | using Spice86.Core.Emulator.Memory; 5 | using Spice86.Core.Emulator.ReverseEngineer; 6 | using Spice86.Shared.Utils; 7 | 8 | using System; 9 | 10 | /// 11 | /// Partial class containing map view and cursor handling overrides. 12 | /// 13 | /// 14 | /// Method names contain underscores to separate segment, offset, and linear addresses 15 | /// for traceability back to the original DOS disassembly. This file handles different 16 | /// map views (flat map, globe, ornithopter map) and their associated click handlers. 17 | /// 18 | public partial class Overrides { 19 | /// Address of the click handler for flat strategic map view. 20 | public const ushort CLICK_HANDLER_FLAT_MAP = 0x1A9E; 21 | 22 | /// Address of the click handler for globe/planetary view. 23 | public const ushort CLICK_HANDLER_GLOBE_MAP = 0x2562; 24 | 25 | /// Address of the click handler for in-game exploration view. 26 | public const ushort CLICK_HANDLER_INGAME = 0x2572; 27 | 28 | /// Address of the click handler for troop movement map. 29 | public const ushort CLICK_HANDLER_MOVE_TROOP_MAP = 0x1AAC; 30 | 31 | /// Address of the click handler for ornithopter flight map. 32 | public const ushort CLICK_HANDLER_ORNI_MAP = 0x1AC8; 33 | 34 | /// 35 | /// Registers map view and cursor handling function overrides with Spice86. 36 | /// 37 | public void DefineMapCodeOverrides() { 38 | DefineFunction(cs1, 0xD95B, SetMapClickHandlerAddressToInGame_1000_D95B_01D95B); 39 | DefineFunction(cs1, 0xD95E, SetMapClickHandlerAddressFromAx_1000_D95E_01D95E); 40 | DefineFunction(cs1, 0xDAA3, InitMapCursorTypeDC58To0_1000_DAA3_01DAA3); 41 | DefineFunction(cs1, 0xDAAA, SetSiToMapCursorTypeDC58_1000_DAAA_01DAAA); 42 | DefineFunction(cs1, 0x5B96, UnknownMemcopy_1000_5B96_015B96); 43 | } 44 | 45 | public Action InitMapCursorTypeDC58To0_1000_DAA3_01DAA3(int gotoAddress) { 46 | // when 0 or any other value, map cursor is disabled for globe / orni. 47 | this.globalsOnDs.Set1138_DC58_Word16_MapCursorType(0); 48 | return NearRet(); 49 | } 50 | 51 | public Action SetMapClickHandlerAddressFromAx_1000_D95E_01D95E(int gotoAddress) { 52 | globalsOnDs.Set1138_2570_Word16_MapClickHandlerAddress(AX); 53 | if (_loggerService.IsEnabled(Serilog.Events.LogEventLevel.Debug)) { 54 | _loggerService.Debug("setMapClickHandlerAddressFromAx: DS:{@Ds}, AX:{@Ax}", ConvertUtils.ToHex16(DS), ConvertUtils.ToHex16(AX)); 55 | } 56 | 57 | return NearRet(); 58 | } 59 | 60 | public Action SetMapClickHandlerAddressToInGame_1000_D95B_01D95B(int gotoAddress) { 61 | // called when starting to fly the orni, exiting maps and when switching from intro to game 62 | // at load time 63 | // See setMapClickHandlerAddressFromAx_1ED_D95E_F82E 64 | AX = CLICK_HANDLER_INGAME; 65 | return SetMapClickHandlerAddressFromAx_1000_D95E_01D95E(0); 66 | } 67 | 68 | public Action SetSiToMapCursorTypeDC58_1000_DAAA_01DAAA(int gotoAddress) { 69 | // when taking an orni: 0x149C, when loading globe or results: 0x2448 70 | ushort value = SI; 71 | if (_loggerService.IsEnabled(Serilog.Events.LogEventLevel.Debug)) { 72 | _loggerService.Debug("setSiToMapCursorTypeDC58: value:{@Value}", ConvertUtils.ToHex16(value)); 73 | } 74 | 75 | this.globalsOnDs.Set1138_DC58_Word16_MapCursorType(value); 76 | return NearRet(); 77 | } 78 | 79 | public Action UnknownMemcopy_1000_5B96_015B96(int gotoAddress) { 80 | // Called on map display / move, data to be copied never seems to change. 81 | uint sourceAddress = MemoryUtils.ToPhysicalAddress(DS, 0x46e3); 82 | uint destinationAddress = MemoryUtils.ToPhysicalAddress(DS, DI); 83 | Memory.MemCopy(sourceAddress, destinationAddress, 4 * 2); 84 | return NearRet(); 85 | } 86 | } -------------------------------------------------------------------------------- /src/Cryogenic/Generated/GlobalsOnCsSegment0x1ED.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Generated; 2 | 3 | using Spice86.Core.Emulator.Memory.ReaderWriter; 4 | using Spice86.Core.Emulator.ReverseEngineer.DataStructure; 5 | using Spice86.Core.Emulator.VM; 6 | 7 | public class GlobalsOnCsSegment0x1ED : MemoryBasedDataStructure { 8 | 9 | public GlobalsOnCsSegment0x1ED(IByteReaderWriter byteReaderWriter, uint baseAddress) : base(byteReaderWriter, baseAddress) { 10 | 11 | } 12 | 13 | // Getters and Setters for address 0x1ED:0xAA/0x1F7A. 14 | // Operation not registered by running code 15 | public int Get01ED_00AA_Word16() { 16 | return UInt16[0xAA]; 17 | } 18 | 19 | // Getters and Setters for address 0x1ED:0xAC/0x1F7C. 20 | // Operation not registered by running code 21 | public int Get01ED_00AC_Word16() { 22 | return UInt16[0xAC]; 23 | } 24 | 25 | // Getters and Setters for address 0x1ED:0xAE/0x1F7E. 26 | // Operation not registered by running code 27 | public int Get01ED_00AE_Word16() { 28 | return UInt16[0xAE]; 29 | } 30 | 31 | // Getters and Setters for address 0x1ED:0xB0/0x1F80. 32 | // Operation not registered by running code 33 | public int Get01ED_00B0_Word16() { 34 | return UInt16[0xB0]; 35 | } 36 | 37 | // Getters and Setters for address 0x1ED:0xA6D3/0xC5A3. 38 | // Operation not registered by running code 39 | public int Get01ED_A6D3_Byte8() { 40 | return UInt8[0xA6D3]; 41 | } 42 | 43 | // Getters and Setters for address 0x1ED:0xC13C/0xE00C. 44 | // Operation not registered by running code 45 | public int Get01ED_C13C_Byte8() { 46 | return UInt8[0xC13C]; 47 | } 48 | 49 | // Getters and Setters for address 0x1ED:0xC21A/0xE0EA. 50 | // Was accessed via the following registers: CS 51 | public int Get01ED_C21A_Byte8_paletteOffset() { 52 | return UInt8[0xC21A]; 53 | } 54 | 55 | // Getters and Setters for address 0x1ED:0xCC94/0xEB64. 56 | // Operation not registered by running code 57 | public int Get01ED_CC94_Word16_blitFunction() { 58 | return UInt16[0xCC94]; 59 | } 60 | 61 | // Getters and Setters for address 0x1ED:0xE8D2/0x107A2. 62 | // Was accessed via the following registers: CS 63 | public int Get01ED_E8D2_Word16_pitTimerValue() { 64 | return UInt16[0xE8D2]; 65 | } 66 | 67 | // Getters and Setters for address 0x1ED:0xE8D4/0x107A4. 68 | // Was accessed via the following registers: CS 69 | public int Get01ED_E8D4_Byte8_pitTimerCounter() { 70 | return UInt8[0xE8D4]; 71 | } 72 | 73 | // Getters and Setters for address 0x1ED:0xED3A/0x10C0A. 74 | // Was accessed via the following registers: CS 75 | public int Get01ED_ED3A_Word16_emsEmmHandle() { 76 | return UInt16[0xED3A]; 77 | } 78 | 79 | // Getters and Setters for address 0x1ED:0xED3E/0x10C0E. 80 | // Was accessed via the following registers: CS 81 | public int Get01ED_ED3E_Word16() { 82 | return UInt16[0xED3E]; 83 | } 84 | 85 | // Getters and Setters for address 0x1ED:0xEE8A/0x10D5A. 86 | // Was accessed via the following registers: CS 87 | public int Get01ED_EE8A_Word16_xmsMemoryBlock() { 88 | return UInt16[0xEE8A]; 89 | } 90 | 91 | // Was accessed via the following registers: CS 92 | public void Set01ED_00AA_Word16(ushort value) { 93 | UInt16[0xAA] = value; 94 | } 95 | 96 | // Was accessed via the following registers: CS 97 | public void Set01ED_00AC_Word16(ushort value) { 98 | UInt16[0xAC] = value; 99 | } 100 | 101 | // Was accessed via the following registers: CS 102 | public void Set01ED_00AE_Word16(ushort value) { 103 | UInt16[0xAE] = value; 104 | } 105 | 106 | // Was accessed via the following registers: CS 107 | public void Set01ED_00B0_Word16(ushort value) { 108 | UInt16[0xB0] = value; 109 | } 110 | 111 | // Was accessed via the following registers: CS 112 | public void Set01ED_A6D3_Byte8(byte value) { 113 | UInt8[0xA6D3] = value; 114 | } 115 | 116 | // Was accessed via the following registers: CS 117 | public void Set01ED_C13C_Byte8(byte value) { 118 | UInt8[0xC13C] = value; 119 | } 120 | 121 | // Was accessed via the following registers: CS 122 | public void Set01ED_C21A_Byte8_paletteOffset(byte value) { 123 | UInt8[0xC21A] = value; 124 | } 125 | 126 | // Was accessed via the following registers: CS 127 | public void Set01ED_CC94_Word16_blitFunction(byte value) { 128 | UInt16[0xCC94] = value; 129 | } 130 | 131 | // Was accessed via the following registers: CS 132 | public void Set01ED_E8D2_Word16_pitTimerValue(ushort value) { 133 | UInt16[0xE8D2] = value; 134 | } 135 | 136 | // Was accessed via the following registers: CS 137 | public void Set01ED_E8D4_Byte8_pitTimerCounter(byte value) { 138 | UInt8[0xE8D4] = value; 139 | } 140 | 141 | // Operation not registered by running code 142 | public void Set01ED_ED3A_Word16_emsEmmHandle(ushort value) { 143 | UInt16[0xED3A] = value; 144 | } 145 | 146 | // Operation not registered by running code 147 | public void Set01ED_ED3E_Word16(ushort value) { 148 | UInt16[0xED3E] = value; 149 | } 150 | 151 | // Operation not registered by running code 152 | public void Set01ED_EE8A_Word16_xmsMemoryBlock(ushort value) { 153 | UInt16[0xEE8A] = value; 154 | } 155 | } -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/MenuCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | /// 4 | /// Partial class containing menu system overrides and constants for the Dune CD game. 5 | /// 6 | /// 7 | /// Method names contain underscores to separate segment, offset, and linear addresses 8 | /// for traceability back to the original DOS disassembly. 9 | /// 10 | public partial class Overrides { 11 | /// Menu type constant for the book/journal interface. 12 | public static readonly int MENU_TYPE_BOOK = 0x2032; 13 | 14 | /// Menu type constant for changing troop occupation assignments. 15 | public static readonly int MENU_TYPE_CHANGE_TROOP_OCCUPATION = 0x216E; 16 | 17 | /// Menu type constant for dialogue/conversation screens. 18 | public static readonly int MENU_TYPE_DIALOGUE = 0x1F7E; 19 | 20 | /// Menu type constant for the exit/quit game confirmation. 21 | public static readonly int MENU_TYPE_EXIT_DUNE = 0x20B6; 22 | 23 | /// Menu type constant for the flat strategic map view. 24 | public static readonly int MENU_TYPE_FLAT_MAP = 0x20F2; 25 | 26 | /// Menu type constant for the globe/planetary view. 27 | public static readonly int MENU_TYPE_GLOBE = 0x204A; 28 | 29 | /// Menu type constant for the load game screen. 30 | public static readonly int MENU_TYPE_LOAD_GAME = 0x208A; 31 | 32 | /// Menu type constant for the mirror/reflection interface. 33 | public static readonly int MENU_TYPE_MIRROR = 0x20C2; 34 | 35 | /// Menu type constant for the audio mixer settings. 36 | public static readonly int MENU_TYPE_MIXER = 0x201A; 37 | 38 | /// Menu type constant for the equipment modification screen. 39 | public static readonly int MENU_TYPE_MODIFY_EQUIPMENT = 0x2012; 40 | 41 | /// Menu type constant for troop movement interface. 42 | public static readonly int MENU_TYPE_MOVE_TROOP = 0x212E; 43 | 44 | /// Menu type constant for ornithopter map view (shares value with MOVE_TROOP). 45 | public static readonly int MENU_TYPE_ORNI_MAP = 0x212E; 46 | 47 | /// Menu type constant for the save game screen. 48 | public static readonly int MENU_TYPE_SAVE_GAME = 0x207A; 49 | 50 | /// Menu type constant for selecting troop occupation. 51 | public static readonly int MENU_TYPE_SELECT_TROOP_OCCUPATION = 0x215A; 52 | 53 | /// Menu type constant for talking to troop members. 54 | public static readonly int MENU_TYPE_TALKING_TO_TROOP = 0x210A; 55 | 56 | /// 57 | /// Menu type constant for the walk-around/exploration mode. 58 | /// 59 | /// 60 | /// This value was found empirically; it may actually be an address to something else 61 | /// rather than a pure menu type identifier. 62 | /// 63 | public static readonly int MENU_TYPE_WALK_AROUND = 0x1F0E; 64 | 65 | /// 66 | /// Registers menu-related function overrides with Spice86. 67 | /// 68 | public void DefineMenuCodeOverrides() { 69 | DefineFunction(cs1, 0xD316, MenuAnimationRelated_1000_D316_01D316); 70 | DefineFunction(cs1, 0xD41B, SetBpToCurrentMenuTypeForScreenAction_1000_D41B_01D41B); 71 | } 72 | 73 | /// 74 | /// Override for CS1:D316 - Handles menu animation state when a menu has a submenu. 75 | /// 76 | /// Target address for potential jumps (unused in this override). 77 | /// A near return action to exit the function. 78 | /// 79 | /// This function is called when a menu has a submenu. It checks if menu animation is needed 80 | /// and sets the transition bitmask accordingly to control visual transitions. 81 | /// 82 | public Action MenuAnimationRelated_1000_D316_01D316(int gotoAddress) { 83 | // called when a menu has a submenu 84 | int isAnimateMenuUneeded = globalsOnDs.Get1138_35A6_Word16_IsAnimateMenuUnneeded(); 85 | byte value2 = globalsOnDs.Get1138_DCE6_Byte8_TransitionBitmask(); 86 | _loggerService.Debug("isAnimateMenuUneeded={@IsAnimateMenuUneeded},DCE6={@DCE6}", isAnimateMenuUneeded, value2); 87 | if (isAnimateMenuUneeded == 0) { 88 | value2 |= 1; 89 | globalsOnDs.Set1138_DCE6_Byte8_TransitionBitmask(value2); 90 | } 91 | 92 | return NearRet(); 93 | } 94 | 95 | /// 96 | /// Override for CS1:D41B - Sets the BP register to the current menu type for screen action handling. 97 | /// 98 | /// Target address for potential jumps (unused in this override). 99 | /// A near return action to exit the function. 100 | /// Thrown if SS != DS, indicating an untested code path. 101 | /// 102 | /// If BP does not point to the correct menu type, the menu will still display but 103 | /// no actions will be clickable on the screen. This override ensures BP is correctly 104 | /// set from the global menu type state. 105 | /// 106 | public Action SetBpToCurrentMenuTypeForScreenAction_1000_D41B_01D41B(int gotoAddress) { 107 | // If BP does not point to a correct menu type, menu is still OK but no action is clickable on the screen 108 | if (SS != DS) { 109 | throw FailAsUntested( 110 | "Was implemented considering base address is DS since I couldnt see a case where DS!=SS for this method, but you found one :)"); 111 | } 112 | 113 | ushort value = globalsOnDs.GetMenuType(); 114 | BP = value; 115 | return NearRet(); 116 | } 117 | } -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/DisplayCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | using Spice86.Core.Emulator.CPU; 4 | using Spice86.Core.Emulator.CPU.Registers; 5 | 6 | /// 7 | /// Partial class containing display and framebuffer management overrides. 8 | /// 9 | /// 10 | /// Method names contain underscores to separate segment, offset, and linear addresses 11 | /// for traceability back to the original DOS disassembly. This file handles video buffer 12 | /// switching, clearing, font selection, and character coordinate management. 13 | /// 14 | public partial class Overrides { 15 | /// 16 | /// Registers display and framebuffer management function overrides with Spice86. 17 | /// 18 | public void DefineDisplayCodeOverrides() { 19 | //DefineFunction(cs1, 0x0579, ClearGlobalVgaOffset_1000_0579_010579); 20 | DefineFunction(cs1, 0x98F5, ClearUnknownValuesAndAX_1000_98F5_0198F5); 21 | DefineFunction(cs1, 0x9901, Set479ETo0_1000_9901_019901); 22 | DefineFunction(cs1, 0xC07C, SetFrontBufferAsActiveFrameBuffer_1000_C07C_01C07C); 23 | DefineFunction(cs1, 0xC085, SetBackBufferAsActiveFrameBuffer_1000_C085_01C085); 24 | DefineFunction(cs1, 0xC08E, SetTextBufferAsActiveFrameBuffer_1000_C08E_01C08E); 25 | DefineFunction(cs1, 0xC0AD, ClearCurrentVideoBuffer_1000_C0AD_01C0AD); 26 | DefineFunction(cs1, 0xD05F, GetCharacterCoordsXY_1000_D05F_01D05F); 27 | DefineFunction(cs1, 0xD068, SetFontToIntro_1000_D068_01D068); 28 | DefineFunction(cs1, 0xD075, SetFontToMenu_1000_D075_01D075); 29 | DefineFunction(cs1, 0xD082, SetFontToBook_1ED_D082_EF52); 30 | DefineFunction(cs1, 0xE270, PushAll_1000_E270_01E270); 31 | DefineFunction(cs1, 0xE283, PopAll_1000_E283_01E283); 32 | } 33 | 34 | public Action ClearCurrentVideoBuffer_1000_C0AD_01C0AD(int gotoAddress) { 35 | State.ES = globalsOnDs.Get1138_DBDA_Word16_framebufferActive(); 36 | VgaFunc08FillWithZeroFor64000AtES_334B_0118_0335C8(0); 37 | return NearRet(); 38 | } 39 | 40 | public Action ClearUnknownValuesAndAX_1000_98F5_0198F5(int gotoAddress) { 41 | // Called after screen change (video, room, dialogue, map ...). 42 | // When set to 255, cannot enter orni and enter palace instead 43 | _loggerService.Debug("Before: 1C06:{@1C06}, 1BF8:{@1BF8}, 1BEA:{@1BEA}", globalsOnDs.Get1138_1C06_Word16(), globalsOnDs.Get1138_1BF8_Word16(), globalsOnDs.Get1138_1BEA_Word16()); 44 | globalsOnDs.Set1138_1C06_Word16(0); 45 | 46 | // 128 after end of dialogue if character is in the room 47 | globalsOnDs.Set1138_1BF8_Word16(0); 48 | 49 | // 128 after end of dialogue 50 | globalsOnDs.Set1138_1BEA_Word16(0); 51 | 52 | // If not done, book videos will show a character on screen instead 53 | AX = 0; 54 | return NearRet(); 55 | } 56 | 57 | // sets the gfx offset to 0 58 | public Action ClearGlobalVgaOffset_1000_0579_010579(int gotoAddress) { 59 | _loggerService.Debug("Clearing VGA offset"); 60 | CheckVtableContainsExpected((int)SegmentRegisterIndex.DsIndex, 0x3939, cs2, 0x163); 61 | AX = 0; 62 | VgaFunc33UpdateVgaOffset01A3FromLineNumberAsAx_334B_0163_033613(0); 63 | return NearRet(); 64 | } 65 | 66 | public Action GetCharacterCoordsXY_1000_D05F_01D05F(int gotoAddress) { 67 | ushort x = globalsOnDs.Get1138_D82C_Word16_CharacterXCoord(); 68 | ushort y = globalsOnDs.Get1138_D82E_Word16_CharacterYCoord(); 69 | DX = x; 70 | BX = y; 71 | _loggerService.Debug("getCharacterCoordsXY x:{@X} y:{@Y}", DX, BX); 72 | return NearRet(); 73 | } 74 | 75 | public Action PopAll_1000_E283_01E283(int gotoAddress) { 76 | _loggerService.Debug("popAll"); 77 | 78 | // Called in most changes related to display like scene change, displaying map, clicking on map, clicking on 79 | // characters ... 80 | // XCHG AX <-> Stack[0x0C] (or 0x0E if done before the pop) 81 | ushort ax = Stack.Pop16(); 82 | ushort stackPeek = Stack.Peek16(0x0C); 83 | Stack.Poke16(0x0C, ax); 84 | AX = stackPeek; 85 | 86 | // Regular pops 87 | BP = Stack.Pop16(); 88 | DI = Stack.Pop16(); 89 | SI = Stack.Pop16(); 90 | DX = Stack.Pop16(); 91 | CX = Stack.Pop16(); 92 | BX = Stack.Pop16(); 93 | return NearRet(); 94 | } 95 | 96 | public Action PushAll_1000_E270_01E270(int gotoAddress) { 97 | _loggerService.Debug("pushAll"); 98 | Stack.Push16(BX); 99 | Stack.Push16(CX); 100 | Stack.Push16(DX); 101 | Stack.Push16(SI); 102 | Stack.Push16(DI); 103 | Stack.Push16(BP); 104 | ushort stackTop = Stack.Peek16(0); 105 | 106 | // XCHG AX <-> Stack[0x0C] 107 | ushort stackPeek = Stack.Peek16(0x0C); 108 | Stack.Poke16(0x0C, AX); 109 | 110 | // In the original assembly code, AX seems modified but it's not the case as it's restored to its original value 111 | // later. 112 | Stack.Push16(stackPeek); 113 | BP = stackTop; 114 | return NearRet(); 115 | } 116 | 117 | public Action Set479ETo0_1000_9901_019901(int gotoAddress) { 118 | // Called in intro when skipping scenes and in the book when clicking subjects or quitting. 119 | // Screen in intro becomes garbled when setting something else than 0. 120 | globalsOnDs.Set1138_479E_Word16(0); 121 | return NearRet(); 122 | } 123 | 124 | public Action SetBackBufferAsActiveFrameBuffer_1000_C085_01C085(int gotoAddress) { 125 | ushort value = globalsOnDs.Get1138_DC32_Word16_framebufferBack(); 126 | return SetVideoBuffer(value, "setDialogueVideoBufferSegmentDC32"); 127 | } 128 | 129 | // book fonts related 130 | public Action SetFontToBook_1ED_D082_EF52(int gotoAddress) { 131 | globalsOnDs.Set1138_2518_Word16_FontRelated(0xD0FF); 132 | globalsOnDs.Set1138_47A0_Word16_FontRelated(0xCEEC); 133 | return NearRet(); 134 | } 135 | 136 | // intro and map fonts 137 | public Action SetFontToIntro_1000_D068_01D068(int gotoAddress) { 138 | globalsOnDs.Set1138_2518_Word16_FontRelated(0xD096); 139 | globalsOnDs.Set1138_47A0_Word16_FontRelated(0xCEEC); 140 | return NearRet(); 141 | } 142 | 143 | // menu fonts related 144 | public Action SetFontToMenu_1000_D075_01D075(int gotoAddress) { 145 | globalsOnDs.Set1138_2518_Word16_FontRelated(0xD12F); 146 | globalsOnDs.Set1138_47A0_Word16_FontRelated(0xCF6C); 147 | return NearRet(); 148 | } 149 | 150 | public Action SetTextBufferAsActiveFrameBuffer_1000_C08E_01C08E(int gotoAddress) { 151 | ushort value = globalsOnDs.Get1138_DBD8_Word16_screenBuffer(); 152 | return SetVideoBuffer(value, "setTextVideoBufferSegmentDBD8"); 153 | } 154 | 155 | public Action SetFrontBufferAsActiveFrameBuffer_1000_C07C_01C07C(int gotoAddress) { 156 | ushort value = globalsOnDs.Get1138_DBD6_Word16_framebufferFront(); 157 | return SetVideoBuffer(value, "setVideoBufferSegmentDBD6"); 158 | } 159 | 160 | private Action SetVideoBuffer(ushort value, string functionName) { 161 | ushort oldValue = globalsOnDs.Get1138_DBDA_Word16_framebufferActive(); 162 | if (value != oldValue) { 163 | globalsOnDs.Set1138_DBDA_Word16_framebufferActive(value); 164 | _loggerService.Debug("{@FunctionName} value:{@Value}, oldValue:{@OldValue}", functionName, value, oldValue); 165 | } 166 | 167 | return NearRet(); 168 | } 169 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | src/Cryogenic/Properties/ 8 | *.rsuser 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Mono auto generated files 18 | mono_crash.* 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # StyleCop 66 | StyleCopReport.xml 67 | 68 | # Files built by Visual Studio 69 | *_i.c 70 | *_p.c 71 | *_h.h 72 | *.ilk 73 | *.meta 74 | *.obj 75 | *.iobj 76 | *.pch 77 | *.pdb 78 | *.ipdb 79 | *.pgc 80 | *.pgd 81 | *.rsp 82 | *.sbr 83 | *.tlb 84 | *.tli 85 | *.tlh 86 | *.tmp 87 | *.tmp_proj 88 | *_wpftmp.csproj 89 | *.log 90 | *.vspscc 91 | *.vssscc 92 | .builds 93 | *.pidb 94 | *.svclog 95 | *.scc 96 | 97 | # Chutzpah Test files 98 | _Chutzpah* 99 | 100 | # Visual C++ cache files 101 | ipch/ 102 | *.aps 103 | *.ncb 104 | *.opendb 105 | *.opensdf 106 | *.sdf 107 | *.cachefile 108 | *.VC.db 109 | *.VC.VC.opendb 110 | 111 | # Visual Studio profiler 112 | *.psess 113 | *.vsp 114 | *.vspx 115 | *.sap 116 | 117 | # Visual Studio Trace Files 118 | *.e2e 119 | 120 | # TFS 2012 Local Workspace 121 | $tf/ 122 | 123 | # Guidance Automation Toolkit 124 | *.gpState 125 | 126 | # ReSharper is a .NET coding add-in 127 | _ReSharper*/ 128 | *.[Rr]e[Ss]harper 129 | *.DotSettings.user 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Visual Studio code coverage results 142 | *.coverage 143 | *.coveragexml 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # NuGet Symbol Packages 189 | *.snupkg 190 | # The packages folder can be ignored because of Package Restore 191 | **/[Pp]ackages/* 192 | # except build/, which is used as an MSBuild target. 193 | !**/[Pp]ackages/build/ 194 | # Uncomment if necessary however generally it will be regenerated when needed 195 | #!**/[Pp]ackages/repositories.config 196 | # NuGet v3's project.json files produces more ignorable files 197 | *.nuget.props 198 | *.nuget.targets 199 | 200 | # Microsoft Azure Build Output 201 | csx/ 202 | *.build.csdef 203 | 204 | # Microsoft Azure Emulator 205 | ecf/ 206 | rcf/ 207 | 208 | # Windows Store app package directories and files 209 | AppPackages/ 210 | BundleArtifacts/ 211 | Package.StoreAssociation.xml 212 | _pkginfo.txt 213 | *.appx 214 | *.appxbundle 215 | *.appxupload 216 | 217 | # Visual Studio cache files 218 | # files ending in .cache can be ignored 219 | *.[Cc]ache 220 | # but keep track of directories ending in .cache 221 | !?*.[Cc]ache/ 222 | 223 | # Others 224 | ClientBin/ 225 | ~$* 226 | *~ 227 | *.dbmdl 228 | *.dbproj.schemaview 229 | *.jfm 230 | *.pfx 231 | *.publishsettings 232 | orleans.codegen.cs 233 | 234 | # Including strong name files can present a security risk 235 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 236 | #*.snk 237 | 238 | # Since there are multiple workflows, uncomment next line to ignore bower_components 239 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 240 | #bower_components/ 241 | 242 | # RIA/Silverlight projects 243 | Generated_Code/ 244 | 245 | # Backup & report files from converting an old project file 246 | # to a newer Visual Studio version. Backup files are not needed, 247 | # because we have git ;-) 248 | _UpgradeReport_Files/ 249 | Backup*/ 250 | UpgradeLog*.XML 251 | UpgradeLog*.htm 252 | ServiceFabricBackup/ 253 | *.rptproj.bak 254 | 255 | # SQL Server files 256 | *.mdf 257 | *.ldf 258 | *.ndf 259 | 260 | # Business Intelligence projects 261 | *.rdl.data 262 | *.bim.layout 263 | *.bim_*.settings 264 | *.rptproj.rsuser 265 | *- [Bb]ackup.rdl 266 | *- [Bb]ackup ([0-9]).rdl 267 | *- [Bb]ackup ([0-9][0-9]).rdl 268 | 269 | # Microsoft Fakes 270 | FakesAssemblies/ 271 | 272 | # GhostDoc plugin setting file 273 | *.GhostDoc.xml 274 | 275 | # Node.js Tools for Visual Studio 276 | .ntvs_analysis.dat 277 | node_modules/ 278 | 279 | # Visual Studio 6 build log 280 | *.plg 281 | 282 | # Visual Studio 6 workspace options file 283 | *.opt 284 | 285 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 286 | *.vbw 287 | 288 | # Visual Studio LightSwitch build output 289 | **/*.HTMLClient/GeneratedArtifacts 290 | **/*.DesktopClient/GeneratedArtifacts 291 | **/*.DesktopClient/ModelManifest.xml 292 | **/*.Server/GeneratedArtifacts 293 | **/*.Server/ModelManifest.xml 294 | _Pvt_Extensions 295 | 296 | # Paket dependency manager 297 | .paket/paket.exe 298 | paket-files/ 299 | 300 | # FAKE - F# Make 301 | .fake/ 302 | 303 | # CodeRush personal settings 304 | .cr/personal 305 | 306 | # Python Tools for Visual Studio (PTVS) 307 | __pycache__/ 308 | *.pyc 309 | 310 | # Cake - Uncomment if you are using it 311 | # tools/** 312 | # !tools/packages.config 313 | 314 | # Tabs Studio 315 | *.tss 316 | 317 | # Telerik's JustMock configuration file 318 | *.jmconfig 319 | 320 | # BizTalk build output 321 | *.btp.cs 322 | *.btm.cs 323 | *.odx.cs 324 | *.xsd.cs 325 | 326 | # OpenCover UI analysis results 327 | OpenCover/ 328 | 329 | # Azure Stream Analytics local run output 330 | ASALocalRun/ 331 | 332 | # MSBuild Binary and Structured Log 333 | *.binlog 334 | 335 | # NVidia Nsight GPU debugger configuration file 336 | *.nvuser 337 | 338 | # MFractors (Xamarin productivity tool) working folder 339 | .mfractor/ 340 | 341 | # Local History for Visual Studio 342 | .localhistory/ 343 | 344 | # BeatPulse healthcheck temp database 345 | healthchecksdb 346 | 347 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 348 | MigrationBackup/ 349 | 350 | # Ionide (cross platform F# VS Code tools) working folder 351 | .ionide/ 352 | 353 | #Rider 354 | .idea/ 355 | 356 | #Intellij 357 | *.iml 358 | 359 | #GDB 360 | .gdb_history -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/UnknownCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | using System; 4 | 5 | /// 6 | /// Partial class containing overrides for functions whose exact purpose is not yet fully understood. 7 | /// 8 | /// 9 | /// 10 | /// This file contains overrides for various game functions that have been reverse-engineered 11 | /// but whose complete purpose or significance is still being researched. Function names 12 | /// describe observable behavior or affected memory locations rather than semantic purpose. 13 | /// 14 | /// 15 | /// Method names contain underscores to separate segment, offset, and linear addresses 16 | /// for traceability back to the original DOS disassembly. 17 | /// 18 | /// 19 | public partial class Overrides { 20 | 21 | /// 22 | /// Registers function overrides for routines of uncertain or partially understood purpose. 23 | /// 24 | public void DefineUnknownCodeOverrides() { 25 | DefineFunction(cs1, 0x0F66, NoOp_1000_0F66_10F66); 26 | DefineFunction(cs1, 0x5B99, MemCopy8BytesDsSIToDsDi_1000_5B99_15B99); 27 | DefineFunction(cs1, 0x5BA0, MemCopy8BytesFrom1470ToD83C_1000_5BA0_15BA0); 28 | DefineFunction(cs1, 0x5BA8, MemCopy8Bytes_1000_5BA8_15BA8); 29 | DefineFunction(cs1, 0xAE2F, CheckPcmEnabled_1000_AE2F_1AE2F); 30 | DefineFunction(cs1, 0xAEC6, IsUnknownDBC80x100And2943BitmaskNonZero_1000_AEC6_1AEC6); 31 | DefineFunction(cs1, 0xD443, "DispatcherJumpsToBX"); 32 | DefineFunction(cs1, 0xD454, "DispatcherHelperDeterminesWhereToJump"); 33 | DefineFunction(cs1, 0x4AC4, SetUnknown11CATo0_1000_4AC4_14AC4); 34 | DefineFunction(cs1, 0x4ACA, SetUnknown11CATo1_1000_4ACA_14ACA); 35 | DefineFunction(cs1, 0xABCC, IsUnknownDC2BZero_1000_ABCC_1ABCC); 36 | DefineFunction(cs1, 0xAE28, IsUnknownDBC80x100_1000_AE28_1AE28); 37 | DefineFunction(cs1, 0xB2BE, SetUnknown2788To0_1000_B2BE_1B2BE); 38 | DefineFunction(cs1, 0xD917, NoOp_1000_D917_01D917); 39 | DefineFunction(cs1, 0xDB44, ShlDXAndCXByAX_1000_DB44_01DB44); 40 | DefineFunction(cs1, 0xE26F, NoOp_1000_E26F_01E26F); 41 | DefineFunction(cs1, 0xE75B, UnknownStructCreation_1000_E75B_01E75B); 42 | DefineFunction(cs1, 0xE851, CheckNextFreeMemorySegment39B9_1000_E851_01E851); 43 | DefineFunction(cs1, 0x3AE9, Fill47F8WithFF_1000_3AE9_013AE9); 44 | DefineFunction(cs1, 0xB2B9, Inc2788_1000_B2B9_01B2B9); 45 | DefineFunction(cs1, 0xDE4E, SetCEE8To0_1000_DE4E_01DE4E); 46 | } 47 | 48 | public Action CheckPcmEnabled_1000_AE2F_1AE2F(int gotoAddress) { 49 | ushort value = globalsOnDs.Get1138_DBC8_Word16(); 50 | Alu16.And(value, 1); 51 | return NearRet(); 52 | } 53 | 54 | public Action SetCEE8To0_1000_DE4E_01DE4E(int gotoAddress) { 55 | // Called when skipping some intro screens 56 | globalsOnDs.Set1138_CEE8_Byte8_keyHit(0); 57 | return NearRet(); 58 | } 59 | 60 | public Action Inc2788_1000_B2B9_01B2B9(int gotoAddress) { 61 | // Called when looking at miror or at book, value seems to be always 0 at call time. 62 | byte value = globalsOnDs.Get1138_2788_Byte8(); 63 | globalsOnDs.Set1138_2788_Byte8((byte)(value + 1)); 64 | return NearRet(); 65 | } 66 | 67 | public Action Fill47F8WithFF_1000_3AE9_013AE9(int gotoAddress) { 68 | // Called when leaving or entering a scene. Does not seem to have any effect on game whatever the value is in this 69 | // area. 70 | uint address = MemoryUtils.ToPhysicalAddress(DS, 0x47F8); 71 | Memory.Memset8(address, 0xFF, 2 * 0x2E); 72 | return NearRet(); 73 | } 74 | 75 | public Action NoOp_1000_0F66_10F66(int gotoAddress) { 76 | // called before intro 77 | return NearRet(); 78 | } 79 | 80 | public Action SetUnknown11CATo0_1000_4AC4_14AC4(int gotoAddress) { 81 | // triggered when orni lifts off and lands 82 | globalsOnDs.Set1138_11CA_Byte8(0); 83 | return NearRet(); 84 | } 85 | 86 | public Action SetUnknown11CATo1_1000_4ACA_14ACA(int gotoAddress) { 87 | // triggered on orni map, flat map and discussion when displaying new dialogue on click and play screens and in 88 | // visions 89 | globalsOnDs.Set1138_11CA_Byte8(1); 90 | return NearRet(); 91 | } 92 | 93 | public Action MemCopy8BytesDsSIToDsDi_1000_5B99_15B99(int gotoAddress) { 94 | // Called on scene change (example dialogue, room change) 95 | ES = DS; 96 | uint sourceAddress = MemoryUtils.ToPhysicalAddress(DS, SI); 97 | uint destinationAddress = MemoryUtils.ToPhysicalAddress(ES, DI); 98 | 99 | // Moves 4 words from source to dest, so 8 bytes 100 | Memory.MemCopy(sourceAddress, destinationAddress, 8); 101 | SI = (ushort)(SI + 8); 102 | DI = (ushort)(DI + 8); 103 | return NearRet(); 104 | } 105 | 106 | public Action MemCopy8BytesFrom1470ToD83C_1000_5BA0_15BA0(int gotoAddress) { 107 | // Called on room change 108 | SI = 0x1470; 109 | DI = 0xD83C; 110 | return MemCopy8BytesDsSIToDsDi_1000_5B99_15B99(0); 111 | } 112 | 113 | public Action MemCopy8Bytes_1000_5BA8_15BA8(int gotoAddress) { 114 | // Called on dialogue, screen change, intro demo and globe 115 | SI = 0x1470; 116 | DI = 0xD834; 117 | return MemCopy8BytesDsSIToDsDi_1000_5B99_15B99(0); 118 | } 119 | 120 | public Action IsUnknownDBC80x100And2943BitmaskNonZero_1000_AEC6_1AEC6(int gotoAddress) { 121 | // Called continuously 122 | int value = globalsOnDs.Get1138_2943_Byte8_cmdArgsMemory(); 123 | bool res = true; 124 | if ((value & 0x10) == 0) { 125 | IsUnknownDBC80x100_1000_AE28_1AE28(0); 126 | if (!ZeroFlag) { 127 | res = false; 128 | } 129 | } 130 | 131 | _loggerService.Debug("2943={@Value},res={@Res}", value, res); 132 | CarryFlag = res; 133 | if (res) { 134 | throw FailAsUntested($"isUnknownDBC80x100And2943BitmaskNonZero was called with a true result. value: {value}"); 135 | } 136 | 137 | return NearRet(); 138 | } 139 | 140 | public Action IsUnknownDC2BZero_1000_ABCC_1ABCC(int gotoAddress) { 141 | ZeroFlag = globalsOnDs.Get1138_DC2B_Byte8() == 0; 142 | return NearRet(); 143 | } 144 | 145 | public Action IsUnknownDBC80x100_1000_AE28_1AE28(int gotoAddress) { 146 | // Called constantly in game and at transitions during video 147 | ushort value = globalsOnDs.Get1138_DBC8_Word16(); 148 | 149 | // Seems that this function is called with only JZ / JNZ, but not sure so call the real thing 150 | Alu16.Sub(value, 0x100); 151 | return NearRet(); 152 | } 153 | 154 | public Action SetUnknown2788To0_1000_B2BE_1B2BE(int gotoAddress) { 155 | // Called when game is loaded or when landing with orni. Other values do not seem to have any effect. 156 | globalsOnDs.Set1138_2788_Byte8(0); 157 | return NearRet(); 158 | } 159 | 160 | public Action NoOp_1000_D917_01D917(int gotoAddress) { 161 | // called after first globe display 162 | return NearRet(); 163 | } 164 | 165 | public Action ShlDXAndCXByAX_1000_DB44_01DB44(int gotoAddress) { 166 | // Called before setting mouse parameters 167 | ushort shiftCount = AX; 168 | CX = (ushort)(CX << shiftCount); 169 | DX = (ushort)(DX << shiftCount); 170 | return NearRet(); 171 | } 172 | 173 | public Action NoOp_1000_E26F_01E26F(int gotoAddress) { 174 | // called after or during most screen transitions 175 | return NearRet(); 176 | } 177 | 178 | public Action UnknownStructCreation_1000_E75B_01E75B(int gotoAddress) { 179 | uint destinationAddress = MemoryUtils.ToPhysicalAddress(ES, DI); 180 | Memory.UInt16[destinationAddress] = AX; 181 | Memory.UInt8[destinationAddress + 2] = DL; 182 | uint sourceAddress = MemoryUtils.ToPhysicalAddress(DS, SI) + 0x10; 183 | Memory.MemCopy(sourceAddress, destinationAddress + 3, 3); 184 | Memory.MemCopy(sourceAddress + 4, destinationAddress + 6, 4); 185 | 186 | // 10 bytes copied in total 187 | DI = (ushort)(DI + 10); 188 | return NearRet(); 189 | } 190 | 191 | public Action CheckNextFreeMemorySegment39B9_1000_E851_01E851(int gotoAddress) { 192 | // Game stops if carry flag is unset 193 | ushort value = globalsOnDs.Get1138_39B9_Word16_allocatorNextFreeSegment(); 194 | value += 0x2F13; 195 | Alu16.Sub(value, globalsOnDs.Get1138_CE68_Word16_allocatorLastFreeSegment()); 196 | return NearRet(); 197 | } 198 | } -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_size = 4 11 | indent_style = space 12 | tab_width = 4 13 | 14 | # New line preferences 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET Coding Conventions #### 19 | 20 | # Organize usings 21 | dotnet_separate_import_directive_groups = true 22 | dotnet_sort_system_directives_first = false 23 | file_header_template = unset 24 | 25 | # this. and Me. preferences 26 | dotnet_style_qualification_for_event = false 27 | dotnet_style_qualification_for_field = false 28 | dotnet_style_qualification_for_method = false 29 | dotnet_style_qualification_for_property = false 30 | 31 | # Language keywords vs BCL types preferences 32 | dotnet_style_predefined_type_for_locals_parameters_members = true 33 | dotnet_style_predefined_type_for_member_access = true 34 | 35 | # Parentheses preferences 36 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity 37 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity 38 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary 39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity 40 | 41 | # Modifier preferences 42 | dotnet_style_require_accessibility_modifiers = for_non_interface_members 43 | 44 | # Expression-level preferences 45 | dotnet_style_coalesce_expression = true 46 | dotnet_style_collection_initializer = true 47 | dotnet_style_explicit_tuple_names = true 48 | dotnet_style_namespace_match_folder = true 49 | dotnet_style_null_propagation = true 50 | dotnet_style_object_initializer = true 51 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 52 | dotnet_style_prefer_auto_properties = true 53 | dotnet_style_prefer_compound_assignment = true 54 | dotnet_style_prefer_conditional_expression_over_assignment = true 55 | dotnet_style_prefer_conditional_expression_over_return = true 56 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 57 | dotnet_style_prefer_inferred_tuple_names = true 58 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning 59 | dotnet_style_prefer_simplified_boolean_expressions = true 60 | dotnet_style_prefer_simplified_interpolation = true 61 | 62 | # Field preferences 63 | dotnet_style_readonly_field = true 64 | 65 | # Parameter preferences 66 | dotnet_code_quality_unused_parameters = all 67 | 68 | # Suppression preferences 69 | dotnet_remove_unnecessary_suppression_exclusions = none 70 | 71 | # New line preferences 72 | dotnet_style_allow_multiple_blank_lines_experimental = true 73 | dotnet_style_allow_statement_immediately_after_block_experimental = true 74 | 75 | #### C# Coding Conventions #### 76 | 77 | # var preferences 78 | csharp_style_var_elsewhere = false:warning 79 | csharp_style_var_for_built_in_types = false 80 | csharp_style_var_when_type_is_apparent = true:suggestion 81 | 82 | # Expression-bodied members 83 | csharp_style_expression_bodied_accessors = true 84 | csharp_style_expression_bodied_constructors = false 85 | csharp_style_expression_bodied_indexers = true 86 | csharp_style_expression_bodied_lambdas = true 87 | csharp_style_expression_bodied_local_functions = false 88 | csharp_style_expression_bodied_methods = false 89 | csharp_style_expression_bodied_operators = false 90 | csharp_style_expression_bodied_properties = true 91 | 92 | # Pattern matching preferences 93 | csharp_style_pattern_matching_over_as_with_null_check = true 94 | csharp_style_pattern_matching_over_is_with_cast_check = true 95 | csharp_style_prefer_not_pattern = true 96 | csharp_style_prefer_pattern_matching = true:suggestion 97 | csharp_style_prefer_switch_expression = true 98 | 99 | # Null-checking preferences 100 | csharp_style_conditional_delegate_call = true 101 | 102 | # Modifier preferences 103 | csharp_prefer_static_local_function = true 104 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async 105 | 106 | # Code-block preferences 107 | csharp_prefer_braces = true 108 | csharp_prefer_simple_using_statement = true 109 | csharp_style_namespace_declarations = file_scoped:suggestion 110 | 111 | # Expression-level preferences 112 | csharp_prefer_simple_default_expression = true 113 | csharp_style_deconstructed_variable_declaration = true 114 | csharp_style_implicit_object_creation_when_type_is_apparent = true 115 | csharp_style_inlined_variable_declaration = true 116 | csharp_style_pattern_local_over_anonymous_function = true 117 | csharp_style_prefer_index_operator = true 118 | csharp_style_prefer_null_check_over_type_check = true 119 | csharp_style_prefer_range_operator = true 120 | csharp_style_throw_expression = true 121 | csharp_style_unused_value_assignment_preference = discard_variable 122 | csharp_style_unused_value_expression_statement_preference = discard_variable 123 | 124 | # 'using' directive preferences 125 | csharp_using_directive_placement = inside_namespace:error 126 | 127 | # New line preferences 128 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true 129 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true 130 | csharp_style_allow_embedded_statements_on_same_line_experimental = true 131 | 132 | #### C# Formatting Rules #### 133 | 134 | # New line preferences 135 | csharp_new_line_before_open_brace = none 136 | csharp_new_line_before_catch = false 137 | csharp_new_line_before_else = false 138 | csharp_new_line_before_finally = false 139 | csharp_new_line_before_members_in_anonymous_types = false 140 | csharp_new_line_before_members_in_object_initializers = false 141 | csharp_new_line_between_query_expression_clauses = true 142 | 143 | # Indentation preferences 144 | csharp_indent_block_contents = true 145 | csharp_indent_braces = false 146 | csharp_indent_case_contents = true 147 | csharp_indent_case_contents_when_block = true 148 | csharp_indent_labels = one_less_than_current 149 | csharp_indent_switch_labels = true 150 | 151 | # Space preferences 152 | csharp_space_after_cast = false 153 | csharp_space_after_colon_in_inheritance_clause = true 154 | csharp_space_after_comma = true 155 | csharp_space_after_dot = false 156 | csharp_space_after_keywords_in_control_flow_statements = true 157 | csharp_space_after_semicolon_in_for_statement = true 158 | csharp_space_around_binary_operators = before_and_after 159 | csharp_space_around_declaration_statements = false 160 | csharp_space_before_colon_in_inheritance_clause = true 161 | csharp_space_before_comma = false 162 | csharp_space_before_dot = false 163 | csharp_space_before_open_square_brackets = false 164 | csharp_space_before_semicolon_in_for_statement = false 165 | csharp_space_between_empty_square_brackets = false 166 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 167 | csharp_space_between_method_call_name_and_opening_parenthesis = false 168 | csharp_space_between_method_call_parameter_list_parentheses = false 169 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 170 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 171 | csharp_space_between_method_declaration_parameter_list_parentheses = false 172 | csharp_space_between_parentheses = false 173 | csharp_space_between_square_brackets = false 174 | 175 | # Wrapping preferences 176 | csharp_preserve_single_line_blocks = true 177 | csharp_preserve_single_line_statements = true 178 | 179 | #### Naming styles #### 180 | 181 | # Naming rules 182 | 183 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 184 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 185 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 186 | 187 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 188 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 189 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 190 | 191 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 192 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 193 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 194 | 195 | # Symbol specifications 196 | 197 | dotnet_naming_symbols.interface.applicable_kinds = interface 198 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 199 | dotnet_naming_symbols.interface.required_modifiers = 200 | 201 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 202 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 203 | dotnet_naming_symbols.types.required_modifiers = 204 | 205 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 206 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 207 | dotnet_naming_symbols.non_field_members.required_modifiers = 208 | 209 | # Naming styles 210 | 211 | dotnet_naming_style.pascal_case.required_prefix = 212 | dotnet_naming_style.pascal_case.required_suffix = 213 | dotnet_naming_style.pascal_case.word_separator = 214 | dotnet_naming_style.pascal_case.capitalization = pascal_case 215 | 216 | dotnet_naming_style.begins_with_i.required_prefix = I 217 | dotnet_naming_style.begins_with_i.required_suffix = 218 | dotnet_naming_style.begins_with_i.word_separator = 219 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 220 | -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/StaticDefinitions.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | /// 4 | /// Partial class containing static function definitions from the original DOS executable. 5 | /// 6 | /// 7 | /// This file defines symbolic names for known functions in the DNCDPRG.EXE executable 8 | /// to enable better tracing and debugging in Spice86. Most function names are derived 9 | /// from IDA Pro analysis with "_ida" suffix indicating their origin. 10 | /// These definitions do not provide C# implementations, only symbolic mappings. 11 | /// 12 | public partial class Overrides { 13 | 14 | /// 15 | /// Registers static function definitions with Spice86 for improved tracing and debugging. 16 | /// 17 | /// 18 | /// These function definitions establish symbolic names for addresses in the original 19 | /// DOS executable, enabling more readable logs and stack traces. Functions span 20 | /// various game systems including video playback (HNM), resource loading, audio, 21 | /// memory management, map rendering, and user interface. 22 | /// 23 | public void DefineStaticDefinitionsFunctions() { 24 | DefineFunction(cs1, 0x3A, "exit"); 25 | DefineFunction(cs1, 0xB0, "initialize_2_ida"); 26 | DefineFunction(cs1, 0xD1, "intialize_resources_ida"); 27 | DefineFunction(cs1, 0x0169, "map2_resource_func_ida"); 28 | DefineFunction(cs1, 0x21C, "play_intro2_ida"); 29 | DefineFunction(cs1, 0x0309, "play_CREDITS_HNM_ida"); 30 | DefineFunction(cs1, 0x0580, "play_intro_ida"); 31 | DefineFunction(cs1, 0x61C, "load_VIRGIN_HNM_ida"); 32 | DefineFunction(cs1, 0x0625, "play_VIRGIN_HNM_ida"); 33 | DefineFunction(cs1, 0x64D, "load_CRYO_HNM_ida"); 34 | DefineFunction(cs1, 0x0658, "load_CRYO2_HNM_ida"); 35 | DefineFunction(cs1, 0x0661, "play_CRYO_OR_CRYO2_HNM_ida"); 36 | DefineFunction(cs1, 0x0678, "load_PRESENT_HNM_ida"); 37 | DefineFunction(cs1, 0x0684, "play_PRESENT_HNM_ida"); 38 | DefineFunction(cs1, 0x69E, "load_INTRO_HNM_ida"); 39 | DefineFunction(cs1, 0xF131, "setErrorMessageToNotEnoughMemory"); 40 | DefineFunction(cs1, 0x6AA, "play_hnm_86_frames_ida"); 41 | DefineFunction(cs1, 0x6BD, "play_hnm_skippable_ida"); 42 | DefineFunction(cs1, 0x9EF, "play_CREDITS_HNM_ida"); 43 | DefineFunction(cs1, 0x2D74, "open_SAL_resource_ida"); 44 | DefineFunction(cs1, 0x3B59, "draw_SAL_ida"); 45 | DefineFunction(cs1, 0x3BE9, "SAL_polygon_ida"); 46 | DefineFunction(cs1, 0x3D83, "do_weird_shit_with_stack_buffer_ida"); 47 | DefineFunction(cs1, 0x55DD, "map_func_ida"); 48 | DefineFunction(cs1, 0x5E4F, "calc_SAL_index_ida"); 49 | DefineFunction(cs1, 0x63F0, "map_func_qq_ida"); 50 | DefineFunction(cs1, 0x739E, "map_func_ida"); 51 | DefineFunction(cs1, 0xA87E, "audio_test_frequency_ida"); 52 | DefineFunction(cs1, 0xA90B, "open_res_file_ida"); 53 | DefineFunction(cs1, 0xA93F, "read_audio_file_ida"); 54 | DefineFunction(cs1, 0xA9A1, "close_res_file_handle_ida"); 55 | DefineFunction(cs1, 0xA9E7, "pcm_test_audio_done_ida"); 56 | DefineFunction(cs1, 0xAA0F, "decode_sd_block_ida"); 57 | DefineFunction(cs1, 0xAA70, "transfer_sd_block_qq_ida"); 58 | DefineFunction(cs1, 0xAB15, "audio_start_voc_ida"); 59 | DefineFunction(cs1, 0xABA3, "check_res_file_open_ida"); 60 | DefineFunction(cs1, 0xABE9, "open_voc_resource_ida"); 61 | DefineFunction(cs1, 0xAC14, "pcm_stop_voc_q_ida"); 62 | DefineFunction(cs1, 0xAD57, "play_music_MORNING_HSQ_ida"); 63 | DefineFunction(cs1, 0xAE62, "load_music_ida"); 64 | DefineFunction(cs1, 0xB389, "open_sav_cl_ida"); 65 | DefineFunction(cs1, 0xB427, "map_func_ida"); 66 | DefineFunction(cs1, 0xB58B, "map_func_ida"); 67 | DefineFunction(cs1, 0xB6C3, "map_func_ida"); 68 | DefineFunction(cs1, 0xB977, "map_func_gfx_ida"); 69 | DefineFunction(cs1, 0xBFE3, "map_func_ida"); 70 | DefineFunction(cs1, 0xC097, "gfx_call_bp_with_front_buffer_as_screen_ida"); 71 | DefineFunction(cs1, 0xC108, "transition_ida"); 72 | DefineFunction(cs1, 0xC137, "load_icons_sprites_ida"); 73 | DefineFunction(cs1, 0xC13E, "open_sprite_sheet_ida"); 74 | DefineFunction(cs1, 0xC1BA, "hnm_apply_palette_ida"); 75 | DefineFunction(cs1, 0xC22F, "draw_sprite_ida"); 76 | DefineFunction(cs1, 0xC477, "gfx_copy_rect_at_si_ida"); 77 | DefineFunction(cs1, 0xC49A, "gfx_copy_framebuffer_to_screen_ida"); 78 | DefineFunction(cs1, 0xC4AA, "gfx_copy_rect_to_screen_ida"); 79 | DefineFunction(cs1, 0xC4CD, "gfx_copy_framebuf_to_screen_ida"); 80 | DefineFunction(cs1, 0xC4F0, "rect_at_si_to_regs_ida"); 81 | DefineFunction(cs1, 0xC92B, "hnm_reset_and_read_header_ida"); 82 | DefineFunction(cs1, 0xC93C, "hnm_read_header_ida"); 83 | DefineFunction(cs1, 0xC9E8, "hnm_do_frame_skippable_ida"); 84 | DefineFunction(cs1, 0xC9F4, "do_frame_and_check_if_frame_advanced_ida"); 85 | DefineFunction(cs1, 0xCA01, "hnm_close_resource_ida"); 86 | DefineFunction(cs1, 0xCA1B, "hnm_load_ida"); 87 | DefineFunction(cs1, 0xCA60, "hnm_do_frame_ida"); 88 | DefineFunction(cs1, 0xCC96, "hnm_decode_video_frame_ida"); 89 | DefineFunction(cs1, 0xCD8F, "hnm_read_header_size_ida"); 90 | DefineFunction(cs1, 0xCDA0, "hnm_prepare_header_read_ida"); 91 | DefineFunction(cs1, 0xCE1A, "hnm_reset_ida"); 92 | DefineFunction(cs1, 0xCE3B, "hnm_handle_pal_chunk_ida"); 93 | DefineFunction(cs1, 0xCE6C, "initialize_memory_handler_ida"); 94 | DefineFunction(cs1, 0xCEFC, "load_IRULn_HSQ_ida"); 95 | DefineFunction(cs1, 0xCF1B, "play_IRULx_HSQ_ida"); 96 | DefineFunction(cs1, 0xCF4B, "IRULx_draw_or_clear_subtitle_ida"); 97 | DefineFunction(cs1, 0xCFA0, "check_amr_or_eng_language_ida"); 98 | DefineFunction(cs1, 0xD00F, "load_PHRASExx_HSQ_ida"); 99 | //DefineFunction(segment, 0xDAE3, "set_mouse_pos_ida"); 100 | DefineFunction(cs1, 0xDB14, "define_mouse_range_ida"); 101 | DefineFunction(cs1, 0xDB4C, "mouse_stuff_ida"); 102 | DefineFunction(cs1, 0xDBB2, "call_restore_cursor_ida"); 103 | DefineFunction(cs1, 0xDBEC, "draw_mouse_ida"); 104 | DefineFunction(cs1, 0xDC20, "redraw_mouse_ida"); 105 | DefineFunction(cs1, 0xDCE0, "read_game_port_ida"); 106 | DefineFunction(cs1, 0xDD5A, "get_key_hit_ida"); 107 | DefineFunction(cs1, 0xDD63, "stc_on_user_input_ida"); 108 | DefineFunction(cs1, 0xDE0C, "check_midi_ida"); 109 | DefineFunction(cs1, 0xDF1E, "get_mouse_pos_etc_ida"); 110 | DefineFunction(cs1, 0xE4AD, "parse_command_line_ida"); 111 | DefineFunction(cs1, 0xE56B, "parse_cmd_is_end_of_arg_ida"); 112 | DefineFunction(cs1, 0xE57B, "load_driver_ax_with_vtable_at_si_ida"); 113 | DefineFunction(cs1, 0xE594, "initialize_ida"); 114 | DefineFunction(cs1, 0xE675, "open_dune_dat_ida"); 115 | DefineFunction(cs1, 0xE741, "read_dune_dat_toc_ida"); 116 | DefineFunction(cs1, 0xE76A, "initialize_audio_ida"); 117 | DefineFunction(cs1, 0xE8B8, "get_pit_timer_value_ida"); 118 | DefineFunction(cs1, 0xE8D5, "uninitialize_memory_driver_ida"); 119 | DefineFunction(cs1, 0xE913, "install_interrupt_handlers_ida"); 120 | DefineFunction(cs1, 0xE97A, "initialize_mouse_ida"); 121 | DefineFunction(cs1, 0xE9F4, "mouse_func_uncalled_ida"); 122 | DefineFunction(cs1, 0xEA32, "initialize_joystick_ida"); 123 | DefineFunction(cs1, 0xEA7B, "memory_func_qq_ida"); 124 | DefineFunction(cs1, 0xEAB7, "memory_func_qq_ida"); 125 | DefineFunction(cs1, 0xEC46, "call_memory_func_2_ida"); 126 | DefineFunction(cs1, 0xEC59, "call_memory_func_1_ida"); 127 | DefineFunction(cs1, 0xEC9C, "xms_memory_func_1_ida"); 128 | DefineFunction(cs1, 0xECEC, "xms_memory_func_1_ida"); 129 | DefineFunction(cs1, 0xED40, "get_ems_emm_handle_ida"); 130 | DefineFunction(cs1, 0xED45, "call_ems_func_ida"); 131 | DefineFunction(cs1, 0xEDB9, "map_ems_for_midi_audio_ida"); 132 | DefineFunction(cs1, 0xEE02, "ems_memory_func_2_ida"); 133 | DefineFunction(cs1, 0xEE46, "ems_memory_func_1_ida"); 134 | DefineFunction(cs1, 0xEEA0, "initialize_himem_sys_ida"); 135 | DefineFunction(cs1, 0xEF22, "call_xms_driver_func_ida"); 136 | DefineFunction(cs1, 0xEF2B, "call_xms_func_on_block_ida"); 137 | DefineFunction(cs1, 0xEF32, "xms_move_memory_ida"); 138 | DefineFunction(cs1, 0xF05C, "reset_keyboard_ida"); 139 | DefineFunction(cs1, 0xF08E, "clear_keyboard_array_ida"); 140 | DefineFunction(cs1, 0xF0A0, "open_resource_force_hsq_ida"); 141 | DefineFunction(cs1, 0xF0B9, "open_resource_by_index_si_ida"); 142 | DefineFunction(cs1, 0xF0D6, "read_and_maybe_hsq_ida"); 143 | DefineFunction(cs1, 0xF0F6, "bump_alloc_get_addr_in_di_ida"); 144 | DefineFunction(cs1, 0xF0FF, "bump_allocate_bump_cx_bytes_ida"); 145 | DefineFunction(cs1, 0xF11C, "alloc_cx_pages_to_di_ida"); 146 | DefineFunction(cs1, 0xF13F, "allocator_attempt_to_free_space_ida"); 147 | DefineFunction(cs1, 0xF1FB, "open_res_or_file_to_dx_size_ax_ida"); 148 | DefineFunction(cs1, 0xF229, "open_res_or_file_or_die_ida"); 149 | DefineFunction(cs1, 0xF244, "read_resource_to_esdi_ida"); 150 | DefineFunction(cs1, 0xF255, "open_nonres_file_ida"); 151 | DefineFunction(cs1, 0xF260, "read_ffff_to_esdi_and_close_ida"); 152 | DefineFunction(cs1, 0xF2A7, "seek_dune_dat_to_res_dsdx_ida"); 153 | DefineFunction(cs1, 0xF2D6, "seek_dune_dat_offset_dxax_ida"); 154 | DefineFunction(cs1, 0xF2EA, "read_dune_dat_cx_to_esdi_ida"); 155 | DefineFunction(cs1, 0xF2FC, "strcpy_to_filename_buf_ida"); 156 | DefineFunction(cs1, 0xF314, "locate_res_by_name_dssi_ida"); 157 | DefineFunction(cs1, 0xF403, "hsq_decomp_skip_header_dssi_to_esdi_ida"); 158 | // Jumped to by self modifying code, needs to be there 159 | DefineFunction(cs2, 0x1EC9, "split_C000_1EC9_C1EC9"); 160 | } 161 | } -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/Overrides.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | using Globals; 4 | 5 | using Spice86.Core.CLI; 6 | using Spice86.Core.Emulator.Function; 7 | using Spice86.Core.Emulator.Function.Dump; 8 | using Spice86.Core.Emulator.VM; 9 | using Spice86.Shared.Emulator.Memory; 10 | using Spice86.Shared.Interfaces; 11 | 12 | using System.Collections.Generic; 13 | 14 | /// 15 | /// Central class for defining C# overrides that replace routines in the DNCDPRG.EXE DOS executable. 16 | /// 17 | /// 18 | /// 19 | /// This partial class serves as the main coordinator for all game overrides. It extends 20 | /// from Spice86 and provides the infrastructure for 21 | /// replacing DOS assembly routines with modern C# implementations. 22 | /// 23 | /// 24 | /// The class maintains segment fields (cs1-cs5) that serve as anchors for all injected addresses. 25 | /// These segments are carefully mapped to avoid conflicts and enable easier disassembly analysis: 26 | /// 27 | /// 28 | /// cs1 (0x1000): Main game code segment 29 | /// cs2 (0xD000): VGA driver segment (remapped) 30 | /// cs3 (0xE000): PCM audio driver segment (remapped) 31 | /// cs4 (0xE000): MIDI music driver segment (remapped, shares with cs3) 32 | /// cs5 (0x0800): BIOS/IRQ interrupt handlers segment 33 | /// 34 | /// 35 | /// Override registration uses helper methods like DefineFunction (replaces CALL targets) 36 | /// and DoOnTopOfInstruction (injects inline hooks). These ensure Spice86 correctly 37 | /// patches the emulated program at the specified addresses. 38 | /// 39 | /// 40 | public partial class Overrides : CSharpOverrideHelper { 41 | /// Main game code segment (0x1000). 42 | protected ushort cs1; 43 | 44 | /// VGA driver segment (remapped to 0xD000). 45 | protected ushort cs2; 46 | 47 | /// PCM audio driver segment (remapped to 0xE000). 48 | protected ushort cs3; 49 | 50 | /// MIDI music driver segment (remapped to 0xE000, shares with cs3). 51 | protected ushort cs4; 52 | 53 | /// BIOS/IRQ interrupt handler segment (0x0800). 54 | protected ushort cs5; 55 | 56 | /// Accessor for game global variables stored in the DS segment. 57 | private ExtraGlobalsOnDs globalsOnDs; 58 | 59 | /// Accessor for game global variables stored in CS segment 0x2538. 60 | private ExtraGlobalsOnCsSegment0x2538 globalsOnCsSegment0X2538; 61 | 62 | /// 63 | /// Initializes the override system and registers all function replacements and hooks. 64 | /// 65 | /// Dictionary to populate with function override mappings. 66 | /// The segment where DNCDPRG.EXE was loaded (unused but required by interface). 67 | /// The emulated machine providing access to CPU, memory, and devices. 68 | /// Service for logging debug information during override execution. 69 | /// Spice86 configuration containing runtime settings. 70 | public Overrides(Dictionary functionInformations, ushort entrySegment, 71 | Machine machine, ILoggerService loggerService, Configuration configuration) : 72 | base(functionInformations, machine, loggerService, configuration) { 73 | // Main code 74 | this.cs1 = 0x1000; 75 | // Vga driver is remapped here 76 | this.cs2 = DriverLoadToolbox.DRIVER1_SEGMENT; 77 | // PCM driver 78 | this.cs3 = DriverLoadToolbox.DRIVER2_SEGMENT; 79 | // Midi driver 80 | this.cs4 = DriverLoadToolbox.DRIVER2_SEGMENT; 81 | // Bios, This does not depend on the entry segment. 82 | this.cs5 = DriverLoadToolbox.INTERRUPT_HANDLER_SEGMENT; 83 | globalsOnDs = new ExtraGlobalsOnDs(machine.Memory, machine.Cpu.State.SegmentRegisters); 84 | globalsOnCsSegment0X2538 = new ExtraGlobalsOnCsSegment0x2538(machine.Memory, cs2); 85 | 86 | DefineOverrides(); 87 | DefineStaticDefinitionsFunctions(); 88 | } 89 | 90 | /// 91 | /// Registers all override functions and inline hooks across different game subsystems. 92 | /// 93 | /// 94 | /// This method orchestrates the registration of overrides for: 95 | /// 96 | /// Data structures and memory management 97 | /// VGA graphics operations 98 | /// Dialogue system 99 | /// Display and rendering 100 | /// HNM video playback 101 | /// Initialization sequences 102 | /// Map rendering and navigation 103 | /// Menu system 104 | /// Scripted scenes and cutscenes 105 | /// Time and timer management 106 | /// MT-32 MIDI driver 107 | /// Driver remapping infrastructure 108 | /// Memory dump triggers for debugging 109 | /// 110 | /// Generated code overrides are commented out as they crash for various reasons. 111 | /// 112 | public void DefineOverrides() { 113 | DefineDataStructureOverrides(); 114 | DefineVgaDriverCodeOverrides(); 115 | DefineDialoguesCodeOverrides(); 116 | DefineDisplayCodeOverrides(); 117 | DefineHnmCodeOverrides(); 118 | DefineInitCodeOverrides(); 119 | DefineMapCodeOverrides(); 120 | DefineMenuCodeOverrides(); 121 | DefineScriptedSceneCodeOverrides(); 122 | DefineTimeCodeOverrides(); 123 | DefineTimerCodeOverrides(); 124 | DefineUnknownCodeOverrides(); 125 | DefineVideoCodeOverrides(); 126 | 127 | DefineDriversRemapping(); 128 | DetectDriversEntryPoints(); 129 | // Dump memory at the proper time. Too soon and drivers wont be loaded, too late and init code will be erased 130 | DefineMemoryDumpsMapping(); 131 | DefineMT32DriverCodeOverrides(); 132 | 133 | // Generated code, crashes for various reasons 134 | //DefineGeneratedCodeOverrides(); 135 | } 136 | 137 | /// 138 | /// Registers memory dump triggers at strategic points during game initialization. 139 | /// 140 | /// 141 | /// Memory dumps are triggered at specific addresses to capture game state: 142 | /// 143 | /// After driver loading (CS1:000C) 144 | /// After self-modifying code updates (CS4:02DC and CS4:03EE) 145 | /// 146 | /// Dumps must be timed carefully - too early and drivers won't be loaded, 147 | /// too late and initialization code will be erased by self-modifying code. 148 | /// 149 | private void DefineMemoryDumpsMapping() { 150 | DoOnTopOfInstruction(cs1, 0x000C, () => { 151 | DumpMemoryWithSuffix("_" + ConvertUtils.ToHex16WithoutX(cs1) + "_000C_After_driver_load"); 152 | }); 153 | DoOnTopOfInstruction(cs4, 0x02DC, () => { 154 | callsTo02DB++; 155 | DumpMemoryWithSuffix("_" + ConvertUtils.ToHex16WithoutX(cs4) + "_02DC_After_code_modification_" + 156 | callsTo02DB); 157 | }); 158 | DoOnTopOfInstruction(cs4, 0x03EE, () => { 159 | callsTo03ED++; 160 | DumpMemoryWithSuffix("_" + ConvertUtils.ToHex16WithoutX(cs4) + "_03EE_After_code_modification_" + 161 | callsTo03ED); 162 | }); 163 | } 164 | 165 | /// Counter for memory dumps at CS4:02DC to create unique filenames. 166 | private int callsTo02DB = 0; 167 | 168 | /// Counter for memory dumps at CS4:03EE to create unique filenames. 169 | private int callsTo03ED = 0; 170 | 171 | /// 172 | /// Exports a memory dump with the specified filename suffix. 173 | /// 174 | /// Suffix to append to the dump filename for identification. 175 | private void DumpMemoryWithSuffix(string suffix) { 176 | new MemoryDataExporter(Memory, Machine.CallbackHandler, Configuration, Configuration.RecordedDataDirectory, _loggerService).DumpMemory(suffix); 177 | } 178 | 179 | /// 180 | /// Registers hooks for driver remapping at the beginning and end of the driver load routine. 181 | /// 182 | /// 183 | /// Injects calls to at CS1:E57B 184 | /// and at CS1:E593 to control 185 | /// driver segment allocation. 186 | /// 187 | private void DefineDriversRemapping() { 188 | DoOnTopOfInstruction(cs1, 0xE57B, () => { 189 | DriverLoadToolbox.RemapDrivers(State, Memory); 190 | }); 191 | DoOnTopOfInstruction(cs1, 0xE593, () => { 192 | DriverLoadToolbox.ResetAllocator(State, Memory); 193 | }); 194 | } 195 | 196 | /// 197 | /// Registers a hook to automatically detect and define driver entry point functions. 198 | /// 199 | /// 200 | /// Injects a call to at CS1:E589 201 | /// to parse driver export tables and register their functions for tracing. 202 | /// 203 | private void DetectDriversEntryPoints() { 204 | DoOnTopOfInstruction(cs1, 0xE589, () => { 205 | DriverLoadToolbox.ReadDriverFunctionTable(State, Memory, this); 206 | }); 207 | } 208 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Linux](https://img.shields.io/badge/-Linux-grey?logo=linux) 2 | ![OSX](https://img.shields.io/badge/-OSX-black?logo=apple) 3 | ![Windows](https://img.shields.io/badge/-Windows-red?logo=windows) 4 | 5 | # CRYOGENIC 6 | 7 | [![PR Validation](https://github.com/OpenRakis/Cryogenic/actions/workflows/pr-validation.yml/badge.svg)](https://github.com/OpenRakis/Cryogenic/actions/workflows/pr-validation.yml) 8 | [![Release](https://github.com/OpenRakis/Cryogenic/actions/workflows/release.yml/badge.svg)](https://github.com/OpenRakis/Cryogenic/actions/workflows/release.yml) 9 | [![Deploy Pages](https://github.com/OpenRakis/Cryogenic/actions/workflows/static.yml/badge.svg)](https://github.com/OpenRakis/Cryogenic/actions/workflows/static.yml) 10 | [![CodeQL](https://github.com/OpenRakis/Cryogenic/actions/workflows/pr-validation.yml/badge.svg?event=push)](https://github.com/OpenRakis/Cryogenic/security/code-scanning) 11 | [![License](https://img.shields.io/github/license/OpenRakis/Cryogenic)](LICENSE) 12 | [![Latest Release](https://img.shields.io/github/v/release/OpenRakis/Cryogenic)](https://github.com/OpenRakis/Cryogenic/releases/latest) 13 | [![GitHub Stars](https://img.shields.io/github/stars/OpenRakis/Cryogenic?style=social)](https://github.com/OpenRakis/Cryogenic/stargazers) 14 | [![GitHub Forks](https://img.shields.io/github/forks/OpenRakis/Cryogenic?style=social)](https://github.com/OpenRakis/Cryogenic/network/members) 15 | [![GitHub Issues](https://img.shields.io/github/issues/OpenRakis/Cryogenic)](https://github.com/OpenRakis/Cryogenic/issues) 16 | [![.NET Version](https://img.shields.io/badge/.NET-8.0-512BD4?logo=dotnet)](https://dotnet.microsoft.com/) 17 | 18 | **Reverse Engineering the Classic Dune Game** 19 | 20 | 📚 **[GitHub Page](https://openrakis.github.io/Cryogenic/)** 21 | 22 | --- 23 | 24 | ## Table of Contents 25 | 26 | - [About the Project](#about-the-project) 27 | - [Project Goals](#project-goals) 28 | - [Technology - Spice86](#technology---spice86) 29 | - [Status](#status) 30 | - [Prerequisites](#prerequisites) 31 | - [Build & Run](#build--run) 32 | - [Building](#building) 33 | - [Running (Basic Mode)](#running-basic-mode) 34 | - [Running with Audio](#running-with-audio) 35 | - [Screenshots](#screenshots) 36 | - [Contributing](#contributing) 37 | - [Resources](#resources) 38 | - [License](#license) 39 | 40 | --- 41 | 42 | ## About the Project 43 | 44 | Cryogenic is an ambitious reverse engineering project dedicated to understanding and modernizing **Cryo's Dune** (CD Version, 1992) - one of the most atmospheric adventure games of the early 90s. 45 | 46 | Using the powerful [Spice86](https://github.com/OpenRakis/Spice86) reverse engineering toolkit, we're gradually rewriting the game from x86 assembly into readable, maintainable C# code. The game is **fully playable** right now, complete with sound and music support. 47 | 48 | As we continue to replace assembly routines with C# implementations, the codebase becomes more accessible to modern developers while preserving the exact behavior of the original DOS executable. 49 | 50 | ### Supported Version 51 | 52 | **SHA256 signature of supported DNCDPRG.EXE:** 53 | ``` 54 | 5f30aeb84d67cf2e053a83c09c2890f010f2e25ee877ebec58ea15c5b30cfff9 55 | ``` 56 | 57 | ⚠️ **Note:** The CD release of DUNE (version 3.7, the most widely available one) must be obtained separately, as it is **copyrighted material**. 58 | 59 | --- 60 | 61 | ## Project Goals 62 | 63 | 🔍 **Understand the Game** - Deep dive into the inner workings of Dune's game engine, uncovering how Cryo implemented dialogue systems, resource management, and real-time strategy elements in the DOS era. 64 | 65 | 🔄 **Incremental Rewriting** - Gradually replace x86 assembly routines with clean, documented C# code while maintaining 100% behavioral compatibility with the original game. 66 | 67 | 📚 **Document Everything** - Create comprehensive documentation of game mechanics, data structures, and algorithms to preserve this knowledge for future developers and gaming historians. 68 | 69 | 🎮 **Preserve Gaming History** - Ensure this classic adventure game remains playable on modern systems and provide a foundation for potential future enhancements and ports. 70 | 71 | 🛠️ **Educational Resource** - Serve as a reference implementation for reverse engineering techniques and demonstrate the power of hybrid emulation approaches. 72 | 73 | 🌐 **Cross-Platform Support** - Leverage .NET 8's cross-platform capabilities to run the game natively on Windows, macOS, and Linux without emulation layers. 74 | 75 | --- 76 | 77 | ## Technology - Spice86 78 | 79 | ### What is Spice86? 80 | 81 | [Spice86](https://github.com/OpenRakis/Spice86) is a revolutionary reverse engineering toolkit and PC emulator specifically designed for 16-bit real mode x86 programs. Unlike traditional emulators that simply run old software, Spice86 enables you to **gradually modernize** legacy DOS applications by replacing assembly code with high-level C# implementations. 82 | 83 | Built on .NET 8, Spice86 provides a unique hybrid execution model where the original DOS executable runs alongside your C# overrides, allowing for incremental reverse engineering and testing. 84 | 85 | ### Key Features 86 | 87 | - 🔄 **Hybrid Execution** - Run original DOS binaries while selectively replacing functions with C# implementations. Test your reverse-engineered code against the real thing in real-time. 88 | - 🔬 **Runtime Analysis** - Collect memory dumps, execution traces, and runtime data while the program runs. 89 | - 🎯 **Ghidra Integration** - Import runtime data into Ghidra for static analysis. 90 | - 🐛 **Advanced Debugging** - Built-in debugger with GDB remote protocol support. 91 | - 🎮 **Hardware Emulation** - Complete support for VGA/EGA/CGA graphics, PC Speaker, AdLib, SoundBlaster, keyboard, and mouse. 92 | - 🌍 **Cross-Platform** - Built on .NET 8 for Windows, macOS, and Linux. 93 | 94 | ### How Cryogenic Uses Spice86 95 | 96 | 1. **Override Registration** - Cryogenic registers C# function overrides in `DuneCdOverrideSupplier` that replace specific assembly routines at runtime. 97 | 2. **Segment Management** - Memory segments (CS1=0x1000 for main code, CS2/3/4 for drivers, CS5=0xF000 for BIOS) are carefully managed. 98 | 3. **Hybrid Execution** - When DNCDPRG.EXE calls an overridden function, Spice86 redirects execution to the C# implementation. 99 | 4. **State Synchronization** - Global game state accessors like `globalsOnDs` ensure C# code and assembly share the same memory. 100 | 101 | --- 102 | 103 | ## Status 104 | 105 | Thanks to the hybrid ASM / .NET mode provided by Spice86, the game is **fully playable**, including sound and music. 106 | 107 | The goal is to have more and more logic written in human-readable C#, making the codebase accessible to modern developers while preserving the classic gameplay experience. 108 | 109 | --- 110 | 111 | ## Prerequisites 112 | 113 | Before you begin, ensure you have: 114 | 115 | 1. **[.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)** - Required to build and run the project 116 | 2. **Dune CD Version 3.7** - You must obtain `DNCDPRG.EXE` and `DUNE.DAT` separately (copyrighted material) 117 | 118 | You can verify your executable with PowerShell: 119 | ```powershell 120 | Get-FileHash DNCDPRG.EXE -Algorithm SHA256 121 | ``` 122 | 123 | Or on Linux/Mac: 124 | ```bash 125 | sha256sum DNCDPRG.EXE 126 | ``` 127 | 128 | --- 129 | 130 | ## Build & Run 131 | 132 | ### Building 133 | 134 | Clone the repository and build the project: 135 | 136 | ```bash 137 | git clone https://github.com/OpenRakis/Cryogenic 138 | cd Cryogenic/src 139 | dotnet build 140 | ``` 141 | 142 | ### Running (Basic Mode) 143 | 144 | Ensure that `DUNE.DAT` and `DNCDPRG.EXE` are in the same folder, then run: 145 | 146 | ```bash 147 | cd Cryogenic/src 148 | dotnet run --Exe C:/path/to/dunecd/DNCDPRG.EXE --UseCodeOverride true -p 4096 149 | ``` 150 | 151 | ⚠️ **Important:** Always use `--UseCodeOverride true` or your C# code won't execute! 152 | 153 | ### Running with Audio 154 | 155 | For the full experience with AdLib music and PCM sound effects: 156 | 157 | ```bash 158 | cd Cryogenic/src/Cryogenic 159 | dotnet publish 160 | bin/Release/net8.0/publish/Cryogenic --Exe C:/path/to/dunecd/DNCDPRG.EXE --UseCodeOverride true -p 4096 -a "ADL220 SBP2227" 161 | ``` 162 | 163 | --- 164 | 165 | ## Screenshots 166 | 167 |
168 | 169 | ![Desert Worm](doc/cryodune_worm.png) 170 | *Encounter with the mighty sandworm* 171 | 172 | ![Chani](doc/cryodune_chani.png) 173 | *Meeting with Chani* 174 | 175 | ![Spice Management](doc/cryodune_send_spice.png) 176 | *Sending Spice to the Emperor* 177 | 178 | ![Harkonnen](doc/cryodune_harkonen.png) 179 | *Harkonnen confrontation* 180 | 181 |
182 | 183 | --- 184 | 185 | ## Contributing 186 | 187 | We welcome contributions from developers of all skill levels! Whether you're experienced in reverse engineering or just getting started, there's a place for you in this project. 188 | 189 | ### Ways to Contribute 190 | 191 | - 🔍 **Reverse Engineering** - Analyze assembly code, identify functions, and create C# implementations 192 | - 📝 **Documentation** - Help document game mechanics, data structures, and code functionality 193 | - 🧪 **Testing** - Play the game, find bugs, and verify that C# overrides behave identically to the original 194 | - 🛠️ **Code Quality** - Improve existing C# code, refactor for clarity, and add helpful comments 195 | 196 | ### Getting Started 197 | 198 | 1. Fork the repository on GitHub 199 | 2. Clone your fork locally 200 | 3. Read the [CONTRIBUTING.md](CONTRIBUTING.md) file for detailed guidelines 201 | 4. Pick an issue labeled `good first issue` or `help wanted` 202 | 5. Create a feature branch for your work 203 | 6. Make your changes and test thoroughly 204 | 7. Submit a pull request 205 | 206 | For detailed contribution guidelines, architecture notes, and coding conventions, please see our [CONTRIBUTING.md](CONTRIBUTING.md) file. 207 | 208 | --- 209 | 210 | ## Resources 211 | 212 | - 🌐 **[Project Website](https://openrakis.github.io/Cryogenic/)** - Comprehensive documentation 213 | - 🐙 **[GitHub Repository](https://github.com/OpenRakis/Cryogenic)** - Source code, issues, and releases 214 | - 🌶️ **[Spice86 Project](https://github.com/OpenRakis/Spice86)** - The reverse engineering toolkit powering Cryogenic 215 | - 📦 **[Releases](https://github.com/OpenRakis/Cryogenic/releases)** - Download the latest version 216 | - 📖 **[Dune (1992) - Wikipedia](https://en.wikipedia.org/wiki/Dune_(video_game))** - Learn about the original game 217 | 218 | --- 219 | 220 | ## License 221 | 222 | Cryogenic is open-source software licensed under the **Apache License 2.0**. 223 | 224 | © 2021-2024 Kevin Ferrare and contributors. 225 | 226 | Dune is © Cryo Interactive Entertainment. This project is not affiliated with or endorsed by Cryo Interactive Entertainment or any rights holders. 227 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | permissions: 10 | contents: write 11 | security-events: write 12 | actions: read 13 | 14 | jobs: 15 | build-test-scan: 16 | name: Build, Test & CodeQL Scan 17 | runs-on: ubuntu-latest 18 | outputs: 19 | semver: ${{ steps.gitversion.outputs.semVer }} 20 | assemblysemver: ${{ steps.gitversion.outputs.assemblySemVer }} 21 | assemblysemfilever: ${{ steps.gitversion.outputs.assemblySemFileVer }} 22 | informationalversion: ${{ steps.gitversion.outputs.informationalVersion }} 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 # Full history for GitVersion 29 | fetch-tags: true 30 | 31 | - name: Setup .NET 32 | uses: actions/setup-dotnet@v4 33 | with: 34 | dotnet-version: '8.0.x' 35 | 36 | - name: Install GitVersion 37 | uses: gittools/actions/gitversion/setup@v3.1.1 38 | with: 39 | versionSpec: '6.0.x' 40 | 41 | - name: Determine Version 42 | id: gitversion 43 | uses: gittools/actions/gitversion/execute@v3.1.1 44 | with: 45 | useConfigFile: true 46 | configFilePath: GitVersion.yml 47 | 48 | - name: Display GitVersion outputs 49 | run: | 50 | echo "SemVer: ${{ steps.gitversion.outputs.semVer }}" 51 | echo "AssemblySemVer: ${{ steps.gitversion.outputs.assemblySemVer }}" 52 | echo "InformationalVersion: ${{ steps.gitversion.outputs.informationalVersion }}" 53 | 54 | - name: Initialize CodeQL 55 | uses: github/codeql-action/init@v3 56 | with: 57 | languages: csharp 58 | queries: security-and-quality 59 | 60 | - name: Restore dependencies 61 | working-directory: ./src 62 | run: dotnet restore 63 | 64 | - name: Build 65 | working-directory: ./src 66 | run: | 67 | dotnet build --configuration Release --no-restore \ 68 | /p:Version=${{ steps.gitversion.outputs.semVer }} \ 69 | /p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \ 70 | /p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \ 71 | /p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} 72 | 73 | - name: Run tests 74 | working-directory: ./src 75 | run: dotnet test --configuration Release --no-build --verbosity normal 76 | continue-on-error: true # Continue if no tests exist 77 | 78 | - name: Perform CodeQL Analysis 79 | uses: github/codeql-action/analyze@v3 80 | with: 81 | category: "/language:csharp" 82 | 83 | publish-and-release: 84 | name: Publish & Create Release 85 | needs: build-test-scan 86 | runs-on: ubuntu-latest 87 | 88 | steps: 89 | - name: Checkout code 90 | uses: actions/checkout@v4 91 | with: 92 | fetch-depth: 0 93 | 94 | - name: Setup .NET 95 | uses: actions/setup-dotnet@v4 96 | with: 97 | dotnet-version: '8.0.x' 98 | 99 | - name: Restore dependencies 100 | working-directory: ./src/Cryogenic 101 | run: dotnet restore 102 | 103 | - name: Publish for all platforms 104 | working-directory: ./src/Cryogenic 105 | env: 106 | VERSION: ${{ needs.build-test-scan.outputs.semver }} 107 | ASSEMBLY_VERSION: ${{ needs.build-test-scan.outputs.assemblysemver }} 108 | FILE_VERSION: ${{ needs.build-test-scan.outputs.assemblysemfilever }} 109 | INFORMATIONAL_VERSION: ${{ needs.build-test-scan.outputs.informationalversion }} 110 | run: | 111 | # Release builds 112 | dotnet publish -c Release -r linux-arm64 --self-contained true \ 113 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 114 | -p:Version=$VERSION \ 115 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 116 | -p:FileVersion=$FILE_VERSION \ 117 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 118 | -o Release/linux-arm64 119 | 120 | dotnet publish -c Release -r linux-x64 --self-contained true \ 121 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 122 | -p:Version=$VERSION \ 123 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 124 | -p:FileVersion=$FILE_VERSION \ 125 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 126 | -o Release/linux-x64 127 | 128 | dotnet publish -c Release -r win-x64 --self-contained true \ 129 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 130 | -p:Version=$VERSION \ 131 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 132 | -p:FileVersion=$FILE_VERSION \ 133 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 134 | -o Release/windows-x64 135 | 136 | dotnet publish -c Release -r win-arm64 --self-contained true \ 137 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 138 | -p:Version=$VERSION \ 139 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 140 | -p:FileVersion=$FILE_VERSION \ 141 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 142 | -o Release/windows-arm64 143 | 144 | dotnet publish -c Release -r osx-x64 --self-contained true \ 145 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 146 | -p:Version=$VERSION \ 147 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 148 | -p:FileVersion=$FILE_VERSION \ 149 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 150 | -o Release/macos-x64 151 | 152 | dotnet publish -c Release -r osx-arm64 --self-contained true \ 153 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 154 | -p:Version=$VERSION \ 155 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 156 | -p:FileVersion=$FILE_VERSION \ 157 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 158 | -o Release/macos-arm64 159 | 160 | # Debug builds 161 | dotnet publish -c Debug -r linux-x64 --self-contained true \ 162 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 163 | -p:Version=$VERSION \ 164 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 165 | -p:FileVersion=$FILE_VERSION \ 166 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 167 | -o Debug/linux-x64 168 | 169 | dotnet publish -c Debug -r linux-arm64 --self-contained true \ 170 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 171 | -p:Version=$VERSION \ 172 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 173 | -p:FileVersion=$FILE_VERSION \ 174 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 175 | -o Debug/linux-arm64 176 | 177 | dotnet publish -c Debug -r win-x64 --self-contained true \ 178 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 179 | -p:Version=$VERSION \ 180 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 181 | -p:FileVersion=$FILE_VERSION \ 182 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 183 | -o Debug/windows-x64 184 | 185 | dotnet publish -c Debug -r win-arm64 --self-contained true \ 186 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 187 | -p:Version=$VERSION \ 188 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 189 | -p:FileVersion=$FILE_VERSION \ 190 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 191 | -o Debug/windows-arm64 192 | 193 | dotnet publish -c Debug -r osx-x64 --self-contained true \ 194 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 195 | -p:Version=$VERSION \ 196 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 197 | -p:FileVersion=$FILE_VERSION \ 198 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 199 | -o Debug/macos-x64 200 | 201 | dotnet publish -c Debug -r osx-arm64 --self-contained true \ 202 | -p:PublishReadyToRun=true -p:PublishSingleFile=true \ 203 | -p:Version=$VERSION \ 204 | -p:AssemblyVersion=$ASSEMBLY_VERSION \ 205 | -p:FileVersion=$FILE_VERSION \ 206 | -p:InformationalVersion=$INFORMATIONAL_VERSION \ 207 | -o Debug/macos-arm64 208 | 209 | - name: Create distribution packages 210 | working-directory: ./src/Cryogenic/bin 211 | run: | 212 | zip -qq -r Cryogenic-${{ needs.build-test-scan.outputs.semver }}-Debug.zip Debug 213 | zip -qq -r Cryogenic-${{ needs.build-test-scan.outputs.semver }}-Release.zip Release 214 | 215 | - name: Generate Release Notes 216 | id: release_notes 217 | run: | 218 | # Get the last tag 219 | LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") 220 | 221 | if [ -z "$LAST_TAG" ]; then 222 | echo "## What's Changed" > release_notes.md 223 | echo "" >> release_notes.md 224 | echo "Initial release" >> release_notes.md 225 | else 226 | echo "## What's Changed" > release_notes.md 227 | echo "" >> release_notes.md 228 | git log ${LAST_TAG}..HEAD --pretty=format:"* %s (%h)" --no-merges >> release_notes.md 229 | fi 230 | 231 | echo "" >> release_notes.md 232 | echo "" >> release_notes.md 233 | echo "## Assets" >> release_notes.md 234 | echo "" >> release_notes.md 235 | echo "Download the appropriate build for your platform:" >> release_notes.md 236 | echo "- **Release builds**: Production-ready optimized builds" >> release_notes.md 237 | echo "- **Debug builds**: Builds with debug symbols for troubleshooting" >> release_notes.md 238 | echo "" >> release_notes.md 239 | echo "### Supported Platforms" >> release_notes.md 240 | echo "- Linux (x64, ARM64)" >> release_notes.md 241 | echo "- Windows (x64, ARM64)" >> release_notes.md 242 | echo "- macOS (x64, ARM64)" >> release_notes.md 243 | 244 | - name: Create GitHub Release 245 | uses: softprops/action-gh-release@v2 246 | with: 247 | tag_name: v${{ needs.build-test-scan.outputs.semver }} 248 | name: Release v${{ needs.build-test-scan.outputs.semver }} 249 | body_path: release_notes.md 250 | draft: false 251 | prerelease: false 252 | files: | 253 | ./src/Cryogenic/bin/Cryogenic-${{ needs.build-test-scan.outputs.semver }}-Debug.zip 254 | ./src/Cryogenic/bin/Cryogenic-${{ needs.build-test-scan.outputs.semver }}-Release.zip 255 | env: 256 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 257 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2021 Kevin Ferrare 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/Cryogenic/DriverLoadToolbox.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic; 2 | 3 | using Spice86.Core.Emulator.CPU; 4 | using Spice86.Shared.Emulator.Memory; 5 | 6 | /// 7 | /// Provides utilities for remapping game drivers to clean segment boundaries for easier analysis. 8 | /// 9 | /// 10 | /// 11 | /// The purpose is to load the VGA, PCM, and MIDI drivers at multiples of 0x1000 (D000, E000, F000) 12 | /// which makes it easier to identify code locations in disassembly listings and works around 13 | /// Ghidra issue https://github.com/NationalSecurityAgency/ghidra/issues/981. 14 | /// 15 | /// 16 | /// The remapping process works in two phases: 17 | /// 18 | /// 19 | /// 20 | /// is called at the beginning of CS1:E57B to: 21 | /// - Remove the allocator max segment limit 22 | /// - Set next free pointer to D000, E000, or F000 depending on the driver being loaded 23 | /// 24 | /// 25 | /// is called at the end of CS1:E57B (address CS1:E593) to: 26 | /// - Reset the allocator max segment limit to its original value 27 | /// - Reset the next free pointer to its original value 28 | /// 29 | /// 30 | /// 31 | /// After the sequence completes, the driver is loaded but not allocated. This is acceptable 32 | /// because memory above VRAM (0xA000) is not used by the game. 33 | /// 34 | /// 35 | public class DriverLoadToolbox { 36 | /// 37 | /// Segment address for interrupt handlers (0x800), freeing 0xF000 for driver3. 38 | /// 39 | public const ushort INTERRUPT_HANDLER_SEGMENT = 0x800; 40 | 41 | /// 42 | /// Target segment address for VGA driver (DNVGA) remapping. 43 | /// 44 | public const ushort DRIVER1_SEGMENT = 0xD000; 45 | 46 | /// 47 | /// Target segment address for PCM audio driver (DNPCS2, DNSBP) remapping. 48 | /// 49 | public const ushort DRIVER2_SEGMENT = 0xE000; 50 | 51 | /// 52 | /// Target segment address for MIDI music driver (DNPCS, DNMID) remapping. 53 | /// 54 | public const ushort DRIVER3_SEGMENT = 0xF000; 55 | 56 | /// 57 | /// Stores the original allocator limit before modification. 58 | /// 59 | private static ushort InitialFreeLimit = 0; 60 | 61 | /// 62 | /// Stores the original free segment pointer before modification. 63 | /// 64 | private static ushort InitialFreeSegment = 0; 65 | 66 | /// 67 | /// Stores the original free offset pointer before modification. 68 | /// 69 | private static ushort InitialFreeOffset = 0; 70 | 71 | /// 72 | /// Tracks the currently loading driver by its index. 73 | /// 74 | private static int CurrentDriver = -1; 75 | 76 | /// 77 | /// Indicates whether a driver override has occurred in the current load sequence. 78 | /// 79 | private static bool DriverOverrideHappened = false; 80 | 81 | /// 82 | /// Enumeration of driver identifiers corresponding to AX register values during driver loading. 83 | /// 84 | private enum DriverIndex { DNVGA, DN386, DNPCS, DNADL, DNADP, DNADG, DNMID, DNPCS2, DNSDB, DNSBP } 85 | 86 | /// 87 | /// Maps driver index values to their string names for identification and logging purposes. 88 | /// 89 | private static Dictionary _driverNames = new() { 90 | { (int)DriverIndex.DNVGA, "DNVGA" }, // VGA graphics driver 91 | { (int)DriverIndex.DN386, "DN386" }, // 386 driver 92 | { (int)DriverIndex.DNPCS, "DNPCS" }, // PC Speaker sound driver 93 | { (int)DriverIndex.DNADL, "DNADL" }, // AdLib sound driver 94 | { (int)DriverIndex.DNADP, "DNADP" }, // AdLib Pro driver 95 | { (int)DriverIndex.DNADG, "DNADG" }, // AdLib Gold driver 96 | { (int)DriverIndex.DNMID, "DNMID" }, // MIDI music driver 97 | { (int)DriverIndex.DNPCS2, "DNPCS2" }, // PC Speaker variant 2 98 | { (int)DriverIndex.DNSDB, "DNSDB" }, // Sound Blaster driver 99 | { (int)DriverIndex.DNSBP, "DNSBP" } // Sound Blaster Pro driver 100 | }; 101 | 102 | /// 103 | /// Remaps specific game drivers to predetermined segment addresses for easier analysis. 104 | /// Should be called at the beginning of CS1:E57B during driver loading. 105 | /// 106 | /// CPU state containing the AX register with the driver index. 107 | /// Memory interface for reading and writing game memory. 108 | /// 109 | /// The AX register indicates which driver is being loaded: 110 | /// 111 | /// 0: DNVGA - VGA graphics driver 112 | /// 1: DN386 - 386 driver 113 | /// 2: DNPCS - PC Speaker sound driver 114 | /// 3: DNADL - AdLib sound driver 115 | /// 4: DNADP - AdLib Pro driver 116 | /// 5: DNADG - AdLib Gold driver 117 | /// 6: DNMID - MIDI music driver 118 | /// 7: DNPCS2 - PC Speaker variant 2 119 | /// 8: DNSDB - Sound Blaster driver 120 | /// 9: DNSBP - Sound Blaster Pro driver 121 | /// 122 | /// Only VGA, PCM (DNPCS2/DNSBP), and MIDI (DNPCS/DNMID) drivers are remapped. 123 | /// 124 | static public void RemapDrivers(State state, IMemory memory) { 125 | CurrentDriver = state.AX; 126 | ushort? newSegment = ComputeNewSegment(CurrentDriver); 127 | if (newSegment != null) { 128 | DisableAllocatorLimit(state, memory); 129 | DriverOverrideHappened = true; 130 | memory.UInt16[state.DS, 0x39B9] = (ushort)(newSegment.Value + 0x10); 131 | } else { 132 | DriverOverrideHappened = false; 133 | } 134 | } 135 | 136 | /// 137 | /// Computes the target segment address for a driver based on its identifier. 138 | /// 139 | /// The driver index from the AX register. 140 | /// 141 | /// The target segment address for remapping, or null if the driver should not be remapped. 142 | /// 143 | /// 144 | /// Only VGA, PCM audio, and MIDI music drivers are remapped to clean boundaries: 145 | /// 146 | /// DNVGA → 0xD000 (DRIVER1_SEGMENT) 147 | /// DNPCS2, DNSBP → 0xE000 (DRIVER2_SEGMENT) 148 | /// DNPCS, DNMID → 0xF000 (DRIVER3_SEGMENT) 149 | /// 150 | /// Other drivers are left at their original allocations. 151 | /// 152 | private static ushort? ComputeNewSegment(int driverId) { 153 | switch (driverId) { 154 | // VGA Driver Segment 155 | case (int)DriverIndex.DNVGA: return DRIVER1_SEGMENT; 156 | // PCM Driver segment 157 | case (int)DriverIndex.DNPCS2: return DRIVER2_SEGMENT; 158 | case (int)DriverIndex.DNSBP: return DRIVER2_SEGMENT; 159 | // Music driver segment 160 | case (int)DriverIndex.DNPCS: return DRIVER3_SEGMENT; 161 | case (int)DriverIndex.DNMID: return DRIVER3_SEGMENT; 162 | } 163 | 164 | return null; 165 | } 166 | 167 | /// 168 | /// Disables the memory allocator's segment limit to allow driver loading above 0xA000 (VRAM boundary). 169 | /// 170 | /// CPU state providing the DS register value. 171 | /// Memory interface for reading and writing allocator state. 172 | /// 173 | /// Backs up the original allocator state and sets the limit to 0xFFFF, allowing allocation 174 | /// in the high memory region. This is necessary because drivers need to be loaded above the 175 | /// video RAM at 0xA000, which is normally restricted by the allocator. 176 | /// 177 | public static void DisableAllocatorLimit(State state, IMemory memory) { 178 | InitialFreeLimit = memory.UInt16[state.DS, 0xce68]; 179 | // Override free limit so that allocator accepts to let us load above 0xA000 180 | memory.UInt16[state.DS, 0xce68] = 0xFFFF; 181 | // Backup the allocator last free segment and offset 182 | InitialFreeOffset = memory.UInt16[state.DS, 0x39B7]; 183 | InitialFreeSegment = memory.UInt16[state.DS, 0x39B9]; 184 | } 185 | 186 | /// 187 | /// Restores the memory allocator to its original state after driver remapping is complete. 188 | /// Should be called at the end of CS1:E57B (address CS1:E593). 189 | /// 190 | /// CPU state providing the DS register value. 191 | /// Memory interface for writing allocator state. 192 | /// 193 | /// Restores the allocator limit and free pointers to their original values, allowing 194 | /// normal memory allocation to continue in the region below 0xA000. Only performs 195 | /// restoration if a driver override actually occurred. 196 | /// 197 | public static void ResetAllocator(State state, IMemory memory) { 198 | if (!DriverOverrideHappened) { 199 | return; 200 | } 201 | 202 | // Restore allocator state to what it was so that allocation can continue in memory below 0xA000 203 | memory.UInt16[state.DS, 0xce68] = InitialFreeLimit; 204 | memory.UInt16[state.DS, 0x39B7] = InitialFreeOffset; 205 | memory.UInt16[state.DS, 0x39B9] = InitialFreeSegment; 206 | } 207 | 208 | /// 209 | /// Reads and auto-registers driver function table entries for Spice86 function tracing. 210 | /// Should be injected at address CS1:E589. 211 | /// 212 | /// CPU state containing AX (segment), SI (table offset), and CX (function count) registers. 213 | /// Memory interface for reading the driver's function table. 214 | /// Helper for registering function definitions with Spice86. 215 | /// 216 | /// 217 | /// Parses the driver's exported function table and automatically defines entry points and 218 | /// their internal implementations in Spice86's function information database. This enables 219 | /// better tracing, debugging, and potential overriding of driver functions. 220 | /// 221 | /// 222 | /// The function table is an array of near pointers, often pointing to JMP instructions 223 | /// that redirect to the actual implementation. This method decodes both the entry point 224 | /// and the jump target to create complete function definitions. 225 | /// 226 | /// 227 | /// Supports both near (0xE9) and short (0xEB) jump instructions. 228 | /// 229 | /// 230 | public static void ReadDriverFunctionTable(State state, IMemory memory, CSharpOverrideHelper cSharpOverrideHelper) { 231 | _driverNames.TryGetValue(CurrentDriver, out string? driverName); 232 | if (driverName == null) { 233 | return; 234 | } 235 | 236 | ushort segment = (ushort)(state.AX - 0x10); 237 | // -2 because SI points to the first segment but we want the offset. 238 | ushort functionTableEntryOffset = (ushort)(state.SI - 2); 239 | int numberOfFunctions = state.CX; 240 | for (int i = 0; i < numberOfFunctions; i++) { 241 | ushort pointerTableOffset = memory.UInt16[state.DS, (ushort)(functionTableEntryOffset + i * 4)]; 242 | SegmentedAddress entryAddress = new SegmentedAddress(segment, pointerTableOffset); 243 | DefineFunctionIfNotPresent(entryAddress, $"{driverName}_entry_{i.ToString("D2")}", cSharpOverrideHelper); 244 | // Parse the jump to create the target function address in the driver 245 | SegmentedAddress? jumpTargetAddress = null; 246 | if (memory.UInt8[entryAddress.Linear] == 0xE9) { 247 | // 16bit offset 248 | short jumpDisp = (short)memory.UInt16[entryAddress.Linear + 1]; 249 | ushort jumpOffset = (ushort)(entryAddress.Offset + 3 + jumpDisp); 250 | jumpTargetAddress = new SegmentedAddress(segment, jumpOffset); 251 | } 252 | 253 | if (memory.UInt8[entryAddress.Linear] == 0xEB) { 254 | // 8 bit offset 255 | sbyte jumpDisp = (sbyte)memory.UInt8[entryAddress.Linear + 1]; 256 | ushort jumpOffset = (ushort)(entryAddress.Offset + 2 + jumpDisp); 257 | jumpTargetAddress = new SegmentedAddress(segment, jumpOffset); 258 | } 259 | 260 | DefineFunctionIfNotPresent(jumpTargetAddress, $"{driverName}_internal_function_{i.ToString("D2")}", 261 | cSharpOverrideHelper); 262 | } 263 | } 264 | 265 | /// 266 | /// Defines a function in Spice86's function information database if it doesn't already exist. 267 | /// 268 | /// The segmented address of the function, or null to skip registration. 269 | /// The symbolic name to assign to the function for debugging and tracing. 270 | /// Helper for registering the function definition. 271 | /// 272 | /// Prevents duplicate function definitions by checking if the address is already registered. 273 | /// Does nothing if the address is null. 274 | /// 275 | private static void DefineFunctionIfNotPresent(SegmentedAddress? address, string name, 276 | CSharpOverrideHelper cSharpOverrideHelper) { 277 | if (address == null) { 278 | return; 279 | } 280 | 281 | if (cSharpOverrideHelper.FunctionInformations.ContainsKey(address.Value)) { 282 | return; 283 | } 284 | cSharpOverrideHelper.DefineFunction(address.Value.Segment, address.Value.Offset, name); 285 | } 286 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Cryogenic 2 | 3 | Thank you for your interest in contributing to Cryogenic! This document provides guidelines and information to help you contribute effectively to the project. 4 | 5 | ## Table of Contents 6 | 7 | - [Code of Conduct](#code-of-conduct) 8 | - [Getting Started](#getting-started) 9 | - [Development Environment](#development-environment) 10 | - [Project Architecture](#project-architecture) 11 | - [Contributing Guidelines](#contributing-guidelines) 12 | - [Coding Conventions](#coding-conventions) 13 | - [Reverse Engineering Workflow](#reverse-engineering-workflow) 14 | - [Testing Your Changes](#testing-your-changes) 15 | - [Submitting Changes](#submitting-changes) 16 | - [Communication](#communication) 17 | 18 | ## Code of Conduct 19 | 20 | We are committed to providing a welcoming and inclusive environment for all contributors. Please: 21 | 22 | - Be respectful and constructive in all interactions 23 | - Welcome newcomers and help them get started 24 | - Focus on what is best for the community and the project 25 | - Show empathy towards other community members 26 | - Provide and gracefully accept constructive feedback 27 | 28 | ## Getting Started 29 | 30 | ### Prerequisites 31 | 32 | Before you begin, ensure you have: 33 | 34 | 1. **.NET 8 SDK** - Download from [dotnet.microsoft.com](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 35 | 2. **Dune CD Version 3.7** - You must obtain DNCDPRG.EXE and DUNE.DAT separately (copyrighted material) 36 | 3. **Git** - For version control 37 | 4. **A code editor** - Visual Studio, VS Code, or Rider recommended 38 | 39 | ### Verify Your Executable 40 | 41 | Ensure your DNCDPRG.EXE has the correct SHA256 hash: 42 | ``` 43 | 5f30aeb84d67cf2e053a83c09c2890f010f2e25ee877ebec58ea15c5b30cfff9 44 | ``` 45 | 46 | You can verify with PowerShell: 47 | ```powershell 48 | Get-FileHash DNCDPRG.EXE -Algorithm SHA256 49 | ``` 50 | 51 | Or on Linux/Mac: 52 | ```bash 53 | sha256sum DNCDPRG.EXE 54 | ``` 55 | 56 | ### Fork and Clone 57 | 58 | 1. Fork the repository on GitHub 59 | 2. Clone your fork locally: 60 | ```bash 61 | git clone https://github.com/YOUR-USERNAME/Cryogenic.git 62 | cd Cryogenic 63 | ``` 64 | 65 | 3. Add the upstream repository: 66 | ```bash 67 | git remote add upstream https://github.com/OpenRakis/Cryogenic.git 68 | ``` 69 | 70 | 4. Build the project: 71 | ```bash 72 | cd src 73 | dotnet build 74 | ``` 75 | 76 | 5. Test that it runs: 77 | ```bash 78 | dotnet run --Exe /path/to/DNCDPRG.EXE --UseCodeOverride true -p 4096 79 | ``` 80 | 81 | ## Development Environment 82 | 83 | ### Recommended Tools 84 | 85 | - **IDE**: Visual Studio 2022, JetBrains Rider, or VS Code with C# extensions 86 | - **Debugger**: Built-in IDE debugger for C# code 87 | - **Ghidra**: For static analysis of assembly code (optional but recommended) 88 | - **Hex Editor**: For examining data files (optional) 89 | 90 | ### Project Structure 91 | 92 | ``` 93 | Cryogenic/ 94 | ├── src/ 95 | │ └── Cryogenic/ 96 | │ ├── Program.cs # Entry point 97 | │ ├── DuneCdOverrideSupplier.cs # Override registration 98 | │ ├── DriverLoadToolbox.cs # Driver remapping utilities 99 | │ ├── Overrides/ # C# implementations 100 | │ │ ├── Overrides.cs # Main override class (partial) 101 | │ │ ├── VgaDriverCode.cs # VGA driver overrides 102 | │ │ ├── MenuCode.cs # Menu system overrides 103 | │ │ ├── DialoguesCode.cs # Dialogue system overrides 104 | │ │ └── ... 105 | │ ├── Generated/ # Auto-generated properties for globals 106 | │ └── Globals/ # Game state accessors 107 | ├── doc/ # Documentation and screenshots 108 | ├── dump/ # Memory dumps and analysis configs 109 | └── README.md 110 | ``` 111 | 112 | ## Project Architecture 113 | 114 | ### How Cryogenic Works 115 | 116 | Cryogenic uses **Spice86** to run the original DNCDPRG.EXE while selectively replacing assembly routines with C# implementations. This hybrid approach allows: 117 | 118 | 1. **Incremental rewriting** - Replace one function at a time 119 | 2. **Immediate testing** - Run the game after each change 120 | 3. **Behavioral verification** - Compare C# output with original assembly 121 | 122 | ### Key Concepts 123 | 124 | #### Segments 125 | 126 | Memory segments are the foundation of address translation: 127 | 128 | - `cs1 = 0x1000` - Main game code 129 | - `cs2, cs3, cs4` - Mapped driver segments 130 | - `cs5 = 0xF000` - BIOS/IRQ handlers 131 | 132 | **Important**: Never change segment values without auditing ALL `DefineFunction` and `DoOnTopOfInstruction` calls. 133 | 134 | #### Override Registration 135 | 136 | Overrides are registered in `Overrides.DefineOverrides()` using: 137 | 138 | - `DefineFunction(segment, offset, method)` - Replaces identified functions (note: Dune sometimes modifies the stack to change return addresses instead of using CALL instructions) 139 | - `DoOnTopOfInstruction(segment, offset, method)` - Inline hooks 140 | 141 | Example: 142 | ```csharp 143 | DefineFunction(cs1, 0xC085, SetBackBufferAsActiveFrameBuffer_1000_C085_01C085, false); 144 | ``` 145 | 146 | #### Return Methods 147 | 148 | Use the correct return method based on the original assembly: 149 | 150 | - `NearRet()` - For near CALL/RET (within same segment) 151 | - `FarRet()` - For far CALL/RET (across segments) 152 | 153 | Using the wrong one will corrupt the stack! 154 | 155 | ## Contributing Guidelines 156 | 157 | ### Ways to Contribute 158 | 159 | #### 1. Reverse Engineering 160 | 161 | Analyze assembly code and create C# implementations. 162 | 163 | **Skills needed**: Assembly language, debugging, patience 164 | 165 | **Example tasks**: 166 | - Identify function boundaries in disassembly 167 | - Document function parameters and return values 168 | - Implement C# equivalent that produces identical behavior 169 | 170 | #### 2. Documentation 171 | 172 | Document code, game mechanics, and project processes. 173 | 174 | **Skills needed**: Technical writing, understanding of the codebase 175 | 176 | **Example tasks**: 177 | - Add XML doc comments to override methods 178 | - Document game data structures 179 | - Create tutorials for new contributors 180 | - Explain complex algorithms 181 | 182 | #### 3. Testing & Validation 183 | 184 | Play the game and verify correct behavior. 185 | 186 | **Skills needed**: Attention to detail, systematic testing 187 | 188 | **Example tasks**: 189 | - Test specific game scenarios 190 | - Verify that overrides match original behavior 191 | - Report discrepancies or bugs 192 | - Create test cases 193 | 194 | #### 4. Code Quality 195 | 196 | Improve existing C# implementations. 197 | 198 | **Skills needed**: C# programming, refactoring 199 | 200 | **Example tasks**: 201 | - Refactor complex methods for clarity 202 | - Add error handling 203 | - Improve performance 204 | - Remove code duplication 205 | 206 | ## Coding Conventions 207 | 208 | ### C# Style 209 | 210 | Follow standard C# conventions with these project-specific rules: 211 | 212 | 1. **Keep generated naming** - Override methods use pattern `{Segment}_{Offset}_{Linear}` (e.g., `SetBackBufferAsActiveFrameBuffer_1000_C085_01C085`) 213 | 214 | 2. **Use provided accessors** - Access game state through `globalsOnDs` and `globalsOnCsSegment0x2538` instead of manual pointer math 215 | 216 | 3. **Document extensively** - Explain what the override does, not just how: 217 | ```csharp 218 | /// 219 | /// Switches the active framebuffer to the back buffer for double-buffered rendering. 220 | /// Original assembly at CS1:C085. 221 | /// 222 | public void SetBackBufferAsActiveFrameBuffer_1000_C085_01C085() { 223 | // Implementation 224 | NearRet(); 225 | } 226 | ``` 227 | 228 | 4. **Handle edge cases** - Dune code often has surprising edge cases. Document and handle them: 229 | ```csharp 230 | // Original assembly has special handling for value 0xFFFF 231 | if (value == 0xFFFF) { 232 | // Special case logic 233 | } 234 | ``` 235 | 236 | 5. **Logging** - Use `_loggerService.Debug` for diagnostics: 237 | ```csharp 238 | _loggerService.Debug("Loading dialogue {DialogueId}", dialogueId); 239 | ``` 240 | 241 | ### Override Implementation Guidelines 242 | 243 | 1. **Match original behavior exactly** - Dune is hand-written assembly; we must achieve identical behavior, not necessarily byte-for-byte compatibility 244 | 2. **Test thoroughly** - Play the game and verify the override works in all scenarios 245 | 3. **Add safety checks** - Throw `FailAsUntested` for unobserved code paths 246 | 4. **Keep it simple** - Don't over-engineer; match the original logic 247 | 5. **Use correct return** - `NearRet()` or `FarRet()` based on original instruction 248 | 249 | ### File Organization 250 | 251 | - **Keep related overrides together** - VGA code in `VgaDriverCode.cs`, menu code in `MenuCode.cs`, etc. 252 | - **Extend partial classes** - Add new overrides to `Overrides` partial class in domain-specific files 253 | - **Don't edit Generated/** - Add custom code in `Globals/Extra*` or new override files 254 | 255 | ## Reverse Engineering Workflow 256 | 257 | ### Typical Process 258 | 259 | 1. **Identify a function** in the assembly code 260 | 2. **Understand its behavior** through static analysis or runtime tracing 261 | 3. **Document parameters and return values** 262 | 4. **Implement in C#** maintaining exact behavior 263 | 5. **Register the override** in `DefineOverrides()` 264 | 6. **Test thoroughly** by playing the game 265 | 7. **Document** with comments explaining the function's purpose 266 | 267 | ### Tools and Techniques 268 | 269 | #### Using Spice86's CFGCPU 270 | 271 | Spice86's CFGCPU (Control Flow Graph CPU) is the recommended approach for analyzing Dune's code, as it: 272 | - Handles self-modifying code that Dune uses 273 | - Works reliably with 16-bit x86 assembly 274 | - Is compatible with C# overrides 275 | - Already supports Dune and other games 276 | 277 | **Note**: The Ghidra plugin is largely abandoned and has numerous bugs with 16-bit x86 support. CFGCPU will eventually enable automated code generation. 278 | 279 | #### Using Spice86 Debugger 280 | 281 | 1. Run with debugger enabled 282 | 2. Set breakpoints at function entry 283 | 3. Inspect registers and memory 284 | 4. Step through execution 285 | 5. Compare with your C# implementation 286 | 287 | #### Memory Dumps 288 | 289 | The project includes memory dump functionality. Enable in `DefineMemoryDumpsMapping()` to capture game state at specific points. 290 | 291 | ### Example: Implementing a Simple Override 292 | 293 | Let's say you've identified a function at CS1:1234 that clears a buffer: 294 | 295 | 1. **Analyze the assembly**: 296 | ```asm 297 | CS1:1234 mov cx, 0x100 ; Counter 298 | CS1:1237 mov di, 0x0 ; Destination offset 299 | CS1:123A xor ax, ax ; Value to write (0) 300 | CS1:123C rep stosw ; Fill buffer 301 | CS1:123E ret ; Return 302 | ``` 303 | 304 | 2. **Implement in C#**: 305 | ```csharp 306 | /// 307 | /// Clears a 256-word buffer at ES:0000. 308 | /// Original assembly at CS1:1234. 309 | /// 310 | public void ClearBuffer_1000_1234_011234() { 311 | var buffer = MemoryUtils.ToMemorySpan(_state.Memory, _state.ES.BaseAddress, 512); 312 | buffer.Fill(0); 313 | NearRet(); 314 | } 315 | ``` 316 | 317 | 3. **Register in DefineOverrides()**: 318 | ```csharp 319 | DefineFunction(cs1, 0x1234, ClearBuffer_1000_1234_011234, false); 320 | ``` 321 | 322 | 4. **Test**: Run the game and verify the buffer is cleared at the right time 323 | 324 | 5. **Document**: Add comments explaining when and why this is called 325 | 326 | ## Testing Your Changes 327 | 328 | ### Manual Testing 329 | 330 | 1. **Build the project**: 331 | ```bash 332 | dotnet build 333 | ``` 334 | 335 | 2. **Run the game**: 336 | ```bash 337 | dotnet run --Exe /path/to/DNCDPRG.EXE --UseCodeOverride true -p 4096 338 | ``` 339 | 340 | 3. **Test specific scenarios** that exercise your code: 341 | - Main menu navigation 342 | - Dialogue sequences 343 | - Combat 344 | - Resource management 345 | - Save/Load 346 | 347 | 4. **Compare with original**: If possible, run the original game and compare behavior 348 | 349 | ### Important Test Cases 350 | 351 | - **Main menu** - Navigation, option selection 352 | - **New game start** - Initial setup and intro sequence 353 | - **Dialogue system** - Character conversations 354 | - **Map exploration** - Movement and interactions 355 | - **Resource management** - Spice collection and allocation 356 | - **Combat** - Battle sequences 357 | - **Save/Load** - Game state persistence 358 | 359 | ### Debugging 360 | 361 | Use your IDE's debugger to step through C# code: 362 | 363 | 1. Set breakpoints in your override methods 364 | 2. Run with debugger attached 365 | 3. Inspect variables and execution flow 366 | 4. Verify behavior matches expectations 367 | 368 | ## Submitting Changes 369 | 370 | ### Before You Submit 371 | 372 | - [ ] Code builds without errors 373 | - [ ] Game runs and your changes work as expected 374 | - [ ] You've tested relevant game scenarios 375 | - [ ] Code follows project conventions 376 | - [ ] You've added appropriate comments/documentation 377 | - [ ] No unrelated changes or debugging code left in 378 | 379 | ### Creating a Pull Request 380 | 381 | 1. **Create a feature branch**: 382 | ```bash 383 | git checkout -b feature/your-feature-name 384 | ``` 385 | 386 | 2. **Make your changes and commit**: 387 | ```bash 388 | git add . 389 | git commit -m "Brief description of changes" 390 | ``` 391 | 392 | 3. **Push to your fork**: 393 | ```bash 394 | git push origin feature/your-feature-name 395 | ``` 396 | 397 | 4. **Open a Pull Request** on GitHub with: 398 | - **Clear title** describing the change 399 | - **Description** explaining what and why 400 | - **Testing notes** describing how you verified it works 401 | - **Screenshots** if relevant (UI changes, bug fixes) 402 | 403 | ### Pull Request Template 404 | 405 | ```markdown 406 | ## Description 407 | Brief description of changes 408 | 409 | ## Changes Made 410 | - List of specific changes 411 | - What was added/modified/removed 412 | 413 | ## Testing 414 | - [ ] Built successfully 415 | - [ ] Tested in-game 416 | - [ ] Verified correct behavior 417 | 418 | ## Testing Notes 419 | Describe how you tested and what scenarios work correctly 420 | 421 | ## Related Issues 422 | Fixes #123 (if applicable) 423 | ``` 424 | 425 | ### Code Review 426 | 427 | Maintainers will review your PR and may: 428 | - Request changes or clarifications 429 | - Suggest improvements 430 | - Merge when everything looks good 431 | 432 | Be patient and responsive to feedback! 433 | 434 | ## Communication 435 | 436 | ### GitHub Issues 437 | 438 | Use GitHub issues for: 439 | - Bug reports 440 | - Feature requests 441 | - Questions about the code 442 | - Proposals for significant changes 443 | 444 | ### Discussions 445 | 446 | For general discussion, questions, or showing off progress, use GitHub Discussions. 447 | 448 | ### Finding Tasks 449 | 450 | Look for issues labeled: 451 | - `good first issue` - Good for newcomers 452 | - `help wanted` - Community help needed 453 | - `documentation` - Documentation tasks 454 | - `reverse-engineering` - RE work needed 455 | 456 | ## Additional Resources 457 | 458 | - **Spice86 Documentation**: [github.com/OpenRakis/Spice86](https://github.com/OpenRakis/Spice86) 459 | - **Project Website**: [openrakis.github.io/Cryogenic](https://openrakis.github.io/Cryogenic) 460 | - **Copilot Instructions**: See `.github/copilot-instructions.md` for detailed architectural notes 461 | 462 | ## Questions? 463 | 464 | If you have questions: 465 | 1. Check existing documentation and issues 466 | 2. Open a GitHub issue with the `question` label 467 | 3. Reach out in GitHub Discussions 468 | 469 | Thank you for contributing to Cryogenic! Your efforts help preserve and modernize this classic game for future generations. 470 | -------------------------------------------------------------------------------- /src/Cryogenic/Overrides/VgaDriverCode.cs: -------------------------------------------------------------------------------- 1 | namespace Cryogenic.Overrides; 2 | 3 | using Spice86.Core.Emulator.Devices.Video; 4 | using Spice86.Core.Emulator.Memory; 5 | using Spice86.Shared.Emulator.Errors; 6 | using Spice86.Shared.Utils; 7 | using Spice86.Shared.Interfaces; 8 | 9 | using System; 10 | 11 | /// 12 | /// Partial class containing VGA graphics driver (DNVGA) function overrides. 13 | /// 14 | /// 15 | /// 16 | /// This file provides C# implementations for the custom VGA driver used by Dune CD. 17 | /// The driver is loaded at segment 0xD000 (cs2) and provides hardware-accelerated 18 | /// graphics operations including blitting, palette management, retrace synchronization, 19 | /// and specialized rendering for maps and globe views. 20 | /// 21 | /// 22 | /// Method names contain underscores to separate segment, offset, and linear addresses 23 | /// for traceability back to the original DOS disassembly. 24 | /// 25 | /// 26 | public partial class Overrides { 27 | /// 28 | /// Offset in video memory where the image under the mouse cursor is saved (0xFA00). 29 | /// 30 | private const ushort IMAGE_UNDER_MOUSE_CURSOR_START = 0xFA00; 31 | 32 | /// 33 | /// Registers VGA driver function overrides with Spice86. 34 | /// 35 | /// 36 | /// Defines overrides for DNVGA driver functions including mode setting, blitting, 37 | /// palette operations, mouse cursor handling, retrace synchronization, and specialized 38 | /// rendering routines for maps and textures. 39 | /// 40 | public void DefineVgaDriverCodeOverrides() { 41 | DefineFunction(cs2, 0x100, "VgaFunc00SetMode"); 42 | DefineFunction(cs2, 0x103, VgaFunc01GetInfoInAxCxBp_334B_0103_0335B3); 43 | DefineFunction(cs2, 0x109, "VgaFunc03DrawMouseCursor"); 44 | DefineFunction(cs2, 0x10C, VgaFunc04RestoreImageUnderMouseCursor_334B_010C_0335BC); 45 | DefineFunction(cs2, 0x10F, "VgaFunc05Blit"); 46 | DefineFunction(cs2, 0x118, VgaFunc08FillWithZeroFor64000AtES_334B_0118_0335C8); 47 | DefineFunction(cs2, 0x121, VgaFunc11MemcpyDSToESFor64000_334B_0121_0335D1); 48 | DefineFunction(cs2, 0x124, "VgaFunc12CopyRectangle"); 49 | DefineFunction(cs2, 0x12A, VgaFunc14CopySquareOfPixelsSiIsSourceSegment_334B_012A_0335DA); 50 | DefineFunction(cs2, 0x12D, VgaFunc15MemcpyDSToESFor64000_334B_012D_0335DD); 51 | DefineFunction(cs2, 0x130, VgaFunc16CopySquareOfPixels_334B_0130_0335E0); 52 | DefineFunction(cs2, 0x133, "VgaFunc17CopyframebufferExplodeAndCenter"); 53 | DefineFunction(cs2, 0x13C, VgaFunc20NoOp_334B_013C_0335EC); 54 | DefineFunction(cs2, 0x13F, "VgaFunc21SetPixel"); 55 | DefineFunction(cs2, 0x163, VgaFunc33UpdateVgaOffset01A3FromLineNumberAsAx_334B_0163_033613); 56 | DefineFunction(cs2, 0x16C, VgaFunc36GenerateTextureOutBP_334B_016C_03361C); 57 | DefineFunction(cs2, 0x17B, "VgaFunc41CopyPalette2toPalette1"); 58 | DefineFunction(cs2, 0x9B8, WaitForRetrace_334B_09B8_033E68); 59 | DefineFunction(cs2, 0xA21, SetBxCxPaletteRelated_334B_0A21_033ED1); 60 | DefineFunction(cs2, 0xA58, CopyCsRamB5FToB2F_334B_0A58_033F08); 61 | DefineFunction(cs2, 0xB68, LoadPaletteInVgaDac_334B_0B68_034018); 62 | DefineFunction(cs2, 0xC10, SetDiFromXYCordsDxBx_334B_0C10_0340C0); 63 | DefineFunction(cs2, 0x1B7C, MemcpyDSToESFor64000_334B_1B7C_03502C); 64 | DefineFunction(cs2, 0x1B8E, CopySquareOfPixels_334B_1B8E_03503E); 65 | 66 | // called in globe, without it globe rotation works but stutters when clicking 67 | DefineFunction(cs2, 0x1D07, "UnknownGlobeRelated"); 68 | DefineFunction(cs2, 0x1D5A, UnknownGlobeInitRelated_334B_1D5A_03520A); 69 | DefineFunction(cs2, 0x2025, "UnknownMapRelated"); 70 | DefineFunction(cs2, 0x2343, CopyMapBlock_334B_2343_0357F3); 71 | DefineFunction(cs2, 0x253D, RetraceRelatedCalledOnEnterGlobe_334B_253D_0359ED); 72 | DefineFunction(cs2, 0x2572, WaitForRetraceInTransitions_334B_2572_035A22); 73 | DefineFunction(cs2, 0x261D, WaitForRetraceDuringIntroVideo_334B_261D_035ACD); 74 | DefineFunction(cs2, 0x32C1, "GenerateMenuTransitionFrame"); 75 | } 76 | 77 | public Action CopyCsRamB5FToB2F_334B_0A58_033F08(int gotoAddress) { 78 | _loggerService.Debug("CopyCsRamB5FToB2F"); 79 | 80 | // No jump 81 | uint sourceAddress = MemoryUtils.ToPhysicalAddress(CS, 0x5BF); 82 | uint destinationAddress = MemoryUtils.ToPhysicalAddress(CS, 0x2BF); 83 | 84 | // 768 times (3 blocks of 256) 85 | Memory.MemCopy(sourceAddress, destinationAddress, 768); 86 | return NearRet(); 87 | } 88 | 89 | public Action CopyMapBlock_334B_2343_0357F3(int gotoAddress) { 90 | // 37 lines in ghidra 91 | ushort blockSize = CX; 92 | uint baseSourceAddress = MemoryUtils.ToPhysicalAddress(DS, SI); 93 | uint baseDestinationAddress = MemoryUtils.ToPhysicalAddress(ES, DI); 94 | _loggerService.Debug("unknownMapCopyMapBlock blockSize={@BlockSize}, baseSourceAddress:{@BaseSourceAddress},baseDestinationAddress:{@BaseDestinationAddress}", blockSize, baseSourceAddress, baseDestinationAddress); 95 | for (int i = 0; i < 4; i++) { 96 | for (int j = 0; j < blockSize; j++) { 97 | byte value = Memory.UInt8[baseSourceAddress + j + 400 * i]; 98 | value = (byte)(((value >> 4) & 0xF) + 0x10); 99 | Memory.UInt8[baseDestinationAddress + j + 320 * i] = value; 100 | } 101 | } 102 | 103 | // point to next block 104 | SI = (ushort)(SI + 4 * 400); 105 | DI = (ushort)(DI + 4 * 320); 106 | return NearRet(); 107 | } 108 | 109 | public Action VgaFunc16CopySquareOfPixels_334B_0130_0335E0(int gotoAddress) { 110 | // 26F0E 111 | return CopySquareOfPixels_334B_1B8E_03503E(0); 112 | } 113 | 114 | public Action VgaFunc14CopySquareOfPixelsSiIsSourceSegment_334B_012A_0335DA(int gotoAddress) { 115 | // 26F0C 116 | DS = SI; 117 | return CopySquareOfPixels_334B_1B8E_03503E(0); 118 | } 119 | 120 | public Action VgaFunc08FillWithZeroFor64000AtES_334B_0118_0335C8(int gotoAddress) { 121 | // 26D77 122 | uint address = MemoryUtils.ToPhysicalAddress(ES, 0); 123 | _loggerService.Debug("fillWithZeroFor64000AtES address:{@Address}", address); 124 | Memory.Memset8(address, 0, 64000); 125 | return FarRet(); 126 | } 127 | 128 | // when disabled floors disappear in some rooms. 129 | public Action VgaFunc36GenerateTextureOutBP_334B_016C_03361C(int gotoAddress) { 130 | // 28D69, 30 lines in ghidra 131 | uint destinationBaseAddress = MemoryUtils.ToPhysicalAddress(ES, 0); 132 | ushort initialColor = AX; 133 | ushort colorIncrement = DI; 134 | ushort xorNoise = BP; 135 | ushort xorNoisePattern = SI; 136 | ushort length = CX; 137 | SetDiFromXYCordsDxBx_334B_0C10_0340C0(0); 138 | ushort destinationOffsetAddress = DI; 139 | int direction = DirectionFlag ? -1 : 1; 140 | if (_loggerService.IsEnabled(Serilog.Events.LogEventLevel.Debug)) { 141 | _loggerService.Debug("generateFloors xy:{@X},{@Y} destinationBaseAddress:{@DestinationBaseAddress},destinationOffsetAddress:{@DestinationOffsetAddress}," + "colorIncrement:{@ColorIncrement},initialColor:{@InitialColor},xorNoise:{@XorNoise},xorNoisePattern:{@XorNoisePattern},length:{@Length},direction:{@Direction}", DX, BX, destinationBaseAddress, 142 | destinationOffsetAddress, colorIncrement, initialColor, xorNoise, xorNoisePattern, length, direction); 143 | } 144 | 145 | uint destinationAddress = destinationBaseAddress + destinationOffsetAddress; 146 | for (int i = 0; i < length; i++) { 147 | bool shouldXor = (xorNoise & 1) == 1; 148 | xorNoise >>= 1; 149 | if (shouldXor) { 150 | xorNoise ^= xorNoisePattern; 151 | } 152 | 153 | byte valueToStore = (byte)((uint)ConvertUtils.Int8((byte)((xorNoise & 0x3) - 1)) + (uint)ConvertUtils.Int8((byte)(initialColor >> 8))); 154 | Memory.UInt8[destinationAddress + i * direction] = valueToStore; 155 | initialColor += colorIncrement; 156 | } 157 | 158 | // Needed for next calls 159 | BP = xorNoise; 160 | return FarRet(); 161 | } 162 | 163 | public ushort GetBaseSegment() { 164 | return cs2; 165 | } 166 | 167 | public Action VgaFunc01GetInfoInAxCxBp_334B_0103_0335B3(int gotoAddress) { 168 | // 25D59 169 | _loggerService.Debug("getInfoInAxCxBp"); 170 | AX = MemoryMap.GraphicVideoMemorySegment; 171 | CX = IMAGE_UNDER_MOUSE_CURSOR_START; 172 | BP = 0; 173 | return FarRet(); 174 | } 175 | 176 | public Action LoadPaletteInVgaDac_334B_0B68_034018(int gotoAddress) { 177 | // No jump, 49 lines in ghidra 178 | try { 179 | VgaCard vgaCard = Machine.VgaCard; 180 | uint baseAddress = MemoryUtils.ToPhysicalAddress(ES, DX); 181 | byte writeIndex = BL; 182 | ushort numberOfColors = CX; 183 | byte loadMode = globalsOnCsSegment0X2538.Get2538_01BD_Byte8_PaletteLoadMode(); 184 | _loggerService.Debug("loadPaletteInVgaDac, baseAddress:{@BaseAddress}, writeIndex:{@Writeindex}, loadMode:{@LoadMode}, numberOfColors:{@NumberOfColors}", baseAddress, writeIndex, loadMode, numberOfColors); 185 | IVideoState videoState = Machine.VgaRegisters; 186 | videoState.DacRegisters.IndexRegisterWriteMode = writeIndex; 187 | 188 | if (loadMode == 0) { 189 | for (uint i = 0; i < numberOfColors * 3; i++) { 190 | byte value = UInt8[baseAddress + i]; 191 | videoState.DacRegisters.DataRegister = value; 192 | } 193 | } else { 194 | // Untested ... 25f29 in ghidra, 2538:BA9 in dosbox, probably wrong 195 | throw FailAsUntested("This palette code path was converted to C# but never executed..."); 196 | for (ushort i = 0; i < numberOfColors * 3; i += 3) { 197 | byte r = (byte)(UInt8[baseAddress + i] & 0x3F); 198 | byte g = (byte)(UInt8[baseAddress + i + 1] & 0x3F); 199 | byte b = (byte)(UInt8[baseAddress + i + 2] & 0x3F); 200 | byte value = (byte)((r * 5 + g * 9 + b * 2) >> 4); 201 | videoState.DacRegisters.DataRegister = value; 202 | videoState.DacRegisters.DataRegister = value; 203 | videoState.DacRegisters.DataRegister = value; 204 | } 205 | } 206 | 207 | return NearRet(); 208 | } catch (ArgumentOutOfRangeException e) { 209 | throw new UnrecoverableException(e.Message); 210 | } 211 | } 212 | 213 | public Action VgaFunc11MemcpyDSToESFor64000_334B_0121_0335D1(int gotoAddress) { 214 | return MemcpyDSToESFor64000_334B_1B7C_03502C(0); 215 | } 216 | 217 | public Action VgaFunc15MemcpyDSToESFor64000_334B_012D_0335DD(int gotoAddress) { 218 | // 26EFC, seems used when moving rooms 219 | return MemcpyDSToESFor64000_334B_1B7C_03502C(0); 220 | } 221 | 222 | public Action MemcpyDSToESFor64000_334B_1B7C_03502C(int gotoAddress) { 223 | // No jump, 22 lines in ghidra 224 | uint sourceAddress = MemoryUtils.ToPhysicalAddress(DS, 0); 225 | uint destinationAddress = MemoryUtils.ToPhysicalAddress(ES, 0); 226 | _loggerService.Debug("memcpyDSToESFor64000 sourceAddress:{@SourceAddress},destinationAddress:{@DestinationAddress}", sourceAddress, destinationAddress); 227 | Memory.MemCopy(sourceAddress, destinationAddress, 64000); 228 | return FarRet(); 229 | } 230 | 231 | public Action CopySquareOfPixels_334B_1B8E_03503E(int gotoAddress) { 232 | // No jump, 30 instructions 67 lines in ghidra 233 | // warning: we dont set registers at the end but no idea if their values are used or not. 234 | SetDiFromXYCordsDxBx_334B_0C10_0340C0(0); 235 | ushort baseOffsetDi = DI; 236 | uint sourceAddress = MemoryUtils.ToPhysicalAddress(DS, baseOffsetDi); 237 | uint destinationAddress = MemoryUtils.ToPhysicalAddress(ES, baseOffsetDi); 238 | ushort rowCount = BP; 239 | ushort columnCount = AX; 240 | _loggerService.Debug("moveSquareOfPixels sourceBuffer:{@SourceBuffer}, destinationBuffer:{@DestinationBuffer},startX:{@StartX},startY:{@StartY},columnCount:{@ColumnCount},rowCount:{@RowCount}", DS, ES, DX, BX, columnCount, rowCount); 241 | for (ushort y = 0; y < columnCount; y++) { 242 | for (ushort x = 0; x < rowCount; x++) { 243 | int disp = y * 320 + x; 244 | UInt8[destinationAddress + disp] = UInt8[sourceAddress + disp]; 245 | } 246 | } 247 | 248 | return FarRet(); 249 | } 250 | 251 | public Action VgaFunc20NoOp_334B_013C_0335EC(int gotoAddress) { 252 | return FarRet(); 253 | } 254 | 255 | /// 256 | /// Restores image under mouse cursor. No input apart from globals and no output. 257 | /// 258 | public Action VgaFunc04RestoreImageUnderMouseCursor_334B_010C_0335BC(int gotoAddress) { 259 | // 26CC0 260 | ushort mouseCursorAddressInVram = this.globalsOnCsSegment0X2538.Get2538_018A_Word16_MouseCursorAddressInVram(); 261 | ushort columns = this.globalsOnCsSegment0X2538.Get2538_018C_Word16_ColumnsOfMouseCursorCount(); 262 | ushort lines = this.globalsOnCsSegment0X2538.Get2538_018E_Word16_LinesOfMouseCursorCount(); 263 | _loggerService.Debug("restoreImageUnderMouseCursor mouseCursorAddressInVram:{@MouseCursorAddressInVram},columns:{@Columns},lines:{@Lines}", mouseCursorAddressInVram, columns, lines); 264 | uint sourceAddress = MemoryUtils.ToPhysicalAddress(MemoryMap.GraphicVideoMemorySegment, IMAGE_UNDER_MOUSE_CURSOR_START); 265 | uint destinationAddress = MemoryUtils.ToPhysicalAddress(MemoryMap.GraphicVideoMemorySegment, mouseCursorAddressInVram); 266 | for (ushort i = 0; i < lines; i++) { 267 | Memory.MemCopy((uint)(sourceAddress + columns * i), (uint)(destinationAddress + 320 * i), columns); 268 | } 269 | 270 | return FarRet(); 271 | } 272 | 273 | public Action RetraceRelatedCalledOnEnterGlobe_334B_253D_0359ED(int gotoAddress) { 274 | return NearRet(); 275 | } 276 | 277 | public Action SetBxCxPaletteRelated_334B_0A21_033ED1(int gotoAddress) { 278 | // No jump 279 | BX = (ushort)(BX / 3); 280 | ushort unknownValue = CX; 281 | if (unknownValue < 0x300) { 282 | CX = (ushort)(unknownValue / 3); 283 | return NearRet(); 284 | } 285 | 286 | // crashes when executed, but never reached... 287 | CX = 0x100; 288 | return NearRet(); 289 | } 290 | 291 | public Action UnknownGlobeInitRelated_334B_1D5A_03520A(int gotoAddress) { 292 | // no jump 293 | globalsOnCsSegment0X2538.Set2538_1CA6_Word16_UnknownGlobeRelated(DI); 294 | globalsOnCsSegment0X2538.Set2538_1EA6_Word16_UnknownGlobeRelated(0xFEDD); 295 | globalsOnCsSegment0X2538.Set2538_1F29_Word16_UnknownGlobeRelated(0xFE5A); 296 | globalsOnCsSegment0X2538.Set2538_1CAE_Word16_UnknownGlobeRelated(0x6360 - 1); 297 | globalsOnCsSegment0X2538.Set2538_1CB0_Word16_UnknownGlobeRelated(0x6360); 298 | globalsOnCsSegment0X2538.Set2538_1CB2_Word16_UnknownGlobeRelated(0x6360); 299 | DS = SS; 300 | CarryFlag = true; 301 | return FarRet(); 302 | } 303 | 304 | // line number in AX, offset address in 01A3 305 | public Action VgaFunc33UpdateVgaOffset01A3FromLineNumberAsAx_334B_0163_033613(int gotoAddress) { 306 | // 25F86 307 | ushort lineNumber = AX; 308 | this.globalsOnCsSegment0X2538.Set2538_01A3_Word16_VgaOffset((ushort)(lineNumber * 320)); 309 | _loggerService.Debug("updateVgaOffset01A3FromLineNumberAsAx lineNumber:{@LineNumber},vgaOffset01A3:{@VgaOffset01A3}", lineNumber, globalsOnCsSegment0X2538.Get2538_01A3_Word16_VgaOffset()); 310 | return FarRet(); 311 | } 312 | 313 | public Action WaitForRetrace_334B_09B8_033E68(int gotoAddress) { 314 | // no jump, 28 lines in ghidra, part of the function is not executed in the logs and DX is always 3DA. 315 | // Wait for retrace. 316 | Thread.Sleep(15); 317 | State.CarryFlag = true; 318 | return NearRet(); 319 | } 320 | 321 | public Action WaitForRetraceDuringIntroVideo_334B_261D_035ACD(int gotoAddress) { 322 | // Calls 0x2538_2572_278F2 when 01A1 is not 0 which we dont need 323 | return NearRet(); 324 | } 325 | 326 | public Action WaitForRetraceInTransitions_334B_2572_035A22(int gotoAddress) { 327 | // Calls part of 0x2538_253D_278BD which we dont need 328 | return NearRet(); 329 | } 330 | 331 | private Action SetDiFromXYCordsDxBx_334B_0C10_0340C0(int gotoAddress) { 332 | ushort x = DX; 333 | ushort y = BX; 334 | int offset = globalsOnCsSegment0X2538.Get2538_01A3_Word16_VgaOffset(); 335 | if (y >= 200) { 336 | y = 199; 337 | } 338 | 339 | ushort res = (ushort)(320 * y + x + offset); 340 | DI = res; 341 | _loggerService.Debug("setDiFromXYCordsDxBx x:{@X},y:{@Y},offset:{@Offset},res:{@Res}", x, y, offset, res); 342 | return NearRet(); 343 | } 344 | } --------------------------------------------------------------------------------