diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000..a724b7fddf --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +--- +BasedOnStyle: Chromium +ColumnLimit: 100 +SortIncludes: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..9a5df39548 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# for clion +cmake-build-debug/* +.idea/* +build/* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..3f127b847c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third-party/googletest"] + path = third-party/googletest + url = https://github.com/google/googletest.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000..ae93583ee2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,32 @@ +# Top Level CMakeLists.txt +cmake_minimum_required(VERSION 3.0) # todo - this was picked randomly +project(jak) + +set(CMAKE_CXX_STANDARD 11) + +# optimization level can be set here. Note that game/ overwrites this for building game C++ code. +set(CMAKE_CXX_FLAGS "-O0 -ggdb -Wall \ +-Wextra -Wcast-align -Wcast-qual -Wdisabled-optimization -Wformat=2 \ +-Winit-self -Wmissing-include-dirs -Woverloaded-virtual \ +-Wredundant-decls -Wshadow -Wsign-promo ") + +# includes relative to top level jak-project folder +include_directories(./) + +# build asset packer/unpacker +add_subdirectory(asset_tool) + +# build decompiler +add_subdirectory(decompiler) + +# build the game code in C++ +add_subdirectory(game) + +# build the compiler +add_subdirectory(goalc) + +# build the gtest libraries +add_subdirectory(third-party/googletest) + +# build tests +add_subdirectory(test) \ No newline at end of file diff --git a/README.md b/README.md index eca93a7ef7..ea5fa9d31f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,92 @@ +Project Structure +---------------------- +Requirements: +- `cmake` for build system +- `clang-format` for formatting code (there is already a `.clang-format` provided) +- `gtest` for testing. (Run `git submodule update --init --recursive` to check out the repository) +- `nasm` for assembling x86. There isn't much x86 assembly so if there's a better way to do this for windows, we can change. +- Third party libraries (`nlohmann/json`, `minilzo`, and `linenoise`) are provided in the `third-party` folder + +Layout: +- `goalc` is the GOAL compiler + - `gs` contains GOOS code for parts of GOOS implemented in GOOS + - `gc` contains GOAL code for parts of GOAL implemented in GOAL (must generate no machine code, just defining macros) +- `decompiler` is the decompiler +- `data` will contain big assets and the output of the GOAL compiler (not checked in to git) +- `out` will contain the finished game (not checked into git) +- `resources` will contain data which is checked into git +- `game` will contain the game source code +- `common` will contain all data/type shared between different applications. +- `doc` will contain documentation (markdown format?) +- `iso_data` is where the files from the DVD go +- `third-party` will contain code we didn't write. Google Test is a git submodule in this folder. +- `tests` will contain all tests +- `asset_tool` will contain the asset packer/unpacker + +Design: +(if anybody has better ideas, feel free to suggest improvements! This is just a rough plan for now) +- All C++ code should build from the top-level `cmake`. +- All C++ applications (GOAL compiler, asset extractor, asset packer, runtime, test) should have a script in the top level which launches them. +- All file paths should be relative to the `jak` folder. +- The planned workflow for building a game: + - `git submodule update --init --recursive` : check out gtest + - `mkdir build; cd build` : create build folder for C++ + - `cmake ..; make -j` : build C++ code + - `cd ..` + - `./test.sh` : run gtests + - `./asset_extractor.sh ./iso_data` : extract assets from game + - `./build_engine.sh` : run GOAL compiler to build all game code + - `./build_game.sh` : run the asset packer to build the game + - `./run_game.sh` : run the game +- Workflow for development: + - `./gc.sh` : run the compiler in interactive mode + - `./gs.sh` : run a goos interpreter in interactive mode + - `./decomp.sh ./iso_data` : run the decompiler + +Current state: +- GOAL compiler just implements the GOOS Scheme Macro Language. Running `./gc.sh` just loads the GOOS library (`goalc/gs/goos-lib.gs`) and then goes into an interactive mode. Use `(exit)` to exit. +- `./test.sh` runs tests for some game C++ code, for GOOS, for the reader, for the listener connection, and for some early emitter stuff. +- The runtime boots in `fakeiso` mode which will load some dummy files. Then the C Kernel (`game/kernel`) will load the `KERNEL.CGO` and `GAME.CGO` files, which are from the "proof of concept" GOAL compiler. If you run `./gk.sh`, you should see it load stuff, then print: +``` +calling play! +~~ HACK ~~ : fake play has been called +InitListenerConnect +InitCheckListener +kernel: machine started + +``` +where the `~~ HACK ~~` message is from code in `KERNEL.CGO`. + +Code Guidelines: +- Avoid warnings +- Use asserts over throwing exceptions in game code (throwing exceptions from C code called by GOAL code is sketchy) + +TODOS: +- Build on Windows! + - Networking + - File paths + - Timer + - CMake? + - Assembly + - Windows calling convention for assembly stuff + - pthreads (can probably replace with `std::thread`, I don't remember why I used `pthread`s) + - performance stats for `SystemThread` (probably just get rid of these performance stats completely) + - `mmap`ing executable memory + - line input library (appears windows compatible?) + +- Clean up use of namespaces +- Clean up the print message when `gk` starts. +- Finish commenting runtime stuff +- Runtime document +- GOOS document +- Listener protocol document +- GOAL Compiler IR +- GOAL Compiler Skeleton + +In Progress: +- GOAL emitter / emitter testing setup + + Project Description ----------------------- @@ -29,6 +118,7 @@ Some statistics: The rough timeline is to finish sometime in 2022. If it looks like this is impossible, the project will be abandoned. But I have already spent about 4 months preparing to start this and seems doable. I also have some background in compilers, and familiarity with PS2 (worked on DobieStation PS2 emulator) / MIPS in general (wrote a PS1 emulator). I think the trick will be making good automated tools - the approach taken for SM64 and other N64 decompilations is way too labor-intensive to work. + GOAL Decompiler ------------------ The decompiler is in progress, at @@ -190,3 +280,5 @@ Packs together all assets/compiled code/runtime into a format that can be played It's important that the asset extraction/packing can be automated so we can avoid distributing the assets, which are large and probably not supposed to be distributed. + + diff --git a/asset_tool/CMakeLists.txt b/asset_tool/CMakeLists.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/common_types.h b/common/common_types.h new file mode 100644 index 0000000000..b4da746aaf --- /dev/null +++ b/common/common_types.h @@ -0,0 +1,20 @@ +/*! + * @file common_types.h + * Common Integer Types. + */ + +#ifndef JAK1_COMMON_TYPES_H +#define JAK1_COMMON_TYPES_H + +#include + +using u8 = uint8_t; +using u16 = uint16_t; +using u32 = uint32_t; +using u64 = uint64_t; +using s8 = int8_t; +using s16 = int16_t; +using s32 = int32_t; +using s64 = int64_t; + +#endif // JAK1_COMMON_TYPES_H diff --git a/common/link_types.h b/common/link_types.h new file mode 100644 index 0000000000..ec70f63624 --- /dev/null +++ b/common/link_types.h @@ -0,0 +1,37 @@ +/*! + * @file link_types.h + * Types used in the linking data, shared between the object file generator and the kernel's linker. + */ + +#ifndef JAK1_LINK_TYPES_H +#define JAK1_LINK_TYPES_H + +enum LinkKind { + LINK_TABLE_END = 0, //! no more linking data + LINK_SYMBOL_OFFSET = 1, //! link a symbol (pointer to symbol table entry) + LINK_TYPE_PTR = 2, //! link a pointer to a type. + LINK_DISTANCE_TO_OTHER_SEG_64 = 3, //! link to another segment + LINK_DISTANCE_TO_OTHER_SEG_32 = 4, //! link to another segment +}; + +enum SegmentTypes { MAIN_SEGMENT = 0, DEBUG_SEGMENT = 1, TOP_LEVEL_SEGMENT = 2 }; + +constexpr int N_SEG = 3; + +/*! + * Data at the front of the DGO. + */ +struct DgoHeader { + u32 object_count; + char name[60]; +}; + +/*! + * Data at the front of each OBJ. + */ +struct ObjectHeader { + u32 size; + char name[60]; +}; + +#endif // JAK1_LINK_TYPES_H diff --git a/common/listener_common.h b/common/listener_common.h new file mode 100644 index 0000000000..f18c41b4a5 --- /dev/null +++ b/common/listener_common.h @@ -0,0 +1,59 @@ +/*! + * @file listener_common.h + * Common types shared between the compiler and the runtime for the listener connection. + */ + +#ifndef JAK1_LISTENER_COMMON_H +#define JAK1_LISTENER_COMMON_H + +#include "common/common_types.h" + +/*! + * Header of a DECI2 protocol message + * TODO - there are other copies of this somewhere + */ +struct Deci2Header { + u16 len; //! size of data following header + u16 rsvd; //! zero, used internally by runtime. + u16 proto; //! protocol identification number + u8 src; //! identification code of sender + u8 dst; //! identification code of recipient +}; + +/*! + * Type of message sent to compiler + */ +enum class ListenerMessageKind : u16 { + MSG_ACK = 0, //! Acknowledge a compiler message + MSG_OUTPUT = 1, //! Send output buffer data + MSG_PRINT = 2, //! Send print buffer data + MSG_INVALID = 24 +}; + +/*! + * Type of message sent from compiler + */ +enum ListenerToTargetMsgKind { + LTT_MSG_POKE = 1, //! "Poke" the game and have it flush buffers + LTT_MSG_INSEPCT = 5, //! Inspect an object + LTT_MSG_PRINT = 6, //! Print an object + LTT_MSG_PRINT_SYMBOLS = 7, //! Print all symbols + LTT_MSG_RESET = 8, //! Reset the game + LTT_MSG_CODE = 9 //! Send code to patch into the game +}; + +/*! + * The full header of a listener message, including the Deci2Header + * TODO - there are other copies of this somewhere + */ +struct ListenerMessageHeader { + Deci2Header deci2_header; //! The header used for DECI2 communication + ListenerMessageKind msg_kind; //! GOAL Listener message kind + u16 u6; //! Unknown + u32 msg_size; //! Size of data after this header + u64 u8; //! Unknown +}; + +constexpr int DECI2_PORT = 8112; // TODO - is this a good choise? + +#endif // JAK1_LISTENER_COMMON_H diff --git a/common/symbols.h b/common/symbols.h new file mode 100644 index 0000000000..750e430d87 --- /dev/null +++ b/common/symbols.h @@ -0,0 +1,80 @@ +/*! + * @file symbols.h + * The location of fixed symbols in the GOAL symbol table. + */ + +#ifndef JAK1_SYMBOLS_H +#define JAK1_SYMBOLS_H + +constexpr int FIX_SYM_EMPTY_CAR = -0xc; +constexpr int FIX_SYM_EMPTY_PAIR = -0xa; +constexpr int FIX_SYM_EMPTY_CDR = -0x8; +constexpr int FIX_SYM_FALSE = 0x0; // GOAL boolean #f (note that this is equal to the $s7 register) +constexpr int FIX_SYM_TRUE = 0x8; // GOAL boolean #t + +// types +constexpr int FIX_SYM_FUNCTION_TYPE = 0x10; // GOAL type of function +constexpr int FIX_SYM_BASIC_TYPE = 0x18; // GOAL structure type with type tag +constexpr int FIX_SYM_STRING_TYPE = 0x20; // GOAL string type (gstring) +constexpr int FIX_SYM_SYMBOL_TYPE = 0x28; // GOAL symbol type +constexpr int FIX_SYM_TYPE_TYPE = 0x30; // GOAL type of type +constexpr int FIX_SYM_OBJECT_TYPE = 0x38; // GOAL parent type of all types +constexpr int FIX_SYM_LINK_BLOCK = 0x40; // GOAL type of link-block (used by linker, but seems to be unused by GOAL) +constexpr int FIX_SYM_INTEGER_TYPE = 0x48; // GOAL integer parent type, assumes unboxed +constexpr int FIX_SYM_SINTEGER_TYPE = 0x50; // GOAL signed integer parent type, assumes unboxed +constexpr int FIX_SYM_UINTEGER_TYPE = 0x58; // GOAL unsinged integer parent type, assumes unboxed +constexpr int FIX_SYM_BINTEGER_TYPE = 0x60; // GOAL "boxed integer" type +constexpr int FIX_SYM_INT8_TYPE = 0x68; // GOAL 8-bit signed integer +constexpr int FIX_SYM_INT16_TYPE = 0x70; // ... +constexpr int FIX_SYM_INT32_TYPE = 0x78; // ... +constexpr int FIX_SYM_INT64_TYPE = 0x80; // ... +constexpr int FIX_SYM_INT128_TYPE = 0x88; // GOAL 128-bit integer type, behaves strangely +constexpr int FIX_SYM_UINT8_TYPE = 0x90; // GOAL 8-bit unsigned integer +constexpr int FIX_SYM_UINT16_TYPE = 0x98; // ... +constexpr int FIX_SYM_UINT32_TYPE = 0xA0; // ... +constexpr int FIX_SYM_UINT64_TYPE = 0xA8; // ... +constexpr int FIX_SYM_UINT128_TYPE = 0xB0; // ... +constexpr int FIX_SYM_FLOAT_TYPE = 0xB8; // GOAL 32-bit floating point type +constexpr int FIX_SYM_PROCESS_TREE_TYPE = 0xC0; // GOAL process-tree type. Used in the gkernel +constexpr int FIX_SYM_PROCESS_TYPE = 0xC8; // GOAL process type +constexpr int FIX_SYM_THREAD_TYPE = 0xD0; // GOAL thread type +constexpr int FIX_SYM_STRUCTURE_TYPE = 0xD8; // GOAL structure type. Any type with fields +constexpr int FIX_SYM_PAIR_TYPE = 0xE0; // GOAL pair type +constexpr int FIX_SYM_POINTER_TYPE = 0xE8; // GOAL pointer type (32-bit) +constexpr int FIX_SYM_NUMBER_TYPE = 0xF0; // GOAL number type (parent of integer/float types) +constexpr int FIX_SYM_ARRAY_TYPE = 0xF8; // GOAL array type +constexpr int FIX_SYM_VU_FUNCTION_TYPE = 0x100; // GOAL vu-function type +constexpr int FIX_SYM_CONNECTABLE_TYPE = 0x108; // GOAL connectable +constexpr int FIX_SYM_STACK_FRAME_TYPE = 0x110; // GOAL stack-frame +constexpr int FIX_SYM_FILE_STREAM_TYPE = 0x118; // GOAL file-stream +constexpr int FIX_SYM_KHEAP = 0x120; // GOAL kheap + +// GOAL functions +constexpr int FIX_SYM_NOTHING_FUNC = 0x128; // GOAL nothing-func (does nothing) +constexpr int FIX_SYM_DEL_BASIC_FUNC = 0x130; // GOAL delete-basic function + +// GOAL allocation symbols (?) +constexpr int FIX_SYM_STATIC = 0x138; // GOAL 'static +constexpr int FIX_SYM_GLOBAL_HEAP = 0x140; // GOAL 'global +constexpr int FIX_SYM_DEBUG_HEAP = 0x148; // GOAL 'debug +constexpr int FIX_SYM_LOADING_LEVEL = 0x150; // ?? +constexpr int FIX_SYM_LOADING_PACKAGE = 0x158; // ?? +constexpr int FIX_SYM_PROCESS_LEVEL_HEAP = 0x160; // ?? +constexpr int FIX_SYM_STACK = 0x168; // GOAL 'stack +constexpr int FIX_SYM_SCRATCH = 0x170; // GOAL 'scratch + +// GOAL random stuff +constexpr int FIX_SYM_SCRATCH_TOP = 0x178; // GOAL *scratch-top* +constexpr int FIX_SYM_ZERO_FUNC = 0x180; // GOAL zero-func (returns 0x0 in $v0 register) +constexpr int FIX_SYM_ASIZE_OF_BASIC_FUNC = 0x188; // GOAL asize-of-basic function +constexpr int FIX_SYM_COPY_BASIC_FUNC = 0x190; // GOAL copy-basic function +constexpr int FIX_SYM_LEVEL = 0x198; // ?? +constexpr int FIX_SYM_ART_GROUP = 0x1a0; // ?? +constexpr int FIX_SYM_TX_PAGE_DIR = 0x1a8; // ?? +constexpr int FIX_SYM_TX_PAGE = 0x1b0; // ?? +constexpr int FIX_SYM_SOUND = 0x1b8; // ?? +constexpr int FIX_SYM_DGO = 0x1c0; // ?? +constexpr int FIX_SYM_TOP_LEVEL = 0x1c8; // ?? +constexpr int FIX_FIXED_SYM_END_OFFSET = 0x1d0; + +#endif // JAK1_SYMBOLS_H diff --git a/common/versions.h b/common/versions.h new file mode 100644 index 0000000000..74b1ec0169 --- /dev/null +++ b/common/versions.h @@ -0,0 +1,21 @@ +/*! + * @file versions.h + * Version numbers for GOAL Language, Kernel, etc... + */ + +#ifndef JAK1_VERSIONS_H +#define JAK1_VERSIONS_H + +#include "common/common_types.h" + +namespace versions { +// language version +constexpr s32 GOAL_VERSION_MAJOR = 2; +constexpr s32 GOAL_VERSION_MINOR = 6; +} + +// GOAL kernel version +constexpr int KERNEL_VERSION_MAJOR = 2; +constexpr int KERNEL_VERSION_MINOR = 0; + +#endif // JAK1_VERSIONS_H diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/decompiler/CMakeLists.txt b/decompiler/CMakeLists.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doc/reader.md b/doc/reader.md new file mode 100644 index 0000000000..7f4f248da2 --- /dev/null +++ b/doc/reader.md @@ -0,0 +1,84 @@ +# Reader +GOOS and GOAL both use the same reader, which converts text files to S-Expressions and allows these s-expressions to be mapped back to a line in a source file for error messages. This docuemnt explains the syntax of the reader. Note that these rules do not explain the syntax of the language (for instance, GOAL has a much more complicated system of integers and many more restrictions), but rather the rules of how your program source must look. + +## Integer Input +Integers handled by the reader are 64-bits. Any overflow is considered an error. An integer can be specified as a decimal, like `0` or `-12345`; in hex, like `#xbeef`; or in binary, like `#b101001`. All three representations can be used anywhere an integer is used. Hex numbers do not care about the case of the characters. Decimal numbers are signed, and wrapping from a large positive number to a negative number will generate an error. The valid input range for decimals is `INT64_MIN` to `INT64_MAX`. Hex and binary are unsigned and do not support negative signs, but allow large positive numbers to wrap to negative. Their input range is `0` to `UINT64_MAX`. For example, `-1` can be entered as `-1` or `#xffffffffffffffff`, but not as `UINT64_MAX` in decimal. + +## Floating Point Input +Floating point values handled by the reader are implemented with `double`. Weird numbers (denormals, NaN, infinity) are invalid and not handled by the reader directly. A number _must_ have a decimal point to be interpreted as floating point. Otherwise, it will be an integer. Leading/trailing zeros are optional. + +## Character Input +Characters are used to represent characters that are part of text. The character `c` is represented by `#\c`. This representation is used for all ASCII characters between `!` and `~`. There are three special characters which have a non-standard representation: +- Space : `#\\s` +- New Line: `#\\n` +- Tab: `#\\t` + +All other characters are invalid. + +## String +A string is a sequence of characters, surrounding by double quotes. The ASCII characters from ` ` to `~` excluding `"` can be entered directly. Strings have the following escape codes: +- `\\` : insert a backslash +- `\n` : insert a new line +- `\t` : insert a tab +- `\"` : insert a double quote + + +## Comments +The reader supports line comments with `;` and multi-line comments with `#| |#`. For example + +``` +(print "hi") ; prints hi + +#| +this is a multi-line comment! +(print "hi") <- this is commented out. +|# +``` + +## Array +The reader supports arrays with the following syntax: +``` +; array of 1, 2, 3, 4 +#(1 2 3 4) +``` + +Arrays can be nested with lists, pairs, and other arrays. + +## Pair +The reader supports pairs with the following syntax: +``` +; pair of a, b +(a . b) +``` +Pairs can be nested with lists, pairs, and arrays. + +## List +The reader supports lists. Lists are just an easier way of constructing a linked list of pairs, terminated with the empty list. The empty list is a special list written like `()`. + +``` +; list of 1, 2, 3 +(1 2 3) +; actually the same as +(1 . (2 . (3 . ()))) +``` + +## Symbol +A symbol is a sequence of characters containing no whitespace, and not matching any other data type. (Note: this is not a very good definition). Typically symbols are lower case, and words are separated by a `-`. Examples: +``` +this-is-a-symbol +; you can have weird symbols too: +#f +#t +- +* ++ +__WEIRDLY-NamedSymbol ; this is weird, but OK. +``` + +## Reader Macros +The reader has some default macros which are common in Scheme/LISP: +- `'x` will be replaced with `(quote x)` +- `` `x`` will be replaced with `(quasiquote x)` +- `,x` will be replaced with `(unquote x)` +- `,@` will be replaced with `(unquote-splicing x)` + diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt new file mode 100644 index 0000000000..05e0d99744 --- /dev/null +++ b/game/CMakeLists.txt @@ -0,0 +1,58 @@ +# We define our own compilation flags here. +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_FLAGS "-O0 -ggdb -Wall \ +-Wextra -Wcast-align -Wcast-qual -Wdisabled-optimization -Wformat=2 \ +-Winit-self -Wmissing-include-dirs -Woverloaded-virtual \ +-Wredundant-decls -Wshadow -Wsign-promo ") + +enable_language(ASM_NASM) +set(RUNTIME_SOURCE + main.cpp + runtime.cpp + system/SystemThread.cpp + system/IOP_Kernel.cpp + system/iop_thread.cpp + system/Deci2Server.cpp + sce/libcdvd_ee.cpp + sce/libscf.cpp + sce/deci2.cpp + sce/sif_ee.cpp + sce/iop.cpp + sce/stubs.cpp + kernel/asm_funcs.nasm + kernel/fileio.cpp + kernel/kboot.cpp + kernel/kdgo.cpp + kernel/kdsnetm.cpp + kernel/klink.cpp + kernel/klisten.cpp + kernel/kmachine.cpp + kernel/kmalloc.cpp + kernel/kmemcard.cpp + kernel/kprint.cpp + kernel/kscheme.cpp + kernel/ksocket.cpp + kernel/ksound.cpp + overlord/dma.cpp + overlord/fake_iso.cpp + overlord/iso.cpp + overlord/iso_api.cpp + overlord/iso_cd.cpp + overlord/iso_queue.cpp + overlord/isocommon.cpp + overlord/overlord.cpp + overlord/ramdisk.cpp + overlord/sbank.cpp + overlord/soundcommon.cpp + overlord/srpc.cpp + overlord/ssound.cpp + overlord/stream.cpp) + +# the runtime should be built without any static/dynamic libraries. +add_executable(gk ${RUNTIME_SOURCE}) + +# we also build a runtime library for testing. This version is likely unable to call GOAL code correctly, but +# can be used to test other things. +add_library(runtime ${RUNTIME_SOURCE}) + +target_link_libraries(gk pthread) \ No newline at end of file diff --git a/game/common/dgo_rpc_types.h b/game/common/dgo_rpc_types.h new file mode 100644 index 0000000000..0a83f162ba --- /dev/null +++ b/game/common/dgo_rpc_types.h @@ -0,0 +1,31 @@ +/*! + * @file dgo_rpc_types.h + * Types used for the DGO Remote Procedure Call between the EE and the IOP + */ + +#ifndef JAK1_DGO_RPC_TYPES_H +#define JAK1_DGO_RPC_TYPES_H + +#include "common/common_types.h" + +constexpr int DGO_RPC_ID = 0xdeb4; +constexpr int DGO_RPC_CHANNEL = 3; +constexpr int DGO_RPC_LOAD_FNO = 0; +constexpr int DGO_RPC_LOAD_NEXT_FNO = 1; +constexpr int DGO_RPC_CANCEL_FNO = 2; +constexpr int DGO_RPC_RESULT_INIT = 666; +constexpr int DGO_RPC_RESULT_ABORTED = 3; +constexpr int DGO_RPC_RESULT_MORE = 2; +constexpr int DGO_RPC_RESULT_ERROR = 1; +constexpr int DGO_RPC_RESULT_DONE = 0; + +struct RPC_Dgo_Cmd { + uint16_t rsvd; + uint16_t result; + uint32_t buffer1; + uint32_t buffer2; + uint32_t buffer_heap_top; + char name[16]; +}; + +#endif // JAK1_DGO_RPC_TYPES_H diff --git a/game/common/loader_rpc_types.h b/game/common/loader_rpc_types.h new file mode 100644 index 0000000000..f4f8f1d7bb --- /dev/null +++ b/game/common/loader_rpc_types.h @@ -0,0 +1,12 @@ +/*! + * @file loader_rpc_types.h + * Types used for the Loader Remote Procedure Call between the EE and the IOP + */ + +#ifndef JAK1_LOADER_RPC_TYPES_H +#define JAK1_LOADER_RPC_TYPES_H + +constexpr int LOADER_RPC_ID = 0xdeb2; +constexpr int LOADER_RPC_CHANNEL = 1; + +#endif // JAK1_LOADER_RPC_TYPES_H diff --git a/game/common/play_rpc_types.h b/game/common/play_rpc_types.h new file mode 100644 index 0000000000..119d4cf04e --- /dev/null +++ b/game/common/play_rpc_types.h @@ -0,0 +1,13 @@ +/*! + * @file play_rpc_types.h + * Types used for the play Remote Procedure Call between the EE and the IOP. + * Note that PLAY and PLAYER are different. + */ + +#ifndef JAK1_PLAY_RPC_TYPES_H +#define JAK1_PLAY_RPC_TYPES_H + +constexpr int PLAY_RPC_ID = 0xdeb6; +constexpr int PLAY_RPC_CHANNEL = 5; + +#endif // JAK1_PLAY_RPC_TYPES_H diff --git a/game/common/player_rpc_types.h b/game/common/player_rpc_types.h new file mode 100644 index 0000000000..ef80030ed7 --- /dev/null +++ b/game/common/player_rpc_types.h @@ -0,0 +1,13 @@ +/*! + * @file player_rpc_types.h + * Types used for the player Remote Procedure Call between the EE and the IOP. + * Note that PLAY and PLAYER are different. + */ + +#ifndef JAK1_PLAYER_RPC_TYPES_H +#define JAK1_PLAYER_RPC_TYPES_H + +constexpr int PLAYER_RPC_ID = 0xdeb1; +constexpr int PLAYER_RPC_CHANNEL = 0; + +#endif // JAK1_PLAYER_RPC_TYPES_H diff --git a/game/common/ramdisk_rpc_types.h b/game/common/ramdisk_rpc_types.h new file mode 100644 index 0000000000..aa488d7a5d --- /dev/null +++ b/game/common/ramdisk_rpc_types.h @@ -0,0 +1,25 @@ +/*! + * @file ramdisk_rpc_types.h + * Types used for the RamDisk Remote Procedure Call between the EE and the IOP + */ + +#ifndef JAK1_RAMDISK_RPC_TYPES_H +#define JAK1_RAMDISK_RPC_TYPES_H + +#include "common/common_types.h" + +constexpr int RAMDISK_RPC_ID = 0xdeb3; +constexpr int RAMDISK_RPC_CHANNEL = 2; +constexpr int RAMDISK_GET_DATA_FNO = 0; +constexpr int RAMDISK_RESET_AND_LOAD_FNO = 1; +constexpr int RAMDISK_BYPASS_LOAD_FILE = 4; + +struct RPC_Ramdisk_LoadCmd { + char pad[4]; + uint32_t file_id_or_ee_addr; + uint32_t offset_into_file; + uint32_t size; + char name[16]; // guess on length? +}; + +#endif // JAK1_RAMDISK_RPC_TYPES_H diff --git a/game/fake_iso.txt b/game/fake_iso.txt new file mode 100644 index 0000000000..28f3185fc3 --- /dev/null +++ b/game/fake_iso.txt @@ -0,0 +1,10 @@ +; Fake ISO file - used to map files in jak-project/ to files available for loading from OVERLORD. +; Each entry should consist of an ISO name, followed by a file name +; note that tweakval, vagdir, screen1 have dummy data for now. + +KERNEL.CGO resources/KERNEL.CGO +GAME.CGO resources/GAME.CGO +TEST.CGO resources/TEST.CGO +TWEAKVAL.MUS resources/TWEAKVAL.MUS +VAGDIR.AYB resources/VAGDIR.AYB +SCREEN1.USA resources/SCREEN1.USA diff --git a/game/kernel/Ptr.h b/game/kernel/Ptr.h new file mode 100644 index 0000000000..9764de9fb4 --- /dev/null +++ b/game/kernel/Ptr.h @@ -0,0 +1,96 @@ +/*! + * @file Ptr.h + * Representation of a GOAL pointer which can be converted to/from a C pointer. + */ + +#ifndef JAK_PTR_H +#define JAK_PTR_H + +#include +#include "game/runtime.h" +#include "common/common_types.h" + +/*! + * GOAL pointer to a T. Represented as a 32-bit unsigned offset from g_ee_main_mem. + * A NULL pointer has an offset of 0. + * + * This doesn't have to be very efficient, as this implementation is only used in the C Kernel. + * The GOAL implementation is much more efficient. + * + * Consider putting size checks on these? + */ +template +struct Ptr { + u32 offset; + + /*! + * Default pointer is NULL. + */ + Ptr() { offset = 0; } + + /*! + * Pointer from manual offset. + */ + explicit Ptr(u32 v) { offset = v; } + + /*! + * Dereference a pointer. Will throw if you do this on a null pointer. + */ + T* operator->() { + if (offset) { + return (T*)(g_ee_main_mem + offset); + } else { + throw std::runtime_error("Ptr null dereference!"); + } + } + + /*! + * Dereference a pointer. Will throw if you do this on a null pointer. + */ + T& operator*() { + if (offset) { + return *(T*)(g_ee_main_mem + offset); + } else { + throw std::runtime_error("Ptr null dereference!"); + } + } + + // pointer math + Ptr operator+(s32 diff) { return Ptr(offset + diff); } + s32 operator-(Ptr x) { return offset - x.offset; } + Ptr operator-(s32 diff) { return Ptr(offset - diff); } + bool operator==(const Ptr& x) { return offset == x.offset; } + + /*! + * Convert to a C pointer. + */ + T* c() { + if (!offset) { + return nullptr; + } + return (T*)(g_ee_main_mem + offset); + } + + template + Ptr cast() { + return Ptr(offset); + } +}; + +template +Ptr make_ptr(T* x) { + if (!x) { + return Ptr(0); + } + return Ptr((u8*)x - g_ee_main_mem); +} + +template +Ptr make_u8_ptr(T* x) { + if (!x) { + return Ptr(0); + } + return Ptr((u8*)x - g_ee_main_mem); +} + +#endif // JAK_PTR_H diff --git a/game/kernel/asm_funcs.nasm b/game/kernel/asm_funcs.nasm new file mode 100644 index 0000000000..d449265338 --- /dev/null +++ b/game/kernel/asm_funcs.nasm @@ -0,0 +1,98 @@ +;;;;;;;;;;;;;;;;;;;; +;; asm_funcs.nasm ;; +;;;;;;;;;;;;;;;;;;;; + +;; GOAL Runtime assembly functions. These exist only in the x86 version of GOAL. + +;; declaration of the extern "C" function format_impl +extern format_impl + +SECTION .TEXT + +;; This _format function which will be exported to the GOAL symbol table at runtime start as "_format" +;; This function accepts 8 GOAL arguments and puts them on the stack, then calls format_impl and passes +;; a pointer to this array of GOAL arguments as the argument. The reason for this is that GOAL and +;; the standard System V ABI used in Linux are different for 8 argument function calls. + +global _format +_format: + ; GOAL will call with regs RDI, RSI, RDX, RCX, R8, R9, R10, R11 + + ; to make sure the stack frame is aligned + sub rsp, 8 + + ; push all registers and create the register array on the stack + push r11 + push r10 + push r9 + push r8 + push rcx + push rdx + push rsi + push rdi + + ; set the first argument register to the stack argument array + mov rdi, rsp + + ; call C function to do format, result will go in RAX + call format_impl + + ; restore + ; (note - this could probably just be add rsp 72, we don't care about the value of these register) + pop rdi + pop rsi + pop rdx + pop rcx + pop r8 + pop r9 + pop r10 + pop r11 + add rsp, 8 + ret +;; NOTE: calling format has a _lot_ of indirection... +;; symbol table lookup to find the GOAL "format" symbol value +;; run the GOAL-to-C trampoline (on GOAL heap) to jump to this _format +;; run this wrapper to call the real format_impl + + + + +;; The _call_goal_asm function is used to call a GOAL function from C. +;; It supports up to 3 arguments and a return value. +;; This should be called with the arguments: +;; - first goal arg +;; - second goal arg +;; - third goal arg +;; - address of function to call +;; - address of the symbol table +;; - GOAL memory space offset + +global _call_goal_asm + +_call_goal_asm: + ;; x86 saved registers we need to modify for GOAL should be saved + push r13 + push r14 + push r15 + + ;; RDI - first arg + ;; RSI - second arg + ;; RDX - third arg + ;; RCX - function pointer (goes in r13) + ;; R8 - st (goes in r14) + ;; R9 - off (goes in r15) + + ;; set GOAL function pointer + mov r13, rcx + ;; offset + mov r15, r8 + ;; symbol table + mov r14, r9 + ;; call GOAL by function pointer + call r13 + + ;; retore x86 registers. + pop r15 + pop r14 + pop r13 + ret \ No newline at end of file diff --git a/game/kernel/fileio.cpp b/game/kernel/fileio.cpp new file mode 100644 index 0000000000..585748bf0d --- /dev/null +++ b/game/kernel/fileio.cpp @@ -0,0 +1,500 @@ +/*! + * @file fileio.cpp + * GOAL Low-Level File I/O and String Utilities + * DONE! + */ + +#include +#include +#include +#include "game/sce/stubs.h" +#include "fileio.h" +#include "kprint.h" + +namespace { +// buffer for file paths. This might be static char buffer[512]. Maybe 633 is the line number? +char buffer_633[512]; +} // namespace + +void fileio_init_globals() { + memset(buffer_633, 0, 512); +} + +using namespace ee; + +/*! + * Return pointer to null terminator of string. + * const is for losers. + * DONE, EXACT + */ +char* strend(char* str) { + while (*str) + str++; + return str; +} + +/*! + * An implementation of Huffman decoding. + * In this limited decoder, your data must have lower two bits equal to zero. + * @param loc_ptr pointer to pointer to data to read (will be modified to point to next word) + * @return decoded word + * UNUSED, EXACT + */ +u32 ReadHufWord(u8** loc_ptr) { + u8* loc = *loc_ptr; // pointer to data to read + u32 value = *(u32*)loc; // read word + u8* next_loc = loc + 1; // next data to read + u32 length = value & 3; // length of word is stored in lower two bits. + switch (length) { + case 0: // already all set. + break; + + case 1: + value = (value & 0xfc) | (loc[1] << 8); + next_loc = loc + 2; + break; + + case 2: + value = (value & 0xfc) | (loc[1] << 8) | (loc[2] << 0x10); + next_loc = loc + 3; + break; + + case 3: + value = (value & 0xfc) | (loc[1] << 8) | (loc[2] << 0x10) | (loc[3] << 0x18); + next_loc = loc + 4; + break; + + default: + assert(false); + } + + // update location pointer + *loc_ptr = next_loc; + return value; +} + +/*! + * Copy a string from src to dst. The null terminator is copied too. + * This is identical to normal strcpy. + * DONE, EXACT + */ +void kstrcpy(char* dst, const char* src) { + char* dst_ptr = dst; + const char* src_ptr = src; + + while (*src_ptr != 0) { + *dst_ptr = *src_ptr; + src_ptr++; + dst_ptr++; + } + *dst_ptr = 0; +} + +/*! + * Copy a string from src to dst, making all letters upper case. + * The null terminator is copied too. + * DONE, EXACT + */ +void kstrcpyup(char* dst, const char* src) { + while (*src) { + char c = *src; + if (c >= 'a' && c <= 'z') { // A-Z,a-z + c -= 0x20; + } + *dst = c; + dst++; + src++; + } + *dst = 0; +} + +/*! + * Concatenate two strings. Src is added to dest. + * The new string is null terminated. No bounds checking is done. + * DONE, EXACT + */ +void kstrcat(char* dest, const char* src) { + // seek to end of first string + while (*dest) { + dest++; + } + // copy second string + while (*src) { + *dest = *src; + src++; + dest++; + } + // null terminate + *dest = 0; +} + +/*! + * Concatenate two strings with a maximum length for the resulting string + * The maximum length should be larger than the length of the original string. + * The resulting string will be truncated when it reaches the given length. + * The null terminator is added, but doesn't count toward the length. + * DONE, EXACT + */ +void kstrncat(char* dest, const char* src, s32 count) { + // seek to null terminator of first string, count length + s32 i = 0; + while (*dest) { + dest++; + i++; + } + + // append second string, not exceeding length + while (*src && (i < count)) { + *dest = *src; + src++; + dest++; + i++; + } + + // null terminate + *dest = 0; +} + +/*! + * Insert the pad char at the beginning of a string, count times. + * DONE, EXACT + */ +char* kstrinsert(char* str, char pad, s32 count) { + // shift string+null terminator to the right. + s32 len = strlen(str); + while (len > -1) { + str[len + count] = str[len]; + len--; + } + + // pad + len = 0; + while (len < count) { + str[len++] = pad; + } + return str; +} + +/*! + * Get filename from path. + * This function is renamed to basename_goal so it doesn't conflict with "basename" that is + * already defined on my computer. + * For example: + * a/b/c.e will return c.e + * a\b\c.e will return c.e + * asdf.asdf will return asdf.asdf + * DONE, EXACT + */ +char* basename_goal(char* s) { + char* input = s; + char* pt = s; + + // seek to end + for (;;) { + char c = *pt; + if (c) { + pt++; + } else { + break; + } + } + + // back up... + for (;;) { + if (pt < input) { + return input; + } + pt--; + char c = *pt; + // until we hit a slash. + if (c == '\\' || c == '/') { // slashes + return pt + 1; // and return one past + } + } +} + +/*! + * Turn file name into file's path. + * DONE, EXACT + */ +char* DecodeFileName(const char* name) { + char* result; + // names starting with $ are special: + if (name[0] == '$') { + if (!strncmp(name, "$TEXTURE/", 9)) { + result = MakeFileName(TX_PAGE_FILE_TYPE, name + 9, 0); + } else if (!strncmp(name, "$ART_GROUP/", 0xb)) { + result = MakeFileName(ART_GROUP_FILE_TYPE, name + 0xb, 0); + } else if (!strncmp(name, "$LEVEL/", 7)) { + int len = (int)strlen(name); + if (name[len - 4] == '.') { + result = MakeFileName(LEVEL_WITH_EXTENSION_FILE_TYPE, name + 7, 0); + } else { + // level files can omit a file type if desired + result = MakeFileName(LEVEL_FILE_TYPE, name + 7, 0); + } + } else if (!strncmp(name, "$DATA/", 6)) { + result = MakeFileName(DATA_FILE_TYPE, name + 6, 0); + } else if (!strncmp(name, "$CODE/", 6)) { + result = MakeFileName(CODE_FILE_TYPE, name + 6, 0); + } else if (!strncmp(name, "$RES/", 5)) { + result = MakeFileName(RES_FILE_TYPE, name + 5, 0); + } else { + printf("[ERROR] DecodeFileName: UNKNOWN FILE NAME %s\n", name); + result = nullptr; + } + } else { + // if no special prefix is given, assume $CODE + result = MakeFileName(CODE_FILE_TYPE, name, 0); + } + return result; +} + +/*! + * Build a file name based on type. + * @param type: the file type. + * @param name: the file name + * @param new_string: if true, allocate a new global string for file name. + * will otherwise use a static buffer. + * DONE, Had unused int, char*, and MakeFileNameInfo params. + */ +char* MakeFileName(int type, const char* name, int new_string) { + // start with network filesystem + kstrcpy(buffer_633, "host:"); + char* buf = strend(buffer_633); + + // prefix to build directory + char prefix[64]; + kstrcpy(prefix, FOLDER_PREFIX); + + // build file name + if (type == LISTENER_TO_KERNEL_FILE_TYPE) { + kstrcpy(buf, + "kernel/LISTENERTOKERNEL"); // unused (I guess this is an old method to transfer data?) + } else if (type == KERNEL_TO_LISTENER_FILE_TYPE) { + kstrcpy(buf, + "kernel/KERNELTOLISTENER"); // unused (I guess this is an old method to transfer data?) + } else if (type == CODE_FILE_TYPE) { + sprintf(buf, "game/obj/%s.o", name); // game object file (CODE) + } else if (type == GAMEPAD_FILE_TYPE) { + sprintf(buffer_633, "pad:0"); // I guess the gamepad could be opened like a file at some point? + } else if (type == LISTENER_TO_KERNEL_LOCK_FILE_TYPE) { + kstrcpy(buf, "kernel/LISTENERTOKERNEL_LOCK"); // unused (likely used for LISTENERTOKERNEL?) + } else if (type == KERNEL_TO_LISTENER_LOCK_FILE_TYPE) { + kstrcpy(buf, "kernel/KERNELTOLISTENER_LOCK"); // unused (likley used for KERNELTOLISTENER?) + } else if (type == IOP_MODULE_FILE_TYPE) { // IOP module, overwrite the whole thing. + // this is unused, even by the remaining code to load IOP modules from the network. + // note this uses host0, which I believe is the PS2 TOOL's built in Linux SBC. + sprintf(buffer_633, "host0:/usr/local/sce/iop/modules/%s.irx", name); + } else if (type == DATA_FILE_TYPE) { + // GOAL object file, but containing data instead of code. + // likely packed by a tool that isn't the GOAL compiler. + sprintf(buf, "%sdata/%s.go", prefix, name); + } else if (type == TX_PAGE_FILE_TYPE) { + // Texture Page + // part of level files, so it has a version number. + sprintf(buf, "%sdata/texture-page%d/%s.go", prefix, TX_PAGE_VERSION, name); + } else if (type == JA_FILE_TYPE) { + // Art JA (joint animation? no idea) + // part of level files, so it has a version number + sprintf(buf, "%sdd_next/artdata%d/%s-ja.go", prefix, ART_FILE_VERSION, name); + } else if (type == JG_FILE_TYPE) { + // Art JG (joint group? no idea) + // part of level files, so it has a version number + sprintf(buf, "%sdd_next/artdata%d/%s-jg.go", prefix, ART_FILE_VERSION, name); + } else if (type == MA_FILE_TYPE) { + // Art MA (??) + // part of level files, so it has a version number + sprintf(buf, "%sdd_next/artdata%d/%s-ma.go", prefix, ART_FILE_VERSION, name); + } else if (type == MG_FILE_TYPE) { + // Art MG (??) + // part of level files, so it has a version number + sprintf(buf, "%sdd_next/artdata%d/%s-mg.go", prefix, ART_FILE_VERSION, name); + } else if (type == TG_FILE_TYPE) { + // unused, DATA TG file + sprintf(buf, "%sdata/%s-tg.go", prefix, name); + } else if (type == LEVEL_FILE_TYPE) { + // Level main file. + // part of level files, so it has a version number (a high one, 30!) + sprintf(buf, "%sdata/level%d/%s-bt.go", prefix, LEVEL_FILE_VERSION, name); + } else if (type == ART_GROUP_FILE_TYPE) { + // Level art group file. + // part of level files, so it has a version number + sprintf(buf, "%sdata/art-group%d/%s-ag.go", prefix, ART_FILE_VERSION, name); + } else if (type == VS_FILE_TYPE) { + // Level vs file, unused, unknown + // possibly early visibility file? + sprintf(buf, "%sdata/level%d/%s-vs.go", prefix, LEVEL_FILE_VERSION, name); + } else if (type == TX_FILE_TYPE) { + // Resource? TX file? some sort of texture? + sprintf(buf, "%sdata/res%d/%s-tx.go", prefix, RES_FILE_VERSION, name); + } else if (type == VS_BIN_FILE_TYPE) { + // level VS bin + // perhaps another format of early visibility data + sprintf(buf, "%sdata/level%d/%s-vs.bin", prefix, LEVEL_FILE_VERSION, name); + } else if (type == DGO_TXT_FILE_TYPE) { + // Text file in the DGO directory? + // Could have contained a list of files inside the DGO. + sprintf(buf, "%sdata/dgo%d/%s.txt", prefix, DGO_FILE_VERSION, name); + } else if (type == LEVEL_WITH_EXTENSION_FILE_TYPE) { + // Level file, but with an extension already on it. + sprintf(buf, "%sdata/level%d/%s", prefix, LEVEL_FILE_VERSION, name); + } else if (type == DATA_DGO_FILE_TYPE) { + // data DGO file (unused, all DGO/CGOs loaded through IOP) + sprintf(buf, "%sdata/dgo%d/%s.dgo", prefix, DGO_FILE_VERSION, name); + } else if (type == GAME_DGO_FILE_TYPE) { + // game DGO file (unused, all DGO/CGOs loaded through IOP) + sprintf(buf, "game/dgo%d/%s.dgo", DGO_FILE_VERSION, name); + } else if (type == DATA_CGO_FILE_TYPE) { + // data CGO file (unused, all DGO/CGOs loaded through IOP) + sprintf(buf, "%sdata/dgo%d/%s.cgo", prefix, DGO_FILE_VERSION, name); + } else if (type == GAME_CGO_FILE_TYPE) { + // game CGO file (unused, all DGO/CGOs loaded through IOP) + sprintf(buf, "game/dgo%d/%s.cgo", DGO_FILE_VERSION, name); + } else if (type == CNT_FILE_TYPE) { + // game cnt file (continue point?) + sprintf(buf, "%sdata/res%d/game-cnt.go", prefix, RES_FILE_VERSION); + } else if (type == RES_FILE_TYPE) { + // RES go file? + sprintf(buf, "%sdata/res%d/%s.go", prefix, RES_FILE_VERSION, name); + } else if (type == REFPLANT_FILE_TYPE) { + // REFPLANT? no idea + static char nextDir[] = "/"; + sprintf(buf, "%sconfig_data/refplant/%s", nextDir, name); + } else { + printf("UNKNOWN FILE TYPE %d\n", type); + } + + char* result; + if (!new_string) { + // return pointer to static filename buffer + result = buffer_633; + } else { + // or create a new string on the global heap. + int l = (int)strlen(buffer_633); + result = (char*)kmalloc(kglobalheap, l + 1, 0, "filename").c(); + kstrcpy(result, buffer_633); + } + + return result; +} + +/*! + * Does the file exist? No. It doesn't. + * @return 0 always, even if the file exists. + * DONE, EXACT, UNUSED + */ +u32 FileExists(const char* name) { + (void)name; + return 0; +} + +/*! + * Does nothing. Likely is supposed to delete a file. + * @param name + * DONE, EXACT, UNUSED + */ +void FileDelete(const char* name) { + (void)name; +} + +/*! + * Does nothing. Likely is supposed to copy a file. + * @param a + * @param b + * DONE, EXACT, UNUSED + */ +void FileCopy(const char* a, const char* b) { + (void)a; + (void)b; +} + +/*! + * Determine the file length in bytes. + * DONE, EXACT + */ +s32 FileLength(char* filename) { + s32 fd = sceOpen(filename, SCE_RDONLY); + if (fd < 0) { + MsgErr("dkernel: file length !open \'%s\' (%d)\n", filename, fd); + sceClose(fd); + return 0xfffffffb; + } else { + s32 rv = sceLseek(fd, 0, SCE_SEEK_END); + sceClose(fd); + return rv; + } +} + +/*! + * Load a file into memory + * @param name : file name + * @param heap : heap to allocate into, if memory is null + * @param memory : memory to load into. If null, allocates on the given kheap (with 64 extra bytes) + * @param malloc_flags : flags for the kmalloc + * @param size_out : file size is written here, if it's not null + * @return pointer to file data + * DONE, EXACT + */ +Ptr FileLoad(char* name, Ptr heap, Ptr memory, u32 malloc_flags, s32* size_out) { + s32 fd = sceOpen(name, SCE_RDONLY); + if (fd < 0) { + MsgErr("dkernel: file read !open \'%s\' (%d)\n", name, fd); + sceClose(fd); + return Ptr(0xfffffffb); + } + + // determine size + s32 initial_pos = sceLseek(fd, 0, SCE_SEEK_CUR); + s32 size = sceLseek(fd, 0, SCE_SEEK_END); + sceLseek(fd, initial_pos, SCE_SEEK_SET); + + if (size > 0) { + if (memory.offset == 0) { + memory = kmalloc(heap, size + 0x40, malloc_flags, name); + } + if (memory.offset == 0) { + MsgErr("dkernel: mem full for file read: '%s' (%d bytes)\n", name, size); + return Ptr(0xfffffffd); + } + + s32 read_amount = sceRead(fd, memory.c(), size); + if (read_amount == size) { + sceClose(fd); + if (size_out) + *size_out = size; + return memory; + } else { + MsgErr("dkernel: can't read full file (%d of %d): '%s'\n", read_amount, size, name); + sceClose(fd); + return Ptr(0xfffffffb); + } + } else { + return Ptr(0); + } +} + +/*! + * Write a file. + * DONE, EXACT + */ +s32 FileSave(char* name, u8* data, s32 size) { + s32 fd = sceOpen(name, SCE_WRONLY | SCE_TRUNC | SCE_CREAT); + if (fd < 0) { + MsgErr("dkernel: file write !open '%s'\n", name); + sceClose(fd); + return 0xfffffffa; + } + + if (size != 0) { + s32 written = sceWrite(fd, data, size); + if (written != size) { + MsgErr("dkernel: can't write full file '%s'\n", name); + sceClose(fd); + return 0xfffffffa; + } + } + + sceClose(fd); + return 0; +} \ No newline at end of file diff --git a/game/kernel/fileio.h b/game/kernel/fileio.h new file mode 100644 index 0000000000..d481c25d64 --- /dev/null +++ b/game/kernel/fileio.h @@ -0,0 +1,71 @@ +/*! + * @file fileio.h + * GOAL Low-Level File I/O and String Utilities + */ + +#ifndef RUNTIME_FILEIO_H +#define RUNTIME_FILEIO_H + +#include "common/common_types.h" +#include "Ptr.h" +#include "kmalloc.h" + +// GOAL File Types +enum GoalFileType { + LISTENER_TO_KERNEL_FILE_TYPE = 1, + KERNEL_TO_LISTENER_FILE_TYPE = 2, + CODE_FILE_TYPE = 3, + GAMEPAD_FILE_TYPE = 4, + LISTENER_TO_KERNEL_LOCK_FILE_TYPE = 5, + KERNEL_TO_LISTENER_LOCK_FILE_TYPE = 6, + IOP_MODULE_FILE_TYPE = 8, + DATA_FILE_TYPE = 0x20, + TX_PAGE_FILE_TYPE = 0x21, + JA_FILE_TYPE = 0x22, + JG_FILE_TYPE = 0x23, + MA_FILE_TYPE = 0x24, + MG_FILE_TYPE = 0x25, + TG_FILE_TYPE = 0x26, + LEVEL_FILE_TYPE = 0x27, + ART_GROUP_FILE_TYPE = 0x30, + VS_FILE_TYPE = 0x31, + TX_FILE_TYPE = 0x32, + VS_BIN_FILE_TYPE = 0x33, + DGO_TXT_FILE_TYPE = 0x34, + LEVEL_WITH_EXTENSION_FILE_TYPE = 0x35, + DATA_DGO_FILE_TYPE = 0x36, + GAME_DGO_FILE_TYPE = 0x37, + DATA_CGO_FILE_TYPE = 0x38, + GAME_CGO_FILE_TYPE = 0x39, + CNT_FILE_TYPE = 0x3a, + RES_FILE_TYPE = 0x3b, + REFPLANT_FILE_TYPE = 0x301, +}; + +constexpr char FOLDER_PREFIX[] = ""; + +constexpr u32 ART_FILE_VERSION = 6; +constexpr u32 LEVEL_FILE_VERSION = 30; +constexpr u32 DGO_FILE_VERSION = 1; +constexpr u32 RES_FILE_VERSION = 1; +constexpr u32 TX_PAGE_VERSION = 7; + +char* strend(char* str); +u32 ReadHufWord(u8** loc_ptr); +void kstrcpy(char* dst, const char* src); +void kstrcpyup(char* dst, const char* src); +void kstrcat(char* dest, const char* src); +void kstrncat(char* dest, const char* src, s32 count); +char* kstrinsert(char* str, char pad, s32 count); +char* basename_goal(char* s); +char* DecodeFileName(const char* name); +char* MakeFileName(int type, const char* name, int new_string); +u32 FileExists(const char* name); +void FileDelete(const char* name); +void FileCopy(const char* a, const char* b); +s32 FileLength(char* filename); +Ptr FileLoad(char* name, Ptr heap, Ptr memory, u32 malloc_flags, s32* size_out); +s32 FileSave(char* name, u8* data, s32 size); +void fileio_init_globals(); + +#endif // RUNTIME_FILEIO_H diff --git a/game/kernel/kboot.cpp b/game/kernel/kboot.cpp new file mode 100644 index 0000000000..0d06dcf659 --- /dev/null +++ b/game/kernel/kboot.cpp @@ -0,0 +1,148 @@ +/*! + * @file kboot.cpp + * GOAL Boot. Contains the "main" function to launch GOAL runtime + * DONE! + */ + +#include +#include +#include "common/common_types.h" +#include "game/sce/libscf.h" +#include "kboot.h" +#include "kmachine.h" +#include "kscheme.h" +#include "ksocket.h" +#include "klisten.h" + +using namespace ee; + +// Level to load on boot +char DebugBootLevel[64]; + +// Pass to GOAL kernel on boot +char DebugBootMessage[64]; + +// game configuration +MasterConfig masterConfig; + +// Set to 1 to kill GOAL kernel +u32 MasterExit; + +// Set to 1 to enable debug heap +u32 MasterDebug; + +// Set to 1 to load debug code +u32 DebugSegment; + +// Set to 1 to load game engine after boot automatically +u32 DiskBoot; + +void kboot_init_globals() { + strcpy(DebugBootLevel, "#f"); // no specified level + strcpy(DebugBootMessage, "play"); // play mode, the default retail mode + + MasterExit = 0; + MasterDebug = 1; + DebugSegment = 1; + DiskBoot = 0; + memset(&masterConfig, 0, sizeof(MasterConfig)); +} + +/*! + * Launch the GOAL Kernel (EE). + * DONE! + * See InitParms for launch argument details. + * @param argc : argument count + * @param argv : argument list + * @return 0 on success, otherwise failure. + * + * CHANGES: + * Added InitParms call to handle command line arguments + * Removed hard-coded debug mode disable + * Renamed from `main` to `goal_main` + * Add call to sceDeci2Reset when GOAL shuts down. + */ +s32 goal_main(int argc, const char* const* argv) { + // Initialize global variables based on command line parameters + // This call is not present in the retail version of the game + // but the function is, and it likely goes here. + InitParms(argc, argv); + + // Initialize CRC32 table for string hashing + init_crc(); + + // NTSC V1, NTSC v2, PAL CD Demo, PAL Retail + // Set up game configurations + masterConfig.aspect = (u16)sceScfGetAspect(); + masterConfig.language = (u16)sceScfGetLanguage(); + masterConfig.inactive_timeout = 0; + masterConfig.timeout = 0; + masterConfig.volume = 100; + + // Set up language configuration + if (masterConfig.language == SCE_SPANISH_LANGUAGE) { + masterConfig.language = (u16)Language::Spanish; + } else if (masterConfig.language == SCE_FRENCH_LANGUAGE) { + masterConfig.language = (u16)Language::French; + } else if (masterConfig.language == SCE_GERMAN_LANGUAGE) { + masterConfig.language = (u16)Language::German; + } else if (masterConfig.language == SCE_ITALIAN_LANGUAGE) { + masterConfig.language = (u16)Language::Italian; + } else { + // pick english by default, if language is not supported. + masterConfig.language = (u16)Language::English; + } + + // Set up aspect ratio override in demo + if (!strcmp(DebugBootMessage, "demo") || !strcmp(DebugBootMessage, "demo-shared")) { + masterConfig.aspect = SCE_ASPECT_FULL; + } + + // In retail game, disable debugging modes, and force on DiskBoot + // MasterDebug = 0; + // DiskBoot = 1; + // DebugSegment = 0; + + // Launch GOAL! + if (InitMachine() >= 0) { // init kernel + KernelCheckAndDispatch(); // run kernel + ShutdownMachine(); // kernel died, we should too. + } + + return 0; +} + +/*! + * Main loop to dispatch the GOAL kernel. + */ +void KernelCheckAndDispatch() { + while (!MasterExit) { + // try to get a message from the listener, and process it if needed + Ptr new_message = WaitForMessageAndAck(); + if (new_message.offset) { + ProcessListenerMessage(new_message); + } + + // remember the old listener function + auto old_listener = ListenerFunction->value; + // dispatch the kernel + //(**kernel_dispatcher)(); + call_goal(Ptr(kernel_dispatcher->value), 0, 0, 0, s7.offset, g_ee_main_mem); + ClearPending(); + + // if the listener function changed, it means the kernel ran it, so we should notify compiler. + if (MasterDebug && ListenerFunction->value != old_listener) { + SendAck(); + } + + usleep(1000); // todo - remove this + } +} + +/*! + * Stop running the GOAL Kernel. + * DONE, EXACT + */ +void KernelShutdown() { + MasterExit = 1; // GOAL Kernel Dispatch loop will stop now. +} diff --git a/game/kernel/kboot.h b/game/kernel/kboot.h new file mode 100644 index 0000000000..fd53697f1b --- /dev/null +++ b/game/kernel/kboot.h @@ -0,0 +1,76 @@ +/*! + * @file kboot.h + * GOAL Boot. Contains the "main" function to launch GOAL runtime. + */ + +#ifndef RUNTIME_KBOOT_H +#define RUNTIME_KBOOT_H + +#include "common/common_types.h" + +//! Supported languages. +enum class Language { + English = 0, + French = 1, + German = 2, + Spanish = 3, + Italian = 4, + Japanese = 5, + UK_English = 6, + // uk english? +}; + +struct MasterConfig { + u16 language; //! GOAL language 0 + u16 aspect; //! SCE_ASPECT 2 + u16 disable_game; // 4 + u16 inactive_timeout; // todo 6 + u16 timeout; // todo 8 + u16 volume; // todo 12 +}; + +// Level to load on boot +extern char DebugBootLevel[64]; + +// Pass to GOAL kernel on boot +extern char DebugBootMessage[64]; + +// Set to 1 to kill GOAL kernel +extern u32 MasterExit; + +// Set to 1 to enable debug heap +extern u32 MasterDebug; + +// Set to 1 to load debug code +extern u32 DebugSegment; + +// Set to 1 to load game engine after boot automatically +extern u32 DiskBoot; + +extern MasterConfig masterConfig; + +/*! + * Initialize global variables for kboot + */ +void kboot_init_globals(); + +/*! + * Launch the GOAL Kernel (EE). + * See InitParms for launch argument details. + * @param argc : argument count + * @param argv : argument list + * @return 0 on success, otherwise failure. + */ +s32 goal_main(int argc, const char* const* argv); + +/*! + * Run the GOAL Kernel. + */ +void KernelCheckAndDispatch(); + +/*! + * Stop running the GOAL Kernel. + */ +void KernelShutdown(); + +#endif // RUNTIME_KBOOT_H diff --git a/game/kernel/kdgo.cpp b/game/kernel/kdgo.cpp new file mode 100644 index 0000000000..6181c18bb2 --- /dev/null +++ b/game/kernel/kdgo.cpp @@ -0,0 +1,360 @@ +/*! + * @file kdgo.cpp + * Loading DGO Files. Also has some general SIF RPC stuff used for RPCs other than DGO loading. + * DONE! + */ + +#include +#include "kdgo.h" +#include "kprint.h" +#include "kmalloc.h" +#include "fileio.h" +#include "klink.h" +#include "game/sce/sif_ee.h" +#include "game/common/dgo_rpc_types.h" +#include "game/common/player_rpc_types.h" +#include "game/common/ramdisk_rpc_types.h" +#include "game/common/loader_rpc_types.h" +#include "game/common/play_rpc_types.h" + +using namespace ee; + +sceSifClientData cd[6]; //! client data for each IOP Remove Procedure Call. +u16 x[8]; //! stupid temporary for storing a message +u32 sShowStallMsg; //! setting to show a "stalled on iop" message +u32 sMsgNum; //! Toggle for double buffered message sending. +RPC_Dgo_Cmd* sLastMsg; //! Last DGO command sent to IOP +RPC_Dgo_Cmd sMsg[2]; //! DGO message buffers + +void kdgo_init_globals() { + memset(cd, 0, sizeof(cd)); + memset(x, 0, sizeof(x)); + sShowStallMsg = 1; + sLastMsg = nullptr; + memset(sMsg, 0, sizeof(sMsg)); +} + +/*! + * Call the given RPC with the given function number and buffers. + */ +s32 RpcCall(s32 rpcChannel, + u32 fno, + bool async, + void* sendBuff, + s32 sendSize, + void* recvBuff, + s32 recvSize) { + return sceSifCallRpc(&cd[rpcChannel], fno, async, sendBuff, sendSize, recvBuff, recvSize, nullptr, + nullptr); +} + +/*! + * GOAL Wrapper for RpcCall. + */ +u64 RpcCall_wrapper(s32 rpcChannel, + u32 fno, + u32 async, + u64 send_buff, + s32 send_size, + u64 recv_buff, + s32 recv_size) { + return sceSifCallRpc(&cd[rpcChannel], fno, async, Ptr(send_buff).c(), send_size, + Ptr(recv_buff).c(), recv_size, nullptr, nullptr); +} + +/*! + * Check if the given RPC is busy, by channel. + */ +u32 RpcBusy(s32 channel) { + return sceSifCheckStatRpc(&cd[channel].rpcd); +} + +/*! + * Wait for an RPC to not be busy. Prints a stall message if sShowStallMsg is true and we have + * to wait on the IOP. Stalling here is bad because it means the rest of the game can't run. + */ +void RpcSync(s32 channel) { + if (RpcBusy(channel)) { + if (sShowStallMsg) { + Msg(6, "STALL: [kernel] waiting for IOP on RPC port #%d\n", channel); + } + while (RpcBusy(channel)) { + // an attempt to avoid spamming SIF? + u32 i = 0; + while (i < 1000) { + i++; + } + } + } +} + +/*! + * Setup an RPC. + */ +u32 RpcBind(s32 channel, s32 id) { + while (true) { + if (sceSifBindRpc(&cd[channel], id, 1) < 0) { + MsgErr("Error: RpcBind failed on port #%d [%4.4X]\n", channel, id); + return 1; + } + Msg(6, "kernel: RPC port #%d started [%4.4X]\n", channel, id); + // FlushCache(0); + + // this was not optimized out in Jak 1, but is _almost_ optimized out in Jak 2 and later. + u32 i = 0; + while (i < 10000) { + i++; + } + + if (cd[channel].serve) { + break; + } + Msg(6, "kernel: RPC port #%d not responding.\n", channel); + // it might seem like looping here is a bad idea (unclear if sceSifBindRpc can be called + // multiple times!) but this actually happens sometimes, at least on development hardware! + // (also, it's not clear that the "serve" field having data in it really means anything - maybe + // the sceSifBindRpc doesn't wait for the connection to be fully set up? This seems likely + // because they had to put that little delay in there before checking.) + } + return 0; +} + +/*! + * Setup all RPCs + */ +u32 InitRPC() { + if (!RpcBind(PLAYER_RPC_CHANNEL, PLAYER_RPC_ID) && !RpcBind(LOADER_RPC_CHANNEL, LOADER_RPC_ID) && + !RpcBind(RAMDISK_RPC_CHANNEL, RAMDISK_RPC_ID) && !RpcBind(DGO_RPC_CHANNEL, DGO_RPC_ID) && + !RpcBind(4, 0xdeb5) && !RpcBind(PLAY_RPC_CHANNEL, PLAY_RPC_ID)) { + return 0; + } + printf("Entering endless loop ... please wait\n"); + for (;;) { + } +} + +/*! + * Send a message to the IOP to stop it. + */ +void StopIOP() { + x[2] = 0x14; // todo - this type and message + RpcSync(PLAYER_RPC_CHANNEL); + RpcCall(PLAYER_RPC_CHANNEL, 0, false, x, 0x50, nullptr, 0); + printf("IOP shut down\n"); + // sceDmaSync(0x10009000, 0, 0); + printf("DMA shut down\n"); +} + +/*! + * Send message to IOP to start loading a new DGO file + * Uses a double-buffered message buffer + * @param name: the name of the DGO file + * @param buffer1 : one of the two file loading buffers + * @param buffer2 : the other of the two file loading buffers + * @param currentHeap : the current heap (for loading directly into the heap). + * + * DONE, + * MODIFIED : Added print statement to indicate when DGO load starts. + */ +void BeginLoadingDGO(const char* name, Ptr buffer1, Ptr buffer2, Ptr currentHeap) { + u8 msgID = sMsgNum; + RPC_Dgo_Cmd* mess = sMsg + sMsgNum; + sMsgNum = sMsgNum ^ 1; // toggle message buffer. + RpcSync(DGO_RPC_CHANNEL); // make sure old RPC is finished + + // put a dummy value here just to make sure the IOP overwrites it. + sMsg[msgID].result = DGO_RPC_RESULT_INIT; // !! this is 666 + + // inform IOP of buffers + sMsg[msgID].buffer1 = buffer1.offset; + sMsg[msgID].buffer2 = buffer2.offset; + + // also give a heap pointer so it can load the last object file directly into the heap to save the + // precious time. + sMsg[msgID].buffer_heap_top = currentHeap.offset; + + // file name + strcpy(sMsg[msgID].name, name); + printf("[Begin Loading DGO RPC] %s, 0x%x, 0x%x, 0x%x\n", name, buffer1.offset, buffer2.offset, + currentHeap.offset); + + // this RPC will return once we have loaded the first object file. + // but we call async, so we don't block here. + RpcCall(DGO_RPC_CHANNEL, DGO_RPC_LOAD_FNO, true, mess, sizeof(RPC_Dgo_Cmd), mess, + sizeof(RPC_Dgo_Cmd)); + sLastMsg = mess; +} + +/*! + * Get the next object in the DGO. Will block until something is loaded. + * @param lastObjectFlag: will get set to 1 if this is the last object. + * + * DONE, + * MODIFIED : added exception if the sLastMessage isn't set (game just returns null as buffer) + */ +Ptr GetNextDGO(u32* lastObjectFlag) { + *lastObjectFlag = 1; + // Wait for RPC function to respond. This will happen once the first object file is loaded. + RpcSync(DGO_RPC_CHANNEL); + Ptr buffer(0); + if (sLastMsg) { + // if we got a good result, get pointer to object + if ((sLastMsg->result == DGO_RPC_RESULT_MORE) || (sLastMsg->result == DGO_RPC_RESULT_DONE)) { + buffer.offset = + sLastMsg->buffer1; // buffer 1 always contains location of most recently loaded object. + } + + // not the last one, so don't set the flag. + if (sLastMsg->result == DGO_RPC_RESULT_MORE) { + *lastObjectFlag = 0; + } + + // no pending message. + sLastMsg = nullptr; + } else { + // I don't see how this case can happen unless there's a bug. The game does check for this and + // nothing in this case. (maybe from GOAL this can happen?) + printf("last message not set!\n"); + } + return buffer; +} + +/*! + * Instruct the IOP to continue loading the next object. + * Only should be called once it is safe to overwrite the previous. + * @param heapPtr : pointer to heap so the IOP could try to load directly into a heap if it wants. + * This should be updated after each object file load to make sure the IOP knows the exact location + * of the end of the GOAL heap data. + * DONE, + * EXACT + */ +void ContinueLoadingDGO(Ptr heapPtr) { + u32 msgID = sMsgNum; + RPC_Dgo_Cmd* sendBuff = sMsg + sMsgNum; + sMsgNum = sMsgNum ^ 1; + sendBuff->result = DGO_RPC_RESULT_INIT; + sMsg[msgID].buffer1 = 0; + sMsg[msgID].buffer2 = 0; + sMsg[msgID].buffer_heap_top = heapPtr.offset; + // the IOP will wait for this RpcCall to continue the DGO state machine. + RpcCall(DGO_RPC_CHANNEL, DGO_RPC_LOAD_NEXT_FNO, true, sendBuff, sizeof(RPC_Dgo_Cmd), sendBuff, + sizeof(RPC_Dgo_Cmd)); + // this async RPC call will complete when the next object is fully loaded. + sLastMsg = sendBuff; +} + +/*! + * Load the TEST.DGO file. + * Presumably used for debugging DGO loads. + * We don't have the TEST.DGO file, so this isn't very useful. + * + * DONE, + * EXACT, + * UNUSED + */ +void LoadDGOTest() { + u32 lastObject = 0; + + // backup show stall message and set it to false + // EE will be loading DGO in a loop, so it will always be stalling + // no need to print it. + u32 lastShowStall = sShowStallMsg; + sShowStallMsg = 0; + + // pick somewhat arbitrary memory to load the DGO into + BeginLoadingDGO("TEST.DGO", Ptr(0x4800000), Ptr(0x4c00000), Ptr(0x4000000)); + while (true) { + // keep trying to load. + Ptr dest_buffer(0); + do { + dest_buffer = GetNextDGO(&lastObject); + } while (!dest_buffer.offset); + + // print the name of the object we loaded, its destination, and its size. + Msg(6, "Loaded %s at %8.8X length %d\n", (dest_buffer + 4).cast().c(), dest_buffer.offset, + *(dest_buffer.cast())); + if (lastObject) { + break; + } + + // okay to load the next one + ContinueLoadingDGO(Ptr(0x4000000)); + } + + sShowStallMsg = lastShowStall; +} + +/*! + * Load and link a DGO file. + * This does not use the mutli-threaded linker and will block until the entire file is done. + */ +void load_and_link_dgo(u64 name_gstr, u64 heap_info, u64 flag, u64 buffer_size) { + auto name = Ptr(name_gstr + 4).c(); + auto heap = Ptr(heap_info); + load_and_link_dgo_from_c(name, heap, flag, buffer_size); +} + +/*! + * Load and link a DGO file. + * This does not use the mutli-threaded linker and will block until the entire file is done.e + */ +void load_and_link_dgo_from_c(const char* name, Ptr heap, u32 linkFlag, s32 bufferSize) { + printf("[Load and Link DGO From C] %s\n", name); + u32 oldShowStall = sShowStallMsg; + + // remember where the heap top point is so we can clear temporary allocations + auto oldHeapTop = heap->top; + + // allocate temporary buffers from top of the given heap + // align 64 for IOP DMA + // note: both buffers named dgo-buffer-2 + auto buffer2 = kmalloc(heap, bufferSize, KMALLOC_TOP | KMALLOC_ALIGN_64, "dgo-buffer-2"); + auto buffer1 = kmalloc(heap, bufferSize, KMALLOC_TOP | KMALLOC_ALIGN_64, "dgo-buffer-2"); + + // build filename. If no extension is given, default to CGO. + char fileName[16]; + kstrcpyup(fileName, name); + if (fileName[strlen(fileName) - 4] != '.') { + strcat(fileName, ".CGO"); + } + + // no stall messages, as this is a blocking load and when spending 100% CPU time on linking, + // the linker can beat the DVD drive. + sShowStallMsg = 0; + + // start load on IOP. + BeginLoadingDGO( + fileName, buffer1, buffer2, + Ptr((heap->current + 0x3f).offset & 0xffffffc0)); // 64-byte aligned for IOP DMA + + u32 lastObjectLoaded = 0; + while (!lastObjectLoaded) { + // check to see if next object is loaded (I believe it always is?) + auto dgoObj = GetNextDGO(&lastObjectLoaded); + if (!dgoObj.offset) { + continue; + } + + // if we're on the last object, it is loaded at cheap->current. So we can safely reset the two + // dgo-buffer allocations. We do this _before_ we link! This way, the last file loaded has more + // heap available, which is important when we need to use the entire memory. + if (lastObjectLoaded) { + heap->top = oldHeapTop; + } + + // determine the size and name of the object we got + auto obj = dgoObj + 0x40; // seek past dgo object header + u32 objSize = *(dgoObj.cast()); // size from object's link block + + char objName[64]; + strcpy(objName, (dgoObj + 4).cast().c()); // name from dgo object header + printf("[link and exec] %s %d\n", objName, lastObjectLoaded); + link_and_exec(obj, objName, objSize, heap, linkFlag); // link now! + + // inform IOP we are done + if (!lastObjectLoaded) { + ContinueLoadingDGO(Ptr((heap->current + 0x3f).offset & 0xffffffc0)); + } + } + sShowStallMsg = oldShowStall; +} \ No newline at end of file diff --git a/game/kernel/kdgo.h b/game/kernel/kdgo.h new file mode 100644 index 0000000000..69e8dcae2a --- /dev/null +++ b/game/kernel/kdgo.h @@ -0,0 +1,30 @@ +/*! + * @file kdgo.h + * Loading DGO Files. Also has some general SIF RPC stuff used for RPCs other than DGO loading. + * DONE! + */ + +#ifndef JAK_V2_KDGO_H +#define JAK_V2_KDGO_H + +#include "common/common_types.h" +#include "Ptr.h" +#include "kmalloc.h" + +void kdgo_init_globals(); +u32 InitRPC(); +void load_and_link_dgo_from_c(const char* name, Ptr heap, u32 linkFlag, s32 bufferSize); +void load_and_link_dgo(u64 name_gstr, u64 heap_info, u64 flag, u64 buffer_size); +void StopIOP(); + +u64 RpcCall_wrapper(s32 rpcChannel, + u32 fno, + u32 async, + u64 send_buff, + s32 send_size, + u64 recv_buff, + s32 recv_size); +u32 RpcBusy(s32 channel); +void LoadDGOTest(); + +#endif // JAK_V2_KDGO_H diff --git a/game/kernel/kdsnetm.cpp b/game/kernel/kdsnetm.cpp new file mode 100644 index 0000000000..8dcec18a9b --- /dev/null +++ b/game/kernel/kdsnetm.cpp @@ -0,0 +1,222 @@ +/*! + * @file kdsnetm.cpp + * Low-level DECI2 wrapper for ksocket + * DONE! + */ + +#include +#include +#include +#include "game/sce/deci2.h" +#include "game/system/deci_common.h" // todo, reorganize to avoid this include +#include "kdsnetm.h" +#include "kprint.h" + +using namespace ee; + +/*! + * Current state of the GOAL Protocol + */ + +GoalProtoBlock protoBlock; + +/*! + * Initialize global variables for kdsnetm + */ +void kdsnetm_init_globals() { + protoBlock.reset(); +} + +/*! + * Register GOAL DECI2 Protocol Driver with DECI2 service + * DONE, EXACT + */ +void InitGoalProto() { + protoBlock.socket = sceDeci2Open(DECI2_PROTOCOL, &protoBlock, GoalProtoHandler); + if (protoBlock.socket < 0) { + MsgErr("gproto: open proto error\n"); + } else { + protoBlock.send_buffer = nullptr; + protoBlock.receive_buffer = MessBufArea.cast().c(); + protoBlock.send_status = -1; + protoBlock.last_receive_size = -1; + protoBlock.receive_progress = 0; + protoBlock.deci2count.offset = 0; + Msg(6, "gproto: proto open at socket %d\n", protoBlock.socket); + } +} + +/*! + * Close the DECI2 Protocol Driver + * DONE, EXACT + */ +void ShutdownGoalProto() { + if (protoBlock.socket > 0) { + sceDeci2Close(protoBlock.socket); + } +} + +/*! + * Handle a DECI2 Protocol Event for the GOAL Proto. + * Called by the DECI2 Protocol driver + * DONE, added print statements on errors for debugging, EI and SYNC at the end were removed + */ +void GoalProtoHandler(int event, int param, void* opt) { + // verify we got the correct opt pointer. It's not clear why the opt pointer is used + // like this? + GoalProtoBlock* pb = (GoalProtoBlock*)opt; + if (&protoBlock != pb) { + Msg(6, "gproto: BAD OPT POINTER PASSED IN!!!!\n"); // this print statement is in the game. + pb = &protoBlock; + } + + // increment deci2count, if it's set up + if (pb->deci2count.offset) { + *pb->deci2count = *pb->deci2count + 1; + } + + // remember what event this is + pb->most_recent_event = event; + pb->most_recent_param = param; + + switch (event) { + // get some data - param is the size + case DECI2_READ: + // sanity check the size + if (pb->receive_progress + param <= (int)DEBUG_MESSAGE_BUFFER_SIZE) { + // actually get data from DECI2 + s32 received = + sceDeci2ExRecv(pb->socket, ((u8*)pb->receive_buffer) + pb->receive_progress, param); + + if (received < 0) { + // receive failure + pb->last_receive_size = -1; + protoBlock.receive_progress = 0; // why use protoBlock instead of pb here? + printf("gproto: read error with sceDeci2ExRecv\n"); + } else { + pb->receive_progress += received; + } + } else { + // size was too large + pb->last_receive_size = -1; + protoBlock.receive_progress = 0; // why use protoBlock here? + printf("gproto: read error, message too large!\n"); + } + break; + + // read is finished! + case DECI2_READDONE: + // set last_receive_size to indicate that there is a pending message in the buffer. + pb->last_receive_size = pb->receive_progress; + pb->receive_progress = 0; + break; + + // send some data + case DECI2_WRITE: { + // note that we should not attempt to send more than 0xffff bytes at a time, or this will be + // wrong. This is correctly checked for prints, but not for outputs. + assert(pb->send_remaining < 0xffff); + // why and it with 0xffff? Seems like saturation would be better. Either way some data + // will be lost, so I guess it doesn't matter. + s32 sent = sceDeci2ExSend(pb->socket, (void*)pb->send_ptr, pb->send_remaining & 0xffff); + if (sent < 0) { + // if we got an error, put it in send status, signaling a send error (negative) + pb->send_status = sent; + } else { + // otherwise don't touch send status, leave it positive to indicate we're still sending + pb->send_ptr += sent; + pb->send_remaining -= sent; + } + } break; + + // done sending! + case DECI2_WRITEDONE: + if (pb->send_remaining <= 0) { + // if we've send everything we want, set status to zero to indicate success + pb->send_status = 0; + } else { + // otherwise, set send status to a negative number (the negative absolute value of + // remaining) + s32 a = pb->send_remaining; + if (a < 0) { + a = -a; + } + pb->send_status = -a; + } + break; + + case DECI2_CHSTATUS: + break; + + // other events are undefined, so we just error. + default: + pb->last_receive_size = -1; + break; + } +} + +/*! + * Low level DECI2 send + * Will block until send is complete. + * DONE, original version used an uncached address and had a FlushCache call, which were both + * removed + */ +s32 SendFromBufferD(s32 msg_kind, u64 p2, char* data, s32 size) { + // wait for send to finish or error first... + while (protoBlock.send_status > 0) { + // on actual PS2, the kernel will run this in another thread. + LIBRARY_sceDeci2_run_sends(); + } + + // retry at most 10 times until we complete without an error. + for (s32 i = 0; i < 10; i++) { + // or'd with 0x20000000 to get noncache version + GoalMessageHeader* header = (GoalMessageHeader*)(data - sizeof(GoalMessageHeader)); + protoBlock.send_remaining = size + sizeof(GoalMessageHeader); + protoBlock.send_buffer = header; + protoBlock.send_ptr = (u8*)header; + + protoBlock.send_status = size + sizeof(GoalMessageHeader); + // FlushCache(0); + + // set DECI2 message header + header->deci2_hdr.len = protoBlock.send_remaining; + header->deci2_hdr.rsvd = 0; + header->deci2_hdr.proto = DECI2_PROTOCOL; + header->deci2_hdr.src = 'E'; // from EE + header->deci2_hdr.dst = 'H'; // to HOST + + // set GOAL message header + header->msg_kind = (u16)msg_kind; + header->u6 = 0; + header->msg_size = size; + header->msg_id = p2; + + // start send! + auto rv = sceDeci2ReqSend(protoBlock.socket, header->deci2_hdr.dst); + if (rv < 0) { + printf("1sceDeci2ReqSend fail, reason code = %08x\n", rv); + return 0xfffffffa; + } + + // wait for send to complete or error. + while (protoBlock.send_status > 0) { + LIBRARY_sceDeci2_run_sends(); + } + + // if send completes, exit. Otherwise if there's an error, just try again. + if (protoBlock.send_status == 0) { + break; + } + } + + return 0; +} + +/*! + * Print GOAL Protocol status + */ +void GoalProtoStatus() { + Msg(6, "gproto: got %d %d\n", protoBlock.most_recent_event, protoBlock.most_recent_param); + Msg(6, "gproto: %d %d\n", protoBlock.last_receive_size, protoBlock.send_remaining); +} \ No newline at end of file diff --git a/game/kernel/kdsnetm.h b/game/kernel/kdsnetm.h new file mode 100644 index 0000000000..bdbef66096 --- /dev/null +++ b/game/kernel/kdsnetm.h @@ -0,0 +1,86 @@ +/*! + * @file kdsnetm.h + * Low-level DECI2 wrapper for ksocket + * DONE! + */ + +#ifndef JAK_KDSNETM_H +#define JAK_KDSNETM_H + +#include "Ptr.h" +#include "common/listener_common.h" + +struct GoalMessageHeader { + Deci2Header deci2_hdr; + u16 msg_kind; + u16 u6; + u32 msg_size; + u64 msg_id; +}; + +constexpr u16 DECI2_PROTOCOL = 0xe042; + +struct GoalProtoBlock { + s32 socket = 0; + GoalMessageHeader* send_buffer = nullptr; + GoalMessageHeader* receive_buffer = nullptr; + u8* send_ptr = nullptr; + s32 send_remaining = 0; + s32 send_status = + 0; // positive means send in progress, negative means send error, 0 means complete. + + // size of pending receive to process. + s32 last_receive_size = 0; + s32 receive_progress = 0; + u32 most_recent_event = 0; + u32 most_recent_param = 0; + u32 msg_kind = 0; + u64 msg_id = 0; + Ptr deci2count; + + void reset() { *this = GoalProtoBlock(); } +}; + +/*! + * Current state of the GOAL Protocol + */ +extern GoalProtoBlock protoBlock; + +/*! + * Initialize global variables for kdsnetm + */ +void kdsnetm_init_globals(); + +/*! + * Register GOAL DECI2 Protocol Driver with DECI2 service + * DONE, EXACT + */ +void InitGoalProto(); + +/*! + * Close the DECI2 Protocol Driver + * DONE, EXACT + */ +void ShutdownGoalProto(); + +/*! + * Handle a DECI2 Protocol Event for the GOAL Proto. + * Called by the DECI2 Protocol driver + * DONE, EXACT + */ +void GoalProtoHandler(int event, int param, void* data); + +/*! + * Low level DECI2 send + * Will block until send is complete. + * DONE, original version used an uncached address and had a FlushCache call, which were both + * removed + */ +s32 SendFromBufferD(s32 p1, u64 p2, char* data, s32 size); + +/*! + * Print GOAL Protocol status + */ +void GoalProtoStatus(); + +#endif // JAK_KDSNETM_H diff --git a/game/kernel/klink.cpp b/game/kernel/klink.cpp new file mode 100644 index 0000000000..38eba8ee74 --- /dev/null +++ b/game/kernel/klink.cpp @@ -0,0 +1,538 @@ +/*! + * @file klink.cpp + * GOAL Linker for x86-64 + * Note - this is significantly different from the MIPS linker because the object file format is + * different. + * DONE! + */ + +#include +#include +#include "klink.h" +#include "fileio.h" +#include "kscheme.h" +#include "kboot.h" +#include "kprint.h" +#include "common/symbols.h" + +namespace { +// turn on printf's for debugging linking issues. +constexpr bool link_debug_printfs = false; +} // namespace + +// space to store a single in-progress linking state. +link_control saved_link_control; + +// pointer to GOAL *ultimate-memcpy*, if its loaded. +Ptr gfunc_774; + +void klink_init_globals() { + saved_link_control.reset(); + gfunc_774.offset = 0; +} + +/*! + * Initialize the link control. + */ +void link_control::begin(Ptr object_file, + const char* name, + int32_t size, + Ptr heap, + uint32_t flags) { + // save data from call to begin + m_object_data = object_file; + kstrcpy(m_object_name, name); + m_object_size = size; + m_heap = heap; + m_flags = flags; + + // initialize link control + m_entry.offset = 0; + m_heap_top = m_heap->top; + m_keep_debug = false; + + if (link_debug_printfs) { + char* goal_name = object_file.cast().c(); + printf("link %s\n", goal_name); + printf("link_control::begin %c%c%c%c\n", goal_name[0], goal_name[1], goal_name[2], + goal_name[3]); + } + + // points to the beginning of the linking data + m_link_block_ptr = object_file + BASIC_OFFSET; + m_code_size = 0; + m_code_start = object_file; + m_state = 0; + m_segment_process = 0; + + ObjectFileHeader* ofh = m_link_block_ptr.cast().c(); + if (link_debug_printfs) { + printf("Object file header:\n"); + printf(" GOAL ver %d.%d obj %d len %d\n", ofh->goal_version_major, ofh->goal_version_minor, + ofh->object_file_version, ofh->link_block_length); + printf(" segment count %d\n", ofh->segment_count); + for (int i = 0; i < N_SEG; i++) { + printf(" seg %d link 0x%04x, 0x%04x data 0x%04x, 0x%04x\n", i, ofh->link_infos[i].offset, + ofh->link_infos[i].size, ofh->code_infos[i].offset, ofh->code_infos[i].size); + } + } + + m_version = ofh->object_file_version; + if (ofh->object_file_version < 4) { + // three segment file + + // seek past the header + m_object_data.offset += ofh->link_block_length; + // todo, set m_code_size + + if (m_link_block_ptr.offset < m_heap->base.offset || + m_link_block_ptr.offset >= m_heap->top.offset) { + // the link block is outside our heap, or in the top of our heap. It's somebody else's + // problem. + if (link_debug_printfs) { + printf("Link block somebody else's problem\n"); + } + + if (m_heap->base.offset <= m_object_data.offset && // above heap base + m_object_data.offset < m_heap->top.offset && // less than heap top (not needed?) + m_object_data.offset < m_heap->current.offset) { // less than heap current + if (link_debug_printfs) { + printf("Code block in the heap, kicking it out for copy into heap\n"); + } + m_heap->current = m_object_data; + } + } else { + // in our heap, we need to move it so we can free up its space later on + if (link_debug_printfs) { + printf("Link block needs to be moved!\n"); + } + + // allocate space for a new one + auto new_link_block = kmalloc(m_heap, ofh->link_block_length, KMALLOC_TOP, "link-block"); + auto old_link_block = m_link_block_ptr - BASIC_OFFSET; + + // copy it + ultimate_memcpy(new_link_block.c(), old_link_block.c(), ofh->link_block_length); + m_link_block_ptr = new_link_block + BASIC_OFFSET; + + // if we can save some memory here + if (old_link_block.offset < m_heap->current.offset) { + if (link_debug_printfs) { + printf("Kick out old link block\n"); + } + m_heap->current = old_link_block; + } + } + } else { + printf("UNHANDLED OBJECT FILE VERSION\n"); + assert(false); + } + + if ((m_flags & LINK_FLAG_FORCE_DEBUG) && MasterDebug && !DiskBoot) { + m_keep_debug = true; + } +} + +/*! + * Make progress on linking. + */ +uint32_t link_control::work() { + auto old_debug_segment = DebugSegment; + if (m_keep_debug) { + DebugSegment = s7.offset + FIX_SYM_TRUE; + } + + // set type tag of link block + *((m_link_block_ptr - 4).cast()) = *((s7 + FIX_SYM_LINK_BLOCK).cast()); + + uint32_t rv; + + if (m_version == 3) { + rv = work_v3(); + } else { + printf("UNHANDLED OBJECT FILE VERSION IN WORK!\n"); + assert(false); + return 0; + } + + DebugSegment = old_debug_segment; + return rv; +} + +/*! + * Link type pointers for a single type in "v3 equivalent" link data + * Returns a pointer to the link table data after the typelinking data. + */ +uint32_t typelink_v3(Ptr link, Ptr data) { + // get the name of the type + uint32_t seek = 0; + char sym_name[256]; + while (link.c()[seek]) { + sym_name[seek] = link.c()[seek]; + seek++; + assert(seek < 256); + } + sym_name[seek] = 0; + seek++; + + // determine the number of methods + uint8_t method_count = link.c()[seek++]; + + // intern the GOAL type, creating the vtable if it doesn't exist. + auto type_ptr = intern_type_from_c(sym_name, method_count); + + // prepare to read the locations of the type pointers + Ptr offsets = link.cast() + seek; + uint32_t offset_count = *offsets; + offsets = offsets + 4; + seek += 4; + + // write the type pointers into memory + for (uint32_t i = 0; i < offset_count; i++) { + *(data + offsets.c()[i]).cast() = type_ptr.offset; + seek += 4; + } + + return seek; +} + +/*! + * Link symbols (both offsets and pointers) in "v3 equivalent" link data. + * Returns a pointer to the link table data after the linking data for this symbol. + */ +uint32_t symlink_v3(Ptr link, Ptr data) { + // get the symbol name + uint32_t seek = 0; + char sym_name[256]; + while (link.c()[seek]) { + sym_name[seek] = link.c()[seek]; + seek++; + assert(seek < 256); + } + sym_name[seek] = 0; + seek++; + + // intern + auto sym = intern_from_c(sym_name); + int32_t sym_offset = sym.cast() - s7; + uint32_t sym_addr = sym.cast().offset; + + // prepare to read locations of symbol links + Ptr offsets = link.cast() + seek; + uint32_t offset_count = *offsets; + offsets = offsets + 4; + seek += 4; + + for (uint32_t i = 0; i < offset_count; i++) { + uint32_t offset = offsets.c()[i]; + seek += 4; + auto data_ptr = (data + offset).cast(); + + if (*data_ptr == -1) { + // a "-1" indicates that we should store the address. + *(data + offset).cast() = sym_addr; + } else { + // otherwise store the offset to st. Eventually this should become an s16 instead. + *(data + offset).cast() = sym_offset; + } + } + + return seek; +} + +/*! + * Link a single pointer. + */ +uint32_t cross_seg_dist_link_v3(Ptr link, + ObjectFileHeader* ofh, + int current_seg, + int size) { + // target seg, dist into mine, dist into target, patch loc in mine + uint8_t target_seg = *link; + assert(target_seg < ofh->segment_count); + uint32_t* link_data = (link + 1).cast().c(); + int32_t mine = link_data[0] + ofh->code_infos[current_seg].offset; + int32_t tgt = link_data[1] + ofh->code_infos[target_seg].offset; + int32_t diff = tgt - mine; + uint32_t offset_of_patch = link_data[2] + ofh->code_infos[current_seg].offset; + // printf("link object in seg %d diff %d at %d (%d + %d)\n", target_seg, diff, offset_of_patch, + // link_data[2], ofh->code_infos[current_seg].offset); + + // both 32-bit and 64-bit pointer links are supported, though 64-bit ones should disappear soon. + if (size == 4) { + *Ptr(offset_of_patch).c() = diff; + } else if (size == 8) { + *Ptr(offset_of_patch).c() = diff; + } else { + throw std::runtime_error("unknown size in cross_seg_dist_link_v3"); + } + + return 1 + 3 * 4; +} + +/*! + * Run the linker. For now, all linking is done in two runs. If this turns out to be too slow, + * this should be modified to do incremental linking over multiple runs. + */ +uint32_t link_control::work_v3() { + ObjectFileHeader* ofh = m_link_block_ptr.cast().c(); + if (m_state == 0) { + // state 0 <- copying data. + // the actual game does all copying in one shot. I assume this is ok because v3 files are just + // code and always small. Large data which takes too long to copy should use v2. + + // loop over segments + for (s32 seg_id = ofh->segment_count - 1; seg_id >= 0; seg_id--) { + // link the infos + ofh->link_infos[seg_id].offset += m_link_block_ptr.offset; + ofh->code_infos[seg_id].offset += m_object_data.offset; + + if (seg_id == DEBUG_SEGMENT) { + if (!DebugSegment) { + // clear code info if we aren't going to copy the debug segment. + ofh->code_infos[seg_id].offset = 0; + ofh->code_infos[seg_id].size = 0; + } else { + if (ofh->code_infos[seg_id].size == 0) { + // not actually present + ofh->code_infos[seg_id].offset = 0; + } else { + Ptr src(ofh->code_infos[seg_id].offset); + ofh->code_infos[seg_id].offset = + kmalloc(kdebugheap, ofh->code_infos[seg_id].size, 0, "debug-segment").offset; + if (ofh->code_infos[seg_id].offset == 0) { + MsgErr("dkernel: unable to malloc %d bytes for debug-segment\n", + ofh->code_infos[seg_id].size); + return 1; + } + ultimate_memcpy(Ptr(ofh->code_infos[seg_id].offset).c(), src.c(), + ofh->code_infos[seg_id].size); + } + } + } else if (seg_id == MAIN_SEGMENT) { + if (ofh->code_infos[seg_id].size == 0) { + ofh->code_infos[seg_id].offset = 0; + } else { + Ptr src(ofh->code_infos[seg_id].offset); + ofh->code_infos[seg_id].offset = + kmalloc(m_heap, ofh->code_infos[seg_id].size, 0, "main-segment").offset; + if (ofh->code_infos[seg_id].offset == 0) { + MsgErr("dkernel: unable to malloc %d bytes for main-segment\n", + ofh->code_infos[seg_id].size); + return 1; + } + ultimate_memcpy(Ptr(ofh->code_infos[seg_id].offset).c(), src.c(), + ofh->code_infos[seg_id].size); + } + } else if (seg_id == TOP_LEVEL_SEGMENT) { + if (ofh->code_infos[seg_id].size == 0) { + ofh->code_infos[seg_id].offset = 0; + } else { + Ptr src(ofh->code_infos[seg_id].offset); + ofh->code_infos[seg_id].offset = + kmalloc(m_heap, ofh->code_infos[seg_id].size, KMALLOC_TOP, "top-level-segment") + .offset; + if (ofh->code_infos[seg_id].offset == 0) { + MsgErr("dkernel: unable to malloc %d bytes for top-level-segment\n", + ofh->code_infos[seg_id].size); + return 1; + } + ultimate_memcpy(Ptr(ofh->code_infos[seg_id].offset).c(), src.c(), + ofh->code_infos[seg_id].size); + } + } else { + printf("UNHANDLED SEG ID IN WORK V3 STATE 1\n"); + } + } + + m_state = 1; + m_segment_process = 0; + return 0; + } else if (m_state == 1) { + // state 1: linking. For now all links are done at once. This is probably going to be fine on a + // modern computer. But the game broke this into multiple steps. + if (m_segment_process < ofh->segment_count) { + Ptr lp(ofh->link_infos[m_segment_process].offset); + + while (*lp) { + switch (*lp) { + case LINK_TABLE_END: + break; + case LINK_SYMBOL_OFFSET: + lp = lp + 1; + lp = lp + symlink_v3(lp, Ptr(ofh->code_infos[m_segment_process].offset)); + break; + case LINK_TYPE_PTR: + lp = lp + 1; // seek past id + lp = lp + typelink_v3(lp, Ptr(ofh->code_infos[m_segment_process].offset)); + break; + + case LINK_DISTANCE_TO_OTHER_SEG_64: + lp = lp + 1; + lp = lp + cross_seg_dist_link_v3(lp, ofh, m_segment_process, 8); + break; + + case LINK_DISTANCE_TO_OTHER_SEG_32: + lp = lp + 1; + lp = lp + cross_seg_dist_link_v3(lp, ofh, m_segment_process, 4); + break; + default: + printf("unknown link table thing %d\n", *lp); + exit(0); + break; + } + } + + m_segment_process++; + } else { + // all done, can set the entry point to the top-level. + m_entry = Ptr(ofh->code_infos[TOP_LEVEL_SEGMENT].offset) + 4; + return 1; + } + + return 0; + } + + else { + printf("WORK v3 INVALID STATE\n"); + return 1; + } +} + +// TODO - work_v2, once v2 objects are created. + +/*! + * Complete linking. This will execute the top-level code for v3 object files, if requested. + */ +void link_control::finish() { + CacheFlush(m_code_start.c(), m_code_size); + auto old_debug_segment = DebugSegment; + if (m_keep_debug) { + // note - this probably doesn't work because DebugSegment isn't *debug-segment*. + DebugSegment = s7.offset + FIX_SYM_TRUE; + } + if (m_flags & LINK_FLAG_FORCE_FAST_LINK) { + FastLink = 1; + } + *EnableMethodSet = *EnableMethodSet + m_keep_debug; + + ObjectFileHeader* ofh = m_link_block_ptr.cast().c(); + if (ofh->object_file_version == 3) { + // todo check function type of entry + + // execute top level! + if (m_entry.offset && (m_flags & LINK_FLAG_EXECUTE)) { + call_goal(m_entry.cast(), 0, 0, 0, s7.offset, g_ee_main_mem); + } + + // inform compiler that we loaded. + if (m_flags & LINK_FLAG_OUTPUT_LOAD) { + output_segment_load(m_object_name, m_link_block_ptr, m_flags); + } + } else { + printf("UNHANDELD OBJECT FILE VERSION IN FINISH\n"); + } + + *EnableMethodSet = *EnableMethodSet - m_keep_debug; + FastLink = 0; // nested fast links won't work right. + m_heap->top = m_heap_top; + DebugSegment = old_debug_segment; +} + +/*! + * Immediately link and execute an object file. + * DONE, EXACT + */ +Ptr link_and_exec(Ptr data, + const char* name, + int32_t size, + Ptr heap, + uint32_t flags) { + link_control lc; + lc.begin(data, name, size, heap, flags); + uint32_t done; + do { + done = lc.work(); + } while (!done); + lc.finish(); + return lc.m_entry; +} + +/*! + * Wrapper so this can be called from GOAL. Not in original game. + */ +u64 link_and_exec_wrapper(u64 data, u64 name, s64 size, u64 heap, u64 flags) { + return link_and_exec(Ptr(data), Ptr(name).c(), size, Ptr(heap), flags) + .offset; +} + +/*! + * GOAL exported function for beginning a link with the saved_link_control + * 47 -> output_load, output_true, execute, 8, force fast + * 39 -> no 8 (s7) + */ +uint64_t link_begin(uint64_t object_data, + uint64_t name, + int32_t size, + uint64_t heap, + uint32_t flags) { + saved_link_control.begin(Ptr(object_data), Ptr(name).c(), size, Ptr(heap), + flags); + auto work_result = saved_link_control.work(); + // if we managed to finish in one shot, take care of calling finish + if (work_result) { + saved_link_control.finish(); + } + + return work_result != 0; +} + +/*! + * GOAL exported function for doing a small amount of linking work on the saved_link_control + */ +uint64_t link_resume() { + auto work_result = saved_link_control.work(); + if (work_result) { + saved_link_control.finish(); + } + return work_result != 0; +} + +/*! + * The ULTIMATE MEMORY COPY + * IT IS VERY FAST + * but it may use the scratchpad. It is implemented in GOAL, and falls back to normal C memcpy + * if GOAL isn't loaded, or if the alignment isn't good enough. + */ +void* ultimate_memcpy(void* dst, void* src, uint32_t size) { + // only possible if alignment is good. + if (!(u64(dst) & 0xf) && !(u64(src) & 0xf) && !(u64(size) & 0xf)) { + if (!gfunc_774.offset) { + // GOAL function is unknown, lets see if its loaded: + auto sym = find_symbol_from_c("ultimate-memcpy"); + if (sym->value == 0) { + return memcpy(dst, src, size); + } + gfunc_774.offset = sym->value; + } + printf("calling goal um\n"); + return Ptr(call_goal(gfunc_774, make_u8_ptr(dst).offset, make_u8_ptr(src).offset, size, + s7.offset, g_ee_main_mem)) + .c(); + } else { + return memcpy(dst, src, size); + } +} + +// The functions below are not ported because they are specific to the MIPS implementation. +// In the MIPS implementation, the c_ functions are used until GOAL loads its GOAL-implemented +// versions of the same functions. The update_goal_fns detects this and causes the linker to use +// the GOAL versions once possible. The GOAL version is much faster, but functionally equivalent to +// the C version. The C version is compiled without optimization, so this isn't too surprising. +// the rellink function is unused. +/* +c_rellink3__FPvP12link_segmentPUc +c_symlink2__FPvUiPUc +c_symlink3__FPvUiPUc +update_goal_fns__Fv + */ \ No newline at end of file diff --git a/game/kernel/klink.h b/game/kernel/klink.h new file mode 100644 index 0000000000..2ba1eea78f --- /dev/null +++ b/game/kernel/klink.h @@ -0,0 +1,103 @@ +/*! + * @file klink.cpp + * GOAL Linker for x86-64 + * DONE! + */ + +#ifndef JAK_KLINK_H +#define JAK_KLINK_H + +#include "Ptr.h" +#include "kmalloc.h" +#include "common/link_types.h" +#include "common/common_types.h" + +constexpr int LINK_FLAG_OUTPUT_LOAD = 0x1; +constexpr int LINK_FLAG_OUTPUT_TRUE = 0x2; +constexpr int LINK_FLAG_EXECUTE = 0x4; +constexpr int LINK_FLAG_PRINT_LOGIN = 0x8; //! Note, doesn't actually do anything. +constexpr int LINK_FLAG_FORCE_DEBUG = 0x10; +constexpr int LINK_FLAG_FORCE_FAST_LINK = 0x20; + +/*! + * Stores the state of the linker. Used for multi-threaded linking, so it can be suspended. + */ +struct link_control { + Ptr m_object_data; //! points to the start of the object file + Ptr m_entry; //! points to first code to execute + char m_object_name[64]; //! object file name + int32_t m_object_size; //! object file size + Ptr m_heap; //! heap we are putting the object file on + uint32_t m_flags; //! linker configuration + Ptr m_heap_top; //! where to reset the heap top for clearing temp allocations + bool m_keep_debug; //! keep the debug segment, even if DebugSegment is off? + Ptr m_link_block_ptr; + uint32_t m_code_size; + Ptr m_code_start; + uint32_t m_state; + uint32_t m_segment_process; + uint32_t m_version; + void begin(Ptr object_file, + const char* name, + int32_t size, + Ptr heap, + uint32_t flags); + uint32_t work(); + uint32_t work_v3(); + void finish(); + + void reset() { + m_object_data.offset = 0; + m_entry.offset = 0; + memset(m_object_name, 0, sizeof(m_object_name)); + m_object_size = 0; + m_heap.offset = 0; + m_flags = 0; + m_heap_top.offset = 0; + m_keep_debug = false; + m_link_block_ptr.offset = 0; + m_code_size = 0; + m_code_start.offset = 0; + m_state = 0; + m_segment_process = 0; + m_version = 0; + } +}; + +struct SegmentInfo { + uint32_t offset; + uint32_t size; +}; + +struct ObjectFileHeader { + uint16_t goal_version_major; + uint16_t goal_version_minor; + uint32_t object_file_version; + uint32_t segment_count; + SegmentInfo link_infos[N_SEG]; + SegmentInfo code_infos[N_SEG]; + uint32_t link_block_length; +}; + +void klink_init_globals(); + +u64 link_and_exec_wrapper(u64 data, u64 name, s64 size, u64 heap, u64 flags); + +Ptr link_and_exec(Ptr data, + const char* name, + int32_t size, + Ptr heap, + uint32_t flags); + +uint64_t link_begin(uint64_t object_data, + uint64_t name, + int32_t size, + uint64_t heap, + uint32_t flags); + +uint64_t link_resume(); +void* ultimate_memcpy(void* dst, void* src, uint32_t size); + +extern link_control saved_link_control; + +#endif // JAK_KLINK_H diff --git a/game/kernel/klisten.cpp b/game/kernel/klisten.cpp new file mode 100644 index 0000000000..dc29e08043 --- /dev/null +++ b/game/kernel/klisten.cpp @@ -0,0 +1,156 @@ +/*! + * @file klisten.cpp + * Implementation of the Listener protocol + * Done + */ + +#include +#include "klisten.h" +#include "kboot.h" +#include "kprint.h" +#include "kdsnetm.h" +#include "ksocket.h" +#include "kmalloc.h" +#include "klink.h" +#include "kscheme.h" +#include "common/symbols.h" + +Ptr ListenerLinkBlock; +Ptr ListenerFunction; +Ptr kernel_dispatcher; +Ptr kernel_packages; +Ptr print_column; +u32 ListenerStatus; + +void klisten_init_globals() { + ListenerLinkBlock.offset = 0; + ListenerFunction.offset = 0; + kernel_dispatcher.offset = 0; + kernel_packages.offset = 0; + print_column.offset = 0; + ListenerStatus = 0; +} + +/*! + * Initialize the Listener by setting up symbols shared between GOAL and C for the listener. + * Also adds "kernel" to the kernel_packages list. + * There was an "ACK" message sent here, but this is removed because we don't need it. + */ +void InitListener() { + ListenerLinkBlock = intern_from_c("*listener-link-block*"); + ListenerFunction = intern_from_c("*listener-function*"); + kernel_dispatcher = intern_from_c("kernel-dispatcher"); + kernel_packages = intern_from_c("*kernel-packages*"); + print_column = intern_from_c("*print-column*").cast(); + ListenerLinkBlock->value = s7.offset; + ListenerFunction->value = s7.offset; + + kernel_packages->value = + new_pair(s7.offset + FIX_SYM_GLOBAL_HEAP, *((s7 + FIX_SYM_PAIR_TYPE).cast()), + make_string_from_c("kernel"), kernel_packages->value); + // if(MasterDebug) { + // SendFromBufferD(MSG_ACK, 0, AckBufArea + sizeof(GoalMessageHeader), 0); + // } +} + +/*! + * Flush pending messages. If debugging, will send to compiler, otherwise to stdout. + */ +void ClearPending() { + if (!MasterDebug) { + // if we aren't debugging print the print buffer to stdout. + if (PrintPending.offset != 0) { + auto size = strlen(PrintBufArea.cast().c() + sizeof(GoalMessageHeader)); + if (size > 0) { + printf("%s", PrintBufArea.cast().c() + sizeof(GoalMessageHeader)); + } + } + } else { + if (ListenerStatus) { + if (OutputPending.offset != 0) { + Ptr msg = OutputBufArea.cast() + sizeof(GoalMessageHeader); + auto size = strlen(msg.c()); + // note - if size is ever greater than 2^16 this will cause an issue. + SendFromBuffer(msg.c(), size); + clear_output(); + } + + if (PrintPending.offset != 0) { + char* msg = PrintBufArea.cast().c() + sizeof(GoalMessageHeader); + auto size = strlen(msg); + while (size > 0) { + // sends larger than 64 kB are broken by the GoalProtoBuffer thing, so they are split + auto send_size = size; + if (send_size > 64000) { + send_size = 64000; + } + SendFromBufferD(2, 0, msg, send_size); + size -= send_size; + msg += send_size; + } + clear_print(); + } + } + } +} + +/*! + * Send an "ack" message. The original game had the AckBufArea which stores "ack", but did not + * calculate the length correctly, so the message would not actually contain the "ack" text. + * The "ack" text is unimportant, as the compiler can recognize the messages as ACK due to the + * ListenerMessageKind::MSG_ACK field. Both the type and msg_id fields are sent, which is enough + * for it to work. + */ +void SendAck() { + if (MasterDebug) { + SendFromBufferD(u16(ListenerMessageKind::MSG_ACK), protoBlock.msg_id, + AckBufArea + sizeof(GoalMessageHeader), + strlen(AckBufArea + sizeof(GoalMessageHeader))); + } +} + +/*! + * Handle an incoming listener message + */ +void ProcessListenerMessage(Ptr msg) { + // flag that the listener is connected! + ListenerStatus = 1; + switch (protoBlock.msg_kind) { + case LTT_MSG_POKE: + // just flush any pending stuff. + ClearPending(); + break; + case LTT_MSG_INSEPCT: + inspect_object(atoi(msg.c())); + ClearPending(); + break; + case LTT_MSG_PRINT: + print_object(atoi(msg.c())); + ClearPending(); + break; + case LTT_MSG_PRINT_SYMBOLS: + printf("[ERROR] unsupported message kind LTT_MSG_PRINT_SYMBOLS (NYI)\n"); + break; + case LTT_MSG_RESET: + MasterExit = 1; + break; + case LTT_MSG_CODE: { + auto buffer = kmalloc(kdebugheap, MessCount, 0, "listener-link-block"); + memcpy(buffer.c(), msg.c(), MessCount); + ListenerLinkBlock->value = buffer.offset + 4; + // note - this will stash the linked code in the top level and free it. + // it will then be used-after-free, but this is OK because nobody else will allocate. + // the kernel dispatcher should immediately execute the listener function to avoid this + // getting squashed. + + // this setup allows listener function execution to clean up after itself. + ListenerFunction->value = + link_and_exec(buffer, "*listener*", 0, kdebugheap, LINK_FLAG_FORCE_DEBUG).offset; + return; // don't ack yet, this will happen after the function runs. + } break; + default: + MsgErr("dkernel: unknown message error: <%d> of %d bytes\n", protoBlock.msg_kind, MessCount); + break; + } + SendAck(); +} \ No newline at end of file diff --git a/game/kernel/klisten.h b/game/kernel/klisten.h new file mode 100644 index 0000000000..faf656859c --- /dev/null +++ b/game/kernel/klisten.h @@ -0,0 +1,24 @@ +/*! + * @file klisten.h + * Implementation of the Listener protocol + * Done + */ + +#ifndef JAK_KLISTEN_H +#define JAK_KLISTEN_H + +#include "kmachine.h" +#include "kscheme.h" + +extern Ptr ListenerFunction; +extern Ptr kernel_dispatcher; +extern Ptr print_column; +extern Ptr kernel_packages; + +void klisten_init_globals(); +void InitListener(); +void ClearPending(); +void SendAck(); +void ProcessListenerMessage(Ptr msg); + +#endif // JAK_KLISTEN_H diff --git a/game/kernel/kmachine.cpp b/game/kernel/kmachine.cpp new file mode 100644 index 0000000000..168619844a --- /dev/null +++ b/game/kernel/kmachine.cpp @@ -0,0 +1,618 @@ +/*! + * @file kmachine.cpp + * GOAL Machine. Contains low-level hardware interfaces for GOAL. + * Not yet done - some controller stuff isn't implemented, and also many of the SCE functions + * are just stubs or commented out for now. Legal splash screen stuff is also missing. + */ + +#include +#include +#include +#include "kmachine.h" +#include "kboot.h" +#include "kprint.h" +#include "fileio.h" +#include "kmalloc.h" +#include "kdsnetm.h" +#include "ksocket.h" +#include "kscheme.h" +#include "ksound.h" +#include "kdgo.h" +#include "ksound.h" +#include "klink.h" +#include "klisten.h" +#include "game/sce/sif_ee.h" +#include "game/sce/libcdvd_ee.h" +#include "game/sce/stubs.h" +#include "common/symbols.h" + +using namespace ee; + +/*! + * Where does OVERLORD load its data from? + */ +OverlordDataSource isodrv; + +// Get IOP modules from DVD or from dsefilesv +u32 modsrc; + +// Reboot IOP with IOP kernel from DVD/CD on boot +u32 reboot; + +u8 pad_dma_buf[2 * SCE_PAD_DMA_BUFFER_SIZE]; + +const char* init_types[] = {"fakeiso", "deviso", "iso_cd"}; + +void kmachine_init_globals() { + isodrv = iso_cd; + modsrc = 1; + reboot = 1; + memset(pad_dma_buf, 0, sizeof(pad_dma_buf)); +} + +/*! + * Initialize global variables based on command line parameters. Not called in retail versions, + * but it is present in the ELF. + * DONE + * Modified to use std::string, and removed call to fflush. + */ +void InitParms(int argc, const char* const* argv) { + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + // DVD Settings + // ---------------------------- + + // the "cd" mode uses the DVD drive for everything. This is how the game runs in retail + if (arg == "-cd") { + Msg(6, "dkernel: cd mode\n"); + isodrv = iso_cd; // use the actual DVD drive for data files + modsrc = 1; // use the DVD drive data for IOP modules + reboot = 1; // Reboot the IOP (load new IOP runtime) + } + + // the "cddata" uses the DVD drive for everything but IOP modules. + if (arg == "-cddata") { + Msg(6, "dkernel: cddata mode\n"); + isodrv = iso_cd; // tell IOP to use actual DVD drive for data files + modsrc = 0; // don't use DVD drive for IOP modules + reboot = 0; // no need to reboot the IOP + } + + // the "deviso" mode is one of two modes for testing without the need for DVDs + if (arg == "-deviso") { + Msg(6, "dkernel: deviso mode\n"); + isodrv = deviso; // IOP deviso mode + modsrc = 0; // no IOP module loading (there's no DVD to load from!) + reboot = 0; + } + + // the "fakeiso" mode is the other of two modes for testing without the need for DVDs + if (arg == "-fakeiso") { + Msg(6, "dkernel: fakeiso mode\n"); + isodrv = fakeiso; // IOP fakeeiso mode + modsrc = 0; // no IOP module loading (there's no DVD to load from!) + reboot = 0; + } + + // GOAL Settings + // ---------------------------- + + // the "demo" mode is used to pass the message "demo" to the gkernel in the DebugBootMessage + // (instead of play) + if (arg == "-demo") { + Msg(6, "dkernel: demo mode\n"); + kstrcpy(DebugBootMessage, "demo"); + } + + // the "boot" mode is used to set GOAL up for running the game in retail mode + if (arg == "-boot") { + Msg(6, "dkernel: boot mode\n"); + MasterDebug = 0; + DiskBoot = 1; + DebugSegment = 0; + } + + // the "debug" mode is used to set GOAL up for debugging/developemtn + if (arg == "-debug") { + Msg(6, "dkernel: debug mode\n"); + MasterDebug = 1; + DebugSegment = 1; + } + + // the "debug-mem" mode is used to set up GOAL in debug mode, but not to load debug-segments + if (arg == "-debug-mem") { + Msg(6, "dkernel: debug-mem mode\n"); + MasterDebug = 1; + DebugSegment = 0; + } + + // the "-level [level-name]" mode is used to inform the game to boot a specific level + // the default level is "#f". + if (arg == "-level") { + i++; + std::string levelName = argv[i]; + Msg(6, "dkernel: level %s\n", levelName.c_str()); + kstrcpy(DebugBootLevel, levelName.c_str()); + } + } +} + +/*! + * Initialize the CD Drive + * DONE, EXACT + */ +void InitCD() { + printf("Initializing CD drive\nThis may take a while ...\n"); + sceCdInit(SCECdINIT); + sceCdMmode(SCECdDVD); + while (sceCdDiskReady(0) == SCECdNotReady) { + printf("Drive not ready ... insert a disk!\n"); + } + printf("Disk type %d\n", sceCdGetDiskType()); +} + +/*! + * Initialize the I/O Processor + * Removed calls to exit(0) if loading modules fails. + */ +void InitIOP() { + // before doing anything with the I/O Processor, we need to set up SIF RPC + sceSifInitRpc(0); + + if ((isodrv == iso_cd) || modsrc || reboot) { + // we will need the DVD drive to bring up the IOP + InitCD(); + } + + if (!reboot) { + // reboot with development IOP kernel + printf("Rebooting IOP...\n"); + while (!sceSifRebootIop("host0:/usr/local/sce/iop/modules/ioprp221.img")) { + printf("Failed, retrying...\n"); + } + while (!sceSifSyncIop()) { + printf("Syncing...\n"); + } + } else { + // reboot with IOP kernel off of the disk + // reboot with development IOP kernel + printf("Rebooting IOP...\n"); + while (!sceSifRebootIop("cdrom0:\\DRIVERS\\IOPRP221.IMG;1")) { + printf("Failed, retrying...\n"); + } + while (!sceSifSyncIop()) { + printf("Syncing...\n"); + } + } + + // now that the IOP is booted with the correct kernel, we need to connect SIF RPC again + sceSifInitRpc(0); + + // if we plan to get files off of the DVD drive, we get ready to load files again. + // resetting the file system may not be needed here, but it does not hurt. + if ((isodrv == iso_cd) || modsrc) { + InitCD(); + sceFsReset(); + } + + // we begin putting together a boot command for OVERLORD, the IOP driver, which must know the data + // source and the name of the boot splash screen of the game. + char overlord_boot_command[256]; + kstrcpy(overlord_boot_command, init_types[(int)isodrv]); + char* cmd = overlord_boot_command + strlen(overlord_boot_command) + 1; + kstrcpy(cmd, "SCREEN1.USA"); + auto len = strlen(cmd); + + if (modsrc == fakeiso) { + // load from network + + if (sceSifLoadModule("host0:/usr/local/sce/iop/modules/sio2man.irx", 0, nullptr) < 0) { + MsgErr("loading sio2man.irx failed\n"); + } + + if (sceSifLoadModule("host0:/usr/local/sce/iop/modules/padman.irx", 0, nullptr) < 0) { + MsgErr("loading padman.irx failed\n"); + } + + if (sceSifLoadModule("host0:/usr/local/sce/iop/modules/libsd.irx", 0, nullptr) < 0) { + MsgErr("loading libsd.irx failed\n"); + } + + if (sceSifLoadModule("host0:/usr/local/sce/iop/modules/mcman.irx", 0, nullptr) < 0) { + MsgErr("loading mcman.irx failed\n"); + } + + if (sceSifLoadModule("host0:/usr/local/sce/iop/modules/mcserv.irx", 0, nullptr) < 0) { + MsgErr("loading mcserv.irx failed\n"); + } + + if (sceSifLoadModule("host0:/usr/home/src/989snd10/iop/989snd.irx", 0, nullptr) < 0) { + MsgErr("loading 989snd.irx failed\n"); + } + + sceSifLoadModule("host0:/usr/home/src/989snd10/iop/989ERR.IRX", 0, nullptr); + + printf("Initializing CD library\n"); + auto rv = sceSifLoadModule("host0:binee/overlord.irx", cmd + len + 1 - overlord_boot_command, + overlord_boot_command); + if (rv < 0) { + MsgErr("loading overlord.irx failed\n"); + } + } else { + // load from DVD drive + if (sceSifLoadModule("cdrom0:\\\\DRIVERS\\\\SIO2MAN.IRX;1", 0, nullptr) < 0) { + MsgErr("loading sio2man.irx failed\n"); + } + + if (sceSifLoadModule("cdrom0:\\\\DRIVERS\\\\PADMAN.IRX;1", 0, nullptr) < 0) { + MsgErr("loading padman.irx failed\n"); + } + + if (sceSifLoadModule("cdrom0:\\\\DRIVERS\\\\LIBSD.IRX;1", 0, nullptr) < 0) { + MsgErr("loading libsd.irx failed\n"); + } + + if (sceSifLoadModule("cdrom0:\\\\DRIVERS\\\\MCMAN.IRX;1", 0, nullptr) < 0) { + MsgErr("loading mcman.irx failed\n"); + } + + if (sceSifLoadModule("cdrom0:\\\\DRIVERS\\\\MCSERV.IRX;1", 0, nullptr) < 0) { + MsgErr("loading mcserv.irx failed\n"); + } + + if (sceSifLoadModule("cdrom0:\\\\DRIVERS\\\\989SND.IRX;1", 0, nullptr) < 0) { + MsgErr("loading 989snd.irx failed\n"); + } + + printf("Initializing CD library in ISO_CD mode\n"); + auto rv = sceSifLoadModule("cdrom0:\\\\DRIVERS\\\\OVERLORD.IRX;1", + cmd + len + 1 - overlord_boot_command, overlord_boot_command); + if (rv < 0) { + MsgErr("loading overlord.irx failed\n"); + } + } + auto rv = sceMcInit(); + if (rv < 0) { + MsgErr("MC driver init failed %d\n", rv); + } else { + printf("InitIOP OK\n"); + } +} + +/*! + * Initialize the GS and display the splash screen. + * Not yet implemented. TODO + */ +void InitVideo() {} + +/*! + * Initialize GOAL Runtime. This is the main initialization which is called before entering + * the GOAL kernel dispatch loop (KernelCheckAndDispatch). + * TODO finish up things which are commented. + */ +int InitMachine() { + u32 debug_heap_end = (0xffffffff - DEBUG_HEAP_SPACE_FOR_STACK + 1) & 0x7ffffff; + + // initialize the global heap + u32 global_heap_size = GLOBAL_HEAP_END - HEAP_START; + float size_mb = ((float)global_heap_size) / (float)(1 << 20); + printf("gkernel: global heap - 0x%x to 0x%x (size %.3f MB)\n", HEAP_START, GLOBAL_HEAP_END, + size_mb); + kinitheap(kglobalheap, Ptr(HEAP_START), global_heap_size); + + // initialize the debug heap, if appropriate + if (MasterDebug) { + u32 debug_heap_size = debug_heap_end - DEBUG_HEAP_START; + kinitheap(kdebugheap, Ptr(DEBUG_HEAP_START), debug_heap_size); + float debug_size_mb = ((float)debug_heap_size) / (float)(1 << 20); + float gap_size_mb = ((float)DEBUG_HEAP_START - GLOBAL_HEAP_END) / (float)(1 << 20); + printf("gkernel: debug heap - 0x%x to 0x%x (size %.3f MB, gap %.3f MB)\n", DEBUG_HEAP_START, + debug_heap_end, debug_size_mb, gap_size_mb); + } else { + // if no debug, we make the kheapinfo structure NULL so GOAL knows not to use it. + kdebugheap.offset = 0; + } + + init_output(); // GOAL input/output buffer setup + InitIOP(); // start IOP/OVERLORD, loading our legal splash screen + + // sceGsResetPath(); // reset VIF1, VU1, GIF + + InitVideo(); // display legal splash screen + + // FlushCache(WRITEBACK_DCACHE); + // FlushCache(INVALIDATE_ICACHE); + // sceGsSyncV(0); // wait for it to show up on the screen + // + // if(scePadInit(0) != 1) { // init controllers + // MsgErr("dkernel: !init pad\n"); + // } + + if (MasterDebug) { // connect to GOAL compiler + InitGoalProto(); + } + + printf("InitSound\n"); + InitSound(); // do nothing! + printf("InitRPC\n"); + InitRPC(); // connect to IOP + reset_output(); // reset output buffers + clear_print(); + + s32 goal_status = InitHeapAndSymbol(); // init GOAL runtime, load kernel and engine + if (goal_status < 0) { + return goal_status; + } + + printf("InitListenerConnect\n"); + InitListenerConnect(); + printf("InitCheckListener\n"); + InitCheckListener(); + Msg(6, "kernel: machine started\n"); + return 0; +} + +/*! + * Shutdown the runtime. + */ +int ShutdownMachine() { + StopIOP(); + CloseListener(); + ShutdownSound(); + ShutdownGoalProto(); + Msg(6, "kernel: machine shutdown"); + return 0; +} + +/*! + * Flush caches. Does all the memory, regardless of what you specify + */ +void CacheFlush(void* mem, int size) { + (void)mem; + (void)size; + // FlushCache(0); + // FlushCache(2); +} + +/*! + * Open a new controller pad. + * Set the new_pad flag to 1 and state to 0. + * Prints an error if it fails to open. + */ +u64 CPadOpen(u64 cpad_info, s32 pad_number) { + auto info = Ptr(cpad_info).c(); + if (info->cpad_file == 0) { + // not open, so we will open it + info->cpad_file = + ee::scePadPortOpen(pad_number, 0, pad_dma_buf + pad_number * SCE_PAD_DMA_BUFFER_SIZE); + if (info->cpad_file < 1) { + MsgErr("dkernel: !open cpad #%d (%d)\n", pad_number, info->cpad_file); + } + info->new_pad = 1; + info->state = 0; + } + return cpad_info; +} + +// TODO CPadGetData +void CPadGetData() { + assert(false); +} + +// TODO InstallHandler +void InstallHandler() { + assert(false); +} +// TODO InstallDebugHandler +void InstallDebugHandler() { + assert(false); +} + +/*! + * Open a file-stream. Name is a GOAL string. Mode is a GOAL symbol. Use 'read for readonly + * and anything else for write only. + */ +u64 kopen(u64 fs, u64 name, u64 mode) { + auto file_stream = Ptr(fs).c(); + file_stream->mode = mode; + file_stream->name = name; + file_stream->flags = 0; + printf("****** CALL TO kopen() ******\n"); + char buffer[128]; + sprintf(buffer, "host:%s", Ptr(name)->data()); + if (!strcmp(info(Ptr(mode))->str->data(), "read")) { + file_stream->file = sceOpen(buffer, SCE_RDONLY); + } else { + // 0x602 + file_stream->file = sceOpen(buffer, SCE_TRUNC | SCE_CREAT | SCE_WRONLY); + } + + return fs; +} + +/*! + * Get length of a file. + */ +s32 klength(u64 fs) { + auto file_stream = Ptr(fs).c(); + if ((file_stream->flags ^ 1) & 1) { + // first flag bit not set. This means no errors + auto end_seek = sceLseek(file_stream->file, 0, SCE_SEEK_END); + auto reset_seek = sceLseek(file_stream->file, 0, SEEK_SET); + if (reset_seek < 0 || end_seek < 0) { + // seeking failed, flag it + file_stream->flags |= 1; + } + return end_seek; + } else { + return 0; + } +} + +/*! + * Seek a file stream. + */ +s32 kseek(u64 fs, s32 offset, s32 where) { + s32 result = -1; + auto file_stream = Ptr(fs).c(); + if ((file_stream->flags ^ 1) & 1) { + result = sceLseek(file_stream->file, offset, where); + if (result < 0) { + file_stream->flags |= 1; + } + } + return result; +} + +/*! + * Read from a file stream. + */ +s32 kread(u64 fs, u64 buffer, s32 size) { + s32 result = -1; + auto file_stream = Ptr(fs).c(); + if ((file_stream->flags ^ 1) & 1) { + result = sceRead(file_stream->file, Ptr(buffer).c(), size); + if (result < 0) { + file_stream->flags |= 1; + } + } + return result; +} + +/*! + * Write to a file stream. + */ +s32 kwrite(u64 fs, u64 buffer, s32 size) { + s32 result = -1; + auto file_stream = Ptr(fs).c(); + if ((file_stream->flags ^ 1) & 1) { + result = sceWrite(file_stream->file, Ptr(buffer).c(), size); + if (result < 0) { + file_stream->flags |= 1; + } + } + return result; +} + +/*! + * Close a file stream. + */ +u64 kclose(u64 fs) { + auto file_stream = Ptr(fs).c(); + if ((file_stream->flags ^ 1) & 1) { + sceClose(file_stream->file); + file_stream->file = -1; + } + file_stream->flags = 0; + return fs; +} + +// TODO dma_to_iop +void dma_to_iop() { + assert(false); +} + +u64 DecodeLanguage() { + return masterConfig.language; +} + +u64 DecodeAspect() { + return masterConfig.aspect; +} + +u64 DecodeVolume() { + return masterConfig.volume; +} + +u64 DecodeTerritory() { + return 0; +} + +u64 DecodeTimeout() { + return masterConfig.timeout; +} + +u64 DecodeInactiveTimeout() { + return masterConfig.inactive_timeout; +} + +// TODO DecodeTime +void DecodeTime() { + assert(false); +} + +// TODO PutDisplayEnv +void PutDisplayEnv() { + assert(false); +} + +/*! + * Final initialization of the system after the kernel is loaded. + * This is called from InitHeapAndSymbol at the very end. + * Exports the last of the functions written in C to the GOAL symbol table + * If DiskBooting, will load the GAME CGO, containing the engine, and calls "play", the function + * which should prepare the game engine. + */ +void InitMachineScheme() { + make_function_symbol_from_c("put-display-env", (void*)PutDisplayEnv); // used in drawable + make_function_symbol_from_c("syncv", (void*)ee::sceGsSyncV); // used in drawable + make_function_symbol_from_c("sync-path", (void*)sceGsSyncPath); // used + make_function_symbol_from_c("reset-path", (void*)sceGsResetPath); // used in dma + make_function_symbol_from_c("reset-graph", (void*)sceGsResetGraph); // used + make_function_symbol_from_c("dma-sync", (void*)sceDmaSync); // used + make_function_symbol_from_c("gs-put-imr", (void*)sceGsPutIMR); // unused + make_function_symbol_from_c("gs-get-imr", (void*)sceGsGetIMR); // unused + make_function_symbol_from_c("gs-store-image", (void*)sceGsExecStoreImage); // used + make_function_symbol_from_c("flush-cache", (void*)FlushCache); // used + make_function_symbol_from_c("cpad-open", (void*)CPadOpen); // used + make_function_symbol_from_c("cpad-get-data", (void*)CPadGetData); // used + make_function_symbol_from_c("install-handler", (void*)InstallHandler); // used + make_function_symbol_from_c("install-debug-handler", (void*)InstallDebugHandler); // used + make_function_symbol_from_c("file-stream-open", (void*)kopen); // used + make_function_symbol_from_c("file-stream-close", (void*)kclose); // used + make_function_symbol_from_c("file-stream-length", (void*)klength); // used + make_function_symbol_from_c("file-stream-seek", (void*)kseek); // unused + make_function_symbol_from_c("file-stream-read", (void*)kread); // used + make_function_symbol_from_c("file-stream-write", (void*)kwrite); // used + make_function_symbol_from_c("scf-get-language", (void*)DecodeLanguage); // used + make_function_symbol_from_c("scf-get-time", (void*)DecodeTime); // used + make_function_symbol_from_c("scf-get-aspect", (void*)DecodeAspect); // used + make_function_symbol_from_c("scf-get-volume", (void*)DecodeVolume); // used + make_function_symbol_from_c("scf-get-territory", (void*)DecodeTerritory); // used + make_function_symbol_from_c("scf-get-timeout", (void*)DecodeTimeout); // used + make_function_symbol_from_c("scf-get-inactive-timeout", (void*)DecodeInactiveTimeout); // used + make_function_symbol_from_c("dma-to-iop", (void*)dma_to_iop); // unused + make_function_symbol_from_c("kernel-shutdown", (void*)KernelShutdown); // used + make_function_symbol_from_c("aybabtu", (void*)sceCdMmode); // used + InitSoundScheme(); + intern_from_c("*stack-top*")->value = 0x07ffc000; + intern_from_c("*stack-base*")->value = 0x07ffffff; + intern_from_c("*stack-size*")->value = 0x4000; + + if (DiskBoot) { + intern_from_c("*kernel-boot-message*")->value = intern_from_c(DebugBootMessage).offset; + intern_from_c("*kernel-boot-mode*")->value = intern_from_c("boot").offset; // or debug-boot + intern_from_c("*kernel-boot-level*")->value = intern_from_c(DebugBootLevel).offset; + } + + if (DiskBoot) { + *EnableMethodSet = (*EnableMethodSet) + 1; + load_and_link_dgo_from_c("game", kglobalheap, + LINK_FLAG_OUTPUT_LOAD | LINK_FLAG_EXECUTE | LINK_FLAG_PRINT_LOGIN, + 0x400000); + *EnableMethodSet = (*EnableMethodSet) - 1; + + kernel_packages->value = + new_pair(s7.offset + FIX_SYM_GLOBAL_HEAP, *((s7 + FIX_SYM_PAIR_TYPE).cast()), + make_string_from_c("engine"), kernel_packages->value); + kernel_packages->value = + new_pair(s7.offset + FIX_SYM_GLOBAL_HEAP, *((s7 + FIX_SYM_PAIR_TYPE).cast()), + make_string_from_c("art"), kernel_packages->value); + kernel_packages->value = + new_pair(s7.offset + FIX_SYM_GLOBAL_HEAP, *((s7 + FIX_SYM_PAIR_TYPE).cast()), + make_string_from_c("common"), kernel_packages->value); + + printf("calling play!\n"); + call_goal_function_by_name("play"); + } +} diff --git a/game/kernel/kmachine.h b/game/kernel/kmachine.h new file mode 100644 index 0000000000..01ab86b10a --- /dev/null +++ b/game/kernel/kmachine.h @@ -0,0 +1,115 @@ +/*! + * @file kmachine.h + * GOAL Machine. Contains low-level hardware interfaces for GOAL. + */ + +#ifndef RUNTIME_KMACHINE_H +#define RUNTIME_KMACHINE_H + +#include "common/common_types.h" +#include "Ptr.h" + +//! How much space to leave for the stack when creating the debug heap +constexpr u32 DEBUG_HEAP_SPACE_FOR_STACK = 0x4000; + +//! First free address for the GOAL heap +constexpr u32 HEAP_START = 0x13fd20; + +//! Where to end the global heap so it doesn't overlap with the stack. +constexpr u32 GLOBAL_HEAP_END = 0x1ffc000; + +//! Location of kglobalheap, kdebugheap kheapinfo structures. +constexpr u32 GLOBAL_HEAP_INFO_ADDR = 0x13AD00; +constexpr u32 DEBUG_HEAP_INFO_ADDR = 0x13AD10; + +//! Where to place the debug heap +constexpr u32 DEBUG_HEAP_START = 0x5000000; + +/*! + * Where does OVERLORD load its data from? + */ +enum OverlordDataSource : u32 { + fakeiso = 0, //! some sort of development way of getting data + deviso = 1, //! some sort of development way of getting data + iso_cd = 2, //! use the actual DVD drive +}; + +extern OverlordDataSource isodrv; + +// Get IOP modules from DVD or from dsefilesv +extern u32 modsrc; + +// Reboot IOP on start? +extern u32 reboot; + +/*! + * Initialize globals for kmachine. + * This should be called before running main. + */ +void kmachine_init_globals(); + +/*! + * Initialize global variables based on command line parameters + */ +void InitParms(int argc, const char* const* argv); + +/*! + * Initialize the CD Drive + */ +void InitCD(); + +/*! + * Initialize the I/O Processor + */ +void InitIOP(); + +/*! + * Initialize the GS and display the splash screen. + */ +void InitVideo(); + +/*! + * Initialze GOAL Runtime + */ +int InitMachine(); + +/*! + * Shutdown GOAL runtime. + */ +int ShutdownMachine(); + +/*! + * Flush caches. Does all the memory, regardless of what you specify + */ +void CacheFlush(void* mem, int size); + +void InitMachineScheme(); + +//! Mirror of cpad-info +struct CpadInfo { + u8 valid; + u8 status; + s16 button0; + u8 rx; + u8 ry; + u8 lx; + u8 ly; + u8 abutton[12]; + u8 dummy[12]; + s32 number; + s32 cpad_file; + u8 _pad0[36]; + s32 new_pad; + s32 state; +}; + +struct FileStream { + u32 flags; + u32 mode; // basic + u32 name; // basic + s32 file; // int32 +}; + +// static_assert(offsetof(CpadInfo, new_pad) == 76, "cpad type offset"); + +#endif // RUNTIME_KMACHINE_H diff --git a/game/kernel/kmalloc.cpp b/game/kernel/kmalloc.cpp new file mode 100644 index 0000000000..5a7ed0cddf --- /dev/null +++ b/game/kernel/kmalloc.cpp @@ -0,0 +1,182 @@ +/*! + * @file kmalloc.cpp + * GOAL Kernel memory allocator. + * Simple two-sided bump allocator. + * DONE + */ + +#include +#include "kmalloc.h" +#include "kprint.h" +#include "kscheme.h" + +// global and debug kernel heaps +Ptr kglobalheap; +Ptr kdebugheap; + +void kmalloc_init_globals() { + // _globalheap and _debugheap + kglobalheap.offset = GLOBAL_HEAP_INFO_ADDR; + kdebugheap.offset = DEBUG_HEAP_INFO_ADDR; +} + +/*! + * In the game, this wraps PS2's libc's malloc/calloc. + * These don't work with GOAL's custom memory management, and this function + * is unused. + * DONE, malloc/calloc calls commented out because memory allocated with calloc/malloc + * cannot trivially be accessed from within GOAL. + */ +Ptr ksmalloc(Ptr heap, s32 size, u32 flags, char const* name) { + (void)heap; + (void)size; + (void)name; + printf("[ERROR] ksmalloc : cannot be used!\n"); + u32 align = flags & 0xfff; + Ptr mem; + + if ((flags & KMALLOC_MEMSET) == 0) { + // mem = malloc(size + align); + } else { + // mem = calloc(1, size + align); + } + + if (align == KMALLOC_ALIGN_64) { + mem.offset = (mem.offset + 0x3f) & 0xffffffc0; + } else if (align == KMALLOC_ALIGN_256) { + mem.offset = (mem.offset + 0xff) & 0xffffff00; + } + + return mem; +} + +/*! + * Print the status of a kheap. This prints to stdout on the runtime, + * which will not be sent to the Listener. + * DONE, EXACT + */ +void kheapstatus(Ptr heap) { + Msg(6, + "[%8x] kheap\n" + "\tbase: #x%x\n" + "\ttop-base: #x%x\n" + "\tcur: #x%x\n" + "\ttop: #x%x\n", + heap.offset, heap->base.offset, heap->top_base.offset, heap->current.offset, + heap->top.offset); + Msg(6, + "\t used bot: %d of %d bytes\n" + "\t used top: %d of %d bytes\n" + "\t symbols: %d of %d\n", + heap->current - heap->base, heap->top_base - heap->base, heap->top_base - heap->top, + heap->top_base - heap->base, NumSymbols, GOAL_MAX_SYMBOLS); + + if (heap == kglobalheap) { + Msg(6, "\t %d bytes before stack\n", GLOBAL_HEAP_END - heap->current.offset); + } +} + +/*! + * Initialize a kheapinfo structure, and clear the kheap's memory to 0. + * DONE, EXACT + */ +Ptr kinitheap(Ptr heap, Ptr mem, s32 size) { + heap->base = mem; + heap->current = mem; + heap->top = mem + size; + heap->top_base = heap->top; + std::memset(mem.c(), 0, size); + return heap; +} + +/*! + * Return how much of the bottom (non-temp) allocator is used. + * DONE, EXACT + */ +u32 kheapused(Ptr heap) { + return heap->current - heap->base; +} + +/*! + * Allocate memory using bump allocation strategy. + * @param heapPtr : heap to allocate on. If null heap, use global but print a warning + * @param size : size of memory needed + * @param flags : flags for alignment, top/bottom allocation, set to zero + * @param name : name of allocation (printed if things go wrong) + * @return : memory. 0 if we run out of room + * DONE, PRINT ADDED + */ +Ptr kmalloc(Ptr heap, s32 size, u32 flags, char const* name) { + uint32_t alignment_flag = flags & 0xfff; + + // if we got a null heap, put it on the global heap, but warn about it + if (!heap.offset) { + Msg(6, "-----------> kmalloc: alloc %s, mem %s #x%x (a:%d %dbytes)\n", "DEBUG", name, -1, + alignment_flag, size); + heap = kglobalheap; + } + + uint32_t memstart; + + if (!(flags & KMALLOC_TOP)) { + // allocate from bottom + if (alignment_flag == KMALLOC_ALIGN_64) + memstart = (0xffffffc0 & (heap->current.offset + 0x40 - 1)); + else if (alignment_flag == KMALLOC_ALIGN_256) + memstart = (0xffffff00 & (heap->current.offset + 0x100 - 1)); + else // includes 0x10! + memstart = (0xfffffff0 & (heap->current.offset + 0x10 - 1)); + + if (size == 0) { + Msg(6, "[WARNING] kmalloc : size 0 allocation from bottom.\n"); + return Ptr(memstart); + } + + uint32_t memend = memstart + size; + + if (heap->top.offset < memend) { + kheapstatus(heap); + Msg(6, "kmalloc: !alloc mem %s (%d bytes) heap %x\n", name, size, heap.offset); + return Ptr(0); + } + + heap->current.offset = memend; + if (flags & KMALLOC_MEMSET) + std::memset(Ptr(memstart).c(), 0, (size_t)size); + return Ptr(memstart); + } else { + // allocate from top + if (alignment_flag == 0) { + alignment_flag = KMALLOC_ALIGN_16; + } + + memstart = (heap->top.offset - size) & (-alignment_flag); + + if (size == 0) { + Msg(6, "[WARNING] kmalloc : size 0 allocation from top\n"); + return Ptr(memstart); + } + + if (heap->current.offset >= memstart) { + Msg(6, "kmalloc: !alloc mem from top %s (%d bytes) heap %x\n", name, size, heap.offset); + kheapstatus(heap); + return Ptr(0); + } + + heap->top.offset = memstart; + + if (flags & 0x1000) + std::memset(Ptr(memstart).c(), 0, (size_t)size); + return Ptr(memstart); + } +} + +/*! + * GOAL does not support automatic freeing of memory. This function does nothing. + * Programmers wishing to free memory must do it themselves. + * DONE, PRINT ADDED + */ +void kfree(Ptr a) { + (void)a; + Msg(6, "[ERROR] kmalloc: kfree called\n"); +} diff --git a/game/kernel/kmalloc.h b/game/kernel/kmalloc.h new file mode 100644 index 0000000000..4969567277 --- /dev/null +++ b/game/kernel/kmalloc.h @@ -0,0 +1,46 @@ +/*! + * @file kmalloc.h + * GOAL Kernel memory allocator. + * Simple two-sided bump allocator. + * DONE + */ + +#ifndef JAK_KMALLOC_H +#define JAK_KMALLOC_H + +#include "common/common_types.h" +#include "Ptr.h" +#include "kmachine.h" + +/*! + * A kheap has a top/bottom linear allocator + */ +struct kheapinfo { + Ptr base; //! beginning of heap + Ptr top; //! current location of bottom of top allocations + Ptr current; //! current location of top of bottom allocations + Ptr top_base; //! end of heap +}; + +// Kernel heaps +extern Ptr kglobalheap; +extern Ptr kdebugheap; + +// flags for kmalloc/ksmalloc +constexpr u32 KMALLOC_TOP = 0x2000; //! Flag to allocate temporary memory from heap top +constexpr u32 KMALLOC_MEMSET = 0x1000; //! Flag to clear memory +constexpr u32 KMALLOC_ALIGN_256 = 0x100; +constexpr u32 KMALLOC_ALIGN_64 = 0x40; +constexpr u32 KMALLOC_ALIGN_16 = 0x10; + +// kmalloc funcions +Ptr ksmalloc(Ptr heap, s32 size, u32 flags, char const* name); +void kheapstatus(Ptr heap); +Ptr kinitheap(Ptr heap, Ptr mem, s32 size); +u32 kheapused(Ptr heap); +Ptr kmalloc(Ptr heap, s32 size, u32 flags, char const* name); +void kfree(Ptr a); + +void kmalloc_init_globals(); + +#endif // JAK_KMALLOC_H diff --git a/game/kernel/kmemcard.cpp b/game/kernel/kmemcard.cpp new file mode 100644 index 0000000000..e8279830dd --- /dev/null +++ b/game/kernel/kmemcard.cpp @@ -0,0 +1,214 @@ +/*! + * @file kmemcard.cpp + * Memory card interface. Very messy code. + */ + +//#include "ps2/SCE_MC.h" +//#include "ps2/SCE_FS.h" +//#include "ps2/common_types.h" +//#include "kernel/kmachine.h" +#include "kmemcard.h" + +// static s32 next; +// static s32 language; +// static MemoryCardOperation op; +// static mc_info mc[2]; + +void kmemcard_init_globals() { + // next = 0; +} + +///*! +// * Get a new memory card handle. +// * Will never return 0. +// */ +// s32 new_mc_handle() { +// s32 handle = next++; +// +// // if you wrap around, it avoid the zero handle. +// // it doesn't seem like you will need billions of memory card handles +// if(handle == 0) { +// handle = next++; +// } +// return handle; +//} +// +///*! +// * A questionable checksum. +// */ +// u32 mc_checksum(Ptr data, s32 size) { +// if(size < 0) { +// size += 3; +// } +// +// u32 result = 0; +// u32* data_u32 = (u32*)data.c(); +// for(s32 i = 0; i < size / 4; i++) { +// result = result << 1 ^ result >> 0x1f ^ data_u32[i*4] ^ 0x12345678; +// } +// +// return result ^ 0xedd1e666; +//} +// +// u32 handle_to_slot(s32 handle, s32 p2) { +// if(mc[0].p0 == p2 && mc[0].handle == handle) { +// return 0; +// } +// if(mc[1].p0 == p2 && mc[0].handle == handle) { +// return 1; +// } else { +// return -1; +// } +//} +// +// void MC_run() { +// +//} +// +///*! +// * Set the language or something. +// */ +// void MC_set_language(s32 l) { +// printf("Language set to %d\n", l); +// language = l; +//} +// +// u64 MC_format(s32 param) { +// u64 can_add = op.operation == NO_OP; +// if(can_add) { +// op.operation = FORMAT; +// op.result = 0; +// op.f_10 = 100; +// op.param = param; +// } +// return can_add; +//} +// +// +// u64 MC_unformat(s32 param) { +// u64 can_add = op.operation == NO_OP; +// if(can_add) { +// op.operation = UNFORMAT; +// op.result = 0; +// op.f_10 = 100; +// op.param = param; +// } +// return can_add; +//} +// +// u64 MC_createfile(s32 param, Ptr data) { +// u64 can_add = op.operation == NO_OP; +// if(can_add) { +// op.operation = CREATE_FILE; +// op.result = 0; +// op.f_10 = 100; +// op.param = param; +// op.data_ptr = data; +// } +// return can_add; +//} +// +// u64 MC_save(s32 param, s32 param2, Ptr data, Ptr data2) { +// u64 can_add = op.operation == NO_OP; +// if(can_add) { +// op.operation = SAVE; +// op.result = 0; +// op.f_10 = 100; +// op.param = param; +// op.param2 = param2; +// op.data_ptr = data; +// op.data_ptr2 = data2; +// } +// return can_add; +//} +// +// u64 MC_load(s32 param, s32 param2, Ptr data) { +// u64 can_add = op.operation == NO_OP; +// if(can_add) { +// op.operation = LOAD; +// op.result = 0; +// op.f_10 = 100; +// op.param = param; +// op.param2 = param2; +// op.data_ptr = data; +// } +// return can_add; +//} +// +///*! +// * Some sort of test function for memory card stuff. +// */ +// void MC_makefile(s32 port, s32 size) { +// sceMcMkdir(port, 0, "/BASCUS-00000XXXXXXXX"); +// // wait for operation to complete +// s32 cmd, result, fd; +// sceMcSync(0, &cmd, &result); +// +// if(result == sceMcResSucceed || result == sceMcResNoEntry) { +// // it worked, or the folder already exists... +// +// // open file +// sceMcOpen(port, 0, "/BASCUS-00000XXXXXXXX/BASCUS-00000XXXXXXXX", SCE_CREAT | SCE_WRONLY); +// sceMcSync(0, &cmd, &fd); +// +// if(result < 0) { +// printf("Can\'t open file on memcard [%d]\n", result); +// } else { +// // write some random crap into the memory card. +// sceMcWrite(fd, Ptr(0x1000000).c(), size); +// sceMcSync(0, &cmd, &result); +// if(result != size) { +// printf("Only written %d bytes\n", result); +// } +// sceMcClose(fd); +// sceMcSync(0, &cmd, &result); +// } +// } else { +// printf("Can\'t create garbage folder [%d]\n", result); +// } +//} +// +// u32 MC_check_result() { +// return op.result; +//} +// +// void MC_get_status(s32 slot, Ptr info) { +// info->handle = 0; +// info->known = 0; +// info->formatted = 0; +// info->initted = 0; +// for(s32 i = 0; i < 4; i++) { +// info->files[i].present = 0; +// } +// info->last_file = 0xffffffff; +// info->mem_required = SAVE_SIZE; +// info->mem_actual = 0; +// +// switch(mc[slot].p0) { +// case 1: +// info->known = 1; +// break; +// case 2: +// info->known = 1; +// info->handle = mc[slot].handle; +// break; +// case 3: +// info->known = 1; +// info->handle = mc[slot].handle; +// info->formatted = 1; +// if(mc[slot].inited == 0) { +// info->mem_actual = mc[slot].mem_actual; +// } else { +// info->initted = 1; +// for(s32 file = 0; file < 4; file++) { +// info->files[file].present = mc[slot].files[file].present; +// for(s32 i = 0; i < 64; i++) { // actually a loop over u32's +// info->files[file].data[i] = mc[slot].files[file].data[i]; +// } +// } +// info->last_file = mc[slot].last_file; +// +// } +// } +// +//} \ No newline at end of file diff --git a/game/kernel/kmemcard.h b/game/kernel/kmemcard.h new file mode 100644 index 0000000000..3805724d4a --- /dev/null +++ b/game/kernel/kmemcard.h @@ -0,0 +1,82 @@ +/*! + * @file kmemcard.h + * Memory card interface. Very messy code. + */ + + +#ifndef JAK_KMEMCARD_H +#define JAK_KMEMCARD_H + +#include "common/common_types.h" +#include "kmachine.h" + +void kmemcard_init_globals(); + +constexpr s32 SAVE_SIZE = 0x2b3; // likely different by versions! + +enum MemoryCardOperationKind { + NO_OP = 0, + FORMAT = 1, + UNFORMAT = 2, + CREATE_FILE = 3, + SAVE = 4, + LOAD = 5, +}; + +struct MemoryCardOperation { + uint32_t operation; + uint32_t param; + uint32_t param2; + uint32_t result; + uint32_t f_10; + Ptr data_ptr; + Ptr data_ptr2; +}; + +struct mc_file_info { + u32 present; + u8 data[64]; +}; + +struct mc_file_info_2 { + u32 present; + u32 pad1; + u32 pad2; + u8 data[64]; +}; + +struct mc_slot_info { + u32 handle; + u32 known; + u32 formatted; + u32 initted; + u32 last_file; + u32 mem_required; + u32 mem_actual; + mc_file_info files[4]; +}; + +struct mc_info { + s32 p0; + s32 handle; + s32 inited; + s32 mem_actual; + s32 last_file; + mc_file_info_2 files[4]; +}; + +s32 new_mc_handle(); +u32 mc_checksum(Ptr data, s32 size); +u32 handle_to_slot(s32 p1, s32 p2); +void MC_run(); +void MC_set_language(s32 lang); +u64 MC_format(s32 param); +u64 MC_unformat(s32 param); +u64 MC_createfile(s32 param, Ptr data); +u64 MC_save(s32 param, s32 param2, Ptr data, Ptr data2); +u64 MC_load(s32 param, s32 param2, Ptr data); +void MC_makefile(s32 port, s32 size); +u32 MC_check_result(); +void MC_get_status(s32 slot, Ptr info); + +#endif // JAK_KMEMCARD_H diff --git a/game/kernel/kprint.cpp b/game/kernel/kprint.cpp new file mode 100644 index 0000000000..59b086a667 --- /dev/null +++ b/game/kernel/kprint.cpp @@ -0,0 +1,1079 @@ +/*! + * @file kprint.cpp + * GOAL Print. Contains GOAL I/O, Print, Format... + */ + +#include +#include +#include +#include + +#include "common/common_types.h" +#include "kprint.h" +#include "kmachine.h" +#include "kboot.h" +#include "kmalloc.h" +#include "kdsnetm.h" +#include "fileio.h" +#include "kscheme.h" +#include "klisten.h" +#include "klink.h" +#include "common/symbols.h" + +/////////// +// SDATA +/////////// + +// Pointer set to something in the middle of the output buffer, if there is something in the buffer. +Ptr OutputPending; + +// Pointer set to something in the middle of the print buffer, if there is something in the buffer +Ptr PrintPending; + +// Size of incoming message. +s32 MessCount; + +// Pointer to message buffer, the compiler to target buffer +Ptr MessBufArea; + +// Pointer to the output buffer, the runtime to compiler buffer +Ptr OutputBufArea; + +// Pointer to print buffer, the buffer for printing and string formatting. +Ptr PrintBufArea; + +// integer printing conversion table +char ConvertTable[16]; + +// buffer for sending an "acknowledge" message to the compiler +char AckBufArea[40]; + +/*! + * Initialize global variables for kprint + */ +void kprint_init_globals() { + OutputPending.offset = 0; + PrintPending.offset = 0; + MessCount = 0; + MessBufArea.offset = 0; + OutputBufArea.offset = 0; + PrintBufArea.offset = 0; + memcpy(ConvertTable, "0123456789abcdef", 16); + memset(AckBufArea, 0, sizeof(AckBufArea)); +} + +/*! + * Initialize GOAL Kernel printing/messaging system. + * Allocates buffers. + * DONE, EXACT + */ +void init_output() { + if (MasterDebug) { + MessBufArea = kmalloc(kdebugheap, DEBUG_MESSAGE_BUFFER_SIZE, KMALLOC_MEMSET | KMALLOC_ALIGN_256, + "mess-buf"); + OutputBufArea = kmalloc(kdebugheap, DEBUG_OUTPUT_BUFFER_SIZE, + KMALLOC_MEMSET | KMALLOC_ALIGN_256, "output-buf"); + PrintBufArea = kmalloc(kdebugheap, DEBUG_PRINT_BUFFER_SIZE, KMALLOC_MEMSET | KMALLOC_ALIGN_256, + "print-buf"); + } else { + // no compiler connection, so we do not allocate buffers + MessBufArea = Ptr(0); + OutputBufArea = Ptr(0); + + // we still need a (small) print buffer for string maniuplation and debugging prints. + PrintBufArea = + kmalloc(kglobalheap, PRINT_BUFFER_SIZE, KMALLOC_MEMSET | KMALLOC_ALIGN_256, "print-buf"); + } +} + +/*! + * Empty output buffer (only if MasterDebug) + * DONE + * EXACT + */ +void clear_output() { + if (MasterDebug) { + kstrcpy((char*)Ptr(OutputBufArea + sizeof(GoalMessageHeader)).c(), ""); + OutputPending = Ptr(0); + } +} + +/*! + * Clear all data in the print buffer + * DONE + * EXACT + */ +void clear_print() { + *Ptr(PrintBufArea + sizeof(GoalMessageHeader)) = 0; + PrintPending = Ptr(0); +} + +/*! + * Buffer message to compiler indicating the target has reset. + * Write to the beginning of the output buffer. + * DONE, EXACT + */ +void reset_output() { + if (MasterDebug) { + sprintf(OutputBufArea.cast().c() + sizeof(GoalMessageHeader), "reset #x%x\n", s7.offset); + OutputPending = OutputBufArea + sizeof(GoalMessageHeader); + } +} + +/*! + * Buffer message to compiler indicating some object file has been unloaded. + * DONE, EXACT + */ +void output_unload(const char* name) { + if (!MasterDebug) { + sprintf(strend(OutputBufArea.cast().c() + sizeof(GoalMessageHeader)), "unload \"%s\"\n", + name); + OutputPending = OutputBufArea + sizeof(GoalMessageHeader); + } +} + +/*! + * Buffer message to compiler indicating some object file has been loaded. + */ +void output_segment_load(const char* name, Ptr link_block, u32 flags) { + if (MasterDebug) { + char* buffer = strend(OutputBufArea.cast().c() + sizeof(GoalMessageHeader)); + char true_str[] = "t"; + char false_str[] = "nil"; + char* flag_str = (flags & LINK_FLAG_OUTPUT_TRUE) ? true_str : false_str; + auto lbp = link_block.cast(); + sprintf(buffer, "load \"%s\" %s #x%x #x%x #x%x\n", name, flag_str, lbp->code_infos[0].offset, + lbp->code_infos[1].offset, lbp->code_infos[2].offset); + OutputPending = OutputBufArea + sizeof(GoalMessageHeader); + } +} + +/*! + * Print to the GOAL print buffer from C + * seeks PrintPending to begining of what was just printed. + * This is a different behavior from all the other prints! + * DONE, EXACT + */ +void cprintf(const char* format, ...) { + va_list args; + va_start(args, format); + char* str = PrintPending.cast().c(); + if (!PrintPending.offset) + str = PrintBufArea.cast().c() + sizeof(GoalMessageHeader); + PrintPending = make_ptr(strend(str)).cast(); + vsprintf((char*)PrintPending.c(), format, args); + + va_end(args); +} + +/*! + * Print directly to the C stdout + * The "k" parameter is ignored, so this is just like printf + * DONE, EXACT + */ +void Msg(s32 k, const char* format, ...) { + (void)k; + va_list args; + va_start(args, format); + vprintf(format, args); + va_end(args); +} + +/*! + * Print directly to the C stdout + * This is idential to Msg + * DONE, EXACT + */ +void MsgWarn(const char* format, ...) { + va_list args; + va_start(args, format); + vprintf(format, args); + va_end(args); +} + +/*! + * Print directly to the C stdout + * This is idential to Msg + * DONE, EXACT + */ +void MsgErr(const char* format, ...) { + va_list args; + va_start(args, format); + vprintf(format, args); + va_end(args); +} + +/*! + * Reverse string in place. + * DONE, EXACT + */ +void reverse(char* str) { + s32 i = 0; + s32 end = (s32)strlen(str); + while (end--, i < end) { + char c = str[i]; + str[i] = str[end]; + str[end] = c; + i++; + } +} + +/*! + * Some sort of rounding for printing floating point numbers. + * It is unused and believed to be not correct. + * Currently copy-pasta from GHIDRA + */ +char* round(float x, s32* param1, char* start, char* sEnd, char padchar, s32* param4) { + char cVar1; + char* local_58; + float f; + + if (x == 0.00000000) { + f = padchar - '0'; + } else { + modff(x * 10.00000000, &f); + } + if (4.00000000 < f) { + while (true) { + if (*sEnd == '.') { + sEnd = sEnd + -1; + } + cVar1 = *sEnd; + *sEnd = cVar1 + 1; + if ((char)(cVar1 + 1) <= '9') { + return start; + } + *sEnd = '0'; + if (sEnd == start) + break; + sEnd = sEnd + -1; + } + if (param1 == (int*)0x0) { + sEnd[-1] = '1'; + local_58 = start + -1; + } else { + *sEnd = '1'; + *param1 = *param1 + 1; + local_58 = start; + } + } else { + local_58 = start; + if (*param4 == 0x2d) { + while (true) { + if (*sEnd == '.') { + sEnd = sEnd + -1; + } + local_58 = start; + if (*sEnd != '0') + break; + if (sEnd == start) { + *param4 = 0; + } + sEnd = sEnd + -1; + } + } + } + return local_58; +} + +/*! + * Convert floating point to string intermediate function. + * Places a null character as the first character, then the integers, decimal, fraction digits + * Presumably, if rounding worked, it would sometimes add an additional digit in front instead of + * the null but rounding doesn't, so you get a null in front every time. There is no null terminator + * added, you have to do it yourself. returned value is length : buff[last_char] - buff[1] (not + * including the leading null) if the number is negative, *lead_char = '-'. Otherwise it's '\0'. + * The negative is not inserted for you. + * The precision is how many digits after decimal to print. + * The flag could have a 1 to enable rounding, but this doesn't work, so don't use it. + * Without rounding the printing is a little bit off but you don't notice unless you look too + * closely. + * + * DONE, added some sanity checks and removed support for "rounding" as round isn't implemented and + * rounding is never used in the game. + */ +s32 cvt_float(float x, s32 precision, s32* lead_char, char* buff_start, char* buff_end, u32 flags) { + // put a null at the beginning of the output + *buff_start = 0; + s32 forward_count = 0; + + // compute absolute value and set lead char to `-` if needed. + float abs_x; + if (x < 0.0f) { + abs_x = -x; + *lead_char = '-'; + } else { + *lead_char = 0; + abs_x = x; + } + + // nan check + u32 abs_x_u32 = *(u32*)&abs_x; + if ((abs_x_u32 & 0x7fffffff) == 0x7fffffff) { + kstrcpy(buff_start, "NaN"); + return 3; // the length of NaN + } + + // find fraction and integer parts of absolute value. + float integer_part; + float fraction_part = std::modf(abs_x, &integer_part); + + char* start_ptr = buff_start + 1; // the null terminator is at buff_start[0]. + char* end_ptr = buff_end - 1; // the last char we can write to. + + // loop over integer digits (increasing significance) + while (start_ptr <= end_ptr && (integer_part != 0.0f)) { + // the fractional part will be the lowest place integer divided by 10 + float next_int = std::modf(integer_part / 10.f, &integer_part); + + // the float to round to get the integer + float rounder = next_int * 10.f + 0.5f; + + // but wait, the PS2 is stupid and maybe has weird floats that might not convert well. + // this checks for very large exponent or negative sign bit, which would only happen if the + // float is all messed up. + u32 ru32 = *(u32*)&rounder; + s32 value; + if (((ru32 >> 0x17) & 0xff) < 0x9e) { // exponent + value = (char)rounder; + } else if (!(ru32 >> 31)) { // sign bit + value = 0; + throw std::runtime_error("got very large exponent in rounding calculation"); + } else { + value = -1; // happens on NaN's + // throw std::runtime_error("got negative sign bit in rounding calculation"); + } + + // place number at the end of the buffer and move pointer back + *end_ptr = '0' + value; + end_ptr--; + + // track how many integers we've written + forward_count++; + } + + char* count_chrp = start_ptr; // one after the buffer start (null) + if (forward_count == 0) { // no integers + *start_ptr = '0'; // put leading zero + count_chrp = buff_start + 2; // and set count to two after buffer start + } else { + while (end_ptr = end_ptr + 1, + end_ptr < buff_end) { // copy back to the beginning (will be in right order) + *count_chrp = *end_ptr; + count_chrp++; + } + } + + // if we have digits after decimal, place decimal. + if (precision) { + *count_chrp = '.'; + count_chrp++; + } + + s32 prec = precision; + + // if we have a fractional part, we may need to either print it, or do rounding. + if (fraction_part != 0.0f) { + prec = precision; + if (precision) { + // same loop as before, but only over the number of digits we actually want to print. + do { + float next_int; + fraction_part = std::modf(fraction_part * 10.f, &next_int); + u32 ru32 = *(u32*)&next_int; + s32 value; + if (((ru32 >> 0x17) & 0xff) < 0x9e) { + value = (char)next_int; + } else if (!(ru32 >> 0x1f)) { + value = 0; + throw std::runtime_error("got very large exponent in rounding calculation"); + } else { + value = -1; // happens on NaN's + // throw std::runtime_error("got negative sign bit in rounding calculation"); + } + *count_chrp = value + '0'; + count_chrp++; + prec--; + } while ((prec) && (fraction_part != 0.f)); + } + + // if the rounding flag is enabled, we would round here. + // however, the rounding flag is always disabled and the rounding code doesn't work. + if ((fraction_part != 0.f) && ((flags & 1) != 0)) { + start_ptr = round(fraction_part, nullptr, start_ptr, count_chrp - 1, 0, lead_char); + throw std::runtime_error("cvt_float called round!"); + } + } + + // if there are any left over digits (fraction part = 0 before we got to the end), append zeros. + while (prec = prec - 1, prec != -1) { + *count_chrp = '0'; + count_chrp++; + } + // return length. not including the null character at the beginning. + return count_chrp - start_ptr; +} + +/*! + * Convert floating point to a string. + * Don't set the precision too high or you get NaN (if float is longer than 31 digits without + * negative sign) Don't set the flags. Rounding is a little bit off, but it adds character. Highly + * recommended to pad with spaces, not zeros. If you pad with spaces you get " -1.23" If you pad + * with zeros, you get "0000000-1.23" which most people consider to be wrong. + * @param out_str : output buffer to write null terminated string into + * @param x : number + * @param desired_len : length (will pad if under, nothing if over) + * @param pad_char : character to pad with + * @param precision + * @param flags + * + * DONE + * + */ +void ftoa(char* out_str, float x, s32 desired_len, char pad_char, s32 precision, u32 flags) { + char buff[0x100]; + char* current_buff = buff; + s32 lead_char; + + // do conversion, but only write into the first half of the buffer (128 chars) + s32 count = cvt_float(x, precision, &lead_char, current_buff, buff + 0x7f, flags); + + // if it ends up larger than 31 characters, it's probably gone horribly wrong and we should just + // put NaN instead. Or maybe somebody requested a lot of precision. That would be stupid and they + // deserve to see NaN. + if (count > 0x3f) { + kstrcpy(current_buff, "NaN"); + count = 3; + lead_char = 0; + } + + // always true because we don't round, + if (buff[0] == 0) { + current_buff = buff + 1; + } + + // length, including the leading negative (if we need it). + s32 real_count = (lead_char != 0) + count; + + char* out_ptr = out_str; + + // pad + if ((desired_len > 0) && (desired_len > real_count)) { + for (s32 i = 0; i < (desired_len - real_count); i++) { + *out_ptr = pad_char; + out_ptr++; + } + } + + // leading + if (lead_char) { + *out_ptr = lead_char; + out_ptr++; + } + + // copy numbers + for (s32 i = 0; i < count; i++) { + *out_ptr = *current_buff; + out_ptr++; + current_buff++; + } + + // null terminate! + *out_ptr = 0; +} + +/*! + * Convert integer to string. + * @param buffer : buffer to print into. Must be at least as long as the longest possible number to + * print + * @param value : value to print. + * @param base : base to print in (2, 10, 16 supported) + * @param length : length. if shorter than length, pad with pad. If longer, only truncate leading + * f's 1's in base 16 or 2 + * @param pad : character to pad with + * @param flags : flag. Only the 2nd bit is used, which will disable negative sings on + * binary/hexadecimal truncated numbers. Something like -1 (0xffffffff) will print as -fffffff.... + */ + +char* kitoa(char* buffer, s64 value, u64 base, s32 length, char pad, u32 flag) { + s64 negativeValue = 0; + s64 value_to_print = value; + + // if negative and base ten, we print the opposite of the value and add a negative sign + if ((value < 0) && base == 10) { + negativeValue = value; + value_to_print = -value; + } + + // write number in reverse + int count = 0; + do { + buffer[count++] = ConvertTable[(u64)value_to_print % (u64)base]; + value_to_print = (u64)value_to_print / (u64)base; + } while (value_to_print); + + // append negative if we need to + if (negativeValue < 0) { + buffer[count++] = '-'; + } + + // pad (probably some sort of for loop) + s32 rLen = length; + if (0 < length - count) { + rLen = length - count; + while (0 < rLen) { + buffer[count++] = pad; + rLen--; + } + } + + // truncate f's / 1's + if (rLen > 0 && value < 0 && (base == 2 || base == 16) && rLen < count) { + char c = (base == 16) ? 'f' : '1'; + + while (rLen < count && (buffer[count - 1] == c)) { + count--; + } + + if (!(flag & 2)) { + buffer[count++] = '-'; + } + } + + // null terminate, reverse, return! + buffer[count] = 0; + reverse(buffer); + return buffer; +} + +/*! + * Convert 128-bit integer to string. Not implemented because it is never used in the game. + * It would also require passing 128-bit values between GOAL and C++ and this is not worth + * implementing. It is only used by the "format" function, which cannot use it properly. "format" + * uses C varags, but 128-bit varags don't work, so "format" always passes 0 for quadword printing. + */ +void kqtoa() { + throw std::runtime_error("kqtoa not implemented"); +} + +struct format_struct { + char data[0x40]; + void reset() { + for (auto& c : data) + c = -1; + } +}; + +/*! + * The GOAL "format" function. The actual function is named "format". However, GOAL's calling + * convention differs from x86-64, so GOAL cannot directly call format. There is an assembly + * function in format_wrapper.nasm named format. It takes the GOAL argument registers, stores them + * in an array on the stack, and calls this function with a pointer to that array. + * + * This function is a disaster. The Ghidra analyzer completely fails on it, so this is done by hand. + * + * To make this work correctly from GOAL with up to 8 arguments, there is an assembly function + * defined in format_wrapper.nasm that places the GOAL arguments on the stack and calls this + * format_impl function with a single argument that is a pointer to the argument array. + */ +s32 format_impl(uint64_t* args) { + // first two args are dest, format string + uint64_t* arg_regs = args + 2; + + // data for arguments in a format command + format_struct argument_data[8]; + + u32 arg_reg_idx = 0; + + // the gstring + char* format_gstring = Ptr(args[1]).c(); + + u32 original_dest = args[0]; + + // set up print pending + char* print_temp = PrintPending.cast().c(); + if (!PrintPending.offset) { + print_temp = PrintBufArea.cast().c() + sizeof(GoalMessageHeader); + } + PrintPending = make_ptr(strend(print_temp)).cast(); + + // what we write to + char* output_ptr = PrintPending.cast().c(); + + // convert gstring to cstring + char* format_cstring = format_gstring + 4; + + // mysteries + char* PrintPendingLocal2 = PrintPending.cast().c(); + char* PrintPendingLocal3 = output_ptr; + + // start by computing indentation + u32 indentation = 0; + + // read goal binteger + if (print_column.offset) { + // added the if check so we can format even if the kernel didn't load right. + indentation = (*print_column) >> 3; + } + + // which arg we're on + u32 arg_idx = 0; + + // if last char was newline and we have tabs, do tabs + if (indentation && output_ptr[-1] == '\n') { + for (u32 i = 0; i < indentation; i++) { + *output_ptr = ' '; + output_ptr++; + } + } + + // input pointer + char* format_ptr = format_cstring; + + // loop over the format string + while (*format_ptr) { + // got a command? + if (*format_ptr == '~') { + char* arg_start = format_ptr; + // get some arguments + arg_idx = 0; + u8 justify = 0; + for (auto& x : argument_data) { + x.reset(); + } + + // read arguments + while ((u8)(format_ptr[1] - '0') < 10 || // number 0 to 10 + format_ptr[1] == ',' || // comma + format_ptr[1] == '\'' || // quote + format_ptr[1] == '`' || // backtick + (argument_data[arg_idx].data[0] == -1 && + (format_ptr[1] == '-' || format_ptr[1] == '+') // flags1 == -1 && +/- + )) { + // here format_ptr[1] points to next unread character in argument + // format_ptr[0] is originally the ~ + // should exit loop with format_ptr[1] == the command character + char arg_char = format_ptr[1]; // gVar1 + + if (arg_char == ',') { + // advance to next argument + arg_idx++; // increment which argument we're on + format_ptr++; // increment past comma, and try again + continue; + } + + // character argument + if (arg_char == '\'') { // 0x27 + argument_data[arg_idx].data[0] = format_ptr[2]; + format_ptr += 2; + continue; + } + + // string argument + if (arg_char == '`') { // 0x60 + u32 i = 0; + format_ptr += 2; + // read string + while (*format_ptr != '`') { + argument_data[arg_idx].data[i] = *format_ptr; + i++; + format_ptr++; + } + // null terminate + argument_data[arg_idx].data[i] = 0; + continue; + } + + if (arg_char == '-') { // 0x2d + // negative flag + argument_data[arg_idx].data[1] = 1; + format_ptr++; + continue; + } + + if (arg_char == '+') { // 0x2b + // positive flag does nothing + format_ptr++; + continue; + } + + // otherwise: + + // null terminate if we got no args + if (argument_data[arg_idx].data[0] == -1) { + argument_data[arg_idx].data[0] = 0; + } + + // otherwise it's a number + argument_data[arg_idx].data[0] = argument_data[arg_idx].data[0] * 10 + arg_char - '0'; + format_ptr++; + } // end argument while + + // switch on command + switch (format_ptr[1]) { + // offset of 0x25 + + case '%': // newline + *output_ptr = '\n'; + output_ptr++; + // indent the next line if there is one + if (indentation && format_ptr[2]) { + for (u32 i = 0; i < indentation; i++) { + *output_ptr = ' '; + output_ptr++; + } + } + break; + + case '~': // tilde escape + *output_ptr = '~'; + output_ptr++; + break; + + // pass through arguments + case 'H': // 23 -> 48, H + case 'J': // 25 -> 4A, J + case 'K': // 26 -> 4B, K + case 'L': // 27 -> 4C, L + case 'N': // 29 -> 4E, N + case 'V': // 31 -> 56, V + case 'W': // 32 -> 57, W + case 'Y': // 34 -> 59, Y + case 'Z': // 35 -> 5A, Z + case 'h': + case 'j': + case 'k': + case 'l': + case 'n': + case 'v': + case 'w': + case 'y': + case 'z': + while (arg_start < format_ptr + 1) { + *output_ptr = *arg_start; + arg_start++; + output_ptr++; + } + *output_ptr = format_ptr[1]; + output_ptr++; + break; + + case 'G': // like %s, prints a C string + case 'g': { + *output_ptr = 0; + u32 in = arg_regs[arg_reg_idx++]; + kstrcat(output_ptr, Ptr(in).c()); + output_ptr = strend(output_ptr); + } break; + + case 'A': // print a boxed object + case 'a': // pad,padchar (like ) ~8,'0A + { + s8 arg0 = argument_data[0].data[0]; + s32 desired_length = arg0; + *output_ptr = 0; + u32 in = arg_regs[arg_reg_idx++]; + print_object(in); + if (desired_length != -1) { + s32 print_len = strlen(output_ptr); + if (desired_length < print_len) { + // too long! + if (desired_length > 1) { // mark with tilde that we will truncate + output_ptr[desired_length - 1] = '~'; + } + output_ptr[desired_length] = 0; // and truncate + } else if (print_len < desired_length) { + // too short + if (justify == 0) { + char pad = ' '; + if (argument_data[1].data[0] != -1) { + pad = argument_data[1].data[0]; + } + kstrinsert(output_ptr, pad, desired_length - print_len); + } else { + throw std::runtime_error("unsupported justify in format"); + // output_ptr = strend(output_ptr); + // while(0 < (desired_length - print_len)) { + // char pad = ' '; + // if(argument_data[0].data[1] != -1) { + // pad = argument_data[0].data[1]; + // } + // output_ptr[0] = pad; + // output_ptr++; + // + // } + // *output_ptr = 0; + } + } + } + output_ptr = strend(output_ptr); + + } break; + + case 'S': // like A, but strings are printed without quotes + case 's': { + s8 arg0 = argument_data[0].data[0]; + s32 desired_length = arg0; + *output_ptr = 0; + u32 in = arg_regs[arg_reg_idx++]; + + // if it's a string + if (((in & 0x7) == 0x4) && *Ptr(in - 4) == *(s7 + FIX_SYM_STRING_TYPE)) { + cprintf("%s", Ptr(in).c() + 4); + } else { + print_object(in); + } + + if (desired_length != -1) { + s32 print_len = strlen(output_ptr); + if (desired_length < print_len) { + // too long! + if (desired_length > 1) { // mark with tilde that we will truncate + output_ptr[desired_length - 1] = '~'; + } + output_ptr[desired_length] = 0; // and truncate + } else if (print_len < desired_length) { + // too short + if (justify == 0) { + char pad = ' '; + if (argument_data[1].data[0] != -1) { + pad = argument_data[1].data[0]; + } + kstrinsert(output_ptr, pad, desired_length - print_len); + + } else { + throw std::runtime_error("unsupported justify in format"); + // output_ptr = strend(output_ptr); + // u32 l140 = 0; + // while(l140 < (desired_length - print_len)) { + // char* l108 = output_ptr; + // + // char pad = ' '; + // if(argument_data[0].data[1] != -1) { + // pad = argument_data[0].data[1]; + // } + // output_ptr[0] = pad; + // output_ptr++; + // } + // *output_ptr = 0; + } + } + } + output_ptr = strend(output_ptr); + } break; + + case 'C': // character + case 'c': + *output_ptr = arg_regs[arg_reg_idx++]; + output_ptr++; + break; + + case 'P': // like ~A, but can specify type explicitly + case 'p': { + *output_ptr = 0; + s8 arg0 = argument_data[0].data[0]; + u32 in = arg_regs[arg_reg_idx++]; + if (arg0 == -1) { + print_object(in); + } else { + auto sym = find_symbol_from_c(argument_data[0].data); + if (sym.offset) { + Ptr type = *sym.cast>(); + if (type.offset) { + call_method_of_type(in, type, GOAL_PRINT_FUNC); + } + } else { + throw std::runtime_error("failed to find symbol in format!"); + } + } + output_ptr = strend(output_ptr); + } break; + + case 'I': // like ~P, but calls inpsect + case 'i': { + *output_ptr = 0; + s8 arg0 = argument_data[0].data[0]; + u32 in = arg_regs[arg_reg_idx++]; + if (arg0 == -1) { + inspect_object(in); + } else { + auto sym = find_symbol_from_c(argument_data[0].data); + if (sym.offset) { + Ptr type = *sym.cast>(); + if (type.offset) { + call_method_of_type(in, type, GOAL_INSPECT_FUNC); + } + } else { + throw std::runtime_error("failed to find symbol in format!"); + } + } + output_ptr = strend(output_ptr); + } break; + + case 'Q': // not yet implemented. hopefully andy gavin finishes this one soon. + case 'q': + throw std::runtime_error("nyi q format string"); + break; + + case 'X': // hex, 64 bit, pad padchar + case 'x': { + char pad = '0'; + if (argument_data[1].data[0] != -1) { + pad = argument_data[1].data[0]; + } + u64 in = arg_regs[arg_reg_idx++]; + kitoa(output_ptr, in, 16, argument_data[0].data[0], pad, 0); + output_ptr = strend(output_ptr); + } break; + + case 'D': // integer 64, pad padchar + case 'd': { + char pad = ' '; + if (argument_data[1].data[0] != -1) { + pad = argument_data[1].data[0]; + } + u64 in = arg_regs[arg_reg_idx++]; + kitoa(output_ptr, in, 10, argument_data[0].data[0], pad, 0); + output_ptr = strend(output_ptr); + } break; + + case 'B': // integer 64, pad padchar + case 'b': { + char pad = '0'; + if (argument_data[1].data[0] != -1) { + pad = argument_data[1].data[0]; + } + u64 in = arg_regs[arg_reg_idx++]; + kitoa(output_ptr, in, 2, argument_data[0].data[0], pad, 0); + output_ptr = strend(output_ptr); + } break; + + case 'F': // float 12 pad, 4 precision + { + float in = *(float*)&arg_regs[arg_reg_idx++]; + ftoa(output_ptr, in, 0xc, ' ', 4, 0); + output_ptr = strend(output_ptr); + } break; + + case 'f': // float with args + { + float in = *(float*)&arg_regs[arg_reg_idx++]; + s8 pad_length = argument_data[0].data[0]; + s8 pad_char = argument_data[1].data[0]; + if (pad_char == -1) + pad_char = ' '; + s8 precision = argument_data[2].data[0]; + if (precision == -1) + precision = 4; + ftoa(output_ptr, in, pad_length, pad_char, precision, 0); + output_ptr = strend(output_ptr); + } break; + + case 'R': // rotation degrees + case 'r': { + float in = *(float*)&arg_regs[arg_reg_idx++]; + s8 pad_length = argument_data[0].data[0]; + s8 pad_char = argument_data[1].data[0]; + if (pad_char == -1) + pad_char = ' '; + s8 precision = argument_data[2].data[0]; + if (precision == -1) + precision = 4; + ftoa(output_ptr, in * 360.f / 65536.f, pad_length, pad_char, precision, 0); + output_ptr = strend(output_ptr); + } break; + + case 'M': // distance meters + case 'm': { + float in = *(float*)&arg_regs[arg_reg_idx++]; + s8 pad_length = argument_data[0].data[0]; + s8 pad_char = argument_data[1].data[0]; + if (pad_char == -1) + pad_char = ' '; + s8 precision = argument_data[2].data[0]; + if (precision == -1) + precision = 4; + ftoa(output_ptr, in / 4096.f, pad_length, pad_char, precision, 0); + output_ptr = strend(output_ptr); + } break; + + case 'E': // time seconds + case 'e': { + s64 in = arg_regs[arg_reg_idx++]; + s8 pad_length = argument_data[0].data[0]; + s8 pad_char = argument_data[0].data[1]; + if (pad_char == -1) + pad_char = ' '; + s8 precision = argument_data[0].data[2]; + if (precision == -1) + precision = 4; + float value; + if (in < 0) { + throw std::runtime_error("time seconds format error negative.\n"); + } else { + value = in; + } + ftoa(output_ptr, value / 300.f, pad_length, pad_char, precision, 0); + output_ptr = strend(output_ptr); + } break; + + case 'T': + case 't': { + sprintf(output_ptr, "\t"); + output_ptr = strend(output_ptr); + } break; + + default: + MsgErr("format: unknown code 0x%02x\n", format_ptr[1]); + throw std::runtime_error("format error"); + break; + } + format_ptr++; + } else { + // got normal char, just copy it + *output_ptr = *format_ptr; + output_ptr++; + } + format_ptr++; + } // end format string while + + // end + *output_ptr = 0; + output_ptr++; + + if (original_dest == s7.offset + FIX_SYM_TRUE) { + // do nothing, we're done + return 0; + } else if (original_dest == s7.offset + FIX_SYM_FALSE) { + // #f means print to new string + u32 string = make_string_from_c(PrintPendingLocal3); + PrintPending = make_ptr(PrintPendingLocal2).cast(); + *PrintPendingLocal3 = 0; + return string; + } else if (original_dest == 0) { + printf("%s", PrintPendingLocal3); + fflush(stdout); + PrintPending = make_ptr(PrintPendingLocal2).cast(); + *PrintPendingLocal3 = 0; + return 0; + } else { + if ((original_dest & OFFSET_MASK) == BASIC_OFFSET) { + Ptr type = *Ptr>(original_dest - 4); + if (type == *Ptr>(s7.offset + FIX_SYM_STRING_TYPE)) { + u32 len = *Ptr(original_dest); + char* str = Ptr(original_dest + 4).c(); + kstrncat(str, PrintPendingLocal3, len); + PrintPending = make_ptr(PrintPendingLocal2).cast(); + *PrintPendingLocal3 = 0; + return 0; + } else if (type == *Ptr>(s7.offset + FIX_SYM_FILE_STREAM_TYPE)) { + throw std::runtime_error("FORMAT into a file stream not supported"); + } + } + throw std::runtime_error("unknown format destination"); + return 0; + } + + throw std::runtime_error("how did we get here?"); + return 7; +} \ No newline at end of file diff --git a/game/kernel/kprint.h b/game/kernel/kprint.h new file mode 100644 index 0000000000..d26fea7bc3 --- /dev/null +++ b/game/kernel/kprint.h @@ -0,0 +1,119 @@ +/*! + * @file kprint.h + * GOAL Print. Contains GOAL I/O, Print, Format... + */ + +#ifndef RUNTIME_KPRINT_H +#define RUNTIME_KPRINT_H + +#include "kmachine.h" + +constexpr u32 DEBUG_MESSAGE_BUFFER_SIZE = 0x80000; +constexpr u32 DEBUG_OUTPUT_BUFFER_SIZE = 0x80000; +constexpr u32 DEBUG_PRINT_BUFFER_SIZE = 0x200000; +constexpr u32 PRINT_BUFFER_SIZE = 0x2000; + +/////////// +// SDATA +/////////// +extern Ptr OutputPending; +extern Ptr PrintPending; +extern s32 MessCount; + +extern char AckBufArea[40]; +extern Ptr MessBufArea; +extern Ptr OutputBufArea; +extern Ptr PrintBufArea; + +/*! + * Initialize global variables for kprint + */ +void kprint_init_globals(); + +/*! + * Initialize GOAL Kernel printing/messaging system. + * Allocates buffers. + */ +void init_output(); + +/*! + * Empty output buffer (only if MasterDebug) + */ +void clear_output(); + +/*! + * Clear all data in the print buffer + */ +void clear_print(); + +/*! + * Buffer message to compiler indicating the target has reset. + * Write to the beginning of the output buffer. + */ +void reset_output(); + +/*! + * Buffer message to compiler indicating some object file has been unloaded. + */ +void output_unload(const char* name); + +/*! + * Buffer message to compiler indicating some object file has been loaded. + */ +void output_segment_load(const char* name, Ptr link_block, u32 flags); + +/*! + * Print to the GOAL print buffer from C + */ +void cprintf(const char* format, ...) __attribute__((format(printf, 1, 2))); + +/*! + * Print directly to the C stdout + * The "k" parameter is ignored, so this is just like printf + */ +void Msg(s32 k, const char* format, ...) __attribute__((format(printf, 2, 3))); + +/*! + * Print directly to the C stdout + * This is identical to Msg. + */ +void MsgWarn(const char* format, ...) __attribute__((format(printf, 1, 2))); + +/*! + * Print directly to the C stdout + * This is identical to Msg. + */ +void MsgErr(const char* format, ...) __attribute__((format(printf, 1, 2))); + +/*! + * Reverse string in place. + */ +void reverse(char* s); + +/*! + * Helper function for floating point to string conversion. + */ +s32 cvt_float(float x, s32 precision, s32* lead_char, char* buff_start, char* buff_end, u32 flags); + +/*! + * Convert floating point to a string. + */ +void ftoa(char* out_str, float x, s32 desired_len, char pad_char, s32 precision, u32 flags); + +/*! + * Convert integer to a string. + */ +char* kitoa(char* buffer, s64 value, u64 base, s32 length, char pad, u32 flag); + +/*! + * Convert 128-bit integer to string. Not implemented because it is never used in the game. + * The format function does have the ability to call it, but it always passes a zero because + * getting a 128-bit integer in PS2 gcc's varargs doesn't work. + */ +void kqtoa(); + +extern "C" { +s32 format_impl(uint64_t* args); +} + +#endif // RUNTIME_KPRINT_H diff --git a/game/kernel/kscheme.cpp b/game/kernel/kscheme.cpp new file mode 100644 index 0000000000..eff441ee01 --- /dev/null +++ b/game/kernel/kscheme.cpp @@ -0,0 +1,1886 @@ +/*! + * @file kscheme.cpp + * Implementation of GOAL runtime. + */ + +#include +#include +#include "kscheme.h" +#include "common/common_types.h" +#include "kmachine.h" +#include "klisten.h" +#include "kmalloc.h" +#include "kprint.h" +#include "fileio.h" +#include "kboot.h" +#include "kdsnetm.h" +#include "kdgo.h" +#include "klink.h" +#include "common/symbols.h" +#include "common/versions.h" + +//! Controls link mode when EnableMethodSet = 0, MasterDebug = 1, DiskBoot = 0. Will enable a +//! warning message if EnableMethodSet = 1 +u32 FastLink; + +// where to put a new symbol for the most recently searched for symbol that wasn't found +u32 symbol_slot; + +// pointer to the "second" symbol table +Ptr SymbolTable2; + +// pointer to the last symbol +Ptr LastSymbol; + +// total number of symbols in the table +s32 NumSymbols; + +// set to true to enable propagating method overrides to child types +// this is an O(N_max_symbols) operation, so it is avoided when loading DGOs for levels. +// but is enabled when loading the engine. +Ptr EnableMethodSet; + +// used for crc32 calculation +u32 crc_table[0x100]; + +// value of the GOAL s7 register, pointing to the middle of the symbol table +Ptr s7; + +void kscheme_init_globals() { + for (auto& x : crc_table) { + x = 0; + } + NumSymbols = 0; + s7.offset = 0; + SymbolTable2.offset = 0; + LastSymbol.offset = 0; + EnableMethodSet.offset = 0; + FastLink = 0; +} + +/*! + * Initialize CRC Table. + */ +void init_crc() { + for (u32 i = 0; i < 0x100; i++) { + u32 n = i << 24; + for (u32 j = 0; j < 8; j++) { + n = n & 0x80000000 ? (n << 1) ^ CRC_POLY : (n << 1); + } + crc_table[i] = n; + } +} + +/*! + * Take the CRC32 hash of some data + */ +u32 crc32(const u8* data, s32 size) { + uint32_t crc = 0; + for (int i = size; i != 0; i--, data++) { + crc = crc_table[crc >> 24] ^ ((crc << 8) | *data); + } + + if ((~crc) == 0) { + // if this happens, I think the hash table implementation breaks. + assert(false); + } + return ~crc; +} + +/*! + * New method for types which cannot have "new" used on them. + * Prints an error to stdout and returns false. + */ +u64 new_illegal(u32 allocation, u32 type) { + (void)allocation; + MsgErr("dkernel: illegal attempt to call new method of static object type %s\n", + info(Ptr(type)->symbol)->str->data()); + return s7.offset; +} + +/*! + * Delete method for types which cannot have "delete" used on them. + * Prints an error to stdout and returns false. + */ +u64 delete_illegal(u32 obj) { + MsgErr("dkernel: illegal attempt to call delete of static object @ #x%x\n", obj); + return s7.offset; // todo, maybe don't return anything? +} + +/*! + * Wrapper around kmalloc to allow GOAL programs to allocate on kernel heaps. + */ +u64 goal_malloc(u32 heap, u32 size, u32 flags, u32 name) { + return kmalloc(Ptr(heap), size, flags, Ptr(name)->data()).offset; +} + +/*! + * Allocate memory from the specified heap. If symbol is 'process, does a process allocation. + * If symbol is 'scratch, does a scratch allocation (this is not used). + * + * If it's not a heap, treat it as a stack allocation and just memset it to zero. + * Type is only used to print a debug message if the allocation fails, so it can be null or not + * completely defined. + */ +u64 alloc_from_heap(u32 heapSymbol, u32 type, s32 size) { + if (size <= 0) { + throw std::runtime_error("got <= 0 size allocation in alloc_from_heap!"); + } + + // align to 16 bytes (part one) + s32 alignedSize = size + 0xf; + + // huh? + if (alignedSize < 0) + alignedSize += 0xf; + + // finish aligning. + alignedSize = (alignedSize >> 4) << 4; + + u32 heapOffset = heapSymbol - s7.offset; + + if (heapOffset == FIX_SYM_GLOBAL_HEAP || heapOffset == FIX_SYM_DEBUG_HEAP || + heapOffset == FIX_SYM_PROCESS_LEVEL_HEAP || heapOffset == FIX_SYM_LOADING_LEVEL) { + // it's a kheap, so just kmalloc. + + if (!type) { // no type given, just call it a global-object + return kmalloc(*Ptr>(heapSymbol), size, KMALLOC_MEMSET, "global-object") + .offset; + } + + Ptr typ(type); + if (!typ->symbol.offset) { // type doesn't have a symbol, just call it a global-object + return kmalloc(*Ptr>(heapSymbol), size, KMALLOC_MEMSET, "global-object") + .offset; + } + + Ptr gstr = info(typ->symbol)->str; + if (!gstr->len) { // string has nothing in it. + return kmalloc(*Ptr>(heapSymbol), size, KMALLOC_MEMSET, "global-object") + .offset; + } + + return kmalloc(*Ptr>(heapSymbol), size, KMALLOC_MEMSET, gstr->data()).offset; + } else if (heapOffset == FIX_SYM_PROCESS_TYPE) { + throw std::runtime_error("this type of process allocation is not supported yet!\n"); + // allocate on current process heap + // Ptr start = *ptr(getS6() + 0x4c + 8); + // Ptr heapEnd = *ptr(getS6() + 0x4c + 4); + // Ptr allocEnd = start + alignedSize; + // + // // there's room, bump allocate + // if(allocEnd < heapEnd) { + // *ptr(getS6() + 0x4c + 8) = allocEnd; + // memset(vptr(start), 0, (size_t)alignedSize); + // return start; + // } else { + // MsgErr("kmalloc: !alloc mem in heap for # (%d bytes)\n", getS6(), + // alignedSize); return 0; + // } + } else if (heapOffset == FIX_SYM_SCRATCH) { + throw std::runtime_error("this type of scratchpad allocation is not used!\n"); + } else { + memset(Ptr(heapSymbol).c(), 0, (size_t)alignedSize); // treat it as a stack address + return heapSymbol; + } +} + +/*! + * Allocate untyped memory. + */ +u64 alloc_heap_memory(u32 heap, u32 size) { + return alloc_from_heap(heap, 0, size); +} + +/*! + * Allocate memory and add type tag for an object. + * For allocating basics. + */ +u64 alloc_heap_object(u32 heap, u32 type, u32 size) { + auto mem = alloc_from_heap(heap, type, size); + if (!mem) { + return 0; + } + + *Ptr(mem) = type; + return mem + BASIC_OFFSET; +} + +/*! + * Allocate a structure and get the structure size from the type. + */ +u64 new_structure(u32 heap, u32 type) { + return alloc_from_heap(heap, type, Ptr(type)->allocated_size); +} + +/*! + * Allocate a structure with a dynamic size + */ +u64 new_dynamic_structure(u32 heap, u32 type, u32 size) { + return alloc_from_heap(heap, type, size); +} + +/*! + * Delete a structure. Not supported, as it uses kfree, which doesn't do anything. + */ +void delete_structure(u32 s) { + kfree(Ptr(s)); +} + +/*! + * Allocate a basic of fixed size. + */ +u64 new_basic(u32 heap, u32 type) { + return alloc_heap_object(heap, type, Ptr(type)->allocated_size); +} + +/*! + * Delete a basic. Not supported, as it uses kfree. + */ +void delete_basic(u32 s) { + // note that the game has a bug here and has s as a uint* and does -4 which is actually a + // 16-byte offset. Luckily kfree does nothing so there's no harm done. But it's a good indication + // that the "freeing memory" feature never made it very far in development. This bug exists in + // Jak 3 as well. + kfree(Ptr(s - BASIC_OFFSET * 4)); // replicate the bug +} + +/*! + * Allocate a new pair and set its car and cdr. + */ +u64 new_pair(u32 heap, u32 type, u32 car, u32 cdr) { + auto mem = alloc_from_heap(heap, type, Ptr(type)->allocated_size); + if (!mem) { + return 0; + } + + u32* m = Ptr(mem).c(); + m[0] = car; + m[1] = cdr; + return mem + PAIR_OFFSET; +} + +/*! + * Delete a pair. BUG + */ +void delete_pair(u32 s) { + // the -8 should be a -2, but s is likely a u32* in the code. + kfree(Ptr(s - 8)); +} + +/*! + * Make an empty string of given size. + * Allocates from the global heap. + */ +u64 make_string(u32 size) { + auto mem_size = size + 1; // null + if (mem_size < 8) { + mem_size = 8; // min size of string + } + + // total size is mem_size (chars + null term), plus basic_offset (type tag) + 4 (string size) + auto mem = alloc_heap_object((s7 + FIX_SYM_GLOBAL_HEAP).offset, *(s7 + FIX_SYM_STRING_TYPE), + mem_size + BASIC_OFFSET + sizeof(uint32_t)); + + // set the string size field. + if (mem) { + *Ptr(mem) = size; + } + return mem; +} + +/*! + * Convert a C string to a GOAL string. + * Allocates from the global heap and copies the string data. + */ +u64 make_string_from_c(const char* c_str) { + auto str_size = strlen(c_str); + auto mem_size = str_size + 1; + if (mem_size < 8) { + mem_size = 8; + } + + auto mem = alloc_heap_object((s7 + FIX_SYM_GLOBAL_HEAP).offset, *(s7 + FIX_SYM_STRING_TYPE), + mem_size + BASIC_OFFSET + 4); + // there's no check for failed allocation here! + + // string size field + *Ptr(mem) = str_size; + + // rest is chars + kstrcpy(Ptr(mem + 4).c(), c_str); + return mem; +} + +/*! + * Create a GOAL function from a C function. This doesn't export it as a global function, it just + * creates a function object on the global heap. + * + * The implementation is to create a simple trampoline function which jumps to the C function. + */ +Ptr make_function_from_c(void* func) { + // allocate a function object on the global heap + auto mem = Ptr( + alloc_heap_object(s7.offset + FIX_SYM_GLOBAL_HEAP, *(s7 + FIX_SYM_FUNCTION_TYPE), 0x40)); + auto f = (uint64_t)func; + auto fp = (u8*)&f; + + // we will put the function address in RAX with a movabs rax, imm8 + mem.c()[0] = 0x48; + mem.c()[1] = 0xb8; + for (int i = 0; i < 8; i++) { + mem.c()[2 + i] = fp[i]; + } + + // jmp rax + mem.c()[10] = 0xff; + mem.c()[11] = 0xe0; + + // the C function's ret will return to the caller of this trampoline. + + // CacheFlush(mem, 0x34); + + return mem.cast(); +} + +/*! + * Create a GOAL function which does nothing and immediately returns. + */ +Ptr make_nothing_func() { + auto mem = Ptr( + alloc_heap_object(s7.offset + FIX_SYM_GLOBAL_HEAP, *(s7 + FIX_SYM_FUNCTION_TYPE), 0x14)); + + // a single x86-64 ret. + mem.c()[0] = 0xc3; + // CacheFlush(mem, 8); + return mem.cast(); +} + +/*! + * Create a GOAL function which returns 0. + */ +Ptr make_zero_func() { + auto mem = Ptr( + alloc_heap_object(s7.offset + FIX_SYM_GLOBAL_HEAP, *(s7 + FIX_SYM_FUNCTION_TYPE), 0x14)); + // xor eax, eax + mem.c()[0] = 0x31; + mem.c()[1] = 0xc0; + // ret + mem.c()[2] = 0xc3; + // CacheFlush(mem, 8); + return mem.cast(); +} + +/*! + * Given a C function and a name, create a GOAL function and store it in the symbol with the given + * name. This effectively creates a global GOAL function with the given name which calls the given C + * function. + */ +Ptr make_function_symbol_from_c(const char* name, void* f) { + auto sym = intern_from_c(name); + auto func = make_function_from_c(f); + sym->value = func.offset; + return func; +} + +/*! + * Set the named symbol to the value. This isn't specific to functions. + */ +u32 make_raw_function_symbol_from_c(const char* name, u32 value) { + intern_from_c(name)->value = value; + return value; +} + +/*! + * Configure a "fixed" symbol to have a given name and value. The "fixed" symbols are symbols + * which have their location in the symbol table determined ahead of time and not looked up by the + * hash function. + */ +Ptr set_fixed_symbol(u32 offset, const char* name, u32 value) { + // grab the symbol directly + Ptr sym = (s7 + offset).cast(); + + // grab the symbol type directly (it might not be set up yet, but that's ok) + Ptr typ = *Ptr>(s7.offset + FIX_SYM_SYMBOL_TYPE); + + // set type tag of the symbol. + sym.cast().c()[-1] = typ.offset; + + // set name of the symbol + info(sym)->str = Ptr(make_string_from_c(name)); + + // set hash of the symbol + info(sym)->hash = crc32((const u8*)name, (int)strlen(name)); + + // set value of the symbol + sym->value = value; + + NumSymbols++; + return sym; +} + +/*! + * Search the hash table's fixed area for a symbol. + * Returns null if we didn't find it. + */ +Ptr find_symbol_in_fixed_area(u32 hash, const char* name) { + for (u32 i = s7.offset; i < s7.offset + FIX_FIXED_SYM_END_OFFSET; i += 8) { + auto sym = Ptr(i); + if (info(sym)->hash == hash) { + if (!strcmp(info(sym)->str->data(), name)) { + return sym; + } + } + } + return Ptr(0); +} + +/*! + * Do a linear probe from start to end to find a symbol (or a slot for a new symbol). + * If we run into the end without finding anything, returns 1 to indicate the linear probe needs to + * wrap around. If we run into a blank space, mark that as the slot. Also search the fixed area for + * the symbol. If we fail to find it without wrapping, and it's not in the fixed area, return 0. + */ +Ptr find_symbol_in_area(u32 hash, const char* name, u32 start, u32 end) { + for (u32 i = start; i < end; i += 8) { + auto sym = Ptr(i); + + // note - this may break if any symbols hash to zero! + if (info(sym)->hash == hash) { + if (!strcmp(info(sym)->str->data(), name)) { + return sym; + } + } + + if (!info(sym)->hash) { + // open slot! + // means we don't need to wrap. + symbol_slot = i; + + // check the fixed area, in case it's not dynamically placed. + return find_symbol_in_fixed_area(hash, name); + } + } + + // we got to the end without finding the symbol or an empty slot. Return 1 to indicate this. + return Ptr(1); +} + +/*! + * Searches the table for a symbol. If the symbol is found, returns it. + * If not, returns 0, but symbol_slot will contain the slot for the symbol. + * If both are 0, the symbol table is full and you are sad. + * Also allows you to find the empty pair by searching for _empty_ + */ +Ptr find_symbol_from_c(const char* name) { + symbol_slot = 0; // nowhere to put the symbol yet, clear any old symbol_slot result. + u32 hash = crc32((const u8*)name, (int)strlen(name)); + + // check if we've got the empty pair. + if (hash == EMPTY_HASH) { + if (!strcmp(name, "_empty_")) { + return (s7 + FIX_SYM_EMPTY_PAIR).cast(); + } + } + + s32 sh1 = hash << 0x13; + s32 sh2 = sh1 >> 0x10; + // will be signed, bottom 3 bits 0 (for alignment, symbol are every 8 bytes) + // upper 16 bits are the same, so we will reach +/- 8 kb around 0. + + if (sh2 > 0) { + // upper table first. + auto probe = find_symbol_in_area(hash, name, s7.offset + sh2, LastSymbol.offset); + if (probe.offset != 1) { + return probe; + } + + // overflow! + probe = find_symbol_in_area(hash, name, SymbolTable2.offset, s7.offset - 0x10); + if (probe.offset == 1) { + // uh oh, both overflowed! + printf("[BIG WARNING] symbol table probe double overflow!\n"); + return find_symbol_in_fixed_area(hash, name); + } else { + return probe; + } + + } else { + // lower table first + auto probe = find_symbol_in_area(hash, name, s7.offset + sh2, s7.offset - 0x10); + if (probe.offset != 1) { + return probe; + } + + // overflow! + probe = + find_symbol_in_area(hash, name, s7.offset + FIX_FIXED_SYM_END_OFFSET, LastSymbol.offset); + if (probe.offset == 1) { + printf("[BIG WARNING] symbol table probe double overflow!\n"); + return find_symbol_in_fixed_area(hash, name); + } else { + return probe; + } + } +} + +/*! + * Returns a symbol with the given name. If this is the first time, make a new symbol, otherwise it + * returns the old one. Basically a LISP symbol intern + */ +Ptr intern_from_c(const char* name) { + auto symbol = find_symbol_from_c(name); + if (symbol.offset) { + // already exists, return it! + return symbol; + } + + // otherwise, a new symbol! + symbol = Ptr(symbol_slot); + // set type tag + symbol.cast().c()[-1] = *(s7 + FIX_SYM_SYMBOL_TYPE); + + u32 hash = crc32((const u8*)name, (int)strlen(name)); + auto str = make_string_from_c(name); + info(symbol)->str = Ptr(str); + info(symbol)->hash = hash; + + NumSymbols++; + return symbol; +} + +/*! + * GOAL intern function. + */ +u64 intern(u32 name) { + return intern_from_c(Ptr(name)->data()).offset; +} + +namespace { +u32 size_of_type(u32 method_count) { + return (4 * method_count + 0x23) & 0xfffffff0; +} +} // namespace + +/*! + * Given a symbol for the type name, allocate memory for a type and add it to the symbol table. + */ +Ptr alloc_and_init_type(Ptr sym, u32 method_count) { + // allocate from the global heap + u32 new_type = alloc_heap_object((s7 + FIX_SYM_GLOBAL_HEAP).offset, *(s7 + FIX_SYM_TYPE_TYPE), + size_of_type(method_count)); + + // add to symbol table. + sym->value = new_type; + return Ptr(new_type); +} + +/*! + * Like intern, but returns a type instead of a symbol. If the type doesn't exist, a new one is + * allocated. + */ +Ptr intern_type_from_c(const char* name, u64 methods) { + // there's a weird flag system used here. + // if methods is a number that's not 0 or 1, its used as the desired number of methods. + // If method is 0, and a new type needs to be created, it uses 12 methods + // If method is 1, and a new type needs to be created, it uses 44 methods + // If method is 0 or 1 and no new type needs to be created, there is no error. + // Requesting a type to have fewer methods than the existing type has is ok. + // Requesting a type to have more methods than the existing type is not ok and prints an error. + + auto symbol = intern_from_c(name); + u32 sym_value = symbol->value; + + if (!sym_value) { + // new type + int n_methods = methods; + + if (methods == 0) { + // some stupid types like user-defined children of integers have "0" as the method count + n_methods = DEFAULT_METHOD_COUNT; + } else if (methods == 1) { + // whatever builds the v2/v4 object files (level data) doesn't actually know method counts. + // so it just puts a 1. In this case, we should put lots of methods, just in case. + // I guess 44 was the number they picked. + n_methods = FALLBACK_UNKNOWN_METHOD_COUNT; + } + + // create the type. + auto type = alloc_and_init_type(symbol, n_methods); + type->symbol = symbol; + type->num_methods = n_methods; + return type; + } else { + // the type exists. + auto type = Ptr(sym_value); + // note - flags of 0 or 1 will pass through here without triggering the error. + if (size_of_type(type->num_methods) < size_of_type(methods)) { + MsgErr( + "dkernel: trying to redefine a type '%s' with %d methods when it had %d, try " + "restarting\n", + name, (u32)methods, type->num_methods); + assert(false); + } + return type; + } +} + +/*! + * Wrapper of intern_type_from_c to use with GOAL. It accepts a gstring as a name. + */ +u64 intern_type(u32 name, u64 methods) { + return intern_type_from_c(Ptr(name)->data(), methods).offset; +} + +/*! + * Setup a type which is located in a fixed spot of the symbol table. + */ +Ptr set_fixed_type(u32 offset, + const char* name, + Ptr parent_symbol, + u64 flags, + u32 print, + u32 inspect) { + Ptr type_symbol = (s7 + offset).cast(); + u32 symbol_value = type_symbol->value; + + // set type tag, name, hash of symbol + type_symbol.cast().c()[-1] = *(s7 + FIX_SYM_SYMBOL_TYPE); + info(type_symbol)->str = Ptr(make_string_from_c(name)); + info(type_symbol)->hash = crc32((const u8*)name, (int)strlen(name)); + + // increment + NumSymbols++; + + // construct type if needed + Ptr new_type; + if (!symbol_value) { + new_type = alloc_and_init_type(type_symbol, (u32)((flags >> 32) & 0xffff)); + } else { + new_type.offset = symbol_value; + } + + // set the type of the type + new_type.cast().c()[-1] = *(s7 + FIX_SYM_TYPE_TYPE); + + // set type fields + new_type->symbol = type_symbol; + Ptr parent_type(parent_symbol->value); + + set_type_values(new_type, parent_type, flags); + + // inherit methods + new_type->new_method = parent_type->new_method; + new_type->delete_method = parent_type->delete_method; + + if (!print) { + new_type->print_method = parent_type->print_method; + } else { + new_type->print_method.offset = print; + } + + if (!inspect) { + new_type->inspect_method = parent_type->inspect_method; + } else { + new_type->inspect_method.offset = inspect; + } + + new_type->length_method.offset = *(s7 + FIX_SYM_ZERO_FUNC); + new_type->asize_of_method = parent_type->asize_of_method; + new_type->copy_method = parent_type->copy_method; + + return new_type; +} + +/*! + * New method of type. A GOAL (deftype) will end up calling this method. + * Internally does an intern. + */ +u64 new_type(u32 symbol, u32 parent, u64 flags) { + // printf("flags 0x%lx\n", flags); + u32 n_methods = (flags >> 32) & 0xffff; + if (n_methods == 0) { + // 12 methods used as default, if the user has not provided us with a number + n_methods = DEFAULT_METHOD_COUNT; + } + + assert(n_methods < 127); // will cause issues. + + auto new_type = Ptr(intern_type(info(Ptr(symbol))->str.offset, n_methods)); + + Ptr* child_slots = &(new_type->new_method); + Ptr* parent_slots = &(Ptr(parent)->new_method); + + // if (Ptr(parent)->num_methods < n_methods) { + // printf("%s %d %d\n", info(Ptr(symbol))->str.c()->data(), + // Ptr(parent)->num_methods, + // n_methods); + // assert(false); + // } + + // BUG! This uses the child method count, but should probably use the parent method count. + for (u32 i = 0; i < n_methods; i++) { + child_slots[i] = parent_slots[i]; + } + + return set_type_values(new_type, Ptr(parent), flags).offset; +} + +/*! + * Configure a type. + */ +Ptr set_type_values(Ptr type, Ptr parent, u64 flags) { + type->parent = parent; + type->allocated_size = (flags & 0xffff); + type->heap_base = (flags >> 16) & 0xffff; + type->padded_size = ((type->allocated_size + 0xf) & 0xfff0); + + u16 new_methods = (flags >> 32) & 0xffff; + if (type->num_methods < new_methods) { + type->num_methods = new_methods; + } + + return type; +} + +/*! + * Is t1 a t2? + */ +u64 type_typep(Ptr t1, Ptr t2) { + if (t1 == t2) { + return (s7 + FIX_SYM_TRUE).offset; + } + + do { + t1 = t1->parent; + if (t1 == t2) { + return (s7 + FIX_SYM_TRUE).offset; + } + } while (t1.offset && t1.offset != *(s7 + FIX_SYM_OBJECT_TYPE)); + return s7.offset; +} + +/*! + * Set method of type. + * Looks at the EnableMethodSet symbol to determine if it should loop through all types looking for + * children and updating those. Only updates children who haven't overridden the method previously. + * + * Even if EnableMethodSet is disabled, it will still do this loop if FastLink is disabled, + * MasterDebug is enabled, or DiskBoot is false. This is likely for debugging reasons? + * + * If method is 0, does nothing. + * If method is 1, sets method of type to zero. + * If method is 2, sets method of type to parent's method + * GOAL args: + * arg0 : type + * arg1 : methodID + * arg2 : method + * Return is method + */ +u64 method_set(u32 type_, u32 method_id, u32 method) { + Ptr type(type_); + if (method_id > 127) + printf("[METHOD SET ERROR] tried to set method %d\n", method_id); + + // printf("METHOD SET id %d to 0x%x type 0x%x!\n", method_id, method, type_); + + auto existing_method = type->get_method(method_id).offset; + + if (method == 1) { + method = 0; + printf("[Method Set] got 1, setting null\n"); + } else if (method == 0) { + // no print, this happens a lot in non-debug mode. + return 0; + } else if (method == 2) { + method = type->parent->get_method(method_id).offset; + printf("[Method Set] got 2, inheriting\n"); + } + + // do the set + type->get_method(method_id).offset = method; + + // this is kind of a strange combination... + if (*EnableMethodSet || (!FastLink && MasterDebug && !DiskBoot)) { + // upper table + auto sym = s7.offset; + for (; sym < LastSymbol.offset; sym += 8) { + auto symValue = *Ptr(sym); + if ((symValue < SymbolTable2.offset || 0x7ffffff < symValue) && // not in normal memory + (symValue < 0x84000 || 0x100000 <= symValue)) { // not in kernel memory + continue; + } + + if ((symValue & OFFSET_MASK) != BASIC_OFFSET) { + continue; + } + + auto objType = *Ptr>(symValue - 4); + if (objType.offset != *(s7 + FIX_SYM_TYPE_TYPE)) { + continue; + } + + auto symAsType = Ptr(symValue); + if (method_id >= symAsType->num_methods) { + continue; + } + + if (symAsType->get_method(method_id).offset != existing_method) { + continue; + } + + if (type_typep(symAsType, type) == s7.offset) { + continue; + } + + if (FastLink) { + // you were saved by EnableMethodSet. I guess we warn. + printf("************ WARNING **************\n"); + printf("method %d of %s redefined - you must define class heirarchies in order now\n", + method_id, info(symAsType->symbol)->str->data()); + printf("***********************************\n"); + } + + symAsType->get_method(method_id).offset = method; + } + + sym = SymbolTable2.offset; + for (; sym < s7.offset; sym += 8) { + auto symValue = *Ptr(sym); + if ((symValue < SymbolTable2.offset || 0x7ffffff < symValue) && // not in normal memory + (symValue < 0x84000 || 0x100000 <= symValue)) { // not in kernel memory + continue; + } + + if ((symValue & OFFSET_MASK) != BASIC_OFFSET) { + continue; + } + + auto objType = *Ptr>(symValue - 4); + if (objType.offset != *(s7 + FIX_SYM_TYPE_TYPE)) { + continue; + } + + auto symAsType = Ptr(symValue); + if (method_id >= symAsType->num_methods) { + continue; + } + + if (symAsType->get_method(method_id).offset != existing_method) { + continue; + } + + if (type_typep(symAsType, type) == s7.offset) { + continue; + } + + if (FastLink) { + // you were saved by EnableMethodSet. I guess we warn. + printf("************ WARNING **************\n"); + printf("method %d of %s redefined - you must define class heirarchies in order now\n", + method_id, info(symAsType->symbol)->str->data()); + printf("***********************************\n"); + } + + symAsType->get_method(method_id).offset = method; + } + } + return method; +} + +extern "C" { +// defined in asm_funcs.nasm +uint64_t _call_goal_asm(u64 a0, u64 a1, u64 a2, void* fptr, void* st_ptr, void* offset); +} + +/*! + * Wrapper around _call_goal_asm for calling a GOAL function from C. + */ +u64 call_goal(Ptr f, u64 a, u64 b, u64 c, u64 st, void* offset) { + auto st_ptr = (void*)((uint8_t*)(offset) + st); + void* fptr = f.c(); + return _call_goal_asm(a, b, c, fptr, st_ptr, offset); +} + +/*! + * Call a GOAL method of a given type. + */ +u64 call_method_of_type(u32 arg, Ptr type, u32 method_id) { + if (((type.offset < SymbolTable2.offset || 0x7ffffff < type.offset) && // not in normal memory + (type.offset < 0x84000 || 0x100000 <= type.offset)) // not in kernel memory + || ((type.offset & OFFSET_MASK) != BASIC_OFFSET)) { // invalid type + cprintf("#<#%x has invalid type ptr #x%x>\n", arg, type.offset); + } else { + auto type_tag = Ptr>(type.offset - 4); + if ((*type_tag).offset == *(s7 + FIX_SYM_TYPE_TYPE)) { + auto f = type->get_method(method_id); + return call_goal(f, arg, 0, 0, s7.offset, g_ee_main_mem); + } else { + cprintf("#<#x%x has invalid type ptr #x%x, bad type #x%x>\n", arg, type.offset, + (*type_tag).offset); + } + } + // throw std::runtime_error("call_method_of_type failed!\n"); + printf("[ERROR] call_method_of_type failed!\n"); + printf("type is %s\n", info(type->symbol)->str->data()); + return arg; +} + +/*! + * Call a GOAL function with no arguments. + */ +u64 call_goal_function(Ptr func) { + return call_goal(func, 0, 0, 0, s7.offset, g_ee_main_mem); +} + +/*! + * Call a global GOAL function by name. + */ +u64 call_goal_function_by_name(const char* name) { + return call_goal_function(Ptr(*(intern_from_c(name)).cast())); +} + +/*! + * Like call_method_of_type, but has two arguments. Used to "relocate" v2/s4 loads. + */ +u64 call_method_of_type_arg2(u32 arg, Ptr type, u32 method_id, u32 a1, u32 a2) { + if (((type.offset < SymbolTable2.offset || 0x7ffffff < type.offset) && // not in normal memory + (type.offset < 0x84000 || 0x100000 <= type.offset)) // not in kernel memory + || ((type.offset & OFFSET_MASK) != BASIC_OFFSET)) { // invalid type + cprintf("#<#%x has invalid type ptr #x%x>\n", arg, type.offset); + } else { + auto type_tag = Ptr>(type.offset - 4); + if ((*type_tag).offset == *(s7 + FIX_SYM_TYPE_TYPE)) { + // return type->get_method(method_id).cast().c()(arg,a1,a2); + return call_goal(type->get_method(method_id), arg, a1, a2, s7.offset, g_ee_main_mem); + } else { + cprintf("#<#x%x has invalid type ptr #x%x, bad type #x%x>\n", arg, type.offset, + (*type_tag).offset); + } + } + throw std::runtime_error("call_method_of_type failed!\n"); + return arg; +} + +/*! + * Print an object with a newline after it to the GOAL PrintBuffer (not stdout) + */ +u64 sprint(u32 obj) { + auto rv = print_object(obj); + cprintf("\n"); + return rv; +} + +/*! + * Most generic printing method. + * Does not correctly handle 64 bit boxed integers or object64's correctly. + * It is important that no objects of type object actually exist or this will loop! + */ +u64 print_object(u32 obj) { + if ((obj & OFFSET_MASK) == BINTEGER_OFFSET) { + return print_binteger(obj); + } else { + if ((obj < SymbolTable2.offset || 0x7ffffff < obj) && // not in normal memory + (obj < 0x84000 || 0x100000 <= obj)) { // not in kernel memory + cprintf("#", obj); + } else if ((obj & OFFSET_MASK) == PAIR_OFFSET) { + return print_pair(obj); + } else if ((obj & OFFSET_MASK) == BASIC_OFFSET) { + return call_method_of_type(obj, Ptr(*Ptr(obj - 4)), GOAL_PRINT_FUNC); + } else { + cprintf("#", obj & OFFSET_MASK, obj); + } + } + return obj; +} + +/*! + * Default print method for structures. + * Structures have no runtime type info, so there's not much we can do here. + */ +u64 print_structure(u32 s) { + cprintf("#", s); + return s; +} + +/*! + * Default print method a basic. + * Confirms basic is valid and prints the type name. + */ +u64 print_basic(u32 obj) { + if (((obj < SymbolTable2.offset || 0x7ffffff < obj) && // not in normal memory + (obj < 0x84000 || 0x100000 <= obj)) // not in kernel memory + || ((obj & OFFSET_MASK) != BASIC_OFFSET)) { + cprintf("#", obj); + } else { + cprintf("#<%s @ #x%x>", info(Ptr(*Ptr(obj - 4))->symbol)->str->data(), obj); + } + return obj; +} + +/*! + * Print a pair as a LISP list. Don't try to print circular lists or it will get stuck + * Can print improper lists + */ +u64 print_pair(u32 obj) { + if (obj == s7.offset + FIX_SYM_EMPTY_PAIR) { + cprintf("()"); + } else { + cprintf("("); + auto toPrint = obj; + for (;;) { + if ((toPrint & OFFSET_MASK) == PAIR_OFFSET) { + // print CAR + print_object(*Ptr(toPrint - 2)); + + // load up CDR + auto cdr = *Ptr(toPrint + 2); + toPrint = cdr; + if (cdr == s7.offset + FIX_SYM_EMPTY_PAIR) { // end of proper list + cprintf(")"); + return obj; + } else { // continue list + cprintf(" "); + } + } else { // improper list + cprintf(". "); + print_object(toPrint); + cprintf(")"); + return obj; + } + } + } + return obj; +} + +/*! + * Print an integer. Works correctly for 64-bit integers. + */ +u64 print_integer(u64 obj) { + // not sure why this is any better than cprintf("%ld") or similar. Maybe a tiny bit faster? + char* str = PrintPending.cast().c(); + if (!str) { + str = (PrintBufArea + 0x18).cast().c(); + } + + PrintPending = make_ptr(strend(str)).cast(); + kitoa((char*)PrintPending.c(), obj, 10, 0xffffffff, '0', 0); + return obj; +} + +/*! + * Print a boxed integer. Works correctly for 64-bit integers. Assumes signed. + */ +u64 print_binteger(u64 obj) { + char* str = PrintPending.cast().c(); + if (!PrintPending.offset) { + str = (PrintBufArea + 0x18).cast().c(); + } + + PrintPending = make_ptr(strend(str)).cast(); + kitoa((char*)PrintPending.c(), ((s64)obj) >> 3, 10, 0xffffffff, '0', 0); + return obj; +} + +/*! + * Print floating point number. + */ +u64 print_float(u32 f) { + // again not sure why this is any better than cprintf("%f") or similar. Maybe a tiny bit faster? + float ff; + *(u32*)&ff = f; + char* str = PrintPending.cast().c(); + if (!PrintPending.offset) { + str = (PrintBufArea + 0x18).cast().c(); + } + + PrintPending = make_ptr(strend(str)).cast(); + + ftoa((char*)PrintPending.c(), ff, 0xffffffff, ' ', 4, 0); + return f; +} + +/*! + * Print method for symbol. Just prints the name without quotes or anything fancy. + */ +u64 print_symbol(u32 obj) { + if (((obj < SymbolTable2.offset || 0x7ffffff < obj) && // not in normal memory + (obj < 0x84000 || 0x100000 <= obj)) // not in kernel memory + || ((obj & OFFSET_MASK) != BASIC_OFFSET) || + *Ptr(obj - 4) != *(s7 + FIX_SYM_SYMBOL_TYPE)) { + cprintf("#", obj); + } else { + char* str = info(Ptr(obj))->str->data(); + cprintf("%s", str); + } + return obj; +} + +/*! + * Print method for type. Just prints the name without quotes + */ +u64 print_type(u32 obj) { + if (((obj < SymbolTable2.offset || 0x7ffffff < obj) && // not in normal memory + (obj < 0x84000 || 0x100000 <= obj)) // not in kernel memory + || ((obj & OFFSET_MASK) != BASIC_OFFSET) || *Ptr(obj - 4) != *(s7 + FIX_SYM_TYPE_TYPE)) { + cprintf("#", obj); + } else { + cprintf("%s", info(Ptr(obj)->symbol)->str->data()); + } + return obj; +} + +/*! + * Print method for string. Prints the string in quotes. + */ +u64 print_string(u32 obj) { + if (((obj < SymbolTable2.offset || 0x7ffffff < obj) && // not in normal memory + (obj < 0x84000 || 0x100000 <= obj)) // not in kernel memory + || ((obj & OFFSET_MASK) != BASIC_OFFSET) || + *Ptr(obj - 4) != *(s7 + FIX_SYM_STRING_TYPE)) { + cprintf("#", obj); + } else { + cprintf("\"%s\"", Ptr(obj)->data()); + } + return obj; +} + +/*! + * Print method for function. Just prints the address because functions can't identify themselves. + */ +u64 print_function(u32 obj) { + cprintf("#", info(Ptr(*Ptr(obj - 4))->symbol)->str->data(), obj); + return obj; +} + +/*! + * Print method for VU functions. Again, just prints address. + */ +u64 print_vu_function(u32 obj) { + cprintf("#", obj); + return obj; +} + +/*! + * Get the allocated size field of a basic. By default we grab this from the type struct. + * Dynamically sized basics should override this method. + */ +u64 asize_of_basic(u32 it) { + return Ptr(it - 4)->allocated_size; +} + +/*! + * Copy method that does no copying. + */ +u64 copy_fixed(u32 it) { + return it; +} + +/*! + * Default copy for a structure. Since this has no idea of the actual type, it doesn't know what + * size to copy. So we do no copy and return a reference to the original data. + */ +u64 copy_structure(u32 it, u32 unknown) { + (void)unknown; + return it; +} + +/*! + * Create a copy of a basic. If the destination isn't identified as a symbol, treat it as an + * address. This seems a little bit unsafe to me, as it reads the 4-bytes before the given address + * and checks it against the symbol type pointer to see if its a symbol. It seems possible to have a + * false positive for this check. + */ +u64 copy_basic(u32 obj, u32 heap) { + // determine size of basic. We call a method instead of using asize_of_basic in case the type has + // overridden the default asize_of method. + u32 size = call_method_of_type(obj, Ptr(*Ptr(obj - BASIC_OFFSET)), GOAL_ASIZE_FUNC); + u32 result; + + if (*Ptr(heap - 4) == *(s7 + FIX_SYM_SYMBOL_TYPE)) { + // we think we're creating a new copy on a heap. First allocate memory... + result = alloc_heap_object(heap, *Ptr(obj - BASIC_OFFSET), size); + // then copy! (minus the type tag, alloc_heap_object already did it for us) + memcpy(Ptr(result).c(), Ptr(obj).c(), size - BASIC_OFFSET); + } else { + printf("DANGER COPY BASIC!\n"); + // copy directly (including type tag) + memcpy(Ptr(heap - BASIC_OFFSET).c(), Ptr(obj - BASIC_OFFSET).c(), size); + result = heap; + } + return result; +} + +/*! + * Highest level inspect method. Won't inspect 64-bit bintegers correctly. + */ +u64 inspect_object(u32 obj) { + if ((obj & OFFSET_MASK) == BINTEGER_OFFSET) { + return inspect_binteger(obj); + } else { + if ((obj < SymbolTable2.offset || 0x7ffffff < obj) && // not in normal memory + (obj < 0x84000 || 0x100000 <= obj)) { // not in kernel memory + cprintf("#", obj); + } else if ((obj & OFFSET_MASK) == PAIR_OFFSET) { + return inspect_pair(obj); + } else if ((obj & OFFSET_MASK) == BASIC_OFFSET) { + return call_method_of_type(obj, Ptr(*Ptr(obj - BASIC_OFFSET)), GOAL_INSPECT_FUNC); + } else { + cprintf("#", obj & OFFSET_MASK, obj); + } + } + return obj; +} + +/*! + * Inspect a pair. + */ +u64 inspect_pair(u32 obj) { + cprintf("[%8x] pair ", obj); + print_pair(obj); + cprintf("\n"); + return obj; +} + +/*! + * Inspect an integer (works correctly on 64-bit integers) + */ +u64 inspect_integer(u64 obj) { + // and now we're using cprintf. Why doesn't print do this? + cprintf("[%16lx] fixnum %ld\n", obj, obj); + return obj; +} + +/*! + * Inspect a boxed integer (works correctly on 64-integers) + */ +u64 inspect_binteger(u64 obj) { + cprintf("[%16lx] boxed-fixnum %ld\n", obj, s64(obj) >> 3); + return obj; +} + +/*! + * Inspect a floating point number + */ +u64 inspect_float(u32 f) { + float ff; + ff = *(float*)(&f); + cprintf("[%8x] float ", f); + + // likely copy-pasta - no need for this check because of the cprintf immediately before. + char* str = PrintPending.cast().c(); + if (!str) { + str = (PrintBufArea + 0x18).cast().c(); + } + + PrintPending = make_ptr(strend(str)).cast(); + + ftoa(PrintPending.cast().c(), ff, -1, ' ', 4, 0); + cprintf("\n"); + return f; +} + +/*! + * Inspect a string. There's a typo in allocated_length (has underscore instead of dash). + * This typo is fixed in later games. + */ +u64 inspect_string(u32 obj) { + if (((obj < SymbolTable2.offset || 0x7ffffff < obj) && // not in normal memory + (obj < 0x84000 || 0x100000 <= obj)) // not in kernel memory + || ((obj & OFFSET_MASK) != BASIC_OFFSET) || + *Ptr(obj - 4) != *(s7 + FIX_SYM_STRING_TYPE)) { + cprintf("#\n", obj); + } else { + auto str = Ptr(obj); + cprintf("[%8x] string\n\tallocated_length: %d\n\tdata: \"%s\"\n", obj, str->len, str->data()); + } + return obj; +} + +/*! + * Inspect a symbol. + */ +u64 inspect_symbol(u32 obj) { + if (((obj < SymbolTable2.offset || 0x7ffffff < obj) && // not in normal memory + (obj < 0x84000 || 0x100000 <= obj)) // not in kernel memory + || ((obj & OFFSET_MASK) != BASIC_OFFSET) || + *Ptr(obj - 4) != *(s7 + FIX_SYM_SYMBOL_TYPE)) { + cprintf("#", obj); + } else { + auto sym = Ptr(obj); + auto inf = info(sym); + cprintf("[%8x] symbol\n\tname: %s\n\thash: #x%x\n\tvalue: ", obj, inf->str->data(), inf->hash); + print_object(sym->value); + cprintf("\n"); + } + return obj; +} + +/*! + * Inspect a type. + */ +u64 inspect_type(u32 obj) { + if (((obj < SymbolTable2.offset || 0x7ffffff < obj) && // not in normal memory + (obj < 0x84000 || 0x100000 <= obj)) // not in kernel memory + || ((obj & OFFSET_MASK) != BASIC_OFFSET) || *Ptr(obj - 4) != *(s7 + FIX_SYM_TYPE_TYPE)) { + cprintf("#\n", obj); + } else { + auto typ = Ptr(obj); + auto sym = typ->symbol; + auto inf = info(sym); + + cprintf("[%8x] type\n\tname: %s\n\tparent: ", obj, inf->str->data()); + print_object(typ->parent.offset); + cprintf("\n\tsize: %d/%d\n\theap-base: %d\n\tallocated_length: %d\n\tprint: ", + typ->allocated_size, typ->padded_size, typ->heap_base, typ->num_methods); + print_object(typ->print_method.offset); + cprintf("\n\tinspect: "); + print_object(typ->inspect_method.offset); + cprintf("\n"); + } + return obj; +} + +/*! + * Inspect a structure. + */ +u64 inspect_structure(u32 obj) { + cprintf("[%8x] structure\n", obj); + return obj; +} + +/*! + * Inspect a basic. This is just a fallback for basics which don't know how to inspect themselves. + * We just use print_object. + */ +u64 inspect_basic(u32 obj) { + if (((obj < SymbolTable2.offset || 0x7ffffff < obj) && // not in normal memory + (obj < 0x84000 || 0x100000 <= obj)) // not in kernel memory + || ((obj & OFFSET_MASK) != BASIC_OFFSET)) { + cprintf("#\n", obj); + } else { + cprintf("[%8x] ", obj); + print_object(*Ptr(obj - 4)); + cprintf("\n"); + } + return obj; +} + +/*! + * Inspect a link block. This link block doesn't seem to be used at all. + */ +u64 inspect_link_block(u32 ob) { + struct LinkBlock { + u32 length; + u32 version; + }; + + auto lb = Ptr(ob); + cprintf("[%8x] link-block\n\tallocated_length: %d\n\tversion: %d\n\tfunction: ", ob, lb->length, + lb->version); + print_object(ob + lb->length); + cprintf("\n"); + return ob; +} + +/*! + * Inspect a VU Function. Doesn't seem to be used. Also the concept of "vu-function" + * isn't really used. VU0 macro mode stuff goes in normal functions, and micro-mode stuff goes + * in a giant dump of many functions thats loaded and unloaded all at the same time. + */ +u64 inspect_vu_function(u32 obj) { + struct VuFunction { + u32 length; + u32 origin; + u32 qlength; + }; + + auto vf = Ptr(obj); + cprintf("[%8x] vu-function\n\tlength: %d\n\torigin: #x%x\n\tqlength: %d\n", obj, vf->length, + vf->origin, vf->qlength); + return obj; +} + +/*! + * This doesn't exist in the game, but we add it as a wrapper around kheapstatus. + * Note that this isn't a great inspect as it prints to stdout instead of the printbuffer. + */ +u64 inspect_kheap(u32 obj) { + kheapstatus(Ptr(obj)); + return obj; +} + +/*! + * Doesn't exist in the game. Maybe it was a macro? + */ +u64 pack_type_flag(u64 methods, u64 heap_base, u64 size) { + return (methods << 32) + (heap_base << 16) + (size); +} + +// void iterate_symbol_table(std::function f) { +// auto sym = s7.offset; +// for (; sym < LastSymbol.offset; sym += 8) { +// f(sym); +// } +// +// sym = SymbolTable2.offset; +// for (; sym < s7.offset; sym += 8) { +// f(sym); +// } +//} +// +// void print_symbol_table() { +// u32 sym_cnt = 0; +// iterate_symbol_table([&](u32 sym) { +// if (info(Ptr(sym))->hash) { +// sym_cnt++; +// inspect_object(sym); +// // inspect_object(Ptr(sym)->value); +// // printf("\n"); +// } +// }); +// cprintf("Total %d symbols\n", sym_cnt); +//} + +/*! + * TODO remove me! + */ +s32 test_function(s32 arg0, s32 arg1, s32 arg2, s32 arg3) { + return arg0 + 2 * arg1 + 3 * arg2 + 4 * arg3; +} + +extern "C" { +// defined in asm_funcs. It calls format_impl and sets up arguments correctly. +void _format(); +} + +/*! + * Initializes the GOAL Heap, GOAL Symbol Table, GOAL Funcdamental Types, loads the GOAL kernel, + * exports Machine functions, loads the game engine, and calls "play" to initialize the engine. + * + * This takes care of all initialization that isn't for the hardware itself. + */ +s32 InitHeapAndSymbol() { + // allocate memory for the symbol table + auto symbol_table = kmalloc(kglobalheap, 0x20000, KMALLOC_MEMSET, "symbol-table").cast(); + + // pointer to the middle symbol is stored in the s7 register. + s7 = symbol_table + (GOAL_MAX_SYMBOLS / 2) * 8 + BASIC_OFFSET; + // pointer to the first symbol (SymbolTable2 is the "lower" symbol table) + SymbolTable2 = symbol_table + BASIC_OFFSET; + // the last symbol we will ever access. + LastSymbol = symbol_table + 0xff00; + NumSymbols = 0; + // inform compiler the symbol table is reset, and where it is. + reset_output(); + + // set up the empty pair: + *(s7 + FIX_SYM_EMPTY_CAR) = (s7 + FIX_SYM_EMPTY_PAIR).offset; + *(s7 + FIX_SYM_EMPTY_CDR) = (s7 + FIX_SYM_EMPTY_PAIR).offset; + + // need to set up 'global fixed symbol so allocating memory works. + *(s7 + FIX_SYM_GLOBAL_HEAP) = kglobalheap.offset; + + // allocate fundamental types + alloc_and_init_type((s7 + FIX_SYM_TYPE_TYPE).cast(), 9); + alloc_and_init_type((s7 + FIX_SYM_SYMBOL_TYPE).cast(), 9); + alloc_and_init_type((s7 + FIX_SYM_STRING_TYPE).cast(), 9); + alloc_and_init_type((s7 + FIX_SYM_FUNCTION_TYPE).cast(), 9); + + // booleans + set_fixed_symbol(FIX_SYM_FALSE, "#f", s7.offset + FIX_SYM_FALSE); + set_fixed_symbol(FIX_SYM_TRUE, "#t", s7.offset + FIX_SYM_TRUE); + + // functions + set_fixed_symbol(FIX_SYM_NOTHING_FUNC, "nothing", make_nothing_func().offset); + set_fixed_symbol(FIX_SYM_ZERO_FUNC, "zero-func", make_zero_func().offset); + set_fixed_symbol(FIX_SYM_ASIZE_OF_BASIC_FUNC, "asize-of-basic-func", + make_function_from_c((void*)asize_of_basic).offset); + // NOTE: this is a typo in the game too. + set_fixed_symbol(FIX_SYM_COPY_BASIC_FUNC, "asize-of-basic-func", + make_function_from_c((void*)copy_basic).offset); + set_fixed_symbol(FIX_SYM_DEL_BASIC_FUNC, "delete-basic", + make_function_from_c((void*)delete_basic).offset); + + // heap symbols + set_fixed_symbol(FIX_SYM_GLOBAL_HEAP, "global", kglobalheap.offset); + set_fixed_symbol(FIX_SYM_DEBUG_HEAP, "debug", kdebugheap.offset); + set_fixed_symbol(FIX_SYM_STATIC, "static", (s7 + FIX_SYM_STATIC).offset); + set_fixed_symbol(FIX_SYM_LOADING_LEVEL, "loading-level", (s7 + FIX_SYM_LOADING_LEVEL).offset); + set_fixed_symbol(FIX_SYM_LOADING_PACKAGE, "loading-package", + (s7 + FIX_SYM_LOADING_PACKAGE).offset); + set_fixed_symbol(FIX_SYM_PROCESS_LEVEL_HEAP, "process-level-heap", + (s7 + FIX_SYM_PROCESS_LEVEL_HEAP).offset); + set_fixed_symbol(FIX_SYM_STACK, "stack", (s7 + FIX_SYM_STACK).offset); + set_fixed_symbol(FIX_SYM_SCRATCH, "scratch", (s7 + FIX_SYM_SCRATCH).offset); + set_fixed_symbol(FIX_SYM_SCRATCH_TOP, "*scratch-top*", 0x70000000); + + // level stuff + set_fixed_symbol(FIX_SYM_LEVEL, "level", 0); + set_fixed_symbol(FIX_SYM_ART_GROUP, "art-group", 0); + set_fixed_symbol(FIX_SYM_TX_PAGE_DIR, "texture-page-dir", 0); + set_fixed_symbol(FIX_SYM_TX_PAGE, "texture-page", 0); + set_fixed_symbol(FIX_SYM_SOUND, "sound", 0); + set_fixed_symbol(FIX_SYM_DGO, "dgo", 0); + set_fixed_symbol(FIX_SYM_TOP_LEVEL, "top-level", *(s7 + FIX_SYM_NOTHING_FUNC)); + + // OBJECT type + auto new_illegal_func = make_function_from_c((void*)new_illegal); + auto delete_illegal_func = make_function_from_c((void*)delete_illegal); + auto print_object_func = make_function_from_c((void*)print_object); + auto inspect_object_func = make_function_from_c((void*)inspect_object); + set_fixed_type(FIX_SYM_OBJECT_TYPE, "object", (s7 + FIX_SYM_OBJECT_TYPE).cast(), + pack_type_flag(9, 0, 4), print_object_func.offset, inspect_object_func.offset); + auto object_type = Ptr(*(s7 + FIX_SYM_OBJECT_TYPE)); + object_type->new_method = new_illegal_func; + object_type->delete_method = delete_illegal_func; + object_type->asize_of_method.offset = *(s7 + FIX_SYM_ZERO_FUNC); + auto copy_fixed_function = make_function_from_c((void*)copy_fixed); + object_type->copy_method = copy_fixed_function; + + // STRUCTURE type + auto print_structure_func = make_function_from_c((void*)print_structure); + auto inspect_structure_func = make_function_from_c((void*)inspect_structure); + set_fixed_type(FIX_SYM_STRUCTURE_TYPE, "structure", (s7 + FIX_SYM_OBJECT_TYPE).cast(), + pack_type_flag(9, 0, 4), print_structure_func.offset, + inspect_structure_func.offset); + auto new_structure_func = make_function_from_c((void*)new_structure); + auto delete_structure_func = make_function_from_c((void*)delete_structure); + auto structureType = Ptr(*(s7 + FIX_SYM_STRUCTURE_TYPE)); + structureType->new_method = new_structure_func; + structureType->delete_method = delete_structure_func; + + // BASIC type + auto print_basic_func = make_function_from_c((void*)print_basic); + auto inspect_basic_function = make_function_from_c((void*)inspect_basic); + set_fixed_type(FIX_SYM_BASIC_TYPE, "basic", (s7 + FIX_SYM_STRUCTURE_TYPE).cast(), + pack_type_flag(9, 0, 4), print_basic_func.offset, inspect_basic_function.offset); + auto new_basic_func = make_function_from_c((void*)new_basic); + auto basicType = Ptr(*(s7 + FIX_SYM_BASIC_TYPE)); + basicType->new_method = new_basic_func; + basicType->delete_method.offset = *(s7 + FIX_SYM_DEL_BASIC_FUNC); + basicType->asize_of_method.offset = *(s7 + FIX_SYM_ASIZE_OF_BASIC_FUNC); + basicType->copy_method.offset = *(s7 + FIX_SYM_COPY_BASIC_FUNC); + + // SYMBOL type + auto print_symbol_func = make_function_from_c((void*)print_symbol); + auto inspect_symbol_func = make_function_from_c((void*)inspect_symbol); + set_fixed_type(FIX_SYM_SYMBOL_TYPE, "symbol", (s7 + FIX_SYM_BASIC_TYPE).cast(), + pack_type_flag(9, 0, 8), print_symbol_func.offset, inspect_symbol_func.offset); + auto symbolType = Ptr(*(s7 + FIX_SYM_SYMBOL_TYPE)); + symbolType->new_method = new_illegal_func; + symbolType->delete_method = delete_illegal_func; + + // TYPE type + auto print_type_func = make_function_from_c((void*)print_type); + auto inspect_type_func = make_function_from_c((void*)inspect_type); + set_fixed_type(FIX_SYM_TYPE_TYPE, "type", (s7 + FIX_SYM_BASIC_TYPE).cast(), + pack_type_flag(9, 0, 0x38), print_type_func.offset, inspect_type_func.offset); + auto typeType = Ptr(*(s7 + FIX_SYM_TYPE_TYPE)); + auto new_type_func = make_function_from_c((void*)new_type); + typeType->new_method = new_type_func; + typeType->delete_method = delete_illegal_func; + + // STRING type + auto print_string_func = make_function_from_c((void*)print_string); + auto inspect_string_func = make_function_from_c((void*)inspect_string); + set_fixed_type(FIX_SYM_STRING_TYPE, "string", (s7 + FIX_SYM_BASIC_TYPE).cast(), + pack_type_flag(9, 0, 8), print_string_func.offset, inspect_string_func.offset); + + // FUNCTION type + auto print_function_func = make_function_from_c((void*)print_function); + set_fixed_type(FIX_SYM_FUNCTION_TYPE, "function", (s7 + FIX_SYM_BASIC_TYPE).cast(), + pack_type_flag(9, 0, 4), print_function_func.offset, 0); + + // VU FUNCTION type + auto print_vu_function_func = make_function_from_c((void*)print_vu_function); + auto inspect_vu_function_func = make_function_from_c((void*)inspect_vu_function); + set_fixed_type(FIX_SYM_VU_FUNCTION_TYPE, "vu-function", + (s7 + FIX_SYM_STRUCTURE_TYPE).cast(), pack_type_flag(9, 0, 0x10), + print_vu_function_func.offset, inspect_vu_function_func.offset); + + // LINK BLOCK type + auto inspect_link_block_func = make_function_from_c((void*)inspect_link_block); + set_fixed_type(FIX_SYM_LINK_BLOCK, "link-block", (s7 + FIX_SYM_BASIC_TYPE).cast(), + pack_type_flag(9, 0, 0xc), 0, inspect_link_block_func.offset); + auto linkBlockType = Ptr(*(s7 + FIX_SYM_LINK_BLOCK)); + linkBlockType->new_method = new_illegal_func; + linkBlockType->delete_method = delete_illegal_func; + + // KHEAP + auto inspect_kheap_func = make_function_from_c((void*)inspect_kheap); + set_fixed_type(FIX_SYM_KHEAP, "kheap", (s7 + FIX_SYM_STRUCTURE_TYPE).cast(), + pack_type_flag(9, 0, 0x10), 0, inspect_kheap_func.offset); + + // ARRAY + set_fixed_type(FIX_SYM_ARRAY_TYPE, "array", (s7 + FIX_SYM_BASIC_TYPE).cast(), + pack_type_flag(9, 0, 0x10), 0, 0); + + // PAIR + auto print_pair_func = make_function_from_c((void*)print_pair); + auto inspect_pair_func = make_function_from_c((void*)inspect_pair); + set_fixed_type(FIX_SYM_PAIR_TYPE, "pair", (s7 + FIX_SYM_OBJECT_TYPE).cast(), + pack_type_flag(9, 0, 8), print_pair_func.offset, inspect_pair_func.offset); + auto pairType = Ptr(*(s7 + FIX_SYM_PAIR_TYPE)); + auto new_pair_func = make_function_from_c((void*)new_pair); + auto delete_pair_func = make_function_from_c((void*)delete_pair); + pairType->new_method = new_pair_func; + pairType->delete_method = delete_pair_func; + + // KERNEL TYPES + set_fixed_type(FIX_SYM_PROCESS_TREE_TYPE, "process-tree", + (s7 + FIX_SYM_BASIC_TYPE).cast(), pack_type_flag(0xe, 0, 0x20), 0, 0); + set_fixed_type(FIX_SYM_PROCESS_TYPE, "process", (s7 + FIX_SYM_PROCESS_TREE_TYPE).cast(), + pack_type_flag(0xe, 0, 0x70), 0, 0); + set_fixed_type(FIX_SYM_THREAD_TYPE, "thread", (s7 + FIX_SYM_BASIC_TYPE).cast(), + pack_type_flag(0xc, 0, 0x28), 0, 0); + set_fixed_type(FIX_SYM_CONNECTABLE_TYPE, "connectable", + (s7 + FIX_SYM_STRUCTURE_TYPE).cast(), pack_type_flag(9, 0, 0x10), 0, 0); + set_fixed_type(FIX_SYM_STACK_FRAME_TYPE, "stack-frame", (s7 + FIX_SYM_BASIC_TYPE).cast(), + pack_type_flag(9, 0, 0xc), 0, 0); + set_fixed_type(FIX_SYM_FILE_STREAM_TYPE, "file-stream", (s7 + FIX_SYM_BASIC_TYPE).cast(), + pack_type_flag(9, 0, 0x14), 0, 0); + + // NUMERIC TYPES + set_fixed_type(FIX_SYM_POINTER_TYPE, "pointer", (s7 + FIX_SYM_OBJECT_TYPE).cast(), + pack_type_flag(9, 0, 4), 0, 0); + + // NUMERIC TYPES + auto print_integer_func = make_function_from_c((void*)print_integer); + auto inspect_integer_func = make_function_from_c((void*)inspect_integer); + set_fixed_type(FIX_SYM_NUMBER_TYPE, "number", (s7 + FIX_SYM_OBJECT_TYPE).cast(), + pack_type_flag(9, 0, 8), print_integer_func.offset, inspect_integer_func.offset); + + auto print_float_func = make_function_from_c((void*)print_float); + auto inspect_float_func = make_function_from_c((void*)inspect_float); + set_fixed_type(FIX_SYM_FLOAT_TYPE, "float", (s7 + FIX_SYM_NUMBER_TYPE).cast(), + pack_type_flag(9, 0, 4), print_float_func.offset, inspect_float_func.offset); + + set_fixed_type(FIX_SYM_INTEGER_TYPE, "integer", (s7 + FIX_SYM_NUMBER_TYPE).cast(), + pack_type_flag(9, 0, 8), 0, 0); + + auto print_binteger_func = make_function_from_c((void*)print_binteger); + auto inspect_binteger_func = make_function_from_c((void*)inspect_binteger); + set_fixed_type(FIX_SYM_BINTEGER_TYPE, "binteger", (s7 + FIX_SYM_INTEGER_TYPE).cast(), + pack_type_flag(9, 0, 8), print_binteger_func.offset, inspect_binteger_func.offset); + + set_fixed_type(FIX_SYM_SINTEGER_TYPE, "sinteger", (s7 + FIX_SYM_INTEGER_TYPE).cast(), + pack_type_flag(9, 0, 8), 0, 0); + set_fixed_type(FIX_SYM_INT8_TYPE, "int8", (s7 + FIX_SYM_SINTEGER_TYPE).cast(), + pack_type_flag(9, 0, 1), 0, 0); + set_fixed_type(FIX_SYM_INT16_TYPE, "int16", (s7 + FIX_SYM_SINTEGER_TYPE).cast(), + pack_type_flag(9, 0, 2), 0, 0); + set_fixed_type(FIX_SYM_INT32_TYPE, "int32", (s7 + FIX_SYM_SINTEGER_TYPE).cast(), + pack_type_flag(9, 0, 4), 0, 0); + set_fixed_type(FIX_SYM_INT64_TYPE, "int64", (s7 + FIX_SYM_SINTEGER_TYPE).cast(), + pack_type_flag(9, 0, 8), 0, 0); + set_fixed_type(FIX_SYM_INT128_TYPE, "int128", (s7 + FIX_SYM_SINTEGER_TYPE).cast(), + pack_type_flag(9, 0, 0x10), 0, 0); + + set_fixed_type(FIX_SYM_UINTEGER_TYPE, "uintger", (s7 + FIX_SYM_INTEGER_TYPE).cast(), + pack_type_flag(9, 0, 8), 0, 0); + set_fixed_type(FIX_SYM_UINT8_TYPE, "uint8", (s7 + FIX_SYM_UINTEGER_TYPE).cast(), + pack_type_flag(9, 0, 1), 0, 0); + set_fixed_type(FIX_SYM_UINT16_TYPE, "uint16", (s7 + FIX_SYM_UINTEGER_TYPE).cast(), + pack_type_flag(9, 0, 2), 0, 0); + set_fixed_type(FIX_SYM_UINT32_TYPE, "uint32", (s7 + FIX_SYM_UINTEGER_TYPE).cast(), + pack_type_flag(9, 0, 4), 0, 0); + set_fixed_type(FIX_SYM_UINT64_TYPE, "uint64", (s7 + FIX_SYM_UINTEGER_TYPE).cast(), + pack_type_flag(9, 0, 8), 0, 0); + set_fixed_type(FIX_SYM_UINT128_TYPE, "uint128", (s7 + FIX_SYM_UINTEGER_TYPE).cast(), + pack_type_flag(9, 0, 0x10), 0, 0); + + // Object new macro + auto goal_new_object_func = make_function_from_c((void*)alloc_heap_object); + object_type->new_method = goal_new_object_func; + + // Stuff that isn't in a fixed spot: + make_function_symbol_from_c("string->symbol", (void*)intern); + make_function_symbol_from_c("print", (void*)sprint); + make_function_symbol_from_c("inspect", (void*)inspect_object); + + // loads + make_function_symbol_from_c("load", (void*)load); + make_function_symbol_from_c("loado", (void*)loado); + make_function_symbol_from_c("unload", (void*)unload); + + make_function_symbol_from_c("_format", (void*)_format); + + // allocations + make_function_symbol_from_c("malloc", (void*)alloc_heap_memory); + make_function_symbol_from_c("kmalloc", (void*)goal_malloc); + make_function_symbol_from_c("new-dynamic-structure", (void*)new_dynamic_structure); + + // type system + make_function_symbol_from_c("method-set!", (void*)method_set); + + // dgo + make_function_symbol_from_c("link", (void*)link_and_exec_wrapper); + make_function_symbol_from_c("dgo-load", (void*)load_and_link_dgo); + + // forward declare + make_raw_function_symbol_from_c("ultimate-memcpy", 0); + make_raw_function_symbol_from_c("memcpy-and-rellink", 0); // never used + make_raw_function_symbol_from_c("symlink2", 0); + make_raw_function_symbol_from_c("symlink3", 0); + + // game stuff + make_function_symbol_from_c("link-begin", (void*)link_begin); + make_function_symbol_from_c("link-resume", (void*)link_resume); + // make_function_symbol_from_c("mc-run", &CKernel::not_yet_implemented); + // make_function_symbol_from_c("mc-format", &CKernel::not_yet_implemented); + // make_function_symbol_from_c("mc-unformat", &CKernel::not_yet_implemented); + // make_function_symbol_from_c("mc-create-file", &CKernel::not_yet_implemented); + // make_function_symbol_from_c("mc-save", &CKernel::not_yet_implemented); + // make_function_symbol_from_c("mc-load", &CKernel::not_yet_implemented); + // make_function_symbol_from_c("mc-check-result", &CKernel::not_yet_implemented); + // make_function_symbol_from_c("mc-get-slot-info", &CKernel::not_yet_implemented); + // make_function_symbol_from_c("mc-makefile", &CKernel::not_yet_implemented); + // make_function_symbol_from_c("kset-language", &CKernel::not_yet_implemented); + + // set *debug-segment* + auto ds_symbol = intern_from_c("*debug-segment*"); + if (DebugSegment) { + ds_symbol->value = (s7 + FIX_SYM_TRUE).offset; + } else { + ds_symbol->value = (s7 + FIX_SYM_FALSE).offset; + } + + // setup *enable-method-set* + auto method_set_symbol = intern_from_c("*enable-method-set*"); + EnableMethodSet = method_set_symbol.cast(); + method_set_symbol->value = 0; + + // set *boot-video-mode* + intern_from_c("*boot-video-mode*")->value = 0; + + // load the kernel! + method_set_symbol->value++; + load_and_link_dgo_from_c("kernel", kglobalheap, + LINK_FLAG_OUTPUT_LOAD | LINK_FLAG_EXECUTE | LINK_FLAG_PRINT_LOGIN, + 0x400000); + method_set_symbol->value--; + + // check the kernel version! + auto kernel_version = intern_from_c("*kernel-version*")->value; + if (!kernel_version || ((kernel_version >> 0x13) != KERNEL_VERSION_MAJOR)) { + MsgErr("\n"); + MsgErr( + "dkernel: compiled C kernel version is %d.%d but the goal kernel is %d.%d\n\tfrom the " + "goal> prompt (:mch) then mkee your kernel in linux.\n", + KERNEL_VERSION_MAJOR, KERNEL_VERSION_MINOR, kernel_version >> 0x13, + (kernel_version >> 3) & 0xffff); + return -1; + } else { + printf("Got correct kernel version %d.%d\n", kernel_version >> 0x13, + (kernel_version >> 3) & 0xffff); + } + + // setup deci2count for message counter. + protoBlock.deci2count = intern_from_c("*deci-count*").cast(); + + // load stuff for the listener interface + InitListener(); + + // Do final initialization, including loading and initializing the engine. + InitMachineScheme(); + + // testing stuff: + make_function_symbol_from_c("test-function", (void*)test_function); + + return 0; +} + +/*! + * GOAL "load" function for debug loads. Doesn't load off the CD. + */ +u64 load(u32 file_name_in, u32 heap_in) { + printf("LOAD!\n"); // added by me + Ptr file_name(file_name_in); + Ptr heap(heap_in); + char decodedName[260]; // could be 256 or 260? + + auto loading_pack_sym = Ptr(s7.offset + FIX_SYM_LOADING_PACKAGE); + auto last_loading_pack = loading_pack_sym->value; + loading_pack_sym->value = heap_in; + + kstrcpy(decodedName, DecodeFileName(file_name->data())); + s32 returnValue = load_and_link( + file_name->data(), decodedName, heap.c(), + LINK_FLAG_OUTPUT_TRUE | LINK_FLAG_OUTPUT_LOAD | LINK_FLAG_EXECUTE | LINK_FLAG_PRINT_LOGIN); + + loading_pack_sym->value = last_loading_pack; + + if (returnValue < 0) { + return s7.offset; + } else { + return returnValue; + } +} + +/*! + * Unused. But a C version of loading code. Doesn't load off the CD. + */ +u64 loadc(const char* file_name, kheapinfo* heap, u32 flags) { + char decodedName[260]; // could be 256 or 260? + + auto loading_pack_sym = Ptr(s7.offset + FIX_SYM_LOADING_PACKAGE); + auto last_loading_pack = loading_pack_sym->value; + loading_pack_sym->value = make_ptr(heap).offset; + printf("****** CALL TO loadc() ******\n"); // not added by me + auto name = MakeFileName(CODE_FILE_TYPE, file_name, 0); + kstrcpy(decodedName, name); + + s32 returnValue = load_and_link(file_name, decodedName, heap, flags); + + loading_pack_sym->value = last_loading_pack; + + if (returnValue < 0) { + return s7.offset; + } else { + return returnValue; + } +} + +/*! + * Load Object? Uses DATA_FILE_TYPE and doesn't inform listener about the load, or execute a + * top level segment if a V3 is loaded. Doesn't load off the CD. + */ +u64 loado(u32 file_name_in, u32 heap_in) { + char loadName[272]; + Ptr file_name(file_name_in); + Ptr heap(heap_in); + printf("****** CALL TO loado(%s) ******\n", file_name->data()); + kstrcpy(loadName, MakeFileName(DATA_FILE_TYPE, file_name->data(), 0)); + s32 returnValue = load_and_link(file_name->data(), loadName, heap.c(), LINK_FLAG_PRINT_LOGIN); + + if (returnValue < 0) { + return s7.offset; + } else { + return returnValue; + } +} + +/*! + * "Unload". Doesn't free memory, just informs listener we unloaded. + */ +u64 unload(u32 name) { + output_unload(Ptr(name)->data()); + return 0; +} + +/*! + * load and link and exec. Common function in load/loado/loadc. + * Doesn't load off the CD. + */ +s64 load_and_link(const char* filename, char* decode_name, kheapinfo* heap, u32 flags) { + (void)filename; + s32 sz; + auto rv = FileLoad(decode_name, make_ptr(heap), Ptr(0), KMALLOC_ALIGN_64, &sz); + if (((s32)rv.offset) > -1) { + return (s32)link_and_exec(rv, decode_name, sz, make_ptr(heap), flags).offset; + } + return (s32)rv.offset; +} + +// todo, read lcock code \ No newline at end of file diff --git a/game/kernel/kscheme.h b/game/kernel/kscheme.h new file mode 100644 index 0000000000..38c8df351d --- /dev/null +++ b/game/kernel/kscheme.h @@ -0,0 +1,118 @@ +/*! + * @file kscheme.h + * Implementation of GOAL runtime. + */ + +#ifndef JAK_KSCHEME_H +#define JAK_KSCHEME_H + +#include "common/common_types.h" +#include "kmachine.h" +#include "kmalloc.h" + +extern u32 FastLink; +extern s32 NumSymbols; +extern Ptr EnableMethodSet; +extern Ptr s7; +extern Ptr SymbolTable2; +extern Ptr LastSymbol; + +constexpr s32 GOAL_MAX_SYMBOLS = 0x2000; +constexpr s32 BINTEGER_OFFSET = 0; +constexpr s32 PAIR_OFFSET = 2; +constexpr s32 BASIC_OFFSET = 4; +constexpr s32 SYM_INFO_OFFSET = 0xff34; +constexpr u32 EMPTY_HASH = 0x8454B6E6; +constexpr u32 OFFSET_MASK = 7; +constexpr u32 CRC_POLY = 0x04c11db7; + +constexpr u32 GOAL_NEW_FUNC = 0; // method ID of GOAL new +constexpr u32 GOAL_DEL_FUNC = 1; // method ID of GOAL delete +constexpr u32 GOAL_PRINT_FUNC = 2; // method ID of GOAL print +constexpr u32 GOAL_INSPECT_FUNC = 3; // method ID of GOAL inspect +constexpr u32 GOAL_LENGTH_FUNC = 4; // method ID of GOAL length +constexpr u32 GOAL_ASIZE_FUNC = 5; // method ID of GOAL size +constexpr u32 GOAL_COPY_FUNC = 6; // method ID of GOAL copy +constexpr u32 GOAL_RELOC_FUNC = 7; // method ID of GOAL relocate + +constexpr u32 DEFAULT_METHOD_COUNT = 12; +constexpr u32 FALLBACK_UNKNOWN_METHOD_COUNT = 44; + +struct String { + u32 len; + char* data() { return ((char*)this) + sizeof(String); } +}; + +struct SymInfo { + u32 hash; + Ptr str; +}; + +struct Symbol { + u32 value; +}; + +inline Ptr info(Ptr s) { + return s.cast() + SYM_INFO_OFFSET; +} + +struct Function {}; + +/*! + * GOAL Type + */ +struct Type { + Ptr symbol; //! The type's symbol 0x0 + Ptr parent; //! The type's parent 0x4 + u16 allocated_size; //! The type's size in memory 0x8 + u16 padded_size; //! The type's size, when padded? 0xa + + u16 heap_base; //! relative location of heap 0xc + u16 num_methods; //! allocated-length field 0xe - 0xf + + Ptr new_method; // 16 0 + Ptr delete_method; // 20 1 + Ptr print_method; // 24 2 + Ptr inspect_method; // 28 3 + Ptr length_method; // 32 4 + Ptr asize_of_method; // 36 5 + Ptr copy_method; // 40 6 + Ptr relocate_method; // 44 7 + Ptr memusage_method; // 48 8 + + Ptr& get_method(u32 i) { + Ptr* f = &new_method; + return f[i]; + } +}; + +u32 crc32(const u8* data, s32 size); +void kscheme_init_globals(); +void init_crc(); +u64 alloc_from_heap(u32 heapSymbol, u32 type, s32 size); +Ptr intern_from_c(const char* name); +Ptr intern_type_from_c(const char* name, u64 methods); +Ptr set_type_values(Ptr type, Ptr parent, u64 flags); +u64 print_object(u32 obj); +u64 print_pair(u32 obj); +u64 print_binteger(u64 obj); +u64 inspect_pair(u32 obj); +u64 inspect_binteger(u64 obj); +s32 InitHeapAndSymbol(); +u64 call_goal(Ptr f, u64 a, u64 b, u64 c, u64 st, void* offset); +void print_symbol_table(); +u64 make_string_from_c(const char* c_str); +Ptr find_symbol_from_c(const char* name); +u64 call_method_of_type(u32 arg, Ptr type, u32 method_id); +u64 inspect_object(u32 obj); +u64 new_pair(u32 heap, u32 type, u32 car, u32 cdr); +s64 load_and_link(const char* filename, char* decode_name, kheapinfo* heap, u32 flags); +u64 load(u32 file_name_in, u32 heap_in); +u64 loado(u32 file_name_in, u32 heap_in); +u64 unload(u32 name); +Ptr make_function_symbol_from_c(const char* name, void* f); +u64 call_goal_function_by_name(const char* name); +Ptr alloc_and_init_type(Ptr sym, u32 method_count); +Ptr set_fixed_symbol(u32 offset, const char* name, u32 value); + +#endif // JAK_KSCHEME_H diff --git a/game/kernel/ksocket.cpp b/game/kernel/ksocket.cpp new file mode 100644 index 0000000000..4eb0194017 --- /dev/null +++ b/game/kernel/ksocket.cpp @@ -0,0 +1,109 @@ +/*! + * @file ksocket.cpp + * GOAL Socket connection to listener using DECI2/DSNET + * DONE! + */ + +#include "ksocket.h" +#include "kdsnetm.h" +#include "kprint.h" +#include "kboot.h" +#include "fileio.h" +#include "klisten.h" + +/*! + * Update GOAL message header after receiving and verify message is ok. + * Return the size of the message in bytes (not including DECI or GOAL headers) + * Return -1 on error. + * The buffer parameter is unused. + * DONE, removed call to FlushCache(0); + */ +u32 ReceiveToBuffer(char* buff) { + (void)buff; + + // if we received less than the size of the message header, we either got nothing, or there was an + // error + if (protoBlock.last_receive_size < (int)sizeof(GoalMessageHeader)) { + return -1; + } + + // FlushCache(0); + GoalMessageHeader* gbuff = protoBlock.receive_buffer; + u32 msg_size = gbuff->msg_size; + + // check it's our protocol + if (gbuff->deci2_hdr.proto == DECI2_PROTOCOL) { + // null terminate + ((u8*)gbuff)[sizeof(GoalMessageHeader) + msg_size] = '\0'; + // copy stuff to block + protoBlock.msg_kind = gbuff->msg_kind; + protoBlock.msg_id = gbuff->msg_id; + // and mark message as received! + protoBlock.last_receive_size = -1; + } else { + // not our protocol, something has gone wrong. + MsgErr("dkernel: got a bad packet to goal proto (goal #x%lx bytes %d %d %d %ld %d)\n", + (int64_t)protoBlock.receive_buffer, protoBlock.last_receive_size, + protoBlock.receive_buffer->msg_kind, protoBlock.receive_buffer->u6, + protoBlock.receive_buffer->msg_id, msg_size); + protoBlock.last_receive_size = -1; + return -1; + } + return msg_size; +} + +/*! + * Do a DECI2 send and block until it is complete. + * The message type is OUTPUT + * DONE, EXACT + */ +s32 SendFromBuffer(char* buff, s32 size) { + return SendFromBufferD(u16(ListenerMessageKind::MSG_OUTPUT), 0, buff, size); +} + +/*! + * Just prepare the Ack buffer, doesn't actually connect. + * Must be called before attempting to use the socket connection. + * DONE, EXACT + */ +void InitListenerConnect() { + if (MasterDebug) { + kstrcpy(AckBufArea + sizeof(GoalMessageHeader), "ack"); + } +} + +/*! + * Does nothing. + * DONE, EXACT + */ +void InitCheckListener() {} + +/*! + * Doesn't actually wait for a message, just checks if there's currently a message. + * Doesn't actually send an ack either. + * More accurate name would be "CheckForMessage" + * Returns pointer to the message. + * Updates MessCount to be equal to the size of the new message + * DONE, EXACT + */ +Ptr WaitForMessageAndAck() { + if (!MasterDebug) { + MessCount = -1; + } else { + MessCount = ReceiveToBuffer((char*)MessBufArea.c() + sizeof(GoalMessageHeader)); + } + + if (MessCount < 0) { + return Ptr(0); + } + + return MessBufArea.cast() + sizeof(GoalMessageHeader); +} + +/*! + * Doesn't close anything, just print a closed message. + * DONE, EXACT + */ +void CloseListener() { + Msg(6, "dconnect: closed socket at kernel side\n"); +} \ No newline at end of file diff --git a/game/kernel/ksocket.h b/game/kernel/ksocket.h new file mode 100644 index 0000000000..648033e83a --- /dev/null +++ b/game/kernel/ksocket.h @@ -0,0 +1,51 @@ +/*! + * @file ksocket.h + * GOAL Socket connection to listener using DECI2/DSNET + */ + +#ifndef JAK_KSOCKET_H +#define JAK_KSOCKET_H + +#include "common/common_types.h" +#include "kmachine.h" +#include "Ptr.h" + +/*! + * Update GOAL message header after receiving and verify message is ok. + * Return the size of the message in bytes (not including DECI or GOAL headers) + * Return -1 on error. + * The buffer parameter is unused. + */ +u32 ReceiveToBuffer(char* buff); + +/*! + * Do a DECI2 send and block until it is complete. + * The message type is OUTPUT + */ +s32 SendFromBuffer(char* buff, s32 size); + +/*! + * Just prepare the Ack buffer, doesn't actually connect. + * Must be called before attempting to use the socket connection. + */ +void InitListenerConnect(); + +/*! + * Does nothing. + */ +void InitCheckListener(); + +/*! + * Doesn't actually wait for a message, just checks if there's currently a message. + * Doesn't actually send an ack either. + * More accurate name would be "CheckForMessage" + * Returns pointer to the message. + */ +Ptr WaitForMessageAndAck(); + +/*! + * Doesn't close anything, just print a closed message. + */ +void CloseListener(); + +#endif // JAK_KSOCKET_H diff --git a/game/kernel/ksound.cpp b/game/kernel/ksound.cpp new file mode 100644 index 0000000000..3a8eb60842 --- /dev/null +++ b/game/kernel/ksound.cpp @@ -0,0 +1,28 @@ +/*! + * @file ksound.cpp + * There's not much here. My guess is this was set up as framework to match the kmachine.cpp format, + * but whoever did the sound didn't use this. + */ + +#include "ksound.h" +#include "kscheme.h" +#include "kdgo.h" + +/*! + * Does nothing! + */ +void InitSound() {} + +/*! + * Does nothing! + */ +void ShutdownSound() {} + +/*! + * Set up some functions which are somewhat related to sound. + */ +void InitSoundScheme() { + make_function_symbol_from_c("rpc-call", (void*)RpcCall_wrapper); + make_function_symbol_from_c("rpc-busy?", (void*)RpcBusy); + make_function_symbol_from_c("test-load-dgo-c", (void*)LoadDGOTest); +} \ No newline at end of file diff --git a/game/kernel/ksound.h b/game/kernel/ksound.h new file mode 100644 index 0000000000..3dd047953b --- /dev/null +++ b/game/kernel/ksound.h @@ -0,0 +1,14 @@ +/*! + * @file ksound.h + * There's not much here. My guess is this was set up as framework to match the kmachine.cpp format, + * but whoever did the sound didn't use this. + */ + +#ifndef JAK_KSOUND_H +#define JAK_KSOUND_H + +void InitSound(); +void ShutdownSound(); +void InitSoundScheme(); + +#endif // JAK_KSOUND_H diff --git a/game/kernel/todo.txt b/game/kernel/todo.txt new file mode 100644 index 0000000000..d070d6ba5a --- /dev/null +++ b/game/kernel/todo.txt @@ -0,0 +1,29 @@ +kboot +--------- +usleep in KernelCheckAndDispatch + +kmachine +--------- +rewrite InitParms to not use std::string +InitVideo +InitMachine +CPadGetData +InstallHandler +InstallDebugHandler +dma_to_iop +DecodeTime +PutDisplayEnv + +kscheme +---------- +remove the test function +add memory card stuff +read_clock_code + +klink +------- +v2 support + +kmemcard +--------- +all of it, basically. \ No newline at end of file diff --git a/game/main.cpp b/game/main.cpp new file mode 100644 index 0000000000..442d7ad213 --- /dev/null +++ b/game/main.cpp @@ -0,0 +1,16 @@ +/*! + * @file main.cpp + * Main for the game. Launches the runtime. + */ +#include +#include "runtime.h" +#include "common/versions.h" + +int main(int argc, char** argv) { + while(true) { + // run the runtime in a loop so we can reset the game and have it restart cleanly + printf("gk %d.%d\n", versions::GOAL_VERSION_MAJOR, versions::GOAL_VERSION_MINOR); + exec_runtime(argc, argv); + } + return 0; +} \ No newline at end of file diff --git a/game/overlord/dma.cpp b/game/overlord/dma.cpp new file mode 100644 index 0000000000..738ba1f988 --- /dev/null +++ b/game/overlord/dma.cpp @@ -0,0 +1,93 @@ +/*! + * @file dma.cpp + * DMA Related functions for Overlord. + * This code is not great. + */ + +#include +#include +#include "dma.h" +#include "common/common_types.h" +#include "game/sce/iop.h" + +using namespace iop; + +u32 dmaid; // ID of in-progress DMA. 0 if no DMA in progress +sceSifDmaData cmd; // DMA settings +u32 strobe; // ?? mysterious sound DMA flag. + +void dma_init_globals() { + dmaid = 0; + memset(&cmd, 0, sizeof(cmd)); + strobe = 0; +} + +/*! + * Wait for an ongoing DMA transfer to finish. + * IOP DMAs are instant in this version, so we return immediately and clear dmaid. + */ +void DMA_Sync() { + // The DMA is complete. Clear dmaid. + dmaid = 0; + + // for fun, the original code + // if(dmaid != 0) { + // if(sceSifDmaStat(dmaid) > 0) { + // u32 count = 10000; + // while(sceSifDmaStat(dmaid) > 0) { + // DelayThread(10); + // count--; + // if(count == 0) { + // u32 count = 10000; + // } + // } + // } + // + // // better do that again, just to be sure i did it the first time. + // u32 count = 10000; + // while(sceSifDmaStat(dmaid) > 0) { + // DelayThread(10); + // count--; + // if(count == 0) { + // u32 count = 10000; + // } + // } + // dmaid = 0; + // } +} + +/*! + * Start DMA transfer to the EE. + */ +void DMA_SendToEE(void* data, u32 size, void* dest) { + // finish previous DMA + DMA_Sync(); + + // setup command + cmd.mode = 0; + cmd.data = data; + cmd.addr = dest; + cmd.size = size; + + // start DMA (with disabled interrupts) + CpuDisableIntr(); + dmaid = sceSifSetDma(&cmd, 1); + CpuEnableIntr(); + + if (dmaid == 0) { + do { + printf("Got a bad DMA ID!\n"); // added + } while (true); + } +} + +/*! + * SPU DMA interrupt handler. + + */ +u32 intr() { + strobe = 1; + return 0; +} + +// TODO DMA_SendToSPUAndSync() \ No newline at end of file diff --git a/game/overlord/dma.h b/game/overlord/dma.h new file mode 100644 index 0000000000..a973b7d499 --- /dev/null +++ b/game/overlord/dma.h @@ -0,0 +1,16 @@ +/*! + * @file dma.h + * DMA Related functions for Overlord. + * This code is not great. + */ + +#ifndef JAK_V2_DMA_H +#define JAK_V2_DMA_H + +#include "common/common_types.h" + +void DMA_Sync(); +void DMA_SendToEE(void* data, u32 size, void* dest); +void dma_init_globals(); + +#endif // JAK_V2_DMA_H diff --git a/game/overlord/fake_iso.cpp b/game/overlord/fake_iso.cpp new file mode 100644 index 0000000000..6219f69de7 --- /dev/null +++ b/game/overlord/fake_iso.cpp @@ -0,0 +1,353 @@ +/*! + * @file fake_iso.cpp + * This provides an implementation of IsoFs for reading a "fake iso". + * A "fake iso" is just a map file which maps 8.3 ISO file names to files in the source folder. + * This way we don't need to actually create an ISO. + * + * The game has this compilation unit, but there is nothing in it. Probably it is removed to save + * IOP memory and was only included on TOOL-only builds. So this is my interpretation of how it + * should work. + */ + +#include +#include +#include "fake_iso.h" +#include "game/sce/iop.h" +#include "isocommon.h" +#include "overlord.h" + +using namespace iop; + +IsoFs fake_iso; + +/*! + * Map from iso file name to file path in the src folder. + */ +struct FakeIsoEntry { + char iso_name[16]; + char file_path[128]; +}; + +static LoadStackEntry sLoadStack[MAX_OPEN_FILES]; //! List of all files that are "open" +FakeIsoEntry fake_iso_entries[MAX_ISO_FILES]; //! List of all known files +static FileRecord sFiles[MAX_ISO_FILES]; //! List of "FileRecords" for IsoFs API consumers +u32 fake_iso_entry_count; //! Total count of fake iso files +static bool read_in_progress; //! Does the ISO Thread think we're reading? + +static int FS_Init(u8* buffer); +static FileRecord* FS_Find(const char* name); +static FileRecord* FS_FindIN(const char* iso_name); +static uint32_t FS_GetLength(FileRecord* fr); +static LoadStackEntry* FS_Open(FileRecord* fr, int32_t offset); +static LoadStackEntry* FS_OpenWad(FileRecord* fr, int32_t offset); +static void FS_Close(LoadStackEntry* fd); +static uint32_t FS_BeginRead(LoadStackEntry* fd, void* buffer, int32_t len); +static uint32_t FS_SyncRead(); +static uint32_t FS_LoadSoundBank(char*, void*); +static uint32_t FS_LoadMusic(char*, void*); +static void FS_PollDrive(); + +void fake_iso_init_globals() { + // init file lists + memset(fake_iso_entries, 0, sizeof(fake_iso_entries)); + memset(sFiles, 0, sizeof(sFiles)); + memset(sLoadStack, 0, sizeof(sLoadStack)); + fake_iso_entry_count = 0; + + // init API struct + fake_iso.init = FS_Init; + fake_iso.find = FS_Find; + fake_iso.find_in = FS_FindIN; + fake_iso.get_length = FS_GetLength; + fake_iso.open = FS_Open; + fake_iso.open_wad = FS_OpenWad; + fake_iso.close = FS_Close; + fake_iso.begin_read = FS_BeginRead; + fake_iso.sync_read = FS_SyncRead; + fake_iso.load_sound_bank = FS_LoadSoundBank; + fake_iso.load_music = FS_LoadMusic; + fake_iso.poll_drive = FS_PollDrive; + + read_in_progress = false; +} + +//! will hold prefix for the source folder. +static const char* next_dir = nullptr; + +/*! + * Initialize the file system. + */ +int FS_Init(u8* buffer) { + (void)buffer; + // get path to next/. Will be set in the gk.sh launch script. + next_dir = std::getenv("NEXT_DIR"); // todo windows? + assert(next_dir); + + // get path to next/data/fake_iso.txt, the map file. + char fakeiso_path[512]; + strcpy(fakeiso_path, next_dir); + strcat(fakeiso_path, "/game/fake_iso.txt"); // todo windows paths? + + // open the map. + FILE* fp = fopen(fakeiso_path, "r"); + assert(fp); + fseek(fp, 0, SEEK_END); + size_t len = ftell(fp); + rewind(fp); + char* fakeiso = (char*)malloc(len); + if (fread(fakeiso, len, 1, fp) != 1) { + assert(false); + } + + // loop over lines + char* ptr = fakeiso; + while (*ptr) { + // newlines + while (*ptr && *ptr == '\n') + ptr++; + + // comment line + if (*ptr == ';') { + while (*ptr && (*ptr != '\n')) { + ptr++; + } + continue; + } + + // entry line + assert(fake_iso_entry_count < MAX_ISO_FILES); + FakeIsoEntry* e = &fake_iso_entries[fake_iso_entry_count]; + int i = 0; + while (*ptr && (*ptr != ' ') && i < 16) { + e->iso_name[i] = *ptr; + ptr++; + i++; + } + + while (*ptr == ' ') { + ptr++; + } + + i = 0; + while (*ptr && (*ptr != '\n') && (*ptr != ' ') && i < 128) { + e->file_path[i] = *ptr; + ptr++; + i++; + } + fake_iso_entry_count++; + } + + for (u32 i = 0; i < fake_iso_entry_count; i++) { + MakeISOName(sFiles[i].name, fake_iso_entries[i].iso_name); + // we don't figure out the size yet. + // this is so you can change the file without restarting the game. + sFiles[i].size = -1; + // repurpose "location" as the index. + sFiles[i].location = i; + } + + free(fakeiso); + + // TODO load tweak music. + + return 0; +} + +/*! + * Find a file on the disc and return a FileRecord. + * Find using a "normal" 8.3 name. + * This is an ISO FS API Function + */ +FileRecord* FS_Find(const char* name) { + char name_buff[16]; + MakeISOName(name_buff, name); + return FS_FindIN(name_buff); +} + +/*! + * Find a file on the disc. Uses the "ISO name" of the file, which is different from the normal 8.3 + * name. This can be generated with MakeISOFile. + * This is an ISO FS API Function. + */ +FileRecord* FS_FindIN(const char* iso_name) { + const uint32_t* buff = (const uint32_t*)iso_name; + uint32_t count = 0; + while (count < fake_iso_entry_count) { + const uint32_t* ref = (uint32_t*)sFiles[count].name; + if (ref[0] == buff[0] && ref[1] == buff[1] && ref[2] == buff[2]) { + return sFiles + count; + } + count++; + } + printf("[FAKEISO] failed to find %s\n", iso_name); + assert(false); + return nullptr; +} + +/*! + * Build a full file path for a FileRecord. + */ +static const char* get_file_path(FileRecord* fr) { + assert(fr->location < fake_iso_entry_count); + static char path_buffer[1024]; + strcpy(path_buffer, next_dir); + strcat(path_buffer, "/"); + strcat(path_buffer, fake_iso_entries[fr->location].file_path); + return path_buffer; +} + +/*! + * Determine the length of a file. This isn't very fast, but nobody checks file sizes extremely + * quickly. This is an ISO FS API Function + */ +uint32_t FS_GetLength(FileRecord* fr) { + const char* path = get_file_path(fr); + FILE* fp = fopen(path, "rb"); + assert(fp); + fseek(fp, 0, SEEK_END); + uint32_t len = ftell(fp); + rewind(fp); + fclose(fp); + return len; +} + +/*! + * Open a file by putting it on the load stack. + * Set the offset to 0 or -1 if you do not want to have an offset. + * This is an ISO FS API Function + */ +LoadStackEntry* FS_Open(FileRecord* fr, int32_t offset) { + printf("[OVERLORD] FS Open %s\n", fr->name); // Added + LoadStackEntry* selected = nullptr; + // find first unused spot on load stack. + for (uint32_t i = 0; i < MAX_OPEN_FILES; i++) { + if (!sLoadStack[i].fr) { + selected = sLoadStack + i; + selected->fr = fr; + selected->location = 0; + if (offset != -1) { + selected->location += offset; + } + return selected; + } + } + printf("[OVERLORD ISO CD] Failed to FS_Open %s\n", fr->name); + ExitIOP(); + return nullptr; +} + +/*! + * Open a file by putting it on the load stack. + * Like Open, but allows an offset of -1 to be applied. + * This is an ISO FS API Function + */ +LoadStackEntry* FS_OpenWad(FileRecord* fr, int32_t offset) { + printf("[OVERLORD] FS Open %s\n", fr->name); // Added + LoadStackEntry* selected = nullptr; + for (uint32_t i = 0; i < MAX_OPEN_FILES; i++) { + if (!sLoadStack[i].fr) { + selected = sLoadStack + i; + selected->fr = fr; + selected->location = offset; + return selected; + } + } + printf("[OVERLORD ISO CD] Failed to FS_OpenWad %s\n", fr->name); + ExitIOP(); + return nullptr; +} + +/*! + * Close an open file. + * This is an ISO FS API Function + */ +void FS_Close(LoadStackEntry* fd) { + printf("[OVERLORD] FS Close %s\n", fd->fr->name); + + // close the FD + fd->fr = nullptr; + read_in_progress = false; +} + +/*! + * Begin reading! Returns FS_READ_OK on success (always) + * This is an ISO FS API Function + * + * Idea: do the fopen in FS_Open and keep the file open? It would be faster. + */ +uint32_t FS_BeginRead(LoadStackEntry* fd, void* buffer, int32_t len) { + assert(fd->fr->location < fake_iso_entry_count); + + int32_t real_size = len; + if (len < 0) { + // not sure what this is about... + printf("[OVERLORD ISO CD] negative length warning!\n"); + real_size = len + 0x7ff; + } + + u32 sectors = real_size / SECTOR_SIZE; + real_size = sectors * SECTOR_SIZE; + u32 offset_into_file = SECTOR_SIZE * fd->location; + + const char* path = get_file_path(fd->fr); + FILE* fp = fopen(path, "rb"); + assert(fp); + fseek(fp, 0, SEEK_END); + uint32_t file_len = ftell(fp); + rewind(fp); + + if (offset_into_file < file_len) { + if (offset_into_file) { + fseek(fp, offset_into_file, SEEK_SET); + } + + if (offset_into_file + real_size > file_len) { + real_size = (file_len - offset_into_file); + } + + if (fread(buffer, real_size, 1, fp) != 1) { + assert(false); + } + } + + if (len < 0) { + len = len + 0x7ff; + } + + fd->location += (len / SECTOR_SIZE); + read_in_progress = true; + + return CMD_STATUS_IN_PROGRESS; +} + +/*! + * Block until read completes. + */ +uint32_t FS_SyncRead() { + // FS_BeginRead is blocking, so this is useless. + if(read_in_progress) { + read_in_progress = false; + return CMD_STATUS_IN_PROGRESS; + } else { + return CMD_STATUS_READ_ERR; + } +} + +/*! + * Poll drive + */ +void FS_PollDrive() {} + +// TODO FS_LoadMusic +uint32_t FS_LoadMusic(char* name, void* buffer) { + (void)name; + (void)buffer; + assert(false); +} + +// TODO FS_LoadSoundBank +uint32_t FS_LoadSoundBank(char* name, void* buffer) { + (void)name; + (void)buffer; + assert(false); +} \ No newline at end of file diff --git a/game/overlord/fake_iso.h b/game/overlord/fake_iso.h new file mode 100644 index 0000000000..fcd7013106 --- /dev/null +++ b/game/overlord/fake_iso.h @@ -0,0 +1,20 @@ +/*! + * @file fake_iso.h + * This provides an implementation of IsoFs for reading a "fake iso". + * A "fake iso" is just a map file which maps 8.3 ISO file names to files in the source folder. + * This way we don't need to actually create an ISO. + * + * The game has this compilation unit, but there is nothing in it. Probably it is removed to save + * IOP memory and was only included on TOOL-only builds. So this is my interpretation of how it + * should work. + */ + +#ifndef JAK_V2_FAKE_ISO_H +#define JAK_V2_FAKE_ISO_H + +#include "isocommon.h" + +void fake_iso_init_globals(); +extern IsoFs fake_iso; + +#endif //JAK_V2_FAKE_ISO_H diff --git a/game/overlord/iso.cpp b/game/overlord/iso.cpp new file mode 100644 index 0000000000..e482a174d7 --- /dev/null +++ b/game/overlord/iso.cpp @@ -0,0 +1,926 @@ +/*! + * @file iso.cpp + * CD/DVD Reading. + * This is a huge mess + */ + +#include +#include +#include +#include "iso.h" +#include "iso_cd.h" +#include "iso_queue.h" +#include "iso_api.h" +#include "game/sce/iop.h" +#include "stream.h" +#include "dma.h" +#include "fake_iso.h" +#include "game/common/dgo_rpc_types.h" + +using namespace iop; + +u32 ISOThread(); +u32 DGOThread(); +u32 ProcessVAGData(IsoMessage* _cmd, IsoBufferHeader* buffer_header); +u32 RunDGOStateMachine(IsoMessage* _cmd, IsoBufferHeader* buffer_header); +u32 CopyDataToEE(IsoMessage* _cmd, IsoBufferHeader* buffer_header); +u32 CopyDataToIOP(IsoMessage* _cmd, IsoBufferHeader* buffer_header); +u32 NullCallback(IsoMessage* _cmd, IsoBufferHeader* buffer_header); + +constexpr int VAGDIR_SIZE = 0x28b4; +constexpr int LOADING_SCREEN_SIZE = 0x800000; +constexpr u32 LOADING_SCREEN_DEST_ADDR = 0x1000000; + +IsoFs* isofs; +u32 iso_init_flag; +s32 sync_mbx; +s32 iso_mbx; +s32 dgo_mbx; +s32 iso_thread; +s32 dgo_thread; +s32 str_thread; +s32 play_thread; +u8 gVagDir[VAGDIR_SIZE]; +u32 gPlayPos; +RPC_Dgo_Cmd sRPCBuff[1]; // todo move... +DgoCommand scmd; + +void iso_init_globals() { + isofs = nullptr; + iso_init_flag = 0; + sync_mbx = 0; + iso_mbx = 0; + dgo_mbx = 0; + iso_thread = 0; + dgo_thread = 0; + str_thread = 0; + play_thread = 0; + memset(gVagDir, 0, sizeof(gVagDir)); + gPlayPos = 0; + memset(sRPCBuff, 0, sizeof(sRPCBuff)); + memset(&scmd, 0, sizeof(DgoCommand)); +} + +/*! + * Initialize the ISO Driver. + * Requires a buffer large enough to hold 3 sector (or 4 if you have DUP files) + */ +void InitDriver(u8* buffer) { + MsgPacket msg_packet; + + if (!isofs->init(buffer)) { + // succesful init! + iso_init_flag = 0; + } + + // you idiots, you're giving the kernel a pointer to a stack variable! + // (this is fixed in Jak 1 Japan and NTSC Greatest Hits) + SendMbx(sync_mbx, &msg_packet); +} + +/*! + * Does the messagebox have a message in it? + */ +u32 LookMbx(s32 mbx) { + MsgPacket* msg_packet; + return PollMbx((&msg_packet), mbx) != KE_MBOX_NOMSG; +} + +/*! + * Wait for a messagebox to have a message. This is inefficient and polls with a 100 us wait. + * This is stupid because the IOP does have much better syncronization primitives so you don't have + * to do this. + */ +void WaitMbx(s32 mbx) { + while (!LookMbx(mbx)) { + DelayThread(100); + } +} + +/*! + * Initialize the ISO FileSystem system. + * Returns 0 on success. + */ +u32 InitISOFS(const char* fs_mode, const char* loading_screen) { + // in retail: + // isofs = &iso_cd; + + // ADDED + if (!strcmp(fs_mode, "iso_cd")) { + isofs = &iso_cd_; + } else if (!strcmp(fs_mode, "fakeiso")) { + isofs = &fake_iso; + } else { + printf("[OVERLORD ISO] ISOFS has unknown fs_mode %s\n", fs_mode); + } + // END ADDED + + // mark us as NOT initialized. + iso_init_flag = 1; + + // TODO ADD + // while(!DMA_SendToSPUAndSync(&VAG_SilentLoop, 0x30, gTrapSRAM)) { + // DelayThread(1000); + // } + + // INITIALIZE MESSAGE BOXES + MbxParam mbx_param; + mbx_param.attr = 0; + mbx_param.option = 0; + iso_mbx = CreateMbx(&mbx_param); + if (iso_mbx <= 0) { + return 1; + } + + mbx_param.attr = 0; + mbx_param.option = 0; + dgo_mbx = CreateMbx(&mbx_param); + if (dgo_mbx <= 0) { + return 1; + } + + mbx_param.attr = 0; + mbx_param.option = 0; + sync_mbx = CreateMbx(&mbx_param); + if (sync_mbx <= 0) { + return 1; + } + + // INITIALIZE THREADS + ThreadParam thread_param; + thread_param.attr = TH_C; + thread_param.initPriority = 100; + thread_param.stackSize = 0x1000; + thread_param.option = 0; + thread_param.entry = (void*)ISOThread; + strcpy(thread_param.name, "ISOThread"); + iso_thread = CreateThread(&thread_param); + if (iso_thread <= 0) { + return 1; + } + + thread_param.attr = TH_C; + thread_param.initPriority = 98; + thread_param.stackSize = 0x800; + thread_param.option = 0; + thread_param.entry = (void*)DGOThread; + strcpy(thread_param.name, "DGOThread"); + dgo_thread = CreateThread(&thread_param); + if (dgo_thread <= 0) { + return 1; + } + + // thread_param.attr = TH_C; + // thread_param.initPriority = 97; + // thread_param.stackSize = 0x800; + // thread_param.option = 0; + // thread_param.entry = (void*)STRThread; + // strcpy(thread_param.name, "STRThread"); + // str_thread = CreateThread(&thread_param); + // if(str_thread <= 0) { + // return 1; + // } + // + // thread_param.attr = TH_C; + // thread_param.initPriority = 97; + // thread_param.stackSize = 0x800; + // thread_param.option = 0; + // thread_param.entry = (void*)PLAYThread; + // strcpy(thread_param.name, "PLAYThread"); + // play_thread = CreateThread(&thread_param); + // if(play_thread <= 0) { + // return 1; + // } + + // Start the threads! + StartThread(iso_thread, 0); + StartThread(dgo_thread, 0); + // StartThread(str_thread, 0); + // StartThread(play_thread, 0); + + // wait for ISO Thread to initialize + WaitMbx(sync_mbx); + + // LOAD VAGDIR file + FileRecord* vagdir_file = FindISOFile("VAGDIR.AYB"); + if (vagdir_file) { + LoadISOFileToIOP(vagdir_file, gVagDir, VAGDIR_SIZE); + } + FileRecord* loading_screen_file = FindISOFile(loading_screen); + if (loading_screen_file) { + LoadISOFileToEE(loading_screen_file, LOADING_SCREEN_DEST_ADDR, LOADING_SCREEN_SIZE); + } + + // should be set by ISOThread to 0 before the WaitMbx(sync_mbx); + return iso_init_flag; +} + +/*! + * Find a file by name. Return nullptr if it fails. + */ +FileRecord* FindISOFile(const char* name) { + return isofs->find(name); +} + +/*! + * Get the length of an ISO File by FileRecord + */ +u32 GetISOFileLength(FileRecord* f) { + return isofs->get_length(f); +} + +struct VagDirEntry { + union { + char name[8]; + s32 name_as_s32s[2]; + }; + + u32 unknown; +}; +static_assert(sizeof(VagDirEntry) == 12, "bad size of VagDirEntry"); + +/*! + * Find VAG file by "name", where name is 8 bytes (chars with spaces at the end, treated as two + * s32's). Returns pointer to name in the VAGDIR file data. + */ +VagDirEntry* FindVAGFile(s32* name) { + // First 4 bytes of VAGDIR file are the number of entries. + // Next is a list of entries. + VagDirEntry* entry = (VagDirEntry*)(gVagDir + 4); + + // loop over entries + for (s32 idx = 0; idx < *(s32*)gVagDir; idx++) { + // check if matching name + if (entry->name_as_s32s[0] == name[0] && entry->name_as_s32s[1] == name[1]) { + return entry; + } + entry++; + } + return nullptr; +} + +/*! + * The CD/DVD Reading Thread. This is a mess. + */ +u32 ISOThread() { + // Initialize! + InitBuffers(); + auto temp_buffer = AllocateBuffer(BUFFER_PAGE_SIZE); + InitDriver(temp_buffer->get_data()); // unblocks InitISOFS's WaitMbx + FreeBuffer(temp_buffer); + + // main CD/DVD read loop + for (;;) { + ///////////////////////////////////// + // Receive Messages and Add to Queue + ///////////////////////////////////// + + // receive a message + IsoMessage* msg_from_mbx; + IsoCommandLoadSingle* load_single_cmd; + s32 mbx_status = PollMbx((MsgPacket**)(&msg_from_mbx), iso_mbx); + load_single_cmd = (IsoCommandLoadSingle*)msg_from_mbx; + + if (mbx_status == 0) { + // we got a new message! + + // initialize fields of the message + msg_from_mbx->callback_buffer = nullptr; + msg_from_mbx->ready_for_data = 1; + msg_from_mbx->callback_function = NullCallback; + msg_from_mbx->fd = nullptr; + + if (msg_from_mbx->cmd_id == LOAD_TO_EE_CMD_ID || msg_from_mbx->cmd_id == LOAD_TO_IOP_CMD_ID || + msg_from_mbx->cmd_id == LOAD_TO_EE_OFFSET_CMD_ID) { + // A Simple File Load, add it to the queue + if (QueueMessage(msg_from_mbx, 2, "LoadSingle")) { + // if queued successfully, start by opening the file: + if (load_single_cmd->cmd_id == LOAD_TO_EE_OFFSET_CMD_ID) { + load_single_cmd->fd = + isofs->open(load_single_cmd->file_record, load_single_cmd->offset); + } else { + // open takes -1 as "no offset", same as 0. + load_single_cmd->fd = isofs->open(load_single_cmd->file_record, -1); + } + + // Check to see if it opened correctly: + if (!load_single_cmd->fd) { + // nope, set the status to indicate we failed + load_single_cmd->status = CMD_STATUS_FAILED_TO_OPEN; + // remove us from the queue... + UnqueueMessage(load_single_cmd); + // and wake up whoever requested this. + ReturnMessage(load_single_cmd); + } else { + // yep, opened correctly. Set up the pointers/sizes + load_single_cmd->dst_ptr = load_single_cmd->dest_addr; + load_single_cmd->bytes_done = 0; + // by default, copy size is the full file. + load_single_cmd->length_to_copy = isofs->get_length(load_single_cmd->file_record); + + if (load_single_cmd->length_to_copy == 0) { + // if we get zero for some reason, use the commanded length. + assert(false); + load_single_cmd->length_to_copy = load_single_cmd->length; + } else if (load_single_cmd->length < load_single_cmd->length_to_copy) { + // if we ask for less than the full length, use the smaller value. + load_single_cmd->length_to_copy = load_single_cmd->length; + } + + // set status and callback function. + load_single_cmd->status = CMD_STATUS_IN_PROGRESS; + switch (msg_from_mbx->cmd_id) { + case LOAD_TO_EE_CMD_ID: + case LOAD_TO_EE_OFFSET_CMD_ID: + msg_from_mbx->callback_function = CopyDataToEE; + break; + case LOAD_TO_IOP_CMD_ID: + msg_from_mbx->callback_function = CopyDataToIOP; + break; + } + } + } + } else if (msg_from_mbx->cmd_id == LOAD_DGO_CMD_ID) { + // Got a DGO command. There is one LoadDGO command for the entire DGO. + if (QueueMessage(msg_from_mbx, 0, "LoadDGO")) { + // queued successfully, open the file. + load_single_cmd->fd = isofs->open(load_single_cmd->file_record, -1); + if (!load_single_cmd->fd) { + // failed to open, return error + load_single_cmd->status = CMD_STATUS_FAILED_TO_OPEN; + UnqueueMessage(load_single_cmd); + ReturnMessage(load_single_cmd); + } else { + // init DGO state machine and register as the callback. + load_single_cmd->status = CMD_STATUS_IN_PROGRESS; + ((DgoCommand*)load_single_cmd)->dgo_state = DgoState::Init; + load_single_cmd->callback_function = RunDGOStateMachine; + } + } + } else { + printf("[OVERLORD] Unknown ISOThread message id 0x%x\n", msg_from_mbx->cmd_id); + } + + // TODO magic number + } else if (mbx_status == -0x1a9) { + return 0; + } + + //////////////////////////// + // Handle Sound (TODO) + //////////////////////////// + + //////////////////////////// + // Begin a read + //////////////////////////// + + IsoBufferHeader* read_buffer = nullptr; + IsoMessage* cmd_to_process = GetMessage(); + if (cmd_to_process) { // okay, there's a command queued that we should process + // prep for a read !! DANGER !! - this read _may_ complete after the command is done. + // At this point we don't know if the command actually needs another read or not! + if (cmd_to_process->callback_function == ProcessVAGData) { + read_buffer = AllocateBuffer(STR_BUFFER_DATA_SIZE); + } else { + read_buffer = AllocateBuffer(BUFFER_PAGE_SIZE); + } + + if (!read_buffer) { + // there aren't enough buffers. give up on this command for now. + cmd_to_process = nullptr; + } else { + // kick off read + if (cmd_to_process->callback_function == ProcessVAGData) { + cmd_to_process->status = + isofs->begin_read(cmd_to_process->fd, read_buffer->get_data(), STR_BUFFER_DATA_SIZE); + } else { + cmd_to_process->status = + isofs->begin_read(cmd_to_process->fd, read_buffer->get_data(), BUFFER_PAGE_SIZE); + } + + // if we have bad status, kill read buffer + if (cmd_to_process->status != CMD_STATUS_IN_PROGRESS) { + FreeBuffer(read_buffer); + read_buffer = nullptr; + cmd_to_process = nullptr; + } + } + } + + if (!cmd_to_process) { + // drive is doing nothing, make sure the DVD is still in there. + isofs->poll_drive(); + } + + // Deal with completed reads. NOTE - this can close files and terminate return commands! + ProcessMessageData(); + + if (!read_buffer) { + // didn't actually start a read, just delay for a bit I guess. + DelayThread(100); + } else { + // attempt to sync read. If we closed the file mid-read in ProcessMessageData, this returns + // an error code. + u32 read_status = isofs->sync_read(); + if (read_status == CMD_STATUS_READ_ERR) { + // closed file mid-read, or the read failed. Either way we can't give this read buffer to + // anybody, so we should just free it. + FreeBuffer(read_buffer); + } else { + // read is good! + cmd_to_process->status = read_status; + // setup the buffer for the callback. + if (cmd_to_process->callback_function == ProcessVAGData) { + read_buffer->data = read_buffer->get_data(); + read_buffer->data_size = STR_BUFFER_DATA_SIZE; + } else { + read_buffer->data = read_buffer->get_data(); + read_buffer->data_size = BUFFER_PAGE_SIZE; + } + + // add buffer to linked list of buffers. + if (!cmd_to_process->callback_buffer) { + cmd_to_process->callback_buffer = read_buffer; + } else { + auto* bh = cmd_to_process->callback_buffer; + while (bh->next) { + bh = (IsoBufferHeader*)bh->next; + } + bh->next = read_buffer; + } + } + } + } // for + return 0; +} + +/*! + * Handler for DGO data buffers. + */ +u32 RunDGOStateMachine(IsoMessage* _cmd, IsoBufferHeader* buffer) { + auto* cmd = (DgoCommand*)_cmd; + u32 return_value = CMD_STATUS_IN_PROGRESS; + u8* unprocessed_data = (u8*)buffer->data; + u32 bytes_left = buffer->data_size; + + // loop until we've read all the data + while (bytes_left) { + // printf("run DGO in state %d (%s) with %d unprocessed buffered bytes\n", cmd->dgoState, + // names[cmd->dgoState], buffer->data_size); + switch (cmd->dgo_state) { + case DgoState::Init: // init + cmd->bytes_processed = 0; + // start by reading header. + cmd->dgo_state = DgoState::Read_Header; + cmd->finished_first_obj = 0; + cmd->want_abort = 0; + break; + + case DgoState::Read_Header: // read dgo header. If we are unlucky this crosses a boundary + // and we have to do this in two chunks + { + u32 bytes_to_read = sizeof(DgoHeader) - cmd->bytes_processed; + if (bytes_to_read > bytes_left) { + bytes_to_read = bytes_left; + } + + // copy to our local storage + memcpy((u8*)&cmd->dgo_header + cmd->bytes_processed, unprocessed_data, bytes_to_read); + unprocessed_data += bytes_to_read; + bytes_left -= bytes_to_read; + cmd->bytes_processed += bytes_to_read; + + // if we are done with header + if (cmd->bytes_processed == sizeof(DgoHeader)) { + printf("[Overlord DGO] Got DGO file header for %s with %d objects\n", + cmd->dgo_header.name, + cmd->dgo_header.object_count); // added + cmd->bytes_processed = 0; + cmd->objects_loaded = 0; + if (cmd->dgo_header.object_count == 1) { + // if there's only one object, load to top immediately + cmd->buffer_toggle = 0; + cmd->ee_destination_buffer = cmd->buffer_heaptop; + cmd->dgo_state = DgoState::Read_Obj_Header; + } else { + // otherwise load to buffer1 first. + cmd->buffer_toggle = 1; + cmd->ee_destination_buffer = cmd->buffer1; + cmd->dgo_state = DgoState::Read_Obj_Header; + } + } + } break; + + case DgoState::Finish_Obj: // we have reached the end of an object file! + { + // EE synchronization occurs here. + // we skip this if we're loading the first object so we can double buffer the + // linking/loading process and have two in flight at a time (one loading, other linking) + if (cmd->finished_first_obj) { + s32 isSync = LookMbx(sync_mbx); // did we get a "sync" message? + if (isSync) { + // if so, this means we got a CancelDGO or NextDGO + if (cmd->want_abort) { + // we got a CancelDGO. + cmd->dgo_state = DgoState::Finish_Dgo; + break; + } + } else { + // nope, ee isn't ready. bail and wait for next run. + goto cleanup_and_return; + } + } + + cmd->finished_first_obj = 1; + cmd->status = CMD_STATUS_IN_PROGRESS; + + // select a buffer for next time. + if (cmd->buffer_toggle == 1) { + cmd->selectedBuffer = cmd->buffer1; + } else { + cmd->selectedBuffer = cmd->buffer2; + } + + // we've processed the command, go wake up the DGO RPC thread. + // doesn't terminate the command (ReleaseMessage does this, ReturnMessage just + // wakes up the caller while keeping the command alive). + ReturnMessage(cmd); + + // toggle buffer + if (cmd->buffer_toggle == 1) { + cmd->ee_destination_buffer = cmd->buffer2; + cmd->buffer_toggle = 2; + } else { + cmd->ee_destination_buffer = cmd->buffer1; + cmd->buffer_toggle = 1; + } + + // setup for next run + if (cmd->objects_loaded + 1 == cmd->dgo_header.object_count) { + cmd->dgo_state = DgoState::Read_Last_Obj; + } else { + cmd->dgo_state = DgoState::Read_Obj_Header; + } + break; + } + + case DgoState::Read_Last_Obj: // setup load last + { + // extra sync here + s32 sync = LookMbx(sync_mbx); + if (sync) { + if (cmd->want_abort) { + cmd->dgo_state = DgoState::Finish_Dgo; + } else { + // EE ready, no abort. Nothing in flight, so we are safe to do a top load! + cmd->ee_destination_buffer = cmd->buffer_heaptop; + cmd->buffer_toggle = 0; + cmd->dgo_state = DgoState::Read_Obj_Header; + } + } else { + goto cleanup_and_return; + } + } break; + + case DgoState::Read_Obj_Header: // read object file header + { + u32 bytesToRead = sizeof(ObjectHeader) - cmd->bytes_processed; + if (bytes_left < bytesToRead) { + bytesToRead = bytes_left; + } + + // for now, buffer locally + memcpy((u8*)&cmd->objHeader + cmd->bytes_processed, unprocessed_data, bytesToRead); + unprocessed_data += bytesToRead; + bytes_left -= bytesToRead; + cmd->bytes_processed += bytesToRead; + + // once we're done, send the header to the EE, and start reading object data + if (cmd->bytes_processed == sizeof(ObjectHeader)) { + printf("[Overlord DGO] Got object header for %s, object size 0x%x bytes (sent to 0x%p)\n", + cmd->objHeader.name, cmd->objHeader.size, cmd->ee_destination_buffer); + DMA_SendToEE(&cmd->objHeader, sizeof(ObjectHeader), cmd->ee_destination_buffer); + DMA_Sync(); + cmd->ee_destination_buffer += sizeof(ObjectHeader); + cmd->objHeader.size = (cmd->objHeader.size + 0xf) & 0xfffffff0; + cmd->dgo_state = DgoState::Read_Obj_data; + cmd->bytes_processed = 0; + } + } break; + + case DgoState::Read_Obj_data: // read object file data + { + u32 bytesToRead = cmd->objHeader.size - cmd->bytes_processed; + if (bytes_left < bytesToRead) { + bytesToRead = bytes_left; + } + + // send contents directly to EE + DMA_SendToEE(unprocessed_data, bytesToRead, cmd->ee_destination_buffer); + DMA_Sync(); + unprocessed_data += bytesToRead; + bytes_left -= bytesToRead; + cmd->ee_destination_buffer += bytesToRead; + cmd->bytes_processed += bytesToRead; + + if (cmd->bytes_processed == cmd->objHeader.size) { + cmd->objects_loaded++; + if (cmd->objects_loaded == cmd->dgo_header.object_count) { + cmd->dgo_state = DgoState::Finish_Dgo; + } else { + cmd->dgo_state = DgoState::Finish_Obj; + cmd->bytes_processed = 0; + } + } + } break; + + case DgoState::Finish_Dgo: { + // done with buffer, complete. Kill the ISO thread read. + return_value = CMD_STATUS_DONE; + goto cleanup_and_return; + } + + default: + printf("unknown dgoState!\n"); + } + } + + printf("[DGO State Machine Complete] Out of things to read!\n"); + +cleanup_and_return: + if (return_value == 0) { + buffer->data = nullptr; + buffer->data_size = 0; + } else { + if (!bytes_left) { + buffer->data = nullptr; + buffer->data_size = 0; + } else { + buffer->data = unprocessed_data; + buffer->data_size = bytes_left; + } + } + return return_value; +} + +/*! + * Callback for sending to EE. + */ +u32 CopyDataToEE(IsoMessage* _cmd, IsoBufferHeader* buffer_header) { + auto* cmd = (IsoCommandLoadSingle*)_cmd; + + s32 bytes_to_send = cmd->length_to_copy - cmd->bytes_done; + + // make sure we don't copy too much (if the buffer does not have enough data) + if (buffer_header->data_size < (u32)bytes_to_send) { + bytes_to_send = (s32)buffer_header->data_size; + } + + DMA_SendToEE(buffer_header->get_data(), bytes_to_send, cmd->dest_addr); + DMA_Sync(); + + cmd->dest_addr += bytes_to_send; + cmd->bytes_done += bytes_to_send; + buffer_header->data = nullptr; + buffer_header->data_size = 0; + if (cmd->bytes_done == cmd->length_to_copy) { + return CMD_STATUS_DONE; + } else { + return CMD_STATUS_IN_PROGRESS; + } +} + +/*! + * Callback for loading to IOP buffer. + */ +u32 CopyDataToIOP(IsoMessage* _cmd, IsoBufferHeader* buffer_header) { + auto* cmd = (IsoCommandLoadSingle*)_cmd; + + s32 bytes_to_send = cmd->length_to_copy - cmd->bytes_done; + + // make sure we don't copy too much (if the buffer does not have enough data) + if (buffer_header->data_size < (u32)bytes_to_send) { + bytes_to_send = (s32)buffer_header->data_size; + } + + memcpy(cmd->dst_ptr, buffer_header->get_data(), bytes_to_send); + + cmd->dest_addr += bytes_to_send; + cmd->bytes_done += bytes_to_send; + buffer_header->data = nullptr; + buffer_header->data_size = 0; + if (cmd->bytes_done == cmd->length_to_copy) { + return CMD_STATUS_DONE; + } else { + return CMD_STATUS_IN_PROGRESS; + } +} + +/*! + * Callback which does nothing. + */ +u32 NullCallback(IsoMessage* _cmd, IsoBufferHeader* buffer_header) { + (void)_cmd; + buffer_header->data_size = 0; + return CMD_STATUS_NULL_CB; +} + +/*! + * Initialize a VagCommand. + */ +void InitVAGCmd(VagCommand* cmd, u32 x) { + cmd->field_0x30 = 0; + cmd->field_0x34 = 0; + cmd->field_0x38 = 0; + cmd->field_0x3c = x; + cmd->field_0x40 = 0; + cmd->field_0x44 = 0; + cmd->field_0x48 = 0xffffffff; + gPlayPos = 0x30; + cmd->messagebox_to_reply = 0; + cmd->thread_id = 0; +} + +/*! + * Byte-swap. + */ +u32 bswap(u32 in) { + return ((in >> 0x18) & 0xff) | ((in >> 8) & 0xff00) | ((in & 0xff00) << 8) | (in << 0x18); +} + +/*! + * TODO - implement. + */ +u32 ProcessVAGData(IsoMessage* _cmd, IsoBufferHeader* buffer_header) { + (void)_cmd; + (void)buffer_header; + assert(false); + return 0; +} + +// TODO - StopVAG +// TODO - PauseVAG +// TODO - CalculateVAGVolumes +// TODO - UnpauseVAG +// TODO - SetVAGVol +// TODO - GetPlayPos +// TODO - UpdatePlayPos +// TODO - CheckVAGStreamProgress + + +void* RPC_DGO(unsigned int fno, void* _cmd, int y); +void LoadDGO(RPC_Dgo_Cmd* cmd); +void LoadNextDGO(RPC_Dgo_Cmd* cmd); +void CancelDGO(RPC_Dgo_Cmd* cmd); + +/*! + * DGO RPC Thread. + */ +u32 DGOThread() { + sceSifQueueData dq; + sceSifServeData serve; + + // setup RPC. + CpuDisableIntr(); + sceSifInitRpc(0); + sceSifSetRpcQueue(&dq, GetThreadId()); + sceSifRegisterRpc(&serve, DGO_RPC_ID, RPC_DGO, sRPCBuff, nullptr, nullptr, &dq); + CpuEnableIntr(); + sceSifRpcLoop(&dq); + return 0; +} + +/*! + * DGO RPC Handler. + */ +void* RPC_DGO(unsigned int fno, void* _cmd, int y) { + (void)y; + auto* cmd = (RPC_Dgo_Cmd*)_cmd; + // call appropriate handler. + switch (fno) { + case DGO_RPC_LOAD_FNO: + LoadDGO(cmd); + break; + case DGO_RPC_LOAD_NEXT_FNO: + LoadNextDGO(cmd); + break; + case DGO_RPC_CANCEL_FNO: + CancelDGO(cmd); + break; + default: + cmd->result = DGO_RPC_RESULT_ERROR; + } + return cmd; +} + +/*! + * Begin loading a DGO. Returns when the first obj is loaded. + * Then will load the next obj into the second buffer. + * Then the DGO loader will block until LoadNextDGO is called. + * This approach keeps two loads in flight at a time to increase loading throughput. + * One load will be read from DVD / DMA'd to EE + * Another will be linked on the EE. + * The final load is done directly onto the heap, and isn't double buffered + * (otherwise the linking object could allocate on the heap where the final loading object is + * being copied). This avoids having to relocate the data from the temporary load buffer to the + * heap, and is the only way to make sure that the entire heap can be filled. + */ +void LoadDGO(RPC_Dgo_Cmd* cmd) { + // Find the file + FileRecord* fr = isofs->find(cmd->name); + if (!fr) { + cmd->result = DGO_RPC_RESULT_ERROR; + return; + } + + // cancel an in progress command and wait for it to end. + // note - this doesn't handle a nullptr correctly, so if this actually ends up cancelling + // it will crash. + CancelDGO(nullptr); + + // set up the ISO Command + scmd.cmd_id = LOAD_DGO_CMD_ID; + scmd.messagebox_to_reply = dgo_mbx; + scmd.thread_id = 0; + scmd.buffer1 = (u8*)(u64)(cmd->buffer1); + scmd.buffer2 = (u8*)(u64)(cmd->buffer2); + scmd.buffer_heaptop = (u8*)(u64)(cmd->buffer_heap_top); + scmd.fr = fr; + + // send the command to ISO Thread + SendMbx(iso_mbx, &scmd); + + // wait for the ReturnMessage in the DGO callback state machine. + // this happens when the first file is loaded + WaitMbx(dgo_mbx); + + if (scmd.status == CMD_STATUS_IN_PROGRESS) { + // we got one, but there's more to load. + // we don't set cmd->buffer1 as it's already the correct buffer in this case - + // when there are >1 objs, we load into buffer1 first. + cmd->result = DGO_RPC_RESULT_MORE; + } else if (scmd.status == CMD_STATUS_DONE) { + // all done! make sure our reply says we loaded to the top. + cmd->result = DGO_RPC_RESULT_DONE; + cmd->buffer1 = cmd->buffer_heap_top; + scmd.cmd_id = 0; + } else { + // error. + cmd->result = DGO_RPC_RESULT_ERROR; + scmd.cmd_id = 0; + } +} + +/*! + * Signal to the IOP it can keep loading and overwrite the oldest obj buffer. + * This will return when there's another loaded obj. + */ +void LoadNextDGO(RPC_Dgo_Cmd* cmd) { + if (scmd.cmd_id == 0) { + // something went wrong. + cmd->result = DGO_RPC_RESULT_ERROR; + } else { + // update heap location + scmd.buffer_heaptop = (u8*)(u64)cmd->buffer_heap_top; + // allow DGO state machine to advance + SendMbx(sync_mbx, nullptr); + // wait for another load to finish. + WaitMbx(dgo_mbx); + // another load finished, respond with the result. + if (scmd.status == CMD_STATUS_IN_PROGRESS) { + // more, use the selected buffer. + cmd->result = DGO_RPC_RESULT_MORE; + cmd->buffer1 = (u32)(u64)scmd.selectedBuffer; + } else if (scmd.status == CMD_STATUS_DONE) { + // last obj, always loaded to top. + cmd->result = DGO_RPC_RESULT_DONE; + cmd->buffer1 = cmd->buffer_heap_top; + scmd.cmd_id = 0; + } else { + cmd->result = DGO_RPC_RESULT_ERROR; + scmd.cmd_id = 0; + } + } +} + +/*! + * Abort an in progress load. + */ +void CancelDGO(RPC_Dgo_Cmd* cmd) { + if (scmd.cmd_id) { + scmd.want_abort = 1; + // wake up DGO state machine with abort + SendMbx(sync_mbx, nullptr); + // wait for it to abort. + WaitMbx(dgo_mbx); + assert(cmd); // bug + cmd->result = DGO_RPC_RESULT_ABORTED; + scmd.cmd_id = 0; + } +} + +// TODO - GetVAGStreamPos +// TODO - VAG_MarkLoopStart +// TODO - VAG_MarkLoopEnd +// TODO - VAG_MarkNonloopStart +// TODO - VAG_MarkNonloopEnd \ No newline at end of file diff --git a/game/overlord/iso.h b/game/overlord/iso.h new file mode 100644 index 0000000000..08477dbcbe --- /dev/null +++ b/game/overlord/iso.h @@ -0,0 +1,18 @@ +/*! + * @file iso.h + * CD/DVD Reading. + * This is a huge mess + */ + +#ifndef JAK_V2_ISO_H +#define JAK_V2_ISO_H + +#include "common/common_types.h" +#include "isocommon.h" + +void iso_init_globals(); +FileRecord* FindISOFile(const char* name); +u32 GetISOFileLength(FileRecord* f); +u32 InitISOFS(const char* fs_mode, const char* loading_screen); + +#endif // JAK_V2_ISO_H diff --git a/game/overlord/iso_api.cpp b/game/overlord/iso_api.cpp new file mode 100644 index 0000000000..654e6a943b --- /dev/null +++ b/game/overlord/iso_api.cpp @@ -0,0 +1,44 @@ +#include "iso_api.h" +#include "game/sce/iop.h" + +using namespace iop; + +/*! + * Load a File to IOP memory (blocking) + */ +void LoadISOFileToIOP(FileRecord *file, void *addr, uint32_t length) { + printf("[OVERLORD] LoadISOFileToIOP %s, %d/%d bytes\n", file->name, length, file->size); + IsoCommandLoadSingle cmd; + cmd.cmd_id = LOAD_TO_IOP_CMD_ID; + cmd.messagebox_to_reply = 0; + cmd.thread_id = GetThreadId(); + cmd.file_record = file; + cmd.dest_addr = (u8*)addr; + cmd.length = length; + SendMbx(iso_mbx, &cmd); + SleepThread(); + + if(cmd.status) { + cmd.length_to_copy = 0; + } +} + +/*! + * Load a File to IOP memory (blocking) + */ +void LoadISOFileToEE(FileRecord *file, uint32_t addr, uint32_t length) { + printf("[OVERLORD] LoadISOFileToEE %s, %d/%d bytes\n", file->name, length, file->size); + IsoCommandLoadSingle cmd; + cmd.cmd_id = LOAD_TO_EE_CMD_ID; + cmd.messagebox_to_reply = 0; + cmd.thread_id = GetThreadId(); + cmd.file_record = file; + cmd.dest_addr = (u8*)(u64)addr; + cmd.length = length; + SendMbx(iso_mbx, &cmd); + SleepThread(); + + if(cmd.status) { + cmd.length_to_copy = 0; + } +} \ No newline at end of file diff --git a/game/overlord/iso_api.h b/game/overlord/iso_api.h new file mode 100644 index 0000000000..0b07591e4a --- /dev/null +++ b/game/overlord/iso_api.h @@ -0,0 +1,8 @@ +#ifndef JAK_V2_ISO_API_H +#define JAK_V2_ISO_API_H +#include "isocommon.h" + +void LoadISOFileToIOP(FileRecord *file, void *addr, uint32_t length); +void LoadISOFileToEE(FileRecord *file, uint32_t ee_addr, uint32_t length); + +#endif //JAK_V2_ISO_API_H diff --git a/game/overlord/iso_cd.cpp b/game/overlord/iso_cd.cpp new file mode 100644 index 0000000000..c8a8dcd861 --- /dev/null +++ b/game/overlord/iso_cd.cpp @@ -0,0 +1,985 @@ +/*! + * @file iso_cd.cpp + * IsoFs API for accessing the CD/DVD drive. + */ + +#include +#include "game/sce/iop.h" +#include "game/sce/stubs.h" +#include "iso_cd.h" +#include "isocommon.h" +#include "overlord.h" +#include "soundcommon.h" +#include "srpc.h" + +// iso_cd is an implementation of the IsoFs API for loading files from a CD/DVD with an ISO and/or +// DUP filesystem. +// The DUP filesystem is a custom Naughty Dog filesystem which attempts to hide +// files. The DUP filesystem also stores all files twice on the disk and will try reading from the +// other copy if it reading the first copy encounters errors. The DUP filesystem is unused. + +using namespace iop; +typedef int (*mmode_func)(int); + +// Drive State +// sector to read from (for DUP files, sector of the first copy of the file) +u32 _sector; +// number of sectors to read +u32 _sectors; +// number of retries in the current read +u32 _retries; +// buffer to read into +void* _buffer; +// set to 0 or 1 to indicate if the first or second copy of DUP files should be used. +uint32_t _dupseg; +// the actual sector to read from (differs from _sector when reading second copy of DUP file) +uint32_t _real_sector; +// set 1 if the current read was continuous from the previous read (didn't require a seek) +uint32_t _continuous; +// time when the current read was started +SysClock _starttime; +// time when the current read has ended +SysClock _endtime; + +// Globals +u32 gDirtyCd; // set when we're waiting on a read which has errors +u32 gNoCD; // set when we believe the game disc has been removed. +static u32 sNumFiles; // number of files (includes both ISO and DUP files) +static u32 sArea1; // Sector where the first copy of DUP files live. +static u32 sAreaDiff; // Sectors in between the first and second copy of files. + +u32 pirated; // do we think the game is pirated? +mmode_func cdmmode = nullptr; // function to call to set the expected media (CD/DVD) +static sceCdRMode sNominalMode; // drive settings for "nominal" reading +static sceCdRMode sStreamMode; // drive settings for "streaming" reading +static sceCdRMode* sMode; // pointer to currently selected read mode +LoadStackEntry* sReadInfo; // LoadStackEntry for currently reading file +static u8* sSecBuffer[3]; // Buffers for a single sector +u32 add_files; // Should we add files we discover to the sFiles list? +static FileRecord sFiles[MAX_ISO_FILES]; // Info for all files on the disc +u32 CD_ID_SectorNum; // Sector of the DISK.ID file +s32 CD_ID_Sector[SECTOR_SIZE / 4]; // Contents of the DISK.ID file +s32 CD_ID_SectorSum; // Sum of the CD_ID_SECTOR array +LoadStackEntry sLoadStack[MAX_OPEN_FILES]; // List of all files that are "open" +static u32 sound_bank_loads; // might be a static variable in a function? +IsoFs iso_cd_; // IsoFs function pointers + +constexpr int TIME_SIZE = 16; // how many samples for read timing +s32 _times[TIME_SIZE]; // read timing data +s32 _timesix; +s32 _tsamps[2]; +s32 _tkps[2]; + +s32 gLastSpeed; +s32 gDiskSpeed[2]; +s32 gDupSeg; + +u32 ReadU32(u8* buffer); +u32 ReadSectorsNow(uint32_t sector, uint32_t len, void* buffer); +u32 ReadDirectory(uint32_t sector, uint32_t size, uint32_t secBufID); +void DecodeDUP(u8* buffer); +void LoadMusicTweaks(u8* buffer); +void LoadDiscID(); +u32 CheckDiscID(); +void SetRealSector(); +void CD_WaitReturn(); + +static int FS_Init(u8* buffer); +static FileRecord* FS_Find(const char* name); +static FileRecord* FS_FindIN(const char* iso_name); +static uint32_t FS_GetLength(FileRecord* fr); +static LoadStackEntry* FS_Open(FileRecord* fr, int32_t offset); +static LoadStackEntry* FS_OpenWad(FileRecord* fr, int32_t offset); +static void FS_Close(LoadStackEntry* fd); +static uint32_t FS_BeginRead(LoadStackEntry* fd, void* buffer, int32_t len); +static uint32_t FS_SyncRead(); +static uint32_t FS_LoadSoundBank(char*, void*); +static uint32_t FS_LoadMusic(char*, void*); +static void FS_PollDrive(); + +void iso_cd_init_globals() { + _sector = 0; + _sectors = 0; + _retries = 0; + gDirtyCd = 0; + gNoCD = 0; + _dupseg = 0; + _real_sector = 0; + _continuous = 0; + _buffer = nullptr; + memset(&_starttime, 0, sizeof(SysClock)); + memset(&_endtime, 0, sizeof(SysClock)); + + sNumFiles = 0; + sArea1 = 0; + sAreaDiff = 0; + + pirated = 0; + cdmmode = nullptr; + + sNominalMode.trycount = 0; + sNominalMode.spindlctrl = 1; + sNominalMode.datapattern = 0; + sNominalMode.pad = 0; + + sStreamMode.trycount = 0xf; + sStreamMode.spindlctrl = 0; + sStreamMode.datapattern = 0; + sStreamMode.pad = 0; + + sMode = &sStreamMode; + sReadInfo = nullptr; + + memset(sSecBuffer, 0, sizeof(sSecBuffer)); + add_files = 0; + memset(sFiles, 0, sizeof(sFiles)); + + CD_ID_SectorNum = 0; + memset(CD_ID_Sector, 0, sizeof(CD_ID_Sector)); + CD_ID_SectorSum = 0; + memset(sLoadStack, 0, sizeof(sLoadStack)); + sound_bank_loads = 0; + + iso_cd_.init = FS_Init; + iso_cd_.find = FS_Find; + iso_cd_.find_in = FS_FindIN; + iso_cd_.get_length = FS_GetLength; + iso_cd_.open = FS_Open; + iso_cd_.open_wad = FS_OpenWad; + iso_cd_.close = FS_Close; + iso_cd_.begin_read = FS_BeginRead; + iso_cd_.sync_read = FS_SyncRead; + iso_cd_.load_sound_bank = FS_LoadSoundBank; + iso_cd_.load_music = FS_LoadMusic; + iso_cd_.poll_drive = FS_PollDrive; + + memset(_times, 0, sizeof(_times)); + memset(_tsamps, 0, sizeof(_tsamps)); + memset(_tkps, 0, sizeof(_tkps)); + _timesix = 0; + + memset(gDiskSpeed, 0, sizeof(gDiskSpeed)); + gLastSpeed = 0; + gDupSeg = 0; +} + +/*! + * Read unaligned uint32_t from buffer. ISO file systems may have 32-bit values that aren't word + * aligned. DONE, EXACT + */ +uint32_t ReadU32(u8* data) { + return (uint32_t)data[0] + (((uint32_t)data[1]) * 0x100) + (((uint32_t)data[2]) * 0x10000) + + (((uint32_t)data[3]) * 0x1000000); +} + +/*! + * Read from disc, immediately (blocking), into a local buffer. Will retry if needed, setting + * gDirtyCd. This does not use the DUP file system or drive state, so this should not be used + * outside of initialization. Will clear gDirtyCd on successful read. Returns 1 on success, 0 on + * sceCdRead failure, or otherwise retries forever until sceCdGetError is OK. + * The length is in terms of sectors. + * DONE, EXACT + */ +u32 ReadSectorsNow(uint32_t sector, uint32_t len, void* buffer) { + // reset the sector state to break any continuous reads in progress + _sector = 0; + + // retry loop for read + while (true) { + // Start async read from DVD... + if (sceCdRead(sector, len, buffer, sMode) == 0) { + // if this fails, it indicates catastrophic failure of the DVD drive, so give up immediately. + return 0; + } + + // Wait for read to finish (0x00 is blocking) + sceCdSync(0); + + // check for error + if (sceCdGetError() == 0) { + // no error, we are good! + break; + } + // we got an error. Try again, and set a dirty flag so the EE knows we're having trouble + gDirtyCd = 1; + } + + // success! clear the dirty flag and return! + gDirtyCd = 0; + return 1; +} + +/*! + * Read ISO file systems directory tree, adding files to the filerecord table if add_files is set. + * Regardless of add_files is not set, it will be set for folders under NAUGHTY.DOG. + * This feature is used on demos with multiple games and Jak is in the NAUGHTY.DOG folder. + * Recursively walks the tree. + * There is a stack of single sector buffers used to recursively read the directories. + * Returns 1 on success and 0 on failure. + * Running out of sector buffers because of too many nested folders is considered success? + * DONE + */ +u32 ReadDirectory(uint32_t sector, uint32_t size, uint32_t secBufID) { + if (secBufID < 3) { + // grab our buffer from the stack + u8* buffer = sSecBuffer[secBufID]; + + uint32_t lsector = sector; + int32_t lsize = size; + + // loop over sector reads + while (lsize > 0) { + // ISO low-level read + if (!ReadSectorsNow(lsector, 1, buffer)) { + printf("[OVERLORD ISO CD] Failed to read sector in ReadDirectory\n"); + return 0; + } + u8* lbuffer = buffer; + + // loop over stuff in the sector + while ((*lbuffer != 0) && (lbuffer < buffer + SECTOR_SIZE)) { + u8 dir_record_size = *lbuffer; + if ((lbuffer[0x21] != 0) && (lbuffer[0x21] != 1)) { // skip over whatever these things are + + uint32_t extent = ReadU32(lbuffer + 2); + uint32_t dir_size = ReadU32(lbuffer + 10); + uint32_t name_len = lbuffer[0x20]; + + bool is_directory = true; + if ((lbuffer[0x1f + name_len] == ';') && (lbuffer[0x20 + name_len] == '1')) { + is_directory = false; + } + + if (is_directory) { + if (!add_files) { + // don't add file by default, but add files if we recurse in the NAUGHTY.DOG folder + if (!memcmp(lbuffer + 0x21, "NAUGHTY.DOG", 0xb)) { + add_files = true; + ReadDirectory(extent, dir_size, secBufID + 1); + add_files = false; + } + } else { + // otherwise just recurse + ReadDirectory(extent, dir_size, secBufID + 1); + } + } else { + if (sNumFiles == MAX_ISO_FILES) { + printf("[OVERLORD ISO CD] There are too many files on the disc!\n"); + return 0; + } + + if (add_files) { + lbuffer[0x1f + name_len] = 0; // null terminate the name + MakeISOName(sFiles[sNumFiles].name, (char*)(lbuffer + 0x21)); + sFiles[sNumFiles].location = extent; + sFiles[sNumFiles].size = dir_size; + sNumFiles++; + } + } + } + lbuffer += dir_record_size; + } + lsector++; + lsize -= 0x800; + } + } else { + printf("[OVERLORD ISO CD] ReadDirectory ran out of sector buffers!\n"); + } + return 1; +} + +struct DupIndexEntry { + // 20 bytes + char name[12]; + u32 location; + u32 size; +}; + +/*! + * DUP code. The DUP files aren't on the disc, so this doesn't do anything. + * This would allow for hidden files that aren't in the standard ISO format, presumably to make + * pirating harder? The DUP files store the location of these hidden files. Also it support having + * two copies of some files. There are two areas, both of which are identical. But it was never + * used. + */ +void DecodeDUP(u8* buffer) { + (void)buffer; + + // set sArea1 to point to an impossibly large sector - if DUP initialization fails this means no + // file will be in the DUP zones. + sArea1 = 0x7fffffff; + + char iso_name[16]; + // all three of these will fail. + MakeISOName(iso_name, "Z1INDEX.DUP"); + FileRecord* index_file = FS_FindIN(iso_name); + MakeISOName(iso_name, "Z3AREA1.DUP"); + FileRecord* area1_file = FS_FindIN(iso_name); + MakeISOName(iso_name, "Z5AREA2.DUP"); + FileRecord* area2_file = FS_FindIN(iso_name); + + // Note - this reads 4 sectors, but the buffer only has enough room for 3 sectors. + // So this code would likely cause a crash if it was run. + // Maybe this is why it was removed? + // Or maybe there used to be 4 init buffers, but one was removed once they gave up on DUP? + if (index_file && area1_file && area2_file && ReadSectorsNow(index_file->location, 4, buffer)) { + sArea1 = area1_file->location; // marks start of 1st zone + sAreaDiff = area2_file->location - area1_file->location; // difference between zones + + // make sure we have enough room to store all entries + if (sNumFiles + *(s32*)(buffer) <= MAX_ISO_FILES) { + // read entries + DupIndexEntry* dup_entries = (DupIndexEntry*)(((u8*)buffer) + 4); + for (int i = 0; i < *(s32*)(buffer); i++) { + *(s32*)(&sFiles[sNumFiles].name) = *(s32*)(&dup_entries[i].name); + *(s32*)(&sFiles[sNumFiles].name + 4) = *(s32*)(&dup_entries[i].name + 4); + *(s32*)(&sFiles[sNumFiles].name + 8) = *(s32*)(&dup_entries[i].name + 8); + sFiles[sNumFiles].size = dup_entries[i].size; + sFiles[sNumFiles].location = dup_entries[i].location; + sNumFiles++; + } + } + } +} + +/*! + * Load the TWEAKVAL.MUS file into the gMusicTweakInfo file. + * Only works if the file is less than 1 sector long. + * If loading fails, writes a 0 to the first 32-bits of gMusicTweakInfo + * @param buffer a sector buffer which will be used + */ +void LoadMusicTweaks(u8* buffer) { + char iso_name[16]; + MakeISOName(iso_name, "TWEAKVAL.MUS"); + FileRecord* fr = FS_FindIN(iso_name); + if (!fr || !ReadSectorsNow(fr->location, 1, buffer)) { + *(s32*)gMusicTweakInfo = 0; + printf("[OVERLORD ISO CD] Failed to load music tweaks!\n"); + } else { + memcpy(gMusicTweakInfo, buffer, MUSIC_TWEAK_SIZE); + } +} + +/*! + * Load the DISK ID file and compute the sum. + * This is used as a checksum to make sure the disc is correct. + * A literal sum is not a great checksum + * Also, this function name and the file on the disc itself spell dis{c,k} differently. + * + * If there is no DISK_ID.DIZ file, uses whatever is stored at 0x400 instead. + */ +void LoadDiscID() { + char iso_name[16]; + MakeISOName(iso_name, "DISK_ID.DIZ"); + FileRecord* fr = FS_FindIN(iso_name); + if (!fr) { + printf( + "[OVERLORD ISO CD] LoadDiscID failed to find DISK_ID.DIZ, using sector 0x400 instead!\n"); + CD_ID_SectorNum = 0x400; + } else { + CD_ID_SectorNum = fr->location; + } + + ReadSectorsNow(CD_ID_SectorNum, 1, &CD_ID_Sector); + CD_ID_SectorSum = 0; + for (uint32_t i = 0; i < SECTOR_SIZE / 4; i++) { + CD_ID_SectorSum += CD_ID_Sector[i]; + } + printf("[OVERLORD] DISK_ID.DIZ OK 0x%x\n", CD_ID_SectorSum); +} + +/*! + * Verify that the DISK ID file has not changed. Returns 1 if it is good. + */ +u32 CheckDiskID() { + if (ReadSectorsNow(CD_ID_SectorNum, 1, CD_ID_Sector) == 0) { + // failed to read CD ID data + return 0; + } + + int sum = 0; + for (uint32_t i = 0; i < SECTOR_SIZE / 4; i++) { + sum += CD_ID_Sector[i]; + } + return sum == CD_ID_SectorSum; +} + +/*! + * Set _real_sector in preparation for a read, based on the requested _sector. + * This has logic for a system which has a double copy of some data on the disc and can pick between + * two different copies. This selection is done with the dupseg flag. + */ +void SetRealSector() { + // if we are below sArea1, it's not a duplicated file, so ignore the dupseg flag and read directly + if (_sector < sArea1 || _dupseg == 0) { + _real_sector = _sector; + } else { + // it's a duplicated file, and duplicate read is enabled, so get the area 2 sector. + _real_sector = _sector + sAreaDiff; + printf("[OVERLORD] Warning, adjusting real sector in SetRealSector\n"); + } + + // we suspect the game is pirated, load the wrong sector. + if (pirated) { + _real_sector += 3; + printf("pirated!\n"); // added, so I don't trip this by accident! + } +} + +/*! + * Initialize the ISO CD system and builds file record table. + * This is an ISO_FS API Function. + * Also loads music tweaks/DISK ID + * @param buffer : a buffer larger enough to hold 3 sectors + * this buffer can be freed immediately this returns + * Return 0 on success. + */ +int FS_Init(u8* buffer) { + // determine disk type + int disk_type = SCECdDETCT; + while (disk_type = sceCdGetDiskType(), disk_type == SCECdDETCT) { + // This SleepThread will cause the Overlord initialization to lock up. It's called with an + // argument of 10000, but SleepThread accepts no arguments. Probably they meant to call + // DelayThread. It ends up working because the drive already knows the disk type at this point. + SleepThread(); + } + + // what is this. it's crazy. why? + if (disk_type <= SCECdPS2DVD || disk_type < SCECdCDDA || disk_type <= SCECdDVDV || + disk_type != SCECdIllegalMedia) { + // we are actually using the CD drive, so set the mmode function to the SCE function. + // This is called in FS_LoadMusic. If you call this with the wrong media type, it locks up. + // I guess this is an attempt at making convoluted anti-piracy code so it's harder to find + // calls to sceCdMmode with static analysis. But they left in debug symbols and the variable + // is called "cdmmode", which is not a very sneaky way to hide it! (At least on the EE it's + // called aybabtu and is a GOAL symbol which is way harder to figure out.) Also it seems like + // the primary mode of piracy they were concerned with is somebody swapping a DVD with a CD? + cdmmode = sceCdMmode; + + // verify the disc is a DVD. + sceCdMmode(SCECdDVD); + + // set up sector buffers used for initialization reads. + for (int i = 0; i < 3; i++) { + sSecBuffer[i] = buffer + i * SECTOR_SIZE; + } + + // read primary volume descriptor into buffer + if (!ReadSectorsNow(0x10, 1, sSecBuffer[0])) { + printf("[OVERLORD ISO CD] Failed to read primary volume descriptor\n"); + return 1; + } + + // check volume descriptor identifier + if (memcmp(sSecBuffer[0] + 1, "CD001", 5)) { + printf("[OVERLORD ISO CD] Got the wrong volume descriptor identifier\n"); + char* cptr = (char*)sSecBuffer[0] + 1; + printf("%c%c%c%c%c\n", cptr[0], cptr[1], cptr[2], cptr[3], cptr[4]); + return 1; + } + + // read path table into buffer + uint32_t path_table_sector = ReadU32(sSecBuffer[0] + 0x8c); + + if (!ReadSectorsNow(path_table_sector, 1, sSecBuffer[0])) { + printf("[OVERLORD ISO CD] Failed to read path table\n"); + return 1; + } + + // read path table's extent into buffer + uint32_t path_table_extent = ReadU32(sSecBuffer[0] + 2); + + if (!ReadSectorsNow(path_table_extent, 1, sSecBuffer[0])) { + printf("[OVERLORD ISO CD] Failed to read path table extent\n"); + } + + // read root directory + add_files = true; + uint32_t dir_size = ReadU32(sSecBuffer[0] + 10); + if (!ReadDirectory(path_table_extent, dir_size, 0)) { + printf("[OVERLORD ISO CD] Failed to ReadDirectory\n"); + return 1; + } + + // load filesystem stuff + DecodeDUP(sSecBuffer[0]); + LoadMusicTweaks(sSecBuffer[0]); + LoadDiscID(); + + // there's some sort of weird loop here over all file that does nothing. + // my guess is its some commented out print thing? + + // empty load stack + for (int i = 0; i < MAX_OPEN_FILES; i++) { + sLoadStack[i].fr = nullptr; + } + + // kill sector buffers + for (int i = 0; i < 3; i++) { + sSecBuffer[i] = nullptr; + } + return 0; + } else { + printf("[OVERLORD ISO CD] Bad Media Type\n"); + return 1; + } +} + +/*! + * Find a file on the disc and return a FileRecord. + * This is an ISO FS API Function + */ +FileRecord* FS_Find(const char* name) { + char name_buff[16]; + MakeISOName(name_buff, name); + return FS_FindIN(name_buff); +} + +/*! + * Find a file on the disc. Uses the ISO name of the file. + * This can be generated with MakeISOFile + * This is an ISO FS API Function + * There is a weird anti-piracy thing in here to prevent people from making copies with less than + * 1 GB of data? I guess you could remove the audio in languages you don't care about and put in + * on a CD, and this would block this from happening. + */ +FileRecord* FS_FindIN(const char* iso_name) { + const uint32_t* buff = (const uint32_t*)iso_name; + for (;;) { // this loop will spin forever if you have < 1 GB of files + uint32_t size = 0; // total sum of file sizes + uint32_t count = 0; + while (count < sNumFiles) { + const uint32_t* ref = (uint32_t*)sFiles[count].name; + if (ref[0] == buff[0] && ref[1] == buff[1] && ref[2] == buff[2]) { + return sFiles + count; + } + size += sFiles[count].size; + count++; + } + // if we get here, we haven't found the file, we should return 0 to indicate we don't have it + // however, if we haven't found 1 GB of files after searching the whole thing + // we assume that we've pirated the game and should continue looping + // Note that the game attempts to load DUP files which will fails and will hit this condition. + buff += + 3; // to make this look less suspicious, lets increment buff. also will crash eventually. + if (0x3fffffff < size) { + return nullptr; // we got 1 GB of files, okay to return + } + + // we didn't get 1 GB of files, you're a pirate. + printf("pirated!\n"); // i added this so i know if it hangs here + } +} + +/*! + * Determine the length of a file. + * This is an ISO FS API Function + */ +uint32_t FS_GetLength(FileRecord* fr) { + return fr->size; +} + +/*! + * Open a file by putting it on the load stack. + * Set the offset to 0 or -1 if you do not want to have an offset. + * This is an ISO FS API Function + */ +LoadStackEntry* FS_Open(FileRecord* fr, int32_t offset) { + printf("[OVERLORD] FS Open %s\n", fr->name); // Added + LoadStackEntry* selected = nullptr; + // find first unused spot on load stack. + for (uint32_t i = 0; i < MAX_OPEN_FILES; i++) { + if (!sLoadStack[i].fr) { + selected = sLoadStack + i; + selected->fr = fr; + selected->location = fr->location; + if (offset != -1) { + selected->location += offset; + } + return selected; + } + } + printf("[OVERLORD ISO CD] Failed to FS_Open %s\n", fr->name); + ExitIOP(); + return nullptr; +} + +/*! + * Open a file by putting it on the load stack. + * Like Open, but allows an offset of -1 to be applied. + * This is an ISO FS API Function + */ +LoadStackEntry* FS_OpenWad(FileRecord* fr, int32_t offset) { + printf("[OVERLORD] FS Open %s\n", fr->name); + LoadStackEntry* selected = nullptr; + for (uint32_t i = 0; i < MAX_OPEN_FILES; i++) { + if (!sLoadStack[i].fr) { + selected = sLoadStack + i; + selected->fr = fr; + selected->location = fr->location + offset; + return selected; + } + } + printf("[OVERLORD ISO CD] Failed to FS_OpenWad %s\n", fr->name); + ExitIOP(); + return nullptr; +} + +/*! + * Close an open file. + * This is an ISO FS API Function + */ +void FS_Close(LoadStackEntry* fd) { + printf("[OVERLORD] FS Close %s\n", fd->fr->name); + if (fd == sReadInfo) { + // the file is currently being read, so lets try to finish out the read, if possible. + int count = 0; + + // the non-blocking sync, so we don't get stuck here on a catastrophic error. + while (sceCdSync(1)) { + DelayThread(1000); // wait 1 ms and allow other stuff to run. + count++; + if (count == 1000) { // waited too long to close this file + sceCdBreak(); // interrupt the read + break; + } + } + sReadInfo = nullptr; + } + + // close the FD + fd->fr = nullptr; +} + +/*! + * Begin reading! Returns FS_READ_OK on success (always) + * This is an ISO FS API Function + */ +uint32_t FS_BeginRead(LoadStackEntry* fd, void* buffer, int32_t len) { + // set the reading state: + // I guess continuous stream buffer reads don't count as continuous? + _continuous = (len == BUFFER_PAGE_SIZE) && (fd->location == (_sector + _sectors)); + _sector = fd->location; + int32_t real_size = len; + if (len < 0) { + // not sure what this is about... + printf("[OVERLORD ISO CD] negative length warning!\n"); + real_size = len + 0x7ff; + } + _sectors = real_size >> 11; + _retries = 0; + _buffer = buffer; + GetSystemTime(&_starttime); + + // compute _real_sector + SetRealSector(); + + while (!sceCdRead(_real_sector, _sectors, _buffer, sMode)) { + // error starting the read. this is bad and possibly indicates somebody took the CD out. + // lets wait for the CD to be ready again... + CD_WaitReturn(); + _retries++; + if (_sector >= sArea1) { + // the original file we tried to read is duplicated... + // so lets try reading the other copy of it! + _dupseg = 1 - _dupseg; + _continuous = 0; // mark as noncontinuous read + SetRealSector(); // recompute! + } + } + + // ??? this is strangely set up. + if (len < 0) { + len = len + 0x7ff; + } + + fd->location += (len >> 0xb); + + // set sReadInfo to point to the current read. + sReadInfo = fd; + return CMD_STATUS_IN_PROGRESS; +} + +/*! + * Wait for current read to complete! + * This is an ISO FS API Function + * @return + */ +uint32_t FS_SyncRead() { + // make sure a read is actually in progress + if (!sReadInfo) { + return CMD_STATUS_READ_ERR; + } + + // remember when we start doing a SyncRead. + SysClock now; + GetSystemTime(&now); + + // block and wait for completion + sceCdSync(0); + // remember when we sync. + GetSystemTime(&_endtime); + + // Loop to check if read succeed and start an additional read if not. + while (sceCdGetError()) { + // no, it didn't, lets retry + _retries++; + // toggle dupseg + if (_sector >= sArea1) { + _dupseg = 1 - _dupseg; + _continuous = 0; + SetRealSector(); + } + + // try until a read starts... + while (!sceCdRead(_real_sector, _sectors, _buffer, sMode)) { + // read start failed, possibly CD is removed + CD_WaitReturn(); + // retry! + _retries++; + // toggle dupseg if possible + if (_sector >= sArea1) { + _dupseg = 1 - _dupseg; + _continuous = 0; + SetRealSector(); + } + } + + // read has started + // set dirty cd to indicate we had trouble + gDirtyCd = 1; + // wait for read to finish... + sceCdSync(0); + // if the read/sync fails, the loop will go again. + } + + // Read complete! Mark CD as not dirty and clear active read! + gDirtyCd = 0; + sReadInfo = nullptr; + + // Optionally do some timing checks + // (note that these never run because we don't have DUP files) + // continuous read with no failures from dup zone + // more than half the time spent in FS_SyncRead + // Basically it tries to learn about which segment does worse in "too slow" reads + // by averaging all historical too slow reads. Once the other is winning by a certain amount + // it will swap. It will also swap if there isn't enough samples on one. + if (_retries == 0 && _continuous && _sectors >= sArea1 && + (_endtime.hi - _starttime.hi) / 2 < (_endtime.hi - now.hi)) { + // record the time + _times[_timesix++] = _endtime.hi - _starttime.hi; + + // if we filled the time buffer + if (_timesix == TIME_SIZE) { + // compute total time + s32 total_time = 0; + for (s32 i = 0; i < TIME_SIZE; i++) { + total_time += _times[i]; + } + + // determine read speed + gLastSpeed = 0x69780000 / (total_time >> 4); // todo - work out this constant + + // add to average kps for this seg + if (_tsamps[_dupseg] < 0x40000) { + _tkps[_dupseg] = _tkps[_dupseg] + gLastSpeed; + _tsamps[_dupseg] = _tsamps[_dupseg]; + } + + // average speed of this segment + gDiskSpeed[_dupseg] = _tkps[_dupseg] / _tsamps[_dupseg]; + + _timesix = 0; + + if (_tsamps[0] < 8) { + // not much information about segment 0 + if (_dupseg) { + // and we aren't reading segment 0... + // so let's read segment 0 + _dupseg = 0; + _sector = 0; + } + } else { + // got enough info about segment 0. + if (_tsamps[1] < 8) { + // not enough information about segment 1 + if (!_dupseg) { + // and not reading, so lets read it. + _dupseg = 1; + _sector = 0; + } + } else { + // enough info about both. + if ((_tkps[1] / _tsamps[1] + 0x32) < (_tkps[0] / _tsamps[0])) { + // section 0 wins by at least 0x32, lets use it if we aren't already + if (_dupseg) { + _dupseg = 0; + _sector = 0; + } + } else if ((_tkps[0] / _tsamps[0] + 0x32) < (_tkps[1] / _tsamps[1])) { + if (!_dupseg) { + _dupseg = 1; + _sector = 0; + } + } + } + } + // set our current decision in a global for the EE to read. + gDupSeg = _dupseg; + } + } + return CMD_STATUS_IN_PROGRESS; +} + +/*! + * Load a SoundBank now. Doesn't do any fancy read stuff. + */ +uint32_t FS_LoadSoundBank(char* name, void* buffer) { + char full_name[32]; // may actually be 8, but lets be safe + + // ??? todo this is probably a field of the buffer. + u32 header_size; + if (*(s32*)(((u8*)buffer) + 0x14) == 0x65) { + header_size = 1; + } else { + header_size = 10; + } + + if (strlen(name) > 16) { + printf("[OVERLORD ISO CD] FS_LoadSoundBank has an invalid name!\n"); + } + + // append .sbk + strcpy(full_name, name); + strcat(full_name, ".sbk"); + + FileRecord* fr = FS_Find(full_name); + if (!fr) { + printf("[OVERLORD ISO CD] FS_LoadSoundBank cannot find bank %s, loading empty instead.\n", + full_name); + fr = FS_Find("empty1.sbk"); + } + + // hack to do a read now (the Sound Bank loads bypass all the other fancy loading stuff evidently) + _sector = fr->location; + SetRealSector(); + + // loop until we read header successfully. + // don't set retries or dirty cd + while (!ReadSectorsNow(_real_sector, header_size, buffer)) { + // ReadSectorsNow will only return if the read fails to start. in this case we assume the disc + // was removed: + CD_WaitReturn(); + // we don't increment retries... + if (_sector >= sArea1) { + _dupseg = 1 - _dupseg; + _continuous = 0; + SetRealSector(); + } + } + + // now have the sound library do a load. + // (this time we set dirty cd if it fails, but no retries) + auto load_status = snd_BankLoadByLoc(_real_sector + header_size, 0); + while (!load_status && snd_GetLastLoadError() < 0x100) { + CD_WaitReturn(); + if (_sector >= sArea1) { + _dupseg = 1 - _dupseg; + _continuous = 0; + SetRealSector(); + } + load_status = snd_BankLoadByLoc(_real_sector + header_size, 0); + if (!load_status) { + gDirtyCd = 1; + } + } + gDirtyCd = 0; + + // pirate check sometimes + sound_bank_loads++; + if ((sound_bank_loads & 7) == 0) { + pirated = 1; + // check that one file is past sector 0x80000 (approx 1 GB) + for (u32 i = 0; i < sNumFiles; i++) { + if (sFiles[i].location + (sFiles[i].size >> 11) > 0x80000) { + pirated = 0; + } + } + } + + snd_ResolveBankXREFS(); + PrintBankInfo(buffer); + _sector = 0; + // ??? todo this is probably a field of the buffer. + *(s32*)(((u8*)buffer) + 0x10) = load_status; + return 0; +} + +/*! + * Load a music file. Load now, doesn't do fancy reading stuff. + */ +uint32_t FS_LoadMusic(char* name, void* buffer) { + char full_name[32]; // may actually be 8, but lets be safe + if (strlen(name) > 16) { + printf("[OVERLORD ISO CD] FS_LoadMusic has an invalid name!\n"); + } + + // append .mus + strcpy(full_name, name); + strcat(full_name, ".mus"); + + FileRecord* fr = FS_Find(full_name); + if (!fr) { + printf("[OVERLORD ISO CD] FS_LoadMusic cannot find bank %s.\n", full_name); + return 6; + } + + _sector = fr->location; + SetRealSector(); + // another "piracy" check to make sure the media is the correct type... + (*cdmmode)(SCECdDVD); + + // now have the sound library do a load. + auto load_status = snd_BankLoadByLoc(_real_sector, 0); + // TODO magic constant 0x100 + while (!load_status && snd_GetLastLoadError() < 0x100) { + CD_WaitReturn(); + if (_sector >= sArea1) { + _dupseg = 1 - _dupseg; + _continuous = 0; + SetRealSector(); + } + load_status = snd_BankLoadByLoc(_real_sector, 0); + if (!load_status) { + gDirtyCd = 1; + } + } + gDirtyCd = 0; + snd_ResolveBankXREFS(); + _sector = 0; + *(s32*)buffer = load_status; + return 0; +} + +/*! + * Make sure the drive is happy. + * NOTE - only call this when the drive should have nothing to do! + */ +void FS_PollDrive() { + if (sceCdDiskReady(1) == SCECdNotReady) { // non-blocking + CD_WaitReturn(); + } +} + +/*! + * Wait for the game CD/DVD to be put back in the playstation. + * Only call this if you think the CD/DVD has been removed, as requires a seek. + */ +void CD_WaitReturn() { + gNoCD = 1; + do { + while (sceCdDiskReady(1) == SCECdNotReady) { + } + } while (!CheckDiskID()); + gNoCD = 0; +} \ No newline at end of file diff --git a/game/overlord/iso_cd.h b/game/overlord/iso_cd.h new file mode 100644 index 0000000000..99a3e9b26e --- /dev/null +++ b/game/overlord/iso_cd.h @@ -0,0 +1,15 @@ +/*! + * @file iso_cd.cpp + * IsoFs API for accessing the CD/DVD drive. + */ + +#ifndef JAK_ISO_CD_H +#define JAK_ISO_CD_H + +#include "common/common_types.h" +#include "iso.h" + +void iso_cd_init_globals(); +extern IsoFs iso_cd_; + +#endif // JAK_ISO_CD_H diff --git a/game/overlord/iso_queue.cpp b/game/overlord/iso_queue.cpp new file mode 100644 index 0000000000..a2aa7acc00 --- /dev/null +++ b/game/overlord/iso_queue.cpp @@ -0,0 +1,377 @@ +#include +#include +#include +#include "game/sce/iop.h" +#include "iso_queue.h" +#include "isocommon.h" + +using namespace iop; + +constexpr int N_BUFFERS = 4; +constexpr int N_STR_BUFFERS = 1; +constexpr int N_VAG_CMDS = 64; + +struct IsoBuffer { + IsoBufferHeader header; + u8 data[BUFFER_PAGE_SIZE]; +}; + +struct IsoStrBuffer { + IsoBufferHeader header; + u8 data[STR_BUFFER_DATA_SIZE]; +}; + + +static IsoBuffer sBuffer[N_BUFFERS]; +static IsoStrBuffer sStrBuffer[N_STR_BUFFERS]; +static IsoBuffer* sFreeBuffer; +static IsoStrBuffer* sFreeStrBuffer; +PriStackEntry gPriStack[N_PRIORITIES]; + +u32 vag_cmd_cnt; +u32 vag_cmd_used; +u32 max_vag_cmd_cnt; +VagCommand vag_cmds[N_VAG_CMDS]; + +static s32 sSema; + +IsoBufferHeader* TryAllocateBuffer(uint32_t size); +void ReleaseMessage(IsoMessage *cmd); +void FreeVAGCommand(VagCommand* cmd); + +void iso_queue_init_globals() { + memset(sBuffer, 0, sizeof(sBuffer)); + memset(sStrBuffer, 0, sizeof(sStrBuffer)); + sFreeBuffer = nullptr; + sFreeStrBuffer = nullptr; + for(auto& e : gPriStack) e.reset(); + + vag_cmd_cnt = 0; + vag_cmd_used = 0; + max_vag_cmd_cnt = 0; + memset(vag_cmds, 0, sizeof(vag_cmds)); + sSema = 0; +} + +void PriStackEntry::reset() { + for(auto& c : cmds) c = nullptr; + n = 0; + for(auto& x : names) x.clear(); +} + + +void InitBuffers() { + + // chain all buffers together and set them as free. + for(uint32_t i = 0; i < N_BUFFERS; i++) { + sBuffer[i].header.data = nullptr; + sBuffer[i].header.data_size = 0; + sBuffer[i].header.buffer_size = BUFFER_PAGE_SIZE; + sBuffer[i].header.next = &sBuffer[i+1].header; + } + sBuffer[N_BUFFERS - 1].header.next = nullptr; + sFreeBuffer = &sBuffer[0]; + + for(uint32_t i = 0; i < N_STR_BUFFERS; i++) { + sStrBuffer[i].header.data = nullptr; + sStrBuffer[i].header.data_size = 0; + sStrBuffer[i].header.buffer_size = STR_BUFFER_DATA_SIZE; + sStrBuffer[i].header.next = &sStrBuffer[i + 1].header; + } + sStrBuffer[N_STR_BUFFERS - 1].header.next = nullptr; + sFreeStrBuffer = &sStrBuffer[0]; + + // TODO - this has options + SemaParam params; + params.attr = 1; + params.max_count = 1; + params.option = 1; + params.init_count = 0; + sSema = CreateSema(¶ms); + + if(sSema < 0) { + for(;;) { + printf("[OVERLORD] VAG Semaphore creation failed!\n"); + } + } +} + +/*! + * Allocate a buffer of the given size. If not possible, loop forever. Size must be BUFFER_PAGE_SIZE or STR_BUFFER_DATA_SIZE, + */ +IsoBufferHeader* AllocateBuffer(uint32_t size) { + IsoBufferHeader *buffer = TryAllocateBuffer(size); + if(buffer) { + printf("--------------- allocated buffer size %d\n", size); + return buffer; + } else { + while (true) { + printf("[OVERLORD ISO QUEUE] Failed to allocate buffer!\n"); + } + } +} + + +/*! + * Allocate a buffer of given size. If the size isn't BUFFER_PAGE_SIZE, you get a streaming buffer (STR_BUFFER_DATA_SIZE). + * If no allocation can be done, return nullptr. + */ +IsoBufferHeader* TryAllocateBuffer(uint32_t size) { + IsoStrBuffer* top_str = sFreeStrBuffer; + IsoBuffer* top_buff = sFreeBuffer; + + if(size == BUFFER_PAGE_SIZE) { + if(sFreeBuffer) { + auto next = sFreeBuffer->header.next; + sFreeBuffer->header.data = nullptr; + sFreeBuffer = (IsoBuffer*)next; + top_buff->header.data_size = 0; + top_buff->header.next = nullptr; + return (IsoBufferHeader*)top_buff; + } + } else { + if(sFreeStrBuffer) { + auto next = sFreeStrBuffer->header.next; + sFreeStrBuffer->header.data = nullptr; + sFreeStrBuffer = (IsoStrBuffer*)next; + top_str->header.data_size = 0; + top_str->header.next = nullptr; + return (IsoBufferHeader*)top_str; + } + } + printf("[OVERLORD] Failed to allocate buffer (requested size 0x%x)\n", size); + return nullptr; +} + +/*! + * Return a buffer once you are done using it so somebody else can have a turn + */ +void FreeBuffer(IsoBufferHeader *buffer) { + IsoBufferHeader* b = (IsoBufferHeader*)buffer; + printf("--------------- free buffer size %d\n", b->buffer_size); + if(b->buffer_size == BUFFER_PAGE_SIZE) { + b->next = sFreeBuffer; + sFreeBuffer = (IsoBuffer*)b; + } else { + b->next = sFreeStrBuffer; + sFreeStrBuffer = (IsoStrBuffer*)b; + } +} + +/*! + * Display all messages in the priority stack + * The actual function does nothing. + */ +void DisplayQueue() { + for(int pri = 0; pri < N_PRIORITIES; pri++) { + for(int cmd = 0; cmd < (int)gPriStack[pri].n; cmd++) { + printf(" PRI %d elt %d %s\n", pri, cmd, gPriStack[pri].names[cmd].c_str()); + } + } +} + +/*! + * Add a message to the back of the queue for the given priority. + * If there is no room left in the queue, ReturnMessage with a CMD_STATUS_FAILED_TO_QUEUE. + * Return 1 on success. + */ +u32 QueueMessage(IsoMessage *cmd, int32_t priority, const char *name) { + u32 ok = gPriStack[priority].n != PRI_STACK_LENGTH; + if(ok) { + gPriStack[priority].cmds[gPriStack[priority].n] = cmd; + gPriStack[priority].names[gPriStack[priority].n] = name; + gPriStack[priority].n++; + printf("[OVERLORD] Queue %d (%d/%d), %s\n", priority, gPriStack[priority].n, PRI_STACK_LENGTH, gPriStack[priority].names[gPriStack[priority].n - 1].c_str()); + DisplayQueue(); + } else { + printf("[OVERLORD ISO QUEUE] Failed to queue!\n"); + cmd->status = CMD_STATUS_FAILED_TO_QUEUE; + ReturnMessage(cmd); + } + return ok; +} + +/*! + * Remove a message from the priority stack. + */ +void UnqueueMessage(IsoMessage *cmd) { + int pri = 0; + u32 idx = 0; + PriStackEntry* pse; + + // loop over priorities + for(pri = 0; pri < N_PRIORITIES; pri++) { + pse = gPriStack + pri; + + // loop over entries + for(idx = 0; idx < gPriStack[pri].n; idx++) { + if(pse->cmds[idx] == cmd) { + goto found; + } + } + } + printf("[OVERLORD ISO QUEUE] Failed to unqueue!\n"); + + found: + assert(gPriStack[pri].cmds[idx] == cmd); + + // pop + gPriStack[pri].n--; + // and move other entries up. + while(idx < gPriStack[pri].n) { + pse->cmds[idx] = pse->cmds[idx + 1]; + idx++; + } + DisplayQueue(); +} + +/*! + * Get the highest priority message with an open buffer. + * (Note - messages with priority less than max priority will be gotten if they have < 2 buffers filled) + * @return + */ +IsoMessage* GetMessage() { + // loop over all priorities + for(int pri = (N_PRIORITIES - 1); pri >= 0; pri--) { + auto pse = gPriStack + pri; + int idx = gPriStack[pri].n; + for(idx = idx - 1; idx >= 0; idx--) { + if(pse->cmds[idx]->fd && + pse->cmds[idx]->status == CMD_STATUS_IN_PROGRESS && + pse->cmds[idx]->ready_for_data) { + if(pri == N_PRIORITIES - 1) { + // return high priority commands only if they don't have any buffers filled + if(!pse->cmds[idx]->callback_buffer) { + return pse->cmds[idx]; + } + } else { + // return lower priority commands if they don't have 2 buffers filled. + if(!pse->cmds[idx]->callback_buffer || + !(IsoBufferHeader*)(pse->cmds[idx]->callback_buffer)->next) { + return pse->cmds[idx]; + } + } + } + } + + } + return nullptr; +} + +/*! + * Execute callbacks and maintain buffers for finished reads in the priority stack + */ +void ProcessMessageData() { + int32_t pri = N_PRIORITIES - 1; + + for (;;) { + if (pri < 0) return; + int32_t cmdID = gPriStack[pri].n; + IsoMessage *popped_command; + do { + cmdID--; + if (cmdID < 0) goto end_cur; + popped_command = gPriStack[pri].cmds[cmdID]; + auto* callback_buffer = popped_command->callback_buffer; + if(popped_command->status == CMD_STATUS_IN_PROGRESS && callback_buffer) { // if we have a callback buffer (meaning a read finished and let us know) + // execute the callback! + uint32_t callback_result = popped_command->callback_function(popped_command, callback_buffer); + popped_command->status = callback_result; +// printf("ProcessMessage Data set command %p status to %d\n", popped_command, popped_command->status); + // if we're done with the buffer, free it and load the next one (if there is one) + if(callback_buffer->data_size == 0) { + popped_command->callback_buffer = (IsoBufferHeader*)callback_buffer->next; + printf("free 1\n"); + FreeBuffer(callback_buffer); + } + } + } while (popped_command->status == CMD_STATUS_IN_PROGRESS); + ReleaseMessage(popped_command); + ReturnMessage(popped_command); + // return message todo this will free vag commands! + pri++; + end_cur: + pri--; + } +} + +/*! + * Wakeup thread/message mbx for a message + */ +void ReturnMessage(IsoMessage *cmd) { + if(!cmd->messagebox_to_reply) { + if(cmd->thread_id == 0) { + FreeVAGCommand((VagCommand*)cmd); + } else { + WakeupThread(cmd->thread_id); + } + } else { + SendMbx(cmd->messagebox_to_reply, (MsgPacket*)cmd); + } +} + +/*! + * Free buffers, close files, and remove from priority stack + */ +void ReleaseMessage(IsoMessage *cmd) { + // kill all buffers + while(cmd->callback_buffer) { + auto old_head = cmd->callback_buffer; + cmd->callback_buffer = (IsoBufferHeader*)old_head->next; + printf("free 2\n"); + FreeBuffer(old_head); + } + + // close file + if(cmd->fd) { + isofs->close(cmd->fd); + } + + // unqueue message + UnqueueMessage(cmd); +} + +// GetVAGCommand +VagCommand* GetVAGCommand() { + for(;;) { + // wait for command to be available + while(vag_cmd_cnt == (N_VAG_CMDS - 1)) { + DelayThread(100); + } + + // wait for VAG semaphore + while(WaitSema(sSema)) { + + } + // try to get something. + for(s32 i = 0; i < N_VAG_CMDS; i++) { + if(!((vag_cmd_used >> (i & 0x1f)) & 1)) { + // free! + vag_cmd_used |= (1 << (i & 0x1f)); + vag_cmd_cnt++; + if(vag_cmd_cnt > max_vag_cmd_cnt) { + max_vag_cmd_cnt = vag_cmd_cnt; + } + SignalSema(sSema); + return &vag_cmds[i]; + } + } + + SignalSema(sSema); + } +} + +void FreeVAGCommand(VagCommand* cmd) { + s32 idx = cmd - vag_cmds; + if(idx >= 0 && idx < N_VAG_CMDS && ((vag_cmd_used >> (idx & 0x1f)) & 1)) { + while(WaitSema(sSema)) { + + } + + vag_cmd_used &= ~(1 << (idx & 0x1f)); + vag_cmd_cnt--; + SignalSema(sSema); + } else { + printf("[OVERLORD] Invalid FreeVAGCommand!\n"); + } +} diff --git a/game/overlord/iso_queue.h b/game/overlord/iso_queue.h new file mode 100644 index 0000000000..79c245ad0c --- /dev/null +++ b/game/overlord/iso_queue.h @@ -0,0 +1,21 @@ +#ifndef JAK_V2_ISO_QUEUE_H +#define JAK_V2_ISO_QUEUE_H + + +#include "common/common_types.h" +#include "isocommon.h" + + + +void iso_queue_init_globals(); +void InitBuffers(); +IsoBufferHeader* AllocateBuffer(uint32_t size); +void FreeBuffer(IsoBufferHeader *buffer); +u32 QueueMessage(IsoMessage *cmd, int32_t priority, const char *name); +void UnqueueMessage(IsoMessage *cmd); +IsoMessage* GetMessage(); +void ProcessMessageData(); +void ReturnMessage(IsoMessage *cmd); + + +#endif //JAK_V2_ISO_QUEUE_H diff --git a/game/overlord/isocommon.cpp b/game/overlord/isocommon.cpp new file mode 100644 index 0000000000..01d7f046d5 --- /dev/null +++ b/game/overlord/isocommon.cpp @@ -0,0 +1,196 @@ +/*! + * @file isocommon.cpp + * Common ISO utilities. + */ + +#include +#include "common/common_types.h" +#include +#include "isocommon.h" + +/*! + * Convert file name to "ISO Name" + * ISO names are upper case and 12 bytes long. + * xxxxxxxxyyy0 + * + * x - uppercase letter of file name, or space + * y - uppercase letter of file extension, or space + * 0 - null terminator (\0, not the character zero) + */ +void MakeISOName(char* dst, const char* src) { + int i = 0; + const char* src_ptr = src; + char* dst_ptr = dst; + + // copy name and upper case + while ((i < 8) && (*src_ptr) && (*src_ptr != '.')) { + char c = *src_ptr; + src_ptr++; + if (('`' < c) && (c < '{')) { // lower case + c -= 0x20; + } + *dst_ptr = c; + dst_ptr++; + i++; + } + + // pad out name with spaces + while (i < 8) { + *dst_ptr = ' '; + dst_ptr++; + i++; + } + + // increment past period + if (*src_ptr == '.') + src_ptr++; + + // same for extension + while (i < 11 && (*src_ptr)) { + char c = *src_ptr; + src_ptr++; + if (('`' < c) && (c < '{')) { // lower case + c -= 0x20; + } + *dst_ptr = c; + dst_ptr++; + i++; + } + + while (i < 11) { + *dst_ptr = ' '; + dst_ptr++; + i++; + } + *dst_ptr = 0; +} + +/*! + * Unmakes an ISO name back to the original name. + * Keeps it upper case. + * Not used. + */ +void UnmakeISOName(char* dst, const char* src) { + int i = 0; + const char* src_ptr = src; + char* dst_ptr = dst; + + // copy non-space characters + while ((i < 8) && (*src != ' ')) { + *dst_ptr = *src_ptr; + src_ptr++; + dst_ptr++; + i++; + } + + // skip src to the extension + src_ptr += 8 - i; + + if (*src_ptr != ' ') { + // if there's an extension, add the period + *dst_ptr = '.'; + i = 0; + // copy extension + dst_ptr++; + while (i < 3 && *src_ptr != ' ') { + *dst_ptr = *src_ptr; + src_ptr++; + i++; + } + } + *dst_ptr = 0; +} + +/*! + * Convert an animation name to ISO name. + * The animation name is a bunch of dash separated words. + * The resulting ISO name has the same first two chars as the animation name, and one char from each + * remaining word. Once there are no more words but remaining chars in the ISO name, the ith extra + * char is the i+1 th char of the last word. A word ending in a number (or just a number) is turned + * into the number. The word "resolution" becomes z. The word "accept" becomes y. The word "reject" + * becomes n. Other words become the first char of the word. The result is uppercased and the file + * extension is STR Examples (animation name and disc file name, not ISO name): + * green-sagecage-outro-beat-boss-enough-cells -> GRSOBBEC.STR + * swamp-tetherrock-swamprockexplode-4 -> SWTS4.STR + * minershort-resolution-1-orbs -> MIZ1ORBS.STR + * @param dst + * @param src + */ +void ISONameFromAnimationName(char* dst, const char* src) { + // The Animation Name is a bunch of words separated by dashes + + // copy first two chars of the first word exactly + dst[0] = src[0]; + dst[1] = src[1]; + s32 i = 2; // 2 chars added to dst. + + // skip ahead to the first dash (or \0 if there's no dashes) + const char* src_ptr = src; + while (*src_ptr && *src_ptr != '-') { + src_ptr++; + } + + // the points to the next dash (or \0 if there's none). + const char* next_ptr = src_ptr; + if (*src_ptr) { + // loop over words (next_ptr points to dash before word, i counts chars in dest) + while (src_ptr = next_ptr + 1, i < 8) { + // scan next_ptr forward to next dash + next_ptr = src_ptr; + while (*next_ptr && *next_ptr != '-') { + next_ptr++; + } + + // there's no next word, so break (the current word will be handled there) + if (!*next_ptr) + break; + + // add a char for the current word: + char char_to_add; + if (next_ptr[-1] < '0' || next_ptr[-1] > '9') { + // word doesn't end in a number. + + // some special case words map to special letters (likely to avoid animation name conflicts) + if (next_ptr - src_ptr == 10 && !memcmp(src_ptr, "resolution", 10)) { + char_to_add = 'z'; + } else if (next_ptr - src_ptr == 6 && !memcmp(src_ptr, "accept", 6)) { + char_to_add = 'y'; + } else if (next_ptr - src_ptr == 6 && !memcmp(src_ptr, "reject", 6)) { + char_to_add = 'n'; + } else { + // not a special case, just take the first letter. + char_to_add = *src_ptr; + } + } else { + // the current word ends in a number, just use this number (I think usually the whole word + // is just a number) + char_to_add = next_ptr[-1]; + } + + dst[i++] = char_to_add; + } + + // here we ran out of room in dest, or words in source. + // if there's still room in dest and chars in source, just add them + while (*src_ptr && (i < 8)) { + dst[i] = *src_ptr; + src_ptr++; + i++; + } + } + + // pad with spaces (for ISO Name) + while (i < 8) { + dst[i++] = ' '; + } + + // upper case + for (i = 0; i < 8; i++) { + if (dst[i] > '`' && dst[i] < '{') { + dst[i] -= 0x20; + } + } + + // append file extension + strcpy(dst + 8, "STR"); +} \ No newline at end of file diff --git a/game/overlord/isocommon.h b/game/overlord/isocommon.h new file mode 100644 index 0000000000..1617b9a0dc --- /dev/null +++ b/game/overlord/isocommon.h @@ -0,0 +1,187 @@ +/*! + * @file isocommon.h + * Common ISO utilities. + */ + +#ifndef JAK_V2_ISOCOMMON_H +#define JAK_V2_ISOCOMMON_H + +#include +#include "common/common_types.h" +#include "common/link_types.h" + +constexpr int PRI_STACK_LENGTH = 4; // number of queued commands per priority +constexpr int N_PRIORITIES = 4; // number of priorities + +constexpr u32 CMD_STATUS_READ_ERR = 8; // read encountered a problem or was canceled. +constexpr u32 CMD_STATUS_NULL_CB = 7; // status returned if you don't set a callback +constexpr u32 CMD_STATUS_FAILED_TO_OPEN = 6; // status if file couldn't be opened +constexpr u32 CMD_STATUS_FAILED_TO_QUEUE = 2; // status if we couldn't be queued +constexpr u32 CMD_STATUS_IN_PROGRESS = 0xffffffff; // status if command is running and healthy +constexpr u32 CMD_STATUS_DONE = 0; // status if command is done. + +constexpr int BUFFER_PAGE_SIZE = 0xc000; // size in bytes of normal read buffer +constexpr int STR_BUFFER_DATA_SIZE = 0x6000; // size in bytes of vag read buffer + +constexpr int LOAD_TO_EE_CMD_ID = 0x100; // command to load file to ee +constexpr int LOAD_TO_IOP_CMD_ID = 0x101; // command to load to iop +constexpr int LOAD_TO_EE_OFFSET_CMD_ID = 0x102; // command to load file to ee with offset. +constexpr int LOAD_DGO_CMD_ID = 0x200; // command to load DGO + +constexpr int SECTOR_SIZE = 0x800; // media sector size +constexpr int MAX_ISO_FILES = 350; // maximum files on FS +constexpr int MAX_OPEN_FILES = 16; // maximum number of open files at a time. + +/*! + * Record for file. There is one for each file in the FS, and pointers to each FileRecord act as + * an identifier. + * The location/size can't be counted on to be anything meaningful as it depends on the IsoFs + * implementation being used. + */ +struct FileRecord { + char name[12]; + uint32_t location; + uint32_t size; +}; + +/*! + * Record for an open file. + */ +struct LoadStackEntry { + FileRecord* fr; + uint32_t location; +}; + +/*! + * Header for a ISO data buffer. + */ +struct IsoBufferHeader { + void* data; // 0 + uint32_t data_size; // 1 + uint32_t buffer_size; + void* next; + + // follows the header. + u8* get_data() { return ((u8*)this) + sizeof(IsoBufferHeader); } +}; + +struct IsoMessage; +struct LoadStackEntry; + +//! Callback function for data loads. +typedef u32 (*iso_callback_func)(IsoMessage* cmd, IsoBufferHeader* buffer); + +/*! + * Command, common parent. + */ +struct IsoMessage { + uint32_t field_0x0; // 0x00 + uint32_t field_0x4; // 0x04 + uint32_t cmd_id; // 0x08 + uint32_t status; // 0x0c + s32 messagebox_to_reply; // 0x10 + s32 thread_id; // 0x14 + uint32_t ready_for_data; // 0x18 + IsoBufferHeader* callback_buffer; // 0x1c + iso_callback_func callback_function; // 0x20 + LoadStackEntry* fd; // 0x24 +}; + +/*! + * Command to load a single file. + */ +struct IsoCommandLoadSingle : public IsoMessage { + FileRecord* file_record; // 0x28 + u8* dest_addr; // 0x2c + s32 length; // 0x30 + s32 length_to_copy; // 0x34 + u32 offset; // 0x38 + u8* dst_ptr; // 0x3c + s32 bytes_done; // 0x40 +}; + +/*! + * Command to do something. + */ +struct VagCommand : public IsoMessage { + u32 field_0x30; + u32 field_0x34; + u32 field_0x38; + u32 field_0x3c; + u32 field_0x40; + u32 field_0x44; + u32 field_0x48; + u32 field_0x4c; + // 0x6c max +}; + +/*! + * DGO Load State Machine states. + */ +enum class DgoState { + Init = 0, + Read_Header = 1, + Finish_Obj = 2, + Read_Last_Obj = 3, + Read_Obj_Header = 4, + Read_Obj_data = 5, + Finish_Dgo = 6 +}; + +/*! + * Command to load a DGO. + */ +struct DgoCommand : public IsoMessage { + FileRecord* fr; // 0x28, DGO file that's open + u8* buffer1; // 0x2c, first EE buffer + u8* buffer2; // 0x30, second EE buffer + u8* buffer_heaptop; // 0x34, top of the heap + + DgoHeader dgo_header; // 0x38, current DGO's header + ObjectHeader objHeader; // 0x78, current obj's header + + u8* ee_destination_buffer; // 0xb8, where we are currently loading to on ee + u32 bytes_processed; // 0xbc, how many bytes processed in the current state + u32 objects_loaded; // 0xc0, completed object count + DgoState dgo_state; // 0xc4, state machine state + u32 finished_first_obj; // 0xc8, have we finished loading the first object? + u32 buffer_toggle; // 0xcc, which buffer to load into (top, buffer1, buffer2) + u8* selectedBuffer; // 0xd0, most recently completed load destination + u32 want_abort; // 0xd4, should we quit? +}; + +/*! + * Priority Stack entry. + */ +struct PriStackEntry { + IsoMessage* cmds[PRI_STACK_LENGTH]; // cmds at this priority + std::string names[PRI_STACK_LENGTH]; // my addition for debug + uint32_t n; // how many in this priority? + + void reset(); +}; + +/*! + * API to access files. There are debug modes + reading from an ISO filesystem. + */ +struct IsoFs { + int (*init)(u8*); + FileRecord* (*find)(const char*); + FileRecord* (*find_in)(const char*); + uint32_t (*get_length)(FileRecord*); + LoadStackEntry* (*open)(FileRecord*, int32_t); + LoadStackEntry* (*open_wad)(FileRecord*, int32_t); + void (*close)(LoadStackEntry*); + uint32_t (*begin_read)(LoadStackEntry*, void*, int32_t); + uint32_t (*sync_read)(); + uint32_t (*load_sound_bank)(char*, void*); + uint32_t (*load_music)(char*, void*); + void (*poll_drive)(); +}; + +extern IsoFs* isofs; +extern s32 iso_mbx; + +void MakeISOName(char* dst, const char* src); + +#endif // JAK_V2_ISOCOMMON_H diff --git a/game/overlord/overlord.cpp b/game/overlord/overlord.cpp new file mode 100644 index 0000000000..eea691e7ce --- /dev/null +++ b/game/overlord/overlord.cpp @@ -0,0 +1,72 @@ +#include +#include "overlord.h" +#include "game/sce/iop.h" +#include "ramdisk.h" +#include "iso.h" +#include "ssound.h" +#include "sbank.h" + +using namespace iop; + +int start_overlord(int argc, const char* const* argv) { + (void)argc; + FlushDcache(); + CpuEnableIntr(); + if(!sceSifCheckInit()) { + sceSifInit(); + } + + sceSifInitRpc(0); + InitBanks(); + InitSound_Overlord(); + InitRamdisk(); +// RegisterVblankHandler(0, 0x20, VBlank_Handler, nullptr); + + ThreadParam thread_param; + thread_param.attr = TH_C; + thread_param.initPriority = 98; + thread_param.stackSize = 0x800; + thread_param.option = 0; + thread_param.entry = (void*)Thread_Server; + strcpy(thread_param.name, "Server"); // added + auto thread_server = CreateThread(&thread_param); + if(thread_server <= 0) { + return 1; + } + +// thread_param.attr = TH_C; +// thread_param.initPriority = 96; +// thread_param.stackSize = 0x800; +// thread_param.option = 0; +// thread_param.entry = Thread_Player; +// auto thread_player = CreateThread(&thread_param); +// if(thread_player <= 0) { +// return 1; +// } +// +// thread_param.attr = TH_C; +// thread_param.initPriority = 99; +// thread_param.stackSize = 0x1000; +// thread_param.option = 0; +// thread_param.entry = Thread_Loader; +// auto thread_loader = CreateThread(&thread_param); +// if(thread_loader <= 0) { +// return 1; +// } + + InitISOFS(argv[1], argv[2]); + StartThread(thread_server, 0); + +// StartThread(thread_player, 0); +// StartThread(thread_loader, 0); + return 0; +} + +/*! + * Loop endlessly and never return. + */ +void ExitIOP() { + while(true) { + + } +} \ No newline at end of file diff --git a/game/overlord/overlord.h b/game/overlord/overlord.h new file mode 100644 index 0000000000..4f368c6bc6 --- /dev/null +++ b/game/overlord/overlord.h @@ -0,0 +1,7 @@ +#ifndef JAK_V2_OVERLORD_H +#define JAK_V2_OVERLORD_H + +int start_overlord(int argc, const char* const* argv); +void ExitIOP(); + +#endif //JAK_V2_OVERLORD_H diff --git a/game/overlord/ramdisk.cpp b/game/overlord/ramdisk.cpp new file mode 100644 index 0000000000..5cdbca0b6a --- /dev/null +++ b/game/overlord/ramdisk.cpp @@ -0,0 +1,186 @@ +/*! + * @file ramdisk.cpp + * A RAMDISK RPC for storing files in the extra RAM left over on the IOP. + * Also called "Server". + */ + +#include +#include +#include +#include "common/common_types.h" +#include "game/common/ramdisk_rpc_types.h" +#include "ramdisk.h" +#include "iso.h" +#include "iso_api.h" +#include "game/sce/iop.h" + +// Note - the RAMDISK code supports having multiple files, but it appears only one file can ever be +// used at a time. + +constexpr int RAMDISK_SIZE = 0xcac00; // Memory size of RAMDISK +constexpr int RAMDISK_MAX_FILES = 16; // Maximum number of files to store in RAMDISK. +constexpr int RAMDISK_RETURN_BUFFER_SIZE = 0x2000; // Maximum size of an individual RAMDISK read +constexpr int DEVTOOL_IOP_MEM_ALLOC = + 0x1f5d00; // Extra memory to waste to compensate for extra RAM in dev kit + +u32 gNumFiles; // Number of files in the RAMDISK +u32 gMemUsed; // Memory of RAMDISK used +u32 gMemSize; // Total memory of RAMDISK +u32 gMemFreeAtStart; // Memory free after allocation of RAMDISK +uint8_t* gMem; // Allocation for RAMDISK +uint8_t* gRamdiskRAM; // Also allocation for RAMDISK +uint8_t gRPCBuf[40]; // Buffer for RAMDISK RPC handler + +// Each file stored in the ramdisk has a file record: +struct RamdiskFileRecord { + uint32_t size; // size of file in bytes (will be 16-byte aligned) + uint32_t additional_offset; // an offset into the memory for the file + uint32_t file_id; // an ID number used to identify this file. +}; + +RamdiskFileRecord gFiles[RAMDISK_MAX_FILES]; // File records +uint8_t gReturnBuffer[RAMDISK_RETURN_BUFFER_SIZE]; // Buffer to hold data requested by EE + +using namespace iop; + +void ramdisk_init_globals() { + gNumFiles = 0; + gMemUsed = 0; + gMemSize = 0; + gMemFreeAtStart = 0; + gMem = nullptr; + gRamdiskRAM = nullptr; + memset(gRPCBuf, 0, sizeof(gRPCBuf)); + memset(gFiles, 0, sizeof(gFiles)); + memset(gReturnBuffer, 0, sizeof(gReturnBuffer)); +} + +/*! + * Initialze the RAMDISK IOP System. + * For some reason the name of this function is lost, so this is a guess at the name. + * DONE, EXACT + */ +void InitRamdisk() { + gNumFiles = 0; + gMemUsed = 0; + gMemSize = RAMDISK_SIZE; + + // some sort of "trick" to allocate memory if we are on a debug system to simulate the memory size + // of the real PS2. + if (QueryTotalFreeMemSize() > 0x200000) { + AllocSysMemory(SMEM_Low, DEVTOOL_IOP_MEM_ALLOC, nullptr); + } + + // allocate RAMDISK RAM + gMem = (uint8_t*)AllocSysMemory(SMEM_Low, gMemSize, nullptr); + if (gMem) { + gMemFreeAtStart = QueryTotalFreeMemSize(); + gRamdiskRAM = gMem; + } else { + printf("[OVERLORD RAMDISK] Failed to allocate memory for RAMDISK!\n"); // added + } +} + +void* RPC_Ramdisk(unsigned int fno, void* data, int size); + +/*! + * The main function for the IOP Ramdisk/Server thread. + * DONE, EXACT + */ +u32 Thread_Server() { + sceSifQueueData dq; + sceSifServeData serve; + + // set up RPC + CpuDisableIntr(); + sceSifInitRpc(0); + sceSifSetRpcQueue(&dq, GetThreadId()); + sceSifRegisterRpc(&serve, RAMDISK_RPC_ID, RPC_Ramdisk, gRPCBuf, nullptr, nullptr, &dq); + CpuEnableIntr(); + sceSifRpcLoop(&dq); + return 0; +} + +/*! + * Ramdisk RPC Handler. + * DONE + * Added some debugging print statements. + * Returns a pointer to the file contents on successful GET_DATA. + * Returns nullptr in all other cases. + */ +void* RPC_Ramdisk(unsigned int fno, void* data, int size) { + (void)size; + + auto cmd = (RPC_Ramdisk_LoadCmd*)data; + if (fno == RAMDISK_RESET_AND_LOAD_FNO) { + // reset files and memory + gNumFiles = 0; + gMemUsed = 0; + + // locate file to load into ramdisk + auto file_record = FindISOFile(cmd->name); + if (!file_record) { + printf("[OVERLORD RAMDISK] Failed to find ISO file for load.\n"); // added + return nullptr; + } + + // if we have available memory and records (we'll always have enough records, we just reset it!) + // NOTE - there is a bug here where the rounding up to 16-bytes can cause it to overflow! + auto file_length = GetISOFileLength(file_record); + if ((file_length + gMemUsed <= gMemSize) && (gNumFiles != RAMDISK_MAX_FILES)) { + // Create the new file record + gFiles[gNumFiles].size = (file_length + 0xf) & 0xfffffff0; + assert(gFiles[gNumFiles].size + gMemUsed < + gMemSize); // ADDED! this checks for a real bug in the code. + gFiles[gNumFiles].additional_offset = 0; + gFiles[gNumFiles].file_id = cmd->file_id_or_ee_addr; + + // Increment file count + gNumFiles++; + + // Load file into IOP at the appropriate spot + LoadISOFileToIOP(file_record, gMem + gMemUsed, file_length); + gMemUsed += gFiles[gNumFiles].size; + } else { + printf("[OVERLORD RAMDISK] Failed to load file because RAMDISK is out of memory or files!\n"); + } + } else if (fno == RAMDISK_GET_DATA_FNO) { + // Copy data into a local IOP buffer + + // Total offset into ramdisk memory + auto offset = cmd->offset_into_file; + + // find a matching file, and compute its offset + u32 file_idx = 0; + while (file_idx < gNumFiles && gFiles[file_idx].file_id != cmd->file_id_or_ee_addr) { + offset += gFiles[file_idx].size; + file_idx++; + } + + if (file_idx == gNumFiles) { + // didn't find the file + printf("[OVERLORD RAMDISK] Failed to find ISO file for read.\n"); // added + return nullptr; + } + + if (cmd->size > RAMDISK_RETURN_BUFFER_SIZE) { + printf("[OVERLORD RAMDISK] requested file read size is too large.\n"); // added + return nullptr; + } + + // copy to return buffer. This way RAMDISK data is valid until another GET_DATA. + memcpy(gReturnBuffer, gMem + offset + gFiles[file_idx].additional_offset, size); + return gReturnBuffer; + } else if (fno == RAMDISK_BYPASS_LOAD_FILE) { + // This is just a normal file load to the EE. + auto file_record = FindISOFile(cmd->name); + if (!file_record) { + printf("[OVERLORD RAMDISK] Failed to open file for bypass load.\n"); // added + return nullptr; + } + LoadISOFileToEE(file_record, cmd->file_id_or_ee_addr, cmd->size); + } else { + printf("[OVERLORD RAMDISK] Unsupported fno\n"); // ADDED + } + return nullptr; +} \ No newline at end of file diff --git a/game/overlord/ramdisk.h b/game/overlord/ramdisk.h new file mode 100644 index 0000000000..0fad7da8f3 --- /dev/null +++ b/game/overlord/ramdisk.h @@ -0,0 +1,16 @@ +/*! + * @file ramdisk.cpp + * A RAMDISK RPC for storing files in the extra RAM left over on the IOP. + * Also called "Server". + */ + +#ifndef JAK_RAMDISK_H +#define JAK_RAMDISK_H + +#include "common/common_types.h" + +void ramdisk_init_globals(); +void InitRamdisk(); +u32 Thread_Server(); + +#endif // JAK_RAMDISK_H diff --git a/game/overlord/sbank.cpp b/game/overlord/sbank.cpp new file mode 100644 index 0000000000..ffcb393421 --- /dev/null +++ b/game/overlord/sbank.cpp @@ -0,0 +1,5 @@ +#include "sbank.h" + +void InitBanks() { + +} diff --git a/game/overlord/sbank.h b/game/overlord/sbank.h new file mode 100644 index 0000000000..fae812d529 --- /dev/null +++ b/game/overlord/sbank.h @@ -0,0 +1,6 @@ +#ifndef JAK_V2_SBANK_H +#define JAK_V2_SBANK_H + +void InitBanks(); + +#endif //JAK_V2_SBANK_H diff --git a/game/overlord/soundcommon.cpp b/game/overlord/soundcommon.cpp new file mode 100644 index 0000000000..18078840a3 --- /dev/null +++ b/game/overlord/soundcommon.cpp @@ -0,0 +1,8 @@ +#include +#include "soundcommon.h" + + +void PrintBankInfo(void* buffer) { + (void)buffer; + assert(false); +} \ No newline at end of file diff --git a/game/overlord/soundcommon.h b/game/overlord/soundcommon.h new file mode 100644 index 0000000000..52886be7ee --- /dev/null +++ b/game/overlord/soundcommon.h @@ -0,0 +1,6 @@ +#ifndef JAK_V2_SOUNDCOMMON_H +#define JAK_V2_SOUNDCOMMON_H + +void PrintBankInfo(void* buffer); + +#endif //JAK_V2_SOUNDCOMMON_H diff --git a/game/overlord/srpc.cpp b/game/overlord/srpc.cpp new file mode 100644 index 0000000000..0f43b05a65 --- /dev/null +++ b/game/overlord/srpc.cpp @@ -0,0 +1,8 @@ +#include +#include "srpc.h" + +u8 gMusicTweakInfo[0x204]; + +void srpc_init_globals() { + memset(gMusicTweakInfo, 0, sizeof(gMusicTweakInfo)); +} \ No newline at end of file diff --git a/game/overlord/srpc.h b/game/overlord/srpc.h new file mode 100644 index 0000000000..fd7e9f680b --- /dev/null +++ b/game/overlord/srpc.h @@ -0,0 +1,11 @@ +#ifndef JAK_V2_SRPC_H +#define JAK_V2_SRPC_H + +#include "common/common_types.h" + +void srpc_init_globals(); + +constexpr int MUSIC_TWEAK_SIZE = 0x204; +extern u8 gMusicTweakInfo[MUSIC_TWEAK_SIZE]; + +#endif //JAK_V2_SRPC_H diff --git a/game/overlord/ssound.cpp b/game/overlord/ssound.cpp new file mode 100644 index 0000000000..d308c45702 --- /dev/null +++ b/game/overlord/ssound.cpp @@ -0,0 +1,5 @@ +#include "ssound.h" + +void InitSound_Overlord() { + +} \ No newline at end of file diff --git a/game/overlord/ssound.h b/game/overlord/ssound.h new file mode 100644 index 0000000000..1d19b0d6b6 --- /dev/null +++ b/game/overlord/ssound.h @@ -0,0 +1,6 @@ +#ifndef JAK_V2_SSOUND_H +#define JAK_V2_SSOUND_H + +void InitSound_Overlord(); + +#endif //JAK_V2_SSOUND_H diff --git a/game/overlord/stream.cpp b/game/overlord/stream.cpp new file mode 100644 index 0000000000..7e3397d80c --- /dev/null +++ b/game/overlord/stream.cpp @@ -0,0 +1,12 @@ +#include +#include "stream.h" + +u32 STRThread() { + assert(false); + return 0; +} + +u32 PLAYThread() { + assert(false); + return 0; +} \ No newline at end of file diff --git a/game/overlord/stream.h b/game/overlord/stream.h new file mode 100644 index 0000000000..fd71e298d9 --- /dev/null +++ b/game/overlord/stream.h @@ -0,0 +1,8 @@ +#ifndef JAK_V2_STREAM_H +#define JAK_V2_STREAM_H + +#include "common/common_types.h" +u32 STRThread(); +u32 PLAYThread(); + +#endif //JAK_V2_STREAM_H diff --git a/game/overlord/todo.txt b/game/overlord/todo.txt new file mode 100644 index 0000000000..08e85c18f8 --- /dev/null +++ b/game/overlord/todo.txt @@ -0,0 +1,48 @@ +TESTS! + +fake_iso +--------- +FS_Init should load tweak music file +Consider making it less slow? (probably doesn't matter) +FS_LoadMusic +FS_LoadSoundBank. + + +iso_cd +------- +work out timing constant +FS_LoadSoundBank magic constants +FS_LoadMusic magic constants + +dma +------ +DMA_SendToSPUAndSync + +iso +----- +InitISOFS - DMA_SendToSPUAndSync + - STRThread + - PLAYThread +Move VagDirEntry to somewhere else +magic numbers +more ISOThread message Ids +Handle Sound Stuff +VagCommand field names. +ProcessVAGData +StopVAG +PauseVAG +CalculateVAGVolumes +UnpauseVAG +SetVAGVol +GetPlayPos +UpdatePlayPos +CheckVAGStreamProgress +GetVAGStreamPos +VAG_MarkLoopStart +VAG_MarkLoopEnd +VAG_MarkNonloopStart +VAG_MarkNonloopEnd + +stream +--------- +the whole thing. \ No newline at end of file diff --git a/game/runtime.cpp b/game/runtime.cpp new file mode 100644 index 0000000000..14af11cb9b --- /dev/null +++ b/game/runtime.cpp @@ -0,0 +1,247 @@ +/*! + * @file runtime.cpp + * Setup and launcher for the runtime. + */ + +#include +#include +#include + +#include "runtime.h" +#include "system/SystemThread.h" +#include "sce/libcdvd_ee.h" +#include "sce/deci2.h" +#include "sce/sif_ee.h" +#include "sce/iop.h" +#include "game/system/Deci2Server.h" + +#include "game/kernel/fileio.h" +#include "game/kernel/kboot.h" +#include "game/kernel/klink.h" +#include "game/kernel/kscheme.h" +#include "game/kernel/kdsnetm.h" +#include "game/kernel/klisten.h" +#include "game/kernel/kmemcard.h" +#include "game/kernel/kprint.h" +#include "game/kernel/kdgo.h" + +#include "game/system/iop_thread.h" + +#include "game/overlord/dma.h" +#include "game/overlord/iso.h" +#include "game/overlord/fake_iso.h" +#include "game/overlord/iso_queue.h" +#include "game/overlord/ramdisk.h" +#include "game/overlord/iso_cd.h" +#include "game/overlord/overlord.h" +#include "game/overlord/srpc.h" + +u8* g_ee_main_mem = nullptr; + +namespace { + +/*! + * SystemThread function for running the DECI2 communication with the GOAL compiler. + */ +void deci2_runner(SystemThreadInterface& interface) { + // callback function so the server knows when to give up and shutdown + std::function shutdown_callback = [&]() { return interface.get_want_exit(); }; + + // create and register server + Deci2Server server(shutdown_callback); + ee::LIBRARY_sceDeci2_register(&server); + + // now its ok to continue with initialization + interface.initialization_complete(); + + // in our own thread, wait for the EE to register the first protocol driver + printf("[DECI2] waiting for EE to register protos\n"); + server.wait_for_protos_ready(); + // then allow the server to accept connections + if (!server.init()) { + throw std::runtime_error("DECI2 server init failed"); + } + + printf("[DECI2] waiting for listener...\n"); + bool saw_listener = false; + while (!interface.get_want_exit()) { + if (server.check_for_listener()) { + if (!saw_listener) { + printf("[DECI2] Connected!\n"); + } + saw_listener = true; + // we have a listener, run! + server.run(); + } else { + // no connection yet. Do a sleep so we don't spam checking the listener. + usleep(50000); + } + } +} + +// EE System +constexpr int EE_MAIN_MEM_SIZE = 128 * (1 << 20); // 128 MB, same as PS2 TOOL +constexpr u64 EE_MAIN_MEM_MAP = 0x2000000000; // intentionally > 32-bit to catch pointer bugs + +// when true, attempt to map the EE memory in the low 2 GB of RAM +// this allows us to use EE pointers as real pointers. However, this might not always work, +// so this should be used only for debugging. +constexpr bool EE_MEM_LOW_MAP = false; + +// GOAL Boot arguments +constexpr const char* GOAL_ARGV[] = {"", "-fakeiso", "-boot", "-debug"}; +constexpr int GOAL_ARGC = 4; + +/*! + * SystemThread Function for the EE (PS2 Main CPU) + */ +void ee_runner(SystemThreadInterface& interface) { + // Allocate Main RAM. Must have execute enabled. + if (EE_MEM_LOW_MAP) { + g_ee_main_mem = + (u8*)mmap((void*)0x10000000, EE_MAIN_MEM_SIZE, PROT_EXEC | PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_32BIT | MAP_PRIVATE | MAP_POPULATE, 0, 0); + } else { + g_ee_main_mem = + (u8*)mmap((void*)EE_MAIN_MEM_MAP, EE_MAIN_MEM_SIZE, PROT_EXEC | PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); + } + + if (g_ee_main_mem == (u8*)(-1)) { + printf(" Failed to initialize main memory! %s\n", strerror(errno)); + interface.initialization_complete(); + return; + } + + printf(" Main memory mapped at 0x%016lx\n", (u64)(g_ee_main_mem)); + printf(" Main memory size 0x%x bytes (%.3f MB)\n", EE_MAIN_MEM_SIZE, + (double)EE_MAIN_MEM_SIZE / (1 << 20)); + + printf("[EE] Initialization complete!\n"); + interface.initialization_complete(); + + printf("[EE] Run!\n"); + memset((void*)g_ee_main_mem, 0, EE_MAIN_MEM_SIZE); + fileio_init_globals(); + kboot_init_globals(); + kdgo_init_globals(); + kdsnetm_init_globals(); + klink_init_globals(); + + kmachine_init_globals(); + kscheme_init_globals(); + kmalloc_init_globals(); + + klisten_init_globals(); + kmemcard_init_globals(); + kprint_init_globals(); + + goal_main(GOAL_ARGC, GOAL_ARGV); + printf("[EE] Done!\n"); + + // // kill the IOP todo + iop::LIBRARY_kill(); + + munmap(g_ee_main_mem, EE_MAIN_MEM_SIZE); + + // after main returns, trigger a shutdown. + interface.trigger_shutdown(); +} + +/*! + * SystemThread function for running the IOP (separate I/O Processor) + */ +void iop_runner(SystemThreadInterface& interface) { + IOP iop; + printf("\n\n\n[IOP] Restart!\n"); + iop.reset_allocator(); + ee::LIBRARY_sceSif_register(&iop); + iop::LIBRARY_register(&iop); + + // todo! + dma_init_globals(); + iso_init_globals(); + fake_iso_init_globals(); + // iso_api + iso_cd_init_globals(); + iso_queue_init_globals(); + // isocommon + // overlord + ramdisk_init_globals(); + // sbank + // soundcommon + srpc_init_globals(); + // ssound + // stream + + interface.initialization_complete(); + + printf("[IOP] Wait for OVERLORD to be started...\n"); + iop.wait_for_overlord_start_cmd(); + if (iop.status == IOP_OVERLORD_INIT) { + printf("[IOP] Run!\n"); + } else { + printf("[IOP] shutdown!\n"); + return; + } + + iop.reset_allocator(); + + // init + + start_overlord(iop.overlord_argc, iop.overlord_argv); // todo! + + // unblock the EE, the overlord is set up! + iop.signal_overlord_init_finish(); + + // IOP Kernel loop + while (!interface.get_want_exit() && !iop.want_exit) { + // the IOP kernel just runs at full blast, so we only run the IOP when the EE is waiting on the + // IOP. Each time the EE is waiting on the IOP, it will run an iteration of the IOP kernel. + iop.wait_run_iop(); + iop.kernel.dispatchAll(); + } + + // stop all threads in the iop kernel. + // if the threads are not stopped nicely, we will deadlock on trying to destroy the kernel's + // condition variables. + iop.kernel.shutdown(); +} +} // namespace + +/*! + * Main function to launch the runtime. + * Arguments are currently ignored. + */ +void exec_runtime(int argc, char** argv) { + (void)argc; + (void)argv; + + // step 1: sce library prep + iop::LIBRARY_INIT(); + ee::LIBRARY_INIT_sceCd(); + ee::LIBRARY_INIT_sceDeci2(); + ee::LIBRARY_INIT_sceSif(); + + // step 2: system prep + SystemThreadManager tm; + auto& deci_thread = tm.create_thread("DMP"); + auto& iop_thread = tm.create_thread("IOP"); + auto& ee_thread = tm.create_thread("EE"); + + // step 3: start the EE! + iop_thread.start(iop_runner); + ee_thread.start(ee_runner); + deci_thread.start(deci2_runner); + + // step 4: wait for EE to signal a shutdown, which will cause the DECI thread to join. + deci_thread.join(); + // DECI has been killed, shutdown! + + // to be extra sure + tm.shutdown(); + + // join and exit + tm.join(); + printf("GOAL Runtime Shutdown\n"); +} \ No newline at end of file diff --git a/game/runtime.h b/game/runtime.h new file mode 100644 index 0000000000..b252447ada --- /dev/null +++ b/game/runtime.h @@ -0,0 +1,14 @@ +/*! + * @file runtime.h + * Setup and launcher for the runtime. + */ + +#ifndef JAK1_RUNTIME_H +#define JAK1_RUNTIME_H + +#include "common/common_types.h" + +extern u8* g_ee_main_mem; +void exec_runtime(int argc, char** argv); + +#endif // JAK1_RUNTIME_H diff --git a/game/sce/deci2.cpp b/game/sce/deci2.cpp new file mode 100644 index 0000000000..03c2f7c39a --- /dev/null +++ b/game/sce/deci2.cpp @@ -0,0 +1,136 @@ +/*! + * @file deci2.cpp + * Implementation of SCE DECI2 library. + */ + +#include +#include +#include +#include "deci2.h" +#include "game/system/Deci2Server.h" + +namespace ee { + +namespace { +constexpr int MAX_DECI2_PROTOCOLS = 4; +Deci2Driver protocols[MAX_DECI2_PROTOCOLS]; // info for each deci2 protocol registered +int protocol_count; // number of registered protocols +Deci2Driver* sending_driver; // currently sending protocol driver +::Deci2Server* server; // the server to send data to +} // namespace + +/*! + * Initialize the library. + */ +void LIBRARY_INIT_sceDeci2() { + // reset protocols + for (auto& p : protocols) { + p = Deci2Driver(); + } + protocol_count = 0; + server = nullptr; + sending_driver = nullptr; +} + +/*! + * Run any pending requested sends. + */ +void LIBRARY_sceDeci2_run_sends() { + for (auto& prot : protocols) { + if (prot.active && prot.pending_send == 'H') { + sending_driver = &prot; + (prot.handler)(DECI2_WRITE, 0, prot.opt); + sending_driver = nullptr; + prot.pending_send = 0; + (prot.handler)(DECI2_WRITEDONE, 0, prot.opt); + } + } +} + +/*! + * Register a Deci2Server with this library. + */ +void LIBRARY_sceDeci2_register(::Deci2Server* s) { + server = s; +} + +/*! + * Open a new socket with given protocol number and handler. + * The "opt" pointer is passed to the handler function. + * I don't know why it's like this. + */ +s32 sceDeci2Open(u16 protocol, void* opt, void (*handler)(s32 event, s32 param, void* opt)) { + server->lock(); + Deci2Driver drv; + drv.protocol = protocol; + drv.opt = opt; + drv.handler = handler; + drv.id = protocol_count + 1; + drv.active = true; + protocols[protocol_count++] = drv; + printf("[DECI2] Add new protocol driver %d for 0x%x\n", drv.id, drv.protocol); + server->unlock(); + + if (protocol_count == 1) { + // if we have our first protocol, inform the server we are ready to receive! + // then the server will accept incoming data. + server->send_proto_ready(protocols, &protocol_count); + } + + return drv.id; +} + +/*! + * Deactivate a DECI2 protocol by socket descriptor. + */ +s32 sceDeci2Close(s32 s) { + assert(s - 1 < protocol_count); + protocols[s - 1].active = false; + return 1; +} + +/*! + * Start a send. + */ +s32 sceDeci2ReqSend(s32 s, char dest) { + assert(s - 1 < protocol_count); + auto& proto = protocols[s - 1]; + proto.pending_send = dest; + return 0; +} + +/*! + * Do a receive from socket s into buf of size len. + * Returns after data is copied. + */ +s32 sceDeci2ExRecv(s32 s, void* buf, u16 len) { + assert(s - 1 < protocol_count); + protocols[s - 1].recv_size = len; + auto avail = protocols[s - 1].available_to_receive; + if (len <= avail) { + memcpy(buf, protocols[s - 1].recv_buffer, len); + return len; + } else { + printf("[DECI2] Error: ExRecv %d, only %d available!\n", len, avail); + return -1; + } +} + +/*! + * Do a send. + */ +s32 sceDeci2ExSend(s32 s, void* buf, u16 len) { + assert(s - 1 < protocol_count); + if (!sending_driver) { + printf("sceDeci2ExSend called at illegal time!\n"); + } + + if (&protocols[s - 1] != sending_driver) { + printf("sceDeci2ExSend called with the wrong socket!\n"); + } + + server->send_data(buf, len); + return len; +} + +} // namespace ee \ No newline at end of file diff --git a/game/sce/deci2.h b/game/sce/deci2.h new file mode 100644 index 0000000000..1af3f8cb27 --- /dev/null +++ b/game/sce/deci2.h @@ -0,0 +1,28 @@ +/*! + * @file deci2.h + * Implementation of SCE DECI2 library. + */ + +#ifndef JAK1_DECI2_H +#define JAK1_DECI2_H + +#include "common/listener_common.h" + +class Deci2Server; + + +namespace ee { + +void LIBRARY_INIT_sceDeci2(); +void LIBRARY_sceDeci2_run_sends(); +void LIBRARY_sceDeci2_register(::Deci2Server* server); + +s32 sceDeci2Open(u16 protocol, void* opt, void (*handler)(s32 event, s32 param, void* opt)); +s32 sceDeci2Close(s32 s); +s32 sceDeci2ReqSend(s32 s, char dest); +s32 sceDeci2ExRecv(s32 s, void* buf, u16 len); +s32 sceDeci2ExSend(s32 s, void* buf, u16 len); + +} // namespace ee + +#endif // JAK1_DECI2_H diff --git a/game/sce/iop.cpp b/game/sce/iop.cpp new file mode 100644 index 0000000000..6f954a0183 --- /dev/null +++ b/game/sce/iop.cpp @@ -0,0 +1,216 @@ +#include +#include +#include "iop.h" +#include "game/system/iop_thread.h" + +namespace iop { +/*! + * Is the SIF initialized? + */ +u32 sceSifCheckInit() { + // the SIF is always initialized by the time OVERLORD starts. + // it would only be on an ancient dev kit where this might not be true. + return 1; +} + +/*! + * Initialize SIF + */ +void sceSifInit() { + // do nothing! +} + +/*! + * Initialize RPC + */ +void sceSifInitRpc(int mode) { + assert(mode == 0); +} + +/*! + * Flush Data Cache + */ +void FlushDcache() { + // Do nothing! The data cache does not need to be flushed on x86 as we have no DMA which bypasses cache. +} + +/*! + * Enable CPU Interrupts + */ +void CpuDisableIntr() { + +} + +/*! + * Disable CPU Interrupts + */ +void CpuEnableIntr() { + +} + +namespace { +::IOP* iop; +} + +void LIBRARY_INIT() { + iop = nullptr; +} + +void LIBRARY_register(::IOP* i) { + iop = i; +} + +void LIBRARY_kill() { + iop->kill_from_ee(); +} + +/*! + * How much free memory is there, in bytes? + */ +int QueryTotalFreeMemSize() { + // this value is somewhat arbitrary - it's a lot, but not enough to make OVERLORD think it is running on + // an 8MB-of-IOP-RAM development machine. + return 0x100000; +} + +/*! + * Allocate memory. + */ +void *AllocSysMemory(int type, unsigned long size, void *addr) { + assert(type == SMEM_Low); + assert(addr == nullptr); + return iop->iop_alloc(size); +} + +/*! + * Create a new thread + */ +s32 CreateThread(ThreadParam* param) { + return iop->kernel.CreateThread(param->name, (u32(*)())param->entry); +} + +/*! + * Create a new message box. + */ +s32 CreateMbx(MbxParam* param) { + (void)param; + return iop->kernel.CreateMbx(); +} + +s32 StartThread(s32 thid, u32 arg) { + assert(!arg); + iop->kernel.StartThread(thid); + return 0; +} + +int GetThreadId() { + return iop->kernel.getCurrentThread(); +} + +void sceSifSetRpcQueue(sceSifQueueData* dq, int key) { + dq->key = key; + iop->kernel.set_rpc_queue(dq, key); +} + +void sceSifRegisterRpc(sceSifServeData* serve, unsigned int request, + sceSifRpcFunc func, void* buff, sceSifRpcFunc cfunc, void* cbuff, sceSifQueueData* qd) { + serve->command = request; + serve->func = func; + serve->buff = buff; + (void)cfunc; + (void)cbuff; + assert(!cfunc); + assert(!cbuff); + qd->serve_data = serve; +} + +void sceSifRpcLoop(sceSifQueueData* pd) { + iop->kernel.rpc_loop(pd); +} + +int sceCdRead(uint32_t logical_sector, uint32_t sectors, void* buf, sceCdRMode* mode) { + (void)mode; + iop->kernel.read_disc_sectors(logical_sector, sectors, buf); + return 1; +} + +int sceCdSync(int mode) { + (void)mode; + return 0; +} + +int sceCdGetError() { + return 0; // no error +} + +int sceCdGetDiskType() { + return SCECdPS2DVD; // always a DVD (for now) +} + +int sceCdMmode(int media) { + (void)media; + return 1; +} + +void DelayThread(u32 usec) { + iop->kernel.SuspendThread(); + (void)usec; +} + +int sceCdBreak() { + return 1; +} + +int sceCdDiskReady(int mode) { + (void)mode; + return SCECdComplete; +} + +u32 sceSifSetDma(sceSifDmaData* sdd, int len) { + assert(len == 1); + assert(len <= 0xc000); + // todo - sanity check the destination address. + memcpy(iop->ee_main_mem + (u64)(sdd->addr), sdd->data, sdd->size); + return 1; +} + +s32 SendMbx(s32 mbxid, void* sendmsg) { + return iop->kernel.SendMbx(mbxid, sendmsg); +} + +s32 PollMbx(MsgPacket** recvmsg, int mbxid) { + return iop->kernel.PollMbx((void**)recvmsg, mbxid); +} + +static int now = 0; + +void GetSystemTime(SysClock* time) { + time->lo = 0; + time->hi = now; + now += 10; +} + +void SleepThread() { + iop->kernel.SleepThread(); +} + +s32 CreateSema(SemaParam* param) { + (void)param; + return iop->kernel.CreateSema(); +} + +s32 WaitSema(s32 sema) { + (void)sema; + throw std::runtime_error("NYI"); +} + +s32 SignalSema(s32 sema) { + (void)sema; + throw std::runtime_error("NYI"); +} + +s32 WakeupThread(s32 thid) { + iop->kernel.WakeupThread(thid); + return 0; +} +} \ No newline at end of file diff --git a/game/sce/iop.h b/game/sce/iop.h new file mode 100644 index 0000000000..e10908e096 --- /dev/null +++ b/game/sce/iop.h @@ -0,0 +1,140 @@ +#ifndef JAK1_IOP_H +#define JAK1_IOP_H + +#include "common/common_types.h" + +#define SMEM_Low (0) +#define SMEM_High (1) +#define SMEM_Addr (2) + +#define SCECdCD 1 +#define SCECdDVD 2 + +#define SCECdIllgalMedia 0xff +#define SCECdIllegalMedia 0xff +#define SCECdDVDV 0xfe +#define SCECdCDDA 0xfd +#define SCECdPS2DVD 0x14 +#define SCECdPS2CD 0x12 +#define SCECdDETCT 0x01 + + +#define SCECdComplete 0x02 +#define SCECdNotReady 0x06 +#define KE_MBOX_NOMSG -424 + +#define TH_C 0x02000000 + +class IOP; + +namespace iop { +typedef void * (* sceSifRpcFunc)(unsigned int,void *,int); + +struct sceSifServeData { + unsigned int command; // the RPC ID + sceSifRpcFunc func; + void* buff; +}; + +struct sceSifQueueData { + int key = -1; + sceSifServeData* serve_data = nullptr; +}; + +struct sceCdRMode { + uint8_t trycount; + uint8_t spindlctrl; + uint8_t datapattern; + uint8_t pad; +}; + +struct sceSifDmaData{ + void* data; + void* addr; + unsigned int size; + unsigned int mode; +}; + + +struct SysClock { + uint32_t hi, lo; +}; + +struct MsgPacket { + u32 dummy = 0; +}; + +struct MbxParam { + u32 attr; + u32 option; +}; + +struct ThreadParam { + u32 attr; + u32 option; + void *entry; + int stackSize; + int initPriority; + + // added! + char name[64]; +}; + +struct SemaParam { + uint32_t attr; + int32_t init_count; + int32_t max_count; + uint32_t option; +}; + +//void PS2_RegisterIOP(IOP *iop); +int QueryTotalFreeMemSize(); +void *AllocSysMemory(int type, unsigned long size, void *addr); + +int GetThreadId(); +void CpuDisableIntr(); +void CpuEnableIntr(); +void SleepThread(); +void DelayThread(u32 usec); +s32 CreateThread(ThreadParam* param); +s32 StartThread(s32 thid, u32 arg); +s32 WakeupThread(s32 thid); + +void sceSifInitRpc(int mode); +void sceSifInitRpc(unsigned int mode); +void sceSifSetRpcQueue(sceSifQueueData* dq, int key); +void sceSifRegisterRpc(sceSifServeData* serve, unsigned int request, + sceSifRpcFunc func, void* buff, sceSifRpcFunc cfunc, void* cbuff, sceSifQueueData* qd); +void sceSifRpcLoop(sceSifQueueData* pd); + +int sceCdRead(uint32_t logical_sector, uint32_t sectors, void* buf, sceCdRMode* mode); +int sceCdSync(int mode); +int sceCdGetError(); +int sceCdGetDiskType(); +int sceCdMmode(int media); +int sceCdBreak(); +int sceCdDiskReady(int mode); + +u32 sceSifSetDma(sceSifDmaData* sdd, int len); + +s32 SendMbx(int mbxid, void* sendmsg); +s32 PollMbx(MsgPacket** recvmsg, int mbxid); +s32 CreateMbx(MbxParam* param); + +void GetSystemTime(SysClock* time); + +s32 CreateSema(SemaParam* param); +s32 WaitSema(s32 sema); +s32 SignalSema(s32 sema); + +void FlushDcache(); + +u32 sceSifCheckInit(); +void sceSifInit(); + +void LIBRARY_INIT(); +void LIBRARY_register(::IOP* i); +void LIBRARY_kill(); +} + +#endif // JAK1_IOP_H diff --git a/game/sce/libcdvd_ee.cpp b/game/sce/libcdvd_ee.cpp new file mode 100644 index 0000000000..32e2cc95d7 --- /dev/null +++ b/game/sce/libcdvd_ee.cpp @@ -0,0 +1,62 @@ +/*! + * @file libcdvd_ee.cpp + * Stub implementation of the EE CD/DVD library + */ + +#include +#include "libcdvd_ee.h" + +namespace ee { + +namespace { +// CD/DVD media type set by sceCdMMode +int media_mode; +} + +void LIBRARY_INIT_sceCd() { + media_mode = -1; +} + +/*! + * Initialize the CD/DVD subsystem. + * init_mode should be SCECdINIT + */ +int sceCdInit(int init_mode){ + assert(init_mode == SCECdINIT); + return 1; // Initialization was performed normally +} + +/*! + * Tell the library if we are expecting a CD or DVD. + */ +int sceCdMmode(int media) { + media_mode = media; + return 1; // If successful, returns 1 +} + +/*! + * Is the drive ready for commands? + * Mode is a flag for non-blocking, otherwise block until ready. + */ +int sceCdDiskReady(int mode) { + (void)mode; + // always ready! + return SCECdComplete; +} + +/*! + * What type of disk do we have? + */ +int sceCdGetDiskType() { + // if we set CD or DVD, return the appropriate PS2 game disk type. + switch(media_mode) { + case SCECdCD: + return SCECdPS2CD; + case SCECdDVD: + return SCECdPS2DVD; + default: + // unset/unknown media mode, so drive won't work. + return SCECdIllegalMedia; + } +} +} // namespace ee \ No newline at end of file diff --git a/game/sce/libcdvd_ee.h b/game/sce/libcdvd_ee.h new file mode 100644 index 0000000000..95625dda76 --- /dev/null +++ b/game/sce/libcdvd_ee.h @@ -0,0 +1,36 @@ +/*! + * @file libcdvd_ee.h + * Stub implementation of the EE CD/DVD library + */ + +#ifndef JAK1_LIBCDVD_EE_H +#define JAK1_LIBCDVD_EE_H + +// for sceCdInit +#define SCECdINIT 0x00 + +// Media modes +#define SCECdCD 1 +#define SCECdDVD 2 + +// Status +#define SCECdComplete 0x02 +#define SCECdNotReady 0x06 + +// Disk Types +#define SCECdIllegalMedia 0xff +#define SCECdDVDV 0xfe +#define SCECdCDDA 0xfd +#define SCECdPS2DVD 0x14 +#define SCECdPS2CD 0x12 +#define SCECdDETCT 0x01 + +namespace ee { +void LIBRARY_INIT_sceCd(); +int sceCdInit(int init_mode); +int sceCdMmode(int media); +int sceCdDiskReady(int mode); +int sceCdGetDiskType(); +} // namespace ee + +#endif // JAK1_LIBCDVD_EE_H diff --git a/game/sce/libscf.cpp b/game/sce/libscf.cpp new file mode 100644 index 0000000000..c059cfe5dd --- /dev/null +++ b/game/sce/libscf.cpp @@ -0,0 +1,11 @@ +#include "libscf.h" + +namespace ee { +int sceScfGetAspect() { + return SCE_ASPECT_169; +} + +int sceScfGetLanguage() { + return SCE_ENGLISH_LANGUAGE; +} +} \ No newline at end of file diff --git a/game/sce/libscf.h b/game/sce/libscf.h new file mode 100644 index 0000000000..6647c0cc7c --- /dev/null +++ b/game/sce/libscf.h @@ -0,0 +1,31 @@ +#ifndef JAK1_LIBSCF_H +#define JAK1_LIBSCF_H + +#define SCE_JAPANESE_LANGUAGE 0 +#define SCE_ENGLISH_LANGUAGE 1 +#define SCE_FRENCH_LANGUAGE 2 +#define SCE_SPANISH_LANGUAGE 3 +#define SCE_GERMAN_LANGUAGE 4 +#define SCE_ITALIAN_LANGUAGE 5 +#define SCE_DUTCH_LANGUAGE 6 +#define SCE_PORTUGUESE_LANGUAGE 7 + +#define SCE_ASPECT_43 0 +#define SCE_ASPECT_FULL 1 +#define SCE_ASPECT_169 2 + +namespace ee { +/*! + * Get the aspect ratio setting of the PS2. + * It is either 4:3, 16:9, or FULL. + */ +int sceScfGetAspect(); + +/*! + * Get the language setting of the PS2. + * Return a SONY SCE_LANGUAGE value, which differs from GOAL. + */ +int sceScfGetLanguage(); +} + +#endif // JAK1_LIBSCF_H diff --git a/game/sce/sif_ee.cpp b/game/sce/sif_ee.cpp new file mode 100644 index 0000000000..e13e32d2d2 --- /dev/null +++ b/game/sce/sif_ee.cpp @@ -0,0 +1,87 @@ +#include +#include +#include "sif_ee.h" +#include "game/system/iop_thread.h" +#include "game/runtime.h" + +namespace ee { + +namespace { +::IOP* iop; +} + +void LIBRARY_sceSif_register(::IOP* i) { + iop = i; +} + +void LIBRARY_INIT_sceSif() { + iop = nullptr; +} +void sceSifInitRpc(unsigned int mode) { + (void)mode; +} + +int sceSifRebootIop(const char* imgfile) { + (void)imgfile; + return 1; +} + +int sceSifSyncIop() { + return 1; +} + +void sceFsReset() { + +} + +int sceSifLoadModule(const char* name, int arg_size, const char* args) { + if(!strcmp(name, "cdrom0:\\\\DRIVERS\\\\OVERLORD.IRX;1") || !strcmp(name, "host0:binee/overlord.irx")) { + const char* src = args; + char* dst = iop->overlord_arg_data; + int cnt; + iop->overlord_argv[0] = nullptr; + for(cnt = 1; src - args < arg_size; cnt++) { + auto len = strlen(src); + memcpy(dst, src, len + 1); + iop->overlord_argv[cnt] = dst; + dst += len + 1; + src += len + 1; + } + iop->overlord_argc = cnt; + + for(int i = 0; i < cnt; i++) { + if(iop->overlord_argv[i]) + printf("arg %d : %s\n", i, iop->overlord_argv[i]); + } + iop->set_ee_main_mem(g_ee_main_mem); + iop->send_status(IOP_Status::IOP_OVERLORD_INIT); + iop->wait_for_overlord_init_finish(); + } + + return 1; +} + +int sceMcInit() { + return 1; +} + +s32 sceSifCallRpc(sceSifClientData* bd, u32 fno, u32 mode, void* send, s32 ssize, void* recv, s32 rsize, void* end_func, void* end_para) { + assert(!end_func); + assert(!end_para); + assert(mode == 1); // async + iop->kernel.sif_rpc(bd->rpcd.id, fno, mode, send, ssize, recv, rsize); + return 0; +} + +s32 sceSifCheckStatRpc(sceSifRpcData* bd) { + iop->signal_run_iop(); + return iop->kernel.sif_busy(bd->id); +} + +s32 sceSifBindRpc(sceSifClientData* bd, u32 request, u32 mode) { + assert(mode == 1); // async + bd->rpcd.id = request; + bd->serve = (sceSifServeData*)1; + return 0; +} +} \ No newline at end of file diff --git a/game/sce/sif_ee.h b/game/sce/sif_ee.h new file mode 100644 index 0000000000..174e5d5590 --- /dev/null +++ b/game/sce/sif_ee.h @@ -0,0 +1,44 @@ +#ifndef JAK1_SIF_EE_H +#define JAK1_SIF_EE_H + +#include "common/common_types.h" + +class IOP; + +namespace ee { +struct sceSifRpcData { + u8 dummy; + u32 id; +}; + +struct sceSifServeData { + u8 dummy; +}; + +struct sceSifClientData { + sceSifRpcData rpcd; +// unsigned int command; + void *buff; + void *gp; +// sceSifEndFunc func; + void *para; +// struct _sif_serve_data *serve; + sceSifServeData *serve; +}; + + +void LIBRARY_sceSif_register(::IOP* i); +void LIBRARY_INIT_sceSif(); + +void sceSifInitRpc(unsigned int mode); +int sceSifRebootIop(const char* imgfile); +int sceSifSyncIop(); +void sceFsReset(); +int sceSifLoadModule(const char* name, int arg_size, const char* args); +int sceMcInit(); +s32 sceSifCallRpc(sceSifClientData* bd, u32 fno, u32 mode, void* send, s32 ssize, void* recv, s32 rsize, void* end_func, void* end_para); +s32 sceSifCheckStatRpc(sceSifRpcData* bd); +s32 sceSifBindRpc(sceSifClientData* bd, u32 request, u32 mode); + +} +#endif // JAK1_SIF_EE_H diff --git a/game/sce/stubs.cpp b/game/sce/stubs.cpp new file mode 100644 index 0000000000..4e218a4739 --- /dev/null +++ b/game/sce/stubs.cpp @@ -0,0 +1,101 @@ +#include +#include +#include "stubs.h" + +namespace ee { +s32 sceOpen(const char *filename, s32 flag) { + (void)filename; + (void)flag; + throw std::runtime_error("sceOpen NYI"); +} + +s32 sceClose(s32 fd) { + (void)fd; + throw std::runtime_error("sceClose NYI"); +} + +s32 sceRead(s32 fd, void *buf, s32 nbyte) { + (void)fd; + (void)buf; + (void)nbyte; + throw std::runtime_error("sceRead NYI"); +} + +s32 sceWrite(s32 fd, const void *buf, s32 nbyte) { + (void)fd; + (void)buf; + (void)nbyte; + throw std::runtime_error("sceWrite NYI"); +} + +s32 sceLseek(s32 fd, s32 offset, s32 where) { + (void)fd; + (void)offset; + (void)where; + throw std::runtime_error("sceLseek NYI"); +} + +int scePadPortOpen(int port, int slot, void* data) { + (void)port; + (void)slot; + (void)data; + assert(false); + return 0; +} + +void sceGsSyncV() { + assert(false); +} + +void sceGsSyncPath() { + assert(false); +} + +void sceGsResetPath() { + assert(false); +} + +void sceGsResetGraph() { + assert(false); +} + +void sceDmaSync() { + assert(false); +} + +void sceGsPutIMR() { + assert(false); +} + +void sceGsGetIMR() { + assert(false); +} + +void sceGsExecStoreImage() { + assert(false); +} + +void FlushCache() { + assert(false); +} + +} + +namespace iop { +u32 snd_BankLoadByLoc(u32 sector, u32 unk) { + (void)sector; + (void)unk; + assert(false); + return 0; +} + +u32 snd_GetLastLoadError() { + assert(false); + return 0; +} + +void snd_ResolveBankXREFS() { + assert(false); +} + +} diff --git a/game/sce/stubs.h b/game/sce/stubs.h new file mode 100644 index 0000000000..3ce2b5a781 --- /dev/null +++ b/game/sce/stubs.h @@ -0,0 +1,54 @@ +#ifndef JAK1_STUBS_H +#define JAK1_STUBS_H + +#include "common/common_types.h" + +#ifndef SCE_SEEK_SET +#define SCE_SEEK_SET (0) +#endif +#ifndef SCE_SEEK_CUR +#define SCE_SEEK_CUR (1) +#endif +#ifndef SCE_SEEK_END +#define SCE_SEEK_END (2) +#endif + +#define SCE_RDONLY 0x0001 +#define SCE_WRONLY 0x0002 +#define SCE_RDWR 0x0003 +#define SCE_NBLOCK 0x0010 +#define SCE_APPEND 0x0100 +#define SCE_CREAT 0x0200 +#define SCE_TRUNC 0x0400 +#define SCE_EXCL 0x0800 +#define SCE_NOBUF 0x4000 +#define SCE_NOWAIT 0x8000 + +#define SCE_PAD_DMA_BUFFER_SIZE 0x100 + +namespace ee { +s32 sceOpen(const char *filename, s32 flag); +s32 sceClose(s32 fd); +s32 sceRead(s32 fd, void *buf, s32 nbyte); +s32 sceWrite(s32 fd, const void *buf, s32 nbyte); +s32 sceLseek(s32 fd, s32 offset, s32 where); +void sceGsSyncV(); +void sceGsSyncPath(); +void sceGsResetPath(); +void sceGsResetGraph(); +void sceDmaSync(); +void sceGsPutIMR(); +void sceGsGetIMR(); +void sceGsExecStoreImage(); +void FlushCache(); +int scePadPortOpen(int port, int slot, void* data); +} + +namespace iop { +u32 snd_BankLoadByLoc(u32 sector, u32 unk); +u32 snd_GetLastLoadError(); +void snd_ResolveBankXREFS(); +} + + +#endif // JAK1_STUBS_H diff --git a/game/system/Deci2Server.cpp b/game/system/Deci2Server.cpp new file mode 100644 index 0000000000..9b2a29f942 --- /dev/null +++ b/game/system/Deci2Server.cpp @@ -0,0 +1,264 @@ +/*! + * @file Deci2Server.cpp + * Basic implementation of a DECI2 server. + * Works with deci2.cpp (sceDeci2) to implement the networking on target + */ + +#include +#include +#include +#include +#include +#include + +#include "common/listener_common.h" +#include "common/versions.h" +#include "Deci2Server.h" + +Deci2Server::Deci2Server(std::function shutdown_callback) { + buffer = new char[BUFFER_SIZE]; + want_exit = std::move(shutdown_callback); +} + +Deci2Server::~Deci2Server() { + // if accept thread is running, kill it + if (accept_thread_running) { + kill_accept_thread = true; + accept_thread.join(); + accept_thread_running = false; + } + + delete[] buffer; + + if (server_fd >= 0) { + close(server_fd); + } + + if (new_sock >= 0) { + close(new_sock); + } +} + +/*! + * Start waiting for the Listener to connect + */ +bool Deci2Server::init() { + server_fd = socket(AF_INET, SOCK_STREAM, 0); + if (server_fd < 0) { + server_fd = -1; + return false; + } + + int opt = 1; + if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { + printf("[Deci2Server] Failed to setsockopt 1\n"); + close(server_fd); + server_fd = -1; + return false; + } + + int one = 1; + if (setsockopt(server_fd, SOL_TCP, TCP_NODELAY, &one, sizeof(one))) { + printf("[Deci2Server] Failed to setsockopt 2\n"); + close(server_fd); + server_fd = -1; + return false; + } + + timeval timeout = {}; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + + if (setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)) < 0) { + printf("[Deci2Server] Failed to setsockopt 3\n"); + close(server_fd); + server_fd = -1; + return false; + } + + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(DECI2_PORT); + + if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { + printf("[Deci2Server] Failed to bind\n"); + close(server_fd); + server_fd = -1; + return false; + } + + if (listen(server_fd, 0) < 0) { + printf("[Deci2Server] Failed to listen\n"); + close(server_fd); + server_fd = -1; + return false; + } + + server_initialized = true; + accept_thread_running = true; + kill_accept_thread = false; + accept_thread = std::thread(&Deci2Server::accept_thread_func, this); + return true; +} + +/*! + * Return true if the listener is connected. + */ +bool Deci2Server::check_for_listener() { + if (server_connected) { + if (accept_thread_running) { + accept_thread.join(); + accept_thread_running = false; + } + return true; + } else { + return false; + } +} + +/*! + * Send data from buffer. User must provide appropriate headers. + */ +void Deci2Server::send_data(void* buf, u16 len) { + lock(); + if (!server_connected) { + printf("[DECI2] send while not connected, not sending!\n"); + } else { + uint16_t prog = 0; + while (prog < len) { + auto wrote = write(new_sock, (char*)(buf) + prog, len - prog); + prog += wrote; + if (!server_connected || want_exit()) { + unlock(); + return; + } + } + } + unlock(); +} + +/*! + * Lock the DECI mutex. Should be done before modifying protocols. + */ +void Deci2Server::lock() { + deci_mutex.lock(); +} + +/*! + * Unlock the DECI mutex. Should be done after modifying protocols. + */ +void Deci2Server::unlock() { + deci_mutex.unlock(); +} + +/*! + * Wait for protocols to become ready. + * This avoids the case where we receive messages before protocol handlers are set up. + */ +void Deci2Server::wait_for_protos_ready() { + if (protocols_ready) + return; + std::unique_lock lk(deci_mutex); + cv.wait(lk, [&] { return protocols_ready; }); +} + +/*! + * Inform server that protocol handlers are ready. + * Will unblock wait_for_protos_ready and incoming messages will be dispatched to these + * protocols. You can change the protocol handlers, but you should lock the mutex before + * doing so. + */ +void Deci2Server::send_proto_ready(Deci2Driver* drivers, int* driver_count) { + lock(); + d2_drivers = drivers; + d2_driver_count = driver_count; + protocols_ready = true; + unlock(); + cv.notify_all(); +} + +void Deci2Server::run() { + int desired_size = (int)sizeof(Deci2Header); + int got = 0; + + while (got < desired_size) { + assert(got + desired_size < BUFFER_SIZE); + auto x = read(new_sock, buffer + got, desired_size - got); + if (want_exit()) { + return; + } + got += x > 0 ? x : 0; + } + + auto* hdr = (Deci2Header*)(buffer); + printf("[DECI2] Got message:\n"); + printf(" %d %d 0x%x %c -> %c\n", hdr->len, hdr->rsvd, hdr->proto, hdr->src, hdr->dst); + + hdr->rsvd = got; + + // see what protocol we got: + lock(); + + int handler = -1; + for (int i = 0; i < *d2_driver_count; i++) { + auto& prot = d2_drivers[i]; + if (prot.active && prot.protocol) { + if (handler != -1) { + printf("[DECI2] Warning: more than on protocol handler for this message!\n"); + } + handler = i; + } + } + + if (handler == -1) { + printf("[DECI2] Warning: no handler for this message, ignoring...\n"); + unlock(); + return; + // throw std::runtime_error("no handler!"); + } + + auto& driver = d2_drivers[handler]; + + int sent_to_program = 0; + while (!want_exit() && (hdr->rsvd < hdr->len || sent_to_program < hdr->rsvd)) { + // send what we have to the program + if (sent_to_program < hdr->rsvd) { + // driver.next_recv_size = 0; + // driver.next_recv = nullptr; + driver.recv_buffer = buffer + sent_to_program; + driver.available_to_receive = hdr->rsvd - sent_to_program; + (driver.handler)(DECI2_READ, driver.available_to_receive, driver.opt); + // memcpy(driver.next_recv, buffer + sent_to_program, driver.next_recv_size); + sent_to_program += driver.recv_size; + } + + // receive from network + if (hdr->rsvd < hdr->len) { + auto x = read(new_sock, buffer + hdr->rsvd, hdr->len - hdr->rsvd); + if (want_exit()) { + return; + } + got += x > 0 ? x : 0; + hdr->rsvd += got; + } + } + + (driver.handler)(DECI2_READDONE, 0, driver.opt); + unlock(); +} + +/*! + * Background thread for waiting for the listener. + */ +void Deci2Server::accept_thread_func() { + socklen_t l = sizeof(addr); + while (!kill_accept_thread) { + new_sock = accept(server_fd, (sockaddr*)&addr, &l); + if (new_sock >= 0) { + u32 versions[2] = {versions::GOAL_VERSION_MAJOR, versions::GOAL_VERSION_MINOR}; + send(new_sock, &versions, 8, 0); // todo, check result? + server_connected = true; + return; + } + } +} diff --git a/game/system/Deci2Server.h b/game/system/Deci2Server.h new file mode 100644 index 0000000000..b53547a10c --- /dev/null +++ b/game/system/Deci2Server.h @@ -0,0 +1,56 @@ +/*! + * @file Deci2Server.h + * Basic implementation of a DECI2 server. + * Works with deci2.cpp (sceDeci2) to implement the networking on target + */ + +#ifndef JAK1_DECI2SERVER_H +#define JAK1_DECI2SERVER_H + +#include +#include +#include +#include +#include +#include "game/system/deci_common.h" + +class Deci2Server { + public: + static constexpr int BUFFER_SIZE = 32 * 1024 * 1024; + Deci2Server(std::function shutdown_callback); + ~Deci2Server(); + bool init(); + bool check_for_listener(); + void send_data(void* buf, u16 len); + + void lock(); + void unlock(); + void wait_for_protos_ready(); + void send_proto_ready(Deci2Driver* drivers, int* driver_count); + + void run(); + + + private: + void accept_thread_func(); + bool kill_accept_thread = false; + char* buffer = nullptr; + int server_fd; + sockaddr_in addr; + int new_sock; + bool server_initialized = false; + bool accept_thread_running = false; + bool server_connected = false; + std::function want_exit; + std::thread accept_thread; + + std::condition_variable cv; + bool protocols_ready = false; + std::mutex deci_mutex; + Deci2Driver* d2_drivers = nullptr; + int* d2_driver_count = nullptr; +}; + + + +#endif // JAK1_DECI2SERVER_H diff --git a/game/system/IOP_Kernel.cpp b/game/system/IOP_Kernel.cpp new file mode 100644 index 0000000000..ffa2a9cbad --- /dev/null +++ b/game/system/IOP_Kernel.cpp @@ -0,0 +1,314 @@ +#include +#include +#include "IOP_Kernel.h" +#include "game/sce/iop.h" + +/*! + * Create a new thread. Will not run the thread. + */ +s32 IOP_Kernel::CreateThread(std::string name, u32 (*func)()) { + if(_currentThread != -1) throw std::runtime_error("tried to create thread from thread"); + u32 ID = (u32)_nextThID++; + if(threads.size() != ID) throw std::runtime_error("thread number error?"); + // add entry + threads.emplace_back(name, func, ID, this); + // setup the thread! + // printf("[IOP Kernel] SetupThread %s...\n", name.c_str()); + + // hack to allow creating a "null thread" which doesn't/can't run but occupies slot 0. + if(func) { + _currentThread = ID; + // create OS thread, will run the setupThread function + threads.back().thread = new std::thread(&IOP_Kernel::setupThread, this, ID); + // wait for thread to finish setup. + threads.back().waitForReturnToKernel(); + // ensure we are back in the kernel. + _currentThread = -1; + } + + + return ID; +} + +/*! + * Start a thread. Runs it once, then marks it to run on each dispatch of the IOP kernel. + */ +void IOP_Kernel::StartThread(s32 id) { + threads.at(id).started = true; // mark for run + runThread(id); // run now +} + +/*! + * Wrapper around entry for a thread. + */ +void IOP_Kernel::setupThread(s32 id) { + // printf("\tthread %s has started!\n", threads.at(id).name.c_str()); + returnToKernel(); + threads.at(id).waitForDispatch(); + // printf("[IOP Kernel] Thread %s first dispatch!\n", threads.at(id).name.c_str()); + if(_currentThread != id) { + throw std::runtime_error("the wrong thread has run!\n"); + } + (threads.at(id).function)(); + printf("Thread %s has returned!\n", threads.at(id).name.c_str()); + threads.at(id).done = true; + returnToKernel(); +} + +/*! + * Run a thread (call from kernel) + */ +void IOP_Kernel::runThread(s32 id) { + if(_currentThread != -1) throw std::runtime_error("tried to runThread in a thread"); + _currentThread = id; + threads.at(id).dispatch(); + threads.at(id).waitForReturnToKernel(); + _currentThread = -1; +} + +/*! + * Suspend a thread (call from user thread). Will simply allow other threads to run. + * Unless we are sleeping, in which case this will return when we are woken up + * Like yield + */ +void IOP_Kernel::SuspendThread() { + s32 oldThread = getCurrentThread(); + threads.at(oldThread).returnToKernel(); + threads.at(oldThread).waitForDispatch(); + if(_currentThread != oldThread) { + throw std::runtime_error("bad resume"); + } +} + +/*! + * Sleep a thread. Must be explicitly woken up. + */ +void IOP_Kernel::SleepThread() { + if(getCurrentThread() == -1) { + mainThreadSleep = true; + while(mainThreadSleep) { + dispatchAll(); + } + } else { + threads.at(getCurrentThread()).started = false; + SuspendThread(); + } +} + +/*! + * Wake up a thread. Doesn't run it immediately though. + */ +void IOP_Kernel::WakeupThread(s32 id) { + if(id == -1) { + mainThreadSleep = false; + } else { + threads.at(id).started = true; + } + // todo, should we ever switch directly to that thread? +} + +/*! + * Dispatch all IOP threads. + */ +void IOP_Kernel::dispatchAll() { + for(u64 i = 0; i < threads.size(); i++) { + if(threads[i].started && !threads[i].done) { +// printf("[IOP Kernel] Dispatch %s (%ld)\n", threads[i].name.c_str(), i); + _currentThread = i; + threads[i].dispatch(); + threads[i].waitForReturnToKernel(); + _currentThread = -1; + //printf("[IOP Kernel] back to kernel!\n"); + } + } +} + +/*! + * Start running kernel. + */ +void IopThreadRecord::returnToKernel() { + runThreadReady = false; + if(kernel->getCurrentThread() != thID) throw std::runtime_error("tried to sleep the wrong thread!"); + + { + std::lock_guard lck(*threadToKernelMutex); + syscallReady = true; + } + threadToKernelCV->notify_one(); +} + +/*! + * Start running thread. + */ +void IopThreadRecord::dispatch() { + syscallReady = false; + if(kernel->getCurrentThread() != thID) throw std::runtime_error("tried to dispatch the wrong thread!"); + { + std::lock_guard lck(*kernelToThreadMutex); + runThreadReady = true; + } + kernelToThreadCV->notify_one(); +} + +/*! + * Kernel waits for thread to return + */ +void IopThreadRecord::waitForReturnToKernel() { + std::unique_lock lck(*threadToKernelMutex); + threadToKernelCV->wait(lck, [this]{return syscallReady;}); +// syscallReady = false; +} + +/*! + * Thread waits for kernel to dispatch it. + */ +void IopThreadRecord::waitForDispatch() { + //if(kernel->getCurrentThread() == -1) throw std::runtime_error("tried to suspend main!\n"); + std::unique_lock lck(*kernelToThreadMutex); + kernelToThreadCV->wait(lck, [this]{return runThreadReady;}); + //runThreadReady = false; +} + +void IOP_Kernel::set_rpc_queue(iop::sceSifQueueData *qd, u32 thread) { + for(const auto& r : sif_records) { + assert(!(r.qd == qd || r.thread_to_wake == thread)); + } + SifRecord rec; + rec.thread_to_wake = thread; + rec.qd = qd; + sif_records.push_back(rec); +} + +typedef void * (* sif_rpc_handler)(unsigned int,void *,int); + +bool IOP_Kernel::sif_busy(u32 id) { + sif_mtx.lock(); + bool rv = false; + bool found = false; + for(auto& r : sif_records) { + if(r.qd->serve_data->command == id) { + rv = !r.cmd.finished; + found = true; + break; + } + } + assert(found); + sif_mtx.unlock(); + return rv; +} + +void IOP_Kernel::sif_rpc(s32 rpcChannel, u32 fno, bool async, void *sendBuff, s32 sendSize, void *recvBuff, + s32 recvSize) { + assert(async); + sif_mtx.lock(); + // step 1 - find entry + SifRecord* rec = nullptr; + for(auto& e : sif_records) { + if(e.qd->serve_data->command == (u32)rpcChannel) { + rec = &e; + } + } + assert(rec); + + // step 2 - check entry is safe to give command to + assert(rec->cmd.finished && rec->cmd.started); + + // step 3 - memcpy! + memcpy(rec->qd->serve_data->buff, sendBuff, sendSize); + + // step 4 - setup command + rec->cmd.buff = rec->qd->serve_data->buff; + rec->cmd.size = sendSize; + rec->cmd.fno = fno; + rec->cmd.copy_back_buff = recvBuff; + rec->cmd.copy_back_size = recvSize; + rec->cmd.started = false; + rec->cmd.finished = false; + + sif_mtx.unlock(); +} + +void IOP_Kernel::rpc_loop(iop::sceSifQueueData* qd) { + while(true) { + bool got_cmd = false; + SifRpcCommand cmd; + sif_rpc_handler func = nullptr; + + // get command and mark it as started if we get it + sif_mtx.lock(); + for(auto& r : sif_records) { + if(r.qd == qd) { + cmd = r.cmd; + got_cmd = true; + r.cmd.started = true; + func = r.qd->serve_data->func; + } + } + sif_mtx.unlock(); + + // handle command + if(got_cmd) { + if(cmd.shutdown_now) { + return; + } + + if(!cmd.started) { + // cf + assert(func); + auto data = func(cmd.fno, cmd.buff, cmd.size); + if(cmd.copy_back_buff && cmd.copy_back_size) { + memcpy(cmd.copy_back_buff, data, cmd.copy_back_size); + } + + sif_mtx.lock(); + for(auto& r : sif_records) { + if(r.qd == qd) { + assert(r.cmd.started); + r.cmd.finished = true; + } + } + sif_mtx.unlock(); + + } + } + SuspendThread(); + } +} + +void IOP_Kernel::read_disc_sectors(u32 sector, u32 sectors, void *buffer) { + if(!iso_disc_file) { + iso_disc_file = fopen("./disc.iso", "rb"); + } + + assert(iso_disc_file); + if(fseek(iso_disc_file, sector * 0x800, SEEK_SET)) { + assert(false); + } + auto rv = fread(buffer, sectors * 0x800, 1, iso_disc_file); + assert(rv == 1); +} + +void IOP_Kernel::shutdown() { + // shutdown most threads + for(auto& r : sif_records) { + r.cmd.shutdown_now = true; + } + + for(auto& t : threads) { + t.wantExit = true; + } + + for(auto& t : threads) { + if(t.thID == 0) continue; + while(!t.done) { + dispatchAll(); + } + t.thread->join(); + } +} + +IOP_Kernel::~IOP_Kernel() { + if(iso_disc_file) { + fclose(iso_disc_file); + } +} \ No newline at end of file diff --git a/game/system/IOP_Kernel.h b/game/system/IOP_Kernel.h new file mode 100644 index 0000000000..d05083dbc8 --- /dev/null +++ b/game/system/IOP_Kernel.h @@ -0,0 +1,180 @@ +#ifndef JAK_IOP_KERNEL_H +#define JAK_IOP_KERNEL_H + +#include +#include +#include +#include +#include +#include +#include +#include "common/common_types.h" + +class IOP_Kernel; +namespace iop { + struct sceSifQueueData; +} + +struct SifRpcCommand { + bool started = true; + bool finished = true; + bool shutdown_now = false; + + void* buff; + int fno; + int size; + + void* copy_back_buff; + int copy_back_size; +}; + + +struct SifRecord { + iop::sceSifQueueData* qd; + SifRpcCommand cmd; + u32 thread_to_wake; +}; + +struct IopThreadRecord { + IopThreadRecord(std::string n, u32 (*f)(), s32 ID, IOP_Kernel* k) : name(n), function(f), thID(ID), kernel(k) { + kernelToThreadCV = new std::condition_variable; + threadToKernelCV = new std::condition_variable; + kernelToThreadMutex = new std::mutex; + threadToKernelMutex = new std::mutex; + } + + + ~IopThreadRecord() { + delete kernelToThreadCV; + delete threadToKernelCV; + delete kernelToThreadMutex; + delete threadToKernelMutex; + delete thread; + } + + std::string name; + u32 (*function)(); + std::thread* thread = nullptr; + bool wantExit = false; + bool started = false; + bool done = false; + s32 thID = -1; + IOP_Kernel* kernel; + + bool runThreadReady = false; + bool syscallReady = false; + std::mutex *kernelToThreadMutex, *threadToKernelMutex; + std::condition_variable *kernelToThreadCV, *threadToKernelCV; + + void returnToKernel(); + void waitForReturnToKernel(); + void waitForDispatch(); + void dispatch(); +}; + + +class IOP_Kernel { +public: + IOP_Kernel() { + // this ugly hack + threads.reserve(16); + CreateThread("null-thread", nullptr); + CreateMbx(); + } + + ~IOP_Kernel(); + + s32 CreateThread(std::string n, u32 (*f)()); + void StartThread(s32 id); + void SuspendThread(); + void SleepThread(); + void WakeupThread(s32 id); + void dispatchAll(); + void set_rpc_queue(iop::sceSifQueueData *qd, u32 thread); + void rpc_loop(iop::sceSifQueueData* qd); + void shutdown(); + + /*! + * Resume the kernel. + */ + void returnToKernel() { + if(_currentThread < 0) throw std::runtime_error("tried to return to kernel not in a thread"); + threads[_currentThread].returnToKernel(); + } + + /*! + * Get current thread ID. + */ + s32 getCurrentThread() { + return _currentThread; + } + + + /*! + * Create a message box + */ + s32 CreateMbx() { + s32 id = mbxs.size(); + mbxs.emplace_back(); + return id; + } + + /*! + * Set msg to thing if its there and pop it. + * Returns if it got something. + */ + s32 PollMbx(void** msg, s32 mbx) { + if(_currentThread != -1 && threads.at(_currentThread).wantExit) { + // total hack - returning this value causes the ISO thread to error out and quit. + return -0x1a9; + } +// printf("poll %d %ld\n", mbx, mbxs.size()); + if(mbx >= (s32) mbxs.size()) throw std::runtime_error("invalid PollMbx"); + s32 gotSomething = mbxs[mbx].empty() ? 0 : 1; + if(gotSomething) { + void* thing = mbxs[mbx].front(); +// printf("pop from msgbox %d %p\n", mbx, thing); + if(msg) + *msg = thing; + mbxs[mbx].pop(); + } + + return gotSomething ? 0 : -424; + } + + /*! + * Push something into a mbx + */ + s32 SendMbx(s32 mbx, void* value) { + if(mbx >= (s32) mbxs.size()) throw std::runtime_error("invalid SendMbx"); + mbxs[mbx].push(value); +// printf("push into messagebox %d %p\n", mbx, value); +// printf("mbx size %ld\n", mbxs.size()); + return 0; + } + + s32 CreateSema() { + return 1; + } + + void read_disc_sectors(u32 sector, u32 sectors, void* buffer); + bool sif_busy(u32 id); + + void sif_rpc(s32 rpcChannel, u32 fno, bool async, void *sendBuff, s32 sendSize, void *recvBuff, s32 recvSize); + + +private: + void setupThread(s32 id); + void runThread(s32 id); + s32 _nextThID = 0; + std::atomic _currentThread = {-1}; + std::vector threads; + std::vector> mbxs; + std::vector sif_records; + bool mainThreadSleep = false; + FILE* iso_disc_file = nullptr; + std::mutex sif_mtx; +}; + + +#endif //JAK_IOP_KERNEL_H diff --git a/game/system/SystemThread.cpp b/game/system/SystemThread.cpp new file mode 100644 index 0000000000..aa49932491 --- /dev/null +++ b/game/system/SystemThread.cpp @@ -0,0 +1,167 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "SystemThread.h" + +////////////////////// +// Thread Manager // +////////////////////// + +/*! + * Create a new thread with the given name. + */ +SystemThread& SystemThreadManager::create_thread(const std::string& name) { + if (thread_count >= MAX_SYSTEM_THREADS) { + throw std::runtime_error("Out of System Threads! Please increase MAX_SYSTEM_THREADS"); + } + auto& thread = threads[thread_count]; + + // reset thread + thread.initialization_complete = false; + thread.name = name; + thread.id = thread_count; + thread.manager = this; + thread_count++; + + return thread; +} + +/*! + * Print the CPU usage statistics for all threads. + */ +void SystemThreadManager::print_stats() { + double total_user = 0, total_kernel = 0; + printf("%8s | %5s | %5s\n", "Name", "User", "Kernel"); + printf("--------------------------\n"); + for (int id = 0; id < thread_count; id++) { + auto& thread = threads[id]; + printf("%8s | %5.1f | %5.1f\n", thread.name.c_str(), thread.cpu_user * 100., + thread.cpu_kernel * 100.); + total_kernel += thread.cpu_kernel; + total_user += thread.cpu_user; + } + printf("%8s | %5.1f | %5.1f\n\n", "#TOTAL#", total_user * 100., total_kernel * 100.); +} + +/*! + * Request all threads to stop + */ +void SystemThreadManager::shutdown() { + for (int i = 0; i < thread_count; i++) { + printf("# Stop %s\n", threads[i].name.c_str()); + threads[i].stop(); + } +} + +/*! + * Join all threads, if they are running + */ +void SystemThreadManager::join() { + for (int i = 0; i < thread_count; i++) { + printf("# Join %s\n", threads[i].name.c_str()); + if (threads[i].running) { + threads[i].join(); + } + } +} + +/*! + * bootstrap function to call a SystemThread's function + */ +void* bootstrap_thread_func(void* x) { + SystemThread* thd = (SystemThread*)x; + SystemThreadInterface interface(thd); + thd->function(interface); + printf("[SYSTEM] Thread %s is returning\n", thd->name.c_str()); + return nullptr; +} + +/*! + * Start a thread and wait for its initialization + */ +void SystemThread::start(std::function f) { + printf("# Initialize %s...\n", name.c_str()); + function = f; + pthread_create(&thread, nullptr, bootstrap_thread_func, this); + running = true; + + // and wait for initialization + { + std::unique_lock mlk(initialization_mutex); + while (!initialization_complete) { + initialization_cv.wait(mlk); + } + } +} + +/*! + * Join a system thread + */ +void SystemThread::join() { + void* x; + pthread_join(thread, &x); + running = false; +} + +/*! + * Set flag in system thread so want_exit() returns true. + */ +void SystemThread::stop() { + want_exit = true; +} + +/*! + * Signal from a thread that initialization has complete, and the caller of SystemThread::start() + * will be unblocked. + */ +void SystemThreadInterface::initialization_complete() { + std::unique_lock mlk(thread.initialization_mutex); + thread.initialization_complete = true; + thread.initialization_cv.notify_all(); + printf(" OK\n"); +} + +/*! + * Should we try and exit? + */ +bool SystemThreadInterface::get_want_exit() const { + return thread.want_exit; +} + +/*! + * Trigger a full system shutdown. + */ +void SystemThreadInterface::trigger_shutdown() { + thread.manager->shutdown(); +} + +#include +#include + +/*! + * Get thread performance statistics and report them. + */ +void SystemThreadInterface::report_perf_stats() { + if (thread.stat_diff_timer.getMs() > 16.f) { + thread.stat_diff_timer.start(); + + uint64_t current_ns = thread.stats_timer.getNs(); + rusage stats; + getrusage(RUSAGE_THREAD, &stats); + + uint64_t current_kernel = stats.ru_stime.tv_usec + (1000000 * stats.ru_stime.tv_sec); + uint64_t current_user = stats.ru_utime.tv_usec + (1000000 * stats.ru_utime.tv_sec); + + uint64_t ns_dt = current_ns - thread.last_collection_nanoseconds; + uint64_t dt_kernel = current_kernel - thread.last_cpu_kernel; + uint64_t dt_user = current_user - thread.last_cpu_user; + + thread.cpu_kernel = dt_kernel * 1000. / (double)ns_dt; + thread.cpu_user = dt_user * 1000. / (double)ns_dt; + + thread.last_cpu_kernel = current_kernel; + thread.last_cpu_user = current_user; + thread.last_collection_nanoseconds = current_ns; + } +} \ No newline at end of file diff --git a/game/system/SystemThread.h b/game/system/SystemThread.h new file mode 100644 index 0000000000..0d0bafac0d --- /dev/null +++ b/game/system/SystemThread.h @@ -0,0 +1,89 @@ +/*! + * @file SystemThread.h + * Threads for the runtime. + */ + +#ifndef RUNTIME_SYSTEMTHREAD_H +#define RUNTIME_SYSTEMTHREAD_H + +#include +#include +#include +#include +#include +#include +#include "Timer.h" + + +constexpr int MAX_SYSTEM_THREADS = 16; + +class SystemThreadInterface; +class SystemThreadManager; + +/*! + * Runs a function in a thread and provides a SystemThreadInterface to that function. + * Once the thread is ready, it should tell the interface with intitialization_complete(). + * Thread functions should try to return when get_want_exit() returns true. + * Thread functions should also call report_perf_stats every now and then to update performance + * statistics. + */ +class SystemThread { +public: + void start(std::function f); + void join(); + void stop(); + SystemThread() = default; + +private: + friend class SystemThreadInterface; + friend class SystemThreadManager; + friend void* bootstrap_thread_func(void* thd); + + std::string name = "invalid"; + pthread_t thread; + SystemThreadManager* manager; + std::function function; + bool initialization_complete = false; + std::mutex initialization_mutex; + std::condition_variable initialization_cv; + Timer stats_timer; + Timer stat_diff_timer; + double cpu_user = 0, cpu_kernel = 0; + uint64_t last_cpu_user = 0, last_cpu_kernel = 0; + uint64_t last_collection_nanoseconds = 0; + int id = -1; + bool want_exit = false; + bool running = false; +}; + +/*! + * The interface used by a thread in the runtime. + */ +class SystemThreadInterface { +public: + SystemThreadInterface(SystemThread* p) : thread(*p) { + + } + void initialization_complete(); + void report_perf_stats(); + bool get_want_exit() const; + void trigger_shutdown(); +private: + SystemThread& thread; +}; + +/*! + * A manager of all threads in the runtime. + */ +class SystemThreadManager { +public: + SystemThread& create_thread(const std::string& name); + void print_stats(); + void shutdown(); + void join(); +private: + std::array threads; + int thread_count = 0; +}; + +#endif //RUNTIME_SYSTEMTHREAD_H diff --git a/game/system/Timer.h b/game/system/Timer.h new file mode 100644 index 0000000000..21bfbce8ce --- /dev/null +++ b/game/system/Timer.h @@ -0,0 +1,37 @@ +#ifndef RUNTIME_TIMER_H +#define RUNTIME_TIMER_H + +#include +#include +#include + +class Timer { +public: + explicit Timer() { + start(); + } + + void start() { + clock_gettime(CLOCK_MONOTONIC, &_startTime); + } + + double getMs() { + return (double)getNs() / 1.e6; + } + + int64_t getNs() { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (int64_t)(now.tv_nsec - _startTime.tv_nsec) + 1000000000 * (now.tv_sec - _startTime.tv_sec); + + } + + double getSeconds() { + return (double)getNs() / 1.e9; + } + + struct timespec _startTime; +}; + + +#endif //RUNTIME_TIMER_H diff --git a/game/system/deci_common.h b/game/system/deci_common.h new file mode 100644 index 0000000000..b3f7248064 --- /dev/null +++ b/game/system/deci_common.h @@ -0,0 +1,23 @@ +#ifndef JAK_DECIM_COMMON_H +#define JAK_DECIM_COMMON_H +#include "common/common_types.h" +struct Deci2Driver { + u16 protocol = 0; + void* opt = nullptr; + void (*handler)(s32 event, s32 param, void *opt) = nullptr; + u8 id = 0; + bool active = false; + void* recv_buffer = nullptr; + int recv_size = 0; + int available_to_receive = 0; + char pending_send = 0; +}; + +// handler event values +#define DECI2_READ 1 +#define DECI2_READDONE 2 +#define DECI2_WRITE 3 +#define DECI2_WRITEDONE 4 +#define DECI2_CHSTATUS 5 + +#endif // JAK_DECIM_COMMON_H diff --git a/game/system/iop_thread.cpp b/game/system/iop_thread.cpp new file mode 100644 index 0000000000..1a07a5b1b5 --- /dev/null +++ b/game/system/iop_thread.cpp @@ -0,0 +1,153 @@ +#include "iop_thread.h" + +#include +#include "SystemThread.h" +//#include "shared_config.h" +//#include "ps2/SCE_IOP.h" +//#include "overlord/ramdisk.h" +//#include "ps2/SCE_SIF.h" +//#include "IOP.h" +// +//#include "overlord/dma.h" +//#include "overlord/fake_iso.h" +//#include "overlord/iso.h" +//#include "overlord/iso_api.h" +//#include "overlord/iso_cd.h" +//#include "overlord/iso_queue.h" +//#include "overlord/isocommon.h" +//#include "overlord/overlord.h" +//#include "overlord/ramdisk.h" +//#include "overlord/sbank.h" +//#include "overlord/soundcommon.h" +//#include "overlord/srpc.h" +//#include "overlord/ssound.h" +//#include "overlord/stream.h" + + +IOP::IOP() { + +} + +void IOP::send_status(IOP_Status new_status) { + { + std::lock_guard lck(iop_mutex); + status = new_status; + } + cv.notify_all(); +} + +void IOP::wait_for_overlord_start_cmd() { + std::unique_lock lk(iop_mutex); + if(status != IOP_WAIT_FOR_LOAD) return; + + cv.wait(lk, [&]{return status != IOP_WAIT_FOR_LOAD;}); +} + +void IOP::wait_for_overlord_init_finish() { + std::unique_lock lk(iop_mutex); + if(overlord_init_done) return; + + cv.wait(lk, [&]{return overlord_init_done;}); +} + +void IOP::signal_overlord_init_finish() { + std::unique_lock lk(iop_mutex); + overlord_init_done = true; + cv.notify_all(); +} + +void IOP::reset_allocator() { + for(auto x : allocations) { + free(x); + } + allocations.clear(); +} + +void* IOP::iop_alloc(int size) { + void* mem = malloc(size); + allocations.push_back(mem); + return mem; +} + +void IOP::wait_run_iop() { + std::unique_lock lk(iters_mutex); + if(iop_iters_des > iop_iters_act) { + iop_iters_act++; + return; + } + + iop_run_cv.wait(lk, [&]{return iop_iters_des > iop_iters_act;}); + iop_iters_act++; +} + +void IOP::kill_from_ee() { + want_exit = true; + signal_run_iop(); +} + +void IOP::signal_run_iop() { + std::unique_lock lk(iters_mutex); + iop_iters_des += 100; // todo, tune this + iop_run_cv.notify_all(); +} + +IOP::~IOP() { + reset_allocator(); +} + +//void launch_iop(SystemThreadInterface& interface) { +// IOP iop; +// +// printf("\n\n\n[IOP] Restart!\n"); +// iop.reset_allocator(); +// +//// dma_init_globals(); +//// iso_init_globals(); +//// fake_iso_init_globals(); +//// // iso_api +//// iso_cd_init_globals(); +//// iso_queue_init_globals(); +//// // isocommon +//// // overlord +//// ramdisk_init_globals(); +//// // sbank +//// // soundcommon +//// srpc_init_globals(); +// // ssound +// // stream +// +//// SCE_IOP::PS2_RegisterIOP(&iop); +//// PS2_RegisterIOP_EE(&iop); +// interface.initialization_complete(); +// +// printf("[IOP] Wait for OVERLORD to be started...\n"); +// iop.wait_for_overlord_start_cmd(); +// if(iop.status == IOP_OVERLORD_INIT) { +// printf("[IOP] Run!\n"); +// } else { +// printf("[IOP] shutdown!\n"); +// return; +// } +// +// iop.reset_allocator(); +// +// // init +//#ifdef ENABLE_OVERLORD +// start(iop.overlord_argc, iop.overlord_argv); +//#endif +// +// // unblock the EE, the overlord is set up! +// iop.signal_overlord_init_finish(); +// +// // IOP Kernel loop +// while(!interface.get_want_exit() && !iop.want_exit) { +// // the IOP kernel just runs at full blast, so we only run the IOP when the EE is waiting on the IOP. +// // Each time the EE is waiting on the IOP, it will run an iteration of the IOP kernel. +// iop.wait_run_iop(); +// iop.kernel.dispatchAll(); +// } +// +// // stop all threads in the iop kernel. +// // if the threads are not stopped nicely, we will deadlock on trying to destroy the kernel's condition variables. +// iop.kernel.shutdown(); +//} \ No newline at end of file diff --git a/game/system/iop_thread.h b/game/system/iop_thread.h new file mode 100644 index 0000000000..96c42867ae --- /dev/null +++ b/game/system/iop_thread.h @@ -0,0 +1,51 @@ +#ifndef JAK1_IOP_THREAD_H +#define JAK1_IOP_THREAD_H + +#include "common/common_types.h" +#include "IOP_Kernel.h" + +enum IOP_Status { + IOP_WAIT_FOR_LOAD, + IOP_OVERLORD_INIT, + IOP_OVERLORD_RUN, + IOP_OVERLORD_STOP +}; + +class IOP { + public: + IOP(); + ~IOP(); + void* iop_alloc(int size); + void reset_allocator(); + void send_status(IOP_Status new_status); + void wait_for_overlord_start_cmd(); + void wait_for_overlord_init_finish(); + void signal_overlord_init_finish(); + void signal_run_iop(); + void wait_run_iop(); + void kill_from_ee(); + + void set_ee_main_mem(u8* mem) { + ee_main_mem = mem; + } + + IOP_Status status = IOP_WAIT_FOR_LOAD; + + char overlord_arg_data[512]; + char* overlord_argv[8]; + s32 overlord_argc; + + IOP_Kernel kernel; + u8* ee_main_mem = nullptr; + u64 iop_iters_des = 0; + u64 iop_iters_act = 0; + bool want_exit = false; + private: + std::vector allocations; + std::condition_variable cv; + std::mutex iop_mutex, iters_mutex; + bool overlord_init_done = false; + std::condition_variable iop_run_cv; +}; + +#endif // JAK1_IOP_THREAD_H diff --git a/gc.sh b/gc.sh new file mode 100755 index 0000000000..19ab0e192f --- /dev/null +++ b/gc.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Directory of this script +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +export NEXT_DIR=$DIR +$DIR/build/goalc/goalc "$@" \ No newline at end of file diff --git a/gk.sh b/gk.sh new file mode 100755 index 0000000000..f33bbcd06f --- /dev/null +++ b/gk.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Directory of this script +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +export NEXT_DIR=$DIR +$DIR/build/game/gk "$@" diff --git a/goalc/CMakeLists.txt b/goalc/CMakeLists.txt new file mode 100644 index 0000000000..562b73af64 --- /dev/null +++ b/goalc/CMakeLists.txt @@ -0,0 +1,8 @@ +add_subdirectory(util) +add_subdirectory(goos) +add_subdirectory(listener) +add_subdirectory(emitter) + +add_executable(goalc main.cpp) + +target_link_libraries(goalc util goos) \ No newline at end of file diff --git a/goalc/emitter/CMakeLists.txt b/goalc/emitter/CMakeLists.txt new file mode 100644 index 0000000000..fde45f4cb7 --- /dev/null +++ b/goalc/emitter/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(emitter + CodeTester.cpp + registers.cpp) \ No newline at end of file diff --git a/goalc/emitter/CodeTester.cpp b/goalc/emitter/CodeTester.cpp new file mode 100644 index 0000000000..6a9a9a6e6f --- /dev/null +++ b/goalc/emitter/CodeTester.cpp @@ -0,0 +1,81 @@ +#include +#include +#include "CodeTester.h" +#include "Instruction.h" +#include "IGen.h" + +namespace goal { + +std::string CodeTester::dump_to_hex_string() { + std::string result; + char buff[32]; + for (int i = 0; i < code_buffer_size; i++) { + sprintf(buff, "%02x ", code_buffer[i]); + result += buff; + } + + // remove trailing space + if (!result.empty()) { + result.pop_back(); + } + return result; +} + +void CodeTester::emit(const Instruction& instr) { + code_buffer_size += instr.emit(code_buffer + code_buffer_size); + assert(code_buffer_size <= code_buffer_capacity); +} + +void CodeTester::emit_set_gpr_as_return(X86R gpr) { + assert(is_gpr(gpr)); + emit(IGen::mov_gpr64_gpr64(RAX, gpr)); +} + +void CodeTester::emit_return() { + emit(IGen::ret()); +} + +void CodeTester::emit_pop_all_gprs(bool exclude_rax) { + for(int i = 16; i-- > 0;) { + if(i != RAX || !exclude_rax) { + emit(IGen::pop_gpr64(i)); + } + } +} + +void CodeTester::emit_push_all_gprs(bool exclude_rax) { + for (int i = 0; i < 16; i++) { + if(i != RAX || !exclude_rax) { + emit(IGen::push_gpr64(i)); + } + + } +} + +void CodeTester::clear() { + code_buffer_size = 0; +} + +u64 CodeTester::execute() { + return ((u64(*)())code_buffer)(); +} + +void CodeTester::init_code_buffer(int capacity) { + code_buffer = (u8*)mmap(nullptr, capacity, PROT_EXEC | PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); + if (code_buffer == (u8*)(-1)) { + printf("[CodeTester] Failed to map memory!\n"); + assert(false); + } + + code_buffer_capacity = capacity; + code_buffer_size = 0; +} + +CodeTester::~CodeTester() { + if (code_buffer_capacity) { + munmap(code_buffer, code_buffer_capacity); + } +} + +} // namespace goal \ No newline at end of file diff --git a/goalc/emitter/CodeTester.h b/goalc/emitter/CodeTester.h new file mode 100644 index 0000000000..4ea7eb95d3 --- /dev/null +++ b/goalc/emitter/CodeTester.h @@ -0,0 +1,37 @@ +/*! + * @file CodeTester + * CodeTester is a utility which allows small segments of x86 code to be run, for the purpose of + * testing the compiler's code emitter. It is not suitable for testing compiled GOAL code. + */ + +#ifndef JAK1_CODETESTER_H +#define JAK1_CODETESTER_H + +#include +#include "common/common_types.h" +#include "registers.h" +#include "Instruction.h" + +namespace goal { +class CodeTester { + public: + std::string dump_to_hex_string(); + void init_code_buffer(int capacity); + void emit_push_all_gprs(bool exclude_rax = false); + void emit_pop_all_gprs(bool exclude_rax = false); + void emit_return(); + void emit_set_gpr_as_return(X86R gpr); + void emit(const Instruction& instr); + u64 execute(); + void clear(); + ~CodeTester(); + private: + + + int code_buffer_size = 0; + int code_buffer_capacity = 0; + u8* code_buffer = nullptr; +}; +} // namespace goal + +#endif // JAK1_CODETESTER_H diff --git a/goalc/emitter/IGen.h b/goalc/emitter/IGen.h new file mode 100644 index 0000000000..7c1a206e7f --- /dev/null +++ b/goalc/emitter/IGen.h @@ -0,0 +1,747 @@ +#ifndef JAK1_IGEN_H +#define JAK1_IGEN_H + +#include +#include "Instruction.h" +#include "registers.h" + +namespace goal { +class IGen { + public: + //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // MOVES + //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + /*! + * mov gpr, gpr, 64 bits + */ + static Instruction mov_gpr64_gpr64(uint8_t dst, uint8_t src) { + assert(is_gpr(dst)); + assert(is_gpr(src)); + Instruction instr(0x89); + instr.set_modrm_and_rex(src, dst, 3, true); + return instr; + } + + /*! + * Move a 64-bit constant into a register. + */ + static Instruction mov_gpr64_u64(uint8_t dst, uint64_t val) { + assert(is_gpr(dst)); + bool rex_b = false; + if (dst >= 8) { + dst -= 8; + rex_b = true; + } + Instruction instr(0xb8 + dst); + instr.set(REX(true, false, false, rex_b)); + instr.set(Imm(8, val)); + return instr; + } + + /*! + * Move a 32-bit constant into a register. + */ + static Instruction mov_gpr64_u32(uint8_t dst, uint64_t val) { + assert(val <= UINT32_MAX); + assert(is_gpr(dst)); + bool rex_b = false; + if (dst >= 8) { + dst -= 8; + rex_b = true; + } + + Instruction instr(0xb8 + dst); + if (rex_b) { + instr.set(REX(false, false, false, rex_b)); + } + instr.set(Imm(4, val)); + return instr; + } + + /*! + * Move a signed 32-bit constant into a register. + * When possible prefer mov_gpr64_u32. (use this only for negative values...) + * This is always bigger than mov_gpr64_u32, but smaller than a mov_gpr_u64. + */ + static Instruction mov_gpr64_s32(uint8_t dst, int64_t val) { + assert(val >= INT32_MIN && val <= INT32_MAX); + assert(is_gpr(dst)); + Instruction instr(0xc7); + instr.set_modrm_and_rex(0, dst, 3, true); + instr.set(Imm(4, val)); + return instr; + } + + /*! + * Move 32-bits of xmm to 32 bits of gpr (no sign extension). + */ + static Instruction movd_gpr32_xmm32(uint8_t dst, uint8_t src) { + assert(is_gpr(dst)); + assert(is_xmm(src)); + Instruction instr(0x66); + instr.set_op2(0x0f); + instr.set_op3(0x7e); + instr.set_modrm_and_rex(xmm_to_id(src), dst, 3, false); + instr.swap_op0_rex(); + return instr; + } + + /*! + * Move 32-bits of gpr to 32-bits of xmm (no sign extenion) + */ + static Instruction movd_xmm32_gpr32(uint8_t dst, uint8_t src) { + assert(is_xmm(dst)); + assert(is_gpr(src)); + Instruction instr(0x66); + instr.set_op2(0x0f); + instr.set_op3(0x6e); + instr.set_modrm_and_rex(dst, xmm_to_id(src), 3, false); + instr.swap_op0_rex(); + return instr; + } + + /*! + * Move 32-bits between xmm's + */ + static Instruction mov_xmm32_xmm32(uint8_t dst, uint8_t src) { + assert(is_xmm(dst)); + assert(is_xmm(src)); + Instruction instr(0xf3); + instr.set_op2(0x0f); + instr.set_op3(0x10); + instr.set_modrm_and_rex(xmm_to_id(dst), xmm_to_id(src), 3, false); + return instr; + } + // + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // // LOADS n' STORES + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // + // /*! + // * Store 8-bits from register into a memory location that is the sum of a 64-bit register + // * and signed 32-bit offset. + // */ + // static Instruction store8_r64off32s_gpr8(uint8_t dst_reg, int32_t offset, uint8_t src_reg) { + // Instruction instr(0x88); + // instr.set_modrm_and_rex_for_addr(src_reg, dst_reg, 2, false); + // instr.set_disp(Imm(4, offset)); + // if (src_reg > int(X86R::RBX)) { + // instr.add_rex(); + // } + // return instr; + // } + // + // /*! + // * Store 16-bits from register into a memory location that is the sum of a 64-bit register + // * and signed 32-bit offset. + // */ + // static Instruction store16_r64off32s_gpr16(uint8_t dst_reg, int32_t offset, uint8_t src_reg) { + // Instruction instr(0x66); + // instr.set_op2(0x89); + // instr.set_modrm_and_rex_for_addr(src_reg, dst_reg, 2, false); + // instr.set_disp(Imm(4, offset)); + // return instr; + // } + // + // /*! + // * Store 32-bits from register into a memory location that is the sum of a 64-bit register + // * and signed 32-bit offset. + // */ + // static Instruction store32_r64off32s_gpr32(uint8_t dst_reg, int32_t offset, uint8_t src_reg) { + // Instruction instr(0x89); + // instr.set_modrm_and_rex_for_addr(src_reg, dst_reg, 2, false); + // instr.set_disp(Imm(4, offset)); + // return instr; + // } + // + // /*! + // * Store 64-bits from gpr into memory located at 64-bit reg + 32-bit signed offset. + // */ + // static Instruction store64_r64off32s_gpr64(uint8_t dst_reg, int32_t offset, uint8_t src_reg) { + // Instruction instr(0x89); + // instr.set_modrm_rex_sib_for_reg_reg_disp32(src_reg, 2, dst_reg, true); + // instr.set_disp(Imm(4, offset)); + // return instr; + // } + // + // /*! + // * Load 8-bits from memory (at address of 64-bit reg + 32-bit signed offset) into gpr (zero + // * extended) + // */ + // static Instruction load16_gpr8z_r64off32s(uint8_t dst, uint8_t src, int32_t offset) { + // Instruction instr(0x0f); + // instr.set_op2(0xb6); + // instr.set_modrm_rex_sib_for_reg_reg_disp32(dst, 2, src, true); + // instr.set_disp(Imm(4, offset)); + // return instr; + // } + // + // /*! + // * Load 16-bits from memory (at address of 64-bit reg + 32-bit signed offset) into gpr (zero + // * extended) + // */ + // static Instruction load16_gpr16z_r64off32s(uint8_t dst, uint8_t src, int32_t offset) { + // Instruction instr(0x0f); + // instr.set_op2(0xb7); + // instr.set_modrm_rex_sib_for_reg_reg_disp32(dst, 2, src, true); + // instr.set_disp(Imm(4, offset)); + // return instr; + // } + // + // /*! + // * Load 16-bits from memory (at address of 64-bit reg + 32-bit signed offset) into gpr (sign + // * extended) + // */ + // static Instruction load16_gpr16s_r64off32s(uint8_t dst, uint8_t src, int32_t offset) { + // Instruction instr(0x0f); + // instr.set_op2(0xbf); + // instr.set_modrm_rex_sib_for_reg_reg_disp32(dst, 2, src, true); + // instr.set_disp(Imm(4, offset)); + // return instr; + // } + // + // /*! + // * Load 32-bits from memory (at address of 64-bit reg + 32-bit signed offset) into gpr. + // * Use the sext flag to enable sign extension. + // */ + // static Instruction load32_gpr32sz_r64off32s(uint8_t dst_reg, + // int32_t offset, + // uint8_t src_reg, + // bool sext = false) { + // Instruction instr(0x8b); + // if (sext) { + // instr.op = 0x63; + // } + // instr.set_modrm_rex_sib_for_reg_reg_disp32(dst_reg, 2, src_reg, sext); + // instr.set_disp(Imm(4, offset)); + // return instr; + // } + // + // /*! + // * Load 64-bits from memory located at 64-bit reg + 32-bit signed offset into gpr + // */ + // static Instruction load64_gpr64_r64off32s(uint8_t dst_reg, int32_t offset, uint8_t src_reg) { + // Instruction instr(0x8b); + // instr.set_modrm_rex_sib_for_reg_reg_disp32(dst_reg, 2, src_reg, true); + // instr.set_disp(Imm(4, offset)); + // return instr; + // } + // + // /*! + // * Load 32-bits form memory located at 64-bit reg + 32-bit signed offset into xmm (32-bits) + // * movss + // */ + // static Instruction load32_xmm32_r64off32s(uint8_t dst, uint8_t src, int32_t offset) { + // Instruction instr(0xf3); + // instr.set_op2(0x0f); + // instr.set_op3(0x10); + // instr.set_modrm_rex_sib_for_reg_reg_disp32(dst, 2, src, false); + // instr.set_disp(Imm(4, offset)); + // instr.swap_op0_rex(); + // return instr; + // } + + //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // FUNCTION STUFF + //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + /*! + * Return instruction + */ + static Instruction ret() { return Instruction(0xc3); } + + /*! + * Instruction to push gpr (64-bits) onto the stack + */ + static Instruction push_gpr64(uint8_t reg) { + if (reg >= 8) { + auto i = Instruction(0x50 + reg - 8); + i.set(REX(false, false, false, true)); + return i; + } + return Instruction(0x50 + reg); + } + + /*! + * Instruction to pop 64 bit gpr from the stack + */ + static Instruction pop_gpr64(uint8_t reg) { + if (reg >= 8) { + auto i = Instruction(0x58 + reg - 8); + i.set(REX(false, false, false, true)); + return i; + } + return Instruction(0x58 + reg); + } + + // /*! + // * Call a function stored in a 64-bit gpr + // */ + // static Instruction call_r64(uint8_t reg) { + // Instruction instr(0xff); + // if (reg >= 8) { + // instr.set(REX(false, false, false, true)); + // reg -= 8; + // } + // assert(reg < 8); + // ModRM mrm; + // mrm.rm = reg; + // mrm.reg_op = 2; + // mrm.mod = 3; + // instr.set(mrm); + // return instr; + // } + // + // /*! + // * Call a function stored in a 64-bit gpr + // */ + // static Instruction jmp_r64(uint8_t reg) { + // Instruction instr(0xff); + // if (reg >= 8) { + // instr.set(REX(false, false, false, true)); + // reg -= 8; + // } + // assert(reg < 8); + // ModRM mrm; + // mrm.rm = reg; + // mrm.reg_op = 4; + // mrm.mod = 3; + // instr.set(mrm); + // return instr; + // } + // + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // // INTEGER MATH + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // + // /*! + // * Add 64-bit registers. + // */ + // static Instruction add_gpr64_gpr64(uint8_t dst, uint8_t src) { + // Instruction instr(0x01); + // instr.set_modrm_and_rex(src, dst, 3, true); + // return instr; + // } + // + // /*! + // * Add a signed 32 bit immediate to a 64 bit register + // * TODO: determine if we can decrease to imm16? + // */ + // static Instruction add_gpr64_imm32s(uint8_t dst, int32_t offset) { + // Instruction instr(0x81); + // instr.set_modrm_and_rex(0, dst, 3, true); + // instr.set(Imm(4, offset)); + // return instr; + // } + // + // /*! + // * Add a signed 32 bit immediate to a 64 bit register + // * TODO: determine if we can decrease to imm16? + // */ + // static Instruction add_gpr64_imm8s(uint8_t dst, int8_t v) { + // Instruction instr(0x83); + // instr.set_modrm_and_rex(0, dst, 3, true); + // instr.set(Imm(1, v)); + // return instr; + // } + // + // /*! + // * Subtract 64-bit registers + // */ + // static Instruction sub_gpr64_gpr64(uint8_t dst, uint8_t src) { + // Instruction instr(0x29); + // instr.set_modrm_and_rex(src, dst, 3, true); + // return instr; + // } + // + // /*! + // * Multiply gprs (32-bit, signed). + // */ + // static Instruction imul_gpr32_gpr32(uint8_t dst, uint8_t src) { + // Instruction instr(0xf); + // instr.set_op2(0xaf); + // instr.set_modrm_and_rex(dst, src, 3, false); + // return instr; + // } + // + // /*! + // * Divide (idiv, 32 bit) + // */ + // static Instruction idiv_gpr32(uint8_t reg) { + // Instruction instr(0xf7); + // instr.set_modrm_and_rex(7, reg, 3, false); + // return instr; + // } + // + // /*! + // * Convert doubleword to quadword for division. + // * Blame Intel for this disaster. + // */ + // static Instruction cdq() { + // Instruction instr(0x99); + // return instr; + // } + // + // /*! + // * Move from gpr32 to gpr64, with sign extension. + // * Needed for division madness. + // */ + // static Instruction movsx_r64_r32(uint8_t dst, uint8_t src) { + // Instruction instr(0x63); + // instr.set_modrm_and_rex(dst, src, 3, true); + // return instr; + // } + // + // /*! + // * Compare gpr64. This sets the flags for the jumps. + // */ + // static Instruction cmp_gpr64_gpr64(uint8_t a, uint8_t b) { + // Instruction instr(0x3b); + // instr.set_modrm_and_rex(a, b, 3, true); + // return instr; + // } + // + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // // BIT STUFF + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // + // /*! + // * Or of two gprs + // */ + // static Instruction or_gpr64_gpr64(uint8_t dst, uint8_t src) { + // Instruction instr(0x0b); + // instr.set_modrm_and_rex(dst, src, 3, true); + // return instr; + // } + // + // /*! + // * And of two gprs + // */ + // static Instruction and_gpr64_gpr64(uint8_t dst, uint8_t src) { + // Instruction instr(0x23); + // instr.set_modrm_and_rex(dst, src, 3, true); + // return instr; + // } + // + // /*! + // * Xor of two gprs + // */ + // static Instruction xor_gpr64_gpr64(uint8_t dst, uint8_t src) { + // Instruction instr(0x33); + // instr.set_modrm_and_rex(dst, src, 3, true); + // return instr; + // } + // + // /*! + // * This is the way "real" compilers zero registers, so we should do it too. + // */ + // static Instruction xor_zero_gpr(uint8_t reg) { + // Instruction instr(0x31); + // instr.set_modrm_and_rex(reg, reg, 3, false); + // return instr; + // } + // + // /*! + // * Bitwise not a gpr + // */ + // static Instruction not_gpr64(uint8_t reg) { + // Instruction instr(0xf7); + // instr.set_modrm_and_rex(2, reg, 3, true); + // return instr; + // } + // + // /*! + // * Shift 64-bit gpr left by CL register + // */ + // static Instruction shl_gpr64_cl(uint8_t reg) { + // Instruction instr(0xd3); + // instr.set_modrm_and_rex(4, reg, 3, true); + // return instr; + // } + // + // /*! + // * Shift 64-bit gpr right (logical) by CL register + // */ + // static Instruction shr_gpr64_cl(uint8_t reg) { + // Instruction instr(0xd3); + // instr.set_modrm_and_rex(5, reg, 3, true); + // return instr; + // } + // + // /*! + // * Shift 64-bit gpr right (arithmetic) by CL register + // */ + // static Instruction sar_gpr64_cl(uint8_t reg) { + // Instruction instr(0xd3); + // instr.set_modrm_and_rex(7, reg, 3, true); + // return instr; + // } + // + // /*! + // * Shift 64-ptr left (logical) by the constant shift amount "sa". + // */ + // static Instruction shl_gpr64_u8(uint8_t reg, uint8_t sa) { + // Instruction instr(0xc1); + // instr.set_modrm_and_rex(4, reg, 3, true); + // instr.set(Imm(1, sa)); + // return instr; + // } + // + // /*! + // * Shift 64-ptr right (logical) by the constant shift amount "sa". + // */ + // static Instruction shr_gpr64_u8(uint8_t reg, uint8_t sa) { + // Instruction instr(0xc1); + // instr.set_modrm_and_rex(5, reg, 3, true); + // instr.set(Imm(1, sa)); + // return instr; + // } + // + // /*! + // * Shift 64-ptr right (arithmetic) by the constant shift amount "sa". + // */ + // static Instruction sar_gpr64_u8(uint8_t reg, uint8_t sa) { + // Instruction instr(0xc1); + // instr.set_modrm_and_rex(7, reg, 3, true); + // instr.set(Imm(1, sa)); + // return instr; + // } + // + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // // CONTROL FLOW + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // + // /*! + // * Jump, 32-bit constant offset. The offset is by default 0 and must be patched later. + // */ + // static Instruction jmp_32() { + // Instruction instr(0xe9); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // /*! + // * Jump if equal. + // * TODO - can we get away with 16 bits? + // */ + // static Instruction je_32() { + // Instruction instr(0x0f); + // instr.set_op2(0x84); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // /*! + // * Jump not equal. + // * TODO - can we get away with 16 bits? + // */ + // static Instruction jne_32() { + // Instruction instr(0x0f); + // instr.set_op2(0x85); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // /*! + // * Jump less than or equal. + // * TODO - can we get away with 16 bits? + // */ + // static Instruction jle_32() { + // Instruction instr(0x0f); + // instr.set_op2(0x8e); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // /*! + // * Jump greater than or equal. + // * TODO - can we get away with 16 bits? + // */ + // static Instruction jge_32() { + // Instruction instr(0x0f); + // instr.set_op2(0x8d); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // /*! + // * Jump less than + // * TODO - can we get away with 16 bits? + // */ + // static Instruction jl_32() { + // Instruction instr(0x0f); + // instr.set_op2(0x8c); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // /*! + // * Jump greater than + // * TODO - can we get away with 16 bits? + // */ + // static Instruction jg_32() { + // Instruction instr(0x0f); + // instr.set_op2(0x8f); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // /*! + // * Jump below or equal + // * TODO - can we get away with 16 bits? + // */ + // static Instruction jbe_32() { + // Instruction instr(0x0f); + // instr.set_op2(0x86); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // /*! + // * Jump above or equal + // * TODO - can we get away with 16 bits? + // */ + // static Instruction jae_32() { + // Instruction instr(0x0f); + // instr.set_op2(0x83); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // /*! + // * Jump below + // * TODO - can we get away with 16 bits? + // */ + // static Instruction jb_32() { + // Instruction instr(0x0f); + // instr.set_op2(0x82); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // /*! + // * Jump above + // * TODO - can we get away with 16 bits? + // */ + // static Instruction ja_32() { + // Instruction instr(0x0f); + // instr.set_op2(0x87); + // instr.set(Imm(4, 0)); + // return instr; + // } + // + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // // FLOAT MATH + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // + // /*! + // * Compare two floats and set flag register for jump + // */ + // static Instruction cmp_flt_flt(uint8_t a, uint8_t b) { + // Instruction instr(0x0f); + // instr.set_op2(0x2e); + // instr.set_modrm_and_rex(a, b, 3, false); + // return instr; + // } + // + // /*! + // * Multiply two floats in xmm's + // */ + // static Instruction mulss_xmm_xmm(uint8_t dst, uint8_t src) { + // Instruction instr(0xf3); + // instr.set_op2(0x0f); + // instr.set_op3(0x59); + // instr.set_modrm_and_rex(dst, src, 3, false); + // instr.swap_op0_rex(); + // return instr; + // } + // + // /*! + // * Divide two floats in xmm's + // */ + // static Instruction divss_xmm_xmm(uint8_t dst, uint8_t src) { + // Instruction instr(0xf3); + // instr.set_op2(0x0f); + // instr.set_op3(0x5e); + // instr.set_modrm_and_rex(dst, src, 3, false); + // instr.swap_op0_rex(); + // return instr; + // } + // + // /*! + // * Subtract two floats in xmm's + // */ + // static Instruction subss_xmm_xmm(uint8_t dst, uint8_t src) { + // Instruction instr(0xf3); + // instr.set_op2(0x0f); + // instr.set_op3(0x5c); + // instr.set_modrm_and_rex(dst, src, 3, false); + // instr.swap_op0_rex(); + // return instr; + // } + // + // /*! + // * Add two floats in xmm's + // */ + // static Instruction addss_xmm_xmm(uint8_t dst, uint8_t src) { + // Instruction instr(0xf3); + // instr.set_op2(0x0f); + // instr.set_op3(0x58); + // instr.set_modrm_and_rex(dst, src, 3, false); + // instr.swap_op0_rex(); + // return instr; + // } + // + // /*! + // * Convert GPR int32 to XMM float (single precision) + // */ + // static Instruction int32_to_float(uint8_t dst, uint8_t src) { + // Instruction instr(0xf3); + // instr.set_op2(0x0f); + // instr.set_op3(0x2a); + // instr.set_modrm_and_rex(dst, src, 3, false); + // instr.swap_op0_rex(); + // return instr; + // } + // + // /*! + // * Convert XMM float to GPR int32(single precision) (truncate) + // */ + // static Instruction float_to_int64(uint8_t dst, uint8_t src) { + // Instruction instr(0xf3); + // instr.set_op2(0x0f); + // instr.set_op3(0x2c); + // instr.set_modrm_and_rex(dst, src, 3, true); + // instr.swap_op0_rex(); + // return instr; + // } + // + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // // UTILITIES + // //;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + // + // /*! + // * A "null" instruction. This instruction does not generate any bytes + // * but can be referred to by a label. Useful to insert in place of a real instruction + // * if the real instruction has been optimized out. + // */ + // static Instruction null() { + // Instruction i(0); + // i.is_null = true; + // return i; + // } + // + // /*! + // * A "function start" instruction. This emits no opcodes, but is used + // * to determine where to insert the function type tag and how to align a function. + // */ + // static Instruction function_start() { + // Instruction i(0); + // i.is_null = true; + // i.is_function_start = true; + // return i; + // } +}; +} // namespace goal + +#endif // JAK1_IGEN_H diff --git a/goalc/emitter/Instruction.h b/goalc/emitter/Instruction.h new file mode 100644 index 0000000000..1049ed0790 --- /dev/null +++ b/goalc/emitter/Instruction.h @@ -0,0 +1,340 @@ +/*! + * @file Instruction.h + * x86-64 instruction encoding + */ +#ifndef JAK1_INSTRUCTION_H +#define JAK1_INSTRUCTION_H + +#include +#include "common/common_types.h" + +namespace goal { + +/*! + * The ModRM byte + */ +struct ModRM { + uint8_t mod; + uint8_t reg_op; + uint8_t rm; + + uint8_t operator()() const { + return (mod << 6) | (reg_op << 3) | (rm << 0); + } +}; + +/*! + * The SIB Byte + */ +struct SIB { + uint8_t scale, index, base; + + uint8_t operator()() const { + return (scale << 6) | (index << 3) | (base << 0); + } +}; + +/*! + * An Immediate (either imm or disp) + */ +struct Imm { + Imm() = default; + Imm(uint8_t sz, uint64_t v) : size(sz), value(v) { } + uint8_t size; + union { + uint64_t value; + uint8_t v_arr[8]; + }; + +}; + +/*! + * The REX prefix byte + */ +struct REX { + explicit REX(bool w = false, bool r = false, bool x = false, bool b = false) : W(w), R(r), X(x), B(b) { } + // W - 64-bit operands + // R - reg extension + // X - SIB i extnsion + // B - other extension + bool W, R, X, B; + uint8_t operator()() const { + return (1 << 6) | (W << 3) | (R << 2) | (X << 1) | (B << 0); + } +}; + +/*! + * A high-level description of an x86-64 opcode. It can emit itself. + */ +struct Instruction { + Instruction(uint8_t opcode) : op(opcode) { } + uint8_t op; + + bool op2_set = false; + uint8_t op2; + + bool op3_set = false; + uint8_t op3; + + // if true, don't emit anything + bool is_null = false; + + // flag to indicate it's the first instruction of a function and needs align and type tag + bool is_function_start = false; + + // the rex byte + bool set_rex = false; + uint8_t m_rex = 0; + + // the modrm byte + bool set_modrm = false; + uint8_t m_modrm = 0; + + // the sib byte + bool set_sib = false; + uint8_t m_sib = 0; + + // the displacement + bool set_disp_imm = false; + Imm disp; + + // the immediate + bool set_imm = false; + Imm imm; + + // which IR instruction does this go with? + // this is only set for the first instruction generated from an IR. + int ir_index = -1; + + /*! + * Move opcode byte 0 to before the rex prefix. + */ + void swap_op0_rex() { + if(!set_rex) return; + auto temp = op; + op = m_rex; + m_rex = temp; + } + + void set(REX r) { + m_rex = r(); + set_rex = true; + } + + void set(ModRM modrm) { + m_modrm = modrm(); + set_modrm = true; + } + + void set(SIB sib) { + m_sib = sib(); + set_sib = true; + } + + void set_disp(Imm i) { + disp = i; + set_disp_imm = true; + } + + void set(Imm i) { + imm = i; + set_imm = true; + } + + void set_op2(uint8_t b) { + op2_set = true; + op2 = b; + } + + void set_op3(uint8_t b) { + op3_set = true; + op3 = b; + } + + /*! + * Set modrm and rex as needed for two regs. + */ + void set_modrm_and_rex(uint8_t reg, uint8_t rm, uint8_t mod, bool rex_w = false) { + bool rex_b = false, rex_r = false; + + if(rm >= 8) { + rm -= 8; + rex_b = true; + } + + if(reg >= 8) { + reg -= 8; + rex_r = true; + } + + ModRM modrm; + modrm.mod = mod; + modrm.reg_op = reg; + modrm.rm = rm; + + set(modrm); + + if(rex_b || rex_w || rex_r) { + set(REX(rex_w, rex_r, false, rex_b)); + } + } + + /*! + * Set modrm and rex as needed for two regs for an addressing mode. + * Will set SIB if R12 or RSP indexing is used. + */ + void set_modrm_and_rex_for_addr(uint8_t reg, uint8_t rm, uint8_t mod, bool rex_w = false) { + bool rex_b = false, rex_r = false; + + if(rm >= 8) { + rm -= 8; + rex_b = true; + } + + if(reg >= 8) { + reg -= 8; + rex_r = true; + } + + ModRM modrm; + modrm.mod = mod; + modrm.reg_op = reg; + modrm.rm = rm; + + set(modrm); + + if(rm == 4) { + SIB sib; + sib.scale = 0; + sib.base = 4; + sib.index = 4; + + set(sib); + } + + if(rex_b || rex_w || rex_r) { + set(REX(rex_w, rex_r, false, rex_b)); + } + } + + void add_rex() { + if(!set_rex) { + set(REX()); + } + } + + /*! + * Set up modrm and rex for the commonly used 32-bit immediate displacement indexing mode. + */ + void set_modrm_rex_sib_for_reg_reg_disp32(uint8_t reg, uint8_t mod, uint8_t rm, bool rex_w) { + ModRM modrm; + + bool rex_r = false; + if(reg >= 8) { + reg -= 8; + rex_r = true; + } + modrm.reg_op = reg; + + modrm.mod = mod; + + modrm.rm = 4; // use sib + + SIB sib; + sib.scale = 0; + sib.index = 4; + bool rex_b = false; + if(rm >= 8) { + rex_b = true; + rm -= 8; + } + + sib.base = rm; + + set(modrm); + set(sib); + + if(rex_r || rex_w || rex_b) { + set(REX(rex_w, rex_r, false, rex_b)); + } + } + + + /*! + * Get the position of the disp immediate relative to the start of the instruction + */ + int offset_of_disp() const { + if(is_null) return 0; + assert(set_disp_imm); + int offset = 0; + if(set_rex) offset++; + offset++; // opcode + if(op2_set) offset++; + if(op3_set) offset++; + if(set_modrm) offset++; + if(set_sib) offset++; + return offset; + } + + /*! + * Get the position of the imm immediate relative to the start of the instruction + */ + int offset_of_imm() const { + if(is_null) return 0; + assert(set_imm); + int offset = 0; + if(set_rex) offset++; + offset++; // opcode + if(op2_set) offset++; + if(op3_set) offset++; + if(set_modrm) offset++; + if(set_sib) offset++; + if(set_disp_imm) offset += disp.size; + return offset; + } + + /*! + * Emit into a buffer and return how many bytes written (can be zero) + */ + uint8_t emit(uint8_t* buffer) const { + if(is_null) return 0; + uint8_t count = 0; + if(set_rex) { + buffer[count++] = m_rex; + } + + buffer[count++] = op; + + if(op2_set) { + buffer[count++] = op2; + } + + if(op3_set) { + buffer[count++] = op3; + } + + if(set_modrm) { + buffer[count++] = m_modrm; + } + + if(set_sib) { + buffer[count++] = m_sib; + } + + if(set_disp_imm) { + for(int i = 0; i < disp.size; i++) { + buffer[count++] = disp.v_arr[i]; + } + } + + if(set_imm) { + for(int i = 0; i < imm.size; i++) { + buffer[count++] = imm.v_arr[i]; + } + } + return count; + } +}; +} + +#endif // JAK1_INSTRUCTION_H diff --git a/goalc/emitter/registers.cpp b/goalc/emitter/registers.cpp new file mode 100644 index 0000000000..7fd6796d6f --- /dev/null +++ b/goalc/emitter/registers.cpp @@ -0,0 +1,20 @@ +#include "registers.h" + +namespace goal { +bool is_gpr(u8 reg) { + return reg <= R15; +} + +u8 get_nth_xmm(u8 id) { + return id + XMM0; +} + +bool is_xmm(u8 reg) { + return reg >= XMM0 && reg <= XMM15; +} + +u8 xmm_to_id(u8 reg) { + return reg - 16; +} + +} \ No newline at end of file diff --git a/goalc/emitter/registers.h b/goalc/emitter/registers.h new file mode 100644 index 0000000000..4d6892c189 --- /dev/null +++ b/goalc/emitter/registers.h @@ -0,0 +1,111 @@ +/*! + * @file registers.h + * Definitions and conventions for x86-64 registers. + */ + +#ifndef JAK1_REGISTERS_H +#define JAK1_REGISTERS_H + +#include "common/common_types.h" + +namespace goal { +enum X86R : u8 { + RAX, // return, temp + RCX, // arg 3 + RDX, // arg 2 + RBX, // X saved + + RSP, // stack pointer + RBP, // X base pointer (like fp) + RSI, // arg 1 + RDI, // arg 0 + + R8, // arg 4 + R9, // arg 5, saved + R10, // arg 6, saved (arg in GOAL only) + R11, // arg 7, saved (arg in GOAL only) + R12, // X saved - pp register (like s6) + R13, // X saved - function call register (like t9) + R14, // X saved - offset (added in GOAL x86) + R15, // X saved - st (like s7) + XMM0, + XMM1, + XMM2, + XMM3, + XMM4, + XMM5, + XMM6, + XMM7, + XMM8, + XMM9, + XMM10, + XMM11, + XMM12, + XMM13, + XMM14, + XMM15 +}; + +// the argument registers of GOAL. +// We must have 8 to be compatible with GOAL's 8-argument function calls. +constexpr int ARG_REG_COUNT = 8; + +// the first 6 are shared with Linux, and the last two are unique to GOAL. +constexpr X86R ARG_REGS[ARG_REG_COUNT] = { + X86R::RDI, X86R::RSI, X86R::RDX, X86R::RCX, X86R::R8, X86R::R9, X86R::R10, X86R::R11, +}; + +// The saved registers of GOAL. Note that RSP, RBP, R12, R13, R14, R15 shouldn't be changed by the +// caller, but these are special registers and won't be allocated to hold variables. +constexpr int SAVED_REG_COUNT = 4; +constexpr X86R SAVED_REGS[SAVED_REG_COUNT] = {X86R::RBX, X86R::R9, X86R::R10, X86R::R11}; + +// special registers +constexpr X86R PP_REG = X86R::R12; +constexpr X86R FUNC_REG = X86R::R13; +constexpr X86R OFF_REG = X86R::R14; +constexpr X86R ST_REG = X86R::R15; +constexpr X86R FP_REG = X86R::RBP; +constexpr X86R RET_REG = X86R::RAX; + +// size in bytes of a pointer +constexpr int PTR_SIZE = 4; + +// size in bytes of a general purpose register +constexpr int GPR_SIZE = 8; + +constexpr const char* x86_gpr_names[] = { + "rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi", "r8", "r9", "r10", + "r11", "r12", "r13", "r14", "r15", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", + "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15"}; + +/* + Name Arg ID Clobber? Special + RAX - y return + RCX 3 y arg + RDX 2 y arg + RBX - n + + RSP - n stack pointer + RBP - n base pointer + RSI 1 y arg + RDI 0 y arg + + R8 4 y arg + R9 5 n arg + R10 6 n arg + R11 7 n arg + R12 - n pp + R13 - n func + R14 - n + R15 + */ + +bool is_gpr(u8 reg); +u8 get_nth_xmm(u8 id); +bool is_xmm(u8 reg); +u8 xmm_to_id(u8 reg); + +} // namespace goal + +#endif // JAK1_REGISTERS_H diff --git a/goalc/goos/CMakeLists.txt b/goalc/goos/CMakeLists.txt new file mode 100644 index 0000000000..a6b2e6b489 --- /dev/null +++ b/goalc/goos/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(goos SHARED Object.cpp TextDB.cpp Reader.cpp Interpreter.cpp InterpreterEval.cpp) +target_link_libraries(goos util) \ No newline at end of file diff --git a/goalc/goos/Interpreter.cpp b/goalc/goos/Interpreter.cpp new file mode 100644 index 0000000000..26bbc94852 --- /dev/null +++ b/goalc/goos/Interpreter.cpp @@ -0,0 +1,921 @@ +/*! + * @file Interpreter.cpp + * The GOOS Interpreter and implementation of special forms. + * Additional "built-in" forms are implemented in InterpreterEval + */ + +#include +#include "Interpreter.h" + +namespace goos { +Interpreter::Interpreter() { + // Interpreter startup: + goal_to_goos.reset(); + + // create the GOOS global environment + global_environment = EnvironmentObject::make_new("global"); + + // create the environment which is be visible from GOAL + goal_env = EnvironmentObject::make_new("goal"); + + // make both environments available in both. + define_var_in_env(global_environment, global_environment, "*global-env*"); + define_var_in_env(goal_env, goal_env, "*goal-env*"); + define_var_in_env(goal_env, global_environment, "*global-env*"); + define_var_in_env(global_environment, goal_env, "*goal-env*"); + + // setup maps + special_forms = { + {"define", &Interpreter::eval_define}, + {"quote", &Interpreter::eval_quote}, + {"set!", &Interpreter::eval_set}, + {"lambda", &Interpreter::eval_lambda}, + {"cond", &Interpreter::eval_cond}, + {"or", &Interpreter::eval_or}, + {"and", &Interpreter::eval_and}, + {"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-file", &Interpreter::eval_read_file}, + {"print", &Interpreter::eval_print}, + {"inspect", &Interpreter::eval_inspect}, + {"load-file", &Interpreter::eval_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}, + {"current-method-type", &Interpreter::eval_current_method_type}, + }; + + string_to_type = {{"empty-list", ObjectType::EMPTY_LIST}, + {"integer", ObjectType::INTEGER}, + {"float", ObjectType::FLOAT}, + {"char", ObjectType::CHAR}, + {"symbol", ObjectType::SYMBOL}, + {"string", ObjectType::STRING}, + {"pair", ObjectType::PAIR}, + {"array", ObjectType::ARRAY}, + {"lambda", ObjectType::LAMBDA}, + {"macro", ObjectType::MACRO}, + {"environment", ObjectType::ENVIRONMENT}}; + + // load the standard library + load_goos_library(); +} + +/*! + * Disable printfs on errors, to make test output look less messy. + */ +void Interpreter::disable_printfs() { + disable_printing = true; +} + +/*! + * Load the goos library, by interpreting (load-file "goal/gs/goos-lib.gs") in the global env. + */ +void Interpreter::load_goos_library() { + auto cmd = "(load-file \"goalc/gs/goos-lib.gs\")"; + eval_with_rewind(reader.read_from_string(cmd), global_environment.as_env()); +} + +/*! + * 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(name).as_symbol()] = 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); +} + +/*! + * Display the REPL, which will run until the user executes exit. + */ +void Interpreter::execute_repl() { + while (!want_exit) { + try { + // read something from the user + Object obj = reader.read_from_stdin("goos"); + // evaluate + Object evald = eval_with_rewind(obj, global_environment.as_env()); + // print + printf("%s\n", evald.print().c_str()); + } catch (std::exception& e) { + printf("REPL Error: %s\n", e.what()); + } + } +} + +/*! + * Signal an evaluation error. This throws an exception which will unwind the evaluation stack + * for debugging. + */ +void Interpreter::throw_eval_error(const Object& o, const std::string& err) { + throw std::runtime_error("[GOOS] Evaluation error on " + o.print() + ": " + err + "\n" + + reader.db.get_info_for(o)); +} + +/*! + * Evaluate the given expression, with a "checkpoint" in the evaluation stack here. If there is an + * evaluation error, there will be a print indicating there was an error in the evaluation of "obj", + * and if possible what file/line "obj" comes from. + */ +Object Interpreter::eval_with_rewind(const Object& obj, + const std::shared_ptr& env) { + Object result = EmptyListObject::make_new(); + try { + result = eval(obj, env); + } catch (std::runtime_error& e) { + if (!disable_printing) { + printf("-----------------------------------------\n"); + printf("From object %s\nat %s\n", obj.inspect().c_str(), reader.db.get_info_for(obj).c_str()); + } + throw e; + } + return result; +} + +/*! + * Sets dest to the global variable with the given name, if the variable exists. + * 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; + return true; + } + return false; +} + +/*! + * Get arguments being passed to a form. Don't evaluate them. There are two modes, "varargs" and + * "not varargs". With varargs enabled, any number of unnamed and named arguments can be given. + * Without varags, the unnamed/named arguments must match the spec. By default specs are "not + * vararg" - use make_varags() to get a varargs spec. In general, macros/lambdas use specs, but + * built-in forms use varargs. + * + * If form is "varargs", all arguments go to unnamed or named. + * Ex: (.... a b :key-1 c d) will put a, b, d in unnamed and d in key-1 + * + * If form isn't "varargs", the expected number of unnamed arguments must match, unless "rest" + * is specified, in which case the additional arguments are stored in rest. + * + * Also, if "varargs" isn't set, all keyword arguments must be defined. If the use doesn't provide + * a value, the default value will be used instead. + */ +Arguments Interpreter::get_args(const Object& form, const Object& rest, const ArgumentSpec& spec) { + Arguments args; + + // loop over forms in list + Object current = rest; + while (!current.is_empty_list()) { + 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); + 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"); + } + + // 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"); + } + + // check for well-formed :key value expression + current = current.as_pair()->cdr; + if (current.is_empty_list()) { + throw_eval_error(form, "Key argument didn't have a value"); + } + + args.named[key_name] = current.as_pair()->car; + } else { + // not a keyword. Add to unnamed or rest, depending on what we expect + if (spec.varargs || args.unnamed.size() < spec.unnamed.size()) { + args.unnamed.push_back(arg); + } else { + args.rest.push_back(arg); + } + } + current = current.as_pair()->cdr; + } + + // Check expected key args and set default values on unset ones if possible + for (auto& kv : spec.named) { + auto defined_kv = args.named.find(kv.first); + if (defined_kv == args.named.end()) { + // key arg not given by user, try to use a default value. + if (kv.second.has_default) { + args.named[kv.first] = kv.second.default_value; + } else { + throw_eval_error(form, + "key argument \"" + kv.first + "\" wasn't given and has no default value"); + } + } + } + + // Check argument size, if spec defines it + if (!spec.varargs) { + if (args.unnamed.size() < spec.unnamed.size()) { + throw_eval_error(form, "didn't get enough arguments"); + } + assert(args.unnamed.size() == spec.unnamed.size()); + + if (!args.rest.empty() && spec.rest.empty()) { + throw_eval_error(form, "got too many arguments"); + } + } + + return args; +} + +/*! + * Evaluate arguments in-place in the given environment. + * Evaluation order is: + * - unnamed, in order of appearance + * - keyword, in alphabetical order + * - rest, in order of appearance + * + * Note that in varargs mode, all unnamed arguments are put in unnamed, not rest. + */ +void Interpreter::eval_args(Arguments* args, const std::shared_ptr& env) { + for (auto& arg : args->unnamed) { + arg = eval_with_rewind(arg, env); + } + + for (auto& kv : args->named) { + kv.second = eval_with_rewind(kv.second, env); + } + + for (auto& arg : args->rest) { + arg = eval_with_rewind(arg, env); + } +} + +/*! + * Parse argument spec found in lambda/macro definition. + * Like (x y &key z &key (w my-default-value) &rest body) + */ +ArgumentSpec Interpreter::parse_arg_spec(const Object& form, Object& rest) { + ArgumentSpec spec; + + Object current = rest; + while (!current.is_empty_list()) { + auto arg = current.as_pair()->car; + if (!arg.is_symbol()) { + throw_eval_error(form, "args must be symbols"); + } + + if (arg.as_symbol()->name == "&rest") { + // special case for &rest + current = current.as_pair()->cdr; + if (!current.is_pair()) { + throw_eval_error(form, "rest arg must have a name"); + } + auto rest_name = current.as_pair()->car; + if (!rest_name.is_symbol()) { + throw_eval_error(form, "rest name must be a symbol"); + } + + spec.rest = rest_name.as_symbol()->name; + + 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") { + // 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; + if (spec.named.find(key_arg_name) != spec.named.end()) { + throw_eval_error(form, "key argument " + key_arg_name + " multiply defined"); + } + spec.named[key_arg_name] = NamedArg(); + } else if (key_arg.is_pair()) { + // form is &key (name default-value) + auto key_iter = key_arg; + auto kn = key_iter.as_pair()->car; + key_iter = key_iter.as_pair()->cdr; + 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; + if (spec.named.find(key_arg_name) != spec.named.end()) { + throw_eval_error(form, "key argument " + key_arg_name + " multiply defined"); + } + NamedArg na; + + if (!key_iter.is_pair()) { + throw_eval_error(form, "invalid keyword argument definition"); + } + + na.has_default = true; + na.default_value = key_iter.as_pair()->car; + + if (!key_iter.as_pair()->cdr.is_empty_list()) { + throw_eval_error(form, "invalid keyword argument definition"); + } + + spec.named[key_arg_name] = na; + } else { + throw_eval_error(form, "invalid key argument"); + } + } else { + spec.unnamed.push_back(arg.as_symbol()->name); + } + + current = current.as_pair()->cdr; + } + return spec; +} + +/*! + * Argument check. + * Must have the right number of unnamed arguments, with the right type. + * Keyword arguments have a bool for "required" or not. + * Extra keyword arguments are an error. + */ +void Interpreter::vararg_check( + const Object& form, + const Arguments& args, + const std::vector>& unnamed, + const std::unordered_map>>& named) { + assert(args.rest.empty()); + if (unnamed.size() != args.unnamed.size()) { + throw_eval_error(form, "Got " + std::to_string(args.unnamed.size()) + + " arguments, but expected " + std::to_string(unnamed.size())); + } + + for (size_t i = 0; i < unnamed.size(); i++) { + if (unnamed[i] != args.unnamed[i].type) { + assert(!unnamed[i].is_wildcard); + throw_eval_error(form, "Argument " + std::to_string(i) + " has type " + + object_type_to_string(args.unnamed[i].type) + " but " + + object_type_to_string(unnamed[i].value) + " was expected"); + } + } + + for (const auto& kv : named) { + auto kv2 = args.named.find(kv.first); + if (kv2 == args.named.end()) { + // argument not given. + if (kv.second.first) { + // but was required + throw_eval_error(form, "Required named argument \"" + kv.first + "\" was not found"); + } + } else { + // argument given. + if (kv.second.second != kv2->second.type) { + // but is wrong type + assert(!kv.second.second.is_wildcard); + throw_eval_error(form, "Argument \"" + kv.first + "\" has type " + + object_type_to_string(kv2->second.type) + " but " + + object_type_to_string(kv.second.second.value) + " was expected"); + } + } + } + + for (const auto& kv : args.named) { + if (named.find(kv.first) == named.end()) { + throw_eval_error(form, "Got unrecognized keyword argument \"" + kv.first + "\""); + } + } +} + +/*! + * Evaluate a list and return the result of the last evaluation. + */ +Object Interpreter::eval_list_return_last(const Object& form, + Object rest, + const std::shared_ptr& env) { + Object o = std::move(rest); + Object rv = EmptyListObject::make_new(); + 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; + } else { + throw_eval_error(form, "malformed body to evaluate"); + } + } +} + +/*! + * If o isn't an environment object, throws an evaluation error on form. + */ +void Interpreter::expect_env(const Object& form, const Object& o) { + if (!o.is_env()) { + throw_eval_error(form, "Object " + o.print() + " is a " + object_type_to_string(o.type) + + " but was expected to be an environment"); + } +} + +/*! + * Highest-level evaluation dispatch. + */ +Object Interpreter::eval(Object obj, const std::shared_ptr& env) { + switch (obj.type) { + case ObjectType::SYMBOL: + return eval_symbol(obj, env); + case ObjectType::PAIR: + return eval_pair(obj, env); + case ObjectType::INTEGER: + case ObjectType::FLOAT: + case ObjectType::STRING: + case ObjectType::CHAR: + return obj; + default: + throw_eval_error(obj, "cannot evaluate this object"); + return Object(); + } +} + +namespace { + +/*! + * Try to find a symbol in an env or parent env. If successful, set dest and return true. Otherwise + * return false. + */ +bool try_symbol_lookup(const Object& sym, + const std::shared_ptr& env, + Object* dest) { + // booleans are hard-coded here + if (sym.as_symbol()->name == "#t" || sym.as_symbol()->name == "#f") { + *dest = sym; + return true; + } + + // loop up envs until we find it. + std::shared_ptr search_env = env; + for (;;) { + auto kv = search_env->vars.find(sym.as_symbol()); + if (kv != search_env->vars.end()) { + *dest = kv->second; + return true; + } + + auto pe = search_env->parent_env; + if (pe) { + search_env = pe; + } else { + return false; + } + } +} +} // namespace + +/*! + * Evaluate a symbol by finding the closest scoped variable with matching name. + */ +Object Interpreter::eval_symbol(const Object& sym, const std::shared_ptr& env) { + Object result; + if (!try_symbol_lookup(sym, env, &result)) { + throw_eval_error(sym, "symbol is not defined"); + } + return result; +} + +/*! + * Evaluate a pair, either as special form, builtin form, macro application, or lambda application. + */ +Object Interpreter::eval_pair(const Object& obj, const std::shared_ptr& env) { + auto pair = obj.as_pair(); + Object head = pair->car; + Object rest = pair->cdr; + + // first see if we got a symbol: + if (head.type == ObjectType::SYMBOL) { + auto head_sym = head.as_symbol(); + + // try a special form first + auto kv_sf = special_forms.find(head_sym->name); + if (kv_sf != special_forms.end()) { + return ((*this).*(kv_sf->second))(obj, rest, env); + } + + // try builtins next + auto kv_b = builtin_forms.find(head_sym->name); + 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) + eval_args(&args, env); + return ((*this).*(kv_b->second))(obj, args, env); + } + + // try macros next + Object macro_obj; + if (try_symbol_lookup(head, env, ¯o_obj) && macro_obj.is_macro()) { + auto macro = macro_obj.as_macro(); + Arguments args = get_args(obj, rest, macro->args); + + auto mac_env_obj = EnvironmentObject::make_new(); + auto mac_env = mac_env_obj.as_env(); + mac_env->parent_env = env; // not 100% clear that this is right + set_args_in_env(obj, args, macro->args, mac_env); + // expand the macro! + return eval_with_rewind(eval_list_return_last(macro->body, macro->body, mac_env), env); + } + } + + // eval the head and try it as a lambda + Object eval_head = eval_with_rewind(head, env); + if (eval_head.type != ObjectType::LAMBDA) { + throw_eval_error(obj, "head of form didn't evaluate to lambda"); + } + + auto lam = eval_head.as_lambda(); + Arguments args = get_args(obj, rest, lam->args); + eval_args(&args, env); + auto lam_env_obj = EnvironmentObject::make_new(); + auto lam_env = lam_env_obj.as_env(); + lam_env->parent_env = lam->parent_env; + set_args_in_env(obj, args, lam->args, lam_env); + return eval_list_return_last(lam->body, lam->body, lam_env); +} + +/*! + * Given some arguments, an argument spec, and and environment, define the arguments are variables + * in the environment. + */ +void Interpreter::set_args_in_env(const Object& form, + const Arguments& args, + const ArgumentSpec& arg_spec, + const std::shared_ptr& env) { + if (arg_spec.rest.empty() && args.unnamed.size() != arg_spec.unnamed.size()) { + throw_eval_error(form, "did not get the expected number of unnamed arguments (got " + + std::to_string(args.unnamed.size()) + ", expected " + + std::to_string(arg_spec.unnamed.size()) + ")"); + } else if (!arg_spec.rest.empty() && args.unnamed.size() < arg_spec.unnamed.size()) { + throw_eval_error(form, "args with rest didn't get enough arguments (got " + + std::to_string(args.unnamed.size()) + " but need at least " + + std::to_string(arg_spec.unnamed.size()) + ")"); + } + + // 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); + } + + // named args + for (const auto& kv : arg_spec.named) { + env->vars[intern(kv.first).as_symbol()] = 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); + } else { + if (!args.rest.empty()) { + throw_eval_error(form, "got too many arguments"); + } + } +} + +/*! + * Define a variable in the current environment. The env can be overwritten with :env keyword arg + */ +Object Interpreter::eval_define(const Object& form, + const Object& rest, + const std::shared_ptr& env) { + auto args = get_args(form, rest, make_varargs()); + vararg_check(form, args, {ObjectType::SYMBOL, {}}, {{"env", {false, {}}}}); + + auto define_env = env; + if (args.has_named("env")) { + auto result = eval_with_rewind(args.get_named("env"), env); + expect_env(form, result); + define_env = result.as_env(); + } + + Object value = eval_with_rewind(args.unnamed[1], env); + define_env->vars[args.unnamed[0].as_symbol()] = value; + return value; +} + +/*! + * Set an existing variable. If there is no existing variable in the current environment, will + * look at the parent environment. + */ +Object Interpreter::eval_set(const Object& form, + const Object& rest, + const std::shared_ptr& env) { + auto args = get_args(form, rest, make_varargs()); + vararg_check(form, args, {ObjectType::SYMBOL, {}}, {}); + auto to_define = args.unnamed.at(0); + Object to_set = eval_with_rewind(args.unnamed.at(1), env); + + std::shared_ptr 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 pe = search_env->parent_env; + if (pe) { + search_env = pe; + } else { + throw_eval_error(to_define, "symbol is not defined"); + } + } +} + +/*! + * Lambda definition special form. + */ +Object Interpreter::eval_lambda(const Object& form, + const Object& rest, + const std::shared_ptr& env) { + if (!rest.is_pair()) { + throw_eval_error(form, "lambda must receive two arguments"); + } + + Object arg_list = rest.as_pair()->car; + if (!arg_list.is_pair() && !arg_list.is_empty_list()) { + throw_eval_error(form, "lambda argument list must be a list"); + } + + Object new_lambda = LambdaObject::make_new(); + auto l = new_lambda.as_lambda(); + l->args = parse_arg_spec(form, arg_list); + + Object rrest = rest.as_pair()->cdr; + if (!rrest.is_pair()) { + throw_eval_error(form, "lambda body must be a list"); + } + + l->body = rrest; + l->parent_env = env; + return new_lambda; +} + +/*! + * Macro definition special form. + */ +Object Interpreter::eval_macro(const Object& form, + const Object& rest, + const std::shared_ptr& env) { + if (!rest.is_pair()) { + throw_eval_error(form, "macro must receive two arguments"); + } + + Object arg_list = rest.as_pair()->car; + if (!arg_list.is_pair() && !arg_list.is_empty_list()) { + throw_eval_error(form, "macro argument list must be a list"); + } + + Object new_macro = MacroObject::make_new(); + auto m = new_macro.as_macro(); + m->args = parse_arg_spec(form, arg_list); + + Object rrest = rest.as_pair()->cdr; + if (!rrest.is_pair()) { + throw_eval_error(form, "macro body must be a list"); + } + + m->body = rrest; + m->parent_env = env; + return new_macro; +} + +/*! + * Quote special form: (quote x) -> x + */ +Object Interpreter::eval_quote(const Object& form, + const Object& rest, + const std::shared_ptr& env) { + (void)env; + auto args = get_args(form, rest, make_varargs()); + vararg_check(form, args, {{}}, {}); + return args.unnamed.front(); +} + +/*! + * Recursive quasi-quote evaluation + */ +Object Interpreter::quasiquote_helper(const Object& form, + const std::shared_ptr& env) { + Object lst = form; + std::vector result; + for (;;) { + if (lst.type == ObjectType::PAIR) { + Object item = lst.as_pair()->car; + if (item.type == ObjectType::PAIR) { + if (item.as_pair()->car.type == ObjectType::SYMBOL && + item.as_pair()->car.as_symbol()->name == "unquote") { + Object unquote_arg = item.as_pair()->cdr; + if (unquote_arg.type != ObjectType::PAIR || + unquote_arg.as_pair()->cdr.type != ObjectType::EMPTY_LIST) { + throw_eval_error(form, "unquote must have exactly 1 arg"); + } + item = eval_with_rewind(unquote_arg.as_pair()->car, env); + } else if (item.as_pair()->car.type == ObjectType::SYMBOL && + item.as_pair()->car.as_symbol()->name == "unquote-splicing") { + Object unquote_arg = item.as_pair()->cdr; + if (unquote_arg.type != ObjectType::PAIR || + unquote_arg.as_pair()->cdr.type != ObjectType::EMPTY_LIST) { + throw_eval_error(form, "unquote must have exactly 1 arg"); + } + item = eval_with_rewind(unquote_arg.as_pair()->car, env); + + // bypass normal addition: + lst = lst.as_pair()->cdr; + Object to_add = item; + for (;;) { + if (to_add.type == ObjectType::PAIR) { + result.push_back(to_add.as_pair()->car); + to_add = to_add.as_pair()->cdr; + } else if (to_add.type == ObjectType::EMPTY_LIST) { + break; + } else { + throw_eval_error(form, "malformed unquote-splicing result"); + } + } + continue; + } + + else { + item = quasiquote_helper(item, env); + } + } + lst = lst.as_pair()->cdr; + result.push_back(item); + } else if (lst.type == ObjectType::EMPTY_LIST) { + return build_list(result); + } else { + throw_eval_error(form, "malformed quasiquote"); + } + } +} + +/*! + * Quasiquote (backtick) evaluation + */ +Object Interpreter::eval_quasiquote(const Object& form, + const Object& rest, + const std::shared_ptr& env) { + if (rest.type != ObjectType::PAIR || rest.as_pair()->cdr.type != ObjectType::EMPTY_LIST) + throw_eval_error(form, "quasiquote must have one argument!"); + return quasiquote_helper(rest.as_pair()->car, env); +} + +namespace { +bool truthy(const Object& o) { + return !(o.is_symbol() && o.as_symbol()->name == "#f"); +} +} // namespace + +/*! + * Scheme "cond" statement - tested by integrated tests only. + */ +Object Interpreter::eval_cond(const Object& form, + const Object& rest, + const std::shared_ptr& env) { + if (rest.type != ObjectType::PAIR) + throw_eval_error(form, "cond must have at least one clause, which must be a form"); + Object result; + + Object lst = rest; + for (;;) { + if (lst.type == ObjectType::PAIR) { + Object current_case = lst.as_pair()->car; + if (current_case.type != ObjectType::PAIR) + throw_eval_error(lst, "bogus cond case"); + + // check condition: + Object condition_result = eval_with_rewind(current_case.as_pair()->car, env); + if (truthy(condition_result)) { + if (current_case.as_pair()->cdr.type == ObjectType::EMPTY_LIST) { + return condition_result; + } + // got a match! + return eval_list_return_last(current_case, current_case.as_pair()->cdr, env); + } else { + // no match, continue. + lst = lst.as_pair()->cdr; + } + } else if (lst.type == ObjectType::EMPTY_LIST) { + return SymbolObject::make_new(reader.symbolTable, "#f"); + } else { + throw_eval_error(form, "malformed cond"); + } + } +} + +/*! + * Short circuiting "or" statement + */ +Object Interpreter::eval_or(const Object& form, + const Object& rest, + const std::shared_ptr& env) { + if (rest.type != ObjectType::PAIR) { + throw_eval_error(form, "or must have at least one argument!"); + } + + Object lst = rest; + for (;;) { + if (lst.type == ObjectType::PAIR) { + Object current = eval_with_rewind(lst.as_pair()->car, env); + if (truthy(current)) { + return current; + } + lst = lst.as_pair()->cdr; + } else if (lst.type == ObjectType::EMPTY_LIST) { + return SymbolObject::make_new(reader.symbolTable, "#f"); + } else { + throw_eval_error(form, "invalid or form"); + } + } +} + +/*! + * Short circuiting "and" statement + */ +Object Interpreter::eval_and(const Object& form, + const Object& rest, + const std::shared_ptr& env) { + if (rest.type != ObjectType::PAIR) { + throw_eval_error(form, "and must have at least one argument!"); + } + + Object lst = rest; + Object current; + for (;;) { + if (lst.type == ObjectType::PAIR) { + current = eval_with_rewind(lst.as_pair()->car, env); + if (!truthy(current)) { + return SymbolObject::make_new(reader.symbolTable, "#f"); + } + lst = lst.as_pair()->cdr; + } else if (lst.type == ObjectType::EMPTY_LIST) { + return current; + } else { + throw_eval_error(form, "invalid and form"); + } + } +} + +/*! + * Cheating "while loop" because we do not have tail recursion optimization yet. + */ +Object Interpreter::eval_while(const Object& form, + const Object& rest, + const std::shared_ptr& env) { + if (rest.type != ObjectType::PAIR) { + throw_eval_error(form, "while must have condition and body"); + } + + Object condition = rest.as_pair()->car; + Object body = rest.as_pair()->cdr; + if (body.type != ObjectType::PAIR) { + throw_eval_error(form, "while must have condition and body"); + } + + Object rv = SymbolObject::make_new(reader.symbolTable, "#f"); + while (truthy(eval_with_rewind(condition, env))) { + rv = eval_list_return_last(form, body, env); + } + + return rv; +} + +} // namespace goos \ No newline at end of file diff --git a/goalc/goos/Interpreter.h b/goalc/goos/Interpreter.h new file mode 100644 index 0000000000..7d37dbe210 --- /dev/null +++ b/goalc/goos/Interpreter.h @@ -0,0 +1,232 @@ +/*! + * @file Interpreter.h + * The GOOS Interpreter + */ + +#ifndef JAK1_INTERPRETER_H +#define JAK1_INTERPRETER_H + +#include +#include "Object.h" +#include "Reader.h" +#include "goalc/util/MatchParam.h" + +namespace goos { +class Interpreter { + public: + Interpreter(); + void execute_repl(); + void throw_eval_error(const Object& o, const std::string& err); + Object eval_with_rewind(const Object& obj, const std::shared_ptr& env); + bool get_global_variable_by_name(const std::string& name, Object* dest); + Object eval(Object obj, const std::shared_ptr& env); + Object intern(const std::string& name); + void disable_printfs(); + + Reader reader; + Object global_environment; + Object goal_env; + + // data passed from GOAL to GOOS available to any evaluation. + struct GoalToGoosData { + std::string enclosing_method_type; + + void reset() { enclosing_method_type = "#f"; } + } goal_to_goos; + + private: + friend class Goal; + void load_goos_library(); + void define_var_in_env(Object& env, Object& var, const std::string& name); + void expect_env(const Object& form, const Object& o); + void vararg_check( + const Object& form, + const Arguments& args, + const std::vector>& unnamed, + const std::unordered_map>>& named); + + Object eval_pair(const Object& o, const std::shared_ptr& env); + Object eval_symbol(const Object& sym, const std::shared_ptr& env); + Arguments get_args(const Object& form, const Object& rest, const ArgumentSpec& spec); + void eval_args(Arguments* args, const std::shared_ptr& env); + ArgumentSpec parse_arg_spec(const Object& form, Object& rest); + + Object eval_list_return_last(const Object& form, + Object rest, + const std::shared_ptr& env); + Object quasiquote_helper(const Object& form, const std::shared_ptr& env); + + IntType number_to_integer(const Object& obj); + FloatType number_to_float(const Object& obj); + + template + T number(const Object& obj); + + template + Object num_lt(const Object& form, Arguments& args, const std::shared_ptr& env); + template + Object num_gt(const Object& form, Arguments& args, const std::shared_ptr& env); + template + Object num_leq(const Object& form, + Arguments& args, + const std::shared_ptr& env); + template + Object num_geq(const Object& form, + Arguments& args, + const std::shared_ptr& env); + template + Object num_plus(const Object& form, + Arguments& args, + const std::shared_ptr& env); + template + Object num_minus(const Object& form, + Arguments& args, + const std::shared_ptr& env); + template + Object num_divide(const Object& form, + Arguments& args, + const std::shared_ptr& env); + template + Object num_times(const Object& form, + Arguments& args, + const std::shared_ptr& env); + + Object eval_eval(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_equals(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_exit(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_begin(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_read(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_read_file(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_load_file(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_print(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_inspect(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_plus(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_minus(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_times(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_divide(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_numequals(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_lt(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_gt(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_leq(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_geq(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_car(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_cdr(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_set_car(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_set_cdr(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_gensym(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_cons(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_null(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_type(const Object& form, + Arguments& args, + const std::shared_ptr& env); + Object eval_current_method_type(const Object& form, + Arguments& args, + const std::shared_ptr& env); + + // specials + Object eval_define(const Object& form, + const Object& rest, + const std::shared_ptr& env); + Object eval_quote(const Object& form, + const Object& rest, + const std::shared_ptr& env); + Object eval_set(const Object& form, + const Object& rest, + const std::shared_ptr& env); + Object eval_lambda(const Object& form, + const Object& rest, + const std::shared_ptr& env); + Object eval_cond(const Object& form, + const Object& rest, + const std::shared_ptr& env); + Object eval_or(const Object& form, + const Object& rest, + const std::shared_ptr& env); + Object eval_and(const Object& form, + const Object& rest, + const std::shared_ptr& env); + Object eval_quasiquote(const Object& form, + const Object& rest, + const std::shared_ptr& env); + Object eval_macro(const Object& form, + const Object& rest, + const std::shared_ptr& env); + Object eval_while(const Object& form, + const Object& rest, + const std::shared_ptr& env); + + void set_args_in_env(const Object& form, + const Arguments& args, + const ArgumentSpec& arg_spec, + const std::shared_ptr& env); + + bool want_exit = false; + bool disable_printing = false; + + std::unordered_map& env)> + builtin_forms; + std::unordered_map& env)> + special_forms; + int64_t gensym_id = 0; + + std::unordered_map string_to_type; +}; +} // namespace goos + +#endif // JAK1_INTERPRETER_H diff --git a/goalc/goos/InterpreterEval.cpp b/goalc/goos/InterpreterEval.cpp new file mode 100644 index 0000000000..8f73c46052 --- /dev/null +++ b/goalc/goos/InterpreterEval.cpp @@ -0,0 +1,589 @@ +/*! + * @file InterpreterEval.cpp + * Implementation of built-in GOOS functions. + */ + +#include "Interpreter.h" + +namespace goos { + +/*! + * Exit GOOS. Accepts and ignores all arguments; + */ +Object Interpreter::eval_exit(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)form; + (void)args; + (void)env; + want_exit = true; + return EmptyListObject::make_new(); +} + +/*! + * Begin form + */ +Object Interpreter::eval_begin(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + if (!args.named.empty()) { + throw_eval_error(form, "begin form cannot have keyword arguments"); + } + + if (args.unnamed.empty()) { + return EmptyListObject::make_new(); + } else { + return args.unnamed.back(); + } +} + +/*! + * Read form, which runs the Reader on a string. + */ +Object Interpreter::eval_read(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {ObjectType::STRING}, {}); + + try { + return reader.read_from_string(args.unnamed.at(0).as_string()->data); + } catch (std::runtime_error& e) { + throw_eval_error(form, std::string("reader error inside of read:\n") + e.what()); + } + + return EmptyListObject::make_new(); +} + +/*! + * Open and run the Reader on a text file. + */ +Object Interpreter::eval_read_file(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {ObjectType::STRING}, {}); + + try { + return reader.read_from_file(args.unnamed.at(0).as_string()->data); + } catch (std::runtime_error& e) { + throw_eval_error(form, std::string("reader error inside of read-file:\n") + e.what()); + } + return EmptyListObject::make_new(); +} + +/*! + * Combines read-file and eval to load in a file. + */ +Object Interpreter::eval_load_file(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {ObjectType::STRING}, {}); + + Object o; + try { + o = reader.read_from_file(args.unnamed.at(0).as_string()->data); + } catch (std::runtime_error& e) { + throw_eval_error(form, std::string("reader error inside of load-file:\n") + e.what()); + } + + try { + return eval_with_rewind(o, global_environment.as_env()); + } catch (std::runtime_error& e) { + throw_eval_error(form, std::string("eval error inside of load-file:\n") + e.what()); + } + return EmptyListObject::make_new(); +} + +/*! + * Print the form to stdout, including a newline. + * Returns () + */ +Object Interpreter::eval_print(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {{}}, {}); + + if (!disable_printing) { + printf("%s\n", args.unnamed.at(0).print().c_str()); + } + return EmptyListObject::make_new(); +} + +/*! + * Print the inspection of a form to stdout, including a newline. + * Returns () + */ +Object Interpreter::eval_inspect(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {{}}, {}); + + if (!disable_printing) { + printf("%s\n", args.unnamed.at(0).inspect().c_str()); + } + + return EmptyListObject::make_new(); +} + +/*! + * Fancy equality check (using Object::operator==) + */ +Object Interpreter::eval_equals(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {{}, {}}, {}); + return SymbolObject::make_new(reader.symbolTable, + args.unnamed[0] == args.unnamed[1] ? "#t" : "#f"); +} + +/*! + * Convert a number to an integer + */ +IntType Interpreter::number_to_integer(const Object& obj) { + switch (obj.type) { + case ObjectType::INTEGER: + return obj.integer_obj.value; + case ObjectType::FLOAT: + return (int64_t)obj.float_obj.value; + default: + throw_eval_error(obj, "object cannot be interpreted as a number!"); + } + return 0; +} + +/*! + * Convert a number to floating point + */ +FloatType Interpreter::number_to_float(const Object& obj) { + switch (obj.type) { + case ObjectType::INTEGER: + return obj.integer_obj.value; + case ObjectType::FLOAT: + return obj.float_obj.value; + default: + throw_eval_error(obj, "object cannot be interpreted as a number!"); + } + return 0; +} + +/*! + * Convert number to template type. + */ +template <> +FloatType Interpreter::number(const Object& obj) { + return number_to_float(obj); +} + +/*! + * Convert number to template type. + */ +template <> +IntType Interpreter::number(const Object& obj) { + return number_to_integer(obj); +} + +/*! + * Template implementation of addition. + */ +template +Object Interpreter::num_plus(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + (void)form; + T result = 0; + for (const auto& arg : args.unnamed) { + result += number(arg); + } + return Object::make_number(result); +} + +/*! + * Addition + */ +Object Interpreter::eval_plus(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + if (!args.named.empty() || args.unnamed.empty()) { + throw_eval_error(form, "+ must receive at least one unnamed argument!"); + } + + switch (args.unnamed.front().type) { + case ObjectType::INTEGER: + return num_plus(form, args, env); + + case ObjectType::FLOAT: + return num_plus(form, args, env); + + default: + throw_eval_error(form, "+ must have a numeric argument"); + return EmptyListObject::make_new(); + } +} + +/*! + * Template implementation of multiplication. + */ +template +Object Interpreter::num_times(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + (void)form; + T result = 1; + for (const auto& arg : args.unnamed) { + result *= number(arg); + } + return Object::make_number(result); +} + +/*! + * Multiplication + */ +Object Interpreter::eval_times(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + if (!args.named.empty() || args.unnamed.empty()) { + throw_eval_error(form, "* must receive at least one unnamed argument!"); + } + + switch (args.unnamed.front().type) { + case ObjectType::INTEGER: + return num_times(form, args, env); + + case ObjectType::FLOAT: + return num_times(form, args, env); + + default: + throw_eval_error(form, "* must have a numeric argument"); + return EmptyListObject::make_new(); + } +} + +/*! + * Template implementation of subtraction. + */ +template +Object Interpreter::num_minus(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + (void)form; + T result; + if (args.unnamed.size() > 1) { + result = number(args.unnamed[0]); + for (uint32_t i = 1; i < args.unnamed.size(); i++) { + result -= number(args.unnamed[i]); + } + } else { + result = -number(args.unnamed[0]); + } + return Object::make_number(result); +} + +/*! + * Subtraction + */ +Object Interpreter::eval_minus(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + if (!args.named.empty() || args.unnamed.empty()) { + throw_eval_error(form, "- must receive at least one unnamed argument!"); + } + + switch (args.unnamed.front().type) { + case ObjectType::INTEGER: + return num_minus(form, args, env); + + case ObjectType::FLOAT: + return num_minus(form, args, env); + + default: + throw_eval_error(form, "- must have a numeric argument"); + return EmptyListObject::make_new(); + } +} + +/*! + * Template implementation of division. + */ +template +Object Interpreter::num_divide(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + (void)form; + T result = number(args.unnamed[0]) / number(args.unnamed[1]); + return Object::make_number(result); +} + +/*! + * Division + */ +Object Interpreter::eval_divide(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + vararg_check(form, args, {{}, {}}, {}); + switch (args.unnamed.front().type) { + case ObjectType::INTEGER: + return num_divide(form, args, env); + + case ObjectType::FLOAT: + return num_divide(form, args, env); + + default: + throw_eval_error(form, "/ must have a numeric argument"); + return EmptyListObject::make_new(); + } +} + +/*! + * Compare numbers for equality + */ +Object Interpreter::eval_numequals(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + if (!args.named.empty() || args.unnamed.size() < 2) { + throw_eval_error(form, "= must receive at least two unnamed arguments!"); + } + + bool result = true; + switch (args.unnamed.front().type) { + case ObjectType::INTEGER: { + int64_t ref = number_to_integer(args.unnamed.front()); + for (uint32_t i = 1; i < args.unnamed.size(); i++) { + if (ref != number_to_integer(args.unnamed[i])) { + result = false; + break; + } + } + } break; + + case ObjectType::FLOAT: { + double ref = number_to_float(args.unnamed.front()); + for (uint32_t i = 1; i < args.unnamed.size(); i++) { + if (ref != number_to_float(args.unnamed[i])) { + result = false; + break; + } + } + } break; + + default: + throw_eval_error(form, "+ must have a numeric argument"); + return EmptyListObject::make_new(); + } + + return SymbolObject::make_new(reader.symbolTable, result ? "#t" : "#f"); +} + +template +Object Interpreter::num_lt(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)form; + (void)env; + T a = number(args.unnamed[0]); + T b = number(args.unnamed[1]); + return SymbolObject::make_new(reader.symbolTable, (a < b) ? "#t" : "#f"); +} + +Object Interpreter::eval_lt(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + vararg_check(form, args, {{}, {}}, {}); + switch (args.unnamed.front().type) { + case ObjectType::INTEGER: + return num_lt(form, args, env); + + case ObjectType::FLOAT: + return num_lt(form, args, env); + + default: + throw_eval_error(form, "< must have a numeric argument"); + return EmptyListObject::make_new(); + } +} + +template +Object Interpreter::num_gt(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)form; + (void)env; + T a = number(args.unnamed[0]); + T b = number(args.unnamed[1]); + return SymbolObject::make_new(reader.symbolTable, (a > b) ? "#t" : "#f"); +} + +Object Interpreter::eval_gt(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + vararg_check(form, args, {{}, {}}, {}); + switch (args.unnamed.front().type) { + case ObjectType::INTEGER: + return num_gt(form, args, env); + + case ObjectType::FLOAT: + return num_gt(form, args, env); + + default: + throw_eval_error(form, "> must have a numeric argument"); + return EmptyListObject::make_new(); + } +} + +template +Object Interpreter::num_leq(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)form; + (void)env; + T a = number(args.unnamed[0]); + T b = number(args.unnamed[1]); + return SymbolObject::make_new(reader.symbolTable, (a <= b) ? "#t" : "#f"); +} + +Object Interpreter::eval_leq(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + vararg_check(form, args, {{}, {}}, {}); + switch (args.unnamed.front().type) { + case ObjectType::INTEGER: + return num_leq(form, args, env); + + case ObjectType::FLOAT: + return num_leq(form, args, env); + + default: + throw_eval_error(form, "<= must have a numeric argument"); + return EmptyListObject::make_new(); + } +} + +template +Object Interpreter::num_geq(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)form; + (void)env; + T a = number(args.unnamed[0]); + T b = number(args.unnamed[1]); + return SymbolObject::make_new(reader.symbolTable, (a >= b) ? "#t" : "#f"); +} + +Object Interpreter::eval_geq(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + vararg_check(form, args, {{}, {}}, {}); + switch (args.unnamed.front().type) { + case ObjectType::INTEGER: + return num_geq(form, args, env); + + case ObjectType::FLOAT: + return num_geq(form, args, env); + + default: + throw_eval_error(form, ">= must have a numeric argument"); + return EmptyListObject::make_new(); + } +} + +Object Interpreter::eval_eval(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + vararg_check(form, args, {{}}, {}); + return eval(args.unnamed[0], env); +} + +Object Interpreter::eval_car(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {ObjectType::PAIR}, {}); + return args.unnamed[0].as_pair()->car; +} + +Object Interpreter::eval_set_car(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {ObjectType::PAIR, {}}, {}); + args.unnamed[0].as_pair()->car = args.unnamed[1]; + return args.unnamed[0]; +} + +Object Interpreter::eval_set_cdr(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {ObjectType::PAIR, {}}, {}); + args.unnamed[0].as_pair()->cdr = args.unnamed[1]; + return args.unnamed[0]; +} + +Object Interpreter::eval_cdr(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {ObjectType::PAIR}, {}); + return args.unnamed[0].as_pair()->cdr; +} + +Object Interpreter::eval_gensym(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {}, {}); + return SymbolObject::make_new(reader.symbolTable, "gensym" + std::to_string(gensym_id++)); +} + +Object Interpreter::eval_cons(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {{}, {}}, {}); + return PairObject::make_new(args.unnamed[0], args.unnamed[1]); +} + +Object Interpreter::eval_null(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {{}}, {}); + return SymbolObject::make_new(reader.symbolTable, args.unnamed[0].is_empty_list() ? "#t" : "#f"); +} + +Object Interpreter::eval_type(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {{ObjectType::SYMBOL}, {}}, {}); + + auto kv = string_to_type.find(args.unnamed[0].as_symbol()->name); + 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"); + } else { + return SymbolObject::make_new(reader.symbolTable, "#f"); + } +} + +Object Interpreter::eval_current_method_type(const Object& form, + Arguments& args, + const std::shared_ptr& env) { + (void)env; + vararg_check(form, args, {}, {}); + return SymbolObject::make_new(reader.symbolTable, goal_to_goos.enclosing_method_type); +} +} // namespace goos \ No newline at end of file diff --git a/goalc/goos/Object.cpp b/goalc/goos/Object.cpp new file mode 100644 index 0000000000..1023ff3581 --- /dev/null +++ b/goalc/goos/Object.cpp @@ -0,0 +1,236 @@ +#include "Object.h" +#include "goalc/util/text_util.h" + +namespace goos { + +std::shared_ptr gEmptyList = nullptr; + +/*! + * Convert type to string (name in brackets) + */ +std::string object_type_to_string(ObjectType type) { + switch (type) { + case ObjectType::EMPTY_LIST: + return "[empty list]"; + case ObjectType::INTEGER: + return "[integer]"; + case ObjectType::FLOAT: + return "[float]"; + case ObjectType::CHAR: + return "[char]"; + case ObjectType::SYMBOL: + return "[symbol]"; + case ObjectType::STRING: + return "[string]"; + case ObjectType::PAIR: + return "[pair]"; + case ObjectType::ARRAY: + return "[array]"; + case ObjectType::LAMBDA: + return "[lambda]"; + case ObjectType::MACRO: + return "[macro]"; + case ObjectType::ENVIRONMENT: + return "[environment]"; + default: + throw std::runtime_error("unknown object type in object_type_to_string"); + } +} + +/*! + * Special case to print a float with the %g format specifier + */ +template <> +std::string fixed_to_string(FloatType x) { + char buff[256]; + sprintf(buff, "%.6f", x); + return {buff}; +} + +/*! + * Special case to print a character and escape the weird ones. + */ +template <> +std::string fixed_to_string(char x) { + char buff[256]; + if (util::is_printable_char(x) && x != ' ') { + // can print directly + sprintf(buff, "#\\%c", x); + return {buff}; + } + + // not printable, special case + switch (x) { + case '\n': + sprintf(buff, "#\\\\n"); + break; + case ' ': + sprintf(buff, "#\\\\s"); + break; + case '\t': + sprintf(buff, "#\\\\t"); + break; + + default: + sprintf(buff, "#\\{%d}", u8(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; +} + +/*! + * Build a list of objects from a vector of objects. + */ +Object build_list(const std::vector& objects) { + if (objects.empty()) { + return EmptyListObject::make_new(); + } + + Object empty = EmptyListObject::make_new(); + Object head = PairObject::make_new(objects[0], empty); + Object last = head; + + for (std::size_t i = 1; i < objects.size(); i++) { + last.as_pair()->cdr = PairObject::make_new(objects[i], empty); + last = last.as_pair()->cdr; + } + + return head; +} + +/*! + * Compare two objects for equality. + * Does "expensive" checking. + */ +bool Object::operator==(const Object& other) const { + if (type != other.type) + return false; + + switch (type) { + case ObjectType::STRING: + return as_string()->data == other.as_string()->data; + case ObjectType::INTEGER: + return integer_obj == other.integer_obj; + case ObjectType::FLOAT: + return float_obj == other.float_obj; + case ObjectType::CHAR: + return char_obj == other.char_obj; + + case ObjectType::SYMBOL: + case ObjectType::ENVIRONMENT: + case ObjectType::LAMBDA: + case ObjectType::MACRO: + return heap_obj == other.heap_obj; + + case ObjectType::EMPTY_LIST: + return true; + case ObjectType::PAIR: + return as_pair()->car == other.as_pair()->car && as_pair()->cdr == other.as_pair()->cdr; + case ObjectType::ARRAY: { + auto a = as_array(); + auto b = other.as_array(); + if (a->size() != b->size()) { + return false; + } + for (size_t i = 0; i < a->data.size(); i++) { + if ((*a)[i] != (*b)[i]) { + return false; + } + } + return true; + } + + default: + throw std::runtime_error("equality not implemented for " + print()); + } +} + +template <> +Object Object::make_number(FloatType value) { + return Object::make_float(value); +} + +template <> +Object Object::make_number(IntType value) { + return Object::make_integer(value); +} + +/*! + * Debug print argument specification. + */ +std::string ArgumentSpec::print() const { + std::string result = " unnamed args:\n"; + for (auto& arg : unnamed) { + result += " " + arg + "\n"; + } + result += " named args:\n"; + for (auto& arg : named) { + result += " " + arg.first; + if (arg.second.has_default) { + result += " (default " + arg.second.default_value.print() + ")"; + } + result += "\n"; + } + if (!rest.empty()) { + result += " rest: " + rest + "\n"; + } + + return result; +} + +std::string Arguments::print() const { + std::string result = " unnamed args:\n"; + for (auto& arg : unnamed) { + result += " " + arg.print() + "\n"; + } + result += " named args:\n"; + for (auto& arg : named) { + result += " " + arg.first + " " + arg.second.print() + "\n"; + } + if (!rest.empty()) { + result += " rest: \n"; + for (auto& x : rest) { + result += " " + x.print() + "\n"; + } + } + + return result; +} + +Object Arguments::get_named(const std::string& name, const Object& default_value) { + Object result = default_value; + auto kv = named.find(name); + if (kv != named.end()) { + result = kv->second; + } + return result; +} + +Object Arguments::get_named(const std::string& name) { + return named.at(name); +} + +bool Arguments::has_named(const std::string& name) { + return named.find(name) != named.end(); +} + +/*! + * Make an argument specification which accepts any arguments + */ +ArgumentSpec make_varargs() { + ArgumentSpec as; + as.varargs = true; + return as; +} + +} // namespace goos diff --git a/goalc/goos/Object.h b/goalc/goos/Object.h new file mode 100644 index 0000000000..926014bc6e --- /dev/null +++ b/goalc/goos/Object.h @@ -0,0 +1,624 @@ +/*! + * @file Object.h + * An "Object" represents a scheme object. + * There are different types of objects, as represented by ObjectType. + * An "Object" is an efficient wrapper around any of these types. + * Some types are "heap allocated", and have reference semantics, and others are + * "fixed" and have value semantics. Heap allocated objects implement reference counting with + * std::shared_ptr. + * + * To create a new Object for a heap allocated type, use the make_new static method of the type of + * object you want to make. This will return a correctly setup Object. For fixed objects, use + * Object::make_ + * + * To convert an Object into a more specific object, use the as_ method of Object. + * It will throw an exception is you get the type wrong. + * + * These are all the types: + * + * EMPTY_LIST - a special heap allocated object. There is only one EMPTY_LIST allocated, and + * EmptyListObject::make_new() will always return an Object which references that one. + * + * INTEGER - a fixed type. Use Object::make_integer() to create one. Internally uses int64_t + * FLOAT - a fixed type. Use Object::make_float() to create one. Internally uses double + * CHAR - a fixed type. Use Object::make_char() to create one. Internally uses char + * + * SYMBOL - a special heap allocated object. SymbolObject::make_new requires a SymbolTable to + * store the newly allocated symbol in, and will return an existing symbol if there already is one. + * + * STRING - a heap allocated object. Create with StringObject::make_new. Uses std::string internally + * + * PAIR - a heap allocated object containing two Objects. + * + * ARRAY - a heap allocated object containing a std::vector + * + * LAMBDA - a heap allocated object representing a GOOS lambda + * MACRO - a heap allocated object representing a GOOS macro + * ENVIRONMENT - a heap allocated object representing a GOOS environment + * + */ + +#ifndef JAK1_OBJECT_H +#define JAK1_OBJECT_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/common_types.h" + +namespace goos { + +using FloatType = double; +using IntType = s64; + +/*! + * All objects have one of these kinds + */ +enum class ObjectType : u8 { + // special + EMPTY_LIST, + + // fixed + INTEGER, + FLOAT, + CHAR, + + // allocated + SYMBOL, + STRING, + PAIR, + ARRAY, + LAMBDA, + MACRO, + ENVIRONMENT, + INVALID +}; + +std::string object_type_to_string(ObjectType kind); + +// Some objects are "fixed", meaning they are stored inline in the Object, and not heap allocated. + +/*! + * By default, convert a fixed object data to string with std::to_string. + * This will be used for integers, but float and char define their own. + */ +template +std::string fixed_to_string(T x) { + return std::to_string(x); +} + +/*! + * Special case to print floating point + */ +template <> +std::string fixed_to_string(FloatType); + +/*! + * Special case to print character + */ +template <> +std::string fixed_to_string(char); + +/*! + * Common implementation for a fixed object + */ +template +class FixedObject { + public: + T value; + + explicit FixedObject(T v) : value(v) {} + FixedObject() = default; + std::string print() const { return fixed_to_string(value); } + std::string inspect() const { return type_as_string() + " " + fixed_to_string(value) + "\n"; } + ~FixedObject() = default; + + bool operator==(const FixedObject& other) const { return value == other.value; } + + private: + std::string type_as_string() const { + if (std::is_same()) + return object_type_to_string(ObjectType::FLOAT); + if (std::is_same()) + return object_type_to_string(ObjectType::INTEGER); + if (std::is_same()) + return object_type_to_string(ObjectType::CHAR); + assert(false); + } +}; + +/*! + * The fixed objects supported by GOOS + */ +using IntegerObject = FixedObject; +using FloatObject = FixedObject; +using CharObject = FixedObject; + +// Other objects are separate allocated on the heap. These objects should be HeapObjects. + +class HeapObject { + public: + virtual std::string print() const = 0; + virtual std::string inspect() const = 0; + virtual ~HeapObject() = default; +}; + +// forward declare all HeapObjects +class PairObject; +class EnvironmentObject; +class SymbolObject; +class StringObject; +class LambdaObject; +class MacroObject; +class ArrayObject; + +// Wrapper Object class for all objects +class Object { + public: + std::shared_ptr heap_obj = nullptr; + + union { + IntegerObject integer_obj; + FloatObject float_obj; + CharObject char_obj; + }; + + ObjectType type = ObjectType::INVALID; + + std::string print() const { + switch (type) { + case ObjectType::INTEGER: + return integer_obj.print(); + case ObjectType::FLOAT: + return float_obj.print(); + case ObjectType::CHAR: + return char_obj.print(); + default: + return heap_obj->print(); + } + } + + std::string inspect() const { + switch (type) { + case ObjectType::INTEGER: + return integer_obj.inspect(); + case ObjectType::FLOAT: + return float_obj.inspect(); + case ObjectType::CHAR: + return char_obj.inspect(); + default: + return heap_obj->inspect(); + } + } + + template + static Object make_number(T value); + + static Object make_integer(IntType value) { + Object o; + o.type = ObjectType::INTEGER; + o.integer_obj.value = value; + return o; + } + + static Object make_float(FloatType value) { + Object o; + o.type = ObjectType::FLOAT; + o.float_obj.value = value; + return o; + } + + static Object make_char(char value) { + Object o; + o.type = ObjectType::CHAR; + o.char_obj.value = value; + return o; + } + + std::shared_ptr as_pair() const { + if (type != ObjectType::PAIR) { + throw std::runtime_error("as_pair called on a " + object_type_to_string(type) + " " + + print()); + } + return std::dynamic_pointer_cast(heap_obj); + } + + std::shared_ptr as_env() const { + if (type != ObjectType::ENVIRONMENT) { + throw std::runtime_error("as_env called on a " + object_type_to_string(type) + " " + print()); + } + return std::dynamic_pointer_cast(heap_obj); + } + + std::shared_ptr as_symbol() const { + if (type != ObjectType::SYMBOL) { + throw std::runtime_error("as_symbol called on a " + object_type_to_string(type) + " " + + print()); + } + return std::dynamic_pointer_cast(heap_obj); + } + + std::shared_ptr as_string() const { + if (type != ObjectType::STRING) { + throw std::runtime_error("as_string called on a " + object_type_to_string(type) + " " + + print()); + } + return std::dynamic_pointer_cast(heap_obj); + } + + std::shared_ptr as_lambda() const { + if (type != ObjectType::LAMBDA) { + throw std::runtime_error("as_lambda called on a " + object_type_to_string(type) + " " + + print()); + } + return std::dynamic_pointer_cast(heap_obj); + } + + std::shared_ptr as_macro() const { + if (type != ObjectType::MACRO) { + throw std::runtime_error("as_macro called on a " + object_type_to_string(type) + " " + + print()); + } + return std::dynamic_pointer_cast(heap_obj); + } + + std::shared_ptr as_array() const { + if (type != ObjectType::ARRAY) { + throw std::runtime_error("as_array called on a " + object_type_to_string(type) + " " + + print()); + } + return std::dynamic_pointer_cast(heap_obj); + } + + IntType& as_int() { + if (type != ObjectType::INTEGER) { + throw std::runtime_error("as_int called on a " + object_type_to_string(type) + " " + print()); + } + return integer_obj.value; + } + + FloatType& as_float() { + if (type != ObjectType::FLOAT) { + throw std::runtime_error("as_float called on a " + object_type_to_string(type) + " " + + print()); + } + return float_obj.value; + } + + char& as_char() { + if (type != ObjectType::CHAR) { + throw std::runtime_error("as_char called on a " + object_type_to_string(type) + " " + + print()); + } + return char_obj.value; + } + + bool is_empty_list() const { return type == ObjectType::EMPTY_LIST; } + bool is_int() const { return type == ObjectType::INTEGER; } + bool is_float() const { return type == ObjectType::FLOAT; } + bool is_char() const { return type == ObjectType::CHAR; } + bool is_symbol() const { return type == ObjectType::SYMBOL; } + bool is_string() const { return type == ObjectType::STRING; } + bool is_pair() const { return type == ObjectType::PAIR; } + bool is_array() const { return type == ObjectType::ARRAY; } + bool is_env() const { return type == ObjectType::ENVIRONMENT; } + bool is_macro() const { return type == ObjectType::MACRO; } + + bool operator==(const Object& other) const; + bool operator!=(const Object& other) const { return !((*this) == other); } +}; + +// There is a single heap allocated EmptyListObject. +class EmptyListObject; +extern std::shared_ptr gEmptyList; + +class EmptyListObject : public HeapObject { + public: + EmptyListObject() = default; + static Object make_new() { + Object obj; + obj.type = ObjectType::EMPTY_LIST; + if (!gEmptyList) { + gEmptyList = std::make_shared(); + } + obj.heap_obj = gEmptyList; + return obj; + } + + std::string print() const override { return "()"; } + + std::string inspect() const override { + char buff[256]; + sprintf(buff, "[empty list] ()\n"); + return std::string(buff); + } + + ~EmptyListObject() = default; +}; + +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 intern(const std::string& name) { + auto kv = table.find(name); + if (kv == table.end()) { + auto iter = table.insert({name, std::make_shared(name)}); + return (*iter.first).second; + } else { + return kv->second; + } + } + + ~SymbolTable() = default; + + private: + std::unordered_map> table; +}; + +class StringObject : public HeapObject { + public: + std::string data; + explicit StringObject(std::string text) : data(std::move(text)) {} + + static Object make_new(const std::string& text) { + Object obj; + obj.type = ObjectType::STRING; + obj.heap_obj = std::make_shared(text); + return obj; + } + + std::string print() const override { return "\"" + data + "\""; } + + std::string inspect() const override { return "[string] \"" + data + "\"\n"; } + + ~StringObject() override = default; +}; + +class PairObject : public HeapObject { + public: + Object car, cdr; + + PairObject(Object car_, Object cdr_) : car(car_), cdr(cdr_) {} + + static Object make_new(Object a, Object b) { + Object obj; + obj.type = ObjectType::PAIR; + obj.heap_obj = std::make_shared(a, b); + return obj; + } + + std::string print() const override { + std::pair to_print_pair = std::make_pair(car, cdr); + + std::string result = "("; + + // print first thing: + result += car.print(); + + // print second thing + Object to_print = cdr; + if (to_print.type == ObjectType::EMPTY_LIST) { + result += ")"; + return result; + } else { + result += " "; + } + + for (;;) { + if (to_print.type == ObjectType::PAIR) { + Object to_print_car = std::dynamic_pointer_cast(to_print.heap_obj)->car; + result += to_print_car.print(); + to_print = std::dynamic_pointer_cast(to_print.heap_obj)->cdr; + if (to_print.type == ObjectType::EMPTY_LIST) { + result += ")"; + return result; + } else { + result += " "; + } + } else { + result += ". "; + result += to_print.print(); + result += ")"; + return result; + } + } + } + + std::string inspect() const override { return "[pair] " + print() + "\n"; } + + ~PairObject() = default; +}; + +class EnvironmentObject : public HeapObject { + public: + std::string name; + std::shared_ptr parent_env; + std::unordered_map, Object> vars; + + EnvironmentObject() = default; + + static Object make_new() { + Object obj; + obj.type = ObjectType::ENVIRONMENT; + obj.heap_obj = std::make_shared(); + return obj; + } + + static Object make_new(std::string name, + std::shared_ptr parent_env = nullptr) { + Object obj; + obj.type = ObjectType::ENVIRONMENT; + auto env = std::make_shared(); + env->name = std::move(name); + env->parent_env = std::move(parent_env); + obj.heap_obj = std::move(env); + return obj; + } + + std::string print() const override { + if (name.empty()) { + return ""; + } else { + return ""; + } + } + + 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"; + } + return result; + } +}; + +struct NamedArg { + bool has_default = false; + Object default_value; +}; + +struct ArgumentSpec { + bool varargs = false; + std::vector unnamed; + std::unordered_map named; + std::string rest; + std::string print() const; +}; + +ArgumentSpec make_varargs(); + +struct Arguments { + std::vector unnamed; + // note that this is _not_ an unordered map so that named arguments have a consistent (but + // arbitrary) alphabetical evaluation order. + std::map named; + std::vector rest; + bool has_rest = false; + std::string print() const; + + Object get_named(const std::string& name, const Object& default_value); + Object get_named(const std::string& name); + bool has_named(const std::string& name); +}; + +class LambdaObject : public HeapObject { + public: + std::string name; + std::shared_ptr parent_env; + Object body; + ArgumentSpec args; + + LambdaObject() = default; + + static Object make_new() { + Object obj; + obj.type = ObjectType::LAMBDA; + obj.heap_obj = std::make_shared(); + return obj; + } + + std::string print() const override { + if (name.empty()) { + return ""; + } else { + return ""; + } + } + + std::string inspect() const override { return "[lambda]\n name: " + name + "\n" + args.print(); } +}; + +class MacroObject : public HeapObject { + public: + std::string name; + std::shared_ptr parent_env; + Object body; + ArgumentSpec args; + + MacroObject() = default; + + static Object make_new() { + Object obj; + obj.type = ObjectType::MACRO; + obj.heap_obj = std::make_shared(); + return obj; + } + + std::string print() const override { + if (name.empty()) { + return ""; + } else { + return ""; + } + } + + std::string inspect() const override { return "[macro]\n name: " + name + "\n" + args.print(); } +}; + +class ArrayObject : public HeapObject { + public: + std::vector data; + ArrayObject(std::vector objects) : data(std::move(objects)) {} + static Object make_new(std::vector objects) { + Object obj; + obj.type = ObjectType::ARRAY; + obj.heap_obj = std::make_shared(std::move(objects)); + return obj; + } + + std::string print() const override { + std::string result = "#("; + if (data.empty()) { + return result + ")"; + } + for (const auto& obj : data) { + result += obj.print() + " "; + } + result.pop_back(); // remove last space + return result + ")"; + } + + std::string inspect() const override { + return "[array] size: " + std::to_string(data.size()) + " data: " + print() + "\n"; + } + + std::size_t size() const { return data.size(); } + + const Object& operator[](size_t idx) const { return data.at(idx); } + + Object& operator[](size_t idx) { return data.at(idx); } +}; + +Object build_list(const std::vector& objects); + +} // namespace goos + +#endif // JAK1_OBJECT_H diff --git a/goalc/goos/Reader.cpp b/goalc/goos/Reader.cpp new file mode 100644 index 0000000000..7e195a73cc --- /dev/null +++ b/goalc/goos/Reader.cpp @@ -0,0 +1,710 @@ +/*! + * @file Reader.cpp + * + * The Reader converts text into GOOS object, for interpreting or compiling. + * The Reader also stores the GOOS symbol table, and is able to figure out where forms + * came from, for printing error messages about forms. + * + * The reader also know where the source folder is, through an environment variable set when + * launching the compiler or the compiler test. + */ + +#include "Reader.h" +#include "third-party/linenoise.h" +#include "goalc/util/file_io.h" +#include "goalc/util/text_util.h" + +namespace goos { + +/*! + * Advance a TextStream through any comments or whitespace. + * This will leave the stream at the next non-whitespace character (or at the end) + */ +void TextStream::seek_past_whitespace_and_comments() { + while (text_remains()) { + char c = peek(); + switch (c) { + case ' ': + case '\t': + case '\n': + // just a whitespace, eat it! + read(); + break; + + case ';': + // line comment. + while (text_remains() && read() != '\n') { + } + break; + + case '#': + if (text_remains(1) && peek(1) == '|') { + assert(read() == '#'); // # + assert(read() == '|'); // | + + bool found_end = false; + // find |# + while (text_remains() && !found_end) { + // find | + while (text_remains() && read() != '|') { + } + if (text_remains() && read() == '#') { + found_end = true; + } + } + continue; + + } else { + // not a line comment + return; + } + break; + + default: + return; + } + } +} + +Reader::Reader() { + // third-party library used for a fancy line in + linenoise::SetHistoryMaxLen(400); + + // add default macros + add_reader_macro("'", "quote"); + add_reader_macro("`", "quasiquote"); + add_reader_macro(",", "unquote"); + add_reader_macro(",@", "unquote-splicing"); + + // setup table of which characters are valid for starting a symbol + for (auto& x : valid_symbols_chars) { + x = false; + } + + for (char x = 'a'; x <= 'z'; x++) { + valid_symbols_chars[(int)x] = true; + } + + for (char x = 'A'; x <= 'Z'; x++) { + valid_symbols_chars[(int)x] = true; + } + + for (char x = '0'; x <= '9'; x++) { + valid_symbols_chars[(int)x] = true; + } + + const char bonus[] = "!$%&*+-/\\.,@^_-;:<>?~=#"; + + for (const char* c = bonus; *c; c++) { + valid_symbols_chars[(int)*c] = true; + } + + // find the source directory + auto result = std::getenv("NEXT_DIR"); + if (!result) { + throw std::runtime_error( + "Environment variable NEXT_DIR is not set. Please set this to point to next/"); + } + + source_dir = result; +} + +/*! + * Prompt the user and read the result. + */ +Object Reader::read_from_stdin(const std::string& prompt_name) { + std::string line; + // escape code will make sure that we remove any color + std::string prompt_full = "\033[0m" + prompt_name + "> "; + linenoise::Readline(prompt_full.c_str(), line); + linenoise::AddHistory(line.c_str()); + // todo, decide if we should keep reading or not. + + // create text fragment and add to the DB + auto textFrag = std::make_shared(line); + db.insert(textFrag); + + // perform read + auto result = internal_read(textFrag); + db.link(result, textFrag, 0); + return result; +} + +/*! + * Read a string. + */ +Object Reader::read_from_string(const std::string& str) { + // create text fragment and add to the DB + auto textFrag = std::make_shared(str); + db.insert(textFrag); + + // perform read + auto result = internal_read(textFrag); + db.link(result, textFrag, 0); + return result; +} + +/*! + * Read a file + */ +Object Reader::read_from_file(const std::string& filename) { + auto textFrag = std::make_shared(util::combine_path(get_source_dir(), filename)); + db.insert(textFrag); + + auto result = internal_read(textFrag); + db.link(result, textFrag, 0); + return result; +} + +/*! + * Common read for a SourceText + */ +Object Reader::internal_read(std::shared_ptr text) { + // first create stream + TextStream ts(text); + + // clean up first whitespace + ts.seek_past_whitespace_and_comments(); + + // read list! + auto objs = read_list(ts, false); + return PairObject::make_new(SymbolObject::make_new(symbolTable, "top-level"), objs); +} + +/*! + * Given a stream starting at the first character of a token, get the token. Doesn't consume + * whitespace at the end and leaves the stream on the first character after the token. + */ +Token Reader::get_next_token(TextStream& stream) { + assert(stream.text_remains()); + Token t; + t.source_line = stream.line_count; + t.source_offset = stream.seek; + t.source_text = stream.text; + + char first = stream.read(); + t.text.push_back(first); + + // First - look for special tokens which end early: + + // parens, double quotes, quotes, and backticks are tokens. + if (first == '(' || first == ')' || first == '"' || first == '\'' || first == '`') + return t; + + // ",@" is its own token + if (first == ',' && stream.text_remains() && stream.peek() == '@') { + t.text.push_back(stream.read()); + return t; + } else if (first == ',') { + // "," is its own token. + return t; + } else if (first == '#' && stream.text_remains() && stream.peek() == '(') { + t.text.push_back(stream.read()); + return t; + } + + // Second - not a special token, so we read until we get a character that ends the token. + while (stream.text_remains()) { + char next = stream.peek(); + if (next == ' ' || next == '\n' || next == '\t' || next == ')' || next == ';' || next == '#' || + next == '(') { + return t; + } else { + // not the end, so add to token. + t.text.push_back(stream.read()); + } + } + + return t; +} + +/*! + * Add a macro that replaces the sequence of [shortcut, other_token] with + * (replacement other_token) <- a list with two objects, replacement is a symbol. + * These are used to make 'x turn into (quote x) and similar. + */ +void Reader::add_reader_macro(const std::string& shortcut, std::string replacement) { + reader_macros[shortcut] = std::move(replacement); +} + +/*! + * Try to read an object. + */ +bool Reader::read_object(Token& tok, TextStream& ts, Object& obj) { + try { + // try as integer + if (try_token_as_integer(tok, obj)) { + return true; + } + + // try as hex + if (try_token_as_hex(tok, obj)) { + return true; + } + + // try as binary + if (try_token_as_binary(tok, obj)) { + return true; + } + + // try as float + if (try_token_as_float(tok, obj)) { + return true; + } + + // try as string + if (tok.text[0] == '"') { + // it's a string. + assert(tok.text.length() == 1); + if (read_string(ts, obj)) { + return true; + } else { + throw_reader_error(ts, "failed to read string, close quote not found", -1); + return false; + } + } + + if (tok.text[0] == '#' && tok.text.size() >= 2 && tok.text[1] == '(') { + if (read_array(ts, obj)) { + return true; + } + } + + if (try_token_as_char(tok, obj)) { + return true; + } + + // try as symbol + if (try_token_as_symbol(tok, obj)) { + return true; + } + } catch (std::exception& e) { + throw_reader_error(ts, "parsing token " + tok.text + " failed: " + e.what(), -1); + } + + return false; +} + +bool Reader::read_array(TextStream& stream, Object& o) { + // assert(stream.read() == '('); + stream.seek_past_whitespace_and_comments(); + std::vector objects; + + bool got_close_paren = false; + while (stream.text_remains()) { + auto tok = get_next_token(stream); + assert(!tok.text.empty()); + + if (tok.text[0] == '(') { + assert(tok.text.length() == 1); + objects.push_back(read_list(stream, true)); + stream.seek_past_whitespace_and_comments(); + continue; + } else if (tok.text[0] == ')') { + assert(tok.text.length() == 1); + got_close_paren = true; + break; + } else { + Object next_obj; + if (read_object(tok, stream, next_obj)) { + stream.seek_past_whitespace_and_comments(); + objects.push_back(next_obj); + } else { + throw_reader_error(stream, "invalid token encountered in array reader: " + tok.text, + -int(tok.text.size())); + } + } + } + + if (!got_close_paren) { + throw_reader_error(stream, "An array must end in a close parenthesis", -1); + return false; + } + + o = ArrayObject::make_new(objects); + return true; +} + +/*! + * 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 objects; + + bool got_close_paren = false; // does this list end? + bool got_dot = false; // did we get a . ? + bool got_thing_after_dot = false; // did we get an object after the . ? + int start_offset = ts.seek; + + // loop over tokens + while (ts.text_remains()) { + auto tok = get_next_token(ts); + + // reader macro thing: + bool got_reader_macro = false; + + std::string reader_macro_string; + auto kv = reader_macros.find(tok.text); + if (kv != reader_macros.end()) { + // we found a reader macro! Remember this, and get the next token. + got_reader_macro = true; + reader_macro_string = kv->second; + tok = get_next_token(ts); + } else { + // no reader macro + if (tok.text == ".") { + // list dot notation (ex, (1 . 2)) + if (got_dot) { + throw_reader_error(ts, "A list cannot have multiple dots.", -1); + } + ts.seek_past_whitespace_and_comments(); + if (!ts.text_remains()) { + throw_reader_error(ts, "A list cannot end in a dot", -1); + } + tok = get_next_token(ts); + got_dot = true; + } + } + + // inserter function, used to properly insert a next object + auto insert_object = [&](Object o) { + if (got_thing_after_dot) { + throw_reader_error(ts, "A list cannot have multiple entries after the dot", -1); + } + + // create child list if we got a reader macro (ex 'x -> (quote x)) + if (got_reader_macro) { + objects.push_back( + build_list({SymbolObject::make_new(symbolTable, reader_macro_string), o})); + } else { + objects.push_back(o); + } + + // remember if we got an object after the dot + if (got_dot) { + got_thing_after_dot = true; + } + }; + + if (tok.text.empty()) { + assert(false); + // empty list + break; + } else if (tok.text[0] == '(') { + // nested list + assert(tok.text.length() == 1); + insert_object(read_list(ts, true)); + ts.seek_past_whitespace_and_comments(); + continue; + } else if (tok.text[0] == ')') { + // end of this list + got_close_paren = true; + assert(tok.text.length() == 1); + break; + } else { + // try to get an object + Object obj; + + if (read_object(tok, ts, obj)) { + ts.seek_past_whitespace_and_comments(); + insert_object(obj); + } else { + throw_reader_error(ts, "invalid token encountered in reader: " + tok.text, + -int(tok.text.size())); + } + } + } + + // done getting objects. Check close paren and dot + if (expect_close_paren && !got_close_paren) { + throw_reader_error(ts, "failed to find close paren", -1); + } + + if (got_close_paren && !expect_close_paren) { + throw_reader_error(ts, "found an unexpected close paren", -1); + } + + if (got_dot && !got_thing_after_dot) { + throw_reader_error(ts, "A list must have an entry after the dot", -1); + } + + // build up list or improper list, link it, and return! + if (got_thing_after_dot) { + if (objects.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 lst = rv; + while (true) { + if (lst.as_pair()->cdr.is_empty_list()) { + lst.as_pair()->cdr = back; + break; + } else { + lst = lst.as_pair()->cdr; + } + } + db.link(rv, ts.text, start_offset); + return rv; + } else { + auto rv = build_list(objects); + db.link(rv, ts.text, start_offset); + return rv; + } +} + +/*! + * Try decoding as symbol. Returns success. + */ +bool Reader::try_token_as_symbol(const Token& tok, Object& obj) { + // check start character is valid: + assert(!tok.text.empty()); + char start = tok.text[0]; + if (valid_symbols_chars[(int)start]) { + obj = SymbolObject::make_new(symbolTable, tok.text); + return true; + } else { + return false; + } +} + +/*! + * Read a string and escape. Start on the first char after the first double quote. + * Supported escapes are \n, \t, \\ and work like they do in C. + */ +bool Reader::read_string(TextStream& stream, Object& obj) { + bool got_close_quote = false; + std::string str; + + while (stream.text_remains()) { + char c = stream.read(); + if (c == '"') { + obj = StringObject::make_new(str); + got_close_quote = true; + break; + } + + if (c == '\\') { + if (!stream.text_remains()) { + throw_reader_error(stream, "incomplete string escape code", -1); + } + if (stream.peek() == 'n') { + stream.read(); + str.push_back('\n'); + } else if (stream.peek() == 't') { + stream.read(); + str.push_back('\t'); + } else if (stream.peek() == '\\') { + stream.read(); + str.push_back('\\'); + } else if (stream.peek() == '"') { + stream.read(); + str.push_back('"'); + } else { + throw_reader_error(stream, "unknown string escape code", -1); + } + } else { + str.push_back(c); + } + } + + return got_close_quote; +} + +namespace { +/*! + * Is this a valid character to start a decimal integer number? + */ +bool decimal_start(char c) { + return (c >= '0' && c <= '9') || c == '-'; +} + +/*! + * Is this a valid character to start a floating point number? + */ +bool float_start(char c) { + return (c >= '0' && c <= '9') || c == '-' || c == '.'; +} + +/*! + * Does the given string contain c? + */ +bool str_contains(const std::string& str, char c) { + for (auto& x : str) { + if (x == c) { + return true; + } + } + return false; +} +} // namespace + +/*! + * Try decoding as a float. Must have a "." in it. + * Otherwise all combinations of leading zeros, "."'s, negative signs, etc are ok. + * Trailing zeros not required. + */ +bool Reader::try_token_as_float(const Token& tok, Object& obj) { + if (float_start(tok.text[0]) && str_contains(tok.text, '.')) { + size_t offset = tok.text[0] == '-' ? 1 : 0; + for (; offset < tok.text.size(); offset++) { + char c = tok.text.at(offset); + if ((c < '0' || c > '9') && (c != '.')) { + return false; + } + } + + try { + std::size_t end = 0; + double v = std::stod(tok.text, &end); + if (end != tok.text.size()) + return false; + obj = Object::make_float(v); + return true; + } catch (std::exception& e) { + return false; + } + } + return false; +} + +/*! + * Try decoding as binary. Looks like #b101010 ... + * 64-bit unsigned + */ +bool Reader::try_token_as_binary(const Token& tok, Object& obj) { + if (tok.text.size() >= 3 && tok.text[0] == '#' && tok.text[1] == 'b') { + for (size_t offset = 2; offset < tok.text.size(); offset++) { + char c = tok.text.at(offset); + if (c != '0' && c != '1') { + return false; + } + } + + uint64_t value = 0; + + for (uint32_t i = 2; i < tok.text.size(); i++) { + if (value & (0x8000000000000000)) { + throw std::runtime_error("overflow in binary constant: " + tok.text); + } + + value <<= 1u; + if (tok.text[i] == '1') { + value++; + } else if (tok.text[i] != '0') { + return false; + } + } + obj = Object::make_integer((int64_t)value); + return true; + } + return false; +} + +/*! + * Try decoding as hex. Looks like #xdeadBEEF . Don't care about case. + * 64-bit unsigned + */ +bool Reader::try_token_as_hex(const Token& tok, Object& obj) { + if (tok.text.size() >= 3 && tok.text[0] == '#' && tok.text[1] == 'x') { + // determine if we look like a number or not. If we look like a number, but stoll fails, + // it means that the number is too big or too small, and we should error + for (size_t offset = 2; offset < tok.text.size(); offset++) { + char c = tok.text.at(offset); + if ((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) { + return false; + } + } + + uint64_t v = 0; + try { + std::size_t end = 0; + v = std::stoull(tok.text.substr(2), &end, 16); + if (end + 2 != tok.text.size()) + return false; + obj = Object::make_integer(v); + return true; + } catch (std::exception& e) { + throw std::runtime_error("The number " + tok.text + " cannot be a hexadecimal constant"); + } + } + return false; +} + +/*! + * Try decoding as integer. No decimals points allowed. + * 64-bit signed. Won't accept values between INT64_MAX and UINT64_MAX. + */ +bool Reader::try_token_as_integer(const Token& tok, Object& obj) { + if (decimal_start(tok.text[0]) && !str_contains(tok.text, '.')) { + // determine if we look like a number or not. If we look like a number, but stoll fails, + // it means that the number is too big or too small, and we should error + size_t offset = tok.text[0] == '-' ? 1 : 0; + if (offset == 1 && tok.text.size() == 1) { + return false; // - by itself is not a number! + } + for (; offset < tok.text.size(); offset++) { + char c = tok.text.at(offset); + if (c < '0' || c > '9') { + return false; + } + } + + uint64_t v = 0; + try { + std::size_t end = 0; + v = std::stoll(tok.text, &end); + if (end != tok.text.size()) + return false; + obj = Object::make_integer(v); + return true; + } catch (std::exception& e) { + throw std::runtime_error("The number " + tok.text + " cannot be an integer constant"); + } + } + return false; +} + +bool Reader::try_token_as_char(const Token& tok, Object& obj) { + if (tok.text.size() >= 3 && tok.text[0] == '#' && tok.text[1] == '\\') { + if (tok.text.size() == 3 && util::is_printable_char(tok.text[2]) && tok.text[2] != ' ') { + obj = Object::make_char(tok.text[2]); + return true; + } + + if (tok.text.size() == 4 && tok.text[2] == '\\') { + switch (tok.text[3]) { + case 'n': + obj = Object::make_char('\n'); + return true; + case 's': + obj = Object::make_char(' '); + return true; + case 't': + obj = Object::make_char('\t'); + return true; + } + } + } + return false; +} + +/*! + * Throw an exception with useful information because of an error in the text stream. + * Used for reader errors, like "missing close paren" or similar. + */ +void Reader::throw_reader_error(TextStream& here, const std::string& err, int seek_offset) { + throw std::runtime_error("Reader error:\n" + err + "\nat " + + db.get_info_for(here.text, here.seek + seek_offset)); +} + +/*! + * Get the source directory of the current project. + */ +std::string Reader::get_source_dir() { + return source_dir; +} +} // namespace goos \ No newline at end of file diff --git a/goalc/goos/Reader.h b/goalc/goos/Reader.h new file mode 100644 index 0000000000..1ac7e6a7a1 --- /dev/null +++ b/goalc/goos/Reader.h @@ -0,0 +1,105 @@ +/*! + * @file Reader.h + * + * The Reader converts text into GOOS object, for interpreting or compiling. + * The Reader also stores the GOOS symbol table, and is able to figure out where forms + * came from, for printing error messages about forms. + * + * The reader also know where the source folder is, through an environment variable set when + * launching the compiler or the compiler test. + */ + +#ifndef JAK1_READER_H +#define JAK1_READER_H + +#include +#include +#include +#include + +#include "goalc/goos/Object.h" +#include "goalc/goos/TextDB.h" + +namespace goos { + +/*! + * Wrapper around a source of text that allows reading/peeking. + */ +struct TextStream { + explicit TextStream(std::shared_ptr ptr) { text = std::move(ptr); } + + std::shared_ptr text; + int seek = 0; + int line_count = 0; + + char peek() { + assert(seek < text->get_size()); + return text->get_text()[seek]; + } + + char peek(int i) { + assert(seek + i < text->get_size()); + return text->get_text()[seek + i]; + } + + char read() { + assert(seek < text->get_size()); + char c = text->get_text()[seek++]; + if (c == '\n') + line_count++; + return c; + } + + bool text_remains() { return seek < text->get_size(); } + bool text_remains(int i) { return seek + i < text->get_size(); } + void seek_past_whitespace_and_comments(); +}; + +/*! + * A Token used for parsing. + */ +struct Token { + std::shared_ptr source_text; + int source_offset; + int source_line; + std::string text; +}; + +class Reader { + public: + Reader(); + Object read_from_string(const std::string& str); + Object read_from_stdin(const std::string& prompt_name); + Object read_from_file(const std::string& filename); + + std::string get_source_dir(); + + SymbolTable symbolTable; + TextDb db; + + private: + Object internal_read(std::shared_ptr text); + Object read_list(TextStream& stream, bool expect_close_paren = true); + bool read_object(Token& tok, TextStream& ts, Object& obj); + bool read_array(TextStream& stream, Object& o); + + void throw_reader_error(TextStream& here, const std::string& err, int seek_offset); + Token get_next_token(TextStream& stream); + + bool try_token_as_symbol(const Token& tok, Object& obj); + bool try_token_as_char(const Token& tok, Object& obj); + bool try_token_as_float(const Token& tok, Object& obj); + bool try_token_as_binary(const Token& tok, Object& obj); + bool try_token_as_hex(const Token& tok, Object& obj); + bool try_token_as_integer(const Token& tok, Object& obj); + bool read_string(TextStream& stream, Object& obj); + void add_reader_macro(const std::string& shortcut, std::string replacement); + + char valid_symbols_chars[256]; + + std::string source_dir; + std::unordered_map reader_macros; +}; +} // namespace goos + +#endif // JAK1_READER_H diff --git a/goalc/goos/TextDB.cpp b/goalc/goos/TextDB.cpp new file mode 100644 index 0000000000..75ad3369a1 --- /dev/null +++ b/goalc/goos/TextDB.cpp @@ -0,0 +1,132 @@ +/*! + * @file TextDB.h + * The Text Database for storing source code text. + * This allows us to ask for things like "where did this form come from?" + * and be able to print the file name and offset into the file, as well as the line. + * + * The purpose is to be able to have an error message like: + * + * Error on (+ a b): a has invalid type (string) + * From my-file.gc, line 25: + * (+ 1 (+ a b)) ; compute the sum + */ + +#include "goalc/util/file_io.h" + +#include "TextDB.h" + +namespace goos { + +/*! + * Initialize with the given string + */ +SourceText::SourceText(std::string r) : text(std::move(r)) { + // find line breaks + build_offsets(); +} + +/*! + * Update line break data. Should be called any time the text is updated. + */ +void SourceText::build_offsets() { + offset_by_line.clear(); + offset_by_line.push_back(0); + for (uint32_t i = 0; i < text.size(); i++) { + if (text[i] == '\n') { + offset_by_line.push_back(i); + } + } + offset_by_line.push_back(text.size()); +} + +/*! + * Get the text of the line containing the character at position "offset" from this source. + */ +std::string SourceText::get_line_containing_offset(int offset) { + auto range = get_containing_line(offset); + int start_offset = range.first ? 1 : 0; + return text.substr(range.first + start_offset, + std::max(0, range.second - range.first - start_offset)); +} + +/*! + * Get the index of the line containing the character at position "offset". + * O(n_lines) crappy implementation. + * Error if not found. + */ +int SourceText::get_line_idx(int offset) { + for (uint32_t line = 0; line < offset_by_line.size() - 1; line++) { + if (offset >= offset_by_line[line] && offset <= offset_by_line[line + 1]) { + return line; + } + } + assert(false); +} + +/*! + * Gets the [start, end) character offset of the line containing the given offset. + */ +std::pair SourceText::get_containing_line(int offset) { + for (uint32_t line = 0; line < offset_by_line.size() - 1; line++) { + if (offset >= offset_by_line[line] && offset <= offset_by_line[line + 1]) { + return std::make_pair(offset_by_line[line], offset_by_line[line + 1]); + } + } + return std::make_pair(0, text.size()); +} + +/*! + * Read text from a file. + */ +FileText::FileText(std::string filename_) : filename(std::move(filename_)) { + text = util::read_text_file(filename); + build_offsets(); +} + +/*! + * Inform the TextDB about a source of text. + */ +void TextDb::insert(const std::shared_ptr& frag) { + fragments.push_back(frag); +} + +/*! + * Link the GOOS object o to the offset into the given text fragment. + * The object _must_ be a pair or empty list. + */ +void TextDb::link(const Object& o, std::shared_ptr frag, int offset) { + if (o.is_empty_list()) + return; + assert(o.is_pair()); + TextRef ref; + ref.offset = offset; + ref.frag = std::move(frag); + map[o.heap_obj] = ref; +} + +/*! + * Given an object, get a string representing where it's from. Or "?" if we can't find it. + */ +std::string TextDb::get_info_for(const Object& o) { + if (o.is_pair()) { + auto kv = map.find(o.heap_obj); + if (kv != map.end()) { + return get_info_for(kv->second.frag, kv->second.offset); + } else { + return "?"; + } + } else { + return "?"; + } +} + +/*! + * Given a source text and an offset, print a description of where it is. + */ +std::string TextDb::get_info_for(const std::shared_ptr& frag, int offset) { + std::string result = "text from " + frag->get_description() + + ", line: " + std::to_string(frag->get_line_idx(offset) + 1) + "\n"; + result += frag->get_line_containing_offset(offset) + "\n"; + return result; +} +} // namespace goos diff --git a/goalc/goos/TextDB.h b/goalc/goos/TextDB.h new file mode 100644 index 0000000000..6abf8f7f60 --- /dev/null +++ b/goalc/goos/TextDB.h @@ -0,0 +1,100 @@ +/*! + * @file TextDB.h + * The Text Database for storing source code text. + * This allows us to ask for things like "where did this form come from?" + * and be able to print the file name and offset into the file, as well as the line. + * + * The purpose is to be able to have an error message like: + * + * Error on (+ a b): a has invalid type (string) + * From my-file.gc, line 25: + * (+ 1 (+ a b)) ; compute the sum + */ + +#ifndef JAK1_TEXTDB_H +#define JAK1_TEXTDB_H + +#include +#include +#include +#include +#include + +#include "goalc/goos/Object.h" + +namespace goos { +/*! + * Parent class for source-code text organized in lines + */ +class SourceText { + public: + explicit SourceText(std::string r); + SourceText() = default; + const char* get_text() { return text.c_str(); } + int get_size() { return text.size(); } + virtual std::string get_description() = 0; + std::string get_line_containing_offset(int offset); + int get_line_idx(int offset); + + virtual ~SourceText(){}; + + protected: + void build_offsets(); + std::string text; + std::vector offset_by_line; + std::pair get_containing_line(int offset); +}; + +/*! + * Text from the REPL prompt + */ +class ReplText : public SourceText { + public: + explicit ReplText(const std::string& text_) : SourceText(text_) {} + std::string get_description() override { return "REPL"; } + ~ReplText() = default; +}; + +/*! + * Text generated by the program itself (like for example, with (read "hello")) + */ +class ProgramString : public SourceText { + public: + explicit ProgramString(const std::string& text_) : SourceText(text_) {} + std::string get_description() override { return "Program string"; } + ~ProgramString() = default; +}; + +/*! + * Text from a file. + */ +class FileText : public SourceText { + public: + FileText(std::string filename_); + + std::string get_description() { return filename; } + ~FileText() = default; + + private: + std::string filename; +}; + +struct TextRef { + int offset; + std::shared_ptr frag; +}; + +class TextDb { + public: + void insert(const std::shared_ptr& frag); + void link(const Object& o, std::shared_ptr frag, int offset); + std::string get_info_for(const Object& o); + std::string get_info_for(const std::shared_ptr& frag, int offset); + + private: + std::vector> fragments; + std::unordered_map, TextRef> map; +}; +} // namespace goos + +#endif // JAK1_TEXTDB_H diff --git a/goalc/gs/goos-lib.gs b/goalc/gs/goos-lib.gs new file mode 100644 index 0000000000..dd6a9c8228 --- /dev/null +++ b/goalc/gs/goos-lib.gs @@ -0,0 +1,145 @@ +;-*-Scheme-*- + +;; THE GOOS COMMON LIBRARY + +;; goos macro to define a new goos macro +(define defsmacro (macro (name args &rest body) + `(define ,name (macro ,args ,@body)) + ) + ) + +;; macro to define a new goos function +(defsmacro desfun (name args &rest body) + `(define ,name (lambda ,args ,@body)) + ) + +;; goos macro to let us define goal macros from goos: +(defsmacro defgmacro (name args &rest body) + `(define :env *goal-env* ,name (macro ,args ,@body)) + ) + + +(defsmacro if (clause true false) + `(cond (,clause ,true) (#t ,false)) + ) + +(defsmacro not (x) + `(if ,x #f #t) + ) + +(desfun factorial (x) + (if (= x 1) + 1 + (* x (factorial (- x 1))) + ) + ) + +(defsmacro caar (x) + `(car (car ,x))) + +(defsmacro cadr (x) + `(car (cdr ,x))) + +(defsmacro cdar (x) + `(cdr (car ,x))) + +(defsmacro cddr (x) + `(cdr (cdr ,x))) + +(desfun first (x) + (car x)) + +(desfun rest (x) + (cdr x)) + +(desfun second (x) + (car (cdr x)) + ) + +(desfun third (x) + (car (cddr x))) + +(desfun apply (fun x) + (if (null? x) + '() + (cons (fun (car x)) + (apply fun (cdr x)) + ) + ) + ) + +(desfun filter (pred lst) + (cond ((null? lst) '()) + ((pred (first lst)) + (cons (first lst) (filter pred (cdr lst)))) + (#t (filter pred (cdr lst))))) + +(desfun assoc (x a) + (if (eq? (caar a) x) + (car a) + (assoc x (cdr a)) + ) + ) + +(defsmacro let (bindings &rest body) + `((lambda ,(apply first bindings) ,@body) + ,@(apply second bindings))) + +(defsmacro let* (bindings &rest body) + (if (null? bindings) + `(begin ,@body) + `((lambda (,(caar bindings)) + (let* ,(cdr bindings) ,@body)) + ;;(begin ,@(cdar bindings)) + ,(car (cdar bindings)) + ) + ) + ) + + + +(defsmacro with-gensyms (names &rest body) + `(let + ,(apply (lambda (x) `(,x (gensym))) names) + ,@body + ) + ) + + +(desfun length (lst) + (if (null? lst) + 0 + (+ 1 (length (cdr lst))) + ) + ) + +(defsmacro inc! (x) `(set! ,x (+ ,x 1))) +(defsmacro dec! (x) `(set! ,x (- ,x 1))) +(defsmacro +! (place arg) `(set! ,place (+ ,place ,arg))) +(defsmacro -! (place arg) `(set! ,place (- ,place ,arg))) + + +(defsmacro string? (x) + `(type? 'string ,x)) + + +;; Bootstrap GOAL macro system + + +;; goal macro to define a goal macro +(defgmacro defmacro (name args &rest body) + `(seval (defgmacro ,name ,args ,@body)) + ) + +;; goal macro to define a goos macro +(defgmacro defsmacro (name args &rest body) + `(seval (defsmacro ,name ,args ,@body)) + ) + +;; goal macro to define a goos function +(defgmacro desfun (name args &rest body) + `(seval (desfun ,name ,args ,@body)) + ) + +;; this is checked in a test to see if this file is loaded. +(define __goos-lib-loaded__ #t) \ No newline at end of file diff --git a/goalc/listener/CMakeLists.txt b/goalc/listener/CMakeLists.txt new file mode 100644 index 0000000000..470fe89d37 --- /dev/null +++ b/goalc/listener/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(listener SHARED + Listener.cpp Deci2Server.cpp Deci2Server.h) diff --git a/goalc/listener/Deci2Server.cpp b/goalc/listener/Deci2Server.cpp new file mode 100644 index 0000000000..9b2a29f942 --- /dev/null +++ b/goalc/listener/Deci2Server.cpp @@ -0,0 +1,264 @@ +/*! + * @file Deci2Server.cpp + * Basic implementation of a DECI2 server. + * Works with deci2.cpp (sceDeci2) to implement the networking on target + */ + +#include +#include +#include +#include +#include +#include + +#include "common/listener_common.h" +#include "common/versions.h" +#include "Deci2Server.h" + +Deci2Server::Deci2Server(std::function shutdown_callback) { + buffer = new char[BUFFER_SIZE]; + want_exit = std::move(shutdown_callback); +} + +Deci2Server::~Deci2Server() { + // if accept thread is running, kill it + if (accept_thread_running) { + kill_accept_thread = true; + accept_thread.join(); + accept_thread_running = false; + } + + delete[] buffer; + + if (server_fd >= 0) { + close(server_fd); + } + + if (new_sock >= 0) { + close(new_sock); + } +} + +/*! + * Start waiting for the Listener to connect + */ +bool Deci2Server::init() { + server_fd = socket(AF_INET, SOCK_STREAM, 0); + if (server_fd < 0) { + server_fd = -1; + return false; + } + + int opt = 1; + if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { + printf("[Deci2Server] Failed to setsockopt 1\n"); + close(server_fd); + server_fd = -1; + return false; + } + + int one = 1; + if (setsockopt(server_fd, SOL_TCP, TCP_NODELAY, &one, sizeof(one))) { + printf("[Deci2Server] Failed to setsockopt 2\n"); + close(server_fd); + server_fd = -1; + return false; + } + + timeval timeout = {}; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + + if (setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)) < 0) { + printf("[Deci2Server] Failed to setsockopt 3\n"); + close(server_fd); + server_fd = -1; + return false; + } + + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(DECI2_PORT); + + if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { + printf("[Deci2Server] Failed to bind\n"); + close(server_fd); + server_fd = -1; + return false; + } + + if (listen(server_fd, 0) < 0) { + printf("[Deci2Server] Failed to listen\n"); + close(server_fd); + server_fd = -1; + return false; + } + + server_initialized = true; + accept_thread_running = true; + kill_accept_thread = false; + accept_thread = std::thread(&Deci2Server::accept_thread_func, this); + return true; +} + +/*! + * Return true if the listener is connected. + */ +bool Deci2Server::check_for_listener() { + if (server_connected) { + if (accept_thread_running) { + accept_thread.join(); + accept_thread_running = false; + } + return true; + } else { + return false; + } +} + +/*! + * Send data from buffer. User must provide appropriate headers. + */ +void Deci2Server::send_data(void* buf, u16 len) { + lock(); + if (!server_connected) { + printf("[DECI2] send while not connected, not sending!\n"); + } else { + uint16_t prog = 0; + while (prog < len) { + auto wrote = write(new_sock, (char*)(buf) + prog, len - prog); + prog += wrote; + if (!server_connected || want_exit()) { + unlock(); + return; + } + } + } + unlock(); +} + +/*! + * Lock the DECI mutex. Should be done before modifying protocols. + */ +void Deci2Server::lock() { + deci_mutex.lock(); +} + +/*! + * Unlock the DECI mutex. Should be done after modifying protocols. + */ +void Deci2Server::unlock() { + deci_mutex.unlock(); +} + +/*! + * Wait for protocols to become ready. + * This avoids the case where we receive messages before protocol handlers are set up. + */ +void Deci2Server::wait_for_protos_ready() { + if (protocols_ready) + return; + std::unique_lock lk(deci_mutex); + cv.wait(lk, [&] { return protocols_ready; }); +} + +/*! + * Inform server that protocol handlers are ready. + * Will unblock wait_for_protos_ready and incoming messages will be dispatched to these + * protocols. You can change the protocol handlers, but you should lock the mutex before + * doing so. + */ +void Deci2Server::send_proto_ready(Deci2Driver* drivers, int* driver_count) { + lock(); + d2_drivers = drivers; + d2_driver_count = driver_count; + protocols_ready = true; + unlock(); + cv.notify_all(); +} + +void Deci2Server::run() { + int desired_size = (int)sizeof(Deci2Header); + int got = 0; + + while (got < desired_size) { + assert(got + desired_size < BUFFER_SIZE); + auto x = read(new_sock, buffer + got, desired_size - got); + if (want_exit()) { + return; + } + got += x > 0 ? x : 0; + } + + auto* hdr = (Deci2Header*)(buffer); + printf("[DECI2] Got message:\n"); + printf(" %d %d 0x%x %c -> %c\n", hdr->len, hdr->rsvd, hdr->proto, hdr->src, hdr->dst); + + hdr->rsvd = got; + + // see what protocol we got: + lock(); + + int handler = -1; + for (int i = 0; i < *d2_driver_count; i++) { + auto& prot = d2_drivers[i]; + if (prot.active && prot.protocol) { + if (handler != -1) { + printf("[DECI2] Warning: more than on protocol handler for this message!\n"); + } + handler = i; + } + } + + if (handler == -1) { + printf("[DECI2] Warning: no handler for this message, ignoring...\n"); + unlock(); + return; + // throw std::runtime_error("no handler!"); + } + + auto& driver = d2_drivers[handler]; + + int sent_to_program = 0; + while (!want_exit() && (hdr->rsvd < hdr->len || sent_to_program < hdr->rsvd)) { + // send what we have to the program + if (sent_to_program < hdr->rsvd) { + // driver.next_recv_size = 0; + // driver.next_recv = nullptr; + driver.recv_buffer = buffer + sent_to_program; + driver.available_to_receive = hdr->rsvd - sent_to_program; + (driver.handler)(DECI2_READ, driver.available_to_receive, driver.opt); + // memcpy(driver.next_recv, buffer + sent_to_program, driver.next_recv_size); + sent_to_program += driver.recv_size; + } + + // receive from network + if (hdr->rsvd < hdr->len) { + auto x = read(new_sock, buffer + hdr->rsvd, hdr->len - hdr->rsvd); + if (want_exit()) { + return; + } + got += x > 0 ? x : 0; + hdr->rsvd += got; + } + } + + (driver.handler)(DECI2_READDONE, 0, driver.opt); + unlock(); +} + +/*! + * Background thread for waiting for the listener. + */ +void Deci2Server::accept_thread_func() { + socklen_t l = sizeof(addr); + while (!kill_accept_thread) { + new_sock = accept(server_fd, (sockaddr*)&addr, &l); + if (new_sock >= 0) { + u32 versions[2] = {versions::GOAL_VERSION_MAJOR, versions::GOAL_VERSION_MINOR}; + send(new_sock, &versions, 8, 0); // todo, check result? + server_connected = true; + return; + } + } +} diff --git a/goalc/listener/Deci2Server.h b/goalc/listener/Deci2Server.h new file mode 100644 index 0000000000..45e3727851 --- /dev/null +++ b/goalc/listener/Deci2Server.h @@ -0,0 +1,57 @@ +/*! + * @file Deci2Server.h + * Basic implementation of a DECI2 server. + * Works with deci2.cpp (sceDeci2) to implement the networking on target + */ + +#ifndef JAK1_DECI2SERVER_H +#define JAK1_DECI2SERVER_H + +#include +#include +#include +#include +#include +#include "game/system/deci_common.h" // todo, move me! + + +class Deci2Server { + public: + static constexpr int BUFFER_SIZE = 32 * 1024 * 1024; + Deci2Server(std::function shutdown_callback); + ~Deci2Server(); + bool init(); + bool check_for_listener(); + void send_data(void* buf, u16 len); + + void lock(); + void unlock(); + void wait_for_protos_ready(); + void send_proto_ready(Deci2Driver* drivers, int* driver_count); + + void run(); + + + private: + void accept_thread_func(); + bool kill_accept_thread = false; + char* buffer = nullptr; + int server_fd; + sockaddr_in addr; + int new_sock; + bool server_initialized = false; + bool accept_thread_running = false; + bool server_connected = false; + std::function want_exit; + std::thread accept_thread; + + std::condition_variable cv; + bool protocols_ready = false; + std::mutex deci_mutex; + Deci2Driver* d2_drivers = nullptr; + int* d2_driver_count = nullptr; +}; + + + +#endif // JAK1_DECI2SERVER_H diff --git a/goalc/listener/Listener.cpp b/goalc/listener/Listener.cpp new file mode 100644 index 0000000000..72265faaba --- /dev/null +++ b/goalc/listener/Listener.cpp @@ -0,0 +1,241 @@ +/*! + * @file Listener.cpp + * The Listener can connect to a Deci2Server for debugging. + */ + +#include +#include +#include +#include +#include +#include "Listener.h" +#include "common/versions.h" + +using namespace versions; + +namespace listener { +Listener::Listener() { + m_buffer = new char[BUFFER_SIZE]; +} + +Listener::~Listener() { + // shut down the receive thread if needed + disconnect(); + + delete[] m_buffer; + if (socket_fd >= 0) { + close(socket_fd); + } +} + +void Listener::disconnect() { + m_connected = false; + if (receive_thread_running) { + rcv_thread.join(); + receive_thread_running = false; + } +} + +bool Listener::is_connected() const { + return m_connected; +} + +/*! + * Attempt to connect to the target. If the target isn't running, this should fail quickly. + * Returns true if successfully connected. + */ +bool Listener::connect_to_target(const std::string& ip, int port) { + if (m_connected) { + throw std::runtime_error("attempted a Listener::connect_to_target when already connected!"); + } + + if (socket_fd >= 0) { + close(socket_fd); + } + + // construct socket + socket_fd = socket(AF_INET, SOCK_STREAM, 0); + if (socket_fd < 0) { + printf("[Listener] Failed to create socket.\n"); + socket_fd = -1; + return false; + } + + // set timeout for receive + timeval timeout = {}; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + if (setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)) < 0) { + printf("[Listener] setsockopt failed\n"); + close(socket_fd); + socket_fd = -1; + return false; + } + + // set nodelay, which makes small rapid messages faster, but large messages slower + int one = 1; + if (setsockopt(socket_fd, SOL_TCP, TCP_NODELAY, &one, sizeof(one))) { + printf("[Listener] failed to TCP_NODELAY\n"); + close(socket_fd); + socket_fd = -1; + return false; + } + + // setup address + sockaddr_in server_address = {}; + server_address.sin_family = AF_INET; + server_address.sin_port = htons(port); + if (inet_pton(AF_INET, ip.c_str(), &server_address.sin_addr) <= 0) { + printf("[Listener] Invalid IP address.\n"); + close(socket_fd); + socket_fd = -1; + return false; + } + + // connect! + int rv = connect(socket_fd, (sockaddr*)&server_address, sizeof(server_address)); + if (rv < 0) { + printf("[Listener] Failed to connect\n"); + close(socket_fd); + socket_fd = -1; + return false; + } + + // get the GOAL version number, to make sure we connected to the right thing + int32_t version_buffer[2] = {-1, -1}; + int read_tries = 0; + int prog = 0; + bool ok = true; + while (prog < 8) { + auto r = read(socket_fd, version_buffer + prog, 8 - prog); + if (r < 0) { + ok = false; + break; + } + prog += r; + read_tries++; + if (read_tries > 50) { + ok = false; + break; + } + } + if (!ok) { + printf("[Listener] Failed to get version number\n"); + close(socket_fd); + socket_fd = -1; + return false; + } + + printf("Got version %d.%d", version_buffer[0], version_buffer[1]); + if (version_buffer[0] == GOAL_VERSION_MAJOR && version_buffer[1] == GOAL_VERSION_MINOR) { + printf(" OK!\n"); + rcv_thread = std::thread(&Listener::receive_func, this); + m_connected = true; + receive_thread_running = true; + return true; + } else { + printf(", expected %d.%d. Cannot connect.\n", GOAL_VERSION_MAJOR, GOAL_VERSION_MINOR); + close(socket_fd); + socket_fd = -1; + return false; + } +} + +/*! + * Runs in a separate thread to receive messages. + * Will print messages to stdout, or optionally save them. + */ +void Listener::receive_func() { + while (m_connected) { + // attempt to receive a ListenerMessageHeader + int rcvd = 0; + int rcvd_desired = sizeof(ListenerMessageHeader); + char buff[sizeof(ListenerMessageHeader)]; + while (rcvd < rcvd_desired) { + auto got = read(socket_fd, buff + rcvd, rcvd_desired - rcvd); + rcvd += got > 0 ? got : 0; + + // kick us out if we got a bogus read result + if (got == 0 || (got == -1 && errno != EAGAIN)) { + m_connected = false; + } + + // exit this loop if we don't want to be running any more + if (!m_connected) { + return; + } + } + + ListenerMessageHeader* hdr = (ListenerMessageHeader*)buff; + // if(debug_listener) { + // printf("[T -> L] received %d bytes, kind %d\n", + // hdr->deci2_hdr.len, hdr->msg_kind); + // } + + switch (hdr->msg_kind) { + case ListenerMessageKind::MSG_ACK: + // an "ack" message, sent by the target to indicate it got something. + if (!waiting_for_ack) { + printf("[Listener] Got an ack message when we weren't expecting one."); + } + + if (hdr->deci2_header.len < 512) { + // ack's should be < 512 bytes (they are just "ack"). + int ack_recv_prog = 0; + while (rcvd < hdr->deci2_header.len) { + if (!m_connected) + return; + int got = read(socket_fd, ack_recv_buff + ack_recv_prog, hdr->deci2_header.len - rcvd); + got = got > 0 ? got : 0; + rcvd += got; + ack_recv_prog += got; + } + ack_recv_buff[ack_recv_prog] = '\0'; + assert(ack_recv_prog < 512); + got_ack = true; + } else { + printf("[Listener] got invalid ack!\n"); + } + break; + + case ListenerMessageKind::MSG_OUTPUT: + case ListenerMessageKind::MSG_PRINT: { + auto* str_buff = new char[hdr->msg_size + 1]; // plus one for the null terminator + int msg_prog = 0; + while (rcvd < hdr->deci2_header.len) { + if (!m_connected) { + return; + } + + int got = read(socket_fd, str_buff + msg_prog, hdr->deci2_header.len - rcvd); + got = got > 0 ? got : 0; + rcvd += got; + msg_prog += got; + if (got == 0 || (got == -1 && errno != EAGAIN)) { + m_connected = false; + } + } + str_buff[hdr->msg_size] = '\0'; + + if (hdr->msg_kind == ListenerMessageKind::MSG_PRINT) { + printf("%s\n", str_buff); + } else { + printf("[OUTPUT] %s\n", str_buff); + } + + rcv_mtx.lock(); + if (hdr->msg_kind == filter) { + message_record.emplace_back(str_buff); + } + rcv_mtx.unlock(); + + } break; + + default: + printf("unhandled message type %d from target\n", int(hdr->msg_kind)); + break; + } + } +} + +} // namespace listener diff --git a/goalc/listener/Listener.h b/goalc/listener/Listener.h new file mode 100644 index 0000000000..0b5bb59eaa --- /dev/null +++ b/goalc/listener/Listener.h @@ -0,0 +1,47 @@ +/*! + * @file Listener.h + * The Listener can connect to a Deci2Server for debugging. + */ + +#ifndef JAK1_LISTENER_H +#define JAK1_LISTENER_H + +#include +#include +#include +#include +#include "common/common_types.h" +#include "common/listener_common.h" + +namespace listener { +class Listener { + public: + static constexpr int BUFFER_SIZE = 32 * 1024 * 1024; + Listener(); + ~Listener(); + bool connect_to_target(const std::string& ip = "127.0.0.1", int port = DECI2_PORT); + void record_messages(ListenerMessageKind kind); + void stop_recording_messages(); + bool is_connected() const; + void disconnect(); + + + private: + char* m_buffer = nullptr; //! buffer for incoming messages + bool m_connected = false; //! do we think we are connected? + bool receive_thread_running = false; //! is the receive thread unjoined? + int socket_fd = -1; //! socket + bool got_ack = false; + bool waiting_for_ack = false; + + std::thread rcv_thread; + std::mutex rcv_mtx; + void receive_func(); + ListenerMessageKind filter = ListenerMessageKind::MSG_INVALID; + std::vector message_record; + char ack_recv_buff[512]; +}; +} + + +#endif // JAK1_LISTENER_H diff --git a/goalc/main.cpp b/goalc/main.cpp new file mode 100644 index 0000000000..8ff9026a5e --- /dev/null +++ b/goalc/main.cpp @@ -0,0 +1,14 @@ +#include +#include "goalc/goos/Interpreter.h" + +int main(int argc, char** argv) { + (void)argc; + (void)argv; + printf("goal compiler\n"); + + goos::Interpreter interp; + interp.execute_repl(); + + return 0; +} + diff --git a/goalc/util/CMakeLists.txt b/goalc/util/CMakeLists.txt new file mode 100644 index 0000000000..609a1fbb68 --- /dev/null +++ b/goalc/util/CMakeLists.txt @@ -0,0 +1 @@ +add_library(util SHARED text_util.cpp file_io.cpp) \ No newline at end of file diff --git a/goalc/util/MatchParam.h b/goalc/util/MatchParam.h new file mode 100644 index 0000000000..e8a5cc8c12 --- /dev/null +++ b/goalc/util/MatchParam.h @@ -0,0 +1,23 @@ +#ifndef JAK1_MATCHPARAM_H +#define JAK1_MATCHPARAM_H + +namespace util { +template +struct MatchParam { + MatchParam() { is_wildcard = true; } + + // intentionally not explicit so you don't have to put MatchParam(blah) everywhere + MatchParam(T x) { + value = x; + is_wildcard = false; + } + + T value; + bool is_wildcard = true; + + bool operator==(const T& other) const { return is_wildcard || (value == other); } + bool operator!=(const T& other) const { return !(*this == other); } +}; +} // namespace util + +#endif // JAK1_MATCHPARAM_H diff --git a/goalc/util/file_io.cpp b/goalc/util/file_io.cpp new file mode 100644 index 0000000000..d6b35e1735 --- /dev/null +++ b/goalc/util/file_io.cpp @@ -0,0 +1,32 @@ +#include "file_io.h" +#include +#include +#include + +namespace util { +std::string read_text_file(const std::string& path) { + std::ifstream file(path); + if (!file.good()) { + throw std::runtime_error("couldn't open " + path); + } + std::stringstream ss; + ss << file.rdbuf(); + return ss.str(); +} + +std::string combine_path(const std::string& parent, const std::string& child) { + return parent + "/" + child; +} + +std::string combine_path(std::vector path) { + if (path.empty()) { + return {}; + } + std::string result = path.front(); + for (size_t i = 1; i < path.size(); i++) { + result = combine_path(result, path.at(i)); + } + return result; +} + +} // namespace util diff --git a/goalc/util/file_io.h b/goalc/util/file_io.h new file mode 100644 index 0000000000..aae7340ee7 --- /dev/null +++ b/goalc/util/file_io.h @@ -0,0 +1,13 @@ +#ifndef JAK1_FILE_IO_H +#define JAK1_FILE_IO_H + +#include +#include + +namespace util { +std::string read_text_file(const std::string& path); +std::string combine_path(const std::string& parent, const std::string& child); +std::string combine_path(std::vector path); +} // namespace util + +#endif // JAK1_FILE_IO_H diff --git a/goalc/util/text_util.cpp b/goalc/util/text_util.cpp new file mode 100644 index 0000000000..9c6c94b0bb --- /dev/null +++ b/goalc/util/text_util.cpp @@ -0,0 +1,16 @@ +/*! + * @file text_util.cpp + * Utilities for dealing with text. + */ + +#include "text_util.h" + +namespace util { +/*! + * Is c printable? Is true for letters, numbers, symbols, space, false for everything else. + * Note: newline/tab is not considered printable. + */ +bool is_printable_char(char c) { + return c >= ' ' && c <= '~'; +} +} diff --git a/goalc/util/text_util.h b/goalc/util/text_util.h new file mode 100644 index 0000000000..fc61e5aa29 --- /dev/null +++ b/goalc/util/text_util.h @@ -0,0 +1,13 @@ +/*! + * @file text_util.h + * Utilities for dealing with text. + */ + +#ifndef JAK1_TEXT_UTIL_H +#define JAK1_TEXT_UTIL_H + +namespace util { +bool is_printable_char(char c); +} + +#endif // JAK1_TEXT_UTIL_H diff --git a/iso_data/.gitignore b/iso_data/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/iso_data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/out/.gitignore b/out/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/out/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/resources/GAME.CGO b/resources/GAME.CGO new file mode 100644 index 0000000000..43fd3b1526 Binary files /dev/null and b/resources/GAME.CGO differ diff --git a/resources/KERNEL.CGO b/resources/KERNEL.CGO new file mode 100644 index 0000000000..e3d9725111 Binary files /dev/null and b/resources/KERNEL.CGO differ diff --git a/resources/SCREEN1.USA b/resources/SCREEN1.USA new file mode 100644 index 0000000000..72943a16fb --- /dev/null +++ b/resources/SCREEN1.USA @@ -0,0 +1 @@ +aaa diff --git a/resources/TEST.CGO b/resources/TEST.CGO new file mode 100644 index 0000000000..1be4fd9614 Binary files /dev/null and b/resources/TEST.CGO differ diff --git a/resources/TWEAKVAL.MUS b/resources/TWEAKVAL.MUS new file mode 100644 index 0000000000..72943a16fb --- /dev/null +++ b/resources/TWEAKVAL.MUS @@ -0,0 +1 @@ +aaa diff --git a/resources/VAGDIR.AYB b/resources/VAGDIR.AYB new file mode 100644 index 0000000000..72943a16fb --- /dev/null +++ b/resources/VAGDIR.AYB @@ -0,0 +1 @@ +aaa diff --git a/test.sh b/test.sh new file mode 100755 index 0000000000..aa21329103 --- /dev/null +++ b/test.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Directory of this script +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +export NEXT_DIR=$DIR +$DIR/build/test/goalc-test "$@" \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000000..336818892a --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,11 @@ +add_executable(goalc-test + test_main.cpp + test_test.cpp + test_reader.cpp + test_goos.cpp + test_listener_deci2.cpp + test_kernel.cpp + test_CodeTester.cpp + all_jak1_symbols.cpp) + +target_link_libraries(goalc-test goos util listener runtime emitter gtest) \ No newline at end of file diff --git a/test/all_jak1_symbols.cpp b/test/all_jak1_symbols.cpp new file mode 100644 index 0000000000..9004c70890 --- /dev/null +++ b/test/all_jak1_symbols.cpp @@ -0,0 +1,7943 @@ +#include "all_jak1_symbols.h" + +const char* all_syms[7941] = {"ripple-for-lava", + "lavaspoutdrip", + "steam-small", + "steam-medium", + "small-steam", + "sages-machine", + "flame-pot", + "fire-boulder", + "control-panel", + "v2ogre-roar2", + "evilplant", + "seagulls-2", + "fly8", + "fly1", + "fire-crackle", + "fire-bubble", + "cage-bird-2", + "bubbling-still", + "bird-house", + "bird-4", + "11", + "water-lap-cls", + "training-amb", + "break-dummy", + "startup", + "ndi", + "*logo-volumes-japan-sg*", + "*logo-cam-sg*", + "*ndi-sg*", + "*ndi-cam-sg*", + "*ndi-volumes-sg*", + "*logo-japan-sg*", + "target-title-play", + "target-title-wait", + "logo-slave-init-by-other", + "pole-down", + "path8", + "path7-k", + "path4-k", + "path3-k", + "path2-k", + "path2", + "path1-k", + "submerge", + "chamber-land", + "ramboss-yell", + "ramboss-shield", + "ramboss-land", + "roling-amb", + "robber-taunt", + "robber-flap", + "mole-taunt-1", + "get-mole", + "plant-move", + "plant-dies", + "ogre-amb", + "lava-loop", + "large-steam-lp", + "walk-wood1", + "teeter-wobble", + "bonebridge-fall", + "lava-amb", + "assistant-lavatube-start", + "*assistant-lavatube-start-sg*", + "lavatube-part", + "open?", + "energyhub", + "energyball-init-by-other", + "*energyarm-sg*", + "energylava-idle", + "energybase", + "energyarm-init-by-other", + "energydoor-closing", + "energyarm-fall", + "energyarm-stop", + "energyarm-idle", + "energydoor-closed-till-near", + "energydoor-open-handler", + "energyarm-init", + "energyarm-trans", + "*energyball-sg*", + "*energyhub-sg*", + "*energydoor-sg*", + "energylava", + "energyhub-stopped", + "energybase-idle", + "*energylava-sg*", + "energyhub-set-lava-height", + "darkecobarrel-cycle-time", + "*chainmine-sg*", + "*lavafallsewera-sg*", + "darkecobarrel", + "*lavashortcut-sg*", + "*lavafall-sg*", + "darkecobarrel-mover", + "ripple-for-lavatube-lava", + "*lavabase-sg*", + "lavafall", + "darkecobarrel-leak", + "darkecobarrel-advance-curspawn", + "lavayellowtarp-idle", + "*lavayellowtarp-sg*", + "lavayellowtarp", + "lavafallsewera-idle", + "darkecobarrel-spawner", + "darkecobarrel-mover-move", + "darkecobarrel-mover-die", + "darkecobarrel-base-init", + "darkecobarrel-base-pos", + "lavashortcut-idle", + "lavafall-idle", + "lavafallsewerb", + "ice-monster3", + "yeti-idle", + "yeti-resuming-start", + "yeti-slave", + "*yeti-sg*", + "yeti-first-time-start", + "yeti-slave-appear-land", + "*yeti-nav-enemy-info*", + "snow-bird-bob-func", + "sparticle-snow-birds-moon", + "*ram-sg*", + "ram-boss-proj", + "ram-boss-idle", + "ram-boss-nav-resume", + "ram-boss-proj-growing", + "ram-boss-show-anims", + "ram-boss-init-by-other", + "ram-boss-up-defend-block", + "*ram-boss-nav-enemy-info-no-shield*", + "ram-boss-tracking", + "*ram-boss-sg*", + "ram-boss-forward-defend-block", + "snow-bumper-spawn-fuel-cell", + "snow-bumper", + "snow-bumper-active-close-idle", + "snow-bumper-active-far-idle", + "flutflut-plat-hidden-idle", + "elevator-travel-to-fort", + "*snow-button-sg*", + "flutflut-plat-large", + "snow-button-up-idle", + "flutflut-plat", + "elevator-travel-to-cave", + "snow-log-active", + "snow-eggtop-idle-up", + "*snow-spatula-sg*", + "*snowcam-sg*", + "snow-switch-idle-up", + "snowpusher", + "snow-log", + "snow-switch-event-handler", + "snow-fort-gate-idle-open", + "snow-eggtop-idle-down", + "snow-log-button-activate", + "snowpusher-idle", + "snow-gears-stopped", + "*snow-ball-sg*", + "snow-ball-shadow-idle", + "snow-ball-shadow-init-by-other", + "snow-ball-roller-init-by-other", + "snow-ball-path-info", + "icelurk-step", + "icelurk-land", + "ice-spike-out", + "ice-spike-in", + "ice-monster2", + "ice-breathin", + "group-ice-cube-foot-puff", + "ice-cube-mean-charge", + "ice-cube-trying-to-appear", + "ice-cube-face-player", + "ice-cube-mean-charge-done", + "*ice-cube-break-sg*", + "*ice-cube-nav-enemy-info*", + "ice-cube-default-event-handler", + "target-snowball", + "target-snowball-post", + "cave-trap-default-event-handler", + "*sage-village3-sg*", + "*assistant-village3-sg*", + "*cavegem-sg*", + "*minershort-sg*", + "*minertall-sg*", + "miners-anim-loop", + "minertall", + "minecartsteel", + "minecartsteel-initialize-by-other", + "*minecartsteel-sg*", + "ride-up", + "*med-res-ogre3-sg*", + "villagec-lava", + "*pistons-sg*", + "pistons", + "*gondola-sg*", + "flylurk-plane", + "clone-and-kill-links", + "saw-player", + "plunger-lurker-idle", + "babak-death", + "play-movie?", + "plunger-lurker-flee", + "flying-lurker-inc-try-count", + "*flying-lurker-sg*", + "flying-lurker-rotate", + "plunger-lurker-die", + "flying-lurker-play-intro", + "flying-lurker-handler", + "flying-lurker-calc-anim-speed", + "flying-lurker-idle", + "flying-lurker-die", + "die-big", + "splash", + "*ogre-step-constants*", + "ogre-plat", + "*tntbarrel-sg*", + "ogre-bridge-idle", + "ogre-isle-d", + "*ogre-isle-a-sg*", + "ogre-isle-b", + "*ogre-bridge-sg*", + "ogre-bridge-update-joints", + "ogre-step-c", + "*med-res-snow-sg*", + "ogre-bridge-activate", + "ogre-step-b", + "*ogre-step-c-sg*", + "*ogre-step-a-sg*", + "rock-in-lava", + "ogre-windup", + "ogre-throw", + "sage-villagec", + "ogre-dies", + "cave-trap-give-up", + "hits-head", + "*darkecobarrel-sg*", + "group-ogreboss-bounce-boulder-splash", + "group-ogreboss-boulder-splash", + "speedup", + "break-no-damage", + "emerge2", + "emerge1", + "next-stage", + "boulder", + "ogreboss-idle-loop", + "ogreboss-missile-seek", + "ogreboss-rock-explosion-effect", + "ogreboss-dead", + "ram-give-fuel-cell", + "ogreboss-emerge", + "ogreboss-super-boulder-idle", + "ogreboss-super-boulder-roll", + "ogreboss-blend-hit-anim", + "ogreboss-trigger-steps", + "ogreboss-update-super-boulder", + "ogreboss-bounce-boulder-init-by-other", + "*ogreboss-missile-shadow-control*", + "ogreboss-super-boulder-land", + "ogreboss-die", + "*ogreboss-bounce-boulder-sg*", + "ogreboss-wait-for-player", + "ogreboss-get-targets", + "*ogreboss-shoot-boulder-break-sg*", + "ogreboss-post", + "ogreboss-inc-try-count", + "ogreboss-super-boulder-play-hit-anim", + "ogreboss-roll-boulder", + "ogreboss-debug-adjust-difficulty", + "ogreboss-super-boulder-die", + "ogreboss-update-shuffling", + "ogreboss-move-near", + "ogreboss-stage3-hit", + "ogreboss-stage1", + "ogreboss-idle", + "*spike-sg*", + "crate-darkeco-cluster", + "balloon-popping", + "spike-idle", + "spike", + "spike-down", + "*crate-darkeco-cluster-sg*", + "balloon-idle", + "race-ring-wait", + "*race-ring-sg*", + "race-ring-idle", + "first-ring?", + "race-ring-blue-set-particle-rotation-callback", + "race-ring", + "initial-spline-pos", + "robber-dead", + "robber-calc-anim-speed", + "robber-move", + "robber-initial-notice", + "robber-event-handler", + "*robber-sg*", + "robber-flee", + "snow-fort-gate-activate", + "peeper", + "lightning-mole-hiding", + "check-drop-level-rolling-dirt-finish", + "fly7", + "peeper-down", + "lightning-mole-head-for-hole", + "lightning-mole-yelp", + "fleeing-nav-enemy-adjust-travel", + "fleeing-nav-enemy-adjust-nav-info", + "lightning-mole-debug-run", + "find-adjacent-bounds", + "lightning-mole-task-complete?", + "submerge2", + "fleeing-nav-enemy-info", + "*lightning-mole-hole*", + "lightning-mole-dive", + "lightning-mole", + "aborted", + "unbreak", + "break", + "min-frame", + "*rolling-start-broken-sg*", + "gorge-start", + "dark-plant", + "race-time-less-than", + "pusher-base-init", + "gorge-init", + "gorge-start-ready", + "dark-plant-startup", + "rolling-water", + "rolling-part", + "*pusher-sg*", + "gorge-abort", + "gorge-start-race-aborted", + "pusher-idle", + "gorge-finish-idle", + "gorge-abort-init-by-other", + "lavashortcut", + "happy-plant-init", + "pusher-base", + "gorge-start-racing", + "rolling-start-break", + "ogre-walk", + "gorge-trans", + "spider-vent", + "gorge-finish", + "dark-plant-trans", + "race-time->seconds", + "rollingcam", + "race-time", + "gorge-pusher-idle", + "gorge-start-launch-start-banner", + "race-time-read", + "dark-plant-death", + "gorge-pusher", + "gorge-finish-init-by-other", + "race-time-save", + "*rollingcam-sg*", + "gorge-start-race-finished", + "dark-plant-has-bad-neighbor", + "dark-plant-idle", + "race-time-copy!", + "rolling-start-init-by-other", + "sunkenfisha-idle", + "puffer-wing", + "paddle-boat", + "puffer", + "puffer-patrol", + "puffer-attack", + "*puffer-mean-sg*", + "puffer-die", + "*helix-water*", + "*happy-plant-sg*", + "*helix-button*", + "helix-button-activate", + "helix-slide-door-idle-open", + "*helix-button-sg*", + "helix-water-activated", + "helix-dark-eco", + "helix-button", + "helix-button-idle-up", + "helix-water-idle", + "wind-chimes", + "helix-slide-door-idle-closed", + "helix-water", + "darkecobarrel-mover-init-by-other", + "helix-button-quick-activate", + "*helix-slide-door-sg*", + "helix-button-startup", + "splitb-taunt", + "splitb-spot", + "lightning-mole-hole-post", + "splitb-roars", + "double-lurker-default-event-handler", + "*double-lurker-top-nav-enemy-info*", + "double-lurker-top-resume", + "double-lurker-both-knocked-back", + "double-lurker-show-anims", + "double-lurker-resume", + "*double-lurker-sg*", + "double-lurker-knocked-back", + "double-lurker-waiting-to-die", + "double-lurker-top-on-shoulders-die", + "bully-spin2", + "bully-land", + "bully-jump", + "bully-broken-cage", + "*bully-sg*", + "bully-idle", + "*bully-shadow-control*", + "bully", + "*bully-broken-cage-sg*", + "bully-default-event-handler", + "bully-broken-cage-explode", + "bully-die", + "bully-broken-cage-init-by-other", + "sunken-pipegame-button-init-by-other", + "sunken-pipegame-end-play", + "sunken-pipegame-beat-challenge", + "sunken-pipegame-prize", + "*whirlpool-sg*", + "sunken-water", + "ripple-for-sunken-water", + "robber-got-away", + "trigger-height", + "v2ogre-roar1", + "*floating-launcher-sg*", + "floating-launcher-ready", + "floating-launcher-idle", + "exit-chamber-idle-in-village", + "*blue-eco-charger-orb-sg*", + "exit-chamber-button", + "*exit-chamber-sg*", + "blue-eco-charger-idle", + "blue-eco-charger-orb-init-by-other", + "blue-eco-charger-orb-active", + "exit-chamber-charger-puzzle-beaten", + "blue-eco-charger-stuck-open", + "exit-chamber-items", + "*blue-eco-charger-sg*", + "steam-cap-idle", + "steam-cap", + "qbert-plat-on-die", + "qbert-plat-master", + "*qbert-plat-constants*", + "*qbert-plat-on-sg*", + "qbert-plat-on", + "qbert-plat-event-handler", + "qbert-plat-master-wait-for-door", + "ice-cube-tired", + "qbert-plat-master-do-door", + "tunemeters", + "wall-plat-retracted", + "wall-plat", + "wall-plat-extending", + "*wall-plat-sg*", + "wall-plat-sync-idle", + "wedge-plat-master-idle", + "wedge-plat-outer", + "path6", + "wedge-plat-outer-idle", + "wedge-plat-master", + "wedge-plat-tip", + "wedge-plat", + "puffer-post", + "orbit-plat-bottom", + "*orbit-plat-sg*", + "spider-vent-idle", + "orbit-plat-wait-for-other", + "orbit-plat-bottom-init-by-other", + "orbit-plat", + "orbit-plat-idle", + "orbit-plat-bottom-idle", + "*sun-iris-door-sg*", + "sun-iris-door-closed", + "sun-iris-door-opening", + "square-platform-master-idle", + "square-platform-button", + "*square-platform-sg*", + "ogreboss-bounce-boulder", + "square-platform", + "square-platform-master", + "square-platform-lowering", + "collision-mesh-id", + "*shover-sg*", + "shover", + "*seaweed*", + "side-to-side-plat", + "seaweed-idle", + "water-vol-deadly", + "sunkencam", + "*side-to-side-plat-sg*", + "slide-control-ride", + "slide-control-watch", + "find-target-point", + "target-tube-post", + "tube-sounds", + "*tube-mods*", + "target-tube-death", + "*tube-jump-mods*", + "slide-control", + "target-tube-hit", + "*tube-hit-mods*", + "*TUBE-bank*", + "sunken-part", + "race-time->string", + "drlurker-roar", + "drill-stop", + "driller-lurker-chase", + "driller-lurker-attack", + "driller-lurker", + "driller-lurker-patrol-pause", + "driller-lurker-debug-play-anims", + "driller-lurker-jammed-standing", + "driller-lurker-idle-drilling", + "gnawer-dies", + "gnawer-dying-give-pickups", + "gnawer-joint-callback", + "gnawer-put-items-at-dest", + "gnawer-chewing-on-post", + "*gnawer-segment-sg*", + "gnawer-route", + "gnawer-falling-segment-init-by-other", + "gnawer", + "*gnawer-sg*", + "mother-spider-egg-die", + "*mother-spider-egg-unbroken-sg*", + "mother-spider-egg-on-ground", + "mother-spider-egg-hatch", + "mother-spider-egg-falling", + "mother-spider-history", + "mother-spider-leg-info", + "mother-spider-history-array", + "ogreboss-pick-target", + "mother-spider-thread", + "*spider-jump-mods*", + "robber-debug", + "spiderwebs-idle", + "spiderwebs", + "maincave-part", + "darkcave-part", + "happy-plant-opened", + "cavespatulatwo-idle", + "cavecrusher-idle", + "*cavespatula-sg*", + "cavespatula-idle", + "caveflamepots-active", + "ripple-for-cave-water", + "caveelevator-one-way-idle-end", + "*cavecrusher-sg*", + "caveflamepots", + "snow-part", + "caveelevator-one-way-travel-to-end", + "*maincavecam-sg*", + "maincavecam", + "*cavespatulatwo-sg*", + "cavecrystal-light-control-caveelevator-callback", + "eat", + "billy-rat-die", + "*billy-sidekick-sg*", + "billy-game-update", + "billy-snack-init-by-other", + "billy-done", + "billy-rat-eat", + "billy-snack", + "billy-snack-eat", + "assistant-villagec", + "billy-playing", + "*ogrecam-sg*", + "*billy-sg*", + "*ogreboss-cam-sg*", + "swamp-part", + "square-platform-master-activate", + "kermit-shoot", + "sunken-pipegame-button", + "kermit-dies", + "kermit-set-rotate-dir-to-nav-target", + "kermit-simple-post", + "kermit-get-head-dir", + "kermit-pulse", + "kermit-get-head-dir-xz", + "fly2", + "kermit-long-hop", + "kermit-give-up", + "kermit-get-tongue-target-callback", + "joint-mod-tracker", + "kermit-get-new-patrol-point", + "kermit", + "kermit-tongue-stuck", + "kermit-tongue-pos", + "kermit-attack", + "kermit-retract-tongue", + "kermit-check-tongue-is-clear?", + "double-lurker-top-init-by-other", + "kermit-enable-tongue", + "kermit-pulse-impact", + "kermit-notice", + "kermit-pulse-init-by-other", + "kermit-idle", + "kermit-patrol", + "*kermit-nav-enemy-info*", + "kermit-disable-tongue", + "kermit-player-target-pos", + "swamp-rat-nest-die", + "swamp-rat-nest-idle", + "swamp-rat-nest-dummy-take-damage", + "swamp-rat-nest", + "swamp-rat-nest-victory", + "*swamp-rat-nest-dummy-a-sg*", + "swamp-rat-nest-dummy-a", + "swamp-rat-nest-dummy-idle", + "swamp-rat-nest-check-dummy", + "swamp-rat-nest-active", + "swamp-rat-nest-spawn-rat", + "swamp-rat-nest-default-event-handler", + "swamp-rat-nest-gestate", + "swamp-rat-nest-dummy-draw-spawn-joints", + "*swamp-rat-nest-dummy-b-sg*", + "rat-celebrate", + "lurkrat-bounce", + "6", + "3", + "-2", + "pointer", + "swamp-rat-init-by-other", + "*swamp-rat-sg*", + "swamp-rat", + "*swamp-rat-nav-enemy-info*", + "swamp-rat-default-event-handler", + "swamp-rat-update-wiggle-params", + "swamp-rat-spawn", + "lurkbat-dies", + "swamp-bat-slave-path-post", + "swamp-bat-slave-event-handler", + "swamp-bat-slave-init-by-other", + "swamp-bat-update-path", + "swamp-bat-launch-slaves", + "swamp-bat-slave-get-new-path", + "swamp-bat-slave-die", + "swamp-bat-slave-idle", + "swamp-bat-check-slave-paths-match?", + "swamp-bat-slave-post", + "double-lurker-top-knocked-down", + "swamp-bat-make-path-select-plane", + "swamp-bat-slave-strafe", + "swamp-bat-debug", + "swamp-bat-idle", + "kermit-short-hop", + "swamp-bat-launch-slave", + "grow", + "swamp-rock", + "swamp-spike", + "swamp-battlecontroller", + "swamp-spike-gate-up", + "*swampcam-sg*", + "swamp-rock-init-by-other", + "*tar-plat-constants*", + "balance-plat", + "*tar-plat-sg*", + "swampcam", + "*balance-plat-sg*", + "assistant-levitator-blue-glow", + "*assistant-village2-sg*", + "just-particles", + "assistant-bluehut", + "*cavespatula-darkcave-sg*", + "sage-bluehut", + "arm-sink-evt", + "ogreboss-missile-idle", + "tetherrock-break-evt", + "*swamp-rope-sg*", + "swamp-blimp-idle", + "swamp-rope-break", + "*SWAMP_BLIMP-bank*", + "precursor-arm-die", + "swamp-blimp-oscillator", + "precursor-arm-idle", + "*swamp-tetherrock-sg*", + "swamp-tetherrock", + "swamp-blimp-rand-vector", + "swamp-tetherrock-hide", + "swamp-rope-idle-rock", + "swamp-blimp", + "*swamp-tetherrock-explode-sg*", + "swamp-rope-update-bounding-spheres", + "swamp-blimp-bye-bye", + "tetherrock-get-info", + "*precursor-arm-sg*", + "swamp-rope-init-by-other", + "warrior", + "fireboulder-hover", + "fireboulder-hover-stuff", + "*med-res-rolling1-sg*", + "boulder4-trans-2", + "sunken-pipegame", + "pontoonfive", + "allpontoons-be-clone", + "*ceilingflag-sg*", + "pontoon", + "fleeing-nav-enemy-chase-post-func", + "villageb-water", + "ogreboss-village2-throw", + "fireboulder-idle", + "boulder2-trans-2", + "pontoon-die", + "fly3", + "ogreboss-village2", + "exit-chamber-dummy-idle", + "*pontoonten-constants*", + "allpontoons-idle", + "*allpontoons-sg*", + "ogre-isle", + "villageb-ogreboss", + "ceilingflag-idle", + "meteor", + "puffer-idle", + "exit-chamber-dummy", + "pontoonten", + "*village2cam-sg*", + "*gambler-sg*", + "*med-res-rolling-sg*", + "*ogreboss-village2-sg*", + "snow-switch-idle-down", + "check-drop-level-sagehut2", + "*assistant-firecanyon-sg*", + "offset-army", + "*darkecocan-glow-sg*", + "evilsib-trans-hook-wait", + "evilsib-trans-hook-hover", + "sequenceC", + "sequenceC-trans-hook", + "sequenceA", + "army-info", + "balloonlurker-find-nearest-path-point", + "balloonlurker-die", + "balloonlurker-pilot-die", + "balloonlurker-get-next-path-point", + "bridge-appears", + "balloonlurker-pilot-init-by-other", + "*balloonlurker-constants*", + "balloonlurker-mine-explode", + "balloonlurker-snap-to-path-point", + "balloonlurker-pilot-idle", + "balloonlurker-post", + "balloonlurker-get-path-point", + "*BALLOONLURKER-bank*", + "*swamp-bat-slave-sg*", + "teetertotter-bend", + "target-on-end-of-teetertotter?", + "teetertotter-idle", + "teetertotter-launch", + "spooge1", + "mudlurker-idle", + "mud-lurk-laugh", + "mud-lurk-inhale", + "orient-to-face-target", + "quicksandlurker-missile-impact", + "quicksandlurker-spit", + "quicksandlurker-default-event-handler", + "*quicksandlurker-sg*", + "intersects-nav-mesh?", + "quicksandlurker-hide", + "quicksandlurker-missile-idle", + "quicksandlurker-missile-init-data", + "bonelurker-dies", + "bone-stepl", + "bone-helmet", + "binelurk-roar", + "bonelurker-set-large-bounds-sphere", + "*bonelurker-nav-enemy-info*", + "*bonelurker-sg*", + "walk-sand2", + "ram-boss-proj-launch", + "*muse-sg*", + "muse-caught", + "boulder3-trans", + "point-on-path-segment-info", + "muse-get-path-point", + "*muse-nav-enemy-info*", + "muse-idle", + "muse", + "water-anim-fade-dist", + "ripple-for-mud", + "gnawer-segment", + "ripple-for-small-mud", + "detach", + "keg-init-by-other", + "snow-ball-roller-idle", + "*keg-conveyor-keg-spawn-table*", + "snow-log-button", + "keg-die", + "keg-in-chute", + "keg-on-paddle", + "keg-post", + "keg-update-smush", + "*keg-conveyor-paddle-sg*", + "keg-on-path", + "keg-conveyor-spawn-keg", + "keg-conveyor-idle", + "trigger-rise", + "*silostep-sg*", + "silostep-rise", + "silostep", + "misty-camera-view", + "silostep-camera", + "*rounddoor-sg*", + "animation-select", + "particle-select", + "robber-task-complete?", + "task-complete", + "swamp-amb", + "*flutflut-bluehut-sg*", + "*mis-bone-bridge-sg*", + "boat-fuelcell-spawn", + "*breakaway-right-sg*", + "breakaway-idle", + "boat-fuelcell-idle", + "*sunkenfisha-yellow-blue-sg*", + "*boatpaddle-sg*", + "boat-fuelcell-die", + "bone-platform", + "breakaway-mid", + "minershort-trans-hook", + "breakaway", + "breakaway-fall", + "breakaway-right", + "*bone-platform-constants*", + "mis-bone-bridge-idle", + "boat-fuelcell", + "breakaway-about-to-fall", + "mis-bone-bridge-event-handler", + "boatpaddle", + "actor-wait-for-period", + "tra-bird-bob-func", + "training-part", + "tra-sparticle-seagull-moon", + "quicksandlurker-missile", + "check-drop-level-training-mist", + "exit-chamber-rise", + "*driller-lurker-sg*", + "group-scarecrow-joint-explode", + "group-scarecrow-hit", + "*tra-iris-door-sg*", + "*scarecrow-a-break-sg*", + "scarecrow-b", + "tra-pontoon", + "scarecrow-a", + "*sidekick-human-sg*", + "sidekick-human", + "keg-conveyor-spawn-bouncing-keg", + "sequenceA-village1-trans-hook", + "sequenceA-village1-init-by-other", + "sequenceA-village1", + "check-drop-level-sagehut", + "bird-bob-func", + "fishermans-boat-next-path-point", + "fishermans-boat-leaving-village", + "fishermans-boat-leaving-misty", + "fishermans-boat-entering-misty", + "*fb-evilbro-sg*", + "fishermans-boat-post", + "swamp-rat-nest-dummy-init-by-other", + "fishermans-boat-set-throttle-by-speed", + "*fishermans-boat-constants*", + "fishermans-boat-entering-village", + "ramboss-step", + "boat-stabilizer", + "fishermans-boat-enter-dock?", + "fishermans-boat-wave", + "*boat-turning-radius-table*", + "*fb-evilsis-sg*", + "vehicle-path", + "fishermans-boat-set-dock-point", + "fishermans-boat", + "fishermans-boat-reset-physics", + "fishermans-boat-play-sounds", + "fishermans-boat-measurements", + "fishermans-boat-player-control", + "*windspinner-sg*", + "windspinner", + "*med-res-misty-sg*", + "*med-res-village12-sg*", + "*starfish-nav-enemy-info*", + "reflector-end-idle", + "*med-res-jungle1-sg*", + "puffer-change", + "villagea-water", + "*med-res-village11-sg*", + "*revcycle-sg*", + "snow-spatula-idle", + "sagesail", + "villa-fishb", + "joint-mod-tracker-callback", + "villa-fisha", + "*reflector-middle-sg*", + "village-fish", + "sagesail-idle", + "villa-starfish", + "keg-conveyor", + "mayorgears-idle", + "windmill-sail", + "bully-dies", + "*med-res-beach3-sg*", + "villa-starfish-idle", + "starfish-patrol", + "reflector-middle", + "*hutlamp-sg*", + "yakow-grazing", + "yakow-1", + "yakow-2", + "yakow-task-complete?", + "ogreboss-intro", + "yakow-default-event-handler", + "yakow-die", + "yakow-graze", + "yakow-simple-post", + "yakow-run-away", + "tar-plat", + "yakow-run-post", + "yakow-facing-point?", + "yakow-blend-walk-run", + "yakow-notice", + "*YAKOW-bank*", + "yakow-facing-player?", + "*yakow-sg*", + "yakow-walk-to", + "*tra-pontoon-constants*", + "yakow-kicked", + "sage", + "assistant", + "*assistant-sg*", + "walk-wood2", + "explorer", + "sunkenfisha", + "farmer", + "gondola", + "*farmer-sg*", + "flut-death", + "target-flut-get-off-hit-ground", + "target-flut-running-attack", + "target-flut-get-off", + "target-flut-post", + "target-flut-walk", + "target-flut-jump", + "target-flut-get-off-jump", + "target-flut-falling-anim-trans", + "qsl-fire", + "target-flut-death", + "target-flut-falling", + "target-flut-grab", + "target-flut-post-post", + "flut-bank", + "target-flut-stance", + "target-flut-standard-event-handler", + "target-flut-dangerous-event-handler", + "*flut-walk-mods*", + "target-flut-get-on", + "target-flut-hit-ground-anim", + "blocking-plane-init-by-other", + "*ef-plane-sg*", + "group-target-hit", + "balloonlurker", + "target-racing-get-off-hit-ground", + "target-racing-clone-anim", + "target-racing-get-on", + "target-racing-grab", + "target-racing", + "winter-amb", + "boost", + "racer-collision-reaction", + "target-racing-bounce", + "target-racing-jump-anim", + "racer-thrust", + "racer-cushion", + "racer-buzz", + "racer-effects", + "racer-collision", + "*racer-air-mods*", + "racer-xz", + "*racer-sg*", + "*racer-explode-sg*", + "hud-bike-heat", + "racer-find-prop-point", + "part-hud-racer-heat-func", + "*med-res-jungle2-sg*", + "part-hud-zoomer-heat-slice-02-func", + "part-hud-zoomer-heat-slice-03-func", + "*RACER-bank*", + "racer-info", + "racer-bank", + "steam-release", + "lurkerm-hum", + "plunger-lurker", + "jungle-river", + "heart-drone", + "bridge-hover", + "pc-bridge", + "lurkerm-squeak", + "quicksandlurker-wait", + "logtrap1", + "balloonlurker-patrol", + "accordian-pump", + "starfish-spawn-child", + "launcherdoor", + "*launcherdoor-sg*", + "*launcherdoor-maincave-sg*", + "snow-switch-activate", + "launcherdoor-closed", + "ogre-roar2", + "bad", + "snow-bumper-inactive-idle", + "sun-iris-door", + "emissive-on", + "emissive-off", + "fisher", + "*catch-fishb-sg*", + "fisher-bank", + "fisher-fish-water", + "fisher-params", + "*catch-fisha-sg*", + "*fisher-sg*", + "fisher-fish-die", + "*fish-net-sg*", + "fisher-spawn-ambient", + "lurkerfish-swim", + "lurkerfish-bite", + "activate", + "target-close-to-point?", + "*reflector-mirror-break-sg*", + "periscope-wait-for-power-input", + "periscope-draw-beam-impact", + "reflector-mirror", + "periscope-power-on", + "periscope-find-reflection-angles", + "draw-power-beam", + "periscope-crosshair", + "periscope-wait-for-player", + "periscope-player-control", + "ram-boss-lose-shield", + "*flutflut-plat-large-sg*", + "cam-periscope", + "*periscope-base-sg*", + "periscope-draw-beam", + "reflector-idle", + "exit-chamber", + "periscope-update-joints", + "periscope-has-power-input?", + "*reflector-mirror-sg*", + "periscope-debug-trans", + "reflector-init-by-other", + "periscope-activate", + "peri-beamcam-init-by-other", + "jngpusher", + "ripple-for-jungle-water", + "lurkerm-piston-idle", + "maindoor-open", + "precurbridgecam", + "*jngpusher-sg*", + "jungle-water", + "ogreboss-shoot-boulder", + "*towertop-sg*", + "junglecam", + "sidedoor", + "precurbridge-activate", + "jngpusher-idle", + "swamp-spike-default-event-handler", + "*sidedoor-sg*", + "*jungle-camera-sg*", + "*med-res-firecanyon-sg*", + "*junglecam-sg*", + "*logtrap-sg*", + "maindoor", + "accordian", + "*accordian-sg*", + "ram-fun-idle", + "logtrap", + "*lurkerm-tall-sail-sg*", + "bonelurker-stun", + "precurbridge-active", + "precurbridge-span", + "lurkerm-tall-sail", + "snake-idle", + "*junglesnake-sg*", + "junglesnake-give-up", + "junglesnake-joint-callback", + "gorge-start-idle", + "junglesnake-sleeping", + "junglesnake-attack", + "kermit-post", + "junglesnake-die", + "junglesnake", + "junglesnake-tracking", + "shover-idle", + "frog-jump", + "ogreboss-missile-scale-explosion", + "*FISHER-bank*", + "frog-dies", + "hopper-find-ground", + "hopper", + "splitb-breathin", + "hopper-jump-to", + "hopper-do-jump", + "bouncer-fire", + "sunken-amb", + "bouncer-smush", + "jungle-elevator", + "extra-count", + "helix-slide-door-close", + "irisdoor1", + "darkvine-die", + "darkvine-retreat", + "darkvine-idle", + "darkvine-down", + "aphid-roar", + "aphid-dies", + "plant-roar", + "lurkerm-short-sail-idle", + "plant-ouch", + "plant-laugh", + "plant-fall", + "plant-eye", + "plant-chomp", + "plant-boss-vine-die", + "plant-boss-arm-idle", + "plant-boss-back-arms-idle", + "plant-boss-far-idle", + "plant-boss-leaf-close", + "plant-boss-dead", + "lav-spin-gen", + "plant-boss-arm-init", + "plant-boss-back-arms-die", + "*plant-boss-arm-sg*", + "plant-boss-default-event-handler", + "plant-boss-eat", + "plant-boss-arm-hit", + "*plant-boss-leaf-sg*", + "*plant-boss-sg*", + "grow-faster", + "plant-boss-vine-idle", + "plant-boss-back-arms-hit", + "*plant-boss-back-arms-sg*", + "plant-boss-vulnerable", + "plant-boss-dead-idle", + "plant-boss-dead-bounce", + "plant-boss-intro", + "plant-boss-root-idle", + "aphid-invulnerable", + "aphid-init-by-other", + "*aphid-sg*", + "sync-percent", + "plat-flip-idle", + "*jng-iris-door-sg*", + "misty-part", + "evilbro", + "*plant-boss-vine-sg*", + "evilsis", + "*evilbro-intro-sg*", + "gnawer-wait-to-run", + "frog-idle", + "activate-particle", + "plat-eco-finalboss", + "snow-log-activate", + "sage-finalboss-extra-trans", + "*plat-eco-finalboss-lit-sg*", + "sage-finalboss-particle", + "sage-finalboss-credits", + "*plat-eco-finalboss-unlit-sg*", + "iceskate", + "*robotboss-cinematic-sg*", + "brightness", + "powercellalt-init-by-other", + "fin-door", + "power-right", + "*power-right-sg*", + "*power-left-sg*", + "powercellalt", + "robo-servo9", + "robo-servo6", + "robo-servo5", + "robo-servo3", + "robo-servo2", + "explod-bfg", + "dark-eco-fire", + "dark-eco-buzz", + "*sunkenfisha-yellow-eye-sg*", + "bfg-fizzle", + "robotboss-handler", + "*geologist-sg*", + "robotboss-red-wait", + "*farthy-snack-sg*", + "robotboss-redshot-fill-array", + "robotboss-yellow-dark-bomb", + "robotboss-darkecobomb", + "*sage-bluehut-sg*", + "robotboss-cut-cam-exit", + "robotboss-yellow-dark-bomb-wait", + "ripple-for-villagec-lava", + "robotboss-cut-cam", + "robotboss-shooting-trans", + "*robotboss-yelloweco-sg*", + "*robotboss-blueeco-sg*", + "robotboss-is-yellow-hit", + "robotboss-green-dark-bomb", + "robotboss-yellow", + "robotboss-blue-wait", + "orbit-plat-reset", + "robotboss-greenshot", + "robotboss-bomb-handler", + "robotboss-green", + "robotboss-red-dark-bomb-wait", + "robotboss-anim-blend-loop", + "robotboss-yellow-eco-off", + "junglefish", + "*robotboss-redeco-sg*", + "*dark-plant-sg*", + "robotboss-always-trans", + "redshot-launch-info", + "blob-roar", + "eggtop-idle", + "blob-dies", + "group-flut-attack-strike-ground", + "blob-hit-jak", + "mole-dig", + "*green-eco-lurker-nav-enemy-info*", + "green-eco-lurker-gen-init-by-other", + "*green-eco-lurker-sg*", + "*ecoclaw*", + "set-pivot", + "silodoor", + "darkecobarrel-base", + "*med-res-snow1-sg*", + "finalbosscam-init-by-other", + "ecoclaw-handler", + "ecoclaw", + "ecoclaw-part-info", + "ecoclaw-beam-particle-callback", + "ecoclaw-activate", + "orbit-plat-still", + "target-racing-smack-check", + "bomb-done", + "hit-jak", + "*darkecobomb-sg*", + "redshot-handler", + "arcing-shot-draw", + "redshot-trans", + "darkecobomb-idle", + "arcing-shot", + "greenshot-idle", + "yellowshot-init-by-other", + "yellowshot-idle", + "darkecobomb-handler", + "redshot-particle-callback", + "darkecobomb-explode", + "peeper-hide", + "*greenshot-sg*", + "darkecobomb-explode-if-player-high-enough", + "greenshot", + "torus-verts", + "balloonlurker-play-sounds", + "arcing-shot-debug-trajectory", + "pathb-k", + "darkecobomb-countdown", + "darkecobomb-init-by-other", + "beam-off", + "plant-boss-leaf-open", + "light-eco-child-hit-ground", + "light-eco-mother-appear", + "light-eco-child", + "light-eco-mother", + "light-eco-mother-default-event-handler", + "light-eco-mother-init-by-other", + "flutflut-bluehut", + "light-eco-child-die", + "check-drop-level-lighteco-pops", + "light-eco-child-grabbed", + "check-drop-level-lighteco-big-pops", + "light-eco-child-init-by-other", + "light-eco-mother-active", + "check-drop-level-eichar-lighteco-pops", + "target-has-all-the-cells?", + "robotboss-dda", + "cavespatula", + "cave-water", + "snow-log-hidden", + "shortcut-boulder", + "notify-spawned", + "spider-egg-idle", + "mother-spider-egg", + "mother-spider-proj", + "*mother-spider-leg-infos*", + "*mother-spider-threads*", + "mother-spider-die", + "*mother-spider-leg-sg*", + "check-drop-level-bigdoor-open-pops", + "mother-spider-leg-init-by-other", + "mother-spider-death-event-handler", + "mother-spider-traveling", + "mother-spider-birth-baby", + "mother-spider-stop-traveling", + "mother-spider-default-event-handler", + "mother-spider-spit", + "wait-for-children", + "mom-spid-dies", + "dark-crystal-explode", + "dark-crystal-activate", + "*dark-crystal-explode-sg*", + "yakow-cam", + "dark-crystal", + "trapdoor", + "cavelevator", + "fishermans-boat-docked-misty", + "cavecrystal-light-control-default-callback", + "victory", + "baby-spider", + "baby-spider-spawn-params", + "cavewind", + "baby-spider-resume", + "quicksandlurker-post", + "*baby-spider-sg*", + "*baby-spider-nav-enemy-info*", + "sunken-pipegame-start-up", + "*baby-spider-nav-enemy-info-for-cave-trap*", + "eggs-hatch", + "bab-spid-roar", + "bab-spid-dies", + "*cavecrystal-light-control*", + "warpgate-loop", + "continue-name", + "teleporter-on", + "prec-button2", + "*assistant-lavatube-end-sg*", + "*citb-drop-plat-blue-sg*", + "ogreboss-super-boulder-killed-player", + "*citb-drop-plat-sg*", + "drop-plat", + "drop-plat-rise", + "handle-inline-array", + "drop-plat-drop", + "*citb-drop-plat-red-sg*", + "drop-plat-set-fade", + "lurkerm-short-sail", + "citb-drop-plat-drop-children", + "citb-bunny", + "snow-bunny1", + "snow-bunny-defend", + "snow-bunny-lunge", + "snow-bunny-chase-hop", + "snow-switch", + "snow-bunny-attack", + "training-cam", + "*snow-bunny*", + "*ogreboss-column-sg*", + "snow-bunny-patrol-hop", + "flutflut-plat-appear", + "snow-bunny-retreat-hop", + "snow-bunny-nav-resume", + "snow-bunny-tune-spheres", + "*snow-bunny-nav-enemy-info*", + "snow-bunny-default-event-handler", + "snow-bunny-execute-jump", + "*snow-bunny-sg*", + "spawn-robot", + "disable-bars", + "enable-bars", + "citb-sagecage-update-collision", + "*evilsis-citadel-sg*", + "*yellowsage-sg*", + "green-sagecage", + "*redsage-sg*", + "lurkrat-idle", + "citb-sagecage-draw-bars", + "citb-sage", + "citb-sagecage-idle", + "citb-sage-draw-beam", + "sunkenfisha-init-by-other", + "citb-base-plat-idle", + "height-info", + "*plat-eco-citb-lit-sg*", + "citb-exit-plat-rise", + "billy-game-update-wave", + "citb-chain-plat", + "citb-firehose-active", + "blue-eco-charger-orb-idle", + "*citb-firehose-sg*", + "mudlurker-dies", + "citb-stopbox", + "snow-ball-idle", + "*citb-donut-sg*", + "citb-plat", + "citb-exit-plat", + "citb-firehose-blast-particles", + "quicksandlurker-popup", + "*plat-eco-citb-unlit-sg*", + "citb-exit-plat-move-player", + "0", + "citb-rotatebox", + "*citb-stopbox-sg*", + "caveelevator", + "*plat-citb-sg*", + "citb-firehose", + "citb-exit-plat-idle", + "citb-chain-plat-settle", + "plant-boss-leaf-open-idle", + "citb-base-plat", + "citb-plat-eco", + "shield-on", + "shield-off", + "open-actor", + "*citb-disc-d-sg*", + "*finalbosscam-sg*", + "*citb-arm-b-sg*", + "*citb-generator-broken-sg*", + "*citb-arm-a-sg*", + "redshot-launch-array", + "citb-coil-broken", + "qbert-plat-on-mimic", + "muse-check-dest-point", + "*citb-coil-sg*", + "citb-disc-d", + "citb-generator-idle", + "citb-coil-idle", + "citb-chains", + "citb-disc-b", + "*citb-button-sg*", + "citb-disc", + "citb-arm-a", + "citb-iris-door", + "*citb-disc-a-sg*", + "redshot", + "*citb-arm-shoulder-b-sg*", + "citb-arm-shoulder", + "launcherdoor-open", + "citb-generator-break", + "citb-button", + "*citb-iris-door-sg*", + "*citb-robotboss-sg*", + "citb-generator", + "citb-generator-broken", + "*citb-launcher-sg*", + "plant-boss-leaf-bounce", + "robotboss-manipy-trans-hook", + "citadelcam-idle", + "*citb-hose-sg*", + "citb-arm-section", + "citadelcam", + "*citb-robotboss-leftshoulder-sg*", + "citb-launcher", + "citb-robotboss-idle", + "citb-hose-die", + "kermit-check-to-hit-player?", + "*citb-arm-d-sg*", + "*citb-arm-c-sg*", + "birth-func-random-rot", + "battlecontroller-idle", + "pathspawn", + "pathh", + "max-pickup-count", + "final-pickup", + "pickup-percent", + "pathc", + "spawner-trigger-actor", + "target-flut-air-attack-hit-ground", + "battlecontroller-die", + "lurker-type", + "kill-actor", + "pickup-type", + "percent", + "pathd", + "fade-actor", + "pathg", + "pathb", + "num-lurkers", + "battlecontroller-set-special-contents-collected", + "plant-boss-vine-init", + "battlecontroller-battle-begin", + "battlecontroller-disable-ocean", + "battlecontroller-spawn-creature-at-spawner", + "ogre-bridge-break", + "battlecontroller-off", + "battlecontroller-special-contents-collected?", + "*evilsis-intro-sg*", + "battlecontroller-creature-type", + "battlecontroller-spawners-full?", + "battlecontroller-special-contents?", + "*logo-black-sg*", + "battlecontroller-fill-all-spawners", + "battlecontroller-draw-debug", + "battlecontroller-task-completed?", + "battlecontroller", + "battlecontroller-set-task-completed", + "battlecontroller-spawn-creature", + "alt-task", + "trigger-actor", + "ogreboss", + "warp-gate-init-by-other", + "warp-gate-switch", + "mis-bone-bridge-hit", + "warpgate", + "*village-cam-sg*", + "fisher-game-update", + "get-next-slot-up", + "path6-k", + "village-cam", + "waterfall", + "water-lap", + "*med-res-beach1-sg*", + "tower-winds3", + "tower-winds", + "*pontoonfive-constants*", + "tower-wind3", + "tower-wind2", + "path-max-offset", + "flags-on", + "campoints-flags-on", + "TIME_PE0IOD_LOOP", + "dark-plant-randomize", + "sack-land", + "*jaws-sg*", + "blocking-plane-spawn", + "wait-for-start", + "flutflut-effect", + "beach-part", + "sound-beach-waterfall", + "aybabtu", + "seagullflock-at-waterfall", + "seagullflock-idle", + "seagull", + "seagullflock", + "bully-start-spinning", + "beach-rock-trigger", + "ice-cube-appear-land", + "seagull-post", + "seagull-soaring", + "*seaweed-sg*", + "sound-seagull-squall", + "seagull-landing", + "fallen", + "go-spike-up", + "beach-rock", + "run-step-left", + "puppy-bark", + "lurkerdog-idle", + "lurkerdog-dies", + "lurkerdog-bite", + "ogreboss-super-boulder-event-handler", + "*lurkerpuppy-nav-enemy-info*", + "snap", + "shell-up", + "lurkercrab-dies", + "*ogre-isle-b-sg*", + "crab-slide", + "EFEFCT(8", + "lurkercrab-pushed", + "spike-up", + "lurkercrab-invulnerable", + "lurkercrab", + "lurkercrab-vulnerable", + "*lurkercrab-sg*", + "worm-sink", + "worm-dies", + "worm-bite", + "plant-boss-arm", + "flutflut", + "lurkerworm-prebind-function", + "lurkerworm-joint-callback", + "snow-ram-proj-update-velocity", + "exit-chamber-dummy-wait-to-appear", + "*lurkerworm-sg*", + "sleep", + "lurkerworm-default-post-behavior", + "swamp-rock-break", + "lurkerworm-die", + "lurkerworm-idle", + "lurkerworm-sink", + "lurkerworm", + "swamp-rat-nest-dummy-event-handler", + "walk-straw2", + "walk-straw1", + "pelican-gulp", + "pelican-flap", + "zero-func", + "pelican-from-nest", + "pelican-bank", + "pelican-wait-at-end", + "sequenceB", + "pelican-to-nest", + "reflector-mirror-broken", + "pelican-circle", + "pelican-wait-at-nest", + "pelican-fly", + "*sculptor-sg*", + "*sculptor-muse-sg*", + "muse-to-idle", + "mayor-step-wood", + "mayor-lurkerm-reward-speech", + "lurkrat-notice", + "mayor", + "*bird-lady-beach-sg*", + "yakow-post", + "arcing-shot-setup", + "bird-lady-beach", + "*bird-lady-sg*", + "floating-launcher-lowering", + "num-positions", + "submerge1", + "harvester", + "fishgame", + "flutflutegg", + "*flut-run-attack-mods*", + "flutflutegg-physics-fall", + "flutflutegg-hit-sounds", + "beachcam", + "*harvester-sg*", + "crab-walk2)", + "flutflutegg-break", + "harvester-inflate", + "bladeassm-prebind-function", + "*beachcam-sg*", + "grottopole-moving-down", + "*ecoventrock-sg*", + "pick-patrol-point-away-from-buddy-work", + "grottopole", + "*kickrock-sg*", + "windmill-one-idle", + "flying-lurker-calc-speed", + "flying-rock-init-by-other", + "harvester-idle", + "flying-rock-idle", + "bladeassm", + "bird-3", + "grottopole-moving-up", + "move-grottopole-to-position", + "gnawer-taunt", + "grottopole-idle", + "flutflutegg-physics", + "*flutflutegg-sg*", + "*windmill-one-sg*", + "*flutflut-naked-sg*", + "twister", + "twist-joint", + "*citb-exit-plat-sg*", + "wobbler", + "point-in-air?", + "snow-log-button-event-handler", + "points-in-air?", + "add-debug-air-box", + "spawn", + "point-in-air-box-area?", + "point-in-air-box?", + "babak-with-cannon-compute-ride-point", + "babak-with-cannon", + "babak-run-to-cannon", + "yeti", + "tiltmax", + "tiltspeed", + "tiltmin", + "rotmin", + "center-radius", + "rotspeed", + "rotmax", + "ram-boss", + "smack", + "mistycannon-init-data", + "mistycannon-prebind-function", + "mother-spider-proj-update-velocity", + "floating-rings", + "mistycannon-do-aim", + "angle-tracker-set-value", + "mistycannon-postbind-function", + "mistycannon-player-control", + "target-tube", + "mistycannon-aim-at-player", + "campoints-flags-off", + "solve-missile-tilt", + "angle-tracker-init-range!", + "*mistycannon-missile-sg*", + "mistycannon-missile-init-by-other", + "quadratic-solution", + "mistycannon-idle", + "ogreboss-village2-trans", + "mistycannon-missile-idle", + "mistycannon-waiting-for-player-to-fuck-off", + "mistycannon-pick-random-target-point", + "angle-tracker-seek!", + "mistycannon-find-best-solution", + "angle-tracker-get-value", + "trajectory-params", + "ticky", + "ropebridge-spring-point", + "*ropebridge-36-sg*", + "*ropebridge-36-rest-state*", + "*snow-bridge-36-sg*", + "citb-part", + "*ropebridge-70-rest-state*", + "*ropebridge-52-rest-state*", + "ropebridge-joint-callback", + "gnawer-segment-info", + "*ropebridge-52-sg*", + "*ropebridge-32-sg*", + "*ropebridge-tunings*", + "frogspeak", + "*ropebridge-32-rest-state*", + "target-tube-turn-anim", + "ropebridge", + "ropebridge-tuning", + "notice-dist", + "*plat-eco-unlit-sg*", + "*plat-eco-lit-sg*", + "plat-eco", + "prec-button3", + "plant-boss-root-init", + "plat-button-teleport-to-other-end", + "plat-button-at-end", + "plat-button-move-upward", + "plat-button-move-downward", + "plat-button-idle", + "camera-name", + "plat-button-camera-on", + "plat-button-camera-off", + "*plat-button-sg*", + "plat-startup", + "plat-path-active", + "plat-idle", + "*plat-sunken-sg*", + "*plat-jungleb-sg*", + "plat", + "puffer-default-event-handler", + "orb-cache-count", + "plat-type", + "*orb-cache-top-sg*", + "sharkey-follow-trajectory", + "*catch-fishc-sg*", + "sharkey-move-to-attack-position", + "get-nav-point!", + "sharkey-notice-player?", + "sharkey-reset-position", + "seagull-takeoff", + "babk-taunt", + "babak-giveup", + "logo-slave", + "babak-breathout", + "babak-breathin", + "*babak-nav-enemy-info*", + "joint-exploder-init-by-other", + "joint-exploder", + "joint-exploder-tuning", + "*green-sagecage-sg*", + "joint-exploder-joint", + "robber-find-ground", + "robotboss-white-eco-movie", + "tippy", + "basebutton-up-idle", + "use", + "basebutton-startup", + "extra-id", + "reflector-mirror-idle", + "basebutton-going-down", + "basebutton-going-up", + "basebutton", + "bird-1", + "*warp-jump-mods*", + "*warp-info*", + "12", + "green-eco-lurker-tune-spheres", + "warp-gate", + "*generic-button-sg*", + "door-opening", + "door-closed", + "scale", + "eco-door", + "plat-code", + "plat-trans", + "nav-enemy-attack", + "nav-enemy-flee", + "nav-enemy-idle", + "cue-jump-to-point", + "nav-enemy-jump-land", + "nav-enemy-jump", + "darkecobarrel-base-done?", + "go-wait-for-cue", + "nav-enemy-die", + "nav-enemy-jump-blocked", + "nav-enemy-patrol", + "village2cam", + "cue-patrol", + "shadow-execute-all", + "mother-spider-full-joint-callback", + "shadow-calc-dual-verts", + "shadow-init-vars", + "parent", + "generic-tie-convert", + "*water-anim-village1-fountain-sg*", + "ash", + "snake-bite", + "lb-copy", + "default-buffer-init", + "mercneric-matrix-asm", + "*balloon-sg*", + "invinitdata", + "check-drop-level-village1-fountain-nosplash", + "mercneric-vu0-block", + "generic-merc-execute-all", + "mercneric-shader-asm", + "snowball-info", + "*swamp-mood-lights-table*", + "*display-camera-marks*", + "generic-upload-vu0", + "untrigger", + "generic-interp-dproc", + "breath-out", + "generic-copy-vtx-dclr-dtex", + "upload-vu0-program", + "ram-boss-already-down", + "touching-list", + "bully-spin1", + "generic-initialize", + "lurkrat-dies", + "depth-cue-data", + "baby-spider-hatching", + "gs-smode2", + "teeter_rockup", + "generic-no-light-proc", + "error", + "generic-prepare-dma-single", + "lav", + "gs-texclut", + "generic-post-debug", + "periscope-set-target-direction", + "*generic-consts*", + "snowball-bank", + "*training-mood*", + "generic-setup-constants", + "generic-reset-buffers", + "generic-init-buffers", + "energydoor-opening", + "fisher-playing", + "paused?", + "texscroll-globals", + "draw-bones-hud", + "pontoon-hidden", + "id", + "bones-vu0-block", + "*default-shadow-settings*", + "generic-merc-init-asm", + "group-ogreboss-lava-splash", + "vector4-lerp!", + "__assert-min-max-range-int", + "open", + "ripple-for-maincave-dark-eco-pool", + "*use-generic*", + "precursor-arm-slip", + "target-racing-falling", + "bone-calculation-list", + "ripple-add-debug-sphere", + "ripple-matrix-scale", + "generic-merc-dcache", + "target-final-door", + "dark-plant-sprout", + "ripple-execute-init", + "cam-calc-follow!", + "get-eye-block", + "merc-stats-display", + "wake", + "scf-get-language", + "merc-vu1-add-vu-function", + "merc-edge-stats", + "shrubbery-login-post-texture", + "blerc-init", + "blerc-stats-init", + "merc-blend-shape", + "*sp-particle-system-3d*", + "stuck", + "mistycannon-missile-explode", + "*travel-timer*", + "merc-vu1-block", + "debug-set-camera-pos-rot!", + "periscope-find-aim-at-angles", + "make-rolling-light-kit", + "fade-depth", + "plat-flip", + "menu", + "get-debug-line", + "*eye-work*", + "dma-timeout-cam", + "sp-field-init-spec", + "bones-mtx-calc", + "birth-func-copy-omega-to-z", + "add-debug-triangle-normal", + "drawable-frag-count", + "*village1cam-sg*", + "spawn-mistycannon-missile", + "orb-cache-top-idle", + "add-debug-lights", + "add-debug-sphere-with-transform", + "*debug-text-3ds*", + "debug-pad-display", + "add-debug-point", + "debug-percent-bar", + "*yellowline-table*", + "gsf-ik", + "quicksandlurker-missile-init-by-other", + "dma-send-no-scratch", + "*debug-lines*", + "add-debug-text-3d", + "firecanyon-vis", + "ogreboss-missile-init-data", + "joint-mod-set-local-callback", + "sound-command->string", + "uint32", + "debug-sphere-table", + "clmf-cam-float", + "poi", + "*shadow-middot-texture*", + "cut", + "*sunken-mood*", + "target-falling", + "sprite-draw-distorters", + "keg-paddle-to-path", + "*sprite-array-2d*", + "ocean-texture-setup-constants", + "sprite-add-shadow-chunk", + "sprite-add-3d-chunk", + "sprite-add-matrix-data", + "sprite-setup-header", + "sprite-allocate-user-hvdf", + "matrix-from-joint-anim-frame", + "generic-merc-input", + "*thunder-id1*", + "sprite-hvdf-control", + "clear-sprite-aux-list", + "sprite-add-frame-data", + "*tfrag-work*", + "num-func-none", + "*gomi-stats-hack*", + "GSH_WHICH_STAT", + "finalbosscam", + "*stat-string-tfrag-near*", + "print-cl-stat", + "texture-relocate-later", + "clear-cl-stat", + "*test-shrub*", + "snow-fort-gate-idle-closed", + "plant-boss-generic-event-handler", + "stack1", + "map-bsp-tree", + "flatten-joint-control-to-spr", + "door-closing", + "matrix-inv-scale!", + "caveelevator-one-way-idle-start", + "generic-none", + "joint-anim-inspect-elt", + "flying-lurker-fly", + "lurkbat-idle", + "matrix-from-control-channel!", + "quaternion-rotate-y!", + "air-box", + "prototype-bucket-type", + "target-flut-air-attack", + "generic-none-dma-wait", + "draw-bones-shadow", + "*scarecrow-a-sg*", + "*stats-blerc*", + "yakow-bank", + "*water-anim-misty-mud-lonely-island-sg*", + "joint-control-remap!", + "cspace<-transformq+trans!", + "cspace<-rot-yxy!", + "joint-control-copy!", + "cave-spatula", + "ripple-update-waveform-offs", + "gs-miptbp", + "cylinder-flat-verts", + "ray-triangle-intersect", + "gs-xy-offset", + "target-pos", + "*merc-bucket-info*", + "mud", + "state-actor", + "nav-control-cfs-work", + "sfx-volume", + "nassoce", + "matrix-4x4-inverse!", + "racer-integrate", + "sky-roof-polygons", + "math-camera-pos", + "sound-reload-info", + "*teetertotter-sg*", + "dead", + "sound-set-flava", + "sound-bank-load", + "ram-boss-nav-start", + "credits", + "ogre", + "particle-adgif-cache", + "update-mood-village2", + "sound-angle-convert", + "touch-tracker-idle", + "citadel", + "*sound-player-enable*", + "vector4w-4", + "*part-tester*", + "sprite-init-distorter", + "sound-continue", + "collide-shape-prim", + "missed-jak", + "target-periscope", + "matrix<-transformq+trans!", + "sound-music-load", + "ripple-for-villageb-water", + "busy", + "target-clone-anim", + "show-iop-info", + "sound-set-falloff-curve", + "robber-initial", + "current-str-pos", + "kermit-set-rotate-dir-to-player", + "debug-menu-context-close-submenu", + "flava-table", + "column-scale-matrix!", + "beach", + "gorge", + "mid", + "empty1", + "*nav-triangle-test-count*", + "long-jump", + "jacc-mem-usage", + "moving-sphere-moving-sphere-intersect", + "*font24-table*", + "sound-set-ear-trans", + "*darkvine-sg*", + "sound-group-stop", + "collide-cache", + "spider-step", + "pov-camera-playing", + "misty", + "4", + "set-dist", + "*FLUT-bank*", + "*debug-vertex-stats*", + "game-info", + "complete", + "str-play-stop", + "matrixp*!", + "circle-triangle-intersection-proc?", + "dgo-load-cancel", + "str-load-cancel", + "str-load-status", + "cam-layout-save-fov", + "*load-str-lock*", + "str-ambient-play", + "sage-finalboss-credit-particle", + "*dgo-name*", + "swamp-rope-post", + "citb-arm-b", + "process-taskable-play-anim-trans", + "*crate-barrel-sg*", + "xyzwh", + "next-level", + "glst-next", + "nav-engine", + "nav-mesh-actor", + "mother-spider-hit-while-birthing", + "nav-lookup-elem", + "hud-particle", + "slave-option?", + "nav-vertex", + "bonelurk-roar", + "vector-orient-by-quat!", + "nav-node", + "vector-negate!", + "pad-1", + "path", + "windmill-one", + "gs-trxpos", + "spawner-blocker-actor", + "add-to-sprite-aux-list", + "at-pick-object", + "jak-deatha", + "alive", + "generic-tie-decompress", + "clone", + "task-info-data", + "*display-pool*", + "*progress-process*", + "light-volume", + "jng-iris-door", + "kernel-check-hardwired-addresses", + "game-option", + "generic-sink", + "*ACTOR-bank*", + "load-boundary", + "sound-group-continue", + "*camera-other-fov*", + "joint-mod-set-world", + "*camera-read-buttons*", + "deinstall-debug-handlers", + "pathf", + "sphere", + "billy-kill-all-but-farthy", + "get-debug-text-3d", + "birth-func-copy-rot-color", + "*camera-init-mat*", + "sprite-get-3d-quaternion!", + "swamp-bat-idle-path", + "battlecontroller-spawn-creature-random-spawner", + "*redline-table*", + "quaternion-pseudo-slerp!", + "pov-camera-init-by-other", + "float-save-timeplot", + "*yellowline-index*", + "*cam-layout*", + "drop-plat-idle", + "float-save-blueline", + "float-lookup-blueline", + "ocean-texture-add-call-start", + "ogreboss-submerge", + "green-eco-lurker-appear", + "float-save-greenline", + "*blueline-table*", + "rotate-y<-vector+vector", + "arena", + "float-lookup-redline", + "add-debug-vector", + "float-lookup-yellowline", + "*orbit-plat-bottom-sg*", + "*timeplot-index*", + "camera-bank", + "sound-rpc-set-param", + "camera-combiner", + "clone-anim", + "hud-money-all", + "actor-pause", + "prev-actor", + "target-racing-jump", + "clip-polygon-against-negative-hyperplane", + "trans-offset", + "entity-actor-lookup", + "plant-boss-attack", + "wind-vector", + "return-from-thread", + "cspace-by-name-no-fail", + "*instance-tie-work*", + "sparticle-launch-group", + "target-edge-grab", + "target-swim-stance", + "str-play-async", + "str-ambient-stop", + "eye-work", + "*greenline-table*", + "sound-rpc-union", + "debug-menus-active", + "swamp-rat-nest-dummy", + "vector-flatten!", + "sound-bank-unload", + "*display-actor-graph*", + "*gnawer-segment-infos*", + "tie-vu1-block", + "qbert-plat-on-init-by-other", + "eye-control-array", + "*play-str-rpc*", + "add-debug-light", + "rand-vu-sphere-point!", + "*fake-shadow-buffer*", + "fake-shadow-buffer", + "sprite-vec-data-2d", + "task-hint-control", + "*sidekick-sg*", + "entity-ambient-data", + "periscope-test-task-complete?", + "aphid-step", + "entity-ambient-data-array", + "*fireboulder-sg*", + "assistant-firecanyon", + "sharkey", + "*blerc-globals*", + "entity-info", + "swingpole-stance", + "entity-nav-login", + "group-slide-poof-gravel", + "*generic-foreground-sinks*", + "number", + "tracking-spline", + "group-land-poof-dirt", + "steam-cap-control-pt", + "eye-control", + "*generate-actor-vis-start*", + "gs-packed-rgba", + "terrain-stats", + "sun-iris-door-init-by-other", + "vector-xz-normalize!", + "background-area", + "dma-area", + "*collide-stats*", + "ogre-bridgeend-idle", + "*display-cam-los-info*", + "gs-packed-stq", + "work-area", + "ripple-for-training-water", + "eggtop", + "music-volume", + "*greenline-index*", + "shadow-dcache", + "*sunken-mood-fog-table*", + "darkecobomb-land", + "add-debug-curve2", + "tie-debug-between", + "target-falling-trans", + "cspace<-cspace!", + "*ram-boss-nav-enemy-info*", + "background-work", + "fountain", + "tfrag-dists", + "100", + "swamp-spike-post", + "fuel-cell", + "tfrag-packet", + "*target-lock*", + "plant-boss-arm-die", + "*stats-poly*", + "tfrag-stats", + "draw-ocean-texture", + "target-flut-hit-ground", + "tfragment-stats", + "reverse-conversions", + "reset-target-tracking", + "ramdisk-rpc-load", + "tfragment", + "village1", + "light-index", + "*camera-other-root*", + "tracking-spline-sampler", + "ogreboss-super-boulder-init-by-other", + "*oracle-village1-tasks*", + "tie-matrix", + "vector-normalize-copy!", + "drawable-tree-instance-tie", + "*double-lurker-top-sg*", + "campoints-flags", + "prototype-trans-shrubbery", + "fisher-fish-caught", + "*progress-save-info*", + "village2", + "instance-shrub-work", + "gnawer-falling-segment", + "billboard", + "shrub-near-packet", + "sp-launch-queue", + "find-closest-circle-ray-intersection", + "third-power", + "*collide-test-flag*", + "smack-surface?", + "*tube-surface*", + "collide-puyp-work", + "*file-temp-string*", + "change-anim", + "*bones-first*", + "shadow-vu1-constants", + "vent-blocked", + "*max-dma*", + "*collide-work*", + "*merc-ctrl-header*", + "*display-load-boundaries*", + "projectile", + "generic-light", + "projectile-blue", + "mis-bone-bridge-bump", + "collide-frag-vertex", + "citb-hose", + "*high-jump-mods*", + "group-slide-poof-ice", + "robotboss-green-wait", + "collide-frag-mesh", + "effect-name", + "joint-mod-joint-set*-handler", + "joint-control-reset!", + "num-func-loop!", + "towertop", + "anim-test-obj-remove-invalid", + "jungleb", + "num-func--!", + "add-debug-sphere", + "process-drawable-idle", + "*water-anim-misty-mud-near-skull-sg*", + "crystal-light", + "nav-enemy-test-point-in-nav-mesh?", + "cspace-index-by-name-no-fail", + "bones-set-sqwc", + "edge-grab-info", + "*edge-grab-info*", + "collide-stats", + "tie-float-reg", + "bg-b", + "collide-edge-tri", + "alt-actor-list-subtask-incomplete-count", + "*fishermans-boat-sg*", + "drawable-tree-tfrag", + "*color-yellow*", + "generic-tie-execute", + "*dma-mem-usage*", + "*touching-list*", + "load-chunk-msg", + "install-handler", + "touching-prims-entry", + "blerc-dcache", + "camera", + "progress", + "dma-sync-crash", + "*sp-launcher-enable*", + "ray-arbitrary-circle-intersect", + "maincave", + "citb-hose-event-handler", + "danger", + "sound-rpc-continue-sound", + "*collide-hit-by-others-list*", + "pke-nav-hack", + "collide-prim-core", + "get-string-length", + "*collide-shape-prim-backgnd*", + "collide-sticky-rider", + "ocean-matrix*!", + "clear-frame-accumulator", + "*med-res-beach-sg*", + "file-info", + "printl", + "collide-sticky-rider-group", + "ocean-generate-verts", + "prototype-tie-dma", + "collide-shape-prim-sphere", + "*collide-usually-hit-by-player-list*", + "square-platform-lowered", + "collide-shape-moving", + "collide-tri-result", + "collide-mesh-tri", + "v-slrp!", + "ogreboss-spawn-super-boulder", + "cspace<-parented-transformq-joint!", + "nav-max-users", + "hidden", + "init-game-options", + "*target*", + "attacking", + "*breakaway-mid-sg*", + "file-stream-read-string", + "collide-puss-sphere", + "add-debug-matrix", + "*touching-prims-entry-pool*", + "joint-mod-blend-local", + "joint-mod-debug-draw", + "bone-memory", + "joint-mod-blend-local-callback", + "health", + "swamp-bat-slave-launch", + "link-begin", + "dma-bank-spr", + "sp-relaunch-setup-fields", + "cam-layout-save-pivot", + "debug-menu-item-flag", + "joint-mod-wheel", + "oscillating-float", + "sync-info-eased", + "*load-dgo-rpc*", + "vector-cos-rad!", + "oscillating-vector", + "add-tfrag-mtx-1", + "gs-block-width", + "check-drop-level-maincave-drip", + "*explorer-sg*", + "*racer-mods*", + "*silodoor-sg*", + "sync-info", + "touch-tracker", + "sound-rpc-unload-music", + "*rolling-start-whole-sg*", + "create-interpolated-joint-animation-frame", + "vector-seek!", + "projectile-impact", + "activate-orb-all", + "buzzer-init-by-other", + "add-debug-circle", + "energyhub-stop", + "path-trans-offset", + "tr-stat", + "camera-tracker", + "*camera-old-cpu*", + "entity-perm-array", + "*swamp-spike-sg*", + "unpack-comp-rle", + "process-hidden", + "othercam", + "process-drawable-reserved", + "sphere-in-view-frustum?", + "target-attacked", + "process-drawable", + "*external-cam-options*", + "draw-bones-check-longest-edge-asm", + "instance-tie", + "cam-horz", + "inspect-process-heap", + "set-fov", + "cam-notice-dist", + "keg-event-handler", + "*clm-index-attr*", + "palette-fade-controls", + "target-wheel-flip", + "kernel-dispatcher", + "target-powerup-effect", + "sulphur", + "fact-info", + "ocean-map", + "eco-door-event-handler", + "*sprite-distorter-sine-tables*", + "darkecobarrel-mover-pos", + "generic-input-buffer", + "string=", + "sky-make-light", + "radmod", + "*pat-mode-info*", + "wall-plat-retracting", + "debug-menu-context-default-selection", + "tfrag-dma", + "bone-calculation", + "joint-mod-joint-set-handler", + "free-last-sound-buffer-entry", + "snow-eggtop-activate", + "pat-event->string", + "run", + "nav-route-portal", + "snow-ram-proj", + "no-look-around", + "*ogre-isle-constants*", + "plant-boss-root-die", + "matrix-rotate-yx!", + "wade", + "square-platform-raised", + "*camera-look-through-other*", + "jump-double", + "uppercut", + "target-compute-slopes", + "mom-spid-grunt", + "clip-vector-to-halfspace!", + "debug-tracking-thang", + "dive", + "gnawer-retreat-into-post", + "add-debug-box", + "air", + "generic-no-light", + "build-matrix-from-up-and-forward-axes!", + "cam-los-spline-collide", + "vector2uh", + "empty", + "*merc-global-stats*", + "nav-enemy-turn-to-face-point", + "*wheel-flip-mods*", + "light-volume-sphere", + "merc-global-stats", + "baby-spider-die-fast", + "dm-enable-instance-func", + "*wade-mods*", + "nav-enemy", + "splash-spawn", + "*neutral-mods*", + "*swim-mods*", + "*wheel-mods*", + "load-dgo-msg", + "starfish-idle", + "*frame-stats*", + "bone-buffer", + "plant-recover", + "generic-add-constants", + "*duck-attack-mods*", + "logtrap2", + "aphid-vulnerable", + "check-vector-collision-with-nav-spheres-info", + "read", + "*duck-mods*", + "mother-spider-hit-while-tracking", + "calc-terminal-vel", + "deg-", + "part-progress-hud-buzzer-func", + "*walk-no-turn-mods*", + "*light-eco-big-sg*", + "*run-attack-mods*", + "drawable-inline-array-trans-tfrag", + "get-sound-buffer-entry", + "zoomer-heat-slice-color", + "*grab-mods*", + "*part-tester-name*", + "periscope-post", + "sprite-add-shadow-all", + "dma-add-process-drawable-hud", + "surface-mult!", + "*tread-surface*", + "print-perf-stats", + "process-drawable-child-count", + "print-tree-bitmask", + "dma-send", + "path1", + "voicebox-spawn", + "target-flop", + "try-corner", + "battlecontroller-update-spawners", + "*jump-attack-mods*", + "part-water-splash-callback", + "meters", + "group-run-poof-metal", + "group-slide-poof-grass", + "yakow-common-post", + "*turn-around-mods*", + "camera-free-floating-move-info", + "reflector-end", + "revcycleprop", + "draw-string", + "curve-evaluate!", + "*wade-surface*", + "line-in-view-frustum?", + "calc-terminal2-vel", + "running", + "*balloonlurker-pilot-sg*", + "maxAngle-offset", + "*dive-mods*", + "*water-anim-village3-lava-sg*", + "revcycle", + "surface-clamp-speed", + "#t)", + "*instance-shrub-menu*", + "*uppercut-jump-mods*", + "balloonlurker-player-impulse", + "drawable-tree-collide-fragment", + "cmds", + "*edge-surface*", + "citb-disc-a", + "trs", + "fact-info-enemy", + "loading-done", + "str-play-queue", + "time-to-apex", + "light-group-slerp", + "gorge-behind", + "gs-display", + "shadow-vu1-gifbuf-template", + "collide-edge-edge", + "*stats-collide*", + "fire-loop", + "vu-lights-default!", + "base", + "tracking-point", + "plant-boss-post", + "warpgate-tele", + "*ripple-globals*", + "sky-work", + "drawable-inline-array-tfrag", + "process-type", + "cam-layout-stop", + "flying-lurker-start", + "tfrag-control", + "merc-dma-chain-to-spr", + "exact", + "yellowshot", + "target-high-jump", + "energydoor-opened", + "walk-step-right", + "default", + "*evilsis-sg*", + "ripple-merc-query", + "uinteger", + "robotboss-position", + "iterate-process-tree", + "wheel", + "anim-tester-save-object-seqs", + "darkecobarrel-base-time", + "collide-mesh", + "pat-material->string", + "nav-mesh", + "part-group-pointer?", + "*ogreboss-sg*", + "*res-key-string*", + "array", + "*ogre-step-b-sg*", + "*setting-control*", + "vif-disasm-element", + "last-try-to-look-at-data", + "*background-draw-engine*", + "entity-links", + "camera-eng", + "float-type", + "merc-global-array", + "vector-float*!", + "sound-id", + "close-specific-task!", + "bone-regs", + "bone-debug", + "volume", + "target-exit", + "prototype-tie-work", + "adjust-pos", + "bone-work", + "*terrain-context*", + "collide-hit-by-others-list", + "teetertotter", + "debug-menu-make-task-need-introduction-menu", + "joint-control-channel", + "channel-upload-info", + "lightning-mole-debug-blend", + "debug-vertex-stats", + "prototype-array-shrub-info", + "spooling", + "driller-lurker-die", + "prototype-array-tie", + "generic-envmap-proc", + "water-vol", + "*water-anim-sunken-hall-with-one-whirlpool-sg*", + "dma-buffer-add-vu-function", + "prototype-bucket-shrub", + "spline-offset", + "*main-options-demo*", + "actor-link-subtask-complete-hook", + "gs-alpha", + "*GAME-bank*", + "binteger", + "shadow-dma-end", + "money-init-by-other", + "gs-packed-gt", + "actor-id", + "sin-rad", + "sound-play-by-spec", + "sound-music-unload", + "*firecanyon-mood-sun-table*", + "*exit-chamber-dummy-sg*", + "game-bank", + "mc-sync", + "elevator-idle-at-fort", + "ogre-bridge-activated", + "mother-spider-tracking", + "anim-tester-real-post", + "fuel-cell-init-as-spline-slider", + "boulder1-trans", + "citb-drop-plat-drop-all-children", + "mc-file-info", + "curve-get-pos!", + "shadow-tri", + "powerup", + "forward-down-nopitch->inv-matrix", + "shadow-matrix-ref", + "*collide-vif0-init*", + "shadow-queue-reset", + "pelican-post", + "*shadow*", + "rpc-buffer-pair", + "shadow-queue", + "*generic-debug*", + "group-land-poof-crwood", + "swamp-rope-break-code", + "target-racing-get-off-jump", + "sound-buffer-dump", + "generic-effect-buffer", + "anim-tester-add-newobj", + "generic-work", + "atan-series-rad", + "robot-arm", + "generic-texbuf", + "*periscope-mirror-sg*", + "ropebridge-idle", + "force-blend", + "sound-stop", + "dm-task-reminder", + "target-joint-pos", + "generic-tie-instance", + "*x-vector*", + "generic-tie-shadow", + "generic-tie-control", + "try-to-look-at-info", + "fractional-part", + "texscroll-make-request", + "rotate-y-angle", + "drown", + "ground-tween-initialize", + "group-land-poof-metal", + "generic-merc-output", + "duck-spin", + "generic-merc-tag", + "debug-menu-item-var", + "citb-coil-break", + "generic-merc-ctrl", + "rot", + "nav-enemy-stare", + "generic-tie-input", + "ripple-control", + "robotboss-yellow-wait", + "merc-ctrl", + "debug-menu-item-function", + "flutflutegg-idle", + "merc-fragment-fp-data", + "actor-link-info", + "cam-eye", + "prototype-inline-array-shrub", + "*res-static-buf*", + "*hopper-sg*", + "menu-respond-to-pause", + "merc-blend-ctrl", + "sky-frame-data", + "bladeassm-idle", + "can-hands?", + "debug-reset-buffers", + "part-tracker", + "group-scarecrow-explode", + "task-cstage", + "race-ring-active", + "lb-add-load", + "get-next-slot-down", + "merc-byte-header", + "target-rot", + "merc-extra-info", + "set!", + "caveelevator-one-way-travel-to-start", + "*forward-high-jump-mods*", + "gif-tag-prim", + "*null-process*", + "villageb-part", + "sound-volume-off", + "query-state", + "ocean-interp-wave", + "battlecontroller-camera-off", + "shadow-edge", + "merc-blend-data", + "pris-mtx", + "chainmine", + "generic-constants", + "glst-list", + "entity-by-name", + "catch-frame", + "*math-camera*", + "*driller-lurker-shadow-control*", + "bonelurk-raor", + "wind-work", + "entity-camera", + "raw-ray-sphere-intersect", + "*jak-white-sg*", + "joint-anim-drawable", + "mai", + "dma-packet-array", + "joint-anim-compressed", + "vector-length-max!", + "drawable-inline-array-instance-tie", + "*citb-bluesage-tasks*", + "lb-add-vtx-before", + "check-water-level-drop", + "rotoffset", + "art-mesh-anim", + "joint-exploder-list", + "sprite-frame-data", + "time-of-day-proc", + "dgo-entry", + "*citb-robotboss-head-sg*", + "touching-shapes-entry", + "group-just-poof-grass", + "*smack-jump-mods*", + "sound-rpc-continue-group", + "mercneric-convert", + "draw-ocean-near", + "*flava-table*", + "string-push-help", + "palette-fade-control", + "plant-boss-leaf", + "time-of-day-context", + "update-mood-village3", + "joint-anim-login", + "*timer-value*", + "merc-eye-anim-block", + "enter-playing", + "mood-context", + "blackout", + "jungle", + "mood-fog-table", + "plane-volume-intersect-dist", + "options", + "babak-charge", + "draw-control", + "*target-pool*", + "shadow-xform-verts", + "mood-fog", + "drawable-tree-lowres-tfrag", + "*water-anim-sunken-room-above-exit-chamber-sg*", + "vu-lights", + "blue-eco-charger-open", + "rand-vu-percent?", + "*display-path-marks*", + "cl-stat", + "*sky-upload-data*", + "*sky-parms*", + "generic-tie-debug", + "float-lookup-greenline", + "sky-upload-data", + "sculptor", + "*text-group-names*", + "add-tfrag-mtx-0", + "swamp-vis", + "sky-orbit", + "*txt-dma-list*", + "*citb-chain-plat-sg*", + "*maincave-mood-sun-table*", + "*cloud-drawn*", + "*rolling-spheres-light0*", + "sky-sun-data", + "group-land-poof-wood", + "lavafallsewera", + "nav-enemy-stop-chase", + "*generate-actor-vis*", + "sky-circle-data", + "*gsf-buffer*", + "debug-menu-make-task-need-reminder-a-menu", + "nav-enemy-fuel-cell", + "sky-vertex", + "precursor-arm", + "*ocean-map-village1*", + "cam-start", + "history-init", + "transformq-copy!", + "*ocean-mid-masks-village1*", + "*beach-mood*", + "merc-matrix", + "*warp-gate-switch-sg*", + "*ocean-mid-masks-village2*", + "crate-bank", + "collide-shape", + "notice-top", + "point-of-interest", + "joint-anim-compressed-control", + "*ocean-map-village2*", + "fin", + "*walk-mods*", + "*ocean-trans-corner-table*", + "sound-name=", + "*ocean-up-right-table*", + "reset-actors", + "ambient-type-sound", + "*ocean-left-table*", + "nav-mesh-connect", + "align-control", + "unload-package", + "*ocean-down-right-table*", + "*fake-shadow-buffer-1*", + "snow-spatula", + "*ocean-trans-right-table*", + "*ocean-trans-st-table*", + "*shortcut-boulder-whole-sg*", + "cylinder-verts", + "vector-length-squared", + "*ocean-trans-up-left-table*", + "flag", + "fishermans-boat-set-path-point", + "sparticle-cpuinfo", + "group-run-poof-tar", + "junglesnake-twist-joint", + "*ocean-trans-down-left-table*", + "target-look-around", + "*nav-update-route-table-ray-count*", + "merc-fragment-control", + "*debug-engine*", + "ocean-near-vertex", + "connection-process-apply", + "*rolling-spheres-light1*", + "group-just-poof-swamp", + "pos-history", + "dist-from-interp-src", + "muse-taunt-1", + "debug-menu-item-var-make-float", + "ocean-mid-constants", + "target-stance-ambient", + "ocean-trans-mask", + "gondolacables", + "collide-offset", + "run-function-in-process", + "ocean-wave-frames", + "mem-usage-bsp-tree", + "ocean-near-indices", + "ocean-wave-data", + "citb-battlecontroller", + "triangulate-boundary", + "update-wind", + "subdivide-settings", + "dax-face-smak", + "*ocean-up-left-table*", + "ocean-near-work", + "sparticle-launch-control", + "*ropebridge-70-sg*", + "*ocean-off*", + "debug-menu-nodestring", + "lightning-mole-run-code", + "*", + "ocean-mid-upload2", + "sound-play-by-name", + "target-hit-setup-anim", + "load-state-want-levels", + "next-actor", + "mood-sun", + "*ocean-near-off*", + "generic-tie-matrix", + "ocean-texture-constants", + "update-mood-snow", + "quaterion<-rotate-y-vector", + "cam-layout-save-focalpull", + "ocean-trans-index", + "junglesnake-tilt-joint", + "*ocean-mid-off*", + "matrix-3x3-inverse!", + "*lava-time*", + "matrix-translate+!", + "debug-text-3d", + "bg", + "pov-camera", + "ear-trans", + "max", + "tube-thrust", + "process-drawable-pair-random-point!", + "*ocean-facing*", + "red-sagecage", + "pov", + "vi2", + "count-info", + "ocean-work", + "work", + "anim-tester-post", + "print-collide-stats", + "plant-boss-hit", + "generic-merc-ctrl-with-sfx", + "generic-no-light-dproc-only", + "dump-vu1-mem", + "light-volume-array", + "tfrag-init-buffer", + "hud-health", + "bones-debug", + "generic-consts", + "look-for-points-of-interest", + "sincos-rad!", + "*pontoonfive-sg*", + "focalpull-flags", + "gsf-header", + "joint-anim-matrix", + "move-grottopole", + "ad-cmd", + "*flutflut-plat-med-sg*", + "generic-frag", + "ice-cube-shatter", + "gsf-buffer", + "debug-menus-handler", + "*snow-bumper-sg*", + "gsf-vertex-array", + "loado", + "generic-gif-tag", + "*matrix-engine*", + "snow-ball-roller", + "light-ellipse", + "gsf-vertex", + "fishermans-boat-ride-to-village1", + "junglesnake-default-event-handler", + "blend-from-as-fixed", + "list-keeper-init", + "*hud-parts*", + "swamp-bat-setup-new-path", + "generic-interp-job", + "camera-tracker-process", + "level-hint-control", + "sound-rpc-play", + "happy-plant-opening", + "nav-sphere", + "*subdivide-settings*", + "drawable-inline-array-ambient", + "*citb-robotboss-gun-sg*", + "*thunder-count*", + "level-hint", + "interpTime-offset", + "mei-envmap-tint", + "snow-log-button-idle-down", + "load-package", + "*death-spool-array*", + "drawable-ambient", + "clm-list", + "robber-tired", + "cue-chase", + "sound-basic-cb", + "generic-tie-upload-next", + "name=", + "drawable-tree", + "muse-taunt-2", + "nav-enemy-rnd-int-count", + "setup-blerc-chains-for-one-fragment", + "*pole-mods*", + "prototype-bucket-tie", + "actor", + "drawable-inline-array-node", + "1", + "land-poof", + "joint-anim-compressed-hdr", + "normal-of-plane", + "speed", + "cam-decel", + "draw-node", + "swamp-rope-oscillator", + "voicebox-init-by-other", + "anim-tester-pick-item-setup", + "collide-shape-prim-mesh", + "baseplat", + "drawable-group", + "random-generator", + "*merc-globals*", + "*ogre2-mood-lights-table*", + "*valid-con*", + "starfish", + "drawable-error", + "reserved", + "target-post", + "skeleton", + "can-feet?", + "group-just-poof-straw", + "flop", + "boundary-set-color", + "*sound-player-rpc*", + "cspace", + "*display-actor-vis*", + "add-debug-rot-matrix", + "*lightning-frame-done*", + "sprite-vec-data-3d", + "*display-vol-marks*", + "cavecrusher", + "pelican-explode", + "seconds", + "art-mesh-geo", + "add-debug-curve", + "target-collide-set!", + "string-strip-whitespace!", + "vector-float/!", + "ogr", + "windmill-sail-idle", + "sinerp", + "joint-exploder-joint-callback", + "ct16s-block-table", + "ogreboss-reset-camera", + "*citb-robotboss-belly-sg*", + "*misty-mood*", + "gs-xyzf", + "nav-poly", + "coserp180", + "seconds->race-time", + "deg-seek", + "dm-cam-mode-func", + "*test-ray-start-poly-id*", + "sound-rpc-get-irx-version", + "group-land-poof-ice", + "loading", + "prototype-shrubbery", + "coserp180-clamp", + "vector-vector-distance-squared", + "shadow-find-single-edges", + "can-spawn?", + "clmf-to-select", + "med-res-level", + "*null-kernel-context*", + "*sunkenfisha-red-yellow-sg*", + "tie-init-consts", + "*dive-bottom-mods*", + "high-speed-reject", + "notice-bottom", + "*med-res-village13-sg*", + "vector-reflect-true-flat!", + "teeter-rockland", + "vector--float*!", + "death-end", + "blocking-plane-idle", + "vector-v*float++!", + "target-duck-stance", + "ogre-part", + "sparticle-launchinfo", + "babak-alarmed", + "*irx-version*", + "check-drop-level-assistant", + "quaternion-!", + "string>?", + "ogre-lava", + "GSH_ENABLE", + "halfpipe", + "vector-deg-seek", + "make-vfile-name", + "nav-enemy-test-nav-mesh-intersection-xz?", + "*slope-surface*", + "part-progress-hud-orb-func", + "snow-ball", + "*sky-base-block*", + "*attack-mods*", + "progress-gone", + "vector-deg-slerp", + "shadow-add-double-edges", + "vector-v*float+!", + "campath-time", + "gs-texa", + "curve-length", + "camera-pov-from", + "lavaballoon", + "quicksandlurker-check-hide-transition", + "touch", + "num-func-seek!", + "nav-enemy-player-vulnerable?", + "depth-cue-work", + "entity-info-lookup", + "cspace<-parented-matrix-joint!", + "ripple-create-wave-table", + "effect-param->sound-spec", + "ocean-mid-setup-constants", + "billy-rat-needs-destination", + "glst-tail", + "matrix-from-two-vectors-max-angle!", + "drown-death", + "cavespatulatwo", + "matrix-from-two-vectors!", + "citb-disc-idle", + "babak-roar", + "scf-get-volume", + "store-image", + "cspace<-transform-yxy!", + "can-duck?", + "reset-height", + "put-draw-env", + "nav-ray", + "drawable-tree-array", + "vector-xz-length", + "*citb-robotboss-rightarm-sg*", + "*ocean-colors-village2*", + "cycle-speed", + "debug-menu-item", + "racer-service-slide", + "*already-printed-exeeded-max-cache-tris*", + "effect-just-footprint", + "relocate-later", + "*teleport*", + "find-nearest-attackable", + "do-rotations1", + "merc-death-spawn", + "acos-rad", + "sound-rpc-list-sounds", + "*sunken-elevator-sg*", + "vector-length", + "sun-iris-door-closing", + "reset-vif1-path", + "memory-usage-block", + "generic-vu1-header", + "dark", + "citb-hose-spawn", + "touching-prim", + "vector-sincos-rad!", + "*entity-pool*", + "balloonlurker-pilot", + "effect-control", + "*sprite-aux-list*", + "*game-text-word*", + "internal-draw-debug-line", + "kernel-set-exception-vector", + "*progress-last-task-index*", + "robotboss-green-dark-bomb-wait", + "light-array", + "beachcam-spawn", + "vector-y-quaternion!", + "battlecontroller-spawner", + "add-debug-points", + "update-mood-interp", + "spline-follow-dist", + "nav-enemy-frustrated?", + "*fisher-params*", + "swap-sound-buffers", + "quaternion-conjugate!", + "*no-walk-surface*", + "quaternion-copy!", + "*ocean-trans-indices-sunken*", + "quaternion-vector-y-angle", + "generic-tie-run-control", + "shrub-init-frame", + "gs-csr", + "ready", + "ct16-block-table", + "pick-object", + "waiting-to-run", + "quaternion-rotate-y-to-vector!", + "*water-anim-misty-mud-above-skull-front-sg*", + "distance", + "merc-vtx", + "shrub-upload-model", + "part-progress-card-cell-func", + "*texture-enable-user-menu*", + "timer-count", + "collide-shape-intersect", + "quaternion-inverse!", + "fireboulder", + "process-count", + "lavabase", + "mc-get-slot-info", + "periscope-find-next", + "reset-cameras", + "dead-pool-heap-rec", + "qbert-plat-master-idle", + "vis-gif-tag", + "*pke-hack*", + "recursive-inside-poly", + "nav-enemy-chase", + "shadow-data", + "set-movie-pos", + "mei-ripple", + "eco-bg-red", + "*ocean-spheres-village1*", + "ogre-step-d", + "cam-free-floating-move", + "target-attack", + "inactive", + "*ANIM_TESTER-bank*", + "quaternion-rotate-local-z!", + "swamp-spike-gate-down", + "gs-frame", + "quaternion-log!", + "xyzw", + "quaternion+!", + "quad-copy!", + "*camera-orbit-target*", + "vector-seek-2d-xz-smooth!", + "first?", + "gsf-fx-vertex-array", + "spin-kick", + "matrix-with-scale->quaternion", + "gif-p3tag", + "vector-circle-tangent", + "*generate-actor-vis-output*", + "game-task->string", + "breakaway-left", + "*water-anim-misty-mud-by-dock-sg*", + "initialize", + "seekl", + "*wind-work*", + "game-count-info", + "has-nav-mesh?", + "draw-debug-text-box", + "collide-fragment", + "qbert-plat", + "*eyes-base-vram-word*", + "task-hint-control-group", + "sparticle-track-root-money", + "gs-pmode", + "TIME_PEROIOD(8", + "process-level-heap", + "close", + "manipy", + "update-eyes", + "vector-zero!", + "sprite-distort-vu1-block", + "*master-mode*", + "joint-control", + "trs-matrix-calc!", + "*texscroll-globals*", + "atan", + "*sprite-array-3d*", + "generic-merc-work", + "nav-control", + "matrix3-determinant", + "*display-nav-marks*", + "add-debug-line2d", + "dma-buffer-free", + "plant-boss-leaf-die", + "matrix-rotate-zxy!", + "*ok-options*", + "walk", + "abs", + "rigid-body-platform-float", + "anim-tester-stop", + "quaternion-zxy!", + "want-force-inside", + "effect-param", + "generic-tie-base-point", + "finalboss", + "dma-bank-source", + "no-intro", + "sinerp-clamp", + "light", + "part-hud-health-01-func", + "decompress-fixed-data-to-accumulator", + "spawn-baby-spider-best", + "babak-with-cannon-compute-cannon-dir", + "activate-progress", + "teeter-launch", + "add-debug-sphere-from-table", + "*subdivide-draw-mode*", + "+", + "reflector-origin-update", + "*print-column*", + "group-flop-hit", + "matrix-axis-angle!", + "matrix-y-angle", + "stringMaxLength-offset", + "gs-bitbltbuf", + "matrix-inverse-of-rot-trans!", + "*balloonlurker-rudder-joint-home*", + "quaternion-rotate-x!", + "*ambient-sound-class*", + "50", + "collide-mesh-cache-tri", + "test-xz-point-on-line-segment?", + "box-vector-inside?", + "sfx-movie-volume", + "entity-pool", + "lava-ripple", + "*misty-mood-sun-table*", + "ray-plane-intersect", + "gsf-info", + "charp-basename", + "ogreboss-move-far", + "double-lurker", + "calc-animation-from-spr", + "mem-set32!", + "demo-vis", + "*water-anim-sunken-dark-eco-platform-room-sg*", + "init-time-of-day-context", + "vector4b", + "windturbine-idle", + "camera-master-debug", + "darkvine-event-handler", + "cam-vector-seeker", + "quaternion-negate!", + "*cpad-debug*", + "prototype-tie", + "*blueline-index*", + "glst-add-head", + "vector4w-3", + "rolling-states", + "snow-bunny2", + "group-just-footprint-neutral", + "anim-tester-standard-event-handler", + "joint-work", + "battlecontroller-active", + "*inv-init-table*", + "fisher-done", + "min", + "view-frustum", + "*pre-draw-hook*", + "str-play-kick", + "entity-actor-count", + "attack-or-shove", + "collide-shape-prim-group", + "ocean-transition-check", + "double-lurker-buddy-was-hit", + "*citb-bunny-sg*", + "vector-normalize-ret-len!", + "command-get-float", + "transform-matrix-parent-calc!", + "lod-group", + "bones-wrapup", + "update-subdivide-settings!", + "target-warp-out", + "quaternion", + "clear-entity", + "last", + "lavatube", + "sound-rpc-set-falloff-curve", + "*unity-quaternion*", + "master-is-hopeful-better?", + "mem-size", + "ogreboss-bounce-boulder-event-handler", + "*ocean-colors-village1*", + "interp", + "matrix-4x4-determinant", + "kermit-chase-new-position", + "dma-foreground-sink-group", + "matrix-copy!", + "transformq", + "group-uppercut-hit", + "clone-anim-once", + "matrix4h", + "bounding-box-both", + "set-tfrag-dists!", + "lurkerworm-rise", + "rolling", + "path-control", + "*ocean-near-indices-village2*", + "bounding-box4w", + "exp-slead", + "*ocean-base-vram-word*", + "*time-of-day-fast*", + "tie-near-init-consts", + "float-save-yellowline", + "pull-rider-info", + "ocean-trans-add-upload-table", + "vector", + "matrix-3x3-determinant", + "quaternion->matrix", + "convert-to-hud-object", + "flut-info", + "*draw-hook*", + "skeleton-group", + "float-save-redline", + "sound-iop-info", + "vector-vector-deg-slerp!", + "curve-control", + "force", + "upload-vram-data", + "*collide-player-list*", + "*flut-double-jump-mods*", + "eco-bg-blue", + "dm-actor-vis-pick-func", + "vector3h", + "flava", + "*eye-control-array*", + "*sound-loader-rpc*", + "add-blue-shake", + "dm-subdiv-float", + "joint-mod-rotate-handler", + "*collide-cache*", + "continue-point", + "glst-init-list!", + "*up-vector*", + "*default-dead-pool*", + "shadow-control", + "*texture-relocate-later*", + "dm-task-reward-speech", + "ray-circle-intersect", + "*display-camera-old-stats*", + "citb-base-plat-active", + "*oracle-sg*", + "qword", + "*fuel-cell-tune-pos*", + "*tar-surface*", + "vector4h", + "lod-dist", + "delayed-rand-vector", + "lerp", + "font-set-tex0", + "breath-in", + "rolling-vis", + "sync-path", + "*jungleb-mood-fog-table*", + "matrix-from-two-vectors-partial-linear!", + "blue-eco-charger-close", + "kermit-speak1", + "debug-menus-default-joypad-func", + "spat-work", + "clmf-save-single", + "*depth-cue-base-block*", + "lurkerworm-default-event-handler", + "perf-stat", + "*kernel-boot-mode*", + "target-flop-hit-ground", + "green-eco-lurker-wait-to-appear", + "debug-menu-item-render", + "mood-lights-table", + "plant-boss-spawn", + "describe-methods", + "cylinder-flat", + "debug-menu-render", + "mc-run", + "is-cd-in?", + "fuse", + "stopwatch-end", + "*lurkerpuppy-sg*", + "vector<-pad-in-surface!", + "matrix-!", + "swamp-barrel", + "generic-strip", + "*ogreboss*", + "*med-res-jungle-sg*", + "mesh", + "blue-eco-charger", + "dma-foreground-sink", + "sprite-add-3d-all", + "*external-cam-mode*", + "vif-fbrst", + "slave-set-rotation!", + "quicksandlurker-die", + "part-first-person-hud-right-func", + "reset-graph", + "target-tube-jump", + "*language-name-remap*", + "*swamp-rock-sg*", + "*ocean-subdivide-draw-mode*", + "file-stream", + "forward-up-nopitch->quaternion", + "group-just-footprint-dirt", + "*spiderwebs-sg*", + "isphere", + "euler-angles", + "stringMinLength", + "swim", + "*boundary-polygon*", + "intro-time", + "get-powered", + "time-to-ground", + "towertop-idle", + "kill-not-type", + "hutlamp", + "part-progress-hud-left-func", + "*flip-jump-mods*", + "dm-cam-render-float", + "generic-tie-work", + "target-jump-forward", + "exit-chamber-lower", + "rand-vu-float-range", + "*race-track-surface*", + "*plat-flip-sg*", + "display", + "endlessfall", + "*task-controls*", + "*random-generator*", + "*flutflut-shadow-control*", + "*last-loado-global-usage*", + "collide-shape-draw-debug-marks", + "gs-st", + "idle-distance", + "ambient-list", + "mt8-block-table", + "num-func-chan", + "merc-ctrl-header", + "seek", + "cutoutvol-flags", + "rand-vu-int-count", + "add-debug-flat-triangle", + "rot-offset", + "matrix-scale!", + "tntbarrel", + "*mistycam-sg*", + "movie?", + "ogreboss-super-boulder-impact-effect", + "accordian-idle", + "quaternion-float*!", + "file-stream-write", + "_empty_", + "position", + "merc-globals", + "*hopper-nav-enemy-info*", + "pelican", + "vi1", + "target-align-vel-z-adjust", + "group-run-poof-snow", + "*camera-old-level*", + "cat-string<-string_to_charp", + "ocean-mid-indices", + "adgif-shader<-texture-with-update!", + "cspace<-transformq!", + "make-sqrt-table", + "part-hud-eco-timer-func", + "vector<-cspace!", + "dma-send-chain", + "*camera-master-dead-pool*", + "locked", + "*flash7*", + "group-smack-surface", + "*title-credits-spacing*", + "*maincave-mood-fog-table*", + "str-load", + "*sunken-mood-sun-table*", + "*sage-villagec-tasks*", + "citb-drop-plat", + "*generic-envmap-texture*", + "quaternion-dot", + "harmless", + "generic-dma-from-spr", + "*artist-fix-visible*", + "swamp-rope-rand-float", + "*debug-ray-test-capture-output*", + "save-sequences", + "*timeplot-table*", + "sp-process-block-2d", + "flava-lookup", + "debug-menu-context-select-next-or-prev-item", + "str-is-playing?", + "generic-dma-foreground-sink", + "snow-eggtop", + "joint-control-channel-eval", + "string<=?", + "ogreboss-super-boulder", + "*debug-draw-pauseable*", + "quaternion-vector-angle!", + "target-duck-walk", + "robber-dies", + "nav-enemy-player-at-frustration-point?", + "floating-launcher", + "*wedge-plat-outer-sg*", + "vector-y-angle", + "quaternion-seek", + "vector2w", + "*water-anim-misty-dark-eco-pool-sg*", + "*kernel-packages*", + "quaternion-zero!", + "reflector-middle-idle", + "robotboss-blue-dark-bomb", + "sidekick", + "cave-trap-active", + "*deci-count*", + "CAM_ORBIT-bank", + "string-charp=", + "translation_info", + "*ocean-verts*", + "projectile-yellow", + "return-from-thread-dead", + "draw-eco-beam", + "tfragment-debug-data", + "logxor", + "*village2-mood-lights-table*", + "check-drop-level-assistant-bluehut", + "group-slide-poof-stone", + "flames-state", + "event-message-block", + "ramdisk-rpc-fill", + "plant-boss-leaf-init", + "shadow-vu0-upload", + "*identity-vector*", + "clm-list-item", + "rand-uint31-gen", + "nav-enemy-set-hit-from-direction", + "coserp-clamp", + "protect-frame", + "flying-lurker-sleep", + "gs-scissor", + "ocean-vu0-work", + "ice-cube-mean-turn-to-charge", + "generic-tie-header", + "nav-enemy-victory", + "*default-skel-template*", + "*eyes-base-page*", + "dead-pool-heap", + "upload-generic-shrub", + "pause", + "bird", + "*profile-h*", + "matrix-rotate-zyx!", + "*search-info*", + "dgo-header", + "vector=", + "light-eco-child-appear", + "mc-handle", + "*pickup-dead-pool*", + "*enable-instance-tie-menu*", + "vector-line-distance", + "bubling-lava", + "texture-page-dir", + "gs-trxreg", + "*time-of-day-context*", + "launcher", + "ripple-slow-add-sine-waves", + "load-boundary-from-template", + "valid?", + "sun", + "gs-uv", + "quaternion-y-angle", + "ct32-24-block-table", + "hutlamp-idle", + "vif1-handler", + "gs-zbuf", + "*ogreboss-shadow-control*", + "drawable-tree-ambient", + "cpad-set-buzz!", + "eggtop-close", + "dma-sync-fast", + "effect-walk-step-right", + "vector4w-2", + "atan0", + "*scarecrow-b-break-sg*", + "curve-closest-point", + "logior", + "list-keeper-active", + "GSH_MAX_DISPLAY", + "center-joint", + "terrain-context", + "quaternion-rotate-local-x!", + "uint8", + "target-demo", + "path5", + "generic-tie-convert-proc", + "entity-deactivate-handler", + "generic-pris-mtx-save", + "static-screen-init-by-other", + "process-status-bits", + "stack", + "exp-strail", + "sharkey-get-player-position", + "mood-lights", + "*gondolacables-sg*", + "*miners-tasks*", + "snowcam", + "*start-pos*", + "int64", + "fuel-cell-hud-orbit-callback", + "blocking-plane", + "gs-gif-tag", + "update-mood-itimes", + "string-get-int32!!", + "hit", + "gif-tag64", + "misty-cam-restore", + "bird-2", + "*gambler-tasks*", + "quaternion-rotate-local-y!", + "snow-gears", + "sky-tng-data", + "cavecrystal-active", + "maxAngle", + "int128", + "plane", + "gui-query", + "resend-exception", + "debug-menu-find-from-template", + "string", + "*tra-pontoon-sg*", + "dm-cam-settings-func", + "*display*", + "parameter-ease-sqrt-clamp", + "gs-tex0", + "transform", + "deg-seek-smooth", + "*default-pool*", + "sincos!", + "generic-debug", + "ambient-type-dark", + "int32", + "sprite-get-user-hvdf", + "camera-teleport-to-entity", + "unload", + "debug-create-cam-restore", + "vector4w", + "cam-fixed", + "mem-copy!", + "*display-art-control*", + "keg", + "calc-terminal4-vel", + "texture-pool", + "deinstall-debug-handler", + "texscroll-execute", + "cit", + "swim-stroke", + "prototype-bucket-recalc-fields", + "art", + "*display-memcard-info*", + "peeper-up", + "find-adjacent-bounds-one", + "*global-attack-id*", + "actor-get-arg!", + "empty2", + "vis-cull", + "*kernel-boot-level*", + "trsqv", + "string-get-arg!!", + "clmf-cam-flag", + "gsf-fx-vertex", + "group-spin-hit", + "update-mood-finalboss", + "calculate-basis-functions-vector!", + "*plant-boss-shadow-control*", + "collide-planes-test1", + "*flop-land-mods*", + "task-control-reset", + "fact", + "*launch-jump-mods*", + "*quicksand-surface*", + "vector3s-copy!", + "*progress-hook*", + "merc-effect-bucket-info", + "bidirectional", + "viewer-geo-name", + "ground-tween-info", + "kill-by-type", + "vu-code-block", + "draw-inline-array-prototype-tie-near-asm", + "target-turn-around", + "projectile-collision-reaction", + "dm-strip-lines-toggle-pick-func", + "keg-conveyor-paddle", + "ripple-for-misty-dark-eco-pool", + "border-plane", + "-", + "ocean-corner", + "light-group", + "cam-rotation-tracker", + "gs-display-fb", + "sp-process-block-3d", + "copy-tracking", + "*z-vector*", + "exp", + "allpontoons", + "scale-matrix!", + "cavecrystal-light", + "string->float", + "swamp-blimp-bank", + "*shadow-data*", + "quaternion-xz-angle", + "tan-rad", + "paals-cam-restore", + "ogreboss-stage2", + "*citb-disc-b-sg*", + "joint-exploder-static-joint-params", + "dma-packet", + "sprite-setup-frame-data", + "inline-array-class", + "cspace-index-by-name", + "gs-color-clamp", + "yakow-graze-kicked", + "bomb-going", + "draw", + "*display-profile*", + "generic-tie-stats", + "sound-rpc-set-master-volume", + "target-actor", + "gnawer-die", + "change-brother", + "target-swim-jump-jump", + "target-hit-orient", + "sprite-distorter-generate-tables", + "ref", + "tra", + "ramdisk-load", + "*med-res-ogre-sg*", + "*cam-debug-los-tri*", + "*ogre-isle-d-sg*", + "login-state", + "rgbaf", + "quaternion-k!", + "*puffer-sg*", + "nothing", + "kill-by-name", + "*joint-axis-vectors*", + "*temp-mem-usage*", + "forward-down->inv-matrix", + "mat-remove-z-rot", + "nav-enemy-wait-for-cue", + "clmf-to-vol-attr", + "*load-str-rpc*", + "*cos-poly-vec*", + "vector-v*float!", + "box-vector-enside?", + "*maindoor-sg*", + "texture-page-near-allocate-1", + "drawable-tree-ice-tfrag", + "want-vis", + "eul->quat", + "*16k-dead-pool*", + "error-sphere", + "*progress-stack*", + "dark-plants-all-done", + "*sp-frame-time*", + "vector3s", + "fuel-cell-hud-starburst-4-callback", + "*mis-bone-platform-sg*", + "dead-state", + "*debug-part-dead-pool*", + "cpad-open", + "*village3-mood-fog-table*", + "frame-stats", + "*volume-normal*", + "ramboss-laugh", + "eco-bg-yellow", + "jungle-states", + "swamp-rope", + "joint-anim-transformq", + "warp-vector-into-surface!", + "instance-tie-work", + "*warrior-sg*", + "part-tracker-move-to-target", + "tie-fragment", + "anim", + "*ocean-base-page*", + "install-default-debug-handler", + "robotboss-yellowshot", + "make-joint-jump-tables", + "land", + "*firecanyon-palette-interp-table*", + "adgif-shader", + "debug-menu-item-var-update-display-str", + "target-hit", + "cpad-get-data", + "matrix-stack", + "false-func", + "*debug-lines-trk*", + "look", + "snow-bunny-patrol-idle", + "*double-jump-mods*", + "art-load", + "back", + "*eyes-base-block*", + "*string-tmp-str*", + "kermit-letgo", + "vector4ub", + "*yellow-jump-mods*", + "jak-fall", + "ripple-find-height", + "merc-vu1-initialize-chain", + "*FACT-bank*", + "forall-particles", + "draw-bones", + "nav-enemy-rnd-float-range", + "los-cw-ccw", + "sin", + "vif-bank", + "rpc-call", + "nearest-y-threshold", + "damp-up", + "insert-cons!", + "projectile-init-by-other", + "matrix3-inverse-transpose!", + "foothit", + "kernel-copy-to-kernel-ram", + "*lavaballoon-sg*", + "*default-continue*", + "*listener-process*", + "*snow-mood-fog-table*", + "target-racing-start", + "blend", + "orbit-plat-rotating", + "delete-car!", + "*rolling-spheres-light2*", + "be-clone", + "lognor", + "group-run-poof-grass", + "ocean-texture-add-constants", + "symbol", + "*misty-mood-lights-table*", + "kmalloc", + "*display-cam-master-marks*", + "update-mood-default", + "identity", + "*lightning-mole-sg*", + "snow-bunny", + "effect-slide", + "vector-vector-distance", + "*mother-spider-sg*", + "1/", + "*display-xyz-axes*", + "vector-!", + "print", + "*snowpusher-sg*", + "vector-rotate-around-y!", + "no-push", + "*sin-poly-vec*", + "quaternion-left-mult-matrix!", + "*ocean-vu0-work*", + "nav-enemy-facing-direction?", + "quaternion-right-mult-matrix!", + "*time-of-day-proc*", + "hud-going-out", + "breakpoint-range-set!", + "moving-sphere-triangle-intersect", + "atan-rad", + "snow-button-deactivate", + "stringsymbol", + "generic-storage", + "target-flut-double-jump", + "*mistycannon-sg*", + "true-func", + "add-debug-outline-triangle", + "vector-rotate-y!", + "fleeing-nav-enemy-debug", + "set-eul!", + "ocean-trans-strip", + "target-attack-uppercut", + "fuel-cell-pick-anim", + "alt-vector", + "*debug-dead-pool*", + "energybase-stopped", + "start-collect-nav", + "-offset", + "display-pool", + "memory-usage-info", + "sp-queued-launch-particles", + "*camera-other-trans*", + "get-rotate-point!", + "group-land-poof-dpsnow", + "eco-info", + "assoce", + "global", + "game", + "ocean-near-upload", + "generic-no-light+envmap", + "process-by-name", + "focalpull-flags-off", + "end-calc", + "drop", + "string->int", + "member", + "logand", + "generic-light-proc", + "*title*", + "citb-arm-d", + "*color-dark-red*", + "crate-post", + "set-frame-num", + "profile-bar", + "merc-fragment", + "sound-rpc-load-music", + "else", + "part-tracker-track-root", + "generic-init-buf", + "blocked", + "jump-low", + "reset-and-call", + "*camera-layout-blink*", + "anim-mode", + "#t", + "*standard-ground-surface*", + "vector-reset!", + "*volume-normal-current*", + "*cavecrystal-engine*", + "debug-menu-append-item", + "*ramdisk-rpc*", + "vector-reflect-flat-above!", + "sound-rpc-cmd", + "*display-lights*", + "*master-exit*", + "perf-stat-array", + "kernel-write", + "generic-saves", + "zoom-land", + "snow-states", + "hud-leaving", + "snow-log-wait-for-master", + "*ogre-isle-c-sg*", + "adgif-shader-login-no-remap", + "qbert-plat-wait-for-master", + "cylinder", + "deactivate-particle", + "vif-unpack-imm", + "swamp-bat-slave", + "memcpy", + "nav-gap-info", + "sort", + "*__private-assert-info*", + "details", + "ride-down", + "art-group", + "cspace<-matrix-no-push-joint!", + "debug-menu-make-shader-menu", + "service-cpads", + "keg-conveyor-paddle-init-by-other", + "ripple-request", + "asin", + "arcing-shot-calculate", + "drawable-tree-lowres-trans-tfrag", + "point-in-plane-<-point+normal!", + "*display-actor-marks*", + "debug-vertex", + "main-menu", + "dma-sync-with-count", + "vector-v++!", + "merc-eye-anim-frame", + "process-tree", + "lognot", + "*last-loado-length*", + "cpad-info", + "*rigid-body-platform-constants*", + "*trace-list*", + "*bladeassm-sg*", + "#f", + "minershort", + "vector-v+!", + "matrix<-transformq+world-trans!", + "become-hud-object", + "instance-shrubbery", + "crab-walk1", + "drawable", + "dgo-load-get-next", + "matrix-transpose!", + "string>=?", + "handle->process-safe", + "orb-cache-top-complete", + "duck-slide", + "anim-test-sequence-list-handler", + "plat-button", + "clear-vu0-mem", + "sp-process-particle-system", + "sunkenb", + "movie-pos", + "*depth-cue-base-page*", + "jungle-part", + "cam-layout-save-splineoffset", + "village1-vis", + "*load-options*", + "energyball-init", + "snow-fort-gate", + "cave-trap-idle", + "eye", + "overlaps-others-params", + "group-run-poof-ice", + "sound-rpc-sound-cmd", + "rand-vu-init", + "set-side-vel", + "*last-loado-debug-usage*", + "matrix-identity!", + "mistycannon-find-trajectory", + "indent-to", + "*qbert-plat-sg*", + "merc-vu1-low-mem", + "quaternion-from-two-vectors!", + "*video-reset-parm*", + "gs-fog", + "snow-gears-idle", + "set-font-color-alpha", + "*artist-fix-frustum*", + "trsq", + "deg-lerp-clamp", + "exit", + "bone-cache", + "disasm-dma-tag", + "circle-test", + "debug", + "initialize-go", + "*global-search-name*", + "*jump-mods*", + "target-move-dist", + "lerp-clamp", + "level-hint-displayed?", + "matrix-axis-sin-cos-vu!", + "default-pool", + "double-lurker-top-on-shoulders", + "static-screen-spawn", + "vector-seconds", + "quicksandlurker-victory", + "*water-anim-village2-bucket-sg*", + "*ocean-mid-indices-village2*", + "shadow-find-double-edges", + "auto-save-command", + "change-parent", + "misty-ambush-height-probe", + "initialize-dead", + "*kernel-context*", + "setting-reset", + "num-func-blend-in!", + "bone-bigswing", + "*post-draw-hook*", + "*target-dead-pool*", + "sound-set-sound-falloff", + "sprite-release-user-hvdf", + "*shadow-debug*", + "eval", + "transform-point-vector-scale!", + "execute-process-tree", + "*stone-surface*", + "*fake-shadow-buffer-2*", + "ogreboss-super-boulder-throw", + "dgo-load", + "*med-res-beach2-sg*", + "joint-exploder-joints", + "draw-inline-array-tfrag-near", + "loc-name-id", + "*temp-string*", + "*bouncer-sg*", + "vector4s-3", + "*default-lights*", + "*spider-egg-unbroken-sg*", + "group-land-poof-pcmetal", + "bounding-box", + "quaternion-norm2", + "jump", + "*display-collision-marks*", + "path8-k", + "energydoor-player-dist", + "*display-instance-info*", + "*dram-stack*", + "collide-puss-work", + "*display-split-boxes*", + "add-debug-arc", + "res-lump", + "vector-plane-distance", + "*display-actor-anim*", + "citb-sagecage-init-by-other", + "*water-anim-maincave-lower-right-pool-sg*", + "art-joint-anim", + "cpu-thread", + "dma-bucket-insert-tag", + "texture-page-dir-entry", + "*ocean-work*", + "cam-string-find-position-rel!", + "process-not-name", + "vector-line-distance-point!", + "pair", + "*sp-particle-system-2d*", + "quaternion-axis-angle!", + "mem-print", + "*kernel-version*", + "catn-string<-charp", + "collide-probe-stack", + "active-pool", + "group-ram-boss-foot-puff", + "inspect-process-tree", + "collide-list-item", + "mother-spider-leg-flying", + "ocean-mid-masks", + "dma-buffer-patch-buckets", + "part-id", + "yeti-slave-default-event-handler", + "progress-coming-in", + "swamp-blimp-setup", + "*CAMERA-bank*", + "camera-pool", + "*robocave-mood*", + "pathe", + "ocean-spheres", + "append!", + "truncate", + "check-irx-version", + "l2", + "weather-off", + "drawable-tree-dirt-tfrag", + "cam-layout-active", + "load-dir-art-group", + "*texture-enable-user*", + "actor-link-dead-hook", + "query-reset", + "abort-request", + "go-away", + "lurkrat-walk", + "vec4s", + "ease-in-out", + "quat->eul", + "sound-name", + "camera-slave", + "replace-load-boundary", + "swap-display", + "*citb-generator-sg*", + "sound-pause", + "mother-spider", + "clone-copy-trans", + "stopwatch-init", + "dax-land", + "effect-slide-poof", + "*listener-function*", + "plat-post", + "*ocean-heights*", + "*spider-egg-broken-sg*", + "*enable-method-set*", + "bully-notice", + "unknown-cpu-time", + "rand-vu-int-range", + "find-parent-method", + "cam-circular", + "*evilbro-sg*", + "*display-ambient-ocean-near-off-marks*", + "wedge-plat-outer-tip", + "sprite-header", + "anim-test-seq-item-copy!", + "*ocean-near-indices-village1*", + "ja-min?", + "shrubbery", + "*ocean-trans-strip-array*", + "post", + "*lev-string*", + "level-deactivate", + "*logo-sg*", + "return-from-exception", + "upload-vis-bits", + "split-steps", + "*8k-dead-pool*", + "snow-ball-roller-path-update", + "*tfrag-display-stats*", + "ocean-texture-vu1-block", + "vector-to-ups!", + "cam-string-bank", + "res-tag", + "hide-progress-screen", + "gs-test", + "*display-ambient-sound-marks*", + "lavatube-states", + "lowmemmap", + "trans", + "wind-loop", + "texture-page-level-allocate", + "string-strip-trailing-whitespace!", + "ogreboss-stage3-shuffle", + "*active-pool*", + "*citb-yellowsage-tasks*", + "trajectory", + "GSH_TIME", + "robber-die", + "kernel-set-level2-vector", + "matrix-3x3-inverse-transpose!", + "spiderwebs-default-event-handler", + "*grottopole-sg*", + "copy-charp<-charp", + "kernel-set-interrupt-vector", + "*mem-usage*", + "mistycam-spawn", + "*debug-ray-offset*", + "listener", + "kernel-context", + "sp-get-approx-alloc-size", + "matrix-rot-diff!", + "lavatube-lava", + "copyn-string<-charp", + "pat-surface", + "new-dynamic-structure", + "vector-copy!", + "update-rain", + "blob-died", + "string<-charp", + "sprite-set-3d-quaternion!", + "active", + "ocean-colors", + "*lavatube-mood*", + "target-racing-post", + "*water-anim-sunken-hall-with-three-whirlpools-sg*", + "attack", + "*citb-robotboss-nose-sg*", + "quaternion-exp!", + "joystick", + "*crate-iron-sg*", + "dma-bank-control", + "string-get-float!!", + "*math-camera-fog-correction*", + "ecovalve", + "*junglesnake-twist-max-deltas*", + "string-skip-whitespace", + "*energybase-sg*", + "fleeing-nav-enemy-chase-post", + "string-strip-leading-whitespace!", + "sp-copy-to-spr", + "mercneric-bittable-asm", + "mei-texture-scroll", + "robotboss-blue-beam", + "stats-tfrag-asm", + "generic-merc-add-to-cue", + "lbvtx", + "swamp-spike-idle", + "blerc-execute", + "*clm-save-all*", + "generic-tie-dma-to-spad", + "ja-anim-done?", + "finalboss-states", + "group-just-poof-neutral", + "texture", + "seagull-idle", + "joint-mod-set-world-callback", + "flutflut-plat-med", + "*nav-patch-route-table*", + "*water-anim-misty-mud-above-skeleton-sg*", + "*global-search-count*", + "*slow-frame-rate*", + "joint-anim", + "gif-ctrl", + "full", + "event", + "adgif-shader-array", + "drawable-inline-array-actor", + "nav-sphere-from-cam", + "kermit-chase", + "matrix-rotate-x!", + "plunge", + "upload-vram-pages-pris", + "swamp-bat-slave-swoop", + "shrub-make-perspective-matrix", + "actor-bank", + "target-pool", + "robotboss-red-dark-bomb", + "target-pole-flip-up", + "string-suck-up!", + "*pause-context*", + "driller-lurker-default-event-handler", + "matrix-rotate-yzx!", + "query", + "rope-stretch", + "vector-reflect-flat!", + "hud-icon", + "add-debug-line", + "sound-rpc-group-cmd", + "quaternion-rotate-z!", + "sky-add-frame-data", + "*ocean-map-sunken*", + "high-jump", + "cavegem", + "vector-seek-3d-smooth!", + "vector-average!", + "group-just-poof-gravel", + "nav-enemy-set-base-collide-sphere-collide-with", + "collide-history", + "orb-cache-top", + "level-hint-normal", + "set-credits-font-color", + "birth-func-ocean-height", + "explode", + "sync", + "process-drawable-art-error", + "profile-frame", + "static-vectorm", + "matrix+!", + "splita-spot", + "dm-edit-instance-toggle-pick-func", + "robocave", + "draw-bones-generic-merc", + "string-cat-to-last-char", + "ogreboss-super-boulder-hit", + "prejoint", + "box8s", + "vector4-dot-vu", + "sinteger", + "continue", + "flush-cache", + "dgo-file", + "plat-button-pressed", + "firecanyon", + "group-just-footprint-ice", + "cavecrystal-light-control", + "target-racing-death", + "font-work", + "shader-ptr", + "set-standoff-height", + "enter-state", + "balloon-dies", + "__assert-info-private-struct", + "*sp-launcher-lock*", + "*video-parms*", + "*ocean-spheres-village2*", + "inherit-state", + "cpad-list", + "*redring-sg*", + "*volume-descriptor-current*", + "*null-vector*", + "vector8h", + "racer-bend-gravity", + "qmem-copy<-!", + "print-tr-stat", + "bone-layout", + "vector-normalize!", + "green-eco-lurker-init-by-other", + "structure", + "target", + "group-just-footprint-wood", + "blue-eco-charger-orb", + "adgif-shader-login", + "dm-time-of-day-pick-func", + "swim-bottom", + "*zero-vector*", + "update-visible", + "*flut-jump-mods*", + "mt4-block-table", + "message", + "ocean-mid-vertex", + "*display-camera-info*", + "bg-r", + "stopwatch-start", + "kset-language", + "*sin-poly-vec2*", + "*camera-other-matrix*", + "*ecoclaw-sg*", + "*login-state*", + "plunger-lurker-plunge", + "nav-enemy-chase-post", + "binary-table", + "*evilbro-village3-sg*", + "vector-rad<-vector-deg!", + "*perf-stats*", + "ice-cube-appear", + "spider-egg-die", + "tfrag-end-buffer", + "bsp-node", + "matrix", + "*edit-shader*", + "sound-rpc-reload-info", + "cavetrapdoor", + "*clm-focalpull-attr*", + "cage-bird-5", + "pelican-fly-to-end", + "*y-vector*", + "depth-cue-calc-z", + "target-effect-exit", + "*logo-volumes-sg*", + "sync-info-paused", + "sound-rpc-stop-sound", + "vif-err", + "camera-look-at", + "melt", + "kernel-copy-function", + "generic-work-init", + "eco", + "joint-mod-look-at-handler", + "*misty-palette-interp-table*", + "real-main-draw-hook", + "*options*", + "collide-hit-by-player-list", + "swamp-rope-idle-arm", + "*pontoonten-sg*", + "save-last", + "*ocean-up-table*", + "joint-control-channel-group!", + "drawable-tree-trans-tfrag", + "cam-float-seeker", + "*current-sound-id*", + "merc-fp-header", + "sound-rpc-set-ear-trans", + "ambient-sound", + "sound-rpc-pause-group", + "sound-rpc-shutdown", + "print-language-name", + "*stdcon1*", + "citb-generator-trigger-others", + "tan", + "lerp-scale", + "set-display2", + "yellow-sagecage", + "debug-menu-make-task-menu", + "mood-sun-table", + "tie-near-vu1-block", + "helix-button-idle-down", + "swamp-rat-nest-dummy-c", + "throw-dispatch", + "target-tube-start", + "robotboss-blue-dark-bomb-wait", + "*display-cam-collide-history*", + "sound-rpc-unload-bank", + "*vu1-enable-user*", + "particle-adgif", + "level", + "timer-mode", + "dist-info-init", + "print-level-name", + "set-point", + "master-unset-region", + "sound-rpc-pause-sound", + "collide-overlap-result", + "decomp-work", + "close-gif-packet", + "*babak-sg*", + "cam-master-active", + "render-sky-quad", + "sound-play-parms", + "merc-vu1-init-buffer", + "*ocean-texture*", + "flava-table-row", + "*sound-bank-1*", + "white-eco-picked-up", + "generic-tie-bps", + "sound-spec", + "sound-rpc-set-sound-falloff", + "*mother-spider-egg-broken-sg*", + "citb-disc-c", + "shrub-init-view-data", + "lb-add-load-plane", + "*ocean-mid-indices-village1*", + "vehicle-controller", + "dma-gif-packet", + "clmf-cam-meters", + "quaternion-i!", + "delete!", + "sound-rpc-set-reverb", + "vector3s-!", + "master-switch-to-entity", + "robotboss-daxter-sacrifice-movie", + "citb-firehose-idle", + "*camera-orbit-info*", + "instance-shrub-dma", + "/", + "process-drawable-from-entity!", + "physical-address", + "ice-cube-become-mean", + "dma-buffer-send", + "sound-rpc-bank-cmd", + "search-process-tree", + "ram", + "list-field", + "kernel-read-function", + "sound-rpc-set-flava", + "game-save-tag", + "vector-array", + "mod", + "alternates", + "precursor-arm-sink", + "*mayor-sg*", + "walk-dwater1", + "enable-irq", + "*water-anim-look*", + "bg-a", + "*camera-read-analog*", + "level-buffer-state", + "energydoor-closed-handler", + "string->sound-name", + "cam-string-code", + "boulder3-trans-2", + "*ticks-per-frame*", + "process-drawable-birth-fuel-cell", + "logo", + "entity-perm", + "dma-bank-vif", + "end-collect-nav", + "fade", + "timer-bank", + "target-racing-get-off", + "duck", + "collide-edge-hold-list", + "GSH_BUCKET", + "sky-set-sun-colors-sun", + "vector4-dot", + "hud-bike-speed", + "*last-master-mode*", + "sound-rpc-set-language", + "stinger", + "stopwatch-elapsed-seconds", + "*ocean-trans-indices-village1*", + "*ogre-bridge-joint-array*", + "set-display-env", + "clmf-cam-float-adjust", + "disable-hud", + "EulSafe", + "stopwatch-elapsed-ticks", + "hud-parts", + "dark-crystal-idle", + "*ogre3-mood*", + "splita-taunt", + "cfs-work", + "effect-run-poof", + "generic-tie-normal", + "timer-reset", + "level-hint-surpress!", + "citb-drop-plat-idle", + "stopwatch-reset", + "vif-stat", + "nav-mesh-lookup-route", + "joint-mod-world-look-at-handler", + "clip-travel-vector-to-mesh-return-info", + "yeti-slave-init-by-other", + "*target-shadow-control*", + "swingpole", + "matrix-rotate-yxy!", + "dma-send-chain-no-flush", + "vif-tag", + "shortcut-boulder-idle", + "level-tasks-info", + "vector3s-matrix*!", + "eul->matrix", + "dma-bucket", + "citb-hose-idle", + "code", + "method-set!", + "vif-mask", + "dma-enable", + "master-track-target", + "vu-stat", + "gnawer-give-fuel-cell", + "init-background", + "type-type?", + "dma-bank", + "generic-bucket-state", + "*4k-dead-pool*", + "dump-bone-mem", + "swamp-rope-trans", + "draw-title-credits", + "symlink3", + "level-vis-info", + "dma-send-from-spr", + "smush-control", + "hw-cpad", + "fisher-fish", + "camera-bounding-box-draw", + "start-hint-timer", + "shadow-work", + "dma-sync-hang", + "sprite-array-3d", + "launcher-active", + "village2-amb", + "flylurk-taunt", + "generic-envmap-saves", + "sequenceC-can-trans-hook-2", + "blocking-plane-destroy", + "target-snowball-start", + "boatpaddle-idle", + "find-instance-by-name", + "*main-options-demo-shared*", + "joint-mod", + "symlink2", + "align-offset", + "ultimate-memcpy", + "shadow-vertex", + "notice-blue", + "time-of-day-dma", + "dma-initialize", + "group-slide-poof-snow", + "*collide-hit-by-player-list*", + "process-drawable-random-point!", + "shadow-vu0-block", + "target-attack-up", + "logo-init-by-other", + "citb-drop-plat-active", + "get-current-time", + "dma-send-chain-no-tte", + "v-slrp2!", + "quaternion-float/!", + "vector-3pt-cross!", + "dma-sync", + "*cam-free-move-along-z*", + "current-str-id", + "rpc-buffer", + "off", + "reflector-origin", + "dma-buffer-length", + "*ocean-trans-indices-village2*", + "vector-x-angle", + "redshot-wait", + "shadow-queue-append", + "get-video-mode", + "shadow-execute", + "boulder4-trans", + "greenshot-init-by-other", + "*find-poly-timer*", + "dma-buffer-send-chain", + "process-taskable-hide-handler", + "quaternion-slerp!", + "wind-get-hashed-index", + "external-art-buffer", + "*water-anim-maincave-water-with-crystal-sg*", + "proxy-prototype-array-tie", + "part-first-person-hud-left-func", + "level-activate", + "dma-buffer-add-buckets", + "path-k", + "focalPull-offset", + "merc-mat-dest", + "target-state-hook-exit", + "darkvine", + "mc-slot-info", + "dma-buffer", + "snow-gears-activate", + "ocean-off", + "mother-spider-die-from-uppercut", + "*display-ambient-music-marks*", + "ripple-apply-wave-table", + "*vif-disasm-table*", + "quaternion-norm", + "intro-exitValue", + "shock-in", + "append-character-to-string", + "disasm-vif-tag", + "collide-cache-prim", + "disasm-vif-details", + "*palette-fade-controls*", + "time-frame", + "*dma-disasm*", + "interp-test", + "disasm-dma-list", + "texture-page-segment", + "*sky-drawn*", + "effect-walk-step-left", + "camera-line-rel-len", + "instance-tie-dma", + "*dgo-time*", + "vector-dot", + "joint-anim-compressed-frame", + "cam-index", + "*stat-string-tfrag*", + "*TARGET-bank*", + "generic-envmap-dproc", + "sound-set-reverb", + "hud", + "flying-lurker", + "analog-input", + "eco-pill", + "collide-planes", + "subdivide-dists", + "process-disconnect", + "cpad-invalid!", + "*ogre-bridgeend-sg*", + "deactivate-progress", + "*stats-bsp*", + "fisher-fish-init-by-other", + "draw-ocean-far", + "time-of-day-setup", + "ramdisk-rpc-load-to-ee", + "*cheat-mode*", + "game-save-elt->string", + "shadow-find-facing-double-tris", + "cam-endlessfall", + "water-actor", + "*cpad-list*", + "burnup", + "get-integral-current-time", + "psm->string", + "box8s-array", + "cam-mistycannon", + "restore", + "target-shoved", + "vector-vector-xz-distance-squared", + "gif-tag", + "matrix-from-control!", + "check-boundary", + "main-draw-hook", + "texture-mip->segment", + "dma-send-from-spr-no-flush", + "gif-packet", + "ocean-trans-add-upload-strip", + "drawable-tree-instance-shrub", + "ocean-mid-block", + "gs-fogcol", + "spawn-baby-spider-work", + "mother-spider-leg", + "no-load-wait", + "gif-bank", + "draw-sprite2d-xy", + "quaternion-from-two-vectors-max-angle!", + "*global-toggle*", + "*sprite-hvdf-control*", + "quaternion*!", + "generic-vu1-block", + "camera-line", + "*water-anim-sunken-start-of-helix-slide-sg*", + "dialog-volume", + "yes", + "*display-cam-other*", + "gs-prim", + "eco-track-root-prim-fadeout", + "path-actor", + "*artist-error-spheres*", + "*shadow-object*", + "reset-path", + "village-fish-idle", + "dar", + "tube", + "bone-stepr", + "nav-enemy-rnd-int-range", + "light-time-state", + "group-run-poof-wood", + "draw-context", + "*camera-old-vu*", + "blerc-globals", + "vector3s-rotate*!", + "gs-blocks-used", + "ocean-near-constants", + "edge-grab", + "*kermit-sg*", + "push1", + "gs-xyz", + "initialize-eco-by-other", + "kill-not-name", + "texture-page-size-check", + "solve-missile-velocity", + "debug-menu-context-release-joypad", + "dgo-load-continue", + "adgif-shader-login-no-remap-fast", + "sp-relaunch-particle-2d", + "draw-context-set-xy", + "shadow-mask", + "bat-celebrate", + "*sp-60-hz*", + "sprite-distorter-sine-tables", + "camera-line2d", + "gif-p3cnt", + "set-period", + "rigid-body-platform-constants", + "vif-stcycl-imm", + "generic-initialize-without-sink", + "gs-clamp", + "bone", + "ambient-volume-movie", + "*display-file-info*", + "*sage-finalboss-sg*", + "lsmi-work", + "vector-z-quaternion!", + "psm-page-height", + "target-eco-powerup", + "gif-mode", + "*tie*", + "process-mask", + "coserp", + "caveelevator-joint-callback", + "file-stream-close", + "still", + "firecanyon-part", + "put-display-alpha-env", + "adgif-shader<-texture-simple!", + "ocean-mid-upload", + "spawn-minions", + "effect-just-poof", + "touching-prims-entry-pool", + "gs-trxdir", + "reset", + "update-mood-fog", + "part-tester-idle", + "blerc-block-header", + "generic-merc-execute-asm", + "shadow-add-facing-single-tris", + "target-hit-ground-hard", + "gif-cnt", + "disable-irq", + "village3", + "display-frame", + "update-task-hints", + "print-game-text", + "display-env", + "racer-calc-gravity", + "glst-remove-tail", + "*debug-offset*", + "draw-env", + "*ogre3-mood-fog-table*", + "group-slide-poof-sand", + "curve", + "ripple-for-villagea-water", + "vector-seek-2d-yz-smooth!", + "vector/!", + "clip-restore", + "*dproc*", + "lod-set", + "trsq->continue-point", + "*merc-global-array*", + "*display-quad-stats*", + "gorge-start-draw-time", + "gs-store-image-packet", + "vector-sin-rad!", + "proximity", + "vector-dot-vu", + "pivot", + "vector-vector-xz-distance", + "vector*!", + "*ocean-down-left-table*", + "quaternion-j!", + "flut-attack", + "vector-deg-lerp-clamp!", + "baby-spider-default-event-handler", + "cam-free-bank", + "sprite-vu1-block", + "volume-descriptor-array", + "bone-freehead", + "entity-by-meters", + "ocean-mid-mask", + "*fog-color*", + "vector-local+!", + "sunken-states", + "halfpipe-vis", + "rigid-body-control-point-inline-array", + "joint-mod-wheel-callback", + "file-info-correct-version?", + "cam-collision-record-step", + "cam-slave-get-rot", + "rot-zyx-from-vector!", + "cspace<-transformq+world-trans!", + "clmf-to-spline-attr", + "edge-grabbed", + "vector-seconds!", + "gs-rgbaq", + "*sound-bank-2*", + "vector-degi", + "clear-vu1-mem", + "othercam-calc", + "shell-down", + "merc-bucket-info", + "vector3s+!", + "joint-exploder-shatter", + "master-check-regions", + "vector-cvt.w.s!", + "clmf-to-focalpull-attr", + "group-land-poof-water", + "pal", + "root", + "cam-string-set-position-rel!", + "dma-send-to-spr-no-flush", + "shock-out", + "scf-time", + "sprite-add-2d-chunk", + "vector-degf", + "fisher-draw-display", + "prototype-bucket", + "*display-sidekick-stats*", + "dma-add-process-drawable", + "ecoventrock-break", + "render-eyes", + "quicksandlurker", + "drawable-actor", + "*display-edge-collision-marks*", + "collide-mesh-cache", + "charp<-string", + "*display-iop-info*", + "*oracle-village3-tasks*", + "burst-out", + "vector-xz-length-squared", + "mod-var-jump", + "type", + "*village2-mood-fog-table*", + "sound-set-volume", + "grunt", + "nmember", + "stopwatch", + "make-file-name", + "vector+float!", + "spheres-overlap?", + "ocean-trans-indices", + "move-camera-from-pad", + "debug-menu-context-render", + "vector16b", + "timeout", + "dma-buffer-inplace-new", + "patha", + "*stats-memory-short*", + "add-reg-gif-packet", + "*citadelcam-sg*", + "vector-*!", + "throw", + "energyball-idle", + "seek-with-smooth", + "*sky-work*", + "attack-info", + "*cam-collision-record*", + "sphere<-vector!", + "file-stream-length", + "rolling-start-whole", + "file-stream-read", + "matrix-lerp!", + "matrix-local->world", + "exit-chamber-button-init-by-other", + "ambient-type-error", + "load-dir", + "*depth-cue-base-vram-word*", + "silostep-idle", + "snow", + "falling", + "*default-regs-buffer*", + "static-screen", + "set-display-gs-state", + "distance-from-tangent", + "write", + "psm-size", + "attack-invinc", + "*ocean-base-block*", + "flop-forward", + "collide-do-primitives", + "art-joint-geo", + "bsp-header", + "target-flut-hit", + "quaternion-delta-y", + "texture-page", + "entity-links-array", + "*sky-base-vram-word*", + "angle-tracker", + "push", + "tie-near-float-reg", + "debug-init-buffer", + "malloc", + "texture-id", + "citb-sagecage", + "matrix-from-control-pair!", + "invalidate-cache-line", + "level-load-info", + "collide-edge-hold-item", + "cspace-children", + "*level*", + "part-tester", + "shadow-run", + "collide-planes-test0", + "nav-enemy-notice", + "vector4-lerp-clamp!", + "*citb-rotatebox-sg*", + "demo-shared", + "init-for-transform", + "*game-text-line*", + "nav-enemy-patrol-post", + "dump-vu1-range", + "art-part-name", + "ripple-wave-set", + "mc-check-result", + "splitb-roar", + "*debug-sphere-table*", + "integral?", + "target-falling-anim", + "crab-walk2", + "fog-corrector-setup", + "camera-master", + "nav-enemy-send-attack", + "plant-boss-vine-hit", + "*menu-hook*", + "quaternion-identity!", + "*display-ambient-ocean-off-marks*", + "ogre-vis", + "alt-actor", + "gs-find-block", + "fog-corrector", + "fact-bank", + "until", + "mistycannon-waiting-for-player", + "math-cam-start-smoothing", + "suspend", + "update-math-camera", + "training-states", + "joint-mod-spinner-callback", + "go-die", + "gif-stat", + "*font-default-matrix*", + "swamp-rat-nest-dummy-die", + "basic", + "sequenceC-can-trans-hook", + "aphid", + "green-eco-lurker", + "*display-target-marks*", + "sp-queue-launch", + "*profile-x*", + "*village1-mood-fog-table*", + "tiltAdjust", + "sunken-pipegame-begin-play", + "set-draw-env-offset", + "pelican-dive", + "level-group", + "dma-sqwc", + "target-billy-game", + "target-racing-center-anim", + "redshot-init-by-other", + "font-context", + "*test-ray-dest-pos*", + "ogreboss-bounce-boulder-idle", + "debug-draw-buffers", + "set-display-gs-state-offset", + "dma-from-spr", + "rem", + "screen-gradient", + "texture-pool-segment", + "race-ring-set-particle-rotation-callback", + "set-draw-env", + "vblank-handler", + "vector-sincos!", + "*run*", + "cage-bird-4", + "*font-context*", + "foreground-area", + "bike-cam-limit", + "yakow-generate-travel-vector", + "*vu0-dma-list*", + "bridge-breaks", + "vif1-handler-debug", + "dm-boolean-toggle-pick-func", + "5", + "target-collision-low-coverage", + "gs-packed-gt4", + "assembly-moves", + "draw-string-xy", + "shake", + "matrix<-no-trans-transformq!", + "*common-text*", + "end-draw", + "*debug-traverse*", + "target-timed-invulnerable-off", + "start", + "balloonlurker-event-handler", + "*progress-cheat*", + "matrix<-parented-transformq!", + "connectable", + "level-hint-sidekick", + "bouncing-float", + "entity-actor", + "texture-link", + "connection", + "spring-height", + "sphere<-vector+r!", + "*water-anim-sunken-dark-eco-helix-room-sg*", + "kheap", + "collide-probe-make-list", + "generic-tie-dma-to-spad-sync", + "get-height-over-navmesh!", + "land-hard", + "game-text", + "*nk-dead-pool*", + "setting-control", + "atan2-rad", + "gs-set-default-store-image", + "sound-bank-id", + "*load-boundary-target*", + "huf-dictionary-node", + "control-info", + "cam-state-from-entity", + "yes-follow", + "*pelican-sg*", + "curve-copy!", + "*shader-list*", + "texture-page-login", + "adgif-shader<-texture!", + "add-blue-motion", + "ramdisk-sync", + "prototype-generic-shrub", + "make-debug-sphere-table", + "target-no-move-post", + "texture-bpp", + "adgif-shader-login-fast", + "attack-event", + "*keg-conveyor-sg*", + "spline-follow-dist-offset", + "hud-fuel-cell", + "citadelcam-stair-plats", + "*texture-pool*", + "matrix-rotate-y!", + "shadow-add-double-tris", + "dma-buffer-add-ref-texture", + "scf-get-territory", + "rot-zxy-from-vector!", + "anim-tester-update-anim-info", + "link-texture-by-id", + "*ocean-mid-indices-sunken*", + "pending", + "*game-info*", + "part-tracker-process", + "texture-page-common-allocate", + "*sunkenb-mood*", + "sprite-hvdf-data", + "shrub-num-tris", + "stopwatch-stop", + "vector-delta", + "texture-qwc", + "gs-largest-block", + "setting-unset", + "cam-layout-save-campoints-flags", + "merc-vu1-init-buffers", + "bone-list-init", + "matrix-translate!", + "*display-ambient-dark-marks*", + "render-ocean-far", + "bouncer-wait", + "texture-relocate", + "closest-pt-in-triangle", + "vector-deg-diff", + "sparticle-system", + "forward-up-nopitch->inv-matrix", + "bones-init", + "attackable", + "vector-lerp!", + "redshot-explode", + "mz32-24-block-table", + "can-wheel?", + "art-element", + "texture-page-default-allocate", + "generic-effect-work", + "blerc-context", + "*identity-matrix*", + "gs-block-height", + "focalpull", + "joint-control-channel-group-eval!", + "start-perf-stat-collection", + "bones-mtx-calc-execute", + "pole", + "screen-filter", + "release", + "level-remap-texture", + "clm-basic", + "*instance-tie-work-copy*", + "*display-collide-cache*", + "compute-and-draw-shadow", + "*display-ambient-light-marks*", + "cam-point-watch", + "perf-stat-bucket->string", + "*debug-pause*", + "matrix-3x3-triple-transpose-product", + "draw-bones-check-longest-edge", + "joint-mod-set-local", + "*sagesail-sg*", + "*sp-launch-queue*", + "*training-cam-sg*", + "*camera-engine*", + "*flutflut-sg*", + "open-gif-packet", + "vector3s*float!", + "particle-setup-adgif", + "*stats-dma-test*", + "clmf-cam-string", + "part-tracker-init", + "*display-split-box-info*", + "*display-ground-stats*", + "*teleport-count*", + "assistant-lavatube-end", + "*sync-dma*", + "*display-entity-errors*", + "*display-perf-stats*", + "*display-level-spheres*", + "internal-draw-debug-text-3d", + "decompress-frame-data-to-accumulator", + "projectile-update-velocity-space-wars", + "kernel-write-function", + "burn", + "find-knot-span", + "spawn-flying-rock", + "update-vram-pages", + "ocean-near-add-call", + "cam-pov", + "debug-menu-func-decode", + "*display-cam-coll-marks*", + "shrubbery-matrix", + "*display-ambient-hint-marks*", + "sound-trans-convert", + "gs-page-height", + "blerc-block", + "find-temp-buffer", + "dist-info-valid?", + "*display-process-anim*", + "forward-up->quaternion", + "ambient-hint-spawn", + "*display-water-marks*", + "target-pole-flip-forward-jump", + "set-display", + "update-mood-firecanyon", + "update-mood-quick", + "yakow-idle", + "*stat-string-total*", + "*display-texture-download*", + "*artist-use-menu-subdiv*", + "stop-cloning", + "*clm-spline-attr*", + "balance-plat-idle", + "pov-camera-done-playing", + "nav-enemy-rnd-go-idle?", + "cam-vert", + "*record-cam-collide-history*", + "vector-x-quaternion!", + "assoc", + "*camera-no-mip-correction*", + "draw-ocean-transition", + "command-get-process", + "launch-jump", + "quicksandlurker-track", + "generic-tie-ips", + "jungleb-vis", + "ray-sphere-intersect", + "citb-robotboss", + "babak-win", + "*display-hipri-collision-marks*", + "*col-timer-enable*", + "cyclegen", + "*seagull-boxes*", + "ecoventrock-idle", + "nassoc", + "ogreboss-stage3-throw", + "instance-tfragment-add-debug-sphere", + "*ocean-trans-down-table*", + "get-heap", + "float-lookup-timeplot", + "*stats-buffer*", + "new-sound-id", + "shadow-add-single-edges", + "got-cell?", + "shadow-dma-init", + "shadow-add-verts", + "*robocave-mood-fog-table*", + "shadow-stats", + "windturbine", + "shadow-scissor-top", + "*clm*", + "shadow-vu1-init-buffer", + "cam-bike-code", + "vector-identity!", + "tie-near-make-perspective-matrix", + "process-taskable-play-anim-exit", + "*shadow-vu1-tri-template*", + "shadow-vu1-add-constants", + "shadow-vu1-add-matrix", + "shadow-vu1-block", + "depth-cue-draw-depth", + "light-eco-mother-discipate", + "*depth-cue-work*", + "blimp-trans", + "list-sounds", + "part-progress-card-slot-01-func", + "depth-cue", + "depth-cue-set-stencil", + "*debug-nav-travel*", + "draw-string-adv", + "gs-bank", + "group-just-footprint-gravel", + "*font12-table*", + "swamp-rat-nest-pick-spawn-joint", + "background-vu0-block", + "snow-ball-roller-path-init", + "target-yellow-blast", + "basic-type?", + "*background-work*", + "background-upload-vu0", + "quaternion-set!", + "finish-background", + "calculate-rotation-and-color-for-slice", + "cam-combiner-init", + "draw-drawable-tree-ice-tfrag", + "*graphic-options*", + "*jungleb-mood-lights-table*", + "dynamics", + "draw-drawable-tree-instance-shrub", + "*color-light-magenta*", + "time-of-day-interp-colors-scratch", + "*instance-shrub-work*", + "time-of-day-interp-colors", + "at-save-sequences", + "draw-drawable-tree-trans-tfrag", + "*part-id-table*", + "lurkerpuppy", + "draw-node-cull", + "collide-cache-using-y-probe-test", + "joint-anim-compressed-fixed", + "*collide-edge-work*", + "collide-cache-using-line-sphere-test", + "sparticle-kill-it-level0", + "double-lurker-top", + "collide-cache-using-box-test", + "int8", + "mem-usage-shrub-walk", + "draw-prototype-inline-array-shrub", + "cam-bike-bank", + "ogre-isle-c", + "*task-egg-starting-x*", + "*lurker-army*", + "shrub-time", + "*vu1-enable-user-menu*", + "shrub-vu1-block", + "shrub-do-init-frame", + "racing", + "int-var", + "draw-inline-array-instance-shrub", + "test-func", + "mother-spider-idle", + "vector+float*!", + "shrub-upload-test", + "matrix->eul", + "springbox", + "looping-code", + "tfrag-details", + "clmf-to-edit", + "tnear-vu1-block", + "tfrag-near-end-buffer", + "interesting-offset", + "dm-time-of-day-func2", + "power-left", + "t-stat", + "fishermans-boat-leave-dock?", + "tfrag-data-setup", + "command-get-quoted-param", + "debug-menu-node", + "lb-del-vtx", + "tfrag-near-init-buffer", + "sparticle-seagull-moon", + "move-target-from-pad", + "tfrag-vu1-block", + "lurkbat-wing", + "cam-layout-print", + "tfrag-print-stats", + "add-tfrag-data", + "basebutton-down-idle", + "copy-load-boundary!", + "edge-debug-lines", + "tie-ints", + "tie-floats", + "cavecrystal-idle", + "*edit-instance*", + "light-volume-planes", + "tie-consts", + "ram-idle", + "ice-monster4", + "ripple-execute", + "cam-layout-restart", + "tie-near-end-buffer", + "ecoventrock", + "tie-near-int-reg", + "quicksandlurker-yawn", + "racer-effect", + "tie-near-consts", + "sky-set-orbit", + "double-lurker-break-apart", + "tie-near-init-engine", + "plant-boss-idle", + "sp-rotate-system", + "*prototype-tie-work*", + "tie-test-cam-restore", + "cspace-by-name", + "glst-end-of-list?", + "tie-init-buffers", + "draw-inline-array-instance-tie", + "spiderwebs-bounce", + "enable", + "sky-color-hour", + "draw-inline-array-prototype-tie-generic-asm", + "tie-debug-one", + "*kernel-sp*", + "draw-inline-array-prototype-tie-asm", + "anim-speed", + "sun-iris-door-open", + "num-func-+!", + "*flash5*", + "rand-vu", + "tie-instance-debug", + "cam-layout-save-interesting", + "*part-group-id-table*", + "inspect", + "part-spawner", + "lookup-part-group-pointer-by-name", + "generic-envmap-consts", + "*ice-surface*", + "sp-launch-particles-death", + "mother-spider-egg-init-by-other", + "unlink-part-group-by-heap", + "cam-layout-entity-volume-info", + "sp-adjust-launch", + "snow-ball-shadow", + "nav-ray-test", + "sp-clear-queue", + "ja-aframe-num", + "billy-rat-salivate", + "sparticle-track-root-prim", + "*particle-adgif-cache*", + "*clm-save-one*", + "sp-launch-particles-var", + "gs-dthe", + "sparticle-birthinfo", + "*plat-sg*", + "*color-dark-yellow*", + "*citb-robotboss-leftarm-sg*", + "draw-percent-bar", + "birth-func-random-next-time", + "cavecrystal", + "*particle-300hz-timer*", + "group-run-poof-dirt", + "uint128", + "engine", + "sp-euler-convert", + "analyze-point-on-path-segment", + "*cam-debug-los-tri-current*", + "lookup-part-group-by-name", + "sp-relaunch-particle-3d", + "birth-func-copy2-rot-color", + "add-debug-text-sphere", + "sp-init-fields!", + "tetherrock-info", + "irisdoor2", + "sparticle-track-root", + "kill-all-particles-with-key", + "ceilingflag", + "unpack-comp-huf", + "target-start-attack", + "sparticle-kill-it-level1", + "end-perf-stat-collection", + "sp-particle-copy!", + "debug-menu-item-var-render", + "villagec-part", + "sp-copy-from-spr", + "draw-bones-mtx-calc", + "matrix-rotate-yxz!", + "forall-particles-with-key", + "sp-get-block-size", + "boulder4-trans-3", + "cam-layout-intersect-dist", + "*lavafallsewerb-sg*", + "drop-plat-init-by-other", + "*lrocklrg-sg*", + "angle-tracker-apply-move!", + "set-particle-frame-time", + "lb-add-vtx-after", + "drawable-inline-array", + "all-particles-60-to-50", + "dril-step", + "probe-traverse-draw-node", + "swamp-rat-nest-dummy-shake", + "pickup-radius", + "dm-compact-actor-pick-func", + "forall-particles-with-key-runner", + "end-mode", + "debug-menu-send-msg", + "effect-land-poof", + "kill-all-particles-in-level", + "process-particles", + "*edge-vert0-table*", + "sparticle-50-to-60", + "*snow-ball-shadow-control*", + "play", + "draw-instance-info", + "sp-orbiter", + "sunken", + "sp-free-particle", + "*particles-flag*", + "*assistant-village2-tasks*", + "all-particles-50-to-60", + "sp-kill-particle", + "sp-process-block", + "wheel-solid", + "*citb-robotboss-rightshoulder-sg*", + "num-func-identity", + "*entity-info*", + "target-racing-smack", + "*forward-jump-mods*", + "money", + "assistant-levitator", + "buzzer", + "sage-finalboss", + "cam-layout-save-maxAngle", + "ogreboss-attack-event-handler", + "target-start", + "racer", + "robotboss", + "rat-about-to-eat?", + "part-hud-racer-speed-func", + "pickup-spawner", + "kermit-speak2", + "barrel", + "ja-abort-spooled-anim", + "build-instance-list", + "art-group-load-check", + "check-drop-level-village1-fountain", + "*art-control*", + "ja-play-spooled-anim", + "darkecobomb", + "*evilbro-citadel-sg*", + "citadel-vis", + "vertical-planes", + "glst-node-name", + "*preload-spool-anims*", + "drawable-load", + "generic-wrapup", + "ja-channel-set!", + "ja-channel-push!", + "junglesnake-wake", + "die", + "loop", + "mc-unformat", + "kill-current-level-hint", + "entity", + "*debug-hook*", + "*terrain-stats*", + "process", + "loaded", + "swamp-tetherrock-idle", + "*stack-top*", + "*display-sprite-info*", + "*load-state*", + "process-taskable", + "task-control", + "*merc-death-globals*", + "periscope", + "sp-get-particle", + "load", + "go-throw", + "ja-eval", + "game-task", + "allow-pause", + "reset-collide", + "dark-plant-gone", + "get-game-count", + "collide-los-result", + "dma-send-to-spr", + "set-letterbox-frames", + "eco-blue", + "quaternion-vector-len", + "target-swim-post", + "try", + "ambient-volume", + "*rolling-mood-lights-table*", + "*breakaway-left-sg*", + "vent", + "render-ocean-quad", + "*starfish-sg*", + "reset-all-hint-controls", + "*level-task-data-remap*", + "touch-tracker-init", + "*level-load-list*", + "*spawn-actors*", + "*balloonlurker-sg*", + "log2", + "set-master-mode", + "cam-debug-add-coll-tri", + "ogre-roar3", + "*flut-air-attack-mods*", + "set-blackout-frames", + "group-just-poof-dirt", + "get-task-control", + "*kernel-boot-message*", + "stop", + "group-just-footprint-swamp", + "auto-save-check", + "ogreboss-missile-init-by-other", + "auto-save", + "aspect16x9", + "*camera-dead-pool*", + "*default-nav-mesh*", + "*auto-save-info*", + "*village2-mood-sun-table*", + "shrub-upload-view-data", + "game-save", + "auto-save-post", + "mc-load", + "glst-get-node-by-index", + "lookup-level-info", + "done", + "blue-sagecage", + "get-aspect-ratio", + "life", + "dist-info-print", + "progress-allowed?", + "*collide-nodes*", + "*lb-editor-parms*", + "*level-task-data*", + "aspect4x3", + "format-card", + "ray-cylinder-intersect", + "sparticle-launch-state", + "training", + "mc-format", + "wedge-plat-idle", + "get-task-status", + "*swamp-blimp-sg*", + "create-file", + "notify", + "tit", + "ja-frame-num", + "lb-set-camera", + "*generic-effect-mode*", + "unformat-card", + "blerc-a-fragment", + "*darkcave-mood*", + "mc-create-file", + "mc-save", + "tube-info", + "scf-get-time", + "*font-texture*", + "calculate-completion", + "citb-firehose-blast", + "sky-cloud-polygon-indices", + "scf-get-aspect", + "dma-ctrl", + "*boot-video-mode*", + "char-color", + "language", + "*display-ambient-poi-marks*", + "talking", + "go-hit", + "hint", + "change-state", + "set-video-mode", + "send-event-function", + "near", + "rel", + "transform-matrix-calc!", + "music", + "water-anim", + "babak-dies", + "video-parms", + "common-page", + "*debug-nav-ray*", + "dialog-volume-hint", + "swamp-rat-nest-dummy-b", + "cull-info", + "sopt-work", + "movie", + "bg-a-force", + "splita-charge", + "bg-g", + "master-draw-coordinates", + "border-mode", + "plant-boss-back-arms-init", + "vibration", + "ambient", + "copy-string<-string", + "clm", + "play-hints", + "gorge-in-front", + "los-dist", + "music-volume-movie", + "draw-drawable-tree-dirt-tfrag", + "*bone-calculation-list*", + "set-aspect-ratio", + "robotboss-blue-done", + "*rolling-mood*", + "*time-of-day-mode*", + "*ogre-mood-fog-table*", + "*citadel-mood-sun-table*", + "sunken-elevator", + "make-village2-light-kit", + "matrix-remove-z-rot", + "far", + "*dm-cam-mode-interpolation*", + "*sunken-mood-lights-table*", + "*training-mood-fog-table*", + "bucket", + "*finalboss-interp-table*", + "target-print-stats", + "training-water", + "*firecanyon-mood-fog-table*", + "cam-debug-draw-tris", + "shrub-view-data", + "int16", + "*jungleb-mood-sun-table*", + "*village1-mood-sun-table*", + "lookup-texture-by-id", + "*lavatube-mood-sun-table*", + "*village1-mood-lights-table*", + "collide-planes-intersect", + "*cavecrystal-sg*", + "*misty-mood-fog-table*", + "*ocean-down-table*", + "*darkcave-mood-lights-table*", + "*maincave-mood-lights-table*", + "*jungleb-mood*", + "*default-interp-table*", + "mode", + "*village1-palette-interp-table*", + "target-edge-grab-jump", + "make-village3-light-kit", + "*default-mood*", + "*snow-mood*", + "*time-of-day-effects*", + "update-mood-erase-color", + "ventblue", + "make-village1-light-kit", + "nav-enemy-jump-land-post", + "flutflut-plat-small", + "reset-drawable-tracking", + "*ocean-trans-up-table*", + "*finalboss-mood*", + "default-level", + "vector-xz-length-max!", + "x", + "swamp-bat-slave-return", + "update-mood-erase-color2", + "nav-enemy-facing-point?", + "*snow-mood-lights-table*", + "*maincave-mood*", + "pusher-post", + "update-mood-sunken", + "ocean-mid-add-upload-bottom", + "*village3-mood*", + "*village3-mood-lights-table*", + "*finalboss2-mood*", + "start-cloning", + "*rolling-palette-interp-table*", + "*ogre-mood*", + "*lavatube-mood-lights-table*", + "update-mood-shadow-direction", + "vector-rotate*!", + "fuel-cell-animate", + "make-light-kit", + "debug-menu-context", + "splita-dies", + "seagull-flying", + "*shader-pick-menu*", + "normal", + "*swamp-mood*", + "main-cheats", + "projectile-moving", + "*firecanyon-mood*", + "hide-quick", + "*lavatube-mood-fog-table*", + "exit-chamber-idle-in-sunken", + "*ogre-mood-lights-table*", + "yakow", + "*water-anim-sunken-dark-eco-qbert-sg*", + "assistant-levitator-blue-beam", + "*citb-arm-shoulder-a-sg*", + "*rolling-mood-fog-table*", + "ambient-area", + "*flash0*", + "generic-envmap-only-proc", + "ogre-states", + "process-taskable-anim-loop", + "*seagull-sg*", + "update-mood-robocave", + "texture-level", + "group-just-poof-ice", + "*flash4*", + "bustarock", + "debug-menu-item-flag-msg", + "debug-line", + "dm-task-hint", + "robocave-states", + "ocean-trans-strip-array", + "update-mood-rolling", + "*rolling-spheres-on*", + "lb-add-plane", + "move-to", + "trigger", + "lightning-state", + "*stdcon0*", + "*flash3*", + "citadel-states", + "maincave-states", + "update-mood-flames", + "set", + "update-light-kit", + "progress-waiting", + "stream<-process-mask", + "clear-mood-times", + "group-just-poof-water", + "steam-large", + "bonelurker-stunned-event-handler", + "*current-ramdisk-id*", + "update-mood-misty", + "render-boundary", + "*rolling-spheres-light3*", + "update-mood-ogre", + "reflector", + "normalize-frame-quaternions", + "cam-layout-init", + "*thunder-id*", + "count", + "target-startup", + "*flash2*", + "update-mood-jungle", + "time-of-day-update", + "lava-state", + "update-mood-sky-texture", + "update-mood-training", + "update-mood-palette", + "update-mood-swamp", + "add-thrust", + "update-mood-lava", + "gs-bgcolor", + "grab", + "*fp-hud-stack*", + "rpc-busy?", + "update-mood-jungleb", + "update-mood-light", + "babak-with-cannon-jump-onto-cannon", + "*lightning-time*", + "*lightning-realtime-done*", + "pat-mode-info", + "*flash6*", + "matrix-4x4-inverse-transpose!", + "title-vis", + "camera-line-draw", + "float-var", + "gm-shadow", + "*thunder-id0*", + "matrix3", + "*village1-mood*", + "*flash1*", + "debug-menu-item-get-max-width", + "group-ogreboss-column-break", + "swamp-village2-states", + "intro-k", + "collide-probe-instance-tie", + "all-texture-tweak-adjust", + "robotboss-red", + "punch-hit", + "*water-anim-maincave-mid-right-pool-sg*", + "acos", + "lb-set-player", + "update-mood-caustics", + "splita-roar", + "seaweed", + "firecanyon-states", + "change-to-last-brother", + "village1-states", + "snow-bunny-retreat-work", + "anim-test-obj", + "set-target-light-index", + "fov", + "*particle-quat*", + "village3-states", + "misty-states", + "*mayorgears-sg*", + "shadow-header", + "draw-ocean-transition-seams", + "light-state", + "group-red-eco-strike-ground", + "target-continue", + "update-snow", + "drop-plat-spawn", + "check-drop-level-rain", + "glst-set-name!", + "cam-master-effect", + "keg-bounce-set-particle-rotation-callback", + "*village2-mood*", + "simple-collision-reaction", + "babak-chest", + "sprite-draw", + "ray-line-segment-intersection?", + "sparticle-track-sun", + "time-of-day-effect", + "set-time-of-day", + "citb-arm-shoulder-a", + "update-time-of-day", + "enter", + "nav-enemy-simple-post", + "sky-base-polygons", + "path5-k", + "*lurkercrab-nav-enemy-info*", + "play-mode", + "make-sky-textures", + "show-iop-memory", + "sky-set-sun-colors-halo", + "death-warp-out", + "sky-set-sun-colors-aurora", + "sky-make-sun-data", + "sky-vu1-block", + "sky-init-upload-data", + "sky-upload", + "*display-tri-stats*", + "ambient-type-sound-loop", + "sky-draw", + "init-time-of-day", + "sky-make-moon-data", + "level-hint-process-cmd", + "misty-vis", + "*camera-smush-control*", + "render-sky-tng", + "init-sky-tng-data", + "sky-cloud-polygons", + "sky-tng-setup-cloud-layer", + "light-group-process!", + "nav-mesh-update-route-table", + "copy-cloud-texture", + "swamp-rat-nest-dummy-hit", + "deg-diff", + "*color-red*", + "clip-polygon-against-positive-hyperplane", + "cloud-track", + "energybase-stopping", + "joint-exploder-static-params", + "sky-duplicate-polys", + "*weather-off*", + "clmf-cam-intro-time", + "inc-angle", + "sky-tng-setup-clouds", + "init-sky-regs", + "ram-boss-on-ground-event-handler", + "render-sky-tri", + "ram-boss-jump-down", + "*crate-darkeco-sg*", + "debug-menu-make-camera-menu", + "copy-sky-texture", + "sign", + "*clm-edit*", + "close-sky-buffer", + "*rotate-surface*", + "*snow-eggtop-sg*", + "draw-large-polygon", + "vector-negate-in-place!", + "*sky-tng-data*", + "look-around", + "var", + "*load-boundary-list*", + "*smack-up-mods*", + "load-boundary-crossing-command", + "walk-dwater2", + "*display-cam-los-debug*", + "edit-load-boundaries", + "fix-boundary-normals", + "save-boundary-cmd", + "light-slerp", + "add-boundary-shader", + "*med-res-village2-sg*", + "vol-flags", + "nav-enemy-fall-and-play-death-anim", + "clmf-save-all", + "ray-ccw-line-segment-intersection?", + "draw-boundary-polygon", + "cam-slave-get-flags", + "*shover*", + "suspended", + "target-grab", + "---lb-save", + "training-vis", + "*racer-shadow-control*", + "format-boundary-cmd", + "point-in-polygon", + "baby-spider-init-by-other", + "update-mood-jungleb-blue", + "copy-load-command!", + "nav-enemy-initialize-custom-jump", + "render-boundaries", + "fishermans-boat-ride-to-misty", + "draw-boundary-cap", + "birth-func-ground-orient", + "*PELICAN-bank*", + "load-state-want-display-level", + "clmf-next-volume", + "lb-del", + "render-boundary-quad", + "timer-hold-bank", + "lb-flip", + "*evilsis-village3-sg*", + "generic-tie-display-stats", + "lb-editor-parms", + "citb-drop-plat-spawn-children", + "target-cam-pos", + "*triangulation-buffer*", + "*standard-dynamics*", + "check-open-boundary", + "yakow-facing-direction?", + "*display-load-commands*", + "*precurbridge-sg*", + "transform-point-vector!", + "*redline-index*", + "stringCliffHeight", + "ventred", + "render-boundary-tri", + "battlecontroller-default-event-handler", + "decompress-frame-data-pair-to-accumulator", + "command-list-get-process", + "*ocean-near-indices-sunken*", + "find-bounding-circle", + "check-closed-boundary", + "debug-menu-item-var-msg", + "max-frame", + "set-tex-offset", + "group-slide-poof-tar", + "lb-add", + "play-accept", + "command-get-param", + "draw-boundary-side", + "windspinner-idle", + "*profile-ticks*", + "debug-menu-make-task-unknown-menu", + "robocave-part", + "jungle-shores", + "walk-step-left", + "*backup-load-state*", + "load-state-want-force-vis", + "debug-menu-item-function-render", + "generic-prepare-dma-double", + "split-monotone-polygon", + "shadow", + "special", + "vector+!", + "ground-height", + "vector2h", + "quote", + "teleport", + "billy-rat-init-by-other", + "shadow-dma-packet", + "deg", + "want-force-vis", + "walk-water2", + "want-levels", + "ogreboss-set-stage2-camera", + "kill", + "time-of-day", + "cspace<-matrix-joint!", + "*static-load-boundary-list*", + "jub", + "clear-tr-stat", + "rob", + "sno", + "sub", + "rol", + "ogreboss-village2-idle", + "misty-battlecontroller", + "*ocean-right-table*", + "darkcave", + "cspace-calc-total-matrix!", + "bea", + "vi3", + "moving-sphere-sphere-intersect", + "display-no-wait", + "*swamp-rat-nest-dummy-c-sg*", + "jun", + "special-vis", + "title", + "*thunder-id2*", + "*water-anim-finalboss-dark-eco-pool-sg*", + "intro", + "*windmill-sail-sg*", + "none", + "*sunkencam-sg*", + "robocave-vis", + "default-level-vis", + "lavatube-vis", + "maincave-vis", + "cam-stick-bank", + "fly6", + "*camera-old-tfrag-bytes*", + "beach-vis", + "quaternion-normalize!", + "*firecanyon-mood-lights-table*", + "slave-activated", + "instance-sphere-box-intersect?", + "sunkenb-vis", + "*clm-intro-attr*", + "wall-plat-extended", + "part-first-person-hud-selector-func", + "*ocean-trans-down-right-table*", + "mis", + "notice", + "*debug-text-3d-trk*", + "village3-vis", + "fleeing-nav-enemy-clip-travel", + "int", + "deadly", + "starfish-init-by-other", + "light-eco-child-default-event-handler", + "fuel-cell-progress-hud-orbit-callback", + "*caveelevator-sg*", + "cam-slave-get-vector-with-offset", + "dem", + "periscope-idle", + "*ambient-spec*", + "build-conversions", + "surface-interp!", + "sky-set-sun-colors", + "sunken-vis", + "darkcave-vis", + "intro-vis", + "show-level", + "load-vis-info", + "cam-standard-event-handler", + "*jungle-mood*", + "level-update-after-load", + "fly5", + "*clip-for-spheres-timer*", + "snow-vis", + "update-sound-banks", + "remap-level-name", + "add-bsp-drawable", + "ocean-mid-add-call-flush", + "bones-reset-sqwc", + "link-resume", + "level-hint-spawn", + "display-self", + "collide-player-list", + "login", + "loading-bt", + "debug-boot", + "on", + "pickup", + "enable-level-text-file-loading", + "text-is-loading", + "keg-conveyor-paddle-idle", + "load-game-text-info", + "previous-brother", + "group-just-footprint-metal", + "group-just-poof-snow", + "load-level-text-files", + "*level-text-file-load-flag*", + "print-game-text-scaled", + "citb-arm-c", + "probe-traverse-inline-array-node", + "part-spawner-active", + "collide-probe-instance-tie-collide-frags", + "ja-speed", + "target-hit-ground", + "rounddoor", + "*citadel-mood-fog-table*", + "fic", + "dm-instance-pick-func", + "local-pad-angle", + "probe-traverse-collide-fragment", + "flying-lurker-clone", + "*ocean-trans-left-table*", + "pke-collide-test", + "misty-ambush-height", + "roll", + "collide-probe-collide-fragment-tree-make-list", + "progress-level-index->string", + "forall-particles-runner", + "creates-new-method?", + "swamp-rat-update-wiggle-target", + "collide-probe-instance-tie-tree-make-list", + "update-mood-citadel", + "collide-probe-stack-elem", + "uint64", + "add-prims-touching-work", + "cam-layout-save-flags", + "collide-probe-node", + "light-eco-child-idle", + "*shadow-queue*", + "print-out", + "distc", + "*boat-throttle-control-table*", + "bounce", + "*lightning-index*", + "check-blue-suck", + "overrides-parent-method?", + "sphere-cull", + "letterbox", + "pbhp-stack-vars", + "*fuel-cell-sg*", + "change-target", + "debug-menu-context-send-msg", + "debug-report-col-stats", + "find-ground-point", + "save", + "*frame-timer*", + "draw-drawable-tree-instance-tie", + "*col-timer*", + "set-string-parms", + "sound-name-with-material", + "display-frame-start", + "default-collision-reaction", + "cam-string-line-of-sight", + "shove", + "collide-mesh-group", + "*color-light-green*", + "debug-menu-item-var-joypad-handler", + "quaternion-validate", + "ocean-texture-add-call-rest", + "deadlyup", + "vector-for-ambient", + "up", + "birth-pickup-at-point", + "part-hud-health-02-func", + "ridden", + "ice-monster1", + "camera-orbit-info", + "camera-cross", + "caveelevator-cycle-active", + "process-drawable-burn-effect", + "at-pick-joint-anim", + "death-warp-in", + "group-just-footprint-water", + "death-info", + "death-default", + "death-beach-puppy", + "ripple-for-rolling-water", + "death-jungle-snake", + "matrix<-transformq!", + "hide-bottom-hud", + "fireboulder-disable-blocking-collision", + "load-state-want-vis", + "temp-enemy-die", + "glst-add-tail", + "water-control", + "*darkecocan-sg*", + "parameter-ease-clamp", + "dma-tag", + "cam-slave-get-float", + "target-timed-invulnerable", + "fourth-power", + "*snow-switch-sg*", + "undecided", + "cam-slave-init", + "vector-into-frustum-nosmooth!", + "ogre-step", + "merc-eye-ctrl", + "main-debug-hook", + "check-drop-level-firehose-pops", + "cam-slave-get-interp-time", + "stringCliffHeight-offset", + "vector+*!", + "parameter-ease-none", + "parameter-ease-sqr-clamp", + "matrix-from-two-vectors-max-angle-partial!", + "handle", + "process-drawable-delay-player", + "target-flut-clone-anim", + "*sky-base-page*", + "parameter-ease-sin-clamp", + "vector-segment-distance-point!", + "cam-debug-tri", + "slave-matrix-blend-2", + "matrix-axis-sin-cos!", + "cam-slave-get-intro-step", + "citb-donut", + "cam-curve-setup", + "*dma-timeout-hook*", + "cam-slave-init-vars", + "cam-slave-go", + "update-mood-darkcave", + "parameter-ease-lerp-clamp", + "cam-standoff-read-entity", + "cam-spline", + "basebutton-init-by-other", + "change-state-no-go", + "target-swim-up", + "*wedge-plat-sg*", + "precurbridge-idle", + "*camera-base-mode*", + "reset-display-gs-state", + "campath", + "-off", + "camera-slave-debug", + "cam-fixed-read-entity", + "orbit-plat-riding", + "interpTime", + "mistycannon-collision-reaction", + "cam-voicebox", + "fly-away", + "campath-k", + "-on", + "stringMaxLength", + "task", + "game-text-info", + "align", + "can-play-stance-amibent?", + "DISP_LIST-bank", + "*camera-dummy-vector*", + "gs-packed-xyzw", + "state", + "matrix-world->local", + "vector-circle-tangent-new", + "collide-vu0-block", + "while", + "camera-angle", + "in-cam-entity-volume?", + "cam-master-init", + "robotboss-is-red-hit", + "*water-anim-misty-mud-by-arena-sg*", + "reset-follow", + "spawn-quicksandlurker-missile", + "check-drop-level-training-spout-rain", + "master-base-region", + "nav-enemy-death-post", + "setup-slave-for-hopefull", + "get-card", + "reset-drawable-follow", + "target-compute-edge", + "flags", + "*plant-boss-root-sg*", + "teleport-to-other-start-string", + "gs-store-image", + "toggle-slave-option", + "no-follow", + "sparticle-60-to-50", + "*ocean-wave-frames*", + "dma-count-until-done", + "pole-grab", + "set-slave-option", + "query-fov", + "intro-done?", + "*DISP_LIST-bank*", + "pvol", + "cam-layout-entity-info", + "target-duck-high-jump-jump", + "reset-root", + "ram-boss-throw", + "cam-billy-bank", + "stringMaxHeight", + "gear-creak", + "stringMinHeight", + "*snow-ball-shadow-sg*", + "last-ring?", + "cam-string", + "ocean-mid-add-constants", + "starts", + "cutoutvol", + "change-target-bone", + "*water-anim-training-lake-sg*", + "*village2-sky-texture-table*", + "set-interpolation", + "*citb-drop-plat-green-sg*", + "group-run-poof-straw", + "stop-tracking", + "*lightning-time2*", + "cam-stick", + "target-warp-in", + "slide", + "terrain-bsp", + "dist-from-interp-dest", + "cam-free-floating", + "change-to-entity-by-name", + "cam-billy", + "clmf-cam-deg", + "ambient-hint-init-by-other", + "glst-num-elements", + "ocean-mid-add-upload-top", + "vol", + "name", + "drawable-inline-array-collide-fragment", + "clear-slave-option", + "cam-dist-analog-input", + "*sage-bluehut-tasks*", + "search-info", + "cam-draw-collide-cache", + "matrix-rotate-z!", + "cam-circular-position", + "dist-info-append", + "cam-string-find-hidden", + "camera-tracker-init", + "walk-water1", + "ripple-globals", + "build-shader-list", + "cam-string-joystick", + "collide-work", + "*display-geo-marks*", + "cam-free-floating-input", + "tie-init-engine", + "start-tracking", + "cam-lookat", + "oracle", + "cam-string-move", + "focalpull-flags-on", + "ocean-texture-add-call-done", + "inspect-bsp-tree", + "update-mood-village1", + "*CAM_STICK-bank*", + "cam-los-setup-lateral", + "anim-tester-disp-frame-num", + "*cam-res-string*", + "*CAM_BILLY-bank*", + "ripple-for-ogre-lava", + "circle", + "cam-string-follow", + "pelican-path-update", + "collide-los-dist-info", + "cam-bike", + "cam-pov-track", + "cam-calc-bike-follow!", + "cam-circular-position-into-max-angle", + "scale-factor", + "*CAM_EYE-bank*", + "rigid-body", + "*CAM_STRING-bank*", + "cam-circular-code", + "ocean-mid-camera-masks-set!", + "cam-los-collide", + "*sidekick*", + "cam-standoff-calc-trans", + "energyhub-trans", + "send-event", + "camera-line-setup", + "set-language", + "hand-grab", + "cam-pov180", + "lightning-mole-gone", + "cam-debug-reset-coll-tri", + "slave-los-state->string", + "outro-done", + "group-just-poof-dpsnow", + "fleeing-nav-enemy", + "blocked-side?", + "cam-collision-record-save", + "no-hit", + "progress-global-state", + "ambient-control", + "*CAM_ORBIT-bank*", + "*CAM_FREE-bank*", + "cam-orbit", + "current", + "cam-point-watch-bank", + "*med-res-finalboss-sg*", + "*med-res-training-sg*", + "*citb-sagecage-sg*", + "cspace-array", + "*CAM_POINT_WATCH-bank*", + "cam-combiner-active", + "texture-page-near-allocate-0", + "*start-timer*", + "*save-camera-inv-rot*", + "*dark-crystal-exploder-params*", + "plane-from-points", + "*generic-tie*", + "update-camera", + "update-view-planes", + "babak-with-cannon-ride-cannon-post", + "vu-lights<-light-group!", + "village2-vis", + "*update-leaf-when-outside-bsp*", + "target-death-anim", + "generic-debug-light-proc", + "pad-0", + "vol-control", + "plane-volume", + "shadow-min-max", + "cam-layout-do-menu", + "clmf-save-one", + "kermit-set-nav-mesh-target", + "clmf-cam-fov", + "target-collision-no-reaction", + "cam-layout-save-align", + "cam-layout-bank", + "collide-using-spheres-params", + "*volume-point*", + "othercam-running", + "clmf-look-through", + "*cavetrapdoor-sg*", + "time-of-day-palette", + "bfloat", + "*volume-point-current*", + "turn-around?", + "drop-plat-die", + "anim-tester-bank", + "citb-robotboss-die", + "cam-layout-start", + "cam-layout-save-tiltAdjust", + "clmf-cam-flag-toggle-info", + "joint-anim-frame", + "*clm-cam-lookthrough*", + "cam-layout-function-call", + "cam-layout", + "enable-hud", + "clmf-next-vol-dpad", + "snow-button-activate", + "*clm-cam-attr*", + "path3", + "*death-adgif*", + "teleport-to-vector-start-string", + "cam-layout-save-stringMinLength", + "cam-layout-do-action", + "*clm-vol-attr*", + "flash", + "*run-time-assert-enable*", + "fisher-fish-fall", + "clmf-implement", + "cam-stop", + "fov->maya", + "cam-layout-save-introsplinetime", + "cam-layout-save-focalpull-flags", + "bird-lady", + "cam-layout-save-stringCliffHeight", + "battlecontroller-play-intro-camera", + "cspace<-cspace+quaternion!", + "clmf-bna", + "robber", + "command-get-int", + "clmf-input", + "orb-cache-top-activate", + "*water-anim-village1-rice-paddy-top-sg*", + "go", + "*clm-select*", + "cam-layout-save-stringMinHeight", + "generic-vu0-block", + "clmf-cam-interp-time", + "*viewer*", + "tra-iris-door", + "clmf-to-intro-attr", + "bg-a-speed", + "cam-layout-save-stringMaxHeight", + "surface", + "cam-layout-save-campointsoffset", + "path4", + "fisher-fish-move", + "add-debug-spheres", + "*smack-mods*", + "cam-layout-save-introsplineexitval", + "bonelurker", + "update-mood-lavatube", + "interp-test-deg", + "anim-test-seq-item", + "clmf-cam-flag-toggle", + "cam-layout-save-cam-trans", + "vector-rad<-vector-deg/2!", + "clmf-button-test", + "blob-land", + "cam-layout-save-spline-follow-dist-offset", + "balloonlurker-bank", + "*ocean-mid-masks-sunken*", + "clmf-pos-rot", + "vector-angle<-quaternion!", + "clm-item-action", + "interp-test-info", + "sincos-table", + "*last-cur-entity*", + "string-skip-to-char", + "clmf-next-entity", + "depth-cue-draw-front", + "index", + "villagea-part", + "campoints-offset", + "camera-fov-frame", + "cat-string<-string", + "vector-cvt.s.w!", + "final-door", + "spline", + "tie-int-reg", + "stringMinLength-offset", + "group-land-poof-straw", + "cam-slave-options->string", + "stringMinHeight-offset", + "dark-plant-check-target", + "change-mode", + "intro-time-offset", + "egg-crack", + "fov-offset", + "group-run-poof-sand", + "dm-task-unknown", + "*collide-list*", + "*finalboss-mood-sun-table*", + "right", + "triangle", + "intro-exitValue-offset", + "*citadel-mood*", + "pivot-offset", + "cam-index-options->string", + "tiltAdjust-offset", + "not", + "yeti-slave-show-anims", + "r2", + "down", + "*profile-y*", + "square", + "target-land-effect", + "*display-camera-last-attacker*", + "left", + "stringMaxHeight-offset", + "play1", + "wait-for", + "*game-options-japan*", + "energyball", + "pvol-flags", + "draw-bones-merc", + "foreground-engine-execute", + "effect-droppings", + "cam-collision-record-array", + "cam-collision-record-draw", + "function", + "*cam-debug-coll-tri-current*", + "rider-post", + "camera-sphere", + "*cam-debug-coll-tri*", + "ecoclaw-idle", + "load-state", + "gregs-texture-cam-restore", + "cam-line-dma", + "camera-plot-float-func", + "mis-bone-bridge-fall", + "entity-task-complete-off", + "*camera-combiner*", + "camera-fov-draw", + "debug-euler", + "*camera-old-stat-string-tfrag-near*", + "seagull-init-by-other", + "cam-launcher-longfall", + "camera-line-rel", + "cam-dbg-scratch", + "effect-land-water", + "*camera-old-stat-string-total*", + "*SNOWBALL-bank*", + "part-progress-save-icon-func", + "swamp-rock-idle", + "ripple-for-sunken-dark-eco-helix-room", + "*artist-flip-visible*", + "nav-enemy-test-point-near-nav-mesh?", + "joint-control-channel-eval!", + "*camera-old-stat-string-tfrag*", + "*cam-collision-record-show*", + "ocean-make-trans-camera-masks", + "*cam-collision-record-first*", + "group-rain-screend-drop", + "anim-tester-string-get-frame!!", + "process-drawable-valid?", + "swim-noseblow", + "anim-loop", + "*collide-probe-stack*", + "ja-post", + "jungle-vis", + "object", + "ja-max?", + "ja-done?", + "make-nodes-from-jg", + "make-misty-light-kit", + "ja-blend-eval", + "qmem-copy->!", + "target-bonk-event-handler", + "draw-joint-spheres", + "crate-standard-event-handler", + "mayor-step-carp", + "ocean-mid-add-upload", + "ja-aframe", + "rider-trans", + "ogre-bridgeend", + "kernel-read", + "cspace-inspect-tree", + "ja-num-frames", + "exit-chamber-charger-puzzle", + "*display-cam-los-marks*", + "ja-group-size", + "vector<-cspace+vector!", + "process-drawable-fuel-cell-handler", + "ja-step", + "transform-post", + "*sound-iop-info*", + "drawable-vertex-ratio", + "timer-init", + "drawable-tri-count", + "*grass-surface*", + "texture-bucket", + "*camera-pool*", + "vis-dist", + "ripple-for-dark-eco-pool", + "*ecovalve-sg*", + "process-release?", + "merc-eye-anim", + "mask", + "*display-deci-count*", + "joint-channel", + "entity-birth-no-kill", + "process-entity-status!", + "can-hint-be-played?", + "increment-success-for-hint", + "flip", + "find-hint-control-index", + "generic-tfragment", + "*hint-semaphore*", + "*debug-segment*", + "level-hint-init-by-other", + "*steam-cap-sg*", + "redshot-idle", + "ambient-inspect", + "shortcut-boulder-break", + "clmf-to-edit-cam", + "ambient-type-ocean-off", + "ambient-type-weather-off", + "iron", + "*execute-ambients*", + "level-hint-exit", + "dgo-load-begin", + "target-slide-down-post", + "joint-points", + "robotboss-redshot", + "group-slide-poof-metal", + "level-hint-error", + "level-hint-ambient-sound", + "explosion2", + "wait-for-return", + "*print-login*", + "debug-menu", + "ambient-type-poi", + "ambient-type-music", + "draw-node-dma", + "ambient-type-light", + "*crate-steel-sg*", + "pole-up", + "ambient-type-ocean-near-off", + "robotboss-time-to-shoot-yellow", + "add-debug-x", + "can-grab-display?", + "sound", + "ogreboss-out", + "ocean-near-off", + "snow-ball-junction", + "swamp-tetherrock-die", + "entity-meters", + "process-by-ename", + "text-id", + "voicebox", + "task-known?", + "priority", + "pause-allowed?", + "__assert-min-max-range-float", + "energyarm", + "__assert-zero-lim-range-int", + "kermit-pulse-idle", + "ripple-make-request", + "circle-circle-xz-intersect", + "__assert", + "bully-stop-spinning", + "launcher-idle", + "pov-camera-start-playing", + "get-next-level-down", + "manipy-idle", + "camera-change-to", + "command-get-camera", + "birth-func-set-quat", + "ogreboss-player-inside-range?", + "cam-eye-bank", + "cam-launcher-long-joystick", + "*profile-w*", + "launcher-deactivated", + "kernel-shutdown", + "med-res-level-idle", + "player-stepped", + "gs-prmode-cont", + "sparticle-kill-it", + "manipy-init", + "sky-parms", + "interesting", + "camera-anim", + "bully-post", + "run-step-right", + "cam-launcher-joystick", + "generic-dma-foreground-sink-init", + "cam-stick-code", + "launcher-init-by-other", + "plant-boss-leaf-idle", + "swingpole-active", + "vector-turn-to", + "debug-menu-make-task-need-resolution-menu", + "cave-trap", + "*crate-wood-sg*", + "camera-start", + "part-tracker-notify", + "8)", + "play-reject", + "cam-launcher-shortfall", + "wait", + "part-hud-zoomer-heat-slice-01-func", + "slide-poof", + "look-at", + "mother-spider-egg-die-while-falling", + "clone-parent", + "finalboss-vis", + "*swamp-low-ocean-marker*", + "fixed", + "fuel-cell-spline-slider", + "beach-part-grotto-1", + "hide-hud-quick", + "hex-var", + "hud-collecting", + "rot-quat", + "math-camera", + "event-hook", + "sfx-volume-movie", + "work-set!", + "instant-death", + "alive?", + "copy-parent", + "mayorgears", + "flying-rock-rolling", + "message?", + "trans-hook", + "blend-shape", + "entity-ambient", + "null", + "punch", + "target-launch", + "*color-orange*", + "dm-anim-tester-flag-func", + "post-hook", + "glst-remove", + "*ogre3-mood-lights-table*", + "*water-anim-ogre-lava-sg*", + "vector3w", + "cam-slave-get-fov", + "origin-joint-index", + "square-platform-rising", + "touched", + "cam-layout-save-stringMaxLength", + "cam-curve-pos", + "group-just-poof-crwood", + "can-exit-duck?", + "cam-layout-save-cam-rot", + "ocean-trans-add-constants", + "command-get-trans", + "ground-tween-update", + "group-blue-hit-ground-effect", + "break-and-die", + "process-grab?", + "fall-test", + "fly4", + "compute-dir-parm", + "*citadel-mood-lights-table*", + "slide-down-test", + "target-height-above-ground", + "dm-setting-language", + "whirlpool", + "geologist", + "target-bank", + "dump-qword", + "standard", + "cam-standoff", + "*edge-vert1-table*", + "check-drop-level-rolling-dirt", + "effect-zoom-hit", + "babak", + "target-danger-set!", + "*shrub-state*", + "target-collision-reaction", + "*water-anim-maincave-lower-left-pool-sg*", + "average-turn-angle", + "hover-bike-hum", + "activate-hud", + "list-keeper", + "projectile-die", + "happy-plant", + "delete-back-vel", + "move-legs?", + "fishermans-boat-docked-village", + "citb-coil", + "move-forward", + "draw-inline-array-tfrag", + "flop-down", + "merc-effect", + "flut", + "spin-air", + "fake-shadow", + "target-slide-down", + "birth-func-copy-target-y-rot", + "chamber-lift", + "part-tracker-track-target-joint", + "birth-func-vector-orient", + "birth-func-target-orient", + "auto-save-init-by-other", + "collide-shape-moving-angle-set!", + "sound-group-pause", + "poly-find-nearest-edge", + "interpolate", + "read-pad", + "level-setup", + "hud-hidden", + "visvol", + "init-target", + "init-boundary-regs", + "target-no-ja-move-post", + "print-history", + "generic-tie-interp-point", + "reset-target-state", + "*texture-page-dir*", + "shadow-geo", + "target-compute-pole", + "target-no-stick-post", + "post-flag-setup", + "draw-history", + "*darkcave-mood-sun-table*", + "flag-setup", + "turn-to-vector", + "*water-anim-misty-mud-behind-skeleton-sg*", + "*display-strip-lines*", + "*finalboss-mood-fog-table*", + "do-rotations2", + "bend-gravity", + "*stats-target*", + "*jchar-sg*", + "target-calc-camera-pos", + "snow-log-button-idle-up", + "*double-lurker-nav-enemy-info*", + "add-gravity", + "matrix*!", + "target-real-post", + "babak-taunt", + "target-powerup-process", + "target-generic-event-handler", + "*swamp-mood-fog-table*", + "target-walk", + "do-target-shadow", + "sidekick-clone", + "lava-steam", + "door-open", + "target-send-attack", + "*sidekick-remap*", + "driller-lurker-patrol", + "*game-counts*", + "play-anim", + "energydoor", + "dax-leg-spin", + "fishermans-boat-spawn-particles", + "ocean-near-setup-constants", + "mother-spider-die-wait-for-children", + "dax-stretch-in", + "ram-boss-jump-down-hit-ground", + "dax-stretch-out", + "voicebox-track", + "command-get-time", + "camera-voicebox", + "ice-cube-retract-spikes", + "draw-drawable-tree-tfrag", + "idle", + "sprite-aux-list", + "init-ocean-far-regs", + "target-walk-event-handler", + "target-dangerous-event-handler", + "target-apply-tongue", + "stack-frame", + "target-jump-event-handler", + "neck", + "stance", + "citb-arm-shoulder-b", + "joint", + "reset-pickup", + "target-stance", + "babak-with-cannon-jump-off-cannon", + "get-attack-count", + "get-pickup", + "*font-work*", + "time-of-day-tick", + "target-wade-stance", + "launch", + "stopwatch-begin", + "tongue", + "target-fishing", + "target-pole-cycle", + "update-clock", + "art-name", + "ocean-mid-add-upload-table", + "swamp", + "rigid-body-platform-event-handler", + "birth-func-death-sparks", + "*water-anim-sunken-circular-with-bullys-sg*", + "update-mood-prt-color", + "target-load-wait", + "lurkerworm-rest", + "bonk", + "target-double-jump", + "damage", + "nav-enemy-init-by-other", + "spin-pole", + "target-jump", + "*cam-collision-record-last*", + "dry", + "drawable-sphere-box-intersect?", + "fishing", + "target-stance-look-around", + "snowball", + "robotboss-setup-for-hits", + "target-flut-start", + "spider-mom-t1", + "target-wheel", + "target-hit-ground-anim", + "target-attack-uppercut-jump", + "target-falling-anim-trans", + "*que-str-lock*", + "make-levels-with-tasks-available-to-progress", + "target-running-attack", + "buzz-stop!", + "init-var-jump", + "target-duck-high-jump", + "darkvine-move", + "target-attack-air", + "mz16s-block-table", + "swim-flop", + "target-ice-stance", + "energyarm-no-ball", + "sound-rpc-load-bank", + "circle-tangent-directions", + "target-ice-walk", + "*rolling-mood-sun-table*", + "set-to-run-bootstrap", + "target-swim-down", + "spider-egg-hatch", + "target-wade-walk", + "group-red-eco-spinkick", + "target-swim-jump", + "first-person-hud-init-by-other", + "gorge-abort-idle", + "hud-normal", + "*pause-lock*", + "target-pole-flip-forward", + "*windturbine-sg*", + "hud-waiting", + "first-person-hud", + "hud-coming-in", + "target-swim-walk", + "bonelurker-set-small-bounds-sphere", + "battlecontroller-camera-on", + "*collide-mesh-cache*", + "target-swim-tilt", + "sparticle-group-item", + "merc-stats", + "min-max", + "group-just-footprint-dpsnow", + "progress-going-out", + "generic", + "vertical-planes-array", + "target-death", + "ambient-type-hint", + "target-hit-push", + "anim-test-edit-sequence-list-handler", + "spider-egg", + "death-movie-remap", + "*aphid-nav-enemy-info*", + "*auto-continue*", + "debug-menu-context-grab-joypad", + "velocity-set-to-target!", + "target-hit-move", + "*camera*", + "*voicebox-sg*", + "target-hit-effect", + "flutflut-plat-hide", + "task-exists?", + "green-eco-lurker-gen", + "lava", + "billy-snack-idle", + "task-closed?", + "*sharkey-sg*", + "music-movie-volume", + "up-forward", + "dark-eco-pool", + "sprite-array-2d", + "entity-task-complete-on", + "vector-cross!", + "start-sequence-a", + "target-title", + "balloon", + "sparticle-launcher", + "tar", + "group-smack-surface-dizzy", + "front", + "ice-cube", + "*artist-all-visible*", + "hidden-other", + "*nav-timer*", + "spawn-projectile-blue", + "plant-boss", + "debug-menu-rebuild", + "torus", + "gs-tex1", + "debug-menu-item-send-msg", + "fireboulder-be-clone", + "debug-menu-item-submenu", + "debug-menu-item-submenu-render", + "eddie-cam-restore", + "debug-menu-make-from-template", + "debug-menu-context-set-root-menu", + "debug-menu-item-function-msg", + "debug-menu-item-submenu-msg", + "viewer-ja-name", + "cos", + "external-cam-reset!", + "debug-menu-context-activate-selection", + "*darkcave-mood-fog-table*", + "debug-menu-item-var-make-int", + "*snow-mood-sun-table*", + "allocate-dma-buffers", + "*color-light-yellow*", + "main", + "debug-menu-remove-all-items", + "energydoor-closed-till-task", + "lrocklrg", + "debug-menu-item-flag-render", + "debug-menu-context-open-submenu", + "external-art-control", + "*stdcon*", + "debug-menu-context-select-new-item", + "snow-bumper-deactivate", + "anim-tester-set-name", + "game-level", + "setting-data", + "int-var-gat1", + "clmf-to-index-attr", + "gregs-jungle-cam-restore", + "miners-fire", + "invalid", + "quicksandlurker-idle", + "float-fixed-var", + "gregs-village1-cam-restore", + "*keg-sg*", + "display-sync", + "swamp-tetherrock-break", + "sage-finalboss-extra-enter", + "disable-level-text-file-loading", + "*hud-lights*", + "display-frame-finish", + "gregs-texture2-cam-restore", + "*add-sphere*", + "flying-lurker-move", + "generic-vu1-texbuf", + "*instance-mem-usage*", + "*screen-shot*", + "cave-cam-restore", + "*surrogate-dma-buffer*", + "cam-layout-save-interptime", + "find-instance-by-index", + "marks-cam-restore", + "dec-mod3", + "determine-pause-mode", + "guard-band-cull", + "draw-ocean-mid-seams", + "*stats-memory*", + "draw-ocean", + "syncv", + "*citb-drop-plat-yellow-sg*", + "toggle-pause", + "swap-fake-shadow-buffers", + "ntsc", + "uint16", + "put-display-env", + "*lightning-mole-nav-enemy-info*", + "update-ocean", + "set-hud-aspect-ratio", + "display-loop", + "*cheat-temp*", + "cam-collision-record", + "process-taskable-clean-up-after-talking", + "sound-flava", + "anim-test-field-highlight-lw", + "*screen-filter*", + "scf-get-timeout", + "gs-page-width", + "ocean-near-index", + "scf-get-inactive-timeout", + "robber-idle", + "lurkerworm-spot", + "make-current-level-available-to-progress", + "snow-gears-halt", + "test-closest-pt-in-triangle", + "make-collide-list-using-line-sphere-inst-test", + "upload-vram-pages", + "collide-puls-work", + "entity-by-type", + "ocean-trans-mask-ptrs-bit?", + "entity-remap-names", + "*compact-actors*", + "plat-event", + "spin", + "update-actor-vis-box", + "sounds", + "at-pick-sequence", + "entity-process-count", + "entity-count", + "*deathcam-sg*", + "*vis-actors*", + "entity-speed-test", + "init-entity", + "ogreboss-missile-impact", + "empty-state", + "ocean-trans-mask-ptrs-set!", + "expand-vis-box-with-point", + "*wind-scales*", + "rand-vu-nostep", + "box", + "ogre-bridge", + "vis", + "part-progress-hud-tint-func", + "birth-viewer", + "*flop-mods*", + "inc-mod3", + "delayed-rand-float", + "vent-standard-event-handler", + "history-draw-and-update", + "add-collide-shape-spheres", + "*color-dark-green*", + "thread", + "*color-cyan*", + "_format", + "circle-triangle-intersection?", + "update-mood-lightning", + "debug-report-nav-stats", + "part-water-drip", + "*color-dark-magenta*", + "target-racing-turn-anim", + "choose-travel-portal-vertex", + "hammer-tap", + "add-process-drawable", + "*color-black*", + "drill-hit", + "*color-blue*", + "target-racing-land-anim", + "*sharkey-nav-enemy-info*", + "*edge-mask-table*", + "dark-crystal-spawn-fuel-cell", + "*debug-nav*", + "dgo-load-link", + "*test-ray-src-pos*", + "*nav-last-triangle-test-count*", + "*instance-tie-menu*", + "init-ray-dir-local", + "darkeco", + "glst-prev", + "*swim-surface*", + "*color-light-cyan*", + "lavafallsewerb-idle", + "add-nav-sphere", + "oot-work", + "hud-money", + "lurkerfish-dies", + "border", + "can-jump?", + "point-inside-poly?", + "*debug-ray-test*", + "*color-light-blue*", + "virtual-frame", + "hud-pickups", + "battlecontroller-battle-end", + "sign-bit", + "init-ray", + "dm-anim-tester-func", + "*nav-update-route-table-route-count*", + "*nav-timer-enable*", + "target-pole-flip-up-jump", + "group-run-poof-stone", + "debug-nav-validate-current-poly", + "ripple-wave", + "*color-gray*", + "reflector-origin-idle", + "*nav-one-third*", + "*color-magenta*", + "lava-erupt", + "init-ray-local", + "*color-light-red*", + "position-in-front-of-camera!", + "*debug-ray-test-capture-mode*", + "point-inside-rect?", + "*debug-output*", + "cfs-travel-vec", + "*color-green*", + "bonelurker-push-post", + "nav-ray-test-local?", + "disable", + "vu-point-triangle-intersection?", + "point-triangle-distance-min", + "*color-dark-blue*", + "*ice-cube-sg*", + "collide-upload-vu0", + "*color-white*", + "nav-mesh-sphere", + "*footstep-surface*", + "peeper-wait", + "effect-run-step-left", + "plant-boss-reset", + "babak-with-cannon-shooting", + "death-start", + "precurbridge", + "set-to-run", + "build-continue-menu", + "group-land-poof-neutral", + "dm-ocean-subdiv-draw-func", + "group-slide-poof-dpsnow", + "group-just-footprint-snow", + "snow-button", + "group-land-poof-grass", + "group-land-poof-snow", + "effect-zoom-land", + "rolling-start", + "*citb-disc-c-sg*", + "*citb-greensage-tasks*", + "robber-calc-speed", + "target-yellow-jump-blast", + "group-run-poof-water", + "integer", + "ocean-near-block", + "group-run-poof-pcmetal", + "group-slide-poof-straw", + "group-just-footprint-tar", + "group-just-poof-wood", + "group-just-footprint-straw", + "generic-tie-calls", + "group-run-poof-crwood", + "effect-flut-land", + "group-just-poof-sand", + "group-slide-poof-wood", + "kermit-hop", + "campoints", + "dm-strip-lines-set-pick-func", + "helix-slide-door", + "*dark-crystal-flash-delays*", + "camera-shake", + "lurkerm-tall-sail-idle", + "*display-render-collision*", + "focalPull", + "group-just-footprint-pcmetal", + "group-land-poof-sand", + "billy-rat", + "effect-jump-droppings", + "*powercellalt-sg*", + "*ocean-trans-up-right-table*", + "cam-layout-entity-volume-info-create", + "group-slide-poof-pcmetal", + "group-slide-poof-water", + "group-land-poof-swamp", + "group-slide-poof-neutral", + "group-just-poof-metal", + "group-just-footprint-grass", + "target-compute-edge-rider", + "effect-roll", + "movie-mask", + "group-slide-poof-crwood", + "effect-land", + "aid", + "flut-land", + "group-just-poof-stone", + "mz16-block-table", + "zoom-hit", + "group-just-poof-tar", + "piston-open", + "group-run-poof-neutral", + "group-run-poof-dpsnow", + "group-just-footprint-crwood", + "*snow-gears-sg*", + "effect-run-step-right", + "group-just-footprint-stone", + "walk-tie-generic-prototypes", + "target-standard-event-handler", + "group-run-poof-swamp", + "group-land-poof-tar", + "*double-lurker-when-both-nav-enemy-info*", + "tube-bank", + "texture-page-common-boot-allocate", + "group-just-poof-pcmetal", + "group-slide-poof-swamp", + "group-land-poof-stone", + "group-just-footprint-sand", + "clm-item", + "group-land-poof-gravel", + "*robotboss-sg*", + "give-cell", + "effect-joint", + "group-run-poof-gravel", + "check-water-level-drop-and-die", + "*scarecrow-b-sg*", + "check-water-level-above-and-die", + "birth-func-y->userdata", + "robotboss-yellow-eco-on", + "tie-end-buffer", + "link", + "water-vol-init-by-other", + "update", + "water-vol-startup", + "ogre-boulder", + "pelican-spit", + "heat", + "water-vol-idle", + "smack-surface", + "path7", + "ocean-get-height", + "*ogre2-mood*", + "water-height", + "eco-fadeout", + "point-in-triangle-cross", + "init-viewer", + "*CAMERA_MASTER-bank*", + "part-tracker-track-target", + "generic-no-light-dproc", + "vent-wait-for-touch", + "ecovalve-idle", + "*plunger-lurker-sg*", + "v-slrp3!", + "ventyellow", + "eco-yellow", + "fuel-cell-clone-anim", + "*display-level-border*", + "eco-collectable", + "dm-task-get-money", + "sky-color-day", + "install-debug-handler", + "money-init-by-other-no-bob", + "*shortcut-boulder-broken-sg*", + "collide-cache-tri", + "joint-mod-spinner", + "fuel-cell-init-as-clone", + "*vil3-bridge-36-sg*", + "transform-float-point", + "vent-pickup", + "set-standoff-dist", + "*buzzer-sg*", + "ecovalve-init-by-other", + "collectable", + "*bluesage-sg*", + "fuel-cell-init-by-other", + "pickup-type->string", + "*display-ambient-weather-off-marks*", + "eco-red", + "*eco-pill-count*", + "mole-taunt-2", + "*money-sg*", + "show", + "remove-exit", + "othercam-init-by-other", + "show-particles", + "*ogreboss-super-boulder-sg*", + "update-sky-tng-data", + "hide", + "eco-bg-green", + "eco-blue-glow", + "*citb-redsage-tasks*", + "sages-kidnapped?", + "*snow-fort-gate-sg*", + "shadow-settings", + "*oddeven*", + "update-mood-maincave", + "*null-task-control*", + "*mayor-tasks*", + "change-pov", + "*assistant-tasks*", + "spawn-kermit-pulse", + "task-status->string", + "crate", + "*oracle-village2-tasks*", + "*geologist-tasks*", + "collide-edge-work", + "open-specific-task!", + "robber-rotate", + "process-taskable-hide-exit", + "process-taskable-play-anim-enter", + "process-taskable-hide-enter", + "process-taskable-play-anim-code", + "lose", + "no", + "*ogreboss-shoot-boulder-sg*", + "viewer-process", + "hud-hidden?", + "at-cam-free-floating", + "hide-hud", + "*sprite-hvdf-data*", + "pov-camera-play-and-reposition", + "abort", + "progress-init-by-other", + "crab-walk3", + "pov-camera-abort", + "matrix-rotate-xyz!", + "pov-camera-startup", + "*sage-sg*", + "crate-init-by-other", + "*CRATE-bank*", + "wheel-flip", + "*crate-bucket-sg*", + "crate-buzzer", + "mistycannon-missile", + "special-contents-die", + "steel", + "bounce-on", + "ogre-step-a", + "swim-down", + "demo", + "flut-bonk", + "collide-usually-hit-by-player-list", + "wood", + "part-progress-hud-right-func", + "show-mc-info", + "crate-type", + "at-apply-align", + "tfrag-work", + "hud-arriving", + "spool-anim", + "hud-in", + "send-hud-increment-event", + "hud-init-by-other", + "increment", + "part-hud-health-03-func", + "group-slide-poof-dirt", + "fuel-cell-hud-center-callback", + "seagull-reaction", + "display-file-info", + "hud-buzzers", + "part-hud-eco-timer-02-func", + "green-eco-lurker-appear-land", + "*citb-chain-plat-constants*", + "generic-shrub-fragment", + "show-hud", + "sound-rpc-stop-group", + "part-hud-eco-timer-03-func", + "execute-math-engine", + "part-hud-eco-timer-01-func", + "*fuelcell-naked-sg*", + "part-hud-eco-timer-backing-func", + "file-stream-open", + "part-hud-buzzer-func", + "swamp-bat", + "self", + "fuel-cell-hud-starburst-3-callback", + "*light-eco-small-sg*", + "*graphic-title-options-pal*", + "camera-pos", + "*game-options-demo*", + "vector-reflect!", + "*options-remap*", + "yeti-slave-appear-jump-up", + "*main-options*", + "*game-options*", + "*save-options*", + "*yes-no-options*", + "sprite-add-2d-all", + "*save-options-title*", + "string-get-flag!!", + "*sound-options*", + "glst-head", + "mistycannon", + "cos-rad", + "part-progress-hud-button-func", + "*med-res-ogre2-sg*", + "part-progress-card-slot-02-func", + "part-progress-card-slot-03-func", + "part-progress-card-slot-04-func", + "hide-progress-icons", + "progress-debug", + "*empty-mods*", + "progress-normal", + "*junglefish-sg*", + "get-next-level-up", + "*progress-state*", + "matrix->quaternion", + "get-next-task-up", + "entity-by-aid", + "get-next-task-down", + "*title-credits-scale*", + "draw-large-polygon-ocean", + "clear", + "anim-test-sequence-init", + "projectile-dissipate", + "swa", + "find-ground-and-draw-shadow", + "ocean-init-buffer", + "target-edge-grab-off", + "glst-node", + "ocean-end-buffer", + "*ocean-map*", + "draw-ocean-mid", + "EulNext", + "ocean-vu0-block", + "snake-rattle", + "ocean-texture-add-verts-last", + "nav-enemy-rnd-float", + "ocean-texture-add-verts", + "*forward-pole-jump-mods*", + "ocean-texture-add-envmap", + "gif-tag-count", + "*ocean-texture-work*", + "gambler", + "ocean-mid-mask-ptrs-bit?", + "ocean-vector-matrix*!", + "float", + "ocean-mid-camera-masks-bit?", + "sky-moon-data", + "ocean-mid-add-call", + "mother-spider-birthing", + "ocean-mid-check", + "ocean-mid-add-matrices", + "draw-quad2d", + "ocean-seams-add-constants", + "ocean-trans-add-upload", + "ease-in", + "ocean-trans-camera-masks-bit?", + "ocean-vertex", + "display-list-control", + "ocean-near-add-upload", + "swampgate", + "ocean-near-add-constants", + "gnawer-run", + "ocean-near-add-call-flush", + "ocean-near-add-heights", + "ocean-near-add-matrices", + "dead-pool", + "draw-shadow", + "citb-stair-plat", + "add-fake-shadow-to-buffer", + "convert-eye-data", + "fact-info-target", + "*common-text-heap*", + "nav-enemy-common-post", + "snow-bunny-initialize-jump", + "initialize-anim-tester", + "glst-named-node", + "do-effect", + "glst-empty?", + "nav-enemy-give-up", + "glst-start-of-list?", + "texture-page-dir-inspect", + "adgif-shader-update!", + "glst-get-node-index", + "child-die", + "glst-find-node-by-name", + "glst-remove-head", + "glst-insert-after", + "dm-task-resolution", + "mother-spider-egg-die-exit", + "glst-length-of-longest-name", + "anim-tester-start", + "anim-tester", + "tfrag-data", + "anim-tester-reset", + "fill-skeleton-cache", + "anim-test-seq-mark-as-edited", + "anim-test-sequence", + "anim-test-obj-list-handler", + "flags-off", + "anim-tester-save-all-objects", + "citb-arm", + "anim-tester-load-object-seqs", + "shadow-find-facing-single-tris", + "display-level", + "anim-tester-add-object", + "*revcycleprop-sg*", + "anim-test-obj-item-valid?", + "anim-test-obj-init", + "anim-test-anim-list-handler", + "instance", + "drawable-inline-array-instance-shrub", + "anim-tester-add-sequence", + "anim-tester-num-print", + "*anim-tester*", + "list-control", + "sunken-pipegame-idle", + "anim-test-edit-seq-insert-item", + "anim-tester-interface", + "anim-tester-adjust-frame", + "extra-radius", + "*volume-descriptor*", + "anim-tester-process", + "anim-tester-get-playing-item", + "beam-on", + "*debug-menu-context*", + "*swamp-mood-sun-table*", + "pick-sequence", + "nav-enemy-rnd-percent?", + "edit-sequence", + "pick-joint-anim", + "init-viewer-for-other", + "energyhub-idle", + "ecovent", + "viewer-string", + "add-a-bunch", + "elevator-idle-at-cave", + "loading-level", + "viewer", + "racer-on-ground?", + "dump-mem", + "*viewer-sg*", + "level-hint-task-process", + "part-tester-init-by-other", + "start-part", + "mistycam", + "*eggtop-sg*", + "dm-current-continue", + "dm-time-of-day-func", + "dm-give-all-cells", + "debug-menu-make-instance-menu", + "dm-task-introduction", + "*CAM_LAYOUT-bank*", + "allow-progress", + "dm-subdiv-int", + "target-racing-hit", + "*lurkerm-short-sail-sg*", + "dm-cam-externalize", + "bit-array", + "dm-subdiv-draw-func", + "dm-game-mode-pick-func", + "dm-lavabike-ready", + "dm-texture-user-set-pick-func", + "center-point", + "ocean-mid-add-upload-middle", + "dm-levitator-ready", + "dm-cam-settings-func-int", + "vector-v!", + "dm-actor-marks-pick-func", + "*helix-slide-door*", + "dm-task-reminder-a", + "debug-menu-make-task-need-reward-speech-menu", + "popup-menu-context-make-default-menus", + "*debug-tests*", + "*popup-menu-context*", + "dm-shader-pick-func", + "dm-vu1-user-set-pick-func", + "*snow-log-sg*", + "whirlpool-idle", + "debug-menu-make-task-need-reminder-menu", + "delay", + "dm-vu1-user-toggle-pick-func", + "*color-dark-cyan*", + "dm-give-cell", + "debug-menu-make-camera-mode-menu", + "dm-texture-user-toggle-pick-func", + "debug-menu-make-task-need-hint-menu", + "debug-menu-context-make-default-menus", + "cam-robotboss", + "allow-z", + "at-show-joint-info", + "at-cam-stick", + "breath-in-loud", + "lurkerm-piston", + "breath-out-loud", + "splitb-dies", + "flop-land", + "target-play-anim", + "sky-set-sun-radii", + "drawable-tree-actor", + "jump-long", + "*junglefish-nav-enemy-info*", + "swim-dive", + "*rolling-spheres-light4*", + "swim-idle1", + "*lurkerm-piston-sg*", + "ocean-wave-info", + "*display-collide-history*", + "swim-idle2", + "swim-jump", + "robotboss-set-dda", + "swim-kick-surf", + "lurkerworm-strike", + "format", + "swim-kick-under", + "swim-surface", + "swim-to-down", + "mis-bone-bridge", + "robo-yell", + "swim-turn", + "maindoor-closed", + "bottom-hud-hidden?", + "swim-up", + "bounceytarp", + "rigid-body-control-point", + "*water-anim-rolling-water-back-sg*", + "mistycannon-missile-in-water", + "*water-anim-village1-rice-paddy-mid-sg*", + "*water-anim-sunken-first-room-from-entrance-sg*", + "*water-anim-sunken-big-room-upper-water-sg*", + "*water-anim-sunken-first-right-branch-sg*", + "*camera-layout-message-ypos*", + "*water-anim-misty-mud-under-spine-sg*", + "*water-anim-sunken-hall-before-big-room-sg*", + "max-vis-dist", + "*water-anim-robocave-main-pool-sg*", + "*water-anim-misty-mud-island-near-dock-sg*", + "*water-anim-jungle-river-sg*", + "*water-anim-lavatube-energy-lava-sg*", + "*citb-bunny-nav-enemy-info*", + "*vblank-counter*", + "*water-anim-maincave-mid-left-pool-sg*", + "*water-anim-sunken-big-room-sg*", + "*water-anim-misty-mud-above-skull-back-sg*", + "*water-anim-darkcave-water-with-crystal-sg*", + "boulder2-trans", + "glst-insert-before", + "*water-anim-sunken-qbert-room-sg*", + "*water-anim-misty-mud-other-near-skull-sg*", + "*water-anim-sunken-short-piece-sg*", + "lavabase-idle", + "*water-anim-village1-rice-paddy-sg*", + "ogreboss-set-stage1-camera", + "water-anim-look", + "teleport-to-transformq", + "*water-anim-rolling-water-front-sg*", + "*water-anim-maincave-center-pool-sg*", + "ripple-for-finalboss-dark-eco-pool", + "flying-rock", + "char-verts", + "cam-debug-add-los-tri", + "rigid-body-platform", + "rigid-body-platform-post", + "vector-degmod", + "impulse", + "rigid-body-platform-idle", + "draw-end-credits", + "*vis-boot*", + "start-time-of-day", + "nav-enemy-info", + "bsp-camera-asm", + "vector-matrix*!", + "nav-enemy-reset-frustration", + "*edge-grab-mods*", + "nav-enemy-falling-post", + "nav-enemy-travel-post", + "init-sidekick", + "nav-enemy-jump-land-anim", + "nav-enemy-execute-custom-jump", + "set-forward-vel", + "nav-enemy-jump-post", + "nav-enemy-turn-to-face-dir", + "death", + "nav-enemy-execute-jump", + "rgba", + "nav-enemy-default-event-handler", + "nav-enemy-neck-control-look-at", + "nav-enemy-flee-post", + "racer-sounds", + "nav-enemy-notice-player?", + "allow-look-around", + "nav-enemy-face-player-post", + "nav-enemy-jump-event-handler", + "nav-enemy-facing-player?", + "*CAM_BIKE-bank*", + "nav-enemy-initialize-jump", + "*nav-enemy-dummy-shadow-control*", + "ogreboss-missile", + "finished", + "add-debug-yrot-vector", + "process-drawable-death-event-handler", + "destroy-mem", + "nav-enemy-get-new-patrol-point", + "bone-smallswing", + "*sage-tasks*", + "setup-blerc-chains", + "nav-enemy-neck-control-inactive", + "camera-master-bank", + "ja-group-index?", + "*dark-crystal-sg*", + "nav-enemy-jump-to-point"}; diff --git a/test/all_jak1_symbols.h b/test/all_jak1_symbols.h new file mode 100644 index 0000000000..ae8e5e8726 --- /dev/null +++ b/test/all_jak1_symbols.h @@ -0,0 +1,6 @@ +#ifndef JAK1_ALL_JAK1_SYMBOLS_H +#define JAK1_ALL_JAK1_SYMBOLS_H + +extern const char* all_syms[7941]; + +#endif // JAK1_ALL_JAK1_SYMBOLS_H diff --git a/test/test_CodeTester.cpp b/test/test_CodeTester.cpp new file mode 100644 index 0000000000..c5e1ba832a --- /dev/null +++ b/test/test_CodeTester.cpp @@ -0,0 +1,167 @@ +/*! + * @file test_CodeTester.cpp + * Tests for the CodeTester, a tool for testing the emitter by emitting code and running it + * from within the test application. + */ + +#include "gtest/gtest.h" +#include "goalc/emitter/CodeTester.h" +#include "goalc/emitter/IGen.h" + +using namespace goal; + +TEST(CodeTester, prologue) { + CodeTester tester; + tester.init_code_buffer(256); + tester.emit_push_all_gprs(); + // check we generate the right code for pushing all gpr's + EXPECT_EQ(tester.dump_to_hex_string(), + "50 51 52 53 54 55 56 57 41 50 41 51 41 52 41 53 41 54 41 55 41 56 41 57"); +} + +TEST(CodeTester, epilogue) { + CodeTester tester; + tester.init_code_buffer(256); + tester.emit_pop_all_gprs(); + // check we generate the right code for popping all gpr's + EXPECT_EQ(tester.dump_to_hex_string(), + "41 5f 41 5e 41 5d 41 5c 41 5b 41 5a 41 59 41 58 5f 5e 5d 5c 5b 5a 59 58"); +} + +TEST(CodeTester, execute_return) { + CodeTester tester; + tester.init_code_buffer(256); + // test creating a function which simply returns + tester.emit_return(); + // and execute it! + tester.execute(); +} + +TEST(CodeTester, execute_push_pop_gprs) { + CodeTester tester; + tester.init_code_buffer(256); + // test we can push/pop gprs without crashing. + tester.emit_push_all_gprs(); + tester.emit_pop_all_gprs(); + tester.emit_return(); + tester.execute(); +} + +TEST(CodeTester, load_constant_64_and_move_gpr_gpr_64) { + std::vector u64_constants = {0, UINT64_MAX, INT64_MAX, 7, 12}; + + // test we can load a 64-bit constant into all gprs, move it to any other gpr, and return it. + // rsp is skipping because that's the stack pointer and would prevent us from popping gprs after + + CodeTester tester; + tester.init_code_buffer(256); + + for (auto constant : u64_constants) { + for (int r1 = 0; r1 < 16; r1++) { + if (r1 == RSP) { + continue; + } + + for (int r2 = 0; r2 < 16; r2++) { + if (r2 == RSP) { + continue; + } + tester.clear(); + tester.emit_push_all_gprs(true); + tester.emit(IGen::mov_gpr64_u64(r1, constant)); + tester.emit(IGen::mov_gpr64_gpr64(r2, r1)); + tester.emit(IGen::mov_gpr64_gpr64(RAX, r2)); + tester.emit_pop_all_gprs(true); + tester.emit_return(); + EXPECT_EQ(tester.execute(), constant); + } + } + } +} + +TEST(CodeTester, load_constant_32_unsigned) { + std::vector u64_constants = {0, UINT32_MAX, INT32_MAX, 7, 12}; + + // test loading 32-bit constants, with all upper 32-bits zero. + // this uses a different opcode than 64-bit loads. + CodeTester tester; + tester.init_code_buffer(256); + + for (auto constant : u64_constants) { + for (int r1 = 0; r1 < 16; r1++) { + if (r1 == RSP) { + continue; + } + + tester.clear(); + tester.emit_push_all_gprs(true); + tester.emit(IGen::mov_gpr64_u32(r1, constant)); + tester.emit(IGen::mov_gpr64_gpr64(RAX, r1)); + tester.emit_pop_all_gprs(true); + tester.emit_return(); + EXPECT_EQ(tester.execute(), constant); + } + } +} + +TEST(CodeTester, load_constant_32_signed) { + std::vector s32_constants = {0, 1, INT32_MAX, INT32_MIN, 12, -1}; + + // test loading signed 32-bit constants. for values < 0 this will sign extend. + CodeTester tester; + tester.init_code_buffer(256); + + for (auto constant : s32_constants) { + for (int r1 = 0; r1 < 16; r1++) { + if (r1 == RSP) { + continue; + } + + tester.clear(); + tester.emit_push_all_gprs(true); + tester.emit(IGen::mov_gpr64_s32(r1, constant)); + tester.emit(IGen::mov_gpr64_gpr64(RAX, r1)); + tester.emit_pop_all_gprs(true); + tester.emit_return(); + EXPECT_EQ(tester.execute(), constant); + } + } +} + +TEST(CodeTester, xmm_move) { + std::vector u32_constants = {0, INT32_MAX, UINT32_MAX, 17}; + + // test moving between xmms (32-bit) and gprs. + CodeTester tester; + tester.init_code_buffer(256); + + for(auto constant : u32_constants) { + for(int r1 = 0; r1 < 16; r1++) { + if(r1 == RSP) { + continue; + } + for(int r2 = 0; r2 < 16; r2++) { + if(r2 == RSP) { + continue; + } + for(int r3 = 0; r3 < 16; r3++) { + for(int r4 = 0; r4 < 16; r4++) { + tester.clear(); + tester.emit_push_all_gprs(true); + // move constant to gpr + tester.emit(IGen::mov_gpr64_u32(r1, constant)); + // move gpr to xmm + tester.emit(IGen::movd_xmm32_gpr32(get_nth_xmm(r3), r1)); + // move xmm to xmm + tester.emit(IGen::mov_xmm32_xmm32(get_nth_xmm(r4), get_nth_xmm(r3))); + // move xmm to gpr + tester.emit(IGen::movd_gpr32_xmm32(r2, get_nth_xmm(r4))); + // return! + tester.emit(IGen::mov_gpr64_gpr64(RAX, r2)); + tester.emit_return(); + } + } + } + } + } +} \ No newline at end of file diff --git a/test/test_goos.cpp b/test/test_goos.cpp new file mode 100644 index 0000000000..6060fcbeef --- /dev/null +++ b/test/test_goos.cpp @@ -0,0 +1,1289 @@ +/*! + * @file test_goos.cpp + * Tests for the GOOS macro language. + */ + +#include "gtest/gtest.h" +#include "goalc/goos/Interpreter.h" + +using namespace goos; + +namespace { +// helper to evaluate a string as a goos expression. +std::string e(Interpreter& interp, const std::string& in) { + return interp.eval(interp.reader.read_from_string(in), interp.global_environment.as_env()) + .print(); +} +} // namespace + +TEST(GoosBuiltins, Begin) { + Interpreter i; + // begin returns the last thing in the list + EXPECT_EQ(e(i, "(begin 1 2)"), "2"); + // begin with nothing returns the empty list + EXPECT_EQ(e(i, "(begin)"), "()"); + + e(i, "(begin (define x 123) 3)"); + EXPECT_EQ(e(i, "x"), "123"); + + i.disable_printfs(); + for (auto x : {"(begin :test 1)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, Read) { + Interpreter i; + // test that we can read a string into the reader at runtime. + EXPECT_EQ(e(i, "(read \"1\")"), "(top-level 1)"); + + i.disable_printfs(); + // check bad expressions. + for (auto x : + {"(read 1)", "(read)", R"((read "a" "b"))", "(read :a b \"a\")", "(read \"(((((\")"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, ReadFile) { + Interpreter i; + // check that we can read a file. + EXPECT_EQ(e(i, "(read-file \"test/test_reader_file0.gc\")"), "(top-level (1 2 3 4))"); + + i.disable_printfs(); + for (auto x : {"(read-file 1)", "(read-file)", "(read-file \"goal/test/not_a_real_file.gc\")"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, LoadFile) { + Interpreter i; + // check that we can read and execute a file. + e(i, "(load-file \"test/test_goos_file0.gs\")"); + EXPECT_EQ(e(i, "x"), "23"); + i.disable_printfs(); + for (auto x : {"(load-file 1)", "(load-file)", "(load-file \"goal/test/not_a_real_file.gc\")"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, PrintAndInspect) { + Interpreter i; + i.disable_printfs(); + // check that we can print/inspect (doesn't check result, just sees that it doesn't crash) + EXPECT_EQ(e(i, "(print 10)"), "()"); + EXPECT_EQ(e(i, "(inspect 10)"), "()"); + for (auto x : {"(print 1 2)", "(print)", "(print 'a :a b)", "(print :a b)", "(inspect 1 2)", + "(inspect)", "(inspect 'a :a b)", "(inspect :a b)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +namespace { +// checks if two things are equal by running them through the GOOS eq? form. +bool eq(Interpreter& i, const std::string& a, const std::string& b) { + return e(i, "(eq? " + a + " " + " " + b + ")") == "#t"; +} +} // namespace + +TEST(GoosBuiltins, Equality) { + Interpreter i; + + // STRING + EXPECT_TRUE(eq(i, "\"asdf\"", "\"asdf\"")); + EXPECT_FALSE(eq(i, "\"asdgf\"", "\"asdf\"")); + + // INTEGER + EXPECT_TRUE(eq(i, "123", "0123")); + EXPECT_FALSE(eq(i, "123", "10123")); + + // FLOAT + EXPECT_TRUE(eq(i, "1.23", "01.23")); + EXPECT_FALSE(eq(i, "1.23", "10.123")); + + // CHAR + EXPECT_TRUE(eq(i, "#\\a", "#\\a")); + EXPECT_FALSE(eq(i, "#\\b", "#\\a")); + + // SYMBOL + EXPECT_TRUE(eq(i, "'a", "'a")); + EXPECT_FALSE(eq(i, "'a", "'b")); + + // ENV + EXPECT_TRUE(eq(i, "*global-env*", "*global-env*")); + EXPECT_FALSE(eq(i, "*global-env*", "*goal-env*")); + + // LAMBDA + e(i, "(desfun test-fun-1 () 2)"); + e(i, "(desfun test-fun-2 () 3)"); + e(i, "(define test-fun-3 test-fun-1)"); + + EXPECT_TRUE(eq(i, "test-fun-1", "test-fun-3")); + EXPECT_FALSE(eq(i, "test-fun-1", "test-fun-2")); + + // MACRO + e(i, "(defsmacro test-mac-1 () 2)"); + e(i, "(defsmacro test-mac-2 () 3)"); + e(i, "(define test-mac-3 test-mac-1)"); + + EXPECT_TRUE(eq(i, "test-mac-1", "test-mac-3")); + EXPECT_FALSE(eq(i, "test-mac-1", "test-mac-2")); + + // EMPTY LIST + EXPECT_TRUE(eq(i, "'()", "'()")); + + // PAIR + EXPECT_TRUE(eq(i, "'(1 2 3)", "'(1 2 3)")); + EXPECT_FALSE(eq(i, "'(1 2 3)", "'(1 2 4)")); + + // ARRAY + EXPECT_TRUE(eq(i, "'#(1 2 3)", "'#(1 2 3)")); + EXPECT_FALSE(eq(i, "'#(1 2 3)", "'#(1 2 4)")); + EXPECT_FALSE(eq(i, "'#(1 2 3)", "'#(1 2 3 4)")); + EXPECT_FALSE(eq(i, "'#(1 2 3 4 5)", "'#(1 2 3 4)")); + i.disable_printfs(); + for (auto x : {"(eq?)", "(eq? 1)", "(eq? 1 2 3)", "(eq? 1 2 :keyword 2)", "(eq? 1 :keyword 2)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, Addition) { + Interpreter i; + // single element adding + EXPECT_EQ(e(i, "(+ 1)"), "1"); + EXPECT_EQ(e(i, "(+ 1.)"), "1.000000"); + + // two element adding + EXPECT_EQ(e(i, "(+ 1 2)"), "3"); + EXPECT_EQ(e(i, "(+ 1.1 2.2)"), "3.300000"); + + // mixed + EXPECT_EQ(e(i, "(+ 1 1.1)"), "2"); + EXPECT_EQ(e(i, "(+ 1.1 1)"), "2.100000"); + + // many, and check rounding happens at the right time + EXPECT_EQ(e(i, "(+ 1 1.4 1.4 1.4 1.4)"), "5"); + + i.disable_printfs(); + for (auto x : {"(+)", "(+ 'a)", "(+ #\\a)", "(+ 1 :test 2)", "(+ 1 2 :test 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, Multiplication) { + Interpreter i; + // single element adding + EXPECT_EQ(e(i, "(* 2)"), "2"); + EXPECT_EQ(e(i, "(* 2.)"), "2.000000"); + + // two element adding + EXPECT_EQ(e(i, "(* 3 2)"), "6"); + EXPECT_EQ(e(i, "(* 1.1 2.2)"), "2.420000"); + + // mixed + EXPECT_EQ(e(i, "(* 1 1.1)"), "1"); + EXPECT_EQ(e(i, "(* 1.1 1)"), "1.100000"); + + // many, and check rounding happens at the right time + EXPECT_EQ(e(i, "(* 3 1.4 1.4 1.4 1.4)"), "3"); + + i.disable_printfs(); + for (auto x : {"(*)", "(* 'a)", "(* #\\a)", "(* 1 :test 2)", "(* 1 2 :test 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, Subtraction) { + Interpreter i; + // single element adding + EXPECT_EQ(e(i, "(- 2)"), "-2"); + EXPECT_EQ(e(i, "(- 2.)"), "-2.000000"); + + // two element adding + EXPECT_EQ(e(i, "(- 3 2)"), "1"); + EXPECT_EQ(e(i, "(- 1.1 2.2)"), "-1.100000"); + + // mixed + EXPECT_EQ(e(i, "(- 1 1.1)"), "0"); + EXPECT_EQ(e(i, "(- 1.1 1)"), "0.100000"); + + // many, and check rounding happens at the right time + EXPECT_EQ(e(i, "(- 3 1.4 1.4 1.4 1.4)"), "-1"); + + i.disable_printfs(); + for (auto x : {"(-)", "(- 'a)", "(- #\\a)", "(- 1 :test 2)", "(- 1 2 :test 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, Division) { + Interpreter i; + + // two element adding + EXPECT_EQ(e(i, "(/ 16 2)"), "8"); + EXPECT_EQ(e(i, "(/ 9. 2.)"), "4.500000"); + + // mixed + EXPECT_EQ(e(i, "(/ 3 2.)"), "1"); + EXPECT_EQ(e(i, "(/ 3. 2)"), "1.500000"); + + i.disable_printfs(); + for (auto x : {"(/ 1)", "(/ 1.0)", "(/)", "(/ 'a)", "(/ #\\a)", "(/ 1 :test 2)", + "(/ 1 2 :test 3)", "(/ 1 2 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, NumberEquals) { + Interpreter i; + EXPECT_EQ(e(i, "(= 16 2)"), "#f"); + EXPECT_EQ(e(i, "(= 2 2)"), "#t"); + + EXPECT_EQ(e(i, "(= 16.3 2.4)"), "#f"); + EXPECT_EQ(e(i, "(= 2.2 2.2)"), "#t"); + + EXPECT_EQ(e(i, "(= 2.0 2)"), "#t"); + EXPECT_EQ(e(i, "(= 2 2.2)"), "#t"); + EXPECT_EQ(e(i, "(= 2.2 2)"), "#f"); + EXPECT_EQ(e(i, "(= 2 3.2)"), "#f"); + + EXPECT_EQ(e(i, "(= 2 2 2 2 2)"), "#t"); + EXPECT_EQ(e(i, "(= 2 2 2 2.2 2.3)"), "#t"); + EXPECT_EQ(e(i, "(= 2 2 2 2.2 3.0)"), "#f"); + + i.disable_printfs(); + for (auto x : {"(= 1)", "(=)", "(= a 1)", "(= 'a 1)", "(= 1 2 :test 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, NumberLessThan) { + Interpreter i; + EXPECT_EQ(e(i, "(< 16 2)"), "#f"); + EXPECT_EQ(e(i, "(< 2 2)"), "#f"); + EXPECT_EQ(e(i, "(< 1 2)"), "#t"); + + EXPECT_EQ(e(i, "(< 16.8 16.2)"), "#f"); + EXPECT_EQ(e(i, "(< 2.2 2.2)"), "#f"); + EXPECT_EQ(e(i, "(< 1.9 2.1)"), "#t"); + + EXPECT_EQ(e(i, "(< 1.1 2)"), "#t"); + EXPECT_EQ(e(i, "(< 1 2.1)"), "#t"); + EXPECT_EQ(e(i, "(< 2.2 2)"), "#f"); + EXPECT_EQ(e(i, "(< 2 2.2)"), "#f"); + + i.disable_printfs(); + for (auto x : {"(< 1)", "(<)", "(< a 1)", "(< 'a 1)", "(< 1 2 :test 3)", "(< 1 2 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, NumberLessThanEqual) { + Interpreter i; + EXPECT_EQ(e(i, "(<= 16 2)"), "#f"); + EXPECT_EQ(e(i, "(<= 2 2)"), "#t"); + EXPECT_EQ(e(i, "(<= 1 2)"), "#t"); + + EXPECT_EQ(e(i, "(<= 16.8 16.2)"), "#f"); + EXPECT_EQ(e(i, "(<= 2.2 2.2)"), "#t"); + EXPECT_EQ(e(i, "(<= 1.9 2.1)"), "#t"); + + EXPECT_EQ(e(i, "(<= 1.1 2)"), "#t"); + EXPECT_EQ(e(i, "(<= 1 2.1)"), "#t"); + EXPECT_EQ(e(i, "(<= 2.2 2)"), "#f"); + EXPECT_EQ(e(i, "(<= 2 2.2)"), "#t"); + + i.disable_printfs(); + for (auto x : {"(<= 1)", "(<=)", "(<= a 1)", "(<= 'a 1)", "(<= 1 2 :test 3)", "(<= 1 2 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, NumberGreaterThan) { + Interpreter i; + EXPECT_EQ(e(i, "(> 16 2)"), "#t"); + EXPECT_EQ(e(i, "(> 2 2)"), "#f"); + EXPECT_EQ(e(i, "(> 1 2)"), "#f"); + + EXPECT_EQ(e(i, "(> 16.8 16.2)"), "#t"); + EXPECT_EQ(e(i, "(> 2.2 2.2)"), "#f"); + EXPECT_EQ(e(i, "(> 1.9 2.1)"), "#f"); + + EXPECT_EQ(e(i, "(> 1.1 2)"), "#f"); + EXPECT_EQ(e(i, "(> 1 2.1)"), "#f"); + EXPECT_EQ(e(i, "(> 2.2 2)"), "#t"); + EXPECT_EQ(e(i, "(> 2 2.2)"), "#f"); + + i.disable_printfs(); + for (auto x : {"(> 1)", "(>)", "(> a 1)", "(> 'a 1)", "(> 1 2 :test 3)", "(> 1 2 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, NumberGreaterThanEqual) { + Interpreter i; + EXPECT_EQ(e(i, "(>= 16 2)"), "#t"); + EXPECT_EQ(e(i, "(>= 2 2)"), "#t"); + EXPECT_EQ(e(i, "(>= 1 2)"), "#f"); + + EXPECT_EQ(e(i, "(>= 16.8 16.2)"), "#t"); + EXPECT_EQ(e(i, "(>= 2.2 2.2)"), "#t"); + EXPECT_EQ(e(i, "(>= 1.9 2.1)"), "#f"); + + EXPECT_EQ(e(i, "(>= 1.1 2)"), "#f"); + EXPECT_EQ(e(i, "(>= 1 2.1)"), "#f"); + EXPECT_EQ(e(i, "(>= 2.2 2)"), "#t"); + EXPECT_EQ(e(i, "(>= 2 2.2)"), "#t"); + + i.disable_printfs(); + for (auto x : {"(>= 1)", "(>=)", "(>= a 1)", "(>= 'a 1)", "(>= 1 2 :test 3)", "(>= 1 2 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, Eval) { + Interpreter i; + EXPECT_EQ(e(i, "(eval '(+ 1 2))"), "3"); + + i.disable_printfs(); + for (auto x : {"(eval bad-to-evaluate-me)", "(eval)", "(eval 1 2)", "(eval :test 3)", + "(eval 1 :test 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, CarCdr) { + Interpreter i; + EXPECT_EQ(e(i, "(car '(3 . 4))"), "3"); + EXPECT_EQ(e(i, "(cdr '(3 . 4))"), "4"); + i.disable_printfs(); + for (auto x : {"(car)", "(car '(1 . 2) '(3 . 4))", "(cdr)", "(cdr '(1 . 2) '(3 . 4))"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, SetCarCdr) { + Interpreter i; + e(i, "(define x (cons 1 2))"); + EXPECT_EQ(e(i, "(set-car! x (+ 1 3))"), "(4 . 2)"); + EXPECT_EQ(e(i, "x"), "(4 . 2)"); + EXPECT_EQ(e(i, "(set-cdr! x (+ 2 3))"), "(4 . 5)"); + EXPECT_EQ(e(i, "x"), "(4 . 5)"); + i.disable_printfs(); + for (auto x : {"(set-car!)", "(set-car! '(1 . 2))", "(set-cdr!)", "(set-cdr! '(1 . 2))"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, GenSym) { + Interpreter i; + EXPECT_EQ(e(i, "(eq? (gensym) (gensym))"), "#f"); + i.disable_printfs(); + for (auto x : {"(gensym 1)", "(gensym :a 2)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, Cons) { + Interpreter i; + EXPECT_EQ(e(i, "(cons 'a 2)"), "(a . 2)"); + EXPECT_EQ(e(i, "(cons 'a (cons 2 (cons 3 '())))"), "(a 2 3)"); + i.disable_printfs(); + for (auto x : {"(cons 1)", "(cons)", "(const 1 2 3)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, Null) { + Interpreter i; + EXPECT_EQ(e(i, "(null? 1)"), "#f"); + EXPECT_EQ(e(i, "(null? '())"), "#t"); + i.disable_printfs(); + for (auto x : {"(null? 1 2)", "(null?)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, CurrentMethodType) { + Interpreter i; + i.goal_to_goos.enclosing_method_type = "test-type"; + EXPECT_EQ(e(i, "(current-method-type)"), "test-type"); + + i.disable_printfs(); + for (auto x : {"(current-method-type 1)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosBuiltins, Type) { + Interpreter i; + EXPECT_EQ(e(i, "(type? 'empty-list '())"), "#t"); + EXPECT_EQ(e(i, "(type? 'empty-list 1)"), "#f"); + + EXPECT_EQ(e(i, "(type? 'integer 2)"), "#t"); + EXPECT_EQ(e(i, "(type? 'integer '())"), "#f"); + + EXPECT_EQ(e(i, "(type? 'float 2.)"), "#t"); + EXPECT_EQ(e(i, "(type? 'float '())"), "#f"); + + EXPECT_EQ(e(i, "(type? 'char #\\c)"), "#t"); + EXPECT_EQ(e(i, "(type? 'char '())"), "#f"); + + EXPECT_EQ(e(i, "(type? 'symbol 'a)"), "#t"); + EXPECT_EQ(e(i, "(type? 'symbol '())"), "#f"); + + EXPECT_EQ(e(i, "(type? 'string \"test\")"), "#t"); + EXPECT_EQ(e(i, "(type? 'string '())"), "#f"); + + EXPECT_EQ(e(i, "(type? 'pair '(1 . 2))"), "#t"); + EXPECT_EQ(e(i, "(type? 'pair '())"), "#f"); + + EXPECT_EQ(e(i, "(type? 'array '#(1 2))"), "#t"); + EXPECT_EQ(e(i, "(type? 'array '())"), "#f"); + + EXPECT_EQ(e(i, "(type? 'lambda (lambda () 1))"), "#t"); + EXPECT_EQ(e(i, "(type? 'lambda '())"), "#f"); + + EXPECT_EQ(e(i, "(type? 'macro (macro () 1))"), "#t"); + EXPECT_EQ(e(i, "(type? 'macro '())"), "#f"); + + EXPECT_EQ(e(i, "(type? 'environment *global-env*)"), "#t"); + EXPECT_EQ(e(i, "(type? 'environment '())"), "#f"); +} + +/*! + * Confirm that the global and goos env variables appear + */ +TEST(GoosEval, GlobalAndGoalEnv) { + Interpreter i; + Object goal_env, goos_env; + EXPECT_TRUE(i.get_global_variable_by_name("*global-env*", &goos_env)); + EXPECT_TRUE(i.get_global_variable_by_name("*goal-env*", &goal_env)); + + auto& goal_vars = goal_env.as_env()->vars; + auto& goos_vars = goos_env.as_env()->vars; + + EXPECT_TRUE(goal_vars.find(i.intern("*global-env*").as_symbol()) != goal_vars.end()); + EXPECT_TRUE(goal_vars.find(i.intern("*goal-env*").as_symbol()) != goal_vars.end()); + EXPECT_TRUE(goos_vars.find(i.intern("*global-env*").as_symbol()) != goos_vars.end()); + EXPECT_TRUE(goos_vars.find(i.intern("*goal-env*").as_symbol()) != goos_vars.end()); + + EXPECT_TRUE(goos_vars.find(i.intern("*goal-env*").as_symbol())->second == + goal_vars.find(i.intern("*goal-env*").as_symbol())->second); + EXPECT_TRUE(goos_vars.find(i.intern("*global-env*").as_symbol())->second == + goal_vars.find(i.intern("*global-env*").as_symbol())->second); + EXPECT_TRUE(goos_vars.find(i.intern("*global-env*").as_symbol())->second != + goos_vars.find(i.intern("*goal-env*").as_symbol())->second); +} + +/*! + * Confirm that the GOOS Library is loaded automatically on interpreter start. + */ +TEST(GoosEval, GoosLibLoaded) { + Interpreter i; + Object loaded; + EXPECT_TRUE(i.get_global_variable_by_name("__goos-lib-loaded__", &loaded)); + EXPECT_EQ(loaded.print(), "#t"); +} + +/*! + * Check that integers are evaluated. + */ +TEST(GoosEval, EvalSelfEvaluating) { + Interpreter i; + EXPECT_EQ(e(i, "010"), "10"); + EXPECT_EQ(e(i, "-010"), "-10"); + EXPECT_EQ(e(i, "\"test\""), "\"test\""); + EXPECT_EQ(e(i, "1.2"), "1.200000"); // this depends on how we decide to print floats + EXPECT_EQ(e(i, "#\\a"), "#\\a"); + EXPECT_EQ(e(i, "#\\\\n"), "#\\\\n"); + + i.disable_printfs(); + EXPECT_ANY_THROW(e(i, "#\\\\a")); +} + +/*! + * Check named "keyword" arguments + */ +TEST(GoosEval, KeywordArgs) { + Interpreter i; + e(i, "(desfun test (a b &key c &key d) c)"); + EXPECT_EQ(e(i, "(test 1 2 :c 3 :d 4)"), "3"); + EXPECT_EQ(e(i, "(test 1 2 :d 3 :c 4)"), "4"); + + e(i, "(desfun test (a b &key c &key d) (+ b d))"); + EXPECT_EQ(e(i, "(test 1 2 :c 3 :d 4)"), "6"); + EXPECT_EQ(e(i, "(test 1 2 :d 3 :c 4)"), "5"); + EXPECT_EQ(e(i, "(test 1 :d 3 2 :c 4)"), "5"); + EXPECT_EQ(e(i, "(test :d 3 1 2 :c 4)"), "5"); + + i.disable_printfs(); + for (const auto& x : {"(test 1 2 :c 3 :d)", "(test 1 2 :c 3)", "(test 1 2 :c 3 :d 4 :e 3)", + "(test 1 2 :c 3 :d 4 :c 4)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosEval, KeywordArgsWithDefault) { + Interpreter i; + e(i, "(desfun test (a b &key (c 3) &key d) c)"); + + EXPECT_EQ(e(i, "(test 1 2 :c 7 :d 4)"), "7"); + EXPECT_EQ(e(i, "(test 1 2 :d 4)"), "3"); + + e(i, "(desfun test (a b &key (c 3) &key d) c)"); + i.disable_printfs(); + for (const auto& x : + {"(test 1 2 :c 3 :d)", "(test 1 2 :c :d 3)", "(test 1 2 :d 3 :c )", "(test 1 2 :c 3)", + "(test 1 2 :c 3 :d 4 :e 3)", "(test 1 2 :c 3 :d 4 :c 4)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosIntegrated, Begin) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define *test-value* (begin + (define x 10) +#| +asdfasdfasdfa +|# + (define y 20) + (set! x y) + x +; y + ) + ) +)"), + "20"); +} + +TEST(GoosIntegrated, Lambda1) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define x 10) + +(define f (lambda (x) ((lambda () x)))) + +(define *test-actual* (f 20)) +)"), + "20"); +} + +TEST(GoosIntegrated, Lambda2) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define make-returner (lambda (thing-to-return) + (lambda () thing-to-return))) + +(define my-func (make-returner "beans")) +(define fake-func (make-returner "cheese")) +(fake-func) +(define *test-actual* (fake-func)) +(set! *test-actual* (my-func)) +)"), + "\"beans\""); +} + +TEST(GoosIntegrated, Cond) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define s1 (cond + (#f a) + ((+ 2 3) (+ 3 4)) + (#t 3333) + (#t this-cannot-be-evaluated) + ) + ) + + +(define s2 (cond (2))) + +(define s3 (cond (#f a) + (#t (define s4 10) + (+ 1 1) + ) + ) + ) + +(define *test-actual* (+ s1 s2 s3 s4)) +)"), + "21"); +} + +TEST(GoosIntegrated, CountingChange) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define count-change + (lambda (amount) + (cc amount 5) + ) + ) + +(define cc + (lambda (amount kinds-of-coins) + (cond + ;; base case + ((= amount 0) 1) + ;; impossible + ((or (< amount 0) (= kinds-of-coins 0)) 0) + ;; otherwise + (#t (+ (cc amount (- kinds-of-coins 1)) + (cc (- amount (first-denom kinds-of-coins)) kinds-of-coins) + ) + ) + ) + ) + ) + + +(define first-denom + (lambda (kinds-of-coins) + (cond + ((= kinds-of-coins 1) 1) + ((= kinds-of-coins 2) 5) + ((= kinds-of-coins 3) 10) + ((= kinds-of-coins 4) 25) + ((= kinds-of-coins 5) 50) + ) + ) + ) + +(define *test-actual* (count-change 100)) +)"), + "292"); +} + +TEST(GoosIntegrated, Eval) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define *test-actual* + (eval '(+ 1 2) + ) + ) +)"), + "3"); +} + +TEST(GoosIntegrated, Read) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define *test-actual* + (eval (read "(+ 1 2) ") + ) + ) +)"), + "3"); +} + +// todo - test read? + +TEST(GoosIntegrated, LambdaWithRest) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define func (lambda (x &rest y) y)) +(define x (func -123 2 3 1)) + +(define *test-actual* (+ (car x) (car (cdr x)) (car (cdr (cdr x))))) +)"), + "6"); +} + +TEST(GoosIntegrated, IntegerEquality1) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define x 2) +(set! x 1) +(define *test-actual* (eq? x 2)) +)"), + "#f"); +} + +TEST(GoosIntegrated, IntegerEquality2) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define x 2) +(define *test-actual* (eq? x 2)) +)"), + "#t"); +} + +TEST(GoosIntegrated, Lambda3) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define my-func (lambda (a b c) b)) +(define *test-actual* (my-func 1 2 3)) +)"), + "2"); +} + +TEST(GoosIntegrated, Lambda4) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define func1 (lambda () 1)) +(define func2 (lambda () (func1))) +(set! func1 (lambda () 2)) +(define *test-actual* (func2)) +)"), + "2"); +} + +TEST(GoosIntegrated, Lambda5) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define x 10) +(define my-func (lambda (x) x)) +(define s1 (my-func 20)) +(define s2 x) +(define s3 ((lambda (x) x) 30)) +(define my-func-2 (lambda (x y) (set! y x) y)) +(define s4 (my-func-2 11 12)) +(define f3 (lambda (x) (set! x (+ 1 x)) x)) +(define s5 (f3 14)) +(define x ((lambda (x) (set! x (+ x 1)) x) 16)) +(define s6 x) +(define f1 (lambda (x) (+ x 1))) +(define f2 (lambda (x) (+ 2 (f1 x)))) +(define s7 (f2 (+ 1 2))) +(define *test-actual* (+ s1 s2 s3 s4 s5 s6 s7)) +)"), + "109"); +} + +TEST(GoosIntegrated, Let1) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define s1 + (let ((x (+ 1 2)) + (y (+ 3 4))) + (+ x y) + ) + ) + +(define var-1 2) + +(define s2 + (let ((x (lambda () var-1))) + (set! var-1 1000) + (x) + ) + ) + +(define *test-actual* (+ s1 s2)) +)"), + "1010"); +} + +TEST(GoosIntegrated, Let2) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define s1 + (let* ((x (+ 1 2)) + (y (+ x 5))) + (+ x y) + ) + ) + +(define var-1 2) + +(define s2 + (let ((x (lambda () var-1))) + (set! var-1 1000) + (x) + ) + ) + +(define x 0) +(define s3 + (let ((x (+ 1 2)) + (y x)) + y + ) + ) + +(define x 0) +(define s4 + (let* ((x (+ 1 2)) + (y x)) + y + ) + ) + +(define *test-actual* (+ s1 s2 s3 s4)) +)"), + "1014"); +} + +TEST(GoosIntegrated, WeirdSymbols) { + Interpreter i; + EXPECT_EQ(e(i, R"( +'+ +'- +'... +'!.. +'$.+ +'%.- +'&.! +'*.: +'/:. +;':+. ; forget this one, it gets recognized as a keyword argument to (quote). +'<-. +'=. +'>. +'?. +'~. +'_. +'^. +1 +)"), + "1"); +} + +TEST(GoosIntegrated, WhileLoop) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(define count 0) +(define sum 0) + +(while (< count 100) + (set! sum (+ sum count)) + (set! count (+ count 1)) + ) + +(define *test-actual* sum) +)"), + "4950"); +} + +TEST(GoosLib, Desfun) { + Interpreter i; + EXPECT_EQ(e(i, R"( +(desfun test-function (a b c &rest d) + (set! a (+ a b c)) + (cons a d) +) + +(test-function 1 2 3 4 5 6) + +)"), + "(6 4 5 6)"); +} + +TEST(GoosLib, Factorial) { + Interpreter i; + EXPECT_EQ(e(i, "(factorial 10)"), "3628800"); +} + +TEST(GoosLib, ApplySimple) { + Interpreter i; + EXPECT_EQ(e(i, "(apply (lambda (x) (* x 2)) '(1 2 3))"), "(2 4 6)"); +} + +TEST(GoosLib, ApplyComplex1) { + Interpreter i; + e(i, "(define y 2)"); + e(i, "(define test-func (lambda (x) (* x y)))"); + EXPECT_EQ(e(i, "(apply test-func '(1 2 3))"), "(2 4 6)"); +} +TEST(GoosLib, ApplyComplex2) { + Interpreter i; + e(i, "(define y 2)"); + e(i, "(define test-func (lambda (x) (* x y)))"); + e(i, "(set! y 4)"); + EXPECT_EQ(e(i, "(apply test-func '(1 2 3))"), "(4 8 12)"); +} + +TEST(GoosLib, ApplyComplex3) { + Interpreter i; + e(i, "(desfun make-mult-by-x-fun (x) (lambda (y) (* y x)))"); + e(i, "(define mult3 (make-mult-by-x-fun 3))"); + e(i, "(define mult2 (make-mult-by-x-fun 2))"); + EXPECT_EQ(e(i, "(apply mult2 '(1 2 3))"), "(2 4 6)"); + EXPECT_EQ(e(i, "(apply mult3 '(1 2 3))"), "(3 6 9)"); + EXPECT_EQ(e(i, "(apply mult2 '(1 2 3))"), "(2 4 6)"); +} + +TEST(GoosLib, Let) { + Interpreter i; + EXPECT_EQ(e(i, R"( + (let ((x 1) + (y 2) + (z 3)) + (+ x y z)) + +)"), + "6"); +} + +TEST(GoosLib, LetRec) { + Interpreter i; + EXPECT_EQ(e(i, R"( + (let* ((x 1) ; 1 + (y (+ 2 x)) ; 3 + (z (+ 3 y))) ; 6 + (+ x y z)) +)"), + "10"); +} + +/*! + * Test special case character printing. + */ +TEST(GoosObject, char_to_string) { + // printable + EXPECT_EQ("#\\d", goos::fixed_to_string('d')); + + // special ones + EXPECT_EQ("#\\\\s", goos::fixed_to_string(' ')); + EXPECT_EQ("#\\\\n", goos::fixed_to_string('\n')); + EXPECT_EQ("#\\\\t", goos::fixed_to_string('\t')); + + // decimal for ones we don't have special cased + EXPECT_EQ("#\\{17}", goos::fixed_to_string(char(17))); +} + +/*! + * Test the EmptyListObject + */ +TEST(GoosObject, EmptyList) { + // create two empty lists + Object nil = EmptyListObject::make_new(); + Object nil2 = EmptyListObject::make_new(); + + // check type is set + EXPECT_TRUE(nil.is_empty_list()); + + // check equality operator + EXPECT_TRUE(nil == nil2); + + // check we get the same heap allocated object + auto elo = std::dynamic_pointer_cast(nil.heap_obj); + auto elo2 = std::dynamic_pointer_cast(nil2.heap_obj); + EXPECT_TRUE(elo); + EXPECT_TRUE(elo == elo2); + + // check print and inspect + EXPECT_EQ(nil.print(), "()"); + EXPECT_EQ(nil.inspect(), "[empty list] ()\n"); +} + +/*! + * Test IntegerObject + */ +TEST(GoosObject, Integer) { + // create integer objects + Object io = Object::make_integer(-1234); + Object different = Object::make_integer(3); + Object same = Object::make_integer(-1234); + + // check type + EXPECT_TRUE(io.is_int()); + + // check equality + EXPECT_TRUE(same == io); + EXPECT_FALSE(different == io); + + // check changing the value through the as_int reference + different.as_int() = -1234; + EXPECT_TRUE(different.as_int() == -1234); + EXPECT_TRUE(different == same); + + // check print and inspect + EXPECT_EQ(different.print(), "-1234"); + EXPECT_EQ(different.inspect(), "[integer] -1234\n"); +} + +/*! + * Test FloatObject + */ +TEST(GoosObject, Float) { + // create integer objects + Object fo = Object::make_float(-12.34); + Object different = Object::make_float(3); + Object same = Object::make_float(-12.34); + + // check type + EXPECT_FALSE(fo.is_int()); + EXPECT_TRUE(fo.is_float()); + + // check equality + EXPECT_TRUE(same == fo); + EXPECT_FALSE(different == fo); + + // check changing the value through the as_float reference + different.as_float() = -12.34; + EXPECT_TRUE(different.as_float() == -12.34); + EXPECT_TRUE(different == same); + + // check print and inspect + EXPECT_EQ(different.print(), "-12.340000"); + EXPECT_EQ(different.inspect(), "[float] -12.340000\n"); +} + +/*! + * Test CharObject + */ +TEST(GoosObject, Char) { + // create integer objects + Object co = Object::make_char('w'); + Object different = Object::make_char('X'); + Object same = Object::make_char('w'); + + // check type + EXPECT_FALSE(co.is_int()); + EXPECT_TRUE(co.is_char()); + + // check equality + EXPECT_TRUE(same == co); + EXPECT_FALSE(different == co); + + // check changing the value through the as_char reference + different.as_char() = 'w'; + EXPECT_TRUE(different.as_char() == 'w'); + EXPECT_TRUE(different == same); + + // check print and inspect + EXPECT_EQ(different.print(), "#\\w"); + EXPECT_EQ(different.inspect(), "[char] #\\w\n"); +} + +/*! + * Test SymbolObject + */ +TEST(GoosObject, Symbol) { + SymbolTable st, st2; + Object obj = SymbolObject::make_new(st, "test1"); + Object obj2 = SymbolObject::make_new(st, "test2"); + Object obj3 = SymbolObject::make_new(st, "test1"); + Object obj4 = SymbolObject::make_new(st2, "test1"); + + // check type + EXPECT_TRUE(obj.is_symbol()); + + // check equality + EXPECT_TRUE(obj == obj3); + EXPECT_FALSE(obj == obj2); + EXPECT_FALSE(obj == obj4); // different because different st's + + // check interning works + auto obj_as = obj.as_symbol(); + auto obj2_as = obj2.as_symbol(); + auto obj3_as = obj3.as_symbol(); + auto obj4_as = obj4.as_symbol(); + + EXPECT_TRUE(obj_as == obj3_as); + EXPECT_FALSE(obj_as == obj2_as); + EXPECT_FALSE(obj_as == obj4_as); // different because different st's. + + // check print and inspect + EXPECT_EQ(obj.print(), "test1"); + EXPECT_EQ(obj.inspect(), "[symbol] test1\n"); +} + +/*! + * Test StringObject + */ +TEST(GoosObject, String) { + Object obj1 = StringObject::make_new("test1"); + Object obj2 = StringObject::make_new("test2"); + Object obj3 = StringObject::make_new("test1"); + + EXPECT_TRUE(obj1.is_string()); + + EXPECT_TRUE(obj1 == obj3); + EXPECT_FALSE(obj1 == obj2); + + obj2.as_string()->data = "test1"; + EXPECT_TRUE(obj1 == obj2); + + EXPECT_EQ(obj1.print(), "\"test1\""); + EXPECT_EQ(obj1.inspect(), "[string] \"test1\"\n"); +} + +/*! + * Test PairObject + */ +TEST(GoosObject, Pair) { + Object obj = PairObject::make_new(Object::make_integer(1), Object::make_integer(2)); + Object obj2 = PairObject::make_new(Object::make_integer(2), Object::make_integer(2)); + Object obj3 = PairObject::make_new(Object::make_integer(1), Object::make_integer(2)); + + EXPECT_TRUE(obj.is_pair()); + + EXPECT_TRUE(obj == obj3); + EXPECT_FALSE(obj == obj2); + obj2.as_pair()->car = Object::make_integer(1); + EXPECT_TRUE(obj == obj2); + + EXPECT_EQ(obj.print(), "(1 . 2)"); + EXPECT_EQ(obj.inspect(), "[pair] (1 . 2)\n"); +} + +/*! + * Test proper list stuff + */ +TEST(GoosObject, PairList) { + Object obj = build_list({}); + EXPECT_TRUE(obj.is_empty_list()); + + obj = build_list({Object::make_integer(1)}); + EXPECT_EQ(obj.print(), "(1)"); + + obj = build_list({Object::make_integer(1), Object::make_integer(2), Object::make_integer(3)}); + auto obj2 = + build_list({Object::make_integer(1), Object::make_integer(2), Object::make_integer(2)}); + auto obj3 = + build_list({Object::make_integer(1), Object::make_integer(2), Object::make_integer(3)}); + + EXPECT_EQ(obj.print(), "(1 2 3)"); + EXPECT_TRUE(obj == obj3); + EXPECT_FALSE(obj == obj2); +} + +/*! + * Test ArrayObject + */ +TEST(GoosObject, Array) { + Object empty_array = ArrayObject::make_new({}); + EXPECT_EQ(empty_array.as_array()->size(), 0); + EXPECT_TRUE(empty_array.is_array()); + EXPECT_EQ(empty_array.print(), "#()"); + + auto array1 = ArrayObject::make_new( + {Object::make_integer(1), Object::make_integer(2), Object::make_integer(3)}); + auto array2 = ArrayObject::make_new( + {Object::make_integer(1), Object::make_integer(2), Object::make_integer(4)}); + auto array3 = ArrayObject::make_new( + {Object::make_integer(1), Object::make_integer(2), Object::make_integer(3)}); + + EXPECT_TRUE(array1 == array3); + EXPECT_TRUE(array1 != array2); + + EXPECT_EQ(array1.print(), "#(1 2 3)"); + EXPECT_EQ(array1.inspect(), "[array] size: 3 data: #(1 2 3)\n"); +} + +TEST(GoosSpecialForms, Define) { + Interpreter i; + + e(i, "(define x 010)"); + EXPECT_EQ(e(i, "x"), "10"); + + e(i, "(define :env *goal-env* x 20)"); + EXPECT_EQ(e(i, "x"), "10"); + + Object goal_env; + EXPECT_TRUE(i.get_global_variable_by_name("*goal-env*", &goal_env)); + + auto x_in_goal_env = goal_env.as_env()->vars.find(i.intern("x").as_symbol()); + EXPECT_TRUE(x_in_goal_env != goal_env.as_env()->vars.end()); + EXPECT_EQ(x_in_goal_env->second.print(), "20"); + + // test automatic environment of define + e(i, "(begin (desfun test-define () (define x 500)) (test-define))"); + EXPECT_EQ(e(i, "x"), "10"); + + // test manual setting of global env for define + e(i, "(begin (desfun test-define () (define :env *global-env* x 500)) (test-define))"); + EXPECT_EQ(e(i, "x"), "500"); + + e(i, "(begin (desfun test-define () (define x :env *global-env* 600)) (test-define))"); + EXPECT_EQ(e(i, "x"), "600"); + + e(i, "(begin (desfun test-define () (define x 700 :env *global-env*)) (test-define))"); + EXPECT_EQ(e(i, "x"), "700"); + + i.disable_printfs(); + EXPECT_ANY_THROW(e(i, "(define :env 3 x 10)")); + EXPECT_ANY_THROW(e(i, "(define :beans 3 x 10)")); + EXPECT_ANY_THROW(e(i, "(define x)")); + EXPECT_ANY_THROW(e(i, "(define x 1 2)")); + EXPECT_ANY_THROW(e(i, "(define 1)")); + EXPECT_ANY_THROW(e(i, "(define 1 2)")); + EXPECT_ANY_THROW(e(i, "(define)")); +} + +TEST(GoosSpecialForms, Set) { + Interpreter i; + e(i, "(define x 10)"); + EXPECT_EQ(e(i, "(set! x (+ 10 10))"), "20"); + EXPECT_EQ(e(i, "x"), "20"); + + // set parent env + EXPECT_EQ(e(i, "(begin (desfun test-set () (set! x 30)) (test-set))"), "30"); + EXPECT_EQ(e(i, "x"), "30"); + + // check non-global env (also a good check with two different variables named x) + EXPECT_EQ(e(i, "(begin (desfun test-set (x) (set! x (+ x 30))) (test-set x))"), "60"); + EXPECT_EQ(e(i, "x"), "30"); + + i.disable_printfs(); + for (auto x : + {"(set!)", "(set! x)", "(set! 1)", "(set! 1 1)", "(set! x 1 2)", "(set! x 1 :test 2)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosSpecialForms, Quote) { + Interpreter i; + e(i, "(define x 'y)"); + EXPECT_EQ(e(i, "x"), "y"); + e(i, "(define x (quote z))"); + EXPECT_EQ(e(i, "x"), "z"); + e(i, "(define x '(1 2 3))"); + EXPECT_EQ(e(i, "x"), "(1 2 3)"); + e(i, "(define x '(1 2 3 ,w))"); + EXPECT_EQ(e(i, "x"), "(1 2 3 (unquote w))"); + + i.disable_printfs(); + for (auto x : {"(quote)", "(quote 1 2)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosSpecialForms, QuasiQuote) { + Interpreter i; + e(i, "(define x 'y)"); + EXPECT_EQ(e(i, "(define z `(x ,x z))"), "(x y z)"); + i.disable_printfs(); + for (auto x : {"(quasiquote)", "(quasiquote 1 2)", "(unquote 1)", "(unquote-spicing 1)", + "`( (unquote-splicing) 2)", "`( (unquote-splicing 1 2) 2)", + "`( (unquote-splicing 1) 2)", "`( (unquote) 2)", "`( (unquote 1 2) 2)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosSpecialForms, QuasiQuoteSplicing) { + Interpreter i; + e(i, "(define x '(1 2 3))"); + EXPECT_EQ(e(i, "(define z `(x ,x z))"), "(x (1 2 3) z)"); + EXPECT_EQ(e(i, "(define z `(x ,@x z))"), "(x 1 2 3 z)"); +} + +TEST(GoosSpecialForms, Or) { + Interpreter i; + e(i, "(desfun rf () #f)"); + e(i, "(desfun r1 () 1)"); + EXPECT_EQ(e(i, "(or #f (rf) #f)"), "#f"); + EXPECT_EQ(e(i, "(or #f (rf) (r1) 2 #f)"), "1"); + EXPECT_EQ(e(i, "(or #f (rf) 1 2 cannot-be-evaluated)"), "1"); + + i.disable_printfs(); + for (auto x : {"(or)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} + +TEST(GoosSpecialForms, And) { + Interpreter i; + e(i, "(desfun rf () #f)"); + e(i, "(desfun r1 () 1)"); + EXPECT_EQ(e(i, "(and #f (rf) #f)"), "#f"); + EXPECT_EQ(e(i, "(and (r1) 2 3 4 3 #t 3 2)"), "2"); + EXPECT_EQ(e(i, "(and #f (rf) 1 2 cannot-be-evaluated 2 3 4)"), "#f"); + + i.disable_printfs(); + for (auto x : {"(and)"}) { + EXPECT_ANY_THROW(e(i, x)); + } +} \ No newline at end of file diff --git a/test/test_goos_file0.gs b/test/test_goos_file0.gs new file mode 100644 index 0000000000..5bc2264eaa --- /dev/null +++ b/test/test_goos_file0.gs @@ -0,0 +1 @@ +(define x 23) \ No newline at end of file diff --git a/test/test_kernel.cpp b/test/test_kernel.cpp new file mode 100644 index 0000000000..064449aed9 --- /dev/null +++ b/test/test_kernel.cpp @@ -0,0 +1,389 @@ +#include +#include +#include +#include "gtest/gtest.h" +#include "common/symbols.h" +#include "game/kernel/fileio.h" +#include "game/kernel/kboot.h" +#include "game/kernel/kprint.h" +#include "game/kernel/kdsnetm.h" +#include "game/kernel/kscheme.h" +#include "all_jak1_symbols.h" + +TEST(Kernel, strend) { + char test[] = "test"; + char* end = strend(test); + EXPECT_TRUE(*end == 0); + EXPECT_TRUE(end - test == 4); +} + +TEST(Kernel, kstrcpy) { + char buffer[24]; + memset(buffer, 1, 24); + kstrcpy(buffer, "test"); + EXPECT_TRUE(buffer[0] == 't'); + EXPECT_TRUE(buffer[1] == 'e'); + EXPECT_TRUE(buffer[2] == 's'); + EXPECT_TRUE(buffer[3] == 't'); + EXPECT_TRUE(buffer[4] == '\0'); + EXPECT_TRUE(buffer[5] == 1); +} + +TEST(Kernel, kstrcpyup) { + char buffer[24]; + memset(buffer, 1, 24); + kstrcpyup(buffer, "tesT"); + EXPECT_TRUE(buffer[0] == 'T'); + EXPECT_TRUE(buffer[1] == 'E'); + EXPECT_TRUE(buffer[2] == 'S'); + EXPECT_TRUE(buffer[3] == 'T'); + EXPECT_TRUE(buffer[4] == '\0'); + EXPECT_TRUE(buffer[5] == 1); +} + +TEST(Kernel, kstrcat) { + char buffer[24] = "test"; + kstrcat(buffer, "cat"); + const char expected[] = "testcat"; + for (int i = 0; i < 8; i++) { + EXPECT_TRUE(expected[i] == buffer[i]); + } +} + +TEST(Kernel, kstrncat) { + { + char buffer[24] = "test"; + kstrncat(buffer, "cat", 6); + const char expected[] = "testca"; + for (int i = 0; i < 7; i++) { + EXPECT_TRUE(expected[i] == buffer[i]); + } + } + + { + char buffer[24] = "test"; + kstrncat(buffer, "cat", 80); + const char expected[] = "testcat"; + for (int i = 0; i < 8; i++) { + EXPECT_TRUE(expected[i] == buffer[i]); + } + } +} + +TEST(Kernel, kstrinsert) { + char buffer[24]; + memset(buffer, 1, 24); + buffer[0] = 't'; + buffer[1] = 'o'; + buffer[2] = '\0'; + + EXPECT_TRUE(buffer == kstrinsert(buffer, 'a', 2)); + + EXPECT_TRUE(buffer[0] == 'a'); + EXPECT_TRUE(buffer[1] == 'a'); + EXPECT_TRUE(buffer[2] == 't'); + EXPECT_TRUE(buffer[3] == 'o'); + EXPECT_TRUE(buffer[4] == '\0'); + EXPECT_TRUE(buffer[5] == 1); +} + +TEST(Kernel, basename) { + std::string x; + + char name0[] = "test1.asdf"; + x = basename_goal(name0); + EXPECT_EQ(x, "test1.asdf"); + + char name1[] = "a/b/test1.asdf"; + x = basename_goal(name1); + EXPECT_EQ(x, "test1.asdf"); + + char name2[] = "a\\b\\test1.asdf"; + x = basename_goal(name2); + EXPECT_EQ(x, "test1.asdf"); +} + +TEST(Kernel, DecodeFileName) { + std::string x; + x = DecodeFileName("$TEXTURE/beans"); + EXPECT_EQ(x, "host:data/texture-page7/beans.go"); + + x = DecodeFileName("$ART_GROUP/stuff"); + EXPECT_EQ(x, "host:data/art-group6/stuff-ag.go"); + + x = DecodeFileName("$LEVEL/my-level"); + EXPECT_EQ(x, "host:data/level30/my-level-bt.go"); + + x = DecodeFileName("$LEVEL/my-level.123"); + EXPECT_EQ(x, "host:data/level30/my-level.123"); + + x = DecodeFileName("$DATA/my-data"); + EXPECT_EQ(x, "host:data/my-data.go"); + + x = DecodeFileName("$CODE/my-code"); + EXPECT_EQ(x, "host:game/obj/my-code.o"); + + x = DecodeFileName("$RES/my-res"); + EXPECT_EQ(x, "host:data/res1/my-res.go"); + + x = DecodeFileName("asdf"); + EXPECT_EQ(x, "host:game/obj/asdf.o"); +} + +TEST(Kernel, reverse) { + char in1[] = "asdf"; + reverse(in1); + char in2[] = "asdfg"; + reverse(in2); + EXPECT_EQ("fdsa", std::string(in1)); + EXPECT_EQ("gfdsa", std::string(in2)); +} + +TEST(Kernel, ftoa) { + char buffer[128]; + ftoa(buffer, 1.23, 1, ' ', 4, 0); + EXPECT_EQ("1.2300", std::string(buffer)); + + ftoa(buffer, -1.23, 1, ' ', 4, 0); + EXPECT_EQ("-1.2300", std::string(buffer)); + + ftoa(buffer, 1., 1, ' ', 4, 0); + EXPECT_EQ("1.0000", std::string(buffer)); + + ftoa(buffer, 1., 1, ' ', 0, 0); + EXPECT_EQ("1", std::string(buffer)); + + ftoa(buffer, 1., 1, ' ', 1, 0); + EXPECT_EQ("1.0", std::string(buffer)); + + ftoa(buffer, 0.f / 0.f, 1, ' ', 4, 0); + EXPECT_EQ("NaN", std::string(buffer)); + + ftoa(buffer, 1., 8, ' ', 1, 0); + EXPECT_EQ(" 1.0", std::string(buffer)); + + ftoa(buffer, -1., 8, '0', 1, 0); + EXPECT_EQ("0000-1.0", std::string(buffer)); + + ftoa(buffer, 0.f / 0.f, 8, ' ', 4, 0); + EXPECT_EQ(" NaN", std::string(buffer)); + + ftoa(buffer, 0.1, 1, ' ', 4, 0); + EXPECT_EQ("0.1000", std::string(buffer)); +} + +TEST(Kernel, itoa_base_10) { + char buffer[128]; + + kprint_init_globals(); + + // simple print 1 + kitoa(buffer, 1, 10, -1, ' ', 0); + EXPECT_EQ("1", std::string(buffer)); + + // simple print 0 + kitoa(buffer, 0, 10, -1, ' ', 0); + EXPECT_EQ("0", std::string(buffer)); + + // simple print -1 + kitoa(buffer, -1, 10, -1, ' ', 0); + EXPECT_EQ("-1", std::string(buffer)); + + // print negative which asks to be truncated, but shouldn't be + kitoa(buffer, -123456, 10, 4, ' ', 0); + EXPECT_EQ("-123456", std::string(buffer)); + + // print an interesting number + kitoa(buffer, 123321456789, 10, -1, ' ', 0); + EXPECT_EQ("123321456789", std::string(buffer)); + + // MAX + kitoa(buffer, INT64_MAX, 10, -1, ' ', 0); + EXPECT_EQ("9223372036854775807", std::string(buffer)); + + // MIN + kitoa(buffer, INT64_MIN, 10, -1, ' ', 0); + EXPECT_EQ("-9223372036854775808", std::string(buffer)); + + // Pad + kitoa(buffer, 3, 10, 4, 'j', 0); + EXPECT_EQ("jjj3", std::string(buffer)); + + kitoa(buffer, 333, 10, 4, 'j', 0); + EXPECT_EQ("j333", std::string(buffer)); + + kitoa(buffer, 3333, 10, 4, 'j', 0); + EXPECT_EQ("3333", std::string(buffer)); + + kitoa(buffer, 33333, 10, 4, 'j', 0); + EXPECT_EQ("33333", std::string(buffer)); +} + +TEST(Kernel, itoa_base_2) { + char buffer[128]; + + kprint_init_globals(); + kitoa(buffer, 1, 2, -1, ' ', 0); + EXPECT_EQ("1", std::string(buffer)); + + kitoa(buffer, -1, 2, -1, ' ', 0); + EXPECT_EQ("1111111111111111111111111111111111111111111111111111111111111111", + std::string(buffer)); + + kitoa(buffer, -1, 2, 0, ' ', 0); + EXPECT_EQ("1111111111111111111111111111111111111111111111111111111111111111", + std::string(buffer)); + + kitoa(buffer, -1, 2, 5, ' ', 0); + EXPECT_EQ("-11111", std::string(buffer)); + + kitoa(buffer, 0, 2, -1, ' ', 0); + EXPECT_EQ("0", std::string(buffer)); + + kitoa(buffer, INT64_MAX, 2, -1, ' ', 0); + EXPECT_EQ("111111111111111111111111111111111111111111111111111111111111111", std::string(buffer)); + + kitoa(buffer, 476, 2, -1, ' ', 0); + EXPECT_EQ("111011100", std::string(buffer)); + + kitoa(buffer, 476, 2, 11, 'j', 0); + EXPECT_EQ("jj111011100", std::string(buffer)); + + kitoa(buffer, INT64_MIN, 2, 0, ' ', 0); + EXPECT_EQ("1000000000000000000000000000000000000000000000000000000000000000", + std::string(buffer)); +} + +TEST(Kernel, itoa_base_16) { + char buffer[128]; + + kprint_init_globals(); + kitoa(buffer, 1, 16, -1, ' ', 0); + EXPECT_EQ("1", std::string(buffer)); + + kitoa(buffer, -1, 16, -1, ' ', 0); + EXPECT_EQ("ffffffffffffffff", + std::string(buffer)); + + kitoa(buffer, -1, 16, 5, ' ', 0); + EXPECT_EQ("-fffff", std::string(buffer)); + + kitoa(buffer, 0, 16, -1, ' ', 0); + EXPECT_EQ("0", std::string(buffer)); + + kitoa(buffer, INT64_MAX, 16, -1, ' ', 0); + EXPECT_EQ("7fffffffffffffff", std::string(buffer)); + + kitoa(buffer, 195935983, 16, -1, ' ', 0); + EXPECT_EQ("badbeef", std::string(buffer)); + + kitoa(buffer, 195935983, 16, 11, 'j', 0); + EXPECT_EQ("jjjjbadbeef", std::string(buffer)); + + kitoa(buffer, INT64_MIN, 16, 0, ' ', 0); + EXPECT_EQ("8000000000000000", + std::string(buffer)); +} + +namespace { +void setup_hack_heaps(void* mem, int size) { + g_ee_main_mem = (u8*)mem; + int heap_size = (size - HEAP_START) / 2; + MasterDebug = 1; + EXPECT_TRUE(heap_size > 8 * 1024 * 1024); + kmalloc_init_globals(); + kprint_init_globals(); + + kinitheap(kglobalheap, Ptr(HEAP_START), heap_size); + kinitheap(kdebugheap, Ptr(HEAP_START + heap_size), heap_size); + init_output(); + init_crc(); + + // create a fake symbol table + auto symbol_table = kmalloc(kglobalheap, 0x20000, KMALLOC_MEMSET, "symbol-table").cast(); + s7 = symbol_table + (GOAL_MAX_SYMBOLS / 2) * 8 + BASIC_OFFSET; + SymbolTable2 = symbol_table + BASIC_OFFSET; + LastSymbol = symbol_table + 0xff00; + NumSymbols = 0; + + // set up the empty pair (might not be needed?) + *(s7 + FIX_SYM_EMPTY_CAR) = (s7 + FIX_SYM_EMPTY_PAIR).offset; + *(s7 + FIX_SYM_EMPTY_CDR) = (s7 + FIX_SYM_EMPTY_PAIR).offset; + + // need to set up 'global fixed symbol so allocating memory works. + *(s7 + FIX_SYM_GLOBAL_HEAP) = kglobalheap.offset; + + // allocate fundamental types + alloc_and_init_type((s7 + FIX_SYM_TYPE_TYPE).cast(), 9); + alloc_and_init_type((s7 + FIX_SYM_SYMBOL_TYPE).cast(), 9); + alloc_and_init_type((s7 + FIX_SYM_STRING_TYPE).cast(), 9); + alloc_and_init_type((s7 + FIX_SYM_FUNCTION_TYPE).cast(), 9); + // booleans + set_fixed_symbol(FIX_SYM_FALSE, "#f", s7.offset + FIX_SYM_FALSE); + set_fixed_symbol(FIX_SYM_TRUE, "#t", s7.offset + FIX_SYM_TRUE); + // heap symbols + set_fixed_symbol(FIX_SYM_GLOBAL_HEAP, "global", kglobalheap.offset); +} +} // namespace + +TEST(Kernel, PrintBuffer) { + // hack to setup + constexpr int size = 32 * 1024 * 1024; + auto mem = new u8[size]; + setup_hack_heaps(mem, size); + clear_print(); + cprintf("test!\n"); + + std::string result = PrintBufArea.cast().c() + sizeof(GoalMessageHeader); + EXPECT_EQ(result, "test!\n"); + + delete[] mem; + + // more complicated tests for format will be done from within GOAL. +} + + +TEST(Kernel, HashTable) { + constexpr int size = 32 * 1024 * 1024; + auto mem = new u8[size]; + setup_hack_heaps(mem, size); + + // check crc32 - the game has a hardcoded hash for _empty_ so we should check + // that our implementation matches this hardcoded value. + + const char empty_sym_name[] = "_empty_"; + const char wrong_sym_name[] = "_Empty_"; + EXPECT_EQ(EMPTY_HASH, crc32((const u8*)empty_sym_name, strlen(empty_sym_name))); + EXPECT_NE(EMPTY_HASH, crc32((const u8*)wrong_sym_name, strlen(wrong_sym_name))); + + std::unordered_map symbol_locations; + std::unordered_set unique_locations; + + for(auto name : all_syms) { + auto loc = intern_from_c(name).offset; + symbol_locations[name] = loc; + unique_locations.insert(loc); + } + + EXPECT_EQ(7941, symbol_locations.size()); + EXPECT_EQ(7941, unique_locations.size()); + + for(auto name : all_syms) { + EXPECT_EQ(symbol_locations.at(name), intern_from_c(name).offset); + } + + EXPECT_EQ(intern_from_c("global").offset - s7.offset, FIX_SYM_GLOBAL_HEAP); + EXPECT_EQ(intern_from_c("#f").offset - s7.offset, 0); + EXPECT_EQ(intern_from_c("#t").offset - s7.offset, 8); + EXPECT_EQ(intern_from_c("_empty_").offset - s7.offset, FIX_SYM_EMPTY_PAIR); + + // expect no crc32 hash collisions. This doesn't matter, but it's nice to know. + std::unordered_set crc32s; + for(auto name : all_syms) { + crc32s.insert(crc32((const u8*)name, strlen(name))); + } + EXPECT_EQ(7941, crc32s.size()); + + delete[] mem; +} + diff --git a/test/test_listener_deci2.cpp b/test/test_listener_deci2.cpp new file mode 100644 index 0000000000..4efa4d5bd9 --- /dev/null +++ b/test/test_listener_deci2.cpp @@ -0,0 +1,122 @@ +#include "gtest/gtest.h" +#include "goalc/listener/Listener.h" +#include "goalc/listener/Deci2Server.h" + +using namespace listener; + +namespace { +bool always_false() { + return false; +} +} + +TEST(Listener, ListenerCreation) { + listener::Listener l; +} + +TEST(Listener, DeciCreation) { + Deci2Server s(always_false); +} + +TEST(Listener, DeciInit) { + Deci2Server s(always_false); + EXPECT_TRUE(s.init()); +} + +//TEST(Listener, TwoDeciServers) { +// Deci2Server s1, s2; +// EXPECT_TRUE(s1.init()); +// EXPECT_TRUE(s2.init()); +//} + +/*! + * Try to connect when no Deci2Server is running + */ +TEST(Listener, ListenToNothing) { + Listener l; + EXPECT_FALSE(l.connect_to_target()); + l.disconnect(); +} + +TEST(Listener, DeciCheckNoListener) { + Deci2Server s(always_false); + EXPECT_TRUE(s.init()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_FALSE(s.check_for_listener()); +} + +TEST(Listener, DeciThenListener) { + for(int i = 0; i < 10; i++) { + Deci2Server s(always_false); + EXPECT_TRUE(s.init()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_FALSE(s.check_for_listener()); + Listener l; + EXPECT_FALSE(s.check_for_listener()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_TRUE(l.connect_to_target()); + // kind of a hack. + while(!s.check_for_listener()) { + printf("...\n"); + } + + EXPECT_TRUE(s.check_for_listener()); + } +} + +TEST(Listener, DeciThenListener2) { + for(int i = 0; i < 10; i++) { + Deci2Server s(always_false); + EXPECT_TRUE(s.init()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_FALSE(s.check_for_listener()); + Listener l; + EXPECT_FALSE(s.check_for_listener()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_TRUE(l.connect_to_target()); + } +} + +TEST(Listener, ListenerThenDeci) { + for(int i = 0; i < 10; i++) { + Listener l; + EXPECT_FALSE(l.connect_to_target()); + Deci2Server s(always_false); + EXPECT_TRUE(s.init()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_TRUE(l.connect_to_target()); + while(!s.check_for_listener()) { + printf("...\n"); + } + } +} + +TEST(Listener, ListenerMultipleDecis) { + Listener l; + EXPECT_FALSE(l.connect_to_target()); + { + Deci2Server s(always_false); + EXPECT_TRUE(s.init()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_TRUE(l.connect_to_target()); + while(!s.check_for_listener()) { + printf("...\n"); + } + l.disconnect(); + } + + { + Deci2Server s(always_false); + EXPECT_TRUE(s.init()); + EXPECT_FALSE(s.check_for_listener()); + EXPECT_TRUE(l.connect_to_target()); + while(!s.check_for_listener()) { + printf("...\n"); + } + l.disconnect(); + } + +} \ No newline at end of file diff --git a/test/test_main.cpp b/test/test_main.cpp new file mode 100644 index 0000000000..9a17845d82 --- /dev/null +++ b/test/test_main.cpp @@ -0,0 +1,6 @@ +#include "gtest/gtest.h" + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/test/test_reader.cpp b/test/test_reader.cpp new file mode 100644 index 0000000000..b49eda1242 --- /dev/null +++ b/test/test_reader.cpp @@ -0,0 +1,332 @@ +/*! + * @file test_reader.cpp + * Test the reader. + * For some reason this runs at ~5 fps in CLion IDE. + */ + +#include "gtest/gtest.h" +#include "goalc/goos/Reader.h" +#include "goalc/util/file_io.h" + +using namespace goos; + +TEST(GoosReader, Construction) { + // test that reader is able to find the source directory + Reader reader; +} + +namespace { +bool check_first_integer(Object o, int64_t x) { + return o.as_pair()->cdr.as_pair()->car.as_int() == x; +} + +bool check_first_float(Object o, double x) { + return o.as_pair()->cdr.as_pair()->car.as_float() == x; +} + +bool check_first_symbol(Object o, const std::string& sym) { + return o.as_pair()->cdr.as_pair()->car.as_symbol()->name == sym; +} + +bool check_first_string(Object o, const std::string& str) { + return o.as_pair()->cdr.as_pair()->car.as_string()->data == str; +} +} // namespace + +TEST(GoosReader, Integer) { + Reader reader; + EXPECT_TRUE(check_first_integer(reader.read_from_string("123"), 123)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("1"), 1)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("-1"), -1)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("0"), 0)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("9223372036854775807"), INT64_MAX)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("-9223372036854775808"), INT64_MIN)); + + EXPECT_TRUE(check_first_integer(reader.read_from_string("-0"), 0)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("-000000"), 0)); + + EXPECT_TRUE(check_first_integer( + reader.read_from_string( + "-0000000000000000000000000000000000000000000000000000000000000000000000000000000001"), + -1)); + + EXPECT_TRUE(reader.read_from_string("--0").as_pair()->cdr.as_pair()->car.is_symbol()); + // too big or too small. + EXPECT_ANY_THROW(reader.read_from_string("9223372036854775808")); + EXPECT_ANY_THROW(reader.read_from_string("-9223372036854775809")); +} + +TEST(GoosReader, Hex) { + Reader reader; + EXPECT_TRUE(check_first_integer(reader.read_from_string("#x0"), 0)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#x1"), 1)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#xf"), 15)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#xF"), 15)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#x0F"), 15)); + EXPECT_TRUE(check_first_integer( + reader.read_from_string("#x0000000000000000000000000000000000000000000000000000f"), 15)); + + EXPECT_TRUE(check_first_integer(reader.read_from_string("#xffffffff"), UINT32_MAX)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#x100000000"), (1LL << 32LL))); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#x7FFFFFFFFFFFFFFF"), INT64_MAX)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#x8000000000000000"), INT64_MIN)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#xffffffffffffffff"), -1)); + + EXPECT_ANY_THROW(reader.read_from_string("#x10000000000000000")); + + EXPECT_TRUE(check_first_symbol(reader.read_from_string("#x"), "#x")); + EXPECT_TRUE(check_first_symbol(reader.read_from_string("#x-1"), "#x-1")); + EXPECT_TRUE(check_first_symbol(reader.read_from_string("#x.1"), "#x.1")); +} + +TEST(GoosReader, Binary) { + Reader reader; + + EXPECT_TRUE(check_first_integer(reader.read_from_string("#b0"), 0)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#b0000000000"), 0)); + EXPECT_TRUE(check_first_integer( + reader.read_from_string("#b000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000"), + 0)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#b1"), 1)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#b10"), 2)); + EXPECT_TRUE(check_first_integer(reader.read_from_string("#b01011"), 11)); + EXPECT_TRUE(check_first_integer( + reader.read_from_string("#b1111111111111111111111111111111111111111111111111111111111111111"), + -1)); + EXPECT_TRUE(check_first_integer( + reader.read_from_string( + "#b000001111111111111111111111111111111111111111111111111111111111111111"), + -1)); + + EXPECT_TRUE(check_first_integer( + reader.read_from_string("#b0111111111111111111111111111111111111111111111111111111111111111"), + INT64_MAX)); + EXPECT_TRUE(check_first_integer( + reader.read_from_string("#b1000000000000000000000000000000000000000000000000000000000000000"), + INT64_MIN)); + + EXPECT_ANY_THROW(reader.read_from_string( + "#b11111111111111111111111111111111111111111111111111111111111111111")); + + EXPECT_TRUE(check_first_symbol(reader.read_from_string("#b"), "#b")); + EXPECT_TRUE(check_first_symbol(reader.read_from_string("#b-1"), "#b-1")); + EXPECT_TRUE(check_first_symbol(reader.read_from_string("#b.1"), "#b.1")); +} + +TEST(GoosReader, Float) { + Reader reader; + + EXPECT_TRUE(check_first_float(reader.read_from_string("1.6"), 1.6)); + EXPECT_TRUE(check_first_float(reader.read_from_string("0000001.6"), 1.6)); + EXPECT_TRUE(check_first_float(reader.read_from_string("0.6"), 0.6)); + EXPECT_TRUE(check_first_float(reader.read_from_string("00000.6"), 0.6)); + EXPECT_TRUE(check_first_float(reader.read_from_string("-0.6"), -0.6)); + EXPECT_TRUE(check_first_float(reader.read_from_string("-000000.6"), -0.6)); + EXPECT_TRUE(check_first_float(reader.read_from_string("-.6"), -.6)); + + EXPECT_TRUE(check_first_float(reader.read_from_string("1."), 1)); + EXPECT_TRUE(check_first_float(reader.read_from_string("1.0"), 1)); + EXPECT_TRUE(check_first_float(reader.read_from_string("01."), 1)); + EXPECT_TRUE(check_first_float(reader.read_from_string("01.0"), 1)); + + EXPECT_TRUE(check_first_float(reader.read_from_string("0."), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string(".0"), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("0.0"), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("000."), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string(".000"), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("0.000"), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("000.0"), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("000.0000"), 0)); + + EXPECT_TRUE(check_first_float(reader.read_from_string("-0."), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("-.0"), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("-0.0"), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("-000."), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("-.000"), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("-0.000"), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("-000.0"), 0)); + EXPECT_TRUE(check_first_float(reader.read_from_string("-000.0000"), 0)); + + EXPECT_TRUE(check_first_symbol(reader.read_from_string("1e0"), "1e0")); + EXPECT_ANY_THROW(reader.read_from_string(".")); +} + +TEST(GoosReader, Boolean) { + Reader reader; + EXPECT_TRUE(check_first_symbol(reader.read_from_string("#f"), "#f")); + EXPECT_TRUE(check_first_symbol(reader.read_from_string("#t"), "#t")); +} + +TEST(GoosReader, String) { + Reader reader; + EXPECT_TRUE( + check_first_string(reader.read_from_string("\"testing string ()\""), "testing string ()")); + EXPECT_TRUE(check_first_string(reader.read_from_string("\"\""), "")); + EXPECT_TRUE(check_first_string(reader.read_from_string("\" \\t \""), " \t ")); + EXPECT_TRUE(check_first_string(reader.read_from_string("\" \\n \""), " \n ")); + EXPECT_TRUE(check_first_string(reader.read_from_string("\"test \\n\""), "test \n")); + EXPECT_TRUE(check_first_string(reader.read_from_string("\" \\\\ \""), " \\ ")); + EXPECT_ANY_THROW(reader.read_from_string("\"\\\"")); // "\" invalid escape + EXPECT_ANY_THROW(reader.read_from_string("\"\\w\"")); // "\w" invalid escape +} + +TEST(GoosReader, Symbol) { + std::vector test_symbols = { + "test", "test-two", "__werid-sym__", "-a", "-", "/", "*", "+", "a", "#f"}; + + Reader reader; + + for (const auto& sym : test_symbols) { + EXPECT_TRUE(check_first_symbol(reader.read_from_string(sym), sym)); + } +} + +namespace { +bool first_list_matches(Object o, std::vector stuff) { + auto lst = o.as_pair()->cdr.as_pair()->car; + for (const auto& x : stuff) { + const auto check = x.as_pair()->cdr.as_pair()->car; + if (lst.as_pair()->car != check) { + return false; + } + lst = lst.as_pair()->cdr; + } + + return lst.is_empty_list(); +} + +bool first_array_matches(Object o, std::vector stuff) { + auto array = o.as_pair()->cdr.as_pair()->car.as_array(); + if (stuff.size() != array->size()) { + return false; + } + + for (size_t i = 0; i < array->size(); i++) { + if ((*array)[i] != stuff.at(i)) { + return false; + } + } + return true; +} + +bool first_pair_matches(Object o, Object car, Object cdr) { + auto lst = o.as_pair()->cdr.as_pair()->car; + return (lst.as_pair()->car == car) && (lst.as_pair()->cdr == cdr); +} + +bool print_matches(Object o, std::string expected) { + return o.as_pair()->cdr.as_pair()->car.print() == expected; +} + +bool first_char_matches(Object o, char c) { + return o.as_pair()->cdr.as_pair()->car.as_char() == c; +} + +} // namespace + +TEST(GoosReader, List) { + Reader reader; + auto r = [&](std::string s) { return reader.read_from_string(s); }; + EXPECT_TRUE(first_list_matches(r("()"), {})); + EXPECT_TRUE(first_list_matches(r("(1)"), {r("1")})); + EXPECT_TRUE(first_list_matches(r(" ( 1 ) "), {r("1")})); + EXPECT_TRUE(first_list_matches(r("(1 2 3)"), {r("1"), r("2"), r("3")})); + EXPECT_TRUE(first_list_matches(r(" ( 1 bbbb 3 ) "), {r("1"), r("bbbb"), r("3")})); + + EXPECT_TRUE(first_pair_matches(r("(1 . 2)"), Object::make_integer(1), Object::make_integer(2))); + + EXPECT_TRUE(print_matches(r(" ( 1 . 2 ) "), "(1 . 2)")); + EXPECT_TRUE(print_matches(r(" ( 1 1 . 2 ) "), "(1 1 . 2)")); + EXPECT_TRUE(print_matches(r(" ( 1 . ( 1 . 2 ) ) "), "(1 1 . 2)")); + EXPECT_TRUE( + print_matches(r(" ( 1 ( 1 2 ) ( 1 ( 12 3 ) ) . 3 ) "), "(1 (1 2) (1 (12 3)) . 3)")); + EXPECT_TRUE( + print_matches(r(" ( 1 ( 1 2 ) ( 1 ( 12 3 ) ) . ( ) ) "), "(1 (1 2) (1 (12 3)))")); + + std::vector expected_to_throw = {"(", ")", " (", " )()() ", + ")(", "(1 2 ))", "(( 1 2)", "(1 . . 2)", + "(1 . )", "(1 . 2 3)", "( . 2)"}; + + for (const auto& x : expected_to_throw) { + EXPECT_ANY_THROW(r(x)); + } +} + +TEST(GoosReader, Comments) { + Reader reader; + auto r = [&](std::string s) { return reader.read_from_string(s); }; + EXPECT_TRUE(first_list_matches(r(";;\n(1)\n;;"), {r("1")})); + EXPECT_TRUE(first_list_matches(r(";;\n(;1\n1;)\n);;\n;"), {r("1")})); + + r(";"); + r(" ;"); + r("\n;"); + r(";\n"); + + EXPECT_TRUE(first_list_matches( + r("#|multi line\n com(((((ment |# (1) #| multi line\n comm)))))ent |#"), {r("1")})); + EXPECT_TRUE(first_list_matches( + r("#| #| multi l#|ine\n com#|ment |# (1) #| multi line\n commen))))))t |#"), {r("1")})); + + std::vector expected_to_throw = {"|#", "#| |# |#"}; + + for (const auto& x : expected_to_throw) { + EXPECT_ANY_THROW(r(x)); + } +} + +TEST(GoosReader, Char) { + Reader reader; + auto r = [&](std::string s) { return reader.read_from_string(s); }; + + EXPECT_TRUE(first_char_matches(r("#\\c"), 'c')); + EXPECT_TRUE(first_char_matches(r("#\\n"), 'n')); + EXPECT_TRUE(first_char_matches(r("#\\\\n"), '\n')); + EXPECT_TRUE(first_char_matches(r("#\\\\t"), '\t')); + EXPECT_TRUE(first_char_matches(r("#\\\\s"), ' ')); +} + +TEST(GoosReader, Array) { + Reader reader; + auto r = [&](std::string s) { return reader.read_from_string(s); }; + EXPECT_TRUE(print_matches(r(" #( ) "), "#()")); + EXPECT_TRUE(first_array_matches(r("#()"), {})); + EXPECT_TRUE(first_array_matches(r("#(1 2)"), {Object::make_integer(1), Object::make_integer(2)})); + EXPECT_TRUE(first_array_matches(r("#( 1 #| 2 |# 3 )"), + {Object::make_integer(1), Object::make_integer(3)})); + EXPECT_TRUE( + first_array_matches(r("#( 1 #|2|# 3 )"), {Object::make_integer(1), Object::make_integer(3)})); +} + +TEST(GoosReader, Macros) { + Reader reader; + auto r = [&](std::string s) { return reader.read_from_string(s); }; + EXPECT_TRUE(print_matches(r("'x"), "(quote x)")); + EXPECT_TRUE(print_matches(r("`x"), "(quasiquote x)")); + EXPECT_TRUE(print_matches(r(",x"), "(unquote x)")); + EXPECT_TRUE(print_matches(r(",@x"), "(unquote-splicing x)")); +} + +TEST(GoosReader, TopLevel) { + Reader reader; + auto r = [&](std::string s) { return reader.read_from_string(s); }; + EXPECT_EQ(r("x").print(), "(top-level x)"); +} + +TEST(GoosReader, FromFile) { + Reader reader; + auto result = + reader.read_from_file(util::combine_path({"test", "test_reader_file0.gc"})).print(); + EXPECT_TRUE(result == "(top-level (1 2 3 4))"); +} + +TEST(GoosReader, TextDb) { + // very specific to this particular test file, but whatever. + Reader reader; + auto file_path = util::combine_path({"test", "test_reader_file0.gc"}); + auto result = reader.read_from_file(file_path).as_pair()->cdr.as_pair()->car; + std::string expected = "text from " + util::combine_path(reader.get_source_dir(), file_path) + + ", line: 5\n(1 2 3 4)\n"; + EXPECT_EQ(expected, reader.db.get_info_for(result)); +} diff --git a/test/test_reader_file0.gc b/test/test_reader_file0.gc new file mode 100644 index 0000000000..ffc153759e --- /dev/null +++ b/test/test_reader_file0.gc @@ -0,0 +1,9 @@ +; this is a test file for test_reader.cpp + +#| some comment |# + +(1 2 3 4) +;a +; aaaa + +#| some comment |# \ No newline at end of file diff --git a/test/test_test.cpp b/test/test_test.cpp new file mode 100644 index 0000000000..19131f66a5 --- /dev/null +++ b/test/test_test.cpp @@ -0,0 +1,6 @@ +#include "gtest/gtest.h" + +TEST(test, test) { + EXPECT_TRUE(true); + EXPECT_FALSE(false); +} \ No newline at end of file diff --git a/third-party/googletest b/third-party/googletest new file mode 160000 index 0000000000..adeef19294 --- /dev/null +++ b/third-party/googletest @@ -0,0 +1 @@ +Subproject commit adeef192947fbc0f68fa14a6c494c8df32177508 diff --git a/third-party/linenoise.h b/third-party/linenoise.h new file mode 100644 index 0000000000..c6e6f7cb2f --- /dev/null +++ b/third-party/linenoise.h @@ -0,0 +1,2415 @@ +/* + * linenoise.hpp -- Multi-platfrom C++ header-only linenoise library. + * + * All credits and commendations have to go to the authors of the + * following excellent libraries. + * + * - linenoise.h and linenose.c (https://github.com/antirez/linenoise) + * - ANSI.c (https://github.com/adoxa/ansicon) + * - Win32_ANSI.h and Win32_ANSI.c (https://github.com/MSOpenTech/redis) + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2015 yhirose + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* linenoise.h -- guerrilla line editing library against the idea that a + * line editing lib needs to be 20,000 lines of C code. + * + * See linenoise.c for more information. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010, Salvatore Sanfilippo + * Copyright (c) 2010, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * ANSI.c - ANSI escape sequence console driver. + * + * Copyright (C) 2005-2014 Jason Hood + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the author be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + * Jason Hood + * jadoxa@yahoo.com.au + */ + +/* + * Win32_ANSI.h and Win32_ANSI.c + * + * Derived from ANSI.c by Jason Hood, from his ansicon project (https://github.com/adoxa/ansicon), with modifications. + * + * Copyright (c), Microsoft Open Technologies, Inc. + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LINENOISE_HPP +#define LINENOISE_HPP + +#ifndef _WIN32 +#include +#include +#include +#else +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#ifndef STDIN_FILENO +#define STDIN_FILENO (_fileno(stdin)) +#endif +#ifndef STDOUT_FILENO +#define STDOUT_FILENO 1 +#endif +#define isatty _isatty +#define write win32_write +#define read _read +#pragma warning(push) +#pragma warning(disable : 4996) +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace linenoise { + + typedef std::function&)> CompletionCallback; + +#ifdef _WIN32 + + namespace ansi { + +#define lenof(array) (sizeof(array)/sizeof(*(array))) + +typedef struct +{ + BYTE foreground; // ANSI base color (0 to 7; add 30) + BYTE background; // ANSI base color (0 to 7; add 40) + BYTE bold; // console FOREGROUND_INTENSITY bit + BYTE underline; // console BACKGROUND_INTENSITY bit + BYTE rvideo; // swap foreground/bold & background/underline + BYTE concealed; // set foreground/bold to background/underline + BYTE reverse; // swap console foreground & background attributes +} GRM, *PGRM; // Graphic Rendition Mode + + +inline bool is_digit(char c) { return '0' <= c && c <= '9'; } + +// ========== Global variables and constants + +HANDLE hConOut; // handle to CONOUT$ + +const char ESC = '\x1B'; // ESCape character +const char BEL = '\x07'; +const char SO = '\x0E'; // Shift Out +const char SI = '\x0F'; // Shift In + +const int MAX_ARG = 16; // max number of args in an escape sequence +int state; // automata state +WCHAR prefix; // escape sequence prefix ( '[', ']' or '(' ); +WCHAR prefix2; // secondary prefix ( '?' or '>' ); +WCHAR suffix; // escape sequence suffix +int es_argc; // escape sequence args count +int es_argv[MAX_ARG]; // escape sequence args +WCHAR Pt_arg[MAX_PATH * 2]; // text parameter for Operating System Command +int Pt_len; +BOOL shifted; + + +// DEC Special Graphics Character Set from +// http://vt100.net/docs/vt220-rm/table2-4.html +// Some of these may not look right, depending on the font and code page (in +// particular, the Control Pictures probably won't work at all). +const WCHAR G1[] = +{ + ' ', // _ - blank + L'\x2666', // ` - Black Diamond Suit + L'\x2592', // a - Medium Shade + L'\x2409', // b - HT + L'\x240c', // c - FF + L'\x240d', // d - CR + L'\x240a', // e - LF + L'\x00b0', // f - Degree Sign + L'\x00b1', // g - Plus-Minus Sign + L'\x2424', // h - NL + L'\x240b', // i - VT + L'\x2518', // j - Box Drawings Light Up And Left + L'\x2510', // k - Box Drawings Light Down And Left + L'\x250c', // l - Box Drawings Light Down And Right + L'\x2514', // m - Box Drawings Light Up And Right + L'\x253c', // n - Box Drawings Light Vertical And Horizontal + L'\x00af', // o - SCAN 1 - Macron + L'\x25ac', // p - SCAN 3 - Black Rectangle + L'\x2500', // q - SCAN 5 - Box Drawings Light Horizontal + L'_', // r - SCAN 7 - Low Line + L'_', // s - SCAN 9 - Low Line + L'\x251c', // t - Box Drawings Light Vertical And Right + L'\x2524', // u - Box Drawings Light Vertical And Left + L'\x2534', // v - Box Drawings Light Up And Horizontal + L'\x252c', // w - Box Drawings Light Down And Horizontal + L'\x2502', // x - Box Drawings Light Vertical + L'\x2264', // y - Less-Than Or Equal To + L'\x2265', // z - Greater-Than Or Equal To + L'\x03c0', // { - Greek Small Letter Pi + L'\x2260', // | - Not Equal To + L'\x00a3', // } - Pound Sign + L'\x00b7', // ~ - Middle Dot +}; + +#define FIRST_G1 '_' +#define LAST_G1 '~' + + +// color constants + +#define FOREGROUND_BLACK 0 +#define FOREGROUND_WHITE FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_BLUE + +#define BACKGROUND_BLACK 0 +#define BACKGROUND_WHITE BACKGROUND_RED|BACKGROUND_GREEN|BACKGROUND_BLUE + +const BYTE foregroundcolor[8] = + { + FOREGROUND_BLACK, // black foreground + FOREGROUND_RED, // red foreground + FOREGROUND_GREEN, // green foreground + FOREGROUND_RED | FOREGROUND_GREEN, // yellow foreground + FOREGROUND_BLUE, // blue foreground + FOREGROUND_BLUE | FOREGROUND_RED, // magenta foreground + FOREGROUND_BLUE | FOREGROUND_GREEN, // cyan foreground + FOREGROUND_WHITE // white foreground + }; + +const BYTE backgroundcolor[8] = + { + BACKGROUND_BLACK, // black background + BACKGROUND_RED, // red background + BACKGROUND_GREEN, // green background + BACKGROUND_RED | BACKGROUND_GREEN, // yellow background + BACKGROUND_BLUE, // blue background + BACKGROUND_BLUE | BACKGROUND_RED, // magenta background + BACKGROUND_BLUE | BACKGROUND_GREEN, // cyan background + BACKGROUND_WHITE, // white background + }; + +const BYTE attr2ansi[8] = // map console attribute to ANSI number +{ + 0, // black + 4, // blue + 2, // green + 6, // cyan + 1, // red + 5, // magenta + 3, // yellow + 7 // white +}; + +GRM grm; + +// saved cursor position +COORD SavePos; + +// ========== Print Buffer functions + +#define BUFFER_SIZE 2048 + +int nCharInBuffer; +WCHAR ChBuffer[BUFFER_SIZE]; + +//----------------------------------------------------------------------------- +// FlushBuffer() +// Writes the buffer to the console and empties it. +//----------------------------------------------------------------------------- + +inline void FlushBuffer(void) +{ + DWORD nWritten; + if (nCharInBuffer <= 0) return; + WriteConsoleW(hConOut, ChBuffer, nCharInBuffer, &nWritten, NULL); + nCharInBuffer = 0; +} + +//----------------------------------------------------------------------------- +// PushBuffer( WCHAR c ) +// Adds a character in the buffer. +//----------------------------------------------------------------------------- + +inline void PushBuffer(WCHAR c) +{ + if (shifted && c >= FIRST_G1 && c <= LAST_G1) + c = G1[c - FIRST_G1]; + ChBuffer[nCharInBuffer] = c; + if (++nCharInBuffer == BUFFER_SIZE) + FlushBuffer(); +} + +//----------------------------------------------------------------------------- +// SendSequence( LPCWSTR seq ) +// Send the string to the input buffer. +//----------------------------------------------------------------------------- + +inline void SendSequence(LPCWSTR seq) +{ + DWORD out; + INPUT_RECORD in; + HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); + + in.EventType = KEY_EVENT; + in.Event.KeyEvent.bKeyDown = TRUE; + in.Event.KeyEvent.wRepeatCount = 1; + in.Event.KeyEvent.wVirtualKeyCode = 0; + in.Event.KeyEvent.wVirtualScanCode = 0; + in.Event.KeyEvent.dwControlKeyState = 0; + for (; *seq; ++seq) + { + in.Event.KeyEvent.uChar.UnicodeChar = *seq; + WriteConsoleInput(hStdIn, &in, 1, &out); + } +} + +// ========== Print functions + +//----------------------------------------------------------------------------- +// InterpretEscSeq() +// Interprets the last escape sequence scanned by ParseAndPrintANSIString +// prefix escape sequence prefix +// es_argc escape sequence args count +// es_argv[] escape sequence args array +// suffix escape sequence suffix +// +// for instance, with \e[33;45;1m we have +// prefix = '[', +// es_argc = 3, es_argv[0] = 33, es_argv[1] = 45, es_argv[2] = 1 +// suffix = 'm' +//----------------------------------------------------------------------------- + +inline void InterpretEscSeq(void) +{ + int i; + WORD attribut; + CONSOLE_SCREEN_BUFFER_INFO Info; + CONSOLE_CURSOR_INFO CursInfo; + DWORD len, NumberOfCharsWritten; + COORD Pos; + SMALL_RECT Rect; + CHAR_INFO CharInfo; + + if (prefix == '[') + { + if (prefix2 == '?' && (suffix == 'h' || suffix == 'l')) + { + if (es_argc == 1 && es_argv[0] == 25) + { + GetConsoleCursorInfo(hConOut, &CursInfo); + CursInfo.bVisible = (suffix == 'h'); + SetConsoleCursorInfo(hConOut, &CursInfo); + return; + } + } + // Ignore any other \e[? or \e[> sequences. + if (prefix2 != 0) + return; + + GetConsoleScreenBufferInfo(hConOut, &Info); + switch (suffix) + { + case 'm': + if (es_argc == 0) es_argv[es_argc++] = 0; + for (i = 0; i < es_argc; i++) + { + if (30 <= es_argv[i] && es_argv[i] <= 37) + grm.foreground = es_argv[i] - 30; + else if (40 <= es_argv[i] && es_argv[i] <= 47) + grm.background = es_argv[i] - 40; + else switch (es_argv[i]) + { + case 0: + case 39: + case 49: + { + WCHAR def[4]; + int a; + *def = '7'; def[1] = '\0'; + GetEnvironmentVariableW(L"ANSICON_DEF", def, lenof(def)); + a = wcstol(def, NULL, 16); + grm.reverse = FALSE; + if (a < 0) + { + grm.reverse = TRUE; + a = -a; + } + if (es_argv[i] != 49) + grm.foreground = attr2ansi[a & 7]; + if (es_argv[i] != 39) + grm.background = attr2ansi[(a >> 4) & 7]; + if (es_argv[i] == 0) + { + if (es_argc == 1) + { + grm.bold = a & FOREGROUND_INTENSITY; + grm.underline = a & BACKGROUND_INTENSITY; + } + else + { + grm.bold = 0; + grm.underline = 0; + } + grm.rvideo = 0; + grm.concealed = 0; + } + } + break; + + case 1: grm.bold = FOREGROUND_INTENSITY; break; + case 5: // blink + case 4: grm.underline = BACKGROUND_INTENSITY; break; + case 7: grm.rvideo = 1; break; + case 8: grm.concealed = 1; break; + case 21: // oops, this actually turns on double underline + case 22: grm.bold = 0; break; + case 25: + case 24: grm.underline = 0; break; + case 27: grm.rvideo = 0; break; + case 28: grm.concealed = 0; break; + } + } + if (grm.concealed) + { + if (grm.rvideo) + { + attribut = foregroundcolor[grm.foreground] + | backgroundcolor[grm.foreground]; + if (grm.bold) + attribut |= FOREGROUND_INTENSITY | BACKGROUND_INTENSITY; + } + else + { + attribut = foregroundcolor[grm.background] + | backgroundcolor[grm.background]; + if (grm.underline) + attribut |= FOREGROUND_INTENSITY | BACKGROUND_INTENSITY; + } + } + else if (grm.rvideo) + { + attribut = foregroundcolor[grm.background] + | backgroundcolor[grm.foreground]; + if (grm.bold) + attribut |= BACKGROUND_INTENSITY; + if (grm.underline) + attribut |= FOREGROUND_INTENSITY; + } + else + attribut = foregroundcolor[grm.foreground] | grm.bold + | backgroundcolor[grm.background] | grm.underline; + if (grm.reverse) + attribut = ((attribut >> 4) & 15) | ((attribut & 15) << 4); + SetConsoleTextAttribute(hConOut, attribut); + return; + + case 'J': + if (es_argc == 0) es_argv[es_argc++] = 0; // ESC[J == ESC[0J + if (es_argc != 1) return; + switch (es_argv[0]) + { + case 0: // ESC[0J erase from cursor to end of display + len = (Info.dwSize.Y - Info.dwCursorPosition.Y - 1) * Info.dwSize.X + + Info.dwSize.X - Info.dwCursorPosition.X - 1; + FillConsoleOutputCharacter(hConOut, ' ', len, + Info.dwCursorPosition, + &NumberOfCharsWritten); + FillConsoleOutputAttribute(hConOut, Info.wAttributes, len, + Info.dwCursorPosition, + &NumberOfCharsWritten); + return; + + case 1: // ESC[1J erase from start to cursor. + Pos.X = 0; + Pos.Y = 0; + len = Info.dwCursorPosition.Y * Info.dwSize.X + + Info.dwCursorPosition.X + 1; + FillConsoleOutputCharacter(hConOut, ' ', len, Pos, + &NumberOfCharsWritten); + FillConsoleOutputAttribute(hConOut, Info.wAttributes, len, Pos, + &NumberOfCharsWritten); + return; + + case 2: // ESC[2J Clear screen and home cursor + Pos.X = 0; + Pos.Y = 0; + len = Info.dwSize.X * Info.dwSize.Y; + FillConsoleOutputCharacter(hConOut, ' ', len, Pos, + &NumberOfCharsWritten); + FillConsoleOutputAttribute(hConOut, Info.wAttributes, len, Pos, + &NumberOfCharsWritten); + SetConsoleCursorPosition(hConOut, Pos); + return; + + default: + return; + } + + case 'K': + if (es_argc == 0) es_argv[es_argc++] = 0; // ESC[K == ESC[0K + if (es_argc != 1) return; + switch (es_argv[0]) + { + case 0: // ESC[0K Clear to end of line + len = Info.dwSize.X - Info.dwCursorPosition.X + 1; + FillConsoleOutputCharacter(hConOut, ' ', len, + Info.dwCursorPosition, + &NumberOfCharsWritten); + FillConsoleOutputAttribute(hConOut, Info.wAttributes, len, + Info.dwCursorPosition, + &NumberOfCharsWritten); + return; + + case 1: // ESC[1K Clear from start of line to cursor + Pos.X = 0; + Pos.Y = Info.dwCursorPosition.Y; + FillConsoleOutputCharacter(hConOut, ' ', + Info.dwCursorPosition.X + 1, Pos, + &NumberOfCharsWritten); + FillConsoleOutputAttribute(hConOut, Info.wAttributes, + Info.dwCursorPosition.X + 1, Pos, + &NumberOfCharsWritten); + return; + + case 2: // ESC[2K Clear whole line. + Pos.X = 0; + Pos.Y = Info.dwCursorPosition.Y; + FillConsoleOutputCharacter(hConOut, ' ', Info.dwSize.X, Pos, + &NumberOfCharsWritten); + FillConsoleOutputAttribute(hConOut, Info.wAttributes, + Info.dwSize.X, Pos, + &NumberOfCharsWritten); + return; + + default: + return; + } + + case 'X': // ESC[#X Erase # characters. + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[X == ESC[1X + if (es_argc != 1) return; + FillConsoleOutputCharacter(hConOut, ' ', es_argv[0], + Info.dwCursorPosition, + &NumberOfCharsWritten); + FillConsoleOutputAttribute(hConOut, Info.wAttributes, es_argv[0], + Info.dwCursorPosition, + &NumberOfCharsWritten); + return; + + case 'L': // ESC[#L Insert # blank lines. + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[L == ESC[1L + if (es_argc != 1) return; + Rect.Left = 0; + Rect.Top = Info.dwCursorPosition.Y; + Rect.Right = Info.dwSize.X - 1; + Rect.Bottom = Info.dwSize.Y - 1; + Pos.X = 0; + Pos.Y = Info.dwCursorPosition.Y + es_argv[0]; + CharInfo.Char.UnicodeChar = ' '; + CharInfo.Attributes = Info.wAttributes; + ScrollConsoleScreenBuffer(hConOut, &Rect, NULL, Pos, &CharInfo); + return; + + case 'M': // ESC[#M Delete # lines. + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[M == ESC[1M + if (es_argc != 1) return; + if (es_argv[0] > Info.dwSize.Y - Info.dwCursorPosition.Y) + es_argv[0] = Info.dwSize.Y - Info.dwCursorPosition.Y; + Rect.Left = 0; + Rect.Top = Info.dwCursorPosition.Y + es_argv[0]; + Rect.Right = Info.dwSize.X - 1; + Rect.Bottom = Info.dwSize.Y - 1; + Pos.X = 0; + Pos.Y = Info.dwCursorPosition.Y; + CharInfo.Char.UnicodeChar = ' '; + CharInfo.Attributes = Info.wAttributes; + ScrollConsoleScreenBuffer(hConOut, &Rect, NULL, Pos, &CharInfo); + return; + + case 'P': // ESC[#P Delete # characters. + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[P == ESC[1P + if (es_argc != 1) return; + if (Info.dwCursorPosition.X + es_argv[0] > Info.dwSize.X - 1) + es_argv[0] = Info.dwSize.X - Info.dwCursorPosition.X; + Rect.Left = Info.dwCursorPosition.X + es_argv[0]; + Rect.Top = Info.dwCursorPosition.Y; + Rect.Right = Info.dwSize.X - 1; + Rect.Bottom = Info.dwCursorPosition.Y; + CharInfo.Char.UnicodeChar = ' '; + CharInfo.Attributes = Info.wAttributes; + ScrollConsoleScreenBuffer(hConOut, &Rect, NULL, Info.dwCursorPosition, + &CharInfo); + return; + + case '@': // ESC[#@ Insert # blank characters. + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[@ == ESC[1@ + if (es_argc != 1) return; + if (Info.dwCursorPosition.X + es_argv[0] > Info.dwSize.X - 1) + es_argv[0] = Info.dwSize.X - Info.dwCursorPosition.X; + Rect.Left = Info.dwCursorPosition.X; + Rect.Top = Info.dwCursorPosition.Y; + Rect.Right = Info.dwSize.X - 1 - es_argv[0]; + Rect.Bottom = Info.dwCursorPosition.Y; + Pos.X = Info.dwCursorPosition.X + es_argv[0]; + Pos.Y = Info.dwCursorPosition.Y; + CharInfo.Char.UnicodeChar = ' '; + CharInfo.Attributes = Info.wAttributes; + ScrollConsoleScreenBuffer(hConOut, &Rect, NULL, Pos, &CharInfo); + return; + + case 'k': // ESC[#k + case 'A': // ESC[#A Moves cursor up # lines + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[A == ESC[1A + if (es_argc != 1) return; + Pos.Y = Info.dwCursorPosition.Y - es_argv[0]; + if (Pos.Y < 0) Pos.Y = 0; + Pos.X = Info.dwCursorPosition.X; + SetConsoleCursorPosition(hConOut, Pos); + return; + + case 'e': // ESC[#e + case 'B': // ESC[#B Moves cursor down # lines + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[B == ESC[1B + if (es_argc != 1) return; + Pos.Y = Info.dwCursorPosition.Y + es_argv[0]; + if (Pos.Y >= Info.dwSize.Y) Pos.Y = Info.dwSize.Y - 1; + Pos.X = Info.dwCursorPosition.X; + SetConsoleCursorPosition(hConOut, Pos); + return; + + case 'a': // ESC[#a + case 'C': // ESC[#C Moves cursor forward # spaces + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[C == ESC[1C + if (es_argc != 1) return; + Pos.X = Info.dwCursorPosition.X + es_argv[0]; + if (Pos.X >= Info.dwSize.X) Pos.X = Info.dwSize.X - 1; + Pos.Y = Info.dwCursorPosition.Y; + SetConsoleCursorPosition(hConOut, Pos); + return; + + case 'j': // ESC[#j + case 'D': // ESC[#D Moves cursor back # spaces + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[D == ESC[1D + if (es_argc != 1) return; + Pos.X = Info.dwCursorPosition.X - es_argv[0]; + if (Pos.X < 0) Pos.X = 0; + Pos.Y = Info.dwCursorPosition.Y; + SetConsoleCursorPosition(hConOut, Pos); + return; + + case 'E': // ESC[#E Moves cursor down # lines, column 1. + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[E == ESC[1E + if (es_argc != 1) return; + Pos.Y = Info.dwCursorPosition.Y + es_argv[0]; + if (Pos.Y >= Info.dwSize.Y) Pos.Y = Info.dwSize.Y - 1; + Pos.X = 0; + SetConsoleCursorPosition(hConOut, Pos); + return; + + case 'F': // ESC[#F Moves cursor up # lines, column 1. + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[F == ESC[1F + if (es_argc != 1) return; + Pos.Y = Info.dwCursorPosition.Y - es_argv[0]; + if (Pos.Y < 0) Pos.Y = 0; + Pos.X = 0; + SetConsoleCursorPosition(hConOut, Pos); + return; + + case '`': // ESC[#` + case 'G': // ESC[#G Moves cursor column # in current row. + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[G == ESC[1G + if (es_argc != 1) return; + Pos.X = es_argv[0] - 1; + if (Pos.X >= Info.dwSize.X) Pos.X = Info.dwSize.X - 1; + if (Pos.X < 0) Pos.X = 0; + Pos.Y = Info.dwCursorPosition.Y; + SetConsoleCursorPosition(hConOut, Pos); + return; + + case 'd': // ESC[#d Moves cursor row #, current column. + if (es_argc == 0) es_argv[es_argc++] = 1; // ESC[d == ESC[1d + if (es_argc != 1) return; + Pos.Y = es_argv[0] - 1; + if (Pos.Y < 0) Pos.Y = 0; + if (Pos.Y >= Info.dwSize.Y) Pos.Y = Info.dwSize.Y - 1; + SetConsoleCursorPosition(hConOut, Pos); + return; + + case 'f': // ESC[#;#f + case 'H': // ESC[#;#H Moves cursor to line #, column # + if (es_argc == 0) + es_argv[es_argc++] = 1; // ESC[H == ESC[1;1H + if (es_argc == 1) + es_argv[es_argc++] = 1; // ESC[#H == ESC[#;1H + if (es_argc > 2) return; + Pos.X = es_argv[1] - 1; + if (Pos.X < 0) Pos.X = 0; + if (Pos.X >= Info.dwSize.X) Pos.X = Info.dwSize.X - 1; + Pos.Y = es_argv[0] - 1; + if (Pos.Y < 0) Pos.Y = 0; + if (Pos.Y >= Info.dwSize.Y) Pos.Y = Info.dwSize.Y - 1; + SetConsoleCursorPosition(hConOut, Pos); + return; + + case 's': // ESC[s Saves cursor position for recall later + if (es_argc != 0) return; + SavePos = Info.dwCursorPosition; + return; + + case 'u': // ESC[u Return to saved cursor position + if (es_argc != 0) return; + SetConsoleCursorPosition(hConOut, SavePos); + return; + + case 'n': // ESC[#n Device status report + if (es_argc != 1) return; // ESC[n == ESC[0n -> ignored + switch (es_argv[0]) + { + case 5: // ESC[5n Report status + SendSequence(L"\33[0n"); // "OK" + return; + + case 6: // ESC[6n Report cursor position + { + WCHAR buf[32]; + swprintf(buf, 32, L"\33[%d;%dR", Info.dwCursorPosition.Y + 1, + Info.dwCursorPosition.X + 1); + SendSequence(buf); + } + return; + + default: + return; + } + + case 't': // ESC[#t Window manipulation + if (es_argc != 1) return; + if (es_argv[0] == 21) // ESC[21t Report xterm window's title + { + WCHAR buf[MAX_PATH * 2]; + DWORD len = GetConsoleTitleW(buf + 3, lenof(buf) - 3 - 2); + // Too bad if it's too big or fails. + buf[0] = ESC; + buf[1] = ']'; + buf[2] = 'l'; + buf[3 + len] = ESC; + buf[3 + len + 1] = '\\'; + buf[3 + len + 2] = '\0'; + SendSequence(buf); + } + return; + + default: + return; + } + } + else // (prefix == ']') + { + // Ignore any \e]? or \e]> sequences. + if (prefix2 != 0) + return; + + if (es_argc == 1 && es_argv[0] == 0) // ESC]0;titleST + { + SetConsoleTitleW(Pt_arg); + } + } +} + +//----------------------------------------------------------------------------- +// ParseAndPrintANSIString(hDev, lpBuffer, nNumberOfBytesToWrite) +// Parses the string lpBuffer, interprets the escapes sequences and prints the +// characters in the device hDev (console). +// The lexer is a three states automata. +// If the number of arguments es_argc > MAX_ARG, only the MAX_ARG-1 firsts and +// the last arguments are processed (no es_argv[] overflow). +//----------------------------------------------------------------------------- + +inline BOOL ParseAndPrintANSIString(HANDLE hDev, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten) +{ + DWORD i; + LPCSTR s; + + if (hDev != hConOut) // reinit if device has changed + { + hConOut = hDev; + state = 1; + shifted = FALSE; + } + for (i = nNumberOfBytesToWrite, s = (LPCSTR)lpBuffer; i > 0; i--, s++) + { + if (state == 1) + { + if (*s == ESC) state = 2; + else if (*s == SO) shifted = TRUE; + else if (*s == SI) shifted = FALSE; + else PushBuffer(*s); + } + else if (state == 2) + { + if (*s == ESC); // \e\e...\e == \e + else if ((*s == '[') || (*s == ']')) + { + FlushBuffer(); + prefix = *s; + prefix2 = 0; + state = 3; + Pt_len = 0; + *Pt_arg = '\0'; + } + else if (*s == ')' || *s == '(') state = 6; + else state = 1; + } + else if (state == 3) + { + if (is_digit(*s)) + { + es_argc = 0; + es_argv[0] = *s - '0'; + state = 4; + } + else if (*s == ';') + { + es_argc = 1; + es_argv[0] = 0; + es_argv[1] = 0; + state = 4; + } + else if (*s == '?' || *s == '>') + { + prefix2 = *s; + } + else + { + es_argc = 0; + suffix = *s; + InterpretEscSeq(); + state = 1; + } + } + else if (state == 4) + { + if (is_digit(*s)) + { + es_argv[es_argc] = 10 * es_argv[es_argc] + (*s - '0'); + } + else if (*s == ';') + { + if (es_argc < MAX_ARG - 1) es_argc++; + es_argv[es_argc] = 0; + if (prefix == ']') + state = 5; + } + else + { + es_argc++; + suffix = *s; + InterpretEscSeq(); + state = 1; + } + } + else if (state == 5) + { + if (*s == BEL) + { + Pt_arg[Pt_len] = '\0'; + InterpretEscSeq(); + state = 1; + } + else if (*s == '\\' && Pt_len > 0 && Pt_arg[Pt_len - 1] == ESC) + { + Pt_arg[--Pt_len] = '\0'; + InterpretEscSeq(); + state = 1; + } + else if (Pt_len < lenof(Pt_arg) - 1) + Pt_arg[Pt_len++] = *s; + } + else if (state == 6) + { + // Ignore it (ESC ) 0 is implicit; nothing else is supported). + state = 1; + } + } + FlushBuffer(); + if (lpNumberOfBytesWritten != NULL) + *lpNumberOfBytesWritten = nNumberOfBytesToWrite - i; + return (i == 0); +} + +} // namespace ansi + +HANDLE hOut; +HANDLE hIn; +DWORD consolemodeIn = 0; + +inline int win32read(int *c) { + DWORD foo; + INPUT_RECORD b; + KEY_EVENT_RECORD e; + BOOL altgr; + + while (1) { + if (!ReadConsoleInput(hIn, &b, 1, &foo)) return 0; + if (!foo) return 0; + + if (b.EventType == KEY_EVENT && b.Event.KeyEvent.bKeyDown) { + + e = b.Event.KeyEvent; + *c = b.Event.KeyEvent.uChar.AsciiChar; + + altgr = e.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED); + + if (e.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) && !altgr) { + + /* Ctrl+Key */ + switch (*c) { + case 'D': + *c = 4; + return 1; + case 'C': + *c = 3; + return 1; + case 'H': + *c = 8; + return 1; + case 'T': + *c = 20; + return 1; + case 'B': /* ctrl-b, left_arrow */ + *c = 2; + return 1; + case 'F': /* ctrl-f right_arrow*/ + *c = 6; + return 1; + case 'P': /* ctrl-p up_arrow*/ + *c = 16; + return 1; + case 'N': /* ctrl-n down_arrow*/ + *c = 14; + return 1; + case 'U': /* Ctrl+u, delete the whole line. */ + *c = 21; + return 1; + case 'K': /* Ctrl+k, delete from current to end of line. */ + *c = 11; + return 1; + case 'A': /* Ctrl+a, go to the start of the line */ + *c = 1; + return 1; + case 'E': /* ctrl+e, go to the end of the line */ + *c = 5; + return 1; + } + + /* Other Ctrl+KEYs ignored */ + } else { + + switch (e.wVirtualKeyCode) { + + case VK_ESCAPE: /* ignore - send ctrl-c, will return -1 */ + *c = 3; + return 1; + case VK_RETURN: /* enter */ + *c = 13; + return 1; + case VK_LEFT: /* left */ + *c = 2; + return 1; + case VK_RIGHT: /* right */ + *c = 6; + return 1; + case VK_UP: /* up */ + *c = 16; + return 1; + case VK_DOWN: /* down */ + *c = 14; + return 1; + case VK_HOME: + *c = 1; + return 1; + case VK_END: + *c = 5; + return 1; + case VK_BACK: + *c = 8; + return 1; + case VK_DELETE: + *c = 127; + return 1; + default: + if (*c) return 1; + } + } + } + } + + return -1; /* Makes compiler happy */ +} + +inline int win32_write(int fd, const void *buffer, unsigned int count) { + if (fd == _fileno(stdout)) { + DWORD bytesWritten = 0; + if (FALSE != ansi::ParseAndPrintANSIString(GetStdHandle(STD_OUTPUT_HANDLE), buffer, (DWORD)count, &bytesWritten)) { + return (int)bytesWritten; + } else { + errno = GetLastError(); + return 0; + } + } else if (fd == _fileno(stderr)) { + DWORD bytesWritten = 0; + if (FALSE != ansi::ParseAndPrintANSIString(GetStdHandle(STD_ERROR_HANDLE), buffer, (DWORD)count, &bytesWritten)) { + return (int)bytesWritten; + } else { + errno = GetLastError(); + return 0; + } + } else { + return _write(fd, buffer, count); + } +} +#endif // _WIN32 + +#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100 +#define LINENOISE_MAX_LINE 4096 + static const char *unsupported_term[] = {"dumb","cons25","emacs",NULL}; + static CompletionCallback completionCallback; + +#ifndef _WIN32 + static struct termios orig_termios; /* In order to restore at exit.*/ +#endif + static bool rawmode = false; /* For atexit() function to check if restore is needed*/ + static bool mlmode = false; /* Multi line mode. Default is single line. */ + static bool atexit_registered = false; /* Register atexit just 1 time. */ + static size_t history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN; + static std::vector history; + +/* The linenoiseState structure represents the state during line editing. + * We pass this state to functions implementing specific editing + * functionalities. */ + struct linenoiseState { + int ifd; /* Terminal stdin file descriptor. */ + int ofd; /* Terminal stdout file descriptor. */ + char *buf; /* Edited line buffer. */ + int buflen; /* Edited line buffer size. */ + std::string prompt; /* Prompt to display. */ + int pos; /* Current cursor position. */ + int oldcolpos; /* Previous refresh cursor column position. */ + int len; /* Current edited line length. */ + int cols; /* Number of columns in terminal. */ + int maxrows; /* Maximum num of rows used so far (multiline mode) */ + int history_index; /* The history index we are currently editing. */ + }; + + enum KEY_ACTION { + KEY_NULL = 0, /* NULL */ + CTRL_A = 1, /* Ctrl+a */ + CTRL_B = 2, /* Ctrl-b */ + CTRL_C = 3, /* Ctrl-c */ + CTRL_D = 4, /* Ctrl-d */ + CTRL_E = 5, /* Ctrl-e */ + CTRL_F = 6, /* Ctrl-f */ + CTRL_H = 8, /* Ctrl-h */ + TAB = 9, /* Tab */ + CTRL_K = 11, /* Ctrl+k */ + CTRL_L = 12, /* Ctrl+l */ + ENTER = 13, /* Enter */ + CTRL_N = 14, /* Ctrl-n */ + CTRL_P = 16, /* Ctrl-p */ + CTRL_T = 20, /* Ctrl-t */ + CTRL_U = 21, /* Ctrl+u */ + CTRL_W = 23, /* Ctrl+w */ + ESC = 27, /* Escape */ + BACKSPACE = 127 /* Backspace */ + }; + + void linenoiseAtExit(void); + bool AddHistory(const char *line); + void refreshLine(struct linenoiseState *l); + +/* ============================ UTF8 utilities ============================== */ + + static unsigned long unicodeWideCharTable[][2] = { + { 0x1100, 0x115F }, { 0x2329, 0x232A }, { 0x2E80, 0x2E99, }, { 0x2E9B, 0x2EF3, }, + { 0x2F00, 0x2FD5, }, { 0x2FF0, 0x2FFB, }, { 0x3000, 0x303E, }, { 0x3041, 0x3096, }, + { 0x3099, 0x30FF, }, { 0x3105, 0x312D, }, { 0x3131, 0x318E, }, { 0x3190, 0x31BA, }, + { 0x31C0, 0x31E3, }, { 0x31F0, 0x321E, }, { 0x3220, 0x3247, }, { 0x3250, 0x4DBF, }, + { 0x4E00, 0xA48C, }, { 0xA490, 0xA4C6, }, { 0xA960, 0xA97C, }, { 0xAC00, 0xD7A3, }, + { 0xF900, 0xFAFF, }, { 0xFE10, 0xFE19, }, { 0xFE30, 0xFE52, }, { 0xFE54, 0xFE66, }, + { 0xFE68, 0xFE6B, }, { 0xFF01, 0xFFE6, }, + { 0x1B000, 0x1B001, }, { 0x1F200, 0x1F202, }, { 0x1F210, 0x1F23A, }, + { 0x1F240, 0x1F248, }, { 0x1F250, 0x1F251, }, { 0x20000, 0x3FFFD, }, + }; + + static int unicodeWideCharTableSize = sizeof(unicodeWideCharTable) / sizeof(unicodeWideCharTable[0]); + + static int unicodeIsWideChar(unsigned long cp) + { + int i; + for (i = 0; i < unicodeWideCharTableSize; i++) { + if (unicodeWideCharTable[i][0] <= cp && cp <= unicodeWideCharTable[i][1]) { + return 1; + } + } + return 0; + } + + static unsigned long unicodeCombiningCharTable[] = { + 0x0300,0x0301,0x0302,0x0303,0x0304,0x0305,0x0306,0x0307, + 0x0308,0x0309,0x030A,0x030B,0x030C,0x030D,0x030E,0x030F, + 0x0310,0x0311,0x0312,0x0313,0x0314,0x0315,0x0316,0x0317, + 0x0318,0x0319,0x031A,0x031B,0x031C,0x031D,0x031E,0x031F, + 0x0320,0x0321,0x0322,0x0323,0x0324,0x0325,0x0326,0x0327, + 0x0328,0x0329,0x032A,0x032B,0x032C,0x032D,0x032E,0x032F, + 0x0330,0x0331,0x0332,0x0333,0x0334,0x0335,0x0336,0x0337, + 0x0338,0x0339,0x033A,0x033B,0x033C,0x033D,0x033E,0x033F, + 0x0340,0x0341,0x0342,0x0343,0x0344,0x0345,0x0346,0x0347, + 0x0348,0x0349,0x034A,0x034B,0x034C,0x034D,0x034E,0x034F, + 0x0350,0x0351,0x0352,0x0353,0x0354,0x0355,0x0356,0x0357, + 0x0358,0x0359,0x035A,0x035B,0x035C,0x035D,0x035E,0x035F, + 0x0360,0x0361,0x0362,0x0363,0x0364,0x0365,0x0366,0x0367, + 0x0368,0x0369,0x036A,0x036B,0x036C,0x036D,0x036E,0x036F, + 0x0483,0x0484,0x0485,0x0486,0x0487,0x0591,0x0592,0x0593, + 0x0594,0x0595,0x0596,0x0597,0x0598,0x0599,0x059A,0x059B, + 0x059C,0x059D,0x059E,0x059F,0x05A0,0x05A1,0x05A2,0x05A3, + 0x05A4,0x05A5,0x05A6,0x05A7,0x05A8,0x05A9,0x05AA,0x05AB, + 0x05AC,0x05AD,0x05AE,0x05AF,0x05B0,0x05B1,0x05B2,0x05B3, + 0x05B4,0x05B5,0x05B6,0x05B7,0x05B8,0x05B9,0x05BA,0x05BB, + 0x05BC,0x05BD,0x05BF,0x05C1,0x05C2,0x05C4,0x05C5,0x05C7, + 0x0610,0x0611,0x0612,0x0613,0x0614,0x0615,0x0616,0x0617, + 0x0618,0x0619,0x061A,0x064B,0x064C,0x064D,0x064E,0x064F, + 0x0650,0x0651,0x0652,0x0653,0x0654,0x0655,0x0656,0x0657, + 0x0658,0x0659,0x065A,0x065B,0x065C,0x065D,0x065E,0x065F, + 0x0670,0x06D6,0x06D7,0x06D8,0x06D9,0x06DA,0x06DB,0x06DC, + 0x06DF,0x06E0,0x06E1,0x06E2,0x06E3,0x06E4,0x06E7,0x06E8, + 0x06EA,0x06EB,0x06EC,0x06ED,0x0711,0x0730,0x0731,0x0732, + 0x0733,0x0734,0x0735,0x0736,0x0737,0x0738,0x0739,0x073A, + 0x073B,0x073C,0x073D,0x073E,0x073F,0x0740,0x0741,0x0742, + 0x0743,0x0744,0x0745,0x0746,0x0747,0x0748,0x0749,0x074A, + 0x07A6,0x07A7,0x07A8,0x07A9,0x07AA,0x07AB,0x07AC,0x07AD, + 0x07AE,0x07AF,0x07B0,0x07EB,0x07EC,0x07ED,0x07EE,0x07EF, + 0x07F0,0x07F1,0x07F2,0x07F3,0x0816,0x0817,0x0818,0x0819, + 0x081B,0x081C,0x081D,0x081E,0x081F,0x0820,0x0821,0x0822, + 0x0823,0x0825,0x0826,0x0827,0x0829,0x082A,0x082B,0x082C, + 0x082D,0x0859,0x085A,0x085B,0x08E3,0x08E4,0x08E5,0x08E6, + 0x08E7,0x08E8,0x08E9,0x08EA,0x08EB,0x08EC,0x08ED,0x08EE, + 0x08EF,0x08F0,0x08F1,0x08F2,0x08F3,0x08F4,0x08F5,0x08F6, + 0x08F7,0x08F8,0x08F9,0x08FA,0x08FB,0x08FC,0x08FD,0x08FE, + 0x08FF,0x0900,0x0901,0x0902,0x093A,0x093C,0x0941,0x0942, + 0x0943,0x0944,0x0945,0x0946,0x0947,0x0948,0x094D,0x0951, + 0x0952,0x0953,0x0954,0x0955,0x0956,0x0957,0x0962,0x0963, + 0x0981,0x09BC,0x09C1,0x09C2,0x09C3,0x09C4,0x09CD,0x09E2, + 0x09E3,0x0A01,0x0A02,0x0A3C,0x0A41,0x0A42,0x0A47,0x0A48, + 0x0A4B,0x0A4C,0x0A4D,0x0A51,0x0A70,0x0A71,0x0A75,0x0A81, + 0x0A82,0x0ABC,0x0AC1,0x0AC2,0x0AC3,0x0AC4,0x0AC5,0x0AC7, + 0x0AC8,0x0ACD,0x0AE2,0x0AE3,0x0B01,0x0B3C,0x0B3F,0x0B41, + 0x0B42,0x0B43,0x0B44,0x0B4D,0x0B56,0x0B62,0x0B63,0x0B82, + 0x0BC0,0x0BCD,0x0C00,0x0C3E,0x0C3F,0x0C40,0x0C46,0x0C47, + 0x0C48,0x0C4A,0x0C4B,0x0C4C,0x0C4D,0x0C55,0x0C56,0x0C62, + 0x0C63,0x0C81,0x0CBC,0x0CBF,0x0CC6,0x0CCC,0x0CCD,0x0CE2, + 0x0CE3,0x0D01,0x0D41,0x0D42,0x0D43,0x0D44,0x0D4D,0x0D62, + 0x0D63,0x0DCA,0x0DD2,0x0DD3,0x0DD4,0x0DD6,0x0E31,0x0E34, + 0x0E35,0x0E36,0x0E37,0x0E38,0x0E39,0x0E3A,0x0E47,0x0E48, + 0x0E49,0x0E4A,0x0E4B,0x0E4C,0x0E4D,0x0E4E,0x0EB1,0x0EB4, + 0x0EB5,0x0EB6,0x0EB7,0x0EB8,0x0EB9,0x0EBB,0x0EBC,0x0EC8, + 0x0EC9,0x0ECA,0x0ECB,0x0ECC,0x0ECD,0x0F18,0x0F19,0x0F35, + 0x0F37,0x0F39,0x0F71,0x0F72,0x0F73,0x0F74,0x0F75,0x0F76, + 0x0F77,0x0F78,0x0F79,0x0F7A,0x0F7B,0x0F7C,0x0F7D,0x0F7E, + 0x0F80,0x0F81,0x0F82,0x0F83,0x0F84,0x0F86,0x0F87,0x0F8D, + 0x0F8E,0x0F8F,0x0F90,0x0F91,0x0F92,0x0F93,0x0F94,0x0F95, + 0x0F96,0x0F97,0x0F99,0x0F9A,0x0F9B,0x0F9C,0x0F9D,0x0F9E, + 0x0F9F,0x0FA0,0x0FA1,0x0FA2,0x0FA3,0x0FA4,0x0FA5,0x0FA6, + 0x0FA7,0x0FA8,0x0FA9,0x0FAA,0x0FAB,0x0FAC,0x0FAD,0x0FAE, + 0x0FAF,0x0FB0,0x0FB1,0x0FB2,0x0FB3,0x0FB4,0x0FB5,0x0FB6, + 0x0FB7,0x0FB8,0x0FB9,0x0FBA,0x0FBB,0x0FBC,0x0FC6,0x102D, + 0x102E,0x102F,0x1030,0x1032,0x1033,0x1034,0x1035,0x1036, + 0x1037,0x1039,0x103A,0x103D,0x103E,0x1058,0x1059,0x105E, + 0x105F,0x1060,0x1071,0x1072,0x1073,0x1074,0x1082,0x1085, + 0x1086,0x108D,0x109D,0x135D,0x135E,0x135F,0x1712,0x1713, + 0x1714,0x1732,0x1733,0x1734,0x1752,0x1753,0x1772,0x1773, + 0x17B4,0x17B5,0x17B7,0x17B8,0x17B9,0x17BA,0x17BB,0x17BC, + 0x17BD,0x17C6,0x17C9,0x17CA,0x17CB,0x17CC,0x17CD,0x17CE, + 0x17CF,0x17D0,0x17D1,0x17D2,0x17D3,0x17DD,0x180B,0x180C, + 0x180D,0x18A9,0x1920,0x1921,0x1922,0x1927,0x1928,0x1932, + 0x1939,0x193A,0x193B,0x1A17,0x1A18,0x1A1B,0x1A56,0x1A58, + 0x1A59,0x1A5A,0x1A5B,0x1A5C,0x1A5D,0x1A5E,0x1A60,0x1A62, + 0x1A65,0x1A66,0x1A67,0x1A68,0x1A69,0x1A6A,0x1A6B,0x1A6C, + 0x1A73,0x1A74,0x1A75,0x1A76,0x1A77,0x1A78,0x1A79,0x1A7A, + 0x1A7B,0x1A7C,0x1A7F,0x1AB0,0x1AB1,0x1AB2,0x1AB3,0x1AB4, + 0x1AB5,0x1AB6,0x1AB7,0x1AB8,0x1AB9,0x1ABA,0x1ABB,0x1ABC, + 0x1ABD,0x1B00,0x1B01,0x1B02,0x1B03,0x1B34,0x1B36,0x1B37, + 0x1B38,0x1B39,0x1B3A,0x1B3C,0x1B42,0x1B6B,0x1B6C,0x1B6D, + 0x1B6E,0x1B6F,0x1B70,0x1B71,0x1B72,0x1B73,0x1B80,0x1B81, + 0x1BA2,0x1BA3,0x1BA4,0x1BA5,0x1BA8,0x1BA9,0x1BAB,0x1BAC, + 0x1BAD,0x1BE6,0x1BE8,0x1BE9,0x1BED,0x1BEF,0x1BF0,0x1BF1, + 0x1C2C,0x1C2D,0x1C2E,0x1C2F,0x1C30,0x1C31,0x1C32,0x1C33, + 0x1C36,0x1C37,0x1CD0,0x1CD1,0x1CD2,0x1CD4,0x1CD5,0x1CD6, + 0x1CD7,0x1CD8,0x1CD9,0x1CDA,0x1CDB,0x1CDC,0x1CDD,0x1CDE, + 0x1CDF,0x1CE0,0x1CE2,0x1CE3,0x1CE4,0x1CE5,0x1CE6,0x1CE7, + 0x1CE8,0x1CED,0x1CF4,0x1CF8,0x1CF9,0x1DC0,0x1DC1,0x1DC2, + 0x1DC3,0x1DC4,0x1DC5,0x1DC6,0x1DC7,0x1DC8,0x1DC9,0x1DCA, + 0x1DCB,0x1DCC,0x1DCD,0x1DCE,0x1DCF,0x1DD0,0x1DD1,0x1DD2, + 0x1DD3,0x1DD4,0x1DD5,0x1DD6,0x1DD7,0x1DD8,0x1DD9,0x1DDA, + 0x1DDB,0x1DDC,0x1DDD,0x1DDE,0x1DDF,0x1DE0,0x1DE1,0x1DE2, + 0x1DE3,0x1DE4,0x1DE5,0x1DE6,0x1DE7,0x1DE8,0x1DE9,0x1DEA, + 0x1DEB,0x1DEC,0x1DED,0x1DEE,0x1DEF,0x1DF0,0x1DF1,0x1DF2, + 0x1DF3,0x1DF4,0x1DF5,0x1DFC,0x1DFD,0x1DFE,0x1DFF,0x20D0, + 0x20D1,0x20D2,0x20D3,0x20D4,0x20D5,0x20D6,0x20D7,0x20D8, + 0x20D9,0x20DA,0x20DB,0x20DC,0x20E1,0x20E5,0x20E6,0x20E7, + 0x20E8,0x20E9,0x20EA,0x20EB,0x20EC,0x20ED,0x20EE,0x20EF, + 0x20F0,0x2CEF,0x2CF0,0x2CF1,0x2D7F,0x2DE0,0x2DE1,0x2DE2, + 0x2DE3,0x2DE4,0x2DE5,0x2DE6,0x2DE7,0x2DE8,0x2DE9,0x2DEA, + 0x2DEB,0x2DEC,0x2DED,0x2DEE,0x2DEF,0x2DF0,0x2DF1,0x2DF2, + 0x2DF3,0x2DF4,0x2DF5,0x2DF6,0x2DF7,0x2DF8,0x2DF9,0x2DFA, + 0x2DFB,0x2DFC,0x2DFD,0x2DFE,0x2DFF,0x302A,0x302B,0x302C, + 0x302D,0x3099,0x309A,0xA66F,0xA674,0xA675,0xA676,0xA677, + 0xA678,0xA679,0xA67A,0xA67B,0xA67C,0xA67D,0xA69E,0xA69F, + 0xA6F0,0xA6F1,0xA802,0xA806,0xA80B,0xA825,0xA826,0xA8C4, + 0xA8E0,0xA8E1,0xA8E2,0xA8E3,0xA8E4,0xA8E5,0xA8E6,0xA8E7, + 0xA8E8,0xA8E9,0xA8EA,0xA8EB,0xA8EC,0xA8ED,0xA8EE,0xA8EF, + 0xA8F0,0xA8F1,0xA926,0xA927,0xA928,0xA929,0xA92A,0xA92B, + 0xA92C,0xA92D,0xA947,0xA948,0xA949,0xA94A,0xA94B,0xA94C, + 0xA94D,0xA94E,0xA94F,0xA950,0xA951,0xA980,0xA981,0xA982, + 0xA9B3,0xA9B6,0xA9B7,0xA9B8,0xA9B9,0xA9BC,0xA9E5,0xAA29, + 0xAA2A,0xAA2B,0xAA2C,0xAA2D,0xAA2E,0xAA31,0xAA32,0xAA35, + 0xAA36,0xAA43,0xAA4C,0xAA7C,0xAAB0,0xAAB2,0xAAB3,0xAAB4, + 0xAAB7,0xAAB8,0xAABE,0xAABF,0xAAC1,0xAAEC,0xAAED,0xAAF6, + 0xABE5,0xABE8,0xABED,0xFB1E,0xFE00,0xFE01,0xFE02,0xFE03, + 0xFE04,0xFE05,0xFE06,0xFE07,0xFE08,0xFE09,0xFE0A,0xFE0B, + 0xFE0C,0xFE0D,0xFE0E,0xFE0F,0xFE20,0xFE21,0xFE22,0xFE23, + 0xFE24,0xFE25,0xFE26,0xFE27,0xFE28,0xFE29,0xFE2A,0xFE2B, + 0xFE2C,0xFE2D,0xFE2E,0xFE2F, + 0x101FD,0x102E0,0x10376,0x10377,0x10378,0x10379,0x1037A,0x10A01, + 0x10A02,0x10A03,0x10A05,0x10A06,0x10A0C,0x10A0D,0x10A0E,0x10A0F, + 0x10A38,0x10A39,0x10A3A,0x10A3F,0x10AE5,0x10AE6,0x11001,0x11038, + 0x11039,0x1103A,0x1103B,0x1103C,0x1103D,0x1103E,0x1103F,0x11040, + 0x11041,0x11042,0x11043,0x11044,0x11045,0x11046,0x1107F,0x11080, + 0x11081,0x110B3,0x110B4,0x110B5,0x110B6,0x110B9,0x110BA,0x11100, + 0x11101,0x11102,0x11127,0x11128,0x11129,0x1112A,0x1112B,0x1112D, + 0x1112E,0x1112F,0x11130,0x11131,0x11132,0x11133,0x11134,0x11173, + 0x11180,0x11181,0x111B6,0x111B7,0x111B8,0x111B9,0x111BA,0x111BB, + 0x111BC,0x111BD,0x111BE,0x111CA,0x111CB,0x111CC,0x1122F,0x11230, + 0x11231,0x11234,0x11236,0x11237,0x112DF,0x112E3,0x112E4,0x112E5, + 0x112E6,0x112E7,0x112E8,0x112E9,0x112EA,0x11300,0x11301,0x1133C, + 0x11340,0x11366,0x11367,0x11368,0x11369,0x1136A,0x1136B,0x1136C, + 0x11370,0x11371,0x11372,0x11373,0x11374,0x114B3,0x114B4,0x114B5, + 0x114B6,0x114B7,0x114B8,0x114BA,0x114BF,0x114C0,0x114C2,0x114C3, + 0x115B2,0x115B3,0x115B4,0x115B5,0x115BC,0x115BD,0x115BF,0x115C0, + 0x115DC,0x115DD,0x11633,0x11634,0x11635,0x11636,0x11637,0x11638, + 0x11639,0x1163A,0x1163D,0x1163F,0x11640,0x116AB,0x116AD,0x116B0, + 0x116B1,0x116B2,0x116B3,0x116B4,0x116B5,0x116B7,0x1171D,0x1171E, + 0x1171F,0x11722,0x11723,0x11724,0x11725,0x11727,0x11728,0x11729, + 0x1172A,0x1172B,0x16AF0,0x16AF1,0x16AF2,0x16AF3,0x16AF4,0x16B30, + 0x16B31,0x16B32,0x16B33,0x16B34,0x16B35,0x16B36,0x16F8F,0x16F90, + 0x16F91,0x16F92,0x1BC9D,0x1BC9E,0x1D167,0x1D168,0x1D169,0x1D17B, + 0x1D17C,0x1D17D,0x1D17E,0x1D17F,0x1D180,0x1D181,0x1D182,0x1D185, + 0x1D186,0x1D187,0x1D188,0x1D189,0x1D18A,0x1D18B,0x1D1AA,0x1D1AB, + 0x1D1AC,0x1D1AD,0x1D242,0x1D243,0x1D244,0x1DA00,0x1DA01,0x1DA02, + 0x1DA03,0x1DA04,0x1DA05,0x1DA06,0x1DA07,0x1DA08,0x1DA09,0x1DA0A, + 0x1DA0B,0x1DA0C,0x1DA0D,0x1DA0E,0x1DA0F,0x1DA10,0x1DA11,0x1DA12, + 0x1DA13,0x1DA14,0x1DA15,0x1DA16,0x1DA17,0x1DA18,0x1DA19,0x1DA1A, + 0x1DA1B,0x1DA1C,0x1DA1D,0x1DA1E,0x1DA1F,0x1DA20,0x1DA21,0x1DA22, + 0x1DA23,0x1DA24,0x1DA25,0x1DA26,0x1DA27,0x1DA28,0x1DA29,0x1DA2A, + 0x1DA2B,0x1DA2C,0x1DA2D,0x1DA2E,0x1DA2F,0x1DA30,0x1DA31,0x1DA32, + 0x1DA33,0x1DA34,0x1DA35,0x1DA36,0x1DA3B,0x1DA3C,0x1DA3D,0x1DA3E, + 0x1DA3F,0x1DA40,0x1DA41,0x1DA42,0x1DA43,0x1DA44,0x1DA45,0x1DA46, + 0x1DA47,0x1DA48,0x1DA49,0x1DA4A,0x1DA4B,0x1DA4C,0x1DA4D,0x1DA4E, + 0x1DA4F,0x1DA50,0x1DA51,0x1DA52,0x1DA53,0x1DA54,0x1DA55,0x1DA56, + 0x1DA57,0x1DA58,0x1DA59,0x1DA5A,0x1DA5B,0x1DA5C,0x1DA5D,0x1DA5E, + 0x1DA5F,0x1DA60,0x1DA61,0x1DA62,0x1DA63,0x1DA64,0x1DA65,0x1DA66, + 0x1DA67,0x1DA68,0x1DA69,0x1DA6A,0x1DA6B,0x1DA6C,0x1DA75,0x1DA84, + 0x1DA9B,0x1DA9C,0x1DA9D,0x1DA9E,0x1DA9F,0x1DAA1,0x1DAA2,0x1DAA3, + 0x1DAA4,0x1DAA5,0x1DAA6,0x1DAA7,0x1DAA8,0x1DAA9,0x1DAAA,0x1DAAB, + 0x1DAAC,0x1DAAD,0x1DAAE,0x1DAAF,0x1E8D0,0x1E8D1,0x1E8D2,0x1E8D3, + 0x1E8D4,0x1E8D5,0x1E8D6,0xE0100,0xE0101,0xE0102,0xE0103,0xE0104, + 0xE0105,0xE0106,0xE0107,0xE0108,0xE0109,0xE010A,0xE010B,0xE010C, + 0xE010D,0xE010E,0xE010F,0xE0110,0xE0111,0xE0112,0xE0113,0xE0114, + 0xE0115,0xE0116,0xE0117,0xE0118,0xE0119,0xE011A,0xE011B,0xE011C, + 0xE011D,0xE011E,0xE011F,0xE0120,0xE0121,0xE0122,0xE0123,0xE0124, + 0xE0125,0xE0126,0xE0127,0xE0128,0xE0129,0xE012A,0xE012B,0xE012C, + 0xE012D,0xE012E,0xE012F,0xE0130,0xE0131,0xE0132,0xE0133,0xE0134, + 0xE0135,0xE0136,0xE0137,0xE0138,0xE0139,0xE013A,0xE013B,0xE013C, + 0xE013D,0xE013E,0xE013F,0xE0140,0xE0141,0xE0142,0xE0143,0xE0144, + 0xE0145,0xE0146,0xE0147,0xE0148,0xE0149,0xE014A,0xE014B,0xE014C, + 0xE014D,0xE014E,0xE014F,0xE0150,0xE0151,0xE0152,0xE0153,0xE0154, + 0xE0155,0xE0156,0xE0157,0xE0158,0xE0159,0xE015A,0xE015B,0xE015C, + 0xE015D,0xE015E,0xE015F,0xE0160,0xE0161,0xE0162,0xE0163,0xE0164, + 0xE0165,0xE0166,0xE0167,0xE0168,0xE0169,0xE016A,0xE016B,0xE016C, + 0xE016D,0xE016E,0xE016F,0xE0170,0xE0171,0xE0172,0xE0173,0xE0174, + 0xE0175,0xE0176,0xE0177,0xE0178,0xE0179,0xE017A,0xE017B,0xE017C, + 0xE017D,0xE017E,0xE017F,0xE0180,0xE0181,0xE0182,0xE0183,0xE0184, + 0xE0185,0xE0186,0xE0187,0xE0188,0xE0189,0xE018A,0xE018B,0xE018C, + 0xE018D,0xE018E,0xE018F,0xE0190,0xE0191,0xE0192,0xE0193,0xE0194, + 0xE0195,0xE0196,0xE0197,0xE0198,0xE0199,0xE019A,0xE019B,0xE019C, + 0xE019D,0xE019E,0xE019F,0xE01A0,0xE01A1,0xE01A2,0xE01A3,0xE01A4, + 0xE01A5,0xE01A6,0xE01A7,0xE01A8,0xE01A9,0xE01AA,0xE01AB,0xE01AC, + 0xE01AD,0xE01AE,0xE01AF,0xE01B0,0xE01B1,0xE01B2,0xE01B3,0xE01B4, + 0xE01B5,0xE01B6,0xE01B7,0xE01B8,0xE01B9,0xE01BA,0xE01BB,0xE01BC, + 0xE01BD,0xE01BE,0xE01BF,0xE01C0,0xE01C1,0xE01C2,0xE01C3,0xE01C4, + 0xE01C5,0xE01C6,0xE01C7,0xE01C8,0xE01C9,0xE01CA,0xE01CB,0xE01CC, + 0xE01CD,0xE01CE,0xE01CF,0xE01D0,0xE01D1,0xE01D2,0xE01D3,0xE01D4, + 0xE01D5,0xE01D6,0xE01D7,0xE01D8,0xE01D9,0xE01DA,0xE01DB,0xE01DC, + 0xE01DD,0xE01DE,0xE01DF,0xE01E0,0xE01E1,0xE01E2,0xE01E3,0xE01E4, + 0xE01E5,0xE01E6,0xE01E7,0xE01E8,0xE01E9,0xE01EA,0xE01EB,0xE01EC, + 0xE01ED,0xE01EE,0xE01EF, + }; + + static int unicodeCombiningCharTableSize = sizeof(unicodeCombiningCharTable) / sizeof(unicodeCombiningCharTable[0]); + + inline int unicodeIsCombiningChar(unsigned long cp) + { + int i; + for (i = 0; i < unicodeCombiningCharTableSize; i++) { + if (unicodeCombiningCharTable[i] == cp) { + return 1; + } + } + return 0; + } + +/* Get length of previous UTF8 character + */ + inline int unicodePrevUTF8CharLen(char* buf, int pos) + { + int end = pos--; + while (pos >= 0 && ((unsigned char)buf[pos] & 0xC0) == 0x80) { + pos--; + } + return end - pos; + } + +/* Get length of previous UTF8 character + */ + inline int unicodeUTF8CharLen(char* buf, int buf_len, int pos) + { + if (pos == buf_len) { return 0; } + unsigned char ch = buf[pos]; + if (ch < 0x80) { return 1; } + else if (ch < 0xE0) { return 2; } + else if (ch < 0xF0) { return 3; } + else { return 4; } + } + +/* Convert UTF8 to Unicode code point + */ + inline int unicodeUTF8CharToCodePoint( + const char* buf, + int len, + int* cp) + { + if (len) { + unsigned char byte = buf[0]; + if ((byte & 0x80) == 0) { + *cp = byte; + return 1; + } else if ((byte & 0xE0) == 0xC0) { + if (len >= 2) { + *cp = (((unsigned long)(buf[0] & 0x1F)) << 6) | + ((unsigned long)(buf[1] & 0x3F)); + return 2; + } + } else if ((byte & 0xF0) == 0xE0) { + if (len >= 3) { + *cp = (((unsigned long)(buf[0] & 0x0F)) << 12) | + (((unsigned long)(buf[1] & 0x3F)) << 6) | + ((unsigned long)(buf[2] & 0x3F)); + return 3; + } + } else if ((byte & 0xF8) == 0xF0) { + if (len >= 4) { + *cp = (((unsigned long)(buf[0] & 0x07)) << 18) | + (((unsigned long)(buf[1] & 0x3F)) << 12) | + (((unsigned long)(buf[2] & 0x3F)) << 6) | + ((unsigned long)(buf[3] & 0x3F)); + return 4; + } + } + } + return 0; + } + +/* Get length of grapheme + */ + inline int unicodeGraphemeLen(char* buf, int buf_len, int pos) + { + if (pos == buf_len) { + return 0; + } + int beg = pos; + pos += unicodeUTF8CharLen(buf, buf_len, pos); + while (pos < buf_len) { + int len = unicodeUTF8CharLen(buf, buf_len, pos); + int cp = 0; + unicodeUTF8CharToCodePoint(buf + pos, len, &cp); + if (!unicodeIsCombiningChar(cp)) { + return pos - beg; + } + pos += len; + } + return pos - beg; + } + +/* Get length of previous grapheme + */ + inline int unicodePrevGraphemeLen(char* buf, int pos) + { + if (pos == 0) { + return 0; + } + int end = pos; + while (pos > 0) { + int len = unicodePrevUTF8CharLen(buf, pos); + pos -= len; + int cp = 0; + unicodeUTF8CharToCodePoint(buf + pos, len, &cp); + if (!unicodeIsCombiningChar(cp)) { + return end - pos; + } + } + return 0; + } + + inline int isAnsiEscape(const char* buf, int buf_len, int* len) + { + if (buf_len > 2 && !memcmp("\033[", buf, 2)) { + int off = 2; + while (off < buf_len) { + switch (buf[off++]) { + case 'A': case 'B': case 'C': case 'D': + case 'E': case 'F': case 'G': case 'H': + case 'J': case 'K': case 'S': case 'T': + case 'f': case 'm': + *len = off; + return 1; + } + } + } + return 0; + } + +/* Get column position for the single line mode. + */ + inline int unicodeColumnPos(const char* buf, int buf_len) + { + int ret = 0; + + int off = 0; + while (off < buf_len) { + int len; + if (isAnsiEscape(buf + off, buf_len - off, &len)) { + off += len; + continue; + } + + int cp = 0; + len = unicodeUTF8CharToCodePoint(buf + off, buf_len - off, &cp); + + if (!unicodeIsCombiningChar(cp)) { + ret += unicodeIsWideChar(cp) ? 2 : 1; + } + + off += len; + } + + return ret; + } + +/* Get column position for the multi line mode. + */ + inline int unicodeColumnPosForMultiLine(char* buf, int buf_len, int pos, int cols, int ini_pos) + { + int ret = 0; + int colwid = ini_pos; + + int off = 0; + while (off < buf_len) { + int cp = 0; + int len = unicodeUTF8CharToCodePoint(buf + off, buf_len - off, &cp); + + int wid = 0; + if (!unicodeIsCombiningChar(cp)) { + wid = unicodeIsWideChar(cp) ? 2 : 1; + } + + int dif = (int)(colwid + wid) - (int)cols; + if (dif > 0) { + ret += dif; + colwid = wid; + } else if (dif == 0) { + colwid = 0; + } else { + colwid += wid; + } + + if (off >= pos) { + break; + } + + off += len; + ret += wid; + } + + return ret; + } + +/* Read UTF8 character from file. + */ + inline int unicodeReadUTF8Char(int fd, char* buf, int* cp) + { + int nread = read(fd,&buf[0],1); + + if (nread <= 0) { return nread; } + + unsigned char byte = buf[0]; + + if ((byte & 0x80) == 0) { + ; + } else if ((byte & 0xE0) == 0xC0) { + nread = read(fd,&buf[1],1); + if (nread <= 0) { return nread; } + } else if ((byte & 0xF0) == 0xE0) { + nread = read(fd,&buf[1],2); + if (nread <= 0) { return nread; } + } else if ((byte & 0xF8) == 0xF0) { + nread = read(fd,&buf[1],3); + if (nread <= 0) { return nread; } + } else { + return -1; + } + + return unicodeUTF8CharToCodePoint(buf, 4, cp); + } + +/* ======================= Low level terminal handling ====================== */ + +/* Set if to use or not the multi line mode. */ + inline void SetMultiLine(bool ml) { + mlmode = ml; + } + +/* Return true if the terminal name is in the list of terminals we know are + * not able to understand basic escape sequences. */ + inline bool isUnsupportedTerm(void) { +#ifndef _WIN32 + char *term = getenv("TERM"); + int j; + + if (term == NULL) return false; + for (j = 0; unsupported_term[j]; j++) + if (!strcasecmp(term,unsupported_term[j])) return true; +#endif + return false; + } + +/* Raw mode: 1960 magic shit. */ + inline bool enableRawMode(int fd) { +#ifndef _WIN32 + struct termios raw; + + if (!isatty(STDIN_FILENO)) goto fatal; + if (!atexit_registered) { + atexit(linenoiseAtExit); + atexit_registered = true; + } + if (tcgetattr(fd,&orig_termios) == -1) goto fatal; + + raw = orig_termios; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + /* output modes - disable post processing */ + // NOTE: Multithreaded issue #20 (https://github.com/yhirose/cpp-linenoise/issues/20) + // raw.c_oflag &= ~(OPOST); + /* control modes - set 8 bit chars */ + raw.c_cflag |= (CS8); + /* local modes - echoing off, canonical off, no extended functions, + * no signal chars (^Z,^C) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. + * We want read to return every single byte, without timeout. */ + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; + rawmode = true; +#else + if (!atexit_registered) { + /* Cleanup them at exit */ + atexit(linenoiseAtExit); + atexit_registered = true; + + /* Init windows console handles only once */ + hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hOut==INVALID_HANDLE_VALUE) goto fatal; + } + + DWORD consolemodeOut; + if (!GetConsoleMode(hOut, &consolemodeOut)) { + CloseHandle(hOut); + errno = ENOTTY; + return false; + }; + + hIn = GetStdHandle(STD_INPUT_HANDLE); + if (hIn == INVALID_HANDLE_VALUE) { + CloseHandle(hOut); + errno = ENOTTY; + return false; + } + + GetConsoleMode(hIn, &consolemodeIn); + /* Enable raw mode */ + SetConsoleMode(hIn, consolemodeIn & ~ENABLE_PROCESSED_INPUT); + + rawmode = true; +#endif + return true; + + fatal: + errno = ENOTTY; + return false; + } + + inline void disableRawMode(int fd) { +#ifdef _WIN32 + if (consolemodeIn) { + SetConsoleMode(hIn, consolemodeIn); + consolemodeIn = 0; + } + rawmode = false; +#else + /* Don't even check the return value as it's too late. */ + if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1) + rawmode = false; +#endif + } + +/* Use the ESC [6n escape sequence to query the horizontal cursor position + * and return it. On error -1 is returned, on success the position of the + * cursor. */ + inline int getCursorPosition(int ifd, int ofd) { + char buf[32]; + int cols, rows; + unsigned int i = 0; + + /* Report cursor location */ + if (write(ofd, "\x1b[6n", 4) != 4) return -1; + + /* Read the response: ESC [ rows ; cols R */ + while (i < sizeof(buf)-1) { + if (read(ifd,buf+i,1) != 1) break; + if (buf[i] == 'R') break; + i++; + } + buf[i] = '\0'; + + /* Parse it. */ + if (buf[0] != ESC || buf[1] != '[') return -1; + if (sscanf(buf+2,"%d;%d",&rows,&cols) != 2) return -1; + return cols; + } + +/* Try to get the number of columns in the current terminal, or assume 80 + * if it fails. */ + inline int getColumns(int ifd, int ofd) { +#ifdef _WIN32 + CONSOLE_SCREEN_BUFFER_INFO b; + + if (!GetConsoleScreenBufferInfo(hOut, &b)) return 80; + return b.srWindow.Right - b.srWindow.Left; +#else + struct winsize ws; + + if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { + /* ioctl() failed. Try to query the terminal itself. */ + int start, cols; + + /* Get the initial position so we can restore it later. */ + start = getCursorPosition(ifd,ofd); + if (start == -1) goto failed; + + /* Go to right margin and get position. */ + if (write(ofd,"\x1b[999C",6) != 6) goto failed; + cols = getCursorPosition(ifd,ofd); + if (cols == -1) goto failed; + + /* Restore position. */ + if (cols > start) { + char seq[32]; + snprintf(seq,32,"\x1b[%dD",cols-start); + if (write(ofd,seq,strlen(seq)) == -1) { + /* Can't recover... */ + } + } + return cols; + } else { + return ws.ws_col; + } + + failed: + return 80; +#endif + } + +/* Clear the screen. Used to handle ctrl+l */ + inline void linenoiseClearScreen(void) { + if (write(STDOUT_FILENO,"\x1b[H\x1b[2J",7) <= 0) { + /* nothing to do, just to avoid warning. */ + } + } + +/* Beep, used for completion when there is nothing to complete or when all + * the choices were already shown. */ + inline void linenoiseBeep(void) { + fprintf(stderr, "\x7"); + fflush(stderr); + } + +/* ============================== Completion ================================ */ + +/* This is an helper function for linenoiseEdit() and is called when the + * user types the key in order to complete the string currently in the + * input. + * + * The state of the editing is encapsulated into the pointed linenoiseState + * structure as described in the structure definition. */ + inline int completeLine(struct linenoiseState *ls, char *cbuf, int *c) { + std::vector lc; + int nread = 0, nwritten; + *c = 0; + + completionCallback(ls->buf,lc); + if (lc.empty()) { + linenoiseBeep(); + } else { + int stop = 0, i = 0; + + while(!stop) { + /* Show completion or original buffer */ + if (i < static_cast(lc.size())) { + struct linenoiseState saved = *ls; + + ls->len = ls->pos = static_cast(lc[i].size()); + ls->buf = &lc[i][0]; + refreshLine(ls); + ls->len = saved.len; + ls->pos = saved.pos; + ls->buf = saved.buf; + } else { + refreshLine(ls); + } + + //nread = read(ls->ifd,&c,1); +#ifdef _WIN32 + nread = win32read(c); + if (nread == 1) { + cbuf[0] = *c; + } +#else + nread = unicodeReadUTF8Char(ls->ifd,cbuf,c); +#endif + if (nread <= 0) { + *c = -1; + return nread; + } + + switch(*c) { + case 9: /* tab */ + i = (i+1) % (lc.size()+1); + if (i == static_cast(lc.size())) linenoiseBeep(); + break; + case 27: /* escape */ + /* Re-show original buffer */ + if (i < static_cast(lc.size())) refreshLine(ls); + stop = 1; + break; + default: + /* Update buffer and return */ + if (i < static_cast(lc.size())) { + nwritten = snprintf(ls->buf,ls->buflen,"%s",&lc[i][0]); + ls->len = ls->pos = nwritten; + } + stop = 1; + break; + } + } + } + + return nread; + } + +/* Register a callback function to be called for tab-completion. */ + inline void SetCompletionCallback(CompletionCallback fn) { + completionCallback = fn; + } + +/* =========================== Line editing ================================= */ + +/* Single line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. */ + inline void refreshSingleLine(struct linenoiseState *l) { + char seq[64]; + int pcolwid = unicodeColumnPos(l->prompt.c_str(), static_cast(l->prompt.length())); + int fd = l->ofd; + char *buf = l->buf; + int len = l->len; + int pos = l->pos; + std::string ab; + + while((pcolwid+unicodeColumnPos(buf, pos)) >= l->cols) { + int glen = unicodeGraphemeLen(buf, len, 0); + buf += glen; + len -= glen; + pos -= glen; + } + while (pcolwid+unicodeColumnPos(buf, len) > l->cols) { + len -= unicodePrevGraphemeLen(buf, len); + } + + /* Cursor to left edge */ + snprintf(seq,64,"\r"); + ab += seq; + /* Write the prompt and the current buffer content */ + ab += l->prompt; + ab.append(buf, len); + /* Erase to right */ + snprintf(seq,64,"\x1b[0K"); + ab += seq; + /* Move cursor to original position. */ + snprintf(seq,64,"\r\x1b[%dC", (int)(unicodeColumnPos(buf, pos)+pcolwid)); + ab += seq; + if (write(fd,ab.c_str(), static_cast(ab.length())) == -1) {} /* Can't recover from write error. */ + } + +/* Multi line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. */ + inline void refreshMultiLine(struct linenoiseState *l) { + char seq[64]; + int pcolwid = unicodeColumnPos(l->prompt.c_str(), static_cast(l->prompt.length())); + int colpos = unicodeColumnPosForMultiLine(l->buf, l->len, l->len, l->cols, pcolwid); + int colpos2; /* cursor column position. */ + int rows = (pcolwid+colpos+l->cols-1)/l->cols; /* rows used by current buf. */ + int rpos = (pcolwid+l->oldcolpos+l->cols)/l->cols; /* cursor relative row. */ + int rpos2; /* rpos after refresh. */ + int col; /* colum position, zero-based. */ + int old_rows = (int)l->maxrows; + int fd = l->ofd, j; + std::string ab; + + /* Update maxrows if needed. */ + if (rows > (int)l->maxrows) l->maxrows = rows; + + /* First step: clear all the lines used before. To do so start by + * going to the last row. */ + if (old_rows-rpos > 0) { + snprintf(seq,64,"\x1b[%dB", old_rows-rpos); + ab += seq; + } + + /* Now for every row clear it, go up. */ + for (j = 0; j < old_rows-1; j++) { + snprintf(seq,64,"\r\x1b[0K\x1b[1A"); + ab += seq; + } + + /* Clean the top line. */ + snprintf(seq,64,"\r\x1b[0K"); + ab += seq; + + /* Write the prompt and the current buffer content */ + ab += l->prompt; + ab.append(l->buf, l->len); + + /* Get text width to cursor position */ + colpos2 = unicodeColumnPosForMultiLine(l->buf, l->len, l->pos, l->cols, pcolwid); + + /* If we are at the very end of the screen with our prompt, we need to + * emit a newline and move the prompt to the first column. */ + if (l->pos && + l->pos == l->len && + (colpos2+pcolwid) % l->cols == 0) + { + ab += "\n"; + snprintf(seq,64,"\r"); + ab += seq; + rows++; + if (rows > (int)l->maxrows) l->maxrows = rows; + } + + /* Move cursor to right position. */ + rpos2 = (pcolwid+colpos2+l->cols)/l->cols; /* current cursor relative row. */ + + /* Go up till we reach the expected positon. */ + if (rows-rpos2 > 0) { + snprintf(seq,64,"\x1b[%dA", rows-rpos2); + ab += seq; + } + + /* Set column. */ + col = (pcolwid + colpos2) % l->cols; + if (col) + snprintf(seq,64,"\r\x1b[%dC", col); + else + snprintf(seq,64,"\r"); + ab += seq; + + l->oldcolpos = colpos2; + + if (write(fd,ab.c_str(), static_cast(ab.length())) == -1) {} /* Can't recover from write error. */ + } + +/* Calls the two low level functions refreshSingleLine() or + * refreshMultiLine() according to the selected mode. */ + inline void refreshLine(struct linenoiseState *l) { + if (mlmode) + refreshMultiLine(l); + else + refreshSingleLine(l); + } + +/* Insert the character 'c' at cursor current position. + * + * On error writing to the terminal -1 is returned, otherwise 0. */ + inline int linenoiseEditInsert(struct linenoiseState *l, const char* cbuf, int clen) { + if (l->len < l->buflen) { + if (l->len == l->pos) { + memcpy(&l->buf[l->pos],cbuf,clen); + l->pos+=clen; + l->len+=clen;; + l->buf[l->len] = '\0'; + if ((!mlmode && unicodeColumnPos(l->prompt.c_str(), static_cast(l->prompt.length()))+unicodeColumnPos(l->buf,l->len) < l->cols) /* || mlmode */) { + /* Avoid a full update of the line in the + * trivial case. */ + if (write(l->ofd,cbuf,clen) == -1) return -1; + } else { + refreshLine(l); + } + } else { + memmove(l->buf+l->pos+clen,l->buf+l->pos,l->len-l->pos); + memcpy(&l->buf[l->pos],cbuf,clen); + l->pos+=clen; + l->len+=clen; + l->buf[l->len] = '\0'; + refreshLine(l); + } + } + return 0; + } + +/* Move cursor on the left. */ + inline void linenoiseEditMoveLeft(struct linenoiseState *l) { + if (l->pos > 0) { + l->pos -= unicodePrevGraphemeLen(l->buf, l->pos); + refreshLine(l); + } + } + +/* Move cursor on the right. */ + inline void linenoiseEditMoveRight(struct linenoiseState *l) { + if (l->pos != l->len) { + l->pos += unicodeGraphemeLen(l->buf, l->len, l->pos); + refreshLine(l); + } + } + +/* Move cursor to the start of the line. */ + inline void linenoiseEditMoveHome(struct linenoiseState *l) { + if (l->pos != 0) { + l->pos = 0; + refreshLine(l); + } + } + +/* Move cursor to the end of the line. */ + inline void linenoiseEditMoveEnd(struct linenoiseState *l) { + if (l->pos != l->len) { + l->pos = l->len; + refreshLine(l); + } + } + +/* Substitute the currently edited line with the next or previous history + * entry as specified by 'dir'. */ +#define LINENOISE_HISTORY_NEXT 0 +#define LINENOISE_HISTORY_PREV 1 + inline void linenoiseEditHistoryNext(struct linenoiseState *l, int dir) { + if (history.size() > 1) { + /* Update the current history entry before to + * overwrite it with the next one. */ + history[history.size() - 1 - l->history_index] = l->buf; + /* Show the new entry */ + l->history_index += (dir == LINENOISE_HISTORY_PREV) ? 1 : -1; + if (l->history_index < 0) { + l->history_index = 0; + return; + } else if (l->history_index >= (int)history.size()) { + l->history_index = static_cast(history.size())-1; + return; + } + memset(l->buf, 0, l->buflen); + strcpy(l->buf,history[history.size() - 1 - l->history_index].c_str()); + l->len = l->pos = static_cast(strlen(l->buf)); + refreshLine(l); + } + } + +/* Delete the character at the right of the cursor without altering the cursor + * position. Basically this is what happens with the "Delete" keyboard key. */ + inline void linenoiseEditDelete(struct linenoiseState *l) { + if (l->len > 0 && l->pos < l->len) { + int glen = unicodeGraphemeLen(l->buf,l->len,l->pos); + memmove(l->buf+l->pos,l->buf+l->pos+glen,l->len-l->pos-glen); + l->len-=glen; + l->buf[l->len] = '\0'; + refreshLine(l); + } + } + +/* Backspace implementation. */ + inline void linenoiseEditBackspace(struct linenoiseState *l) { + if (l->pos > 0 && l->len > 0) { + int glen = unicodePrevGraphemeLen(l->buf,l->pos); + memmove(l->buf+l->pos-glen,l->buf+l->pos,l->len-l->pos); + l->pos-=glen; + l->len-=glen; + l->buf[l->len] = '\0'; + refreshLine(l); + } + } + +/* Delete the previosu word, maintaining the cursor at the start of the + * current word. */ + inline void linenoiseEditDeletePrevWord(struct linenoiseState *l) { + int old_pos = l->pos; + int diff; + + while (l->pos > 0 && l->buf[l->pos-1] == ' ') + l->pos--; + while (l->pos > 0 && l->buf[l->pos-1] != ' ') + l->pos--; + diff = old_pos - l->pos; + memmove(l->buf+l->pos,l->buf+old_pos,l->len-old_pos+1); + l->len -= diff; + refreshLine(l); + } + +/* This function is the core of the line editing capability of linenoise. + * It expects 'fd' to be already in "raw mode" so that every key pressed + * will be returned ASAP to read(). + * + * The resulting string is put into 'buf' when the user type enter, or + * when ctrl+d is typed. + * + * The function returns the length of the current buffer. */ + inline int linenoiseEdit(int stdin_fd, int stdout_fd, char *buf, int buflen, const char *prompt) + { + struct linenoiseState l; + + /* Populate the linenoise state that we pass to functions implementing + * specific editing functionalities. */ + l.ifd = stdin_fd; + l.ofd = stdout_fd; + l.buf = buf; + l.buflen = buflen; + l.prompt = prompt; + l.oldcolpos = l.pos = 0; + l.len = 0; + l.cols = getColumns(stdin_fd, stdout_fd); + l.maxrows = 0; + l.history_index = 0; + + /* Buffer starts empty. */ + l.buf[0] = '\0'; + l.buflen--; /* Make sure there is always space for the nulterm */ + + /* The latest history entry is always our current buffer, that + * initially is just an empty string. */ + AddHistory(""); + + if (write(l.ofd,prompt, static_cast(l.prompt.length())) == -1) return -1; + while(1) { + int c; + char cbuf[4]; + int nread; + char seq[3]; + +#ifdef _WIN32 + nread = win32read(&c); + if (nread == 1) { + cbuf[0] = c; + } +#else + nread = unicodeReadUTF8Char(l.ifd,cbuf,&c); +#endif + if (nread <= 0) return (int)l.len; + + /* Only autocomplete when the callback is set. It returns < 0 when + * there was an error reading from fd. Otherwise it will return the + * character that should be handled next. */ + if (c == 9 && completionCallback != NULL) { + nread = completeLine(&l,cbuf,&c); + /* Return on errors */ + if (c < 0) return l.len; + /* Read next character when 0 */ + if (c == 0) continue; + } + + switch(c) { + case ENTER: /* enter */ + if (!history.empty()) history.pop_back(); + if (mlmode) linenoiseEditMoveEnd(&l); + return (int)l.len; + case CTRL_C: /* ctrl-c */ + errno = EAGAIN; + return -1; + case BACKSPACE: /* backspace */ + case 8: /* ctrl-h */ + linenoiseEditBackspace(&l); + break; + case CTRL_D: /* ctrl-d, remove char at right of cursor, or if the + line is empty, act as end-of-file. */ + if (l.len > 0) { + linenoiseEditDelete(&l); + } else { + history.pop_back(); + return -1; + } + break; + case CTRL_T: /* ctrl-t, swaps current character with previous. */ + if (l.pos > 0 && l.pos < l.len) { + char aux = buf[l.pos-1]; + buf[l.pos-1] = buf[l.pos]; + buf[l.pos] = aux; + if (l.pos != l.len-1) l.pos++; + refreshLine(&l); + } + break; + case CTRL_B: /* ctrl-b */ + linenoiseEditMoveLeft(&l); + break; + case CTRL_F: /* ctrl-f */ + linenoiseEditMoveRight(&l); + break; + case CTRL_P: /* ctrl-p */ + linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_PREV); + break; + case CTRL_N: /* ctrl-n */ + linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_NEXT); + break; + case ESC: /* escape sequence */ + /* Read the next two bytes representing the escape sequence. + * Use two calls to handle slow terminals returning the two + * chars at different times. */ + if (read(l.ifd,seq,1) == -1) break; + if (read(l.ifd,seq+1,1) == -1) break; + + /* ESC [ sequences. */ + if (seq[0] == '[') { + if (seq[1] >= '0' && seq[1] <= '9') { + /* Extended escape, read additional byte. */ + if (read(l.ifd,seq+2,1) == -1) break; + if (seq[2] == '~') { + switch(seq[1]) { + case '3': /* Delete key. */ + linenoiseEditDelete(&l); + break; + } + } + } else { + switch(seq[1]) { + case 'A': /* Up */ + linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_PREV); + break; + case 'B': /* Down */ + linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_NEXT); + break; + case 'C': /* Right */ + linenoiseEditMoveRight(&l); + break; + case 'D': /* Left */ + linenoiseEditMoveLeft(&l); + break; + case 'H': /* Home */ + linenoiseEditMoveHome(&l); + break; + case 'F': /* End*/ + linenoiseEditMoveEnd(&l); + break; + } + } + } + + /* ESC O sequences. */ + else if (seq[0] == 'O') { + switch(seq[1]) { + case 'H': /* Home */ + linenoiseEditMoveHome(&l); + break; + case 'F': /* End*/ + linenoiseEditMoveEnd(&l); + break; + } + } + break; + default: + if (linenoiseEditInsert(&l,cbuf,nread)) return -1; + break; + case CTRL_U: /* Ctrl+u, delete the whole line. */ + buf[0] = '\0'; + l.pos = l.len = 0; + refreshLine(&l); + break; + case CTRL_K: /* Ctrl+k, delete from current to end of line. */ + buf[l.pos] = '\0'; + l.len = l.pos; + refreshLine(&l); + break; + case CTRL_A: /* Ctrl+a, go to the start of the line */ + linenoiseEditMoveHome(&l); + break; + case CTRL_E: /* ctrl+e, go to the end of the line */ + linenoiseEditMoveEnd(&l); + break; + case CTRL_L: /* ctrl+l, clear screen */ + linenoiseClearScreen(); + refreshLine(&l); + break; + case CTRL_W: /* ctrl+w, delete previous word */ + linenoiseEditDeletePrevWord(&l); + break; + } + } + return l.len; + } + +/* This function calls the line editing function linenoiseEdit() using + * the STDIN file descriptor set in raw mode. */ + inline bool linenoiseRaw(const char *prompt, std::string& line) { + bool quit = false; + + if (!isatty(STDIN_FILENO)) { + /* Not a tty: read from file / pipe. */ + std::getline(std::cin, line); + } else { + /* Interactive editing. */ + if (enableRawMode(STDIN_FILENO) == false) { + return quit; + } + + char buf[LINENOISE_MAX_LINE]; + auto count = linenoiseEdit(STDIN_FILENO, STDOUT_FILENO, buf, LINENOISE_MAX_LINE, prompt); + if (count == -1) { + quit = true; + } else { + line.assign(buf, count); + } + + disableRawMode(STDIN_FILENO); + printf("\n"); + } + return quit; + } + +/* The high level function that is the main API of the linenoise library. + * This function checks if the terminal has basic capabilities, just checking + * for a blacklist of stupid terminals, and later either calls the line + * editing function or uses dummy fgets() so that you will be able to type + * something even in the most desperate of the conditions. */ + inline bool Readline(const char *prompt, std::string& line) { + if (isUnsupportedTerm()) { + printf("%s",prompt); + fflush(stdout); + std::getline(std::cin, line); + return false; + } else { + return linenoiseRaw(prompt, line); + } + } + + inline std::string Readline(const char *prompt, bool& quit) { + std::string line; + quit = Readline(prompt, line); + return line; + } + + inline std::string Readline(const char *prompt) { + bool quit; // dummy + return Readline(prompt, quit); + } + +/* ================================ History ================================= */ + +/* At exit we'll try to fix the terminal to the initial conditions. */ + inline void linenoiseAtExit(void) { + disableRawMode(STDIN_FILENO); + } + +/* This is the API call to add a new entry in the linenoise history. + * It uses a fixed array of char pointers that are shifted (memmoved) + * when the history max length is reached in order to remove the older + * entry and make room for the new one, so it is not exactly suitable for huge + * histories, but will work well for a few hundred of entries. + * + * Using a circular buffer is smarter, but a bit more complex to handle. */ + inline bool AddHistory(const char* line) { + if (history_max_len == 0) return false; + + /* Don't add duplicated lines. */ + if (!history.empty() && history.back() == line) return false; + + /* If we reached the max length, remove the older line. */ + if (history.size() == history_max_len) { + history.erase(history.begin()); + } + history.push_back(line); + + return true; + } + +/* Set the maximum length for the history. This function can be called even + * if there is already some history, the function will make sure to retain + * just the latest 'len' elements if the new history length value is smaller + * than the amount of items already inside the history. */ + inline bool SetHistoryMaxLen(size_t len) { + if (len < 1) return false; + history_max_len = len; + if (len < history.size()) { + history.resize(len); + } + return true; + } + +/* Save the history in the specified file. On success *true* is returned + * otherwise *false* is returned. */ + inline bool SaveHistory(const char* path) { + std::ofstream f(path); // TODO: need 'std::ios::binary'? + if (!f) return false; + for (const auto& h: history) { + f << h << std::endl; + } + return true; + } + +/* Load the history from the specified file. If the file does not exist + * zero is returned and no operation is performed. + * + * If the file exists and the operation succeeded *true* is returned, otherwise + * on error *false* is returned. */ + inline bool LoadHistory(const char* path) { + std::ifstream f(path); + if (!f) return false; + std::string line; + while (std::getline(f, line)) { + AddHistory(line.c_str()); + } + return true; + } + + inline const std::vector& GetHistory() { + return history; + } + +} // namespace linenoise + +#ifdef _WIN32 +#undef isatty +#undef write +#undef read +#pragma warning(pop) +#endif + +#endif /* __LINENOISE_HPP */ \ No newline at end of file