Compare commits

...

123 Commits

Author SHA1 Message Date
Martin Michelsen 2bab3f2f8f fix episode 4 boss drops
Docker / Build (push) Has been cancelled
2025-09-30 23:19:44 -07:00
Martin Michelsen fdd0bfea08 rewrite quest metadata indexing
- split ep3 download quests from quest index
- fix Ep3 NTE download quests
- automatically detect battle/challenge params and area remaps
2025-09-28 23:26:14 -07:00
Martin Michelsen 48c225366f rewrite trade sequence 2025-09-26 21:45:24 -07:00
Martin Michelsen 0d88253334 add deadzone hint to font bitmap decoder 2025-09-26 21:45:04 -07:00
Martin Michelsen d7b17aa383 update some notes 2025-09-26 21:44:44 -07:00
Martin Michelsen ba131ab94a handle 6xE2 full inventory case 2025-09-25 21:20:48 -07:00
Martin Michelsen 648d9c5164 remove leader check on 6x17 2025-09-25 09:06:53 -07:00
Martin Michelsen 60487daf6f fix 6x17 checks for Vol Opt arena 2025-09-24 21:02:05 -07:00
Martin Michelsen e0c43836b3 add English AR code for Ep1&2 Trial 2025-09-22 18:05:42 -07:00
Martin Michelsen 719a403b1d show dmc patch in patches menu 2025-09-22 18:05:42 -07:00
Martin Michelsen 6f88c3d31a fix size field in 6xDD 2025-09-22 09:20:45 -07:00
Martin Michelsen 7114798e69 fix size check on 6xDD extension 2025-09-21 17:18:42 -07:00
Martin Michelsen 65384435a3 add extension for fractional EXP multipliers on BB 2025-09-21 13:16:28 -07:00
Martin Michelsen 4236ff62b1 add ep1 boss rush test 2025-09-19 09:16:28 -07:00
Martin Michelsen 277be9bcd6 obscure security updates 2025-09-18 23:48:14 -07:00
Martin Michelsen 9493e2d3e7 add some ar codes 2025-09-18 21:51:55 -07:00
Martin Michelsen 16b15162d5 add decrypt_pr1_data 2025-09-16 08:39:19 -07:00
Martin Michelsen 9854b93d02 support AFS tables in convert-common-item-set 2025-09-16 08:39:12 -07:00
Martin Michelsen d02ab1e7a5 add node about D5 non-repeatability on BB 2025-09-16 08:38:49 -07:00
Martin Michelsen e0c8ca677f add Windows build outline 2025-09-14 21:03:42 -07:00
Martin Michelsen 2cea44f790 add Ep3 JP subcommands in handler-tables 2025-09-14 13:37:39 -07:00
Martin Michelsen fb783034bc handle incorrect flags in 10 command 2025-09-14 13:04:42 -07:00
Martin Michelsen 40a6f49b29 fix crossplay challenge restart logic 2025-09-13 22:38:32 -07:00
Martin Michelsen dea0ac99c3 update some command notes 2025-09-13 22:38:27 -07:00
Martin Michelsen 24cf8e73c6 fix incorrect symlink on q080-gcn 2025-09-12 23:50:47 -07:00
Martin Michelsen c301a921e6 assume all GC NTE quests are Episode 1 2025-09-12 23:50:47 -07:00
Martin Michelsen 22d7825ba3 handle devil's/demon's in EnemyDamageSync 2025-09-12 23:45:51 -07:00
Martin Michelsen 526bfb64e5 fix memcpy call that gcc is unhappy with 2025-09-11 16:17:38 -07:00
Martin Michelsen 55cbf6e20b fix out-of-bounds access in 6x46, etc. 2025-09-11 10:14:39 -07:00
Martin Michelsen 0b86ffb227 fix use-after-free in AsyncPromise 2025-09-11 10:14:39 -07:00
Matt Swift e28596c825 Add Aberrant Grove custom quest 2025-09-11 09:31:14 -07:00
Matt Swift 716676b87d Add GC NTE quest symlinks 2025-09-11 09:31:14 -07:00
Martin Michelsen 5ca0265c37 remove unused argument 2025-09-10 22:10:47 -07:00
Martin Michelsen c7a0873ca8 fix cross-floor commands in EnemyDamageSync 2025-09-10 21:15:22 -07:00
Martin Michelsen b1d51cdbbe fix visibility for some patches 2025-09-09 23:18:09 -07:00
Martin Michelsen 5a7151bc63 minor proxy bugfixes 2025-09-09 23:18:01 -07:00
Martin Michelsen 49d861919f update some notes 2025-09-06 22:53:59 -07:00
Martin Michelsen 3f20c4239f remove cmake from explicit-install list in GH Actions script 2025-09-02 21:37:58 -07:00
Martin Michelsen 038f306661 update notes on some 6xB5 subcommands 2025-09-02 21:34:39 -07:00
Martin Michelsen 0575f3c9cf fix windows build 2025-09-02 21:34:19 -07:00
Martin Michelsen e37307acb3 fix bank load function when index not set 2025-08-29 18:49:32 -07:00
Martin Michelsen 4b32b41183 add note in readme about xbox connectivity 2025-08-29 10:33:51 -07:00
Martin Michelsen c8f8a6f65b clean up legacy format notes 2025-08-26 23:54:56 -07:00
Martin Michelsen 0c93275e88 describe some esoteric NTE and 11/2000 commands 2025-08-24 22:47:33 -07:00
Martin Michelsen c44ab27c7e update some command notes 2025-08-24 18:17:39 -07:00
Martin Michelsen 3f09a7b57b add version checks around bank access 2025-08-24 17:28:26 -07:00
Martin Michelsen 0b4d5b2f89 add BB BankSize patch 2025-08-22 22:39:32 -07:00
Martin Michelsen 45824b46fe support per-quest common and rare tables 2025-08-22 14:09:41 -07:00
Martin Michelsen e78f3142e3 update comment on send_lobby_list 2025-08-21 10:37:35 -07:00
Martin Michelsen 4166149841 add player check in HungryMagSound 2025-08-19 23:18:03 -07:00
Martin Michelsen 45131dabc0 fix dice range parsing in create-tournament 2025-08-19 20:22:41 -07:00
Martin Michelsen b235644575 expand leaf containers in text set serialization 2025-08-15 12:54:13 -07:00
Martin Michelsen 377d8beac3 implement $switchchar command 2025-08-14 23:44:16 -07:00
Martin Michelsen 16bff52575 update comments in expand_rate 2025-08-13 11:51:35 -07:00
Martin Michelsen 49fb7eba60 fix $bank when used with MoreSaveSlots 2025-08-13 11:42:20 -07:00
Martin Michelsen 00b46d7161 update game_flags notes 2025-08-13 11:42:07 -07:00
Martin Michelsen 5bea9d3a2b add warning about crossplay + stack limits 2025-08-07 00:00:25 -07:00
Martin Michelsen a9dcd4b87e enforce stack limits when loading BB character data
Docker / Build (push) Has been cancelled
2025-08-06 21:23:30 -07:00
Martin Michelsen 5c84581978 add names in show-battle-params 2025-08-06 21:03:20 -07:00
Martin Michelsen ab38a58e39 mention address config in readme 2025-08-06 21:02:30 -07:00
Martin Michelsen d430112a94 support chat shell command for non-proxy clients 2025-07-27 14:18:48 -07:00
Martin Michelsen 0cf59f874d use remote_addr for SocketChannel in send_reconnect 2025-07-26 16:54:13 -07:00
Martin Michelsen bf028ed0f6 fix data2 handling in 30 command from GetExtendedPlayerInfo 2025-07-24 21:37:36 -07:00
Martin Michelsen 1ecc41dea9 format show-item-tables output more cleanly 2025-07-24 18:38:14 -07:00
Justin Schwartz 648e15a016 document the original unit stars random state 2025-07-22 23:18:53 -07:00
Martin Michelsen 1729edc1d2 add dynamic switching in EnemyDamageSync 2025-07-22 00:27:21 -07:00
Martin Michelsen bbcc03f832 improve CommonItemSet JSON parser/serializer 2025-07-20 22:30:04 -07:00
Martin Michelsen 6827229c83 refine 6x79 a bit 2025-07-20 22:30:01 -07:00
Martin Michelsen 60291993b6 add configurable min levels for non-BB; closes #666 2025-07-11 17:57:39 -07:00
Martin Michelsen 118512ebb2 fix websocket timeout 2025-07-10 09:38:31 -07:00
Martin Michelsen ae9eaccd29 fix disconnect for websocket clients 2025-07-08 20:09:20 -07:00
Martin Michelsen 3025420aea fix headers in show-item-tables 2025-07-08 20:09:04 -07:00
Martin Michelsen 3c4ad43e71 add belra arm bug fix 2025-07-06 23:25:03 -07:00
Martin Michelsen 9e02b6c666 add $sound command 2025-07-06 21:41:31 -07:00
Martin Michelsen fe435c13d3 fix local address detection 2025-07-06 20:48:44 -07:00
Martin Michelsen 3b5145880c fix $loadchar description in readme 2025-07-06 15:35:56 -07:00
Martin Michelsen d965ff5031 add stat boosts to ItemPMT formatting 2025-07-06 13:57:31 -07:00
Martin Michelsen 22a89deb8b fix save game data timer 2025-07-05 20:27:24 -07:00
Martin Michelsen c9ba61a4b0 fix NAME_ONLY for units with kill counts 2025-07-05 19:54:30 -07:00
Martin Michelsen 0cdf2784cc fix text alignment in MoreSaveSlots 2025-07-05 19:49:20 -07:00
Martin Michelsen 76a948a45d fix unused variable 2025-07-03 00:27:38 -07:00
Martin Michelsen fd39a89957 fix BB proxy bugs 2025-07-02 21:14:32 -07:00
Martin Michelsen 0a5065707c use new phosg::Image class 2025-07-01 09:56:42 -07:00
Martin Michelsen 072e647c7b update readme 2025-06-29 11:22:40 -07:00
Martin Michelsen 148db03a9a fix copy-paste error in MoreSaveSlots patch 2025-06-24 20:53:33 -07:00
Martin Michelsen cff5ad23fc fix scroll bar setup in MoreSaveSlots 2025-06-24 20:12:49 -07:00
Martin Michelsen 3e174b7397 add notes on TObjSinBoard 2025-06-24 20:12:33 -07:00
Martin Michelsen e9bf51f3f7 save all fields when applying npc skins 2025-06-24 20:12:24 -07:00
Martin Michelsen 28ab1bea9c add IPv6 support in proxy 2025-06-17 01:19:26 -07:00
Martin Michelsen 923cc4ebb0 add missing xbox includes 2025-06-16 19:22:38 -07:00
Martin Michelsen e24a0e3c40 decrypt Ep3 player config at load time 2025-06-16 00:30:53 -07:00
Martin Michelsen a857cc9d03 update some notes 2025-06-16 00:10:50 -07:00
Martin Michelsen 8746b544b6 describe the PCv2-exclusive quest opcodes 2025-06-14 20:40:53 -07:00
Martin Michelsen ccd5baedf1 add notes from BB trial edition 2025-06-14 12:00:36 -07:00
Martin Michelsen 9621e89cd7 add notes and support for final PCv2 version 2025-06-14 00:35:56 -07:00
Martin Michelsen 3844c9881c add AccurateKillCount patch 2025-06-12 18:49:38 -07:00
Martin Michelsen 6999694f89 rewrite 6xE4 logic 2025-06-12 01:27:54 -07:00
Martin Michelsen 54acd931da use .label/.address in xbox client functions 2025-06-09 10:00:38 -07:00
Martin Michelsen 9bc9e219b5 add patch for disabling Xbox save signature validation 2025-06-07 19:32:21 -07:00
Martin Michelsen e8b2765a71 add xbox disk file formats 2025-06-07 19:26:34 -07:00
Martin Michelsen d4bc880018 make $killcount work for units too 2025-06-07 09:53:56 -07:00
Martin Michelsen c1a2742617 update readme 2025-06-07 09:53:35 -07:00
Martin Michelsen ebaeb2f70a update docs for find_inventory_item quest opcode 2025-06-05 21:33:51 -07:00
Martin Michelsen 0366e36edb add Xbox-US1 quest handlers 2025-06-05 20:59:41 -07:00
Martin Michelsen a0f52f01bb use 6x2F for infinite HP 2025-06-04 00:18:57 -07:00
Martin Michelsen bee4c55446 make client functions parameterizable by version 2025-06-04 00:16:43 -07:00
Martin Michelsen 1a6b26e56b add text-only matching in AddressTranslator 2025-06-03 09:59:19 -07:00
Martin Michelsen 1047d089d5 fix 6x0B error message 2025-05-31 23:15:23 -07:00
Martin Michelsen 2d6096cfda fix $savechar on BB 2025-05-31 23:15:00 -07:00
Martin Michelsen 7cbd9402d0 fix CallNativeFunctionGC
Docker / Build (push) Has been cancelled
2025-05-31 15:15:03 -07:00
Martin Michelsen 0396337994 fix inventory/bank debug messages 2025-05-31 15:14:04 -07:00
Martin Michelsen 6fbc0829ae add patch to replace Pinz shop cards 2025-05-31 10:56:01 -07:00
Martin Michelsen 4f41cbc9ce fix description generated in $item command 2025-05-31 10:07:11 -07:00
Martin Michelsen d1e6d75d70 fix TethVer detection hack 2025-05-31 10:04:09 -07:00
Martin Michelsen 067f2439ca make redirect wait apply to SocketChannels as well 2025-05-31 09:34:09 -07:00
Martin Michelsen 2d2edbd7be fix ping exception handler 2025-05-31 09:29:01 -07:00
Vargur f5f457aa6f Fix HTTP endpoint logic: remove incorrect negation in rare-tables path check
The !req.path.starts_with( was causing every subsequent command to be processed as a rare-tables substring command.
2025-05-30 19:29:52 -07:00
Martin Michelsen aabbafb749 fix game flag translation across v2/v3 boundary 2025-05-28 22:01:54 -07:00
Martin Michelsen e72e37f713 implement extended $infhp features on proxy server; closes #501 2025-05-27 19:34:47 -07:00
Martin Michelsen f884893b18 reprioritize to-do list 2025-05-27 19:34:25 -07:00
Martin Michelsen c74c0e2250 fix conditions 2025-05-26 23:52:43 -07:00
Martin Michelsen 5f4d2ec891 complete implementation of $checkchar and make slot count configurable; closes #645 2025-05-26 21:55:19 -07:00
Martin Michelsen 33b0ab3ed3 improve BB proxy functionality 2025-05-26 18:56:23 -07:00
871 changed files with 63553 additions and 26648 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- name: Install libraries (macOS)
if: ${{ matrix.os == 'macos-latest' }}
run: |
brew install cmake asio libiconv
brew install asio libiconv
cat << EOF > nproc
#!/bin/sh
+2
View File
@@ -14,6 +14,7 @@ CTestTestfile.cmake
install_manifest.txt
Makefile
Testing
build
# Files modified by the user and/or server that don't have defaults
system/config.json
@@ -35,6 +36,7 @@ system/patch-bb/.metadata-cache.json
# repository
files
make_release.py
notes-private
old-khyller
old-newserv
release
+3 -1
View File
@@ -102,7 +102,7 @@ set(SOURCES
src/Menu.cc
src/NetworkAddresses.cc
src/PatchFileIndex.cc
src/PlayerFilesManager.cc
src/PlayerInventory.cc
src/PlayerSubordinates.cc
src/PPKArchive.cc
src/ProxyCommands.cc
@@ -111,6 +111,7 @@ set(SOURCES
src/PSOGCObjectGraph.cc
src/PSOProtocol.cc
src/Quest.cc
src/QuestMetadata.cc
src/QuestScript.cc
src/RareItemSet.cc
src/ReceiveCommands.cc
@@ -136,6 +137,7 @@ target_link_libraries(newserv phosg::phosg ${Iconv_LIBRARIES} pthread resource_f
if (WIN32)
target_compile_definitions(newserv PUBLIC -DWINVER=0x0A00 -D_WIN32_WINNT=0x0A00)
target_link_libraries(newserv ws2_32 mswsock bcrypt iphlpapi -static -static-libgcc -static-libstdc++)
target_compile_options(newserv PRIVATE -Wa,-mbig-obj)
endif()
add_dependencies(newserv newserv-Revision-cc)
+84 -63
View File
@@ -26,7 +26,7 @@ See TODO.md for a list of known issues and future work I've curated, or go to th
* [Cross-version play](#cross-version-play)
* [Server-side saves](#server-side-saves)
* [Episode 3 features](#episode-3-features)
* [Memory patches, client functions, and DOL files](#memory-patches-client-functions-and-dol-files)
* [Memory patches, client functions, and DOL files](#memory-patches-and-client-functions)
* [Using newserv as a proxy](#using-newserv-as-a-proxy)
* [Chat commands](#chat-commands)
* [REST API](#rest-api)
@@ -54,16 +54,17 @@ At the time of its inception, Aeon was also called newserv, and you may find som
Independently of this project, there are many other PSO servers out there. Those that I know of that are (or were) public are listed here in approximate chronological order:
* (Early 2000s) **[Schtserv](https://schtserv.com/)**: The first public-access PSO server; written in Delphi by Schthack. Still active and popular as of early 2025. Schtserv is also the only other unofficial server to support all versions of PSO, including Episode 3. (Their implementation of Episode 3 is based on newserv's, which is itself based on Sega's.)
* (Early 2000s) **[Schtserv](https://schtserv.com/)**: The first public-access PSO server, written in Delphi by Schthack. Schtserv is the only other unofficial server to support Episode 3, their implementation of which is based on newserv's (which is based on Sega's).
* (2005) **Khyller**: An early attempt of mine to support PSO PC, GC, and BB. See above for more details.
* (2006) **Aeon**: My second attempt. Better than Khyller, but still unreliable.
* (2008) **Tethealla**: A fairly extensive implementation of PSOBB, written in C by Sodaboy. The public version of Tethealla has been [officially disowned](https://www.pioneer2.net/community/threads/tethealla-server-forums-removal.26365/) (as it is now more than 15 years old), but closed-source development continues. [Ephinea](https://ephinea.pioneer2.net/), currently the most popular PSOBB server, is the continuation of this project. Several other modern PSOBB servers are forks of the initial public version of Tethealla as well.
* (2008) **[Sylverant](https://sylverant.net/)** [(source)](https://sourceforge.net/projects/sylverant/): The second public-access PSO server; written in C by BlueCrab. Still active and popular as of early 2025.
* (2008) **Tethealla**: A fairly extensive implementation of PSOBB, written in C by Sodaboy. The public version of Tethealla has been [officially disowned](https://www.pioneer2.net/community/threads/tethealla-server-forums-removal.26365/) as it is now more than 15 years old, but closed-source development continues. [Ephinea](https://ephinea.pioneer2.net/) is the continuation of this project. Several other modern PSOBB servers are forks of the initial public version of Tethealla as well.
* (2008) **[Sylverant](https://sylverant.net/)** [(source)](https://sourceforge.net/projects/sylverant/): The second public-access PSO server, written in C by BlueCrab.
* (2015) **[Archon](https://github.com/dcrodman/archon)**: A PSOBB server written in Go by Drew Rodman.
* (2015) **[Idola](https://github.com/HybridEidolon/idolapsoserv)**: A PSOBB server written in Rust by HybridEidolon. Functionality status unknown; the project has been archived.
* (2017) **[Aselia](https://github.com/Solybum/Aselia)**: A PSOBB server written written in C# by Soly. It seems this was planned to be open-source at some point, but that has not (yet) happened.
* (2017) **[Aselia](https://github.com/Solybum/Aselia)**: A PSOBB server written in C# by Soly. It seems this was planned to be open-source at some point, but that has not (yet) happened.
* (2018) **newserv**: This project right here.
* (2019) **[Mechonis](https://gitlab.com/sora3087/mechonis)**: A PSOBB server with a microservice architecture written in TypeScript by TrueVision.
* (2020) **[Booma.Server](https://github.com/HelloKitty/Booma.Server)**: A PSOBB server written in C# by Glader, with Soly's help.
* (2021) **[Phantasmal World](https://github.com/DaanVandenBosch/phantasmal-world)**: A set of PSO tools, including a web-based model viewer and quest builder, and a PSO server, written by Daan Vanden Bosch.
* (2021) **[Elseware](http://git.sharnoth.com/jake/elseware)**: A PSOBB server written in Rust by Jake.
@@ -117,14 +118,15 @@ newserv supports all known versions of PSO, including various development protot
| GC Ep1&2 Plus | Yes | Yes | Yes |
| GC Ep3 NTE | Yes | Yes (2) | Yes |
| GC Ep3 | Yes | Yes | Yes |
| Xbox Ep1&2 Beta | Yes | Yes | Yes |
| Xbox Ep1&2 | Yes | Yes | Yes |
| Xbox Ep1&2 Beta | Yes (3) | Yes (3) | Yes (3) |
| Xbox Ep1&2 | Yes (3) | Yes (3) | Yes (3) |
| BB (vanilla) | Yes | Yes | Yes |
| BB (Tethealla) | Yes | Yes | Yes |
*Notes:*
1. *This is the only version of PSO that doesn't have any way to identify the player's account - there is no serial number or username. For this reason, AllowUnregisteredUsers must be enabled in config.json to support PC NTE, and PC NTE players receive a random Guild Card number every time they connect. To prevent abuse, PC NTE support can be disabled in config.json.*
2. *Episode 3 NTE battles are not well-tested; some things may not work. See notes/ep3-nte-differences.txt for a list of known differences between NTE and the final version. NTE and non-NTE players cannot battle each other.*
3. *PSO Xbox connects through Xbox Live, so you can't easily host a private server for this version of the game. See the [How to connect](#pso-xbox) section.*
# Setup
@@ -136,12 +138,10 @@ Currently newserv works on macOS, Windows, and Ubuntu Linux. It will likely work
1. Download the latest release.zip file from the [releases page](https://github.com/fuzziqersoftware/newserv/releases).
2. Extract the contents of the archive to some location on your computer.
3. (Optional) If you want to change any config options, go into the system/ folder, open config.json in a text editor, and edit it to your liking. There are comments in the file that describe what all the options do.
3. Go into the system/ folder, open config.json in a text editor, and edit it to your liking. There are comments in the file that describe what all the options do. Most of the options can be left alone if you want default behavior, but on Windows, you must change LocalAddress and ExternalAddress.
4. (Optional) If you plan to play Blue Burst on newserv, set up the patch directory. See [client patch directories](#client-patch-directories) for details.
5. Run the newserv executable.
If you're on an older version of Windows (before Windows 10), the Cygwin libraries included with the release may be incompatible. See [this issue](https://github.com/fuzziqersoftware/newserv/issues/621) for a possible workaround.
### Linux
There are currently no precompiled releases for Linux. To run newserv on Linux, you'll have to build it from source - see the section below.
@@ -168,6 +168,12 @@ To use newserv in other ways (e.g. for translating data), see the end of this do
The current version of newserv is cross-compiled using mingw-w64 on a macOS build machine, with the necessary libraries manually installed. Setting up such a build environment is tedious and not recommended; it's recommended to just use a release version instead.
Here is a rough outline of the Windows build process. You should only attempt this yourself if you're familiar with setting up build environments and can deal with issues you may encounter along the way.
1. Install recent versions of MinGW and CMake.
2. Build and install zlib, libiconv, asio, phosg, and resource_dasm into your MinGW environment.
3. Clone the newserv repository with symlinks enabled: `git clone -c core.symlinks=true https://github.com/fuzziqersoftware/newserv.git`
4. Build newserv via CMake.
## Client patch directories
newserv implements a patch server for PSO PC and PSO BB game data. Any file or directory you put in the system/patch-bb or system/patch-pc directories will be synced to clients when they connect to the patch server.
@@ -257,6 +263,10 @@ If you're using the tapserver BBA or modem type, you can make it connect to a ne
3. In PSO's network settings, enable DHCP ("Automatically obtain an IP address"), set DNS server address to "Automatic", and leave DHCP Hostname as "Not set". Leave the proxy server settings blank.
4. Start an online game.
### PSO Xbox
Unfortunately, you can't easily host a private server for PSO Xbox because the Xbox version of the game tunnels its connections through Xbox Live. There is a modern replacement for Xbox Live named [Insignia](https://insignia.live/), which supports the three main PSO Xbox servers, but as of now does not support other private PSO servers.
### PSO BB
The PSO BB client has been modified and distributed in many different forms. newserv supports most, but not all, of the common distributions. Unlike other versions, it's common for various BB clients to have different map files. It's important that the client and server have the same map files, so make sure to set up the patch directory based on the client you'll be using with newserv. (See the [client patch directories](#client-patch-directories) section for instructions on setting this up.)
@@ -308,7 +318,10 @@ For .dat files, the `LANGUAGE` token may be omitted. If it's present, then that
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-gc-e.bin` and `q058-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the files are within the retrieval/ directory within system/quests/.
Some quests (mostly battle and challenge mode quests) have additional JSON metadata files that describe how the server should handle them. These files include flags that can be used to hide the quest unless a preceding quest has been cleared, or to hide the quest unless purchased as a BB team reward. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See system/quests/battle/b88001.json for documentation on the exact format of the JSON file.
Some quests have additional JSON metadata files that describe how the server should handle them. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See the comments in system/quests/retrieval/q058.json for all of the available options and how to use them. Some of the options are:
- Disable or hide the quest if certain preceding quests aren't cleared or other conditions aren't met
- Enable the quest to be joined while in progress
- Override the common and/or rare item tables and set the allowed drop modes
Some quests may also include a .pvr file, which contains an image used in the quest. These files are named similarly to their .bin and .dat counterparts.
@@ -367,7 +380,7 @@ In the server drop modes, the item tables used to generate common items are in t
## Cross-version play
All versions of PSO can see and interact with each other in the lobby. By default, newserv allows V1 and V2 players to play together, and allows GC and Xbox players to play together. You can change these rules to allow all versions to play together, or to prevent versions from playing together, with the CompatibilityGroups setting in config.json.
All versions of PSO can see and interact with each other in the lobby. By default, newserv allows V1 and V2 players to play in games together, and allows GC and Xbox players to play in games together. You can change these rules to allow all versions to play in games together, or to prevent versions from playing in games together, with the CompatibilityGroups setting in config.json.
There are several cross-version restrictions that always apply regardless of the compatibility groups setting:
* DC V1 players cannot join DC V2 games if the game creator didn't choose to allow them.
@@ -448,7 +461,7 @@ There is no public editor for Episode 3 maps and quests, but the format is descr
Like quests, Episode 3 card definitions, maps, and quests are cached in memory. If you've changed any of these files, you can run `reload ep3-cards` or `reload ep3-maps` in the interactive shell to make the changes take effect without restarting the server.
## Memory patches, client functions, and DOL files
## Memory patches and client functions
You can put assembly files in the system/client-functions directory with filenames like PatchName.VERS.patch.s and they will appear in the Patches menu for clients that support client functions. Client functions are written in SH-4, PowerPC, or x86 assembly and are compiled when newserv is started. The assembly system's features are documented in the comments in system/client-functions/System/WriteMemoryGC.ppc.s.
@@ -456,50 +469,53 @@ The VERS token in client function filenames refers to the specific version of th
The specific versions are:
| Game | VERS | Architecture |
|------------------------------|------|---------------|
| PSO DC Network Trial Edition | 1OJ1 | Not supported |
| PSO DC 11/2000 prototype | 1OJ2 | Not supported |
| PSO DC 12/2000 prototype | 1OJ3 | Not supported |
| PSO DC 01/2001 prototype | 1OJ4 | Not supported |
| PSO DC v1 JP | 1OJF | Not supported |
| PSO DC v1 US | 1OEF | Not supported |
| PSO DC v1 EU | 1OPF | Not supported |
| PSO DC 08/2001 prototype | 2OJ5 | SH-4 |
| PSO DC v2 JP | 2OJF | SH-4 |
| PSO DC v2 US | 2OEF | SH-4 |
| PSO DC v2 EU | 2OPF | SH-4 |
| PSO PC (v2) | 2OJW | Not supported |
| PSO GC Trial Edition | 3OJT | PowerPC |
| PSO GC v1.2 JP | 3OJ2 | PowerPC |
| PSO GC v1.3 JP | 3OJ3 | PowerPC |
| PSO GC v1.4 (Plus) JP | 3OJ4 | PowerPC |
| PSO GC v1.5 (Plus) JP | 3OJ5 | PowerPC (1) |
| PSO GC v1.0 US | 3OE0 | PowerPC |
| PSO GC v1.1 US | 3OE1 | PowerPC |
| PSO GC v1.2 (Plus) US | 3OE2 | PowerPC (1) |
| PSO GC v1.0 EU | 3OP0 | PowerPC |
| PSO GC Ep3 Trial Edition | 3SJT | PowerPC |
| PSO GC Ep3 JP | 3SJ0 | PowerPC |
| PSO GC Ep3 US | 3SE0 | PowerPC (1) |
| PSO GC Ep3 EU | 3SP0 | PowerPC (1) |
| PSO Xbox Beta | 4OJB | x86 |
| PSO Xbox JP Disc | 4OJD | x86 |
| PSO Xbox JP TU | 4OJU | x86 |
| PSO Xbox US Disc | 4OED | x86 |
| PSO Xbox US TU | 4OEU | x86 |
| PSO Xbox EU Disc | 4OPD | x86 |
| PSO Xbox EU TU | 4OPU | x86 |
| PSO BB JP 1.25.11 | 59NJ | x86 |
| PSO BB JP 1.25.13 | 59NL | x86 |
| PSO BB Tethealla | 59NL | x86 |
| Game | VERS | CPU architecture |
|------------------------------|------|--------------------------------|
| PSO DC Network Trial Edition | 1OJ1 | Client functions not supported |
| PSO DC 11/2000 prototype | 1OJ2 | Client functions not supported |
| PSO DC 12/2000 prototype | 1OJ3 | Client functions not supported |
| PSO DC 01/2001 prototype | 1OJ4 | Client functions not supported |
| PSO DC v1 JP | 1OJF | Client functions not supported |
| PSO DC v1 US | 1OEF | Client functions not supported |
| PSO DC v1 EU | 1OPF | Client functions not supported |
| PSO DC 08/2001 prototype | 2OJ5 | SH-4 |
| PSO DC v2 JP | 2OJF | SH-4 |
| PSO DC v2 US | 2OEF | SH-4 |
| PSO DC v2 EU | 2OPF | SH-4 |
| PSO PC (v2) 04/2002 | 2OJW | Client functions not supported |
| PSO PC (v2) 02/2003 | 2OJZ | Client functions not supported |
| PSO GC Trial Edition | 3OJT | PowerPC |
| PSO GC v1.2 JP | 3OJ2 | PowerPC |
| PSO GC v1.3 JP | 3OJ3 | PowerPC |
| PSO GC v1.4 (Plus) JP | 3OJ4 | PowerPC |
| PSO GC v1.5 (Plus) JP | 3OJ5 | PowerPC (1) |
| PSO GC v1.0 US | 3OE0 | PowerPC |
| PSO GC v1.1 US | 3OE1 | PowerPC |
| PSO GC v1.2 (Plus) US | 3OE2 | PowerPC (1) |
| PSO GC v1.0 EU | 3OP0 | PowerPC |
| PSO GC Ep3 Trial Edition | 3SJT | PowerPC |
| PSO GC Ep3 JP | 3SJ0 | PowerPC |
| PSO GC Ep3 US | 3SE0 | PowerPC (1) |
| PSO GC Ep3 EU | 3SP0 | PowerPC (1) |
| PSO Xbox Beta | 4OJB | x86 |
| PSO Xbox JP Disc | 4OJD | x86 |
| PSO Xbox JP TU | 4OJU | x86 |
| PSO Xbox US Disc | 4OED | x86 |
| PSO Xbox US TU | 4OEU | x86 |
| PSO Xbox EU Disc | 4OPD | x86 |
| PSO Xbox EU TU | 4OPU | x86 |
| PSO BB JP 1.25.11 | 59NJ | x86 |
| PSO BB JP 1.25.13 | 59NL | x86 |
| PSO BB Tethealla | 59NL | x86 |
*Notes:*
1. *Client functions are only supported on these versions if EnableSendFunctionCallQuestNumbers is set in config.json. See the comments there for more information.*
newserv comes with a set of patches for many of the above versions, based on AR codes originally made by Ralf at GC-Forever and Aleron Ives. Many of them were originally posted in [this thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050).
newserv comes with a set of patches for many of the above versions. These are organized in subdirectories within system/client-functions/.
You can also put DOL files in the system/dol directory, and they will appear in the Programs menu for GC clients. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWordGC.ppc.s, WriteMemoryGC.ppc.s, and RunDOL.ppc.s must be present in the system/client-functions/System directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
### DOL loader
You can put DOL files in the system/dol directory, and they will appear in the Programs menu for GC clients. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWordGC.ppc.s, WriteMemoryGC.ppc.s, and RunDOL.ppc.s must be present in the system/client-functions/System directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
Like other kinds of data, functions and DOL files are cached in memory. If you've changed any of these files, you can run `reload functions` or `reload dol-files` in the interactive shell to make the changes take effect without restarting the server.
@@ -562,22 +578,25 @@ Some commands only work for clients not in proxy sessions. The chat commands are
* `$where`: Show your current floor number and coordinates. Mainly useful for debugging.
* `$qfread <field-name>` (non-proxy only): Show the value of a quest counter in your player data. The field names are defined in config.json.
* Debugging commands
* `$debug`: Enable or disable debug. You need the DEBUG flag in your user account to use this command. Enabling debug does several things:
* Basic debugging commands (special permissions not required)
* `$whatobj` and `$whatene` (non-proxy only): Tells you what the closest object or enemy spawn point is to your position, along with its coordinates and object or enemy ID. The full definition is also printed to the server's log.
* `$qcheck <flag-num>` (non-proxy only): Show the value of a quest flag. If you're in a game, show the value of the flag in that game; if you're in the lobby, show the saved value of that quest flag for your character (BB only).
* `$qgread <flag-num>` (non-proxy only): Show the value of a quest counter ("global flag").
* `$sound <sound-id>`: Play the given sound (GC only).
* Restricted debugging commands (`$debug` permission required)
* `$debug`: Enable debug mode. You need the DEBUG flag in your user account to use this command. Enabling debug does several things:
* You'll be able to use the rest of the commands in this section.
* You'll see in-game messages from the server when you take some actions, like killing enemies, opening boxes, or flipping switches.
* You'll see the rare seed value and floor variations when you join a game.
* You'll be placed into the last available slot in lobbies and games instead of the first, unless you're joining a BB solo-mode game.
* You'll be able to join games with any PSO version, not only those for which cross-version play is normally enabled. See the "Cross-version play" section above for details on this.
* Most of the commands in this section are enabled. (A few of them are always enabled and don't require `$debug`.)
* `$whatobj` and `$whatene` (non-proxy only): Tells you what the closest object or enemy spawn point is to your position, along with its coordinates and object or enemy ID. The full definition is also printed to the server's log. These commands can be used without `$debug` enabled.
* `$readmem <address>`: Read 4 bytes from the given address and show you the values.
* `$writemem <address> <data>`: Write data to the given address. Data is not required to be any specific size.
* `$nativecall <address> [arg1 ...]` (GC only): Call a native function on your client. Only arguments passed in registers are supported; calling functions that take many arguments is not supported.
* `$quest <number>` (non-proxy only): Load a quest by quest number. Can be used to load battle or challenge quests with only one player present. `$debug` is not required for this command if the specified quest has the AllowStartFromChatCommand field set in its metadata file.
* `$qcall <function-id>`: Call a quest function on your client.
* `$qcheck <flag-num>` (non-proxy only): Show the value of a quest flag. This command can be used without `$debug` enabled. If you're in a game, show the value of the flag in that game; if you're in the lobby, show the saved value of that quest flag for your character (BB only).
* `$qset <flag-num>` or `$qclear <flag-num>`: Set or clear a quest flag for everyone in the game. If you're in the lobby and on BB, set or clear the saved value of a quest flag in your character file.
* `$qgread <flag-num>` (non-proxy only): Show the value of a quest counter ("global flag"). This command can be used without `$debug` enabled.
* `$qgwrite <flag-num> <value>` (non-proxy only): Set the value of a quest counter ("global flag") for yourself.
* `$qsync <reg-num> <value>`: Set a quest register's value for yourself only. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
* `$qsyncall <reg-num> <value>`: Set a quest register's value for everyone in the game. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
@@ -587,7 +606,7 @@ Some commands only work for clients not in proxy sessions. The chat commands are
* `$sc <data>`: Send a command to yourself.
* `$ss <data>`: Send a command to the remote server (if in a proxy session) or to the game server.
* `$sb <data>`: Send a command to yourself, and to the remote server or game server.
* `$auction` (Episode 3 only): Bring up the CARD Auction menu, regardless of how many players are in the game or if you have a VIP card.
* `$auction` (Episode 3 only): Bring up the CARD Auction menu, even if there are fewer than 4 players are in the game or you don't have a VIP card.
* Personal state commands
* `$arrow <color-id>`: Change your lobby arrow color. The color may be specified by number (0-12) or by name (red, blue, green, yellow, purple, cyan, orange, pink, white, white2, white3, or black).
@@ -599,10 +618,11 @@ Some commands only work for clients not in proxy sessions. The chat commands are
* `$patch <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
* Character data commands (non-proxy only)
* `$switchchar <slot>` (BB only): Switch to a different character from your account without logging out.
* `$savechar <slot>`: Save your current character data on the server in the specified slot. See the [server-side saves section](#server-side-saves) for more details.
* `$loadchar <slot>`: Save your current character data on the server in the specified slot. See the [server-side saves section](#server-side-saves) for more details.
* `$loadchar <slot>`: Load character data from the specified slot on the server, and replace your current character with it. See the [server-side saves section](#server-side-saves) for more details.
* `$bbchar <username> <password> <slot>`: Save your current character data on the server in a different account's BB character slots. See the [server-side saves section](#server-side-saves) for more details.
* `$checkchar <slot>`: Tells you basic information about a server-side character previously saved using `$savechar`.
* `$checkchar [slot]`: Tells you basic information about a server-side character previously saved using `$savechar`. If `slot` is not given, tells you which slots are used and which are free.
* `$deletechar <slot>`: Deletes a server-side character previously saved using `$savechar`.
* `$edit <stat> <value>`: Modify your character data. See the [using $edit](#using-edit) section for details.
@@ -628,8 +648,8 @@ Some commands only work for clients not in proxy sessions. The chat commands are
* Cheat mode commands
* `$cheat` (non-proxy only): Enable or disable cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. By default, cheat mode is off in new games but can be enabled; there is an option in config.json that allows you to disable cheat mode entirely, or set it to on by default in new games. Cheat mode is always enabled on the proxy, unless cheat mode is disabled on the entire server.
* `$infhp`: Enable or disable infinite HP mode. Applies to only you; does not affect other players. When enabled, one-hit KO attacks will still kill you, but on most versions of the game (not DCv1, GC US 1.2, or GC JP 1.5), the server will automatically revive you if you die. On all versions except GC US 1.2 and GC JP 1.5, infinite HP also automatically cures status ailments.
* `$inftp`: Enable or disable infinite TP mode. Applies to only you; does not affect other players.
* `$infhp`: Enable or disable infinite HP mode. Applies to only you; does not affect other players. When enabled, one-hit KO attacks will still kill you, but on most versions of the game, the server will automatically revive you if you die. Infinite HP also automatically cures status ailments.
* `$inftp`: Enable or disable infinite TP mode. Applies to only you; does not affect other players. Does not work on DCv1 or earlier versions.
* `$warpme <floor-id>` (or `$warp <floor-id>`): Warp yourself to the given floor.
* `$warpall <floor-id>`: Warp everyone in the game to the given floor. You must be the leader to use this command, unless you're on the proxy.
* `$next`: Warp yourself to the next floor.
@@ -753,6 +773,7 @@ The data formats that newserv can convert to/from are:
| PSO DC save file (.vms) | `encrypt-vms-save` | `decrypt-vms-save` |
| PSO PC save file | `encrypt-pc-save` | `decrypt-pc-save` |
| PSO GC save file (.gci) | `encrypt-gci-save` | `decrypt-gci-save` |
| PSO Xbox save file | None | `decrypt-xbox-save` |
| PSO GC snapshot file | None | `decode-gci-snapshot` |
| Quest script (.bin) | `assemble-quest-script` | `disassemble-quest-script` |
| Quest map (.dat) | None | `disassemble-quest-map` |
+6 -5
View File
@@ -1,13 +1,14 @@
## General
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
- Clean up ItemParameterTable implementation (see comment at the top of the class definition)
- Handle MeetUserExtensions properly in 41 and C4 commands on the proxy (rewrite the embedded 19 command and put some metadata in the persistent config, perhaps)
- Make a server patch version of story flag fixer quest
- Make proxy server handle all login commands, including sending 9C when needed
- Fix enemy flag mapping in v2/v3 crossplay and test
- Handle items in crossplay - use the replacement table
- Make proxy server handle all login commands on non-BB, including sending 9C when needed
- Add $switchit command (activates switch flag(s) for nearest object, e.g. laser fence, door, fog collision)
- Add a way to persist flags across connections, at least on v3, because of Meet User + B2 enable quest interactions - maybe update the quest to patch one of the login commands so the server can tell it's enabled
- Handle items in crossplay - use the replacement table
- Handle MeetUserExtensions properly in 41 and C4 commands on the proxy (rewrite the embedded 19 command and put some metadata in the persistent config, perhaps)
- Clean up ItemParameterTable implementation (see comment at the top of the class definition)
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
## PSO DC
+72 -1
View File
@@ -17,7 +17,8 @@ Version codes (from README.md):
2OJF: PSO DC v2 JP
2OEF: PSO DC v2 US
2OPF: PSO DC v2 EU
2OJW: PSO PC (v2)
2OJW: PSO PC (v2) 04/2002
2OJZ: PSO PC (v2) 02/2003
3OJT: PSO GC Trial Edition
3OJ2: PSO GC v1.2 JP
3OJ3: PSO GC v1.3 JP
@@ -69,14 +70,50 @@ Disable serial number validation (untested)
8C2670B6 01E0
Disable item equip restrictions ("God of equip")
3OE0 => 0410521C 38000005
3OE1 => 0410521C 38000005
3OE2 => 041050E4 38000005
3OJ2 => 04104F78 38000005
3OJ3 => 04105154 38000005
3OJ4 => 04105240 38000005
3OJ5 => 041050D4 38000005
3OJT => 0415BF50 38000005
3OP0 => 041052D4 38000005
59NL => 005C9F31 E9A7000000
All rareable enemies are rare
3OE0 => 040AC944 60000000 // Hildeblue
040C1B70 60000000 // Rappies
040C3FC8 60000000 // Nar Lily
040EB050 48000010 // Pouilly Slime
3OE1 => 040AC944 60000000 // Hildeblue
040C1B70 60000000 // Rappies
040C3FC8 60000000 // Nar Lily
040EB050 48000010 // Pouilly Slime
3OE2 => 040ACAFC 60000000 // Hildeblue
040C1D08 60000000 // Rappies
040C4160 60000000 // Nar Lily
040EB1E8 48000010 // Pouilly Slime
3OJ2 => 040AC6B8 60000000 // Hildeblue
040C18CC 60000000 // Rappies
040C3D24 60000000 // Nar Lily
040EADAC 48000010 // Pouilly Slime
3OJ3 => 040AC9C4 60000000 // Hildeblue
040C1BD0 60000000 // Rappies
040C4028 60000000 // Nar Lily
040EB0B0 48000010 // Pouilly Slime
3OJ4 => 040ACB3C 60000000 // Hildeblue
040C1E04 60000000 // Rappies
040C41A0 60000000 // Nar Lily
040EB374 48000010 // Pouilly Slime
3OJ5 => 040ACAEC 60000000 // Hildeblue
040C1CF8 60000000 // Rappies
040C4150 60000000 // Nar Lily
040EB1D8 48000010 // Pouilly Slime
3OP0 => 040ACAC4 60000000 // Hildeblue
040C1CD0 60000000 // Rappies
040C4128 60000000 // Nar Lily
040EB1B0 48000010 // Pouilly Slime
Unlock all songs in BGM test
Note: sadly, there are no secret/unused ones
@@ -198,6 +235,16 @@ Unlock all COM decks
3SP0 => 042CB414 38600001
3SE0 => 042CA908 38600001
Enable marker color menu in all lobbies
3OJ2 => 04138200 3800000E
3OJ3 => 04138508 3800000E
3OJ4 => 041390AC 3800000E
3OJ5 => 041385B0 3800000E
3OE0 => 041384BC 3800000E
3OE1 => 041384BC 3800000E
3OE2 => 041385C0 3800000E
3OP0 => 04138840 3800000E
Enable all lobby counter options in non-CARD lobbies
3SE0 => 04096A8C 480000C0
04096B4C 38800007
@@ -218,7 +265,10 @@ Change HUD color mask
0438CA90 6000BBAA
Disable lobby event music (but keep the visuals)
3OJT => 040B2394 38000000
3SE0 => 040B705C 38000000
3SJ0 => 040B7078 38000000
3SP0 => 040B74A0 38000000
Enable Pinz's Shop Super Card Capsule Machine as a fourth option
3SE0 => 043101C0 38800004
@@ -433,6 +483,11 @@ Note: Without a TextEnglish.pr2/pr3 patch, the menu items for these sounds will
0442B6E0 802C0000
Use English language files
3OJT => 04189FE8 38000001
0418A010 38000001
0418A0A0 38000001
0418A0C8 38000001
04189EC4 3BC00001
3SJT => 0408E414 38600001
0408E448 38000001
0408E44C 900DA62C
@@ -721,4 +776,20 @@ Show extended item info when targeting a dropped item
04005190 4E800020
All weapons can do 3-hit combos
3OE0 => 041D3248 38000001
3OE1 => 041D3248 38000001
3OE2 => 041D3448 38000001
3OJ2 => 041D2DEC 38000001
3OJ3 => 041D3318 38000001
3OJ4 => 041D3144 38000001
3OJ5 => 041D33E4 38000001
3OP0 => 041D3904 38000001
Disable save file signature validation (for moving Xbox saves across consoles)
4OJB => 002F01CB 9090
4OJD => 002F0CDB 9090
4OJU => 002F22DB 9090
4OED => 002F212B 9090
4OEU => 002F22DB 9090
4OPD => 002F215B 9090
4OPU => 002F234B 9090
+935 -932
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -17,7 +17,7 @@
0019 = P2 Scientist after defeating dragon
001E = Entered Caves 1 (Gov 2-1)
001F = Entered De Rol Le in 2-4
0020 = De Ro lee defeated
0020 = De Rol Le defeated
0021 = Mines unlocked (P2 Tyrell after defeating De Rol Le)
0028 = Entered Mines 1
0029 = Entered Vol Opt Area
+4
View File
@@ -23,6 +23,10 @@ public:
return this->entries;
}
inline size_t num_entries() const {
return this->entries.size();
}
std::pair<const void*, size_t> get(size_t index) const;
std::string get_copy(size_t index) const;
phosg::StringReader get_reader(size_t index) const;
+52 -6
View File
@@ -635,7 +635,13 @@ public:
throw runtime_error("scan field too long; too many matches");
}
void find_all_matches(uint32_t src_addr, uint32_t src_size) const {
enum class MatchType {
ANY = 0,
TEXT,
DATA,
};
void find_all_matches(uint32_t src_addr, uint32_t src_size, MatchType type) const {
if (!this->src_mem) {
throw runtime_error("no source file selected");
}
@@ -660,19 +666,48 @@ public:
ExpandMethod::PPC_DATA_BACKWARD,
ExpandMethod::PPC_DATA_BOTH,
};
static const vector<ExpandMethod> ppc_text_methods = {
ExpandMethod::PPC_TEXT_FORWARD,
ExpandMethod::PPC_TEXT_FORWARD_WITH_BARRIER,
ExpandMethod::PPC_TEXT_BACKWARD,
ExpandMethod::PPC_TEXT_BACKWARD_WITH_BARRIER,
ExpandMethod::PPC_TEXT_BOTH,
ExpandMethod::PPC_TEXT_BOTH_WITH_BARRIER,
ExpandMethod::PPC_TEXT_BOTH_IGNORE_ORIGIN,
};
static const vector<ExpandMethod> ppc_data_methods = {
ExpandMethod::PPC_DATA_FORWARD,
ExpandMethod::PPC_DATA_BACKWARD,
ExpandMethod::PPC_DATA_BOTH,
};
static const vector<ExpandMethod> raw_methods = {
ExpandMethod::RAW_FORWARD,
ExpandMethod::RAW_BACKWARD,
ExpandMethod::RAW_BOTH,
};
const auto& methods = this->ppc_mems.count(it.second) ? ppc_methods : raw_methods;
for (size_t z = 0; z < methods.size(); z++) {
futures.emplace_back(async(&AddressTranslator::find_match, this, it.second, src_addr, src_size, methods[z]));
const vector<ExpandMethod>* methods;
if (this->ppc_mems.count(it.second)) {
if (type == MatchType::ANY) {
methods = &ppc_methods;
} else if (type == MatchType::TEXT) {
methods = &ppc_text_methods;
} else if (type == MatchType::DATA) {
methods = &ppc_data_methods;
} else {
throw logic_error("invalid match type");
}
} else {
methods = &raw_methods;
}
for (size_t z = 0; z < methods->size(); z++) {
futures.emplace_back(async(&AddressTranslator::find_match, this, it.second, src_addr, src_size, methods->at(z)));
}
unordered_set<uint32_t> match_addrs;
for (size_t z = 0; z < futures.size(); z++) {
const char* method_name = this->name_for_expand_method(methods[z]);
const char* method_name = this->name_for_expand_method(methods->at(z));
try {
uint32_t ret = futures[z].get();
log.info_f("({}) ({}) {:08X}", it.first, method_name, ret);
@@ -831,7 +866,18 @@ public:
} else if (tokens[0] == "match") {
this->find_all_matches(
stoul(tokens.at(1), nullptr, 16),
tokens.size() >= 3 ? stoul(tokens[2], nullptr, 16) : 0);
tokens.size() >= 3 ? stoul(tokens[2], nullptr, 16) : 0,
MatchType::ANY);
} else if (tokens[0] == "match-text") {
this->find_all_matches(
stoul(tokens.at(1), nullptr, 16),
tokens.size() >= 3 ? stoul(tokens[2], nullptr, 16) : 0,
MatchType::TEXT);
} else if (tokens[0] == "match-data") {
this->find_all_matches(
stoul(tokens.at(1), nullptr, 16),
tokens.size() >= 3 ? stoul(tokens[2], nullptr, 16) : 0,
MatchType::DATA);
} else if (tokens[0] == "match-be-le") {
this->find_all_be_to_le_data_matches(
stoul(tokens.at(1), nullptr, 16),
+6 -4
View File
@@ -87,7 +87,7 @@ struct HTTPServerLimits {
size_t max_http_data_size = 0x200000; // 2MB
size_t max_http_keepalive_idle_usecs = 300 * 1000 * 1000; // 5 minutes (0 = no limit)
size_t max_websocket_message_size = 0x200000; // 2MB
size_t max_websocket_idle_usecs = 300 * 1000 * 1000; // 5 minutes (0 = no limit)
size_t max_websocket_idle_usecs = 0; // No limit by default
};
extern const HTTPServerLimits DEFAULT_HTTP_LIMITS;
@@ -205,9 +205,11 @@ protected:
if (resp) {
co_await c->send_http_response(*resp);
}
auto* conn_header = req.get_header("connection");
if (!conn_header || (*conn_header != "keep-alive")) {
c->r.close();
if (!c->is_websocket) {
auto* conn_header = req.get_header("connection");
if (!conn_header || (*conn_header != "keep-alive")) {
c->r.close();
}
}
}
+31 -18
View File
@@ -34,7 +34,7 @@ public:
}
void set_value(T&& result) {
if (this->exc || this->val.has_value()) {
if (this->done()) {
throw std::logic_error("attempted to set value on completed promise");
}
this->val = result;
@@ -42,7 +42,7 @@ public:
}
void set_exception(std::exception_ptr ex) {
if (this->exc || this->val.has_value()) {
if (this->done()) {
throw std::logic_error("attempted to set value on completed promise");
}
this->exc = ex;
@@ -67,12 +67,13 @@ private:
std::optional<ResolverRef> resolver_ref;
void resolve() {
if (this->resolver_ref.has_value()) {
if (this->resolver_ref) {
auto* executor = this->resolver_ref->executor;
asio::post(*executor, [ref = std::move(this->resolver_ref)]() mutable -> void {
ref->resolve(std::error_code{});
});
ResolverRef ref = std::move(*this->resolver_ref);
this->resolver_ref.reset();
asio::post(*executor, [ref = std::move(ref)]() mutable -> void {
ref.resolve(std::error_code{});
});
}
}
};
@@ -102,7 +103,7 @@ public:
}
void set_value() {
if (this->exc || this->returned) {
if (this->done()) {
throw std::logic_error("attempted to set value on completed promise");
}
this->returned = true;
@@ -110,7 +111,7 @@ public:
}
void set_exception(std::exception_ptr ex) {
if (this->exc || this->returned) {
if (this->done()) {
throw std::logic_error("attempted to set value on completed promise");
}
this->exc = ex;
@@ -130,17 +131,18 @@ private:
asio::detail::awaitable_handler<asio::any_io_executor, std::error_code> resolve;
asio::any_io_executor* executor;
};
bool returned;
bool returned = false;
std::exception_ptr exc;
std::optional<ResolverRef> resolver_ref;
void resolve() {
if (this->resolver_ref.has_value()) {
if (this->resolver_ref) {
auto* executor = this->resolver_ref->executor;
asio::post(*executor, [ref = std::move(this->resolver_ref)]() mutable -> void {
ref->resolve(std::error_code{});
});
ResolverRef ref = std::move(*this->resolver_ref);
this->resolver_ref.reset();
asio::post(*executor, [ref = std::move(ref)]() mutable -> void {
ref.resolve(std::error_code{});
});
}
}
};
@@ -224,6 +226,14 @@ inline asio::ip::tcp::endpoint make_endpoint_ipv4(uint32_t addr, uint16_t port)
return asio::ip::tcp::endpoint(asio::ip::address_v4(addr), port);
}
inline asio::ip::tcp::endpoint make_endpoint_ipv6(const void* addr, uint16_t port) {
std::array<uint8_t, 0x10> bytes;
for (size_t z = 0; z < 0x10; z++) {
bytes[z] = reinterpret_cast<const uint8_t*>(addr)[z];
}
return asio::ip::tcp::endpoint(asio::ip::address_v6(bytes), port);
}
inline std::string str_for_endpoint(const asio::ip::tcp::endpoint& ep) {
return ep.address().to_string() + std::format(":{}", ep.port());
}
@@ -232,7 +242,7 @@ inline uint32_t ipv4_addr_for_asio_addr(const asio::ip::address& addr) {
if (!addr.is_v4()) {
throw std::runtime_error("Address is not IPv4");
}
return ntohl(addr.to_v4().to_uint());
return addr.to_v4().to_uint();
}
asio::awaitable<asio::ip::tcp::socket> async_connect_tcp(uint32_t ipv4_addr, uint16_t port);
@@ -243,10 +253,13 @@ template <typename FnT, typename... ArgTs>
asio::awaitable<std::invoke_result_t<FnT, ArgTs...>> call_on_thread_pool(asio::thread_pool& pool, FnT&& f, ArgTs&&... args) {
using ReturnT = std::invoke_result_t<FnT, ArgTs...>;
auto bound = std::bind(std::forward<FnT>(f), std::forward<ArgTs>(args)...);
AsyncPromise<ReturnT> promise;
asio::post(pool, [&promise, &bound]() -> void {
promise.set_value(bound());
// We have to use a shared_ptr here in case call_on_thread_pool is canceled
// (in that case, the posted callback will try to use promise after the
// call_on_thread_pool coroutine has been destroyed)
auto promise = std::make_shared<AsyncPromise<ReturnT>>();
asio::post(pool, [bound = std::move(bound), promise]() mutable {
promise->set_value(bound());
});
co_return co_await promise.get();
co_return co_await promise->get();
}
+14 -6
View File
@@ -9,10 +9,17 @@
using namespace std;
void BattleParamsIndex::Table::print(FILE* stream) const {
auto print_entry = +[](FILE* stream, const PlayerStats& e) {
void BattleParamsIndex::Table::print(FILE* stream, Episode episode) const {
auto print_entry = [stream, episode](const PlayerStats& e, size_t z) {
string names_str;
for (auto type : enemy_types_for_battle_param_index(episode, z)) {
if (!names_str.empty()) {
names_str += ", ";
}
names_str += phosg::name_for_enum(type);
}
phosg::fwrite_fmt(stream,
"{:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5}",
"{:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {}",
e.char_stats.atp,
e.char_stats.mst,
e.char_stats.evp,
@@ -22,15 +29,16 @@ void BattleParamsIndex::Table::print(FILE* stream) const {
e.char_stats.lck,
e.esp,
e.experience,
e.meseta);
e.meseta,
names_str);
};
for (size_t diff = 0; diff < 4; diff++) {
phosg::fwrite_fmt(stream, "{} ZZ ATP PSV EVP HP DFP ATA LCK ESP EXP DIFF\n",
phosg::fwrite_fmt(stream, "{} ZZ ATP PSV EVP HP DFP ATA LCK ESP EXP DIFF NAMES\n",
abbreviation_for_difficulty(diff));
for (size_t z = 0; z < 0x60; z++) {
phosg::fwrite_fmt(stream, " {:02X} ", z);
print_entry(stream, this->stats[diff][z]);
print_entry(this->stats[diff][z], z);
fputc('\n', stream);
}
}
+1 -1
View File
@@ -76,7 +76,7 @@ public:
/* AE00 */ parray<parray<MovementData, 0x60>, 4> movement_data;
/* F600 */
void print(FILE* stream) const;
void print(FILE* stream, Episode episode) const;
} __packed_ws__(Table, 0xF600);
BattleParamsIndex(
+260 -166
View File
@@ -24,6 +24,41 @@
using namespace std;
////////////////////////////////////////////////////////////////////////////////
// Tools
string str_for_flag_ranges(const vector<bool>& flags) {
string ret;
auto add_result = [&](size_t start, size_t end) {
if (!ret.empty()) {
ret.push_back(',');
}
if (start == end) {
ret += std::format("{}", start);
} else if (start == end - 1) {
ret += std::format("{},{}", start, end);
} else {
ret += std::format("{}-{}", start, end);
}
};
size_t range_start = 0;
bool in_range = false;
for (size_t z = 0; z < flags.size(); z++) {
if (flags[z] && !in_range) {
in_range = true;
range_start = z;
} else if (!flags[z] && in_range) {
in_range = false;
add_result(range_start, z - 1);
}
}
if (in_range) {
add_result(range_start, flags.size() - 1);
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////
// Checks
@@ -190,7 +225,7 @@ static asio::awaitable<void> server_command_announce_inner(const Args& a, bool m
send_text_or_scrolling_message(s, a.text, a.text);
}
} else {
auto from_name = a.c->character()->disp.name.decode(a.c->language());
auto from_name = a.c->character_file()->disp.name.decode(a.c->language());
if (mail) {
send_simple_mail(s, 0, from_name, a.text);
} else {
@@ -297,7 +332,7 @@ ChatCommandDefinition cc_auction(
});
static string name_for_client(shared_ptr<Client> c) {
auto player = c->character(false);
auto player = c->character_file(false);
if (player.get()) {
return escape_player_name(player->disp.name.decode(player->inventory.language));
}
@@ -382,28 +417,24 @@ ChatCommandDefinition cc_bank(
ssize_t new_char_index = a.text.empty() ? (a.c->bb_character_index + 1) : stol(a.text, nullptr, 0);
if (new_char_index == 0) {
if (a.c->use_shared_bank()) {
send_text_message(a.c, "$C6Using shared bank (0)");
} else {
send_text_message(a.c, "$C6Created shared bank (0)");
}
} else if (new_char_index <= 4) {
a.c->use_character_bank(new_char_index - 1);
auto bp = a.c->current_bank_character();
if (new_char_index <= 0) {
a.c->change_bank(-1);
send_text_message(a.c, "$C6Using shared bank");
} else if (new_char_index <= 127) {
a.c->change_bank(new_char_index - 1);
send_text_message_fmt(a.c, "$C6Using character {}'s bank", new_char_index);
auto name = escape_player_name(bp->disp.name.decode(a.c->language()));
send_text_message_fmt(a.c, "$C6Using {}\'s bank ({})", name, new_char_index);
} else {
throw precondition_failed("$C6Invalid bank number");
}
auto& bank = a.c->current_bank();
bank.assign_ids(0x99000000 + (a.c->lobby_client_id << 20));
auto bank = a.c->bank_file();
bank->assign_ids(0x99000000 + (a.c->lobby_client_id << 20));
a.c->log.info_f("Assigned bank item IDs");
a.c->print_bank();
send_text_message_fmt(a.c, "{} items\n{} Meseta", bank.num_items, bank.meseta);
send_text_message_fmt(a.c, "{} items\n{} Meseta", bank->items.size(), bank->meseta);
co_return;
});
@@ -421,7 +452,7 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
shared_ptr<Account> dest_account;
shared_ptr<BBLicense> dest_bb_license;
ssize_t dest_character_index = 0;
size_t dest_character_index = 0;
if (is_bb_conversion) {
vector<string> tokens = phosg::split(a.text, ' ');
if (tokens.size() != 3) {
@@ -429,9 +460,9 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
}
// username/password are tokens[0] and [1]
dest_character_index = stoll(tokens[2]) - 1;
if ((dest_character_index > 3) || (dest_character_index < 0)) {
throw precondition_failed("$C6Player index must\nbe in range 1-4");
dest_character_index = stoull(tokens[2]) - 1;
if (dest_character_index >= 127) {
throw precondition_failed("$C6Player index must\nbe in range 1-127");
}
try {
@@ -443,26 +474,27 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
}
} else {
dest_character_index = stoll(a.text) - 1;
if ((dest_character_index > 15) || (dest_character_index < 0)) {
throw precondition_failed("$C6Player index must\nbe in range 1-16");
dest_character_index = stoull(a.text) - 1;
if (dest_character_index >= s->num_backup_character_slots) {
throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots);
}
dest_account = a.c->login->account;
}
// If the client isn't BB, request the player info. (If they are BB, the
// server already has it)
auto ch = co_await send_get_player_info(a.c, true);
GetPlayerInfoResult ch;
if (a.c->version() == Version::BB_V4) {
ch.character = a.c->character_file();
ch.is_full_info = true;
} else {
ch = co_await send_get_player_info(a.c, true);
}
string filename = dest_bb_license
? Client::character_filename(dest_bb_license->username, dest_character_index)
: Client::backup_character_filename(dest_account->account_id, dest_character_index, is_ep3(a.c->version()));
if (s->player_files_manager->get_character(filename)) {
send_text_message(a.c, "$C6The target player\nis currently loaded.\nSign off in Blue\nBurst and try again.");
co_return;
}
if (ch.is_full_info) {
// Client sent 30; ch contains the verbatim save file from the client
if (ch.ep3_character) {
@@ -574,30 +606,49 @@ ChatCommandDefinition cc_checkchar(
throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount");
}
size_t index = stoull(a.text, nullptr, 0) - 1;
if (index > 15) {
throw precondition_failed("$C6Player index must\nbe in range 1-16");
}
auto s = a.c->require_server_state();
try {
if (is_ep3(a.c->version())) {
string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, true);
auto ch = phosg::load_object_file<PSOGCEp3CharacterFile::Character>(filename);
send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nCLv: on {}.{}, off {}.{}",
index + 1, ch.disp.visual.name.decode(),
name_for_section_id(ch.disp.visual.section_id), name_for_char_class(ch.disp.visual.char_class),
(ch.ep3_config.online_clv_exp / 100) + 1, ch.ep3_config.online_clv_exp % 100,
(ch.ep3_config.offline_clv_exp / 100) + 1, ch.ep3_config.offline_clv_exp % 100);
} else {
string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, false);
auto ch = PSOCHARFile::load_shared(filename, false).character_file;
send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nLevel {}",
index + 1, ch->disp.name.decode(),
name_for_section_id(ch->disp.visual.section_id), name_for_char_class(ch->disp.visual.char_class),
ch->disp.stats.level + 1);
if (a.text.empty()) {
bool is_ep3 = ::is_ep3(a.c->version());
vector<bool> flags;
flags.emplace_back(false);
for (size_t z = 0; z < s->num_backup_character_slots; z++) {
string filename = a.c->backup_character_filename(a.c->login->account->account_id, z, is_ep3);
flags.emplace_back(std::filesystem::is_regular_file(filename));
}
string used_str = str_for_flag_ranges(flags);
flags.flip();
flags[0] = false;
string free_str = str_for_flag_ranges(flags);
send_text_message_fmt(a.c, "Used: {}\nFree: {}", used_str, free_str);
} else {
size_t index = stoull(a.text, nullptr, 0) - 1;
if (index >= s->num_backup_character_slots) {
throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots);
}
try {
if (is_ep3(a.c->version())) {
string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, true);
auto ch = phosg::load_object_file<PSOGCEp3CharacterFile::Character>(filename);
send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nCLv: on {}.{}, off {}.{}",
index + 1, ch.disp.visual.name.decode(),
name_for_section_id(ch.disp.visual.section_id), name_for_char_class(ch.disp.visual.char_class),
(ch.ep3_config.online_clv_exp / 100) + 1, ch.ep3_config.online_clv_exp % 100,
(ch.ep3_config.offline_clv_exp / 100) + 1, ch.ep3_config.offline_clv_exp % 100);
} else {
string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, false);
auto ch = PSOCHARFile::load_shared(filename, false).character_file;
send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nLevel {}",
index + 1, ch->disp.name.decode(),
name_for_section_id(ch->disp.visual.section_id), name_for_char_class(ch->disp.visual.char_class),
ch->disp.stats.level + 1);
}
} catch (const phosg::cannot_open_file&) {
send_text_message_fmt(a.c, "No character in\nslot {}", index + 1);
}
} catch (const phosg::cannot_open_file&) {
send_text_message_fmt(a.c, "No character in\nslot {}", index + 1);
}
co_return;
@@ -723,41 +774,40 @@ ChatCommandDefinition cc_dropmode(
if (a.c->proxy_session) {
using DropMode = ProxySession::DropMode;
if (a.text.empty()) {
switch (a.c->proxy_session->drop_mode) {
case DropMode::DISABLED:
case ProxyDropMode::DISABLED:
send_text_message(a.c, "Drop mode: disabled");
break;
case DropMode::PASSTHROUGH:
case ProxyDropMode::PASSTHROUGH:
send_text_message(a.c, "Drop mode: default");
break;
case DropMode::INTERCEPT:
case ProxyDropMode::INTERCEPT:
send_text_message(a.c, "Drop mode: proxy");
break;
}
} else {
DropMode new_mode;
ProxyDropMode new_mode;
if ((a.text == "none") || (a.text == "disabled")) {
new_mode = DropMode::DISABLED;
new_mode = ProxyDropMode::DISABLED;
} else if ((a.text == "default") || (a.text == "passthrough")) {
new_mode = DropMode::PASSTHROUGH;
new_mode = ProxyDropMode::PASSTHROUGH;
} else if ((a.text == "proxy") || (a.text == "intercept")) {
new_mode = DropMode::INTERCEPT;
new_mode = ProxyDropMode::INTERCEPT;
} else {
throw precondition_failed("Invalid drop mode");
}
a.c->proxy_session->set_drop_mode(s, a.c->version(), a.c->override_random_seed, new_mode);
switch (a.c->proxy_session->drop_mode) {
case DropMode::DISABLED:
case ProxyDropMode::DISABLED:
send_text_message(a.c->channel, "Item drops disabled");
break;
case DropMode::PASSTHROUGH:
case ProxyDropMode::PASSTHROUGH:
send_text_message(a.c->channel, "Item drops changed\nto default mode");
break;
case DropMode::INTERCEPT:
case ProxyDropMode::INTERCEPT:
send_text_message(a.c->channel, "Item drops changed\nto proxy mode");
break;
}
@@ -767,36 +817,36 @@ ChatCommandDefinition cc_dropmode(
auto l = a.c->require_lobby();
if (a.text.empty()) {
switch (l->drop_mode) {
case Lobby::DropMode::DISABLED:
case ServerDropMode::DISABLED:
send_text_message(a.c, "Drop mode: disabled");
break;
case Lobby::DropMode::CLIENT:
case ServerDropMode::CLIENT:
send_text_message(a.c, "Drop mode: client");
break;
case Lobby::DropMode::SERVER_SHARED:
case ServerDropMode::SERVER_SHARED:
send_text_message(a.c, "Drop mode: server\nshared");
break;
case Lobby::DropMode::SERVER_PRIVATE:
case ServerDropMode::SERVER_PRIVATE:
send_text_message(a.c, "Drop mode: server\nprivate");
break;
case Lobby::DropMode::SERVER_DUPLICATE:
case ServerDropMode::SERVER_DUPLICATE:
send_text_message(a.c, "Drop mode: server\nduplicate");
break;
}
} else {
a.check_is_leader();
Lobby::DropMode new_mode;
ServerDropMode new_mode;
if ((a.text == "none") || (a.text == "disabled")) {
new_mode = Lobby::DropMode::DISABLED;
new_mode = ServerDropMode::DISABLED;
} else if (a.text == "client") {
new_mode = Lobby::DropMode::CLIENT;
new_mode = ServerDropMode::CLIENT;
} else if ((a.text == "shared") || (a.text == "server")) {
new_mode = Lobby::DropMode::SERVER_SHARED;
new_mode = ServerDropMode::SERVER_SHARED;
} else if ((a.text == "private") || (a.text == "priv")) {
new_mode = Lobby::DropMode::SERVER_PRIVATE;
new_mode = ServerDropMode::SERVER_PRIVATE;
} else if ((a.text == "duplicate") || (a.text == "dup")) {
new_mode = Lobby::DropMode::SERVER_DUPLICATE;
new_mode = ServerDropMode::SERVER_DUPLICATE;
} else {
throw precondition_failed("Invalid drop mode");
}
@@ -807,19 +857,19 @@ ChatCommandDefinition cc_dropmode(
l->drop_mode = new_mode;
switch (l->drop_mode) {
case Lobby::DropMode::DISABLED:
case ServerDropMode::DISABLED:
send_text_message(l, "Item drops disabled");
break;
case Lobby::DropMode::CLIENT:
case ServerDropMode::CLIENT:
send_text_message(l, "Item drops changed\nto client mode");
break;
case Lobby::DropMode::SERVER_SHARED:
case ServerDropMode::SERVER_SHARED:
send_text_message(l, "Item drops changed\nto server shared\nmode");
break;
case Lobby::DropMode::SERVER_PRIVATE:
case ServerDropMode::SERVER_PRIVATE:
send_text_message(l, "Item drops changed\nto server private\nmode");
break;
case Lobby::DropMode::SERVER_DUPLICATE:
case ServerDropMode::SERVER_DUPLICATE:
send_text_message(l, "Item drops changed\nto server duplicate\nmode");
break;
}
@@ -850,7 +900,7 @@ ChatCommandDefinition cc_edit(
using MatType = PSOBBCharacterFile::MaterialType;
try {
auto p = a.c->character();
auto p = a.c->character_file();
if (tokens.at(0) == "atp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
p->disp.stats.char_stats.atp = stoul(tokens.at(1));
} else if (tokens.at(0) == "mst" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
@@ -941,46 +991,13 @@ ChatCommandDefinition cc_edit(
if (tokens.at(1) == "none") {
p->disp.visual.extra_model = 0;
p->disp.visual.validation_flags &= 0xFD;
// Restore saved fields, if any
if (p->disp.visual.unused[0] == 0x8D) {
p->disp.visual.char_class = p->disp.visual.unused[1];
p->disp.visual.head = p->disp.visual.unused[2];
p->disp.visual.hair = p->disp.visual.unused[3];
p->disp.visual.unused.clear(0);
}
p->disp.visual.restore_npc_saved_fields();
} else {
uint8_t npc = npc_for_name(tokens.at(1), a.c->version());
if (npc == 0xFF) {
throw precondition_failed("$C6No such NPC");
}
// Some NPCs can crash the client if the character's class is
// incorrect. To handle this, we save the affected fields in the unused
// bytes after extra_model.
int8_t replacement_class = -1;
switch (npc) {
case 1: // Rico (replace with HUnewearl)
case 6: // Elly (replace with HUnewearl)
replacement_class = 0x01;
break;
case 0: // Ninja (replace with HUmar)
case 2: // Sonic (replace with HUmar)
case 5: // Flowen (replace with HUmar)
replacement_class = 0x00;
break;
}
if (replacement_class >= 0) {
if (p->disp.visual.unused[0] != 0x8D) {
p->disp.visual.unused[0] = 0x8D;
p->disp.visual.unused[1] = p->disp.visual.char_class;
p->disp.visual.unused[2] = p->disp.visual.head;
p->disp.visual.unused[3] = p->disp.visual.hair;
}
p->disp.visual.char_class = replacement_class;
p->disp.visual.head = 0x00;
p->disp.visual.hair = 0x00;
}
p->disp.visual.backup_npc_saved_fields();
p->disp.visual.extra_model = npc;
p->disp.visual.validation_flags |= 0x02;
}
@@ -1230,10 +1247,10 @@ ChatCommandDefinition cc_item(
} else {
auto l = a.c->require_lobby();
ItemData item = s->parse_item_description(a.c->version(), a.text);
item = s->parse_item_description(a.c->version(), a.text);
item.id = l->generate_item_id(a.c->lobby_client_id);
if ((l->drop_mode == Lobby::DropMode::SERVER_PRIVATE) || (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE)) {
if ((l->drop_mode == ServerDropMode::SERVER_PRIVATE) || (l->drop_mode == ServerDropMode::SERVER_DUPLICATE)) {
l->add_item(a.c->floor, item, a.c->pos, nullptr, nullptr, (1 << a.c->lobby_client_id));
send_drop_stacked_item_to_channel(s, a.c->channel, item, a.c->floor, a.c->pos);
} else {
@@ -1242,7 +1259,7 @@ ChatCommandDefinition cc_item(
}
}
string name = s->describe_item(a.c->version(), item, true);
string name = s->describe_item(a.c->version(), item, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES);
send_text_message(a.c, "$C7Item created:\n" + name);
co_return;
});
@@ -1303,29 +1320,39 @@ ChatCommandDefinition cc_killcount(
+[](const Args& a) -> asio::awaitable<void> {
a.check_is_proxy(false);
auto p = a.c->character();
size_t item_index;
try {
item_index = p->inventory.find_equipped_item(EquipSlot::WEAPON);
} catch (const out_of_range&) {
throw precondition_failed("No weapon equipped");
auto p = a.c->character_file();
vector<size_t> item_indexes;
for (size_t z = 0; z < p->inventory.num_items; z++) {
const auto& item = p->inventory.items[z];
if (item.is_equipped() && item.data.has_kill_count()) {
item_indexes.emplace_back(z);
}
}
const auto& item = p->inventory.items.at(item_index);
if (!item.data.has_kill_count()) {
throw precondition_failed("Weapon does not\nhave a kill count");
}
if (item_indexes.empty()) {
throw precondition_failed("No equipped items\nhave kill counts");
// Kill counts are only accurate on the server side at all times on BB. On
// other versions, we update the server's view of the client's inventory
// during games, but we can't track kills because the client doesn't inform
// the server whether it counted a kill for any individual enemy. So, on
// non-BB versions, the kill count is accurate at all times in the lobby
// (since kills can't occur there), or at the beginning of a game.
if ((a.c->version() == Version::BB_V4) || !a.c->require_lobby()->is_game()) {
send_text_message_fmt(a.c, "{} kills", item.data.get_kill_count());
} else {
send_text_message_fmt(a.c, "{} kills as of\ngame join", item.data.get_kill_count());
// Kill counts are only accurate on the server side at all times on BB.
// On other versions, we update the server's view of the client's
// inventory during games, but we can't track kills because the client
// doesn't inform the server whether it counted a kill for any
// individual enemy. So, on non-BB versions, the kill count is accurate
// at all times in the lobby (since kills can't occur there), or at the
// beginning of a game.
if ((a.c->version() == Version::BB_V4) || !a.c->require_lobby()->is_game()) {
send_text_message(a.c, "As of now:");
} else {
send_text_message(a.c, "As of game join:");
}
auto s = a.c->require_server_state();
for (size_t z : item_indexes) {
const auto& item = p->inventory.items[z];
string name = s->describe_item(
a.c->version(), item.data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES | ItemNameIndex::Flag::NAME_ONLY);
send_text_message_fmt(a.c, "{}$C7: {} kills", name, item.data.get_kill_count());
}
}
co_return;
});
@@ -1413,19 +1440,19 @@ ChatCommandDefinition cc_lobby_info(
"$C7Section ID: $C6{}$C7", name_for_section_id(l->effective_section_id())));
switch (l->drop_mode) {
case Lobby::DropMode::DISABLED:
case ServerDropMode::DISABLED:
lines.emplace_back("Drops disabled");
break;
case Lobby::DropMode::CLIENT:
case ServerDropMode::CLIENT:
lines.emplace_back("Client item table");
break;
case Lobby::DropMode::SERVER_SHARED:
case ServerDropMode::SERVER_SHARED:
lines.emplace_back("Server item table");
break;
case Lobby::DropMode::SERVER_PRIVATE:
case ServerDropMode::SERVER_PRIVATE:
lines.emplace_back("Server indiv items");
break;
case Lobby::DropMode::SERVER_DUPLICATE:
case ServerDropMode::SERVER_DUPLICATE:
lines.emplace_back("Server dup items");
break;
default:
@@ -1508,11 +1535,12 @@ ChatCommandDefinition cc_loadchar(
throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount");
}
auto s = a.c->require_server_state();
auto l = a.c->require_lobby();
size_t index = stoull(a.text, nullptr, 0) - 1;
if (index > 15) {
throw precondition_failed("$C6Player index must\nbe in range 1-16");
if (index >= s->num_backup_character_slots) {
throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots);
}
shared_ptr<PSOGCEp3CharacterFile::Character> ep3_char;
@@ -1524,7 +1552,6 @@ ChatCommandDefinition cc_loadchar(
if (a.c->version() == Version::BB_V4) {
// On BB, it suffices to simply send the character file again
auto s = a.c->require_server_state();
send_complete_player_bb(a.c);
send_player_leave_notification(l, a.c->lobby_client_id);
s->send_lobby_join_notifications(l, a.c);
@@ -1559,13 +1586,13 @@ ChatCommandDefinition cc_loadchar(
};
if (a.c->version() == Version::DC_V2) {
PSODCV2CharacterFile::Character dc_char = *a.c->character();
PSODCV2CharacterFile::Character dc_char = *a.c->character_file();
co_await send_set_extended_player_info(dc_char);
} else if (a.c->version() == Version::GC_NTE) {
PSOGCNTECharacterFileCharacter gc_char = *a.c->character();
PSOGCNTECharacterFileCharacter gc_char = *a.c->character_file();
co_await send_set_extended_player_info(gc_char);
} else if (a.c->version() == Version::GC_V3) {
PSOGCCharacterFile::Character gc_char = *a.c->character();
PSOGCCharacterFile::Character gc_char = *a.c->character_file();
co_await send_set_extended_player_info(gc_char);
} else if (a.c->version() == Version::GC_EP3_NTE) {
PSOGCEp3NTECharacter nte_char = *ep3_char;
@@ -1576,7 +1603,7 @@ ChatCommandDefinition cc_loadchar(
if (!a.c->login || !a.c->login->xb_license) {
throw runtime_error("XB client is not logged in");
}
PSOXBCharacterFileCharacter xb_char = *a.c->character();
PSOXBCharacterFile::Character xb_char = *a.c->character_file();
xb_char.guild_card.xb_user_id_high = (a.c->login->xb_license->user_id >> 32) & 0xFFFFFFFF;
xb_char.guild_card.xb_user_id_low = a.c->login->xb_license->user_id & 0xFFFFFFFF;
co_await send_set_extended_player_info(xb_char);
@@ -1599,7 +1626,7 @@ ChatCommandDefinition cc_matcount(
+[](const Args& a) -> asio::awaitable<void> {
a.check_is_proxy(false);
auto p = a.c->character();
auto p = a.c->character_file();
if (is_v1_or_v2(a.c->version())) {
send_text_message_fmt(a.c, "{} HP, {} TP",
p->get_material_usage(PSOBBCharacterFile::MaterialType::HP),
@@ -1841,7 +1868,7 @@ ChatCommandDefinition cc_qcheck(
if (!l->quest_flags_known || l->quest_flags_known->get(l->difficulty, flag_num)) {
send_text_message_fmt(a.c, "$C7Game: flag 0x{:X} ({})\nis {} on {}",
flag_num, flag_num,
a.c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
a.c->character_file()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
name_for_difficulty(l->difficulty));
} else {
send_text_message_fmt(a.c, "$C7Game: flag 0x{:X} ({})\nis unknown on {}",
@@ -1850,7 +1877,7 @@ ChatCommandDefinition cc_qcheck(
} else if (a.c->version() == Version::BB_V4) {
send_text_message_fmt(a.c, "$C7Player: flag 0x{:X} ({})\nis {} on {}",
flag_num, flag_num,
a.c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
a.c->character_file()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
name_for_difficulty(l->difficulty));
}
co_return;
@@ -1875,7 +1902,7 @@ static void command_qset_qclear(const Args& a, bool should_set) {
}
}
auto p = a.c->character(false);
auto p = a.c->character_file(false);
if (p) {
if (should_set) {
p->quest_flags.set(l->difficulty, flag_num);
@@ -1928,7 +1955,7 @@ ChatCommandDefinition cc_qfread(
throw runtime_error("invalid quest counter definition");
}
uint32_t counter_value = a.c->character()->quest_counters.at(counter_index) & mask;
uint32_t counter_value = a.c->character_file()->quest_counters.at(counter_index) & mask;
while (!(mask & 1)) {
mask >>= 1;
@@ -1948,7 +1975,7 @@ ChatCommandDefinition cc_qgread(
+[](const Args& a) -> asio::awaitable<void> {
a.check_is_proxy(false);
uint8_t counter_num = stoul(a.text, nullptr, 0);
const auto& counters = a.c->character()->quest_counters;
const auto& counters = a.c->character_file()->quest_counters;
if (counter_num >= counters.size()) {
throw precondition_failed("$C7Counter ID must be\nless than {}", counters.size());
} else {
@@ -1977,11 +2004,11 @@ ChatCommandDefinition cc_qgwrite(
uint8_t counter_num = stoul(tokens[0], nullptr, 0);
uint32_t value = stoul(tokens[1], nullptr, 0);
auto& counters = a.c->character()->quest_counters;
auto& counters = a.c->character_file()->quest_counters;
if (counter_num >= counters.size()) {
throw precondition_failed("$C7Counter ID must be\nless than {}", counters.size());
} else {
a.c->character()->quest_counters[counter_num] = value;
a.c->character_file()->quest_counters[counter_num] = value;
G_SetQuestCounter_BB_6xD2 cmd = {{0xD2, sizeof(G_SetQuestCounter_BB_6xD2) / 4, a.c->lobby_client_id}, counter_num, value};
send_command_t(a.c, 0x60, 0x00, cmd);
send_text_message_fmt(a.c, "$C7Quest counter {}\nset to {}", counter_num, value);
@@ -2047,8 +2074,7 @@ ChatCommandDefinition cc_quest(
a.check_is_game(true);
auto s = a.c->require_server_state();
Version effective_version = is_ep3(a.c->version()) ? Version::GC_V3 : a.c->version();
auto q = s->quest_index(effective_version)->get(stoul(a.text));
auto q = s->quest_index->get(stoul(a.text));
if (!q) {
throw precondition_failed("$C6Quest not found");
}
@@ -2058,11 +2084,20 @@ ChatCommandDefinition cc_quest(
if (l->count_clients() > 1) {
throw precondition_failed("$C6This command can only\nbe used with no\nother players present");
}
if (!q->allow_start_from_chat_command) {
if (!q->meta.allow_start_from_chat_command) {
throw precondition_failed("$C6This quest cannot\nbe started with the\n%squest command");
}
}
for (size_t client_id = 0; client_id < l->max_clients; client_id++) {
auto lc = l->clients[client_id];
if (lc) {
if (!q->version(lc->version(), lc->language())) {
throw precondition_failed("$C6Quest does not exist\nfor all players\' game\nversions");
}
}
}
set_lobby_quest(a.c->require_lobby(), q, true);
co_return;
});
@@ -2332,6 +2367,27 @@ ChatCommandDefinition cc_song(
co_return;
});
ChatCommandDefinition cc_sound(
{"$sound"},
+[](const Args& a) -> asio::awaitable<void> {
bool echo_to_all = (!a.text.empty() && a.text[0] == '!');
uint32_t sound_id = stoul(echo_to_all ? a.text.substr(1) : a.text, nullptr, 16);
auto l = a.c->require_lobby();
uint8_t area = l->area_for_floor(a.c->version(), a.c->floor);
G_PlaySoundFromPlayer_6xB2 cmd = {{0xB2, 0x03, 0x0000}, area, 0x00, a.c->lobby_client_id, sound_id};
if (!echo_to_all) {
send_command_t(a.c, 0x60, 0x00, cmd);
} else if (a.c->proxy_session) {
send_command_t(a.c, 0x60, 0x00, cmd);
send_command_t(a.c->proxy_session->server_channel, 0x60, 0x00, cmd);
} else {
a.check_debug_enabled();
send_command_t(a.c->require_lobby(), 0x60, 0x00, cmd);
}
co_return;
});
ChatCommandDefinition cc_spec(
{"$spec"},
+[](const Args& a) -> asio::awaitable<void> {
@@ -2476,7 +2532,7 @@ ChatCommandDefinition cc_surrender(
if (!ps || !ps->is_alive()) {
throw precondition_failed("$C6Defeated players\ncannot surrender");
}
string name = remove_color(a.c->character()->disp.name.decode(a.c->language()));
string name = remove_color(a.c->character_file()->disp.name.decode(a.c->language()));
send_text_message_fmt(l, "$C6{} has\nsurrendered", name);
for (const auto& watcher_l : l->watcher_lobbies) {
send_text_message_fmt(watcher_l, "$C6{} has\nsurrendered", name);
@@ -2581,6 +2637,47 @@ ChatCommandDefinition cc_swsetall(
co_return;
});
ChatCommandDefinition cc_switchchar(
{"$switchchar"},
+[](const Args& a) -> asio::awaitable<void> {
auto l = a.c->require_lobby();
auto s = a.c->require_server_state();
a.check_is_proxy(false);
a.check_is_game(false);
if (a.c->version() != Version::BB_V4) {
throw precondition_failed("This command can only\nbe used on BB");
}
int32_t index = stol(a.text, nullptr, 0) - 1;
if (index < 0) {
throw precondition_failed("Invalid slot number");
}
auto filename = Client::character_filename(a.c->login->bb_license->username, index);
if (!std::filesystem::is_regular_file(filename)) {
throw precondition_failed("No character exists\nin that slot");
}
a.c->save_and_unload_character();
a.c->bb_character_index = index;
a.c->bb_bank_character_index = index;
// TODO: This can trigger a client bug where the previous character's
// name label object isn't deleted if the leave and join notifications
// are received on the same frame. This results in the receiving player
// seeing both labels over the new character, with the latest one
// appearing on top. We could fix this by requiring each recipient to
// reply to a ping between the two commands, similar to how the 64 and
// 6x6D commands are split during game joining, but implementing that
// here seems not worth the effort given the low likelihood and impact of
// this bug.
send_complete_player_bb(a.c);
send_player_leave_notification(l, a.c->lobby_client_id);
s->send_lobby_join_notifications(l, a.c);
co_return;
});
ChatCommandDefinition cc_unset(
{"$unset"},
+[](const Args& a) -> asio::awaitable<void> {
@@ -2708,7 +2805,7 @@ ChatCommandDefinition cc_what(
throw precondition_failed("$C4No items are near you");
} else {
auto s = a.c->require_server_state();
string name = s->describe_item(a.c->version(), nearest_fi->data, true);
string name = s->describe_item(a.c->version(), nearest_fi->data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES);
send_text_message(a.c, name);
}
co_return;
@@ -2724,13 +2821,10 @@ static void whatobj_whatene_fn(const Args& a, bool include_objs, bool include_en
throw precondition_failed("$C4No map loaded");
}
// TODO: We should use the actual area if a loaded quest has reassigned
// them; it's likely that the variations will be wrong if we don't
uint8_t area, layout_var;
auto s = a.c->require_server_state();
if (l->episode != Episode::EP3) {
auto sdt = s->set_data_table(a.c->version(), l->episode, l->mode, l->difficulty);
area = sdt->default_area_for_floor(l->episode, a.c->floor);
area = l->area_for_floor(a.c->version(), a.c->floor);
layout_var = (a.c->floor < 0x10) ? l->variations.entries[a.c->floor].layout.load() : 0x00;
} else {
area = a.c->floor;
@@ -2854,7 +2948,7 @@ ChatCommandDefinition cc_where(
if (!a.c->proxy_session && l && l->is_game()) {
for (auto lc : l->clients) {
if (lc && (lc != a.c)) {
string name = lc->character()->disp.name.decode(lc->language());
string name = lc->character_file()->disp.name.decode(lc->language());
send_text_message_fmt(a.c, "$C6{}$C7 {:X}:{}",
name, lc->floor, FloorDefinition::get(l->episode, lc->floor).short_name);
}
+7 -7
View File
@@ -28,10 +28,10 @@ const vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
if (choice_id == 0x0000) {
return true;
}
uint32_t target_level = target_c->character()->disp.stats.level + 1;
uint32_t target_level = target_c->character_file()->disp.stats.level + 1;
switch (choice_id) {
case 0x0001:
return (labs(static_cast<int32_t>(target_level - searcher_c->character()->disp.stats.level)) <= 5);
return (labs(static_cast<int32_t>(target_level - searcher_c->character_file()->disp.stats.level)) <= 5);
case 0x0002:
return (target_level <= 10);
case 0x0003:
@@ -80,13 +80,13 @@ const vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
case 0x0000:
return true;
case 0x0010:
return target_c->character()->disp.visual.class_flags & 0x20;
return target_c->character_file()->disp.visual.class_flags & 0x20;
case 0x0011:
return target_c->character()->disp.visual.class_flags & 0x40;
return target_c->character_file()->disp.visual.class_flags & 0x40;
case 0x0012:
return target_c->character()->disp.visual.class_flags & 0x80;
return target_c->character_file()->disp.visual.class_flags & 0x80;
default:
return ((choice_id - 1) == target_c->character()->disp.visual.char_class);
return ((choice_id - 1) == target_c->character_file()->disp.visual.char_class);
}
},
},
@@ -143,7 +143,7 @@ const vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
{0x0006, "Challenge"},
},
.client_matches = +[](shared_ptr<Client>, shared_ptr<Client> target_c, uint16_t choice_id) -> bool {
uint16_t target_choice_id = target_c->character()->choice_search_config.get_setting(0x0204);
uint16_t target_choice_id = target_c->character_file()->choice_search_config.get_setting(0x0204);
return (choice_id == 0) || (target_choice_id == 0) || (choice_id == target_choice_id);
},
},
+338 -307
View File
@@ -244,12 +244,11 @@ Client::~Client() {
void Client::update_channel_name() {
string default_name = this->channel->default_name();
auto player = this->character(false, false);
auto player = this->character_file(false, false);
if (player) {
string name_str = player->disp.name.decode(this->language());
size_t level = player->disp.stats.level + 1;
this->channel->name = std::format("C-{:X} ({} Lv.{}) @ {}",
this->id, name_str, level, default_name);
this->channel->name = std::format("C-{:X} ({} Lv.{}) @ {}", this->id, name_str, level, default_name);
} else {
this->channel->name = std::format("C-{:X} @ {}", this->id, default_name);
}
@@ -261,11 +260,12 @@ void Client::reschedule_save_game_data_timer() {
return;
}
this->save_game_data_timer.expires_after(std::chrono::seconds(60));
this->idle_timeout_timer.async_wait([this](std::error_code ec) {
this->save_game_data_timer.async_wait([this](std::error_code ec) {
if (!ec) {
if (this->character(false)) {
if (this->character_file(false)) {
this->save_all();
}
this->reschedule_save_game_data_timer();
}
});
}
@@ -277,9 +277,13 @@ void Client::reschedule_ping_and_timeout_timers() {
this->send_ping_timer.async_wait([this](std::error_code ec) {
if (!ec) {
this->log.info_f("Sending ping command");
// The game doesn't use this timestamp; we only use it for debugging purposes
be_uint64_t timestamp = phosg::now();
this->channel->send(0x1D, 0x00, &timestamp, sizeof(be_uint64_t));
try {
// The game doesn't use this timestamp; we only use it for debugging purposes
be_uint64_t timestamp = phosg::now();
this->channel->send(0x1D, 0x00, &timestamp, sizeof(be_uint64_t));
} catch (const exception& e) {
this->log.warning_f("Failed to send ping: {}", e.what());
}
}
});
}
@@ -331,7 +335,7 @@ shared_ptr<const TeamIndex::Team> Client::team() const {
return nullptr;
}
auto p = this->character(false);
auto p = this->character_file(false);
auto s = this->require_server_state();
auto team = s->team_index->get_by_id(this->login->account->bb_team_id);
if (!team) {
@@ -379,7 +383,7 @@ bool Client::evaluate_quest_availability_expression(
if (game && !game->quest_flag_values) {
throw logic_error("quest flags are missing from game");
}
auto p = this->character();
auto p = this->character_file();
IntegralExpression::Env env = {
.flags = &p->quest_flags.data.at(difficulty),
.challenge_records = &p->challenge_records,
@@ -406,7 +410,8 @@ bool Client::can_see_quest(
if (!q->has_version_any_language(this->version())) {
return false;
}
return this->evaluate_quest_availability_expression(q->available_expression, game, event, difficulty, num_players, v1_present);
return this->evaluate_quest_availability_expression(
q->meta.available_expression, game, event, difficulty, num_players, v1_present);
}
bool Client::can_play_quest(
@@ -419,10 +424,11 @@ bool Client::can_play_quest(
if (!q->has_version_any_language(this->version())) {
return false;
}
if (num_players > q->max_players) {
if (num_players > q->meta.max_players) {
return false;
}
return this->evaluate_quest_availability_expression(q->enabled_expression, game, event, difficulty, num_players, v1_present);
return this->evaluate_quest_availability_expression(
q->meta.enabled_expression, game, event, difficulty, num_players, v1_present);
}
bool Client::can_use_chat_commands() const {
@@ -443,8 +449,184 @@ void Client::set_login(shared_ptr<Login> login) {
}
}
// System file
string Client::system_filename(const string& bb_username) {
return std::format("system/players/system_{}.psosys", bb_username);
}
string Client::system_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have system data");
}
if (!this->login || !this->login->bb_license) {
throw logic_error("client is not logged in");
}
return this->system_filename(this->login->bb_license->username);
}
shared_ptr<PSOBBBaseSystemFile> Client::system_file(bool allow_load) {
if (!this->system_data && allow_load) {
this->load_all_files();
}
return this->system_data;
}
shared_ptr<const PSOBBBaseSystemFile> Client::system_file(bool throw_if_missing) const {
if (!this->system_data.get() && throw_if_missing) {
throw runtime_error("system file is not loaded");
}
return this->system_data;
}
void Client::save_system_file() const {
if (!this->system_data) {
throw logic_error("no system file loaded");
}
string filename = this->system_filename();
phosg::save_object_file(filename, *this->system_data);
this->log.info_f("Saved system file {}", filename);
}
// Guild Card file
string Client::guild_card_filename(const string& bb_username) {
return std::format("system/players/guild_cards_{}.psocard", bb_username);
}
string Client::guild_card_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have saved Guild Card files");
}
if (!this->login || !this->login->bb_license) {
throw logic_error("client is not logged in");
}
return this->guild_card_filename(this->login->bb_license->username);
}
shared_ptr<PSOBBGuildCardFile> Client::guild_card_file(bool allow_load) {
if (!this->guild_card_data && allow_load) {
this->load_all_files();
}
return this->guild_card_data;
}
shared_ptr<const PSOBBGuildCardFile> Client::guild_card_file(bool allow_load) const {
if (!this->guild_card_data && allow_load) {
throw runtime_error("account data is not loaded");
}
return this->guild_card_data;
}
void Client::save_guild_card_file() const {
if (!this->guild_card_data.get()) {
throw logic_error("no Guild Card file loaded");
}
string filename = this->guild_card_filename();
phosg::save_object_file(filename, *this->guild_card_data);
this->log.info_f("Saved Guild Card file {}", filename);
}
// Character file
string Client::character_filename(const std::string& bb_username, ssize_t index) {
if (bb_username.empty()) {
throw logic_error("non-BB players do not have saved character filenames");
}
if (index < 0) {
throw logic_error("character index is not set");
}
return std::format("system/players/player_{}_{}.psochar", bb_username, index);
}
string Client::backup_character_filename(uint32_t account_id, size_t index, bool is_ep3) {
return std::format("system/players/backup_player_{}_{}.{}",
account_id, index, is_ep3 ? "pso3char" : "psochar");
}
string Client::character_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have saved character filenames");
}
if (!this->login || !this->login->bb_license) {
throw logic_error("client is not logged in");
}
return this->character_filename(this->login->bb_license->username, this->bb_character_index);
}
shared_ptr<PSOBBCharacterFile> Client::character_file(bool allow_load, bool allow_overlay) {
if (this->overlay_character_data && allow_overlay) {
return this->overlay_character_data;
}
if (!this->character_data && allow_load) {
if ((this->version() == Version::BB_V4) && (this->bb_character_index < 0)) {
throw runtime_error("character index not specified");
}
this->load_all_files();
}
return this->character_data;
}
shared_ptr<const PSOBBCharacterFile> Client::character_file(bool throw_if_missing, bool allow_overlay) const {
if (allow_overlay && this->overlay_character_data) {
return this->overlay_character_data;
}
if (!this->character_data && throw_if_missing) {
throw runtime_error("character data is not loaded");
}
return this->character_data;
}
void Client::save_character_file(
const string& filename,
shared_ptr<const PSOBBBaseSystemFile> system,
shared_ptr<const PSOBBCharacterFile> character) {
PSOCHARFile::save(filename, system, character);
}
void Client::save_ep3_character_file(
const string& filename,
const PSOGCEp3CharacterFile::Character& character) {
phosg::save_file(filename, &character, sizeof(character));
}
void Client::save_character_file() {
if (!this->system_data.get()) {
throw logic_error("no system file loaded");
}
if (!this->character_data.get()) {
throw logic_error("no character file loaded");
}
if (this->should_update_play_time) {
// This is slightly inaccurate, since fractions of a second are truncated
// off each time we save. I'm lazy, so insert shrug emoji here.
uint64_t t = phosg::now();
uint64_t seconds = (t - this->last_play_time_update) / 1000000;
this->character_data->play_time_seconds += seconds;
this->log.info_f("Added {} seconds to play time", seconds);
this->last_play_time_update = t;
if (this->bank_data && (this->bb_bank_character_index == this->bb_character_index)) {
this->character_data->bank = *this->bank_data;
this->log.info_f("Committed bank data back to character file");
}
}
auto filename = this->character_filename();
this->save_character_file(filename, this->system_data, this->character_data);
this->log.info_f("Saved character file {}", filename);
}
void Client::create_character_file(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
shared_ptr<const LevelTable> level_table) {
this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table);
this->save_character_file();
}
void Client::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_ptr<const LevelTable> level_table) {
this->overlay_character_data = make_shared<PSOBBCharacterFile>(*this->character(true, false));
this->overlay_character_data = make_shared<PSOBBCharacterFile>(*this->character_file(true, false));
if (rules->weapon_and_armor_mode != BattleRules::WeaponAndArmorMode::ALLOW) {
this->overlay_character_data->inventory.remove_all_items_of_type(0);
@@ -494,7 +676,7 @@ void Client::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_p
}
void Client::create_challenge_overlay(Version version, size_t template_index, shared_ptr<const LevelTable> level_table) {
auto p = this->character(true, false);
auto p = this->character_file(true, false);
const auto& tpl = get_challenge_template_definition(version, p->disp.visual.class_flags, template_index);
this->overlay_character_data = make_shared<PSOBBCharacterFile>(*p);
@@ -538,124 +720,109 @@ void Client::create_challenge_overlay(Version version, size_t template_index, sh
}
}
void Client::import_blocked_senders(const parray<le_uint32_t, 30>& blocked_senders) {
this->blocked_senders.clear();
for (size_t z = 0; z < blocked_senders.size(); z++) {
if (blocked_senders[z]) {
this->blocked_senders.emplace(blocked_senders[z]);
}
}
}
// Bank file
shared_ptr<PSOBBBaseSystemFile> Client::system_file(bool allow_load) {
if (!this->system_data && allow_load) {
this->load_all_files();
}
return this->system_data;
}
shared_ptr<const PSOBBBaseSystemFile> Client::system_file(bool allow_load) const {
if (!this->system_data.get() && allow_load) {
throw runtime_error("system data is not loaded");
}
return this->system_data;
}
shared_ptr<PSOBBCharacterFile> Client::character(bool allow_load, bool allow_overlay) {
if (this->overlay_character_data && allow_overlay) {
return this->overlay_character_data;
}
if (!this->character_data && allow_load) {
if ((this->version() == Version::BB_V4) && (this->bb_character_index < 0)) {
throw runtime_error("character index not specified");
}
this->load_all_files();
}
return this->character_data;
}
shared_ptr<const PSOBBCharacterFile> Client::character(bool allow_load, bool allow_overlay) const {
if (allow_overlay && this->overlay_character_data) {
return this->overlay_character_data;
}
if (!this->character_data && allow_load) {
throw runtime_error("character data is not loaded");
}
return this->character_data;
}
shared_ptr<PSOBBGuildCardFile> Client::guild_card_file(bool allow_load) {
if (!this->guild_card_data && allow_load) {
this->load_all_files();
}
return this->guild_card_data;
}
shared_ptr<const PSOBBGuildCardFile> Client::guild_card_file(bool allow_load) const {
if (!this->guild_card_data && allow_load) {
throw runtime_error("account data is not loaded");
}
return this->guild_card_data;
}
string Client::system_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have system data");
}
if (!this->login || !this->login->bb_license) {
throw logic_error("client is not logged in");
}
return std::format("system/players/system_{}.psosys", this->login->bb_license->username);
}
string Client::character_filename(const std::string& bb_username, ssize_t index) {
string Client::bank_filename(const std::string& bb_username, ssize_t index) {
if (bb_username.empty()) {
throw logic_error("non-BB players do not have character data");
throw logic_error("non-BB players do not have saved bank files");
}
if (index < 0) {
throw logic_error("character index is not set");
return std::format("system/players/shared_bank_{}.psobank", bb_username);
} else {
return std::format("system/players/player_{}_{}.psobank", bb_username, index);
}
return std::format("system/players/player_{}_{}.psochar", bb_username, index);
}
string Client::backup_character_filename(uint32_t account_id, size_t index, bool is_ep3) {
return std::format("system/players/backup_player_{}_{}.{}",
account_id, index, is_ep3 ? "pso3char" : "psochar");
}
string Client::character_filename(ssize_t index) const {
string Client::bank_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have character data");
throw logic_error("non-BB players do not have saved bank filenames");
}
if (!this->login || !this->login->bb_license) {
throw logic_error("client is not logged in");
}
return this->character_filename(this->login->bb_license->username, (index < 0) ? this->bb_character_index : index);
return this->bank_filename(this->login->bb_license->username, this->bb_bank_character_index);
}
string Client::guild_card_filename() const {
std::shared_ptr<PlayerBank> Client::bank_file(bool allow_load) {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have character data");
throw logic_error("non-BB players do not have saved bank files");
}
if (!this->login || !this->login->bb_license) {
throw logic_error("client is not logged in");
if (this->has_overlay()) {
throw std::runtime_error("bank is inaccessible when overlay is present");
}
return std::format("system/players/guild_cards_{}.psocard", this->login->bb_license->username);
if (!this->bank_data && allow_load) {
try {
// If there's a psobank file, load it and ignore the character file bank
auto filename = this->bank_filename();
auto f = phosg::fopen_unique(filename, "rb");
this->bank_data = make_shared<PlayerBank>();
this->bank_data->load(f.get());
this->log.info_f("Loaded bank data from {}", filename);
} catch (const phosg::cannot_open_file&) {
// If there isn't a psobank file, use the loaded character data if the
// bank character index matches the current character index (that is, we
// should use the current character's bank); otherwise, load the
// corresponding character and parse the bank from that character file
if (this->bb_bank_character_index == this->bb_character_index) {
this->bank_data = std::make_shared<PlayerBank>(this->character_file(true, false)->bank);
this->log.info_f("Using bank data from loaded character");
} else {
if (!this->login || !this->login->bb_license) {
throw logic_error("client is not logged in");
}
string filename = this->character_filename(this->login->bb_license->username, this->bb_bank_character_index);
auto character = PSOCHARFile::load_shared(filename, false).character_file;
this->bank_data = std::make_shared<PlayerBank>(character->bank);
this->log.info_f("Using bank data from {}", filename);
}
}
auto s = this->require_server_state();
this->bank_data->max_items = s->bb_max_bank_items;
this->bank_data->max_meseta = s->bb_max_bank_meseta;
}
return this->bank_data;
}
string Client::shared_bank_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have character data");
std::shared_ptr<const PlayerBank> Client::bank_file(bool throw_if_missing) const {
if (!this->bank_data && throw_if_missing) {
throw std::runtime_error("bank is not loaded");
}
if (!this->login || !this->login->bb_license) {
throw logic_error("client is not logged in");
}
return std::format("system/players/shared_bank_{}.psobank", this->login->bb_license->username);
return this->bank_data;
}
void Client::save_bank_file(const string& filename, const PlayerBank& bank) {
auto f = phosg::fopen_unique(filename, "wb");
bank.save(f.get());
}
void Client::save_bank_file() const {
if (!this->bank_data) {
throw logic_error("no bank file loaded");
}
auto filename = this->bank_filename();
this->save_bank_file(filename, *this->bank_data);
this->log.info_f("Saved bank file {}", filename);
}
void Client::change_bank(ssize_t index) {
if (this->bank_data) {
this->save_bank_file();
this->bank_data.reset();
if (this->bb_bank_character_index < 0) {
this->log.info_f("Unloaded shared bank");
} else {
this->log.info_f("Unloaded bank from character {}", this->bb_bank_character_index);
}
}
this->bb_bank_character_index = index;
}
// Legacy files
string Client::legacy_account_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have character data");
throw logic_error("non-BB players do not have saved account data");
}
if (!this->login || !this->login->bb_license) {
throw logic_error("client is not logged in");
@@ -665,7 +832,7 @@ string Client::legacy_account_filename() const {
string Client::legacy_player_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have character data");
throw logic_error("non-BB players do not have saved player files");
}
if (!this->login || !this->login->bb_license) {
throw logic_error("client is not logged in");
@@ -679,13 +846,13 @@ string Client::legacy_player_filename() const {
static_cast<ssize_t>(this->bb_character_index + 1));
}
void Client::create_character_file(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
shared_ptr<const LevelTable> level_table) {
this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table);
this->save_character_file();
void Client::import_blocked_senders(const parray<le_uint32_t, 30>& blocked_senders) {
this->blocked_senders.clear();
for (size_t z = 0; z < blocked_senders.size(); z++) {
if (blocked_senders[z]) {
this->blocked_senders.emplace(blocked_senders[z]);
}
}
}
void Client::load_all_files() {
@@ -693,6 +860,7 @@ void Client::load_all_files() {
this->system_data = make_shared<PSOBBBaseSystemFile>();
this->character_data = make_shared<PSOBBCharacterFile>();
this->guild_card_data = make_shared<PSOBBGuildCardFile>();
this->bank_data = make_shared<PlayerBank>();
return;
}
if (!this->login || !this->login->bb_license) {
@@ -702,31 +870,22 @@ void Client::load_all_files() {
this->system_data.reset();
this->character_data.reset();
this->guild_card_data.reset();
auto files_manager = this->require_server_state()->player_files_manager;
this->bank_data.reset();
string sys_filename = this->system_filename();
this->system_data = files_manager->get_system(sys_filename);
if (this->system_data) {
player_data_log.info_f("Using loaded system file {}", sys_filename);
} else if (std::filesystem::is_regular_file(sys_filename)) {
if (std::filesystem::is_regular_file(sys_filename)) {
this->system_data = make_shared<PSOBBBaseSystemFile>(phosg::load_object_file<PSOBBBaseSystemFile>(sys_filename, true));
files_manager->set_system(sys_filename, this->system_data);
player_data_log.info_f("Loaded system data from {}", sys_filename);
this->log.info_f("Loaded system data from {}", sys_filename);
} else {
player_data_log.info_f("System file is missing: {}", sys_filename);
this->log.info_f("System file is missing: {}", sys_filename);
}
if (this->bb_character_index >= 0) {
string char_filename = this->character_filename();
this->character_data = files_manager->get_character(char_filename);
if (this->character_data) {
player_data_log.info_f("Using loaded character file {}", char_filename);
} else if (std::filesystem::is_regular_file(char_filename)) {
if (std::filesystem::is_regular_file(char_filename)) {
auto psochar = PSOCHARFile::load_shared(char_filename, !this->system_data);
this->character_data = psochar.character_file;
files_manager->set_character(char_filename, this->character_data);
player_data_log.info_f("Loaded character data from {}", char_filename);
this->log.info_f("Loaded character data from {}", char_filename);
// If there was no .psosys file, use the system file from the .psochar
// file instead
@@ -735,28 +894,23 @@ void Client::load_all_files() {
throw logic_error("account system data not present, and also not loaded from psochar file");
}
this->system_data = psochar.system_file;
files_manager->set_system(sys_filename, this->system_data);
player_data_log.info_f("Loaded system data from {}", char_filename);
this->log.info_f("Loaded system data from {}", char_filename);
}
this->update_character_data_after_load(this->character_data);
this->system_data->language = this->language();
} else {
player_data_log.info_f("Character file is missing: {}", char_filename);
this->log.info_f("Character file is missing: {}", char_filename);
}
}
string card_filename = this->guild_card_filename();
this->guild_card_data = files_manager->get_guild_card(card_filename);
if (this->guild_card_data) {
player_data_log.info_f("Using loaded Guild Card file {}", card_filename);
} else if (std::filesystem::is_regular_file(card_filename)) {
if (std::filesystem::is_regular_file(card_filename)) {
this->guild_card_data = make_shared<PSOBBGuildCardFile>(phosg::load_object_file<PSOBBGuildCardFile>(card_filename));
files_manager->set_guild_card(card_filename, this->guild_card_data);
player_data_log.info_f("Loaded Guild Card data from {}", card_filename);
this->log.info_f("Loaded Guild Card data from {}", card_filename);
} else {
player_data_log.info_f("Guild Card file is missing: {}", card_filename);
this->log.info_f("Guild Card file is missing: {}", card_filename);
}
// If any of the above files were missing, try to load from .nsa/.nsc files instead
@@ -770,13 +924,11 @@ void Client::load_all_files() {
}
if (!this->system_data) {
this->system_data = make_shared<PSOBBBaseSystemFile>(nsa_data->system_file);
files_manager->set_system(sys_filename, this->system_data);
player_data_log.info_f("Loaded legacy system data from {}", nsa_filename);
this->log.info_f("Loaded legacy system data from {}", nsa_filename);
}
if (!this->guild_card_data) {
this->guild_card_data = make_shared<PSOBBGuildCardFile>(nsa_data->guild_card_file);
files_manager->set_guild_card(card_filename, this->guild_card_data);
player_data_log.info_f("Loaded legacy Guild Card data from {}", nsa_filename);
this->log.info_f("Loaded legacy Guild Card data from {}", nsa_filename);
}
}
@@ -789,13 +941,11 @@ void Client::load_all_files() {
if (s->bb_default_joystick_config) {
this->system_data->joystick_config = *s->bb_default_joystick_config;
}
files_manager->set_system(sys_filename, this->system_data);
player_data_log.info_f("Created new system data");
this->log.info_f("Created new system data");
}
if (!this->guild_card_data) {
this->guild_card_data = make_shared<PSOBBGuildCardFile>();
files_manager->set_guild_card(card_filename, this->guild_card_data);
player_data_log.info_f("Created new Guild Card data");
this->log.info_f("Created new Guild Card data");
}
if (!this->character_data && (this->bb_character_index >= 0)) {
@@ -812,7 +962,6 @@ void Client::load_all_files() {
}
this->character_data = make_shared<PSOBBCharacterFile>();
files_manager->set_character(this->character_filename(), this->character_data);
this->character_data->inventory = nsc_data.inventory;
this->character_data->disp = nsc_data.disp;
this->character_data->play_time_seconds = 0;
@@ -836,14 +985,22 @@ void Client::load_all_files() {
this->character_data->option_flags = nsa_data->option_flags;
this->character_data->symbol_chats = nsa_data->symbol_chats;
this->character_data->shortcuts = nsa_data->shortcuts;
player_data_log.info_f("Loaded legacy player data from {} and {}", nsa_filename, nsc_filename);
this->log.info_f("Loaded legacy player data from {} and {}", nsa_filename, nsc_filename);
} else {
player_data_log.info_f("Loaded legacy player data from {}", nsc_filename);
this->log.info_f("Loaded legacy player data from {}", nsc_filename);
}
this->update_character_data_after_load(this->character_data);
}
}
auto s = this->require_server_state();
auto stack_limits = s->item_stack_limits(this->version());
if (this->bb_character_index >= 0) {
// bank_file() loads the bank data
this->bank_file()->enforce_stack_limits(stack_limits);
}
this->blocked_senders.clear();
for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) {
if (this->guild_card_data->blocked[z].present) {
@@ -854,6 +1011,9 @@ void Client::load_all_files() {
if (this->character_data) {
// Clear legacy play_time field
this->character_data->disp.name.clear_after_bytes(0x18);
this->character_data->inventory.enforce_stack_limits(stack_limits);
this->login->account->auto_reply_message = this->character_data->auto_reply.decode();
this->login->account->save();
this->last_play_time_update = phosg::now();
@@ -864,7 +1024,7 @@ void Client::update_character_data_after_load(shared_ptr<PSOBBCharacterFile> cha
charfile->import_tethealla_material_usage(this->require_server_state()->level_table(this->version()));
uint8_t lang = this->language();
player_data_log.info_f("Overriding language fields in save files with {:02X} ({})", lang, char_for_language_code(lang));
this->log.info_f("Overriding language fields in save files with {:02X} ({})", lang, char_for_language_code(lang));
charfile->inventory.language = lang;
charfile->guild_card.language = lang;
}
@@ -879,70 +1039,9 @@ void Client::save_all() {
if (this->guild_card_data) {
this->save_guild_card_file();
}
if (this->external_bank) {
string filename = this->shared_bank_filename();
phosg::save_object_file<PlayerBank200>(filename, *this->external_bank);
player_data_log.info_f("Saved shared bank file {}", filename);
if (this->bank_data) {
this->save_bank_file();
}
if (this->external_bank_character) {
this->save_character_file(
this->character_filename(this->external_bank_character_index),
this->system_data,
this->external_bank_character);
}
}
void Client::save_system_file() const {
if (!this->system_data) {
throw logic_error("no system file loaded");
}
string filename = this->system_filename();
phosg::save_object_file(filename, *this->system_data);
player_data_log.info_f("Saved system file {}", filename);
}
void Client::save_character_file(
const string& filename,
shared_ptr<const PSOBBBaseSystemFile> system,
shared_ptr<const PSOBBCharacterFile> character) {
PSOCHARFile::save(filename, system, character);
player_data_log.info_f("Saved character file {}", filename);
}
void Client::save_ep3_character_file(
const string& filename,
const PSOGCEp3CharacterFile::Character& character) {
phosg::save_file(filename, &character, sizeof(character));
player_data_log.info_f("Saved Episode 3 character file {}", filename);
}
void Client::save_character_file() {
if (!this->system_data.get()) {
throw logic_error("no system file loaded");
}
if (!this->character_data.get()) {
throw logic_error("no character file loaded");
}
if (this->should_update_play_time) {
// This is slightly inaccurate, since fractions of a second are truncated
// off each time we save. I'm lazy, so insert shrug emoji here.
uint64_t t = phosg::now();
uint64_t seconds = (t - this->last_play_time_update) / 1000000;
this->character_data->play_time_seconds += seconds;
player_data_log.info_f("Added {} seconds to play time", seconds);
this->last_play_time_update = t;
}
this->save_character_file(this->character_filename(), this->system_data, this->character_data);
}
void Client::save_guild_card_file() const {
if (!this->guild_card_data.get()) {
throw logic_error("no Guild Card file loaded");
}
string filename = this->guild_card_filename();
phosg::save_object_file(filename, *this->guild_card_data);
player_data_log.info_f("Saved Guild Card file {}", filename);
}
void Client::load_backup_character(uint32_t account_id, size_t index) {
@@ -967,109 +1066,41 @@ void Client::save_and_unload_character() {
this->save_character_file();
this->character_data.reset();
this->log.info_f("Unloaded character");
}
}
PlayerBank200& Client::current_bank() {
if (this->external_bank) {
return *this->external_bank;
} else if (this->external_bank_character) {
return this->external_bank_character->bank;
}
return this->character()->bank;
}
const PlayerBank200& Client::current_bank() const {
return const_cast<Client*>(this)->current_bank();
}
std::shared_ptr<PSOBBCharacterFile> Client::current_bank_character() {
return this->external_bank_character ? this->external_bank_character : this->character();
}
void Client::use_default_bank() {
if (this->external_bank) {
string filename = this->shared_bank_filename();
phosg::save_object_file<PlayerBank200>(filename, *this->external_bank);
this->external_bank.reset();
player_data_log.info_f("Detached shared bank {}", filename);
}
if (this->external_bank_character) {
string filename = this->character_filename(this->external_bank_character_index);
this->save_character_file(filename, this->system_data, this->external_bank_character);
this->external_bank_character.reset();
player_data_log.info_f("Detached character {} from bank", filename);
}
}
bool Client::use_shared_bank() {
this->use_default_bank();
string filename = this->shared_bank_filename();
auto files_manager = this->require_server_state()->player_files_manager;
this->external_bank = files_manager->get_bank(filename);
if (this->external_bank) {
player_data_log.info_f("Using loaded shared bank {}", filename);
return true;
} else if (std::filesystem::is_regular_file(filename)) {
this->external_bank = make_shared<PlayerBank200>(phosg::load_object_file<PlayerBank200>(filename));
files_manager->set_bank(filename, this->external_bank);
player_data_log.info_f("Loaded shared bank {}", filename);
return true;
} else {
this->external_bank = make_shared<PlayerBank200>();
files_manager->set_bank(filename, this->external_bank);
player_data_log.info_f("Created shared bank for {}", filename);
return false;
}
}
void Client::use_character_bank(ssize_t index) {
this->use_default_bank();
if (index != this->bb_character_index) {
auto files_manager = this->require_server_state()->player_files_manager;
string filename = this->character_filename(index);
this->external_bank_character = files_manager->get_character(filename);
if (this->external_bank_character) {
this->external_bank_character_index = index;
player_data_log.info_f("Using loaded character file {} for external bank", filename);
} else if (std::filesystem::is_regular_file(filename)) {
this->external_bank_character = PSOCHARFile::load_shared(filename, false).character_file;
this->update_character_data_after_load(this->external_bank_character);
this->external_bank_character_index = index;
files_manager->set_character(filename, this->external_bank_character);
player_data_log.info_f("Loaded character data from {} for external bank", filename);
} else {
throw runtime_error("character does not exist");
if (this->bank_data) {
this->save_bank_file();
this->bank_data.reset();
this->log.info_f("Unloaded bank");
}
}
}
void Client::print_inventory() const {
auto s = this->require_server_state();
auto p = this->character();
this->log.info_f("[PlayerInventory] Meseta: {}\n", p->disp.stats.meseta);
this->log.info_f("[PlayerInventory] {} items\n", p->inventory.num_items);
auto p = this->character_file();
this->log.info_f("[PlayerInventory] Meseta: {}", p->disp.stats.meseta);
this->log.info_f("[PlayerInventory] {} items", p->inventory.num_items);
for (size_t x = 0; x < p->inventory.num_items; x++) {
const auto& item = p->inventory.items[x];
auto hex = item.data.hex();
auto name = s->describe_item(this->version(), item.data, false);
this->log.info_f("[PlayerInventory] {:2}: [+{:08X}] {} ({})\n", x, item.flags, hex, name);
auto name = s->describe_item(this->version(), item.data);
this->log.info_f("[PlayerInventory] {:2}: [+{:08X}] {} ({})", x, item.flags, hex, name);
}
}
void Client::print_bank() const {
auto s = this->require_server_state();
auto bank = this->current_bank();
this->log.info_f("[PlayerBank] Meseta: {}\n", bank.meseta);
this->log.info_f("[PlayerBank] {} items\n", bank.num_items);
for (size_t x = 0; x < bank.num_items; x++) {
const auto& item = bank.items[x];
const char* present_token = item.present ? "" : " (missing present flag)";
auto hex = item.data.hex();
auto name = s->describe_item(this->version(), item.data, false);
this->log.info_f("[PlayerBank] {:3}: {} ({}) (x{}){}\n", x, hex, name, item.amount, present_token);
if (this->bank_data) {
auto s = this->require_server_state();
this->log.info_f("[PlayerBank] Meseta: {}", this->bank_data->meseta);
this->log.info_f("[PlayerBank] {} items", this->bank_data->items.size());
for (size_t x = 0; x < this->bank_data->items.size(); x++) {
const auto& item = this->bank_data->items[x];
const char* present_token = item.present ? "" : " (missing present flag)";
auto hex = item.data.hex();
auto name = s->describe_item(this->version(), item.data);
this->log.info_f("[PlayerBank] {:3}: {} ({}) (x{}){}", x, hex, name, item.amount, present_token);
}
} else {
this->log.info_f("[PlayerBank] Bank data not loaded");
}
}
+55 -57
View File
@@ -50,29 +50,29 @@ public:
SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE = 0x0000000000004000,
SEND_FUNCTION_CALL_NO_CACHE_PATCH = 0x0000000000008000,
CAN_RECEIVE_ENABLE_B2_QUEST = 0x0000000000020000,
AWAITING_ENABLE_B2_QUEST = 0x0000000000040000, // Server-side only
AWAITING_ENABLE_B2_QUEST = 0x0000000000040000,
// State flags
LOADING = 0x0000000000100000, // Server-side only
LOADING_QUEST = 0x0000000000200000, // Server-side only
LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000, // Server-side only
LOADING_TOURNAMENT = 0x0000000000800000, // Server-side only
IN_INFORMATION_MENU = 0x0000000001000000, // Server-side only
AT_WELCOME_MESSAGE = 0x0000000002000000, // Server-side only
LOADING = 0x0000000000100000,
LOADING_QUEST = 0x0000000000200000,
LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000,
LOADING_TOURNAMENT = 0x0000000000800000,
IN_INFORMATION_MENU = 0x0000000001000000,
AT_WELCOME_MESSAGE = 0x0000000002000000,
SAVE_ENABLED = 0x0000000004000000,
HAS_EP3_CARD_DEFS = 0x0000000008000000,
HAS_EP3_MEDIA_UPDATES = 0x0000000010000000,
HAS_AUTO_PATCHES = 0x0000004000000000,
AT_BANK_COUNTER = 0x0000000080000000, // Server-side only
SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only
SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE = 0x0040000000000000, // Server-side only
SHOULD_SEND_ARTIFICIAL_OBJECT_STATE = 0x0080000000000000, // Server-side only
SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Server-side only
SHOULD_SEND_ARTIFICIAL_PLAYER_STATES = 0x0200000000000000, // Server-side only
AT_BANK_COUNTER = 0x0000000080000000,
SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000,
SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE = 0x0040000000000000,
SHOULD_SEND_ARTIFICIAL_OBJECT_STATE = 0x0080000000000000,
SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000,
SHOULD_SEND_ARTIFICIAL_PLAYER_STATES = 0x0200000000000000,
SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000,
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
IS_CLIENT_CUSTOMIZATION = 0x0100000000000000,
EP3_ALLOW_6xBC = 0x1000000000000000, // Server-side only
EP3_ALLOW_6xBC = 0x1000000000000000,
// Cheat mode and option flags
INFINITE_HP_ENABLED = 0x0000000200000000,
@@ -80,6 +80,7 @@ public:
DEBUG_ENABLED = 0x0000000800000000,
ITEM_DROP_NOTIFICATIONS_1 = 0x0010000000000000,
ITEM_DROP_NOTIFICATIONS_2 = 0x0020000000000000,
HAS_ENEMY_DAMAGE_SYNC_PATCH = 0x2000000000000000, // Must be same as in EnemyDamageSync*.s
// Proxy option flags
PROXY_SAVE_FILES = 0x0000001000000000,
@@ -113,6 +114,7 @@ public:
uint8_t bb_client_code = 0;
uint8_t bb_connection_phase = 0xFF;
ssize_t bb_character_index = -1; // -1 = not set
ssize_t bb_bank_character_index = -1; // -1 = shared bank
uint32_t bb_security_token = 0;
parray<uint8_t, 0x28> bb_client_config;
std::string login_character_name;
@@ -287,6 +289,36 @@ public:
void set_login(std::shared_ptr<Login> login);
void import_blocked_senders(const parray<le_uint32_t, 30>& blocked_senders);
static std::string system_filename(const std::string& bb_username);
std::string system_filename() const;
std::shared_ptr<PSOBBBaseSystemFile> system_file(bool allow_load = true);
std::shared_ptr<const PSOBBBaseSystemFile> system_file(bool throw_if_missing = true) const;
void save_system_file() const;
static std::string guild_card_filename(const std::string& bb_username);
std::string guild_card_filename() const;
std::shared_ptr<PSOBBGuildCardFile> guild_card_file(bool allow_load = true);
std::shared_ptr<const PSOBBGuildCardFile> guild_card_file(bool allow_load = true) const;
void save_guild_card_file() const;
static std::string character_filename(const std::string& bb_username, ssize_t index);
static std::string backup_character_filename(uint32_t account_id, size_t index, bool is_ep3);
std::string character_filename() const;
std::shared_ptr<PSOBBCharacterFile> character_file(bool allow_load = true, bool allow_overlay = true);
std::shared_ptr<const PSOBBCharacterFile> character_file(bool throw_if_missing = true, bool allow_overlay = true) const;
static void save_character_file(
const std::string& filename,
std::shared_ptr<const PSOBBBaseSystemFile> sys,
std::shared_ptr<const PSOBBCharacterFile> character);
static void save_ep3_character_file(const std::string& filename, const PSOGCEp3CharacterFile::Character& character);
void save_character_file();
void create_character_file(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
std::shared_ptr<const LevelTable> level_table);
void create_battle_overlay(std::shared_ptr<const BattleRules> rules, std::shared_ptr<const LevelTable> level_table);
void create_challenge_overlay(Version version, size_t template_index, std::shared_ptr<const LevelTable> level_table);
inline void delete_overlay() {
@@ -296,55 +328,23 @@ public:
return this->overlay_character_data.get() != nullptr;
}
void import_blocked_senders(const parray<le_uint32_t, 30>& blocked_senders);
static std::string bank_filename(const std::string& bb_username, ssize_t index);
std::string bank_filename() const;
std::shared_ptr<PlayerBank> bank_file(bool allow_load = true);
std::shared_ptr<const PlayerBank> bank_file(bool throw_if_missing = true) const;
static void save_bank_file(const std::string& filename, const PlayerBank& bank);
void save_bank_file() const;
void change_bank(ssize_t bb_character_index); // -1 = use shared bank
std::shared_ptr<PSOBBBaseSystemFile> system_file(bool allow_load = true);
std::shared_ptr<PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true);
std::shared_ptr<PSOBBGuildCardFile> guild_card_file(bool allow_load = true);
std::shared_ptr<const PSOBBBaseSystemFile> system_file(bool allow_load = true) const;
std::shared_ptr<const PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true) const;
std::shared_ptr<const PSOBBGuildCardFile> guild_card_file(bool allow_load = true) const;
void create_character_file(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
std::shared_ptr<const LevelTable> level_table);
std::string system_filename() const;
static std::string character_filename(const std::string& bb_username, ssize_t index);
static std::string backup_character_filename(uint32_t account_id, size_t index, bool is_ep3);
std::string character_filename(ssize_t index = -1) const;
std::string guild_card_filename() const;
std::string shared_bank_filename() const;
std::string legacy_player_filename() const;
std::string legacy_account_filename() const;
std::string legacy_player_filename() const;
void save_all();
void save_system_file() const;
static void save_character_file(
const std::string& filename,
std::shared_ptr<const PSOBBBaseSystemFile> sys,
std::shared_ptr<const PSOBBCharacterFile> character);
static void save_ep3_character_file(
const std::string& filename,
const PSOGCEp3CharacterFile::Character& character);
// Note: This function is not const because it updates the player's play time.
void save_character_file();
void save_guild_card_file() const;
void load_backup_character(uint32_t account_id, size_t index);
std::shared_ptr<PSOGCEp3CharacterFile::Character> load_ep3_backup_character(uint32_t account_id, size_t index);
void save_and_unload_character();
PlayerBank200& current_bank();
const PlayerBank200& current_bank() const;
std::shared_ptr<PSOBBCharacterFile> current_bank_character();
bool use_shared_bank(); // Returns true if the bank exists; false if it was created
void use_character_bank(ssize_t bb_character_index);
void use_default_bank();
void print_inventory() const;
void print_bank() const;
@@ -358,9 +358,7 @@ private:
std::shared_ptr<PSOBBCharacterFile> overlay_character_data;
std::shared_ptr<PSOBBCharacterFile> character_data;
std::shared_ptr<PSOBBGuildCardFile> guild_card_data;
std::shared_ptr<PlayerBank200> external_bank;
std::shared_ptr<PSOBBCharacterFile> external_bank_character;
ssize_t external_bank_character_index = -1;
std::shared_ptr<PlayerBank> bank_data;
uint64_t last_play_time_update = 0;
void load_all_files();
+315 -173
View File
@@ -344,7 +344,6 @@ struct S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B {
// 03 (C->S): Legacy register (non-BB)
// Internal name: SndRegist
// TODO: Are the DCv1 and DCv2 formats the same as this structure?
struct C_LegacyLogin_PC_V3_03 {
/* 00 */ be_uint64_t hardware_id;
@@ -400,7 +399,6 @@ struct S_ServerInitWithAfterMessageT_BB_03_9B {
// now-unused sequence. Like 03, this command isn't used by any known PSO
// version.
// header.flag is 1 if the client has UDP disabled.
// TODO: Are the DCv1 and DCv2 formats the same as this structure?
struct C_LegacyLogin_PC_V3_04 {
/* 00 */ be_uint64_t hardware_id;
@@ -435,9 +433,9 @@ struct C_LegacyLogin_BB_04 {
// 05 = Server down for maintenance (108)
// 06 = Incorrect password (127)
// Any other nonzero value = Generic failure (101)
// The client config field in this command is ignored by pre-V3 clients as well
// as Episodes 1&2 Trial Edition. All other V3 clients save it as opaque data to
// be returned in a 9E or 9F command later.
// The client config field in this command is ignored by all clients that never
// send 9E. Clients that do send 9E will save thie client config as opaque data
// to be returned in a 9E or 9F command later.
// The client will respond with a 96 command, but only the first time it
// receives this command - for later 04 commands, the client will still update
// its client config but will not respond. Changing the security data at any
@@ -676,49 +674,59 @@ struct S_LegacyJoinGame_XB_0E {
// 10 (C->S): Menu selection
// Internal name: SndAction
// header.flag contains two flags: 02 specifies if a password is present, and 01
// specifies... something else. These two bits directly correspond to the two
// lowest bits in the flags field of the game menu: 02 specifies that the game
// is locked, but the function of 01 is unknown.
// Annoyingly, the no-arguments form of the command can have any flag value, so
// it doesn't suffice to check the flag value to know which format is being
// used!
// header.flag has different meanings depending on which menu is selected.
// For most menus, header.flag is a bit field containing two flags: 02
// specifies if a password is present, and 01 specifies if a name is present.
// (If both are set, the name comes first, as described below). These two bits
// directly correspond to the two lowest bits in the flags field of the game
// menu: 02 specifies that the game is locked, but the function of 01 is
// unknown. The ability to send a name along with a menu choice is unused in
// all client versions except Episode 3, where it's used in the tournament
// entries menu. It's not clear why all other versions have the ability send a
// name here - it may be a relic from very early development.
// For the quest categories menu, header.flag specifies the player's
// progression through the story. The values are:
// 0 = has not yet defeated Dragon
// 1 = has defeated Dragon but not De Rol Le
// 2 = has defeated De Rol Le but not Vol Opt
// 3 = has defeated Vol Opt but not Dark Falz
// 4 = has defeated Dark Falz
// For the challenge categories menu, header.flag specifies something related
// to challenge stage completion (TODO: reverse-engineer function at
// 59NL:004DA300 to see what this is)
struct C_MenuSelection_10_Flag00 {
struct C_MenuSelectionBase_10 {
le_uint32_t menu_id = 0;
le_uint32_t item_id = 0;
} __packed_ws__(C_MenuSelection_10_Flag00, 8);
} __packed_ws__(C_MenuSelectionBase_10, 8);
template <TextEncoding Encoding>
struct C_MenuSelectionT_10_Flag01 {
C_MenuSelection_10_Flag00 basic_cmd;
struct C_MenuSelectionWithNameT_10 : C_MenuSelectionBase_10 {
pstring<Encoding, 0x10> name;
} __attribute__((packed));
using C_MenuSelection_DC_V3_10_Flag01 = C_MenuSelectionT_10_Flag01<TextEncoding::MARKED>;
using C_MenuSelection_PC_BB_10_Flag01 = C_MenuSelectionT_10_Flag01<TextEncoding::UTF16>;
check_struct_size(C_MenuSelection_DC_V3_10_Flag01, 0x18);
check_struct_size(C_MenuSelection_PC_BB_10_Flag01, 0x28);
using C_MenuSelectionWithName_DC_V3_10 = C_MenuSelectionWithNameT_10<TextEncoding::MARKED>;
using C_MenuSelectionWithName_PC_BB_10 = C_MenuSelectionWithNameT_10<TextEncoding::UTF16>;
check_struct_size(C_MenuSelectionWithName_DC_V3_10, 0x18);
check_struct_size(C_MenuSelectionWithName_PC_BB_10, 0x28);
template <TextEncoding Encoding>
struct C_MenuSelectionT_10_Flag02 {
C_MenuSelection_10_Flag00 basic_cmd;
struct C_MenuSelectionWithPasswordT_10 : C_MenuSelectionBase_10 {
pstring<Encoding, 0x10> password;
} __attribute__((packed));
using C_MenuSelection_DC_V3_10_Flag02 = C_MenuSelectionT_10_Flag02<TextEncoding::MARKED>;
using C_MenuSelection_PC_BB_10_Flag02 = C_MenuSelectionT_10_Flag02<TextEncoding::UTF16>;
check_struct_size(C_MenuSelection_DC_V3_10_Flag02, 0x18);
check_struct_size(C_MenuSelection_PC_BB_10_Flag02, 0x28);
using C_MenuSelectionWithPassword_DC_V3_10 = C_MenuSelectionWithPasswordT_10<TextEncoding::MARKED>;
using C_MenuSelectionWithPassword_PC_BB_10 = C_MenuSelectionWithPasswordT_10<TextEncoding::UTF16>;
check_struct_size(C_MenuSelectionWithPassword_DC_V3_10, 0x18);
check_struct_size(C_MenuSelectionWithPassword_PC_BB_10, 0x28);
template <TextEncoding Encoding>
struct C_MenuSelectionT_10_Flag03 {
C_MenuSelection_10_Flag00 basic_cmd;
struct C_MenuSelectionWithNameAndPasswordT_10 : C_MenuSelectionBase_10 {
pstring<Encoding, 0x10> name;
pstring<Encoding, 0x10> password;
} __attribute__((packed));
using C_MenuSelection_DC_V3_10_Flag03 = C_MenuSelectionT_10_Flag03<TextEncoding::MARKED>;
using C_MenuSelection_PC_BB_10_Flag03 = C_MenuSelectionT_10_Flag03<TextEncoding::UTF16>;
check_struct_size(C_MenuSelection_DC_V3_10_Flag03, 0x28);
check_struct_size(C_MenuSelection_PC_BB_10_Flag03, 0x48);
using C_MenuSelectionWithNameAndPassword_DC_V3_10 = C_MenuSelectionWithNameAndPasswordT_10<TextEncoding::MARKED>;
using C_MenuSelectionWithNameAndPassword_PC_BB_10 = C_MenuSelectionWithNameAndPasswordT_10<TextEncoding::UTF16>;
check_struct_size(C_MenuSelectionWithNameAndPassword_DC_V3_10, 0x28);
check_struct_size(C_MenuSelectionWithNameAndPassword_PC_BB_10, 0x48);
// 11 (S->C): Ship info
// Internal name: RcvMessage
@@ -807,6 +815,20 @@ struct C_WriteFileConfirmation_V3_BB_13_A7 {
using S_Reconnect_19 = S_ReconnectT<le_uint16_t>;
check_struct_size(S_Reconnect_19, 8);
// Sylverant implements an IPv6 version of this command, but it's not obvious
// why. IPv6 technically did exist as a draft standard at the time of PSO's
// development, but it wasn't widely used until over a decade later. IPv6
// support is not implemented in any version of the PSO client that I've seen,
// but we implement Sylverant's version of this command anyway because newserv
// may connect to Sylverant via IPv6 when using the proxy. Sylverant sends the
// value 6 in header.flag in this case, presumably to indicate the protocol.
struct S_ReconnectIPv6_Extension_19 {
parray<uint8_t, 0x10> address;
le_uint16_t port = 0;
le_uint16_t unused = 0;
} __packed_ws__(S_ReconnectIPv6_Extension_19, 0x14);
// Because PSO PC and some versions of PSO DC/GC use the same port but different
// protocols, we use a specially-crafted 19 command to send them to two
// different ports depending on the client version. I first saw this technique
@@ -834,7 +856,8 @@ struct S_ReconnectSplit_19 {
// the chat log window contents will appear in the message box, prepended to
// the message text from the command.
// The maximum length of the message is 0x400 bytes. This is the only
// difference between this command and the D5 command.
// difference between this command and the D5 command (except on BB - see the
// notes on D5 for more information).
// 1B (S->C): Valid but ignored (all versions)
// Internal name: RcvBattleData
@@ -876,10 +899,8 @@ struct S_ReconnectSplit_19 {
// it's not clear if this is accurate. At least, BB US v1.24.3 and later do not
// support this command.
// 0022: GameGuard check (BB)
// Command 0022 is a 16-byte challenge (sent in the data field) using the
// following structure.
// 0022: GameGuard challenge/response (BB)
// This command is not valid on BB Trial Edition.
struct SC_GameGuardCheck_BB_0022 {
parray<le_uint32_t, 4> data;
@@ -890,11 +911,13 @@ struct SC_GameGuardCheck_BB_0022 {
// the returned timestamp is before the previous timestamp returned, but not by
// too much - it seems the game only considers deltas between 3 seconds and 30
// minutes suspicious for these purposes.
// This command is not valid on BB Trial Edition.
// 23 (S->C): Momoka Item Exchange result (BB)
// Sent in response to a 6xD9 command from the client.
// header.flag indicates if an item was exchanged: 0 means success, 1 means
// failure.
// This command is not valid on BB Trial Edition.
// 24 (S->C): Secret Lottery Ticket exchange result (BB)
// Sent in response to a 6xDE command from the client.
@@ -904,6 +927,7 @@ struct SC_GameGuardCheck_BB_0022 {
// header.flag indicates whether the client had any Secret Lottery Tickets in
// their inventory (and hence could participate): 0 means success, 1 means
// failure. However, this value is unused by the client.
// This command is not valid on BB Trial Edition.
struct S_ExchangeSecretLotteryTicketResult_BB_24 {
le_uint16_t label = 0;
@@ -914,8 +938,9 @@ struct S_ExchangeSecretLotteryTicketResult_BB_24 {
// 25 (S->C): Gallon's Plan result (BB)
// Sent in response to a 6xE1 command from the client.
// The client sets the quest registers reg_num1 to reg_value1 and reg_num2 to
// reg_value2, then starts a new quest thread at the specified label.
// The client sets the quest registers reg_num1 to value1 and reg_num2 to
// value2, then starts a new quest thread at the specified label.
// This command is not valid on BB Trial Edition.
struct S_GallonPlanResult_BB_25 {
le_uint16_t label = 0;
@@ -1351,9 +1376,6 @@ struct LobbyFlags {
// DCv2 and later, the game mode option is always present.
uint8_t enable_battle_mode_v1 = 1;
uint8_t event = 0;
// TODO: This is a partially-informed, but untested, guess based on
// disassembly of the Xbox client. It'd be nice if someone let me know if
// this is correct or not
uint8_t xb_enable_voice_chat = 1;
le_uint32_t random_seed = 0; // Unused for lobbies
} __packed_ws__(LobbyFlags, 0x0C);
@@ -1726,7 +1748,7 @@ struct C_RegisterV1_DC_92 {
be_uint64_t hardware_id;
le_uint32_t sub_version;
uint8_t unused1 = 0;
uint8_t language = 0; // TODO: This is a guess; verify it
uint8_t language = 0;
parray<uint8_t, 2> unused2;
pstring<TextEncoding::ASCII, 0x30> serial_number2;
pstring<TextEncoding::ASCII, 0x30> access_key2;
@@ -2000,8 +2022,9 @@ struct C_LoginExtended_PC_9D : C_Login_DC_PC_GC_9D {
SC_MeetUserExtension_PC_BB extension;
} __packed_ws__(C_LoginExtended_PC_9D, 0x14C);
// 9E (C->S): Log in with client config (V3/BB)
// Not used on GC Episodes 1&2 Trial Edition.
// 9E (C->S): Log in with client config (PC/V3/BB)
// Not used on GC Episodes 1&2 Trial Edition, nor on v1 or most v2 versions.
// Of all pre-v3 versions, only the latest version of PCv2 appears to use this.
// The extended version of this command is used in the same circumstances as
// when PSO PC uses the extended version of the 9D command.
// PSO XB does not send the client config (security data) in the 9E command,
@@ -2009,11 +2032,13 @@ struct C_LoginExtended_PC_9D : C_Login_DC_PC_GC_9D {
// retrieve the client config.
// header.flag is 1 if the client has UDP disabled.
struct C_Login_GC_9E : C_Login_DC_PC_GC_9D {
struct C_Login_PC_GC_9E : C_Login_DC_PC_GC_9D {
parray<uint8_t, 0x20> client_config;
} __packed_ws__(C_Login_GC_9E, 0xE8);
struct C_LoginExtended_GC_9E : C_Login_GC_9E {
} __packed_ws__(C_Login_PC_GC_9E, 0xE8);
struct C_LoginExtended_PC_9E : C_Login_PC_GC_9E {
SC_MeetUserExtension_PC_BB extension;
} __packed_ws__(C_LoginExtended_PC_9E, 0x16C);
struct C_LoginExtended_GC_9E : C_Login_PC_GC_9E {
SC_MeetUserExtension_DC_V3 extension;
} __packed_ws__(C_LoginExtended_GC_9E, 0x14C);
@@ -2051,12 +2076,13 @@ struct C_LoginExtended_BB_9E {
/* 0170 */
} __packed_ws__(C_LoginExtended_BB_9E, 0x170);
// 9F (S->C): Request client config / security data (V3/BB)
// This command is not valid on PSO GC Episodes 1&2 Trial Edition, nor any
// pre-V3 PSO versions. Client will respond with a 9F command.
// 9F (S->C): Request client config / security data (PC/V3/BB)
// This command is not valid on PSO GC Episodes 1&2 Trial Edition nor on any
// other pre-v3 versions, except the latest PC v2 version, which does have it.
// Client will respond with a 9F command.
// No arguments
// 9F (C->S): Client config / security data response (V3/BB)
// 9F (C->S): Client config / security data response (PC/V3/BB)
// The data is opaque to the client, as described at the top of this file.
// On BB, this command does not work during the data server phase.
@@ -2356,8 +2382,9 @@ struct S_RankUpdate_Ep3_B7 {
// No arguments
// The client sends this after it receives a B8 from the server.
// B8 (C->S): Unknown (BB)
// The client accepts this command, but ignores it.
// B8 (C->S): Valid but ignored (BB)
// The client accepts this command, but ignores it. It may have had some
// later-removed purpose during BB's development.
// B9 (S->C): Update CARD lobby media (Episode 3)
// This command is not valid on Episode 3 Trial Edition.
@@ -2684,26 +2711,27 @@ struct S_ConfirmTournamentEntry_Ep3_CC {
// CF: Invalid command
// D0 (C->S): Start trade sequence (V3/BB)
// The trade window sequence is a bit complicated. The normal flow is:
// The trade window sequence is a bit complicated. On pre-BB versions, the
// normal flow is:
// - Clients sync trade state with 6xA6 commands
// - When both have confirmed, one client (the initiator) sends a D0
// - Server sends a D1 to the non-initiator
// - Non-initiator sends a D0
// - Server sends a D1 to both clients
// - Both clients delete the sent items from their inventories (and send the
// appropriate subcommand)
// - Both clients send a D2 (similarly to how AC works, the server should not
// proceed until both D2s are received)
// - Server sends a D3 to both clients with each other's data from their D0s,
// followed immediately by a D4 01 to both clients, which completes the trade
// - When both have confirmed, one client (the initiator) sends D0
// - The server sends D1 to the other client (the responder)
// - The responder sends D0
// - The server sends D1 to both clients
// - Both clients delete the sent items from their inventories and send the
// appropriate subcommand (6x29)
// - Both clients send D2; similarly to how AC works, the server doesn't
// proceed until both D2 commands are received
// - The server sends D3 to both clients with each other's data from their D0
// commands, followed immediately by D4 01 to both clients, which completes
// the trade
// - Both clients send the appropriate subcommand to create inventory items
// TODO: On BB, is the server responsible for sending the appropriate item
// delete/create subcommands?
// On BB, the flow is similar, except after both D2 commands are received, the
// server instead handles the rest of the process - it sends 6x29 commands to
// delete the inventory items and 6xBE to create the traded items.
// At any point if an error occurs, either client may send a D4 00, which
// cancels the entire sequence. The server should then send D4 00 to both
// clients.
// TODO: The server should presumably also send a D4 00 if either client
// disconnects during the sequence.
struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server
le_uint16_t target_client_id = 0;
@@ -2738,7 +2766,10 @@ struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server
// See D0 description for usage information.
// D5: Large message box (V3/BB)
// Same as 1A command, except the maximum length of the message is 0x1000 bytes.
// Same as 1A command, except the maximum length of the message is 0x1000
// bytes. On BB, this command is not valid during the data server phase
// (whereas 1A is valid there). The BB client ignores all D5 commands after the
// first one sent in each connection; this logic does not apply to 1A.
// D6 (C->S): Large message box closed (V3)
// No arguments
@@ -2929,7 +2960,11 @@ struct S_TournamentList_Ep3NTE_E0 {
le_uint16_t max_teams = 0;
} __packed_ws__(Entry, 0x34);
parray<Entry, 0x20> entries;
} __packed_ws__(S_TournamentList_Ep3NTE_E0, 0x680);
uint8_t unknown_a1 = 0;
uint8_t unknown_a2 = 0;
uint8_t unknown_a3 = 0;
uint8_t unknown_a4 = 0;
} __packed_ws__(S_TournamentList_Ep3NTE_E0, 0x684);
struct S_TournamentList_Ep3_E0 {
struct Entry {
@@ -2963,7 +2998,13 @@ struct S_TournamentList_Ep3_E0 {
le_uint16_t unknown_a4 = 0xFFFF;
} __packed_ws__(Entry, 0x38);
parray<Entry, 0x20> entries;
} __packed_ws__(S_TournamentList_Ep3_E0, 0x700);
// These fields exist in the command (the copy constructor copies them over)
// but it seems they aren't used by the client at all.
uint8_t unknown_a1 = 0;
uint8_t unknown_a2 = 0;
uint8_t unknown_a3 = 0;
uint8_t unknown_a4 = 0;
} __packed_ws__(S_TournamentList_Ep3_E0, 0x704);
// E0 (C->S): Request system file (BB)
// No arguments. The server should respond with an E1 or E2 command.
@@ -3027,10 +3068,9 @@ struct S_SystemFileCreated_00E1_BB {
// E2 (S->C): Tournament entry list (Episode 3)
// Client may send 09 commands if the player presses X. It's not clear what the
// server should respond with in this case.
// If the player selects an entry slot, client will respond with a long-form 10
// command (the Flag03 variant); in this case, unknown_a1 is the team name, and
// password is the team password. The server should respond to that with a CC
// command.
// If the player selects an entry slot, client will respond with a 10 command
// containing both a team name and password (with flag = 3). The server should
// respond to that with a CC command.
struct S_TournamentEntryList_Ep3_E2 {
le_uint16_t players_per_team = 0;
@@ -3467,7 +3507,9 @@ struct S_TeamMemberList_BB_09EA {
} __packed_ws__(S_TeamMemberList_BB_09EA, 4);
// 0CEA (S->C): Unknown
// The client ignores this command.
// The client ignores this command. It calls filter_curse_words on the
// presumably variable-sized text that follows the first 0x20 bytes of the
// command, but then does nothing with the result.
struct S_Unknown_BB_0CEA {
parray<uint8_t, 0x20> unknown_a1;
@@ -3538,7 +3580,7 @@ struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry {
// the same as for the 13EA command.
// 16EA (S->C): Transfer item via Simple Mail result
// No arguments except header.flag, which is 0 if the transfer failed and
// No arguments except header.flag, which is zero if the transfer failed or
// nonzero if it succeeded.
// 18EA: Intra-team ranking information
@@ -3789,7 +3831,8 @@ struct S_SetShutdownCommand_BB_01EF {
// F0 (S->C): Force update player lobby data (BB)
// Format is PlayerLobbyDataBB (in PlayerSubordinates.hh). This command
// overwrites the lobby data for the player given by .client_id without
// reloading the game or lobby.
// reloading the game or lobby. This command is not valid on PSOBB Trial
// Edition.
// This command probably exists to handle cases like the following:
// 1. Player A is in a team and is not the team master. Player A creates a game.
@@ -3915,7 +3958,8 @@ struct G_ExtendedHeaderT {
// 6x03: Unknown
// These subcommands are completely ignored on V3 and later.
// On all known DC versions (NTE through V2), the contents of these commands
// are written to a global array, but nothing reads from this array.
// are written to a global array, but nothing reads from this array. This
// command is likely a relic from pre-NTE development.
struct G_Unknown_6x02_6x03 {
G_ClientIDHeader header;
@@ -3927,16 +3971,16 @@ struct G_Unknown_6x02_6x03 {
} __packed_ws__(G_Unknown_6x02_6x03, 0x14);
// 6x04: Activate switch by token (deprecated)
// This appears to be an early version of 6x0B; it only affects TObjDoorKey
// objects and can only activate them, not deactivate them. 6x0B is much more
// versatile and is used instead in all known versions of the game. This
// command is likely a relic from pre-NTE development.
// This appears to be an early version of 6x05 or 6x0B; it only affects
// TObjDoorKey objects and can only activate them, not deactivate them. 6x05
// and 6x0B are much more versatile and are used instead in all known versions
// of the game. This command is likely a relic from pre-NTE development.
struct G_Unknown_6x04 {
struct G_LegacyActivateSwitchByToken_6x04 {
G_ParameterHeader header; // param = door token (NOT entity ID or index)
le_uint16_t unused1 = 0;
le_uint16_t unused2 = 0;
} __packed_ws__(G_Unknown_6x04, 8);
le_uint16_t switch_token = 0;
le_uint16_t unused = 0;
} __packed_ws__(G_LegacyActivateSwitchByToken_6x04, 8);
// 6x05: Write switch flag
// Some things that don't look like switches are implemented as switches using
@@ -3947,11 +3991,14 @@ struct G_Unknown_6x04 {
struct G_WriteSwitchFlag_6x05 {
// header.entity_id may be 0xFFFF if no object is responsible for the switch
// flag state change - this can happen when a wave event script sets a switch
// flag state change - this happens when a wave event script sets a switch
// flag, for example.
G_EntityIDHeader header;
// TODO: Some of these might be big-endian on GC; it only byteswaps
// switch_flag_num. Are the others actually uint16, or are they uint8[2]?
// It seems client_id isn't used anywhere. Some switch-like objects set it
// when sending this command, but it seems it's never read when 6x05 is
// received (including by virtual functions called on the affected object).
// PSO GC doesn't even bother to byteswap it, and we don't either since it's
// unused.
le_uint16_t client_id = 0;
le_uint16_t unused = 0;
le_uint16_t switch_flag_num = 0;
@@ -3963,6 +4010,9 @@ struct G_WriteSwitchFlag_6x05 {
} __packed_ws__(G_WriteSwitchFlag_6x05, 0x0C);
// 6x06: Send guild card
// On BB, the server is responsible for generating and sending the Guild Card
// data. newserv applies this logic for all versions of the game, to prevent
// players from sending Guild Cards other than their own.
struct G_SendGuildCard_DCNTE_6x06 {
G_UnusedHeader header;
@@ -4012,17 +4062,19 @@ struct G_SymbolChat_6x07 {
// 6x08: Invalid subcommand
// 6x09: Unknown
// 6x09: Kill enemy (broken/unused)
// header.entity_id is expected to be an enemy ID, but is also expected to be
// in the range [0x00, 0x80) since it writes to an array of 0x80 entries. This
// duality makes no sense because enemy IDs are greater than or equal to
// 0x1000, so any valid enemy ID would be far outside the array's range, and
// the write is not bounds-checked. For this reason, newserv unconditionally
// blocks this command.
// in the range [0x00, 0x80) since the command handler writes to an array of
// 0x80 entries. This duality is nonsense because enemy IDs are greater than or
// equal to 0x1000, so any valid enemy ID would be far outside the array's
// range. newserv unconditionally blocks this command because it appears never
// to be used, and the array write is not bounds-checked, so it could be used
// to cause undefined behavior on other clients. It seems that this broken
// logic predates even DC NTE.
struct G_Unknown_6x09 {
struct G_LegacyKillEnemy_6x09 {
G_EntityIDHeader header;
} __packed_ws__(G_Unknown_6x09, 4);
} __packed_ws__(G_LegacyKillEnemy_6x09, 4);
// 6x0A: Update enemy state
// In Ultimate mode, the low 6 bits of game_flags are ignored, and 6x9C is used
@@ -4150,19 +4202,20 @@ struct G_VolOptBossActions_6x15 {
struct G_VolOptBossActions_6x16 {
G_EntityIDHeader header;
parray<uint8_t, 6> unknown_a2;
le_uint16_t unknown_a3 = 0;
parray<uint8_t, 6> entity_index_table;
le_uint16_t entity_index_count = 0;
} __packed_ws__(G_VolOptBossActions_6x16, 0x0C);
// 6x17: Vol Opt phase 2 boss actions (not valid on Episode 3)
// 6x17: Set entity position and angle (not valid on Episode 3)
// This command sets an entity's position and angle without performing any
// validity checks, even on v3 and later. We unconditionally block this if it
// affects a player other than the sender.
struct G_VolOpt2BossActions_6x17 {
struct G_SetEntityPositionAndAngle_6x17 {
G_EntityIDHeader header;
le_float unknown_a2 = 0.0f;
le_float unknown_a3 = 0.0f;
le_float unknown_a4 = 0.0f;
le_uint32_t unknown_a5 = 0;
} __packed_ws__(G_VolOpt2BossActions_6x17, 0x14);
VectorXYZF pos;
le_uint32_t angle = 0;
} __packed_ws__(G_SetEntityPositionAndAngle_6x17, 0x14);
// 6x18: Vol Opt phase 2 boss actions (not valid on Episode 3)
@@ -4195,8 +4248,34 @@ struct G_DisablePKModeForPlayer_6x1C {
G_ClientIDHeader header;
} __packed_ws__(G_DisablePKModeForPlayer_6x1C, 4);
// 6x1D: Invalid subcommand
// 6x1E: Invalid subcommand
// 6x1D: Request partial player data (pre-v1 only)
// The subcommand number 6x1D is not used in any final version of PSO; this
// number is assigned based on what the command number would be if it were. On
// DC NTE, this is subcommand 6x19; on 11/2000, it's 6x1B.
// This command does not appear to ever be sent by the client; however, it will
// respond with 6x1E if it receives this command.
struct G_RequestPartialPlayerData_DCProtos_6x1D {
G_UnusedHeader header;
} __packed_ws__(G_RequestPartialPlayerData_DCProtos_6x1D, 4);
// 6x1E: Partial player data (pre-v1 only)
// The subcommand number 6x1E is not used in any final version of PSO; this
// number is assigned based on what the command number would be if it were. On
// DC NTE, this is subcommand 6x1A; on 11/2000, it's 6x1C.
// The command is truncated after the last valid item in the inventory (that
// is, there will be less than 0x360 bytes if the player has fewer than 30
// items on hand).
struct G_PartialPlayerData_DCProtos_6x1E {
/* 0000 */ G_ClientIDHeader header;
/* 0004 */ le_uint16_t floor;
/* 0006 */ le_uint16_t num_items;
/* 0008 */ VectorXYZF pos;
/* 0014 */ le_uint32_t angle_y;
/* 0018 */ parray<PlayerInventoryItem, 30> items;
/* 0360 */
} __packed_ws__(G_PartialPlayerData_DCProtos_6x1E, 0x360);
// 6x1F: Set player floor and request positions
@@ -4275,7 +4354,8 @@ struct G_FeedMag_6x28 {
le_uint32_t fed_item_id = 0;
} __packed_ws__(G_FeedMag_6x28, 0x0C);
// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG) (protected on V3 but not V4)
// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG)
// (protected on V3 but not on V4)
// This subcommand is also used for reducing the size of stacks - if amount is
// less than the stack count, the item is not deleted and its ID remains valid.
@@ -4391,7 +4471,18 @@ struct G_RevivePlayer_6x33 {
// 6x34: Unknown
// This subcommand is ignored by all versions of PSO.
// 6x35: Invalid subcommand
// 6x35: Unknown (pre-v1 only)
// This command seems to have a unique history. In DC NTE, it is 6x30, and has
// a handler that does something related to what 6x36 does (6x31 in DC NTE). In
// 11/2000, however, it seems to have been entirely deleted, and has no command
// number at all. But then in DC v1 and later, there is a gap in the subcommand
// number table, implying that this command was assigned a number, but there is
// no function to send or handle it.
struct G_Unknown_DCNTE_6x35 {
G_ClientIDHeader header;
parray<uint8_t, 4> unused;
} __packed_ws__(G_Unknown_DCNTE_6x35, 8);
// 6x36: Unknown (supported; game only)
// This subcommand is completely ignored on V3.
@@ -4415,10 +4506,20 @@ struct G_PhotonBlast_6x37 {
struct G_DonateToPhotonBlast_6x38 {
G_ClientIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t target_client_id = 0;
le_uint16_t unused = 0;
} __packed_ws__(G_DonateToPhotonBlast_6x38, 8);
// 6x38.5 (nominally) / 6x33 (DC NTE) / 6x35 (11/2000): Unknown (related to
// level up sequence)
// This command was deleted after 11/2000 and has no assigned number in any
// final version of PSO, hence the odd numbering. It is sent during the level
// up sequence in certain situations (TODO).
struct G_UnknownLevelUpSequence_DCNTE_6x33_112000_6x35 {
G_ClientIDHeader header;
} __packed_ws__(G_UnknownLevelUpSequence_DCNTE_6x33_112000_6x35, 4);
// 6x39: Photon blast ready (protected on V3/V4)
// This is sent when a player's PB meter reaches 100.
@@ -4444,7 +4545,21 @@ struct G_ClearTemporaryPhotonBlastStateFlags_6x3B {
// 6x3C: Unknown (DCv1 and earlier)
// This command has a handler, but it does nothing, even on DC NTE.
// 6x3D: Invalid subcommand
// 6x3D: Target list base
// This appears to be a base class for 6x46, 6x47, and 6x49 (and possibly other
// subcommands), but it is never sent on the wire. Its likely purpose is to
// provide the TargetEntry structure and related functions to derived classes,
// but it does still have a subcommand number and a structure of its own, as
// described here.
struct TargetEntry {
le_uint16_t entity_id = 0;
le_uint16_t unknown_a2 = 0;
} __packed_ws__(TargetEntry, 4);
struct G_TargetBase_6x3D {
G_UnusedHeader header;
} __packed_ws__(G_TargetBase_6x3D, 4);
// 6x3E: Stop moving (protected on V3/V4)
@@ -4496,7 +4611,7 @@ struct G_MoveToPosition_6x41_6x42 {
struct G_Attack_6x43_6x44_6x45 {
G_ClientIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t angle_y = 0;
le_uint16_t unknown_a2 = 0;
} __packed_ws__(G_Attack_6x43_6x44_6x45, 8);
@@ -4506,22 +4621,16 @@ struct G_Attack_6x43_6x44_6x45 {
// targets is too large, the client will byteswap the function's return address
// on the stack, and it will crash.
struct TargetEntry {
le_uint16_t entity_id = 0;
le_uint16_t unknown_a2 = 0;
} __packed_ws__(TargetEntry, 4);
struct G_AttackFinished_6x46 {
struct G_AttackFinished_Header_6x46 {
G_ClientIDHeader header;
le_uint32_t target_count = 0;
// The client may send a shorter command if not all of these are used.
parray<TargetEntry, 10> targets;
} __packed_ws__(G_AttackFinished_6x46, 0x30);
// Up to 10 TargetEntries are sent here
} __packed_ws__(G_AttackFinished_Header_6x46, 8);
// 6x47: Cast technique (protected on V3/V4)
// On GC, this command has the same bounds-check bug as 6x46.
struct G_CastTechnique_6x47 {
struct G_CastTechnique_Header_6x47 {
G_ClientIDHeader header;
uint8_t technique_number = 0;
uint8_t unused = 0; // Must not be negative
@@ -4532,9 +4641,8 @@ struct G_CastTechnique_6x47 {
// cleaned it up.
uint8_t level = 0;
uint8_t target_count = 0; // Must be in [0, 10]
// The client may send a shorter command if not all of these are used.
parray<TargetEntry, 10> targets;
} __packed_ws__(G_CastTechnique_6x47, 0x30);
// Up to 10 TargetEntries are sent here
} __packed_ws__(G_CastTechnique_Header_6x47, 8);
// 6x48: Cast technique complete (protected on V3/V4)
@@ -4549,16 +4657,15 @@ struct G_CastTechniqueComplete_6x48 {
// 6x49: Execute Photon Blast (protected on V3/V4)
// On GC, this command has the same bounds-check bug as 6x46.
struct G_ExecutePhotonBlast_6x49 {
struct G_ExecutePhotonBlast_Header_6x49 {
G_ClientIDHeader header;
uint8_t unknown_a1 = 0;
uint8_t unknown_a2 = 0;
int8_t unknown_a1 = 0;
uint8_t unused1 = 0;
le_uint16_t target_count = 0;
le_uint16_t unknown_a3 = 0;
le_uint16_t unknown_a4 = 0;
// The client may send a shorter command if not all of these are used.
parray<TargetEntry, 10> targets;
} __packed_ws__(G_ExecutePhotonBlast_6x49, 0x34);
le_uint16_t pb_meter_value = 0;
le_uint16_t unused2 = 0;
// Up to 10 TargetEntries are sent here
} __packed_ws__(G_ExecutePhotonBlast_Header_6x49, 0x0C);
// 6x4A: Fully shield attack (protected on V3/V4)
@@ -4584,6 +4691,8 @@ struct G_PlayerDied_6x4D {
} __packed_ws__(G_PlayerDied_6x4D, 8);
// 6x4E: Player is dead can be revived (protected on V3/V4)
// This command creates the particle effect that Reverser and Moon Atomizers
// can target.
struct G_PlayerRevivable_6x4E {
G_ClientIDHeader header;
@@ -4606,8 +4715,8 @@ struct G_SwitchInteraction_6x50 {
// 6x51: Set player angle
// If UDP mode is enabled, this command is sent via UDP.
// This command appears to be vestigial - no version of the game has a handler
// for it (it is always ignored), but PSO GC has a function that sends it. It's
// not known if this function is ever called, or how to trigger it.
// for it (it is always ignored), but most versions have a function that sends
// it. It's not known if this function is ever called, or how to trigger it.
struct G_SetPlayerAngle_6x51 {
G_ClientIDHeader header;
@@ -4982,9 +5091,7 @@ struct G_6x70_Base_DCNTE {
/* 0002 */ le_uint16_t room_id = 0;
/* 0004 */ le_uint32_t flags1 = 0;
/* 0008 */ VectorXYZF pos;
/* 0014 */ le_uint32_t angle_x = 0;
/* 0018 */ le_uint32_t angle_y = 0;
/* 001C */ le_uint32_t angle_z = 0;
/* 0014 */ VectorXYZI angle;
/* 0020 */ le_uint16_t unknown_a3a = 0;
/* 0022 */ le_uint16_t current_hp = 0;
} __packed_ws__(G_6x70_Base_DCNTE, 0x24);
@@ -5175,6 +5282,10 @@ struct G_SyncQuestRegister_6x77 {
} __packed_ws__(G_SyncQuestRegister_6x77, 0x0C);
// 6x78: Unknown
// This command appears to set a per-player timer of some sort. It was
// introduced in v2, and there is an object that uses this timer, but it seems
// that that object is never constructed. In v3 and later, that object has been
// entirely deleted, so the command does nothing.
struct G_Unknown_6x78 {
G_UnusedHeader header;
@@ -5187,10 +5298,10 @@ struct G_Unknown_6x78 {
struct G_GogoBall_6x79 {
G_UnusedHeader header;
le_uint32_t unknown_a1 = 0;
le_uint32_t unknown_a2 = 0;
le_uint32_t unknown_a1 = 0; // Appears to be time-related; it's based on server_time_delta_frames
le_uint32_t angle = 0;
VectorXZF ball_pos;
uint8_t unknown_a5 = 0;
uint8_t use_missile_sound = 0;
parray<uint8_t, 3> unused;
} __packed_ws__(G_GogoBall_6x79, 0x18);
@@ -5306,10 +5417,7 @@ struct G_PlaceTrap_6x83 {
// 6x84: Vol Opt boss actions (not valid on Episode 3)
// Same format and usage as 6x16, except unknown_a2 is ignored in 6x84.
struct G_VolOptBossActions_6x84 {
G_UnusedHeader header;
parray<uint8_t, 6> unknown_a1;
le_uint16_t unknown_a2 = 0;
struct G_VolOptBossActions_6x84 : G_VolOptBossActions_6x16 {
le_uint16_t unknown_a3 = 0;
le_uint16_t unused = 0;
} __packed_ws__(G_VolOptBossActions_6x84, 0x10);
@@ -5668,11 +5776,13 @@ struct G_Unknown_GCNTE_6xAB {
struct G_CreateLobbyChair_6xAB {
G_ClientIDHeader header;
le_uint16_t unused = 0;
le_uint16_t flags = 0; // Only the low two bits are used
// Only two bits in this field have meanings:
// 01 = unknown
// 02 = which pose/animation (on GC, 0 = X+A, 1 = X+B)
le_uint16_t flags = 0;
} __packed_ws__(G_CreateLobbyChair_6xAB, 8);
// 6xAC: Unknown (not valid on pre-V3)
// This command appears to be different on GC NTE than on any other version.
// 6xAC: De Rol Le / Barba Ray boss actions (GC NTE)
struct G_Unknown_GCNTE_6xAC {
G_EntityIDHeader header;
@@ -5702,28 +5812,30 @@ struct G_OlgaFlowSubordinateBossActions_6xAD {
parray<uint8_t, 0x40> unknown_a1;
} __packed_ws__(G_OlgaFlowSubordinateBossActions_6xAD, 0x44);
// 6xAE: Set lobby chair state (sent by existing clients at join time)
// 6xAE: Set animation state (sent by existing clients at join time)
// This subcommand is not valid on DC, PC, or GC Trial Edition.
struct G_SetLobbyChairState_6xAE {
struct G_SetAnimationState_6xAE {
G_ClientIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t animation_number = 0;
le_uint16_t unused = 0;
// This field contains the flags field on the sender's TObjPlayer object.
// If the bit 04000000 is set in this field, then (flags & 1C000000) is or'ed
// into the TObjPlayer's flags field. All other bits are ignored.
le_uint32_t flags = 0;
le_float unknown_a4 = 0;
} __packed_ws__(G_SetLobbyChairState_6xAE, 0x10);
le_float animation_timer = 0;
} __packed_ws__(G_SetAnimationState_6xAE, 0x10);
// 6xAF: Turn lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4)
// 6xAF: Turn lobby chair (not valid on pre-V3 or GC Trial Edition) (protected
// on V3/V4)
struct G_TurnLobbyChair_6xAF {
G_ClientIDHeader header;
le_uint32_t angle = 0; // In range [0x0000, 0xFFFF]
} __packed_ws__(G_TurnLobbyChair_6xAF, 8);
// 6xB0: Move lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4)
// 6xB0: Move lobby chair (not valid on pre-V3 or GC Trial Edition) (protected
// on V3/V4)
struct G_MoveLobbyChair_6xB0 {
G_ClientIDHeader header;
@@ -6276,7 +6388,7 @@ struct G_ExchangeItemInQuest_BB_6xDB {
G_ClientIDHeader header;
// If this is 0, the command is identical to 6x29. If this is 1, a function
// similar to find_item_by_id is called instead of find_item_by_id, but I
// don't yet know what exactly the logic differences are.
// don't yet know what exactly the logic differences are (TODO).
le_uint32_t unknown_a1 = 0;
le_uint32_t item_id = 0;
le_uint32_t amount = 0;
@@ -6292,12 +6404,21 @@ struct G_Episode4BossActions_BB_6xDC {
// 6xDD: Set EXP multiplier (BB)
// header.param specifies the EXP multiplier. It is 1-based, so the value 2
// means all EXP is doubled, for example.
// means all EXP is doubled, for example. This only affects what the client
// shows when an enemy is killed; actual EXP gains are controlled by the server
// in response to the 6xC8 command.
// newserv supports an extension to this command that supports fractional
// multipliers. This is implemented in FractionalEXPMultiplier.59NL.patch.s.
struct G_SetEXPMultiplier_BB_6xDD {
G_ParameterHeader header;
} __packed_ws__(G_SetEXPMultiplier_BB_6xDD, 4);
struct G_SetFractionalEXPMultiplier_Extension_BB_6xDD {
G_ParameterHeader header;
le_float multiplier;
} __packed_ws__(G_SetFractionalEXPMultiplier_Extension_BB_6xDD, 8);
// 6xDE: Exchange Secret Lottery Ticket (BB; handled by server)
// The client sends this when it executes an F95C quest opcode.
// There appears to be a bug in the client here: it sets the subcommand size to
@@ -6333,7 +6454,8 @@ struct G_RequestItemDropFromQuest_BB_6xE0 {
} __packed_ws__(G_RequestItemDropFromQuest_BB_6xE0, 0x10);
// 6xE1: Exchange Photon Tickets (BB; handled by server)
// The client sends this when it executes an F95F quest opcode.
// The client sends this when it executes an F95F quest opcode. The comments
// denote where in the command each argument to that opcode is sent.
struct G_ExchangePhotonTickets_BB_6xE1 {
G_ClientIDHeader header;
@@ -6366,7 +6488,7 @@ struct G_SetMesetaSlotPrizeResult_BB_6xE3 {
ItemData item;
} __packed_ws__(G_SetMesetaSlotPrizeResult_BB_6xE3, 0x18);
// 6xE4: Invalid subcommand
// 6xE4: Invalid subcommand (but used as an extension; see end of this file)
// 6xE5: Invalid subcommand
// 6xE6: Invalid subcommand
// 6xE7: Invalid subcommand
@@ -6661,9 +6783,10 @@ struct G_ForceDisconnect_Ep3_6xB5x1A {
// No arguments
} __packed_ws__(G_ForceDisconnect_Ep3_6xB5x1A, 8);
// 6xB3x1B / CAx1B: Set player name during setup
// Curiously, this command can be used during a non-setup phase; the server
// should ignore the command's contents but still send a 6xB4x1C in response.
// 6xB3x1B / CAx1B: Set player name
// This command is normally sent during battle setup to populate player slots,
// but is also sent if a player disconnects during battle to switch their
// player type from human to CPU.
struct G_SetPlayerName_Ep3_CAx1B {
G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetPlayerName_Ep3_CAx1B) / 4, 0, 0x1B, 0, 0, 0, 0, 0};
@@ -6817,7 +6940,7 @@ struct G_EnqueueAnimation_Ep3_6xB4x2C {
/* 0A */ parray<le_uint16_t, 3> card_refs;
/* 10 */ Episode3::Location loc;
/* 14 */ le_uint32_t trap_card_id = 0xFFFFFFFF;
/* 14 */ le_uint32_t unknown_a3 = 0xFFFFFFFF;
/* 18 */ le_uint32_t unknown_a3 = 0xFFFFFFFF;
/* 1C */
} __packed_ws__(G_EnqueueAnimation_Ep3_6xB4x2C, 0x1C);
@@ -7085,7 +7208,11 @@ struct G_OpenBlockingMenu_Ep3_6xB5x3F {
int8_t menu_type = 0; // Must be in the range [-1, 0x14]
uint8_t client_id = 0;
parray<uint8_t, 2> unused1;
le_uint32_t unknown_a3 = 0;
// The random_seed field is only used for the battle prep menu (01/02); for
// other menu types, it contains uninitialized data. This is used as the
// seed for a PSOV2Encryption instance, but it's not clear what that instance
// is used for (if anything).
le_uint32_t random_seed = 0;
parray<uint8_t, 4> unused2;
} __packed_ws__(G_OpenBlockingMenu_Ep3_6xB5x3F, 0x14);
@@ -7122,9 +7249,9 @@ struct G_InitiateCardAuction_Ep3_6xB5x42 {
// 6xB5x43: Unused legacy card auction
// This command stores the card IDs and counts in a global array on the client,
// but this array is never read from. The function that handles this command is
// remarkably similar to the function that handles the EF command, so It's
// likely that this command is a now-unused early implementation of the card
// auction sequence.
// very similar to the function that handles the EF command, so it's likely
// that this command is a now-unused early implementation of the card auction
// initiation sequence.
struct G_UnusedLegacyCardAuction_Ep3_6xB5x43 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_UnusedLegacyCardAuction_Ep3_6xB5x43) / 4, 0, 0x43, 0, 0, 0};
@@ -7132,7 +7259,7 @@ struct G_UnusedLegacyCardAuction_Ep3_6xB5x43 {
// Both fields here are masked. To get the actual values used by the game,
// XOR the values here with 0x39AB.
le_uint16_t masked_card_id = 0xFFFF; // Must be < 0x2F1 (when unmasked)
le_uint16_t masked_count = 0; // Must be in [1, 99] (when unmasked)
le_uint16_t masked_cost = 0; // Must be in [1, 99] (when unmasked)
} __packed_ws__(Entry, 4);
parray<Entry, 0x14> entries;
} __packed_ws__(G_UnusedLegacyCardAuction_Ep3_6xB5x43, 0x58);
@@ -7431,4 +7558,19 @@ struct G_RejectBattleStartRequest_Ep3_6xB4x53 {
// Requested with the GetExtendedPlayerInfo patch. Format depends on version:
// DC v2: PSODCV2CharacterFile
// GC v3: PSOGCCharacterFile::Character
// XB v3: PSOXBCharacterFileCharacter
// XB v3: PSOXBCharacterFile::Character
// 6xE4: Increment enemy damage
// This command increments or decrements the amount of damage an enemy has
// sustained. This replaces the use of total_damage in 6x0A to update enemy HP
// when used with the EnemyDamageSync patch.
struct G_IncrementEnemyDamage_Extension_6xE4 {
/* 00 */ G_EntityIDHeader header = {0xE4, sizeof(G_IncrementEnemyDamage_Extension_6xE4) / 4, 0x0000};
/* 04 */ le_int16_t hit_amount = 0;
/* 06 */ le_uint16_t total_damage_before_hit = 0;
/* 08 */ le_uint16_t current_hp_before_hit = 0;
/* 0A */ le_uint16_t max_hp = 0;
/* 0C */ le_float factor = -1.0;
/* 10 */
} __packed_ws__(G_IncrementEnemyDamage_Extension_6xE4, 0x10);
+232 -57
View File
@@ -48,11 +48,7 @@ void from_json_into(const phosg::JSON& json, parray<CommonItemSet::Table::Range<
template <typename IntT>
phosg::JSON to_json(const CommonItemSet::Table::Range<IntT>& v) {
if (v.min == v.max) {
return phosg::JSON(v.min);
} else {
return phosg::JSON::list({v.min, v.max});
}
return (v.min == v.max) ? phosg::JSON(v.min) : phosg::JSON::list({v.min, v.max});
}
template <typename IntT>
@@ -128,8 +124,15 @@ CommonItemSet::Table::Table(const phosg::JSON& json, Episode episode)
for (size_t z = 0; z < 0x64; z++) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
auto types = enemy_types_for_rare_table_index(episode, z);
vector<string> names;
if (types.empty()) {
names.emplace_back(std::format("{}:!{:02X}", abbreviation_for_episode(episode), z));
}
for (auto type : enemy_types_for_rare_table_index(episode, z)) {
string name = std::format("{}:{}", abbreviation_for_episode(episode), phosg::name_for_enum(type));
names.emplace_back(std::format("{}:{}", abbreviation_for_episode(episode), phosg::name_for_enum(type)));
}
for (const auto& name : names) {
from_json_into(*enemy_meseta_ranges_json.at(name), this->enemy_meseta_ranges[z]);
this->enemy_type_drop_probs[z] = enemy_type_drop_probs_json.at(name)->as_int();
this->enemy_item_classes[z] = enemy_item_classes_json.at(name)->as_int();
@@ -336,6 +339,122 @@ void CommonItemSet::Table::print(FILE* stream) const {
}
}
void CommonItemSet::Table::print_diff(FILE* stream, const Table& other) const {
if (this->episode != other.episode) {
phosg::fwrite_fmt(stream, "> Episode: {} -> {}\n", name_for_episode(this->episode), name_for_episode(other.episode));
}
if (this->base_weapon_type_prob_table != other.base_weapon_type_prob_table) {
phosg::fwrite_fmt(stream, "> base_weapon_type_prob_table: {} -> {}\n",
phosg::format_data_string(&this->base_weapon_type_prob_table, sizeof(this->base_weapon_type_prob_table)),
phosg::format_data_string(&other.base_weapon_type_prob_table, sizeof(other.base_weapon_type_prob_table)));
}
if (this->subtype_base_table != other.subtype_base_table) {
phosg::fwrite_fmt(stream, "> subtype_base_table: {} -> {}\n",
phosg::format_data_string(&this->subtype_base_table, sizeof(this->subtype_base_table)),
phosg::format_data_string(&other.subtype_base_table, sizeof(other.subtype_base_table)));
}
if (this->subtype_area_length_table != other.subtype_area_length_table) {
phosg::fwrite_fmt(stream, "> subtype_area_length_table: {} -> {}\n",
phosg::format_data_string(&this->subtype_area_length_table, sizeof(this->subtype_area_length_table)),
phosg::format_data_string(&other.subtype_area_length_table, sizeof(other.subtype_area_length_table)));
}
if (this->grind_prob_table != other.grind_prob_table) {
phosg::fwrite_fmt(stream, "> grind_prob_table: {} -> {}\n",
phosg::format_data_string(&this->grind_prob_table, sizeof(this->grind_prob_table)),
phosg::format_data_string(&other.grind_prob_table, sizeof(other.grind_prob_table)));
}
if (this->armor_shield_type_index_prob_table != other.armor_shield_type_index_prob_table) {
phosg::fwrite_fmt(stream, "> armor_shield_type_index_prob_table: {} -> {}\n",
phosg::format_data_string(&this->armor_shield_type_index_prob_table, sizeof(this->armor_shield_type_index_prob_table)),
phosg::format_data_string(&other.armor_shield_type_index_prob_table, sizeof(other.armor_shield_type_index_prob_table)));
}
if (this->armor_slot_count_prob_table != other.armor_slot_count_prob_table) {
phosg::fwrite_fmt(stream, "> armor_slot_count_prob_table: {} -> {}\n",
phosg::format_data_string(&this->armor_slot_count_prob_table, sizeof(this->armor_slot_count_prob_table)),
phosg::format_data_string(&other.armor_slot_count_prob_table, sizeof(other.armor_slot_count_prob_table)));
}
if (this->enemy_meseta_ranges != other.enemy_meseta_ranges) {
phosg::fwrite_fmt(stream, "> enemy_meseta_ranges: {} -> {}\n",
phosg::format_data_string(&this->enemy_meseta_ranges, sizeof(this->enemy_meseta_ranges)),
phosg::format_data_string(&other.enemy_meseta_ranges, sizeof(other.enemy_meseta_ranges)));
}
if (this->enemy_type_drop_probs != other.enemy_type_drop_probs) {
phosg::fwrite_fmt(stream, "> enemy_type_drop_probs: {} -> {}\n",
phosg::format_data_string(&this->enemy_type_drop_probs, sizeof(this->enemy_type_drop_probs)),
phosg::format_data_string(&other.enemy_type_drop_probs, sizeof(other.enemy_type_drop_probs)));
}
if (this->enemy_item_classes != other.enemy_item_classes) {
phosg::fwrite_fmt(stream, "> enemy_item_classes: {} -> {}\n",
phosg::format_data_string(&this->enemy_item_classes, sizeof(this->enemy_item_classes)),
phosg::format_data_string(&other.enemy_item_classes, sizeof(other.enemy_item_classes)));
}
if (this->box_meseta_ranges != other.box_meseta_ranges) {
phosg::fwrite_fmt(stream, "> box_meseta_ranges: {} -> {}\n",
phosg::format_data_string(&this->box_meseta_ranges, sizeof(this->box_meseta_ranges)),
phosg::format_data_string(&other.box_meseta_ranges, sizeof(other.box_meseta_ranges)));
}
if (this->has_rare_bonus_value_prob_table != other.has_rare_bonus_value_prob_table) {
phosg::fwrite_fmt(stream, "> Has rare bonus value prob table: {} -> {}\n",
this->has_rare_bonus_value_prob_table ? "true" : "false",
other.has_rare_bonus_value_prob_table ? "true" : "false");
}
if (this->bonus_value_prob_table != other.bonus_value_prob_table) {
phosg::fwrite_fmt(stream, "> bonus_value_prob_table: {} -> {}\n",
phosg::format_data_string(&this->bonus_value_prob_table, sizeof(this->bonus_value_prob_table)),
phosg::format_data_string(&other.bonus_value_prob_table, sizeof(other.bonus_value_prob_table)));
}
if (this->nonrare_bonus_prob_spec != other.nonrare_bonus_prob_spec) {
phosg::fwrite_fmt(stream, "> nonrare_bonus_prob_spec: {} -> {}\n",
phosg::format_data_string(&this->nonrare_bonus_prob_spec, sizeof(this->nonrare_bonus_prob_spec)),
phosg::format_data_string(&other.nonrare_bonus_prob_spec, sizeof(other.nonrare_bonus_prob_spec)));
}
if (this->bonus_type_prob_table != other.bonus_type_prob_table) {
phosg::fwrite_fmt(stream, "> bonus_type_prob_table: {} -> {}\n",
phosg::format_data_string(&this->bonus_type_prob_table, sizeof(this->bonus_type_prob_table)),
phosg::format_data_string(&other.bonus_type_prob_table, sizeof(other.bonus_type_prob_table)));
}
if (this->special_mult != other.special_mult) {
phosg::fwrite_fmt(stream, "> special_mult: {} -> {}\n",
phosg::format_data_string(&this->special_mult, sizeof(this->special_mult)),
phosg::format_data_string(&other.special_mult, sizeof(other.special_mult)));
}
if (this->special_percent != other.special_percent) {
phosg::fwrite_fmt(stream, "> special_percent: {} -> {}\n",
phosg::format_data_string(&this->special_percent, sizeof(this->special_percent)),
phosg::format_data_string(&other.special_percent, sizeof(other.special_percent)));
}
if (this->tool_class_prob_table != other.tool_class_prob_table) {
phosg::fwrite_fmt(stream, "> tool_class_prob_table: {} -> {}\n",
phosg::format_data_string(&this->tool_class_prob_table, sizeof(this->tool_class_prob_table)),
phosg::format_data_string(&other.tool_class_prob_table, sizeof(other.tool_class_prob_table)));
}
if (this->technique_index_prob_table != other.technique_index_prob_table) {
phosg::fwrite_fmt(stream, "> technique_index_prob_table: {} -> {}\n",
phosg::format_data_string(&this->technique_index_prob_table, sizeof(this->technique_index_prob_table)),
phosg::format_data_string(&other.technique_index_prob_table, sizeof(other.technique_index_prob_table)));
}
if (this->technique_level_ranges != other.technique_level_ranges) {
phosg::fwrite_fmt(stream, "> technique_level_ranges: {} -> {}\n",
phosg::format_data_string(&this->technique_level_ranges, sizeof(this->technique_level_ranges)),
phosg::format_data_string(&other.technique_level_ranges, sizeof(other.technique_level_ranges)));
}
if (this->armor_or_shield_type_bias != other.armor_or_shield_type_bias) {
phosg::fwrite_fmt(stream, "> Armor/shield type bias: {} -> {}\n",
this->armor_or_shield_type_bias ? "true" : "false",
other.armor_or_shield_type_bias ? "true" : "false");
}
if (this->unit_max_stars_table != other.unit_max_stars_table) {
phosg::fwrite_fmt(stream, "> unit_max_stars_table: {} -> {}\n",
phosg::format_data_string(&this->unit_max_stars_table, sizeof(this->unit_max_stars_table)),
phosg::format_data_string(&other.unit_max_stars_table, sizeof(other.unit_max_stars_table)));
}
if (this->box_item_class_prob_table != other.box_item_class_prob_table) {
phosg::fwrite_fmt(stream, "> box_item_class_prob_table: {} -> {}\n",
phosg::format_data_string(&this->box_item_class_prob_table, sizeof(this->box_item_class_prob_table)),
phosg::format_data_string(&other.box_item_class_prob_table, sizeof(other.box_item_class_prob_table)));
}
}
phosg::JSON CommonItemSet::Table::json() const {
phosg::JSON enemy_meseta_ranges_json = phosg::JSON::dict();
phosg::JSON enemy_type_drop_probs_json = phosg::JSON::dict();
@@ -343,8 +462,16 @@ phosg::JSON CommonItemSet::Table::json() const {
for (size_t z = 0; z < 0x64; z++) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
for (auto type : enemy_types_for_rare_table_index(episode, z)) {
string name = std::format("{}:{}", abbreviation_for_episode(episode), phosg::name_for_enum(type));
auto types = enemy_types_for_rare_table_index(episode, z);
vector<string> names;
if (types.empty()) {
names.emplace_back(std::format("{}:!{:02X}", abbreviation_for_episode(episode), z));
} else {
for (auto type : types) {
names.emplace_back(std::format("{}:{}", abbreviation_for_episode(episode), phosg::name_for_enum(type)));
}
}
for (const auto& name : names) {
enemy_meseta_ranges_json.emplace(name, to_json(this->enemy_meseta_ranges[z]));
enemy_type_drop_probs_json.emplace(name, this->enemy_type_drop_probs[z]);
enemy_item_classes_json.emplace(name, this->enemy_item_classes[z]);
@@ -424,6 +551,43 @@ void CommonItemSet::print(FILE* stream) const {
}
}
void CommonItemSet::print_diff(FILE* stream, const CommonItemSet& other) const {
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (const auto& mode : modes) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (const auto& episode : episodes) {
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
for (uint8_t section_id = 0; section_id < 10; section_id++) {
shared_ptr<const Table> this_table;
shared_ptr<const Table> other_table;
try {
this_table = this->get_table(episode, mode, difficulty, section_id);
} catch (const runtime_error&) {
}
try {
other_table = other.get_table(episode, mode, difficulty, section_id);
} catch (const runtime_error&) {
}
if (!this_table && !other_table) {
continue;
} else if (!this_table) {
phosg::fwrite_fmt(stream, "> Table present in other but not this: {} {} {} {}\n",
name_for_mode(mode), name_for_episode(episode), name_for_difficulty(difficulty), name_for_section_id(section_id));
} else if (!other_table) {
phosg::fwrite_fmt(stream, "> Table present in this but not other: {} {} {} {}\n",
name_for_mode(mode), name_for_episode(episode), name_for_difficulty(difficulty), name_for_section_id(section_id));
} else if (*this_table != *other_table) {
phosg::fwrite_fmt(stream, "> Tables do not match: {} {} {} {}\n",
name_for_mode(mode), name_for_episode(episode), name_for_difficulty(difficulty), name_for_section_id(section_id));
this_table->print_diff(stream, *other_table);
}
}
}
}
}
}
CommonItemSet::Table::Table(const phosg::StringReader& r, bool is_big_endian, bool is_v3, Episode episode)
: episode(episode) {
if (is_big_endian) {
@@ -509,29 +673,49 @@ shared_ptr<const CommonItemSet::Table> CommonItemSet::get_table(
}
AFSV2CommonItemSet::AFSV2CommonItemSet(
std::shared_ptr<const std::string> pt_afs_data,
std::shared_ptr<const std::string> ct_afs_data) {
// ItemPT.afs has 40 entries; the first 10 are for Normal, then Hard, etc.
AFSArchive pt_afs(pt_afs_data);
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
for (size_t section_id = 0; section_id < 10; section_id++) {
auto entry = pt_afs.get(difficulty * 10 + section_id);
phosg::StringReader r(entry.first, entry.second);
auto table = make_shared<Table>(r, false, false, Episode::EP1);
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::NORMAL, difficulty, section_id), table);
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::BATTLE, difficulty, section_id), table);
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::SOLO, difficulty, section_id), table);
std::shared_ptr<const std::string> pt_afs_data, std::shared_ptr<const std::string> ct_afs_data) {
// Each AFS file has 40 entries (30 on v1); the first 10 are for Normal, then
// Hard, etc.
{
AFSArchive pt_afs(pt_afs_data);
size_t max_difficulty;
if (pt_afs.num_entries() >= 40) {
max_difficulty = 4;
} else if (pt_afs.num_entries() >= 30) {
max_difficulty = 3;
} else {
throw std::runtime_error(std::format("PT AFS file has unexpected entry count ({})", pt_afs.num_entries()));
}
for (size_t difficulty = 0; difficulty < max_difficulty; difficulty++) {
for (size_t section_id = 0; section_id < 10; section_id++) {
auto entry = pt_afs.get(difficulty * 10 + section_id);
phosg::StringReader r(entry.first, entry.second);
auto table = make_shared<Table>(r, false, false, Episode::EP1);
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::NORMAL, difficulty, section_id), table);
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::BATTLE, difficulty, section_id), table);
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::SOLO, difficulty, section_id), table);
}
}
}
// ItemCT.afs also has 40 entries, but only the 0th, 10th, 20th, and 30th are
// used (section_id is ignored)
AFSArchive ct_afs(ct_afs_data);
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
auto r = ct_afs.get_reader(difficulty * 10);
auto table = make_shared<Table>(r, false, false, Episode::EP1);
for (size_t section_id = 0; section_id < 10; section_id++) {
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::CHALLENGE, difficulty, section_id), table);
// ItemCT AFS files also have 40 entries, but only the 0th, 10th, 20th, and
// 30th are used (section_id is ignored)
if (ct_afs_data) {
AFSArchive ct_afs(ct_afs_data);
size_t max_difficulty;
if (ct_afs.num_entries() >= 40) {
max_difficulty = 4;
} else if (ct_afs.num_entries() >= 30) {
max_difficulty = 3;
} else {
throw std::runtime_error(std::format("CT AFS file has unexpected entry count ({})", ct_afs.num_entries()));
}
for (size_t difficulty = 0; difficulty < max_difficulty; difficulty++) {
auto r = ct_afs.get_reader(difficulty * 10);
auto table = make_shared<Table>(r, false, false, Episode::EP1);
for (size_t section_id = 0; section_id < 10; section_id++) {
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::CHALLENGE, difficulty, section_id), table);
}
}
}
}
@@ -590,10 +774,14 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gs
if (episode != Episode::EP4) {
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
auto table = make_shared<Table>(r, is_big_endian, true, episode);
for (size_t section_id = 0; section_id < 10; section_id++) {
this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table);
try {
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
auto table = make_shared<Table>(r, is_big_endian, true, episode);
for (size_t section_id = 0; section_id < 10; section_id++) {
this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table);
}
} catch (const out_of_range&) {
// GC NTE doesn't have Ep2 challenge; just skip adding the table
}
}
}
@@ -628,8 +816,7 @@ JSONCommonItemSet::JSONCommonItemSet(const phosg::JSON& json) {
}
RELFileSet::RELFileSet(std::shared_ptr<const std::string> data)
: data(data),
r(*this->data) {}
: data(data), r(*this->data) {}
ArmorRandomSet::ArmorRandomSet(std::shared_ptr<const std::string> data)
: RELFileSet(data) {
@@ -657,18 +844,13 @@ ArmorRandomSet::get_unit_table(size_t index) const {
ToolRandomSet::ToolRandomSet(std::shared_ptr<const std::string> data)
: RELFileSet(data) {
uint32_t specs_offset = r.pget_u32b(data->size() - 0x10);
this->common_recovery_table_spec = &r.pget<TableSpec>(r.pget_u32b(
specs_offset));
this->rare_recovery_table_spec = &r.pget<TableSpec>(
r.pget_u32b(specs_offset + sizeof(uint32_t)),
2 * sizeof(TableSpec));
this->common_recovery_table_spec = &r.pget<TableSpec>(r.pget_u32b(specs_offset));
this->rare_recovery_table_spec = &r.pget<TableSpec>(r.pget_u32b(specs_offset + sizeof(uint32_t)), 2 * sizeof(TableSpec));
this->tech_disk_table_spec = this->rare_recovery_table_spec + 1;
this->tech_disk_level_table_spec = &r.pget<TableSpec>(r.pget_u32b(
specs_offset + 2 * sizeof(uint32_t)));
this->tech_disk_level_table_spec = &r.pget<TableSpec>(r.pget_u32b(specs_offset + 2 * sizeof(uint32_t)));
}
pair<const uint8_t*, size_t> ToolRandomSet::get_common_recovery_table(
size_t index) const {
pair<const uint8_t*, size_t> ToolRandomSet::get_common_recovery_table(size_t index) const {
return this->get_table<uint8_t>(*this->common_recovery_table_spec, index);
}
@@ -695,25 +877,21 @@ WeaponRandomSet::WeaponRandomSet(std::shared_ptr<const std::string> data)
std::pair<const WeaponRandomSet::WeightTableEntry8*, size_t>
WeaponRandomSet::get_weapon_type_table(size_t index) const {
const auto& spec = this->r.pget<TableSpec>(
this->offsets->weapon_type_table + index * sizeof(TableSpec));
const auto* data = &this->r.pget<WeightTableEntry8>(
spec.offset, spec.entries_per_table * sizeof(WeightTableEntry8));
const auto& spec = this->r.pget<TableSpec>(this->offsets->weapon_type_table + index * sizeof(TableSpec));
const auto* data = &this->r.pget<WeightTableEntry8>(spec.offset, spec.entries_per_table * sizeof(WeightTableEntry8));
return make_pair(data, spec.entries_per_table);
}
const parray<WeaponRandomSet::WeightTableEntry32, 6>*
WeaponRandomSet::get_bonus_type_table(size_t which, size_t index) const {
uint32_t base_offset = which ? this->offsets->bonus_type_table2 : this->offsets->bonus_type_table1;
return &this->r.pget<parray<WeightTableEntry32, 6>>(
base_offset + sizeof(parray<WeightTableEntry32, 6>) * index);
return &this->r.pget<parray<WeightTableEntry32, 6>>(base_offset + sizeof(parray<WeightTableEntry32, 6>) * index);
}
const WeaponRandomSet::RangeTableEntry*
WeaponRandomSet::get_bonus_range(size_t which, size_t index) const {
uint32_t base_offset = which ? this->offsets->bonus_range_table2 : this->offsets->bonus_range_table1;
return &this->r.pget<RangeTableEntry>(
base_offset + sizeof(RangeTableEntry) * index);
return &this->r.pget<RangeTableEntry>(base_offset + sizeof(RangeTableEntry) * index);
}
const parray<WeaponRandomSet::WeightTableEntry32, 3>*
@@ -724,19 +902,16 @@ WeaponRandomSet::get_special_mode_table(size_t index) const {
const WeaponRandomSet::RangeTableEntry*
WeaponRandomSet::get_standard_grind_range(size_t index) const {
return &this->r.pget<RangeTableEntry>(
this->offsets->standard_grind_range_table + sizeof(RangeTableEntry) * index);
return &this->r.pget<RangeTableEntry>(this->offsets->standard_grind_range_table + sizeof(RangeTableEntry) * index);
}
const WeaponRandomSet::RangeTableEntry*
WeaponRandomSet::get_favored_grind_range(size_t index) const {
return &this->r.pget<RangeTableEntry>(
this->offsets->favored_grind_range_table + sizeof(RangeTableEntry) * index);
return &this->r.pget<RangeTableEntry>(this->offsets->favored_grind_range_table + sizeof(RangeTableEntry) * index);
}
TekkerAdjustmentSet::TekkerAdjustmentSet(std::shared_ptr<const std::string> data)
: data(data),
r(*data) {
: data(data), r(*data) {
this->offsets = &this->r.pget<Offsets>(this->r.pget_u32b(this->r.size() - 0x10));
}
+11
View File
@@ -18,10 +18,16 @@ public:
Table(const phosg::JSON& json, Episode episode);
Table(const phosg::StringReader& r, bool big_endian, bool is_v3, Episode episode);
bool operator==(const Table& other) const = default;
bool operator!=(const Table& other) const = default;
template <typename IntT>
struct Range {
IntT min;
IntT max;
bool operator==(const Range& other) const = default;
bool operator!=(const Range& other) const = default;
} __attribute__((packed));
Episode episode;
@@ -50,6 +56,7 @@ public:
phosg::JSON json() const;
void print(FILE* stream) const;
void print_diff(FILE* stream, const Table& other) const;
private:
template <bool BE>
@@ -261,9 +268,13 @@ public:
check_struct_size(OffsetsBE, 0x54);
};
bool operator==(const CommonItemSet& other) const = default;
bool operator!=(const CommonItemSet& other) const = default;
std::shared_ptr<const Table> get_table(Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) const;
phosg::JSON json() const;
void print(FILE* stream) const;
void print_diff(FILE* stream, const CommonItemSet& other) const;
protected:
CommonItemSet() = default;
+3 -3
View File
@@ -184,7 +184,7 @@ void DownloadSession::send_93_9D_9E(bool extended) {
ret.access_key2 = ret.access_key;
ret.login_character_name.encode(this->character->disp.name.decode());
ret.client_config = this->client_config;
this->channel->send(0x9E, 0x01, &ret, extended ? sizeof(ret) : sizeof(C_Login_GC_9E));
this->channel->send(0x9E, 0x01, &ret, extended ? sizeof(ret) : sizeof(C_Login_PC_GC_9E));
} else if (this->version == Version::XB_V3) {
C_LoginExtended_XB_9E ret;
@@ -457,7 +457,7 @@ asio::awaitable<void> DownloadSession::on_message(Channel::Message& msg) {
case 0x1F:
case 0xA0:
case 0xA1: {
C_MenuSelection_10_Flag00 ret;
C_MenuSelectionBase_10 ret;
auto handle_command = [&]<typename CmdT>() {
const auto* items = check_size_vec_t<CmdT>(msg.data, msg.flag + 1);
@@ -764,7 +764,7 @@ void DownloadSession::send_next_request() {
this->log.info_f("Sending request {:016X}", this->current_request);
}
C_MenuSelection_10_Flag00 cmd;
C_MenuSelectionBase_10 cmd;
cmd.menu_id = (this->current_request >> 32) & 0xFFFFFFFF;
cmd.item_id = this->current_request & 0xFFFFFFFF;
this->channel->send(0x10, 0x00, cmd);
+27
View File
@@ -198,6 +198,33 @@ const vector<EnemyType>& enemy_types_for_rare_table_index(Episode episode, uint8
}
}
const vector<EnemyType>& enemy_types_for_battle_param_index(Episode episode, uint8_t bp_index) {
const auto& generate_table = +[](Episode episode) -> vector<vector<EnemyType>> {
vector<vector<EnemyType>> ret;
for (const auto& def : type_defs) {
if (def.valid_in_episode(episode) && (def.bp_index != 0xFF)) {
if (def.bp_index >= ret.size()) {
ret.resize(def.bp_index + 1);
}
ret[def.bp_index].emplace_back(def.type);
}
}
return ret;
};
static array<vector<vector<EnemyType>>, 5> data;
auto& ret = data.at(static_cast<size_t>(episode));
if (ret.empty()) {
ret = generate_table(episode);
}
try {
return ret.at(bp_index);
} catch (const out_of_range&) {
static const vector<EnemyType> empty_vec;
return empty_vec;
}
}
EnemyType EnemyTypeDefinition::rare_type(Episode episode, uint8_t event, uint8_t floor) const {
switch (this->type) {
case EnemyType::HILDEBEAR:
+1
View File
@@ -181,3 +181,4 @@ template <>
EnemyType phosg::enum_for_name<EnemyType>(const char* name);
const std::vector<EnemyType>& enemy_types_for_rare_table_index(Episode episode, uint8_t rt_index);
const std::vector<EnemyType>& enemy_types_for_battle_param_index(Episode episode, uint8_t bp_index);
+36 -28
View File
@@ -1177,6 +1177,19 @@ void PlayerConfig::encrypt(uint8_t basis) {
this->basis = basis;
}
bool PlayerConfig::card_count_checksums_correct() const {
for (size_t z = 0; z < this->card_count_checksums.size(); z++) {
uint16_t checksum_value = 0;
for (size_t w = 0; w < 20; w++) {
checksum_value += reinterpret_cast<const uint8_t*>(&this->card_counts)[z * 50 + w];
}
if (this->card_count_checksums[z] != checksum_value) {
return false;
}
}
return true;
}
PlayerConfigNTE::PlayerConfigNTE(const PlayerConfig& config)
: rank_text(config.rank_text),
unknown_a1(config.unknown_a1),
@@ -2613,8 +2626,8 @@ MapIndex::VersionedMap::VersionedMap(shared_ptr<const MapDefinition> map, uint8_
MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t language)
: language(language),
compressed_data(std::move(compressed_data)) {
string decompressed = prs_decompress(this->compressed_data);
compressed_data(make_shared<string>(std::move(compressed_data))) {
string decompressed = prs_decompress(*this->compressed_data);
if (decompressed.size() == sizeof(MapDefinitionTrial)) {
this->map = make_shared<MapDefinition>(*reinterpret_cast<const MapDefinitionTrial*>(decompressed.data()));
} else if (decompressed.size() == sizeof(MapDefinition)) {
@@ -2633,21 +2646,30 @@ shared_ptr<const MapDefinitionTrial> MapIndex::VersionedMap::trial() const {
return this->trial_map;
}
const std::string& MapIndex::VersionedMap::compressed(bool is_nte) const {
if (is_nte) {
if (this->compressed_trial_data.empty()) {
std::shared_ptr<const std::string> MapIndex::VersionedMap::compressed(bool trial) const {
if (trial) {
if (!this->compressed_data_trial) {
auto md = this->trial();
this->compressed_trial_data = prs_compress(md.get(), sizeof(*md));
this->compressed_data_trial = make_shared<string>(prs_compress(md.get(), sizeof(*md)));
}
return this->compressed_trial_data;
return this->compressed_data_trial;
} else {
if (this->compressed_data.empty()) {
this->compressed_data = prs_compress(this->map.get(), sizeof(*this->map));
if (!this->compressed_data) {
this->compressed_data = make_shared<string>(prs_compress(this->map.get(), sizeof(*this->map)));
}
return this->compressed_data;
}
}
std::shared_ptr<const std::string> MapIndex::VersionedMap::trial_download() const {
if (!this->download_data_trial) {
MapDefinitionTrial trial_map = *this->map;
trial_map.tag = 0x96;
this->download_data_trial = make_shared<string>(prs_compress(&trial_map, sizeof(trial_map)));
}
return this->download_data_trial;
}
MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version)
: map_number(initial_version->map->map_number),
initial_version(initial_version) {
@@ -2691,6 +2713,7 @@ shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(uint8_t language
}
MapIndex::MapIndex(const string& directory) {
map<uint32_t, shared_ptr<Map>> mutable_maps;
for (const auto& item : std::filesystem::directory_iterator(directory)) {
string filename = item.path().filename().string();
try {
@@ -2743,9 +2766,10 @@ MapIndex::MapIndex(const string& directory) {
}
string name = vm->map->name.decode(vm->language);
auto map_it = this->maps.find(vm->map->map_number);
if (map_it == this->maps.end()) {
map_it = this->maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first;
auto map_it = mutable_maps.find(vm->map->map_number);
if (map_it == mutable_maps.end()) {
map_it = mutable_maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first;
this->maps.emplace(vm->map->map_number, map_it->second);
static_game_data_log.debug_f("({}) Created Episode 3 map {:08X} {} ({}; {})",
filename,
vm->map->map_number,
@@ -2853,22 +2877,6 @@ const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language
return compressed_map_list;
}
shared_ptr<const MapIndex::Map> MapIndex::for_number(uint32_t id) const {
return this->maps.at(id);
}
shared_ptr<const MapIndex::Map> MapIndex::for_name(const string& name) const {
return this->maps_by_name.at(name);
}
set<uint32_t> MapIndex::all_numbers() const {
set<uint32_t> ret;
for (const auto& it : this->maps) {
ret.emplace(it.first);
}
return ret;
}
COMDeckIndex::COMDeckIndex(const string& filename) {
try {
auto json = phosg::JSON::parse(phosg::load_file(filename));
+25 -12
View File
@@ -605,9 +605,10 @@ struct CardDefinition {
// actions, and assist cards have 1 here.
/* 008F */ uint8_t cannot_attack;
/* 0090 */ uint8_t unused3;
// If cannot_drop is 0, this card can't appear in post-battle rewards. A
// value of 0 here also prevents the card from being used as a God Whim
// random assist.
// If cannot_drop is 1, this card can't appear in post-battle rewards and is
// considered unobtainable by players, so the game will remove it from the
// player's collection if they have any copies of it. A value of 1 here also
// prevents the card from being used as a God Whim random assist.
/* 0091 */ uint8_t cannot_drop;
// This criterion code specifies who can use the card, and when it can be
// used. This specifies which Hero-side SCs can use which items, for example,
@@ -864,7 +865,8 @@ struct PlayerConfig {
/* 0138:---- */ PlayerRecordsBattleBE unused_offline_records;
/* 0150:---- */ parray<uint8_t, 4> unknown_a4;
// The PlayerDataSegment structure begins here. In newserv, we combine this
// structure into PlayerConfig since the two are always used together.
// structure into PlayerConfig since the two are always used together on the
// server side.
/* 0154:0000 */ uint8_t is_encrypted;
/* 0155:0001 */ uint8_t basis;
/* 0156:0002 */ parray<uint8_t, 2> unused;
@@ -937,6 +939,8 @@ struct PlayerConfig {
void decrypt();
void encrypt(uint8_t basis);
bool card_count_checksums_correct() const;
} __packed_ws__(PlayerConfig, 0x2350);
struct PlayerConfigNTE {
@@ -1175,7 +1179,8 @@ struct OverlayState {
struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
// If tag is not 0x00000100, the game considers the map to be corrupt in
// offline mode and will delete it (if it's a download quest). The tag field
// doesn't seem to have any other use.
// doesn't seem to have any other use. In Trial Edition, download quests are
// expected to have 0x96 here instead.
/* 0000 */ be_uint32_t tag;
/* 0004 */ be_uint32_t map_number; // Must be unique across all maps
@@ -1593,12 +1598,14 @@ public:
VersionedMap(std::string&& compressed_data, uint8_t language);
std::shared_ptr<const MapDefinitionTrial> trial() const;
const std::string& compressed(bool is_nte) const;
std::shared_ptr<const std::string> compressed(bool trial) const;
std::shared_ptr<const std::string> trial_download() const;
private:
mutable std::shared_ptr<const MapDefinitionTrial> trial_map;
mutable std::string compressed_data;
mutable std::string compressed_trial_data;
mutable std::shared_ptr<std::string> compressed_data;
mutable std::shared_ptr<std::string> compressed_data_trial;
mutable std::shared_ptr<std::string> download_data_trial;
};
class Map {
@@ -1620,14 +1627,20 @@ public:
};
const std::string& get_compressed_list(size_t num_players, uint8_t language) const;
std::shared_ptr<const Map> for_number(uint32_t id) const;
std::shared_ptr<const Map> for_name(const std::string& name) const;
std::set<uint32_t> all_numbers() const;
inline std::shared_ptr<const Map> get(uint32_t id) const {
return this->maps.at(id);
}
inline std::shared_ptr<const Map> get(const std::string& name) const {
return this->maps_by_name.at(name);
}
inline const std::map<uint32_t, std::shared_ptr<const Map>>& all() const {
return this->maps;
}
private:
// The compressed map lists are generated on demand from the maps map below
mutable std::vector<std::array<std::string, 4>> compressed_map_lists;
std::map<uint32_t, std::shared_ptr<Map>> maps;
std::map<uint32_t, std::shared_ptr<const Map>> maps;
std::unordered_map<std::string, std::shared_ptr<Map>> maps_by_name;
};
+4 -4
View File
@@ -287,9 +287,9 @@ string Server::prepare_6xB6x41_map_definition(shared_ptr<const MapIndex::Map> ma
const auto& compressed = vm->compressed(is_nte);
phosg::StringWriter w;
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3);
w.put<G_MapData_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed.size(), 0});
w.write(compressed);
uint32_t subcommand_size = (compressed->size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3);
w.put<G_MapData_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed->size(), 0});
w.write(*compressed);
return std::move(w.str());
}
@@ -2588,7 +2588,7 @@ void Server::handle_CAx41_map_request(shared_ptr<Client>, const string& data) {
const auto& cmd = check_size_t<G_MapDataRequest_Ep3_CAx41>(data);
this->send_debug_command_received_message(cmd.header.subsubcommand, "MAP DATA");
if (!this->options.tournament || (this->options.tournament->get_map()->map_number == cmd.map_number)) {
this->last_chosen_map = this->options.map_index->for_number(cmd.map_number);
this->last_chosen_map = this->options.map_index->get(cmd.map_number);
this->send_6xB6x41_to_all_clients();
}
}
+2 -2
View File
@@ -18,7 +18,7 @@ Tournament::PlayerEntry::PlayerEntry(uint32_t account_id, const string& player_n
Tournament::PlayerEntry::PlayerEntry(shared_ptr<Client> c)
: account_id(c->login->account->account_id),
client(c),
player_name(c->character()->disp.name.decode(c->language())) {}
player_name(c->character_file()->disp.name.decode(c->language())) {}
Tournament::PlayerEntry::PlayerEntry(
shared_ptr<const COMDeckDefinition> com_deck)
@@ -357,7 +357,7 @@ void Tournament::init() {
bool is_registration_complete;
if (!this->source_json.is_null()) {
this->name = this->source_json.get_string("name");
this->map = this->map_index->for_number(this->source_json.get_int("map_number"));
this->map = this->map_index->get(this->source_json.get_int("map_number"));
this->rules = Rules(this->source_json.at("rules"));
this->flags = this->source_json.get_int("flags", 0x02);
if (this->source_json.get_bool("is_2v2", false)) {
+162 -84
View File
@@ -96,11 +96,9 @@ string CompiledFunctionCode::generate_client_command(
size_t suffix_size,
uint32_t override_relocations_offset) const {
if (this->arch == Architecture::POWERPC) {
return this->generate_client_command_t<true>(
label_writes, suffix_data, suffix_size, override_relocations_offset);
return this->generate_client_command_t<true>(label_writes, suffix_data, suffix_size, override_relocations_offset);
} else if ((this->arch == Architecture::X86) || (this->arch == Architecture::SH4)) {
return this->generate_client_command_t<false>(
label_writes, suffix_data, suffix_size, override_relocations_offset);
return this->generate_client_command_t<false>(label_writes, suffix_data, suffix_size, override_relocations_offset);
} else {
throw logic_error("invalid architecture");
}
@@ -110,18 +108,12 @@ bool CompiledFunctionCode::is_big_endian() const {
return this->arch == Architecture::POWERPC;
}
shared_ptr<CompiledFunctionCode> compile_function_code(
static vector<shared_ptr<CompiledFunctionCode>> compile_function_code(
CompiledFunctionCode::Architecture arch,
const string& function_directory,
const string& system_directory,
const string& name,
const string& text) {
auto ret = make_shared<CompiledFunctionCode>();
ret->arch = arch;
ret->short_name = name;
ret->index = 0;
ret->hide_from_patches_menu = false;
unordered_set<string> get_include_stack;
function<string(const string&)> get_include = [&](const string& name) -> string {
const char* arch_name_token;
@@ -177,56 +169,137 @@ shared_ptr<CompiledFunctionCode> compile_function_code(
throw runtime_error("data not found for include: " + name + " (from " + asm_filename + " or " + bin_filename + ")");
};
ResourceDASM::EmulatorBase::AssembleResult assembled;
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
assembled = ResourceDASM::PPC32Emulator::assemble(text, get_include);
} else if (arch == CompiledFunctionCode::Architecture::X86) {
assembled = ResourceDASM::X86Emulator::assemble(text, get_include);
} else if (arch == CompiledFunctionCode::Architecture::SH4) {
assembled = ResourceDASM::SH4Emulator::assemble(text, get_include);
} else {
throw runtime_error("invalid architecture");
}
ret->code = std::move(assembled.code);
ret->label_offsets = std::move(assembled.label_offsets);
for (const auto& it : assembled.metadata_keys) {
if (it.first == "hide_from_patches_menu") {
ret->hide_from_patches_menu = true;
} else if (it.first == "index") {
if (it.second.size() != 1) {
throw runtime_error("invalid index value in .meta directive");
// Handle VERS tokens
vector<uint32_t> specific_versions;
auto lines = phosg::split(text, '\n');
for (auto& line : lines) {
if (line.starts_with(".versions ")) {
if (!specific_versions.empty()) {
throw std::runtime_error("multiple .versions directives in file");
}
ret->index = it.second[0];
} else if (it.first == "name") {
ret->long_name = it.second;
} else if (it.first == "description") {
ret->description = it.second;
} else {
throw runtime_error("unknown metadata key: " + it.first);
for (auto& vers_token : phosg::split(line.substr(10), ' ')) {
phosg::strip_whitespace(vers_token);
if (vers_token.empty()) {
continue;
}
if (vers_token.size() != 4) {
throw std::runtime_error("invalid token in .version directive: " + vers_token);
}
specific_versions.emplace_back(*reinterpret_cast<const be_uint32_t*>(vers_token.data()));
}
line.clear();
}
}
set<uint32_t> reloc_indexes;
for (const auto& it : ret->label_offsets) {
if (it.first.starts_with("reloc")) {
reloc_indexes.emplace(it.second / 4);
// Preprocess <VERS> tokens in the text if a .versions directive was given
vector<string> version_texts;
if (specific_versions.empty()) {
specific_versions.emplace_back(0);
version_texts.emplace_back(text);
} else {
vector<deque<string>> version_lines;
version_lines.resize(specific_versions.size());
size_t line_num = 1;
for (const auto& line : lines) {
size_t vers_offset = line.find("<VERS ");
if (vers_offset == string::npos) {
for (auto& lines : version_lines) {
lines.emplace_back(line);
}
} else {
for (size_t vers_index = 0; vers_index < specific_versions.size(); vers_index++) {
string version_line = line;
size_t vers_offset = line.find("<VERS ");
while (vers_offset != string::npos) {
size_t end_offset = version_line.find('>', vers_offset + 6);
if (end_offset == string::npos) {
throw runtime_error(std::format("(line {}) unterminated <VERS> replacement", line_num));
}
auto tokens = phosg::split(version_line.substr(vers_offset + 6, end_offset - vers_offset - 6), ' ');
if (tokens.size() != specific_versions.size()) {
throw runtime_error(std::format("(line {}) invalid <VERS> replacement", line_num));
}
version_line = version_line.substr(0, vers_offset) + tokens.at(vers_index) + version_line.substr(end_offset + 1);
vers_offset = version_line.find("<VERS ");
}
version_lines[vers_index].emplace_back(version_line);
}
}
line_num++;
}
for (const auto& lines : version_lines) {
version_texts.emplace_back(phosg::join(lines, "\n"));
}
}
try {
ret->entrypoint_offset_offset = ret->label_offsets.at("entry_ptr");
} catch (const out_of_range&) {
throw runtime_error("code does not contain entry_ptr label");
}
vector<shared_ptr<CompiledFunctionCode>> ret;
for (size_t vers_index = 0; vers_index < specific_versions.size(); vers_index++) {
uint32_t specific_version = specific_versions[vers_index];
const auto& version_text = version_texts.at(vers_index);
uint32_t prev_index = 0;
for (const auto& it : reloc_indexes) {
uint32_t delta = it - prev_index;
if (delta > 0xFFFF) {
throw runtime_error("relocation delta too far away");
try {
ResourceDASM::EmulatorBase::AssembleResult assembled;
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
assembled = ResourceDASM::PPC32Emulator::assemble(version_text, get_include);
} else if (arch == CompiledFunctionCode::Architecture::X86) {
assembled = ResourceDASM::X86Emulator::assemble(version_text, get_include);
} else if (arch == CompiledFunctionCode::Architecture::SH4) {
assembled = ResourceDASM::SH4Emulator::assemble(version_text, get_include);
} else {
throw runtime_error("invalid architecture");
}
auto compiled = ret.emplace_back(make_shared<CompiledFunctionCode>());
compiled->arch = arch;
compiled->short_name = name;
compiled->specific_version = specific_version;
compiled->code = std::move(assembled.code);
compiled->label_offsets = std::move(assembled.label_offsets);
for (const auto& it : assembled.metadata_keys) {
if (it.first == "hide_from_patches_menu") {
compiled->hide_from_patches_menu = true;
} else if (it.first == "name") {
compiled->long_name = it.second;
} else if (it.first == "description") {
compiled->description = it.second;
} else if (it.first == "client_flag") {
compiled->client_flag = stoull(it.second, nullptr, 0);
} else {
throw runtime_error("unknown metadata key: " + it.first);
}
}
set<uint32_t> reloc_indexes;
for (const auto& it : compiled->label_offsets) {
if (it.first.starts_with("reloc")) {
reloc_indexes.emplace(it.second / 4);
}
}
try {
compiled->entrypoint_offset_offset = compiled->label_offsets.at("entry_ptr");
} catch (const out_of_range&) {
throw runtime_error("code does not contain entry_ptr label");
}
uint32_t prev_index = 0;
for (const auto& it : reloc_indexes) {
uint32_t delta = it - prev_index;
if (delta > 0xFFFF) {
throw runtime_error("relocation delta too far away");
}
compiled->relocation_deltas.emplace_back(delta);
prev_index = it;
}
} catch (const exception& e) {
string version_str = specific_version ? (" (" + str_for_specific_version(specific_version) + ")") : "";
function_compiler_log.warning_f("Failed to compile function {}{}: {}", name, version_str, e.what());
}
ret->relocation_deltas.emplace_back(delta);
prev_index = it;
}
return ret;
@@ -239,21 +312,16 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
for (const auto& item : std::filesystem::directory_iterator(directory)) {
string subdir_name = item.path().filename().string();
string subdir_path = directory.ends_with("/") ? (directory + subdir_name) : (directory + "/" + subdir_name);
if (!std::filesystem::is_directory(subdir_path)) {
function_compiler_log.warning_f("Skipping {} (not a directory)", subdir_name);
continue;
}
for (const auto& item : std::filesystem::directory_iterator(subdir_path)) {
string filename = item.path().filename().string();
auto add_file = [&](string filename) -> void {
try {
if (!filename.ends_with(".s")) {
continue;
return;
}
string name = filename.substr(0, filename.size() - 2);
if (name.ends_with(".inc")) {
continue;
return;
}
bool is_patch = name.ends_with(".patch");
@@ -299,50 +367,60 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
string path = subdir_path + "/" + filename;
string text = phosg::load_file(path);
auto code = compile_function_code(arch, subdir_path, system_dir_path, name, text);
if (code->index != 0) {
if (!this->index_to_function.emplace(code->index, code).second) {
throw runtime_error(std::format(
"duplicate function index: {:08X}", code->index));
for (auto code : compile_function_code(arch, subdir_path, system_dir_path, name, text)) {
if (code->specific_version == 0) {
code->specific_version = specific_version;
}
code->source_path = path;
code->short_name = short_name;
this->name_to_function.emplace(name, code);
if (is_patch) {
code->menu_item_id = next_menu_item_id++;
this->menu_item_id_and_specific_version_to_patch_function.emplace(
static_cast<uint64_t>(code->menu_item_id) << 32 | code->specific_version, code);
this->name_and_specific_version_to_patch_function.emplace(
std::format("{}-{:08X}", code->short_name, code->specific_version), code);
}
}
code->specific_version = specific_version;
code->source_path = path;
code->short_name = short_name;
this->name_to_function.emplace(name, code);
if (is_patch) {
code->menu_item_id = next_menu_item_id++;
this->menu_item_id_and_specific_version_to_patch_function.emplace(
static_cast<uint64_t>(code->menu_item_id) << 32 | specific_version, code);
this->name_and_specific_version_to_patch_function.emplace(
std::format("{}-{:08X}", short_name, specific_version), code);
}
string index_prefix = code->index ? std::format("{:02X} => ", code->index) : "";
string patch_prefix = is_patch ? std::format("[{:08X}/{:08X}] ", code->menu_item_id, code->specific_version) : "";
function_compiler_log.debug_f("Compiled function {}{}{} ({})",
index_prefix, patch_prefix, name, name_for_architecture(code->arch));
string patch_prefix = is_patch ? std::format("[{:08X}] ", code->menu_item_id) : "";
function_compiler_log.debug_f("Compiled function {}{} ({}; {})",
patch_prefix, name, str_for_specific_version(code->specific_version), name_for_architecture(code->arch));
}
} catch (const exception& e) {
function_compiler_log.warning_f("Failed to compile function {}: {}", filename, e.what());
}
};
if (std::filesystem::is_regular_file(subdir_path)) {
add_file(subdir_path);
} else if (std::filesystem::is_directory(subdir_path)) {
for (const auto& item : std::filesystem::directory_iterator(subdir_path)) {
string filename = item.path().filename().string();
add_file(filename);
}
} else {
function_compiler_log.warning_f("Skipping {} (unknown file type)", subdir_name);
continue;
}
}
}
shared_ptr<const Menu> FunctionCodeIndex::patch_switches_menu(
uint32_t specific_version, const std::unordered_set<std::string>& auto_patches_enabled) const {
uint32_t specific_version,
const std::unordered_set<std::string>& server_auto_patches_enabled,
const std::unordered_set<std::string>& client_auto_patches_enabled) const {
auto suffix = std::format("-{:08X}", specific_version);
auto ret = make_shared<Menu>(MenuID::PATCH_SWITCHES, "Patches");
ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0);
for (const auto& it : this->name_and_specific_version_to_patch_function) {
const auto& fn = it.second;
if (fn->hide_from_patches_menu || !it.first.ends_with(suffix)) {
if (fn->hide_from_patches_menu || !it.first.ends_with(suffix) || server_auto_patches_enabled.count(fn->short_name)) {
continue;
}
string name;
name.push_back(auto_patches_enabled.count(fn->short_name) ? '*' : '-');
name.push_back(client_auto_patches_enabled.count(fn->short_name) ? '*' : '-');
name += fn->long_name.empty() ? fn->short_name : fn->long_name;
ret->items.emplace_back(fn->menu_item_id, name, fn->description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL_RUNS_CODE);
}
+9 -12
View File
@@ -25,15 +25,15 @@ struct CompiledFunctionCode {
std::string code;
std::vector<uint16_t> relocation_deltas;
std::unordered_map<std::string, uint32_t> label_offsets;
uint32_t entrypoint_offset_offset;
uint32_t entrypoint_offset_offset = 0;
std::string source_path; // Path to source file from newserv root
std::string short_name; // Based on filename
std::string long_name; // From .meta name directive
std::string description; // From .meta description directive
uint8_t index; // 0 = unused (not registered in index_to_function)
uint32_t menu_item_id;
bool hide_from_patches_menu;
uint32_t specific_version;
uint64_t client_flag = 0; // From .meta client_flag directive
uint32_t menu_item_id = 0;
bool hide_from_patches_menu = false;
uint32_t specific_version = 0; // 0 = not a client-selectable patch
bool is_big_endian() const;
@@ -52,12 +52,6 @@ struct CompiledFunctionCode {
const char* name_for_architecture(CompiledFunctionCode::Architecture arch);
std::shared_ptr<CompiledFunctionCode> compile_function_code(
CompiledFunctionCode::Architecture arch,
const std::string& directory,
const std::string& name,
const std::string& text);
struct FunctionCodeIndex {
FunctionCodeIndex() = default;
explicit FunctionCodeIndex(const std::string& directory);
@@ -68,7 +62,10 @@ struct FunctionCodeIndex {
// Key here is e.g. "PATCHNAME-SPECIFICVERSION", with the latter in hex
std::map<std::string, std::shared_ptr<CompiledFunctionCode>> name_and_specific_version_to_patch_function;
std::shared_ptr<const Menu> patch_switches_menu(uint32_t specific_version, const std::unordered_set<std::string>& auto_patches_enabled) const;
std::shared_ptr<const Menu> patch_switches_menu(
uint32_t specific_version,
const std::unordered_set<std::string>& server_auto_patches_enabled,
const std::unordered_set<std::string>& client_auto_patches_enabled) const;
bool patch_menu_empty(uint32_t specific_version) const;
std::shared_ptr<const CompiledFunctionCode> get_patch(const std::string& name, uint32_t specific_version) const;
+1 -1
View File
@@ -96,7 +96,7 @@ vector<shared_ptr<Client>> GameServer::get_clients_by_identifier(const string& i
continue;
}
auto p = c->character(false, false);
auto p = c->character_file(false, false);
if (p && p->disp.name.eq(ident, p->inventory.language)) {
results.emplace_back(c);
continue;
+37 -24
View File
@@ -169,7 +169,7 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
if (c->version() == Version::BB_V4) {
ret->emplace("BBCharacterIndex", c->bb_character_index);
}
auto p = c->character(false, false);
auto p = c->character_file(false, false);
if (p) {
if (!is_ep3(c->version())) {
if (c->version() != Version::DC_NTE) {
@@ -193,7 +193,7 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
{"ItemID", item.data.id.load()},
});
if (item_name_index) {
item_dict.emplace("Description", item_name_index->describe_item(item.data, false));
item_dict.emplace("Description", item_name_index->describe_item(item.data));
}
items_json.emplace_back(std::move(item_dict));
}
@@ -328,13 +328,13 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
{"LobbyPlayers", std::move(lobby_players_json)},
});
switch (ses->drop_mode) {
case ProxySession::DropMode::DISABLED:
case ProxyDropMode::DISABLED:
ses_json.emplace("DropMode", "none");
break;
case ProxySession::DropMode::PASSTHROUGH:
case ProxyDropMode::PASSTHROUGH:
ses_json.emplace("DropMode", "default");
break;
case ProxySession::DropMode::INTERCEPT:
case ProxyDropMode::INTERCEPT:
ses_json.emplace("DropMode", "proxy");
break;
}
@@ -386,19 +386,19 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_lobby_json(
ret->emplace("EXPShareMultiplier", l->exp_share_multiplier);
ret->emplace("AllowedDropModes", l->allowed_drop_modes);
switch (l->drop_mode) {
case Lobby::DropMode::DISABLED:
case ServerDropMode::DISABLED:
ret->emplace("DropMode", "none");
break;
case Lobby::DropMode::CLIENT:
case ServerDropMode::CLIENT:
ret->emplace("DropMode", "client");
break;
case Lobby::DropMode::SERVER_SHARED:
case ServerDropMode::SERVER_SHARED:
ret->emplace("DropMode", "shared");
break;
case Lobby::DropMode::SERVER_PRIVATE:
case ServerDropMode::SERVER_PRIVATE:
ret->emplace("DropMode", "private");
break;
case Lobby::DropMode::SERVER_DUPLICATE:
case ServerDropMode::SERVER_DUPLICATE:
ret->emplace("DropMode", "duplicate");
break;
}
@@ -431,7 +431,7 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_lobby_json(
{"ItemID", item->data.id.load()},
});
if (item_name_index) {
item_dict.emplace("Description", item_name_index->describe_item(item->data, false));
item_dict.emplace("Description", item_name_index->describe_item(item->data));
}
floor_items_json.emplace_back(std::move(item_dict));
}
@@ -596,7 +596,7 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_lobbies_json() const {
std::shared_ptr<phosg::JSON> HTTPServer::generate_summary_json() const {
auto clients_json = phosg::JSON::list();
for (const auto& c : this->state->game_server->all_clients()) {
auto p = c->character(false, false);
auto p = c->character_file(false, false);
auto l = c->lobby.lock();
clients_json.emplace_back(phosg::JSON::dict({
{"ID", c->id},
@@ -669,12 +669,23 @@ asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_ep3_cards_jso
});
}
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_common_tables_json() const {
auto v2_table = this->state->common_item_set_v2;
auto v3_v4_table = this->state->common_item_set_v3_v4;
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> {
return make_shared<phosg::JSON>(phosg::JSON::dict({{"v1_v2", v2_table->json()}, {"v3_v4", v3_v4_table->json()}}));
});
std::shared_ptr<phosg::JSON> HTTPServer::generate_common_table_list_json() const {
auto ret = make_shared<phosg::JSON>(phosg::JSON::list());
for (const auto& it : this->state->common_item_sets) {
ret->emplace_back(it.first);
}
return ret;
}
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_common_table_json(const std::string& table_name) const {
try {
const auto& table = this->state->common_item_sets.at(table_name);
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> {
return make_shared<phosg::JSON>(table->json());
});
} catch (const out_of_range&) {
throw HTTPError(404, "Table does not exist");
}
}
std::shared_ptr<phosg::JSON> HTTPServer::generate_rare_table_list_json() const {
@@ -706,10 +717,9 @@ asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_rare_table_js
}
}
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_quest_list_json(
std::shared_ptr<const QuestIndex> quest_index) {
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_quest_list_json() {
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> {
return make_shared<phosg::JSON>(quest_index->json());
return make_shared<phosg::JSON>(this->state->quest_index->json());
});
}
@@ -794,16 +804,19 @@ asio::awaitable<std::unique_ptr<HTTPResponse>> HTTPServer::handle_request(shared
ret = co_await this->generate_ep3_cards_json(true);
} else if (req.path == "/y/data/common-tables") {
this->require_GET(req);
ret = co_await this->generate_common_tables_json();
ret = this->generate_common_table_list_json();
} else if (req.path.starts_with("/y/data/common-tables/")) {
this->require_GET(req);
ret = co_await this->generate_common_table_json(req.path.substr(22));
} else if (req.path == "/y/data/rare-tables") {
this->require_GET(req);
ret = this->generate_rare_table_list_json();
} else if (!req.path.starts_with("/y/data/rare-tables/")) {
} else if (req.path.starts_with("/y/data/rare-tables/")) {
this->require_GET(req);
ret = co_await this->generate_rare_table_json(req.path.substr(20));
} else if (req.path == "/y/data/quests") {
this->require_GET(req);
ret = co_await this->generate_quest_list_json(this->state->quest_index(Version::GC_V3));
ret = co_await this->generate_quest_list_json();
} else if (req.path == "/y/data/config") {
this->require_GET(req);
ret = this->state->config_json;
+3 -2
View File
@@ -37,10 +37,11 @@ protected:
std::shared_ptr<phosg::JSON> generate_all_json() const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_ep3_cards_json(bool trial) const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_common_tables_json() const;
std::shared_ptr<phosg::JSON> generate_common_table_list_json() const;
std::shared_ptr<phosg::JSON> generate_rare_table_list_json() const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_common_table_json(const std::string& table_name) const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_rare_table_json(const std::string& table_name) const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_quest_list_json(std::shared_ptr<const QuestIndex> q);
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_quest_list_json();
void require_GET(const HTTPRequest& req);
phosg::JSON require_POST(const HTTPRequest& req);
+10 -11
View File
@@ -35,7 +35,7 @@ struct GVRHeader {
be_uint16_t height;
} __packed_ws__(GVRHeader, 0x10);
string encode_gvm(const phosg::Image& img, GVRDataFormat data_format, const string& internal_name, uint32_t global_index) {
string encode_gvm(const phosg::ImageRGBA8888N& img, GVRDataFormat data_format, const string& internal_name, uint32_t global_index) {
int8_t dimensions_field = -2;
{
size_t h = img.get_height();
@@ -90,17 +90,16 @@ string encode_gvm(const phosg::Image& img, GVRDataFormat data_format, const stri
for (size_t x = 0; x < img.get_width(); x += 4) {
for (size_t yy = 0; yy < 4; yy++) {
for (size_t xx = 0; xx < 4; xx++) {
uint64_t a, r, g, b;
img.read_pixel(x + xx, y + yy, &r, &g, &b, &a);
uint32_t c = img.read(x + xx, y + yy);
switch (data_format) {
case GVRDataFormat::RGB565:
w.put_u16b(encode_rgb565(r, g, b));
w.put_u16b(phosg::rgb565_for_rgba8888(c));
break;
case GVRDataFormat::RGB5A3:
w.put_u16b(encode_rgb5a3(r, g, b, a));
w.put_u16b(encode_rgb5a3(c));
break;
case GVRDataFormat::ARGB8888:
w.put_u32b(encode_argb8888(r, g, b, a));
w.put_u32b(phosg::argb8888_for_rgba8888(c));
break;
default:
throw logic_error("cannot encode pixel format");
@@ -115,15 +114,15 @@ string encode_gvm(const phosg::Image& img, GVRDataFormat data_format, const stri
static const array<uint32_t, 4> fon_colors = {0x000000FF, 0x555555FF, 0xAAAAAAFF, 0xFFFFFFFF};
phosg::Image decode_fon(const string& data, size_t width) {
phosg::ImageRGB888 decode_fon(const string& data, size_t width) {
size_t num_pixels = data.size() * 4;
size_t height = num_pixels / width;
phosg::Image ret(width, height);
phosg::ImageRGB888 ret(width, height);
phosg::BitReader r(data);
for (size_t y = 0; y < height; y++) {
for (size_t x = 0; x < width; x++) {
ret.write_pixel(x, y, fon_colors[r.read(2)]);
ret.write(x, y, fon_colors[r.read(2)]);
}
}
return ret;
@@ -133,11 +132,11 @@ constexpr size_t uabs(size_t a, size_t b) {
return (a > b) ? (a - b) : (b - a);
}
string encode_fon(const phosg::Image& img) {
string encode_fon(const phosg::ImageRGB888& img) {
phosg::BitWriter w;
for (size_t y = 0; y < img.get_height(); y++) {
for (size_t x = 0; x < img.get_width(); x++) {
uint32_t color = img.read_pixel(x, y);
uint32_t color = img.read(x, y);
size_t result_delta = 0x400;
size_t result_index = 0;
+20 -34
View File
@@ -19,43 +19,29 @@ enum class GVRDataFormat : uint8_t {
DXT1 = 0x0E,
};
std::string encode_gvm(const phosg::Image& img, GVRDataFormat data_format, const std::string& internal_name, uint32_t global_index);
phosg::Image decode_fon(const std::string& data, size_t width);
std::string encode_fon(const phosg::Image& img);
std::string encode_gvm(
const phosg::ImageRGBA8888N& img, GVRDataFormat data_format, const std::string& internal_name, uint32_t global_index);
phosg::ImageRGB888 decode_fon(const std::string& data, size_t width);
std::string encode_fon(const phosg::ImageRGB888& img);
constexpr uint16_t encode_rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((r << 8) & 0xF800) | ((g << 3) & 0x07E0) | ((b >> 3) & 0x001F);
}
constexpr uint16_t encode_rgb5a3(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
if ((a & 0xE0) == 0xE0) {
return 0x8000 | ((r << 7) & 0x7C00) | ((g << 2) & 0x03E0) | ((b >> 3) & 0x001F);
constexpr uint16_t encode_rgb5a3(uint32_t c) {
if ((phosg::get_a(c) & 0xE0) == 0xE0) {
return 0x8000 | ((phosg::get_r(c) << 7) & 0x7C00) | ((phosg::get_g(c) << 2) & 0x03E0) | ((phosg::get_b(c) >> 3) & 0x001F);
} else {
return ((a << 7) & 0x7000) | ((r << 4) & 0x0F00) | (g & 0x00F0) | ((b >> 4) & 0x000F);
return ((phosg::get_a(c) << 7) & 0x7000) | ((phosg::get_r(c) << 4) & 0x0F00) | (phosg::get_g(c) & 0x00F0) | ((phosg::get_b(c) >> 4) & 0x000F);
}
}
constexpr uint32_t encode_argb8888(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
return (a << 24) | (r << 16) | (g << 8) | b;
}
constexpr uint16_t encode_argb8888_to_argb1555(uint32_t argb8888) {
// In: aaaaaaaarrrrrrrrggggggggbbbbbbbb
// Out: arrrrrgggggbbbbb
return ((argb8888 >> 9) & 0x7C00) | ((argb8888 >> 6) & 0x03E0) | ((argb8888 >> 3) & 0x001F) | ((argb8888 >> 16) & 0x8000);
}
constexpr uint16_t encode_rgba8888_to_argb1555(uint32_t rgba8888) {
// In: rrrrrrrrggggggggbbbbbbbbaaaaaaaa
// Out: arrrrrgggggbbbbb
return ((rgba8888 >> 17) & 0x7C00) | ((rgba8888 >> 14) & 0x03E0) | ((rgba8888 >> 11) & 0x001F) | ((rgba8888 << 8) & 0x8000);
}
constexpr uint32_t decode_argb1555_to_rgba8888(uint16_t argb1555) {
// In: arrrrrgggggbbbbb
// Out: rrrrrrrrggggggggbbbbbbbbaaaaaaaa
return ((argb1555 << 17) & 0xF8000000) | ((argb1555 << 12) & 0x07000000) |
((argb1555 << 14) & 0x00F80000) | ((argb1555 << 9) & 0x00070000) |
((argb1555 << 11) & 0x0000F800) | ((argb1555 << 6) & 0x00000700) |
((argb1555 & 0x8000) ? 0x000000FF : 0x00000000);
template <phosg::PixelFormat Format>
bool has_any_transparent_pixels(const phosg::Image<Format>& img) {
if constexpr (phosg::Image<Format>::HAS_ALPHA) {
for (size_t y = 0; y < img.get_height(); y++) {
for (size_t x = 0; x < img.get_height(); x++) {
if (phosg::get_a(img.read(x, y)) != 0xFF) {
return true;
}
}
}
}
return false;
}
-1
View File
@@ -9,7 +9,6 @@
#include <vector>
#include "PlayerSubordinates.hh"
#include "QuestScript.hh"
#include "StaticGameData.hh"
#include "TeamIndex.hh"
+1 -1
View File
@@ -342,7 +342,7 @@ bool ItemCreator::should_allow_meseta_drops() const {
ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_type, uint8_t area_norm) {
ItemData item;
if (this->are_rare_drops_allowed() && (enemy_type > 0) && (enemy_type < 0x58)) {
if (this->are_rare_drops_allowed() && (enemy_type > 0) && (enemy_type < 0x64)) {
// Note: In the original implementation, enemies can only have one possible
// rare drop. In our implementation, they can have multiple rare drops if
// JSONRareItemSet is used (the other RareItemSet implementations never
+1 -1
View File
@@ -96,7 +96,7 @@ private:
// [0x0B] - unit modifiers
// [0x0C] - common armor DFP bonuses
// [0x0D] - common armor EVP bonuses
// [0x0E] - apparently unused
// [0x0E] - unit stars
// [0x0F] - which common weapon special to generate
// [0x10] - apparently unused
std::shared_ptr<RandomGenerator> rand_crypt;
+8 -3
View File
@@ -254,9 +254,14 @@ size_t ItemData::max_stack_size(const StackLimits& limits) const {
return limits.get(this->data1[0], this->data1[1]);
}
void ItemData::enforce_min_stack_size(const StackLimits& limits) {
if (this->stack_size(limits) == 0) {
this->data1[5] = 1;
void ItemData::enforce_stack_size_limits(const StackLimits& limits) {
if (this->data1[0] == 0x03) {
size_t max_stack_size = this->max_stack_size(limits);
if (max_stack_size > 1) {
this->data1[5] = std::clamp<uint8_t>(this->data1[5], 1, max_stack_size);
} else {
this->data1[5] = 0;
}
}
}
+9 -8
View File
@@ -82,14 +82,14 @@ struct ItemData {
// QUICK ITEM FORMAT REFERENCE
// data1/0 data1/4 data1/8 data2
// Weapon: 00ZZZZGG SSNNAABB AABBAABB 00000000
// Armor: 0101ZZ00 FFTTDDDD EEEE0000 00000000
// Shield: 0102ZZ00 FFTTDDDD EEEE0000 00000000
// Unit: 0103ZZ00 FF00RRRR 00000000 00000000
// Armor: 0101ZZ00 FFTTDDDD EEEEXXXX 00000000
// Shield: 0102ZZ00 FFTTDDDD EEEEXXXX 00000000
// Unit: 0103ZZ00 FF00RRRR 0000XXXX 00000000
// Mag: 02ZZLLWW HHHHIIII JJJJKKKK YYQQPPVV
// Tool: 03ZZZZUU 00CC0000 00000000 00000000
// Tool: 03ZZZZUU 00CC0000 0000XXXX 00000000
// Meseta: 04000000 00000000 00000000 MMMMMMMM
// A = attribute type (for S-ranks, custom name)
// B = attribute amount (for S-ranks, custom name)
// A = attribute type (for S-ranks, custom name; last pair is kill count for some weapons)
// B = attribute amount (for S-ranks, custom name; last pair is kill count for some weapons)
// C = stack size (for tools)
// D = DEF bonus
// E = EVP bonus
@@ -110,6 +110,7 @@ struct ItemData {
// U = tool flags (40=present; unused if item is stackable)
// V = mag color
// W = photon blasts
// X = kill count (big-endian; high bit always set)
// Y = mag synchro
// Z = item ID
// Note: PSO GC erroneously byteswaps data2 even when the item is a mag. This
@@ -118,7 +119,7 @@ struct ItemData {
// sending where needed.
// Related note: PSO V2 has an annoyingly complicated format for mags that
// doesn't match the above table. We decode this upon receipt and encode it
// imemdiately before sending when interacting with V2 clients; see the
// immediately before sending when interacting with V2 clients; see the
// implementation of decode_for_version() for details.
union {
@@ -163,7 +164,7 @@ struct ItemData {
bool is_stackable(const StackLimits& limits) const;
size_t stack_size(const StackLimits& limits) const;
size_t max_stack_size(const StackLimits& limits) const;
void enforce_min_stack_size(const StackLimits& limits);
void enforce_stack_size_limits(const StackLimits& limits);
static bool is_common_consumable(uint32_t primary_identifier);
bool is_common_consumable() const;
+89 -46
View File
@@ -97,7 +97,10 @@ const array<const char*, 0x11> name_for_s_rank_special = {
"King\'s",
};
std::string ItemNameIndex::describe_item(const ItemData& item, bool include_color_escapes, bool hide_mag_stats) const {
std::string ItemNameIndex::describe_item(const ItemData& item, uint8_t flags) const {
bool include_color_escapes = flags & ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES;
bool name_only = flags & ItemNameIndex::Flag::NAME_ONLY;
if (item.data1[0] == 0x04) {
return std::format("{}{} Meseta", include_color_escapes ? "$C7" : "", item.data2d);
}
@@ -108,13 +111,14 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
bool is_unidentified = false;
if ((item.data1[0] == 0x00) && (item.data1[4] != 0x00) && !item.is_s_rank_weapon()) {
is_unidentified = item.data1[4] & 0x80;
bool is_present = item.data1[4] & 0x40;
uint8_t special_id = item.data1[4] & 0x3F;
if (is_present) {
ret_tokens.emplace_back("Wrapped");
}
if (is_unidentified) {
ret_tokens.emplace_back("????");
if (!name_only) {
if (item.data1[4] & 0x40) {
ret_tokens.emplace_back("Wrapped");
}
if (is_unidentified) {
ret_tokens.emplace_back("????");
}
}
if (special_id) {
try {
@@ -124,7 +128,7 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
}
}
}
if ((item.data1[0] == 0x00) && (item.data1[2] != 0x00) && item.is_s_rank_weapon()) {
if (!name_only && (item.data1[0] == 0x00) && (item.data1[2] != 0x00) && item.is_s_rank_weapon()) {
try {
ret_tokens.emplace_back(name_for_s_rank_special.at(item.data1[2]));
} catch (const out_of_range&) {
@@ -135,9 +139,10 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
// Armors, shields, and units (0x01) can be wrapped, as can mags (0x02) and
// non-stackable tools (0x03). However, each of these item classes has its
// flags in a different location.
if (((item.data1[0] == 0x01) && (item.data1[4] & 0x40)) ||
((item.data1[0] == 0x02) && (item.data2[2] & 0x40)) ||
((item.data1[0] == 0x03) && !item.is_stackable(*this->limits) && (item.data1[3] & 0x40))) {
if (!name_only &&
(((item.data1[0] == 0x01) && (item.data1[4] & 0x40)) ||
((item.data1[0] == 0x02) && (item.data2[2] & 0x40)) ||
((item.data1[0] == 0x03) && !item.is_stackable(*this->limits) && (item.data1[3] & 0x40)))) {
ret_tokens.emplace_back("Wrapped");
}
@@ -168,7 +173,7 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
if (item.data1[0] == 0x00) {
// For weapons, add the grind and bonuses, or S-rank name if applicable
if (item.data1[3] > 0) {
if (!name_only && item.data1[3] > 0) {
ret_tokens.emplace_back(std::format("+{}", item.data1[3]));
}
@@ -210,18 +215,22 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
}
}
} else { // Not S-rank (extended name bits not set)
} else if (!name_only) { // Not S-rank (extended name bits not set)
size_t num_bonuses = 3;
if (item.data1[10] & 0x80) {
ret_tokens.emplace_back(std::format("K:{}", item.get_kill_count()));
num_bonuses = 2;
}
parray<int8_t, 5> bonuses(0);
for (size_t x = 0; x < 3; x++) {
for (size_t x = 0; x < num_bonuses; x++) {
uint8_t which = item.data1[6 + 2 * x];
uint8_t value = item.data1[7 + 2 * x];
if (which == 0) {
continue;
}
if (which & 0x80) {
uint16_t kill_count = ((which << 8) & 0x7F00) | (value & 0xFF);
ret_tokens.emplace_back(std::format("K:{}", kill_count));
} else if (which > 5) {
if (which > 5) {
ret_tokens.emplace_back(std::format("!PC:{:02X}{:02X}", which, value));
} else {
bonuses[which - 1] = value;
@@ -258,7 +267,11 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
ret_tokens.emplace_back(std::format("!MD:{:04X}", modifier));
}
} else { // Armor/shields
if (!name_only && (item.data1[10] & 0x80)) {
ret_tokens.emplace_back(std::format("K:{}", item.get_kill_count()));
}
} else if (!name_only) { // Armor/shields
if (item.data1[5] > 0) {
if (item.data1[5] == 1) {
ret_tokens.emplace_back("(1 slot)");
@@ -276,7 +289,7 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
}
}
} else if (!hide_mag_stats && (item.data1[0] == 0x02)) {
} else if (!name_only && (item.data1[0] == 0x02)) {
// For mags, add tons of info
ret_tokens.emplace_back(std::format("LV{}", item.data1[2]));
@@ -383,7 +396,7 @@ ItemData ItemNameIndex::parse_item_description(const std::string& desc) const {
}
}
}
ret.enforce_min_stack_size(*this->limits);
ret.enforce_stack_size_limits(*this->limits);
return ret;
}
@@ -522,6 +535,9 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
ret.data1w[4] = phosg::bswap16(0x8000 | (char_indexes[4] & 0x1F) | ((char_indexes[3] & 0x1F) << 5) | ((char_indexes[2] & 0x1F) << 10));
ret.data1w[5] = phosg::bswap16(0x8000 | (char_indexes[7] & 0x1F) | ((char_indexes[6] & 0x1F) << 5) | ((char_indexes[5] & 0x1F) << 10));
} else if (token.starts_with("k:")) {
ret.set_kill_count(stoul(token.substr(2), nullptr, 0));
} else {
auto p_tokens = phosg::split(token, '/');
if (p_tokens.size() > 5) {
@@ -662,7 +678,8 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
void ItemNameIndex::print_table(FILE* stream) const {
auto pmt = this->item_parameter_table;
phosg::fwrite_fmt(stream, "WEAPON => ---ID--- TYPE SKIN POINTS FLAG ATPLO ATPHI ATPRQ MSTRQ ATARQ -MST- GND PH SP ATA SB PJ 1X 1Y 2X 2Y CL A1 A2 A3 A4 A5 TB BF V1 ST* USL ---DIVISOR--- NAME\n");
phosg::fwrite_fmt(stream, "WEAPONS\n");
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS FLAG ATPLO ATPHI ATPRQ MSTRQ ATARQ -MST- GND PH SP ATA SB(S1:AMT1,S2:AMT2) PJ 1X 1Y 2X 2Y CL --A1-- A4 A5 TB BF V1 ST* USL ---DIVISOR--- NAME\n");
for (size_t data1_1 = 0; data1_1 < pmt->num_weapon_classes; data1_1++) {
uint8_t v1_replacement = pmt->get_weapon_v1_replacement(data1_1);
float sale_divisor = pmt->get_sale_divisor(0x00, data1_1);
@@ -681,7 +698,8 @@ void ItemNameIndex::print_table(FILE* stream) const {
item.data1[2] = data1_2;
string name = this->describe_item(item);
phosg::fwrite_fmt(stream, "00{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:04X} {:5} {:5} {:5} {:5} {:5} {:5} {:3} {:02X} {:02X} {:3} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:2}* {} {} {}\n",
auto& stat_boost = pmt->get_stat_boost(w.stat_boost_entry_index);
phosg::fwrite_fmt(stream, " 00{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:04X} {:5} {:5} {:5} {:5} {:5} {:5} {:3} {:02X} {:02X} {:3} {:02X}({:02X}:{:04X},{:02X}:{:04X}) {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}{:02X}{:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:2}* {} {} {}\n",
data1_1,
data1_2,
w.base.id,
@@ -699,16 +717,20 @@ void ItemNameIndex::print_table(FILE* stream) const {
w.photon,
w.special,
w.ata,
w.stat_boost,
w.stat_boost_entry_index,
stat_boost.stats[0],
stat_boost.amounts[0],
stat_boost.stats[1],
stat_boost.amounts[1],
w.projectile,
w.trail1_x,
w.trail1_y,
w.trail2_x,
w.trail2_y,
w.color,
w.unknown_a1,
w.unknown_a2,
w.unknown_a3,
w.unknown_a1[0],
w.unknown_a1[1],
w.unknown_a1[2],
w.unknown_a4,
w.unknown_a5,
w.tech_boost,
@@ -721,7 +743,8 @@ void ItemNameIndex::print_table(FILE* stream) const {
}
}
phosg::fwrite_fmt(stream, "ARMOR => ---ID--- TYPE SKIN POINTS -DFP- -EVP- BP BE FLAG LVL EFR ETH EIC EDK ELT DFR EVR SB TB FT A4 ST* ---DIVISOR--- NAME\n");
phosg::fwrite_fmt(stream, "ARMORS\n");
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS -DFP- -EVP- BP BE FLAG LVL EFR ETH EIC EDK ELT DFR EVR SB(S1:AMT1,S2:AMT2) TB FT A4 ST* ---DIVISOR--- NAME\n");
for (size_t data1_1 = 1; data1_1 < 3; data1_1++) {
float sale_divisor = pmt->get_sale_divisor(0x01, data1_1);
string divisor_str = std::format("{:g}", sale_divisor);
@@ -738,7 +761,8 @@ void ItemNameIndex::print_table(FILE* stream) const {
item.data1[2] = data1_2;
string name = this->describe_item(item);
phosg::fwrite_fmt(stream, "01{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:5} {:5} {:02X} {:02X} {:04X} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:02X} {:02X} {:02X} {:02X} {:2}* {} {}\n",
auto& stat_boost = pmt->get_stat_boost(a.stat_boost_entry_index);
phosg::fwrite_fmt(stream, " 01{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:5} {:5} {:02X} {:02X} {:04X} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:02X}({:02X}:{:04X},{:02X}:{:04X}) {:02X} {:02X} {:02X} {:2}* {} {}\n",
data1_1,
data1_2,
a.base.id,
@@ -758,7 +782,11 @@ void ItemNameIndex::print_table(FILE* stream) const {
a.elt,
a.dfp_range,
a.evp_range,
a.stat_boost,
a.stat_boost_entry_index,
stat_boost.stats[0],
stat_boost.amounts[0],
stat_boost.stats[1],
stat_boost.amounts[1],
a.tech_boost,
a.flags_type,
a.unknown_a4,
@@ -768,7 +796,8 @@ void ItemNameIndex::print_table(FILE* stream) const {
}
}
phosg::fwrite_fmt(stream, "UNIT => ---ID--- TYPE SKIN POINTS STAT COUNT ST-MOD ST* ---DIVISOR--- NAME\n");
phosg::fwrite_fmt(stream, "UNITS\n");
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS STAT COUNT ST-MOD ST* ---DIVISOR--- NAME\n");
{
float sale_divisor = pmt->get_sale_divisor(0x01, 0x03);
string divisor_str = std::format("{:g}", sale_divisor);
@@ -785,7 +814,7 @@ void ItemNameIndex::print_table(FILE* stream) const {
item.data1[2] = data1_2;
string name = this->describe_item(item);
phosg::fwrite_fmt(stream, "0103{:02X} => {:08X} {:04X} {:04X} {:6} {:04X} {:5} {:6} {:2}* {} {}\n",
phosg::fwrite_fmt(stream, " 0103{:02X} => {:08X} {:04X} {:04X} {:6} {:04X} {:5} {:6} {:2}* {} {}\n",
data1_2,
u.base.id,
u.base.type,
@@ -800,7 +829,8 @@ void ItemNameIndex::print_table(FILE* stream) const {
}
}
phosg::fwrite_fmt(stream, "MAG => ---ID--- TYPE SKIN POINTS FTBL PB AC E1 E2 E3 E4 C1 C2 C3 C4 FLAG ---DIVISOR--- NAME\n");
phosg::fwrite_fmt(stream, "MAGS\n");
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS FTBL PB AC E1 E2 E3 E4 C1 C2 C3 C4 FLAG ---DIVISOR--- NAME\n");
{
size_t data1_1_limit = pmt->num_mags();
for (size_t data1_1 = 0; data1_1 < data1_1_limit; data1_1++) {
@@ -816,7 +846,7 @@ void ItemNameIndex::print_table(FILE* stream) const {
item.data1[2] = 0x00;
string name = this->describe_item(item);
phosg::fwrite_fmt(stream, "02{:02X}00 => {:08X} {:04X} {:04X} {:6} {:04X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:04X} {} {}\n",
phosg::fwrite_fmt(stream, " 02{:02X}00 => {:08X} {:04X} {:04X} {:6} {:04X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:04X} {} {}\n",
data1_1,
m.base.id,
m.base.type,
@@ -839,7 +869,8 @@ void ItemNameIndex::print_table(FILE* stream) const {
}
}
phosg::fwrite_fmt(stream, "TOOL => ---ID--- TYPE SKIN POINTS COUNT TECH -COST- ITEMFLAG ---DIVISOR--- NAME\n");
phosg::fwrite_fmt(stream, "TOOLS\n");
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS COUNT TECH -COST- ITEMFLAG ---DIVISOR--- NAME\n");
for (size_t data1_1 = 0; data1_1 < pmt->num_tool_classes; data1_1++) {
float sale_divisor = pmt->get_sale_divisor(0x03, data1_1);
string divisor_str = std::format("{:g}", sale_divisor);
@@ -856,7 +887,7 @@ void ItemNameIndex::print_table(FILE* stream) const {
item.set_tool_item_amount(*this->limits, 1);
string name = this->describe_item(item);
phosg::fwrite_fmt(stream, "03{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:5} {:04X} {:6} {:08X} {} {}\n",
phosg::fwrite_fmt(stream, " 03{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:5} {:04X} {:6} {:08X} {} {}\n",
data1_1,
data1_2,
t.base.id,
@@ -872,9 +903,10 @@ void ItemNameIndex::print_table(FILE* stream) const {
}
}
phosg::fwrite_fmt(stream, "CLASS => F GF RF B GB RB Z GZ RZ GR DB JL ZL SH RY RS AT RV MG\n");
phosg::fwrite_fmt(stream, "MAX TECH LEVELS\n");
phosg::fwrite_fmt(stream, " CLASS => F GF RF B GB RB Z GZ RZ GR DB JL ZL SH RY RS AT RV MG\n");
for (size_t char_class = 0; char_class < 12; char_class++) {
phosg::fwrite_fmt(stream, "{:9} =>", name_for_char_class(char_class));
phosg::fwrite_fmt(stream, " {:9} =>", name_for_char_class(char_class));
for (size_t tech_num = 0; tech_num < 0x13; tech_num++) {
uint8_t max_level = pmt->get_max_tech_level(char_class, tech_num) + 1;
if (max_level == 0x00) {
@@ -886,30 +918,40 @@ void ItemNameIndex::print_table(FILE* stream) const {
phosg::fwrite_fmt(stream, "\n");
}
phosg::fwrite_fmt(stream, "MAG FEED TABLES\n");
for (size_t table_index = 0; table_index < 8; table_index++) {
static const char* names[11] = {
"Monomate", "Dimate", "Trimate", "Monofluid",
"Difluid", "Trifluid", "Antidote", "Antiparalysis",
"Sol Atomizer", "Moon Atomizer", "Star Atomizer"};
phosg::fwrite_fmt(stream, "TABLE {:02X} => -DEF -POW -DEX MIND -IQ- SYNC\n", table_index);
phosg::fwrite_fmt(stream, " TABLE {:02X} => -DEF -POW -DEX MIND -IQ- SYNC\n", table_index);
for (size_t which = 0; which < 11; which++) {
const auto& res = pmt->get_mag_feed_result(table_index, which);
phosg::fwrite_fmt(stream, "{:14} => {:4} {:4} {:4} {:4} {:4} {:4}\n",
phosg::fwrite_fmt(stream, " {:14} => {:4} {:4} {:4} {:4} {:4} {:4}\n",
names[which], res.def, res.pow, res.dex, res.mind, res.iq, res.synchro);
}
}
phosg::fwrite_fmt(stream, "SPECIAL => TYPE COUNT ST*\n");
phosg::fwrite_fmt(stream, "SPECIAL DEFINITIONS\n");
phosg::fwrite_fmt(stream, " SPECIAL => TYPE COUNT ST* NAME\n");
for (size_t index = 0; index < pmt->num_specials; index++) {
const auto& sp = pmt->get_special(index);
uint8_t stars = pmt->get_special_stars(index);
phosg::fwrite_fmt(stream, " {:02X} => {:04X} {:5} {:2}*\n", index, sp.type, sp.amount, stars);
const char* name = "";
if (index) {
try {
name = name_for_weapon_special.at(index);
} catch (const out_of_range&) {
}
}
phosg::fwrite_fmt(stream, " {:02X} => {:04X} {:5} {:2}* {}\n", index, sp.type, sp.amount, stars, name);
}
phosg::fwrite_fmt(stream, "---USE + -EQUIP => RESULT MLV GND LVL CLS\n");
phosg::fwrite_fmt(stream, "ITEM COMBINATIONS\n");
phosg::fwrite_fmt(stream, " ---USE + -EQUIP => RESULT MLV GND LVL CLS\n");
for (const auto& combo_list_it : pmt->get_all_item_combinations()) {
for (const auto& combo : combo_list_it.second) {
phosg::fwrite_fmt(stream, "{:02X}{:02X}{:02X} + {:02X}{:02X}{:02X} => {:02X}{:02X}{:02X}",
phosg::fwrite_fmt(stream, " {:02X}{:02X}{:02X} + {:02X}{:02X}{:02X} => {:02X}{:02X}{:02X}",
combo.used_item[0], combo.used_item[1], combo.used_item[2],
combo.equipped_item[0], combo.equipped_item[1], combo.equipped_item[2],
combo.result_item[0], combo.result_item[1], combo.result_item[2]);
@@ -936,13 +978,14 @@ void ItemNameIndex::print_table(FILE* stream) const {
}
}
phosg::fwrite_fmt(stream, "EVENT ITEMS\n");
size_t num_events = pmt->num_events();
for (size_t event_number = 0; event_number < num_events; event_number++) {
phosg::fwrite_fmt(stream, "EV {:3} => PRB\n", event_number);
phosg::fwrite_fmt(stream, " EV {:3} => PRB\n", event_number);
auto events_list = pmt->get_event_items(event_number);
for (size_t z = 0; z < events_list.second; z++) {
const auto& event_item = events_list.first[z];
phosg::fwrite_fmt(stream, "{:02X}{:02X}{:02X} => {:3}\n",
phosg::fwrite_fmt(stream, " {:02X}{:02X}{:02X} => {:3}\n",
event_item.item[0], event_item.item[1], event_item.item[2], event_item.probability);
}
}
+6 -1
View File
@@ -38,7 +38,12 @@ public:
inline bool exists(const ItemData& item) const {
return this->primary_identifier_index.count(item.primary_identifier());
}
std::string describe_item(const ItemData& item, bool include_color_escapes = false, bool hide_mag_stats = false) const;
enum Flag : uint8_t {
INCLUDE_PSO_COLOR_ESCAPES = 0x01,
NAME_ONLY = 0x02,
};
std::string describe_item(const ItemData& item, uint8_t flags = 0) const;
ItemData parse_item_description(const std::string& description) const;
void print_table(FILE* stream) const;
+119 -63
View File
@@ -4,6 +4,41 @@
using namespace std;
template <>
ServerDropMode phosg::enum_for_name<ServerDropMode>(const char* name) {
if (!strcmp(name, "DISABLED")) {
return ServerDropMode::DISABLED;
} else if (!strcmp(name, "CLIENT")) {
return ServerDropMode::CLIENT;
} else if (!strcmp(name, "SERVER_SHARED")) {
return ServerDropMode::SERVER_SHARED;
} else if (!strcmp(name, "SERVER_PRIVATE")) {
return ServerDropMode::SERVER_PRIVATE;
} else if (!strcmp(name, "SERVER_DUPLICATE")) {
return ServerDropMode::SERVER_DUPLICATE;
} else {
throw runtime_error("invalid drop mode");
}
}
template <>
const char* phosg::name_for_enum<ServerDropMode>(ServerDropMode value) {
switch (value) {
case ServerDropMode::DISABLED:
return "DISABLED";
case ServerDropMode::CLIENT:
return "CLIENT";
case ServerDropMode::SERVER_SHARED:
return "SERVER_SHARED";
case ServerDropMode::SERVER_PRIVATE:
return "SERVER_PRIVATE";
case ServerDropMode::SERVER_DUPLICATE:
return "SERVER_DUPLICATE";
default:
throw runtime_error("invalid drop mode");
}
}
ItemParameterTable::ItemParameterTable(shared_ptr<const string> data, Version version)
: version(version),
data(data),
@@ -175,7 +210,7 @@ ItemParameterTable::WeaponV4 ItemParameterTable::WeaponV1V2::to_v4() const {
ret.photon = this->photon;
ret.special = this->special;
ret.ata = this->ata;
ret.stat_boost = this->stat_boost;
ret.stat_boost_entry_index = this->stat_boost_entry_index;
return ret;
}
@@ -195,7 +230,7 @@ ItemParameterTable::WeaponV4 ItemParameterTable::WeaponGCNTE::to_v4() const {
ret.photon = this->photon;
ret.special = this->special;
ret.ata = this->ata;
ret.stat_boost = this->stat_boost;
ret.stat_boost_entry_index = this->stat_boost_entry_index;
ret.projectile = this->projectile;
ret.trail1_x = this->trail1_x;
ret.trail1_y = this->trail1_y;
@@ -203,8 +238,6 @@ ItemParameterTable::WeaponV4 ItemParameterTable::WeaponGCNTE::to_v4() const {
ret.trail2_y = this->trail2_y;
ret.color = this->color;
ret.unknown_a1 = this->unknown_a1;
ret.unknown_a2 = this->unknown_a2;
ret.unknown_a3 = this->unknown_a3;
return ret;
}
@@ -225,7 +258,7 @@ ItemParameterTable::WeaponV4 ItemParameterTable::WeaponV3T<BE>::to_v4() const {
ret.photon = this->photon;
ret.special = this->special;
ret.ata = this->ata;
ret.stat_boost = this->stat_boost;
ret.stat_boost_entry_index = this->stat_boost_entry_index;
ret.projectile = this->projectile;
ret.trail1_x = this->trail1_x;
ret.trail1_y = this->trail1_y;
@@ -233,8 +266,6 @@ ItemParameterTable::WeaponV4 ItemParameterTable::WeaponV3T<BE>::to_v4() const {
ret.trail2_y = this->trail2_y;
ret.color = this->color;
ret.unknown_a1 = this->unknown_a1;
ret.unknown_a2 = this->unknown_a2;
ret.unknown_a3 = this->unknown_a3;
ret.unknown_a4 = this->unknown_a4;
ret.unknown_a5 = this->unknown_a5;
ret.tech_boost = this->tech_boost;
@@ -277,7 +308,7 @@ ItemParameterTable::ArmorOrShieldV4 ItemParameterTable::ArmorOrShieldV1V2::to_v4
ret.elt = this->elt;
ret.dfp_range = this->dfp_range;
ret.evp_range = this->evp_range;
ret.stat_boost = this->stat_boost;
ret.stat_boost_entry_index = this->stat_boost_entry_index;
ret.tech_boost = this->tech_boost;
ret.flags_type = this->flags_type;
ret.unknown_a4 = this->unknown_a4;
@@ -303,7 +334,7 @@ ItemParameterTable::ArmorOrShieldV4 ItemParameterTable::ArmorOrShieldV3T<BE>::to
ret.elt = this->elt;
ret.dfp_range = this->dfp_range;
ret.evp_range = this->evp_range;
ret.stat_boost = this->stat_boost;
ret.stat_boost_entry_index = this->stat_boost_entry_index;
ret.tech_boost = this->tech_boost;
ret.flags_type = this->flags_type;
ret.unknown_a4 = this->unknown_a4;
@@ -522,25 +553,22 @@ const ItemParameterTable::ArmorOrShieldV4& ItemParameterTable::get_armor_or_shie
}
return ret;
} catch (const std::out_of_range&) {
ArmorOrShieldV4 def_v4;
if (this->offsets_dc_protos) {
def_v4 = indirect_lookup_2d<ArmorOrShieldDCProtos, false>(this->r, this->offsets_dc_protos->armor_table, data1_1 - 1, data1_2).to_v4();
} else if (this->offsets_v1_v2) {
def_v4 = indirect_lookup_2d<ArmorOrShieldV1V2, false>(this->r, this->offsets_v1_v2->armor_table, data1_1 - 1, data1_2).to_v4();
} else if (this->offsets_gc_nte) {
def_v4 = indirect_lookup_2d<ArmorOrShieldV3BE, true>(this->r, this->offsets_gc_nte->armor_table, data1_1 - 1, data1_2).to_v4();
} else if (this->offsets_v3_le) {
def_v4 = indirect_lookup_2d<ArmorOrShieldV3, false>(this->r, this->offsets_v3_le->armor_table, data1_1 - 1, data1_2).to_v4();
} else if (this->offsets_v3_be) {
def_v4 = indirect_lookup_2d<ArmorOrShieldV3BE, true>(this->r, this->offsets_v3_be->armor_table, data1_1 - 1, data1_2).to_v4();
} else {
throw logic_error("table is not v2, v3, or v4");
while (data1_2 >= parsed_vec.size()) {
auto& def_v4 = parsed_vec.emplace_back();
if (this->offsets_dc_protos) {
def_v4 = indirect_lookup_2d<ArmorOrShieldDCProtos, false>(this->r, this->offsets_dc_protos->armor_table, data1_1 - 1, parsed_vec.size() - 1).to_v4();
} else if (this->offsets_v1_v2) {
def_v4 = indirect_lookup_2d<ArmorOrShieldV1V2, false>(this->r, this->offsets_v1_v2->armor_table, data1_1 - 1, parsed_vec.size() - 1).to_v4();
} else if (this->offsets_gc_nte) {
def_v4 = indirect_lookup_2d<ArmorOrShieldV3BE, true>(this->r, this->offsets_gc_nte->armor_table, data1_1 - 1, parsed_vec.size() - 1).to_v4();
} else if (this->offsets_v3_le) {
def_v4 = indirect_lookup_2d<ArmorOrShieldV3, false>(this->r, this->offsets_v3_le->armor_table, data1_1 - 1, parsed_vec.size() - 1).to_v4();
} else if (this->offsets_v3_be) {
def_v4 = indirect_lookup_2d<ArmorOrShieldV3BE, true>(this->r, this->offsets_v3_be->armor_table, data1_1 - 1, parsed_vec.size() - 1).to_v4();
} else {
throw logic_error("table is not v2, v3, or v4");
}
}
if (data1_2 >= parsed_vec.size()) {
parsed_vec.resize(data1_2 + 1);
}
parsed_vec[data1_2] = def_v4;
return parsed_vec[data1_2];
}
}
@@ -575,24 +603,22 @@ const ItemParameterTable::UnitV4& ItemParameterTable::get_unit(uint8_t data1_2)
}
return ret;
} catch (const std::out_of_range&) {
UnitV4 def_v4;
if (this->offsets_dc_protos) {
def_v4 = indirect_lookup_2d<UnitDCProtos, false>(this->r, this->offsets_dc_protos->unit_table, 0, data1_2).to_v4();
} else if (this->offsets_v1_v2) {
def_v4 = indirect_lookup_2d<UnitV1V2, false>(this->r, this->offsets_v1_v2->unit_table, 0, data1_2).to_v4();
} else if (this->offsets_gc_nte) {
def_v4 = indirect_lookup_2d<UnitV3BE, true>(this->r, this->offsets_gc_nte->unit_table, 0, data1_2).to_v4();
} else if (this->offsets_v3_le) {
def_v4 = indirect_lookup_2d<UnitV3, false>(this->r, this->offsets_v3_le->unit_table, 0, data1_2).to_v4();
} else if (this->offsets_v3_be) {
def_v4 = indirect_lookup_2d<UnitV3BE, true>(this->r, this->offsets_v3_be->unit_table, 0, data1_2).to_v4();
} else {
throw logic_error("table is not v2, v3, or v4");
while (data1_2 >= this->parsed_units.size()) {
auto& def_v4 = this->parsed_units.emplace_back();
if (this->offsets_dc_protos) {
def_v4 = indirect_lookup_2d<UnitDCProtos, false>(this->r, this->offsets_dc_protos->unit_table, 0, this->parsed_units.size() - 1).to_v4();
} else if (this->offsets_v1_v2) {
def_v4 = indirect_lookup_2d<UnitV1V2, false>(this->r, this->offsets_v1_v2->unit_table, 0, this->parsed_units.size() - 1).to_v4();
} else if (this->offsets_gc_nte) {
def_v4 = indirect_lookup_2d<UnitV3BE, true>(this->r, this->offsets_gc_nte->unit_table, 0, this->parsed_units.size() - 1).to_v4();
} else if (this->offsets_v3_le) {
def_v4 = indirect_lookup_2d<UnitV3, false>(this->r, this->offsets_v3_le->unit_table, 0, this->parsed_units.size() - 1).to_v4();
} else if (this->offsets_v3_be) {
def_v4 = indirect_lookup_2d<UnitV3BE, true>(this->r, this->offsets_v3_be->unit_table, 0, this->parsed_units.size() - 1).to_v4();
} else {
throw logic_error("table is not v2, v3, or v4");
}
}
if (data1_2 >= this->parsed_units.size()) {
this->parsed_units.resize(data1_2 + 1);
}
this->parsed_units[data1_2] = def_v4;
return this->parsed_units[data1_2];
}
}
@@ -627,28 +653,26 @@ const ItemParameterTable::MagV4& ItemParameterTable::get_mag(uint8_t data1_1) co
}
return ret;
} catch (const std::out_of_range&) {
MagV4 def_v4;
if (this->offsets_dc_protos) {
def_v4 = indirect_lookup_2d<MagV1, false>(this->r, this->offsets_dc_protos->mag_table, 0, data1_1).to_v4();
} else if (this->offsets_v1_v2) {
if (is_v1(this->version)) {
def_v4 = indirect_lookup_2d<MagV1, false>(this->r, this->offsets_v1_v2->mag_table, 0, data1_1).to_v4();
while (data1_1 >= this->parsed_mags.size()) {
auto& def_v4 = this->parsed_mags.emplace_back();
if (this->offsets_dc_protos) {
def_v4 = indirect_lookup_2d<MagV1, false>(this->r, this->offsets_dc_protos->mag_table, 0, this->parsed_mags.size() - 1).to_v4();
} else if (this->offsets_v1_v2) {
if (is_v1(this->version)) {
def_v4 = indirect_lookup_2d<MagV1, false>(this->r, this->offsets_v1_v2->mag_table, 0, this->parsed_mags.size() - 1).to_v4();
} else {
def_v4 = indirect_lookup_2d<MagV2, false>(this->r, this->offsets_v1_v2->mag_table, 0, this->parsed_mags.size() - 1).to_v4();
}
} else if (this->offsets_gc_nte) {
def_v4 = indirect_lookup_2d<MagV3BE, true>(this->r, this->offsets_gc_nte->mag_table, 0, this->parsed_mags.size() - 1).to_v4();
} else if (this->offsets_v3_le) {
def_v4 = indirect_lookup_2d<MagV3, false>(this->r, this->offsets_v3_le->mag_table, 0, this->parsed_mags.size() - 1).to_v4();
} else if (this->offsets_v3_be) {
def_v4 = indirect_lookup_2d<MagV3BE, true>(this->r, this->offsets_v3_be->mag_table, 0, this->parsed_mags.size() - 1).to_v4();
} else {
def_v4 = indirect_lookup_2d<MagV2, false>(this->r, this->offsets_v1_v2->mag_table, 0, data1_1).to_v4();
throw logic_error("table is not v2, v3, or v4");
}
} else if (this->offsets_gc_nte) {
def_v4 = indirect_lookup_2d<MagV3BE, true>(this->r, this->offsets_gc_nte->mag_table, 0, data1_1).to_v4();
} else if (this->offsets_v3_le) {
def_v4 = indirect_lookup_2d<MagV3, false>(this->r, this->offsets_v3_le->mag_table, 0, data1_1).to_v4();
} else if (this->offsets_v3_be) {
def_v4 = indirect_lookup_2d<MagV3BE, true>(this->r, this->offsets_v3_be->mag_table, 0, data1_1).to_v4();
} else {
throw logic_error("table is not v2, v3, or v4");
}
if (data1_1 >= this->parsed_mags.size()) {
this->parsed_mags.resize(data1_1 + 1);
}
this->parsed_mags[data1_1] = def_v4;
return this->parsed_mags[data1_1];
}
}
@@ -928,6 +952,38 @@ const ItemParameterTable::Special& ItemParameterTable::get_special(uint8_t speci
}
}
const ItemParameterTable::StatBoost& ItemParameterTable::get_stat_boost(uint8_t entry_index) const {
if (this->offsets_dc_protos) {
return this->r.pget<StatBoost>(this->offsets_dc_protos->stat_boost_table + sizeof(StatBoost) * entry_index);
} else if (this->offsets_v1_v2) {
return this->r.pget<StatBoost>(this->offsets_v1_v2->stat_boost_table + sizeof(StatBoost) * entry_index);
} else if (this->offsets_v3_le) {
return this->r.pget<StatBoost>(this->offsets_v3_le->stat_boost_table + sizeof(StatBoost) * entry_index);
} else if (this->offsets_gc_nte) {
while (entry_index >= this->parsed_stat_boosts.size()) {
const auto& sb_be = this->r.pget<StatBoostBE>(this->offsets_gc_nte->stat_boost_table + sizeof(StatBoostBE) * this->parsed_stat_boosts.size());
auto& sb = this->parsed_stat_boosts.emplace_back();
sb.stats = sb_be.stats;
sb.amounts[0] = sb_be.amounts[0];
sb.amounts[1] = sb_be.amounts[1];
}
return this->parsed_stat_boosts[entry_index];
} else if (this->offsets_v3_be) {
while (entry_index >= this->parsed_stat_boosts.size()) {
const auto& sb_be = this->r.pget<StatBoostBE>(this->offsets_v3_be->stat_boost_table + sizeof(StatBoostBE) * this->parsed_stat_boosts.size());
auto& sb = this->parsed_stat_boosts.emplace_back();
sb.stats = sb_be.stats;
sb.amounts[0] = sb_be.amounts[0];
sb.amounts[1] = sb_be.amounts[1];
}
return this->parsed_stat_boosts[entry_index];
} else if (this->offsets_v4) {
return this->r.pget<StatBoost>(this->offsets_v4->stat_boost_table + sizeof(StatBoost) * entry_index);
} else {
throw logic_error("table is not v2, v3, or v4");
}
}
uint8_t ItemParameterTable::get_max_tech_level(uint8_t char_class, uint8_t tech_num) const {
if (char_class >= 12) {
throw out_of_range("invalid character class");
+57 -19
View File
@@ -1,5 +1,7 @@
#pragma once
#include "WindowsPlatform.hh"
#include <stdint.h>
#include <array>
@@ -16,6 +18,26 @@
#include "Types.hh"
#include "Version.hh"
// TODO: These don't really belong here, but putting them anywhere else creates
// annoying dependency cycles. Find or make a better place for these.
enum class ServerDropMode {
DISABLED = 0,
CLIENT = 1, // Not allowed for BB games
SERVER_SHARED = 2,
SERVER_PRIVATE = 3,
SERVER_DUPLICATE = 4,
};
enum class ProxyDropMode {
DISABLED = 0,
PASSTHROUGH,
INTERCEPT,
};
template <>
ServerDropMode phosg::enum_for_name<ServerDropMode>(const char* name);
template <>
const char* phosg::name_for_enum<ServerDropMode>(ServerDropMode value);
class ItemParameterTable {
public:
// TODO: This implementation is ugly. We should use real classes and virtual
@@ -71,7 +93,7 @@ public:
/* 11 */ uint8_t photon = 0;
/* 12 */ uint8_t special = 0;
/* 13 */ uint8_t ata = 0;
/* 14 */ uint8_t stat_boost = 0; // TODO: This could be larger (16 or 32 bits)
/* 14 */ uint8_t stat_boost_entry_index = 0; // TODO: This could be larger (16 or 32 bits)
/* 15 */ parray<uint8_t, 3> unknown_a9;
/* 18 */
@@ -91,16 +113,14 @@ public:
/* 17 */ uint8_t photon = 0;
/* 18 */ uint8_t special = 0;
/* 19 */ uint8_t ata = 0;
/* 1A */ uint8_t stat_boost = 0;
/* 1A */ uint8_t stat_boost_entry_index = 0;
/* 1B */ uint8_t projectile = 0;
/* 1C */ int8_t trail1_x = 0;
/* 1D */ int8_t trail1_y = 0;
/* 1E */ int8_t trail2_x = 0;
/* 1F */ int8_t trail2_y = 0;
/* 20 */ int8_t color = 0;
/* 21 */ uint8_t unknown_a1 = 0;
/* 22 */ uint8_t unknown_a2 = 0;
/* 23 */ uint8_t unknown_a3 = 0;
/* 21 */ parray<uint8_t, 3> unknown_a1 = 0;
/* 24 */
WeaponV4 to_v4() const;
@@ -120,16 +140,14 @@ public:
/* 17 */ uint8_t photon = 0;
/* 18 */ uint8_t special = 0;
/* 19 */ uint8_t ata = 0;
/* 1A */ uint8_t stat_boost = 0;
/* 1A */ uint8_t stat_boost_entry_index = 0;
/* 1B */ uint8_t projectile = 0;
/* 1C */ int8_t trail1_x = 0;
/* 1D */ int8_t trail1_y = 0;
/* 1E */ int8_t trail2_x = 0;
/* 1F */ int8_t trail2_y = 0;
/* 20 */ int8_t color = 0;
/* 21 */ uint8_t unknown_a1 = 0;
/* 22 */ uint8_t unknown_a2 = 0;
/* 23 */ uint8_t unknown_a3 = 0;
/* 21 */ parray<uint8_t, 3> unknown_a1 = 0;
/* 24 */ uint8_t unknown_a4 = 0;
/* 25 */ uint8_t unknown_a5 = 0;
/* 26 */ uint8_t tech_boost = 0;
@@ -162,16 +180,14 @@ public:
/* 1B */ uint8_t photon = 0;
/* 1C */ uint8_t special = 0;
/* 1D */ uint8_t ata = 0;
/* 1E */ uint8_t stat_boost = 0;
/* 1E */ uint8_t stat_boost_entry_index = 0;
/* 1F */ uint8_t projectile = 0;
/* 20 */ int8_t trail1_x = 0;
/* 21 */ int8_t trail1_y = 0;
/* 22 */ int8_t trail2_x = 0;
/* 23 */ int8_t trail2_y = 0;
/* 24 */ int8_t color = 0;
/* 25 */ uint8_t unknown_a1 = 0;
/* 26 */ uint8_t unknown_a2 = 0;
/* 27 */ uint8_t unknown_a3 = 0;
/* 25 */ parray<uint8_t, 3> unknown_a1 = 0;
/* 28 */ uint8_t unknown_a4 = 0;
/* 29 */ uint8_t unknown_a5 = 0;
/* 2A */ uint8_t tech_boost = 0;
@@ -201,7 +217,7 @@ public:
template <typename BaseT, bool BE>
struct ArmorOrShieldFinalT : ArmorOrShieldT<BaseT, BE> {
/* 14 */ uint8_t stat_boost = 0;
/* 14 */ uint8_t stat_boost_entry_index = 0;
/* 15 */ uint8_t tech_boost = 0;
// TODO: Figure out what this does. Only a few values appear to do anything:
// Shields:
@@ -391,10 +407,30 @@ public:
template <bool BE>
struct StatBoostT {
uint8_t stat1 = 0;
uint8_t stat2 = 0;
U16T<BE> amount1 = 0;
U16T<BE> amount2 = 0;
// Only the first of these stat/amount pairs is used in most versions of
// the game. In DC 11/2000 Sega apparently changed the loop from
// `for (z = 0; z != 2; z++)` to `for (z = 0; z != 1; z++)`, so only the
// first stat/amount pair is used on all versions after DC NTE.
// Values for stats:
// 01 = ATP bonus
// 02 = ATA bonus
// 03 = EVP bonus
// 04 = DFP bonus
// 05 = MST bonus
// 06 = HP bonus
// 07 = LCK bonus
// 08 = all of the above bonuses except HP
// 09 = ATP penalty
// 0A = ATA penalty
// 0B = EVP penalty
// 0C = DFP penalty
// 0D = MST penalty
// 0E = HP penalty
// 0F = LCK penalty
// 10 = all of the above penalties except HP
// Anything else (including 00) = no bonus or penalty
parray<uint8_t, 2> stats = 0;
parray<U16T<BE>, 2> amounts;
} __attribute__((packed));
using StatBoost = StatBoostT<false>;
using StatBoostBE = StatBoostT<true>;
@@ -476,6 +512,7 @@ public:
uint8_t get_item_stars(uint32_t id) const;
uint8_t get_special_stars(uint8_t special) const;
const Special& get_special(uint8_t special) const;
const StatBoost& get_stat_boost(uint8_t entry_index) const;
uint8_t get_max_tech_level(uint8_t char_class, uint8_t tech_num) const;
uint8_t get_weapon_v1_replacement(uint8_t data1_1) const;
@@ -624,6 +661,7 @@ protected:
mutable std::vector<MagV4> parsed_mags;
mutable std::unordered_map<uint16_t, ToolV4> parsed_tools;
mutable std::vector<Special> parsed_specials;
mutable std::vector<StatBoost> parsed_stat_boosts;
// Key is used_item. We can't index on (used_item, equipped_item) because
// equipped_item may contain wildcards, and the matching order matters.
@@ -647,7 +685,7 @@ public:
// This specifies which entry in ItemMagMotion.dat is used. The file is
// just a list of 0x64-byte structures. 0xFF = no TItemMagSub is created
uint8_t motion_table_entry = 0xFF;
parray<uint8_t, 5> unknown_a1;
parray<uint8_t, 5> unknown_a1 = 0;
} __packed_ws__(Side, 0x06);
parray<Side, 2> sides; // [0] = right side, [1] = left side
} __packed_ws__(MotionReference, 0x0C);
+3 -3
View File
@@ -16,7 +16,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<RandomG
bool is_v3_or_later = is_v3(c->version()) || is_v4;
bool should_delete_item = is_v3_or_later;
auto player = c->character();
auto player = c->character_file();
auto& item = player->inventory.items[item_index];
uint32_t primary_identifier = item.data.primary_identifier();
@@ -47,7 +47,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<RandomG
weapon.data.data1[3] = min<uint8_t>(weapon.data.data1[3] + item.data.data1[2] + 1, weapon_def.max_grind);
} else if ((primary_identifier & 0xFFFF0000) == 0x030B0000) { // Material
auto p = c->character();
auto p = c->character_file();
using Type = PSOBBCharacterFile::MaterialType;
Type type;
@@ -499,7 +499,7 @@ void apply_mag_feed_result(
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index) {
auto s = c->require_server_state();
auto player = c->character();
auto player = c->character_file();
apply_mag_feed_result(
player->inventory.items[mag_item_index].data,
player->inventory.items[fed_item_index].data,
+43 -102
View File
@@ -154,12 +154,12 @@ Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id, bool is_game)
episode(Episode::NONE),
mode(GameMode::NORMAL),
difficulty(0),
base_exp_multiplier(1),
exp_share_multiplier(0.5),
base_exp_multiplier(1.0f),
exp_share_multiplier(0.5f),
challenge_exp_multiplier(1.0f),
random_seed(phosg::random_object<uint32_t>()),
rand_crypt(make_shared<DisabledRandomGenerator>()),
drop_mode(DropMode::CLIENT),
drop_mode(ServerDropMode::CLIENT),
event(0),
block(0),
leader_id(0),
@@ -186,6 +186,14 @@ void Lobby::reset_next_item_ids() {
this->next_game_item_id = 0xCC000000;
}
uint8_t Lobby::area_for_floor(Version version, uint8_t floor) const {
if (this->quest) {
return this->quest->meta.area_for_floor.at(floor);
}
auto sdt = this->require_server_state()->set_data_table(version, this->episode, this->mode, this->difficulty);
return sdt->default_area_for_floor(this->episode, floor);
}
shared_ptr<ServerState> Lobby::require_server_state() const {
auto s = this->server_state.lock();
if (!s) {
@@ -214,41 +222,6 @@ void Lobby::create_item_creator(Version logic_version) {
logic_version = leader_c ? leader_c->version() : Version::BB_V4;
}
shared_ptr<const RareItemSet> rare_item_set;
shared_ptr<const CommonItemSet> common_item_set;
switch (logic_version) {
case Version::PC_PATCH:
case Version::BB_PATCH:
case Version::GC_EP3_NTE:
case Version::GC_EP3:
throw runtime_error("cannot create item creator for this base version");
case Version::DC_NTE:
case Version::DC_11_2000:
case Version::DC_V1:
// TODO: We should probably have a v1 common item set at some point too
common_item_set = s->common_item_set_v2;
rare_item_set = s->rare_item_sets.at("rare-table-v1");
break;
case Version::DC_V2:
case Version::PC_NTE:
case Version::PC_V2:
common_item_set = s->common_item_set_v2;
rare_item_set = s->rare_item_sets.at("rare-table-v2");
break;
case Version::GC_NTE:
case Version::GC_V3:
case Version::XB_V3:
common_item_set = s->common_item_set_v3_v4;
rare_item_set = s->rare_item_sets.at("rare-table-v3");
break;
case Version::BB_V4:
common_item_set = s->common_item_set_v3_v4;
rare_item_set = s->rare_item_sets.at("rare-table-v4");
break;
default:
throw logic_error("invalid lobby base version");
}
shared_ptr<RandomGenerator> rand_crypt;
if (s->use_psov2_rand_crypt) {
rand_crypt = make_shared<PSOV2Encryption>(this->rand_crypt->seed());
@@ -256,8 +229,8 @@ void Lobby::create_item_creator(Version logic_version) {
rand_crypt = make_shared<MT19937Generator>(this->rand_crypt->seed());
}
this->item_creator = make_shared<ItemCreator>(
common_item_set,
rare_item_set,
s->common_item_set(logic_version, this->quest),
s->rare_item_set(logic_version, this->quest),
s->armor_random_set,
s->tool_random_set,
s->weapon_random_sets.at(this->difficulty),
@@ -269,7 +242,7 @@ void Lobby::create_item_creator(Version logic_version) {
this->difficulty,
this->effective_section_id(),
rand_crypt,
this->quest ? this->quest->battle_rules : nullptr);
this->quest ? this->quest->meta.battle_rules : nullptr);
}
uint8_t Lobby::effective_section_id() const {
@@ -281,7 +254,7 @@ uint8_t Lobby::effective_section_id() const {
}
auto leader = this->clients.at(this->leader_id);
if (leader) {
return leader->character()->disp.visual.section_id;
return leader->character_file()->disp.visual.section_id;
}
return 0;
}
@@ -296,28 +269,29 @@ uint16_t Lobby::quest_version_flags() const {
return ret;
}
uint8_t Lobby::client_extension_flags() const {
for (auto lc : this->clients) {
if (lc && !lc->check_flag(Client::Flag::HAS_ENEMY_DAMAGE_SYNC_PATCH)) {
return 0x01;
}
}
return 0x81;
}
void Lobby::load_maps() {
auto rare_rates = this->rare_enemy_rates ? this->rare_enemy_rates : MapState::DEFAULT_RARE_ENEMIES;
if (this->quest) {
this->log.info_f("Loading quest supermap");
auto supermap = this->quest->get_supermap(this->random_seed);
this->map_state = make_shared<MapState>(
this->lobby_id,
this->difficulty,
this->event,
this->random_seed,
this->rare_enemy_rates,
this->rand_crypt,
this->quest->get_supermap(this->random_seed));
this->lobby_id, this->difficulty, this->event, this->random_seed, this->rare_enemy_rates, this->rand_crypt, supermap);
} else {
this->log.info_f("Loading free play supermaps");
auto s = this->require_server_state();
auto supermaps = s->supermaps_for_variations(this->episode, this->mode, this->difficulty, this->variations);
this->map_state = make_shared<MapState>(
this->lobby_id,
this->difficulty,
this->event,
this->random_seed,
this->rare_enemy_rates,
this->rand_crypt,
s->supermaps_for_variations(this->episode, this->mode, this->difficulty, this->variations));
this->lobby_id, this->difficulty, this->event, this->random_seed, this->rare_enemy_rates, this->rand_crypt, supermaps);
}
if (this->check_flag(Lobby::Flag::DEBUG)) {
@@ -491,7 +465,7 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
// If the lobby is recording a battle record, add the player join event
if (this->battle_record) {
auto p = c->character();
auto p = c->character_file();
PlayerLobbyDataDCGC lobby_data;
lobby_data.player_tag = 0x00010000;
lobby_data.guild_card_number = c->login->account->account_id;
@@ -633,7 +607,7 @@ shared_ptr<Client> Lobby::find_client(const string* identifier, uint64_t account
if (account_id && lc->login && (lc->login->account->account_id == account_id)) {
return lc;
}
if (identifier && (lc->character()->disp.name.eq(*identifier, lc->language()))) {
if (identifier && (lc->character_file()->disp.name.eq(*identifier, lc->language()))) {
return lc;
}
}
@@ -670,7 +644,7 @@ Lobby::JoinError Lobby::join_error_for_client(std::shared_ptr<Client> c, const s
if (password && !this->password.empty() && (*password != this->password)) {
return JoinError::INCORRECT_PASSWORD;
}
auto p = c->character();
auto p = c->character_file();
if (p->disp.stats.level < this->min_level) {
return JoinError::LEVEL_TOO_LOW;
}
@@ -764,7 +738,7 @@ void Lobby::on_item_id_generated_externally(uint32_t item_id) {
}
void Lobby::assign_inventory_and_bank_item_ids(shared_ptr<Client> c, bool consume_ids) {
auto p = c->character();
auto p = c->character_file();
uint32_t orig_next_item_id = this->next_item_id_for_client.at(c->lobby_client_id);
for (size_t z = 0; z < p->inventory.num_items; z++) {
p->inventory.items[z].data.id = this->generate_item_id(c->lobby_client_id);
@@ -775,13 +749,15 @@ void Lobby::assign_inventory_and_bank_item_ids(shared_ptr<Client> c, bool consum
if (c->log.info_f("Assigned inventory item IDs{}", consume_ids ? "" : " but did not mark IDs as used")) {
c->print_inventory();
auto& bank = c->current_bank();
if (p->bank.num_items) {
bank.assign_ids(0x99000000 + (c->lobby_client_id << 20));
c->log.info_f("Assigned bank item IDs");
c->print_bank();
} else {
c->log.info_f("Bank is empty");
if ((c->version() == Version::BB_V4) && !c->has_overlay()) {
auto bank = c->bank_file();
if (!bank->items.empty()) {
bank->assign_ids(0x99000000 + (c->lobby_client_id << 20));
c->log.info_f("Assigned bank item IDs");
c->print_bank();
} else {
c->log.info_f("Bank is empty");
}
}
}
}
@@ -875,38 +851,3 @@ bool Lobby::compare_shared(const shared_ptr<const Lobby>& a, const shared_ptr<co
return a->name < b->name;
}
template <>
Lobby::DropMode phosg::enum_for_name<Lobby::DropMode>(const char* name) {
if (!strcmp(name, "DISABLED")) {
return Lobby::DropMode::DISABLED;
} else if (!strcmp(name, "CLIENT")) {
return Lobby::DropMode::CLIENT;
} else if (!strcmp(name, "SERVER_SHARED")) {
return Lobby::DropMode::SERVER_SHARED;
} else if (!strcmp(name, "SERVER_PRIVATE")) {
return Lobby::DropMode::SERVER_PRIVATE;
} else if (!strcmp(name, "SERVER_DUPLICATE")) {
return Lobby::DropMode::SERVER_DUPLICATE;
} else {
throw runtime_error("invalid drop mode");
}
}
template <>
const char* phosg::name_for_enum<Lobby::DropMode>(Lobby::DropMode value) {
switch (value) {
case Lobby::DropMode::DISABLED:
return "DISABLED";
case Lobby::DropMode::CLIENT:
return "CLIENT";
case Lobby::DropMode::SERVER_SHARED:
return "SERVER_SHARED";
case Lobby::DropMode::SERVER_PRIVATE:
return "SERVER_PRIVATE";
case Lobby::DropMode::SERVER_DUPLICATE:
return "SERVER_DUPLICATE";
default:
throw runtime_error("invalid drop mode");
}
}
+7 -11
View File
@@ -90,13 +90,6 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
IS_OVERFLOW = 0x08000000,
// clang-format on
};
enum class DropMode {
DISABLED = 0,
CLIENT = 1, // Not allowed for BB games
SERVER_SHARED = 2,
SERVER_PRIVATE = 3,
SERVER_DUPLICATE = 4,
};
std::weak_ptr<ServerState> server_state;
phosg::PrefixedLogger log;
@@ -127,7 +120,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
Episode episode;
GameMode mode;
uint8_t difficulty; // 0-3
uint16_t base_exp_multiplier;
float base_exp_multiplier;
float exp_share_multiplier;
float challenge_exp_multiplier;
std::string password;
@@ -136,7 +129,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
uint32_t random_seed;
std::shared_ptr<RandomGenerator> rand_crypt;
uint8_t allowed_drop_modes;
DropMode drop_mode;
ServerDropMode drop_mode;
std::shared_ptr<ItemCreator> item_creator; // Always null for lobbies, never null for games
struct ChallengeParameters {
@@ -208,11 +201,14 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
this->enabled_flags ^= static_cast<uint32_t>(flag);
}
uint8_t area_for_floor(Version version, uint8_t floor) const;
std::shared_ptr<ServerState> require_server_state() const;
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
void create_item_creator(Version logic_version = Version::UNKNOWN);
uint8_t effective_section_id() const;
uint16_t quest_version_flags() const;
uint8_t client_extension_flags() const;
void load_maps();
void create_ep3_server();
@@ -290,6 +286,6 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
};
template <>
Lobby::DropMode phosg::enum_for_name<Lobby::DropMode>(const char* name);
ServerDropMode phosg::enum_for_name<ServerDropMode>(const char* name);
template <>
const char* phosg::name_for_enum<Lobby::DropMode>(Lobby::DropMode value);
const char* phosg::name_for_enum<ServerDropMode>(ServerDropMode value);
+17
View File
@@ -28,6 +28,23 @@ static void set_log_level_from_json(
}
}
void set_all_log_levels(phosg::LogLevel level) {
channel_exceptions_log.min_level = level;
client_log.min_level = level;
command_data_log.min_level = level;
config_log.min_level = level;
dns_server_log.min_level = level;
function_compiler_log.min_level = level;
ip_stack_simulator_log.min_level = level;
lobby_log.min_level = level;
patch_index_log.min_level = level;
player_data_log.min_level = level;
proxy_server_log.min_level = level;
replay_log.min_level = level;
server_log.min_level = level;
static_game_data_log.min_level = level;
}
void set_log_levels_from_json(const phosg::JSON& json) {
set_log_level_from_json(channel_exceptions_log, json, "ChannelExceptions");
set_log_level_from_json(client_log, json, "Clients");
+1
View File
@@ -18,4 +18,5 @@ extern phosg::PrefixedLogger replay_log;
extern phosg::PrefixedLogger server_log;
extern phosg::PrefixedLogger static_game_data_log;
void set_all_log_levels(phosg::LogLevel level);
void set_log_levels_from_json(const phosg::JSON& json);
+250 -131
View File
@@ -663,6 +663,19 @@ Action a_generate_pc_v2_registry(
write_output_data(args, output_data.data(), output_data.size(), "reg");
});
Action a_encrypt_challenge_time(
"encrypt-challenge-time", nullptr, +[](phosg::Arguments& args) {
uint16_t time = args.get<uint16_t>(1);
uint32_t ret = encrypt_challenge_time(time);
phosg::fwrite_fmt(stderr, "{} => {:08X}\n", phosg::format_duration(time * 1000000), ret);
});
Action a_decrypt_challenge_time(
"decrypt-challenge-time", nullptr, +[](phosg::Arguments& args) {
uint32_t time = args.get<uint32_t>(1, phosg::Arguments::IntFormat::HEX);
uint16_t ret = decrypt_challenge_time(time);
phosg::fwrite_fmt(stderr, "{:08X} => {}\n", time, phosg::format_duration(ret * 1000000));
});
Action a_encrypt_challenge_data(
"encrypt-challenge-data", nullptr, +[](phosg::Arguments& args) {
string data = read_input_data(args);
@@ -846,85 +859,6 @@ Action a_encrypt_vms_save("encrypt-vms-save", "\
hexadecimal value.\n",
a_encrypt_decrypt_vms_save_fn);
static void a_encrypt_decrypt_gci_save_fn(phosg::Arguments& args) {
bool is_decrypt = (args.get<string>(0) == "decrypt-gci-save");
bool skip_checksum = args.get<bool>("skip-checksum");
string seed = args.get<string>("seed");
string system_filename = args.get<string>("sys");
int64_t override_round2_seed = args.get<int64_t>("round2-seed", -1, phosg::Arguments::IntFormat::HEX);
uint32_t round1_seed;
if (!system_filename.empty()) {
string system_data = phosg::load_file(system_filename);
phosg::StringReader r(system_data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
const auto& system = r.get<PSOGCSystemFile>();
round1_seed = system.creation_timestamp;
} else if (!seed.empty()) {
round1_seed = stoul(seed, nullptr, 16);
} else {
throw runtime_error("either --sys or --seed must be given");
}
auto data = read_input_data(args);
phosg::StringReader r(data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
size_t data_start_offset = r.where();
auto process_file = [&]<typename StructT>() {
if (is_decrypt) {
const void* data_section = r.getv(header.data_size);
auto decrypted = decrypt_fixed_size_data_section_t<StructT, true>(
data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed);
*reinterpret_cast<StructT*>(data.data() + data_start_offset) = decrypted;
} else {
const auto& s = r.get<StructT>();
auto encrypted = encrypt_fixed_size_data_section_t<StructT, true>(s, round1_seed);
if (data_start_offset + encrypted.size() > data.size()) {
throw runtime_error("encrypted result exceeds file size");
}
memcpy(data.data() + data_start_offset, encrypted.data(), encrypted.size());
}
};
if (header.data_size == sizeof(PSOGCGuildCardFile)) {
process_file.template operator()<PSOGCGuildCardFile>();
} else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) {
process_file.template operator()<PSOGCCharacterFile>();
} else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) {
auto* charfile = reinterpret_cast<PSOGCEp3CharacterFile*>(data.data() + data_start_offset);
if (!is_decrypt) {
for (size_t z = 0; z < charfile->characters.size(); z++) {
charfile->characters[z].ep3_config.encrypt(phosg::random_object<uint8_t>());
}
}
process_file.template operator()<PSOGCEp3CharacterFile>();
if (is_decrypt) {
for (size_t z = 0; z < charfile->characters.size(); z++) {
charfile->characters[z].ep3_config.decrypt();
}
}
} else {
throw runtime_error("unrecognized save type");
}
write_output_data(args, data.data(), data.size(), is_decrypt ? "gcid" : "gci");
}
Action a_decrypt_gci_save("decrypt-gci-save", nullptr, a_encrypt_decrypt_gci_save_fn);
Action a_encrypt_gci_save("encrypt-gci-save", "\
encrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
decrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
Encrypt or decrypt a character or Guild Card file in GCI format. If\n\
encrypting, the checksum is also recomputed and stored in the encrypted\n\
file. CRYPT-OPTION is required; it can be either --sys=SYSTEM-FILENAME\n\
(specifying the name of the corresponding PSO_SYSTEM .gci file) or\n\
--seed=ROUND1-SEED (specified as a 32-bit hexadecimal number).\n",
a_encrypt_decrypt_gci_save_fn);
static void a_encrypt_decrypt_pc_save_fn(phosg::Arguments& args) {
bool is_decrypt = (args.get<string>(0) == "decrypt-pc-save");
bool skip_checksum = args.get<bool>("skip-checksum");
@@ -1021,6 +955,137 @@ static void a_encrypt_decrypt_save_data_fn(phosg::Arguments& args) {
write_output_data(args, output_data.data(), output_data.size(), "dec");
}
static void a_encrypt_decrypt_gci_save_fn(phosg::Arguments& args) {
bool is_decrypt = (args.get<string>(0) == "decrypt-gci-save");
bool skip_checksum = args.get<bool>("skip-checksum");
string seed = args.get<string>("seed");
string system_filename = args.get<string>("sys");
int64_t override_round2_seed = args.get<int64_t>("round2-seed", -1, phosg::Arguments::IntFormat::HEX);
uint32_t round1_seed;
if (!system_filename.empty()) {
string system_data = phosg::load_file(system_filename);
phosg::StringReader r(system_data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
const auto& system = r.get<PSOGCSystemFile>();
round1_seed = system.creation_timestamp;
} else if (!seed.empty()) {
round1_seed = stoul(seed, nullptr, 16);
} else {
throw runtime_error("either --sys or --seed must be given");
}
auto data = read_input_data(args);
phosg::StringReader r(data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
size_t data_start_offset = r.where();
auto process_file = [&]<typename StructT>() {
if (is_decrypt) {
const void* data_section = r.getv(header.data_size);
auto decrypted = decrypt_fixed_size_data_section_t<StructT, true>(
data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed);
*reinterpret_cast<StructT*>(data.data() + data_start_offset) = decrypted;
} else {
const auto& s = r.get<StructT>();
auto encrypted = encrypt_fixed_size_data_section_t<StructT, true>(s, round1_seed);
if (data_start_offset + encrypted.size() > data.size()) {
throw runtime_error("encrypted result exceeds file size");
}
memcpy(data.data() + data_start_offset, encrypted.data(), encrypted.size());
}
};
if (header.data_size == sizeof(PSOGCGuildCardFile)) {
process_file.template operator()<PSOGCGuildCardFile>();
} else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) {
process_file.template operator()<PSOGCCharacterFile>();
} else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) {
auto* charfile = reinterpret_cast<PSOGCEp3CharacterFile*>(data.data() + data_start_offset);
if (!is_decrypt) {
for (size_t z = 0; z < charfile->characters.size(); z++) {
charfile->characters[z].ep3_config.encrypt(phosg::random_object<uint8_t>());
}
}
process_file.template operator()<PSOGCEp3CharacterFile>();
if (is_decrypt) {
for (size_t z = 0; z < charfile->characters.size(); z++) {
charfile->characters[z].ep3_config.decrypt();
}
}
} else {
throw runtime_error("unrecognized save type");
}
write_output_data(args, data.data(), data.size(), is_decrypt ? "gcid" : "gci");
}
Action a_decrypt_gci_save("decrypt-gci-save", nullptr, a_encrypt_decrypt_gci_save_fn);
Action a_encrypt_gci_save("encrypt-gci-save", "\
encrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
decrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
Encrypt or decrypt a character or Guild Card file in GCI format. If\n\
encrypting, the checksum is also recomputed and stored in the encrypted\n\
file. CRYPT-OPTION is required; it can be either --sys=SYSTEM-FILENAME\n\
(specifying the name of the corresponding PSO_SYSTEM .gci file) or\n\
--seed=ROUND1-SEED (specified as a 32-bit hexadecimal number).\n",
a_encrypt_decrypt_gci_save_fn);
Action a_decrypt_xbox_save(
"decrypt-xbox-save", "\
decrypt-xbox-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
Decrypt a character or Guild Card file in Xbox format. CRYPT-OPTION is\n\
required; it can be either --sys=SYSTEM-FILENAME (specifying the name of\n\
the corresponding PSO_SYSTEM file) or --seed=ROUND1-SEED (specified as a\n\
32-bit hexadecimal number).\n",
+[](phosg::Arguments& args) {
bool skip_checksum = args.get<bool>("skip-checksum");
string seed = args.get<string>("seed");
string system_filename = args.get<string>("sys");
int64_t override_round2_seed = args.get<int64_t>("round2-seed", -1, phosg::Arguments::IntFormat::HEX);
uint32_t round1_seed;
if (!system_filename.empty()) {
string system_data = phosg::load_file(system_filename);
phosg::StringReader r(system_data);
const auto& header = r.get<PSOXBFileHeader>();
header.check();
const auto& system = r.get<PSOXBSystemFile>();
round1_seed = system.creation_timestamp;
} else if (!seed.empty()) {
round1_seed = stoul(seed, nullptr, 16);
} else {
throw runtime_error("either --sys or --seed must be given");
}
auto data = read_input_data(args);
phosg::StringReader r(data);
const auto& header = r.get<PSOXBFileHeader>();
header.check();
size_t data_start_offset = r.where();
auto process_file = [&]<typename StructT>() {
const void* data_section = r.getv(header.data_size);
auto decrypted = decrypt_fixed_size_data_section_t<StructT, false>(
data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed);
*reinterpret_cast<StructT*>(data.data() + data_start_offset) = decrypted;
};
if (header.data_size == sizeof(PSOXBGuildCardFile)) {
process_file.template operator()<PSOXBGuildCardFile>();
} else if (header.data_size == sizeof(PSOXBCharacterFile)) {
process_file.template operator()<PSOXBCharacterFile>();
} else {
throw runtime_error("unrecognized save type");
}
write_output_data(args, data.data(), data.size(), "dec");
});
// TODO: Write usage text for these actions
Action a_decrypt_save_data("decrypt-save-data", nullptr, a_encrypt_decrypt_save_data_fn);
Action a_encrypt_save_data("encrypt-save-data", nullptr, a_encrypt_decrypt_save_data_fn);
@@ -1101,7 +1166,7 @@ Action a_decode_gci_snapshot(
}
auto img = file.decode_image();
string saved = img.save(phosg::Image::Format::WINDOWS_BITMAP);
string saved = img.serialize(phosg::ImageFormat::WINDOWS_BITMAP);
write_output_data(args, saved.data(), saved.size(), "bmp");
});
@@ -1112,13 +1177,16 @@ Action a_encode_gvm(
GVM file can be used as an Episode 3 lobby banner.\n",
+[](phosg::Arguments& args) {
const string& input_filename = args.get<string>(1, false);
phosg::Image img;
string data;
if (!input_filename.empty() && (input_filename != "-")) {
img = phosg::Image(input_filename);
data = phosg::load_file(input_filename);
} else {
img = phosg::Image(stdin);
data = phosg::read_all(stdin);
}
string encoded = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565, "image.gvr", 0);
auto img = phosg::ImageRGBA8888N::from_file_data(data);
// If the image has any transparent pixels at all, use RGB5A3
string encoded = encode_gvm(
img, has_any_transparent_pixels(img) ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565, "image.gvr", 0);
write_output_data(args, encoded.data(), encoded.size(), "gvm");
});
@@ -1127,11 +1195,24 @@ Action a_decode_bitmap_font(
decode-bitmap-font --width=WIDTH [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Decode a 2-bit bitmap font file (.fon) into a BMP image. The --width\n\
option is required; if the output looks wrong, try increasing or\n\
decreasing this number. For S18all04.fon, the width should be 20.\n",
decreasing this number. For S18all04.fon, the width should be 20. If\n\
--show-unused is given, highlights the unused ares of ISO8859 characters\n\
in red.\n",
+[](phosg::Arguments& args) {
std::string data = read_input_data(args);
size_t width = args.get<size_t>("width");
std::string bmp_data = decode_fon(data, width).save(phosg::Image::Format::WINDOWS_BITMAP);
phosg::Image res = decode_fon(data, width);
if (width == 20 && args.get<bool>("show-unused")) {
static const array<uint8_t, 0xBF> iso8859_widths{7, 9, 13, 11, 15, 14, 7, 8, 8, 11, 11, 7, 11, 7, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 7, 7, 9, 11, 9, 10, 15, 13, 12, 13, 12, 11, 11, 13, 12, 8, 11, 12, 11, 15, 12, 13, 11, 13, 12, 11, 13, 12, 13, 15, 12, 13, 11, 8, 11, 8, 8, 9, 8, 12, 11, 12, 11, 12, 10, 12, 11, 6, 9, 11, 6, 14, 11, 12, 11, 11, 9, 11, 10, 11, 12, 15, 11, 11, 11, 9, 8, 9, 9, 9, 12, 7, 10, 13, 10, 10, 7, 10, 8, 17, 9, 12, 11, 9, 17, 9, 7, 11, 8, 8, 8, 11, 11, 8, 7, 6, 9, 12, 13, 13, 13, 10, 13, 13, 13, 13, 13, 13, 17, 13, 11, 11, 11, 11, 8, 8, 8, 8, 12, 12, 13, 13, 13, 13, 13, 11, 13, 12, 12, 12, 12, 15, 11, 10, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 6, 6, 6, 6, 11, 11, 12, 12, 12, 12, 12, 11, 12, 11, 11, 11, 11, 11, 11, 11};
for (size_t z = 0; z < iso8859_widths.size(); z++) {
for (size_t y = (z + 1) * 0x12; y < (z + 2) * 0x12; y++) {
for (size_t x = iso8859_widths.at(z); x < width; x++) {
res.write(x, y, 0xFF0000FF);
}
}
}
}
string bmp_data = res.serialize(phosg::ImageFormat::WINDOWS_BITMAP);
write_output_data(args, bmp_data.data(), bmp_data.size(), "bmp");
});
Action a_encode_bitmap_font(
@@ -1142,12 +1223,13 @@ Action a_encode_bitmap_font(
original fon\'s dimensions.\n",
+[](phosg::Arguments& args) {
const string& input_filename = args.get<string>(1, false);
phosg::Image img;
string data;
if (!input_filename.empty() && (input_filename != "-")) {
img = phosg::Image(input_filename);
data = phosg::load_file(input_filename);
} else {
img = phosg::Image(stdin);
data = phosg::read_all(stdin);
}
auto img = phosg::ImageRGB888::from_file_data(data);
string encoded = encode_fon(img);
write_output_data(args, encoded.data(), encoded.size(), "fon");
});
@@ -1647,9 +1729,10 @@ Action a_assemble_all_patches(
phosg::StringWriter w;
string data = prepare_send_function_call_data(
code, {}, nullptr, 0, checksum_addr, checksum_size, override_start_addr, encrypted);
w.put(PSOCommandHeaderDCV3{.command = 0xB2, .flag = code->index, .size = data.size() + 4});
w.put(PSOCommandHeaderDCV3{.command = 0xB2, .flag = 0x00, .size = data.size() + 4});
w.write(data);
string out_path = code->source_path + (encrypted ? ".enc.bin" : ".std.bin");
string out_path = std::format("{}.{}.{}.bin",
code->source_path, str_for_specific_version(code->specific_version), (encrypted ? "enc" : "std"));
phosg::save_file(out_path, w.str());
phosg::fwrite_fmt(stderr, "... {}\n", out_path);
}
@@ -1822,7 +1905,7 @@ Action a_decode_text_archive(
ts = make_unique<BinaryTextAndKeyboardsSet>(data, args.get<bool>("big-endian"), is_sjis);
}
phosg::JSON j = ts->json();
string out_data = j.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY);
string out_data = j.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY | phosg::JSON::SerializeOption::EXPAND_LEAF_CONTAINERS);
write_output_data(args, out_data.data(), out_data.size(), "json");
});
Action a_encode_text_archive(
@@ -1862,7 +1945,7 @@ Action a_decode_unicode_text_set(
"decode-unicode-text-set", nullptr, +[](phosg::Arguments& args) {
UnicodeTextSet uts(read_input_data(args));
phosg::JSON j = uts.json();
string out_data = j.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY);
string out_data = j.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY | phosg::JSON::SerializeOption::EXPAND_LEAF_CONTAINERS);
write_output_data(args, out_data.data(), out_data.size(), "json");
});
Action a_encode_unicode_text_set(
@@ -2082,6 +2165,24 @@ Action a_convert_rare_item_set(
throw runtime_error("cannot determine output format; use a filename ending with .json, .gsl, .gslb, or .afs");
}
});
static shared_ptr<CommonItemSet> load_common_item_set(
const std::string& filename, const std::string& ct_filename, bool big_endian) {
auto data = make_shared<string>(phosg::load_file(filename));
if (filename.ends_with(".json")) {
return make_shared<JSONCommonItemSet>(phosg::JSON::parse(*data));
} else if (filename.ends_with(".afs")) {
auto ct_data = make_shared<string>(phosg::load_file(ct_filename));
return make_shared<AFSV2CommonItemSet>(data, ct_data);
} else if (filename.ends_with(".gsl")) {
return make_shared<GSLV3V4CommonItemSet>(data, big_endian);
} else if (filename.ends_with(".gslb")) {
return make_shared<GSLV3V4CommonItemSet>(data, true);
} else {
throw runtime_error("cannot determine input format; use a filename ending with .json, .afs, .gsl, or .gslb");
}
}
Action a_convert_common_item_set(
"convert-common-item-set", "\
convert-common-item-set INPUT-FILENAME [OUTPUT-FILENAME]\n\
@@ -2089,6 +2190,7 @@ Action a_convert_common_item_set(
OUTPUT-FILENAME or stdout. The input filename must end in one of the\n\
following extensions:\n\
.json (newserv JSON common item table)\n\
.afs (PSO v2 AFS archive; --ct-filename is required in this case)\n\
.gsl (PSO BB little-endian GSL archive)\n\
.gslb (PSO GC big-endian GSL archive)\n",
+[](phosg::Arguments& args) {
@@ -2097,27 +2199,32 @@ Action a_convert_common_item_set(
throw runtime_error("input filename must be given");
}
auto data = make_shared<string>(read_input_data(args));
shared_ptr<CommonItemSet> cs;
if (input_filename.ends_with(".json")) {
cs = make_shared<JSONCommonItemSet>(phosg::JSON::parse(*data));
} else if (input_filename.ends_with(".gsl")) {
cs = make_shared<GSLV3V4CommonItemSet>(data, args.get<bool>("big-endian"));
} else if (input_filename.ends_with(".gslb")) {
cs = make_shared<GSLV3V4CommonItemSet>(data, true);
} else {
throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, or .afs");
}
auto cs = load_common_item_set(input_filename, args.get<string>("ct-filename", false), args.get<bool>("big-endian"));
const string& output_filename = args.get<string>(2, false);
if (output_filename.empty()) {
cs->print(stdout);
} else {
auto json = cs->json();
string json_data = json.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS | phosg::JSON::SerializeOption::SORT_DICT_KEYS);
string json_data = json.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::SORT_DICT_KEYS);
write_output_data(args, json_data.data(), json_data.size(), "json");
}
});
Action a_compare_common_item_set(
"compare-common-item-set", nullptr,
+[](phosg::Arguments& args) {
string input_filename1 = args.get<string>(1, false);
if (input_filename1.empty() || (input_filename1 == "-")) {
throw runtime_error("two input filenames must be given");
}
string input_filename2 = args.get<string>(2, false);
if (input_filename2.empty() || (input_filename2 == "-")) {
throw runtime_error("two input filenames must be given");
}
auto cs1 = load_common_item_set(input_filename1, args.get<string>("ct-filename1", false), args.get<bool>("big-endian1"));
auto cs2 = load_common_item_set(input_filename2, args.get<string>("ct-filename2", false), args.get<bool>("big-endian2"));
cs1->print_diff(stdout, *cs2);
});
Action a_describe_item(
"describe-item", "\
@@ -2144,7 +2251,7 @@ Action a_describe_item(
}
string desc = name_index->describe_item(item);
string desc_colored = name_index->describe_item(item, true);
string desc_colored = name_index->describe_item(item, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES);
phosg::log_info_f("Data (decoded): {:02X}{:02X}{:02X}{:02X} {:02X}{:02X}{:02X}{:02X} {:02X}{:02X}{:02X}{:02X} -------- {:02X}{:02X}{:02X}{:02X}",
item.data1[0], item.data1[1], item.data1[2], item.data1[3],
@@ -2525,22 +2632,19 @@ Action a_generate_ep3_cards_html(
phosg::parallel_range<uint32_t>([&](uint32_t index, size_t) -> bool {
auto& info = this->card_infos[index];
if (!info.large_filename.empty()) {
phosg::Image img(info.large_filename);
phosg::Image cropped(512, 399);
cropped.blit(img, 0, 0, 512, 399, 0, 0);
info.large_data_url = cropped.png_data_url();
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.large_filename));
img.resize(512, 399);
info.large_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
}
if (!info.medium_filename.empty()) {
phosg::Image img(info.medium_filename);
phosg::Image cropped(184, 144);
cropped.blit(img, 0, 0, 184, 144, 0, 0);
info.medium_data_url = cropped.png_data_url();
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.medium_filename));
img.resize(184, 144);
info.medium_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
}
if (!info.small_filename.empty()) {
phosg::Image img(info.small_filename);
phosg::Image cropped(58, 43);
cropped.blit(img, 0, 0, 58, 43, 0, 0);
info.small_data_url = cropped.png_data_url();
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.small_filename));
img.resize(58, 43);
info.small_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
}
return false;
},
@@ -2697,10 +2801,9 @@ Action a_show_ep3_maps(
s->load_ep3_cards();
s->load_ep3_maps();
auto map_ids = s->ep3_map_index->all_numbers();
const auto& map_ids = s->ep3_map_index->all();
phosg::log_info_f("{} maps", map_ids.size());
for (uint32_t map_id : map_ids) {
auto map = s->ep3_map_index->for_number(map_id);
for (const auto& [map_number, map] : map_ids) {
const auto& vms = map->all_versions();
for (size_t language = 0; language < vms.size(); language++) {
if (!vms[language]) {
@@ -2723,17 +2826,17 @@ Action a_show_battle_params(
s->load_battle_params();
phosg::fwrite_fmt(stdout, "Episode 1 multi\n");
s->battle_params->get_table(false, Episode::EP1).print(stdout);
s->battle_params->get_table(false, Episode::EP1).print(stdout, Episode::EP1);
phosg::fwrite_fmt(stdout, "Episode 1 solo\n");
s->battle_params->get_table(true, Episode::EP1).print(stdout);
s->battle_params->get_table(true, Episode::EP1).print(stdout, Episode::EP1);
phosg::fwrite_fmt(stdout, "Episode 2 multi\n");
s->battle_params->get_table(false, Episode::EP2).print(stdout);
s->battle_params->get_table(false, Episode::EP2).print(stdout, Episode::EP2);
phosg::fwrite_fmt(stdout, "Episode 2 solo\n");
s->battle_params->get_table(true, Episode::EP2).print(stdout);
s->battle_params->get_table(true, Episode::EP2).print(stdout, Episode::EP2);
phosg::fwrite_fmt(stdout, "Episode 4 multi\n");
s->battle_params->get_table(false, Episode::EP4).print(stdout);
s->battle_params->get_table(false, Episode::EP4).print(stdout, Episode::EP4);
phosg::fwrite_fmt(stdout, "Episode 4 solo\n");
s->battle_params->get_table(true, Episode::EP4).print(stdout);
s->battle_params->get_table(true, Episode::EP4).print(stdout, Episode::EP4);
});
Action a_check_supermaps(
@@ -2824,7 +2927,7 @@ Action a_check_supermaps(
SuperMap::EfficiencyStats all_quests_eff;
uint32_t random_seed = args.get<uint32_t>("random-seed", 0, phosg::Arguments::IntFormat::HEX);
for (const auto& it : s->default_quest_index->quests_by_number) {
for (const auto& it : s->quest_index->quests_by_number) {
auto supermap = it.second->get_supermap(random_seed);
if (!supermap) {
throw logic_error("quest does not have a supermap, even with a specified random seed");
@@ -2834,7 +2937,7 @@ Action a_check_supermaps(
if (save_disassembly) {
string filename = std::format("supermap_quest_{}_{:08X}.txt", it.first, random_seed);
auto f = phosg::fopen_unique(filename, "wt");
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->name);
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->meta.name);
supermap->print(f.get());
filename_token = " => " + filename;
}
@@ -2845,7 +2948,7 @@ Action a_check_supermaps(
}
string filename = std::format("supermap_quest_{}_{:08X}_enemy_counts.txt", it.first, random_seed);
auto f = phosg::fopen_unique(filename, "wt");
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->name);
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->meta.name);
phosg::fwrite_fmt(f.get(), "ENEMY--------------- DCNTE 11/2K DC-V1 DC-V2 PCNTE PC-V2 GCNTE GC-V3 XB-V3 BB-V4\n");
for (size_t type_ss = 0; type_ss < static_cast<size_t>(EnemyType::MAX_ENEMY_TYPE); type_ss++) {
EnemyType type = static_cast<EnemyType>(type_ss);
@@ -2901,6 +3004,19 @@ Action a_check_supermaps(
phosg::fwrite_fmt(stderr, "ALL QUEST MAPS: {}\n", all_quests_eff_str);
});
Action a_check_quests(
"check-quests", nullptr,
+[](phosg::Arguments& args) {
auto s = make_shared<ServerState>(get_config_filename(args));
s->is_debug = true;
s->load_config_early();
s->clear_file_caches();
s->load_patch_indexes();
s->load_set_data_tables();
s->load_maps();
s->load_quest_index();
});
Action a_parse_object_graph(
"parse-object-graph", nullptr, +[](phosg::Arguments& args) {
uint32_t root_object_address = args.get<uint32_t>("root", phosg::Arguments::IntFormat::HEX);
@@ -3182,6 +3298,9 @@ Action a_run_server_replay_log(
}
auto state = make_shared<ServerState>(get_config_filename(args));
if (args.get<bool>("debug")) {
state->is_debug = true;
}
state->load_all(true);
if (state->dns_server_port) {
+39 -76
View File
@@ -67,7 +67,7 @@ vector<string> SetDataTableBase::map_filenames_for_variations(
return ret;
}
uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const {
uint8_t SetDataTableBase::default_area_for_floor(Version version, Episode episode, uint8_t floor) {
// For some inscrutable reason, Pioneer 2's area number in Episode 4 is
// discontiguous with all the rest. Why, Sega??
static const array<uint8_t, 0x12> areas_ep1 = {
@@ -82,7 +82,7 @@ uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor)
case Episode::EP1:
return areas_ep1.at(floor);
case Episode::EP2: {
const auto& areas = ((this->version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2);
const auto& areas = ((version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2);
return areas.at(floor);
}
case Episode::EP4:
@@ -92,6 +92,10 @@ uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor)
}
}
uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const {
return this->default_area_for_floor(this->version, episode, floor);
}
SetDataTable::SetDataTable(Version version, const string& data) : SetDataTableBase(version) {
if (is_big_endian(this->version)) {
this->load_table_t<true>(data);
@@ -972,7 +976,11 @@ static const vector<DATEntityDefinition> dat_object_definitions({
// In offline mode, this object constructs TObjWarpBossMulti instead.
{0x0019, F_V0_V4, 0x00006FFC3FFC04A5, "TObjWarpBoss"},
// Sign board. This shows the loaded image from a quest (via load_pvr).
// Sign board. This shows the loaded image from a quest (via load_pvr). On
// the final version of PCv2, this object doesn't render at all; my guess
// is that Sega hardcoded the PVR filenames for the various events in the
// executable, then after those events ended, they just deleted the
// load_pvr code entirely, leaving this object nonfunctional.
// Params:
// param1-3 = scale factors (x, y, z)
{0x001A, F_V1_V4, 0x0000600000040001, "TObjSinBoard"},
@@ -2003,8 +2011,18 @@ static const vector<DATEntityDefinition> dat_object_definitions({
{0x018C, F_V3_V4, 0x0000400000008000, "TObjParticleLobby"},
{0x018C, F_EP3, 0x0000000000008000, "TObjParticleLobby"},
// Episode 3 lobby battle table. Params:
// param4 = player count (1-4); only 2 and 4 are used in-game
// Episode 3 lobby battle table. This object is responsible for the red
// panels on the floor next to the battle table that turn green when you
// step on them; it also shows the confirmation window and sends the
// necessary commands to the server. The actual table model and the non-lit
// parts of the floor panels are part of the lobby geometry, not this
// object. Params:
// param4 = player count
// 1 = 1 player (doesn't work properly - there's no way to confirm)
// 2 = 2 players
// 3 = 3 players (unused, but works)
// 4 = 4 players
// anything else = object doesn't load
// param5 = table number (used in E4 and E5 commands)
{0x018D, F_EP3, 0x0000000000008000, "TObjLobbyTable"},
@@ -2720,8 +2738,8 @@ static const vector<DATEntityDefinition> dat_enemy_definitions({
{0x0110, F_EP3, 0x0000000000000001, "TObjNpcWalkingMeka_Hero"}, // Small talking robot in Morgue
{0x0111, F_EP3, 0x0000000000000001, "TObjNpcWalkingMeka_Dark"}, // Small talking robot in Morgue
// Episode 3 scientist and aide NPCs. These NPC take all the same params as
// the NPCs defined above, but also:
// Episode 3 scientist and aide NPCs. These NPCs take all the same params
// as the NPCs defined above, but also:
// angle.x = model number (clamped to [0, 3] for scientists, [0, 2] for
// aides)
// The two type values for scientists (00D4 and 00D5) are direct aliases
@@ -2927,7 +2945,7 @@ static const vector<DATEntityDefinition> dat_enemy_definitions({
// Monest that has param1 = 3 and param2 = 10. This looks like just an
// off-by-one error on Sega's part where they accidentally shifted the
// parameters down by one place. As described above, this Monest expels
// 6 Mothmants, then no more after they are killed.
// 6 Mothmants immediately, then no more after those 6 are killed.
{0x0042, F_V0_V4, 0x0000000000180006, "TObjEneBm3FlyNest"},
// Savage Wolf or Barbarous Wolf. Params:
@@ -4034,12 +4052,12 @@ string MapFile::disassemble_action_stream(const void* data, size_t size) {
}
case 0x0A: {
uint16_t id = r.get_u16l();
ret.emplace_back(std::format(" 0A {:04X} enable_switch_flag id={:04X}", id, id));
ret.emplace_back(std::format(" 0A {:04X} set_switch_flag id={:04X}", id, id));
break;
}
case 0x0B: {
uint16_t id = r.get_u16l();
ret.emplace_back(std::format(" 0B {:04X} disable_switch_flag id={:04X}", id, id));
ret.emplace_back(std::format(" 0B {:04X} clear_switch_flag id={:04X}", id, id));
break;
}
case 0x0C: {
@@ -4881,8 +4899,8 @@ static size_t get_action_stream_size(const void* data, size_t size) {
r.skip(4);
done = (cmd == 0x0D);
break;
case 0x0A: // enable_switch_flag(uint16_t flag_num)
case 0x0B: // disable_switch_flag(uint16_t flag_num)
case 0x0A: // set_switch_flag(uint16_t flag_num)
case 0x0B: // clear_switch_flag(uint16_t flag_num)
r.skip(2);
break;
default:
@@ -5819,7 +5837,7 @@ phosg::JSON MapState::RareEnemyRates::json() const {
});
}
uint32_t MapState::RareEnemyRates::for_enemy_type(EnemyType type) const {
uint32_t MapState::RareEnemyRates::get(EnemyType type) const {
switch (type) {
case EnemyType::HILDEBEAR:
return this->hildeblue;
@@ -5848,55 +5866,12 @@ uint32_t MapState::RareEnemyRates::for_enemy_type(EnemyType type) const {
}
}
const shared_ptr<const MapState::RareEnemyRates> MapState::NO_RARE_ENEMIES = make_shared<MapState::RareEnemyRates>(
0, 0, 0);
const shared_ptr<const MapState::RareEnemyRates> MapState::NO_RARE_ENEMIES = make_shared<MapState::RareEnemyRates>(0, 0, 0);
const shared_ptr<const MapState::RareEnemyRates> MapState::DEFAULT_RARE_ENEMIES = make_shared<MapState::RareEnemyRates>(
MapState::RareEnemyRates::DEFAULT_RARE_ENEMY_RATE_V3,
MapState::RareEnemyRates::DEFAULT_MERICARAND_RATE_V3,
MapState::RareEnemyRates::DEFAULT_RARE_BOSS_RATE_V4);
uint32_t MapState::EnemyState::convert_game_flags(uint32_t game_flags, bool to_v3) {
// The format of game_flags was changed significantly between v2 and v3, and
// not accounting for this results in odd effects like other characters not
// appearing when joining a game. Unfortunately, some bits were deleted on v3
// and other bits were added, so it doesn't suffice to simply store the most
// complete format of this field - we have to be able to convert between the
// two.
// Bits on v2: ?IHCBAzy xwvutsrq ponmlkji hgfedcba
// Bits on v3: ?IHGFEDC BAzyxwvu srqponkj hgfedcba
// The bits ilmt were removed in v3 and the bits to their left were shifted
// right. The bits DEFG were added in v3 and do not exist on v2.
// Known meanings for these bits:
// o = is dead
// n = should play hit animation
// y = is near enemy
// H = is enemy?
// I = is object? (some entities have both H and I set though)
// TODO: The above might all be wrong.
// GC 00100000 10010000 00001110 00000000
// PC 00101001 00000000 01100100 00000000
// PC 00101001 10110000 00101110 00000000
// GC 00100000 10011011 00000111 00000000
// PC 00101001 10010000 00101110 00000000
// GC 00100000 10011001 00000111 00000000
if (to_v3) {
return (game_flags & 0xE00000FF) |
((game_flags & 0x00000600) >> 1) |
((game_flags & 0x0007E000) >> 3) |
((game_flags & 0x1FF00000) >> 4);
} else {
return (game_flags & 0xE00000FF) |
((game_flags << 1) & 0x00000600) |
((game_flags << 3) & 0x0007E000) |
((game_flags << 4) & 0x1FF00000);
}
}
MapState::EntityIterator::EntityIterator(MapState* map_state, Version version, bool at_end)
: map_state(map_state),
version(version),
@@ -6100,7 +6075,7 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptr<RandomGenerator
auto rare_type = type_definition_for_enemy(type).rare_type(fc.super_map->get_episode(), this->event, ene->floor);
if ((type == EnemyType::MERICARAND) || (rare_type != type)) {
unordered_map<uint32_t, float> det_cache;
uint32_t bb_rare_rate = this->bb_rare_rates->for_enemy_type(type);
uint32_t bb_rare_rate = this->bb_rare_rates->get(type);
for (Version v : ALL_NON_PATCH_VERSIONS) {
// Skip this version if the enemy doesn't exist there
uint16_t relative_enemy_index = ene->version(v).relative_enemy_index;
@@ -6413,7 +6388,6 @@ void MapState::import_object_states_from_sync(
void MapState::import_enemy_states_from_sync(Version from_version, const SyncEnemyStateEntry* entries, size_t entry_count) {
this->log.info_f("Importing enemy state from sync command");
size_t enemy_index = 0;
bool is_v3 = !is_v1_or_v2(from_version);
for (const auto& fc : this->floor_config_entries) {
if (!fc.super_map) {
continue;
@@ -6434,15 +6408,10 @@ void MapState::import_enemy_states_from_sync(Version from_version, const SyncEne
if (ene_st->super_ene != ene) {
throw logic_error("super enemy link is incorrect");
}
if (ene_st->get_game_flags(is_v3) != entry.flags) {
this->log.warning_f("({:04X} => E-{:03X}) Flags from client ({:08X}({})) do not match game flags from map ({:08X}({}))",
enemy_index,
ene_st->e_id,
entry.flags,
is_v3 ? "v3" : "v2",
ene_st->game_flags,
(ene_st->server_flags & MapState::EnemyState::Flag::GAME_FLAGS_IS_V3) ? "v3" : "v2");
ene_st->set_game_flags(entry.flags, !is_v1_or_v2(from_version));
if (ene_st->game_flags != entry.flags) {
this->log.warning_f("({:04X} => E-{:03X}) Flags from client ({:08X}) do not match game flags from map ({:08X})",
enemy_index, ene_st->e_id, entry.flags, ene_st->game_flags);
ene_st->game_flags = entry.flags;
}
if (ene_st->total_damage != entry.total_damage) {
this->log.warning_f("({:04X} => E-{:03X}) Total damage from client ({}) does not match total damage from map ({})",
@@ -6771,14 +6740,8 @@ void MapState::print(FILE* stream) const {
}
}
string ene_str = ene_st->super_ene->str();
phosg::fwrite_fmt(stream, " {} total_damage={:04X} rare_flags={:04X} game_flags={:08X}({}) set_flags={:04X} server_flags={:04X}\n",
ene_str,
ene_st->total_damage,
ene_st->rare_flags,
ene_st->game_flags,
(ene_st->server_flags & MapState::EnemyState::Flag::GAME_FLAGS_IS_V3) ? "v3" : "v2",
ene_st->set_flags,
ene_st->server_flags);
phosg::fwrite_fmt(stream, " {} total_damage={:04X} rare_flags={:04X} game_flags={:08X} set_flags={:04X} server_flags={:04X}\n",
ene_str, ene_st->total_damage, ene_st->rare_flags, ene_st->game_flags, ene_st->set_flags, ene_st->server_flags);
}
if (this->bb_rare_enemy_indexes.empty()) {
+2 -21
View File
@@ -67,6 +67,7 @@ public:
std::vector<std::string> map_filenames_for_variations(
Episode episode, GameMode mode, const Variations& variations, FilenameType type) const;
static uint8_t default_area_for_floor(Version version, Episode episode, uint8_t floor);
uint8_t default_area_for_floor(Episode episode, uint8_t floor) const;
protected:
@@ -672,7 +673,7 @@ public:
RareEnemyRates(uint32_t enemy_rate, uint32_t mericarand_rate, uint32_t boss_rate);
explicit RareEnemyRates(const phosg::JSON& json);
uint32_t for_enemy_type(EnemyType type) const;
uint32_t get(EnemyType type) const;
std::string str() const;
phosg::JSON json() const;
@@ -716,7 +717,6 @@ public:
ITEM_DROPPED = 0x0008,
ALL_HITS_MASK_FIRST = 0x0010,
ALL_HITS_MASK = 0x00F0,
GAME_FLAGS_IS_V3 = 0x0100,
};
size_t e_id = 0;
size_t set_id = 0;
@@ -727,8 +727,6 @@ public:
uint16_t set_flags = 0; // Only used if super_ene->child_index == 0
uint16_t server_flags = 0;
static uint32_t convert_game_flags(uint32_t game_flags, bool to_v3);
inline void reset() {
this->total_damage = 0;
this->rare_flags = 0;
@@ -737,23 +735,6 @@ public:
this->server_flags = 0;
}
inline void set_game_flags(uint32_t game_flags, bool is_v3) {
this->game_flags = game_flags;
if (is_v3) {
this->server_flags |= Flag::GAME_FLAGS_IS_V3;
} else {
this->server_flags &= ~Flag::GAME_FLAGS_IS_V3;
}
}
inline uint32_t get_game_flags(bool is_v3) const {
bool flags_is_v3 = (this->server_flags & Flag::GAME_FLAGS_IS_V3);
if (flags_is_v3 == is_v3) {
return this->game_flags;
} else {
return this->convert_game_flags(this->game_flags, is_v3);
}
}
inline bool is_rare(Version version) const {
return (((this->rare_flags >> static_cast<size_t>(version)) & 1) ||
((version == Version::BB_V4) ? this->super_ene->is_default_rare_bb : this->super_ene->is_default_rare_v123));
+1
View File
@@ -21,6 +21,7 @@ constexpr uint32_t LOBBY = 0x33000033;
constexpr uint32_t GAME = 0x44000044;
constexpr uint32_t QUEST_EP1 = 0x55010155;
constexpr uint32_t QUEST_EP2 = 0x55020255;
constexpr uint32_t QUEST_EP3 = 0x55030355;
// See the decsription of the A2 command in CommandFormats.hh for why these
// menu IDs don't fit the rest of the pattern.
constexpr uint32_t QUEST_CATEGORIES_EP1 = 0x01000001;
+21 -20
View File
@@ -706,27 +706,28 @@ void PSOBBMultiKeyDetectorEncryption::encrypt(void* data, size_t size) {
}
void PSOBBMultiKeyDetectorEncryption::decrypt(void* data, size_t size) {
if (!this->active_crypt.get()) {
if (size != 8) {
throw logic_error("initial decryption size does not match expected first data size");
}
for (const auto& key : this->possible_keys) {
this->active_key = key;
this->active_crypt = make_shared<PSOBBEncryption>(*this->active_key, this->seed.data(), this->seed.size());
string test_data(reinterpret_cast<const char*>(data), size);
this->active_crypt->decrypt(test_data.data(), test_data.size());
if (this->expected_first_data.count(test_data)) {
break;
}
this->active_key.reset();
this->active_crypt.reset();
}
if (!this->active_crypt.get()) {
throw runtime_error("none of the registered private keys are valid for this client");
}
if (this->active_crypt.get()) {
this->active_crypt->decrypt(data, size);
return;
}
this->active_crypt->decrypt(data, size);
if (size != 8) {
throw logic_error("initial decryption size does not match expected first data size");
}
for (const auto& key : this->possible_keys) {
this->active_key = key;
this->active_crypt = make_shared<PSOBBEncryption>(*this->active_key, this->seed.data(), this->seed.size());
string test_data(reinterpret_cast<const char*>(data), size);
this->active_crypt->decrypt(test_data.data(), test_data.size());
if (this->expected_first_data.count(test_data)) {
memcpy(data, test_data.data(), size);
return;
}
this->active_key.reset();
this->active_crypt.reset();
}
throw runtime_error("none of the registered private keys are valid for this client");
}
PSOEncryption::Type PSOBBMultiKeyDetectorEncryption::type() const {
+30
View File
@@ -356,6 +356,36 @@ inline std::string decrypt_v2_registry_value(const std::string& s) {
return decrypt_v2_registry_value(s.data(), s.size());
}
template <bool BE>
std::string decrypt_pr1_data(const void* data, size_t size) {
if (size < 4) {
throw std::runtime_error("not enough data for PR1 footer");
}
phosg::StringReader r(data, size);
std::string ret = r.read(size - 4);
PSOV2Encryption crypt(r.get<U32T<BE>>());
if constexpr (BE) {
crypt.encrypt_big_endian(ret.data(), ret.size());
} else {
crypt.decrypt(ret.data(), ret.size());
}
return ret;
}
template <bool BE>
std::string encrypt_pr1_data(const void* data, size_t size, uint32_t seed) {
phosg::StringWriter w;
w.write(data, size);
w.put<U32T<BE>>(seed);
PSOV2Encryption crypt(seed);
if constexpr (BE) {
crypt.encrypt_big_endian(w.str().data(), size);
} else {
crypt.encrypt(w.str().data(), size);
}
return std::move(w.str());
}
struct DecryptedPR2 {
std::string compressed_data;
size_t decompressed_size;
-122
View File
@@ -1,122 +0,0 @@
#include "PlayerFilesManager.hh"
#include <stdio.h>
#include <string.h>
#include <wchar.h>
#include <phosg/Filesystem.hh>
#include <phosg/Hash.hh>
#include <stdexcept>
#include "FileContentsCache.hh"
#include "ItemData.hh"
#include "Loggers.hh"
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "StaticGameData.hh"
#include "Text.hh"
#include "Version.hh"
using namespace std;
PlayerFilesManager::PlayerFilesManager(std::shared_ptr<asio::io_context> io_context)
: io_context(io_context),
clear_expired_files_timer(*this->io_context) {
this->schedule_callback();
}
std::shared_ptr<PSOBBBaseSystemFile> PlayerFilesManager::get_system(const std::string& filename) {
try {
return this->loaded_system_files.at(filename);
} catch (const out_of_range&) {
return nullptr;
}
}
std::shared_ptr<PSOBBCharacterFile> PlayerFilesManager::get_character(const std::string& filename) {
try {
return this->loaded_character_files.at(filename);
} catch (const out_of_range&) {
return nullptr;
}
}
std::shared_ptr<PSOBBGuildCardFile> PlayerFilesManager::get_guild_card(const std::string& filename) {
try {
return this->loaded_guild_card_files.at(filename);
} catch (const out_of_range&) {
return nullptr;
}
}
std::shared_ptr<PlayerBank200> PlayerFilesManager::get_bank(const std::string& filename) {
try {
return this->loaded_bank_files.at(filename);
} catch (const out_of_range&) {
return nullptr;
}
}
void PlayerFilesManager::set_system(const std::string& filename, std::shared_ptr<PSOBBBaseSystemFile> file) {
if (!this->loaded_system_files.emplace(filename, file).second) {
throw runtime_error("Guild Card file already loaded: " + filename);
}
}
void PlayerFilesManager::set_character(const std::string& filename, std::shared_ptr<PSOBBCharacterFile> file) {
if (!this->loaded_character_files.emplace(filename, file).second) {
throw runtime_error("character file already loaded: " + filename);
}
}
void PlayerFilesManager::set_guild_card(const std::string& filename, std::shared_ptr<PSOBBGuildCardFile> file) {
if (!this->loaded_guild_card_files.emplace(filename, file).second) {
throw runtime_error("Guild Card file already loaded: " + filename);
}
}
void PlayerFilesManager::set_bank(const std::string& filename, std::shared_ptr<PlayerBank200> file) {
if (!this->loaded_bank_files.emplace(filename, file).second) {
throw runtime_error("bank file already loaded: " + filename);
}
}
void PlayerFilesManager::schedule_callback() {
this->clear_expired_files_timer.expires_after(std::chrono::seconds(30));
this->clear_expired_files_timer.async_wait(bind(&PlayerFilesManager::clear_expired_files, this));
}
template <typename KeyT, typename ValueT>
size_t erase_unused(std::unordered_map<KeyT, std::shared_ptr<ValueT>>& m) {
size_t ret = 0;
for (auto it = m.begin(); it != m.end();) {
if (it->second.use_count() <= 1) {
it = m.erase(it);
ret++;
} else {
it++;
}
}
return ret;
}
void PlayerFilesManager::clear_expired_files() {
size_t num_deleted = erase_unused(this->loaded_system_files);
if (num_deleted) {
player_data_log.info_f("Cleared {} expired system file(s)", num_deleted);
}
num_deleted = erase_unused(this->loaded_character_files);
if (num_deleted) {
player_data_log.info_f("Cleared {} expired character file(s)", num_deleted);
}
num_deleted = erase_unused(this->loaded_guild_card_files);
if (num_deleted) {
player_data_log.info_f("Cleared {} expired Guild Card file(s)", num_deleted);
}
num_deleted = erase_unused(this->loaded_bank_files);
if (num_deleted) {
player_data_log.info_f("Cleared {} expired bank file(s)", num_deleted);
}
this->schedule_callback();
}
-48
View File
@@ -1,48 +0,0 @@
#pragma once
#include <inttypes.h>
#include <stddef.h>
#include <array>
#include <asio.hpp>
#include <phosg/Encoding.hh>
#include <string>
#include <utility>
#include <vector>
#include "Episode3/DataIndexes.hh"
#include "ItemCreator.hh"
#include "ItemNameIndex.hh"
#include "LevelTable.hh"
#include "PlayerSubordinates.hh"
#include "SaveFileFormats.hh"
#include "Text.hh"
#include "Version.hh"
class PlayerFilesManager {
public:
explicit PlayerFilesManager(std::shared_ptr<asio::io_context> io_context);
~PlayerFilesManager() = default;
std::shared_ptr<PSOBBBaseSystemFile> get_system(const std::string& filename);
std::shared_ptr<PSOBBCharacterFile> get_character(const std::string& filename);
std::shared_ptr<PSOBBGuildCardFile> get_guild_card(const std::string& filename);
std::shared_ptr<PlayerBank200> get_bank(const std::string& filename);
void set_system(const std::string& filename, std::shared_ptr<PSOBBBaseSystemFile> file);
void set_character(const std::string& filename, std::shared_ptr<PSOBBCharacterFile> file);
void set_guild_card(const std::string& filename, std::shared_ptr<PSOBBGuildCardFile> file);
void set_bank(const std::string& filename, std::shared_ptr<PlayerBank200> file);
private:
std::shared_ptr<asio::io_context> io_context;
asio::steady_timer clear_expired_files_timer;
std::unordered_map<std::string, std::shared_ptr<PSOBBBaseSystemFile>> loaded_system_files;
std::unordered_map<std::string, std::shared_ptr<PSOBBCharacterFile>> loaded_character_files;
std::unordered_map<std::string, std::shared_ptr<PSOBBGuildCardFile>> loaded_guild_card_files;
std::unordered_map<std::string, std::shared_ptr<PlayerBank200>> loaded_bank_files;
void schedule_callback();
void clear_expired_files();
};
+116
View File
@@ -0,0 +1,116 @@
#include "PlayerInventory.hh"
void PlayerBank::load(FILE* f) {
le_uint32_t num_items;
le_uint32_t meseta;
phosg::freadx(f, &num_items, sizeof(num_items));
phosg::freadx(f, &meseta, sizeof(meseta));
this->meseta = meseta;
this->items.reserve(num_items);
while (this->items.size() < num_items) {
auto& item = this->items.emplace_back();
phosg::freadx(f, &item, sizeof(item));
}
}
void PlayerBank::save(FILE* f) const {
le_uint32_t num_items = this->items.size();
le_uint32_t meseta = this->meseta;
phosg::fwritex(f, &num_items, sizeof(num_items));
phosg::fwritex(f, &meseta, sizeof(meseta));
for (const auto& item : this->items) {
phosg::fwritex(f, &item, sizeof(item));
}
}
uint32_t PlayerBank::bb_checksum() const {
le_uint32_t num_items = this->items.size();
le_uint32_t meseta = this->meseta;
uint32_t ret = phosg::crc32(&num_items, sizeof(num_items));
ret = phosg::crc32(&meseta, sizeof(meseta), ret);
for (const auto& item : this->items) {
ret = phosg::crc32(&item, sizeof(item), ret);
}
return ret;
}
void PlayerBank::add_item(const ItemData& item, const ItemData::StackLimits& limits) {
uint32_t primary_identifier = item.primary_identifier();
if (primary_identifier == 0x04000000) {
this->meseta += item.data2d;
if (this->meseta > this->max_meseta) {
this->meseta = this->max_meseta;
}
return;
}
size_t combine_max = item.max_stack_size(limits);
if (combine_max > 1) {
size_t y;
for (y = 0; y < this->items.size(); y++) {
if (this->items[y].data.primary_identifier() == primary_identifier) {
break;
}
}
if (y < this->items.size()) {
uint8_t new_count = this->items[y].data.data1[5] + item.data1[5];
if (new_count > combine_max) {
throw std::runtime_error("stack size would exceed limit");
}
this->items[y].data.data1[5] = new_count;
this->items[y].amount = new_count;
return;
}
}
if (this->items.size() >= this->max_items) {
throw std::runtime_error("no free space in bank");
}
auto& new_item = this->items.emplace_back();
new_item.data = item;
new_item.amount = (item.max_stack_size(limits) > 1) ? item.data1[5] : 1;
new_item.present = 1;
}
ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) {
size_t index = this->find_item(item_id);
auto& bank_item = this->items[index];
ItemData ret = bank_item.data;
if (amount && (bank_item.data.stack_size(limits) > 1) && (amount < bank_item.data.data1[5])) {
ret.data1[5] = amount;
bank_item.data.data1[5] -= amount;
bank_item.amount -= amount;
} else {
this->items.erase(this->items.begin() + index);
}
return ret;
}
size_t PlayerBank::find_item(uint32_t item_id) {
for (size_t x = 0; x < this->items.size(); x++) {
if (this->items[x].data.id == item_id) {
return x;
}
}
throw std::out_of_range("item not present");
}
void PlayerBank::sort() {
std::sort(this->items.begin(), this->items.end());
}
void PlayerBank::assign_ids(uint32_t base_id) {
for (size_t z = 0; z < this->items.size(); z++) {
this->items[z].data.id = base_id + z;
}
}
void PlayerBank::enforce_stack_limits(std::shared_ptr<const ItemData::StackLimits> stack_limits) {
for (auto& item : this->items) {
item.data.enforce_stack_size_limits(*stack_limits);
}
}
+53 -89
View File
@@ -77,6 +77,10 @@ struct PlayerInventoryItemT {
ret.data.id.store_raw(phosg::bswap32(ret.data.id.load_raw()));
return ret;
}
bool is_equipped() const {
return (this->flags & 8);
}
} __attribute__((packed));
using PlayerInventoryItem = PlayerInventoryItemT<false>;
using PlayerInventoryItemBE = PlayerInventoryItemT<true>;
@@ -283,6 +287,12 @@ struct PlayerInventoryT {
}
}
void enforce_stack_limits(std::shared_ptr<const ItemData::StackLimits> stack_limits) {
for (size_t z = 0; z < std::min<uint8_t>(this->num_items, this->items.size()); z++) {
this->items[z].data.enforce_stack_size_limits(*stack_limits);
}
}
operator PlayerInventoryT<!BE>() const {
PlayerInventoryT<!BE> ret;
ret.num_items = this->num_items;
@@ -305,95 +315,6 @@ struct PlayerBankT {
/* 0008 */ parray<PlayerBankItemT<BE>, SlotCount> items;
/* 05A8 for 60 items (v1/v2), 12C8 for 200 items (v3/v4) */
uint32_t checksum() const {
return phosg::crc32(this, 2 * sizeof(U32T<BE>) + sizeof(PlayerBankItemT<BE>) * std::min<size_t>(SlotCount, this->num_items));
}
void add_item(const ItemData& item, const ItemData::StackLimits& limits) {
uint32_t primary_identifier = item.primary_identifier();
if (primary_identifier == 0x04000000) {
this->meseta += item.data2d;
if (this->meseta > 999999) {
this->meseta = 999999;
}
return;
}
size_t combine_max = item.max_stack_size(limits);
if (combine_max > 1) {
size_t y;
for (y = 0; y < this->num_items; y++) {
if (this->items[y].data.primary_identifier() == primary_identifier) {
break;
}
}
if (y < this->num_items) {
uint8_t new_count = this->items[y].data.data1[5] + item.data1[5];
if (new_count > combine_max) {
throw std::runtime_error("stack size would exceed limit");
}
this->items[y].data.data1[5] = new_count;
this->items[y].amount = new_count;
return;
}
}
if (this->num_items >= SlotCount) {
throw std::runtime_error("no free space in bank");
}
auto& last_item = this->items[this->num_items];
last_item.data = item;
last_item.amount = (item.max_stack_size(limits) > 1) ? item.data1[5] : 1;
last_item.present = 1;
this->num_items++;
}
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) {
size_t index = this->find_item(item_id);
auto& bank_item = this->items[index];
ItemData ret;
if (amount && (bank_item.data.stack_size(limits) > 1) && (amount < bank_item.data.data1[5])) {
ret = bank_item.data;
ret.data1[5] = amount;
bank_item.data.data1[5] -= amount;
bank_item.amount -= amount;
return ret;
}
ret = bank_item.data;
this->num_items--;
for (size_t x = index; x < this->num_items; x++) {
this->items[x] = this->items[x + 1];
}
auto& last_item = this->items[this->num_items];
last_item.amount = 0;
last_item.present = 0;
last_item.data.clear();
return ret;
}
size_t find_item(uint32_t item_id) {
for (size_t x = 0; x < this->num_items; x++) {
if (this->items[x].data.id == item_id) {
return x;
}
}
throw std::out_of_range("item not present");
}
void sort() {
std::sort(this->items.data(), this->items.data() + this->num_items);
}
void assign_ids(uint32_t base_id) {
for (size_t z = 0; z < this->num_items; z++) {
this->items[z].data.id = base_id + z;
}
}
void decode_from_client(Version v) {
for (size_t z = 0; z < this->items.size(); z++) {
this->items[z].data.decode_for_version(v);
@@ -423,3 +344,46 @@ using PlayerBank200BE = PlayerBankT<200, true>;
check_struct_size(PlayerBank60, 0x05A8);
check_struct_size(PlayerBank200, 0x12C8);
check_struct_size(PlayerBank200BE, 0x12C8);
struct PlayerBank {
uint32_t max_meseta = 999999;
uint32_t max_items = 200;
uint32_t meseta = 0;
std::vector<PlayerBankItem> items;
PlayerBank() = default;
template <size_t SrcSlotCount, bool SrcBE>
PlayerBank(const PlayerBankT<SrcSlotCount, SrcBE>& src)
: max_meseta(999999), max_items(SrcSlotCount), meseta(src.meseta) {
this->items.reserve(src.num_items);
for (size_t z = 0; z < src.num_items; z++) {
this->items.emplace_back(src.items[z]);
}
}
template <size_t DestSlotCount, bool DestBE>
operator PlayerBankT<DestSlotCount, DestBE>() const {
PlayerBankT<DestSlotCount, DestBE> ret;
ret.num_items = std::min<size_t>(ret.items.size(), this->items.size());
ret.meseta = this->meseta;
for (size_t z = 0; z < ret.num_items; z++) {
ret.items[z] = this->items[z];
}
return ret;
}
void load(FILE* f);
void save(FILE* f) const;
uint32_t bb_checksum() const;
void add_item(const ItemData& item, const ItemData::StackLimits& limits);
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits);
size_t find_item(uint32_t item_id);
void sort();
void assign_ids(uint32_t base_id);
void enforce_stack_limits(std::shared_ptr<const ItemData::StackLimits> stack_limits);
};
+83 -1
View File
@@ -32,7 +32,21 @@ struct PlayerVisualConfigT {
/* 10 */ parray<uint8_t, 8> unknown_a2;
/* 18 */ U32T<BE> name_color = 0xFFFFFFFF; // ARGB
/* 1C */ uint8_t extra_model = 0;
/* 1D */ parray<uint8_t, 0x0F> unused;
// Some NPCs can crash the client if the character's class is incorrect. To
// handle this, we save the affected fields in the unused bytes after
// extra_model. This is a newserv-specific extension; it appears the
// following 15 bytes were simply never used by Sega.
/* 1D */ uint8_t npc_saved_data_type = 0;
/* 1E */ uint8_t npc_saved_costume = 0;
/* 1F */ uint8_t npc_saved_skin = 0;
/* 20 */ uint8_t npc_saved_face = 0;
/* 21 */ uint8_t npc_saved_head = 0;
/* 22 */ uint8_t npc_saved_hair = 0;
/* 23 */ uint8_t npc_saved_hair_r = 0;
/* 24 */ uint8_t npc_saved_hair_g = 0;
/* 25 */ uint8_t npc_saved_hair_b = 0;
/* 26 */ parray<uint8_t, 2> unused;
/* 28 */ F32T<BE> npc_saved_proportion_y = 0.0;
// See compute_name_color_checksum for details on how this is computed. If the
// value is incorrect, V1 and V2 will ignore the name_color field and use the
// default color instead. This field is ignored on GC; on BB (and presumably
@@ -81,6 +95,72 @@ struct PlayerVisualConfigT {
this->name_color_checksum = this->compute_name_color_checksum(this->name_color);
}
void backup_npc_saved_fields() {
if (this->npc_saved_data_type == 0x8E) {
return;
}
// Restore old-format data if needed before backing up again
this->restore_npc_saved_fields();
this->npc_saved_data_type = 0x8E;
this->npc_saved_costume = this->costume;
this->npc_saved_skin = this->skin;
this->npc_saved_face = this->face;
this->npc_saved_head = this->head;
this->npc_saved_hair = this->hair;
this->npc_saved_hair_r = this->hair_r;
this->npc_saved_hair_g = this->hair_g;
this->npc_saved_hair_b = this->hair_b;
this->npc_saved_proportion_y = this->proportion_y;
this->costume = 0;
this->skin = 0;
this->face = 0;
this->head = 0;
this->hair = 0;
this->hair_r = 0;
this->hair_g = 0;
this->hair_b = 0;
this->proportion_y = 0;
}
void restore_npc_saved_fields() {
switch (this->npc_saved_data_type) {
case 0x00:
break;
case 0x8D: // Old format
this->char_class = this->npc_saved_costume;
this->head = this->npc_saved_skin;
this->hair = this->npc_saved_face;
break;
case 0x8E: // New format
this->costume = this->npc_saved_costume;
this->skin = this->npc_saved_skin;
this->face = this->npc_saved_face;
this->head = this->npc_saved_head;
this->hair = this->npc_saved_hair;
this->hair_r = this->npc_saved_hair_r;
this->hair_g = this->npc_saved_hair_g;
this->hair_b = this->npc_saved_hair_b;
this->proportion_y = this->npc_saved_proportion_y;
break;
default:
throw std::runtime_error("unknown saved NPC data format");
}
this->npc_saved_data_type = 0;
this->npc_saved_costume = 0;
this->npc_saved_skin = 0;
this->npc_saved_face = 0;
this->npc_saved_head = 0;
this->npc_saved_hair = 0;
this->npc_saved_hair_r = 0;
this->npc_saved_hair_g = 0;
this->npc_saved_hair_b = 0;
this->unused.clear(0);
this->npc_saved_proportion_y = 0.0;
}
void enforce_lobby_join_limits_for_version(Version v) {
struct ClassMaxes {
uint16_t costume;
@@ -483,6 +563,8 @@ struct XBNetworkLocation {
/* 04 */ le_uint32_t external_ipv4_address = 0x23232323;
/* 08 */ le_uint16_t port = 9500;
/* 0A */ parray<uint8_t, 6> mac_address = 0x77;
// The remainder of this struct appears to be private/opaque in the XDK (and
// newserv doesn't use it either)
/* 10 */ le_uint32_t sg_ip_address = 0x0B0B0B0B;
/* 14 */ le_uint32_t spi = 0xCCCCCCCC;
/* 18 */ le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
+133 -45
View File
@@ -236,7 +236,7 @@ static asio::awaitable<HandlerResult> S_G_9A(shared_ptr<Client> c, Channel::Mess
// right after the client config data
c->proxy_session->server_channel->send(
0x9E, 0x01, &cmd,
cmd.is_extended ? sizeof(C_LoginExtended_GC_9E) : sizeof(C_Login_GC_9E));
cmd.is_extended ? sizeof(C_LoginExtended_GC_9E) : sizeof(C_Login_PC_GC_9E));
co_return HandlerResult::SUPPRESS;
}
@@ -356,11 +356,11 @@ static asio::awaitable<HandlerResult> S_B_03(shared_ptr<Client> c, Channel::Mess
resp.character_slot = c->bb_character_index;
resp.connection_phase = c->bb_connection_phase;
resp.client_code = c->bb_client_code;
resp.security_token = c->proxy_session->remote_bb_security_token;
resp.security_token = c->bb_security_token;
resp.username.encode(c->username, c->language());
resp.password.encode(c->password, c->language());
resp.hardware_id = c->hardware_id;
resp.client_config = c->proxy_session->remote_client_config_data;
resp.client_config = c->bb_client_config;
if (c->proxy_session->enable_remote_ip_crc_patch) {
*reinterpret_cast<le_uint32_t*>(resp.client_config.data() + 0x10) =
c->proxy_session->remote_ip_crc ^ (1309539928UL + 1248334810UL);
@@ -373,27 +373,17 @@ static asio::awaitable<HandlerResult> S_B_03(shared_ptr<Client> c, Channel::Mess
static asio::awaitable<HandlerResult> S_B_E6(shared_ptr<Client> c, Channel::Message& msg) {
const auto& cmd = msg.check_size_t<S_ClientInit_BB_00E6>(0xFFFF);
c->proxy_session->remote_guild_card_number = cmd.guild_card_number;
c->proxy_session->remote_bb_security_token = cmd.security_token;
c->proxy_session->remote_client_config_data = cmd.client_config;
c->bb_security_token = cmd.security_token;
c->bb_client_config = cmd.client_config;
auto s = c->require_server_state();
auto& pc = s->proxy_persistent_configs[c->login->account->account_id];
pc.account_id = c->login->account->account_id;
pc.remote_guild_card_number = c->proxy_session->remote_guild_card_number;
pc.remote_bb_security_token = c->proxy_session->remote_bb_security_token;
pc.remote_client_config_data = c->proxy_session->remote_client_config_data;
pc.enable_remote_ip_crc_patch = c->proxy_session->enable_remote_ip_crc_patch;
c->log.info_f("Updated persistent config for proxy session");
if ((c->bb_connection_phase == 0) && c->proxy_session->received_reconnect) {
c->proxy_session->server_channel->send(0x00E0); // Request system file
}
co_return HandlerResult::SUPPRESS;
}
static asio::awaitable<HandlerResult> C_B_E0(shared_ptr<Client>, Channel::Message&) {
co_return HandlerResult::SUPPRESS;
co_return HandlerResult::FORWARD;
}
static asio::awaitable<HandlerResult> S_V123_04(shared_ptr<Client> c, Channel::Message& msg) {
@@ -434,10 +424,7 @@ static asio::awaitable<HandlerResult> S_V123_04(shared_ptr<Client> c, Channel::M
// If there was previously a guild card number, assume we got the lobby server
// init text instead of the port map init text.
memcpy(c->proxy_session->remote_client_config_data.data(),
had_guild_card_number
? "t Lobby Server. Copyright SEGA E"
: "t Port Map. Copyright SEGA Enter",
0x20);
had_guild_card_number ? "t Lobby Server. Copyright SEGA E" : "t Port Map. Copyright SEGA Enter", 0x20);
memcpy(c->proxy_session->remote_client_config_data.data(), &cmd.client_config,
min<size_t>(msg.data.size() - offsetof(S_UpdateClientConfig_V3_04, client_config),
c->proxy_session->remote_client_config_data.bytes()));
@@ -688,6 +675,12 @@ static asio::awaitable<HandlerResult> C_B3(shared_ptr<Client> c, Channel::Messag
}
}
static asio::awaitable<HandlerResult> C_B_E0(shared_ptr<Client> c, Channel::Message&) {
auto ret = c->proxy_session->bb_client_sent_E0 ? HandlerResult::FORWARD : HandlerResult::SUPPRESS;
c->proxy_session->bb_client_sent_E0 = true;
co_return ret;
}
static asio::awaitable<HandlerResult> S_B_E2(shared_ptr<Client> c, Channel::Message& msg) {
if (c->check_flag(Client::Flag::PROXY_SAVE_FILES)) {
string output_filename = std::format("system.{}.psosys", phosg::now());
@@ -748,18 +741,18 @@ static asio::awaitable<HandlerResult> S_G_E4(shared_ptr<Client> c, Channel::Mess
static asio::awaitable<HandlerResult> S_B_22(shared_ptr<Client> c, Channel::Message& msg) {
// We use this command (which is sent before the init encryption command) to
// detect a particular server behavior that we'll have to work around later.
// It looks like this command's existence is another anti-proxy measure, since
// It looks like this command's existence is an anti-proxy measure, since
// this command is 0x34 bytes in total, and the logic that adds padding bytes
// when the command size isn't a multiple of 8 is only active when encryption
// is enabled. Presumably some simpler proxies would get this wrong.
// Editor's note: There's an unsavory message in this command's data field,
// hence the hash here instead of a direct string comparison. I'd love to hear
// the story behind why they put that string there.
// hence the hash here instead of a direct string comparison. I'd love to
// hear the story behind why they put that string there.
if ((msg.data.size() == 0x2C) && (phosg::fnv1a64(msg.data.data(), msg.data.size()) == 0x8AF8314316A27994)) {
c->log.info_f("Enabling remote IP CRC patch");
c->proxy_session->enable_remote_ip_crc_patch = true;
}
co_return HandlerResult::FORWARD;
co_return HandlerResult::SUPPRESS;
}
static asio::awaitable<HandlerResult> S_19_U_14(shared_ptr<Client> c, Channel::Message& msg) {
@@ -789,6 +782,9 @@ static asio::awaitable<HandlerResult> S_19_U_14(shared_ptr<Client> c, Channel::M
if (is_patch(c->version())) {
auto& cmd = msg.check_size_t<S_Reconnect_Patch_14>();
new_ep = make_endpoint_ipv4(cmd.address, cmd.port);
} else if (msg.flag == 6 && msg.data.size() >= sizeof(S_ReconnectIPv6_Extension_19)) {
auto& cmd = msg.check_size_t<S_ReconnectIPv6_Extension_19>(0xFFFF);
new_ep = make_endpoint_ipv6(cmd.address.data(), cmd.port);
} else {
// This weird maximum size is here to properly handle the version-split
// command that some servers (including newserv) use on port 9100
@@ -848,13 +844,12 @@ static asio::awaitable<HandlerResult> SC_6x60_6xA2(shared_ptr<Client> c, Channel
co_return HandlerResult::FORWARD;
}
using DropMode = ProxySession::DropMode;
switch (c->proxy_session->drop_mode) {
case DropMode::DISABLED:
case ProxyDropMode::DISABLED:
co_return HandlerResult::SUPPRESS;
case DropMode::PASSTHROUGH:
case ProxyDropMode::PASSTHROUGH:
co_return HandlerResult::FORWARD;
case DropMode::INTERCEPT:
case ProxyDropMode::INTERCEPT:
break;
default:
throw logic_error("invalid drop mode");
@@ -894,7 +889,7 @@ static asio::awaitable<HandlerResult> SC_6x60_6xA2(shared_ptr<Client> c, Channel
c->log.info_f("No item was created");
} else {
auto s = c->require_server_state();
string name = s->describe_item(c->version(), res.item, false);
string name = s->describe_item(c->version(), res.item);
c->log.info_f("Entity {:04X} (area {:02X}) created item {}", cmd.entity_index, cmd.effective_area, name);
res.item.id = c->proxy_session->next_item_id++;
c->log.info_f("Creating item {:08X} at {:02X}:{:g},{:g} for all clients",
@@ -920,10 +915,43 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
c->log.warning_f("Blocking invalid subcommand from server");
co_return HandlerResult::SUPPRESS;
case 0x16:
case 0x84: {
const auto& cmd = msg.check_size_t<G_VolOptBossActions_6x16>(0xFFFF);
if (cmd.entity_index_count > 6) {
c->log.warning_f("Blocking subcommand 6x16/6x84 with invalid entity index count");
co_return HandlerResult::SUPPRESS;
}
for (size_t z = 0; z < cmd.entity_index_table.size(); z++) {
if (cmd.entity_index_table[z] >= 6) {
c->log.warning_f("Blocking subcommand 6x16/6x84 with invalid entity index");
co_return HandlerResult::SUPPRESS;
}
}
break;
}
case 0x17: {
const auto& cmd = msg.check_size_t<G_SetEntityPositionAndAngle_6x17>();
if (cmd.header.entity_id == c->lobby_client_id) {
c->log.warning_f("Blocking subcommand 6x17 targeting local client");
co_return HandlerResult::SUPPRESS;
}
break;
}
case 0x2F: {
const auto& cmd = msg.check_size_t<G_ChangePlayerHP_6x2F>();
if (cmd.client_id == c->lobby_client_id) {
c->log.warning_f("Blocking subcommand 6x2F targeting local player");
co_return HandlerResult::SUPPRESS;
}
break;
}
case 0x46: {
const auto& cmd = msg.check_size_t<G_AttackFinished_6x46>(
offsetof(G_AttackFinished_6x46, targets), sizeof(G_AttackFinished_6x46));
if (cmd.target_count > min<size_t>(cmd.header.size - 2, cmd.targets.size())) {
const auto& header = msg.check_size_t<G_AttackFinished_Header_6x46>(0xFFFF);
if (header.target_count > min<size_t>(header.header.size - sizeof(G_AttackFinished_Header_6x46) / 4, 10)) {
c->log.warning_f("Blocking subcommand 6x46 with invalid count");
co_return HandlerResult::SUPPRESS;
}
@@ -931,9 +959,8 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
}
case 0x47: {
const auto& cmd = msg.check_size_t<G_CastTechnique_6x47>(
offsetof(G_CastTechnique_6x47, targets), sizeof(G_CastTechnique_6x47));
if (cmd.target_count > min<size_t>(cmd.header.size - 2, cmd.targets.size())) {
const auto& header = msg.check_size_t<G_CastTechnique_Header_6x47>(0xFFFF);
if (header.target_count > min<size_t>(header.header.size - sizeof(G_CastTechnique_Header_6x47) / 4, 10)) {
c->log.warning_f("Blocking subcommand 6x47 with invalid count");
co_return HandlerResult::SUPPRESS;
}
@@ -941,9 +968,8 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
}
case 0x49: {
const auto& cmd = msg.check_size_t<G_ExecutePhotonBlast_6x49>(
offsetof(G_ExecutePhotonBlast_6x49, targets), sizeof(G_ExecutePhotonBlast_6x49));
if (cmd.target_count > min<size_t>(cmd.header.size - 3, cmd.targets.size())) {
const auto& header = msg.check_size_t<G_ExecutePhotonBlast_Header_6x49>(0xFFFF);
if (header.target_count > min<size_t>(header.header.size - sizeof(G_ExecutePhotonBlast_Header_6x49) / 4, 10)) {
c->log.warning_f("Blocking subcommand 6x49 with invalid count");
co_return HandlerResult::SUPPRESS;
}
@@ -962,6 +988,42 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
case 0xA2:
co_return co_await SC_6x60_6xA2(c, msg);
case 0x6A: {
auto& cmd = msg.check_size_t<G_SetBossWarpFlags_6x6A>();
if (c->proxy_session->map_state) {
shared_ptr<MapState::ObjectState> obj_st;
try {
obj_st = c->proxy_session->map_state->object_state_for_index(c->version(), c->floor, cmd.header.entity_id - 0x4000);
} catch (const exception& e) {
c->log.warning_f("Invalid object reference ({})", e.what());
}
if (!obj_st || !obj_st->super_obj) {
c->log.warning_f("Blocking subcommand 6x6A with missing object");
co_return HandlerResult::SUPPRESS;
}
auto set_entry = obj_st->super_obj->version(c->version()).set_entry;
if (!set_entry) {
c->log.warning_f("Blocking subcommand 6x6A with missing set entry");
co_return HandlerResult::SUPPRESS;
}
if (set_entry->base_type != 0x0019 && set_entry->base_type != 0x0055) {
c->log.warning_f("Blocking subcommand 6x6A with incorrect object type");
co_return HandlerResult::SUPPRESS;
}
}
break;
}
case 0x7D: {
const auto& cmd = msg.check_size_t<G_SetBattleModeData_6x7D>();
if ((cmd.what == 3 || cmd.what == 4) && cmd.params[0] >= 4) {
c->log.warning_f("Blocking subcommand 6x7D with invalid client ID");
co_return HandlerResult::SUPPRESS;
}
break;
}
case 0xB3:
case 0xB4:
case 0xB5: {
@@ -1382,6 +1444,14 @@ static asio::awaitable<HandlerResult> S_G_B9(shared_ptr<Client> c, Channel::Mess
co_return (c->version() == Version::GC_EP3) ? HandlerResult::FORWARD : HandlerResult::SUPPRESS;
}
static asio::awaitable<HandlerResult> C_G_B9(shared_ptr<Client> c, Channel::Message&) {
if (c->proxy_session->suppress_next_ep3_media_update_confirmation) {
c->proxy_session->suppress_next_ep3_media_update_confirmation = false;
co_return HandlerResult::SUPPRESS;
}
co_return HandlerResult::FORWARD;
}
static asio::awaitable<HandlerResult> S_G_EF(shared_ptr<Client> c, Channel::Message& msg) {
if (is_ep3(c->version())) {
if (c->check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED)) {
@@ -1557,7 +1627,7 @@ template <typename CmdT>
static asio::awaitable<HandlerResult> S_64(shared_ptr<Client> c, Channel::Message& msg) {
CmdT* cmd;
S_JoinGame_Ep3_64* cmd_ep3 = nullptr;
if (c->sub_version >= 0x40) {
if ((c->sub_version >= 0x40) && is_v3(c->version())) {
cmd = &msg.check_size_t<CmdT>(sizeof(S_JoinGame_Ep3_64));
cmd_ep3 = &msg.check_size_t<S_JoinGame_Ep3_64>();
} else if (c->version() == Version::XB_V3) {
@@ -1605,7 +1675,7 @@ static asio::awaitable<HandlerResult> S_64(shared_ptr<Client> c, Channel::Messag
} else {
c->proxy_session->lobby_event = 0;
c->proxy_session->lobby_difficulty = 0;
c->proxy_session->lobby_section_id = c->character()->disp.visual.section_id;
c->proxy_session->lobby_section_id = c->character_file()->disp.visual.section_id;
c->proxy_session->lobby_mode = GameMode::NORMAL;
c->proxy_session->lobby_random_seed = phosg::random_object<uint32_t>();
}
@@ -1936,9 +2006,9 @@ asio::awaitable<HandlerResult> C_6x(shared_ptr<Client> c, Channel::Message& msg)
case 0x4A:
case 0x4B:
case 0x4C:
if (!is_v1(c->version()) && c->check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
send_player_stats_change(c->channel, c->lobby_client_id, PlayerStatsChange::ADD_HP, 2550);
send_player_stats_change(c->proxy_session->server_channel, c->lobby_client_id, PlayerStatsChange::ADD_HP, 2550);
if (c->check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
send_change_player_hp(c->channel, c->lobby_client_id, PlayerHPChange::MAXIMIZE_HP, 0);
send_change_player_hp(c->proxy_session->server_channel, c->lobby_client_id, PlayerHPChange::MAXIMIZE_HP, 0);
}
break;
@@ -1965,6 +2035,20 @@ asio::awaitable<HandlerResult> C_6x(shared_ptr<Client> c, Channel::Message& msg)
}
break;
case 0x4E: {
if (c->check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
if (is_v1_or_v2(c->version()) && (c->version() != Version::GC_NTE)) {
G_UseMedicalCenter_6x31 cmd = {0x31, 0x01, c->lobby_client_id};
send_command_t(c->channel, 0x60, 0x00, cmd);
send_command_t(c->proxy_session->server_channel, 0x60, 0x00, cmd);
} else {
G_RevivePlayer_V3_BB_6xA1 cmd = {0xA1, 0x01, c->lobby_client_id};
co_await send_protected_command(c, &cmd, sizeof(cmd), true);
}
}
break;
}
case 0x5F:
send_item_notification_if_needed(
c, msg.check_size_t<G_DropItem_DC_6x5F>(sizeof(G_DropItem_PC_V3_BB_6x5F)).item.item, true);
@@ -2207,7 +2291,7 @@ static on_message_t handlers[0x100][NUM_VERSIONS][2] = {
/* B6 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* B7 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_B7, nullptr}, {S_G_B7, nullptr}, {S_G_B7, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* B8 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_B8, nullptr}, {S_G_B8, nullptr}, {S_G_B8, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
/* B9 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_B9, nullptr}, {S_G_B9, nullptr}, {S_G_B9, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* B9 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_B9, C_G_B9}, {S_G_B9, C_G_B9}, {S_G_B9, C_G_B9}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* BA */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_BA, nullptr}, {S_G_BA, nullptr}, {S_G_BA, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* BB */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* BC */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
@@ -2317,7 +2401,8 @@ asio::awaitable<void> on_proxy_command(shared_ptr<Client> c, bool from_server, u
}
}
asio::awaitable<void> handle_proxy_server_commands(shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) {
asio::awaitable<void> handle_proxy_server_commands(
shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) {
std::string error_str;
// server_channel can be changed by receiving a 19 command, hence the
// exception handler is inside the loop here
@@ -2326,6 +2411,9 @@ asio::awaitable<void> handle_proxy_server_commands(shared_ptr<Client> c, shared_
try {
msg = make_unique<Channel::Message>(co_await channel->recv());
if (c->proxy_session == ses) {
for (size_t z = 0; z < std::min<size_t>(c->proxy_session->prev_server_command_bytes.size(), msg->data.size()); z++) {
c->proxy_session->prev_server_command_bytes[z] = msg->data[z];
}
asio::co_spawn(co_await asio::this_coro::executor, on_proxy_command(c, true, std::move(msg)), asio::detached);
}
} catch (const std::system_error& e) {
+5 -40
View File
@@ -10,8 +10,6 @@ ProxySession::ProxySession(shared_ptr<Channel> server_channel, const PersistentC
: server_channel(server_channel) {
if (pc) {
this->remote_guild_card_number = pc->remote_guild_card_number;
this->remote_bb_security_token = pc->remote_bb_security_token;
this->remote_client_config_data = pc->remote_client_config_data;
this->enable_remote_ip_crc_patch = pc->enable_remote_ip_crc_patch;
} else if (is_v4(this->server_channel->version)) {
this->remote_guild_card_number = 0;
@@ -23,47 +21,14 @@ ProxySession::~ProxySession() {
this->num_proxy_sessions--;
}
void ProxySession::set_drop_mode(shared_ptr<ServerState> s, Version version, int64_t override_random_seed, DropMode new_mode) {
void ProxySession::set_drop_mode(
shared_ptr<ServerState> s, Version version, int64_t override_random_seed, ProxyDropMode new_mode) {
this->drop_mode = new_mode;
if (this->drop_mode == DropMode::INTERCEPT) {
shared_ptr<const RareItemSet> rare_item_set;
shared_ptr<const CommonItemSet> common_item_set;
switch (version) {
case Version::PC_PATCH:
case Version::BB_PATCH:
case Version::GC_EP3_NTE:
case Version::GC_EP3:
throw runtime_error("cannot create item creator for this base version");
case Version::DC_NTE:
case Version::DC_11_2000:
case Version::DC_V1:
// TODO: We should probably have a v1 common item set at some point too
common_item_set = s->common_item_set_v2;
rare_item_set = s->rare_item_sets.at("rare-table-v1");
break;
case Version::DC_V2:
case Version::PC_NTE:
case Version::PC_V2:
common_item_set = s->common_item_set_v2;
rare_item_set = s->rare_item_sets.at("rare-table-v2");
break;
case Version::GC_NTE:
case Version::GC_V3:
case Version::XB_V3:
common_item_set = s->common_item_set_v3_v4;
rare_item_set = s->rare_item_sets.at("rare-table-v3");
break;
case Version::BB_V4:
common_item_set = s->common_item_set_v3_v4;
rare_item_set = s->rare_item_sets.at("rare-table-v4");
break;
default:
throw logic_error("invalid lobby base version");
}
if (this->drop_mode == ProxyDropMode::INTERCEPT) {
auto rand_crypt = make_shared<MT19937Generator>((override_random_seed >= 0) ? override_random_seed : this->lobby_random_seed);
this->item_creator = make_shared<ItemCreator>(
common_item_set,
rare_item_set,
s->common_item_set(version, nullptr),
s->rare_item_set(version, nullptr),
s->armor_random_set,
s->tool_random_set,
s->weapon_random_sets.at(this->lobby_difficulty),
+4 -10
View File
@@ -22,6 +22,7 @@ struct ProxySession {
uint32_t remote_ip_crc = 0;
bool received_reconnect = false;
bool enable_remote_ip_crc_patch = false;
bool bb_client_sent_E0 = false;
struct LobbyPlayer {
uint32_t guild_card_number = 0;
@@ -42,17 +43,12 @@ struct ProxySession {
Episode lobby_episode = Episode::EP1;
uint32_t lobby_random_seed = 0;
uint64_t server_ping_start_time = 0;
bool suppress_next_ep3_media_update_confirmation = false;
int64_t remote_guild_card_number = -1;
uint32_t remote_bb_security_token = 0;
parray<uint8_t, 0x28> remote_client_config_data;
enum class DropMode {
DISABLED = 0,
PASSTHROUGH,
INTERCEPT,
};
DropMode drop_mode = DropMode::PASSTHROUGH;
ProxyDropMode drop_mode = ProxyDropMode::PASSTHROUGH;
std::shared_ptr<std::string> quest_dat_data;
std::shared_ptr<ItemCreator> item_creator;
std::shared_ptr<MapState> map_state;
@@ -64,8 +60,6 @@ struct ProxySession {
struct PersistentConfig {
uint32_t account_id;
uint32_t remote_guild_card_number;
uint32_t remote_bb_security_token;
parray<uint8_t, 0x28> remote_client_config_data;
bool enable_remote_ip_crc_patch;
std::unique_ptr<asio::steady_timer> expire_timer;
};
@@ -82,7 +76,7 @@ struct ProxySession {
};
std::unordered_map<std::string, SavingFile> saving_files;
void set_drop_mode(std::shared_ptr<ServerState> s, Version version, int64_t override_random_seed, DropMode new_mode);
void set_drop_mode(std::shared_ptr<ServerState> s, Version version, int64_t override_random_seed, ProxyDropMode new_mode);
void clear_lobby_players(size_t num_slots);
};
+164 -344
View File
@@ -194,10 +194,10 @@ struct PSODownloadQuestHeader {
} __packed_ws__(PSODownloadQuestHeader, 8);
void VersionedQuest::assert_valid() const {
if (this->category_id == 0xFFFFFFFF) {
if (this->meta.category_id == 0xFFFFFFFF) {
throw runtime_error("category ID is not set");
}
if (this->quest_number == 0xFFFFFFFF) {
if (this->meta.quest_number == 0xFFFFFFFF) {
throw runtime_error("quest number is not set");
}
if (this->version == Version::UNKNOWN) {
@@ -206,81 +206,107 @@ void VersionedQuest::assert_valid() const {
if (this->language == 0xFF) {
throw runtime_error("language is not set");
}
if (this->episode == Episode::NONE) {
throw runtime_error("episode is not set");
switch (this->meta.episode) {
case Episode::EP1:
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
uint8_t area = this->meta.area_for_floor[floor];
if (area >= 0x12) {
throw runtime_error("Episode 1 quest specifies invalid area");
}
}
break;
case Episode::EP2:
if (is_v1_or_v2(this->version)) {
throw runtime_error("v1 or v2 quest specifies Episode 2");
}
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
uint8_t area = this->meta.area_for_floor[floor];
if ((area < 0x12) || (area >= 0x24)) {
throw runtime_error("Episode 2 quest specifies invalid area");
}
}
break;
case Episode::EP3:
if (!is_ep3(this->version)) {
throw runtime_error("non-Ep3 quest specifies Episode 3");
}
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
if (this->meta.area_for_floor[floor] != 0xFF) {
throw runtime_error("Episode 3 quest specifies floor overrides");
}
}
break;
case Episode::EP4:
if (!is_v4(this->version)) {
throw runtime_error("non-v4 quest specifies Episode 4");
}
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
uint8_t area = this->meta.area_for_floor[floor];
if (area != 0xFF && (area < 0x24 || area >= 0x2F)) {
throw runtime_error("Episode 4 quest specifies invalid floor");
}
}
break;
case Episode::NONE:
throw runtime_error("episode is not set");
default:
throw runtime_error("episode is not valid");
}
if (this->max_players == 0) {
if (this->meta.max_players == 0) {
throw runtime_error("max players is not set");
}
if (!this->bin_contents) {
throw runtime_error("bin file is missing");
}
if (!is_ep3(this->version) && !this->dat_contents) {
if (!this->dat_contents) {
throw runtime_error("dat file is missing");
}
if (!is_ep3(this->version) && !this->map_file) {
if (!this->map_file) {
throw runtime_error("parsed map file is missing");
}
if (this->meta.common_item_set_name.empty() != !this->meta.common_item_set) {
throw runtime_error("common item set name/pointer mismatch");
}
if (this->meta.rare_item_set_name.empty() != !this->meta.rare_item_set) {
throw runtime_error("rare item set name/pointer mismatch");
}
if (this->meta.allowed_drop_modes &&
!(this->meta.allowed_drop_modes & (1 << static_cast<size_t>(this->meta.default_drop_mode)))) {
throw runtime_error("default drop mode is not allowed");
}
}
string VersionedQuest::bin_filename() const {
if (this->episode == Episode::EP3) {
return std::format("m{:06}p_e.bin", this->quest_number);
} else {
return std::format("quest{}.bin", this->quest_number);
}
return std::format("quest{}.bin", this->meta.quest_number);
}
string VersionedQuest::dat_filename() const {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have .dat files");
} else {
return std::format("quest{}.dat", this->quest_number);
}
return std::format("quest{}.dat", this->meta.quest_number);
}
string VersionedQuest::pvr_filename() const {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have .pvr files");
} else {
return std::format("quest{}.pvr", this->quest_number);
}
return std::format("quest{}.pvr", this->meta.quest_number);
}
string VersionedQuest::xb_filename() const {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have Xbox filenames");
} else {
return std::format("quest{}_{}.dat", this->quest_number, static_cast<char>(tolower(char_for_language_code(this->language))));
}
return std::format("quest{}_{}.dat",
this->meta.quest_number, static_cast<char>(tolower(char_for_language_code(this->language))));
}
string VersionedQuest::encode_qst() const {
unordered_map<string, shared_ptr<const string>> files;
files.emplace(std::format("quest{}.bin", this->quest_number), this->bin_contents);
files.emplace(std::format("quest{}.dat", this->quest_number), this->dat_contents);
files.emplace(std::format("quest{}.bin", this->meta.quest_number), this->bin_contents);
files.emplace(std::format("quest{}.dat", this->meta.quest_number), this->dat_contents);
if (this->pvr_contents) {
files.emplace(std::format("quest{}.pvr", this->quest_number), this->pvr_contents);
files.emplace(std::format("quest{}.pvr", this->meta.quest_number), this->pvr_contents);
}
string xb_filename = std::format("quest{}_{}.dat", quest_number, static_cast<char>(tolower(char_for_language_code(language))));
return encode_qst_file(files, this->name, this->quest_number, xb_filename, this->version, this->is_dlq_encoded);
string xb_filename = std::format("quest{}_{}.dat",
this->meta.quest_number, static_cast<char>(tolower(char_for_language_code(language))));
return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->version, this->is_dlq_encoded);
}
Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
: quest_number(initial_version->quest_number),
category_id(initial_version->category_id),
episode(initial_version->episode),
allow_start_from_chat_command(initial_version->allow_start_from_chat_command),
joinable(initial_version->joinable),
max_players(initial_version->max_players),
lock_status_register(initial_version->lock_status_register),
name(initial_version->name),
supermap(nullptr),
battle_rules(initial_version->battle_rules),
challenge_template_index(initial_version->challenge_template_index),
description_flag(initial_version->description_flag),
available_expression(initial_version->available_expression),
enabled_expression(initial_version->enabled_expression) {
: meta(initial_version->meta), supermap(nullptr) {
this->add_version(initial_version);
}
@@ -290,32 +316,17 @@ phosg::JSON Quest::json() const {
versions_json.emplace_back(phosg::JSON::dict({
{"Version", phosg::name_for_enum(vq->version)},
{"Language", name_for_language_code(vq->language)},
{"ShortDescription", vq->short_description},
{"LongDescription", vq->long_description},
{"Name", vq->meta.name},
{"ShortDescription", vq->meta.short_description},
{"LongDescription", vq->meta.long_description},
{"BINFileSize", vq->bin_contents ? vq->bin_contents->size() : phosg::JSON(nullptr)},
{"DATFileSize", vq->dat_contents ? vq->dat_contents->size() : phosg::JSON(nullptr)},
{"PVRFileSize", vq->pvr_contents ? vq->pvr_contents->size() : phosg::JSON(nullptr)},
}));
}
auto battle_rules_json = this->battle_rules ? this->battle_rules->json() : nullptr;
auto challenge_template_index_json = (this->challenge_template_index >= 0)
? this->challenge_template_index
: phosg::JSON(nullptr);
return phosg::JSON::dict({
{"Number", this->quest_number},
{"CategoryID", this->category_id},
{"Episode", name_for_episode(this->episode)},
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
{"Joinable", this->joinable},
{"MaxPlayers", this->max_players},
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
{"Name", this->name},
{"BattleRules", std::move(battle_rules_json)},
{"ChallengeTemplateIndex", std::move(challenge_template_index_json)},
{"DescriptionFlag", this->description_flag},
{"AvailableExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
{"EnabledExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
{"Metadata", this->meta.json()},
{"Versions", std::move(versions_json)},
});
}
@@ -325,88 +336,7 @@ uint32_t Quest::versions_key(Version v, uint8_t language) {
}
void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
if (this->quest_number != vq->quest_number) {
throw logic_error(std::format(
"incorrect versioned quest number (existing: {:08X}, new: {:08X})",
this->quest_number, vq->quest_number));
}
if (this->category_id != vq->category_id) {
throw runtime_error(std::format(
"quest version is in a different category (existing: {:08X}, new: {:08X})",
this->category_id, vq->category_id));
}
if (this->episode != vq->episode) {
throw runtime_error(std::format(
"quest version is in a different episode (existing: {}, new: {})",
name_for_episode(this->episode), name_for_episode(vq->episode)));
}
if (this->allow_start_from_chat_command != vq->allow_start_from_chat_command) {
throw runtime_error(std::format(
"quest version has a different allow_start_from_chat_command state (existing: {}, new: {})",
this->allow_start_from_chat_command ? "true" : "false", vq->allow_start_from_chat_command ? "true" : "false"));
}
if (this->joinable != vq->joinable) {
throw runtime_error(std::format(
"quest version has a different joinability state (existing: {}, new: {})",
this->joinable ? "true" : "false", vq->joinable ? "true" : "false"));
}
if (this->max_players != vq->max_players) {
throw runtime_error(std::format(
"quest version has a different maximum player count (existing: {}, new: {})",
this->max_players, vq->max_players));
}
if (this->lock_status_register != vq->lock_status_register) {
throw runtime_error(std::format(
"quest version has a different lock status register (existing: {:04X}, new: {:04X})",
this->lock_status_register, vq->lock_status_register));
}
if (!this->battle_rules != !vq->battle_rules) {
throw runtime_error(std::format(
"quest version has a different battle rules presence state (existing: {}, new: {})",
this->battle_rules ? "present" : "absent", vq->battle_rules ? "present" : "absent"));
}
if (this->battle_rules && (*this->battle_rules != *vq->battle_rules)) {
string existing_str = this->battle_rules->json().serialize();
string new_str = vq->battle_rules->json().serialize();
throw runtime_error(std::format(
"quest version has different battle rules (existing: {}, new: {})",
existing_str, new_str));
}
if (this->challenge_template_index != vq->challenge_template_index) {
throw runtime_error(std::format(
"quest version has different challenge template index (existing: {}, new: {})",
this->challenge_template_index, vq->challenge_template_index));
}
if (this->description_flag != vq->description_flag) {
throw runtime_error(std::format(
"quest version has different description flag (existing: {:02X}, new: {:02X})",
this->description_flag, vq->description_flag));
}
if (!this->available_expression != !vq->available_expression) {
throw runtime_error(std::format(
"quest version has available expression but root quest does not, or vice versa (existing: {}, new: {})",
this->available_expression ? "present" : "absent", vq->available_expression ? "present" : "absent"));
}
if (this->available_expression && *this->available_expression != *vq->available_expression) {
string existing_str = this->available_expression->str();
string new_str = vq->available_expression->str();
throw runtime_error(std::format(
"quest version has a different available expression (existing: {}, new: {})",
existing_str, new_str));
}
if (!this->enabled_expression != !vq->enabled_expression) {
throw runtime_error(std::format(
"quest version has enabled expression but root quest does not, or vice versa (existing: {}, new: {})",
this->enabled_expression ? "present" : "absent", vq->enabled_expression ? "present" : "absent"));
}
if (this->enabled_expression && *this->enabled_expression != *vq->enabled_expression) {
string existing_str = this->enabled_expression->str();
string new_str = vq->enabled_expression->str();
throw runtime_error(std::format(
"quest version has a different enabled expression (existing: {}, new: {})",
existing_str, new_str));
}
this->meta.assert_compatible(vq->meta);
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
}
@@ -438,12 +368,12 @@ std::shared_ptr<const SuperMap> Quest::get_supermap(int64_t random_seed) const {
return nullptr;
}
auto supermap = make_shared<SuperMap>(this->episode, map_files);
auto supermap = make_shared<SuperMap>(this->meta.episode, map_files);
if (save_to_cache) {
this->supermap = supermap;
}
static_game_data_log.info_f("Constructed {} supermap for quest {} ({})",
save_to_cache ? "cacheable" : "temporary", this->quest_number, this->name);
save_to_cache ? "cacheable" : "temporary", this->meta.quest_number, this->meta.name);
return supermap;
}
@@ -482,7 +412,8 @@ shared_ptr<const VersionedQuest> Quest::version(Version v, uint8_t language) con
QuestIndex::QuestIndex(
const string& directory,
shared_ptr<const QuestCategoryIndex> category_index,
bool is_ep3)
const unordered_map<string, shared_ptr<const CommonItemSet>>& common_item_sets,
const unordered_map<string, shared_ptr<const RareItemSet>>& rare_item_sets)
: directory(directory),
category_index(category_index) {
@@ -492,7 +423,7 @@ QuestIndex::QuestIndex(
};
struct BINFileData {
string filename;
unique_ptr<QuestMetadata> metadata;
shared_ptr<const AssembledQuestScript> assembled;
shared_ptr<const string> data;
};
struct DATFileData {
@@ -506,12 +437,6 @@ QuestIndex::QuestIndex(
map<string, FileData> json_files;
map<string, uint32_t> categories;
for (const auto& cat : this->category_index->categories) {
// Don't index Ep3 download categories for non-Ep3 quest indexing, and vice
// versa
if (is_ep3 != cat->check_flag(QuestMenuType::EP3_DOWNLOAD)) {
continue;
}
auto add_file = [&](map<string, FileData>& files, const string& basename, const string& filename, string&& value, bool check_chunk_size) {
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("file " + basename + " exists in multiple categories");
@@ -528,7 +453,7 @@ QuestIndex::QuestIndex(
}
};
auto add_bin_file = [&](const string& basename, const string& filename, string&& data, const QuestMetadata* metadata) {
auto add_bin_file = [&](const string& basename, const string& filename, string&& data, shared_ptr<AssembledQuestScript> assembled) {
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("bin file " + basename + " exists in multiple categories");
}
@@ -540,9 +465,7 @@ QuestIndex::QuestIndex(
auto& entry = emplace_ret.first->second;
entry.filename = filename;
entry.data = data_ptr;
if (metadata) {
entry.metadata = make_unique<QuestMetadata>(*metadata);
}
entry.assembled = assembled;
if (!(data_ptr->size() & 0x3FF)) {
data_ptr->push_back(0x00);
}
@@ -573,7 +496,7 @@ QuestIndex::QuestIndex(
}
string file_path = cat_path + "/" + filename;
unique_ptr<AssembledQuestScript> assembled;
shared_ptr<AssembledQuestScript> assembled;
try {
string orig_filename = filename;
string file_data;
@@ -588,7 +511,7 @@ QuestIndex::QuestIndex(
filename.resize(filename.size() - 4);
} else if (filename.ends_with(".bin.txt")) {
string include_dir = phosg::dirname(file_path);
assembled = make_unique<AssembledQuestScript>(assemble_quest_script(
assembled = make_shared<AssembledQuestScript>(assemble_quest_script(
phosg::load_file(file_path),
{include_dir, "system/quests/includes"},
{include_dir, "system/quests/includes", "system/client-functions/System"}));
@@ -614,9 +537,9 @@ QuestIndex::QuestIndex(
if (extension == "json") {
add_file(json_files, file_basename, orig_filename, std::move(file_data), false);
} else if (extension == "bin" || extension == "mnm") {
add_bin_file(file_basename, orig_filename, std::move(file_data), assembled ? &assembled->metadata : nullptr);
add_bin_file(file_basename, orig_filename, std::move(file_data), assembled);
} else if (extension == "bind" || extension == "mnmd") {
add_bin_file(file_basename, orig_filename, prs_compress_optimal(file_data), assembled ? &assembled->metadata : nullptr);
add_bin_file(file_basename, orig_filename, prs_compress_optimal(file_data), assembled);
} else if (extension == "dat") {
add_dat_file(file_basename, orig_filename, std::move(file_data));
} else if (extension == "datd") {
@@ -669,28 +592,18 @@ QuestIndex::QuestIndex(
version_token = std::move(filename_tokens[1]);
language_token = std::move(filename_tokens[2]);
}
vq->category_id = categories.at(basename);
// Find the quest's metadata. If the quest was assembled (that is, if it
// came from a .bin.txt file), use the metadata from the source file;
// otherwise, figure it out from the already-assembled code
if (entry.metadata) {
vq->quest_number = entry.metadata->quest_number;
vq->version = ::is_ep3(entry.metadata->version) ? Version::GC_V3 : entry.metadata->version;
vq->language = entry.metadata->language;
vq->episode = entry.metadata->episode;
vq->joinable = entry.metadata->joinable;
vq->max_players = entry.metadata->max_players;
vq->name = entry.metadata->name;
vq->short_description = entry.metadata->short_description;
vq->long_description = entry.metadata->long_description;
vq->meta.category_id = categories.at(basename);
if (entry.assembled) {
vq->meta.quest_number = entry.assembled->quest_number;
vq->version = entry.assembled->version;
vq->language = entry.assembled->language;
} else {
// Get the number from the first token
if (quest_number_token.empty()) {
throw runtime_error("quest number token is missing");
}
vq->quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10);
vq->meta.quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10);
// Get the version from the second token
static const unordered_map<string, Version> name_to_version({
@@ -714,147 +627,45 @@ QuestIndex::QuestIndex(
throw runtime_error("language token is not a single character");
}
vq->language = language_code_for_char(language_token[0]);
auto bin_decompressed = prs_decompress(*entry.data);
switch (vq->version) {
case Version::DC_NTE: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDCNTE)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderDCNTE*>(bin_decompressed.data());
vq->episode = Episode::EP1;
vq->max_players = 4;
vq->name = header->name.decode(vq->language);
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = phosg::fnv1a64(vq->name);
}
break;
}
case Version::DC_11_2000:
case Version::DC_V1:
case Version::DC_V2: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderDC*>(bin_decompressed.data());
vq->episode = Episode::EP1;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
case Version::PC_NTE:
case Version::PC_V2: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderPC*>(bin_decompressed.data());
vq->episode = Episode::EP1;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
case Version::GC_EP3_NTE:
case Version::GC_EP3: {
// Note: This codepath handles Episode 3 download quests, which are not
// the same as Episode 3 quest scripts. The latter are only used offline
// in story mode, but can be disassembled with disassemble_quest_script.
// It's unfortunate that Version::GC_EP3 is used here for Episode 3
// download quests (maps) and there for offline story mode scripts, but
// it's probably not worth refactoring this logic, at least right now.
if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) {
throw invalid_argument("file is incorrect size");
}
auto* map = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
vq->episode = Episode::EP3;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = map->map_number;
}
vq->name = map->name.decode(vq->language);
vq->short_description = map->quest_name.decode(vq->language);
vq->long_description = map->description.decode(vq->language);
break;
}
case Version::XB_V3:
case Version::GC_NTE:
case Version::GC_V3: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderGC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderGC*>(bin_decompressed.data());
vq->episode = find_quest_episode_from_script(
bin_decompressed.data(), bin_decompressed.size(), vq->version);
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
case Version::BB_V4: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderBB*>(bin_decompressed.data());
vq->episode = find_quest_episode_from_script(
bin_decompressed.data(), bin_decompressed.size(), vq->version);
vq->joinable |= header->joinable;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
default:
throw logic_error("invalid quest game version");
}
}
// Find the corresponding dat and pvr files
auto bin_decompressed = prs_decompress(*entry.data);
populate_quest_metadata_from_script(vq->meta, bin_decompressed.data(), bin_decompressed.size(), vq->version, vq->language);
// If the quest was assembled (that is, if it came from a .bin.txt file),
// the metadata from the source file overrides any automatically-detected
// values from above
if (entry.assembled) {
vq->meta.quest_number = entry.assembled->quest_number;
vq->meta.episode = entry.assembled->episode;
vq->meta.joinable = entry.assembled->joinable;
vq->meta.max_players = entry.assembled->max_players;
vq->meta.name = entry.assembled->name;
vq->meta.short_description = entry.assembled->short_description;
vq->meta.long_description = entry.assembled->long_description;
}
// Find the corresponding dat and pvr files with the same basename as the
// bin file; if not found, look for them without the language suffix
const DATFileData* dat_filedata = nullptr;
const FileData* pvr_filedata = nullptr;
if (!::is_ep3(vq->version)) {
// Look for dat and pvr files with the same basename as the bin file; if
// not found, look for them without the language suffix
try {
dat_filedata = &dat_files.at(basename);
} catch (const out_of_range&) {
try {
dat_filedata = &dat_files.at(basename);
dat_filedata = &dat_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) {
try {
dat_filedata = &dat_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) {
throw runtime_error("no dat file found for bin file " + basename);
}
throw runtime_error("no dat file found for bin file " + basename);
}
}
try {
pvr_filedata = &pvr_files.at(basename);
} catch (const out_of_range&) {
try {
pvr_filedata = &pvr_files.at(basename);
pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) {
try {
pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) {
// pvr files aren't required (and most quests do not have them), so
// don't fail if it's missing
}
// pvr files aren't required (and most quests do not have them), so
// don't fail if it's missing
}
}
vq->bin_contents = entry.data;
@@ -883,42 +694,56 @@ QuestIndex::QuestIndex(
if (json_filedata) {
auto metadata_json = phosg::JSON::parse(*json_filedata->data);
try {
vq->battle_rules = make_shared<BattleRules>(metadata_json.at("BattleRules"));
vq->meta.description_flag = metadata_json.at("DescriptionFlag").as_int();
} catch (const out_of_range&) {
}
try {
vq->challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int();
vq->meta.available_expression = make_shared<IntegralExpression>(metadata_json.get_string("AvailableIf"));
} catch (const out_of_range&) {
}
try {
vq->description_flag = metadata_json.at("DescriptionFlag").as_int();
vq->meta.enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
} catch (const out_of_range&) {
}
try {
vq->available_expression = make_shared<IntegralExpression>(metadata_json.get_string("AvailableIf"));
vq->meta.allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand");
} catch (const out_of_range&) {
}
try {
vq->enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
vq->meta.joinable = metadata_json.get_bool("Joinable");
} catch (const out_of_range&) {
}
try {
vq->allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand");
vq->meta.lock_status_register = metadata_json.get_int("LockStatusRegister");
} catch (const out_of_range&) {
}
try {
vq->joinable = metadata_json.get_bool("Joinable");
vq->meta.common_item_set_name = metadata_json.at("CommonItemSetName").as_string();
} catch (const out_of_range&) {
}
if (!vq->meta.common_item_set_name.empty()) {
vq->meta.common_item_set = common_item_sets.at(vq->meta.common_item_set_name);
}
try {
vq->meta.rare_item_set_name = metadata_json.at("RareItemSetName").as_string();
} catch (const out_of_range&) {
}
if (!vq->meta.rare_item_set_name.empty()) {
vq->meta.rare_item_set = rare_item_sets.at(vq->meta.rare_item_set_name);
}
try {
vq->meta.allowed_drop_modes = metadata_json.at("AllowedDropModes").as_int();
} catch (const out_of_range&) {
}
try {
vq->lock_status_register = metadata_json.get_int("LockStatusRegister");
vq->meta.default_drop_mode = phosg::enum_for_name<ServerDropMode>(metadata_json.at("DefaultDropMode").as_string());
} catch (const out_of_range&) {
}
}
vq->assert_valid();
auto category_name = this->category_index->at(vq->category_id)->name;
auto category_name = this->category_index->at(vq->meta.category_id)->name;
string filenames_str = entry.filename;
if (dat_filedata) {
filenames_str += std::format("/{}", dat_filedata->filename);
@@ -929,30 +754,32 @@ QuestIndex::QuestIndex(
if (json_filedata) {
filenames_str += std::format("/{}", json_filedata->filename);
}
auto q_it = this->quests_by_number.find(vq->quest_number);
auto q_it = this->quests_by_number.find(vq->meta.quest_number);
if (q_it != this->quests_by_number.end()) {
q_it->second->add_version(vq);
static_game_data_log.debug_f("({}) Added {} {} version of quest {} ({})",
static_game_data_log.debug_f("({}) Added {} {} version of quest {} ({}) with floors {}",
filenames_str,
phosg::name_for_enum(vq->version),
char_for_language_code(vq->language),
vq->quest_number,
vq->name);
vq->meta.quest_number,
vq->meta.name,
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
} else {
auto q = make_shared<Quest>(vq);
this->quests_by_number.emplace(vq->quest_number, q);
this->quests_by_name.emplace(vq->name, q);
this->quests_by_category_id_and_number[q->category_id].emplace(vq->quest_number, q);
static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {})",
this->quests_by_number.emplace(vq->meta.quest_number, q);
this->quests_by_name.emplace(vq->meta.name, q);
this->quests_by_category_id_and_number[q->meta.category_id].emplace(vq->meta.quest_number, q);
static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {}) with floors {}",
filenames_str,
phosg::name_for_enum(vq->version),
char_for_language_code(vq->language),
vq->quest_number,
vq->name,
name_for_episode(vq->episode),
vq->meta.quest_number,
vq->meta.name,
name_for_episode(vq->meta.episode),
category_name,
vq->category_id,
vq->joinable ? "joinable" : "not joinable");
vq->meta.category_id,
vq->meta.joinable ? "joinable" : "not joinable",
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
}
} catch (const exception& e) {
static_game_data_log.warning_f("({}) Failed to index quest file: {}", basename, e.what());
@@ -1033,7 +860,7 @@ vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>> QuestIndex::filt
return ret;
}
for (auto it : category_it->second) {
if ((effective_episode != Episode::NONE) && (it.second->episode != effective_episode)) {
if ((effective_episode != Episode::NONE) && (it.second->meta.episode != effective_episode)) {
continue;
}
bool all_required_versions_present = true;
@@ -1082,8 +909,7 @@ string encode_download_quest_data(const string& compressed_data, size_t decompre
data.resize((data.size() + 3) & (~3));
PSOV2Encryption encr(encryption_seed);
encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader),
data.size() - sizeof(PSODownloadQuestHeader));
encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader), data.size() - sizeof(PSODownloadQuestHeader));
data.resize(original_size);
return data;
@@ -1095,12 +921,6 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
// this flag, we need to decompress the quest's .bin file, set the flag, then
// recompress it again.
// This function should not be used for Episode 3 quests (they should be sent
// to the client as-is, without any encryption or other preprocessing)
if (this->episode == Episode::EP3 || is_ep3(this->version)) {
throw logic_error("Episode 3 quests cannot be converted to download quests");
}
string decompressed_bin = prs_decompress(*this->bin_contents);
void* data_ptr = decompressed_bin.data();
+12 -30
View File
@@ -8,10 +8,14 @@
#include <unordered_map>
#include <vector>
#include "CommonItemSet.hh"
#include "IntegralExpression.hh"
#include "ItemParameterTable.hh"
#include "Map.hh"
#include "PlayerSubordinates.hh"
#include "QuestMetadata.hh"
#include "QuestScript.hh"
#include "RareItemSet.hh"
#include "StaticGameData.hh"
#include "TeamIndex.hh"
@@ -31,7 +35,6 @@ enum class QuestMenuType {
SOLO = 3,
GOVERNMENT = 4,
DOWNLOAD = 5,
EP3_DOWNLOAD = 6,
// 7 can't be used as a menu type (it enables the per-episode filter)
};
@@ -64,29 +67,16 @@ struct QuestCategoryIndex {
};
struct VersionedQuest {
QuestMetadata meta;
// Most of these default values are intentionally invalid; we use these
// values to check if each field was parsed during quest indexing.
uint32_t category_id = 0xFFFFFFFF;
uint32_t quest_number = 0xFFFFFFFF;
Version version = Version::UNKNOWN;
uint8_t language = 0xFF;
Episode episode = Episode::NONE;
bool joinable = false;
uint8_t max_players = 0x00;
std::string name;
std::string short_description;
std::string long_description;
std::shared_ptr<const std::string> bin_contents;
std::shared_ptr<const std::string> dat_contents;
std::shared_ptr<const MapFile> map_file;
std::shared_ptr<const std::string> pvr_contents;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index = -1;
uint8_t description_flag = 0x00;
std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> enabled_expression;
bool allow_start_from_chat_command = false;
int16_t lock_status_register = -1;
bool is_dlq_encoded = false;
void assert_valid() const;
@@ -101,20 +91,8 @@ struct VersionedQuest {
};
struct Quest {
uint32_t quest_number;
uint32_t category_id;
Episode episode;
bool allow_start_from_chat_command;
bool joinable;
uint8_t max_players;
int16_t lock_status_register;
std::string name;
QuestMetadata meta;
mutable std::shared_ptr<const SuperMap> supermap;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
uint8_t description_flag;
std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> enabled_expression;
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
Quest() = delete;
@@ -151,7 +129,11 @@ struct QuestIndex {
std::map<std::string, std::shared_ptr<Quest>> quests_by_name;
std::map<uint32_t, std::map<uint32_t, std::shared_ptr<Quest>>> quests_by_category_id_and_number;
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index, bool is_ep3);
QuestIndex(
const std::string& directory,
std::shared_ptr<const QuestCategoryIndex> category_index,
const std::unordered_map<std::string, std::shared_ptr<const CommonItemSet>>& common_item_sets,
const std::unordered_map<std::string, std::shared_ptr<const RareItemSet>>& rare_item_sets);
phosg::JSON json() const;
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
+164
View File
@@ -0,0 +1,164 @@
#include "QuestMetadata.hh"
using namespace std;
void QuestMetadata::assign_default_areas(Version version, Episode episode) {
for (size_t z = 0; z < 0x12; z++) {
this->area_for_floor[z] = SetDataTableBase::default_area_for_floor(version, episode, z);
}
}
void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
if (this->quest_number != other.quest_number) {
throw logic_error(std::format(
"incorrect versioned quest number (existing: {:08X}, new: {:08X})",
this->quest_number, other.quest_number));
}
if (this->category_id != other.category_id) {
throw runtime_error(std::format(
"quest version is in a different category (existing: {:08X}, new: {:08X})",
this->category_id, other.category_id));
}
if (this->episode != other.episode) {
throw runtime_error(std::format(
"quest version is in a different episode (existing: {}, new: {})",
name_for_episode(this->episode), name_for_episode(other.episode)));
}
if (this->allow_start_from_chat_command != other.allow_start_from_chat_command) {
throw runtime_error(std::format(
"quest version has a different allow_start_from_chat_command state (existing: {}, new: {})",
this->allow_start_from_chat_command ? "true" : "false", other.allow_start_from_chat_command ? "true" : "false"));
}
if (this->joinable != other.joinable) {
throw runtime_error(std::format(
"quest version has a different joinability state (existing: {}, new: {})",
this->joinable ? "true" : "false", other.joinable ? "true" : "false"));
}
if (this->max_players != other.max_players) {
throw runtime_error(std::format(
"quest version has a different maximum player count (existing: {}, new: {})",
this->max_players, other.max_players));
}
if (this->lock_status_register != other.lock_status_register) {
throw runtime_error(std::format(
"quest version has a different lock status register (existing: {:04X}, new: {:04X})",
this->lock_status_register, other.lock_status_register));
}
if (!this->battle_rules != !other.battle_rules) {
throw runtime_error(std::format(
"quest version has a different battle rules presence state (existing: {}, new: {})",
this->battle_rules ? "present" : "absent", other.battle_rules ? "present" : "absent"));
}
if (this->battle_rules && (*this->battle_rules != *other.battle_rules)) {
string existing_str = this->battle_rules->json().serialize();
string new_str = other.battle_rules->json().serialize();
throw runtime_error(std::format(
"quest version has different battle rules (existing: {}, new: {})",
existing_str, new_str));
}
if (this->challenge_template_index != other.challenge_template_index) {
throw runtime_error(std::format(
"quest version has different challenge template index (existing: {}, new: {})",
this->challenge_template_index, other.challenge_template_index));
}
if (this->challenge_exp_multiplier != other.challenge_exp_multiplier) {
throw runtime_error(std::format(
"quest version has different challenge EXP multiplier (existing: {}, new: {})",
this->challenge_exp_multiplier, other.challenge_exp_multiplier));
}
if (this->challenge_difficulty != other.challenge_difficulty) {
throw runtime_error(std::format(
"quest version has different challenge difficulty (existing: {}, new: {})",
this->challenge_difficulty, other.challenge_difficulty));
}
for (size_t z = 0; z < this->area_for_floor.size(); z++) {
const auto& this_fa = this->area_for_floor[z];
const auto& other_fa = other.area_for_floor[z];
if (this_fa != other_fa) {
throw runtime_error(std::format(
"quest version has different area on floor 0x{:02X} (existing: {}, new: {})",
z, phosg::format_data_string(this->area_for_floor.data(), 0x12), phosg::format_data_string(other.area_for_floor.data(), 0x12)));
}
}
if (this->description_flag != other.description_flag) {
throw runtime_error(std::format(
"quest version has different description flag (existing: {:02X}, new: {:02X})",
this->description_flag, other.description_flag));
}
if (!this->available_expression != !other.available_expression) {
throw runtime_error(std::format(
"quest version has available expression but root quest does not, or vice versa (existing: {}, new: {})",
this->available_expression ? "present" : "absent", other.available_expression ? "present" : "absent"));
}
if (this->available_expression && *this->available_expression != *other.available_expression) {
string existing_str = this->available_expression->str();
string new_str = other.available_expression->str();
throw runtime_error(std::format(
"quest version has a different available expression (existing: {}, new: {})",
existing_str, new_str));
}
if (!this->enabled_expression != !other.enabled_expression) {
throw runtime_error(std::format(
"quest version has enabled expression but root quest does not, or vice versa (existing: {}, new: {})",
this->enabled_expression ? "present" : "absent", other.enabled_expression ? "present" : "absent"));
}
if (this->enabled_expression && *this->enabled_expression != *other.enabled_expression) {
string existing_str = this->enabled_expression->str();
string new_str = other.enabled_expression->str();
throw runtime_error(std::format(
"quest version has a different enabled expression (existing: {}, new: {})",
existing_str, new_str));
}
if (this->common_item_set_name != other.common_item_set_name) {
throw runtime_error(std::format(
"quest version has different common table name (existing: {}, new: {})",
this->common_item_set_name, other.common_item_set_name));
}
if (this->common_item_set != other.common_item_set) {
throw runtime_error("quest version has different common table");
}
if (this->rare_item_set_name != other.rare_item_set_name) {
throw runtime_error(std::format(
"quest version has different rare table name (existing: {}, new: {})",
this->rare_item_set_name, other.rare_item_set_name));
}
if (this->rare_item_set != other.rare_item_set) {
throw runtime_error("quest version has different rare table");
}
if (this->allowed_drop_modes != other.allowed_drop_modes) {
throw runtime_error(format("quest version has different allowed drop modes (existing: {:02X}, new: {:02X})",
this->allowed_drop_modes, other.allowed_drop_modes));
}
if (this->default_drop_mode != other.default_drop_mode) {
throw runtime_error(format("quest version has different default drop mode (existing: {}, new: {})",
phosg::name_for_enum(this->default_drop_mode), phosg::name_for_enum(other.default_drop_mode)));
}
}
phosg::JSON QuestMetadata::json() const {
auto floors_json = phosg::JSON::list();
for (const auto& fa : this->area_for_floor) {
floors_json.emplace_back(fa);
}
return phosg::JSON::dict({
{"CategoryID", this->category_id},
{"Number", this->quest_number},
{"Episode", name_for_episode(this->episode)},
{"FloorAssignments", floors_json},
{"Joinable", this->joinable},
{"MaxPlayers", this->max_players},
{"BattleRules", this->battle_rules ? this->battle_rules->json() : phosg::JSON(nullptr)},
{"ChallengeTemplateIndex", (this->challenge_template_index >= 0) ? this->challenge_template_index : phosg::JSON(nullptr)},
{"ChallengeEXPMultiplier", (this->challenge_exp_multiplier >= 0) ? this->challenge_exp_multiplier : phosg::JSON(nullptr)},
{"ChallengeDifficulty", (this->challenge_difficulty >= 0) ? this->challenge_difficulty : phosg::JSON(nullptr)},
{"DescriptionFlag", this->description_flag},
{"AvailableExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
{"EnabledExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
{"CommonItemSetName", this->common_item_set_name.empty() ? phosg::JSON(nullptr) : this->common_item_set_name},
{"RareItemSetName", this->rare_item_set_name.empty() ? phosg::JSON(nullptr) : this->rare_item_set_name},
{"AllowedDropModes", this->allowed_drop_modes},
{"DefaultDropMode", phosg::name_for_enum(this->default_drop_mode)},
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
});
}
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <stdint.h>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "CommonItemSet.hh"
#include "IntegralExpression.hh"
#include "Map.hh"
#include "PlayerSubordinates.hh"
#include "RareItemSet.hh"
struct QuestMetadata {
// This structure contains configuration that should be the same across all
// versions of the quest, except for the name and description strings. This
// is used in both the Quest and VersionedQuest structures; in Quest, the
// name and description are used only internally.
uint32_t category_id = 0xFFFFFFFF;
uint32_t quest_number = 0xFFFFFFFF;
Episode episode = Episode::NONE;
std::array<uint8_t, 0x12> area_for_floor;
bool joinable = false;
uint8_t max_players = 0x00;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index = -1;
float challenge_exp_multiplier = -1.0f;
int8_t challenge_difficulty = -1;
uint8_t description_flag = 0x00;
std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> enabled_expression;
std::string common_item_set_name; // blank = use default
std::string rare_item_set_name; // blank = use default
std::shared_ptr<const CommonItemSet> common_item_set;
std::shared_ptr<const RareItemSet> rare_item_set;
uint8_t allowed_drop_modes = 0x00; // 0 = use server default
ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0
bool allow_start_from_chat_command = false;
int16_t lock_status_register = -1;
std::string name;
std::string short_description;
std::string long_description;
void assign_default_areas(Version version, Episode episode);
void assert_compatible(const QuestMetadata& other) const;
phosg::JSON json() const;
std::string areas_str() const;
};
+1369 -731
View File
File diff suppressed because it is too large Load Diff
+16 -6
View File
@@ -5,6 +5,7 @@
#include <phosg/Encoding.hh>
#include <phosg/Tools.hh>
#include "QuestMetadata.hh"
#include "StaticGameData.hh"
#include "Text.hh"
#include "Version.hh"
@@ -19,6 +20,18 @@ struct PSOQuestHeaderDCNTE {
/* 0020 */
} __packed_ws__(PSOQuestHeaderDCNTE, 0x20);
struct PSOQuestHeaderDC112000 {
/* 0000 */ le_uint32_t code_offset = 0;
/* 0004 */ le_uint32_t function_table_offset = 0;
/* 0008 */ le_uint32_t size = 0;
/* 000C */ le_uint16_t unknown_a1 = 0;
/* 000E */ le_uint16_t unknown_a2 = 0;
/* 0010 */ pstring<TextEncoding::MARKED, 0x20> name;
/* 0030 */ pstring<TextEncoding::MARKED, 0x80> short_description;
/* 00B0 */ pstring<TextEncoding::MARKED, 0x120> long_description;
/* 01D0 */
} __packed_ws__(PSOQuestHeaderDC112000, 0x1D0);
struct PSOQuestHeaderDC { // Same format for DC v1 and v2
/* 0000 */ le_uint32_t code_offset = 0;
/* 0004 */ le_uint32_t function_table_offset = 0;
@@ -100,7 +113,8 @@ std::string disassemble_quest_script(
bool reassembly_mode = false,
bool use_qedit_names = false);
struct QuestMetadata {
struct AssembledQuestScript {
std::string data;
int64_t quest_number = -1;
Version version = Version::UNKNOWN;
uint8_t language = 0xFF;
@@ -111,13 +125,9 @@ struct QuestMetadata {
std::string short_description;
std::string long_description;
};
struct AssembledQuestScript {
std::string data;
QuestMetadata metadata;
};
AssembledQuestScript assemble_quest_script(
const std::string& text,
const std::vector<std::string>& script_include_directories,
const std::vector<std::string>& native_include_directories);
Episode find_quest_episode_from_script(const void* data, size_t size, Version version);
void populate_quest_metadata_from_script(QuestMetadata& meta, const void* data, size_t size, Version version, uint8_t language);
+4 -5
View File
@@ -28,11 +28,10 @@ string RareItemSet::ExpandedDrop::str(shared_ptr<const ItemNameIndex> name_index
}
uint32_t RareItemSet::expand_rate(uint8_t pc) {
// To compute the actual drop rare drop rate from pc, first decode pc into
// shift and value:
// To compute the actual rare drop rate from pc, first decode pc:
// pc = bits SSSSSVVV
// shift = S - 4 (so shift is 0-27)
// value = V + 7 (so value is 7-14)
// shift = S - 4 (so shift is 0-27)
// value = V + 7 (so value is 7-14)
// Then, take the value 0x00000002, shift it left by shift (0-27), and
// multiply the result by value (7-14) to get the actual drop rate. The result
// is a probability out of 0xFFFFFFFF (so 0x40000000 means the item will drop
@@ -722,7 +721,7 @@ string RareItemSet::serialize_html(
}
string hex = example_item.short_hex();
string desc = name_index->describe_item(example_item, false, true);
string desc = name_index->describe_item(example_item, ItemNameIndex::Flag::NAME_ONLY);
tokens.emplace_back(std::format("<span class=\"item\" title=\"Hex: {}\">{}</span>", hex, desc));
float denom = static_cast<float>(frac.second) / static_cast<double>(frac.first);
+334 -237
View File
File diff suppressed because it is too large Load Diff
+433 -292
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -121,6 +121,7 @@ protected:
Version from_version,
bool from_client_customization);
G_6x70_Base_V1 base_v1(bool is_v3) const;
static uint32_t convert_game_flags(uint32_t game_flags, bool to_v3);
uint32_t get_game_flags(bool is_v3) const;
};
+23 -18
View File
@@ -218,16 +218,7 @@ bool PSOGCSnapshotFile::checksum_correct() const {
return (crc == this->checksum);
}
static uint32_t decode_rgb565(uint16_t c) {
// Input bits: rrrrrggg gggbbbbb
// Output bits: rrrrrrrr gggggggg bbbbbbbb aaaaaaaa
return ((c << 16) & 0xF8000000) | ((c << 11) & 0x07000000) | // R
((c << 13) & 0x00FC0000) | ((c << 7) & 0x00030000) | // G
((c << 11) & 0x0000F800) | ((c << 6) & 0x00000700) | // B
0x000000FF; // A
}
phosg::Image PSOGCSnapshotFile::decode_image() const {
phosg::ImageRGB888 PSOGCSnapshotFile::decode_image() const {
size_t width = this->width ? this->width.load() : 256;
size_t height = this->height ? this->height.load() : 192;
if (width != 256) {
@@ -238,14 +229,13 @@ phosg::Image PSOGCSnapshotFile::decode_image() const {
}
// 4x4 blocks of pixels
phosg::Image ret(width, height, false);
phosg::ImageRGB888 ret(width, height);
size_t offset = 0;
for (size_t y = 0; y < this->height; y += 4) {
for (size_t x = 0; x < this->width; x += 4) {
for (size_t yy = 0; yy < 4; yy++) {
for (size_t xx = 0; xx < 4; xx++) {
uint32_t color = decode_rgb565(this->pixels[offset++]);
ret.write_pixel(x + xx, y + yy, color);
ret.write(x + xx, y + yy, phosg::rgba8888_for_rgb565(this->pixels[offset++]));
}
}
}
@@ -312,6 +302,22 @@ PSOGCEp3CharacterFile::Character::operator PSOGCEp3NTECharacter() const {
return ret;
}
bool PSOXBFileHeader::checksum_correct() const {
uint32_t cs = phosg::crc32(&this->game_name, this->game_name.bytes());
cs = phosg::crc32(&this->file_name, this->file_name.bytes(), cs);
cs = phosg::crc32(&this->banner, this->banner.bytes(), cs);
cs = phosg::crc32(&this->icon, this->icon.bytes(), cs);
cs = phosg::crc32(&this->data_size, sizeof(this->data_size), cs);
cs = phosg::crc32("\0\0\0\0", 4, cs); // this->checksum (treated as zero)
return (cs == this->checksum);
}
void PSOXBFileHeader::check() const {
if (!this->checksum_correct()) {
throw runtime_error("Xbox file intermediate header checksum is incorrect");
}
}
void PSOBBGuildCardFile::Entry::clear() {
this->data.clear();
this->unknown_a1.clear(0);
@@ -830,7 +836,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOGCE
return ret;
}
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOXBCharacterFileCharacter& src) {
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOXBCharacterFile::Character& src) {
auto ret = PSOBBCharacterFile::create_from_config(
src.guild_card.guild_card_number,
src.inventory.language,
@@ -1101,10 +1107,10 @@ PSOBBCharacterFile::operator PSOGCCharacterFile::Character() const {
return ret;
}
PSOBBCharacterFile::operator PSOXBCharacterFileCharacter() const {
PSOBBCharacterFile::operator PSOXBCharacterFile::Character() const {
uint8_t language = this->inventory.language;
PSOXBCharacterFileCharacter ret;
PSOXBCharacterFile::Character ret;
ret.inventory = this->inventory;
ret.inventory.encode_for_client(Version::XB_V3, nullptr);
ret.disp = this->disp.to_dcpcv3<false>(language, language);
@@ -1262,8 +1268,7 @@ ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, cons
// then create a new item and reduce the amount of the existing stack. Note
// that passing amount == 0 means to remove the entire stack, so this only
// applies if amount is nonzero.
if (amount && (inventory_item.data.stack_size(limits) > 1) &&
(amount < inventory_item.data.data1[5])) {
if (amount && (inventory_item.data.stack_size(limits) > 1) && (amount < inventory_item.data.data1[5])) {
if (is_equipped) {
throw runtime_error("character has a combine item equipped");
}
+124 -45
View File
@@ -91,6 +91,31 @@ struct PSOGCIFileHeader {
bool is_nte() const;
} __packed_ws__(PSOGCIFileHeader, 0x2088);
struct PSOXBFileHeader {
// The signature is computed by doing the following:
// // TODO: Should flags be 0 or 1? It looks like it should be 0 for
// // character files, but not sure about this
// auto handle = XCalculateSignatureBegin(flags);
// XCalculateSignatureUpdate(
// handle,
// &header.source_size,
// total_size - offsetof(PSOXBFileHeader, source_size));
// XCalculateSignatureEnd(handle, header.signature);
/* 0000 */ parray<uint8_t, 0x14> signature;
/* 0014 */ le_uint32_t source_size = 0; // == total file size - 0x4000
/* 0018 */ parray<uint8_t, 0x3FE8> unused; // Always blank (zeroes)
/* 4000 */ pstring<TextEncoding::MARKED, 0x20> game_name;
/* 4020 */ pstring<TextEncoding::MARKED, 0x20> file_name;
/* 4040 */ parray<uint8_t, 0x1800> banner; // Always blank (zeroes)
/* 5840 */ parray<uint8_t, 0x800> icon; // Always blank (zeroes)
/* 6040 */ le_uint32_t data_size = 0;
/* 6044 */ le_uint32_t checksum = 0; // Starts at game_name
/* 6048 */
bool checksum_correct() const;
void check() const;
} __packed_ws__(PSOXBFileHeader, 0x6048);
////////////////////////////////////////////////////////////////////////////////
// Subordinate structures
@@ -280,6 +305,35 @@ struct PSOGCEp3SystemFile {
/* 012C */
} __packed_ws__(PSOGCEp3SystemFile, 0x12C);
struct PSOXBSystemFile {
/* 0000 */ le_uint32_t checksum = 0;
/* 0004 */ le_int16_t music_volume = -50;
/* 0006 */ int8_t sound_volume = 0;
/* 0007 */ uint8_t language = 0;
/* 0008 */ be_int32_t server_time_delta_frames = 200;
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
/* 000E */ be_uint16_t surround_sound_enabled = 0;
/* 0010 */ parray<uint8_t, 0x0100> event_flags;
/* 0110 */ parray<uint8_t, 8> unknown_a1;
struct UserEntry {
/* 00 */ le_uint32_t xb_user_id_high = 0;
/* 04 */ le_uint32_t xb_user_id_low = 0;
/* 08 */ le_uint32_t unknown_a2;
/* 0C */ le_uint32_t last_write_year;
/* 10 */ le_uint32_t last_write_month;
/* 14 */ le_uint32_t last_write_day;
/* 18 */ le_uint32_t last_write_hour;
/* 1C */ le_uint32_t last_write_minute;
/* 20 */ le_uint32_t last_write_second;
/* 24 */ le_uint32_t flags = 1; // 1 = not present
/* 28 */ pstring<TextEncoding::ASCII, 0x10> gamertag;
/* 38 */
} __packed_ws__(UserEntry, 0x38);
/* 0118 */ parray<UserEntry, 4> users;
/* 01F8 */ le_uint32_t creation_timestamp = 0;
/* 01FC */
} __packed_ws__(PSOXBSystemFile, 0x1FC);
struct PSOBBMinimalSystemFile {
/* 0000 */ be_uint32_t checksum = 0;
/* 0004 */ be_int16_t music_volume = 0;
@@ -717,48 +771,65 @@ struct PSOGCEp3CharacterFile {
/* 194B0 */
} __packed_ws__(PSOGCEp3CharacterFile, 0x194B0);
struct PSOXBCharacterFileCharacter {
// This structure is internally split into two by the game. The offsets here
// are relative to the start of this structure (first column), and relative
// to the start of the second internal structure (second column).
// Most fields have the same meanings as in PSOGCCharacterFile::Character.
/* 0000:---- */ PlayerInventory inventory;
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
/* 041C:0000 */ le_uint32_t validation_flags = 0;
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
/* 0424:0008 */ le_uint32_t signature = 0xC87ED5B1;
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
/* 0430:0014 */ le_uint32_t save_count = 1;
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
/* 0460:0044 */ QuestFlags quest_flags;
/* 0660:0244 */ le_uint32_t death_count = 0;
/* 0664:0248 */ PlayerBank200 bank;
/* 192C:1510 */ GuildCardXB guild_card;
/* 1B58:173C */ parray<SaveFileSymbolChatEntryDCXB, 12> symbol_chats;
/* 1F78:1B5C */ parray<SaveFileShortcutEntryXB, 16> shortcuts;
/* 24B8:209C */ pstring<TextEncoding::MARKED, 0xAC> auto_reply;
/* 2518:20FC */ pstring<TextEncoding::MARKED, 0xAC> info_board;
// TODO: The following fields are guesses and have not been verified.
/* 2610:21F4 */ PlayerRecordsBattle battle_records;
/* 2628:220C */ parray<uint8_t, 4> unknown_a4;
/* 262C:2210 */ PlayerRecordsChallengeV3 challenge_records;
/* 272C:2310 */ parray<le_uint16_t, 20> tech_menu_shortcut_entries;
/* 2754:2338 */ ChoiceSearchConfig choice_search_config;
/* 276C:2350 */ parray<uint8_t, 0x10> unknown_a6;
/* 277C:2360 */ parray<le_uint32_t, 0x10> quest_counters;
/* 27BC:23A0 */ PlayerRecordsBattle offline_battle_records;
/* 27D4:23B8 */ parray<uint8_t, 4> unknown_a7;
struct UnknownA8Entry {
/* 00 */ le_uint32_t unknown_a1 = 0;
/* 04 */ parray<uint8_t, 0x1C> unknown_a2;
/* 20 */ parray<le_float, 4> unknown_a3;
/* 30 */
} __packed_ws__(UnknownA8Entry, 0x30);
/* 27D8:23BC */ parray<UnknownA8Entry, 5> unknown_a8;
/* 28C8:24AC */
} __packed_ws__(PSOXBCharacterFileCharacter, 0x28C8);
struct PSOXBCharacterFile {
struct Character {
// This structure is internally split into two by the game. The offsets here
// are relative to the start of this structure (first column), and relative
// to the start of the second internal structure (second column).
// Most fields have the same meanings as in PSOGCCharacterFile::Character.
/* 0000:---- */ PlayerInventory inventory;
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
/* 041C:0000 */ le_uint32_t validation_flags = 0;
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
/* 0424:0008 */ le_uint32_t signature = 0xC87ED5B1;
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
/* 0430:0014 */ le_uint32_t save_count = 1;
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
/* 0460:0044 */ QuestFlags quest_flags;
/* 0660:0244 */ le_uint32_t death_count = 0;
/* 0664:0248 */ PlayerBank200 bank;
/* 192C:1510 */ GuildCardXB guild_card;
/* 1B58:173C */ parray<SaveFileSymbolChatEntryDCXB, 12> symbol_chats;
/* 1F78:1B5C */ parray<SaveFileShortcutEntryXB, 16> shortcuts;
/* 24B8:209C */ pstring<TextEncoding::MARKED, 0xAC> auto_reply;
/* 2518:20FC */ pstring<TextEncoding::MARKED, 0xAC> info_board;
// TODO: The following fields are guesses and have not been verified.
/* 2610:21F4 */ PlayerRecordsBattle battle_records;
/* 2628:220C */ parray<uint8_t, 4> unknown_a4;
/* 262C:2210 */ PlayerRecordsChallengeV3 challenge_records;
/* 272C:2310 */ parray<le_uint16_t, 20> tech_menu_shortcut_entries;
/* 2754:2338 */ ChoiceSearchConfig choice_search_config;
/* 276C:2350 */ parray<uint8_t, 0x10> unknown_a6;
/* 277C:2360 */ parray<le_uint32_t, 0x10> quest_counters;
/* 27BC:23A0 */ PlayerRecordsBattle offline_battle_records;
/* 27D4:23B8 */ parray<uint8_t, 4> unknown_a7;
struct UnknownA8Entry {
/* 00 */ le_uint32_t unknown_a1 = 0;
/* 04 */ parray<uint8_t, 0x1C> unknown_a2;
/* 20 */ parray<le_float, 4> unknown_a3;
/* 30 */
} __packed_ws__(UnknownA8Entry, 0x30);
/* 27D8:23BC */ parray<UnknownA8Entry, 5> unknown_a8;
/* 28C8:24AC */
} __packed_ws__(Character, 0x28C8);
struct CharEntry {
Character character;
parray<uint8_t, 0x18> unknown_a1;
} __packed_ws__(CharEntry, 0x28E0);
/* 00000 */ le_uint32_t checksum = 0;
/* 00004 */ parray<CharEntry, 15> characters;
/* 26524 */ pstring<TextEncoding::ASCII, 0x10> serial_number;
/* 26534 */ pstring<TextEncoding::ASCII, 0x10> access_key;
/* 26544 */ pstring<TextEncoding::ASCII, 0x10> password;
/* 26554 */ be_uint64_t bgm_test_songs_unlocked = 0;
/* 2655C */ le_uint32_t save_count = 1;
/* 26560 */ le_uint32_t round2_seed = 0;
/* 26564 */
} __packed_ws__(PSOXBCharacterFile, 0x26564);
struct PSOBBCharacterFile {
// Most fields have the same meanings as in PSOGCCharacterFile::Character.
@@ -816,7 +887,7 @@ struct PSOBBCharacterFile {
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSOGCNTECharacterFileCharacter& src);
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSOGCCharacterFile::Character& src);
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSOGCEp3CharacterFile::Character& src);
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSOXBCharacterFileCharacter& src);
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSOXBCharacterFile::Character& src);
PSODCNTECharacterFile::Character as_dc_nte(uint64_t hardware_id) const;
PSODC112000CharacterFile::Character as_11_2000(uint64_t hardware_id) const;
@@ -825,7 +896,7 @@ struct PSOBBCharacterFile {
operator PSOGCNTECharacterFileCharacter() const;
operator PSOGCCharacterFile::Character() const;
operator PSOGCEp3CharacterFile::Character() const;
operator PSOXBCharacterFileCharacter() const;
operator PSOXBCharacterFile::Character() const;
void add_item(const ItemData& item, const ItemData::StackLimits& limits);
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits);
@@ -947,6 +1018,14 @@ struct PSOGCGuildCardFile {
/* E28C */
} __packed_ws__(PSOGCGuildCardFile, 0xE28C);
struct PSOXBGuildCardFile {
/* 00000 */ le_uint32_t checksum = 0;
/* 00004 */ parray<GuildCardXB, 100> entries;
/* 0D934 */ parray<GuildCardXB, 0x1C> blocked_senders;
/* 11604 */ le_uint32_t creation_timestamp = 0;
/* 11608 */ le_uint32_t round2_seed = 0;
} __packed_ws__(PSOXBGuildCardFile, 0x1160C);
struct PSOBBGuildCardFile {
struct Entry {
/* 0000 */ GuildCardBB data;
@@ -987,7 +1066,7 @@ struct PSOGCSnapshotFile {
/* 1818C */
bool checksum_correct() const;
phosg::Image decode_image() const;
phosg::ImageRGB888 decode_image() const;
} __packed_ws__(PSOGCSnapshotFile, 0x1818C);
////////////////////////////////////////////////////////////////////////////////
+156 -104
View File
@@ -237,8 +237,6 @@ void send_server_init_bb(shared_ptr<Client> c, uint8_t flags) {
auto cmd = prepare_server_init_contents_bb(server_key, client_key, flags);
send_command_t(c, use_secondary_message ? 0x9B : 0x03, 0x00, cmd);
static const string primary_expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8);
static const string secondary_expected_first_data("\xDC\x00\xDB\x00\x00\x00\x00\x00", 8);
c->bb_detector_crypt = make_shared<PSOBBMultiKeyDetectorEncryption>(
c->require_server_state()->bb_private_keys,
bb_crypt_initial_client_commands,
@@ -452,10 +450,12 @@ asio::awaitable<C_ExecuteCodeResult_B3> send_function_call(
override_relocations_offset,
ignore_actually_runs_code_flag);
c->function_call_response_queue.emplace_back(promise);
c->enabled_flags |= code->client_flag;
co_return co_await promise->get();
}
asio::awaitable<void> send_function_call_multi(shared_ptr<Client> c, vector<shared_ptr<const CompiledFunctionCode>> codes) {
asio::awaitable<void> send_function_call_multi(
shared_ptr<Client> c, unordered_set<shared_ptr<const CompiledFunctionCode>> codes) {
if (codes.empty()) {
co_return;
}
@@ -468,6 +468,7 @@ asio::awaitable<void> send_function_call_multi(shared_ptr<Client> c, vector<shar
last_promise = make_shared<AsyncPromise<C_ExecuteCodeResult_B3>>();
c->function_call_response_queue.emplace_back(last_promise);
send_function_call(c->channel, c->enabled_flags, code);
c->enabled_flags |= code->client_flag;
}
if (c->channel->connected()) {
co_await last_promise->get();
@@ -498,7 +499,7 @@ void send_function_call(
code, label_writes, suffix_data, suffix_size, checksum_addr, checksum_size, override_relocations_offset,
Client::check_flag(client_enabled_flags, Client::Flag::ENCRYPTED_SEND_FUNCTION_CALL));
ch->send(0xB2, code ? code->index : 0x00, data);
ch->send(0xB2, 0x00, data);
}
asio::awaitable<bool> send_protected_command(
@@ -647,7 +648,7 @@ void send_client_init_bb(shared_ptr<Client> c, uint32_t error_code) {
cmd.player_tag = 0x00010000;
cmd.guild_card_number = c->login->account->account_id;
cmd.security_token = team ? team->team_id : 0;
cmd.client_config.clear(0xFF);
cmd.client_config = c->bb_client_config;
cmd.can_create_team = 1;
cmd.episode_4_unlocked = 1;
@@ -789,7 +790,7 @@ void send_approve_player_choice_bb(shared_ptr<Client> c) {
}
void send_complete_player_bb(shared_ptr<Client> c) {
auto p = c->character(true, false);
auto p = c->character_file(true, false);
auto sys = c->system_file(true);
auto team = c->team();
if (c->check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) {
@@ -869,32 +870,49 @@ static void send_header_text(std::shared_ptr<Channel> ch, uint16_t command, uint
}
void send_message_box(shared_ptr<Client> c, const string& text) {
uint16_t command;
switch (c->version()) {
case Version::PC_PATCH:
case Version::BB_PATCH:
command = 0x13;
break;
case Version::DC_NTE:
case Version::DC_11_2000:
case Version::DC_V1:
case Version::DC_V2:
case Version::PC_NTE:
case Version::PC_V2:
command = 0x1A;
break;
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_NTE:
case Version::GC_EP3:
case Version::XB_V3:
case Version::BB_V4:
command = 0xD5;
break;
default:
throw logic_error("invalid game version");
if (is_v4(c->version())) {
phosg::StringWriter w;
try {
w.write(tt_encode_marked_optional(add_color(text), c->language(), true));
} catch (const runtime_error& e) {
phosg::log_warning_f("Failed to encode text for message box command: {}", e.what());
return;
}
w.put_u16(0);
while (w.str().size() & 3) {
w.put_u8(0);
}
send_command(c, (w.size() <= 0x400) ? 0x1A : 0xD5, 0x00, w.str());
} else {
uint16_t command;
switch (c->version()) {
case Version::PC_PATCH:
case Version::BB_PATCH:
command = 0x13;
break;
case Version::DC_NTE:
case Version::DC_11_2000:
case Version::DC_V1:
case Version::DC_V2:
case Version::PC_NTE:
case Version::PC_V2:
command = 0x1A;
break;
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_NTE:
case Version::GC_EP3:
case Version::XB_V3:
command = 0xD5;
break;
case Version::BB_V4:
throw std::logic_error("BB not handled before version switch");
default:
throw logic_error("invalid game version");
}
send_text(c->channel, command, 0x00, text, ColorMode::ADD);
}
send_text(c->channel, command, 0x00, text, ColorMode::ADD);
}
void send_ep3_timed_message_box(std::shared_ptr<Channel> ch, uint32_t frames, const string& message) {
@@ -1150,7 +1168,7 @@ void send_info_board_t(shared_ptr<Client> c) {
if (!other_c.get()) {
continue;
}
auto other_p = other_c->character(true, false);
auto other_p = other_c->character_file(true, false);
auto& e = entries.emplace_back();
e.name.encode(other_p->disp.name.decode(other_p->inventory.language), c->language());
e.message.encode(add_color(other_p->info_board.decode(other_p->inventory.language)), c->language());
@@ -1234,7 +1252,7 @@ void send_card_search_result_t(
cmd.location_string.encode(location_string, c->language());
cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY;
cmd.extension.lobby_refs[0].item_id = result_lobby->lobby_id;
auto rp = result->character(true, false);
auto rp = result->character_file(true, false);
cmd.extension.player_name.encode(rp->disp.name.decode(rp->inventory.language), c->language());
send_command_t(c, 0x41, 0x00, cmd);
@@ -1392,7 +1410,7 @@ void send_guild_card(shared_ptr<Client> c, shared_ptr<Client> source) {
throw runtime_error("source player does not have an account");
}
auto source_p = source->character(true, false);
auto source_p = source->character_file(true, false);
auto source_team = source->team();
uint64_t xb_user_id = (source->login->xb_license && source->login->xb_license->user_id)
@@ -1627,10 +1645,10 @@ void send_quest_menu_t(
}
auto& e = entries.emplace_back();
e.menu_id = ((it.second->episode == Episode::EP1) || (it.second->episode == Episode::EP3)) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2;
e.item_id = it.second->quest_number;
e.name.encode(vq->name, c->language());
e.short_description.encode(add_color(vq->short_description), c->language());
e.menu_id = (it.second->meta.episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1;
e.item_id = it.second->meta.quest_number;
e.name.encode(vq->meta.name, c->language());
e.short_description.encode(add_color(vq->meta.short_description), c->language());
}
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
}
@@ -1648,21 +1666,31 @@ void send_quest_menu_bb(
}
auto& e = entries.emplace_back();
e.menu_id = (it.second->episode == Episode::EP1) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2;
e.item_id = it.second->quest_number;
e.name.encode(vq->name, c->language());
e.short_description.encode(add_color(vq->short_description), c->language());
e.menu_id = (it.second->meta.episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1;
e.item_id = it.second->meta.quest_number;
e.name.encode(vq->meta.name, c->language());
e.short_description.encode(add_color(vq->meta.short_description), c->language());
e.disabled = (it.first == QuestIndex::IncludeState::DISABLED) ? 1 : 0;
}
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
}
void send_ep3_download_quest_menu(shared_ptr<Client> c) {
auto s = c->require_server_state();
vector<S_QuestMenuEntry_DC_GC_A2_A4> entries;
for (const auto& it : s->ep3_download_map_index->all()) {
auto vm = it.second->version(c->language());
auto& e = entries.emplace_back();
e.menu_id = MenuID::QUEST_EP3;
e.item_id = it.first; // map_number
e.name.encode(vm->map->name.decode(vm->language), c->language());
e.short_description.encode(add_color(vm->map->location_name.decode(vm->language)), c->language());
}
send_command_vt(c, 0xA4, entries.size(), entries);
}
template <typename EntryT>
void send_quest_categories_menu_t(
shared_ptr<Client> c,
shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode) {
void send_quest_categories_menu_t(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
QuestIndex::IncludeCondition include_condition = nullptr;
if (!c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
auto l = c->lobby.lock();
@@ -1676,7 +1704,8 @@ void send_quest_categories_menu_t(
}
vector<EntryT> entries;
for (const auto& cat : quest_index->categories(menu_type, episode, version_flags, include_condition)) {
auto s = c->require_server_state();
for (const auto& cat : s->quest_index->categories(menu_type, episode, version_flags, include_condition)) {
auto& e = entries.emplace_back();
e.menu_id = cat->use_ep2_icon() ? MenuID::QUEST_CATEGORIES_EP2 : MenuID::QUEST_CATEGORIES_EP1;
e.item_id = cat->category_id;
@@ -1684,7 +1713,7 @@ void send_quest_categories_menu_t(
e.short_description.encode(add_color(cat->description), c->language());
}
bool is_download_menu = (menu_type == QuestMenuType::DOWNLOAD) || (menu_type == QuestMenuType::EP3_DOWNLOAD);
bool is_download_menu = (menu_type == QuestMenuType::DOWNLOAD);
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
}
@@ -1718,15 +1747,11 @@ void send_quest_menu(
}
}
void send_quest_categories_menu(
shared_ptr<Client> c,
shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode) {
void send_quest_categories_menu(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
switch (c->version()) {
case Version::PC_NTE:
case Version::PC_V2:
send_quest_categories_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_type, episode);
break;
case Version::DC_NTE:
case Version::DC_11_2000:
@@ -1736,13 +1761,13 @@ void send_quest_categories_menu(
case Version::GC_V3:
case Version::GC_EP3_NTE:
case Version::GC_EP3:
send_quest_categories_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_type, episode);
break;
case Version::XB_V3:
send_quest_categories_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_type, episode);
break;
case Version::BB_V4:
send_quest_categories_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, menu_type, episode);
break;
default:
throw logic_error("unimplemented versioned command");
@@ -1750,9 +1775,12 @@ void send_quest_categories_menu(
}
void send_lobby_list(shared_ptr<Client> c) {
// This command appears to be deprecated, as PSO expects it to be exactly how
// this server sends it, and does not react if it's different, except by
// changing the lobby IDs.
// DC v1 expects 10 lobbies in this list; DC v2 and later accept a variable
// number, but other parts of the code expect there to always be 15 lobbies.
// Furthermore, there are only 16 entries in the array in TProtocol and the
// writes aren't bounds-checked, so the 83 command could overwrite later
// parts of TProtocol if more than 16 entries are sent. (On Episode 3, there
// are 21 entries instead.)
auto s = c->require_server_state();
vector<S_LobbyListEntry_83> entries;
@@ -1779,7 +1807,7 @@ template <typename EntryT>
void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client) {
vector<EntryT> entries;
auto add_client = [&](shared_ptr<Client> lc) -> void {
auto lp = lc->character(true, false);
auto lp = lc->character_file(true, false);
auto& e = entries.emplace_back();
e.client_id = lc->lobby_client_id;
e.challenge = lp->challenge_records;
@@ -1804,7 +1832,7 @@ void populate_lobby_data_for_client(LobbyDataT& ret, shared_ptr<const Client> c,
ret.player_tag = 0x00010000;
ret.guild_card_number = c->login->account->account_id;
ret.client_id = c->lobby_client_id;
string name = c->character()->disp.name.decode(c->language());
string name = c->character_file()->disp.name.decode(c->language());
ret.name.encode(name, viewer_c->language());
}
@@ -1818,7 +1846,7 @@ void populate_lobby_data_for_client(PlayerLobbyDataXB& ret, shared_ptr<const Cli
ret.netloc.account_id = 0xAE00000000000000 | c->login->account->account_id;
}
ret.client_id = c->lobby_client_id;
string name = c->character()->disp.name.decode(c->language());
string name = c->character_file()->disp.name.decode(c->language());
ret.name.encode(name, viewer_c->language());
}
@@ -1835,7 +1863,7 @@ void populate_lobby_data_for_client<PlayerLobbyDataBB>(PlayerLobbyDataBB& ret, s
ret.team_master_guild_card_number = 0;
ret.team_id = 0;
}
string name = c->character()->disp.name.decode(c->language());
string name = c->character_file()->disp.name.decode(c->language());
ret.name.encode(name, viewer_c->language());
}
@@ -1871,7 +1899,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
if (!wc) {
continue;
}
auto wc_p = wc->character();
auto wc_p = wc->character_file();
auto& p = cmd.players[z];
populate_lobby_data_for_client(p.lobby_data, wc, c);
p.inventory = wc_p->inventory;
@@ -1941,7 +1969,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
for (size_t z = 4; z < 12; z++) {
if (l->clients[z]) {
auto other_c = l->clients[z];
auto other_p = other_c->character();
auto other_p = other_c->character_file();
auto& cmd_p = cmd.spectator_players[z - 4];
auto& cmd_e = cmd.entries[z];
populate_lobby_data_for_client(cmd_p.lobby_data, other_c, c);
@@ -1995,7 +2023,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
cmd.variations = l->variations;
cmd.client_id = c->lobby_client_id;
cmd.leader_id = l->leader_id;
cmd.disable_udp = 0x01; // Unused on PC/XB/BB
cmd.disable_udp = l->client_extension_flags();
cmd.difficulty = l->difficulty;
cmd.battle_mode = (l->mode == GameMode::BATTLE) ? 1 : 0;
cmd.event = l->event;
@@ -2030,7 +2058,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
S_JoinGame_DCNTE_64 cmd;
cmd.client_id = c->lobby_client_id;
cmd.leader_id = l->leader_id;
cmd.disable_udp = 0x01;
cmd.disable_udp = l->client_extension_flags();
cmd.variations = l->variations;
size_t player_count = populate_lobby_data(cmd);
send_command_t(c, 0x64, player_count, cmd);
@@ -2065,7 +2093,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
for (size_t x = 0; x < 4; x++) {
auto lc = l->clients[x];
if (lc) {
auto other_p = lc->character();
auto other_p = lc->character_file();
auto& cmd_p = cmd.players_ep3[x];
cmd_p.inventory = other_p->inventory;
cmd_p.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version()));
@@ -2159,6 +2187,7 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
S_JoinLobbyT<LobbyFlags, LobbyDataT, DispDataT> cmd;
cmd.lobby_flags.client_id = c->lobby_client_id;
cmd.lobby_flags.leader_id = l->leader_id;
cmd.lobby_flags.disable_udp = l->client_extension_flags();
cmd.lobby_flags.lobby_number = lobby_type;
cmd.lobby_flags.block_number = lobby_block;
cmd.lobby_flags.event = l->event;
@@ -2176,7 +2205,7 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
size_t used_entries = 0;
for (const auto& lc : lobby_clients) {
auto lp = lc->character();
auto lp = lc->character_file();
auto& e = cmd.entries[used_entries++];
populate_lobby_data_for_client(e.lobby_data, lc, c);
e.inventory = lp->inventory;
@@ -2231,6 +2260,7 @@ void send_join_lobby_xb(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cl
S_JoinLobby_XB_65_67_68 cmd;
cmd.lobby_flags.client_id = c->lobby_client_id;
cmd.lobby_flags.leader_id = l->leader_id;
cmd.lobby_flags.disable_udp = l->client_extension_flags();
cmd.lobby_flags.lobby_number = lobby_type;
cmd.lobby_flags.block_number = l->block;
cmd.lobby_flags.event = l->event;
@@ -2248,7 +2278,7 @@ void send_join_lobby_xb(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cl
size_t used_entries = 0;
for (const auto& lc : lobby_clients) {
auto lp = lc->character();
auto lp = lc->character_file();
auto& e = cmd.entries[used_entries++];
populate_lobby_data_for_client(e.lobby_data, lc, c);
e.inventory = lp->inventory;
@@ -2280,6 +2310,7 @@ void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l,
S_JoinLobby_DCNTE_65_67_68 cmd;
cmd.lobby_flags.client_id = c->lobby_client_id;
cmd.lobby_flags.leader_id = l->leader_id;
cmd.lobby_flags.disable_udp = l->client_extension_flags();
vector<shared_ptr<Client>> lobby_clients;
if (joining_client) {
@@ -2296,7 +2327,7 @@ void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l,
size_t used_entries = 0;
for (const auto& lc : lobby_clients) {
auto lp = lc->character();
auto lp = lc->character_file();
auto& e = cmd.entries[used_entries++];
populate_lobby_data_for_client(e.lobby_data, lc, c);
e.inventory = lp->inventory;
@@ -2402,7 +2433,7 @@ void send_update_lobby_data_bb(std::shared_ptr<Client> c) {
}
void send_player_leave_notification(shared_ptr<Lobby> l, uint8_t leaving_client_id) {
S_LeaveLobby_66_69_Ep3_E9 cmd = {leaving_client_id, l->leader_id, 1, 0};
S_LeaveLobby_66_69_Ep3_E9 cmd = {leaving_client_id, l->leader_id, l->client_extension_flags(), 0};
uint8_t cmd_num;
if (l->is_game()) {
cmd_num = l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM) ? 0xE9 : 0x66;
@@ -2593,6 +2624,29 @@ void send_player_stats_change(std::shared_ptr<Channel> ch, uint16_t client_id, P
send_command_vt(ch, (subs.size() > 0x400 / sizeof(G_UpdateEntityStat_6x9A)) ? 0x6C : 0x60, 0x00, subs);
}
void send_change_player_hp(std::shared_ptr<Channel> ch, uint16_t client_id, PlayerHPChange what, int16_t amount) {
uint8_t subcommand_number;
if (ch->version == Version::DC_NTE) {
subcommand_number = 0x2B;
} else if (ch->version == Version::DC_11_2000) {
subcommand_number = 0x2D;
} else {
subcommand_number = 0x2F;
}
G_ChangePlayerHP_6x2F cmd = {
{subcommand_number, sizeof(G_ChangePlayerHP_6x2F) / 4, client_id},
static_cast<uint32_t>(what), amount, client_id};
send_command_t(ch, 0x60, 0x00, cmd);
}
void send_change_player_hp(std::shared_ptr<Lobby> l, uint16_t client_id, PlayerHPChange what, int16_t amount) {
for (const auto& lc : l->clients) {
if (lc) {
send_change_player_hp(lc->channel, client_id, what, amount);
}
}
}
asio::awaitable<void> send_remove_negative_conditions(shared_ptr<Client> c) {
G_AddStatusEffect_6x0C cmd;
cmd.header = {0x0C, sizeof(G_AddStatusEffect_6x0C) >> 2, c->lobby_client_id};
@@ -2788,10 +2842,9 @@ void send_game_enemy_state(shared_ptr<Client> c) {
auto s = c->require_server_state();
vector<SyncEnemyStateEntry> entries;
bool is_v3 = !is_v1_or_v2(c->version());
for (auto ene_st : l->map_state->iter_enemy_states(c->version())) {
auto& entry = entries.emplace_back();
entry.flags = ene_st->get_game_flags(is_v3);
entry.flags = ene_st->game_flags;
entry.item_drop_id = (ene_st->server_flags & MapState::EnemyState::Flag::ITEM_DROPPED)
? 0xFFFF
: (0xCA0 + l->map_state->index_for_enemy_state(c->version(), ene_st));
@@ -2929,7 +2982,7 @@ void send_game_flag_state_t(shared_ptr<Client> c) {
cmd.header.subcommand = 0x6F;
cmd.header.size = sizeof(CmdT) >> 2;
cmd.header.unused = 0x0000;
cmd.quest_flags = (l && !l->quest_flags_known) ? *l->quest_flag_values : c->character()->quest_flags;
cmd.quest_flags = (l && !l->quest_flags_known) ? *l->quest_flag_values : c->character_file()->quest_flags;
if (c->game_join_command_queue) {
c->log.info_f("Client not ready to receive join commands; adding to queue");
@@ -2978,7 +3031,7 @@ void send_game_player_state(shared_ptr<Client> to_c, shared_ptr<Client> from_c,
}
if (apply_overrides) {
auto from_p = from_c->character();
auto from_p = from_c->character_file();
to_send.base.pos.x = from_c->pos.x;
to_send.base.pos.y = 0.0;
to_send.base.pos.z = from_c->pos.z;
@@ -3143,17 +3196,15 @@ void send_bank(shared_ptr<Client> c) {
throw logic_error("6xBC can only be sent to BB clients");
}
auto p = c->character();
auto& bank = c->current_bank();
bank.sort();
const auto* items_it = bank.items.data();
vector<PlayerBankItem> items(items_it, items_it + bank.num_items);
auto p = c->character_file();
auto bank = c->bank_file();
bank->sort();
G_BankContentsHeader_BB_6xBC cmd = {
{{0xBC, 0, 0}, sizeof(G_BankContentsHeader_BB_6xBC) + items.size() * sizeof(PlayerBankItem)},
bank.checksum(), bank.num_items, bank.meseta};
{{0xBC, 0, 0}, sizeof(G_BankContentsHeader_BB_6xBC) + bank->items.size() * sizeof(PlayerBankItem)},
bank->bb_checksum(), bank->items.size(), bank->meseta};
send_command_t_vt(c, 0x6C, 0x00, cmd, items);
send_command_t_vt(c, 0x6C, 0x00, cmd, bank->items);
}
void send_shop(shared_ptr<Client> c, uint8_t shop_type) {
@@ -3179,7 +3230,7 @@ void send_shop(shared_ptr<Client> c, uint8_t shop_type) {
void send_level_up(shared_ptr<Client> c) {
auto l = c->require_lobby();
auto p = c->character();
auto p = c->character_file();
CharacterStats stats = p->disp.stats.char_stats;
const ItemData* mag = nullptr;
@@ -3217,7 +3268,12 @@ void send_set_exp_multiplier(shared_ptr<Lobby> l) {
if (!l->is_game()) {
throw logic_error("6xDD can only be sent in games (not in lobbies)");
}
G_SetEXPMultiplier_BB_6xDD cmd = {{0xDD, sizeof(G_SetEXPMultiplier_BB_6xDD) / 4, (l->mode == GameMode::CHALLENGE) ? 1 : l->base_exp_multiplier}};
G_SetFractionalEXPMultiplier_Extension_BB_6xDD cmd = {
{0xDD, sizeof(G_SetFractionalEXPMultiplier_Extension_BB_6xDD) / 4, 1}, 1.0f};
if (l->mode != GameMode::CHALLENGE) {
cmd.header.param = l->base_exp_multiplier;
cmd.multiplier = l->base_exp_multiplier;
}
for (auto lc : l->clients) {
if (lc && (lc->version() == Version::BB_V4)) {
send_command_t(lc, 0x60, 0x00, cmd);
@@ -3265,11 +3321,7 @@ void send_ep3_card_list_update(shared_ptr<Client> c) {
}
}
void send_ep3_media_update(
shared_ptr<Client> c,
uint32_t type,
uint32_t which,
const string& compressed_data) {
void send_ep3_media_update(shared_ptr<Client> c, uint32_t type, uint32_t which, const string& compressed_data) {
phosg::StringWriter w;
w.put<S_UpdateMediaHeader_Ep3_B9>({type, which, compressed_data.size(), 0});
w.write(compressed_data);
@@ -3470,7 +3522,7 @@ string ep3_description_for_client(shared_ptr<Client> c) {
if (!is_ep3(c->version())) {
throw runtime_error("client is not Episode 3");
}
auto p = c->character();
auto p = c->character_file();
return std::format(
"{} CLv{} {}",
name_for_char_class(p->disp.visual.char_class),
@@ -3520,7 +3572,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
if (player.is_human()) {
try {
auto other_c = account_id_to_client.at(player.account_id);
entry.name.encode(other_c->character()->disp.name.decode(other_c->language()), c->language());
entry.name.encode(other_c->character_file()->disp.name.decode(other_c->language()), c->language());
entry.description.encode(ep3_description_for_client(other_c), c->language());
} catch (const out_of_range&) {
entry.name.encode(player.player_name, c->language());
@@ -3541,7 +3593,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
for (auto spec_c : l->clients) {
if (spec_c) {
auto& entry = cmd.spectator_entries[cmd.num_spectators++];
entry.name.encode(spec_c->character()->disp.name.decode(spec_c->language()), c->language());
entry.name.encode(spec_c->character_file()->disp.name.decode(spec_c->language()), c->language());
entry.description.encode(ep3_description_for_client(spec_c), c->language());
}
}
@@ -3558,7 +3610,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
size_t num_players = 0;
for (const auto& opp_c : primary_lobby->clients) {
if (opp_c) {
cmd.player_entries[num_players].name.encode(opp_c->character()->disp.name.decode(opp_c->language()), c->language());
cmd.player_entries[num_players].name.encode(opp_c->character_file()->disp.name.decode(opp_c->language()), c->language());
cmd.player_entries[num_players].description.encode(ep3_description_for_client(opp_c), c->language());
num_players++;
}
@@ -3571,7 +3623,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
for (auto spec_c : l->clients) {
if (spec_c) {
auto& entry = cmd.spectator_entries[num_spectators++];
entry.name.encode(spec_c->character()->disp.name.decode(spec_c->language()), c->language());
entry.name.encode(spec_c->character_file()->disp.name.decode(spec_c->language()), c->language());
entry.description.encode(ep3_description_for_client(spec_c), c->language());
}
}
@@ -3692,7 +3744,7 @@ void send_ep3_tournament_match_result(shared_ptr<Lobby> l, uint32_t meseta_rewar
if (player.is_human()) {
try {
auto pc = account_id_to_client.at(player.account_id);
entry.player_names[z].encode(pc->character()->disp.name.decode(pc->language()), lc->language());
entry.player_names[z].encode(pc->character_file()->disp.name.decode(pc->language()), lc->language());
} catch (const out_of_range&) {
entry.player_names[z].encode(player.player_name, lc->language());
}
@@ -3978,12 +4030,11 @@ void send_open_quest_file(
if (chunk_bytes > 0x400) {
chunk_bytes = 0x400;
}
send_quest_file_chunk(c, filename, offset / 0x400,
contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
send_quest_file_chunk(c, filename, offset / 0x400, contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
}
// If there are still chunks to send, track the file so the chunk
// acknowledgement handler (13 or A7) cna know what to send next
// acknowledgement handler (13 or A7) can know what to send next
if (chunks_to_send < total_chunks) {
c->sending_files.emplace(filename, contents);
c->log.info_f("Opened file {}", filename);
@@ -4182,7 +4233,7 @@ static S_TeamInfoForPlayer_BB_13EA_15EA_Entry team_metadata_for_client(shared_pt
S_TeamInfoForPlayer_BB_13EA_15EA_Entry cmd;
cmd.lobby_client_id = c->lobby_client_id;
cmd.guild_card_number = c->login->account->account_id;
cmd.player_name = c->character()->disp.name;
cmd.player_name = c->character_file()->disp.name;
if (team) {
cmd.membership = team->base_membership_for_member(c->login->account->account_id);
if (team->flag_data) {
@@ -4324,7 +4375,8 @@ void send_team_reward_list(shared_ptr<Client> c, bool show_purchased) {
auto s = c->require_server_state();
// Hide item rewards if the player's bank is full
bool show_item_rewards = show_purchased || (c->current_bank().num_items < 200);
auto bank = c->bank_file();
bool show_item_rewards = show_purchased || (bank->items.size() < bank->max_items);
vector<S_TeamRewardList_BB_19EA_1AEA::Entry> entries;
for (const auto& reward : s->team_index->reward_definitions()) {
+22 -13
View File
@@ -118,8 +118,12 @@ void send_command_vt(std::shared_ptr<Channel> ch, uint16_t command, uint32_t fla
}
template <typename TargetT, typename StructT, typename EntryT>
void send_command_t_vt(std::shared_ptr<TargetT> c, uint16_t command,
uint32_t flag, const StructT& data, const std::vector<EntryT>& array_data) {
void send_command_t_vt(
std::shared_ptr<TargetT> c,
uint16_t command,
uint32_t flag,
const StructT& data,
const std::vector<EntryT>& array_data) {
std::string all_data(reinterpret_cast<const char*>(&data), sizeof(StructT));
all_data.append(reinterpret_cast<const char*>(array_data.data()),
array_data.size() * sizeof(EntryT));
@@ -186,7 +190,7 @@ asio::awaitable<C_ExecuteCodeResult_B3> send_function_call(
bool ignore_actually_runs_code_flag = false);
asio::awaitable<void> send_function_call_multi(
std::shared_ptr<Client> c,
std::vector<std::shared_ptr<const CompiledFunctionCode>> codes);
std::unordered_set<std::shared_ptr<const CompiledFunctionCode>> codes);
asio::awaitable<bool> send_protected_command(std::shared_ptr<Client> c, const void* data, size_t size, bool echo_to_lobby);
asio::awaitable<void> send_dol_file(std::shared_ptr<Client> c, std::shared_ptr<DOLFileIndex::File> dol);
@@ -308,11 +312,8 @@ void send_quest_menu(
std::shared_ptr<Client> c,
const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests,
bool is_download_menu);
void send_quest_categories_menu(
std::shared_ptr<Client> c,
std::shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode);
void send_ep3_download_quest_menu(std::shared_ptr<Client> c);
void send_quest_categories_menu(std::shared_ptr<Client> c, QuestMenuType menu_type, Episode episode);
void send_lobby_list(std::shared_ptr<Client> c);
void send_player_records(
@@ -334,7 +335,7 @@ void send_arrow_update(std::shared_ptr<Lobby> l);
void send_unblock_join(std::shared_ptr<Client> c);
void send_resume_game(std::shared_ptr<Lobby> l, std::shared_ptr<Client> ready_client);
enum PlayerStatsChange {
enum class PlayerStatsChange {
SUBTRACT_HP = 0,
SUBTRACT_TP = 1,
SUBTRACT_MESETA = 2,
@@ -342,10 +343,18 @@ enum PlayerStatsChange {
ADD_TP = 4,
};
void send_player_stats_change(
std::shared_ptr<Client> c, PlayerStatsChange stat, uint32_t amount);
void send_player_stats_change(
std::shared_ptr<Channel> ch, uint16_t client_id, PlayerStatsChange stat, uint32_t amount);
void send_player_stats_change(std::shared_ptr<Client> c, PlayerStatsChange stat, uint32_t amount);
void send_player_stats_change(std::shared_ptr<Channel> ch, uint16_t client_id, PlayerStatsChange stat, uint32_t amount);
enum class PlayerHPChange {
SET_HP = 0,
INCREMENT_HP = 1,
MAXIMIZE_HP = 2,
};
void send_change_player_hp(std::shared_ptr<Channel> ch, uint16_t client_id, PlayerHPChange what, int16_t amount);
void send_change_player_hp(std::shared_ptr<Lobby> l, uint16_t client_id, PlayerHPChange what, int16_t amount);
asio::awaitable<void> send_remove_negative_conditions(std::shared_ptr<Client> c);
void send_remove_negative_conditions(std::shared_ptr<Channel> ch, uint16_t client_id);
void send_warp(std::shared_ptr<Channel> ch, uint8_t client_id, uint32_t floor, bool is_private);
+203 -124
View File
@@ -72,8 +72,7 @@ ServerState::ServerState(const string& config_filename)
thread_pool(make_unique<asio::thread_pool>()),
bb_stream_files_cache(new FileContentsCache(3600000000ULL)),
bb_system_cache(new FileContentsCache(3600000000ULL)),
gba_files_cache(new FileContentsCache(3600000000ULL)),
player_files_manager(make_shared<PlayerFilesManager>(this->io_context)) {}
gba_files_cache(new FileContentsCache(3600000000ULL)) {}
void ServerState::add_client_to_available_lobby(shared_ptr<Client> c) {
shared_ptr<Lobby> added_to_lobby;
@@ -289,7 +288,7 @@ uint32_t ServerState::connect_address_for_client(shared_ptr<Client> c) const {
{
auto socket_channel = dynamic_pointer_cast<SocketChannel>(c->channel);
if (socket_channel) {
uint32_t addr = ipv4_addr_for_asio_addr(socket_channel->local_addr.address());
uint32_t addr = ipv4_addr_for_asio_addr(socket_channel->remote_addr.address());
uint32_t ret = is_local_address(addr) ? this->local_address : this->external_address;
return ret ? ret : addr;
}
@@ -395,28 +394,21 @@ shared_ptr<const vector<string>> ServerState::information_contents_for_client(sh
return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3;
}
shared_ptr<const QuestIndex> ServerState::quest_index(Version version) const {
return is_ep3(version) ? this->ep3_download_quest_index : this->default_quest_index;
}
size_t ServerState::default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const {
// A player's actual level is their displayed level - 1, so the minimums for
// Episode 1 (for example) are actually 1, 20, 40, 80.
const auto& min_levels = is_v4(version)
? this->min_levels_v4
: is_v3(version)
? this->min_levels_v3
: this->min_levels_v1_v2;
switch (episode) {
case Episode::EP1: {
const auto& min_levels = (version == Version::BB_V4) ? this->min_levels_v4[0] : DEFAULT_MIN_LEVELS_V3;
return min_levels.at(difficulty);
}
case Episode::EP2: {
const auto& min_levels = (version == Version::BB_V4) ? this->min_levels_v4[1] : DEFAULT_MIN_LEVELS_V3;
return min_levels.at(difficulty);
}
case Episode::EP1:
return min_levels[0].at(difficulty);
case Episode::EP2:
return min_levels[1].at(difficulty);
case Episode::EP3:
return 0;
case Episode::EP4: {
const auto& min_levels = (version == Version::BB_V4) ? this->min_levels_v4[2] : DEFAULT_MIN_LEVELS_V3;
return min_levels.at(difficulty);
}
case Episode::EP4:
return min_levels[2].at(difficulty);
default:
throw runtime_error("invalid episode");
}
@@ -501,13 +493,13 @@ shared_ptr<const ItemNameIndex> ServerState::item_name_index(Version version) co
return ret;
}
string ServerState::describe_item(Version version, const ItemData& item, bool include_color_codes) const {
string ServerState::describe_item(Version version, const ItemData& item, uint8_t flags) const {
if (is_v1(version)) {
ItemData encoded = item;
encoded.encode_for_version(version, this->item_parameter_table(version));
return this->item_name_index(version)->describe_item(encoded, include_color_codes);
return this->item_name_index(version)->describe_item(encoded, flags);
} else {
return this->item_name_index(version)->describe_item(item, include_color_codes);
return this->item_name_index(version)->describe_item(item, flags);
}
}
@@ -515,6 +507,35 @@ ItemData ServerState::parse_item_description(Version version, const string& desc
return this->item_name_index(version)->parse_item_description(description);
}
shared_ptr<const CommonItemSet> ServerState::common_item_set(Version logic_version, shared_ptr<const Quest> q) const {
if (q && q->meta.common_item_set) {
return q->meta.common_item_set;
} else if (is_v1_or_v2(logic_version)) {
// TODO: We should probably have a v1 common item set at some point too
return this->common_item_sets.at("common-table-v1-v2");
} else if (is_v3(logic_version) || is_v4(logic_version)) {
return this->common_item_sets.at("common-table-v3-v4");
} else {
throw runtime_error(std::format("no default common item set is available for {}", phosg::name_for_enum(logic_version)));
}
}
shared_ptr<const RareItemSet> ServerState::rare_item_set(Version logic_version, shared_ptr<const Quest> q) const {
if (q && q->meta.rare_item_set) {
return q->meta.rare_item_set;
} else if (is_v1(logic_version)) {
return this->rare_item_sets.at("rare-table-v1");
} else if (is_v2(logic_version)) {
return this->rare_item_sets.at("rare-table-v2");
} else if (is_v3(logic_version)) {
return this->rare_item_sets.at("rare-table-v3");
} else if (is_v4(logic_version)) {
return this->rare_item_sets.at("rare-table-v4");
} else {
throw runtime_error(std::format("no default rare item set is available for {}", phosg::name_for_enum(logic_version)));
}
}
void ServerState::set_port_configuration(const vector<PortConfiguration>& port_configs) {
this->name_to_port_config.clear();
this->number_to_port_config.clear();
@@ -838,22 +859,22 @@ void ServerState::load_config_early() {
this->allowed_drop_modes_v4_normal = this->config_json->get_int("AllowedDropModesV4Normal", 0x1D);
this->allowed_drop_modes_v4_battle = this->config_json->get_int("AllowedDropModesV4Battle", 0x05);
this->allowed_drop_modes_v4_challenge = this->config_json->get_int("AllowedDropModesV4Challenge", 0x05);
this->default_drop_mode_v1_v2_normal = this->config_json->get_enum("DefaultDropModeV1V2Normal", Lobby::DropMode::CLIENT);
this->default_drop_mode_v1_v2_battle = this->config_json->get_enum("DefaultDropModeV1V2Battle", Lobby::DropMode::CLIENT);
this->default_drop_mode_v1_v2_challenge = this->config_json->get_enum("DefaultDropModeV1V2Challenge", Lobby::DropMode::CLIENT);
this->default_drop_mode_v3_normal = this->config_json->get_enum("DefaultDropModeV3Normal", Lobby::DropMode::CLIENT);
this->default_drop_mode_v3_battle = this->config_json->get_enum("DefaultDropModeV3Battle", Lobby::DropMode::CLIENT);
this->default_drop_mode_v3_challenge = this->config_json->get_enum("DefaultDropModeV3Challenge", Lobby::DropMode::CLIENT);
this->default_drop_mode_v4_normal = this->config_json->get_enum("DefaultDropModeV4Normal", Lobby::DropMode::SERVER_SHARED);
this->default_drop_mode_v4_battle = this->config_json->get_enum("DefaultDropModeV4Battle", Lobby::DropMode::SERVER_SHARED);
this->default_drop_mode_v4_challenge = this->config_json->get_enum("DefaultDropModeV4Challenge", Lobby::DropMode::SERVER_SHARED);
if ((this->default_drop_mode_v4_normal == Lobby::DropMode::CLIENT) ||
(this->default_drop_mode_v4_battle == Lobby::DropMode::CLIENT) ||
(this->default_drop_mode_v4_challenge == Lobby::DropMode::CLIENT)) {
this->default_drop_mode_v1_v2_normal = this->config_json->get_enum("DefaultDropModeV1V2Normal", ServerDropMode::CLIENT);
this->default_drop_mode_v1_v2_battle = this->config_json->get_enum("DefaultDropModeV1V2Battle", ServerDropMode::CLIENT);
this->default_drop_mode_v1_v2_challenge = this->config_json->get_enum("DefaultDropModeV1V2Challenge", ServerDropMode::CLIENT);
this->default_drop_mode_v3_normal = this->config_json->get_enum("DefaultDropModeV3Normal", ServerDropMode::CLIENT);
this->default_drop_mode_v3_battle = this->config_json->get_enum("DefaultDropModeV3Battle", ServerDropMode::CLIENT);
this->default_drop_mode_v3_challenge = this->config_json->get_enum("DefaultDropModeV3Challenge", ServerDropMode::CLIENT);
this->default_drop_mode_v4_normal = this->config_json->get_enum("DefaultDropModeV4Normal", ServerDropMode::SERVER_SHARED);
this->default_drop_mode_v4_battle = this->config_json->get_enum("DefaultDropModeV4Battle", ServerDropMode::SERVER_SHARED);
this->default_drop_mode_v4_challenge = this->config_json->get_enum("DefaultDropModeV4Challenge", ServerDropMode::SERVER_SHARED);
if ((this->default_drop_mode_v4_normal == ServerDropMode::CLIENT) ||
(this->default_drop_mode_v4_battle == ServerDropMode::CLIENT) ||
(this->default_drop_mode_v4_challenge == ServerDropMode::CLIENT)) {
throw runtime_error("default V4 drop mode cannot be CLIENT");
}
if ((this->allowed_drop_modes_v4_normal & (1 << static_cast<size_t>(Lobby::DropMode::CLIENT))) ||
(this->allowed_drop_modes_v4_battle & (1 << static_cast<size_t>(Lobby::DropMode::CLIENT))) || (this->allowed_drop_modes_v4_challenge & (1 << static_cast<size_t>(Lobby::DropMode::CLIENT)))) {
if ((this->allowed_drop_modes_v4_normal & (1 << static_cast<size_t>(ServerDropMode::CLIENT))) ||
(this->allowed_drop_modes_v4_battle & (1 << static_cast<size_t>(ServerDropMode::CLIENT))) || (this->allowed_drop_modes_v4_challenge & (1 << static_cast<size_t>(ServerDropMode::CLIENT)))) {
throw runtime_error("CLIENT drop mode cannot be allowed in V4");
}
@@ -966,10 +987,10 @@ void ServerState::load_config_early() {
} else if (lower_path.ends_with(".gvm")) {
decompressed_gvm_data = phosg::load_file(path);
} else if (lower_path.ends_with(".bmp")) {
phosg::Image img(path);
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(path));
decompressed_gvm_data = encode_gvm(
img,
img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565,
has_any_transparent_pixels(img) ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565,
std::format("bnr{}", banner_index),
0x80 | banner_index);
banner_index++;
@@ -1039,6 +1060,9 @@ void ServerState::load_config_early() {
} catch (const out_of_range&) {
}
this->bb_max_bank_items = this->config_json->get_int("BBMaxBankItems", 200);
this->bb_max_bank_meseta = this->config_json->get_int("BBMaxBankMeseta", 999999);
for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) {
if (!this->item_stack_limits_tables[v_s]) {
Version v = static_cast<Version>(v_s);
@@ -1055,11 +1079,15 @@ void ServerState::load_config_early() {
}
}
this->bb_global_exp_multiplier = this->config_json->get_int("BBGlobalEXPMultiplier", 1);
this->exp_share_multiplier = this->config_json->get_float("BBEXPShareMultiplier", 0.5);
this->server_global_drop_rate_multiplier = this->config_json->get_float("ServerGlobalDropRateMultiplier", 1);
this->bb_global_exp_multiplier = this->config_json->get_float("BBGlobalEXPMultiplier", 1.0f);
this->exp_share_multiplier = this->config_json->get_float("BBEXPShareMultiplier", 0.5f);
this->server_global_drop_rate_multiplier = this->config_json->get_float("ServerGlobalDropRateMultiplier", 1.0f);
set_log_levels_from_json(this->config_json->get("LogLevels", phosg::JSON::dict()));
if (this->is_debug) {
set_all_log_levels(phosg::LogLevel::L_DEBUG);
} else {
set_log_levels_from_json(this->config_json->get("LogLevels", phosg::JSON::dict()));
}
try {
this->run_shell_behavior = this->config_json->at("RunInteractiveShell").as_bool()
@@ -1095,6 +1123,7 @@ void ServerState::load_config_early() {
}
this->enable_chat_commands = this->config_json->get_bool("EnableChatCommands", true);
this->num_backup_character_slots = this->config_json->get_int("BackupCharacterSlots", 16);
this->version_name_colors.reset();
this->client_customization_name_color = 0;
@@ -1268,36 +1297,54 @@ void ServerState::load_config_early() {
this->rare_enemy_rates_challenge = MapState::DEFAULT_RARE_ENEMIES;
}
this->min_levels_v1_v2[0] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v1_v2[1] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v1_v2[2] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v3[0] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v3[1] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v3[2] = DEFAULT_MIN_LEVELS_V123;
this->min_levels_v4[0] = DEFAULT_MIN_LEVELS_V4_EP1;
this->min_levels_v4[1] = DEFAULT_MIN_LEVELS_V4_EP2;
this->min_levels_v4[2] = DEFAULT_MIN_LEVELS_V4_EP4;
try {
for (const auto& ep_it : this->config_json->get_dict("BBMinimumLevels")) {
array<size_t, 4> levels({0, 0, 0, 0});
for (size_t z = 0; z < 4; z++) {
levels[z] = ep_it.second->get_int(z) - 1;
}
switch (episode_for_token_name(ep_it.first)) {
case Episode::EP1:
this->min_levels_v4[0] = levels;
break;
case Episode::EP2:
this->min_levels_v4[1] = levels;
break;
case Episode::EP4:
this->min_levels_v4[2] = levels;
break;
default:
throw runtime_error("unknown episode");
auto populate_min_levels = [&](std::array<std::array<size_t, 4>, 3>& dest, const char* key_name) -> void {
try {
for (const auto& ep_it : this->config_json->get_dict(key_name)) {
array<size_t, 4> levels({0, 0, 0, 0});
for (size_t z = 0; z < 4; z++) {
levels[z] = ep_it.second->get_int(z) - 1;
}
switch (episode_for_token_name(ep_it.first)) {
case Episode::EP1:
dest[0] = levels;
break;
case Episode::EP2:
dest[1] = levels;
break;
case Episode::EP4:
dest[2] = levels;
break;
default:
throw runtime_error("unknown episode");
}
}
} catch (const out_of_range&) {
}
} catch (const out_of_range&) {
}
};
populate_min_levels(this->min_levels_v1_v2, "V1V2MinimumLevels");
populate_min_levels(this->min_levels_v3, "V3MinimumLevels");
populate_min_levels(this->min_levels_v4, "BBMinimumLevels");
this->bb_required_patches.clear();
try {
for (const auto& it : this->config_json->get_list("BBRequiredPatches")) {
this->bb_required_patches.emplace_back(it->as_string());
this->bb_required_patches.emplace(it->as_string());
}
} catch (const out_of_range&) {
}
this->auto_patches.clear();
try {
for (const auto& it : this->config_json->get_list("AutoPatches")) {
this->auto_patches.emplace(it->as_string());
}
} catch (const out_of_range&) {
}
@@ -1920,61 +1967,103 @@ void ServerState::load_item_name_indexes() {
}
void ServerState::load_drop_tables() {
config_log.info_f("Loading rare item sets");
config_log.info_f("Loading item sets");
unordered_map<string, shared_ptr<RareItemSet>> new_rare_item_sets;
unordered_map<string, shared_ptr<const RareItemSet>> new_rare_item_sets;
unordered_map<string, shared_ptr<const CommonItemSet>> new_common_item_sets;
for (const auto& item : std::filesystem::directory_iterator("system/item-tables")) {
string filename = item.path().filename().string();
if (!filename.starts_with("rare-table-")) {
continue;
}
string path = "system/item-tables/" + filename;
size_t ext_offset = filename.rfind('.');
string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset);
if (filename.starts_with("common-table-") || filename.starts_with("ItemPT-")) {
string path = "system/item-tables/" + filename;
size_t ext_offset = filename.rfind('.');
string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset);
if (filename.ends_with("-v1.json")) {
config_log.info_f("Loading v1 JSON rare item table {}", filename);
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::DC_V1)));
} else if (filename.ends_with("-v2.json")) {
config_log.info_f("Loading v2 JSON rare item table {}", filename);
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::PC_V2)));
} else if (filename.ends_with("-v3.json")) {
config_log.info_f("Loading v3 JSON rare item table {}", filename);
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::GC_V3)));
} else if (filename.ends_with("-v4.json")) {
config_log.info_f("Loading v4 JSON rare item table {}", filename);
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::BB_V4)));
// AFSV2CommonItemSet(std::shared_ptr<const std::string> pt_afs_data, std::shared_ptr<const std::string> ct_afs_data);
} else if (filename.ends_with(".afs")) {
config_log.info_f("Loading AFS rare item table {}", filename);
auto data = make_shared<string>(phosg::load_file(path));
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(AFSArchive(data), false));
if (filename.ends_with(".json")) {
config_log.info_f("Loading JSON common item table {}", filename);
new_common_item_sets.emplace(basename, make_shared<JSONCommonItemSet>(phosg::JSON::parse(phosg::load_file(path))));
} else if (filename.ends_with(".afs")) {
string ct_filename;
if (filename.starts_with("ItemPT-")) {
ct_filename = "ItemCT-" + filename.substr(7);
} else if (filename.starts_with("common-table-")) {
ct_filename = "challenge-common-table-" + filename.substr(13);
} else {
throw std::runtime_error(std::format("cannot determine challenge table filename for common table file: {}", filename));
}
auto data = make_shared<string>(phosg::load_file(path));
shared_ptr<string> ct_data;
try {
string ct_path = "system/item-tables/" + ct_filename;
ct_data = make_shared<string>(phosg::load_file(ct_path));
config_log.info_f("Loading AFS common item table {} with challenge table {}", filename, ct_filename);
} catch (const phosg::cannot_open_file&) {
config_log.info_f("Loading AFS common item table {} without challenge table", filename);
}
new_common_item_sets.emplace(basename, make_shared<AFSV2CommonItemSet>(data, ct_data));
} else if (filename.ends_with(".gsl")) {
config_log.info_f("Loading little-endian GSL common item table {}", filename);
auto data = make_shared<string>(phosg::load_file(path));
new_common_item_sets.emplace(basename, make_shared<GSLV3V4CommonItemSet>(data, false));
} else if (filename.ends_with(".gslb")) {
config_log.info_f("Loading big-endian GSL common item table {}", filename);
auto data = make_shared<string>(phosg::load_file(path));
new_common_item_sets.emplace(basename, make_shared<GSLV3V4CommonItemSet>(data, true));
} else {
throw std::runtime_error(std::format("unknown format for common table file: {}", filename));
}
} else if (filename.ends_with(".gsl")) {
config_log.info_f("Loading GSL rare item table {}", filename);
auto data = make_shared<string>(phosg::load_file(path));
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(GSLArchive(data, false), false));
} else if (filename.starts_with("rare-table-") || filename.starts_with("ItemRT-")) {
string path = "system/item-tables/" + filename;
size_t ext_offset = filename.rfind('.');
string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset);
} else if (filename.ends_with(".gslb")) {
config_log.info_f("Loading GSL rare item table {}", filename);
auto data = make_shared<string>(phosg::load_file(path));
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(GSLArchive(data, true), true));
shared_ptr<RareItemSet> rare_set;
if (filename.ends_with("-v1.json")) {
config_log.info_f("Loading v1 JSON rare item table {}", filename);
rare_set = make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::DC_V1));
} else if (filename.ends_with("-v2.json")) {
config_log.info_f("Loading v2 JSON rare item table {}", filename);
rare_set = make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::PC_V2));
} else if (filename.ends_with("-v3.json")) {
config_log.info_f("Loading v3 JSON rare item table {}", filename);
rare_set = make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::GC_V3));
} else if (filename.ends_with("-v4.json")) {
config_log.info_f("Loading v4 JSON rare item table {}", filename);
rare_set = make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::BB_V4));
} else if (filename.ends_with(".rel")) {
config_log.info_f("Loading REL rare item table {}", filename);
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(phosg::load_file(path), true));
} else if (filename.ends_with(".afs")) {
config_log.info_f("Loading AFS rare item table {}", filename);
auto data = make_shared<string>(phosg::load_file(path));
rare_set = make_shared<RareItemSet>(AFSArchive(data), false);
} else if (filename.ends_with(".gsl")) {
config_log.info_f("Loading GSL rare item table {}", filename);
auto data = make_shared<string>(phosg::load_file(path));
rare_set = make_shared<RareItemSet>(GSLArchive(data, false), false);
} else if (filename.ends_with(".gslb")) {
config_log.info_f("Loading GSL rare item table {}", filename);
auto data = make_shared<string>(phosg::load_file(path));
rare_set = make_shared<RareItemSet>(GSLArchive(data, true), true);
} else if (filename.ends_with(".rel")) {
config_log.info_f("Loading REL rare item table {}", filename);
rare_set = make_shared<RareItemSet>(phosg::load_file(path), true);
} else {
throw std::runtime_error(std::format("unknown format for rare table file: {}", filename));
}
if (this->server_global_drop_rate_multiplier != 1.0) {
rare_set->multiply_all_rates(this->server_global_drop_rate_multiplier);
}
new_rare_item_sets.emplace(basename, std::move(rare_set));
}
}
config_log.info_f("Loading v2 common item table");
auto ct_data_v2 = make_shared<string>(phosg::load_file("system/item-tables/ItemCT-pc-v2.afs"));
auto pt_data_v2 = make_shared<string>(phosg::load_file("system/item-tables/ItemPT-pc-v2.afs"));
auto new_common_item_set_v2 = make_shared<AFSV2CommonItemSet>(pt_data_v2, ct_data_v2);
config_log.info_f("Loading v3+v4 common item table");
auto pt_data_v3_v4 = make_shared<string>(phosg::load_file("system/item-tables/ItemPT-gc-v3.gsl"));
auto new_common_item_set_v3_v4 = make_shared<GSLV3V4CommonItemSet>(pt_data_v3_v4, true);
config_log.info_f("Loading armor table");
auto armor_data = make_shared<string>(phosg::load_file("system/item-tables/ArmorRandom-gc-v3.rel"));
auto new_armor_random_set = make_shared<ArmorRandomSet>(armor_data);
@@ -2000,19 +2089,8 @@ void ServerState::load_drop_tables() {
auto tekker_data = make_shared<string>(phosg::load_file("system/item-tables/JudgeItem-gc-v3.rel"));
auto new_tekker_adjustment_set = make_shared<TekkerAdjustmentSet>(tekker_data);
if (this->server_global_drop_rate_multiplier != 1.0) {
for (auto& it : new_rare_item_sets) {
it.second->multiply_all_rates(this->server_global_drop_rate_multiplier);
}
}
// We can't just std::move() new_rare_item_sets into place because its values are
// not const :(
this->rare_item_sets.clear();
for (auto& it : new_rare_item_sets) {
this->rare_item_sets.emplace(it.first, std::move(it.second));
}
this->common_item_set_v2 = std::move(new_common_item_set_v2);
this->common_item_set_v3_v4 = std::move(new_common_item_set_v3_v4);
this->rare_item_sets = std::move(new_rare_item_sets);
this->common_item_sets = std::move(new_common_item_sets);
this->armor_random_set = std::move(new_armor_random_set);
this->tool_random_set = std::move(new_tool_random_set);
this->weapon_random_sets = std::move(new_weapon_random_sets);
@@ -2075,6 +2153,8 @@ void ServerState::load_ep3_cards() {
void ServerState::load_ep3_maps() {
config_log.info_f("Collecting Episode 3 maps");
this->ep3_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps");
config_log.info_f("Collecting Episode 3 download maps");
this->ep3_download_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps-download");
}
void ServerState::load_ep3_tournament_state() {
@@ -2087,9 +2167,8 @@ void ServerState::load_ep3_tournament_state() {
void ServerState::load_quest_index() {
config_log.info_f("Collecting quests");
this->default_quest_index = make_shared<QuestIndex>("system/quests", this->quest_category_index, false);
config_log.info_f("Collecting Episode 3 download quests");
this->ep3_download_quest_index = make_shared<QuestIndex>("system/ep3/maps-download", this->quest_category_index, true);
this->quest_index = make_shared<QuestIndex>(
"system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets);
}
void ServerState::compile_functions() {
+27 -21
View File
@@ -24,7 +24,6 @@
#include "LevelTable.hh"
#include "Lobby.hh"
#include "Menu.hh"
#include "PlayerFilesManager.hh"
#include "Quest.hh"
#include "TeamIndex.hh"
#include "WordSelectTable.hh"
@@ -109,12 +108,14 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
uint64_t client_ping_interval_usecs = 30000000;
uint64_t client_idle_timeout_usecs = 60000000;
uint64_t patch_client_idle_timeout_usecs = 300000000;
bool is_debug = false;
bool ip_stack_debug = false;
bool allow_unregistered_users = false;
bool allow_pc_nte = false;
bool use_temp_accounts_for_prototypes = true;
std::array<uint16_t, NUM_VERSIONS> compatibility_groups = {};
bool enable_chat_commands = true;
size_t num_backup_character_slots = 16;
std::unique_ptr<std::array<uint32_t, NUM_NON_PATCH_VERSIONS>> version_name_colors;
uint32_t client_customization_name_color = 0x00000000;
uint8_t allowed_drop_modes_v1_v2_normal = 0x1F;
@@ -126,15 +127,15 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
uint8_t allowed_drop_modes_v4_normal = 0x1D; // CLIENT not allowed
uint8_t allowed_drop_modes_v4_battle = 0x05;
uint8_t allowed_drop_modes_v4_challenge = 0x05;
Lobby::DropMode default_drop_mode_v1_v2_normal = Lobby::DropMode::CLIENT;
Lobby::DropMode default_drop_mode_v1_v2_battle = Lobby::DropMode::CLIENT;
Lobby::DropMode default_drop_mode_v1_v2_challenge = Lobby::DropMode::CLIENT;
Lobby::DropMode default_drop_mode_v3_normal = Lobby::DropMode::CLIENT;
Lobby::DropMode default_drop_mode_v3_battle = Lobby::DropMode::CLIENT;
Lobby::DropMode default_drop_mode_v3_challenge = Lobby::DropMode::CLIENT;
Lobby::DropMode default_drop_mode_v4_normal = Lobby::DropMode::SERVER_SHARED;
Lobby::DropMode default_drop_mode_v4_battle = Lobby::DropMode::SERVER_SHARED;
Lobby::DropMode default_drop_mode_v4_challenge = Lobby::DropMode::SERVER_SHARED;
ServerDropMode default_drop_mode_v1_v2_normal = ServerDropMode::CLIENT;
ServerDropMode default_drop_mode_v1_v2_battle = ServerDropMode::CLIENT;
ServerDropMode default_drop_mode_v1_v2_challenge = ServerDropMode::CLIENT;
ServerDropMode default_drop_mode_v3_normal = ServerDropMode::CLIENT;
ServerDropMode default_drop_mode_v3_battle = ServerDropMode::CLIENT;
ServerDropMode default_drop_mode_v3_challenge = ServerDropMode::CLIENT;
ServerDropMode default_drop_mode_v4_normal = ServerDropMode::SERVER_SHARED;
ServerDropMode default_drop_mode_v4_battle = ServerDropMode::SERVER_SHARED;
ServerDropMode default_drop_mode_v4_challenge = ServerDropMode::SERVER_SHARED;
std::unordered_map<uint16_t, IntegralExpression> quest_flag_rewrites_v1_v2;
std::unordered_map<uint16_t, IntegralExpression> quest_flag_rewrites_v3;
std::unordered_map<uint16_t, IntegralExpression> quest_flag_rewrites_v4;
@@ -182,21 +183,20 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const Episode3::CardIndex> ep3_card_index;
std::shared_ptr<const Episode3::CardIndex> ep3_card_index_trial;
std::shared_ptr<const Episode3::MapIndex> ep3_map_index;
std::shared_ptr<const Episode3::MapIndex> ep3_download_map_index;
std::shared_ptr<const Episode3::COMDeckIndex> ep3_com_deck_index;
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_default_ex_values;
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_tournament_ex_values;
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_tournament_final_round_ex_values;
std::shared_ptr<const QuestCategoryIndex> quest_category_index;
std::shared_ptr<const QuestIndex> default_quest_index;
std::shared_ptr<const QuestIndex> ep3_download_quest_index;
std::shared_ptr<const QuestIndex> quest_index;
std::shared_ptr<const LevelTableV2> level_table_v1_v2;
std::shared_ptr<const LevelTable> level_table_v3;
std::shared_ptr<const LevelTable> level_table_v4;
std::shared_ptr<const BattleParamsIndex> battle_params;
std::shared_ptr<const GSLArchive> bb_data_gsl;
std::unordered_map<std::string, std::shared_ptr<const CommonItemSet>> common_item_sets;
std::unordered_map<std::string, std::shared_ptr<const RareItemSet>> rare_item_sets;
std::shared_ptr<const CommonItemSet> common_item_set_v2;
std::shared_ptr<const CommonItemSet> common_item_set_v3_v4;
std::shared_ptr<const ArmorRandomSet> armor_random_set;
std::shared_ptr<const ToolRandomSet> tool_random_set;
std::array<std::shared_ptr<const WeaponRandomSet>, 4> weapon_random_sets;
@@ -204,6 +204,8 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::array<std::shared_ptr<const ItemParameterTable>, NUM_VERSIONS> item_parameter_tables;
std::shared_ptr<const ItemTranslationTable> item_translation_table;
std::array<std::shared_ptr<const ItemData::StackLimits>, NUM_VERSIONS> item_stack_limits_tables;
size_t bb_max_bank_items = 200;
size_t bb_max_bank_meseta = 999999;
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v1_v2;
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v3;
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v4;
@@ -216,8 +218,11 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const SetDataTableBase> bb_solo_set_data_table_ep1_ult;
std::array<std::shared_ptr<const MapState::RareEnemyRates>, 4> rare_enemy_rates_by_difficulty;
std::shared_ptr<const MapState::RareEnemyRates> rare_enemy_rates_challenge;
std::array<std::array<size_t, 4>, 3> min_levels_v1_v2; // Indexed as [episode][difficulty]
std::array<std::array<size_t, 4>, 3> min_levels_v3; // Indexed as [episode][difficulty]
std::array<std::array<size_t, 4>, 3> min_levels_v4; // Indexed as [episode][difficulty]
std::vector<std::string> bb_required_patches;
std::unordered_set<std::string> bb_required_patches;
std::unordered_set<std::string> auto_patches;
CheatFlags cheat_flags;
struct QuestF960Result {
@@ -236,9 +241,9 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::vector<QuestF960Result> quest_F960_success_results;
QuestF960Result quest_F960_failure_results;
std::vector<ItemData> secret_lottery_results;
uint16_t bb_global_exp_multiplier = 1;
float exp_share_multiplier = 0.5;
double server_global_drop_rate_multiplier = 1.0;
float bb_global_exp_multiplier = 1.0f;
float exp_share_multiplier = 0.5f;
float server_global_drop_rate_multiplier = 1.0f;
std::shared_ptr<Episode3::TournamentIndex> ep3_tournament_index;
@@ -283,7 +288,6 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::string pc_patch_server_message;
std::string bb_patch_server_message;
std::shared_ptr<PlayerFilesManager> player_files_manager;
std::map<int64_t, std::shared_ptr<Lobby>> id_to_lobby;
std::array<std::vector<uint32_t>, NUM_VERSIONS> public_lobby_search_orders;
std::vector<uint32_t> client_customization_public_lobby_search_order;
@@ -349,9 +353,12 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const ItemData::StackLimits> item_stack_limits(Version version) const;
std::shared_ptr<const ItemNameIndex> item_name_index_opt(Version version) const; // Returns null if missing
std::shared_ptr<const ItemNameIndex> item_name_index(Version version) const; // Throws if missing
std::string describe_item(Version version, const ItemData& item, bool include_color_codes) const;
std::string describe_item(Version version, const ItemData& item, uint8_t flags = 0) const;
ItemData parse_item_description(Version version, const std::string& description) const;
std::shared_ptr<const CommonItemSet> common_item_set(Version logic_version, std::shared_ptr<const Quest> q) const;
std::shared_ptr<const RareItemSet> rare_item_set(Version logic_version, std::shared_ptr<const Quest> q) const;
const std::vector<uint32_t>& public_lobby_search_order(Version version, bool is_client_customization) const;
inline const std::vector<uint32_t>& public_lobby_search_order(std::shared_ptr<const Client> c) const {
return this->public_lobby_search_order(c->version(), c->check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION));
@@ -368,7 +375,6 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
}
std::shared_ptr<const std::vector<std::string>> information_contents_for_client(std::shared_ptr<const Client> c) const;
std::shared_ptr<const QuestIndex> quest_index(Version version) const;
size_t default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const;
+53 -29
View File
@@ -703,7 +703,7 @@ ShellCommand c_create_tournament(
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
string name = get_quoted_string(args.args);
string map_name = get_quoted_string(args.args);
auto map = args.s->ep3_map_index->for_name(map_name);
auto map = args.s->ep3_map_index->get(map_name);
uint32_t num_teams = stoul(get_quoted_string(args.args), nullptr, 0);
Episode3::Rules rules;
rules.set_defaults();
@@ -740,17 +740,17 @@ ShellCommand c_create_tournament(
if (subtokens.size() < 1) {
throw runtime_error("no dice ranges specified in dice= option");
}
auto atk_range = parse_range_p(tokens[0]);
auto atk_range = parse_range_p(subtokens[0]);
rules.min_dice_value = atk_range.first;
rules.max_dice_value = atk_range.second;
if (subtokens.size() >= 2) {
rules.def_dice_value_range = parse_range_c(tokens[1]);
rules.def_dice_value_range = parse_range_c(subtokens[1]);
if (subtokens.size() >= 3) {
rules.atk_dice_value_range_2v1 = parse_range_c(tokens[2]);
rules.atk_dice_value_range_2v1 = parse_range_c(subtokens[2]);
if (subtokens.size() == 3) {
rules.def_dice_value_range_2v1 = rules.atk_dice_value_range_2v1;
} else if (subtokens.size() == 4) {
rules.def_dice_value_range_2v1 = parse_range_c(tokens[3]);
rules.def_dice_value_range_2v1 = parse_range_c(subtokens[3]);
} else {
throw runtime_error("too many range specs given");
}
@@ -908,7 +908,11 @@ asio::awaitable<deque<string>> f_sc_ss(ShellCommand::Args& args) {
auto c = args.get_client();
if (args.command[1] == 's') {
co_await on_command_with_header(c, data);
if (c->proxy_session) {
send_command_with_header(c->proxy_session->server_channel, data.data(), data.size());
} else {
co_await on_command_with_header(c, data);
}
} else {
send_command_with_header(c->channel, data.data(), data.size());
}
@@ -947,23 +951,33 @@ ShellCommand c_show_slots(
});
asio::awaitable<deque<string>> fn_chat(ShellCommand::Args& args) {
auto c = args.get_proxy_client();
auto c = args.get_client();
bool is_dchat = (args.command == "dchat");
if (!is_dchat && uses_utf16(c->version())) {
send_chat_message_from_client(c->proxy_session->server_channel, args.args, 0);
} else {
string data(8, '\0');
data.push_back('\x09');
data.push_back('E');
if (is_dchat) {
data += phosg::parse_data_string(args.args, nullptr, phosg::ParseDataFlags::ALLOW_FILES);
if (c->proxy_session) {
if (!is_dchat && uses_utf16(c->version())) {
send_chat_message_from_client(c->proxy_session->server_channel, args.args, 0);
} else {
data += args.args;
data.push_back('\0');
string data(8, '\0');
data.push_back('\x09');
data.push_back('E');
if (is_dchat) {
data += phosg::parse_data_string(args.args, nullptr, phosg::ParseDataFlags::ALLOW_FILES);
} else {
data += args.args;
data.push_back('\0');
}
data.resize((data.size() + 3) & (~3));
c->proxy_session->server_channel->send(0x06, 0x00, data);
}
} else if (c->login) {
string text = is_dchat ? phosg::parse_data_string(args.args, nullptr, phosg::ParseDataFlags::ALLOW_FILES) : args.args;
auto l = c->require_lobby();
for (auto& lc : l->clients) {
if (lc) {
send_chat_message(lc, c->login->account->account_id, c->character_file()->disp.name.decode(c->language()), text, 0);
}
}
data.resize((data.size() + 3) & (~3));
c->proxy_session->server_channel->send(0x06, 0x00, data);
}
co_return deque<string>{};
@@ -977,18 +991,28 @@ ShellCommand c_dchat("dchat", "dchat DATA\n\
fn_chat);
asio::awaitable<deque<string>> fn_wchat(ShellCommand::Args& args) {
auto c = args.get_proxy_client();
auto c = args.get_client();
if (!is_ep3(c->version())) {
throw runtime_error("wchat can only be used on Episode 3");
}
string data(8, '\0');
data.push_back('\x40'); // private_flags: visible to all
data.push_back('\x09');
data.push_back('E');
data += args.args;
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
c->proxy_session->server_channel->send(0x06, 0x00, data);
if (c->proxy_session) {
string data(8, '\0');
data.push_back('\x40'); // private_flags: visible to all
data.push_back('\x09');
data.push_back('E');
data += args.args;
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
c->proxy_session->server_channel->send(0x06, 0x00, data);
} else if (c->login) {
auto l = c->require_lobby();
for (auto& lc : l->clients) {
if (lc) {
send_chat_message(
lc, c->login->account->account_id, c->character_file()->disp.name.decode(c->language()), args.args, 0x40);
}
}
}
co_return deque<string>{};
}
ShellCommand c_wc("wc", "wc TEXT", fn_wchat);
@@ -1068,7 +1092,7 @@ ShellCommand c_create_item(
send_drop_stacked_item_to_channel(args.s, c->channel, item, c->floor, c->pos);
send_drop_stacked_item_to_channel(args.s, c->proxy_session->server_channel, item, c->floor, c->pos);
string name = args.s->describe_item(c->version(), item, true);
string name = args.s->describe_item(c->version(), item, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES);
send_text_message(c->channel, "$C7Item created:\n" + name);
co_return deque<string>{};
});
+1 -1
View File
@@ -805,7 +805,7 @@ char char_for_challenge_rank(uint8_t rank) {
return "BAS"[rank];
}
const array<size_t, 4> DEFAULT_MIN_LEVELS_V3({0, 19, 39, 79});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V123({0, 19, 39, 79});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1({0, 19, 39, 79});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2({0, 29, 49, 89});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4({0, 39, 79, 109});
+1 -1
View File
@@ -106,7 +106,7 @@ uint32_t class_flags_for_class(uint8_t char_class);
char char_for_challenge_rank(uint8_t rank);
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V3;
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V123;
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1;
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2;
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4;
+5 -5
View File
@@ -98,14 +98,14 @@ void TeamIndex::Team::save_config() const {
}
void TeamIndex::Team::load_flag() {
phosg::Image img(this->flag_filename());
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(this->flag_filename()));
if (img.get_width() != 32 || img.get_height() != 32) {
throw runtime_error("incorrect flag image dimensions");
}
this->flag_data.reset(new parray<le_uint16_t, 0x20 * 0x20>());
for (size_t y = 0; y < 32; y++) {
for (size_t x = 0; x < 32; x++) {
this->flag_data->at(y * 0x20 + x) = encode_rgba8888_to_argb1555(img.read_pixel(x, y));
this->flag_data->at(y * 0x20 + x) = phosg::argb1555_for_rgba8888(img.read(x, y));
}
}
}
@@ -114,13 +114,13 @@ void TeamIndex::Team::save_flag() const {
if (!this->flag_data) {
return;
}
phosg::Image img(32, 32, true);
phosg::ImageRGBA8888N img(32, 32);
for (size_t y = 0; y < 32; y++) {
for (size_t x = 0; x < 32; x++) {
img.write_pixel(x, y, decode_argb1555_to_rgba8888(this->flag_data->at(y * 0x20 + x)));
img.write(x, y, phosg::rgba8888_for_argb1555(this->flag_data->at(y * 0x20 + x)));
}
}
img.save(this->flag_filename(), phosg::Image::Format::WINDOWS_BITMAP);
phosg::save_file(this->flag_filename(), img.serialize(phosg::ImageFormat::WINDOWS_BITMAP));
}
void TeamIndex::Team::delete_files() const {
+17 -28
View File
@@ -215,51 +215,40 @@ bool specific_version_is_indeterminate(uint32_t specific_version) {
}
bool specific_version_is_dc(uint32_t specific_version) {
// All v1 and v2 specific_versions are DC except 324F4A57 (2OJW), which is PC
// All v1 and v2 specific_versions are DC except 2OJW and 2OJZ, which are PC
uint8_t major_version = specific_version >> 24;
if (major_version < 0x31 || major_version > 0x32) {
return false;
}
return (specific_version != SPECIFIC_VERSION_PC_V2_DEFAULT);
return !specific_version_is_pc_v2(specific_version);
}
bool specific_version_is_pc_v2(uint32_t specific_version) {
return (specific_version == SPECIFIC_VERSION_PC_V2_DEFAULT);
return ((specific_version == SPECIFIC_VERSION_PC_V2_DEFAULT) || (specific_version == SPECIFIC_VERSION_PC_V2_FINAL));
}
bool specific_version_is_gc(uint32_t specific_version) {
// GC specific_versions are 3GRV, where G is [OS], R is [JEP], V is [0-9T]
if ((specific_version & 0xFF000000) != 0x33000000) {
return false;
}
char game = specific_version >> 16;
if ((game != 'O') && (game != 'S')) {
return false;
}
char region = specific_version >> 8;
if ((region != 'J') && (region != 'E') && (region != 'P')) {
return false;
}
char revision = specific_version;
return (isdigit(revision) || (revision == 'T'));
// GC specific_versions are 3___
return ((specific_version & 0xFF000000) == 0x33000000);
}
bool specific_version_is_xb(uint32_t specific_version) {
// XB specific_versions are 4ORV, where R is [JEP], V is [BDU]
if ((specific_version & 0xFFFF0000) != 0x344F0000) {
return false;
}
char region = specific_version >> 8;
if ((region != 'J') && (region != 'E') && (region != 'P')) {
return false;
}
char revision = specific_version;
return ((revision == 'B') || (revision == 'D') || (revision == 'U'));
// XB specific_versions are 4O__
return ((specific_version & 0xFF000000) == 0x34000000);
}
bool specific_version_is_bb(uint32_t specific_version) {
// BB specific_versions are 5XXX, where X is an encoding of the revision number
return (specific_version & 0xFF000000) == 0x35000000;
return ((specific_version & 0xFF000000) == 0x35000000);
}
string str_for_specific_version(uint32_t specific_version) {
string ret;
for (size_t z = 0; z < 4; z++) {
char ch = specific_version >> (24 - (z << 3));
ret.push_back(isalnum(ch) ? ch : '_');
}
return ret;
}
const char* file_path_token_for_version(Version version) {
+4 -1
View File
@@ -191,8 +191,9 @@ constexpr uint32_t SPECIFIC_VERSION_DC_V1_US = 0x314F4546; // 1OEF
constexpr uint32_t SPECIFIC_VERSION_DC_V1_EU_INDETERMINATE = 0x314F5000; // 1OP_
constexpr uint32_t SPECIFIC_VERSION_DC_V1_INDETERMINATE = 0x31000000; // 1___
constexpr uint32_t SPECIFIC_VERSION_DC_V2_INDETERMINATE = 0x32000000; // 2___
constexpr uint32_t SPECIFIC_VERSION_PC_V2_INDETERMINATE = 0x324F4A00; // 2OJW
constexpr uint32_t SPECIFIC_VERSION_PC_V2_INDETERMINATE = 0x324F4A00; // 2OJ_
constexpr uint32_t SPECIFIC_VERSION_PC_V2_DEFAULT = 0x324F4A57; // 2OJW
constexpr uint32_t SPECIFIC_VERSION_PC_V2_FINAL = 0x324F4A5A; // 2OJZ
constexpr uint32_t SPECIFIC_VERSION_GC_NTE = 0x334F4A54; // 3OJT
constexpr uint32_t SPECIFIC_VERSION_GC_V3_EU = 0x334F5030; // 3OP0
constexpr uint32_t SPECIFIC_VERSION_GC_V3_US_12 = 0x334F4532; // 3OE2
@@ -218,6 +219,8 @@ bool specific_version_is_gc(uint32_t specific_version);
bool specific_version_is_xb(uint32_t specific_version);
bool specific_version_is_bb(uint32_t specific_version);
std::string str_for_specific_version(uint32_t specific_version);
enum class ServerBehavior {
PC_CONSOLE_DETECT = 0,
GAME_SERVER,
+6
View File
@@ -9,7 +9,13 @@
#ifdef PHOSG_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#ifdef DELETE
#undef DELETE
#endif
#ifdef ERROR
#undef ERROR
#endif
#ifdef PASSTHROUGH
#undef PASSTHROUGH
#endif
#endif
@@ -0,0 +1,41 @@
.meta name="Kill count fix"
.meta description="Fixes client-side\nkill counts when\nmultiple enemies are\nkilled on the same\nframe"
.versions 3OJ2 3OJ3 3OJ4 3OJ5 3OE0 3OE1 3OE2 3OP0
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksGC
.data <VERS 0x8012D2D4 0x8012D518 0x8012D550 0x8012D4B0 0x8012D578 0x8012D578 0x8012D4C0 0x8012D698>
.deltaof TItemWeapon_SealedJSword_count_kill, TItemWeapon_SealedJSword_count_kill_end
.address <VERS 0x8012D2D4 0x8012D518 0x8012D550 0x8012D4B0 0x8012D578 0x8012D578 0x8012D4C0 0x8012D698>
TItemWeapon_SealedJSword_count_kill: # [std] (TItemWeapon_SealedJSword* this @ r3) -> void
lwz r4, [r3 + 0xF0] # r4 = this->owner_player
lha r5, [r4 + 0x11A] # r5 = this->owner_player->num_kills_since_map_load
lha r6, [r3 + 0x1F8] # r6 = this->last_owner_player_kill_count
lhz r7, [r3 + 0xE8] # r7 = this->kill_count
cmp r6, r5
bge TItemWeapon_SealedJSword_count_kill_skip_update
lwz r8, [r3 + 0xDC]
andi. r8, r8, 0x100
beq TItemWeapon_SealedJSword_count_kill_skip_incr # if (!(flags & 0x100)) don't incr kill count
sub r8, r5, r6
add r7, r7, r8
sth [r3 + 0xE8], r7
TItemWeapon_SealedJSword_count_kill_skip_incr:
sth [r3 + 0x1F8], r5
TItemWeapon_SealedJSword_count_kill_skip_update:
cmplwi r7, 23000
blt TItemWeapon_SealedJSword_count_kill_skip_set_flag
lwz r8, [r3 + 0xDC]
ori r8, r8, 0x200
stw [r3 + 0xDC], r8
TItemWeapon_SealedJSword_count_kill_skip_set_flag:
blr
TItemWeapon_SealedJSword_count_kill_end:
.data 0x00000000
.data 0x00000000
@@ -0,0 +1,35 @@
.meta name="Kill count fix"
.meta description="Fixes client-side\nkill counts when\nmultiple enemies are\nkilled on the same\nframe"
.versions 4OJB 4OJD 4OJU 4OED 4OEU 4OPD 4OPU
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksXB
.data <VERS 0x00197610 0x001977A0 0x00197920 0x00197880 0x00197810 0x001978A0 0x00197840>
.deltaof TItemWeapon_SealedJSword_count_kill, TItemWeapon_SealedJSword_count_kill_end
.address <VERS 0x00197610 0x001977A0 0x00197920 0x00197880 0x00197810 0x001978A0 0x00197840>
TItemWeapon_SealedJSword_count_kill:
mov eax, [ecx + 0xF0]
movsx eax, word [eax + 0x11A]
movsx edx, word [ecx + 0x1F8]
sub edx, eax
jge TItemWeapon_SealedJSword_count_kill_skip_update
test dword [ecx + 0xDC], 0x100
jz TItemWeapon_SealedJSword_count_kill_skip_incr
sub [ecx + 0xE8], dx
TItemWeapon_SealedJSword_count_kill_skip_incr:
mov [ecx + 0x1F8], ax
TItemWeapon_SealedJSword_count_kill_skip_update:
cmp word [ecx + 0xE8], 23000
jb TItemWeapon_SealedJSword_count_kill_skip_set_flag
or dword [ecx + 0xDC], 0x200
TItemWeapon_SealedJSword_count_kill_skip_set_flag:
ret
TItemWeapon_SealedJSword_count_kill_end:
.data 0x00000000
.data 0x00000000
@@ -0,0 +1,85 @@
.meta name="Kill count fix"
.meta description="Fixes client-side\nkill counts when\nmultiple enemies are\nkilled on the same\nframe"
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksBB
.data 0x005E32C8
.deltaof TItemUnitUnsealable_count_kill, TItemUnitUnsealable_count_kill_end
.address 0x005E32C8
TItemUnitUnsealable_count_kill: # [std] (TItemUnitUnsealable* this @ ecx) -> void
mov eax, [ecx + 0xF8]
movsx eax, word [eax + 0x11A] # eax = this->owner_player->num_kills_since_map_load
movsx edx, word [ecx + 0x1E4] # edx = this->last_owner_player_kill_count
sub edx, eax # edx = this->last_owner_player_kill_count - this->owner_player->num_kills_since_map_load (edx should be 0 or negative)
jge TItemUnitUnsealable_count_kill_skip_update
test dword [ecx + 0xDC], 0x100
jz TItemUnitUnsealable_count_kill_skip_incr # if (!(this->flags & 0x100)) don't incr kill count
sub [ecx + 0xE8], dx # this->kill_count -= edx
TItemUnitUnsealable_count_kill_skip_incr:
mov [ecx + 0x1E4], ax # this->last_owner_player_kill_count = this->owner_player->num_kills_since_map_load
TItemUnitUnsealable_count_kill_skip_update:
cmp word [ecx + 0xE8], 20000
jb TItemUnitUnsealable_count_kill_skip_set_flag
or dword [ecx + 0xDC], 0x200
TItemUnitUnsealable_count_kill_skip_set_flag:
jmp 0x005E2C34
TItemUnitUnsealable_count_kill_end:
.data 0x005F3EFC
.deltaof TItemWeapon_LameDArgent_count_kill, TItemWeapon_LameDArgent_count_kill_end
.address 0x005F3EFC
TItemWeapon_LameDArgent_count_kill:
mov eax, [ecx + 0xF8]
movsx eax, word [eax + 0x11A]
movsx edx, word [ecx + 0x240]
sub edx, eax
jge TItemWeapon_LameDArgent_count_kill_skip_update
test dword [ecx + 0xDC], 0x100
jz TItemWeapon_LameDArgent_count_kill_skip_incr
sub [ecx + 0xE8], dx
TItemWeapon_LameDArgent_count_kill_skip_incr:
mov [ecx + 0x240], ax
TItemWeapon_LameDArgent_count_kill_skip_update:
cmp word [ecx + 0xE8], 10000
jb TItemWeapon_LameDArgent_count_kill_skip_set_flag
or dword [ecx + 0xDC], 0x200
TItemWeapon_LameDArgent_count_kill_skip_set_flag:
ret
TItemWeapon_LameDArgent_count_kill_end:
.data 0x005FCA74
.deltaof TItemWeapon_SealedJSword_count_kill, TItemWeapon_SealedJSword_count_kill_end
.address 0x005FCA74
TItemWeapon_SealedJSword_count_kill:
mov eax, [ecx + 0xF8]
movsx eax, word [eax + 0x11A]
movsx edx, word [ecx + 0x240]
sub edx, eax
jge TItemWeapon_SealedJSword_count_kill_skip_update
test dword [ecx + 0xDC], 0x100
jz TItemWeapon_SealedJSword_count_kill_skip_incr
sub [ecx + 0xE8], dx
TItemWeapon_SealedJSword_count_kill_skip_incr:
mov [ecx + 0x240], ax
TItemWeapon_SealedJSword_count_kill_skip_update:
cmp word [ecx + 0xE8], 23000
jb TItemWeapon_SealedJSword_count_kill_skip_set_flag
or dword [ecx + 0xDC], 0x200
TItemWeapon_SealedJSword_count_kill_skip_set_flag:
ret
TItemWeapon_SealedJSword_count_kill_end:
.data 0x00000000
.data 0x00000000
@@ -0,0 +1,102 @@
# This patch changes the amount of items and Meseta that can be stored in the
# bank. If the bank item limit is increased beyond 200, this patch requires
# server support for extended bank data stored outside of the player's data.
# newserv has support for this, but you must set the BBBankItemLimit and
# BBBankMesetaLimit values in config.json to match the values used here.
# As written, this changes the meseta limit to 2000000000 and the item limit to
# 1000. The meseta limit can be any value up to 2147483647, and the item limit
# can be any value up to 1321. To use different values than the defaults, first
# compute the data size as ((slot count * 0x18) + 8), then replace each value
# below appropriately.
.meta name="More bank slots"
.meta description=""
.meta hide_from_patches_menu
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksBB
.data 0x006C8C0F
.data 4
.data 1000 # slot count
.data 0x006C8C4D
.data 4
.data 1000 # slot count
.data 0x006C8B54
.data 4
.data 999 # slot count - 1
.data 0x006C8B94
.data 4
.data 0x5DC0 # data size - 8
.data 0x006C8D16
.data 4
.data 999 # slot count - 1
.data 0x006C8E5E
.data 4
.data 999 # slot count - 1
.data 0x006C8F2C
.data 4
.data 999 # slot count - 1
.data 0x006C9016
.data 4
.data 0x5DB0 # data size - 0x18
.data 0x006C9034
.data 4
.data 0x5DC0 # data size - 8
.data 0x006C910D
.data 4
.data 0x5DB0 # data size - 0x18
.data 0x006C9129
.data 4
.data 0x5DC8 # data size
.data 0x006C9236
.data 4
.data 1000 # slot count
.data 0x006C924C
.data 4
.data 999 # slot count - 1
.data 0x006C9286
.data 4
.data 999 # slot count - 1
.data 0x006C92FA
.data 4
.data 1000 # slot count
.data 0x006C9883
.data 4
.data 1000 # slot count
.data 0x006C9A22
.data 4
.data 2000000000 # max meseta
.data 0x006CA2DB
.data 4
.data 0x5DC8 # data size
.data 0x006CA303
.data 4
.data 1000 # slot count
.data 0x006CA37F
.data 4
.data 0x5DC8 # data size
.data 0x006D7DAC
.data 4
.data 1000 # slot count
.data 0x006D7DBD
.data 4
.data 1000 # slot count
.data 0x006D7E14
.data 4
.data 1000 # slot count
.data 0x006D7BF5
.data 4
.data 1000 # slot count
.data 0x006C8DBF
.data 2
jmp +0x27
.data 0
.data 0
@@ -1,32 +0,0 @@
# This patch disables the logic that causes all unlockable areas to be open by
# default for all players, instead restoring the logic that checks quest flags
# to open areas (as previous PSO versions used).
# This patch is intended to be used in the BBRequiredPatches field in
# config.json if you want the classic behavior, hence the presence of the
# hide_from_patches_menu directive here.
.meta name="Classic main warp behavior"
.meta description=""
.meta hide_from_patches_menu
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksBB
.data 0x0064A5DE # Episode 1
.data 1
.binary 01
.data 0x0064A448 # Episode 2
.data 2
.binary 0100
.data 0x0064A529 # Episode 4
.data 1
.binary 01
.data 0x0064A658 # Non-Normal difficulty check
.data 2
nop
nop
.data 0
.data 0
@@ -10,21 +10,23 @@
.meta description=""
.meta hide_from_patches_menu
.versions 59NJ 59NL
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksBB
.data 0x0064A642 # Episode 1
.data <VERS 0x0064A642 0x0064A5DE> # Episode 1
.data 1
.binary 01
.data 0x0064A4AC # Episode 2
.data <VERS 0x0064A4AC 0x0064A448> # Episode 2
.data 2
.binary 0100
.data 0x0064A58D # Episode 4
.data <VERS 0x0064A58D 0x0064A529> # Episode 4
.data 1
.binary 01
.data 0x0064A6BC # Non-Normal difficulty check
.data <VERS 0x0064A6BC 0x0064A658> # Non-Normal difficulty check
.data 2
nop
nop
@@ -1,26 +0,0 @@
# It would be a bad idea to remove `.meta hide_from_patches_menu` to make this
# patch an option for players to be able to select; either all players on the
# server should have this patch, or none should have it.
# This patch clears the list of unreleased items on the client, so the client
# never creates buggy items when the server generates an item that wasn't
# released on the official servers.
.meta name="Clear unreleased item list"
.meta description=""
.meta hide_from_patches_menu
entry_ptr:
reloc0:
.offsetof start
start:
xor eax, eax
mov edx, esp
mov esp, 0x009F81B0
mov ecx, 0x3C
again:
push 0
dec ecx
jnz again
mov esp, edx
ret
@@ -10,13 +10,15 @@
.meta description=""
.meta hide_from_patches_menu
.versions 59NJ 59NL
entry_ptr:
reloc0:
.offsetof start
start:
xor eax, eax
mov edx, esp
mov esp, 0x009F61B0
mov esp, <VERS 0x009F61B0 0x009F81B0>
mov ecx, 0x3C
again:
push 0
@@ -0,0 +1,63 @@
# This patch changes the 6xDD command to support fractional multipliers.
.meta name="Fractional EXP multiplier"
.meta description=""
.meta hide_from_patches_menu
entry_ptr:
reloc0:
.offsetof start
start:
call install_hook
call apply_static_patches
fild st0, dword [0x009F9EE0]
fstp dword [0x009F9EE0], st0
ret
install_hook:
pop ecx
push 7
push 0x0078747E
call get_code_size
.deltaof hook_start, hook_end
get_code_size:
pop eax
push dword [eax]
call hook_end
hook_start: # [eax, ebx]() -> void
push edx
fild st0, dword [esp]
fld st0, dword [0x009F9EE0]
fmulp st1, st0
fistp dword [esp], st0
pop edx
ret
hook_end:
push ecx
.include WriteCallToCode-59NL
apply_static_patches:
.include WriteCodeBlocksBB
.data 0x00787998
.deltaof handle_6xDD_start, handle_6xDD_end
handle_6xDD_start: # [std](G_6xDD* cmd @ [esp + 4]) -> void
mov eax, [esp + 4]
test eax, eax
je handle_6xDD_ret
cmp byte [eax + 1], 1
jg handle_6xDD_use_float
fild st0, word [eax + 2]
jmp handle_6xDD_write_float
handle_6xDD_use_float:
fld st0, dword [eax + 4]
handle_6xDD_write_float:
fstp dword [0x009F9EE0], st0
handle_6xDD_ret:
ret
handle_6xDD_end:
.data 0x00000000
.data 0x00000000
@@ -67,8 +67,6 @@ enable_scroll_start:
mov ecx, [eax + 0xEC] # ecx = scroll_bar->client_id
imul ecx, ecx, 0x24
# Set up scroll bar graphics (in struct at scroll_bar + 0x1C)
# TODO: Even though we set this up the same way PSO Xbox does, it still
# doesn't render. Figure this out and fix it.
mov dword [eax + ecx + 0x1C], 0x439D0000
mov dword [eax + ecx + 0x20], 0x43360000
mov dword [eax + ecx + 0x24], 0x439D0000
@@ -77,7 +75,7 @@ enable_scroll_start:
mov dword [eax + ecx + 0x30], 0x425EA3D7
mov dword [eax + ecx + 0x34], 0x00000008
mov dword [eax + ecx + 0x38], 0x00000000
mov dword [eax + ecx + 0x2C], 0x00000000
mov dword [eax + ecx + 0x3C], 0x00000000
or dword [eax + 0xF0], 1 # scroll_bar->flags |= 1
mov ecx, [eax + 0xEC]
shl ecx, 4
@@ -119,7 +117,7 @@ fix_scroll_patch1_end:
apply_fix_scroll_patch2:
# This patch changes the TAdSinglePlyChrSelectGC::selected_index_within_view
# to be the selected character's absolute index (including scroll_offset),
# not the index only within to the displayed four characters
# not the index only within the displayed four characters
push 6 # Call size
push 0x00413CD8 # Call address
call get_code_size_for_fix_scroll_patch2
@@ -166,7 +164,7 @@ selection_index_fix2_end:
apply_preview_window_fix:
# This patch fixes the preview display so it will show the correct section
# ID, etc.
# ID, level, etc.
push 5 # Call size
push 0x0040216C # Call address
call get_code_size_for_preview_window_fix
@@ -528,14 +526,14 @@ show_slot_number_strend_done:
lea ecx, [ecx + ebp + 1]
push ecx # Slot number (scroll_offset + z)
call get_show_slot_number_suffix_fmt
.binary 2000280023002500640029000000 # L" (#%d)"
.binary 20002800230025006400290020000000 # L" (#%d) "
get_show_slot_number_suffix_fmt:
push eax # Destination buffer
mov eax, 0x00857E29 # _swprintf
call eax
add esp, 0x0C
jmp show_slot_number_end
.zero 0x98
.zero 0x96
show_slot_number_end: # 00401E4D
# End static patches
@@ -1,564 +0,0 @@
.meta name="Bug fixes"
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
# Original code by Ralf @ GC-Forever and Aleron Ives
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksGC
# region @ 8000B088 (88 bytes)
.data 0x8000B088 # address
.data 0x00000058 # size
.data 0x7FA3EB78 # 8000B088 => mr r3, r29
.data 0x38800000 # 8000B08C => li r4, 0x0000
.data 0x481AEB11 # 8000B090 => bl +0x001AEB10 /* 801B9BA0 */
.data 0x7FA3EB78 # 8000B094 => mr r3, r29
.data 0x481AEDE0 # 8000B098 => b +0x001AEDE0 /* 801B9E78 */
.data 0x881F0000 # 8000B09C => lbz r0, [r31]
.data 0x28090001 # 8000B0A0 => cmplwi r9, 1
.data 0x4082000C # 8000B0A4 => bne +0x0000000C /* 8000B0B0 */
.data 0x881F0001 # 8000B0A8 => lbz r0, [r31 + 0x0001]
.data 0x3BFF0002 # 8000B0AC => addi r31, r31, 0x0002
.data 0x48100B68 # 8000B0B0 => b +0x00100B68 /* 8010BC18 */
.data 0x39200000 # 8000B0B4 => li r9, 0x0000
.data 0x48100AF9 # 8000B0B8 => bl +0x00100AF8 /* 8010BBB0 */
.data 0x7F43D378 # 8000B0BC => mr r3, r26
.data 0x7F64DB78 # 8000B0C0 => mr r4, r27
.data 0x7F85E378 # 8000B0C4 => mr r5, r28
.data 0x7FA6EB78 # 8000B0C8 => mr r6, r29
.data 0x7FC7F378 # 8000B0CC => mr r7, r30
.data 0x7FE8FB78 # 8000B0D0 => mr r8, r31
.data 0x39200001 # 8000B0D4 => li r9, 0x0001
.data 0x48100AD9 # 8000B0D8 => bl +0x00100AD8 /* 8010BBB0 */
.data 0x48102F64 # 8000B0DC => b +0x00102F64 /* 8010E040 */
# region @ 8000B5C8 (20 bytes)
.data 0x8000B5C8 # address
.data 0x00000014 # size
.data 0x80630098 # 8000B5C8 => lwz r3, [r3 + 0x0098]
.data 0x483D5999 # 8000B5CC => bl +0x003D5998 /* 803E0F64 */
.data 0x807F042C # 8000B5D0 => lwz r3, [r31 + 0x042C]
.data 0x809F0430 # 8000B5D4 => lwz r4, [r31 + 0x0430]
.data 0x48178C7C # 8000B5D8 => b +0x00178C7C /* 80184254 */
# region @ 8000BBD0 (32 bytes)
.data 0x8000BBD0 # address
.data 0x00000020 # size
.data 0x809F0370 # 8000BBD0 => lwz r4, [r31 + 0x0370]
.data 0x3884FC00 # 8000BBD4 => subi r4, r4, 0x0400
.data 0x909F0370 # 8000BBD8 => stw [r31 + 0x0370], r4
.data 0x807F0014 # 8000BBDC => lwz r3, [r31 + 0x0014]
.data 0x28030000 # 8000BBE0 => cmplwi r3, 0
.data 0x41820008 # 8000BBE4 => beq +0x00000008 /* 8000BBEC */
.data 0x90830060 # 8000BBE8 => stw [r3 + 0x0060], r4
.data 0x48165428 # 8000BBEC => b +0x00165428 /* 80171014 */
# region @ 8000C3F8 (124 bytes)
.data 0x8000C3F8 # address
.data 0x0000007C # size
.data 0x28040000 # 8000C3F8 => cmplwi r4, 0
.data 0x4D820020 # 8000C3FC => beqlr
.data 0x9421FFF0 # 8000C400 => stwu [r1 - 0x0010], r1
.data 0x481AD7A0 # 8000C404 => b +0x001AD7A0 /* 801B9BA4 */
.data 0x9421FFE0 # 8000C408 => stwu [r1 - 0x0020], r1
.data 0x7C0802A6 # 8000C40C => mflr r0
.data 0x90010024 # 8000C410 => stw [r1 + 0x0024], r0
.data 0xBF410008 # 8000C414 => stmw [r1 + 0x0008], r26
.data 0x7C7F1B78 # 8000C418 => mr r31, r3
.data 0x4BFFFFDD # 8000C41C => bl -0x00000024 /* 8000C3F8 */
.data 0x3BC00000 # 8000C420 => li r30, 0x0000
.data 0x3BBF0D04 # 8000C424 => addi r29, r31, 0x0D04
.data 0x837F032C # 8000C428 => lwz r27, [r31 + 0x032C]
.data 0x839D0000 # 8000C42C => lwz r28, [r29]
.data 0x7F83E379 # 8000C430 => mr. r3, r28
.data 0x41820018 # 8000C434 => beq +0x00000018 /* 8000C44C */
.data 0x38800001 # 8000C438 => li r4, 0x0001
.data 0x480FED81 # 8000C43C => bl +0x000FED80 /* 8010B1BC */
.data 0x7F83E378 # 8000C440 => mr r3, r28
.data 0x38800001 # 8000C444 => li r4, 0x0001
.data 0x480FEEF1 # 8000C448 => bl +0x000FEEF0 /* 8010B338 */
.data 0x3BBD0004 # 8000C44C => addi r29, r29, 0x0004
.data 0x3BDE0001 # 8000C450 => addi r30, r30, 0x0001
.data 0x2C1E000D # 8000C454 => cmpwi r30, 13
.data 0x4180FFD4 # 8000C458 => blt -0x0000002C /* 8000C42C */
.data 0x937F032C # 8000C45C => stw [r31 + 0x032C], r27
.data 0xBB410008 # 8000C460 => lmw r26, [r1 + 0x0008]
.data 0x80010024 # 8000C464 => lwz r0, [r1 + 0x0024]
.data 0x7C0803A6 # 8000C468 => mtlr r0
.data 0x38210020 # 8000C46C => addi r1, r1, 0x0020
.data 0x4E800020 # 8000C470 => blr
# region @ 8000C640 (20 bytes)
.data 0x8000C640 # address
.data 0x00000014 # size
.data 0x54800673 # 8000C640 => rlwinm. r0, r4, 0, 25, 25
.data 0x41820008 # 8000C644 => beq +0x00000008 /* 8000C64C */
.data 0x38800000 # 8000C648 => li r4, 0x0000
.data 0x38040009 # 8000C64C => addi r0, r4, 0x0009
.data 0x4810C938 # 8000C650 => b +0x0010C938 /* 80118F88 */
# region @ 8000C6D0 (32 bytes)
.data 0x8000C6D0 # address
.data 0x00000020 # size
.data 0x38000001 # 8000C6D0 => li r0, 0x0001
.data 0x901D0054 # 8000C6D4 => stw [r29 + 0x0054], r0
.data 0x807D0024 # 8000C6D8 => lwz r3, [r29 + 0x0024]
.data 0x48211244 # 8000C6DC => b +0x00211244 /* 8021D920 */
.data 0x38000001 # 8000C6E0 => li r0, 0x0001
.data 0x901F0378 # 8000C6E4 => stw [r31 + 0x0378], r0
.data 0x807F0024 # 8000C6E8 => lwz r3, [r31 + 0x0024]
.data 0x482146F4 # 8000C6EC => b +0x002146F4 /* 80220DE0 */
# region @ 8000C8A0 (20 bytes)
.data 0x8000C8A0 # address
.data 0x00000014 # size
.data 0x1C00000A # 8000C8A0 => mulli r0, r0, 10
.data 0x57E407BD # 8000C8A4 => rlwinm. r4, r31, 0, 30, 30
.data 0x41820008 # 8000C8A8 => beq +0x00000008 /* 8000C8B0 */
.data 0x7FA00734 # 8000C8AC => extsh r0, r29
.data 0x4810605C # 8000C8B0 => b +0x0010605C /* 8011290C */
# region @ 8000C8C0 (16 bytes)
.data 0x8000C8C0 # address
.data 0x00000010 # size
.data 0x7000000F # 8000C8C0 => andi. r0, r0, 0x000F
.data 0x7000004F # 8000C8C4 => andi. r0, r0, 0x004F
.data 0x2C000004 # 8000C8C8 => cmpwi r0, 4
.data 0x4E800020 # 8000C8CC => blr
# region @ 8000D980 (20 bytes)
.data 0x8000D980 # address
.data 0x00000014 # size
.data 0x807C0000 # 8000D980 => lwz r3, [r28]
.data 0x2C030013 # 8000D984 => cmpwi r3, 19
.data 0x40820008 # 8000D988 => bne +0x00000008 /* 8000D990 */
.data 0x38600002 # 8000D98C => li r3, 0x0002
.data 0x482AE568 # 8000D990 => b +0x002AE568 /* 802BBEF8 */
# region @ 8000D9A0 (24 bytes)
.data 0x8000D9A0 # address
.data 0x00000018 # size
.data 0xC042FC88 # 8000D9A0 => lfs f2, [r2 - 0x0378]
.data 0x807E0030 # 8000D9A4 => lwz r3, [r30 + 0x0030]
.data 0x70630020 # 8000D9A8 => andi. r3, r3, 0x0020
.data 0x41820008 # 8000D9AC => beq +0x00000008 /* 8000D9B4 */
.data 0xC042FCA0 # 8000D9B0 => lfs f2, [r2 - 0x0360]
.data 0x483280A0 # 8000D9B4 => b +0x003280A0 /* 80335A54 */
# region @ 8000E1E0 (28 bytes)
.data 0x8000E1E0 # address
.data 0x0000001C # size
.data 0x7FC802A6 # 8000E1E0 => mflr r30
.data 0x38A00000 # 8000E1E4 => li r5, 0x0000
.data 0x38C0001E # 8000E1E8 => li r6, 0x001E
.data 0x38E00040 # 8000E1EC => li r7, 0x0040
.data 0x4807853D # 8000E1F0 => bl +0x0007853C /* 8008672C */
.data 0x7FC803A6 # 8000E1F4 => mtlr r30
.data 0x4E800020 # 8000E1F8 => blr
# region @ 80013084 (4 bytes)
.data 0x80013084 # address
.data 0x00000004 # size
.data 0x4BFFFCC0 # 80013084 => b -0x00000340 /* 80012D44 */
# region @ 800142F4 (4 bytes)
.data 0x800142F4 # address
.data 0x00000004 # size
.data 0x4BFF85CD # 800142F4 => bl -0x00007A34 /* 8000C8C0 */
# region @ 80015D1C (4 bytes)
.data 0x80015D1C # address
.data 0x00000004 # size
.data 0x4BFF6BA9 # 80015D1C => bl -0x00009458 /* 8000C8C4 */
# region @ 800917B4 (8 bytes)
.data 0x800917B4 # address
.data 0x00000008 # size
.data 0x4800024D # 800917B4 => bl +0x0000024C /* 80091A00 */
.data 0xB3C3032C # 800917B8 => sth [r3 + 0x032C], r30
# region @ 800BC9E8 (4 bytes)
.data 0x800BC9E8 # address
.data 0x00000004 # size
.data 0x48000010 # 800BC9E8 => b +0x00000010 /* 800BC9F8 */
# region @ 80101EB8 (4 bytes)
.data 0x80101EB8 # address
.data 0x00000004 # size
.data 0x60000000 # 80101EB8 => nop
# region @ 80104DEC (4 bytes)
.data 0x80104DEC # address
.data 0x00000004 # size
.data 0x4182000C # 80104DEC => beq +0x0000000C /* 80104DF8 */
# region @ 8010771C (4 bytes)
.data 0x8010771C # address
.data 0x00000004 # size
.data 0x4800000C # 8010771C => b +0x0000000C /* 80107728 */
# region @ 80107730 (4 bytes)
.data 0x80107730 # address
.data 0x00000004 # size
.data 0x7C030378 # 80107730 => mr r3, r0
# region @ 8010BC14 (4 bytes)
.data 0x8010BC14 # address
.data 0x00000004 # size
.data 0x4BEFF488 # 8010BC14 => b -0x00100B78 /* 8000B09C */
# region @ 8010E03C (4 bytes)
.data 0x8010E03C # address
.data 0x00000004 # size
.data 0x4BEFD078 # 8010E03C => b -0x00102F88 /* 8000B0B4 */
# region @ 80112908 (4 bytes)
.data 0x80112908 # address
.data 0x00000004 # size
.data 0x4BEF9F98 # 80112908 => b -0x00106068 /* 8000C8A0 */
# region @ 8011461C (4 bytes)
.data 0x8011461C # address
.data 0x00000004 # size
.data 0x38000012 # 8011461C => li r0, 0x0012
# region @ 80118854 (4 bytes)
.data 0x80118854 # address
.data 0x00000004 # size
.data 0x88040016 # 80118854 => lbz r0, [r4 + 0x0016]
# region @ 80118860 (4 bytes)
.data 0x80118860 # address
.data 0x00000004 # size
.data 0x88040017 # 80118860 => lbz r0, [r4 + 0x0017]
# region @ 80118F84 (4 bytes)
.data 0x80118F84 # address
.data 0x00000004 # size
.data 0x4BEF36BC # 80118F84 => b -0x0010C944 /* 8000C640 */
# region @ 8011CD34 (12 bytes)
.data 0x8011CD34 # address
.data 0x0000000C # size
.data 0x7C030378 # 8011CD34 => mr r3, r0
.data 0x3863FFFF # 8011CD38 => subi r3, r3, 0x0001
.data 0x4BFFFFE8 # 8011CD3C => b -0x00000018 /* 8011CD24 */
# region @ 8011CDF0 (12 bytes)
.data 0x8011CDF0 # address
.data 0x0000000C # size
.data 0x7C030378 # 8011CDF0 => mr r3, r0
.data 0x3863FFFF # 8011CDF4 => subi r3, r3, 0x0001
.data 0x4BFFFFE8 # 8011CDF8 => b -0x00000018 /* 8011CDE0 */
# region @ 8011CE40 (12 bytes)
.data 0x8011CE40 # address
.data 0x0000000C # size
.data 0x7C040378 # 8011CE40 => mr r4, r0
.data 0x3884FFFF # 8011CE44 => subi r4, r4, 0x0001
.data 0x4BFFFFE8 # 8011CE48 => b -0x00000018 /* 8011CE30 */
# region @ 801666E0 (8 bytes)
.data 0x801666E0 # address
.data 0x00000008 # size
.data 0x3C604005 # 801666E0 => lis r3, 0x4005
.data 0x4800009C # 801666E4 => b +0x0000009C /* 80166780 */
# region @ 8016677C (4 bytes)
.data 0x8016677C # address
.data 0x00000004 # size
.data 0x4800001C # 8016677C => b +0x0000001C /* 80166798 */
# region @ 80171010 (4 bytes)
.data 0x80171010 # address
.data 0x00000004 # size
.data 0x4BE9ABC0 # 80171010 => b -0x00165440 /* 8000BBD0 */
# region @ 80171030 (4 bytes)
.data 0x80171030 # address
.data 0x00000004 # size
.data 0x60800420 # 80171030 => ori r0, r4, 0x0420
# region @ 80184250 (4 bytes)
.data 0x80184250 # address
.data 0x00000004 # size
.data 0x4BE87378 # 80184250 => b -0x00178C88 /* 8000B5C8 */
# region @ 80184290 (4 bytes)
.data 0x80184290 # address
.data 0x00000004 # size
.data 0x60000000 # 80184290 => nop
# region @ 80189E20 (4 bytes)
.data 0x80189E20 # address
.data 0x00000004 # size
.data 0x60000000 # 80189E20 => nop
# region @ 801937A8 (4 bytes)
.data 0x801937A8 # address
.data 0x00000004 # size
.data 0x60000000 # 801937A8 => nop
# region @ 801B9BA0 (4 bytes)
.data 0x801B9BA0 # address
.data 0x00000004 # size
.data 0x4BE52868 # 801B9BA0 => b -0x001AD798 /* 8000C408 */
# region @ 801B9E74 (4 bytes)
.data 0x801B9E74 # address
.data 0x00000004 # size
.data 0x4BE51214 # 801B9E74 => b -0x001AEDEC /* 8000B088 */
# region @ 801C62C0 (4 bytes)
.data 0x801C62C0 # address
.data 0x00000004 # size
.data 0x389F02FC # 801C62C0 => addi r4, r31, 0x02FC
# region @ 801CA610 (4 bytes)
.data 0x801CA610 # address
.data 0x00000004 # size
.data 0x48000010 # 801CA610 => b +0x00000010 /* 801CA620 */
# region @ 8021D91C (4 bytes)
.data 0x8021D91C # address
.data 0x00000004 # size
.data 0x4BDEEDB4 # 8021D91C => b -0x0021124C /* 8000C6D0 */
# region @ 80220DDC (4 bytes)
.data 0x80220DDC # address
.data 0x00000004 # size
.data 0x4BDEB904 # 80220DDC => b -0x002146FC /* 8000C6E0 */
# region @ 80229C10 (4 bytes)
.data 0x80229C10 # address
.data 0x00000004 # size
.data 0x2C000001 # 80229C10 => cmpwi r0, 1
# region @ 8022A410 (4 bytes)
.data 0x8022A410 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8022A410 => li r4, 0xFFFFFF00
# region @ 8022A440 (4 bytes)
.data 0x8022A440 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8022A440 => li r4, 0xFFFFFE80
# region @ 8022A470 (4 bytes)
.data 0x8022A470 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8022A470 => li r4, 0xFFFFFDB0
# region @ 8022D10C (4 bytes)
.data 0x8022D10C # address
.data 0x00000004 # size
.data 0x60000000 # 8022D10C => nop
# region @ 8022D840 (4 bytes)
.data 0x8022D840 # address
.data 0x00000004 # size
.data 0x41810630 # 8022D840 => bgt +0x00000630 /* 8022DE70 */
# region @ 8022DB34 (4 bytes)
.data 0x8022DB34 # address
.data 0x00000004 # size
.data 0x4181033C # 8022DB34 => bgt +0x0000033C /* 8022DE70 */
# region @ 8022DC28 (4 bytes)
.data 0x8022DC28 # address
.data 0x00000004 # size
.data 0x41810248 # 8022DC28 => bgt +0x00000248 /* 8022DE70 */
# region @ 8022EB64 (4 bytes)
.data 0x8022EB64 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8022EB64 => li r4, 0xFFFFFF00
# region @ 8022EB94 (4 bytes)
.data 0x8022EB94 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8022EB94 => li r4, 0xFFFFFE80
# region @ 8022EBC4 (4 bytes)
.data 0x8022EBC4 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8022EBC4 => li r4, 0xFFFFFDB0
# region @ 8022F370 (4 bytes)
.data 0x8022F370 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8022F370 => li r4, 0xFFFFFF00
# region @ 8022F3A0 (4 bytes)
.data 0x8022F3A0 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8022F3A0 => li r4, 0xFFFFFE80
# region @ 8022F3D0 (4 bytes)
.data 0x8022F3D0 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8022F3D0 => li r4, 0xFFFFFDB0
# region @ 80230974 (4 bytes)
.data 0x80230974 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80230974 => li r4, 0xFFFFFF00
# region @ 802309A4 (4 bytes)
.data 0x802309A4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802309A4 => li r4, 0xFFFFFE80
# region @ 802309D4 (4 bytes)
.data 0x802309D4 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802309D4 => li r4, 0xFFFFFDB0
# region @ 802316E4 (4 bytes)
.data 0x802316E4 # address
.data 0x00000004 # size
.data 0x3880FF00 # 802316E4 => li r4, 0xFFFFFF00
# region @ 80231714 (4 bytes)
.data 0x80231714 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80231714 => li r4, 0xFFFFFE80
# region @ 80231744 (4 bytes)
.data 0x80231744 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80231744 => li r4, 0xFFFFFDB0
# region @ 80231FD8 (4 bytes)
.data 0x80231FD8 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80231FD8 => li r4, 0xFFFFFF00
# region @ 80232010 (4 bytes)
.data 0x80232010 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80232010 => li r4, 0xFFFFFE80
# region @ 80232048 (4 bytes)
.data 0x80232048 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80232048 => li r4, 0xFFFFFDB0
# region @ 80234084 (4 bytes)
.data 0x80234084 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80234084 => li r4, 0xFFFFFF00
# region @ 802340B4 (4 bytes)
.data 0x802340B4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802340B4 => li r4, 0xFFFFFE80
# region @ 802340E4 (4 bytes)
.data 0x802340E4 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802340E4 => li r4, 0xFFFFFDB0
# region @ 802366B0 (4 bytes)
.data 0x802366B0 # address
.data 0x00000004 # size
.data 0x3880FF00 # 802366B0 => li r4, 0xFFFFFF00
# region @ 802366EC (4 bytes)
.data 0x802366EC # address
.data 0x00000004 # size
.data 0x3880FE80 # 802366EC => li r4, 0xFFFFFE80
# region @ 80236728 (4 bytes)
.data 0x80236728 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80236728 => li r4, 0xFFFFFDB0
# region @ 80236E88 (4 bytes)
.data 0x80236E88 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80236E88 => li r4, 0xFFFFFF00
# region @ 80236EB8 (4 bytes)
.data 0x80236EB8 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80236EB8 => li r4, 0xFFFFFE80
# region @ 80236EE8 (4 bytes)
.data 0x80236EE8 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80236EE8 => li r4, 0xFFFFFDB0
# region @ 8023789C (4 bytes)
.data 0x8023789C # address
.data 0x00000004 # size
.data 0x3880FF00 # 8023789C => li r4, 0xFFFFFF00
# region @ 802378CC (4 bytes)
.data 0x802378CC # address
.data 0x00000004 # size
.data 0x3880FE80 # 802378CC => li r4, 0xFFFFFE80
# region @ 802378FC (4 bytes)
.data 0x802378FC # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802378FC => li r4, 0xFFFFFDB0
# region @ 80238274 (4 bytes)
.data 0x80238274 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80238274 => li r4, 0xFFFFFF00
# region @ 802382A4 (4 bytes)
.data 0x802382A4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802382A4 => li r4, 0xFFFFFE80
# region @ 802382D4 (4 bytes)
.data 0x802382D4 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802382D4 => li r4, 0xFFFFFDB0
# region @ 8023BBA4 (4 bytes)
.data 0x8023BBA4 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8023BBA4 => li r4, 0xFFFFFF00
# region @ 8023BBD4 (4 bytes)
.data 0x8023BBD4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8023BBD4 => li r4, 0xFFFFFE80
# region @ 8023BC04 (4 bytes)
.data 0x8023BC04 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8023BC04 => li r4, 0xFFFFFDB0
# region @ 80250AEC (4 bytes)
.data 0x80250AEC # address
.data 0x00000004 # size
.data 0x60000000 # 80250AEC => nop
# region @ 80268788 (4 bytes)
.data 0x80268788 # address
.data 0x00000004 # size
.data 0x60000000 # 80268788 => nop
# region @ 8026E2D4 (4 bytes)
.data 0x8026E2D4 # address
.data 0x00000004 # size
.data 0x3884AAFA # 8026E2D4 => subi r4, r4, 0x5506
# region @ 8026E3E8 (4 bytes)
.data 0x8026E3E8 # address
.data 0x00000004 # size
.data 0x3863AAFA # 8026E3E8 => subi r3, r3, 0x5506
# region @ 8026E470 (4 bytes)
.data 0x8026E470 # address
.data 0x00000004 # size
.data 0x3883AAFA # 8026E470 => subi r4, r3, 0x5506
# region @ 802BBEF4 (4 bytes)
.data 0x802BBEF4 # address
.data 0x00000004 # size
.data 0x4BD51A8C # 802BBEF4 => b -0x002AE574 /* 8000D980 */
# region @ 802FC2F4 (4 bytes)
.data 0x802FC2F4 # address
.data 0x00000004 # size
.data 0x2C030001 # 802FC2F4 => cmpwi r3, 1
# region @ 80301F58 (28 bytes)
.data 0x80301F58 # address
.data 0x0000001C # size
.data 0x48000020 # 80301F58 => b +0x00000020 /* 80301F78 */
.data 0x3863A830 # 80301F5C => subi r3, r3, 0x57D0
.data 0x800DB9A4 # 80301F60 => lwz r0, [r13 - 0x465C]
.data 0x2C000023 # 80301F64 => cmpwi r0, 35
.data 0x40820008 # 80301F68 => bne +0x00000008 /* 80301F70 */
.data 0x3863FB28 # 80301F6C => subi r3, r3, 0x04D8
.data 0x4800008C # 80301F70 => b +0x0000008C /* 80301FFC */
# region @ 80301FF8 (4 bytes)
.data 0x80301FF8 # address
.data 0x00000004 # size
.data 0x4BFFFF64 # 80301FF8 => b -0x0000009C /* 80301F5C */
# region @ 80335A50 (4 bytes)
.data 0x80335A50 # address
.data 0x00000004 # size
.data 0x4BCD7F50 # 80335A50 => b -0x003280B0 /* 8000D9A0 */
# region @ 80356814 (4 bytes)
.data 0x80356814 # address
.data 0x00000004 # size
.data 0x388001E8 # 80356814 => li r4, 0x01E8
# region @ 80356838 (4 bytes)
.data 0x80356838 # address
.data 0x00000004 # size
.data 0x4BCB79A9 # 80356838 => bl -0x00348658 /* 8000E1E0 */
# region @ 803568A8 (4 bytes)
.data 0x803568A8 # address
.data 0x00000004 # size
.data 0x388001E8 # 803568A8 => li r4, 0x01E8
# region @ 803568B8 (4 bytes)
.data 0x803568B8 # address
.data 0x00000004 # size
.data 0x4BCB7929 # 803568B8 => bl -0x003486D8 /* 8000E1E0 */
# region @ 804B3EF0 (8 bytes)
.data 0x804B3EF0 # address
.data 0x00000008 # size
.data 0x70808080 # 804B3EF0 => andi. r0, r4, 0x8080
.data 0x60707070 # 804B3EF4 => ori r16, r3, 0x7070
# region @ 804C76B4 (4 bytes)
.data 0x804C76B4 # address
.data 0x00000004 # size
.data 0x0000001E # 804C76B4 => .invalid
# region @ 804C770C (4 bytes)
.data 0x804C770C # address
.data 0x00000004 # size
.data 0x00000028 # 804C770C => .invalid
# region @ 804C7738 (4 bytes)
.data 0x804C7738 # address
.data 0x00000004 # size
.data 0x00000032 # 804C7738 => .invalid
# region @ 804C7764 (4 bytes)
.data 0x804C7764 # address
.data 0x00000004 # size
.data 0x0000003C # 804C7764 => .invalid
# region @ 804C7774 (4 bytes)
.data 0x804C7774 # address
.data 0x00000004 # size
.data 0x0018003C # 804C7774 => .invalid
# region @ 804C79CC (4 bytes)
.data 0x804C79CC # address
.data 0x00000004 # size
.data 0x00000028 # 804C79CC => .invalid
# region @ 804CC310 (4 bytes)
.data 0x804CC310 # address
.data 0x00000004 # size
.data 0xFF0074EE # 804CC310 => fsel f24, f0, f14, f19
# region @ 805CA274 (4 bytes)
.data 0x805CA274 # address
.data 0x00000004 # size
.data 0x435C0000 # 805CA274 => bc 26, 28, +0x00000000 /* 805CA274 */
# region @ 805CBF10 (4 bytes)
.data 0x805CBF10 # address
.data 0x00000004 # size
.data 0x46AFC800 # 805CBF10 => .invalid sc
# region @ 805CC1B0 (4 bytes)
.data 0x805CC1B0 # address
.data 0x00000004 # size
.data 0x43480000 # 805CC1B0 => bc 26, 8, +0x00000000 /* 805CC1B0 */
# end sentinel
.data 0x00000000 # address
.data 0x00000000 # size
@@ -1,564 +0,0 @@
.meta name="Bug fixes"
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
# Original code by Ralf @ GC-Forever and Aleron Ives
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksGC
# region @ 8000B088 (88 bytes)
.data 0x8000B088 # address
.data 0x00000058 # size
.data 0x7FA3EB78 # 8000B088 => mr r3, r29
.data 0x38800000 # 8000B08C => li r4, 0x0000
.data 0x481AEB11 # 8000B090 => bl +0x001AEB10 /* 801B9BA0 */
.data 0x7FA3EB78 # 8000B094 => mr r3, r29
.data 0x481AEDE0 # 8000B098 => b +0x001AEDE0 /* 801B9E78 */
.data 0x881F0000 # 8000B09C => lbz r0, [r31]
.data 0x28090001 # 8000B0A0 => cmplwi r9, 1
.data 0x4082000C # 8000B0A4 => bne +0x0000000C /* 8000B0B0 */
.data 0x881F0001 # 8000B0A8 => lbz r0, [r31 + 0x0001]
.data 0x3BFF0002 # 8000B0AC => addi r31, r31, 0x0002
.data 0x48100B68 # 8000B0B0 => b +0x00100B68 /* 8010BC18 */
.data 0x39200000 # 8000B0B4 => li r9, 0x0000
.data 0x48100AF9 # 8000B0B8 => bl +0x00100AF8 /* 8010BBB0 */
.data 0x7F43D378 # 8000B0BC => mr r3, r26
.data 0x7F64DB78 # 8000B0C0 => mr r4, r27
.data 0x7F85E378 # 8000B0C4 => mr r5, r28
.data 0x7FA6EB78 # 8000B0C8 => mr r6, r29
.data 0x7FC7F378 # 8000B0CC => mr r7, r30
.data 0x7FE8FB78 # 8000B0D0 => mr r8, r31
.data 0x39200001 # 8000B0D4 => li r9, 0x0001
.data 0x48100AD9 # 8000B0D8 => bl +0x00100AD8 /* 8010BBB0 */
.data 0x48102F64 # 8000B0DC => b +0x00102F64 /* 8010E040 */
# region @ 8000B5C8 (20 bytes)
.data 0x8000B5C8 # address
.data 0x00000014 # size
.data 0x80630098 # 8000B5C8 => lwz r3, [r3 + 0x0098]
.data 0x483D59F1 # 8000B5CC => bl +0x003D59F0 /* 803E0FBC */
.data 0x807F042C # 8000B5D0 => lwz r3, [r31 + 0x042C]
.data 0x809F0430 # 8000B5D4 => lwz r4, [r31 + 0x0430]
.data 0x48178C7C # 8000B5D8 => b +0x00178C7C /* 80184254 */
# region @ 8000BBD0 (32 bytes)
.data 0x8000BBD0 # address
.data 0x00000020 # size
.data 0x809F0370 # 8000BBD0 => lwz r4, [r31 + 0x0370]
.data 0x3884FC00 # 8000BBD4 => subi r4, r4, 0x0400
.data 0x909F0370 # 8000BBD8 => stw [r31 + 0x0370], r4
.data 0x807F0014 # 8000BBDC => lwz r3, [r31 + 0x0014]
.data 0x28030000 # 8000BBE0 => cmplwi r3, 0
.data 0x41820008 # 8000BBE4 => beq +0x00000008 /* 8000BBEC */
.data 0x90830060 # 8000BBE8 => stw [r3 + 0x0060], r4
.data 0x48165428 # 8000BBEC => b +0x00165428 /* 80171014 */
# region @ 8000C3F8 (124 bytes)
.data 0x8000C3F8 # address
.data 0x0000007C # size
.data 0x28040000 # 8000C3F8 => cmplwi r4, 0
.data 0x4D820020 # 8000C3FC => beqlr
.data 0x9421FFF0 # 8000C400 => stwu [r1 - 0x0010], r1
.data 0x481AD7A0 # 8000C404 => b +0x001AD7A0 /* 801B9BA4 */
.data 0x9421FFE0 # 8000C408 => stwu [r1 - 0x0020], r1
.data 0x7C0802A6 # 8000C40C => mflr r0
.data 0x90010024 # 8000C410 => stw [r1 + 0x0024], r0
.data 0xBF410008 # 8000C414 => stmw [r1 + 0x0008], r26
.data 0x7C7F1B78 # 8000C418 => mr r31, r3
.data 0x4BFFFFDD # 8000C41C => bl -0x00000024 /* 8000C3F8 */
.data 0x3BC00000 # 8000C420 => li r30, 0x0000
.data 0x3BBF0D04 # 8000C424 => addi r29, r31, 0x0D04
.data 0x837F032C # 8000C428 => lwz r27, [r31 + 0x032C]
.data 0x839D0000 # 8000C42C => lwz r28, [r29]
.data 0x7F83E379 # 8000C430 => mr. r3, r28
.data 0x41820018 # 8000C434 => beq +0x00000018 /* 8000C44C */
.data 0x38800001 # 8000C438 => li r4, 0x0001
.data 0x480FED81 # 8000C43C => bl +0x000FED80 /* 8010B1BC */
.data 0x7F83E378 # 8000C440 => mr r3, r28
.data 0x38800001 # 8000C444 => li r4, 0x0001
.data 0x480FEEF1 # 8000C448 => bl +0x000FEEF0 /* 8010B338 */
.data 0x3BBD0004 # 8000C44C => addi r29, r29, 0x0004
.data 0x3BDE0001 # 8000C450 => addi r30, r30, 0x0001
.data 0x2C1E000D # 8000C454 => cmpwi r30, 13
.data 0x4180FFD4 # 8000C458 => blt -0x0000002C /* 8000C42C */
.data 0x937F032C # 8000C45C => stw [r31 + 0x032C], r27
.data 0xBB410008 # 8000C460 => lmw r26, [r1 + 0x0008]
.data 0x80010024 # 8000C464 => lwz r0, [r1 + 0x0024]
.data 0x7C0803A6 # 8000C468 => mtlr r0
.data 0x38210020 # 8000C46C => addi r1, r1, 0x0020
.data 0x4E800020 # 8000C470 => blr
# region @ 8000C640 (20 bytes)
.data 0x8000C640 # address
.data 0x00000014 # size
.data 0x54800673 # 8000C640 => rlwinm. r0, r4, 0, 25, 25
.data 0x41820008 # 8000C644 => beq +0x00000008 /* 8000C64C */
.data 0x38800000 # 8000C648 => li r4, 0x0000
.data 0x38040009 # 8000C64C => addi r0, r4, 0x0009
.data 0x4810C938 # 8000C650 => b +0x0010C938 /* 80118F88 */
# region @ 8000C6D0 (32 bytes)
.data 0x8000C6D0 # address
.data 0x00000020 # size
.data 0x38000001 # 8000C6D0 => li r0, 0x0001
.data 0x901D0054 # 8000C6D4 => stw [r29 + 0x0054], r0
.data 0x807D0024 # 8000C6D8 => lwz r3, [r29 + 0x0024]
.data 0x48211244 # 8000C6DC => b +0x00211244 /* 8021D920 */
.data 0x38000001 # 8000C6E0 => li r0, 0x0001
.data 0x901F0378 # 8000C6E4 => stw [r31 + 0x0378], r0
.data 0x807F0024 # 8000C6E8 => lwz r3, [r31 + 0x0024]
.data 0x482146F4 # 8000C6EC => b +0x002146F4 /* 80220DE0 */
# region @ 8000C8A0 (20 bytes)
.data 0x8000C8A0 # address
.data 0x00000014 # size
.data 0x1C00000A # 8000C8A0 => mulli r0, r0, 10
.data 0x57E407BD # 8000C8A4 => rlwinm. r4, r31, 0, 30, 30
.data 0x41820008 # 8000C8A8 => beq +0x00000008 /* 8000C8B0 */
.data 0x7FA00734 # 8000C8AC => extsh r0, r29
.data 0x4810605C # 8000C8B0 => b +0x0010605C /* 8011290C */
# region @ 8000C8C0 (16 bytes)
.data 0x8000C8C0 # address
.data 0x00000010 # size
.data 0x7000000F # 8000C8C0 => andi. r0, r0, 0x000F
.data 0x7000004F # 8000C8C4 => andi. r0, r0, 0x004F
.data 0x2C000004 # 8000C8C8 => cmpwi r0, 4
.data 0x4E800020 # 8000C8CC => blr
# region @ 8000D980 (20 bytes)
.data 0x8000D980 # address
.data 0x00000014 # size
.data 0x807C0000 # 8000D980 => lwz r3, [r28]
.data 0x2C030013 # 8000D984 => cmpwi r3, 19
.data 0x40820008 # 8000D988 => bne +0x00000008 /* 8000D990 */
.data 0x38600002 # 8000D98C => li r3, 0x0002
.data 0x482AE5AC # 8000D990 => b +0x002AE5AC /* 802BBF3C */
# region @ 8000D9A0 (24 bytes)
.data 0x8000D9A0 # address
.data 0x00000018 # size
.data 0xC042FC88 # 8000D9A0 => lfs f2, [r2 - 0x0378]
.data 0x807E0030 # 8000D9A4 => lwz r3, [r30 + 0x0030]
.data 0x70630020 # 8000D9A8 => andi. r3, r3, 0x0020
.data 0x41820008 # 8000D9AC => beq +0x00000008 /* 8000D9B4 */
.data 0xC042FCA0 # 8000D9B0 => lfs f2, [r2 - 0x0360]
.data 0x483280E4 # 8000D9B4 => b +0x003280E4 /* 80335A98 */
# region @ 8000E1E0 (28 bytes)
.data 0x8000E1E0 # address
.data 0x0000001C # size
.data 0x7FC802A6 # 8000E1E0 => mflr r30
.data 0x38A00000 # 8000E1E4 => li r5, 0x0000
.data 0x38C0001E # 8000E1E8 => li r6, 0x001E
.data 0x38E00040 # 8000E1EC => li r7, 0x0040
.data 0x4807853D # 8000E1F0 => bl +0x0007853C /* 8008672C */
.data 0x7FC803A6 # 8000E1F4 => mtlr r30
.data 0x4E800020 # 8000E1F8 => blr
# region @ 80013084 (4 bytes)
.data 0x80013084 # address
.data 0x00000004 # size
.data 0x4BFFFCC0 # 80013084 => b -0x00000340 /* 80012D44 */
# region @ 800142F4 (4 bytes)
.data 0x800142F4 # address
.data 0x00000004 # size
.data 0x4BFF85CD # 800142F4 => bl -0x00007A34 /* 8000C8C0 */
# region @ 80015D1C (4 bytes)
.data 0x80015D1C # address
.data 0x00000004 # size
.data 0x4BFF6BA9 # 80015D1C => bl -0x00009458 /* 8000C8C4 */
# region @ 800917B4 (8 bytes)
.data 0x800917B4 # address
.data 0x00000008 # size
.data 0x4800024D # 800917B4 => bl +0x0000024C /* 80091A00 */
.data 0xB3C3032C # 800917B8 => sth [r3 + 0x032C], r30
# region @ 800BC9E8 (4 bytes)
.data 0x800BC9E8 # address
.data 0x00000004 # size
.data 0x48000010 # 800BC9E8 => b +0x00000010 /* 800BC9F8 */
# region @ 80101EB8 (4 bytes)
.data 0x80101EB8 # address
.data 0x00000004 # size
.data 0x60000000 # 80101EB8 => nop
# region @ 80104DEC (4 bytes)
.data 0x80104DEC # address
.data 0x00000004 # size
.data 0x4182000C # 80104DEC => beq +0x0000000C /* 80104DF8 */
# region @ 8010771C (4 bytes)
.data 0x8010771C # address
.data 0x00000004 # size
.data 0x4800000C # 8010771C => b +0x0000000C /* 80107728 */
# region @ 80107730 (4 bytes)
.data 0x80107730 # address
.data 0x00000004 # size
.data 0x7C030378 # 80107730 => mr r3, r0
# region @ 8010BC14 (4 bytes)
.data 0x8010BC14 # address
.data 0x00000004 # size
.data 0x4BEFF488 # 8010BC14 => b -0x00100B78 /* 8000B09C */
# region @ 8010E03C (4 bytes)
.data 0x8010E03C # address
.data 0x00000004 # size
.data 0x4BEFD078 # 8010E03C => b -0x00102F88 /* 8000B0B4 */
# region @ 80112908 (4 bytes)
.data 0x80112908 # address
.data 0x00000004 # size
.data 0x4BEF9F98 # 80112908 => b -0x00106068 /* 8000C8A0 */
# region @ 8011461C (4 bytes)
.data 0x8011461C # address
.data 0x00000004 # size
.data 0x38000012 # 8011461C => li r0, 0x0012
# region @ 80118854 (4 bytes)
.data 0x80118854 # address
.data 0x00000004 # size
.data 0x88040016 # 80118854 => lbz r0, [r4 + 0x0016]
# region @ 80118860 (4 bytes)
.data 0x80118860 # address
.data 0x00000004 # size
.data 0x88040017 # 80118860 => lbz r0, [r4 + 0x0017]
# region @ 80118F84 (4 bytes)
.data 0x80118F84 # address
.data 0x00000004 # size
.data 0x4BEF36BC # 80118F84 => b -0x0010C944 /* 8000C640 */
# region @ 8011CD34 (12 bytes)
.data 0x8011CD34 # address
.data 0x0000000C # size
.data 0x7C030378 # 8011CD34 => mr r3, r0
.data 0x3863FFFF # 8011CD38 => subi r3, r3, 0x0001
.data 0x4BFFFFE8 # 8011CD3C => b -0x00000018 /* 8011CD24 */
# region @ 8011CDF0 (12 bytes)
.data 0x8011CDF0 # address
.data 0x0000000C # size
.data 0x7C030378 # 8011CDF0 => mr r3, r0
.data 0x3863FFFF # 8011CDF4 => subi r3, r3, 0x0001
.data 0x4BFFFFE8 # 8011CDF8 => b -0x00000018 /* 8011CDE0 */
# region @ 8011CE40 (12 bytes)
.data 0x8011CE40 # address
.data 0x0000000C # size
.data 0x7C040378 # 8011CE40 => mr r4, r0
.data 0x3884FFFF # 8011CE44 => subi r4, r4, 0x0001
.data 0x4BFFFFE8 # 8011CE48 => b -0x00000018 /* 8011CE30 */
# region @ 801666E0 (8 bytes)
.data 0x801666E0 # address
.data 0x00000008 # size
.data 0x3C604005 # 801666E0 => lis r3, 0x4005
.data 0x4800009C # 801666E4 => b +0x0000009C /* 80166780 */
# region @ 8016677C (4 bytes)
.data 0x8016677C # address
.data 0x00000004 # size
.data 0x4800001C # 8016677C => b +0x0000001C /* 80166798 */
# region @ 80171010 (4 bytes)
.data 0x80171010 # address
.data 0x00000004 # size
.data 0x4BE9ABC0 # 80171010 => b -0x00165440 /* 8000BBD0 */
# region @ 80171030 (4 bytes)
.data 0x80171030 # address
.data 0x00000004 # size
.data 0x60800420 # 80171030 => ori r0, r4, 0x0420
# region @ 80184250 (4 bytes)
.data 0x80184250 # address
.data 0x00000004 # size
.data 0x4BE87378 # 80184250 => b -0x00178C88 /* 8000B5C8 */
# region @ 80184290 (4 bytes)
.data 0x80184290 # address
.data 0x00000004 # size
.data 0x60000000 # 80184290 => nop
# region @ 80189E20 (4 bytes)
.data 0x80189E20 # address
.data 0x00000004 # size
.data 0x60000000 # 80189E20 => nop
# region @ 801937A8 (4 bytes)
.data 0x801937A8 # address
.data 0x00000004 # size
.data 0x60000000 # 801937A8 => nop
# region @ 801B9BA0 (4 bytes)
.data 0x801B9BA0 # address
.data 0x00000004 # size
.data 0x4BE52868 # 801B9BA0 => b -0x001AD798 /* 8000C408 */
# region @ 801B9E74 (4 bytes)
.data 0x801B9E74 # address
.data 0x00000004 # size
.data 0x4BE51214 # 801B9E74 => b -0x001AEDEC /* 8000B088 */
# region @ 801C62C0 (4 bytes)
.data 0x801C62C0 # address
.data 0x00000004 # size
.data 0x389F02FC # 801C62C0 => addi r4, r31, 0x02FC
# region @ 801CA610 (4 bytes)
.data 0x801CA610 # address
.data 0x00000004 # size
.data 0x48000010 # 801CA610 => b +0x00000010 /* 801CA620 */
# region @ 8021D91C (4 bytes)
.data 0x8021D91C # address
.data 0x00000004 # size
.data 0x4BDEEDB4 # 8021D91C => b -0x0021124C /* 8000C6D0 */
# region @ 80220DDC (4 bytes)
.data 0x80220DDC # address
.data 0x00000004 # size
.data 0x4BDEB904 # 80220DDC => b -0x002146FC /* 8000C6E0 */
# region @ 80229C10 (4 bytes)
.data 0x80229C10 # address
.data 0x00000004 # size
.data 0x2C000001 # 80229C10 => cmpwi r0, 1
# region @ 8022A410 (4 bytes)
.data 0x8022A410 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8022A410 => li r4, 0xFFFFFF00
# region @ 8022A440 (4 bytes)
.data 0x8022A440 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8022A440 => li r4, 0xFFFFFE80
# region @ 8022A470 (4 bytes)
.data 0x8022A470 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8022A470 => li r4, 0xFFFFFDB0
# region @ 8022D10C (4 bytes)
.data 0x8022D10C # address
.data 0x00000004 # size
.data 0x60000000 # 8022D10C => nop
# region @ 8022D840 (4 bytes)
.data 0x8022D840 # address
.data 0x00000004 # size
.data 0x41810630 # 8022D840 => bgt +0x00000630 /* 8022DE70 */
# region @ 8022DB34 (4 bytes)
.data 0x8022DB34 # address
.data 0x00000004 # size
.data 0x4181033C # 8022DB34 => bgt +0x0000033C /* 8022DE70 */
# region @ 8022DC28 (4 bytes)
.data 0x8022DC28 # address
.data 0x00000004 # size
.data 0x41810248 # 8022DC28 => bgt +0x00000248 /* 8022DE70 */
# region @ 8022EB64 (4 bytes)
.data 0x8022EB64 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8022EB64 => li r4, 0xFFFFFF00
# region @ 8022EB94 (4 bytes)
.data 0x8022EB94 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8022EB94 => li r4, 0xFFFFFE80
# region @ 8022EBC4 (4 bytes)
.data 0x8022EBC4 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8022EBC4 => li r4, 0xFFFFFDB0
# region @ 8022F370 (4 bytes)
.data 0x8022F370 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8022F370 => li r4, 0xFFFFFF00
# region @ 8022F3A0 (4 bytes)
.data 0x8022F3A0 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8022F3A0 => li r4, 0xFFFFFE80
# region @ 8022F3D0 (4 bytes)
.data 0x8022F3D0 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8022F3D0 => li r4, 0xFFFFFDB0
# region @ 80230974 (4 bytes)
.data 0x80230974 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80230974 => li r4, 0xFFFFFF00
# region @ 802309A4 (4 bytes)
.data 0x802309A4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802309A4 => li r4, 0xFFFFFE80
# region @ 802309D4 (4 bytes)
.data 0x802309D4 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802309D4 => li r4, 0xFFFFFDB0
# region @ 802316E4 (4 bytes)
.data 0x802316E4 # address
.data 0x00000004 # size
.data 0x3880FF00 # 802316E4 => li r4, 0xFFFFFF00
# region @ 80231714 (4 bytes)
.data 0x80231714 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80231714 => li r4, 0xFFFFFE80
# region @ 80231744 (4 bytes)
.data 0x80231744 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80231744 => li r4, 0xFFFFFDB0
# region @ 80231FD8 (4 bytes)
.data 0x80231FD8 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80231FD8 => li r4, 0xFFFFFF00
# region @ 80232010 (4 bytes)
.data 0x80232010 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80232010 => li r4, 0xFFFFFE80
# region @ 80232048 (4 bytes)
.data 0x80232048 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80232048 => li r4, 0xFFFFFDB0
# region @ 80234084 (4 bytes)
.data 0x80234084 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80234084 => li r4, 0xFFFFFF00
# region @ 802340B4 (4 bytes)
.data 0x802340B4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802340B4 => li r4, 0xFFFFFE80
# region @ 802340E4 (4 bytes)
.data 0x802340E4 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802340E4 => li r4, 0xFFFFFDB0
# region @ 802366B0 (4 bytes)
.data 0x802366B0 # address
.data 0x00000004 # size
.data 0x3880FF00 # 802366B0 => li r4, 0xFFFFFF00
# region @ 802366EC (4 bytes)
.data 0x802366EC # address
.data 0x00000004 # size
.data 0x3880FE80 # 802366EC => li r4, 0xFFFFFE80
# region @ 80236728 (4 bytes)
.data 0x80236728 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80236728 => li r4, 0xFFFFFDB0
# region @ 80236E88 (4 bytes)
.data 0x80236E88 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80236E88 => li r4, 0xFFFFFF00
# region @ 80236EB8 (4 bytes)
.data 0x80236EB8 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80236EB8 => li r4, 0xFFFFFE80
# region @ 80236EE8 (4 bytes)
.data 0x80236EE8 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80236EE8 => li r4, 0xFFFFFDB0
# region @ 8023789C (4 bytes)
.data 0x8023789C # address
.data 0x00000004 # size
.data 0x3880FF00 # 8023789C => li r4, 0xFFFFFF00
# region @ 802378CC (4 bytes)
.data 0x802378CC # address
.data 0x00000004 # size
.data 0x3880FE80 # 802378CC => li r4, 0xFFFFFE80
# region @ 802378FC (4 bytes)
.data 0x802378FC # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802378FC => li r4, 0xFFFFFDB0
# region @ 80238274 (4 bytes)
.data 0x80238274 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80238274 => li r4, 0xFFFFFF00
# region @ 802382A4 (4 bytes)
.data 0x802382A4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802382A4 => li r4, 0xFFFFFE80
# region @ 802382D4 (4 bytes)
.data 0x802382D4 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802382D4 => li r4, 0xFFFFFDB0
# region @ 8023BBA4 (4 bytes)
.data 0x8023BBA4 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8023BBA4 => li r4, 0xFFFFFF00
# region @ 8023BBD4 (4 bytes)
.data 0x8023BBD4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8023BBD4 => li r4, 0xFFFFFE80
# region @ 8023BC04 (4 bytes)
.data 0x8023BC04 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8023BC04 => li r4, 0xFFFFFDB0
# region @ 80250AEC (4 bytes)
.data 0x80250AEC # address
.data 0x00000004 # size
.data 0x60000000 # 80250AEC => nop
# region @ 80268788 (4 bytes)
.data 0x80268788 # address
.data 0x00000004 # size
.data 0x60000000 # 80268788 => nop
# region @ 8026E2D4 (4 bytes)
.data 0x8026E2D4 # address
.data 0x00000004 # size
.data 0x3884AAFA # 8026E2D4 => subi r4, r4, 0x5506
# region @ 8026E3E8 (4 bytes)
.data 0x8026E3E8 # address
.data 0x00000004 # size
.data 0x3863AAFA # 8026E3E8 => subi r3, r3, 0x5506
# region @ 8026E470 (4 bytes)
.data 0x8026E470 # address
.data 0x00000004 # size
.data 0x3883AAFA # 8026E470 => subi r4, r3, 0x5506
# region @ 802BBF38 (4 bytes)
.data 0x802BBF38 # address
.data 0x00000004 # size
.data 0x4BD51A48 # 802BBF38 => b -0x002AE5B8 /* 8000D980 */
# region @ 802FC338 (4 bytes)
.data 0x802FC338 # address
.data 0x00000004 # size
.data 0x2C030001 # 802FC338 => cmpwi r3, 1
# region @ 80301F9C (28 bytes)
.data 0x80301F9C # address
.data 0x0000001C # size
.data 0x48000020 # 80301F9C => b +0x00000020 /* 80301FBC */
.data 0x3863A830 # 80301FA0 => subi r3, r3, 0x57D0
.data 0x800DB9A4 # 80301FA4 => lwz r0, [r13 - 0x465C]
.data 0x2C000023 # 80301FA8 => cmpwi r0, 35
.data 0x40820008 # 80301FAC => bne +0x00000008 /* 80301FB4 */
.data 0x3863FB28 # 80301FB0 => subi r3, r3, 0x04D8
.data 0x4800008C # 80301FB4 => b +0x0000008C /* 80302040 */
# region @ 8030203C (4 bytes)
.data 0x8030203C # address
.data 0x00000004 # size
.data 0x4BFFFF64 # 8030203C => b -0x0000009C /* 80301FA0 */
# region @ 80335A94 (4 bytes)
.data 0x80335A94 # address
.data 0x00000004 # size
.data 0x4BCD7F0C # 80335A94 => b -0x003280F4 /* 8000D9A0 */
# region @ 80356858 (4 bytes)
.data 0x80356858 # address
.data 0x00000004 # size
.data 0x388001E8 # 80356858 => li r4, 0x01E8
# region @ 8035687C (4 bytes)
.data 0x8035687C # address
.data 0x00000004 # size
.data 0x4BCB7965 # 8035687C => bl -0x0034869C /* 8000E1E0 */
# region @ 803568EC (4 bytes)
.data 0x803568EC # address
.data 0x00000004 # size
.data 0x388001E8 # 803568EC => li r4, 0x01E8
# region @ 803568FC (4 bytes)
.data 0x803568FC # address
.data 0x00000004 # size
.data 0x4BCB78E5 # 803568FC => bl -0x0034871C /* 8000E1E0 */
# region @ 804B43D0 (8 bytes)
.data 0x804B43D0 # address
.data 0x00000008 # size
.data 0x70808080 # 804B43D0 => andi. r0, r4, 0x8080
.data 0x60707070 # 804B43D4 => ori r16, r3, 0x7070
# region @ 804C7B94 (4 bytes)
.data 0x804C7B94 # address
.data 0x00000004 # size
.data 0x0000001E # 804C7B94 => .invalid
# region @ 804C7BEC (4 bytes)
.data 0x804C7BEC # address
.data 0x00000004 # size
.data 0x00000028 # 804C7BEC => .invalid
# region @ 804C7C18 (4 bytes)
.data 0x804C7C18 # address
.data 0x00000004 # size
.data 0x00000032 # 804C7C18 => .invalid
# region @ 804C7C44 (4 bytes)
.data 0x804C7C44 # address
.data 0x00000004 # size
.data 0x0000003C # 804C7C44 => .invalid
# region @ 804C7C54 (4 bytes)
.data 0x804C7C54 # address
.data 0x00000004 # size
.data 0x0018003C # 804C7C54 => .invalid
# region @ 804C7EAC (4 bytes)
.data 0x804C7EAC # address
.data 0x00000004 # size
.data 0x00000028 # 804C7EAC => .invalid
# region @ 804CC7F0 (4 bytes)
.data 0x804CC7F0 # address
.data 0x00000004 # size
.data 0xFF0074EE # 804CC7F0 => fsel f24, f0, f14, f19
# region @ 805D1294 (4 bytes)
.data 0x805D1294 # address
.data 0x00000004 # size
.data 0x435C0000 # 805D1294 => bc 26, 28, +0x00000000 /* 805D1294 */
# region @ 805D2F30 (4 bytes)
.data 0x805D2F30 # address
.data 0x00000004 # size
.data 0x46AFC800 # 805D2F30 => .invalid sc
# region @ 805D31D0 (4 bytes)
.data 0x805D31D0 # address
.data 0x00000004 # size
.data 0x43480000 # 805D31D0 => bc 26, 8, +0x00000000 /* 805D31D0 */
# end sentinel
.data 0x00000000 # address
.data 0x00000000 # size
@@ -1,552 +0,0 @@
.meta name="Bug fixes"
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
# Original code by Ralf @ GC-Forever and Aleron Ives
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksGC
# region @ 8000B088 (88 bytes)
.data 0x8000B088 # address
.data 0x00000058 # size
.data 0x7FA3EB78 # 8000B088 => mr r3, r29
.data 0x38800000 # 8000B08C => li r4, 0x0000
.data 0x481AECC1 # 8000B090 => bl +0x001AECC0 /* 801B9D50 */
.data 0x7FA3EB78 # 8000B094 => mr r3, r29
.data 0x481AEF90 # 8000B098 => b +0x001AEF90 /* 801BA028 */
.data 0x881F0000 # 8000B09C => lbz r0, [r31]
.data 0x28090001 # 8000B0A0 => cmplwi r9, 1
.data 0x4082000C # 8000B0A4 => bne +0x0000000C /* 8000B0B0 */
.data 0x881F0001 # 8000B0A8 => lbz r0, [r31 + 0x0001]
.data 0x3BFF0002 # 8000B0AC => addi r31, r31, 0x0002
.data 0x48100A54 # 8000B0B0 => b +0x00100A54 /* 8010BB04 */
.data 0x39200000 # 8000B0B4 => li r9, 0x0000
.data 0x481009E5 # 8000B0B8 => bl +0x001009E4 /* 8010BA9C */
.data 0x7F43D378 # 8000B0BC => mr r3, r26
.data 0x7F64DB78 # 8000B0C0 => mr r4, r27
.data 0x7F85E378 # 8000B0C4 => mr r5, r28
.data 0x7FA6EB78 # 8000B0C8 => mr r6, r29
.data 0x7FC7F378 # 8000B0CC => mr r7, r30
.data 0x7FE8FB78 # 8000B0D0 => mr r8, r31
.data 0x39200001 # 8000B0D4 => li r9, 0x0001
.data 0x481009C5 # 8000B0D8 => bl +0x001009C4 /* 8010BA9C */
.data 0x48102E5C # 8000B0DC => b +0x00102E5C /* 8010DF38 */
# region @ 8000B5C8 (20 bytes)
.data 0x8000B5C8 # address
.data 0x00000014 # size
.data 0x80630098 # 8000B5C8 => lwz r3, [r3 + 0x0098]
.data 0x483D90F1 # 8000B5CC => bl +0x003D90F0 /* 803E46BC */
.data 0x807F042C # 8000B5D0 => lwz r3, [r31 + 0x042C]
.data 0x809F0430 # 8000B5D4 => lwz r4, [r31 + 0x0430]
.data 0x48178DB0 # 8000B5D8 => b +0x00178DB0 /* 80184388 */
# region @ 8000BBD0 (32 bytes)
.data 0x8000BBD0 # address
.data 0x00000020 # size
.data 0x809F0370 # 8000BBD0 => lwz r4, [r31 + 0x0370]
.data 0x3884FC00 # 8000BBD4 => subi r4, r4, 0x0400
.data 0x909F0370 # 8000BBD8 => stw [r31 + 0x0370], r4
.data 0x807F0014 # 8000BBDC => lwz r3, [r31 + 0x0014]
.data 0x28030000 # 8000BBE0 => cmplwi r3, 0
.data 0x41820008 # 8000BBE4 => beq +0x00000008 /* 8000BBEC */
.data 0x90830060 # 8000BBE8 => stw [r3 + 0x0060], r4
.data 0x48165548 # 8000BBEC => b +0x00165548 /* 80171134 */
# region @ 8000C3F8 (124 bytes)
.data 0x8000C3F8 # address
.data 0x0000007C # size
.data 0x28040000 # 8000C3F8 => cmplwi r4, 0
.data 0x4D820020 # 8000C3FC => beqlr
.data 0x9421FFF0 # 8000C400 => stwu [r1 - 0x0010], r1
.data 0x481AD950 # 8000C404 => b +0x001AD950 /* 801B9D54 */
.data 0x9421FFE0 # 8000C408 => stwu [r1 - 0x0020], r1
.data 0x7C0802A6 # 8000C40C => mflr r0
.data 0x90010024 # 8000C410 => stw [r1 + 0x0024], r0
.data 0xBF410008 # 8000C414 => stmw [r1 + 0x0008], r26
.data 0x7C7F1B78 # 8000C418 => mr r31, r3
.data 0x4BFFFFDD # 8000C41C => bl -0x00000024 /* 8000C3F8 */
.data 0x3BC00000 # 8000C420 => li r30, 0x0000
.data 0x3BBF0D04 # 8000C424 => addi r29, r31, 0x0D04
.data 0x837F032C # 8000C428 => lwz r27, [r31 + 0x032C]
.data 0x839D0000 # 8000C42C => lwz r28, [r29]
.data 0x7F83E379 # 8000C430 => mr. r3, r28
.data 0x41820018 # 8000C434 => beq +0x00000018 /* 8000C44C */
.data 0x38800001 # 8000C438 => li r4, 0x0001
.data 0x480FEC6D # 8000C43C => bl +0x000FEC6C /* 8010B0A8 */
.data 0x7F83E378 # 8000C440 => mr r3, r28
.data 0x38800001 # 8000C444 => li r4, 0x0001
.data 0x480FEDDD # 8000C448 => bl +0x000FEDDC /* 8010B224 */
.data 0x3BBD0004 # 8000C44C => addi r29, r29, 0x0004
.data 0x3BDE0001 # 8000C450 => addi r30, r30, 0x0001
.data 0x2C1E000D # 8000C454 => cmpwi r30, 13
.data 0x4180FFD4 # 8000C458 => blt -0x0000002C /* 8000C42C */
.data 0x937F032C # 8000C45C => stw [r31 + 0x032C], r27
.data 0xBB410008 # 8000C460 => lmw r26, [r1 + 0x0008]
.data 0x80010024 # 8000C464 => lwz r0, [r1 + 0x0024]
.data 0x7C0803A6 # 8000C468 => mtlr r0
.data 0x38210020 # 8000C46C => addi r1, r1, 0x0020
.data 0x4E800020 # 8000C470 => blr
# region @ 8000C640 (20 bytes)
.data 0x8000C640 # address
.data 0x00000014 # size
.data 0x54800673 # 8000C640 => rlwinm. r0, r4, 0, 25, 25
.data 0x41820008 # 8000C644 => beq +0x00000008 /* 8000C64C */
.data 0x38800000 # 8000C648 => li r4, 0x0000
.data 0x38040009 # 8000C64C => addi r0, r4, 0x0009
.data 0x4810C858 # 8000C650 => b +0x0010C858 /* 80118EA8 */
# region @ 8000C6D0 (32 bytes)
.data 0x8000C6D0 # address
.data 0x00000020 # size
.data 0x38000001 # 8000C6D0 => li r0, 0x0001
.data 0x901D0054 # 8000C6D4 => stw [r29 + 0x0054], r0
.data 0x807D0024 # 8000C6D8 => lwz r3, [r29 + 0x0024]
.data 0x482122F8 # 8000C6DC => b +0x002122F8 /* 8021E9D4 */
.data 0x38000001 # 8000C6E0 => li r0, 0x0001
.data 0x901F0378 # 8000C6E4 => stw [r31 + 0x0378], r0
.data 0x807F0024 # 8000C6E8 => lwz r3, [r31 + 0x0024]
.data 0x482157A8 # 8000C6EC => b +0x002157A8 /* 80221E94 */
# region @ 8000C8A0 (20 bytes)
.data 0x8000C8A0 # address
.data 0x00000014 # size
.data 0x1C00000A # 8000C8A0 => mulli r0, r0, 10
.data 0x57E407BD # 8000C8A4 => rlwinm. r4, r31, 0, 30, 30
.data 0x41820008 # 8000C8A8 => beq +0x00000008 /* 8000C8B0 */
.data 0x7FA00734 # 8000C8AC => extsh r0, r29
.data 0x48105F54 # 8000C8B0 => b +0x00105F54 /* 80112804 */
# region @ 8000C8C0 (16 bytes)
.data 0x8000C8C0 # address
.data 0x00000010 # size
.data 0x7000000F # 8000C8C0 => andi. r0, r0, 0x000F
.data 0x7000004F # 8000C8C4 => andi. r0, r0, 0x004F
.data 0x2C000004 # 8000C8C8 => cmpwi r0, 4
.data 0x4E800020 # 8000C8CC => blr
# region @ 8000D980 (20 bytes)
.data 0x8000D980 # address
.data 0x00000014 # size
.data 0x807C0000 # 8000D980 => lwz r3, [r28]
.data 0x2C030013 # 8000D984 => cmpwi r3, 19
.data 0x40820008 # 8000D988 => bne +0x00000008 /* 8000D990 */
.data 0x38600002 # 8000D98C => li r3, 0x0002
.data 0x482AFAE8 # 8000D990 => b +0x002AFAE8 /* 802BD478 */
# region @ 8000D9A0 (24 bytes)
.data 0x8000D9A0 # address
.data 0x00000018 # size
.data 0xC042FC88 # 8000D9A0 => lfs f2, [r2 - 0x0378]
.data 0x807E0030 # 8000D9A4 => lwz r3, [r30 + 0x0030]
.data 0x70630020 # 8000D9A8 => andi. r3, r3, 0x0020
.data 0x41820008 # 8000D9AC => beq +0x00000008 /* 8000D9B4 */
.data 0xC042FCA0 # 8000D9B0 => lfs f2, [r2 - 0x0360]
.data 0x48329BC0 # 8000D9B4 => b +0x00329BC0 /* 80337574 */
# region @ 8000E1E0 (28 bytes)
.data 0x8000E1E0 # address
.data 0x0000001C # size
.data 0x7FC802A6 # 8000E1E0 => mflr r30
.data 0x38A00000 # 8000E1E4 => li r5, 0x0000
.data 0x38C0001E # 8000E1E8 => li r6, 0x001E
.data 0x38E00040 # 8000E1EC => li r7, 0x0040
.data 0x480786D5 # 8000E1F0 => bl +0x000786D4 /* 800868C4 */
.data 0x7FC803A6 # 8000E1F4 => mtlr r30
.data 0x4E800020 # 8000E1F8 => blr
# region @ 8001304C (4 bytes)
.data 0x8001304C # address
.data 0x00000004 # size
.data 0x4BFFFCC0 # 8001304C => b -0x00000340 /* 80012D0C */
# region @ 800142BC (4 bytes)
.data 0x800142BC # address
.data 0x00000004 # size
.data 0x4BFF8605 # 800142BC => bl -0x000079FC /* 8000C8C0 */
# region @ 80015CE4 (4 bytes)
.data 0x80015CE4 # address
.data 0x00000004 # size
.data 0x4BFF6BE1 # 80015CE4 => bl -0x00009420 /* 8000C8C4 */
# region @ 8009194C (8 bytes)
.data 0x8009194C # address
.data 0x00000008 # size
.data 0x4800024D # 8009194C => bl +0x0000024C /* 80091B98 */
.data 0xB3C3032C # 80091950 => sth [r3 + 0x032C], r30
# region @ 800BCB90 (4 bytes)
.data 0x800BCB90 # address
.data 0x00000004 # size
.data 0x48000010 # 800BCB90 => b +0x00000010 /* 800BCBA0 */
# region @ 80104CB4 (4 bytes)
.data 0x80104CB4 # address
.data 0x00000004 # size
.data 0x4182000C # 80104CB4 => beq +0x0000000C /* 80104CC0 */
# region @ 801075E4 (4 bytes)
.data 0x801075E4 # address
.data 0x00000004 # size
.data 0x4800000C # 801075E4 => b +0x0000000C /* 801075F0 */
# region @ 801075F8 (4 bytes)
.data 0x801075F8 # address
.data 0x00000004 # size
.data 0x7C030378 # 801075F8 => mr r3, r0
# region @ 8010BB00 (4 bytes)
.data 0x8010BB00 # address
.data 0x00000004 # size
.data 0x4BEFF59C # 8010BB00 => b -0x00100A64 /* 8000B09C */
# region @ 8010DF34 (4 bytes)
.data 0x8010DF34 # address
.data 0x00000004 # size
.data 0x4BEFD180 # 8010DF34 => b -0x00102E80 /* 8000B0B4 */
# region @ 80112800 (4 bytes)
.data 0x80112800 # address
.data 0x00000004 # size
.data 0x4BEFA0A0 # 80112800 => b -0x00105F60 /* 8000C8A0 */
# region @ 80114534 (4 bytes)
.data 0x80114534 # address
.data 0x00000004 # size
.data 0x38000012 # 80114534 => li r0, 0x0012
# region @ 80118774 (4 bytes)
.data 0x80118774 # address
.data 0x00000004 # size
.data 0x88040016 # 80118774 => lbz r0, [r4 + 0x0016]
# region @ 80118780 (4 bytes)
.data 0x80118780 # address
.data 0x00000004 # size
.data 0x88040017 # 80118780 => lbz r0, [r4 + 0x0017]
# region @ 80118EA4 (4 bytes)
.data 0x80118EA4 # address
.data 0x00000004 # size
.data 0x4BEF379C # 80118EA4 => b -0x0010C864 /* 8000C640 */
# region @ 8011CC7C (12 bytes)
.data 0x8011CC7C # address
.data 0x0000000C # size
.data 0x7C030378 # 8011CC7C => mr r3, r0
.data 0x3863FFFF # 8011CC80 => subi r3, r3, 0x0001
.data 0x4BFFFFE8 # 8011CC84 => b -0x00000018 /* 8011CC6C */
# region @ 8011CD38 (12 bytes)
.data 0x8011CD38 # address
.data 0x0000000C # size
.data 0x7C030378 # 8011CD38 => mr r3, r0
.data 0x3863FFFF # 8011CD3C => subi r3, r3, 0x0001
.data 0x4BFFFFE8 # 8011CD40 => b -0x00000018 /* 8011CD28 */
# region @ 8011CD88 (12 bytes)
.data 0x8011CD88 # address
.data 0x0000000C # size
.data 0x7C040378 # 8011CD88 => mr r4, r0
.data 0x3884FFFF # 8011CD8C => subi r4, r4, 0x0001
.data 0x4BFFFFE8 # 8011CD90 => b -0x00000018 /* 8011CD78 */
# region @ 80166800 (8 bytes)
.data 0x80166800 # address
.data 0x00000008 # size
.data 0x3C604005 # 80166800 => lis r3, 0x4005
.data 0x4800009C # 80166804 => b +0x0000009C /* 801668A0 */
# region @ 8016689C (4 bytes)
.data 0x8016689C # address
.data 0x00000004 # size
.data 0x4800001C # 8016689C => b +0x0000001C /* 801668B8 */
# region @ 80171130 (4 bytes)
.data 0x80171130 # address
.data 0x00000004 # size
.data 0x4BE9AAA0 # 80171130 => b -0x00165560 /* 8000BBD0 */
# region @ 80171150 (4 bytes)
.data 0x80171150 # address
.data 0x00000004 # size
.data 0x60800420 # 80171150 => ori r0, r4, 0x0420
# region @ 80184384 (4 bytes)
.data 0x80184384 # address
.data 0x00000004 # size
.data 0x4BE87244 # 80184384 => b -0x00178DBC /* 8000B5C8 */
# region @ 801843C4 (4 bytes)
.data 0x801843C4 # address
.data 0x00000004 # size
.data 0x60000000 # 801843C4 => nop
# region @ 80189F54 (4 bytes)
.data 0x80189F54 # address
.data 0x00000004 # size
.data 0x60000000 # 80189F54 => nop
# region @ 801938D8 (4 bytes)
.data 0x801938D8 # address
.data 0x00000004 # size
.data 0x60000000 # 801938D8 => nop
# region @ 801B9D50 (4 bytes)
.data 0x801B9D50 # address
.data 0x00000004 # size
.data 0x4BE526B8 # 801B9D50 => b -0x001AD948 /* 8000C408 */
# region @ 801BA024 (4 bytes)
.data 0x801BA024 # address
.data 0x00000004 # size
.data 0x4BE51064 # 801BA024 => b -0x001AEF9C /* 8000B088 */
# region @ 801C6490 (4 bytes)
.data 0x801C6490 # address
.data 0x00000004 # size
.data 0x389F02FC # 801C6490 => addi r4, r31, 0x02FC
# region @ 801CA810 (4 bytes)
.data 0x801CA810 # address
.data 0x00000004 # size
.data 0x48000010 # 801CA810 => b +0x00000010 /* 801CA820 */
# region @ 8021E9D0 (4 bytes)
.data 0x8021E9D0 # address
.data 0x00000004 # size
.data 0x4BDEDD00 # 8021E9D0 => b -0x00212300 /* 8000C6D0 */
# region @ 80221E90 (4 bytes)
.data 0x80221E90 # address
.data 0x00000004 # size
.data 0x4BDEA850 # 80221E90 => b -0x002157B0 /* 8000C6E0 */
# region @ 8022ACC4 (4 bytes)
.data 0x8022ACC4 # address
.data 0x00000004 # size
.data 0x2C000001 # 8022ACC4 => cmpwi r0, 1
# region @ 8022B4C4 (4 bytes)
.data 0x8022B4C4 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8022B4C4 => li r4, 0xFFFFFF00
# region @ 8022B4F4 (4 bytes)
.data 0x8022B4F4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8022B4F4 => li r4, 0xFFFFFE80
# region @ 8022B524 (4 bytes)
.data 0x8022B524 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8022B524 => li r4, 0xFFFFFDB0
# region @ 8022E1C0 (4 bytes)
.data 0x8022E1C0 # address
.data 0x00000004 # size
.data 0x60000000 # 8022E1C0 => nop
# region @ 8022E8F4 (4 bytes)
.data 0x8022E8F4 # address
.data 0x00000004 # size
.data 0x41810630 # 8022E8F4 => bgt +0x00000630 /* 8022EF24 */
# region @ 8022FC18 (4 bytes)
.data 0x8022FC18 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8022FC18 => li r4, 0xFFFFFF00
# region @ 8022FC48 (4 bytes)
.data 0x8022FC48 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8022FC48 => li r4, 0xFFFFFE80
# region @ 8022FC78 (4 bytes)
.data 0x8022FC78 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8022FC78 => li r4, 0xFFFFFDB0
# region @ 80230424 (4 bytes)
.data 0x80230424 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80230424 => li r4, 0xFFFFFF00
# region @ 80230454 (4 bytes)
.data 0x80230454 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80230454 => li r4, 0xFFFFFE80
# region @ 80230484 (4 bytes)
.data 0x80230484 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80230484 => li r4, 0xFFFFFDB0
# region @ 80231A28 (4 bytes)
.data 0x80231A28 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80231A28 => li r4, 0xFFFFFF00
# region @ 80231A58 (4 bytes)
.data 0x80231A58 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80231A58 => li r4, 0xFFFFFE80
# region @ 80231A88 (4 bytes)
.data 0x80231A88 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80231A88 => li r4, 0xFFFFFDB0
# region @ 80232798 (4 bytes)
.data 0x80232798 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80232798 => li r4, 0xFFFFFF00
# region @ 802327C8 (4 bytes)
.data 0x802327C8 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802327C8 => li r4, 0xFFFFFE80
# region @ 802327F8 (4 bytes)
.data 0x802327F8 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802327F8 => li r4, 0xFFFFFDB0
# region @ 8023308C (4 bytes)
.data 0x8023308C # address
.data 0x00000004 # size
.data 0x3880FF00 # 8023308C => li r4, 0xFFFFFF00
# region @ 802330C4 (4 bytes)
.data 0x802330C4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802330C4 => li r4, 0xFFFFFE80
# region @ 802330FC (4 bytes)
.data 0x802330FC # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802330FC => li r4, 0xFFFFFDB0
# region @ 80235138 (4 bytes)
.data 0x80235138 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80235138 => li r4, 0xFFFFFF00
# region @ 80235168 (4 bytes)
.data 0x80235168 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80235168 => li r4, 0xFFFFFE80
# region @ 80235198 (4 bytes)
.data 0x80235198 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80235198 => li r4, 0xFFFFFDB0
# region @ 80237764 (4 bytes)
.data 0x80237764 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80237764 => li r4, 0xFFFFFF00
# region @ 802377A0 (4 bytes)
.data 0x802377A0 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802377A0 => li r4, 0xFFFFFE80
# region @ 802377DC (4 bytes)
.data 0x802377DC # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802377DC => li r4, 0xFFFFFDB0
# region @ 80237F3C (4 bytes)
.data 0x80237F3C # address
.data 0x00000004 # size
.data 0x3880FF00 # 80237F3C => li r4, 0xFFFFFF00
# region @ 80237F6C (4 bytes)
.data 0x80237F6C # address
.data 0x00000004 # size
.data 0x3880FE80 # 80237F6C => li r4, 0xFFFFFE80
# region @ 80237F9C (4 bytes)
.data 0x80237F9C # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80237F9C => li r4, 0xFFFFFDB0
# region @ 80238950 (4 bytes)
.data 0x80238950 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80238950 => li r4, 0xFFFFFF00
# region @ 80238980 (4 bytes)
.data 0x80238980 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80238980 => li r4, 0xFFFFFE80
# region @ 802389B0 (4 bytes)
.data 0x802389B0 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802389B0 => li r4, 0xFFFFFDB0
# region @ 80239328 (4 bytes)
.data 0x80239328 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80239328 => li r4, 0xFFFFFF00
# region @ 80239358 (4 bytes)
.data 0x80239358 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80239358 => li r4, 0xFFFFFE80
# region @ 80239388 (4 bytes)
.data 0x80239388 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80239388 => li r4, 0xFFFFFDB0
# region @ 8023CC58 (4 bytes)
.data 0x8023CC58 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8023CC58 => li r4, 0xFFFFFF00
# region @ 8023CC88 (4 bytes)
.data 0x8023CC88 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8023CC88 => li r4, 0xFFFFFE80
# region @ 8023CCB8 (4 bytes)
.data 0x8023CCB8 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8023CCB8 => li r4, 0xFFFFFDB0
# region @ 80251C68 (4 bytes)
.data 0x80251C68 # address
.data 0x00000004 # size
.data 0x60000000 # 80251C68 => nop
# region @ 80269B5C (4 bytes)
.data 0x80269B5C # address
.data 0x00000004 # size
.data 0x60000000 # 80269B5C => nop
# region @ 8026F6FC (4 bytes)
.data 0x8026F6FC # address
.data 0x00000004 # size
.data 0x3884AAFA # 8026F6FC => subi r4, r4, 0x5506
# region @ 8026F810 (4 bytes)
.data 0x8026F810 # address
.data 0x00000004 # size
.data 0x3863AAFA # 8026F810 => subi r3, r3, 0x5506
# region @ 8026F898 (4 bytes)
.data 0x8026F898 # address
.data 0x00000004 # size
.data 0x3883AAFA # 8026F898 => subi r4, r3, 0x5506
# region @ 802BD474 (4 bytes)
.data 0x802BD474 # address
.data 0x00000004 # size
.data 0x4BD5050C # 802BD474 => b -0x002AFAF4 /* 8000D980 */
# region @ 802FDD28 (4 bytes)
.data 0x802FDD28 # address
.data 0x00000004 # size
.data 0x2C030001 # 802FDD28 => cmpwi r3, 1
# region @ 8030398C (28 bytes)
.data 0x8030398C # address
.data 0x0000001C # size
.data 0x48000020 # 8030398C => b +0x00000020 /* 803039AC */
.data 0x3863A830 # 80303990 => subi r3, r3, 0x57D0
.data 0x800DB9C4 # 80303994 => lwz r0, [r13 - 0x463C]
.data 0x2C000023 # 80303998 => cmpwi r0, 35
.data 0x40820008 # 8030399C => bne +0x00000008 /* 803039A4 */
.data 0x3863FB28 # 803039A0 => subi r3, r3, 0x04D8
.data 0x4800008C # 803039A4 => b +0x0000008C /* 80303A30 */
# region @ 80303A2C (4 bytes)
.data 0x80303A2C # address
.data 0x00000004 # size
.data 0x4BFFFF64 # 80303A2C => b -0x0000009C /* 80303990 */
# region @ 80337570 (4 bytes)
.data 0x80337570 # address
.data 0x00000004 # size
.data 0x4BCD6430 # 80337570 => b -0x00329BD0 /* 8000D9A0 */
# region @ 80358440 (4 bytes)
.data 0x80358440 # address
.data 0x00000004 # size
.data 0x388001E8 # 80358440 => li r4, 0x01E8
# region @ 80358464 (4 bytes)
.data 0x80358464 # address
.data 0x00000004 # size
.data 0x4BCB5D7D # 80358464 => bl -0x0034A284 /* 8000E1E0 */
# region @ 803584D4 (4 bytes)
.data 0x803584D4 # address
.data 0x00000004 # size
.data 0x388001E8 # 803584D4 => li r4, 0x01E8
# region @ 803584E4 (4 bytes)
.data 0x803584E4 # address
.data 0x00000004 # size
.data 0x4BCB5CFD # 803584E4 => bl -0x0034A304 /* 8000E1E0 */
# region @ 804B8990 (8 bytes)
.data 0x804B8990 # address
.data 0x00000008 # size
.data 0x70808080 # 804B8990 => andi. r0, r4, 0x8080
.data 0x60707070 # 804B8994 => ori r16, r3, 0x7070
# region @ 804CC1E4 (4 bytes)
.data 0x804CC1E4 # address
.data 0x00000004 # size
.data 0x0000001E # 804CC1E4 => .invalid
# region @ 804CC23C (4 bytes)
.data 0x804CC23C # address
.data 0x00000004 # size
.data 0x00000028 # 804CC23C => .invalid
# region @ 804CC268 (4 bytes)
.data 0x804CC268 # address
.data 0x00000004 # size
.data 0x00000032 # 804CC268 => .invalid
# region @ 804CC294 (4 bytes)
.data 0x804CC294 # address
.data 0x00000004 # size
.data 0x0000003C # 804CC294 => .invalid
# region @ 804CC2A4 (4 bytes)
.data 0x804CC2A4 # address
.data 0x00000004 # size
.data 0x0018003C # 804CC2A4 => .invalid
# region @ 804CC4FC (4 bytes)
.data 0x804CC4FC # address
.data 0x00000004 # size
.data 0x00000028 # 804CC4FC => .invalid
# region @ 804D0E58 (4 bytes)
.data 0x804D0E58 # address
.data 0x00000004 # size
.data 0xFF0074EE # 804D0E58 => fsel f24, f0, f14, f19
# region @ 805DAAB4 (4 bytes)
.data 0x805DAAB4 # address
.data 0x00000004 # size
.data 0x435C0000 # 805DAAB4 => bc 26, 28, +0x00000000 /* 805DAAB4 */
# region @ 805DC750 (4 bytes)
.data 0x805DC750 # address
.data 0x00000004 # size
.data 0x46AFC800 # 805DC750 => .invalid sc
# region @ 805DC9F0 (4 bytes)
.data 0x805DC9F0 # address
.data 0x00000004 # size
.data 0x43480000 # 805DC9F0 => bc 26, 8, +0x00000000 /* 805DC9F0 */
# end sentinel
.data 0x00000000 # address
.data 0x00000000 # size
@@ -1,564 +0,0 @@
.meta name="Bug fixes"
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
# Original code by Ralf @ GC-Forever and Aleron Ives
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
entry_ptr:
reloc0:
.offsetof start
start:
.include WriteCodeBlocksGC
# region @ 8000B088 (88 bytes)
.data 0x8000B088 # address
.data 0x00000058 # size
.data 0x7FA3EB78 # 8000B088 => mr r3, r29
.data 0x38800000 # 8000B08C => li r4, 0x0000
.data 0x481AE725 # 8000B090 => bl +0x001AE724 /* 801B97B4 */
.data 0x7FA3EB78 # 8000B094 => mr r3, r29
.data 0x481AE9F4 # 8000B098 => b +0x001AE9F4 /* 801B9A8C */
.data 0x881F0000 # 8000B09C => lbz r0, [r31]
.data 0x28090001 # 8000B0A0 => cmplwi r9, 1
.data 0x4082000C # 8000B0A4 => bne +0x0000000C /* 8000B0B0 */
.data 0x881F0001 # 8000B0A8 => lbz r0, [r31 + 0x0001]
.data 0x3BFF0002 # 8000B0AC => addi r31, r31, 0x0002
.data 0x481008C4 # 8000B0B0 => b +0x001008C4 /* 8010B974 */
.data 0x39200000 # 8000B0B4 => li r9, 0x0000
.data 0x48100855 # 8000B0B8 => bl +0x00100854 /* 8010B90C */
.data 0x7F43D378 # 8000B0BC => mr r3, r26
.data 0x7F64DB78 # 8000B0C0 => mr r4, r27
.data 0x7F85E378 # 8000B0C4 => mr r5, r28
.data 0x7FA6EB78 # 8000B0C8 => mr r6, r29
.data 0x7FC7F378 # 8000B0CC => mr r7, r30
.data 0x7FE8FB78 # 8000B0D0 => mr r8, r31
.data 0x39200001 # 8000B0D4 => li r9, 0x0001
.data 0x48100835 # 8000B0D8 => bl +0x00100834 /* 8010B90C */
.data 0x48102CC0 # 8000B0DC => b +0x00102CC0 /* 8010DD9C */
# region @ 8000B5C8 (20 bytes)
.data 0x8000B5C8 # address
.data 0x00000014 # size
.data 0x80630098 # 8000B5C8 => lwz r3, [r3 + 0x0098]
.data 0x483D46F5 # 8000B5CC => bl +0x003D46F4 /* 803DFCC0 */
.data 0x807F042C # 8000B5D0 => lwz r3, [r31 + 0x042C]
.data 0x809F0430 # 8000B5D4 => lwz r4, [r31 + 0x0430]
.data 0x481788C0 # 8000B5D8 => b +0x001788C0 /* 80183E98 */
# region @ 8000BBD0 (32 bytes)
.data 0x8000BBD0 # address
.data 0x00000020 # size
.data 0x809F0370 # 8000BBD0 => lwz r4, [r31 + 0x0370]
.data 0x3884FC00 # 8000BBD4 => subi r4, r4, 0x0400
.data 0x909F0370 # 8000BBD8 => stw [r31 + 0x0370], r4
.data 0x807F0014 # 8000BBDC => lwz r3, [r31 + 0x0014]
.data 0x28030000 # 8000BBE0 => cmplwi r3, 0
.data 0x41820008 # 8000BBE4 => beq +0x00000008 /* 8000BBEC */
.data 0x90830060 # 8000BBE8 => stw [r3 + 0x0060], r4
.data 0x4816506C # 8000BBEC => b +0x0016506C /* 80170C58 */
# region @ 8000C3F8 (124 bytes)
.data 0x8000C3F8 # address
.data 0x0000007C # size
.data 0x28040000 # 8000C3F8 => cmplwi r4, 0
.data 0x4D820020 # 8000C3FC => beqlr
.data 0x9421FFF0 # 8000C400 => stwu [r1 - 0x0010], r1
.data 0x481AD3B4 # 8000C404 => b +0x001AD3B4 /* 801B97B8 */
.data 0x9421FFE0 # 8000C408 => stwu [r1 - 0x0020], r1
.data 0x7C0802A6 # 8000C40C => mflr r0
.data 0x90010024 # 8000C410 => stw [r1 + 0x0024], r0
.data 0xBF410008 # 8000C414 => stmw [r1 + 0x0008], r26
.data 0x7C7F1B78 # 8000C418 => mr r31, r3
.data 0x4BFFFFDD # 8000C41C => bl -0x00000024 /* 8000C3F8 */
.data 0x3BC00000 # 8000C420 => li r30, 0x0000
.data 0x3BBF0D04 # 8000C424 => addi r29, r31, 0x0D04
.data 0x837F032C # 8000C428 => lwz r27, [r31 + 0x032C]
.data 0x839D0000 # 8000C42C => lwz r28, [r29]
.data 0x7F83E379 # 8000C430 => mr. r3, r28
.data 0x41820018 # 8000C434 => beq +0x00000018 /* 8000C44C */
.data 0x38800001 # 8000C438 => li r4, 0x0001
.data 0x480FEADD # 8000C43C => bl +0x000FEADC /* 8010AF18 */
.data 0x7F83E378 # 8000C440 => mr r3, r28
.data 0x38800001 # 8000C444 => li r4, 0x0001
.data 0x480FEC4D # 8000C448 => bl +0x000FEC4C /* 8010B094 */
.data 0x3BBD0004 # 8000C44C => addi r29, r29, 0x0004
.data 0x3BDE0001 # 8000C450 => addi r30, r30, 0x0001
.data 0x2C1E000D # 8000C454 => cmpwi r30, 13
.data 0x4180FFD4 # 8000C458 => blt -0x0000002C /* 8000C42C */
.data 0x937F032C # 8000C45C => stw [r31 + 0x032C], r27
.data 0xBB410008 # 8000C460 => lmw r26, [r1 + 0x0008]
.data 0x80010024 # 8000C464 => lwz r0, [r1 + 0x0024]
.data 0x7C0803A6 # 8000C468 => mtlr r0
.data 0x38210020 # 8000C46C => addi r1, r1, 0x0020
.data 0x4E800020 # 8000C470 => blr
# region @ 8000C640 (20 bytes)
.data 0x8000C640 # address
.data 0x00000014 # size
.data 0x54800673 # 8000C640 => rlwinm. r0, r4, 0, 25, 25
.data 0x41820008 # 8000C644 => beq +0x00000008 /* 8000C64C */
.data 0x38800000 # 8000C648 => li r4, 0x0000
.data 0x38040009 # 8000C64C => addi r0, r4, 0x0009
.data 0x4810C694 # 8000C650 => b +0x0010C694 /* 80118CE4 */
# region @ 8000C6D0 (32 bytes)
.data 0x8000C6D0 # address
.data 0x00000020 # size
.data 0x38000001 # 8000C6D0 => li r0, 0x0001
.data 0x901D0054 # 8000C6D4 => stw [r29 + 0x0054], r0
.data 0x807D0024 # 8000C6D8 => lwz r3, [r29 + 0x0024]
.data 0x482109C0 # 8000C6DC => b +0x002109C0 /* 8021D09C */
.data 0x38000001 # 8000C6E0 => li r0, 0x0001
.data 0x901F0378 # 8000C6E4 => stw [r31 + 0x0378], r0
.data 0x807F0024 # 8000C6E8 => lwz r3, [r31 + 0x0024]
.data 0x48165AA0 # 8000C6EC => b +0x00165AA0 /* 8017218C */
# region @ 8000C8A0 (20 bytes)
.data 0x8000C8A0 # address
.data 0x00000014 # size
.data 0x1C00000A # 8000C8A0 => mulli r0, r0, 10
.data 0x57E407BD # 8000C8A4 => rlwinm. r4, r31, 0, 30, 30
.data 0x41820008 # 8000C8A8 => beq +0x00000008 /* 8000C8B0 */
.data 0x7FA00734 # 8000C8AC => extsh r0, r29
.data 0x48105DB8 # 8000C8B0 => b +0x00105DB8 /* 80112668 */
# region @ 8000C8C0 (16 bytes)
.data 0x8000C8C0 # address
.data 0x00000010 # size
.data 0x7000000F # 8000C8C0 => andi. r0, r0, 0x000F
.data 0x7000004F # 8000C8C4 => andi. r0, r0, 0x004F
.data 0x2C000004 # 8000C8C8 => cmpwi r0, 4
.data 0x4E800020 # 8000C8CC => blr
# region @ 8000D980 (20 bytes)
.data 0x8000D980 # address
.data 0x00000014 # size
.data 0x807C0000 # 8000D980 => lwz r3, [r28]
.data 0x2C030013 # 8000D984 => cmpwi r3, 19
.data 0x40820008 # 8000D988 => bne +0x00000008 /* 8000D990 */
.data 0x38600002 # 8000D98C => li r3, 0x0002
.data 0x482ADB24 # 8000D990 => b +0x002ADB24 /* 802BB4B4 */
# region @ 8000D9A0 (24 bytes)
.data 0x8000D9A0 # address
.data 0x00000018 # size
.data 0xC042FC78 # 8000D9A0 => lfs f2, [r2 - 0x0388]
.data 0x807E0030 # 8000D9A4 => lwz r3, [r30 + 0x0030]
.data 0x70630020 # 8000D9A8 => andi. r3, r3, 0x0020
.data 0x41820008 # 8000D9AC => beq +0x00000008 /* 8000D9B4 */
.data 0xC042FC90 # 8000D9B0 => lfs f2, [r2 - 0x0370]
.data 0x483276B0 # 8000D9B4 => b +0x003276B0 /* 80335064 */
# region @ 8000E1E0 (28 bytes)
.data 0x8000E1E0 # address
.data 0x0000001C # size
.data 0x7FC802A6 # 8000E1E0 => mflr r30
.data 0x38A00000 # 8000E1E4 => li r5, 0x0000
.data 0x38C0001E # 8000E1E8 => li r6, 0x001E
.data 0x38E00040 # 8000E1EC => li r7, 0x0040
.data 0x480782B1 # 8000E1F0 => bl +0x000782B0 /* 800864A0 */
.data 0x7FC803A6 # 8000E1F4 => mtlr r30
.data 0x4E800020 # 8000E1F8 => blr
# region @ 8001306C (4 bytes)
.data 0x8001306C # address
.data 0x00000004 # size
.data 0x4BFFFCC0 # 8001306C => b -0x00000340 /* 80012D2C */
# region @ 800142DC (4 bytes)
.data 0x800142DC # address
.data 0x00000004 # size
.data 0x4BFF85E5 # 800142DC => bl -0x00007A1C /* 8000C8C0 */
# region @ 80015D04 (4 bytes)
.data 0x80015D04 # address
.data 0x00000004 # size
.data 0x4BFF6BC1 # 80015D04 => bl -0x00009440 /* 8000C8C4 */
# region @ 80091528 (8 bytes)
.data 0x80091528 # address
.data 0x00000008 # size
.data 0x4800024D # 80091528 => bl +0x0000024C /* 80091774 */
.data 0xB3C3032C # 8009152C => sth [r3 + 0x032C], r30
# region @ 800BC750 (4 bytes)
.data 0x800BC750 # address
.data 0x00000004 # size
.data 0x48000010 # 800BC750 => b +0x00000010 /* 800BC760 */
# region @ 80101C14 (4 bytes)
.data 0x80101C14 # address
.data 0x00000004 # size
.data 0x60000000 # 80101C14 => nop
# region @ 80104B48 (4 bytes)
.data 0x80104B48 # address
.data 0x00000004 # size
.data 0x4182000C # 80104B48 => beq +0x0000000C /* 80104B54 */
# region @ 80107478 (4 bytes)
.data 0x80107478 # address
.data 0x00000004 # size
.data 0x4800000C # 80107478 => b +0x0000000C /* 80107484 */
# region @ 8010748C (4 bytes)
.data 0x8010748C # address
.data 0x00000004 # size
.data 0x7C030378 # 8010748C => mr r3, r0
# region @ 8010B970 (4 bytes)
.data 0x8010B970 # address
.data 0x00000004 # size
.data 0x4BEFF72C # 8010B970 => b -0x001008D4 /* 8000B09C */
# region @ 8010DD98 (4 bytes)
.data 0x8010DD98 # address
.data 0x00000004 # size
.data 0x4BEFD31C # 8010DD98 => b -0x00102CE4 /* 8000B0B4 */
# region @ 80112664 (4 bytes)
.data 0x80112664 # address
.data 0x00000004 # size
.data 0x4BEFA23C # 80112664 => b -0x00105DC4 /* 8000C8A0 */
# region @ 80114378 (4 bytes)
.data 0x80114378 # address
.data 0x00000004 # size
.data 0x38000012 # 80114378 => li r0, 0x0012
# region @ 801185B0 (4 bytes)
.data 0x801185B0 # address
.data 0x00000004 # size
.data 0x88040016 # 801185B0 => lbz r0, [r4 + 0x0016]
# region @ 801185BC (4 bytes)
.data 0x801185BC # address
.data 0x00000004 # size
.data 0x88040017 # 801185BC => lbz r0, [r4 + 0x0017]
# region @ 80118CE0 (4 bytes)
.data 0x80118CE0 # address
.data 0x00000004 # size
.data 0x4BEF3960 # 80118CE0 => b -0x0010C6A0 /* 8000C640 */
# region @ 8011CA90 (12 bytes)
.data 0x8011CA90 # address
.data 0x0000000C # size
.data 0x7C030378 # 8011CA90 => mr r3, r0
.data 0x3863FFFF # 8011CA94 => subi r3, r3, 0x0001
.data 0x4BFFFFE8 # 8011CA98 => b -0x00000018 /* 8011CA80 */
# region @ 8011CB4C (12 bytes)
.data 0x8011CB4C # address
.data 0x0000000C # size
.data 0x7C030378 # 8011CB4C => mr r3, r0
.data 0x3863FFFF # 8011CB50 => subi r3, r3, 0x0001
.data 0x4BFFFFE8 # 8011CB54 => b -0x00000018 /* 8011CB3C */
# region @ 8011CB9C (12 bytes)
.data 0x8011CB9C # address
.data 0x0000000C # size
.data 0x7C040378 # 8011CB9C => mr r4, r0
.data 0x3884FFFF # 8011CBA0 => subi r4, r4, 0x0001
.data 0x4BFFFFE8 # 8011CBA4 => b -0x00000018 /* 8011CB8C */
# region @ 80166324 (8 bytes)
.data 0x80166324 # address
.data 0x00000008 # size
.data 0x3C604005 # 80166324 => lis r3, 0x4005
.data 0x4800009C # 80166328 => b +0x0000009C /* 801663C4 */
# region @ 801663C0 (4 bytes)
.data 0x801663C0 # address
.data 0x00000004 # size
.data 0x4800001C # 801663C0 => b +0x0000001C /* 801663DC */
# region @ 80170C54 (4 bytes)
.data 0x80170C54 # address
.data 0x00000004 # size
.data 0x4BE9AF7C # 80170C54 => b -0x00165084 /* 8000BBD0 */
# region @ 80170C74 (4 bytes)
.data 0x80170C74 # address
.data 0x00000004 # size
.data 0x60800420 # 80170C74 => ori r0, r4, 0x0420
# region @ 80172188 (4 bytes)
.data 0x80172188 # address
.data 0x00000004 # size
.data 0x4BE9A558 # 80172188 => b -0x00165AA8 /* 8000C6E0 */
# region @ 80183E94 (4 bytes)
.data 0x80183E94 # address
.data 0x00000004 # size
.data 0x4BE87734 # 80183E94 => b -0x001788CC /* 8000B5C8 */
# region @ 80183ED4 (4 bytes)
.data 0x80183ED4 # address
.data 0x00000004 # size
.data 0x60000000 # 80183ED4 => nop
# region @ 80189A54 (4 bytes)
.data 0x80189A54 # address
.data 0x00000004 # size
.data 0x60000000 # 80189A54 => nop
# region @ 801933DC (4 bytes)
.data 0x801933DC # address
.data 0x00000004 # size
.data 0x60000000 # 801933DC => nop
# region @ 801B97B4 (4 bytes)
.data 0x801B97B4 # address
.data 0x00000004 # size
.data 0x4BE52C54 # 801B97B4 => b -0x001AD3AC /* 8000C408 */
# region @ 801B9A88 (4 bytes)
.data 0x801B9A88 # address
.data 0x00000004 # size
.data 0x4BE51600 # 801B9A88 => b -0x001AEA00 /* 8000B088 */
# region @ 801C5EA4 (4 bytes)
.data 0x801C5EA4 # address
.data 0x00000004 # size
.data 0x389F02FC # 801C5EA4 => addi r4, r31, 0x02FC
# region @ 801CA1F4 (4 bytes)
.data 0x801CA1F4 # address
.data 0x00000004 # size
.data 0x48000010 # 801CA1F4 => b +0x00000010 /* 801CA204 */
# region @ 8021D098 (4 bytes)
.data 0x8021D098 # address
.data 0x00000004 # size
.data 0x4BDEF638 # 8021D098 => b -0x002109C8 /* 8000C6D0 */
# region @ 80229354 (4 bytes)
.data 0x80229354 # address
.data 0x00000004 # size
.data 0x2C000001 # 80229354 => cmpwi r0, 1
# region @ 80229B54 (4 bytes)
.data 0x80229B54 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80229B54 => li r4, 0xFFFFFF00
# region @ 80229B84 (4 bytes)
.data 0x80229B84 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80229B84 => li r4, 0xFFFFFE80
# region @ 80229BB4 (4 bytes)
.data 0x80229BB4 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80229BB4 => li r4, 0xFFFFFDB0
# region @ 8022C850 (4 bytes)
.data 0x8022C850 # address
.data 0x00000004 # size
.data 0x60000000 # 8022C850 => nop
# region @ 8022CF84 (4 bytes)
.data 0x8022CF84 # address
.data 0x00000004 # size
.data 0x41810630 # 8022CF84 => bgt +0x00000630 /* 8022D5B4 */
# region @ 8022D278 (4 bytes)
.data 0x8022D278 # address
.data 0x00000004 # size
.data 0x4181033C # 8022D278 => bgt +0x0000033C /* 8022D5B4 */
# region @ 8022D36C (4 bytes)
.data 0x8022D36C # address
.data 0x00000004 # size
.data 0x41810248 # 8022D36C => bgt +0x00000248 /* 8022D5B4 */
# region @ 8022E2A8 (4 bytes)
.data 0x8022E2A8 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8022E2A8 => li r4, 0xFFFFFF00
# region @ 8022E2D8 (4 bytes)
.data 0x8022E2D8 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8022E2D8 => li r4, 0xFFFFFE80
# region @ 8022E308 (4 bytes)
.data 0x8022E308 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8022E308 => li r4, 0xFFFFFDB0
# region @ 8022EAB4 (4 bytes)
.data 0x8022EAB4 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8022EAB4 => li r4, 0xFFFFFF00
# region @ 8022EAE4 (4 bytes)
.data 0x8022EAE4 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8022EAE4 => li r4, 0xFFFFFE80
# region @ 8022EB14 (4 bytes)
.data 0x8022EB14 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8022EB14 => li r4, 0xFFFFFDB0
# region @ 802300B8 (4 bytes)
.data 0x802300B8 # address
.data 0x00000004 # size
.data 0x3880FF00 # 802300B8 => li r4, 0xFFFFFF00
# region @ 802300E8 (4 bytes)
.data 0x802300E8 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802300E8 => li r4, 0xFFFFFE80
# region @ 80230118 (4 bytes)
.data 0x80230118 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80230118 => li r4, 0xFFFFFDB0
# region @ 80230E08 (4 bytes)
.data 0x80230E08 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80230E08 => li r4, 0xFFFFFF00
# region @ 80230E38 (4 bytes)
.data 0x80230E38 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80230E38 => li r4, 0xFFFFFE80
# region @ 80230E68 (4 bytes)
.data 0x80230E68 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80230E68 => li r4, 0xFFFFFDB0
# region @ 802316FC (4 bytes)
.data 0x802316FC # address
.data 0x00000004 # size
.data 0x3880FF00 # 802316FC => li r4, 0xFFFFFF00
# region @ 80231734 (4 bytes)
.data 0x80231734 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80231734 => li r4, 0xFFFFFE80
# region @ 8023176C (4 bytes)
.data 0x8023176C # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8023176C => li r4, 0xFFFFFDB0
# region @ 802337A8 (4 bytes)
.data 0x802337A8 # address
.data 0x00000004 # size
.data 0x3880FF00 # 802337A8 => li r4, 0xFFFFFF00
# region @ 802337D8 (4 bytes)
.data 0x802337D8 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802337D8 => li r4, 0xFFFFFE80
# region @ 80233808 (4 bytes)
.data 0x80233808 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80233808 => li r4, 0xFFFFFDB0
# region @ 80235DD4 (4 bytes)
.data 0x80235DD4 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80235DD4 => li r4, 0xFFFFFF00
# region @ 80235E10 (4 bytes)
.data 0x80235E10 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80235E10 => li r4, 0xFFFFFE80
# region @ 80235E4C (4 bytes)
.data 0x80235E4C # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80235E4C => li r4, 0xFFFFFDB0
# region @ 802365AC (4 bytes)
.data 0x802365AC # address
.data 0x00000004 # size
.data 0x3880FF00 # 802365AC => li r4, 0xFFFFFF00
# region @ 802365DC (4 bytes)
.data 0x802365DC # address
.data 0x00000004 # size
.data 0x3880FE80 # 802365DC => li r4, 0xFFFFFE80
# region @ 8023660C (4 bytes)
.data 0x8023660C # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8023660C => li r4, 0xFFFFFDB0
# region @ 80236FC0 (4 bytes)
.data 0x80236FC0 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80236FC0 => li r4, 0xFFFFFF00
# region @ 80236FF0 (4 bytes)
.data 0x80236FF0 # address
.data 0x00000004 # size
.data 0x3880FE80 # 80236FF0 => li r4, 0xFFFFFE80
# region @ 80237020 (4 bytes)
.data 0x80237020 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 80237020 => li r4, 0xFFFFFDB0
# region @ 80237998 (4 bytes)
.data 0x80237998 # address
.data 0x00000004 # size
.data 0x3880FF00 # 80237998 => li r4, 0xFFFFFF00
# region @ 802379C8 (4 bytes)
.data 0x802379C8 # address
.data 0x00000004 # size
.data 0x3880FE80 # 802379C8 => li r4, 0xFFFFFE80
# region @ 802379F8 (4 bytes)
.data 0x802379F8 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 802379F8 => li r4, 0xFFFFFDB0
# region @ 8023B2C8 (4 bytes)
.data 0x8023B2C8 # address
.data 0x00000004 # size
.data 0x3880FF00 # 8023B2C8 => li r4, 0xFFFFFF00
# region @ 8023B2F8 (4 bytes)
.data 0x8023B2F8 # address
.data 0x00000004 # size
.data 0x3880FE80 # 8023B2F8 => li r4, 0xFFFFFE80
# region @ 8023B328 (4 bytes)
.data 0x8023B328 # address
.data 0x00000004 # size
.data 0x3880FDB0 # 8023B328 => li r4, 0xFFFFFDB0
# region @ 80250264 (4 bytes)
.data 0x80250264 # address
.data 0x00000004 # size
.data 0x60000000 # 80250264 => nop
# region @ 80267DDC (4 bytes)
.data 0x80267DDC # address
.data 0x00000004 # size
.data 0x60000000 # 80267DDC => nop
# region @ 8026DA74 (4 bytes)
.data 0x8026DA74 # address
.data 0x00000004 # size
.data 0x3884AAFA # 8026DA74 => subi r4, r4, 0x5506
# region @ 8026DB88 (4 bytes)
.data 0x8026DB88 # address
.data 0x00000004 # size
.data 0x3863AAFA # 8026DB88 => subi r3, r3, 0x5506
# region @ 8026DC10 (4 bytes)
.data 0x8026DC10 # address
.data 0x00000004 # size
.data 0x3883AAFA # 8026DC10 => subi r4, r3, 0x5506
# region @ 802BB4B0 (4 bytes)
.data 0x802BB4B0 # address
.data 0x00000004 # size
.data 0x4BD524D0 # 802BB4B0 => b -0x002ADB30 /* 8000D980 */
# region @ 802FB99C (4 bytes)
.data 0x802FB99C # address
.data 0x00000004 # size
.data 0x2C030001 # 802FB99C => cmpwi r3, 1
# region @ 80301600 (28 bytes)
.data 0x80301600 # address
.data 0x0000001C # size
.data 0x48000020 # 80301600 => b +0x00000020 /* 80301620 */
.data 0x3863A830 # 80301604 => subi r3, r3, 0x57D0
.data 0x800DB98C # 80301608 => lwz r0, [r13 - 0x4674]
.data 0x2C000023 # 8030160C => cmpwi r0, 35
.data 0x40820008 # 80301610 => bne +0x00000008 /* 80301618 */
.data 0x3863FB28 # 80301614 => subi r3, r3, 0x04D8
.data 0x4800008C # 80301618 => b +0x0000008C /* 803016A4 */
# region @ 803016A0 (4 bytes)
.data 0x803016A0 # address
.data 0x00000004 # size
.data 0x4BFFFF64 # 803016A0 => b -0x0000009C /* 80301604 */
# region @ 80335060 (4 bytes)
.data 0x80335060 # address
.data 0x00000004 # size
.data 0x4BCD8940 # 80335060 => b -0x003276C0 /* 8000D9A0 */
# region @ 80355960 (4 bytes)
.data 0x80355960 # address
.data 0x00000004 # size
.data 0x388001E8 # 80355960 => li r4, 0x01E8
# region @ 80355984 (4 bytes)
.data 0x80355984 # address
.data 0x00000004 # size
.data 0x4BCB885D # 80355984 => bl -0x003477A4 /* 8000E1E0 */
# region @ 803559F4 (4 bytes)
.data 0x803559F4 # address
.data 0x00000004 # size
.data 0x388001E8 # 803559F4 => li r4, 0x01E8
# region @ 80355A04 (4 bytes)
.data 0x80355A04 # address
.data 0x00000004 # size
.data 0x4BCB87DD # 80355A04 => bl -0x00347824 /* 8000E1E0 */
# region @ 804B3738 (8 bytes)
.data 0x804B3738 # address
.data 0x00000008 # size
.data 0x70808080 # 804B3738 => andi. r0, r4, 0x8080
.data 0x60707070 # 804B373C => ori r16, r3, 0x7070
# region @ 804C6EE4 (4 bytes)
.data 0x804C6EE4 # address
.data 0x00000004 # size
.data 0x0000001E # 804C6EE4 => .invalid
# region @ 804C6F3C (4 bytes)
.data 0x804C6F3C # address
.data 0x00000004 # size
.data 0x00000028 # 804C6F3C => .invalid
# region @ 804C6F68 (4 bytes)
.data 0x804C6F68 # address
.data 0x00000004 # size
.data 0x00000032 # 804C6F68 => .invalid
# region @ 804C6F94 (4 bytes)
.data 0x804C6F94 # address
.data 0x00000004 # size
.data 0x0000003C # 804C6F94 => .invalid
# region @ 804C6FA4 (4 bytes)
.data 0x804C6FA4 # address
.data 0x00000004 # size
.data 0x0018003C # 804C6FA4 => .invalid
# region @ 804C71FC (4 bytes)
.data 0x804C71FC # address
.data 0x00000004 # size
.data 0x00000028 # 804C71FC => .invalid
# region @ 804CBB40 (4 bytes)
.data 0x804CBB40 # address
.data 0x00000004 # size
.data 0xFF0074EE # 804CBB40 => fsel f24, f0, f14, f19
# region @ 805C996C (4 bytes)
.data 0x805C996C # address
.data 0x00000004 # size
.data 0x435C0000 # 805C996C => bc 26, 28, +0x00000000 /* 805C996C */
# region @ 805CB608 (4 bytes)
.data 0x805CB608 # address
.data 0x00000004 # size
.data 0x46AFC800 # 805CB608 => .invalid sc
# region @ 805CB8A8 (4 bytes)
.data 0x805CB8A8 # address
.data 0x00000004 # size
.data 0x43480000 # 805CB8A8 => bc 26, 8, +0x00000000 /* 805CB8A8 */
# end sentinel
.data 0x00000000 # address
.data 0x00000000 # size

Some files were not shown because too many files have changed in this diff Show More