├── .clang-format ├── .devcontainer └── devcontainer.json ├── .github └── workflows │ ├── ci.yml │ ├── compare.yml │ ├── docker.yml │ ├── export-ghidra.yml │ └── update-mapping.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── config ├── ghidra-user-maps.toml ├── ghidra_ns_to_obj.csv ├── globals.csv ├── implemented.csv ├── mapping.csv ├── order.txt └── stubbed.csv ├── objdiff.json ├── resources ├── placeholder.ico ├── placeholder.png ├── progress.svg ├── progress_dark.svg ├── progress_template.svg └── th06.rc ├── scripts ├── Dockerfile ├── __init__.py ├── build.py ├── coff.py ├── configure.py ├── create_devenv.py ├── create_th06_prefix ├── diff_all_functions.py ├── export_ghidra_database.py ├── export_ghidra_objs.py ├── extract_icon.py ├── gendef.py ├── generate_decompme_toolchain.py ├── generate_detours.py ├── generate_function_decompme.py ├── generate_function_diff.py ├── generate_globals.py ├── generate_objdiff_objs.py ├── generate_stubs.py ├── ghidra │ ├── ExportDecomp.java │ ├── ExportDelinker.java │ ├── ExportFileVersions.java │ ├── ExportToXML.java │ ├── GenerateMapping.java │ ├── ImportFromCsv.java │ ├── ImportFromXml.java │ └── debug.py ├── ghidra_helpers.py ├── icon_extractor.py ├── ninja_syntax.py ├── pefile.py ├── pragma_var_order.cpp ├── test.py ├── th06run.bat ├── th06vars.bat ├── update_decompile_stats.py ├── update_mapping.py └── winhelpers.py ├── src ├── AnmIdx.hpp ├── AnmManager.cpp ├── AnmManager.hpp ├── AnmVm.hpp ├── AsciiManager.cpp ├── AsciiManager.hpp ├── BombData.cpp ├── BombData.hpp ├── BulletData.cpp ├── BulletData.hpp ├── BulletManager.cpp ├── BulletManager.hpp ├── CMyFont.cpp ├── CMyFont.hpp ├── Chain.cpp ├── Chain.hpp ├── ChainPriorities.hpp ├── Controller.cpp ├── Controller.hpp ├── EclManager.cpp ├── EclManager.hpp ├── Effect.hpp ├── EffectManager.cpp ├── EffectManager.hpp ├── Ending.cpp ├── Ending.hpp ├── Enemy.hpp ├── EnemyEclInstr.cpp ├── EnemyEclInstr.hpp ├── EnemyManager.cpp ├── EnemyManager.hpp ├── FileSystem.cpp ├── FileSystem.hpp ├── GameErrorContext.cpp ├── GameErrorContext.hpp ├── GameManager.cpp ├── GameManager.hpp ├── GameWindow.cpp ├── GameWindow.hpp ├── Gui.cpp ├── Gui.hpp ├── ItemManager.cpp ├── ItemManager.hpp ├── MainMenu.cpp ├── MainMenu.hpp ├── MidiOutput.cpp ├── MidiOutput.hpp ├── MusicRoom.cpp ├── MusicRoom.hpp ├── Player.cpp ├── Player.hpp ├── ReplayData.hpp ├── ReplayManager.cpp ├── ReplayManager.hpp ├── ResultScreen.cpp ├── ResultScreen.hpp ├── Rng.cpp ├── Rng.hpp ├── ScreenEffect.cpp ├── ScreenEffect.hpp ├── SoundPlayer.cpp ├── SoundPlayer.hpp ├── Stage.cpp ├── Stage.hpp ├── StageMenu.hpp ├── Supervisor.cpp ├── Supervisor.hpp ├── TextHelper.cpp ├── TextHelper.hpp ├── ZunBool.hpp ├── ZunColor.hpp ├── ZunMath.hpp ├── ZunMemory.hpp ├── ZunResult.hpp ├── ZunTimer.cpp ├── ZunTimer.hpp ├── diffbuild.hpp ├── dllbuild.cpp ├── dxutil.hpp ├── i18n.tpl ├── inttypes.hpp ├── main.cpp ├── pbg3 │ ├── FileAbstraction.cpp │ ├── FileAbstraction.hpp │ ├── IPbg3Parser.cpp │ ├── IPbg3Parser.hpp │ ├── Pbg3Archive.cpp │ ├── Pbg3Archive.hpp │ ├── Pbg3Parser.cpp │ └── Pbg3Parser.hpp ├── stubs.cpp ├── th06.hpp ├── th06.rc ├── utils.cpp ├── utils.hpp ├── zwave.cpp └── zwave.hpp └── tests ├── test_Pbg3Archive.cpp └── tests.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Microsoft 2 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ghcr.io/happyhavoc/th06:master", 3 | "features": {} 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build binary 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | devenv: 11 | runs-on: windows-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Get toolchain from cache 15 | id: cache-toolchain 16 | uses: actions/cache/restore@v3 17 | with: 18 | # A directory to store and save the cache 19 | path: scripts/prefix 20 | # An explicit key for restoring and saving the cache 21 | key: prefix-${{hashFiles('scripts/create_devenv.py')}} 22 | # No need to download here. 23 | lookup-only: true 24 | - if: ${{ steps.cache-toolchain.outputs.cache-hit != 'true' }} 25 | name: Install prefix 26 | id: install_prefix 27 | run: python scripts/create_devenv.py scripts/dls scripts/prefix 28 | - if: always() && steps.cache-toolchain.outputs.cache-hit != 'true' && steps.install_prefix.outcome == 'success' 29 | name: Save toolchain to cache 30 | uses: actions/cache/save@v3 31 | with: 32 | # A directory to store and save the cache 33 | path: scripts/prefix 34 | # An explicit key for restoring and saving the cache 35 | key: ${{ steps.cache-toolchain.outputs.cache-primary-key }} 36 | 37 | build: 38 | needs: ['devenv'] 39 | runs-on: windows-latest 40 | steps: 41 | - uses: actions/checkout@v3 42 | - uses: actions/setup-python@v5 43 | with: 44 | python-version: '3.4' 45 | - name: Get toolchain from cache 46 | id: cache-toolchain 47 | uses: actions/cache/restore@v3 48 | with: 49 | # A directory to store and save the cache 50 | path: scripts/prefix 51 | # An explicit key for restoring and saving the cache 52 | key: prefix-${{hashFiles('scripts/create_devenv.py')}} 53 | # This should never happen, as the devenv step plays before us. 54 | fail-on-cache-miss: true 55 | - name: Build binary 56 | run: python scripts/build.py -j0 57 | - uses: actions/upload-artifact@v4 58 | with: 59 | name: th06e 60 | path: | 61 | build/th06e.exe 62 | build/th06e.pdb 63 | 64 | build-diff: 65 | needs: ['devenv'] 66 | runs-on: windows-latest 67 | steps: 68 | - uses: actions/checkout@v3 69 | - uses: actions/setup-python@v5 70 | with: 71 | python-version: '3.4' 72 | - name: Get toolchain from cache 73 | id: cache-toolchain 74 | uses: actions/cache/restore@v3 75 | with: 76 | # A directory to store and save the cache 77 | path: scripts/prefix 78 | # An explicit key for restoring and saving the cache 79 | key: prefix-${{hashFiles('scripts/create_devenv.py')}} 80 | # This should never happen, as the devenv step plays before us. 81 | fail-on-cache-miss: true 82 | - name: Build binary 83 | run: python scripts/build.py -j0 --build-type objdiffbuild 84 | - uses: actions/upload-artifact@v4 85 | with: 86 | name: th06e-diff 87 | path: | 88 | build/objdiff/reimpl 89 | 90 | build-dll: 91 | needs: ['devenv'] 92 | runs-on: windows-latest 93 | steps: 94 | - uses: actions/checkout@v3 95 | with: 96 | submodules: true 97 | - uses: actions/setup-python@v5 98 | with: 99 | python-version: '3.4' 100 | - name: Get toolchain from cache 101 | id: cache-toolchain 102 | uses: actions/cache/restore@v3 103 | with: 104 | # A directory to store and save the cache 105 | path: scripts/prefix 106 | # An explicit key for restoring and saving the cache 107 | key: prefix-${{hashFiles('scripts/create_devenv.py')}} 108 | # This should never happen, as the devenv step plays before us. 109 | fail-on-cache-miss: true 110 | - name: Build binary 111 | run: python scripts/build.py -j0 --build-type dllbuild 112 | - uses: actions/upload-artifact@v4 113 | with: 114 | name: th06e-dll 115 | path: | 116 | build/th06e.dll 117 | build/th06e.pdb 118 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish devenv docker image 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | # Specify the image name as {owner}/{repo}. 8 | IMAGE_NAME: ${{ github.repository }} 9 | 10 | # Contents: read the contents of the repo (to build the image) 11 | # Packages: write the image to GHCR 12 | # ID-Token: create a token for cosign container signing 13 | permissions: 14 | contents: read 15 | packages: write 16 | id-token: write 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | - name: Install cosign 25 | uses: sigstore/cosign-installer@v3.8.1 26 | with: 27 | cosign-release: 'v2.4.3' 28 | - name: Setup Docker buildx 29 | uses: docker/setup-buildx-action@v3.10.0 30 | with: 31 | version: latest 32 | - name: Log into registry ghcr 33 | uses: docker/login-action@v3.4.0 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | - name: Docker meta 39 | id: meta 40 | uses: docker/metadata-action@v5 41 | with: 42 | images: ghcr.io/${{ env.IMAGE_NAME }} 43 | - name: Build and push Docker image 44 | id: build-and-push 45 | uses: docker/build-push-action@v6.15.0 46 | with: 47 | context: ./scripts 48 | push: true 49 | platforms: linux/amd64 50 | tags: ${{ steps.meta.outputs.tags }} 51 | labels: ${{ steps.meta.outputs.labels }} 52 | - name: Sign the published Docker image 53 | env: 54 | COSIGN_EXPERIMENTAL: "true" 55 | run: cosign sign -y ghcr.io/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} 56 | -------------------------------------------------------------------------------- /.github/workflows/export-ghidra.yml: -------------------------------------------------------------------------------- 1 | name: Export Ghidra Database 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 2 * * *' 7 | 8 | env: 9 | GHIDRA_VERSION: 11.1.2 10 | GHIDRA_DATE: 20240709 11 | 12 | jobs: 13 | export-ghidra: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | include: 19 | - branch: xml 20 | type: xml 21 | - branch: main 22 | type: decomp 23 | 24 | permissions: 25 | contents: write 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | # Don't use actions/checkout@v4 because hurr durr it be broken: https://github.com/actions/checkout/issues/1477 30 | - name: Clone th06-re repo 31 | run: | 32 | echo "$GHIDRA_SSH_AUTH" > ssh_key 33 | chmod 0600 ssh_key 34 | GIT_SSH_COMMAND="ssh -i $PWD/ssh_key -o IdentitiesOnly=yes" git clone git@github.com:happyhavoc/th06-re th06-re --branch ${{ matrix.branch }} 35 | rm ssh_key 36 | env: 37 | GHIDRA_SSH_AUTH: ${{ secrets.GHIDRA_SSH_AUTH }} 38 | - name: Install python 3.11 39 | uses: actions/setup-python@v4 40 | with: 41 | python-version: '3.11' 42 | - name: Get ghidra 43 | run: | 44 | curl -L https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip -o /tmp/ghidra.zip 45 | unzip -d /tmp /tmp/ghidra.zip 46 | echo /tmp/ghidra_*/support >> $GITHUB_PATH 47 | - name: Export ghidra 48 | run: | 49 | echo "$GHIDRA_SSH_AUTH" > ssh_key 50 | python scripts/export_ghidra_database.py --user-mappings config/ghidra-user-maps.toml --username github-action --ssh-key ssh_key --program 'th06_102h.exe' 'ghidra://roblab.la/Touhou/Touhou 06 - The Embodiment of Scarlet Devil/' th06-re ${{ matrix.type }} 51 | rm ssh_key 52 | env: # Or as an environment variable 53 | GHIDRA_SSH_AUTH: ${{ secrets.GHIDRA_SSH_AUTH }} 54 | - name: Push th06-re 55 | run: | 56 | echo "$GHIDRA_SSH_AUTH" > ssh_key 57 | chmod 0600 ssh_key 58 | GIT_SSH_COMMAND="ssh -i $PWD/ssh_key -o IdentitiesOnly=yes" git -C th06-re push origin HEAD 59 | rm ssh_key 60 | env: 61 | GHIDRA_SSH_AUTH: ${{ secrets.GHIDRA_SSH_AUTH }} 62 | -------------------------------------------------------------------------------- /.github/workflows/update-mapping.yml: -------------------------------------------------------------------------------- 1 | name: Update mapping file 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 2 * * *' 7 | 8 | env: 9 | GHIDRA_VERSION: 11.1.2 10 | GHIDRA_DATE: 20240709 11 | 12 | jobs: 13 | update-mapping: 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Install python 3.11 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: '3.11' 26 | - name: Get ghidra 27 | run: | 28 | curl -L https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip -o /tmp/ghidra.zip 29 | unzip -d /tmp /tmp/ghidra.zip 30 | echo /tmp/ghidra_*/support >> $GITHUB_PATH 31 | - name: Update mapping 32 | run: | 33 | echo "$GHIDRA_SSH_AUTH" > ssh_key 34 | python scripts/update_mapping.py --username github-action --ssh-key ssh_key --program 'th06_102h.exe' 'ghidra://roblab.la/Touhou/Touhou 06 - The Embodiment of Scarlet Devil' 35 | rm ssh_key 36 | env: # Or as an environment variable 37 | GHIDRA_SSH_AUTH: ${{ secrets.GHIDRA_SSH_AUTH }} 38 | - name: Create PR to TH06 with updated mapping 39 | uses: peter-evans/create-pull-request@v6 40 | with: 41 | commit-message: Update mapping to latest ghidra changes 42 | branch: update-mapping 43 | title: Update mapping from ghidra 44 | body: Updates the mapping to the latest changes in the ghidra database. 45 | token: ${{ secrets.GH_TOKEN_PAT }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | diff 3 | *.obj 4 | *.exe 5 | resources/original.ico 6 | log.txt 7 | scripts/dls 8 | scripts/prefix 9 | build.ninja 10 | __pycache__ 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "3rdparty/munit"] 2 | path = 3rdparty/munit 3 | url = https://github.com/roblabla/munit 4 | branch = msvc2002 5 | [submodule "3rdparty/Detours"] 6 | path = 3rdparty/Detours 7 | url = https://github.com/roblabla/Detours 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/mirrors-clang-format 3 | rev: 'v18.1.8' 4 | hooks: 5 | - id: clang-format 6 | - repo: https://github.com/astral-sh/ruff-pre-commit 7 | # Ruff version. 8 | rev: v0.5.1 9 | hooks: 10 | # Run the linter. 11 | - id: ruff 12 | # Run the formatter. 13 | - id: ruff-format 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 東方紅魔郷 ~ the Embodiment of Scarlet Devil 2 | 3 | 4 | 5 | Decomp Progress 6 | 7 | 8 | [![Discord][discord-badge]][discord] <- click here to join discord server. 9 | 10 | [discord]: https://discord.gg/VyGwAjrh9a 11 | [discord-badge]: https://img.shields.io/discord/1147558514840064030?color=%237289DA&logo=discord&logoColor=%23FFFFFF 12 | 13 | This project aims to perfectly reconstruct the source code of [Touhou Koumakyou ~ the Embodiment of Scarlet Devil 1.02h](https://en.touhouwiki.net/wiki/Embodiment_of_Scarlet_Devil) by Team Shanghai Alice. 14 | 15 | **This project is still highly work in progress and in its early stages.** 16 | 17 | 18 | ## Installation 19 | 20 | ### Executable 21 | 22 | This project requires the original `東方紅魔郷.exe` version 1.02h (SHA256 hashsum 9f76483c46256804792399296619c1274363c31cd8f1775fafb55106fb852245, you can check hashsum on windows with command `certutil -hashfile SHA256`.) 23 | 24 | Copy `東方紅魔郷.exe` to `resources/game.exe`. 25 | 26 | ### Dependencies 27 | 28 | The build system has the following package requirements: 29 | 30 | - `python3` >= 3.4 31 | - `msiextract` (On linux/macos only) 32 | - `wine` (on linux/macos only, prefer CrossOver on macOS to avoid possible CL.EXE heap issues) 33 | - `aria2c` (optional, allows for torrent downloads, will automatically install on Windows if selected.) 34 | 35 | The rest of the build system is constructed out of Visual Studio 2002 and DirectX 8.0 from the Web Archive. 36 | 37 | #### Configure devenv 38 | 39 | This will download and install compiler, libraries, and other tools. 40 | 41 | If you are on windows, and for some reason want to download dependencies manually, 42 | run this command to get the list of files to download: 43 | 44 | ``` 45 | python scripts/create_devenv.py scripts/dls scripts/prefix --no-download 46 | ``` 47 | 48 | But if you want everything to be downloaded automatically, run it like this instead: 49 | 50 | ``` 51 | python scripts/create_devenv.py scripts/dls scripts/prefix 52 | ``` 53 | 54 | And if you want to use torrent to download those dependencies, use this: 55 | 56 | ``` 57 | python scripts/create_devenv.py scripts/dls scripts/prefix --torrent 58 | ``` 59 | 60 | On linux and mac, run the following script: 61 | ```bash 62 | # NOTE: On macOS if you use CrossOver. 63 | # export WINE=/wine 64 | ./scripts/create_th06_prefix 65 | ``` 66 | 67 | #### Building 68 | 69 | Run the following script: 70 | 71 | ``` 72 | python3 ./scripts/build.py 73 | ``` 74 | 75 | This will automatically generate a ninja build script `build.ninja`, and run 76 | ninja on it. 77 | 78 | ## Contributing 79 | 80 | ### Reverse Engineering 81 | 82 | You can find an XML export of our Ghidra RE in the companion repository 83 | [th06-re], in the `xml` branch. This repo is updated nightly through 84 | [`scripts/export_ghidra_database.py`], and its history matches the checkin 85 | history from our team's Ghidra Server. 86 | 87 | If you wish to help us in our Reverse Engineering effort, please contact 88 | @roblabla on discord so we can give you an account on the Ghidra Server. 89 | 90 | ### Reimplementation 91 | 92 | The easiest way to work on the reimplementation is through the use of 93 | [`objdiff`](https://github.com/encounter/objdiff). Here's how to get started: 94 | 95 | 1. First, follow the instruction above to get a devenv setup. 96 | 1. Copy the original `東方紅魔郷.exe` file (version 1.02h) to the 97 | `resources/` folder, and rename it into `game.exe`. This will be used as the source to compare the 98 | reimplementations against. 99 | 1. Download the latest version of objdiff. 100 | 1. Run `python3 scripts/export_ghidra_objs.py --import-csv`. This will extract 101 | from `resources/game.exe` the object files that objdiff can compare against. 102 | 1. Finally, run objdiff and open the th06 project. 103 | 104 | #### Choosing a function to decompile 105 | 106 | The easiest is to look at the `config/stubbed.csv` files. Those are all 107 | functions that are automatically stubbed out. You should pick one of them, open 108 | the associated object file in objdiff, and click on the function of interest. 109 | 110 | Then, open the correct `cpp` file, copy/paste the declaration, and start 111 | hacking! It may be useful to take the ghidra decompiler output as a base. You 112 | can find this output in the [th06-re] repository. 113 | 114 | # Credits 115 | 116 | We would like to extend our thanks to the following individuals for their 117 | invaluable contributions: 118 | 119 | - @EstexNT for porting the [`var_order` pragma](scripts/pragma_var_order.cpp) to 120 | MSVC7. 121 | 122 | [th06-re]: https://github.com/happyhavoc/th06-re 123 | -------------------------------------------------------------------------------- /config/ghidra-user-maps.toml: -------------------------------------------------------------------------------- 1 | mary = "Mary " 2 | roblabla = "roblabla " 3 | wearr = "wearrrrr " 4 | VieRo = "PieVieRo " 5 | reisen = "[[Reisen]] <95779166+0x32767@users.noreply.github.com>" 6 | renzo904 = "Renzo904 <63516530+Renzo904@users.noreply.github.com>" 7 | -------------------------------------------------------------------------------- /config/ghidra_ns_to_obj.csv: -------------------------------------------------------------------------------- 1 | AnmManager,th06::AnmVm,th06::AnmManager 2 | AsciiManager,th06::AsciiManager,th06::StageMenu 3 | BombData,th06::BombData 4 | BulletManager,th06::BulletManager 5 | Chain,th06::Chain,th06::ChainElem 6 | CMyFont,th06::CMyFont 7 | Controller,th06::Controller 8 | EclManager,th06::EclManager 9 | EffectManager,th06::EffectManager 10 | Ending,th06::Ending 11 | EnemyEclInstr,th06::EnemyEclInstr 12 | EnemyManager,th06::EnemyManager,th06::Enemy 13 | FileAbstraction,th06::FileAbstraction 14 | FileSystem,th06::FileSystem 15 | GameErrorContext,th06::GameErrorContext 16 | GameManager,th06::GameManager 17 | GameWindow,th06::GameWindow 18 | Gui,th06::Gui,th06::GuiImpl 19 | IPbg3Parser,th06::IPbg3Parser 20 | ItemManager,th06::ItemManager 21 | MainMenu,th06::MainMenu 22 | MidiOutput,th06::MidiDevice,th06::MidiTimer,th06::MidiOutput 23 | MusicRoom,th06::MusicRoom 24 | Pbg3Archive,th06::Pbg3Archive 25 | Pbg3Parser,th06::Pbg3Parser 26 | Player,th06::Player 27 | ReplayManager,th06::ReplayManager 28 | ResultScreen,th06::ResultScreen 29 | Rng,th06::Rng 30 | ScreenEffect,th06::ScreenEffect 31 | SoundPlayer,th06::SoundPlayer 32 | Stage,th06::Stage,th06::AnmManager::ExecuteAnmIdx 33 | Supervisor,th06::Supervisor 34 | TextHelper,th06::TextHelper 35 | utils,th06::utils 36 | ZunTimer,th06::ZunTimer 37 | zwave,th06::CSound,th06::CSoundManager,th06::CStreamingSound,th06::CWaveFile 38 | main,WinMain 39 | -------------------------------------------------------------------------------- /config/order.txt: -------------------------------------------------------------------------------- 1 | ??0AsciiManager@th06@@QAE@XZ 2 | ??0StageMenu@th06@@QAE@XZ 3 | ?OnUpdate@AsciiManager@th06@@SA?AW4ChainCallbackResult@2@PAU12@@Z 4 | ?OnDrawMenus@AsciiManager@th06@@SA?AW4ChainCallbackResult@2@PAU12@@Z 5 | ?OnDrawPopups@AsciiManager@th06@@SA?AW4ChainCallbackResult@2@PAU12@@Z 6 | ?RegisterChain@AsciiManager@th06@@SA?AW4ZunResult@@XZ 7 | ?AddedCallback@AsciiManager@th06@@SA?AW4ZunResult@@PAU12@@Z 8 | ?InitializeVms@AsciiManager@th06@@QAEXXZ 9 | ?DeletedCallback@AsciiManager@th06@@SA?AW4ZunResult@@PAU12@@Z 10 | ?CutChain@AsciiManager@th06@@SAXXZ 11 | ?AddString@AsciiManager@th06@@QAEXPAUD3DXVECTOR3@@PAD@Z 12 | ?AddFormatText@AsciiManager@th06@@QAAXPAUD3DXVECTOR3@@PBDZZ 13 | ?DrawStrings@AsciiManager@th06@@QAEXXZ 14 | ?CreatePopup1@AsciiManager@th06@@QAEXPAUD3DXVECTOR3@@HK@Z 15 | ?CreatePopup2@AsciiManager@th06@@QAEXPAUD3DXVECTOR3@@HK@Z 16 | ?OnUpdateGameMenu@StageMenu@th06@@QAEHXZ 17 | ?OnDrawGameMenu@StageMenu@th06@@QAEXXZ 18 | ?OnUpdateRetryMenu@StageMenu@th06@@QAEHXZ 19 | ?OnDrawRetryMenu@StageMenu@th06@@QAEXXZ 20 | ?DrawPopupsWithHwVertexProcessing@AsciiManager@th06@@QAEXXZ 21 | ?DrawPopupsWithoutHwVertexProcessing@AsciiManager@th06@@QAEXXZ 22 | ?Initialize@AnmVm@th06@@QAEXXZ 23 | ?D3DXMatrixIdentity@@YAPAUD3DXMATRIX@@PAU1@@Z 24 | ??0AnmVm@th06@@QAE@XZ 25 | ??0Stage@th06@@QAE@XZ 26 | ?OnUpdate@Stage@th06@@SA?AW4ChainCallbackResult@2@PAU12@@Z 27 | ?OnDrawHighPrio@Stage@th06@@SA?AW4ChainCallbackResult@2@PAU12@@Z 28 | ?OnDrawLowPrio@Stage@th06@@SA?AW4ChainCallbackResult@2@PAU12@@Z 29 | ?AddedCallback@Stage@th06@@SA?AW4ZunResult@@PAU12@@Z 30 | ?RegisterChain@Stage@th06@@SA?AW4ZunResult@@I@Z 31 | ?DeletedCallback@Stage@th06@@SA?AW4ZunResult@@PAU12@@Z 32 | ?CutChain@Stage@th06@@SAXXZ 33 | ?LoadStageData@Stage@th06@@QAE?AW4ZunResult@@PAD0@Z 34 | ?UpdateObjects@Stage@th06@@QAE?AW4ZunResult@@XZ 35 | ?RenderObjects@Stage@th06@@QAE?AW4ZunResult@@H@Z 36 | ?ExecuteAnmIdx@AnmManager@th06@@QAEXPAUAnmVm@2@H@Z 37 | ?BombReimuACalc@BombData@th06@@SAXPAUPlayer@2@@Z 38 | ?BombReimuADraw@BombData@th06@@SAXPAUPlayer@2@@Z 39 | ?DarkenViewport@BombData@th06@@SAXPAUPlayer@2@@Z 40 | ?BombReimuBCalc@BombData@th06@@SAXPAUPlayer@2@@Z 41 | ?BombReimuBDraw@BombData@th06@@SAXPAUPlayer@2@@Z 42 | ?BombMarisaACalc@BombData@th06@@SAXPAUPlayer@2@@Z 43 | ?BombMarisaADraw@BombData@th06@@SAXPAUPlayer@2@@Z 44 | ?BombMarisaBCalc@BombData@th06@@SAXPAUPlayer@2@@Z 45 | ?BombMarisaBDraw@BombData@th06@@SAXPAUPlayer@2@@Z 46 | -------------------------------------------------------------------------------- /config/stubbed.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/happyhavoc/th06/ca1d00894bc701e6b134cd37cf1d34540cafb921/config/stubbed.csv -------------------------------------------------------------------------------- /resources/placeholder.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/happyhavoc/th06/ca1d00894bc701e6b134cd37cf1d34540cafb921/resources/placeholder.ico -------------------------------------------------------------------------------- /resources/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/happyhavoc/th06/ca1d00894bc701e6b134cd37cf1d34540cafb921/resources/placeholder.png -------------------------------------------------------------------------------- /resources/progress_template.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | TH06.EXE 9 | 10 | 11 | 12 | 13 | Implemented: {FUNC_PROG_PERCENT}% 14 | Byte Accuracy: {BYTES_PROG_PERCENT}% 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {FUNC_PROG_PERCENT}% 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /resources/th06.rc: -------------------------------------------------------------------------------- 1 | 105 ICON DISCARDABLE "..\build\icon.ico" 2 | -------------------------------------------------------------------------------- /scripts/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:noble AS base 2 | 3 | # Install wine and msitools 4 | RUN dpkg --add-architecture i386 && \ 5 | mkdir -pm755 /etc/apt/keyrings && \ 6 | apt-get update && apt-get install -y wget gpg && \ 7 | wget -O - https://dl.winehq.org/wine-builds/winehq.key | gpg --dearmor -o /etc/apt/keyrings/winehq-archive.key - && \ 8 | wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/noble/winehq-noble.sources && \ 9 | apt-get update && apt-get install -y --install-recommends winehq-stable msitools git && \ 10 | apt-get remove --autoremove -y wget gpg && apt-get clean && rm -rf /var/lib/apt/lists/* 11 | 12 | FROM base AS build 13 | COPY create_devenv.py /tmp/create_devenv.py 14 | COPY winhelpers.py /tmp/winhelpers.py 15 | COPY th06run.bat /tmp/th06run.bat 16 | COPY th06vars.bat /tmp/th06vars.bat 17 | COPY pragma_var_order.cpp /tmp/pragma_var_order.cpp 18 | RUN python3 /tmp/create_devenv.py /tmp/dls /th06_prefix 19 | 20 | FROM base AS final 21 | COPY --from=build /th06_prefix /th06_prefix 22 | ENV TH06_DEVENV_PREFIX=/th06_prefix 23 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/happyhavoc/th06/ca1d00894bc701e6b134cd37cf1d34540cafb921/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/build.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from pathlib import Path 3 | import textwrap 4 | 5 | from configure import BuildType, configure 6 | from winhelpers import run_windows_program 7 | 8 | SCRIPTS_DIR = Path(__file__).parent 9 | 10 | 11 | def build(build_type, verbose=False, jobs=1, target=None): 12 | configure(build_type) 13 | 14 | ninja_args = [] 15 | if verbose: 16 | ninja_args += ["-v"] 17 | 18 | if jobs != 0: 19 | ninja_args += ["-j" + str(jobs)] 20 | 21 | if target is not None: 22 | ninja_args += [target] 23 | elif build_type == BuildType.TESTS: 24 | ninja_args += ["build/th06e-tests.exe"] 25 | elif build_type == BuildType.DLLBUILD: 26 | ninja_args += ["build/th06e.dll"] 27 | elif build_type == BuildType.OBJDIFFBUILD: 28 | ninja_args += ["objdiff"] 29 | else: 30 | ninja_args += ["build/th06e.exe"] 31 | 32 | # Then, run the build. We use run_windows_program to automatically go through 33 | # wine if running on linux/macos. scripts/th06run.bat will setup PATH and other 34 | # environment variables for the MSVC toolchain to work before calling ninja. 35 | run_windows_program( 36 | [str(SCRIPTS_DIR / "th06run.bat"), "ninja"] + ninja_args, 37 | cwd=str(SCRIPTS_DIR.parent), 38 | ) 39 | 40 | 41 | def main(): 42 | parser = argparse.ArgumentParser( 43 | "th06-build", formatter_class=argparse.RawTextHelpFormatter 44 | ) 45 | parser.add_argument( 46 | "--build-type", 47 | choices=[ 48 | "normal", 49 | "diffbuild", 50 | "tests", 51 | "dllbuild", 52 | "objdiffbuild", 53 | "binary_matchbuild", 54 | ], 55 | default="normal", 56 | ) 57 | parser.add_argument( 58 | "-j", 59 | "--jobs", 60 | type=int, 61 | default=1, 62 | help=textwrap.dedent(""" 63 | Number of jobs to run in parallel. Set to 0 to run one job per CPU core. Defaults to 1. 64 | Note that parallel builds may not work when running through wine. 65 | See https://github.com/happyhavoc/th06/issues/79 for more information."""), 66 | ) 67 | parser.add_argument("--verbose", action="store_true") 68 | parser.add_argument("--object-name", required=False) 69 | parser.add_argument( 70 | "target", 71 | nargs="?", 72 | help=textwrap.dedent(""" 73 | Ninja target to build. Default depends on the build type: 74 | - Normal and diff builds will build th06e.exe 75 | - dll builds will build th06e.dll 76 | - Test builds will build th06e-tests.exe 77 | - objdiff builds will build all the object files necessary for objdiff. 78 | """), 79 | ) 80 | args = parser.parse_args() 81 | target = None 82 | 83 | # First, create the build.ninja file that will be used to build. 84 | if args.build_type == "normal": 85 | build_type = BuildType.NORMAL 86 | elif args.build_type == "diffbuild": 87 | build_type = BuildType.DIFFBUILD 88 | elif args.build_type == "tests": 89 | build_type = BuildType.TESTS 90 | elif args.build_type == "dllbuild": 91 | build_type = BuildType.DLLBUILD 92 | elif args.build_type == "objdiffbuild": 93 | build_type = BuildType.OBJDIFFBUILD 94 | elif args.build_type == "binary_matchbuild": 95 | build_type = BuildType.BINARY_MATCHBUILD 96 | 97 | if args.object_name is not None: 98 | object_name = Path(args.object_name).name 99 | target = "build/objdiff/reimpl/" + object_name 100 | elif args.target is not None: 101 | target = args.target 102 | 103 | build(build_type, args.verbose, args.jobs, target=target) 104 | 105 | 106 | if __name__ == "__main__": 107 | main() 108 | -------------------------------------------------------------------------------- /scripts/create_th06_prefix: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 6 | 7 | PREFIX_PATH=$SCRIPT_DIR/prefix 8 | 9 | echo "Creating devenv in $PREFIX_PATH" 10 | # $@ is used to pass all arguments to the python script 11 | python3 "$SCRIPT_DIR/create_devenv.py" "$SCRIPT_DIR/dls" "$PREFIX_PATH" $@ 12 | 13 | echo "DONE" 14 | -------------------------------------------------------------------------------- /scripts/diff_all_functions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import csv 5 | from pathlib import Path 6 | import subprocess 7 | import sys 8 | import traceback 9 | 10 | from generate_function_diff import ( 11 | generate_function_diff_objdiff, 12 | generate_function_diff_satsuki, 13 | ) 14 | 15 | 16 | def main(): 17 | parser = argparse.ArgumentParser( 18 | prog="diff_all_functions", 19 | description="Generate a diff report of all implemented functions.", 20 | ) 21 | parser.add_argument( 22 | "--diff-method", 23 | action="store", 24 | choices=["satsuki", "objdiff"], 25 | default="satsuki", 26 | help="Which program to use to generate the diff.", 27 | ) 28 | args = parser.parse_args() 29 | 30 | base_dir = Path(__file__).parent.parent 31 | config_dir = base_dir / "config" 32 | 33 | implemented_csv = config_dir / "implemented.csv" 34 | 35 | success = True 36 | 37 | with open(implemented_csv) as f: 38 | vals = [] 39 | for row in csv.reader(f): 40 | try: 41 | if args.diff_method == "satsuki": 42 | left, right, diff, ratio = generate_function_diff_satsuki(row[0]) 43 | else: 44 | left, right, diff, ratio = generate_function_diff_objdiff(row[0]) 45 | vals.append( 46 | { 47 | "name": row[0], 48 | "diff": diff, 49 | "ratio": ratio, 50 | } 51 | ) 52 | except subprocess.CalledProcessError as e: 53 | success = False 54 | if e.stderr is not None: 55 | vals.append( 56 | {"name": row[0], "error": str(e.stderr, "utf8").strip()} 57 | ) 58 | else: 59 | vals.append({"name": row[0], "error": "failed"}) 60 | except Exception as e: 61 | vals.append( 62 | { 63 | "name": row[0], 64 | "error": "".join( 65 | traceback.format_exception(None, e, e.__traceback__) 66 | ), 67 | } 68 | ) 69 | 70 | print("# Report") 71 | print("") 72 | print("name | result") 73 | print("-----|-------") 74 | for val in vals: 75 | name = val["name"] 76 | id = val["name"].lower().replace(":", "__") 77 | if "error" in val: 78 | name = f"[{name}](#user-content-{id})" 79 | sys.stdout.flush() 80 | sys.stdout.buffer.write(f"{name} | 💥\n".encode("utf8")) 81 | else: 82 | if val["ratio"] != 1: 83 | name = f"[{name}](#user-content-{id})" 84 | print(f"{name} | {val['ratio'] * 100:.2f}%") 85 | 86 | for val in vals: 87 | if "error" not in val and val["ratio"] == 1: 88 | # 100% matching, nothing to see here. 89 | continue 90 | 91 | print("") 92 | print("") 93 | id = val["name"].lower().replace(":", "__") 94 | print(f'

