From 0b8437737bdd02774b842f99fb913216315355de Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 23 Sep 2023 20:20:47 +0200 Subject: [PATCH] initial commit --- .clang-format | 12 + .github/workflows/build_docker.yml | 26 + .github/workflows/lint.yml | 38 ++ .github/workflows/release.yml | 145 +++++ .pre-commit-config.yaml | 10 + CHANGELOG.md | 1 + CONTRIBUTING.md | 222 ++++++++ COPYING.md | 676 ++++++++++++++++++++++++ Makefile | 39 ++ README.md | 61 +++ docker/game-win/Dockerfile | 44 ++ docker/game-win/entrypoint.sh | 17 + docker/game-win/meson_linux_mingw32.txt | 19 + docs/ida_types.h | 5 + docs/ida_variables.txt | 8 + meson.build | 97 ++++ src/filesystem.c | 207 ++++++++ src/filesystem.h | 57 ++ src/log.c | 51 ++ src/log.h | 10 + src/main_dll.c | 23 + src/main_exe.c | 82 +++ src/memory.c | 44 ++ src/memory.h | 37 ++ src/specific/s_filesystem.c | 149 ++++++ src/specific/s_filesystem.h | 5 + tools/generate_init | 40 ++ tools/generate_rcfile | 41 ++ tools/generate_version | 26 + tools/ida_import.py | 61 +++ tools/resources/icon.ico | Bin 0 -> 57471 bytes tools/resources/icon.rc | 1 + tools/resources/version.rc | 22 + tools/sort_imports | 104 ++++ 34 files changed, 2380 insertions(+) create mode 100644 .clang-format create mode 100644 .github/workflows/build_docker.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 COPYING.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 docker/game-win/Dockerfile create mode 100755 docker/game-win/entrypoint.sh create mode 100644 docker/game-win/meson_linux_mingw32.txt create mode 100644 docs/ida_types.h create mode 100644 docs/ida_variables.txt create mode 100644 meson.build create mode 100644 src/filesystem.c create mode 100644 src/filesystem.h create mode 100644 src/log.c create mode 100644 src/log.h create mode 100644 src/main_dll.c create mode 100644 src/main_exe.c create mode 100644 src/memory.c create mode 100644 src/memory.h create mode 100644 src/specific/s_filesystem.c create mode 100644 src/specific/s_filesystem.h create mode 100644 tools/generate_init create mode 100644 tools/generate_rcfile create mode 100644 tools/generate_version create mode 100644 tools/ida_import.py create mode 100644 tools/resources/icon.ico create mode 100644 tools/resources/icon.rc create mode 100644 tools/resources/version.rc create mode 100644 tools/sort_imports diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..6a8f79df --- /dev/null +++ b/.clang-format @@ -0,0 +1,12 @@ +--- +AlignAfterOpenBracket: AlwaysBreak +AllowShortFunctionsOnASingleLine: None +AlwaysBreakAfterReturnType: None +BasedOnStyle: Webkit +BreakBeforeBinaryOperators: NonAssignment +ColumnLimit: 80 +IndentPPDirectives: BeforeHash +NamespaceIndentation: None +PenaltyReturnTypeOnItsOwnLine: 1000 +PointerAlignment: Right +SortIncludes: false diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml new file mode 100644 index 00000000..4ca0de0f --- /dev/null +++ b/.github/workflows/build_docker.yml @@ -0,0 +1,26 @@ +name: Build Docker toolchain + +on: + - workflow_dispatch + +jobs: + publish_docker_image: + name: Build Docker toolchain + runs-on: ubuntu-latest + steps: + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Checkout code + uses: actions/checkout@v2 + with: + path: . + fetch-depth: 0 + + - name: Build Docker image + run: | + docker build -t "rrdash/tomb2main:latest" . -f docker/game/Dockerfile + docker push "rrdash/tomb2main:latest" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..9f02fd83 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,38 @@ +name: Run code linters + +on: + - push + - pull_request + +jobs: + lint: + name: Run code linters + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + path: . + fetch-depth: 0 + + - name: Install dependencies + run: | + wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|sudo apt-key add - + echo 'deb http://apt.llvm.org/focal/ llvm-toolchain-focal-12 main' | sudo tee -a /etc/apt/sources.list + echo 'deb-src http://apt.llvm.org/focal/ llvm-toolchain-focal-12 main' | sudo tee -a /etc/apt/sources.list + sudo apt update + sudo apt-get install -y clang-format-12 iwyu + sudo ln -s /usr/bin/clang-format-12 /usr/local/bin/clang-format + sudo apt-get install -y make python3-pip + sudo python3 -m pip install pyjson5 + + - name: Check imports + run: | + git add -A + python3 tools/sort_imports + git diff --exit-code || ( echo 'Please run `make imports` and commit the changes.'; exit 1 ) + + - name: Check formatted code differences + run: | + make lint + git diff --exit-code || ( echo 'Please run `make lint` and commit the changes.'; exit 1 ) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f59c94de --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,145 @@ + +name: Publish a new release + +permissions: + contents: write + +on: + push: + branch: stable + tags: + - 'v?[0-9]*' + workflow_call: + inputs: + draft: + description: 'Draft' + required: true + default: false + type: boolean + prerelease: + description: 'Prerelease' + required: true + type: boolean + workflow_dispatch: + inputs: + draft: + description: 'Draft' + required: true + default: true + type: boolean + prerelease: + description: 'Prerelease' + required: true + default: false + type: boolean + +jobs: + publish_release: + name: Create a GitHub release + runs-on: ubuntu-latest + needs: [build_game_win] + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + path: . + fetch-depth: 0 + + - name: Download built game asset + uses: actions/download-artifact@v1 + with: + path: artifacts/ + name: game-win-all + + - name: Extract tag name + id: get_version + run: echo ::set-output name=VERSION::$(git describe --abbrev=7 --tags) + + - name: Rename assets + run: | + mv artifacts/game-win.zip artifacts/Tomb2Main-${{ steps.get_version.outputs.VERSION }}-Windows.zip + + - name: Generate Changelog + run: | + python3 -c ''' + import re + from pathlib import Path + section = [s for s in Path("CHANGELOG.md").read_text().split("\n\n") if re.search("- \w", s)][0] + print("\n".join(line for line in section.splitlines() if not line.startswith("#"))) + ''' > artifacts/changes.txt + + - name: Release + uses: softprops/action-gh-release@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Release ${{ github.ref_name }} + body_path: artifacts/changes.txt + draft: ${{ inputs.draft }} + prerelease: ${{ inputs.prerelease }} + fail_on_unmatched_files: true + files: | + artifacts/Tomb2Main-${{ steps.get_version.outputs.VERSION }}-Windows.zip + + build_game_win: + name: Build the game (Windows) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + path: . + fetch-depth: 0 + + - name: Install dependencies + run: | + echo "$GITHUB_CONTEXT" + sudo apt-get update + sudo apt-get install -y make moby-engine moby-cli + + - name: Build the game + run: | + make clean release + mkdir out/ + cp build/win/*.exe out/ + cp build/win/*.dll out/ + cp -r bin/* out/ + + - name: Upload the artifact + uses: actions/upload-artifact@v1 + with: + name: game-win + path: out/ + + package_game_win: + name: Package the game (Windows) + needs: [build_game_win] + runs-on: ubuntu-latest + steps: + - name: Download built config tool assets + uses: actions/download-artifact@v1 + with: + path: artifacts/ + name: configtool + + - name: Download built game assets + uses: actions/download-artifact@v1 + with: + path: artifacts/ + name: game-win + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y make p7zip-full + + - name: Package the game + run: | + mkdir out + cd artifacts + 7z a ../out/game-win.zip * + + - name: Upload the artifact + uses: actions/upload-artifact@v1 + with: + name: game-win-all + path: out/game-win.zip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..995333c7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: local + hooks: + - id: clang-format + name: clang-format + entry: clang-format + args: ["-style=file", "-i"] + language: system + files: \.[ch](pp)?$ + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ef2cc65b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +## [Unreleased](https://github.com/rr-/Tomb2Main/compare/stable...develop) - ××××-××-×× diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..44bafa6f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,222 @@ +# Development guidelines + +## Build workflow + +- Compile the project (described in the next section) +- Copy all .dll and .exe files from `build/` to your game directory +- Copy the contents of `bin/` to your game directory + +## Compiling + +### Compiling on Ubuntu + +- **With Docker**: + + Make sure to install Docker and make, then run `make debug`. + The binaries should appear in the `build/` directory. + +- **Without Docker**: + + This scenario is not officially supported, but you can see how it's done by + examining the `docker` configuration files for the external dependencies, + and `meson.build` for the local files, then tailoring your system to match + the process. + +### Compiling on Windows + +- **Using WSL**: + + Install WSL (video guide: https://www.youtube.com/watch?v=5RTSlby-l9w) + + - Run Powershell as Administrator + - Copy and paste the following command: `Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux` + - Restart the computer + - Go to Windows App Store + - Install Ubuntu + + Run WSL and continue with the instructions from the `Compiling on Ubuntu` section. + +### Supported compilers + +Please be advised that any build systems that are not the one we use for +automating releases (= mingw-w64) come at user's own risk. They might crash or +even refuse to compile. + +## Coding conventions + +While the existing source code does not conform to the rules because it uses +the original Core Design's naming, new code should adhere to the following +guidelines: + +- Variables are `lower_snake_case` +- Global variables are `g_PascalCase` +- Module variables are `m_PascalCase` +- Function names are `Module_PascalCase` +- Macros are `UPPER_SNAKE_CASE` +- Struct names are `UPPER_SNAKE_CASE` +- Struct members are `lower_snake_case` +- Enum names are `UPPER_SNAKE_CASE` +- Enum members are `UPPER_SNAKE_CASE` + +Try to avoid global variables, if possible declare them as `static` in the +module you're using them. Changes to original game functionality most often +should be configurable. + +Other things: + +- We use clang-format to automatically format the code +- We do not omit `{` and `}` +- We use K&R brace style +- We condense `if` expressions into one, so: + ``` + if (a && b) { + } + ``` + + and not: + + ``` + if (a) { + if (b) { + } + } + ``` + + If the expressions are extraordinarily complex, we refactor these into + smaller conditions or functions. + +### Code formatting + +This project uses `clang-format` to take care of automatic code formatting. To +ensure your code conforms to the standard, please run `make lint` after each +commit. If for some reason you can't run it, don't worry, our CI pipeline will +show you what need to be changed in case of mistakes. + +## Submitting changes + +We commit via pull requests and avoid committing directly to `develop`, which +is a protected branch. Each pull request gets peer-reviewed and should have at +least one approval from the code developer team before merging. We never merge +until all discussions are marked as resolved and generally try to test things +before merging. When a remark on the code review is trivial and the PR author +has pushed a fix for it, it should be resolved by the pull request author. +Otherwise we don't mark the discussions as resolved and give a chance for the +reviewer to reply. Once all change requests are addressed, we should re-request +a review from the interested parties. + +## Changelog + +We keep a changelog in `CHANGELOG.md`. Anything other than an internal change +or refactor needs an entry there. Furthermore, new features and OG bugfixes +should be documented in README as well. + +## Commit scope + +Either you can make a lot of throwaway commits such as 'test' 'continue +testing' 'fix 1' 'fix 2' 'fix of the fix' and then squash your pull request as +a whole, or you can craft a nice history with proper commit messages and then +merge-rebase. The first case is mostly acceptable for isolated features, but in +general we prefer the latter approach. As a principle, refactors should made in +separate commits. Code review changes are best made incrementally and then +squashed prior to merging, for easing the review process. + +## Commit messages + +**The most important thing to remember:** bug fixes and feature implementations +should always include the phrase `Resolves #123`. If there's no ticket and the +pull request you're making contains player-facing changes, a ticket needs +to be created first – no exceptions. + +Anything else is just for consistency and general neatness. Our commit messages +aim to respect the 50/72 rule and have the following form: + + module-prefix: description in an imperative mood (max 50 characters) + + Longer description of what happens that can span multiple lines. Each + line should be maximally 72 characters long, with the exceptions of + code/log dumps. + +The prefix should describe the module that the pull request touches the most. +In general this is the name of the `.c` or `.h` file with the most changes. +Note that this includes the folder names which are separated with `/`. Avoid +underscores (`_`) in favor of dashes (`-`). + +The description should be as concise as possible; any details should be given +in the commit message body. Use simple, to the point words like `add`, `fix`, +`remove`, `improve`. + +Good: +``` +ui: improve resolution changing + +Added the ability for the player to switch resolutions directly from +the game ui. + +Resolves #123. +``` + +Great: +``` +log: fix varargs for Log_Message() + +On Linux, the engine crashes when printing the log messages. This +happens because the current code re-uses the same va_list variable on +two calls to vprintf() and vfprintf(). Actually, this is not allowed. +For using the same information on multiple formatting functions, it is +needed to create a copy of the primary va_list to a second one, by using +va_copy(). After rewriting properly the Log_Message() function, the +segmentation fault is gone. Tested on both Linux and Windows builds. +``` + +- This has no ticket number, but it was an internal change improving support + for a platform unsupported at that time, which made it acceptable. + +Bad: +``` +ui: implemented the ability to switch resolutions from the ui +``` +- the subject doesn't use imperative mood +- the subject is too long +- missing ticket number + +Bad: +``` +dart: added dart emitters to the savegame (#779) + +dart: added dart emitters to the savegame + +Add function for checking legacy savegame save flags +Resolves #774. +``` + +- it duplicates the subject in the message body +- the subject doesn't use imperative mood + +When merging via squash, it's OK to have GitHub append the pull request number, +but pay special attention to the body field which often gets filled with +garbage. + +## Branching model + +We have two branches: `develop` and `stable`. `develop` is where all changes +about to be published in the next release land. `stable` is the latest release. +We avoid creating merge commits between these two – they should always point to +the same HEAD when applicable. This means that any hotfixes that need to be +released ahead of unpublished work in `develop` are merged directly to +`stable`, and `develop` needs to be then rebased on top of the now-patched +`stable`. + +## Releasing a new version + +New version releases happen automatically whenever a new tag is pushed to the +`stable` branch with the help of GitHub actions. In general this is accompanied +with a special commit `docs: release X.Y.Z` that assigns unreleased changes to +a specific version. See git history for details. + +## Glossary + +- T2M: Tomb2Main +- OG: original game +- UK Box: the version released on discs in the UK +- Multipatch: the version released on Steam +- PS: PlayStation version of the game diff --git a/COPYING.md b/COPYING.md new file mode 100644 index 00000000..5fdf13ca --- /dev/null +++ b/COPYING.md @@ -0,0 +1,676 @@ +GNU GENERAL PUBLIC LICENSE +========================== +Version 3, 29 June 2007 +========================== + +> Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +# Preamble + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + +# TERMS AND CONDITIONS + +## 0. Definitions. + + _"This License"_ refers to version 3 of the GNU General Public License. + + _"Copyright"_ also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + _"The Program"_ refers to any copyrightable work licensed under this +License. Each licensee is addressed as _"you"_. _"Licensees"_ and +"recipients" may be individuals or organizations. + + To _"modify"_ a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a _"modified version"_ of the +earlier work or a work _"based on"_ the earlier work. + + A _"covered work"_ means either the unmodified Program or a work based +on the Program. + + To _"propagate"_ a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To _"convey"_ a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +## 1. Source Code. + + The _"source code"_ for a work means the preferred form of the work +for making modifications to it. _"Object code"_ means any non-source +form of a work. + + A _"Standard Interface"_ means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The _"System Libraries"_ of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The _"Corresponding Source"_ for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + +## 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +## 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +## 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +## 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +## 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A _"User Product"_ is either (1) a _"consumer product"_, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + _"Installation Information"_ for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +## 7. Additional Terms. + + _"Additional permissions"_ are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +## 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +## 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +## 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An _"entity transaction"_ is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +## 11. Patents. + + A _"contributor"_ is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's _"essential patent claims"_ are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +## 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +## 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +## 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +## 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +## 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +## 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +# END OF TERMS AND CONDITIONS +-------------------------------------------------------------------------- + + +# How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type 'show c' for details. + + The hypothetical commands _'show w'_ and _'show c'_ should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b12244b0 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +CWD = $(shell pwd) +HOST_USER_UID = $(shell id -u) +HOST_USER_GID = $(shell id -g) + +define build + $(eval TARGET := $(1)) + mkdir -p build + docker run --rm \ + --user $(HOST_USER_UID):$(HOST_USER_GID) \ + --entrypoint /app/docker/game-win/entrypoint.sh \ + -e TARGET="$(TARGET)" \ + -v $(CWD):/app/ \ + rrdash/tomb2main:latest +endef + +docker-image: + docker build --progress plain . -f docker/game-win/Dockerfile -t rrdash/tomb2main + +debug: + $(call build,debug) + +debugopt: + $(call build,debugoptimized) + +release: + $(call build,release) + +clean: + -find build/ -type f -delete + -find build/ -mindepth 1 -empty -type d -delete + +imports: + tools/sort_imports + +lint: + bash -c 'shopt -s globstar; clang-format -i **/*.c **/*.h' + +.PHONY: debug debugopt release clean imports lint + diff --git a/README.md b/README.md new file mode 100644 index 00000000..8d5ada51 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Tomb2Main + +Tomb2Main is an open-source decompilation project for Tomb Raider 2, created as +a sequel to the successful +[Tomb1Main](https://github.com/LostArtefacts/Tomb1Main/) project for Tomb +Raider 1. Our project is in the early stages, and our +main focus is on decompiling as much of the game as possible. + +## The situation + +Tomb2Main draws inspiration from existing decompilation efforts, including the +achievements of [TR2Main](https://github.com/Arsunt/TR2Main) developed by +[Arsunt](https://github.com/Arsunt/). However, we emphasize that Tomb2Main is +an independent project and does not directly copy code from TR2Main. + +## Our Mission + +Our mission is to contribute to the decompilation of Tomb Raider 2, providing a +deeper understanding of the game's inner workings. We aim to uncover its +secrets and enhance the overall gameplay experience. + +### Key Goals + +- **Comprehensive Decompilation:** + Our primary objective is to achieve a thorough decompilation of Tomb Raider + 2, enabling a deeper understanding of its inner workings. + +- **Enhancement of UK Box Version:** + Our work is based on the UK Box version of Tomb Raider 2. Eventually we hope + to reach feature parity with the Steam/multipatch version, and possibly + TR2Main as well. + +- **Cross-platform Compatibility:** + We are committed to making Tomb Raider 2 run natively on Linux, with the + possibility of supporting Mac systems in the future. + +- **Transparent Development Process:** + We value transparency and aim to maintain a transparent development process, + ensuring regular updates and engaging with the community. + +- **Changelog and Progress Updates:** + We strive to provide detailed and reliable changelogs, documenting + significant updates and changes made to Tomb2Main. Additionally, we will keep + the community informed about the progress of the project. + +Tomb2Main is currently in the early stages of development, focusing on the +decompilation process. We recognize that there is much work to be done. + +## Contributions + +We warmly welcome contributions from individuals who share our passion for +decompiling Tomb Raider 2. While our project is in the early stages, we believe +that through collaboration, we can achieve our goals together. + +Please refer to our [CONTRIBUTING.md](CONTRIBUTING.md) file for more information on how to +contribute to the project. + +## License + +Tomb2Main follows the same GPL license as TR2Main. Please refer to the LICENSE +file for further details. diff --git a/docker/game-win/Dockerfile b/docker/game-win/Dockerfile new file mode 100644 index 00000000..7f408094 --- /dev/null +++ b/docker/game-win/Dockerfile @@ -0,0 +1,44 @@ +# Tomb2Main building toolchain. +# +# This is a multi-stage Docker image. It is designed to keep the final image +# size low. Each stage builds an external dependency. The final stage takes the +# artifacts (binaries, includes etc.) from previous stages and installs all the +# tools necessary to build Tomb2Main. + +# MinGW +FROM ubuntu:latest as mingw + +# don't prompt during potential installation/update of tzinfo +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=Europe/Warsaw + +RUN apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y \ + gcc-mingw-w64-i686 \ + g++-mingw-w64-i686 \ + git \ + make + + +# Tomb2Main +FROM mingw + +# set the build dir - actual files are mounted with a Docker volume +RUN mkdir /app +WORKDIR /app + +# system dependencies +# configure pkgconfig manually +# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=967969 +RUN apt-get install -y \ + mingw-w64-tools \ + pkg-config \ + upx \ + python3-pip \ + && python3 -m pip install \ + pyjson5 \ + meson \ + ninja + +ENTRYPOINT ["/app/docker/game-win/entrypoint.sh"] diff --git a/docker/game-win/entrypoint.sh b/docker/game-win/entrypoint.sh new file mode 100755 index 00000000..519ba694 --- /dev/null +++ b/docker/game-win/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -x +set -e + +export CFLAGS=-DDOCKER_BUILD + +if [ ! -f /app/build/win/build.ninja ]; then + meson --buildtype "$TARGET" /app/build/win/ --cross /app/docker/game-win/meson_linux_mingw32.txt --pkg-config-path=$PKG_CONFIG_PATH +fi + +cd /app/build/win; meson compile + +if [ "$TARGET" = release ]; then + for file in *.exe; do + upx -t "$file" || ( i686-w64-mingw32-strip "$file" && upx "$file" ) + done +fi diff --git a/docker/game-win/meson_linux_mingw32.txt b/docker/game-win/meson_linux_mingw32.txt new file mode 100644 index 00000000..700919b9 --- /dev/null +++ b/docker/game-win/meson_linux_mingw32.txt @@ -0,0 +1,19 @@ +[binaries] +c = '/usr/bin/i686-w64-mingw32-gcc' +cpp = '/usr/bin/i686-w64-mingw32-g++' +objc = '/usr/bin/i686-w64-mingw32-gcc' +ar = '/usr/bin/i686-w64-mingw32-ar' +strip = '/usr/bin/i686-w64-mingw32-strip' +pkgconfig = '/usr/bin/i686-w64-mingw32-pkg-config' +windres = '/usr/bin/i686-w64-mingw32-windres' +exe_wrapper = 'wine' +ld = '/usr/bin/i686-w64-mingw32-ld' + +[properties] +skip_sanity_check = true + +[host_machine] +system = 'windows' +cpu_family = 'x86' +cpu = 'i686' +endian = 'little' diff --git a/docs/ida_types.h b/docs/ida_types.h new file mode 100644 index 00000000..f8e83753 --- /dev/null +++ b/docs/ida_types.h @@ -0,0 +1,5 @@ +// Type information for the IDA importer. +// +// This is unrelated to the Tomb2Main codebase which is a subject to changes +// and improvements with regard to the original game. This file documents the +// Tomb2.exe layout (UKBox version). diff --git a/docs/ida_variables.txt b/docs/ida_variables.txt new file mode 100644 index 00000000..2dddfc04 --- /dev/null +++ b/docs/ida_variables.txt @@ -0,0 +1,8 @@ +# Type information for the IDA importer. +# +# This is unrelated to the Tomb2Main codebase which is a subject to changes +# and improvements with regard to the original game. This file documents the +# Tomb2.exe layout (UKBox version). + +# Keep this file sorted by the offsets to find duplicates easily. + diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..6138171a --- /dev/null +++ b/meson.build @@ -0,0 +1,97 @@ +project('Tomb2Main', ['c'], + default_options: [ + 'c_std=c11', + 'warning_level=2', + ], +) + +warning_level = 3 + +c_compiler = meson.get_compiler('c') + +is_mingw = c_compiler.get_id() == 'gcc' and host_machine.system() == 'windows' +if is_mingw + add_project_link_arguments([], language: 'c') +endif + +build_opts = [ + '-Wno-unused', + '-D_GNU_SOURCE', +] +c_opts = [] +add_project_arguments(build_opts + c_opts, language: 'c') + +# autogenerated files +resources = [] +python3 = find_program('python3', required: true) +git = find_program('git', required: true) + +version = custom_target('version', + output: ['version.txt'], + command: [python3, meson.source_root() + '/tools/generate_version', '-o', '@OUTPUT0@'], + build_by_default: true, + build_always_stale: true +) + +init = custom_target( + 'fake_init', + depends: [version], + input: [version[0]], + output: ['init.c'], + command: [python3, meson.source_root() + '/tools/generate_init', '--version-file', '@INPUT@', '-o', '@OUTPUT0@'], + build_by_default: true, +) +version_rc = custom_target( + 'fake_version', + depends: [version], + input: [version[0]], + output: ['version.rc'], + command: [python3, meson.source_root() + '/tools/generate_rcfile', '--version-file', '@INPUT@', '-o', '@OUTPUT0@'], + build_by_default: true, +) +icon_rc = custom_target( + 'fake_icon', + input: [version[0]], + output: ['icon.rc'], + command: [python3, meson.source_root() + '/tools/generate_rcfile', '--version-file', '@INPUT@', '-o', '@OUTPUT0@'], +) +if host_machine.system() == 'windows' + windows = import('windows') + resources = [ + windows.compile_resources(version_rc), + windows.compile_resources(icon_rc), + ] + link_args = ['-static'] +else + link_args = [] +endif + +exe_sources = [ + init, + 'src/main_exe.c', + resources, +] + +dll_sources = [ + 'src/main_dll.c', + 'src/filesystem.c', + 'src/specific/s_filesystem.c', + 'src/log.c', + 'src/memory.c', +] + +executable( + 'Tomb2Main', + exe_sources, + name_prefix: '', + include_directories: ['src/'], + link_args: link_args, + gui_app: true, +) +library( + 'Tomb2Main', + dll_sources, + name_prefix: '', + include_directories: ['src/'], + link_args: link_args, +) diff --git a/src/filesystem.c b/src/filesystem.c new file mode 100644 index 00000000..1c64a2fb --- /dev/null +++ b/src/filesystem.c @@ -0,0 +1,207 @@ +#include "filesystem.h" + +#include "log.h" +#include "memory.h" +#include "specific/s_filesystem.h" + +#include +#include + +struct MYFILE { + FILE *fp; + const char *path; +}; + +static bool File_ExistsRaw(const char *path); + +static bool File_ExistsRaw(const char *path) +{ + FILE *fp = fopen(path, "rb"); + if (fp) { + fclose(fp); + return true; + } + return false; +} + +bool File_IsAbsolute(const char *path) +{ + return path && (path[0] == '/' || strstr(path, ":\\")); +} + +bool File_IsRelative(const char *path) +{ + return path && !File_IsAbsolute(path); +} + +const char *File_GetGameDirectory(void) +{ + return S_File_GetGameDirectory(); +} + +bool File_Exists(const char *path) +{ + char *full_path = File_GetFullPath(path); + bool ret = File_ExistsRaw(full_path); + Memory_FreePointer(&full_path); + return ret; +} + +char *File_GetFullPath(const char *path) +{ + char *full_path = NULL; + if (File_IsRelative(path)) { + const char *game_dir = File_GetGameDirectory(); + if (game_dir) { + full_path = Memory_Alloc(strlen(game_dir) + strlen(path) + 1); + sprintf(full_path, "%s%s", game_dir, path); + } + } + if (!full_path) { + full_path = Memory_DupStr(path); + } + + char *case_path = S_File_CasePath(full_path); + if (case_path) { + Memory_FreePointer(&full_path); + return case_path; + } + + return full_path; +} + +char *File_GuessExtension(const char *path, const char **extensions) +{ + if (!File_Exists(path)) { + const char *dot = strrchr(path, '.'); + if (dot) { + for (const char **ext = &extensions[0]; *ext; ext++) { + size_t out_size = dot - path + strlen(*ext) + 1; + char *out = Memory_Alloc(out_size); + strncpy(out, path, dot - path); + out[dot - path] = '\0'; + strcat(out, *ext); + if (File_Exists(out)) { + return out; + } + Memory_FreePointer(&out); + } + } + } + return Memory_DupStr(path); +} + +MYFILE *File_Open(const char *path, FILE_OPEN_MODE mode) +{ + char *full_path = File_GetFullPath(path); + MYFILE *file = Memory_Alloc(sizeof(MYFILE)); + file->path = Memory_DupStr(path); + switch (mode) { + case FILE_OPEN_WRITE: + file->fp = fopen(full_path, "wb"); + break; + case FILE_OPEN_READ: + file->fp = fopen(full_path, "rb"); + break; + case FILE_OPEN_READ_WRITE: + file->fp = fopen(full_path, "r+b"); + break; + default: + file->fp = NULL; + break; + } + Memory_FreePointer(&full_path); + if (!file->fp) { + Memory_FreePointer(&file); + } + return file; +} + +size_t File_Read(void *data, size_t item_size, size_t count, MYFILE *file) +{ + return fread(data, item_size, count, file->fp); +} + +size_t File_Write( + const void *data, size_t item_size, size_t count, MYFILE *file) +{ + return fwrite(data, item_size, count, file->fp); +} + +void File_CreateDirectory(const char *path) +{ + char *full_path = File_GetFullPath(path); + S_File_CreateDirectory(full_path); + Memory_FreePointer(&full_path); +} + +void File_Skip(MYFILE *file, size_t bytes) +{ + File_Seek(file, bytes, FILE_SEEK_CUR); +} + +void File_Seek(MYFILE *file, size_t pos, FILE_SEEK_MODE mode) +{ + switch (mode) { + case FILE_SEEK_SET: + fseek(file->fp, pos, SEEK_SET); + break; + case FILE_SEEK_CUR: + fseek(file->fp, pos, SEEK_CUR); + break; + case FILE_SEEK_END: + fseek(file->fp, pos, SEEK_END); + break; + } +} + +size_t File_Pos(MYFILE *file) +{ + return ftell(file->fp); +} + +size_t File_Size(MYFILE *file) +{ + size_t old = ftell(file->fp); + fseek(file->fp, 0, SEEK_END); + size_t size = ftell(file->fp); + fseek(file->fp, old, SEEK_SET); + return size; +} + +const char *File_GetPath(MYFILE *file) +{ + return file->path; +} + +void File_Close(MYFILE *file) +{ + fclose(file->fp); + Memory_FreePointer(&file->path); + Memory_FreePointer(&file); +} + +bool File_Load(const char *path, char **output_data, size_t *output_size) +{ + MYFILE *fp = File_Open(path, FILE_OPEN_READ); + if (!fp) { + LOG_ERROR("Can't open file %s", path); + return false; + } + + size_t data_size = File_Size(fp); + char *data = Memory_Alloc(data_size + 1); + if (File_Read(data, sizeof(char), data_size, fp) != data_size) { + LOG_ERROR("Can't read file %s", path); + Memory_FreePointer(&data); + return false; + } + File_Close(fp); + data[data_size] = '\0'; + + *output_data = data; + if (output_size) { + *output_size = data_size; + } + return true; +} diff --git a/src/filesystem.h b/src/filesystem.h new file mode 100644 index 00000000..14934b0a --- /dev/null +++ b/src/filesystem.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +typedef enum { + FILE_SEEK_SET, + FILE_SEEK_CUR, + FILE_SEEK_END, +} FILE_SEEK_MODE; + +typedef enum { + FILE_OPEN_READ, + FILE_OPEN_READ_WRITE, + FILE_OPEN_WRITE, +} FILE_OPEN_MODE; + +typedef struct MYFILE MYFILE; + +bool File_IsAbsolute(const char *path); + +bool File_IsRelative(const char *path); + +bool File_Exists(const char *path); + +const char *File_GetGameDirectory(void); + +// Get the absolute path to the given file, if possible. +// Internaly all operations on files within filesystem.c +// perform this normalization, so calling this function should +// only be necessary when interacting with external libraries. +char *File_GetFullPath(const char *path); + +char *File_GuessExtension(const char *path, const char **extensions); + +MYFILE *File_Open(const char *path, FILE_OPEN_MODE mode); + +size_t File_Read(void *data, size_t item_size, size_t count, MYFILE *file); + +size_t File_Write( + const void *data, size_t item_size, size_t count, MYFILE *file); + +void File_CreateDirectory(const char *path); + +size_t File_Pos(MYFILE *file); + +size_t File_Size(MYFILE *file); + +const char *File_GetPath(MYFILE *file); + +void File_Skip(MYFILE *file, size_t bytes); + +void File_Seek(MYFILE *file, size_t pos, FILE_SEEK_MODE mode); + +void File_Close(MYFILE *file); + +bool File_Load(const char *path, char **output_data, size_t *output_size); diff --git a/src/log.c b/src/log.c new file mode 100644 index 00000000..38517107 --- /dev/null +++ b/src/log.c @@ -0,0 +1,51 @@ +#include "log.h" + +#include "filesystem.h" +#include "memory.h" + +#include +#include + +FILE *m_LogHandle = NULL; + +void Log_Init(void) +{ + char *full_path = File_GetFullPath("Tomb2Main.log"); + m_LogHandle = fopen(full_path, "w"); + Memory_FreePointer(&full_path); +} + +void Log_Message( + const char *file, int line, const char *func, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + + // print to log file + if (m_LogHandle) { + va_list vb; + + va_copy(vb, va); + fprintf(m_LogHandle, "%s %d %s ", file, line, func); + vfprintf(m_LogHandle, fmt, vb); + fprintf(m_LogHandle, "\n"); + fflush(m_LogHandle); + + va_end(vb); + } + + // print to stdout + printf("%s %d %s ", file, line, func); + vprintf(fmt, va); + printf("\n"); + fflush(stdout); + + va_end(va); +} + +void Log_Shutdown(void) +{ + if (m_LogHandle) { + fclose(m_LogHandle); + } +} diff --git a/src/log.h b/src/log.h new file mode 100644 index 00000000..31e7b329 --- /dev/null +++ b/src/log.h @@ -0,0 +1,10 @@ +#pragma once + +#define LOG_INFO(...) Log_Message(__FILE__, __LINE__, __func__, __VA_ARGS__) +#define LOG_WARNING(...) Log_Message(__FILE__, __LINE__, __func__, __VA_ARGS__) +#define LOG_ERROR(...) Log_Message(__FILE__, __LINE__, __func__, __VA_ARGS__) +#define LOG_DEBUG(...) Log_Message(__FILE__, __LINE__, __func__, __VA_ARGS__) + +void Log_Init(void); +void Log_Message( + const char *file, int line, const char *func, const char *fmt, ...); diff --git a/src/main_dll.c b/src/main_dll.c new file mode 100644 index 00000000..435f3571 --- /dev/null +++ b/src/main_dll.c @@ -0,0 +1,23 @@ +#include "log.h" + +#include +#include + +BOOL APIENTRY +DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) +{ + switch (ul_reason_for_call) { + case DLL_PROCESS_ATTACH: + Log_Init(); + LOG_DEBUG("Injected DLL: DLL_PROCESS_ATTACH\n"); + break; + case DLL_THREAD_ATTACH: + break; + case DLL_THREAD_DETACH: + break; + case DLL_PROCESS_DETACH: + LOG_DEBUG("Injected DLL: DLL_PROCESS_DETACH\n"); + break; + } + return TRUE; +} diff --git a/src/main_exe.c b/src/main_exe.c new file mode 100644 index 00000000..14a24635 --- /dev/null +++ b/src/main_exe.c @@ -0,0 +1,82 @@ +#include +#include +#include + +// The path to the legitimate host process +const char *hostProcessPath = "Tomb2.exe"; + +bool fileExists(const char *filePath) +{ + DWORD fileAttributes = GetFileAttributes(filePath); + + if (fileAttributes != INVALID_FILE_ATTRIBUTES + && !(fileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + return true; + } + + return false; +} + +int main() +{ + char dll_path[MAX_PATH]; + GetModuleFileNameA(NULL, dll_path, MAX_PATH); + strcpy(strstr(dll_path, ".exe"), ".dll"); + + STARTUPINFO si; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(pi)); + + if (!CreateProcess( + hostProcessPath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, + NULL, &si, &pi)) { + fprintf(stderr, "Failed to create the process.\n"); + return 1; + } + + LPVOID loadLibraryAddr = + (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA"); + + if (!fileExists(dll_path)) { + fprintf(stderr, "DLL does not exist.\n"); + return 1; + } + + LPVOID dllPathAddr = VirtualAllocEx( + pi.hProcess, NULL, strlen(dll_path) + 1, MEM_RESERVE | MEM_COMMIT, + PAGE_READWRITE); + if (!dllPathAddr) { + fprintf(stderr, "Failed to allocate remote memory.\n"); + return 1; + } + + if (!WriteProcessMemory( + pi.hProcess, dllPathAddr, dll_path, strlen(dll_path) + 1, NULL)) { + fprintf(stderr, "Failed to write remote memory.\n"); + return 1; + } + + HANDLE hRemoteThread = CreateRemoteThread( + pi.hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)loadLibraryAddr, + dllPathAddr, 0, NULL); + if (hRemoteThread == INVALID_HANDLE_VALUE) { + fprintf(stderr, "Failed to create remote thread.\n"); + return 1; + } + WaitForSingleObject(hRemoteThread, INFINITE); + + VirtualFreeEx(pi.hProcess, dllPathAddr, strlen(dll_path) + 1, MEM_RELEASE); + CloseHandle(hRemoteThread); + + if (ResumeThread(pi.hThread) == (DWORD)-1) { + fprintf(stderr, "Failed to resume the execution of the process.\n"); + return 1; + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return 0; +} diff --git a/src/memory.c b/src/memory.c new file mode 100644 index 00000000..27c3c760 --- /dev/null +++ b/src/memory.c @@ -0,0 +1,44 @@ +#include "memory.h" + +#include +#include +#include + +void *Memory_Alloc(size_t size) +{ + void *result = malloc(size); + assert(result); + memset(result, 0, size); + return result; +} + +void *Memory_Realloc(void *memory, size_t size) +{ + void *result = realloc(memory, size); + assert(result); + return result; +} + +void Memory_Free(void *memory) +{ + if (memory) { + free(memory); + } +} + +void Memory_FreePointer(void *arg) +{ + assert(arg); + void *memory; + memcpy(&memory, arg, sizeof(void *)); + memcpy(arg, &(void *) { NULL }, sizeof(void *)); + Memory_Free(memory); +} + +char *Memory_DupStr(const char *string) +{ + assert(string); + char *memory = Memory_Alloc(strlen(string) + 1); + strcpy(memory, string); + return memory; +} diff --git a/src/memory.h b/src/memory.h new file mode 100644 index 00000000..1d4098d5 --- /dev/null +++ b/src/memory.h @@ -0,0 +1,37 @@ +#pragma once + +// Basic memory utilities that exit the game in case the system runs out of +// memory. + +#include + +// Allocate n bytes. In case the memory allocation fails, shows an error to the +// user and exits the application. The allocated memory is filled with zeros. +void *Memory_Alloc(size_t size); + +// Reallocate existing memory to n bytes, returning an address to the +// reallocated memory. In case the memory allocation fails, shows an error to +// the user and exits the application. All pointers to the old memory address +// become invalid. Preserves the previous memory contents. If the memory is +// NULL, the function acts like Memory_Alloc. +void *Memory_Realloc(void *memory, size_t size); + +// Frees the memory associated with a given address. If the memory is NULL, the +// function is a no-op. +void Memory_Free(void *memory); + +// Frees the memory associated with a given pointer and sets it to NULL. The +// user is expected to pass a pointer of their variable like so: +// +// char *mem = Memory_Alloc(10); +// Memory_FreePointer(&mem); +// (mem is now NULL) +// +// Giving a NULL to this function is a fatal error. Passing mem directly is +// also an error. +void Memory_FreePointer(void *memory); + +// Duplicates a string. In case the memory allocation fails, shows an error to +// the user and exits the application. The string must be NULL-terminated. +// Giving a NULL to this function is a fatal error. +char *Memory_DupStr(const char *string); diff --git a/src/specific/s_filesystem.c b/src/specific/s_filesystem.c new file mode 100644 index 00000000..c3272d7c --- /dev/null +++ b/src/specific/s_filesystem.c @@ -0,0 +1,149 @@ +#include "specific/s_filesystem.h" + +#include "log.h" +#include "memory.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) + #include + #include + #define PATH_SEPARATOR "\\" +#else + #include + #define PATH_SEPARATOR "/" +#endif + +const char *m_GameDir = NULL; + +static bool S_File_StringEndsWith(const char *str, const char *suffix); +static void S_File_PathAppendSeparator(char *path); +static void S_File_PathAppendPart(char *path, const char *part); + +static bool S_File_StringEndsWith(const char *str, const char *suffix) +{ + int str_len = strlen(str); + int suffix_len = strlen(suffix); + + if (suffix_len > str_len) { + return 0; + } + + return strcmp(str + str_len - suffix_len, suffix) == 0; +} + +static void S_File_PathAppendSeparator(char *path) +{ + if (!S_File_StringEndsWith(path, PATH_SEPARATOR)) { + strcat(path, PATH_SEPARATOR); + } +} + +static void S_File_PathAppendPart(char *path, const char *part) +{ + S_File_PathAppendSeparator(path); + strcat(path, part); +} + +const char *S_File_GetGameDirectory(void) +{ + if (!m_GameDir) { +#if defined(_WIN32) + char szFileName[MAX_PATH]; + GetModuleFileNameA(NULL, szFileName, MAX_PATH); + strrchr(szFileName, PATH_SEPARATOR[0])[1] = '\0'; + m_GameDir = strdup(szFileName); +#else + #error Not implemented +#endif + if (!m_GameDir) { + LOG_ERROR("Can't get module handle"); + return NULL; + } + } + return m_GameDir; +} + +void S_File_CreateDirectory(const char *path) +{ + assert(path); +#if defined(_WIN32) + _mkdir(path); +#else + mkdir(path, 0775); +#endif +} + +char *S_File_CasePath(char const *path) +{ + assert(path); + + char *current_path = Memory_Alloc(strlen(path) + 2); + + char *path_copy = Memory_DupStr(path); + char *path_piece = path_copy; + + if (path_copy[0] == '/') { + strcpy(current_path, "/"); + path_piece++; + } else if (strstr(path_copy, ":\\")) { + strcpy(current_path, path_copy); + strstr(current_path, ":\\")[1] = '\0'; + path_piece += 3; + } else { + strcpy(current_path, "."); + } + + while (path_piece) { + char *delim = strpbrk(path_piece, "/\\"); + char old_delim = delim ? *delim : '\0'; + if (delim) { + *delim = '\0'; + } + + DIR *path_dir = opendir(current_path); + if (!path_dir) { + Memory_FreePointer(&path_copy); + Memory_FreePointer(¤t_path); + return NULL; + } + + struct dirent *cur_file = readdir(path_dir); + while (cur_file) { + if (strcasecmp(path_piece, cur_file->d_name) == 0) { + S_File_PathAppendPart(current_path, cur_file->d_name); + break; + } + cur_file = readdir(path_dir); + } + closedir(path_dir); + + if (!cur_file) { + S_File_PathAppendPart(current_path, path_piece); + } + + if (delim) { + *delim = old_delim; + path_piece = delim + 1; + } else { + break; + } + } + + Memory_FreePointer(&path_copy); + + char *result; + if (current_path[0] == '.' + && strcmp(current_path + 1, PATH_SEPARATOR) + == 0) { /* strip leading ./ */ + result = Memory_DupStr(current_path + 1 + strlen(PATH_SEPARATOR)); + } else { + result = Memory_DupStr(current_path); + } + Memory_FreePointer(¤t_path); + return result; +} diff --git a/src/specific/s_filesystem.h b/src/specific/s_filesystem.h new file mode 100644 index 00000000..27a961bc --- /dev/null +++ b/src/specific/s_filesystem.h @@ -0,0 +1,5 @@ +#pragma once + +const char *S_File_GetGameDirectory(void); +void S_File_CreateDirectory(const char *path); +char *S_File_CasePath(char const *path); diff --git a/tools/generate_init b/tools/generate_init new file mode 100644 index 00000000..d4ba6563 --- /dev/null +++ b/tools/generate_init @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import argparse +import io +from pathlib import Path + +BODY = """ +const char *g_T2MVersion = "{version}"; +""" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--version-file", type=Path) + parser.add_argument("-o", "--output", type=Path) + return parser.parse_args() + + +def get_init_c(version: str) -> str: + with io.StringIO() as handle: + print(BODY.format(version=version).lstrip(), file=handle) + return handle.getvalue() + + +def update_init_c(output_path: Path, version: str) -> None: + new_text = get_init_c(version=version) + if not output_path.exists() or output_path.read_text() != new_text: + output_path.write_text(new_text) + + +def main() -> None: + args = parse_args() + if args.version_file and args.version_file.exists(): + version = args.version_file.read_text().strip() + else: + version = "" + update_init_c(output_path=args.output, version=version) + + +if __name__ == "__main__": + main() diff --git a/tools/generate_rcfile b/tools/generate_rcfile new file mode 100644 index 00000000..df847902 --- /dev/null +++ b/tools/generate_rcfile @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import argparse +from pathlib import Path + +REPO_DIR = Path(__file__).parent.parent +RESOURCES_DIR = REPO_DIR / "tools" / "resources" +SRC_DIR = REPO_DIR / "src" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--version-file", type=Path) + parser.add_argument('-o', "--output", type=Path, nargs="+") + return parser.parse_args() + + +def write_rc_template(input_path: Path, output_path: Path, version: str) -> None: + template = input_path.read_text() + template = template.replace("{version}", version) + template = template.replace("{icon_path}", str(RESOURCES_DIR / "icon.ico")) + output_path.write_text(template) + + +def main() -> None: + args = parse_args() + if args.version_file and args.version_file.exists(): + version = args.version_file.read_text().strip() + else: + version = "" + + for output_path in args.output: + write_rc_template( + input_path=RESOURCES_DIR / output_path.name, + output_path=output_path, + version=version, + ) + + +if __name__ == "__main__": + main() + diff --git a/tools/generate_version b/tools/generate_version new file mode 100644 index 00000000..b24a2012 --- /dev/null +++ b/tools/generate_version @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import argparse +from pathlib import Path +from subprocess import run + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("-o", "--output", type=Path) + return parser.parse_args() + + +def generate_version() -> str: + cmd = ["git", "describe", "--abbrev=7", "--tags"] + version = run(cmd, capture_output=True, text=True).stdout + return f'T2M {version or "?"}' + + +def main() -> None: + args = parse_args() + version = generate_version() + args.output.write_text(version) + + +if __name__ == "__main__": + main() diff --git a/tools/ida_import.py b/tools/ida_import.py new file mode 100644 index 00000000..7276c316 --- /dev/null +++ b/tools/ida_import.py @@ -0,0 +1,61 @@ +import ctypes +from pathlib import Path +from typing import Any, Dict, List, Optional + +import ida_typeinf +import idaapi +import idc + +REPO_DIR = Path(__file__).parent.parent +DOCS_DIR = REPO_DIR / "docs" +TYPES_FILE = DOCS_DIR / "ida_types.h" +VARIABLES_FILE = DOCS_DIR / "ida_variables.txt" + + +def to_int(source: str) -> Optional[int]: + source = source.strip() + if source.startswith("/*"): + source = source[2:] + if source.endswith("*/"): + source = source[:-2] + source = source.strip() + if not source.replace("-", ""): + return None + if source.startswith(("0x", "0X")): + source = source[2:] + return int(source, 16) + + +def import_types() -> None: + print(f"Importing type information from {TYPES_FILE}:") + error_count = idaapi.idc_parse_types(str(TYPES_FILE), idc.PT_FILE) + print(f" done ({error_count} errors)") + + +def import_variables() -> None: + print(f"Importing variables information from {VARIABLES_FILE}:") + with VARIABLES_FILE.open("r") as handle: + for line in handle: + line = line.strip() + if not line or line.startswith("#"): + continue + offset, decl = line.split(maxsplit=1) + offset = to_int(offset) + if True: + print(f" renaming 0x{offset:08x} to {decl}") + + til = idaapi.get_idati() + ti = idaapi.tinfo_t() + + name = idaapi.parse_decl(ti, til, decl, idaapi.PT_VAR) + if name.startswith("_"): + name = name[1:] + if not name.startswith('dword_'): + idaapi.set_name(offset, name) + idaapi.apply_tinfo(offset, ti, 0) + print(" done") + + +if __name__ == "__main__": + import_types() + import_variables() diff --git a/tools/resources/icon.ico b/tools/resources/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7793d7c08b9758bef4d5e66d942472a5998145da GIT binary patch literal 57471 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%0D%8*|W z_y)cM`XjlbhC#%Ek0n6j#qe=N&NdzcvJBD363%0o{et=d9;Q4TA3su-rC@j8-onuL z9f>pE|8P83Twv7)Z%b~3jUNN{Ct)VjvasYcy_md1G6qz2EMV(R%r^4?#>tD`Plo!E zUug%1*MYdt(gmIQ6&dlk_I@>?JC2VZF1~g|-TjNZhQ<;mKG|r>Abh}NjUBPs$DU#w z(vFzjqAJWPWk0%)kBbbu;c?5YpdRzz{uvX|#K$hc@UaN*3z}x~*rJV@SXpf;wxBQr zi%1H`yn~!Do2k)EIfL2^;p5oD@$s02<6AAP#q4s95iyX3%{t?KF67`lY+CtGj7vfr z!~GA(*Yy)2;sK6}*p!WzG4rIYn7r#O zB32CWGGaB}&+&F3`V)!`D$abEWvo0lGj#@mcm2gR!VmFt57v#@xLc3F$Fa$ihT^NZ z24J&}|3S=GjG`72d8(SlP+q1U`1pd?LBn?$fj?`fCx*9w+qXvqHr@xz8}?(@FA~FTf&1HN*mya&xEwLxF`2uI z@Uw)1c+6z7JErL{{g-@r|05%8{21K+zrq^z9v+XFF^Og>jnNGFFZ30r5T2pq^kkp5BPjo&3iF>ekT4HTfCiy>W||Etk{9M zEti1Xn5 zzluXc^<&NF@iQB2y#L_(BW$Wu?4d8}MtIYZTjq$0ZWOX@2Lxl9q* zVp!rBXN=}i5*UepJVxS^A2@jBF!DGwH56nT4;z&~K0(*mw(94DEbL+$U3Ln6SDPMiNr&m#1TDg=v^F=6*N^|5zF(r#2qr z;T6W*GFM^tQ)l+mDm%=?xMdB8@Z&r}s(P4N!tQ=L5(f#>(pSg!KW@f?lIIZRvZlV6 zq#=4f;}*n7!VG+>lpaPRi(?$LahS>MGR!s|eHhQwQ^`;oyZr1NcH!x1q#qU2^j$>U zBZ(+tBvDl&Z$G~X)0|p>srjU#YZB(NtR7SINGI+|s*l5pDmG(f)y0^7&~!}J93Ptj zwVx(BG>E)iQUT_&{4_?EP$#bO`#wdFF)34jOxh5zxyuu<@-w@zb5H9r1x*Gfr=$i( z5@+OnSMI~SRvy80g68$}(`3~!_cix1-`pUqvG)>IeWwa@4faF#@%Vw`t79Yy#yBjZ zVTcK-8KHJ3Gx-@b!rG3QvW*{R;%|+KXxQ}2CioFPhm;nhjD(&OA__1P51V-zHwhDQ z8jl&wT#fNb8}`fKkV5)PYxiHf=b&~Yjr3)zq9CR?#S*hg+K)--4$NCPWVJ97S|h__ zlDLs08rLc@QM3j?B99zfAFnuX~G%|>w^ zUsoI2pGZhQetCSpOzFSJA+3RtjomR4>Zc$g-+$nB;*irphc@%j!AOV}MCCCKS#(dH NaZN$I2 None: + iwyu_result = run( + ["include-what-you-use", "-I", "src", path], + capture_output=True, + text=True, + ).stderr + run( + [which("fix_include") and "fix_include" or "iwyu-fix-includes"], + input=iwyu_result, + text=True, + ) + + +def sort_imports(path: Path) -> None: + source = path.read_text() + rel_path = path.relative_to(SRC_PATH) + own_include = str(rel_path.with_suffix(".h")) + own_include = { + # files headers of which are not a 1:1 match with their filename + "game/game/game.c": "game/game.h", + "game/game/game_cutscene.c": "game/game.h", + "game/game/game_demo.c": "game/game.h", + "game/game/game_draw.c": "game/game.h", + "game/game/game_pause.c": "game/game.h", + "game/gun/gun.c": "game/gun.h", + "game/inventry.c": "game/inv.h", + "game/invfunc.c": "game/inv.h", + "game/invvars.c": "game/inv.h", + "game/lara/lara.c": "game/lara.h", + "game/option/option.c": "game/option.h", + "game/savegame/savegame.c": "game/savegame.h", + "specific/s_audio_sample.c": "specific/s_audio.h", + "specific/s_audio_stream.c": "specific/s_audio.h", + }.get(str(rel_path), own_include) + + def cb(match): + includes = re.findall(r'#include (["<][^"<>]+[">])', match.group(0)) + groups = { + "own": set(), + "proj": set(), + "lib": set(), + } + for include in includes: + if include.strip('"') == own_include: + groups["own"].add(include) + elif include.startswith("<"): + groups["lib"].add(include) + else: + groups["proj"].add(include) + + groups = {key: value for key, value in groups.items() if value} + + ret = "\n\n".join( + "\n".join(f"#include {include}" for include in sorted(group)) + for group in groups.values() + ).strip() + return ret + + source = re.sub( + "^#include [^\n]+(\n*#include [^\n]+)*", + cb, + source, + flags=re.M, + ) + if source != path.read_text(): + path.write_text(source) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument(metavar="path", type=Path, nargs="*", dest='paths') + return parser.parse_args() + + +def main() -> None: + args = parse_args() + paths = [path.absolute() for path in args.paths] + + if not paths: + paths = sorted( + path + for path in SRC_PATH.glob("**/*.[ch]") + if path != SRC_PATH / "init.c" + ) + + for path in paths: + fix_imports(path) + sort_imports(path) + + +if __name__ == "__main__": + main()