Compare commits

...
Author SHA1 Message Date
water 2e6bbcbce8 splitscreen hack 2023-11-28 20:19:08 -05:00
water111 72ee35ada2 [Loader] Unload less stuff per frame (#3227)
This should avoid the stuttering due to slow unloading on some drivers.

I also turned up the amount of stuff we load per frame since nobody has
been complaining about stutters there, but there has been a few cases of
levels loading in too slowly.

(this only changes graphics, not actual GOAL level load times).
2023-11-27 19:22:34 -05:00
OpenGOAL BotandOpenGOALBot 1400fef13e CI: Periodic Controller Database Update (#3226)
Updating Controller Database

Co-authored-by: OpenGOALBot <[email protected]>
2023-11-27 18:00:08 -05:00
Tyler Wilding e6f9909101 i18n: add back crowdin config change (i think things got into a bad state) 2023-11-22 19:13:54 -05:00
Tyler Wilding 3c74434211 Update Crowdin configuration file 2023-11-22 19:11:16 -05:00
OpenGOAL BotandOpenGOALBot 1bbf616e3b CI: Periodic Controller Database Update (#3215)
Co-authored-by: OpenGOALBot <[email protected]>
2023-11-22 18:45:43 -05:00
Tyler Wilding b43a5d45d7 New Crowdin updates (#3218) 2023-11-22 18:45:08 -05:00
Tyler Wilding 7909119c64 i18n: fix filepath for jak 2 subtitle translation file (#3217) 2023-11-22 18:04:38 -05:00
ManDude a7f2776782 fix dark jak punch sound and some samos sounds (#3214) 2023-11-19 11:35:07 +00:00
ManDude 7f1bf1f892 [jak2] extended shadows hack + add/fix progress options (#3213) 2023-11-19 10:57:56 +00:00
Tyler Wilding 8258c409d5 New Crowdin updates (#3211) 2023-11-18 14:52:49 -05:00
Radu13 74e90a84d5 Update subtitle_lines_it-IT.json (#3208) 2023-11-18 14:06:29 -05:00
014cbb04da i18n: transcribe missing english subtitles (#3199)
Co-authored-by: Hat Kid <[email protected]>
Co-authored-by: ManDude <[email protected]>
Co-authored-by: blahpy <[email protected]>
2023-11-18 14:06:01 -05:00
ZiemasandHat Kid cca829aecd j2: fix enemy-method-135 (#3206)
Co-authored-by: Hat Kid <[email protected]>
2023-11-15 22:29:51 +01:00
RoyalForgotten 6abfc9d129 Update subtitle_lines_fr-FR.json (#3201) 2023-11-13 20:01:09 -05:00
OpenGOAL BotandOpenGOALBot 22bb01205e CI: Periodic Controller Database Update (#3200)
Co-authored-by: OpenGOALBot <[email protected]>
2023-11-13 15:59:26 -05:00
ManDude 0b1639c8c0 fix forest youngsamos crash when he runs out of health (#3197)
Thought this may have been a regression but apparently we never patched
this one.

Fixes #3196
2023-11-12 18:35:58 +00:00
ManDude 42f995b059 reduce glow renderer vram usage + raise glow sprite limit (#3194)
Makes the glow sprite renderer flush when full capacity is reached,
instead of at the end. Also allows us to reduce the textures used for it
(finally). Worst case scenario there's 4-5 flushes per frame.

Fixes incessant flickering in the dig.
2023-11-12 00:11:50 +00:00
Tyler Wilding 2b1876f862 formatter: Add support for a few common functions and fix an LSP startup issue (#3190) 2023-11-11 17:51:55 -05:00
ManDude 288624b90f fix mistakes in projectiles.gc (#3187)
Fixes #3184
2023-11-11 11:40:29 +00:00
Tyler Wilding 55611169ee g/j2: Speedrunner mode improvements for Jak 2 (#3182) 2023-11-10 18:25:55 -05:00
ManDude a150e59e38 automatically re-enable autosave safely when starting a speedrun (#3181) 2023-11-09 17:48:47 +00:00
Hat Kid b2453fe23b jak2: add missing pc options to progress menu (#3167) 2023-11-09 17:24:50 +01:00
Hat Kid a80b4f1b97 jak2: some high fps fixes (#3177) 2023-11-08 23:34:58 +01:00
Hat Kid 4e3d35e97c jak2: fix discord rpc mission images in retail (#3176) 2023-11-07 00:39:58 +01:00
ManDude 299a25b997 [jak2] better handling of invalid formats (#3171)
see #3143
2023-11-06 22:05:51 +00:00
ManDude 8179d0aee3 [jak2] Localize board trick names (#3161)
It also looks like this now which I think is a bit cleaner:


![image](https://github.com/open-goal/jak-project/assets/7569514/0bed1d82-3407-4ca5-be1f-712ca8f96bcc)
2023-11-06 04:14:28 +00:00
Tyler Wilding c415d28270 cmake: use static OpenSSL libraries on linux (#3162) 2023-11-05 16:30:25 -05:00
Tyler Wilding f44f4dbe49 g/j2: use the large font for the jetboard trick combo display (#3157) 2023-11-05 01:32:01 -04:00
Tyler Wilding 595bc83105 g/j2: Add ending condition checks to the autosplitter struct (#3155) 2023-11-04 22:16:48 -04:00
water111 6067c25f67 [jak2] Pass 0x01 through format (#3143) 2023-11-04 20:02:28 -04:00
water111 2a7016be6a [jak2] Fix palace rotation at high fps (#3147) 2023-11-04 20:02:13 -04:00
ManDude 8d1fec34b4 allow per-file override of type-to-artgroup map + use correct divide by zero handler in civilian.gc (#3148) 2023-11-04 18:25:13 +00:00
Tyler Wilding de4b3d272d New Crowdin updates (#3146) 2023-11-04 13:58:38 -04:00
water111 ae4ee9c0c6 [jak2] Try to fix glow again (#3144)
Maybe fix https://github.com/open-goal/jak-project/issues/3065?
2023-11-04 13:30:22 -04:00
Tyler Wilding 959921e988 i18n: Add jak 2 custom text to Crowdin (#3141) 2023-11-04 13:14:14 -04:00
water111 85725401d2 [jak2] Hopefully improve sky performance (#3130)
Switches the slime look up table to be a texture, since I guess intel
drivers are terrible and putting the array in the shader makes it
extremely slow.

Also, a few minor changes:
- removed art-groups from the test-zone levels since this causes the
compiler to re-decompile the game, and makes the launcher slower. (left
it in commented out)
- Switched `decompile_code` to false by default in jak 2, in case people
run the decompiler and don't want to wait forever
- Fixed build warnings
2023-11-04 09:33:16 -04:00
ManDude adada4751d [jak2]do not force UK english on PAL (#3142) 2023-11-04 11:17:40 +00:00
BlqncandTyler Wilding d96f0524ea Jak 2 French Game Text (#3138)
Co-authored-by: Tyler Wilding <[email protected]>
2023-11-03 22:20:16 -04:00
Xavier Siguero Mora 43e0f11be5 Translated Jak 2 Game Texts to Spanish (#3139) 2023-11-03 22:02:29 -04:00
ZedB0T c1e81e1b4b Add fallback text functionality to Jak 2 (#3128) 2023-11-03 21:45:10 -04:00
Tyler Wilding cca2897e3e g/jak2: disable speech volume when starting a new speedrun (#3132) 2023-11-02 23:35:52 -04:00
Tyler Wilding fff3a85531 g/jak2: allow L1 + R1 + Start to open speedrun menu as well (#3131) 2023-11-02 20:42:51 -04:00
Tyler Wilding a0e998afb3 ci: properly statically link on macOS (#3127) 2023-11-02 19:57:34 -04:00
Tyler Wilding 385b8e6e6a docs: Re-order root README a bit and add new link to new tutorial (#3119) 2023-11-02 13:19:11 -04:00
Tyler Wilding 4fbc9258a1 ci/windows: stop Strawberry perl from changing resulting built artifacts in CI (#3114) 2023-11-02 02:29:19 -04:00
Tyler Wilding ae35b546e4 New Crowdin updates (#3113) 2023-11-02 01:03:15 -04:00
ManDude a949dada93 rename joint node and art element macros (#3111) 2023-11-01 23:16:00 +00:00
ManDude 5587593204 fps selection fixes i missed (#3109) 2023-10-30 19:55:16 +00:00
ManDude 99cb51ff57 [jak2] simplify fps disclaimer (#3108) 2023-10-30 18:17:11 +00:00
ManDudeandHat Kid cd68cb671e deftype and defmethod syntax major changes (#3094)
Major change to how `deftype` shows up in our code:
- the decompiler will no longer emit the `offset-assert`,
`method-count-assert`, `size-assert` and `flag-assert` parameters. There
are extremely few cases where having this in the decompiled code is
helpful, as the types there come from `all-types` which already has
those parameters. This also doesn't break type consistency because:
  - the asserts aren't compared.
- the first step of the test uses `all-types`, which has the asserts,
which will throw an error if they're bad.
- the decompiler won't emit the `heap-base` parameter unless necessary
now.
- the decompiler will try its hardest to turn a fixed-offset field into
an `overlay-at` field. It falls back to the old offset if all else
fails.
- `overlay-at` now supports field "dereferencing" to specify the offset
that's within a field that's a structure, e.g.:
```lisp
(deftype foobar (structure)
  ((vec    vector  :inline)
   (flags  int32   :overlay-at (-> vec w))
   )
  )
```
in this structure, the offset of `flags` will be 12 because that is the
final offset of `vec`'s `w` field within this structure.
- **removed ID from all method declarations.** IDs are only ever
automatically assigned now. Fixes #3068.
- added an `:overlay` parameter to method declarations, in order to
declare a new method that goes on top of a previously-defined method.
Syntax is `:overlay <method-name>`. Please do not ever use this.
- added `state-methods` list parameter. This lets you quickly specify a
list of states to be put in the method table. Same syntax as the
`states` list parameter. The decompiler will try to put as many states
in this as it can without messing with the method ID order.

Also changes `defmethod` to make the first type definition (before the
arguments) optional. The type can now be inferred from the first
argument. Fixes #3093.

---------

Co-authored-by: Hat Kid <[email protected]>
2023-10-30 03:20:02 +00:00
ManDude 09536c68ac [compiler] asm-only disasm output + fix spacing bug (#3104) 2023-10-29 10:16:14 +00:00
Matt Dallmeyer 7c74b0c999 Fix starting continue point for Hub 2/3 category extensions (#3103) 2023-10-29 02:17:32 +01:00
OpenGOAL BotandOpenGOALBot 2ce7b36568 CI: Periodic Controller Database Update (#3101)
Co-authored-by: OpenGOALBot <[email protected]>
2023-10-24 18:43:00 -04:00
Hat Kidandwater c7c615a043 custom levels: refactor level building code and jak 2 support (#3090)
Co-authored-by: water <[email protected]>
2023-10-21 06:07:43 +02:00
Tyler Wilding dccc3da1b3 formatter: rewrite and refactor, address more edge-cases, begin documenting my work (#3096) 2023-10-20 21:24:31 -04:00
Tyler Wilding 94603bce49 ci: Workaround CMake/Perl regression in recent windows-2022 images (#3097) 2023-10-20 21:06:12 -04:00
jabermony cdf13a92ec Add label_types file merge for PAL version (#3086) 2023-10-20 01:40:24 -04:00
OpenGOAL BotandOpenGOALBot fcebf977f8 CI: Periodic Controller Database Update (#3091)
Co-authored-by: OpenGOALBot <[email protected]>
2023-10-20 01:29:29 -04:00
Hat Kid 2c825ab7bf jak2: add warning text when changing frame rate for the first time (#3092) 2023-10-17 18:29:02 +02:00
jabermony 95dca764fc Fix build failure at llvm-rc on windows due to codepage error on zydis (#3088)
This fix is for the following error when building with llvm on windows

```
[build] llvm-rc: Error in VERSIONINFO statement (ID 1): 
[proc] The command: "C:\Program Files\CMake\bin\cmake.exe" --build C:/.../git/jak-project-test/out/build/Release --parallel 8 exited with code: 1
[build] Non-ASCII 8-bit codepoint (´┐¢) can't be interpreted in the current codepage
[build] ninja: build stopped: subcommand failed.
```

It's caused by llvm-rc not handling non 8-bit characters.
The same issue is discussed in the curl project here
https://github.com/curl/curl/issues/7765

This commit copies the fix from curl, replacing '©' with '(C)'.
2023-10-15 10:38:14 -04:00
Hat Kid e6b73f1058 custom levels: support for packing textures (#3089) 2023-10-15 16:36:14 +02:00
water111 ec23e6c5d9 [glb export] Export bones. (#3087)
![image](https://github.com/open-goal/jak-project/assets/48171810/0f26e77b-af68-4450-882e-762a501bdef4)
2023-10-14 16:49:23 -04:00
ManDude ae45037489 fix *jak1-full-game* being flipped (#3085) 2023-10-13 03:41:55 +01:00
Tyler Wilding 60eaac8051 decompiler: add texture merging feature (#3083) 2023-10-12 19:02:36 -04:00
Hat Kid 598ba1aaa3 jak3: add missing files and implement pexcw (#3084)
Some files were in the `banned_objects` list and were thus excluded from
the `all_objs` file.

Also implements the `pexcw` instruction which is only used in `hfrag`
code.
2023-10-12 18:07:37 -04:00
AloqasandTyler Wilding ce1c2eb7c2 [jak1] Finnish base game strings (#3058)
Finnish translations for the remaining UI texts

---------

Co-authored-by: Tyler Wilding <[email protected]>
2023-10-12 18:05:40 -04:00
Tyler Wilding 8b3b96761d g/j2: Integrate highscores with Speedrun.com/JakSpeedruns.com when speedrunner mode is enabled (#3037) 2023-10-11 20:43:55 -04:00
water111 97e04a7612 [jak2] Fix collision renderer extract (#3081)
Fixes missing collision geometry reported in
https://github.com/open-goal/jak-project/issues/3011

The issue happens when there are 256 polygons. In this case `num-polys`
is 0 (it's a u8). There are actual cases where there are 0 polygons, so
we have to do a more complicated check to get the real count. I should
have done this in the first place, but it seemed to work...
2023-10-11 19:32:25 -04:00
Hat Kid 5be46c9852 decompiler: allow jak 3 texture and model extraction (#3080)
Added some hacks and stubs to allow extracting textures and models.
2023-10-11 19:32:12 -04:00
Hat Kid 6285c61662 decompiler: fix for v5 art group info dump, update taskfile for jak 3 (#3077) 2023-10-11 10:17:46 +02:00
water111 cfce5e5916 [decompiler] Support v5 data file link data (#3076)
Fix the implementation of `link_v5` so it works on "data" files for jak
3.
2023-10-10 21:56:03 -04:00
Hat Kid 5a6aab4fab jak3: add goal_src skeleton (#3069) 2023-10-10 15:41:05 +02:00
Tyler Wilding 2adf440e3e github: add issue template for jak 2 (#3073) 2023-10-09 19:41:41 -04:00
ManDude 27980218aa [jak2] fix minor original game visual bugs (#3072)
Fixes #2994 the underport and consite icons being green (same color as
the goal, likely copy-pasta).

Fixes the hundredths timer in the race timer actually being a 60ths
timer.
2023-10-09 22:39:35 +01:00
ManDude 884f0ef6e3 [glow] potentially fix bad glow texture (#3071)
Might fix #3065 

fixes #3070
2023-10-09 14:16:13 +01:00
Hat Kidandwater bf961a36f4 decompiler: some hacks to allow running decompiler on jak 3 v5 code files, improve all-types generation (#2526)
Co-authored-by: water <[email protected]>
2023-10-07 22:14:12 +02:00
water111 5f8415320b [jak2] new glow fix (#3067)
Fix a typo in the shader. (fixes
https://github.com/open-goal/jak-project/issues/3045, and fixes fadeout
when glows move offscreen on the bottom).

Also, try a different format for the depth blit, in an attempt to fix
this issue https://github.com/open-goal/jak-project/issues/3065.
2023-10-07 12:37:06 -04:00
water111 395c98db19 [goalc] Cleaned up speedups (#3066)
Started at 349,880,038 allocations and 42s

- Switched to making `Symbol` in GOOS be a "fixed type", just a wrapper
around a `const char*` pointing to the string in the symbol table. This
is a step toward making a lot of things better, but by itself not a huge
improvement. Some things may be worse due to more temp `std::string`
allocations, but one day all these can be removed. On linux it saved
allocations (347,685,429), and saved a second or two (41 s).
- cache `#t` and `#f` in interpreter, better lookup for special
forms/builtins (hashtable of pointers instead of strings, vector for the
small special form list). Dropped time to 38s.
- special-case in quasiquote when splicing is the last thing in a list.
Allocation dropped to 340,603,082
- custom hash table for environment lookups (lexical vars). Dropped to
36s and 314,637,194
- less allocation in `read_list` 311,613,616. Time about the same.
- `let` and `let*` in Interpreter.cpp 191,988,083, time down to 28s.
2023-10-07 10:48:17 -04:00
water111andManDude af6f489657 [jak2] Set up extractor (#3042)
This sets up the extractor for jak 2. I was expecting that I'd have to
make some more significant changes to the decompiler/compiler path
stuff, but this was not the case!

The only real change is that you can now provide multiple ISO hashes for
an entry in `ISOMetadata`. This is needed for the two different NTSC
versions, which have the same configs, serials, and ELF hashes, but
slightly different contents.

I also didn't add the korean version because I don't have the info for
it.

---------

Co-authored-by: ManDude <[email protected]>
2023-10-06 23:09:09 -04:00
Hat Kid 3c27b3942b decompiler: better automatic detection of art groups and joint-node-index macro detection (#3061) 2023-10-06 16:42:16 +02:00
OpenGOAL BotandOpenGOALBot 2d480a6df6 CI: Periodic Controller Database Update (#3060)
Co-authored-by: OpenGOALBot <[email protected]>
2023-10-03 13:55:31 -04:00
ManDude 1338fed9c9 fix eco pickup time bug (#3056) 2023-10-02 01:22:05 +01:00
Tyler Wilding 42e3b7f89c g/j2: don't force auto-save on in jak 2 when starting a speedrun (#3048) 2023-09-30 23:40:00 -04:00
dependabot[bot] 4731ae6e22 Bump actions/checkout from 3 to 4 (#3047)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-30 23:36:22 -04:00
ManDude dfeb88b35d [jak2] fully implement *user* (#3046)
Fixes #1918
2023-10-01 04:28:30 +01:00
Matt Dallmeyer d149838485 fix draw-string-xy-scaled cropping (#3041)
fixes #3038 


https://github.com/open-goal/jak-project/assets/2515356/5b7a490f-e1fb-4a12-a3d9-b576a2c1d574
2023-10-01 00:17:50 +01:00
ManDude e130244271 [jak2] scalable debug font + fix debug menu widescreen bugs (#3044)
also hides empty (no commands) regions from the viewers by default now.
2023-09-30 21:19:47 +01:00
water111 0e31a9c407 [decompiler] Handle find-parent-method (#3018)
This change adds a few new features:
- Decompiler automatically knows the type of `find-parent-method` use in
jak 1 and jak2 when used in a method or virtual state handler.
- Decompiler inserts a call to `call-parent-method` or
`find-parent-state`
- Removed most casts related to these functions

There are still a few minor issues around this:
- There are still some casts needed when using `post` methods, as `post`
is just a `function`, and needs a cast to `(function none)` or similar.
It didn't seem easy to change the type of `post`, so I'm not going to
worry about it for this PR. It only shows up in like 3 places in jak 2.
(and 0 in jak 1)
- If "call the handler if it's not #f" logic should probably be another
macro.

Fixes #805
2023-09-30 11:06:09 -04:00
7303 changed files with 748497 additions and 127073 deletions

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ body:
label: Acknowledgements
description: Take a couple minutes to help our maintainers work faster.
options:
- label: I have [searched](https://github.com/open-goal/jak-project/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Aupdated-desc) for duplicate or closed bug reports
- label: I have [searched](https://github.com/open-goal/jak-project/issues?q=is%3Aissue+is%3Aopen+label%3Abug+label%3Ajak1+sort%3Aupdated-desc+) for duplicate or closed bug reports
required: true
- label: I understand that I am supposed to provide my own legitimately obtained copy of the game
required: true
@@ -0,0 +1,74 @@
name: "\U0001F41B Jak 2 Bug Report"
description: Create a bug report for Jak 2.
labels: ["bug", "jak2"]
body:
- type: checkboxes
attributes:
label: Acknowledgements
description: Take a couple minutes to help our maintainers work faster.
options:
- label: I have [searched](https://github.com/open-goal/jak-project/issues?q=is%3Aissue+is%3Aopen+label%3Abug+label%3Ajak2+sort%3Aupdated-desc+) for duplicate or closed bug reports
required: true
- label: I understand that I am supposed to provide my own legitimately obtained copy of the game
required: true
- type: textarea
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is. You may post screenshots or videos of the bug here.
validations:
required: true
- type: textarea
attributes:
label: How To Reproduce
description: Steps to reproduce the behavior. You can also post a video of it here.
validations:
required: true
- type: dropdown
attributes:
label: Does this problem occur on original hardware or PCSX2?
description: Some things that may seem like bugs are actually exactly how the original game behaved.
options:
- Yes, it's unique to OpenGOAL
- Didn't check
- Not needed, bug is obvious
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A clear and concise description of the expected behavior.
placeholder: When I do X, Y should happen.
validations:
required: true
- type: textarea
attributes:
label: Environment Information
description: "You can upload the [Support Package](https://github.com/open-goal/launcher#asking-for-help) provided by the Launcher here, or you can provide the following information: CPU, GPU, OS Version, OpenGOAL Version (found in the window's title bar)"
validations:
required: true
- type: dropdown
attributes:
label: Game Version
options:
- NTSC 1.0 (black label)
- NTSC Greatest Hits version (red label)
- PAL
- JP
validations:
required: true
- type: dropdown
attributes:
label: Have you set the game to something other than `60fps`?
options:
- "No"
- "Yes"
validations:
required: true
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
BUILDCACHE_LOG_FILE: ${{ github.workspace }}/buildcache.log
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Package Dependencies
run: |
+3 -3
View File
@@ -16,7 +16,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
# sudo add-apt-repository 'deb http://apt.llvm.org/focal/ llvm-toolchain-focal-15 main'
@@ -39,7 +39,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Check for Unresolved Conflicts
run: python ./scripts/gsrc/check-for-conflicts.py
@@ -56,7 +56,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Python Dependencies
run: pip install unidiff colorama
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Package Dependencies
run: |
@@ -37,7 +37,7 @@ jobs:
sudo apt install build-essential cmake \
clang gcc g++ lcov make nasm libxrandr-dev \
libxinerama-dev libxcursor-dev libpulse-dev \
libxi-dev zip ninja-build libgl1-mesa-dev
libxi-dev zip ninja-build libgl1-mesa-dev libssl-dev
- name: Setup Buildcache
uses: mikehardy/[email protected]
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Package Dependencies
run: |
@@ -33,7 +33,7 @@ jobs:
sudo apt install build-essential cmake \
clang gcc g++ lcov make nasm libxrandr-dev \
libxinerama-dev libxcursor-dev libpulse-dev \
libxi-dev zip ninja-build libgl1-mesa-dev
libxi-dev zip ninja-build libgl1-mesa-dev libssl-dev
- name: Setup Buildcache
uses: mikehardy/[email protected]
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up ARM64 environment
run: sudo softwareupdate --install-rosetta --agree-to-license
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Package Dependencies
run: brew install cmake nasm ninja
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
name: "Upload Artifacts"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Prepare Artifact Folder
run: mkdir -p ./ci-artifacts/final
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
if: github.repository == 'open-goal/jak-project'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
token: ${{ secrets.BOT_PAT }}
+8 -1
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install NASM
# TODO - Simplify this with just the first command once choco 2.0 rolls out everywhere
@@ -49,6 +49,13 @@ jobs:
- uses: ilammy/msvc-dev-cmd@v1
# https://github.com/actions/runner-images/issues/5459
# Causes binaries to be built differently, get rid of this garbage
- name: Remove Strawberry Perl from PATH
run: |
$env:PATH = $env:PATH -replace "C:\\Strawberry\\c\\bin;", ""
"PATH=$env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: CMake Generation
shell: cmd
run: cmake -B build --preset=${{ inputs.cmakePreset }} -DCMAKE_C_COMPILER_LAUNCHER=${{ github.workspace }}/buildcache/bin/buildcache.exe -DCMAKE_CXX_COMPILER_LAUNCHER=${{ github.workspace }}/buildcache/bin/buildcache.exe .
+7 -1
View File
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install NASM
# TODO - Simplify this with just the first command once choco 2.0 rolls out everywhere
@@ -44,6 +44,12 @@ jobs:
- uses: ilammy/msvc-dev-cmd@v1
# Fixes an issue with the image causing builds to fail - https://github.com/actions/runner-images/issues/8598
- name: Remove Strawberry Perl from PATH
run: |
$env:PATH = $env:PATH -replace "C:\\Strawberry\\c\\bin;", ""
"PATH=$env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: CMake Generation - MSVC
shell: cmd
run: cmake -B build --preset=${{ inputs.cmakePreset }} -DCMAKE_C_COMPILER_LAUNCHER=${{ github.workspace }}/buildcache/bin/buildcache.exe -DCMAKE_CXX_COMPILER_LAUNCHER=${{ github.workspace }}/buildcache/bin/buildcache.exe .
+2 -2
View File
@@ -267,8 +267,8 @@
"type" : "default",
"project" : "CMakeLists.txt",
"projectTarget" : "formatter.exe (bin\\formatter.exe)",
"name" : "Tools - Formatter",
"args" : ["--new", "--file", "C:\\Users\\xtvas\\Repos\\opengoal\\jak-project\\test-formatter.gc"]
"name" : "Tools - Formatter - Inplace",
"args" : ["--write", "--file", "C:\\Users\\xtvas\\Repos\\opengoal\\jak-project\\goal_src\\jak2\\pc\\util\\popup-menu-h.gc"]
},
{
"type": "default",
+94 -1
View File
@@ -8,5 +8,98 @@
"editor.wordBasedSuggestions": true,
"editor.snippetSuggestions": "top"
},
"python.formatting.provider": "black"
"python.formatting.provider": "black",
"files.associations": {
"optional": "cpp",
"algorithm": "cpp",
"any": "cpp",
"array": "cpp",
"atomic": "cpp",
"bit": "cpp",
"bitset": "cpp",
"cctype": "cpp",
"charconv": "cpp",
"chrono": "cpp",
"cinttypes": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"codecvt": "cpp",
"compare": "cpp",
"concepts": "cpp",
"condition_variable": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"exception": "cpp",
"filesystem": "cpp",
"format": "cpp",
"forward_list": "cpp",
"fstream": "cpp",
"functional": "cpp",
"future": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"ios": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"iterator": "cpp",
"limits": "cpp",
"list": "cpp",
"locale": "cpp",
"map": "cpp",
"memory": "cpp",
"mutex": "cpp",
"new": "cpp",
"numeric": "cpp",
"ostream": "cpp",
"queue": "cpp",
"random": "cpp",
"ranges": "cpp",
"ratio": "cpp",
"regex": "cpp",
"set": "cpp",
"shared_mutex": "cpp",
"span": "cpp",
"sstream": "cpp",
"stack": "cpp",
"stdexcept": "cpp",
"stop_token": "cpp",
"streambuf": "cpp",
"string": "cpp",
"system_error": "cpp",
"thread": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"typeinfo": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"utility": "cpp",
"valarray": "cpp",
"variant": "cpp",
"vector": "cpp",
"xfacet": "cpp",
"xhash": "cpp",
"xiosbase": "cpp",
"xlocale": "cpp",
"xlocbuf": "cpp",
"xlocinfo": "cpp",
"xlocmes": "cpp",
"xlocmon": "cpp",
"xlocnum": "cpp",
"xloctime": "cpp",
"xmemory": "cpp",
"xstddef": "cpp",
"xstring": "cpp",
"xtr1common": "cpp",
"xtree": "cpp",
"xutility": "cpp"
}
}
+25 -1
View File
@@ -18,14 +18,16 @@ option(STATICALLY_LINK "Build for release purposes (statically link everything)"
message(STATUS "Statically Link? ${STATICALLY_LINK}")
if(STATICALLY_LINK)
set(BUILD_SHARED_LIBS OFF)
set(BUILD_STATIC_LIBS ON)
else()
set(BUILD_SHARED_LIBS ON)
set(BUILD_STATIC_LIBS OFF)
endif()
# For clangd
if (EXISTS "${CMAKE_CURRENT_BINARY_DIR}/compile_commands.json" )
configure_file(
"${CMAKE_CURRENT_BINARY_DIR}/compile_commands.json"
"${CMAKE_CURRENT_BINARY_DIR}/compile_commands.json"
"${PROJECT_SOURCE_DIR}/build/compile_commands.json")
endif()
@@ -183,6 +185,28 @@ include_directories(./)
# build templating engine library
include_directories(SYSTEM third-party/inja)
# libcurl for HTTP requests
# Enable SSL Support, most URLs now-a-days use SSL!
# TODO - probably integrate with ZSTD since we have it already
set(CURL_USE_LIBSSH2 OFF)
set(CURL_ZLIB OFF)
if(WIN32)
set(CURL_USE_SCHANNEL ON) # native Windows SSL support
elseif(APPLE)
set(CURL_USE_SECTRANSP ON) # native macOS SSL support
else()
if(STATICALLY_LINK)
set(OPENSSL_USE_STATIC_LIBS TRUE)
endif()
set(CURL_USE_OPENSSL ON) # not native, but seems to be the best choice for linux
endif()
include_directories(third-party/curl/include)
if(STATICALLY_LINK)
build_third_party_lib(curl libcurl_static)
else()
build_third_party_lib(curl libcurl_shared)
endif()
# build repl library
build_third_party_lib(replxx replxx)
+15 -21
View File
@@ -6,14 +6,24 @@
<a href="https://opengoal.dev/docs/intro" rel="nofollow"><img src="https://img.shields.io/badge/Documentation-Here-informational" alt="Documentation Badge" style="max-width:100%;"></a>
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/opengoal"><img src="https://badges.crowdin.net/opengoal/localized.svg"></a>
<a target="_blank" rel="noopener noreferrer" href="https://github.com/open-goal/jak-project/actions/workflows/build-matrix.yaml"><img src="https://github.com/open-goal/jak-project/actions/workflows/build-matrix.yaml/badge.svg" alt="Linux and Windows Build" style="max-width:100%;"></a>
<a href="https://www.codacy.com/gh/open-goal/jak-project/dashboard?utm_source=github.com&utm_medium=referral&utm_content=open-goal/jak-project&utm_campaign=Badge_Coverage" rel="nofollow"><img src="https://app.codacy.com/project/badge/Coverage/29316d04a1644aa390c33be07289f3f5" alt="Codacy Badge" style="max-width:100%;"></a>
<a href="https://www.codacy.com/gh/open-goal/jak-project/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=open-goal/jak-project&amp;utm_campaign=Badge_Grade" rel="nofollow"><img src="https://app.codacy.com/project/badge/Grade/29316d04a1644aa390c33be07289f3f5" alt="Codacy Badge" style="max-width:100%;"></a>
<a href="https://discord.gg/VZbXMHXzWv"><img src="https://img.shields.io/discord/756287461377703987" alt="Discord"></a>
<a href="https://makeapullrequest.com"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square" alt=PRs Welcome></a>
</p>
- [Please read first](#please-read-first)
- [Quick Start](#quick-start)
## Please read first <!-- omit from toc -->
> [!IMPORTANT]
> Our repositories on GitHub are primarily for development of the project and tracking active issues. Most of the information you will find here pertains to setting up the project for development purposes and is not relevant to the end-user.
For a setup guide on how to install and play the game there is the following video that you can check out: https://youtu.be/K84UUMnkJc4
For questions or additional information pertaining to the project, we have a Discord for discussion here: https://discord.gg/VZbXMHXzWv
Additionally, you can find further documentation and answers to **frequently asked questions** on the project's main website: https://opengoal.dev
> [!WARNING]
> **Do not use this decompilation project without providing your own legally purchased copy of the game.** We do not distribute any assets from the game - you must use your own legitimately obtained PS2 copy of the game. We support every retail PAL, NTSC, and NTSC-J build, including Greatest Hits copies.
- [Project Description](#project-description)
- [Current Status](#current-status)
- [Methodology](#methodology)
@@ -46,22 +56,6 @@
- [`goal_src/`](#goal_src)
- [`game` runtime](#game-runtime)
## Please read first
Our repositories on GitHub are primarily for development of the project and tracking active issues. Most of the information you will find here pertains to setting up the project for development purposes and is not relevant to the end-user.
For questions or additional information pertaining to the project, we have a Discord for discussion here: https://discord.gg/VZbXMHXzWv
Additionally, you can find further documentation and answers to **frequently asked questions** on the project's main website: https://opengoal.dev
**Do not use this decompilation project without providing your own legally purchased copy of the game.**
### Quick Start
If you just want to play the game, you can follow the steps in this video: https://www.youtube.com/watch?v=p8I9NfuZOgE
We do not distribute any assets from the game - you must use your own legitimately obtained PS2 copy of the game. We support every retail PAL, NTSC, and NTSC-J build, including Greatest Hits copies.
## Project Description
This project is to port the original Jak and Daxter and Jak II to PC. Over 98% of the games are written in GOAL, a custom Lisp language developed by Naughty Dog. Our strategy is:
@@ -133,7 +127,7 @@ Unfortunately you'll still need task runner on your local machine to run the gam
Install packages and init repository:
```sh
sudo apt install gcc make cmake build-essential g++ nasm clang-format libxrandr-dev libxinerama-dev libxcursor-dev libpulse-dev libxi-dev python libgl1-mesa-dev
sudo apt install gcc make cmake build-essential g++ nasm clang-format libxrandr-dev libxinerama-dev libxcursor-dev libpulse-dev libxi-dev python libgl1-mesa-dev libssl-dev
sudo sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
```
+2
View File
@@ -15,6 +15,8 @@ tasks:
- 'python ./scripts/tasks/update-env.py --game jak1'
set-game-jak2:
- 'python ./scripts/tasks/update-env.py --game jak2'
set-game-jak3:
- 'python ./scripts/tasks/update-env.py --game jak3'
set-decomp-ntscv1:
desc: "aka black label"
cmds:
+3 -2
View File
@@ -43,7 +43,7 @@ void write_wave_file(const std::vector<s16>& left_samples,
writer.add(sample);
}
} else {
for (int i = 0; i < left_samples.size(); i++) {
for (size_t i = 0; i < left_samples.size(); i++) {
writer.add(left_samples.at(i));
if (i < right_samples.size()) {
writer.add(right_samples.at(i));
@@ -86,8 +86,9 @@ std::pair<std::vector<s16>, std::vector<s16>> decode_adpcm(BinaryReader& reader,
u8 shift = shift_filter & 0b1111;
u8 filter = shift_filter >> 4;
u8 flags = reader.read<u8>();
(void)flags;
// removed assertions here
// removed assertions here (and that's probably why the audio doesn't sound right)
u8 input_buffer[14];
+186
View File
@@ -0,0 +1,186 @@
# OpenGOAL Formatter Documentation
> [!IMPORTANT]
> This is still in a highly experimental state, adjust your expectations accordingly.
Despite LISP being very simple, and taking the stance of a highly opinionated formatter that doesn't concern itself with line-length in most situations writing a code formatter is still an incredibly annoying and difficult thing to do. This documentation serves as a place for me to collect my thoughts and explain how it works (or more likely, how it does not).
Also in general I found it _really_ hard to find modern and easy to understand articles on how to get started (with the exception of zprint's usage docs, but that doesn't really cover implementation ideas). This area is either dominated by 40 year old journal articles, or discussions around really difficult formatting projects. There isn't a whole lot in the vain of _here's how I made a decent, simple formatter_, so maybe it'll help someone else (even if it's teaching them potentially what not to do!)
## Architecture / Overview
Atleast for me, it helps to understand the path code takes as it flows through the formatting process. Originally I tried to do as much at once for sake of efficiency but this just makes things incredibly hard to reason about and fix bugs. So like any problem that is complex, break it down, the formatter goes through many distinct phases chipping away at the problem:
```mermaid
%%{init: {'theme': 'dark', "flowchart" : { "curve" : "basis" } } }%%
flowchart TB
subgraph top1 [Build a Formatting Tree]
direction TB
a1(AST Parse) --> a2(Consolidate AST Nodes)
a1 --> a3(Collect Metadata)
end
subgraph top2 [Apply Formatting Configuration]
direction TB
b1(Recursively Iterate each Node) --> b2{Form head has\na predefined config?}
b2 --> |yes|b3(Fetch it and set the config on the node)
b2 --> |no|b4(Apply a default configuration)
end
subgraph top3 [Apply Formatting]
direction TB
c1(For each node, recursively build up a\nlist of source code lines, no indentation) --> c2(Consolidate the first two lines if hanging\nor if the config demands it)
c2 --> c3(Append indentation to each line as appropriate\nand surround each form with relevant parens)
end
subgraph top4 [Finalize Lines]
direction TB
d1(Concatenate all lines with whatever\nfile ending the file started with) --> d2(If we're formatting a file append a trailing new-line\n otherwise we are done)
end
top1-->|FormatterTree|top2
top2-->|FormatterTree with Configurations|top3
top3-->|String List|top4
```
### Build a Formatting Tree
At the end of the day, formatting is about changing the location and amounts of white-space in a file. Meaning it would be ideal if we could start with a representation that is whitespace-agnostic. Some formatters (like cljfmt) choose to use the original whitespace as an input into the formatting process, essentially meaning that your code should already be somewhat-formatted. This is somewhat advantageous as it can simplify the formatter's implementation or be a bit more flexible as to what the original programmer intended (assuming they knew what they were doing). But of course it has one fatal flaw -- the code has to already be well formatted.
For our formatter I wanted it to be able to take in horribly, ugly, minified code and output an optimal result -- especially since so much of our code gets spat out by another tool. To do that, we need to be able to format the code without any existing whitespace helping us, adding in the whitespace from the ground up.
The easiest way to do this is to construct an AST of the language which has a lot of other benefits for building language features but for here all we care about is getting rid of the whitespace and having some basic interpretation of the source code structure. The easiest way now-a-days to make an AST for a language is to create a [treesitter grammar](https://github.com/open-goal/tree-sitter-opengoal).
#### Consolidate AST Nodes and Collect Metadata
One downside with using an AST, atleast with the way the grammar is currently implemented is it creates a somewhat difficult tree to work with from a formatting perspective. For example in a LISP, everything is a list. In our AST it does not look like how you may intuitively expect:
`(+ 1 2)`
```mermaid
flowchart TB
a("source\n(+ 1 2)") --> b("list_lit\n")
b --> c("(") --> ca("(")
b --> d("sym_lit\n+") --> da("+")
b --> e("sym_lit\n1") --> ea("1")
b --> f("sym_lit\n2") --> fa("2")
b --> g(")") --> ga(")")
```
vs what you would probably prefer:
```mermaid
flowchart TB
a("top-level") --> b("list\n- surrounded in ()")
b --> da("+")
b --> ea("1")
b --> fa("2")
```
As you can see from even this simple example, the AST representation can be very verbose and in a sense has duplication. Each node is aware of it's original location in the parsed source-code, but it's the leaf-node tokens that we care about for formatting and perhaps the form types they were a part of (ie. were they comments or in a list, etc).
So to keep things brief, the AST representation is a bit too cumbersome and superfluous to use for formatting directly. So the first step is recurse through it and optimize it, removing unneeded height and capturing crucial metadata that is actually relevant for formatting.
We build a `FormatterTree` which consists of `FormatterNode`s. Each `FormatterNode` has either a single token, or a list of `refs`. A `ref` is just another `FormatterNode`. Each node also has a bunch of metadata and configuration on how it should be formatted (just initialized for now).
Having a dead-simple representation of the code like this is really only possible because it's a LISP, but it's also future proofed for us. If OpenGOAL ever decides to support different brackets -- for example in Clojure a vector uses `[]` or a map uses `{}` it would be easy for update the formatter to wrap each list node in the right bracket type.
### Applying Formatting Configuration
Now that we have our simplified tree, we can get to actually formatting. The first step is to figure out _how_ we intend to format things. By separating this from the actual formatting it's a lot easier to reason about and find bugs. You can also do potentially interesting things, where you do a full traversal of your source-code tree to figure out an optimal decision without trying to build up the final formatted string at the same time.
It is lists, or `form`s that we care about figuring out how to format. Each `form` has a head element (unless it's empty) that we can use to specify how it would be formatted. For example we cannot specify the formatting for every single function name, but we can format a `defun` or a `let` in a special way. With this mechanism, the formatter is easily extensible and configurable. There is even the ability to specify formatting configuration for a list's children by index -- for example you may want to format the first element in a `let` (the binding list) differently than the rest of the code.
There are also some forms that don't have a head-item, that we still identify and set some default formatting rules:
- The top-level node - indentation is disabled
- Lists of only literals, called `constant lists` - These are indented with `1` space instead of the default `2` for a `flow`'d form.
- If these lists literals are paired with keywords we call these `constant pairs` - We strive to put the pairs on the same line, if they are too long we spill to the next with `2` space indentation
- TODO - cond/case pairs (similar to constant pairs)
This brings up two important terms that you will see in the formatter implementation a lot -- `flow` and `hang`. The difference is as such:
```clj
(println "this"
"form"
"is"
"hung / hanged")
```
```clj
(println
"this"
"form"
"is"
"flowed")
```
The difference between the two other than visually is that hanging always results in 1 less line spent _because we don't care about line-length_. This is great because it means we can always default to hanging unless overridden. Flowing trades off that 1 vertical line for horizontal space and looks great for block-style forms. Like the top-level of a `defun` or the content of a `let`.
### Applying Formatting
The final real-step in the process, actually using the configuration and generating the formatting code. This phase is actually broken up into 3 sub-phases as we recurse through the `FormatterTree`
#### Build Lines
First we build the source code lines without indentation, it's a lot easier to figure out the indentation after all tokens are on the correct lines instead of doing it in tandem. Most tokens are given their own line, but in the case of a hung form we combine the first two elements on the first line. Other forms are formatted in even more complicated ways, or comments that were originally inline should remain next to that token.
Secondly, once we have all our consolidated lines we can apply the indentation. This is most often based on the configuration of the list node itself. For example a `hung` form must be indented the width of a `(` and the length of the head-form, plus the whitespace separator -- conversely a `flowed` form only needs to be indented by `2` spaces, regardless of token length. Because all of these algorithms are recursive we can hyper-focus on getting the indentation and formatting correct from the bottom-up. In otherwords, we don't care if a form is 200 spaces indented into a function, once the lines are returned recursively, they will get another layer of indentation added on until we hit the top-level node.
### Finalize Lines
Finally, we concatenate our list of strings, respecting the original file's line-endings.
As it is best practice, if we are formatting an entire file we should terminate with a trailing new-line.
Both of these items are technically a TODO.
## Configuration
TODO
## Reference
This formatter is highly influenced by a lot of other existing formatters and writings. The most significant of which is Clojure's [zprint](https://github.com/kkinnear/zprint/tree/main/doc/options) which has incredible documentation around it's capabilities and why it chooses what to do.
The formatter I've built basically copies zprint in terms of basic functionality, but simplifies things by being even more opinionated (for example, doing far less in terms of trying to adhere to a line-length limit).
### Other references
- [The Hardest Program I’ve Ever Written - Dart Formatter](https://journal.stuffwithstuff.com/2015/09/08/the-hardest-program-ive-ever-written/)
- [cljfmt](https://github.com/weavejester/cljfmt)
### Why not respect a line-length limit
The OpenGOAL formatter will respect line-length in only very specific situations:
- When determining if a form can be fit entirely on a single line
- Probably in the future for multi-line comments
Besides it adding a massive amount of complexity, even zprint explains that a line-length limit won't always be followed
> zprint will work very hard to fit the formatted output into this width, though there are limits to its effort. For instance, it will not reduce the minimum indents in order to satisfy a particular width requirement.
Let's illustrate with an example:
```clj
(defun some-func
(+ 123
456
(+ 123
456
;; imagine this form exceeds our width, how can you "fix" this without
;; messing up the indentation levels of the entire expression
;; - you can't inline it (you're already past the width)
;; - you can't insert a line break because it'll still be at the same horizontal level
(+ 123
456
789))))
```
In LISP code, we don't have many ideal options to deal with this, all options are syntactically correct, but in terms of conventional readability they are bad because when reading LISP code you want to use the indentation as a guide (which is why new-line parens are considered ugly by basically all LISP style guides)
The other reason which is equally relevant is it is hard, it means the formatter would have to try to find an optimal solution by exploring different solutions until it finds the one that minimizes lines.
But with an opinionated formatting style you can avoid this complexity, if you want your code to look nice don't write code like the decompiler sometimes outputs when it encounters something truly cursed:
![](./docs/triangle-of-death.png)
> `game-save.gc` about 200+ nested `let`s!
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+297 -101
View File
@@ -16,124 +16,307 @@ extern "C" {
extern const TSLanguage* tree_sitter_opengoal();
}
std::string apply_formatting(
const FormatterTreeNode& curr_node,
std::string output,
std::optional<formatter_rules::config::FormFormattingConfig> form_element_config) {
using namespace formatter_rules;
if (!curr_node.token && curr_node.refs.empty()) {
return output;
int hang_indentation_width(const FormatterTreeNode& curr_node) {
if (curr_node.token || curr_node.refs.empty()) {
return 0;
}
std::string curr_form = "";
// Print the token
if (curr_node.token) {
curr_form += curr_node.token.value();
return curr_form;
// Get the first element of the form
const auto& first_elt = curr_node.refs.at(0);
if (first_elt.token) {
return first_elt.token->length() +
2; // +2 because the opening paren and then the following space
}
if (!curr_node.metadata.is_top_level) {
curr_form += "(";
}
// Iterate the form
// Otherwise, continue nesting
return 1 + hang_indentation_width(first_elt);
}
bool inline_form = false;
// Also check if the form should be constant-paired
const bool constant_pair_form = constant_pairs::form_should_be_constant_paired(curr_node);
if (!constant_pair_form) {
// Determine if the form should be inlined or hung/flowed
// TODO - this isn't entirely accurate, needs current cursor positioning (which is tricky
// because recursion!)
inline_form = indent::form_can_be_inlined(curr_form, curr_node);
// TODO - compute length of each node and store it
void apply_formatting_config(
FormatterTreeNode& curr_node,
std::optional<std::shared_ptr<formatter_rules::config::FormFormattingConfig>>
config_from_parent = {}) {
using namespace formatter_rules;
// node is empty, base-case
if (curr_node.token || curr_node.refs.empty()) {
return;
}
const bool flowing = indent::should_form_flow(curr_node, inline_form);
std::optional<formatter_rules::config::FormFormattingConfig> form_config;
if (!curr_node.refs.empty() && curr_node.refs.at(0).token) {
// first, check to see if this form already has a predefined formatting configuration
// if it does, that simplifies things because there is only 1 way of formatting the form
std::optional<formatter_rules::config::FormFormattingConfig> predefined_config;
if (!config_from_parent && !curr_node.refs.empty() && curr_node.refs.at(0).token) {
const auto& form_head = curr_node.refs.at(0).token;
if (form_head && config::opengoal_form_config.find(form_head.value()) !=
config::opengoal_form_config.end()) {
form_config = config::opengoal_form_config.at(form_head.value());
predefined_config = config::opengoal_form_config.at(form_head.value());
curr_node.formatting_config = predefined_config.value();
}
} else if (config_from_parent) {
predefined_config = *config_from_parent.value();
curr_node.formatting_config = predefined_config.value();
}
// TODO - might want to make some kind of per-form config struct, simplify the passing around of
// info below
for (int i = 0; i < curr_node.refs.size(); i++) {
const auto& ref = curr_node.refs.at(i);
// Figure out if the element should be inlined or not
bool inline_element = inline_form;
if (indent::inline_form_element(curr_node, i)) {
inline_element = indent::inline_form_element(curr_node, i).value();
}
// Append a newline if needed
// TODO - cleanup / move
bool is_binding_list = false;
bool force_newline = false;
bool override_force_flow = false;
if (form_config) {
force_newline = std::find(form_config->force_newline_at_indices.begin(),
form_config->force_newline_at_indices.end(),
i) != form_config->force_newline_at_indices.end();
// Check if it's a small enough binding list, if so we don't force a newline if the element
// can be inlined
if (inline_element && i > 0 && form_config->bindings_at_index == i - 1 &&
curr_node.refs.at(i - 1).refs.size() < form_config->allow_inlining_if_size_less_than) {
force_newline = false;
override_force_flow = true;
// In order to keep things simple, as well as because its ineffectual in lisp code (you can only
// enforce it so much without making things unreadable), line width will not matter for deciding
// whether or not to hang or flow the form
//
// This means that a hang would ALWAYS win, because it's 1 less line break. Therefore this
// simplifies our approach there is no need to explore both braches to see which one would be
// preferred.
//
// Instead, we either use the predefined configuration (obviously) or we do some checks for some
// outlier conditions to see if things should be formatted differently
//
// Otherwise, we always default to a hang.
//
// NOTE - any modifications here to child elements could be superseeded later in the recursion
// in order to maintain your sanity, only modify things here that _arent_ touched by default
// configurations. These are explicitly prepended with `parent_mutable_`
if (!predefined_config) {
if (curr_node.metadata.is_top_level) {
curr_node.formatting_config.indentation_width = 0;
curr_node.formatting_config.hang_forms = false;
} else if (constant_list::is_constant_list(curr_node)) {
// - Check if the form is a constant list (ie. a list of numbers)
curr_node.formatting_config.indentation_width = 1;
curr_node.formatting_config.hang_forms = false;
curr_node.formatting_config.has_constant_pairs =
constant_pairs::form_should_be_constant_paired(curr_node);
// If applicable, iterate through the constant pairs, since we can potentially pair up
// non-constant second elements in a pair (like a function call), there is the potential that
// they need to spill to the next line and get indented in extra. This is an exceptional
// circumstance, we do NOT do this sort of thing when formatting normal forms (cond/case pairs
// are another similar situation)
if (curr_node.formatting_config.has_constant_pairs) {
for (int i = 0; i < (int)curr_node.refs.size(); i++) {
auto& child_ref = curr_node.refs.at(i);
const auto type = child_ref.metadata.node_type;
if (constant_types.find(type) == constant_types.end() &&
constant_pairs::is_element_second_in_constant_pair(curr_node, child_ref, i)) {
child_ref.formatting_config.parent_mutable_extra_indent = 2;
}
}
}
is_binding_list = form_config->bindings_at_index == i;
}
if (!curr_node.metadata.is_top_level &&
(!inline_element || is_binding_list || force_newline ||
(form_element_config && form_element_config->force_flow))) {
indent::append_newline(curr_form, ref, curr_node, i, flowing, constant_pair_form,
(form_element_config && form_element_config->force_flow));
} else if (curr_node.formatting_config.hang_forms && curr_node.refs.size() > 1 &&
curr_node.refs.at(1).metadata.is_comment) {
// - Check if the second argument is a comment, it looks better if we flow instead
curr_node.formatting_config.hang_forms = false;
}
// TODO - indent the line (or don't)
// Either print the element's token, or recursively format it as well
}
// If we are hanging, lets determine the indentation width since it is based on the form itself
if (curr_node.formatting_config.hang_forms) {
curr_node.formatting_config.indentation_width = hang_indentation_width(curr_node);
}
// iterate through the refs
for (int i = 0; i < (int)curr_node.refs.size(); i++) {
auto& ref = curr_node.refs.at(i);
if (!ref.token) {
// If the child has a pre-defined configuration at that index, we pass it along
if (predefined_config &&
predefined_config->index_configs.find(i) != predefined_config->index_configs.end()) {
apply_formatting_config(ref, predefined_config->index_configs.at(i));
} else if (predefined_config && predefined_config->default_index_config) {
apply_formatting_config(ref, predefined_config->default_index_config);
} else {
apply_formatting_config(ref);
}
}
}
}
int get_total_form_inlined_width(const FormatterTreeNode& curr_node) {
if (curr_node.token) {
return curr_node.token->length();
}
int width = 1;
for (const auto& ref : curr_node.refs) {
width += get_total_form_inlined_width(ref);
}
return width + 1;
}
bool form_contains_comment(const FormatterTreeNode& curr_node) {
if (curr_node.metadata.is_comment) {
return true;
}
for (const auto& ref : curr_node.refs) {
const auto contains_comment = form_contains_comment(ref);
if (contains_comment) {
return true;
}
}
return false;
}
bool form_contains_node_that_prevents_inlining(const FormatterTreeNode& curr_node) {
if (curr_node.formatting_config.should_prevent_inlining(curr_node.formatting_config,
curr_node.refs.size())) {
return true;
}
for (const auto& ref : curr_node.refs) {
const auto prevents_inlining = form_contains_node_that_prevents_inlining(ref);
if (prevents_inlining) {
return true;
}
}
return false;
}
bool can_node_be_inlined(const FormatterTreeNode& curr_node, int cursor_pos) {
using namespace formatter_rules;
// First off, we cannot inline the top level
if (curr_node.metadata.is_top_level) {
return false;
}
// If the config explicitly prevents inlining, or it contains a sub-node that prevents inlining
if (curr_node.formatting_config.prevent_inlining ||
form_contains_node_that_prevents_inlining(curr_node)) {
return false;
}
// nor can we inline something that contains a comment in the middle
if (form_contains_comment(curr_node)) {
return false;
}
// constant pairs are not inlined!
if (curr_node.formatting_config.has_constant_pairs) {
return false;
}
// If this is set in the config, then the form is intended to be partially inlined
if (curr_node.formatting_config.inline_until_index({})) {
return false;
}
// let's see if we can inline the form all on one line to do that, we recursively explore
// the form to find the total width
int line_width = cursor_pos + get_total_form_inlined_width(curr_node);
return line_width <= indent::line_width_target; // TODO - comments
}
std::vector<std::string> apply_formatting(const FormatterTreeNode& curr_node,
std::vector<std::string> output = {},
int cursor_pos = 0) {
using namespace formatter_rules;
if (!curr_node.token && curr_node.refs.empty()) {
// special case to handle an empty list
return {"()"};
}
// If its a token, just print the token and move on
if (curr_node.token) {
return {curr_node.token.value()};
}
bool inline_form = can_node_be_inlined(curr_node, cursor_pos);
// TODO - also if the form is inlinable, we can skip all the complication below and just...inline
// it!
// TODO - should figure out the inlining here as well, instead of the bool above
// Iterate the form, building up a list of the final lines but don't worry about indentation
// at this stage. Once the lines are finalized, it's easy to add the indentation later
//
// This means we may combine elements onto the same line in this step.
std::vector<std::string> form_lines = {};
for (int i = 0; i < (int)curr_node.refs.size(); i++) {
const auto& ref = curr_node.refs.at(i);
// Add new line entry
if (ref.token) {
// TODO depth hard-coded to 1, i think this can be removed, since
// forms are always done bottom-top recursively, they always act
// independently as if it was the shallowest depth
if (!inline_element || force_newline) {
indent::indent_line(curr_form, ref, curr_node, 1, i, flowing);
}
if (ref.metadata.node_type == "comment" && ref.metadata.is_inline) {
curr_form += " " + ref.token.value();
} else if (ref.metadata.node_type == "block_comment") {
curr_form += comments::format_block_comment(ref.token.value());
} else {
curr_form += ref.token.value();
}
if (!curr_node.metadata.is_top_level) {
curr_form += " ";
// Cleanup block-comments
std::string val = ref.token.value();
if (ref.metadata.node_type == "block_comment") {
// TODO - change this sanitization to return a list of lines instead of a single new-lined
// line
val = comments::format_block_comment(ref.token.value());
}
form_lines.push_back(val);
} else {
// See if the item at this position has specific formatting
std::optional<formatter_rules::config::FormFormattingConfig> config = {};
std::string formatted_form;
if (form_config && form_config->index_configs.find(i) != form_config->index_configs.end()) {
formatted_form = apply_formatting(ref, "", *form_config->index_configs.at(i));
} else {
formatted_form = apply_formatting(ref, "", {});
}
// TODO - align inner lines only
if (!curr_node.metadata.is_top_level) {
indent::align_lines(
formatted_form, ref, curr_node, constant_pair_form, flowing,
(!override_force_flow && form_config && i >= form_config->start_flow_at_index),
inline_element);
}
curr_form += formatted_form;
if (!curr_node.metadata.is_top_level) {
curr_form += " ";
// If it's not a token, we have to recursively build up the form
// TODO - add the cursor_pos here
const auto& lines = apply_formatting(ref, {}, cursor_pos);
for (int i = 0; i < (int)lines.size(); i++) {
const auto& line = lines.at(i);
form_lines.push_back(fmt::format(
"{}{}", str_util::repeat(ref.formatting_config.parent_mutable_extra_indent, " "),
line));
}
}
// Handle blank lines at the top level, skip if it's the final element
blank_lines::separate_by_newline(curr_form, curr_node, ref, i);
// If we are hanging forms, combine the first two forms onto the same line
if (i == (int)curr_node.refs.size() - 1 && form_lines.size() > 1 &&
(curr_node.formatting_config.hang_forms ||
curr_node.formatting_config.combine_first_two_lines)) {
form_lines.at(0) += fmt::format(" {}", form_lines.at(1));
form_lines.erase(form_lines.begin() + 1);
} else if ((i + 1) < (int)curr_node.refs.size()) {
const auto& next_ref = curr_node.refs.at(i + 1);
// combine the next inline comment or constant pair
if ((next_ref.metadata.node_type == "comment" && next_ref.metadata.is_inline) ||
(curr_node.formatting_config.has_constant_pairs &&
constant_pairs::is_element_second_in_constant_pair(curr_node, next_ref, i + 1))) {
if (next_ref.token) {
form_lines.at(form_lines.size() - 1) += fmt::format(" {}", next_ref.token.value());
i++;
} else if (can_node_be_inlined(next_ref, cursor_pos)) {
const auto& lines = apply_formatting(next_ref, {}, cursor_pos); // TODO - cursor pos
for (const auto& line : lines) {
form_lines.at(form_lines.size() - 1) += fmt::format(" {}", line);
}
i++;
}
}
}
// If we are at the top level, potential separate with a new line
if (blank_lines::should_insert_blank_line(curr_node, ref, i)) {
form_lines.at(form_lines.size() - 1) += "\n";
}
}
// Consolidate any lines if the configuration requires it
if (curr_node.formatting_config.inline_until_index(form_lines)) {
std::vector<std::string> new_form_lines = {};
for (int i = 0; i < (int)form_lines.size(); i++) {
if (i < curr_node.formatting_config.inline_until_index(form_lines)) {
if (new_form_lines.empty()) {
new_form_lines.push_back(form_lines.at(i));
} else {
new_form_lines.at(0) += fmt::format(" {}", form_lines.at(i));
}
} else {
new_form_lines.push_back(form_lines.at(i));
}
}
form_lines = new_form_lines;
}
// Apply necessary indentation to each line and add parens
if (!curr_node.metadata.is_top_level) {
curr_form = str_util::rtrim(curr_form) + ")";
std::string form_surround_start = "(";
std::string form_surround_end = ")";
form_lines[0] = fmt::format("{}{}", form_surround_start, form_lines[0]);
form_lines[form_lines.size() - 1] =
fmt::format("{}{}", form_lines[form_lines.size() - 1], form_surround_end);
}
return curr_form;
std::string curr_form = "";
if (curr_node.formatting_config.parent_mutable_extra_indent > 0) {
curr_form += str_util::repeat(curr_node.formatting_config.parent_mutable_extra_indent, " ");
}
if (inline_form) {
form_lines = {fmt::format("{}", fmt::join(form_lines, " "))};
} else {
for (int i = 0; i < (int)form_lines.size(); i++) {
if (i > 0) {
auto& line = form_lines.at(i);
line = fmt::format("{}{}",
str_util::repeat(curr_node.formatting_config.indentation_width_for_index(
curr_node.formatting_config, i),
" "),
line);
}
}
}
return form_lines;
}
std::string join_formatted_lines(const std::vector<std::string> lines) {
// TODO - respect original file line endings
return fmt::format("{}", fmt::join(lines, "\n"));
}
std::optional<std::string> formatter::format_code(const std::string& source) {
@@ -155,9 +338,22 @@ std::optional<std::string> formatter::format_code(const std::string& source) {
}
try {
const auto formatting_tree = FormatterTree(source, root_node);
std::string formatted_code = apply_formatting(formatting_tree.root, "", {});
return formatted_code;
// There are three phases of formatting
// 1. Simplify the AST down to something that is easier to work on from a formatting perspective
// this also gathers basic metadata that can be done at this stage, like if the token is a
// comment or if the form is on the top-level
auto formatting_tree = FormatterTree(source, root_node);
// 2. Recursively iterate through this simplified FormatterTree and figure out what rules
// need to be applied to produce an optimal result
apply_formatting_config(formatting_tree.root);
// 3. Use this updated FormatterTree to print out the final source-code, while doing so
// we may deviate from the optimal result to produce something even more optimal by inlining
// forms that can fit within the line width.
const auto formatted_lines = apply_formatting(formatting_tree.root);
// 4. Now we joint he lines together, it's easier when formatting to leave all lines independent
// so adding indentation is easier
const auto formatted_source = join_formatted_lines(formatted_lines);
return formatted_source;
} catch (std::exception& e) {
lg::error("Unable to format code - {}", e.what());
}
+5
View File
@@ -96,6 +96,11 @@ void FormatterTree::construct_formatter_tree_recursive(const std::string& source
// formatting So for strings, we treat them as if they should be a single token
tree_node.refs.push_back(FormatterTreeNode(source, curr_node));
return;
} else if (curr_node_type == "quoting_lit") {
// same story for quoted symbols
// TODO - expect to have to add more here
tree_node.refs.push_back(FormatterTreeNode(source, curr_node));
return;
}
for (size_t i = 0; i < ts_node_child_count(curr_node); i++) {
const auto child_node = ts_node_child(curr_node, i);
+4 -1
View File
@@ -5,6 +5,7 @@
#include <string>
#include <vector>
#include "rules/rule_config.h"
#include "tree_sitter/api.h"
// Treesitter is fantastic for validating and parsing our code into a structured tree format without
@@ -39,11 +40,13 @@ class FormatterTreeNode {
// eventually token node refs
std::optional<std::string> token;
formatter_rules::config::FormFormattingConfig formatting_config;
FormatterTreeNode() = default;
FormatterTreeNode(const std::string& source, const TSNode& node);
FormatterTreeNode(const Metadata& _metadata) : metadata(_metadata){};
bool is_list() const { return token.has_value(); }
bool is_list() const { return !token.has_value(); }
};
// A FormatterTree has a very simple and crude tree structure where:
+32 -293
View File
@@ -14,27 +14,41 @@ namespace formatter_rules {
// differentiate between a quoted symbol and a quoted form
const std::set<std::string> constant_types = {"kwd_lit", "num_lit", "str_lit",
"char_lit", "null_lit", "bool_lit"};
namespace constant_list {
bool is_constant_list(const FormatterTreeNode& node) {
if (!node.is_list() || node.refs.empty()) {
return false;
}
const auto& type = node.refs.at(0).metadata.node_type;
return constant_types.find(type) != constant_types.end();
}
} // namespace constant_list
namespace blank_lines {
void separate_by_newline(std::string& curr_text,
const FormatterTreeNode& containing_node,
const FormatterTreeNode& node,
const int index) {
// We only are concerned with top level forms or elements
// Skip the last element, no trailing new-lines (let the editors handle this!)
// Also peek ahead to see if there was a comment on this line, if so don't separate things!
if (!containing_node.metadata.is_top_level || index >= containing_node.refs.size() - 1 ||
(containing_node.refs.at(index + 1).metadata.is_comment &&
containing_node.refs.at(index + 1).metadata.is_inline)) {
return;
bool should_insert_blank_line(const FormatterTreeNode& containing_node,
const FormatterTreeNode& node,
const int index) {
// We only do this at the top level and don't leave a trailing new-line
if (!containing_node.metadata.is_top_level || index >= (int)containing_node.refs.size() - 1) {
return false;
}
curr_text += "\n";
// If it's a comment, but has no following blank lines, dont insert a blank line
if (node.metadata.is_comment && node.metadata.num_blank_lines_following == 0) {
return;
return false;
}
// Otherwise, add only 1 blank line
curr_text += "\n";
// If the next form is a comment and is inline, don't insert a comment
if ((index + 1) < (int)containing_node.refs.size() &&
containing_node.refs.at(index + 1).metadata.is_comment &&
containing_node.refs.at(index + 1).metadata.is_inline) {
return false;
}
// TODO - only if the form doesn't fit on a single line
return true;
}
} // namespace blank_lines
namespace comments {
@@ -70,6 +84,7 @@ std::string format_block_comment(const std::string& comment) {
namespace constant_pairs {
// TODO - remove index, not needed, could just pass in the previous node
bool is_element_second_in_constant_pair(const FormatterTreeNode& containing_node,
const FormatterTreeNode& node,
const int index) {
@@ -85,11 +100,7 @@ bool is_element_second_in_constant_pair(const FormatterTreeNode& containing_node
// not be paired
return false;
}
// Check the type of the element
if (constant_types.find(node.metadata.node_type) != constant_types.end()) {
return true;
}
return false;
return true;
}
bool form_should_be_constant_paired(const FormatterTreeNode& node) {
@@ -100,7 +111,7 @@ bool form_should_be_constant_paired(const FormatterTreeNode& node) {
return false;
}
int num_pairs = 0;
for (int i = 0; i < node.refs.size() - 1; i++) {
for (int i = 0; i < (int)node.refs.size() - 1; i++) {
const auto& ref = node.refs.at(i);
const auto& next_ref = node.refs.at(i + 1);
if (ref.token && next_ref.token) {
@@ -118,276 +129,4 @@ bool form_should_be_constant_paired(const FormatterTreeNode& node) {
} // namespace constant_pairs
namespace indent {
int cursor_pos(const std::string& curr_text) {
if (curr_text.empty()) {
return 0;
}
// Get the last line of the text (which is also the line we are on!)
int pos = 0;
for (int i = curr_text.size() - 1; i >= 0; i--) {
const auto& c = curr_text.at(i);
if (c == '\n') {
break;
}
pos++;
}
return pos;
}
int compute_form_width_after_index(const FormatterTreeNode& node,
const int index,
const int depth = 0) {
if (node.refs.empty()) {
if (node.token) {
return node.token->size();
} else {
return 0;
}
}
int form_width = 0;
for (int i = 0; i < node.refs.size(); i++) {
const auto& ref = node.refs.at(i);
if (depth == 0 && i < index) {
continue;
}
if (ref.token) {
form_width += ref.token->size() + 1;
} else {
form_width += compute_form_width_after_index(ref, index, depth + 1) + 1;
}
}
return form_width;
}
bool form_exceed_line_width(const std::string& curr_text,
const FormatterTreeNode& containing_node,
const int index) {
// Compute length from the current cursor position on the line as this check is done for every
// element of the form and not in advance
//
// This is for a good reason, intermediate nodes may override this styling and force to be
// formatted inline
//
// We early out as soon as we exceed the width
int curr_line_pos = cursor_pos(curr_text);
if (curr_line_pos >= line_width_target) {
return true;
}
int remaining_width_required = compute_form_width_after_index(containing_node, index);
if (curr_line_pos + remaining_width_required >= line_width_target) {
return true;
}
return false;
}
bool form_contains_comment(const FormatterTreeNode& node) {
if (node.metadata.is_comment) {
return true;
}
for (const auto& ref : node.refs) {
if (ref.metadata.is_comment) {
return true;
} else if (!node.refs.empty()) {
if (form_contains_comment(ref)) {
return true;
}
}
}
return false;
}
bool form_can_be_inlined(const std::string& curr_text, const FormatterTreeNode& list_node) {
// is the form too long to fit on a line TODO - increase accuracy here
if (form_exceed_line_width(curr_text, list_node, 0)) {
return false;
}
// are there any comments? (inlined or not, doesn't matter)
if (form_contains_comment(list_node)) {
return false;
}
return true;
}
bool should_form_flow(const FormatterTreeNode& list_node, const bool inlining_form) {
if (form_contains_comment(list_node)) {
return true;
}
// does the form begin with a constant (a list of content elements)
if (!inlining_form && !list_node.refs.empty() &&
constant_types.find(list_node.refs.at(0).metadata.node_type) != constant_types.end()) {
return true;
}
// TODO - make a function to make grabbing this metadata easier...
// TODO - honestly should just have an is_list metadata
if (!list_node.refs.empty() && !list_node.refs.at(0).token) {
// if the first element is a comment, force a flow
if (list_node.refs.size() > 1 && list_node.refs.at(1).metadata.is_comment) {
return true;
}
const auto& form_head = list_node.refs.at(0).token;
// See if we have any configuration for this form
if (form_head && config::opengoal_form_config.find(form_head.value()) !=
config::opengoal_form_config.end()) {
const auto& form_config = config::opengoal_form_config.at(form_head.value());
return form_config.force_flow;
}
}
// TODO - cleanup, might be inside a let
/*if (!containing_form.refs.empty() && containing_form.refs.at(0).token) {
const auto& form_head = containing_form.refs.at(0).token;
if (form_head && config::opengoal_form_config.find(form_head.value()) !=
config::opengoal_form_config.end()) {
const auto& form_config = config::opengoal_form_config.at(form_head.value());
if (form_config.force_flow) {
return true;
}
}
}*/
return false;
}
std::optional<bool> inline_form_element(const FormatterTreeNode& list_node, const int index) {
// TODO - honestly should just have an is_list metadata
if (list_node.refs.empty() || !list_node.refs.at(0).token) {
return std::nullopt;
}
const auto& form_head = list_node.refs.at(0).token;
// See if we have any configuration for this form
if (form_head &&
config::opengoal_form_config.find(form_head.value()) != config::opengoal_form_config.end()) {
const auto& form_config = config::opengoal_form_config.at(form_head.value());
if (form_config.inline_until_index != -1) {
return index < form_config.inline_until_index;
}
}
return std::nullopt;
}
void append_newline(std::string& curr_text,
const FormatterTreeNode& node,
const FormatterTreeNode& containing_node,
const int index,
const bool flowing,
const bool constant_pair_form,
const bool force_newline) {
if (force_newline && index >= 1 || (node.metadata.is_comment && !node.metadata.is_inline)) {
curr_text = str_util::rtrim(curr_text) + "\n";
return;
}
if (index <= 0 || containing_node.metadata.is_top_level ||
(node.metadata.is_comment && node.metadata.is_inline) || (!flowing && index <= 1)) {
return;
}
// Check if it's a constant pair
if (constant_pair_form &&
constant_pairs::is_element_second_in_constant_pair(containing_node, node, index)) {
return;
}
curr_text = str_util::rtrim(curr_text) + "\n";
}
void indent_line(std::string& curr_text,
const FormatterTreeNode& node,
const FormatterTreeNode& containing_node,
const int depth,
const int index,
const bool flowing) {
if (node.metadata.is_top_level || (node.metadata.is_inline && node.metadata.is_comment)) {
return;
}
// If the element is the second element in a constant pair, that means we did not append a
// new-line before hand so we require no indentation (it's inline with the previous element)
if (constant_pairs::is_element_second_in_constant_pair(containing_node, node, index)) {
return;
}
// If the first element in the list is a constant, we only indent with 1 space instead
if (index > 0 &&
constant_types.find(containing_node.refs.at(0).metadata.node_type) != constant_types.end()) {
curr_text += str_util::repeat(depth, " ");
} else if (index > 0 && flowing) {
curr_text += str_util::repeat(depth, " ");
} else if (index > 1 && !flowing) {
curr_text += str_util::repeat(containing_node.refs.at(0).token.value().length() + 2, " ");
}
}
// Recursively iterate through the node until we hit a token
int length_to_hang(const FormatterTreeNode& node, int length) {
if (node.token || node.refs.at(0).token) {
return length;
}
return length_to_hang(node.refs.at(0), length + 1);
}
void align_lines(std::string& text,
const FormatterTreeNode& node,
const FormatterTreeNode& containing_node,
const bool constant_pair_form,
const bool flowing,
const bool force_flow,
const bool inline_element) {
const auto lines = str_util::split(text);
int start_index = 0;
if (inline_element) {
start_index = 1;
}
int alignment_width = 2;
if (force_flow) {
start_index = 0;
} else if (constant_pair_form &&
constant_types.find(containing_node.refs.at(0).metadata.node_type) !=
constant_types.end()) {
start_index = 0;
alignment_width = 3;
} else if (!flowing) {
// If the form has a token (it's a normal list)
if (containing_node.refs.at(0).token) {
alignment_width = length_to_hang(containing_node.refs.at(1),
containing_node.refs.at(0).token.value().length()) +
1;
if (!node.token) {
alignment_width++;
}
} else {
// otherwise, it's a list of lists
alignment_width = 1;
}
} else if (!node.token) {
// If it's a list of lists
alignment_width = 1;
}
std::string aligned_form = "";
for (int i = 0; i < lines.size(); i++) {
if (i >= start_index) {
aligned_form += str_util::repeat(alignment_width, " ");
}
aligned_form += lines.at(i);
if (i != lines.size() - 1) {
aligned_form += "\n";
}
}
if (!aligned_form.empty()) {
text = aligned_form;
}
}
} // namespace indent
namespace let {
bool can_be_inlined(const FormatterTreeNode& form) {
// Check a variety of things specific to `let` style forms (ones with bindings)
// - does the binding list have more than one binding?
const auto& bindings = form.refs.at(1); // TODO - assuming
if (bindings.refs.size() > 1) {
return false;
}
return true;
}
} // namespace let
} // namespace formatter_rules
+13 -39
View File
@@ -1,12 +1,19 @@
#pragma once
#include <set>
#include <string>
#include "common/formatter/formatter_tree.h"
namespace formatter_rules {
extern const std::set<std::string> constant_types;
namespace constant_list {
bool is_constant_list(const FormatterTreeNode& node);
}
// The formatter will try to collapse as much space as possible in the top-level, this means
// separating forms by a single empty blank line
// separating forms by a single empty blank line
//
// The exception are comments, top level comments will retain their following blank lines from the
// original source
@@ -18,11 +25,10 @@ namespace formatter_rules {
//
// Reference - https://github.com/kkinnear/zprint/blob/main/doc/options/blank.md
namespace blank_lines {
void separate_by_newline(std::string& curr_text,
const FormatterTreeNode& containing_node,
const FormatterTreeNode& node,
const int index);
}
bool should_insert_blank_line(const FormatterTreeNode& containing_node,
const FormatterTreeNode& node,
const int index);
} // namespace blank_lines
// TODO:
// - align consecutive comment lines
@@ -93,35 +99,6 @@ bool form_should_be_constant_paired(const FormatterTreeNode& node);
namespace indent {
const static int line_width_target = 120;
bool form_can_be_inlined(const std::string& curr_text, const FormatterTreeNode& curr_node);
// TODO - right now this is very primitive in that it only checks against our hard-coded config
// eventually make this explore both routes and determine which is best
// Also factor in distance from the gutter (theres some zprint rationale somewhere on this)
bool should_form_flow(const FormatterTreeNode& list_node, const bool inlining_form);
std::optional<bool> inline_form_element(const FormatterTreeNode& list_node, const int index);
void append_newline(std::string& curr_text,
const FormatterTreeNode& node,
const FormatterTreeNode& containing_node,
const int index,
const bool flowing,
const bool constant_pair_form,
const bool force_newline);
void indent_line(std::string& curr_text,
const FormatterTreeNode& node,
const FormatterTreeNode& containing_node,
const int depth,
const int index,
const bool flowing);
void align_lines(std::string& text,
const FormatterTreeNode& node,
const FormatterTreeNode& containing_node,
const bool constant_pair_form,
const bool flowing,
const bool force_flow,
const bool inline_element);
} // namespace indent
// Let forms fall into two main categories
@@ -138,8 +115,5 @@ void align_lines(std::string& text,
// - forms inside the let binding are flowed
//
// Reference - https://github.com/kkinnear/zprint/blob/main/doc/options/let.md
namespace let {
// TODO - like above, factor in current cursor position
bool can_be_inlined(const FormatterTreeNode& form);
} // namespace let
namespace let {} // namespace let
} // namespace formatter_rules
+66 -12
View File
@@ -1,34 +1,88 @@
#include "rule_config.h"
#include "common/formatter/formatter_tree.h"
namespace formatter_rules {
namespace config {
// TODO - populate these more
// TODO - this could be greatly simplified with C++20's designated initialization
FormFormattingConfig new_flow_rule(int start_index) {
FormFormattingConfig cfg;
cfg.force_flow = true;
cfg.start_flow_at_index = start_index;
cfg.inline_until_index = start_index;
cfg.hang_forms = false;
cfg.inline_until_index = [start_index](const std::vector<std::string>& curr_lines) {
return start_index;
};
return cfg;
}
FormFormattingConfig new_flow_rule_prevent_inlining_indexes(
int start_index,
const std::vector<int>& inlining_preventation_indices) {
FormFormattingConfig cfg;
cfg.hang_forms = false;
cfg.inline_until_index = [start_index](std::vector<std::string> curr_lines) {
if (curr_lines.size() >= 4 && curr_lines.at(3) == "()") {
return 4;
}
return start_index;
};
for (const auto& index : inlining_preventation_indices) {
auto temp_config = std::make_shared<FormFormattingConfig>();
temp_config->prevent_inlining = true;
temp_config->hang_forms = false;
temp_config->indentation_width = 1;
cfg.index_configs.emplace(index, temp_config);
}
return cfg;
}
FormFormattingConfig new_binding_rule() {
FormFormattingConfig cfg;
cfg.start_flow_at_index = 2;
cfg.bindings_at_index = 1;
cfg.force_flow = true;
cfg.force_newline_at_indices = {2};
cfg.allow_inlining_if_size_less_than = 2;
cfg.hang_forms = false;
cfg.combine_first_two_lines = true;
auto binding_list_config = std::make_shared<FormFormattingConfig>();
binding_list_config->force_flow = true;
binding_list_config->hang_forms = false;
binding_list_config->indentation_width = 1;
binding_list_config->indentation_width_for_index = [](FormFormattingConfig /*cfg*/, int index) {
if (index == 0) {
return 0;
}
return 4;
};
binding_list_config->should_prevent_inlining = [](FormFormattingConfig /*config*/, int num_refs) {
// Only prevent inlining a binding list, if there are more than 1 bindings
if (num_refs > 1) {
return true;
}
return false;
};
binding_list_config->prevent_inlining =
true; // TODO - we only want to prevent inlining if there are more than 2 elements
cfg.index_configs.emplace(1, binding_list_config);
return cfg;
}
FormFormattingConfig new_pair_rule(bool combine_first_two_expr) {
FormFormattingConfig cfg;
cfg.hang_forms = false;
cfg.prevent_inlining = true;
cfg.combine_first_two_lines = combine_first_two_expr;
auto pair_config = std::make_shared<FormFormattingConfig>();
pair_config->hang_forms = false;
pair_config->indentation_width = 1;
cfg.default_index_config = pair_config;
return cfg;
}
const std::unordered_map<std::string, FormFormattingConfig> opengoal_form_config = {
{"case", new_pair_rule(true)},
{"cond", new_pair_rule(false)},
{"defmethod", new_flow_rule(3)},
{"deftype", new_flow_rule_prevent_inlining_indexes(3, {3, 4, 5})},
{"defun", new_flow_rule(3)},
{"let", new_binding_rule()}};
{"dotimes", new_flow_rule(2)},
{"let", new_binding_rule()},
{"when", new_flow_rule(2)},
{"with-dma-buffer-add-bucket", new_flow_rule(2)}};
} // namespace config
} // namespace formatter_rules
+21 -12
View File
@@ -1,25 +1,34 @@
#pragma once
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "common/formatter/rules/formatting_rules.h"
namespace formatter_rules {
namespace config {
struct FormFormattingConfig {
bool force_hang = false;
bool force_flow = false;
std::optional<int> allow_inlining_if_size_less_than = {};
int start_hang_at_index = 0;
int start_flow_at_index = 0;
int inline_until_index = -1;
std::optional<int> bindings_at_index = {};
std::optional<int> skip_newlines_until_index = {};
std::vector<int> force_newline_at_indices = {};
bool bindings_force_newlines = false;
// new
bool hang_forms = true; // TODO - remove this eventually, it's only involved in setting the
// indentation width, which we can do via the indentation_width function
int indentation_width =
2; // 2 for a flow // TODO - also remove this, prefer storing the first node's width in the
// metadata on the first pass, that's basically all this does
std::function<int(FormFormattingConfig, int)> indentation_width_for_index =
[](FormFormattingConfig config, int /*index*/) { return config.indentation_width; };
bool combine_first_two_lines =
false; // NOTE - basically hang, but will probably stick around after hang is gone, may be
// redundant (inline_until_index!)
std::function<std::optional<int>(const std::vector<std::string>& curr_lines)> inline_until_index =
[](std::vector<std::string> curr_lines) { return std::nullopt; };
bool has_constant_pairs = false;
bool prevent_inlining = false; // TODO - duplicate of below
std::function<bool(FormFormattingConfig, int num_refs)> should_prevent_inlining =
[](FormFormattingConfig config, int /*num_refs*/) { return config.prevent_inlining; };
int parent_mutable_extra_indent = 0;
std::optional<std::shared_ptr<FormFormattingConfig>> default_index_config;
std::unordered_map<int, std::shared_ptr<FormFormattingConfig>> index_configs = {};
};
+17
View File
@@ -62,12 +62,29 @@ constexpr int LEVEL_MAX = 6;
constexpr int LEVEL_TOTAL = LEVEL_MAX + 1;
} // namespace jak2
// TODO copypaste from jak 2 for now
namespace jak3 {
// for now, we don't have the ability to extend the size of the symbol table
constexpr s32 GOAL_MAX_SYMBOLS = 0x4000;
constexpr s32 SYM_TABLE_MEM_SIZE = 0x30000;
// from the "off-by-one" symbol pointer
constexpr int SYM_TO_STRING_OFFSET = 0xff37;
constexpr int SYM_TO_HASH_OFFSET = 0x1fe6f;
// amount of levels in level heap
constexpr int LEVEL_MAX = 10;
// total amount of levels, including ones outside level heap (default-level)
constexpr int LEVEL_TOTAL = LEVEL_MAX + 1;
} // namespace jak3
constexpr s32 max_symbols(GameVersion version) {
switch (version) {
case GameVersion::Jak1:
return jak1::GOAL_MAX_SYMBOLS;
case GameVersion::Jak2:
return jak2::GOAL_MAX_SYMBOLS;
case GameVersion::Jak3:
return jak2::GOAL_MAX_SYMBOLS;
}
}
+369 -138
View File
@@ -12,6 +12,7 @@
#include "common/goos/Printer.h"
#include "common/log/log.h"
#include "common/util/FileUtil.h"
#include "common/util/crc32.h"
#include "common/util/string_util.h"
#include "common/util/unicode_util.h"
@@ -19,6 +20,10 @@
namespace goos {
Interpreter::Interpreter(const std::string& username) {
m_true_object = intern("#t");
m_false_object = intern("#f");
m_true_sym = m_true_object.as_symbol().name_ptr;
m_false_sym = m_false_object.as_symbol().name_ptr;
// Interpreter startup:
// create the GOOS global environment
global_environment = EnvironmentObject::make_new("global");
@@ -33,15 +38,16 @@ Interpreter::Interpreter(const std::string& username) {
define_var_in_env(goal_env, global_environment, "*global-env*");
// set user profile name
auto user = SymbolObject::make_new(reader.symbolTable, username);
auto user = Object::make_symbol(&reader.symbolTable, username.c_str());
define_var_in_env(global_environment, user, "*user*");
define_var_in_env(goal_env, user, "*user*");
// setup maps
special_forms = {
init_special_forms({
{"define", &Interpreter::eval_define},
{"quote", &Interpreter::eval_quote},
{"set!", &Interpreter::eval_set},
{"let", &Interpreter::eval_let},
{"let*", &Interpreter::eval_let_star},
{"lambda", &Interpreter::eval_lambda},
{"cond", &Interpreter::eval_cond},
{"or", &Interpreter::eval_or},
@@ -49,53 +55,54 @@ Interpreter::Interpreter(const std::string& username) {
{"macro", &Interpreter::eval_macro},
{"quasiquote", &Interpreter::eval_quasiquote},
{"while", &Interpreter::eval_while},
};
});
builtin_forms = {{"top-level", &Interpreter::eval_begin},
{"begin", &Interpreter::eval_begin},
{"exit", &Interpreter::eval_exit},
{"read", &Interpreter::eval_read},
{"read-data-file", &Interpreter::eval_read_data_file},
{"read-file", &Interpreter::eval_read_file},
{"print", &Interpreter::eval_print},
{"inspect", &Interpreter::eval_inspect},
{"load-file", &Interpreter::eval_load_file},
{"try-load-file", &Interpreter::eval_try_load_file},
{"eq?", &Interpreter::eval_equals},
{"gensym", &Interpreter::eval_gensym},
{"eval", &Interpreter::eval_eval},
{"cons", &Interpreter::eval_cons},
{"car", &Interpreter::eval_car},
{"cdr", &Interpreter::eval_cdr},
{"set-car!", &Interpreter::eval_set_car},
{"set-cdr!", &Interpreter::eval_set_cdr},
{"+", &Interpreter::eval_plus},
{"-", &Interpreter::eval_minus},
{"*", &Interpreter::eval_times},
{"/", &Interpreter::eval_divide},
{"=", &Interpreter::eval_numequals},
{"<", &Interpreter::eval_lt},
{">", &Interpreter::eval_gt},
{"<=", &Interpreter::eval_leq},
{">=", &Interpreter::eval_geq},
{"null?", &Interpreter::eval_null},
{"type?", &Interpreter::eval_type},
{"fmt", &Interpreter::eval_format},
{"error", &Interpreter::eval_error},
{"string-ref", &Interpreter::eval_string_ref},
{"string-length", &Interpreter::eval_string_length},
{"string-append", &Interpreter::eval_string_append},
{"string-starts-with?", &Interpreter::eval_string_starts_with},
{"string-ends-with?", &Interpreter::eval_string_ends_with},
{"string-split", &Interpreter::eval_string_split},
{"string-substr", &Interpreter::eval_string_substr},
{"ash", &Interpreter::eval_ash},
{"symbol->string", &Interpreter::eval_symbol_to_string},
{"string->symbol", &Interpreter::eval_string_to_symbol},
{"get-environment-variable", &Interpreter::eval_get_env},
{"make-string-hash-table", &Interpreter::eval_make_string_hash_table},
{"hash-table-set!", &Interpreter::eval_hash_table_set},
{"hash-table-try-ref", &Interpreter::eval_hash_table_try_ref}};
init_builtin_forms({{"top-level", &Interpreter::eval_begin},
{"begin", &Interpreter::eval_begin},
{"exit", &Interpreter::eval_exit},
{"read", &Interpreter::eval_read},
{"read-data-file", &Interpreter::eval_read_data_file},
{"read-file", &Interpreter::eval_read_file},
{"print", &Interpreter::eval_print},
{"inspect", &Interpreter::eval_inspect},
{"load-file", &Interpreter::eval_load_file},
{"try-load-file", &Interpreter::eval_try_load_file},
{"eq?", &Interpreter::eval_equals},
{"gensym", &Interpreter::eval_gensym},
{"eval", &Interpreter::eval_eval},
{"cons", &Interpreter::eval_cons},
{"car", &Interpreter::eval_car},
{"cdr", &Interpreter::eval_cdr},
{"set-car!", &Interpreter::eval_set_car},
{"set-cdr!", &Interpreter::eval_set_cdr},
{"+", &Interpreter::eval_plus},
{"-", &Interpreter::eval_minus},
{"*", &Interpreter::eval_times},
{"/", &Interpreter::eval_divide},
{"=", &Interpreter::eval_numequals},
{"<", &Interpreter::eval_lt},
{">", &Interpreter::eval_gt},
{"<=", &Interpreter::eval_leq},
{">=", &Interpreter::eval_geq},
{"null?", &Interpreter::eval_null},
{"type?", &Interpreter::eval_type},
{"fmt", &Interpreter::eval_format},
{"error", &Interpreter::eval_error},
{"string-ref", &Interpreter::eval_string_ref},
{"string-length", &Interpreter::eval_string_length},
{"int->string", &Interpreter::eval_int_to_string},
{"string-append", &Interpreter::eval_string_append},
{"string-starts-with?", &Interpreter::eval_string_starts_with},
{"string-ends-with?", &Interpreter::eval_string_ends_with},
{"string-split", &Interpreter::eval_string_split},
{"string-substr", &Interpreter::eval_string_substr},
{"ash", &Interpreter::eval_ash},
{"symbol->string", &Interpreter::eval_symbol_to_string},
{"string->symbol", &Interpreter::eval_string_to_symbol},
{"get-environment-variable", &Interpreter::eval_get_env},
{"make-string-hash-table", &Interpreter::eval_make_string_hash_table},
{"hash-table-set!", &Interpreter::eval_hash_table_set},
{"hash-table-try-ref", &Interpreter::eval_hash_table_try_ref}});
string_to_type = {{"empty-list", ObjectType::EMPTY_LIST},
{"integer", ObjectType::INTEGER},
@@ -113,6 +120,28 @@ Interpreter::Interpreter(const std::string& username) {
load_goos_library();
}
void Interpreter::init_builtin_forms(
const std::unordered_map<std::string,
Object (Interpreter::*)(const Object&,
Arguments&,
const std::shared_ptr<EnvironmentObject>&)>&
forms) {
for (const auto& [name, fn] : forms) {
builtin_forms[(void*)intern_ptr(name).name_ptr] = fn;
}
}
void Interpreter::init_special_forms(
const std::unordered_map<std::string,
Object (Interpreter::*)(const Object&,
const Object&,
const std::shared_ptr<EnvironmentObject>&)>&
forms) {
for (const auto& [name, fn] : forms) {
special_forms.push_back(std::make_pair((void*)intern_ptr(name).name_ptr, fn));
}
}
/*!
* Add a user defined special form. The given function will be called with unevaluated arguments.
* Lookup from these forms occurs after special/builtin, but before any env lookups.
@@ -121,7 +150,7 @@ void Interpreter::register_form(
const std::string& name,
const std::function<
Object(const Object&, Arguments&, const std::shared_ptr<EnvironmentObject>&)>& form) {
m_custom_forms[name] = form;
m_custom_forms.push_back(std::make_pair((void*)intern_ptr(name).name_ptr, form));
}
Interpreter::~Interpreter() {
@@ -149,19 +178,19 @@ void Interpreter::load_goos_library() {
/*!
* In env, set the variable named "name" to the value var.
*/
void Interpreter::define_var_in_env(Object& env, Object& var, const std::string& name) {
env.as_env()->vars[intern_ptr(name)] = var;
void Interpreter::define_var_in_env(Object& env, const Object& var, const std::string& name) {
env.as_env()->vars.set(InternedSymbolPtr{intern_ptr(name)}, var);
}
/*!
* Get a symbol with the given name, creating one if none exist.
*/
Object Interpreter::intern(const std::string& name) {
return SymbolObject::make_new(reader.symbolTable, name);
return Object::make_symbol(&reader.symbolTable, name.c_str());
}
HeapObject* Interpreter::intern_ptr(const std::string& name) {
return reader.symbolTable.intern_ptr(name);
InternedSymbolPtr Interpreter::intern_ptr(const std::string& name) {
return reader.symbolTable.intern(name.c_str());
}
/*!
@@ -218,10 +247,9 @@ Object Interpreter::eval_with_rewind(const Object& obj,
* Returns if the variable was found.
*/
bool Interpreter::get_global_variable_by_name(const std::string& name, Object* dest) {
auto kv = global_environment.as_env()->vars.find(
SymbolObject::make_new(reader.symbolTable, name).as_symbol());
if (kv != global_environment.as_env()->vars.end()) {
*dest = kv->second;
auto* obj = global_environment.as_env()->find(name.c_str(), &reader.symbolTable);
if (obj) {
*dest = *obj;
return true;
}
return false;
@@ -231,12 +259,11 @@ bool Interpreter::get_global_variable_by_name(const std::string& name, Object* d
* Sets the variable to the value. Overwrites an existing value, or creates a new global.
*/
void Interpreter::set_global_variable_by_name(const std::string& name, const Object& value) {
auto sym = SymbolObject::make_new(reader.symbolTable, name).as_symbol();
global_environment.as_env()->vars[sym] = value;
define_var_in_env(global_environment, value, name);
}
void Interpreter::set_global_variable_to_symbol(const std::string& name, const std::string& value) {
auto sym = SymbolObject::make_new(reader.symbolTable, value);
auto sym = Object::make_symbol(&reader.symbolTable, value.c_str());
set_global_variable_by_name(name, sym);
}
@@ -269,18 +296,18 @@ Arguments Interpreter::get_args(const Object& form, const Object& rest, const Ar
const auto& arg = current->as_pair()->car;
// did we get a ":keyword"
if (arg.is_symbol() && arg.as_symbol()->name.at(0) == ':') {
auto key_name = arg.as_symbol()->name.substr(1);
if (arg.is_symbol() && arg.as_symbol().name_ptr[0] == ':') {
auto key_name = arg.as_symbol().name_ptr + 1;
const auto& kv = spec.named.find(key_name);
// check for unknown key name
if (!spec.varargs && kv == spec.named.end()) {
throw_eval_error(form, "Key argument " + key_name + " wasn't expected");
throw_eval_error(form, fmt::format("Key argument {} wasn't expected", key_name));
}
// check for multiple definition of key
if (args.named.find(key_name) != args.named.end()) {
throw_eval_error(form, "Key argument " + key_name + " multiply defined");
throw_eval_error(form, fmt::format("Key argument {} multiply defined", key_name));
}
// check for well-formed :key value expression
@@ -409,7 +436,7 @@ ArgumentSpec Interpreter::parse_arg_spec(const Object& form, Object& rest) {
throw_eval_error(form, "args must be symbols");
}
if (arg.as_symbol()->name == "&rest") {
if (arg.as_symbol() == "&rest") {
// special case for &rest
current = current.as_pair()->cdr;
if (!current.is_pair()) {
@@ -420,20 +447,20 @@ ArgumentSpec Interpreter::parse_arg_spec(const Object& form, Object& rest) {
throw_eval_error(form, "rest name must be a symbol");
}
spec.rest = rest_name.as_symbol()->name;
spec.rest = rest_name.as_symbol().name_ptr;
if (!current.as_pair()->cdr.is_empty_list()) {
throw_eval_error(form, "rest must be the last argument");
}
} else if (arg.as_symbol()->name == "&key") {
} else if (arg.as_symbol() == "&key") {
// special case for &key
current = current.as_pair()->cdr;
auto key_arg = current.as_pair()->car;
if (key_arg.is_symbol()) {
// form is &key name
auto key_arg_name = key_arg.as_symbol()->name;
auto key_arg_name = key_arg.as_symbol().name_ptr;
if (spec.named.find(key_arg_name) != spec.named.end()) {
throw_eval_error(form, "key argument " + key_arg_name + " multiply defined");
throw_eval_error(form, fmt::format("key argument {} multiply defined", key_arg_name));
}
spec.named[key_arg_name] = NamedArg();
} else if (key_arg.is_pair()) {
@@ -444,9 +471,9 @@ ArgumentSpec Interpreter::parse_arg_spec(const Object& form, Object& rest) {
if (!kn.is_symbol()) {
throw_eval_error(form, "key argument must have a symbol as a name");
}
auto key_arg_name = kn.as_symbol()->name;
auto key_arg_name = kn.as_symbol().name_ptr;
if (spec.named.find(key_arg_name) != spec.named.end()) {
throw_eval_error(form, "key argument " + key_arg_name + " multiply defined");
throw_eval_error(form, fmt::format("key argument {} multiply defined", key_arg_name));
}
NamedArg na;
@@ -466,7 +493,7 @@ ArgumentSpec Interpreter::parse_arg_spec(const Object& form, Object& rest) {
throw_eval_error(form, "invalid key argument");
}
} else {
spec.unnamed.push_back(arg.as_symbol()->name);
spec.unnamed.push_back(arg.as_symbol().name_ptr);
}
current = current.as_pair()->cdr;
@@ -494,20 +521,22 @@ void Interpreter::vararg_check(
/*!
* Evaluate a list and return the result of the last evaluation.
*/
Object Interpreter::eval_list_return_last(const Object& form,
Object Interpreter::eval_list_return_last(const Object& /*form*/,
Object rest,
const std::shared_ptr<EnvironmentObject>& env) {
Object o = std::move(rest);
Object rv = Object::make_empty_list();
for (;;) {
if (o.is_pair()) {
auto op = o.as_pair();
rv = eval_with_rewind(op->car, env);
o = op->cdr;
} else if (o.is_empty_list()) {
return rv;
if (rest.is_empty_list()) {
return rest;
}
const Object* iter = &rest;
while (true) {
const Object* next = &iter->as_pair()->cdr;
const Object* item = &iter->as_pair()->car;
if (next->is_empty_list()) {
return eval_with_rewind(*item, env);
} else {
throw_eval_error(form, "malformed body to evaluate");
eval(*item, env);
iter = next;
}
}
}
@@ -542,6 +571,104 @@ Object Interpreter::eval(Object obj, const std::shared_ptr<EnvironmentObject>& e
}
}
EnvironmentMap::EnvironmentMap() {
clear();
}
void EnvironmentMap::clear() {
m_entries.clear();
m_power_of_two_size = 3; // 2 ^ 3 = 8
m_entries.resize(8);
m_used_entries = 0;
m_next_resize = (m_entries.size() * kMaxUsed);
m_mask = 0b111;
}
Object* EnvironmentMap::lookup(InternedSymbolPtr str) {
if (m_entries.size() < 10) {
for (auto& e : m_entries) {
if (e.key == str.name_ptr) {
return &e.value;
}
}
return nullptr;
}
u32 hash = crc32((const u8*)&str.name_ptr, sizeof(const char*));
// probe
for (u32 i = 0; i < m_entries.size(); i++) {
u32 slot_addr = (hash + i) & m_mask;
auto& slot = m_entries[slot_addr];
if (!slot.key) {
return nullptr;
} else {
if (slot.key != str.name_ptr) {
continue; // bad hash
}
return &slot.value;
}
}
// should be impossible to reach.
ASSERT_NOT_REACHED();
}
void EnvironmentMap::set(InternedSymbolPtr ptr, const Object& obj) {
u32 hash = crc32((const u8*)&ptr.name_ptr, sizeof(const char*));
// probe
for (u32 i = 0; i < m_entries.size(); i++) {
u32 slot_addr = (hash + i) & m_mask;
auto& slot = m_entries[slot_addr];
if (!slot.key) {
// not found, insert!
slot.key = ptr.name_ptr;
slot.value = obj;
m_used_entries++;
if (m_used_entries >= m_next_resize) {
resize();
}
return;
} else {
if (slot.key == ptr.name_ptr) {
slot.value = obj;
return;
}
}
}
// should be impossible to reach.
ASSERT_NOT_REACHED();
}
void EnvironmentMap::resize() {
m_power_of_two_size++;
m_mask = (1U << m_power_of_two_size) - 1;
std::vector<Entry> new_entries(m_entries.size() * 2);
for (const auto& old_entry : m_entries) {
if (old_entry.key) {
bool done = false;
u32 hash = crc32((const u8*)&old_entry.key, sizeof(const char*));
for (u32 i = 0; i < new_entries.size(); i++) {
u32 slot_addr = (hash + i) & m_mask;
auto& slot = new_entries[slot_addr];
if (!slot.key) {
slot.key = old_entry.key;
slot.value = std::move(old_entry.value);
done = true;
break;
}
}
ASSERT(done);
}
}
m_entries = std::move(new_entries);
m_next_resize = kMaxUsed * m_entries.size();
}
namespace {
/*!
@@ -552,7 +679,7 @@ bool try_symbol_lookup(const Object& sym,
const std::shared_ptr<EnvironmentObject>& env,
Object* dest) {
// booleans are hard-coded here
if (sym.as_symbol()->name == "#t" || sym.as_symbol()->name == "#f") {
if (sym.as_symbol() == "#t" || sym.as_symbol() == "#f") {
*dest = sym;
return true;
}
@@ -560,9 +687,9 @@ bool try_symbol_lookup(const Object& sym,
// loop up envs until we find it.
EnvironmentObject* search_env = env.get();
for (;;) {
const auto& kv = search_env->vars.find(sym.as_symbol());
if (kv != search_env->vars.end()) {
*dest = kv->second;
auto* obj = search_env->vars.lookup(sym.as_symbol());
if (obj) {
*dest = *obj;
return true;
}
@@ -593,6 +720,59 @@ bool Interpreter::eval_symbol(const Object& sym,
return try_symbol_lookup(sym, env, result);
}
Object Interpreter::eval_let_star(const goos::Object& form,
const goos::Object& rest,
const std::shared_ptr<EnvironmentObject>& env) {
return eval_let_common(form, rest, env, true);
}
Object Interpreter::eval_let(const goos::Object& form,
const goos::Object& rest,
const std::shared_ptr<EnvironmentObject>& env) {
return eval_let_common(form, rest, env, false);
}
Object Interpreter::eval_let_common(const goos::Object& form,
const goos::Object& rest,
const std::shared_ptr<EnvironmentObject>& env,
bool is_star) {
if (!rest.is_pair()) {
throw_eval_error(form, "first argument to let must be bindings");
}
const auto* bindings_iter = &rest.as_pair()->car;
const auto* body_iter = &rest.as_pair()->cdr;
if (!bindings_iter->is_pair()) {
throw_eval_error(form, "let cannot have empty bindings");
}
std::shared_ptr<EnvironmentObject> new_env = std::make_shared<EnvironmentObject>();
new_env->parent_env = env;
while (!bindings_iter->is_empty_list()) {
const auto* binding = &bindings_iter->as_pair()->car;
if (!binding->is_pair()) {
throw_eval_error(form, "let binding invalid");
}
const auto& name = binding->as_pair()->car;
if (!name.is_symbol()) {
throw_eval_error(form, "let binding invalid");
}
binding = &binding->as_pair()->cdr;
if (!binding->is_pair() || !binding->as_pair()->cdr.is_empty_list()) {
throw_eval_error(form, "let binding invalid");
}
new_env->vars.set(name.as_symbol(), eval(binding->as_pair()->car, is_star ? new_env : env));
bindings_iter = &bindings_iter->as_pair()->cdr;
}
return eval_list_return_last(*body_iter, *body_iter, new_env);
}
/*!
* Evaluate a pair, either as special form, builtin form, macro application, or lambda application.
*/
@@ -606,13 +786,14 @@ Object Interpreter::eval_pair(const Object& obj, const std::shared_ptr<Environme
const auto& head_sym = head.as_symbol();
// try a special form first
const auto& kv_sf = special_forms.find(head_sym->name);
if (kv_sf != special_forms.end()) {
return ((*this).*(kv_sf->second))(obj, rest, env);
for (const auto& sf : special_forms) {
if (sf.first == head_sym.name_ptr) {
return ((*this).*(sf.second))(obj, rest, env);
}
}
// try builtins next
const auto& kv_b = builtin_forms.find(head_sym->name);
const auto& kv_b = builtin_forms.find((void*)head_sym.name_ptr);
if (kv_b != builtin_forms.end()) {
Arguments args = get_args(obj, rest, make_varargs());
// all "built-in" forms expect arguments to be evaluated (that's why they aren't special)
@@ -621,10 +802,11 @@ Object Interpreter::eval_pair(const Object& obj, const std::shared_ptr<Environme
}
// try custom forms next
const auto& kv_u = m_custom_forms.find(head_sym->name);
if (kv_u != m_custom_forms.end()) {
Arguments args = get_args(obj, rest, make_varargs());
return (kv_u->second)(obj, args, env);
for (const auto& cf : m_custom_forms) {
if (cf.first == head_sym.name_ptr) {
Arguments args = get_args(obj, rest, make_varargs());
return (cf.second)(obj, args, env);
}
}
// try macros next
@@ -678,18 +860,18 @@ void Interpreter::set_args_in_env(const Object& form,
// unnamed args
for (size_t i = 0; i < arg_spec.unnamed.size(); i++) {
env->vars[intern(arg_spec.unnamed.at(i)).as_symbol()] = args.unnamed.at(i);
env->vars.set(intern_ptr(arg_spec.unnamed.at(i).c_str()), args.unnamed.at(i));
}
// named args
for (const auto& kv : arg_spec.named) {
env->vars[intern(kv.first).as_symbol()] = args.named.at(kv.first);
env->vars.set(intern_ptr(kv.first), args.named.at(kv.first));
}
// rest args
if (!arg_spec.rest.empty()) {
// will correctly handle the '() case
env->vars[intern(arg_spec.rest).as_symbol()] = build_list(args.rest);
env->vars.set(intern_ptr(arg_spec.rest), build_list(args.rest));
} else {
if (!args.rest.empty()) {
throw_eval_error(form, "got too many arguments");
@@ -714,7 +896,7 @@ Object Interpreter::eval_define(const Object& form,
}
Object value = eval_with_rewind(args.unnamed[1], env);
define_env->vars[args.unnamed[0].as_symbol()] = value;
define_env->vars.set(args.unnamed[0].as_symbol(), value);
return value;
}
@@ -732,10 +914,10 @@ Object Interpreter::eval_set(const Object& form,
std::shared_ptr<EnvironmentObject> search_env = env;
for (;;) {
auto kv = search_env->vars.find(to_define.as_symbol());
if (kv != search_env->vars.end()) {
kv->second = to_set;
return kv->second;
auto kv = search_env->vars.lookup(to_define.as_symbol());
if (kv) {
search_env->vars.set(to_define.as_symbol(), to_set);
return to_set;
}
auto pe = search_env->parent_env;
@@ -819,6 +1001,32 @@ Object Interpreter::eval_quote(const Object& form,
return args.unnamed.front();
}
Object build_list_with_spliced_tail(std::vector<Object>&& objects, const Object& tail) {
if (objects.empty()) {
return tail;
}
std::shared_ptr<PairObject> head = std::make_shared<PairObject>(objects.back(), tail);
s64 idx = ((s64)objects.size()) - 2;
while (idx >= 0) {
Object next;
next.type = ObjectType::PAIR;
next.heap_obj = std::move(head);
head = std::make_shared<PairObject>();
head->car = std::move(objects[idx]);
head->cdr = std::move(next);
idx--;
}
Object result;
result.type = ObjectType::PAIR;
result.heap_obj = std::move(head);
return result;
}
/*!
* Recursive quasi-quote evaluation
*/
@@ -831,7 +1039,7 @@ Object Interpreter::quasiquote_helper(const Object& form,
const Object& item = lst_iter->as_pair()->car;
if (item.type == ObjectType::PAIR) {
if (item.as_pair()->car.type == ObjectType::SYMBOL &&
item.as_pair()->car.as_symbol()->name == "unquote") {
item.as_pair()->car.as_symbol() == "unquote") {
const Object& unquote_arg = item.as_pair()->cdr;
if (unquote_arg.type != ObjectType::PAIR ||
unquote_arg.as_pair()->cdr.type != ObjectType::EMPTY_LIST) {
@@ -841,7 +1049,7 @@ Object Interpreter::quasiquote_helper(const Object& form,
lst_iter = &lst_iter->as_pair()->cdr;
continue;
} else if (item.as_pair()->car.type == ObjectType::SYMBOL &&
item.as_pair()->car.as_symbol()->name == "unquote-splicing") {
item.as_pair()->car.as_symbol() == "unquote-splicing") {
const Object& unquote_arg = item.as_pair()->cdr;
if (unquote_arg.type != ObjectType::PAIR ||
unquote_arg.as_pair()->cdr.type != ObjectType::EMPTY_LIST) {
@@ -851,6 +1059,11 @@ Object Interpreter::quasiquote_helper(const Object& form,
// bypass normal addition:
lst_iter = &lst_iter->as_pair()->cdr;
Object splice_result = eval_with_rewind(unquote_arg.as_pair()->car, env);
if (lst_iter->type == ObjectType::EMPTY_LIST) {
// optimization!
return build_list_with_spliced_tail(std::move(result), splice_result);
}
const Object* to_add = &splice_result;
for (;;) {
if (to_add->type == ObjectType::PAIR) {
@@ -864,8 +1077,13 @@ Object Interpreter::quasiquote_helper(const Object& form,
}
continue;
} else {
result.push_back(quasiquote_helper(item, env));
lst_iter = &lst_iter->as_pair()->cdr;
if (item.is_pair()) {
result.push_back(quasiquote_helper(item, env));
} else {
result.push_back(item);
}
continue;
}
}
@@ -891,7 +1109,7 @@ Object Interpreter::eval_quasiquote(const Object& form,
}
bool Interpreter::truthy(const Object& o) {
return !(o.is_symbol() && o.as_symbol()->name == "#f");
return !(o.is_symbol() && o.as_symbol().name_ptr == m_false_sym);
}
/*!
@@ -924,7 +1142,7 @@ Object Interpreter::eval_cond(const Object& form,
lst = lst.as_pair()->cdr;
}
} else if (lst.type == ObjectType::EMPTY_LIST) {
return SymbolObject::make_new(reader.symbolTable, "#f");
return m_false_object;
} else {
throw_eval_error(form, "malformed cond");
}
@@ -950,7 +1168,7 @@ Object Interpreter::eval_or(const Object& form,
}
lst = lst.as_pair()->cdr;
} else if (lst.type == ObjectType::EMPTY_LIST) {
return SymbolObject::make_new(reader.symbolTable, "#f");
return m_false_object;
} else {
throw_eval_error(form, "invalid or form");
}
@@ -973,7 +1191,7 @@ Object Interpreter::eval_and(const Object& form,
if (lst.type == ObjectType::PAIR) {
current = eval_with_rewind(lst.as_pair()->car, env);
if (!truthy(current)) {
return SymbolObject::make_new(reader.symbolTable, "#f");
return m_false_object;
}
lst = lst.as_pair()->cdr;
} else if (lst.type == ObjectType::EMPTY_LIST) {
@@ -1000,7 +1218,7 @@ Object Interpreter::eval_while(const Object& form,
throw_eval_error(form, "while must have condition and body");
}
Object rv = SymbolObject::make_new(reader.symbolTable, "#f");
Object rv = m_false_object;
while (truthy(eval_with_rewind(condition, env))) {
rv = eval_list_return_last(form, body, env);
}
@@ -1126,7 +1344,7 @@ Object Interpreter::eval_try_load_file(const Object& form,
auto path = {args.unnamed.at(0).as_string()->data};
if (!fs::exists(file_util::get_file_path(path))) {
return SymbolObject::make_new(reader.symbolTable, "#f");
return m_false_object;
}
Object o;
@@ -1141,7 +1359,7 @@ Object Interpreter::eval_try_load_file(const Object& form,
} catch (std::runtime_error& e) {
throw_eval_error(form, std::string("eval error inside of try-load-file:\n") + e.what());
}
return SymbolObject::make_new(reader.symbolTable, "#t");
return m_true_object;
}
/*!
@@ -1177,6 +1395,14 @@ Object Interpreter::eval_inspect(const Object& form,
return Object::make_empty_list();
}
const Object& Interpreter::true_or_false(bool val) {
if (val) {
return m_true_object;
} else {
return m_false_object;
}
}
/*!
* Fancy equality check (using Object::operator==)
*/
@@ -1185,8 +1411,7 @@ Object Interpreter::eval_equals(const Object& form,
const std::shared_ptr<EnvironmentObject>& env) {
(void)env;
vararg_check(form, args, {{}, {}}, {});
return SymbolObject::make_new(reader.symbolTable,
args.unnamed[0] == args.unnamed[1] ? "#t" : "#f");
return true_or_false(args.unnamed[0] == args.unnamed[1]);
}
/*!
@@ -1430,7 +1655,7 @@ Object Interpreter::eval_numequals(const Object& form,
return Object::make_empty_list();
}
return SymbolObject::make_new(reader.symbolTable, result ? "#t" : "#f");
return true_or_false(result);
}
template <typename T>
@@ -1441,7 +1666,7 @@ Object Interpreter::num_lt(const Object& form,
(void)env;
T a = number<T>(args.unnamed[0]);
T b = number<T>(args.unnamed[1]);
return SymbolObject::make_new(reader.symbolTable, (a < b) ? "#t" : "#f");
return true_or_false(a < b);
}
Object Interpreter::eval_lt(const Object& form,
@@ -1469,7 +1694,7 @@ Object Interpreter::num_gt(const Object& form,
(void)env;
T a = number<T>(args.unnamed[0]);
T b = number<T>(args.unnamed[1]);
return SymbolObject::make_new(reader.symbolTable, (a > b) ? "#t" : "#f");
return true_or_false(a > b);
}
Object Interpreter::eval_gt(const Object& form,
@@ -1497,7 +1722,7 @@ Object Interpreter::num_leq(const Object& form,
(void)env;
T a = number<T>(args.unnamed[0]);
T b = number<T>(args.unnamed[1]);
return SymbolObject::make_new(reader.symbolTable, (a <= b) ? "#t" : "#f");
return true_or_false(a <= b);
}
Object Interpreter::eval_leq(const Object& form,
@@ -1525,7 +1750,7 @@ Object Interpreter::num_geq(const Object& form,
(void)env;
T a = number<T>(args.unnamed[0]);
T b = number<T>(args.unnamed[1]);
return SymbolObject::make_new(reader.symbolTable, (a >= b) ? "#t" : "#f");
return true_or_false(a >= b);
}
Object Interpreter::eval_geq(const Object& form,
@@ -1591,7 +1816,7 @@ Object Interpreter::eval_gensym(const Object& form,
const std::shared_ptr<EnvironmentObject>& env) {
(void)env;
vararg_check(form, args, {}, {});
return SymbolObject::make_new(reader.symbolTable, "gensym" + std::to_string(gensym_id++));
return Object::make_symbol(&reader.symbolTable, ("gensym" + std::to_string(gensym_id++)).c_str());
}
Object Interpreter::eval_cons(const Object& form,
@@ -1607,7 +1832,7 @@ Object Interpreter::eval_null(const Object& form,
const std::shared_ptr<EnvironmentObject>& env) {
(void)env;
vararg_check(form, args, {{}}, {});
return SymbolObject::make_new(reader.symbolTable, args.unnamed[0].is_empty_list() ? "#t" : "#f");
return true_or_false(args.unnamed[0].is_empty_list());
}
Object Interpreter::eval_type(const Object& form,
@@ -1616,15 +1841,15 @@ Object Interpreter::eval_type(const Object& form,
(void)env;
vararg_check(form, args, {{ObjectType::SYMBOL}, {}}, {});
auto kv = string_to_type.find(args.unnamed[0].as_symbol()->name);
auto kv = string_to_type.find(args.unnamed[0].as_symbol().name_ptr);
if (kv == string_to_type.end()) {
throw_eval_error(form, "invalid type given to type?");
}
if (args.unnamed[1].type == kv->second) {
return SymbolObject::make_new(reader.symbolTable, "#t");
return m_true_object;
} else {
return SymbolObject::make_new(reader.symbolTable, "#f");
return m_false_object;
}
}
@@ -1729,9 +1954,9 @@ Object Interpreter::eval_string_starts_with(const Object& form,
auto& suffix = args.unnamed.at(1).as_string()->data;
if (str_util::starts_with(str, suffix)) {
return SymbolObject::make_new(reader.symbolTable, "#t");
return m_true_object;
}
return SymbolObject::make_new(reader.symbolTable, "#f");
return m_false_object;
}
Object Interpreter::eval_string_ends_with(const Object& form,
@@ -1743,9 +1968,9 @@ Object Interpreter::eval_string_ends_with(const Object& form,
auto& suffix = args.unnamed.at(1).as_string()->data;
if (str_util::ends_with(str, suffix)) {
return SymbolObject::make_new(reader.symbolTable, "#t");
return m_true_object;
}
return SymbolObject::make_new(reader.symbolTable, "#f");
return m_false_object;
}
Object Interpreter::eval_string_split(const Object& form,
@@ -1792,14 +2017,21 @@ Object Interpreter::eval_symbol_to_string(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>&) {
vararg_check(form, args, {ObjectType::SYMBOL}, {});
return StringObject::make_new(args.unnamed.at(0).as_symbol()->name);
return StringObject::make_new(args.unnamed.at(0).as_symbol().name_ptr);
}
Object Interpreter::eval_string_to_symbol(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>&) {
vararg_check(form, args, {ObjectType::STRING}, {});
return SymbolObject::make_new(reader.symbolTable, args.unnamed.at(0).as_string()->data);
return Object::make_symbol(&reader.symbolTable, args.unnamed.at(0).as_string()->data.c_str());
}
Object Interpreter::eval_int_to_string(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>&) {
vararg_check(form, args, {ObjectType::INTEGER}, {});
return StringObject::make_new(std::to_string(args.unnamed.at(0).as_int()));
}
Object Interpreter::eval_get_env(const Object& form,
@@ -1839,7 +2071,7 @@ Object Interpreter::eval_hash_table_set(const Object& form,
vararg_check(form, args, {ObjectType::STRING_HASH_TABLE, {}, {}}, {});
const char* str = nullptr;
if (args.unnamed.at(1).is_symbol()) {
str = args.unnamed.at(1).as_symbol()->name.c_str();
str = args.unnamed.at(1).as_symbol().name_ptr;
} else if (args.unnamed.at(1).is_string()) {
str = args.unnamed.at(1).as_string()->data.c_str();
} else {
@@ -1861,7 +2093,7 @@ Object Interpreter::eval_hash_table_try_ref(const Object& form,
const char* str = nullptr;
if (args.unnamed.at(1).is_symbol()) {
str = args.unnamed.at(1).as_symbol()->name.c_str();
str = args.unnamed.at(1).as_symbol().name_ptr;
} else if (args.unnamed.at(1).is_string()) {
str = args.unnamed.at(1).as_string()->data.c_str();
} else {
@@ -1870,10 +2102,9 @@ Object Interpreter::eval_hash_table_try_ref(const Object& form,
const auto& it = table->data.find(str);
if (it == table->data.end()) {
// not in table
return PairObject::make_new(SymbolObject::make_new(reader.symbolTable, "#f"),
Object::make_empty_list());
return PairObject::make_new(m_false_object, Object::make_empty_list());
} else {
return PairObject::make_new(SymbolObject::make_new(reader.symbolTable, "#t"), it->second);
return PairObject::make_new(m_true_object, it->second);
}
}
} // namespace goos
+46 -13
View File
@@ -25,7 +25,7 @@ class Interpreter {
void set_global_variable_to_int(const std::string& name, int value);
Object eval(Object obj, const std::shared_ptr<EnvironmentObject>& env);
Object intern(const std::string& name);
HeapObject* intern_ptr(const std::string& name);
InternedSymbolPtr intern_ptr(const std::string& name);
void disable_printfs();
Object eval_symbol(const Object& sym, const std::shared_ptr<EnvironmentObject>& env);
bool eval_symbol(const Object& sym,
@@ -55,7 +55,7 @@ class Interpreter {
private:
friend class Goal;
void load_goos_library();
void define_var_in_env(Object& env, Object& var, const std::string& name);
void define_var_in_env(Object& env, const Object& var, const std::string& name);
void expect_env(const Object& form, const Object& o);
void vararg_check(
const Object& form,
@@ -226,6 +226,9 @@ class Interpreter {
Object eval_string_to_symbol(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_int_to_string(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_get_env(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
@@ -261,6 +264,16 @@ class Interpreter {
Object eval_while(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_let(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_let_star(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env);
Object eval_let_common(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env,
bool star);
Object eval_make_string_hash_table(const Object& form,
Arguments& args,
@@ -272,26 +285,46 @@ class Interpreter {
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env);
const Object& true_or_false(bool val);
void init_special_forms(
const std::unordered_map<std::string,
Object (Interpreter::*)(const Object&,
const Object&,
const std::shared_ptr<EnvironmentObject>&)>&
forms);
void init_builtin_forms(
const std::unordered_map<std::string,
Object (Interpreter::*)(const Object&,
Arguments&,
const std::shared_ptr<EnvironmentObject>&)>&
forms);
bool want_exit = false;
bool disable_printing = false;
std::unordered_map<std::string,
Object (Interpreter::*)(const Object& form,
Arguments& args,
const std::shared_ptr<EnvironmentObject>& env)>
std::unordered_map<
void*,
Object (Interpreter::*)(const Object&, Arguments&, const std::shared_ptr<EnvironmentObject>&)>
builtin_forms;
std::unordered_map<
std::string,
std::function<Object(const Object&, Arguments&, const std::shared_ptr<EnvironmentObject>&)>>
std::vector<std::pair<
void*,
std::function<Object(const Object&, Arguments&, const std::shared_ptr<EnvironmentObject>&)>>>
m_custom_forms;
std::unordered_map<std::string,
Object (Interpreter::*)(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env)>
std::vector<std::pair<void*,
Object (Interpreter::*)(const Object& form,
const Object& rest,
const std::shared_ptr<EnvironmentObject>& env)>>
special_forms;
int64_t gensym_id = 0;
std::unordered_map<std::string, ObjectType> string_to_type;
const char* m_false_sym = nullptr;
const char* m_true_sym = nullptr;
Object m_false_object, m_true_object;
};
} // namespace goos
+83 -10
View File
@@ -44,12 +44,89 @@
#include <cstring>
#include "common/util/FileUtil.h"
#include "common/util/crc32.h"
#include "common/util/print_float.h"
#include "third-party/fmt/core.h"
namespace goos {
SymbolTable::SymbolTable() {
m_power_of_two_size = 1; // 2 ^ 1 = 2
m_entries.resize(2);
m_used_entries = 0;
m_next_resize = (m_entries.size() * kMaxUsed);
m_mask = 0b1;
}
SymbolTable::~SymbolTable() {
for (auto& e : m_entries) {
delete[] e.name;
}
}
InternedSymbolPtr SymbolTable::intern(const char* str) {
size_t string_len = strlen(str);
u32 hash = crc32((const u8*)str, string_len);
// probe
for (u32 i = 0; i < m_entries.size(); i++) {
u32 slot_addr = (hash + i) & m_mask;
auto& slot = m_entries[slot_addr];
if (!slot.name) {
// not found, insert!
slot.hash = hash;
auto* name = new char[string_len + 1];
memcpy(name, str, string_len + 1);
slot.name = name;
m_used_entries++;
if (m_used_entries >= m_next_resize) {
resize();
return intern(str);
}
return {name};
} else {
if (slot.hash != hash) {
continue; // bad hash
}
if (strcmp(slot.name, str) != 0) {
continue; // bad name
}
return {slot.name};
}
}
// should be impossible to reach.
ASSERT_NOT_REACHED();
}
void SymbolTable::resize() {
m_power_of_two_size++;
m_mask = (1U << m_power_of_two_size) - 1;
std::vector<Entry> new_entries(m_entries.size() * 2);
for (const auto& old_entry : m_entries) {
if (old_entry.name) {
bool done = false;
for (u32 i = 0; i < new_entries.size(); i++) {
u32 slot_addr = (old_entry.hash + i) & m_mask;
auto& slot = new_entries[slot_addr];
if (!slot.name) {
slot.name = old_entry.name;
slot.hash = old_entry.hash;
done = true;
break;
}
}
ASSERT(done);
}
}
m_entries = std::move(new_entries);
m_next_resize = kMaxUsed * m_entries.size();
}
/*!
* Convert type to string (name in brackets)
*/
@@ -137,14 +214,9 @@ std::string fixed_to_string(char x) {
return {buff};
}
/*!
* Create a new symbol object by interning
*/
Object SymbolObject::make_new(SymbolTable& st, const std::string& name) {
Object obj;
obj.type = ObjectType::SYMBOL;
obj.heap_obj = st.intern(name);
return obj;
template <>
std::string fixed_to_string(InternedSymbolPtr x) {
return x.name_ptr;
}
/*!
@@ -238,8 +310,9 @@ bool Object::operator==(const Object& other) const {
return float_obj == other.float_obj;
case ObjectType::CHAR:
return char_obj == other.char_obj;
case ObjectType::SYMBOL:
return symbol_obj == other.symbol_obj;
case ObjectType::ENVIRONMENT:
case ObjectType::LAMBDA:
case ObjectType::MACRO:
@@ -271,7 +344,7 @@ bool Object::operator==(const Object& other) const {
}
bool Object::is_symbol(const std::string& name) const {
return is_symbol() && as_symbol()->name == name;
return is_symbol() && name == as_symbol().name_ptr;
}
bool Object::is_string(const std::string& val) const {
+105 -72
View File
@@ -43,6 +43,7 @@
*
*/
#include <cstring>
#include <map>
#include <memory>
#include <stdexcept>
@@ -57,9 +58,6 @@
namespace goos {
using FloatType = double;
using IntType = s64;
/*!
* All objects have one of these kinds
*/
@@ -71,9 +69,9 @@ enum class ObjectType : u8 {
INTEGER,
FLOAT,
CHAR,
SYMBOL,
// allocated
SYMBOL,
STRING,
PAIR,
ARRAY,
@@ -87,6 +85,29 @@ enum class ObjectType : u8 {
std::string object_type_to_string(ObjectType kind);
// Some objects are "fixed", meaning they are stored inline in the Object, and not heap allocated.
// These use value semantics and are copied on object assignment.
// For int/float, these are simply double/int64_t
using FloatType = double;
using IntType = s64;
// For symbols, we simply store a pointer to the symbol's name string.
// this is wrapped in a type just to avoid confusion with normal const char*.
struct InternedSymbolPtr {
const char* name_ptr;
struct hash {
auto operator()(const InternedSymbolPtr& x) const {
return std::hash<const void*>()((const void*)x.name_ptr);
}
};
bool operator==(const char* msg) const { return strcmp(msg, name_ptr) == 0; }
bool operator!=(const char* msg) const { return strcmp(msg, name_ptr) != 0; }
bool operator==(const std::string& str) const { return str == name_ptr; }
bool operator!=(const std::string& str) const { return str != name_ptr; }
bool operator==(const InternedSymbolPtr& other) const { return other.name_ptr == name_ptr; }
bool operator!=(const InternedSymbolPtr& other) const { return other.name_ptr != name_ptr; }
};
/*!
* By default, convert a fixed object data to string with std::to_string.
@@ -115,6 +136,31 @@ std::string fixed_to_string<char>(char);
template <>
std::string fixed_to_string<IntType>(IntType);
template <>
std::string fixed_to_string<InternedSymbolPtr>(InternedSymbolPtr);
class SymbolTable {
public:
SymbolTable(const SymbolTable&) = delete;
SymbolTable& operator=(const SymbolTable&) = delete;
SymbolTable();
~SymbolTable();
InternedSymbolPtr intern(const char* str);
private:
void resize();
int m_power_of_two_size = 0;
struct Entry {
u32 hash = 0;
const char* name = nullptr;
};
std::vector<Entry> m_entries;
int m_used_entries = 0;
int m_next_resize = 0;
u32 m_mask = 0;
static constexpr float kMaxUsed = 0.7;
};
/*!
* Common implementation for a fixed object
*/
@@ -139,6 +185,8 @@ class FixedObject {
return object_type_to_string(ObjectType::INTEGER);
if (std::is_same<T, char>())
return object_type_to_string(ObjectType::CHAR);
if (std::is_same<T, InternedSymbolPtr>())
return object_type_to_string(ObjectType::SYMBOL);
throw std::runtime_error("Unsupported FixedObject type");
}
};
@@ -149,6 +197,7 @@ class FixedObject {
using IntegerObject = FixedObject<int64_t>;
using FloatObject = FixedObject<double>;
using CharObject = FixedObject<char>;
using SymbolObject = FixedObject<InternedSymbolPtr>;
// Other objects are separate allocated on the heap. These objects should be HeapObjects.
@@ -162,7 +211,6 @@ class HeapObject {
// forward declare all HeapObjects
class PairObject;
class EnvironmentObject;
class SymbolObject;
class StringObject;
class LambdaObject;
class MacroObject;
@@ -180,6 +228,7 @@ class Object {
IntegerObject integer_obj;
FloatObject float_obj;
CharObject char_obj;
SymbolObject symbol_obj;
};
ObjectType type = ObjectType::INVALID;
@@ -192,6 +241,8 @@ class Object {
return float_obj.print();
case ObjectType::CHAR:
return char_obj.print();
case ObjectType::SYMBOL:
return symbol_obj.print();
case ObjectType::EMPTY_LIST:
return "()";
default:
@@ -207,6 +258,8 @@ class Object {
return float_obj.inspect();
case ObjectType::CHAR:
return char_obj.inspect();
case ObjectType::SYMBOL:
return symbol_obj.inspect();
case ObjectType::EMPTY_LIST:
return "[empty list] ()\n";
default:
@@ -244,16 +297,30 @@ class Object {
return o;
}
static Object make_symbol(SymbolTable* table, const char* name) {
Object o;
o.type = ObjectType::SYMBOL;
o.symbol_obj.value = table->intern(name);
return o;
}
PairObject* as_pair() const;
EnvironmentObject* as_env() const;
std::shared_ptr<EnvironmentObject> as_env_ptr() const;
SymbolObject* as_symbol() const;
StringObject* as_string() const;
LambdaObject* as_lambda() const;
MacroObject* as_macro() const;
ArrayObject* as_array() const;
StringHashTableObject* as_string_hash_table() const;
const InternedSymbolPtr& as_symbol() const {
if (type != ObjectType::SYMBOL) {
throw std::runtime_error("as_symbol called on a " + object_type_to_string(type) + " " +
print());
}
return symbol_obj.value;
}
IntType& as_int() {
if (type != ObjectType::INTEGER) {
throw std::runtime_error("as_int called on a " + object_type_to_string(type) + " " + print());
@@ -315,56 +382,6 @@ class Object {
bool operator!=(const Object& other) const { return !((*this) == other); }
};
class SymbolTable;
/*!
* A Symbol Object, which is just a wrapper around a string.
* The make_new function will correctly
*/
class SymbolObject : public HeapObject {
public:
std::string name;
explicit SymbolObject(std::string _name) : name(std::move(_name)) {}
static Object make_new(SymbolTable& st, const std::string& name);
std::string print() const override { return name; }
std::string inspect() const override { return "[symbol] " + name + "\n"; }
~SymbolObject() = default;
};
/*!
* A Symbol Table, which holds all symbols.
*/
class SymbolTable {
public:
std::shared_ptr<HeapObject> intern(const std::string& name) {
const auto& kv = table.find(name);
if (kv == table.end()) {
auto iter = table.insert({name, std::make_shared<SymbolObject>(name)});
return (*iter.first).second;
} else {
return kv->second;
}
}
HeapObject* intern_ptr(const std::string& name) {
const auto& kv = table.find(name);
if (kv == table.end()) {
auto iter = table.insert({name, std::make_shared<SymbolObject>(name)});
return (*iter.first).second.get();
} else {
return kv->second.get();
}
}
~SymbolTable() = default;
private:
std::unordered_map<std::string, std::shared_ptr<HeapObject>> table;
};
class StringObject : public HeapObject {
public:
std::string data;
@@ -439,14 +456,41 @@ class PairObject : public HeapObject {
~PairObject() = default;
};
class EnvironmentMap {
public:
EnvironmentMap(const EnvironmentMap&) = delete;
EnvironmentMap& operator=(const EnvironmentMap&) = delete;
EnvironmentMap();
Object* lookup(InternedSymbolPtr ptr);
void set(InternedSymbolPtr ptr, const Object& obj);
void clear();
private:
struct Entry {
const char* key = nullptr;
Object value;
};
std::vector<Entry> m_entries;
void resize();
int m_power_of_two_size = 0;
int m_used_entries = 0;
int m_next_resize = 0;
u32 m_mask = 0;
static constexpr float kMaxUsed = 0.7;
};
class EnvironmentObject : public HeapObject {
public:
std::string name;
std::shared_ptr<EnvironmentObject> parent_env;
// the symbols will be stored in the symbol table and never removed, so we don't need shared
// pointers here.
std::unordered_map<HeapObject*, Object> vars;
// note: this is keyed on the address in the symbol table.
EnvironmentMap vars;
// this find work by any name string
Object* find(const char* n, SymbolTable* st) { return vars.lookup(st->intern(n)); }
Object* find(InternedSymbolPtr ptr) { return vars.lookup(ptr); }
EnvironmentObject() = default;
@@ -478,11 +522,8 @@ class EnvironmentObject : public HeapObject {
std::string inspect() const override {
std::string result = "[environment]\n name: " + name +
"\n parent: " + (parent_env ? parent_env->print() : "NONE") +
"\n vars:\n";
for (const auto& kv : vars) {
result += " " + kv.first->print() + ": " + kv.second.print() + "\n";
}
"\n parent: " + (parent_env ? parent_env->print() : "NONE") + "\n";
return result;
}
};
@@ -662,14 +703,6 @@ inline std::shared_ptr<EnvironmentObject> Object::as_env_ptr() const {
return std::dynamic_pointer_cast<EnvironmentObject>(heap_obj);
}
inline SymbolObject* Object::as_symbol() const {
if (type != ObjectType::SYMBOL) {
throw std::runtime_error("as_symbol called on a " + object_type_to_string(type) + " " +
print());
}
return static_cast<SymbolObject*>(heap_obj.get());
}
inline StringObject* Object::as_string() const {
if (type != ObjectType::STRING) {
throw std::runtime_error("as_string called on a " + object_type_to_string(type) + " " +
+3 -3
View File
@@ -12,12 +12,12 @@ bool get_va(const goos::Object& rest, std::string* err_string, goos::Arguments*
auto arg = current.as_pair()->car;
// did we get a ":keyword"
if (arg.is_symbol() && arg.as_symbol()->name.at(0) == ':') {
auto key_name = arg.as_symbol()->name.substr(1);
if (arg.is_symbol() && arg.as_symbol().name_ptr[0] == ':') {
auto key_name = arg.as_symbol().name_ptr + 1;
// check for multiple definition of key
if (args.named.find(key_name) != args.named.end()) {
*err_string = "Key argument " + key_name + " multiply defined";
*err_string = fmt::format("Key argument {} multiply defined", key_name);
return false;
}
+1 -1
View File
@@ -98,7 +98,7 @@ void add_to_token_list(const goos::Object& obj, std::vector<FormToken>* tokens)
// look for a (quote x) to print as 'x
auto& first = obj.as_pair()->car;
if (first.is_symbol() && first.as_symbol()->name == "quote") {
if (first.is_symbol() && first.as_symbol() == "quote") {
auto& second = obj.as_pair()->cdr;
if (second.is_pair() && second.as_pair()->cdr.is_empty_list()) {
tokens->emplace_back(FormToken::TokenKind::QUOTE);
+5 -1
View File
@@ -288,7 +288,11 @@ void break_list(Node* node) {
node->sub_elt_indent += name.size();
} else if (name == "defmethod") {
// things with 4 things in the top line: (defmethod <method> <type> <args>
node->top_line_count = 4;
// or just 3 things in the top line: (defmethod <method> <args>
node->top_line_count = 3;
if (node->child_nodes.size() >= 4 && node->child_nodes[2].kind == Node::Kind::ATOM) {
node->top_line_count = 4;
}
} else if (name == "until" || name == "while" || name == "dotimes" || name == "countdown" ||
name == "when" || name == "behavior" || name == "lambda" || name == "defpart" ||
name == "define") {
+1 -1
View File
@@ -49,7 +49,7 @@ goos::Reader& get_pretty_printer_reader() {
goos::Object to_symbol(const std::string& str) {
std::lock_guard<std::mutex> guard(pretty_printer_reader_mutex);
return goos::SymbolObject::make_new(get_pretty_printer_reader().symbolTable, str);
return goos::Object::make_symbol(&get_pretty_printer_reader().symbolTable, str.c_str());
}
goos::Object new_string(const std::string& str) {
+57 -16
View File
@@ -286,7 +286,7 @@ Object Reader::internal_read(std::shared_ptr<SourceText> text,
try {
auto objs = read_list(ts, false);
if (add_top_level) {
return PairObject::make_new(SymbolObject::make_new(symbolTable, "top-level"), objs);
return PairObject::make_new(Object::make_symbol(&symbolTable, "top-level"), objs);
} else {
return objs;
}
@@ -459,12 +459,51 @@ bool Reader::read_array(TextStream& stream, Object& o) {
return true;
}
struct ListBuilder {
Object head;
std::shared_ptr<PairObject> prev_tail;
std::shared_ptr<PairObject> tail;
int size = 0;
ListBuilder() { head = Object::make_empty_list(); }
void push_back(Object&& o) {
size++;
if (!tail) {
tail = std::make_shared<PairObject>(o, Object{});
head.type = ObjectType::PAIR;
head.heap_obj = tail;
} else {
auto next = std::make_shared<PairObject>(o, Object{});
tail->cdr.type = ObjectType::PAIR;
tail->cdr.heap_obj = next;
prev_tail = std::move(tail);
tail = std::move(next);
}
}
Object pop_back() {
auto obj = tail->car;
tail = std::move(prev_tail);
return obj;
}
void finalize() {
if (tail) {
tail->cdr = Object::make_empty_list();
} else {
head = Object::make_empty_list();
}
}
};
/*!
* Call this on the character after the open paren.
*/
Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
ts.seek_past_whitespace_and_comments();
std::vector<Object> objects;
// std::vector<Object> objects;
ListBuilder list_builder;
bool got_close_paren = false; // does this list end?
bool got_dot = false; // did we get a . ?
@@ -506,22 +545,23 @@ Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
}
// inserter function, used to properly insert a next object
auto insert_object = [&](const Object& o) {
auto insert_object = [&](Object&& o) {
if (got_thing_after_dot) {
throw_reader_error(ts, "A list cannot have multiple entries after the dot", -1);
}
if (reader_macro_string_stack.empty()) {
objects.push_back(o);
list_builder.push_back(std::move(o));
// objects.push_back(o);
} else {
Object to_push_back = o;
Object to_push_back = std::move(o);
while (!reader_macro_string_stack.empty()) {
to_push_back =
build_list({SymbolObject::make_new(symbolTable, reader_macro_string_stack.back()),
to_push_back});
to_push_back = build_list(
{Object::make_symbol(&symbolTable, reader_macro_string_stack.back().c_str()),
to_push_back});
reader_macro_string_stack.pop_back();
}
objects.push_back(to_push_back);
list_builder.push_back(std::move(to_push_back));
}
// remember if we got an object after the dot
@@ -551,7 +591,7 @@ Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
if (read_object(tok, ts, obj)) {
ts.seek_past_whitespace_and_comments();
insert_object(obj);
insert_object(std::move(obj));
} else {
throw_reader_error(ts, "invalid token encountered in reader: " + tok.text,
-int(tok.text.size()));
@@ -574,12 +614,12 @@ Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
// build up list or improper list, link it, and return!
if (got_thing_after_dot) {
if (objects.size() < 2) {
if (list_builder.size < 2) {
throw_reader_error(ts, "A list with a dot must have at least one thing before the dot", -1);
}
auto back = objects.back();
objects.pop_back();
auto rv = build_list(objects);
auto back = list_builder.pop_back();
list_builder.finalize();
auto rv = list_builder.head;
auto lst = rv;
while (true) {
@@ -593,7 +633,8 @@ Object Reader::read_list(TextStream& ts, bool expect_close_paren) {
db.link(rv, ts.text, start_offset);
return rv;
} else {
auto rv = build_list(std::move(objects));
list_builder.finalize();
auto rv = list_builder.head;
db.link(rv, ts.text, start_offset);
return rv;
}
@@ -607,7 +648,7 @@ bool Reader::try_token_as_symbol(const Token& tok, Object& obj) {
ASSERT(!tok.text.empty());
char start = tok.text[0];
if (m_valid_symbols_chars[(int)start]) {
obj = SymbolObject::make_new(symbolTable, tok.text);
obj = Object::make_symbol(&symbolTable, tok.text.c_str());
return true;
} else {
return false;
+4 -2
View File
@@ -147,13 +147,15 @@ void set_file(const std::string& filename,
existing_log_files = file_util::sort_filepaths(existing_log_files, true);
if (existing_log_files.size() > (LOG_ROTATE_MAX - 1)) {
lg::info("removing {} log files", existing_log_files.size() - (LOG_ROTATE_MAX - 1));
for (int i = 0; i < existing_log_files.size() - (LOG_ROTATE_MAX - 1); i++) {
for (int i = 0; i < (int)existing_log_files.size() - (LOG_ROTATE_MAX - 1); i++) {
lg::info("removing {}", existing_log_files.at(i).string());
fs::remove(existing_log_files.at(i));
}
}
} else {
complete_filename += ".log";
if (!str_util::ends_with(complete_filename, ".log")) {
complete_filename += ".log";
}
}
if (append) {
+22
View File
@@ -18,6 +18,12 @@ class Vector {
return result;
}
static Vector<T, Size> unit(int idx) {
Vector<T, Size> result = Vector<T, Size>::zero();
result[idx] = T(1);
return result;
}
template <typename... Args>
constexpr Vector(Args... args) : m_data{T(args)...} {
static_assert(sizeof...(args) == Size, "Incorrect number of args");
@@ -226,6 +232,22 @@ class Vector {
}
}
Vector<T, Size> min(const Vector<T, Size>& other) const {
Vector<T, Size> result;
for (int i = 0; i < Size; i++) {
result[i] = std::min(m_data[i], other[i]);
}
return result;
}
Vector<T, Size> max(const Vector<T, Size>& other) const {
Vector<T, Size> result;
for (int i = 0; i < Size; i++) {
result[i] = std::max(m_data[i], other[i]);
}
return result;
}
std::string to_string_aligned() const {
std::string result = "[";
for (auto x : m_data) {
+74
View File
@@ -50,4 +50,78 @@ math::Vector4f bsphere_of_triangle(const Vector3f* vertices);
inline bool point_in_bsphere(const Vector4f& sphere, const Vector3f& pt) {
return (sphere.xyz() - pt).squared_length() <= (sphere.w() * sphere.w());
}
template <typename T>
math::Matrix<T, 4, 4> affine_inverse(const math::Matrix<T, 4, 4>& in) {
math::Matrix<T, 4, 4> result;
// transpose rotation
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
result(i, j) = in(j, i);
}
}
result(3, 0) = 0;
result(3, 1) = 0;
result(3, 2) = 0;
result(3, 3) = 1;
result(0, 3) = 0;
result(1, 3) = 0;
result(2, 3) = 0;
for (int rx = 0; rx < 3; rx++) {
for (int cx = 0; cx < 3; cx++) {
result(rx, 3) -= result(rx, cx) * in(cx, 3);
}
}
return result;
}
template <typename T>
math::Matrix<T, 4, 4> inverse(const math::Matrix<T, 4, 4>& m) {
math::Matrix<T, 4, 4> im;
T A2323 = m(2, 2) * m(3, 3) - m(2, 3) * m(3, 2);
T A1323 = m(2, 1) * m(3, 3) - m(2, 3) * m(3, 1);
T A1223 = m(2, 1) * m(3, 2) - m(2, 2) * m(3, 1);
T A0323 = m(2, 0) * m(3, 3) - m(2, 3) * m(3, 0);
T A0223 = m(2, 0) * m(3, 2) - m(2, 2) * m(3, 0);
T A0123 = m(2, 0) * m(3, 1) - m(2, 1) * m(3, 0);
T A2313 = m(1, 2) * m(3, 3) - m(1, 3) * m(3, 2);
T A1313 = m(1, 1) * m(3, 3) - m(1, 3) * m(3, 1);
T A1213 = m(1, 1) * m(3, 2) - m(1, 2) * m(3, 1);
T A2312 = m(1, 2) * m(2, 3) - m(1, 3) * m(2, 2);
T A1312 = m(1, 1) * m(2, 3) - m(1, 3) * m(2, 1);
T A1212 = m(1, 1) * m(2, 2) - m(1, 2) * m(2, 1);
T A0313 = m(1, 0) * m(3, 3) - m(1, 3) * m(3, 0);
T A0213 = m(1, 0) * m(3, 2) - m(1, 2) * m(3, 0);
T A0312 = m(1, 0) * m(2, 3) - m(1, 3) * m(2, 0);
T A0212 = m(1, 0) * m(2, 2) - m(1, 2) * m(2, 0);
T A0113 = m(1, 0) * m(3, 1) - m(1, 1) * m(3, 0);
T A0112 = m(1, 0) * m(2, 1) - m(1, 1) * m(2, 0);
T det = m(0, 0) * (m(1, 1) * A2323 - m(1, 2) * A1323 + m(1, 3) * A1223) -
m(0, 1) * (m(1, 0) * A2323 - m(1, 2) * A0323 + m(1, 3) * A0223) +
m(0, 2) * (m(1, 0) * A1323 - m(1, 1) * A0323 + m(1, 3) * A0123) -
m(0, 3) * (m(1, 0) * A1223 - m(1, 1) * A0223 + m(1, 2) * A0123);
det = 1 / det;
im(0, 0) = det * (m(1, 1) * A2323 - m(1, 2) * A1323 + m(1, 3) * A1223);
im(0, 1) = det * -(m(0, 1) * A2323 - m(0, 2) * A1323 + m(0, 3) * A1223);
im(0, 2) = det * (m(0, 1) * A2313 - m(0, 2) * A1313 + m(0, 3) * A1213);
im(0, 3) = det * -(m(0, 1) * A2312 - m(0, 2) * A1312 + m(0, 3) * A1212);
im(1, 0) = det * -(m(1, 0) * A2323 - m(1, 2) * A0323 + m(1, 3) * A0223);
im(1, 1) = det * (m(0, 0) * A2323 - m(0, 2) * A0323 + m(0, 3) * A0223);
im(1, 2) = det * -(m(0, 0) * A2313 - m(0, 2) * A0313 + m(0, 3) * A0213);
im(1, 3) = det * (m(0, 0) * A2312 - m(0, 2) * A0312 + m(0, 3) * A0212);
im(2, 0) = det * (m(1, 0) * A1323 - m(1, 1) * A0323 + m(1, 3) * A0123);
im(2, 1) = det * -(m(0, 0) * A1323 - m(0, 1) * A0323 + m(0, 3) * A0123);
im(2, 2) = det * (m(0, 0) * A1313 - m(0, 1) * A0313 + m(0, 3) * A0113);
im(2, 3) = det * -(m(0, 0) * A1312 - m(0, 1) * A0312 + m(0, 3) * A0112);
im(3, 0) = det * -(m(1, 0) * A1223 - m(1, 1) * A0223 + m(1, 2) * A0123);
im(3, 1) = det * (m(0, 0) * A1223 - m(0, 1) * A0223 + m(0, 2) * A0123);
im(3, 2) = det * -(m(0, 0) * A1213 - m(0, 1) * A0213 + m(0, 2) * A0113);
im(3, 3) = det * (m(0, 0) * A1212 - m(0, 1) * A0212 + m(0, 2) * A0112);
return im;
}
} // namespace math
+1 -1
View File
@@ -207,7 +207,7 @@ std::string find_repl_username() {
}
}
return "#f";
return "unknown";
}
fs::path get_startup_file_path(const std::string& username, const GameVersion game_version) {
+4 -4
View File
@@ -13,20 +13,20 @@ void open_subtitle_project(const std::string& project_kind,
goos::Reader reader;
auto& proj = reader.read_from_file({file_path}).as_pair()->cdr.as_pair()->car;
if (!proj.is_pair() || !proj.as_pair()->car.is_symbol() ||
proj.as_pair()->car.as_symbol()->name != project_kind) {
proj.as_pair()->car.as_symbol() != project_kind) {
throw std::runtime_error(fmt::format("invalid project '{}'", project_kind));
}
goos::for_each_in_list(proj.as_pair()->cdr, [&](const goos::Object& o) {
if (o.is_pair() && o.as_pair()->cdr.is_pair()) {
auto args = o.as_pair();
auto& action = args->car.as_symbol()->name;
auto& action = args->car.as_symbol();
args = args->cdr.as_pair();
if (action == "file-json") {
auto new_file = GameSubtitleDefinitionFile();
while (true) {
const auto& kwarg = args->car.as_symbol()->name;
const auto& kwarg = args->car.as_symbol();
args = args->cdr.as_pair();
if (kwarg == ":language-id") {
new_file.language_id = args->car.as_int();
@@ -49,7 +49,7 @@ void open_subtitle_project(const std::string& project_kind,
subtitle_files.push_back(new_file);
} else {
throw std::runtime_error(
fmt::format("unknown action {} in {} project", action, project_kind));
fmt::format("unknown action {} in {} project", action.name_ptr, project_kind));
}
} else {
throw std::runtime_error(fmt::format("invalid entry in {} project", project_kind));
+8 -7
View File
@@ -147,7 +147,7 @@ void parse_text_goal(const goos::Object& data,
throw std::runtime_error("invalid text version entry");
}
font = get_font_bank(ver_name.as_symbol()->name);
font = get_font_bank(ver_name.as_symbol().name_ptr);
}
else if (head.is_int()) {
@@ -278,7 +278,7 @@ GameTextVersion parse_text_only_version(const goos::Object& data) {
throw std::runtime_error("invalid text version entry");
}
font = get_font_bank(ver_name.as_symbol()->name);
font = get_font_bank(ver_name.as_symbol().name_ptr);
}
}
});
@@ -294,14 +294,14 @@ void open_text_project(const std::string& kind,
goos::Reader reader;
auto& proj = reader.read_from_file({filename}).as_pair()->cdr.as_pair()->car;
if (!proj.is_pair() || !proj.as_pair()->car.is_symbol() ||
proj.as_pair()->car.as_symbol()->name != kind) {
proj.as_pair()->car.as_symbol() != kind) {
throw std::runtime_error(fmt::format("invalid {} project", kind));
}
goos::for_each_in_list(proj.as_pair()->cdr, [&](const goos::Object& o) {
if (o.is_pair() && o.as_pair()->cdr.is_pair()) {
auto args = o.as_pair();
auto& action = args->car.as_symbol()->name;
auto& action = args->car.as_symbol();
args = args->cdr.as_pair();
if (action == "file") {
@@ -313,17 +313,18 @@ void open_text_project(const std::string& kind,
} else if (action == "file-json") {
auto& language_id = args->car.as_int();
args = args->cdr.as_pair();
auto& text_version = args->car.as_symbol()->name;
auto& text_version = args->car.as_symbol();
args = args->cdr.as_pair();
std::optional<std::string> group_name = std::nullopt;
group_name = args->car.as_string()->data;
args = args->cdr.as_pair()->car.as_pair();
goos::for_each_in_list(args->cdr.as_pair()->car, [&](const goos::Object& o) {
text_files.push_back({GameTextDefinitionFile::Format::JSON, o.as_string()->data,
(int)language_id, text_version, group_name});
(int)language_id, text_version.name_ptr, group_name});
});
} else {
throw std::runtime_error(fmt::format("unknown action {} in {} project", action, kind));
throw std::runtime_error(
fmt::format("unknown action {} in {} project", action.name_ptr, kind));
}
} else {
throw std::runtime_error(fmt::format("invalid entry in {} project", kind));
+2
View File
@@ -159,6 +159,7 @@ constexpr int true_symbol_offset(GameVersion version) {
case GameVersion::Jak1:
return jak1_symbols::FIX_SYM_TRUE;
case GameVersion::Jak2:
case GameVersion::Jak3:
return jak2_symbols::FIX_SYM_TRUE;
}
}
@@ -168,6 +169,7 @@ constexpr int empty_pair_offset_from_s7(GameVersion version) {
case GameVersion::Jak1:
return jak1_symbols::FIX_SYM_EMPTY_PAIR;
case GameVersion::Jak2:
case GameVersion::Jak3:
// minus 1 for the symbol table pointer's offset.
return jak2_symbols::S7_OFF_FIX_SYM_EMPTY_PAIR;
}
+7 -1
View File
@@ -42,8 +42,14 @@ std::vector<std::string> jak2_slots = {
"cas-toxic-slime-scroll-dest",
};
}
std::vector<std::string> jak3_slots = {};
} // namespace
const std::vector<std::string>& jak2_animated_texture_slots() {
return jak2_slots;
}
const std::vector<std::string>& jak3_animated_texture_slots() {
return jak3_slots;
}
+1
View File
@@ -4,3 +4,4 @@
#include <vector>
const std::vector<std::string>& jak2_animated_texture_slots();
const std::vector<std::string>& jak3_animated_texture_slots();
+6 -4
View File
@@ -421,10 +421,12 @@ int Type::get_num_methods() const {
* Add a method defined specifically for this type.
*/
const MethodInfo& Type::add_method(const MethodInfo& info) {
for (auto it = m_methods.rbegin(); it != m_methods.rend(); it++) {
if (!it->overrides_parent && !it->only_overrides_docstring) {
ASSERT(it->id + 1 == info.id);
break;
if (!info.overrides_parent) {
for (auto it = m_methods.rbegin(); it != m_methods.rend(); it++) {
if (!it->overrides_parent && !it->only_overrides_docstring) {
ASSERT(it->id + 1 == info.id);
break;
}
}
}
+5
View File
@@ -32,6 +32,7 @@ struct MethodInfo {
bool overrides_parent = false;
bool only_overrides_docstring = false;
std::optional<std::string> docstring;
std::optional<std::string> overlay_name;
bool operator==(const MethodInfo& other) const;
bool operator!=(const MethodInfo& other) const { return !((*this) == other); }
@@ -238,6 +239,9 @@ class Field {
int offset() const { return m_offset; }
bool skip_in_decomp() const { return m_skip_in_static_decomp; }
bool user_placed() const { return m_placed_by_user; }
void set_comment(const std::string& comment) { m_comment = comment; }
const std::string& comment() const { return m_comment; }
bool has_comment() const { return !m_comment.empty(); }
bool operator==(const Field& other) const;
bool operator!=(const Field& other) const { return !((*this) == other); }
std::string diff(const Field& other) const;
@@ -274,6 +278,7 @@ class Field {
int m_alignment = -1;
bool m_skip_in_static_decomp = false;
bool m_placed_by_user = false; // was this field placed manually by the user?
std::string m_comment; // optional comment placed next to the field
double m_field_score = 0.;
+313 -74
View File
@@ -451,6 +451,25 @@ Type* TypeSystem::lookup_type(const TypeSpec& ts) const {
return lookup_type(ts.base_type());
}
/*!
* Same as lookup_type, but returns null instead of throwing.
*/
Type* TypeSystem::lookup_type_no_throw(const std::string& name) const {
auto kv = m_types.find(name);
if (kv != m_types.end()) {
return kv->second.get();
}
return nullptr;
}
/*!
* Same as lookup_type, but returns null instead of throwing.
*/
Type* TypeSystem::lookup_type_no_throw(const TypeSpec& ts) const {
return lookup_type_no_throw(ts.base_type());
}
/*!
* Get type info. If the type is not fully defined (ie, we are parsing its deftype now) and its
* forward defined as a basic or structure, just get basic/structure.
@@ -516,18 +535,24 @@ int TypeSystem::get_load_size_allow_partial_def(const TypeSpec& ts) const {
}
MethodInfo TypeSystem::override_method(Type* type,
const std::string& /*type_name*/,
const int method_id,
const std::string& method_name,
const std::optional<std::string>& docstring) {
// Lookup the method from the parent type
MethodInfo existing_info;
bool exists = try_lookup_method(type->get_parent(), method_id, &existing_info);
bool exists = try_lookup_method(type->get_parent(), method_name, &existing_info);
if (!exists) {
throw_typesystem_error("Trying to use override a method that has no parent declaration");
throw_typesystem_error("Trying to override a method that has no parent declaration");
}
// use the existing ID.
return type->add_method({existing_info.id, existing_info.name, existing_info.type,
type->get_name(), existing_info.no_virtual, false, true, docstring});
return type->add_method({existing_info.id,
existing_info.name,
existing_info.type,
type->get_name(),
existing_info.no_virtual,
false,
true,
docstring,
{}});
}
MethodInfo TypeSystem::declare_method(const std::string& type_name,
@@ -554,8 +579,7 @@ MethodInfo TypeSystem::declare_method(Type* type,
const std::optional<std::string>& docstring,
bool no_virtual,
const TypeSpec& ts,
bool override_type,
int id) {
bool override_type) {
if (method_name == "new") {
if (override_type) {
throw_typesystem_error("Cannot use :replace option with a new method.");
@@ -569,7 +593,7 @@ MethodInfo TypeSystem::declare_method(Type* type,
if (override_type) {
if (!got_existing) {
if (id != -1 && try_lookup_method(type->get_parent(), id, &existing_info)) {
if (try_lookup_method(type->get_parent(), method_name, &existing_info)) {
} else {
throw_typesystem_error(
"Cannot use :replace on method {} of {} because this method was not previously "
@@ -579,8 +603,15 @@ MethodInfo TypeSystem::declare_method(Type* type,
}
// use the existing ID.
return type->add_method(
{existing_info.id, method_name, ts, type->get_name(), no_virtual, true, false, docstring});
return type->add_method({existing_info.id,
method_name,
ts,
type->get_name(),
no_virtual,
true,
false,
docstring,
{}});
} else {
if (got_existing) {
// make sure we aren't changing anything.
@@ -610,12 +641,47 @@ MethodInfo TypeSystem::declare_method(Type* type,
return existing_info;
} else {
// add a new method!
return type->add_method({get_next_method_id(type), method_name, ts, type->get_name(),
no_virtual, false, false, docstring});
return type->add_method({get_next_method_id(type),
method_name,
ts,
type->get_name(),
no_virtual,
false,
false,
docstring,
{}});
}
}
}
/*!
* Adds a new method that is overlayed on top of a different, existing method.
* This should be used basically never (happens once in Jak 1).
*/
MethodInfo TypeSystem::overlay_method(Type* type,
const std::string& method_name,
const std::string& method_overlay_name,
const std::optional<std::string>& docstring,
const TypeSpec& ts) {
// look up the method
MethodInfo existing_info;
bool got_existing = try_lookup_method(type, method_overlay_name, &existing_info);
if (!got_existing) {
if (try_lookup_method(type->get_parent(), method_overlay_name, &existing_info)) {
} else {
throw_typesystem_error(
"Cannot use :overlay-at on method {} of {} because this method was not previously "
"declared in a parent.",
method_overlay_name, type->get_name());
}
}
// use the existing ID.
return type->add_method({existing_info.id, method_name, ts, type->get_name(), false, true, false,
docstring, std::make_optional(method_overlay_name)});
}
MethodInfo TypeSystem::define_method(const std::string& type_name,
const std::string& method_name,
const TypeSpec& ts,
@@ -691,7 +757,8 @@ MethodInfo TypeSystem::add_new_method(Type* type,
return existing;
} else {
return type->add_new_method({0, "new", ts, type->get_name(), false, false, false, docstring});
return type->add_new_method(
{0, "new", ts, type->get_name(), false, false, false, docstring, {}});
}
}
@@ -1027,6 +1094,7 @@ void TypeSystem::add_builtin_types(GameVersion version) {
symbol_type = add_builtin_basic("basic", "symbol");
break;
case GameVersion::Jak2:
case GameVersion::Jak3:
symbol_type = add_builtin_structure("object", "symbol", true);
symbol_type->override_offset(1);
break;
@@ -1884,30 +1952,36 @@ std::string TypeSystem::generate_deftype_footer(const Type* type) const {
}
}
if (type->heap_base()) {
if (type->heap_base() &&
type->heap_base() !=
((type->get_size_in_memory() - get_type_of_type<BasicType>("process")->size() + 0xf) &
~0xf)) {
// don't print if auto heap-base does the job
result.append(fmt::format(" :heap-base #x{:x}\n", type->heap_base()));
}
auto method_count = get_next_method_id(type);
result.append(fmt::format(" :method-count-assert {}\n", get_next_method_id(type)));
result.append(fmt::format(" :size-assert #x{:x}\n", type->get_size_in_memory()));
// result.append(fmt::format(" :method-count-assert {}\n", get_next_method_id(type)));
// result.append(fmt::format(" :size-assert #x{:x}\n", type->get_size_in_memory()));
TypeFlags flags;
flags.heap_base = type->heap_base();
flags.size = type->get_size_in_memory();
flags.pad = 0;
flags.methods = method_count;
result.append(fmt::format(" :flag-assert #x{:x}\n ", flags.flag));
// result.append(fmt::format(" :flag-assert #x{:x}\n", flags.flag));
if (!type->gen_inspect()) {
result.append(":no-inspect\n ");
result.append(" :no-inspect\n ");
}
std::string methods_string;
std::string state_methods_string;
std::string states_string;
// New Method
auto new_info = type->get_new_method_defined_for_type();
if (new_info) {
methods_string.append("(new (");
methods_string.append(" (new (");
for (size_t i = 0; i < new_info->type.arg_count() - 1; i++) {
methods_string.append(new_info->type.get_arg(i).print());
if (i != new_info->type.arg_count() - 2) {
@@ -1915,24 +1989,41 @@ std::string TypeSystem::generate_deftype_footer(const Type* type) const {
}
}
methods_string.append(
fmt::format(") {} ", new_info->type.get_arg(new_info->type.arg_count() - 1).print(), 0));
fmt::format(") {}", new_info->type.get_arg(new_info->type.arg_count() - 1).print(), 0));
auto behavior = new_info->type.try_get_tag("behavior");
if (behavior) {
methods_string.append(fmt::format(":behavior {} ", *behavior));
methods_string.append(fmt::format(" :behavior {}", *behavior));
}
methods_string.append("0)\n ");
methods_string.append(")\n");
}
// Rest of methods
bool done_with_state_methods = false; // TODO fix this... this depends on the order of m_methods
for (auto& info : type->get_methods_defined_for_type()) {
if (!done_with_state_methods && info.type.base_type() == "state" && !info.overrides_parent) {
if (info.type.arg_count() > 1) {
state_methods_string.append(fmt::format(" ({}", info.name));
for (size_t i = 0; i < info.type.arg_count() - 1; ++i) {
state_methods_string.push_back(' ');
state_methods_string.append(info.type.get_arg(i).print());
}
state_methods_string.append(")\n");
} else {
state_methods_string.append(fmt::format(" {}\n", info.name));
}
continue;
} else {
done_with_state_methods = true;
}
// check if we only override the docstring
if (info.only_overrides_docstring) {
continue;
}
methods_string.append(fmt::format("({} (", info.name));
methods_string.append(fmt::format(" ({} (", info.name));
for (size_t i = 0; i < info.type.arg_count() - 1; i++) {
methods_string.append(info.type.get_arg(i).print());
if (i != info.type.arg_count() - 2) {
@@ -1940,35 +2031,32 @@ std::string TypeSystem::generate_deftype_footer(const Type* type) const {
}
}
methods_string.append(
fmt::format(") {} ", info.type.get_arg(info.type.arg_count() - 1).print()));
if (info.no_virtual) {
methods_string.append(":no-virtual ");
}
if (info.overrides_parent) {
methods_string.append(":replace ");
}
fmt::format(") {}", info.type.get_arg(info.type.arg_count() - 1).print()));
auto behavior = info.type.try_get_tag("behavior");
if (behavior) {
methods_string.append(fmt::format(":behavior {} ", *behavior));
methods_string.append(fmt::format(" :behavior {}", *behavior));
}
if (info.type.base_type() == "state") {
methods_string.append(":state ");
methods_string.append(" :state");
}
methods_string.append(fmt::format("{})\n ", info.id));
if (info.no_virtual) {
methods_string.append(" :no-virtual");
}
if (info.overrides_parent) {
if (info.overlay_name.has_value()) {
methods_string.append(fmt::format(" :overlay-at {}", *info.overlay_name));
} else {
methods_string.append(" :replace");
}
}
methods_string.append(fmt::format(")\n", info.id));
}
if (!methods_string.empty()) {
result.append("(:methods\n ");
result.append(methods_string);
result.append(")\n ");
}
std::string states_string;
for (auto& info : type->get_states_declared_for_type()) {
if (info.second.arg_count() > 1) {
states_string.append(fmt::format(" ({}", info.first));
@@ -1982,16 +2070,157 @@ std::string TypeSystem::generate_deftype_footer(const Type* type) const {
}
}
if (!states_string.empty()) {
result.append("(:states\n");
result.append(states_string);
result.append(" )\n ");
if (!state_methods_string.empty()) {
result.append(" (:state-methods\n");
result.append(state_methods_string);
result.append(" )\n");
}
result.append(")\n");
if (!methods_string.empty()) {
result.append(" (:methods\n");
result.append(methods_string);
result.append(" )\n");
}
if (!states_string.empty()) {
result.append(" (:states\n");
result.append(states_string);
result.append(" )\n");
}
result.append(" )\n");
return result;
}
std::optional<std::string> find_best_field_in_structure(const TypeSystem& ts,
const StructureType* st,
int offset,
const Field& requesting_field,
bool want_fixed,
int start_field,
int end_field = -1) {
// performs best field lookup within a structure, at an offset.
const Field* best_val = nullptr;
const Field* best_exact = nullptr;
const Field* best_struct = nullptr;
std::pair<const Field*, int> best_val_arr = {nullptr, -1};
std::pair<const Field*, int> best_exact_arr = {nullptr, -1};
std::pair<const Field*, int> best_struct_arr = {nullptr, -1};
std::optional<std::string> best_struct_field_deref;
const Field* best = nullptr;
if (end_field == -1) {
end_field = st->fields().size();
}
for (size_t i = start_field; i < (size_t)end_field; ++i) {
const auto& field = st->fields().at(i);
auto type = ts.lookup_type(field.type());
if (field.is_dynamic() || field.offset() > offset || field.user_placed() != want_fixed) {
continue;
}
if (!field.is_array()) {
if (!field.is_inline() && field.offset() + type->get_load_size() > offset) {
if (field.offset() == offset) {
// not array, not inline - can fit in register, only check exact offset.
if (!best_val ||
type->get_load_size() == ts.lookup_type(requesting_field.type())->get_load_size()) {
best_val = &field;
}
}
} else if (field.is_inline() && field.offset() + type->get_size_in_memory() > offset) {
if (field.type() == requesting_field.type() && field.offset() == offset) {
// not array, inlined and exact same as this field, just overlay directly on top
best_exact = &field;
} else {
auto f_type = dynamic_cast<StructureType*>(type);
if (f_type) {
// struct that encompasses this field
// simply search that structure for the field we want, offset by the field's offset
auto best_field_in_struct = find_best_field_in_structure(
ts, f_type, offset - field.offset(), requesting_field, want_fixed, 0);
if (best_field_in_struct) {
best_struct_field_deref = best_field_in_struct;
best_struct = &field;
}
}
}
}
} else {
int rel_offset = offset - field.offset();
// array case (and array encompasses what we want)
int array_idx = rel_offset / type->get_size_in_memory();
if (!field.is_inline() &&
field.offset() + field.array_size() * type->get_load_size() > offset) {
if (rel_offset % type->get_load_size() == 0) {
// found exact match for array index
if (!best_val_arr.first ||
type->get_load_size() == ts.lookup_type(requesting_field.type())->get_load_size()) {
best_val_arr.first = &field;
best_val_arr.second = rel_offset / type->get_load_size();
}
}
} else if (field.is_inline() &&
field.offset() + field.array_size() * type->get_size_in_memory() > offset) {
if (field.type() == requesting_field.type() &&
rel_offset % type->get_size_in_memory() == 0) {
// same type
best_exact_arr.first = &field;
best_exact_arr.second = array_idx;
} else if (requesting_field.is_array() && rel_offset % type->get_size_in_memory() == 0 &&
array_idx == 0) {
// starts at the same offset as another array. just use the field with nothing extra
best_exact = &field;
} else {
auto f_type = dynamic_cast<StructureType*>(type);
if (f_type && field.offset() + f_type->get_size_in_memory() > offset) {
// struct that encompasses this field
// simply search that structure for the field we want, offset by the field's offset
auto best_field_in_struct =
find_best_field_in_structure(ts, f_type, rel_offset % type->get_size_in_memory(),
requesting_field, want_fixed, 0);
if (best_field_in_struct) {
best_struct_field_deref = best_field_in_struct;
best_struct_arr.first = &field;
best_struct_arr.second = array_idx;
}
}
}
}
}
}
int best_array_idx = -1;
if (best_exact) {
best = best_exact;
} else if (best_exact_arr.first) {
best = best_exact_arr.first;
best_array_idx = best_exact_arr.second;
} else if (best_val) {
best = best_val;
} else if (best_val_arr.first) {
best = best_val_arr.first;
best_array_idx = best_val_arr.second;
} else if (best_struct) {
best = best_struct;
} else if (best_struct_arr.first) {
best = best_struct_arr.first;
best_array_idx = best_struct_arr.second;
}
if (best) {
auto ret =
best_array_idx == -1 ? best->name() : fmt::format("{} {}", best->name(), best_array_idx);
if (best == best_struct || best == best_struct_arr.first) {
return ret + " " + *best_struct_field_deref;
} else {
return ret;
}
} else if (!want_fixed) {
// try again but with a user-placed offset
return find_best_field_in_structure(ts, st, offset, requesting_field, true, start_field,
end_field);
}
return {};
}
std::string TypeSystem::generate_deftype_for_structure(const StructureType* st) const {
std::string result;
result += fmt::format("(deftype {} ({})\n", st->get_name(), st->get_parent());
@@ -2003,10 +2232,10 @@ std::string TypeSystem::generate_deftype_for_structure(const StructureType* st)
int longest_field_name = 0;
int longest_type_name = 0;
int longest_mods = 0;
int longest_mods_with_user_placed = 0;
const std::string inline_string = ":inline";
const std::string dynamic_string = ":dynamic";
bool has_offset_assert = false;
// calculate longest strings needed, for basic linting
@@ -2020,33 +2249,33 @@ std::string TypeSystem::generate_deftype_for_structure(const StructureType* st)
// normal fields
for (size_t i = st->first_unique_field_idx(); i < st->fields().size(); i++) {
const auto& field = st->fields().at(i);
longest_field_name = std::max(longest_field_name, int(field.name().size()));
longest_type_name = std::max(longest_type_name, int(field.type().print().size()));
int mods = 0;
// mods are array size, :inline, :dynamic
if (field.is_array() && !field.is_dynamic()) {
mods++;
mods += std::to_string(field.array_size()).size();
}
if (field.is_inline()) {
if (mods) {
mods++; // space
}
mods++; // space
mods += inline_string.size();
}
if (field.is_dynamic()) {
if (mods) {
mods++; // space
}
mods++; // space
mods += dynamic_string.size();
}
if (!field.user_placed()) {
has_offset_assert = true;
longest_field_name = std::max(longest_field_name, int(field.name().size()));
if (mods > 0 || field.user_placed()) {
// this is only relevant for fields that have mods
longest_type_name = std::max(longest_type_name, int(field.type().print().size()));
}
longest_mods = std::max(longest_mods, mods);
if (field.user_placed()) {
longest_mods_with_user_placed = std::max(longest_mods_with_user_placed, mods);
}
}
// now actually write out the fields
@@ -2056,10 +2285,9 @@ std::string TypeSystem::generate_deftype_for_structure(const StructureType* st)
const auto& field = st->fields().at(i);
result += "(";
result += field.name();
result.append(1 + (longest_field_name - int(field.name().size())), ' ');
result.append(2 + (longest_field_name - int(field.name().size())), ' ');
result += field.type().print();
result.append(1 + (longest_type_name - int(field.type().print().size())), ' ');
result.append(1 + longest_mods, ' ');
result.append(":override)\n ");
}
@@ -2068,40 +2296,51 @@ std::string TypeSystem::generate_deftype_for_structure(const StructureType* st)
const auto& field = st->fields().at(i);
result += "(";
result += field.name();
result.append(1 + (longest_field_name - int(field.name().size())), ' ');
result.append(2 + (longest_field_name - int(field.name().size())), ' ');
result += field.type().print();
result.append(1 + (longest_type_name - int(field.type().print().size())), ' ');
std::string mods;
if (field.is_array() && !field.is_dynamic()) {
mods += std::to_string(field.array_size());
mods += " ";
mods += std::to_string(field.array_size());
}
if (field.is_inline()) {
mods += inline_string;
mods += " ";
mods += inline_string;
}
if (field.is_dynamic()) {
mods += dynamic_string;
mods += " ";
mods += dynamic_string;
}
if (!mods.empty()) {
result.append(1 + longest_type_name - int(field.type().print().size()), ' ');
}
result.append(mods);
result.append(longest_mods - int(mods.size() - 1), ' ');
if (!field.user_placed()) {
result.append(":offset-assert ");
} else {
if (has_offset_assert) {
result.append(":offset ");
if (field.user_placed()) {
result.append(longest_mods_with_user_placed - int(mods.size()), ' ');
if (mods.empty()) {
result.append(1 + longest_type_name - int(field.type().print().size()), ' ');
}
// find best field for :overlay-at
// we find the first field that does not come after the current one
// and either use it, or check if one of its fields (recursively) is appropriate
// we also check for array offsets. we ALSO do bounds-checking!
// non-fixed offset fields get priority! dynamic fields are IGNORED.
// if all else fails, print as fixed offset.
auto best_match = find_best_field_in_structure(*this, st, field.offset(), field, false, 0, i);
if (!best_match) {
result.append(fmt::format(" :offset {:3d}", field.offset()));
} else if (best_match->find(' ') == std::string::npos) {
result.append(fmt::format(" :overlay-at {}", *best_match));
} else {
result.append(":offset ");
result.append(fmt::format(" :overlay-at (-> {})", *best_match));
}
}
result.append(fmt::format("{:3d}", field.offset()));
result.append(")\n ");
}
+10 -4
View File
@@ -155,14 +155,16 @@ class TypeSystem {
Type* lookup_type(const TypeSpec& ts) const;
Type* lookup_type(const std::string& name) const;
Type* lookup_type_no_throw(const TypeSpec& ts) const;
Type* lookup_type_no_throw(const std::string& name) const;
Type* lookup_type_allow_partial_def(const TypeSpec& ts) const;
Type* lookup_type_allow_partial_def(const std::string& name) const;
int get_load_size_allow_partial_def(const TypeSpec& ts) const;
MethodInfo override_method(Type* type,
const std::string& type_name,
const int method_id,
const std::string& method_name,
const std::optional<std::string>& docstring);
MethodInfo declare_method(const std::string& type_name,
const std::string& method_name,
@@ -175,8 +177,12 @@ class TypeSystem {
const std::optional<std::string>& docstring,
bool no_virtual,
const TypeSpec& ts,
bool override_type,
int id = -1);
bool override_type);
MethodInfo overlay_method(Type* type,
const std::string& method_name,
const std::string& method_overlay_name,
const std::optional<std::string>& docstring,
const TypeSpec& ts);
MethodInfo define_method(const std::string& type_name,
const std::string& method_name,
const TypeSpec& ts,
+2 -2
View File
@@ -38,7 +38,7 @@ bool is_type(const std::string& expected, const TypeSpec& actual, const TypeSyst
std::string symbol_string(const goos::Object& obj) {
if (obj.is_symbol()) {
return obj.as_symbol()->name;
return obj.as_symbol().name_ptr;
}
throw std::runtime_error(obj.print() + " was supposed to be a symbol, but isn't");
}
@@ -69,7 +69,7 @@ EnumType* parse_defenum(const goos::Object& defenum,
if (!enum_name_obj.is_symbol()) {
throw std::runtime_error("defenum must be given a symbol as its name");
}
std::string name = enum_name_obj.as_symbol()->name;
std::string name = enum_name_obj.as_symbol().name_ptr;
auto current = car(iter);
while (current.is_symbol() && symbol_string(current).at(0) == ':') {
+149 -58
View File
@@ -54,7 +54,7 @@ std::string deftype_parent_list(const goos::Object& list) {
throw std::runtime_error("invalid parent in deftype parent list");
}
return parent.as_symbol()->name;
return parent.as_symbol().name_ptr;
}
bool is_type(const std::string& expected, const TypeSpec& actual, const TypeSystem* ts) {
@@ -63,7 +63,7 @@ bool is_type(const std::string& expected, const TypeSpec& actual, const TypeSyst
std::string symbol_string(const goos::Object& obj) {
if (obj.is_symbol()) {
return obj.as_symbol()->name;
return obj.as_symbol().name_ptr;
}
throw std::runtime_error(obj.print() + " was supposed to be a symbol, but isn't");
}
@@ -84,10 +84,12 @@ double get_float(const goos::Object& obj) {
throw std::runtime_error(obj.print() + " was supposed to be an number, but isn't");
}
void add_field(StructureType* structure,
TypeSystem* ts,
const goos::Object& def,
std::unordered_map<goos::HeapObject*, goos::Object>& constants) {
void add_field(
StructureType* structure,
TypeSystem* ts,
const goos::Object& def,
std::unordered_map<goos::InternedSymbolPtr, goos::Object, goos::InternedSymbolPtr::hash>&
constants) {
auto rest = &def;
auto name = symbol_string(car(rest));
@@ -134,14 +136,66 @@ void add_field(StructureType* structure,
throw std::runtime_error(fmt::format("Field {} not found to override", name));
}
} else if (opt_name == ":overlay-at") {
auto field_name = symbol_string(car(rest));
Field overlay_field;
if (!structure->lookup_field(field_name, &overlay_field)) {
throw std::runtime_error(
fmt::format("Field {} not found to overlay for {}", field_name, name));
}
offset_override = overlay_field.offset();
const auto& param = car(rest);
rest = cdr(rest);
Field overlay_field;
if (param.is_symbol()) {
auto field_name = symbol_string(param);
if (!structure->lookup_field(field_name, &overlay_field)) {
throw std::runtime_error(
fmt::format("Field {} not found to overlay for {}", field_name, name));
}
offset_override = overlay_field.offset();
} else if (param.is_pair() && car(&param).is_symbol("->")) {
auto name_it = cdr(&param);
if (name_it->is_empty_list()) {
throw std::runtime_error(
fmt::format("Field list for overlay-at in {} was empty", name));
}
auto type_to_use = structure;
offset_override = 0;
while (!name_it->is_empty_list()) {
const auto& deref_field = car(name_it);
if (deref_field.is_int()) {
auto ref_array_field = !type_to_use && !overlay_field.is_inline()
? ts->lookup_type_allow_partial_def(overlay_field.type())
: nullptr;
if (ref_array_field) {
// we can have an array of references (non-inline) to a forward-declared type
offset_override += ref_array_field->get_load_size() * deref_field.as_int();
} else {
auto type_to_deref = type_to_use && overlay_field.is_inline()
? TypeSpec("inline-array")
: TypeSpec("pointer");
type_to_deref.add_arg(overlay_field.type());
auto deref_info = ts->get_deref_info(type_to_deref);
if (!deref_info.can_deref) {
throw std::runtime_error(
fmt::format("Array could not be dereferenced for overlay-at in {}", name));
}
// overlay_field.type() = deref_info.result_type;
offset_override += deref_info.stride * deref_field.as_int();
}
} else {
if (!type_to_use) {
throw std::runtime_error(
fmt::format("Field {} not inside a structure for overlay-at in {}",
overlay_field.name(), name));
}
auto field_name = symbol_string(car(name_it));
if (!type_to_use->lookup_field(field_name, &overlay_field)) {
throw std::runtime_error(
fmt::format("Field {} not found to overlay for {}", field_name, name));
}
type_to_use =
dynamic_cast<StructureType*>(ts->lookup_type_no_throw(overlay_field.type()));
offset_override += overlay_field.offset();
}
name_it = cdr(name_it);
}
} else {
throw std::runtime_error(fmt::format("Unknown parameter for overlay-at in {}", name));
}
} else if (opt_name == ":score") {
score = get_float(car(rest));
rest = cdr(rest);
@@ -274,14 +328,20 @@ void declare_method(Type* type,
// - this effectively does a :replace without having to re-define the name and signature and
// keep that in-sync
std::string method_name;
std::string method_overlay_name;
TypeSpec function_typespec("function");
std::optional<std::string> docstring;
goos::Object args;
goos::Object return_type;
bool no_virtual = false;
bool replace_method = false;
bool overlay_method = false;
bool overriding_doc = false;
// name
method_name = symbol_string(car(obj));
obj = cdr(obj);
if (!obj->is_empty_list() && car(obj).is_symbol(":override-doc")) {
obj = cdr(obj);
if (car(obj).is_string()) {
@@ -294,10 +354,6 @@ void declare_method(Type* type,
}
if (!overriding_doc) {
// name
method_name = symbol_string(car(obj));
obj = cdr(obj);
// docstring
if (obj->is_pair() && car(obj).is_string()) {
docstring = str_util::trim_newline_indents(car(obj).as_string()->data);
@@ -317,25 +373,21 @@ void declare_method(Type* type,
//
// Doing it like this makes the ordering not critical
while (!obj->is_empty_list() && car(obj).is_symbol()) {
const auto& keyword = car(obj).as_symbol()->name;
const auto& keyword = car(obj).as_symbol();
if (keyword == ":no-virtual") {
no_virtual = true;
} else if (keyword == ":replace") {
replace_method = true;
} else if (keyword == ":state") {
auto behavior_tag = function_typespec.try_get_tag("behavior");
function_typespec = TypeSpec("state");
if (behavior_tag) {
function_typespec.add_new_tag("behavior", behavior_tag.value());
}
// parse state docstrings if available
if (car(cdr(obj)).is_list()) {
if (!cdr(obj)->is_empty_list() && car(cdr(obj)).is_list()) {
obj = cdr(obj);
auto docstring_list = &car(obj);
auto elem = docstring_list;
while (!elem->is_empty_list() && car(elem).is_symbol()) {
const auto& handler = car(elem).as_symbol()->name;
const auto handler_kind = handler_keyword_to_kind(handler);
const auto& handler = car(elem).as_symbol();
const auto handler_kind = handler_keyword_to_kind(handler.name_ptr);
// Get the docstring
elem = cdr(elem);
@@ -360,6 +412,13 @@ void declare_method(Type* type,
throw std::runtime_error("Bad usage of :behavior in a method declaration");
}
function_typespec.add_new_tag("behavior", symbol_string(obj->as_pair()->car));
} else if (keyword == ":overlay-at") {
obj = cdr(obj);
if (!car(obj).is_symbol()) {
throw std::runtime_error("Invalid parameter to method overlay-at");
}
method_overlay_name = symbol_string(car(obj));
overlay_method = true;
}
obj = cdr(obj);
}
@@ -371,36 +430,62 @@ void declare_method(Type* type,
function_typespec.add_arg(parse_typespec(type_system, return_type));
}
// determine the method id, it should be the last in the list
int id = -1;
if (!obj->is_empty_list() && car(obj).is_int()) {
auto& id_obj = car(obj);
id = get_int(id_obj);
obj = cdr(obj);
if (!obj->is_empty_list()) {
throw std::runtime_error(fmt::format("found unknown data in a method declaration:\n{}\n\n{}",
obj->print(), _obj.print()));
}
if (!obj->is_empty_list()) {
throw std::runtime_error("found symbols after the `id` in a method defintion: " +
def.print());
if (overlay_method && (no_virtual || replace_method)) {
throw std::runtime_error(
fmt::format("method {} in type {} has invalid combination of keywords", method_name,
type->get_name()));
}
MethodInfo info;
if (overriding_doc) {
info = type_system->override_method(type, method_name, id, docstring);
info = type_system->override_method(type, method_name, docstring);
} else if (overlay_method) {
info = type_system->overlay_method(type, method_name, method_overlay_name, docstring,
function_typespec);
} else {
info = type_system->declare_method(type, method_name, docstring, no_virtual,
function_typespec, replace_method, id);
function_typespec, replace_method);
}
});
}
// check the method assert
if (id != -1) {
// method id assert!
if (id != info.id) {
lg::print("WARNING - ID assert failed on method {} of type {} (wanted {} got {})\n",
method_name.c_str(), type->get_name().c_str(), id, info.id);
throw std::runtime_error("Method ID assert failed");
void declare_state_methods(Type* type,
TypeSystem* type_system,
const goos::Object& def,
StructureDefResult& /*struct_def*/) {
for_each_in_list(def, [&](const goos::Object& _obj) {
auto obj = &_obj;
// either state-name or (state-name args...) or (state-name "docstring" args...)
std::string method_name;
TypeSpec function_typespec("state");
std::optional<std::string> docstring;
if (obj->is_symbol()) {
method_name = obj->as_symbol().name_ptr;
} else if (obj->is_list()) {
if (!car(obj).is_symbol()) {
throw std::runtime_error(
fmt::format("{} is not a valid name for a state-method", obj->print()));
}
method_name = car(obj).as_symbol().name_ptr;
auto& args = *cdr(obj);
if (car(obj).is_string()) {
// docstring first
docstring = car(obj).as_string()->data;
obj = cdr(obj);
}
for_each_in_list(args, [&](const goos::Object& o) {
function_typespec.add_arg(parse_typespec(type_system, o));
});
}
function_typespec.add_arg(TypeSpec("_type_"));
type_system->declare_method(type, method_name, docstring, false, function_typespec, false);
});
}
@@ -419,8 +504,8 @@ void declare_state(Type* type,
auto docstring_list = &car(obj);
auto elem = docstring_list;
while (!elem->is_empty_list() && car(elem).is_symbol()) {
const auto& handler = car(elem).as_symbol()->name;
const auto handler_kind = handler_keyword_to_kind(handler);
const auto& handler = car(elem).as_symbol();
const auto handler_kind = handler_keyword_to_kind(handler.name_ptr);
// Get the docstring
elem = cdr(elem);
@@ -463,7 +548,8 @@ StructureDefResult parse_structure_def(
TypeSystem* ts,
const goos::Object& fields,
const goos::Object& options,
std::unordered_map<goos::HeapObject*, goos::Object>& constants) {
std::unordered_map<goos::InternedSymbolPtr, goos::Object, goos::InternedSymbolPtr::hash>&
constants) {
StructureDefResult result;
for_each_in_list(fields, [&](const goos::Object& o) { add_field(type, ts, o, constants); });
TypeFlags flags;
@@ -489,6 +575,8 @@ StructureDefResult parse_structure_def(
declare_method(type, ts, *opt_list, result);
} else if (list_name == ":states") {
declare_state(type, ts, *opt_list, result);
} else if (list_name == ":state-methods") {
declare_state_methods(type, ts, *opt_list, result);
} else {
throw std::runtime_error("Invalid option list in field specification: " +
car(rest).print());
@@ -686,8 +774,8 @@ TypeSpec parse_typespec(const TypeSystem* type_system, const goos::Object& src)
while (rest->is_pair()) {
auto& it = rest->as_pair()->car;
if (it.is_symbol() && it.as_symbol()->name.at(0) == ':') {
auto tag_name = it.as_symbol()->name.substr(1);
if (it.is_symbol() && it.as_symbol().name_ptr[0] == ':') {
auto tag_name = it.as_symbol().name_ptr + 1;
rest = &rest->as_pair()->cdr;
if (!rest->is_pair()) {
@@ -696,13 +784,13 @@ TypeSpec parse_typespec(const TypeSystem* type_system, const goos::Object& src)
auto& tag_val = rest->as_pair()->car;
if (tag_name == "behavior") {
if (!type_system->fully_defined_type_exists(tag_val.as_symbol()->name) &&
!type_system->partially_defined_type_exists(tag_val.as_symbol()->name)) {
if (std::string_view(tag_name) == "behavior") {
if (!type_system->fully_defined_type_exists(tag_val.as_symbol().name_ptr) &&
!type_system->partially_defined_type_exists(tag_val.as_symbol().name_ptr)) {
throw std::runtime_error(
fmt::format("Behavior tag uses an unknown type {}", tag_val.as_symbol()->name));
fmt::format("Behavior tag uses an unknown type {}", tag_val.as_symbol().name_ptr));
}
ts.add_new_tag(tag_name, tag_val.as_symbol()->name);
ts.add_new_tag(tag_name, tag_val.as_symbol().name_ptr);
} else {
throw std::runtime_error(fmt::format("Type tag {} is unknown", tag_name));
}
@@ -723,11 +811,14 @@ TypeSpec parse_typespec(const TypeSystem* type_system, const goos::Object& src)
return {};
}
DeftypeResult parse_deftype(const goos::Object& deftype,
TypeSystem* ts,
std::unordered_map<goos::HeapObject*, goos::Object>* constants) {
DeftypeResult parse_deftype(
const goos::Object& deftype,
TypeSystem* ts,
std::unordered_map<goos::InternedSymbolPtr, goos::Object, goos::InternedSymbolPtr::hash>*
constants) {
DefinitionMetadata symbol_metadata;
std::unordered_map<goos::HeapObject*, goos::Object> no_consts;
std::unordered_map<goos::InternedSymbolPtr, goos::Object, goos::InternedSymbolPtr::hash>
no_consts;
auto& constants_to_use = no_consts;
if (constants != nullptr) {
constants_to_use = *constants;
@@ -752,7 +843,7 @@ DeftypeResult parse_deftype(const goos::Object& deftype,
throw std::runtime_error("deftype must be given a symbol as the type name");
}
auto& name = type_name_obj.as_symbol()->name;
auto& name = type_name_obj.as_symbol().name_ptr;
auto parent_type_name = deftype_parent_list(parent_list_obj);
auto parent_type = ts->make_typespec(parent_type_name);
DeftypeResult result;
+2 -1
View File
@@ -20,5 +20,6 @@ struct DeftypeResult {
DeftypeResult parse_deftype(
const goos::Object& deftype,
TypeSystem* ts,
std::unordered_map<goos::HeapObject*, goos::Object>* constants = nullptr);
std::unordered_map<goos::InternedSymbolPtr, goos::Object, goos::InternedSymbolPtr::hash>*
constants = nullptr);
TypeSpec parse_typespec(const TypeSystem* type_system, const goos::Object& src);
+45 -1
View File
@@ -550,6 +550,13 @@ static std::vector<ReplaceInfo> s_replace_info_jak1 = {
{"E~Y~-6H~+2V,~Z", "Ę"},
{"L~Y~-16H~+0V/~Z", "Ł"},
{"Z~Y~-21H~-5Vº~Z", "Ż"},
{"E~Y~-20H~-5Vº~Z", "Ė"},
{"C~Y~-20H~-4Vˇ~Z", "Č"},
{"S~Y~-22H~-4Vˇ~Z", "Š"},
{"Z~Y~-22H~-4Vˇ~Z", "Ž"},
{"U~Y~-13H~+2V,~Z", "Ų"},
{"U~Y~-18H~-10V-~Z", "Ū"},
{"I~Y~-8H~+1V,~Z", "Į"},
// tildes
{"N~Y~-22H~-4V<TIL>~Z", "Ñ"},
@@ -919,6 +926,43 @@ static std::vector<ReplaceInfo> s_replace_info_jak2 = {
{"N~Y~-6Hº~Z~+10H", "Nº"},
{"~+4Vç~-4V", ",c"},
// added for translations TODO - check these for jak 2
{"O~Y~-25H~-1V/~Z", "Ø"},
{"o~Y~-23H~+4V/~Z", "ø"},
{"A~Y~-13H~+8V,~Z", "Ą"},
{"a~Y~-8H~+6V,~Z", "ą"},
{"E~Y~-6H~+8V,~Z", "Ę"},
{"e~Y~-10H~+7V,~Z", "ę"},
{"L~Y~-21H~+0V/~Z", "Ł"},
{"l~Y~-16H~+0V/~Z", "ł"}, // TODO - this one is ugly, font character addition (small slash)
{"Z~Y~-25H~-11Vº~Z", "Ż"},
{"z~Y~-23H~-5Vº~Z", "ż"},
{"a~Y~-25H~-5Vº~Z", "å"},
{"S~Y~-21H~-5V'~Z", "Ś"},
{"s~Y~-25H~-5V'~Z", "ś"},
{"n~Y~-25H~-5V'~Z", "ń"},
{"c~Y~-25H~-5V'~Z", "ć"},
{"o~Y~-24H~-4V<TIL>~Z", "õ"},
{"a~Y~-24H~-4V<TIL>~Z", "ã"},
{"O~Y~-28H~-4V'~-9H'~Z", "Ő"},
{"U~Y~-27H~-4V'~-12H'~Z", "Ű"},
{"o~Y~-28H~-4V'~-9H'~Z", "ő"},
{"u~Y~-28H~-4V'~-9H'~Z", "ű"},
{"E~Y~-22H~-11Vº~Z", "Ė"},
{"e~Y~-25H~-5Vº~Z", "ė"},
{"C~Y~-27H~-10Vˇ~Z", "Č"},
{"c~Y~-25H~-5Vˇ~Z", "č"},
{"S~Y~-24H~-10Vˇ~Z", "Š"},
{"s~Y~-22H~-4Vˇ~Z", "š"},
{"Z~Y~-25H~-10Vˇ~Z", "Ž"},
{"z~Y~-23H~-4Vˇ~Z", "ž"},
{"U~Y~-15H~+5V,~Z", "Ų"},
{"u~Y~-15H~+5V,~Z", "ų"},
{"U~Y~-20H~-18V-~Z", "Ū"},
{"u~Y~-18H~-15V-~Z", "ū"},
{"I~Y~-8H~+4V,~Z", "Į"},
{"i~Y~-8H~+4V,~Z", "į"},
// tildes
{"N~Y~-22H~-4V<TIL>~Z", "Ñ"},
{"n~Y~-24H~-4V<TIL>~Z", "ñ"},
@@ -942,7 +986,7 @@ static std::vector<ReplaceInfo> s_replace_info_jak2 = {
{"A~Y~-20H~-4V^~Z", "Â"},
{"a~Y~-24H~-5V^~Z", "â"},
{"E~Y~-20H~-5V^~Z", "Ê"},
{"e~Y~-25H~-4V^~Zt", "ê"},
{"e~Y~-25H~-4V^~Z", "ê"},
{"I~Y~-19H~-5V^~Z", "Î"},
{"i~Y~-19H~-8V^~Z", "î"},
{"O~Y~-20H~-4V^~Z", "Ô"},
-40
View File
@@ -1,41 +1 @@
#include "crc32.h"
#include <cstring>
#ifdef __aarch64__
#include <arm_acle.h>
u32 crc32(const u8* data, size_t size) {
u32 result = 0xffffffff;
while (size >= 4) {
u32 x;
memcpy(&x, data, 4);
data += 4;
size -= 4;
result = __crc32w(result, x);
}
while (size) {
result = __crc32b(result, *data);
data++;
size--;
}
return ~result;
}
#else
#include <immintrin.h>
u32 crc32(const u8* data, size_t size) {
u32 result = 0xffffffff;
while (size >= 4) {
u32 x;
memcpy(&x, data, 4);
data += 4;
size -= 4;
result = _mm_crc32_u32(result, x);
}
while (size) {
result = _mm_crc32_u8(result, *data);
data++;
size--;
}
return ~result;
}
#endif
+41 -1
View File
@@ -2,4 +2,44 @@
#include "common/common_types.h"
u32 crc32(const u8* data, size_t size);
u32 crc32(const u8* data, size_t size);
#include <cstring>
#ifdef __aarch64__
#include <arm_acle.h>
inline u32 crc32(const u8* data, size_t size) {
u32 result = 0xffffffff;
while (size >= 4) {
u32 x;
memcpy(&x, data, 4);
data += 4;
size -= 4;
result = __crc32w(result, x);
}
while (size) {
result = __crc32b(result, *data);
data++;
size--;
}
return ~result;
}
#else
#include <immintrin.h>
inline u32 crc32(const u8* data, size_t size) {
u32 result = 0xffffffff;
while (size >= 4) {
u32 x;
memcpy(&x, data, 4);
data += 4;
size -= 4;
result = _mm_crc32_u32(result, x);
}
while (size) {
result = _mm_crc32_u8(result, *data);
data++;
size--;
}
return ~result;
}
#endif
+2 -1
View File
@@ -24,7 +24,8 @@ void assert_string_empty_after(const char* str, int size) {
std::string get_object_file_name(const std::string& original_name, u8* data, int size) {
const std::string art_group_text_strings[] = {
fmt::format("/src/next/data/art-group{}/", versions::jak1::ART_FILE_VERSION),
fmt::format("/src/jak2/final/art-group{}/", versions::jak2::ART_FILE_VERSION)};
fmt::format("/src/jak2/final/art-group{}/", versions::jak2::ART_FILE_VERSION),
fmt::format("/src/jak3/final/art-group{}/", versions::jak3::ART_FILE_VERSION)};
const std::string suffix = "-ag.go";
for (auto& art_group_text : art_group_text_strings) {
+9
View File
@@ -73,6 +73,15 @@ std::string strip_cpp_style_comments(const std::string& input) {
return output;
}
std::optional<nlohmann::json> safe_parse_json(const std::string& input) {
json j = json::parse(input, nullptr, false);
if (j.is_discarded()) {
lg::error("Failed to parse json");
return {};
}
return j;
}
/*!
* Parse JSON file with comments stripped. Unlike the default comment stripping feature
* of nlohmann::json, this allows you to have multiple comments in a row!
+1
View File
@@ -10,6 +10,7 @@
using json = nlohmann::json;
std::string strip_cpp_style_comments(const std::string& input);
std::optional<nlohmann::json> safe_parse_json(const std::string& input);
nlohmann::json parse_commented_json(const std::string& input, const std::string& source_name);
Range<int> parse_json_optional_integer_range(const nlohmann::json& json);
+10 -2
View File
@@ -34,10 +34,18 @@ void IsoFile::Entry::print(std::string* result, const std::string& prefix) const
namespace {
constexpr int SECTOR_SIZE = 0x800;
int fseek_64(FILE* fp, u64 offset, int origin) {
#ifdef _WIN32
return _fseeki64(fp, offset, origin);
#else
return fseek(fp, offset, origin);
#endif
}
template <typename T>
T read_file(FILE* fp, u32 sector, u32 offset_in_sector) {
T result;
if (fseek(fp, sector * SECTOR_SIZE + offset_in_sector, SEEK_SET)) {
if (fseek_64(fp, u64(sector) * SECTOR_SIZE + offset_in_sector, SEEK_SET)) {
ASSERT_MSG(false, "Failed to fseek iso");
}
if (fread(&result, sizeof(T), 1, fp) != 1) {
@@ -95,7 +103,7 @@ void unpack_entry(FILE* fp,
lg::info("Extracting {}...", entry.name);
}
std::vector<u8> buffer(entry.size);
if (fseek(fp, entry.offset_in_file, SEEK_SET)) {
if (fseek_64(fp, entry.offset_in_file, SEEK_SET)) {
ASSERT_MSG(false, "Failed to fseek iso when unpacking");
}
if (fread(buffer.data(), buffer.size(), 1, fp) != 1) {
+11 -5
View File
@@ -29,9 +29,14 @@ constexpr u32 ART_FILE_VERSION = 7;
constexpr u32 LEVEL_FILE_VERSION = 36;
constexpr u32 DGO_FILE_VERSION = 1;
constexpr u32 TX_PAGE_VERSION = 8;
} // namespace jak2
namespace jak3 {
constexpr u32 ART_FILE_VERSION = 8;
constexpr u32 LEVEL_FILE_VERSION = 36;
constexpr u32 TX_PAGE_VERSION = 8;
} // namespace jak3
} // namespace versions
// GOAL kernel version (OpenGOAL changes this version from the game's version)
@@ -42,16 +47,17 @@ constexpr int KERNEL_VERSION_MINOR = 0;
constexpr int IRX_VERSION_MAJOR = 2;
constexpr int IRX_VERSION_MINOR = 0;
enum class GameVersion { Jak1 = 1, Jak2 = 2 };
enum class GameVersion { Jak1 = 1, Jak2 = 2, Jak3 = 3 };
// TODO: most usages of this are currently stubs for jak 3
template <typename T>
struct PerGameVersion {
constexpr PerGameVersion(T jak1, T jak2) : data{jak1, jak2} {}
constexpr PerGameVersion(T jak1, T jak2, T jak3) : data{jak1, jak2, jak3} {}
constexpr const T& operator[](GameVersion v) const { return data[(int)v - 1]; }
T data[2];
T data[3];
};
constexpr PerGameVersion<const char*> game_version_names = {"jak1", "jak2"};
constexpr PerGameVersion<const char*> game_version_names = {"jak1", "jak2", "jak3"};
GameVersion game_name_to_version(const std::string& name);
bool valid_game_version(const std::string& name);
+12
View File
@@ -3,10 +3,22 @@ files:
translation: /game/assets/jak1/text/game_custom_text_%locale%.json
- source: /game/assets/jak1/subtitle/subtitle_lines_en-US.json
translation: /game/assets/jak1/subtitle/subtitle_lines_%locale%.json
excluded_target_languages:
- fr
- de
- es-ES
- it
- ja
- en-GB
- source: /game/assets/jak2/text/game_custom_text_en-US.json
translation: /game/assets/jak2/text/game_custom_text_%locale%.json
- source: /game/assets/jak2/subtitle/subtitle_lines_en-US.json
translation: /game/assets/jak2/subtitle/subtitle_lines_%locale%.json
excluded_target_languages:
- "fr"
- "de"
- "es-ES"
- "it"
- "ja"
- "ko"
- "en-GB"
@@ -10,7 +10,7 @@
// Must have vertex colors. Use the blender cycles renderer, bake, diffuse, uncheck color,
// and bake to vertex colors. For now, only the first vertex color group is used, so make sure you
// only have 1.
"gltf_file": "custom_levels/test-zone/test-zone2.glb",
"gltf_file": "custom_levels/jak1/test-zone/test-zone2.glb",
// automatically set wall vs. ground based on angle. Useful if you don't want to assign this yourself
"automatic_wall_detection": true,
@@ -36,7 +36,25 @@
// All art groups you want to use in your custom level. Will add their models and corresponding textures to the FR3 file.
// Note: You will still have to add them to your level's .gd file.
"art_groups": ["plat-ag"],
// Removed so that the release builds don't have to double-decompile the game
// "art_groups": ["plat-ag"],
"art_groups": [],
// Any textures you want to include in your custom level. This is mainly useful for things such as the zoomer HUD,
// which is not in the common level files and has no art group associated with it.
// To get a list of all the textures, you can extract all of the game's textures
// by setting "save_texture_pngs" to true in the decompiler config.
"textures": [
// all textures required for the zoomer HUD
// "zoomerhud",
// "zoomerhud-dial",
// "zoomerhud-main-02",
// "zoomerhud-main-03",
// "zoomerhud-pieslice",
// "zoomerhud-heatbg-01",
// "zoomerhud-main-03arrow",
// "zoomerhud-main-03knob"
],
// Ambients you want to use in your custom level. Ambients can be used for various things like changing the music variation (flava),
// adding ambient sounds, level name hints, etc.
@@ -58,10 +76,10 @@
"trans": [-21.6238, 20.0496, 17.1191], // translation
"etype": "fuel-cell", // actor type
"game_task": 0, // associated game task (for powercells, etc)
"quat" : [0, 0, 0, 1], // quaternion
"quat": [0, 0, 0, 1], // quaternion
"bsphere": [-21.6238, 19.3496, 17.1191, 10], // bounding sphere
"lump": {
"name":"test-fuel-cell"
"name": "test-fuel-cell"
}
},
@@ -69,11 +87,11 @@
"trans": [-15.2818, 15.2461, 17.1360], // translation
"etype": "crate", // actor type
"game_task": 0, // associated game task (for powercells, etc)
"quat" : [0, 0, 0, 1], // quaternion
"quat": [0, 0, 0, 1], // quaternion
"bsphere": [-15.2818, 15.2461, 17.1360, 10], // bounding sphere
"lump": {
"name":"test-crate",
"crate-type":"'steel",
"name": "test-crate",
"crate-type": "'steel",
"eco-info": ["int32", 5, 10]
}
},
@@ -82,21 +100,21 @@
"trans": [-5.4630, 17.4553, 1.6169], // translation
"etype": "eco-yellow", // actor type
"game_task": 0, // associated game task (for powercells, etc)
"quat" : [0, 0, 0, 1], // quaternion
"quat": [0, 0, 0, 1], // quaternion
"bsphere": [-5.4630, 17.4553, 1.6169, 10], // bounding sphere
"lump": {
"name":"test-eco"
}
},
{
"trans": [-7.41, 3.5, 28.42], // translation
"etype": "plat", // actor type
"game_task": 0, // associated game task (for powercells, etc)
"quat": [0, 0, 0, 1], // quaternion
"bsphere": [-7.41, 3.5, 28.42, 10], // bounding sphere
"lump": {
"name": "test-plat"
"name": "test-eco"
}
}
// {
// "trans": [-7.41, 3.5, 28.42], // translation
// "etype": "plat", // actor type
// "game_task": 0, // associated game task (for powercells, etc)
// "quat": [0, 0, 0, 1], // quaternion
// "bsphere": [-7.41, 3.5, 28.42, 10], // bounding sphere
// "lump": {
// "name": "test-plat"
// }
// }
]
}
File renamed without changes.
@@ -0,0 +1,85 @@
{
// The "in-game" name of the level. Should be lower case, with dashes (GOAL symbol name)
// the name of this file, and the folder this file is in must have the same name.
"long_name": "test-zone",
// The file name, should be upper case and 8 characters or less.
"iso_name": "TESTZONE",
// The nickname, should be exactly 3 characters
"nickname": "tsz", // 3 char name, all lowercase
// Background mesh file.
// Must have vertex colors. Use the blender cycles renderer, bake, diffuse, uncheck color,
// and bake to vertex colors. For now, only the first vertex color group is used, so make sure you
// only have 1.
"gltf_file": "custom_levels/jak2/test-zone/test-zone2.glb",
// automatically set wall vs. ground based on angle. Useful if you don't want to assign this yourself
"automatic_wall_detection": true,
"automatic_wall_angle": 45.0,
// if your mesh has triangles with incorrect orientation, set this to make all collision mesh triangles double sided
// this makes collision 2x slower and bigger, so only use if really needed
"double_sided_collide": false,
// available res-lump tag data types:
// int32, float, meters, vector, vector4m (meters)
//
// examples:
//
// adds a float tag 'spring-height' with value of 200 meters (1 meter = 4096.0 units):
// "spring-height": ["meters", 200.0]
//
// adds a vector tag 'movie-pos':
// "movie-pos": ["vector", [4096000.0, -176128.0, 1353973.76, 1.0]]
// The base actor id for your custom level. If you have multiple levels, this should be unique!
"base_id": 100,
// All art groups you want to use in your custom level. Will add their models and corresponding textures to the FR3 file.
// Removed so that the release builds don't have to double-decompile the game
//"art_groups": ["prsn-torture-ag"],
"art_groups": [],
// Any textures you want to include in your custom level.
// This is mainly useful for textures which are not in the common level files and have no art group associated with them.
// To get a list of all the textures, you can extract all of the game's textures
// by setting "save_texture_pngs" to true in the decompiler config.
"textures": [],
"actors" : [
{
"trans": [-15.2818, 15.2461, 17.1360], // translation
"etype": "crate", // actor type
"game_task": 0, // associated game task (for powercells, etc)
"kill_mask": 0,
"quat": [0, 0, 0, 1], // quaternion
"bsphere": [-15.2818, 15.2461, 17.1360, 10], // bounding sphere
"lump": {
"name": "test-crate",
"eco-info": ["int32", 18, 2]
}
},
{
"trans": [-5.4630, 17.4553, 1.6169], // translation
"etype": "eco-yellow", // actor type
"game_task": 0, // associated game task (for powercells, etc)
"kill_mask": 0,
"quat": [0, 0, 0, 1], // quaternion
"bsphere": [-5.4630, 17.4553, 1.6169, 10], // bounding sphere
"lump": {
"name": "test-eco"
}
},
{
"trans": [-7.41, 13.5, 28.42], // translation
"etype": "prsn-torture", // actor type
"game_task": 0, // associated game task (for powercells, etc)
"quat": [0, 0, 0, 1], // quaternion
"bsphere": [-7.41, 13.5, 28.42, 10], // bounding sphere
"lump": {
"name": "test-torture"
}
}
]
}
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
;; DGO definition file for Awful Village level
;; We use the convention of having a longer DGO name for levels without precomputed visibility.
;; the actual file name still needs to be 8.3
("TSZ.DGO"
(
"prison-obs.o"
"test-zone.go"
))
+2 -1
View File
@@ -58,6 +58,7 @@ add_library(
level_extractor/extract_actors.cpp
level_extractor/extract_collide_frags.cpp
level_extractor/extract_common.cpp
level_extractor/extract_joint_group.cpp
level_extractor/extract_level.cpp
level_extractor/extract_merc.cpp
level_extractor/extract_tfrag.cpp
@@ -111,7 +112,7 @@ target_link_libraries(decompiler
add_executable(extractor
extractor/main.cpp extractor/extractor_util.cpp)
extractor/main.cpp)
target_link_libraries(extractor
decomp
+6 -1
View File
@@ -37,7 +37,10 @@ struct OpcodeFields {
uint32_t sa() { return (data >> 6) & 0x1f; }
// 0 - 5
// TODO - remove once we update clang-format
// clang-format off
uint32_t function() { return (data)&0x3f; }
// clang-format on
////////////////
// Immediates //
@@ -556,8 +559,10 @@ static InstructionKind decode_mmi3(OpcodeFields fields) {
case 0b11011:
ASSERT(fields.rs() == 0);
return IK::PCPYH;
case 0b11110:
return IK::PEXCW;
default:
ASSERT(false);
ASSERT_MSG(false, fmt::format("unknown mmi3: 0b{:b}\n", fields.MMI_func()));
return IK::UNKNOWN;
}
}
+4
View File
@@ -317,6 +317,10 @@ void init_opcode_info() {
drd_srs_srt(def(IK::PXOR, "pxor").gpr128()); // Parallel Exclusive Or
def(IK::PEXEW, "pexew").gpr128().dst_gpr(FT::RD).src_gpr(FT::RT); // Parallel Exchange Even Word
def(IK::PEXCW, "pexcw")
.gpr128()
.dst_gpr(FT::RD)
.src_gpr(FT::RT); // Parallel Exchange Center Word
drd_srt_ssa(def(IK::PSLLW, "psllw").gpr128()); // Parallel Shift Left Logical Word
drd_srt_ssa(def(IK::PSLLH, "psllh").gpr128()); // Parallel Shift Left Logical Halfword
+1
View File
@@ -182,6 +182,7 @@ enum class InstructionKind {
PNOR,
PCPYH,
PINTEH,
PEXCW,
// COP1 / FPU
ADDS,
+15 -2
View File
@@ -584,6 +584,13 @@ void Function::find_type_defs(LinkedObjectFile& file, DecompilerTypeSystem& dts)
int label_idx = -1;
for (const auto& instr : instructions) {
// far labels
if (label_idx != -1 && state == 5 &&
!(instr.kind == InstructionKind::JALR && instr.get_dst(0).get_reg() == make_gpr(Reg::RA) &&
instr.get_src(0).get_reg() == make_gpr(Reg::T9))) {
continue;
}
// look for lw xx, type(s7)
if (instr.kind == InstructionKind::LW && instr.get_src(0).is_sym() &&
instr.get_src(0).get_sym() == "type" && instr.get_src(1).get_reg() == make_gpr(Reg::S7)) {
@@ -630,8 +637,14 @@ void Function::find_type_defs(LinkedObjectFile& file, DecompilerTypeSystem& dts)
if (state == 4) {
// look for ld a2, LXX(fp)
if (instr.kind == InstructionKind::LD && instr.get_dst(0).get_reg() == make_gpr(Reg::A2) &&
instr.get_src(0).is_label() && instr.get_src(1).get_reg() == make_gpr(Reg::FP)) {
if ((instr.kind == InstructionKind::LD && instr.get_dst(0).get_reg() == make_gpr(Reg::A2) &&
instr.get_src(0).is_label() && instr.get_src(1).get_reg() == make_gpr(Reg::FP))) {
state = 5;
label_idx = instr.get_src(0).get_label();
continue;
} else if ((instr.kind == InstructionKind::LUI &&
instr.get_dst(0).get_reg() == make_gpr(Reg::V1) && instr.get_src(0).is_label())) {
// far labels
state = 5;
label_idx = instr.get_src(0).get_label();
continue;
+71 -6
View File
@@ -93,6 +93,8 @@ TP_Type SimpleAtom::get_type(const TypeState& input,
return TP_Type::make_run_function_in_process_function();
} else if (m_string == "set-to-run" && env.func->name() != "enter-state") {
return TP_Type::make_set_to_run_function();
} else if (m_string == "find-parent-method") {
return TP_Type::make_find_parent_method_function();
}
// look up the type of the symbol
@@ -1355,10 +1357,6 @@ TypeState CallOp::propagate_types_internal(const TypeState& input,
in_type = TypeSpec("function", new_arg_types);
}
if (in_type.arg_count() < 1) {
throw std::runtime_error("Called a function, but we do not know its type");
}
if (in_type.arg_count() == 2 && in_type.get_arg(0) == TypeSpec("_varargs_")) {
// we're calling a varags function, which is format. We can determine the argument count
// by looking at the format string, if we can get it.
@@ -1418,12 +1416,79 @@ TypeState CallOp::propagate_types_internal(const TypeState& input,
arg_type.print());
}
}
bool use_normal_last_arg = true;
if (in_tp.kind == TP_Type::Kind::FIND_PARENT_METHOD_FUNCTION) {
bool can_use_call_parent = true;
TypeSpec call_parent_result_type;
const auto& guessed_name = env.func->guessed_name;
if (guessed_name.kind == FunctionName::FunctionKind::METHOD ||
guessed_name.kind == FunctionName::FunctionKind::V_STATE) {
// should call something like:
// (find-parent-method sharkey 39)
const auto& type_arg = input.get(Register(Reg::GPR, Reg::A0));
if (can_use_call_parent && type_arg.kind != TP_Type::Kind::TYPE_OF_TYPE_NO_VIRTUAL) {
lg::warn(
"Can't use call-parent-method because the first argument to find-parent-method is a "
"{}, which should be a type",
type_arg.print());
can_use_call_parent = false;
}
if (can_use_call_parent && type_arg.get_type_objects_typespec() != guessed_name.type_name) {
lg::warn(
"Can't use call-parent-method because the first argument type is wrong: got {}, but "
"expected {}",
type_arg.get_type_objects_typespec().print(), guessed_name.type_name);
can_use_call_parent = false;
}
const auto& id_arg = input.get(Register(Reg::GPR, Reg::A1));
int expected_id = -1;
if (guessed_name.kind == FunctionName::FunctionKind::V_STATE) {
auto state_info = dts.ts.lookup_method(guessed_name.type_name, guessed_name.state_name);
expected_id = state_info.id;
call_parent_result_type =
state_info.type.substitute_for_method_call(guessed_name.type_name);
} else {
expected_id = guessed_name.method_id;
call_parent_result_type = env.func->type; // same type as this method!
}
if (can_use_call_parent && !id_arg.is_integer_constant(expected_id)) {
lg::warn(
"Can't use call-parent-method because the second argument is wrong: got {}, but "
"expected a constant integer {}",
id_arg.print(), expected_id);
can_use_call_parent = false;
}
} else {
can_use_call_parent = false;
lg::warn(
"Can't use call-parent-method because find-parent-method was called in {}, which isn't a "
"method or state handler.",
env.func->name());
}
if (can_use_call_parent) {
end_types.get(Register(Reg::GPR, Reg::V0)) = TP_Type::make_from_ts(call_parent_result_type);
use_normal_last_arg = false;
}
}
if (in_type.arg_count() < 1) {
throw std::runtime_error("Called a function, but we do not know its type");
}
if (use_normal_last_arg) {
end_types.get(Register(Reg::GPR, Reg::V0)) = TP_Type::make_from_ts(in_type.last_arg());
}
// set the call type!
m_call_type = in_type;
m_call_type_set = true;
end_types.get(Register(Reg::GPR, Reg::V0)) = TP_Type::make_from_ts(in_type.last_arg());
if (in_tp.kind == TP_Type::Kind::NON_OBJECT_NEW_METHOD &&
in_type.last_arg() == TypeSpec("array")) {
// array new:
+16
View File
@@ -617,4 +617,20 @@ std::optional<std::string> Env::get_art_elt_name(int idx) const {
}
}
std::optional<std::string> Env::get_joint_node_name(int idx) const {
ASSERT(dts);
auto it = dts->jg_info.find(joint_geo());
if (it == dts->jg_info.end()) {
return {};
} else {
const auto& jg = it->second;
auto it2 = jg.find(idx);
if (it2 == jg.end()) {
return {};
} else {
return it2->second;
}
}
}
} // namespace decompiler
+10
View File
@@ -157,6 +157,15 @@ class Env {
void set_art_group(const std::string& art_group) { m_art_group = art_group; }
const std::string& art_group() const { return m_art_group; }
std::optional<std::string> get_art_elt_name(int idx) const;
void set_jg(const std::string& art_group) {
if (art_group.substr(art_group.size() - 3) == "-ag") {
m_joint_geo = art_group.substr(0, art_group.size() - 3) + "-lod0-jg";
} else {
m_joint_geo = art_group + "-lod0-jg";
}
}
const std::string& joint_geo() const { return m_joint_geo; }
std::optional<std::string> get_joint_node_name(int idx) const;
void set_remap_for_function(const Function& func);
void set_remap_for_method(const TypeSpec& ts);
@@ -244,5 +253,6 @@ class Env {
StackSpillMap m_stack_spill_map;
std::string m_art_group;
std::string m_joint_geo;
};
} // namespace decompiler
+1
View File
@@ -1299,6 +1299,7 @@ class DerefElement : public FormElement {
private:
ConstantTokenElement* try_as_art_const(const Env& env, FormPool& pool);
GenericElement* try_as_joint_node_index(const Env& env, FormPool& pool);
GenericElement* try_as_curtime(const Env& env, FormPool& pool);
GenericElement* try_as_seconds_per_frame(const Env& env, FormPool& pool);
+69 -2
View File
@@ -2621,7 +2621,9 @@ void SetVarElement::push_to_stack(const Env& env, FormPool& pool, FormStack& sta
}
}
FormElement* SetFormFormElement::make_set_time(const Env& env, FormPool& pool, FormStack& stack) {
FormElement* SetFormFormElement::make_set_time(const Env& /*env*/,
FormPool& pool,
FormStack& /*stack*/) {
auto matcher = match(
Matcher::op(GenericOpMatcher::func(Matcher::constant_token("current-time")), {}), m_src);
if (matcher.matched) {
@@ -3535,7 +3537,7 @@ void FunctionCallElement::update_from_stack(const Env& env,
Form* so_group_f = nullptr;
if (!elt_group->elts().at(0)->to_form(env).is_symbol("sfx")) {
so_group_f = pool.form<ConstantTokenElement>(
elt_group->elts().at(0)->to_form(env).as_symbol()->name);
elt_group->elts().at(0)->to_form(env).as_symbol().name_ptr);
}
auto so_positional_f = arg_forms.at(6);
if (so_positional_f->to_form(env).is_symbol("#t")) {
@@ -3908,6 +3910,39 @@ void FunctionCallElement::update_from_stack(const Env& env,
}
}
// detect call-parent-method
{
const auto& guessed_name = env.func->guessed_name;
if (guessed_name.kind == FunctionName::FunctionKind::METHOD) {
// detect stuff like: ((find-parent-method...) arg...)
auto mr_find_parent =
match(Matcher::func(Matcher::symbol("find-parent-method"),
{Matcher::symbol(env.func->method_of_type),
Matcher::integer(env.func->guessed_name.method_id)}),
unstacked.at(0)
);
if (mr_find_parent.matched) {
new_form = pool.alloc_element<GenericElement>(
GenericOperator::make_function(pool.form<ConstantTokenElement>("call-parent-method")),
arg_forms);
}
} else if (guessed_name.kind == FunctionName::FunctionKind::V_STATE && arg_forms.size() == 2) {
// here, simply detect (find-parent-method...)
//
auto mr_find_parent = match(Matcher::symbol("find-parent-method"), unstacked.at(0));
if (mr_find_parent.matched) {
auto state_info =
env.dts->ts.lookup_method(guessed_name.type_name, guessed_name.state_name);
if (arg_forms.at(0)->to_string(env) == guessed_name.type_name &&
arg_forms.at(1)->to_string(env) == std::to_string(state_info.id)) {
new_form = pool.alloc_element<GenericElement>(
GenericOperator::make_function(pool.form<ConstantTokenElement>("find-parent-state")));
}
}
}
}
result->push_back(new_form);
}
@@ -3942,6 +3977,32 @@ ConstantTokenElement* DerefElement::try_as_art_const(const Env& env, FormPool& p
return nullptr;
}
GenericElement* DerefElement::try_as_joint_node_index(const Env& env, FormPool& pool) {
auto mr =
match(Matcher::deref(Matcher::s6(), false,
{DerefTokenMatcher::string("node-list"),
DerefTokenMatcher::string("data"), DerefTokenMatcher::any_integer(0)}),
this);
if (mr.matched) {
// lg::print("func {} joint-geo: {}\n", env.func->name(), env.joint_geo());
auto info = env.dts->jg_info;
std::vector<Form*> args;
auto joint_name = env.get_joint_node_name(mr.maps.ints.at(0));
args.push_back(pool.form<ConstantTokenElement>(env.joint_geo()));
if (joint_name) {
args.push_back(pool.form<ConstantTokenElement>(joint_name.value()));
return pool.alloc_element<GenericElement>(
GenericOperator::make_function(pool.form<ConstantTokenElement>("joint-node")), args);
} else {
lg::error("function `{}`: did not find joint node {} in {}", env.func->name(),
mr.maps.ints.at(0), env.joint_geo());
}
}
return nullptr;
}
GenericElement* DerefElement::try_as_curtime(const Env& env, FormPool& pool) {
if (env.version == GameVersion::Jak1) {
auto mr = match(Matcher::deref(Matcher::symbol("*display*"), false,
@@ -4013,6 +4074,12 @@ void DerefElement::update_from_stack(const Env& env,
return;
}
auto as_jnode = try_as_joint_node_index(env, pool);
if (as_jnode) {
result->push_back(as_jnode);
return;
}
auto as_simple_expr = m_base->try_as_element<SimpleExpressionElement>();
if (env.version == GameVersion::Jak2 && as_simple_expr && as_simple_expr->expr().is_identity() &&
as_simple_expr->expr().get_arg(0).is_sym_val() &&
+4 -4
View File
@@ -276,7 +276,7 @@ bool Matcher::do_match(Form* input, MatchResult::Maps* maps_out, const Env* cons
} else if (m_kind == Kind::VAR_NAME) {
return env && env->get_variable_name_name_only(result) == m_str;
} else if (m_reg_out_id != -1) {
if (m_kind == Kind::SAME_VAR && maps_out->regs.size() > m_reg_out_id &&
if (m_kind == Kind::SAME_VAR && (int)maps_out->regs.size() > m_reg_out_id &&
maps_out->regs.at(m_reg_out_id)) {
return env && env->get_variable_name_name_only(result) ==
env->get_variable_name_name_only(*maps_out->regs.at(m_reg_out_id));
@@ -695,7 +695,7 @@ bool Matcher::do_match(Form* input, MatchResult::Maps* maps_out, const Env* cons
if (as_let) {
// fail if we have wrong number of entries/body elts or recursive marker
if ((m_entry_matchers.size() != as_let->entries().size()) ||
(m_sub_matchers.size() != as_let->body()->size()) ||
((int)m_sub_matchers.size() != as_let->body()->size()) ||
(as_let->is_star() != m_let_is_star)) {
return false;
}
@@ -706,7 +706,7 @@ bool Matcher::do_match(Form* input, MatchResult::Maps* maps_out, const Env* cons
}
}
// now match body
for (int i = 0; i < m_sub_matchers.size(); ++i) {
for (int i = 0; i < (int)m_sub_matchers.size(); ++i) {
Form fake;
fake.elts().push_back(as_let->body()->elts().at(i));
if (!m_sub_matchers.at(i).do_match(&fake, maps_out, env)) {
@@ -908,7 +908,7 @@ bool LetEntryMatcher::do_match(const LetElement::Entry& input,
case Kind::ANY:
case Kind::NAME:
if (m_reg_out_id != -1) {
if (m_kind == Kind::NAME && maps_out->regs.size() > m_reg_out_id &&
if (m_kind == Kind::NAME && (int)maps_out->regs.size() > m_reg_out_id &&
maps_out->regs.at(m_reg_out_id)) {
return env && env->get_variable_name_name_only(input.dest) ==
env->get_variable_name_name_only(*maps_out->regs.at(m_reg_out_id));
+4 -3
View File
@@ -113,7 +113,7 @@ Function* LinkedObjectFile::try_get_function_at_label(int label_id) {
Function* LinkedObjectFile::try_get_function_at_label(const DecompilerLabel& label) {
for (auto& func : functions_by_seg.at(label.target_segment)) {
// + 4 to skip past type tag to the first word, which is were the label points.
// + 4 to skip past type tag to the first word, which is where the label points.
if (func.start_word * 4 + 4 == label.offset) {
return &func;
}
@@ -128,7 +128,7 @@ const Function* LinkedObjectFile::try_get_function_at_label(int label_id) const
const Function* LinkedObjectFile::try_get_function_at_label(const DecompilerLabel& label) const {
for (auto& func : functions_by_seg.at(label.target_segment)) {
// + 4 to skip past type tag to the first word, which is were the label points.
// + 4 to skip past type tag to the first word, which is where the label points.
if (func.start_word * 4 + 4 == label.offset) {
return &func;
}
@@ -156,7 +156,8 @@ bool LinkedObjectFile::pointer_link_word(int source_segment,
ASSERT(word.kind() == LinkedWord::PLAIN_DATA);
if (dest_offset / 4 > (int)words_by_seg.at(dest_segment).size()) {
// printf("HACK bad link ignored!\n");
// printf("HACK bad link ignored src %d, %d vs %d!\n", source_offset, dest_offset / 4,
// int(words_by_seg.at(dest_segment).size()));
return false;
}
ASSERT(dest_offset / 4 <= (int)words_by_seg.at(dest_segment).size());
@@ -55,11 +55,10 @@ struct LinkHeaderV5 {
uint32_t length_to_get_to_code; // 4 length.. of link data?
uint16_t version; // 8
uint16_t unknown; // 10
uint32_t pad; // 12
uint32_t length_to_get_to_link; // 12
uint32_t link_length; // 16
uint8_t n_segments; // 20
char name[59]; // 21 (really??)
SegmentInfo segment_info[3];
};
// The types of symbol links
@@ -421,72 +420,83 @@ static void link_v5(LinkedObjectFile& f,
const std::string& name,
DecompilerTypeSystem& dts) {
auto header = (const LinkHeaderV5*)(&data.at(0));
if (header->n_segments == 1) {
printf("abandon %s!\n", name.c_str());
return;
// for jak 3, both code and data use a "v5" format for linking.
// code has 3 segments (top-level, main, debug), and data has just 1.
// they appear to be generated by different programs, so there's some hard-coded checks for
// each.
// the "v5" format allows for multiple segments (like v3), "split-pointer" linking to support
// splitting a pointer link between a lui/ori instruction (needed for code), but uses "v2"
// symbol linking. For a reason that I don't understand, "v3" symlinks uses a less-space efficient
// encoding of large integers.
static_assert(0x50 == sizeof(LinkHeaderV5));
if (header->n_segments == 3) {
ASSERT(header->type_tag == 0);
ASSERT(name == header->name);
// the linker for code placed the link data at the beginning.
// but we expect the link data to start just after the object file header
ASSERT(header->length_to_get_to_link == sizeof(LinkHeaderV5));
// and then the code sould come after that
ASSERT(header->length_to_get_to_code == sizeof(LinkHeaderV5) + header->link_length);
} else if (header->n_segments == 1) {
ASSERT(header->type_tag == UINT32_MAX);
// name is inconsistent, so don't check is
// data files have the data first, which is good, as the last object in a DGO gets loaded
// directly to the heap, and putting the data first means that we can "free" the link data just
// by bumping the heap pointer back, rather than memcpy the code back to cover the hole if link
// data came first.
// the offset is always 0x80, which is bigger than the header, but is needed to make data
// aligned with the PS2's cache line size (64 bytes), which makes sense.
ASSERT(header->length_to_get_to_code == 0x80);
} else {
lg::die("bad segment count {}", header->n_segments);
}
ASSERT(header->type_tag == 0);
ASSERT(name == header->name);
ASSERT(header->n_segments == 3);
ASSERT(header->pad == 0x50);
ASSERT(header->length_to_get_to_code - header->link_length == 0x50);
f.set_segment_count(3);
// link v3's data size is data.size() - link_length
// link v5's data size is data.size() - new_link_length - 0x50.
// lbp + 4 points to version?
// lbp points to 4 past start of header.
// lbp[1] = version + unknown 16 bit thing.
// lbp[3] = link block length (minus 0x50)
f.set_segment_count(header->n_segments);
// todo - check this against the code size we actually got.
// size_t expected_code_size = data.size() - (header->link_length + 0x50);
uint32_t data_ptr_offset = header->length_to_get_to_code;
const int n_segs = header->n_segments;
// the first think in the link data is the segment info array, which we need to find stuff.
const SegmentInfo* seg_info_array =
(const SegmentInfo*)(data.data() + header->length_to_get_to_link);
// for convenience, we'll find the data/link offsets for each segment.
uint32_t segment_data_offsets[3];
uint32_t segment_link_offsets[3];
uint32_t segment_link_ends[3];
for (int i = 0; i < 3; i++) {
segment_data_offsets[i] = data_ptr_offset + header->segment_info[i].data;
segment_link_offsets[i] = header->segment_info[i].relocs + 0x50;
ASSERT(header->segment_info[i].magic == 1);
uint32_t segment_link_ends[3]; // set in linking, once we get to the end.
for (int i = 0; i < n_segs; i++) {
segment_data_offsets[i] = header->length_to_get_to_code + seg_info_array[i].data;
segment_link_offsets[i] = header->length_to_get_to_link + seg_info_array[i].relocs;
ASSERT(seg_info_array[i].magic == 1);
}
// check that the data region is filled
for (int i = 0; i < 2; i++) {
ASSERT(align16(segment_data_offsets[i] + header->segment_info[i].size) ==
for (int i = 0; i < n_segs - 1; i++) {
ASSERT(align16(segment_data_offsets[i] + seg_info_array[i].size) ==
segment_data_offsets[i + 1]);
}
ASSERT(align16(segment_data_offsets[2] + header->segment_info[2].size) == data.size());
if (n_segs == 3) {
ASSERT(align16(segment_data_offsets[2] + seg_info_array[2].size) == data.size());
}
// loop over segments (reverse order for now)
for (int seg_id = 3; seg_id-- > 0;) {
// ?? is this right?
if (header->segment_info[seg_id].size == 0)
// loop over segments
for (int seg_id = n_segs; seg_id-- > 0;) {
int segment_size = seg_info_array[seg_id].size;
if (segment_size == 0) {
continue;
}
auto segment_size = header->segment_info[seg_id].size;
f.stats.v3_code_bytes += segment_size;
// if(gGameVersion == JAK2) {
bool adjusted = false;
// the decompiler uses 4-byte words, so pad to 4-bytes.
while (segment_size % 4) {
segment_size++;
adjusted = true;
}
if (adjusted) {
printf(
"Adjusted the size of segment %d in %s, this is fine, but rare (and may indicate a "
"bigger problem if it happens often)\n",
seg_id, name.c_str());
}
// }
// set up pointers for linker.
auto base_ptr = segment_data_offsets[seg_id];
auto data_ptr = base_ptr - 4;
auto link_ptr = segment_link_offsets[seg_id];
@@ -494,13 +504,15 @@ static void link_v5(LinkedObjectFile& f,
ASSERT((data_ptr % 4) == 0);
ASSERT((segment_size % 4) == 0);
// add data to the decompiler.
auto code_start = (const uint32_t*)(&data.at(data_ptr + 4));
auto code_end = ((const uint32_t*)(&data.at(data_ptr + segment_size))) + 1;
for (auto x = code_start; x < code_end; x++) {
f.push_back_word_to_segment(*((const uint32_t*)x), seg_id);
}
bool fixing = false;
// pointer linking.
bool fixing = false;
if (data.at(link_ptr)) {
// we have pointers
while (true) {
@@ -517,7 +529,8 @@ static void link_v5(LinkedObjectFile& f,
if ((old_code >> 24) == 0) {
f.stats.v3_word_pointers++;
if (!f.pointer_link_word(seg_id, data_ptr - base_ptr, seg_id, old_code)) {
printf("WARNING bad pointer_link_word (2) in %s\n", name.c_str());
// the art groups just have bogus links. we ignored them in jak 2, so do the same
// here. The joint-anim-compressed-control's have a few bogus frames at the end.
}
} else {
f.stats.v3_split_pointers++;
@@ -526,12 +539,9 @@ static void link_v5(LinkedObjectFile& f,
ASSERT(lo_hi_offset);
ASSERT(dest_seg < 3);
auto offset_upper = old_code & 0xff;
// ASSERT(offset_upper == 0);
uint32_t low_code = *(const uint32_t*)(&data.at(data_ptr + 4 * lo_hi_offset));
uint32_t offset = low_code & 0xffff;
if (offset_upper) {
// seems to work fine, no need to warn.
// printf("WARNING - offset upper is set in %s\n", name.c_str());
offset += (offset_upper << 16);
}
f.pointer_link_split_word(seg_id, data_ptr - base_ptr,
@@ -558,6 +568,7 @@ static void link_v5(LinkedObjectFile& f,
}
link_ptr++;
// symbol linking.
if (data.at(link_ptr)) {
auto sub_link_ptr = link_ptr;
@@ -604,21 +615,23 @@ static void link_v5(LinkedObjectFile& f,
segment_link_ends[seg_id] = link_ptr;
}
ASSERT(segment_link_offsets[0] == 128);
if (n_segs == 3) {
ASSERT(segment_link_offsets[0] == 128);
if (header->segment_info[0].size) {
ASSERT(segment_link_ends[0] + 1 == segment_link_offsets[1]);
} else {
ASSERT(segment_link_offsets[0] + 2 == segment_link_offsets[1]);
if (seg_info_array[0].size) {
ASSERT(segment_link_ends[0] + 1 == segment_link_offsets[1]);
} else {
ASSERT(segment_link_offsets[0] + 2 == segment_link_offsets[1]);
}
if (seg_info_array[1].size) {
ASSERT(segment_link_ends[1] + 1 == segment_link_offsets[2]);
} else {
ASSERT(segment_link_offsets[1] + 2 == segment_link_offsets[2]);
}
ASSERT(align16(segment_link_ends[2] + 2) == segment_data_offsets[0]);
}
if (header->segment_info[1].size) {
ASSERT(segment_link_ends[1] + 1 == segment_link_offsets[2]);
} else {
ASSERT(segment_link_offsets[1] + 2 == segment_link_offsets[2]);
}
ASSERT(align16(segment_link_ends[2] + 2) == segment_data_offsets[0]);
}
static void link_v3(LinkedObjectFile& f,
@@ -677,10 +690,8 @@ static void link_v3(LinkedObjectFile& f,
}
if (game_version == GameVersion::Jak2) {
[[maybe_unused]] bool adjusted = false;
while (segment_size % 4) {
segment_size++;
adjusted = true;
}
}
+68 -12
View File
@@ -624,7 +624,9 @@ void ObjectFileDB::write_disassembly(const fs::path& output_dir,
std::string asm_functions;
for_each_obj([&](ObjectFileData& obj) {
if ((obj.obj_version == 3 && disassemble_code) || (obj.obj_version != 3 && disassemble_data)) {
if (((obj.obj_version == 3 || (obj.obj_version == 5 && obj.linked_data.has_any_functions())) &&
disassemble_code) ||
(obj.obj_version != 3 && disassemble_data)) {
auto file_text = obj.linked_data.print_disassembly(print_hex);
asm_functions += obj.linked_data.print_asm_function_disassembly(obj.to_unique_name());
auto file_name = output_dir / (obj.to_unique_name() + ".asm");
@@ -730,6 +732,9 @@ std::string ObjectFileDB::process_tpages(TextureDB& tex_db,
case GameVersion::Jak2:
animated_slots = jak2_animated_texture_slots();
break;
case GameVersion::Jak3:
animated_slots = jak3_animated_texture_slots();
break;
default:
ASSERT_NOT_REACHED();
}
@@ -873,8 +878,31 @@ std::string ObjectFileDB::process_game_count_file() {
}
namespace {
struct JointGeo {
u32 offset{};
std::string name;
u32 length{};
};
void get_joint_info(ObjectFileDB& db, ObjectFileData& obj, JointGeo jg) {
const auto& words = obj.linked_data.words_by_seg.at(MAIN_SEGMENT);
for (size_t i = 0; i < jg.length; ++i) {
u32 label = 0x0;
if (db.version() == GameVersion::Jak3) {
label = words.at((jg.offset / 4) + 11 + i).label_id();
} else {
label = words.at((jg.offset / 4) + 7 + i).label_id();
}
const auto& joint = obj.linked_data.labels.at(label);
const auto& name =
obj.linked_data.get_goal_string_by_label(words.at(joint.offset / 4).label_id());
// lg::print("{} joint idx {}/{}: {}\n", jg.name, i + 1, jg.length, name);
db.dts.add_joint_node(jg.name, name, i + 1);
}
}
void get_art_info(ObjectFileDB& db, ObjectFileData& obj) {
if (obj.obj_version == 4) {
if (obj.obj_version == 4 || (obj.obj_version == 5 && obj.linked_data.segments == 1)) {
const auto& words = obj.linked_data.words_by_seg.at(MAIN_SEGMENT);
if (words.at(0).kind() == LinkedWord::Kind::TYPE_PTR &&
words.at(0).symbol_name() == "art-group") {
@@ -908,6 +936,11 @@ void get_art_info(ObjectFileDB& db, ObjectFileData& obj) {
if (elt_type == "art-joint-geo") {
// the skeleton!
unique_name += "-jg";
JointGeo jg;
jg.offset = label.offset;
jg.name = unique_name;
jg.length = words.at(label.offset / 4 + 2).data;
get_joint_info(db, obj, jg);
} else if (elt_type == "merc-ctrl" || elt_type == "shadow-geo") {
// (maybe mesh-geo as well but that doesnt exist)
// the skin!
@@ -915,6 +948,9 @@ void get_art_info(ObjectFileDB& db, ObjectFileData& obj) {
} else if (elt_type == "art-joint-anim") {
// the animations!
unique_name += "-ja";
} else if (elt_type == "art-cloth-geo") {
// cloth geometry (jak 3)
unique_name += "-cg";
} else {
// the something idk!
throw std::runtime_error(
@@ -953,23 +989,40 @@ void ObjectFileDB::dump_art_info(const fs::path& output_dir) {
lg::info("Writing art group info...");
Timer timer;
if (!dts.art_group_info.empty()) {
if (!dts.art_group_info.empty() || !dts.jg_info.empty()) {
file_util::create_dir_if_needed(output_dir / "import");
}
auto ag_fpath = output_dir / "import" / "art-elts.gc";
std::string ag_result;
for (const auto& [ag_name, info] : dts.art_group_info) {
auto ag_fname = ag_name + ".gc";
auto filename = output_dir / "import" / ag_fname;
std::string result = ";;-*-Lisp-*-\n";
result += "(in-package goal)\n\n";
result += fmt::format(";; {} - art group OpenGOAL import file\n", ag_fname);
result += ";; THIS FILE IS AUTOMATICALLY GENERATED!\n\n";
// auto ag_fname = ag_name + ".gc";
// auto filename = output_dir / "import" / ag_fname;
// std::string result = ";;-*-Lisp-*-\n";
// result += "(in-package goal)\n\n";
// result += fmt::format(";; {} - art group OpenGOAL import file\n", ag_fname);
// result += ";; THIS FILE IS AUTOMATICALLY GENERATED!\n\n";
for (const auto& [idx, elt_name] : info) {
result += print_art_elt_for_dump(ag_name, elt_name, idx);
ag_result += print_art_elt_for_dump(ag_name, elt_name, idx);
}
result += "\n";
file_util::write_text_file(filename, result);
ag_result += "\n";
}
file_util::write_text_file(ag_fpath, ag_result);
auto jg_fpath = output_dir / "import" / "joint-nodes.gc";
std::string jg_result;
for (const auto& [jg_name, info] : dts.jg_info) {
for (const auto& [idx, joint] : info) {
jg_result += print_jg_for_dump(jg_name, joint, idx);
}
jg_result += "\n";
}
file_util::write_text_file(jg_fpath, jg_result);
lg::info("Written art group info: in {:.2f} ms", timer.getMs());
}
@@ -988,4 +1041,7 @@ std::string print_art_elt_for_dump(const std::string& group_name,
int idx) {
return fmt::format("(def-art-elt {} {} {})\n", group_name, name, idx);
}
std::string print_jg_for_dump(const std::string& jg_name, const std::string& joint_name, int idx) {
return fmt::format("(def-joint-node {} \"{}\" {})\n", jg_name, joint_name, idx);
}
} // namespace decompiler
+1
View File
@@ -395,4 +395,5 @@ class ObjectFileDB {
};
std::string print_art_elt_for_dump(const std::string& group_name, const std::string& name, int idx);
std::string print_jg_for_dump(const std::string& jg_name, const std::string& joint_name, int idx);
} // namespace decompiler
+76 -39
View File
@@ -321,7 +321,7 @@ void ObjectFileDB::ir2_analyze_all_types(const fs::path& output_file,
const std::unordered_set<std::string>& bad_types) {
std::vector<PerObjectAllTypeInfo> per_object;
DecompilerTypeSystem previous_game_ts(GameVersion::Jak1); // version here doesn't matter.
DecompilerTypeSystem previous_game_ts(GameVersion::Jak2); // version here doesn't matter.
if (previous_game_types) {
previous_game_ts.parse_type_defs({*previous_game_types});
}
@@ -329,44 +329,43 @@ void ObjectFileDB::ir2_analyze_all_types(const fs::path& output_file,
TypeInspectorCache ti_cache;
for_each_obj([&](ObjectFileData& data) {
if (data.obj_version != 3) {
return;
}
if (data.obj_version == 3 || (data.obj_version == 5 && data.linked_data.has_any_functions())) {
auto& object_result = per_object.emplace_back();
object_result.object_name = data.to_unique_name();
auto& object_result = per_object.emplace_back();
object_result.object_name = data.to_unique_name();
// Go through the top-level segment first to identify the type names associated with each
// symbol def
for_each_function_in_seg_in_obj(TOP_LEVEL_SEGMENT, data, [&](Function& f) {
inspect_top_level_for_metadata(f, data.linked_data, dts, previous_game_ts, object_result);
});
// Go through the top-level segment first to identify the type names associated with each symbol
// def
for_each_function_in_seg_in_obj(TOP_LEVEL_SEGMENT, data, [&](Function& f) {
inspect_top_level_for_metadata(f, data.linked_data, dts, previous_game_ts, object_result);
});
// Handle the top level last, which is fine as all symbol_defs are always written after typedefs
for_each_function_def_order_in_obj(data, [&](Function& f, int seg) {
if (seg != TOP_LEVEL_SEGMENT) {
if (f.is_inspect_method && bad_types.find(f.guessed_name.type_name) == bad_types.end()) {
auto deftype_from_inspect =
inspect_inspect_method(f, f.guessed_name.type_name, dts, data.linked_data,
previous_game_ts, ti_cache, object_result);
bool already_seen = object_result.type_info.count(f.guessed_name.type_name) > 0;
if (!already_seen) {
object_result.type_names_in_order.push_back(f.guessed_name.type_name);
// Handle the top level last, which is fine as all symbol_defs are always written after
// typedefs
for_each_function_def_order_in_obj(data, [&](Function& f, int seg) {
if (seg != TOP_LEVEL_SEGMENT) {
if (f.is_inspect_method && bad_types.find(f.guessed_name.type_name) == bad_types.end()) {
auto deftype_from_inspect =
inspect_inspect_method(f, f.guessed_name.type_name, dts, data.linked_data,
previous_game_ts, ti_cache, object_result);
bool already_seen = object_result.type_info.count(f.guessed_name.type_name) > 0;
if (!already_seen) {
object_result.type_names_in_order.push_back(f.guessed_name.type_name);
}
auto& info = object_result.type_info[f.guessed_name.type_name];
info.from_inspect_method = true;
info.type_definition = deftype_from_inspect;
} else {
// no inspect methods
// - can we solve custom print methods in a generic way? ie `entity-links`
}
auto& info = object_result.type_info[f.guessed_name.type_name];
info.from_inspect_method = true;
info.type_definition = deftype_from_inspect;
} else {
// no inspect methods
// - can we solve custom print methods in a generic way? ie `entity-links`
}
}
});
});
for_each_function_in_seg_in_obj(TOP_LEVEL_SEGMENT, data, [&](Function& f) {
object_result.symbol_defs += inspect_top_level_symbol_defines(
f, data.linked_data, dts, previous_game_ts, object_result);
});
for_each_function_in_seg_in_obj(TOP_LEVEL_SEGMENT, data, [&](Function& f) {
object_result.symbol_defs += inspect_top_level_symbol_defines(
f, data.linked_data, dts, previous_game_ts, object_result);
});
}
});
std::string result;
@@ -511,6 +510,20 @@ Value try_lookup(const std::unordered_map<Key, Value>& map, const Key& key) {
}
}
const std::string* find_file_override_for_art_group(const Config& config,
const std::string& obj_name,
const std::string& type_name) {
// find file override for this type
auto it_file = config.art_group_file_override.find(obj_name);
if (it_file != config.art_group_file_override.end()) {
auto it_type = it_file->second.find(type_name);
if (it_type != it_file->second.end()) {
return &it_type->second;
}
}
return nullptr;
}
/*!
* Analyze registers and determine the type in each register at each instruction.
* - Figure out the type of each function, from configs.
@@ -548,14 +561,38 @@ void ObjectFileDB::ir2_type_analysis_pass(int seg, const Config& config, ObjectF
func.ir2.env.set_stack_structure_hints(
try_lookup(config.stack_structure_hints_by_function, func_name));
if (config.art_groups_by_function.find(func_name) != config.art_groups_by_function.end()) {
func.ir2.env.set_art_group(config.art_groups_by_function.at(func_name));
} else if (config.art_groups_by_file.find(obj_name) != config.art_groups_by_file.end()) {
func.ir2.env.set_art_group(config.art_groups_by_file.at(obj_name));
if (func.guessed_name.kind == FunctionName::FunctionKind::V_STATE) {
if (config.art_group_type_remap.find(func.guessed_name.type_name) !=
config.art_group_type_remap.end()) {
auto ag_override =
find_file_override_for_art_group(config, obj_name, func.guessed_name.type_name);
func.ir2.env.set_art_group(
ag_override ? *ag_override
: config.art_group_type_remap.at(func.guessed_name.type_name));
} else {
func.ir2.env.set_art_group(func.guessed_name.type_name + "-ag");
}
} else if (func.guessed_name.kind == FunctionName::FunctionKind::NV_STATE ||
func.type.try_get_tag("behavior").has_value()) {
std::string type = func.type.get_tag("behavior");
if (config.art_group_type_remap.find(type) != config.art_group_type_remap.end()) {
auto ag_override = find_file_override_for_art_group(config, obj_name, type);
func.ir2.env.set_art_group(ag_override ? *ag_override
: config.art_group_type_remap.at(type));
} else {
func.ir2.env.set_art_group(type + "-ag");
}
} else {
func.ir2.env.set_art_group(obj_name + "-ag");
}
func.ir2.env.set_jg(func.ir2.env.art_group());
if (config.joint_node_hacks.find(func.ir2.env.art_group()) !=
config.joint_node_hacks.end()) {
func.ir2.env.set_jg(config.joint_node_hacks.at(func.ir2.env.art_group()));
}
constexpr bool kForceNewTypes = false;
if (config.game_version == GameVersion::Jak2 || kForceNewTypes) {
// use new types for jak 2 always
@@ -1162,7 +1199,7 @@ bool ObjectFileDB::lookup_function_type(const FunctionName& name,
std::string ObjectFileDB::ir2_final_out(ObjectFileData& data,
const std::vector<std::string>& imports,
const std::unordered_set<std::string>& skip_functions) {
if (data.obj_version == 3) {
if (data.obj_version == 3 || (data.obj_version == 5 && data.linked_data.has_any_functions())) {
std::string result;
result += ";;-*-Lisp-*-\n";
result += "(in-package goal)\n\n";
+454 -81
View File
@@ -5,6 +5,11 @@
namespace decompiler {
// types with duplicate inspects
static const std::vector<std::string> g_duplicate_inspects_jak3 = {
"sky-vertex", "shadow-edge", "hfrag-poly4", "hfrag-poly9",
"hfrag-poly25", "hfrag-mip-packet", "sprite-aux-list", "game-save"};
bool is_set_reg_to_int(AtomicOp* op, Register dst, s64 value) {
// should be a set reg to int math 2 ir
auto set = dynamic_cast<SetVarOp*>(op);
@@ -186,33 +191,63 @@ std::optional<u64> get_set_reg_to_u64_load(AtomicOp* op,
const LinkedObjectFile& file) {
auto lvo = dynamic_cast<LoadVarOp*>(op);
if (!lvo) {
return false;
return std::nullopt;
}
// destination should be a register
auto dest = lvo->get_set_destination();
if (dst != dest.reg()) {
return false;
return std::nullopt;
}
if (lvo->src().kind() != SimpleExpression::Kind::IDENTITY) {
return false;
return std::nullopt;
}
if (lvo->size() != 8) {
return false;
return std::nullopt;
}
const auto& s = lvo->src().get_arg(0);
if (!s.is_label()) {
return false;
return std::nullopt;
}
auto lab = file.labels.at(s.label());
auto& low = file.words_by_seg.at(lab.target_segment).at(lab.offset / 4);
auto& hi = file.words_by_seg.at(lab.target_segment).at((lab.offset / 4) + 1);
if (low.kind() != LinkedWord::PLAIN_DATA || hi.kind() != LinkedWord::PLAIN_DATA) {
return false;
return std::nullopt;
}
return ((u64)low.data) | (((u64)hi.data) << 32);
}
std::optional<u64> get_set_reg_to_lui(AtomicOp* op, Register dst, const LinkedObjectFile& file) {
auto lvo = dynamic_cast<SetVarOp*>(op);
if (!lvo) {
return std::nullopt;
}
// destination should be a register
auto dest = lvo->get_set_destination();
if (dst != dest.reg()) {
return std::nullopt;
}
if (lvo->src().kind() != SimpleExpression::Kind::IDENTITY) {
return std::nullopt;
}
const auto& s = lvo->src().get_arg(0);
if (!s.is_label()) {
return std::nullopt;
}
auto lab = file.labels.at(s.label());
auto& low = file.words_by_seg.at(lab.target_segment).at(lab.offset / 4);
auto& hi = file.words_by_seg.at(lab.target_segment).at((lab.offset / 4) + 1);
if (low.kind() != LinkedWord::PLAIN_DATA || hi.kind() != LinkedWord::PLAIN_DATA) {
return std::nullopt;
}
return ((u64)low.data) | (((u64)hi.data) << 32);
}
@@ -246,13 +281,23 @@ std::optional<std::string> get_string_loaded_to_reg(AtomicOp* op,
}
struct FieldPrint {
static constexpr int NO_ARR = -1;
static constexpr int DYNAMIC_ARRAY = -2;
static constexpr int UNKNOWN_ARR_SIZE = -3;
char format = '\0';
std::string field_name;
std::string field_type_name;
bool has_array = false;
int array_size = -1;
int array_size = NO_ARR;
};
// if a field has a weird inspect, just return the FieldPrint instead of asserting,
// there's too many edge cases in custom prints to account for all of them
FieldPrint handle_custom_prints(FieldPrint& fp, const std::string& /*str*/) {
return fp;
}
FieldPrint get_field_print(const std::string& str) {
int idx = 0;
auto next = [&]() { return str.at(idx++); };
@@ -272,7 +317,10 @@ FieldPrint get_field_print(const std::string& str) {
// next the name:
char name_char = next();
while (name_char != ':' && name_char != '[') {
if (name_char == '~') {
return handle_custom_prints(field_print, str);
}
while (name_char != ':' && name_char != '[' && name_char != ' ') {
field_print.field_name.push_back(name_char);
name_char = next();
}
@@ -281,6 +329,18 @@ FieldPrint get_field_print(const std::string& str) {
if (name_char == '[') {
int size = 0;
char num_char = next();
// dynamic array using a ~D print
// (format "~Tstack[~D] @ #x~X~%" (-> obj allocated-length) (-> obj stack))
if (num_char == '~') {
num_char = next();
ASSERT(num_char == 'D');
num_char = next();
ASSERT(num_char == ']');
// distinguish from dynamic arrays that are set to size 0
field_print.array_size = size = FieldPrint::DYNAMIC_ARRAY;
}
while (num_char >= '0' && num_char <= '9') {
size = size * 10 + (num_char - '0');
num_char = next();
@@ -290,30 +350,68 @@ FieldPrint get_field_print(const std::string& str) {
ASSERT(num_char == ']');
char c = next();
ASSERT(c == ' ');
// (method 3 array) and some others have a colon instead of a space here
if (c == ':') {
c = next();
}
if (c != ' ') {
return handle_custom_prints(field_print, str);
}
c = next();
ASSERT(c == '@');
if (c != '@') {
return handle_custom_prints(field_print, str);
}
c = next();
ASSERT(c == ' ');
if (c != ' ') {
return handle_custom_prints(field_print, str);
}
c = next();
ASSERT(c == '#');
if (c != '#') {
return handle_custom_prints(field_print, str);
}
c = next();
ASSERT(c == 'x');
if (c != 'x') {
return handle_custom_prints(field_print, str);
}
} else {
// next a space
char space_char = next();
ASSERT(space_char == ' ');
if (space_char != ' ') {
return handle_custom_prints(field_print, str);
}
}
// next the format
char fmt1 = next();
// if there are extra spaces
if (fmt1 == ' ') {
while (fmt1 == ' ') {
fmt1 = next();
}
}
if (fmt1 == '~' && peek(0) != '`') { // normal ~_~%
char fmt_code = next();
field_print.format = fmt_code;
char end1 = next();
ASSERT(end1 == '~');
if (end1 != '~') {
return handle_custom_prints(field_print, str);
}
char end2 = next();
ASSERT(end2 == '%');
if (end2 != '%') {
return handle_custom_prints(field_print, str);
}
ASSERT(idx == (int)str.size());
} else if (fmt1 == '~' && (peek(0) == 'g' || peek(0) == 'G')) { // ~g~%
char fmt_code = next();
field_print.format = fmt_code;
char end1 = next();
if (end1 != '~') {
return handle_custom_prints(field_print, str);
}
char end2 = next();
if (end2 != '%') {
return handle_custom_prints(field_print, str);
}
ASSERT(idx == (int)str.size());
} else if (fmt1 == '#' && peek(0) == '<') { // struct #<my-struct @ #x~X>~%
next();
@@ -331,6 +429,30 @@ FieldPrint get_field_print(const std::string& str) {
field_print.format = 'X';
ASSERT(idx == (int)str.size());
} else if (fmt1 == '#' && peek(4) == ':') { // #x~X : (enum-name
// OR
// #x~X : ~S~%
if (peek(6) != '(' && peek(7) == 'S') {
next();
std::string expect_end = "~X : ~S~%";
for (char i : expect_end) {
char c = next();
ASSERT(i == c);
}
field_print.format = 'X';
} else {
// skip to paren
for (int i = 0; i < 7; i++) {
next();
}
auto name = str.substr(idx);
// some of these don't have the enum name
if (!name.empty()) {
name.pop_back();
field_print.field_type_name = name;
}
field_print.format = 'X';
}
} else if (fmt1 == '#' && peek(0) == 'x') { // #x~X~%
next();
std::string expect_end = "~X~%";
@@ -364,17 +486,31 @@ FieldPrint get_field_print(const std::string& str) {
}
else {
throw std::runtime_error("other format nyi in get_field_print " + str.substr(idx));
// throw std::runtime_error("other format nyi in get_field_print " + str.substr(idx));
lg::print("other format nyi in get_field_print {}\n", str.substr(idx));
}
return field_print;
}
int get_start_idx_process(Function& function, const std::string& parent_type, Env& env) {
int get_start_idx_process(Function& function,
const std::string& parent_type,
Env& env,
TypeInspectorResult* result) {
// hack
if (function.name() == "(method 3 process-tree)" || function.name() == "(method 3 process)") {
result->is_basic = true;
return 7;
}
if (parent_type == "process-focusable") {
result->is_basic = true;
}
if (function.basic_blocks.size() != 5) {
lg::print("[iim] inspect {} had {} basic blocks, expected 5\n", function.name(),
function.basic_blocks.size());
return -1;
return 1;
}
if (!function.ir2.atomic_ops) {
@@ -475,12 +611,27 @@ L2:
}
op_idx++;
if (!is_set_reg_to_symbol_value(aos.ops.at(op_idx).get(), Register(Reg::GPR, Reg::V1),
parent_type)) {
lg::print("[iim] op5 bad in {}: {} (bad set parent type)\n", aos.ops.at(op_idx)->to_string(env),
function.name());
return -1;
// try to determine parent type for far labels
if (parent_type == "UNKNOWN") {
auto parent_op_str = aos.ops.at(op_idx)->to_string(env);
auto parent_type_str = parent_op_str.substr(9);
parent_type_str.pop_back();
result->parent_type_name = parent_type_str;
} else {
if (!is_set_reg_to_symbol_value(aos.ops.at(op_idx).get(), Register(Reg::GPR, Reg::V1),
parent_type)) {
lg::print("[iim] op5 bad in {}: {} (bad set parent type)\n",
aos.ops.at(op_idx)->to_string(env), function.name());
return -1;
}
}
// TODO check if this catches all cases or if there are false positives
// hack to get correct field offsets for children of process
if (result->parent_type_name != "structure") {
result->is_basic = true;
}
op_idx++;
if (aos.ops.at(op_idx).get()->to_string(env) != "(set! t9 (l.wu (+ v1 28)))") {
@@ -536,12 +687,15 @@ L3:
int get_start_idx(Function& function,
LinkedObjectFile& file,
TypeInspectorResult* result,
const std::string& /*parent_type*/,
const std::string& parent_type,
const std::string& type_name,
Env& env) {
if (function.basic_blocks.size() != 5) {
lg::print("[iim] inspect {} had {} basic blocks, expected 5\n", function.name(),
function.basic_blocks.size());
if (parent_type == "basic") {
result->is_basic = true;
}
return -1;
}
@@ -673,7 +827,9 @@ int get_start_idx(Function& function,
if (is_set_reg_to_symbol_ptr(aos.ops.at(op_idx).get(), Register(Reg::GPR, Reg::A3), type_name)) {
result->is_basic = false;
} else if (aos.ops.at(op_idx)->to_string(env) == "(set! a3 (l.wu (+ gp -4)))") {
} else if (aos.ops.at(op_idx)->to_string(env) == "(set! a3 (l.wu (+ gp -4)))" ||
aos.ops.at(op_idx)->to_string(env) == "(set! a3-0 (l.wu (+ a0-0 -4)))" ||
aos.ops.at(op_idx)->to_string(env) == "(set! a3-0 (l.wu (+ obj -4)))") {
result->is_basic = true;
} else {
lg::print("[iim] op 9 bad in {}: {}\n", aos.ops.at(op_idx)->to_string(env), function.name());
@@ -877,9 +1033,24 @@ int identify_array_field(int idx,
Function& function,
TypeInspectorResult* result,
FieldPrint& print_info) {
auto& get_op = function.ir2.atomic_ops->ops.at(idx++);
AtomicOp* get_op;
// dynamic array with ~D inspect print
if (print_info.array_size == FieldPrint::DYNAMIC_ARRAY) {
idx++;
get_op = function.ir2.atomic_ops->ops.at(idx).get();
} else {
get_op = function.ir2.atomic_ops->ops.at(idx++).get();
}
int offset = 0;
if (!get_ptr_offset(get_op.get(), make_gpr(Reg::A2), make_gpr(Reg::GP), &offset)) {
bool ptr;
if (print_info.array_size == FieldPrint::DYNAMIC_ARRAY) {
ptr = get_ptr_offset(get_op, make_gpr(Reg::A3), make_gpr(Reg::GP), &offset);
} else {
ptr = get_ptr_offset(get_op, make_gpr(Reg::A2), make_gpr(Reg::GP), &offset);
}
if (!ptr) {
printf("bad get ptr offset %s\n", get_op->to_string(function.ir2.env).c_str());
ASSERT(false);
}
@@ -888,7 +1059,7 @@ int identify_array_field(int idx,
}
Field field(print_info.field_name, TypeSpec("UNKNOWN"), offset);
if (print_info.array_size) {
if (print_info.array_size > 0) {
field.set_array(print_info.array_size);
} else {
field.set_dynamic();
@@ -904,7 +1075,7 @@ int identify_struct_not_inline_field(int idx,
auto load_info = get_load_info_from_set(function.ir2.atomic_ops->ops.at(idx++).get());
if (!(load_info.size == 4 && load_info.kind == LoadVarOp::Kind::UNSIGNED)) {
result->warnings += "field " + print_info.field_type_name + " is likely a value type";
result->warnings += "field " + print_info.field_type_name + " is likely a value type. ";
}
int offset = load_info.offset;
if (result->is_basic) {
@@ -924,7 +1095,7 @@ int identify_struct_inline_field(int idx,
int offset = 0;
if (!get_ptr_offset(get_op.get(), make_gpr(Reg::A2), make_gpr(Reg::GP), &offset)) {
printf("bad get ptr offset %s\n", get_op->to_string(function.ir2.env).c_str());
ASSERT(false);
// ASSERT(false);
}
if (result->is_basic) {
offset += BASIC_OFFSET;
@@ -946,12 +1117,12 @@ int identify_basic_field(int idx,
ASSERT(load_info.kind == LoadVarOp::Kind::UNSIGNED || load_info.kind == LoadVarOp::Kind::SIGNED);
TypeSpec field_type("basic");
if (load_info.size == 8) {
result->warnings += "field " + print_info.field_name + " uses ~A with a 64-bit load ";
result->warnings += "field " + print_info.field_name + " uses ~A with a 64-bit load. ";
field_type = TypeSpec("uint64");
} else if (load_info.size == 4) {
// I wonder if this actually "object", or some other type? It seems to be
if (load_info.kind == LoadVarOp::Kind::SIGNED) {
result->warnings += "field " + print_info.field_name + " uses ~A with a signed load ";
result->warnings += "field " + print_info.field_name + " uses ~A with a signed load. ";
}
} else {
ASSERT(false);
@@ -967,9 +1138,79 @@ int identify_basic_field(int idx,
return idx;
}
int identify_string_field(int idx,
Function& function,
LinkedObjectFile& file,
TypeInspectorResult* result,
FieldPrint& print_info) {
(void)file;
auto load_info = get_load_info_from_set(function.ir2.atomic_ops->ops.at(idx++).get());
ASSERT(load_info.kind == LoadVarOp::Kind::UNSIGNED || load_info.kind == LoadVarOp::Kind::SIGNED);
TypeSpec field_type("string");
if (load_info.size == 8) {
result->warnings += "field " + print_info.field_name + " uses ~S with a 64-bit load. ";
field_type = TypeSpec("uint64");
} else if (load_info.size == 4) {
// I wonder if this actually "object", or some other type? It seems to be
if (load_info.kind == LoadVarOp::Kind::SIGNED) {
result->warnings += "field " + print_info.field_name + " uses ~S with a signed load. ";
}
} else {
ASSERT(false);
}
int offset = load_info.offset;
if (result->is_basic) {
offset += BASIC_OFFSET;
}
Field field(print_info.field_name, field_type, offset);
result->fields_of_type.push_back(field);
return idx;
}
int identify_cstring_field(int idx,
Function& function,
LinkedObjectFile& file,
TypeInspectorResult* result,
FieldPrint& print_info) {
(void)file;
auto& get_op = function.ir2.atomic_ops->ops.at(idx++);
// assuming unknown array size at first
int size = FieldPrint::UNKNOWN_ARR_SIZE;
int offset = 0;
std::string comment;
// usually either a daddiu or lq
if (!get_ptr_offset(get_op.get(), make_gpr(Reg::A2), make_gpr(Reg::GP), &offset)) {
// daddiu failed, try lq
auto load_info = get_load_info_from_set(get_op.get());
if (load_info.size == 16) {
size = 16;
offset = load_info.offset;
comment = "field uses ~g print with a quadword load!";
} else {
printf("bad get ptr offset %s\n", get_op->to_string(function.ir2.env).c_str());
ASSERT(false);
}
}
if (result->is_basic) {
offset += BASIC_OFFSET;
}
Field field(print_info.field_name, TypeSpec("uint8"), offset);
field.set_array(size);
if (!comment.empty()) {
field.set_comment(comment);
}
result->fields_of_type.push_back(field);
return idx;
}
int detect(int idx, Function& function, LinkedObjectFile& file, TypeInspectorResult* result) {
auto& get_format_op = function.ir2.atomic_ops->ops.at(idx++);
if (!is_set_reg_to_symbol_value(get_format_op.get(), make_gpr(Reg::T9), "format")) {
return idx;
ASSERT_MSG(false,
fmt::format("bad get format: {}\n", get_format_op->to_string(function.ir2.env)));
}
@@ -985,13 +1226,35 @@ int detect(int idx, Function& function, LinkedObjectFile& file, TypeInspectorRes
ASSERT_MSG(false, "bad sstr");
}
// hack to ignore format print from enum->string and other unexpected stuff
if (sstr->substr(0, 1) != "~" || sstr == "~T [~D]~2Tactor-group: ~`actor-group`P~%" ||
sstr == "~T [~D]~2Tbuffer: ~A~%") {
return idx;
}
auto info = get_field_print(*sstr);
auto& first_get_op = function.ir2.atomic_ops->ops.at(idx);
if (is_get_load(first_get_op.get(), make_gpr(Reg::A2), make_gpr(Reg::GP)) &&
(info.format == 'D' || info.format == 'X' || info.format == 'e') && !info.has_array &&
info.field_type_name.empty()) {
// v1 load (process pointer):
// lw t9, format(s7)
// daddiu a0, s7, #t
// daddiu a1, fp, L389
// lwu v1, 12(gp)
// beq s7, v1, L281
// or a2, s7, r0
// B1:
// lwu v1, 0(v1)
// lwu a2, 28(v1)
// B2:
// L281:
// jalr ra, t9
auto load_a2_gp = is_get_load(first_get_op.get(), make_gpr(Reg::A2), make_gpr(Reg::GP));
auto load_v1_gp = is_get_load(first_get_op.get(), make_gpr(Reg::V1), make_gpr(Reg::GP));
if ((load_a2_gp || load_v1_gp) &&
(info.format == 'D' || info.format == 'd' || info.format == 'X' || info.format == 'e') &&
!info.has_array && info.field_type_name.empty()) {
idx = identify_int_field(idx, function, result, info);
// it's a load!
} else if (is_get_load(first_get_op.get(), make_fpr(0), make_gpr(Reg::GP)) &&
@@ -999,11 +1262,17 @@ int detect(int idx, Function& function, LinkedObjectFile& file, TypeInspectorRes
info.format == 'X') &&
!info.has_array && info.field_type_name.empty()) {
idx = identify_float_field(idx, function, result, info);
} else if (is_get_load(first_get_op.get(), make_gpr(Reg::A2), make_gpr(Reg::GP)) &&
info.format == 'A' && !info.has_array && info.field_type_name.empty()) {
} else if ((load_a2_gp || load_v1_gp) && info.format == 'A' && !info.has_array &&
info.field_type_name.empty()) {
idx = identify_basic_field(idx, function, file, result, info);
} else if (is_get_load(first_get_op.get(), make_gpr(Reg::A2), make_gpr(Reg::GP)) &&
info.format == 'X' && !info.has_array && info.field_type_name.empty()) {
} else if ((load_a2_gp || load_v1_gp) && info.format == 'S' && !info.has_array &&
info.field_type_name.empty()) {
idx = identify_string_field(idx, function, file, result, info);
} else if ((load_a2_gp || load_v1_gp) && (info.format == 'G' || info.format == 'g') &&
!info.has_array && info.field_type_name.empty()) {
idx = identify_cstring_field(idx, function, file, result, info);
} else if ((load_a2_gp || load_v1_gp) && info.format == 'X' && !info.has_array &&
info.field_type_name.empty()) {
idx = identify_pointer_field(idx, function, result, info);
} else if (info.has_array && (info.format == 'X' || info.format == 'P') &&
info.field_type_name.empty()) {
@@ -1020,14 +1289,51 @@ int detect(int idx, Function& function, LinkedObjectFile& file, TypeInspectorRes
}
else {
printf("couldn't do %s, %s\n", sstr->c_str(),
printf("couldn't do %s, %s, adding unknown field\n", sstr->c_str(),
first_get_op->to_string(function.ir2.env).c_str());
return -1;
// if all else fails, create an unknown field so the rest of the inspect can pass.
Field unknown("UNKNOWN", TypeSpec("UNKNOWN"), -1);
unknown.set_comment("field could not be read.");
result->fields_of_type.push_back(unknown);
return idx;
}
if (!dynamic_cast<CallOp*>(function.ir2.atomic_ops->ops.at(idx++).get())) {
CallOp* call_op;
if (load_v1_gp) {
call_op = dynamic_cast<CallOp*>(function.ir2.atomic_ops->ops.at(idx = idx + 3).get());
} else {
// dynamic array with ~D inspect print
if (info.array_size == FieldPrint::DYNAMIC_ARRAY) {
idx++;
call_op = dynamic_cast<CallOp*>(function.ir2.atomic_ops->ops.at(idx++).get());
} else {
call_op = dynamic_cast<CallOp*>(function.ir2.atomic_ops->ops.at(idx++).get());
}
}
// inspect strings like "#x~X : ~S~%" load the field twice, once into a2, then into v1,
// then have a bunch of string branches, so we just skip this, since we already have the field.
//
// lw t9, format(s7) ;; [218] (set! t9 format)
// daddiu a0, s7, #t ;; [219] (set! a0 #t)
// daddiu a1, fp, L503 ;; [220] (set! a1 L503) "~1Tclass: #x~X : ~S~%"
// lhu a2, 26(gp) ;; [221] (set! a2 (l.hu (+ gp 26)))
// lhu v1, 26(gp) ;; [222] (set! v1 (l.hu (+ gp 26)))
// addiu a3, r0, 149 ;; [223] (set! a3 149)
// bne v1, a3, L70 ;; [224] (b! (!= v1 a3) L70 (nop!))
// sll r0, r0, 0
if (load_a2_gp) {
auto load_v1_gp_again = is_get_load(function.ir2.atomic_ops->ops.at(idx - 1).get(),
make_gpr(Reg::V1), make_gpr(Reg::GP));
if (load_v1_gp_again) {
return idx;
}
}
if (!call_op) {
printf("bad call\n");
ASSERT(false);
// ASSERT(false);
return -1;
}
@@ -1053,6 +1359,13 @@ std::string inspect_inspect_method(Function& inspect_method,
result.type_size = flags.size;
result.type_method_count = flags.methods;
// ignore duplicate inspects
if (ti_cache.previous_results.find(type_name) != ti_cache.previous_results.end() &&
!(std::find(g_duplicate_inspects_jak3.begin(), g_duplicate_inspects_jak3.end(), type_name) !=
g_duplicate_inspects_jak3.end())) {
return fmt::format(";; {} is already defined!\n", type_name);
}
// Only set heap-base if it's different from the automatic one
// A child (or child of a child) of process ALWAYS has heap-base set.
if (flags.heap_base > 0) {
@@ -1081,7 +1394,8 @@ std::string inspect_inspect_method(Function& inspect_method,
inspect_method.ir2.env);
if (idx < 0) {
idx = get_start_idx_process(inspect_method, result.parent_type_name, inspect_method.ir2.env);
idx = get_start_idx_process(inspect_method, result.parent_type_name, inspect_method.ir2.env,
&result);
}
StructureType* old_game_type = nullptr;
if (previous_game_ts.ts.fully_defined_type_exists(type_name)) {
@@ -1096,6 +1410,12 @@ std::string inspect_inspect_method(Function& inspect_method,
object_file_meta);
}
while (idx < int(inspect_method.ir2.atomic_ops->ops.size()) - 2 && idx != -1) {
// skip over non-format calls in inspects
auto sstr = inspect_method.ir2.atomic_ops->ops.at(idx)->to_string(inspect_method.ir2.env);
if (sstr.substr(sstr.size() - 7) != "format)") {
idx++;
continue;
}
idx = detect(idx, inspect_method, file, &result);
}
@@ -1158,7 +1478,7 @@ bool allow_guess(const Field& field) {
std::string TypeInspectorResult::print_as_deftype(
StructureType* old_game_type,
std::unordered_map<std::string, TypeInspectorResult>& previous_results,
DecompilerTypeSystem& /*previous_game_ts*/,
DecompilerTypeSystem& previous_game_ts,
ObjectFileDB::PerObjectAllTypeInfo& object_file_meta) {
std::string result;
@@ -1216,7 +1536,12 @@ std::string TypeInspectorResult::print_as_deftype(
int mods = 0;
// mods are array size, :inline, :dynamic
if (field.is_array() && !field.is_dynamic()) {
mods += std::to_string(field.array_size()).size();
// "??" for unknown array size
if (field.array_size() == FieldPrint::UNKNOWN_ARR_SIZE) {
mods += 2;
} else {
mods += std::to_string(field.array_size()).size();
}
}
if (field.is_inline()) {
@@ -1248,8 +1573,13 @@ std::string TypeInspectorResult::print_as_deftype(
std::string mods;
if (field.is_array() && !field.is_dynamic()) {
mods += std::to_string(field.array_size());
mods += " ";
if (field.array_size() == FieldPrint::UNKNOWN_ARR_SIZE) {
mods += "??";
mods += " ";
} else {
mods += std::to_string(field.array_size());
mods += " ";
}
}
if (field.is_inline()) {
@@ -1284,6 +1614,9 @@ std::string TypeInspectorResult::print_as_deftype(
}
}
}
if (field.has_comment()) {
result += fmt::format(" ;; {}", field.comment());
}
if (was_guess[field_idx]) {
result += " ;; guessed by decompiler";
@@ -1310,56 +1643,84 @@ std::string TypeInspectorResult::print_as_deftype(
}
if (type_method_count > 9) {
result.append("(:methods\n ");
std::string methods_list;
std::string state_methods_list;
MethodInfo old_new_method;
if (old_game_type && old_game_type->get_my_new_method(&old_new_method)) {
result.append(old_method_string(old_new_method));
result.append("\n ");
methods_list.append(" ");
methods_list.append(old_method_string(old_new_method));
methods_list.push_back('\n');
}
bool done_with_state_methods = false;
for (int i = parent_method_count; i < type_method_count; i++) {
// If the method is actually a state, skip it!
bool print_as_state_method = false;
if (method_states.count(i) != 0) {
result.append(fmt::format("({} () _type_ :state {})", method_states.at(i), i));
if (!done_with_state_methods) {
print_as_state_method = true;
state_methods_list.append(fmt::format(" {}", method_states.at(i)));
} else {
methods_list.append(
fmt::format(" ({} () _type_ :state) ;; {}", method_states.at(i), i));
}
} else {
result.append(fmt::format("({}-method-{} () none {})", type_name, i, i));
done_with_state_methods = true;
methods_list.append(fmt::format(" ({}-method-{} () none) ;; {}", type_name, i, i));
}
if (old_game_type) {
MethodInfo info;
if (old_game_type->get_my_method(i, &info)) {
result += old_method_string(info);
if (print_as_state_method) {
state_methods_list += old_method_string(info);
} else {
methods_list += old_method_string(info);
}
}
}
if (print_as_state_method) {
state_methods_list.push_back('\n');
} else {
methods_list.push_back('\n');
}
}
if (!state_methods_list.empty()) {
result.append("(:state-methods\n");
result.append(state_methods_list);
result.append(" )\n ");
}
if (!methods_list.empty()) {
result.append("(:methods");
result.append(methods_list);
result.append(" )\n ");
}
}
// Print out (normal) states if we have em
// - Could probably assume the process name comes first and associate it with the right type
// but that may or may not be risky so, edit the types yourself...
if (method_states.size() > 0) {
result.append("(:states\n ");
for (const auto& [id, name] : method_states) {
result.append(name);
// Append old symbol def if we have it
auto it = previous_game_ts.symbol_types.find(name);
if (it != previous_game_ts.symbol_types.end()) {
result.append(fmt::format(" ;; {}", it->second.print()));
}
// Add symbol name to `already_seen_symbols`
object_file_meta.already_seen_symbols.insert(name);
result.append("\n ");
}
result.append(")\n ");
}
// Print out states if we have em
// - Could probably assume the process name comes first and associate it with the right type
// but that may or may not be risky so, edit the types yourself...
// if (method_states.size() > 0) {
// result.append("(:states\n ");
// for (const auto& [id, name] : method_states) {
// result.append(name);
// // Append old symbol def if we have it
// auto it = previous_game_ts.symbol_types.find(name);
// if (it != previous_game_ts.symbol_types.end()) {
// result.append(fmt::format(" ;; {}", it->second.print()));
// }
// // Add symbol name to `already_seen_symbols`
// object_file_meta.already_seen_symbols.insert(name);
// result.append("\n ");
// }
// result.append(")\n ");
//}
result.append(")\n");
result += "|#\n";
return result;
}
std::string get_regex_match(std::string form, std::regex regex) {
std::string get_regex_match(const std::string& form, const std::regex& regex) {
std::smatch matches;
if (std::regex_search(form, matches, regex)) {
if (matches.size() == 2) {
@@ -1369,7 +1730,7 @@ std::string get_regex_match(std::string form, std::regex regex) {
return "";
}
std::string get_state_symbol_name(LinkedObjectFile& file, std::string label_name) {
std::string get_state_symbol_name(LinkedObjectFile& file, const std::string& label_name) {
try {
auto& label = file.get_label_by_name(label_name);
auto& label_words = file.words_by_seg.at(label.target_segment);
@@ -1391,7 +1752,7 @@ std::string get_state_symbol_name(LinkedObjectFile& file, std::string label_name
}
}
std::string get_label_type_name(LinkedObjectFile& file, std::string label_name) {
std::string get_label_type_name(LinkedObjectFile& file, const std::string& label_name) {
try {
auto& label = file.get_label_by_name(label_name);
auto& label_words = file.words_by_seg.at(label.target_segment);
@@ -1433,7 +1794,7 @@ void inspect_top_level_for_metadata(Function& top_level,
}
// Check for non-method states
std::string last_seen_label = "";
std::string last_seen_label;
// TODO - safely increment op number
for (int i = 0; i < (int)top_level.ir2.atomic_ops->ops.size(); i++) {
const auto& aop = top_level.ir2.atomic_ops->ops.at(i);
@@ -1521,13 +1882,25 @@ void inspect_top_level_for_metadata(Function& top_level,
const auto& aop_4 = top_level.ir2.atomic_ops->ops.at(i + 4);
auto flags = get_set_reg_to_u64_load(aop_4.get(), Register(Reg::GPR, Reg::A2), file);
if (!flags) {
continue;
// far label load
// lui v1, L1352 ;; [ 24] (set! v1-10 L1352)
// ori v1, v1, L1352
// addu v1, fp, v1
// ld a2, 0(v1) ;; [ 25] (set! a2-0 (l.d v1-10))
flags = get_set_reg_to_lui(aop_4.get(), Register(Reg::GPR, Reg::V1), file);
if (!flags) {
continue;
}
}
// jalr ra, t9 ;; [ 25] (call! a0-0 a1-0 a2-0)
const auto& aop_5 = top_level.ir2.atomic_ops->ops.at(i + 5);
if (!dynamic_cast<CallOp*>(aop_5.get())) {
continue;
// far labels
const auto& aop_6 = top_level.ir2.atomic_ops->ops.at(i + 6);
if (!dynamic_cast<CallOp*>(aop_6.get())) {
continue;
}
}
if (objectFile.type_info.count(*type_name) == 0) {
@@ -364,6 +364,7 @@ std::unique_ptr<AtomicOp> make_asm_op(const Instruction& i0, int idx) {
case InstructionKind::PMADDH:
case InstructionKind::PMULTH:
case InstructionKind::PEXEW:
case InstructionKind::PEXCW:
case InstructionKind::PNOR:
case InstructionKind::PCPYH:
case InstructionKind::PINTEH:
@@ -1795,6 +1796,7 @@ std::unique_ptr<AtomicOp> convert_5(const Instruction& i0,
process_offset = 44;
break;
case GameVersion::Jak2:
case GameVersion::Jak3:
process_offset = 48;
break;
default:
+1
View File
@@ -1569,6 +1569,7 @@ Form* try_sc_as_type_of(FormPool& pool, Function& f, const ShortCircuit* vtx, Ga
case GameVersion::Jak1:
return try_sc_as_type_of_jak1(pool, f, vtx);
case GameVersion::Jak2:
case GameVersion::Jak3:
return try_sc_as_type_of_jak2(pool, f, vtx);
default:
ASSERT(false);
+3 -1
View File
@@ -178,7 +178,9 @@ std::string final_defun_out(const Function& func,
auto method_info =
dts.ts.lookup_method(func.guessed_name.type_name, func.guessed_name.method_id);
top.push_back(pretty_print::to_symbol(method_info.name));
top.push_back(pretty_print::to_symbol(func.guessed_name.type_name));
if (method_info.name == "new") {
top.push_back(pretty_print::to_symbol(func.guessed_name.type_name));
}
top.push_back(arguments);
auto top_form = pretty_print::build_list(top);
+1 -1
View File
@@ -1281,7 +1281,7 @@ FormElement* rewrite_joint_macro(LetElement* in, const Env& env, FormPool& pool)
}
auto group_lisp = strip_cast("art-joint-anim", arg_group)->to_form(env);
if (group_lisp.is_symbol() && group_lisp.as_symbol()->name == "#f") {
if (group_lisp.is_symbol() && group_lisp.as_symbol() == "#f") {
arg_group = nullptr;
}
+2
View File
@@ -1176,6 +1176,8 @@ Mips2C_Line handle_normal_instr(Mips2C_Output& output,
return handle_generic_op2(i0, instr_str, "cvtsw");
case InstructionKind::PEXEW:
return handle_generic_op2(i0, instr_str, "pexew");
case InstructionKind::PEXCW:
return handle_generic_op2(i0, instr_str, "pexcw");
case InstructionKind::SQRTS:
return handle_generic_op2(i0, instr_str, "sqrts");
case InstructionKind::PLZCW:
+19 -5
View File
@@ -25,7 +25,7 @@ nlohmann::json read_json_file_from_config(const nlohmann::json& json, const std:
Config make_config_via_json(nlohmann::json& json) {
Config config;
int version_int = json.at("game_version").get<int>();
ASSERT(version_int == 1 || version_int == 2);
ASSERT(version_int == 1 || version_int == 2 || version_int == 3);
config.game_version = (GameVersion)version_int;
config.text_version = json.at("text_version").get<GameTextVersion>();
config.game_name = json.at("game_name").get<std::string>();
@@ -56,6 +56,14 @@ Config make_config_via_json(nlohmann::json& json) {
config.art_group_info_dump = serialized;
}
if (json.contains("joint_node_dump_file")) {
auto json_data = file_util::read_text_file(
file_util::get_file_path({json.at("joint_node_dump_file").get<std::string>()}));
std::unordered_map<std::string, std::unordered_map<int, std::string>> serialized =
parse_commented_json(json_data, "joint_node_dump_file");
config.jg_info_dump = serialized;
}
if (json.contains("obj_file_name_map_file")) {
config.obj_file_name_map_file = json.at("obj_file_name_map_file").get<std::string>();
}
@@ -75,6 +83,7 @@ Config make_config_via_json(nlohmann::json& json) {
config.process_subtitle_images = json.at("process_subtitle_images").get<bool>();
}
config.dump_art_group_info = json.at("dump_art_group_info").get<bool>();
config.dump_joint_geo_info = json.at("dump_joint_geo_info").get<bool>();
config.hexdump_code = json.at("hexdump_code").get<bool>();
config.hexdump_data = json.at("hexdump_data").get<bool>();
config.find_functions = json.at("find_functions").get<bool>();
@@ -286,10 +295,15 @@ Config make_config_via_json(nlohmann::json& json) {
}
auto art_info_json = read_json_file_from_config(json, "art_info_file");
config.art_groups_by_file =
art_info_json.at("files").get<std::unordered_map<std::string, std::string>>();
config.art_groups_by_function =
art_info_json.at("functions").get<std::unordered_map<std::string, std::string>>();
config.art_group_type_remap =
art_info_json.at("type_remap").get<std::unordered_map<std::string, std::string>>();
if (art_info_json.contains("file_override")) {
config.art_group_file_override =
art_info_json.at("file_override")
.get<std::unordered_map<std::string, std::unordered_map<std::string, std::string>>>();
}
config.joint_node_hacks =
art_info_json.at("joint_node_hacks").get<std::unordered_map<std::string, std::string>>();
auto import_deps = read_json_file_from_config(json, "import_deps_file");
config.import_deps_by_file =
+6 -2
View File
@@ -116,6 +116,7 @@ struct Config {
bool process_subtitle_text = false;
bool process_subtitle_images = false;
bool dump_art_group_info = false;
bool dump_joint_geo_info = false;
bool rip_levels = false;
bool extract_collision = false;
bool find_functions = false;
@@ -169,9 +170,12 @@ struct Config {
DecompileHacks hacks;
std::unordered_map<std::string, std::string> art_groups_by_file;
std::unordered_map<std::string, std::string> art_groups_by_function;
std::unordered_map<std::string, std::string> art_group_type_remap;
std::unordered_map<std::string, std::unordered_map<std::string, std::string>>
art_group_file_override;
std::unordered_map<std::string, std::unordered_map<int, std::string>> art_group_info_dump;
std::unordered_map<std::string, std::unordered_map<int, std::string>> jg_info_dump;
std::unordered_map<std::string, std::string> joint_node_hacks;
std::unordered_map<std::string, std::vector<std::string>> import_deps_by_file;
};
File diff suppressed because it is too large. Load diff
+3
View File
@@ -42,6 +42,8 @@
"process_art_groups": false,
// write out a json file containing the art info mapping, run this with all objects allowed
"dump_art_group_info": false,
// write out a json file containing the joint node mapping, run this with all objects allowed
"dump_joint_geo_info": false,
///////////////////////////
// WEIRD OPTIONS
@@ -88,6 +90,7 @@
"import_deps_file": "decompiler/config/jak1/ntsc_v1/import_deps.jsonc",
"all_types_file": "decompiler/config/jak1/all-types.gc",
"art_group_dump_file": "decompiler/config/jak1/ntsc_v1/art-group-info.min.json",
"joint_node_dump_file": "decompiler/config/jak1/ntsc_v1/joint-node-info.min.json",
// optional: a predetermined object file name map from a file.
// this will make decompilation naming consistent even if you only run on some objects.
+38 -14
View File
@@ -4,24 +4,48 @@
//////////////////////
// defines what art group each file or function is using.
// by default, the decompiler assumes to be the name of the current file + -ag
// by default, the decompiler assumes this to be the name of the current type + -ag
// so you only need to specify it when that's not the case.
// NOTE: it's fine to have a function and its file both in here. the function takes priority.
"files": {
// remap names for states and behaviors
"type_remap": {
"target": "eichar-ag",
"target2": "eichar-ag",
"target-death": "eichar-ag",
"powerups": "eichar-ag",
"snow-ram": "ram-ag"
"aphid": "aphid-lurker-ag",
"mistycannon-missile": "sack-ag",
"beach-rock": "lrocklrg-ag",
"billy-snack": "farthy-snack-ag",
"billy-rat": "swamp-rat-ag",
"bully-broken-cage": "bully-ag",
"precurbridgecam": "junglecam-ag",
"assistant-bluehut": "assistant-village2-ag",
"assistant-levitator": "assistant-village2-ag",
"assistant-villagec": "assistant-village3-ag",
"balloonlurker-pilot": "balloonlurker-ag",
"flutflut": "flut-saddle-ag",
"gnawer-falling-segment": "gnawer-ag",
"babak-with-cannon": "babak-ag",
"springbox": "bounceytarp-ag",
"light-eco-mother": "light-eco-ag",
"light-eco-child": "light-eco-ag",
"bone-platform": "mis-bone-platform-ag",
"peeper": "lightning-mole-ag",
"mother-spider": "mother-spider-ag",
"mother-spider-leg": "mother-spider-ag",
"mother-spider-egg": "spider-egg-ag",
"ogreboss-super-boulder": "ogreboss-ag",
"ogreboss-bounce-boulder": "ogreboss-ag",
"plant-boss-arm": "plant-boss-ag",
"plant-boss-leaf": "plant-boss-ag",
"snow-ball-shadow": "snow-ball-ag",
"swamp-bat-slave": "swamp-bat-ag",
"swampgate": "swamp-spike-ag",
"starfish": "villa-starfish-ag",
"yeti-slave": "yeti-ag"
},
"functions": {
"(code target-warp-out)": "eichar-ag",
"(code mistycannon-missile-idle)": "sack-ag",
"(code billy-snack-eat)": "farthy-snack-ag",
"(code plunger-lurker-plunge)": "plunger-lurker-ag",
"(code plunger-lurker-flee)": "plunger-lurker-ag",
"(code plunger-lurker-idle)": "plunger-lurker-ag"
// some art groups (like robotboss-ag) have a name for their model that differs
// from the usual ag-name + "-lod0". you can add those exceptions here.
"joint_node_hacks": {
"robotboss-ag": "robotboss-basic"
}
}
@@ -9,9 +9,7 @@
// you want to run on the entire game.
"dgo_names": [
"CGO/KERNEL.CGO",
"CGO/ENGINE.CGO",
"CGO/GAME.CGO",
"CGO/ART.CGO",
"DGO/BEA.DGO",
"DGO/CIT.DGO",
"CGO/COMMON.CGO",
@@ -0,0 +1 @@
{"accordian-lod0-jg":[[22,"three"],[21,"two"],[20,"one"],[19,"top"],[18,"midpeicetwo"],[17,"midpeiceone"],[16,"midpeicetop"],[15,"twistythree"],[14,"twistytwo"],[1,"align"],[2,"prejoint"],[3,"toptubeparent"],[4,"topthree"],[5,"onethree"],[6,"twothree"],[7,"threethree"],[8,"toptwo"],[9,"onetwo"],[10,"twotwo"],[11,"threetwo"],[12,"twistytop"],[13,"twistyone"]],"allpontoons-lod0-jg":[[9,"6"],[8,"camera"],[7,"5"],[6,"4"],[5,"3"],[4,"2"],[3,"1"],[2,"prejoint"],[1,"align"]],"aphid-lurker-lod0-jg":[[36,"ring"],[35,"head_spike"],[34,"r_antenna2"],[33,"r_antenna1"],[32,"l_antenna2"],[31,"l_antenna1"],[30,"spike5"],[13,"spike3"],[12,"jaw"],[11,"head"],[10,"neck"],[9,"chest"],[8,"spike6"],[7,"spike9"],[6,"spike1"],[5,"r_seg2"],[4,"r_seg1"],[3,"main"],[2,"prejoint"],[1,"align"],[14,"spike7"],[15,"spike4"],[16,"rleg_c1"],[17,"rleg_c2"],[18,"lleg_c1"],[19,"lleg_c2"],[20,"lleg_b1"],[21,"lleg_b2"],[22,"lleg_a1"],[23,"lleg_a2"],[24,"rleg_b1"],[25,"rleg_b2"],[26,"rleg_a1"],[27,"rleg_a2"],[28,"spike2"],[29,"spike8"]],"assistant-firecanyon-lod0-jg":[[94,"belt"],[93,"RpantFlap"],[92,"LpantFlap"],[91,"rBotStrap2"],[90,"rBotStrap1"],[89,"lBotStrap2"],[88,"lBotStrap1"],[87,"rKneeFlap"],[86,"RfootFlaps"],[85,"RtoeStrap"],[84,"lKneeFlap"],[83,"LfootFlaps"],[82,"gogglesLeft"],[81,"gogglesRight"],[80,"gogglesMid"],[79,"gogglesBase"],[78,"rBoob"],[77,"lBoob"],[76,"rTopStrap2"],[75,"rTopStrap1"],[74,"rGlove"],[73,"RpinkyC"],[72,"RpinkyB"],[71,"RpinkyA"],[70,"RringC"],[69,"RringB"],[68,"RringA"],[67,"RmiddleC"],[66,"RmiddleB"],[65,"RmiddleA"],[64,"RindexC"],[63,"RindexB"],[62,"RindexA"],[61,"RthumbC"],[60,"RthumbB"],[29,"camera"],[28,"Rball"],[27,"Rankle"],[26,"Rknee"],[25,"Rthigh"],[24,"LtoeStrap"],[23,"Lball"],[22,"Lankle"],[21,"Lknee"],[20,"Lthigh"],[19,"hips"],[18,"Rhand"],[17,"Relbow"],[16,"Rshoulder"],[15,"lTopStrap1"],[14,"Lhand"],[1,"align"],[2,"prejoint"],[3,"main"],[4,"chest"],[5,"neck"],[6,"head"],[7,"lEar1"],[8,"lEar2"],[9,"rEar1"],[10,"rEar2"],[11,"backHair1"],[12,"Lshoulder"],[13,"Lelbow"],[30,"cameraB"],[31,"flipR1"],[32,"flipR2"],[33,"flipR3"],[34,"flipL1"],[35,"flipL2"],[36,"flipL3"],[37,"Lhair1"],[38,"Lhair2"],[39,"Rhair1"],[40,"Rhair2"],[41,"backHair2"],[42,"LthumbA"],[43,"LthumbB"],[44,"LthumbC"],[45,"LindexA"],[46,"LindexB"],[47,"LindexC"],[48,"LmiddleA"],[49,"LmiddleB"],[50,"LmiddleC"],[51,"LringA"],[52,"LringB"],[53,"LringC"],[54,"LpinkyA"],[55,"LpinkyB"],[56,"LpinkyC"],[57,"lGlove"],[58,"lTopStrap2"],[59,"RthumbA"]],"assistant-lavatube-end-lod0-jg":[[94,"belt"],[93,"RpantFlap"],[92,"LpantFlap"],[91,"rBotStrap2"],[90,"rBotStrap1"],[89,"lBotStrap2"],[88,"lBotStrap1"],[87,"rKneeFlap"],[86,"RfootFlaps"],[85,"RtoeStrap"],[84,"lKneeFlap"],[83,"LfootFlaps"],[82,"gogglesLeft"],[81,"gogglesRight"],[80,"gogglesMid"],[79,"gogglesBase"],[78,"rBoob"],[77,"lBoob"],[76,"rTopStrap2"],[75,"rTopStrap1"],[74,"rGlove"],[73,"RpinkyC"],[72,"RpinkyB"],[71,"RpinkyA"],[70,"RringC"],[69,"RringB"],[68,"RringA"],[67,"RmiddleC"],[66,"RmiddleB"],[65,"RmiddleA"],[64,"RindexC"],[63,"RindexB"],[62,"RindexA"],[61,"RthumbC"],[60,"RthumbB"],[29,"camera"],[28,"Rball"],[27,"Rankle"],[26,"Rknee"],[25,"Rthigh"],[24,"LtoeStrap"],[23,"Lball"],[22,"Lankle"],[21,"Lknee"],[20,"Lthigh"],[19,"hips"],[18,"Rhand"],[17,"Relbow"],[16,"Rshoulder"],[15,"lTopStrap1"],[14,"Lhand"],[1,"align"],[2,"prejoint"],[3,"main"],[4,"chest"],[5,"neck"],[6,"head"],[7,"lEar1"],[8,"lEar2"],[9,"rEar1"],[10,"rEar2"],[11,"backHair1"],[12,"Lshoulder"],[13,"Lelbow"],[30,"cameraB"],[31,"flipR1"],[32,"flipR2"],[33,"flipR3"],[34,"flipL1"],[35,"flipL2"],[36,"flipL3"],[37,"Lhair1"],[38,"Lhair2"],[39,"Rhair1"],[40,"Rhair2"],[41,"backHair2"],[42,"LthumbA"],[43,"LthumbB"],[44,"LthumbC"],[45,"LindexA"],[46,"LindexB"],[47,"LindexC"],[48,"LmiddleA"],[49,"LmiddleB"],[50,"LmiddleC"],[51,"LringA"],[52,"LringB"],[53,"LringC"],[54,"LpinkyA"],[55,"LpinkyB"],[56,"LpinkyC"],[57,"lGlove"],[58,"lTopStrap2"],[59,"RthumbA"]],"assistant-lavatube-start-lod0-jg":[[94,"belt"],[93,"RpantFlap"],[92,"LpantFlap"],[91,"rBotStrap2"],[90,"rBotStrap1"],[89,"lBotStrap2"],[88,"lBotStrap1"],[87,"rKneeFlap"],[86,"RfootFlaps"],[85,"RtoeStrap"],[84,"lKneeFlap"],[83,"LfootFlaps"],[82,"gogglesLeft"],[81,"gogglesRight"],[80,"gogglesMid"],[79,"gogglesBase"],[78,"rBoob"],[77,"lBoob"],[76,"rTopStrap2"],[75,"rTopStrap1"],[74,"rGlove"],[73,"RpinkyC"],[72,"RpinkyB"],[71,"RpinkyA"],[70,"RringC"],[69,"RringB"],[68,"RringA"],[67,"RmiddleC"],[66,"RmiddleB"],[65,"RmiddleA"],[64,"RindexC"],[63,"RindexB"],[62,"RindexA"],[61,"RthumbC"],[60,"RthumbB"],[29,"camera"],[28,"Rball"],[27,"Rankle"],[26,"Rknee"],[25,"Rthigh"],[24,"LtoeStrap"],[23,"Lball"],[22,"Lankle"],[21,"Lknee"],[20,"Lthigh"],[19,"hips"],[18,"Rhand"],[17,"Relbow"],[16,"Rshoulder"],[15,"lTopStrap1"],[14,"Lhand"],[1,"align"],[2,"prejoint"],[3,"main"],[4,"chest"],[5,"neck"],[6,"head"],[7,"lEar1"],[8,"lEar2"],[9,"rEar1"],[10,"rEar2"],[11,"backHair1"],[12,"Lshoulder"],[13,"Lelbow"],[30,"cameraB"],[31,"flipR1"],[32,"flipR2"],[33,"flipR3"],[34,"flipL1"],[35,"flipL2"],[36,"flipL3"],[37,"LhaLine truncated
@@ -1190,7 +1190,7 @@
"(code projectile-die projectile)": [[16, "event-message-block"]],
"projectile-init-by-other": [[16, "collide-edge-hold-list"]],
"projectile-init-by-other": [[16, "overlaps-others-params"]],
"(method 27 projectile-yellow)": [[16, "vector"]],
+5 -252
View File
@@ -515,7 +515,6 @@
"(method 10 shadow-control)": [[1, "v1", "int"]],
"(method 0 fact-info-enemy)": [[[3, 92], "gp", "fact-info-enemy"]],
"(method 0 fact-info)": [
//[16, "t9", "(function string none)"],
["_stack_", 16, "res-tag"],
[[32, 43], "v1", "(pointer int32)"],
[86, "gp", "fact-info"]
@@ -1568,10 +1567,7 @@
[58, "v1", "(state symbol none)"]
],
"(method 7 rigid-body-platform)": [
[5, "v1", "int"],
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(method 7 rigid-body-platform)": [[5, "v1", "int"]],
"(method 10 rigid-body)": [[50, "v1", "vector"]],
@@ -1605,10 +1601,6 @@
[49, "v1", "village2cam"]
],
"(trans plat-button-move-downward sunken-elevator)": [
[13, "v0", "(state sunken-elevator)"]
],
"(method 27 sunken-elevator)": [[37, "v1", "art-joint-anim"]],
"nav-enemy-set-base-collide-sphere-collide-with": [
@@ -1651,8 +1643,7 @@
"(method 7 nav-enemy)": [
[5, "v1", "int"],
[10, "v1", "int"],
[19, "t9", "(function process-drawable int none)"]
[10, "v1", "int"]
],
"(enter nav-enemy-patrol nav-enemy)": [[8, "v1", "int"]],
@@ -1972,8 +1963,6 @@
[87, "s4", "swingpole"]
],
"(method 10 target)": [[[10, 13], "t9", "(function process-drawable none)"]],
"draw-history": [[[99, 101], "v1", "int"]],
"(method 9 attack-info)": [
@@ -2232,12 +2221,9 @@
"(method 7 beach-rock)": [
[5, "v1", "int"],
[10, "v1", "int"],
[19, "t9", "(function process-drawable int none)"]
[10, "v1", "int"]
],
"(method 10 beach-rock)": [[21, "t9", "(function process-drawable none)"]],
"(code falling beach-rock)": [
[138, "gp", "handle"],
[150, "gp", "handle"],
@@ -2296,18 +2282,12 @@
[42, "s3", "(inline-array sparticle-cpuinfo)"]
],
"(trans plat-button-move-downward jungle-elevator)": [
[11, "v0", "(state plat-button)"]
],
"(code plat-button-at-end jungle-elevator)": [
[43, "t9", "(function none :behavior plat-button)"]
],
"(code bouncer-fire)": [[17, "v1", "art-joint-anim"]],
"(method 39 hopper)": [[16, "t9", "(function nav-enemy none)"]],
"(code nav-enemy-idle hopper)": [
[16, "v1", "art-joint-anim"],
[70, "v1", "art-joint-anim"]
@@ -2323,8 +2303,6 @@
[105, "v1", "art-joint-anim"]
],
"(method 39 junglefish)": [[12, "t9", "(function nav-enemy none)"]],
"(code nav-enemy-patrol junglefish)": [
[27, "v1", "art-joint-anim"],
[107, "v1", "art-joint-anim"],
@@ -2402,8 +2380,6 @@
[329, "v1", "art-joint-anim"]
],
"(method 39 sharkey)": [[71, "t9", "(function nav-enemy none)"]],
"(code nav-enemy-patrol sharkey)": [[27, "v1", "art-joint-anim"]],
"(code nav-enemy-attack sharkey)": [[144, "v1", "art-joint-anim"]],
@@ -2774,8 +2750,6 @@
"(code babak-with-cannon-jump-off-cannon)": [[28, "v1", "art-joint-anim"]],
"(trans nav-enemy-die babak-with-cannon)": [[40, "v0", "(state nav-enemy)"]],
"(enter othercam-running)": [
[50, "gp", "process-taskable"],
[53, "gp", "process-taskable"]
@@ -2808,10 +2782,6 @@
"(code yakow-kicked)": [[81, "v1", "art-joint-anim"]],
"(method 7 fishermans-boat)": [
[20, "t9", "(function rigid-body-platform int rigid-body-platform)"]
],
"(code muse-idle)": [
[35, "v1", "art-joint-anim"],
[87, "v1", "art-joint-anim"]
@@ -2896,10 +2866,6 @@
"(event square-platform-master-idle)": [[6, "a0", "square-platform-button"]],
"(method 7 square-platform)": [
[24, "t9", "(function baseplat int baseplat)"]
],
"(method 11 square-platform)": [
["_stack_", 16, "res-tag"],
["_stack_", 32, "res-tag"],
@@ -2925,10 +2891,6 @@
[3, "t9", "(function none :behavior qbert-plat)"]
],
"(method 23 qbert-plat)": [
[9, "t9", "(function rigid-body-platform float none)"]
],
"(code keg-on-paddle)": [
[5, "a0", "keg"], // i have learned that parent does not always equal the parent type!
[16, "v1", "process-drawable"]
@@ -2954,14 +2916,6 @@
"keg-init-by-other": [[142, "v1", "process-drawable"]],
"(method 7 keg-conveyor)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(method 7 swamp-bat)": [
[26, "t9", "(function process-drawable int process-drawable)"]
],
"(code swamp-bat-slave-die)": [[21, "v1", "swamp-bat"]],
// these casts should not be required
@@ -2970,8 +2924,6 @@
[7, "a1", "swamp-bat-slave"]
],
"(method 39 swamp-rat)": [[37, "t9", "(function nav-enemy none)"]],
"(code nav-enemy-patrol swamp-rat)": [[23, "v1", "art-joint-anim"]],
"(code nav-enemy-stare swamp-rat)": [[26, "v1", "art-joint-anim"]],
@@ -2987,8 +2939,6 @@
"(code spiderwebs-bounce)": [[80, "v1", "art-joint-anim"]],
"(method 39 baby-spider)": [[37, "t9", "(function nav-enemy none)"]],
"(code baby-spider-hatching)": [[14, "v1", "art-joint-anim"]],
"(code nav-enemy-attack baby-spider)": [[14, "v1", "art-joint-anim"]],
@@ -3058,10 +3008,6 @@
[230, "v1", "baby-spider"]
],
"(method 7 cave-trap)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(method 57 ice-cube)": [
[16, "v1", "collide-shape-prim-group"],
[26, "v1", "collide-shape-prim-group"]
@@ -3092,8 +3038,6 @@
[238, "v1", "art-joint-anim"]
],
"(method 7 yeti-slave)": [[14, "t9", "(function nav-enemy int nav-enemy)"]],
"(method 21 yeti)": [[5, "s5", "(pointer yeti-slave)"]],
"(code idle assistant-lavatube-start)": [
@@ -3199,6 +3143,8 @@
[20, "s3", "hud-fuel-cell"]
],
"(enter pickup fuel-cell)": [[9, "v0", "(state eco-collectable)"]],
"(method 15 hud-pickups)": [
[51, "v1", "dma-packet"],
[52, "v1", "dma-packet"]
@@ -3239,14 +3185,6 @@
[[53, 136], "gp", "(pointer nav-enemy)"]
],
"(method 7 battlecontroller)": [
[29, "t9", "(function process-drawable int process-drawable)"]
],
"(method 10 battlecontroller)": [
[13, "t9", "(function process-drawable none)"]
],
"(method 27 battlecontroller)": [
["_stack_", 16, "res-tag"],
[182, "v0", "(pointer int32)"]
@@ -3710,8 +3648,6 @@
[79, "v1", "float"]
],
"(method 38 fisher)": [[33, "t9", "(function fisher none)"]],
"(enter fisher-done)": [
[137, "f0", "float"],
[148, "f0", "float"],
@@ -3725,10 +3661,6 @@
[69, "v1", "float"]
],
"(trans play-accept fisher)": [[101, "v0", "state"]],
"(trans idle fisher)": [[4, "v0", "state"]],
"(code idle fisher)": [
[132, "v1", "art-joint-anim"],
[35, "v1", "float"]
@@ -4096,10 +4028,6 @@
[101, "s2", "handle"]
],
"(method 7 cavegeyserrock)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(trans junglesnake-wake)": [[15, "v1", "collide-shape-prim-group"]],
"(trans junglesnake-attack)": [[15, "v1", "collide-shape-prim-group"]],
@@ -4140,10 +4068,6 @@
[191, "v1", "art-joint-anim"]
],
"(method 7 flutflutegg)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(event flutflutegg-idle)": [[30, "gp", "process-drawable"]],
"(event flutflutegg-physics)": [[37, "gp", "process-drawable"]],
@@ -4164,10 +4088,6 @@
"(method 26 warp-gate-switch)": [[60, "v1", "art-joint-anim"]],
"(method 31 warp-gate-switch)": [
[61, "t9", "(function basebutton symbol none)"]
],
"(code basebutton-going-down warp-gate-switch)": [
[79, "v0", "(state basebutton)"],
[81, "t9", "(function none :behavior basebutton)"]
@@ -4177,24 +4097,6 @@
"(code idle warp-gate)": [[35, "a0", "symbol"]],
"(method 21 citb-arm)": [[7, "t9", "(function citb-arm-section none)"]],
"(method 21 citb-arm-shoulder)": [
[7, "t9", "(function citb-arm-section none)"]
],
"(method 21 citb-arm-a)": [[14, "t9", "(function citb-arm none)"]],
"(method 21 citb-arm-b)": [[14, "t9", "(function citb-arm none)"]],
"(method 21 citb-arm-c)": [[14, "t9", "(function citb-arm none)"]],
"(method 21 citb-arm-d)": [[14, "t9", "(function citb-arm none)"]],
"(method 21 citb-arm-shoulder-a)": [[14, "t9", "(function citb-arm none)"]],
"(method 21 citb-arm-shoulder-b)": [[14, "t9", "(function citb-arm none)"]],
"(method 26 citb-button)": [[31, "v1", "art-joint-anim"]],
"(code citb-coil-break)": [[19, "v1", "art-joint-anim"]],
@@ -4219,14 +4121,9 @@
],
"(code battlecontroller-die citb-battlecontroller)": [
[9, "v0", "(state battlecontroller)"],
[11, "t9", "(function none :behavior battlecontroller)"]
],
"(method 27 citb-battlecontroller)": [
[7, "t9", "(function battlecontroller none)"]
],
"(code eggtop-close)": [
[108, "v1", "art-joint-anim"],
[176, "v1", "art-joint-anim"]
@@ -4246,10 +4143,6 @@
"(event precurbridge-active)": [[4, "gp", "touching-shapes-entry"]],
"(method 7 jngpusher)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(method 11 lurkerm-piston)": [
["_stack_", 16, "res-tag"],
["_stack_", 32, "res-tag"],
@@ -4258,20 +4151,12 @@
"(code starfish-patrol)": [[16, "v1", "art-joint-anim"]],
"(method 7 hutlamp)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(code mis-bone-bridge-bump)": [[10, "v1", "art-joint-anim"]],
"(code mis-bone-bridge-hit)": [[21, "v1", "art-joint-anim"]],
"(code mis-bone-bridge-fall)": [[50, "v1", "art-joint-anim"]],
"(method 23 bone-platform)": [
[9, "t9", "(function rigid-body-platform basic none)"]
],
"mistycam-spawn": [
[[68, 76], "v1", "handle"],
[80, "v1", "pov-camera"],
@@ -4283,10 +4168,6 @@
[34, "gp", "handle"]
],
"(method 27 misty-battlecontroller)": [
[7, "t9", "(function battlecontroller none)"]
],
"(code boat-fuelcell-spawn)": [[50, "gp", "handle"]],
"(code pov-camera-playing village2cam)": [
@@ -4294,10 +4175,6 @@
[65, "v1", "art-joint-anim"]
],
"(method 23 pontoon)": [
[9, "t9", "(function rigid-body-platform basic none)"]
],
"fireboulder-disable-blocking-collision": [
[5, "v1", "collide-shape-prim-group"],
[14, "v1", "collide-shape-prim-group"]
@@ -4329,10 +4206,6 @@
"(code swamp-spike-gate-up)": [[60, "v1", "art-joint-anim"]],
"(method 23 tar-plat)": [
[9, "t9", "(function rigid-body-platform basic none)"]
],
"(code battlecontroller-play-intro-camera swamp-battlecontroller)": [
[38, "gp", "handle"]
],
@@ -4425,32 +4298,6 @@
"(trans spike-idle)": [[70, "v1", "float"]],
"(method 23 ogre-plat)": [
[9, "t9", "(function rigid-body-platform basic none)"]
],
"(method 31 ogre-step)": [[23, "t9", "(function ogre-plat none)"]],
"(method 31 ogre-step-a)": [[25, "t9", "(function ogre-step none)"]],
"(method 31 ogre-step-b)": [[25, "t9", "(function ogre-step none)"]],
"(method 31 ogre-step-c)": [[25, "t9", "(function ogre-step none)"]],
"(method 31 ogre-step-d)": [[25, "t9", "(function ogre-step none)"]],
"(method 31 ogre-isle)": [[17, "t9", "(function ogre-plat none)"]],
"(method 31 ogre-isle-b)": [[31, "t9", "(function ogre-isle none)"]],
"(method 31 ogre-isle-c)": [[31, "t9", "(function ogre-isle none)"]],
"(method 31 ogre-isle-d)": [[37, "t9", "(function ogre-isle none)"]],
"(method 7 ogre-bridge)": [
[26, "t9", "(function process-drawable int process-drawable)"]
],
"(code ogre-bridge-activate)": [[33, "v1", "art-joint-anim"]],
"(code water-vol-idle ogre-lava)": [[36, "v1", "art-joint-anim"]],
@@ -4489,10 +4336,6 @@
[346, "v1", "art-joint-anim"]
],
"(method 7 snow-fort-gate)": [
[19, "t9", "(function process-drawable int process-drawable)"]
],
"(code snow-button-activate)": [[25, "v1", "art-joint-anim"]],
"(code snow-button-deactivate)": [[26, "v1", "art-joint-anim"]],
@@ -4501,10 +4344,6 @@
[62, "t9", "(function none :behavior plat)"]
],
"(method 7 darkecobarrel)": [
[14, "t9", "(function darkecobarrel-base int darkecobarrel-base)"]
],
"(event darkecobarrel-mover-move)": [[76, "v1", "process-drawable"]],
"(code darkecobarrel-mover-move)": [
@@ -4531,10 +4370,6 @@
"(method 11 training-cam)": [[21, "s5", "entity-actor"]],
"(method 23 tra-pontoon)": [
[9, "t9", "(function rigid-body-platform float none)"]
],
"(event idle scarecrow-a)": [
[75, "v1", "process-drawable"],
[135, "gp", "target"],
@@ -4768,10 +4603,6 @@
"command-get-camera": [[27, "gp", "symbol"]],
"(method 7 plant-boss)": [
[47, "t9", "(function process-drawable int process-drawable)"]
],
"(code plant-boss-arm-hit)": [[88, "v1", "art-joint-anim"]],
"(code plant-boss-arm-die)": [
@@ -4850,8 +4681,6 @@
"(code plant-boss-back-arms-hit)": [[149, "v1", "art-joint-anim"]],
"(method 7 ice-cube)": [[24, "t9", "(function nav-enemy int nav-enemy)"]],
"(code ice-cube-appear)": [[14, "v1", "art-joint-anim"]],
"(code ice-cube-tired)": [
@@ -4905,14 +4734,6 @@
[22, "v1", "collide-shape-prim-group"]
],
"(method 7 billy)": [
[26, "t9", "(function process-drawable int process-drawable)"]
],
"(method 38 billy)": [[33, "t9", "(function nav-enemy none)"]],
"(enter nav-enemy-victory billy-rat)": [[4, "v0", "(state nav-enemy)"]],
"(code billy-rat-salivate)": [[43, "v1", "art-joint-anim"]],
"(code idle billy)": [
@@ -4956,10 +4777,6 @@
[89, "v1", "float"]
],
"(method 7 pelican)": [
[36, "t9", "(function process-drawable int process-drawable)"]
],
"pelican-fly": [
[61, "v1", "art-joint-anim"],
[186, "v1", "art-joint-anim"]
@@ -5014,14 +4831,6 @@
"(post idle citb-sage)": [[3, "t9", "(function none :behavior citb-sage)"]],
"(method 44 red-sagecage)": [[35, "t9", "(function citb-sage none)"]],
"(method 44 blue-sagecage)": [[35, "t9", "(function citb-sage none)"]],
"(method 44 yellow-sagecage)": [[35, "t9", "(function citb-sage none)"]],
"(method 44 green-sagecage)": [[35, "t9", "(function citb-sage none)"]],
"(method 43 red-sagecage)": [[24, "v1", "float"]],
"(method 43 blue-sagecage)": [[24, "v1", "float"]],
@@ -5058,8 +4867,6 @@
"(event wait fuel-cell)": [[95, "a0", "vector"]],
"(enter pickup fuel-cell)": [[9, "v0", "(state eco-collectable)"]],
"(code ecovalve-idle)": [[41, "v1", "process-drawable"]],
"(code vent-pickup)": [
@@ -5107,10 +4914,6 @@
"sun-iris-door-init-by-other": [[97, "v1", "art-joint-anim"]],
"(method 7 steam-cap)": [
[19, "t9", "(function process-drawable int process-drawable)"]
],
"(method 11 steam-cap)": [
["_stack_", 16, "res-tag"],
[139, "v0", "(pointer float)"]
@@ -5774,8 +5577,6 @@
[198, "v1", "art-joint-anim"]
],
"(method 10 first-person-hud)": [[32, "t9", "(function process none)"]],
"(code target-pole-flip-forward-jump)": [
[40, "t9", "(function none :behavior target)"]
],
@@ -5924,10 +5725,6 @@
"(code snow-bumper-inactive-idle)": [[19, "v1", "art-joint-anim"]],
"(method 7 snow-bumper)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(method 11 snow-bumper)": [
["_stack_", 16, "res-tag"],
[216, "v0", "(pointer float)"]
@@ -5965,10 +5762,6 @@
"(method 31 puffer)": [[16, "v1", "(array collide-shape-prim)"]],
"(method 7 puffer)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(method 11 puffer)": [
["_stack_", 16, "res-tag"],
[213, "v0", "(pointer float)"]
@@ -5984,10 +5777,6 @@
"(code driller-lurker-die)": [[28, "v1", "art-joint-anim"]],
"(method 7 driller-lurker)": [
[24, "t9", "(function process-drawable int process-drawable)"]
],
"(method 11 driller-lurker)": [
["_stack_", 16, "res-tag"],
[373, "v0", "(pointer float)"]
@@ -6012,8 +5801,6 @@
[90, "v1", "art-joint-anim"]
],
"(method 39 kermit)": [[7, "t9", "(function nav-enemy none)"]],
"(code falling gnawer-falling-segment)": [
[16, "v1", "art-joint-anim"],
[70, "v1", "art-joint-anim"]
@@ -6035,8 +5822,6 @@
"(event gnawer-run)": [[54, "a2", "touching-shapes-entry"]],
"(method 7 gnawer)": [[19, "t9", "(function nav-enemy int nav-enemy)"]],
"(code gnawer-give-fuel-cell)": [
[43, "v0", "maincavecam"],
[64, "v1", "maincavecam"]
@@ -6064,10 +5849,6 @@
[124, "v1", "art-joint-anim"]
],
"(method 7 swamp-blimp)": [
[19, "t9", "(function process-drawable int process-drawable)"]
],
"(code swamp-tetherrock-break)": [
[238, "s4", "handle"],
[261, "s4", "handle"],
@@ -6117,10 +5898,6 @@
[47, "v1", "vector"]
],
"(method 23 citb-chain-plat)": [
[9, "t9", "(function rigid-body-platform float none)"]
],
"(code citb-firehose-blast)": [
[10, "v1", "art-joint-anim"],
[86, "v1", "art-joint-anim"],
@@ -6194,10 +5971,6 @@
[69, "v1", "art-joint-anim"]
],
"(method 7 balloonlurker)": [
[29, "t9", "(function process-drawable int process-drawable)"]
],
"(code balloonlurker-pilot-die)": [[58, "v1", "art-joint-anim"]],
"(code orbit-plat-bottom-idle)": [
@@ -6269,10 +6042,6 @@
"(trans bully-stop-spinning)": [[10, "v1", "collide-shape-prim-group"]],
"(method 7 bully)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(enter seagull-idle)": [[20, "v1", "float"]],
"seagull-init-by-other": [[96, "v1", "float"]],
@@ -6389,10 +6158,6 @@
"joint-mod-tracker-callback": [[[3, 99], "s4", "joint-mod-tracker"]],
"(method 7 snow-ball)": [
[26, "t9", "(function process-drawable int process-drawable)"]
],
"(method 11 mistycannon)": [
["_stack_", 16, "res-tag"],
["_stack_", 32, "res-tag"],
@@ -6420,10 +6185,6 @@
"(method 21 helix-water)": [[27, "a0", "process-drawable"]],
"(method 7 helix-water)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"(code helix-button-activate)": [[58, "v1", "(pointer sunkencam)"]],
"(code target-flut-jump)": [[137, "v1", "float"]],
@@ -6520,10 +6281,6 @@
[[407, 409], "v1", "(pointer sunken-pipegame-button)"]
],
"(method 7 sunken-pipegame)": [
[33, "t9", "(function process-drawable int process-drawable)"]
],
"(code sunken-pipegame-begin-play)": [
[179, "v1", "float"],
[260, "v1", "float"],
@@ -6898,10 +6655,6 @@
"ogreboss-shoot-boulder": [[41, "a1", "process-drawable"]],
"(method 7 ogreboss-super-boulder)": [
[14, "t9", "(function process-drawable int process-drawable)"]
],
"ogreboss-bounce-boulder-init-by-other": [[112, "v1", "float"]],
"(code ogreboss-stage3-shuffle)": [
File diff suppressed because it is too large. Load diff
Loaded 100 of 7303 files, more files were not shown because too many files have changed in this diff. Show more