{val["name"]}

') 95 | print("") 96 | if "error" in val: 97 | print("Failed to generate diff:") 98 | print(val["error"]) 99 | elif val["ratio"] != 1: 100 | print("```diff") 101 | print(val["diff"]) 102 | print("```") 103 | print("") 104 | print("
") 105 | 106 | if not success: 107 | sys.exit(1) 108 | 109 | 110 | if __name__ == "__main__": 111 | main() 112 | -------------------------------------------------------------------------------- /scripts/export_ghidra_objs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell -p python311 -i python3 3 | 4 | import argparse 5 | import os 6 | from pathlib import Path 7 | import tempfile 8 | import urllib.request 9 | 10 | import ghidra_helpers 11 | 12 | SCRIPT_PATH = Path(os.path.realpath(__file__)).parent 13 | 14 | 15 | def main(): 16 | parser = argparse.ArgumentParser( 17 | description="Export a ghidra database history to git", 18 | ) 19 | parser.add_argument("--local-project-dir", help="Path to the local ghidra project") 20 | parser.add_argument("--local-project-name", help="Path to the local ghidra project") 21 | parser.add_argument("--import-xml", action="store_true", help="Use the XML export") 22 | parser.add_argument( 23 | "--import-csv", action="store_true", help="Use the mapping.csv file" 24 | ) 25 | parser.add_argument("--program", help="Program to export", default="th06_102h.exe") 26 | args = parser.parse_args() 27 | 28 | os.makedirs(str(SCRIPT_PATH.parent / "build" / "objdiff" / "orig"), exist_ok=True) 29 | 30 | if args.import_xml: 31 | with tempfile.TemporaryDirectory() as tempdir: 32 | filename, _ = urllib.request.urlretrieve( 33 | "https://raw.githubusercontent.com/happyhavoc/th06-re/xml/th06_102h.exe.xml" 34 | ) 35 | ghidra_helpers.runAnalyze( 36 | str(tempdir), 37 | "Touhou 06", 38 | import_file=str(SCRIPT_PATH.parent / "resources" / "game.exe"), 39 | analysis=True, 40 | post_scripts=[ 41 | ["ImportFromXml.java", filename], 42 | [ 43 | "ExportDelinker.java", 44 | str(SCRIPT_PATH.parent / "config" / "ghidra_ns_to_obj.csv"), 45 | str(SCRIPT_PATH.parent / "build" / "objdiff" / "orig"), 46 | ], 47 | ], 48 | ) 49 | elif args.import_csv: 50 | with tempfile.TemporaryDirectory() as tempdir: 51 | mapping_csv = SCRIPT_PATH.parent / "config" / "mapping.csv" 52 | ghidra_helpers.runAnalyze( 53 | str(tempdir), 54 | "Touhou 06", 55 | import_file=str(SCRIPT_PATH.parent / "resources" / "game.exe"), 56 | analysis=True, 57 | post_scripts=[ 58 | ["ImportFromCsv.java", str(mapping_csv)], 59 | [ 60 | "ExportDelinker.java", 61 | str(SCRIPT_PATH.parent / "config" / "ghidra_ns_to_obj.csv"), 62 | str(SCRIPT_PATH.parent / "build" / "objdiff" / "orig"), 63 | ], 64 | ], 65 | ) 66 | else: 67 | repo = args.local_project_dir 68 | project_name = args.local_project_name 69 | program = args.program 70 | 71 | ghidra_helpers.runAnalyze( 72 | repo, 73 | project_name, 74 | process=program, 75 | pre_scripts=[ 76 | [ 77 | "ExportDelinker.java", 78 | str(SCRIPT_PATH.parent / "config" / "ghidra_ns_to_obj.csv"), 79 | str(SCRIPT_PATH.parent / "build" / "objdiff" / "orig"), 80 | ] 81 | ], 82 | ) 83 | 84 | 85 | if __name__ == "__main__": 86 | main() 87 | -------------------------------------------------------------------------------- /scripts/extract_icon.py: -------------------------------------------------------------------------------- 1 | import icon_extractor 2 | from pathlib import Path 3 | import sys 4 | import os 5 | import argparse 6 | 7 | 8 | SCRIPT_PATH = Path(os.path.realpath(__file__)).parent 9 | RESOURCES_PATH = SCRIPT_PATH.parent / "resources" 10 | FILENAME = RESOURCES_PATH / "game.exe" 11 | 12 | 13 | parser = argparse.ArgumentParser( 14 | prog="extract_icon", description="Extract the original icon from the game." 15 | ) 16 | parser.add_argument( 17 | "-o", "--output", required=True, help="Path to write the extracted icon" 18 | ) 19 | args = parser.parse_args() 20 | 21 | if not FILENAME.exists(): 22 | sys.stderr.write( 23 | "extract_icon.py: 'game.exe' not found. Copy your executable of Touhou 06 to 'resources/game.exe'" 24 | ) 25 | sys.exit(1) 26 | icon = icon_extractor.ExtractIcon(str(FILENAME)) 27 | 28 | with open(str(args.output), "wb") as icon_file: 29 | entries = icon.get_group_icons() 30 | icon_file.write(icon.export_raw(entries[0], 0)) 31 | -------------------------------------------------------------------------------- /scripts/gendef.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import itertools 3 | import string 4 | import subprocess 5 | import sys 6 | 7 | parser = argparse.ArgumentParser( 8 | prog="gendef", description="Generate .def file based on the provided object files." 9 | ) 10 | parser.add_argument( 11 | "-o", "--output", action="store", help="File to store the generated stubs in" 12 | ) 13 | parser.add_argument("input", nargs="+", help="File to store the generated stubs in") 14 | args = parser.parse_args() 15 | 16 | f = open(args.output, "w") 17 | print("EXPORTS", file=f) 18 | 19 | for arg in args.input: 20 | out = subprocess.check_output(["dumpbin", "/HEADERS", "/SYMBOLS", arg]) 21 | 22 | # First, find the appropriate section 23 | cur_section = None 24 | text_sections = [] 25 | for line in out.split(b"\n"): 26 | if line.startswith(b"SECTION HEADER #"): 27 | cur_section = int(line[len("SECTION HEADER #") :], 16) 28 | if b".text" in line: 29 | text_sections.append(cur_section) 30 | 31 | if len(text_sections) == 0: 32 | print("WARNING: Failed to find .text in object file " + arg, file=sys.stderr) 33 | 34 | for line in out.split(b"\n"): 35 | line = str(line, "utf8") 36 | if "External" in line: 37 | sect_idx = line.find("SECT") 38 | 39 | def ishexdigit(c): 40 | return c in string.hexdigits 41 | 42 | if sect_idx == -1: 43 | continue 44 | sect_int = int( 45 | "".join( 46 | itertools.takewhile(ishexdigit, line[sect_idx + len("SECT") :]) 47 | ), 48 | 16, 49 | ) 50 | if sect_int in text_sections: 51 | symbols = line.split("| ")[1].strip() 52 | symbols = symbols.split(" ", 1) 53 | if len(symbols) > 1: 54 | mangled_symbol, demangled_symbol = symbols 55 | else: 56 | mangled_symbol = symbols[0] 57 | demangled_symbol = "" 58 | print(" " + mangled_symbol + " PRIVATE", file=f) 59 | -------------------------------------------------------------------------------- /scripts/generate_decompme_toolchain.py: -------------------------------------------------------------------------------- 1 | # Generates a toolchain compatible with decompme's win9x support[0] from a 2 | # prefix generated with create_th06_prefix 3 | # 4 | # [0]: https://github.com/decompme/decomp.me/pull/802 5 | import os 6 | import shutil 7 | from pathlib import Path 8 | 9 | 10 | def main(): 11 | script_path = Path(os.path.dirname(os.path.realpath(__file__))) 12 | 13 | prefix_path = script_path / "prefix" 14 | 15 | vc7_path = prefix_path / "PROGRAM FILES" / "MICROSOFT VISUAL STUDIO .NET" / "VC7" 16 | ide_path = ( 17 | prefix_path 18 | / "PROGRAM FILES" 19 | / "MICROSOFT VISUAL STUDIO .NET" 20 | / "COMMON7" 21 | / "IDE" 22 | ) 23 | dx8sdk_path = prefix_path / "mssdk" 24 | 25 | decompme_toolchain = script_path / "msvc70" 26 | 27 | decompme_toolchain.mkdir() 28 | 29 | # First, create bin directory 30 | decompme_bin = decompme_toolchain / "Bin" 31 | decompme_bin.mkdir() 32 | # Copy all the programs in vc7_path bin to our bin. Lowercase them. 33 | vc7_bin = vc7_path / "BIN" 34 | for src_file in vc7_bin.glob("*"): 35 | if not src_file.is_file(): 36 | continue 37 | 38 | dst_file = decompme_bin / src_file.relative_to(vc7_bin) 39 | dst_file = dst_file.parent / dst_file.name.lower() 40 | 41 | print(src_file, "->", dst_file) 42 | shutil.copyfile(src_file, dst_file) 43 | 44 | # Copy a handful of necessary dlls from ide_path to our bin 45 | shutil.copyfile(ide_path / "MSPDB70.DLL", decompme_bin / "mspdb70.dll") 46 | shutil.copyfile(ide_path / "MSOBJ10.DLL", decompme_bin / "msobj10.dll") 47 | 48 | # Next, create the Include directory 49 | decompme_include = decompme_toolchain / "Include" 50 | decompme_include.mkdir() 51 | 52 | # Start with VC7 includes. 53 | vc7_include = vc7_path / "INCLUDE" 54 | for src_file in vc7_include.rglob("*"): 55 | if not src_file.is_file(): 56 | continue 57 | 58 | dst_file = decompme_include / src_file.relative_to(vc7_include) 59 | dst_file = dst_file.parent / dst_file.name.upper() 60 | 61 | # First, ensure parent directory exists 62 | dst_file.parent.mkdir(exist_ok=True) 63 | 64 | # Then, copy the file. 65 | shutil.copyfile(src_file, dst_file) 66 | 67 | # Then, copy the PlatformSDK 68 | platform_sdk_include = vc7_path / "PlatformSDK" / "Include" 69 | for src_file in platform_sdk_include.rglob("*"): 70 | if not src_file.is_file(): 71 | continue 72 | 73 | dst_file = decompme_include / src_file.relative_to(platform_sdk_include) 74 | dst_file = dst_file.parent / dst_file.name.upper() 75 | 76 | # First, ensure parent directory exists 77 | dst_file.parent.mkdir(exist_ok=True) 78 | 79 | # Then, copy the file. 80 | shutil.copyfile(src_file, dst_file) 81 | 82 | # Finally, copy DXSDK 83 | dx8sdk_include = dx8sdk_path / "include" 84 | for src_file in dx8sdk_include.rglob("*"): 85 | if not src_file.is_file(): 86 | continue 87 | 88 | dst_file = decompme_include / src_file.relative_to(dx8sdk_include) 89 | dst_file = dst_file.parent / dst_file.name.upper() 90 | 91 | # First, ensure parent directory exists 92 | dst_file.parent.mkdir(exist_ok=True) 93 | 94 | # Then, copy the file. 95 | shutil.copyfile(src_file, dst_file) 96 | 97 | 98 | if __name__ == "__main__": 99 | main() 100 | -------------------------------------------------------------------------------- /scripts/generate_detours.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import csv 3 | import sys 4 | 5 | parser = argparse.ArgumentParser( 6 | prog="generate_detours", description="Generate stubs based on the stubs.csv file." 7 | ) 8 | parser.add_argument( 9 | "-o", "--output", action="store", help="File to store the generated stubs in" 10 | ) 11 | parser.add_argument( 12 | "-i", "--input-def", action="store", help="Def file to find mangled symbols in" 13 | ) 14 | args = parser.parse_args() 15 | 16 | 17 | def get_path_of_mangled_symbol(symbol): 18 | if symbol[0] == "?": 19 | cpp_symbol = symbol[1:] 20 | path = cpp_symbol.split("@") 21 | last = next((idx for idx, x in enumerate(path) if x == ""), None) 22 | path = path[0:last] 23 | 24 | first_elem = path[0] 25 | if first_elem[0] == "?": 26 | if first_elem[1] == "0": 27 | cls = first_elem[2:] 28 | path[0] = cls + "::" + cls 29 | elif first_elem[1] == "1": 30 | cls = first_elem[2:] 31 | path[0] = cls + "::~" + cls 32 | elif first_elem[1:3] == "_H": 33 | return None 34 | else: 35 | print("WARNING: Unknown special symbol " + symbol) 36 | 37 | return "::".join(reversed(path)) 38 | elif symbol[0] == "_": 39 | return symbol[1:].split("@", 1)[0] 40 | else: 41 | raise Exception("Unknown symbol kind " + symbol) 42 | 43 | 44 | fun_to_mangled_map = {} 45 | with open(args.input_def) as f: 46 | for line in f: 47 | if len(line.strip()) == 0: 48 | continue 49 | 50 | if line.strip() == "EXPORTS": 51 | continue 52 | 53 | mangled_symbol = line.rsplit(" ", 1)[0].strip() 54 | fun_path = get_path_of_mangled_symbol(mangled_symbol) 55 | if fun_path is None: 56 | continue 57 | print(fun_path) 58 | if ( 59 | fun_path in fun_to_mangled_map 60 | and fun_to_mangled_map[fun_path] != mangled_symbol 61 | ): 62 | raise Exception("Overload detected, two functions patch " + fun_path) 63 | fun_to_mangled_map[fun_path] = mangled_symbol 64 | 65 | fun_to_mangled_map["operator_new"] = "??2@YAPAXI@Z" 66 | fun_to_mangled_map["_malloc"] = "malloc" 67 | fun_to_mangled_map["_calloc"] = "calloc" 68 | fun_to_mangled_map["_realloc"] = "realloc" 69 | fun_to_mangled_map["_free"] = "free" 70 | fun_to_mangled_map["__msize"] = "_msize" 71 | 72 | with open("config/mapping.csv") as f: 73 | mapping_csv = csv.reader(f) 74 | mapping_obj = {} 75 | for func in mapping_csv: 76 | fun_name = func[0] 77 | fun_addr = int(func[1], 16) 78 | mapping_obj[fun_name] = fun_addr 79 | 80 | detours = {} 81 | f = open("config/implemented.csv") 82 | implemented_csv = csv.reader(f) 83 | 84 | for implemented in implemented_csv: 85 | fun_name = implemented[0] 86 | fun_mangled_name = fun_to_mangled_map[fun_name] 87 | fun_addr = mapping_obj[fun_name] 88 | detours[fun_name] = { 89 | "fun_addr": fun_addr, 90 | "fun_mangled_name": fun_mangled_name, 91 | "stub": False, 92 | } 93 | 94 | f = open("config/stubbed.csv") 95 | stubbed_csv = csv.reader(f) 96 | for implemented in stubbed_csv: 97 | fun_name = implemented[0] 98 | fun_mangled_name = fun_to_mangled_map[fun_name] 99 | fun_addr = mapping_obj[fun_name] 100 | detours[fun_name] = { 101 | "fun_addr": fun_addr, 102 | "fun_mangled_name": fun_mangled_name, 103 | "stub": True, 104 | } 105 | 106 | # Add some necessary detouring to share MSVCRT heap with the main executable 107 | for fun_name in ["_malloc", "_calloc", "_realloc", "operator_new", "_free", "__msize"]: 108 | fun_mangled_name = fun_to_mangled_map[fun_name] 109 | fun_addr = mapping_obj[fun_name] 110 | detours[fun_name] = { 111 | "fun_addr": fun_addr, 112 | "fun_mangled_name": fun_mangled_name, 113 | "stub": False, 114 | } 115 | 116 | output = sys.stdout 117 | if args.output: 118 | output = open(args.output, "w") 119 | 120 | print("Detouring detours[] = {", file=output) 121 | 122 | first = True 123 | for detour in detours.values(): 124 | if not first: 125 | print(",", file=output) 126 | first = False 127 | print( 128 | " { " 129 | + hex(detour["fun_addr"]) 130 | + ', "' 131 | + detour["fun_mangled_name"] 132 | + '", ' 133 | + str(detour["stub"]).upper() 134 | + " }", 135 | end="", 136 | file=output, 137 | ) 138 | 139 | print("\n};", file=output) 140 | -------------------------------------------------------------------------------- /scripts/generate_function_decompme.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | import json 3 | from pathlib import Path 4 | import re 5 | import subprocess 6 | import sys 7 | import urllib.error 8 | import urllib.request 9 | 10 | REPO_DIR = Path(__file__).parent.parent 11 | SCRIPTS_DIR = REPO_DIR / "scripts" 12 | CONFIG_DIR = REPO_DIR / "config" 13 | RESOURCES_DIR = REPO_DIR / "resources" 14 | INCLUDE_PATHS = [REPO_DIR / "src"] 15 | 16 | 17 | def cpp_expand(file, path, matched_files): 18 | out = "" 19 | for line in file: 20 | val = re.match(r'#include "(.*)"', line) 21 | if val: 22 | out += cpp_expand_path(val.group(1), matched_files) 23 | elif re.match(r"#pragma once", line): 24 | matched_files.add(path) 25 | else: 26 | out += line 27 | return out 28 | 29 | 30 | def cpp_expand_path(path, matched_files): 31 | # i18n.hpp is auto-generated by converting i18n.tpl to shift-jis. This might 32 | # cause issues, so let's just take the original utf8-encoded template file. 33 | if path == "i18n.hpp": 34 | path = "i18n.tpl" 35 | 36 | if path in matched_files: 37 | return "" 38 | 39 | for include_path in INCLUDE_PATHS: 40 | with suppress(FileNotFoundError): 41 | with open(include_path / path) as f: 42 | return cpp_expand(f, path, matched_files) 43 | 44 | raise Exception("No include found for " + path + " in " + str(INCLUDE_PATHS)) 45 | 46 | 47 | def main(): 48 | asm = subprocess.check_output( 49 | [ 50 | str(SCRIPTS_DIR / "prefix" / "satsuki" / "satsuki"), 51 | "--mapping-file-csv", 52 | str(CONFIG_DIR / "mapping.csv"), 53 | "disassemble", 54 | "--att", 55 | "--force-address-zero", 56 | RESOURCES_DIR / "game.exe", 57 | sys.argv[1], 58 | ] 59 | ).decode("utf8") 60 | asm = ".att_syntax\n" + asm 61 | ctx = cpp_expand_path("th06.hpp", set()) 62 | req = urllib.request.Request( 63 | "https://decomp.me/api/scratch", 64 | headers={ 65 | "Content-Type": "application/json", 66 | }, 67 | data=json.dumps( 68 | { 69 | "compiler": "msvc7.0", 70 | "compiler_flags": "/MT /EHsc /G5 /Gs /GS /Od /Oi /Ob1 /Op /TP", 71 | "context": ctx, 72 | "diff_flags": [], 73 | "diff_label": sys.argv[1], 74 | "libraries": [ 75 | {"name": "directx", "version": "8.0"}, 76 | ], 77 | "platform": "win32", 78 | "preset": 111, 79 | "target_asm": asm, 80 | } 81 | ).encode("utf8"), 82 | ) 83 | 84 | try: 85 | with urllib.request.urlopen(req) as res: 86 | out_data = json.load(res) 87 | except urllib.error.HTTPError as err: 88 | print(json.load(err.fp)) 89 | raise 90 | 91 | print( 92 | "https://decomp.me/scratch/" 93 | + out_data["slug"] 94 | + "/claim?token=" 95 | + out_data["claim_token"] 96 | ) 97 | 98 | 99 | if __name__ == "__main__": 100 | main() 101 | -------------------------------------------------------------------------------- /scripts/generate_globals.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import struct 3 | import sys 4 | 5 | 6 | def main(): 7 | with open(sys.argv[1]) as f: 8 | globals_csv = csv.reader(f) 9 | globals_list = [ 10 | (glob[0].strip().encode("utf8") + b"\0", int(glob[1].strip(), 16)) 11 | for glob in globals_csv 12 | ] 13 | 14 | coff_file = struct.pack( 15 | "HHIIIHH", 16 | 0x014C, # machine - i386 17 | 0, # Number of sections - we have none 18 | 0, # Timestamp - unnecessary 19 | 0x14, # Pointer to symbol table. Goes right after the COFF header 20 | len(globals_list), # Number of symbols. 21 | 0, # Size of optional header. We don't have one. 22 | 5, # Characteristics. relocsStripped | lineNumsStripped 23 | ) 24 | 25 | curStrTableOffset = 4 26 | for symbol, addr in globals_list: 27 | coff_file += struct.pack( 28 | "IIIHHBB", 29 | 0, # Put all names in the string table. 30 | curStrTableOffset, # offset in string table 31 | addr, # Address 32 | 0xFFFF, # SectionNumber - none of ours, we don't have one. 33 | 0, # Type - Null 34 | 2, # StorageClass - External 35 | 0, # NumOfAuxSymbols 36 | ) 37 | curStrTableOffset += len(symbol) 38 | 39 | coff_file += struct.pack("I", curStrTableOffset) 40 | for symbol, _ in globals_list: 41 | coff_file += symbol 42 | 43 | with open(sys.argv[2], "wb") as f: 44 | f.write(coff_file) 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /scripts/generate_objdiff_objs.py: -------------------------------------------------------------------------------- 1 | import coff 2 | import csv 3 | from pathlib import Path 4 | import struct 5 | import sys 6 | 7 | SCRIPTS_DIR = Path(__file__).parent 8 | 9 | 10 | def demangle_msvc(sym): 11 | if len(sym) == 0: 12 | return sym 13 | 14 | if sym[0:1] == b"_": 15 | # Handle stdcall symbols first, those are simple. First remove the leading 16 | # underscore, then split on the last @ and remove everything that comes afterwards 17 | end_of_sym = sym.rfind(b"@") 18 | if end_of_sym == -1: 19 | # Not an stdcall, let's just not demangle it. 20 | return sym 21 | else: 22 | return sym[1:end_of_sym] 23 | 24 | if sym[0:1] != b"?": 25 | # Unmangled symbol? 26 | return sym 27 | 28 | # Handle CPP mangling. 29 | offset = 1 30 | 31 | # Read name. Start with special symbols 32 | special = None 33 | name = None 34 | if sym[offset : offset + 1] == b"?" and sym[offset + 1 : offset + 2].isdigit(): 35 | # Special symbol. 36 | special = sym[offset + 1] - ord("0") 37 | offset += 2 38 | else: 39 | # Read a normal name. 40 | start_of_name = offset 41 | end_of_name = sym.find(b"@", offset) 42 | if end_of_name == -1: 43 | end_of_name = len(sym) 44 | offset = len(sym) 45 | else: 46 | offset = end_of_name + 1 47 | name = sym[start_of_name:end_of_name] 48 | 49 | # Read scope 50 | scope = [] 51 | while True: 52 | start_of_scope = offset 53 | end_of_scope = sym.find(b"@", offset) 54 | if end_of_scope == -1: 55 | end_of_scope = len(sym) 56 | offset = len(sym) 57 | else: 58 | offset = end_of_scope + 1 59 | cur_scope = sym[start_of_scope:end_of_scope] 60 | if len(cur_scope) == 0: 61 | break 62 | scope.append(cur_scope) 63 | 64 | if name is not None: 65 | return b"::".join(scope[::-1]) + b"::" + name 66 | elif special == 0: 67 | return b"::".join(scope[::-1]) + b"::" + scope[0] 68 | elif special == 1: 69 | return b"::".join(scope[::-1]) + b"::~" + scope[0] 70 | else: 71 | return sym 72 | 73 | 74 | def sym_prefix(full_sym, prefix): 75 | return full_sym == prefix or full_sym.startswith(prefix + b"::") 76 | 77 | 78 | def rename_symbols(filename): 79 | reimpl_folder = SCRIPTS_DIR.parent / "build" / "objdiff" / "reimpl" 80 | orig_folder = SCRIPTS_DIR.parent / "build" / "objdiff" / "orig" 81 | config_folder = SCRIPTS_DIR.parent / "config" 82 | 83 | ns_to_obj = {} 84 | 85 | with open(str(config_folder / "ghidra_ns_to_obj.csv")) as f: 86 | ghidra_ns_to_obj = csv.reader(f) 87 | for vals in ghidra_ns_to_obj: 88 | ns_to_obj[vals[0]] = vals[1:] 89 | 90 | obj = coff.ObjectModule() 91 | with open(str(filename), "rb") as f: 92 | obj.unpack(f.read(), 0) 93 | 94 | # We filter to only the symbols with the namespace=filename, and we scrape everything but the function name 95 | seen = {} 96 | for sym_obj in obj.symbols: 97 | sym = sym_obj.get_name(obj.string_table) 98 | if seen.get(sym, False): 99 | continue 100 | seen[sym] = True 101 | 102 | demangled_sym = demangle_msvc(sym) 103 | if not any( 104 | sym_prefix(demangled_sym, val.encode("utf8")) 105 | for val in ns_to_obj[filename.stem] 106 | ): 107 | continue 108 | 109 | offset = obj.string_table.append(demangled_sym) 110 | sym_obj.name = b"\0\0\0\0" + struct.pack("I", offset) 111 | 112 | if not reimpl_folder.exists(): 113 | reimpl_folder.mkdir(parents=True, exist_ok=True) 114 | orig_folder.mkdir(parents=True, exist_ok=True) 115 | 116 | with open(str(reimpl_folder / filename.name), "wb") as f: 117 | f.write(obj.get_buffer()) 118 | 119 | 120 | if __name__ == "__main__": 121 | rename_symbols(Path(sys.argv[1])) 122 | -------------------------------------------------------------------------------- /scripts/generate_stubs.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import csv 3 | import sys 4 | 5 | parser = argparse.ArgumentParser( 6 | prog="generate_stubs", description="Generate stubs based on the stubs.csv file." 7 | ) 8 | parser.add_argument( 9 | "-o", "--output", action="store", help="File to store the generated stubs in" 10 | ) 11 | args = parser.parse_args() 12 | 13 | with open("config/mapping.csv") as f: 14 | mapping_csv = csv.reader(f) 15 | mapping_obj = {} 16 | for func in mapping_csv: 17 | fun_name = func[0] 18 | fun_addr = int(func[1], 16) 19 | mapping_obj[fun_name] = { 20 | "fun_addr": int(func[1], 16), 21 | "fun_size": int(func[2], 16), 22 | "calling_convention": func[3], 23 | "varargs": func[4] == "varargs", 24 | "ret_type": func[5], 25 | "arg_types": func[6:], 26 | } 27 | 28 | with open("config/implemented.csv") as f: 29 | implemented_csv = csv.reader(f) 30 | for func in implemented_csv: 31 | mapping_obj[func[0]]["implemented"] = True 32 | 33 | 34 | f = open("config/stubbed.csv") 35 | stubbed_csv = csv.reader(f) 36 | 37 | output = sys.stdout 38 | if args.output: 39 | output = open(args.output, "w") 40 | 41 | ret_vals = { 42 | "void": "", 43 | "bool": "false", 44 | "u8": "0", 45 | "i8": "0", 46 | "u16": "0", 47 | "i16": "0", 48 | "short": "0", 49 | "unsigned short": "0", 50 | "u32": "0", 51 | "i32": "0", 52 | "unsigned int": "0", 53 | "int": "0", 54 | "unsigned long": "0", 55 | "long": "0", 56 | "f32": "0.0", 57 | "float": "0.0", 58 | "ZunResult": "ZUN_ERROR", 59 | "ChainCallbackResult": "CHAIN_CALLBACK_RESULT_EXIT_GAME_SUCCESS", 60 | "FireBulletResult": "FBR_STOP_SPAWNING", 61 | } 62 | 63 | for stub in stubbed_csv: 64 | fun_name = stub[0] 65 | fun = mapping_obj[fun_name] 66 | if fun.get("implemented", False): 67 | # We don't need to generate a stub for implemented functions. 68 | continue 69 | 70 | calling_convention = fun["calling_convention"] 71 | ret_type = fun["ret_type"] 72 | fun_name_parts = fun_name.split("::") 73 | if len(fun_name_parts) >= 2 and fun_name_parts[-1] == fun_name_parts[-2]: 74 | # Constructor 75 | ret_type = "" 76 | ret_val = "" 77 | elif len(fun_name_parts) >= 2 and fun_name_parts[-1] == "~" + fun_name_parts[-2]: 78 | # Destructor 79 | ret_type = "" 80 | ret_val = "" 81 | elif ret_type.endswith("*"): 82 | ret_val = "NULL" 83 | else: 84 | ret_val = ret_vals[ret_type] 85 | args_types = fun["arg_types"] 86 | 87 | if calling_convention == "__thiscall": 88 | this_type = args_types.pop(0) 89 | 90 | callconv = "" 91 | if calling_convention == "__stdcall": 92 | callconv = "__stdcall" 93 | 94 | fun_sig = ret_type + " " + callconv + " " + fun_name + "(" 95 | fun_sig += ", ".join( 96 | [arg_type + " " + "a" + str(idx) for idx, arg_type in enumerate(args_types)] 97 | + (["..."] if fun["varargs"] else []) 98 | ) 99 | fun_sig += ")" 100 | print(fun_sig + " {", file=output) 101 | print(' printf("STUBBED: ' + fun_sig + '\\n");', file=output) 102 | print(" return " + ret_val + ";", file=output) 103 | print("}", file=output) 104 | -------------------------------------------------------------------------------- /scripts/ghidra/ExportDecomp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * LICENSE 3 | */ 4 | // Description 5 | //@author roblabla 6 | //@category exports 7 | //@keybinding 8 | //@menupath Skeleton 9 | //@toolbar Skeleton 10 | import ghidra.app.decompiler.DecompInterface; 11 | import ghidra.app.decompiler.DecompileOptions; 12 | import ghidra.app.decompiler.DecompileResults; 13 | import ghidra.app.script.GhidraScript; 14 | import ghidra.framework.model.DomainFile; 15 | import ghidra.framework.model.DomainObject; 16 | import ghidra.program.model.data.DataTypeWriter; 17 | import ghidra.program.model.listing.Function; 18 | import ghidra.program.model.listing.Program; 19 | import ghidra.program.model.mem.Memory; 20 | import ghidra.program.model.symbol.Namespace; 21 | import ghidra.program.model.symbol.Symbol; 22 | import ghidra.program.model.symbol.SymbolIterator; 23 | import ghidra.program.model.symbol.SymbolTable; 24 | import ghidra.program.model.symbol.SymbolType; 25 | import java.io.File; 26 | import java.io.FileWriter; 27 | import java.io.IOException; 28 | import java.io.PrintWriter; 29 | 30 | public class ExportDecomp extends GhidraScript 31 | { 32 | @Override protected void run() throws Exception 33 | { 34 | File outDir = askDirectory("Output Folder", ""); 35 | int outVer = askInt("File Version to Export", ""); 36 | 37 | // We get the DomainFile this way to ensure we get a GhidraFile and not 38 | // a DomainProxyFile. This is because DomainProxyFile does not handle 39 | // getting anything but the latest version of a file. 40 | DomainFile f = parseDomainFile(currentProgram.getDomainFile().getPathname()); 41 | 42 | DomainObject obj = f.getReadOnlyDomainObject(this, outVer, monitor); 43 | 44 | Program p = (Program)obj; 45 | 46 | DecompInterface decomp = new DecompInterface(); 47 | DecompileOptions options = new DecompileOptions(); 48 | decomp.setOptions(options); 49 | decomp.toggleCCode(true); 50 | decomp.toggleSyntaxTree(true); 51 | decomp.setSimplificationStyle("decompile"); 52 | 53 | decomp.openProgram(p); 54 | 55 | SymbolTable st = p.getSymbolTable(); 56 | SymbolIterator si = st.getSymbolIterator(); 57 | 58 | while (si.hasNext()) 59 | { 60 | Symbol s = si.next(); 61 | if (s.getSymbolType() == SymbolType.FUNCTION && !s.isExternal() && s.getAddress().getOffset() < 0x0043d160) 62 | { 63 | Function fun = p.getFunctionManager().getFunctionAt(s.getAddress()); 64 | if (!fun.isThunk()) 65 | { 66 | extractFunction(decomp, fun, outDir); 67 | } 68 | } 69 | } 70 | 71 | File datatypesFile = new File(outDir, "types.h"); 72 | FileWriter fw = new FileWriter(datatypesFile); 73 | 74 | DataTypeWriter dtw = new DataTypeWriter(p.getDataTypeManager(), fw); 75 | dtw.write(p.getDataTypeManager(), monitor); 76 | fw.close(); 77 | 78 | obj.release(this); 79 | } 80 | 81 | String getFullName(Namespace n) 82 | { 83 | String s = n.getName(); 84 | while (!n.getParentNamespace().isGlobal()) 85 | { 86 | n = n.getParentNamespace(); 87 | s = n.getName() + "::" + s; 88 | } 89 | return s; 90 | } 91 | 92 | void extractFunction(DecompInterface decomp, Function f, File outputDir) throws IOException 93 | { 94 | DecompileResults res = decomp.decompileFunction(f, 120, monitor); 95 | if (res.getDecompiledFunction() != null) 96 | { 97 | outputDir = new File(outputDir, f.getParentNamespace().getName()); 98 | outputDir.mkdirs(); 99 | File outputFile = new File(outputDir, f.getName() + ".c"); 100 | FileWriter fw = new FileWriter(outputFile); 101 | PrintWriter pw = new PrintWriter(fw); 102 | pw.write(res.getDecompiledFunction().getC()); 103 | pw.close(); 104 | } 105 | else 106 | { 107 | printf("Can't decompile %s\n", getFullName(f)); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /scripts/ghidra/ExportDelinker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * LICENSE 3 | */ 4 | // Description 5 | //@author renzo904 6 | //@category exports 7 | //@keybinding 8 | //@menupath Skeleton 9 | //@toolbar Skeleton 10 | import static java.util.Map.entry; 11 | 12 | import ghidra.app.analyzers.RelocationTableSynthesizerAnalyzer; 13 | import ghidra.app.script.GhidraScript; 14 | import ghidra.app.services.Analyzer; 15 | import ghidra.app.util.DomainObjectService; 16 | import ghidra.app.util.Option; 17 | import ghidra.app.util.exporter.CoffRelocatableObjectExporter; 18 | import ghidra.app.util.importer.MessageLog; 19 | import ghidra.framework.model.DomainFile; 20 | import ghidra.framework.model.DomainObject; 21 | import ghidra.program.model.address.AddressSet; 22 | import ghidra.program.model.listing.GhidraClass; 23 | import ghidra.program.model.mem.Memory; 24 | import ghidra.program.model.symbol.Namespace; 25 | import ghidra.program.model.symbol.Symbol; 26 | import java.io.File; 27 | import java.nio.charset.StandardCharsets; 28 | import java.nio.file.Files; 29 | import java.util.ArrayList; 30 | import java.util.Arrays; 31 | import java.util.Iterator; 32 | import java.util.List; 33 | import java.util.Map; 34 | 35 | public class ExportDelinker extends GhidraScript 36 | { 37 | @Override protected void run() throws Exception 38 | { 39 | // First run the Relocation Table Synthesizer, to pickup any potentially 40 | // new globals in the reloc table. 41 | Analyzer analyzer = new RelocationTableSynthesizerAnalyzer(); 42 | analyzer.added(currentProgram, currentProgram.getMemory(), monitor, new MessageLog()); 43 | 44 | // Then, export the COFFs. 45 | CoffRelocatableObjectExporter exporter = new CoffRelocatableObjectExporter(); 46 | 47 | List