diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4dfcd1c6..91dcc72b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: jobs: build: - container: ghcr.io/rainchus/marioparty4-build:main + container: ghcr.io/mariopartyrd/mp-gc-build:main runs-on: ubuntu-latest strategy: @@ -15,11 +15,10 @@ jobs: version: [GMPE01_00] steps: - # Checkout the repository (shallow clone) + # Checkout the repository - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 submodules: recursive # Set Git config diff --git a/.gitignore b/.gitignore index 81558d47..275db754 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,35 @@ +# IDE folders +.idea/ +.vs/ + +# Caches __pycache__ -.idea -.vscode -.ninja_* -*.exe -build -build.ninja -objdiff.json +.mypy_cache +.cache/ + +# Original files orig/*/* !orig/*/.gitkeep +*.dol +*.rel +*.elf +*.o +*.map +*.MAP + +# Build files +build/ +.ninja_* +build.ninja + +# decompctx output +ctx.* +*.ctx + +# Generated configs +objdiff.json +compile_commands.json + +# Miscellaneous /*.txt -ctx.c -tools/output/* \ No newline at end of file +*.exe diff --git a/.vscode.example/c_cpp_properties.json b/.vscode.example/c_cpp_properties.json deleted file mode 100644 index 3bace88c..00000000 --- a/.vscode.example/c_cpp_properties.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "configurations": [ - { - "name": "Linux", - "includePath": [ - "${workspaceFolder}/include/**", - "${workspaceFolder}/build/GMPE01_00/include/**" - ], - "cStandard": "c99", - "cppStandard": "c++98", - "intelliSenseMode": "linux-clang-x86", - "compilerPath": "", - "configurationProvider": "ms-vscode.makefile-tools", - "browse": { - "path": [ - "${workspaceFolder}/include" - ], - "limitSymbolsToIncludedHeaders": true - } - } - ], - "version": 4 -} diff --git a/.vscode.example/settings.json b/.vscode.example/settings.json deleted file mode 100644 index 60e7e27d..00000000 --- a/.vscode.example/settings.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "[c]": { - "files.encoding": "utf8", - "editor.defaultFormatter": "xaver.clang-format" - }, - "[cpp]": { - "files.encoding": "utf8", - "editor.defaultFormatter": "xaver.clang-format" - }, - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" - }, - "files.insertFinalNewline": true, - "files.trimFinalNewlines": true, - "files.associations": { - "*.inc": "cpp" - }, - "search.useIgnoreFiles": false, - "search.exclude": { - "build/*/config.json": true, - "build/**/*.MAP": true, - "build.ninja": true, - ".ninja_*": true, - "objdiff.json": true - } -} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..c20797ff --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "llvm-vs-code-extensions.vscode-clangd", + "ms-python.black-formatter", + "ms-python.flake8", + ], + "unwantedRecommendations": [ + "ms-vscode.cmake-tools", + "ms-vscode.cpptools-extension-pack", + "ms-vscode.cpptools", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e0b63102 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,33 @@ +{ + "[c]": { + "files.encoding": "utf8", + "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd" + }, + "[cpp]": { + "files.encoding": "utf8", + "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd" + }, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.associations": { + "*.inc": "c", + ".clangd": "yaml" + }, + "search.useIgnoreFiles": false, + "search.exclude": { + "build/*/config.json": true, + "build/**/*.MAP": true, + "build.ninja": true, + ".ninja_*": true, + "objdiff.json": true, + "progress.json": true, + "report.json": true, + "compile_commands.json": true, + }, + // Disable C/C++ IntelliSense, use clangd instead + "C_Cpp.intelliSenseEngine": "disabled", + "cmake.ignoreCMakeListsMissing": true, +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..6dc12130 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + // Use Ctrl+Shift+B to run build tasks. + // Or "Run Build Task" in the Command Palette. + "version": "2.0.0", + "tasks": [ + { + "label": "ninja", + "type": "shell", + "command": "ninja", + "group": { + "kind": "build", + "isDefault": true + } + }, + ] +} diff --git a/README.md b/README.md index bca1a5fb..af12d2d7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ Mario Party 4 -[![Build Status]][actions] ![Progress] ![DOL Progress] ![RELs Progress] [![Discord Badge]][discord] +[![Build Status]][actions] [![Progress]][progress site] [![DOL Progress]][progress site] [![RELs Progress]][progress site] [![Discord Badge]][discord] ============= -[Build Status]: https://github.com/Rainchus/marioparty4/actions/workflows/build.yml/badge.svg -[actions]: https://github.com/Rainchus/marioparty4/actions/workflows/build.yml -[Progress]: https://img.shields.io/endpoint?label=Code&url=https%3A%2F%2Fprogress.decomp.club%2Fdata%2Fmarioparty4%2FGMPE01_00%2Fall%2F%3Fmode%3Dshield%26measure%3Dcode -[DOL Progress]: https://img.shields.io/endpoint?label=DOL&url=https%3A%2F%2Fprogress.decomp.club%2Fdata%2Fmarioparty4%2FGMPE01_00%2Fdol%2F%3Fmode%3Dshield%26measure%3Dcode -[RELs Progress]: https://img.shields.io/endpoint?label=RELs&url=https%3A%2F%2Fprogress.decomp.club%2Fdata%2Fmarioparty4%2FGMPE01_00%2Fmodules%2F%3Fmode%3Dshield%26measure%3Dcode +[Build Status]: https://github.com/mariopartyrd/marioparty4/actions/workflows/build.yml/badge.svg +[actions]: https://github.com/mariopartyrd/marioparty4/actions/workflows/build.yml +[Progress]: https://decomp.dev/mariopartyrd/marioparty4.svg?mode=shield&measure=code&label=Code&category=all +[DOL Progress]: https://decomp.dev/mariopartyrd/marioparty4.svg?mode=shield&measure=code&label=DOL&category=dol +[RELs Progress]: https://decomp.dev/mariopartyrd/marioparty4.svg?mode=shield&measure=code&label=RELs&category=modules +[progress site]: https://decomp.dev/mariopartyrd/marioparty4 [Discord Badge]: https://img.shields.io/discord/994839212618690590?color=%237289DA&logo=discord&logoColor=%23FFFFFF [discord]: https://discord.gg/T4faGveujK @@ -59,28 +60,29 @@ Building - Clone the repository: ``` - git clone https://github.com/Rainchus/marioparty4.git + git clone https://github.com/mariopartyrd/marioparty4.git ``` -- Using [Dolphin Emulator](https://dolphin-emu.org/), extract your game to `orig/GMPE01_00`. -![](assets/dolphin-extract.png) - - To save space, the only necessary files are the following. Any others can be deleted. - - `sys/main.dol` - - `files/dll/*.rel` + +- Initialize and update submodules: + + ```sh + git submodule update --init --recursive + ``` + +- Copy your game's disc image to `orig/GMPE01_00`. + - Supported formats: ISO (GCM), RVZ, WIA, WBFS, CISO, NFS, GCZ, TGC + - After the initial build, the disc image can be deleted to save space. + - Configure: ``` python configure.py ``` - To use a version other than `GMPE01_00` (USA), specify it with `--version`. + - Build: ``` ninja ``` -Visual Studio Code -================== - -If desired, use the recommended Visual Studio Code settings by renaming the `.vscode.example` directory to `.vscode`. - Diffing ======= @@ -89,5 +91,3 @@ Once the initial build succeeds, an `objdiff.json` should exist in the project r Download the latest release from [encounter/objdiff](https://github.com/encounter/objdiff). Under project settings, set `Project directory`. The configuration should be loaded automatically. Select an object from the left sidebar to begin diffing. Changes to the project will rebuild automatically: changes to source files, headers, `configure.py`, `splits.txt` or `symbols.txt`. - -![](assets/objdiff.png) diff --git a/config/GMPE01_00/config.yml b/config/GMPE01_00/config.yml index 8e687a60..81fc8915 100644 --- a/config/GMPE01_00/config.yml +++ b/config/GMPE01_00/config.yml @@ -1,4 +1,5 @@ -object: orig/GMPE01_00/sys/main.dol +object_base: orig/GMPE01_00 +object: sys/main.dol hash: ff8d894776d37167e454ae3f0c9e644407c1e6b1 symbols: config/GMPE01_00/symbols.txt splits: config/GMPE01_00/splits.txt @@ -7,12 +8,12 @@ mw_comment_version: 10 # GC 2.6 linker force_active: [__register_global_object, lbl_801D40D0, _kerent] modules: -- object: orig/GMPE01_00/files/dll/_minigameDll.rel +- object: files/dll/_minigameDll.rel hash: 3fbbebc0440f0d91432ecd6a27ef68d5309b87b6 symbols: config/GMPE01_00/rels/_minigameDll/symbols.txt splits: config/GMPE01_00/rels/_minigameDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/bootDll.rel +- object: files/dll/bootDll.rel hash: bdfca4f9bce60519badca0d2d0a5b71f6d99706f symbols: config/GMPE01_00/rels/bootDll/symbols.txt splits: config/GMPE01_00/rels/bootDll/splits.txt @@ -21,487 +22,487 @@ modules: - symbol: logoNintendoData binary: logoNintendoData.bin header: logoNintendoData.inc -- object: orig/GMPE01_00/files/dll/instDll.rel +- object: files/dll/instDll.rel hash: f09399fee83c63abfe2adb25341152adad959a93 symbols: config/GMPE01_00/rels/instDll/symbols.txt splits: config/GMPE01_00/rels/instDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m302Dll.rel +- object: files/dll/m302Dll.rel hash: f1ad7b5a5198a14d34141b28c654bc1704c9dcd9 symbols: config/GMPE01_00/rels/m302Dll/symbols.txt splits: config/GMPE01_00/rels/m302Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m303Dll.rel +- object: files/dll/m303Dll.rel hash: f1ad7b5a5198a14d34141b28c654bc1704c9dcd9 symbols: config/GMPE01_00/rels/m303Dll/symbols.txt splits: config/GMPE01_00/rels/m303Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m330Dll.rel +- object: files/dll/m330Dll.rel hash: f1ad7b5a5198a14d34141b28c654bc1704c9dcd9 symbols: config/GMPE01_00/rels/m330Dll/symbols.txt splits: config/GMPE01_00/rels/m330Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m333Dll.rel +- object: files/dll/m333Dll.rel hash: f1ad7b5a5198a14d34141b28c654bc1704c9dcd9 symbols: config/GMPE01_00/rels/m333Dll/symbols.txt splits: config/GMPE01_00/rels/m333Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m402Dll.rel +- object: files/dll/m402Dll.rel hash: 136d192a1464e593cd0b767691dfa012c58730ed symbols: config/GMPE01_00/rels/m402Dll/symbols.txt splits: config/GMPE01_00/rels/m402Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m403Dll.rel +- object: files/dll/m403Dll.rel hash: b834eb5f8a2749f3be52aa9023cc81403075eba9 symbols: config/GMPE01_00/rels/m403Dll/symbols.txt splits: config/GMPE01_00/rels/m403Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m404Dll.rel +- object: files/dll/m404Dll.rel hash: c46b7814cefa8e5dee8dfd1883e369877ac78c0c symbols: config/GMPE01_00/rels/m404Dll/symbols.txt splits: config/GMPE01_00/rels/m404Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m405Dll.rel +- object: files/dll/m405Dll.rel hash: 7857e0822079d0c7bbfec756a7cf4206b754d100 symbols: config/GMPE01_00/rels/m405Dll/symbols.txt splits: config/GMPE01_00/rels/m405Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m406Dll.rel +- object: files/dll/m406Dll.rel hash: bea398ac8abe018dce80914e6b3d6d7578eb86bb symbols: config/GMPE01_00/rels/m406Dll/symbols.txt splits: config/GMPE01_00/rels/m406Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m407dll.rel +- object: files/dll/m407dll.rel hash: 6f63338c417ab62740a40f0968c03c570b440b8a symbols: config/GMPE01_00/rels/m407dll/symbols.txt splits: config/GMPE01_00/rels/m407dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m408Dll.rel +- object: files/dll/m408Dll.rel hash: 7a8ff34b4bcaff39037c9e2f717505cbc63d4230 symbols: config/GMPE01_00/rels/m408Dll/symbols.txt splits: config/GMPE01_00/rels/m408Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m409Dll.rel +- object: files/dll/m409Dll.rel hash: 984f031fc50121369d5b04d1ec2c54322efdf281 symbols: config/GMPE01_00/rels/m409Dll/symbols.txt splits: config/GMPE01_00/rels/m409Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m410Dll.rel +- object: files/dll/m410Dll.rel hash: 94e308e409038f7919177d190110ce589cc8a8e8 symbols: config/GMPE01_00/rels/m410Dll/symbols.txt splits: config/GMPE01_00/rels/m410Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m411Dll.rel +- object: files/dll/m411Dll.rel hash: 26ac81a3db9f3850bb43b23cdb7168dcddccaeea symbols: config/GMPE01_00/rels/m411Dll/symbols.txt splits: config/GMPE01_00/rels/m411Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m412Dll.rel +- object: files/dll/m412Dll.rel hash: 3ebb173a52aaea75acb414f73264e72a2943c6f8 symbols: config/GMPE01_00/rels/m412Dll/symbols.txt splits: config/GMPE01_00/rels/m412Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m413Dll.rel +- object: files/dll/m413Dll.rel hash: e59d4b66b1f57637c335b4745a696e3dbcb4bbe3 symbols: config/GMPE01_00/rels/m413Dll/symbols.txt splits: config/GMPE01_00/rels/m413Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m414Dll.rel +- object: files/dll/m414Dll.rel hash: f7fe1aa24c7b6b8ca2bb28922696c0392dc7d029 symbols: config/GMPE01_00/rels/m414Dll/symbols.txt splits: config/GMPE01_00/rels/m414Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m415Dll.rel +- object: files/dll/m415Dll.rel hash: 3c697b54ebdd01971b99af8c812b4850fa181f4f symbols: config/GMPE01_00/rels/m415Dll/symbols.txt splits: config/GMPE01_00/rels/m415Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m416Dll.rel +- object: files/dll/m416Dll.rel hash: d26526935455b26beb2b9eaed1bfae3f3f458c25 symbols: config/GMPE01_00/rels/m416Dll/symbols.txt splits: config/GMPE01_00/rels/m416Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m417Dll.rel +- object: files/dll/m417Dll.rel hash: b5c3805ec3cb023299ed09b782b92441085d0054 symbols: config/GMPE01_00/rels/m417Dll/symbols.txt splits: config/GMPE01_00/rels/m417Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m418Dll.rel +- object: files/dll/m418Dll.rel hash: 0d02610005c46ad4f47ff51f5e154f9b0d16a4f4 symbols: config/GMPE01_00/rels/m418Dll/symbols.txt splits: config/GMPE01_00/rels/m418Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m419Dll.rel +- object: files/dll/m419Dll.rel hash: bbceb138b8cc43e578179a7892801633892cb03c symbols: config/GMPE01_00/rels/m419Dll/symbols.txt splits: config/GMPE01_00/rels/m419Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m420dll.rel +- object: files/dll/m420dll.rel hash: 2ef4ee163bc2aa15f87c8c89afb4f28939f088c8 symbols: config/GMPE01_00/rels/m420dll/symbols.txt splits: config/GMPE01_00/rels/m420dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m421Dll.rel +- object: files/dll/m421Dll.rel hash: 216a99780a3915d7096385bbcddfc979ab3a4025 symbols: config/GMPE01_00/rels/m421Dll/symbols.txt splits: config/GMPE01_00/rels/m421Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m422Dll.rel +- object: files/dll/m422Dll.rel hash: 9ad72ba6c3ac277e521dca2fc1372a5d31c18930 symbols: config/GMPE01_00/rels/m422Dll/symbols.txt splits: config/GMPE01_00/rels/m422Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m423Dll.rel +- object: files/dll/m423Dll.rel hash: 377c6a56b3faf8f991ec4fccaf1972cb41910438 symbols: config/GMPE01_00/rels/m423Dll/symbols.txt splits: config/GMPE01_00/rels/m423Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m424Dll.rel +- object: files/dll/m424Dll.rel hash: 5814f59970268406bd86a86d0fee5a09359ab506 symbols: config/GMPE01_00/rels/m424Dll/symbols.txt splits: config/GMPE01_00/rels/m424Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m425Dll.rel +- object: files/dll/m425Dll.rel hash: ecaeae453393d228f2769aab9c022820bcdb58f1 symbols: config/GMPE01_00/rels/m425Dll/symbols.txt splits: config/GMPE01_00/rels/m425Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m426Dll.rel +- object: files/dll/m426Dll.rel hash: c240697a57cc08d34e27e89d8d3455454799e8b1 symbols: config/GMPE01_00/rels/m426Dll/symbols.txt splits: config/GMPE01_00/rels/m426Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m427Dll.rel +- object: files/dll/m427Dll.rel hash: c78a0857e9c44fb33bd4fdc3e392a15b4dec0431 symbols: config/GMPE01_00/rels/m427Dll/symbols.txt splits: config/GMPE01_00/rels/m427Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m428Dll.rel +- object: files/dll/m428Dll.rel hash: a1d5672d8ef0aac089dda00287fd68bf2bb67807 symbols: config/GMPE01_00/rels/m428Dll/symbols.txt splits: config/GMPE01_00/rels/m428Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m429Dll.rel +- object: files/dll/m429Dll.rel hash: d25f198ce04aa5ca7b54ada9de52f722bd751447 symbols: config/GMPE01_00/rels/m429Dll/symbols.txt splits: config/GMPE01_00/rels/m429Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m430Dll.rel +- object: files/dll/m430Dll.rel hash: 8bef3e2f51db3afa82dcaf64e209067a58c04bf9 symbols: config/GMPE01_00/rels/m430Dll/symbols.txt splits: config/GMPE01_00/rels/m430Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m431Dll.rel +- object: files/dll/m431Dll.rel hash: 7148ec21ca6e0aa8d213a1dce0aeb9d93a9b496c symbols: config/GMPE01_00/rels/m431Dll/symbols.txt splits: config/GMPE01_00/rels/m431Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m432Dll.rel +- object: files/dll/m432Dll.rel hash: 096f5a85bb837af68bd491e962eda7726d8d26a0 symbols: config/GMPE01_00/rels/m432Dll/symbols.txt splits: config/GMPE01_00/rels/m432Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m433Dll.rel +- object: files/dll/m433Dll.rel hash: c36f56264ab9cf74b882c2544cdb9b89f6a098cd symbols: config/GMPE01_00/rels/m433Dll/symbols.txt splits: config/GMPE01_00/rels/m433Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m434Dll.rel +- object: files/dll/m434Dll.rel hash: 55912c8441002b61701fc8c769f707c69e2740c1 symbols: config/GMPE01_00/rels/m434Dll/symbols.txt splits: config/GMPE01_00/rels/m434Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m435Dll.rel +- object: files/dll/m435Dll.rel hash: 56f1fc330e8b97fa426fac3d901b1feed946566e symbols: config/GMPE01_00/rels/m435Dll/symbols.txt splits: config/GMPE01_00/rels/m435Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m436Dll.rel +- object: files/dll/m436Dll.rel hash: e53f3d381bfb99351483e70b9d79c2e8676f73d5 symbols: config/GMPE01_00/rels/m436Dll/symbols.txt splits: config/GMPE01_00/rels/m436Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m437Dll.rel +- object: files/dll/m437Dll.rel hash: 404698b53acad1231e00d84d0196ddce8d9d9dd9 symbols: config/GMPE01_00/rels/m437Dll/symbols.txt splits: config/GMPE01_00/rels/m437Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m438Dll.rel +- object: files/dll/m438Dll.rel hash: 9bddf4b81b9a792260000abe2631da493a8b8564 symbols: config/GMPE01_00/rels/m438Dll/symbols.txt splits: config/GMPE01_00/rels/m438Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m439Dll.rel +- object: files/dll/m439Dll.rel hash: a2a18c9d26dedfa8e2ae003c3a2e1bdcc1cbd4cc symbols: config/GMPE01_00/rels/m439Dll/symbols.txt splits: config/GMPE01_00/rels/m439Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m440Dll.rel +- object: files/dll/m440Dll.rel hash: 227ca13bcd72059bdd506e9611d9b52f661af6c5 symbols: config/GMPE01_00/rels/m440Dll/symbols.txt splits: config/GMPE01_00/rels/m440Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m441Dll.rel +- object: files/dll/m441Dll.rel hash: d6cbf72115374e06ffea3f15001977c288a9c3fb symbols: config/GMPE01_00/rels/m441Dll/symbols.txt splits: config/GMPE01_00/rels/m441Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m442Dll.rel +- object: files/dll/m442Dll.rel hash: 347fcb836b5266dfe3d1088e1aa796c043b0fe60 symbols: config/GMPE01_00/rels/m442Dll/symbols.txt splits: config/GMPE01_00/rels/m442Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m443Dll.rel +- object: files/dll/m443Dll.rel hash: 77444c6739a4d9390f30ba56c5fcd82ee4efc0c2 symbols: config/GMPE01_00/rels/m443Dll/symbols.txt splits: config/GMPE01_00/rels/m443Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m444dll.rel +- object: files/dll/m444dll.rel hash: 76154b165cd86472ce3155028ea049ba0a0f3dcb symbols: config/GMPE01_00/rels/m444dll/symbols.txt splits: config/GMPE01_00/rels/m444dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m445Dll.rel +- object: files/dll/m445Dll.rel hash: d4d9f2aad53aa7f4e1c313438a0e3767f8e58eb9 symbols: config/GMPE01_00/rels/m445Dll/symbols.txt splits: config/GMPE01_00/rels/m445Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m446dll.rel +- object: files/dll/m446dll.rel hash: 4b16e5eff613eaf4cc98411809cd1f1578e33fd1 symbols: config/GMPE01_00/rels/m446dll/symbols.txt splits: config/GMPE01_00/rels/m446dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m447dll.rel +- object: files/dll/m447dll.rel hash: 775ca8f8b3be867ad67f91bc3b6ebcc8c3068d2b symbols: config/GMPE01_00/rels/m447dll/symbols.txt splits: config/GMPE01_00/rels/m447dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m448Dll.rel +- object: files/dll/m448Dll.rel hash: e6f7586dadbe3f5b5045c0bdf2fe618d42ef3a44 symbols: config/GMPE01_00/rels/m448Dll/symbols.txt splits: config/GMPE01_00/rels/m448Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m449Dll.rel +- object: files/dll/m449Dll.rel hash: e797c2ff132ab35e768be62cb62219592464ca9d symbols: config/GMPE01_00/rels/m449Dll/symbols.txt splits: config/GMPE01_00/rels/m449Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m450Dll.rel +- object: files/dll/m450Dll.rel hash: f2ea7da07cf8be4f449c97b829d3231911e158a0 symbols: config/GMPE01_00/rels/m450Dll/symbols.txt splits: config/GMPE01_00/rels/m450Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m451Dll.rel +- object: files/dll/m451Dll.rel hash: 3f82fd24071582b6494a9f99750c0bc3c10d502e symbols: config/GMPE01_00/rels/m451Dll/symbols.txt splits: config/GMPE01_00/rels/m451Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m453Dll.rel +- object: files/dll/m453Dll.rel hash: 49ef28c2862000fae4d8efd8fec0ff9b41f24461 symbols: config/GMPE01_00/rels/m453Dll/symbols.txt splits: config/GMPE01_00/rels/m453Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m455Dll.rel +- object: files/dll/m455Dll.rel hash: e0b21cfbe592955c84662ff50d8a33e147403906 symbols: config/GMPE01_00/rels/m455Dll/symbols.txt splits: config/GMPE01_00/rels/m455Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m456Dll.rel +- object: files/dll/m456Dll.rel hash: 97d5701b151c6ad8cf8cea6e776973daa95eea34 symbols: config/GMPE01_00/rels/m456Dll/symbols.txt splits: config/GMPE01_00/rels/m456Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m457Dll.rel +- object: files/dll/m457Dll.rel hash: d4a411f9ef850f52506afd1ad1de88d2d8de361b symbols: config/GMPE01_00/rels/m457Dll/symbols.txt splits: config/GMPE01_00/rels/m457Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m458Dll.rel +- object: files/dll/m458Dll.rel hash: 7ac361c5a04f83d46f516a13a43e43b59b929435 symbols: config/GMPE01_00/rels/m458Dll/symbols.txt splits: config/GMPE01_00/rels/m458Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m459dll.rel +- object: files/dll/m459dll.rel hash: 1aafc4f38ddfee64ea8f7dda7cbd7113e86997ba symbols: config/GMPE01_00/rels/m459dll/symbols.txt splits: config/GMPE01_00/rels/m459dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m460Dll.rel +- object: files/dll/m460Dll.rel hash: 5d28f1a87edee5216b9bb28d9cbff2b22d617747 symbols: config/GMPE01_00/rels/m460Dll/symbols.txt splits: config/GMPE01_00/rels/m460Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m461Dll.rel +- object: files/dll/m461Dll.rel hash: ab058151bd25600f05f8b6fedbec1c7448175123 symbols: config/GMPE01_00/rels/m461Dll/symbols.txt splits: config/GMPE01_00/rels/m461Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m462Dll.rel +- object: files/dll/m462Dll.rel hash: 78d6ab00677791ece684a0c149d1ae72efc52d1c symbols: config/GMPE01_00/rels/m462Dll/symbols.txt splits: config/GMPE01_00/rels/m462Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/m463Dll.rel +- object: files/dll/m463Dll.rel hash: 32010595b4fd0b75293fc46b9026c5613d0ea4c2 symbols: config/GMPE01_00/rels/m463Dll/symbols.txt splits: config/GMPE01_00/rels/m463Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/mentDll.rel +- object: files/dll/mentDll.rel hash: 9e63dd96943ffd2747be5aad924b95892d46051a symbols: config/GMPE01_00/rels/mentDll/symbols.txt splits: config/GMPE01_00/rels/mentDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/messDll.rel +- object: files/dll/messDll.rel hash: 7e119de456b3557dcf514c70bd7ab81dc5f9cf54 symbols: config/GMPE01_00/rels/messDll/symbols.txt splits: config/GMPE01_00/rels/messDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/mgmodedll.rel +- object: files/dll/mgmodedll.rel hash: 376c78e2d1b7d7a429959c532e9cb92c98f73b7e symbols: config/GMPE01_00/rels/mgmodedll/symbols.txt splits: config/GMPE01_00/rels/mgmodedll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/modeltestDll.rel +- object: files/dll/modeltestDll.rel hash: 8de28c0f254a2c574c4da9b6a3a17e5ad7ffe1f9 symbols: config/GMPE01_00/rels/modeltestDll/symbols.txt splits: config/GMPE01_00/rels/modeltestDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/modeseldll.rel +- object: files/dll/modeseldll.rel hash: bdf8cd57fa23f07c429393817037ade04e2c6024 symbols: config/GMPE01_00/rels/modeseldll/symbols.txt splits: config/GMPE01_00/rels/modeseldll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/mpexDll.rel +- object: files/dll/mpexDll.rel hash: 4524e63b63510b42bca907f72e67affddec856f9 symbols: config/GMPE01_00/rels/mpexDll/symbols.txt splits: config/GMPE01_00/rels/mpexDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/msetupDll.rel +- object: files/dll/msetupDll.rel hash: 86ff8288bb92472376d02cade1d3c9603b613c57 symbols: config/GMPE01_00/rels/msetupDll/symbols.txt splits: config/GMPE01_00/rels/msetupDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/mstory2Dll.rel +- object: files/dll/mstory2Dll.rel hash: 7579ff7aa9638488d876dbb61d3e4b868974a040 symbols: config/GMPE01_00/rels/mstory2Dll/symbols.txt splits: config/GMPE01_00/rels/mstory2Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/mstory3Dll.rel +- object: files/dll/mstory3Dll.rel hash: 951f1951eac515fce1d3314059f27fdc7b39f035 symbols: config/GMPE01_00/rels/mstory3Dll/symbols.txt splits: config/GMPE01_00/rels/mstory3Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/mstory4Dll.rel +- object: files/dll/mstory4Dll.rel hash: 7f9cc4a18a137f861b2c326b099c02ba4e5bd94b symbols: config/GMPE01_00/rels/mstory4Dll/symbols.txt splits: config/GMPE01_00/rels/mstory4Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/mstoryDll.rel +- object: files/dll/mstoryDll.rel hash: 979a83f2d6f5200e2de4333ec8f943701a250131 symbols: config/GMPE01_00/rels/mstoryDll/symbols.txt splits: config/GMPE01_00/rels/mstoryDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/nisDll.rel +- object: files/dll/nisDll.rel hash: d74a5147245a1877b140ee2af7f75b2b6c90892d symbols: config/GMPE01_00/rels/nisDll/symbols.txt splits: config/GMPE01_00/rels/nisDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/option.rel +- object: files/dll/option.rel hash: 5c6973fe7e0271885a5a87d87d4e7164ef711abb symbols: config/GMPE01_00/rels/option/symbols.txt splits: config/GMPE01_00/rels/option/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/present.rel +- object: files/dll/present.rel hash: ddb8d8a825578a588276b9d4f65eceee26f91253 symbols: config/GMPE01_00/rels/present/symbols.txt splits: config/GMPE01_00/rels/present/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/resultDll.rel +- object: files/dll/resultDll.rel hash: 41a820438884ae8566f34f895198a186aae60305 symbols: config/GMPE01_00/rels/resultDll/symbols.txt splits: config/GMPE01_00/rels/resultDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/safDll.rel +- object: files/dll/safDll.rel hash: f34c104078b41971412cb09b37c4dc9526be02e3 symbols: config/GMPE01_00/rels/safDll/symbols.txt splits: config/GMPE01_00/rels/safDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/selmenuDll.rel +- object: files/dll/selmenuDll.rel hash: 81a00a71b8d1ab22bfd3df10d815c87ba618ec84 symbols: config/GMPE01_00/rels/selmenuDll/symbols.txt splits: config/GMPE01_00/rels/selmenuDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/staffDll.rel +- object: files/dll/staffDll.rel hash: a6210fbcdf3d7f0063f3052e2ae525abacac08e7 symbols: config/GMPE01_00/rels/staffDll/symbols.txt splits: config/GMPE01_00/rels/staffDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/subchrselDll.rel +- object: files/dll/subchrselDll.rel hash: 7488bc249dc7c656f60105e4d24c4011983c2ba2 symbols: config/GMPE01_00/rels/subchrselDll/symbols.txt splits: config/GMPE01_00/rels/subchrselDll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/w01Dll.rel +- object: files/dll/w01Dll.rel hash: d3de36269886995d959e1fd58d8ad806843c8819 symbols: config/GMPE01_00/rels/w01Dll/symbols.txt splits: config/GMPE01_00/rels/w01Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/w02Dll.rel +- object: files/dll/w02Dll.rel hash: 750c4d6cd6a49d290f5cc63432d4f65642fbd7e4 symbols: config/GMPE01_00/rels/w02Dll/symbols.txt splits: config/GMPE01_00/rels/w02Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/w03Dll.rel +- object: files/dll/w03Dll.rel hash: f9e8d9c4581d44488adaffa3d46d9bc835d4f9bd symbols: config/GMPE01_00/rels/w03Dll/symbols.txt splits: config/GMPE01_00/rels/w03Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/w04Dll.rel +- object: files/dll/w04Dll.rel hash: 821126eeb7054150744b925cc4cb06c751ddcefd symbols: config/GMPE01_00/rels/w04Dll/symbols.txt splits: config/GMPE01_00/rels/w04Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/w05Dll.rel +- object: files/dll/w05Dll.rel hash: d29f7ccadcba23c8446d05101c4efa5caba26dfd symbols: config/GMPE01_00/rels/w05Dll/symbols.txt splits: config/GMPE01_00/rels/w05Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/w06Dll.rel +- object: files/dll/w06Dll.rel hash: d11a7a6d44189ba8d1e3a7168ea640ea9f2152fe symbols: config/GMPE01_00/rels/w06Dll/symbols.txt splits: config/GMPE01_00/rels/w06Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/w10Dll.rel +- object: files/dll/w10Dll.rel hash: 54bb23228b04c5aaa6aa7c8ca1f846baec5a1f2c symbols: config/GMPE01_00/rels/w10Dll/symbols.txt splits: config/GMPE01_00/rels/w10Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/w20Dll.rel +- object: files/dll/w20Dll.rel hash: 98cc41456afa9e1ccbb8e03718b8d3cb64e4e86f symbols: config/GMPE01_00/rels/w20Dll/symbols.txt splits: config/GMPE01_00/rels/w20Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/w21Dll.rel +- object: files/dll/w21Dll.rel hash: b888f2c6703e680699ff8c59ad9200b9d78d30a1 symbols: config/GMPE01_00/rels/w21Dll/symbols.txt splits: config/GMPE01_00/rels/w21Dll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/ztardll.rel +- object: files/dll/ztardll.rel hash: 87d9fee70ed4f011e0b30e05156d087204cf686f symbols: config/GMPE01_00/rels/ztardll/symbols.txt splits: config/GMPE01_00/rels/ztardll/splits.txt links: [] -- object: orig/GMPE01_00/files/dll/E3setupDLL.rel +- object: files/dll/E3setupDLL.rel hash: 234e07cee1491c7060e30805681bf5f39150122d symbols: config/GMPE01_00/rels/E3setupDLL/symbols.txt splits: config/GMPE01_00/rels/E3setupDLL/splits.txt links: [_minigameDll] -- object: orig/GMPE01_00/files/dll/m300Dll.rel +- object: files/dll/m300Dll.rel hash: eba8a17e8b263bc9cd601b1aea7e698a8785416a symbols: config/GMPE01_00/rels/m300Dll/symbols.txt splits: config/GMPE01_00/rels/m300Dll/splits.txt links: [_minigameDll] -- object: orig/GMPE01_00/files/dll/m401Dll.rel +- object: files/dll/m401Dll.rel hash: f3ec526c25986a3fcf7dfbc8c463626839a3a801 symbols: config/GMPE01_00/rels/m401Dll/symbols.txt splits: config/GMPE01_00/rels/m401Dll/splits.txt diff --git a/configure.py b/configure.py index 0de9c77d..2fe8e8ee 100644 --- a/configure.py +++ b/configure.py @@ -16,7 +16,15 @@ import argparse import sys from pathlib import Path from typing import Any, Dict, List -from tools.project import * + +from tools.project import ( + Object, + ProgressCategory, + ProjectConfig, + calculate_progress, + generate_build, + is_windows, +) # Game versions DEFAULT_VERSION = 0 @@ -105,6 +113,12 @@ parser.add_argument( action="store_true", help="builds equivalent (but non-matching) or modded objects", ) +parser.add_argument( + "--no-progress", + dest="progress", + action="store_false", + help="disable progress calculation", +) args = parser.parse_args() config = ProjectConfig() @@ -117,10 +131,10 @@ config.dtk_path = args.dtk config.objdiff_path = args.objdiff config.binutils_path = args.binutils config.compilers_path = args.compilers -config.debug = args.debug config.generate_map = args.map config.non_matching = args.non_matching config.sjiswrap_path = args.sjiswrap +config.progress = args.progress if not is_windows(): config.wrapper = args.wrapper # Don't build asm unless we're --non-matching @@ -130,8 +144,8 @@ if not config.non_matching: # Tool versions config.binutils_tag = "2.42-1" config.compilers_tag = "20240706" -config.dtk_tag = "v0.9.6" -config.objdiff_tag = "v2.0.0-beta.6" +config.dtk_tag = "v1.1.4" +config.objdiff_tag = "v2.3.3" config.sjiswrap_tag = "v1.1.1" config.wibo_tag = "0.6.11" @@ -149,6 +163,10 @@ config.ldflags = [ "-fp hardware", "-nodefaults", ] +if args.debug: + config.ldflags.append("-g") +if args.map: + config.ldflags.append("-mapunused") # Base flags, common to most GC/Wii games. # Generally leave untouched, with overrides added below. @@ -178,7 +196,7 @@ cflags_base = [ ] # Debug flags -if config.debug: +if args.debug: cflags_base.extend(["-sym on", "-DDEBUG=1"]) else: cflags_base.append("-DNDEBUG=1") @@ -324,6 +342,12 @@ Matching = True # Object matches and should be linked NonMatching = False # Object does not match and should not be linked Equivalent = config.non_matching # Object should be linked when configured with --non-matching + +# Object is only matching for specific versions +def MatchingFor(*versions): + return config.version in versions + + config.warn_missing_config = True config.warn_missing_source = False config.libs = [ diff --git a/tools/decompctx.py b/tools/decompctx.py index 290946f1..87cfb7ef 100644 --- a/tools/decompctx.py +++ b/tools/decompctx.py @@ -23,7 +23,7 @@ include_dirs = [ # Add additional include directories here ] -include_pattern = re.compile(r'^#\s*include\s*[<"](.+?)[>"]$') +include_pattern = re.compile(r'^#\s*include\s*[<"](.+?)[>"]') guard_pattern = re.compile(r"^#\s*ifndef\s+(.*)$") defines = set() diff --git a/tools/download_tool.py b/tools/download_tool.py index 69ef96a7..f4512d01 100644 --- a/tools/download_tool.py +++ b/tools/download_tool.py @@ -55,6 +55,7 @@ def dtk_url(tag: str) -> str: repo = "https://github.com/encounter/decomp-toolkit" return f"{repo}/releases/download/{tag}/dtk-{system}-{arch}{suffix}" + def objdiff_cli_url(tag: str) -> str: uname = platform.uname() suffix = "" diff --git a/tools/project.py b/tools/project.py index 502b2f4b..b4469fcd 100644 --- a/tools/project.py +++ b/tools/project.py @@ -17,7 +17,7 @@ import os import platform import sys from pathlib import Path -from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast +from typing import IO, Any, Dict, Iterable, List, Optional, Set, Tuple, Union, cast from . import ninja_syntax from .ninja_syntax import serialize_path @@ -41,8 +41,9 @@ class Object: "asflags": None, "asm_dir": None, "cflags": None, - "extra_asflags": None, - "extra_cflags": None, + "extra_asflags": [], + "extra_cflags": [], + "extra_clang_flags": [], "host": None, "lib": None, "mw_version": None, @@ -81,6 +82,20 @@ class Object: set_default("shift_jis", config.shift_jis) set_default("src_dir", config.src_dir) + # Validate progress categories + def check_category(category: str): + if not any(category == c.id for c in config.progress_categories): + sys.exit( + f"Progress category '{category}' missing from config.progress_categories" + ) + + progress_category = obj.options["progress_category"] + if isinstance(progress_category, list): + for category in progress_category: + check_category(category) + elif progress_category is not None: + check_category(progress_category) + # Resolve paths build_dir = config.out_path() obj.src_path = Path(obj.options["src_dir"]) / obj.options["source"] @@ -131,7 +146,6 @@ class ProjectConfig: self.build_rels: bool = True # Build REL files self.check_sha_path: Optional[Path] = None # Path to version.sha1 self.config_path: Optional[Path] = None # Path to config.yml - self.debug: bool = False # Build with debug info self.generate_map: bool = False # Generate map file(s) self.asflags: Optional[List[str]] = None # Assembler flags self.ldflags: Optional[List[str]] = None # Linker flags @@ -156,14 +170,22 @@ class ProjectConfig: self.custom_build_steps: Optional[Dict[str, List[Dict[str, Any]]]] = ( None # Custom build steps, types are ["pre-compile", "post-compile", "post-link", "post-build"] ) + self.generate_compile_commands: bool = ( + True # Generate compile_commands.json for clangd + ) + self.extra_clang_flags: List[str] = [] # Extra flags for clangd # Progress output, progress.json and report.json config + self.progress = True # Enable report.json generation and CLI progress output self.progress_all: bool = True # Include combined "all" category self.progress_modules: bool = True # Include combined "modules" category self.progress_each_module: bool = ( False # Include individual modules, disable for large numbers of modules ) self.progress_categories: List[ProgressCategory] = [] # Additional categories + self.print_progress_categories: Union[bool, List[str]] = ( + True # Print additional progress categories in the CLI progress output + ) # Progress fancy printing self.progress_use_fancy: bool = False @@ -200,9 +222,40 @@ class ProjectConfig: out[obj.name] = obj.resolve(self, lib) return out + # Gets the output path for build-related files. def out_path(self) -> Path: return self.build_dir / str(self.version) + # Gets the path to the compilers directory. + # Exits the program if neither `compilers_path` nor `compilers_tag` is provided. + def compilers(self) -> Path: + if self.compilers_path: + return self.compilers_path + elif self.compilers_tag: + return self.build_dir / "compilers" + else: + sys.exit("ProjectConfig.compilers_tag missing") + + # Gets the wrapper to use for compiler commands, if set. + def compiler_wrapper(self) -> Optional[Path]: + wrapper = self.wrapper + + if self.use_wibo(): + wrapper = self.build_dir / "tools" / "wibo" + if not is_windows() and wrapper is None: + wrapper = Path("wine") + + return wrapper + + # Determines whether or not to use wibo as the compiler wrapper. + def use_wibo(self) -> bool: + return ( + self.wibo_tag is not None + and sys.platform == "linux" + and platform.machine() in ("i386", "x86_64") + and self.wrapper is None + ) + def is_windows() -> bool: return os.name == "nt" @@ -214,11 +267,26 @@ CHAIN = "cmd /c " if is_windows() else "" EXE = ".exe" if is_windows() else "" -def make_flags_str(cflags: Union[str, List[str]]) -> str: - if isinstance(cflags, list): - return " ".join(cflags) - else: - return cflags +def file_is_asm(path: Path) -> bool: + return path.suffix.lower() == ".s" + + +def file_is_c(path: Path) -> bool: + return path.suffix.lower() == ".c" + + +def file_is_cpp(path: Path) -> bool: + return path.suffix.lower() in (".cc", ".cp", ".cpp", ".cxx") + + +def file_is_c_cpp(path: Path) -> bool: + return file_is_c(path) or file_is_cpp(path) + + +def make_flags_str(flags: Optional[List[str]]) -> str: + if flags is None: + return "" + return " ".join(flags) # Load decomp-toolkit generated config.json @@ -235,14 +303,14 @@ def load_build_config( build_config: Dict[str, Any] = json.load(f) config_version = build_config.get("version") if config_version is None: - # Invalid config.json + print("Invalid config.json, regenerating...") f.close() os.remove(build_config_path) return None dtk_version = str(config.dtk_tag)[1:] # Strip v if versiontuple(config_version) < versiontuple(dtk_version): - # Outdated config.json + print("Outdated config.json, regenerating...") f.close() os.remove(build_config_path) return None @@ -251,13 +319,14 @@ def load_build_config( return build_config -# Generate build.ninja and objdiff.json +# Generate build.ninja, objdiff.json and compile_commands.json def generate_build(config: ProjectConfig) -> None: config.validate() objects = config.objects() build_config = load_build_config(config, config.out_path() / "config.json") generate_build_ninja(config, objects, build_config) generate_objdiff_config(config, objects, build_config) + generate_compile_commands(config, objects, build_config) # Generate build.ninja @@ -283,12 +352,7 @@ def generate_build_ninja( # Variables ### n.comment("Variables") - ldflags = " ".join(config.ldflags or []) - if config.generate_map: - ldflags += " -mapunused" - if config.debug: - ldflags += " -g" - n.variable("ldflags", ldflags) + n.variable("ldflags", make_flags_str(config.ldflags)) if config.linker_version is None: sys.exit("ProjectConfig.linker_version missing") n.variable("mw_version", Path(config.linker_version)) @@ -409,16 +473,10 @@ def generate_build_ninja( else: sys.exit("ProjectConfig.sjiswrap_tag missing") + wrapper = config.compiler_wrapper() # Only add an implicit dependency on wibo if we download it - wrapper = config.wrapper wrapper_implicit: Optional[Path] = None - if ( - config.wibo_tag is not None - and sys.platform == "linux" - and platform.machine() in ("i386", "x86_64") - and config.wrapper is None - ): - wrapper = build_tools_path / "wibo" + if wrapper is not None and config.use_wibo(): wrapper_implicit = wrapper n.build( outputs=wrapper, @@ -429,15 +487,11 @@ def generate_build_ninja( "tag": config.wibo_tag, }, ) - if not is_windows() and wrapper is None: - wrapper = Path("wine") wrapper_cmd = f"{wrapper} " if wrapper else "" + compilers = config.compilers() compilers_implicit: Optional[Path] = None - if config.compilers_path: - compilers = config.compilers_path - elif config.compilers_tag: - compilers = config.build_dir / "compilers" + if config.compilers_path is None and config.compilers_tag is not None: compilers_implicit = compilers n.build( outputs=compilers, @@ -448,8 +502,6 @@ def generate_build_ninja( "tag": config.compilers_tag, }, ) - else: - sys.exit("ProjectConfig.compilers_tag missing") binutils_implicit = None if config.binutils_path: @@ -663,7 +715,6 @@ def generate_build_ninja( n.comment(f"Link {self.name}") if self.module_id == 0: elf_path = build_path / f"{self.name}.elf" - dol_path = build_path / f"{self.name}.dol" elf_ldflags = f"$ldflags -lcf {serialize_path(self.ldscript)}" if config.generate_map: elf_map = map_path(elf_path) @@ -728,17 +779,33 @@ def generate_build_ninja( source_added: Set[Path] = set() def c_build(obj: Object, src_path: Path) -> Optional[Path]: - cflags_str = make_flags_str(obj.options["cflags"]) - if obj.options["extra_cflags"] is not None: - extra_cflags_str = make_flags_str(obj.options["extra_cflags"]) - cflags_str += " " + extra_cflags_str - used_compiler_versions.add(obj.options["mw_version"]) - # Avoid creating duplicate build rules if obj.src_obj_path is None or obj.src_obj_path in source_added: return obj.src_obj_path source_added.add(obj.src_obj_path) + cflags = obj.options["cflags"] + extra_cflags = obj.options["extra_cflags"] + + # Add appropriate language flag if it doesn't exist already + # Added directly to the source so it flows to other generation tasks + if not any(flag.startswith("-lang") for flag in cflags) and not any( + flag.startswith("-lang") for flag in extra_cflags + ): + # Ensure extra_cflags is a unique instance, + # and insert into there to avoid modifying shared sets of flags + extra_cflags = obj.options["extra_cflags"] = list(extra_cflags) + if file_is_cpp(src_path): + extra_cflags.insert(0, "-lang=c++") + else: + extra_cflags.insert(0, "-lang=c") + + cflags_str = make_flags_str(cflags) + if len(extra_cflags) > 0: + extra_cflags_str = make_flags_str(extra_cflags) + cflags_str += " " + extra_cflags_str + used_compiler_versions.add(obj.options["mw_version"]) + # Add MWCC build rule lib_name = obj.options["lib"] n.comment(f"{obj.name}: {lib_name} (linked {obj.completed})") @@ -770,7 +837,7 @@ def generate_build_ninja( if obj.options["host"] and obj.host_obj_path is not None: n.build( outputs=obj.host_obj_path, - rule="host_cc" if src_path.suffix == ".c" else "host_cpp", + rule="host_cc" if file_is_c(src_path) else "host_cpp", inputs=src_path, variables={ "basedir": os.path.dirname(obj.host_obj_path), @@ -792,7 +859,7 @@ def generate_build_ninja( if obj.options["asflags"] is None: sys.exit("ProjectConfig.asflags missing") asflags_str = make_flags_str(obj.options["asflags"]) - if obj.options["extra_asflags"] is not None: + if len(obj.options["extra_asflags"]) > 0: extra_asflags_str = make_flags_str(obj.options["extra_asflags"]) asflags_str += " " + extra_asflags_str @@ -830,10 +897,10 @@ def generate_build_ninja( link_built_obj = obj.completed built_obj_path: Optional[Path] = None if obj.src_path is not None and obj.src_path.exists(): - if obj.src_path.suffix in (".c", ".cp", ".cpp"): + if file_is_c_cpp(obj.src_path): # Add MWCC & host build rules built_obj_path = c_build(obj, obj.src_path) - elif obj.src_path.suffix == ".s": + elif file_is_asm(obj.src_path): # Add assembler build rule built_obj_path = asm_build(obj, obj.src_path, obj.src_obj_path) else: @@ -1040,7 +1107,12 @@ def generate_build_ninja( n.build( outputs=progress_path, rule="progress", - implicit=[ok_path, configure_script, python_lib, config.config_path], + implicit=[ + ok_path, + configure_script, + python_lib, + report_path, + ], ) ### @@ -1153,8 +1225,10 @@ def generate_build_ninja( if build_config: if config.non_matching: n.default(link_outputs) - else: + elif config.progress: n.default(progress_path) + else: + n.default(ok_path) else: n.default(build_config_path) @@ -1173,6 +1247,13 @@ def generate_objdiff_config( if build_config is None: return + # Load existing objdiff.json + existing_units = {} + if Path("objdiff.json").is_file(): + with open("objdiff.json", "r", encoding="utf-8") as r: + existing_config = json.load(r) + existing_units = {unit["name"]: unit for unit in existing_config["units"]} + objdiff_config: Dict[str, Any] = { "min_version": "2.0.0-beta.5", "custom_make": "ninja", @@ -1194,7 +1275,6 @@ def generate_objdiff_config( } # decomp.me compiler name mapping - # Commented out versions have not been added to decomp.me yet COMPILER_MAP = { "GC/1.0": "mwcc_233_144", "GC/1.1": "mwcc_233_159", @@ -1231,15 +1311,27 @@ def generate_objdiff_config( ) -> None: obj_path, obj_name = build_obj["object"], build_obj["name"] base_object = Path(obj_name).with_suffix("") + name = str(Path(module_name) / base_object).replace(os.sep, "/") unit_config: Dict[str, Any] = { - "name": Path(module_name) / base_object, + "name": name, "target_path": obj_path, + "base_path": None, + "scratch": None, "metadata": { - "auto_generated": build_obj["autogenerated"], + "complete": None, + "reverse_fn_order": None, + "source_path": None, "progress_categories": progress_categories, + "auto_generated": build_obj["autogenerated"], }, + "symbol_mappings": None, } + # Preserve existing symbol mappings + existing_unit = existing_units.get(name) + if existing_unit is not None: + unit_config["symbol_mappings"] = existing_unit.get("symbol_mappings") + obj = objects.get(obj_name) if obj is None: objdiff_config["units"].append(unit_config) @@ -1248,40 +1340,31 @@ def generate_objdiff_config( src_exists = obj.src_path is not None and obj.src_path.exists() if src_exists: unit_config["base_path"] = obj.src_obj_path + unit_config["metadata"]["source_path"] = obj.src_path cflags = obj.options["cflags"] reverse_fn_order = False - if type(cflags) is list: - for flag in cflags: - if not flag.startswith("-inline "): - continue - for value in flag.split(" ")[1].split(","): - if value == "deferred": - reverse_fn_order = True - elif value == "nodeferred": - reverse_fn_order = False + for flag in cflags: + if not flag.startswith("-inline "): + continue + for value in flag.split(" ")[1].split(","): + if value == "deferred": + reverse_fn_order = True + elif value == "nodeferred": + reverse_fn_order = False - # Filter out include directories - def keep_flag(flag): - return not flag.startswith("-i ") and not flag.startswith("-I ") + # Filter out include directories + def keep_flag(flag): + return not flag.startswith("-i ") and not flag.startswith("-I ") - cflags = list(filter(keep_flag, cflags)) - - # Add appropriate lang flag - if obj.src_path is not None and not any( - flag.startswith("-lang") for flag in cflags - ): - if obj.src_path.suffix in (".cp", ".cpp"): - cflags.insert(0, "-lang=c++") - else: - cflags.insert(0, "-lang=c") + cflags = list(filter(keep_flag, cflags)) compiler_version = COMPILER_MAP.get(obj.options["mw_version"]) if compiler_version is None: print(f"Missing scratch compiler mapping for {obj.options['mw_version']}") else: cflags_str = make_flags_str(cflags) - if obj.options["extra_cflags"] is not None: + if len(obj.options["extra_cflags"]) > 0: extra_cflags_str = make_flags_str(obj.options["extra_cflags"]) cflags_str += " " + extra_cflags_str unit_config["scratch"] = { @@ -1305,7 +1388,6 @@ def generate_objdiff_config( { "complete": obj.completed, "reverse_fn_order": reverse_fn_order, - "source_path": obj.src_path, "progress_categories": progress_categories, } ) @@ -1349,160 +1431,357 @@ def generate_objdiff_config( for category in config.progress_categories: add_category(category.id, category.name) + def cleandict(d): + if isinstance(d, dict): + return {k: cleandict(v) for k, v in d.items() if v is not None} + elif isinstance(d, list): + return [cleandict(v) for v in d] + else: + return d + # Write objdiff.json with open("objdiff.json", "w", encoding="utf-8") as w: def unix_path(input: Any) -> str: return str(input).replace(os.sep, "/") if input else "" - json.dump(objdiff_config, w, indent=4, default=unix_path) + json.dump(cleandict(objdiff_config), w, indent=2, default=unix_path) + + +def generate_compile_commands( + config: ProjectConfig, + objects: Dict[str, Object], + build_config: Optional[Dict[str, Any]], +) -> None: + if build_config is None or not config.generate_compile_commands: + return + + # The following code attempts to convert mwcc flags to clang flags + # for use with clangd. + + # Flags to ignore explicitly + CFLAG_IGNORE: Set[str] = { + # Search order modifier + # Has a different meaning to Clang, and would otherwise + # be picked up by the include passthrough prefix + "-I-", + "-i-", + } + CFLAG_IGNORE_PREFIX: Tuple[str, ...] = ( + # Recursive includes are not supported by modern compilers + "-ir ", + ) + + # Flags to replace + CFLAG_REPLACE: Dict[str, str] = {} + CFLAG_REPLACE_PREFIX: Tuple[Tuple[str, str], ...] = ( + # Includes + ("-i ", "-I"), + ("-I ", "-I"), + ("-I+", "-I"), + # Defines + ("-d ", "-D"), + ("-D ", "-D"), + ("-D+", "-D"), + ) + + # Flags with a finite set of options + CFLAG_REPLACE_OPTIONS: Tuple[Tuple[str, Dict[str, Tuple[str, ...]]], ...] = ( + # Exceptions + ( + "-Cpp_exceptions", + { + "off": ("-fno-cxx-exceptions",), + "on": ("-fcxx-exceptions",), + }, + ), + # RTTI + ( + "-RTTI", + { + "off": ("-fno-rtti",), + "on": ("-frtti",), + }, + ), + # Language configuration + ( + "-lang", + { + "c": ("--language=c", "--std=c99"), + "c99": ("--language=c", "--std=c99"), + "c++": ("--language=c++", "--std=c++98"), + "cplus": ("--language=c++", "--std=c++98"), + }, + ), + # Enum size + ( + "-enum", + { + "min": ("-fshort-enums",), + "int": ("-fno-short-enums",), + }, + ), + # Common BSS + ( + "-common", + { + "off": ("-fno-common",), + "on": ("-fcommon",), + }, + ), + ) + + # Flags to pass through + CFLAG_PASSTHROUGH: Set[str] = set() + CFLAG_PASSTHROUGH_PREFIX: Tuple[str, ...] = ( + "-I", # includes + "-D", # defines + ) + + clangd_config = [] + + def add_unit(build_obj: Dict[str, Any]) -> None: + obj = objects.get(build_obj["name"]) + if obj is None: + return + + # Skip unresolved objects + if ( + obj.src_path is None + or obj.src_obj_path is None + or not file_is_c_cpp(obj.src_path) + ): + return + + # Gather cflags for source file + cflags: list[str] = [] + + def append_cflags(flags: Iterable[str]) -> None: + # Match a flag against either a set of concrete flags, or a set of prefixes. + def flag_match( + flag: str, concrete: Set[str], prefixes: Tuple[str, ...] + ) -> bool: + if flag in concrete: + return True + + for prefix in prefixes: + if flag.startswith(prefix): + return True + + return False + + # Determine whether a flag should be ignored. + def should_ignore(flag: str) -> bool: + return flag_match(flag, CFLAG_IGNORE, CFLAG_IGNORE_PREFIX) + + # Determine whether a flag should be passed through. + def should_passthrough(flag: str) -> bool: + return flag_match(flag, CFLAG_PASSTHROUGH, CFLAG_PASSTHROUGH_PREFIX) + + # Attempts replacement for the given flag. + def try_replace(flag: str) -> bool: + replacement = CFLAG_REPLACE.get(flag) + if replacement is not None: + cflags.append(replacement) + return True + + for prefix, replacement in CFLAG_REPLACE_PREFIX: + if flag.startswith(prefix): + cflags.append(flag.replace(prefix, replacement, 1)) + return True + + for prefix, options in CFLAG_REPLACE_OPTIONS: + if not flag.startswith(prefix): + continue + + # "-lang c99" and "-lang=c99" are both generally valid option forms + option = flag.removeprefix(prefix).removeprefix("=").lstrip() + replacements = options.get(option) + if replacements is not None: + cflags.extend(replacements) + + return True + + return False + + for flag in flags: + # Ignore flags first + if should_ignore(flag): + continue + + # Then find replacements + if try_replace(flag): + continue + + # Pass flags through last + if should_passthrough(flag): + cflags.append(flag) + continue + + append_cflags(obj.options["cflags"]) + append_cflags(obj.options["extra_cflags"]) + cflags.extend(config.extra_clang_flags) + cflags.extend(obj.options["extra_clang_flags"]) + + unit_config = { + "directory": Path.cwd(), + "file": obj.src_path, + "output": obj.src_obj_path, + "arguments": [ + "clang", + "-nostdinc", + "-fno-builtin", + "--target=powerpc-eabi", + *cflags, + "-c", + obj.src_path, + "-o", + obj.src_obj_path, + ], + } + clangd_config.append(unit_config) + + # Add DOL units + for unit in build_config["units"]: + add_unit(unit) + + # Add REL units + for module in build_config["modules"]: + for unit in module["units"]: + add_unit(unit) + + # Write compile_commands.json + with open("compile_commands.json", "w", encoding="utf-8") as w: + + def default_format(o): + if isinstance(o, Path): + return o.resolve().as_posix() + return str(o) + + json.dump(clangd_config, w, indent=2, default=default_format) # Calculate, print and write progress to progress.json def calculate_progress(config: ProjectConfig) -> None: config.validate() - objects = config.objects() out_path = config.out_path() - build_config = load_build_config(config, out_path / "config.json") - if build_config is None: - return + report_path = out_path / "report.json" + if not report_path.is_file(): + sys.exit(f"Report file {report_path} does not exist") - class ProgressUnit: - def __init__(self, name: str) -> None: - self.name: str = name - self.code_total: int = 0 - self.code_fancy_frac: int = config.progress_code_fancy_frac - self.code_fancy_item: str = config.progress_code_fancy_item - self.code_progress: int = 0 - self.data_total: int = 0 - self.data_fancy_frac: int = config.progress_data_fancy_frac - self.data_fancy_item: str = config.progress_data_fancy_item - self.data_progress: int = 0 - self.objects_progress: int = 0 - self.objects_total: int = 0 - self.objects: Set[Object] = set() + report_data: Dict[str, Any] = {} + with open(report_path, "r", encoding="utf-8") as f: + report_data = json.load(f) - def add(self, build_obj: Dict[str, Any]) -> None: - self.code_total += build_obj["code_size"] - self.data_total += build_obj["data_size"] + # Convert string numbers (u64) to int + def convert_numbers(data: Dict[str, Any]) -> None: + for key, value in data.items(): + if isinstance(value, str) and value.isdigit(): + data[key] = int(value) - # Avoid counting the same object in different modules twice - include_object = build_obj["name"] not in self.objects - if include_object: - self.objects.add(build_obj["name"]) - self.objects_total += 1 + convert_numbers(report_data["measures"]) + for category in report_data.get("categories", []): + convert_numbers(category["measures"]) - if build_obj["autogenerated"]: - # Skip autogenerated objects - return + # Output to GitHub Actions job summary, if available + summary_path = os.getenv("GITHUB_STEP_SUMMARY") + summary_file: Optional[IO[str]] = None + if summary_path: + summary_file = open(summary_path, "a", encoding="utf-8") + summary_file.write("```\n") - obj = objects.get(build_obj["name"]) - if obj is None or not obj.completed: - return - - self.code_progress += build_obj["code_size"] - self.data_progress += build_obj["data_size"] - if include_object: - self.objects_progress += 1 - - def code_frac(self) -> float: - if self.code_total == 0: - return 1.0 - return self.code_progress / self.code_total - - def data_frac(self) -> float: - if self.data_total == 0: - return 1.0 - return self.data_progress / self.data_total - - progress_units: Dict[str, ProgressUnit] = {} - if config.progress_all: - progress_units["all"] = ProgressUnit("All") - progress_units["dol"] = ProgressUnit("DOL") - if len(build_config["modules"]) > 0: - if config.progress_modules: - progress_units["modules"] = ProgressUnit("Modules") - if len(config.progress_categories) > 0: - for category in config.progress_categories: - progress_units[category.id] = ProgressUnit(category.name) - if config.progress_each_module: - for module in build_config["modules"]: - progress_units[module["name"]] = ProgressUnit(module["name"]) - - def add_unit(id: str, unit: Dict[str, Any]) -> None: - progress = progress_units.get(id) - if progress is not None: - progress.add(unit) - - # Add DOL units - for unit in build_config["units"]: - add_unit("all", unit) - add_unit("dol", unit) - obj = objects.get(unit["name"]) - if obj is not None: - category_opt = obj.options["progress_category"] - if isinstance(category_opt, list): - for id in category_opt: - add_unit(id, unit) - elif category_opt is not None: - add_unit(category_opt, unit) - - # Add REL units - for module in build_config["modules"]: - for unit in module["units"]: - add_unit("all", unit) - add_unit("modules", unit) - add_unit(module["name"], unit) - obj = objects.get(unit["name"]) - if obj is not None: - category_opt = obj.options["progress_category"] - if isinstance(category_opt, list): - for id in category_opt: - add_unit(id, unit) - elif category_opt is not None: - add_unit(category_opt, unit) + def progress_print(s: str) -> None: + print(s) + if summary_file: + summary_file.write(s + "\n") # Print human-readable progress - print("Progress:") + progress_print("Progress:") - def print_category(unit: Optional[ProgressUnit]) -> None: - if unit is None: - return + def print_category(name: str, measures: Dict[str, Any]) -> None: + total_code = measures.get("total_code", 0) + matched_code = measures.get("matched_code", 0) + matched_code_percent = measures.get("matched_code_percent", 0) + total_data = measures.get("total_data", 0) + matched_data = measures.get("matched_data", 0) + matched_data_percent = measures.get("matched_data_percent", 0) + total_functions = measures.get("total_functions", 0) + matched_functions = measures.get("matched_functions", 0) + complete_code_percent = measures.get("complete_code_percent", 0) + total_units = measures.get("total_units", 0) + complete_units = measures.get("complete_units", 0) - code_frac = unit.code_frac() - data_frac = unit.data_frac() - print( - f" {unit.name}: {code_frac:.2%} code, {data_frac:.2%} data ({unit.objects_progress} / {unit.objects_total} files)" + progress_print( + f" {name}: {matched_code_percent:.2f}% matched, {complete_code_percent:.2f}% linked ({complete_units} / {total_units} files)" + ) + progress_print( + f" Code: {matched_code} / {total_code} bytes ({matched_functions} / {total_functions} functions)" + ) + progress_print( + f" Data: {matched_data} / {total_data} bytes ({matched_data_percent:.2f}%)" ) - print(f" Code: {unit.code_progress} / {unit.code_total} bytes") - print(f" Data: {unit.data_progress} / {unit.data_total} bytes") - if config.progress_use_fancy: - print( - "\nYou have {} out of {} {} and {} out of {} {}.".format( - math.floor(code_frac * unit.code_fancy_frac), - unit.code_fancy_frac, - unit.code_fancy_item, - math.floor(data_frac * unit.data_fancy_frac), - unit.data_fancy_frac, - unit.data_fancy_item, - ) - ) - for progress in progress_units.values(): - print_category(progress) + print_category("All", report_data["measures"]) + for category in report_data.get("categories", []): + if config.print_progress_categories is True or ( + isinstance(config.print_progress_categories, list) + and category["id"] in config.print_progress_categories + ): + print_category(category["name"], category["measures"]) + + if config.progress_use_fancy: + measures = report_data["measures"] + total_code = measures.get("total_code", 0) + total_data = measures.get("total_data", 0) + if total_code == 0 or total_data == 0: + return + code_frac = measures.get("complete_code", 0) / total_code + data_frac = measures.get("complete_data", 0) / total_data + + progress_print( + "\nYou have {} out of {} {} and {} out of {} {}.".format( + math.floor(code_frac * config.progress_code_fancy_frac), + config.progress_code_fancy_frac, + config.progress_code_fancy_item, + math.floor(data_frac * config.progress_data_fancy_frac), + config.progress_data_fancy_frac, + config.progress_data_fancy_item, + ) + ) + + # Finalize GitHub Actions job summary + if summary_file: + summary_file.write("```\n") + summary_file.close() # Generate and write progress.json progress_json: Dict[str, Any] = {} - def add_category(category: str, unit: ProgressUnit) -> None: - progress_json[category] = { - "code": unit.code_progress, - "code/total": unit.code_total, - "data": unit.data_progress, - "data/total": unit.data_total, + def add_category(id: str, measures: Dict[str, Any]) -> None: + progress_json[id] = { + "code": measures.get("complete_code", 0), + "code/total": measures.get("total_code", 0), + "data": measures.get("complete_data", 0), + "data/total": measures.get("total_data", 0), + "matched_code": measures.get("matched_code", 0), + "matched_code/total": measures.get("total_code", 0), + "matched_data": measures.get("matched_data", 0), + "matched_data/total": measures.get("total_data", 0), + "matched_functions": measures.get("matched_functions", 0), + "matched_functions/total": measures.get("total_functions", 0), + "fuzzy_match": int(measures.get("fuzzy_match_percent", 0) * 100), + "fuzzy_match/total": 10000, + "units": measures.get("complete_units", 0), + "units/total": measures.get("total_units", 0), } - for id, progress in progress_units.items(): - add_category(id, progress) + if config.progress_all: + add_category("all", report_data["measures"]) + else: + # Support for old behavior where "dol" was the main category + add_category("dol", report_data["measures"]) + for category in report_data.get("categories", []): + add_category(category["id"], category["measures"]) + with open(out_path / "progress.json", "w", encoding="utf-8") as w: - json.dump(progress_json, w, indent=4) + json.dump(progress_json, w, indent=2)