Compare commits
249 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d015406fa6 | |||
| eea9eaf672 | |||
| c79e5017ad | |||
| c3d56f630e | |||
| b1f419e337 | |||
| 068ef68dd6 | |||
| 51d74b092a | |||
| 884a5ce75a | |||
| d0c3e1b7d8 | |||
| 0fe28c021a | |||
| e6e599d760 | |||
| 753c8da4bb | |||
| 8165f240dc | |||
| f98fae470b | |||
| fcc274ce3e | |||
| 47533e1a5f | |||
| 20f5a92d81 | |||
| dcea0e4520 | |||
| 729d9af4b0 | |||
| 01afe12487 | |||
| c57dc64950 | |||
| 07996444a1 | |||
| ba53f67097 | |||
| 7fad72ef9c | |||
| 964f646654 | |||
| 7a23b37c0c | |||
| bfd5c246de | |||
| b89f18ce4e | |||
| 97cf9c5093 | |||
| c6e930b994 | |||
| 611193610b | |||
| 4c735d055e | |||
| adb79e8a41 | |||
| 0f4e4fa48e | |||
| 5bf868e2aa | |||
| f6f5c358eb | |||
| 50f3ebca5e | |||
| ef89699d59 | |||
| b6817e278a | |||
| 4830f5a41e | |||
| 340fbb8ca5 | |||
| 7aa05f39e2 | |||
| 5e2cc6f07f | |||
| 34f05e5162 | |||
| d75891e78b | |||
| 9bf1114535 | |||
| 9084910235 | |||
| 33407f88d7 | |||
| 82854604b8 | |||
| 6ac2ceca45 | |||
| 082f88d242 | |||
| 0fff4ebd4e | |||
| 36a370078c | |||
| 1788aebd00 | |||
| 0de3d2737f | |||
| fc6b0992e9 | |||
| 111d45220e | |||
| fed1044813 | |||
| 3b9c887dbe | |||
| 80a57f9d3e | |||
| db3cecdd2b | |||
| e13b5950ca | |||
| fe1d5a874a | |||
| ea76a537fd | |||
| be0569d2cb | |||
| c5e8d2c77c | |||
| 408bc1befc | |||
| 86e98fbfe5 | |||
| c85b3c144e | |||
| 9311483932 | |||
| c15e154846 | |||
| 02e8f8ea8b | |||
| 31ddde6e80 | |||
| 4a23d86f56 | |||
| 1453cd4c9c | |||
| be8130b621 | |||
| 9e8f7a6c6b | |||
| d052163a9e | |||
| f188ea1554 | |||
| a9894e2d05 | |||
| 0a60a24783 | |||
| d8f8dfc53f | |||
| cc8dd77d51 | |||
| d5d85bf5d9 | |||
| 2dff814e8f | |||
| ad86acd8ef | |||
| 68be13dd62 | |||
| 9e0dfc7749 | |||
| 3747025a11 | |||
| e5d4ae1f80 | |||
| 07ea97a6ea | |||
| 9a5d8f9d1a | |||
| ad2312efee | |||
| dfe1944d2b | |||
| 695404165b | |||
| d3bc2dad4f | |||
| 194e408863 | |||
| b2350a537d | |||
| ba4681e35d | |||
| 3b9684d8ac | |||
| d32c5f1d61 | |||
| cf2c8f0699 | |||
| c8681bcf05 | |||
| fe256cff2a | |||
| 1df03c45f7 | |||
| 458e2ef0cd | |||
| dd4284ab63 | |||
| 251cc80233 | |||
| c6baed2d23 | |||
| 90e2889204 | |||
| ea4f6da48e | |||
| b69cf96aa9 | |||
| cbf4540602 | |||
| 058d1ede54 | |||
| d3c2a0bad0 | |||
| 83f5487e7b | |||
| d3d89f0168 | |||
| b7257a793f | |||
| e50d7a4e65 | |||
| 4be431471c | |||
| 649a7c9871 | |||
| 7fc3cca11b | |||
| c9d7fe1c2a | |||
| 612b5d28ba | |||
| 70207896e3 | |||
| 08437844e4 | |||
| e13b220be9 | |||
| fccc0f7346 | |||
| 1449bf090b | |||
| c9902e386f | |||
| fb7d70c943 | |||
| e066c383a0 | |||
| 0e9f66f72e | |||
| ec99dad874 | |||
| b85fd4fced | |||
| 2050173666 | |||
| df29a60a6e | |||
| 78e407a70f | |||
| 04e2f94e2b | |||
| 4124f2714a | |||
| e21365db78 | |||
| dae7946526 | |||
| 6a37a2de3d | |||
| 4f650bebf0 | |||
| eb5827e059 | |||
| 6917f40d3e | |||
| efe2515a44 | |||
| c6ce39623e | |||
| 962ee6874e | |||
| 2fda85c750 | |||
| f1e00ccf0e | |||
| 09b7885013 | |||
| e126015b5f | |||
| 4ff4c86047 | |||
| cd4a8050d7 | |||
| c09bd56e19 | |||
| 6945a55584 | |||
| 32c79a7b6a | |||
| 57f47f147a | |||
| 6a65940720 | |||
| 40dcbb77ad | |||
| f479f586cb | |||
| a24d0ad703 | |||
| ac39db2f36 | |||
| 9b4da7e3b3 | |||
| 1f1f4bd815 | |||
| 00258d4607 | |||
| 3aaaf0353e | |||
| f54d7b0476 | |||
| 111260cdf3 | |||
| 91c8cba0d2 | |||
| 0f8dcd3713 | |||
| e89802f288 | |||
| c1ac34c1f7 | |||
| c74a931986 | |||
| 686bae25f3 | |||
| ff5d0af7ad | |||
| 8518349cce | |||
| 818204a93f | |||
| eea12d8d75 | |||
| 43ee4a9c5a | |||
| 7ee7af0b0f | |||
| d15f1cc1a3 | |||
| 4f2432cbac | |||
| 60f6b609da | |||
| 1058998550 | |||
| c00b554b56 | |||
| 0bd3bb7b77 | |||
| b6cfb5b2a2 | |||
| c1bcd45ea1 | |||
| 5ba652aa38 | |||
| 1ba50e96ca | |||
| 7b7c9d371f | |||
| 09ac8921fe | |||
| 29a4347f2b | |||
| 68cf06c6d0 | |||
| 5307051e04 | |||
| 045ff9b169 | |||
| c1122e1f90 | |||
| d478e9b0be | |||
| 2aa699b5b0 | |||
| c96cfad4d2 | |||
| bf26e437ff | |||
| 9efdf88101 | |||
| 4273ae84f4 | |||
| b49408a88b | |||
| 764fbf8841 | |||
| f74b416c19 | |||
| 8104fd0853 | |||
| 910555f299 | |||
| 2dd7601dbd | |||
| d7e390e494 | |||
| c8b001411e | |||
| a5265874a2 | |||
| 81eaa893b9 | |||
| a0e84b5d5c | |||
| e8891adf8e | |||
| 1a2d5c1772 | |||
| 65b9048ab6 | |||
| ccd1b56cae | |||
| 5382e12b8d | |||
| 2cdebd5f20 | |||
| 61e5460bc1 | |||
| c100d76a5b | |||
| d59b59cd51 | |||
| c7059874d3 | |||
| dfc451e86a | |||
| d1022e9b53 | |||
| 7c9309f6c5 | |||
| 441457a873 | |||
| 9255037f50 | |||
| 4c95adcdb3 | |||
| 2ef6acaa0e | |||
| a8061efc0d | |||
| 206552ed63 | |||
| 9e48259414 | |||
| ad32c0a986 | |||
| 84ed80365c | |||
| 87440437fb | |||
| 2aca408a9e | |||
| 3991d7b534 | |||
| 3823fc94f1 | |||
| 190e89181e | |||
| 20ca2529ac | |||
| 143da7e5a5 | |||
| 37b95f35c2 | |||
| aed2c61706 | |||
| 0955d1e5fd | |||
| e51924bf49 |
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report incorrect behavior or unexpected errors
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
Write a clear and concise description of what the bug is, and what you expected to happen instead.
|
||||
|
||||
**To reproduce**
|
||||
Fill in steps to reproduce the behavior, such as:
|
||||
1. Connect to server
|
||||
2. Create a game
|
||||
3. Start quest X
|
||||
4. Talk to NPC Y
|
||||
|
||||
**Game version(s) (choose one or more of the following):**
|
||||
DC NTE, DC prototype, DC v1, DC v2, PC, GC Ep1&2, GC Ep3, Xbox, BB
|
||||
|
||||
**Server log output**
|
||||
On macOS/Linux, or in a Cygwin shell on Windows, you can run the server as `./newserv 2>&1 | tee server-log.txt` to generate a log file. Do that, then do whatever you need to do to get the bug to happen, then upload the log file here.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
Write a clear and concise description of what the problem is. For example, "I'm always frustrated when [...]"
|
||||
|
||||
**Describe the solution you'd like**
|
||||
Write a clear and concise description of what you want to happen.
|
||||
|
||||
**Game version(s) (choose one or more of the following):**
|
||||
DC NTE, DC prototype, DC v1, DC v2, PC, GC Ep1&2, GC Ep3, Xbox, BB
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -2,6 +2,7 @@
|
||||
.DS_Store
|
||||
|
||||
# Build products
|
||||
src/Revision.cc
|
||||
newserv
|
||||
|
||||
# CMake files
|
||||
@@ -26,6 +27,7 @@ system/players/*.psocard
|
||||
system/players/*.nsc
|
||||
system/players/*.nsa
|
||||
system/teams/*.json
|
||||
system/teams/*.bmp
|
||||
system/patch-pc/.metadata-cache.json
|
||||
system/patch-bb/.metadata-cache.json
|
||||
|
||||
|
||||
+19
-2
@@ -38,9 +38,24 @@ find_package(resource_file QUIET)
|
||||
|
||||
|
||||
|
||||
# Git metadata
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/src/Revision.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/__Revision__.cc
|
||||
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/src/Revision-generate.sh ${CMAKE_CURRENT_SOURCE_DIR}/src/Revision.cc
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
add_custom_target(
|
||||
newserv-Revision-cc
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/Revision.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/__Revision__.cc
|
||||
)
|
||||
|
||||
|
||||
|
||||
# Executable definition
|
||||
|
||||
set(SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Revision.cc
|
||||
src/AFSArchive.cc
|
||||
src/BattleParamsIndex.cc
|
||||
src/BMLArchive.cc
|
||||
@@ -100,6 +115,7 @@ set(SOURCES
|
||||
src/ReceiveCommands.cc
|
||||
src/ReceiveSubcommands.cc
|
||||
src/ReplaySession.cc
|
||||
src/Revision.cc
|
||||
src/SaveFileFormats.cc
|
||||
src/SendCommands.cc
|
||||
src/Server.cc
|
||||
@@ -107,10 +123,10 @@ set(SOURCES
|
||||
src/ServerState.cc
|
||||
src/Shell.cc
|
||||
src/StaticGameData.cc
|
||||
src/StepGraph.cc
|
||||
src/TeamIndex.cc
|
||||
src/Text.cc
|
||||
src/TextArchive.cc
|
||||
src/UnicodeTextSet.cc
|
||||
src/TextIndex.cc
|
||||
src/Version.cc
|
||||
src/WordSelectTable.cc
|
||||
)
|
||||
@@ -122,6 +138,7 @@ endif()
|
||||
add_executable(newserv ${SOURCES})
|
||||
target_include_directories(newserv PUBLIC ${LIBEVENT_INCLUDE_DIR} ${Iconv_INCLUDE_DIRS})
|
||||
target_link_libraries(newserv phosg ${LIBEVENT_LIBRARIES} ${Iconv_LIBRARIES} pthread)
|
||||
add_dependencies(newserv newserv-Revision-cc)
|
||||
|
||||
if(resource_file_FOUND)
|
||||
target_compile_definitions(newserv PUBLIC HAVE_RESOURCE_FILE)
|
||||
|
||||
@@ -11,27 +11,20 @@ See TODO.md for a list of known issues and future work I've curated, or go to th
|
||||
**Table of contents**
|
||||
* [Compatibility](#compatibility)
|
||||
* Setup
|
||||
* [Configuration](#configuration)
|
||||
* [Server setup](#server-setup)
|
||||
* [Client patch directories for PC and BB](#client-patch-directories)
|
||||
* [How to connect](#how-to-connect)
|
||||
* Features and configuration
|
||||
* [Installing quests](#installing-quests)
|
||||
* [Item tables and drop modes](#item-tables-and-drop-modes)
|
||||
* [Cross-version play](#cross-version-play)
|
||||
* [Episode 3 features](#episode-3-features)
|
||||
* [Client patch directories for PC and BB](#client-patch-directories)
|
||||
* [Memory patches and DOL files for GC](#memory-patches-and-dol-files)
|
||||
* [Memory patches, client functions, and DOL files](#memory-patches-client-functions-and-dol-files)
|
||||
* [Using newserv as a proxy](#using-newserv-as-a-proxy)
|
||||
* [Chat commands](#chat-commands)
|
||||
* How to connect
|
||||
* Connecting local clients
|
||||
* [PSO DC](#pso-dc)
|
||||
* [PSO DC on Flycast](#pso-dc-on-flycast)
|
||||
* [PSO PC](#pso-pc)
|
||||
* [PSO GC on a real GameCube](#pso-gc-on-a-real-gamecube)
|
||||
* [PSO GC on Dolphin](#pso-gc-on-dolphin)
|
||||
* [PSO BB](#pso-bb)
|
||||
* [Connecting external clients](#connecting-external-clients)
|
||||
* [Non-server features](#non-server-features)
|
||||
|
||||
## Compatibility
|
||||
# Compatibility
|
||||
|
||||
newserv supports several versions of PSO, including various development prototypes. Specifically:
|
||||
| Version | Lobbies | Games | Proxy |
|
||||
@@ -48,44 +41,137 @@ newserv supports several versions of PSO, including various development prototyp
|
||||
| GC Ep1&2 NTE | Yes | Yes | Yes |
|
||||
| GC Ep1&2 | Yes | Yes | Yes |
|
||||
| GC Ep1&2 Plus | Yes | Yes | Yes |
|
||||
| GC Ep3 NTE | Yes | Partial (1) | Yes |
|
||||
| GC Ep3 NTE | Yes | Yes (1) | Yes |
|
||||
| GC Ep3 | Yes | Yes | Yes |
|
||||
| Xbox Ep1&2 | Yes | Yes | Yes |
|
||||
| BB (vanilla) | Yes | Yes (2) | Yes |
|
||||
| BB (Tethealla) | Yes | Yes (2) | Yes |
|
||||
|
||||
*Notes:*
|
||||
1. *Players can create games, edit decks, trade cards, and participate in auctions, but CARD battles don't work on Episode 3 Trial Edition on newserv.*
|
||||
1. *Ep3 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.*
|
||||
2. *Some BB-specific features are not well-tested (for example, some quests that use rare commands may not work properly). Please submit a GitHub issue if you find something that doesn't work.*
|
||||
3. *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.*
|
||||
|
||||
## Setup
|
||||
# Setup
|
||||
|
||||
### Configuration
|
||||
## Server setup
|
||||
|
||||
Currently newserv works on macOS, Windows, and Ubuntu Linux. It will likely work on other Linux flavors too.
|
||||
|
||||
There is a fairly recent macOS ARM64 release on the newserv GitHub repository. You may need to install libevent manually even if you use this release (run `brew install libevent`).
|
||||
### Windows/macOS
|
||||
|
||||
There is a fairly recent Windows release on the newserv GitHub repository also. It's built with Cygwin, and all the necessary DLL files should be included. That said, I've only tested it on my own machine and there is no CI for Windows builds like there is for macOS and Linux, so if it doesn't work for you, please open a GitHub issue to let me know.
|
||||
1. Download the latest release-windows-amd64.zip or release-macos-arm64.zip file from the [releases page](https://github.com/fuzziqersoftware/newserv/releases).
|
||||
2. Extract the contents of the release folder to a location on your computer.
|
||||
3. Edit the config.example.json file in the system folder as needed, then rename it to config.json.
|
||||
4. If you plan to play Blue Burst on newserv, set up the patch directory. See [client patch directories](#client-patch-directories) for more information.
|
||||
5. Run the newserv executable.
|
||||
|
||||
If you're not using a release from the GitHub repository, do this to build newserv:
|
||||
1. If you're on Windows, install Cygwin. While doing so, install the `cmake`, `gcc-core`, `gcc-g++`, `git`, `libevent2.1_7`, `make`, `libiconv-devel`, and `zlib` packages. Do the rest of these steps inside a Cygwin shell (not a Windows cmd shell or PowerShell).
|
||||
2. Make sure you have CMake, libevent, and libiconv installed. (On macOS, `brew install cmake libevent libiconv`; on most Linuxes, `sudo apt-get install cmake libevent-dev`; on Windows, you already did this in step 1.)
|
||||
3. Build and install phosg (https://github.com/fuzziqersoftware/phosg).
|
||||
4. Optionally, install resource_dasm (https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to send memory patches and load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this.
|
||||
### Linux
|
||||
|
||||
There are currently no precompiled releases for Linux. To run newserv on Linux, see the "Building from source" section below.
|
||||
|
||||
### Building from source
|
||||
|
||||
1. Install the packages newserv depends on.
|
||||
* If you're on Windows, install [Cygwin](https://www.cygwin.com/). While doing so, install the `cmake`, `gcc-core`, `gcc-g++`, `git`, `libevent2.1_7`, `make`, `libiconv-devel`, and `zlib` packages. Do the rest of these steps inside a Cygwin shell (not a Windows cmd shell or PowerShell).
|
||||
* If you're on macOS, run `brew install cmake libevent libiconv`.
|
||||
* If you're on Linux, run `sudo apt-get install cmake libevent-dev` (or use your Linux distribution's package manager).
|
||||
3. Build and install [phosg](https://github.com/fuzziqersoftware/phosg).
|
||||
4. Optionally, install [resource_dasm](https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to send memory patches and load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this.
|
||||
5. Run `cmake . && make` in the newserv directory.
|
||||
|
||||
After building newserv or downloading a release, do this to set it up and use it:
|
||||
1. In the system/ directory, make a copy of config.example.json named config.json, and edit it appropriately.
|
||||
2. If you plan to play PSO Blue Burst on newserv, set up the patch directory. See the "Client patch directories" section below.
|
||||
3. Run `./newserv` in the newserv directory. This will start the game server and run the interactive shell. You may need `sudo` if newserv's built-in DNS server is enabled.
|
||||
4. If you set AllowUnregisteredUsers to false in config.json, use the interactive shell to add your license. Run `help` in the shell to see how to do this.
|
||||
5. Set your client's network settings appropriately and start an online game. See the "Connecting local clients" or "Connecting remote clients" section to see how to get your game client to connect.
|
||||
After building newserv, edit system/config.example.json as needed and rename it to system/config.json, set up [client patch directories](#client-patch-directories) if you're planning to play Blue Burst, then run `./newserv` in newserv's directory.
|
||||
|
||||
To use newserv in other ways (e.g. for translating data), see the end of this document.
|
||||
|
||||
### Installing quests
|
||||
## 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.
|
||||
|
||||
For Blue Burst set up, the below is mandatory for a smooth experience:
|
||||
|
||||
1. Browse to your chosen client's data directory.
|
||||
2. Copy all the map_*.dat files and the data.gsl file and place them in `system/patch-bb/data`
|
||||
|
||||
For BB clients, newserv reads some files out of the patch data to implement game logic, so it's important that certain game files are synchronized between the server and the client. newserv contains defaults for these files in the system/maps/bb-v4 directory, but if these don't match the client's copies of the files, odd behavior will occur in games.
|
||||
|
||||
To make server startup faster, newserv caches the modification times, sizes, and checksums of the files in the patch directories. If the patch server appears to be misbehaving, try deleting the .metadata-cache.json file in the relevant patch directory to force newserv to recompute all the checksums. Also, in the case when checksums are cached, newserv may not actually load the data for a patch file until it's needed by a client. Therefore, modifying any part of the patch tree while newserv is running can cause clients to see an inconsistent view of it.
|
||||
|
||||
Patch directory contents are cached in memory. If you've changed any of these files, you can run `reload patch-indexes` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
## How to connect
|
||||
|
||||
### PSO DC
|
||||
|
||||
Depending on the version of PSO DC that you have, the instructions to connect to a newserv instance will vary.
|
||||
|
||||
If you have NTE, USv1, EUv1, or EUv2 and a Broadband Adapter, edit the broadband DNS address to newserv's IP address with newserv's DNS server running. Otherwise, it is necessary to patch the disc or use a codebreaker code to remove the Hunter License server check and/or redirect PSO to the newserv instance. Patching the disc or creating a codebreaker code is beyond the scope of this document.
|
||||
|
||||
### PSO DC on Flycast
|
||||
|
||||
If you're emulating PSO DC, the NTE, USv1, EUv1, and EUv2 versions will connect to newserv by setting the following options in Flycast's `emu.cfg` file under `[network]`:
|
||||
- DNS = Your newserv's server address (newserv's DNS server must be running on port 53)
|
||||
- EmulateBBA = yes
|
||||
- Enable = yes
|
||||
|
||||
It is also necessary to save any DNS information to the flash memory of the Dreamcast to use the BBA - the easiest way to do this is to use the website option in USv2 and then choose the save to flash option.
|
||||
|
||||
If the server is running on the same machine as Flycast, this might not work, even if you point Flycast's DNS queries at your local IP address (instead of 127.0.0.1). In this case, you can modify the loaded executable in memory to make it connect anywhere you want. There is a script included with newserv that can do this on macOS; a similar technique could be done manually using scanmem on Linux or Cheat Engine on Windows. To use the script, do this:
|
||||
1. Build and install [memwatch](https://github.com/fuzziqersoftware/memwatch).
|
||||
2. Start Flycast and run PSO. (You must start PSO before running the script; it won't work if you run the script before loading the game.)
|
||||
3. Run `sudo patch_flycast_memory.py <original-destination>`. Replace `<original-destination>` with the hostname that PSO wants to connect to (you can find this out by using Wireshark and looking for DNS queries). The script may take up to a minute; you can continue using Flycast while it runs, but don't start an online game until the script is done.
|
||||
4. Run newserv and start an online game in PSO.
|
||||
|
||||
If you use this method, you'll have to run the script every time you start PSO in Flycast, but you won't have to run it again if you start another online game without restarting emulation.
|
||||
|
||||
If using JPv1, JPv2, or USv2, it is also necessary to remove the Hunter Licence server check, either with a disc patch or codebreaker code. Patching the disc or creating a codebreaker code is beyond the scope of this document.
|
||||
|
||||
### PSO PC
|
||||
|
||||
PSO PC has its connection addresses in `pso.exe`. Hex edit the executable with the connection address you want to connect to. Common server addresses to search for to replace are:
|
||||
- pso20.sonic.isao.net
|
||||
- sg207634.sonicteam.com
|
||||
- pso-mp01.sonic.isao.net
|
||||
- gsproduc.ath.cx
|
||||
- sylverant.net
|
||||
|
||||
The version of PSO PC I have has the server addresses starting at offset 0x29CB34 in pso.exe. Change those addresses to "localhost" (without quotes) if you just want to connect to a locally-running newserv instance. Alternatively, you can add an entry to the Windows hosts file (C:\Windows\System32\drivers\etc\hosts) to redirect the connection to 127.0.0.1 (localhost) or any other IP address.
|
||||
|
||||
### PSO GC on a real GameCube
|
||||
|
||||
You can make PSO connect to newserv by setting its default gateway and DNS server addresses in network settings to newserv's address. newserv's DNS server must be running on port 53 and must be accessible to the GameCube.
|
||||
|
||||
If you have PSO Plus or Episode III, it won't want to connect to a server on the same local network as the GameCube itself, as determined by the GameCube's IP address and subnet mask. In the old days, one way to get around this was to create a fake network adapter on the server (or use an existing real one) that has an IP address on a different subnet, tell the GameCube that the server is the default gateway (as above), and have the server reply to the DNS request with its non-local IP address. To do this with newserv, just set LocalAddress in the config file to a different interface. For example, if the GameCube is on the 192.168.0.x network and your other adapter has address 10.0.1.6, set newserv's LocalAddress to 10.0.1.6 and set PSO's DNS server and default gateway addresses to the server's 192.168.0.x address. This may not work on modern systems or on non-Windows machines - I haven't tested it in many years.
|
||||
|
||||
### PSO GC on Dolphin
|
||||
|
||||
If you're using the HLE BBA type, set the BBA's DNS server address to newserv's IP address and it should work. (If newserv is on the same machine as Dolphin, you will need to use an action replay code directed at 127.0.0.1 to connect, as PSO rejects DNS queries from the same IP address.) Set PSO's network settings the same as listed below.
|
||||
|
||||
If you're using the TAP BBA type, you'll have to set PSO's network settings appropriately for your tap interface. Set the DNS server address in PSO's network settings to newserv's IP address.
|
||||
|
||||
If you're using a version of Dolphin with tapserver support, you can make it connect to a newserv instance running on the same machine via the tapserver interface. You do not need to install or run tapserver. To do this:
|
||||
1. Set Dolphin's BBA type to tapserver (Config -> GameCube -> SP1).
|
||||
2. Enable newserv's IP stack simulator according to the comments in config.json and start newserv.
|
||||
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 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 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" section for instructions on setting this up.)
|
||||
|
||||
The original Japanese and US versions of PSO BB should work, but you'll have to modify your hosts file or edit psobb.exe to point to your newserv instance. The original versions are packed, so this is a more involved process than simply opening the executable in a hex editor and finding/replacing some strings.
|
||||
|
||||
Alternatively, you can use the Tethealla client (https://archive.org/details/psobb-tethealla-client); you can find the connection addresses starting at 0x56D724 in psobb.exe. Overwrite these addresses with your server's hostname or IP address, and you should be able to connect.
|
||||
|
||||
### Connecting external clients
|
||||
|
||||
If you want to accept connections from outside your local network, you'll need to set ExternalAddress to your public IP address in the configuration file, and you'll likely need to open some ports in your router's NAT configuration - specifically, all the TCP ports listed in PortConfiguration in config.json.
|
||||
|
||||
For GC clients, you'll have to use newserv's built-in DNS server or set up your own DNS server as well. If you want external clients to be able to use your DNS server, you'll have to forward UDP port 53 to your newserv instance. Remote players can then connect to your server by entering your DNS server's IP address in their client's network configuration.
|
||||
|
||||
# Server feature configuration
|
||||
|
||||
## Installing quests
|
||||
|
||||
newserv automatically finds quests in the subdirectories of the system/quests/ directory. To install your own quests, or to use quests you've saved using the proxy's Save Files option, just put them in one of the subdirectories there and name them appropriately. The subdirectories and their behaviors (e.g. in which game modes they should appear and for which PSO versions) is defined in the QuestCategories field in config.json.
|
||||
|
||||
@@ -137,9 +223,9 @@ Episode 3 download quests consist only of a .bin file - there is no correspondin
|
||||
|
||||
When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats.
|
||||
|
||||
Quest contents are cached in memory, but if you've changed the contents of the quests directory, you can re-index the quests without restarting the server by running `reload quests` in the interactive shell. The new quests will be available immediately, but any games with quests already in progress will continue using the old versions of the quests until those quests end.
|
||||
Quest contents are cached in memory, but if you've changed the contents of the quests directory, you can re-index the quests without restarting the server by running `reload quest-index` in the interactive shell. The new quests will be available immediately, but any games with quests already in progress will continue using the old versions of the quests until those quests end.
|
||||
|
||||
### Item tables and drop modes
|
||||
## Item tables and drop modes
|
||||
|
||||
newserv supports server-side item generation on all game versions, except for the earliest DC prototypes (NTE and 11/2000). By default, the game behaves as it did on the original servers - on all versions except BB, item drops are controlled by the leader client in each game, and on BB, item drops are controlled by the server.
|
||||
|
||||
@@ -156,7 +242,7 @@ The drop mode can be changed at any time during a game with the `$dropmode` chat
|
||||
|
||||
In the server drop modes, the item tables used to generate common items are in the `system/item-tables/ItemPT-*` files. (The V2 files are used for V1 as well.) The rare item tables are in the `rare-table-*.json` files. Unlike the original formats, it's possible to make each enemy drop multiple different rare items at different rates, though the default tables never do this.
|
||||
|
||||
### Cross-version play
|
||||
## Cross-version play
|
||||
|
||||
All versions of PSO can see and interact with each other in the lobby. newserv also allows some versions to play in-game with each other:
|
||||
* DC V1 players can join DC V2 games if the difficulty level isn't set to Ultimate and the creator chose to allow V1 players.
|
||||
@@ -166,7 +252,7 @@ All versions of PSO can see and interact with each other in the lobby. newserv a
|
||||
|
||||
In V1/V2 cross-version play, when any of the server drop modes are used, the server uses the drop table corresponding to the version the game was created with. (For example, if a DC V1 player created the game, rare-table-v1.json will be used, even after V2 players join.)
|
||||
|
||||
### Episode 3 features
|
||||
## Episode 3 features
|
||||
|
||||
newserv supports many features unique to Episode 3:
|
||||
* CARD battles. Not every combination of abilities has been tested yet, so if you find a feature or card ability that doesn't work like it's supposed to, please make a GitHub issue and describe the situation (the attacking card(s), defending card(s), and ability or condition that didn't work).
|
||||
@@ -177,11 +263,11 @@ newserv supports many features unique to Episode 3:
|
||||
* Participating in card auctions. (The auction contents must be configured in config.json.)
|
||||
* Decorations in lobbies. Currently only images are supported; the game also supports loading custom 3D models in lobbies, but newserv does not implement this (yet).
|
||||
|
||||
#### Battle records
|
||||
### Battle records
|
||||
|
||||
After playing a battle, you can save the record of the battle with the `$saverec` command. You can then replay the battle later by using the `$playrec` command in a lobby - this will create a spectator team and play the recording of the battle as if it were happening in realtime. Note that there is a bug in older versions of Dolphin that seems to be frequently triggered when playing battle records, which causes the emulator to crash with the message `QObject::~QObject: Timers cannot be stopped from another thread`. To avoid this, use the latest version of Dolphin.
|
||||
|
||||
#### Tournaments
|
||||
### Tournaments
|
||||
|
||||
Tournaments work differently than they did on Sega's servers. Tournaments can be created with the `create-tournament` shell command, which enables players to register for them. (Use `help` to see all the arguments - there are many!) The `start-tournament` shell command starts the tournament (and prevents further registrations), but this doesn't schedule any matches. Instead, players who are ready to play their next match can all stand at the 4-player battle table near the lobby warp in the same CARD lobby, and the tournament match will start automatically.
|
||||
|
||||
@@ -189,7 +275,7 @@ These tournament semantics mean that there can be multiple matches in the same t
|
||||
|
||||
The Meseta rewards for winning tournament matches can be configured in config.json.
|
||||
|
||||
#### Episode 3 files
|
||||
### Episode 3 files
|
||||
|
||||
Episode 3 state and game data is stored in the system/ep3 directory. The files in there are:
|
||||
* card-definitions.mnr: Compressed card definition list, sent to Episode 3 clients at connect time. Card stats and abilities can be changed by editing this file.
|
||||
@@ -203,44 +289,35 @@ Episode 3 state and game data is stored in the system/ep3 directory. The files i
|
||||
|
||||
There is no public editor for Episode 3 maps and quests, but the format is described fairly thoroughly in src/Episode3/DataIndexes.hh (see the MapDefinition structure). You'll need to use `newserv decompress-prs ...` to decompress .bin or .mnm files before editing them, but you don't need to compress the files again to use them - just put the .bind or .mnmd file in the maps directory and newserv will make it available.
|
||||
|
||||
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` in the interactive shell to make the changes take effect without restarting the server.
|
||||
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-data` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
### Client patch directories
|
||||
## Memory patches, client functions, and DOL files
|
||||
|
||||
If you're not playing PSO Blue Burst on newserv, you can skip these steps.
|
||||
|
||||
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.
|
||||
|
||||
To make server startup faster, newserv caches the modification times, sizes, and checksums of the files in the patch directories. If the patch server appears to be misbehaving, try deleting the .metadata-cache.json file in the relevant patch directory to force newserv to recompute all the checksums. Also, in the case when checksums are cached, newserv may not actually load the data for a patch file until it's needed by a client. Therefore, modifying any part of the patch tree while newserv is running can cause clients to see an inconsistent view of it.
|
||||
|
||||
For BB clients, newserv reads some files out of the patch data to implement game logic, so it's important that certain game files are synchronized between the server and the client. newserv contains defaults for these files in the system/blueburst/map directory, but if these don't match the client's copies of the files, odd behavior will occur in games.
|
||||
|
||||
Specifically, the patch-bb directory should contain at least the data.gsl file and all map_*.dat files from the version of PSOBB that you want to play on newserv. You can copy these files out of the client's data directory from a clean installation, and put them in system/patch-bb/data.
|
||||
|
||||
Patch directory contents are cached in memory. If you've changed any of these files, you can run `reload patches` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
### Memory patches and DOL files
|
||||
|
||||
Everything in this section requires resource_dasm to be installed, so newserv can use the PowerPC assembler and disassembler from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available.
|
||||
Everything in this section requires resource_dasm to be installed, so newserv can use the assemblers and disassemblers from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available.
|
||||
|
||||
In addition, these features are only supported for the following game versions:
|
||||
* PSO GameCube Episodes 1&2 JP, USA, and EU (not Plus)
|
||||
* PSO GameCube Episodes 1&2 Plus JP v1.04 (not v1.05)
|
||||
* PSO GameCube Episodes 1&2 Trial Edition
|
||||
* PSO GameCube Episodes 1&2 JP, USA, and EU but not Plus
|
||||
* PSO GameCube Episodes 1&2 Plus JP v1.4 but not v1.5
|
||||
* PSO GameCube Episode 3 Trial Edition
|
||||
* PSO GameCube Episode 3 JP
|
||||
* PSO GameCube Episode 3 USA (experimental; must be manually enabled in config.json)
|
||||
* PSO Xbox (all versions)
|
||||
* PSO BB
|
||||
|
||||
You can put memory patches in the system/ppc directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC clients that support patching. Memory patches are written in PowerPC assembly and are compiled when newserv is started. The PowerPC assembly system's features are documented in the comments in system/ppc/WriteMemory.s - this file is not a memory patch itself, but it describes how memory patches may be written and the restrictions that apply to them.
|
||||
*Note: newserv uses the shorter GameCube versioning convention, where discs labeled DOL-XXXX-0-0Y are version 1.Y. The PSO community seems to use the convention 1.0Y in some places instead, but these are the same version. For example, the version that newserv calls v1.4 is the same as v1.04, and is labeled DOL-GPOJ-0-04 on the underside of the disc.*
|
||||
|
||||
newserv comes with a set of patches for Episodes 1&2 based on AR codes originally made by Ralf at GC-Forever. Many of them were originally posted in [this thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050).
|
||||
You can put memory patches in the system/client-functions directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC, XB, and BB clients that support patching. Memory patches are written in 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/WriteMemory.ppc.s.
|
||||
|
||||
You can also put DOL files in the system/dol directory, and they will appear in the Programs menu. 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, ReadMemoryWord.s, WriteMemory.s, and RunDOL.s must be present in the system/ppc directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
|
||||
newserv comes with a set of patches for GC Episodes 1&2 based on AR codes originally made by Ralf at GC-Forever. Many of them were originally posted in [this thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050).
|
||||
|
||||
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, ReadMemoryWord.ppc.s, WriteMemory.ppc.s, and RunDOL.ppc.s must be present in the system/client-functions 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.
|
||||
|
||||
I mainly built the DOL loading functionality for documentation purposes. By now, there are many better ways to load homebrew code on an unmodified GameCube, but to my knowledge there isn't another open-source implementation of this method in existence.
|
||||
|
||||
### Using newserv as a proxy
|
||||
## Using newserv as a proxy
|
||||
|
||||
If you want to play online on remote servers rather than running your own server, newserv also includes a PSO proxy. Currently this works with PSO GC and may work with PC and DC; it also works with some BB clients in specific situations.
|
||||
|
||||
@@ -275,7 +352,7 @@ The remote server will probably try to assign you a Guild Card number that doesn
|
||||
|
||||
Some chat commands (see below) have the same basic function on the proxy server but have different effects or conditions. In addition, there are some server shell commands that affect clients on the proxy (run `help` in the shell to see what they are). If there's only one proxy session open, the shell's proxy commands will affect that session. Otherwise, you'll have to specify which session to affect with the `on` prefix - to send a chat message in LinkedSession:17205AE4, for example, you would run `on 17205AE4 chat ...`.
|
||||
|
||||
### Chat commands
|
||||
## Chat commands
|
||||
|
||||
newserv supports a variety of commands players can use by chatting in-game. Any chat message that begins with `$` is treated as a chat command. (If you actually want to send a chat message starting with `$`, type `$$` instead.) On the DC 11/2000 prototype, `@` is used instead of `$` for all chat commands, since `$` does not appear on the English virtual keyboard.
|
||||
|
||||
@@ -283,36 +360,49 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
|
||||
* Information commands
|
||||
* `$li`: Shows basic information about the lobby or game you're in. If you're on the proxy server, shows information about your connection instead (remote Guild Card number, client ID, etc.).
|
||||
* `$si` (game server only): Shows basic information about the server.
|
||||
* `$ping`: Shows round-trip ping time from the server to you. On the proxy server, shows the ping time from you to the proxy and from the proxy to the server.
|
||||
* `$matcount` (game server only): Shows how many of each type of material you've used.
|
||||
* `$itemnotifs <mode>`: Enables item drop notification messages. The modes are `off`, `rare`, and `on`, which should be self-explanatory. If the game has private drops enabled, you will only see a notification if the dropped item is visible to you; you won't be notified of other players' rare drops.
|
||||
* `$what` (game server only): Shows the type, name, and stats of the nearest item on the ground.
|
||||
* `$where` (game server only): Shows your current floor number and coordinates. Mainly useful for debugging.
|
||||
|
||||
* Debugging commands
|
||||
* `$debug` (game server only): Enable or disable debug. You need the DEBUG permission in your user license to use this command. When debug is enabled, you'll see in-game messages from the server when you take certain actions. You'll also be placed into the highest available slot in lobbies and games instead of the lowest, which is useful for finding commands for which newserv doesn't handle client IDs properly. This setting also disables certain safeguards and allows you to do some things that might crash your client.
|
||||
* `$quest <number>`: Load a quest by quest number. Can be used to load battle or challenge quests with only one player present.
|
||||
* `$debug` (game server only): Enable or disable debug. You need the DEBUG permission in your user license to use this command. Enabling debug does a few things:
|
||||
* You'll see in-game messages from the server when you take certain actions, like killing an enemy in BB.
|
||||
* You'll see the rare seed value and floor variations when you join a game.
|
||||
* You'll be placed into the highest available slot in lobbies and games instead of the lowest, 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 crossplay is normally supported. Be prepared for client crashes and other client-side brokenness if you do this. Please do not submit any issues for broken behaviors in crossplay, unless the situation is explicitly supported (see the "Cross-version play" section above).
|
||||
* The rest of the commands in this section are enabled on the game server. (They are always enabled on the proxy server.)
|
||||
* `$quest <number>` (game server only): Load a quest by quest number. Can be used to load battle or challenge quests with only one player present.
|
||||
* `$qcall <function-id>`: Call a quest function on your client.
|
||||
* `$qcheck <flag-num>`: Show the value of a quest flag.
|
||||
* `$qset <flag-num>` or `$qclear <flag-num>`: Set or clear a global quest flag for everyone in the game.
|
||||
* `$qsync <reg-num> <value>`: Set a quest register's value on your client. `<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.
|
||||
* `$qcheck <flag-num>` (game server only): Show the value of a quest flag. This command can be used without debug mode enabled.
|
||||
* `$qset <flag-num>` or `$qclear <flag-num>`: Set or clear a quest flag for everyone in the game.
|
||||
* `$qgread <flag-num>` (game server only): Get the value of a quest counter ("global flag"). This command can be used without debug mode enabled.
|
||||
* `$qgwrite <flag-num> <value>` (game server 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.
|
||||
* `$gc` (game server only): Send your own Guild Card to yourself.
|
||||
* `$persist` (game server only): Enable or disable persistence for the current game. When persistence is on, the game will not be deleted when the last player leaves. The state of enemies and objects on the map will be reset when the last player leaves.
|
||||
* `$sc <data>`: Send a command to yourself.
|
||||
* `$ss <data>` (proxy server only): Send a command to the remote server.
|
||||
* `$meseta <amount>` (game server only; Episode 3 only): Add the given amount to your Meseta total.
|
||||
* `$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.
|
||||
* `$ep3battledebug` (game server only; Episode 3 only): Enable or disable TCard00_Select. If enabled, the game will enter the debug menu when you start a battle.
|
||||
|
||||
* Personal state commands
|
||||
* `$arrow <color-id>`: Changes your lobby arrow color.
|
||||
* `$secid <section-id>`: Sets your override section ID. After running this command, any games you create will use your override section ID for rare drops instead of your character's actual section ID. To revert to your actual section id, run `$secid` with no name after it. On the proxy server, this will not work if the remote server controls item drops (e.g. on BB, or on Schtserv with server drops enabled). If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing.
|
||||
* `$rand <seed>`: Sets your override random seed (specified as a 32-bit hex value). This will make any games you create use the given seed for rare enemies. This also makes item drops deterministic in Blue Burst games hosted by newserv. On the proxy server, this command can cause desyncs with other players in the same game, since they will not see the overridden random seed. To remove the override, run `$rand` with no arguments. If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing.
|
||||
* `$ln [name-or-type]`: Sets the lobby number. Visible only to you. This command exists because some non-lobby maps can be loaded as lobbies with invalid lobby numbers. See the "GC lobby types" and "Ep3 lobby types" entries in the information menu for acceptable values here. Note that non-lobby maps do not have a lobby counter, so there's no way to exit the lobby without using either `$ln` again or `$exit`. On the game server, `$ln` reloads the lobby immediately; on the proxy server, it doesn't take effect until you load another lobby yourself (which means you'll like have to use `$exit` to escape). Run this command with no argument to return to the default lobby.
|
||||
* `$swa`: Enables or disables switch assist. When enabled, the server will attempt to automatically unlock two-player doors in non-quest games if you step on both switches sequentially.
|
||||
* `$exit`: If you're in a lobby, sends you to the main menu (which ends your proxy session, if you're in one). If you're in a game or spectator team, sends you to the lobby (but does not end your proxy session if you're in one). Does nothing if you're in a non-Episode 3 game and no quest is in progress.
|
||||
* `$patch <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
|
||||
|
||||
* Character data commands
|
||||
* Character data commands (game server only)
|
||||
* `$savechar <slot>`: Saves your current character data on the server in the specified slot (each serial number has 4 slots, numbered 1-4). These slots are separate from BB character slots; using this command does not affect BB characters.
|
||||
* `$loadchar <slot>` (v1 and v2 only): Loads your character data from the specified slot. The changes will be undone if you join a game - to save your changes, disconnect from the lobby.
|
||||
* `$bbchar <username> <password> <slot>`: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot (1-4). Any character already in that slot is overwritten. (This command is similar to `$savechar`, except it overwrites a BB character slot, and can transfer characters across accounts.) Note that the character's chat data, quick menu config, and bank contents are not copied, since there is no way for the server to request those types of data.
|
||||
* `$edit <stat> <value>`: Modifies your character data. If you are on V3 (GameCube/Xbox), or if the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing. If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby.
|
||||
* `$edit <stat> <value>`: Modifies your character data. If you are on V3 (GameCube/Xbox), this command does nothing. If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby. If cheats are allowed on the server, `<stat>` can be any of `atp`, `mst`, `evp`, `hp`, `dfp`, `ata`, `lck`, `meseta`, `exp`, `level`, `namecolor`, `secid`, `name`, `npc`, or `tech`. If cheats are not allowed, only `namecolor`, `name`, and `npc` can be used.
|
||||
|
||||
* Blue Burst player commands (game server only)
|
||||
* `$bank [number]`: Switches your current bank, so you can access your other character's banks (if `number` is 1-4) or your shared account bank (if `number` is 0). If `number` is not given, switches back to your current character's bank.
|
||||
@@ -323,6 +413,7 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$minlevel <level>`: Sets the minimum level for players to join the current game.
|
||||
* `$password <password>`: Sets the game's join password. To unlock the game, run `$password` with nothing after it.
|
||||
* `$dropmode [mode]`: Changes the way item drops behave in the current game. `mode` can be `none`, `client`, `shared`, `private`, or `duplicate`. If `mode` is not given, tells you the current drop mode without changing it. See the "Item tables and drop modes" section for more information.
|
||||
* `$persist`: Enable or disable persistence for the current game. When persistence is on, the game will not be deleted when the last player leaves. The state of enemies and objects on the map will be reset when the last player leaves, but dropped items will not be deleted. If the game is empty for too long (15 minutes by default), it is then deleted.
|
||||
|
||||
* Episode 3 commands (game server only)
|
||||
* `$spec`: Toggles the allow spectators flag for Episode 3 games. If any players are spectating when this flag is disabled, they will be sent back to the lobby.
|
||||
@@ -334,14 +425,13 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$playrec <name>`: Plays a battle recording. This command creates a spectator team and replays the specified battle log within it. There is a bug in Dolphin that makes use of this command unstable in emulation (see the "Battle records" section above).
|
||||
|
||||
* Cheat mode commands
|
||||
* `$cheat`: Enables or disables 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.
|
||||
* `$infhp` / `$inftp`: Enables or disables infinite HP or TP mode. Applies to only you. In infinite HP mode, one-hit KO attacks will still kill you.
|
||||
* `$warpme <floor-id>`: Warps yourself to the given floor.
|
||||
* `$cheat` (game server only): Enables or disables 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 server, unless cheat mode is disabled on the entire server.
|
||||
* `$infhp` / `$inftp`: Enables or disables infinite HP or TP mode. Applies to only you. In infinite HP mode, one-hit KO attacks will still kill you. On V1 and V2, infinite HP also automatically cures status ailments.
|
||||
* `$warpme <floor-id>` (or `$warp <floor-id>`): Warps yourself to the given floor.
|
||||
* `$warpall <floor-id>`: Warps everyone in the game to the given floor. You must be the leader to use this command, unless you're on the proxy server.
|
||||
* `$next`: Warps yourself to the next floor.
|
||||
* `$swa`: Enables or disables switch assist. When enabled, the server will attempt to automatically unlock two-player doors in solo games if you step on both switches sequentially.
|
||||
* `$item <desc>` (or `$i <desc>`): Create an item. `desc` may be a description of the item (e.g. "Hell Saber +5 0/10/25/0/10") or a string of hex data specifying the item code. Item codes are 16 hex bytes; at least 2 bytes must be specified, and all unspecified bytes are zeroes. If you are on the proxy server, you must not be using Blue Burst for this command to work. On the game server, this command works for all versions.
|
||||
* `$unset <index>`: In an Episode 3 battle, removes one of your set cards from the field. `<index>` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed.
|
||||
* `$unset <index>` (game server only): In an Episode 3 battle, removes one of your set cards from the field. `<index>` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed.
|
||||
|
||||
* Configuration commands
|
||||
* `$event <event>`: Sets the current holiday event in the current lobby. Holiday events are documented in the "Using $event" item in the information menu. If you're on the proxy server, this applies to all lobbies and games you join, but only you will see the new event - other players will not.
|
||||
@@ -355,88 +445,47 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$kick <identifier>`: Disconnects a player. The identifier may be the player's name or Guild Card number.
|
||||
* `$ban <identifier>`: Bans a player. The identifier may be the player's name or Guild Card number.
|
||||
|
||||
### Connecting local clients
|
||||
# Non-server features
|
||||
|
||||
#### PSO DC
|
||||
newserv has many CLI options, which can be used to access functionality other than the game and proxy server. Run `newserv help` to see a full list of the options and how to use each one.
|
||||
|
||||
Some versions of PSO DC will connect to a private server if you just set their DNS server address (in the network configuration) to newserv's address, and enable newserv's DNS server. This will not work for other versions; for those, you'll need a cheat code. Creating such a code is beyond the scope of this document.
|
||||
The data formats that newserv can convert to/from are:
|
||||
|
||||
If you're emulating PSO DC or have a disc image, you can patch the appropriate files within the disc image to make it connect to any address you want. Creating such a patch is also beyond the scope of this document.
|
||||
| Format | Encode/compress action | Decode/extract action |
|
||||
|--------------------------------|---------------------------|------------------------------|
|
||||
| PRS compression | `compress-prs` | `decompress-prs` |
|
||||
| PR2/PRC compression | `compress-pr2` | `decompress-pr2` |
|
||||
| BC0 compression | `compress-bc0` | `decompress-bc0` |
|
||||
| Raw encrypted data | `encrypt-data` | `decrypt-data` |
|
||||
| Episode 3 command mask | `encrypt-trivial-data` | `decrypt-trivial-data` |
|
||||
| Challenge Mode rank text | `encrypt-challenge-data` | `decrypt-challenge-data` |
|
||||
| PSO DC quest file (.vms) | None | `decode-vms` |
|
||||
| PSO GC quest file (.gci) | None | `decode-gci` |
|
||||
| Download quest file (.dlq) | None | `decode-dlq` |
|
||||
| Server quest file (.qst) | `encode-qst` | `decode-qst` |
|
||||
| PSO PC save file | `encrypt-pc-save` | `decrypt-pc-save` |
|
||||
| PSO GC save file (.gci) | `encrypt-gci-save` | `decrypt-gci-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` |
|
||||
| AFS archive | None | `extract-afs` |
|
||||
| BML archive | None | `extract-bml` |
|
||||
| GSL archive | None | `extract-gsl` |
|
||||
| GVM texture | `encode-gvm` | None |
|
||||
| Text archive | `encode-text-archive` | `decode-text-archive` |
|
||||
| Unicode text set | `encode-unicode-text-set` | `decode-unicode-text-set` |
|
||||
| Word Select data set | None | `decode-word-select-set` |
|
||||
| Set data table | None | `disassemble-set-data-table` |
|
||||
| Rare item table (AFS/GSL/JSON) | `convert-rare-item-set` | `convert-rare-item-set` |
|
||||
|
||||
#### PSO DC on Flycast
|
||||
There are several actions that don't fit well into the table above, which let you do other things:
|
||||
|
||||
If you're emulating PSO DC, all versions will connect to newserv by setting the following options in Flycast's `emu.cfg` file under `[network]`:
|
||||
- DNS = Your newserv's server address (newserv's DNS server must be running on port 53)
|
||||
- EmulateBBA = yes
|
||||
- Enable = yes
|
||||
|
||||
It is also necessary to save any DNS information to the flash memory of the Dreamcast to use the BBA - the easiest way to do this is to use the website option in USv2 and then choose the save to flash option.
|
||||
|
||||
Once set up, the EU and US versions will work without any extra set up (other than the HL Check Disable code for USv2), while the JP versions require HL Check Disable codes to be running.
|
||||
|
||||
If the server is running on the same machine as Flycast, this might not work, even if you point Flycast's DNS queries at your local IP address (instead of 127.0.0.1). In this case, you can modify the loaded executable in memory to make it connect anywhere you want. There is a script included with newserv that can do this on macOS; a similar technique could be done manually using scanmem on Linux or Cheat Engine on Windows. To use the script, do this:
|
||||
1. Build and install memwatch (https://github.com/fuzziqersoftware/memwatch).
|
||||
2. Start Flycast and run PSO. (You must start PSO before running the script; it won't work if you run the script before loading the game.)
|
||||
3. Run `sudo patch_flycast_memory.py <original-destination>`. Replace `<original-destination>` with the hostname that PSO wants to connect to (you can find this out by using Wireshark and looking for DNS queries). The script may take up to a minute; you can continue using Flycast while it runs, but don't start an online game until the script is done.
|
||||
4. Run newserv and start an online game in PSO.
|
||||
|
||||
If you use this method, you'll have to run the script every time you start PSO in Flycast, but you won't have to run it again if you start another online game without restarting emulation.
|
||||
|
||||
#### PSO PC
|
||||
|
||||
The version of PSO PC I have has the server addresses starting at offset 0x29CB34 in pso.exe. Using a hex editor, change those to "localhost" (without quotes) if you just want to connect to a locally-running newserv instance. Alternatively, you can add an entry to the Windows hosts file (C:\Windows\System32\drivers\etc\hosts) to redirect the connection to 127.0.0.1 (localhost) or any other IP address.
|
||||
|
||||
#### PSO GC on a real GameCube
|
||||
|
||||
You can make PSO connect to newserv by setting its default gateway and DNS server addresses to newserv's address. newserv's DNS server must be running on port 53 and must be accessible to the GameCube.
|
||||
|
||||
If you have PSO Plus or Episode III, it won't want to connect to a server on the same local network as the GameCube itself, as determined by the GameCube's IP address and subnet mask. In the old days, one way to get around this was to create a fake network adapter on the server (or use an existing real one) that has an IP address on a different subnet, tell the GameCube that the server is the default gateway (as above), and have the server reply to the DNS request with its non-local IP address. To do this with newserv, just set LocalAddress in the config file to a different interface. For example, if the GameCube is on the 192.168.0.x network and your other adapter has address 10.0.1.6, set newserv's LocalAddress to 10.0.1.6 and set PSO's DNS server and default gateway addresses to the server's 192.168.0.x address. This may not work on modern systems or on non-Windows machines - I haven't tested it in many years.
|
||||
|
||||
#### PSO GC on Dolphin
|
||||
|
||||
If you're using the HLE BBA type, set the BBA's DNS server address to newserv's IP address and it should work. (If newserv is on the same machine as Dolphin, try your local IP address or 127.0.0.1.) In PSO, use the example values below in PSO's network configuration.
|
||||
|
||||
If you're using the TAP BBA type, you'll have to set PSO's network settings appropriately for your tap interface. Set the DNS server address in PSO's network settings to newserv's IP address.
|
||||
|
||||
If you're using a version of Dolphin with tapserver support, you can make it connect to a newserv instance running on the same machine via the tapserver interface. You do not need to install or run tapserver. To do this:
|
||||
1. Set Dolphin's BBA type to tapserver (Config -> GameCube -> SP1).
|
||||
2. Enable newserv's IP stack simulator according to the comments in config.json and start newserv.
|
||||
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 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 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" section for instructions on setting this up.)
|
||||
|
||||
The original Japanese and US versions of PSO BB should work, but you'll have to modify your hosts file or edit psobb.exe to point to your newserv instance. The original versions are packed, so this is a more involved process than simply opening the executable in a hex editor and finding/replacing some strings.
|
||||
|
||||
Alternatively, you can use the Tethealla client (https://archive.org/details/psobb-tethealla-client); you can find the connection addresses starting at 0x56D724 in psobb.exe. Overwrite these addresses with your server's hostname or IP address, and you should be able to connect.
|
||||
|
||||
### Connecting external clients
|
||||
|
||||
If you want to accept connections from outside your local network, you'll need to set ExternalAddress to your public IP address in the configuration file, and you'll likely need to open some ports in your router's NAT configuration - specifically, all the TCP ports listed in PortConfiguration in config.json.
|
||||
|
||||
For GC clients, you'll have to use newserv's built-in DNS server or set up your own DNS server as well. If you want external clients to be able to use your DNS server, you'll have to forward UDP port 53 to your newserv instance. Remote players can then connect to your server by entering your DNS server's IP address in their client's network configuration.
|
||||
|
||||
### Non-server features
|
||||
|
||||
newserv has many CLI options, which can be used to access functionality other than the game and proxy server. Run `newserv help` to see these options and how to use them. The non-server things newserv can do are:
|
||||
|
||||
* Compress or decompress data in PRS, PR2, or BC0 format (`compress-prs`, `compress-pr2`, `compress-bc0`, `decompress-prs`, `decompress-pr2`, `decompress-bc0`)
|
||||
* Compute the decompressed size of compressed PRS data without decompressing it (`prs-size`)
|
||||
* Encrypt or decrypt data using any PSO version's network encryption scheme (`encrypt-data`, `decrypt-data`)
|
||||
* Encrypt or decrypt data using Episode 3's trivial scheme (`encrypt-trivial-data`, `decrypt-trivial-data`)
|
||||
* Encrypt or decrypt data using the Challenge Mode text algorithm (`encrypt-challenge-data`, `decrypt-challenge-data`)
|
||||
* Encrypt or decrypt PSO GC save data (.gci files) (`encrypt-gci-save`, `decrypt-gci-save`)
|
||||
* Convert a PSO GC or Episode 3 snapshot file to a BMP image (`decode-gci-snapshot`)
|
||||
* Find the likely round1 or round2 seed for a corrupt save file (`salvage-gci`)
|
||||
* Run a brute-force search for a decryption seed (`find-decryption-seed`)
|
||||
* Convert quests in .gci, .vms, .dlq, or .qst format to .bin/.dat format (`decode-gci`, `decode-vms`, `decode-dlq`, `decode-qst`)
|
||||
* Convert quests in .bin/.dat to .qst format (`encode-qst`)
|
||||
* Convert text archives (e.g. TextEnglish.pr2) to JSON and vice versa (`decode-text-archive`, `encode-text-archive`)
|
||||
* Compile or disassemble quest scripts (`assemble-quest-script`, `disassemble-quest-script`)
|
||||
* Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`)
|
||||
* Convert item data to a human-readable description, or vice versa (`describe-item`, `encode-item`)
|
||||
* Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`, `generate-ep3-cards-html`)
|
||||
* Format Blue Burst battle parameter files in a human-readable manner (`show-battle-params`)
|
||||
* Search for rare enemy seeds that result in rare enemies on console versions (`find-rare-enemy-seeds`)
|
||||
* Convert item data to a human-readable description, or vice versa (`describe-item`)
|
||||
* Connect to another PSO server and pretend to be a client (`cat-client`)
|
||||
* Replay a session log for testing (`replay-log`)
|
||||
* Extract the contents of a .gsl or .bml archive (`extract-gsl`, `extract-bml`)
|
||||
* Generate or describe DC serial numbers (`generate-dc-serial-number`, `inspect-dc-serial-number`)
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
## General
|
||||
|
||||
- Encapsulate BB server-side random state and make replays deterministic
|
||||
- Write a simple status API
|
||||
- Implement per-game logging
|
||||
- Make reloading happen on separate threads so compression doesn't block active clients
|
||||
- Implement decrypt/encrypt actions for VMS files
|
||||
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
|
||||
- Figure out what causes the corruption message on PC proxy sessions and fix it
|
||||
- Add an idle connection timeout for proxy sessions
|
||||
- Look into JP heart symbol bug on Linux
|
||||
- Clean up ItemParameterTable implementation (see comment ad the top of the class definition)
|
||||
|
||||
## PSO DC
|
||||
|
||||
- Investigate if https://crates.io/crates/blaze-ssl-async can be used to implement the HL check server
|
||||
|
||||
## Episode 3
|
||||
|
||||
- Enforce tournament deck restrictions (e.g. rank checks, No Assist option) when populating COMs at tournament start time
|
||||
- Add support for recording battles on the proxy server (both in primary and spectator teams)
|
||||
- Make `reload licenses` not vulnerable to online players' licenses overwriting licenses on disk somehow
|
||||
- Implement ranks (based on total Meseta earned)
|
||||
- Support Trial Edition battles
|
||||
|
||||
## PSO XBOX
|
||||
|
||||
@@ -25,5 +25,4 @@
|
||||
## PSOBB
|
||||
|
||||
- Test all quest item subcommands
|
||||
- Check if Commander Blade effect works and implement it if not
|
||||
- Figure out why Pouilly Slime EXP doesn't work
|
||||
|
||||
+10
-4
@@ -3,7 +3,7 @@
|
||||
04368960 38600001
|
||||
04368964 4E800020
|
||||
|
||||
(Ep1&2 USA v1.01) Play lobby (and event) music on Pioneer 2 also
|
||||
(Ep1&2 USA v1.1) Play lobby (and event) music on Pioneer 2 also
|
||||
0417E0F0 60000000
|
||||
|
||||
(Ep3 USA) Play lobby (and event) music in Morgue also
|
||||
@@ -11,12 +11,12 @@
|
||||
|
||||
(Ep3 USA) Skip white logo screens during startup
|
||||
0409D774 38000007
|
||||
(Episodes 1&2 USA v1.01) Skip white logo screens during startup
|
||||
(Episodes 1&2 USA v1.1) Skip white logo screens during startup
|
||||
0413F190 38000007
|
||||
|
||||
(Ep3 USA) Skip agreement prompts before online game
|
||||
041B50C8 38000003
|
||||
(Episodes 1&2 USA v1.01) Skip agreement prompt before online game
|
||||
(Episodes 1&2 USA v1.1) Skip agreement prompt before online game
|
||||
04327D80 38000003
|
||||
|
||||
(Ep3 USA) Disable rate limit for pressing A during loading screens
|
||||
@@ -155,7 +155,7 @@
|
||||
0400BEAC 7C0803A6
|
||||
0400BEB0 482E9FC0
|
||||
|
||||
(Episodes 1&2 USA v1.01) Press L for enemy debug; enable various other debug messages
|
||||
(Episodes 1&2 USA v1.1) Press L for enemy debug; enable various other debug messages
|
||||
040FD9D8 38600001 # Various enemy debug messages
|
||||
00153E53 00000001 # Poison fog debug 1
|
||||
00153E4B 00000001 # Poison fog debug 2
|
||||
@@ -264,3 +264,9 @@ Note: Without a TextEnglish.pr2/pr3 patch, the menu items for these sounds will
|
||||
0442B6D8 804D804E
|
||||
0442B6DC 804F802A
|
||||
0442B6E0 802C0000
|
||||
|
||||
(Ep3 NTE) Use English language files
|
||||
0408E414 38600001
|
||||
0408E448 38000001
|
||||
0408E44C 900DA62C
|
||||
0408E450 4E800020
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
This is a list of common shared serials for the Dreamcast version of the game.
|
||||
These serials are listed in decimal format for use with newserv and are not valid
|
||||
for use in the game itself.
|
||||
|
||||
If you are looking for a serial number to use for your Dreamcast copy of the game,
|
||||
please use newserv's DC serial number generator, or PSO Tool GUI at
|
||||
https://segaxtreme.net/resources/pso-tool-gui-by-razorx.224/
|
||||
|
||||
To allow the below shared serials to be used on your server by multiple users, use
|
||||
the below command (this works if the serial is already registered too):
|
||||
|
||||
add-license serial=<serial-number> flags=80000000
|
||||
|
||||
---
|
||||
|
||||
144243108
|
||||
297233506
|
||||
400533035
|
||||
446310728
|
||||
532044219
|
||||
1315107383
|
||||
1567634924
|
||||
1748940599
|
||||
2004318071
|
||||
2309795986
|
||||
3811232030
|
||||
3828776100
|
||||
4098754580
|
||||
@@ -0,0 +1,56 @@
|
||||
List of differences in Ep3 NTE compared to Final:
|
||||
- Assist cards
|
||||
- - Dice Fever sets dice to 6, not 5, and there is no Dice Fever +
|
||||
- - Rich + and Charity + also don't exist
|
||||
- - Powerless Rain, Brave Wind, Influence, Fix apply at a different phase of the attack procedure
|
||||
- - Tech Field applies to SCs only; on Final, it applies to SCs and creatures
|
||||
- - The denominator for Vengeance is 2; on Final it's 3
|
||||
- - AP Absorption logic is different (TODO: see apply_ap_and_tp_adjust_assists_to_attack)
|
||||
- - God Whim can use ANY assist card, not only the normally-obtainable ones, and it assigns all 4 players an assist, not only those who already had assists
|
||||
- - Inflation and Deflation only cause +1 or -1 cost per action, not per card
|
||||
- - Exchange can be set on other players
|
||||
- - The SET_MV condition overrides Snail Pace and Stamina completely
|
||||
- - Stamina sets your effective MV to 99 instead of 9
|
||||
- - Land Price is 2x instead of 1.5x
|
||||
- - Shuffle All and Shuffle Group don't respect deck shuffle/loop disabled settings
|
||||
- - Assist Vanish clears immediately, which means it can override other assists that happen at the same time (Trash 1, Empty Hand, etc.); in Final it happens after those
|
||||
- Abilities
|
||||
- - Rampage and Pierce are not player-specific; that is, if an attack has Rampage against one target, it has Rampage against all targets (this distinction is important for conditional abilities like Major Rampage and Heavy Pierce)
|
||||
- - Several abilities don't exist (TODO: Which ones? 0x64 and above?)
|
||||
- - Abnormal conditions do not have priorities like they do on Final
|
||||
- - Ability Trap seems incompletely implemented (or not implemented at all?)
|
||||
- - It appears that Major Pierce doesn't work against Arkz SCs, and this was fixed in Final
|
||||
- Conditions
|
||||
- - Anti-Abnormality doesn't prevent Freeze, Drop, Guom, or Curse
|
||||
- - SCs can get Freeze (they can't in Final)
|
||||
- - Bosses do not have automatic Anti-Abnormality
|
||||
- - Ability Trap prevents all abnormal conditions
|
||||
- Traps
|
||||
- - Traps trigger as soon as you move into their tile; on Final, they trigger at the end of the Move phase
|
||||
- - Traps may use any assist card, and this can be configured in the map definition (TODO: verify this last part)
|
||||
- Rules
|
||||
- - Dice Boost does not exist
|
||||
- - ATK and DEF dice ranges can be set independently, but there are only 7 options for each: 1-6, 1-1, 2-2, 3-3, 4-4, 5-5, 6-6
|
||||
- - There may be a bug when either die is set to 1-1 so you'll always get 2 instead (TODO: verify this)
|
||||
- COM interference is not implemented
|
||||
- The target's defense power is computed after checking if the attack is Resta instead of before
|
||||
- Card definitions
|
||||
- - The n21 and n22 arg2 conditions don't exist
|
||||
- - The p25 condition finds cards with Paralyze or Fly in NTE, vs. Aerial or Fly in Final; looks like a copy-paste error by Sega
|
||||
- - The p36 condition includes SCs and items on NTE, but only SCs on Final
|
||||
- - The p41 condition includes only your team on NTE, but both teams on Final
|
||||
- - Several tokens can't be used in expr fields: ddm, sat, edm, ldm, rdm, fdm, ndm, ehp
|
||||
- Missing rule checks
|
||||
- - Boss SCs can use items
|
||||
- - Move logic is different, which I didn't reverse-engineer because I was too lazy and couldn't imagine how it could be meaningfully different from Final
|
||||
- - Many values are not clamped (in Final, many are clamped to 0-9 or -99-99)
|
||||
- - You can set cards that aren't actually in your hand
|
||||
- - The game assumes team A always is at the top facing down and team B is always at the bottom facing up; if the map defines them to start on different edges or facing different directions than expected, the creature summoning areas will be wrong
|
||||
- - Character HP rule completely overrides the HP stat on SC cards; in Final, the SC's HP stat is added to Character HP
|
||||
- - Boss SCs are not exempt from this either; they have the same HP as normal SCs
|
||||
- - Cards marked as dead but not yet deleted can still attack
|
||||
- - The HOLD (6) and CANNOT_DEFEND (7) conditions don't actually stop you from defending
|
||||
- - There is no hard limit of 1000 turns for any battle
|
||||
- - In case of a draw, the first two tiebreaker rules (number of dead SCs and remaining SC HP) are skipped
|
||||
- The server cannot override EXP result values (thus post-battle EXP loss cannot be disabled)
|
||||
- Surprisingly, the code for PBs is identical between NTE and Final; it seems like they didn't spend any time on PBs after NTE at all
|
||||
@@ -81,7 +81,7 @@ blr
|
||||
|
||||
|
||||
|
||||
Ep1&2 v1.01 version of the above code
|
||||
Ep1&2 v1.1 version of the above code
|
||||
|
||||
send_D9
|
||||
./m68kdasm --assemble-ppc32 --ppc32 --start-address=801DA398
|
||||
|
||||
@@ -72,7 +72,7 @@ def write_patches_for_code(
|
||||
f.write("reloc0:\n")
|
||||
f.write(" .offsetof start\n")
|
||||
f.write("start:\n")
|
||||
f.write(" .include WriteCodeBlocks\n")
|
||||
f.write(" .include WriteCodeBlocksGC\n")
|
||||
for region in write_regions:
|
||||
f.write(
|
||||
f" # region @ {region.address:08X} ({len(region.data) * 4} bytes)\n"
|
||||
|
||||
+981
-958
File diff suppressed because it is too large
Load Diff
@@ -718,8 +718,8 @@ JP12------------- JP13------------- JP14------------- JP15------------- US10----
|
||||
80267EC8 4BDA57D8 80268B74 4BDA4B2C 80269BD0 4BDA3AD0 80269984 4BDA3D1C 80268874 4BDA4E2C 80268874 4BDA4E2C 80269C48 4BDA3A58 80269490 4BDA4210 b -0x0025B1D4 /* 8000D6A0 */
|
||||
|
||||
Improved Draw Distance of most objects
|
||||
DrawDistance
|
||||
*** name=DrawDistance
|
||||
Draw Distance
|
||||
*** name=Draw Distance
|
||||
*** desc=Extend the draw\ndistance of many\nobjects
|
||||
JP12------------- JP13------------- JP14------------- JP15------------- US10------------- US11------------- US12------------- EU--------------- DISASSEMBLY (US10)
|
||||
8000DFA0 C3C2C1F8 8000DFA0 C3C2C1F8 8000DFA0 C3C2C1F8 8000DFA0 C3C2C1F8 8000DFA0 C3C2C200 8000DFA0 C3C2C200 8000DFA0 C3C2C200 8000DFA0 C3C2C200 lfs f30, [r2 - 0x3E00]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
########################################################################
|
||||
Restore JP PSOBB original IME Behavior
|
||||
########################################################################
|
||||
|
||||
The default Tethealla client included a custom patch to disable the IME
|
||||
system in-game which allows you to type in Japanese (presumably to allow
|
||||
English versions of Windows to type properly)
|
||||
|
||||
However, if you plan to play PSOBB in it's original Japanese language it
|
||||
is recommended you remove this patch to restore the original functions
|
||||
|
||||
|
||||
Open a unpacked PSOBB.exe in a hex editor and:
|
||||
|
||||
FIND 9CC38E
|
||||
|
||||
REPLACE WITH A8838F
|
||||
|
||||
|
||||
Make sure to install Japanese Language Support in Windows 10/11 to enable
|
||||
the Japanese keyboard and IME.
|
||||
|
||||
If there's a problem, you can also use the Legacy IME by heading into the
|
||||
options of the Japanese Language settings and scroll down to the bottom of
|
||||
the page to enable Legacy IME Support to restore the original Pre-Windows 7
|
||||
IME system.
|
||||
|
||||
Last but not least, remember the default Tethealla client is the original
|
||||
Japanese client, so you don't need to apply any other special patch but this
|
||||
one and make sure you have the original Japanese files set in your data folder
|
||||
the game should start entirely in Japanese.
|
||||
|
||||
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Regular → Executable
BIN
Binary file not shown.
Regular → Executable
BIN
Binary file not shown.
Regular → Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -271,7 +271,7 @@ void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<cons
|
||||
case Version::DC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
PSOCommandHeaderDCV3 header;
|
||||
|
||||
+277
-72
@@ -14,6 +14,7 @@
|
||||
#include "Loggers.hh"
|
||||
#include "ProxyServer.hh"
|
||||
#include "ReceiveCommands.hh"
|
||||
#include "Revision.hh"
|
||||
#include "SendCommands.hh"
|
||||
#include "Server.hh"
|
||||
#include "StaticGameData.hh"
|
||||
@@ -37,11 +38,11 @@ private:
|
||||
std::string user_msg;
|
||||
};
|
||||
|
||||
static void check_license_flags(shared_ptr<Client> c, uint32_t mask) {
|
||||
static void check_license_flag(shared_ptr<Client> c, License::Flag flag) {
|
||||
if (!c->license) {
|
||||
throw precondition_failed("$C6You are not\nlogged in.");
|
||||
}
|
||||
if ((c->license->flags & mask) != mask) {
|
||||
if (!c->license->check_flag(flag)) {
|
||||
throw precondition_failed("$C6You do not have\npermission to\nrun this command.");
|
||||
}
|
||||
}
|
||||
@@ -65,19 +66,20 @@ static void check_is_ep3(shared_ptr<Client> c, bool is_ep3) {
|
||||
}
|
||||
|
||||
static void check_cheats_enabled(shared_ptr<Lobby> l, shared_ptr<Client> c) {
|
||||
if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) && !(c->license->flags & License::Flag::CHEAT_ANYWHERE)) {
|
||||
if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) && !c->license->check_flag(License::Flag::CHEAT_ANYWHERE)) {
|
||||
throw precondition_failed("$C6This command can\nonly be used in\ncheat mode.");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_cheats_allowed(shared_ptr<ServerState> s, shared_ptr<Client> c) {
|
||||
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && !(c->license->flags & License::Flag::CHEAT_ANYWHERE)) {
|
||||
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && !c->license->check_flag(License::Flag::CHEAT_ANYWHERE)) {
|
||||
throw precondition_failed("$C6Cheats are disabled\non this server.");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_cheats_allowed(shared_ptr<ServerState> s, shared_ptr<ProxyServer::LinkedSession> ses) {
|
||||
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && (!ses->license || !(ses->license->flags & License::Flag::CHEAT_ANYWHERE))) {
|
||||
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) &&
|
||||
(!ses->license || !ses->license->check_flag(License::Flag::CHEAT_ANYWHERE))) {
|
||||
throw precondition_failed("$C6Cheats are disabled\non this proxy.");
|
||||
}
|
||||
}
|
||||
@@ -91,6 +93,20 @@ static void check_is_leader(shared_ptr<Lobby> l, shared_ptr<Client> c) {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Message commands
|
||||
|
||||
static void server_command_server_info(shared_ptr<Client> c, const std::string&) {
|
||||
auto s = c->require_server_state();
|
||||
string uptime_str = format_duration(now() - s->creation_time);
|
||||
string build_date = format_time(BUILD_TIMESTAMP);
|
||||
send_text_message_printf(c,
|
||||
"Revision: $C6%s$C7\n$C6%s$C7\nUptime: $C6%s$C7\nLobbies: $C6%zu$C7\nClients: $C6%zu$C7(g) $C6%zu$C7(p)",
|
||||
GIT_REVISION_HASH,
|
||||
build_date.c_str(),
|
||||
uptime_str.c_str(),
|
||||
s->id_to_lobby.size(),
|
||||
s->channel_to_client.size(),
|
||||
s->proxy_server->num_sessions());
|
||||
}
|
||||
|
||||
static void server_command_lobby_info(shared_ptr<Client> c, const std::string&) {
|
||||
vector<string> lines;
|
||||
|
||||
@@ -204,9 +220,6 @@ static void proxy_command_lobby_info(shared_ptr<ProxyServer::LinkedSession> ses,
|
||||
}
|
||||
|
||||
vector<const char*> cheats_tokens;
|
||||
if (ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) {
|
||||
cheats_tokens.emplace_back("SWA");
|
||||
}
|
||||
if (ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
||||
cheats_tokens.emplace_back("HP");
|
||||
}
|
||||
@@ -219,14 +232,17 @@ static void proxy_command_lobby_info(shared_ptr<ProxyServer::LinkedSession> ses,
|
||||
}
|
||||
|
||||
vector<const char*> behaviors_tokens;
|
||||
if (ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) {
|
||||
behaviors_tokens.emplace_back("SWA");
|
||||
}
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_SAVE_FILES)) {
|
||||
behaviors_tokens.emplace_back("SAVE");
|
||||
behaviors_tokens.emplace_back("SF");
|
||||
}
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN)) {
|
||||
behaviors_tokens.emplace_back("SL");
|
||||
}
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_BLOCK_FUNCTION_CALLS)) {
|
||||
behaviors_tokens.emplace_back("BFC");
|
||||
behaviors_tokens.emplace_back("BF");
|
||||
}
|
||||
if (!behaviors_tokens.empty()) {
|
||||
msg += "\n$C7Flags: $C6";
|
||||
@@ -242,13 +258,13 @@ static void proxy_command_lobby_info(shared_ptr<ProxyServer::LinkedSession> ses,
|
||||
}
|
||||
|
||||
static void server_command_ax(shared_ptr<Client> c, const std::string& args) {
|
||||
check_license_flags(c, License::Flag::ANNOUNCE);
|
||||
check_license_flag(c, License::Flag::ANNOUNCE);
|
||||
ax_messages_log.info("%s", args.c_str());
|
||||
}
|
||||
|
||||
static void server_command_announce(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
check_license_flags(c, License::Flag::ANNOUNCE);
|
||||
check_license_flag(c, License::Flag::ANNOUNCE);
|
||||
send_text_message(s, args);
|
||||
}
|
||||
|
||||
@@ -265,7 +281,7 @@ static void proxy_command_arrow(shared_ptr<ProxyServer::LinkedSession> ses, cons
|
||||
}
|
||||
|
||||
static void server_command_debug(shared_ptr<Client> c, const std::string&) {
|
||||
check_license_flags(c, License::Flag::DEBUG);
|
||||
check_license_flag(c, License::Flag::DEBUG);
|
||||
c->config.toggle_flag(Client::Flag::DEBUG_ENABLED);
|
||||
send_text_message_printf(c, "Debug %s", (c->config.check_flag(Client::Flag::DEBUG_ENABLED) ? "enabled" : "disabled"));
|
||||
}
|
||||
@@ -280,7 +296,7 @@ static void server_command_quest(shared_ptr<Client> c, const std::string& args)
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
auto q = s->quest_index_for_version(effective_version)->get(stoul(args));
|
||||
auto q = s->quest_index(effective_version)->get(stoul(args));
|
||||
if (!q) {
|
||||
send_text_message(c, "$C6Quest not found");
|
||||
} else {
|
||||
@@ -360,7 +376,52 @@ static void proxy_command_qclear(shared_ptr<ProxyServer::LinkedSession> ses, con
|
||||
return proxy_command_qset_qclear(ses, args, false);
|
||||
}
|
||||
|
||||
static void server_command_qsync(shared_ptr<Client> c, const std::string& args) {
|
||||
static void server_command_qgread(shared_ptr<Client> c, const std::string& args) {
|
||||
uint8_t flag_num = stoul(args, nullptr, 0);
|
||||
const auto& flags = c->character()->quest_counters;
|
||||
if (flag_num >= flags.size()) {
|
||||
send_text_message_printf(c, "$C7Flag number must be\nless than %zu", flags.size());
|
||||
} else {
|
||||
send_text_message_printf(c, "$C7Quest counter %hhu\nhas value %" PRIu32,
|
||||
flag_num, flags[flag_num].load());
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_qgwrite(shared_ptr<Client> c, const std::string& args) {
|
||||
if (c->version() != Version::BB_V4) {
|
||||
send_text_message(c, "$C6This command can\nonly be used on BB");
|
||||
return;
|
||||
}
|
||||
if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message(c, "$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
||||
return;
|
||||
}
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game()) {
|
||||
send_text_message(c, "$C6This command cannot\nbe used in the lobby");
|
||||
return;
|
||||
}
|
||||
|
||||
auto tokens = split(args, ' ');
|
||||
if (tokens.size() != 2) {
|
||||
send_text_message(c, "$C6Incorrect number\nof arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t flag_num = stoul(tokens[0], nullptr, 0);
|
||||
uint32_t value = stoul(tokens[1], nullptr, 0);
|
||||
auto& flags = c->character()->quest_counters;
|
||||
if (flag_num >= flags.size()) {
|
||||
send_text_message_printf(c, "$C7Flag number must be\nless than %zu", flags.size());
|
||||
} else {
|
||||
c->character()->quest_counters[flag_num] = value;
|
||||
G_SetQuestCounter_BB_6xD2 cmd = {{0xD2, sizeof(G_SetQuestCounter_BB_6xD2) / 4, c->lobby_client_id}, flag_num, value};
|
||||
send_command_t(c, 0x60, 0x00, cmd);
|
||||
send_text_message_printf(c, "$C7Quest counter %hhu\nset to %" PRIu32, flag_num, value);
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_qsync_qsyncall(shared_ptr<Client> c, const std::string& args, bool send_to_lobby) {
|
||||
if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message(c, "$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
||||
return;
|
||||
@@ -389,10 +450,22 @@ static void server_command_qsync(shared_ptr<Client> c, const std::string& args)
|
||||
send_text_message(c, "$C6First argument must\nbe a register");
|
||||
return;
|
||||
}
|
||||
send_command_t(c, 0x60, 0x00, cmd);
|
||||
if (send_to_lobby) {
|
||||
send_command_t(l, 0x60, 0x00, cmd);
|
||||
} else {
|
||||
send_command_t(c, 0x60, 0x00, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
static void proxy_command_qsync(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
static void server_command_qsync(shared_ptr<Client> c, const std::string& args) {
|
||||
server_command_qsync_qsyncall(c, args, false);
|
||||
}
|
||||
|
||||
static void server_command_qsyncall(shared_ptr<Client> c, const std::string& args) {
|
||||
server_command_qsync_qsyncall(c, args, true);
|
||||
}
|
||||
|
||||
static void proxy_command_qsync_qsyncall(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args, bool send_to_lobby) {
|
||||
if (!ses->is_in_game) {
|
||||
send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby");
|
||||
return;
|
||||
@@ -417,6 +490,17 @@ static void proxy_command_qsync(shared_ptr<ProxyServer::LinkedSession> ses, cons
|
||||
return;
|
||||
}
|
||||
ses->client_channel.send(0x60, 0x00, cmd);
|
||||
if (send_to_lobby) {
|
||||
ses->server_channel.send(0x60, 0x00, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
static void proxy_command_qsync(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
proxy_command_qsync_qsyncall(ses, args, false);
|
||||
}
|
||||
|
||||
static void proxy_command_qsyncall(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
proxy_command_qsync_qsyncall(ses, args, true);
|
||||
}
|
||||
|
||||
static void server_command_qcall(shared_ptr<Client> c, const std::string& args) {
|
||||
@@ -460,17 +544,17 @@ static void server_command_show_material_counts(shared_ptr<Client> c, const std:
|
||||
}
|
||||
|
||||
static void server_command_auction(shared_ptr<Client> c, const std::string&) {
|
||||
check_license_flags(c, License::Flag::DEBUG);
|
||||
check_license_flag(c, License::Flag::DEBUG);
|
||||
auto l = c->require_lobby();
|
||||
if (l->is_game() && l->is_ep3()) {
|
||||
G_InitiateCardAuction_GC_Ep3_6xB5x42 cmd;
|
||||
G_InitiateCardAuction_Ep3_6xB5x42 cmd;
|
||||
cmd.header.sender_client_id = c->lobby_client_id;
|
||||
send_command_t(l, 0xC9, 0x00, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
static void proxy_command_auction(shared_ptr<ProxyServer::LinkedSession> ses, const std::string&) {
|
||||
G_InitiateCardAuction_GC_Ep3_6xB5x42 cmd;
|
||||
G_InitiateCardAuction_Ep3_6xB5x42 cmd;
|
||||
cmd.header.sender_client_id = ses->lobby_client_id;
|
||||
ses->client_channel.send(0xC9, 0x00, &cmd, sizeof(cmd));
|
||||
ses->server_channel.send(0xC9, 0x00, &cmd, sizeof(cmd));
|
||||
@@ -517,13 +601,15 @@ static void proxy_command_patch(shared_ptr<ProxyServer::LinkedSession> ses, cons
|
||||
};
|
||||
|
||||
auto send_version_detect_or_send_call = [args, ses, send_call]() {
|
||||
if (is_gc(ses->version()) &&
|
||||
bool is_gc = ::is_gc(ses->version());
|
||||
bool is_xb = (ses->version() == Version::XB_V3);
|
||||
if ((is_gc || is_xb) &&
|
||||
ses->config.specific_version == default_specific_version_for_version(ses->version(), -1)) {
|
||||
auto s = ses->require_server_state();
|
||||
send_function_call(
|
||||
ses->client_channel,
|
||||
ses->config,
|
||||
s->function_code_index->name_to_function.at("VersionDetect"));
|
||||
s->function_code_index->name_to_function.at(is_xb ? "VersionDetectXB" : "VersionDetectGC"));
|
||||
ses->function_call_return_handler_queue.emplace_back(send_call);
|
||||
} else {
|
||||
send_call(ses->config.specific_version, 0);
|
||||
@@ -552,7 +638,7 @@ static void server_command_persist(shared_ptr<Client> c, const std::string&) {
|
||||
auto l = c->require_lobby();
|
||||
if (l->check_flag(Lobby::Flag::DEFAULT)) {
|
||||
send_text_message(c, "$C6Default lobbies\ncannot be marked\ntemporary");
|
||||
} else if (!l->check_flag(Lobby::Flag::GAME)) {
|
||||
} else if (!l->is_game()) {
|
||||
send_text_message(c, "$C6Private lobbies\ncannot be marked\npersistent");
|
||||
} else if (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
|
||||
send_text_message(c, "$C6Games cannot be\npersistent if a\nquest has already\nbegun");
|
||||
@@ -624,7 +710,7 @@ static void proxy_command_get_player_card(shared_ptr<ProxyServer::LinkedSession>
|
||||
send_guild_card(ses->client_channel, p.guild_card_number, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class);
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
send_text_message_printf(ses->client_channel, "Error: %s", e.what());
|
||||
send_text_message(ses->client_channel, "Error: " + remove_color(e.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -660,17 +746,25 @@ static void server_command_cheat(shared_ptr<Client> c, const std::string&) {
|
||||
} else {
|
||||
l->toggle_flag(Lobby::Flag::CHEATS_ENABLED);
|
||||
send_text_message_printf(l, "Cheat mode %s", l->check_flag(Lobby::Flag::CHEATS_ENABLED) ? "enabled" : "disabled");
|
||||
|
||||
if (!c->license->check_flag(License::Flag::CHEAT_ANYWHERE)) {
|
||||
size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty);
|
||||
if (l->min_level < default_min_level) {
|
||||
l->min_level = default_min_level;
|
||||
send_text_message_printf(l, "$C6Minimum level set\nto %" PRIu32, l->min_level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_lobby_event(shared_ptr<Client> c, const std::string& args) {
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, false);
|
||||
check_license_flags(c, License::Flag::CHANGE_EVENT);
|
||||
check_license_flag(c, License::Flag::CHANGE_EVENT);
|
||||
|
||||
uint8_t new_event = event_for_name(args);
|
||||
if (new_event == 0xFF) {
|
||||
send_text_message(c, "$C6No such lobby event.");
|
||||
send_text_message(c, "$C6No such lobby event");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -684,7 +778,7 @@ static void proxy_command_lobby_event(shared_ptr<ProxyServer::LinkedSession> ses
|
||||
} else {
|
||||
uint8_t new_event = event_for_name(args);
|
||||
if (new_event == 0xFF) {
|
||||
send_text_message(ses->client_channel, "$C6No such lobby event.");
|
||||
send_text_message(ses->client_channel, "$C6No such lobby event");
|
||||
} else {
|
||||
ses->config.override_lobby_event = new_event;
|
||||
if (!is_v1_or_v2(ses->version())) {
|
||||
@@ -695,11 +789,11 @@ static void proxy_command_lobby_event(shared_ptr<ProxyServer::LinkedSession> ses
|
||||
}
|
||||
|
||||
static void server_command_lobby_event_all(shared_ptr<Client> c, const std::string& args) {
|
||||
check_license_flags(c, License::Flag::CHANGE_EVENT);
|
||||
check_license_flag(c, License::Flag::CHANGE_EVENT);
|
||||
|
||||
uint8_t new_event = event_for_name(args);
|
||||
if (new_event == 0xFF) {
|
||||
send_text_message(c, "$C6No such lobby event.");
|
||||
send_text_message(c, "$C6No such lobby event");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -899,12 +993,13 @@ static void server_command_password(shared_ptr<Client> c, const std::string& arg
|
||||
check_is_leader(l, c);
|
||||
|
||||
if (!args[0]) {
|
||||
l->password[0] = 0;
|
||||
l->password.clear();
|
||||
send_text_message(l, "$C6Game unlocked");
|
||||
|
||||
} else {
|
||||
l->password = args;
|
||||
send_text_message_printf(l, "$C6Game password:\n%s", l->password.c_str());
|
||||
string escaped = remove_color(l->password);
|
||||
send_text_message_printf(l, "$C6Game password:\n%s", escaped.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,7 +1041,19 @@ static void server_command_min_level(shared_ptr<Client> c, const std::string& ar
|
||||
check_is_game(l, true);
|
||||
check_is_leader(l, c);
|
||||
|
||||
l->min_level = stoull(args) - 1;
|
||||
size_t new_min_level = stoull(args) - 1;
|
||||
|
||||
auto s = c->require_server_state();
|
||||
bool cheats_allowed = (l->check_flag(Lobby::Flag::CHEATS_ENABLED) || c->license->check_flag(License::Flag::CHEAT_ANYWHERE));
|
||||
if (!cheats_allowed) {
|
||||
size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty);
|
||||
if (new_min_level < default_min_level) {
|
||||
send_text_message_printf(c, "$C6Cannot set minimum\nlevel below %zu", default_min_level + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
l->min_level = new_min_level;
|
||||
send_text_message_printf(l, "$C6Minimum level set\nto %" PRIu32, l->min_level + 1);
|
||||
}
|
||||
|
||||
@@ -978,35 +1085,33 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
|
||||
throw precondition_failed("$C6This command cannot\nbe used for your\nversion of PSO.");
|
||||
}
|
||||
|
||||
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && !(c->license->flags & License::Flag::CHEAT_ANYWHERE)) {
|
||||
send_text_message(l, "$C6Cheats are disabled\non this server");
|
||||
return;
|
||||
}
|
||||
bool cheats_allowed = ((s->cheat_mode_behavior != ServerState::BehaviorSwitch::OFF) ||
|
||||
c->license->check_flag(License::Flag::CHEAT_ANYWHERE));
|
||||
|
||||
string encoded_args = tolower(args);
|
||||
vector<string> tokens = split(encoded_args, ' ');
|
||||
|
||||
try {
|
||||
auto p = c->character();
|
||||
if (tokens.at(0) == "atp") {
|
||||
if (tokens.at(0) == "atp" && cheats_allowed) {
|
||||
p->disp.stats.char_stats.atp = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "mst") {
|
||||
} else if (tokens.at(0) == "mst" && cheats_allowed) {
|
||||
p->disp.stats.char_stats.mst = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "evp") {
|
||||
} else if (tokens.at(0) == "evp" && cheats_allowed) {
|
||||
p->disp.stats.char_stats.evp = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "hp") {
|
||||
} else if (tokens.at(0) == "hp" && cheats_allowed) {
|
||||
p->disp.stats.char_stats.hp = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "dfp") {
|
||||
} else if (tokens.at(0) == "dfp" && cheats_allowed) {
|
||||
p->disp.stats.char_stats.dfp = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "ata") {
|
||||
} else if (tokens.at(0) == "ata" && cheats_allowed) {
|
||||
p->disp.stats.char_stats.ata = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "lck") {
|
||||
} else if (tokens.at(0) == "lck" && cheats_allowed) {
|
||||
p->disp.stats.char_stats.lck = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "meseta") {
|
||||
} else if (tokens.at(0) == "meseta" && cheats_allowed) {
|
||||
p->disp.stats.meseta = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "exp") {
|
||||
} else if (tokens.at(0) == "exp" && cheats_allowed) {
|
||||
p->disp.stats.experience = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "level") {
|
||||
} else if (tokens.at(0) == "level" && cheats_allowed) {
|
||||
uint32_t level = stoul(tokens.at(1)) - 1;
|
||||
p->disp.stats.reset_to_base(p->disp.visual.char_class, s->level_table);
|
||||
p->disp.stats.advance_to_level(p->disp.visual.char_class, level, s->level_table);
|
||||
@@ -1014,7 +1119,7 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
|
||||
uint32_t new_color;
|
||||
sscanf(tokens.at(1).c_str(), "%8X", &new_color);
|
||||
p->disp.visual.name_color = new_color;
|
||||
} else if (tokens.at(0) == "secid") {
|
||||
} else if (tokens.at(0) == "secid" && cheats_allowed) {
|
||||
uint8_t secid = section_id_for_name(tokens.at(1));
|
||||
if (secid == 0xFF) {
|
||||
send_text_message(c, "$C6No such section ID");
|
||||
@@ -1040,7 +1145,7 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
|
||||
p->disp.visual.extra_model = npc;
|
||||
p->disp.visual.validation_flags |= 0x02;
|
||||
}
|
||||
} else if (tokens.at(0) == "tech") {
|
||||
} else if (tokens.at(0) == "tech" && cheats_allowed) {
|
||||
uint8_t level = stoul(tokens.at(2)) - 1;
|
||||
if (tokens.at(1) == "all") {
|
||||
for (size_t x = 0; x < 0x14; x++) {
|
||||
@@ -1098,7 +1203,8 @@ static void server_command_change_bank(shared_ptr<Client> c, const std::string&
|
||||
} else if (new_char_index <= 4) {
|
||||
c->use_character_bank(new_char_index - 1);
|
||||
auto bp = c->current_bank_character();
|
||||
auto name = bp->disp.name.decode(c->language());
|
||||
|
||||
auto name = escape_player_name(bp->disp.name.decode(c->language()));
|
||||
send_text_message_printf(c, "$C6Using %s\'s bank (%zu)", name.c_str(), new_char_index);
|
||||
} else {
|
||||
throw runtime_error("invalid bank number");
|
||||
@@ -1157,6 +1263,10 @@ static void server_command_bbchar(shared_ptr<Client> c, const std::string& args)
|
||||
}
|
||||
|
||||
static void server_command_savechar(shared_ptr<Client> c, const std::string& args) {
|
||||
if (c->license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
send_text_message(c, "$C7This command cannot\nbe used on a shared\nserial number");
|
||||
return;
|
||||
}
|
||||
server_command_bbchar_savechar(c, args, false);
|
||||
}
|
||||
|
||||
@@ -1165,6 +1275,10 @@ static void server_command_loadchar(shared_ptr<Client> c, const std::string& arg
|
||||
send_text_message(c, "$C7This command can only\nbe used on v1 or v2");
|
||||
return;
|
||||
}
|
||||
if (c->license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
send_text_message(c, "$C7This command cannot\nbe used on a shared\nserial number");
|
||||
return;
|
||||
}
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, false);
|
||||
|
||||
@@ -1186,7 +1300,7 @@ static void server_command_save(shared_ptr<Client> c, const std::string&) {
|
||||
c->save_all();
|
||||
send_text_message(c, "All data saved");
|
||||
} catch (const exception& e) {
|
||||
send_text_message_printf(c, "Can\'t save data:\n%s", e.what());
|
||||
send_text_message(c, "Can\'t save data:\n" + remove_color(e.what()));
|
||||
}
|
||||
c->reschedule_save_game_data_event();
|
||||
}
|
||||
@@ -1197,7 +1311,7 @@ static void server_command_save(shared_ptr<Client> c, const std::string&) {
|
||||
static string name_for_client(shared_ptr<Client> c) {
|
||||
auto player = c->character(false);
|
||||
if (player.get()) {
|
||||
return player->disp.name.decode(player->inventory.language);
|
||||
return escape_player_name(player->disp.name.decode(player->inventory.language));
|
||||
}
|
||||
|
||||
if (c->license.get()) {
|
||||
@@ -1210,7 +1324,7 @@ static string name_for_client(shared_ptr<Client> c) {
|
||||
static void server_command_silence(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_license_flags(c, License::Flag::SILENCE_USER);
|
||||
check_license_flag(c, License::Flag::SILENCE_USER);
|
||||
|
||||
auto target = s->find_client(&args);
|
||||
if (!target->license) {
|
||||
@@ -1219,7 +1333,7 @@ static void server_command_silence(shared_ptr<Client> c, const std::string& args
|
||||
return;
|
||||
}
|
||||
|
||||
if (target->license->flags & License::Flag::MODERATOR) {
|
||||
if (target->license->check_flag(License::Flag::SILENCE_USER)) {
|
||||
send_text_message(c, "$C6You do not have\nsufficient privileges.");
|
||||
return;
|
||||
}
|
||||
@@ -1233,7 +1347,7 @@ static void server_command_silence(shared_ptr<Client> c, const std::string& args
|
||||
static void server_command_kick(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_license_flags(c, License::Flag::KICK_USER);
|
||||
check_license_flag(c, License::Flag::KICK_USER);
|
||||
|
||||
auto target = s->find_client(&args);
|
||||
if (!target->license) {
|
||||
@@ -1242,7 +1356,7 @@ static void server_command_kick(shared_ptr<Client> c, const std::string& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target->license->flags & License::Flag::MODERATOR) {
|
||||
if (target->license->check_flag(License::Flag::KICK_USER)) {
|
||||
send_text_message(c, "$C6You do not have\nsufficient privileges.");
|
||||
return;
|
||||
}
|
||||
@@ -1256,7 +1370,7 @@ static void server_command_kick(shared_ptr<Client> c, const std::string& args) {
|
||||
static void server_command_ban(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_license_flags(c, License::Flag::BAN_USER);
|
||||
check_license_flag(c, License::Flag::BAN_USER);
|
||||
|
||||
size_t space_pos = args.find(' ');
|
||||
if (space_pos == string::npos) {
|
||||
@@ -1272,7 +1386,7 @@ static void server_command_ban(shared_ptr<Client> c, const std::string& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target->license->flags & License::Flag::BAN_USER) {
|
||||
if (target->license->check_flag(License::Flag::BAN_USER)) {
|
||||
send_text_message(c, "$C6You do not have\nsufficient privileges.");
|
||||
return;
|
||||
}
|
||||
@@ -1441,6 +1555,32 @@ static void proxy_command_song(shared_ptr<ProxyServer::LinkedSession> ses, const
|
||||
send_ep3_change_music(ses->client_channel, song);
|
||||
}
|
||||
|
||||
static void command_item_notifs(Channel& ch, Client::Config& config, const std::string& args) {
|
||||
if (args == "all" || args == "on") {
|
||||
config.clear_flag(Client::Flag::RARE_DROP_NOTIFICATIONS_ENABLED);
|
||||
config.set_flag(Client::Flag::ALL_DROP_NOTIFICATIONS_ENABLED);
|
||||
send_text_message_printf(ch, "$C6Notifications enabled\nfor all items");
|
||||
} else if (args == "rare" || args == "rares") {
|
||||
config.set_flag(Client::Flag::RARE_DROP_NOTIFICATIONS_ENABLED);
|
||||
config.clear_flag(Client::Flag::ALL_DROP_NOTIFICATIONS_ENABLED);
|
||||
send_text_message_printf(ch, "$C6Notifications enabled\nfor rare items only");
|
||||
} else if (args == "none" || args == "off") {
|
||||
config.clear_flag(Client::Flag::RARE_DROP_NOTIFICATIONS_ENABLED);
|
||||
config.clear_flag(Client::Flag::ALL_DROP_NOTIFICATIONS_ENABLED);
|
||||
send_text_message_printf(ch, "$C6Notifications disabled\nfor all items");
|
||||
} else {
|
||||
send_text_message_printf(ch, "$C6You must specify\n$C6off$C7, $C6rare$C7, or $C6on$C7");
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_item_notifs(shared_ptr<Client> c, const std::string& args) {
|
||||
command_item_notifs(c->channel, c->config, args);
|
||||
}
|
||||
|
||||
static void proxy_command_item_notifs(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
command_item_notifs(ses->client_channel, ses->config, args);
|
||||
}
|
||||
|
||||
static void server_command_infinite_hp(shared_ptr<Client> c, const std::string&) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
@@ -1448,15 +1588,23 @@ static void server_command_infinite_hp(shared_ptr<Client> c, const std::string&)
|
||||
check_cheats_enabled(l, c);
|
||||
|
||||
c->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED);
|
||||
send_text_message_printf(c, "$C6Infinite HP %s", c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED) ? "enabled" : "disabled");
|
||||
bool enabled = c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED);
|
||||
send_text_message_printf(c, "$C6Infinite HP %s", enabled ? "enabled" : "disabled");
|
||||
if (enabled && l->is_game() && is_v1_or_v2(c->version())) {
|
||||
send_remove_conditions(c);
|
||||
}
|
||||
}
|
||||
|
||||
static void proxy_command_infinite_hp(shared_ptr<ProxyServer::LinkedSession> ses, const std::string&) {
|
||||
auto s = ses->require_server_state();
|
||||
check_cheats_allowed(s, ses);
|
||||
ses->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED);
|
||||
send_text_message_printf(ses->client_channel, "$C6Infinite HP %s",
|
||||
ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED) ? "enabled" : "disabled");
|
||||
bool enabled = ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED);
|
||||
send_text_message_printf(ses->client_channel, "$C6Infinite HP %s", enabled ? "enabled" : "disabled");
|
||||
if (enabled && ses->is_in_game && is_v1_or_v2(ses->version())) {
|
||||
send_remove_conditions(ses->client_channel, ses->lobby_client_id);
|
||||
send_remove_conditions(ses->server_channel, ses->lobby_client_id);
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_infinite_tp(shared_ptr<Client> c, const std::string&) {
|
||||
@@ -1481,7 +1629,6 @@ static void server_command_switch_assist(shared_ptr<Client> c, const std::string
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l, c);
|
||||
|
||||
c->config.toggle_flag(Client::Flag::SWITCH_ASSIST_ENABLED);
|
||||
send_text_message_printf(c, "$C6Switch assist %s",
|
||||
@@ -1490,7 +1637,6 @@ static void server_command_switch_assist(shared_ptr<Client> c, const std::string
|
||||
|
||||
static void proxy_command_switch_assist(shared_ptr<ProxyServer::LinkedSession> ses, const std::string&) {
|
||||
auto s = ses->require_server_state();
|
||||
check_cheats_allowed(s, ses);
|
||||
ses->config.toggle_flag(Client::Flag::SWITCH_ASSIST_ENABLED);
|
||||
send_text_message_printf(ses->client_channel, "$C6Switch assist %s",
|
||||
ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? "enabled" : "disabled");
|
||||
@@ -1568,7 +1714,7 @@ static void server_command_item(shared_ptr<Client> c, const std::string& args) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l, c);
|
||||
|
||||
ItemData item = s->item_name_index->parse_item_description(c->version(), args);
|
||||
ItemData item = s->parse_item_description(c->version(), args);
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
|
||||
if ((l->drop_mode == Lobby::DropMode::SERVER_PRIVATE) || (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE)) {
|
||||
@@ -1601,8 +1747,8 @@ static void proxy_command_item(shared_ptr<ProxyServer::LinkedSession> ses, const
|
||||
|
||||
bool set_drop = (!args.empty() && (args[0] == '!'));
|
||||
|
||||
ItemData item = s->item_name_index->parse_item_description(ses->version(), (set_drop ? args.substr(1) : args));
|
||||
item.id = random_object<uint32_t>();
|
||||
ItemData item = s->parse_item_description(ses->version(), (set_drop ? args.substr(1) : args));
|
||||
item.id = random_object<uint32_t>() | 0x80000000;
|
||||
|
||||
if (set_drop) {
|
||||
ses->next_drop_item = item;
|
||||
@@ -1688,6 +1834,10 @@ static void server_command_ep3_set_def_dice_range(shared_ptr<Client> c, const st
|
||||
send_text_message(c, "$C6Battle is already\nin progress");
|
||||
return;
|
||||
}
|
||||
if (l->tournament_match) {
|
||||
send_text_message(c, "$C6Cannot override\nDEF range in a\ntournament");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.empty()) {
|
||||
l->ep3_server->map_and_rules->rules.def_dice_range = 0;
|
||||
@@ -1719,6 +1869,58 @@ static void server_command_ep3_set_def_dice_range(shared_ptr<Client> c, const st
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_ep3_replace_assist_card(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, true);
|
||||
check_is_ep3(c, true);
|
||||
check_cheats_enabled(l, c);
|
||||
|
||||
if (l->episode != Episode::EP3) {
|
||||
throw logic_error("non-Ep3 client in Ep3 game");
|
||||
}
|
||||
if (!l->ep3_server) {
|
||||
send_text_message(c, "$C6Episode 3 server\nis not initialized");
|
||||
return;
|
||||
}
|
||||
if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) {
|
||||
send_text_message(c, "$C6Battle has not\nyet begun");
|
||||
return;
|
||||
}
|
||||
if (args.empty()) {
|
||||
send_text_message(c, "$C6Missing arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t client_id;
|
||||
string card_name;
|
||||
if (isdigit(args[0])) {
|
||||
auto tokens = split(args, ' ', 1);
|
||||
client_id = stoul(tokens.at(0), nullptr, 0) - 1;
|
||||
card_name = tokens.at(1);
|
||||
} else {
|
||||
client_id = c->lobby_client_id;
|
||||
card_name = args;
|
||||
}
|
||||
if (client_id >= 4) {
|
||||
send_text_message(c, "$C6Invalid client ID");
|
||||
return;
|
||||
}
|
||||
|
||||
shared_ptr<const Episode3::CardIndex::CardEntry> ce;
|
||||
try {
|
||||
ce = l->ep3_server->options.card_index->definition_for_name_normalized(card_name);
|
||||
} catch (const out_of_range&) {
|
||||
send_text_message(c, "$C6Card not found");
|
||||
return;
|
||||
}
|
||||
if (ce->def.type != Episode3::CardType::ASSIST) {
|
||||
send_text_message(c, "$C6Card is not an\nAssist card");
|
||||
return;
|
||||
}
|
||||
l->ep3_server->force_replace_assist_card(client_id, ce->def.card_id);
|
||||
}
|
||||
|
||||
static void server_command_ep3_unset_field_character(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
@@ -1757,7 +1959,7 @@ static void server_command_surrender(shared_ptr<Client> c, const std::string&) {
|
||||
send_text_message(c, "$C6Battle has not\nyet started");
|
||||
return;
|
||||
}
|
||||
const string& name = c->character()->disp.name.decode(c->language());
|
||||
string name = remove_color(c->character()->disp.name.decode(c->language()));
|
||||
send_text_message_printf(l, "$C6%s has\nsurrendered", name.c_str());
|
||||
for (const auto& watcher_l : l->watcher_lobbies) {
|
||||
send_text_message_printf(watcher_l, "$C6%s has\nsurrendered", name.c_str());
|
||||
@@ -1780,10 +1982,7 @@ static void server_command_get_ep3_battle_stat(shared_ptr<Client> c, const std::
|
||||
send_text_message(c, "$C6Battle has not\nyet started");
|
||||
return;
|
||||
}
|
||||
if (c->lobby_client_id >= 4) {
|
||||
throw logic_error("client ID is too large");
|
||||
}
|
||||
auto ps = l->ep3_server->player_states[c->lobby_client_id];
|
||||
auto ps = l->ep3_server->player_states.at(c->lobby_client_id);
|
||||
if (!ps) {
|
||||
send_text_message(c, "$C6Player is missing");
|
||||
return;
|
||||
@@ -1879,6 +2078,7 @@ static const unordered_map<string, ChatCommandDefinition> chat_commands({
|
||||
{"$inftime", {server_command_ep3_infinite_time, nullptr}},
|
||||
{"$inftp", {server_command_infinite_tp, proxy_command_infinite_tp}},
|
||||
{"$item", {server_command_item, proxy_command_item}},
|
||||
{"$itemnotifs", {server_command_item_notifs, proxy_command_item_notifs}},
|
||||
{"$i", {server_command_item, proxy_command_item}},
|
||||
{"$kick", {server_command_kick, nullptr}},
|
||||
{"$li", {server_command_lobby_info, proxy_command_lobby_info}},
|
||||
@@ -1897,8 +2097,11 @@ static const unordered_map<string, ChatCommandDefinition> chat_commands({
|
||||
{"$qcall", {server_command_qcall, proxy_command_qcall}},
|
||||
{"$qcheck", {server_command_qcheck, nullptr}},
|
||||
{"$qclear", {server_command_qclear, proxy_command_qclear}},
|
||||
{"$qgread", {server_command_qgread, nullptr}},
|
||||
{"$qgwrite", {server_command_qgwrite, nullptr}},
|
||||
{"$qset", {server_command_qset, proxy_command_qset}},
|
||||
{"$qsync", {server_command_qsync, proxy_command_qsync}},
|
||||
{"$qsyncall", {server_command_qsyncall, proxy_command_qsyncall}},
|
||||
{"$quest", {server_command_quest, nullptr}},
|
||||
{"$rand", {server_command_rand, proxy_command_rand}},
|
||||
{"$save", {server_command_save, nullptr}},
|
||||
@@ -1906,6 +2109,8 @@ static const unordered_map<string, ChatCommandDefinition> chat_commands({
|
||||
{"$saverec", {server_command_saverec, nullptr}},
|
||||
{"$sc", {server_command_send_client, proxy_command_send_client}},
|
||||
{"$secid", {server_command_secid, proxy_command_secid}},
|
||||
{"$setassist", {server_command_ep3_replace_assist_card, nullptr}},
|
||||
{"$si", {server_command_server_info, nullptr}},
|
||||
{"$silence", {server_command_silence, nullptr}},
|
||||
{"$song", {server_command_song, proxy_command_song}},
|
||||
{"$spec", {server_command_toggle_spectator_flag, nullptr}},
|
||||
@@ -1961,7 +2166,7 @@ void on_chat_command(std::shared_ptr<Client> c, const std::string& text) {
|
||||
} catch (const precondition_failed& e) {
|
||||
send_text_message(c, e.what());
|
||||
} catch (const exception& e) {
|
||||
send_text_message_printf(c, "$C6Failed:\n%s", e.what());
|
||||
send_text_message(c, "$C6Failed:\n" + remove_color(e.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1985,7 +2190,7 @@ void on_chat_command(shared_ptr<ProxyServer::LinkedSession> ses, const std::stri
|
||||
} catch (const precondition_failed& e) {
|
||||
send_text_message(ses->client_channel, e.what());
|
||||
} catch (const exception& e) {
|
||||
send_text_message_printf(ses->client_channel, "$C6Failed:\n%s", e.what());
|
||||
send_text_message(ses->client_channel, "$C6Failed:\n" + remove_color(e.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ const vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
|
||||
case Version::GC_V3:
|
||||
case Version::XB_V3:
|
||||
return (choice_id == 0x0004);
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
return (choice_id == 0x0005);
|
||||
case Version::BB_V4:
|
||||
|
||||
+113
-25
@@ -23,7 +23,15 @@ static atomic<uint64_t> next_id(1);
|
||||
|
||||
void Client::Config::set_flags_for_version(Version version, int64_t sub_version) {
|
||||
this->set_flag(Flag::PROXY_CHAT_COMMANDS_ENABLED);
|
||||
this->set_flag(Flag::PROXY_CHAT_FILTER_ENABLED);
|
||||
|
||||
// BB shares some sub_version values with GC Episode 3, so we handle it
|
||||
// separately first.
|
||||
if (version == Version::BB_V4) {
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::SAVE_ENABLED);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (sub_version) {
|
||||
case -1: // Initial check (before sub_version recognition)
|
||||
@@ -48,7 +56,7 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
break;
|
||||
case Version::XB_V3:
|
||||
@@ -56,11 +64,6 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
break;
|
||||
case Version::BB_V4:
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::SAVE_ENABLED);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid game version");
|
||||
}
|
||||
@@ -88,9 +91,9 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
break;
|
||||
case 0x30: // GC Ep1&2 GameJam demo, GC Ep1&2 Trial Edition, GC Ep1&2 JP v1.02, at least one version of XB
|
||||
case 0x31: // GC Ep1&2 US v1.00, GC US v1.01, XB US
|
||||
case 0x34: // GC Ep1&2 JP v1.03
|
||||
case 0x30: // GC Ep1&2 GameJam demo, GC Ep1&2 Trial Edition, GC Ep1&2 JP v1.2, at least one version of XB
|
||||
case 0x31: // GC Ep1&2 US v1.0, GC US v1.1, XB US
|
||||
case 0x34: // GC Ep1&2 JP v1.3
|
||||
// In the case of GC Trial Edition, the IS_GC_TRIAL_EDITION flag is
|
||||
// already set when we get here (because the client has used V2 encryption
|
||||
// instead of V3)
|
||||
@@ -99,17 +102,17 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
case 0x33: // GC Ep1&2 EU 60Hz
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
break;
|
||||
case 0x35: // GC Ep1&2 JP v1.04 (Plus)
|
||||
case 0x35: // GC Ep1&2 JP v1.4 (Plus)
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
this->set_flag(Flag::ENCRYPTED_SEND_FUNCTION_CALL);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
break;
|
||||
case 0x36: // GC Ep1&2 US v1.02 (Plus)
|
||||
case 0x39: // GC Ep1&2 JP v1.05 (Plus)
|
||||
case 0x36: // GC Ep1&2 US v1.2 (Plus)
|
||||
case 0x39: // GC Ep1&2 JP v1.5 (Plus)
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
this->set_flag(Flag::NO_SEND_FUNCTION_CALL);
|
||||
break;
|
||||
case 0x40: // GC Ep3 JP and Trial Edition
|
||||
case 0x40: // GC Ep3 JP and Trial Edition (and BB)
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
this->set_flag(Flag::ENCRYPTED_SEND_FUNCTION_CALL);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
@@ -117,7 +120,7 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
// instead look at header.flag in the 61 command and set the
|
||||
// IS_EP3_TRIAL_EDITION flag there.
|
||||
break;
|
||||
case 0x41: // GC Ep3 US
|
||||
case 0x41: // GC Ep3 US (and BB)
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
this->set_flag(Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
@@ -132,6 +135,18 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
}
|
||||
}
|
||||
|
||||
bool Client::Config::should_update_vs(const Config& other) const {
|
||||
constexpr uint64_t mask = static_cast<uint64_t>(Flag::CLIENT_SIDE_MASK);
|
||||
return ((this->enabled_flags ^ other.enabled_flags) & mask) ||
|
||||
(this->specific_version != other.specific_version) ||
|
||||
(this->override_random_seed != other.override_random_seed) ||
|
||||
(this->override_section_id != other.override_section_id) ||
|
||||
(this->override_lobby_event != other.override_lobby_event) ||
|
||||
(this->override_lobby_number != other.override_lobby_number) ||
|
||||
(this->proxy_destination_address != other.proxy_destination_address) ||
|
||||
(this->proxy_destination_port != other.proxy_destination_port);
|
||||
}
|
||||
|
||||
Client::Client(
|
||||
shared_ptr<Server> server,
|
||||
struct bufferevent* bev,
|
||||
@@ -180,6 +195,10 @@ Client::Client(
|
||||
external_bank_character_index(-1),
|
||||
last_play_time_update(0) {
|
||||
this->config.set_flags_for_version(version, -1);
|
||||
auto s = server->get_state();
|
||||
if (is_v1_or_v2(this->version()) ? s->default_rare_notifs_enabled_v1_v2 : s->default_rare_notifs_enabled_v3_v4) {
|
||||
this->config.set_flag(Flag::RARE_DROP_NOTIFICATIONS_ENABLED);
|
||||
}
|
||||
this->config.specific_version = default_specific_version_for_version(version, -1);
|
||||
|
||||
this->last_switch_enabled_command.header.subcommand = 0;
|
||||
@@ -191,7 +210,8 @@ Client::Client(
|
||||
// Don't print data sent to patch clients to the logs. The patch server
|
||||
// protocol is fully understood and data logs for patch clients are generally
|
||||
// more annoying than helpful at this point.
|
||||
if ((this->channel.version == Version::PC_PATCH) || (this->channel.version == Version::BB_PATCH)) {
|
||||
if ((s->hide_download_commands) &&
|
||||
((this->channel.version == Version::PC_PATCH) || (this->channel.version == Version::BB_PATCH))) {
|
||||
this->channel.terminal_recv_color = TerminalFormat::END;
|
||||
this->channel.terminal_send_color = TerminalFormat::END;
|
||||
}
|
||||
@@ -221,9 +241,10 @@ void Client::reschedule_save_game_data_event() {
|
||||
}
|
||||
|
||||
void Client::reschedule_ping_and_timeout_events() {
|
||||
struct timeval ping_tv = usecs_to_timeval(30000000); // 30 seconds
|
||||
auto s = this->require_server_state();
|
||||
struct timeval ping_tv = usecs_to_timeval(s->client_ping_interval_usecs);
|
||||
event_add(this->send_ping_event.get(), &ping_tv);
|
||||
struct timeval idle_tv = usecs_to_timeval(60000000); // 1 minute
|
||||
struct timeval idle_tv = usecs_to_timeval(s->client_idle_timeout_usecs);
|
||||
event_add(this->idle_timeout_event.get(), &idle_tv);
|
||||
}
|
||||
|
||||
@@ -239,6 +260,24 @@ void Client::set_license(shared_ptr<License> l) {
|
||||
this->license = l;
|
||||
}
|
||||
|
||||
void Client::convert_license_to_temporary_if_nte() {
|
||||
// If the session is a prototype version and the license was created and we
|
||||
// should use a temporary license instead, delete the permanent license and
|
||||
// replace it with a temporary license.
|
||||
auto s = this->require_server_state();
|
||||
if (s->use_temp_licenses_for_prototypes &&
|
||||
this->config.check_flag(Client::Flag::LICENSE_WAS_CREATED) &&
|
||||
is_any_nte(this->version())) {
|
||||
this->log.info("Client is a prototype version and the license was created during this session; converting permanent license to temporary license");
|
||||
s->license_index->remove(this->license->serial_number);
|
||||
auto new_l = s->license_index->create_temporary_license();
|
||||
this->license->delete_file();
|
||||
*new_l = std::move(*this->license);
|
||||
this->set_license(new_l);
|
||||
this->config.clear_flag(Client::Flag::LICENSE_WAS_CREATED);
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<ServerState> Client::require_server_state() const {
|
||||
auto server = this->server.lock();
|
||||
if (!server) {
|
||||
@@ -296,36 +335,42 @@ shared_ptr<const TeamIndex::Team> Client::team() const {
|
||||
return team;
|
||||
}
|
||||
|
||||
bool Client::can_see_quest(shared_ptr<const Quest> q, uint8_t difficulty, size_t num_players) const {
|
||||
if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
bool Client::can_see_quest(shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players) const {
|
||||
if (this->license && this->license->check_flag(License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
return true;
|
||||
}
|
||||
if (!q->available_expression) {
|
||||
return true;
|
||||
}
|
||||
auto p = this->character();
|
||||
string expr = q->available_expression->str();
|
||||
QuestAvailabilityExpression::Env env = {
|
||||
.flags = &this->character()->quest_flags.data.at(difficulty),
|
||||
.flags = &p->quest_flags.data.at(difficulty),
|
||||
.challenge_records = &p->challenge_records,
|
||||
.team = this->team(),
|
||||
.num_players = num_players,
|
||||
.event = event,
|
||||
};
|
||||
int64_t ret = q->available_expression->evaluate(env);
|
||||
this->log.info("Evaluated quest availability expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE");
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool Client::can_play_quest(shared_ptr<const Quest> q, uint8_t difficulty, size_t num_players) const {
|
||||
if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
bool Client::can_play_quest(shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players) const {
|
||||
if (this->license && this->license->check_flag(License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
return true;
|
||||
}
|
||||
if (!q->enabled_expression) {
|
||||
return true;
|
||||
}
|
||||
auto p = this->character();
|
||||
string expr = q->enabled_expression->str();
|
||||
QuestAvailabilityExpression::Env env = {
|
||||
.flags = &this->character()->quest_flags.data.at(difficulty),
|
||||
.flags = &p->quest_flags.data.at(difficulty),
|
||||
.challenge_records = &p->challenge_records,
|
||||
.team = this->team(),
|
||||
.num_players = num_players,
|
||||
.event = event,
|
||||
};
|
||||
bool ret = q->enabled_expression->evaluate(env);
|
||||
this->log.info("Evaluating quest enabled expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE");
|
||||
@@ -767,7 +812,7 @@ void Client::load_all_files() {
|
||||
this->character_data->battle_records = nsc_data.battle_records;
|
||||
this->character_data->challenge_records = nsc_data.challenge_records;
|
||||
this->character_data->tech_menu_config = nsc_data.tech_menu_config;
|
||||
this->character_data->quest_global_flags = nsc_data.quest_global_flags;
|
||||
this->character_data->quest_counters = nsc_data.quest_counters;
|
||||
if (nsa_data) {
|
||||
this->character_data->option_flags = nsa_data->option_flags;
|
||||
this->character_data->symbol_chats = nsa_data->symbol_chats;
|
||||
@@ -988,3 +1033,46 @@ void Client::use_character_bank(int8_t index) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Client::print_inventory(FILE* stream) const {
|
||||
auto p = this->character();
|
||||
shared_ptr<const ItemNameIndex> name_index;
|
||||
try {
|
||||
name_index = this->require_server_state()->item_name_index(this->version());
|
||||
} catch (const runtime_error&) {
|
||||
}
|
||||
fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", p->disp.stats.meseta.load());
|
||||
fprintf(stream, "[PlayerInventory] %hhu items\n", 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();
|
||||
if (name_index) {
|
||||
auto name = name_index->describe_item(item.data);
|
||||
fprintf(stream, "[PlayerInventory] %2zu: [+%08" PRIX32 "] %s (%s)\n", x, item.flags.load(), hex.c_str(), name.c_str());
|
||||
} else {
|
||||
fprintf(stream, "[PlayerInventory] %2zu: [+%08" PRIX32 "] %s\n", x, item.flags.load(), hex.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Client::print_bank(FILE* stream) const {
|
||||
auto p = this->character();
|
||||
shared_ptr<const ItemNameIndex> name_index;
|
||||
try {
|
||||
name_index = this->require_server_state()->item_name_index(this->version());
|
||||
} catch (const runtime_error&) {
|
||||
}
|
||||
fprintf(stream, "[PlayerBank] Meseta: %" PRIu32 "\n", p->bank.meseta.load());
|
||||
fprintf(stream, "[PlayerBank] %" PRIu32 " items\n", p->bank.num_items.load());
|
||||
for (size_t x = 0; x < p->bank.num_items; x++) {
|
||||
const auto& item = p->bank.items[x];
|
||||
const char* present_token = item.present ? "" : " (missing present flag)";
|
||||
auto hex = item.data.hex();
|
||||
if (name_index) {
|
||||
auto name = name_index->describe_item(item.data);
|
||||
fprintf(stream, "[PlayerBank] %3zu: %s (%s) (x%hu)%s\n", x, hex.c_str(), name.c_str(), item.amount.load(), present_token);
|
||||
} else {
|
||||
fprintf(stream, "[PlayerBank] %3zu: %s (x%hu)%s\n", x, hex.c_str(), item.amount.load(), present_token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-17
@@ -30,8 +30,15 @@ public:
|
||||
enum class Flag : uint64_t {
|
||||
// clang-format off
|
||||
|
||||
// This mask specifies which flags are sent to the client
|
||||
// TODO: It'd be nice to use a pattern here (e.g. all server-side flags are
|
||||
// in the high bits) but that would require re-recording or manually
|
||||
// rewriting all the tests
|
||||
CLIENT_SIDE_MASK = 0xFFFFFFFFFC0FFFFB,
|
||||
|
||||
// Version-related flags
|
||||
CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002,
|
||||
LICENSE_WAS_CREATED = 0x0000000000000004, // Server-side only
|
||||
NO_D6_AFTER_LOBBY = 0x0000000000000100,
|
||||
NO_D6 = 0x0000000000000200,
|
||||
FORCE_ENGLISH_LANGUAGE_BB = 0x0000000000000400,
|
||||
@@ -44,32 +51,33 @@ public:
|
||||
USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x0000000000010000,
|
||||
|
||||
// State flags
|
||||
LOADING = 0x0000000000100000,
|
||||
LOADING_QUEST = 0x0000000000200000,
|
||||
LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000,
|
||||
LOADING_TOURNAMENT = 0x0000000000800000,
|
||||
IN_INFORMATION_MENU = 0x0000000001000000,
|
||||
AT_WELCOME_MESSAGE = 0x0000000002000000,
|
||||
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
|
||||
SAVE_ENABLED = 0x0000000004000000,
|
||||
HAS_EP3_CARD_DEFS = 0x0000000008000000,
|
||||
HAS_EP3_MEDIA_UPDATES = 0x0000000010000000,
|
||||
USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000,
|
||||
HAS_GUILD_CARD_NUMBER = 0x0000000040000000,
|
||||
AT_BANK_COUNTER = 0x0000000080000000,
|
||||
SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000,
|
||||
SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000,
|
||||
AT_BANK_COUNTER = 0x0000000080000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Server-side only
|
||||
SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000,
|
||||
|
||||
// Cheat mode flags
|
||||
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
|
||||
|
||||
// Cheat mode and option flags
|
||||
INFINITE_HP_ENABLED = 0x0000000200000000,
|
||||
INFINITE_TP_ENABLED = 0x0000000400000000,
|
||||
DEBUG_ENABLED = 0x0000000800000000,
|
||||
RARE_DROP_NOTIFICATIONS_ENABLED = 0x0010000000000000,
|
||||
ALL_DROP_NOTIFICATIONS_ENABLED = 0x0020000000000000,
|
||||
|
||||
// Proxy option flags
|
||||
PROXY_SAVE_FILES = 0x0000001000000000,
|
||||
PROXY_CHAT_COMMANDS_ENABLED = 0x0000002000000000,
|
||||
PROXY_CHAT_FILTER_ENABLED = 0x0000004000000000,
|
||||
PROXY_PLAYER_NOTIFICATIONS_ENABLED = 0x0000008000000000,
|
||||
PROXY_SUPPRESS_CLIENT_PINGS = 0x0000010000000000,
|
||||
PROXY_SUPPRESS_REMOTE_LOGIN = 0x0000020000000000,
|
||||
@@ -83,8 +91,7 @@ public:
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static constexpr uint64_t DEFAULT_FLAGS = static_cast<uint64_t>(Flag::PROXY_CHAT_COMMANDS_ENABLED) |
|
||||
static_cast<uint64_t>(Flag::PROXY_CHAT_FILTER_ENABLED);
|
||||
static constexpr uint64_t DEFAULT_FLAGS = static_cast<uint64_t>(Flag::PROXY_CHAT_COMMANDS_ENABLED);
|
||||
|
||||
struct Config {
|
||||
uint64_t enabled_flags = DEFAULT_FLAGS; // Client::Flag enum
|
||||
@@ -101,6 +108,8 @@ public:
|
||||
bool operator==(const Config& other) const = default;
|
||||
bool operator!=(const Config& other) const = default;
|
||||
|
||||
bool should_update_vs(const Config& other) const;
|
||||
|
||||
[[nodiscard]] static inline bool check_flag(uint64_t enabled_flags, Flag flag) {
|
||||
return !!(enabled_flags & static_cast<uint64_t>(flag));
|
||||
}
|
||||
@@ -141,7 +150,7 @@ public:
|
||||
StringWriter w;
|
||||
w.put_u32l(CLIENT_CONFIG_MAGIC);
|
||||
w.put_u32l(this->specific_version);
|
||||
w.put_u64l(this->enabled_flags);
|
||||
w.put_u64l(this->enabled_flags & static_cast<uint64_t>(Flag::CLIENT_SIDE_MASK));
|
||||
w.put_u32l(this->override_random_seed);
|
||||
w.put_u32b(this->proxy_destination_address);
|
||||
w.put_u16l(this->proxy_destination_port);
|
||||
@@ -265,6 +274,7 @@ public:
|
||||
}
|
||||
|
||||
void set_license(std::shared_ptr<License> l);
|
||||
void convert_license_to_temporary_if_nte();
|
||||
|
||||
void sync_config();
|
||||
|
||||
@@ -273,8 +283,8 @@ public:
|
||||
|
||||
std::shared_ptr<const TeamIndex::Team> team() const;
|
||||
|
||||
bool can_see_quest(std::shared_ptr<const Quest> q, uint8_t difficulty, size_t num_players) const;
|
||||
bool can_play_quest(std::shared_ptr<const Quest> q, uint8_t difficulty, size_t num_players) const;
|
||||
bool can_see_quest(std::shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players) const;
|
||||
bool can_play_quest(std::shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players) const;
|
||||
|
||||
static void dispatch_save_game_data(evutil_socket_t, short, void* ctx);
|
||||
void save_game_data();
|
||||
@@ -341,6 +351,9 @@ public:
|
||||
void use_character_bank(int8_t bb_character_index);
|
||||
void use_default_bank();
|
||||
|
||||
void print_inventory(FILE* stream) const;
|
||||
void print_bank(FILE* stream) const;
|
||||
|
||||
private:
|
||||
// The overlay character data is used in battle and challenge modes, when
|
||||
// character data is temporarily replaced in-game. In other play modes and in
|
||||
|
||||
+709
-519
File diff suppressed because it is too large
Load Diff
@@ -1094,3 +1094,14 @@ const vector<EnemyType>& enemy_types_for_rare_table_index(Episode episode, uint8
|
||||
return empty_vec;
|
||||
}
|
||||
}
|
||||
|
||||
bool enemy_type_is_rare(EnemyType type) {
|
||||
return ((type == EnemyType::HILDEBLUE) ||
|
||||
(type == EnemyType::AL_RAPPY) ||
|
||||
(type == EnemyType::NAR_LILY) ||
|
||||
(type == EnemyType::POUILLY_SLIME) ||
|
||||
(type == EnemyType::MERISSA_AA) ||
|
||||
(type == EnemyType::PAZUZU_ALT) ||
|
||||
(type == EnemyType::DORPHON_ECLAIR) ||
|
||||
(type == EnemyType::KONDRIEU));
|
||||
}
|
||||
|
||||
@@ -145,3 +145,4 @@ bool enemy_type_valid_for_episode(Episode episode, EnemyType enemy_type);
|
||||
uint8_t battle_param_index_for_enemy_type(Episode episode, EnemyType enemy_type);
|
||||
uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type);
|
||||
const std::vector<EnemyType>& enemy_types_for_rare_table_index(Episode episode, uint8_t rt_index);
|
||||
bool enemy_type_is_rare(EnemyType type);
|
||||
|
||||
@@ -6,21 +6,38 @@ using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
// Note: This order matches the order that the cards are defined in the original
|
||||
// code. This is relevant for consistency of results when choosing a random card
|
||||
// (for God Whim).
|
||||
const vector<uint16_t> ALL_ASSIST_CARD_IDS = {
|
||||
0x0018, 0x0019, 0x001A, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA,
|
||||
0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, 0x0100, 0x0101, 0x0102, 0x0103,
|
||||
0x0104, 0x0105, 0x0106, 0x0107, 0x0108, 0x0109, 0x010A, 0x010B, 0x010C,
|
||||
0x010D, 0x010E, 0x010F, 0x0121, 0x0125, 0x0126, 0x0127, 0x0128, 0x0129,
|
||||
0x012A, 0x012B, 0x012C, 0x012D, 0x012E, 0x012F, 0x0130, 0x0131, 0x0132,
|
||||
0x0133, 0x0134, 0x0135, 0x0136, 0x0137, 0x0138, 0x0139, 0x013A, 0x013B,
|
||||
0x013C, 0x013D, 0x013E, 0x013F, 0x0140, 0x0141, 0x0142, 0x0143, 0x0144,
|
||||
0x0145, 0x0146, 0x0148, 0x014A, 0x014B, 0x014C, 0x014D, 0x014E, 0x023F,
|
||||
0x0240, 0x0241, 0x0242};
|
||||
const vector<uint16_t>& all_assist_card_ids(bool is_nte) {
|
||||
// Note: This order matches the order that the cards are defined in the original
|
||||
// code. This is relevant for consistency of results when choosing a random card
|
||||
// (for God Whim).
|
||||
static const vector<uint16_t> ALL_ASSIST_CARD_IDS_TRIAL = {
|
||||
0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD,
|
||||
0x00FE, 0x00FF, 0x0100, 0x0101, 0x0102, 0x0103, 0x0104, 0x0105, 0x0106,
|
||||
0x0107, 0x0108, 0x0109, 0x010A, 0x010B, 0x010C, 0x010D, 0x010E, 0x010F,
|
||||
0x0121, 0x0125, 0x0126, 0x0127, 0x0128, 0x0129, 0x012A, 0x012B, 0x012C,
|
||||
0x012D, 0x012E, 0x012F, 0x0130, 0x0131, 0x0132, 0x0133, 0x0134, 0x0135,
|
||||
0x0136, 0x0137, 0x0138, 0x0139, 0x013A, 0x013B, 0x013C, 0x013D, 0x013E,
|
||||
0x013F, 0x0140, 0x0141, 0x0142, 0x0143, 0x0144, 0x0145, 0x0146, 0x0148,
|
||||
0x014A, 0x014B, 0x014C, 0x014D, 0x014E, 0x023F, 0x0240, 0x0241, 0x0242};
|
||||
static const vector<uint16_t> ALL_ASSIST_CARD_IDS_FINAL = {
|
||||
0x0018, 0x0019, 0x001A, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA,
|
||||
0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, 0x0100, 0x0101, 0x0102, 0x0103,
|
||||
0x0104, 0x0105, 0x0106, 0x0107, 0x0108, 0x0109, 0x010A, 0x010B, 0x010C,
|
||||
0x010D, 0x010E, 0x010F, 0x0121, 0x0125, 0x0126, 0x0127, 0x0128, 0x0129,
|
||||
0x012A, 0x012B, 0x012C, 0x012D, 0x012E, 0x012F, 0x0130, 0x0131, 0x0132,
|
||||
0x0133, 0x0134, 0x0135, 0x0136, 0x0137, 0x0138, 0x0139, 0x013A, 0x013B,
|
||||
0x013C, 0x013D, 0x013E, 0x013F, 0x0140, 0x0141, 0x0142, 0x0143, 0x0144,
|
||||
0x0145, 0x0146, 0x0148, 0x014A, 0x014B, 0x014C, 0x014D, 0x014E, 0x023F,
|
||||
0x0240, 0x0241, 0x0242};
|
||||
return is_nte ? ALL_ASSIST_CARD_IDS_TRIAL : ALL_ASSIST_CARD_IDS_FINAL;
|
||||
}
|
||||
|
||||
AssistEffect assist_effect_number_for_card_id(uint16_t card_id) {
|
||||
AssistEffect assist_effect_number_for_card_id(uint16_t card_id, bool is_nte) {
|
||||
static const unordered_map<uint16_t, AssistEffect> card_id_to_effect_final_only({
|
||||
{0x0018, /* 0x0049 */ AssistEffect::DICE_FEVER_PLUS},
|
||||
{0x0019, /* 0x004A */ AssistEffect::RICH_PLUS},
|
||||
{0x001A, /* 0x004B */ AssistEffect::CHARITY_PLUS},
|
||||
});
|
||||
static const unordered_map<uint16_t, AssistEffect> card_id_to_effect({
|
||||
{0x00F5, /* 0x0001 */ AssistEffect::DICE_HALF},
|
||||
{0x00F6, /* 0x0002 */ AssistEffect::DICE_PLUS_1},
|
||||
@@ -94,15 +111,18 @@ AssistEffect assist_effect_number_for_card_id(uint16_t card_id) {
|
||||
{0x0240, /* 0x0046 */ AssistEffect::BOMB},
|
||||
{0x0241, /* 0x0047 */ AssistEffect::SKIP_TURN},
|
||||
{0x0242, /* 0x0048 */ AssistEffect::BATTLE_ROYALE},
|
||||
{0x0018, /* 0x0049 */ AssistEffect::DICE_FEVER_PLUS},
|
||||
{0x0019, /* 0x004A */ AssistEffect::RICH_PLUS},
|
||||
{0x001A, /* 0x004B */ AssistEffect::CHARITY_PLUS},
|
||||
});
|
||||
try {
|
||||
return card_id_to_effect.at(card_id);
|
||||
} catch (const out_of_range&) {
|
||||
return AssistEffect::NONE;
|
||||
}
|
||||
if (!is_nte) {
|
||||
try {
|
||||
return card_id_to_effect_final_only.at(card_id);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
return AssistEffect::NONE;
|
||||
}
|
||||
|
||||
AssistServer::AssistServer(shared_ptr<Server> server)
|
||||
@@ -224,6 +244,7 @@ AssistEffect AssistServer::get_active_assist_by_index(size_t index) const {
|
||||
}
|
||||
|
||||
void AssistServer::populate_effects() {
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
this->assist_card_defs[z] = nullptr;
|
||||
this->assist_effects[z] = AssistEffect::NONE;
|
||||
@@ -232,7 +253,7 @@ void AssistServer::populate_effects() {
|
||||
uint16_t card_id = hes->assist_card_id == 0xFFFF
|
||||
? this->card_id_for_card_ref(hes->assist_card_ref)
|
||||
: hes->assist_card_id.load();
|
||||
this->assist_effects[z] = assist_effect_number_for_card_id(card_id);
|
||||
this->assist_effects[z] = assist_effect_number_for_card_id(card_id, is_nte);
|
||||
if (this->assist_effects[z] != AssistEffect::NONE) {
|
||||
this->assist_card_defs[z] = this->definition_for_card_id(card_id);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,8 @@ namespace Episode3 {
|
||||
|
||||
class Server;
|
||||
|
||||
extern const std::vector<uint16_t> ALL_ASSIST_CARD_IDS;
|
||||
|
||||
AssistEffect assist_effect_number_for_card_id(uint16_t card_id);
|
||||
const std::vector<uint16_t>& all_assist_card_ids(bool is_nte);
|
||||
AssistEffect assist_effect_number_for_card_id(uint16_t card_id, bool is_nte);
|
||||
|
||||
class AssistServer {
|
||||
public:
|
||||
|
||||
@@ -191,7 +191,7 @@ bool BattleRecord::is_map_definition_event(const Event& ev) {
|
||||
if (ev.type == Event::Type::BATTLE_COMMAND) {
|
||||
auto& header = check_size_t<G_CardBattleCommandHeader>(ev.data, 0xFFFF);
|
||||
if (header.subcommand == 0xB6) {
|
||||
auto& header = check_size_t<G_MapSubsubcommand_GC_Ep3_6xB6>(ev.data, 0xFFFF);
|
||||
auto& header = check_size_t<G_MapSubsubcommand_Ep3_6xB6>(ev.data, 0xFFFF);
|
||||
if (header.subsubcommand == 0x41) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+554
-290
File diff suppressed because it is too large
Load Diff
+15
-12
@@ -34,16 +34,17 @@ public:
|
||||
int16_t value,
|
||||
int8_t dice_roll_value,
|
||||
int8_t random_percent);
|
||||
void apply_ap_adjust_assists_to_attack(
|
||||
void apply_ap_and_tp_adjust_assists_to_attack(
|
||||
std::shared_ptr<const Card> attacker_card,
|
||||
int16_t* inout_attacker_ap,
|
||||
int16_t* in_defense_power) const;
|
||||
int16_t* in_defense_power,
|
||||
int16_t* inout_attacker_tp) const;
|
||||
bool card_type_is_sc_or_creature() const;
|
||||
bool check_card_flag(uint32_t flags) const;
|
||||
void commit_attack(
|
||||
int16_t damage,
|
||||
std::shared_ptr<Card> attacker_card,
|
||||
G_ApplyConditionEffect_GC_Ep3_6xB4x06* cmd,
|
||||
G_ApplyConditionEffect_Ep3_6xB4x06* cmd,
|
||||
size_t strike_number,
|
||||
int16_t* out_effective_damage);
|
||||
int16_t compute_defense_power_for_attacker_card(
|
||||
@@ -51,12 +52,14 @@ public:
|
||||
void destroy_set_card(std::shared_ptr<Card> attacker_card);
|
||||
int32_t error_code_for_move_to_location(const Location& loc) const;
|
||||
void execute_attack(std::shared_ptr<Card> attacker_card);
|
||||
bool get_attack_condition_value(
|
||||
bool get_condition_value(
|
||||
ConditionType cond_type,
|
||||
uint16_t card_ref,
|
||||
uint8_t def_effect_index,
|
||||
uint16_t value,
|
||||
uint16_t* out_value) const;
|
||||
uint16_t card_ref = 0xFFFF,
|
||||
uint8_t def_effect_index = 0xFF,
|
||||
uint16_t value = 0xFFFF,
|
||||
uint16_t* out_value = nullptr) const;
|
||||
Condition* find_condition(ConditionType cond_type);
|
||||
const Condition* find_condition(ConditionType cond_type) const;
|
||||
std::shared_ptr<const CardIndex::CardEntry> get_definition() const;
|
||||
uint16_t get_card_ref() const;
|
||||
uint16_t get_card_id() const;
|
||||
@@ -79,14 +82,14 @@ public:
|
||||
void unknown_802380C0();
|
||||
void unknown_80237F98(bool require_condition_20_or_21);
|
||||
void unknown_80237F88();
|
||||
void unknown_80235AA0();
|
||||
void unknown_80235AD4();
|
||||
void unknown_80235B10();
|
||||
void draw_phase_before();
|
||||
void action_phase_before();
|
||||
void move_phase_before();
|
||||
void unknown_80236374(std::shared_ptr<Card> other_card, const ActionState* as);
|
||||
void unknown_802379BC(uint16_t card_ref);
|
||||
void unknown_802379DC(const ActionState& pa);
|
||||
void unknown_80237A90(const ActionState& pa, uint16_t action_card_ref);
|
||||
void unknown_8023813C();
|
||||
void dice_phase_before();
|
||||
bool is_guard_item() const;
|
||||
bool unknown_80236554(std::shared_ptr<Card> other_card, const ActionState* as);
|
||||
void unknown_802362D8(std::shared_ptr<Card> other_card);
|
||||
|
||||
+945
-620
File diff suppressed because it is too large
Load Diff
+50
-50
@@ -45,45 +45,49 @@ public:
|
||||
};
|
||||
|
||||
struct AttackEnvStats {
|
||||
uint32_t num_set_cards; // "f" in expr
|
||||
uint32_t dice_roll_value1; // "d" in expr
|
||||
uint32_t effective_ap; // "ap" in expr
|
||||
uint32_t effective_tp; // "tp" in expr
|
||||
uint32_t current_hp; // "hp" in expr
|
||||
uint32_t max_hp; // "mhp" in expr
|
||||
uint32_t effective_ap_if_not_tech; // "dm" in expr
|
||||
uint32_t effective_ap_if_not_physical; // "tdm" in expr
|
||||
uint32_t player_num_destroyed_fcs; // "tf" in expr
|
||||
uint32_t player_num_atk_points; // "ac" in expr
|
||||
uint32_t defined_max_hp; // "php" in expr
|
||||
uint32_t dice_roll_value2; // "dc" in expr
|
||||
uint32_t card_cost; // "cs" in expr
|
||||
uint32_t total_num_set_cards; // "a" in expr
|
||||
uint32_t action_cards_ap; // "kap" in expr
|
||||
uint32_t action_cards_tp; // "ktp" in expr
|
||||
uint32_t unknown_a1; // "dn" in expr
|
||||
uint32_t num_item_or_creature_cards_in_hand; // "hf" in expr
|
||||
uint32_t num_destroyed_ally_fcs; // "df" in expr
|
||||
uint32_t target_team_num_set_cards; // "ff" in expr
|
||||
uint32_t condition_giver_team_num_set_cards; // "ef" in expr
|
||||
uint32_t num_native_creatures; // "bi" in expr
|
||||
uint32_t num_a_beast_creatures; // "ab" in expr
|
||||
uint32_t num_machine_creatures; // "mc" in expr
|
||||
uint32_t num_dark_creatures; // "dk" in expr
|
||||
uint32_t num_sword_type_items; // "sa" in expr
|
||||
uint32_t num_gun_type_items; // "gn" in expr
|
||||
uint32_t num_cane_type_items; // "wd" in expr
|
||||
uint32_t effective_ap_if_not_tech2; // "tt" in expr
|
||||
uint32_t team_dice_boost; // "lv" in expr
|
||||
uint32_t sc_effective_ap; // "adm" in expr
|
||||
uint32_t attack_bonus; // "ddm" in expr
|
||||
uint32_t num_sword_type_items_on_team; // "sat" in expr
|
||||
uint32_t target_attack_bonus; // "edm" in expr
|
||||
uint32_t last_attack_preliminary_damage; // "ldm" in expr
|
||||
uint32_t last_attack_damage; // "rdm" in expr
|
||||
uint32_t total_last_attack_damage; // "fdm" in expr
|
||||
uint32_t last_attack_damage_count; // "ndm" in expr
|
||||
uint32_t target_current_hp; // "ehp" in expr
|
||||
/* 00 */ uint32_t num_set_cards; // "f" in expr
|
||||
/* 04 */ uint32_t dice_roll_value1; // "d" in expr
|
||||
/* 08 */ uint32_t effective_ap; // "ap" in expr
|
||||
/* 0C */ uint32_t effective_tp; // "tp" in expr
|
||||
/* 10 */ uint32_t current_hp; // "hp" in expr
|
||||
/* 14 */ uint32_t max_hp; // "mhp" in expr
|
||||
/* 18 */ uint32_t effective_ap_if_not_tech; // "dm" in expr
|
||||
/* 1C */ uint32_t effective_ap_if_not_physical; // "tdm" in expr
|
||||
/* 20 */ uint32_t player_num_destroyed_fcs; // "tf" in expr
|
||||
/* 24 */ uint32_t player_num_atk_points; // "ac" in expr
|
||||
/* 28 */ uint32_t defined_max_hp; // "php" in expr
|
||||
/* 2C */ uint32_t dice_roll_value2; // "dc" in expr
|
||||
/* 30 */ uint32_t card_cost; // "cs" in expr
|
||||
/* 34 */ uint32_t total_num_set_cards; // "a" in expr
|
||||
/* 38 */ uint32_t action_cards_ap; // "kap" in expr
|
||||
/* 3C */ uint32_t action_cards_tp; // "ktp" in expr
|
||||
/* 40 */ uint32_t unknown_a1; // "dn" in expr
|
||||
/* 44 */ uint32_t num_item_or_creature_cards_in_hand; // "hf" in expr
|
||||
/* 48 */ uint32_t num_destroyed_ally_fcs; // "df" in expr
|
||||
/* 4C */ uint32_t target_team_num_set_cards; // "ff" in expr
|
||||
/* 50 */ uint32_t non_target_team_num_set_cards; // "ef" in expr
|
||||
/* 54 */ uint32_t num_native_creatures; // "bi" in expr
|
||||
/* 58 */ uint32_t num_a_beast_creatures; // "ab" in expr
|
||||
/* 5C */ uint32_t num_machine_creatures; // "mc" in expr
|
||||
/* 60 */ uint32_t num_dark_creatures; // "dk" in expr
|
||||
/* 64 */ uint32_t num_sword_type_items; // "sa" in expr
|
||||
/* 68 */ uint32_t num_gun_type_items; // "gn" in expr
|
||||
/* 6C */ uint32_t num_cane_type_items; // "wd" in expr
|
||||
/* 70 */ uint32_t effective_ap_if_not_tech2; // "tt" in expr
|
||||
/* 74 */ uint32_t team_dice_bonus; // "lv" in expr
|
||||
/* 78 */ uint32_t sc_effective_ap; // "adm" in expr
|
||||
// The following fields do not exist in Trial Edition. Because this struct
|
||||
// is never sent to the client, we use the full struct even when playing
|
||||
// Trial Edition, just for simplicity.
|
||||
/* 7C */ uint32_t attack_bonus; // "ddm" in expr
|
||||
/* 80 */ uint32_t num_sword_type_items_on_team; // "sat" in expr
|
||||
/* 84 */ uint32_t target_attack_bonus; // "edm" in expr
|
||||
/* 88 */ uint32_t last_attack_preliminary_damage; // "ldm" in expr
|
||||
/* 8C */ uint32_t last_attack_damage; // "rdm" in expr
|
||||
/* 90 */ uint32_t total_last_attack_damage; // "fdm" in expr
|
||||
/* 94 */ uint32_t last_attack_damage_count; // "ndm" in expr
|
||||
/* 98 */ uint32_t target_current_hp; // "ehp" in expr
|
||||
/* 9C */
|
||||
|
||||
AttackEnvStats();
|
||||
void clear();
|
||||
@@ -159,7 +163,7 @@ public:
|
||||
uint32_t* unknown_p11,
|
||||
uint16_t sc_card_ref);
|
||||
StatSwapType compute_stat_swap_type(std::shared_ptr<const Card> card) const;
|
||||
void compute_team_dice_boost(uint8_t team_id);
|
||||
void compute_team_dice_bonus(uint8_t team_id);
|
||||
bool condition_has_when_20_or_21(const Condition& cond) const;
|
||||
size_t count_action_cards_with_condition_for_all_current_attacks(
|
||||
ConditionType cond_type, uint16_t card_ref) const;
|
||||
@@ -309,14 +313,13 @@ public:
|
||||
const Location& card1_loc,
|
||||
std::shared_ptr<const Card> card2) const;
|
||||
void unknown_8024AAB8(const ActionState& as);
|
||||
void unknown_80244BE4(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_80244CA8(std::shared_ptr<Card> card);
|
||||
void move_phase_before_for_card(std::shared_ptr<Card> unknown_p2);
|
||||
void dice_phase_before_for_card(std::shared_ptr<Card> card);
|
||||
template <uint8_t When1, uint8_t When2>
|
||||
void unknown1_t(
|
||||
std::shared_ptr<Card> unknown_p2, const ActionState* existing_as = nullptr);
|
||||
void unknown_80249060(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_80249254(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_8024945C(std::shared_ptr<Card> unknown_p2, const ActionState& existing_as);
|
||||
void apply_effects_on_phase_change_t(std::shared_ptr<Card> unknown_p2, const ActionState* existing_as = nullptr);
|
||||
void draw_phase_before_for_card(std::shared_ptr<Card> unknown_p2);
|
||||
void action_phase_before_for_card(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_8024945C(std::shared_ptr<Card> unknown_p2, const ActionState* existing_as);
|
||||
void unknown_8024966C(std::shared_ptr<Card> unknown_p2, const ActionState* existing_as);
|
||||
static std::shared_ptr<Card> sc_card_for_card(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_8024A9D8(const ActionState& pa, uint16_t action_card_ref);
|
||||
@@ -333,9 +336,6 @@ public:
|
||||
|
||||
private:
|
||||
std::weak_ptr<Server> w_server;
|
||||
ActionState unknown_action_state_a1;
|
||||
ActionState action_state;
|
||||
uint16_t unknown_a2;
|
||||
};
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
+40
-21
@@ -341,10 +341,14 @@ const char* name_for_direction(Direction d) {
|
||||
}
|
||||
}
|
||||
|
||||
bool card_class_is_tech_like(CardClass cc) {
|
||||
return (cc == CardClass::TECH) ||
|
||||
(cc == CardClass::PHOTON_BLAST) ||
|
||||
(cc == CardClass::BOSS_TECH);
|
||||
bool card_class_is_tech_like(CardClass cc, bool is_nte) {
|
||||
// NTE does not consider BOSS_TECH to be a tech-like card class, but that's
|
||||
// probably because that card class just doesn't exist on NTE.
|
||||
if (is_nte) {
|
||||
return (cc == CardClass::TECH) || (cc == CardClass::PHOTON_BLAST);
|
||||
} else {
|
||||
return (cc == CardClass::TECH) || (cc == CardClass::PHOTON_BLAST) || (cc == CardClass::BOSS_TECH);
|
||||
}
|
||||
}
|
||||
|
||||
static const unordered_map<string, const char*> description_for_expr_token({
|
||||
@@ -745,8 +749,14 @@ string CardDefinition::Effect::str_for_arg(const string& arg) {
|
||||
} catch (const out_of_range&) {
|
||||
return arg + " (Req. condition: unknown)";
|
||||
}
|
||||
case 'o':
|
||||
return arg + " (Req. prev effect conditions passed)";
|
||||
case 'o': {
|
||||
const char* suffix = ((value / 10) == 1) ? " on opponent card" : " on self";
|
||||
if (value == 0) {
|
||||
return string_printf("%s (Req. any previous effect%s)", arg.c_str(), suffix);
|
||||
} else {
|
||||
return string_printf("%s (Req. effect %zu passed%s)", arg.c_str(), static_cast<size_t>(value % 10), suffix);
|
||||
}
|
||||
}
|
||||
case 'p':
|
||||
try {
|
||||
return string_printf("%s (Target: %s)", arg.c_str(), description_for_p_target.at(value));
|
||||
@@ -764,7 +774,7 @@ string CardDefinition::Effect::str_for_arg(const string& arg) {
|
||||
}
|
||||
}
|
||||
|
||||
string CardDefinition::Effect::str(const char* separator, const TextArchive* text_archive) const {
|
||||
string CardDefinition::Effect::str(const char* separator, const TextSet* text_archive) const {
|
||||
vector<string> tokens;
|
||||
tokens.emplace_back(string_printf("%hhu:", this->effect_num));
|
||||
{
|
||||
@@ -802,7 +812,7 @@ string CardDefinition::Effect::str(const char* separator, const TextArchive* tex
|
||||
const char* name = nullptr;
|
||||
if (this->name_index && text_archive) {
|
||||
try {
|
||||
name = text_archive->get_string(45, this->name_index).c_str();
|
||||
name = text_archive->get(45, this->name_index).c_str();
|
||||
} catch (const exception&) {
|
||||
}
|
||||
}
|
||||
@@ -1061,7 +1071,7 @@ static const char* name_for_assist_ai_param_target(uint8_t target) {
|
||||
}
|
||||
}
|
||||
|
||||
string CardDefinition::str(bool single_line, const TextArchive* text_archive) const {
|
||||
string CardDefinition::str(bool single_line, const TextSet* text_archive) const {
|
||||
string type_str;
|
||||
try {
|
||||
type_str = name_for_card_type(this->type);
|
||||
@@ -1455,8 +1465,8 @@ RulesTrial::RulesTrial(const Rules& r)
|
||||
: overall_time_limit(r.overall_time_limit),
|
||||
phase_time_limit(r.phase_time_limit),
|
||||
allowed_cards(r.allowed_cards),
|
||||
atk_dice_max(r.max_dice),
|
||||
def_dice_max(r.max_dice),
|
||||
atk_die_behavior((r.max_dice == r.min_dice) ? r.max_dice : 0),
|
||||
def_die_behavior(r.def_dice_range == 0xFF ? 0xFF : ((r.min_def_dice() == r.max_def_dice()) ? r.max_def_dice() : 0)),
|
||||
disable_deck_shuffle(r.disable_deck_shuffle),
|
||||
disable_deck_loop(r.disable_deck_loop),
|
||||
char_hp(r.char_hp),
|
||||
@@ -1470,8 +1480,13 @@ RulesTrial::operator Rules() const {
|
||||
ret.overall_time_limit = this->overall_time_limit;
|
||||
ret.phase_time_limit = this->phase_time_limit;
|
||||
ret.allowed_cards = this->allowed_cards;
|
||||
ret.min_dice = 1;
|
||||
ret.max_dice = this->atk_dice_max;
|
||||
if (this->atk_die_behavior) {
|
||||
ret.min_dice = this->atk_die_behavior;
|
||||
ret.max_dice = this->atk_die_behavior;
|
||||
} else {
|
||||
ret.min_dice = 1;
|
||||
ret.max_dice = 6;
|
||||
}
|
||||
ret.disable_deck_shuffle = this->disable_deck_shuffle;
|
||||
ret.disable_deck_loop = this->disable_deck_loop;
|
||||
ret.char_hp = this->char_hp;
|
||||
@@ -1480,7 +1495,11 @@ RulesTrial::operator Rules() const {
|
||||
ret.disable_dialogue = this->disable_dialogue;
|
||||
ret.dice_exchange_mode = this->dice_exchange_mode;
|
||||
ret.disable_dice_boost = 0;
|
||||
ret.def_dice_range = 0x10 | (this->def_dice_max ? this->def_dice_max : 0x06);
|
||||
if (this->def_die_behavior) {
|
||||
ret.def_dice_range = (this->def_die_behavior << 4) | this->def_die_behavior;
|
||||
} else {
|
||||
ret.def_dice_range = 0x16;
|
||||
}
|
||||
ret.unused.clear(0);
|
||||
return ret;
|
||||
}
|
||||
@@ -1498,7 +1517,7 @@ bool StateFlags::operator==(const StateFlags& other) const {
|
||||
(this->setup_phase == other.setup_phase) &&
|
||||
(this->registration_phase == other.registration_phase) &&
|
||||
(this->team_exp == other.team_exp) &&
|
||||
(this->team_dice_boost == other.team_dice_boost) &&
|
||||
(this->team_dice_bonus == other.team_dice_bonus) &&
|
||||
(this->first_team_turn == other.first_team_turn) &&
|
||||
(this->tournament_flag == other.tournament_flag) &&
|
||||
(this->client_sc_card_types == other.client_sc_card_types);
|
||||
@@ -1516,7 +1535,7 @@ void StateFlags::clear() {
|
||||
this->setup_phase = SetupPhase::REGISTRATION;
|
||||
this->registration_phase = RegistrationPhase::AWAITING_NUM_PLAYERS;
|
||||
this->team_exp.clear(0);
|
||||
this->team_dice_boost.clear(0);
|
||||
this->team_dice_bonus.clear(0);
|
||||
this->first_team_turn = 0;
|
||||
this->tournament_flag = 0;
|
||||
this->client_sc_card_types.clear(CardType::HUNTERS_SC);
|
||||
@@ -1531,7 +1550,7 @@ void StateFlags::clear_FF() {
|
||||
this->setup_phase = SetupPhase::INVALID_FF;
|
||||
this->registration_phase = RegistrationPhase::INVALID_FF;
|
||||
this->team_exp.clear(0xFFFFFFFF);
|
||||
this->team_dice_boost.clear(0xFF);
|
||||
this->team_dice_bonus.clear(0xFF);
|
||||
this->first_team_turn = 0xFF;
|
||||
this->tournament_flag = 0xFF;
|
||||
this->client_sc_card_types.clear(CardType::INVALID_FF);
|
||||
@@ -2304,7 +2323,7 @@ CardIndex::CardIndex(
|
||||
this->compressed_card_definitions = prs_compress(decompressed_data);
|
||||
uint64_t diff = now() - start;
|
||||
static_game_data_log.info(
|
||||
"Compressed card definitions (%zu bytes -> %zu bytes) in %" PRIu64 "ms",
|
||||
"Compressed card definitions (%zu bytes -> %zu bytes) in %" PRIu64 "us",
|
||||
decompressed_data.size(), this->compressed_card_definitions.size(), diff);
|
||||
}
|
||||
|
||||
@@ -2322,7 +2341,7 @@ CardIndex::CardIndex(
|
||||
this->compressed_card_definitions = prs_compress_optimal(decompressed_data.data(), decompressed_data.size());
|
||||
uint64_t diff = now() - start;
|
||||
static_game_data_log.info(
|
||||
"Compressed card definitions (0x%zX bytes -> 0x%zX bytes) in %" PRIu64 "ms",
|
||||
"Compressed card definitions (0x%zX bytes -> 0x%zX bytes) in %" PRIu64 "us",
|
||||
decompressed_data.size(), this->compressed_card_definitions.size(), diff);
|
||||
}
|
||||
|
||||
@@ -2401,8 +2420,8 @@ shared_ptr<const MapDefinitionTrial> MapIndex::VersionedMap::trial() const {
|
||||
return this->trial_map;
|
||||
}
|
||||
|
||||
const std::string& MapIndex::VersionedMap::compressed(bool is_trial) const {
|
||||
if (is_trial) {
|
||||
const std::string& MapIndex::VersionedMap::compressed(bool is_nte) const {
|
||||
if (is_nte) {
|
||||
if (this->compressed_trial_data.empty()) {
|
||||
auto md = this->trial();
|
||||
this->compressed_trial_data = prs_compress(md.get(), sizeof(*md));
|
||||
|
||||
+19
-15
@@ -15,7 +15,7 @@
|
||||
|
||||
#include "../PlayerSubordinates.hh"
|
||||
#include "../Text.hh"
|
||||
#include "../TextArchive.hh"
|
||||
#include "../TextIndex.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
@@ -36,6 +36,7 @@ enum BehaviorFlag : uint32_t {
|
||||
DISABLE_MASKING = 0x00000080,
|
||||
DISABLE_INTERFERENCE = 0x00000100,
|
||||
ALLOW_NON_COM_INTERFERENCE = 0x00000200,
|
||||
IS_TRIAL_EDITION = 0x00000400,
|
||||
};
|
||||
|
||||
enum class StatSwapType : uint8_t {
|
||||
@@ -163,7 +164,7 @@ enum class CardClass : uint16_t {
|
||||
};
|
||||
|
||||
const char* name_for_card_class(CardClass cc);
|
||||
bool card_class_is_tech_like(CardClass cc);
|
||||
bool card_class_is_tech_like(CardClass cc, bool is_nte);
|
||||
|
||||
enum class TargetMode : uint8_t {
|
||||
NONE = 0x00, // Used for defense cards, mags, shields, etc.
|
||||
@@ -299,7 +300,7 @@ enum class ConditionType : uint8_t {
|
||||
UNKNOWN_75 = 0x75,
|
||||
REFLECT = 0x76, // Generate reverse attack
|
||||
UNKNOWN_77 = 0x77,
|
||||
ANY = 0x78, // Not a real condition; used as a wildcard in search functions
|
||||
ANY = 0x78, // Not a real condition; used as a wildcard in search functions. Has value 0x64 on NTE
|
||||
UNKNOWN_79 = 0x79,
|
||||
UNKNOWN_7A = 0x7A,
|
||||
UNKNOWN_7B = 0x7B,
|
||||
@@ -514,7 +515,7 @@ struct CardDefinition {
|
||||
|
||||
bool is_empty() const;
|
||||
static std::string str_for_arg(const std::string& arg);
|
||||
std::string str(const char* separator = ", ", const TextArchive* text_archive = nullptr) const;
|
||||
std::string str(const char* separator = ", ", const TextSet* text_archive = nullptr) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
/* 0000 */ be_uint32_t card_id;
|
||||
@@ -780,7 +781,7 @@ struct CardDefinition {
|
||||
CardClass card_class() const;
|
||||
|
||||
void decode_range();
|
||||
std::string str(bool single_line = true, const TextArchive* text_archive = nullptr) const;
|
||||
std::string str(bool single_line = true, const TextSet* text_archive = nullptr) const;
|
||||
} __attribute__((packed)); // 0x128 bytes in total
|
||||
|
||||
struct CardDefinitionsFooter {
|
||||
@@ -827,7 +828,7 @@ struct PlayerConfig {
|
||||
/* 000C:---- */ parray<uint8_t, 0x1C> unknown_a1;
|
||||
/* 0028:---- */ parray<be_uint16_t, 20> tech_menu_shortcut_entries;
|
||||
/* 0050:---- */ parray<be_uint32_t, 10> choice_search_config;
|
||||
// This field maps to quest_global_flags on Episodes 1 & 2
|
||||
// This field maps to quest_counters on Episodes 1 & 2
|
||||
/* 0078:---- */ parray<be_uint32_t, 0x30> scenario_progress;
|
||||
// place_counts[0] and [1] from this field are added to the player's win and
|
||||
// loss count respectively when they're shown in the status menu. However,
|
||||
@@ -979,13 +980,15 @@ struct Rules {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct RulesTrial {
|
||||
// The fields here have the same meaning as in the final version. The only
|
||||
// difference is that Dice Boost does not exist in the trial version.
|
||||
// Most fields here have the same meanings as in the final version.
|
||||
/* 00 */ uint8_t overall_time_limit = 0;
|
||||
/* 01 */ uint8_t phase_time_limit = 0;
|
||||
/* 02 */ AllowedCards allowed_cards = AllowedCards::ALL;
|
||||
/* 03 */ uint8_t atk_dice_max = 1;
|
||||
/* 04 */ uint8_t def_dice_max = 6;
|
||||
// In NTE, the dice behave differently than in non-NTE. A zero in either of
|
||||
// these fields means the corresponding die is random in the range [1, 6];
|
||||
// any nonzero value means that die will always take that value.
|
||||
/* 03 */ uint8_t atk_die_behavior = 0;
|
||||
/* 04 */ uint8_t def_die_behavior = 0;
|
||||
/* 05 */ uint8_t disable_deck_shuffle = 0;
|
||||
/* 06 */ uint8_t disable_deck_loop = 0;
|
||||
/* 07 */ uint8_t char_hp = 15;
|
||||
@@ -996,7 +999,7 @@ struct RulesTrial {
|
||||
/* 0C */
|
||||
|
||||
RulesTrial() = default;
|
||||
explicit RulesTrial(const Rules&);
|
||||
RulesTrial(const Rules&);
|
||||
operator Rules() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
@@ -1009,7 +1012,7 @@ struct StateFlags {
|
||||
/* 06 */ SetupPhase setup_phase;
|
||||
/* 07 */ RegistrationPhase registration_phase;
|
||||
/* 08 */ parray<le_uint32_t, 2> team_exp;
|
||||
/* 10 */ parray<uint8_t, 2> team_dice_boost;
|
||||
/* 10 */ parray<uint8_t, 2> team_dice_bonus;
|
||||
/* 12 */ uint8_t first_team_turn;
|
||||
/* 13 */ uint8_t tournament_flag;
|
||||
/* 14 */ parray<CardType, 4> client_sc_card_types;
|
||||
@@ -1183,8 +1186,9 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
// 10 = blocked by rock (as if the corresponding map_tiles value was 00)
|
||||
// 20 = blocked by fence (as if the corresponding map_tiles value was 00)
|
||||
// 30-34 = teleporters (2 of each value may be present)
|
||||
// 40-44 = traps (one of each type is chosen at random to be a real trap at
|
||||
// battle start time)
|
||||
// 40-4F = traps on NTE
|
||||
// 40-44 = traps on non-NTE (one of each type is chosen at random to be a real
|
||||
// trap at battle start time)
|
||||
// 50 = blocked by metal box (appears as improperly-z-buffered teal cube in
|
||||
// preview; behaves like 10 and 20 in game)
|
||||
// The assist cards that each trap type can contain are:
|
||||
@@ -1482,7 +1486,7 @@ public:
|
||||
VersionedMap(std::string&& compressed_data, uint8_t language);
|
||||
|
||||
std::shared_ptr<const MapDefinitionTrial> trial() const;
|
||||
const std::string& compressed(bool is_trial) const;
|
||||
const std::string& compressed(bool is_nte) const;
|
||||
|
||||
private:
|
||||
mutable std::shared_ptr<const MapDefinitionTrial> trial_map;
|
||||
|
||||
@@ -88,15 +88,16 @@ bool DeckState::draw_card_by_ref(uint16_t card_ref) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the card is discarded, then it should be before the draw index, and we
|
||||
// can just change its state.
|
||||
if (this->entries[index].state == CardState::DISCARDED) {
|
||||
// If the card is discarded, then it should be before the draw index, and we
|
||||
// can just change its state.
|
||||
this->entries[index].state = CardState::IN_HAND;
|
||||
return true;
|
||||
|
||||
// If the card is still drawable, we need to move it so it's just in front of
|
||||
// the draw index, then immediately draw it
|
||||
} else if (this->entries[index].state == CardState::DRAWABLE) {
|
||||
// If the card is still drawable, we need to move it so it's just in front
|
||||
// of the draw index, then immediately draw it. Ep3 NTE does not handle this
|
||||
// case, but we do even when playing NTE.
|
||||
ssize_t ref_index;
|
||||
for (ref_index = this->card_refs.size(); ref_index >= 0; ref_index--) {
|
||||
if (this->card_refs[ref_index] == card_ref) {
|
||||
@@ -182,7 +183,7 @@ void DeckState::restart() {
|
||||
this->shuffle();
|
||||
}
|
||||
|
||||
void DeckState::do_mulligan() {
|
||||
void DeckState::do_mulligan(bool is_nte) {
|
||||
for (size_t z = 0; z < this->entries.size(); z++) {
|
||||
if (this->entries[z].state == CardState::DISCARDED) {
|
||||
this->entries[z].state = CardState::DRAWABLE;
|
||||
@@ -190,7 +191,7 @@ void DeckState::do_mulligan() {
|
||||
}
|
||||
this->draw_index = 1;
|
||||
|
||||
if (this->shuffle_enabled) {
|
||||
if (is_nte || this->shuffle_enabled) {
|
||||
// Get the next 5 cards from the deck, and put the previous 5 cards after
|
||||
// them (so they will be shuffled back in).
|
||||
for (uint8_t z = 0; z < 5; z++) {
|
||||
|
||||
+15
-13
@@ -11,27 +11,29 @@
|
||||
namespace Episode3 {
|
||||
|
||||
struct NameEntry {
|
||||
parray<char, 0x10> name;
|
||||
uint8_t client_id;
|
||||
uint8_t present;
|
||||
uint8_t is_cpu_player;
|
||||
uint8_t unused;
|
||||
/* 00 */ parray<char, 0x10> name;
|
||||
/* 10 */ uint8_t client_id;
|
||||
/* 11 */ uint8_t present;
|
||||
/* 12 */ uint8_t is_cpu_player;
|
||||
/* 13 */ uint8_t unused;
|
||||
/* 14 */
|
||||
|
||||
NameEntry();
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct DeckEntry {
|
||||
pstring<TextEncoding::SJIS, 0x10> name;
|
||||
le_uint32_t team_id;
|
||||
parray<le_uint16_t, 31> card_ids;
|
||||
/* 00 */ pstring<TextEncoding::SJIS, 0x10> name;
|
||||
/* 10 */ le_uint32_t team_id;
|
||||
/* 14 */ parray<le_uint16_t, 31> card_ids;
|
||||
// If the following flag is not set to 3, then the God Whim assist effect can
|
||||
// use cards that are hidden from the player during deck building. The client
|
||||
// always sets this to 3, and it's not clear why this even exists.
|
||||
uint8_t god_whim_flag;
|
||||
uint8_t unused1;
|
||||
le_uint16_t player_level;
|
||||
parray<uint8_t, 2> unused2;
|
||||
/* 52 */ uint8_t god_whim_flag;
|
||||
/* 53 */ uint8_t unused1;
|
||||
/* 54 */ le_uint16_t player_level;
|
||||
/* 56 */ parray<uint8_t, 2> unused2;
|
||||
/* 58 */
|
||||
|
||||
DeckEntry();
|
||||
void clear();
|
||||
@@ -92,7 +94,7 @@ public:
|
||||
|
||||
void restart();
|
||||
void shuffle();
|
||||
void do_mulligan();
|
||||
void do_mulligan(bool is_nte);
|
||||
|
||||
void print(FILE* stream, std::shared_ptr<const CardIndex> card_index = nullptr) const;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ void MapAndRulesState::clear() {
|
||||
this->num_team0_players = 0;
|
||||
this->unused2 = 0;
|
||||
this->start_facing_directions = 0;
|
||||
this->unused3 = 0;
|
||||
this->unknown_a3 = 0;
|
||||
this->map_number = 0;
|
||||
this->unused4 = 0;
|
||||
this->rules.clear();
|
||||
@@ -68,6 +68,37 @@ void MapAndRulesState::clear_occupied_bit_for_tile(uint8_t x, uint8_t y) {
|
||||
this->map.tiles[y][x] &= 0xEF;
|
||||
}
|
||||
|
||||
MapAndRulesStateTrial::MapAndRulesStateTrial(const MapAndRulesState& state)
|
||||
: map(state.map),
|
||||
num_players(state.num_players),
|
||||
unused1(state.unused1),
|
||||
environment_number(state.environment_number),
|
||||
num_players_per_team(state.num_players_per_team),
|
||||
num_team0_players(state.num_team0_players),
|
||||
unused2(state.unused2),
|
||||
unused5(state.start_facing_directions),
|
||||
unknown_a3(state.unknown_a3),
|
||||
map_number(state.map_number),
|
||||
unused4(state.unused4),
|
||||
rules(state.rules) {}
|
||||
|
||||
MapAndRulesStateTrial::operator MapAndRulesState() const {
|
||||
MapAndRulesState ret;
|
||||
ret.map = this->map;
|
||||
ret.num_players = this->num_players;
|
||||
ret.unused1 = this->unused1;
|
||||
ret.environment_number = this->environment_number;
|
||||
ret.num_players_per_team = this->num_players_per_team;
|
||||
ret.num_team0_players = this->num_team0_players;
|
||||
ret.unused2 = this->unused2;
|
||||
ret.start_facing_directions = this->unused5;
|
||||
ret.unknown_a3 = this->unknown_a3;
|
||||
ret.map_number = this->map_number;
|
||||
ret.unused4 = this->unused4;
|
||||
ret.rules = this->rules;
|
||||
return ret;
|
||||
}
|
||||
|
||||
OverlayState::OverlayState() {
|
||||
this->clear();
|
||||
}
|
||||
@@ -78,7 +109,7 @@ void OverlayState::clear() {
|
||||
}
|
||||
this->unused1.clear(0);
|
||||
this->unused2.clear(0);
|
||||
this->unused3.clear(0);
|
||||
this->trap_card_ids_nte.clear(0);
|
||||
}
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
+39
-17
@@ -10,10 +10,11 @@
|
||||
namespace Episode3 {
|
||||
|
||||
struct MapState {
|
||||
le_uint16_t width;
|
||||
le_uint16_t height;
|
||||
parray<parray<uint8_t, 0x10>, 0x10> tiles;
|
||||
parray<parray<uint8_t, 6>, 2> start_tile_definitions;
|
||||
/* 0000 */ le_uint16_t width = 0;
|
||||
/* 0002 */ le_uint16_t height = 0;
|
||||
/* 0004 */ parray<parray<uint8_t, 0x10>, 0x10> tiles;
|
||||
/* 0104 */ parray<parray<uint8_t, 6>, 2> start_tile_definitions;
|
||||
/* 0110 */
|
||||
|
||||
MapState();
|
||||
void clear();
|
||||
@@ -22,18 +23,19 @@ struct MapState {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MapAndRulesState {
|
||||
MapState map;
|
||||
uint8_t num_players;
|
||||
uint8_t unused1;
|
||||
uint8_t environment_number;
|
||||
uint8_t num_players_per_team;
|
||||
uint8_t num_team0_players;
|
||||
uint8_t unused2;
|
||||
le_uint16_t start_facing_directions;
|
||||
uint32_t unused3;
|
||||
le_uint32_t map_number;
|
||||
uint32_t unused4;
|
||||
Rules rules;
|
||||
/* 0000 */ MapState map;
|
||||
/* 0110 */ uint8_t num_players = 0;
|
||||
/* 0111 */ uint8_t unused1 = 0;
|
||||
/* 0112 */ uint8_t environment_number = 0;
|
||||
/* 0113 */ uint8_t num_players_per_team = 0;
|
||||
/* 0114 */ uint8_t num_team0_players = 0;
|
||||
/* 0115 */ uint8_t unused2 = 0;
|
||||
/* 0116 */ le_uint16_t start_facing_directions = 0;
|
||||
/* 0118 */ be_uint32_t unknown_a3 = 0;
|
||||
/* 011C */ le_uint32_t map_number = 0;
|
||||
/* 0120 */ be_uint32_t unused4 = 0;
|
||||
/* 0124 */ Rules rules;
|
||||
/* 0138 */
|
||||
|
||||
MapAndRulesState();
|
||||
void clear();
|
||||
@@ -45,11 +47,31 @@ struct MapAndRulesState {
|
||||
void clear_occupied_bit_for_tile(uint8_t x, uint8_t y);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MapAndRulesStateTrial {
|
||||
/* 0000 */ MapState map;
|
||||
/* 0110 */ uint8_t num_players = 0;
|
||||
/* 0111 */ uint8_t unused1 = 0;
|
||||
/* 0112 */ uint8_t environment_number = 0;
|
||||
/* 0113 */ uint8_t num_players_per_team = 0;
|
||||
/* 0114 */ uint8_t num_team0_players = 0;
|
||||
/* 0115 */ uint8_t unused2 = 0;
|
||||
/* 0116 */ le_uint16_t unused5 = 0;
|
||||
/* 0118 */ be_uint32_t unknown_a3 = 0;
|
||||
/* 011C */ le_uint32_t map_number = 0;
|
||||
/* 0120 */ be_uint32_t unused4 = 0;
|
||||
/* 0124 */ RulesTrial rules;
|
||||
/* 0130 */
|
||||
|
||||
MapAndRulesStateTrial() = default;
|
||||
MapAndRulesStateTrial(const MapAndRulesState& state);
|
||||
operator MapAndRulesState() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct OverlayState {
|
||||
parray<parray<uint8_t, 0x10>, 0x10> tiles;
|
||||
parray<le_uint32_t, 5> unused1;
|
||||
parray<le_uint32_t, 0x10> unused2;
|
||||
parray<le_uint16_t, 0x10> unused3;
|
||||
parray<le_uint16_t, 0x10> trap_card_ids_nte; // Unused on non-NTE
|
||||
|
||||
OverlayState();
|
||||
void clear();
|
||||
|
||||
+351
-166
@@ -42,7 +42,7 @@ PlayerState::PlayerState(uint8_t client_id, shared_ptr<Server> server)
|
||||
void PlayerState::init() {
|
||||
auto s = this->server();
|
||||
|
||||
if (s->player_states[this->client_id].get() != this) {
|
||||
if (s->player_states.at(this->client_id).get() != this) {
|
||||
// Note: The original code handles this, but we don't. This appears not to
|
||||
// ever happen, so we didn't bother implementing it.
|
||||
throw logic_error("replacing a player state object is not permitted");
|
||||
@@ -91,6 +91,10 @@ void PlayerState::init() {
|
||||
this->sc_card = make_shared<Card>(this->deck_state->sc_card_id(), this->sc_card_ref, this->client_id, s);
|
||||
this->sc_card->init();
|
||||
this->draw_initial_hand();
|
||||
if (s->options.is_nte()) {
|
||||
this->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
this->send_set_card_updates(true);
|
||||
}
|
||||
|
||||
s->assist_server->hand_and_equip_states[this->client_id] = this->hand_and_equip;
|
||||
s->assist_server->card_short_statuses[this->client_id] = this->card_short_statuses;
|
||||
@@ -152,7 +156,7 @@ void PlayerState::apply_assist_card_effect_on_set(
|
||||
assist_card_id = s->card_id_for_card_ref(this->card_refs[6]);
|
||||
}
|
||||
|
||||
auto assist_effect = assist_effect_number_for_card_id(assist_card_id);
|
||||
auto assist_effect = assist_effect_number_for_card_id(assist_card_id, s->options.is_nte());
|
||||
if ((assist_effect == AssistEffect::RESISTANCE) ||
|
||||
(assist_effect == AssistEffect::INDEPENDENT)) {
|
||||
this->assist_card_set_number = 0;
|
||||
@@ -172,11 +176,17 @@ void PlayerState::apply_assist_card_effect_on_set(
|
||||
}
|
||||
|
||||
if (hand_index < 6) {
|
||||
for (size_t z = 0; z < 0x10; z++) {
|
||||
if (this->deck_state->draw_card_by_ref(this->discard_log_card_refs[z])) {
|
||||
this->card_refs[hand_index] = this->discard_log_card_refs[z];
|
||||
this->discard_log_card_refs[z] = 0xFFFF;
|
||||
break;
|
||||
if (s->options.is_nte()) {
|
||||
if (this->deck_state->draw_card_by_ref(this->discard_log_card_refs[0])) {
|
||||
this->pop_from_discard_log(0);
|
||||
}
|
||||
} else {
|
||||
for (size_t z = 0; z < 0x10; z++) {
|
||||
if (this->deck_state->draw_card_by_ref(this->discard_log_card_refs[z])) {
|
||||
this->card_refs[hand_index] = this->discard_log_card_refs[z];
|
||||
this->discard_log_card_refs[z] = 0xFFFF;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +208,9 @@ void PlayerState::apply_assist_card_effect_on_set(
|
||||
|
||||
case AssistEffect::SKIP_SET:
|
||||
case AssistEffect::SKIP_ACT:
|
||||
this->assist_delay_turns = 2;
|
||||
if (!s->options.is_nte()) {
|
||||
this->assist_delay_turns = 2;
|
||||
}
|
||||
break;
|
||||
|
||||
case AssistEffect::NECROMANCER: {
|
||||
@@ -240,11 +252,16 @@ void PlayerState::apply_assist_card_effect_on_set(
|
||||
}
|
||||
}
|
||||
}
|
||||
this->on_cards_destroyed();
|
||||
|
||||
bool is_nte = s->options.is_nte();
|
||||
if (!is_nte) {
|
||||
this->on_cards_destroyed();
|
||||
}
|
||||
this->atk_points = min<uint8_t>(9, this->atk_points + (total_cost >> 1));
|
||||
this->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
s->send_6xB4x05();
|
||||
if (!is_nte) {
|
||||
s->send_6xB4x05();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -303,86 +320,104 @@ void PlayerState::apply_assist_card_effect_on_set(
|
||||
s->update_battle_state_flags_and_send_6xB4x03_if_needed();
|
||||
|
||||
this->num_destroyed_fcs = 0;
|
||||
s->team_num_cards_destroyed[this->team_id] = 0;
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
const auto other_ps = s->get_player_state(client_id);
|
||||
if (other_ps && (this->team_id == other_ps->get_team_id())) {
|
||||
auto card = other_ps->get_sc_card();
|
||||
if (card) {
|
||||
card->num_cards_destroyed_by_team_at_set_time = 0;
|
||||
card->num_destroyed_ally_fcs = 0;
|
||||
}
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
auto set_card = other_ps->get_set_card(set_index);
|
||||
if (set_card) {
|
||||
set_card->num_cards_destroyed_by_team_at_set_time = 0;
|
||||
set_card->num_destroyed_ally_fcs = 0;
|
||||
if (!s->options.is_nte()) {
|
||||
s->team_num_cards_destroyed[this->team_id] = 0;
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
const auto other_ps = s->get_player_state(client_id);
|
||||
if (other_ps && (this->team_id == other_ps->get_team_id())) {
|
||||
auto card = other_ps->get_sc_card();
|
||||
if (card) {
|
||||
card->num_cards_destroyed_by_team_at_set_time = 0;
|
||||
card->num_destroyed_ally_fcs = 0;
|
||||
}
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
auto set_card = other_ps->get_set_card(set_index);
|
||||
if (set_card) {
|
||||
set_card->num_cards_destroyed_by_team_at_set_time = 0;
|
||||
set_card->num_destroyed_ally_fcs = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case AssistEffect::SLOW_TIME:
|
||||
case AssistEffect::SLOW_TIME: {
|
||||
bool is_nte = s->options.is_nte();
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
auto other_ps = s->get_player_state(client_id);
|
||||
if (!other_ps) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (other_ps->assist_remaining_turns < 10) {
|
||||
if (is_nte
|
||||
? (other_ps->assist_remaining_turns != 90 && other_ps->assist_remaining_turns != 99)
|
||||
: (other_ps->assist_remaining_turns < 10)) {
|
||||
other_ps->assist_remaining_turns = min<uint8_t>(9, other_ps->assist_remaining_turns << 1);
|
||||
}
|
||||
|
||||
for (ssize_t set_index = -1; set_index < 8; set_index++) {
|
||||
for (ssize_t set_index = is_nte ? 0 : -1; set_index < 8; set_index++) {
|
||||
auto card = (set_index == -1)
|
||||
? other_ps->get_sc_card()
|
||||
: other_ps->get_set_card(set_index);
|
||||
if (card) {
|
||||
for (size_t cond_index = 0; cond_index < 9; cond_index++) {
|
||||
auto& cond = card->action_chain.conditions[cond_index];
|
||||
if ((cond.type != ConditionType::NONE) &&
|
||||
(cond.remaining_turns < 10)) {
|
||||
if (cond.type == ConditionType::NONE) {
|
||||
continue;
|
||||
}
|
||||
if (is_nte) {
|
||||
if (cond.remaining_turns < 49) {
|
||||
cond.remaining_turns <<= 1;
|
||||
}
|
||||
} else if (cond.remaining_turns < 10) {
|
||||
cond.remaining_turns = min<uint8_t>(9, cond.remaining_turns << 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
other_ps->send_set_card_updates();
|
||||
if (!is_nte) {
|
||||
other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
other_ps->send_set_card_updates();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case AssistEffect::QUICK_TIME:
|
||||
case AssistEffect::QUICK_TIME: {
|
||||
bool is_nte = s->options.is_nte();
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
auto other_ps = s->get_player_state(client_id);
|
||||
if (!other_ps) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (other_ps->assist_remaining_turns < 10) {
|
||||
if (is_nte
|
||||
? (other_ps->assist_remaining_turns != 90 && other_ps->assist_remaining_turns != 99)
|
||||
: (other_ps->assist_remaining_turns < 10)) {
|
||||
other_ps->assist_remaining_turns = ((other_ps->assist_remaining_turns + 1) >> 1);
|
||||
}
|
||||
|
||||
for (ssize_t set_index = -1; set_index < 8; set_index++) {
|
||||
for (ssize_t set_index = is_nte ? 0 : -1; set_index < 8; set_index++) {
|
||||
auto card = (set_index == -1)
|
||||
? other_ps->get_sc_card()
|
||||
: other_ps->get_set_card(set_index);
|
||||
if (card) {
|
||||
for (size_t cond_index = 0; cond_index < 9; cond_index++) {
|
||||
auto& cond = card->action_chain.conditions[cond_index];
|
||||
if ((cond.type != ConditionType::NONE) &&
|
||||
(cond.remaining_turns < 10)) {
|
||||
if ((cond.type != ConditionType::NONE) && (cond.remaining_turns < (is_nte ? 99 : 10))) {
|
||||
cond.remaining_turns = (cond.remaining_turns + 1) >> 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
other_ps->send_set_card_updates();
|
||||
if (!is_nte) {
|
||||
other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
other_ps->send_set_card_updates();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case AssistEffect::SQUEEZE:
|
||||
this->set_random_assist_card_from_hand_for_free();
|
||||
@@ -393,7 +428,7 @@ void PlayerState::apply_assist_card_effect_on_set(
|
||||
break;
|
||||
|
||||
case AssistEffect::SKIP_TURN:
|
||||
if (!setter_ps || (setter_ps->team_id == this->team_id)) {
|
||||
if (!s->options.is_nte() && (!setter_ps || (setter_ps->team_id == this->team_id))) {
|
||||
this->assist_delay_turns = 6;
|
||||
} else {
|
||||
this->assist_delay_turns = 5;
|
||||
@@ -415,7 +450,7 @@ void PlayerState::apply_dice_effects() {
|
||||
case AssistEffect::DICE_FEVER:
|
||||
for (size_t die_index = 0; die_index < 2; die_index++) {
|
||||
if (this->dice_results[die_index] > 0) {
|
||||
this->dice_results[die_index] = 5;
|
||||
this->dice_results[die_index] = s->options.is_nte() ? 6 : 5;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -434,6 +469,9 @@ void PlayerState::apply_dice_effects() {
|
||||
}
|
||||
break;
|
||||
case AssistEffect::DICE_FEVER_PLUS:
|
||||
if (s->options.is_nte()) {
|
||||
break;
|
||||
}
|
||||
for (size_t die_index = 0; die_index < 2; die_index++) {
|
||||
if (this->dice_results[die_index] > 0) {
|
||||
this->dice_results[die_index] = 6;
|
||||
@@ -473,6 +511,16 @@ void PlayerState::compute_total_set_cards_cost() {
|
||||
}
|
||||
}
|
||||
|
||||
size_t PlayerState::count_set_cards_for_env_stats_nte() const {
|
||||
size_t ret = 0;
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
if (this->card_refs[8 + set_index] != 0xFFFF) {
|
||||
ret++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t PlayerState::count_set_cards() const {
|
||||
size_t ret = 0;
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
@@ -559,20 +607,21 @@ void PlayerState::discard_and_redraw_hand() {
|
||||
this->discard_ref_from_hand(this->card_refs[0]);
|
||||
}
|
||||
|
||||
G_Unknown_GC_Ep3_6xB4x2C cmd;
|
||||
cmd.change_type = 3;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.unknown_a2.clear(0xFFFFFFFF);
|
||||
s->send(cmd);
|
||||
if (!s->options.is_nte()) {
|
||||
G_Unknown_Ep3_6xB4x2C cmd;
|
||||
cmd.change_type = 3;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.unknown_a2.clear(0xFFFFFFFF);
|
||||
s->send(cmd);
|
||||
}
|
||||
|
||||
this->deck_state->restart();
|
||||
this->draw_hand();
|
||||
this->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
}
|
||||
|
||||
bool PlayerState::discard_card_or_add_to_draw_pile(
|
||||
uint16_t card_ref, bool add_to_draw_pile) {
|
||||
bool PlayerState::discard_card_or_add_to_draw_pile(uint16_t card_ref, bool add_to_draw_pile) {
|
||||
ssize_t set_index = this->set_index_for_card_ref(card_ref);
|
||||
if (set_index < 0) {
|
||||
return false;
|
||||
@@ -580,7 +629,15 @@ bool PlayerState::discard_card_or_add_to_draw_pile(
|
||||
|
||||
this->deck_state->set_card_discarded(card_ref);
|
||||
this->card_refs[set_index + 8] = 0xFFFF;
|
||||
this->set_cards[set_index]->card_flags |= 2;
|
||||
auto card = this->set_cards[set_index];
|
||||
if (card) {
|
||||
if (this->server()->options.is_nte()) {
|
||||
card->update_stats_on_destruction();
|
||||
this->set_cards[set_index].reset();
|
||||
} else {
|
||||
card->card_flags |= 2;
|
||||
}
|
||||
}
|
||||
if (add_to_draw_pile) {
|
||||
this->deck_state->set_card_ref_drawable_at_end(card_ref);
|
||||
}
|
||||
@@ -654,17 +711,21 @@ bool PlayerState::do_mulligan() {
|
||||
this->discard_ref_from_hand(this->card_refs[0]);
|
||||
}
|
||||
|
||||
G_Unknown_GC_Ep3_6xB4x2C cmd;
|
||||
cmd.change_type = 3;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.unknown_a2.clear(0xFFFFFFFF);
|
||||
s->send(cmd);
|
||||
if (!s->options.is_nte()) {
|
||||
G_Unknown_Ep3_6xB4x2C cmd;
|
||||
cmd.change_type = 3;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.unknown_a2.clear(0xFFFFFFFF);
|
||||
s->send(cmd);
|
||||
}
|
||||
|
||||
this->deck_state->do_mulligan();
|
||||
this->deck_state->do_mulligan(s->options.is_nte());
|
||||
this->draw_hand(5);
|
||||
|
||||
this->discard_log_card_refs.clear(0xFFFF);
|
||||
if (!s->options.is_nte()) {
|
||||
this->discard_log_card_refs.clear(0xFFFF);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -675,7 +736,7 @@ void PlayerState::draw_hand(ssize_t override_count) {
|
||||
size_t num_assists = s->assist_server->compute_num_assist_effects_for_client(this->client_id);
|
||||
for (size_t z = 0; z < num_assists; z++) {
|
||||
auto eff = s->assist_server->get_active_assist_by_index(z);
|
||||
if (eff == AssistEffect::RICH_PLUS) {
|
||||
if ((eff == AssistEffect::RICH_PLUS) && !s->options.is_nte()) {
|
||||
count = 4 - this->get_hand_size();
|
||||
} else if (eff == AssistEffect::RICH) {
|
||||
count = 6 - this->get_hand_size();
|
||||
@@ -694,7 +755,7 @@ void PlayerState::draw_hand(ssize_t override_count) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (s->get_setup_phase() == SetupPhase::MAIN_BATTLE) {
|
||||
if (!s->options.is_nte() && (s->get_setup_phase() == SetupPhase::MAIN_BATTLE)) {
|
||||
this->stats.num_cards_drawn++;
|
||||
}
|
||||
}
|
||||
@@ -791,8 +852,7 @@ uint8_t PlayerState::get_atk_points() const {
|
||||
return this->atk_points;
|
||||
}
|
||||
|
||||
void PlayerState::get_short_status_for_card_index_in_hand(
|
||||
size_t hand_index, CardShortStatus* stat) const {
|
||||
void PlayerState::get_short_status_for_card_index_in_hand(size_t hand_index, CardShortStatus* stat) const {
|
||||
stat->card_ref = this->card_refs[hand_index - 1];
|
||||
}
|
||||
|
||||
@@ -885,7 +945,7 @@ bool PlayerState::is_team_turn() const {
|
||||
}
|
||||
|
||||
void PlayerState::log_discard(uint16_t card_ref, uint16_t reason) {
|
||||
for (size_t z = 15; z > 0; z--) {
|
||||
for (size_t z = this->discard_log_card_refs.size() - 1; z > 0; z--) {
|
||||
this->discard_log_card_refs[z] = this->discard_log_card_refs[z - 1];
|
||||
this->discard_log_reasons[z] = this->discard_log_reasons[z - 1];
|
||||
}
|
||||
@@ -893,8 +953,28 @@ void PlayerState::log_discard(uint16_t card_ref, uint16_t reason) {
|
||||
this->discard_log_reasons[0] = reason;
|
||||
}
|
||||
|
||||
bool PlayerState::move_card_to_location_by_card_index(
|
||||
size_t card_index, const Location& new_loc) {
|
||||
uint16_t PlayerState::pop_from_discard_log(uint16_t) {
|
||||
// NTE appears to have a bug here (or some obviated code): it searches for an
|
||||
// entry with the given reason, then ignores the result of that search and
|
||||
// always returns the first entry instead.
|
||||
// size_t z;
|
||||
// for (size_t z = 0; z < this->discard_log_card_refs.size(); z++) {
|
||||
// if ((this->discard_log_card_refs[z] != 0xFFFF) && (this->discard_log_reasons[z] == reason)) {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
uint16_t ret = this->discard_log_card_refs[0];
|
||||
for (size_t z = 0; z < this->discard_log_card_refs.size() - 1; z++) {
|
||||
this->discard_log_card_refs[z] = this->discard_log_card_refs[z + 1];
|
||||
this->discard_log_reasons[z] = this->discard_log_reasons[z + 1];
|
||||
}
|
||||
this->discard_log_card_refs[this->discard_log_card_refs.size() - 1] = 0xFFFF;
|
||||
this->discard_log_reasons[this->discard_log_reasons.size() - 1] = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool PlayerState::move_card_to_location_by_card_index(size_t card_index, const Location& new_loc) {
|
||||
auto s = this->server();
|
||||
|
||||
shared_ptr<Card> card;
|
||||
@@ -1015,15 +1095,18 @@ void PlayerState::on_cards_destroyed() {
|
||||
void PlayerState::replace_all_set_assists_with_random_assists() {
|
||||
auto s = this->server();
|
||||
|
||||
bool is_nte = s->options.is_nte();
|
||||
const auto& assist_card_ids = all_assist_card_ids(is_nte);
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
auto other_ps = s->get_player_state(client_id);
|
||||
if (other_ps &&
|
||||
((other_ps->card_refs[6] != 0xFFFF) || (other_ps->set_assist_card_id != 0xFFFF))) {
|
||||
((other_ps->card_refs[6] != 0xFFFF) || (!is_nte && (other_ps->set_assist_card_id != 0xFFFF)))) {
|
||||
uint16_t card_id = 0x0130;
|
||||
while (card_id == 0x0130) { // God Whim
|
||||
size_t index = s->get_random(ALL_ASSIST_CARD_IDS.size());
|
||||
card_id = ALL_ASSIST_CARD_IDS[index];
|
||||
if (!this->god_whim_can_use_hidden_cards) {
|
||||
size_t index = s->get_random(assist_card_ids.size());
|
||||
card_id = assist_card_ids[index];
|
||||
// In NTE, God Whim can use ANY card, even unobtainable cards.
|
||||
if (!is_nte && !this->god_whim_can_use_hidden_cards) {
|
||||
auto ce = s->definition_for_card_id(card_id);
|
||||
if (!ce || ce->def.cannot_drop) {
|
||||
continue;
|
||||
@@ -1050,13 +1133,15 @@ bool PlayerState::replace_assist_card_by_id(uint16_t card_id) {
|
||||
this->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
s->assist_server->populate_effects();
|
||||
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
auto other_ps = s->get_player_state(client_id);
|
||||
if (other_ps) {
|
||||
uint32_t prev_assist_flags = other_ps->assist_flags;
|
||||
other_ps->set_assist_flags_from_assist_effects();
|
||||
if (prev_assist_flags != other_ps->assist_flags) {
|
||||
other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
if (!s->options.is_nte()) {
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
auto other_ps = s->get_player_state(client_id);
|
||||
if (other_ps) {
|
||||
uint32_t prev_assist_flags = other_ps->assist_flags;
|
||||
other_ps->set_assist_flags_from_assist_effects();
|
||||
if (prev_assist_flags != other_ps->assist_flags) {
|
||||
other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1108,7 +1193,12 @@ bool PlayerState::return_set_card_to_hand1(uint16_t card_ref) {
|
||||
if (card && (card->get_card_ref() == card_ref)) {
|
||||
uint16_t set_card_ref = this->card_refs[set_index + 8];
|
||||
this->card_refs[set_index + 8] = 0xFFFF;
|
||||
card->card_flags |= 2;
|
||||
if (this->server()->options.is_nte()) {
|
||||
card->update_stats_on_destruction();
|
||||
this->set_cards[set_index].reset();
|
||||
} else {
|
||||
card->card_flags |= 2;
|
||||
}
|
||||
this->deck_state->set_card_discarded(set_card_ref);
|
||||
if (this->deck_state->draw_card_by_ref(set_card_ref)) {
|
||||
this->card_refs[hand_index] = set_card_ref;
|
||||
@@ -1148,30 +1238,35 @@ uint8_t PlayerState::roll_dice_with_effects(size_t num_dice) {
|
||||
|
||||
void PlayerState::send_set_card_updates(bool always_send) {
|
||||
auto s = this->server();
|
||||
bool is_nte = s->options.is_nte();
|
||||
|
||||
uint16_t mask;
|
||||
if (!this->sc_card) {
|
||||
uint16_t mask = 0;
|
||||
if (this->sc_card) {
|
||||
this->sc_card->send_6xB4x4E_4C_4D_if_needed(always_send);
|
||||
} else if (is_nte) {
|
||||
this->send_6xB4x0A_for_set_card(0);
|
||||
} else {
|
||||
this->set_card_action_chains->at(0).clear();
|
||||
this->set_card_action_metadatas->at(0).clear();
|
||||
mask = 1;
|
||||
} else {
|
||||
this->sc_card->send_6xB4x4E_4C_4D_if_needed(always_send);
|
||||
mask = 0;
|
||||
mask |= 1;
|
||||
}
|
||||
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
auto card = this->set_cards[set_index];
|
||||
if (!card) {
|
||||
if (card) {
|
||||
card->send_6xB4x4E_4C_4D_if_needed(always_send);
|
||||
} else if (is_nte) {
|
||||
this->send_6xB4x0A_for_set_card(set_index + 1);
|
||||
} else {
|
||||
mask |= 1 << (set_index + 1);
|
||||
this->set_card_action_chains->at(set_index + 1).clear();
|
||||
this->set_card_action_metadatas->at(set_index + 1).clear();
|
||||
} else {
|
||||
card->send_6xB4x4E_4C_4D_if_needed(always_send);
|
||||
}
|
||||
}
|
||||
|
||||
// mask will always be 0 here if is_nte is true
|
||||
if (mask && !s->get_should_copy_prev_states_to_current_states()) {
|
||||
G_ClearSetCardConditions_GC_Ep3_6xB4x4F cmd;
|
||||
G_ClearSetCardConditions_Ep3_6xB4x4F cmd;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.clear_mask = mask;
|
||||
s->send(cmd);
|
||||
@@ -1246,12 +1341,15 @@ bool PlayerState::set_card_from_hand(
|
||||
|
||||
this->deck_state->set_card_ref_in_play(card_ref);
|
||||
|
||||
bool is_nte = s->options.is_nte();
|
||||
auto ce = s->definition_for_card_ref(card_ref);
|
||||
if (ce->def.type == CardType::ITEM || ce->def.type == CardType::CREATURE) {
|
||||
if ((card_index < 7) || (card_index >= 15)) {
|
||||
return 0;
|
||||
}
|
||||
this->card_refs[card_index + 1] = card_ref;
|
||||
// Note: NTE doesn't call the destructor on the existing card, if there is
|
||||
// one. Is that a bug?
|
||||
this->set_cards[card_index - 7] = make_shared<Card>(s->card_id_for_card_ref(card_ref), card_ref, this->client_id, s);
|
||||
auto new_card = this->set_cards[card_index - 7];
|
||||
new_card->init();
|
||||
@@ -1260,6 +1358,8 @@ bool PlayerState::set_card_from_hand(
|
||||
new_card->loc.x = loc->x;
|
||||
new_card->loc.y = loc->y;
|
||||
}
|
||||
// Note: NTE doesn't track this, but NTE can't use it anyway, so we don't
|
||||
// check for NTE here.
|
||||
this->stats.num_item_or_creature_cards_set++;
|
||||
|
||||
} else if (ce->def.type == CardType::ASSIST) {
|
||||
@@ -1267,7 +1367,7 @@ bool PlayerState::set_card_from_hand(
|
||||
return false;
|
||||
}
|
||||
|
||||
auto target_ps = s->player_states[assist_target_client_id];
|
||||
auto target_ps = s->player_states.at(assist_target_client_id);
|
||||
if (target_ps) {
|
||||
uint16_t prev_assist_card_ref = target_ps->card_refs[6];
|
||||
target_ps->discard_set_assist_card();
|
||||
@@ -1279,7 +1379,9 @@ bool PlayerState::set_card_from_hand(
|
||||
target_ps->assist_card_set_number = s->next_assist_card_set_number++;
|
||||
|
||||
this->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
target_ps->apply_assist_card_effect_on_set(this->shared_from_this());
|
||||
if (!is_nte) {
|
||||
target_ps->apply_assist_card_effect_on_set(this->shared_from_this());
|
||||
}
|
||||
target_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
s->assist_server->populate_effects();
|
||||
|
||||
@@ -1294,26 +1396,33 @@ bool PlayerState::set_card_from_hand(
|
||||
other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
}
|
||||
}
|
||||
if (is_nte) {
|
||||
target_ps->apply_assist_card_effect_on_set(this->shared_from_this());
|
||||
}
|
||||
}
|
||||
// NTE doesn't track this, but NTE also doesn't have access to it.
|
||||
this->stats.num_assist_cards_set++;
|
||||
}
|
||||
// NTE doesn't track this, but NTE also doesn't have access to it.
|
||||
this->stats.num_cards_set++;
|
||||
|
||||
this->compute_total_set_cards_cost();
|
||||
s->card_special->on_card_set(this->shared_from_this(), card_ref);
|
||||
if (ce->def.type == CardType::ASSIST) {
|
||||
if (!is_nte && (ce->def.type == CardType::ASSIST)) {
|
||||
s->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02();
|
||||
}
|
||||
this->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
s->send_6xB4x05();
|
||||
|
||||
G_Unknown_GC_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.card_refs[0] = card_ref;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.entry_count = 1;
|
||||
cmd.round_num = s->get_round_num();
|
||||
s->send(cmd);
|
||||
if (!is_nte) {
|
||||
G_Unknown_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.card_refs[0] = card_ref;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.entry_count = 1;
|
||||
cmd.round_num = s->get_round_num();
|
||||
s->send(cmd);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1352,17 +1461,21 @@ void PlayerState::set_initial_location() {
|
||||
this->start_facing_direction = facing_direction;
|
||||
mr->start_facing_directions |= (static_cast<uint16_t>(this->start_facing_direction) << (this->client_id << 2));
|
||||
|
||||
for (size_t y = 0; y < 0x10; y++) {
|
||||
for (size_t x = 0; x < 0x10; x++) {
|
||||
bool start_tile_found = false;
|
||||
for (size_t y = 0; (y < 0x10) && !start_tile_found; y++) {
|
||||
for (size_t x = 0; (x < 0x10) && !start_tile_found; x++) {
|
||||
if (mr->map.tiles[y][x] == (player_start_tile & 0x3F)) {
|
||||
this->sc_card->loc.x = x;
|
||||
this->sc_card->loc.y = y;
|
||||
this->sc_card->loc.direction = facing_direction;
|
||||
y = 0x10;
|
||||
start_tile_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!start_tile_found) {
|
||||
throw runtime_error("player start location not set");
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerState::set_map_occupied_bit_for_card_on_warp_tile(
|
||||
@@ -1438,8 +1551,8 @@ void PlayerState::subtract_atk_points(uint8_t cost) {
|
||||
this->atk_points2 = min<uint8_t>(this->atk_points, this->atk_points2_max);
|
||||
}
|
||||
|
||||
G_UpdateHand_GC_Ep3_6xB4x02 PlayerState::prepare_6xB4x02() const {
|
||||
G_UpdateHand_GC_Ep3_6xB4x02 cmd;
|
||||
G_UpdateHand_Ep3_6xB4x02 PlayerState::prepare_6xB4x02() const {
|
||||
G_UpdateHand_Ep3_6xB4x02 cmd;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.state.dice_results = this->dice_results;
|
||||
cmd.state.atk_points = this->atk_points;
|
||||
@@ -1483,13 +1596,14 @@ void PlayerState::update_hand_and_equip_state_and_send_6xB4x02_if_needed(
|
||||
|
||||
void PlayerState::set_random_assist_card_from_hand_for_free() {
|
||||
auto s = this->server();
|
||||
bool is_nte = s->options.is_nte();
|
||||
|
||||
vector<uint16_t> candidate_card_refs;
|
||||
for (size_t hand_index = 0; hand_index < 6; hand_index++) {
|
||||
uint16_t card_ref = this->card_refs[hand_index];
|
||||
auto ce = s->definition_for_card_ref(card_ref);
|
||||
if (ce && (ce->def.type == CardType::ASSIST) &&
|
||||
(assist_effect_number_for_card_id(ce->def.card_id) != AssistEffect::SQUEEZE)) {
|
||||
(assist_effect_number_for_card_id(ce->def.card_id, is_nte) != AssistEffect::SQUEEZE)) {
|
||||
candidate_card_refs.emplace_back(card_ref);
|
||||
}
|
||||
}
|
||||
@@ -1502,8 +1616,8 @@ void PlayerState::set_random_assist_card_from_hand_for_free() {
|
||||
}
|
||||
}
|
||||
|
||||
G_UpdateShortStatuses_GC_Ep3_6xB4x04 PlayerState::prepare_6xB4x04() const {
|
||||
G_UpdateShortStatuses_GC_Ep3_6xB4x04 cmd;
|
||||
G_UpdateShortStatuses_Ep3_6xB4x04 PlayerState::prepare_6xB4x04() const {
|
||||
G_UpdateShortStatuses_Ep3_6xB4x04 cmd;
|
||||
cmd.client_id = this->client_id;
|
||||
// Note: The original code calls memset to clear all the short status structs
|
||||
// at once. We don't do this because the default constructor has already
|
||||
@@ -1546,7 +1660,7 @@ void PlayerState::send_6xB4x04_if_needed(bool always_send) {
|
||||
if (always_send || (cmd.card_statuses != *this->card_short_statuses)) {
|
||||
auto s = this->server();
|
||||
*this->card_short_statuses = cmd.card_statuses;
|
||||
if (!s->get_should_copy_prev_states_to_current_states()) {
|
||||
if (s->options.is_nte() || !s->get_should_copy_prev_states_to_current_states()) {
|
||||
s->send(cmd);
|
||||
}
|
||||
}
|
||||
@@ -1569,35 +1683,35 @@ vector<uint16_t> PlayerState::get_card_refs_within_range_from_all_players(
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PlayerState::unknown_80239460() {
|
||||
void PlayerState::draw_phase_before() {
|
||||
if (this->sc_card) {
|
||||
this->sc_card->unknown_80235AA0();
|
||||
this->sc_card->draw_phase_before();
|
||||
}
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
if (this->set_cards[set_index]) {
|
||||
this->set_cards[set_index]->unknown_80235AA0();
|
||||
this->set_cards[set_index]->draw_phase_before();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerState::unknown_802394C4() {
|
||||
void PlayerState::action_phase_before() {
|
||||
if (this->sc_card) {
|
||||
this->sc_card->unknown_80235AD4();
|
||||
this->sc_card->action_phase_before();
|
||||
}
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
if (this->set_cards[set_index]) {
|
||||
this->set_cards[set_index]->unknown_80235AD4();
|
||||
this->set_cards[set_index]->action_phase_before();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerState::unknown_80239528() {
|
||||
void PlayerState::move_phase_before() {
|
||||
if (this->sc_card) {
|
||||
this->sc_card->unknown_80235B10();
|
||||
this->sc_card->move_phase_before();
|
||||
}
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
if (this->set_cards[set_index]) {
|
||||
this->set_cards[set_index]->unknown_80235B10();
|
||||
this->set_cards[set_index]->move_phase_before();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1640,6 +1754,7 @@ int16_t PlayerState::get_assist_turns_remaining() {
|
||||
|
||||
bool PlayerState::set_action_cards_for_action_state(const ActionState& pa) {
|
||||
auto s = this->server();
|
||||
bool is_nte = s->options.is_nte();
|
||||
|
||||
auto attacker_card = s->card_for_set_card_ref(pa.attacker_card_ref);
|
||||
if (attacker_card) {
|
||||
@@ -1647,67 +1762,78 @@ bool PlayerState::set_action_cards_for_action_state(const ActionState& pa) {
|
||||
}
|
||||
|
||||
auto action_type = s->ruler_server->get_pending_action_type(pa);
|
||||
this->subtract_or_check_atk_or_def_points_for_action(pa, 1);
|
||||
if (!is_nte) {
|
||||
this->subtract_or_check_atk_or_def_points_for_action(pa, 1);
|
||||
}
|
||||
|
||||
if (action_type == ActionType::ATTACK) {
|
||||
G_Unknown_GC_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.round_num = s->get_round_num();
|
||||
cmd.entry_count = 0;
|
||||
auto card = s->card_for_set_card_ref(pa.attacker_card_ref);
|
||||
if (card) {
|
||||
card->loc.direction = pa.facing_direction;
|
||||
|
||||
G_Unknown_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.round_num = s->get_round_num();
|
||||
cmd.entry_count = 0;
|
||||
size_t z = 0;
|
||||
do {
|
||||
card->unknown_80237A90(pa, pa.action_card_refs[z]);
|
||||
card->unknown_802379BC(pa.action_card_refs[z]);
|
||||
if (pa.action_card_refs[z] != 0xFFFF) {
|
||||
cmd.card_refs[z] = pa.action_card_refs[z];
|
||||
cmd.entry_count++;
|
||||
}
|
||||
auto ce = s->definition_for_card_ref(pa.action_card_refs[z]);
|
||||
if (ce) {
|
||||
auto card_class = ce->def.card_class();
|
||||
if ((card_class == CardClass::TECH) ||
|
||||
(card_class == CardClass::PHOTON_BLAST) ||
|
||||
(card_class == CardClass::BOSS_TECH)) {
|
||||
this->stats.num_tech_cards_set++;
|
||||
if (!is_nte) {
|
||||
if (pa.action_card_refs[z] != 0xFFFF) {
|
||||
cmd.card_refs[z] = pa.action_card_refs[z];
|
||||
cmd.entry_count++;
|
||||
}
|
||||
if ((card_class == CardClass::ATTACK_ACTION) ||
|
||||
(card_class == CardClass::CONNECT_ONLY_ATTACK_ACTION) ||
|
||||
(card_class == CardClass::BOSS_ATTACK_ACTION)) {
|
||||
this->stats.num_attack_actions_set++;
|
||||
auto ce = s->definition_for_card_ref(pa.action_card_refs[z]);
|
||||
if (ce) {
|
||||
auto card_class = ce->def.card_class();
|
||||
if (card_class_is_tech_like(card_class, is_nte)) {
|
||||
this->stats.num_tech_cards_set++;
|
||||
}
|
||||
if ((card_class == CardClass::ATTACK_ACTION) ||
|
||||
(card_class == CardClass::CONNECT_ONLY_ATTACK_ACTION) ||
|
||||
(card_class == CardClass::BOSS_ATTACK_ACTION)) {
|
||||
this->stats.num_attack_actions_set++;
|
||||
}
|
||||
this->stats.num_cards_set++;
|
||||
}
|
||||
this->stats.num_cards_set++;
|
||||
}
|
||||
z++;
|
||||
} while ((z < 8) && (pa.action_card_refs[z] != 0xFFFF));
|
||||
// Note: This is never sent on NTE because entry_count will always be zero
|
||||
if (cmd.entry_count > 0) {
|
||||
s->send(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (action_type == ActionType::DEFENSE) {
|
||||
G_Unknown_GC_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.round_num = s->get_round_num();
|
||||
for (size_t z = 0; (z < 4 * 9) && (pa.target_card_refs[z] != 0xFFFF); z++) {
|
||||
auto target_card = s->card_for_set_card_ref(pa.target_card_refs[z]);
|
||||
if (target_card) {
|
||||
target_card->unknown_802379DC(pa);
|
||||
if (this->client_id == target_card->get_client_id()) {
|
||||
this->stats.defense_actions_set_on_self++;
|
||||
} else {
|
||||
this->stats.defense_actions_set_on_ally++;
|
||||
if (!is_nte) {
|
||||
if (this->client_id == target_card->get_client_id()) {
|
||||
this->stats.defense_actions_set_on_self++;
|
||||
} else {
|
||||
this->stats.defense_actions_set_on_ally++;
|
||||
}
|
||||
this->stats.num_cards_set++;
|
||||
}
|
||||
this->stats.num_cards_set++;
|
||||
}
|
||||
}
|
||||
cmd.card_refs[0] = pa.defense_card_ref;
|
||||
cmd.entry_count = 1;
|
||||
s->send(cmd);
|
||||
if (!is_nte) {
|
||||
G_Unknown_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.round_num = s->get_round_num();
|
||||
cmd.card_refs[0] = pa.defense_card_ref;
|
||||
cmd.entry_count = 1;
|
||||
s->send(cmd);
|
||||
}
|
||||
}
|
||||
if (is_nte) {
|
||||
this->subtract_or_check_atk_or_def_points_for_action(pa, 1);
|
||||
}
|
||||
for (size_t z = 0; (z < pa.action_card_refs.size()) && (pa.action_card_refs[z] != 0xFFFF); z++) {
|
||||
this->discard_ref_from_hand(pa.action_card_refs[z]);
|
||||
@@ -1716,13 +1842,13 @@ bool PlayerState::set_action_cards_for_action_state(const ActionState& pa) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlayerState::unknown_8023C174() {
|
||||
void PlayerState::dice_phase_before() {
|
||||
if (this->sc_card) {
|
||||
this->sc_card->unknown_8023813C();
|
||||
this->sc_card->dice_phase_before();
|
||||
}
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
if (this->set_cards[set_index]) {
|
||||
this->set_cards[set_index]->unknown_8023813C();
|
||||
this->set_cards[set_index]->dice_phase_before();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1746,13 +1872,13 @@ void PlayerState::unknown_8023C174() {
|
||||
this->assist_flags &= (AssistFlag::HAS_WON_BATTLE |
|
||||
AssistFlag::WINNER_DECIDED_BY_DEFEAT |
|
||||
AssistFlag::WINNER_DECIDED_BY_RANDOM |
|
||||
AssistFlag::ELIGIBLE_FOR_DICE_BOOST);
|
||||
(this->server()->options.is_nte() ? AssistFlag::NONE : AssistFlag::ELIGIBLE_FOR_DICE_BOOST));
|
||||
this->set_assist_flags_from_assist_effects();
|
||||
this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(0);
|
||||
this->send_set_card_updates(0);
|
||||
this->send_set_card_updates();
|
||||
}
|
||||
|
||||
void PlayerState::handle_homesick_assist_effect(shared_ptr<Card> card) {
|
||||
void PlayerState::handle_homesick_assist_effect_from_bomb(shared_ptr<Card> card) {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
@@ -1773,14 +1899,26 @@ void PlayerState::handle_homesick_assist_effect(shared_ptr<Card> card) {
|
||||
if (s->assist_server->get_active_assist_by_index(z) == AssistEffect::HOMESICK) {
|
||||
this->return_set_card_to_hand2(card_ref);
|
||||
this->log_discard(card_ref, 1);
|
||||
this->set_cards[set_index]->card_flags |= 2;
|
||||
// On NTE, the card is destroyed immediately
|
||||
if (s->options.is_nte()) {
|
||||
this->set_cards[set_index]->update_stats_on_destruction();
|
||||
this->set_cards[set_index].reset();
|
||||
} else {
|
||||
this->set_cards[set_index]->card_flags |= 2;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->deck_state->set_card_ref_drawable_next(card_ref)) {
|
||||
this->log_discard(card_ref, 1);
|
||||
this->set_cards[set_index]->card_flags |= 2;
|
||||
// On NTE, the card is destroyed immediately
|
||||
if (s->options.is_nte()) {
|
||||
this->set_cards[set_index]->update_stats_on_destruction();
|
||||
this->set_cards[set_index].reset();
|
||||
} else {
|
||||
this->set_cards[set_index]->card_flags |= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1792,7 +1930,7 @@ void PlayerState::apply_main_die_assist_effects(uint8_t* die_value) const {
|
||||
for (size_t z = 0; z < num_assists; z++) {
|
||||
switch (s->assist_server->get_active_assist_by_index(z)) {
|
||||
case AssistEffect::DICE_FEVER:
|
||||
*die_value = 5;
|
||||
*die_value = s->options.is_nte() ? 6 : 5;
|
||||
break;
|
||||
case AssistEffect::DICE_HALF:
|
||||
*die_value = ((*die_value + 1) >> 1);
|
||||
@@ -1801,7 +1939,9 @@ void PlayerState::apply_main_die_assist_effects(uint8_t* die_value) const {
|
||||
(*die_value)++;
|
||||
break;
|
||||
case AssistEffect::DICE_FEVER_PLUS:
|
||||
*die_value = 6;
|
||||
if (!s->options.is_nte()) {
|
||||
*die_value = 6;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -1809,10 +1949,17 @@ void PlayerState::apply_main_die_assist_effects(uint8_t* die_value) const {
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerState::roll_main_dice() {
|
||||
void PlayerState::roll_main_dice_or_apply_after_effects() {
|
||||
auto s = this->server();
|
||||
const auto& rules = s->map_and_rules->rules;
|
||||
|
||||
// In NTE, the dice behave differently - there is no minimum, and instead the
|
||||
// player can specify a fixed value for each die or a random value (1-6). The
|
||||
// implementation of this function is therefore quite different on NTE, but
|
||||
// since we already support custom ranges for ATK and DEF dice, we just use
|
||||
// the non-NTE logic and assign the dice ranges at battle start time to yield
|
||||
// the NTE behavior. (See RulesTrial in DataIndexes.cc for how this is done.)
|
||||
|
||||
uint8_t min_atk_dice = rules.min_dice;
|
||||
uint8_t max_atk_dice = rules.max_dice;
|
||||
if (min_atk_dice == 0) {
|
||||
@@ -1880,12 +2027,18 @@ void PlayerState::roll_main_dice() {
|
||||
this->dice_results[0] = this->atk_points;
|
||||
this->dice_results[1] = this->def_points;
|
||||
|
||||
this->atk_points += s->team_dice_boost[this->team_id];
|
||||
this->def_points += s->team_dice_boost[this->team_id];
|
||||
if (s->options.is_nte()) {
|
||||
this->atk_bonuses = this->atk_points - atk_before_bonuses;
|
||||
this->def_bonuses = this->def_points - def_before_bonuses;
|
||||
}
|
||||
this->atk_points += s->team_dice_bonus[this->team_id];
|
||||
this->def_points += s->team_dice_bonus[this->team_id];
|
||||
this->atk_points = clamp<uint8_t>(this->atk_points, 1, 9);
|
||||
this->def_points = clamp<uint8_t>(this->def_points, 1, 9);
|
||||
this->atk_bonuses = this->atk_points - atk_before_bonuses;
|
||||
this->def_bonuses = this->def_points - def_before_bonuses;
|
||||
if (!s->options.is_nte()) {
|
||||
this->atk_bonuses = this->atk_points - atk_before_bonuses;
|
||||
this->def_bonuses = this->def_points - def_before_bonuses;
|
||||
}
|
||||
this->atk_points2 = min<uint8_t>(this->atk_points2_max, this->atk_points);
|
||||
this->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
}
|
||||
@@ -1902,7 +2055,7 @@ void PlayerState::unknown_8023C110() {
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerState::compute_team_dice_boost_after_draw_phase() {
|
||||
void PlayerState::compute_team_dice_bonus_after_draw_phase() {
|
||||
auto s = this->server();
|
||||
|
||||
if (this->sc_card) {
|
||||
@@ -1920,8 +2073,40 @@ void PlayerState::compute_team_dice_boost_after_draw_phase() {
|
||||
(s->team_client_count[current_team_turn] * 12);
|
||||
s->card_special->adjust_dice_boost_if_team_has_condition_52(
|
||||
current_team_turn, &dice_boost, 0);
|
||||
s->team_dice_boost[current_team_turn] = clamp<int16_t>(dice_boost, 0, 8);
|
||||
s->team_dice_bonus[current_team_turn] = clamp<int16_t>(dice_boost, 0, 8);
|
||||
this->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
}
|
||||
|
||||
void PlayerState::send_6xB4x0A_for_set_card(size_t set_index) {
|
||||
if (set_index >= 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto s = this->server();
|
||||
|
||||
// The original code (in NTE) calls memcmp here, but then ignores the results
|
||||
// and always copies the chain and metadata.
|
||||
// this->set_card_action_chains->at(set_index) == this->unknown_a12;
|
||||
// this->set_card_action_metadatas->at(set_index) == this->unknown_a13;
|
||||
this->set_card_action_chains->at(set_index) = this->unknown_a12;
|
||||
this->set_card_action_metadatas->at(set_index) = this->unknown_a13;
|
||||
|
||||
if (s->options.is_nte()) {
|
||||
G_UpdateActionChainAndMetadata_Ep3NTE_6xB4x0A cmd;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.index = set_index;
|
||||
cmd.chain = this->unknown_a12;
|
||||
cmd.metadata = this->unknown_a13;
|
||||
s->send(cmd);
|
||||
|
||||
} else {
|
||||
G_UpdateActionChainAndMetadata_Ep3_6xB4x0A cmd;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.index = set_index;
|
||||
cmd.chain = this->unknown_a12;
|
||||
cmd.metadata = this->unknown_a13;
|
||||
s->send(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
+19
-21
@@ -20,6 +20,7 @@ enum AssistFlag : uint32_t {
|
||||
// be bits used only by the client which are not documented here.
|
||||
|
||||
// clang-format off
|
||||
NONE = 0x0000,
|
||||
READY_TO_END_PHASE = 0x0001,
|
||||
DICE_WERE_EXCHANGED = 0x0002,
|
||||
HAS_WON_BATTLE = 0x0004,
|
||||
@@ -52,14 +53,14 @@ public:
|
||||
uint16_t card_ref_for_hand_index(size_t hand_index) const;
|
||||
int16_t compute_attack_or_defense_atk_costs(const ActionState& pa) const;
|
||||
void compute_total_set_cards_cost();
|
||||
size_t count_set_cards_for_env_stats_nte() const;
|
||||
size_t count_set_cards() const;
|
||||
size_t count_set_refs() const;
|
||||
void discard_all_assist_cards_from_hand();
|
||||
void discard_all_attack_action_cards_from_hand();
|
||||
void discard_all_item_and_creature_cards_from_hand();
|
||||
void discard_and_redraw_hand();
|
||||
bool discard_card_or_add_to_draw_pile(
|
||||
uint16_t card_ref, bool add_to_draw_pile);
|
||||
bool discard_card_or_add_to_draw_pile(uint16_t card_ref, bool add_to_draw_pile);
|
||||
void discard_random_hand_card();
|
||||
bool discard_ref_from_hand(uint16_t card_ref);
|
||||
void discard_set_assist_card();
|
||||
@@ -76,8 +77,7 @@ public:
|
||||
const Location& loc,
|
||||
uint8_t target_team_id) const;
|
||||
uint8_t get_atk_points() const;
|
||||
void get_short_status_for_card_index_in_hand(
|
||||
size_t hand_index, CardShortStatus* stat) const;
|
||||
void get_short_status_for_card_index_in_hand(size_t hand_index, CardShortStatus* stat) const;
|
||||
std::shared_ptr<DeckState> get_deck();
|
||||
uint8_t get_def_points() const;
|
||||
uint8_t get_dice_result(size_t which) const;
|
||||
@@ -96,8 +96,8 @@ public:
|
||||
bool is_mulligan_allowed() const;
|
||||
bool is_team_turn() const;
|
||||
void log_discard(uint16_t card_ref, uint16_t reason);
|
||||
bool move_card_to_location_by_card_index(
|
||||
size_t card_index, const Location& new_loc);
|
||||
uint16_t pop_from_discard_log(uint16_t reason);
|
||||
bool move_card_to_location_by_card_index(size_t card_index, const Location& new_loc);
|
||||
void move_null_hand_refs_to_end();
|
||||
void on_cards_destroyed();
|
||||
void replace_all_set_assists_with_random_assists();
|
||||
@@ -115,35 +115,33 @@ public:
|
||||
uint8_t assist_target_client_id,
|
||||
bool skip_error_checks_and_atk_sub);
|
||||
void set_initial_location();
|
||||
void set_map_occupied_bit_for_card_on_warp_tile(
|
||||
std::shared_ptr<const Card> card);
|
||||
void set_map_occupied_bit_for_card_on_warp_tile(std::shared_ptr<const Card> card);
|
||||
void set_map_occupied_bits_for_sc_and_creatures();
|
||||
void subtract_def_points(uint8_t cost);
|
||||
bool subtract_or_check_atk_or_def_points_for_action(
|
||||
const ActionState& pa, bool deduct_points);
|
||||
bool subtract_or_check_atk_or_def_points_for_action(const ActionState& pa, bool deduct_points);
|
||||
void subtract_atk_points(uint8_t cost);
|
||||
G_UpdateHand_GC_Ep3_6xB4x02 prepare_6xB4x02() const;
|
||||
void update_hand_and_equip_state_and_send_6xB4x02_if_needed(
|
||||
bool always_send = false);
|
||||
G_UpdateHand_Ep3_6xB4x02 prepare_6xB4x02() const;
|
||||
void update_hand_and_equip_state_and_send_6xB4x02_if_needed(bool always_send = false);
|
||||
void set_random_assist_card_from_hand_for_free();
|
||||
G_UpdateShortStatuses_GC_Ep3_6xB4x04 prepare_6xB4x04() const;
|
||||
G_UpdateShortStatuses_Ep3_6xB4x04 prepare_6xB4x04() const;
|
||||
void send_6xB4x04_if_needed(bool always_send = false);
|
||||
std::vector<uint16_t> get_card_refs_within_range_from_all_players(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& loc,
|
||||
CardType type) const;
|
||||
void unknown_80239460();
|
||||
void unknown_802394C4();
|
||||
void unknown_80239528();
|
||||
void draw_phase_before();
|
||||
void action_phase_before();
|
||||
void move_phase_before();
|
||||
void handle_before_turn_assist_effects();
|
||||
int16_t get_assist_turns_remaining();
|
||||
bool set_action_cards_for_action_state(const ActionState& pa);
|
||||
void unknown_8023C174();
|
||||
void handle_homesick_assist_effect(std::shared_ptr<Card> card);
|
||||
void dice_phase_before();
|
||||
void handle_homesick_assist_effect_from_bomb(std::shared_ptr<Card> card);
|
||||
void apply_main_die_assist_effects(uint8_t* die_value) const;
|
||||
void roll_main_dice();
|
||||
void roll_main_dice_or_apply_after_effects();
|
||||
void unknown_8023C110();
|
||||
void compute_team_dice_boost_after_draw_phase();
|
||||
void compute_team_dice_bonus_after_draw_phase();
|
||||
void send_6xB4x0A_for_set_card(size_t set_index);
|
||||
|
||||
private:
|
||||
std::weak_ptr<Server> w_server;
|
||||
|
||||
@@ -234,8 +234,8 @@ bool ActionChain::operator==(const ActionChain& other) const {
|
||||
(this->damage_multiplier == other.damage_multiplier) &&
|
||||
(this->attack_number == other.attack_number) &&
|
||||
(this->tp_effect_bonus == other.tp_effect_bonus) &&
|
||||
(this->unused1 == other.unused1) &&
|
||||
(this->unused2 == other.unused2) &&
|
||||
(this->physical_attack_bonus_nte == other.physical_attack_bonus_nte) &&
|
||||
(this->tech_attack_bonus_nte == other.tech_attack_bonus_nte) &&
|
||||
(this->card_ap == other.card_ap) &&
|
||||
(this->card_tp == other.card_tp) &&
|
||||
(this->flags == other.flags) &&
|
||||
@@ -254,7 +254,7 @@ std::string ActionChain::str() const {
|
||||
"attack_action_refs=%s, attack_action_ref_count=%hhu, "
|
||||
"medium=%s, target_ref_count=%hhu, subphase=%s, "
|
||||
"strikes=%hhu, damage_mult=%hhd, attack_num=%hhu, "
|
||||
"tp_bonus=%hhd, u1=%hhu, u2=%hhu, card_ap=%hhd, "
|
||||
"tp_bonus=%hhd, phys_bonus_nte=%hhu, tech_bonus_nte=%hhu, card_ap=%hhd, "
|
||||
"card_tp=%hhd, flags=%08" PRIX32 ", target_refs=%s]",
|
||||
this->effective_ap,
|
||||
this->effective_tp,
|
||||
@@ -271,8 +271,8 @@ std::string ActionChain::str() const {
|
||||
this->damage_multiplier,
|
||||
this->attack_number,
|
||||
this->tp_effect_bonus,
|
||||
this->unused1,
|
||||
this->unused2,
|
||||
this->physical_attack_bonus_nte,
|
||||
this->tech_attack_bonus_nte,
|
||||
this->card_ap,
|
||||
this->card_tp,
|
||||
this->flags.load(),
|
||||
@@ -294,8 +294,8 @@ void ActionChain::clear() {
|
||||
this->damage_multiplier = 1;
|
||||
this->attack_number = 0xFF;
|
||||
this->tp_effect_bonus = 0;
|
||||
this->unused1 = 0;
|
||||
this->unused2 = 0;
|
||||
this->physical_attack_bonus_nte = 0;
|
||||
this->tech_attack_bonus_nte = 0;
|
||||
this->card_ap = 0;
|
||||
this->card_tp = 0;
|
||||
this->flags = 0;
|
||||
@@ -319,8 +319,8 @@ void ActionChain::clear_FF() {
|
||||
this->damage_multiplier = -1;
|
||||
this->attack_number = 0xFF;
|
||||
this->tp_effect_bonus = -1;
|
||||
this->unused1 = 0xFF;
|
||||
this->unused2 = 0xFF;
|
||||
this->physical_attack_bonus_nte = 0xFF;
|
||||
this->tech_attack_bonus_nte = 0xFF;
|
||||
this->card_ap = -1;
|
||||
this->card_tp = -1;
|
||||
this->flags = 0xFFFFFFFF;
|
||||
@@ -393,8 +393,8 @@ void ActionChainWithConds::reset() {
|
||||
this->chain.effective_tp = 0;
|
||||
this->chain.ap_effect_bonus = 0;
|
||||
this->chain.tp_effect_bonus = 0;
|
||||
this->chain.unused1 = 0;
|
||||
this->chain.unused2 = 0;
|
||||
this->chain.physical_attack_bonus_nte = 0;
|
||||
this->chain.tech_attack_bonus_nte = 0;
|
||||
this->chain.damage = 0;
|
||||
this->chain.strike_count = 1;
|
||||
this->chain.damage_multiplier = 1;
|
||||
@@ -439,7 +439,7 @@ void ActionChainWithConds::compute_attack_medium(shared_ptr<Server> server) {
|
||||
if (!ce) {
|
||||
continue;
|
||||
}
|
||||
if (card_class_is_tech_like(ce->def.card_class())) {
|
||||
if (card_class_is_tech_like(ce->def.card_class(), server->options.is_nte())) {
|
||||
this->chain.attack_medium = AttackMedium::TECH;
|
||||
}
|
||||
}
|
||||
@@ -481,6 +481,85 @@ bool ActionChainWithConds::can_apply_attack() const {
|
||||
return this->check_flag(4) ? false : (this->chain.target_card_ref_count != 0);
|
||||
}
|
||||
|
||||
uint8_t ActionChainWithConds::get_adjusted_move_ability_nte(uint8_t ability) const {
|
||||
for (size_t z = 0; z < this->conditions.size(); z++) {
|
||||
const auto& cond = this->conditions[z];
|
||||
switch (cond.type) {
|
||||
case ConditionType::IMMOBILE:
|
||||
case ConditionType::FREEZE:
|
||||
ability = 0;
|
||||
break;
|
||||
case ConditionType::SET_MV_COST_TO_0:
|
||||
ability = 99;
|
||||
break;
|
||||
case ConditionType::ADD_1_TO_MV_COST:
|
||||
ability--;
|
||||
break;
|
||||
case ConditionType::SCALE_MV_COST:
|
||||
if (cond.value == 0) {
|
||||
ability = 99;
|
||||
} else {
|
||||
ability /= cond.value;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ability;
|
||||
}
|
||||
|
||||
ActionChainWithCondsTrial::ActionChainWithCondsTrial(const ActionChainWithConds& src)
|
||||
: effective_ap(src.chain.effective_ap),
|
||||
effective_tp(src.chain.effective_tp),
|
||||
ap_effect_bonus(src.chain.ap_effect_bonus),
|
||||
damage(src.chain.damage),
|
||||
acting_card_ref(src.chain.acting_card_ref),
|
||||
unknown_card_ref_a3(src.chain.unknown_card_ref_a3),
|
||||
attack_action_card_refs(src.chain.attack_action_card_refs),
|
||||
attack_action_card_ref_count(src.chain.attack_action_card_ref_count),
|
||||
attack_medium(src.chain.attack_medium),
|
||||
target_card_ref_count(src.chain.target_card_ref_count),
|
||||
action_subphase(src.chain.action_subphase),
|
||||
strike_count(src.chain.strike_count),
|
||||
damage_multiplier(src.chain.damage_multiplier),
|
||||
attack_number(src.chain.attack_number),
|
||||
tp_effect_bonus(src.chain.tp_effect_bonus),
|
||||
physical_attack_bonus_nte(src.chain.physical_attack_bonus_nte),
|
||||
tech_attack_bonus_nte(src.chain.tech_attack_bonus_nte),
|
||||
card_ap(src.chain.card_ap),
|
||||
card_tp(src.chain.card_tp),
|
||||
flags(src.chain.flags),
|
||||
conditions(src.conditions),
|
||||
target_card_refs(src.chain.target_card_refs) {}
|
||||
|
||||
ActionChainWithCondsTrial::operator ActionChainWithConds() const {
|
||||
ActionChainWithConds ret;
|
||||
ret.chain.effective_ap = this->effective_ap;
|
||||
ret.chain.effective_tp = this->effective_tp;
|
||||
ret.chain.ap_effect_bonus = this->ap_effect_bonus;
|
||||
ret.chain.damage = this->damage;
|
||||
ret.chain.acting_card_ref = this->acting_card_ref;
|
||||
ret.chain.unknown_card_ref_a3 = this->unknown_card_ref_a3;
|
||||
ret.chain.attack_action_card_refs = this->attack_action_card_refs;
|
||||
ret.chain.attack_action_card_ref_count = this->attack_action_card_ref_count;
|
||||
ret.chain.attack_medium = this->attack_medium;
|
||||
ret.chain.target_card_ref_count = this->target_card_ref_count;
|
||||
ret.chain.action_subphase = this->action_subphase;
|
||||
ret.chain.strike_count = this->strike_count;
|
||||
ret.chain.damage_multiplier = this->damage_multiplier;
|
||||
ret.chain.attack_number = this->attack_number;
|
||||
ret.chain.tp_effect_bonus = this->tp_effect_bonus;
|
||||
ret.chain.physical_attack_bonus_nte = this->physical_attack_bonus_nte;
|
||||
ret.chain.tech_attack_bonus_nte = this->tech_attack_bonus_nte;
|
||||
ret.chain.card_ap = this->card_ap;
|
||||
ret.chain.card_tp = this->card_tp;
|
||||
ret.chain.flags = this->flags;
|
||||
ret.chain.target_card_refs = this->target_card_refs;
|
||||
ret.conditions = this->conditions;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ActionMetadata::ActionMetadata() {
|
||||
this->clear();
|
||||
}
|
||||
@@ -531,6 +610,8 @@ void ActionMetadata::clear() {
|
||||
this->action_subphase = ActionSubphase::INVALID_FF;
|
||||
this->defense_power = 0;
|
||||
this->defense_bonus = 0;
|
||||
// TODO: Ep3 NTE doesn't set attack_bonus to zero here. Is the field just
|
||||
// unused in NTE?
|
||||
this->attack_bonus = 0;
|
||||
this->flags = 0;
|
||||
this->target_card_refs.clear(0xFFFF);
|
||||
@@ -759,6 +840,23 @@ const char* PlayerBattleStats::name_for_rank(uint8_t rank) {
|
||||
return RANK_NAMES[rank];
|
||||
}
|
||||
|
||||
PlayerBattleStatsTrial::PlayerBattleStatsTrial(const PlayerBattleStats& data)
|
||||
: damage_given(data.damage_given.load()),
|
||||
damage_taken(data.damage_taken.load()),
|
||||
num_opponent_cards_destroyed(data.num_opponent_cards_destroyed.load()),
|
||||
num_owned_cards_destroyed(data.num_owned_cards_destroyed.load()),
|
||||
total_move_distance(data.total_move_distance.load()) {}
|
||||
|
||||
PlayerBattleStatsTrial::operator PlayerBattleStats() const {
|
||||
PlayerBattleStats ret;
|
||||
ret.damage_given = this->damage_given.load();
|
||||
ret.damage_taken = this->damage_taken.load();
|
||||
ret.num_opponent_cards_destroyed = this->num_opponent_cards_destroyed.load();
|
||||
ret.num_owned_cards_destroyed = this->num_owned_cards_destroyed.load();
|
||||
ret.total_move_distance = this->total_move_distance.load();
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool is_card_within_range(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& anchor_loc,
|
||||
|
||||
@@ -13,19 +13,20 @@ class Server;
|
||||
class Card;
|
||||
|
||||
struct Condition {
|
||||
ConditionType type;
|
||||
uint8_t remaining_turns;
|
||||
int8_t a_arg_value;
|
||||
uint8_t dice_roll_value;
|
||||
uint8_t flags;
|
||||
uint8_t card_definition_effect_index;
|
||||
le_uint16_t card_ref;
|
||||
le_int16_t value;
|
||||
le_uint16_t condition_giver_card_ref;
|
||||
uint8_t random_percent;
|
||||
int8_t value8;
|
||||
uint8_t order;
|
||||
uint8_t unknown_a8;
|
||||
/* 00 */ ConditionType type;
|
||||
/* 01 */ uint8_t remaining_turns;
|
||||
/* 02 */ int8_t a_arg_value;
|
||||
/* 03 */ uint8_t dice_roll_value;
|
||||
/* 04 */ uint8_t flags;
|
||||
/* 05 */ uint8_t card_definition_effect_index;
|
||||
/* 06 */ le_uint16_t card_ref;
|
||||
/* 08 */ le_int16_t value;
|
||||
/* 0A */ le_uint16_t condition_giver_card_ref;
|
||||
/* 0C */ uint8_t random_percent;
|
||||
/* 0D */ int8_t value8;
|
||||
/* 0E */ uint8_t order;
|
||||
/* 0F */ uint8_t unknown_a8;
|
||||
/* 10 */
|
||||
|
||||
Condition();
|
||||
bool operator==(const Condition& other) const;
|
||||
@@ -38,16 +39,17 @@ struct Condition {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct EffectResult {
|
||||
le_uint16_t attacker_card_ref;
|
||||
le_uint16_t target_card_ref;
|
||||
int8_t value;
|
||||
int8_t current_hp;
|
||||
int8_t ap;
|
||||
int8_t tp;
|
||||
uint8_t flags;
|
||||
int8_t operation; // May be a negative condition number
|
||||
uint8_t condition_index;
|
||||
uint8_t dice_roll_value;
|
||||
/* 00 */ le_uint16_t attacker_card_ref;
|
||||
/* 02 */ le_uint16_t target_card_ref;
|
||||
/* 04 */ int8_t value;
|
||||
/* 05 */ int8_t current_hp;
|
||||
/* 06 */ int8_t ap;
|
||||
/* 07 */ int8_t tp;
|
||||
/* 08 */ uint8_t flags;
|
||||
/* 09 */ int8_t operation; // May be a negative condition number
|
||||
/* 0A */ uint8_t condition_index;
|
||||
/* 0B */ uint8_t dice_roll_value;
|
||||
/* 0C */
|
||||
|
||||
EffectResult();
|
||||
bool operator==(const EffectResult& other) const;
|
||||
@@ -59,13 +61,14 @@ struct EffectResult {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct CardShortStatus {
|
||||
le_uint16_t card_ref;
|
||||
le_uint16_t current_hp;
|
||||
le_uint32_t card_flags;
|
||||
Location loc;
|
||||
le_uint16_t unused1;
|
||||
int8_t max_hp;
|
||||
uint8_t unused2;
|
||||
/* 00 */ le_uint16_t card_ref;
|
||||
/* 02 */ le_uint16_t current_hp;
|
||||
/* 04 */ le_uint32_t card_flags;
|
||||
/* 08 */ Location loc;
|
||||
/* 0C */ le_uint16_t unused1;
|
||||
/* 0E */ int8_t max_hp;
|
||||
/* 0F */ uint8_t unused2;
|
||||
/* 10 */
|
||||
|
||||
CardShortStatus();
|
||||
bool operator==(const CardShortStatus& other) const;
|
||||
@@ -78,14 +81,15 @@ struct CardShortStatus {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionState {
|
||||
le_uint16_t client_id;
|
||||
uint8_t unused;
|
||||
Direction facing_direction;
|
||||
le_uint16_t attacker_card_ref;
|
||||
le_uint16_t defense_card_ref;
|
||||
parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
parray<le_uint16_t, 9> action_card_refs;
|
||||
le_uint16_t original_attacker_card_ref;
|
||||
/* 00 */ le_uint16_t client_id;
|
||||
/* 02 */ uint8_t unused;
|
||||
/* 03 */ Direction facing_direction;
|
||||
/* 04 */ le_uint16_t attacker_card_ref;
|
||||
/* 06 */ le_uint16_t defense_card_ref;
|
||||
/* 08 */ parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
/* 50 */ parray<le_uint16_t, 9> action_card_refs;
|
||||
/* 62 */ le_uint16_t original_attacker_card_ref;
|
||||
/* 64 */
|
||||
|
||||
ActionState();
|
||||
bool operator==(const ActionState& other) const;
|
||||
@@ -97,27 +101,30 @@ struct ActionState {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionChain {
|
||||
int8_t effective_ap;
|
||||
int8_t effective_tp;
|
||||
int8_t ap_effect_bonus;
|
||||
int8_t damage;
|
||||
le_uint16_t acting_card_ref;
|
||||
le_uint16_t unknown_card_ref_a3;
|
||||
parray<le_uint16_t, 8> attack_action_card_refs;
|
||||
uint8_t attack_action_card_ref_count;
|
||||
AttackMedium attack_medium;
|
||||
uint8_t target_card_ref_count;
|
||||
ActionSubphase action_subphase;
|
||||
uint8_t strike_count;
|
||||
int8_t damage_multiplier;
|
||||
uint8_t attack_number;
|
||||
int8_t tp_effect_bonus;
|
||||
uint8_t unused1;
|
||||
uint8_t unused2;
|
||||
int8_t card_ap;
|
||||
int8_t card_tp;
|
||||
le_uint32_t flags;
|
||||
parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
// Note: Episode 3 Trial Edition has a different format for this structure.
|
||||
// See ActionChainWithCondsTrial for details.
|
||||
/* 00 */ int8_t effective_ap;
|
||||
/* 01 */ int8_t effective_tp;
|
||||
/* 02 */ int8_t ap_effect_bonus;
|
||||
/* 03 */ int8_t damage;
|
||||
/* 04 */ le_uint16_t acting_card_ref;
|
||||
/* 06 */ le_uint16_t unknown_card_ref_a3;
|
||||
/* 08 */ parray<le_uint16_t, 8> attack_action_card_refs;
|
||||
/* 18 */ uint8_t attack_action_card_ref_count;
|
||||
/* 19 */ AttackMedium attack_medium;
|
||||
/* 1A */ uint8_t target_card_ref_count;
|
||||
/* 1B */ ActionSubphase action_subphase;
|
||||
/* 1C */ uint8_t strike_count;
|
||||
/* 1D */ int8_t damage_multiplier;
|
||||
/* 1E */ uint8_t attack_number;
|
||||
/* 1F */ int8_t tp_effect_bonus;
|
||||
/* 20 */ int8_t physical_attack_bonus_nte;
|
||||
/* 21 */ int8_t tech_attack_bonus_nte;
|
||||
/* 22 */ int8_t card_ap;
|
||||
/* 23 */ int8_t card_tp;
|
||||
/* 24 */ le_uint32_t flags;
|
||||
/* 28 */ parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
/* 70 */
|
||||
|
||||
ActionChain();
|
||||
bool operator==(const ActionChain& other) const;
|
||||
@@ -130,8 +137,9 @@ struct ActionChain {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionChainWithConds {
|
||||
ActionChain chain;
|
||||
parray<Condition, 9> conditions;
|
||||
/* 0000 */ ActionChain chain;
|
||||
/* 0070 */ parray<Condition, 9> conditions;
|
||||
/* 0100 */
|
||||
|
||||
ActionChainWithConds();
|
||||
bool operator==(const ActionChainWithConds& other) const;
|
||||
@@ -161,21 +169,56 @@ struct ActionChainWithConds {
|
||||
void set_action_subphase_from_card(std::shared_ptr<const Card> card);
|
||||
bool can_apply_attack() const;
|
||||
|
||||
uint8_t get_adjusted_move_ability_nte(uint8_t ability) const;
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionChainWithCondsTrial {
|
||||
/* 0000 */ int8_t effective_ap;
|
||||
/* 0001 */ int8_t effective_tp;
|
||||
/* 0002 */ int8_t ap_effect_bonus;
|
||||
/* 0003 */ int8_t damage;
|
||||
/* 0004 */ le_uint16_t acting_card_ref;
|
||||
/* 0006 */ le_uint16_t unknown_card_ref_a3;
|
||||
/* 0008 */ parray<le_uint16_t, 8> attack_action_card_refs;
|
||||
/* 0018 */ uint8_t attack_action_card_ref_count;
|
||||
/* 0019 */ AttackMedium attack_medium;
|
||||
/* 001A */ uint8_t target_card_ref_count;
|
||||
/* 001B */ ActionSubphase action_subphase;
|
||||
/* 001C */ uint8_t strike_count;
|
||||
/* 001D */ int8_t damage_multiplier;
|
||||
/* 001E */ uint8_t attack_number;
|
||||
/* 001F */ int8_t tp_effect_bonus;
|
||||
/* 0020 */ int8_t physical_attack_bonus_nte;
|
||||
/* 0021 */ int8_t tech_attack_bonus_nte;
|
||||
/* 0022 */ int8_t card_ap;
|
||||
/* 0023 */ int8_t card_tp;
|
||||
/* 0024 */ le_uint32_t flags;
|
||||
// The only difference between this structure and ActionChainWithConds is that
|
||||
// these two fields are in the opposite order.
|
||||
/* 0028 */ parray<Condition, 9> conditions;
|
||||
/* 00B8 */ parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
/* 0100 */
|
||||
|
||||
ActionChainWithCondsTrial() = default;
|
||||
ActionChainWithCondsTrial(const ActionChainWithConds& src);
|
||||
operator ActionChainWithConds() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionMetadata {
|
||||
le_uint16_t card_ref;
|
||||
uint8_t target_card_ref_count;
|
||||
uint8_t defense_card_ref_count;
|
||||
ActionSubphase action_subphase;
|
||||
int8_t defense_power;
|
||||
int8_t defense_bonus;
|
||||
int8_t attack_bonus;
|
||||
le_uint32_t flags;
|
||||
parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
parray<le_uint16_t, 8> defense_card_refs;
|
||||
parray<le_uint16_t, 8> original_attacker_card_refs;
|
||||
/* 00 */ le_uint16_t card_ref;
|
||||
/* 02 */ uint8_t target_card_ref_count;
|
||||
/* 03 */ uint8_t defense_card_ref_count;
|
||||
/* 04 */ ActionSubphase action_subphase;
|
||||
/* 05 */ int8_t defense_power;
|
||||
/* 06 */ int8_t defense_bonus;
|
||||
/* 07 */ int8_t attack_bonus;
|
||||
/* 08 */ le_uint32_t flags;
|
||||
/* 0C */ parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
/* 54 */ parray<le_uint16_t, 8> defense_card_refs;
|
||||
/* 64 */ parray<le_uint16_t, 8> original_attacker_card_refs;
|
||||
/* 74 */
|
||||
|
||||
ActionMetadata();
|
||||
bool operator==(const ActionMetadata& other) const;
|
||||
@@ -200,28 +243,29 @@ struct ActionMetadata {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct HandAndEquipState {
|
||||
parray<uint8_t, 2> dice_results;
|
||||
uint8_t atk_points;
|
||||
uint8_t def_points;
|
||||
uint8_t atk_points2; // TODO: rename this to something more appropriate
|
||||
uint8_t unknown_a1;
|
||||
uint8_t total_set_cards_cost;
|
||||
uint8_t is_cpu_player;
|
||||
le_uint32_t assist_flags;
|
||||
parray<le_uint16_t, 6> hand_card_refs;
|
||||
le_uint16_t assist_card_ref;
|
||||
parray<le_uint16_t, 8> set_card_refs;
|
||||
le_uint16_t sc_card_ref;
|
||||
parray<le_uint16_t, 6> hand_card_refs2;
|
||||
parray<le_uint16_t, 8> set_card_refs2;
|
||||
le_uint16_t assist_card_ref2;
|
||||
le_uint16_t assist_card_set_number;
|
||||
le_uint16_t assist_card_id;
|
||||
uint8_t assist_remaining_turns;
|
||||
uint8_t assist_delay_turns;
|
||||
uint8_t atk_bonuses;
|
||||
uint8_t def_bonuses;
|
||||
parray<uint8_t, 2> unused2;
|
||||
/* 00 */ parray<uint8_t, 2> dice_results;
|
||||
/* 02 */ uint8_t atk_points;
|
||||
/* 03 */ uint8_t def_points;
|
||||
/* 04 */ uint8_t atk_points2; // TODO: rename this to something more appropriate
|
||||
/* 05 */ uint8_t unknown_a1;
|
||||
/* 06 */ uint8_t total_set_cards_cost;
|
||||
/* 07 */ uint8_t is_cpu_player;
|
||||
/* 08 */ le_uint32_t assist_flags;
|
||||
/* 0C */ parray<le_uint16_t, 6> hand_card_refs;
|
||||
/* 18 */ le_uint16_t assist_card_ref;
|
||||
/* 1A */ parray<le_uint16_t, 8> set_card_refs;
|
||||
/* 2A */ le_uint16_t sc_card_ref;
|
||||
/* 2C */ parray<le_uint16_t, 6> hand_card_refs2;
|
||||
/* 38 */ parray<le_uint16_t, 8> set_card_refs2;
|
||||
/* 48 */ le_uint16_t assist_card_ref2;
|
||||
/* 4A */ le_uint16_t assist_card_set_number;
|
||||
/* 4C */ le_uint16_t assist_card_id;
|
||||
/* 4E */ uint8_t assist_remaining_turns;
|
||||
/* 4F */ uint8_t assist_delay_turns;
|
||||
/* 50 */ uint8_t atk_bonuses;
|
||||
/* 51 */ uint8_t def_bonuses;
|
||||
/* 52 */ parray<uint8_t, 2> unused2;
|
||||
/* 54 */
|
||||
|
||||
HandAndEquipState();
|
||||
bool operator==(const HandAndEquipState& other) const;
|
||||
@@ -234,26 +278,27 @@ struct HandAndEquipState {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerBattleStats {
|
||||
le_uint16_t damage_given;
|
||||
le_uint16_t damage_taken;
|
||||
le_uint16_t num_opponent_cards_destroyed;
|
||||
le_uint16_t num_owned_cards_destroyed;
|
||||
le_uint16_t total_move_distance;
|
||||
le_uint16_t num_cards_set;
|
||||
le_uint16_t num_item_or_creature_cards_set;
|
||||
le_uint16_t num_attack_actions_set;
|
||||
le_uint16_t num_tech_cards_set;
|
||||
le_uint16_t num_assist_cards_set;
|
||||
le_uint16_t defense_actions_set_on_self;
|
||||
le_uint16_t defense_actions_set_on_ally;
|
||||
le_uint16_t num_cards_drawn;
|
||||
le_uint16_t max_attack_damage;
|
||||
le_uint16_t max_attack_combo_size;
|
||||
le_uint16_t num_attacks_given;
|
||||
le_uint16_t num_attacks_taken;
|
||||
le_uint16_t sc_damage_taken;
|
||||
le_uint16_t action_card_negated_damage;
|
||||
le_uint16_t unused;
|
||||
/* 00 */ le_uint16_t damage_given;
|
||||
/* 02 */ le_uint16_t damage_taken;
|
||||
/* 04 */ le_uint16_t num_opponent_cards_destroyed;
|
||||
/* 06 */ le_uint16_t num_owned_cards_destroyed;
|
||||
/* 08 */ le_uint16_t total_move_distance;
|
||||
/* 0A */ le_uint16_t num_cards_set;
|
||||
/* 0C */ le_uint16_t num_item_or_creature_cards_set;
|
||||
/* 0E */ le_uint16_t num_attack_actions_set;
|
||||
/* 10 */ le_uint16_t num_tech_cards_set;
|
||||
/* 12 */ le_uint16_t num_assist_cards_set;
|
||||
/* 14 */ le_uint16_t defense_actions_set_on_self;
|
||||
/* 16 */ le_uint16_t defense_actions_set_on_ally;
|
||||
/* 18 */ le_uint16_t num_cards_drawn;
|
||||
/* 1A */ le_uint16_t max_attack_damage;
|
||||
/* 1C */ le_uint16_t max_attack_combo_size;
|
||||
/* 1E */ le_uint16_t num_attacks_given;
|
||||
/* 20 */ le_uint16_t num_attacks_taken;
|
||||
/* 22 */ le_uint16_t sc_damage_taken;
|
||||
/* 24 */ le_uint16_t action_card_negated_damage;
|
||||
/* 26 */ le_uint16_t unused;
|
||||
/* 28 */
|
||||
|
||||
PlayerBattleStats();
|
||||
void clear();
|
||||
@@ -266,6 +311,19 @@ struct PlayerBattleStats {
|
||||
static const char* name_for_rank(uint8_t rank);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerBattleStatsTrial {
|
||||
/* 00 */ le_uint32_t damage_given = 0;
|
||||
/* 04 */ le_uint32_t damage_taken = 0;
|
||||
/* 08 */ le_uint32_t num_opponent_cards_destroyed = 0;
|
||||
/* 0C */ le_uint32_t num_owned_cards_destroyed = 0;
|
||||
/* 10 */ le_uint32_t total_move_distance = 0;
|
||||
/* 14 */
|
||||
|
||||
PlayerBattleStatsTrial() = default;
|
||||
PlayerBattleStatsTrial(const PlayerBattleStats& data);
|
||||
operator PlayerBattleStats() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
std::vector<uint16_t> get_card_refs_within_range(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& loc,
|
||||
|
||||
+264
-148
@@ -1,5 +1,7 @@
|
||||
#include "RulerServer.hh"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "Server.hh"
|
||||
|
||||
using namespace std;
|
||||
@@ -204,6 +206,8 @@ const ActionChainWithConds* RulerServer::action_chain_with_conds_for_card_ref(
|
||||
uint16_t card_ref) const {
|
||||
uint8_t client_id = client_id_for_card_ref(card_ref);
|
||||
if (client_id != 0xFF) {
|
||||
// There appears to be a bug in Trial Edition: the bound on this loop is
|
||||
// 0x10, not 9.
|
||||
for (size_t z = 0; z < 9; z++) {
|
||||
const auto* chain = &this->set_card_action_chains[client_id]->at(z);
|
||||
if (card_ref == chain->chain.acting_card_ref) {
|
||||
@@ -235,19 +239,25 @@ bool RulerServer::card_has_pierce_or_rampage(
|
||||
uint16_t action_card_ref,
|
||||
uint8_t def_effect_index,
|
||||
AttackMedium attack_medium) const {
|
||||
auto short_statuses = (client_id != 0xFF) ? this->short_statuses[client_id] : nullptr;
|
||||
auto short_statuses = (client_id < 4) ? this->short_statuses[client_id] : nullptr;
|
||||
*out_has_rampage = false;
|
||||
|
||||
if (cond_type == ConditionType::NONE) {
|
||||
return false;
|
||||
bool ret;
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
if (is_nte) {
|
||||
ret = true;
|
||||
} else {
|
||||
if (cond_type == ConditionType::NONE) {
|
||||
return false;
|
||||
}
|
||||
ret = this->check_usability_or_apply_condition_for_card_refs(
|
||||
action_card_ref,
|
||||
attacker_card_ref,
|
||||
// Original code omitted this null check and presumably could crash here
|
||||
short_statuses ? short_statuses->at(0).card_ref.load() : 0xFFFF,
|
||||
def_effect_index,
|
||||
attack_medium);
|
||||
}
|
||||
bool ret = this->check_usability_or_apply_condition_for_card_refs(
|
||||
action_card_ref,
|
||||
attacker_card_ref,
|
||||
// Original code omitted this null check and presumably could crash here
|
||||
short_statuses ? short_statuses->at(0).card_ref.load() : 0xFFFF,
|
||||
def_effect_index,
|
||||
attack_medium);
|
||||
|
||||
switch (cond_type) {
|
||||
case ConditionType::RAMPAGE:
|
||||
@@ -280,7 +290,10 @@ bool RulerServer::card_has_pierce_or_rampage(
|
||||
if (short_statuses) {
|
||||
const auto& sc_status = short_statuses->at(0);
|
||||
auto ce = this->definition_for_card_ref(sc_status.card_ref);
|
||||
if (ce && (this->get_card_ref_max_hp(sc_status.card_ref) <= sc_status.current_hp * 2)) {
|
||||
// This appears to be an NTE bug: Major Pierce doesn't work on Arkz SCs.
|
||||
if (ce &&
|
||||
(!is_nte || (ce->def.type == CardType::HUNTERS_SC)) &&
|
||||
(this->get_card_ref_max_hp(sc_status.card_ref) <= sc_status.current_hp * 2)) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -290,8 +303,7 @@ bool RulerServer::card_has_pierce_or_rampage(
|
||||
}
|
||||
}
|
||||
|
||||
bool RulerServer::attack_action_has_rampage_and_not_pierce(
|
||||
const ActionState& pa, uint16_t card_ref) const {
|
||||
bool RulerServer::attack_action_has_rampage_and_not_pierce(const ActionState& pa, uint16_t card_ref) const {
|
||||
uint16_t orig_card_ref;
|
||||
uint16_t effective_range_card_id;
|
||||
TargetMode effective_target_mode;
|
||||
@@ -365,15 +377,15 @@ bool RulerServer::attack_action_has_rampage_and_not_pierce(
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RulerServer::attack_action_has_pierce_and_not_rampage(
|
||||
const ActionState& pa, uint8_t client_id) {
|
||||
if ((client_id_for_card_ref(pa.attacker_card_ref) == 0xFF) || (client_id == 0xFF)) {
|
||||
bool RulerServer::attack_action_has_pierce_and_not_rampage(const ActionState& pa, uint8_t client_id) const {
|
||||
if ((client_id_for_card_ref(pa.attacker_card_ref) == 0xFF) || (client_id >= 4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
auto attack_medium = this->get_attack_medium(pa);
|
||||
auto stat = this->short_statuses[client_id];
|
||||
if (!stat || !this->card_exists_by_status(stat->at(0)) || (stat->at(0).card_ref == 0xFFFF)) {
|
||||
if (!stat || (!is_nte && !this->card_exists_by_status(stat->at(0))) || (stat->at(0).card_ref == 0xFFFF)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -392,6 +404,33 @@ bool RulerServer::attack_action_has_pierce_and_not_rampage(
|
||||
last_action_card_index = z;
|
||||
}
|
||||
|
||||
auto check_chain = [&]() -> optional<bool> {
|
||||
const auto* chain = this->action_chain_with_conds_for_card_ref(pa.attacker_card_ref);
|
||||
if (chain) {
|
||||
for (ssize_t cond_index = 8; cond_index >= 0; cond_index--) {
|
||||
bool has_rampage = false;
|
||||
if (this->card_has_pierce_or_rampage(
|
||||
client_id, chain->conditions[cond_index].type, &has_rampage,
|
||||
pa.attacker_card_ref, chain->conditions[cond_index].card_ref,
|
||||
chain->conditions[cond_index].card_definition_effect_index,
|
||||
attack_medium)) {
|
||||
return true;
|
||||
}
|
||||
if (has_rampage) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullopt;
|
||||
};
|
||||
|
||||
if (is_nte) {
|
||||
auto res = check_chain();
|
||||
if (res.has_value()) {
|
||||
return res.value();
|
||||
}
|
||||
}
|
||||
|
||||
for (; last_action_card_index >= 0; last_action_card_index--) {
|
||||
auto ce = this->definition_for_card_ref(
|
||||
pa.action_card_refs[last_action_card_index]);
|
||||
@@ -418,20 +457,10 @@ bool RulerServer::attack_action_has_pierce_and_not_rampage(
|
||||
}
|
||||
}
|
||||
|
||||
const auto* chain = this->action_chain_with_conds_for_card_ref(pa.attacker_card_ref);
|
||||
if (chain) {
|
||||
for (ssize_t cond_index = 8; cond_index >= 0; cond_index--) {
|
||||
bool has_rampage = false;
|
||||
if (this->card_has_pierce_or_rampage(
|
||||
client_id, chain->conditions[cond_index].type, &has_rampage,
|
||||
pa.attacker_card_ref, chain->conditions[cond_index].card_ref,
|
||||
chain->conditions[cond_index].card_definition_effect_index,
|
||||
attack_medium)) {
|
||||
return true;
|
||||
}
|
||||
if (has_rampage) {
|
||||
return false;
|
||||
}
|
||||
if (!is_nte) {
|
||||
auto res = check_chain();
|
||||
if (res.has_value()) {
|
||||
return res.value();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,9 +663,11 @@ bool RulerServer::card_ref_has_free_maneuver(uint16_t card_ref) const {
|
||||
}
|
||||
|
||||
bool RulerServer::card_ref_is_aerial(uint16_t card_ref) const {
|
||||
const auto* stat = this->short_status_for_card_ref(card_ref);
|
||||
if (!stat || !this->card_exists_by_status(*stat)) {
|
||||
return false;
|
||||
if (!this->server()->options.is_nte()) {
|
||||
const auto* stat = this->short_status_for_card_ref(card_ref);
|
||||
if (!stat || !this->card_exists_by_status(*stat)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t client_id = client_id_for_card_ref(card_ref);
|
||||
@@ -791,10 +822,13 @@ bool RulerServer::check_pierce_and_rampage(
|
||||
uint16_t action_card_ref,
|
||||
uint8_t def_effect_index,
|
||||
AttackMedium attack_medium) const {
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
|
||||
// Note: NTE doesn't set this to zero; it apparently expects the caller to.
|
||||
*out_has_pierce = false;
|
||||
|
||||
const auto* card_short_status = this->short_status_for_card_ref(card_ref);
|
||||
if (cond_type == ConditionType::NONE) {
|
||||
if (!is_nte && (cond_type == ConditionType::NONE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -816,8 +850,9 @@ bool RulerServer::check_pierce_and_rampage(
|
||||
client_short_statuses = nullptr;
|
||||
}
|
||||
|
||||
bool apply_check_result = this->check_usability_or_apply_condition_for_card_refs(
|
||||
action_card_ref, attacker_card_ref, card_ref, def_effect_index, attack_medium);
|
||||
bool apply_check_result = (is_nte ||
|
||||
this->check_usability_or_apply_condition_for_card_refs(
|
||||
action_card_ref, attacker_card_ref, card_ref, def_effect_index, attack_medium));
|
||||
|
||||
switch (cond_type) {
|
||||
case ConditionType::PIERCE:
|
||||
@@ -903,7 +938,8 @@ bool RulerServer::check_usability_or_condition_apply(
|
||||
uint8_t def_effect_index,
|
||||
bool is_item_usability_check,
|
||||
AttackMedium attack_medium) const {
|
||||
auto log = this->server()->log_stack(string_printf("check_usability_or_condition_apply(%02hhX, #%04hX, %02hhX, #%04hX, #%04hX, %02hhX, %s, %s): ", client_id1, card_id1, client_id2, card_id2, card_id3, def_effect_index, is_item_usability_check ? "true" : "false", name_for_attack_medium(attack_medium)));
|
||||
auto s = this->server();
|
||||
auto log = s->log_stack(string_printf("check_usability_or_condition_apply(%02hhX, #%04hX, %02hhX, #%04hX, #%04hX, %02hhX, %s, %s): ", client_id1, card_id1, client_id2, card_id2, card_id3, def_effect_index, is_item_usability_check ? "true" : "false", name_for_attack_medium(attack_medium)));
|
||||
|
||||
if (static_cast<uint8_t>(attack_medium) & 0x80) {
|
||||
attack_medium = AttackMedium::UNKNOWN;
|
||||
@@ -916,7 +952,7 @@ bool RulerServer::check_usability_or_condition_apply(
|
||||
log.debug("ce1 missing");
|
||||
return false;
|
||||
}
|
||||
if ((ce1->def.type == CardType::ITEM) && this->card_id_is_boss_sc(card_id2)) {
|
||||
if (!s->options.is_nte() && (ce1->def.type == CardType::ITEM) && this->card_id_is_boss_sc(card_id2)) {
|
||||
log.debug("ce1 is item and card_id2 is boss sc");
|
||||
return false;
|
||||
}
|
||||
@@ -952,8 +988,8 @@ bool RulerServer::check_usability_or_condition_apply(
|
||||
// creature card is usable, the two client IDs should be the same or the
|
||||
// second should not be given, so we'd return true if the criterion passes. If
|
||||
// neither of these cases apply, we should return false as a failsafe even if
|
||||
// the criterion passes.
|
||||
bool ret = (!(def_effect_index & 0x80) || (client_id1 == client_id2)) || (client_id2 == 0xFF);
|
||||
// the criterion passes. NTE did not have such a check.
|
||||
bool ret = s->options.is_nte() || (!(def_effect_index & 0x80) || (client_id1 == client_id2)) || (client_id2 == 0xFF);
|
||||
switch (criterion_code) {
|
||||
case CriterionCode::NONE:
|
||||
return ret;
|
||||
@@ -1361,6 +1397,7 @@ uint16_t RulerServer::compute_attack_or_defense_costs(
|
||||
cost_bias++;
|
||||
}
|
||||
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
if (pa.action_card_refs[0] == 0xFFFF) {
|
||||
total_cost = cost_bias + 1;
|
||||
} else {
|
||||
@@ -1369,26 +1406,29 @@ uint16_t RulerServer::compute_attack_or_defense_costs(
|
||||
tech_cost_bias = -1;
|
||||
}
|
||||
|
||||
auto s = this->server();
|
||||
for (size_t z = 0; pa.action_card_refs[z] != 0xFFFF; z++) {
|
||||
auto ce = this->definition_for_card_ref(pa.action_card_refs[z]);
|
||||
if (has_mighty_knuckle || !ce || (ce->def.type != CardType::ACTION)) {
|
||||
return 99;
|
||||
}
|
||||
total_cost += (ce->def.self_cost + cost_bias);
|
||||
if (card_class_is_tech_like(ce->def.card_class())) {
|
||||
if (card_class_is_tech_like(ce->def.card_class(), s->options.is_nte())) {
|
||||
total_cost += tech_cost_bias;
|
||||
}
|
||||
total_ally_cost += ce->def.ally_cost;
|
||||
if (this->card_has_mighty_knuckle(pa.action_card_refs[z])) {
|
||||
has_mighty_knuckle = true;
|
||||
}
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(pa.client_id);
|
||||
for (size_t w = 0; w < num_assists; w++) {
|
||||
auto assist_effect = this->assist_server->get_active_assist_by_index(w);
|
||||
if (assist_effect == AssistEffect::INFLATION) {
|
||||
assist_cost_bias++;
|
||||
} else if (assist_effect == AssistEffect::DEFLATION) {
|
||||
assist_cost_bias--;
|
||||
if (!is_nte) {
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(pa.client_id);
|
||||
for (size_t w = 0; w < num_assists; w++) {
|
||||
auto assist_effect = this->assist_server->get_active_assist_by_index(w);
|
||||
if (assist_effect == AssistEffect::INFLATION) {
|
||||
assist_cost_bias++;
|
||||
} else if (assist_effect == AssistEffect::DEFLATION) {
|
||||
assist_cost_bias--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1397,7 +1437,11 @@ uint16_t RulerServer::compute_attack_or_defense_costs(
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(pa.client_id);
|
||||
for (size_t w = 0; w < num_assists; w++) {
|
||||
auto assist_effect = this->assist_server->get_active_assist_by_index(w);
|
||||
if ((assist_effect == AssistEffect::BATTLE_ROYALE) &&
|
||||
if (is_nte && (assist_effect == AssistEffect::INFLATION)) {
|
||||
assist_cost_bias++;
|
||||
} else if (is_nte && (assist_effect == AssistEffect::DEFLATION)) {
|
||||
assist_cost_bias--;
|
||||
} else if ((assist_effect == AssistEffect::BATTLE_ROYALE) &&
|
||||
(pa.action_card_refs[0] == 0xFFFF)) {
|
||||
total_cost = 0;
|
||||
final_cost = 0;
|
||||
@@ -1406,7 +1450,9 @@ uint16_t RulerServer::compute_attack_or_defense_costs(
|
||||
|
||||
if (has_mighty_knuckle) {
|
||||
if (!allow_mighty_knuckle) {
|
||||
final_cost = 0;
|
||||
if (!is_nte) {
|
||||
final_cost = 0;
|
||||
}
|
||||
} else {
|
||||
final_cost = max<int16_t>(final_cost, this->hand_and_equip_states[pa.client_id]->atk_points);
|
||||
}
|
||||
@@ -1449,9 +1495,11 @@ bool RulerServer::compute_effective_range_and_target_mode_for_attack(
|
||||
auto target_mode = ce->def.target_mode;
|
||||
if (this->card_ref_or_sc_has_fixed_range(pa.attacker_card_ref)) {
|
||||
card_id = this->card_id_for_card_ref(pa.attacker_card_ref);
|
||||
auto sc_ce = this->definition_for_card_id(card_id);
|
||||
if (sc_ce && (static_cast<uint8_t>(target_mode) < 6)) {
|
||||
target_mode = sc_ce->def.target_mode;
|
||||
if (!this->server()->options.is_nte()) {
|
||||
auto sc_ce = this->definition_for_card_id(card_id);
|
||||
if (sc_ce && (static_cast<uint8_t>(target_mode) < 6)) {
|
||||
target_mode = sc_ce->def.target_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1474,8 +1522,7 @@ bool RulerServer::compute_effective_range_and_target_mode_for_attack(
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t RulerServer::count_rampage_targets_for_attack(
|
||||
const ActionState& pa, uint8_t client_id) const {
|
||||
size_t RulerServer::count_rampage_targets_for_attack(const ActionState& pa, uint8_t client_id) const {
|
||||
if (client_id == 0xFF) {
|
||||
return 0;
|
||||
}
|
||||
@@ -1632,7 +1679,8 @@ int32_t RulerServer::error_code_for_client_setting_card(
|
||||
return -0x76;
|
||||
}
|
||||
|
||||
if (!this->is_card_ref_in_hand(card_ref)) {
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
if (!is_nte && !this->is_card_ref_in_hand(card_ref)) {
|
||||
return -0x5E;
|
||||
}
|
||||
|
||||
@@ -1672,8 +1720,8 @@ int32_t RulerServer::error_code_for_client_setting_card(
|
||||
}
|
||||
|
||||
// Check for assists that can only be set on yourself
|
||||
auto eff = assist_effect_number_for_card_id(ce->def.card_id);
|
||||
if (((eff == AssistEffect::LEGACY) || (eff == AssistEffect::EXCHANGE)) &&
|
||||
auto eff = assist_effect_number_for_card_id(ce->def.card_id, is_nte);
|
||||
if (((eff == AssistEffect::LEGACY) || (!is_nte && (eff == AssistEffect::EXCHANGE))) &&
|
||||
(assist_target_client_id != 0xFF) &&
|
||||
(assist_target_client_id != client_id_for_card_ref(card_ref))) {
|
||||
return -0x75;
|
||||
@@ -1712,8 +1760,8 @@ int32_t RulerServer::error_code_for_client_setting_card(
|
||||
|
||||
if ((ce->def.type == CardType::ITEM) || (ce->def.type == CardType::CREATURE)) {
|
||||
int16_t existing_fcs_cost = 0;
|
||||
bool limit_summoning_by_count = this->find_condition_on_card_ref(
|
||||
short_statuses->at(0).card_ref, ConditionType::FC_LIMIT_BY_COUNT);
|
||||
bool limit_summoning_by_count = !is_nte &&
|
||||
this->find_condition_on_card_ref(short_statuses->at(0).card_ref, ConditionType::FC_LIMIT_BY_COUNT);
|
||||
for (size_t z = 7; z < 15; z++) {
|
||||
const auto& this_status = short_statuses->at(z);
|
||||
if ((this_status.card_ref != 0xFFFF) && this->card_exists_by_status(this_status)) {
|
||||
@@ -1752,71 +1800,91 @@ int32_t RulerServer::error_code_for_client_setting_card(
|
||||
return 0;
|
||||
}
|
||||
|
||||
Location summon_area_loc;
|
||||
uint8_t summon_area_size;
|
||||
if (!this->get_creature_summon_area(
|
||||
client_id, &summon_area_loc, &summon_area_size)) {
|
||||
if (team_id != 1) {
|
||||
if ((loc->x > 0) && (loc->x < this->map_and_rules->map.width - 1)) {
|
||||
if ((loc->y < this->map_and_rules->map.height - summon_cost - 1) &&
|
||||
(loc->y > 0)) {
|
||||
return 0;
|
||||
if (is_nte) {
|
||||
// It seems NTE assumes that teams always start on the same ends of the
|
||||
// map; non-NTE removes this restriction.
|
||||
if (team_id == 1) {
|
||||
if (((loc->x < 1) ||
|
||||
(loc->x >= this->map_and_rules->map.width - 1) ||
|
||||
(loc->y < summon_cost + 1) ||
|
||||
(loc->y >= this->map_and_rules->map.height - 1)) &&
|
||||
(loc->y != this->map_and_rules->map.height - 2)) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else if (((loc->x < 1) ||
|
||||
(loc->x >= this->map_and_rules->map.width - 1) ||
|
||||
(loc->y < 1) ||
|
||||
(loc->y >= this->map_and_rules->map.height - summon_cost - 1)) &&
|
||||
(loc->y != 1)) {
|
||||
return -0x7E;
|
||||
}
|
||||
|
||||
} else {
|
||||
Location summon_area_loc;
|
||||
uint8_t summon_area_size;
|
||||
if (!this->get_creature_summon_area(client_id, &summon_area_loc, &summon_area_size)) {
|
||||
if (team_id != 1) {
|
||||
if ((loc->x > 0) && (loc->x < this->map_and_rules->map.width - 1)) {
|
||||
if ((loc->y < this->map_and_rules->map.height - summon_cost - 1) &&
|
||||
(loc->y > 0)) {
|
||||
return 0;
|
||||
}
|
||||
if (loc->y == 1) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (loc->y == 1) {
|
||||
return 0;
|
||||
} else {
|
||||
if ((loc->x > 0) &&
|
||||
(loc->x < this->map_and_rules->map.width - 1)) {
|
||||
if ((summon_cost + 1 <= loc->y) && (loc->y < this->map_and_rules->map.height - 1)) {
|
||||
return 0;
|
||||
}
|
||||
if (loc->y == this->map_and_rules->map.height - 2) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -0x7E;
|
||||
}
|
||||
|
||||
int32_t x_offset, y_offset;
|
||||
this->offsets_for_direction(summon_area_loc, &x_offset, &y_offset);
|
||||
if (x_offset == 0) {
|
||||
if ((loc->x < 1) && (loc->x >= this->map_and_rules->map.width - 1)) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else {
|
||||
if ((loc->x > 0) &&
|
||||
(loc->x < this->map_and_rules->map.width - 1)) {
|
||||
if ((summon_cost + 1 <= loc->y) && (loc->y < this->map_and_rules->map.height - 1)) {
|
||||
return 0;
|
||||
int16_t diff = max<int16_t>(summon_area_size - summon_cost, 0);
|
||||
if (x_offset > 0) {
|
||||
if (loc->x < summon_area_loc.x) {
|
||||
return -0x7E;
|
||||
}
|
||||
if (loc->y == this->map_and_rules->map.height - 2) {
|
||||
return 0;
|
||||
if (loc->x > summon_area_loc.x + diff) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else if (x_offset < 0) {
|
||||
if ((loc->x > summon_area_loc.x) || (loc->x < summon_area_loc.x - diff)) {
|
||||
return -0x7E;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -0x7E;
|
||||
}
|
||||
|
||||
int32_t x_offset, y_offset;
|
||||
this->offsets_for_direction(summon_area_loc, &x_offset, &y_offset);
|
||||
if (x_offset == 0) {
|
||||
if ((loc->x < 1) && (loc->x >= this->map_and_rules->map.width - 1)) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else {
|
||||
int16_t diff = max<int16_t>(summon_area_size - summon_cost, 0);
|
||||
if (x_offset > 0) {
|
||||
if (loc->x < summon_area_loc.x) {
|
||||
if (y_offset == 0) {
|
||||
if ((loc->y < 1) && (loc->y >= this->map_and_rules->map.height - 1)) {
|
||||
return -0x7E;
|
||||
}
|
||||
if (loc->x > summon_area_loc.x + diff) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else if (x_offset < 0) {
|
||||
if ((loc->x > summon_area_loc.x) || (loc->x < summon_area_loc.x - diff)) {
|
||||
return -0x7E;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (y_offset == 0) {
|
||||
if ((loc->y < 1) && (loc->y >= this->map_and_rules->map.height - 1)) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else {
|
||||
int16_t diff = max<int16_t>(summon_area_size - summon_cost, 0);
|
||||
if (y_offset > 0) {
|
||||
if (loc->y < summon_area_loc.y) {
|
||||
return -0x7E;
|
||||
}
|
||||
if (loc->y > summon_area_loc.y + diff) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else if (y_offset < 0) {
|
||||
if ((loc->y > summon_area_loc.y) || (loc->y < summon_area_loc.y - diff)) {
|
||||
return -0x7E;
|
||||
} else {
|
||||
int16_t diff = max<int16_t>(summon_area_size - summon_cost, 0);
|
||||
if (y_offset > 0) {
|
||||
if (loc->y < summon_area_loc.y) {
|
||||
return -0x7E;
|
||||
}
|
||||
if (loc->y > summon_area_loc.y + diff) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else if (y_offset < 0) {
|
||||
if ((loc->y > summon_area_loc.y) || (loc->y < summon_area_loc.y - diff)) {
|
||||
return -0x7E;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2020,7 +2088,7 @@ uint8_t RulerServer::get_card_ref_max_hp(uint16_t card_ref) const {
|
||||
return 0;
|
||||
} else if (((ce->def.type == CardType::HUNTERS_SC) || (ce->def.type == CardType::ARKZ_SC)) &&
|
||||
(this->map_and_rules->rules.char_hp > 0) &&
|
||||
!this->card_ref_is_boss_sc(card_ref)) {
|
||||
(this->server()->options.is_nte() || !this->card_ref_is_boss_sc(card_ref))) {
|
||||
return this->map_and_rules->rules.char_hp;
|
||||
} else {
|
||||
return ce->def.hp.stat;
|
||||
@@ -2169,7 +2237,7 @@ bool RulerServer::is_attack_valid(const ActionState& pa) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attacker_card_status->card_flags & 2) {
|
||||
if (!this->server()->options.is_nte() && (attacker_card_status->card_flags & 2)) {
|
||||
this->error_code3 = -0x60;
|
||||
return false;
|
||||
}
|
||||
@@ -2285,7 +2353,9 @@ bool RulerServer::is_attack_or_defense_valid(const ActionState& pa) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int16_t cost = this->compute_attack_or_defense_costs(pa, false, nullptr);
|
||||
// NTE apparently does not check the action's cost here
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
int16_t cost = is_nte ? 0 : this->compute_attack_or_defense_costs(pa, false, nullptr);
|
||||
|
||||
switch (this->get_pending_action_type(pa)) {
|
||||
case ActionType::ATTACK:
|
||||
@@ -2381,8 +2451,9 @@ bool RulerServer::is_defense_valid(const ActionState& pa) {
|
||||
}
|
||||
}
|
||||
|
||||
if (this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::HOLD) ||
|
||||
this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::CANNOT_DEFEND)) {
|
||||
if (!this->server()->options.is_nte() &&
|
||||
(this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::HOLD) ||
|
||||
this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::CANNOT_DEFEND))) {
|
||||
this->error_code3 = -0x63;
|
||||
return false;
|
||||
}
|
||||
@@ -2411,30 +2482,53 @@ size_t RulerServer::max_move_distance_for_card_ref(uint32_t card_ref) const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t ret = ce->def.mv.stat;
|
||||
|
||||
Condition cond;
|
||||
if (this->find_condition_on_card_ref(card_ref, ConditionType::MV_BONUS, &cond, nullptr, true)) {
|
||||
ret += cond.value;
|
||||
}
|
||||
if (this->find_condition_on_card_ref(card_ref, ConditionType::SET_MV, &cond, nullptr, true)) {
|
||||
ret = cond.value;
|
||||
}
|
||||
ret = max<ssize_t>(0, ret);
|
||||
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id);
|
||||
bool has_stamina_effect = false;
|
||||
for (size_t z = 0; z < num_assists; z++) {
|
||||
auto eff = this->assist_server->get_active_assist_by_index(z);
|
||||
if (eff == AssistEffect::SNAIL_PACE) {
|
||||
return 1;
|
||||
if (this->server()->options.is_nte()) {
|
||||
if (ce->def.type == CardType::ITEM) {
|
||||
return ce->def.mv.stat;
|
||||
}
|
||||
if (eff == AssistEffect::STAMINA) {
|
||||
has_stamina_effect = true;
|
||||
}
|
||||
}
|
||||
|
||||
return (has_stamina_effect) ? 9 : min<ssize_t>(9, ret);
|
||||
Condition cond;
|
||||
if (this->find_condition_on_card_ref(card_ref, ConditionType::SET_MV, &cond)) {
|
||||
return cond.value;
|
||||
}
|
||||
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id);
|
||||
bool has_stamina_effect = false;
|
||||
for (size_t z = 0; z < num_assists; z = z + 1) {
|
||||
auto assist = this->assist_server->get_active_assist_by_index(z);
|
||||
if (assist == AssistEffect::SNAIL_PACE) {
|
||||
return 1;
|
||||
} else if (assist == AssistEffect::STAMINA) {
|
||||
has_stamina_effect = true;
|
||||
}
|
||||
}
|
||||
return has_stamina_effect ? 99 : ce->def.mv.stat;
|
||||
|
||||
} else {
|
||||
ssize_t ret = ce->def.mv.stat;
|
||||
Condition cond;
|
||||
if (this->find_condition_on_card_ref(card_ref, ConditionType::MV_BONUS, &cond, nullptr, true)) {
|
||||
ret += cond.value;
|
||||
}
|
||||
if (this->find_condition_on_card_ref(card_ref, ConditionType::SET_MV, &cond, nullptr, true)) {
|
||||
ret = cond.value;
|
||||
}
|
||||
ret = max<ssize_t>(0, ret);
|
||||
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id);
|
||||
bool has_stamina_effect = false;
|
||||
for (size_t z = 0; z < num_assists; z++) {
|
||||
auto eff = this->assist_server->get_active_assist_by_index(z);
|
||||
if (eff == AssistEffect::SNAIL_PACE) {
|
||||
return 1;
|
||||
}
|
||||
if (eff == AssistEffect::STAMINA) {
|
||||
has_stamina_effect = true;
|
||||
}
|
||||
}
|
||||
|
||||
return has_stamina_effect ? 9 : min<ssize_t>(9, ret);
|
||||
}
|
||||
}
|
||||
|
||||
RulerServer::MovePath::MovePath()
|
||||
@@ -2518,13 +2612,14 @@ void RulerServer::replace_D1_D2_rank_cards_with_Attack(
|
||||
}
|
||||
|
||||
AttackMedium RulerServer::get_attack_medium(const ActionState& pa) const {
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
for (size_t z = 0; z < 8; z++) {
|
||||
uint16_t card_ref = pa.action_card_refs[z];
|
||||
if (card_ref == 0xFFFF) {
|
||||
return AttackMedium::PHYSICAL;
|
||||
}
|
||||
auto ce = this->definition_for_card_ref(card_ref);
|
||||
if (ce && card_class_is_tech_like(ce->def.card_class())) {
|
||||
if (ce && card_class_is_tech_like(ce->def.card_class(), is_nte)) {
|
||||
return AttackMedium::TECH;
|
||||
}
|
||||
}
|
||||
@@ -2545,9 +2640,11 @@ int32_t RulerServer::set_cost_for_card(uint8_t client_id, uint16_t card_ref) con
|
||||
return -0x7D;
|
||||
}
|
||||
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
auto short_statuses = this->short_statuses[client_id];
|
||||
int32_t ret = ce->def.self_cost;
|
||||
if (short_statuses &&
|
||||
if (!is_nte &&
|
||||
short_statuses &&
|
||||
this->card_exists_by_status(short_statuses->at(0)) &&
|
||||
this->find_condition_on_card_ref(short_statuses->at(0).card_ref, ConditionType::UNKNOWN_69)) {
|
||||
ret = 0;
|
||||
@@ -2580,9 +2677,8 @@ int32_t RulerServer::set_cost_for_card(uint8_t client_id, uint16_t card_ref) con
|
||||
for (size_t z = 0; z < num_assists; z++) {
|
||||
auto eff = this->assist_server->get_active_assist_by_index(z);
|
||||
if (eff == AssistEffect::LAND_PRICE) {
|
||||
// Note: Original code had an extra addend (ret < 0 && (ret & 1) != 0),
|
||||
// but ret cannot be negatve here, so we omit it.
|
||||
ret += ret >> 1;
|
||||
// In NTE, Land Price is apparently 2x rather than 1.5x
|
||||
ret = is_nte ? (ret << 1) : (ret + (ret >> 1));
|
||||
} else if (eff == AssistEffect::DEFLATION) {
|
||||
ret = max<int32_t>(0, ret - 1);
|
||||
} else if (eff == AssistEffect::INFLATION) {
|
||||
@@ -2667,4 +2763,24 @@ int32_t RulerServer::verify_deck(
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t RulerServer::count_targets_with_rampage_and_not_pierce_nte(const ActionState& as) const {
|
||||
size_t ret = 0;
|
||||
for (size_t z = 0; (z < as.target_card_refs.size()) && (as.target_card_refs[z] != 0xFFFF); z++) {
|
||||
if (this->attack_action_has_rampage_and_not_pierce(as, as.target_card_refs[z])) {
|
||||
ret++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t RulerServer::count_targets_with_pierce_and_not_rampage_nte(const ActionState& as) const {
|
||||
size_t ret = 0;
|
||||
for (size_t z = 0; (z < as.target_card_refs.size()) && (as.target_card_refs[z] != 0xFFFF); z++) {
|
||||
if (this->attack_action_has_pierce_and_not_rampage(as, client_id_for_card_ref(as.target_card_refs[z]))) {
|
||||
ret++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
+17
-30
@@ -49,12 +49,9 @@ public:
|
||||
std::shared_ptr<Server> server();
|
||||
std::shared_ptr<const Server> server() const;
|
||||
|
||||
ActionChainWithConds* action_chain_with_conds_for_card_ref(
|
||||
uint16_t card_ref);
|
||||
const ActionChainWithConds* action_chain_with_conds_for_card_ref(
|
||||
uint16_t card_ref) const;
|
||||
bool any_attack_action_card_is_support_tech_or_support_pb(
|
||||
const ActionState& pa) const;
|
||||
ActionChainWithConds* action_chain_with_conds_for_card_ref(uint16_t card_ref);
|
||||
const ActionChainWithConds* action_chain_with_conds_for_card_ref(uint16_t card_ref) const;
|
||||
bool any_attack_action_card_is_support_tech_or_support_pb(const ActionState& pa) const;
|
||||
bool card_has_pierce_or_rampage(
|
||||
uint8_t client_id,
|
||||
ConditionType cond_type,
|
||||
@@ -63,27 +60,23 @@ public:
|
||||
uint16_t action_card_ref,
|
||||
uint8_t def_effect_index,
|
||||
AttackMedium attack_medium) const;
|
||||
bool attack_action_has_rampage_and_not_pierce(
|
||||
const ActionState& pa, uint16_t card_ref) const;
|
||||
bool attack_action_has_pierce_and_not_rampage(
|
||||
const ActionState& pa, uint8_t client_id);
|
||||
bool attack_action_has_rampage_and_not_pierce(const ActionState& pa, uint16_t card_ref) const;
|
||||
bool attack_action_has_pierce_and_not_rampage(const ActionState& pa, uint8_t client_id) const;
|
||||
size_t count_targets_with_rampage_and_not_pierce_nte(const ActionState& as) const;
|
||||
size_t count_targets_with_pierce_and_not_rampage_nte(const ActionState& as) const;
|
||||
bool card_exists_by_status(const CardShortStatus& stat) const;
|
||||
bool card_has_mighty_knuckle(uint32_t card_ref) const;
|
||||
uint16_t card_id_for_card_ref(uint16_t card_ref) const;
|
||||
static bool card_id_is_boss_sc(uint16_t card_id);
|
||||
static bool card_id_is_support_tech_or_support_pb(uint16_t card_id);
|
||||
bool card_ref_can_attack(uint16_t card_ref);
|
||||
bool card_ref_can_move(
|
||||
uint8_t client_id, uint16_t card_ref, bool ignore_atk_points) const;
|
||||
bool card_ref_has_class_usability_condition(
|
||||
uint16_t card_ref) const;
|
||||
bool card_ref_can_move(uint8_t client_id, uint16_t card_ref, bool ignore_atk_points) const;
|
||||
bool card_ref_has_class_usability_condition(uint16_t card_ref) const;
|
||||
bool card_ref_has_free_maneuver(uint16_t card_ref) const;
|
||||
bool card_ref_is_aerial(uint16_t card_ref) const;
|
||||
bool card_ref_is_aerial_or_has_free_maneuver(
|
||||
uint16_t card_ref) const;
|
||||
bool card_ref_is_aerial_or_has_free_maneuver(uint16_t card_ref) const;
|
||||
bool card_ref_is_boss_sc(uint32_t card_ref) const;
|
||||
bool card_ref_or_any_set_card_has_condition_46(
|
||||
uint16_t card_ref) const;
|
||||
bool card_ref_or_any_set_card_has_condition_46(uint16_t card_ref) const;
|
||||
bool card_ref_or_sc_has_fixed_range(uint16_t card_ref) const;
|
||||
bool check_move_path_and_get_cost(
|
||||
uint8_t client_id,
|
||||
@@ -123,14 +116,12 @@ public:
|
||||
uint16_t* out_effective_card_id,
|
||||
TargetMode* out_effective_target_mode,
|
||||
uint16_t* out_orig_card_ref) const;
|
||||
size_t count_rampage_targets_for_attack(
|
||||
const ActionState& pa, uint8_t client_id) const;
|
||||
size_t count_rampage_targets_for_attack(const ActionState& pa, uint8_t client_id) const;
|
||||
bool defense_card_can_apply_to_attack(
|
||||
uint16_t defense_card_ref,
|
||||
uint16_t attacker_card_ref,
|
||||
uint16_t attacker_sc_card_ref) const;
|
||||
bool defense_card_matches_any_attack_card_top_color(
|
||||
const ActionState& pa) const;
|
||||
bool defense_card_matches_any_attack_card_top_color(const ActionState& pa) const;
|
||||
std::shared_ptr<const CardIndex::CardEntry> definition_for_card_ref(uint16_t card_ref) const;
|
||||
int32_t error_code_for_client_setting_card(
|
||||
uint8_t client_id,
|
||||
@@ -157,17 +148,14 @@ public:
|
||||
size_t num_occupied_tiles,
|
||||
size_t num_vacant_tiles) const;
|
||||
uint16_t get_ally_sc_card_ref(uint16_t card_ref) const;
|
||||
std::shared_ptr<const CardIndex::CardEntry> definition_for_card_id(
|
||||
uint32_t card_id) const;
|
||||
std::shared_ptr<const CardIndex::CardEntry> definition_for_card_id(uint32_t card_id) const;
|
||||
uint32_t get_card_id_with_effective_range(
|
||||
uint16_t card_ref, uint16_t card_id_override, TargetMode* out_target_mode) const;
|
||||
uint8_t get_card_ref_max_hp(uint16_t card_ref) const;
|
||||
bool get_creature_summon_area(
|
||||
uint8_t client_id, Location* out_loc, uint8_t* out_region_size) const;
|
||||
std::shared_ptr<HandAndEquipState> get_hand_and_equip_state_for_client_id(
|
||||
uint8_t client_id);
|
||||
std::shared_ptr<const HandAndEquipState> get_hand_and_equip_state_for_client_id(
|
||||
uint8_t client_id) const;
|
||||
std::shared_ptr<HandAndEquipState> get_hand_and_equip_state_for_client_id(uint8_t client_id);
|
||||
std::shared_ptr<const HandAndEquipState> get_hand_and_equip_state_for_client_id(uint8_t client_id) const;
|
||||
bool get_move_path_length_and_cost(
|
||||
uint32_t client_id,
|
||||
uint32_t card_ref,
|
||||
@@ -188,8 +176,7 @@ public:
|
||||
std::shared_ptr<StateFlags> state_flags,
|
||||
std::shared_ptr<AssistServer> assist_server);
|
||||
size_t max_move_distance_for_card_ref(uint32_t card_ref) const;
|
||||
static void offsets_for_direction(
|
||||
const Location& loc, int32_t* out_x_offset, int32_t* out_y_offset);
|
||||
static void offsets_for_direction(const Location& loc, int32_t* out_x_offset, int32_t* out_y_offset);
|
||||
void register_player(
|
||||
uint8_t client_id,
|
||||
std::shared_ptr<HandAndEquipState> hes,
|
||||
|
||||
+770
-504
File diff suppressed because it is too large
Load Diff
+27
-20
@@ -74,6 +74,10 @@ public:
|
||||
std::shared_ptr<PSOLFGEncryption> random_crypt;
|
||||
std::shared_ptr<const Tournament> tournament;
|
||||
std::array<std::vector<uint16_t>, 5> trap_card_ids;
|
||||
|
||||
inline bool is_nte() const {
|
||||
return (this->behavior_flags & BehaviorFlag::IS_TRIAL_EDITION);
|
||||
}
|
||||
};
|
||||
Server(std::shared_ptr<Lobby> lobby, Options&& options);
|
||||
~Server() noexcept(false);
|
||||
@@ -98,23 +102,24 @@ public:
|
||||
int8_t get_winner_team_id() const;
|
||||
|
||||
template <typename T>
|
||||
void send(const T& cmd) const {
|
||||
void send(const T& cmd, uint8_t command = 0xC9, bool enable_masking = true) const {
|
||||
if (cmd.header.size != sizeof(cmd) / 4) {
|
||||
throw std::logic_error("outbound command size field is incorrect");
|
||||
}
|
||||
if (cmd.header.subsubcommand == 0x06) {
|
||||
if (!this->options.is_nte() && (cmd.header.subsubcommand == 0x06)) {
|
||||
this->num_6xB4x06_commands_sent++;
|
||||
this->prev_num_6xB4x06_commands_sent = this->num_6xB4x06_commands_sent;
|
||||
if (this->num_6xB4x06_commands_sent > 0x100) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->send(&cmd, cmd.header.size * 4);
|
||||
this->send(&cmd, cmd.header.size * 4, command, enable_masking);
|
||||
}
|
||||
void send(const void* data, size_t size) const;
|
||||
void send(const void* data, size_t size, uint8_t command = 0xC9, bool enable_masking = true) const;
|
||||
void send_commands_for_joining_spectator(Channel& ch) const;
|
||||
|
||||
void force_battle_result(uint8_t surrendered_client_id, bool set_winner);
|
||||
void force_replace_assist_card(uint8_t client_id, uint16_t card_id);
|
||||
void force_destroy_field_character(uint8_t client_id, size_t set_index);
|
||||
|
||||
__attribute__((format(printf, 2, 3))) void send_debug_message_printf(const char* fmt, ...) const;
|
||||
@@ -142,7 +147,7 @@ public:
|
||||
bool check_presence_entry(uint8_t client_id) const;
|
||||
void clear_player_flags_after_dice_phase();
|
||||
void compute_all_map_occupied_bits();
|
||||
void compute_team_dice_boost(uint8_t team_id);
|
||||
void compute_team_dice_bonus(uint8_t team_id);
|
||||
void copy_player_states_to_prev_states();
|
||||
std::shared_ptr<const CardIndex::CardEntry> definition_for_card_id(uint16_t card_id) const;
|
||||
void destroy_cards_with_zero_hp();
|
||||
@@ -173,14 +178,13 @@ public:
|
||||
void send_set_card_updates_and_6xB4x04_if_needed();
|
||||
void set_battle_ended();
|
||||
void set_battle_started();
|
||||
void set_client_id_ready_to_advance_phase(uint8_t client_id);
|
||||
void set_client_id_ready_to_advance_phase(uint8_t client_id, BattlePhase battle_phase);
|
||||
void set_phase_after();
|
||||
void move_phase_before();
|
||||
void set_player_deck_valid(uint8_t client_id);
|
||||
void setup_and_start_battle();
|
||||
G_SetStateFlags_GC_Ep3_6xB4x03 prepare_6xB4x03() const;
|
||||
void update_battle_state_flags_and_send_6xB4x03_if_needed(
|
||||
bool always_send = false);
|
||||
G_SetStateFlags_Ep3_6xB4x03 prepare_6xB4x03() const;
|
||||
void update_battle_state_flags_and_send_6xB4x03_if_needed(bool always_send = false);
|
||||
bool update_registration_phase();
|
||||
void on_server_data_input(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx0B_mulligan_hand(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
@@ -191,6 +195,8 @@ public:
|
||||
void handle_CAx10_move_fc_to_location(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx11_enqueue_attack_or_defense(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx12_end_attack_list(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
template <typename CmdT>
|
||||
void handle_CAx13_update_map_during_setup_t(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx13_update_map_during_setup(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx14_update_deck_during_setup(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx15_unused_hard_reset_server_state(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
@@ -208,12 +214,11 @@ public:
|
||||
void handle_CAx49_card_counts(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void compute_losing_team_id_and_add_winner_flags(uint32_t flags);
|
||||
uint32_t get_team_exp(uint8_t team_id) const;
|
||||
uint32_t send_6xB4x06_if_card_ref_invalid(
|
||||
uint16_t card_ref, int16_t negative_value);
|
||||
uint32_t send_6xB4x06_if_card_ref_invalid(uint16_t card_ref, int16_t negative_value);
|
||||
void unknown_8023EEF4();
|
||||
void execute_bomb_assist_effect();
|
||||
void replace_targets_due_to_destruction_or_conditions(
|
||||
ActionState* as);
|
||||
void replace_targets_due_to_destruction_nte(ActionState* as);
|
||||
void replace_targets_due_to_destruction_or_conditions(ActionState* as);
|
||||
bool any_target_exists_for_attack(const ActionState& as);
|
||||
uint8_t get_current_team_turn2() const;
|
||||
void unknown_8023EE48();
|
||||
@@ -224,12 +229,11 @@ public:
|
||||
void send_6xB4x02_for_all_players_if_needed(bool always_send = false);
|
||||
void send_6xB4x50_trap_tile_locations() const;
|
||||
|
||||
G_UpdateDecks_GC_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
|
||||
G_SetPlayerNames_GC_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
|
||||
static std::string prepare_6xB6x41_map_definition(
|
||||
std::shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_trial);
|
||||
G_UpdateDecks_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
|
||||
G_SetPlayerNames_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
|
||||
static std::string prepare_6xB6x41_map_definition(std::shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_nte);
|
||||
void send_6xB6x41_to_all_clients() const;
|
||||
G_SetTrapTileLocations_GC_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
|
||||
G_SetTrapTileLocations_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
|
||||
|
||||
std::vector<std::shared_ptr<Card>> const_cast_set_cards_v(
|
||||
const std::vector<std::shared_ptr<const Card>>& cards);
|
||||
@@ -241,6 +245,7 @@ private:
|
||||
public:
|
||||
// These fields are not part of the original implementation
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
bool has_lobby;
|
||||
Options options;
|
||||
std::shared_ptr<const MapIndex::Map> last_chosen_map;
|
||||
bool tournament_match_result_sent;
|
||||
@@ -288,7 +293,7 @@ public:
|
||||
uint32_t should_copy_prev_states_to_current_states;
|
||||
std::shared_ptr<CardSpecial> card_special;
|
||||
std::shared_ptr<StateFlags> state_flags;
|
||||
std::shared_ptr<PlayerState> player_states[4];
|
||||
std::array<std::shared_ptr<PlayerState>, 4> player_states;
|
||||
parray<uint32_t, 4> clients_done_in_mulligan_phase;
|
||||
uint32_t num_pending_attacks_with_cards;
|
||||
std::shared_ptr<Card> attack_cards[0x20];
|
||||
@@ -301,13 +306,15 @@ public:
|
||||
std::shared_ptr<RulerServer> ruler_server;
|
||||
parray<parray<parray<uint8_t, 2>, 2>, 5> warp_positions; // Array indexes are (type, end, x/y)
|
||||
parray<int16_t, 2> team_exp;
|
||||
parray<int16_t, 2> team_dice_boost;
|
||||
parray<int16_t, 2> team_dice_bonus;
|
||||
parray<uint32_t, 2> team_client_count;
|
||||
parray<uint32_t, 2> team_num_ally_fcs_destroyed;
|
||||
parray<uint32_t, 2> team_num_cards_destroyed;
|
||||
parray<uint8_t, 5> num_trap_tiles_of_type;
|
||||
parray<uint8_t, 5> chosen_trap_tile_index_of_type;
|
||||
parray<parray<parray<uint8_t, 2>, 8>, 5> trap_tile_locs;
|
||||
parray<parray<uint8_t, 2>, 0x10> trap_tile_locs_nte;
|
||||
size_t num_trap_tiles_nte;
|
||||
ActionState pb_action_states[4];
|
||||
parray<uint8_t, 4> has_done_pb;
|
||||
parray<parray<uint8_t, 4>, 4> has_done_pb_with_client;
|
||||
|
||||
@@ -842,7 +842,7 @@ void TournamentIndex::save() const {
|
||||
for (const auto& it : this->name_to_tournament) {
|
||||
json.emplace(it.second->get_name(), it.second->json());
|
||||
}
|
||||
save_file(this->state_filename, json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS));
|
||||
save_file(this->state_filename, json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS | JSON::SerializeOption::ESCAPE_CONTROLS_ONLY));
|
||||
}
|
||||
|
||||
shared_ptr<Tournament> TournamentIndex::create_tournament(
|
||||
|
||||
@@ -74,3 +74,18 @@ FileContentsCache::GetResult FileContentsCache::get(const char* name,
|
||||
std::function<std::string(const std::string&)> generate) {
|
||||
return this->get(string(name), generate);
|
||||
}
|
||||
|
||||
shared_ptr<const string> ThreadSafeFileCache::get(
|
||||
const string& name, std::function<shared_ptr<const string>(const std::string&)> generate) {
|
||||
try {
|
||||
shared_lock g(this->lock);
|
||||
return this->name_to_file.at(name);
|
||||
} catch (const out_of_range&) {
|
||||
unique_lock g(this->lock);
|
||||
auto it = this->name_to_file.find(name);
|
||||
if (it == this->name_to_file.end()) {
|
||||
it = this->name_to_file.emplace(name, generate(name)).first;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -102,3 +104,21 @@ private:
|
||||
std::unordered_map<std::string, std::shared_ptr<File>> name_to_file;
|
||||
uint64_t ttl_usecs;
|
||||
};
|
||||
|
||||
class ThreadSafeFileCache {
|
||||
public:
|
||||
explicit ThreadSafeFileCache() = default;
|
||||
ThreadSafeFileCache(const ThreadSafeFileCache&) = delete;
|
||||
ThreadSafeFileCache(ThreadSafeFileCache&&) = delete;
|
||||
ThreadSafeFileCache& operator=(const ThreadSafeFileCache&) = delete;
|
||||
ThreadSafeFileCache& operator=(ThreadSafeFileCache&&) = delete;
|
||||
~ThreadSafeFileCache() = default;
|
||||
|
||||
// Warning: generate() is called while the lock is held for writing, so it
|
||||
// will block other threads.
|
||||
std::shared_ptr<const std::string> get(const std::string& name, std::function<std::shared_ptr<const std::string>(const std::string&)> generate);
|
||||
|
||||
private:
|
||||
std::shared_mutex lock;
|
||||
std::unordered_map<std::string, std::shared_ptr<const std::string>> name_to_file;
|
||||
};
|
||||
|
||||
+117
-43
@@ -10,6 +10,7 @@
|
||||
|
||||
#ifdef HAVE_RESOURCE_FILE
|
||||
#include <resource_file/Emulators/PPC32Emulator.hh>
|
||||
#include <resource_file/Emulators/X86Emulator.hh>
|
||||
#endif
|
||||
|
||||
#include "CommandFormats.hh"
|
||||
@@ -133,28 +134,74 @@ shared_ptr<CompiledFunctionCode> compile_function_code(
|
||||
ret->index = 0;
|
||||
ret->hide_from_patches_menu = false;
|
||||
|
||||
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
|
||||
auto assembled = PPC32Emulator::assemble(text, {directory});
|
||||
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");
|
||||
}
|
||||
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);
|
||||
}
|
||||
unordered_set<string> get_include_stack;
|
||||
function<string(const string&)> get_include = [&](const string& name) -> string {
|
||||
const char* arch_name_token;
|
||||
switch (arch) {
|
||||
case CompiledFunctionCode::Architecture::POWERPC:
|
||||
arch_name_token = "ppc";
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::X86:
|
||||
arch_name_token = "x86";
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::SH4:
|
||||
arch_name_token = "sh4";
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("unknown architecture");
|
||||
}
|
||||
|
||||
string asm_filename = string_printf("%s/%s.%s.inc.s", directory.c_str(), name.c_str(), arch_name_token);
|
||||
if (isfile(asm_filename)) {
|
||||
if (!get_include_stack.emplace(name).second) {
|
||||
throw runtime_error("mutual recursion between includes: " + name);
|
||||
}
|
||||
EmulatorBase::AssembleResult ret;
|
||||
switch (arch) {
|
||||
case CompiledFunctionCode::Architecture::POWERPC:
|
||||
ret = PPC32Emulator::assemble(load_file(asm_filename), get_include);
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::X86:
|
||||
ret = X86Emulator::assemble(load_file(asm_filename), get_include);
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::SH4:
|
||||
throw runtime_error("cannot compuile SH-4 assembly");
|
||||
default:
|
||||
throw runtime_error("unknown architecture");
|
||||
}
|
||||
get_include_stack.erase(name);
|
||||
return ret.code;
|
||||
}
|
||||
string bin_filename = directory + "/" + name + ".inc.bin";
|
||||
if (isfile(bin_filename)) {
|
||||
return load_file(bin_filename);
|
||||
}
|
||||
throw runtime_error("data not found for include: " + name + " (from " + asm_filename + " or " + bin_filename + ")");
|
||||
};
|
||||
|
||||
EmulatorBase::AssembleResult assembled;
|
||||
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
|
||||
assembled = PPC32Emulator::assemble(text, get_include);
|
||||
} else if (arch == CompiledFunctionCode::Architecture::X86) {
|
||||
throw runtime_error("x86 assembler is not implemented");
|
||||
assembled = X86Emulator::assemble(text, get_include);
|
||||
}
|
||||
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");
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
set<uint32_t> reloc_indexes;
|
||||
@@ -191,31 +238,57 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
|
||||
}
|
||||
|
||||
uint32_t next_menu_item_id = 0;
|
||||
for (const auto& filename : list_directory(directory)) {
|
||||
if (!ends_with(filename, ".s") || ends_with(filename, ".inc.s")) {
|
||||
continue;
|
||||
}
|
||||
bool is_patch = ends_with(filename, ".patch.s");
|
||||
string name = filename.substr(0, filename.size() - (is_patch ? 8 : 2));
|
||||
|
||||
// Check for specific_version token
|
||||
uint32_t specific_version = 0;
|
||||
string short_name = name;
|
||||
if (is_patch &&
|
||||
(filename.size() >= 13) &&
|
||||
(filename[filename.size() - 13] == '.') &&
|
||||
isdigit(filename[filename.size() - 12]) &&
|
||||
(filename[filename.size() - 11] == 'O' || filename[filename.size() - 11] == 'S') &&
|
||||
(filename[filename.size() - 10] == 'E' || filename[filename.size() - 10] == 'J' || filename[filename.size() - 10] == 'P') &&
|
||||
(isdigit(filename[filename.size() - 9]) || filename[filename.size() - 9] == 'T')) {
|
||||
specific_version = 0x33000000 | (filename[filename.size() - 11] << 16) | (filename[filename.size() - 10] << 8) | filename[filename.size() - 9];
|
||||
short_name = filename.substr(0, filename.size() - 13);
|
||||
}
|
||||
|
||||
for (const auto& filename : list_directory_sorted(directory)) {
|
||||
try {
|
||||
if (!ends_with(filename, ".s")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = filename.substr(0, filename.size() - 2);
|
||||
if (ends_with(name, ".inc")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool is_patch = ends_with(name, ".patch");
|
||||
if (is_patch) {
|
||||
name.resize(name.size() - 6);
|
||||
}
|
||||
|
||||
// Figure out the version or specific_version
|
||||
CompiledFunctionCode::Architecture arch = CompiledFunctionCode::Architecture::UNKNOWN;
|
||||
uint32_t specific_version = 0;
|
||||
string short_name = name;
|
||||
if (ends_with(name, ".ppc")) {
|
||||
arch = CompiledFunctionCode::Architecture::POWERPC;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (ends_with(name, ".x86")) {
|
||||
arch = CompiledFunctionCode::Architecture::X86;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (ends_with(name, ".sh4")) {
|
||||
arch = CompiledFunctionCode::Architecture::SH4;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (is_patch && (name.size() >= 5) && (name[name.size() - 5] == '.')) {
|
||||
specific_version = (name[name.size() - 4] << 24) | (name[name.size() - 3] << 16) | (name[name.size() - 2] << 8) | name[name.size() - 1];
|
||||
if (specific_version_is_gc(specific_version)) {
|
||||
arch = CompiledFunctionCode::Architecture::POWERPC;
|
||||
} else if (specific_version_is_xb(specific_version) || specific_version_is_bb(specific_version)) {
|
||||
arch = CompiledFunctionCode::Architecture::X86;
|
||||
} else {
|
||||
throw runtime_error("unable to determine architecture from specific_version");
|
||||
}
|
||||
short_name = name.substr(0, name.size() - 5);
|
||||
}
|
||||
|
||||
if (arch == CompiledFunctionCode::Architecture::UNKNOWN) {
|
||||
throw runtime_error("unable to determine architecture");
|
||||
}
|
||||
|
||||
string path = directory + "/" + filename;
|
||||
string text = load_file(path);
|
||||
auto code = compile_function_code(CompiledFunctionCode::Architecture::POWERPC, directory, name, text);
|
||||
auto code = compile_function_code(arch, directory, name, text);
|
||||
if (code->index != 0) {
|
||||
if (!this->index_to_function.emplace(code->index, code).second) {
|
||||
throw runtime_error(string_printf(
|
||||
@@ -223,6 +296,7 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
|
||||
}
|
||||
}
|
||||
code->specific_version = specific_version;
|
||||
code->source_path = path;
|
||||
code->short_name = short_name;
|
||||
this->name_to_function.emplace(name, code);
|
||||
if (is_patch) {
|
||||
@@ -239,7 +313,7 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
|
||||
index_prefix.c_str(), patch_prefix.c_str(), name.c_str(), name_for_architecture(code->arch));
|
||||
|
||||
} catch (const exception& e) {
|
||||
function_compiler_log.warning("Failed to compile function %s: %s", name.c_str(), e.what());
|
||||
function_compiler_log.warning("Failed to compile function %s: %s", filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ void set_function_compiler_available(bool is_available);
|
||||
|
||||
struct CompiledFunctionCode {
|
||||
enum class Architecture {
|
||||
POWERPC = 0, // GC
|
||||
UNKNOWN = 0,
|
||||
POWERPC, // GC
|
||||
X86, // PC, XB, BB
|
||||
SH4, // Dreamcast
|
||||
};
|
||||
@@ -27,6 +28,7 @@ struct CompiledFunctionCode {
|
||||
std::vector<uint16_t> relocation_deltas;
|
||||
std::unordered_map<std::string, uint32_t> label_offsets;
|
||||
uint32_t entrypoint_offset_offset;
|
||||
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
|
||||
|
||||
+16
-16
@@ -37,23 +37,23 @@ 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_xrgb8888_to_xrgb1555(uint32_t xrgb8888) {
|
||||
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: -rrrrrgggggbbbbb
|
||||
return ((xrgb8888 >> 9) & 0x7C00) | ((xrgb8888 >> 6) & 0x03E0) | ((xrgb8888 >> 3) & 0x001F);
|
||||
// Out: arrrrrgggggbbbbb
|
||||
return ((rgba8888 >> 17) & 0x7C00) | ((rgba8888 >> 14) & 0x03E0) | ((rgba8888 >> 11) & 0x001F) | ((rgba8888 << 8) & 0x8000);
|
||||
}
|
||||
|
||||
constexpr uint16_t encode_rgbx8888_to_xrgb1555(uint32_t rgbx8888) {
|
||||
// In: rrrrrrrrggggggggbbbbbbbbxxxxxxxx
|
||||
// Out: -rrrrrgggggbbbbb
|
||||
return ((rgbx8888 >> 17) & 0x7C00) | ((rgbx8888 >> 14) & 0x03E0) | ((rgbx8888 >> 11) & 0x001F);
|
||||
}
|
||||
|
||||
constexpr uint32_t decode_xrgb1555_to_rgba8888(uint16_t xrgb1555) {
|
||||
// In: -rrrrrgggggbbbbb
|
||||
// Out: rrrrrrrrggggggggbbbbbbbbaaaaaaaa (a is always FF)
|
||||
return ((xrgb1555 << 17) & 0xF8000000) | ((xrgb1555 << 12) & 0x07000000) |
|
||||
((xrgb1555 << 14) & 0x00F80000) | ((xrgb1555 << 9) & 0x00070000) |
|
||||
((xrgb1555 << 11) & 0x0000F800) | ((xrgb1555 << 6) & 0x00000700) |
|
||||
0x000000FF;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -187,7 +187,8 @@ IPStackSimulator::IPClient::IPClient(shared_ptr<IPStackSimulator> sim, FrameInfo
|
||||
mac_addr(0),
|
||||
ipv4_addr(0),
|
||||
idle_timeout_event(event_new(sim->base.get(), -1, EV_TIMEOUT, &IPStackSimulator::IPClient::dispatch_on_idle_timeout, this), event_free) {
|
||||
struct timeval tv = usecs_to_timeval(60 * 1000 * 1000);
|
||||
uint64_t idle_timeout_usecs = sim->state->client_idle_timeout_usecs;
|
||||
struct timeval tv = usecs_to_timeval(idle_timeout_usecs);
|
||||
event_add(this->idle_timeout_event.get(), &tv);
|
||||
}
|
||||
|
||||
@@ -294,7 +295,9 @@ void IPStackSimulator::on_client_input(struct bufferevent* bev) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct timeval tv = usecs_to_timeval(60 * 1000 * 1000);
|
||||
auto sim = c->sim.lock();
|
||||
uint64_t idle_timeout_usecs = sim ? sim->state->client_idle_timeout_usecs : 60000000;
|
||||
struct timeval tv = usecs_to_timeval(idle_timeout_usecs);
|
||||
event_add(c->idle_timeout_event.get(), &tv);
|
||||
|
||||
while (evbuffer_get_length(buf) >= 2) {
|
||||
@@ -1426,7 +1429,9 @@ void IPStackSimulator::on_server_input(shared_ptr<IPClient> c, IPClient::TCPConn
|
||||
ip_stack_simulator_log.debug("Server input event: 0x%zX bytes to read",
|
||||
evbuffer_get_length(buf));
|
||||
|
||||
struct timeval tv = usecs_to_timeval(60 * 1000 * 1000);
|
||||
auto sim = c->sim.lock();
|
||||
uint64_t idle_timeout_usecs = sim ? sim->state->client_idle_timeout_usecs : 60000000;
|
||||
struct timeval tv = usecs_to_timeval(idle_timeout_usecs);
|
||||
event_add(c->idle_timeout_event.get(), &tv);
|
||||
|
||||
evbuffer_add_buffer(conn.pending_data.get(), buf);
|
||||
|
||||
+96
-89
@@ -52,19 +52,6 @@ void ItemCreator::set_random_state(uint32_t seed, uint32_t absolute_offset) {
|
||||
}
|
||||
}
|
||||
|
||||
void ItemCreator::set_box_destroyed(uint16_t entity_id) {
|
||||
this->destroyed_boxes.emplace(entity_id);
|
||||
}
|
||||
|
||||
void ItemCreator::set_monster_destroyed(uint16_t entity_id) {
|
||||
this->destroyed_monsters.emplace(entity_id);
|
||||
}
|
||||
|
||||
void ItemCreator::clear_destroyed_entities() {
|
||||
this->destroyed_monsters.clear();
|
||||
this->destroyed_boxes.clear();
|
||||
}
|
||||
|
||||
bool ItemCreator::are_rare_drops_allowed() const {
|
||||
// Note: The client has an additional check here, which appears to be a subtle
|
||||
// anti-cheating measure. There is a flag on the client, initially zero, which
|
||||
@@ -130,50 +117,49 @@ uint8_t ItemCreator::normalize_area_number(uint8_t area) const {
|
||||
}
|
||||
|
||||
} else {
|
||||
return this->restrictions->box_drop_area;
|
||||
return this->restrictions->box_drop_area - 1;
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_box_item_drop(uint16_t entity_id, uint8_t area) {
|
||||
return this->destroyed_boxes.count(entity_id)
|
||||
? ItemData()
|
||||
: this->on_box_item_drop_with_area_norm(this->normalize_area_number(area));
|
||||
ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) {
|
||||
return this->on_box_item_drop_with_area_norm(this->normalize_area_number(area));
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area) {
|
||||
return this->destroyed_monsters.count(entity_id)
|
||||
? ItemData()
|
||||
: this->on_monster_item_drop_with_area_norm(enemy_type, this->normalize_area_number(area));
|
||||
ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, uint8_t area) {
|
||||
return this->on_monster_item_drop_with_area_norm(enemy_type, this->normalize_area_number(area));
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
|
||||
ItemCreator::DropResult ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
|
||||
this->log.info("Box drop checks for area_norm %02hhX; random state: %08" PRIX32 " %08" PRIX32,
|
||||
area_norm, this->random_crypt.seed(), this->random_crypt.absolute_offset());
|
||||
ItemData item = this->check_rare_specs_and_create_rare_box_item(area_norm);
|
||||
if (item.empty()) {
|
||||
DropResult res;
|
||||
res.item = this->check_rare_specs_and_create_rare_box_item(area_norm);
|
||||
if (!res.item.empty()) {
|
||||
res.is_from_rare_table = true;
|
||||
} else {
|
||||
uint8_t item_class = this->get_rand_from_weighted_tables_2d_vertical(this->pt->box_item_class_prob_table, area_norm);
|
||||
this->log.info("Item class is %02hhX", item_class);
|
||||
switch (item_class) {
|
||||
case 0: // Weapon
|
||||
item.data1[0] = 0;
|
||||
res.item.data1[0] = 0;
|
||||
break;
|
||||
case 1: // Armor
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 1;
|
||||
res.item.data1[0] = 1;
|
||||
res.item.data1[1] = 1;
|
||||
break;
|
||||
case 2: // Shield
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 2;
|
||||
res.item.data1[0] = 1;
|
||||
res.item.data1[1] = 2;
|
||||
break;
|
||||
case 3: // Unit
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 3;
|
||||
res.item.data1[0] = 1;
|
||||
res.item.data1[1] = 3;
|
||||
break;
|
||||
case 4: // Tool
|
||||
item.data1[0] = 3;
|
||||
res.item.data1[0] = 3;
|
||||
break;
|
||||
case 5: // Meseta
|
||||
item.data1[0] = 4;
|
||||
res.item.data1[0] = 4;
|
||||
break;
|
||||
case 6: // Nothing
|
||||
break;
|
||||
@@ -181,16 +167,16 @@ ItemData ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
|
||||
throw logic_error("this should be impossible");
|
||||
}
|
||||
if (item_class < 6) {
|
||||
this->generate_common_item_variances(area_norm, item);
|
||||
this->generate_common_item_variances(area_norm, res.item);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm) {
|
||||
ItemCreator::DropResult ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm) {
|
||||
if (enemy_type > 0x58) {
|
||||
this->log.warning("Invalid enemy type: %" PRIX32, enemy_type);
|
||||
return ItemData();
|
||||
return DropResult();
|
||||
}
|
||||
this->log.info("Enemy type: %" PRIX32 "; random state: %08" PRIX32 " %08" PRIX32, enemy_type, this->random_crypt.seed(), this->random_crypt.absolute_offset());
|
||||
|
||||
@@ -198,13 +184,16 @@ ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, u
|
||||
uint8_t drop_sample = this->rand_int(100);
|
||||
if (drop_sample >= type_drop_prob) {
|
||||
this->log.info("Drop not chosen (%hhu >= %hhu)", drop_sample, type_drop_prob);
|
||||
return ItemData();
|
||||
return DropResult();
|
||||
} else {
|
||||
this->log.info("Drop chosen (%hhu < %hhu)", drop_sample, type_drop_prob);
|
||||
}
|
||||
|
||||
ItemData item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area_norm);
|
||||
if (item.empty()) {
|
||||
DropResult res;
|
||||
res.item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area_norm);
|
||||
if (!res.item.empty()) {
|
||||
res.is_from_rare_table = true;
|
||||
} else {
|
||||
uint32_t item_class_determinant =
|
||||
this->should_allow_meseta_drops()
|
||||
? this->rand_int(3)
|
||||
@@ -229,34 +218,34 @@ ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, u
|
||||
|
||||
switch (item_class) {
|
||||
case 0: // Weapon
|
||||
item.data1[0] = 0x00;
|
||||
res.item.data1[0] = 0x00;
|
||||
break;
|
||||
case 1: // Armor
|
||||
item.data1w[0] = 0x0101;
|
||||
res.item.data1w[0] = 0x0101;
|
||||
break;
|
||||
case 2: // Shield
|
||||
item.data1w[0] = 0x0201;
|
||||
res.item.data1w[0] = 0x0201;
|
||||
break;
|
||||
case 3: // Unit
|
||||
item.data1w[0] = 0x0301;
|
||||
res.item.data1w[0] = 0x0301;
|
||||
break;
|
||||
case 4: // Tool
|
||||
item.data1[0] = 0x03;
|
||||
res.item.data1[0] = 0x03;
|
||||
break;
|
||||
case 5: // Meseta
|
||||
item.data1[0] = 0x04;
|
||||
item.data2d = this->choose_meseta_amount(this->pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
|
||||
res.item.data1[0] = 0x04;
|
||||
res.item.data2d = this->choose_meseta_amount(this->pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
|
||||
break;
|
||||
default:
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
if (item.data1[0] != 0x04) {
|
||||
this->generate_common_item_variances(area_norm, item);
|
||||
if (res.item.data1[0] != 0x04) {
|
||||
this->generate_common_item_variances(area_norm, res.item);
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area_norm) {
|
||||
@@ -296,16 +285,17 @@ uint32_t ItemCreator::choose_meseta_amount(
|
||||
uint16_t min = ranges[table_index].min;
|
||||
uint16_t max = ranges[table_index].max;
|
||||
|
||||
// Note: The original code seems like it has a bug here: it compares to 0xFF
|
||||
// instead of 0xFFFF (and returns 0xFF if either limit matches 0xFF).
|
||||
uint32_t ret = 0;
|
||||
if (((min == 0xFFFF) || (max == 0xFFFF)) || (max < min)) {
|
||||
ret = 0xFFFF;
|
||||
} else if (min != max) {
|
||||
ret = this->rand_int((max - min) + 1) + min;
|
||||
} else {
|
||||
// Note: The original code returns 0xFF here if either limit is equal to 0xFF
|
||||
// (despite them being 16-bit integers!)
|
||||
uint16_t ret;
|
||||
if (min == max) {
|
||||
ret = min;
|
||||
} else if (max < min) {
|
||||
ret = this->rand_int((min - max) + 1) + max;
|
||||
} else {
|
||||
ret = this->rand_int((max - min) + 1) + min;
|
||||
}
|
||||
|
||||
this->log.info("Chose %" PRIu32 " Meseta from range [%hu, %hu]", ret, min, max);
|
||||
return ret;
|
||||
}
|
||||
@@ -474,7 +464,7 @@ void ItemCreator::set_item_unidentified_flag_if_not_challenge(ItemData& item) co
|
||||
|
||||
void ItemCreator::set_tool_item_amount_to_1(ItemData& item) const {
|
||||
if (item.data1[0] == 0x03) {
|
||||
item.set_tool_item_amount(1);
|
||||
item.set_tool_item_amount(this->version, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,10 +685,20 @@ uint8_t ItemCreator::generate_tech_disk_level(uint32_t tech_num, uint32_t area_n
|
||||
return range.min;
|
||||
}
|
||||
|
||||
void ItemCreator::generate_common_mag_variances(ItemData& item) const {
|
||||
void ItemCreator::generate_common_mag_variances(ItemData& item) {
|
||||
if (item.data1[0] == 0x02) {
|
||||
item.data1[1] = 0x00;
|
||||
item.assign_mag_stats(ItemMagStats());
|
||||
|
||||
// The original code (on PSO GC) assigns the mag color as 0x0E. We assign
|
||||
// a random color instead.
|
||||
if (is_pre_v1(this->version)) {
|
||||
item.data2[3] = 0x00;
|
||||
} else if (is_v1_or_v2(this->version)) {
|
||||
item.data2[3] = this->random_crypt.next() % 0x0E;
|
||||
} else {
|
||||
item.data2[3] = this->random_crypt.next() % 0x12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,20 +813,28 @@ void ItemCreator::generate_unit_stars_tables() {
|
||||
switch (this->version) {
|
||||
case Version::PC_PATCH:
|
||||
case Version::BB_PATCH:
|
||||
case Version::GC_NTE:
|
||||
throw logic_error("unknown parameters for version");
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
throw logic_error("ItemCreator cannot be created for Episode 3 games");
|
||||
case Version::DC_NTE:
|
||||
star_base_index = 0x124;
|
||||
num_units = 0x43;
|
||||
break;
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
case Version::DC_V1:
|
||||
star_base_index = 0x128;
|
||||
num_units = 0x44;
|
||||
break;
|
||||
case Version::DC_V2:
|
||||
case Version::PC_NTE:
|
||||
case Version::PC_V2:
|
||||
star_base_index = 0x1D1;
|
||||
num_units = 0x44;
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
star_base_index = 0x251;
|
||||
num_units = 0x47;
|
||||
break;
|
||||
case Version::GC_V3:
|
||||
case Version::XB_V3:
|
||||
star_base_index = 0x2AF;
|
||||
@@ -987,7 +995,7 @@ bool ItemCreator::shop_does_not_contain_duplicate_or_too_many_similar_weapons(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemCreator::shop_does_not_contain_duplicate_item_by_primary_identifier(
|
||||
bool ItemCreator::shop_does_not_contain_duplicate_item_by_data1_0_1_2(
|
||||
const vector<ItemData>& shop, const ItemData& item) {
|
||||
for (const auto& shop_item : shop) {
|
||||
if ((shop_item.data1[0] == item.data1[0]) &&
|
||||
@@ -1080,7 +1088,7 @@ void ItemCreator::generate_armor_shop_shields(vector<ItemData>& shop, size_t pla
|
||||
}
|
||||
}
|
||||
|
||||
if (this->shop_does_not_contain_duplicate_item_by_primary_identifier(shop, item)) {
|
||||
if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) {
|
||||
shop.emplace_back(std::move(item));
|
||||
items_generated++;
|
||||
}
|
||||
@@ -1114,7 +1122,7 @@ void ItemCreator::generate_armor_shop_units(vector<ItemData>& shop, size_t playe
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 3;
|
||||
item.data1[2] = pt.pop();
|
||||
if (this->shop_does_not_contain_duplicate_item_by_primary_identifier(shop, item)) {
|
||||
if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) {
|
||||
shop.emplace_back(std::move(item));
|
||||
items_generated++;
|
||||
}
|
||||
@@ -1225,7 +1233,7 @@ void ItemCreator::generate_rare_tool_shop_recovery_items(
|
||||
item.data1[0] = 3;
|
||||
item.data1[1] = tool_item_defs[type].first;
|
||||
item.data1[2] = tool_item_defs[type].second;
|
||||
if (this->shop_does_not_contain_duplicate_item_by_primary_identifier(shop, item)) {
|
||||
if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) {
|
||||
shop.emplace_back(std::move(item));
|
||||
items_generated++;
|
||||
}
|
||||
@@ -1663,23 +1671,20 @@ void ItemCreator::generate_weapon_shop_item_bonus2(ItemData& item, size_t player
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_specialized_box_item_drop(
|
||||
uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2) {
|
||||
if (this->destroyed_boxes.count(entity_id)) {
|
||||
return ItemData();
|
||||
}
|
||||
|
||||
ItemData item = this->base_item_for_specialized_box(def0, def1, def2);
|
||||
ItemCreator::DropResult ItemCreator::on_specialized_box_item_drop(
|
||||
uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2) {
|
||||
DropResult res;
|
||||
res.item = this->base_item_for_specialized_box(def0, def1, def2);
|
||||
if (def_z == 0.0f) {
|
||||
uint16_t type = item.data1w[0];
|
||||
item.clear();
|
||||
item.data1w[0] = type;
|
||||
this->generate_common_item_variances(this->normalize_area_number(area), item);
|
||||
uint16_t type = res.item.data1w[0];
|
||||
res.item.clear();
|
||||
res.item.data1w[0] = type;
|
||||
this->generate_common_item_variances(this->normalize_area_number(area), res.item);
|
||||
}
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemData ItemCreator::base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) {
|
||||
ItemData ItemCreator::base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) const {
|
||||
ItemData item;
|
||||
item.data1[0] = (def0 >> 0x18) & 0x0F;
|
||||
item.data1[1] = (def0 >> 0x10) + ((item.data1[0] == 0x00) || (item.data1[0] == 0x01));
|
||||
@@ -1708,7 +1713,7 @@ ItemData ItemCreator::base_item_for_specialized_box(uint32_t def0, uint32_t def1
|
||||
if (item.data1[1] == 0x02) {
|
||||
item.data1[4] = def0 & 0xFF;
|
||||
}
|
||||
item.set_tool_item_amount(1);
|
||||
item.set_tool_item_amount(this->version, 1);
|
||||
break;
|
||||
case 0x04:
|
||||
item.data2d = ((def1 >> 0x10) & 0xFFFF) * 10;
|
||||
@@ -1753,13 +1758,15 @@ ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) {
|
||||
} else {
|
||||
new_special = item.data1[4];
|
||||
}
|
||||
if ((new_special != item.data1[4]) &&
|
||||
(this->item_parameter_table->get_special(item.data1[4]).type ==
|
||||
this->item_parameter_table->get_special(new_special).type)) {
|
||||
this->log.info("(Special) Delta canceled because it would change special category");
|
||||
item.data1[4] = new_special;
|
||||
if (new_special != item.data1[4]) {
|
||||
if (this->item_parameter_table->get_special(item.data1[4]).type ==
|
||||
this->item_parameter_table->get_special(new_special).type) {
|
||||
item.data1[4] = new_special;
|
||||
} else {
|
||||
this->log.info("(Special) Delta canceled because it would change special category");
|
||||
}
|
||||
}
|
||||
} catch (const runtime_error&) {
|
||||
} catch (const out_of_range&) {
|
||||
// Invalid special number passed to get_special; just ignore it
|
||||
}
|
||||
luck += this->tekker_adjustment_set->get_luck_for_special_upgrade(delta_index);
|
||||
|
||||
+12
-13
@@ -29,16 +29,17 @@ public:
|
||||
~ItemCreator() = default;
|
||||
|
||||
void set_random_state(uint32_t seed, uint32_t absolute_offset);
|
||||
void clear_destroyed_entities();
|
||||
|
||||
ItemData on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area);
|
||||
ItemData on_box_item_drop(uint16_t entity_id, uint8_t area);
|
||||
ItemData on_specialized_box_item_drop(uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2);
|
||||
struct DropResult {
|
||||
ItemData item;
|
||||
bool is_from_rare_table = false;
|
||||
};
|
||||
|
||||
void set_monster_destroyed(uint16_t entity_id);
|
||||
void set_box_destroyed(uint16_t entity_id);
|
||||
DropResult on_monster_item_drop(uint32_t enemy_type, uint8_t area);
|
||||
DropResult on_box_item_drop(uint8_t area);
|
||||
DropResult on_specialized_box_item_drop(uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2);
|
||||
|
||||
static ItemData base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2);
|
||||
ItemData base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) const;
|
||||
|
||||
std::vector<ItemData> generate_armor_shop_contents(size_t player_level);
|
||||
std::vector<ItemData> generate_tool_shop_contents(size_t player_level);
|
||||
@@ -77,8 +78,6 @@ private:
|
||||
// Note: The original implementation uses 17 different random states for some
|
||||
// reason. We forego that and use only one for simplicity.
|
||||
PSOV2Encryption random_crypt;
|
||||
std::unordered_set<uint16_t> destroyed_monsters;
|
||||
std::unordered_set<uint16_t> destroyed_boxes;
|
||||
|
||||
inline bool is_v3() const {
|
||||
return !is_v1_or_v2(this->version);
|
||||
@@ -87,8 +86,8 @@ private:
|
||||
bool are_rare_drops_allowed() const;
|
||||
uint8_t normalize_area_number(uint8_t area) const;
|
||||
|
||||
ItemData on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm);
|
||||
ItemData on_box_item_drop_with_area_norm(uint8_t area_norm);
|
||||
DropResult on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm);
|
||||
DropResult on_box_item_drop_with_area_norm(uint8_t area_norm);
|
||||
|
||||
uint32_t rand_int(uint64_t max);
|
||||
float rand_float_0_1_from_crypt();
|
||||
@@ -116,7 +115,7 @@ private:
|
||||
void generate_common_armor_or_shield_type_and_variances(char area_norm, ItemData& item);
|
||||
void generate_common_tool_variances(uint32_t area_norm, ItemData& item);
|
||||
uint8_t generate_tech_disk_level(uint32_t tech_num, uint32_t area_norm);
|
||||
void generate_common_mag_variances(ItemData& item) const;
|
||||
void generate_common_mag_variances(ItemData& item);
|
||||
void generate_common_weapon_variances(uint8_t area_norm, ItemData& item);
|
||||
void generate_common_weapon_grind(ItemData& item, uint8_t offset_within_subtype_range);
|
||||
void generate_common_weapon_bonuses(ItemData& item, uint8_t area_norm);
|
||||
@@ -135,7 +134,7 @@ private:
|
||||
const std::vector<ItemData>& shop, const ItemData& item);
|
||||
static bool shop_does_not_contain_duplicate_or_too_many_similar_weapons(
|
||||
const std::vector<ItemData>& shop, const ItemData& item);
|
||||
static bool shop_does_not_contain_duplicate_item_by_primary_identifier(
|
||||
static bool shop_does_not_contain_duplicate_item_by_data1_0_1_2(
|
||||
const std::vector<ItemData>& shop, const ItemData& item);
|
||||
void generate_armor_shop_armors(
|
||||
std::vector<ItemData>& shop, size_t player_level);
|
||||
|
||||
+51
-28
@@ -71,20 +71,21 @@ uint32_t ItemData::primary_identifier() const {
|
||||
// The game treats any item starting with 04 as Meseta, and ignores the rest
|
||||
// of data1 (the value is in data2)
|
||||
if (this->data1[0] == 0x04) {
|
||||
return 0x040000;
|
||||
return 0x04000000;
|
||||
}
|
||||
if (this->data1[0] == 0x03 && this->data1[1] == 0x02) {
|
||||
return 0x030200; // Tech disk (data1[2] is level, so omit it)
|
||||
// Tech disk (tech ID is data1[4], not [2])
|
||||
return 0x03020000 | (this->data1[4] << 8) | this->data1[2];
|
||||
} else if (this->data1[0] == 0x02) {
|
||||
return 0x020000 | (this->data1[1] << 8); // Mag
|
||||
return 0x02000000 | (this->data1[1] << 16); // Mag
|
||||
} else if (this->is_s_rank_weapon()) {
|
||||
return (this->data1[0] << 16) | (this->data1[1] << 8);
|
||||
return (this->data1[0] << 24) | (this->data1[1] << 16);
|
||||
} else {
|
||||
return (this->data1[0] << 16) | (this->data1[1] << 8) | this->data1[2];
|
||||
return (this->data1[0] << 24) | (this->data1[1] << 16) | (this->data1[2] << 8);
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemData::is_wrapped() const {
|
||||
bool ItemData::is_wrapped(Version version) const {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
@@ -92,7 +93,7 @@ bool ItemData::is_wrapped() const {
|
||||
case 2:
|
||||
return this->data2[2] & 0x40;
|
||||
case 3:
|
||||
return !this->is_stackable() && (this->data1[3] & 0x40);
|
||||
return !this->is_stackable(version) && (this->data1[3] & 0x40);
|
||||
case 4:
|
||||
return false;
|
||||
default:
|
||||
@@ -100,7 +101,7 @@ bool ItemData::is_wrapped() const {
|
||||
}
|
||||
}
|
||||
|
||||
void ItemData::wrap() {
|
||||
void ItemData::wrap(Version version) {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
@@ -110,7 +111,7 @@ void ItemData::wrap() {
|
||||
this->data2[2] |= 0x40;
|
||||
break;
|
||||
case 3:
|
||||
if (!this->is_stackable()) {
|
||||
if (!this->is_stackable(version)) {
|
||||
this->data1[3] |= 0x40;
|
||||
}
|
||||
break;
|
||||
@@ -121,7 +122,7 @@ void ItemData::wrap() {
|
||||
}
|
||||
}
|
||||
|
||||
void ItemData::unwrap() {
|
||||
void ItemData::unwrap(Version version) {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
@@ -131,7 +132,7 @@ void ItemData::unwrap() {
|
||||
this->data2[2] &= 0xBF;
|
||||
break;
|
||||
case 3:
|
||||
if (!this->is_stackable()) {
|
||||
if (!this->is_stackable(version)) {
|
||||
this->data1[3] &= 0xBF;
|
||||
}
|
||||
break;
|
||||
@@ -142,32 +143,31 @@ void ItemData::unwrap() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemData::is_stackable() const {
|
||||
return this->max_stack_size() > 1;
|
||||
bool ItemData::is_stackable(Version version) const {
|
||||
return this->max_stack_size(version) > 1;
|
||||
}
|
||||
|
||||
size_t ItemData::stack_size() const {
|
||||
if (max_stack_size_for_item(this->data1[0], this->data1[1]) > 1) {
|
||||
size_t ItemData::stack_size(Version version) const {
|
||||
if (max_stack_size_for_item(version, this->data1[0], this->data1[1]) > 1) {
|
||||
return this->data1[5];
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t ItemData::max_stack_size() const {
|
||||
return max_stack_size_for_item(this->data1[0], this->data1[1]);
|
||||
size_t ItemData::max_stack_size(Version version) const {
|
||||
return max_stack_size_for_item(version, this->data1[0], this->data1[1]);
|
||||
}
|
||||
|
||||
void ItemData::enforce_min_stack_size() {
|
||||
if (this->stack_size() == 0) {
|
||||
void ItemData::enforce_min_stack_size(Version version) {
|
||||
if (this->stack_size(version) == 0) {
|
||||
this->data1[5] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemData::is_common_consumable(uint32_t primary_identifier) {
|
||||
if (primary_identifier == 0x030200) {
|
||||
return false;
|
||||
}
|
||||
return (primary_identifier >= 0x030000) && (primary_identifier < 0x030A00);
|
||||
return (primary_identifier >= 0x03000000) &&
|
||||
(primary_identifier < 0x030A0000) &&
|
||||
((primary_identifier & 0xFFFF0000) != 0x03020000);
|
||||
}
|
||||
|
||||
bool ItemData::is_common_consumable() const {
|
||||
@@ -298,8 +298,8 @@ void ItemData::add_mag_photon_blast(uint8_t pb_num) {
|
||||
}
|
||||
if (pb_num >= 4) {
|
||||
throw runtime_error("left photon blast number is too high");
|
||||
pb_nums |= (pb_num << 6);
|
||||
}
|
||||
pb_nums |= (pb_num << 6);
|
||||
flags |= 4;
|
||||
}
|
||||
}
|
||||
@@ -502,12 +502,12 @@ void ItemData::set_sealed_item_kill_count(uint16_t v) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t ItemData::get_tool_item_amount() const {
|
||||
return this->is_stackable() ? this->data1[5] : 1;
|
||||
uint8_t ItemData::get_tool_item_amount(Version version) const {
|
||||
return this->is_stackable(version) ? this->data1[5] : 1;
|
||||
}
|
||||
|
||||
void ItemData::set_tool_item_amount(uint8_t amount) {
|
||||
if (this->is_stackable()) {
|
||||
void ItemData::set_tool_item_amount(Version version, uint8_t amount) {
|
||||
if (this->is_stackable(version)) {
|
||||
this->data1[5] = amount;
|
||||
} else if (this->data1[0] == 0x03) {
|
||||
this->data1[5] = 0x00;
|
||||
@@ -647,6 +647,9 @@ bool ItemData::compare_for_sort(const ItemData& a, const ItemData& b) {
|
||||
}
|
||||
|
||||
ItemData ItemData::from_data(const string& data) {
|
||||
if (data.size() < 2) {
|
||||
throw runtime_error("data is too short");
|
||||
}
|
||||
if (data.size() > 0x10) {
|
||||
throw runtime_error("data is too long");
|
||||
}
|
||||
@@ -658,6 +661,26 @@ ItemData ItemData::from_data(const string& data) {
|
||||
for (size_t z = 12; z < min<size_t>(data.size(), 16); z++) {
|
||||
ret.data2[z - 12] = data[z];
|
||||
}
|
||||
if (ret.data1[0] > 4) {
|
||||
throw runtime_error("invalid item class");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
ItemData ItemData::from_primary_identifier(Version version, uint32_t primary_identifier) {
|
||||
ItemData ret;
|
||||
if (primary_identifier > 0x04000000) {
|
||||
throw runtime_error("invalid item class");
|
||||
}
|
||||
ret.data1[0] = (primary_identifier >> 24) & 0xFF;
|
||||
ret.data1[1] = (primary_identifier >> 16) & 0xFF;
|
||||
if ((primary_identifier & 0xFFFF0000) == 0x03020000) {
|
||||
ret.data1[4] = (primary_identifier >> 8) & 0xFF;
|
||||
ret.data1[2] = primary_identifier & 0xFF;
|
||||
} else {
|
||||
ret.data1[2] = (primary_identifier >> 8) & 0xFF;
|
||||
}
|
||||
ret.set_tool_item_amount(version, 1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
+19
-31
@@ -6,8 +6,6 @@
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
constexpr uint32_t MESETA_IDENTIFIER = 0x040000;
|
||||
|
||||
class ItemParameterTable;
|
||||
|
||||
enum class EquipSlot {
|
||||
@@ -29,26 +27,15 @@ enum class EquipSlot {
|
||||
};
|
||||
|
||||
struct ItemMagStats {
|
||||
uint16_t iq;
|
||||
uint16_t synchro;
|
||||
uint16_t def;
|
||||
uint16_t pow;
|
||||
uint16_t dex;
|
||||
uint16_t mind;
|
||||
uint8_t flags;
|
||||
uint8_t photon_blasts;
|
||||
uint8_t color;
|
||||
|
||||
ItemMagStats()
|
||||
: iq(0),
|
||||
synchro(40),
|
||||
def(500),
|
||||
pow(0),
|
||||
dex(0),
|
||||
mind(0),
|
||||
flags(0),
|
||||
photon_blasts(0),
|
||||
color(14) {}
|
||||
uint16_t iq = 0;
|
||||
uint16_t synchro = 40;
|
||||
uint16_t def = 500;
|
||||
uint16_t pow = 0;
|
||||
uint16_t dex = 0;
|
||||
uint16_t mind = 0;
|
||||
uint8_t flags = 0;
|
||||
uint8_t photon_blasts = 0;
|
||||
uint8_t color = 14;
|
||||
|
||||
inline uint16_t def_level() const {
|
||||
return this->def / 100;
|
||||
@@ -137,17 +124,18 @@ struct ItemData { // 0x14 bytes
|
||||
void clear();
|
||||
|
||||
static ItemData from_data(const std::string& data);
|
||||
static ItemData from_primary_identifier(Version version, uint32_t primary_identifier);
|
||||
std::string hex() const;
|
||||
uint32_t primary_identifier() const;
|
||||
|
||||
bool is_wrapped() const;
|
||||
void wrap();
|
||||
void unwrap();
|
||||
bool is_wrapped(Version version) const;
|
||||
void wrap(Version version);
|
||||
void unwrap(Version version);
|
||||
|
||||
bool is_stackable() const;
|
||||
size_t stack_size() const;
|
||||
size_t max_stack_size() const;
|
||||
void enforce_min_stack_size();
|
||||
bool is_stackable(Version version) const;
|
||||
size_t stack_size(Version version) const;
|
||||
size_t max_stack_size(Version version) const;
|
||||
void enforce_min_stack_size(Version version);
|
||||
|
||||
static bool is_common_consumable(uint32_t primary_identifier);
|
||||
bool is_common_consumable() const;
|
||||
@@ -166,8 +154,8 @@ struct ItemData { // 0x14 bytes
|
||||
|
||||
uint16_t get_sealed_item_kill_count() const;
|
||||
void set_sealed_item_kill_count(uint16_t v);
|
||||
uint8_t get_tool_item_amount() const;
|
||||
void set_tool_item_amount(uint8_t amount);
|
||||
uint8_t get_tool_item_amount(Version version) const;
|
||||
void set_tool_item_amount(Version version, uint8_t amount);
|
||||
int16_t get_armor_or_shield_defense_bonus() const;
|
||||
void set_armor_or_shield_defense_bonus(int16_t bonus);
|
||||
int16_t get_common_armor_evasion_bonus() const;
|
||||
|
||||
+360
-100
@@ -4,36 +4,28 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
ItemNameIndex::ItemNameIndex(JSON&& v2_names, JSON&& v3_names, JSON&& v4_names) {
|
||||
auto get_or_create_meta = [&](uint32_t primary_identifier) {
|
||||
shared_ptr<ItemMetadata> meta;
|
||||
ItemNameIndex::ItemNameIndex(
|
||||
Version version,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
const std::vector<std::string>& name_coll)
|
||||
: version(version),
|
||||
item_parameter_table(item_parameter_table) {
|
||||
|
||||
for (uint32_t primary_identifier : item_parameter_table->compute_all_valid_primary_identifiers()) {
|
||||
const string* name = nullptr;
|
||||
try {
|
||||
return this->primary_identifier_index.at(primary_identifier);
|
||||
ItemData item = ItemData::from_primary_identifier(this->version, primary_identifier);
|
||||
name = &name_coll.at(item_parameter_table->get_item_id(item));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
if (name) {
|
||||
auto meta = make_shared<ItemMetadata>();
|
||||
meta->primary_identifier = primary_identifier;
|
||||
this->primary_identifier_index.emplace(primary_identifier, meta);
|
||||
return meta;
|
||||
meta->name = *name;
|
||||
this->primary_identifier_index.emplace(meta->primary_identifier, meta);
|
||||
this->name_index.emplace(tolower(meta->name), meta);
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& it : v2_names.as_dict()) {
|
||||
uint32_t primary_identifier = stoul(it.first, nullptr, 16);
|
||||
auto meta = get_or_create_meta(primary_identifier);
|
||||
meta->v2_name = std::move(it.second->as_string());
|
||||
this->v2_name_index.emplace(tolower(meta->v2_name), meta);
|
||||
}
|
||||
for (const auto& it : v3_names.as_dict()) {
|
||||
uint32_t primary_identifier = stoul(it.first, nullptr, 16);
|
||||
auto meta = get_or_create_meta(primary_identifier);
|
||||
meta->v3_name = std::move(it.second->as_string());
|
||||
this->v3_name_index.emplace(tolower(meta->v3_name), meta);
|
||||
}
|
||||
for (const auto& it : v4_names.as_dict()) {
|
||||
uint32_t primary_identifier = stoul(it.first, nullptr, 16);
|
||||
auto meta = get_or_create_meta(primary_identifier);
|
||||
meta->v4_name = std::move(it.second->as_string());
|
||||
this->v4_name_index.emplace(tolower(meta->v4_name), meta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,11 +98,10 @@ const array<const char*, 0x11> name_for_s_rank_special = {
|
||||
};
|
||||
|
||||
std::string ItemNameIndex::describe_item(
|
||||
Version version,
|
||||
const ItemData& item,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table) const {
|
||||
bool include_color_escapes) const {
|
||||
if (item.data1[0] == 0x04) {
|
||||
return string_printf("%s%" PRIu32 " Meseta", item_parameter_table ? "$C7" : "", item.data2d.load());
|
||||
return string_printf("%s%" PRIu32 " Meseta", include_color_escapes ? "$C7" : "", item.data2d.load());
|
||||
}
|
||||
|
||||
vector<string> ret_tokens;
|
||||
@@ -147,21 +138,19 @@ std::string ItemNameIndex::describe_item(
|
||||
// flags in a different location.
|
||||
if (((item.data1[1] == 0x01) && (item.data1[4] & 0x40)) ||
|
||||
((item.data1[0] == 0x02) && (item.data2[2] & 0x40)) ||
|
||||
((item.data1[0] == 0x03) && !item.is_stackable() && (item.data1[3] & 0x40))) {
|
||||
((item.data1[0] == 0x03) && !item.is_stackable(this->version) && (item.data1[3] & 0x40))) {
|
||||
ret_tokens.emplace_back("Wrapped");
|
||||
}
|
||||
|
||||
// Add the item name. Technique disks are special because the level is part of
|
||||
// the primary identifier, so we manually generate the name instead of looking
|
||||
// it up.
|
||||
// Add the item name
|
||||
uint32_t primary_identifier = item.primary_identifier();
|
||||
if ((primary_identifier & 0xFFFFFF00) == 0x00030200) {
|
||||
if ((primary_identifier & 0xFFFF0000) == 0x03020000) {
|
||||
string technique_name;
|
||||
try {
|
||||
technique_name = tech_id_to_name.at(item.data1[4]);
|
||||
technique_name[0] = toupper(technique_name[0]);
|
||||
} catch (const out_of_range&) {
|
||||
technique_name = string_printf("!TECH:%02hhX", item.data1[4]);
|
||||
technique_name = string_printf("!TD:%02hhX", item.data1[4]);
|
||||
}
|
||||
// Hide the level for Reverser and Ryuker, unless the level isn't 1
|
||||
if ((item.data1[2] == 0) && ((item.data1[4] == 0x0E) || (item.data1[4] == 0x11))) {
|
||||
@@ -172,21 +161,9 @@ std::string ItemNameIndex::describe_item(
|
||||
} else {
|
||||
try {
|
||||
auto meta = this->primary_identifier_index.at(primary_identifier);
|
||||
const string* name;
|
||||
if (is_v4(version)) {
|
||||
name = &meta->v4_name;
|
||||
} else if (is_v3(version)) {
|
||||
name = &meta->v3_name;
|
||||
} else {
|
||||
name = &meta->v2_name;
|
||||
}
|
||||
if (name->empty()) {
|
||||
throw out_of_range("item does not exist");
|
||||
}
|
||||
ret_tokens.emplace_back(*name);
|
||||
|
||||
ret_tokens.emplace_back(meta->name);
|
||||
} catch (const out_of_range&) {
|
||||
ret_tokens.emplace_back(string_printf("!ID:%06" PRIX32, primary_identifier));
|
||||
ret_tokens.emplace_back(string_printf("!ID:%08" PRIX32, primary_identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,16 +224,16 @@ std::string ItemNameIndex::describe_item(
|
||||
// For armors, add the slots, unit modifiers, and/or DEF/EVP bonuses
|
||||
} else if (item.data1[0] == 0x01) {
|
||||
if (item.data1[1] == 0x03) { // Units
|
||||
uint16_t modifier = item.data1w[3];
|
||||
if (modifier == 0x0001 || modifier == 0x0002) {
|
||||
int16_t modifier = item.data1w[3];
|
||||
if (modifier == 1 || modifier == 2) {
|
||||
ret_tokens.back().append("+");
|
||||
} else if (modifier == 0x0003 || modifier == 0x0004) {
|
||||
} else if (modifier >= 3) {
|
||||
ret_tokens.back().append("++");
|
||||
} else if (modifier == 0xFFFF || modifier == 0xFFFE) {
|
||||
} else if (modifier == -1 || modifier == -2) {
|
||||
ret_tokens.back().append("-");
|
||||
} else if (modifier == 0xFFFD || modifier == 0xFFFC) {
|
||||
} else if (modifier <= -3) {
|
||||
ret_tokens.back().append("--");
|
||||
} else if (modifier != 0x0000) {
|
||||
} else if (modifier != 0) {
|
||||
ret_tokens.emplace_back(string_printf("!MD:%04hX", modifier));
|
||||
}
|
||||
|
||||
@@ -341,16 +318,16 @@ std::string ItemNameIndex::describe_item(
|
||||
|
||||
// For tools, add the amount (if applicable)
|
||||
} else if (item.data1[0] == 0x03) {
|
||||
if (item.max_stack_size() > 1) {
|
||||
if (item.max_stack_size(this->version) > 1) {
|
||||
ret_tokens.emplace_back(string_printf("x%hhu", item.data1[5]));
|
||||
}
|
||||
}
|
||||
|
||||
string ret = join(ret_tokens, " ");
|
||||
if (item_parameter_table) {
|
||||
if (include_color_escapes) {
|
||||
if (item.is_s_rank_weapon()) {
|
||||
return "$C4" + ret;
|
||||
} else if (item_parameter_table->is_item_rare(item)) {
|
||||
} else if (this->item_parameter_table->is_item_rare(item)) {
|
||||
return "$C6" + ret;
|
||||
} else if (item.has_bonuses()) {
|
||||
return "$C2" + ret;
|
||||
@@ -362,48 +339,32 @@ std::string ItemNameIndex::describe_item(
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemNameIndex::parse_item_description(Version version, const std::string& desc) const {
|
||||
ItemData ItemNameIndex::parse_item_description(const std::string& desc) const {
|
||||
ItemData ret;
|
||||
try {
|
||||
ret = this->parse_item_description_phase(version, desc, false);
|
||||
ret = this->parse_item_description_phase(desc, false);
|
||||
} catch (const exception& e1) {
|
||||
try {
|
||||
ret = this->parse_item_description_phase(version, desc, true);
|
||||
ret = this->parse_item_description_phase(desc, true);
|
||||
} catch (const exception& e2) {
|
||||
try {
|
||||
string data = parse_data_string(desc);
|
||||
if (data.size() < 2) {
|
||||
throw runtime_error("item code too short");
|
||||
}
|
||||
if (data[0] > 4) {
|
||||
throw runtime_error("invalid item class");
|
||||
}
|
||||
if (data.size() > 16) {
|
||||
throw runtime_error("item code too long");
|
||||
}
|
||||
|
||||
if (data.size() <= 12) {
|
||||
memcpy(ret.data1.data(), data.data(), data.size());
|
||||
} else {
|
||||
memcpy(ret.data1.data(), data.data(), 12);
|
||||
memcpy(ret.data2.data(), data.data() + 12, data.size() - 12);
|
||||
}
|
||||
ret = ItemData::from_data(parse_data_string(desc));
|
||||
} catch (const exception& ed) {
|
||||
if (strcmp(e1.what(), e2.what())) {
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" in %s (as text 1: %s) (as text 2: %s) (as data: %s)",
|
||||
desc.c_str(), name_for_enum(version), e1.what(), e2.what(), ed.what()));
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" (as text 1: %s) (as text 2: %s) (as data: %s)",
|
||||
desc.c_str(), e1.what(), e2.what(), ed.what()));
|
||||
} else {
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" in %s (as text: %s) (as data: %s)",
|
||||
desc.c_str(), name_for_enum(version), e1.what(), ed.what()));
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" (as text: %s) (as data: %s)",
|
||||
desc.c_str(), e1.what(), ed.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ret.enforce_min_stack_size();
|
||||
ret.enforce_min_stack_size(this->version);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ItemData ItemNameIndex::parse_item_description_phase(Version version, const std::string& description, bool skip_special) const {
|
||||
ItemData ItemNameIndex::parse_item_description_phase(const std::string& description, bool skip_special) const {
|
||||
ItemData ret;
|
||||
ret.data1d.clear(0);
|
||||
ret.id = 0xFFFFFFFF;
|
||||
@@ -464,24 +425,15 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
}
|
||||
}
|
||||
|
||||
const map<string, shared_ptr<ItemMetadata>>* name_index;
|
||||
if (is_v4(version)) {
|
||||
name_index = &this->v4_name_index;
|
||||
} else if (is_v3(version)) {
|
||||
name_index = &this->v3_name_index;
|
||||
} else {
|
||||
name_index = &this->v2_name_index;
|
||||
}
|
||||
|
||||
auto name_it = name_index->lower_bound(desc);
|
||||
auto name_it = this->name_index.lower_bound(desc);
|
||||
// Look up to 3 places before the lower bound. We have to do this to catch
|
||||
// cases like Sange vs. Sange & Yasha - if the input is like "Sange 0/...",
|
||||
// then we'll see Sange & Yasha first, which we should skip.
|
||||
size_t lookback = 0;
|
||||
while (lookback < 4) {
|
||||
if (name_it != name_index->end() && desc.starts_with(name_it->first)) {
|
||||
if (name_it != this->name_index.end() && desc.starts_with(name_it->first)) {
|
||||
break;
|
||||
} else if (name_it == name_index->begin()) {
|
||||
} else if (name_it == this->name_index.begin()) {
|
||||
throw runtime_error("no such item");
|
||||
} else {
|
||||
name_it--;
|
||||
@@ -497,10 +449,12 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
desc = desc.substr(1);
|
||||
}
|
||||
|
||||
// Tech disks should have already been handled above, so we don't need to
|
||||
// special-case 0302xxxx identifiers here.
|
||||
uint32_t primary_identifier = name_it->second->primary_identifier;
|
||||
ret.data1[0] = (primary_identifier >> 16) & 0xFF;
|
||||
ret.data1[1] = (primary_identifier >> 8) & 0xFF;
|
||||
ret.data1[2] = primary_identifier & 0xFF;
|
||||
ret.data1[0] = (primary_identifier >> 24) & 0xFF;
|
||||
ret.data1[1] = (primary_identifier >> 16) & 0xFF;
|
||||
ret.data1[2] = (primary_identifier >> 8) & 0xFF;
|
||||
|
||||
if (ret.data1[0] == 0x00) {
|
||||
// Weapons: add special, grind and percentages (or name, if S-rank)
|
||||
@@ -642,7 +596,7 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
ret.data2[2] |= 0x40;
|
||||
}
|
||||
} else if (ret.data1[0] == 0x03) {
|
||||
if (ret.max_stack_size() > 1) {
|
||||
if (ret.max_stack_size(this->version) > 1) {
|
||||
if (starts_with(desc, "x")) {
|
||||
ret.data1[5] = stoul(desc.substr(1), nullptr, 10);
|
||||
} else {
|
||||
@@ -653,7 +607,7 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
}
|
||||
|
||||
if (is_wrapped) {
|
||||
if (ret.is_stackable()) {
|
||||
if (ret.is_stackable(this->version)) {
|
||||
throw runtime_error("stackable items cannot be wrapped");
|
||||
} else {
|
||||
ret.data1[3] |= 0x40;
|
||||
@@ -665,3 +619,309 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ItemNameIndex::print_table(FILE* stream) const {
|
||||
auto pmt = this->item_parameter_table;
|
||||
|
||||
fprintf(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 CT 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);
|
||||
string divisor_str = string_printf("%g", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
|
||||
size_t data1_2_limit = pmt->num_weapons_in_class(data1_1);
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& w = pmt->get_weapon(data1_1, data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(w.base.id);
|
||||
bool is_unsealable = pmt->is_unsealable_item(0x00, data1_1, data1_2);
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x00;
|
||||
item.data1[1] = data1_1;
|
||||
item.data1[2] = data1_2;
|
||||
string name = this->describe_item(item);
|
||||
|
||||
fprintf(stream, "00%02zX%02zX => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %04hX %5hu %5hu %5hu %5hu %5hu %5hu %3hhu %02hhX %02hhX %3hhu %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %2hhu* %s %s %s\n",
|
||||
data1_1,
|
||||
data1_2,
|
||||
w.base.id.load(),
|
||||
w.base.type.load(),
|
||||
w.base.skin.load(),
|
||||
w.base.team_points.load(),
|
||||
w.class_flags.load(),
|
||||
w.atp_min.load(),
|
||||
w.atp_max.load(),
|
||||
w.atp_required.load(),
|
||||
w.mst_required.load(),
|
||||
w.ata_required.load(),
|
||||
w.mst.load(),
|
||||
w.max_grind,
|
||||
w.photon,
|
||||
w.special,
|
||||
w.ata,
|
||||
w.stat_boost,
|
||||
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_a4,
|
||||
w.unknown_a5,
|
||||
w.tech_boost,
|
||||
w.combo_type,
|
||||
v1_replacement,
|
||||
stars,
|
||||
is_unsealable ? "YES" : " no",
|
||||
divisor_str.c_str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "ARMOR => ---ID--- TYPE SKIN POINTS -DFP- -EVP- BP BE FLAG LVL EFR ETH EIC EDK ELT DFR EVR SB TB -A2- 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 = string_printf("%g", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
|
||||
size_t data1_2_limit = pmt->num_armors_or_shields_in_class(data1_1);
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& a = pmt->get_armor_or_shield(data1_1, data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(a.base.id);
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x01;
|
||||
item.data1[1] = data1_1;
|
||||
item.data1[2] = data1_2;
|
||||
string name = this->describe_item(item);
|
||||
|
||||
fprintf(stream, "01%02zX%02zX => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %5hu %5hu %02hhX %02hhX %04hX %3hhu %3hhu %3hhu %3hhu %3hhu %3hhu %3hhu %3hhu %02hhX %02hhX %04hX %2hhu* %s %s\n",
|
||||
data1_1,
|
||||
data1_2,
|
||||
a.base.id.load(),
|
||||
a.base.type.load(),
|
||||
a.base.skin.load(),
|
||||
a.base.team_points.load(),
|
||||
a.dfp.load(),
|
||||
a.evp.load(),
|
||||
a.block_particle,
|
||||
a.block_effect,
|
||||
a.class_flags.load(),
|
||||
static_cast<uint8_t>(a.required_level + 1),
|
||||
a.efr,
|
||||
a.eth,
|
||||
a.eic,
|
||||
a.edk,
|
||||
a.elt,
|
||||
a.dfp_range,
|
||||
a.evp_range,
|
||||
a.stat_boost,
|
||||
a.tech_boost,
|
||||
a.unknown_a2.load(),
|
||||
stars,
|
||||
divisor_str.c_str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "UNIT => ---ID--- TYPE SKIN POINTS STAT COUNT ST-MOD ST* ---DIVISOR--- NAME\n");
|
||||
{
|
||||
float sale_divisor = pmt->get_sale_divisor(0x01, 0x03);
|
||||
string divisor_str = string_printf("%g", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
|
||||
size_t data1_2_limit = pmt->num_units();
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& u = pmt->get_unit(data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(u.base.id);
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x01;
|
||||
item.data1[1] = 0x03;
|
||||
item.data1[2] = data1_2;
|
||||
string name = this->describe_item(item);
|
||||
|
||||
fprintf(stream, "0103%02zX => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %04hX %5hu %6hd %2hhu* %s %s\n",
|
||||
data1_2,
|
||||
u.base.id.load(),
|
||||
u.base.type.load(),
|
||||
u.base.skin.load(),
|
||||
u.base.team_points.load(),
|
||||
u.stat.load(),
|
||||
u.stat_amount.load(),
|
||||
u.modifier_amount.load(),
|
||||
stars,
|
||||
divisor_str.c_str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "MAG => ---ID--- TYPE SKIN POINTS FTBL PB AC E1 E2 E3 E4 C1 C2 C3 C4 FLAG ST* ---DIVISOR--- NAME\n");
|
||||
{
|
||||
size_t data1_1_limit = pmt->num_mags();
|
||||
for (size_t data1_1 = 0; data1_1 < data1_1_limit; data1_1++) {
|
||||
const auto& m = pmt->get_mag(data1_1);
|
||||
uint8_t stars = pmt->get_item_stars(m.base.id);
|
||||
|
||||
float sale_divisor = pmt->get_sale_divisor(0x02, data1_1);
|
||||
string divisor_str = string_printf("%g", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x02;
|
||||
item.data1[1] = data1_1;
|
||||
item.data1[2] = 0x00;
|
||||
string name = this->describe_item(item);
|
||||
|
||||
fprintf(stream, "02%02zX00 => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %04hX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %04hX %2hhu* %s %s\n",
|
||||
data1_1,
|
||||
m.base.id.load(),
|
||||
m.base.type.load(),
|
||||
m.base.skin.load(),
|
||||
m.base.team_points.load(),
|
||||
m.feed_table.load(),
|
||||
m.photon_blast,
|
||||
m.activation,
|
||||
m.on_pb_full,
|
||||
m.on_low_hp,
|
||||
m.on_death,
|
||||
m.on_boss,
|
||||
m.on_pb_full_flag,
|
||||
m.on_low_hp_flag,
|
||||
m.on_death_flag,
|
||||
m.on_boss_flag,
|
||||
m.class_flags.load(),
|
||||
stars,
|
||||
divisor_str.c_str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "TOOL => ---ID--- TYPE SKIN POINTS COUNT TECH -COST- ITEMFLAG ST* ---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 = string_printf("%g", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
|
||||
size_t data1_2_limit = pmt->num_tools_in_class(data1_1);
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& t = pmt->get_tool(data1_1, data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(t.base.id);
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x03;
|
||||
item.data1[1] = data1_1;
|
||||
item.data1[(data1_1 == 0x02) ? 4 : 2] = data1_2;
|
||||
item.set_tool_item_amount(this->version, 1);
|
||||
string name = this->describe_item(item);
|
||||
|
||||
fprintf(stream, "03%02zX%02zX => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %5hu %04hX %6" PRId32 " %08" PRIX32 " %2hhu* %s %s\n",
|
||||
data1_1,
|
||||
data1_2,
|
||||
t.base.id.load(),
|
||||
t.base.type.load(),
|
||||
t.base.skin.load(),
|
||||
t.base.team_points.load(),
|
||||
t.amount.load(),
|
||||
t.tech.load(),
|
||||
t.cost.load(),
|
||||
t.item_flag.load(),
|
||||
stars,
|
||||
divisor_str.c_str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(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++) {
|
||||
fprintf(stream, "%9s =>", 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) {
|
||||
fprintf(stream, " ");
|
||||
} else {
|
||||
fprintf(stream, " %2hhu", max_level);
|
||||
}
|
||||
}
|
||||
fprintf(stream, "\n");
|
||||
}
|
||||
|
||||
fprintf(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++) {
|
||||
fprintf(stream, "%9s =>", 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) {
|
||||
fprintf(stream, " ");
|
||||
} else {
|
||||
fprintf(stream, " %2hhu", max_level);
|
||||
}
|
||||
}
|
||||
fprintf(stream, "\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"};
|
||||
fprintf(stream, "TABLE %02zX => -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);
|
||||
fprintf(stream, "%14s => %4hhd %4hhd %4hhd %4hhd %4hhd %4hhd\n",
|
||||
names[which], res.def, res.pow, res.dex, res.mind, res.iq, res.synchro);
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "SPECIAL => TYPE COUNT ST*\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);
|
||||
fprintf(stream, " %02zX => %04hX %5hu %2hu*\n", index, sp.type.load(), sp.amount.load(), stars);
|
||||
}
|
||||
|
||||
fprintf(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) {
|
||||
fprintf(stream, "%02hhX%02hhX%02hhX + %02hhX%02hhX%02hhX => %02hhX%02hhX%02hhX",
|
||||
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]);
|
||||
if (combo.mag_level != 0xFF) {
|
||||
fprintf(stream, " %3hu", combo.mag_level);
|
||||
} else {
|
||||
fprintf(stream, " ");
|
||||
}
|
||||
if (combo.grind != 0xFF) {
|
||||
fprintf(stream, " %3hu", combo.grind);
|
||||
} else {
|
||||
fprintf(stream, " ");
|
||||
}
|
||||
if (combo.level != 0xFF) {
|
||||
fprintf(stream, " %3hu", combo.level);
|
||||
} else {
|
||||
fprintf(stream, " ");
|
||||
}
|
||||
if (combo.char_class != 0xFF) {
|
||||
fprintf(stream, " %3hu\n", combo.char_class);
|
||||
} else {
|
||||
fprintf(stream, " \n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t num_events = pmt->num_events();
|
||||
for (size_t event_number = 0; event_number < num_events; event_number++) {
|
||||
fprintf(stream, "EV %3zu => 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];
|
||||
fprintf(stream, "%02hhX%02hhX%02hhX => %3hhu\n",
|
||||
event_item.item[0], event_item.item[1], event_item.item[2], event_item.probability);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-18
@@ -14,26 +14,38 @@
|
||||
|
||||
class ItemNameIndex {
|
||||
public:
|
||||
ItemNameIndex(JSON&& v2_names, JSON&& v3_names, JSON&& v4_names);
|
||||
|
||||
std::string describe_item(
|
||||
Version version,
|
||||
const ItemData& item,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table = nullptr) const;
|
||||
ItemData parse_item_description(Version version, const std::string& description) const;
|
||||
|
||||
private:
|
||||
ItemData parse_item_description_phase(Version version, const std::string& description, bool skip_special) const;
|
||||
|
||||
struct ItemMetadata {
|
||||
uint32_t primary_identifier;
|
||||
std::string v2_name;
|
||||
std::string v3_name;
|
||||
std::string v4_name;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::unordered_map<uint32_t, std::shared_ptr<ItemMetadata>> primary_identifier_index;
|
||||
std::map<std::string, std::shared_ptr<ItemMetadata>> v2_name_index;
|
||||
std::map<std::string, std::shared_ptr<ItemMetadata>> v3_name_index;
|
||||
std::map<std::string, std::shared_ptr<ItemMetadata>> v4_name_index;
|
||||
ItemNameIndex(
|
||||
Version version,
|
||||
std::shared_ptr<const ItemParameterTable> pmt,
|
||||
const std::vector<std::string>& name_coll);
|
||||
|
||||
inline size_t entry_count() const {
|
||||
return this->primary_identifier_index.size();
|
||||
}
|
||||
|
||||
inline const std::unordered_map<uint32_t, std::shared_ptr<const ItemMetadata>>& all_by_primary_identifier() const {
|
||||
return this->primary_identifier_index;
|
||||
}
|
||||
inline const std::map<std::string, std::shared_ptr<const ItemMetadata>>& all_by_name() const {
|
||||
return this->name_index;
|
||||
}
|
||||
|
||||
std::string describe_item(const ItemData& item, bool include_color_escapes = false) const;
|
||||
ItemData parse_item_description(const std::string& description) const;
|
||||
|
||||
void print_table(FILE* stream) const;
|
||||
|
||||
private:
|
||||
ItemData parse_item_description_phase(const std::string& description, bool skip_special) const;
|
||||
|
||||
Version version;
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table;
|
||||
|
||||
std::unordered_map<uint32_t, std::shared_ptr<const ItemMetadata>> primary_identifier_index;
|
||||
std::map<std::string, std::shared_ptr<const ItemMetadata>> name_index;
|
||||
};
|
||||
|
||||
+714
-319
File diff suppressed because it is too large
Load Diff
+329
-150
@@ -5,6 +5,7 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -13,11 +14,16 @@
|
||||
|
||||
class ItemParameterTable {
|
||||
public:
|
||||
// TODO: This implementation is ugly. We should use real classes and virtual
|
||||
// functions instead of manually branching on various offset table pointers
|
||||
// being null or not in each public function. Rewrite this and make it better.
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct ArrayRef {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
U32T count;
|
||||
U32T offset;
|
||||
/* 00 */ U32T count;
|
||||
/* 04 */ U32T offset;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
struct ArrayRefLE : ArrayRef<false> {
|
||||
} __attribute__((packed));
|
||||
@@ -29,151 +35,240 @@ public:
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
// id specifies several things; notably, it doubles as the index of the
|
||||
// item's name in the text archive (e.g. TextEnglish) collection 0.
|
||||
U32T id = 0xFFFFFFFF;
|
||||
/* 00 */ U32T id = 0xFFFFFFFF;
|
||||
/* 04 */
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian>
|
||||
struct ItemBaseV3 : ItemBaseV2<IsBigEndian> {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
U16T type = 0;
|
||||
U16T skin = 0;
|
||||
/* 04 */ U16T type = 0;
|
||||
/* 06 */ U16T skin = 0;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian>
|
||||
struct ItemBaseV4 : ItemBaseV3<IsBigEndian> {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
U32T team_points = 0;
|
||||
/* 08 */ U32T team_points = 0;
|
||||
/* 0C */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct WeaponV2 {
|
||||
ItemBaseV2<false> base;
|
||||
le_uint16_t class_flags = 0;
|
||||
le_uint16_t atp_min = 0;
|
||||
le_uint16_t atp_max = 0;
|
||||
le_uint16_t atp_required = 0;
|
||||
le_uint16_t mst_required = 0;
|
||||
le_uint16_t ata_required = 0;
|
||||
uint8_t max_grind = 0;
|
||||
uint8_t photon = 0;
|
||||
uint8_t special = 0;
|
||||
uint8_t ata = 0;
|
||||
parray<uint8_t, 4> unknown_a9;
|
||||
struct WeaponV4;
|
||||
struct WeaponDCProtos {
|
||||
/* 00 */ ItemBaseV2<false> base;
|
||||
/* 04 */ le_uint16_t class_flags = 0;
|
||||
/* 06 */ le_uint16_t atp_min = 0;
|
||||
/* 08 */ le_uint16_t atp_max = 0;
|
||||
/* 0A */ le_uint16_t atp_required = 0;
|
||||
/* 0C */ le_uint16_t mst_required = 0;
|
||||
/* 0E */ le_uint16_t ata_required = 0;
|
||||
/* 10 */ uint8_t max_grind = 0;
|
||||
/* 11 */ uint8_t photon = 0;
|
||||
/* 12 */ uint8_t special = 0;
|
||||
/* 13 */ uint8_t ata = 0;
|
||||
/* 14 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct WeaponV1V2 {
|
||||
/* 00 */ ItemBaseV2<false> base;
|
||||
/* 04 */ le_uint16_t class_flags = 0;
|
||||
/* 06 */ le_uint16_t atp_min = 0;
|
||||
/* 08 */ le_uint16_t atp_max = 0;
|
||||
/* 0A */ le_uint16_t atp_required = 0;
|
||||
/* 0C */ le_uint16_t mst_required = 0;
|
||||
/* 0E */ le_uint16_t ata_required = 0;
|
||||
/* 10 */ uint8_t max_grind = 0;
|
||||
/* 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)
|
||||
/* 15 */ parray<uint8_t, 3> unknown_a9;
|
||||
/* 18 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct WeaponGCNTE {
|
||||
/* 00 */ ItemBaseV3<true> base;
|
||||
/* 08 */ be_uint16_t class_flags = 0;
|
||||
/* 0A */ be_uint16_t atp_min = 0;
|
||||
/* 0C */ be_uint16_t atp_max = 0;
|
||||
/* 0E */ be_uint16_t atp_required = 0;
|
||||
/* 10 */ be_uint16_t mst_required = 0;
|
||||
/* 12 */ be_uint16_t ata_required = 0;
|
||||
/* 14 */ be_uint16_t mst = 0;
|
||||
/* 16 */ uint8_t max_grind = 0;
|
||||
/* 17 */ uint8_t photon = 0;
|
||||
/* 18 */ uint8_t special = 0;
|
||||
/* 19 */ uint8_t ata = 0;
|
||||
/* 1A */ uint8_t stat_boost = 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;
|
||||
/* 24 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct WeaponV3 {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
ItemBaseV3<IsBigEndian> base;
|
||||
U16T class_flags = 0;
|
||||
U16T atp_min = 0;
|
||||
U16T atp_max = 0;
|
||||
U16T atp_required = 0;
|
||||
U16T mst_required = 0;
|
||||
U16T ata_required = 0;
|
||||
U16T mst = 0;
|
||||
uint8_t max_grind = 0;
|
||||
uint8_t photon = 0;
|
||||
uint8_t special = 0;
|
||||
uint8_t ata = 0;
|
||||
uint8_t stat_boost = 0;
|
||||
uint8_t projectile = 0;
|
||||
int8_t trail1_x = 0;
|
||||
int8_t trail1_y = 0;
|
||||
int8_t trail2_x = 0;
|
||||
int8_t trail2_y = 0;
|
||||
int8_t color = 0;
|
||||
uint8_t unknown_a1 = 0;
|
||||
uint8_t unknown_a2 = 0;
|
||||
uint8_t unknown_a3 = 0;
|
||||
uint8_t unknown_a4 = 0;
|
||||
uint8_t unknown_a5 = 0;
|
||||
uint8_t tech_boost = 0;
|
||||
uint8_t combo_type = 0;
|
||||
/* 00 */ ItemBaseV3<IsBigEndian> base;
|
||||
/* 08 */ U16T class_flags = 0;
|
||||
/* 0A */ U16T atp_min = 0;
|
||||
/* 0C */ U16T atp_max = 0;
|
||||
/* 0E */ U16T atp_required = 0;
|
||||
/* 10 */ U16T mst_required = 0;
|
||||
/* 12 */ U16T ata_required = 0;
|
||||
/* 14 */ U16T mst = 0;
|
||||
/* 16 */ uint8_t max_grind = 0;
|
||||
/* 17 */ uint8_t photon = 0;
|
||||
/* 18 */ uint8_t special = 0;
|
||||
/* 19 */ uint8_t ata = 0;
|
||||
/* 1A */ uint8_t stat_boost = 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;
|
||||
/* 24 */ uint8_t unknown_a4 = 0;
|
||||
/* 25 */ uint8_t unknown_a5 = 0;
|
||||
/* 26 */ uint8_t tech_boost = 0;
|
||||
/* 27 */ uint8_t combo_type = 0;
|
||||
/* 28 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct WeaponV4 {
|
||||
ItemBaseV4<false> base;
|
||||
le_uint16_t class_flags = 0x00FF;
|
||||
le_uint16_t atp_min = 0;
|
||||
le_uint16_t atp_max = 0;
|
||||
le_uint16_t atp_required = 0;
|
||||
le_uint16_t mst_required = 0;
|
||||
le_uint16_t ata_required = 0;
|
||||
le_uint16_t mst = 0;
|
||||
uint8_t max_grind = 0;
|
||||
uint8_t photon = 0;
|
||||
uint8_t special = 0;
|
||||
uint8_t ata = 0;
|
||||
uint8_t stat_boost = 0;
|
||||
uint8_t projectile = 0;
|
||||
int8_t trail1_x = 0;
|
||||
int8_t trail1_y = 0;
|
||||
int8_t trail2_x = 0;
|
||||
int8_t trail2_y = 0;
|
||||
int8_t color = 0;
|
||||
uint8_t unknown_a1 = 0;
|
||||
uint8_t unknown_a2 = 0;
|
||||
uint8_t unknown_a3 = 0;
|
||||
uint8_t unknown_a4 = 0;
|
||||
uint8_t unknown_a5 = 0;
|
||||
uint8_t tech_boost = 0;
|
||||
uint8_t combo_type = 0;
|
||||
/* 00 */ ItemBaseV4<false> base;
|
||||
/* 0C */ le_uint16_t class_flags = 0x00FF;
|
||||
/* 0E */ le_uint16_t atp_min = 0;
|
||||
/* 10 */ le_uint16_t atp_max = 0;
|
||||
/* 12 */ le_uint16_t atp_required = 0;
|
||||
/* 14 */ le_uint16_t mst_required = 0;
|
||||
/* 16 */ le_uint16_t ata_required = 0;
|
||||
/* 18 */ le_uint16_t mst = 0;
|
||||
/* 1A */ uint8_t max_grind = 0;
|
||||
/* 1B */ uint8_t photon = 0;
|
||||
/* 1C */ uint8_t special = 0;
|
||||
/* 1D */ uint8_t ata = 0;
|
||||
/* 1E */ uint8_t stat_boost = 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;
|
||||
/* 28 */ uint8_t unknown_a4 = 0;
|
||||
/* 29 */ uint8_t unknown_a5 = 0;
|
||||
/* 2A */ uint8_t tech_boost = 0;
|
||||
/* 2B */ uint8_t combo_type = 0;
|
||||
/* 2C */
|
||||
} __attribute__((packed));
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct ArmorOrShield {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
BaseT base;
|
||||
U16T dfp = 0;
|
||||
U16T evp = 0;
|
||||
uint8_t block_particle = 0;
|
||||
uint8_t block_effect = 0;
|
||||
U16T class_flags = 0x00FF;
|
||||
uint8_t required_level = 0;
|
||||
uint8_t efr = 0;
|
||||
uint8_t eth = 0;
|
||||
uint8_t eic = 0;
|
||||
uint8_t edk = 0;
|
||||
uint8_t elt = 0;
|
||||
uint8_t dfp_range = 0;
|
||||
uint8_t evp_range = 0;
|
||||
uint8_t stat_boost = 0;
|
||||
uint8_t tech_boost = 0;
|
||||
U16T unknown_a2 = 0;
|
||||
/* V1/V2 offsets */
|
||||
/* 00 */ BaseT base;
|
||||
/* 04 */ U16T dfp = 0;
|
||||
/* 06 */ U16T evp = 0;
|
||||
/* 08 */ uint8_t block_particle = 0;
|
||||
/* 09 */ uint8_t block_effect = 0;
|
||||
/* 0A */ U16T class_flags = 0x00FF;
|
||||
/* 0C */ uint8_t required_level = 0;
|
||||
/* 0D */ uint8_t efr = 0;
|
||||
/* 0E */ uint8_t eth = 0;
|
||||
/* 0F */ uint8_t eic = 0;
|
||||
/* 10 */ uint8_t edk = 0;
|
||||
/* 11 */ uint8_t elt = 0;
|
||||
/* 12 */ uint8_t dfp_range = 0;
|
||||
/* 13 */ uint8_t evp_range = 0;
|
||||
/* 14 */
|
||||
} __attribute__((packed));
|
||||
struct ArmorOrShieldV2 : ArmorOrShield<ItemBaseV2<false>, false> {
|
||||
|
||||
struct ArmorOrShieldV4;
|
||||
struct ArmorOrShieldDCProtos : ArmorOrShield<ItemBaseV2<false>, false> {
|
||||
ArmorOrShieldV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct ArmorOrShieldV3 : ArmorOrShield<ItemBaseV3<true>, true> {
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct ArmorOrShieldFinal : ArmorOrShield<BaseT, IsBigEndian> {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
/* 14 */ uint8_t stat_boost = 0;
|
||||
/* 15 */ uint8_t tech_boost = 0;
|
||||
/* 16 */ U16T unknown_a2 = 0;
|
||||
/* 18 */
|
||||
} __attribute__((packed));
|
||||
struct ArmorOrShieldV4 : ArmorOrShield<ItemBaseV4<false>, false> {
|
||||
|
||||
struct ArmorOrShieldV1V2 : ArmorOrShieldFinal<ItemBaseV2<false>, false> {
|
||||
ArmorOrShieldV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian>
|
||||
struct ArmorOrShieldV3 : ArmorOrShieldFinal<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
ArmorOrShieldV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct ArmorOrShieldV4 : ArmorOrShieldFinal<ItemBaseV4<false>, false> {
|
||||
} __attribute__((packed));
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct Unit {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
using S16T = typename std::conditional<IsBigEndian, be_int16_t, le_int16_t>::type;
|
||||
BaseT base;
|
||||
U16T stat = 0;
|
||||
U16T stat_amount = 0;
|
||||
S16T modifier_amount = 0;
|
||||
parray<uint8_t, 2> unused;
|
||||
/* V1/V2 offsets */
|
||||
/* 00 */ BaseT base;
|
||||
/* 04 */ U16T stat = 0;
|
||||
/* 06 */ U16T stat_amount = 0;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
struct UnitV2 : Unit<ItemBaseV2<false>, false> {
|
||||
|
||||
struct UnitV4;
|
||||
struct UnitDCProtos : Unit<ItemBaseV2<false>, false> {
|
||||
UnitV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct UnitV3 : Unit<ItemBaseV3<true>, true> {
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct UnitFinal : Unit<BaseT, IsBigEndian> {
|
||||
using S16T = typename std::conditional<IsBigEndian, be_int16_t, le_int16_t>::type;
|
||||
/* 08 */ S16T modifier_amount = 0;
|
||||
/* 0A */ parray<uint8_t, 2> unused;
|
||||
/* 0C */
|
||||
} __attribute__((packed));
|
||||
struct UnitV4 : Unit<ItemBaseV4<false>, false> {
|
||||
struct UnitV1V2 : UnitFinal<ItemBaseV2<false>, false> {
|
||||
UnitV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian>
|
||||
struct UnitV3 : UnitFinal<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
UnitV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct UnitV4 : UnitFinal<ItemBaseV4<false>, false> {
|
||||
} __attribute__((packed));
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct Mag {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
BaseT base;
|
||||
U16T feed_table = 0;
|
||||
uint8_t photon_blast = 0;
|
||||
uint8_t activation = 0;
|
||||
uint8_t on_pb_full = 0;
|
||||
uint8_t on_low_hp = 0;
|
||||
uint8_t on_death = 0;
|
||||
uint8_t on_boss = 0;
|
||||
/* V1/V2 offsets */
|
||||
/* 00 */ BaseT base;
|
||||
/* 04 */ U16T feed_table = 0;
|
||||
/* 06 */ uint8_t photon_blast = 0;
|
||||
/* 07 */ uint8_t activation = 0;
|
||||
/* 08 */ uint8_t on_pb_full = 0;
|
||||
/* 09 */ uint8_t on_low_hp = 0;
|
||||
/* 0A */ uint8_t on_death = 0;
|
||||
/* 0B */ uint8_t on_boss = 0;
|
||||
// These flags control how likely each effect is to activate. First, the
|
||||
// game computes step_synchro as follows:
|
||||
// if synchro in [0, 30], step_synchro = 0
|
||||
@@ -189,18 +284,37 @@ public:
|
||||
// flag == 3 => activation - 10
|
||||
// flag == 4 => step_synchro - 10
|
||||
// anything else => 0 (effect never occurs)
|
||||
uint8_t on_pb_full_flag = 0;
|
||||
uint8_t on_low_hp_flag = 0;
|
||||
uint8_t on_death_flag = 0;
|
||||
uint8_t on_boss_flag = 0;
|
||||
U16T class_flags = 0x00FF;
|
||||
parray<uint8_t, 2> unused;
|
||||
/* 0C */ uint8_t on_pb_full_flag = 0;
|
||||
/* 0D */ uint8_t on_low_hp_flag = 0;
|
||||
/* 0E */ uint8_t on_death_flag = 0;
|
||||
/* 0F */ uint8_t on_boss_flag = 0;
|
||||
/* 10 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MagV4;
|
||||
struct MagV1 : Mag<ItemBaseV2<false>, false> {
|
||||
MagV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct MagV2 : Mag<ItemBaseV2<false>, false> {
|
||||
/* 10 */ le_uint16_t class_flags = 0x00FF;
|
||||
/* 12 */ parray<uint8_t, 2> unused;
|
||||
/* 14 */
|
||||
|
||||
MagV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct MagV3 : Mag<ItemBaseV3<true>, true> {
|
||||
template <bool IsBigEndian>
|
||||
struct MagV3 : Mag<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
/* 10 */ U16T class_flags = 0x00FF;
|
||||
/* 12 */ parray<uint8_t, 2> unused;
|
||||
/* 14 */
|
||||
|
||||
MagV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct MagV4 : Mag<ItemBaseV4<false>, false> {
|
||||
/* 10 */ le_uint16_t class_flags = 0x00FF;
|
||||
/* 12 */ parray<uint8_t, 2> unused;
|
||||
/* 14 */
|
||||
} __attribute__((packed));
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
@@ -208,15 +322,22 @@ public:
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
using S32T = typename std::conditional<IsBigEndian, be_int32_t, le_int32_t>::type;
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
BaseT base;
|
||||
U16T amount = 0;
|
||||
U16T tech = 0;
|
||||
S32T cost = 0;
|
||||
U32T item_flag = 0;
|
||||
/* V1/V2 offsets */
|
||||
/* 00 */ BaseT base;
|
||||
/* 04 */ U16T amount = 0;
|
||||
/* 06 */ U16T tech = 0;
|
||||
/* 08 */ S32T cost = 0;
|
||||
/* 0C */ U32T item_flag = 0;
|
||||
/* 10 */
|
||||
} __attribute__((packed));
|
||||
struct ToolV2 : Tool<ItemBaseV2<false>, false> {
|
||||
|
||||
struct ToolV4;
|
||||
struct ToolV1V2 : Tool<ItemBaseV2<false>, false> {
|
||||
ToolV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct ToolV3 : Tool<ItemBaseV3<true>, true> {
|
||||
template <bool IsBigEndian>
|
||||
struct ToolV3 : Tool<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
ToolV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct ToolV4 : Tool<ItemBaseV4<false>, false> {
|
||||
} __attribute__((packed));
|
||||
@@ -300,25 +421,28 @@ public:
|
||||
FloatT mag_divisor = 0.0f;
|
||||
} __attribute__((packed));
|
||||
|
||||
enum class Version {
|
||||
V2,
|
||||
V3,
|
||||
V4,
|
||||
};
|
||||
|
||||
ItemParameterTable(std::shared_ptr<const std::string> data, Version version);
|
||||
~ItemParameterTable() = default;
|
||||
|
||||
void print(FILE* stream) const;
|
||||
std::set<uint32_t> compute_all_valid_primary_identifiers() const;
|
||||
|
||||
size_t num_weapons_in_class(uint8_t data1_1) const;
|
||||
const WeaponV4& get_weapon(uint8_t data1_1, uint8_t data1_2) const;
|
||||
size_t num_armors_or_shields_in_class(uint8_t data1_1) const;
|
||||
const ArmorOrShieldV4& get_armor_or_shield(uint8_t data1_1, uint8_t data1_2) const;
|
||||
size_t num_units() const;
|
||||
const UnitV4& get_unit(uint8_t data1_2) const;
|
||||
size_t num_mags() const;
|
||||
const MagV4& get_mag(uint8_t data1_1) const;
|
||||
size_t num_tools_in_class(uint8_t data1_1) const;
|
||||
const ToolV4& get_tool(uint8_t data1_1, uint8_t data1_2) const;
|
||||
std::pair<uint8_t, uint8_t> find_tool_by_id(uint32_t id) const;
|
||||
|
||||
float get_sale_divisor(uint8_t data1_0, uint8_t data1_1) const;
|
||||
const MagFeedResult& get_mag_feed_result(uint8_t table_index, uint8_t which) const;
|
||||
uint8_t get_item_stars(uint32_t id) const;
|
||||
uint8_t get_special_stars(uint8_t det) const;
|
||||
uint8_t get_special_stars(uint8_t special) const;
|
||||
const Special<false>& get_special(uint8_t special) 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;
|
||||
@@ -328,10 +452,12 @@ public:
|
||||
uint8_t get_item_base_stars(const ItemData& item) const;
|
||||
uint8_t get_item_adjusted_stars(const ItemData& item) const;
|
||||
bool is_item_rare(const ItemData& item) const;
|
||||
bool is_unsealable_item(uint8_t data1_0, uint8_t data1_1, uint8_t data1_2) const;
|
||||
bool is_unsealable_item(const ItemData& param_1) const;
|
||||
const ItemCombination& get_item_combination(const ItemData& used_item, const ItemData& equipped_item) const;
|
||||
const std::vector<ItemCombination>& get_all_combinations_for_used_item(const ItemData& used_item) const;
|
||||
const std::map<uint32_t, std::vector<ItemCombination>>& get_all_item_combinations() const;
|
||||
size_t num_events() const;
|
||||
std::pair<const EventItem*, size_t> get_event_items(uint8_t event_number) const;
|
||||
|
||||
size_t price_for_item(const ItemData& item) const;
|
||||
@@ -343,29 +469,76 @@ public:
|
||||
size_t special_stars_begin_index;
|
||||
size_t num_specials;
|
||||
size_t first_rare_mag_index;
|
||||
size_t star_value_table_size;
|
||||
|
||||
private:
|
||||
struct TableOffsetsV2 {
|
||||
struct TableOffsetsDCProtos {
|
||||
/* ## / NTE / 11/2000 */
|
||||
/* 00 / 0013 / 0013 */ le_uint32_t unknown_a0;
|
||||
/* 04 / 2E1C / 2EB8 */ le_uint32_t weapon_table;
|
||||
/* 08 / 2D94 / 2E28 */ le_uint32_t armor_table;
|
||||
/* 0C / 2DA4 / 2E38 */ le_uint32_t unit_table;
|
||||
/* 10 / 2DB4 / 2E48 */ le_uint32_t tool_table;
|
||||
/* 14 / 2DAC / 2E40 */ le_uint32_t mag_table;
|
||||
/* 18 / 1F98 / 202C */ le_uint32_t v1_replacement_table;
|
||||
/* 1C / 1994 / 1A28 */ le_uint32_t photon_color_table;
|
||||
/* 20 / 1C64 / 1CF8 */ le_uint32_t weapon_range_table;
|
||||
/* 24 / 1FBF / 2053 */ le_uint32_t weapon_sale_divisor_table;
|
||||
/* 28 / 1FE6 / 207A */ le_uint32_t sale_divisor_table;
|
||||
/* 2C / 2F54 / 2FF0 */ le_uint32_t mag_feed_table;
|
||||
/* 30 / 22A9 / 233D */ le_uint32_t star_value_table;
|
||||
/* 34 / 23EE / 2484 */ le_uint32_t unknown_a1;
|
||||
/* 38 / 275E / 27F4 */ le_uint32_t special_data_table;
|
||||
/* 3C / 2804 / 2898 */ le_uint32_t stat_boost_table;
|
||||
/* 40 / 1908 / 199C */ le_uint32_t shield_effect_table;
|
||||
/* 44 / 0668 / 0668 */ le_uint32_t unknown_a2;
|
||||
/* 48 / 030C / 030C */ le_uint32_t unknown_a3;
|
||||
/* 4C / 2CE4 / 2D78 */ le_uint32_t unknown_a4;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct TableOffsetsV1V2 {
|
||||
// TODO: Is weapon count 0x89 or 0x8A? It could be that the last entry in
|
||||
// weapon_table is used for ???? items.
|
||||
/* 00 / 0013 */ le_uint32_t unknown_a0;
|
||||
/* 04 / 5AFC */ le_uint32_t weapon_table; // -> [{count, offset -> [WeaponV2]}](0x89)
|
||||
/* 08 / 5A5C */ le_uint32_t armor_table; // -> [{count, offset -> [ArmorOrShieldV2]}](2; armors and shields)
|
||||
/* 0C / 5A6C */ le_uint32_t unit_table; // -> {count, offset -> [UnitV2]} (last if out of range)
|
||||
/* 10 / 5A7C */ le_uint32_t tool_table; // -> [{count, offset -> [ToolV2]}](0x10) (last if out of range)
|
||||
/* 14 / 5A74 */ le_uint32_t mag_table; // -> {count, offset -> [MagV2]}
|
||||
/* 18 / 3DF8 */ le_uint32_t v1_replacement_table; // -> [uint8_t](0x89)
|
||||
/* 1C / 2E4C */ le_uint32_t photon_color_table; // -> [0x24-byte structs](0x20)
|
||||
/* 20 / 32CC */ le_uint32_t weapon_range_table; // -> ???
|
||||
/* 24 / 3E84 */ le_uint32_t weapon_sale_divisor_table; // -> [float](0x89)
|
||||
/* 28 / 40A8 */ le_uint32_t sale_divisor_table; // -> NonWeaponSaleDivisors
|
||||
/* 2C / 5F4C */ le_uint32_t mag_feed_table; // -> MagFeedResultsTable
|
||||
/* 30 / 4378 */ le_uint32_t star_value_table; // -> [uint8_t](0x1C7)
|
||||
/* 34 / 4540 */ le_uint32_t special_data_table; // -> [Special](0x29)
|
||||
/* 38 / 45E4 */ le_uint32_t weapon_effect_table; // -> [16-byte structs]
|
||||
/* 3C / 58DC */ le_uint32_t stat_boost_table; // -> [StatBoost]
|
||||
/* 40 / 5704 */ le_uint32_t shield_effect_table; // -> [8-byte structs]
|
||||
/* ## / V1 / V2*/
|
||||
/* 00 / 0013 / 0013 */ le_uint32_t unknown_a0;
|
||||
/* 04 / 32E8 / 5AFC */ le_uint32_t weapon_table; // -> [{count, offset -> [WeaponV2]}](0x89)
|
||||
/* 08 / 3258 / 5A5C */ le_uint32_t armor_table; // -> [{count, offset -> [ArmorOrShieldV2]}](2; armors and shields)
|
||||
/* 0C / 3268 / 5A6C */ le_uint32_t unit_table; // -> {count, offset -> [UnitV2]} (last if out of range)
|
||||
/* 10 / 3278 / 5A7C */ le_uint32_t tool_table; // -> [{count, offset -> [ToolV2]}](0x10) (last if out of range)
|
||||
/* 14 / 3270 / 5A74 */ le_uint32_t mag_table; // -> {count, offset -> [MagV2]}
|
||||
/* 18 / 23C8 / 3DF8 */ le_uint32_t v1_replacement_table; // -> [uint8_t](0x89)
|
||||
/* 1C / 1DB0 / 2E4C */ le_uint32_t photon_color_table; // -> [0x24-byte structs](0x20)
|
||||
/* 20 / 2080 / 32CC */ le_uint32_t weapon_range_table; // -> ???
|
||||
/* 24 / 23F0 / 3E84 */ le_uint32_t weapon_sale_divisor_table; // -> [float](0x89)
|
||||
/* 28 / 248C / 40A8 */ le_uint32_t sale_divisor_table; // -> NonWeaponSaleDivisors
|
||||
/* 2C / 3420 / 5F4C */ le_uint32_t mag_feed_table; // -> MagFeedResultsTable
|
||||
/* 30 / 275C / 4378 */ le_uint32_t star_value_table; // -> [uint8_t](0x1C7)
|
||||
/* 34 / 28A2 / 45E4 */ le_uint32_t unknown_a1;
|
||||
/* 38 / 2C12 / 4540 */ le_uint32_t special_data_table; // -> [Special](0x29)
|
||||
/* 3C / 2CB8 / 58DC */ le_uint32_t stat_boost_table; // -> [StatBoost]
|
||||
/* 40 / 3198 / 5704 */ le_uint32_t shield_effect_table; // -> [8-byte structs]
|
||||
} __attribute__((packed));
|
||||
|
||||
struct TableOffsetsGCNTE {
|
||||
/* 00 / 6F0C */ be_uint32_t weapon_table; // -> [{count, offset -> [WeaponV3/WeaponV4]}](0xED)
|
||||
/* 04 / 6E4C */ be_uint32_t armor_table; // -> [{count, offset -> [ArmorOrShieldV3/ArmorOrShieldV4]}](2; armors and shields)
|
||||
/* 08 / 6E5C */ be_uint32_t unit_table; // -> {count, offset -> [UnitV3/UnitV4]} (last if out of range)
|
||||
/* 0C / 6E6C */ be_uint32_t tool_table; // -> [{count, offset -> [ToolV3/ToolV4]}](0x1A) (last if out of range)
|
||||
/* 10 / 6E64 */ be_uint32_t mag_table; // -> {count, offset -> [MagV3/MagV4]}
|
||||
/* 14 / 47BC */ be_uint32_t v1_replacement_table; // -> [uint8_t](0xED)
|
||||
/* 18 / 37A4 */ be_uint32_t photon_color_table; // -> [0x24-byte structs](0x20)
|
||||
/* 1C / 3A74 */ be_uint32_t weapon_range_table; // -> ???
|
||||
/* 20 / 484C */ be_uint32_t weapon_sale_divisor_table; // -> [float](0xED)
|
||||
/* 24 / 4A80 */ be_uint32_t sale_divisor_table; // -> NonWeaponSaleDivisors
|
||||
/* 28 / 7384 */ be_uint32_t mag_feed_table; // -> MagFeedResultsTable
|
||||
/* 2C / 4D50 */ be_uint32_t star_value_table; // -> [uint8_t](0x330) (indexed by .id from weapon, armor, etc.)
|
||||
/* 30 / 4F72 */ be_uint32_t special_data_table; // -> [Special]
|
||||
/* 34 / 5018 */ be_uint32_t weapon_effect_table; // -> [16-byte structs]
|
||||
/* 38 / 68B8 */ be_uint32_t stat_boost_table; // -> [StatBoost]
|
||||
/* 3C / 61B8 */ be_uint32_t shield_effect_table; // -> [8-byte structs]
|
||||
/* 40 / 69D8 */ be_uint32_t max_tech_level_table; // -> MaxTechniqueLevels
|
||||
/* 44 / 737C */ be_uint32_t combination_table; // -> {count, offset -> [ItemCombination]}
|
||||
/* 48 / 68B0 */ be_uint32_t unknown_a1;
|
||||
/* 4C / 6B1C */ be_uint32_t tech_boost_table; // -> [TechniqueBoost] (always 0x2C of them? from counts struct?)
|
||||
} __attribute__((packed));
|
||||
|
||||
template <bool IsBigEndian>
|
||||
@@ -397,10 +570,14 @@ private:
|
||||
/* 58 / F600 / 15024 */ U32T ranged_special_table; // -> {count, offset -> [4-byte structs]}
|
||||
} __attribute__((packed));
|
||||
|
||||
Version version;
|
||||
std::shared_ptr<const std::string> data;
|
||||
StringReader r;
|
||||
const TableOffsetsV2* offsets_v2;
|
||||
const TableOffsetsV3V4<true>* offsets_v3;
|
||||
const TableOffsetsDCProtos* offsets_dc_protos;
|
||||
const TableOffsetsV1V2* offsets_v1_v2;
|
||||
const TableOffsetsGCNTE* offsets_gc_nte;
|
||||
const TableOffsetsV3V4<false>* offsets_v3_le;
|
||||
const TableOffsetsV3V4<true>* offsets_v3_be;
|
||||
const TableOffsetsV3V4<false>* offsets_v4;
|
||||
|
||||
// These are unused if offsets_v4 is not null (in that case, we just return
|
||||
@@ -419,8 +596,10 @@ private:
|
||||
|
||||
template <typename ToolT, bool IsBigEndian>
|
||||
std::pair<uint8_t, uint8_t> find_tool_by_id_t(uint32_t tool_table_offset, uint32_t id) const;
|
||||
template <bool IsBigEndian, typename OffsetsT>
|
||||
float get_sale_divisor_t(const OffsetsT* offsets, uint8_t data1_0, uint8_t data1_1) const;
|
||||
template <bool IsBigEndian>
|
||||
float get_sale_divisor_t(uint32_t weapon_table_offset, uint32_t non_weapon_table_offset, uint8_t data1_0, uint8_t data1_1) const;
|
||||
size_t num_events_t(uint32_t base_offset) const;
|
||||
template <bool IsBigEndian>
|
||||
std::pair<const ItemParameterTable::EventItem*, size_t> get_event_items_t(uint32_t base_offset, uint8_t event_number) const;
|
||||
};
|
||||
|
||||
+119
-106
@@ -2,13 +2,11 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Random.hh>
|
||||
|
||||
#include "SendCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGEncryption> random_crypt) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
// On PC (and presumably DC), the client sends a 6x29 after this to delete the
|
||||
@@ -19,20 +17,20 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
|
||||
auto player = c->character();
|
||||
auto& item = player->inventory.items[item_index];
|
||||
uint32_t item_identifier = item.data.primary_identifier();
|
||||
uint32_t primary_identifier = item.data.primary_identifier();
|
||||
|
||||
if (item.data.is_common_consumable()) { // Monomate, etc.
|
||||
// Nothing to do (it should be deleted)
|
||||
|
||||
} else if (item_identifier == 0x030200) { // Technique disk
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x03020000) { // Technique disk
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
uint8_t max_level = item_parameter_table->get_max_tech_level(player->disp.visual.char_class, item.data.data1[4]);
|
||||
if (item.data.data1[2] > max_level) {
|
||||
throw runtime_error("technique level too high");
|
||||
}
|
||||
player->set_technique_level(item.data.data1[4], item.data.data1[2]);
|
||||
|
||||
} else if ((item_identifier & 0xFFFF00) == 0x030A00) { // Grinder
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x030A0000) { // Grinder
|
||||
if (item.data.data1[2] > 2) {
|
||||
throw runtime_error("incorrect grinder value");
|
||||
}
|
||||
@@ -45,7 +43,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
// get an accurate picture of what's actually in the player's inventory, so
|
||||
// there's no way to know if we would be enforcing the correct grind limit.
|
||||
if (is_v3_or_later) {
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
auto weapon_def = item_parameter_table->get_weapon(weapon.data.data1[1], weapon.data.data1[2]);
|
||||
if (weapon.data.data1[3] >= weapon_def.max_grind) {
|
||||
throw runtime_error("weapon already at maximum grind");
|
||||
@@ -53,7 +51,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
}
|
||||
weapon.data.data1[3] += (item.data.data1[2] + 1);
|
||||
|
||||
} else if ((item_identifier & 0xFFFF00) == 0x030B00) { // Material
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x030B0000) { // Material
|
||||
auto p = c->character();
|
||||
|
||||
using Type = PSOBBCharacterFile::MaterialType;
|
||||
@@ -106,71 +104,71 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
p->set_material_usage(type, p->get_material_usage(type) + 1);
|
||||
}
|
||||
|
||||
} else if ((item_identifier & 0xFFFF00) == 0x030F00) { // AddSlot
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x030F0000) { // AddSlot
|
||||
auto& armor = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::ARMOR)];
|
||||
if (armor.data.data1[5] >= 4) {
|
||||
throw runtime_error("armor already at maximum slot count");
|
||||
}
|
||||
armor.data.data1[5]++;
|
||||
|
||||
} else if (item.data.is_wrapped()) {
|
||||
} else if (item.data.is_wrapped(c->version())) {
|
||||
// Unwrap present
|
||||
item.data.unwrap();
|
||||
item.data.unwrap(c->version());
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x003300) {
|
||||
} else if (primary_identifier == 0x00330000) {
|
||||
// Unseal Sealed J-Sword => Tsumikiri J-Sword
|
||||
item.data.data1[1] = 0x32;
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x00AB00) {
|
||||
} else if (primary_identifier == 0x00AB0000) {
|
||||
// Unseal Lame d'Argent => Excalibur
|
||||
item.data.data1[1] = 0xAC;
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x01034D) {
|
||||
} else if (primary_identifier == 0x01034D00) {
|
||||
// Unseal Limiter => Adept
|
||||
item.data.data1[2] = 0x4E;
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x01034F) {
|
||||
} else if (primary_identifier == 0x01034F00) {
|
||||
// Unseal Swordsman Lore => Proof of Sword-Saint
|
||||
item.data.data1[2] = 0x50;
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x030C00) {
|
||||
} else if (primary_identifier == 0x030C0000) {
|
||||
// Cell of MAG 502
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x1D : 0x21;
|
||||
|
||||
} else if (item_identifier == 0x030C01) {
|
||||
} else if (primary_identifier == 0x030C0100) {
|
||||
// Cell of MAG 213
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x27 : 0x22;
|
||||
|
||||
} else if (item_identifier == 0x030C02) {
|
||||
} else if (primary_identifier == 0x030C0200) {
|
||||
// Parts of RoboChao
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = 0x28;
|
||||
|
||||
} else if (item_identifier == 0x030C03) {
|
||||
} else if (primary_identifier == 0x030C0300) {
|
||||
// Heart of Opa Opa
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = 0x29;
|
||||
|
||||
} else if (item_identifier == 0x030C04) {
|
||||
} else if (primary_identifier == 0x030C0400) {
|
||||
// Heart of Pian
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = 0x2A;
|
||||
|
||||
} else if (item_identifier == 0x030C05) {
|
||||
} else if (primary_identifier == 0x030C0500) {
|
||||
// Heart of Chao
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = 0x2B;
|
||||
|
||||
} else if ((item_identifier & 0xFFFF00) == 0x031500) {
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x03150000) {
|
||||
// Christmas Present, etc. - use unwrap_table + probabilities therein
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
auto table = item_parameter_table->get_event_items(item.data.data1[2]);
|
||||
size_t sum = 0;
|
||||
for (size_t z = 0; z < table.second; z++) {
|
||||
@@ -179,7 +177,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
if (sum == 0) {
|
||||
throw runtime_error("no unwrap results available for event");
|
||||
}
|
||||
size_t det = random_object<size_t>() % sum;
|
||||
size_t det = random_crypt->next() % sum;
|
||||
for (size_t z = 0; z < table.second; z++) {
|
||||
const auto& entry = table.first[z];
|
||||
if (det > entry.probability) {
|
||||
@@ -209,7 +207,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
const auto& combo = item_parameter_table->get_item_combination(item.data, inv_item.data);
|
||||
if (combo.char_class != 0xFF && combo.char_class != player->disp.visual.char_class) {
|
||||
throw runtime_error("item combination requires specific char_class");
|
||||
@@ -252,33 +250,35 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
if (should_delete_item) {
|
||||
// Allow overdrafting meseta if the client is not BB, since the server isn't
|
||||
// informed when meseta is added or removed from the bank.
|
||||
player->remove_item(item.data.id, 1, !is_v4(c->version()));
|
||||
player->remove_item(item.data.id, 1, c->version());
|
||||
}
|
||||
}
|
||||
|
||||
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index) {
|
||||
void apply_mag_feed_result(
|
||||
ItemData& mag_item,
|
||||
const ItemData& fed_item,
|
||||
shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
shared_ptr<const MagEvolutionTable> mag_evolution_table,
|
||||
uint8_t char_class,
|
||||
uint8_t section_id,
|
||||
bool version_has_rare_mags) {
|
||||
|
||||
static const unordered_map<uint32_t, size_t> result_index_for_fed_item({
|
||||
{0x030000, 0}, // Monomate
|
||||
{0x030001, 1}, // Dimate
|
||||
{0x030002, 2}, // Trimate
|
||||
{0x030100, 3}, // Monofluid
|
||||
{0x030101, 4}, // Difluid
|
||||
{0x030102, 5}, // Trifluid
|
||||
{0x030600, 6}, // Antidote
|
||||
{0x030601, 7}, // Antiparalysis
|
||||
{0x030300, 8}, // Sol Atomizer
|
||||
{0x030400, 9}, // Moon Atomizer
|
||||
{0x030500, 10}, // Star Atomizer
|
||||
{0x03000000, 0}, // Monomate
|
||||
{0x03000100, 1}, // Dimate
|
||||
{0x03000200, 2}, // Trimate
|
||||
{0x03010000, 3}, // Monofluid
|
||||
{0x03010100, 4}, // Difluid
|
||||
{0x03010200, 5}, // Trifluid
|
||||
{0x03060000, 6}, // Antidote
|
||||
{0x03060100, 7}, // Antiparalysis
|
||||
{0x03030000, 8}, // Sol Atomizer
|
||||
{0x03040000, 9}, // Moon Atomizer
|
||||
{0x03050000, 10}, // Star Atomizer
|
||||
});
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto player = c->character();
|
||||
auto& fed_item = player->inventory.items[fed_item_index];
|
||||
auto& mag_item = player->inventory.items[mag_item_index];
|
||||
|
||||
size_t result_index = result_index_for_fed_item.at(fed_item.data.primary_identifier());
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
const auto& mag_def = item_parameter_table->get_mag(mag_item.data.data1[1]);
|
||||
size_t result_index = result_index_for_fed_item.at(fed_item.primary_identifier());
|
||||
const auto& mag_def = item_parameter_table->get_mag(mag_item.data1[1]);
|
||||
const auto& feed_result = item_parameter_table->get_mag_feed_result(mag_def.feed_table, result_index);
|
||||
|
||||
auto update_stat = +[](ItemData& data, size_t which, int8_t delta) -> void {
|
||||
@@ -295,17 +295,17 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
}
|
||||
};
|
||||
|
||||
update_stat(mag_item.data, 2, feed_result.def);
|
||||
update_stat(mag_item.data, 3, feed_result.pow);
|
||||
update_stat(mag_item.data, 4, feed_result.dex);
|
||||
update_stat(mag_item.data, 5, feed_result.mind);
|
||||
mag_item.data.data2[0] = clamp<ssize_t>(static_cast<ssize_t>(mag_item.data.data2[0]) + feed_result.synchro, 0, 120);
|
||||
mag_item.data.data2[1] = clamp<ssize_t>(static_cast<ssize_t>(mag_item.data.data2[1]) + feed_result.iq, 0, 200);
|
||||
update_stat(mag_item, 2, feed_result.def);
|
||||
update_stat(mag_item, 3, feed_result.pow);
|
||||
update_stat(mag_item, 4, feed_result.dex);
|
||||
update_stat(mag_item, 5, feed_result.mind);
|
||||
mag_item.data2[0] = clamp<ssize_t>(static_cast<ssize_t>(mag_item.data2[0]) + feed_result.synchro, 0, 120);
|
||||
mag_item.data2[1] = clamp<ssize_t>(static_cast<ssize_t>(mag_item.data2[1]) + feed_result.iq, 0, 200);
|
||||
|
||||
uint8_t mag_level = mag_item.data.compute_mag_level();
|
||||
mag_item.data.data1[2] = mag_level;
|
||||
uint8_t evolution_number = s->mag_evolution_table->get_evolution_number(mag_item.data.data1[1]);
|
||||
uint8_t mag_number = mag_item.data.data1[1];
|
||||
uint8_t mag_level = mag_item.compute_mag_level();
|
||||
mag_item.data1[2] = mag_level;
|
||||
uint8_t evolution_number = mag_evolution_table->get_evolution_number(mag_item.data1[1]);
|
||||
uint8_t mag_number = mag_item.data1[1];
|
||||
|
||||
// Note: Sega really did just hardcode all these rules into the client. There
|
||||
// is no data file describing these evolutions, unfortunately.
|
||||
@@ -315,24 +315,24 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
|
||||
} else if (mag_level < 35) { // Level 10 evolution
|
||||
if (evolution_number < 1) {
|
||||
switch (player->disp.visual.char_class) {
|
||||
switch (char_class) {
|
||||
case 0: // HUmar
|
||||
case 1: // HUnewearl
|
||||
case 2: // HUcast
|
||||
case 9: // HUcaseal
|
||||
mag_item.data.data1[1] = 0x01; // Varuna
|
||||
mag_item.data1[1] = 0x01; // Varuna
|
||||
break;
|
||||
case 3: // RAmar
|
||||
case 11: // RAmarl
|
||||
case 4: // RAcast
|
||||
case 5: // RAcaseal
|
||||
mag_item.data.data1[1] = 0x0D; // Kalki
|
||||
mag_item.data1[1] = 0x0D; // Kalki
|
||||
break;
|
||||
case 10: // FOmar
|
||||
case 6: // FOmarl
|
||||
case 7: // FOnewm
|
||||
case 8: // FOnewearl
|
||||
mag_item.data.data1[1] = 0x19; // Vritra
|
||||
mag_item.data1[1] = 0x19; // Vritra
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("invalid character class");
|
||||
@@ -341,30 +341,30 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
|
||||
} else if (mag_level < 50) { // Level 35 evolution
|
||||
if (evolution_number < 2) {
|
||||
uint16_t flags = mag_item.data.compute_mag_strength_flags();
|
||||
uint16_t flags = mag_item.compute_mag_strength_flags();
|
||||
if (mag_number == 0x0D) {
|
||||
if ((flags & 0x110) == 0) {
|
||||
mag_item.data.data1[1] = 0x02;
|
||||
mag_item.data1[1] = 0x02;
|
||||
} else if (flags & 8) {
|
||||
mag_item.data.data1[1] = 0x03;
|
||||
mag_item.data1[1] = 0x03;
|
||||
} else if (flags & 0x20) {
|
||||
mag_item.data.data1[1] = 0x0B;
|
||||
mag_item.data1[1] = 0x0B;
|
||||
}
|
||||
} else if (mag_number == 1) {
|
||||
if (flags & 0x108) {
|
||||
mag_item.data.data1[1] = 0x0E;
|
||||
mag_item.data1[1] = 0x0E;
|
||||
} else if (flags & 0x10) {
|
||||
mag_item.data.data1[1] = 0x0F;
|
||||
mag_item.data1[1] = 0x0F;
|
||||
} else if (flags & 0x20) {
|
||||
mag_item.data.data1[1] = 0x04;
|
||||
mag_item.data1[1] = 0x04;
|
||||
}
|
||||
} else if (mag_number == 0x19) {
|
||||
if (flags & 0x120) {
|
||||
mag_item.data.data1[1] = 0x1A;
|
||||
mag_item.data1[1] = 0x1A;
|
||||
} else if (flags & 8) {
|
||||
mag_item.data.data1[1] = 0x1B;
|
||||
mag_item.data1[1] = 0x1B;
|
||||
} else if (flags & 0x10) {
|
||||
mag_item.data.data1[1] = 0x14;
|
||||
mag_item.data1[1] = 0x14;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,18 +372,18 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
} else if ((mag_level % 5) == 0) { // Level 50 (and beyond) evolutions
|
||||
if (evolution_number < 4) {
|
||||
|
||||
if ((mag_level >= 100) && !is_v1_or_v2(c->version())) {
|
||||
uint8_t section_id_group = player->disp.visual.section_id % 3;
|
||||
uint16_t def = mag_item.data.data1w[2] / 100;
|
||||
uint16_t pow = mag_item.data.data1w[3] / 100;
|
||||
uint16_t dex = mag_item.data.data1w[4] / 100;
|
||||
uint16_t mind = mag_item.data.data1w[5] / 100;
|
||||
bool is_male = char_class_is_male(player->disp.visual.char_class);
|
||||
if ((mag_level >= 100) && version_has_rare_mags) {
|
||||
uint8_t section_id_group = section_id % 3;
|
||||
uint16_t def = mag_item.data1w[2] / 100;
|
||||
uint16_t pow = mag_item.data1w[3] / 100;
|
||||
uint16_t dex = mag_item.data1w[4] / 100;
|
||||
uint16_t mind = mag_item.data1w[5] / 100;
|
||||
bool is_male = char_class_is_male(char_class);
|
||||
size_t table_index = (is_male ? 0 : 1) + section_id_group * 2;
|
||||
|
||||
bool is_hunter = char_class_is_hunter(player->disp.visual.char_class);
|
||||
bool is_ranger = char_class_is_ranger(player->disp.visual.char_class);
|
||||
bool is_force = char_class_is_force(player->disp.visual.char_class);
|
||||
bool is_hunter = char_class_is_hunter(char_class);
|
||||
bool is_ranger = char_class_is_ranger(char_class);
|
||||
bool is_force = char_class_is_force(char_class);
|
||||
if (is_force) {
|
||||
table_index += 12;
|
||||
} else if (is_ranger) {
|
||||
@@ -406,77 +406,77 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
0x41, 0x3F, 0x41, 0x40, 0x41, 0x40, // Force
|
||||
};
|
||||
// clang-format on
|
||||
mag_item.data.data1[1] = result_table[table_index];
|
||||
mag_item.data1[1] = result_table[table_index];
|
||||
}
|
||||
}
|
||||
|
||||
// If a special evolution did not occur, do a normal level 50 evolution
|
||||
if (mag_number == mag_item.data.data1[1]) {
|
||||
uint16_t flags = mag_item.data.compute_mag_strength_flags();
|
||||
uint16_t def = mag_item.data.data1w[2] / 100;
|
||||
uint16_t pow = mag_item.data.data1w[3] / 100;
|
||||
uint16_t dex = mag_item.data.data1w[4] / 100;
|
||||
uint16_t mind = mag_item.data.data1w[5] / 100;
|
||||
if (mag_number == mag_item.data1[1]) {
|
||||
uint16_t flags = mag_item.compute_mag_strength_flags();
|
||||
uint16_t def = mag_item.data1w[2] / 100;
|
||||
uint16_t pow = mag_item.data1w[3] / 100;
|
||||
uint16_t dex = mag_item.data1w[4] / 100;
|
||||
uint16_t mind = mag_item.data1w[5] / 100;
|
||||
|
||||
bool is_hunter = char_class_is_hunter(player->disp.visual.char_class);
|
||||
bool is_ranger = char_class_is_ranger(player->disp.visual.char_class);
|
||||
bool is_force = char_class_is_force(player->disp.visual.char_class);
|
||||
bool is_hunter = char_class_is_hunter(char_class);
|
||||
bool is_ranger = char_class_is_ranger(char_class);
|
||||
bool is_force = char_class_is_force(char_class);
|
||||
if (is_hunter + is_ranger + is_force != 1) {
|
||||
throw logic_error("char class is not exactly one of the top-level classes");
|
||||
}
|
||||
|
||||
if (is_hunter) {
|
||||
if (flags & 0x108) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((dex < mind) ? 0x08 : 0x06)
|
||||
: ((dex < mind) ? 0x0C : 0x05);
|
||||
} else if (flags & 0x010) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((mind < pow) ? 0x12 : 0x10)
|
||||
: ((mind < pow) ? 0x17 : 0x13);
|
||||
} else if (flags & 0x020) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((pow < dex) ? 0x16 : 0x24)
|
||||
: ((pow < dex) ? 0x07 : 0x1E);
|
||||
}
|
||||
} else if (is_ranger) {
|
||||
if (flags & 0x110) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((mind < pow) ? 0x0A : 0x05)
|
||||
: ((mind < pow) ? 0x0C : 0x06);
|
||||
} else if (flags & 0x008) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((dex < mind) ? 0x0A : 0x26)
|
||||
: ((dex < mind) ? 0x0C : 0x06);
|
||||
} else if (flags & 0x020) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((pow < dex) ? 0x18 : 0x1E)
|
||||
: ((pow < dex) ? 0x08 : 0x05);
|
||||
}
|
||||
} else if (is_force) {
|
||||
if (flags & 0x120) {
|
||||
if (def < 45) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((pow < dex) ? 0x17 : 0x09)
|
||||
: ((pow < dex) ? 0x1E : 0x1C);
|
||||
} else {
|
||||
mag_item.data.data1[1] = 0x24;
|
||||
mag_item.data1[1] = 0x24;
|
||||
}
|
||||
} else if (flags & 0x008) {
|
||||
if (def < 45) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((dex < mind) ? 0x1C : 0x20)
|
||||
: ((dex < mind) ? 0x1F : 0x25);
|
||||
} else {
|
||||
mag_item.data.data1[1] = 0x23;
|
||||
mag_item.data1[1] = 0x23;
|
||||
}
|
||||
} else if (flags & 0x010) {
|
||||
if (def < 45) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((mind < pow) ? 0x12 : 0x0C)
|
||||
: ((mind < pow) ? 0x15 : 0x11);
|
||||
} else {
|
||||
mag_item.data.data1[1] = 0x24;
|
||||
mag_item.data1[1] = 0x24;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,8 +485,21 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
}
|
||||
|
||||
// If the mag has evolved, add its new photon blast
|
||||
if (mag_number != mag_item.data.data1[1]) {
|
||||
const auto& new_mag_def = item_parameter_table->get_mag(mag_item.data.data1[1]);
|
||||
mag_item.data.add_mag_photon_blast(new_mag_def.photon_blast);
|
||||
if (mag_number != mag_item.data1[1]) {
|
||||
const auto& new_mag_def = item_parameter_table->get_mag(mag_item.data1[1]);
|
||||
mag_item.add_mag_photon_blast(new_mag_def.photon_blast);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
apply_mag_feed_result(
|
||||
player->inventory.items[mag_item_index].data,
|
||||
player->inventory.items[fed_item_index].data,
|
||||
s->item_parameter_table(c->version()),
|
||||
s->mag_evolution_table,
|
||||
player->disp.visual.char_class,
|
||||
player->disp.visual.section_id,
|
||||
!is_v1_or_v2(c->version()));
|
||||
}
|
||||
|
||||
+13
-1
@@ -6,8 +6,20 @@
|
||||
#include <random>
|
||||
|
||||
#include "Client.hh"
|
||||
#include "ItemData.hh"
|
||||
#include "ItemParameterTable.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "ServerState.hh"
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
void player_use_item(std::shared_ptr<Client> c, size_t item_index);
|
||||
void player_use_item(std::shared_ptr<Client> c, size_t item_index, std::shared_ptr<PSOLFGEncryption> random_crypt);
|
||||
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index);
|
||||
|
||||
void apply_mag_feed_result(
|
||||
ItemData& mag_item,
|
||||
const ItemData& fed_item,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table,
|
||||
uint8_t char_class,
|
||||
uint8_t section_id,
|
||||
bool version_has_rare_mags);
|
||||
|
||||
+145
-28
@@ -5,6 +5,7 @@
|
||||
#include <phosg/Filesystem.hh>
|
||||
|
||||
#include "Compression.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -31,43 +32,159 @@ void PlayerStats::advance_to_level(uint8_t char_class, uint32_t level, shared_pt
|
||||
}
|
||||
}
|
||||
|
||||
LevelTable::LevelTable(shared_ptr<const string> data, bool compressed) {
|
||||
LevelTableV2::LevelTableV2(const string& data, bool compressed) {
|
||||
struct Offsets {
|
||||
// TODO: The overall format of this file on V2 has much more data than we
|
||||
// actually use. What's known of the structure so far:
|
||||
le_uint32_t level_deltas; // -> u32[9] -> LevelStatsDelta[200]
|
||||
le_uint32_t unknown_a1; // -> float[6]
|
||||
le_uint32_t max_stats; // -> PlayerStats[9]
|
||||
le_uint32_t level_100_stats; // -> Level100Entry[9]
|
||||
le_uint32_t base_stats; // -> u32[9] -> CharacterStats
|
||||
le_uint32_t unknown_a2; // -> (0x120 zero bytes)
|
||||
le_uint32_t attack_data; // -> AttackData[9]
|
||||
le_uint32_t unknown_a4; // -> (0x14-byte struct)[9]
|
||||
le_uint32_t unknown_a5; // -> float[9]
|
||||
le_uint32_t unknown_a6; // -> (0x30 bytes)
|
||||
le_uint32_t unknown_a7; // -> (0x2D bytes)
|
||||
le_uint32_t unknown_a8; // -> u32[3] -> float[0x2D]
|
||||
le_uint32_t unknown_a9; // -> (0x90 bytes)
|
||||
le_uint32_t unknown_a10; // -> u32[3] -> (0x10-byte struct)[0x0C]
|
||||
le_uint32_t unknown_a11; // -> u32[3] -> (0x30-bytes)
|
||||
le_uint32_t unknown_a12; // -> u32[3] -> (0x14-byte struct)[0x0F]
|
||||
} __attribute__((packed));
|
||||
|
||||
StringReader r;
|
||||
string decompressed_data;
|
||||
if (compressed) {
|
||||
this->data = make_shared<string>(prs_decompress(*data));
|
||||
decompressed_data = prs_decompress(data);
|
||||
r = StringReader(decompressed_data);
|
||||
} else {
|
||||
this->data = data;
|
||||
r = StringReader(data);
|
||||
}
|
||||
|
||||
if (this->data->size() < sizeof(Table)) {
|
||||
throw invalid_argument("level table size is incorrect");
|
||||
const auto& offsets = r.pget<Offsets>(r.pget_u32l(r.size() - 0x10));
|
||||
const auto& level_deltas_offsets = r.pget<parray<le_uint32_t, 9>>(offsets.level_deltas);
|
||||
const auto& base_stats_offsets = r.pget<parray<le_uint32_t, 9>>(offsets.base_stats);
|
||||
for (size_t char_class = 0; char_class < 9; char_class++) {
|
||||
const auto& src_level_deltas = r.pget<parray<LevelStatsDelta, 200>>(level_deltas_offsets[char_class]);
|
||||
for (size_t level = 0; level < 200; level++) {
|
||||
this->level_deltas[char_class][level] = src_level_deltas[level];
|
||||
}
|
||||
this->max_stats[char_class] = r.pget<PlayerStats>(offsets.max_stats + char_class * sizeof(PlayerStats));
|
||||
this->level_100_stats[char_class] = r.pget<Level100Entry>(offsets.level_100_stats + char_class * sizeof(Level100Entry));
|
||||
this->base_stats[char_class] = r.pget<CharacterStats>(base_stats_offsets[char_class]);
|
||||
}
|
||||
this->table = reinterpret_cast<const Table*>(this->data->data());
|
||||
}
|
||||
|
||||
const CharacterStats& LevelTable::base_stats_for_class(uint8_t char_class) const {
|
||||
if (char_class >= 12) {
|
||||
throw out_of_range("invalid character class");
|
||||
}
|
||||
return this->table->base_stats[char_class];
|
||||
const CharacterStats& LevelTableV2::base_stats_for_class(uint8_t char_class) const {
|
||||
return this->base_stats.at(char_class);
|
||||
}
|
||||
|
||||
const LevelTable::LevelStats& LevelTable::stats_delta_for_level(
|
||||
uint8_t char_class, uint8_t level) const {
|
||||
if (char_class >= 12) {
|
||||
throw invalid_argument("invalid character class");
|
||||
}
|
||||
if (level >= 200) {
|
||||
throw invalid_argument("invalid character level");
|
||||
}
|
||||
return this->table->levels[char_class][level];
|
||||
const LevelTableV2::Level100Entry& LevelTableV2::level_100_stats_for_class(uint8_t char_class) const {
|
||||
return this->level_100_stats.at(char_class);
|
||||
}
|
||||
|
||||
void LevelTable::LevelStats::apply(CharacterStats& ps) const {
|
||||
ps.ata += this->ata;
|
||||
ps.atp += this->atp;
|
||||
ps.dfp += this->dfp;
|
||||
ps.evp += this->evp;
|
||||
ps.hp += this->hp;
|
||||
ps.mst += this->mst;
|
||||
ps.lck += this->lck;
|
||||
const PlayerStats& LevelTableV2::max_stats_for_class(uint8_t char_class) const {
|
||||
return this->max_stats.at(char_class);
|
||||
}
|
||||
|
||||
const LevelStatsDelta& LevelTableV2::stats_delta_for_level(uint8_t char_class, uint8_t level) const {
|
||||
return this->level_deltas.at(char_class).at(level);
|
||||
}
|
||||
|
||||
LevelTableV3BE::LevelTableV3BE(const string& data, bool encrypted) {
|
||||
StringReader r;
|
||||
string decompressed_data;
|
||||
if (encrypted) {
|
||||
auto decrypted = decrypt_pr2_data<true>(data);
|
||||
decompressed_data = prs_decompress(decrypted.compressed_data);
|
||||
if (decompressed_data.size() != decrypted.decompressed_size) {
|
||||
throw runtime_error("decompressed data size does not match expected size");
|
||||
}
|
||||
r = StringReader(decompressed_data);
|
||||
} else {
|
||||
r = StringReader(data);
|
||||
}
|
||||
|
||||
// The GC format is very simple (but everything is big-endian):
|
||||
// root:
|
||||
// u32 offset:
|
||||
// u32[12] offsets:
|
||||
// LevelStatsDeltaBE[200] level_deltas
|
||||
const auto& offsets = r.pget<parray<be_uint32_t, 12>>(r.pget_u32b(r.pget_u32b(r.size() - 0x10)));
|
||||
for (size_t char_class = 0; char_class < 12; char_class++) {
|
||||
const auto& src_deltas = r.pget<parray<LevelStatsDeltaBE, 200>>(offsets[char_class]);
|
||||
for (size_t level = 0; level < 200; level++) {
|
||||
const auto& src_delta = src_deltas[level];
|
||||
auto& dest_delta = this->level_deltas[char_class][level];
|
||||
dest_delta.atp = src_delta.atp;
|
||||
dest_delta.mst = src_delta.mst;
|
||||
dest_delta.evp = src_delta.evp;
|
||||
dest_delta.hp = src_delta.hp;
|
||||
dest_delta.dfp = src_delta.dfp;
|
||||
dest_delta.ata = src_delta.ata;
|
||||
dest_delta.lck = src_delta.lck;
|
||||
dest_delta.tp = src_delta.tp;
|
||||
dest_delta.experience = src_delta.experience.load();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CharacterStats& LevelTableV3BE::base_stats_for_class(uint8_t char_class) const {
|
||||
static const array<CharacterStats, 12> data = {
|
||||
// ATP MST EVP HP DFP ATA LCK
|
||||
CharacterStats{0x0023, 0x001D, 0x002D, 0x0014, 0x0011, 0x001E, 0x000A},
|
||||
CharacterStats{0x001E, 0x0028, 0x003C, 0x0013, 0x0016, 0x0019, 0x000A},
|
||||
CharacterStats{0x0023, 0x0000, 0x0023, 0x0016, 0x0012, 0x0023, 0x000A},
|
||||
CharacterStats{0x0012, 0x0014, 0x0024, 0x0010, 0x000D, 0x0028, 0x000A},
|
||||
CharacterStats{0x0019, 0x0000, 0x001F, 0x0012, 0x0012, 0x002D, 0x000A},
|
||||
CharacterStats{0x0014, 0x0000, 0x001F, 0x0011, 0x0017, 0x002D, 0x000A},
|
||||
CharacterStats{0x000D, 0x0035, 0x0023, 0x0014, 0x000A, 0x000F, 0x000A},
|
||||
CharacterStats{0x000D, 0x003C, 0x0032, 0x0013, 0x0007, 0x000C, 0x000A},
|
||||
CharacterStats{0x000A, 0x003A, 0x0035, 0x0013, 0x000D, 0x000A, 0x000A},
|
||||
CharacterStats{0x0023, 0x0000, 0x0023, 0x0016, 0x0012, 0x0023, 0x000A},
|
||||
CharacterStats{0x000D, 0x0035, 0x0023, 0x0014, 0x000A, 0x000F, 0x000A},
|
||||
CharacterStats{0x0012, 0x0014, 0x0024, 0x0010, 0x000D, 0x0028, 0x000A},
|
||||
};
|
||||
return data.at(char_class);
|
||||
}
|
||||
|
||||
const LevelStatsDelta& LevelTableV3BE::stats_delta_for_level(uint8_t char_class, uint8_t level) const {
|
||||
return this->level_deltas.at(char_class).at(level);
|
||||
}
|
||||
|
||||
LevelTableV4::LevelTableV4(const string& data, bool compressed) {
|
||||
struct Offsets {
|
||||
le_uint32_t base_stats; // -> u32[12] -> CharacterStats
|
||||
le_uint32_t level_deltas; // -> u32[12] -> LevelStatsDelta[200]
|
||||
} __attribute__((packed));
|
||||
|
||||
StringReader r;
|
||||
string decompressed_data;
|
||||
if (compressed) {
|
||||
decompressed_data = prs_decompress(data);
|
||||
r = StringReader(decompressed_data);
|
||||
} else {
|
||||
r = StringReader(data);
|
||||
}
|
||||
|
||||
const auto& offsets = r.pget<Offsets>(r.pget_u32l(r.size() - 0x10));
|
||||
const auto& level_deltas_offsets = r.pget<parray<le_uint32_t, 12>>(offsets.level_deltas);
|
||||
const auto& base_stats_offsets = r.pget<parray<le_uint32_t, 12>>(offsets.base_stats);
|
||||
for (size_t char_class = 0; char_class < 12; char_class++) {
|
||||
const auto& src_level_deltas = r.pget<parray<LevelStatsDelta, 200>>(level_deltas_offsets[char_class]);
|
||||
for (size_t level = 0; level < 200; level++) {
|
||||
this->level_deltas[char_class][level] = src_level_deltas[level];
|
||||
}
|
||||
this->base_stats[char_class] = r.pget<CharacterStats>(base_stats_offsets[char_class]);
|
||||
}
|
||||
}
|
||||
|
||||
const CharacterStats& LevelTableV4::base_stats_for_class(uint8_t char_class) const {
|
||||
return this->base_stats.at(char_class);
|
||||
}
|
||||
|
||||
const LevelStatsDelta& LevelTableV4::stats_delta_for_level(uint8_t char_class, uint8_t level) const {
|
||||
return this->level_deltas.at(char_class).at(level);
|
||||
}
|
||||
|
||||
+97
-62
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <string>
|
||||
@@ -9,13 +10,14 @@
|
||||
class LevelTable;
|
||||
|
||||
struct CharacterStats {
|
||||
le_uint16_t atp = 0;
|
||||
le_uint16_t mst = 0;
|
||||
le_uint16_t evp = 0;
|
||||
le_uint16_t hp = 0;
|
||||
le_uint16_t dfp = 0;
|
||||
le_uint16_t ata = 0;
|
||||
le_uint16_t lck = 0;
|
||||
/* 00 */ le_uint16_t atp = 0;
|
||||
/* 02 */ le_uint16_t mst = 0;
|
||||
/* 04 */ le_uint16_t evp = 0;
|
||||
/* 06 */ le_uint16_t hp = 0;
|
||||
/* 08 */ le_uint16_t dfp = 0;
|
||||
/* 0A */ le_uint16_t ata = 0;
|
||||
/* 0C */ le_uint16_t lck = 0;
|
||||
/* 0E */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerStats {
|
||||
@@ -32,66 +34,99 @@ struct PlayerStats {
|
||||
void advance_to_level(uint8_t char_class, uint32_t level, std::shared_ptr<const LevelTable> level_table);
|
||||
} __attribute__((packed));
|
||||
|
||||
class LevelTable { // from PlyLevelTbl.prs
|
||||
template <bool IsBigEndian>
|
||||
struct LevelStatsDeltaBase {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
/* 00 */ uint8_t atp;
|
||||
/* 01 */ uint8_t mst;
|
||||
/* 02 */ uint8_t evp;
|
||||
/* 03 */ uint8_t hp;
|
||||
/* 04 */ uint8_t dfp;
|
||||
/* 05 */ uint8_t ata;
|
||||
/* 06 */ uint8_t lck;
|
||||
/* 07 */ uint8_t tp;
|
||||
/* 08 */ U32T experience;
|
||||
/* 0C */
|
||||
|
||||
void apply(CharacterStats& ps) const {
|
||||
ps.ata += this->ata;
|
||||
ps.atp += this->atp;
|
||||
ps.dfp += this->dfp;
|
||||
ps.evp += this->evp;
|
||||
ps.hp += this->hp;
|
||||
ps.mst += this->mst;
|
||||
ps.lck += this->lck;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
|
||||
struct LevelStatsDelta : LevelStatsDeltaBase<false> {
|
||||
} __attribute__((packed));
|
||||
struct LevelStatsDeltaBE : LevelStatsDeltaBase<true> {
|
||||
} __attribute__((packed));
|
||||
|
||||
class LevelTable {
|
||||
// This is the base class for all the LevelTable implementations. The public
|
||||
// interface here only defines functions that the server needs to handle
|
||||
// requests, but some subclasses implement more functionality. See the
|
||||
// comments and Offsets structures inside the subclasses' constructor
|
||||
// implementations for more details on the file formats.
|
||||
public:
|
||||
struct LevelStats {
|
||||
uint8_t atp;
|
||||
uint8_t mst;
|
||||
uint8_t evp;
|
||||
uint8_t hp;
|
||||
uint8_t dfp;
|
||||
uint8_t ata;
|
||||
uint8_t lck;
|
||||
uint8_t tp;
|
||||
le_uint32_t experience;
|
||||
virtual ~LevelTable() = default;
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const = 0;
|
||||
virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const = 0;
|
||||
|
||||
void apply(CharacterStats& ps) const;
|
||||
protected:
|
||||
LevelTable() = default;
|
||||
};
|
||||
|
||||
class LevelTableV2 : public LevelTable { // from PlayerTable.prs (PC)
|
||||
public:
|
||||
struct Level100Entry {
|
||||
/* 00 */ CharacterStats char_stats;
|
||||
/* 0E */ le_uint16_t unknown_a1 = 0;
|
||||
/* 10 */ le_float height = 0.0;
|
||||
/* 14 */ le_float unknown_a3 = 0.0;
|
||||
/* 18 */ le_uint32_t level = 0;
|
||||
/* 1C */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Table {
|
||||
CharacterStats base_stats[12];
|
||||
le_uint32_t unknown[12];
|
||||
LevelStats levels[12][200];
|
||||
} __attribute__((packed));
|
||||
LevelTableV2(const std::string& data, bool compressed);
|
||||
virtual ~LevelTableV2() = default;
|
||||
|
||||
LevelTable(std::shared_ptr<const std::string> data, bool compressed);
|
||||
|
||||
const CharacterStats& base_stats_for_class(uint8_t char_class) const;
|
||||
const LevelStats& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
|
||||
const Level100Entry& level_100_stats_for_class(uint8_t char_class) const;
|
||||
const PlayerStats& max_stats_for_class(uint8_t char_class) const;
|
||||
virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
|
||||
|
||||
private:
|
||||
// TODO: Currently we only support the BB version of this file. It'd be nice
|
||||
// to support non-BB versions, but their formats are very different:
|
||||
//
|
||||
// BB:
|
||||
// root:
|
||||
// u32 offset:
|
||||
// u32[12] unknown
|
||||
// u32 offset:
|
||||
// u32[12] offsets:
|
||||
// LevelStats[200] level_stats
|
||||
// u32 offset:
|
||||
// CharacterStats[12] base_stats
|
||||
// GC:
|
||||
// root:
|
||||
// u32 offset:
|
||||
// u32[12] offsets:
|
||||
// LevelStats[200] level_stats
|
||||
// PC:
|
||||
// root:
|
||||
// u32 offset:
|
||||
// u32 offset[9]:
|
||||
// LevelStats[200] level_stats
|
||||
// u32 offset:
|
||||
// (0x18 bytes)
|
||||
// u32 offset:
|
||||
// PlayerStats[9] max_stats
|
||||
// u32 offset:
|
||||
// PlayerStats[9] level100_stats
|
||||
// u32 offset:
|
||||
// u32 offset[9]:
|
||||
// CharacterStats level1_stats
|
||||
// (11 more pointers)
|
||||
std::shared_ptr<const std::string> data;
|
||||
const Table* table;
|
||||
std::array<CharacterStats, 9> base_stats;
|
||||
std::array<Level100Entry, 9> level_100_stats;
|
||||
std::array<PlayerStats, 9> max_stats;
|
||||
std::array<std::array<LevelStatsDelta, 200>, 9> level_deltas;
|
||||
};
|
||||
|
||||
class LevelTableV3BE : public LevelTable { // from PlyLevelTbl.cpt (GC)
|
||||
public:
|
||||
LevelTableV3BE(const std::string& data, bool encrypted);
|
||||
virtual ~LevelTableV3BE() = default;
|
||||
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
|
||||
virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
|
||||
|
||||
private:
|
||||
std::array<std::array<LevelStatsDelta, 200>, 12> level_deltas;
|
||||
};
|
||||
|
||||
class LevelTableV4 : public LevelTable { // from PlyLevelTbl.prs (BB)
|
||||
public:
|
||||
LevelTableV4(const std::string& data, bool compressed);
|
||||
virtual ~LevelTableV4() = default;
|
||||
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
|
||||
virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
|
||||
|
||||
private:
|
||||
std::array<CharacterStats, 12> base_stats;
|
||||
std::array<std::array<LevelStatsDelta, 200>, 12> level_deltas;
|
||||
};
|
||||
|
||||
+68
-17
@@ -3,6 +3,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Hash.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "License.hh"
|
||||
@@ -127,6 +128,14 @@ shared_ptr<License> LicenseIndex::get(uint32_t serial_number) const {
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::get_by_bb_username(const string& bb_username) const {
|
||||
try {
|
||||
return this->bb_username_to_license.at(bb_username);
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
vector<shared_ptr<License>> LicenseIndex::all() const {
|
||||
vector<shared_ptr<License>> ret;
|
||||
ret.reserve(this->serial_number_to_license.size());
|
||||
@@ -157,57 +166,76 @@ void LicenseIndex::remove(uint32_t serial_number) {
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_v1_v2(uint32_t serial_number, const string& access_key) const {
|
||||
shared_ptr<License> LicenseIndex::verify_v1_v2(
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& character_name) const {
|
||||
if (serial_number == 0) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
return this->create_temporary_license_for_shared_license(license->flags, serial_number, access_key, "", character_name);
|
||||
}
|
||||
if (license->access_key.compare(0, 8, access_key) != 0) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_gc(uint32_t serial_number, const string& access_key) const {
|
||||
shared_ptr<License> LicenseIndex::verify_gc_no_password(
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& character_name) const {
|
||||
if (serial_number == 0) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
return this->create_temporary_license_for_shared_license(license->flags, serial_number, access_key, "", character_name);
|
||||
}
|
||||
if (license->access_key != access_key) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_gc(uint32_t serial_number, const string& access_key, const string& password) const {
|
||||
shared_ptr<License> LicenseIndex::verify_gc_with_password(
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& password,
|
||||
const string& character_name) const {
|
||||
if (serial_number == 0) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
return this->create_temporary_license_for_shared_license(license->flags, serial_number, access_key, password, character_name);
|
||||
}
|
||||
if (license->access_key != access_key) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->gc_password != password) {
|
||||
throw incorrect_password();
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
@@ -220,15 +248,18 @@ shared_ptr<License> LicenseIndex::verify_xb(const string& gamertag, uint64_t use
|
||||
}
|
||||
try {
|
||||
auto& license = this->xb_gamertag_to_license.at(gamertag);
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
throw missing_license(); // XB users cannot use shared serials
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->xb_user_id && (license->xb_user_id != user_id)) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->xb_account_id && (license->xb_account_id != account_id)) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
@@ -241,18 +272,38 @@ shared_ptr<License> LicenseIndex::verify_bb(const string& username, const string
|
||||
}
|
||||
try {
|
||||
auto& license = this->bb_username_to_license.at(username);
|
||||
if (license->bb_password != password) {
|
||||
throw incorrect_password();
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
throw missing_license(); // BB users cannot use shared serials
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->bb_password != password) {
|
||||
throw incorrect_password();
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::create_temporary_license_for_shared_license(
|
||||
uint32_t base_flags,
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& password,
|
||||
const string& character_name) const {
|
||||
uint32_t temp_serial_number = fnv1a32(&serial_number, sizeof(serial_number));
|
||||
temp_serial_number = fnv1a32(access_key, temp_serial_number);
|
||||
temp_serial_number = fnv1a32(password, temp_serial_number);
|
||||
temp_serial_number = fnv1a32(character_name, temp_serial_number);
|
||||
auto ret = this->create_temporary_license();
|
||||
ret->serial_number = temp_serial_number & 0x7FFFFFFF;
|
||||
ret->flags = base_flags;
|
||||
ret->set_flag(License::Flag::IS_SHARED_SERIAL);
|
||||
return ret;
|
||||
}
|
||||
|
||||
DiskLicenseIndex::DiskLicenseIndex() {
|
||||
struct BinaryLicense {
|
||||
pstring<TextEncoding::ASCII, 0x14> username; // BB username (max. 16 chars; should technically be Unicode)
|
||||
|
||||
+41
-6
@@ -12,22 +12,23 @@ class LicenseIndex;
|
||||
|
||||
class License {
|
||||
public:
|
||||
enum Flag : uint32_t {
|
||||
enum class Flag : uint32_t {
|
||||
// clang-format off
|
||||
KICK_USER = 0x00000001,
|
||||
BAN_USER = 0x00000002,
|
||||
SILENCE_USER = 0x00000004,
|
||||
CHANGE_LOBBY_INFO = 0x00000008,
|
||||
CHANGE_EVENT = 0x00000010,
|
||||
ANNOUNCE = 0x00000020,
|
||||
FREE_JOIN_GAMES = 0x00000040,
|
||||
UNLOCK_GAMES = 0x00000080,
|
||||
DEBUG = 0x01000000,
|
||||
CHEAT_ANYWHERE = 0x02000000,
|
||||
DISABLE_QUEST_REQUIREMENTS = 0x04000000,
|
||||
MODERATOR = 0x00000007,
|
||||
ADMINISTRATOR = 0x000000FF,
|
||||
ROOT = 0x7FFFFFFF,
|
||||
IS_SHARED_SERIAL = 0x80000000,
|
||||
// NOTE: When adding or changing license flags, don't forget to change the
|
||||
// documentation in the shell's help text.
|
||||
|
||||
UNUSED_BITS = 0x78FFFF00,
|
||||
// clang-format on
|
||||
@@ -60,6 +61,22 @@ public:
|
||||
virtual void save() const;
|
||||
virtual void delete_file() const;
|
||||
|
||||
[[nodiscard]] inline bool check_flag(Flag flag) const {
|
||||
return !!(this->flags & static_cast<uint32_t>(flag));
|
||||
}
|
||||
inline void set_flag(Flag flag) {
|
||||
this->flags |= static_cast<uint32_t>(flag);
|
||||
}
|
||||
inline void clear_flag(Flag flag) {
|
||||
this->flags &= (~static_cast<uint32_t>(flag));
|
||||
}
|
||||
inline void toggle_flag(Flag flag) {
|
||||
this->flags ^= static_cast<uint32_t>(flag);
|
||||
}
|
||||
inline void replace_all_flags(Flag mask) {
|
||||
this->flags = static_cast<uint32_t>(mask);
|
||||
}
|
||||
|
||||
std::string str() const;
|
||||
};
|
||||
|
||||
@@ -100,14 +117,25 @@ public:
|
||||
|
||||
size_t count() const;
|
||||
std::shared_ptr<License> get(uint32_t serial_number) const;
|
||||
std::shared_ptr<License> get_by_bb_username(const std::string& bb_username) const;
|
||||
std::vector<std::shared_ptr<License>> all() const;
|
||||
|
||||
void add(std::shared_ptr<License> l);
|
||||
void remove(uint32_t serial_number);
|
||||
|
||||
std::shared_ptr<License> verify_v1_v2(uint32_t serial_number, const std::string& access_key) const;
|
||||
std::shared_ptr<License> verify_gc(uint32_t serial_number, const std::string& access_key) const;
|
||||
std::shared_ptr<License> verify_gc(uint32_t serial_number, const std::string& access_key, const std::string& password) const;
|
||||
std::shared_ptr<License> verify_v1_v2(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name) const;
|
||||
std::shared_ptr<License> verify_gc_no_password(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name) const;
|
||||
std::shared_ptr<License> verify_gc_with_password(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& password,
|
||||
const std::string& character_name) const;
|
||||
std::shared_ptr<License> verify_xb(const std::string& gamertag, uint64_t user_id, uint64_t account_id) const;
|
||||
std::shared_ptr<License> verify_bb(const std::string& username, const std::string& password) const;
|
||||
|
||||
@@ -115,6 +143,13 @@ protected:
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> bb_username_to_license;
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> xb_gamertag_to_license;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<License>> serial_number_to_license;
|
||||
|
||||
std::shared_ptr<License> create_temporary_license_for_shared_license(
|
||||
uint32_t base_flags,
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& password,
|
||||
const std::string& character_name) const;
|
||||
};
|
||||
|
||||
class DiskLicenseIndex : public LicenseIndex {
|
||||
|
||||
+229
-116
@@ -123,7 +123,7 @@ void Lobby::FloorItemManager::clear() {
|
||||
}
|
||||
|
||||
uint32_t Lobby::FloorItemManager::reassign_all_item_ids(uint32_t next_item_id) {
|
||||
unordered_map<uint32_t, shared_ptr<FloorItem>> old_items;
|
||||
::map<uint32_t, shared_ptr<FloorItem>> old_items;
|
||||
old_items.swap(this->items);
|
||||
for (auto& queue : this->queue_for_client) {
|
||||
queue.clear();
|
||||
@@ -135,9 +135,9 @@ uint32_t Lobby::FloorItemManager::reassign_all_item_ids(uint32_t next_item_id) {
|
||||
return next_item_id;
|
||||
}
|
||||
|
||||
Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id)
|
||||
Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id, bool is_game)
|
||||
: server_state(s),
|
||||
log(string_printf("[Lobby:%" PRIX32 "] ", id), lobby_log.min_level),
|
||||
log(string_printf("[%s:%" PRIX32 "] ", is_game ? "Game" : "Lobby", id), lobby_log.min_level),
|
||||
lobby_id(id),
|
||||
min_level(0),
|
||||
max_level(0xFFFFFFFF),
|
||||
@@ -162,15 +162,24 @@ Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id)
|
||||
event_new(s->base.get(), -1, EV_TIMEOUT | EV_PERSIST, &Lobby::dispatch_on_idle_timeout, this),
|
||||
event_free) {
|
||||
this->log.info("Created");
|
||||
for (size_t x = 0; x < 12; x++) {
|
||||
this->next_item_id_for_client[x] = 0x00010000 + 0x00200000 * x;
|
||||
if (is_game) {
|
||||
this->set_flag(Flag::GAME);
|
||||
}
|
||||
this->reset_next_item_ids();
|
||||
}
|
||||
|
||||
Lobby::~Lobby() {
|
||||
this->log.info("Deleted");
|
||||
}
|
||||
|
||||
void Lobby::reset_next_item_ids() {
|
||||
uint32_t base_item_id = this->is_game() ? 0x00010000 : 0x10010000;
|
||||
for (size_t x = 0; x < 12; x++) {
|
||||
this->next_item_id_for_client[x] = base_item_id + 0x00200000 * x;
|
||||
}
|
||||
this->next_game_item_id = 0xCC000000;
|
||||
}
|
||||
|
||||
shared_ptr<ServerState> Lobby::require_server_state() const {
|
||||
auto s = this->server_state.lock();
|
||||
if (!s) {
|
||||
@@ -206,7 +215,7 @@ void Lobby::create_item_creator() {
|
||||
switch (this->base_version) {
|
||||
case Version::PC_PATCH:
|
||||
case Version::BB_PATCH:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
throw runtime_error("cannot create item creator for this base version");
|
||||
case Version::DC_NTE:
|
||||
@@ -242,7 +251,7 @@ void Lobby::create_item_creator() {
|
||||
s->tool_random_set,
|
||||
s->weapon_random_sets.at(this->difficulty),
|
||||
s->tekker_adjustment_set,
|
||||
s->item_parameter_table_for_version(this->base_version),
|
||||
s->item_parameter_table(this->base_version),
|
||||
this->base_version,
|
||||
this->episode,
|
||||
(this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode,
|
||||
@@ -252,9 +261,114 @@ void Lobby::create_item_creator() {
|
||||
this->quest ? this->quest->battle_rules : nullptr);
|
||||
}
|
||||
|
||||
shared_ptr<Map> Lobby::load_maps(
|
||||
Version version,
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
shared_ptr<PSOLFGEncryption> random_crypt,
|
||||
shared_ptr<const VersionedQuest> vq) {
|
||||
if (!vq->dat_contents_decompressed) {
|
||||
throw runtime_error("quest does not have DAT data");
|
||||
}
|
||||
auto map = make_shared<Map>(version, lobby_id, random_crypt);
|
||||
map->add_enemies_and_objects_from_quest_data(
|
||||
episode,
|
||||
difficulty,
|
||||
event,
|
||||
vq->dat_contents_decompressed->data(),
|
||||
vq->dat_contents_decompressed->size(),
|
||||
rare_rates);
|
||||
return map;
|
||||
}
|
||||
|
||||
shared_ptr<Map> Lobby::load_maps(
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
shared_ptr<const SetDataTableBase> sdt,
|
||||
function<shared_ptr<const string>(Version, const string&)> get_file_data,
|
||||
shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
shared_ptr<PSOLFGEncryption> random_crypt,
|
||||
const parray<le_uint32_t, 0x20>& variations,
|
||||
const PrefixedLogger* log) {
|
||||
auto enemy_filenames = sdt->map_filenames_for_variations(variations, episode, mode, true);
|
||||
auto object_filenames = sdt->map_filenames_for_variations(variations, episode, mode, false);
|
||||
return Lobby::load_maps(enemy_filenames, object_filenames, version, episode, mode, difficulty, event, lobby_id, get_file_data, rare_rates, random_crypt, log);
|
||||
}
|
||||
|
||||
shared_ptr<Map> Lobby::load_maps(
|
||||
const vector<string>& enemy_filenames,
|
||||
const vector<string>& object_filenames,
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
function<shared_ptr<const string>(Version, const string&)> get_file_data,
|
||||
shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
shared_ptr<PSOLFGEncryption> random_crypt,
|
||||
const PrefixedLogger* log) {
|
||||
auto map = make_shared<Map>(version, lobby_id, random_crypt);
|
||||
|
||||
// Don't load free-roam maps in Challenge mode, since players can't go to
|
||||
// Ragol without a quest loaded
|
||||
if (mode == GameMode::CHALLENGE) {
|
||||
return map;
|
||||
}
|
||||
|
||||
for (size_t floor = 0; floor < 0x12; floor++) {
|
||||
const auto& floor_enemy_filename = enemy_filenames.at(floor);
|
||||
if (!floor_enemy_filename.empty()) {
|
||||
auto map_data = get_file_data(version, floor_enemy_filename);
|
||||
if (map_data) {
|
||||
map->add_enemies_from_map_data(
|
||||
episode,
|
||||
difficulty,
|
||||
event,
|
||||
floor,
|
||||
map_data->data(),
|
||||
map_data->size(),
|
||||
rare_rates);
|
||||
if (log) {
|
||||
log->info("Loaded enemies map %s for floor %02zX", floor_enemy_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("Enemies map %s for floor %02zX cannot be used; skipping", floor_enemy_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("No enemies to load for floor %02zX", floor);
|
||||
}
|
||||
|
||||
const auto& floor_object_filename = object_filenames.at(floor);
|
||||
if (!floor_object_filename.empty()) {
|
||||
auto map_data = get_file_data(version, floor_object_filename);
|
||||
if (map_data) {
|
||||
map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
|
||||
if (log) {
|
||||
log->info("Loaded objects map %s for floor %02zX", floor_object_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("Objects map %s for floor %02zX cannot be used; skipping", floor_object_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("No objects to load for floor %02zX", floor);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
void Lobby::load_maps() {
|
||||
auto s = this->require_server_state();
|
||||
this->map = make_shared<Map>(this->lobby_id);
|
||||
auto rare_rates = ((this->base_version == Version::BB_V4) && this->rare_enemy_rates)
|
||||
? this->rare_enemy_rates
|
||||
: Map::DEFAULT_RARE_ENEMIES;
|
||||
|
||||
if (this->quest) {
|
||||
auto leader_c = this->clients.at(this->leader_id);
|
||||
@@ -262,87 +376,40 @@ void Lobby::load_maps() {
|
||||
throw logic_error("lobby leader is missing");
|
||||
}
|
||||
|
||||
auto vq = this->quest->version(Version::BB_V4, leader_c->language());
|
||||
auto dat_contents = prs_decompress(*vq->dat_contents);
|
||||
this->map->clear();
|
||||
this->map->add_enemies_and_objects_from_quest_data(
|
||||
auto vq = this->quest->version(this->base_version, leader_c->language());
|
||||
this->map = this->load_maps(
|
||||
this->base_version,
|
||||
this->episode,
|
||||
this->difficulty,
|
||||
this->event,
|
||||
dat_contents.data(),
|
||||
dat_contents.size(),
|
||||
this->random_seed,
|
||||
this->rare_enemy_rates ? this->rare_enemy_rates : Map::NO_RARE_ENEMIES);
|
||||
this->lobby_id,
|
||||
rare_rates,
|
||||
this->random_crypt,
|
||||
vq);
|
||||
|
||||
} else { // No quest loaded
|
||||
for (size_t floor = 0; floor < 0x10; floor++) {
|
||||
this->log.info("[Map/%zu] Using variations %" PRIX32 ", %" PRIX32,
|
||||
floor, this->variations[floor * 2].load(), this->variations[floor * 2 + 1].load());
|
||||
} else if (this->mode != GameMode::CHALLENGE) {
|
||||
auto s = this->require_server_state();
|
||||
this->map = this->load_maps(
|
||||
this->base_version,
|
||||
this->episode,
|
||||
this->mode,
|
||||
this->difficulty,
|
||||
this->event,
|
||||
this->lobby_id,
|
||||
s->set_data_table(this->base_version, this->episode, this->mode, this->difficulty),
|
||||
bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2),
|
||||
rare_rates,
|
||||
this->random_crypt,
|
||||
this->variations,
|
||||
&this->log);
|
||||
|
||||
auto enemy_filenames = map_filenames_for_variation(
|
||||
this->episode,
|
||||
(this->mode == GameMode::SOLO),
|
||||
floor,
|
||||
this->variations[floor * 2],
|
||||
this->variations[floor * 2 + 1],
|
||||
true);
|
||||
if (enemy_filenames.empty()) {
|
||||
this->log.info("[Map/%zu:e] No file to load", floor);
|
||||
} else {
|
||||
bool any_map_loaded = false;
|
||||
for (const string& filename : enemy_filenames) {
|
||||
try {
|
||||
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
|
||||
this->map->add_enemies_from_map_data(
|
||||
this->episode,
|
||||
this->difficulty,
|
||||
this->event,
|
||||
floor,
|
||||
map_data->data(),
|
||||
map_data->size(),
|
||||
this->rare_enemy_rates);
|
||||
any_map_loaded = true;
|
||||
break;
|
||||
} catch (const exception& e) {
|
||||
this->log.info("[Map/%zu:e] Failed to load %s: %s", floor, filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
if (!any_map_loaded) {
|
||||
throw runtime_error(string_printf("no enemy maps loaded for floor %zu", floor));
|
||||
}
|
||||
}
|
||||
|
||||
auto object_filenames = map_filenames_for_variation(
|
||||
this->episode,
|
||||
(this->mode == GameMode::SOLO),
|
||||
floor,
|
||||
this->variations[floor * 2],
|
||||
this->variations[floor * 2 + 1],
|
||||
false);
|
||||
if (object_filenames.empty()) {
|
||||
this->log.info("[Map/%zu:o] No file to load", floor);
|
||||
} else {
|
||||
bool any_map_loaded = false;
|
||||
for (const string& filename : object_filenames) {
|
||||
try {
|
||||
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
|
||||
this->map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
|
||||
any_map_loaded = true;
|
||||
break;
|
||||
} catch (const exception& e) {
|
||||
this->log.info("[Map/%zu:o] Failed to load %s: %s", floor, filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
if (!any_map_loaded) {
|
||||
throw runtime_error(string_printf("no object maps loaded for floor %zu", floor));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->map = make_shared<Map>(this->base_version, this->lobby_id, this->random_crypt);
|
||||
}
|
||||
|
||||
this->log.info("Generated objects list (%zu entries):", this->map->objects.size());
|
||||
for (size_t z = 0; z < this->map->objects.size(); z++) {
|
||||
string o_str = this->map->objects[z].str(s->item_name_index);
|
||||
string o_str = this->map->objects[z].str();
|
||||
this->log.info("(K-%zX) %s", z, o_str.c_str());
|
||||
}
|
||||
this->log.info("Generated enemies list (%zu entries):", this->map->enemies.size());
|
||||
@@ -352,10 +419,6 @@ void Lobby::load_maps() {
|
||||
}
|
||||
this->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)",
|
||||
this->map->objects.size(), this->map->enemies.size(), this->map->rare_enemy_indexes.size());
|
||||
|
||||
if (this->item_creator) {
|
||||
this->item_creator->clear_destroyed_entities();
|
||||
}
|
||||
}
|
||||
|
||||
void Lobby::create_ep3_server() {
|
||||
@@ -366,15 +429,20 @@ void Lobby::create_ep3_server() {
|
||||
this->log.info("Recreating Episode 3 server state");
|
||||
}
|
||||
auto tourn = this->tournament_match ? this->tournament_match->tournament.lock() : nullptr;
|
||||
bool is_trial = this->base_version == Version::GC_EP3_TRIAL_EDITION;
|
||||
bool is_nte = this->base_version == Version::GC_EP3_NTE;
|
||||
Episode3::Server::Options options = {
|
||||
.card_index = is_trial ? s->ep3_card_index_trial : s->ep3_card_index,
|
||||
.card_index = is_nte ? s->ep3_card_index_trial : s->ep3_card_index,
|
||||
.map_index = s->ep3_map_index,
|
||||
.behavior_flags = s->ep3_behavior_flags,
|
||||
.random_crypt = this->random_crypt,
|
||||
.tournament = tourn,
|
||||
.trap_card_ids = s->ep3_trap_card_ids,
|
||||
};
|
||||
if (this->base_version == Version::GC_EP3_NTE) {
|
||||
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
||||
} else {
|
||||
options.behavior_flags &= (~Episode3::BehaviorFlag::IS_TRIAL_EDITION);
|
||||
}
|
||||
this->ep3_server = make_shared<Episode3::Server>(this->shared_from_this(), std::move(options));
|
||||
this->ep3_server->init();
|
||||
}
|
||||
@@ -465,31 +533,30 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
|
||||
this->leader_id = c->lobby_client_id;
|
||||
}
|
||||
|
||||
// If the lobby is a game and there was no one in it, reassign all the floor
|
||||
// If this is a lobby or no one was here before this, reassign all the floor
|
||||
// item IDs and reset the next item IDs
|
||||
if (this->is_game()) {
|
||||
if (leader_index >= this->max_clients) {
|
||||
for (size_t x = 0; x < 12; x++) {
|
||||
this->next_item_id_for_client[x] = 0x00010000 + 0x00200000 * x;
|
||||
}
|
||||
this->next_game_item_id = 0xCC000000;
|
||||
if (!this->is_game() || (leader_index >= this->max_clients)) {
|
||||
this->reset_next_item_ids();
|
||||
for (auto& m : this->floor_item_managers) {
|
||||
this->next_game_item_id = m.reassign_all_item_ids(this->next_game_item_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Reassign all floor item IDs so they won't conflict with any players'
|
||||
// item IDs
|
||||
for (auto& m : this->floor_item_managers) {
|
||||
this->next_game_item_id = m.reassign_all_item_ids(this->next_game_item_id);
|
||||
}
|
||||
}
|
||||
// On DC NTE and 11/2000, the game assigns item IDs immediately when a
|
||||
// player joins a game, then assigns them again after the 6x6D equivalent is
|
||||
// received. For this reason, we consume item IDs here only if the client is
|
||||
// NTE or 11/2000.
|
||||
this->assign_inventory_and_bank_item_ids(c, is_pre_v1(c->version()));
|
||||
// On BB, we send artificial flag state to fix an Episode 2 bug where the
|
||||
// CCA door lock state is overwritten by quests.
|
||||
if (c->version() == Version::BB_V4) {
|
||||
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
|
||||
}
|
||||
// If this is not a game or the joining client is the leader, they will assign
|
||||
// their item IDs BEFORE they process any inbound commands (therefore a 6x6D
|
||||
// command, which we will send during loading, should reflect the item state
|
||||
// AFTER their IDs are assigned). If the joining client is not the leader,
|
||||
// they will not assign their item IDs until they receive a 6x71 command,
|
||||
// which is sent AFTER the 6x6D command, so the 6x6D should reflect the item
|
||||
// state BEFORE their IDs are assigned. (In the latter case, we'll assign the
|
||||
// IDs for real when they send a 6F command, or 6x1F equivalent in the case of
|
||||
// DC NTE and 11/2000.)
|
||||
this->assign_inventory_and_bank_item_ids(c, (!this->is_game() || (c->lobby_client_id == this->leader_id)));
|
||||
|
||||
// On BB, we send artificial flag state to fix an Episode 2 bug where the
|
||||
// CCA door lock state is overwritten by quests.
|
||||
if (this->is_game() && (c->version() == Version::BB_V4)) {
|
||||
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
|
||||
}
|
||||
|
||||
// If the lobby is recording a battle record, add the player join event
|
||||
@@ -637,6 +704,51 @@ shared_ptr<Client> Lobby::find_client(const string* identifier, uint64_t serial_
|
||||
throw out_of_range("client not found");
|
||||
}
|
||||
|
||||
Lobby::JoinError Lobby::join_error_for_client(std::shared_ptr<Client> c, const std::string* password) const {
|
||||
if (this->count_clients() >= this->max_clients) {
|
||||
return JoinError::FULL;
|
||||
}
|
||||
if (!this->version_is_allowed(c->version()) && !c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
return JoinError::VERSION_CONFLICT;
|
||||
}
|
||||
if (this->is_game()) {
|
||||
if (this->check_flag(Flag::QUEST_IN_PROGRESS)) {
|
||||
return JoinError::QUEST_IN_PROGRESS;
|
||||
}
|
||||
if (this->check_flag(Flag::BATTLE_IN_PROGRESS)) {
|
||||
return JoinError::BATTLE_IN_PROGRESS;
|
||||
}
|
||||
if (this->mode == GameMode::SOLO) {
|
||||
return JoinError::SOLO;
|
||||
}
|
||||
if (!c->license->check_flag(License::Flag::FREE_JOIN_GAMES)) {
|
||||
if (password && !this->password.empty() && (*password != this->password)) {
|
||||
return JoinError::INCORRECT_PASSWORD;
|
||||
}
|
||||
auto p = c->character();
|
||||
if (p->disp.stats.level < this->min_level) {
|
||||
return JoinError::LEVEL_TOO_LOW;
|
||||
}
|
||||
if (p->disp.stats.level > this->max_level) {
|
||||
return JoinError::LEVEL_TOO_HIGH;
|
||||
}
|
||||
if (this->quest) {
|
||||
size_t num_clients = this->count_clients() + 1;
|
||||
if (!c->can_see_quest(this->quest, this->event, this->difficulty, num_clients) ||
|
||||
!c->can_play_quest(this->quest, this->event, this->difficulty, num_clients)) {
|
||||
return JoinError::NO_ACCESS_TO_QUEST;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only prevent joining during loading if the client is actually trying to
|
||||
// join (not just loading the game list)
|
||||
if (password && this->any_client_loading()) {
|
||||
return JoinError::LOADING;
|
||||
}
|
||||
}
|
||||
return JoinError::ALLOWED;
|
||||
}
|
||||
|
||||
uint8_t Lobby::game_event_for_lobby_event(uint8_t lobby_event) {
|
||||
if (lobby_event > 7) {
|
||||
return 0;
|
||||
@@ -713,19 +825,20 @@ 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();
|
||||
uint32_t start_item_id = this->next_item_id_for_client[c->lobby_client_id];
|
||||
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);
|
||||
}
|
||||
if (!consume_ids) {
|
||||
this->next_item_id_for_client[c->lobby_client_id] = start_item_id;
|
||||
this->next_item_id_for_client[c->lobby_client_id] = orig_next_item_id;
|
||||
}
|
||||
|
||||
if (c->log.info("Assigned inventory item IDs%s", consume_ids ? "" : " but did not mark IDs as used")) {
|
||||
p->print_inventory(stderr, c->version(), c->require_server_state()->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
if (p->bank.num_items) {
|
||||
p->bank.assign_ids(0x99000000 + (c->lobby_client_id << 20));
|
||||
c->log.info("Assigned bank item IDs");
|
||||
p->print_bank(stderr, c->version(), c->require_server_state()->item_name_index);
|
||||
c->print_bank(stderr);
|
||||
} else {
|
||||
c->log.info("Bank is empty");
|
||||
}
|
||||
@@ -747,10 +860,10 @@ QuestIndex::IncludeCondition Lobby::quest_include_condition() const {
|
||||
return [this, num_players](shared_ptr<const Quest> q) -> QuestIndex::IncludeState {
|
||||
bool is_enabled = true;
|
||||
for (const auto& lc : this->clients) {
|
||||
if (lc && !lc->can_see_quest(q, this->difficulty, num_players)) {
|
||||
if (lc && !lc->can_see_quest(q, this->event, this->difficulty, num_players)) {
|
||||
return QuestIndex::IncludeState::HIDDEN;
|
||||
}
|
||||
if (lc && !lc->can_play_quest(q, this->difficulty, num_players)) {
|
||||
if (lc && !lc->can_play_quest(q, this->event, this->difficulty, num_players)) {
|
||||
is_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
+57
-3
@@ -36,7 +36,9 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
struct FloorItemManager {
|
||||
PrefixedLogger log;
|
||||
uint64_t next_drop_number;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<FloorItem>> items; // Keyed on item_id
|
||||
// It's important that this is a map and not an unordered_map. See the
|
||||
// comment in send_game_item_state for more details.
|
||||
std::map<uint32_t, std::shared_ptr<FloorItem>> items; // Keyed on item_id
|
||||
std::array<std::map<uint64_t, std::shared_ptr<FloorItem>>, 12> queue_for_client;
|
||||
|
||||
FloorItemManager(uint32_t lobby_id, uint8_t floor);
|
||||
@@ -149,7 +151,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
std::shared_ptr<Episode3::BattleRecord> battle_record; // Not used in watcher games
|
||||
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player; // Only used in replay games
|
||||
std::shared_ptr<Episode3::Tournament::Match> tournament_match;
|
||||
std::shared_ptr<const G_SetEXResultValues_GC_Ep3_6xB4x4B> ep3_ex_result_values;
|
||||
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_ex_result_values;
|
||||
|
||||
// Lobby stuff
|
||||
uint8_t event;
|
||||
@@ -167,13 +169,15 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
uint64_t idle_timeout_usecs;
|
||||
std::unique_ptr<struct event, void (*)(struct event*)> idle_timeout_event;
|
||||
|
||||
Lobby(std::shared_ptr<ServerState> s, uint32_t id);
|
||||
Lobby(std::shared_ptr<ServerState> s, uint32_t id, bool is_game);
|
||||
Lobby(const Lobby&) = delete;
|
||||
Lobby(Lobby&&) = delete;
|
||||
~Lobby();
|
||||
Lobby& operator=(const Lobby&) = delete;
|
||||
Lobby& operator=(Lobby&&) = delete;
|
||||
|
||||
void reset_next_item_ids();
|
||||
|
||||
[[nodiscard]] inline bool check_flag(Flag flag) const {
|
||||
return !!(this->enabled_flags & static_cast<uint32_t>(flag));
|
||||
}
|
||||
@@ -191,6 +195,41 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
|
||||
void set_drop_mode(DropMode new_mode);
|
||||
void create_item_creator();
|
||||
static std::shared_ptr<Map> load_maps(
|
||||
Version version,
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
std::shared_ptr<PSOLFGEncryption> random_crypt,
|
||||
std::shared_ptr<const VersionedQuest> vq);
|
||||
static std::shared_ptr<Map> load_maps(
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
std::shared_ptr<const SetDataTableBase> sdt,
|
||||
std::function<std::shared_ptr<const std::string>(Version, const std::string&)> get_file_data,
|
||||
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
std::shared_ptr<PSOLFGEncryption> random_crypt,
|
||||
const parray<le_uint32_t, 0x20>& variations,
|
||||
const PrefixedLogger* log = nullptr);
|
||||
static std::shared_ptr<Map> load_maps(
|
||||
const std::vector<std::string>& enemy_filenames,
|
||||
const std::vector<std::string>& object_filenames,
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
std::function<std::shared_ptr<const std::string>(Version, const std::string&)> get_file_data,
|
||||
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
std::shared_ptr<PSOLFGEncryption> random_crypt,
|
||||
const PrefixedLogger* log = nullptr);
|
||||
void load_maps();
|
||||
void create_ep3_server();
|
||||
|
||||
@@ -224,6 +263,21 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
const std::string* identifier = nullptr,
|
||||
uint64_t serial_number = 0);
|
||||
|
||||
enum class JoinError {
|
||||
ALLOWED = 0,
|
||||
FULL,
|
||||
VERSION_CONFLICT,
|
||||
QUEST_IN_PROGRESS,
|
||||
BATTLE_IN_PROGRESS,
|
||||
LOADING,
|
||||
SOLO,
|
||||
INCORRECT_PASSWORD,
|
||||
LEVEL_TOO_LOW,
|
||||
LEVEL_TOO_HIGH,
|
||||
NO_ACCESS_TO_QUEST,
|
||||
};
|
||||
JoinError join_error_for_client(std::shared_ptr<Client> c, const std::string* password) const;
|
||||
|
||||
bool item_exists(uint8_t floor, uint32_t item_id) const;
|
||||
std::shared_ptr<FloorItem> find_item(uint8_t floor, uint32_t item_id) const;
|
||||
void add_item(uint8_t floor, const ItemData& item, float x, float z, uint16_t visibility_flags);
|
||||
|
||||
+732
-158
File diff suppressed because it is too large
Load Diff
+1054
-106
File diff suppressed because it is too large
Load Diff
+85
-23
@@ -10,12 +10,13 @@
|
||||
#include <vector>
|
||||
|
||||
#include "BattleParamsIndex.hh"
|
||||
#include "ItemNameIndex.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
struct Map {
|
||||
static const char* name_for_object_type(uint16_t type);
|
||||
|
||||
struct SectionHeader {
|
||||
enum class Type {
|
||||
END = 0,
|
||||
@@ -207,6 +208,8 @@ struct Map {
|
||||
// TODO: Add more fields in here if we ever care about them. Currently we
|
||||
// only care about boxes with fixed item drops.
|
||||
size_t source_index;
|
||||
uint16_t object_id;
|
||||
uint8_t floor;
|
||||
uint16_t base_type;
|
||||
uint16_t section;
|
||||
float param1; // If <= 0, this is a specialized box, and the specialization is in param4/5/6
|
||||
@@ -214,10 +217,9 @@ struct Map {
|
||||
uint32_t param4;
|
||||
uint32_t param5;
|
||||
uint32_t param6;
|
||||
uint8_t floor;
|
||||
bool item_drop_checked;
|
||||
|
||||
std::string str(std::shared_ptr<const ItemNameIndex> name_index) const;
|
||||
std::string str() const;
|
||||
};
|
||||
|
||||
struct Enemy {
|
||||
@@ -229,12 +231,13 @@ struct Map {
|
||||
ITEM_DROPPED = 0x10,
|
||||
};
|
||||
size_t source_index;
|
||||
uint16_t enemy_id;
|
||||
EnemyType type;
|
||||
uint8_t floor;
|
||||
uint8_t state_flags;
|
||||
uint8_t last_hit_by_client_id;
|
||||
|
||||
Enemy(size_t source_index, uint8_t floor, EnemyType type);
|
||||
Enemy(uint16_t enemy_id, size_t source_index, uint8_t floor, EnemyType type);
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
@@ -253,7 +256,7 @@ struct Map {
|
||||
void generate_shuffled_location_table(const Map::RandomEnemyLocationsHeader& header, StringReader r, uint16_t section);
|
||||
};
|
||||
|
||||
explicit Map(uint32_t lobby_id);
|
||||
Map(Version version, uint32_t lobby_id, std::shared_ptr<PSOLFGEncryption> random_crypt);
|
||||
~Map() = default;
|
||||
|
||||
void clear();
|
||||
@@ -303,22 +306,43 @@ struct Map {
|
||||
uint8_t event,
|
||||
const void* data,
|
||||
size_t size,
|
||||
uint32_t rare_seed,
|
||||
std::shared_ptr<const RareEnemyRates> rare_rates = Map::DEFAULT_RARE_ENEMIES);
|
||||
|
||||
const Enemy& find_enemy(uint8_t floor, EnemyType type) const;
|
||||
Enemy& find_enemy(uint8_t floor, EnemyType type);
|
||||
|
||||
static std::string disassemble_quest_data(const void* data, size_t size);
|
||||
|
||||
PrefixedLogger log;
|
||||
Version version;
|
||||
std::shared_ptr<PSOLFGEncryption> random_crypt;
|
||||
std::vector<Object> objects;
|
||||
std::vector<Enemy> enemies;
|
||||
std::vector<size_t> rare_enemy_indexes;
|
||||
};
|
||||
|
||||
// TODO: This class is currently unused. It would be nice if we could use this
|
||||
// to generate variations and link to the corresponding map filenames, but it
|
||||
// seems that SetDataTable.rel files link to map filenames that don't actually
|
||||
// exist in some cases, so we can't just directly use this data structure.
|
||||
class SetDataTable {
|
||||
class SetDataTableBase {
|
||||
public:
|
||||
virtual ~SetDataTableBase() = default;
|
||||
|
||||
parray<le_uint32_t, 0x20> generate_variations(Episode episode, bool is_solo, std::shared_ptr<PSOLFGEncryption> random_crypt) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const = 0;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const = 0;
|
||||
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const = 0;
|
||||
std::vector<std::string> map_filenames_for_variations(
|
||||
const parray<le_uint32_t, 0x20>& variations, Episode episode, GameMode mode, bool is_enemies) const;
|
||||
|
||||
uint8_t default_area_for_floor(Episode episode, uint8_t floor) const;
|
||||
|
||||
protected:
|
||||
explicit SetDataTableBase(Version version);
|
||||
|
||||
Version version;
|
||||
};
|
||||
|
||||
class SetDataTable : public SetDataTableBase {
|
||||
public:
|
||||
struct SetEntry {
|
||||
std::string object_list_basename;
|
||||
@@ -326,30 +350,68 @@ public:
|
||||
std::string event_list_basename;
|
||||
};
|
||||
|
||||
SetDataTable(std::shared_ptr<const std::string> data, bool big_endian);
|
||||
SetDataTable(Version version, const std::string& data);
|
||||
virtual ~SetDataTable() = default;
|
||||
|
||||
inline const std::vector<std::vector<std::vector<SetEntry>>> get() const {
|
||||
return this->entries;
|
||||
}
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const;
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const;
|
||||
|
||||
void print(FILE* stream) const;
|
||||
std::string str() const;
|
||||
|
||||
private:
|
||||
template <bool IsBigEndian>
|
||||
void load_table_t(std::shared_ptr<const std::string> data);
|
||||
void load_table_t(const std::string& data);
|
||||
|
||||
// Indexes are [floor][variation1][variation2]
|
||||
// floor is cumulative per episode, so Ep2 starts at floor=18.
|
||||
std::vector<std::vector<std::vector<SetEntry>>> entries;
|
||||
};
|
||||
|
||||
void generate_variations(
|
||||
class SetDataTableDCNTE : public SetDataTableBase {
|
||||
public:
|
||||
SetDataTableDCNTE();
|
||||
virtual ~SetDataTableDCNTE() = default;
|
||||
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const;
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const;
|
||||
|
||||
private:
|
||||
static const std::array<std::vector<std::vector<std::string>>, 0x10> NAMES;
|
||||
};
|
||||
|
||||
class SetDataTableDC112000 : public SetDataTableBase {
|
||||
public:
|
||||
SetDataTableDC112000();
|
||||
virtual ~SetDataTableDC112000() = default;
|
||||
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const;
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, bool is_enemies) const;
|
||||
|
||||
private:
|
||||
static const std::array<std::vector<std::vector<std::string>>, 0x10> NAMES;
|
||||
};
|
||||
|
||||
void generate_variations_deprecated(
|
||||
parray<le_uint32_t, 0x20>& variations,
|
||||
std::shared_ptr<PSOLFGEncryption> random,
|
||||
Version version,
|
||||
Episode episode,
|
||||
bool is_solo);
|
||||
void generate_variations_dc_nte(
|
||||
parray<le_uint32_t, 0x20>& variations,
|
||||
std::shared_ptr<PSOLFGEncryption> random);
|
||||
std::vector<std::string> map_filenames_for_variation(
|
||||
Episode episode, bool is_solo, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies);
|
||||
|
||||
parray<le_uint32_t, 0x20> variation_maxes_deprecated(Version version, Episode episode, bool is_solo);
|
||||
bool next_variation_deprecated(parray<le_uint32_t, 0x20>& variations, Version version, Episode episode, bool is_solo);
|
||||
|
||||
std::vector<std::string> map_filenames_for_variation_deprecated(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Version version, Episode episode, GameMode mode, bool is_enemies);
|
||||
std::vector<std::vector<std::string>> map_filenames_for_variations_deprecated(
|
||||
const parray<le_uint32_t, 0x20>& variations,
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
bool is_enemies);
|
||||
|
||||
+2
-2
@@ -60,8 +60,8 @@ constexpr uint32_t GO_BACK = 0x99FFFF99;
|
||||
namespace ProxyOptionsMenuItemID {
|
||||
constexpr uint32_t GO_BACK = 0xAAFFFFAA;
|
||||
constexpr uint32_t CHAT_COMMANDS = 0xAA0101AA;
|
||||
constexpr uint32_t CHAT_FILTER = 0xAA0202AA;
|
||||
constexpr uint32_t PLAYER_NOTIFICATIONS = 0xAA0303AA;
|
||||
constexpr uint32_t PLAYER_NOTIFICATIONS = 0xAA0202AA;
|
||||
constexpr uint32_t DROP_NOTIFICATIONS = 0xAA0303AA;
|
||||
constexpr uint32_t BLOCK_PINGS = 0xAA0404AA;
|
||||
constexpr uint32_t INFINITE_HP = 0xAA0505AA;
|
||||
constexpr uint32_t INFINITE_TP = 0xAA0606AA;
|
||||
|
||||
+25
-50
@@ -117,22 +117,14 @@ void PSOLFGEncryption::encrypt_both_endian(
|
||||
}
|
||||
|
||||
PSOV2Encryption::PSOV2Encryption(uint32_t seed)
|
||||
: PSOLFGEncryption(seed, this->STREAM_LENGTH + 1, this->STREAM_LENGTH) {
|
||||
uint32_t esi, ebx, edi, eax, edx, var1;
|
||||
esi = 1;
|
||||
ebx = this->initial_seed;
|
||||
edi = 0x15;
|
||||
this->stream[56] = ebx;
|
||||
this->stream[55] = ebx;
|
||||
while (edi <= 0x46E) {
|
||||
eax = edi;
|
||||
var1 = eax / 55;
|
||||
edx = eax - (var1 * 55);
|
||||
ebx = ebx - esi;
|
||||
edi = edi + 0x15;
|
||||
this->stream[edx] = esi;
|
||||
esi = ebx;
|
||||
ebx = this->stream[edx];
|
||||
: PSOLFGEncryption(seed, STREAM_LENGTH + 1, STREAM_LENGTH) {
|
||||
uint32_t a = 1, b = this->initial_seed;
|
||||
this->stream[0x37] = b;
|
||||
for (uint16_t virtual_index = 0x15; virtual_index <= 0x36 * 0x15; virtual_index += 0x15) {
|
||||
this->stream[virtual_index % 0x37] = a;
|
||||
uint32_t c = b - a;
|
||||
b = a;
|
||||
a = c;
|
||||
}
|
||||
for (size_t x = 0; x < 5; x++) {
|
||||
this->update_stream();
|
||||
@@ -141,26 +133,11 @@ PSOV2Encryption::PSOV2Encryption(uint32_t seed)
|
||||
}
|
||||
|
||||
void PSOV2Encryption::update_stream() {
|
||||
uint32_t esi, edi, eax, ebp, edx;
|
||||
edi = 1;
|
||||
edx = 0x18;
|
||||
eax = edi;
|
||||
while (edx > 0) {
|
||||
esi = this->stream[eax + 0x1F];
|
||||
ebp = this->stream[eax] - esi;
|
||||
this->stream[eax] = ebp;
|
||||
eax++;
|
||||
edx--;
|
||||
for (size_t z = 1; z < 0x19; z++) {
|
||||
this->stream[z] -= this->stream[z + 0x1F];
|
||||
}
|
||||
edi = 0x19;
|
||||
edx = 0x1F;
|
||||
eax = edi;
|
||||
while (edx > 0) {
|
||||
esi = this->stream[eax - 0x18];
|
||||
ebp = this->stream[eax] - esi;
|
||||
this->stream[eax] = ebp;
|
||||
eax++;
|
||||
edx--;
|
||||
for (size_t z = 0x19; z < 0x38; z++) {
|
||||
this->stream[z] -= this->stream[z - 0x18];
|
||||
}
|
||||
this->offset = 1;
|
||||
this->cycles++;
|
||||
@@ -171,7 +148,7 @@ PSOEncryption::Type PSOV2Encryption::type() const {
|
||||
}
|
||||
|
||||
PSOV3Encryption::PSOV3Encryption(uint32_t seed)
|
||||
: PSOLFGEncryption(seed, this->STREAM_LENGTH, this->STREAM_LENGTH) {
|
||||
: PSOLFGEncryption(seed, STREAM_LENGTH, STREAM_LENGTH) {
|
||||
uint32_t x, y, basekey, source1, source2, source3;
|
||||
basekey = 0;
|
||||
|
||||
@@ -194,7 +171,7 @@ PSOV3Encryption::PSOV3Encryption(uint32_t seed)
|
||||
source1 = 0;
|
||||
source2 = 1;
|
||||
source3 = this->offset - 1;
|
||||
while (this->offset != this->STREAM_LENGTH) {
|
||||
while (this->offset != STREAM_LENGTH) {
|
||||
this->stream[this->offset++] = (this->stream[source3++] ^ (((this->stream[source1++] << 23) & 0xFF800000) ^ ((this->stream[source2++] >> 9) & 0x007FFFFF)));
|
||||
}
|
||||
|
||||
@@ -205,19 +182,13 @@ PSOV3Encryption::PSOV3Encryption(uint32_t seed)
|
||||
}
|
||||
|
||||
void PSOV3Encryption::update_stream() {
|
||||
uint32_t r5, r6, r7;
|
||||
r5 = 0;
|
||||
r6 = 489;
|
||||
r7 = 0;
|
||||
|
||||
while (r6 != this->STREAM_LENGTH) {
|
||||
this->stream[r5++] ^= this->stream[r6++];
|
||||
static constexpr size_t PHASE2_OFFSET = STREAM_LENGTH - 489;
|
||||
for (size_t z = 489; z < STREAM_LENGTH; z++) {
|
||||
this->stream[z - 489] ^= this->stream[z];
|
||||
}
|
||||
|
||||
while (r5 != this->STREAM_LENGTH) {
|
||||
this->stream[r5++] ^= this->stream[r7++];
|
||||
for (size_t z = PHASE2_OFFSET; z < STREAM_LENGTH; z++) {
|
||||
this->stream[z] ^= this->stream[z - PHASE2_OFFSET];
|
||||
}
|
||||
|
||||
this->offset = 0;
|
||||
this->cycles++;
|
||||
}
|
||||
@@ -918,15 +889,19 @@ uint32_t encrypt_challenge_time(uint16_t value) {
|
||||
available_bits.erase(it);
|
||||
}
|
||||
|
||||
return (mask << 16) | (value ^ mask);
|
||||
uint32_t ret = (mask << 16) | (value ^ mask);
|
||||
fprintf(stderr, "encrypt_challenge_time %04hX => %08" PRIX32 "\n", value, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint16_t decrypt_challenge_time(uint32_t value) {
|
||||
uint16_t mask = (value >> 0x10);
|
||||
uint8_t mask_one_bits = count_one_bits(mask);
|
||||
return ((mask_one_bits < 4) || (mask_one_bits > 12))
|
||||
uint16_t ret = ((mask_one_bits < 4) || (mask_one_bits > 12))
|
||||
? 0xFFFF
|
||||
: ((mask ^ value) & 0xFFFF);
|
||||
fprintf(stderr, "decrypt_challenge_time %08" PRIX32 " => %04hX\n", value, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
string decrypt_v2_registry_value(const void* data, size_t size) {
|
||||
|
||||
+49
-2
@@ -5,10 +5,12 @@
|
||||
|
||||
#include <memory>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Text.hh" // for parray
|
||||
#include "Compression.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
class PSOEncryption {
|
||||
public:
|
||||
@@ -79,7 +81,7 @@ public:
|
||||
protected:
|
||||
virtual void update_stream();
|
||||
|
||||
static constexpr size_t STREAM_LENGTH = 56;
|
||||
static constexpr size_t STREAM_LENGTH = 0x38;
|
||||
};
|
||||
|
||||
class PSOV3Encryption : public PSOLFGEncryption {
|
||||
@@ -250,6 +252,41 @@ void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis);
|
||||
uint32_t encrypt_challenge_time(uint16_t value);
|
||||
uint16_t decrypt_challenge_time(uint32_t value);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
class ChallengeTime {
|
||||
private:
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
U32T value;
|
||||
|
||||
public:
|
||||
ChallengeTime() = default;
|
||||
ChallengeTime(uint16_t v) {
|
||||
this->store(v);
|
||||
}
|
||||
ChallengeTime(const ChallengeTime& other) = default;
|
||||
ChallengeTime(ChallengeTime&& other) = default;
|
||||
ChallengeTime& operator=(const ChallengeTime& other) = default;
|
||||
ChallengeTime& operator=(ChallengeTime&& other) = default;
|
||||
|
||||
bool has_value() const {
|
||||
return this->value != 0;
|
||||
}
|
||||
|
||||
uint16_t load() const {
|
||||
return decrypt_challenge_time(this->value);
|
||||
}
|
||||
operator uint16_t() const {
|
||||
return this->load();
|
||||
}
|
||||
void store(uint16_t v) {
|
||||
this->value = ((v == 0) || (v == 0xFFFF)) ? 0 : encrypt_challenge_time(v);
|
||||
}
|
||||
ChallengeTime& operator=(uint16_t v) {
|
||||
this->store(v);
|
||||
return *this;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
|
||||
std::string decrypt_v2_registry_value(const void* data, size_t size);
|
||||
|
||||
struct DecryptedPR2 {
|
||||
@@ -277,6 +314,16 @@ DecryptedPR2 decrypt_pr2_data(const std::string& data) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
std::string decrypt_and_decompress_pr2_data(const std::string& data) {
|
||||
auto decrypted = decrypt_pr2_data<IsBigEndian>(data);
|
||||
std::string decompressed = prs_decompress(decrypted.compressed_data);
|
||||
if (decompressed.size() != decrypted.decompressed_size) {
|
||||
throw std::runtime_error("decompressed size does not match expected size");
|
||||
}
|
||||
return decompressed;
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
std::string encrypt_pr2_data(const std::string& data, size_t decompressed_size, uint32_t seed) {
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
|
||||
+7
-7
@@ -31,7 +31,7 @@ uint16_t PSOCommandHeader::command(Version version) const {
|
||||
return this->dc.command;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
return this->gc.command;
|
||||
case Version::XB_V3:
|
||||
@@ -59,7 +59,7 @@ void PSOCommandHeader::set_command(Version version, uint16_t command) {
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
this->gc.command = command;
|
||||
break;
|
||||
@@ -88,7 +88,7 @@ uint16_t PSOCommandHeader::size(Version version) const {
|
||||
return this->dc.size;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
return this->gc.size;
|
||||
case Version::XB_V3:
|
||||
@@ -116,7 +116,7 @@ void PSOCommandHeader::set_size(Version version, uint32_t size) {
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
this->gc.size = size;
|
||||
break;
|
||||
@@ -145,7 +145,7 @@ uint32_t PSOCommandHeader::flag(Version version) const {
|
||||
return this->dc.flag;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
return this->gc.flag;
|
||||
case Version::XB_V3:
|
||||
@@ -173,7 +173,7 @@ void PSOCommandHeader::set_flag(Version version, uint32_t flag) {
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
this->gc.flag = flag;
|
||||
break;
|
||||
@@ -218,7 +218,7 @@ std::string prepend_command_header(
|
||||
case Version::DC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
PSOCommandHeaderDCV3 header;
|
||||
|
||||
+91
-63
@@ -33,8 +33,8 @@ PlayerInventoryItem::PlayerInventoryItem(const ItemData& item, bool equipped)
|
||||
uint32_t PlayerVisualConfig::compute_name_color_checksum(uint32_t name_color) {
|
||||
uint8_t x = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
uint8_t y = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
// name_color = ABCDEFGHabcdefghIJKLMNOPijklmnop
|
||||
// name_color_checksum = ---------ijklmabcdeIJKLM-------- ^ (xxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyy)
|
||||
// name_color (ARGB) = ABCDEFGHabcdefghIJKLMNOPijklmnop
|
||||
// name_color_checksum = 000000000ijklmabcdeIJKLM00000000 ^ xxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyy
|
||||
uint32_t xbrgx95558 = ((name_color << 15) & 0x007C0000) | ((name_color >> 6) & 0x0003E000) | ((name_color >> 3) & 0x00001F00);
|
||||
uint32_t mask = (x << 24) | (y << 16) | (x << 8) | y;
|
||||
return xbrgx95558 ^ mask;
|
||||
@@ -44,7 +44,7 @@ void PlayerVisualConfig::compute_name_color_checksum() {
|
||||
this->name_color_checksum = this->compute_name_color_checksum(this->name_color);
|
||||
}
|
||||
|
||||
void PlayerDispDataDCPCV3::enforce_lobby_join_limits_for_client(shared_ptr<Client> c) {
|
||||
void PlayerVisualConfig::enforce_lobby_join_limits_for_version(Version v) {
|
||||
struct ClassMaxes {
|
||||
uint16_t costume;
|
||||
uint16_t skin;
|
||||
@@ -90,9 +90,9 @@ void PlayerDispDataDCPCV3::enforce_lobby_join_limits_for_client(shared_ptr<Clien
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000}};
|
||||
|
||||
const ClassMaxes* maxes;
|
||||
if (is_v1_or_v2(c->version())) {
|
||||
if (is_v1_or_v2(v)) {
|
||||
// V1/V2 have fewer classes, so we'll substitute some here
|
||||
switch (this->visual.char_class) {
|
||||
switch (this->char_class) {
|
||||
case 0: // HUmar
|
||||
case 1: // HUnewearl
|
||||
case 2: // HUcast
|
||||
@@ -106,53 +106,65 @@ void PlayerDispDataDCPCV3::enforce_lobby_join_limits_for_client(shared_ptr<Clien
|
||||
case 13: // V3 custom 2
|
||||
break;
|
||||
case 9: // HUcaseal
|
||||
this->visual.char_class = 5; // HUcaseal -> RAcaseal
|
||||
this->char_class = 5; // HUcaseal -> RAcaseal
|
||||
break;
|
||||
case 10: // FOmar
|
||||
this->visual.char_class = 0; // FOmar -> HUmar
|
||||
this->char_class = 0; // FOmar -> HUmar
|
||||
break;
|
||||
case 11: // RAmarl
|
||||
this->visual.char_class = 1; // RAmarl -> HUnewearl
|
||||
this->char_class = 1; // RAmarl -> HUnewearl
|
||||
break;
|
||||
case 14: // V2 custom 1 / V3 custom 3
|
||||
case 15: // V2 custom 2 / V3 custom 4
|
||||
case 16: // V2 custom 3 / V3 custom 5
|
||||
case 17: // V2 custom 4 / V3 custom 6
|
||||
case 18: // V2 custom 5 / V3 custom 7
|
||||
this->visual.char_class -= 5;
|
||||
this->char_class -= 5;
|
||||
break;
|
||||
default:
|
||||
this->visual.char_class = 0; // Invalid classes -> HUmar
|
||||
this->char_class = 0; // Invalid classes -> HUmar
|
||||
}
|
||||
|
||||
this->visual.version = min<uint8_t>(this->visual.version, is_v1(c->version()) ? 0 : 2);
|
||||
maxes = &v1_v2_class_maxes[this->visual.char_class];
|
||||
this->version = min<uint8_t>(this->version, is_v1(v) ? 0 : 2);
|
||||
maxes = &v1_v2_class_maxes[this->char_class];
|
||||
|
||||
} else {
|
||||
if (this->visual.char_class >= 19) {
|
||||
this->visual.char_class = 0; // Invalid classes -> HUmar
|
||||
if (this->char_class >= 19) {
|
||||
this->char_class = 0; // Invalid classes -> HUmar
|
||||
}
|
||||
this->visual.version = min<uint8_t>(this->visual.version, 3);
|
||||
maxes = &v3_v4_class_maxes[this->visual.char_class];
|
||||
this->version = min<uint8_t>(this->version, 3);
|
||||
maxes = &v3_v4_class_maxes[this->char_class];
|
||||
}
|
||||
|
||||
// V1/V2 has fewer costumes and android skins, so substitute them here
|
||||
this->visual.costume = maxes->costume ? (this->visual.costume % maxes->costume) : 0;
|
||||
this->visual.skin = maxes->skin ? (this->visual.skin % maxes->skin) : 0;
|
||||
this->visual.face = maxes->face ? (this->visual.face % maxes->face) : 0;
|
||||
this->visual.head = maxes->head ? (this->visual.head % maxes->head) : 0;
|
||||
this->visual.hair = maxes->hair ? (this->visual.hair % maxes->hair) : 0;
|
||||
this->costume = maxes->costume ? (this->costume % maxes->costume) : 0;
|
||||
this->skin = maxes->skin ? (this->skin % maxes->skin) : 0;
|
||||
this->face = maxes->face ? (this->face % maxes->face) : 0;
|
||||
this->head = maxes->head ? (this->head % maxes->head) : 0;
|
||||
this->hair = maxes->hair ? (this->hair % maxes->hair) : 0;
|
||||
|
||||
this->visual.compute_name_color_checksum();
|
||||
this->visual.class_flags = class_flags_for_class(this->visual.char_class);
|
||||
if (this->name_color == 0) {
|
||||
this->name_color = 0xFFFFFFFF;
|
||||
}
|
||||
if (is_v1_or_v2(v)) {
|
||||
this->compute_name_color_checksum();
|
||||
} else {
|
||||
this->name_color_checksum = 0;
|
||||
}
|
||||
this->class_flags = class_flags_for_class(this->char_class);
|
||||
|
||||
if (this->visual.name.at(0) == '\t' && (this->visual.name.at(1) == 'J' || this->visual.name.at(1) == 'E')) {
|
||||
this->visual.name.encode(this->visual.name.decode().substr(2));
|
||||
if (!is_v4(v) && (this->name.at(0) == '\t') && (this->name.at(1) == 'J' || this->name.at(1) == 'E')) {
|
||||
this->name.encode(this->name.decode().substr(2));
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerDispDataBB::enforce_lobby_join_limits_for_client(shared_ptr<Client> c) {
|
||||
if (!is_v4(c->version())) {
|
||||
void PlayerDispDataDCPCV3::enforce_lobby_join_limits_for_version(Version v) {
|
||||
this->visual.enforce_lobby_join_limits_for_version(v);
|
||||
}
|
||||
|
||||
void PlayerDispDataBB::enforce_lobby_join_limits_for_version(Version v) {
|
||||
this->visual.enforce_lobby_join_limits_for_version(v);
|
||||
if (!is_v4(v)) {
|
||||
throw logic_error("PlayerDispDataBB being sent to non-BB client");
|
||||
}
|
||||
this->play_time = 0;
|
||||
@@ -265,7 +277,7 @@ void PlayerLobbyDataXB::clear() {
|
||||
void PlayerLobbyDataBB::clear() {
|
||||
this->player_tag = 0;
|
||||
this->guild_card_number = 0;
|
||||
this->team_guild_card_number = 0;
|
||||
this->team_master_guild_card_number = 0;
|
||||
this->team_id = 0;
|
||||
this->unknown_a1.clear(0);
|
||||
this->client_id = 0;
|
||||
@@ -438,10 +450,10 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge<false>() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PlayerBank::add_item(const ItemData& item) {
|
||||
uint32_t pid = item.primary_identifier();
|
||||
void PlayerBank::add_item(const ItemData& item, Version version) {
|
||||
uint32_t primary_identifier = item.primary_identifier();
|
||||
|
||||
if (pid == MESETA_IDENTIFIER) {
|
||||
if (primary_identifier == 0x04000000) {
|
||||
this->meseta += item.data2d;
|
||||
if (this->meseta > 999999) {
|
||||
this->meseta = 999999;
|
||||
@@ -449,11 +461,11 @@ void PlayerBank::add_item(const ItemData& item) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t combine_max = item.max_stack_size();
|
||||
size_t combine_max = item.max_stack_size(version);
|
||||
if (combine_max > 1) {
|
||||
size_t y;
|
||||
for (y = 0; y < this->num_items; y++) {
|
||||
if (this->items[y].data.primary_identifier() == item.primary_identifier()) {
|
||||
if (this->items[y].data.primary_identifier() == primary_identifier) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -474,17 +486,17 @@ void PlayerBank::add_item(const ItemData& item) {
|
||||
}
|
||||
auto& last_item = this->items[this->num_items];
|
||||
last_item.data = item;
|
||||
last_item.amount = (item.max_stack_size() > 1) ? item.data1[5] : 1;
|
||||
last_item.amount = (item.max_stack_size(version) > 1) ? item.data1[5] : 1;
|
||||
last_item.present = 1;
|
||||
this->num_items++;
|
||||
}
|
||||
|
||||
ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount) {
|
||||
ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount, Version version) {
|
||||
size_t index = this->find_item(item_id);
|
||||
auto& bank_item = this->items[index];
|
||||
|
||||
ItemData ret;
|
||||
if (amount && (bank_item.data.stack_size() > 1) && (amount < bank_item.data.data1[5])) {
|
||||
if (amount && (bank_item.data.stack_size(version) > 1) && (amount < bank_item.data.data1[5])) {
|
||||
ret = bank_item.data;
|
||||
ret.data1[5] = amount;
|
||||
bank_item.data.data1[5] -= amount;
|
||||
@@ -563,11 +575,11 @@ bool PlayerInventory::has_equipped_item(EquipSlot slot) const {
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerInventory::equip_item_id(uint32_t item_id, EquipSlot slot) {
|
||||
this->equip_item_index(this->find_item(item_id), slot);
|
||||
void PlayerInventory::equip_item_id(uint32_t item_id, EquipSlot slot, bool allow_overwrite) {
|
||||
this->equip_item_index(this->find_item(item_id), slot, allow_overwrite);
|
||||
}
|
||||
|
||||
void PlayerInventory::equip_item_index(size_t index, EquipSlot slot) {
|
||||
void PlayerInventory::equip_item_index(size_t index, EquipSlot slot, bool allow_overwrite) {
|
||||
auto& item = this->items[index];
|
||||
|
||||
if (slot == EquipSlot::UNKNOWN) {
|
||||
@@ -578,7 +590,11 @@ void PlayerInventory::equip_item_index(size_t index, EquipSlot slot) {
|
||||
throw runtime_error("incorrect item type for equip slot");
|
||||
}
|
||||
if (this->has_equipped_item(slot)) {
|
||||
throw runtime_error("equip slot is already in use");
|
||||
if (allow_overwrite) {
|
||||
this->unequip_item_slot(slot);
|
||||
} else {
|
||||
throw runtime_error("equip slot is already in use");
|
||||
}
|
||||
}
|
||||
|
||||
item.flags |= 0x00000008;
|
||||
@@ -643,7 +659,8 @@ void PlayerInventory::decode_from_client(shared_ptr<Client> c) {
|
||||
}
|
||||
|
||||
void PlayerInventory::encode_for_client(shared_ptr<Client> c) {
|
||||
if (c->version() == Version::DC_NTE) {
|
||||
Version v = c->version();
|
||||
if (v == Version::DC_NTE) {
|
||||
// DC NTE has the item count as a 32-bit value here, whereas every other
|
||||
// version uses a single byte. To stop DC NTE from crashing by trying to
|
||||
// construct far more than 30 TItem objects, we clear the fields DC NTE
|
||||
@@ -652,7 +669,7 @@ void PlayerInventory::encode_for_client(shared_ptr<Client> c) {
|
||||
this->hp_from_materials = 0;
|
||||
this->tp_from_materials = 0;
|
||||
this->language = 0;
|
||||
} else if ((c->version() != Version::PC_NTE) && (c->version() != Version::PC_V2)) {
|
||||
} else if ((v != Version::PC_NTE) && (v != Version::PC_V2)) {
|
||||
if (this->language > 4) {
|
||||
this->language = 0;
|
||||
}
|
||||
@@ -662,9 +679,11 @@ void PlayerInventory::encode_for_client(shared_ptr<Client> c) {
|
||||
}
|
||||
}
|
||||
|
||||
auto item_parameter_table = c->require_server_state()->item_parameter_table_for_version(c->version());
|
||||
// For pre-V2 clients, use the V2 parameter table, since the V1 table doesn't
|
||||
// have correct encodings for backward-compatible V2 items.
|
||||
auto item_parameter_table = c->require_server_state()->item_parameter_table_for_encode(v);
|
||||
for (size_t z = 0; z < this->items.size(); z++) {
|
||||
this->items[z].data.encode_for_version(c->version(), item_parameter_table);
|
||||
this->items[z].data.encode_for_version(v, item_parameter_table);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,7 +715,7 @@ BattleRules::BattleRules(const JSON& json) {
|
||||
this->tool_mode = json.get_enum("ToolMode", this->tool_mode);
|
||||
this->trap_mode = json.get_enum("TrapMode", this->trap_mode);
|
||||
this->unused_F817 = json.get_int("UnusedF817", this->unused_F817);
|
||||
this->respawn_mode = json.get_int("RespawnMode", this->respawn_mode);
|
||||
this->respawn_mode = json.get_enum("RespawnMode", this->respawn_mode);
|
||||
this->replace_char = json.get_int("ReplaceChar", this->replace_char);
|
||||
this->drop_weapon = json.get_int("DropWeapon", this->drop_weapon);
|
||||
this->is_teams = json.get_int("IsTeams", this->is_teams);
|
||||
@@ -898,25 +917,34 @@ BattleRules::MesetaMode enum_for_name<BattleRules::MesetaMode>(const char* name)
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
const char* name_for_enum<BattleRules::RespawnMode>(BattleRules::RespawnMode v) {
|
||||
switch (v) {
|
||||
case BattleRules::RespawnMode::ALLOW:
|
||||
return "ALLOW";
|
||||
case BattleRules::RespawnMode::FORBID:
|
||||
return "FORBID";
|
||||
case BattleRules::RespawnMode::LIMIT_LIVES:
|
||||
return "LIMIT_LIVES";
|
||||
default:
|
||||
throw invalid_argument("invalid BattleRules::MesetaDropMode value");
|
||||
}
|
||||
}
|
||||
template <>
|
||||
BattleRules::RespawnMode enum_for_name<BattleRules::RespawnMode>(const char* name) {
|
||||
if (!strcmp(name, "ALLOW")) {
|
||||
return BattleRules::RespawnMode::ALLOW;
|
||||
} else if (!strcmp(name, "FORBID")) {
|
||||
return BattleRules::RespawnMode::FORBID;
|
||||
} else if (!strcmp(name, "LIMIT_LIVES")) {
|
||||
return BattleRules::RespawnMode::LIMIT_LIVES;
|
||||
} else {
|
||||
throw invalid_argument("invalid BattleRules::MesetaDropMode name");
|
||||
}
|
||||
}
|
||||
|
||||
static PlayerInventoryItem make_template_item(bool equipped, uint64_t first_data, uint64_t second_data) {
|
||||
PlayerInventoryItem ret(ItemData(), equipped);
|
||||
ret.data.data1[0] = first_data >> 56;
|
||||
ret.data.data1[1] = first_data >> 48;
|
||||
ret.data.data1[2] = first_data >> 40;
|
||||
ret.data.data1[3] = first_data >> 32;
|
||||
ret.data.data1[4] = first_data >> 24;
|
||||
ret.data.data1[5] = first_data >> 16;
|
||||
ret.data.data1[6] = first_data >> 8;
|
||||
ret.data.data1[7] = first_data >> 0;
|
||||
ret.data.data1[8] = second_data >> 56;
|
||||
ret.data.data1[9] = second_data >> 48;
|
||||
ret.data.data1[10] = second_data >> 40;
|
||||
ret.data.data1[11] = second_data >> 32;
|
||||
ret.data.data2[0] = second_data >> 24;
|
||||
ret.data.data2[1] = second_data >> 16;
|
||||
ret.data.data2[2] = second_data >> 8;
|
||||
ret.data.data2[3] = second_data >> 0;
|
||||
return ret;
|
||||
return PlayerInventoryItem(ItemData(first_data, second_data), equipped);
|
||||
}
|
||||
|
||||
static PlayerInventoryItem v2_item(bool equipped, uint64_t first_data, uint64_t second_data) {
|
||||
|
||||
+45
-33
@@ -14,6 +14,7 @@
|
||||
#include "FileContentsCache.hh"
|
||||
#include "ItemData.hh"
|
||||
#include "LevelTable.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
@@ -82,8 +83,8 @@ struct PlayerInventory {
|
||||
|
||||
size_t find_equipped_item(EquipSlot slot) const;
|
||||
bool has_equipped_item(EquipSlot slot) const;
|
||||
void equip_item_id(uint32_t item_id, EquipSlot slot);
|
||||
void equip_item_index(size_t index, EquipSlot slot);
|
||||
void equip_item_id(uint32_t item_id, EquipSlot slot, bool allow_overwrite);
|
||||
void equip_item_index(size_t index, EquipSlot slot, bool allow_overwrite);
|
||||
void unequip_item_id(uint32_t item_id);
|
||||
void unequip_item_slot(EquipSlot slot);
|
||||
void unequip_item_index(size_t index);
|
||||
@@ -100,8 +101,8 @@ struct PlayerBank {
|
||||
/* 0008 */ parray<PlayerBankItem, 200> items;
|
||||
/* 12C8 */
|
||||
|
||||
void add_item(const ItemData& item);
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount);
|
||||
void add_item(const ItemData& item, Version version);
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount, Version version);
|
||||
size_t find_item(uint32_t item_id);
|
||||
|
||||
void sort();
|
||||
@@ -116,8 +117,11 @@ struct PlayerVisualConfig {
|
||||
/* 18 */ le_uint32_t name_color = 0xFFFFFFFF; // ARGB
|
||||
/* 1C */ uint8_t extra_model = 0;
|
||||
/* 1D */ parray<uint8_t, 0x0F> unused;
|
||||
// See compute_name_color_checksum for details on how this is computed. This
|
||||
// field is ignored on V3.
|
||||
// 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
|
||||
// Xbox), if this has a nonzero value, the "Change Name" option appears in the
|
||||
// character selection menu.
|
||||
/* 2C */ le_uint32_t name_color_checksum = 0;
|
||||
/* 30 */ uint8_t section_id = 0;
|
||||
/* 31 */ uint8_t char_class = 0;
|
||||
@@ -149,6 +153,8 @@ struct PlayerVisualConfig {
|
||||
|
||||
static uint32_t compute_name_color_checksum(uint32_t name_color);
|
||||
void compute_name_color_checksum();
|
||||
|
||||
void enforce_lobby_join_limits_for_version(Version v);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerDispDataDCPCV3 {
|
||||
@@ -158,7 +164,7 @@ struct PlayerDispDataDCPCV3 {
|
||||
/* BC */ parray<uint8_t, 0x14> technique_levels_v1;
|
||||
/* D0 */
|
||||
|
||||
void enforce_lobby_join_limits_for_client(std::shared_ptr<Client> c);
|
||||
void enforce_lobby_join_limits_for_version(Version v);
|
||||
PlayerDispDataBB to_bb(uint8_t to_language, uint8_t from_language) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
@@ -184,7 +190,7 @@ struct PlayerDispDataBB {
|
||||
/* 017C */ parray<uint8_t, 0x14> technique_levels_v1;
|
||||
/* 0190 */
|
||||
|
||||
void enforce_lobby_join_limits_for_client(std::shared_ptr<Client> c);
|
||||
void enforce_lobby_join_limits_for_version(Version v);
|
||||
PlayerDispDataDCPCV3 to_dcpcv3(uint8_t to_language, uint8_t from_language) const;
|
||||
PlayerDispDataBBPreview to_preview() const;
|
||||
void apply_preview(const PlayerDispDataBBPreview&);
|
||||
@@ -283,24 +289,26 @@ struct PlayerLobbyDataDCGC {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct XBNetworkLocation {
|
||||
le_uint32_t internal_ipv4_address = 0x0A0A0A0A;
|
||||
le_uint32_t external_ipv4_address = 0x23232323;
|
||||
le_uint16_t port = 9500;
|
||||
parray<uint8_t, 6> mac_address = 0x77;
|
||||
le_uint32_t unknown_a1;
|
||||
le_uint32_t unknown_a2;
|
||||
le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
|
||||
parray<le_uint32_t, 4> unknown_a3;
|
||||
/* 00 */ le_uint32_t internal_ipv4_address = 0x0A0A0A0A;
|
||||
/* 04 */ le_uint32_t external_ipv4_address = 0x23232323;
|
||||
/* 08 */ le_uint16_t port = 9500;
|
||||
/* 0A */ parray<uint8_t, 6> mac_address = 0x77;
|
||||
/* 10 */ le_uint32_t unknown_a1;
|
||||
/* 14 */ le_uint32_t unknown_a2;
|
||||
/* 18 */ le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
|
||||
/* 20 */ parray<le_uint32_t, 4> unknown_a3;
|
||||
/* 24 */
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerLobbyDataXB {
|
||||
le_uint32_t player_tag = 0;
|
||||
le_uint32_t guild_card_number = 0;
|
||||
XBNetworkLocation netloc;
|
||||
le_uint32_t client_id = 0;
|
||||
pstring<TextEncoding::ASCII, 0x10> name;
|
||||
/* 00 */ le_uint32_t player_tag = 0;
|
||||
/* 04 */ le_uint32_t guild_card_number = 0;
|
||||
/* 08 */ XBNetworkLocation netloc;
|
||||
/* 2C */ le_uint32_t client_id = 0;
|
||||
/* 30 */ pstring<TextEncoding::ASCII, 0x10> name;
|
||||
/* 40 */
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
@@ -308,7 +316,7 @@ struct PlayerLobbyDataXB {
|
||||
struct PlayerLobbyDataBB {
|
||||
/* 00 */ le_uint32_t player_tag = 0;
|
||||
/* 04 */ le_uint32_t guild_card_number = 0;
|
||||
/* 08 */ le_uint32_t team_guild_card_number = 0;
|
||||
/* 08 */ le_uint32_t team_master_guild_card_number = 0;
|
||||
/* 0C */ le_uint32_t team_id = 0;
|
||||
/* 10 */ parray<uint8_t, 0x0C> unknown_a1;
|
||||
/* 1C */ le_uint32_t client_id = 0;
|
||||
@@ -325,7 +333,7 @@ template <bool IsBigEndian>
|
||||
struct ChallengeAwardState {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
U32T rank_award_flags = 0;
|
||||
U32T maximum_rank = 0; // Encrypted; see decrypt_challenge_time
|
||||
ChallengeTime<IsBigEndian> maximum_rank;
|
||||
} __attribute__((packed));
|
||||
|
||||
template <TextEncoding UnencryptedEncoding, TextEncoding EncryptedEncoding>
|
||||
@@ -333,7 +341,7 @@ struct PlayerRecordsDCPC_Challenge {
|
||||
/* 00 */ le_uint16_t title_color = 0x7FFF;
|
||||
/* 02 */ parray<uint8_t, 2> unknown_u0;
|
||||
/* 04 */ pstring<EncryptedEncoding, 0x0C> rank_title;
|
||||
/* 10 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time. TODO: This might be offline times
|
||||
/* 10 */ parray<ChallengeTime<false>, 9> times_ep1_online; // TODO: This might be offline times
|
||||
/* 34 */ uint8_t grave_stage_num = 0;
|
||||
/* 35 */ uint8_t grave_floor = 0;
|
||||
/* 36 */ le_uint16_t grave_deaths = 0;
|
||||
@@ -351,7 +359,7 @@ struct PlayerRecordsDCPC_Challenge {
|
||||
/* 48 */ le_float grave_z = 0.0f;
|
||||
/* 4C */ pstring<UnencryptedEncoding, 0x14> grave_team;
|
||||
/* 60 */ pstring<UnencryptedEncoding, 0x18> grave_message;
|
||||
/* 78 */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times
|
||||
/* 78 */ parray<ChallengeTime<false>, 9> times_ep1_offline; // TODO: This might be online times
|
||||
/* 9C */ parray<uint8_t, 4> unknown_l4;
|
||||
/* A0 */
|
||||
} __attribute__((packed));
|
||||
@@ -373,9 +381,9 @@ struct PlayerRecordsV3_Challenge {
|
||||
struct Stats {
|
||||
/* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555
|
||||
/* 02:1E */ parray<uint8_t, 2> unknown_u0;
|
||||
/* 04:20 */ parray<U32T, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time
|
||||
/* 28:44 */ parray<U32T, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time
|
||||
/* 3C:58 */ parray<U32T, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time
|
||||
/* 04:20 */ parray<ChallengeTime<IsBigEndian>, 9> times_ep1_online;
|
||||
/* 28:44 */ parray<ChallengeTime<IsBigEndian>, 5> times_ep2_online;
|
||||
/* 3C:58 */ parray<ChallengeTime<IsBigEndian>, 9> times_ep1_offline;
|
||||
/* 60:7C */ uint8_t grave_is_ep2 = 0;
|
||||
/* 61:7D */ uint8_t grave_stage_num = 0;
|
||||
/* 62:7E */ uint8_t grave_floor = 0;
|
||||
@@ -412,9 +420,9 @@ struct PlayerRecordsV3_Challenge {
|
||||
struct PlayerRecordsBB_Challenge {
|
||||
/* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555
|
||||
/* 0002 */ parray<uint8_t, 2> unknown_u0;
|
||||
/* 0004 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time
|
||||
/* 0028 */ parray<le_uint32_t, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time
|
||||
/* 003C */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time
|
||||
/* 0004 */ parray<ChallengeTime<false>, 9> times_ep1_online;
|
||||
/* 0028 */ parray<ChallengeTime<false>, 5> times_ep2_online;
|
||||
/* 003C */ parray<ChallengeTime<false>, 9> times_ep1_offline;
|
||||
/* 0060 */ uint8_t grave_is_ep2 = 0;
|
||||
/* 0061 */ uint8_t grave_stage_num = 0;
|
||||
/* 0062 */ uint8_t grave_floor = 0;
|
||||
@@ -569,6 +577,11 @@ struct BattleRules {
|
||||
FORBID_ALL = 1,
|
||||
CLEAR_AND_ALLOW = 2,
|
||||
};
|
||||
enum class RespawnMode : uint8_t {
|
||||
ALLOW = 0,
|
||||
FORBID = 1,
|
||||
LIMIT_LIVES = 2,
|
||||
};
|
||||
|
||||
// Set by quest opcode F812, but values are remapped.
|
||||
// F812 00 => FORBID_ALL
|
||||
@@ -600,8 +613,7 @@ struct BattleRules {
|
||||
// F818 00 => 01
|
||||
// F818 01 => 00
|
||||
// F818 02 => 02
|
||||
// TODO: Define an enum class for this field.
|
||||
/* 06 */ uint8_t respawn_mode = 0;
|
||||
/* 06 */ RespawnMode respawn_mode = RespawnMode::ALLOW;
|
||||
// Set by quest opcode F819.
|
||||
/* 07 */ uint8_t replace_char = 0;
|
||||
// Set by quest opcode F81A, but value is inverted.
|
||||
|
||||
+170
-113
@@ -24,6 +24,7 @@
|
||||
#include <phosg/Time.hh>
|
||||
#ifdef HAVE_RESOURCE_FILE
|
||||
#include <resource_file/Emulators/PPC32Emulator.hh>
|
||||
#include <resource_file/Emulators/X86Emulator.hh>
|
||||
#endif
|
||||
|
||||
#include "ChatCommands.hh"
|
||||
@@ -311,7 +312,9 @@ static HandlerResult S_V123P_02_17(
|
||||
cmd.sub_version = ses->sub_version;
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
|
||||
cmd.access_key.encode(ses->license->access_key);
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
if (ses->version() != Version::GC_NTE) {
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
}
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
// TODO: We probably should set email_address, but we currently don't
|
||||
@@ -335,7 +338,9 @@ static HandlerResult S_V123P_02_17(
|
||||
cmd.language = ses->language();
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
|
||||
cmd.access_key.encode(ses->license->access_key);
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
if (ses->version() != Version::GC_NTE) {
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
}
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) {
|
||||
@@ -349,7 +354,7 @@ static HandlerResult S_V123P_02_17(
|
||||
break;
|
||||
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
if (command == 0x17) {
|
||||
C_VerifyLicense_V3_DB cmd;
|
||||
@@ -662,6 +667,7 @@ static HandlerResult S_B1(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
static HandlerResult S_B2(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t flag, string& data) {
|
||||
const auto& cmd = check_size_t<S_ExecuteCode_B2>(data, 0xFFFF);
|
||||
|
||||
@@ -707,33 +713,51 @@ static HandlerResult S_B2(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
ses->log.info("Wrote code from server to file %s", output_filename.c_str());
|
||||
|
||||
#ifdef HAVE_RESOURCE_FILE
|
||||
if (is_gc(ses->version())) {
|
||||
using FooterT = S_ExecuteCode_Footer_B2<IsBigEndian>;
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
// TODO: Support SH-4 disassembly too
|
||||
bool is_ppc = ::is_ppc(ses->version());
|
||||
bool is_x86 = ::is_x86(ses->version());
|
||||
if (is_ppc || is_x86) {
|
||||
try {
|
||||
if (code.size() < sizeof(S_ExecuteCode_Footer_GC_B2)) {
|
||||
if (code.size() < sizeof(FooterT)) {
|
||||
throw runtime_error("code section is too small");
|
||||
}
|
||||
|
||||
size_t footer_offset = code.size() - sizeof(S_ExecuteCode_Footer_GC_B2);
|
||||
size_t footer_offset = code.size() - sizeof(FooterT);
|
||||
|
||||
StringReader r(code.data(), code.size());
|
||||
const auto& footer = r.pget<S_ExecuteCode_Footer_GC_B2>(footer_offset);
|
||||
const auto& footer = r.pget<FooterT>(footer_offset);
|
||||
|
||||
multimap<uint32_t, string> labels;
|
||||
r.go(footer.relocations_offset);
|
||||
uint32_t reloc_offset = 0;
|
||||
for (size_t x = 0; x < footer.num_relocations; x++) {
|
||||
reloc_offset += (r.get_u16b() * 4);
|
||||
reloc_offset += (r.get<U16T>() * 4);
|
||||
labels.emplace(reloc_offset, string_printf("reloc%zu", x));
|
||||
}
|
||||
labels.emplace(footer.entrypoint_addr_offset.load(), "entry_ptr");
|
||||
labels.emplace(footer_offset, "footer");
|
||||
labels.emplace(r.pget_u32b(footer.entrypoint_addr_offset), "start");
|
||||
labels.emplace(r.pget<U32T>(footer.entrypoint_addr_offset), "start");
|
||||
|
||||
string disassembly = PPC32Emulator::disassemble(
|
||||
&r.pget<uint8_t>(0, code.size()),
|
||||
code.size(),
|
||||
0,
|
||||
&labels);
|
||||
string disassembly;
|
||||
if (is_ppc) {
|
||||
disassembly = PPC32Emulator::disassemble(
|
||||
&r.pget<uint8_t>(0, code.size()),
|
||||
code.size(),
|
||||
0,
|
||||
&labels);
|
||||
} else if (is_x86) {
|
||||
disassembly = X86Emulator::disassemble(
|
||||
&r.pget<uint8_t>(0, code.size()),
|
||||
code.size(),
|
||||
0,
|
||||
&labels);
|
||||
} else {
|
||||
// We shouldn't have entered the outer if statement if this happens
|
||||
throw logic_error("unsupported architecture");
|
||||
}
|
||||
|
||||
output_filename = string_printf("code.%" PRId64 ".txt", filename_timestamp);
|
||||
{
|
||||
@@ -814,7 +838,7 @@ constexpr on_command_t S_P_C4 = &S_C4<S_ChoiceSearchResultEntry_PC_C4>;
|
||||
constexpr on_command_t S_B_C4 = &S_C4<S_ChoiceSearchResultEntry_BB_C4>;
|
||||
|
||||
static HandlerResult S_G_E4(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
auto& cmd = check_size_t<S_CardBattleTableState_GC_Ep3_E4>(data);
|
||||
auto& cmd = check_size_t<S_CardBattleTableState_Ep3_E4>(data);
|
||||
bool modified = false;
|
||||
for (size_t x = 0; x < 4; x++) {
|
||||
if (cmd.entries[x].guild_card_number == ses->remote_guild_card_number) {
|
||||
@@ -936,9 +960,9 @@ static HandlerResult S_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_SAVE_FILES)) {
|
||||
if (is_ep3(ses->version()) && (data.size() >= 0x14)) {
|
||||
if (static_cast<uint8_t>(data[0]) == 0xB6) {
|
||||
const auto& header = check_size_t<G_MapSubsubcommand_GC_Ep3_6xB6>(data, 0xFFFF);
|
||||
const auto& header = check_size_t<G_MapSubsubcommand_Ep3_6xB6>(data, 0xFFFF);
|
||||
if (header.subsubcommand == 0x00000041) {
|
||||
const auto& cmd = check_size_t<G_MapData_GC_Ep3_6xB6x41>(data, 0xFFFF);
|
||||
const auto& cmd = check_size_t<G_MapData_Ep3_6xB6x41>(data, 0xFFFF);
|
||||
string filename = string_printf("map%08" PRIX32 ".%" PRIu64 ".mnmd",
|
||||
cmd.map_number.load(), now());
|
||||
string map_data = prs_decompress(
|
||||
@@ -963,25 +987,43 @@ static HandlerResult S_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
(static_cast<uint8_t>(data[0]) == 0xB4) ||
|
||||
(static_cast<uint8_t>(data[0]) == 0xB5))) {
|
||||
const auto& header = check_size_t<G_CardBattleCommandHeader>(data, 0xFFFF);
|
||||
if (header.mask_key) {
|
||||
if (header.mask_key && (ses->version() != Version::GC_EP3_NTE)) {
|
||||
set_mask_for_ep3_game_command(data.data(), data.size(), 0);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_EP3_INFINITE_TIME_ENABLED) && (header.subcommand == 0xB4)) {
|
||||
if (header.subsubcommand == 0x3D) {
|
||||
auto& cmd = check_size_t<G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D>(data);
|
||||
if (cmd.rules.overall_time_limit || cmd.rules.phase_time_limit) {
|
||||
cmd.rules.overall_time_limit = 0;
|
||||
cmd.rules.phase_time_limit = 0;
|
||||
modified = true;
|
||||
if (ses->version() == Version::GC_EP3_NTE) {
|
||||
auto& cmd = check_size_t<G_SetTournamentPlayerDecks_Ep3NTE_6xB4x3D>(data);
|
||||
if (cmd.rules.overall_time_limit || cmd.rules.phase_time_limit) {
|
||||
cmd.rules.overall_time_limit = 0;
|
||||
cmd.rules.phase_time_limit = 0;
|
||||
modified = true;
|
||||
}
|
||||
} else {
|
||||
auto& cmd = check_size_t<G_SetTournamentPlayerDecks_Ep3_6xB4x3D>(data);
|
||||
if (cmd.rules.overall_time_limit || cmd.rules.phase_time_limit) {
|
||||
cmd.rules.overall_time_limit = 0;
|
||||
cmd.rules.phase_time_limit = 0;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
} else if (header.subsubcommand == 0x05) {
|
||||
auto& cmd = check_size_t<G_UpdateMap_GC_Ep3_6xB4x05>(data);
|
||||
if (cmd.state.rules.overall_time_limit || cmd.state.rules.phase_time_limit) {
|
||||
cmd.state.rules.overall_time_limit = 0;
|
||||
cmd.state.rules.phase_time_limit = 0;
|
||||
modified = true;
|
||||
if (ses->version() == Version::GC_EP3_NTE) {
|
||||
auto& cmd = check_size_t<G_UpdateMap_Ep3NTE_6xB4x05>(data);
|
||||
if (cmd.state.rules.overall_time_limit || cmd.state.rules.phase_time_limit) {
|
||||
cmd.state.rules.overall_time_limit = 0;
|
||||
cmd.state.rules.phase_time_limit = 0;
|
||||
modified = true;
|
||||
}
|
||||
} else {
|
||||
auto& cmd = check_size_t<G_UpdateMap_Ep3_6xB4x05>(data);
|
||||
if (cmd.state.rules.overall_time_limit || cmd.state.rules.phase_time_limit) {
|
||||
cmd.state.rules.overall_time_limit = 0;
|
||||
cmd.state.rules.phase_time_limit = 0;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1015,6 +1057,10 @@ static HandlerResult S_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
|
||||
} else if (data[0] == 0x5F) {
|
||||
const auto& cmd = check_size_t<G_DropItem_DC_6x5F>(data, sizeof(G_DropItem_PC_V3_BB_6x5F));
|
||||
send_item_notification_if_needed(ses->require_server_state(), ses->client_channel, ses->config, cmd.item.item, true);
|
||||
|
||||
} else if ((data[0] == 0x60) && ses->next_drop_item.data1d[0] && !is_v4(ses->version())) {
|
||||
const auto& cmd = check_size_t<G_StandardDropItemRequest_DC_6x60>(
|
||||
data, sizeof(G_StandardDropItemRequest_PC_V3_BB_6x60));
|
||||
@@ -1040,7 +1086,7 @@ static HandlerResult S_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
if (data[4] == 0x1A) {
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
} else if (data[4] == 0x36) {
|
||||
const auto& cmd = check_size_t<G_RecreatePlayer_GC_Ep3_6xB5x36>(data);
|
||||
const auto& cmd = check_size_t<G_RecreatePlayer_Ep3_6xB5x36>(data);
|
||||
if (ses->is_in_game && (cmd.client_id >= 4)) {
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
@@ -1049,7 +1095,7 @@ static HandlerResult S_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
} else if ((static_cast<uint8_t>(data[0]) == 0xBD) &&
|
||||
ses->config.check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) &&
|
||||
is_ep3(ses->version())) {
|
||||
auto& cmd = check_size_t<G_WordSelectDuringBattle_GC_Ep3_6xBD>(data);
|
||||
auto& cmd = check_size_t<G_WordSelectDuringBattle_Ep3_6xBD>(data);
|
||||
if (cmd.private_flags & (1 << ses->lobby_client_id)) {
|
||||
cmd.private_flags &= ~(1 << ses->lobby_client_id);
|
||||
modified = true;
|
||||
@@ -1067,9 +1113,7 @@ static HandlerResult C_GXB_61(shared_ptr<ProxyServer::LinkedSession> ses, uint16
|
||||
|
||||
if (is_v4(ses->version())) {
|
||||
auto& pd = check_size_t<C_CharacterData_BB_61_98>(data, 0xFFFF);
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_CHAT_FILTER_ENABLED)) {
|
||||
pd.info_board.encode(add_color(pd.info_board.decode(ses->language())), ses->language());
|
||||
}
|
||||
pd.info_board.encode(add_color(pd.info_board.decode(ses->language())), ses->language());
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) {
|
||||
pd.disp.name.encode(" ", ses->language());
|
||||
modified = true;
|
||||
@@ -1083,14 +1127,14 @@ static HandlerResult C_GXB_61(shared_ptr<ProxyServer::LinkedSession> ses, uint16
|
||||
modified = true;
|
||||
}
|
||||
if (!ses->challenge_rank_title_override.empty()) {
|
||||
pd.records.challenge.title_color = encode_xrgb8888_to_xrgb1555(ses->challenge_rank_color_override);
|
||||
pd.records.challenge.title_color = encode_rgba8888_to_argb1555(ses->challenge_rank_color_override);
|
||||
pd.records.challenge.rank_title.encode(ses->challenge_rank_title_override, ses->language());
|
||||
}
|
||||
|
||||
} else {
|
||||
C_CharacterData_V3_61_98* pd;
|
||||
if (flag == 4) { // Episode 3
|
||||
auto& ep3_pd = check_size_t<C_CharacterData_GC_Ep3_61_98>(data);
|
||||
auto& ep3_pd = check_size_t<C_CharacterData_Ep3_61_98>(data);
|
||||
if (ep3_pd.ep3_config.is_encrypted) {
|
||||
decrypt_trivial_gci_data(
|
||||
&ep3_pd.ep3_config.card_counts,
|
||||
@@ -1102,15 +1146,13 @@ static HandlerResult C_GXB_61(shared_ptr<ProxyServer::LinkedSession> ses, uint16
|
||||
}
|
||||
pd = reinterpret_cast<C_CharacterData_V3_61_98*>(&ep3_pd);
|
||||
} else {
|
||||
if (is_ep3(ses->version())) {
|
||||
ses->log.info("Version changed to GC_EP3_TRIAL_EDITION");
|
||||
ses->set_version(Version::GC_EP3_TRIAL_EDITION);
|
||||
if (is_ep3(ses->version()) && (ses->version() != Version::GC_EP3_NTE)) {
|
||||
ses->log.info("Version changed to GC_EP3_NTE");
|
||||
ses->set_version(Version::GC_EP3_NTE);
|
||||
}
|
||||
pd = &check_size_t<C_CharacterData_V3_61_98>(data, 0xFFFF);
|
||||
}
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_CHAT_FILTER_ENABLED)) {
|
||||
pd->info_board.encode(add_color(pd->info_board.decode(ses->language())), ses->language());
|
||||
}
|
||||
pd->info_board.encode(add_color(pd->info_board.decode(ses->language())), ses->language());
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) {
|
||||
pd->disp.visual.name.encode(" ", ses->language());
|
||||
modified = true;
|
||||
@@ -1124,7 +1166,7 @@ static HandlerResult C_GXB_61(shared_ptr<ProxyServer::LinkedSession> ses, uint16
|
||||
modified = true;
|
||||
}
|
||||
if (!ses->challenge_rank_title_override.empty()) {
|
||||
pd->records.challenge.stats.title_color = encode_xrgb8888_to_xrgb1555(ses->challenge_rank_color_override);
|
||||
pd->records.challenge.stats.title_color = encode_rgba8888_to_argb1555(ses->challenge_rank_color_override);
|
||||
pd->records.challenge.rank_title.encode(ses->challenge_rank_title_override, ses->language());
|
||||
}
|
||||
}
|
||||
@@ -1132,28 +1174,24 @@ static HandlerResult C_GXB_61(shared_ptr<ProxyServer::LinkedSession> ses, uint16
|
||||
return modified ? HandlerResult::Type::MODIFIED : HandlerResult::Type::FORWARD;
|
||||
}
|
||||
|
||||
static HandlerResult C_GX_D9(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_CHAT_FILTER_ENABLED)) {
|
||||
data = add_color(data);
|
||||
// TODO: We should check if the info board text was actually modified and
|
||||
// return MODIFIED if so.
|
||||
}
|
||||
return HandlerResult::Type::FORWARD;
|
||||
static HandlerResult C_GX_D9(shared_ptr<ProxyServer::LinkedSession>, uint16_t, uint32_t, string& data) {
|
||||
data = add_color(data);
|
||||
// TODO: We should check if the info board text was actually modified and
|
||||
// return FORWARD if not.
|
||||
return HandlerResult::Type::MODIFIED;
|
||||
}
|
||||
|
||||
static HandlerResult C_B_D9(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_CHAT_FILTER_ENABLED)) {
|
||||
try {
|
||||
string decoded = tt_utf16_to_utf8(data.data(), data.size());
|
||||
add_color_inplace(decoded);
|
||||
data = tt_utf8_to_utf16(data.data(), data.size());
|
||||
} catch (const runtime_error& e) {
|
||||
ses->log.warning("Failed to replace escape characters in D9 command: %s", e.what());
|
||||
}
|
||||
// TODO: We should check if the info board text was actually modified and
|
||||
// return HandlerResult::MODIFIED if so.
|
||||
try {
|
||||
string decoded = tt_utf16_to_utf8(data.data(), data.size());
|
||||
add_color_inplace(decoded);
|
||||
data = tt_utf8_to_utf16(data.data(), data.size());
|
||||
} catch (const runtime_error& e) {
|
||||
ses->log.warning("Failed to decode and unescape D9 command: %s", e.what());
|
||||
}
|
||||
return HandlerResult::Type::FORWARD;
|
||||
// TODO: We should check if the info board text was actually modified and
|
||||
// return HandlerResult::FORWARD if not.
|
||||
return HandlerResult::Type::MODIFIED;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
@@ -1244,7 +1282,20 @@ static HandlerResult S_13_A7(shared_ptr<ProxyServer::LinkedSession> ses, uint16_
|
||||
if (sf->remaining_bytes == 0) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_SAVE_FILES)) {
|
||||
ses->log.info("Writing file %s => %s", sf->basename.c_str(), sf->output_filename.c_str());
|
||||
sf->write();
|
||||
string data = join(sf->blocks);
|
||||
if (sf->is_download && (ends_with(sf->basename, ".bin") || ends_with(sf->basename, ".dat") || ends_with(sf->basename, ".pvr"))) {
|
||||
data = decode_dlq_data(data);
|
||||
}
|
||||
save_file(sf->output_filename, data);
|
||||
if (ends_with(sf->basename, ".bin")) {
|
||||
try {
|
||||
string decompressed = prs_decompress(data);
|
||||
auto disassembly = disassemble_quest_script(decompressed.data(), decompressed.size(), ses->version(), ses->language(), false);
|
||||
save_file(sf->output_filename + ".txt", disassembly);
|
||||
} catch (const exception& e) {
|
||||
ses->log.warning("Failed to disassemble quest file: %s", e.what());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ses->log.info("Download complete for file %s", sf->basename.c_str());
|
||||
}
|
||||
@@ -1257,7 +1308,7 @@ static HandlerResult S_13_A7(shared_ptr<ProxyServer::LinkedSession> ses, uint16_
|
||||
static HandlerResult S_G_B7(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
if (is_ep3(ses->version())) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED)) {
|
||||
auto& cmd = check_size_t<S_RankUpdate_GC_Ep3_B7>(data);
|
||||
auto& cmd = check_size_t<S_RankUpdate_Ep3_B7>(data);
|
||||
if (cmd.current_meseta != 1000000) {
|
||||
cmd.current_meseta = 1000000;
|
||||
return HandlerResult::Type::MODIFIED;
|
||||
@@ -1299,7 +1350,7 @@ static HandlerResult S_G_B8(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t
|
||||
static HandlerResult S_G_B9(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_SAVE_FILES)) {
|
||||
try {
|
||||
const auto& header = check_size_t<S_UpdateMediaHeader_GC_Ep3_B9>(data, 0xFFFF);
|
||||
const auto& header = check_size_t<S_UpdateMediaHeader_Ep3_B9>(data, 0xFFFF);
|
||||
|
||||
if (data.size() - sizeof(header) < header.size) {
|
||||
throw runtime_error("Media data size extends beyond end of command; not saving file");
|
||||
@@ -1334,8 +1385,8 @@ static HandlerResult S_G_B9(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t
|
||||
static HandlerResult S_G_EF(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
if (is_ep3(ses->version())) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED)) {
|
||||
auto& cmd = check_size_t<S_StartCardAuction_GC_Ep3_EF>(data,
|
||||
offsetof(S_StartCardAuction_GC_Ep3_EF, unused), 0xFFFF);
|
||||
auto& cmd = check_size_t<S_StartCardAuction_Ep3_EF>(data,
|
||||
offsetof(S_StartCardAuction_Ep3_EF, unused), 0xFFFF);
|
||||
if (cmd.points_available != 0x7FFF) {
|
||||
cmd.points_available = 0x7FFF;
|
||||
return HandlerResult::Type::MODIFIED;
|
||||
@@ -1353,7 +1404,7 @@ static HandlerResult S_B_EF(shared_ptr<ProxyServer::LinkedSession>, uint16_t, ui
|
||||
|
||||
static HandlerResult S_G_BA(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED)) {
|
||||
auto& cmd = check_size_t<S_MesetaTransaction_GC_Ep3_BA>(data);
|
||||
auto& cmd = check_size_t<S_MesetaTransaction_Ep3_BA>(data);
|
||||
if (cmd.current_meseta != 1000000) {
|
||||
cmd.current_meseta = 1000000;
|
||||
return HandlerResult::Type::MODIFIED;
|
||||
@@ -1404,8 +1455,7 @@ static HandlerResult S_65_67_68_EB(shared_ptr<ProxyServer::LinkedSession> ses, u
|
||||
if (index >= ses->lobby_players.size()) {
|
||||
ses->log.warning("Ignoring invalid player index %zu at position %zu", index, x);
|
||||
} else {
|
||||
string name = entry.disp.visual.name.decode(entry.inventory.language);
|
||||
|
||||
string name = escape_player_name(entry.disp.visual.name.decode(entry.inventory.language));
|
||||
if (ses->license && (entry.lobby_data.guild_card_number == ses->remote_guild_card_number)) {
|
||||
entry.lobby_data.guild_card_number = ses->license->serial_number;
|
||||
num_replacements++;
|
||||
@@ -1449,16 +1499,16 @@ constexpr on_command_t S_B_65_67_68 = &S_65_67_68_EB<S_JoinLobby_BB_65_67_68>;
|
||||
template <typename CmdT>
|
||||
static HandlerResult S_64(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t flag, string& data) {
|
||||
CmdT* cmd;
|
||||
S_JoinGame_GC_Ep3_64* cmd_ep3 = nullptr;
|
||||
S_JoinGame_Ep3_64* cmd_ep3 = nullptr;
|
||||
if (ses->sub_version >= 0x40) {
|
||||
cmd = &check_size_t<CmdT>(data, sizeof(S_JoinGame_GC_Ep3_64));
|
||||
cmd_ep3 = &check_size_t<S_JoinGame_GC_Ep3_64>(data);
|
||||
cmd = &check_size_t<CmdT>(data, sizeof(S_JoinGame_Ep3_64));
|
||||
cmd_ep3 = &check_size_t<S_JoinGame_Ep3_64>(data);
|
||||
} else if (ses->version() == Version::XB_V3) {
|
||||
// Schtserv doesn't send the unknown_a1 field in this command, and we don't
|
||||
// use it here, so we allow it to be omitted.
|
||||
cmd = &check_size_t<CmdT>(data.data(), data.size(), sizeof(CmdT) - 0x18, sizeof(CmdT));
|
||||
} else {
|
||||
cmd = &check_size_t<CmdT>(data);
|
||||
cmd = &check_size_t<CmdT>(data, 0xFFFF);
|
||||
}
|
||||
|
||||
ses->clear_lobby_players(4);
|
||||
@@ -1513,7 +1563,7 @@ constexpr on_command_t S_X_64 = &S_64<S_JoinGame_XB_64>;
|
||||
constexpr on_command_t S_B_64 = &S_64<S_JoinGame_BB_64>;
|
||||
|
||||
static HandlerResult S_E8(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
auto& cmd = check_size_t<S_JoinSpectatorTeam_GC_Ep3_E8>(data);
|
||||
auto& cmd = check_size_t<S_JoinSpectatorTeam_Ep3_E8>(data);
|
||||
|
||||
ses->clear_lobby_players(12);
|
||||
ses->floor = 0;
|
||||
@@ -1582,9 +1632,10 @@ static HandlerResult S_66_69_E9(shared_ptr<ProxyServer::LinkedSession> ses, uint
|
||||
ses->log.warning("Lobby leave command references missing position");
|
||||
} else {
|
||||
auto& p = ses->lobby_players[index];
|
||||
string name = escape_player_name(p.name);
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED)) {
|
||||
send_text_message_printf(ses->client_channel, "$C4Leave: %zu/%" PRIu32 "\n%s",
|
||||
index, p.guild_card_number, p.name.c_str());
|
||||
index, p.guild_card_number, name.c_str());
|
||||
}
|
||||
p.guild_card_number = 0;
|
||||
p.name.clear();
|
||||
@@ -1614,16 +1665,21 @@ static HandlerResult C_06(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
strip_trailing_zeroes(text);
|
||||
|
||||
uint8_t private_flags = 0;
|
||||
if (uses_utf16(ses->version())) {
|
||||
if (text.size() & 1) {
|
||||
text.push_back(0);
|
||||
try {
|
||||
if (uses_utf16(ses->version())) {
|
||||
if (text.size() & 1) {
|
||||
text.push_back(0);
|
||||
}
|
||||
text = tt_decode_marked(text, ses->language(), true);
|
||||
} else if (!text.empty() && (text[0] != '\t') && is_ep3(ses->version())) {
|
||||
private_flags = text[0];
|
||||
text = tt_decode_marked(text.substr(1), ses->language(), false);
|
||||
} else {
|
||||
text = tt_decode_marked(text, ses->language(), false);
|
||||
}
|
||||
text = tt_decode_marked(text, ses->language(), true);
|
||||
} else if (!text.empty() && (text[0] != '\t') && is_ep3(ses->version())) {
|
||||
private_flags = text[0];
|
||||
text = tt_decode_marked(text.substr(1), ses->language(), false);
|
||||
} else {
|
||||
text = tt_decode_marked(text, ses->language(), false);
|
||||
} catch (const runtime_error& e) {
|
||||
ses->log.warning("Failed to decode and unescape chat text: %s", e.what());
|
||||
return HandlerResult::Type::FORWARD;
|
||||
}
|
||||
|
||||
if (text.empty()) {
|
||||
@@ -1638,21 +1694,13 @@ static HandlerResult C_06(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
offset += (text[offset] == command_sentinel) ? 0 : 2;
|
||||
text = text.substr(offset);
|
||||
if (text.size() >= 2 && text[1] == command_sentinel) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_CHAT_FILTER_ENABLED)) {
|
||||
send_chat_message_from_client(ses->server_channel, add_color(text.substr(1)), private_flags);
|
||||
} else {
|
||||
send_chat_message_from_client(ses->server_channel, text.substr(1), private_flags);
|
||||
}
|
||||
send_chat_message_from_client(ses->server_channel, text.substr(1), private_flags);
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
} else {
|
||||
on_chat_command(ses, text);
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
|
||||
} else if (ses->config.check_flag(Client::Flag::PROXY_CHAT_FILTER_ENABLED)) {
|
||||
send_chat_message_from_client(ses->server_channel, add_color(text), private_flags);
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
|
||||
} else {
|
||||
return HandlerResult::Type::FORWARD;
|
||||
}
|
||||
@@ -1723,6 +1771,11 @@ static HandlerResult C_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t c
|
||||
const auto& cmd = check_size_t<G_InterLevelWarp_6x21>(data);
|
||||
ses->floor = cmd.floor;
|
||||
|
||||
} else if (data[0] == 0x0C) {
|
||||
if (is_v1_or_v2(ses->version()) && ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
||||
send_remove_conditions(ses->client_channel, ses->lobby_client_id);
|
||||
send_remove_conditions(ses->server_channel, ses->lobby_client_id);
|
||||
}
|
||||
} else if (data[0] == 0x2F || data[0] == 0x4B || data[0] == 0x4C) {
|
||||
if (ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
||||
send_player_stats_change(ses->client_channel,
|
||||
@@ -1745,6 +1798,9 @@ static HandlerResult C_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t c
|
||||
send_player_stats_change(ses->server_channel,
|
||||
ses->lobby_client_id, PlayerStatsChange::ADD_TP, 255);
|
||||
}
|
||||
} else if (data[0] == 0x5F) {
|
||||
const auto& cmd = check_size_t<G_DropItem_DC_6x5F>(data, sizeof(G_DropItem_PC_V3_BB_6x5F));
|
||||
send_item_notification_if_needed(ses->require_server_state(), ses->client_channel, ses->config, cmd.item.item, true);
|
||||
}
|
||||
}
|
||||
return C_6x<void>(ses, command, flag, data);
|
||||
@@ -1790,9 +1846,10 @@ static HandlerResult C_V123_A0_A1(shared_ptr<ProxyServer::LinkedSession> ses, ui
|
||||
}
|
||||
|
||||
// Indexed as [command][version][is_from_client]
|
||||
static on_command_t handlers[0x100][14][2] = {
|
||||
static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the ProxyCommands handlers table");
|
||||
static on_command_t handlers[0x100][NUM_VERSIONS][2] = {
|
||||
// clang-format off
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 00 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 01 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}},
|
||||
/* 02 */ {{S_V123P_02_17, nullptr}, {S_V123P_02_17, nullptr}, {nullptr, nullptr}, {S_V123P_02_17, nullptr}, {S_V123P_02_17, nullptr}, {S_V123P_02_17, nullptr}, {S_V123P_02_17, nullptr}, {S_V123P_02_17, nullptr}, {S_V123P_02_17, nullptr}, {S_V123P_02_17, nullptr}, {S_V123P_02_17, nullptr}, {S_V123P_02_17, nullptr}, {S_V123P_02_17, nullptr}, {nullptr, nullptr}},
|
||||
@@ -1809,7 +1866,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* 0D */ {{nullptr, nullptr}, {nullptr, nullptr}, {nullptr, 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}},
|
||||
/* 0E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
/* 0F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 10 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 11 */ {{nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
/* 12 */ {{nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
@@ -1826,7 +1883,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* 1D */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_1D, C_1D}, {S_1D, C_1D}, {S_1D, C_1D}, {S_1D, C_1D}, {S_1D, C_1D}, {S_1D, C_1D}, {S_1D, C_1D}, {S_1D, C_1D}, {S_1D, C_1D}, {S_1D, C_1D}, {S_1D, C_1D}},
|
||||
/* 1E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 1F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 20 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 21 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 22 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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_B_22, nullptr}},
|
||||
@@ -1843,7 +1900,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* 2D */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 2E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 2F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 30 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 31 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 32 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
@@ -1860,7 +1917,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* 3D */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 3E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 3F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 40 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, C_40}, {S_invalid, C_40}, {S_invalid, C_40}, {S_invalid, C_40}, {S_invalid, C_40}, {S_invalid, C_40}, {S_invalid, C_40}, {S_invalid, C_40}, {S_invalid, C_40}, {S_invalid, C_40}, {S_invalid, C_40}},
|
||||
/* 41 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_DGX_41, nullptr}, {S_DGX_41, nullptr}, {S_DGX_41, nullptr}, {S_P_41, nullptr}, {S_P_41, nullptr}, {S_DGX_41, nullptr}, {S_DGX_41, nullptr}, {S_DGX_41, nullptr}, {S_DGX_41, nullptr}, {S_DGX_41, nullptr}, {S_B_41, nullptr}},
|
||||
/* 42 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
@@ -1877,7 +1934,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* 4D */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 4E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 4F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 50 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 51 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 52 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
@@ -1894,7 +1951,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* 5D */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 5E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 5F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 60 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_6x, C_D_6x}, {S_6x, C_D_6x}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_P_6x}, {S_6x, C_D_6x}, {S_6x, C_G_6x}, {S_6x, C_G_6x}, {S_6x, C_G_6x}, {S_6x, C_X_6x}, {S_6x, C_B_6x}},
|
||||
/* 61 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, C_GXB_61}, {S_invalid, C_GXB_61}, {S_invalid, C_GXB_61}, {S_invalid, C_GXB_61}, {S_invalid, C_GXB_61}},
|
||||
/* 62 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_6x, C_D_6x}, {S_6x, C_D_6x}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_P_6x}, {S_6x, C_D_6x}, {S_6x, C_G_6x}, {S_6x, C_G_6x}, {S_6x, C_G_6x}, {S_6x, C_X_6x}, {S_6x, C_B_6x}},
|
||||
@@ -1911,7 +1968,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* 6D */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_6x, C_D_6x}, {S_6x, C_D_6x}, {S_6x, C_D_6x}, {S_6x, C_P_6x}, {S_6x, C_P_6x}, {S_6x, C_D_6x}, {S_6x, C_G_6x}, {S_6x, C_G_6x}, {S_6x, C_G_6x}, {S_6x, C_X_6x}, {S_6x, C_B_6x}},
|
||||
/* 6E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 6F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 70 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 71 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 72 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
@@ -1928,7 +1985,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* 7D */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 7E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 7F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 80 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
/* 81 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_DGX_81, C_DGX_81}, {S_DGX_81, C_DGX_81}, {S_DGX_81, C_DGX_81}, {S_P_81, C_P_81}, {S_P_81, C_P_81}, {S_DGX_81, C_DGX_81}, {S_DGX_81, C_DGX_81}, {S_DGX_81, C_DGX_81}, {S_DGX_81, C_DGX_81}, {S_DGX_81, C_DGX_81}, {S_B_81, C_B_81}},
|
||||
/* 82 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
@@ -1945,7 +2002,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* 8D */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 8E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 8F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* 90 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
/* 91 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
/* 92 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
@@ -1962,7 +2019,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* 9D */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* 9E */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, C_G_9E}, {S_invalid, C_G_9E}, {S_invalid, C_G_9E}, {S_invalid, nullptr}, {S_invalid, nullptr}},
|
||||
/* 9F */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* A0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, C_V123_A0_A1},{nullptr, C_V123_A0_A1},{nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1},{nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1}, {nullptr, nullptr}},
|
||||
/* A1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, C_V123_A0_A1},{nullptr, C_V123_A0_A1},{nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1},{nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1}, {nullptr, C_V123_A0_A1}, {nullptr, nullptr}},
|
||||
/* A2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
@@ -1979,11 +2036,11 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* AD */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* AE */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* AF */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* B0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
/* B1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}},
|
||||
/* B2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_B2, nullptr}, {S_B2, nullptr}, {S_B2, nullptr}, {S_B2, nullptr}, {S_B2, nullptr}},
|
||||
/* B3 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}},
|
||||
/* B1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}, {S_B1, nullptr}},
|
||||
/* B2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_B2<false>, nullptr}, {S_B2<false>, nullptr}, {S_B2<false>, nullptr}, {S_B2<true>, nullptr}, {S_B2<true>, nullptr}, {S_B2<true>, nullptr}, {S_B2<true>, nullptr}, {S_B2<false>, nullptr}, {S_B2<false>, nullptr}},
|
||||
/* B3 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}, {S_invalid, C_B3}},
|
||||
/* B4 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* B5 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* B6 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
@@ -1996,7 +2053,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* BD */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* BE */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* BF */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* C0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
/* C1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* C2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
@@ -2013,7 +2070,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* CD */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* CE */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* CF */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* D0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* D1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
/* D2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
@@ -2030,7 +2087,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* DD */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}, {nullptr, nullptr}},
|
||||
/* DE */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}, {nullptr, nullptr}},
|
||||
/* DF */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* E0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}, {nullptr, nullptr}},
|
||||
/* E1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}, {nullptr, nullptr}},
|
||||
/* E2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}, {nullptr, nullptr}},
|
||||
@@ -2047,7 +2104,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* ED */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}, {nullptr, nullptr}},
|
||||
/* EE */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}, {nullptr, nullptr}},
|
||||
/* EF */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_EF, nullptr}, {S_G_EF, nullptr}, {S_G_EF, nullptr}, {S_invalid, nullptr}, {S_B_EF, nullptr}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
/* F0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}, {nullptr, nullptr}},
|
||||
/* F1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* F2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
@@ -2064,7 +2121,7 @@ static on_command_t handlers[0x100][14][2] = {
|
||||
/* FD */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* FE */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* FF */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_TRIAL_EDITION C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_V1_12_2000_PROTO C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
|
||||
+44
-43
@@ -43,20 +43,23 @@ ProxyServer::ProxyServer(
|
||||
state(state),
|
||||
next_unlicensed_session_id(0xFF00000000000001) {}
|
||||
|
||||
void ProxyServer::listen(uint16_t port, Version version, const struct sockaddr_storage* default_destination) {
|
||||
auto socket_obj = make_shared<ListeningSocket>(this, port, version, default_destination);
|
||||
auto l = this->listeners.emplace(port, socket_obj).first->second;
|
||||
void ProxyServer::listen(const std::string& addr, uint16_t port, Version version, const struct sockaddr_storage* default_destination) {
|
||||
auto socket_obj = make_shared<ListeningSocket>(this, addr, port, version, default_destination);
|
||||
if (!this->listeners.emplace(port, socket_obj).second) {
|
||||
throw runtime_error("duplicate port in proxy server configuration");
|
||||
}
|
||||
}
|
||||
|
||||
ProxyServer::ListeningSocket::ListeningSocket(
|
||||
ProxyServer* server,
|
||||
const std::string& addr,
|
||||
uint16_t port,
|
||||
Version version,
|
||||
const struct sockaddr_storage* default_destination)
|
||||
: server(server),
|
||||
log(string_printf("[ProxyServer:ListeningSocket:%hu] ", port), proxy_server_log.min_level),
|
||||
port(port),
|
||||
fd(::listen("", port, SOMAXCONN)),
|
||||
fd(::listen(addr, port, SOMAXCONN)),
|
||||
listener(nullptr, evconnlistener_free),
|
||||
version(version) {
|
||||
if (!this->fd.is_open()) {
|
||||
@@ -181,7 +184,7 @@ void ProxyServer::on_client_connect(
|
||||
case Version::PC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
uint32_t server_key = random_object<uint32_t>();
|
||||
@@ -275,7 +278,8 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
ses->channel.version = Version::DC_NTE;
|
||||
ses->log.info("Version changed to DC_NTE");
|
||||
const auto& cmd = check_size_t<C_Login_DCNTE_8B>(data, sizeof(C_LoginExtended_DCNTE_8B));
|
||||
ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
|
||||
ses->license = s->license_index->verify_v1_v2(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode());
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
ses->character_name = cmd.name.decode(ses->channel.language);
|
||||
@@ -284,7 +288,8 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
ses->channel.version = Version::DC_V1;
|
||||
ses->log.info("Version changed to DC_V1");
|
||||
const auto& cmd = check_size_t<C_LoginV1_DC_93>(data);
|
||||
ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
|
||||
ses->license = s->license_index->verify_v1_v2(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode());
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
ses->character_name = cmd.name.decode(ses->channel.language);
|
||||
@@ -294,11 +299,13 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
if (cmd.sub_version >= 0x30) {
|
||||
ses->log.info("Version changed to GC_NTE");
|
||||
ses->channel.version = Version::GC_NTE;
|
||||
ses->license = s->license_index->verify_gc(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
|
||||
ses->license = s->license_index->verify_gc_no_password(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode());
|
||||
} else { // DC V2
|
||||
ses->log.info("Version changed to DC_V2");
|
||||
ses->channel.version = Version::DC_V2;
|
||||
ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
|
||||
ses->license = s->license_index->verify_v1_v2(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode());
|
||||
}
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
@@ -316,7 +323,8 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
throw runtime_error("command is not 9D");
|
||||
}
|
||||
const auto& cmd = check_size_t<C_Login_DC_PC_GC_9D>(data, sizeof(C_LoginExtended_PC_9D));
|
||||
ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
|
||||
ses->license = s->license_index->verify_v1_v2(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode());
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
ses->character_name = cmd.name.decode(ses->channel.language);
|
||||
@@ -324,12 +332,13 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
}
|
||||
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
// We should only get a 9E while the session is unlinked
|
||||
if (command == 0x9E) {
|
||||
const auto& cmd = check_size_t<C_Login_GC_9E>(data, sizeof(C_LoginExtended_GC_9E));
|
||||
ses->license = s->license_index->verify_gc(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
|
||||
ses->license = s->license_index->verify_gc_no_password(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode());
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
ses->character_name = cmd.name.decode(ses->channel.language);
|
||||
@@ -372,7 +381,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
if (command != 0x93) {
|
||||
throw runtime_error("command is not 93");
|
||||
}
|
||||
const auto& cmd = check_size_t<C_Login_BB_93>(data);
|
||||
const auto& cmd = check_size_t<C_LoginBase_BB_93>(data, 0xFFFF);
|
||||
try {
|
||||
ses->license = s->license_index->verify_bb(cmd.username.decode(), cmd.password.decode());
|
||||
} catch (const LicenseIndex::missing_license&) {
|
||||
@@ -436,29 +445,25 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
|
||||
if (linked_ses.get()) {
|
||||
server->id_to_session.emplace(ses->license->serial_number, linked_ses);
|
||||
if (linked_ses->version() != ses->version()) {
|
||||
linked_ses->log.error("Linked session has different game version");
|
||||
} else {
|
||||
// Resume the linked session using the unlinked session
|
||||
try {
|
||||
if (ses->version() == Version::BB_V4) {
|
||||
linked_ses->resume(
|
||||
std::move(ses->channel),
|
||||
ses->detector_crypt,
|
||||
std::move(ses->login_command_bb));
|
||||
} else {
|
||||
linked_ses->resume(
|
||||
std::move(ses->channel),
|
||||
ses->detector_crypt,
|
||||
ses->sub_version,
|
||||
ses->character_name,
|
||||
ses->hardware_id,
|
||||
ses->xb_netloc,
|
||||
ses->xb_9E_unknown_a1a);
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
linked_ses->log.error("Failed to resume linked session: %s", e.what());
|
||||
// Resume the linked session using the unlinked session
|
||||
try {
|
||||
if (ses->version() == Version::BB_V4) {
|
||||
linked_ses->resume(
|
||||
std::move(ses->channel),
|
||||
ses->detector_crypt,
|
||||
std::move(ses->login_command_bb));
|
||||
} else {
|
||||
linked_ses->resume(
|
||||
std::move(ses->channel),
|
||||
ses->detector_crypt,
|
||||
ses->sub_version,
|
||||
ses->character_name,
|
||||
ses->hardware_id,
|
||||
ses->xb_netloc,
|
||||
ses->xb_9E_unknown_a1a);
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
linked_ses->log.error("Failed to resume linked session: %s", e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -676,14 +681,6 @@ ProxyServer::LinkedSession::SavingFile::SavingFile(
|
||||
is_download(is_download),
|
||||
remaining_bytes(remaining_bytes) {}
|
||||
|
||||
void ProxyServer::LinkedSession::SavingFile::write() const {
|
||||
string data = join(this->blocks);
|
||||
if (is_download && (ends_with(this->basename, ".bin") || ends_with(this->basename, ".dat"))) {
|
||||
data = decode_dlq_data(data);
|
||||
}
|
||||
save_file(this->output_filename, data);
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::dispatch_on_timeout(
|
||||
evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<LinkedSession*>(ctx)->on_timeout();
|
||||
@@ -910,6 +907,10 @@ void ProxyServer::destroy_sessions() {
|
||||
this->unlinked_sessions_to_destroy.clear();
|
||||
}
|
||||
|
||||
size_t ProxyServer::num_sessions() const {
|
||||
return this->id_to_session.size();
|
||||
}
|
||||
|
||||
size_t ProxyServer::delete_disconnected_sessions() {
|
||||
size_t count = 0;
|
||||
for (auto it = this->id_to_session.begin(); it != this->id_to_session.end();) {
|
||||
|
||||
+4
-3
@@ -26,7 +26,7 @@ public:
|
||||
std::shared_ptr<ServerState> state);
|
||||
virtual ~ProxyServer() = default;
|
||||
|
||||
void listen(uint16_t port, Version version, const struct sockaddr_storage* default_destination = nullptr);
|
||||
void listen(const std::string& addr, uint16_t port, Version version, const struct sockaddr_storage* default_destination = nullptr);
|
||||
|
||||
void connect_client(struct bufferevent* bev, uint16_t server_port);
|
||||
|
||||
@@ -108,8 +108,6 @@ public:
|
||||
const std::string& output_filename,
|
||||
size_t remaining_bytes,
|
||||
bool is_download);
|
||||
|
||||
void write() const;
|
||||
};
|
||||
std::unordered_map<std::string, SavingFile> saving_files;
|
||||
|
||||
@@ -190,6 +188,8 @@ public:
|
||||
void delete_session(uint64_t id);
|
||||
void delete_session(struct bufferevent* bev);
|
||||
|
||||
size_t num_sessions() const;
|
||||
|
||||
size_t delete_disconnected_sessions();
|
||||
|
||||
private:
|
||||
@@ -205,6 +205,7 @@ private:
|
||||
|
||||
ListeningSocket(
|
||||
ProxyServer* server,
|
||||
const std::string& addr,
|
||||
uint16_t port,
|
||||
Version version,
|
||||
const struct sockaddr_storage* default_destination);
|
||||
|
||||
+85
-65
@@ -222,6 +222,10 @@ VersionedQuest::VersionedQuest(
|
||||
available_expression(available_expression),
|
||||
enabled_expression(enabled_expression) {
|
||||
|
||||
if (this->dat_contents) {
|
||||
this->dat_contents_decompressed = make_shared<string>(prs_decompress(*this->dat_contents));
|
||||
}
|
||||
|
||||
auto bin_decompressed = prs_decompress(*this->bin_contents);
|
||||
|
||||
switch (this->version) {
|
||||
@@ -274,7 +278,7 @@ VersionedQuest::VersionedQuest(
|
||||
break;
|
||||
}
|
||||
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
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
|
||||
@@ -352,6 +356,14 @@ string VersionedQuest::dat_filename() const {
|
||||
}
|
||||
}
|
||||
|
||||
string VersionedQuest::pvr_filename() const {
|
||||
if (this->episode == Episode::EP3) {
|
||||
throw logic_error("Episode 3 quests do not have .pvr files");
|
||||
} else {
|
||||
return string_printf("quest%" PRIu32 ".pvr", this->quest_number);
|
||||
}
|
||||
}
|
||||
|
||||
string VersionedQuest::xb_filename() const {
|
||||
if (this->episode == Episode::EP3) {
|
||||
throw logic_error("Episode 3 quests do not have Xbox filenames");
|
||||
@@ -462,10 +474,14 @@ QuestIndex::QuestIndex(
|
||||
: directory(directory),
|
||||
category_index(category_index) {
|
||||
|
||||
map<string, shared_ptr<const string>> bin_files;
|
||||
map<string, shared_ptr<const string>> dat_files;
|
||||
map<string, shared_ptr<const string>> pvr_files;
|
||||
map<string, shared_ptr<const string>> json_files;
|
||||
struct FileData {
|
||||
std::string filename;
|
||||
shared_ptr<const string> data;
|
||||
};
|
||||
map<string, FileData> bin_files;
|
||||
map<string, FileData> dat_files;
|
||||
map<string, FileData> pvr_files;
|
||||
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
|
||||
@@ -474,13 +490,13 @@ QuestIndex::QuestIndex(
|
||||
continue;
|
||||
}
|
||||
|
||||
auto add_file = [&](map<string, shared_ptr<const string>>& files, const string& name, string&& value) {
|
||||
if (categories.emplace(name, cat->category_id).first->second != cat->category_id) {
|
||||
throw runtime_error("file " + name + " exists in multiple categories");
|
||||
auto add_file = [&](map<string, FileData>& files, const string& basename, const string& filename, string&& value) {
|
||||
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
|
||||
throw runtime_error("file " + basename + " exists in multiple categories");
|
||||
}
|
||||
auto data_ptr = make_shared<string>(std::move(value));
|
||||
if (!files.emplace(name, data_ptr).second) {
|
||||
throw runtime_error("file " + name + " already exists");
|
||||
if (!files.emplace(basename, FileData{filename, data_ptr}).second) {
|
||||
throw runtime_error("file " + basename + " already exists");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -490,8 +506,13 @@ QuestIndex::QuestIndex(
|
||||
continue;
|
||||
}
|
||||
for (string filename : list_directory_sorted(cat_path)) {
|
||||
if (filename == ".DS_Store") {
|
||||
continue;
|
||||
}
|
||||
|
||||
string file_path = cat_path + "/" + filename;
|
||||
try {
|
||||
string orig_filename = filename;
|
||||
string file_data;
|
||||
if (ends_with(filename, ".gci")) {
|
||||
file_data = decode_gci_data(load_file(file_path));
|
||||
@@ -523,26 +544,26 @@ QuestIndex::QuestIndex(
|
||||
}
|
||||
|
||||
if (extension == "json") {
|
||||
add_file(json_files, file_basename, std::move(file_data));
|
||||
add_file(json_files, file_basename, orig_filename, std::move(file_data));
|
||||
} else if (extension == "bin" || extension == "mnm") {
|
||||
add_file(bin_files, file_basename, std::move(file_data));
|
||||
add_file(bin_files, file_basename, orig_filename, std::move(file_data));
|
||||
} else if (extension == "bind" || extension == "mnmd") {
|
||||
add_file(bin_files, file_basename, prs_compress_optimal(file_data));
|
||||
add_file(bin_files, file_basename, orig_filename, prs_compress_optimal(file_data));
|
||||
} else if (extension == "dat") {
|
||||
add_file(dat_files, file_basename, std::move(file_data));
|
||||
add_file(dat_files, file_basename, orig_filename, std::move(file_data));
|
||||
} else if (extension == "datd") {
|
||||
add_file(dat_files, file_basename, prs_compress_optimal(file_data));
|
||||
add_file(dat_files, file_basename, orig_filename, prs_compress_optimal(file_data));
|
||||
} else if (extension == "pvr") {
|
||||
add_file(pvr_files, file_basename, std::move(file_data));
|
||||
add_file(pvr_files, file_basename, orig_filename, std::move(file_data));
|
||||
} else if (extension == "qst") {
|
||||
auto files = decode_qst_data(file_data);
|
||||
for (auto& it : files) {
|
||||
if (ends_with(it.first, ".bin")) {
|
||||
add_file(bin_files, file_basename, std::move(it.second));
|
||||
add_file(bin_files, file_basename, orig_filename, std::move(it.second));
|
||||
} else if (ends_with(it.first, ".dat")) {
|
||||
add_file(dat_files, file_basename, std::move(it.second));
|
||||
add_file(dat_files, file_basename, orig_filename, std::move(it.second));
|
||||
} else if (ends_with(it.first, ".pvr")) {
|
||||
add_file(pvr_files, file_basename, std::move(it.second));
|
||||
add_file(pvr_files, file_basename, orig_filename, std::move(it.second));
|
||||
} else {
|
||||
throw runtime_error("qst file contains unsupported file type: " + it.first);
|
||||
}
|
||||
@@ -562,7 +583,7 @@ QuestIndex::QuestIndex(
|
||||
// should be indexed
|
||||
for (auto& bin_it : bin_files) {
|
||||
const string& basename = bin_it.first;
|
||||
shared_ptr<const string> bin_contents = bin_it.second;
|
||||
const auto* bin_filedata = &bin_it.second;
|
||||
|
||||
try {
|
||||
// Quest .bin filenames are like K###-VERS-LANG.EXT, where:
|
||||
@@ -602,7 +623,7 @@ QuestIndex::QuestIndex(
|
||||
{"pc", Version::PC_V2},
|
||||
{"gcn", Version::GC_NTE},
|
||||
{"gc", Version::GC_V3},
|
||||
{"gc3t", Version::GC_EP3_TRIAL_EDITION},
|
||||
{"gc3t", Version::GC_EP3_NTE},
|
||||
{"gc3", Version::GC_EP3},
|
||||
{"xb", Version::XB_V3},
|
||||
{"bb", Version::BB_V4},
|
||||
@@ -616,31 +637,25 @@ QuestIndex::QuestIndex(
|
||||
uint8_t language = language_code_for_char(language_token[0]);
|
||||
|
||||
// Find the corresponding dat and pvr files
|
||||
string dat_filename;
|
||||
string pvr_filename;
|
||||
shared_ptr<const string> dat_contents;
|
||||
shared_ptr<const string> pvr_contents;
|
||||
const FileData* dat_filedata = nullptr;
|
||||
const FileData* pvr_filedata = nullptr;
|
||||
if (!::is_ep3(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_filename = basename;
|
||||
dat_contents = dat_files.at(dat_filename);
|
||||
dat_filedata = &dat_files.at(basename);
|
||||
} catch (const out_of_range&) {
|
||||
try {
|
||||
dat_filename = quest_number_token + "-" + version_token;
|
||||
dat_contents = dat_files.at(dat_filename);
|
||||
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);
|
||||
}
|
||||
}
|
||||
try {
|
||||
pvr_filename = basename;
|
||||
pvr_contents = pvr_files.at(pvr_filename);
|
||||
pvr_filedata = &pvr_files.at(basename);
|
||||
} catch (const out_of_range&) {
|
||||
try {
|
||||
pvr_filename = quest_number_token + "-" + version_token;
|
||||
pvr_contents = pvr_files.at(pvr_filename);
|
||||
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
|
||||
@@ -649,28 +664,25 @@ QuestIndex::QuestIndex(
|
||||
}
|
||||
|
||||
// Load the quest's metadata JSON file, if it exists
|
||||
string json_filename;
|
||||
JSON metadata_json = nullptr;
|
||||
const FileData* json_filedata = nullptr;
|
||||
shared_ptr<BattleRules> battle_rules;
|
||||
ssize_t challenge_template_index = -1;
|
||||
shared_ptr<const QuestAvailabilityExpression> available_expression;
|
||||
shared_ptr<const QuestAvailabilityExpression> enabled_expression;
|
||||
try {
|
||||
json_filename = basename;
|
||||
metadata_json = JSON::parse(*json_files.at(json_filename));
|
||||
json_filedata = &json_files.at(basename);
|
||||
} catch (const out_of_range&) {
|
||||
try {
|
||||
json_filename = quest_number_token + "-" + version_token;
|
||||
metadata_json = JSON::parse(*json_files.at(json_filename));
|
||||
json_filedata = &json_files.at(quest_number_token + "-" + version_token);
|
||||
} catch (const out_of_range&) {
|
||||
try {
|
||||
json_filename = quest_number_token;
|
||||
metadata_json = JSON::parse(*json_files.at(json_filename));
|
||||
json_filedata = &json_files.at(quest_number_token);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!metadata_json.is_null()) {
|
||||
if (json_filedata) {
|
||||
auto metadata_json = JSON::parse(*json_filedata->data);
|
||||
try {
|
||||
battle_rules = make_shared<BattleRules>(metadata_json.at("BattleRules"));
|
||||
} catch (const out_of_range&) {
|
||||
@@ -694,36 +706,41 @@ QuestIndex::QuestIndex(
|
||||
category_id,
|
||||
version,
|
||||
language,
|
||||
bin_contents,
|
||||
dat_contents,
|
||||
pvr_contents,
|
||||
bin_filedata->data,
|
||||
dat_filedata ? dat_filedata->data : nullptr,
|
||||
pvr_filedata ? pvr_filedata->data : nullptr,
|
||||
battle_rules,
|
||||
challenge_template_index,
|
||||
available_expression,
|
||||
enabled_expression);
|
||||
|
||||
auto category_name = this->category_index->at(vq->category_id)->name;
|
||||
string dat_str = dat_filename.empty() ? "" : (" with layout from " + dat_filename + ".dat");
|
||||
string battle_rules_str = battle_rules ? (" with battle rules from " + json_filename + ".json") : "";
|
||||
string challenge_template_str = (challenge_template_index >= 0) ? string_printf(" with challenge template index %zd", vq->challenge_template_index) : "";
|
||||
string filenames_str = bin_filedata->filename;
|
||||
if (dat_filedata) {
|
||||
filenames_str += string_printf("/%s", dat_filedata->filename.c_str());
|
||||
}
|
||||
if (pvr_filedata) {
|
||||
filenames_str += string_printf("/%s", pvr_filedata->filename.c_str());
|
||||
}
|
||||
if (json_filedata) {
|
||||
filenames_str += string_printf("/%s", json_filedata->filename.c_str());
|
||||
}
|
||||
auto q_it = this->quests_by_number.find(vq->quest_number);
|
||||
if (q_it != this->quests_by_number.end()) {
|
||||
q_it->second->add_version(vq);
|
||||
static_game_data_log.info("(%s) Added %s %c version of quest %" PRIu32 " (%s)%s%s%s",
|
||||
basename.c_str(),
|
||||
static_game_data_log.info("(%s) Added %s %c version of quest %" PRIu32 " (%s)",
|
||||
filenames_str.c_str(),
|
||||
name_for_enum(vq->version),
|
||||
char_for_language_code(vq->language),
|
||||
vq->quest_number,
|
||||
vq->name.c_str(),
|
||||
dat_str.c_str(),
|
||||
battle_rules_str.c_str(),
|
||||
challenge_template_str.c_str());
|
||||
vq->name.c_str());
|
||||
} 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.info("(%s) Created %s %c quest %" PRIu32 " (%s) (%s, %s (%" PRIu32 "), %s)%s%s%s",
|
||||
basename.c_str(),
|
||||
static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " (%s) (%s, %s (%" PRIu32 "), %s)",
|
||||
filenames_str.c_str(),
|
||||
name_for_enum(vq->version),
|
||||
char_for_language_code(vq->language),
|
||||
vq->quest_number,
|
||||
@@ -731,10 +748,7 @@ QuestIndex::QuestIndex(
|
||||
name_for_episode(vq->episode),
|
||||
category_name.c_str(),
|
||||
vq->category_id,
|
||||
vq->joinable ? "joinable" : "not joinable",
|
||||
dat_str.c_str(),
|
||||
battle_rules_str.c_str(),
|
||||
challenge_template_str.c_str());
|
||||
vq->joinable ? "joinable" : "not joinable");
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning("(%s) Failed to index quest file: (%s)", basename.c_str(), e.what());
|
||||
@@ -750,6 +764,14 @@ shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const Quest> QuestIndex::get(const std::string& name) const {
|
||||
try {
|
||||
return this->quests_by_name.at(name);
|
||||
} catch (const out_of_range&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories(
|
||||
QuestMenuType menu_type,
|
||||
Episode episode,
|
||||
@@ -897,9 +919,7 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
|
||||
auto dlq = make_shared<VersionedQuest>(*this);
|
||||
dlq->bin_contents = make_shared<string>(encode_download_quest_data(compressed_bin, decompressed_bin.size()));
|
||||
dlq->dat_contents = make_shared<string>(encode_download_quest_data(*this->dat_contents));
|
||||
if (this->pvr_contents) {
|
||||
dlq->pvr_contents = make_shared<string>(encode_download_quest_data(*this->pvr_contents));
|
||||
}
|
||||
dlq->pvr_contents = this->pvr_contents;
|
||||
dlq->is_dlq_encoded = true;
|
||||
return dlq;
|
||||
}
|
||||
@@ -954,7 +974,7 @@ string decode_gci_data(
|
||||
}
|
||||
|
||||
} else if (header.is_ep3()) {
|
||||
if (header.is_trial()) {
|
||||
if (header.is_nte()) {
|
||||
if (known_seed >= 0) {
|
||||
return decrypt_download_quest_data_section<true>(
|
||||
r.getv(header.data_size), header.data_size, known_seed, true, true);
|
||||
@@ -1292,7 +1312,7 @@ string encode_qst_file(
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
for (const auto& it : files) {
|
||||
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
|
||||
|
||||
@@ -68,6 +68,7 @@ struct VersionedQuest {
|
||||
std::string long_description;
|
||||
std::shared_ptr<const std::string> bin_contents;
|
||||
std::shared_ptr<const std::string> dat_contents;
|
||||
std::shared_ptr<const std::string> dat_contents_decompressed;
|
||||
std::shared_ptr<const std::string> pvr_contents;
|
||||
std::shared_ptr<const BattleRules> battle_rules;
|
||||
ssize_t challenge_template_index;
|
||||
@@ -89,6 +90,7 @@ struct VersionedQuest {
|
||||
|
||||
std::string bin_filename() const;
|
||||
std::string dat_filename() const;
|
||||
std::string pvr_filename() const;
|
||||
std::string xb_filename() const;
|
||||
|
||||
std::shared_ptr<VersionedQuest> create_download_quest(uint8_t override_language = 0xFF) const;
|
||||
@@ -135,11 +137,13 @@ struct QuestIndex {
|
||||
std::shared_ptr<const QuestCategoryIndex> category_index;
|
||||
|
||||
std::map<uint32_t, std::shared_ptr<Quest>> quests_by_number;
|
||||
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);
|
||||
|
||||
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
|
||||
std::shared_ptr<const Quest> get(const std::string& name) const;
|
||||
|
||||
std::vector<std::shared_ptr<const QuestCategoryIndex::Category>> categories(
|
||||
QuestMenuType menu_type,
|
||||
|
||||
@@ -184,6 +184,33 @@ string QuestAvailabilityExpression::FlagLookupNode::str() const {
|
||||
return string_printf("F_%04hX", this->flag_index);
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode(
|
||||
Episode episode, uint8_t stage_index)
|
||||
: episode(episode),
|
||||
stage_index(stage_index) {}
|
||||
|
||||
bool QuestAvailabilityExpression::ChallengeCompletionLookupNode::operator==(const Node& other) const {
|
||||
try {
|
||||
const ChallengeCompletionLookupNode& other_cc = dynamic_cast<const ChallengeCompletionLookupNode&>(other);
|
||||
return other_cc.episode == this->episode && other_cc.stage_index == this->stage_index;
|
||||
} catch (const bad_cast&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::ChallengeCompletionLookupNode::evaluate(const Env& env) const {
|
||||
if (this->episode == Episode::EP1) {
|
||||
return env.challenge_records->times_ep1_online.at(this->stage_index).has_value();
|
||||
} else if (this->episode == Episode::EP2) {
|
||||
return env.challenge_records->times_ep2_online.at(this->stage_index).has_value();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::ChallengeCompletionLookupNode::str() const {
|
||||
return string_printf("CC_%s_%hhu", abbreviation_for_episode(this->episode), static_cast<uint8_t>(this->stage_index + 1));
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name)
|
||||
: reward_name(reward_name) {}
|
||||
|
||||
@@ -218,6 +245,20 @@ string QuestAvailabilityExpression::NumPlayersLookupNode::str() const {
|
||||
return "V_NumPlayers";
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::EventLookupNode::EventLookupNode() {}
|
||||
|
||||
bool QuestAvailabilityExpression::EventLookupNode::operator==(const Node& other) const {
|
||||
return dynamic_cast<const EventLookupNode*>(&other) != nullptr;
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::EventLookupNode::evaluate(const Env& env) const {
|
||||
return env.num_players;
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::EventLookupNode::str() const {
|
||||
return "V_Event";
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::ConstantNode::ConstantNode(bool value)
|
||||
: value(value) {}
|
||||
|
||||
@@ -343,12 +384,34 @@ unique_ptr<const QuestAvailabilityExpression::Node> QuestAvailabilityExpression:
|
||||
}
|
||||
return make_unique<FlagLookupNode>(flag);
|
||||
}
|
||||
if (text.starts_with("CC_")) {
|
||||
Episode episode;
|
||||
if (text.starts_with("CC_Ep1_")) {
|
||||
episode = Episode::EP1;
|
||||
} else if (text.starts_with("CC_Ep2_")) {
|
||||
episode = Episode::EP2;
|
||||
} else {
|
||||
throw runtime_error("invalid challenge episode");
|
||||
}
|
||||
char* endptr = nullptr;
|
||||
uint64_t stage_index = strtoul(text.data() + 7, &endptr, 0) - 1;
|
||||
if (endptr != text.data() + text.size()) {
|
||||
throw runtime_error("invalid challenge completion lookup token");
|
||||
}
|
||||
if ((episode == Episode::EP1 && stage_index > 8) || (episode == Episode::EP2 && stage_index > 4)) {
|
||||
throw runtime_error("invalid challenge stage index");
|
||||
}
|
||||
return make_unique<ChallengeCompletionLookupNode>(episode, stage_index);
|
||||
}
|
||||
if (text.starts_with("T_")) {
|
||||
return make_unique<TeamRewardLookupNode>(string(text.substr(2)));
|
||||
}
|
||||
if (text == "V_NumPlayers") {
|
||||
return make_unique<NumPlayersLookupNode>();
|
||||
}
|
||||
if (text == "V_Event") {
|
||||
return make_unique<EventLookupNode>();
|
||||
}
|
||||
|
||||
// Check for constants
|
||||
if (text == "true") {
|
||||
|
||||
@@ -17,8 +17,10 @@ class QuestAvailabilityExpression {
|
||||
public:
|
||||
struct Env {
|
||||
const QuestFlagsForDifficulty* flags;
|
||||
const PlayerRecordsBB_Challenge* challenge_records;
|
||||
std::shared_ptr<const TeamIndex::Team> team;
|
||||
size_t num_players;
|
||||
uint8_t event;
|
||||
};
|
||||
|
||||
QuestAvailabilityExpression(const std::string& text);
|
||||
@@ -115,6 +117,19 @@ protected:
|
||||
uint16_t flag_index;
|
||||
};
|
||||
|
||||
class ChallengeCompletionLookupNode : public Node {
|
||||
public:
|
||||
ChallengeCompletionLookupNode(Episode episode, uint8_t stage_index);
|
||||
virtual ~ChallengeCompletionLookupNode() = default;
|
||||
virtual bool operator==(const Node& other) const;
|
||||
virtual int64_t evaluate(const Env& env) const;
|
||||
virtual std::string str() const;
|
||||
|
||||
protected:
|
||||
Episode episode;
|
||||
uint8_t stage_index;
|
||||
};
|
||||
|
||||
class TeamRewardLookupNode : public Node {
|
||||
public:
|
||||
TeamRewardLookupNode(const std::string& reward_name);
|
||||
@@ -136,6 +151,15 @@ protected:
|
||||
virtual std::string str() const;
|
||||
};
|
||||
|
||||
class EventLookupNode : public Node {
|
||||
public:
|
||||
EventLookupNode();
|
||||
virtual ~EventLookupNode() = default;
|
||||
virtual bool operator==(const Node& other) const;
|
||||
virtual int64_t evaluate(const Env& env) const;
|
||||
virtual std::string str() const;
|
||||
};
|
||||
|
||||
class ConstantNode : public Node {
|
||||
public:
|
||||
ConstantNode(bool value);
|
||||
|
||||
+15
-16
@@ -189,6 +189,8 @@ constexpr uint16_t v_flag(Version v) {
|
||||
|
||||
using Arg = QuestScriptOpcodeDefinition::Argument;
|
||||
|
||||
static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the QuestScript flags and opcode definitions table");
|
||||
|
||||
static constexpr uint16_t F_PASS = 0x0001; // Version::PC_PATCH (unused for quests)
|
||||
static constexpr uint16_t F_ARGS = 0x0002; // Version::BB_PATCH (unused for quests)
|
||||
static constexpr uint16_t F_DC_NTE = 0x0004; // Version::DC_NTE
|
||||
@@ -199,7 +201,7 @@ static constexpr uint16_t F_PC_NTE = 0x0040; // Version::PC_NTE
|
||||
static constexpr uint16_t F_PC_V2 = 0x0080; // Version::PC_V2
|
||||
static constexpr uint16_t F_GC_NTE = 0x0100; // Version::GC_NTE
|
||||
static constexpr uint16_t F_GC_V3 = 0x0200; // Version::GC_V3
|
||||
static constexpr uint16_t F_GC_EP3TE = 0x0400; // Version::GC_EP3_TRIAL_EDITION
|
||||
static constexpr uint16_t F_GC_EP3TE = 0x0400; // Version::GC_EP3_NTE
|
||||
static constexpr uint16_t F_GC_EP3 = 0x0800; // Version::GC_EP3
|
||||
static constexpr uint16_t F_XB_V3 = 0x1000; // Version::XB_V3
|
||||
static constexpr uint16_t F_BB_V4 = 0x2000; // Version::BB_V4
|
||||
@@ -214,7 +216,7 @@ static_assert(F_PC_NTE == v_flag(Version::PC_NTE));
|
||||
static_assert(F_PC_V2 == v_flag(Version::PC_V2));
|
||||
static_assert(F_GC_NTE == v_flag(Version::GC_NTE));
|
||||
static_assert(F_GC_V3 == v_flag(Version::GC_V3));
|
||||
static_assert(F_GC_EP3TE == v_flag(Version::GC_EP3_TRIAL_EDITION));
|
||||
static_assert(F_GC_EP3TE == v_flag(Version::GC_EP3_NTE));
|
||||
static_assert(F_GC_EP3 == v_flag(Version::GC_EP3));
|
||||
static_assert(F_XB_V3 == v_flag(Version::XB_V3));
|
||||
static_assert(F_BB_V4 == v_flag(Version::BB_V4));
|
||||
@@ -277,11 +279,11 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
{0x0010, "set", {REG}, F_V0_V4}, // Sets a register to 1
|
||||
{0x0011, "clear", {REG}, F_V0_V4}, // Sets a register to 0
|
||||
{0x0012, "rev", {REG}, F_V0_V4}, // Sets a register to 0 if it's nonzero and vice versa
|
||||
{0x0013, "gset", {INT16}, F_V0_V4}, // Sets a global flag
|
||||
{0x0014, "gclear", {INT16}, F_V0_V4}, // Clears a global flag
|
||||
{0x0015, "grev", {INT16}, F_V0_V4}, // Flips a global flag
|
||||
{0x0016, "glet", {INT16, REG}, F_V0_V4}, // Sets a global flag to a specific value
|
||||
{0x0017, "gget", {INT16, REG}, F_V0_V4}, // Gets a global flag
|
||||
{0x0013, "gset", {INT16}, F_V0_V4}, // Sets a quest flag
|
||||
{0x0014, "gclear", {INT16}, F_V0_V4}, // Clears a quest flag
|
||||
{0x0015, "grev", {INT16}, F_V0_V4}, // Flips a quest flag
|
||||
{0x0016, "glet", {INT16, REG}, F_V0_V4}, // Sets a quest flag to a specific value
|
||||
{0x0017, "gget", {INT16, REG}, F_V0_V4}, // Gets a quest flag
|
||||
{0x0018, "add", {REG, REG}, F_V0_V4}, // regA += regB
|
||||
{0x0019, "addi", {REG, INT32}, F_V0_V4}, // regA += imm
|
||||
{0x001A, "sub", {REG, REG}, F_V0_V4}, // regA -= regB
|
||||
@@ -777,8 +779,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
{0xF922, "get_player_hp", {CLIENT_ID, {REG_SET_FIXED, 4}}, F_V3_V4 | F_ARGS},
|
||||
{0xF923, "get_floor_number", {CLIENT_ID, {REG_SET_FIXED, 2}}, F_V3_V4 | F_ARGS},
|
||||
{0xF924, "get_coord_player_detect", {{REG_SET_FIXED, 3}, {REG_SET_FIXED, 4}}, F_V3_V4},
|
||||
{0xF925, "read_global_flag", {INT32, REG}, F_V3_V4 | F_ARGS},
|
||||
{0xF926, "write_global_flag", {INT32, INT32}, F_V3_V4 | F_ARGS},
|
||||
{0xF925, "read_counter", {INT32, REG}, F_V3_V4 | F_ARGS},
|
||||
{0xF926, "write_counter", {INT32, INT32}, F_V3_V4 | F_ARGS},
|
||||
{0xF927, "item_detect_bank2", {{REG_SET_FIXED, 4}, REG}, F_V3_V4},
|
||||
{0xF928, "floor_player_detect", {{REG_SET_FIXED, 4}}, F_V3_V4},
|
||||
{0xF929, "prepare_gba_rom_from_disk", {CSTRING}, F_V3 | F_ARGS}, // Prepares to load a GBA ROM from a local GSL file
|
||||
@@ -948,7 +950,7 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
|
||||
}
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
const auto& header = r.get<PSOQuestHeaderGC>();
|
||||
@@ -1397,9 +1399,6 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
|
||||
if (size > 0) {
|
||||
lines.emplace_back();
|
||||
}
|
||||
if ((l->function_id == 0) && !reassembly_mode) {
|
||||
lines.emplace_back("start:");
|
||||
}
|
||||
if (reassembly_mode) {
|
||||
lines.emplace_back(string_printf("%s@0x%04" PRIX32 ":", l->name.c_str(), l->function_id));
|
||||
} else {
|
||||
@@ -1617,7 +1616,7 @@ Episode find_quest_episode_from_script(const void* data, size_t size, Version ve
|
||||
return Episode::EP1;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
const auto& header = r.get<PSOQuestHeaderGC>();
|
||||
@@ -1971,7 +1970,7 @@ std::string assemble_quest_script(const std::string& text) {
|
||||
case Version::DC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3:
|
||||
code_w.write(quest_language ? tt_utf8_to_8859(text) : tt_utf8_to_sjis(text));
|
||||
@@ -2245,7 +2244,7 @@ std::string assemble_quest_script(const std::string& text) {
|
||||
}
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
PSOQuestHeaderGC header;
|
||||
|
||||
+33
-31
@@ -17,7 +17,7 @@ string RareItemSet::ExpandedDrop::str() const {
|
||||
this->probability, frac.first, frac.second, this->item_code[0], this->item_code[1], this->item_code[2]);
|
||||
}
|
||||
|
||||
string RareItemSet::ExpandedDrop::str(Version version, shared_ptr<const ItemNameIndex> name_index) const {
|
||||
string RareItemSet::ExpandedDrop::str(shared_ptr<const ItemNameIndex> name_index) const {
|
||||
ItemData item;
|
||||
item.data1[0] = this->item_code[0];
|
||||
item.data1[1] = this->item_code[1];
|
||||
@@ -25,7 +25,7 @@ string RareItemSet::ExpandedDrop::str(Version version, shared_ptr<const ItemName
|
||||
|
||||
string ret = this->str();
|
||||
ret += " (";
|
||||
ret += name_index->describe_item(version, item);
|
||||
ret += name_index->describe_item(item);
|
||||
ret += ")";
|
||||
return ret;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ void RareItemSet::ParsedRELData::parse_t(StringReader r, bool is_v1) {
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
std::string RareItemSet::ParsedRELData::serialize_t() const {
|
||||
std::string RareItemSet::ParsedRELData::serialize_t(bool is_v1) const {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
|
||||
@@ -129,17 +129,23 @@ std::string RareItemSet::ParsedRELData::serialize_t() const {
|
||||
for (const auto& drop : this->monster_rares) {
|
||||
w.put(PackedDrop(drop));
|
||||
}
|
||||
while (w.size() < root.monster_rares_offset + 0x65 * sizeof(PackedDrop)) {
|
||||
while (w.size() < root.monster_rares_offset + (is_v1 ? 0x33 : 0x65) * sizeof(PackedDrop)) {
|
||||
w.put(empty_drop);
|
||||
}
|
||||
root.box_areas_offset = w.size();
|
||||
for (const auto& drop : this->box_rares) {
|
||||
w.put_u8(drop.area);
|
||||
}
|
||||
for (size_t z = this->box_rares.size(); z < 30; z++) {
|
||||
w.put_u8(0xFF);
|
||||
}
|
||||
root.box_rares_offset = w.size();
|
||||
for (const auto& drop : this->box_rares) {
|
||||
w.put(PackedDrop(drop.drop));
|
||||
}
|
||||
for (size_t z = this->box_rares.size(); z < 30; z++) {
|
||||
w.put_u32l(0x00000000);
|
||||
}
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
@@ -185,9 +191,7 @@ RareItemSet::ParsedRELData::ParsedRELData(const SpecCollection& collection) {
|
||||
throw runtime_error("monster spec cannot be converted to ItemRT format");
|
||||
}
|
||||
}
|
||||
if (!effective_spec.item_code.is_filled_with(0)) {
|
||||
this->monster_rares.emplace_back(specs.empty() ? ExpandedDrop() : specs[0]);
|
||||
}
|
||||
this->monster_rares.emplace_back(specs.empty() ? ExpandedDrop() : specs[0]);
|
||||
}
|
||||
|
||||
if (collection.box_area_to_specs.size() > 0xFF) {
|
||||
@@ -200,11 +204,11 @@ RareItemSet::ParsedRELData::ParsedRELData(const SpecCollection& collection) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string RareItemSet::ParsedRELData::serialize(bool big_endian) const {
|
||||
std::string RareItemSet::ParsedRELData::serialize(bool big_endian, bool is_v1) const {
|
||||
if (big_endian) {
|
||||
return this->serialize_t<true>();
|
||||
return this->serialize_t<true>(is_v1);
|
||||
} else {
|
||||
return this->serialize_t<false>();
|
||||
return this->serialize_t<false>(is_v1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +303,7 @@ RareItemSet::RareItemSet(const string& rel_data, bool is_big_endian) {
|
||||
}
|
||||
}
|
||||
|
||||
RareItemSet::RareItemSet(const JSON& json, Version version, shared_ptr<const ItemNameIndex> name_index) {
|
||||
RareItemSet::RareItemSet(const JSON& json, shared_ptr<const ItemNameIndex> name_index) {
|
||||
for (const auto& mode_it : json.as_dict()) {
|
||||
static const unordered_map<string, GameMode> mode_keys(
|
||||
{{"Normal", GameMode::NORMAL}, {"Battle", GameMode::BATTLE}, {"Challenge", GameMode::CHALLENGE}, {"Solo", GameMode::SOLO}});
|
||||
@@ -365,7 +369,7 @@ RareItemSet::RareItemSet(const JSON& json, Version version, shared_ptr<const Ite
|
||||
if (!name_index) {
|
||||
throw runtime_error("item name index is not available");
|
||||
}
|
||||
ItemData data = name_index->parse_item_description(version, item_desc.as_string());
|
||||
ItemData data = name_index->parse_item_description(item_desc.as_string());
|
||||
d.item_code[0] = data.data1[0];
|
||||
d.item_code[1] = data.data1[1];
|
||||
d.item_code[2] = data.data1[2];
|
||||
@@ -380,12 +384,12 @@ RareItemSet::RareItemSet(const JSON& json, Version version, shared_ptr<const Ite
|
||||
}
|
||||
}
|
||||
|
||||
std::string RareItemSet::serialize_afs() const {
|
||||
std::string RareItemSet::serialize_afs(bool is_v1) const {
|
||||
vector<string> files;
|
||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||
ParsedRELData rel(this->get_collection(GameMode::NORMAL, Episode::EP1, difficulty, section_id));
|
||||
files.emplace_back(rel.serialize(false));
|
||||
files.emplace_back(rel.serialize(false, is_v1));
|
||||
}
|
||||
}
|
||||
return AFSArchive::generate(files, false);
|
||||
@@ -401,7 +405,7 @@ std::string RareItemSet::serialize_gsl(bool big_endian) const {
|
||||
try {
|
||||
string filename = this->gsl_entry_name_for_table(GameMode::NORMAL, episode, difficulty, section_id);
|
||||
ParsedRELData rel(this->get_collection(GameMode::NORMAL, episode, difficulty, section_id));
|
||||
files.emplace(filename, rel.serialize(big_endian));
|
||||
files.emplace(filename, rel.serialize(big_endian, false));
|
||||
} catch (const out_of_range&) {
|
||||
// Collection does not exist; skip it
|
||||
}
|
||||
@@ -414,7 +418,7 @@ std::string RareItemSet::serialize_gsl(bool big_endian) const {
|
||||
try {
|
||||
string filename = this->gsl_entry_name_for_table(GameMode::CHALLENGE, Episode::EP1, difficulty, section_id);
|
||||
ParsedRELData rel(this->get_collection(GameMode::CHALLENGE, Episode::EP1, difficulty, section_id));
|
||||
files.emplace(filename, rel.serialize(big_endian));
|
||||
files.emplace(filename, rel.serialize(big_endian, false));
|
||||
} catch (const out_of_range&) {
|
||||
// Collection does not exist; skip it
|
||||
}
|
||||
@@ -423,7 +427,7 @@ std::string RareItemSet::serialize_gsl(bool big_endian) const {
|
||||
return GSLArchive::generate(files, big_endian);
|
||||
}
|
||||
|
||||
std::string RareItemSet::serialize_json(Version version, shared_ptr<const ItemNameIndex> name_index) const {
|
||||
std::string RareItemSet::serialize_json(shared_ptr<const ItemNameIndex> name_index) const {
|
||||
auto modes_dict = JSON::dict();
|
||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
||||
for (const auto& mode : modes) {
|
||||
@@ -442,19 +446,19 @@ std::string RareItemSet::serialize_json(Version version, shared_ptr<const ItemNa
|
||||
}
|
||||
|
||||
for (const auto& spec : this->get_enemy_specs(GameMode::NORMAL, episode, difficulty, section_id, rt_index)) {
|
||||
uint32_t primary_identifier = (spec.item_code[0] << 16) | (spec.item_code[1] << 8) | spec.item_code[2];
|
||||
if (primary_identifier == 0) {
|
||||
uint32_t data1_0_1_2 = (spec.item_code[0] << 16) | (spec.item_code[1] << 8) | spec.item_code[2];
|
||||
if (data1_0_1_2 == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto frac = reduce_fraction<uint64_t>(spec.probability, 0x100000000);
|
||||
auto spec_json = JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), primary_identifier});
|
||||
auto spec_json = JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), data1_0_1_2});
|
||||
if (name_index) {
|
||||
ItemData data;
|
||||
data.data1[0] = spec.item_code[0];
|
||||
data.data1[1] = spec.item_code[1];
|
||||
data.data1[2] = spec.item_code[2];
|
||||
spec_json.emplace_back(name_index->describe_item(version, data));
|
||||
spec_json.emplace_back(name_index->describe_item(data));
|
||||
}
|
||||
for (const auto& enemy_type : enemy_types) {
|
||||
if (enemy_type_valid_for_episode(episode, enemy_type)) {
|
||||
@@ -469,19 +473,19 @@ std::string RareItemSet::serialize_json(Version version, shared_ptr<const ItemNa
|
||||
auto area_list = JSON::list();
|
||||
|
||||
for (const auto& spec : this->get_box_specs(GameMode::NORMAL, episode, difficulty, section_id, area)) {
|
||||
uint32_t primary_identifier = (spec.item_code[0] << 16) | (spec.item_code[1] << 8) | spec.item_code[2];
|
||||
if (primary_identifier == 0) {
|
||||
uint32_t data1_0_1_2 = (spec.item_code[0] << 16) | (spec.item_code[1] << 8) | spec.item_code[2];
|
||||
if (data1_0_1_2 == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto frac = reduce_fraction<uint64_t>(spec.probability, 0x100000000);
|
||||
area_list.emplace_back(JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), std::move(primary_identifier)}));
|
||||
area_list.emplace_back(JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), data1_0_1_2}));
|
||||
if (name_index) {
|
||||
ItemData data;
|
||||
data.data1[0] = spec.item_code[0];
|
||||
data.data1[1] = spec.item_code[1];
|
||||
data.data1[2] = spec.item_code[2];
|
||||
area_list.back().emplace_back(name_index->describe_item(version, data));
|
||||
area_list.back().emplace_back(name_index->describe_item(data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,7 +512,6 @@ std::string RareItemSet::serialize_json(Version version, shared_ptr<const ItemNa
|
||||
|
||||
void RareItemSet::print_collection(
|
||||
FILE* stream,
|
||||
Version version,
|
||||
GameMode mode,
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
@@ -540,7 +543,7 @@ void RareItemSet::print_collection(
|
||||
}
|
||||
|
||||
for (const auto& spec : collection->rt_index_to_specs[z]) {
|
||||
string s = name_index ? spec.str(version, name_index) : spec.str();
|
||||
string s = name_index ? spec.str(name_index) : spec.str();
|
||||
fprintf(stream, " %02zX: %s (%s)\n", z, s.c_str(), enemy_types_str.c_str());
|
||||
}
|
||||
}
|
||||
@@ -548,14 +551,13 @@ void RareItemSet::print_collection(
|
||||
fprintf(stream, " Box rares:\n");
|
||||
for (size_t area = 0; area < collection->box_area_to_specs.size(); area++) {
|
||||
for (const auto& spec : collection->box_area_to_specs[area]) {
|
||||
string s = name_index ? spec.str(version, name_index) : spec.str();
|
||||
string s = name_index ? spec.str(name_index) : spec.str();
|
||||
fprintf(stream, " (area %02zX) %s\n", area, s.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RareItemSet::print_all_collections(
|
||||
FILE* stream, Version version, std::shared_ptr<const ItemNameIndex> name_index) const {
|
||||
void RareItemSet::print_all_collections(FILE* stream, std::shared_ptr<const ItemNameIndex> name_index) const {
|
||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
||||
for (GameMode mode : modes) {
|
||||
@@ -563,7 +565,7 @@ void RareItemSet::print_all_collections(
|
||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||
try {
|
||||
this->print_collection(stream, version, mode, episode, difficulty, section_id, name_index);
|
||||
this->print_collection(stream, mode, episode, difficulty, section_id, name_index);
|
||||
} catch (const out_of_range& e) {
|
||||
}
|
||||
}
|
||||
|
||||
+7
-8
@@ -22,32 +22,31 @@ public:
|
||||
parray<uint8_t, 3> item_code;
|
||||
|
||||
std::string str() const;
|
||||
std::string str(Version version, std::shared_ptr<const ItemNameIndex> name_index) const;
|
||||
std::string str(std::shared_ptr<const ItemNameIndex> name_index) const;
|
||||
};
|
||||
|
||||
RareItemSet();
|
||||
RareItemSet(const AFSArchive& afs, bool is_v1);
|
||||
RareItemSet(const GSLArchive& gsl, bool is_big_endian);
|
||||
RareItemSet(const std::string& rel, bool is_big_endian);
|
||||
RareItemSet(const JSON& json, Version version, std::shared_ptr<const ItemNameIndex> name_index = nullptr);
|
||||
RareItemSet(const JSON& json, std::shared_ptr<const ItemNameIndex> name_index = nullptr);
|
||||
~RareItemSet() = default;
|
||||
|
||||
std::vector<ExpandedDrop> get_enemy_specs(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t rt_index) const;
|
||||
std::vector<ExpandedDrop> get_box_specs(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t area) const;
|
||||
|
||||
std::string serialize_afs() const;
|
||||
std::string serialize_afs(bool is_v1) const;
|
||||
std::string serialize_gsl(bool big_endian) const;
|
||||
std::string serialize_json(Version version, std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
std::string serialize_json(std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
|
||||
void print_collection(
|
||||
FILE* stream,
|
||||
Version version,
|
||||
GameMode mode,
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
uint8_t section_id,
|
||||
std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
void print_all_collections(FILE* stream, Version version, std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
void print_all_collections(FILE* stream, std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
|
||||
protected:
|
||||
struct SpecCollection {
|
||||
@@ -86,12 +85,12 @@ protected:
|
||||
ParsedRELData() = default;
|
||||
ParsedRELData(StringReader r, bool big_endian, bool is_v1);
|
||||
explicit ParsedRELData(const SpecCollection& collection);
|
||||
std::string serialize(bool big_endian) const;
|
||||
std::string serialize(bool big_endian, bool is_v1) const;
|
||||
|
||||
template <bool IsBigEndian>
|
||||
void parse_t(StringReader r, bool is_v1);
|
||||
template <bool IsBigEndian>
|
||||
std::string serialize_t() const;
|
||||
std::string serialize_t(bool is_v1) const;
|
||||
|
||||
SpecCollection as_collection() const;
|
||||
};
|
||||
|
||||
+603
-383
File diff suppressed because it is too large
Load Diff
+1219
-675
File diff suppressed because it is too large
Load Diff
@@ -9,3 +9,10 @@
|
||||
|
||||
void on_subcommand_multi(std::shared_ptr<Client> c, uint8_t command, uint8_t flag, std::string& data);
|
||||
bool subcommand_is_implemented(uint8_t which);
|
||||
|
||||
void send_item_notification_if_needed(
|
||||
std::shared_ptr<ServerState> s,
|
||||
Channel& ch,
|
||||
const Client::Config& config,
|
||||
const ItemData& item,
|
||||
bool is_from_rare_table);
|
||||
|
||||
+39
-15
@@ -152,7 +152,7 @@ void ReplaySession::check_for_password(shared_ptr<const Event> ev) const {
|
||||
case Version::DC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
const auto& header = check_size_t<PSOCommandHeaderDCV3>(ev->data, 0xFFFF);
|
||||
@@ -203,7 +203,7 @@ void ReplaySession::check_for_password(shared_ptr<const Event> ev) const {
|
||||
if (header.command == 0x04) {
|
||||
check_pw(check_size_t<C_LegacyLogin_BB_04>(cmd_data, cmd_size).password.decode());
|
||||
} else if (header.command == 0x93) {
|
||||
check_pw(check_size_t<C_Login_BB_93>(cmd_data, cmd_size).password.decode());
|
||||
check_pw(check_size_t<C_LoginBase_BB_93>(cmd_data, cmd_size, 0xFFFF).password.decode());
|
||||
} else if (header.command == 0x9C) {
|
||||
check_pw(check_size_t<C_Register_BB_9C>(cmd_data, cmd_size).password.decode());
|
||||
} else if (header.command == 0x9E) {
|
||||
@@ -246,7 +246,7 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
|
||||
case Version::PC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
uint8_t command;
|
||||
@@ -298,13 +298,13 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
|
||||
auto& mask = check_size_t<S_JoinGame_DCNTE_64>(mask_data, mask_size);
|
||||
mask.variations.clear(0);
|
||||
} else {
|
||||
auto& mask = check_size_t<S_JoinGame_DC_64>(mask_data, mask_size, sizeof(S_JoinGame_GC_Ep3_64));
|
||||
auto& mask = check_size_t<S_JoinGame_DC_64>(mask_data, mask_size, sizeof(S_JoinGame_Ep3_64));
|
||||
mask.variations.clear(0);
|
||||
mask.rare_seed = 0;
|
||||
for (size_t offset = sizeof(S_JoinGame_GC_64) +
|
||||
offsetof(S_JoinGame_GC_Ep3_64::Ep3PlayerEntry, disp.visual.name_color_checksum);
|
||||
offsetof(S_JoinGame_Ep3_64::Ep3PlayerEntry, disp.visual.name_color_checksum);
|
||||
offset + 4 <= mask_size;
|
||||
offset += sizeof(S_JoinGame_GC_Ep3_64::Ep3PlayerEntry)) {
|
||||
offset += sizeof(S_JoinGame_Ep3_64::Ep3PlayerEntry)) {
|
||||
*reinterpret_cast<uint32_t*>(reinterpret_cast<char*>(mask_data) + offset) = 0;
|
||||
}
|
||||
}
|
||||
@@ -349,7 +349,7 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
|
||||
break;
|
||||
case 0xE8:
|
||||
if (is_gc(version)) {
|
||||
auto& mask = check_size_t<S_JoinSpectatorTeam_GC_Ep3_E8>(mask_data, mask_size);
|
||||
auto& mask = check_size_t<S_JoinSpectatorTeam_Ep3_E8>(mask_data, mask_size);
|
||||
mask.rare_seed = 0;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
mask.players[z].disp.visual.name_color_checksum = 0;
|
||||
@@ -365,9 +365,12 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
|
||||
}
|
||||
break;
|
||||
case 0xC9:
|
||||
if (mask_size == 0xCC) {
|
||||
auto& mask = check_size_t<G_ServerVersionStrings_GC_Ep3_6xB4x46>(
|
||||
mask_data, mask_size);
|
||||
if (mask_size == sizeof(G_ServerVersionStrings_Ep3NTE_6xB4x46)) {
|
||||
auto& mask = check_size_t<G_ServerVersionStrings_Ep3NTE_6xB4x46>(mask_data, mask_size);
|
||||
mask.version_signature.clear(0);
|
||||
mask.date_str1.clear(0);
|
||||
} else if (mask_size == sizeof(G_ServerVersionStrings_Ep3_6xB4x46)) {
|
||||
auto& mask = check_size_t<G_ServerVersionStrings_Ep3_6xB4x46>(mask_data, mask_size);
|
||||
mask.version_signature.clear(0);
|
||||
mask.date_str1.clear(0);
|
||||
mask.date_str2.clear(0);
|
||||
@@ -375,20 +378,41 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
|
||||
break;
|
||||
case 0x6C:
|
||||
if (is_gc(version) && mask_size >= 0x14) {
|
||||
const auto& cmd = check_size_t<G_MapList_GC_Ep3_6xB6x40>(cmd_data, cmd_size, 0xFFFF);
|
||||
const auto& cmd = check_size_t<G_MapList_Ep3_6xB6x40>(cmd_data, cmd_size, 0xFFFF);
|
||||
if ((cmd.header.header.basic_header.subcommand == 0xB6) &&
|
||||
(cmd.header.subsubcommand == 0x40)) {
|
||||
check_size_t<PSOCommandHeaderDCV3>(ev->mask, 0xFFFF).size = 0;
|
||||
auto& mask = check_size_t<G_MapList_GC_Ep3_6xB6x40>(mask_data, mask_size, 0xFFFF);
|
||||
auto& mask = check_size_t<G_MapList_Ep3_6xB6x40>(mask_data, mask_size, 0xFFFF);
|
||||
mask.header.header.size = 0;
|
||||
mask.compressed_data_size = 0;
|
||||
ev->allow_size_disparity = true;
|
||||
for (size_t z = sizeof(PSOCommandHeaderDCV3) + sizeof(G_MapList_GC_Ep3_6xB6x40); z < ev->mask.size(); z++) {
|
||||
for (size_t z = sizeof(PSOCommandHeaderDCV3) + sizeof(G_MapList_Ep3_6xB6x40); z < ev->mask.size(); z++) {
|
||||
ev->mask[z] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0x6D:
|
||||
if (version == Version::DC_NTE) {
|
||||
const auto& header = check_size_t<G_UnusedHeader>(cmd_data, cmd_size, 0xFFFF);
|
||||
if (header.subcommand == 0x60) {
|
||||
auto& mask = check_size_t<G_SyncPlayerDispAndInventory_DCNTE_6x70>(mask_data, mask_size, 0xFFFF);
|
||||
mask.visual.name_color_checksum = 0;
|
||||
}
|
||||
} else if (version == Version::DC_V1_11_2000_PROTOTYPE) {
|
||||
const auto& header = check_size_t<G_UnusedHeader>(cmd_data, cmd_size, 0xFFFF);
|
||||
if (header.subcommand == 0x67) {
|
||||
auto& mask = check_size_t<G_SyncPlayerDispAndInventory_DC112000_6x70>(mask_data, mask_size, 0xFFFF);
|
||||
mask.visual.name_color_checksum = 0;
|
||||
}
|
||||
} else if (!is_pre_v1(version)) {
|
||||
const auto& header = check_size_t<G_UnusedHeader>(cmd_data, cmd_size, 0xFFFF);
|
||||
if (header.subcommand == 0x70) {
|
||||
auto& mask = check_size_t<G_SyncPlayerDispAndInventory_DC_PC_6x70>(mask_data, mask_size, 0xFFFF);
|
||||
mask.base.visual.name_color_checksum = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -420,7 +444,7 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
|
||||
}
|
||||
case 0x00E6: {
|
||||
auto& mask = check_size_t<S_ClientInit_BB_00E6>(mask_data, mask_size);
|
||||
mask.team_id = 0;
|
||||
mask.security_token = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -737,7 +761,7 @@ void ReplaySession::on_command_received(
|
||||
case Version::PC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3:
|
||||
if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) {
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
OUTPUT_FILENAME="$1"
|
||||
OUTPUT_DIRNAME=$(dirname $OUTPUT_FILENAME)
|
||||
|
||||
GIT_REVISION_HASH=$(git rev-parse --short HEAD)
|
||||
TIMESTAMP_SECS=$(date +%s)
|
||||
|
||||
if [ -z "$GIT_REVISION_HASH" ]; then
|
||||
GIT_REVISION_HASH="????"
|
||||
else
|
||||
if ! git diff-index --quiet HEAD -- ; then
|
||||
GIT_REVISION_HASH="$GIT_REVISION_HASH+"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIRNAME"
|
||||
cat > $OUTPUT_FILENAME <<EOF
|
||||
#include "Revision.hh"
|
||||
|
||||
const char* GIT_REVISION_HASH = "$GIT_REVISION_HASH";
|
||||
const uint64_t BUILD_TIMESTAMP = static_cast<uint64_t>($TIMESTAMP_SECS) * 1000000;
|
||||
EOF
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
extern const char* GIT_REVISION_HASH;
|
||||
extern const uint64_t BUILD_TIMESTAMP;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user