Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e9003b061 | |||
| a59a2d7cd3 | |||
| 8cb7b465da | |||
| 0279b20bb7 | |||
| a140cdbedb | |||
| e7db8f2404 | |||
| 70dfeeba91 | |||
| a860d29636 | |||
| a7811429a8 | |||
| 75be38c38b | |||
| 75de6f259d | |||
| e6a6e862db | |||
| 2d1544edf4 | |||
| 0522b539c4 | |||
| ac20d0c7d4 | |||
| 263622cef8 | |||
| 461bd3d488 | |||
| 7baf5ce327 | |||
| 67c43e803b | |||
| fb9bd077a8 | |||
| 6e808b8340 | |||
| 996509531c | |||
| 4e7d6800cd | |||
| 0c9d4bf338 | |||
| 48641d46a0 | |||
| 84159821e9 | |||
| 823199be2e | |||
| 9eb5601349 | |||
| a7604353c3 | |||
| cfd264e4dc | |||
| e7d0739c8b | |||
| e5afc1d937 | |||
| a9a15600b2 | |||
| 086b2d411a | |||
| c61a13f62e | |||
| 0f25af1ab7 | |||
| 21f1c40408 | |||
| f8e479b4f9 | |||
| 775842dfc5 | |||
| a7d436a894 | |||
| 47bc37e806 | |||
| 080a9ebac4 | |||
| cac9589b81 | |||
| 34bd2cd6a7 | |||
| 8cc8d804bc | |||
| 59124678bf | |||
| b9fd52c6c1 | |||
| 458f5b2d0f | |||
| 7139df0265 | |||
| c6490cb3fb | |||
| b7d37eb169 | |||
| 1d26d1a529 | |||
| 5294a53e1b | |||
| 40d8227504 | |||
| a734bcf483 | |||
| 23e37b8eb7 | |||
| 627c0d949c | |||
| 096f9e46f4 | |||
| 7910556ace | |||
| 2bfcc32b6b | |||
| 0af0f8bc53 | |||
| 46c212f4a1 | |||
| 1e61415c9e | |||
| aa4a773095 | |||
| c8b8bf43f7 | |||
| e50848b52e | |||
| 9e8f7a1cc5 | |||
| 39f3a4afa7 | |||
| 4831f3649a | |||
| a9a524d04a | |||
| b773813f10 | |||
| 00bfae3b62 | |||
| 4dcb49bb34 | |||
| fd25eaadfd | |||
| 2d5b70c734 | |||
| 1ee3caf640 | |||
| e6e11794b8 | |||
| 79eabe5ed2 | |||
| b13e67d491 | |||
| 16a8f91822 | |||
| 82f036f66f | |||
| 3d2b5ebb79 | |||
| 302de15c75 | |||
| 18ce96c84b | |||
| e017279423 | |||
| dbc252a5d6 | |||
| cb0a9dad32 | |||
| 1f6f01a37f | |||
| eaa982aae9 | |||
| 07308b192c | |||
| 27105a3222 | |||
| d915b5e688 | |||
| 089980a6ab |
@@ -1,4 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0110 NEW)
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +66,7 @@ set(SOURCES
|
||||
src/Compression.cc
|
||||
src/DCSerialNumbers.cc
|
||||
src/DNSServer.cc
|
||||
src/DownloadSession.cc
|
||||
src/EnemyType.cc
|
||||
src/Episode3/AssistServer.cc
|
||||
src/Episode3/BattleRecord.cc
|
||||
@@ -121,6 +123,7 @@ set(SOURCES
|
||||
src/Server.cc
|
||||
src/ServerShell.cc
|
||||
src/ServerState.cc
|
||||
src/SignalWatcher.cc
|
||||
src/StaticGameData.cc
|
||||
src/TeamIndex.cc
|
||||
src/Text.cc
|
||||
@@ -155,6 +158,7 @@ add_dependencies(newserv newserv-Revision-cc)
|
||||
enable_testing()
|
||||
|
||||
file(GLOB LogTestCases ${CMAKE_SOURCE_DIR}/tests/*.test.txt)
|
||||
file(GLOB LogRDTestCases ${CMAKE_SOURCE_DIR}/tests/*.rdtest.txt)
|
||||
|
||||
foreach(LogTestCase IN ITEMS ${LogTestCases})
|
||||
add_test(
|
||||
@@ -163,6 +167,15 @@ foreach(LogTestCase IN ITEMS ${LogTestCases})
|
||||
COMMAND ${CMAKE_BINARY_DIR}/newserv --replay-log=${LogTestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json)
|
||||
endforeach()
|
||||
|
||||
if(resource_file_FOUND)
|
||||
foreach(LogRDTestCase IN ITEMS ${LogRDTestCases})
|
||||
add_test(
|
||||
NAME ${LogRDTestCase}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMAND ${CMAKE_BINARY_DIR}/newserv --replay-log=${LogRDTestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
file(GLOB ScriptTestCases ${CMAKE_SOURCE_DIR}/tests/*.test.sh)
|
||||
|
||||
foreach(ScriptTestCase IN ITEMS ${ScriptTestCases})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# newserv <img align="right" src="static/s-newserv.png" />
|
||||
|
||||
newserv is a game server, proxy, and reverse-engineering tool for Phantasy Star Online (PSO).
|
||||
newserv is a game server, proxy, and reverse-engineering tool for Phantasy Star Online (PSO). **To quickly get started using newserv, just read the [server setup](#server-setup) and [how to connect](#how-to-connect) sections.**
|
||||
|
||||
This project includes code that was reverse-engineered by the community in ages long past, and has been included in many projects since then. It also includes some game data from Phantasy Star Online itself, which was originally created by Sega.
|
||||
|
||||
@@ -13,6 +13,7 @@ See TODO.md for a list of known issues and future work I've curated, or go to th
|
||||
* [History](#history)
|
||||
* [Other server projects](#other-server-projects)
|
||||
* [Using newserv in other projects](#using-newserv-in-other-projects)
|
||||
* [Developer information](#developer-information)
|
||||
* [Compatibility](#compatibility)
|
||||
* Setup
|
||||
* [Server setup](#server-setup)
|
||||
@@ -23,6 +24,7 @@ See TODO.md for a list of known issues and future work I've curated, or go to th
|
||||
* [Installing quests](#installing-quests)
|
||||
* [Item tables and drop modes](#item-tables-and-drop-modes)
|
||||
* [Cross-version play](#cross-version-play)
|
||||
* [Server-side saves](#server-side-saves)
|
||||
* [Episode 3 features](#episode-3-features)
|
||||
* [Memory patches, client functions, and DOL files](#memory-patches-client-functions-and-dol-files)
|
||||
* [Using newserv as a proxy](#using-newserv-as-a-proxy)
|
||||
@@ -64,6 +66,20 @@ Independently of this project, there are many other PSO servers out there. Those
|
||||
* (2021) **[Phantasmal World](https://github.com/DaanVandenBosch/phantasmal-world)**: A set of PSO tools, including a web-based model viewer and quest builder, and a PSO server, written by Daan Vanden Bosch.
|
||||
* (2021) **[Elseware](http://git.sharnoth.com/jake/elseware)**: A PSOBB server written in Rust by Jake.
|
||||
|
||||
## Developer information
|
||||
|
||||
There is a lot of code in this project that could be useful as a reference. Some of the more notable files are:
|
||||
* **src/CommandFormats.hh**: Complete listing of all network commands used in all known versions of the game, and their formats
|
||||
* **src/DCSerialNumbers.hh/cc**: PSO DC serial number validation algorithm and serial number generator
|
||||
* **src/ItemData.hh**: Item format reference
|
||||
* **src/ItemCreator.hh/cc**: Reverse-engineered item generator from Episodes 1&2 (used for all versions)
|
||||
* **src/ItemParameterTable.hh**: Format of many structures in ItemPMT.prs
|
||||
* **src/Map.hh/cc**: Map file (.dat) structure and reverse-engineered Challenge Mode random enemy generation algorithm
|
||||
* **src/QuestScript.cc**: Complete listing of all quest opcodes on all versions, along with their arguments and behavior
|
||||
* **src/SaveFileFormats.hh**: Definitions of save file structures for all versions
|
||||
* **src/Episode3/DataIndexes.hh**: Episode 3 file structures, including card definition format and map/quest format
|
||||
* **system/item-tables/names-v4.json**: Names of all items, indexed by the first 3 bytes of data1
|
||||
|
||||
## Using newserv in other projects
|
||||
|
||||
There is a fair amount of code in this project that could potentially be useful to other projects. You are free to use code from newserv in your own open-source projects; the only condition is that the contents of the LICENSE file must be included in your project if you use code from newserv. Your project does not also have to use the MIT license; you can use any license you want.
|
||||
@@ -74,7 +90,8 @@ If you want to use parts of newserv in your project, there are two easy ways to
|
||||
|
||||
# Compatibility
|
||||
|
||||
newserv supports all known versions of PSO, including development prototypes. Specifically:
|
||||
newserv supports all known versions of PSO, including development prototypes. This table lists all versions that newserv supports. (NTE stands for Network Trial Edition; the GameCube beta versions were called Trial Edition instead, but we use the NTE abbreviation anyway for consistency.)
|
||||
|
||||
| Version | Lobbies | Games | Proxy |
|
||||
|-----------------|----------|----------|----------|
|
||||
| DC NTE | Yes | Yes | No |
|
||||
@@ -97,7 +114,7 @@ newserv supports all known versions of PSO, including development prototypes. Sp
|
||||
| BB (Tethealla) | Yes | Yes (2) | Yes |
|
||||
|
||||
*Notes:*
|
||||
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.*
|
||||
1. *Episode 3 NTE battles are not well-tested; some things may not work. See notes/ep3-nte-differences.txt for a list of known differences between NTE and the final version. NTE and non-NTE players cannot battle each other.*
|
||||
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.*
|
||||
|
||||
@@ -110,26 +127,30 @@ Currently newserv works on macOS, Windows, and Ubuntu Linux. It will likely work
|
||||
### Windows/macOS
|
||||
|
||||
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.
|
||||
2. Extract the contents of the archive to some location on your computer.
|
||||
3. (Optional) If you want to change any config options, go into the system/ folder, open config.json in a text editor, and edit it to your liking. There are comments in the file that describe what all the options do.
|
||||
4. (Optional) If you plan to play Blue Burst on newserv, set up the patch directory. See [client patch directories](#client-patch-directories) for details.
|
||||
5. Run the newserv executable.
|
||||
|
||||
### Linux
|
||||
|
||||
There are currently no precompiled releases for Linux. To run newserv on Linux, see the "Building from source" section below.
|
||||
There are currently no precompiled releases for Linux. To run newserv on Linux, you'll have to build it from source - 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 Windows, install [Cygwin](https://www.cygwin.com/). While doing so, install the `cmake`, `gcc-core`, `gcc-g++`, `git`, `libevent2.1_7`, `libevent-devel`, `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, 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.
|
||||
After building newserv, edit system/config.example.json as needed **and rename it to system/config.json** (note that this step is not necessary for the precompiled releases!), set up [client patch directories](#client-patch-directories) if you're planning to play Blue Burst, then run `./newserv` in newserv's directory.
|
||||
|
||||
The server has an interactive shell which can be used to make changes, such as managing user accounts, updating the server's configuration, managing Episode 3 tournaments, and more. Type `help` and press Enter to see all the commands.
|
||||
|
||||
On Linux and macOS, the server also responds to SIGUSR1 and SIGUSR2. SIGUSR1 does the equivalent of the shell's `reload config` command, which reloads config.json but not any dependent files (so quests, Episode 3 maps, etc. will not be reloaded). SIGUSR2 does the equivalent of the shell's `reload all` command, which reloads everything.
|
||||
|
||||
To use newserv in other ways (e.g. for translating data), see the end of this document.
|
||||
|
||||
@@ -143,7 +164,7 @@ For Blue Burst set up, the below is mandatory for a smooth experience:
|
||||
2. Copy all the `map_*.dat` files, `unitxt_*` files and the `data.gsl` file and place them in `system/patch-bb/data`.
|
||||
3. If you're using game files from the Tethealla client, make a copy of `unitxt_j.prs` inside system/patch-bb/data and name it `unitxt_e.prs`. (If `unitxt_e.prs` already exists, replace it with the copied file.)
|
||||
|
||||
If you do not have a BB client, or using a Tethealla client from another source, Tethealla clients that are compatible with newserv can be found here: [English](https://web.archive.org/web/20240402011115/https://ragol.org/files/bb/TethVer12513_English.zip) / [Japanese](https://web.archive.org/web/20240402013127/https://ragol.org/files/bb/TethVer12513_Japanese.zip). These clients connect to 127.0.0.1 (localhost) automatically.
|
||||
If you don't have a BB client, or if you're using a Tethealla client from another source, Tethealla clients that are compatible with newserv can be found here: [English](https://web.archive.org/web/20240402011115/https://ragol.org/files/bb/TethVer12513_English.zip) / [Japanese](https://web.archive.org/web/20240402013127/https://ragol.org/files/bb/TethVer12513_Japanese.zip). These clients connect to 127.0.0.1 (localhost) automatically.
|
||||
|
||||
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.
|
||||
|
||||
@@ -411,7 +432,9 @@ Like quests, Episode 3 card definitions, maps, and quests are cached in memory.
|
||||
|
||||
You can put assembly files in the system/client-functions directory with filenames like PatchName.VERS.patch.s and they will appear in the Patches menu for clients that support client functions. Client functions are written in SH-4, PowerPC, or x86 assembly and are compiled when newserv is started. The assembly system's features are documented in the comments in system/client-functions/System/WriteMemory.ppc.s.
|
||||
|
||||
The VERS token in client function filenames refers to the specific version of the game that the client function applies to. Some versions do not support receiving client functions at all. The specific versions are:
|
||||
The VERS token in client function filenames refers to the specific version of the game that the client function applies to. Some versions do not support receiving client functions at all. *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.*
|
||||
|
||||
The specific versions are:
|
||||
|
||||
| Game | VERS | Architecture |
|
||||
|-------------------|------|---------------|
|
||||
@@ -431,15 +454,15 @@ The VERS token in client function filenames refers to the specific version of th
|
||||
| PSO GC v1.2 JP | 3OJ2 | PowerPC |
|
||||
| PSO GC v1.3 JP | 3OJ3 | PowerPC |
|
||||
| PSO GC v1.4 JP | 3OJ4 | PowerPC |
|
||||
| PSO GC v1.5 JP | 3OJ5 | Not supported |
|
||||
| PSO GC v1.5 JP | 3OJ5 | PowerPC (1) |
|
||||
| PSO GC v1.0 US | 3OE0 | PowerPC |
|
||||
| PSO GC v1.1 US | 3OE1 | PowerPC |
|
||||
| PSO GC v1.2 US | 3OE2 | Not supported |
|
||||
| PSO GC v1.2 US | 3OE2 | PowerPC (1) |
|
||||
| PSO GC v1.0 EU | 3OP0 | PowerPC |
|
||||
| PSO GC Ep3 NTE | 3SJT | PowerPC |
|
||||
| PSO GC Ep3 JP | 3SJ0 | PowerPC |
|
||||
| PSO GC Ep3 US | 3SE0 | Not supported |
|
||||
| PSO GC Ep3 EU | 3SP0 | Not supported |
|
||||
| PSO GC Ep3 US | 3SE0 | PowerPC (1) |
|
||||
| PSO GC Ep3 EU | 3SP0 | PowerPC (1) |
|
||||
| PSO Xbox Beta | 4OJB | x86 |
|
||||
| PSO Xbox JP Disc | 4OJD | x86 |
|
||||
| PSO Xbox JP TU | 4OJU | x86 |
|
||||
@@ -447,12 +470,13 @@ The VERS token in client function filenames refers to the specific version of th
|
||||
| PSO Xbox US TU | 4OEU | x86 |
|
||||
| PSO Xbox EU Disc | 4OPD | x86 |
|
||||
| PSO Xbox EU TU | 4OPU | x86 |
|
||||
| PSO BB JP 1.25.13 | 51OC | x86 |
|
||||
| PSO BB Tethealla | 51OC | x86 |
|
||||
| PSO BB JP 1.25.13 | 59NL | x86 |
|
||||
| PSO BB Tethealla | 59NL | x86 |
|
||||
|
||||
*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.*
|
||||
*Notes:*
|
||||
1. *Client functions are only supported on these versions if EnableSendFunctionCallQuestNumbers is set in config.json. See the comments there for more information.*
|
||||
|
||||
newserv comes with a set of patches for some of the above versions, based on AR codes originally made by Ralf at GC-Forever and Aleron Ives. Many of them were originally posted in [this thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050).
|
||||
newserv comes with a set of patches for many of the above versions, based on AR codes originally made by Ralf at GC-Forever and Aleron Ives. Many of them were originally posted in [this thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050).
|
||||
|
||||
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/System directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
|
||||
|
||||
@@ -474,10 +498,10 @@ There are many options available when starting a proxy session. All options are
|
||||
* **Chat commands**: enables chat commands in the proxy session (on by default).
|
||||
* **Chat filter**: enables escape sequences in chat messages and info board (on by default).
|
||||
* **Player notifications**: shows a message when any player joins or leaves the game or lobby you're in.
|
||||
* **Block pings**: blocks automatic pings sent by the client, and responds to ping commands from the server automatically. This works around a bug in Sylverant's login server.
|
||||
* **Block pings**: blocks automatic pings sent by the client, and responds to ping commands from the server automatically.
|
||||
* **Infinite HP**: automatically heals you whenever you get hit. An attack that kills you in one hit will still kill you, however.
|
||||
* **Infinite TP**: automatically restores your TP whenever you use any technique.
|
||||
* **Switch assist**: attempts to unlock doors that require two or four players in a one-player game.
|
||||
* **Switch assist**: unlocks doors that require two or four players in a one-player game, when you step on one of the switches.
|
||||
* **Infinite Meseta** (Episode 3 only): gives you 1,000,000 Meseta, regardless of the value sent by the remote server.
|
||||
* **Block events**: disables holiday events sent by the remote server.
|
||||
* **Block patches**: prevents any B2 (patch) commands from reaching the client.
|
||||
@@ -512,6 +536,7 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `rare`: You are notified when a rare item drops.
|
||||
* `on`: You are notified when any item drops, except Meseta.
|
||||
* `every`: You are notified when any item drops, including Meseta.
|
||||
* `$announcerares`: Enable or disable announcements for your rare item finds. This determines whether rare items you find will be announced to the game and server, not whether you will see announcements for others finding rare items.
|
||||
* `$what` (game server only): Show the type, name, and stats of the nearest item on the ground.
|
||||
* `$where` (game server only): Show your current floor number and coordinates. Mainly useful for debugging.
|
||||
* `$qfread <field-name>` (game server only): Show the value of a quest counter in your player data. The field names are defined in config.json.
|
||||
@@ -544,9 +569,10 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* Personal state commands
|
||||
* `$arrow <color-id>`: Change your lobby arrow color.
|
||||
* `$secid <section-id>`: Set 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. If you're in a game and you are the leader of the game, this also immediately changes the item tables used by the server when creating items. 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.
|
||||
* `$battle` (game server only; DC v1 only): After using this command, the next game you create will be in battle mode. (A chat command is required for this because DCv1 doesn't allow this natively.) On DCv1, the battle quests are not available, but free-roam is.
|
||||
* `$rand <seed>`: Set 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]`: Set 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`: Enable or disable switch assist. When enabled, the server will attempt to automatically unlock two-player and four-player doors in non-quest games if you step on all the required switches sequentially.
|
||||
* `$swa`: Enable or disable switch assist. When enabled, the server will unlock two-player and four-player doors in non-quest games when you step on any of the required switches.
|
||||
* `$exit`: If you're in a lobby, send you to the main menu (which ends your proxy session, if you're in one). If you're in a game or spectator team, send 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.
|
||||
|
||||
@@ -565,14 +591,14 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$minlevel <level>`: Set the minimum level for players to join the current game.
|
||||
* `$password <password>`: Set the game's join password. To unlock the game, run `$password` with nothing after it.
|
||||
* `$dropmode [mode]`: Change 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 states of enemies, objects, and switches will be saved, and items left on the floor will not be deleted (except items that were only visible to the leaving player, which are deleted). If the game is empty for too long (15 minutes by default), it is then deleted. There is an edge case with persistence: if the player defeats a boss, leaves the room, joins again, and returns to the boss arena, neither the boss nor the exit warp will appear, so they will be stuck there and have to use $warp or Quit Game to get out. For this reason, `$warp 0` is allowed in boss arenas once the boss is defeated, even if cheat mode is disabled.
|
||||
* `$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 states of enemies, objects, and switches will be saved, and items left on the floor will not be deleted. (But if you're in the private or duplicate drop mode, items dropped by enemies are deleted - to make sure a certain item won't be deleted, you can pick it up and drop it again.) If the game is empty for too long (15 minutes by default), it is then deleted.
|
||||
|
||||
* Episode 3 commands (game server only)
|
||||
* `$spec`: Toggle the allow spectators flag for Episode 3 games. If any players are spectating when this flag is disabled, they are sent back to the lobby.
|
||||
* `$inftime`: Toggle infinite-time mode. Must be used before starting a battle. If infinite-time mode is on, the overall and per-phase time limits will be disabled regardless of the values chosen during battle rules setup. After completing a battle, infinite-time mode is reset to the server's default value (which can be set in Episode3BehaviorFlags in config.json).
|
||||
* `$dicerange [d:L-H] [1:L-H] [a1:L-H] [d1:L-H]`: Set override dice ranges for the next battle. The min and max dice values from the rules setup menu always apply to the ATK dice, but you can specify a different range for the DEF dice with `d:2-4` (for example). The `1:` override applies to the 1-player team in a 2v1 game (so you would set the 2-player team's desired dice range in the rules menu). You can also specify the 1-player team's ATK and DEF ranges separately with the `a1:` and `d1:` overrides. Note that these ranges will only be used if the chosen map or quest does not override them.
|
||||
* `$stat <what>`: Show a statistic about your player or team in the current battle. `<what>` can be `duration`, `fcs-destroyed`, `cards-destroyed`, `damage-given`, `damage-taken`, `opp-cards-destroyed`, `own-cards-destroyed`, `move-distance`, `cards-set`, `fcs-set`, `attack-actions-set`, `techs-set`, `assists-set`, `defenses-self`, `defenses-ally`, `cards-drawn`, `max-attack-damage`, `max-combo`, `attacks-given`, `attacks-taken`, `sc-damage`, `damage-defended`, or `rank`.
|
||||
* `$surrender`: Cause your team to immediately lose the current battle.
|
||||
* `$surrender`: Cause your team to immediately lose the current battle. If your story character is already defeated, you can't surrender - only your teammate can.
|
||||
* `$saverec <name>`: Save the recording of the last battle.
|
||||
* `$playrec <name>`: Play a battle recording. This command creates a spectator team immediately but the replay does not start automatically, to give other players a chance to join. To start the battle replay within the spectator team, run `$playrec` again (with no name). There is a bug in Dolphin that makes this command unstable in emulation (see the "Battle records" section above).
|
||||
|
||||
@@ -593,8 +619,8 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$song <song-id>` (Episode 3 only): Play a specific song in the current lobby.
|
||||
|
||||
* Administration commands (game server only)
|
||||
* `$ann <message>`: Send an announcement message. The message is sent as temporary on-screen text to all players in all games and lobbies.
|
||||
* `$ann! <message>`: Send an announcement message. The message is sent as a Simple Mail message to all players in all games and lobbies.
|
||||
* `$ann <message>`: Send an announcement message. The message is sent as temporary on-screen text to all players in all games and lobbies. On BB, the message appears in the scrolling top bar.
|
||||
* `$ann!`, `$ann?`, `$ann?!`: Same as `$ann`, but with `?`, omits the sender's name, and with `!`, sends the message as a Simple Mail message instead of on-screen text.
|
||||
* `$ax <message>`: Send a message to the server's terminal. This cannot be used to run server shell commands; it only prints text to stderr.
|
||||
* `$silence <identifier>`: Silence a player (remove their ability to chat) or unsilence a player. The identifier may be the player's name or Guild Card number.
|
||||
* `$kick <identifier>`: Disconnect a player. The identifier may be the player's name or Guild Card number.
|
||||
@@ -617,7 +643,11 @@ Some subcommands are always available. They are:
|
||||
* `$edit namecolor AARRGGBB`: Set your name color (AARRGGBB specified in hex)
|
||||
* `$edit language L`: Set your language (Generally only useful on BB; values for L: J = Japanese, E = English, G = German, F = French, S = Spanish, B = Simplified Chinese, T = Traditional Chinese, K = Korean)
|
||||
* `$edit name NAME`: Set your character name
|
||||
* `$edit npc NPC-NAME`: Set or remove an NPC skin on your character (NPC-NAME can be ninja, rico, sonic, knuckles, tails, flowen, elly, or none)
|
||||
* `$edit npc NPC-NAME`: Set or remove an NPC skin on your character (use `none` to remove a skin). The NPC names are:
|
||||
* On all versions except DCv1 and early prototypes: `ninja`, `rico`, `sonic`, `knuckles`, `tails`
|
||||
* On GC, Xbox, and BB: `flowen`, `elly`
|
||||
* On BB only: `momoka`, `irene`, `guild`, `nurse`
|
||||
* `$edit secid SECID-NAME`: Set your section ID (cheat mode is required for this unless your character is Level 1)
|
||||
|
||||
The remaining subcommands are only available if cheat mode is enabled on the server. They are:
|
||||
* `$edit atp N`: Set your ATP to N until stats are updated (e.g. by leveling up)
|
||||
@@ -630,7 +660,6 @@ The remaining subcommands are only available if cheat mode is enabled on the ser
|
||||
* `$edit meseta N`: Set the amount of Meseta in your inventory
|
||||
* `$edit exp N`: Set your total amount of EXP (does not affect level)
|
||||
* `$edit level N`: Set your current level (recomputes stats, but does not affect EXP)
|
||||
* `$edit secid SECID-NAME`: Set your section ID
|
||||
* `$edit tech TECH-NAME LEVEL`: Set the level of one of your techniques
|
||||
|
||||
# Non-server features
|
||||
@@ -651,6 +680,7 @@ The data formats that newserv can convert to/from are:
|
||||
| PSO GC quest file (.gci) | None | `decode-gci` |
|
||||
| Download quest file (.dlq) | None | `decode-dlq` |
|
||||
| Server quest file (.qst) | `encode-qst` | `decode-qst` |
|
||||
| PSO DC save file (.vms) | `encrypt-vms-save` | `decrypt-vms-save` |
|
||||
| PSO PC save file | `encrypt-pc-save` | `decrypt-pc-save` |
|
||||
| PSO GC save file (.gci) | `encrypt-gci-save` | `decrypt-gci-save` |
|
||||
| PSO GC snapshot file | None | `decode-gci-snapshot` |
|
||||
@@ -677,3 +707,108 @@ There are several actions that don't fit well into the table above, which let yo
|
||||
* 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`)
|
||||
* Generate or describe DC serial numbers (`generate-dc-serial-number`, `inspect-dc-serial-number`)
|
||||
|
||||
# Docker
|
||||
Docker is new and mostly unsupported at this time. However, here are some best-effort steps to build and run in a docker container on Ubuntu Linux.
|
||||
Tested on Ubuntu 22.04.4 LTS.
|
||||
Note: You cannot have anything except this docker container using port 53 (DNS) on your server.
|
||||
|
||||
Install prerequisites
|
||||
```
|
||||
sudo apt install -y git
|
||||
sudo apt install -y cmake. ## minimum version is 3.10. Check installed version with "cmake --version"
|
||||
```
|
||||
|
||||
Clone repository
|
||||
```
|
||||
cd ~
|
||||
git clone https://github.com/fuzziqersoftware/newserv/
|
||||
cd ~/newserv
|
||||
```
|
||||
|
||||
Build newserv. This will take a while. Don't forget the period at the end!
|
||||
```
|
||||
sudo docker build -t newserv .
|
||||
```
|
||||
|
||||
Create persistent directories. Assuming you want to store the persistent data in your home directory
|
||||
```
|
||||
mkdir ~/newservPersist
|
||||
mkdir ~/newservPersist/players
|
||||
mkdir ~/newservPersist/teams
|
||||
mkdir ~/newservPersist/licenses
|
||||
```
|
||||
|
||||
Copy config file to config dir
|
||||
```
|
||||
cp ~/newserv/system/config.example.json ~/newservPersist/config.json
|
||||
```
|
||||
|
||||
Edit config.json
|
||||
```
|
||||
nano ~/newservPersist/config.json
|
||||
```
|
||||
Pro tip:
|
||||
Set "LocalAddress" to the static, LAN IP address of your server. If your server LAN IP is "192.168.0.10":
|
||||
"LocalAddress": "192.168.0.10",
|
||||
|
||||
Set "ExternalAddress" to the WAN IP address of your network. If your WAN IP is "8.8.8.8":
|
||||
"ExternalAddress": "8.8.8.8",
|
||||
|
||||
For Dolphin > Settings. Set SP1 to "Broadband Adapter (HLE)" Click [...] next to this, and set the DNS to the IP address of your server. Then start the game. Changes will not take affect if the game is running.
|
||||
|
||||
Docker run. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
docker run --name newserv -p 53:53/udp -p 5100:5100 -p 5110:5110 -p 5111:5111 -p 5112:5112 -p 9064:9064 -p 9100:9100 -p 9103:9103 -p 9300:9300 -p 11000:11000 -p 12000:12000 -p 12004:12004 -p 12005:12005 -v /etc/localtime:/etc/localtime:ro -v /home/changeme/newservPersist/config.json:/newserv/system/config.json -v /home/changeme/newservPersist/players:/newserv/system/players -v /home/changeme/newservPersist/teams:/newserv/system/teams -v /home/changeme/newservPersist/licenses:/newserv/system/licenses --restart no newserv:latest
|
||||
```
|
||||
|
||||
Docker run host network mode. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
docker run --net host --name newserv -v /etc/localtime:/etc/localtime:ro -v /home/changeme/newservPersist/config.json:/newserv/system/config.json -v /home/changeme/newservPersist/players:/newserv/system/players -v /home/changeme/newservPersist/teams:/newserv/system/teams -v /home/changeme/newservPersist/licenses:/newserv/system/licenses --restart no newserv:latest
|
||||
```
|
||||
|
||||
Docker compose. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
name: psonewserv
|
||||
services:
|
||||
newserv:
|
||||
container_name: newserv
|
||||
ports:
|
||||
- 53:53/udp
|
||||
- 5100:5100
|
||||
- 5110:5110
|
||||
- 5111:5111
|
||||
- 5112:5112
|
||||
- 9064:9064
|
||||
- 9100:9100
|
||||
- 9103:9103
|
||||
- 9300:9300
|
||||
- 11000:11000
|
||||
- 12000:12000
|
||||
- 12004:12004
|
||||
- 12005:12005
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /home/changeme/newservPersist/config.json:/newserv/system/config.json
|
||||
- /home/changeme/newservPersist/players:/newserv/system/players
|
||||
- /home/changeme/newservPersist/teams:/newserv/system/teams
|
||||
- /home/changeme/newservPersist/licenses:/newserv/system/licenses
|
||||
restart: no ## Set to whatever you want.
|
||||
image: newserv:latest
|
||||
```
|
||||
Docker compose host network mode. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
name: psonewserv
|
||||
services:
|
||||
newserv:
|
||||
container_name: newserv
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /home/changeme/newservPersist/config.json:/newserv/system/config.json
|
||||
- /home/changeme/newservPersist/players:/newserv/system/players
|
||||
- /home/changeme/newservPersist/teams:/newserv/system/teams
|
||||
- /home/changeme/newservPersist/licenses:/newserv/system/licenses
|
||||
restart: no ## Set to whatever you want.
|
||||
network_mode: host
|
||||
image: newserv:latest
|
||||
```
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
## General
|
||||
|
||||
- Implement decrypt/encrypt actions for VMS files
|
||||
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
|
||||
- Add an idle connection timeout for proxy sessions
|
||||
- Clean up ItemParameterTable implementation (see comment at the top of the class definition)
|
||||
|
||||
@@ -11,6 +11,12 @@ See also https://github.com/Solybum/Blue-Burst-Patch-Project
|
||||
Memory: 005C9F31 E9A7000000
|
||||
File: 001C9331 E9A7000000
|
||||
|
||||
All rareable enemies are rare (GC US v1.1)
|
||||
040AC944 60000000 // Hildeblue
|
||||
040C1B70 60000000 // Rappies
|
||||
040C3FC8 60000000 // Nar Lily
|
||||
040EB050 48000010 // Pouilly Slime
|
||||
|
||||
Unlock all songs in BGM test
|
||||
(Note: sadly, there are no secret/unused ones)
|
||||
Ep12-JP12 => 04367A68 38600001
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
000F04 LOGiN
|
||||
006E00 GAME MAGAZNE
|
||||
00AD00 RAGE DE FEU
|
||||
00AD01 RAGE DE FEU
|
||||
00AD02 RAGE DE FEU
|
||||
00D000 UNKNOWN3
|
||||
00D100 UNKNOWN4
|
||||
01013D KROE'S SWEATER
|
||||
01013F SONICTEAM ARMOR
|
||||
010230 HUNTER'S SHELL
|
||||
010233 HUNTER'S SHELL
|
||||
010234 HUNTER'S SHELL
|
||||
010236 Barrier
|
||||
010237 Barrier
|
||||
010238 Barrier
|
||||
010239 Barrier
|
||||
010253 BLUE RING
|
||||
010254 BLUE RING
|
||||
010255 BLUE RING
|
||||
010256 BLUE RING
|
||||
010257 BLUE RING
|
||||
010258 BLUE RING
|
||||
01025A BLUE RING
|
||||
01025B GREEN RING
|
||||
01025C GREEN RING
|
||||
01025D GREEN RING
|
||||
01025E GREEN RING
|
||||
010260 GREEN RING
|
||||
010261 GREEN RING
|
||||
010262 GREEN RING
|
||||
010263 YELLOW RING
|
||||
010264 YELLOW RING
|
||||
010265 YELLOW RING
|
||||
010267 YELLOW RING
|
||||
010268 YELLOW RING
|
||||
010269 YELLOW RING
|
||||
01026A YELLOW RING
|
||||
01026B PURPLE RING
|
||||
01026D PURPLE RING
|
||||
01026E PURPLE RING
|
||||
01026F PURPLE RING
|
||||
010270 PURPLE RING
|
||||
010271 PURPLE RING
|
||||
010272 PURPLE RING
|
||||
010274 WHITE RING
|
||||
010276 WHITE RING
|
||||
010277 WHITE RING
|
||||
010278 WHITE RING
|
||||
010279 WHITE RING
|
||||
01027A WHITE RING
|
||||
01027C BLACK RING
|
||||
01027D BLACK RING
|
||||
01027E BLACK RING
|
||||
01027F BLACK RING
|
||||
010281 BLACK RING
|
||||
01029A UNKNOWN_B
|
||||
024300 \n
|
||||
024A00 Yahoo!
|
||||
024D00 Cell of MAG 0503
|
||||
024E00 Cell of MAG 0504
|
||||
024F00 Cell of MAG 0505
|
||||
025000 Cell of MAG 0506
|
||||
025100 Cell of MAG 0507
|
||||
03120B New Year's Card
|
||||
03120C Christmas Card
|
||||
03120D Birthday Card
|
||||
03120E Proof of Sonic Team
|
||||
03120F Special Event Ticket
|
||||
03140A Bouquet
|
||||
03140B Decoction
|
||||
031603 DISK Vol.4 "Open Your Heart"
|
||||
031604 DISK Vol.5 "Live & Learn"
|
||||
031801 UNKNOWN2
|
||||
031808 Yahoo!'s engine
|
||||
03180B Cell of MAG 0503
|
||||
03180C Cell of MAG 0504
|
||||
03180D Cell of MAG 0505
|
||||
03180E Cell of MAG 0506
|
||||
03180F Cell of MAG 0507
|
||||
200000 (invalid item code)
|
||||
@@ -0,0 +1,9 @@
|
||||
entry counter flags
|
||||
|
||||
01 = rules have any non-default values
|
||||
02 = map number is set
|
||||
04 = UNKNOWN (something to do with deck selection/verification)
|
||||
08 = tournament mode (set by 6xB4x3D; shows timer in battle select menu and skips map select and rule select)
|
||||
10 = UNKNOWN (used by 6xB5x43)
|
||||
20 = command DC received
|
||||
40 = tournament result available (6xB4x51 received)
|
||||
@@ -0,0 +1,18 @@
|
||||
Ep1 Ep2
|
||||
1 Forest 1 Temple
|
||||
2 Forest 2 Temple
|
||||
3 Cave 1 Spaceship
|
||||
4 Cave 2 Spaceship
|
||||
5 Cave 3 CCA
|
||||
6 Mine 1 Jungle
|
||||
7 Mine 2 Jungle
|
||||
8 Ruins 1 (broken) Mountain
|
||||
9 Ruins 2 (broken) Seaside
|
||||
10 Ruins 3 (broken) Void (Seabed doors + Mine music)
|
||||
11 Dragon Void (doors + Dolmolm + Mine music)
|
||||
12 De Rol Le Gal Gryphon
|
||||
13 Vol Opt Olga Flow (unfinished, Flow does no damage)
|
||||
14 void (Falz music) Barba Ray (unfinished)
|
||||
15 Lobby Gol Dragon (unfinished)
|
||||
16 Versus1 crash
|
||||
17 Versus2 crash
|
||||
+925
-912
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
patch required in TethVer12513 to get this to work: 0048210D EB
|
||||
|
||||
is_hangame callsites:
|
||||
0040457C - ??? (something in TDataProtocol?)
|
||||
004820F4 - client version check (use patch above to bypass)
|
||||
00708318 - patch server domain name
|
||||
00708348 - patch server port
|
||||
0070852C - ep4 unlocked setting (always true for hangame)
|
||||
007085F4 - data server domain name
|
||||
00708670 - data server port
|
||||
007618E3 - whether to save user/pass to registry
|
||||
00761C4C - create title screen menu (only shows Start Game and Exit Game in Hangame mode)
|
||||
007623B0 - input password length limit?? (does nothing, since both branches of if statement lead to same result)
|
||||
00762530 - registry account data access
|
||||
00762708 - input password length limit?? (does nothing, since both branches of if statement lead to same result)
|
||||
0076296F - input username length limit?? (limits to 12 instead of 16)
|
||||
00762C30 - input username length limit?? (limits to 12 instead of 16)
|
||||
00762D00 - password length limit again??
|
||||
00762D2C - username length limit again??
|
||||
@@ -0,0 +1,165 @@
|
||||
$qfread
|
||||
|
||||
00 000003FF Garon points
|
||||
00 0003FC00 Garon button-mashing game score
|
||||
00 03FC0000 Garon timing game score
|
||||
00 04000000 Garon Tier 1 (10 cards) of Guild Card counter
|
||||
00 08000000 Garon Tier 2 (30 cards) of Guild Card counter
|
||||
00 10000000 Garon Tier 3 (50 cards) of Guild Card counter
|
||||
00 20000000 Garon Tier 4 (100 cards) of Guild Card counter
|
||||
00 C0000000 __UNUSED__
|
||||
|
||||
01 00000001 Dream Messenger: NiGHTS
|
||||
01 00000002 Pioneer Warehouse: ???
|
||||
01 00000004 Garon's Shop: Puyo Pop
|
||||
01 00000008 Pioneer Warehouse: Chu Chu Challenge
|
||||
01 00000010 Reach for the Dream: Chu Chu Puzzle
|
||||
01 00000020 Seat of the Heart: Checkpoint (Normal)
|
||||
01 00000040 Seat of the Heart: Checkpoint (Sue path)
|
||||
01 00000080 Seat of the Heart: Checkpoint (Elly is mad)
|
||||
01 00000100 Seat of the Heart: Quest complete (no Sue path)
|
||||
01 00000200 Seat of the Heart: Quest complete (Sue path)
|
||||
01 00000400 Seat of the Heart: Got Ragol Ring (Normal)
|
||||
01 00000800 Seat of the Heart: Checkpoint (Hard)
|
||||
01 00000800 White Day: ???
|
||||
01 00001000 Blue Star Memories: Future Forecast
|
||||
01 00001000 Seat of the Heart: Checkpoint (Sue path)
|
||||
01 00002000 Blue Star Memories: Future Bullet
|
||||
01 00002000 Seat of the Heart: Checkpoint (Elly is mad)
|
||||
01 00004000 Seat of the Heart: Quest complete (no Sue path)
|
||||
01 00008000 Seat of the Heart: Quest complete (Sue path)
|
||||
01 00010000 Seat of the Heart: Got Ragol Ring (Hard)
|
||||
01 00020000 Seat of the Heart: Checkpoint (Very Hard)
|
||||
01 00040000 Seat of the Heart: Checkpoint (Sue path)
|
||||
01 00080000 Seat of the Heart: Checkpoint (Elly is mad)
|
||||
01 00100000 Seat of the Heart: Quest complete (no Sue path)
|
||||
01 00200000 Seat of the Heart: Quest complete (Sue path)
|
||||
01 003F8000 Beta Lucky Coins
|
||||
01 00400000 Seat of the Heart: Got Ragol Ring (Very Hard)
|
||||
01 00800000 Seat of the Heart: Checkpoint (Ultimate)
|
||||
01 01000000 Seat of the Heart: Checkpoint (Sue path)
|
||||
01 02000000 Seat of the Heart: Checkpoint (Elly is mad)
|
||||
01 04000000 Seat of the Heart: Quest complete (no Sue path)
|
||||
01 08000000 Seat of the Heart: Quest complete (Sue path)
|
||||
01 10000000 Seat of the Heart: Got Ragol Ring (Ultimate)
|
||||
01 E0000000 __UNUSED__
|
||||
|
||||
02 00000001 Pioneer Halloween: Got Jack-O'-Lantern
|
||||
02 00000002 Pioneer Halloween: Got cake
|
||||
02 00000020 East Tower
|
||||
02 00000020 The East Tower: Paganini side quest
|
||||
02 00000040 The West Tower: Paganini side quest
|
||||
02 00000040 West Tower
|
||||
02 00000080 Labyrinthine Trial: White Ring
|
||||
02 00000100 Garon's Treachery: Rakonia Stone
|
||||
02 00000200 Garon's Treachery: Fragment of Friendship
|
||||
02 00000400 Towards the Future: Purple Ring
|
||||
02 00000800 Towards the Future: Flower Bouquet
|
||||
02 00002000 Heart Of Poumn
|
||||
02 00002000 Rappy's Holiday: Heart of Poumn
|
||||
02 003FC000 Rappy's Holiday points
|
||||
02 07000000 Respective Tomorrow: WIS
|
||||
02 08000000 Respective Tomorrow: S/SS Rank
|
||||
02 10000000 Towards the Future: Black Ring
|
||||
02 20000000 Green Ring
|
||||
02 C0C0101C __UNUSED__
|
||||
|
||||
03 000000FF Lucky Tickets
|
||||
03 003FFF00 Kill count
|
||||
03 07C00000 Song count
|
||||
03 08000000 Couple flag
|
||||
03 7FFFFFFF MA4 kills (Central Dome)
|
||||
03 80000000 __UNUSED__
|
||||
|
||||
04 7FFFFFFF MA4 kills (Gal Da Val)
|
||||
04 80000000 __UNUSED__
|
||||
|
||||
05 00007FFF Principal's Gift: Random Candy ID
|
||||
05 00008000 Candy ID init flag
|
||||
05 00200000 Racket
|
||||
05 00400000 Tree Clippers
|
||||
05 00800000 Synthesizer
|
||||
05 01000000 Shichishito
|
||||
05 02000000 Dirty Life Jacket
|
||||
05 04000000 Lost Hell Pallasch: Gush Raygun
|
||||
05 F81F0000 __UNUSED__
|
||||
|
||||
06 0FF00000 Lucky Tickets
|
||||
06 F00FFFFF __UNUSED__
|
||||
|
||||
07 00000001 Government 4-5: Normal cleared
|
||||
07 00000002 Government 4-5: Hard cleared
|
||||
07 00000004 Government 4-5: Very Hard cleared
|
||||
07 00000008 Government 4-5: Ultimate cleared
|
||||
07 00000010 Government 8-3: Normal cleared
|
||||
07 00000020 Government 8-3: Hard cleared
|
||||
07 00000040 Government 8-3: Very Hard cleared
|
||||
07 00000080 Government 8-3: Ultimate cleared
|
||||
07 FFFFFF00 __UNUSED__
|
||||
|
||||
08 7FFFFFFF MA4 kills (Crater)
|
||||
08 80000000 __UNUSED__
|
||||
|
||||
09 00003FFF MA1v2 points
|
||||
09 00003FFF Maximum Attack 1 Ver.2: points
|
||||
09 0FFFC000 MA2v2 points
|
||||
09 0FFFC000 Maximum Attack 2 Ver.2: points
|
||||
09 10000000 AOL CUP -Sunset Base- (Mag Cell)
|
||||
09 10000000 Maximum Attack 1 Ver.2: Class Master flag
|
||||
09 20000000 AOL CUP -Sunset Base- (Ruins)
|
||||
09 20000000 Maximum Attack 2 Ver.2: ID Master flag
|
||||
09 40000000 Beach Laughter: Got 5 Photon Spheres & Black Ring
|
||||
09 40000000 Blue Ring
|
||||
09 80000000 __UNUSED__
|
||||
|
||||
0A 00000001 Heart of HUmar
|
||||
0A 00000002 Heart of HUnewearl
|
||||
0A 00000004 Heart of HUcast
|
||||
0A 00000008 Heart of HUcaseal
|
||||
0A 00000010 Heart of RAmar
|
||||
0A 00000020 Heart of RAmarl
|
||||
0A 00000040 Heart of RAcast
|
||||
0A 00000080 Heart of RAcaseal
|
||||
0A 00000100 Heart of FOmar
|
||||
0A 00000200 Heart of FOmarl
|
||||
0A 00000400 Heart of FOnewm
|
||||
0A 00000800 Heart of FOnewearl
|
||||
0A 00001000 Heart of Viridia
|
||||
0A 00002000 Heart of Greenill
|
||||
0A 00004000 Heart of Skyly
|
||||
0A 00008000 Heart of Bluefull
|
||||
0A 00010000 Heart of Purplenum
|
||||
0A 00020000 Heart of Pinkal
|
||||
0A 00040000 Heart of Redria
|
||||
0A 00080000 Heart of Oran
|
||||
0A 00100000 Heart of Yellowboze
|
||||
0A 00200000 Heart of Whitill
|
||||
0A 7FC00000 Lucky Tickets
|
||||
0A 80000000 __UNUSED__
|
||||
|
||||
0B 00000001 Garon's Shop: Black Gear
|
||||
0B 00000001 Roulette (SEIRYU)
|
||||
0B 00000002 Beta -> Final Lucky Coins init flag
|
||||
0B 00000002 Roulette (GENBU)
|
||||
0B 000001FC Lucky Coins
|
||||
0B 0007FC00 Pioneer Christmas ???
|
||||
0B 00080000 Cleared 4th Pioneer Christmas tier?
|
||||
0B 1FF00000 Wrapping Papers
|
||||
0B 20000000 Pioneer Christmas Present
|
||||
0B 40000000 Wall
|
||||
0B 40000000 White Day: Flower Bouquet or Heart Key
|
||||
0B 80000200 __UNUSED__
|
||||
|
||||
0C FFFFFFFF __UNUSED__
|
||||
|
||||
0D FFFFFFFF __UNUSED__
|
||||
|
||||
0E 7FFFFFFF MA4 kills (Total)
|
||||
0E 80000000 __UNUSED__
|
||||
|
||||
0F 000000FF MA4 Tickets
|
||||
0F 00000100 MA4 PHOTON CRYSTAL
|
||||
0F 00000200 MA4 FRIEND RING
|
||||
0F 00000400 MA4 GIRASOLE
|
||||
0F 00000800 MA4 SAMURAI ARMOR
|
||||
0F FFFFF000 __UNUSED__
|
||||
@@ -0,0 +1,188 @@
|
||||
0007 = Set by rico capsule in caves
|
||||
000B = P2 Tyrell Start
|
||||
000C = P2 Irene Start
|
||||
000D = P2 Scientist 1 Start
|
||||
000E = P2 Scientist 2 Start
|
||||
000F = P2 More Scientist stuff.
|
||||
0010 = P2 Irene after talking to Tyrell
|
||||
0011 = Read a rico capsule (any)
|
||||
0012 = P2 Scientist after talking to Irene.
|
||||
0013 = P2 Menu 6, quest counter / Tekker talked to
|
||||
0014 = Entered Forest 1
|
||||
0015 = Entered Forest 2
|
||||
0016 = Entered Dragon Area
|
||||
0017 = Dragon defeated
|
||||
0018 = Caves unlocked
|
||||
0018 = P2 Principle after defeating dragon
|
||||
0019 = P2 Scientist after defeating dragon
|
||||
001E = Entered Caves 1 (Gov 2-1)
|
||||
001F = Entered De Rol Le in 2-4
|
||||
0020 = De Ro lee defeated
|
||||
0021 = Mines unlocked (P2 Tyrell after defeating De Rol Le)
|
||||
0028 = Entered Mines 1
|
||||
0029 = Entered Vol Opt Area
|
||||
002A = Defeated Vol Opt
|
||||
002B = Set by rico capsule about the 3 seals (after vol opt).
|
||||
002C = Activated Forest monument
|
||||
002D = Activated Caves monument (Gov 2-2)
|
||||
002E = Activated Mines monument
|
||||
002F = Activated all monuments
|
||||
0030 = Entered Ruins 1
|
||||
0032 = Entered Falz 1
|
||||
0035 = Hard mode unlocked
|
||||
0036 = Entered Falz 3 // Very Hard mode unlocked (?)
|
||||
0037 = Ultimate unlocked
|
||||
0046 = One CCA door lock unlocked
|
||||
0047 = One CCA door lock unlocked
|
||||
0048 = One CCA door lock unlocked
|
||||
0049 = Entered Laboratory
|
||||
004A = Lab Assistant Start
|
||||
004B = Entered Temple Beta
|
||||
004C = Defeated Barba Ray
|
||||
004D = Lab Assistant after defeating barba ray
|
||||
004E = Entered Spaceship Beta
|
||||
004F = Defeated Gol Dragon
|
||||
0051 = Entered CCA
|
||||
0052 = Defeated Gal Gyrphon // Defeated Gol dragon in seat of heart (?)
|
||||
0054 = Entered Seabed Upper
|
||||
0057 = Defeated Olga Flow
|
||||
005B = Lab Natasha Start
|
||||
005C = Lab Natasha after VR temple
|
||||
005D = Lab Natasha after VR Spaceship
|
||||
005E = Lab Assistant after defeating Gal gryphon
|
||||
005F = After reading the last capsule from flowen
|
||||
0060 = Lab Natasha after CCA
|
||||
0065 = Cleared Magnitude of Metal
|
||||
0067 = Cleared Claiming a Stake
|
||||
0069 = Cleared Value of Money
|
||||
006B = Cleared Battle Training
|
||||
006D = Cleared Journalistic Pursuit
|
||||
006F = Cleared The Fake in Yellow
|
||||
0071 = Cleared Native Research
|
||||
0073 = Cleared Forest of Sorrow
|
||||
0075 = Cleared Gran Squall
|
||||
0077 = Cleared Addicting Food
|
||||
0079 = Cleared The Lost Bride
|
||||
007B = Cleared Waterfall Tears
|
||||
007D = Cleared Black Paper
|
||||
007F = Cleared Secret Delivery
|
||||
0081 = Cleared Soul of a Blacksmith
|
||||
0083 = Cleared Letter from Lionel
|
||||
0085 = Cleared The Grave's Butler
|
||||
0087 = Cleared Knowing One's Heart
|
||||
0089 = Cleared The Retired Hunter
|
||||
008B = Cleared Dr. Osto's Research
|
||||
008D = Cleared Unsealed Door
|
||||
008F = Cleared Soul of Steel
|
||||
0091 = Cleared Doc's Secret Plan (able to make enemy part weapons)
|
||||
0093 = Cleared Seek my Master
|
||||
0095 = Cleared From the Depths
|
||||
0096 = Unknown (set in the fake in yellow)
|
||||
0097 = Seat of heart unknown
|
||||
009B = Cleared Central Dome Fire Swirl
|
||||
00A1 = Cleared Seat of the Heart
|
||||
00C9 = Got an enemy weapon converted
|
||||
00CA = unknown Fake In Yellow
|
||||
00CE = unknown Fake In Yellow
|
||||
00D3 = Dr.Osto's research black paper subplot. Told Sue your name
|
||||
00D4 = Dr.Osto's research black paper subplot. Didn't tell Sue your name from before.
|
||||
00D5 = Dr.Osto's research black paper subplot. Did tell Sue your name from before.
|
||||
00D6 = Unsealed door. black paper subplot Talked to Sue. Refused to tell her your name
|
||||
00D7 = Unsealed door. black paper subplot. bernie tells you Sue is part of black paper.
|
||||
00D8 = Black paper subplot in waterfall of tears talking to Sue
|
||||
00D9 = Black paper subplot in Black paper talking to Sue (used option 2)
|
||||
00DB = Black paper subplot in Black paper talking to Sue (used any option)
|
||||
00DE = Black paper subplot in Black paper talked to Sue at the end of quest?
|
||||
00DF = Knowing ones heart talked to Bernie?
|
||||
00E0 = Seek my master. Zoke ,Donoph subplot?
|
||||
00E2 = Bernie Gran Squall
|
||||
00E7 = Defeated Kireek in waterfall of tears
|
||||
00E8 = Black paper subplot in black paper. defeated Kireek...
|
||||
00EB = Black paper subplot in from the depths. Defeated Kireek and got soul eater!
|
||||
00F1 = Secret delivery. Started the Weapons subplot //is cleared if quest is left
|
||||
00F3 = Weapon badge approval for claiming the snake //is cleared if quest is left
|
||||
00F4 = Weapon badge approval for the lost bride //is cleared if quest is left
|
||||
00F5 = Weapon badge approval for gran squall //is cleared if quest is left
|
||||
00F6 = Secret delivery. Got AKIKO's FRYING PAN!
|
||||
00FB = Got Orochi-agito
|
||||
00FB = Received OROCHI-AGITO!
|
||||
00FD = Unknown addicting food
|
||||
0105 = Central dome fire swirl. Got Glory of the past!
|
||||
0106 = Central dome fire swirl. Got Mark3.
|
||||
0107 = Central dome fire swirl. got Sonic knuckles
|
||||
0108 = Central dome fire swirl. got mail from BOGARDE
|
||||
0109 = Central dome fire swirl. got mail from ANNA
|
||||
010A = Central dome fire swirl. got mail from NADJA
|
||||
010B = Central dome fire swirl. got mail from Lionel
|
||||
010C = Soul of the blacksmith. Got one of the 3 special weapons!
|
||||
010D = Donoph Baz dies The Retired Hunter
|
||||
010E = Seat of heart unknown
|
||||
010F = Seat of heart unknown
|
||||
0110 = Seat of heart unknown
|
||||
0111 = Seat of heart unknown
|
||||
0112 = Seat of heart unknown
|
||||
0113 = Seat of heart unknown
|
||||
0187 = Soul of steel. Got Marina's bag! //dreamcast
|
||||
0188 = Soul of steel. Unknown.
|
||||
0191 = Capsule Elly VR
|
||||
0197 = Cleared VR Temple
|
||||
01AD = Capsule elly CCA
|
||||
01AE = Capsule elly CCA
|
||||
01B3 = After reading a capsule from flowen
|
||||
01D6 = Set after unlocking vr spaceship
|
||||
01F5 = Episode1: Cleared government 1-1
|
||||
01F7 = Episode1: Cleared government 1-2
|
||||
01F9 = Episode1: Cleared government 1-3
|
||||
01FB = Episode1: Cleared government 2-1
|
||||
01FD = Episode1: Cleared government 2-2
|
||||
01FF = Episode1: Cleared government 2-3
|
||||
0201 = Episode1: Cleared government 2-4
|
||||
0203 = Episode1: Cleared government 3-1
|
||||
0205 = Episode1: Cleared government 3-2
|
||||
0207 = Episode1: Cleared government 3-3
|
||||
0209 = Episode1: Cleared government 4-1
|
||||
020B = Episode1: Cleared government 4-2
|
||||
020D = Episode1: Cleared government 4-3
|
||||
020F = Episode1: Cleared government 4-4
|
||||
0211 = Episode1: Cleared government 4-5
|
||||
0213 = Episode2: Cleared government 5-1 // Talked to Tekker (?)
|
||||
0214 = Entered Forest 1
|
||||
0215 = Episode2: Cleared government 5-2
|
||||
0217 = Episode2: Cleared government 5-3 // Defeated Dragon (?)
|
||||
0219 = Episode2: Cleared government 5-4
|
||||
021B = Episode2: Cleared government 5-5
|
||||
021D = Episode2: Cleared government 6-1
|
||||
021F = Episode2: Cleared government 6-2
|
||||
0220 = Defeated De Rol Le
|
||||
0221 = Episode2: Cleared government 6-3
|
||||
0223 = Episode2: Cleared government 6-4
|
||||
0225 = Episode2: Cleared government 6-5
|
||||
0227 = Episode2: Cleared government 7-1
|
||||
0229 = Episode2: Cleared government 7-2
|
||||
022A = Defeated Vol Opt (002A and 022A together on hard mode)
|
||||
022B = Episode2: Cleared government 7-3 // Rico capsule after Vol Opt, at Ruins door (?)
|
||||
022D = Episode2: Cleared government 7-4 // Entered Caves 2 (?)
|
||||
022F = Episode2: Cleared government 7-5
|
||||
0230 = Entered Ruins 1
|
||||
0231 = Episode2: Cleared government 8-1
|
||||
0233 = Episode2: Cleared government 8-2
|
||||
0234 = Entered Falz 2
|
||||
0235 = Episode2: Cleared government 8-3
|
||||
0246 = Activated Jungle East big door switch
|
||||
0248 = Activated Seaside big door switch
|
||||
024F = Defeated Gol Dragon
|
||||
0252 = Defeated Gal Gryphon
|
||||
02BD = Episode4: Cleared government 9-1
|
||||
02BE = Episode4: Cleared government 9-2
|
||||
02BF = Episode4: Cleared government 9-3
|
||||
02C0 = Episode4: Cleared government 9-4
|
||||
02C1 = Episode4: Cleared government 9-5
|
||||
02C2 = Episode4: Cleared government 9-6
|
||||
02C3 = Episode4: Cleared government 9-7
|
||||
02C4 = Episode4: Cleared government 9-8
|
||||
0314 = Entered Forest 1
|
||||
0330 = Entered Ruins 1
|
||||
03FA = P2 Menu 7, G-Counter // Talked to Momoka
|
||||
03FB = Nol start
|
||||
03FC = Cleared Ep2 government on ultimate
|
||||
03FE = Cleared Ep2 government on normal-vh
|
||||
@@ -0,0 +1,21 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
|
||||
async def main():
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.ws_connect("ws://localhost:5050/y/rare-drops/stream") as ws:
|
||||
async for msg in ws:
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
data = msg.json()
|
||||
print(f"Received message: {data}")
|
||||
elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
print(f"Received binary data: {msg.data}")
|
||||
elif msg.type == aiohttp.WSMsgType.CLOSE:
|
||||
break
|
||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,27 @@
|
||||
### T FLAG NAME REQUIREMENTS AVAILABLE_IF ENABLED_IF
|
||||
001 1 0065 Magnitude of Metal !F_0065 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
002 1 0067 Claiming A Stake !F_0067 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
003 3 0069 The Value of Money T1, Caves F_0065 && F_0067 && F_006B && F_01F9 !F_0069 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
004 1 006B Battle Training !F_006B || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
005 2 006D Journalistic Pursuit T1 F_0065 && F_0067 && F_006B !F_006D || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
006 2 006F The Fake in yellow T1 F_0065 && F_0067 && F_006B !F_006F || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
007 2 0071 Native Research T1 F_0065 && F_0067 && F_006B !F_0071 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
008 2 0073 Forest of Sorrow 007 F_0071 !F_0073 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
009 2 0075 Gran Squall T1 F_0065 && F_0067 && F_006B !F_0075 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
010 3 0077 Addicting Food T1 F_0065 && F_0067 && F_006B !F_0077 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
011 3 0079 The Lost Bride T1 F_0065 && F_0067 && F_006B !F_0079 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
012 3 007B Waterfall tears 010, 011, 014, 017 F_0077 && F_0079 && F_007F && F_0085 !F_007B || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
013 3 007D Black Paper 012 F_007B !F_007D || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
014 3 007F Secret Delivery T1 F_0065 && F_0067 && F_006B !F_007F || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
015 3 0081 Soul of a Blacksmith 010, 011, 014, 017 F_0077 && F_0079 && F_007F && F_0085 !F_0081 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
016 3 0083 Letter from Lionel T1, Mines F_0065 && F_0067 && F_006B && F_0201 !F_0083 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
017 3 0085 The Grave's Butler T1 F_0065 && F_0067 && F_006B !F_0085 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
018 4 0087 Knowing One's Heart T1, Mines F_0065 && F_0067 && F_006B && F_0201 !F_0087 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
019 5 0089 Retired Hunter T1, Ruins F_0065 && F_0067 && F_006B && F_0207 !F_0089 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
020 4 008B Dr. Osto's Research T1, Mines F_0065 && F_0067 && F_006B && F_0201 !F_008B || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
021 4 008D The Unsealed Door 020, 014 F_008B && F_007F !F_008D || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
022 5 008F Soul of Steel 023 F_0091 !F_008F || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
023 5 0091 Doc's Secret Plan 014, Ruins F_007F && F_0207 !F_0091 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
024 5 0093 Seek My Master T1, Ruins F_0065 && F_0067 && F_006B && F_0207 !F_0093 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
025 5 0095 From the Depths 023 F_0091 !F_0095 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)
|
||||
026 2 009B Central Dome Fire Swirl 008 F_0073
|
||||
+32
-16
@@ -136,6 +136,7 @@ phosg::JSON BBLicense::json() const {
|
||||
Account::Account(const phosg::JSON& json)
|
||||
: account_id(0),
|
||||
flags(0),
|
||||
user_flags(0),
|
||||
ban_end_time(0),
|
||||
ep3_current_meseta(0),
|
||||
ep3_total_meseta_earned(0),
|
||||
@@ -222,6 +223,7 @@ Account::Account(const phosg::JSON& json)
|
||||
}
|
||||
|
||||
this->flags = json.get_int("Flags", 0);
|
||||
this->user_flags = json.get_int("UserFlags", 0);
|
||||
this->ban_end_time = json.get_int("BanEndTime", 0);
|
||||
this->last_player_name = json.get_string("LastPlayerName", "");
|
||||
this->auto_reply_message = json.get_string("AutoReplyMessage", "");
|
||||
@@ -278,6 +280,7 @@ phosg::JSON Account::json() const {
|
||||
{"XBLicenses", std::move(xb_json)},
|
||||
{"BBLicenses", std::move(bb_json)},
|
||||
{"Flags", this->flags},
|
||||
{"UserFlags", this->user_flags},
|
||||
{"BanEndTime", this->ban_end_time},
|
||||
{"LastPlayerName", this->last_player_name},
|
||||
{"AutoReplyMessage", this->auto_reply_message},
|
||||
@@ -300,34 +303,34 @@ void Account::print(FILE* stream) const {
|
||||
} else if (this->flags == static_cast<uint32_t>(Flag::MODERATOR)) {
|
||||
flags_str = "MODERATOR";
|
||||
} else {
|
||||
if (this->flags & static_cast<uint32_t>(Flag::KICK_USER)) {
|
||||
if (this->check_flag(Flag::KICK_USER)) {
|
||||
flags_str += "KICK_USER,";
|
||||
}
|
||||
if (this->flags & static_cast<uint32_t>(Flag::BAN_USER)) {
|
||||
if (this->check_flag(Flag::BAN_USER)) {
|
||||
flags_str += "BAN_USER,";
|
||||
}
|
||||
if (this->flags & static_cast<uint32_t>(Flag::SILENCE_USER)) {
|
||||
if (this->check_flag(Flag::SILENCE_USER)) {
|
||||
flags_str += "SILENCE_USER,";
|
||||
}
|
||||
if (this->flags & static_cast<uint32_t>(Flag::CHANGE_EVENT)) {
|
||||
if (this->check_flag(Flag::CHANGE_EVENT)) {
|
||||
flags_str += "CHANGE_EVENT,";
|
||||
}
|
||||
if (this->flags & static_cast<uint32_t>(Flag::ANNOUNCE)) {
|
||||
if (this->check_flag(Flag::ANNOUNCE)) {
|
||||
flags_str += "ANNOUNCE,";
|
||||
}
|
||||
if (this->flags & static_cast<uint32_t>(Flag::FREE_JOIN_GAMES)) {
|
||||
if (this->check_flag(Flag::FREE_JOIN_GAMES)) {
|
||||
flags_str += "FREE_JOIN_GAMES,";
|
||||
}
|
||||
if (this->flags & static_cast<uint32_t>(Flag::DEBUG)) {
|
||||
if (this->check_flag(Flag::DEBUG)) {
|
||||
flags_str += "DEBUG,";
|
||||
}
|
||||
if (this->flags & static_cast<uint32_t>(Flag::CHEAT_ANYWHERE)) {
|
||||
if (this->check_flag(Flag::CHEAT_ANYWHERE)) {
|
||||
flags_str += "CHEAT_ANYWHERE,";
|
||||
}
|
||||
if (this->flags & static_cast<uint32_t>(Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
if (this->check_flag(Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
flags_str += "ALWAYS_ENABLE_CHAT_COMMANDS,";
|
||||
}
|
||||
if (this->flags & static_cast<uint32_t>(Flag::IS_SHARED_ACCOUNT)) {
|
||||
if (this->check_flag(Flag::IS_SHARED_ACCOUNT)) {
|
||||
flags_str += "IS_SHARED_ACCOUNT,";
|
||||
}
|
||||
}
|
||||
@@ -339,6 +342,19 @@ void Account::print(FILE* stream) const {
|
||||
fprintf(stream, " Flags: %08" PRIX32 " (%s)\n", this->flags, flags_str.c_str());
|
||||
}
|
||||
|
||||
if (this->user_flags) {
|
||||
string user_flags_str = "";
|
||||
if (this->check_user_flag(UserFlag::DISABLE_DROP_NOTIFICATION_BROADCAST)) {
|
||||
user_flags_str += "DISABLE_DROP_NOTIFICATION_BROADCAST,";
|
||||
}
|
||||
if (user_flags_str.empty()) {
|
||||
user_flags_str = "none";
|
||||
} else if (phosg::ends_with(user_flags_str, ",")) {
|
||||
user_flags_str.pop_back();
|
||||
}
|
||||
fprintf(stream, " User flags: %08" PRIX32 " (%s)\n", this->user_flags, user_flags_str.c_str());
|
||||
}
|
||||
|
||||
if (this->ban_end_time) {
|
||||
string time_str = phosg::format_time(this->ban_end_time);
|
||||
fprintf(stream, " Banned until: %" PRIu64 " (%s)\n", this->ban_end_time, time_str.c_str());
|
||||
@@ -421,7 +437,7 @@ shared_ptr<Login> AccountIndex::from_dc_nte_credentials_locked(const string& ser
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (login->account->ban_end_time && (login->account->ban_end_time >= phosg::now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
throw account_banned();
|
||||
}
|
||||
return login;
|
||||
}
|
||||
@@ -471,7 +487,7 @@ shared_ptr<Login> AccountIndex::from_dc_credentials_locked(
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (login->account->ban_end_time && (login->account->ban_end_time >= phosg::now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
throw account_banned();
|
||||
}
|
||||
if (is_shared) {
|
||||
login->account = this->create_temporary_account_for_shared_account(login->account, access_key + ":" + character_name);
|
||||
@@ -544,7 +560,7 @@ shared_ptr<Login> AccountIndex::from_pc_credentials_locked(
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (login->account->ban_end_time && (login->account->ban_end_time >= phosg::now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
throw account_banned();
|
||||
}
|
||||
if (is_shared) {
|
||||
login->account = this->create_temporary_account_for_shared_account(login->account, access_key + ":" + character_name);
|
||||
@@ -600,7 +616,7 @@ shared_ptr<Login> AccountIndex::from_gc_credentials_locked(
|
||||
throw incorrect_password();
|
||||
}
|
||||
if (login->account->ban_end_time && (login->account->ban_end_time >= phosg::now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
throw account_banned();
|
||||
}
|
||||
if (is_shared) {
|
||||
login->account = this->create_temporary_account_for_shared_account(login->account, access_key + ":" + character_name);
|
||||
@@ -653,7 +669,7 @@ shared_ptr<Login> AccountIndex::from_xb_credentials_locked(const string& gamerta
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (login->account->ban_end_time && (login->account->ban_end_time >= phosg::now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
throw account_banned();
|
||||
}
|
||||
return login;
|
||||
}
|
||||
@@ -702,7 +718,7 @@ shared_ptr<Login> AccountIndex::from_bb_credentials_locked(const string& usernam
|
||||
throw incorrect_password();
|
||||
}
|
||||
if (login->account->ban_end_time && (login->account->ban_end_time >= phosg::now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
throw account_banned();
|
||||
}
|
||||
return login;
|
||||
}
|
||||
|
||||
@@ -76,11 +76,15 @@ struct Account {
|
||||
UNUSED_BITS = 0x70FFFF00,
|
||||
// clang-format on
|
||||
};
|
||||
enum class UserFlag : uint32_t {
|
||||
DISABLE_DROP_NOTIFICATION_BROADCAST = 0x00000001,
|
||||
};
|
||||
|
||||
// account_id is also the account's guild card number
|
||||
uint32_t account_id = 0;
|
||||
|
||||
uint32_t flags = 0;
|
||||
uint32_t user_flags = 0;
|
||||
uint64_t ban_end_time = 0; // 0 = not banned
|
||||
std::string last_player_name;
|
||||
std::string auto_reply_message;
|
||||
@@ -124,6 +128,19 @@ struct Account {
|
||||
this->flags = static_cast<uint32_t>(mask);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool check_user_flag(UserFlag flag) const {
|
||||
return !!(this->user_flags & static_cast<uint32_t>(flag));
|
||||
}
|
||||
inline void set_user_flag(UserFlag flag) {
|
||||
this->user_flags |= static_cast<uint32_t>(flag);
|
||||
}
|
||||
inline void clear_user_flag(UserFlag flag) {
|
||||
this->user_flags &= (~static_cast<uint32_t>(flag));
|
||||
}
|
||||
inline void toggle_user_flag(UserFlag flag) {
|
||||
this->user_flags ^= static_cast<uint32_t>(flag);
|
||||
}
|
||||
|
||||
void print(FILE* stream) const;
|
||||
};
|
||||
|
||||
@@ -159,6 +176,10 @@ public:
|
||||
public:
|
||||
missing_account() : invalid_argument("missing account") {}
|
||||
};
|
||||
class account_banned : public std::invalid_argument {
|
||||
public:
|
||||
account_banned() : invalid_argument("account is banned") {}
|
||||
};
|
||||
|
||||
explicit AccountIndex(bool force_all_temporary);
|
||||
virtual ~AccountIndex() = default;
|
||||
|
||||
+137
-78
@@ -71,22 +71,25 @@ static void check_debug_enabled(shared_ptr<Client> c) {
|
||||
}
|
||||
}
|
||||
|
||||
static void check_cheats_enabled(shared_ptr<Lobby> l, shared_ptr<Client> c) {
|
||||
if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) &&
|
||||
static void check_cheats_enabled(shared_ptr<Lobby> l, shared_ptr<Client> c, bool behavior_is_cheating) {
|
||||
if (behavior_is_cheating &&
|
||||
!l->check_flag(Lobby::Flag::CHEATS_ENABLED) &&
|
||||
!c->login->account->check_flag(Account::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) &&
|
||||
static void check_cheats_allowed(shared_ptr<ServerState> s, shared_ptr<Client> c, bool behavior_is_cheating) {
|
||||
if (behavior_is_cheating &&
|
||||
(s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) &&
|
||||
!c->login->account->check_flag(Account::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) &&
|
||||
static void check_cheats_allowed(shared_ptr<ServerState> s, shared_ptr<ProxyServer::LinkedSession> ses, bool behavior_is_cheating) {
|
||||
if (behavior_is_cheating &&
|
||||
(s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) &&
|
||||
(!ses->login || !ses->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE))) {
|
||||
throw precondition_failed("$C6Cheats are disabled\non this proxy.");
|
||||
}
|
||||
@@ -104,12 +107,20 @@ static void check_is_leader(shared_ptr<Lobby> l, shared_ptr<Client> c) {
|
||||
static void server_command_server_info(shared_ptr<Client> c, const std::string&) {
|
||||
auto s = c->require_server_state();
|
||||
string uptime_str = phosg::format_duration(phosg::now() - s->creation_time);
|
||||
send_text_message_printf(c,
|
||||
"Uptime: $C6%s$C7\nLobbies: $C6%zu$C7\nClients: $C6%zu$C7(g) $C6%zu$C7(p)",
|
||||
uptime_str.c_str(),
|
||||
s->id_to_lobby.size(),
|
||||
s->channel_to_client.size(),
|
||||
s->proxy_server->num_sessions());
|
||||
if (s->proxy_server) {
|
||||
send_text_message_printf(c,
|
||||
"Uptime: $C6%s$C7\nLobbies: $C6%zu$C7\nClients: $C6%zu$C7(g) $C6%zu$C7(p)",
|
||||
uptime_str.c_str(),
|
||||
s->id_to_lobby.size(),
|
||||
s->channel_to_client.size(),
|
||||
s->proxy_server->num_sessions());
|
||||
} else {
|
||||
send_text_message_printf(c,
|
||||
"Uptime: $C6%s$C7\nLobbies: $C6%zu$C7\nClients: $C6%zu",
|
||||
uptime_str.c_str(),
|
||||
s->id_to_lobby.size(),
|
||||
s->channel_to_client.size());
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_lobby_info(shared_ptr<Client> c, const std::string&) {
|
||||
@@ -267,16 +278,37 @@ static void server_command_ax(shared_ptr<Client> c, const std::string& args) {
|
||||
ax_messages_log.info("%s", args.c_str());
|
||||
}
|
||||
|
||||
static void server_command_announce(shared_ptr<Client> c, const std::string& args) {
|
||||
static void server_command_announce_inner(shared_ptr<Client> c, const std::string& args, bool mail, bool anonymous) {
|
||||
auto s = c->require_server_state();
|
||||
check_account_flag(c, Account::Flag::ANNOUNCE);
|
||||
send_text_message(s, args);
|
||||
if (anonymous) {
|
||||
if (mail) {
|
||||
send_simple_mail(s, 0, s->name, args);
|
||||
} else {
|
||||
send_text_or_scrolling_message(s, args, args);
|
||||
}
|
||||
} else {
|
||||
auto from_name = c->character()->disp.name.decode(c->language());
|
||||
if (mail) {
|
||||
send_simple_mail(s, 0, from_name, args);
|
||||
} else {
|
||||
auto message = from_name + ": " + args;
|
||||
send_text_or_scrolling_message(s, message, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_announce_mail(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
check_account_flag(c, Account::Flag::ANNOUNCE);
|
||||
send_simple_mail(s, 0, s->name, args);
|
||||
static void server_command_announce_named(shared_ptr<Client> c, const std::string& args) {
|
||||
server_command_announce_inner(c, args, false, false);
|
||||
}
|
||||
static void server_command_announce_anonymous(shared_ptr<Client> c, const std::string& args) {
|
||||
server_command_announce_inner(c, args, false, true);
|
||||
}
|
||||
static void server_command_announce_mail_named(shared_ptr<Client> c, const std::string& args) {
|
||||
server_command_announce_inner(c, args, true, false);
|
||||
}
|
||||
static void server_command_announce_mail_anonymous(shared_ptr<Client> c, const std::string& args) {
|
||||
server_command_announce_inner(c, args, true, true);
|
||||
}
|
||||
|
||||
static void server_command_arrow(shared_ptr<Client> c, const std::string& args) {
|
||||
@@ -864,7 +896,6 @@ static void server_command_exit(shared_ptr<Client> c, const std::string&) {
|
||||
G_UnusedHeader cmd = {0x73, 0x01, 0x0000};
|
||||
c->channel.send(0x60, 0x00, cmd);
|
||||
c->floor = 0;
|
||||
c->recent_switch_flags.clear();
|
||||
} else if (is_ep3(c->version())) {
|
||||
c->channel.send(0xED, 0x00);
|
||||
} else {
|
||||
@@ -973,7 +1004,9 @@ static void server_command_cheat(shared_ptr<Client> c, const std::string&) {
|
||||
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->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) {
|
||||
if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) &&
|
||||
!c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE) &&
|
||||
s->cheat_flags.insufficient_minimum_level) {
|
||||
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;
|
||||
@@ -1148,7 +1181,8 @@ static void server_command_meseta(shared_ptr<Client> c, const std::string& args)
|
||||
|
||||
static void server_command_secid(shared_ptr<Client> c, const std::string& args) {
|
||||
auto l = c->require_lobby();
|
||||
check_cheats_allowed(c->require_server_state(), c);
|
||||
auto s = c->require_server_state();
|
||||
check_cheats_allowed(s, c, s->cheat_flags.override_section_id);
|
||||
|
||||
uint8_t new_override_section_id;
|
||||
|
||||
@@ -1173,7 +1207,9 @@ static void server_command_secid(shared_ptr<Client> c, const std::string& args)
|
||||
}
|
||||
|
||||
static void proxy_command_secid(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
check_cheats_allowed(ses->require_server_state(), ses);
|
||||
auto s = ses->require_server_state();
|
||||
check_cheats_allowed(s, ses, s->cheat_flags.override_section_id);
|
||||
|
||||
if (!args[0]) {
|
||||
ses->config.override_section_id = 0xFF;
|
||||
send_text_message(ses->client_channel, "$C6Override section ID\nremoved");
|
||||
@@ -1195,7 +1231,7 @@ static void server_command_variations(shared_ptr<Client> c, const std::string& a
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, false);
|
||||
check_cheats_allowed(s, c);
|
||||
check_cheats_allowed(s, c, s->cheat_flags.override_variations);
|
||||
|
||||
c->override_variations = make_unique<parray<le_uint32_t, 0x20>>();
|
||||
c->override_variations->clear(0);
|
||||
@@ -1208,7 +1244,7 @@ static void server_command_rand(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, false);
|
||||
check_cheats_allowed(s, c);
|
||||
check_cheats_allowed(s, c, s->cheat_flags.override_random_seed);
|
||||
|
||||
if (!args[0]) {
|
||||
c->config.clear_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED);
|
||||
@@ -1222,7 +1258,8 @@ static void server_command_rand(shared_ptr<Client> c, const std::string& args) {
|
||||
}
|
||||
|
||||
static void proxy_command_rand(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
check_cheats_allowed(ses->require_server_state(), ses);
|
||||
auto s = ses->require_server_state();
|
||||
check_cheats_allowed(s, ses, s->cheat_flags.override_random_seed);
|
||||
if (!args[0]) {
|
||||
ses->config.clear_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED);
|
||||
ses->config.override_random_seed = 0;
|
||||
@@ -1293,7 +1330,7 @@ static void server_command_min_level(shared_ptr<Client> c, const std::string& ar
|
||||
auto s = c->require_server_state();
|
||||
bool cheats_allowed = (l->check_flag(Lobby::Flag::CHEATS_ENABLED) ||
|
||||
c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE));
|
||||
if (!cheats_allowed) {
|
||||
if (!cheats_allowed && s->cheat_flags.insufficient_minimum_level) {
|
||||
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);
|
||||
@@ -1343,28 +1380,28 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
|
||||
|
||||
try {
|
||||
auto p = c->character();
|
||||
if (tokens.at(0) == "atp" && cheats_allowed) {
|
||||
if (tokens.at(0) == "atp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.char_stats.atp = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "mst" && cheats_allowed) {
|
||||
} else if (tokens.at(0) == "mst" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.char_stats.mst = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "evp" && cheats_allowed) {
|
||||
} else if (tokens.at(0) == "evp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.char_stats.evp = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "hp" && cheats_allowed) {
|
||||
} else if (tokens.at(0) == "hp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.char_stats.hp = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "dfp" && cheats_allowed) {
|
||||
} else if (tokens.at(0) == "dfp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.char_stats.dfp = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "ata" && cheats_allowed) {
|
||||
} else if (tokens.at(0) == "ata" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.char_stats.ata = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "lck" && cheats_allowed) {
|
||||
} else if (tokens.at(0) == "lck" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.char_stats.lck = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "meseta" && cheats_allowed) {
|
||||
} else if (tokens.at(0) == "meseta" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.meseta = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "exp" && cheats_allowed) {
|
||||
} else if (tokens.at(0) == "exp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.experience = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "level" && cheats_allowed) {
|
||||
} else if (tokens.at(0) == "level" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.level = stoul(tokens.at(1)) - 1;
|
||||
p->recompute_stats(s->level_table(c->version()));
|
||||
} else if (((tokens.at(0) == "material") || (tokens.at(0) == "mat")) && !is_v1_or_v2(c->version())) {
|
||||
} else if (((tokens.at(0) == "material") || (tokens.at(0) == "mat")) && !is_v1_or_v2(c->version()) && (cheats_allowed || !s->cheat_flags.reset_materials)) {
|
||||
if (tokens.at(1) == "reset") {
|
||||
const auto& which = tokens.at(2);
|
||||
if (which == "power") {
|
||||
@@ -1418,9 +1455,13 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
|
||||
p->guild_card.language = new_language;
|
||||
auto sys = c->system_file(false);
|
||||
if (sys) {
|
||||
sys->base.language = new_language;
|
||||
sys->language = new_language;
|
||||
}
|
||||
} else if (tokens.at(0) == "secid") {
|
||||
if (!cheats_allowed && (p->disp.stats.level > 0) && s->cheat_flags.edit_section_id) {
|
||||
send_text_message(c, "$C6You cannot change\nyour Section ID\nafter level 1");
|
||||
return;
|
||||
}
|
||||
} 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");
|
||||
@@ -1443,7 +1484,7 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
|
||||
p->disp.visual.unused.clear(0);
|
||||
}
|
||||
} else {
|
||||
uint8_t npc = npc_for_name(tokens.at(1));
|
||||
uint8_t npc = npc_for_name(tokens.at(1), c->version());
|
||||
if (npc == 0xFF) {
|
||||
send_text_message(c, "$C6No such NPC");
|
||||
return;
|
||||
@@ -1459,6 +1500,7 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
|
||||
replacement_class = 0x01;
|
||||
break;
|
||||
case 0: // Ninja (replace with HUmar)
|
||||
case 2: // Sonic (replace with HUmar)
|
||||
case 5: // Flowen (replace with HUmar)
|
||||
replacement_class = 0x00;
|
||||
break;
|
||||
@@ -1478,7 +1520,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" && cheats_allowed) {
|
||||
} else if (tokens.at(0) == "tech" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
uint8_t level = stoul(tokens.at(2)) - 1;
|
||||
if (tokens.at(1) == "all") {
|
||||
for (size_t x = 0; x < 0x14; x++) {
|
||||
@@ -1686,8 +1728,8 @@ static void server_command_loadchar(shared_ptr<Client> c, const std::string& arg
|
||||
};
|
||||
|
||||
if (c->version() == Version::DC_V2) {
|
||||
auto dc_char = make_shared<PSODCV2CharacterFile>(c->character()->to_dc_v2());
|
||||
send_set_extended_player_info.operator()<PSODCV2CharacterFile>(c, dc_char);
|
||||
auto dc_char = make_shared<PSODCV2CharacterFile::Character>(c->character()->to_dc_v2());
|
||||
send_set_extended_player_info.operator()<PSODCV2CharacterFile::Character>(c, dc_char);
|
||||
} else if (c->version() == Version::GC_NTE) {
|
||||
auto gc_char = make_shared<PSOGCNTECharacterFileCharacter>(c->character()->to_gc_nte());
|
||||
send_set_extended_player_info.operator()<PSOGCNTECharacterFileCharacter>(c, gc_char);
|
||||
@@ -1849,29 +1891,13 @@ static void server_command_warp(shared_ptr<Client> c, const std::string& args, b
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l, c, s->cheat_flags.warp);
|
||||
|
||||
uint32_t floor = stoul(args, nullptr, 0);
|
||||
if (c->floor == floor) {
|
||||
if (!is_warpall && (c->floor == floor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: $warp[me] 0 is allowed in boss arenas if the boss is already
|
||||
// defeated, even if cheats are disabled. This is because if a player returns
|
||||
// to a boss arena after a persistence gap in the game, the exit warp won't
|
||||
// exist, so they need a way to get out.
|
||||
bool should_check_cheats = is_warpall || (floor != 0) || !floor_is_boss_arena(l->episode, c->floor);
|
||||
if (!should_check_cheats) {
|
||||
for (const auto* event : l->map->get_events(c->floor)) {
|
||||
if (!(event->flags & 0x18)) {
|
||||
should_check_cheats = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (should_check_cheats) {
|
||||
check_cheats_enabled(l, c);
|
||||
}
|
||||
|
||||
size_t limit = floor_limit_for_episode(l->episode);
|
||||
if (limit == 0) {
|
||||
return;
|
||||
@@ -1897,7 +1923,7 @@ static void server_command_warpall(shared_ptr<Client> c, const std::string& args
|
||||
|
||||
static void proxy_command_warp(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args, bool is_warpall) {
|
||||
auto s = ses->require_server_state();
|
||||
check_cheats_allowed(s, ses);
|
||||
check_cheats_allowed(s, ses, s->cheat_flags.warp);
|
||||
if (!ses->is_in_game) {
|
||||
send_text_message(ses->client_channel, "$C6You must be in a\ngame to use this\ncommand");
|
||||
return;
|
||||
@@ -1922,7 +1948,7 @@ static void server_command_next(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);
|
||||
check_cheats_enabled(l, c, s->cheat_flags.warp);
|
||||
|
||||
size_t limit = floor_limit_for_episode(l->episode);
|
||||
if (limit == 0) {
|
||||
@@ -1933,7 +1959,7 @@ static void server_command_next(shared_ptr<Client> c, const std::string&) {
|
||||
|
||||
static void proxy_command_next(shared_ptr<ProxyServer::LinkedSession> ses, const std::string&) {
|
||||
auto s = ses->require_server_state();
|
||||
check_cheats_allowed(s, ses);
|
||||
check_cheats_allowed(s, ses, s->cheat_flags.warp);
|
||||
if (!ses->is_in_game) {
|
||||
send_text_message(ses->client_channel, "$C6You must be in a\ngame to use this\ncommand");
|
||||
return;
|
||||
@@ -2034,25 +2060,25 @@ static void server_command_infinite_hp(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);
|
||||
check_cheats_enabled(l, c, s->cheat_flags.infinite_hp_tp);
|
||||
|
||||
c->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED);
|
||||
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()) {
|
||||
send_remove_conditions(c);
|
||||
send_remove_negative_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);
|
||||
check_cheats_allowed(s, ses, s->cheat_flags.infinite_hp_tp);
|
||||
ses->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED);
|
||||
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) {
|
||||
send_remove_conditions(ses->client_channel, ses->lobby_client_id);
|
||||
send_remove_conditions(ses->server_channel, ses->lobby_client_id);
|
||||
send_remove_negative_conditions(ses->client_channel, ses->lobby_client_id);
|
||||
send_remove_negative_conditions(ses->server_channel, ses->lobby_client_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2060,7 +2086,7 @@ static void server_command_infinite_tp(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);
|
||||
check_cheats_enabled(l, c, s->cheat_flags.infinite_hp_tp);
|
||||
|
||||
c->config.toggle_flag(Client::Flag::INFINITE_TP_ENABLED);
|
||||
send_text_message_printf(c, "$C6Infinite TP %s", c->config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? "enabled" : "disabled");
|
||||
@@ -2068,7 +2094,7 @@ static void server_command_infinite_tp(shared_ptr<Client> c, const std::string&)
|
||||
|
||||
static void proxy_command_infinite_tp(shared_ptr<ProxyServer::LinkedSession> ses, const std::string&) {
|
||||
auto s = ses->require_server_state();
|
||||
check_cheats_allowed(s, ses);
|
||||
check_cheats_allowed(s, ses, s->cheat_flags.infinite_hp_tp);
|
||||
ses->config.toggle_flag(Client::Flag::INFINITE_TP_ENABLED);
|
||||
send_text_message_printf(ses->client_channel, "$C6Infinite TP %s",
|
||||
ses->config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? "enabled" : "disabled");
|
||||
@@ -2091,6 +2117,13 @@ static void proxy_command_switch_assist(shared_ptr<ProxyServer::LinkedSession> s
|
||||
ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
static void server_command_toggle_rare_announce(shared_ptr<Client> c, const std::string&) {
|
||||
c->login->account->toggle_user_flag(Account::UserFlag::DISABLE_DROP_NOTIFICATION_BROADCAST);
|
||||
c->login->account->save();
|
||||
send_text_message_printf(c, "$C6Rare announcements\n%s for your\nitems",
|
||||
c->login->account->check_user_flag(Account::UserFlag::DISABLE_DROP_NOTIFICATION_BROADCAST) ? "disabled" : "enabled");
|
||||
}
|
||||
|
||||
static void server_command_dropmode(shared_ptr<Client> c, const std::string& args) {
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, true);
|
||||
@@ -2158,7 +2191,8 @@ static void server_command_dropmode(shared_ptr<Client> c, const std::string& arg
|
||||
}
|
||||
|
||||
static void proxy_command_dropmode(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
check_cheats_allowed(ses->require_server_state(), ses);
|
||||
auto s = ses->require_server_state();
|
||||
check_cheats_allowed(ses->require_server_state(), ses, s->cheat_flags.proxy_override_drops);
|
||||
|
||||
using DropMode = ProxyServer::LinkedSession::DropMode;
|
||||
if (args.empty()) {
|
||||
@@ -2206,7 +2240,7 @@ static void server_command_item(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_cheats_enabled(l, c);
|
||||
check_cheats_enabled(l, c, s->cheat_flags.create_items);
|
||||
|
||||
ItemData item = s->parse_item_description(c->version(), args);
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
@@ -2225,7 +2259,7 @@ static void server_command_item(shared_ptr<Client> c, const std::string& args) {
|
||||
|
||||
static void proxy_command_item(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
auto s = ses->require_server_state();
|
||||
check_cheats_allowed(s, ses);
|
||||
check_cheats_allowed(s, ses, s->cheat_flags.create_items);
|
||||
if (ses->version() == Version::BB_V4) {
|
||||
send_text_message(ses->client_channel, "$C6This command cannot\nbe used on the proxy\nserver in BB games");
|
||||
return;
|
||||
@@ -2259,6 +2293,21 @@ static void proxy_command_item(shared_ptr<ProxyServer::LinkedSession> ses, const
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_enable_battle_mode_v1(shared_ptr<Client> c, const std::string&) {
|
||||
check_is_game(c->require_lobby(), false);
|
||||
if (!is_v1(c->version())) {
|
||||
send_text_message(c, "$C6This command can\nonly be used on\nDC v1 and earlier");
|
||||
return;
|
||||
}
|
||||
|
||||
c->config.toggle_flag(Client::Flag::FORCE_BATTLE_MODE_GAME);
|
||||
if (c->config.check_flag(Client::Flag::FORCE_BATTLE_MODE_GAME)) {
|
||||
send_text_message(c, "$C6Battle mode enabled\nfor next game");
|
||||
} else {
|
||||
send_text_message(c, "$C6Battle mode disabled\nfor next game");
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_enable_ep3_battle_debug_menu(shared_ptr<Client> c, const std::string& args) {
|
||||
check_debug_enabled(c);
|
||||
|
||||
@@ -2391,7 +2440,7 @@ static void server_command_ep3_replace_assist_card(shared_ptr<Client> c, const s
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, true);
|
||||
check_is_ep3(c, true);
|
||||
check_cheats_enabled(l, c);
|
||||
check_cheats_enabled(l, c, s->cheat_flags.ep3_replace_assist);
|
||||
|
||||
if (l->episode != Episode::EP3) {
|
||||
throw logic_error("non-Ep3 client in Ep3 game");
|
||||
@@ -2443,7 +2492,7 @@ static void server_command_ep3_unset_field_character(shared_ptr<Client> c, const
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, true);
|
||||
check_is_ep3(c, true);
|
||||
check_cheats_enabled(l, c);
|
||||
check_cheats_enabled(l, c, s->cheat_flags.ep3_unset_field_character);
|
||||
|
||||
if (l->episode != Episode::EP3) {
|
||||
throw logic_error("non-Ep3 client in Ep3 game");
|
||||
@@ -2476,6 +2525,11 @@ static void server_command_surrender(shared_ptr<Client> c, const std::string&) {
|
||||
send_text_message(c, "$C6Battle has not\nyet started");
|
||||
return;
|
||||
}
|
||||
auto ps = l->ep3_server->get_player_state(c->lobby_client_id);
|
||||
if (!ps || !ps->is_alive()) {
|
||||
send_text_message(c, "$C6Defeated players\ncannot surrender");
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
@@ -2575,13 +2629,18 @@ struct ChatCommandDefinition {
|
||||
|
||||
static const unordered_map<string, ChatCommandDefinition> chat_commands({
|
||||
{"$allevent", {server_command_lobby_event_all, nullptr}},
|
||||
{"$ann", {server_command_announce, nullptr}},
|
||||
{"$ann!", {server_command_announce_mail, nullptr}},
|
||||
{"$ann", {server_command_announce_named, nullptr}},
|
||||
{"$ann?", {server_command_announce_anonymous, nullptr}},
|
||||
{"$ann!", {server_command_announce_mail_named, nullptr}},
|
||||
{"$ann?!", {server_command_announce_mail_anonymous, nullptr}},
|
||||
{"$ann!?", {server_command_announce_mail_anonymous, nullptr}},
|
||||
{"$announcerares", {server_command_toggle_rare_announce, nullptr}},
|
||||
{"$arrow", {server_command_arrow, proxy_command_arrow}},
|
||||
{"$auction", {server_command_auction, proxy_command_auction}},
|
||||
{"$ax", {server_command_ax, nullptr}},
|
||||
{"$ban", {server_command_ban, nullptr}},
|
||||
{"$bank", {server_command_change_bank, nullptr}},
|
||||
{"$battle", {server_command_enable_battle_mode_v1, nullptr}},
|
||||
{"$bbchar", {server_command_bbchar, nullptr}},
|
||||
{"$cheat", {server_command_cheat, nullptr}},
|
||||
{"$debug", {server_command_debug, nullptr}},
|
||||
|
||||
+17
-63
@@ -83,14 +83,14 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x20: // DCNTE, possibly also DCv1 JP
|
||||
case 0x20: // DC NTE, 11/2000, possibly also DCv1 JP
|
||||
case 0x21: // DCv1 US
|
||||
case 0x22: // DCv1 EU, 12/2000, and 01/2001, at 50Hz (presumably)
|
||||
case 0x23: // DCv1 EU, 12/2000, and 01/2001, at 60Hz (presumably)
|
||||
this->set_flag(Flag::NO_D6);
|
||||
break;
|
||||
case 0x25: // DCv2 JP
|
||||
case 0x26: // DCv2 US
|
||||
case 0x26: // DCv2 US and 08/2001
|
||||
case 0x27: // DCv2 EU 50Hz (presumably)
|
||||
case 0x28: // DCv2 EU 60Hz (presumably)
|
||||
this->set_flag(Flag::NO_D6);
|
||||
@@ -103,7 +103,7 @@ 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.2, at least one version of XB
|
||||
case 0x30: // GC Ep1&2 GameJam demo, GC Ep1&2 Trial Edition, GC Ep1&2 JP v1.2, XB JP
|
||||
case 0x31: // GC Ep1&2 US v1.0, GC US v1.1, XB US
|
||||
this->set_flag(Flag::HAS_SEND_FUNCTION_CALL);
|
||||
break;
|
||||
@@ -119,7 +119,7 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
this->set_flag(Flag::ENCRYPTED_SEND_FUNCTION_CALL);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
break;
|
||||
case 0x3A: // GC Ep1&2 US v1.2 (Plus) GMK edition
|
||||
case 0x3A: // GC Ep1&2 US v1.2 (Plus) Return to Ragol
|
||||
this->set_flag(Flag::IS_CLIENT_CUSTOMIZATION);
|
||||
[[fallthrough]];
|
||||
case 0x36: // GC Ep1&2 US v1.2 (Plus)
|
||||
@@ -135,7 +135,7 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
// sub_version can't be used to tell JP final and Trial Edition apart; we
|
||||
// instead look at header.flag in the 61 command and set the version then.
|
||||
break;
|
||||
case 0x41: // GC Ep3 US (and BB)
|
||||
case 0x41: // GC Ep3 US (and BB, but BB is handled above)
|
||||
case 0x42: // GC Ep3 EU 50Hz
|
||||
case 0x43: // GC Ep3 EU 60Hz
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
@@ -759,32 +759,24 @@ void Client::load_all_files() {
|
||||
if (this->character_data) {
|
||||
player_data_log.info("Using loaded character file %s", char_filename.c_str());
|
||||
} else if (phosg::isfile(char_filename)) {
|
||||
auto f = phosg::fopen_unique(char_filename, "rb");
|
||||
auto header = phosg::freadx<PSOCommandHeaderBB>(f.get());
|
||||
if (header.size != 0x399C) {
|
||||
throw runtime_error("incorrect size in character file header");
|
||||
}
|
||||
if (header.command != 0x00E7) {
|
||||
throw runtime_error("incorrect command in character file header");
|
||||
}
|
||||
if (header.flag != 0x00000000) {
|
||||
throw runtime_error("incorrect flag in character file header");
|
||||
}
|
||||
static_assert(sizeof(PSOBBCharacterFile) + sizeof(PSOBBFullSystemFile) == 0x3994, ".psochar size is incorrect");
|
||||
this->character_data = make_shared<PSOBBCharacterFile>(phosg::freadx<PSOBBCharacterFile>(f.get()));
|
||||
auto psochar = load_psochar(char_filename, !this->system_data);
|
||||
this->character_data = psochar.character_file;
|
||||
files_manager->set_character(char_filename, this->character_data);
|
||||
player_data_log.info("Loaded character data from %s", char_filename.c_str());
|
||||
|
||||
// If there was no .psosys file, load the system file from the .psochar
|
||||
// If there was no .psosys file, use the system file from the .psochar
|
||||
// file instead
|
||||
if (!this->system_data) {
|
||||
this->system_data = make_shared<PSOBBBaseSystemFile>(phosg::freadx<PSOBBBaseSystemFile>(f.get()));
|
||||
if (!psochar.system_file) {
|
||||
throw logic_error("account system data not present, and also not loaded from psochar file");
|
||||
}
|
||||
this->system_data = psochar.system_file;
|
||||
files_manager->set_system(sys_filename, this->system_data);
|
||||
player_data_log.info("Loaded system data from %s", char_filename.c_str());
|
||||
}
|
||||
|
||||
this->update_character_data_after_load(this->character_data);
|
||||
this->system_data->base.language = this->language();
|
||||
this->system_data->language = this->language();
|
||||
|
||||
} else {
|
||||
player_data_log.info("Character file is missing: %s", char_filename.c_str());
|
||||
@@ -813,7 +805,7 @@ void Client::load_all_files() {
|
||||
throw runtime_error("account data header is incorrect");
|
||||
}
|
||||
if (!this->system_data) {
|
||||
this->system_data = make_shared<PSOBBBaseSystemFile>(nsa_data->system_file.base);
|
||||
this->system_data = make_shared<PSOBBBaseSystemFile>(nsa_data->system_file);
|
||||
files_manager->set_system(sys_filename, this->system_data);
|
||||
player_data_log.info("Loaded legacy system data from %s", nsa_filename.c_str());
|
||||
}
|
||||
@@ -949,23 +941,7 @@ void Client::save_character_file(
|
||||
const string& filename,
|
||||
shared_ptr<const PSOBBBaseSystemFile> system,
|
||||
shared_ptr<const PSOBBCharacterFile> character) {
|
||||
auto f = phosg::fopen_unique(filename, "wb");
|
||||
PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBBaseSystemFile) + sizeof(PSOBBTeamMembership), 0x00E7, 0x00000000};
|
||||
phosg::fwritex(f.get(), header);
|
||||
phosg::fwritex(f.get(), *character);
|
||||
phosg::fwritex(f.get(), *system);
|
||||
// TODO: Technically, we should write the actual team membership struct to the
|
||||
// file here, but that would cause Client to depend on Account, which
|
||||
// it currently does not. This data doesn't matter at all for correctness
|
||||
// within newserv, since it ignores this data entirely and instead generates
|
||||
// the membership struct from the team ID in the Account and the team's state.
|
||||
// So, writing correct data here would mostly be for compatibility with other
|
||||
// PSO servers. But if the other server is newserv, then this data would be
|
||||
// used anyway, and if it's not, then it would presumably have a different set
|
||||
// of teams with a different set of team IDs anyway, so the membership struct
|
||||
// here would be useless either way.
|
||||
static const PSOBBTeamMembership empty_membership;
|
||||
phosg::fwritex(f.get(), empty_membership);
|
||||
save_psochar(filename, system, character);
|
||||
player_data_log.info("Saved character file %s", filename.c_str());
|
||||
}
|
||||
|
||||
@@ -1007,18 +983,7 @@ void Client::save_guild_card_file() const {
|
||||
|
||||
void Client::load_backup_character(uint32_t account_id, size_t index) {
|
||||
string filename = this->backup_character_filename(account_id, index, false);
|
||||
auto f = phosg::fopen_unique(filename, "rb");
|
||||
auto header = phosg::freadx<PSOCommandHeaderBB>(f.get());
|
||||
if (header.size != 0x399C) {
|
||||
throw runtime_error("incorrect size in character file header");
|
||||
}
|
||||
if (header.command != 0x00E7) {
|
||||
throw runtime_error("incorrect command in character file header");
|
||||
}
|
||||
if (header.flag != 0x00000000) {
|
||||
throw runtime_error("incorrect flag in character file header");
|
||||
}
|
||||
this->character_data = make_shared<PSOBBCharacterFile>(phosg::freadx<PSOBBCharacterFile>(f.get()));
|
||||
this->character_data = load_psochar(filename, false).character_file;
|
||||
this->update_character_data_after_load(this->character_data);
|
||||
this->v1_v2_last_reported_disp.reset();
|
||||
}
|
||||
@@ -1106,18 +1071,7 @@ void Client::use_character_bank(int8_t index) {
|
||||
this->external_bank_character_index = index;
|
||||
player_data_log.info("Using loaded character file %s for external bank", filename.c_str());
|
||||
} else if (phosg::isfile(filename)) {
|
||||
auto f = phosg::fopen_unique(filename, "rb");
|
||||
auto header = phosg::freadx<PSOCommandHeaderBB>(f.get());
|
||||
if (header.size != 0x399C) {
|
||||
throw runtime_error("incorrect size in character file header");
|
||||
}
|
||||
if (header.command != 0x00E7) {
|
||||
throw runtime_error("incorrect command in character file header");
|
||||
}
|
||||
if (header.flag != 0x00000000) {
|
||||
throw runtime_error("incorrect flag in character file header");
|
||||
}
|
||||
this->external_bank_character = make_shared<PSOBBCharacterFile>(phosg::freadx<PSOBBCharacterFile>(f.get()));
|
||||
this->external_bank_character = load_psochar(filename, false).character_file;
|
||||
this->update_character_data_after_load(this->external_bank_character);
|
||||
this->external_bank_character_index = index;
|
||||
files_manager->set_character(filename, this->external_bank_character);
|
||||
|
||||
+3
-2
@@ -35,7 +35,7 @@ public:
|
||||
// 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 = 0xFF3CFFFF7C0BFFFB,
|
||||
CLIENT_SIDE_MASK = 0xE73CFFFF7C0BFFFB,
|
||||
|
||||
// Version-related flags
|
||||
CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002,
|
||||
@@ -73,6 +73,7 @@ public:
|
||||
SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000,
|
||||
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
|
||||
IS_CLIENT_CUSTOMIZATION = 0x0100000000000000,
|
||||
EP3_ALLOW_6xBC = 0x1000000000000000, // Server-side only
|
||||
|
||||
// Cheat mode and option flags
|
||||
INFINITE_HP_ENABLED = 0x0000000200000000,
|
||||
@@ -80,6 +81,7 @@ public:
|
||||
DEBUG_ENABLED = 0x0000000800000000,
|
||||
ITEM_DROP_NOTIFICATIONS_1 = 0x0010000000000000,
|
||||
ITEM_DROP_NOTIFICATIONS_2 = 0x0020000000000000,
|
||||
FORCE_BATTLE_MODE_GAME = 0x0800000000000000, // Server-side only
|
||||
|
||||
// Proxy option flags
|
||||
PROXY_SAVE_FILES = 0x0000001000000000,
|
||||
@@ -258,7 +260,6 @@ public:
|
||||
|
||||
// Miscellaneous (used by chat commands)
|
||||
uint32_t next_exp_value; // next EXP value to give
|
||||
RecentSwitchFlags recent_switch_flags; // used for switch assist
|
||||
bool can_chat;
|
||||
struct PendingCharacterExport {
|
||||
std::shared_ptr<const Account> dest_account;
|
||||
|
||||
+465
-308
File diff suppressed because it is too large
Load Diff
+81
-48
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <phosg/Random.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <phosg/Time.hh>
|
||||
@@ -24,6 +25,7 @@ static const uint32_t primes1[] = {
|
||||
0x313, 0x31D, 0x329, 0x32B, 0x335, 0x337, 0x33B, 0x33D, 0x347, 0x355, 0x359,
|
||||
0x35B, 0x35F, 0x36D, 0x371, 0x373, 0x377, 0x38B, 0x38F, 0x397, 0x3A1, 0x3A9,
|
||||
0x3AD, 0x3B3, 0x3B9, 0x3C7, 0x3CB, 0x3D1, 0x3D7, 0x3DF, 0x3E5};
|
||||
|
||||
static const uint32_t primes2[] = {
|
||||
0x3F1, 0x3F5, 0x3FB, 0x3FD, 0x407, 0x409, 0x40F, 0x419, 0x41B, 0x425, 0x427,
|
||||
0x42D, 0x43F, 0x443, 0x445, 0x449, 0x44F, 0x455, 0x45D, 0x463, 0x469, 0x47F,
|
||||
@@ -135,6 +137,8 @@ static const uint32_t primes2[] = {
|
||||
0x2627, 0x2629, 0x2635, 0x263B, 0x263F, 0x264B, 0x2653, 0x2659, 0x2665,
|
||||
0x2669, 0x266F, 0x267B, 0x2681, 0x2683, 0x268F, 0x269B, 0x269F, 0x26AD,
|
||||
0x26B3, 0x26C3, 0x26C9, 0x26CB, 0x26D5, 0x26DD, 0x26EF, 0x26F5};
|
||||
static constexpr size_t num_primes2 = sizeof(primes2) / sizeof(primes2[0]);
|
||||
|
||||
static const uint32_t primes3[] = {
|
||||
0x2717, 0x2719, 0x2735, 0x2737, 0x274D, 0x2753, 0x2755, 0x275F, 0x276B,
|
||||
0x276D, 0x2773, 0x2777, 0x277F, 0x2795, 0x279B, 0x279D, 0x27A7, 0x27AF,
|
||||
@@ -1107,15 +1111,19 @@ static const uint32_t primes3[] = {
|
||||
0x18569, 0x1857B, 0x1857D, 0x18581, 0x18587, 0x18589, 0x18595, 0x185B1,
|
||||
0x185B7, 0x185CB, 0x185D1, 0x185E1, 0x185E9, 0x185EF, 0x185F5, 0x185F9,
|
||||
0x185FF, 0x18613, 0x1861F};
|
||||
static constexpr size_t num_primes3 = sizeof(primes3) / sizeof(primes3[0]);
|
||||
|
||||
static bool check_prime3(uint64_t prime3) {
|
||||
static vector<bool> primes3_set;
|
||||
static mutex primes3_init_mutex;
|
||||
if (primes3_set.empty()) {
|
||||
size_t num_primes3 = sizeof(primes3) / sizeof(primes3[0]);
|
||||
size_t primes3_set_size = primes3[num_primes3 - 1] - primes3[0] + 1;
|
||||
primes3_set.resize(primes3_set_size, false);
|
||||
for (size_t z = 0; z < num_primes3; z++) {
|
||||
primes3_set[primes3[z] - primes3[0]] = true;
|
||||
lock_guard g(primes3_init_mutex);
|
||||
if (primes3_set.empty()) {
|
||||
size_t primes3_set_size = primes3[num_primes3 - 1] - primes3[0] + 1;
|
||||
primes3_set.resize(primes3_set_size, false);
|
||||
for (size_t z = 0; z < num_primes3; z++) {
|
||||
primes3_set[primes3[z] - primes3[0]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint64_t offset = prime3 - primes3[0];
|
||||
@@ -1227,8 +1235,8 @@ bool dc_serial_number_is_valid_slow(const string& s, uint8_t domain, uint8_t sub
|
||||
}
|
||||
|
||||
for (; offset1 < limit1; offset1++) {
|
||||
for (size_t offset2 = 0; offset2 < sizeof(primes2) / sizeof(primes2[0]); offset2++) {
|
||||
for (size_t offset3 = 0; offset3 < sizeof(primes3) / sizeof(primes3[0]); offset3++) {
|
||||
for (size_t offset2 = 0; offset2 < num_primes2; offset2++) {
|
||||
for (size_t offset3 = 0; offset3 < num_primes3; offset3++) {
|
||||
if (primes1[offset1] * primes2[offset2] * primes3[offset3] == serial_number) {
|
||||
return true;
|
||||
}
|
||||
@@ -1251,7 +1259,7 @@ bool decoded_dc_serial_number_is_valid_fast(uint32_t serial_number, uint8_t doma
|
||||
continue;
|
||||
}
|
||||
uint64_t sub1 = sub0 / primes1[offset1];
|
||||
for (size_t offset2 = 0; offset2 < sizeof(primes2) / sizeof(primes2[0]); offset2++) {
|
||||
for (size_t offset2 = 0; offset2 < num_primes2; offset2++) {
|
||||
if (sub1 % primes2[offset2]) {
|
||||
continue;
|
||||
}
|
||||
@@ -1293,8 +1301,8 @@ string generate_dc_serial_number(uint8_t domain, uint8_t subdomain) {
|
||||
|
||||
size_t det1 = (subdomain == 0xFF) ? phosg::random_object<uint32_t>() : subdomain;
|
||||
size_t index1 = offset1 + (det1 % (limit1 - offset1));
|
||||
size_t index2 = phosg::random_object<uint32_t>() % (sizeof(primes2) / sizeof(primes2[0]));
|
||||
size_t index3 = phosg::random_object<uint32_t>() % (sizeof(primes3) / sizeof(primes3[0]));
|
||||
size_t index2 = phosg::random_object<uint32_t>() % num_primes2;
|
||||
size_t index3 = phosg::random_object<uint32_t>() % num_primes3;
|
||||
uint32_t value = primes1[index1] * primes2[index2] * primes3[index3];
|
||||
string s = phosg::string_printf("%08X", value);
|
||||
|
||||
@@ -1306,54 +1314,79 @@ string generate_dc_serial_number(uint8_t domain, uint8_t subdomain) {
|
||||
}
|
||||
|
||||
unordered_map<uint32_t, string> generate_all_dc_serial_numbers(uint8_t domain, uint8_t subdomain) {
|
||||
vector<uint8_t> domains;
|
||||
if (domain == 0xFF) {
|
||||
domains.emplace_back(0x00);
|
||||
domains.emplace_back(0x01);
|
||||
domains.emplace_back(0x02);
|
||||
} else {
|
||||
domains.emplace_back(domain);
|
||||
DCSerialNumberIterator iter;
|
||||
|
||||
if (domain < 3) {
|
||||
iter.domain = domain;
|
||||
iter.end_domain = domain + 1;
|
||||
} else if (domain != 0xFF) {
|
||||
throw runtime_error("invalid domain");
|
||||
}
|
||||
|
||||
vector<uint8_t> subdomains;
|
||||
if (subdomain == 0xFF) {
|
||||
subdomains.emplace_back(0x00);
|
||||
subdomains.emplace_back(0x01);
|
||||
subdomains.emplace_back(0x02);
|
||||
} else {
|
||||
subdomains.emplace_back(subdomain);
|
||||
if (subdomain < 3) {
|
||||
iter.subdomain = subdomain;
|
||||
iter.start_subdomain = subdomain;
|
||||
iter.end_subdomain = subdomain + 1;
|
||||
} else if (subdomain != 0xFF) {
|
||||
throw runtime_error("invalid subdomain");
|
||||
}
|
||||
|
||||
uint32_t serial_number;
|
||||
unordered_map<uint32_t, string> ret;
|
||||
for (uint8_t domain : domains) {
|
||||
size_t offset1, limit1;
|
||||
if (domain == 0) {
|
||||
offset1 = 0x00;
|
||||
limit1 = 0x03;
|
||||
} else if (domain == 1) {
|
||||
offset1 = 0x1E;
|
||||
limit1 = 0x21;
|
||||
} else if (domain == 2) {
|
||||
offset1 = 0x3C;
|
||||
limit1 = 0x3F;
|
||||
} else {
|
||||
throw runtime_error("invalid domain");
|
||||
}
|
||||
|
||||
for (uint8_t subdomain : subdomains) {
|
||||
size_t index1 = offset1 + (subdomain % (limit1 - offset1));
|
||||
for (size_t index2 = 0; index2 < sizeof(primes2) / sizeof(primes2[0]); index2++) {
|
||||
for (size_t index3 = 0; index3 < sizeof(primes3) / sizeof(primes3[0]); index3++) {
|
||||
uint32_t value = primes1[index1] * primes2[index2] * primes3[index3];
|
||||
ret[encode_dc_serial_number_int(value)].push_back(((domain << 2) & 3) | (subdomain & 3));
|
||||
}
|
||||
fprintf(stderr, "... domain=%hhu subdomain=%hhu index2=%zu results=%zu (0x%zX)\n", domain, subdomain, index2, ret.size(), ret.size());
|
||||
}
|
||||
while ((serial_number = iter.next()) != 0) {
|
||||
ret[serial_number].push_back(((iter.domain << 2) & 3) | (iter.subdomain & 3));
|
||||
if (iter.index3 == 0) {
|
||||
fprintf(stderr, "... (it) domain=%hhu subdomain=%hhu index2=%hu results=%zu (0x%zX)\n", iter.domain, iter.subdomain, iter.index2, ret.size(), ret.size());
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint32_t DCSerialNumberIterator::next() {
|
||||
if (!this->started) {
|
||||
this->started = true;
|
||||
} else if (!this->complete) {
|
||||
this->index3++;
|
||||
if (this->index3 >= num_primes3) {
|
||||
this->index3 = 0;
|
||||
this->index2++;
|
||||
if (this->index2 >= num_primes2) {
|
||||
this->index2 = 0;
|
||||
this->subdomain++;
|
||||
if (this->subdomain >= this->end_subdomain) {
|
||||
this->subdomain = this->start_subdomain;
|
||||
this->domain++;
|
||||
if (this->domain >= this->end_domain) {
|
||||
this->serial_number = 0;
|
||||
this->complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this->complete) {
|
||||
size_t index1 = (30 * this->domain) + (this->subdomain % 3);
|
||||
return encode_dc_serial_number_int(primes1[index1] * primes2[this->index2] * primes3[this->index3]);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
size_t DCSerialNumberIterator::total_count() const {
|
||||
return (this->end_domain - this->start_domain) * (this->end_subdomain - this->start_subdomain) * num_primes2 * num_primes3;
|
||||
}
|
||||
|
||||
size_t DCSerialNumberIterator::progress() const {
|
||||
size_t domains_done = this->domain - this->start_domain;
|
||||
size_t subdomains_per_domain = this->end_subdomain - this->start_subdomain;
|
||||
size_t subdomains_done = this->subdomain - this->start_subdomain;
|
||||
return (
|
||||
(domains_done * subdomains_per_domain * num_primes2 * num_primes3) +
|
||||
(subdomains_done * num_primes2 * num_primes3) +
|
||||
(this->index2 * num_primes3) +
|
||||
this->index3);
|
||||
}
|
||||
|
||||
void dc_serial_number_speed_test(uint64_t seed) {
|
||||
uint32_t effective_seed = (seed & 0xFFFFFFFF00000000) ? phosg::random_object<uint32_t>() : seed;
|
||||
fprintf(stderr, "Product speed test with seed=%08" PRIX32 "\n", effective_seed);
|
||||
|
||||
@@ -20,6 +20,25 @@ bool decoded_dc_serial_number_is_valid_fast(
|
||||
std::string generate_dc_serial_number(uint8_t domain, uint8_t subdomain = 0xFF);
|
||||
std::unordered_map<uint32_t, std::string> generate_all_dc_serial_numbers(uint8_t domain = 0xFF, uint8_t subdomain = 0xFF);
|
||||
|
||||
struct DCSerialNumberIterator {
|
||||
bool started = false;
|
||||
bool complete = false;
|
||||
uint8_t domain = 0;
|
||||
uint8_t start_domain = 0;
|
||||
uint8_t end_domain = 3;
|
||||
uint8_t subdomain = 0;
|
||||
uint8_t start_subdomain = 0;
|
||||
uint8_t end_subdomain = 3;
|
||||
uint16_t index2 = 0;
|
||||
uint16_t index3 = 0;
|
||||
uint32_t serial_number = 0;
|
||||
|
||||
uint32_t next();
|
||||
|
||||
size_t total_count() const;
|
||||
size_t progress() const;
|
||||
};
|
||||
|
||||
void dc_serial_number_speed_test(uint64_t seed = 0xFFFFFFFFFFFFFFFF);
|
||||
|
||||
struct EncryptedDCv2Executables {
|
||||
|
||||
@@ -0,0 +1,870 @@
|
||||
#include "DownloadSession.hh"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/bufferevent.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/listener.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Random.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "Loggers.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "ProxyCommands.hh"
|
||||
#include "ReceiveCommands.hh"
|
||||
#include "ReceiveSubcommands.hh"
|
||||
#include "SendCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
static string random_name() {
|
||||
string ret;
|
||||
size_t length = (phosg::random_object<size_t>() % 12) + 4;
|
||||
static const string alphabet = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890-+<>:\"\',.";
|
||||
while (ret.size() < length) {
|
||||
ret.push_back(alphabet[phosg::random_object<size_t>() % alphabet.size()]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
DownloadSession::DownloadSession(
|
||||
std::shared_ptr<struct event_base> base,
|
||||
const struct sockaddr_storage& remote,
|
||||
const std::string& output_dir,
|
||||
Version version,
|
||||
uint8_t language,
|
||||
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file,
|
||||
uint32_t hardware_id,
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& username,
|
||||
const std::string& password,
|
||||
const std::string& xb_gamertag,
|
||||
uint64_t xb_user_id,
|
||||
uint64_t xb_account_id,
|
||||
std::shared_ptr<PSOBBCharacterFile> character,
|
||||
const std::unordered_set<std::string>& ship_menu_selections,
|
||||
const std::vector<std::string>& on_request_complete_commands,
|
||||
bool interactive,
|
||||
bool show_command_data)
|
||||
: output_dir(output_dir),
|
||||
bb_key_file(bb_key_file),
|
||||
hardware_id(hardware_id),
|
||||
serial_number(serial_number),
|
||||
access_key(access_key),
|
||||
username(username),
|
||||
password(password),
|
||||
xb_gamertag(xb_gamertag),
|
||||
xb_user_id(xb_user_id),
|
||||
xb_account_id(xb_account_id),
|
||||
character(character),
|
||||
ship_menu_selections(ship_menu_selections),
|
||||
on_request_complete_commands(on_request_complete_commands),
|
||||
interactive(interactive),
|
||||
log(phosg::string_printf("[DownloadSession:%s] ", phosg::name_for_enum(version)), proxy_server_log.min_level),
|
||||
base(base),
|
||||
channel(
|
||||
version,
|
||||
language,
|
||||
DownloadSession::dispatch_on_channel_input,
|
||||
DownloadSession::dispatch_on_channel_error,
|
||||
this,
|
||||
phosg::render_sockaddr_storage(remote),
|
||||
show_command_data ? phosg::TerminalFormat::FG_GREEN : phosg::TerminalFormat::END,
|
||||
show_command_data ? phosg::TerminalFormat::FG_YELLOW : phosg::TerminalFormat::END),
|
||||
guild_card_number(0),
|
||||
prev_cmd_data(0),
|
||||
client_config(0),
|
||||
sent_96(false),
|
||||
should_request_category_list(true),
|
||||
current_request(0),
|
||||
current_game_config_index(0),
|
||||
in_game(false),
|
||||
bin_complete(false),
|
||||
dat_complete(false) {
|
||||
if (this->output_dir.empty()) {
|
||||
this->output_dir = ".";
|
||||
}
|
||||
|
||||
switch (this->channel.version) {
|
||||
case Version::DC_V1:
|
||||
case Version::DC_V2:
|
||||
if (this->hardware_id == 0 || this->serial_number == 0 || this->access_key.empty()) {
|
||||
throw runtime_error("missing credentials");
|
||||
}
|
||||
break;
|
||||
case Version::PC_V2:
|
||||
if (this->serial_number == 0 || this->access_key.empty()) {
|
||||
throw runtime_error("missing credentials");
|
||||
}
|
||||
break;
|
||||
case Version::GC_V3:
|
||||
if (this->serial_number == 0 || this->access_key.empty() || this->password.empty()) {
|
||||
throw runtime_error("missing credentials");
|
||||
}
|
||||
break;
|
||||
case Version::XB_V3:
|
||||
if (this->xb_gamertag.empty() || this->xb_user_id == 0 || this->xb_account_id == 0) {
|
||||
throw runtime_error("missing credentials");
|
||||
}
|
||||
break;
|
||||
case Version::BB_V4:
|
||||
if (this->username.empty() || this->password.empty()) {
|
||||
throw runtime_error("missing credentials");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("unsupported version");
|
||||
}
|
||||
|
||||
this->character->inventory.language = this->channel.language;
|
||||
|
||||
if (remote.ss_family != AF_INET) {
|
||||
throw runtime_error("remote is not AF_INET");
|
||||
}
|
||||
|
||||
string netloc_str = phosg::render_sockaddr_storage(remote);
|
||||
this->log.info("Connecting to %s", netloc_str.c_str());
|
||||
|
||||
struct bufferevent* bev = bufferevent_socket_new(
|
||||
this->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
if (!bev) {
|
||||
throw runtime_error(phosg::string_printf("failed to open socket (%d)", EVUTIL_SOCKET_ERROR()));
|
||||
}
|
||||
this->channel.set_bufferevent(bev, 0);
|
||||
|
||||
if (bufferevent_socket_connect(this->channel.bev.get(),
|
||||
reinterpret_cast<const sockaddr*>(&remote), sizeof(struct sockaddr_in)) != 0) {
|
||||
throw runtime_error(phosg::string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR()));
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadSession::dispatch_on_channel_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data) {
|
||||
auto* session = reinterpret_cast<DownloadSession*>(ch.context_obj);
|
||||
session->on_channel_input(command, flag, data);
|
||||
}
|
||||
|
||||
void DownloadSession::send_93_9D_9E(bool extended) {
|
||||
if (is_v1(this->channel.version)) {
|
||||
C_LoginExtendedV1_DC_93 ret;
|
||||
ret.player_tag = this->guild_card_number ? 0xFFFF0000 : 0x00010000;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.sub_version = default_sub_version_for_version(this->channel.version);
|
||||
ret.is_extended = extended ? 1 : 0;
|
||||
ret.language = this->channel.language;
|
||||
ret.serial_number.encode(phosg::string_printf("%08" PRIX32, this->serial_number));
|
||||
ret.access_key.encode(this->access_key);
|
||||
ret.hardware_id.encode(phosg::string_printf("%08" PRIX32, this->hardware_id));
|
||||
ret.name.encode(this->character->disp.name.decode());
|
||||
this->channel.send(0x93, 0x01, &ret, extended ? sizeof(ret) : sizeof(C_LoginV1_DC_93));
|
||||
|
||||
} else if (is_v2(this->channel.version)) {
|
||||
C_LoginExtended_PC_9D ret;
|
||||
ret.player_tag = this->guild_card_number ? 0xFFFF0000 : 0x00010000;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.sub_version = default_sub_version_for_version(this->channel.version);
|
||||
ret.is_extended = extended ? 1 : 0;
|
||||
ret.language = this->channel.language;
|
||||
ret.serial_number.encode(phosg::string_printf("%08" PRIX32, this->serial_number));
|
||||
ret.access_key.encode(this->access_key);
|
||||
ret.serial_number2 = ret.serial_number;
|
||||
ret.access_key2 = ret.access_key;
|
||||
ret.name.encode(this->character->disp.name.decode());
|
||||
size_t data_size = extended
|
||||
? ((this->channel.version == Version::PC_V2) ? sizeof(ret) : sizeof(C_LoginExtended_DC_GC_9D))
|
||||
: sizeof(C_Login_DC_PC_GC_9D);
|
||||
this->channel.send(0x9D, 0x01, &ret, data_size);
|
||||
|
||||
} else if (this->channel.version == Version::GC_V3) {
|
||||
C_LoginExtended_GC_9E ret;
|
||||
ret.player_tag = this->guild_card_number ? 0xFFFF0000 : 0x00010000;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.sub_version = default_sub_version_for_version(this->channel.version);
|
||||
ret.is_extended = extended ? 1 : 0;
|
||||
ret.language = this->channel.language;
|
||||
ret.serial_number.encode(phosg::string_printf("%08" PRIX32, this->serial_number));
|
||||
ret.access_key.encode(this->access_key);
|
||||
ret.serial_number2 = ret.serial_number;
|
||||
ret.access_key2 = ret.access_key;
|
||||
ret.name.encode(this->character->disp.name.decode());
|
||||
ret.client_config = this->client_config;
|
||||
this->channel.send(0x9E, 0x01, &ret, extended ? sizeof(ret) : sizeof(C_Login_GC_9E));
|
||||
|
||||
} else if (this->channel.version == Version::XB_V3) {
|
||||
C_LoginExtended_XB_9E ret;
|
||||
ret.player_tag = this->guild_card_number ? 0xFFFF0000 : 0x00010000;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.sub_version = default_sub_version_for_version(this->channel.version);
|
||||
ret.is_extended = extended ? 1 : 0;
|
||||
ret.language = this->channel.language;
|
||||
ret.serial_number.encode(this->xb_gamertag);
|
||||
ret.access_key.encode(phosg::string_printf("%016" PRIX64, this->xb_user_id));
|
||||
ret.serial_number2 = ret.serial_number;
|
||||
ret.access_key2 = ret.access_key;
|
||||
ret.name.encode(this->character->disp.name.decode());
|
||||
ret.netloc.internal_ipv4_address = phosg::random_object<uint32_t>();
|
||||
ret.netloc.external_ipv4_address = phosg::random_object<uint32_t>();
|
||||
ret.netloc.port = 9500;
|
||||
phosg::random_data(&ret.netloc.mac_address, sizeof(ret.netloc.mac_address));
|
||||
ret.netloc.sg_ip_address = phosg::random_object<uint32_t>();
|
||||
ret.netloc.spi = phosg::random_object<uint32_t>();
|
||||
ret.netloc.account_id = this->xb_account_id;
|
||||
ret.netloc.unknown_a3.clear(0);
|
||||
ret.xb_user_id_high = this->xb_user_id >> 32;
|
||||
ret.xb_user_id_low = this->xb_user_id;
|
||||
this->channel.send(0x9E, 0x01, &ret, extended ? sizeof(ret) : sizeof(C_Login_DC_PC_GC_9D));
|
||||
|
||||
} else {
|
||||
throw runtime_error("unsupported version");
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadSession::send_61_98(bool is_98) {
|
||||
uint8_t command = is_98 ? 0x98 : 0x61;
|
||||
|
||||
if (is_v1(this->channel.version)) {
|
||||
C_CharacterData_DCv1_61_98 ret;
|
||||
ret.inventory = this->character->inventory;
|
||||
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
|
||||
this->channel.send(command, 0x01, ret);
|
||||
|
||||
} else if (this->channel.version == Version::DC_V2) {
|
||||
C_CharacterData_DCv2_61_98 ret;
|
||||
ret.inventory = this->character->inventory;
|
||||
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
|
||||
ret.records.challenge = this->character->challenge_records;
|
||||
ret.records.battle = this->character->battle_records;
|
||||
ret.choice_search_config = this->character->choice_search_config;
|
||||
this->channel.send(command, 0x02, ret);
|
||||
|
||||
} else if (this->channel.version == Version::PC_V2) {
|
||||
C_CharacterData_PC_61_98 ret;
|
||||
ret.inventory = this->character->inventory;
|
||||
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
|
||||
ret.records.challenge = this->character->challenge_records;
|
||||
ret.records.battle = this->character->battle_records;
|
||||
ret.choice_search_config = this->character->choice_search_config;
|
||||
this->channel.send(command, 0x02, ret);
|
||||
|
||||
} else if (is_v3(this->channel.version)) {
|
||||
C_CharacterData_V3_61_98 ret;
|
||||
ret.inventory = this->character->inventory;
|
||||
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
|
||||
ret.records.challenge = this->character->challenge_records;
|
||||
ret.records.battle = this->character->battle_records;
|
||||
ret.choice_search_config = this->character->choice_search_config;
|
||||
ret.info_board.encode(this->character->info_board.decode());
|
||||
this->channel.send(command, 0x03, ret);
|
||||
|
||||
} else if (this->channel.version == Version::BB_V4) {
|
||||
C_CharacterData_BB_61_98 ret;
|
||||
ret.inventory = this->character->inventory;
|
||||
ret.disp = this->character->disp;
|
||||
ret.records.challenge = this->character->challenge_records;
|
||||
ret.records.battle = this->character->battle_records;
|
||||
ret.choice_search_config = this->character->choice_search_config;
|
||||
ret.info_board.encode(this->character->info_board.decode());
|
||||
this->channel.send(command, 0x04, ret);
|
||||
|
||||
} else {
|
||||
throw runtime_error("unsupported version");
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadSession::on_channel_input(uint16_t command, uint32_t flag, std::string& data) {
|
||||
// TODO: Use the iovec form of print_data here instead of
|
||||
// prepend_command_header (which copies the string)
|
||||
string full_cmd = prepend_command_header(this->channel.version, this->channel.crypt_in.get(), command, flag, data);
|
||||
|
||||
for (size_t z = 0; z < 0x28 && z < data.size(); z++) {
|
||||
this->prev_cmd_data[z] = data[z];
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 0x03: {
|
||||
if (this->channel.version != Version::BB_V4) {
|
||||
throw runtime_error("BB server sent non-BB encryption command");
|
||||
}
|
||||
if (!this->bb_key_file) {
|
||||
throw runtime_error("BB encryption requires a key file");
|
||||
}
|
||||
const auto& cmd = check_size_t<S_ServerInitDefault_BB_03_9B>(data, 0xFFFF);
|
||||
this->channel.crypt_in = make_shared<PSOBBEncryption>(*this->bb_key_file, &cmd.server_key[0], sizeof(cmd.server_key));
|
||||
this->channel.crypt_out = make_shared<PSOBBEncryption>(*this->bb_key_file, &cmd.client_key[0], sizeof(cmd.client_key));
|
||||
this->log.info("Enabled BB encryption");
|
||||
throw runtime_error("not yet implemented"); // Send 93
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x02:
|
||||
case 0x17:
|
||||
case 0x91:
|
||||
case 0x9B: {
|
||||
const auto& cmd = check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>(data, 0xFFFF);
|
||||
if (uses_v3_encryption(this->channel.version)) {
|
||||
this->channel.crypt_in = make_shared<PSOV3Encryption>(cmd.server_key);
|
||||
this->channel.crypt_out = make_shared<PSOV3Encryption>(cmd.client_key);
|
||||
this->log.info("Enabled V3 encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")",
|
||||
cmd.server_key.load(), cmd.client_key.load());
|
||||
} else if (!uses_v4_encryption(this->channel.version)) {
|
||||
this->channel.crypt_in = make_shared<PSOV2Encryption>(cmd.server_key);
|
||||
this->channel.crypt_out = make_shared<PSOV2Encryption>(cmd.client_key);
|
||||
this->log.info("Enabled V2 encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")",
|
||||
cmd.server_key.load(), cmd.client_key.load());
|
||||
} else {
|
||||
throw runtime_error("BB server sent non-BB encryption command");
|
||||
}
|
||||
|
||||
if (command == 0x02) {
|
||||
bool is_extended = (this->channel.version == Version::XB_V3);
|
||||
this->send_93_9D_9E(is_extended);
|
||||
|
||||
} else {
|
||||
if (is_v1(this->channel.version)) {
|
||||
C_LoginV1_DC_PC_V3_90 ret;
|
||||
ret.serial_number.encode(phosg::string_printf("%08" PRIX32, this->serial_number));
|
||||
ret.access_key.encode(this->access_key);
|
||||
this->channel.send(0x90, 0x00, ret);
|
||||
|
||||
} else if (is_v2(this->channel.version)) {
|
||||
C_Login_DC_PC_V3_9A ret;
|
||||
ret.serial_number.encode(phosg::string_printf("%08" PRIX32, this->serial_number));
|
||||
ret.access_key.encode(this->access_key);
|
||||
ret.player_tag = this->guild_card_number ? 0xFFFF0000 : 0x00010000;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.sub_version = default_sub_version_for_version(this->channel.version);
|
||||
ret.serial_number2 = ret.serial_number;
|
||||
ret.access_key2 = ret.access_key;
|
||||
this->channel.send(0x9A, 0x00, ret);
|
||||
|
||||
} else if (this->channel.version == Version::GC_V3) {
|
||||
C_VerifyAccount_V3_DB ret;
|
||||
ret.serial_number.encode(phosg::string_printf("%08" PRIX32, this->serial_number));
|
||||
ret.access_key.encode(this->access_key);
|
||||
ret.sub_version = default_sub_version_for_version(this->channel.version);
|
||||
ret.serial_number2 = ret.serial_number;
|
||||
ret.access_key2 = ret.access_key;
|
||||
ret.password.encode(this->password);
|
||||
this->channel.send(0xDB, 0x00, ret);
|
||||
|
||||
} else if (this->channel.version == Version::XB_V3) {
|
||||
this->send_93_9D_9E(true);
|
||||
|
||||
} else {
|
||||
throw runtime_error("unsupported version");
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x90:
|
||||
case 0x9A: {
|
||||
if (flag == 1) {
|
||||
if (is_v1(this->channel.version)) {
|
||||
C_RegisterV1_DC_92 ret;
|
||||
ret.sub_version = default_sub_version_for_version(this->channel.version);
|
||||
ret.language = this->channel.language;
|
||||
ret.hardware_id.encode(phosg::string_printf("%08" PRIX32, this->hardware_id));
|
||||
this->channel.send(0x92, 0x00, ret);
|
||||
|
||||
} else if (!is_v4(this->channel.version)) {
|
||||
C_Register_DC_PC_V3_9C ret;
|
||||
ret.sub_version = default_sub_version_for_version(this->channel.version);
|
||||
ret.language = this->channel.language;
|
||||
if (this->channel.version == Version::XB_V3) {
|
||||
ret.serial_number.encode(this->xb_gamertag);
|
||||
ret.access_key.encode(phosg::string_printf("%016" PRIX64, this->xb_user_id));
|
||||
ret.password.encode("xbox-pso");
|
||||
} else {
|
||||
ret.serial_number.encode(phosg::string_printf("%08" PRIX32, this->serial_number));
|
||||
ret.access_key.encode(this->access_key);
|
||||
ret.password.encode(this->password);
|
||||
}
|
||||
this->channel.send(0x9C, 0x00, ret);
|
||||
|
||||
} else {
|
||||
throw runtime_error("unsupported version");
|
||||
}
|
||||
|
||||
} else if (flag == 0 || flag == 2) {
|
||||
this->send_93_9D_9E(true);
|
||||
|
||||
} else {
|
||||
throw runtime_error("login failed");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x92:
|
||||
case 0x9C:
|
||||
if (flag == 0) {
|
||||
throw runtime_error("server rejected login credentials");
|
||||
}
|
||||
this->send_93_9D_9E(true);
|
||||
break;
|
||||
|
||||
case 0x9F: {
|
||||
if (is_v1_or_v2(this->channel.version)) {
|
||||
throw runtime_error("invalid command");
|
||||
}
|
||||
this->channel.send(0x9F, 0x00, this->client_config);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xB2: {
|
||||
C_ExecuteCodeResult_B3 ret;
|
||||
ret.checksum = 0;
|
||||
ret.return_value = 0;
|
||||
this->channel.send(0xB3, 0x00, ret);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x04: {
|
||||
const auto& cmd = check_size_t<S_UpdateClientConfig_V3_04>(data, 0x08, sizeof(S_UpdateClientConfig_V3_04));
|
||||
if (!is_v1_or_v2(this->channel.version)) {
|
||||
for (size_t z = 0; z < 0x20; z++) {
|
||||
size_t read_index = z + 8;
|
||||
this->client_config[z] = (read_index < data.size()) ? data[read_index] : this->prev_cmd_data[read_index];
|
||||
}
|
||||
}
|
||||
this->guild_card_number = cmd.guild_card_number;
|
||||
if (!this->sent_96) {
|
||||
C_CharSaveInfo_DCv2_PC_V3_BB_96 ret;
|
||||
ret.creation_timestamp = this->character->creation_timestamp;
|
||||
ret.event_counter = this->character->save_count;
|
||||
this->channel.send(0x96, 0x00, ret);
|
||||
this->sent_96 = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x97:
|
||||
this->channel.send(0xB1, 0x00);
|
||||
break;
|
||||
|
||||
case 0x95:
|
||||
this->send_61_98(false);
|
||||
break;
|
||||
|
||||
case 0xB1:
|
||||
this->channel.send(0x99, 0x00);
|
||||
break;
|
||||
|
||||
case 0x1A:
|
||||
case 0xD5:
|
||||
if (is_v3(this->channel.version)) {
|
||||
this->channel.send(0xD6, 0x00);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x07:
|
||||
case 0x1F:
|
||||
case 0xA0:
|
||||
case 0xA1: {
|
||||
C_MenuSelection_10_Flag00 ret;
|
||||
|
||||
auto handle_command = [&]<typename CmdT>() {
|
||||
const auto* items = check_size_vec_t<CmdT>(data, flag + 1);
|
||||
size_t item_index;
|
||||
this->log.info("Ship Select menu:");
|
||||
for (item_index = 1; item_index <= flag; item_index++) {
|
||||
const auto& item = items[item_index];
|
||||
auto text = strip_color(item.text.decode());
|
||||
this->log.info("%zu: (%08" PRIX32 " %08" PRIX32 ") %s", item_index, item.menu_id.load(), item.item_id.load(), text.c_str());
|
||||
if (this->ship_menu_selections.count(text)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (item_index > flag) {
|
||||
if (this->interactive) {
|
||||
while (item_index == 0 || item_index > flag) {
|
||||
this->log.info("Choose response index:");
|
||||
string input = phosg::fgets(stdin);
|
||||
item_index = stoul(input, nullptr, 0);
|
||||
}
|
||||
} else {
|
||||
throw runtime_error("unhandled menu selection");
|
||||
}
|
||||
}
|
||||
ret.menu_id = items[item_index].menu_id;
|
||||
ret.item_id = items[item_index].item_id;
|
||||
};
|
||||
|
||||
if (uses_utf16(this->channel.version)) {
|
||||
handle_command.operator()<S_MenuEntry_PC_BB_07_1F>();
|
||||
} else {
|
||||
handle_command.operator()<S_MenuEntry_DC_V3_07_1F>();
|
||||
}
|
||||
|
||||
this->channel.send(0x10, 0x00, ret);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x01:
|
||||
case 0x11:
|
||||
case 0x60:
|
||||
case 0x62:
|
||||
case 0x68:
|
||||
case 0x69:
|
||||
case 0x6C:
|
||||
case 0x6D:
|
||||
case 0x88:
|
||||
case 0x8A:
|
||||
case 0xB0:
|
||||
case 0xC5:
|
||||
case 0xDA:
|
||||
break;
|
||||
|
||||
case 0x1D:
|
||||
this->channel.send(0x1D, 0x00);
|
||||
break;
|
||||
|
||||
case 0x19: {
|
||||
const auto& cmd = check_size_t<S_Reconnect_19>(data, sizeof(S_Reconnect_19), 0xFFFF);
|
||||
|
||||
sockaddr_storage ss;
|
||||
auto* sin = reinterpret_cast<sockaddr_in*>(&ss);
|
||||
sin->sin_family = AF_INET;
|
||||
sin->sin_addr.s_addr = htonl(cmd.address);
|
||||
sin->sin_port = htons(cmd.port);
|
||||
string netloc_str = phosg::render_sockaddr_storage(ss);
|
||||
this->log.info("Connecting to %s", netloc_str.c_str());
|
||||
|
||||
struct bufferevent* bev = bufferevent_socket_new(this->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
if (!bev) {
|
||||
throw runtime_error(phosg::string_printf("failed to open socket (%d)", EVUTIL_SOCKET_ERROR()));
|
||||
}
|
||||
this->channel.set_bufferevent(bev, 0);
|
||||
this->channel.crypt_in.reset();
|
||||
this->channel.crypt_out.reset();
|
||||
|
||||
if (bufferevent_socket_connect(this->channel.bev.get(), reinterpret_cast<const sockaddr*>(&ss), sizeof(struct sockaddr_in)) != 0) {
|
||||
throw runtime_error(phosg::string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR()));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x83: {
|
||||
const auto* items = check_size_vec_t<S_LobbyListEntry_83>(data, flag, true);
|
||||
this->lobby_menu_items.clear();
|
||||
for (size_t z = 0; z < flag; z++) {
|
||||
this->lobby_menu_items.emplace_back(items[z]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x67: {
|
||||
// Technically we should assign item IDs here, but the server will never
|
||||
// be able to see that we didn't, so we don't bother
|
||||
|
||||
const auto& game_config = this->game_configs[this->current_game_config_index];
|
||||
if (this->channel.version == Version::PC_V2) {
|
||||
C_CreateGame_PC_C1 ret;
|
||||
ret.name.encode(random_name());
|
||||
ret.password.encode(random_name());
|
||||
ret.difficulty = 0;
|
||||
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
|
||||
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
|
||||
ret.episode = 1;
|
||||
this->channel.send(0xC1, 0x00, ret);
|
||||
|
||||
} else if (!is_v4(this->channel.version)) {
|
||||
C_CreateGame_DC_V3_0C_C1_Ep3_EC ret;
|
||||
ret.name.encode(random_name());
|
||||
ret.password.encode(random_name());
|
||||
ret.difficulty = 0;
|
||||
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
|
||||
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
|
||||
if (is_v1(this->channel.version)) {
|
||||
ret.episode = 0;
|
||||
} else if (game_config.episode == Episode::EP1) {
|
||||
ret.episode = 1;
|
||||
} else if (game_config.episode == Episode::EP2) {
|
||||
ret.episode = 2;
|
||||
} else if (game_config.episode == Episode::EP4) {
|
||||
ret.episode = 4;
|
||||
} else {
|
||||
throw std::logic_error("invalid episode");
|
||||
}
|
||||
this->channel.send(is_v1(this->channel.version) ? 0x0C : 0xC1, 0x00, ret);
|
||||
|
||||
} else {
|
||||
C_CreateGame_BB_C1 ret;
|
||||
ret.name.encode(random_name());
|
||||
ret.password.encode(random_name());
|
||||
ret.difficulty = 0;
|
||||
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
|
||||
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
|
||||
if (game_config.episode == Episode::EP1) {
|
||||
ret.episode = 1;
|
||||
} else if (game_config.episode == Episode::EP2) {
|
||||
ret.episode = 2;
|
||||
} else if (game_config.episode == Episode::EP4) {
|
||||
ret.episode = 4;
|
||||
} else {
|
||||
throw std::logic_error("invalid episode");
|
||||
}
|
||||
ret.solo_mode = (game_config.mode == GameMode::SOLO);
|
||||
this->channel.send(is_v1(this->channel.version) ? 0x0C : 0xC1, 0x00, ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x64: {
|
||||
this->in_game = true;
|
||||
this->bin_complete = false;
|
||||
this->dat_complete = false;
|
||||
for (size_t z = 0; z < this->character->inventory.num_items; z++) {
|
||||
this->character->inventory.items[z].data.id = 0x00010000 + z;
|
||||
}
|
||||
|
||||
if (!is_v1(this->channel.version)) {
|
||||
this->channel.send(0x8A, 0x00);
|
||||
}
|
||||
this->channel.send(0x6F, 0x00);
|
||||
this->send_next_request();
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xA2: {
|
||||
auto handle_command = [&]<typename CmdT>() {
|
||||
const auto* items = check_size_vec_t<CmdT>(data, flag);
|
||||
for (size_t z = 0; z < flag; z++) {
|
||||
const auto& item = items[z];
|
||||
uint64_t request = (static_cast<uint64_t>(item.menu_id) << 32) | static_cast<uint64_t>(item.item_id);
|
||||
if (!this->done_requests.count(request)) {
|
||||
this->log.info("Adding request %016" PRIX64, request);
|
||||
this->pending_requests.emplace(request, item.name.decode());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (this->channel.version == Version::PC_V2) {
|
||||
handle_command.operator()<S_QuestMenuEntry_PC_A2_A4>();
|
||||
} else if (this->channel.version == Version::XB_V3) {
|
||||
handle_command.operator()<S_QuestMenuEntry_XB_A2_A4>();
|
||||
} else if (this->channel.version == Version::BB_V4) {
|
||||
handle_command.operator()<S_QuestMenuEntry_BB_A2_A4>();
|
||||
} else {
|
||||
handle_command.operator()<S_QuestMenuEntry_DC_GC_A2_A4>();
|
||||
}
|
||||
this->send_next_request();
|
||||
break;
|
||||
}
|
||||
case 0x44:
|
||||
case 0xA6: {
|
||||
auto handle_command = [&]<typename CmdT>() {
|
||||
const auto& cmd = check_size_t<CmdT>(data, 0xFFFF);
|
||||
string internal_name = cmd.filename.decode();
|
||||
string filtered_name;
|
||||
for (char ch : internal_name) {
|
||||
filtered_name.push_back((isalnum(ch) || (ch == '-') || (ch == '.') || (ch == '_')) ? ch : '_');
|
||||
}
|
||||
string local_filename = phosg::string_printf(
|
||||
"%s/%016" PRIX64 "_%" PRIu64 "_%s_%c_%s",
|
||||
this->output_dir.c_str(),
|
||||
this->current_request,
|
||||
phosg::now(),
|
||||
phosg::name_for_enum(this->channel.version),
|
||||
char_for_language_code(this->channel.language),
|
||||
filtered_name.c_str());
|
||||
this->open_files.emplace(internal_name, OpenFile{.request = this->current_request, .filename = local_filename, .total_size = cmd.file_size, .data = ""});
|
||||
};
|
||||
|
||||
if (is_dc(this->channel.version)) {
|
||||
handle_command.operator()<S_OpenFile_DC_44_A6>();
|
||||
} else if (!is_v4(this->channel.version)) {
|
||||
handle_command.operator()<S_OpenFile_PC_GC_44_A6>();
|
||||
} else {
|
||||
handle_command.operator()<S_OpenFile_BB_44_A6>();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x13:
|
||||
case 0xA7: {
|
||||
const auto& cmd = check_size_t<S_WriteFile_13_A7>(data);
|
||||
string internal_filename = cmd.filename.decode();
|
||||
|
||||
if (!is_v1_or_v2(this->channel.version)) {
|
||||
C_WriteFileConfirmation_V3_BB_13_A7 ret;
|
||||
ret.filename.encode(internal_filename);
|
||||
this->channel.send(command, flag, ret);
|
||||
}
|
||||
|
||||
auto f_it = this->open_files.find(internal_filename.c_str());
|
||||
if (f_it == this->open_files.end()) {
|
||||
this->log.warning("Received data for non-open file %s", internal_filename.c_str());
|
||||
break;
|
||||
}
|
||||
auto& f = this->open_files.at(cmd.filename.decode());
|
||||
size_t block_offset = flag * 0x400;
|
||||
size_t allowed_block_size = (block_offset < f.total_size)
|
||||
? min<size_t>(f.total_size - block_offset, 0x400)
|
||||
: 0;
|
||||
size_t data_size = min<size_t>(cmd.data_size, allowed_block_size);
|
||||
size_t block_end_offset = block_offset + data_size;
|
||||
if (block_end_offset > f.data.size()) {
|
||||
f.data.resize(block_end_offset);
|
||||
}
|
||||
memcpy(f.data.data() + block_offset, cmd.data.data(), data_size);
|
||||
if (cmd.data_size != 0x400) {
|
||||
phosg::save_file(f.filename, f.data);
|
||||
this->log.info("Wrote file %s (%zu bytes)", f.filename.c_str(), f.data.size());
|
||||
this->open_files.erase(internal_filename);
|
||||
if (phosg::ends_with(internal_filename, ".bin")) {
|
||||
this->bin_complete = true;
|
||||
} else if (phosg::ends_with(internal_filename, ".dat")) {
|
||||
this->dat_complete = true;
|
||||
}
|
||||
if (this->open_files.empty() && this->bin_complete && this->dat_complete) {
|
||||
if (is_v1_or_v2(this->channel.version)) {
|
||||
this->on_request_complete();
|
||||
} else {
|
||||
this->channel.send(0xAC, 0x00);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0xAC: {
|
||||
if (is_v1_or_v2(this->channel.version)) {
|
||||
throw runtime_error("unsupported version");
|
||||
}
|
||||
this->on_request_complete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadSession::send_next_request() {
|
||||
if (should_request_category_list) {
|
||||
this->log.info("Requesting quest list");
|
||||
this->channel.send(0xA2, 0x00);
|
||||
if (is_v4(this->channel.version)) {
|
||||
this->channel.send(0xA2, 0x01);
|
||||
}
|
||||
this->should_request_category_list = false;
|
||||
|
||||
} else if (!this->pending_requests.empty()) {
|
||||
if (interactive) {
|
||||
const auto& config = this->game_configs[this->current_game_config_index];
|
||||
this->log.info("Items available to expand (mode=%s, episode=%s):", name_for_mode(config.mode), name_for_episode(config.episode));
|
||||
for (const auto& it : this->pending_requests) {
|
||||
this->log.info("%016" PRIX64 ": %s", it.first, it.second.c_str());
|
||||
}
|
||||
this->log.info("Choose item to expand by ID (q to quit; s to skip to next config):");
|
||||
string input = phosg::fgets(stdin);
|
||||
if (input.empty() || (input == "q\n")) {
|
||||
this->channel.disconnect();
|
||||
return;
|
||||
} else if (input == "s\n") {
|
||||
this->pending_requests.clear();
|
||||
this->on_request_complete();
|
||||
} else {
|
||||
this->current_request = stoull(input, nullptr, 16);
|
||||
this->done_requests.emplace(this->current_request);
|
||||
this->pending_requests.erase(this->current_request);
|
||||
}
|
||||
|
||||
} else {
|
||||
auto item_it = this->pending_requests.begin();
|
||||
this->current_request = item_it->first;
|
||||
this->done_requests.emplace(this->current_request);
|
||||
this->pending_requests.erase(item_it);
|
||||
this->log.info("Sending request %016" PRIX64, this->current_request);
|
||||
}
|
||||
|
||||
C_MenuSelection_10_Flag00 cmd;
|
||||
cmd.menu_id = (this->current_request >> 32) & 0xFFFFFFFF;
|
||||
cmd.item_id = this->current_request & 0xFFFFFFFF;
|
||||
this->channel.send(0x10, 0x00, cmd);
|
||||
} else {
|
||||
this->log.info("No pending requests with current parameters");
|
||||
this->on_request_complete();
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadSession::on_request_complete() {
|
||||
for (const auto& data : this->on_request_complete_commands) {
|
||||
this->channel.send(data);
|
||||
}
|
||||
|
||||
this->send_61_98(true);
|
||||
this->in_game = false;
|
||||
|
||||
const auto& item = this->lobby_menu_items.at(this->lobby_menu_items.size() / 2);
|
||||
C_LobbySelection_84 ret84;
|
||||
ret84.menu_id = item.menu_id;
|
||||
ret84.item_id = item.item_id;
|
||||
this->channel.send(0x84, 0x00, ret84);
|
||||
|
||||
if (this->pending_requests.empty()) {
|
||||
// Advance to next mode/episode combination
|
||||
this->current_game_config_index++;
|
||||
bool v1 = is_v1(this->channel.version);
|
||||
bool v2 = is_v2(this->channel.version);
|
||||
bool v3 = is_v3(this->channel.version);
|
||||
while ((this->current_game_config_index < this->game_configs.size()) &&
|
||||
((v1 && !this->game_configs[this->current_game_config_index].v1) ||
|
||||
(v2 && !this->game_configs[this->current_game_config_index].v2) ||
|
||||
(v3 && !this->game_configs[this->current_game_config_index].v3))) {
|
||||
this->current_game_config_index++;
|
||||
}
|
||||
if (this->current_game_config_index >= this->game_configs.size()) {
|
||||
this->log.info("All modes complete");
|
||||
this->channel.disconnect();
|
||||
} else {
|
||||
const auto& config = this->game_configs[this->current_game_config_index];
|
||||
this->log.info("Advancing to %s mode in %s", name_for_mode(config.mode), name_for_episode(config.episode));
|
||||
this->should_request_category_list = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadSession::dispatch_on_channel_error(Channel& ch, short events) {
|
||||
auto* session = reinterpret_cast<DownloadSession*>(ch.context_obj);
|
||||
session->on_channel_error(events);
|
||||
}
|
||||
|
||||
void DownloadSession::on_channel_error(short events) {
|
||||
if (events & BEV_EVENT_CONNECTED) {
|
||||
this->log.info("Server channel connected");
|
||||
}
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
this->log.warning("Error %d (%s) in server stream", err, evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) {
|
||||
this->log.info("Server endpoint has disconnected");
|
||||
this->channel.disconnect();
|
||||
event_base_loopexit(this->base.get(), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<DownloadSession::GameConfig> DownloadSession::game_configs({
|
||||
{.mode = GameMode::NORMAL, .episode = Episode::EP1, .v1 = true, .v2 = true, .v3 = true},
|
||||
{.mode = GameMode::NORMAL, .episode = Episode::EP2, .v1 = false, .v2 = false, .v3 = true},
|
||||
{.mode = GameMode::NORMAL, .episode = Episode::EP4, .v1 = false, .v2 = false, .v3 = false},
|
||||
{.mode = GameMode::BATTLE, .episode = Episode::EP1, .v1 = false, .v2 = true, .v3 = true},
|
||||
{.mode = GameMode::CHALLENGE, .episode = Episode::EP1, .v1 = false, .v2 = true, .v3 = true},
|
||||
{.mode = GameMode::CHALLENGE, .episode = Episode::EP2, .v1 = false, .v2 = false, .v3 = true},
|
||||
{.mode = GameMode::SOLO, .episode = Episode::EP1, .v1 = false, .v2 = false, .v3 = false},
|
||||
{.mode = GameMode::SOLO, .episode = Episode::EP2, .v1 = false, .v2 = false, .v3 = false},
|
||||
{.mode = GameMode::SOLO, .episode = Episode::EP4, .v1 = false, .v2 = false, .v3 = false},
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "PSOEncryption.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "ServerState.hh"
|
||||
|
||||
class DownloadSession {
|
||||
public:
|
||||
DownloadSession(
|
||||
std::shared_ptr<struct event_base> base,
|
||||
const struct sockaddr_storage& remote,
|
||||
const std::string& output_dir,
|
||||
Version version,
|
||||
uint8_t language,
|
||||
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file,
|
||||
uint32_t hardware_id,
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& username,
|
||||
const std::string& password,
|
||||
const std::string& xb_gamertag,
|
||||
uint64_t xb_user_id,
|
||||
uint64_t xb_account_id,
|
||||
std::shared_ptr<PSOBBCharacterFile> character,
|
||||
const std::unordered_set<std::string>& ship_menu_selections,
|
||||
const std::vector<std::string>& on_request_complete_commands,
|
||||
bool interactive,
|
||||
bool show_command_data);
|
||||
DownloadSession(const DownloadSession&) = delete;
|
||||
DownloadSession(DownloadSession&&) = delete;
|
||||
DownloadSession& operator=(const DownloadSession&) = delete;
|
||||
DownloadSession& operator=(DownloadSession&&) = delete;
|
||||
virtual ~DownloadSession() = default;
|
||||
|
||||
protected:
|
||||
// Config (must be set by caller)
|
||||
std::string output_dir;
|
||||
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file;
|
||||
uint32_t hardware_id;
|
||||
uint32_t serial_number;
|
||||
std::string access_key;
|
||||
std::string username;
|
||||
std::string password;
|
||||
std::string xb_gamertag;
|
||||
uint64_t xb_user_id;
|
||||
uint64_t xb_account_id;
|
||||
std::shared_ptr<PSOBBCharacterFile> character;
|
||||
std::unordered_set<std::string> ship_menu_selections;
|
||||
std::vector<std::string> on_request_complete_commands;
|
||||
bool interactive;
|
||||
|
||||
// State (set during session)
|
||||
phosg::PrefixedLogger log;
|
||||
std::shared_ptr<struct event_base> base;
|
||||
Channel channel;
|
||||
uint32_t guild_card_number;
|
||||
parray<uint8_t, 0x28> prev_cmd_data;
|
||||
parray<uint8_t, 0x20> client_config;
|
||||
bool sent_96;
|
||||
std::vector<S_LobbyListEntry_83> lobby_menu_items;
|
||||
|
||||
bool should_request_category_list;
|
||||
uint64_t current_request;
|
||||
std::map<uint64_t, std::string> pending_requests;
|
||||
std::unordered_set<uint64_t> done_requests;
|
||||
|
||||
struct OpenFile {
|
||||
uint64_t request;
|
||||
std::string filename;
|
||||
size_t total_size;
|
||||
std::string data;
|
||||
};
|
||||
std::unordered_map<std::string, OpenFile> open_files;
|
||||
|
||||
struct GameConfig {
|
||||
GameMode mode;
|
||||
Episode episode;
|
||||
bool v1;
|
||||
bool v2;
|
||||
bool v3;
|
||||
};
|
||||
static const std::vector<GameConfig> game_configs;
|
||||
size_t current_game_config_index;
|
||||
bool in_game;
|
||||
bool bin_complete;
|
||||
bool dat_complete;
|
||||
|
||||
static void dispatch_on_channel_input(Channel& ch, uint16_t command, uint32_t flag, std::string& msg);
|
||||
static void dispatch_on_channel_error(Channel& ch, short events);
|
||||
void on_channel_input(uint16_t command, uint32_t flag, std::string& msg);
|
||||
void on_channel_error(short events);
|
||||
|
||||
void send_next_request();
|
||||
void on_request_complete();
|
||||
|
||||
void assign_item_ids(uint32_t base_item_id);
|
||||
void send_93_9D_9E(bool extended);
|
||||
void send_61_98(bool is_98);
|
||||
};
|
||||
+54
-13
@@ -714,7 +714,7 @@ int32_t Card::move_to_location(const Location& loc) {
|
||||
uint8_t trap_type = s->overlay_state.tiles[loc.y][loc.x] & 0x0F;
|
||||
uint16_t trap_card_id = s->overlay_state.trap_card_ids_nte[trap_type];
|
||||
if (other_ps->replace_assist_card_by_id(trap_card_id)) {
|
||||
G_Unknown_Ep3_6xB4x2C cmd;
|
||||
G_EnqueueAnimation_Ep3_6xB4x2C cmd;
|
||||
cmd.change_type = 1;
|
||||
cmd.client_id = other_ps->client_id;
|
||||
cmd.unknown_a2[0] = trap_card_id;
|
||||
@@ -728,7 +728,7 @@ int32_t Card::move_to_location(const Location& loc) {
|
||||
for (size_t warp_end = 0; warp_end < 2; warp_end++) {
|
||||
if ((s->warp_positions[warp_type][warp_end][0] == this->loc.x) &&
|
||||
(s->warp_positions[warp_type][warp_end][1] == this->loc.y)) {
|
||||
G_Unknown_Ep3_6xB4x2C cmd;
|
||||
G_EnqueueAnimation_Ep3_6xB4x2C cmd;
|
||||
cmd.loc.x = this->loc.x;
|
||||
cmd.loc.y = this->loc.y;
|
||||
this->loc.x = s->warp_positions[warp_type][warp_end ^ 1][0];
|
||||
@@ -1154,6 +1154,11 @@ void Card::compute_action_chain_results(bool apply_action_conditions, bool ignor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (log.should_log(phosg::LogLevel::DEBUG)) {
|
||||
string chain_str = this->action_chain.str(s);
|
||||
log.debug("result computed as %s", chain_str.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Card::unknown_802380C0() {
|
||||
@@ -1222,11 +1227,22 @@ void Card::unknown_80236374(shared_ptr<Card> other_card, const ActionState* as)
|
||||
auto s = this->server();
|
||||
auto log = s->log_stack(phosg::string_printf("unknown_80236374(@%04hX #%04hX, @%04hX #%04hX): ", this->get_card_ref(), this->get_card_id(), other_card->get_card_ref(), other_card->get_card_id()));
|
||||
|
||||
if (log.should_log(phosg::LogLevel::DEBUG)) {
|
||||
if (as) {
|
||||
string as_str = as->str(s);
|
||||
log.debug("as = %s", as_str.c_str());
|
||||
} else {
|
||||
log.debug("as = null");
|
||||
}
|
||||
}
|
||||
|
||||
auto check_card = [&](shared_ptr<Card> card) -> void {
|
||||
if (card) {
|
||||
if (!card->unknown_80236554(other_card, as)) {
|
||||
log.debug("check_card @%04hX #%04hX => false", card->get_card_ref(), card->get_card_id());
|
||||
card->action_metadata.clear_flags(0x20);
|
||||
} else {
|
||||
log.debug("check_card @%04hX #%04hX => true", card->get_card_ref(), card->get_card_id());
|
||||
card->action_metadata.set_flags(0x20);
|
||||
}
|
||||
}
|
||||
@@ -1363,11 +1379,13 @@ bool Card::unknown_80236554(shared_ptr<Card> other_card, const ActionState* as)
|
||||
auto log = s->log_stack(other_card
|
||||
? phosg::string_printf("unknown_80236554(@%04hX #%04hX, @%04hX #%04hX): ", this->get_card_ref(), this->get_card_id(), other_card->get_card_ref(), other_card->get_card_id())
|
||||
: phosg::string_printf("unknown_80236554(@%04hX #%04hX, null): ", this->get_card_ref(), this->get_card_id()));
|
||||
if (as) {
|
||||
string as_str = as->str(s);
|
||||
log.debug("as = %s", as_str.c_str());
|
||||
} else {
|
||||
log.debug("as = null");
|
||||
if (log.should_log(phosg::LogLevel::DEBUG)) {
|
||||
if (as) {
|
||||
string as_str = as->str(s);
|
||||
log.debug("as = %s", as_str.c_str());
|
||||
} else {
|
||||
log.debug("as = null");
|
||||
}
|
||||
}
|
||||
|
||||
bool ret = false;
|
||||
@@ -1402,32 +1420,38 @@ bool Card::unknown_80236554(shared_ptr<Card> other_card, const ActionState* as)
|
||||
log.debug("last attack damage stats cleared");
|
||||
|
||||
if (other_card) {
|
||||
s->card_special->apply_action_conditions(EffectWhen::BEFORE_ANY_CARD_ATTACK, other_card, this->shared_from_this(), 0x20, as);
|
||||
s->card_special->apply_action_conditions(EffectWhen::BEFORE_THIS_CARD_ATTACKED, other_card, this->shared_from_this(), 0x40, as);
|
||||
log.debug("applying BEFORE_ANY_CARD_ATTACK conditions");
|
||||
s->card_special->apply_action_conditions(
|
||||
EffectWhen::BEFORE_ANY_CARD_ATTACK, other_card, this->shared_from_this(), 0x20, as);
|
||||
log.debug("applying BEFORE_THIS_CARD_ATTACKED conditions");
|
||||
s->card_special->apply_action_conditions(
|
||||
EffectWhen::BEFORE_THIS_CARD_ATTACKED, other_card, this->shared_from_this(), 0x40, as);
|
||||
if (other_card->action_chain.check_flag(0x20000)) {
|
||||
log.debug("attack_bonus cleared due to cancellation");
|
||||
this->action_metadata.attack_bonus = 0;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
if (this->card_flags & 2) {
|
||||
log.debug("attack_bonus cleared due to destruction");
|
||||
this->action_metadata.attack_bonus = 0;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Card::unknown_802362D8(shared_ptr<Card> other_card) {
|
||||
void Card::execute_attack_on_all_valid_targets(shared_ptr<Card> attacker_card) {
|
||||
auto s = this->server();
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
auto ps = s->player_states[client_id];
|
||||
if (ps) {
|
||||
shared_ptr<Card> card = ps->get_sc_card();
|
||||
if (card) {
|
||||
card->execute_attack(other_card);
|
||||
card->execute_attack(attacker_card);
|
||||
}
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
shared_ptr<Card> card = ps->get_set_card(set_index);
|
||||
if (card) {
|
||||
card->execute_attack(other_card);
|
||||
card->execute_attack(attacker_card);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1548,30 +1572,46 @@ void Card::apply_attack_result() {
|
||||
}
|
||||
}
|
||||
|
||||
if (log.should_log(phosg::LogLevel::DEBUG)) {
|
||||
string as_str = as.str(s);
|
||||
log.debug("as constructed as %s", as_str.c_str());
|
||||
}
|
||||
|
||||
for (size_t z = 0; z < this->action_chain.chain.target_card_ref_count; z++) {
|
||||
shared_ptr<Card> card = s->card_for_set_card_ref(this->action_chain.chain.target_card_refs[z]);
|
||||
if (card) {
|
||||
card->current_defense_power = card->action_metadata.attack_bonus;
|
||||
if (!this->action_chain.check_flag(0x40)) {
|
||||
log.debug("unknown_8024A6DC(@%04hX #%04hX) ...", card->get_card_ref(), card->get_card_id());
|
||||
s->card_special->unknown_8024A6DC(this->shared_from_this(), card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("compute_action_chain_results 1 ...");
|
||||
this->compute_action_chain_results(true, false);
|
||||
|
||||
if (!this->action_chain.check_flag(0x40)) {
|
||||
log.debug("apply_effects_before_attack ...");
|
||||
s->card_special->apply_effects_before_attack(this->shared_from_this());
|
||||
}
|
||||
if (!(this->card_flags & 2)) {
|
||||
log.debug("compute_action_chain_results 2 ...");
|
||||
this->compute_action_chain_results(true, false);
|
||||
log.debug("check_for_attack_interference ...");
|
||||
s->card_special->check_for_attack_interference(this->shared_from_this());
|
||||
}
|
||||
log.debug("compute_action_chain_results 3 ...");
|
||||
this->compute_action_chain_results(true, false);
|
||||
|
||||
log.debug("unknown_80236374 ...");
|
||||
this->unknown_80236374(this->shared_from_this(), nullptr);
|
||||
this->unknown_802362D8(this->shared_from_this());
|
||||
log.debug("execute_attack_on_all_valid_targets ...");
|
||||
this->execute_attack_on_all_valid_targets(this->shared_from_this());
|
||||
}
|
||||
|
||||
if (!this->action_chain.check_flag(0x40)) {
|
||||
log.debug("apply_effects_after_attack ...");
|
||||
s->card_special->apply_effects_after_attack(this->shared_from_this());
|
||||
}
|
||||
ps->stats.num_attacks_given++;
|
||||
@@ -1584,6 +1624,7 @@ void Card::apply_attack_result() {
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
auto ps = s->player_states[client_id];
|
||||
if (ps) {
|
||||
log.debug("unknown_8023C110(%zu) ...", client_id);
|
||||
ps->unknown_8023C110();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ public:
|
||||
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);
|
||||
void execute_attack_on_all_valid_targets(std::shared_ptr<Card> attacker_card);
|
||||
void apply_attack_result();
|
||||
|
||||
private:
|
||||
|
||||
+153
-41
@@ -244,15 +244,21 @@ void CardSpecial::apply_action_conditions(
|
||||
shared_ptr<Card> defender_card,
|
||||
uint32_t flags,
|
||||
const ActionState* as) {
|
||||
auto s = this->server();
|
||||
auto log = s->log_stack("apply_action_conditions: ");
|
||||
|
||||
ActionState temp_as;
|
||||
if (attacker_card == defender_card) {
|
||||
temp_as = this->create_attack_state_from_card_action_chain(attacker_card);
|
||||
if (as) {
|
||||
log.debug("using action state from override");
|
||||
temp_as = *as;
|
||||
} else {
|
||||
log.debug("using action state from attacker card");
|
||||
}
|
||||
} else {
|
||||
temp_as = this->create_defense_state_for_card_pair_action_chains(attacker_card, defender_card);
|
||||
log.debug("using action state from card pair");
|
||||
}
|
||||
|
||||
this->apply_defense_conditions(temp_as, when, defender_card, flags);
|
||||
@@ -297,22 +303,47 @@ bool CardSpecial::apply_defense_condition(
|
||||
shared_ptr<Card> defender_card,
|
||||
uint32_t flags,
|
||||
bool unknown_p8) {
|
||||
auto s = this->server();
|
||||
bool is_nte = s->options.is_nte();
|
||||
auto log = s->log_stack("apply_defense_condition: ");
|
||||
|
||||
if (log.should_log(phosg::LogLevel::DEBUG)) {
|
||||
log.debug(
|
||||
"when=%s, cond_index=%hhu, defender_card=(@%04hX #%04hX), flags=%08" PRIX32 ", p8=%s",
|
||||
phosg::name_for_enum(when),
|
||||
cond_index,
|
||||
defender_card->get_card_ref(),
|
||||
defender_card->get_card_id(),
|
||||
flags,
|
||||
unknown_p8 ? "true" : "false");
|
||||
auto defender_cond_str = defender_cond->str(s);
|
||||
auto defense_state_str = defense_state.str(s);
|
||||
log.debug("defender_cond = %s", defender_cond_str.c_str());
|
||||
log.debug("defense_state = %s", defense_state_str.c_str());
|
||||
}
|
||||
|
||||
if (defender_cond->type == ConditionType::NONE) {
|
||||
log.debug("no condition");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto orig_eff = this->original_definition_for_condition(*defender_cond);
|
||||
if (log.should_log(phosg::LogLevel::DEBUG)) {
|
||||
auto orig_eff_str = orig_eff->str();
|
||||
log.debug("orig_eff = %s", orig_eff_str.c_str());
|
||||
}
|
||||
|
||||
uint16_t attacker_card_ref = defense_state.attacker_card_ref;
|
||||
if (attacker_card_ref == 0xFFFF) {
|
||||
attacker_card_ref = defense_state.original_attacker_card_ref;
|
||||
}
|
||||
log.debug("attacker_card_ref = @%04hX", attacker_card_ref);
|
||||
|
||||
auto s = this->server();
|
||||
bool is_nte = s->options.is_nte();
|
||||
bool defender_has_ability_trap = !is_nte && this->card_ref_has_ability_trap(*defender_cond);
|
||||
log.debug("defender_has_ability_trap = %s", defender_has_ability_trap ? "true" : "false");
|
||||
|
||||
if ((is_nte || (flags & 4)) && !this->is_card_targeted_by_condition(*defender_cond, defense_state, defender_card)) {
|
||||
log.debug("not targeted by condition");
|
||||
if (defender_cond->type != ConditionType::NONE) {
|
||||
G_ApplyConditionEffect_Ep3_6xB4x06 cmd;
|
||||
cmd.effect.flags = 0x04;
|
||||
@@ -328,6 +359,7 @@ bool CardSpecial::apply_defense_condition(
|
||||
}
|
||||
|
||||
if ((when == EffectWhen::AFTER_ANY_CARD_ATTACK) && (defender_cond->type == ConditionType::GUOM) && (flags & 4)) {
|
||||
log.debug("deleting guom condition");
|
||||
CardShortStatus stat = defender_card->get_short_status();
|
||||
if (stat.card_flags & 4) {
|
||||
G_ApplyConditionEffect_Ep3_6xB4x06 cmd;
|
||||
@@ -364,6 +396,7 @@ bool CardSpecial::apply_defense_condition(
|
||||
}
|
||||
|
||||
if ((when == EffectWhen::BEFORE_DICE_PHASE_THIS_TEAM_TURN) && (flags & 4) && !defender_has_ability_trap && (defender_cond->type == ConditionType::ACID)) {
|
||||
log.debug("applying acid");
|
||||
int16_t hp = defender_card->get_current_hp();
|
||||
if (hp > 0) {
|
||||
this->send_6xB4x06_for_stat_delta(defender_card, defender_cond->card_ref, 0x20, -1, 0, 1);
|
||||
@@ -373,9 +406,11 @@ bool CardSpecial::apply_defense_condition(
|
||||
}
|
||||
|
||||
if (!orig_eff || (orig_eff->when != when)) {
|
||||
log.debug("unsetting flag 4");
|
||||
flags &= ~4;
|
||||
}
|
||||
if ((flags == 0) || defender_has_ability_trap) {
|
||||
log.debug("no condition remains to apply");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -392,8 +427,10 @@ bool CardSpecial::apply_defense_condition(
|
||||
|
||||
string expr = orig_eff->expr.decode();
|
||||
int16_t expr_value = this->evaluate_effect_expr(astats, expr.c_str(), dice_roll);
|
||||
log.debug("execute_effect ...");
|
||||
this->execute_effect(*defender_cond, defender_card, expr_value, defender_cond->value, orig_eff->type, flags, attacker_card_ref);
|
||||
if (flags & 4) {
|
||||
log.debug("recomputing action chaing results");
|
||||
if (is_nte || !(defender_card->card_flags & 2)) {
|
||||
defender_card->compute_action_chain_results(true, false);
|
||||
}
|
||||
@@ -403,6 +440,7 @@ bool CardSpecial::apply_defense_condition(
|
||||
}
|
||||
|
||||
if (dice_roll.value_used_in_expr && !(original_cond_flags & 1) && !unknown_p8) {
|
||||
log.debug("dice roll was used; setting dice display flag");
|
||||
defender_cond->flags |= 1;
|
||||
G_ApplyConditionEffect_Ep3_6xB4x06 cmd;
|
||||
cmd.effect.flags = 0x08;
|
||||
@@ -3209,17 +3247,27 @@ vector<shared_ptr<const Card>> CardSpecial::get_targeted_cards_for_condition(
|
||||
}
|
||||
break;
|
||||
case 0x27: // p39
|
||||
ret = this->find_all_set_cards_with_cost_in_range(4, 99);
|
||||
if (!s->options.is_nte()) {
|
||||
ret = this->filter_cards_by_range(ret, card1, card1_loc, card2);
|
||||
}
|
||||
break;
|
||||
case 0x28: // p40
|
||||
ret = this->find_all_set_cards_with_cost_in_range(0, 3);
|
||||
case 0x28: { // p40
|
||||
auto log3940 = log.sub("(p39/p40) ");
|
||||
ret = this->find_all_set_cards_with_cost_in_range(
|
||||
(p_target_type == 0x27) ? 4 : 0,
|
||||
(p_target_type == 0x27) ? 99 : 3);
|
||||
if (log3940.should_log(phosg::LogLevel::DEBUG)) {
|
||||
for (const auto& card : ret) {
|
||||
log3940.debug("found target @%04hX #%04hX", card->get_card_ref(), card->get_card_id());
|
||||
}
|
||||
}
|
||||
if (!s->options.is_nte()) {
|
||||
log3940.debug("filtering targets");
|
||||
ret = this->filter_cards_by_range(ret, card1, card1_loc, card2);
|
||||
if (log3940.should_log(phosg::LogLevel::DEBUG)) {
|
||||
for (const auto& card : ret) {
|
||||
log3940.debug("retained target @%04hX #%04hX", card->get_card_ref(), card->get_card_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x29: { // p41
|
||||
auto ps = card1->player_state();
|
||||
if (card1 && ps) {
|
||||
@@ -3476,41 +3524,62 @@ bool CardSpecial::is_card_targeted_by_condition(
|
||||
const ActionState& as,
|
||||
shared_ptr<const Card> card) const {
|
||||
auto s = this->server();
|
||||
auto log = s->log_stack("is_card_targeted_by_condition: ");
|
||||
|
||||
if (log.should_log(phosg::LogLevel::DEBUG)) {
|
||||
log.debug("card=(@%04hX #%04hX)", card->get_card_ref(), card->get_card_id());
|
||||
auto cond_str = cond.str(s);
|
||||
auto as_str = as.str(s);
|
||||
log.debug("cond = %s", cond_str.c_str());
|
||||
log.debug("as = %s", as_str.c_str());
|
||||
}
|
||||
|
||||
if (cond.type == ConditionType::NONE) {
|
||||
log.debug("condition is NONE (=> true)");
|
||||
return true;
|
||||
}
|
||||
|
||||
auto ce = s->definition_for_card_ref(cond.card_ref);
|
||||
auto sc_card = s->card_for_set_card_ref(cond.card_ref);
|
||||
if (cond.type != ConditionType::NONE) {
|
||||
if ((!sc_card || ((sc_card != card) && (sc_card->card_flags & 2))) &&
|
||||
ce &&
|
||||
((ce->def.type == CardType::ITEM) || ce->def.is_sc()) &&
|
||||
(cond.remaining_turns != 100) &&
|
||||
(s->options.is_nte() || (client_id_for_card_ref(card->get_card_ref()) == client_id_for_card_ref(cond.card_ref)))) {
|
||||
return false;
|
||||
}
|
||||
if (cond.remaining_turns == 102) {
|
||||
if (sc_card && ((sc_card == card) || !(sc_card->card_flags & 2))) {
|
||||
string arg3_s = ce->def.effects[cond.card_definition_effect_index].arg3.decode();
|
||||
if (arg3_s.size() < 1) {
|
||||
throw runtime_error("card definition arg3 is missing");
|
||||
}
|
||||
auto target_cards = this->get_targeted_cards_for_condition(
|
||||
cond.card_ref,
|
||||
cond.card_definition_effect_index,
|
||||
cond.condition_giver_card_ref,
|
||||
as,
|
||||
atoi(arg3_s.c_str() + 1),
|
||||
0);
|
||||
for (auto c : target_cards) {
|
||||
if (c == card) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
if ((!sc_card || ((sc_card != card) && (sc_card->card_flags & 2))) &&
|
||||
ce &&
|
||||
((ce->def.type == CardType::ITEM) || ce->def.is_sc()) &&
|
||||
(cond.remaining_turns != 100) &&
|
||||
(s->options.is_nte() || (client_id_for_card_ref(card->get_card_ref()) == client_id_for_card_ref(cond.card_ref)))) {
|
||||
log.debug("failed item or SC check (=> false)");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cond.remaining_turns != 102) {
|
||||
log.debug("remaining_turns != 102 (=> true)");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sc_card && ((sc_card == card) || !(sc_card->card_flags & 2))) {
|
||||
string arg3_s = ce->def.effects[cond.card_definition_effect_index].arg3.decode();
|
||||
if (arg3_s.size() < 1) {
|
||||
throw runtime_error("card definition arg3 is missing");
|
||||
}
|
||||
auto target_cards = this->get_targeted_cards_for_condition(
|
||||
cond.card_ref,
|
||||
cond.card_definition_effect_index,
|
||||
cond.condition_giver_card_ref,
|
||||
as,
|
||||
atoi(arg3_s.c_str() + 1),
|
||||
0);
|
||||
for (auto c : target_cards) {
|
||||
if (c == card) {
|
||||
log.debug("targeted by p condition (=> true)");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
log.debug("not targeted by p condition (=> false)");
|
||||
return false;
|
||||
} else {
|
||||
|
||||
log.debug("SC check does not apply");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CardSpecial::on_card_set(shared_ptr<PlayerState> ps, uint16_t card_ref) {
|
||||
@@ -4641,32 +4710,75 @@ vector<shared_ptr<const Card>> CardSpecial::filter_cards_by_range(
|
||||
shared_ptr<const Card> card1,
|
||||
const Location& card1_loc,
|
||||
shared_ptr<const Card> card2) const {
|
||||
auto log = this->server()->log_stack("filter_cards_by_range: ");
|
||||
|
||||
if (log.should_log(phosg::LogLevel::DEBUG)) {
|
||||
auto card1_str = card1 ? phosg::string_printf("@%04hX #%04hX", card1->get_card_ref(), card1->get_card_id()) : "null";
|
||||
auto card2_str = card2 ? phosg::string_printf("@%04hX #%04hX", card2->get_card_ref(), card2->get_card_id()) : "null";
|
||||
auto loc_str = card1_loc.str();
|
||||
log.debug("card1=(%s), card2=(%s), loc=%s", card1_str.c_str(), card2_str.c_str(), loc_str.c_str());
|
||||
|
||||
for (const auto& card : cards) {
|
||||
if (card) {
|
||||
log.debug("input card: @%04hX #%04hX", card->get_card_ref(), card->get_card_id());
|
||||
} else {
|
||||
log.debug("input card: null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vector<shared_ptr<const Card>> ret;
|
||||
if (!card1 || cards.empty()) {
|
||||
log.debug("card1 missing or input list is blank");
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto ps = card1->player_state();
|
||||
if (!ps) {
|
||||
log.debug("ps is missing");
|
||||
return ret;
|
||||
}
|
||||
|
||||
// TODO: Remove hardcoded card ID here (Earthquake)
|
||||
uint16_t card_id = this->get_card_id_with_effective_range(card1, 0x00ED, card2);
|
||||
log.debug("card_id = #%04hX", card_id);
|
||||
|
||||
parray<uint8_t, 9 * 9> range;
|
||||
compute_effective_range(range, this->server()->options.card_index, card_id, card1_loc, this->server()->map_and_rules);
|
||||
if (log.should_log(phosg::LogLevel::DEBUG)) {
|
||||
auto loc_str = card1_loc.str();
|
||||
log.debug("compute_effective_range(range, ci, #%04hX, %s, map) =>", card_id, loc_str.c_str());
|
||||
for (size_t y = 0; y < 9; y++) {
|
||||
const uint8_t* row = &range[y * 9];
|
||||
log.debug(" range[%zu] = %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX",
|
||||
y, row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8]);
|
||||
}
|
||||
}
|
||||
|
||||
auto card_refs_in_range = ps->get_card_refs_within_range_from_all_players(range, card1_loc, CardType::ITEM);
|
||||
if (log.should_log(phosg::LogLevel::DEBUG)) {
|
||||
for (uint16_t card_ref : card_refs_in_range) {
|
||||
log.debug("ref in range: @%04hX", card_ref);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto card : cards) {
|
||||
if (!card || (card->get_card_ref() == 0xFFFF)) {
|
||||
if (card) {
|
||||
log.debug("(@%04hX #%04hX) out of range", card->get_card_ref(), card->get_card_id());
|
||||
} else {
|
||||
log.debug("(null) card missing");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (uint16_t card_ref_in_range : card_refs_in_range) {
|
||||
if (card_ref_in_range == card->get_card_ref()) {
|
||||
log.debug("(@%04hX #%04hX) in range", card->get_card_ref(), card->get_card_id());
|
||||
ret.emplace_back(card);
|
||||
break;
|
||||
}
|
||||
}
|
||||
log.debug("(@%04hX #%04hX) out of range", card->get_card_ref(), card->get_card_id());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
+92
-31
@@ -975,9 +975,25 @@ string CardDefinition::str(bool single_line, const TextSet* text_archive) const
|
||||
}
|
||||
}
|
||||
}
|
||||
string jp_name_s = this->jp_name.decode();
|
||||
string en_name_short_s = this->en_short_name.decode();
|
||||
string jp_name_short_s = this->jp_short_name.decode();
|
||||
string names_str;
|
||||
if (!en_name_s.empty()) {
|
||||
names_str += phosg::string_printf(" EN: \"%s\"", en_name_s.c_str());
|
||||
if (!en_name_short_s.empty() && en_name_short_s != en_name_s) {
|
||||
names_str += phosg::string_printf(" (Abr. \"%s\")", en_name_short_s.c_str());
|
||||
}
|
||||
}
|
||||
if (!jp_name_s.empty()) {
|
||||
names_str += phosg::string_printf(" JP: \"%s\"", jp_name_s.c_str());
|
||||
if (!jp_name_short_s.empty() && jp_name_short_s != jp_name_s) {
|
||||
names_str += phosg::string_printf(" (Abr. \"%s\")", jp_name_short_s.c_str());
|
||||
}
|
||||
}
|
||||
return phosg::string_printf(
|
||||
"\
|
||||
Card: %04" PRIX32 " \"%s\"\n\
|
||||
Card: %04" PRIX32 "%s\n\
|
||||
Type: %s, class: %s\n\
|
||||
Usability condition: %s\n\
|
||||
Rank: %s\n\
|
||||
@@ -998,7 +1014,7 @@ Card: %04" PRIX32 " \"%s\"\n\
|
||||
%s\n\
|
||||
Effects:%s",
|
||||
this->card_id.load(),
|
||||
en_name_s.c_str(),
|
||||
names_str.c_str(),
|
||||
type_str.c_str(),
|
||||
card_class_str.c_str(),
|
||||
criterion_str.c_str(),
|
||||
@@ -1177,6 +1193,7 @@ PlayerConfigNTE::PlayerConfigNTE(const PlayerConfig& config)
|
||||
offline_clv_exp(config.offline_clv_exp),
|
||||
online_clv_exp(config.online_clv_exp),
|
||||
recent_human_opponents(config.recent_human_opponents),
|
||||
recent_battle_start_timestamps(config.recent_battle_start_timestamps),
|
||||
unknown_a10(config.unknown_a10),
|
||||
init_timestamp(config.init_timestamp),
|
||||
last_online_battle_start_timestamp(config.last_online_battle_start_timestamp),
|
||||
@@ -1206,6 +1223,7 @@ PlayerConfigNTE::operator PlayerConfig() const {
|
||||
ret.offline_clv_exp = this->offline_clv_exp;
|
||||
ret.online_clv_exp = this->online_clv_exp;
|
||||
ret.recent_human_opponents = this->recent_human_opponents;
|
||||
ret.recent_battle_start_timestamps = this->recent_battle_start_timestamps;
|
||||
ret.unknown_a10 = this->unknown_a10;
|
||||
ret.init_timestamp = this->init_timestamp;
|
||||
ret.last_online_battle_start_timestamp = this->last_online_battle_start_timestamp;
|
||||
@@ -1584,6 +1602,19 @@ void StateFlags::clear_FF() {
|
||||
this->client_sc_card_types.clear(CardType::INVALID_FF);
|
||||
}
|
||||
|
||||
OverlayState::OverlayState() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void OverlayState::clear() {
|
||||
for (size_t y = 0; y < this->tiles.size(); y++) {
|
||||
this->tiles[y].clear(0);
|
||||
}
|
||||
this->unused1.clear(0);
|
||||
this->trap_tile_colors_nte.clear(0);
|
||||
this->trap_card_ids_nte.clear(0);
|
||||
}
|
||||
|
||||
void MapDefinition::assert_semantically_equivalent(const MapDefinition& other) const {
|
||||
if (this->map_number != other.map_number) {
|
||||
throw runtime_error("map number not equal");
|
||||
@@ -1603,12 +1634,9 @@ void MapDefinition::assert_semantically_equivalent(const MapDefinition& other) c
|
||||
if (this->start_tile_definitions != other.start_tile_definitions) {
|
||||
throw runtime_error("start tile definitions not equal");
|
||||
}
|
||||
if (this->modification_tiles != other.modification_tiles) {
|
||||
if (this->overlay_state.tiles != other.overlay_state.tiles) {
|
||||
throw runtime_error("modification tiles not equal");
|
||||
}
|
||||
if (this->unknown_a5 != other.unknown_a5) {
|
||||
throw runtime_error("unknown_a5 not equal");
|
||||
}
|
||||
if (this->default_rules != other.default_rules) {
|
||||
throw runtime_error("default rules not equal");
|
||||
}
|
||||
@@ -1814,19 +1842,52 @@ string MapDefinition::str(const CardIndex* card_index, uint8_t language) const {
|
||||
lines.emplace_back(phosg::string_printf(" overview_specs[%zu][team %zu]: %s", w, z, spec_str.c_str()));
|
||||
}
|
||||
}
|
||||
lines.emplace_back(" modification tiles:");
|
||||
add_map(this->modification_tiles);
|
||||
for (size_t z = 0; z < 0x70; z += 0x10) {
|
||||
lines.emplace_back(phosg::string_printf(
|
||||
" a5[0x%02zX:0x%02zX]: %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX", z, z + 0x10,
|
||||
this->unknown_a5[z + 0x00], this->unknown_a5[z + 0x01], this->unknown_a5[z + 0x02], this->unknown_a5[z + 0x03],
|
||||
this->unknown_a5[z + 0x04], this->unknown_a5[z + 0x05], this->unknown_a5[z + 0x06], this->unknown_a5[z + 0x07],
|
||||
this->unknown_a5[z + 0x08], this->unknown_a5[z + 0x09], this->unknown_a5[z + 0x0A], this->unknown_a5[z + 0x0B],
|
||||
this->unknown_a5[z + 0x0C], this->unknown_a5[z + 0x0D], this->unknown_a5[z + 0x0E], this->unknown_a5[z + 0x0F]));
|
||||
}
|
||||
lines.emplace_back(" overlay tiles:");
|
||||
add_map(this->overlay_state.tiles);
|
||||
lines.emplace_back(phosg::string_printf(
|
||||
" a5[0x70:0x74]: %02hhX %02hhX %02hhX %02hhX",
|
||||
this->unknown_a5[0x70], this->unknown_a5[0x71], this->unknown_a5[0x72], this->unknown_a5[0x73]));
|
||||
" unused1: %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32,
|
||||
this->overlay_state.unused1[0].load(),
|
||||
this->overlay_state.unused1[1].load(),
|
||||
this->overlay_state.unused1[2].load(),
|
||||
this->overlay_state.unused1[3].load(),
|
||||
this->overlay_state.unused1[4].load()));
|
||||
lines.emplace_back(phosg::string_printf(
|
||||
" trap_tile_colors_nte: %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32 " %08" PRIX32,
|
||||
this->overlay_state.trap_tile_colors_nte[0].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[1].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[2].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[3].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[4].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[5].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[6].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[7].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[8].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[9].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[10].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[11].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[12].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[13].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[14].load(),
|
||||
this->overlay_state.trap_tile_colors_nte[15].load()));
|
||||
lines.emplace_back(phosg::string_printf(
|
||||
" trap_card_ids_nte: #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX #%04hX",
|
||||
this->overlay_state.trap_card_ids_nte[0].load(),
|
||||
this->overlay_state.trap_card_ids_nte[1].load(),
|
||||
this->overlay_state.trap_card_ids_nte[2].load(),
|
||||
this->overlay_state.trap_card_ids_nte[3].load(),
|
||||
this->overlay_state.trap_card_ids_nte[4].load(),
|
||||
this->overlay_state.trap_card_ids_nte[5].load(),
|
||||
this->overlay_state.trap_card_ids_nte[6].load(),
|
||||
this->overlay_state.trap_card_ids_nte[7].load(),
|
||||
this->overlay_state.trap_card_ids_nte[8].load(),
|
||||
this->overlay_state.trap_card_ids_nte[9].load(),
|
||||
this->overlay_state.trap_card_ids_nte[10].load(),
|
||||
this->overlay_state.trap_card_ids_nte[11].load(),
|
||||
this->overlay_state.trap_card_ids_nte[12].load(),
|
||||
this->overlay_state.trap_card_ids_nte[13].load(),
|
||||
this->overlay_state.trap_card_ids_nte[14].load(),
|
||||
this->overlay_state.trap_card_ids_nte[15].load()));
|
||||
|
||||
lines.emplace_back(" default_rules: " + this->default_rules.str());
|
||||
lines.emplace_back(" name: " + this->name.decode(language));
|
||||
lines.emplace_back(" location_name: " + this->location_name.decode(language));
|
||||
@@ -1868,9 +1929,9 @@ string MapDefinition::str(const CardIndex* card_index, uint8_t language) const {
|
||||
}
|
||||
if (entry) {
|
||||
string name = entry->def.en_name.decode(language);
|
||||
lines.emplace_back(phosg::string_printf(" cards[%02zu]: %04hX (%s)", w, card_id, name.c_str()));
|
||||
lines.emplace_back(phosg::string_printf(" cards[%02zu]: #%04hX (%s)", w, card_id, name.c_str()));
|
||||
} else {
|
||||
lines.emplace_back(phosg::string_printf(" cards[%02zu]: %04hX", w, card_id));
|
||||
lines.emplace_back(phosg::string_printf(" cards[%02zu]: #%04hX", w, card_id));
|
||||
}
|
||||
}
|
||||
for (size_t x = 0; x < 0x10; x++) {
|
||||
@@ -1911,9 +1972,9 @@ string MapDefinition::str(const CardIndex* card_index, uint8_t language) const {
|
||||
}
|
||||
if (entry) {
|
||||
string name = entry->def.en_name.decode(language);
|
||||
lines.emplace_back(phosg::string_printf(" reward_cards[%02zu]: %04hX (%s)", z, card_id, name.c_str()));
|
||||
lines.emplace_back(phosg::string_printf(" reward_cards[%02zu]: #%04hX (%s)", z, card_id, name.c_str()));
|
||||
} else {
|
||||
lines.emplace_back(phosg::string_printf(" reward_cards[%02zu]: %04hX", z, card_id));
|
||||
lines.emplace_back(phosg::string_printf(" reward_cards[%02zu]: #%04hX", z, card_id));
|
||||
}
|
||||
}
|
||||
lines.emplace_back(phosg::string_printf(" level_overrides: [win: %" PRId32 ", loss: %" PRId32 "]",
|
||||
@@ -1921,7 +1982,7 @@ string MapDefinition::str(const CardIndex* card_index, uint8_t language) const {
|
||||
lines.emplace_back(phosg::string_printf(" field_offset: (x: %hd units, y:%hd units) (x: %lg tiles, y: %lg tiles)", this->field_offset_x.load(), this->field_offset_y.load(), static_cast<double>(this->field_offset_x) / 25.0, static_cast<double>(this->field_offset_y) / 25.0));
|
||||
lines.emplace_back(phosg::string_printf(" map_category: %02hhX", this->map_category));
|
||||
lines.emplace_back(phosg::string_printf(" cyber_block_type: %02hhX", this->cyber_block_type));
|
||||
lines.emplace_back(phosg::string_printf(" a11: %02hhX%02hhX", this->unknown_a11[0], this->unknown_a11[1]));
|
||||
lines.emplace_back(phosg::string_printf(" a11: %04hX", this->unknown_a11.load()));
|
||||
static const array<const char*, 0x18> sc_card_entry_names = {
|
||||
"00 (Guykild; 0005)",
|
||||
"01 (Kylria; 0006)",
|
||||
@@ -2022,8 +2083,7 @@ MapDefinitionTrial::MapDefinitionTrial(const MapDefinition& map)
|
||||
camera_zone_maps(map.camera_zone_maps),
|
||||
camera_zone_specs(map.camera_zone_specs),
|
||||
overview_specs(map.overview_specs),
|
||||
modification_tiles(map.modification_tiles),
|
||||
unknown_a5(map.unknown_a5),
|
||||
overlay_state(map.overlay_state),
|
||||
default_rules(map.default_rules),
|
||||
name(map.name),
|
||||
location_name(map.location_name),
|
||||
@@ -2077,9 +2137,7 @@ MapDefinitionTrial::operator MapDefinition() const {
|
||||
ret.camera_zone_maps = this->camera_zone_maps;
|
||||
ret.camera_zone_specs = this->camera_zone_specs;
|
||||
ret.overview_specs = this->overview_specs;
|
||||
ret.modification_tiles = this->modification_tiles;
|
||||
ret.unknown_a5.clear(0xFF);
|
||||
ret.unknown_a5 = this->unknown_a5;
|
||||
ret.overlay_state = this->overlay_state;
|
||||
ret.default_rules = this->default_rules;
|
||||
ret.name = this->name;
|
||||
ret.location_name = this->location_name;
|
||||
@@ -2548,12 +2606,15 @@ MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t lang
|
||||
: language(language),
|
||||
compressed_data(std::move(compressed_data)) {
|
||||
string decompressed = prs_decompress(this->compressed_data);
|
||||
if (decompressed.size() != sizeof(MapDefinition)) {
|
||||
if (decompressed.size() == sizeof(MapDefinitionTrial)) {
|
||||
this->map = make_shared<MapDefinition>(*reinterpret_cast<const MapDefinitionTrial*>(decompressed.data()));
|
||||
} else if (decompressed.size() == sizeof(MapDefinition)) {
|
||||
this->map = make_shared<MapDefinition>(*reinterpret_cast<const MapDefinition*>(decompressed.data()));
|
||||
} else {
|
||||
throw runtime_error(phosg::string_printf(
|
||||
"decompressed data size is incorrect (expected %zu bytes, read %zu bytes)",
|
||||
sizeof(MapDefinition), decompressed.size()));
|
||||
}
|
||||
this->map = make_shared<MapDefinition>(*reinterpret_cast<const MapDefinition*>(decompressed.data()));
|
||||
}
|
||||
|
||||
shared_ptr<const MapDefinitionTrial> MapIndex::VersionedMap::trial() const {
|
||||
@@ -2737,7 +2798,7 @@ const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language
|
||||
e.width = vm->map->width;
|
||||
e.height = vm->map->height;
|
||||
e.map_tiles = vm->map->map_tiles;
|
||||
e.modification_tiles = vm->map->modification_tiles;
|
||||
e.modification_tiles = vm->map->overlay_state.tiles;
|
||||
|
||||
e.name_offset = strings_w.size();
|
||||
strings_w.write(vm->map->name.data, vm->map->name.used_chars_8());
|
||||
|
||||
+63
-32
@@ -523,7 +523,6 @@ struct CardDefinition {
|
||||
// rules; for example, the expression "4+4//2" results in 4, not 6.
|
||||
/* 02 */ pstring<TextEncoding::ASCII, 0x0F> expr;
|
||||
// when specifies in which phase the effect should activate.
|
||||
// TODO: There are many values that can be used here; document them.
|
||||
/* 11 */ EffectWhen when;
|
||||
// arg1 generally specifies how long the effect activates for.
|
||||
/* 12 */ pstring<TextEncoding::ASCII, 4> arg1;
|
||||
@@ -918,13 +917,15 @@ struct PlayerConfig {
|
||||
/* 00 */ be_uint32_t guild_card_number;
|
||||
/* 04 */ pstring<TextEncoding::MARKED, 0x18> name;
|
||||
} __packed_ws__(PlayerReference, 0x1C);
|
||||
// This array is updated when a battle is started (via a 6xB4x05 command). The
|
||||
// client adds the opposing players' info to ths first two entries here if the
|
||||
// opponents are human. (The existing entries are always moved back by two
|
||||
// slots, but if one or both opponents are not humans, one or both of the
|
||||
// newly-vacated slots is not filled in.)
|
||||
// These two arrays are updated when a battle is started (via a 6xB4x05
|
||||
// command). The client adds the opposing players' info to ths first two
|
||||
// entries in recent_human_opponents if the opponents are human. (The
|
||||
// existing entries are always moved back by two slots, but if one or both
|
||||
// opponents are not humans, one or both of the newly-vacated slots is not
|
||||
// filled in.) Both arrays have the most recent entries at the beginning.
|
||||
/* 2128:1FD4 */ parray<PlayerReference, 10> recent_human_opponents;
|
||||
/* 2240:20EC */ parray<uint8_t, 0x28> unknown_a10;
|
||||
/* 2240:20EC */ parray<be_uint32_t, 5> recent_battle_start_timestamps;
|
||||
/* 2254:2100 */ parray<uint8_t, 0x14> unknown_a10;
|
||||
/* 2268:2114 */ be_uint32_t init_timestamp;
|
||||
/* 226C:2118 */ be_uint32_t last_online_battle_start_timestamp;
|
||||
// In a certain situation, unknown_t3 is set to init_timestamp plus a multiple
|
||||
@@ -961,7 +962,8 @@ struct PlayerConfigNTE {
|
||||
/* 1B70 */ be_uint32_t offline_clv_exp;
|
||||
/* 1B74 */ be_uint32_t online_clv_exp;
|
||||
/* 1B78 */ parray<PlayerConfig::PlayerReference, 10> recent_human_opponents;
|
||||
/* 1C90 */ parray<uint8_t, 0x28> unknown_a10;
|
||||
/* 1C90 */ parray<be_uint32_t, 5> recent_battle_start_timestamps;
|
||||
/* 1CA4 */ parray<uint8_t, 0x14> unknown_a10;
|
||||
/* 1CB8 */ be_uint32_t init_timestamp;
|
||||
/* 1CBC */ be_uint32_t last_online_battle_start_timestamp;
|
||||
/* 1CC0 */ be_uint32_t unknown_t3;
|
||||
@@ -1131,6 +1133,42 @@ struct CompressedMapHeader { // .mnm file format
|
||||
// Compressed data immediately follows (which decompresses to a MapDefinition)
|
||||
} __packed_ws__(CompressedMapHeader, 8);
|
||||
|
||||
struct OverlayState {
|
||||
// In the tiles array, the high 4 bits of each value are the tile type, and
|
||||
// the low 4 bits are the subtype. The types are:
|
||||
// 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-4F: traps on NTE
|
||||
// 40-44: traps on non-NTE (there may be up to 8 of each type, and one of
|
||||
// each is chosen to be a real trap at battle start); the trap types are:
|
||||
// 40: Dice Fever, Heavy Fog, Muscular, Immortality, Snail Pace
|
||||
// 41: Gold Rush, Charity, Requiem
|
||||
// 42: Powerless Rain, Trash 1, Empty Hand, Skip Draw
|
||||
// 43: Brave Wind, Homesick, Fly
|
||||
// 44: Dice+1, Battle Royale, Reverse Card, Giant Garden, Fix
|
||||
// 50: blocked by metal box (appears as an improperly-z-buffered teal cube in
|
||||
// preview; behaves like 10 and 20 in game)
|
||||
// Any other value here will behave like 00 (no special tile behavior).
|
||||
parray<parray<uint8_t, 0x10>, 0x10> tiles;
|
||||
|
||||
// This field appears to be unused in both NTE and the final version. Perhaps
|
||||
// it had some meaning in a pre-NTE version.
|
||||
parray<le_uint32_t, 5> unused1;
|
||||
|
||||
// TODO: Figure out exactly where these colors are used
|
||||
parray<le_uint32_t, 0x10> trap_tile_colors_nte; // Unused on non-NTE
|
||||
|
||||
// This specifies the assist card IDs that each trap value (40-4F) will set
|
||||
// when triggered. This only has an effect on NTE; on non-NTE, this is unused
|
||||
// and a fixed set of assist cards is used instead. (On newserv, the set of
|
||||
// used assist cards can be overridden in the server configuration.)
|
||||
parray<le_uint16_t, 0x10> trap_card_ids_nte;
|
||||
|
||||
OverlayState();
|
||||
void clear();
|
||||
} __packed_ws__(OverlayState, 0x174);
|
||||
|
||||
struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
// If tag is not 0x00000100, the game considers the map to be corrupt in
|
||||
// offline mode and will delete it (if it's a download quest). The tag field
|
||||
@@ -1250,24 +1288,10 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
// (it is not yet known what the major index represents).
|
||||
/* 1AB8 */ parray<parray<CameraSpec, 2>, 3> overview_specs;
|
||||
|
||||
// In the modification_tiles array, the values are:
|
||||
// 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-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:
|
||||
// 40: Dice Fever, Heavy Fog, Muscular, Immortality, Snail Pace
|
||||
// 41: Gold Rush, Charity, Requiem
|
||||
// 42: Powerless Rain, Trash 1, Empty Hand, Skip Draw
|
||||
// 43: Brave Wind, Homesick, Fly
|
||||
// 44: Dice+1, Battle Royale, Reverse Card, Giant Garden, Fix
|
||||
/* 1C68 */ parray<parray<uint8_t, 0x10>, 0x10> modification_tiles;
|
||||
// This specifies the locations of blocked tiles, teleporters, and traps. See
|
||||
// the comments in OverlayState for details.
|
||||
/* 1C68 */ OverlayState overlay_state;
|
||||
|
||||
/* 1D68 */ parray<uint8_t, 0x74> unknown_a5;
|
||||
/* 1DDC */ Rules default_rules;
|
||||
|
||||
/* 1DF0 */ pstring<TextEncoding::MARKED, 0x14> name;
|
||||
@@ -1362,7 +1386,13 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 28F0 */ parray<parray<DialogueSet, 0x10>, 3> dialogue_sets;
|
||||
|
||||
// These card IDs are always given to the player when they win a battle on
|
||||
// this map. Unused entries should be set to FFFF.
|
||||
// this map. Unused entries should be set to FFFF. Cards in this array are
|
||||
// ignored if they have any of these features (in the card definition):
|
||||
// - type is HUNTERS_SC or ARKZ_SC
|
||||
// - card_class is BOSS_ATTACK_ACTION or BOSS_TECH
|
||||
// - rank is D1, D2, or D3
|
||||
// - cannot_drop is 1 (specifically 1; other values don't prevent cards from
|
||||
// appearing)
|
||||
/* 59B0 */ parray<be_uint16_t, 0x10> reward_card_ids;
|
||||
|
||||
// These fields are used when determining which cards to drop after the battle
|
||||
@@ -1383,10 +1413,12 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 59DC */ uint8_t map_category;
|
||||
|
||||
// This field determines block graphics to be used in the Cyber environment.
|
||||
// There are 10 block types (0-9); if this value is > 9, type 0 is used.
|
||||
// There are 10 block types (0-9); if this value is > 9, type 0 is used. This
|
||||
// field has no effect in Ep3 NTE, even though there are 6 different block
|
||||
// texture files on the NTE disc.
|
||||
/* 59DD */ uint8_t cyber_block_type;
|
||||
|
||||
/* 59DE */ parray<uint8_t, 2> unknown_a11;
|
||||
/* 59DE */ be_uint16_t unknown_a11;
|
||||
|
||||
// This array specifies which SC characters can't participate in the quest
|
||||
// (that is, the player is not allowed to choose decks with these SC cards).
|
||||
@@ -1470,9 +1502,8 @@ struct MapDefinitionTrial {
|
||||
/* 0118 */ parray<parray<parray<parray<uint8_t, 0x10>, 0x10>, 10>, 2> camera_zone_maps;
|
||||
/* 1518 */ parray<parray<MapDefinition::CameraSpec, 10>, 2> camera_zone_specs;
|
||||
/* 1AB8 */ parray<parray<MapDefinition::CameraSpec, 2>, 3> overview_specs;
|
||||
/* 1C68 */ parray<parray<uint8_t, 0x10>, 0x10> modification_tiles;
|
||||
/* 1D68 */ parray<uint8_t, 0x74> unknown_a5;
|
||||
/* 1DD4 */ RulesTrial default_rules;
|
||||
/* 1C68 */ OverlayState overlay_state;
|
||||
/* 1DDC */ RulesTrial default_rules;
|
||||
/* 1DE8 */ pstring<TextEncoding::MARKED, 0x14> name;
|
||||
/* 1DFC */ pstring<TextEncoding::MARKED, 0x14> location_name;
|
||||
/* 1E10 */ pstring<TextEncoding::MARKED, 0x3C> quest_name;
|
||||
@@ -1494,7 +1525,7 @@ struct MapDefinitionTrial {
|
||||
/* 4172 */ be_int16_t field_offset_y;
|
||||
/* 4174 */ uint8_t map_category;
|
||||
/* 4175 */ uint8_t cyber_block_type;
|
||||
/* 4176 */ parray<uint8_t, 2> unknown_a11;
|
||||
/* 4176 */ be_uint16_t unknown_a11;
|
||||
// TODO: This field may contain some version of unavailable_sc_cards and/or
|
||||
// entry_states from MapDefinition, but the format isn't the same
|
||||
/* 4178 */ parray<uint8_t, 0x28> unknown_t12;
|
||||
|
||||
@@ -99,17 +99,4 @@ MapAndRulesStateTrial::operator MapAndRulesState() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
OverlayState::OverlayState() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void OverlayState::clear() {
|
||||
for (size_t y = 0; y < this->tiles.size(); y++) {
|
||||
this->tiles[y].clear(0);
|
||||
}
|
||||
this->unused1.clear(0);
|
||||
this->unused2.clear(0);
|
||||
this->trap_card_ids_nte.clear(0);
|
||||
}
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
@@ -67,14 +67,4 @@ struct MapAndRulesStateTrial {
|
||||
operator MapAndRulesState() const;
|
||||
} __packed_ws__(MapAndRulesStateTrial, 0x130);
|
||||
|
||||
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> trap_card_ids_nte; // Unused on non-NTE
|
||||
|
||||
OverlayState();
|
||||
void clear();
|
||||
} __packed_ws__(OverlayState, 0x174);
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
@@ -132,6 +132,11 @@ shared_ptr<const Server> PlayerState::server() const {
|
||||
return s;
|
||||
}
|
||||
|
||||
bool PlayerState::is_alive() const {
|
||||
auto sc_card = this->get_sc_card();
|
||||
return (sc_card && !(sc_card->card_flags & 2));
|
||||
}
|
||||
|
||||
bool PlayerState::draw_cards_allowed() const {
|
||||
if (this->assist_flags & AssistFlag::IS_SKIPPING_TURN) {
|
||||
return false;
|
||||
@@ -608,7 +613,7 @@ void PlayerState::discard_and_redraw_hand() {
|
||||
}
|
||||
|
||||
if (!s->options.is_nte()) {
|
||||
G_Unknown_Ep3_6xB4x2C cmd;
|
||||
G_EnqueueAnimation_Ep3_6xB4x2C cmd;
|
||||
cmd.change_type = 3;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
@@ -712,7 +717,7 @@ bool PlayerState::do_mulligan() {
|
||||
}
|
||||
|
||||
if (!s->options.is_nte()) {
|
||||
G_Unknown_Ep3_6xB4x2C cmd;
|
||||
G_EnqueueAnimation_Ep3_6xB4x2C cmd;
|
||||
cmd.change_type = 3;
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
@@ -1415,7 +1420,7 @@ bool PlayerState::set_card_from_hand(
|
||||
s->send_6xB4x05();
|
||||
|
||||
if (!is_nte) {
|
||||
G_AddToSetCardlog_Ep3_6xB4x4A cmd;
|
||||
G_AddToSetCardLog_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.card_refs[0] = card_ref;
|
||||
cmd.client_id = this->client_id;
|
||||
@@ -1782,7 +1787,7 @@ bool PlayerState::set_action_cards_for_action_state(const ActionState& pa) {
|
||||
card->loc.direction = pa.facing_direction;
|
||||
log.debug("set facing direction to %s", phosg::name_for_enum(card->loc.direction));
|
||||
|
||||
G_AddToSetCardlog_Ep3_6xB4x4A cmd;
|
||||
G_AddToSetCardLog_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.round_num = s->get_round_num();
|
||||
@@ -1842,7 +1847,7 @@ bool PlayerState::set_action_cards_for_action_state(const ActionState& pa) {
|
||||
}
|
||||
}
|
||||
if (!is_nte) {
|
||||
G_AddToSetCardlog_Ep3_6xB4x4A cmd;
|
||||
G_AddToSetCardLog_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.round_num = s->get_round_num();
|
||||
|
||||
@@ -47,6 +47,8 @@ public:
|
||||
std::shared_ptr<Server> server();
|
||||
std::shared_ptr<const Server> server() const;
|
||||
|
||||
bool is_alive() const;
|
||||
|
||||
bool draw_cards_allowed() const;
|
||||
void apply_assist_card_effect_on_set(std::shared_ptr<PlayerState> setter_ps);
|
||||
void apply_dice_effects();
|
||||
|
||||
@@ -194,7 +194,7 @@ std::string ActionState::str(shared_ptr<const Server> s) const {
|
||||
string target_refs_s = s->debug_str_for_card_refs(this->target_card_refs);
|
||||
string action_refs_s = s->debug_str_for_card_refs(this->action_card_refs);
|
||||
return phosg::string_printf(
|
||||
"ActionState[client=%hu, u=%hhu, facing=%s, attacker_ref=%s, "
|
||||
"ActionState[client=%hX, u=%hhu, facing=%s, attacker_ref=%s, "
|
||||
"def_ref=%s, target_refs=%s, action_refs=%s, "
|
||||
"orig_attacker_ref=%s]",
|
||||
this->client_id.load(),
|
||||
|
||||
@@ -2076,7 +2076,8 @@ uint32_t RulerServer::get_card_id_with_effective_range(
|
||||
if (this->card_ref_or_sc_has_fixed_range(card_ref)) {
|
||||
// Undo the override that may have been passed in
|
||||
log.debug("@%04hX has FIXED_RANGE", card_ref);
|
||||
auto orig_ce = this->definition_for_card_id(this->card_id_for_card_ref(card_ref));
|
||||
card_id = this->card_id_for_card_ref(card_ref);
|
||||
auto orig_ce = this->definition_for_card_id(card_id);
|
||||
if (orig_ce && (static_cast<uint8_t>(effective_target_mode) < 6)) {
|
||||
log.debug("ce valid for #%04hX with effective target mode %s; overriding to %s", card_id, name_for_target_mode(effective_target_mode), name_for_target_mode(orig_ce->def.target_mode));
|
||||
effective_target_mode = orig_ce->def.target_mode;
|
||||
|
||||
+28
-65
@@ -12,7 +12,8 @@ using namespace std;
|
||||
namespace Episode3 {
|
||||
|
||||
// This is (obviously) not the original string. The original string is:
|
||||
// "[V1][FINAL2.0] 03/09/13 15:30 by K.Toya"
|
||||
// "03/05/29 18:00 by K.Toya" (NTE)
|
||||
// "[V1][FINAL2.0] 03/09/13 15:30 by K.Toya" (Final)
|
||||
static const char* VERSION_SIGNATURE =
|
||||
"newserv Ep3 based on [V1][FINAL2.0] 03/09/13 15:30 by K.Toya";
|
||||
static const char* VERSION_SIGNATURE_NTE =
|
||||
@@ -375,8 +376,7 @@ void Server::send_debug_command_received_message(uint8_t subsubcommand, const ch
|
||||
this->send_debug_message_printf("$C5*/CAx%02hhX %s", subsubcommand, description);
|
||||
}
|
||||
|
||||
void Server::send_debug_message_if_error_code_nonzero(
|
||||
uint8_t client_id, int32_t error_code) const {
|
||||
void Server::send_debug_message_if_error_code_nonzero(uint8_t client_id, int32_t error_code) const {
|
||||
if (error_code < 0) {
|
||||
this->send_debug_message_printf("$C4%hhu/ERROR -0x%zX", client_id, static_cast<ssize_t>(-error_code));
|
||||
} else if (error_code > 0) {
|
||||
@@ -392,8 +392,7 @@ void Server::add_team_exp(uint8_t team_id, int32_t exp) {
|
||||
}
|
||||
}
|
||||
|
||||
this->team_exp[team_id] = clamp<int16_t>(
|
||||
this->team_exp[team_id] + exp, 0, this->team_client_count[team_id] * 96);
|
||||
this->team_exp[team_id] = clamp<int16_t>(this->team_exp[team_id] + exp, 0, this->team_client_count[team_id] * 96);
|
||||
|
||||
uint8_t dice_boost = this->team_exp[team_id] / (this->team_client_count[team_id] * 12);
|
||||
this->card_special->adjust_dice_boost_if_team_has_condition_52(team_id, &dice_boost, 0);
|
||||
@@ -475,29 +474,7 @@ shared_ptr<Card> Server::card_for_set_card_ref(uint16_t card_ref) {
|
||||
}
|
||||
|
||||
shared_ptr<const Card> Server::card_for_set_card_ref(uint16_t card_ref) const {
|
||||
// TODO: It'd be nice to deduplicate this function with the non-const version.
|
||||
if (card_ref == 0xFFFF) {
|
||||
return nullptr;
|
||||
}
|
||||
uint8_t client_id = client_id_for_card_ref(card_ref);
|
||||
if (client_id == 0xFF) {
|
||||
return nullptr;
|
||||
}
|
||||
auto ps = this->player_states.at(client_id);
|
||||
if (!ps) {
|
||||
return nullptr;
|
||||
}
|
||||
auto card = ps->get_sc_card();
|
||||
if (card && (card->get_card_ref() == card_ref)) {
|
||||
return card;
|
||||
}
|
||||
for (size_t set_index = 0; set_index < 8; set_index++) {
|
||||
card = ps->get_set_card(set_index);
|
||||
if (card && (card->get_card_ref() == card_ref)) {
|
||||
return card;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
return const_cast<Server*>(this)->card_for_set_card_ref(card_ref);
|
||||
}
|
||||
|
||||
uint16_t Server::card_id_for_card_ref(uint16_t card_ref) const {
|
||||
@@ -1180,7 +1157,7 @@ void Server::move_phase_after() {
|
||||
(abs(sc_card->loc.x - trap_x) < 2) &&
|
||||
(abs(sc_card->loc.y - trap_y) < 2) &&
|
||||
ps->replace_assist_card_by_id(trap_card_id)) {
|
||||
G_Unknown_Ep3_6xB4x2C cmd;
|
||||
G_EnqueueAnimation_Ep3_6xB4x2C cmd;
|
||||
cmd.change_type = 0x01;
|
||||
cmd.client_id = client_id;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
@@ -1860,8 +1837,7 @@ void Server::on_server_data_input(shared_ptr<Client> sender_c, const string& dat
|
||||
|
||||
void Server::handle_CAx0B_mulligan_hand(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_RedrawInitialHand_Ep3_CAx0B>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "REDRAW");
|
||||
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "REDRAW");
|
||||
if (in_cmd.client_id >= 4) {
|
||||
throw runtime_error("invalid client ID");
|
||||
}
|
||||
@@ -1893,8 +1869,7 @@ void Server::handle_CAx0B_mulligan_hand(shared_ptr<Client>, const string& data)
|
||||
|
||||
void Server::handle_CAx0C_end_mulligan_phase(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndInitialRedrawPhase_Ep3_CAx0C>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "SETUP ADV 2");
|
||||
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "SETUP ADV 2");
|
||||
if (in_cmd.client_id >= 4) {
|
||||
throw runtime_error("invalid client ID");
|
||||
}
|
||||
@@ -1975,8 +1950,7 @@ void Server::handle_CAx0D_end_non_action_phase(shared_ptr<Client>, const string&
|
||||
|
||||
void Server::handle_CAx0E_discard_card_from_hand(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_DiscardCardFromHand_Ep3_CAx0E>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "DISCARD");
|
||||
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "DISCARD");
|
||||
if (in_cmd.client_id >= 4) {
|
||||
throw runtime_error("invalid client ID");
|
||||
}
|
||||
@@ -2061,8 +2035,7 @@ void Server::handle_CAx0F_set_card_from_hand(shared_ptr<Client>, const string& d
|
||||
|
||||
void Server::handle_CAx10_move_fc_to_location(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_MoveFieldCharacter_Ep3_CAx10>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "MOVE");
|
||||
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "MOVE");
|
||||
if (in_cmd.client_id >= 4) {
|
||||
throw runtime_error("invalid client ID");
|
||||
}
|
||||
@@ -2102,8 +2075,7 @@ void Server::handle_CAx10_move_fc_to_location(shared_ptr<Client>, const string&
|
||||
|
||||
void Server::handle_CAx11_enqueue_attack_or_defense(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EnqueueAttackOrDefense_Ep3_CAx11>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "ENQUEUE ACT");
|
||||
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "ENQUEUE ACT");
|
||||
if (in_cmd.client_id >= 4) {
|
||||
throw runtime_error("invalid client ID");
|
||||
}
|
||||
@@ -2141,8 +2113,7 @@ void Server::handle_CAx11_enqueue_attack_or_defense(shared_ptr<Client>, const st
|
||||
|
||||
void Server::handle_CAx12_end_attack_list(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndAttackList_Ep3_CAx12>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "END ATK LIST");
|
||||
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "END ATK LIST");
|
||||
if (in_cmd.client_id >= 4) {
|
||||
throw runtime_error("invalid client ID");
|
||||
}
|
||||
@@ -2237,8 +2208,7 @@ void Server::handle_CAx13_update_map_during_setup(shared_ptr<Client> c, const st
|
||||
|
||||
void Server::handle_CAx14_update_deck_during_setup(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_SetPlayerDeck_Ep3_CAx14>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "UPDATE DECK");
|
||||
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "UPDATE DECK");
|
||||
|
||||
if (!this->battle_in_progress) {
|
||||
if ((this->setup_phase == SetupPhase::REGISTRATION) &&
|
||||
@@ -2284,8 +2254,7 @@ void Server::handle_CAx14_update_deck_during_setup(shared_ptr<Client>, const str
|
||||
|
||||
void Server::handle_CAx15_unused_hard_reset_server_state(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_HardResetServerState_Ep3_CAx15>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.subsubcommand, "HARD RESET");
|
||||
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "HARD RESET");
|
||||
|
||||
// In the original implementation, this command recreates the server object.
|
||||
// This is possible because the dispatch function is not part of the server
|
||||
@@ -2302,8 +2271,7 @@ void Server::handle_CAx15_unused_hard_reset_server_state(shared_ptr<Client>, con
|
||||
|
||||
void Server::handle_CAx1B_update_player_name(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_SetPlayerName_Ep3_CAx1B>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.entry.client_id, in_cmd.header.subsubcommand, "UPDATE NAME");
|
||||
this->send_debug_command_received_message(in_cmd.entry.client_id, in_cmd.header.subsubcommand, "UPDATE NAME");
|
||||
|
||||
if (in_cmd.entry.client_id < 4) {
|
||||
if (!this->is_registration_complete()) {
|
||||
@@ -2334,8 +2302,7 @@ void Server::handle_CAx1B_update_player_name(shared_ptr<Client>, const string& d
|
||||
|
||||
void Server::handle_CAx1D_start_battle(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_StartBattle_Ep3_CAx1D>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.subsubcommand, "START BATTLE");
|
||||
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "START BATTLE");
|
||||
|
||||
if (!this->battle_in_progress) {
|
||||
bool is_nte = this->options.is_nte();
|
||||
@@ -2381,8 +2348,7 @@ void Server::handle_CAx1D_start_battle(shared_ptr<Client>, const string& data) {
|
||||
|
||||
void Server::handle_CAx21_end_battle(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndBattle_Ep3_CAx21>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.subsubcommand, "END BATTLE");
|
||||
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "END BATTLE");
|
||||
if (this->setup_phase == SetupPhase::BATTLE_ENDED) {
|
||||
this->battle_finished = true;
|
||||
|
||||
@@ -2396,8 +2362,7 @@ void Server::handle_CAx21_end_battle(shared_ptr<Client>, const string& data) {
|
||||
|
||||
void Server::handle_CAx28_end_defense_list(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndDefenseList_Ep3_CAx28>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "END DEF LIST");
|
||||
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "END DEF LIST");
|
||||
if (in_cmd.client_id >= 4) {
|
||||
throw runtime_error("invalid client ID");
|
||||
}
|
||||
@@ -2457,8 +2422,7 @@ void Server::handle_CAx34_subtract_ally_atk_points(shared_ptr<Client>, const str
|
||||
const auto& in_cmd = check_size_t<G_PhotonBlastRequest_Ep3_CAx34>(data);
|
||||
|
||||
uint8_t card_ref_client_id = client_id_for_card_ref(in_cmd.card_ref);
|
||||
this->send_debug_command_received_message(
|
||||
card_ref_client_id, in_cmd.header.subsubcommand, "SUB ALLY ATK");
|
||||
this->send_debug_command_received_message(card_ref_client_id, in_cmd.header.subsubcommand, "SUB ALLY ATK");
|
||||
|
||||
if (card_ref_client_id >= 4) {
|
||||
return;
|
||||
@@ -2531,8 +2495,7 @@ void Server::handle_CAx34_subtract_ally_atk_points(shared_ptr<Client>, const str
|
||||
|
||||
void Server::handle_CAx37_client_ready_to_advance_from_starter_roll_phase(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_AdvanceFromStartingRollsPhase_Ep3_CAx37>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "SETUP ADV 1");
|
||||
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "SETUP ADV 1");
|
||||
if (in_cmd.client_id >= 4) {
|
||||
throw runtime_error("invalid client ID");
|
||||
}
|
||||
@@ -2644,14 +2607,15 @@ void Server::send_6xB6x41_to_all_clients() const {
|
||||
void Server::handle_CAx41_map_request(shared_ptr<Client>, const string& data) {
|
||||
const auto& cmd = check_size_t<G_MapDataRequest_Ep3_CAx41>(data);
|
||||
this->send_debug_command_received_message(cmd.header.subsubcommand, "MAP DATA");
|
||||
this->last_chosen_map = this->options.map_index->for_number(cmd.map_number);
|
||||
this->send_6xB6x41_to_all_clients();
|
||||
if (!this->options.tournament || (this->options.tournament->get_map()->map_number == cmd.map_number)) {
|
||||
this->last_chosen_map = this->options.map_index->for_number(cmd.map_number);
|
||||
this->send_6xB6x41_to_all_clients();
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_CAx48_end_turn(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_EndTurn_Ep3_CAx48>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.client_id, in_cmd.header.subsubcommand, "END TURN");
|
||||
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "END TURN");
|
||||
if (in_cmd.client_id >= 4) {
|
||||
throw runtime_error("invalid client ID");
|
||||
}
|
||||
@@ -2668,8 +2632,7 @@ void Server::handle_CAx48_end_turn(shared_ptr<Client>, const string& data) {
|
||||
|
||||
void Server::handle_CAx49_card_counts(shared_ptr<Client>, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_CardCounts_Ep3_CAx49>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.sender_client_id, in_cmd.header.subsubcommand, "CARD COUNTS");
|
||||
this->send_debug_command_received_message(in_cmd.header.sender_client_id, in_cmd.header.subsubcommand, "CARD COUNTS");
|
||||
|
||||
// Note: Sega's implmentation completely ignores this command. This
|
||||
// implementation is not based on the original code.
|
||||
@@ -2858,8 +2821,8 @@ void Server::unknown_8023EEF4() {
|
||||
log.debug("a14 (%" PRIu32 ") < num_pending_attacks_with_cards (%" PRIu32 ")", this->unknown_a14, this->num_pending_attacks_with_cards);
|
||||
this->defense_list_ended_for_client.clear(false);
|
||||
|
||||
G_SetActionState_Ep3_6xB4x29 cmd;
|
||||
cmd.unknown_a1 = this->unknown_a14;
|
||||
G_UpdateAttackTargets_Ep3_6xB4x29 cmd;
|
||||
cmd.attack_number = this->unknown_a14;
|
||||
cmd.state = this->pending_attacks_with_cards[this->unknown_a14];
|
||||
if (is_nte) {
|
||||
this->replace_targets_due_to_destruction_nte(&cmd.state);
|
||||
|
||||
+4
-2
@@ -212,7 +212,9 @@ ItemCreator::DropResult ItemCreator::on_box_item_drop_with_area_norm(uint8_t are
|
||||
}
|
||||
|
||||
ItemCreator::DropResult ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm) {
|
||||
if (enemy_type > 0x58) {
|
||||
// Note: The original GC implementation uses (enemy_type > 0x58) here; we
|
||||
// extend it to the full array size for BB
|
||||
if (enemy_type >= 0x64) {
|
||||
this->log.warning("Invalid enemy type: %" PRIX32, enemy_type);
|
||||
return DropResult();
|
||||
}
|
||||
@@ -556,7 +558,7 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const {
|
||||
break;
|
||||
case BattleRules::WeaponAndArmorMode::FORBID_RARES:
|
||||
if (this->item_parameter_table->is_item_rare(item)) {
|
||||
this->log.info("Restricted: rare items not allowed");
|
||||
this->log.info("Restricted: rare weapons and armors not allowed");
|
||||
item.clear();
|
||||
}
|
||||
break;
|
||||
|
||||
+10
-6
@@ -142,18 +142,22 @@ bool ItemData::is_wrapped(const StackLimits& limits) const {
|
||||
}
|
||||
}
|
||||
|
||||
void ItemData::wrap(const StackLimits& limits) {
|
||||
void ItemData::wrap(const StackLimits& limits, uint8_t present_color) {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
this->data1[4] |= 0x40;
|
||||
this->data1[5] = (this->data1[5] & 0xF0) | (present_color & 0x0F);
|
||||
break;
|
||||
case 1:
|
||||
this->data1[4] = (this->data1[4] & 0xF0) | 0x40 | (present_color & 0x0F);
|
||||
break;
|
||||
case 2:
|
||||
// Mags cannot have custom present colors
|
||||
this->data2[2] |= 0x40;
|
||||
break;
|
||||
case 3:
|
||||
if (!this->is_stackable(limits)) {
|
||||
this->data1[3] |= 0x40;
|
||||
this->data1[3] = (this->data1[3] & 0xF0) | 0x40 | (present_color & 0x0F);
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
@@ -167,14 +171,14 @@ void ItemData::unwrap(const StackLimits& limits) {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
this->data1[4] &= 0xBF;
|
||||
this->data1[4] &= 0xB0;
|
||||
break;
|
||||
case 2:
|
||||
this->data2[2] &= 0xBF;
|
||||
this->data2[2] &= 0xB0;
|
||||
break;
|
||||
case 3:
|
||||
if (!this->is_stackable(limits)) {
|
||||
this->data1[3] &= 0xBF;
|
||||
this->data1[3] &= 0xB0;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
|
||||
+6
-4
@@ -77,19 +77,19 @@ struct ItemData {
|
||||
|
||||
// QUICK ITEM FORMAT REFERENCE
|
||||
// data1/0 data1/4 data1/8 data2
|
||||
// Weapon: 00ZZZZGG SS00AABB AABBAABB 00000000
|
||||
// Weapon: 00ZZZZGG SSNNAABB AABBAABB 00000000
|
||||
// Armor: 0101ZZ00 FFTTDDDD EEEE0000 00000000
|
||||
// Shield: 0102ZZ00 FFTTDDDD EEEE0000 00000000
|
||||
// Unit: 0103ZZ00 FF00RRRR 00000000 00000000
|
||||
// Mag: 02ZZLLWW HHHHIIII JJJJKKKK YYQQPPVV
|
||||
// Tool: 03ZZZZFF 00CC0000 00000000 00000000
|
||||
// Tool: 03ZZZZUU 00CC0000 00000000 00000000
|
||||
// Meseta: 04000000 00000000 00000000 MMMMMMMM
|
||||
// A = attribute type (for S-ranks, custom name)
|
||||
// B = attribute amount (for S-ranks, custom name)
|
||||
// C = stack size (for tools)
|
||||
// D = DEF bonus
|
||||
// E = EVP bonus
|
||||
// F = flags (40=present; for tools, unused if item is stackable)
|
||||
// F = armor/shield/unit flags (40=present; low 16 bits are present color)
|
||||
// G = weapon grind
|
||||
// H = mag DEF
|
||||
// I = mag POW
|
||||
@@ -97,11 +97,13 @@ struct ItemData {
|
||||
// K = mag MIND
|
||||
// L = mag level
|
||||
// M = meseta amount
|
||||
// N = present color (weapon only; for other types this is in the flags field)
|
||||
// P = mag flags (40=present, 04=has left pb, 02=has right pb, 01=has center pb)
|
||||
// Q = mag IQ
|
||||
// R = unit modifier (little-endian)
|
||||
// S = weapon flags (80=unidentified, 40=present) and special (low 6 bits)
|
||||
// T = slot count
|
||||
// U = tool flags (40=present; unused if item is stackable)
|
||||
// V = mag color
|
||||
// W = photon blasts
|
||||
// Y = mag synchro
|
||||
@@ -150,7 +152,7 @@ struct ItemData {
|
||||
uint32_t primary_identifier() const;
|
||||
|
||||
bool is_wrapped(const StackLimits& limits) const;
|
||||
void wrap(const StackLimits& limits);
|
||||
void wrap(const StackLimits& limits, uint8_t present_color);
|
||||
void unwrap(const StackLimits& limits);
|
||||
|
||||
bool is_stackable(const StackLimits& limits) const;
|
||||
|
||||
@@ -135,7 +135,7 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
|
||||
// Armors, shields, and units (0x01) can be wrapped, as can mags (0x02) and
|
||||
// non-stackable tools (0x03). However, each of these item classes has its
|
||||
// flags in a different location.
|
||||
if (((item.data1[1] == 0x01) && (item.data1[4] & 0x40)) ||
|
||||
if (((item.data1[0] == 0x01) && (item.data1[4] & 0x40)) ||
|
||||
((item.data1[0] == 0x02) && (item.data2[2] & 0x40)) ||
|
||||
((item.data1[0] == 0x03) && !item.is_stackable(*this->limits) && (item.data1[3] & 0x40))) {
|
||||
ret_tokens.emplace_back("Wrapped");
|
||||
|
||||
@@ -40,8 +40,8 @@ public:
|
||||
} __packed__;
|
||||
template <bool BE>
|
||||
struct ItemBaseV3T : ItemBaseV2T<BE> {
|
||||
/* 04 */ U16T<BE> type = 0;
|
||||
/* 06 */ U16T<BE> skin = 0;
|
||||
/* 04 */ U16T<BE> type = 0; // "Model" in Soly's ItemPMT editor
|
||||
/* 06 */ U16T<BE> skin = 0; // "Texture" in Soly's ItemPMT editor
|
||||
/* 08 */
|
||||
} __packed__;
|
||||
template <bool BE>
|
||||
|
||||
+42
-18
@@ -36,22 +36,22 @@ 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]
|
||||
le_uint32_t level_deltas; // (5468) -> u32[9] -> LevelStatsDelta[200]
|
||||
le_uint32_t unknown_a1; // (548C) -> float[6]
|
||||
le_uint32_t max_stats; // (54A4) -> PlayerStats[9]
|
||||
le_uint32_t level_100_stats; // (55E8) -> PlayerStats[9]
|
||||
le_uint32_t base_stats; // (57AC) -> u32[9] -> CharacterStats
|
||||
le_uint32_t unknown_a2; // (57D0) -> (0x120 zero bytes)
|
||||
le_uint32_t attack_data; // (58F0) -> AttackData[9]
|
||||
le_uint32_t unknown_a4; // (5AA0) -> parray<parray<float, 5>, 9>
|
||||
le_uint32_t unknown_a5; // (5B54) -> float[9]
|
||||
le_uint32_t unknown_a6; // (5B78) -> (0x30 bytes)
|
||||
le_uint32_t unknown_a7; // (5BA8) -> (0x2D bytes)
|
||||
le_uint32_t unknown_a8; // (5E00) -> u32[3] -> float[0x2D]
|
||||
le_uint32_t unknown_a9; // (5DF4) -> (0x90 bytes)
|
||||
le_uint32_t unknown_a10; // (60D0) -> u32[3] -> (0x10-byte struct)[0x0C]
|
||||
le_uint32_t unknown_a11; // (616C) -> u32[3] -> (0x30-bytes)
|
||||
le_uint32_t unknown_a12; // (64FC) -> u32[3] -> (0x14-byte struct)[0x0F]
|
||||
} __packed_ws__(Offsets, 0x40);
|
||||
|
||||
phosg::StringReader r;
|
||||
@@ -72,7 +72,7 @@ LevelTableV2::LevelTableV2(const string& data, bool compressed) {
|
||||
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->level_100_stats[char_class] = r.pget<PlayerStats>(offsets.level_100_stats + char_class * sizeof(PlayerStats));
|
||||
this->base_stats[char_class] = r.pget<CharacterStats>(base_stats_offsets[char_class]);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ const CharacterStats& LevelTableV2::base_stats_for_class(uint8_t char_class) con
|
||||
return this->base_stats.at(char_class);
|
||||
}
|
||||
|
||||
const LevelTableV2::Level100Entry& LevelTableV2::level_100_stats_for_class(uint8_t char_class) const {
|
||||
const PlayerStats& LevelTableV2::level_100_stats_for_class(uint8_t char_class) const {
|
||||
return this->level_100_stats.at(char_class);
|
||||
}
|
||||
|
||||
@@ -150,6 +150,26 @@ const CharacterStats& LevelTableV3BE::base_stats_for_class(uint8_t char_class) c
|
||||
return data.at(char_class);
|
||||
}
|
||||
|
||||
static const array<PlayerStats, 12> max_stats_v3_v4 = {
|
||||
// ATP MST EVP HP DFP ATA LCK ESP PRX PRY L E M
|
||||
PlayerStats{{0x056B, 0x02DC, 0x02F4, 0x0265, 0x0243, 0x054B, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x04CB, 0x0499, 0x032B, 0x0254, 0x024D, 0x056C, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x065D, 0x0000, 0x0294, 0x0379, 0x0259, 0x0514, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x04E7, 0x0299, 0x02CB, 0x02D5, 0x0203, 0x06CB, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x0541, 0x0000, 0x02BB, 0x0430, 0x025E, 0x05FA, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x0492, 0x0000, 0x0313, 0x0439, 0x02B0, 0x062C, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x0365, 0x0504, 0x024C, 0x0302, 0x01F2, 0x0440, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x032B, 0x05DC, 0x02A7, 0x02E3, 0x01CF, 0x04B0, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x0244, 0x06D6, 0x0373, 0x02BE, 0x0186, 0x04EC, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x050B, 0x0000, 0x036D, 0x02EE, 0x020D, 0x05DC, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x03E7, 0x053C, 0x028B, 0x02CC, 0x01D6, 0x03F2, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
PlayerStats{{0x0474, 0x0407, 0x0384, 0x02CF, 0x0241, 0x06C2, 0x0064}, 0x0064, 0.0f, 0.0f, 0, 0, 0},
|
||||
};
|
||||
|
||||
const PlayerStats& LevelTableV3BE::max_stats_for_class(uint8_t char_class) const {
|
||||
return max_stats_v3_v4.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);
|
||||
}
|
||||
@@ -185,6 +205,10 @@ const CharacterStats& LevelTableV4::base_stats_for_class(uint8_t char_class) con
|
||||
return this->base_stats.at(char_class);
|
||||
}
|
||||
|
||||
const PlayerStats& LevelTableV4::max_stats_for_class(uint8_t char_class) const {
|
||||
return max_stats_v3_v4.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);
|
||||
}
|
||||
|
||||
+6
-12
@@ -104,6 +104,7 @@ class LevelTable {
|
||||
public:
|
||||
virtual ~LevelTable() = default;
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const = 0;
|
||||
virtual const PlayerStats& max_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 reset_to_base(PlayerStats& stats, uint8_t char_class) const;
|
||||
@@ -115,26 +116,17 @@ protected:
|
||||
|
||||
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 */
|
||||
} __packed_ws__(Level100Entry, 0x1C);
|
||||
|
||||
LevelTableV2(const std::string& data, bool compressed);
|
||||
virtual ~LevelTableV2() = default;
|
||||
|
||||
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;
|
||||
const PlayerStats& level_100_stats_for_class(uint8_t char_class) const;
|
||||
virtual 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:
|
||||
std::array<CharacterStats, 9> base_stats;
|
||||
std::array<Level100Entry, 9> level_100_stats;
|
||||
std::array<PlayerStats, 9> level_100_stats;
|
||||
std::array<PlayerStats, 9> max_stats;
|
||||
std::array<std::array<LevelStatsDelta, 200>, 9> level_deltas;
|
||||
};
|
||||
@@ -145,6 +137,7 @@ public:
|
||||
virtual ~LevelTableV3BE() = default;
|
||||
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
|
||||
virtual 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:
|
||||
@@ -157,6 +150,7 @@ public:
|
||||
virtual ~LevelTableV4() = default;
|
||||
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
|
||||
virtual 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:
|
||||
|
||||
+2
-8
@@ -302,13 +302,7 @@ shared_ptr<Map> Lobby::load_maps(
|
||||
shared_ptr<PSOLFGEncryption> opt_rand_crypt,
|
||||
shared_ptr<const string> quest_dat_contents_decompressed) {
|
||||
auto map = make_shared<Map>(version, lobby_id, random_seed, opt_rand_crypt);
|
||||
map->add_entities_from_quest_data(
|
||||
episode,
|
||||
difficulty,
|
||||
event,
|
||||
quest_dat_contents_decompressed->data(),
|
||||
quest_dat_contents_decompressed->size(),
|
||||
rare_rates);
|
||||
map->add_entities_from_quest_data(episode, difficulty, event, quest_dat_contents_decompressed, rare_rates);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -396,7 +390,7 @@ shared_ptr<Map> Lobby::load_maps(
|
||||
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());
|
||||
map->add_objects_from_map_data(floor, map_data);
|
||||
if (log) {
|
||||
log->info("Loaded objects map %s for floor %02zX", floor_object_filename.c_str(), floor);
|
||||
}
|
||||
|
||||
+748
-206
File diff suppressed because it is too large
Load Diff
+84
-38
@@ -519,29 +519,29 @@ const char* Map::name_for_object_type(uint16_t type) {
|
||||
case 0x02BD:
|
||||
return "TObjLaboMapWarp";
|
||||
case 0x0300:
|
||||
return "__UNKNOWN_0300__";
|
||||
return "__EP4_LIGHT__";
|
||||
case 0x0301:
|
||||
return "__UNKNOWN_0301__";
|
||||
return "__WILDS_CRATER_CACTUS__";
|
||||
case 0x0302:
|
||||
return "__UNKNOWN_0302__";
|
||||
return "__WILDS_CRATER_BROWN_ROCK__";
|
||||
case 0x0303:
|
||||
return "__UNKNOWN_0303__";
|
||||
return "__WILDS_CRATER_BROWN_ROCK_DESTRUCTIBLE__";
|
||||
case 0x0340:
|
||||
return "__UNKNOWN_0340__";
|
||||
case 0x0341:
|
||||
return "__UNKNOWN_0341__";
|
||||
case 0x0380:
|
||||
return "__UNKNOWN_0380__";
|
||||
return "__POISON_PLANT__";
|
||||
case 0x0381:
|
||||
return "__UNKNOWN_0381__";
|
||||
case 0x0382:
|
||||
return "__UNKNOWN_0382__";
|
||||
case 0x0383:
|
||||
return "__UNKNOWN_0383__";
|
||||
return "__DESERT_OOZE_PLANT__";
|
||||
case 0x0385:
|
||||
return "__UNKNOWN_0385__";
|
||||
case 0x0386:
|
||||
return "__UNKNOWN_0386__";
|
||||
return "__WILDS_CRATER_BLACK_ROCKS__";
|
||||
case 0x0387:
|
||||
return "__UNKNOWN_0387__";
|
||||
case 0x0388:
|
||||
@@ -551,21 +551,21 @@ const char* Map::name_for_object_type(uint16_t type) {
|
||||
case 0x038A:
|
||||
return "__UNKNOWN_038A__";
|
||||
case 0x038B:
|
||||
return "__UNKNOWN_038B__";
|
||||
return "__FALLING_ROCK__";
|
||||
case 0x038C:
|
||||
return "__UNKNOWN_038C__";
|
||||
return "__DESERT_PLANT_SOLID__";
|
||||
case 0x038D:
|
||||
return "__UNKNOWN_038D__";
|
||||
return "__DESERT_CRYSTALS_BOX__";
|
||||
case 0x038E:
|
||||
return "__UNKNOWN_038E__";
|
||||
case 0x038F:
|
||||
return "__UNKNOWN_038F__";
|
||||
return "__BEE_HIVE__";
|
||||
case 0x0390:
|
||||
return "__UNKNOWN_0390__";
|
||||
case 0x0391:
|
||||
return "__UNKNOWN_0391__";
|
||||
return "__HEAT__";
|
||||
case 0x03C0:
|
||||
return "__UNKNOWN_03C0__";
|
||||
return "__EP4_BOSS_EGG__";
|
||||
case 0x03C1:
|
||||
return "__UNKNOWN_03C1__";
|
||||
default:
|
||||
@@ -704,17 +704,25 @@ string Map::Event::str() const {
|
||||
}
|
||||
|
||||
string Map::Object::str() const {
|
||||
return phosg::string_printf("[Map::Object source %zX %04hX(%s) @%04hX p1=%g p456=[%08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] floor=%02hhX item_drop_checked=%s]",
|
||||
this->source_index,
|
||||
this->base_type,
|
||||
Map::name_for_object_type(this->base_type),
|
||||
this->section,
|
||||
this->param1,
|
||||
this->param4,
|
||||
this->param5,
|
||||
this->param6,
|
||||
return phosg::string_printf("[Map::Object floor %02hhX source %zX %04hX(%s) @%04hX:(%g %g %g)/(%04" PRIX32 " %04" PRIX32 " %04" PRIX32 ") +%04hX params [%g %g %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 "]]",
|
||||
this->floor,
|
||||
this->item_drop_checked ? "true" : "false");
|
||||
this->source_index,
|
||||
this->args->base_type.load(),
|
||||
Map::name_for_object_type(this->args->base_type),
|
||||
this->args->section.load(),
|
||||
this->args->x.load(),
|
||||
this->args->y.load(),
|
||||
this->args->z.load(),
|
||||
this->args->x_angle.load(),
|
||||
this->args->y_angle.load(),
|
||||
this->args->z_angle.load(),
|
||||
this->args->group.load(),
|
||||
this->args->param1.load(),
|
||||
this->args->param2.load(),
|
||||
this->args->param3.load(),
|
||||
this->args->param4.load(),
|
||||
this->args->param5.load(),
|
||||
this->args->param6.load());
|
||||
}
|
||||
|
||||
Map::Map(Version version, uint32_t lobby_id, uint32_t rare_seed, std::shared_ptr<PSOLFGEncryption> opt_rand_crypt)
|
||||
@@ -729,7 +737,7 @@ void Map::clear() {
|
||||
this->rare_enemy_indexes.clear();
|
||||
}
|
||||
|
||||
void Map::add_objects_from_map_data(uint8_t floor, const void* data, size_t size) {
|
||||
void Map::add_objects_from_owned_map_data(uint8_t floor, const void* data, size_t size) {
|
||||
size_t entry_count = size / sizeof(ObjectEntry);
|
||||
if (size != entry_count * sizeof(ObjectEntry)) {
|
||||
throw runtime_error("data size is not a multiple of entry size");
|
||||
@@ -738,27 +746,54 @@ void Map::add_objects_from_map_data(uint8_t floor, const void* data, size_t size
|
||||
const auto* objects = reinterpret_cast<const ObjectEntry*>(data);
|
||||
for (size_t z = 0; z < entry_count; z++) {
|
||||
uint16_t object_id = this->objects.size();
|
||||
this->objects.emplace_back(Object{
|
||||
const auto& object = this->objects.emplace_back(Object{
|
||||
.args = &objects[z],
|
||||
.source_index = z,
|
||||
.floor = floor,
|
||||
.object_id = object_id,
|
||||
.base_type = objects[z].base_type,
|
||||
.section = objects[z].section,
|
||||
.group = objects[z].group,
|
||||
.param1 = objects[z].param1,
|
||||
.param3 = objects[z].param3,
|
||||
.param4 = objects[z].param4,
|
||||
.param5 = objects[z].param5,
|
||||
.param6 = objects[z].param6,
|
||||
.game_flags = 0,
|
||||
.set_flags = 0,
|
||||
.item_drop_checked = false,
|
||||
});
|
||||
|
||||
uint64_t k = section_index_key(floor, objects[z].section, objects[z].group);
|
||||
this->floor_section_and_group_to_object_index.emplace(k, object_id);
|
||||
|
||||
uint32_t base_switch_flag = 0;
|
||||
uint32_t num_switch_flags = 0;
|
||||
switch (object.args->base_type) {
|
||||
case 0x00C1: // TODoorCave01
|
||||
base_switch_flag = object.args->param4;
|
||||
num_switch_flags = (4 - clamp<size_t>(object.args->param5, 0, 4));
|
||||
break;
|
||||
|
||||
case 0x14A: // TODoorAncient08
|
||||
case 0x14B: // TODoorAncient09
|
||||
base_switch_flag = object.args->param4;
|
||||
num_switch_flags = (object.args->base_type == 0x14A) ? 4 : 2;
|
||||
break;
|
||||
|
||||
case 0x1AB: // TODoorFourLightRuins
|
||||
case 0x1C0: // TODoorFourLightSpace
|
||||
case 0x221: // TODoorFourLightSeabed
|
||||
case 0x222: // TODoorFourLightSeabedU
|
||||
base_switch_flag = object.args->param4;
|
||||
num_switch_flags = object.args->param5;
|
||||
break;
|
||||
}
|
||||
if ((num_switch_flags > 1) && !(base_switch_flag & 0xFFFFFF00)) {
|
||||
for (size_t z = 0; z < num_switch_flags; z++) {
|
||||
this->floor_and_switch_flag_to_door_index.emplace((floor << 8) | (base_switch_flag + z), object_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Map::add_objects_from_map_data(uint8_t floor, std::shared_ptr<const string> data) {
|
||||
this->link_owned_data(data);
|
||||
this->add_objects_from_owned_map_data(floor, data->data(), data->size());
|
||||
}
|
||||
|
||||
bool Map::check_and_log_rare_enemy(bool default_is_rare, uint32_t rare_rate) {
|
||||
if (default_is_rare) {
|
||||
return true;
|
||||
@@ -1597,12 +1632,13 @@ void Map::add_entities_from_quest_data(
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
const void* data,
|
||||
size_t size,
|
||||
std::shared_ptr<const string> data,
|
||||
std::shared_ptr<const RareEnemyRates> rare_rates) {
|
||||
auto all_floor_sections = this->collect_quest_map_data_sections(data, size);
|
||||
this->link_owned_data(data);
|
||||
|
||||
phosg::StringReader r(data, size);
|
||||
auto all_floor_sections = this->collect_quest_map_data_sections(data->data(), data->size());
|
||||
|
||||
phosg::StringReader r(*data);
|
||||
shared_ptr<DATParserRandomState> random_state;
|
||||
for (size_t floor = 0; floor < all_floor_sections.size(); floor++) {
|
||||
const auto& floor_sections = all_floor_sections[floor];
|
||||
@@ -1612,7 +1648,7 @@ void Map::add_entities_from_quest_data(
|
||||
if (header.data_size % sizeof(ObjectEntry)) {
|
||||
throw runtime_error("quest layout object section size is not a multiple of object entry size");
|
||||
}
|
||||
this->add_objects_from_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size);
|
||||
this->add_objects_from_owned_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size);
|
||||
}
|
||||
|
||||
if ((floor_sections.wave_events != 0xFFFFFFFF) &&
|
||||
@@ -1741,6 +1777,16 @@ std::vector<Map::Event*> Map::get_events(uint8_t floor) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
vector<Map::Object*> Map::doors_for_switch_flag(uint8_t floor, uint8_t switch_flag) {
|
||||
vector<Map::Object*> ret;
|
||||
for (auto its = this->floor_and_switch_flag_to_door_index.equal_range((floor << 8) | switch_flag);
|
||||
its.first != its.second;
|
||||
its.first++) {
|
||||
ret.emplace_back(&this->objects[its.first->second]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <typename EntryT>
|
||||
static string disassemble_vector_file_t(const void* data, size_t size, size_t* entry_number, char type_ch) {
|
||||
deque<string> ret;
|
||||
|
||||
+14
-13
@@ -51,9 +51,9 @@ struct Map {
|
||||
/* 1C */ le_uint32_t x_angle;
|
||||
/* 20 */ le_uint32_t y_angle;
|
||||
/* 24 */ le_uint32_t z_angle;
|
||||
/* 28 */ le_float param1; // If <= 0, this is a specialized box, and the specialization is in param4/5/6
|
||||
/* 28 */ le_float param1; // Boxes: if <= 0, this is a specialized box, and the specialization is in param4/5/6
|
||||
/* 2C */ le_float param2;
|
||||
/* 30 */ le_float param3;
|
||||
/* 30 */ le_float param3; // Boxes: if == 0, the item should be varied by difficulty and area
|
||||
/* 34 */ le_uint32_t param4;
|
||||
/* 38 */ le_uint32_t param5;
|
||||
/* 3C */ le_uint32_t param6;
|
||||
@@ -211,17 +211,10 @@ struct Map {
|
||||
struct Object {
|
||||
// TODO: Add more fields in here if we ever care about them. Currently we
|
||||
// only care about boxes with fixed item drops.
|
||||
const ObjectEntry* args;
|
||||
size_t source_index;
|
||||
uint8_t floor;
|
||||
uint16_t object_id;
|
||||
uint16_t base_type;
|
||||
uint16_t section;
|
||||
uint16_t group;
|
||||
float param1; // If <= 0, this is a specialized box, and the specialization is in param4/5/6
|
||||
float param3; // If == 0, the item should be varied by difficulty and area
|
||||
uint32_t param4;
|
||||
uint32_t param5;
|
||||
uint32_t param6;
|
||||
uint16_t game_flags;
|
||||
// Technically set_flags shouldn't be part of the Object struct, but all
|
||||
// object entries always generate exactly one object, so we store it here.
|
||||
@@ -305,7 +298,12 @@ struct Map {
|
||||
|
||||
void clear();
|
||||
|
||||
void add_objects_from_map_data(uint8_t floor, const void* data, size_t size);
|
||||
inline void link_owned_data(std::shared_ptr<const std::string> data) {
|
||||
this->linked_data.emplace(data);
|
||||
}
|
||||
|
||||
void add_objects_from_owned_map_data(uint8_t floor, const void* data, size_t size);
|
||||
void add_objects_from_map_data(uint8_t floor, std::shared_ptr<const std::string> data);
|
||||
|
||||
bool check_and_log_rare_enemy(bool default_is_rare, uint32_t rare_rate);
|
||||
void add_enemy(
|
||||
@@ -359,8 +357,7 @@ struct Map {
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
const void* data,
|
||||
size_t size,
|
||||
std::shared_ptr<const std::string> data,
|
||||
std::shared_ptr<const RareEnemyRates> rare_rates = Map::DEFAULT_RARE_ENEMIES);
|
||||
|
||||
const Enemy& find_enemy(uint16_t enemy_id) const;
|
||||
@@ -372,6 +369,8 @@ struct Map {
|
||||
std::vector<Event*> get_events(uint8_t floor, uint16_t section, uint16_t wave_number);
|
||||
std::vector<Event*> get_events(uint8_t floor);
|
||||
|
||||
std::vector<Object*> doors_for_switch_flag(uint8_t floor, uint8_t switch_flag);
|
||||
|
||||
static std::string disassemble_objects_data(const void* data, size_t size, size_t* object_number = nullptr);
|
||||
static std::string disassemble_enemies_data(const void* data, size_t size, size_t* enemy_number = nullptr);
|
||||
static std::string disassemble_wave_events_data(const void* data, size_t size, uint8_t floor = 0xFF);
|
||||
@@ -380,6 +379,7 @@ struct Map {
|
||||
phosg::PrefixedLogger log;
|
||||
Version version;
|
||||
uint32_t rare_seed;
|
||||
std::unordered_set<std::shared_ptr<const std::string>> linked_data;
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt;
|
||||
std::vector<Object> objects;
|
||||
std::vector<Enemy> enemies;
|
||||
@@ -391,6 +391,7 @@ struct Map {
|
||||
std::unordered_multimap<uint64_t, size_t> floor_section_and_group_to_object_index;
|
||||
std::unordered_multimap<uint64_t, size_t> floor_section_and_wave_number_to_enemy_index;
|
||||
std::unordered_multimap<uint64_t, size_t> floor_section_and_wave_number_to_event_index;
|
||||
std::unordered_multimap<uint16_t, size_t> floor_and_switch_flag_to_door_index;
|
||||
};
|
||||
|
||||
class SetDataTableBase {
|
||||
|
||||
@@ -66,9 +66,16 @@ map<string, uint32_t> get_local_addresses() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool is_loopback_address(uint32_t addr) {
|
||||
return ((addr & 0xFF000000) == 0x7F000000); // 127.0.0.0/8
|
||||
}
|
||||
|
||||
bool is_local_address(uint32_t addr) {
|
||||
uint8_t net = (addr >> 24) & 0xFF;
|
||||
return ((net == 127) || (net == 172) || (net == 10) || (net == 192));
|
||||
return is_loopback_address(addr) || // 127.0.0.0/8
|
||||
((addr & 0xFF000000) == 0x0A000000) || // 10.0.0.0/8
|
||||
((addr & 0xFFF00000) == 0xAC100000) || // 172.16.0.0/12
|
||||
((addr & 0xFFFF0000) == 0xC0A80000) || // 192.168.0.0/16
|
||||
((addr & 0xFFFF0000) == 0xA9FE0000); // 169.254.0.0/16
|
||||
}
|
||||
|
||||
bool is_local_address(const sockaddr_storage& daddr) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
uint32_t resolve_address(const char* address);
|
||||
std::map<std::string, uint32_t> get_local_addresses();
|
||||
uint32_t get_connected_address(int fd);
|
||||
bool is_loopback_address(uint32_t addr);
|
||||
bool is_local_address(uint32_t daddr);
|
||||
bool is_local_address(const sockaddr_storage& daddr);
|
||||
|
||||
|
||||
@@ -300,6 +300,10 @@ check_struct_size(ChallengeTimeBE, 4);
|
||||
|
||||
std::string decrypt_v2_registry_value(const void* data, size_t size);
|
||||
|
||||
inline std::string decrypt_v2_registry_value(const std::string& s) {
|
||||
return decrypt_v2_registry_value(s.data(), s.size());
|
||||
}
|
||||
|
||||
struct DecryptedPR2 {
|
||||
std::string compressed_data;
|
||||
size_t decompressed_size;
|
||||
|
||||
@@ -120,6 +120,12 @@ T& check_size_t(void* data, size_t size) {
|
||||
return check_size_generic<T, void*>(data, size, sizeof(T), sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T* check_size_vec_t(std::string& data, size_t count, bool allow_extra = false) {
|
||||
size_t expected_size = count * sizeof(T);
|
||||
return &check_size_generic<T, void*>(data.data(), data.size(), expected_size, allow_extra ? 0xFFFF : expected_size);
|
||||
}
|
||||
|
||||
void check_size_v(size_t size, size_t min_size, size_t max_size = 0);
|
||||
|
||||
std::string prepend_command_header(
|
||||
|
||||
+3
-3
@@ -168,13 +168,13 @@ void PatchServer::on_04(shared_ptr<Client> c, string& data) {
|
||||
this->config->account_index->from_bb_credentials(username, &password, false);
|
||||
|
||||
} catch (const AccountIndex::incorrect_password& e) {
|
||||
this->send_message_box(c, phosg::string_printf("Login failed: %s", e.what()));
|
||||
c->channel.send(0x15, 0x03);
|
||||
this->disconnect_client(c);
|
||||
return;
|
||||
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
if (!this->config->allow_unregistered_users) {
|
||||
this->send_message_box(c, phosg::string_printf("Login failed: %s", e.what()));
|
||||
c->channel.send(0x15, 0x08);
|
||||
this->disconnect_client(c);
|
||||
return;
|
||||
}
|
||||
@@ -184,7 +184,7 @@ void PatchServer::on_04(shared_ptr<Client> c, string& data) {
|
||||
try {
|
||||
this->config->account_index->from_bb_credentials(username, nullptr, false);
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
this->send_message_box(c, phosg::string_printf("Login failed: %s", e.what()));
|
||||
c->channel.send(0x15, 0x08);
|
||||
this->disconnect_client(c);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -699,29 +699,6 @@ const ChallengeTemplateDefinition& get_challenge_template_definition(Version ver
|
||||
}
|
||||
}
|
||||
|
||||
void RecentSwitchFlags::add(uint16_t flag_num) {
|
||||
if ((flag_num != ((this->flag_nums >> 48) & 0xFFFF)) &&
|
||||
(flag_num != ((this->flag_nums >> 32) & 0xFFFF)) &&
|
||||
(flag_num != ((this->flag_nums >> 16) & 0xFFFF)) &&
|
||||
(flag_num != (this->flag_nums & 0xFFFF))) {
|
||||
this->flag_nums = this->flag_nums << 16 | flag_num;
|
||||
}
|
||||
}
|
||||
|
||||
string RecentSwitchFlags::enable_commands(uint8_t floor) const {
|
||||
phosg::StringWriter w;
|
||||
uint64_t flag_nums = this->flag_nums;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
uint16_t flag_num = flag_nums;
|
||||
if (flag_num == 0xFFFF) {
|
||||
continue;
|
||||
}
|
||||
w.put(G_SwitchStateChanged_6x05{{0x05, 0x03, 0xFFFF}, 0, 0, flag_num, static_cast<uint8_t>(floor), 0x01});
|
||||
flag_nums >>= 16;
|
||||
}
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
const QuestFlagsForDifficulty bb_quest_flag_apply_mask{{
|
||||
// clang-format off
|
||||
/* 0000 */ 0x00, 0x3F, 0xFF, 0xE3, 0xE0, 0xFF, 0xFF, 0x00,
|
||||
|
||||
+23
-59
@@ -881,78 +881,54 @@ struct BattleRules {
|
||||
LIMIT_LIVES = 2,
|
||||
};
|
||||
|
||||
// Set by quest opcode F812, but values are remapped.
|
||||
// F812 00 => FORBID_ALL
|
||||
// F812 01 => ALLOW
|
||||
// F812 02 => LIMIT_LEVEL
|
||||
// Set by quest opcode F812, but values are remapped
|
||||
/* 00 */ TechDiskMode tech_disk_mode = TechDiskMode::ALLOW;
|
||||
// Set by quest opcode F813, but values are remapped.
|
||||
// F813 00 => FORBID_ALL
|
||||
// F813 01 => ALLOW
|
||||
// F813 02 => CLEAR_AND_ALLOW
|
||||
// F813 03 => FORBID_RARES
|
||||
// Set by quest opcode F813, but values are remapped
|
||||
/* 01 */ WeaponAndArmorMode weapon_and_armor_mode = WeaponAndArmorMode::ALLOW;
|
||||
// Set by quest opcode F814, but values are remapped.
|
||||
// F814 00 => FORBID_ALL
|
||||
// F814 01 => ALLOW
|
||||
// Set by quest opcode F814, but values are remapped
|
||||
/* 02 */ MagMode mag_mode = MagMode::ALLOW;
|
||||
// Set by quest opcode F815, but values are remapped.
|
||||
// F815 00 => FORBID_ALL
|
||||
// F815 01 => ALLOW
|
||||
// F815 02 => CLEAR_AND_ALLOW
|
||||
// Set by quest opcode F815, but values are remapped
|
||||
/* 03 */ ToolMode tool_mode = ToolMode::ALLOW;
|
||||
// Set by quest opcode F816. Values are not remapped.
|
||||
// F816 00 => DEFAULT
|
||||
// F816 01 => ALL_PLAYERS
|
||||
// Set by quest opcode F816. Values are not remapped
|
||||
/* 04 */ TrapMode trap_mode = TrapMode::DEFAULT;
|
||||
// Set by quest opcode F817. Value appears to be unused in all PSO versions.
|
||||
/* 05 */ uint8_t unused_F817 = 0;
|
||||
// Set by quest opcode F818, but values are remapped.
|
||||
// F818 00 => 01
|
||||
// F818 01 => 00
|
||||
// F818 02 => 02
|
||||
// Set by quest opcode F818, but values are remapped
|
||||
/* 06 */ RespawnMode respawn_mode = RespawnMode::ALLOW;
|
||||
// Set by quest opcode F819.
|
||||
// Set by quest opcode F819
|
||||
/* 07 */ uint8_t replace_char = 0;
|
||||
// Set by quest opcode F81A, but value is inverted.
|
||||
// Set by quest opcode F81A, but value is inverted
|
||||
/* 08 */ uint8_t drop_weapon = 0;
|
||||
// Set by quest opcode F81B.
|
||||
// Set by quest opcode F81B
|
||||
/* 09 */ uint8_t is_teams = 0;
|
||||
// Set by quest opcode F852.
|
||||
// Set by quest opcode F852
|
||||
/* 0A */ uint8_t hide_target_reticle = 0;
|
||||
// Set by quest opcode F81E. Values are not remapped.
|
||||
// F81E 00 => ALLOW
|
||||
// F81E 01 => FORBID_ALL
|
||||
// F81E 02 => CLEAR_AND_ALLOW
|
||||
// Set by quest opcode F81E. Values are not remapped
|
||||
/* 0B */ MesetaMode meseta_mode = MesetaMode::ALLOW;
|
||||
// Set by quest opcode F81D.
|
||||
// Set by quest opcode F81D
|
||||
/* 0C */ uint8_t death_level_up = 0;
|
||||
// Set by quest opcode F851. The trap type is remapped:
|
||||
// F851 00 XX => set count to XX for trap type 00
|
||||
// F851 01 XX => set count to XX for trap type 02
|
||||
// F851 02 XX => set count to XX for trap type 03
|
||||
// F851 03 XX => set count to XX for trap type 01
|
||||
// Set by quest opcode F851. The trap type is remapped
|
||||
/* 0D */ parray<uint8_t, 4> trap_counts;
|
||||
// Set by quest opcode F85E.
|
||||
// Set by quest opcode F85E
|
||||
/* 11 */ uint8_t enable_sonar = 0;
|
||||
// Set by quest opcode F85F.
|
||||
// Set by quest opcode F85F
|
||||
/* 12 */ uint8_t sonar_count = 0;
|
||||
// Set by quest opcode F89E.
|
||||
// Set by quest opcode F89E
|
||||
/* 13 */ uint8_t forbid_scape_dolls = 0;
|
||||
// This value does not appear to be set by any quest opcode.
|
||||
// This value does not appear to be set by any quest opcode
|
||||
/* 14 */ le_uint32_t unknown_a1 = 0;
|
||||
// Set by quest opcode F86F.
|
||||
// Set by quest opcode F86F
|
||||
/* 18 */ le_uint32_t lives = 0;
|
||||
// Set by quest opcode F870.
|
||||
// Set by quest opcode F870
|
||||
/* 1C */ le_uint32_t max_tech_level = 0;
|
||||
// Set by quest opcode F871.
|
||||
// Set by quest opcode F871
|
||||
/* 20 */ le_uint32_t char_level = 0;
|
||||
// Set by quest opcode F872.
|
||||
// Set by quest opcode F872
|
||||
/* 24 */ le_uint32_t time_limit = 0;
|
||||
// Set by quest opcode F8A8.
|
||||
// Set by quest opcode F8A8
|
||||
/* 28 */ le_uint16_t death_tech_level_up = 0;
|
||||
/* 2A */ parray<uint8_t, 2> unused;
|
||||
// Set by quest opcode F86B.
|
||||
// Set by quest opcode F86B
|
||||
/* 2C */ le_uint32_t box_drop_area = 0;
|
||||
/* 30 */
|
||||
|
||||
@@ -1018,16 +994,4 @@ using SymbolChatBE = SymbolChatT<true>;
|
||||
check_struct_size(SymbolChat, 0x3C);
|
||||
check_struct_size(SymbolChatBE, 0x3C);
|
||||
|
||||
struct RecentSwitchFlags {
|
||||
uint64_t flag_nums = 0xFFFFFFFFFFFFFFFF;
|
||||
|
||||
inline void clear() {
|
||||
this->flag_nums = 0xFFFFFFFFFFFFFFFF;
|
||||
}
|
||||
|
||||
void add(uint16_t flag_num);
|
||||
|
||||
std::string enable_commands(uint8_t floor) const;
|
||||
};
|
||||
|
||||
extern const QuestFlagsForDifficulty bb_quest_flag_apply_mask;
|
||||
|
||||
+123
-57
@@ -172,7 +172,11 @@ static HandlerResult S_G_9A(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t
|
||||
cmd.sub_version = ses->effective_sub_version();
|
||||
cmd.is_extended = (ses->remote_guild_card_number < 0) ? 1 : 0;
|
||||
cmd.language = ses->language();
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
if (ses->login->gc_license) {
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->gc_license->serial_number));
|
||||
} else {
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
}
|
||||
cmd.access_key.encode(ses->login->gc_license->access_key);
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
@@ -259,7 +263,7 @@ static HandlerResult S_V123P_02_17(
|
||||
}
|
||||
if (command == 0x17) {
|
||||
C_LoginV1_DC_PC_V3_90 cmd;
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->dc_license->serial_number));
|
||||
cmd.access_key.encode(ses->login->dc_license->access_key);
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
ses->server_channel.send(0x90, 0x00, &cmd, sizeof(cmd));
|
||||
@@ -278,7 +282,7 @@ static HandlerResult S_V123P_02_17(
|
||||
cmd.sub_version = ses->effective_sub_version();
|
||||
cmd.is_extended = 0;
|
||||
cmd.language = ses->language();
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->dc_license->serial_number));
|
||||
cmd.access_key.encode(ses->login->dc_license->access_key);
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
cmd.hardware_id.encode(ses->hardware_id);
|
||||
@@ -291,17 +295,27 @@ static HandlerResult S_V123P_02_17(
|
||||
case Version::DC_V2:
|
||||
case Version::PC_NTE:
|
||||
case Version::PC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_NTE: {
|
||||
uint32_t serial_number;
|
||||
const string* access_key;
|
||||
if (ses->version() == Version::DC_V2) {
|
||||
access_key = ses->login->dc_license ? &ses->login->dc_license->access_key : nullptr;
|
||||
if (!ses->login->dc_license) {
|
||||
throw runtime_error("incorrect login type");
|
||||
}
|
||||
serial_number = ses->login->dc_license->serial_number;
|
||||
access_key = &ses->login->dc_license->access_key;
|
||||
} else if (ses->version() != Version::GC_NTE) {
|
||||
access_key = ses->login->pc_license ? &ses->login->pc_license->access_key : nullptr;
|
||||
if (!ses->login->pc_license) {
|
||||
throw runtime_error("incorrect login type");
|
||||
}
|
||||
serial_number = ses->login->pc_license->serial_number;
|
||||
access_key = &ses->login->pc_license->access_key;
|
||||
} else {
|
||||
access_key = ses->login->gc_license ? &ses->login->gc_license->access_key : nullptr;
|
||||
}
|
||||
if (!access_key) {
|
||||
throw runtime_error("incorrect login type");
|
||||
if (!ses->login->gc_license) {
|
||||
throw runtime_error("incorrect login type");
|
||||
}
|
||||
serial_number = ses->login->gc_license->serial_number;
|
||||
access_key = &ses->login->gc_license->access_key;
|
||||
}
|
||||
|
||||
if (command == 0x17) {
|
||||
@@ -314,12 +328,16 @@ static HandlerResult S_V123P_02_17(
|
||||
cmd.guild_card_number = ses->remote_guild_card_number;
|
||||
}
|
||||
cmd.sub_version = ses->effective_sub_version();
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", serial_number));
|
||||
cmd.access_key.encode(*access_key);
|
||||
if (ses->version() != Version::GC_NTE) {
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
}
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
if (is_dc(ses->version())) {
|
||||
cmd.serial_number2.encode(ses->hardware_id);
|
||||
} else {
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
}
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
// TODO: We probably should set email_address, but we currently don't
|
||||
// keep that value anywhere in the session object, nor is it saved in
|
||||
@@ -340,12 +358,16 @@ static HandlerResult S_V123P_02_17(
|
||||
cmd.sub_version = ses->effective_sub_version();
|
||||
cmd.is_extended = 0;
|
||||
cmd.language = ses->language();
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", serial_number));
|
||||
cmd.access_key.encode(*access_key);
|
||||
if (ses->version() != Version::GC_NTE) {
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
}
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
if (is_dc(ses->version())) {
|
||||
cmd.serial_number2.encode(ses->hardware_id);
|
||||
} else {
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
}
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) {
|
||||
cmd.name.encode(" ", ses->language());
|
||||
@@ -356,6 +378,7 @@ static HandlerResult S_V123P_02_17(
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_NTE:
|
||||
@@ -365,7 +388,7 @@ static HandlerResult S_V123P_02_17(
|
||||
}
|
||||
if (command == 0x17) {
|
||||
C_VerifyAccount_V3_DB cmd;
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.serial_number.encode(phosg::string_printf("%08" PRIX32 "", ses->login->gc_license->serial_number));
|
||||
cmd.access_key.encode(ses->login->gc_license->access_key);
|
||||
cmd.sub_version = ses->effective_sub_version();
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
@@ -1151,21 +1174,45 @@ static HandlerResult S_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
} else if ((data[0] == 0x60) || (static_cast<uint8_t>(data[0]) == 0xA2)) {
|
||||
return SC_6x60_6xA2(ses, data);
|
||||
|
||||
} else if ((static_cast<uint8_t>(data[0]) == 0xB5) && is_ep3(ses->version()) && (data.size() > 4)) {
|
||||
} else if ((static_cast<uint8_t>(data[0]) == 0xB5) && is_ep3(ses->version()) && (data.size() >= 8)) {
|
||||
set_mask_for_ep3_game_command(data.data(), data.size(), 0);
|
||||
if (data[4] == 0x1A) {
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
} else if (data[4] == 0x20) {
|
||||
auto& cmd = check_size_t<G_Unknown_Ep3_6xB5x20>(data);
|
||||
if (cmd.client_id >= 12) {
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
} else if (data[4] == 0x31) {
|
||||
auto& cmd = check_size_t<G_ConfirmDeckSelection_Ep3_6xB5x31>(data);
|
||||
if (cmd.menu_type >= 0x15) {
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
} else if (data[4] == 0x32) {
|
||||
auto& cmd = check_size_t<G_MoveSharedMenuCursor_Ep3_6xB5x32>(data);
|
||||
if (cmd.menu_type >= 0x15) {
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
} else if (data[4] == 0x36) {
|
||||
auto& cmd = check_size_t<G_RecreatePlayer_Ep3_6xB5x36>(data);
|
||||
set_mask_for_ep3_game_command(&cmd, sizeof(cmd), 0);
|
||||
if (ses->is_in_game && (cmd.client_id >= 4)) {
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
}
|
||||
|
||||
} else if ((static_cast<uint8_t>(data[0]) == 0xBB) && is_ep3(ses->version())) {
|
||||
if (!validate_6xBB(check_size_t<G_SyncCardTradeServerState_Ep3_6xBB>(data))) {
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
return HandlerResult::Type::MODIFIED;
|
||||
|
||||
} else if ((static_cast<uint8_t>(data[0]) == 0xBC) && !ses->config.check_flag(Client::Flag::EP3_ALLOW_6xBC)) {
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
|
||||
} 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_Ep3_6xBD>(data);
|
||||
auto& cmd = check_size_t<G_PrivateWordSelect_Ep3_6xBD>(data);
|
||||
if (cmd.private_flags & (1 << ses->lobby_client_id)) {
|
||||
cmd.private_flags &= ~(1 << ses->lobby_client_id);
|
||||
modified = true;
|
||||
@@ -1205,15 +1252,15 @@ static HandlerResult C_GXB_61(shared_ptr<ProxyServer::LinkedSession> ses, uint16
|
||||
C_CharacterData_V3_61_98* pd;
|
||||
if (flag == 4) { // Episode 3
|
||||
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,
|
||||
offsetof(Episode3::PlayerConfig, decks) - offsetof(Episode3::PlayerConfig, card_counts),
|
||||
ep3_pd.ep3_config.basis);
|
||||
ep3_pd.ep3_config.is_encrypted = 0;
|
||||
ep3_pd.ep3_config.basis = 0;
|
||||
modified = true;
|
||||
}
|
||||
// if (ep3_pd.ep3_config.is_encrypted) {
|
||||
// decrypt_trivial_gci_data(
|
||||
// &ep3_pd.ep3_config.card_counts,
|
||||
// offsetof(Episode3::PlayerConfig, decks) - offsetof(Episode3::PlayerConfig, card_counts),
|
||||
// ep3_pd.ep3_config.basis);
|
||||
// ep3_pd.ep3_config.is_encrypted = 0;
|
||||
// ep3_pd.ep3_config.basis = 0;
|
||||
// modified = true;
|
||||
// }
|
||||
pd = reinterpret_cast<C_CharacterData_V3_61_98*>(&ep3_pd);
|
||||
} else {
|
||||
if (is_ep3(ses->version()) && (ses->version() != Version::GC_EP3_NTE)) {
|
||||
@@ -1318,7 +1365,7 @@ constexpr on_command_t S_PG_44_A6 = &S_44_A6<S_OpenFile_PC_GC_44_A6>;
|
||||
constexpr on_command_t S_X_44_A6 = &S_44_A6<S_OpenFile_XB_44_A6>;
|
||||
constexpr on_command_t S_B_44_A6 = &S_44_A6<S_OpenFile_BB_44_A6>;
|
||||
|
||||
static HandlerResult S_13_A7(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
static HandlerResult S_13_A7(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t flag, string& data) {
|
||||
auto& cmd = check_size_t<S_WriteFile_13_A7>(data);
|
||||
bool modified = false;
|
||||
|
||||
@@ -1331,36 +1378,41 @@ static HandlerResult S_13_A7(shared_ptr<ProxyServer::LinkedSession> ses, uint16_
|
||||
return HandlerResult::Type::FORWARD;
|
||||
}
|
||||
|
||||
if (cmd.data_size > sf->remaining_bytes) {
|
||||
ses->log.warning("Chunk size extends beyond original file size; truncating file");
|
||||
cmd.data_size = sf->remaining_bytes;
|
||||
modified = true;
|
||||
} else if (cmd.data_size > 0x400) {
|
||||
ses->log.warning("Chunk data size is invalid; truncating to 0x400");
|
||||
cmd.data_size = 0x400;
|
||||
bool is_last_block = (cmd.data_size != 0x400);
|
||||
size_t block_offset = flag * 0x400;
|
||||
size_t allowed_block_size = (block_offset < sf->total_size)
|
||||
? min<size_t>(sf->total_size - block_offset, 0x400)
|
||||
: 0;
|
||||
|
||||
if (cmd.data_size > allowed_block_size) {
|
||||
ses->log.warning("Block size extends beyond allowed size; truncating block");
|
||||
cmd.data_size = allowed_block_size;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!sf->output_filename.empty()) {
|
||||
ses->log.info("Adding %" PRIu32 " bytes to %s => %s",
|
||||
cmd.data_size.load(), sf->basename.c_str(), sf->output_filename.c_str());
|
||||
ses->log.info("Adding %" PRIu32 " bytes to %s:%02" PRIX32 " => %s:%zX",
|
||||
cmd.data_size.load(), sf->basename.c_str(), flag, sf->output_filename.c_str(), block_offset);
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_SAVE_FILES)) {
|
||||
sf->blocks.emplace_back(reinterpret_cast<const char*>(cmd.data.data()), cmd.data_size);
|
||||
size_t block_end_offset = block_offset + cmd.data_size;
|
||||
if (sf->data.size() < block_end_offset) {
|
||||
sf->data.resize(block_end_offset);
|
||||
}
|
||||
memcpy(sf->data.data() + block_offset, reinterpret_cast<const char*>(cmd.data.data()), cmd.data_size);
|
||||
}
|
||||
}
|
||||
sf->remaining_bytes -= cmd.data_size;
|
||||
|
||||
if (sf->remaining_bytes == 0) {
|
||||
if (is_last_block) {
|
||||
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());
|
||||
string data = phosg::join(sf->blocks);
|
||||
|
||||
if (sf->is_download && (phosg::ends_with(sf->basename, ".bin") || phosg::ends_with(sf->basename, ".dat") || phosg::ends_with(sf->basename, ".pvr"))) {
|
||||
data = decode_dlq_data(data);
|
||||
sf->data = decode_dlq_data(sf->data);
|
||||
}
|
||||
phosg::save_file(sf->output_filename, data);
|
||||
phosg::save_file(sf->output_filename, sf->data);
|
||||
if (phosg::ends_with(sf->basename, ".bin")) {
|
||||
try {
|
||||
string decompressed = prs_decompress(data);
|
||||
string decompressed = prs_decompress(sf->data);
|
||||
auto disassembly = disassemble_quest_script(decompressed.data(), decompressed.size(), ses->version(), ses->language(), false);
|
||||
phosg::save_file(sf->output_filename + ".txt", disassembly);
|
||||
} catch (const exception& e) {
|
||||
@@ -1372,7 +1424,7 @@ static HandlerResult S_13_A7(shared_ptr<ProxyServer::LinkedSession> ses, uint16_
|
||||
}
|
||||
|
||||
if (!sf->is_download && phosg::ends_with(sf->basename, ".dat")) {
|
||||
auto quest_dat_data = make_shared<std::string>(phosg::join(sf->blocks));
|
||||
auto quest_dat_data = make_shared<std::string>(prs_decompress(sf->data));
|
||||
try {
|
||||
ses->map = Lobby::load_maps(
|
||||
ses->version(),
|
||||
@@ -1978,12 +2030,21 @@ HandlerResult C_6x<void>(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, u
|
||||
if (!data.empty()) {
|
||||
if ((data[0] == 0x05) && ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) {
|
||||
auto& cmd = check_size_t<G_SwitchStateChanged_6x05>(data);
|
||||
if ((cmd.flags & 1) && (cmd.header.object_id != 0xFFFF)) {
|
||||
ses->recent_switch_flags.add(cmd.switch_flag_num);
|
||||
string commands = ses->recent_switch_flags.enable_commands(ses->floor);
|
||||
if (!commands.empty()) {
|
||||
ses->server_channel.send(0x60, 0x00, commands);
|
||||
ses->client_channel.send(0x60, 0x00, commands);
|
||||
if (ses->map && (cmd.flags & 1) && (cmd.header.object_id != 0xFFFF)) {
|
||||
for (auto* door : ses->map->doors_for_switch_flag(cmd.switch_flag_floor, cmd.switch_flag_num)) {
|
||||
if (door->game_flags & 0x0001) {
|
||||
continue;
|
||||
}
|
||||
door->game_flags |= 1;
|
||||
|
||||
G_UpdateObjectState_6x0B cmd0B;
|
||||
cmd0B.header.subcommand = 0x0B;
|
||||
cmd0B.header.size = sizeof(cmd0B) / 4;
|
||||
cmd0B.header.client_id = door->object_id | 0x4000;
|
||||
cmd0B.flags = door->game_flags;
|
||||
cmd0B.object_index = door->object_id;
|
||||
ses->client_channel.send(0x60, 0x00, &cmd0B, sizeof(cmd0B));
|
||||
ses->server_channel.send(0x60, 0x00, &cmd0B, sizeof(cmd0B));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1993,8 +2054,8 @@ HandlerResult C_6x<void>(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, u
|
||||
|
||||
} else if (data[0] == 0x0C) {
|
||||
if (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);
|
||||
send_remove_negative_conditions(ses->client_channel, ses->lobby_client_id);
|
||||
send_remove_negative_conditions(ses->server_channel, ses->lobby_client_id);
|
||||
}
|
||||
|
||||
} else if (data[0] == 0x2F || data[0] == 0x4B || data[0] == 0x4C) {
|
||||
@@ -2019,10 +2080,8 @@ HandlerResult C_6x<void>(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, u
|
||||
|
||||
} else if (data[0] == 0x48) {
|
||||
if (ses->config.check_flag(Client::Flag::INFINITE_TP_ENABLED)) {
|
||||
send_player_stats_change(ses->client_channel,
|
||||
ses->lobby_client_id, PlayerStatsChange::ADD_TP, 255);
|
||||
send_player_stats_change(ses->server_channel,
|
||||
ses->lobby_client_id, PlayerStatsChange::ADD_TP, 255);
|
||||
send_player_stats_change(ses->client_channel, ses->lobby_client_id, PlayerStatsChange::ADD_TP, 255);
|
||||
send_player_stats_change(ses->server_channel, ses->lobby_client_id, PlayerStatsChange::ADD_TP, 255);
|
||||
}
|
||||
|
||||
} else if (data[0] == 0x5F) {
|
||||
@@ -2031,6 +2090,13 @@ HandlerResult C_6x<void>(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, u
|
||||
|
||||
} else if (data[0] == 0x60 || static_cast<uint8_t>(data[0]) == 0xA2) {
|
||||
return SC_6x60_6xA2(ses, data);
|
||||
|
||||
} else if (is_ep3(ses->version()) && (data.size() > 4) && (static_cast<uint8_t>(data[0]) == 0xB5)) {
|
||||
if (data[4] == 0x38) {
|
||||
ses->config.set_flag(Client::Flag::EP3_ALLOW_6xBC);
|
||||
} else if (data[4] == 0x3C) {
|
||||
ses->config.clear_flag(Client::Flag::EP3_ALLOW_6xBC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -708,12 +708,12 @@ void ProxyServer::LinkedSession::update_channel_names() {
|
||||
ProxyServer::LinkedSession::SavingFile::SavingFile(
|
||||
const string& basename,
|
||||
const string& output_filename,
|
||||
size_t remaining_bytes,
|
||||
size_t total_size,
|
||||
bool is_download)
|
||||
: basename(basename),
|
||||
output_filename(output_filename),
|
||||
is_download(is_download),
|
||||
remaining_bytes(remaining_bytes) {}
|
||||
total_size(total_size) {}
|
||||
|
||||
void ProxyServer::LinkedSession::dispatch_on_timeout(evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<LinkedSession*>(ctx)->on_timeout();
|
||||
|
||||
+3
-4
@@ -70,7 +70,6 @@ public:
|
||||
Client::Config config;
|
||||
// A null handler in here means to forward the response to the remote server
|
||||
std::deque<std::function<void(uint32_t return_value, uint32_t checksum)>> function_call_return_handler_queue;
|
||||
RecentSwitchFlags recent_switch_flags; // used for switch assist
|
||||
ItemData next_drop_item;
|
||||
uint32_t next_item_id;
|
||||
|
||||
@@ -115,13 +114,13 @@ public:
|
||||
std::string basename;
|
||||
std::string output_filename;
|
||||
bool is_download;
|
||||
size_t remaining_bytes;
|
||||
std::deque<std::string> blocks;
|
||||
size_t total_size;
|
||||
std::string data;
|
||||
|
||||
SavingFile(
|
||||
const std::string& basename,
|
||||
const std::string& output_filename,
|
||||
size_t remaining_bytes,
|
||||
size_t total_size,
|
||||
bool is_download);
|
||||
};
|
||||
std::unordered_map<std::string, SavingFile> saving_files;
|
||||
|
||||
+2
-2
@@ -166,7 +166,7 @@ string find_seed_and_decrypt_download_quest_data_section(
|
||||
const void* data_section, size_t size, bool skip_checksum, bool is_ep3_trial, size_t num_threads) {
|
||||
mutex result_lock;
|
||||
string result;
|
||||
uint64_t result_seed = phosg::parallel_range<uint64_t>([&](uint64_t seed, size_t) {
|
||||
uint64_t result_seed = phosg::parallel_range_blocks<uint64_t>([&](uint64_t seed, size_t) {
|
||||
try {
|
||||
string ret = decrypt_download_quest_data_section<BE>(
|
||||
data_section, size, seed, skip_checksum, is_ep3_trial);
|
||||
@@ -177,7 +177,7 @@ string find_seed_and_decrypt_download_quest_data_section(
|
||||
return false;
|
||||
}
|
||||
},
|
||||
0, 0x100000000, num_threads);
|
||||
0, 0x100000000, 0x1000, num_threads);
|
||||
|
||||
if (!result.empty() && (result_seed < 0x100000000)) {
|
||||
static_game_data_log.info("Found seed %08" PRIX64, result_seed);
|
||||
|
||||
+2552
-682
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -80,9 +80,17 @@ struct PSOQuestHeaderBB {
|
||||
/* 0398 */
|
||||
} __packed_ws__(PSOQuestHeaderBB, 0x398);
|
||||
|
||||
void check_opcode_definitions();
|
||||
|
||||
Episode episode_for_quest_episode_number(uint8_t episode_number);
|
||||
|
||||
std::string disassemble_quest_script(const void* data, size_t size, Version version, uint8_t override_language = 0xFF, bool reassembly_mode = false);
|
||||
std::string disassemble_quest_script(
|
||||
const void* data,
|
||||
size_t size,
|
||||
Version version,
|
||||
uint8_t override_language = 0xFF,
|
||||
bool reassembly_mode = false,
|
||||
bool use_qedit_names = false);
|
||||
std::string assemble_quest_script(const std::string& text, const std::string& include_directory);
|
||||
|
||||
Episode find_quest_episode_from_script(const void* data, size_t size, Version version);
|
||||
|
||||
+96
-44
@@ -141,12 +141,14 @@ void send_first_pre_lobby_commands(shared_ptr<Client> c, std::function<void()> o
|
||||
|
||||
auto s = c->require_server_state();
|
||||
|
||||
size_t num_patches_sent = 0;
|
||||
c->config.set_flag(Client::Flag::HAS_AUTO_PATCHES);
|
||||
send_update_client_config(c, false);
|
||||
|
||||
vector<shared_ptr<const CompiledFunctionCode>> functions_to_send;
|
||||
if (c->version() == Version::BB_V4) {
|
||||
for (const auto& patch_name : s->bb_required_patches) {
|
||||
try {
|
||||
send_function_call(c, s->function_code_index->get_patch(patch_name, c->config.specific_version));
|
||||
num_patches_sent++;
|
||||
functions_to_send.emplace_back(s->function_code_index->get_patch(patch_name, c->config.specific_version));
|
||||
} catch (const out_of_range&) {
|
||||
string message = phosg::string_printf(
|
||||
"Your client is not compatible with a\nrequired patch on this server.\n\nClient version: %08" PRIX32 "\nPatch name: %s", c->config.specific_version, patch_name.c_str());
|
||||
@@ -158,39 +160,52 @@ void send_first_pre_lobby_commands(shared_ptr<Client> c, std::function<void()> o
|
||||
}
|
||||
for (const auto& patch_name : c->login->account->auto_patches_enabled) {
|
||||
try {
|
||||
send_function_call(c, s->function_code_index->get_patch(patch_name, c->config.specific_version));
|
||||
num_patches_sent++;
|
||||
functions_to_send.emplace_back(s->function_code_index->get_patch(patch_name, c->config.specific_version));
|
||||
} catch (const out_of_range&) {
|
||||
c->log.warning("Client has auto patch %s enabled, but it is not available for specific_version %08" PRIX32,
|
||||
patch_name.c_str(), c->config.specific_version);
|
||||
}
|
||||
}
|
||||
|
||||
// We can't just blast all the commands at the client, since the sequence
|
||||
// ends with a reconnect command, which changes the encryption state! We
|
||||
// have to wait until all the patch responses have been received before
|
||||
// sending any command that the client will respond to. It's OK to send
|
||||
// the 04 immediately before the 19 (since the client does not respond to
|
||||
// 04), but it is not OK to send the B2s without waiting.
|
||||
|
||||
if (num_patches_sent == 0) {
|
||||
c->config.set_flag(Client::Flag::HAS_AUTO_PATCHES);
|
||||
send_update_client_config(c, false);
|
||||
if (functions_to_send.empty()) {
|
||||
on_complete();
|
||||
|
||||
} else {
|
||||
while (c->function_call_response_queue.size() < num_patches_sent - 1) {
|
||||
c->function_call_response_queue.emplace_back(empty_function_call_response_handler);
|
||||
}
|
||||
c->function_call_response_queue.emplace_back([wc, on_complete = std::move(on_complete)](uint32_t, uint32_t) {
|
||||
auto last_handler = [wc, on_complete = std::move(on_complete)](uint32_t, uint32_t) {
|
||||
auto c = wc.lock();
|
||||
if (!c) {
|
||||
return;
|
||||
if (c) {
|
||||
on_complete();
|
||||
}
|
||||
c->config.set_flag(Client::Flag::HAS_AUTO_PATCHES);
|
||||
send_update_client_config(c, false);
|
||||
on_complete();
|
||||
});
|
||||
};
|
||||
|
||||
// On most platforms, we can just blast all the commands to the client
|
||||
// at once, and just delay the 19 (reconnect) command until all the
|
||||
// responses come in. But in Xbox we can't do this, since apparently it
|
||||
// messes up connection state somehow. So, for Xbox clients, we send
|
||||
// the patches one at a time.
|
||||
|
||||
if (c->version() != Version::XB_V3) {
|
||||
for (const auto& fn : functions_to_send) {
|
||||
send_function_call(c, fn);
|
||||
}
|
||||
for (size_t z = 0; z < functions_to_send.size() - 1; z++) {
|
||||
c->function_call_response_queue.emplace_back(empty_function_call_response_handler);
|
||||
}
|
||||
c->function_call_response_queue.emplace_back(last_handler);
|
||||
|
||||
} else {
|
||||
auto send_patch = [wc](shared_ptr<const CompiledFunctionCode> fn, uint32_t, uint32_t) {
|
||||
auto c = wc.lock();
|
||||
if (c) {
|
||||
send_function_call(c, fn);
|
||||
}
|
||||
};
|
||||
send_function_call(c, functions_to_send[0]);
|
||||
for (size_t z = 1; z < functions_to_send.size(); z++) {
|
||||
std::function<void(uint32_t, uint32_t)> bound = std::bind(send_patch, functions_to_send[z], placeholders::_1, placeholders::_2);
|
||||
c->function_call_response_queue.emplace_back(std::move(bound));
|
||||
}
|
||||
c->function_call_response_queue.emplace_back(last_handler);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -219,6 +234,11 @@ void send_client_to_lobby_server(shared_ptr<Client> c) {
|
||||
}
|
||||
|
||||
void send_client_to_proxy_server(shared_ptr<Client> c) {
|
||||
auto s = c->require_server_state();
|
||||
if (!s->proxy_server) {
|
||||
throw logic_error("send_client_to_proxy_server called without proxy server present");
|
||||
}
|
||||
|
||||
send_first_pre_lobby_commands(c, [wc = weak_ptr(c)]() {
|
||||
auto c = wc.lock();
|
||||
if (!c) {
|
||||
@@ -599,6 +619,8 @@ static void on_DB_V3(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
send_command(c, 0x9A, 0x01);
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
send_command(c, 0x9A, 0x04);
|
||||
} catch (const AccountIndex::account_banned& e) {
|
||||
send_command(c, 0x9A, 0x0F);
|
||||
}
|
||||
|
||||
c->should_disconnect = !c->login;
|
||||
@@ -624,6 +646,8 @@ static void on_88_DCNTE(shared_ptr<Client> c, uint16_t, uint32_t, string& data)
|
||||
send_message_box(c, "Incorrect access key");
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
send_message_box(c, "Incorrect serial number");
|
||||
} catch (const AccountIndex::account_banned& e) {
|
||||
send_message_box(c, "Account is banned");
|
||||
}
|
||||
|
||||
c->should_disconnect = !c->login;
|
||||
@@ -647,6 +671,8 @@ static void on_8B_DCNTE(shared_ptr<Client> c, uint16_t, uint32_t, string& data)
|
||||
send_message_box(c, "Incorrect access key");
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
send_message_box(c, "Incorrect serial number");
|
||||
} catch (const AccountIndex::account_banned& e) {
|
||||
send_message_box(c, "Account is banned");
|
||||
}
|
||||
|
||||
if (!c->login) {
|
||||
@@ -692,6 +718,8 @@ static void on_90_DC(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
send_command(c, 0x90, 0x03);
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
send_command(c, 0x90, 0x03);
|
||||
} catch (const AccountIndex::account_banned& e) {
|
||||
send_command(c, 0x90, 0x0F);
|
||||
}
|
||||
c->should_disconnect = !c->login;
|
||||
}
|
||||
@@ -739,6 +767,8 @@ static void on_93_DC(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
send_message_box(c, "Incorrect access key");
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
send_message_box(c, "Incorrect serial number");
|
||||
} catch (const AccountIndex::account_banned& e) {
|
||||
send_message_box(c, "Account is banned");
|
||||
}
|
||||
if (!c->login) {
|
||||
c->should_disconnect = true;
|
||||
@@ -833,6 +863,8 @@ static void on_9A(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
send_command(c, 0x9A, 0x01);
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
send_command(c, 0x9A, 0x03);
|
||||
} catch (const AccountIndex::account_banned& e) {
|
||||
send_command(c, 0x9A, 0x0F);
|
||||
}
|
||||
|
||||
c->should_disconnect = !c->login;
|
||||
@@ -875,6 +907,8 @@ static void on_9C(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
send_command(c, 0x9C, 0x00);
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
send_command(c, 0x9C, 0x00);
|
||||
} catch (const AccountIndex::account_banned& e) {
|
||||
send_message_box(c, "Account is banned");
|
||||
}
|
||||
c->should_disconnect = !c->login;
|
||||
}
|
||||
@@ -974,6 +1008,8 @@ static void on_9D_9E(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
|
||||
send_command(c, 0x04, 0x06);
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
send_command(c, 0x04, 0x04);
|
||||
} catch (const AccountIndex::account_banned& e) {
|
||||
send_command(c, 0x04, 0x04);
|
||||
}
|
||||
|
||||
if (!c->login) {
|
||||
@@ -1028,6 +1064,8 @@ static void on_9E_XB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
send_command(c, 0x04, 0x03);
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
send_command(c, 0x04, 0x03);
|
||||
} catch (const AccountIndex::account_banned& e) {
|
||||
send_command(c, 0x04, 0x04);
|
||||
}
|
||||
|
||||
if (!c->login) {
|
||||
@@ -1056,13 +1094,14 @@ static void on_93_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
|
||||
try {
|
||||
c->login = s->account_index->from_bb_credentials(username, &password, s->allow_unregistered_users);
|
||||
|
||||
} catch (const AccountIndex::no_username& e) {
|
||||
send_message_box(c, "Username is missing");
|
||||
send_client_init_bb(c, 0x08);
|
||||
} catch (const AccountIndex::incorrect_password& e) {
|
||||
send_message_box(c, "Incorrect login password");
|
||||
send_client_init_bb(c, 0x03);
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
send_message_box(c, "You are not registered on this server");
|
||||
send_client_init_bb(c, 0x08);
|
||||
} catch (const AccountIndex::account_banned& e) {
|
||||
send_client_init_bb(c, 0x06);
|
||||
}
|
||||
if (!c->login) {
|
||||
c->should_disconnect = true;
|
||||
@@ -2997,8 +3036,8 @@ static void on_AA(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Send the right value here. (When should we send function_id2?)
|
||||
send_quest_function_call(c, cmd.function_id1);
|
||||
// TODO: Send the right value here. (When should we send label2?)
|
||||
send_quest_function_call(c, cmd.label1);
|
||||
}
|
||||
|
||||
static void on_D7_GC(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
@@ -3008,8 +3047,8 @@ static void on_D7_GC(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
send_command(c, 0xD7, 0x00);
|
||||
} else {
|
||||
try {
|
||||
static FileContentsCache gba_file_cache(300 * 1000 * 1000);
|
||||
auto f = gba_file_cache.get_or_load("system/gba/" + filename).file;
|
||||
auto s = c->require_server_state();
|
||||
auto f = s->gba_files_cache->get_or_load("system/gba/" + filename).file;
|
||||
send_open_quest_file(c, "", filename, "", 0, QuestFileType::GBA_DEMO, f->data);
|
||||
} catch (const out_of_range&) {
|
||||
send_command(c, 0xD7, 0x00);
|
||||
@@ -3049,10 +3088,20 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
|
||||
// 98 should only be sent when leaving a game, and we should leave the client
|
||||
// in no lobby (they will send an 84 soon afterward to choose a lobby).
|
||||
if (command == 0x98) {
|
||||
// If the client had an overlay (for battle/challenge modes), delete it
|
||||
// Clear all temporary state from the game
|
||||
c->delete_overlay();
|
||||
c->telepipe_lobby_id = 0;
|
||||
s->remove_client_from_lobby(c);
|
||||
c->config.clear_flag(Client::Flag::LOADING);
|
||||
c->config.clear_flag(Client::Flag::LOADING_QUEST);
|
||||
c->config.clear_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
|
||||
c->config.clear_flag(Client::Flag::LOADING_TOURNAMENT);
|
||||
c->config.clear_flag(Client::Flag::AT_BANK_COUNTER);
|
||||
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE);
|
||||
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE);
|
||||
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_OBJECT_STATE);
|
||||
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
|
||||
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_PLAYER_STATES);
|
||||
}
|
||||
|
||||
auto player = c->character();
|
||||
@@ -3367,7 +3416,7 @@ static void on_30(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
shared_ptr<PSOBBCharacterFile> bb_char;
|
||||
switch (c->version()) {
|
||||
case Version::DC_V2:
|
||||
bb_char = PSOBBCharacterFile::create_from_dc_v2(check_size_t<PSODCV2CharacterFile>(data));
|
||||
bb_char = PSOBBCharacterFile::create_from_dc_v2(check_size_t<PSODCV2CharacterFile::Character>(data));
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
bb_char = PSOBBCharacterFile::create_from_gc_nte(check_size_t<PSOGCNTECharacterFileCharacter>(data));
|
||||
@@ -3798,13 +3847,12 @@ static void on_E7_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
p->challenge_records = cmd.char_file.challenge_records;
|
||||
p->battle_records = cmd.char_file.battle_records;
|
||||
p->death_count = cmd.char_file.death_count;
|
||||
*c->system_file() = cmd.system_file.base;
|
||||
*c->system_file() = cmd.system_file;
|
||||
}
|
||||
|
||||
static void on_E2_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
auto& cmd = check_size_t<PSOBBFullSystemFile>(data);
|
||||
auto sys = c->system_file();
|
||||
*sys = cmd.base;
|
||||
const auto& cmd = check_size_t<S_SyncSystemFile_BB_E2>(data);
|
||||
*c->system_file() = cmd.system_file;
|
||||
c->save_system_file();
|
||||
|
||||
S_SystemFileCreated_00E1_BB out_cmd = {1};
|
||||
@@ -4522,8 +4570,9 @@ static void on_0C_C1_E7_EC(shared_ptr<Client> c, uint16_t command, uint32_t, str
|
||||
|
||||
GameMode mode = GameMode::NORMAL;
|
||||
bool spectators_forbidden = false;
|
||||
if (cmd.battle_mode) {
|
||||
if (cmd.battle_mode || c->config.check_flag(Client::Flag::FORCE_BATTLE_MODE_GAME)) {
|
||||
mode = GameMode::BATTLE;
|
||||
c->config.clear_flag(Client::Flag::FORCE_BATTLE_MODE_GAME);
|
||||
}
|
||||
if (cmd.challenge_mode) {
|
||||
if (client_is_ep3) {
|
||||
@@ -4860,8 +4909,11 @@ static void on_D2_V3_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data)
|
||||
}
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto complete_trade_for_side = [&](shared_ptr<Client> to_c, shared_ptr<Client> from_c) {
|
||||
if (c->version() == Version::BB_V4) {
|
||||
auto complete_trade_for_side = +[](shared_ptr<Client> to_c, shared_ptr<Client> from_c) {
|
||||
auto l = to_c->require_lobby();
|
||||
auto s = to_c->require_server_state();
|
||||
|
||||
if (to_c->version() == Version::BB_V4) {
|
||||
// On BB, the server is expected to generate the delete item and create
|
||||
// item commands
|
||||
auto to_p = to_c->character();
|
||||
@@ -4889,7 +4941,7 @@ static void on_D2_V3_BB(shared_ptr<Client> c, uint16_t, uint32_t, string& data)
|
||||
} else {
|
||||
// On V3, the clients will handle it; we just send their final trade lists
|
||||
// to each other
|
||||
send_execute_item_trade(to_c, target_c->pending_item_trade->items);
|
||||
send_execute_item_trade(to_c, from_c->pending_item_trade->items);
|
||||
}
|
||||
|
||||
send_command(to_c, 0xD4, 0x01);
|
||||
|
||||
+313
-226
@@ -994,7 +994,7 @@ void Parsed6x70Data::clear_dc_protos_unused_item_fields() {
|
||||
}
|
||||
|
||||
Parsed6x70Data::Parsed6x70Data(
|
||||
const G_SyncPlayerDispAndInventory_BaseV1& base,
|
||||
const G_6x70_Base_V1& base,
|
||||
uint32_t guild_card_number,
|
||||
Version from_version,
|
||||
bool from_client_customization)
|
||||
@@ -1004,7 +1004,11 @@ Parsed6x70Data::Parsed6x70Data(
|
||||
base(base.base),
|
||||
bonus_hp_from_materials(base.bonus_hp_from_materials),
|
||||
bonus_tp_from_materials(base.bonus_tp_from_materials),
|
||||
unknown_a4_final(base.unknown_a4),
|
||||
permanent_status_effect(base.permanent_status_effect),
|
||||
temporary_status_effect(base.temporary_status_effect),
|
||||
attack_status_effect(base.attack_status_effect),
|
||||
defense_status_effect(base.defense_status_effect),
|
||||
unused_status_effect(base.unused_status_effect),
|
||||
language(base.language),
|
||||
player_tag(base.player_tag),
|
||||
guild_card_number(guild_card_number), // Ignore the client's GC#
|
||||
@@ -1018,12 +1022,16 @@ Parsed6x70Data::Parsed6x70Data(
|
||||
technique_levels_v1(base.technique_levels_v1),
|
||||
visual(base.visual) {}
|
||||
|
||||
G_SyncPlayerDispAndInventory_BaseV1 Parsed6x70Data::base_v1() const {
|
||||
G_SyncPlayerDispAndInventory_BaseV1 ret;
|
||||
G_6x70_Base_V1 Parsed6x70Data::base_v1() const {
|
||||
G_6x70_Base_V1 ret;
|
||||
ret.base = this->base;
|
||||
ret.bonus_hp_from_materials = this->bonus_hp_from_materials;
|
||||
ret.bonus_tp_from_materials = this->bonus_tp_from_materials;
|
||||
ret.unknown_a4 = this->unknown_a4_final;
|
||||
ret.permanent_status_effect = this->permanent_status_effect;
|
||||
ret.temporary_status_effect = this->temporary_status_effect;
|
||||
ret.attack_status_effect = this->attack_status_effect;
|
||||
ret.defense_status_effect = this->defense_status_effect;
|
||||
ret.unused_status_effect = this->unused_status_effect;
|
||||
ret.language = this->language;
|
||||
ret.player_tag = this->player_tag;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
@@ -1164,14 +1172,6 @@ static void on_forward_check_ep3_lobby(shared_ptr<Client> c, uint8_t command, ui
|
||||
}
|
||||
}
|
||||
|
||||
static void on_forward_check_ep3_game(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
check_size_t<G_UnusedHeader>(data, size, 0xFFFF);
|
||||
auto l = c->require_lobby();
|
||||
if (l->is_game() && l->is_ep3()) {
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Ep3 subcommands
|
||||
|
||||
@@ -1194,11 +1194,30 @@ static void on_ep3_battle_subs(shared_ptr<Client> c, uint8_t command, uint8_t fl
|
||||
|
||||
if (header.subsubcommand == 0x1A) {
|
||||
return;
|
||||
} else if (header.subsubcommand == 0x20) {
|
||||
const auto& cmd = check_size_t<G_Unknown_Ep3_6xB5x20>(data, size);
|
||||
if (cmd.client_id >= 12) {
|
||||
return;
|
||||
}
|
||||
} else if (header.subsubcommand == 0x31) {
|
||||
const auto& cmd = check_size_t<G_ConfirmDeckSelection_Ep3_6xB5x31>(data, size);
|
||||
if (cmd.menu_type >= 0x15) {
|
||||
return;
|
||||
}
|
||||
} else if (header.subsubcommand == 0x32) {
|
||||
const auto& cmd = check_size_t<G_MoveSharedMenuCursor_Ep3_6xB5x32>(data, size);
|
||||
if (cmd.menu_type >= 0x15) {
|
||||
return;
|
||||
}
|
||||
} else if (header.subsubcommand == 0x36) {
|
||||
const auto& cmd = check_size_t<G_RecreatePlayer_Ep3_6xB5x36>(data, size);
|
||||
if (l->is_game() && (cmd.client_id >= 4)) {
|
||||
return;
|
||||
}
|
||||
} else if (header.subsubcommand == 0x38) {
|
||||
c->config.set_flag(Client::Flag::EP3_ALLOW_6xBC);
|
||||
} else if (header.subsubcommand == 0x3C) {
|
||||
c->config.clear_flag(Client::Flag::EP3_ALLOW_6xBC);
|
||||
}
|
||||
|
||||
if (!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING) && (c->version() != Version::GC_EP3_NTE)) {
|
||||
@@ -1208,6 +1227,28 @@ static void on_ep3_battle_subs(shared_ptr<Client> c, uint8_t command, uint8_t fl
|
||||
forward_subcommand(c, command, flag, data.data(), data.size());
|
||||
}
|
||||
|
||||
static void on_ep3_trade_card_counts(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
if (c->version() == Version::GC_EP3_NTE) {
|
||||
check_size_t<G_CardCounts_Ep3NTE_6xBC>(data, size, 0xFFFF);
|
||||
} else {
|
||||
check_size_t<G_CardCounts_Ep3_6xBC>(data, size, 0xFFFF);
|
||||
}
|
||||
|
||||
if (!command_is_private(command)) {
|
||||
return;
|
||||
}
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game() || !l->is_ep3()) {
|
||||
return;
|
||||
}
|
||||
auto target = l->clients.at(flag);
|
||||
if (!target || !target->config.check_flag(Client::Flag::EP3_ALLOW_6xBC)) {
|
||||
return;
|
||||
}
|
||||
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Chat commands and the like
|
||||
|
||||
@@ -1410,7 +1451,6 @@ static void on_change_floor_6x1F(shared_ptr<Client> c, uint8_t command, uint8_t
|
||||
const auto& cmd = check_size_t<G_SetPlayerFloor_6x1F>(data, size);
|
||||
if (cmd.floor >= 0 && c->floor != static_cast<uint32_t>(cmd.floor)) {
|
||||
c->floor = cmd.floor;
|
||||
c->recent_switch_flags.clear();
|
||||
}
|
||||
}
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
@@ -1420,7 +1460,6 @@ static void on_change_floor_6x21(shared_ptr<Client> c, uint8_t command, uint8_t
|
||||
const auto& cmd = check_size_t<G_InterLevelWarp_6x21>(data, size);
|
||||
if (cmd.floor >= 0 && c->floor != static_cast<uint32_t>(cmd.floor)) {
|
||||
c->floor = cmd.floor;
|
||||
c->recent_switch_flags.clear();
|
||||
}
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
@@ -1496,7 +1535,7 @@ static void on_received_condition(shared_ptr<Client> c, uint8_t command, uint8_t
|
||||
if (cmd.client_id == c->lobby_client_id) {
|
||||
bool player_cheats_enabled = l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE));
|
||||
if (player_cheats_enabled && c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
||||
send_remove_conditions(c);
|
||||
send_remove_negative_conditions(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1581,7 +1620,30 @@ static void on_switch_state_changed(shared_ptr<Client> c, uint8_t command, uint8
|
||||
return;
|
||||
}
|
||||
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
if (!l->quest &&
|
||||
(cmd.flags & 1) &&
|
||||
(cmd.header.object_id != 0xFFFF) &&
|
||||
(cmd.switch_flag_num < 0x100) &&
|
||||
c->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) {
|
||||
for (auto* door : l->map->doors_for_switch_flag(cmd.switch_flag_floor, cmd.switch_flag_num)) {
|
||||
if (door->game_flags & 0x0001) {
|
||||
continue;
|
||||
}
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message_printf(c, "$C5SWA K-%hX %02hhX %02hX",
|
||||
door->object_id, cmd.switch_flag_floor, cmd.switch_flag_num.load());
|
||||
}
|
||||
door->game_flags |= 1;
|
||||
|
||||
G_UpdateObjectState_6x0B cmd0B;
|
||||
cmd0B.header.subcommand = 0x0B;
|
||||
cmd0B.header.size = sizeof(cmd0B) / 4;
|
||||
cmd0B.header.client_id = door->object_id | 0x4000;
|
||||
cmd0B.flags = door->game_flags;
|
||||
cmd0B.object_index = door->object_id;
|
||||
send_command_t(l, 0x60, 0x00, cmd0B);
|
||||
}
|
||||
}
|
||||
|
||||
if (l->switch_flags) {
|
||||
if (cmd.flags & 1) {
|
||||
@@ -1597,15 +1659,7 @@ static void on_switch_state_changed(shared_ptr<Client> c, uint8_t command, uint8
|
||||
}
|
||||
}
|
||||
|
||||
if ((cmd.flags & 1) && cmd.header.object_id != 0xFFFF) {
|
||||
c->recent_switch_flags.add(cmd.switch_flag_num);
|
||||
if (!l->quest && c->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) {
|
||||
string commands = c->recent_switch_flags.enable_commands(c->floor);
|
||||
if (!commands.empty()) {
|
||||
send_command(c, 0x60, 0x00, commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
|
||||
static void on_play_sound_from_player(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
@@ -1640,7 +1694,6 @@ void on_movement_with_floor(shared_ptr<Client> c, uint8_t command, uint8_t flag,
|
||||
c->z = cmd.z;
|
||||
if (cmd.floor >= 0 && c->floor != static_cast<uint32_t>(cmd.floor)) {
|
||||
c->floor = cmd.floor;
|
||||
c->recent_switch_flags.clear();
|
||||
}
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
@@ -2059,7 +2112,8 @@ static void on_pick_up_item_generic(
|
||||
}
|
||||
}
|
||||
|
||||
if (fi->flags & 0x1000) {
|
||||
if (!c->login->account->check_user_flag(Account::UserFlag::DISABLE_DROP_NOTIFICATION_BROADCAST) &&
|
||||
(fi->flags & 0x1000)) {
|
||||
uint32_t pi = fi->data.primary_identifier();
|
||||
bool should_send_game_notif, should_send_global_notif;
|
||||
if (is_v1_or_v2(c->version()) && (c->version() != Version::GC_NTE)) {
|
||||
@@ -2322,12 +2376,46 @@ static void on_open_shop_bb_or_ep3_battle_subs(shared_ptr<Client> c, uint8_t com
|
||||
}
|
||||
}
|
||||
|
||||
bool validate_6xBB(G_SyncCardTradeServerState_Ep3_6xBB& cmd) {
|
||||
if ((cmd.header.client_id >= 4) || (cmd.slot > 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TTradeCardServer uses 4 to indicate the slot is empty, so we allow 4 in
|
||||
// the client ID checks below
|
||||
switch (cmd.what) {
|
||||
case 1:
|
||||
if (cmd.args[0] >= 5) {
|
||||
return false;
|
||||
}
|
||||
cmd.args[1] = 0;
|
||||
cmd.args[2] = 0;
|
||||
cmd.args[3] = 0;
|
||||
break;
|
||||
case 0:
|
||||
case 2:
|
||||
case 4:
|
||||
cmd.args.clear(0);
|
||||
break;
|
||||
case 3:
|
||||
if (cmd.args[0] >= 5 || cmd.args[1] >= 5) {
|
||||
return false;
|
||||
}
|
||||
cmd.args[2] = 0;
|
||||
cmd.args[3] = 0;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void on_open_bank_bb_or_card_trade_counter_ep3(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
auto l = c->require_lobby();
|
||||
if ((l->base_version == Version::BB_V4) && l->is_game()) {
|
||||
c->config.set_flag(Client::Flag::AT_BANK_COUNTER);
|
||||
send_bank(c);
|
||||
} else if (l->is_ep3()) {
|
||||
} else if (l->is_ep3() && validate_6xBB(check_size_t<G_SyncCardTradeServerState_Ep3_6xBB>(data, size))) {
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
}
|
||||
@@ -2415,16 +2503,13 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr<Client> c, uint
|
||||
}
|
||||
|
||||
} else if (is_ep3(c->version())) {
|
||||
const auto& cmd = check_size_t<G_PrivateWordSelect_Ep3_6xBD>(data, size);
|
||||
s->word_select_table->validate(cmd.message, c->version());
|
||||
|
||||
const auto& cmd = check_size_t<G_WordSelectDuringBattle_Ep3_6xBD>(data, size);
|
||||
G_WordSelectDuringBattle_Ep3_6xBD masked_cmd = {
|
||||
{0xBD, sizeof(G_WordSelectDuringBattle_Ep3_6xBD) >> 2, cmd.header.client_id},
|
||||
0x0001,
|
||||
0x0001,
|
||||
G_PrivateWordSelect_Ep3_6xBD masked_cmd = {
|
||||
{0xBD, sizeof(G_PrivateWordSelect_Ep3_6xBD) >> 2, cmd.header.client_id},
|
||||
// "Please use the Whispers function."
|
||||
{0x00C1, 0x02C7, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF},
|
||||
0x0000,
|
||||
0x0000,
|
||||
{0x0001, 0x0001, {0x00C1, 0x02C7, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF}, 0x0000, 0x0000},
|
||||
cmd.private_flags,
|
||||
{0, 0, 0}};
|
||||
|
||||
@@ -2579,26 +2664,26 @@ DropReconcileResult reconcile_drop_request_with_map(
|
||||
if (map) {
|
||||
map_object = &map->objects.at(cmd.entity_id);
|
||||
log.info("Drop check for K-%hX %c %s",
|
||||
map_object->object_id, res.ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->base_type));
|
||||
map_object->object_id, res.ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->args->base_type));
|
||||
if (cmd.floor != map_object->floor) {
|
||||
log.warning("Floor %02hhX from command does not match object\'s expected floor %02hhX", cmd.floor, map_object->floor);
|
||||
}
|
||||
if (is_v1_or_v2(version) && (version != Version::GC_NTE)) {
|
||||
// V1 and V2 don't have 6xA2, so we can't get ignore_def or the object
|
||||
// parameters from the client on those versions
|
||||
cmd.param3 = map_object->param3;
|
||||
cmd.param4 = map_object->param4;
|
||||
cmd.param5 = map_object->param5;
|
||||
cmd.param6 = map_object->param6;
|
||||
cmd.param3 = map_object->args->param3;
|
||||
cmd.param4 = map_object->args->param4;
|
||||
cmd.param5 = map_object->args->param5;
|
||||
cmd.param6 = map_object->args->param6;
|
||||
}
|
||||
bool object_ignore_def = (map_object->param1 > 0.0);
|
||||
bool object_ignore_def = (map_object->args->param1 > 0.0);
|
||||
if (res.ignore_def != object_ignore_def) {
|
||||
log.warning("ignore_def value %s from command does not match object\'s expected ignore_def %s (from p1=%g)",
|
||||
res.ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", map_object->param1);
|
||||
res.ignore_def ? "true" : "false", object_ignore_def ? "true" : "false", map_object->args->param1.load());
|
||||
}
|
||||
if (config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message_printf(client_channel, "$C5K-%hX %c %s",
|
||||
map_object->object_id, res.ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->base_type));
|
||||
map_object->object_id, res.ignore_def ? 'G' : 'S', Map::name_for_object_type(map_object->args->base_type));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2999,8 +3084,8 @@ static void on_set_entity_set_flag(shared_ptr<Client> c, uint8_t command, uint8_
|
||||
l->log.info("(W-%02hhX-%" PRIX32 " script) construct_objects %04hX %04hX", event->floor, event->event_id, section, group);
|
||||
for (auto* obj : l->map->get_objects(event->floor, section, group)) {
|
||||
if (!(obj->set_flags & 0x0A)) {
|
||||
l->log.info("(W-%02hhX-%" PRIX32 " script) Setting flags 0012 on object K-%hX", event->floor, event->event_id, obj->object_id);
|
||||
obj->set_flags |= 0x12;
|
||||
l->log.info("(W-%02hhX-%" PRIX32 " script) Setting flags 0010 on object K-%hX", event->floor, event->event_id, obj->object_id);
|
||||
obj->set_flags |= 0x10;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -3328,7 +3413,7 @@ static void on_level_up(shared_ptr<Client> c, uint8_t command, uint8_t flag, voi
|
||||
// increments the player's level by 1.
|
||||
auto p = c->character();
|
||||
if (is_pre_v1(c->version())) {
|
||||
check_size_t<G_LevelUp_DCNTE_6x30>(data, size);
|
||||
check_size_t<G_ChangePlayerLevel_DCNTE_6x30>(data, size);
|
||||
auto s = c->require_server_state();
|
||||
auto level_table = s->level_table(c->version());
|
||||
const auto& level_incrs = level_table->stats_delta_for_level(p->disp.visual.char_class, p->disp.stats.level + 1);
|
||||
@@ -3341,7 +3426,7 @@ static void on_level_up(shared_ptr<Client> c, uint8_t command, uint8_t flag, voi
|
||||
p->disp.stats.char_stats.lck += level_incrs.lck;
|
||||
p->disp.stats.level++;
|
||||
} else {
|
||||
const auto& cmd = check_size_t<G_LevelUp_6x30>(data, size);
|
||||
const auto& cmd = check_size_t<G_ChangePlayerLevel_6x30>(data, size);
|
||||
p->disp.stats.char_stats.atp = cmd.atp;
|
||||
p->disp.stats.char_stats.mst = cmd.mst;
|
||||
p->disp.stats.char_stats.evp = cmd.evp;
|
||||
@@ -4121,7 +4206,6 @@ static void on_challenge_update_records(shared_ptr<Client> c, uint8_t command, u
|
||||
dc_cmd.header = cmd.header;
|
||||
dc_cmd.header.size = sizeof(G_SetChallengeRecords_DC_6x7C) >> 2;
|
||||
dc_cmd.client_id = cmd.client_id;
|
||||
dc_cmd.unknown_a1 = cmd.unknown_a1;
|
||||
dc_cmd.records = p->challenge_records;
|
||||
}
|
||||
data_to_send = dc_data.data();
|
||||
@@ -4133,7 +4217,6 @@ static void on_challenge_update_records(shared_ptr<Client> c, uint8_t command, u
|
||||
pc_cmd.header = cmd.header;
|
||||
pc_cmd.header.size = sizeof(G_SetChallengeRecords_PC_6x7C) >> 2;
|
||||
pc_cmd.client_id = cmd.client_id;
|
||||
pc_cmd.unknown_a1 = cmd.unknown_a1;
|
||||
pc_cmd.records = p->challenge_records;
|
||||
}
|
||||
data_to_send = pc_data.data();
|
||||
@@ -4145,7 +4228,6 @@ static void on_challenge_update_records(shared_ptr<Client> c, uint8_t command, u
|
||||
v3_cmd.header = cmd.header;
|
||||
v3_cmd.header.size = sizeof(G_SetChallengeRecords_V3_6x7C) >> 2;
|
||||
v3_cmd.client_id = cmd.client_id;
|
||||
v3_cmd.unknown_a1 = cmd.unknown_a1;
|
||||
v3_cmd.records = p->challenge_records;
|
||||
}
|
||||
data_to_send = v3_data.data();
|
||||
@@ -4157,7 +4239,6 @@ static void on_challenge_update_records(shared_ptr<Client> c, uint8_t command, u
|
||||
bb_cmd.header = cmd.header;
|
||||
bb_cmd.header.size = sizeof(G_SetChallengeRecords_BB_6x7C) >> 2;
|
||||
bb_cmd.client_id = cmd.client_id;
|
||||
bb_cmd.unknown_a1 = cmd.unknown_a1;
|
||||
bb_cmd.records = p->challenge_records;
|
||||
}
|
||||
data_to_send = bb_data.data();
|
||||
@@ -4214,11 +4295,11 @@ static void on_quest_exchange_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, vo
|
||||
p->add_item(new_item, limits);
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);
|
||||
|
||||
send_quest_function_call(c, cmd.success_function_id);
|
||||
send_quest_function_call(c, cmd.success_label);
|
||||
|
||||
} catch (const exception& e) {
|
||||
c->log.warning("Quest item exchange failed: %s", e.what());
|
||||
send_quest_function_call(c, cmd.failure_function_id);
|
||||
send_quest_function_call(c, cmd.failure_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4232,7 +4313,7 @@ static void on_wrap_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, void* data,
|
||||
auto p = c->character();
|
||||
auto item = p->remove_item(cmd.item.id, 1, *s->item_stack_limits(c->version()));
|
||||
send_destroy_item_to_lobby(c, item.id, 1);
|
||||
item.wrap(*s->item_stack_limits(c->version()));
|
||||
item.wrap(*s->item_stack_limits(c->version()), cmd.present_color);
|
||||
p->add_item(item, *s->item_stack_limits(c->version()));
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
}
|
||||
@@ -4260,11 +4341,11 @@ static void on_photon_drop_exchange_for_item_bb(shared_ptr<Client> c, uint8_t, u
|
||||
p->add_item(new_item, limits);
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);
|
||||
|
||||
send_quest_function_call(c, cmd.success_function_id);
|
||||
send_quest_function_call(c, cmd.success_label);
|
||||
|
||||
} catch (const exception& e) {
|
||||
c->log.warning("Quest Photon Drop exchange for item failed: %s", e.what());
|
||||
send_quest_function_call(c, cmd.failure_function_id);
|
||||
send_quest_function_call(c, cmd.failure_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4296,11 +4377,11 @@ static void on_photon_drop_exchange_for_s_rank_special_bb(shared_ptr<Client> c,
|
||||
p->add_item(item, limits);
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
|
||||
send_quest_function_call(c, cmd.success_function_id);
|
||||
send_quest_function_call(c, cmd.success_label);
|
||||
|
||||
} catch (const exception& e) {
|
||||
c->log.warning("Quest Photon Drop exchange for S-rank special failed: %s", e.what());
|
||||
send_quest_function_call(c, cmd.failure_function_id);
|
||||
send_quest_function_call(c, cmd.failure_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4350,7 +4431,7 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr<Client> c, uint8_t,
|
||||
|
||||
S_ExchangeSecretLotteryTicketResult_BB_24 out_cmd;
|
||||
out_cmd.start_index = cmd.index;
|
||||
out_cmd.function_id = cmd.function_id1;
|
||||
out_cmd.label = cmd.success_label;
|
||||
if (s->secret_lottery_results.empty()) {
|
||||
out_cmd.unknown_a3.clear(0);
|
||||
} else if (s->secret_lottery_results.size() == 1) {
|
||||
@@ -4375,6 +4456,8 @@ static void on_photon_crystal_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t
|
||||
size_t index = p->inventory.find_item_by_primary_identifier(0x03100200);
|
||||
auto item = p->remove_item(p->inventory.items[index].data.id, 1, *s->item_stack_limits(c->version()));
|
||||
send_destroy_item_to_lobby(c, item.id, 1);
|
||||
l->set_drop_mode(Lobby::DropMode::DISABLED);
|
||||
l->allowed_drop_modes = (1 << static_cast<uint8_t>(l->drop_mode)); // DISABLED only
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4446,7 +4529,7 @@ static void on_quest_F95F_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);
|
||||
|
||||
S_GallonPlanResult_BB_25 out_cmd;
|
||||
out_cmd.function_id = cmd.function_id1;
|
||||
out_cmd.label = cmd.success_label;
|
||||
out_cmd.offset1 = 0x3C;
|
||||
out_cmd.offset2 = 0x08;
|
||||
out_cmd.value1 = 0x00;
|
||||
@@ -4603,11 +4686,11 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr<Client> c, uint8_t, uint8_
|
||||
|
||||
send_destroy_item_to_lobby(c, item.id, 1);
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
send_quest_function_call(c, cmd.success_function_id);
|
||||
send_quest_function_call(c, cmd.success_label);
|
||||
|
||||
} catch (const exception& e) {
|
||||
c->log.warning("Weapon attribute upgrade failed: %s", e.what());
|
||||
send_quest_function_call(c, cmd.failure_function_id);
|
||||
send_quest_function_call(c, cmd.failure_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4619,6 +4702,10 @@ static void on_write_quest_counter_bb(shared_ptr<Client> c, uint8_t, uint8_t, vo
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// This makes it easier to see which handlers exist on which prototypes via
|
||||
// syntax highlighting
|
||||
constexpr uint8_t NONE = 0x00;
|
||||
|
||||
const SubcommandDefinition subcommand_definitions[0x100] = {
|
||||
/* 6x00 */ {0x00, 0x00, 0x00, on_invalid},
|
||||
/* 6x01 */ {0x01, 0x01, 0x01, on_invalid},
|
||||
@@ -4633,9 +4720,9 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
|
||||
/* 6x0A */ {0x0A, 0x0A, 0x0A, on_update_enemy_state},
|
||||
/* 6x0B */ {0x0B, 0x0B, 0x0B, on_update_object_state},
|
||||
/* 6x0C */ {0x0C, 0x0C, 0x0C, on_received_condition},
|
||||
/* 6x0D */ {0x00, 0x00, 0x0D, on_forward_check_game},
|
||||
/* 6x0E */ {0x00, 0x00, 0x0E, on_forward_check_game},
|
||||
/* 6x0F */ {0x00, 0x00, 0x0F, on_invalid},
|
||||
/* 6x0D */ {NONE, NONE, 0x0D, on_forward_check_game},
|
||||
/* 6x0E */ {NONE, NONE, 0x0E, on_forward_check_game},
|
||||
/* 6x0F */ {NONE, NONE, 0x0F, on_invalid},
|
||||
/* 6x10 */ {0x0E, 0x0E, 0x10, on_forward_check_game},
|
||||
/* 6x11 */ {0x0F, 0x0F, 0x11, on_forward_check_game},
|
||||
/* 6x12 */ {0x10, 0x10, 0x12, on_dragon_actions},
|
||||
@@ -4646,9 +4733,9 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
|
||||
/* 6x17 */ {0x15, 0x15, 0x17, on_forward_check_game},
|
||||
/* 6x18 */ {0x16, 0x16, 0x18, on_forward_check_game},
|
||||
/* 6x19 */ {0x17, 0x17, 0x19, on_forward_check_game},
|
||||
/* 6x1A */ {0x00, 0x00, 0x1A, on_invalid},
|
||||
/* 6x1B */ {0x00, 0x19, 0x1B, on_forward_check_game},
|
||||
/* 6x1C */ {0x00, 0x1A, 0x1C, on_forward_check_game},
|
||||
/* 6x1A */ {NONE, NONE, 0x1A, on_invalid},
|
||||
/* 6x1B */ {NONE, 0x19, 0x1B, on_forward_check_game},
|
||||
/* 6x1C */ {NONE, 0x1A, 0x1C, on_forward_check_game},
|
||||
/* 6x1D */ {0x19, 0x1B, 0x1D, on_invalid},
|
||||
/* 6x1E */ {0x1A, 0x1C, 0x1E, on_invalid},
|
||||
/* 6x1F */ {0x1B, 0x1D, 0x1F, on_change_floor_6x1F},
|
||||
@@ -4670,19 +4757,19 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
|
||||
/* 6x2F */ {0x2B, 0x2D, 0x2F, on_change_hp<G_ChangePlayerHP_6x2F>},
|
||||
/* 6x30 */ {0x2C, 0x2E, 0x30, on_level_up},
|
||||
/* 6x31 */ {0x2D, 0x2F, 0x31, on_forward_check_game},
|
||||
/* 6x32 */ {0x00, 0x00, 0x32, on_forward_check_game},
|
||||
/* 6x32 */ {NONE, NONE, 0x32, on_forward_check_game},
|
||||
/* 6x33 */ {0x2E, 0x30, 0x33, on_forward_check_game},
|
||||
/* 6x34 */ {0x2F, 0x31, 0x34, on_forward_check_game},
|
||||
/* 6x35 */ {0x30, 0x32, 0x35, on_invalid},
|
||||
/* 6x36 */ {0x00, 0x00, 0x36, on_forward_check_game},
|
||||
/* 6x36 */ {NONE, NONE, 0x36, on_forward_check_game},
|
||||
/* 6x37 */ {0x32, 0x33, 0x37, on_forward_check_game},
|
||||
/* 6x38 */ {0x33, 0x34, 0x38, on_forward_check_game},
|
||||
/* 6x39 */ {0x00, 0x36, 0x39, on_forward_check_game},
|
||||
/* 6x3A */ {0x00, 0x37, 0x3A, on_forward_check_game},
|
||||
/* 6x3B */ {0x00, 0x38, 0x3B, forward_subcommand_m},
|
||||
/* 6x39 */ {NONE, 0x36, 0x39, on_forward_check_game},
|
||||
/* 6x3A */ {NONE, 0x37, 0x3A, on_forward_check_game},
|
||||
/* 6x3B */ {NONE, 0x38, 0x3B, forward_subcommand_m},
|
||||
/* 6x3C */ {0x34, 0x39, 0x3C, forward_subcommand_m},
|
||||
/* 6x3D */ {0x00, 0x00, 0x3D, on_invalid},
|
||||
/* 6x3E */ {0x00, 0x00, 0x3E, on_movement_with_floor<G_StopAtPosition_6x3E>},
|
||||
/* 6x3D */ {NONE, NONE, 0x3D, on_invalid},
|
||||
/* 6x3E */ {NONE, NONE, 0x3E, on_movement_with_floor<G_StopAtPosition_6x3E>},
|
||||
/* 6x3F */ {0x36, 0x3B, 0x3F, on_movement_with_floor<G_SetPosition_6x3F>},
|
||||
/* 6x40 */ {0x37, 0x3C, 0x40, on_movement<G_WalkToPosition_6x40>},
|
||||
/* 6x41 */ {0x38, 0x3D, 0x41, forward_subcommand_m},
|
||||
@@ -4690,25 +4777,25 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
|
||||
/* 6x43 */ {0x3A, 0x3F, 0x43, on_forward_check_game_client},
|
||||
/* 6x44 */ {0x3B, 0x40, 0x44, on_forward_check_game_client},
|
||||
/* 6x45 */ {0x3C, 0x41, 0x45, on_forward_check_game_client},
|
||||
/* 6x46 */ {0x00, 0x42, 0x46, on_attack_finished},
|
||||
/* 6x46 */ {NONE, 0x42, 0x46, on_attack_finished},
|
||||
/* 6x47 */ {0x3D, 0x43, 0x47, on_cast_technique},
|
||||
/* 6x48 */ {0x00, 0x00, 0x48, on_cast_technique_finished},
|
||||
/* 6x48 */ {NONE, NONE, 0x48, on_cast_technique_finished},
|
||||
/* 6x49 */ {0x3E, 0x44, 0x49, on_execute_photon_blast},
|
||||
/* 6x4A */ {0x3F, 0x45, 0x4A, on_forward_check_game_client},
|
||||
/* 6x4B */ {0x40, 0x46, 0x4B, on_change_hp<G_ClientIDHeader>},
|
||||
/* 6x4C */ {0x41, 0x47, 0x4C, on_change_hp<G_ClientIDHeader>},
|
||||
/* 6x4D */ {0x42, 0x48, 0x4D, on_player_died},
|
||||
/* 6x4E */ {0x00, 0x00, 0x4E, on_player_revivable},
|
||||
/* 6x4E */ {NONE, NONE, 0x4E, on_player_revivable},
|
||||
/* 6x4F */ {0x43, 0x49, 0x4F, on_player_revived},
|
||||
/* 6x50 */ {0x44, 0x4A, 0x50, on_forward_check_game_client},
|
||||
/* 6x51 */ {0x00, 0x00, 0x51, on_invalid},
|
||||
/* 6x51 */ {NONE, NONE, 0x51, on_invalid},
|
||||
/* 6x52 */ {0x46, 0x4C, 0x52, on_set_animation_state},
|
||||
/* 6x53 */ {0x47, 0x4D, 0x53, on_forward_check_game},
|
||||
/* 6x54 */ {0x48, 0x4E, 0x54, forward_subcommand_m},
|
||||
/* 6x55 */ {0x49, 0x4F, 0x55, on_forward_check_game_client},
|
||||
/* 6x56 */ {0x4A, 0x50, 0x56, on_forward_check_game_client},
|
||||
/* 6x57 */ {0x00, 0x51, 0x57, on_forward_check_client},
|
||||
/* 6x58 */ {0x00, 0x00, 0x58, on_forward_check_client},
|
||||
/* 6x57 */ {NONE, 0x51, 0x57, on_forward_check_client},
|
||||
/* 6x58 */ {NONE, NONE, 0x58, on_forward_check_client},
|
||||
/* 6x59 */ {0x4B, 0x52, 0x59, on_pick_up_item},
|
||||
/* 6x5A */ {0x4C, 0x53, 0x5A, on_pick_up_item_request},
|
||||
/* 6x5B */ {0x4D, 0x54, 0x5B, forward_subcommand_m},
|
||||
@@ -4722,7 +4809,7 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
|
||||
/* 6x63 */ {0x55, 0x5C, 0x63, on_destroy_floor_item},
|
||||
/* 6x64 */ {0x56, 0x5D, 0x64, on_forward_check_game},
|
||||
/* 6x65 */ {0x57, 0x5E, 0x65, on_forward_check_game},
|
||||
/* 6x66 */ {0x00, 0x00, 0x66, on_forward_check_game},
|
||||
/* 6x66 */ {NONE, NONE, 0x66, on_forward_check_game},
|
||||
/* 6x67 */ {0x58, 0x5F, 0x67, on_trigger_set_event},
|
||||
/* 6x68 */ {0x59, 0x60, 0x68, on_update_telepipe_state},
|
||||
/* 6x69 */ {0x5A, 0x61, 0x69, on_npc_control},
|
||||
@@ -4731,151 +4818,151 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
|
||||
/* 6x6C */ {0x5D, 0x64, 0x6C, on_sync_joining_player_compressed_state},
|
||||
/* 6x6D */ {0x5E, 0x65, 0x6D, on_sync_joining_player_compressed_state},
|
||||
/* 6x6E */ {0x5F, 0x66, 0x6E, on_sync_joining_player_compressed_state},
|
||||
/* 6x6F */ {0x00, 0x00, 0x6F, on_sync_joining_player_quest_flags},
|
||||
/* 6x6F */ {NONE, NONE, 0x6F, on_sync_joining_player_quest_flags},
|
||||
/* 6x70 */ {0x60, 0x67, 0x70, on_sync_joining_player_disp_and_inventory},
|
||||
/* 6x71 */ {0x00, 0x00, 0x71, on_forward_check_game_loading},
|
||||
/* 6x71 */ {NONE, NONE, 0x71, on_forward_check_game_loading},
|
||||
/* 6x72 */ {0x61, 0x68, 0x72, on_forward_check_game_loading},
|
||||
/* 6x73 */ {0x00, 0x00, 0x73, on_forward_check_game_quest},
|
||||
/* 6x73 */ {NONE, NONE, 0x73, on_forward_check_game_quest},
|
||||
/* 6x74 */ {0x62, 0x69, 0x74, on_word_select, SDF::ALWAYS_FORWARD_TO_WATCHERS},
|
||||
/* 6x75 */ {0x00, 0x00, 0x75, on_set_quest_flag},
|
||||
/* 6x76 */ {0x00, 0x00, 0x76, on_set_entity_set_flag},
|
||||
/* 6x77 */ {0x00, 0x00, 0x77, on_sync_quest_register},
|
||||
/* 6x78 */ {0x00, 0x00, 0x78, forward_subcommand_m},
|
||||
/* 6x79 */ {0x00, 0x00, 0x79, on_forward_check_lobby},
|
||||
/* 6x7A */ {0x00, 0x00, 0x7A, on_forward_check_game_client},
|
||||
/* 6x7B */ {0x00, 0x00, 0x7B, forward_subcommand_m},
|
||||
/* 6x7C */ {0x00, 0x00, 0x7C, on_challenge_update_records},
|
||||
/* 6x7D */ {0x00, 0x00, 0x7D, on_forward_check_game},
|
||||
/* 6x7E */ {0x00, 0x00, 0x7E, forward_subcommand_m},
|
||||
/* 6x7F */ {0x00, 0x00, 0x7F, on_battle_scores},
|
||||
/* 6x80 */ {0x00, 0x00, 0x80, on_forward_check_game},
|
||||
/* 6x81 */ {0x00, 0x00, 0x81, on_forward_check_game},
|
||||
/* 6x82 */ {0x00, 0x00, 0x82, on_forward_check_game},
|
||||
/* 6x83 */ {0x00, 0x00, 0x83, on_forward_check_game},
|
||||
/* 6x84 */ {0x00, 0x00, 0x84, on_forward_check_game},
|
||||
/* 6x85 */ {0x00, 0x00, 0x85, on_forward_check_game},
|
||||
/* 6x86 */ {0x00, 0x00, 0x86, on_forward_check_game},
|
||||
/* 6x87 */ {0x00, 0x00, 0x87, on_forward_check_game},
|
||||
/* 6x88 */ {0x00, 0x00, 0x88, on_forward_check_game},
|
||||
/* 6x89 */ {0x00, 0x00, 0x89, on_forward_check_game},
|
||||
/* 6x8A */ {0x00, 0x00, 0x8A, on_forward_check_game},
|
||||
/* 6x8B */ {0x00, 0x00, 0x8B, on_forward_check_game},
|
||||
/* 6x8C */ {0x00, 0x00, 0x8C, on_forward_check_game},
|
||||
/* 6x8D */ {0x00, 0x00, 0x8D, on_forward_check_game_client},
|
||||
/* 6x8E */ {0x00, 0x00, 0x8E, on_forward_check_game},
|
||||
/* 6x8F */ {0x00, 0x00, 0x8F, on_forward_check_game},
|
||||
/* 6x90 */ {0x00, 0x00, 0x90, on_forward_check_game},
|
||||
/* 6x91 */ {0x00, 0x00, 0x91, on_unknown_6x91},
|
||||
/* 6x92 */ {0x00, 0x00, 0x92, on_forward_check_game},
|
||||
/* 6x93 */ {0x00, 0x00, 0x93, on_activate_timed_switch},
|
||||
/* 6x94 */ {0x00, 0x00, 0x94, on_warp},
|
||||
/* 6x95 */ {0x00, 0x00, 0x95, on_forward_check_game},
|
||||
/* 6x96 */ {0x00, 0x00, 0x96, on_forward_check_game},
|
||||
/* 6x97 */ {0x00, 0x00, 0x97, on_challenge_mode_retry_or_quit},
|
||||
/* 6x98 */ {0x00, 0x00, 0x98, on_forward_check_game},
|
||||
/* 6x99 */ {0x00, 0x00, 0x99, on_forward_check_game},
|
||||
/* 6x9A */ {0x00, 0x00, 0x9A, on_forward_check_game_client},
|
||||
/* 6x9B */ {0x00, 0x00, 0x9B, on_forward_check_game},
|
||||
/* 6x9C */ {0x00, 0x00, 0x9C, on_forward_check_game},
|
||||
/* 6x9D */ {0x00, 0x00, 0x9D, on_forward_check_game},
|
||||
/* 6x9E */ {0x00, 0x00, 0x9E, forward_subcommand_m},
|
||||
/* 6x9F */ {0x00, 0x00, 0x9F, on_forward_check_game},
|
||||
/* 6xA0 */ {0x00, 0x00, 0xA0, on_forward_check_game},
|
||||
/* 6xA1 */ {0x00, 0x00, 0xA1, on_forward_check_game},
|
||||
/* 6xA2 */ {0x00, 0x00, 0xA2, on_entity_drop_item_request},
|
||||
/* 6xA3 */ {0x00, 0x00, 0xA3, on_forward_check_game},
|
||||
/* 6xA4 */ {0x00, 0x00, 0xA4, on_forward_check_game},
|
||||
/* 6xA5 */ {0x00, 0x00, 0xA5, on_forward_check_game},
|
||||
/* 6xA6 */ {0x00, 0x00, 0xA6, on_forward_check_game},
|
||||
/* 6xA7 */ {0x00, 0x00, 0xA7, forward_subcommand_m},
|
||||
/* 6xA8 */ {0x00, 0x00, 0xA8, on_gol_dragon_actions},
|
||||
/* 6xA9 */ {0x00, 0x00, 0xA9, on_forward_check_game},
|
||||
/* 6xAA */ {0x00, 0x00, 0xAA, on_forward_check_game},
|
||||
/* 6xAB */ {0x00, 0x00, 0xAB, on_gc_nte_exclusive},
|
||||
/* 6xAC */ {0x00, 0x00, 0xAC, on_gc_nte_exclusive},
|
||||
/* 6xAD */ {0x00, 0x00, 0xAD, on_forward_check_game},
|
||||
/* 6xAE */ {0x00, 0x00, 0xAE, on_forward_check_client},
|
||||
/* 6xAF */ {0x00, 0x00, 0xAF, on_forward_check_lobby_client},
|
||||
/* 6xB0 */ {0x00, 0x00, 0xB0, on_forward_check_lobby_client},
|
||||
/* 6xB1 */ {0x00, 0x00, 0xB1, forward_subcommand_m},
|
||||
/* 6xB2 */ {0x00, 0x00, 0xB2, on_play_sound_from_player},
|
||||
/* 6xB3 */ {0x00, 0x00, 0xB3, on_xbox_voice_chat_control},
|
||||
/* 6xB4 */ {0x00, 0x00, 0xB4, on_xbox_voice_chat_control},
|
||||
/* 6xB5 */ {0x00, 0x00, 0xB5, on_open_shop_bb_or_ep3_battle_subs},
|
||||
/* 6xB6 */ {0x00, 0x00, 0xB6, on_invalid},
|
||||
/* 6xB7 */ {0x00, 0x00, 0xB7, on_buy_shop_item_bb},
|
||||
/* 6xB8 */ {0x00, 0x00, 0xB8, on_identify_item_bb},
|
||||
/* 6xB9 */ {0x00, 0x00, 0xB9, on_invalid},
|
||||
/* 6xBA */ {0x00, 0x00, 0xBA, on_accept_identify_item_bb},
|
||||
/* 6xBB */ {0x00, 0x00, 0xBB, on_open_bank_bb_or_card_trade_counter_ep3},
|
||||
/* 6xBC */ {0x00, 0x00, 0xBC, on_forward_check_ep3_game},
|
||||
/* 6xBD */ {0x00, 0x00, 0xBD, on_ep3_private_word_select_bb_bank_action, SDF::ALWAYS_FORWARD_TO_WATCHERS},
|
||||
/* 6xBE */ {0x00, 0x00, 0xBE, forward_subcommand_m, SDF::ALWAYS_FORWARD_TO_WATCHERS | SDF::ALLOW_FORWARD_TO_WATCHED_LOBBY},
|
||||
/* 6xBF */ {0x00, 0x00, 0xBF, on_forward_check_ep3_lobby},
|
||||
/* 6xC0 */ {0x00, 0x00, 0xC0, on_sell_item_at_shop_bb},
|
||||
/* 6xC1 */ {0x00, 0x00, 0xC1, forward_subcommand_m},
|
||||
/* 6xC2 */ {0x00, 0x00, 0xC2, forward_subcommand_m},
|
||||
/* 6xC3 */ {0x00, 0x00, 0xC3, on_drop_partial_stack_bb},
|
||||
/* 6xC4 */ {0x00, 0x00, 0xC4, on_sort_inventory_bb},
|
||||
/* 6xC5 */ {0x00, 0x00, 0xC5, on_medical_center_bb},
|
||||
/* 6xC6 */ {0x00, 0x00, 0xC6, on_steal_exp_bb},
|
||||
/* 6xC7 */ {0x00, 0x00, 0xC7, on_charge_attack_bb},
|
||||
/* 6xC8 */ {0x00, 0x00, 0xC8, on_enemy_exp_request_bb},
|
||||
/* 6xC9 */ {0x00, 0x00, 0xC9, on_adjust_player_meseta_bb},
|
||||
/* 6xCA */ {0x00, 0x00, 0xCA, on_item_reward_request_bb},
|
||||
/* 6xCB */ {0x00, 0x00, 0xCB, on_transfer_item_via_mail_message_bb},
|
||||
/* 6xCC */ {0x00, 0x00, 0xCC, on_exchange_item_for_team_points_bb},
|
||||
/* 6xCD */ {0x00, 0x00, 0xCD, forward_subcommand_m},
|
||||
/* 6xCE */ {0x00, 0x00, 0xCE, forward_subcommand_m},
|
||||
/* 6xCF */ {0x00, 0x00, 0xCF, on_battle_restart_bb},
|
||||
/* 6xD0 */ {0x00, 0x00, 0xD0, on_battle_level_up_bb},
|
||||
/* 6xD1 */ {0x00, 0x00, 0xD1, on_request_challenge_grave_recovery_item_bb},
|
||||
/* 6xD2 */ {0x00, 0x00, 0xD2, on_write_quest_counter_bb},
|
||||
/* 6xD3 */ {0x00, 0x00, 0xD3, on_invalid},
|
||||
/* 6xD4 */ {0x00, 0x00, 0xD4, on_forward_check_game},
|
||||
/* 6xD5 */ {0x00, 0x00, 0xD5, on_quest_exchange_item_bb},
|
||||
/* 6xD6 */ {0x00, 0x00, 0xD6, on_wrap_item_bb},
|
||||
/* 6xD7 */ {0x00, 0x00, 0xD7, on_photon_drop_exchange_for_item_bb},
|
||||
/* 6xD8 */ {0x00, 0x00, 0xD8, on_photon_drop_exchange_for_s_rank_special_bb},
|
||||
/* 6xD9 */ {0x00, 0x00, 0xD9, on_momoka_item_exchange_bb},
|
||||
/* 6xDA */ {0x00, 0x00, 0xDA, on_upgrade_weapon_attribute_bb},
|
||||
/* 6xDB */ {0x00, 0x00, 0xDB, on_invalid},
|
||||
/* 6xDC */ {0x00, 0x00, 0xDC, on_forward_check_game},
|
||||
/* 6xDD */ {0x00, 0x00, 0xDD, on_invalid},
|
||||
/* 6xDE */ {0x00, 0x00, 0xDE, on_secret_lottery_ticket_exchange_bb},
|
||||
/* 6xDF */ {0x00, 0x00, 0xDF, on_photon_crystal_exchange_bb},
|
||||
/* 6xE0 */ {0x00, 0x00, 0xE0, on_quest_F95E_result_bb},
|
||||
/* 6xE1 */ {0x00, 0x00, 0xE1, on_quest_F95F_result_bb},
|
||||
/* 6xE2 */ {0x00, 0x00, 0xE2, on_quest_F960_result_bb},
|
||||
/* 6xE3 */ {0x00, 0x00, 0xE3, on_invalid},
|
||||
/* 6xE4 */ {0x00, 0x00, 0xE4, on_invalid},
|
||||
/* 6xE5 */ {0x00, 0x00, 0xE5, on_invalid},
|
||||
/* 6xE6 */ {0x00, 0x00, 0xE6, on_invalid},
|
||||
/* 6xE7 */ {0x00, 0x00, 0xE7, on_invalid},
|
||||
/* 6xE8 */ {0x00, 0x00, 0xE8, on_invalid},
|
||||
/* 6xE9 */ {0x00, 0x00, 0xE9, on_invalid},
|
||||
/* 6xEA */ {0x00, 0x00, 0xEA, on_invalid},
|
||||
/* 6xEB */ {0x00, 0x00, 0xEB, on_invalid},
|
||||
/* 6xEC */ {0x00, 0x00, 0xEC, on_invalid},
|
||||
/* 6xED */ {0x00, 0x00, 0xED, on_invalid},
|
||||
/* 6xEE */ {0x00, 0x00, 0xEE, on_invalid},
|
||||
/* 6xEF */ {0x00, 0x00, 0xEF, on_invalid},
|
||||
/* 6xF0 */ {0x00, 0x00, 0xF0, on_invalid},
|
||||
/* 6xF1 */ {0x00, 0x00, 0xF1, on_invalid},
|
||||
/* 6xF2 */ {0x00, 0x00, 0xF2, on_invalid},
|
||||
/* 6xF3 */ {0x00, 0x00, 0xF3, on_invalid},
|
||||
/* 6xF4 */ {0x00, 0x00, 0xF4, on_invalid},
|
||||
/* 6xF5 */ {0x00, 0x00, 0xF5, on_invalid},
|
||||
/* 6xF6 */ {0x00, 0x00, 0xF6, on_invalid},
|
||||
/* 6xF7 */ {0x00, 0x00, 0xF7, on_invalid},
|
||||
/* 6xF8 */ {0x00, 0x00, 0xF8, on_invalid},
|
||||
/* 6xF9 */ {0x00, 0x00, 0xF9, on_invalid},
|
||||
/* 6xFA */ {0x00, 0x00, 0xFA, on_invalid},
|
||||
/* 6xFB */ {0x00, 0x00, 0xFB, on_invalid},
|
||||
/* 6xFC */ {0x00, 0x00, 0xFC, on_invalid},
|
||||
/* 6xFD */ {0x00, 0x00, 0xFD, on_invalid},
|
||||
/* 6xFE */ {0x00, 0x00, 0xFE, on_invalid},
|
||||
/* 6xFF */ {0x00, 0x00, 0xFF, on_invalid},
|
||||
/* 6x75 */ {NONE, NONE, 0x75, on_set_quest_flag},
|
||||
/* 6x76 */ {NONE, NONE, 0x76, on_set_entity_set_flag},
|
||||
/* 6x77 */ {NONE, NONE, 0x77, on_sync_quest_register},
|
||||
/* 6x78 */ {NONE, NONE, 0x78, forward_subcommand_m},
|
||||
/* 6x79 */ {NONE, NONE, 0x79, on_forward_check_lobby},
|
||||
/* 6x7A */ {NONE, NONE, 0x7A, on_forward_check_game_client},
|
||||
/* 6x7B */ {NONE, NONE, 0x7B, forward_subcommand_m},
|
||||
/* 6x7C */ {NONE, NONE, 0x7C, on_challenge_update_records},
|
||||
/* 6x7D */ {NONE, NONE, 0x7D, on_forward_check_game},
|
||||
/* 6x7E */ {NONE, NONE, 0x7E, forward_subcommand_m},
|
||||
/* 6x7F */ {NONE, NONE, 0x7F, on_battle_scores},
|
||||
/* 6x80 */ {NONE, NONE, 0x80, on_forward_check_game},
|
||||
/* 6x81 */ {NONE, NONE, 0x81, on_forward_check_game},
|
||||
/* 6x82 */ {NONE, NONE, 0x82, on_forward_check_game},
|
||||
/* 6x83 */ {NONE, NONE, 0x83, on_forward_check_game},
|
||||
/* 6x84 */ {NONE, NONE, 0x84, on_forward_check_game},
|
||||
/* 6x85 */ {NONE, NONE, 0x85, on_forward_check_game},
|
||||
/* 6x86 */ {NONE, NONE, 0x86, on_forward_check_game},
|
||||
/* 6x87 */ {NONE, NONE, 0x87, on_forward_check_game},
|
||||
/* 6x88 */ {NONE, NONE, 0x88, on_forward_check_game},
|
||||
/* 6x89 */ {NONE, NONE, 0x89, on_forward_check_game},
|
||||
/* 6x8A */ {NONE, NONE, 0x8A, on_forward_check_game},
|
||||
/* 6x8B */ {NONE, NONE, 0x8B, on_forward_check_game},
|
||||
/* 6x8C */ {NONE, NONE, 0x8C, on_forward_check_game},
|
||||
/* 6x8D */ {NONE, NONE, 0x8D, on_forward_check_game_client},
|
||||
/* 6x8E */ {NONE, NONE, 0x8E, on_forward_check_game},
|
||||
/* 6x8F */ {NONE, NONE, 0x8F, on_forward_check_game},
|
||||
/* 6x90 */ {NONE, NONE, 0x90, on_forward_check_game},
|
||||
/* 6x91 */ {NONE, NONE, 0x91, on_unknown_6x91},
|
||||
/* 6x92 */ {NONE, NONE, 0x92, on_forward_check_game},
|
||||
/* 6x93 */ {NONE, NONE, 0x93, on_activate_timed_switch},
|
||||
/* 6x94 */ {NONE, NONE, 0x94, on_warp},
|
||||
/* 6x95 */ {NONE, NONE, 0x95, on_forward_check_game},
|
||||
/* 6x96 */ {NONE, NONE, 0x96, on_forward_check_game},
|
||||
/* 6x97 */ {NONE, NONE, 0x97, on_challenge_mode_retry_or_quit},
|
||||
/* 6x98 */ {NONE, NONE, 0x98, on_forward_check_game},
|
||||
/* 6x99 */ {NONE, NONE, 0x99, on_forward_check_game},
|
||||
/* 6x9A */ {NONE, NONE, 0x9A, on_forward_check_game_client},
|
||||
/* 6x9B */ {NONE, NONE, 0x9B, on_forward_check_game},
|
||||
/* 6x9C */ {NONE, NONE, 0x9C, on_forward_check_game},
|
||||
/* 6x9D */ {NONE, NONE, 0x9D, on_forward_check_game},
|
||||
/* 6x9E */ {NONE, NONE, 0x9E, forward_subcommand_m},
|
||||
/* 6x9F */ {NONE, NONE, 0x9F, on_forward_check_game},
|
||||
/* 6xA0 */ {NONE, NONE, 0xA0, on_forward_check_game},
|
||||
/* 6xA1 */ {NONE, NONE, 0xA1, on_forward_check_game},
|
||||
/* 6xA2 */ {NONE, NONE, 0xA2, on_entity_drop_item_request},
|
||||
/* 6xA3 */ {NONE, NONE, 0xA3, on_forward_check_game},
|
||||
/* 6xA4 */ {NONE, NONE, 0xA4, on_forward_check_game},
|
||||
/* 6xA5 */ {NONE, NONE, 0xA5, on_forward_check_game},
|
||||
/* 6xA6 */ {NONE, NONE, 0xA6, on_forward_check_game},
|
||||
/* 6xA7 */ {NONE, NONE, 0xA7, forward_subcommand_m},
|
||||
/* 6xA8 */ {NONE, NONE, 0xA8, on_gol_dragon_actions},
|
||||
/* 6xA9 */ {NONE, NONE, 0xA9, on_forward_check_game},
|
||||
/* 6xAA */ {NONE, NONE, 0xAA, on_forward_check_game},
|
||||
/* 6xAB */ {NONE, NONE, 0xAB, on_gc_nte_exclusive},
|
||||
/* 6xAC */ {NONE, NONE, 0xAC, on_gc_nte_exclusive},
|
||||
/* 6xAD */ {NONE, NONE, 0xAD, on_forward_check_game},
|
||||
/* 6xAE */ {NONE, NONE, 0xAE, on_forward_check_client},
|
||||
/* 6xAF */ {NONE, NONE, 0xAF, on_forward_check_lobby_client},
|
||||
/* 6xB0 */ {NONE, NONE, 0xB0, on_forward_check_lobby_client},
|
||||
/* 6xB1 */ {NONE, NONE, 0xB1, forward_subcommand_m},
|
||||
/* 6xB2 */ {NONE, NONE, 0xB2, on_play_sound_from_player},
|
||||
/* 6xB3 */ {NONE, NONE, 0xB3, on_xbox_voice_chat_control}, // Ep3 6xBx commands are handled via on_CA_Ep3 instead
|
||||
/* 6xB4 */ {NONE, NONE, 0xB4, on_xbox_voice_chat_control},
|
||||
/* 6xB5 */ {NONE, NONE, 0xB5, on_open_shop_bb_or_ep3_battle_subs},
|
||||
/* 6xB6 */ {NONE, NONE, 0xB6, on_invalid},
|
||||
/* 6xB7 */ {NONE, NONE, 0xB7, on_buy_shop_item_bb},
|
||||
/* 6xB8 */ {NONE, NONE, 0xB8, on_identify_item_bb},
|
||||
/* 6xB9 */ {NONE, NONE, 0xB9, on_invalid},
|
||||
/* 6xBA */ {NONE, NONE, 0xBA, on_accept_identify_item_bb},
|
||||
/* 6xBB */ {NONE, NONE, 0xBB, on_open_bank_bb_or_card_trade_counter_ep3},
|
||||
/* 6xBC */ {NONE, NONE, 0xBC, on_ep3_trade_card_counts},
|
||||
/* 6xBD */ {NONE, NONE, 0xBD, on_ep3_private_word_select_bb_bank_action, SDF::ALWAYS_FORWARD_TO_WATCHERS},
|
||||
/* 6xBE */ {NONE, NONE, 0xBE, forward_subcommand_m, SDF::ALWAYS_FORWARD_TO_WATCHERS | SDF::ALLOW_FORWARD_TO_WATCHED_LOBBY},
|
||||
/* 6xBF */ {NONE, NONE, 0xBF, on_forward_check_ep3_lobby},
|
||||
/* 6xC0 */ {NONE, NONE, 0xC0, on_sell_item_at_shop_bb},
|
||||
/* 6xC1 */ {NONE, NONE, 0xC1, forward_subcommand_m},
|
||||
/* 6xC2 */ {NONE, NONE, 0xC2, forward_subcommand_m},
|
||||
/* 6xC3 */ {NONE, NONE, 0xC3, on_drop_partial_stack_bb},
|
||||
/* 6xC4 */ {NONE, NONE, 0xC4, on_sort_inventory_bb},
|
||||
/* 6xC5 */ {NONE, NONE, 0xC5, on_medical_center_bb},
|
||||
/* 6xC6 */ {NONE, NONE, 0xC6, on_steal_exp_bb},
|
||||
/* 6xC7 */ {NONE, NONE, 0xC7, on_charge_attack_bb},
|
||||
/* 6xC8 */ {NONE, NONE, 0xC8, on_enemy_exp_request_bb},
|
||||
/* 6xC9 */ {NONE, NONE, 0xC9, on_adjust_player_meseta_bb},
|
||||
/* 6xCA */ {NONE, NONE, 0xCA, on_item_reward_request_bb},
|
||||
/* 6xCB */ {NONE, NONE, 0xCB, on_transfer_item_via_mail_message_bb},
|
||||
/* 6xCC */ {NONE, NONE, 0xCC, on_exchange_item_for_team_points_bb},
|
||||
/* 6xCD */ {NONE, NONE, 0xCD, forward_subcommand_m},
|
||||
/* 6xCE */ {NONE, NONE, 0xCE, forward_subcommand_m},
|
||||
/* 6xCF */ {NONE, NONE, 0xCF, on_battle_restart_bb},
|
||||
/* 6xD0 */ {NONE, NONE, 0xD0, on_battle_level_up_bb},
|
||||
/* 6xD1 */ {NONE, NONE, 0xD1, on_request_challenge_grave_recovery_item_bb},
|
||||
/* 6xD2 */ {NONE, NONE, 0xD2, on_write_quest_counter_bb},
|
||||
/* 6xD3 */ {NONE, NONE, 0xD3, on_invalid},
|
||||
/* 6xD4 */ {NONE, NONE, 0xD4, on_forward_check_game},
|
||||
/* 6xD5 */ {NONE, NONE, 0xD5, on_quest_exchange_item_bb},
|
||||
/* 6xD6 */ {NONE, NONE, 0xD6, on_wrap_item_bb},
|
||||
/* 6xD7 */ {NONE, NONE, 0xD7, on_photon_drop_exchange_for_item_bb},
|
||||
/* 6xD8 */ {NONE, NONE, 0xD8, on_photon_drop_exchange_for_s_rank_special_bb},
|
||||
/* 6xD9 */ {NONE, NONE, 0xD9, on_momoka_item_exchange_bb},
|
||||
/* 6xDA */ {NONE, NONE, 0xDA, on_upgrade_weapon_attribute_bb},
|
||||
/* 6xDB */ {NONE, NONE, 0xDB, on_invalid},
|
||||
/* 6xDC */ {NONE, NONE, 0xDC, on_forward_check_game},
|
||||
/* 6xDD */ {NONE, NONE, 0xDD, on_invalid},
|
||||
/* 6xDE */ {NONE, NONE, 0xDE, on_secret_lottery_ticket_exchange_bb},
|
||||
/* 6xDF */ {NONE, NONE, 0xDF, on_photon_crystal_exchange_bb},
|
||||
/* 6xE0 */ {NONE, NONE, 0xE0, on_quest_F95E_result_bb},
|
||||
/* 6xE1 */ {NONE, NONE, 0xE1, on_quest_F95F_result_bb},
|
||||
/* 6xE2 */ {NONE, NONE, 0xE2, on_quest_F960_result_bb},
|
||||
/* 6xE3 */ {NONE, NONE, 0xE3, on_invalid},
|
||||
/* 6xE4 */ {NONE, NONE, 0xE4, on_invalid},
|
||||
/* 6xE5 */ {NONE, NONE, 0xE5, on_invalid},
|
||||
/* 6xE6 */ {NONE, NONE, 0xE6, on_invalid},
|
||||
/* 6xE7 */ {NONE, NONE, 0xE7, on_invalid},
|
||||
/* 6xE8 */ {NONE, NONE, 0xE8, on_invalid},
|
||||
/* 6xE9 */ {NONE, NONE, 0xE9, on_invalid},
|
||||
/* 6xEA */ {NONE, NONE, 0xEA, on_invalid},
|
||||
/* 6xEB */ {NONE, NONE, 0xEB, on_invalid},
|
||||
/* 6xEC */ {NONE, NONE, 0xEC, on_invalid},
|
||||
/* 6xED */ {NONE, NONE, 0xED, on_invalid},
|
||||
/* 6xEE */ {NONE, NONE, 0xEE, on_invalid},
|
||||
/* 6xEF */ {NONE, NONE, 0xEF, on_invalid},
|
||||
/* 6xF0 */ {NONE, NONE, 0xF0, on_invalid},
|
||||
/* 6xF1 */ {NONE, NONE, 0xF1, on_invalid},
|
||||
/* 6xF2 */ {NONE, NONE, 0xF2, on_invalid},
|
||||
/* 6xF3 */ {NONE, NONE, 0xF3, on_invalid},
|
||||
/* 6xF4 */ {NONE, NONE, 0xF4, on_invalid},
|
||||
/* 6xF5 */ {NONE, NONE, 0xF5, on_invalid},
|
||||
/* 6xF6 */ {NONE, NONE, 0xF6, on_invalid},
|
||||
/* 6xF7 */ {NONE, NONE, 0xF7, on_invalid},
|
||||
/* 6xF8 */ {NONE, NONE, 0xF8, on_invalid},
|
||||
/* 6xF9 */ {NONE, NONE, 0xF9, on_invalid},
|
||||
/* 6xFA */ {NONE, NONE, 0xFA, on_invalid},
|
||||
/* 6xFB */ {NONE, NONE, 0xFB, on_invalid},
|
||||
/* 6xFC */ {NONE, NONE, 0xFC, on_invalid},
|
||||
/* 6xFD */ {NONE, NONE, 0xFD, on_invalid},
|
||||
/* 6xFE */ {NONE, NONE, 0xFE, on_invalid},
|
||||
/* 6xFF */ {NONE, NONE, 0xFF, on_invalid},
|
||||
};
|
||||
|
||||
void on_subcommand_multi(shared_ptr<Client> c, uint8_t command, uint8_t flag, string& data) {
|
||||
|
||||
@@ -43,22 +43,26 @@ public:
|
||||
bool from_client_customization;
|
||||
Version item_version;
|
||||
|
||||
G_SyncPlayerDispAndInventory_BaseDCNTE base;
|
||||
G_6x70_Base_DCNTE base;
|
||||
uint32_t unknown_a5_nte = 0;
|
||||
uint32_t unknown_a6_nte = 0;
|
||||
uint16_t bonus_hp_from_materials = 0;
|
||||
uint16_t bonus_tp_from_materials = 0;
|
||||
parray<uint8_t, 0x10> unknown_a5_112000;
|
||||
parray<G_Unknown_6x70_SubA2, 5> unknown_a4_final;
|
||||
G_6x70_StatusEffectState permanent_status_effect;
|
||||
G_6x70_StatusEffectState temporary_status_effect;
|
||||
G_6x70_StatusEffectState attack_status_effect;
|
||||
G_6x70_StatusEffectState defense_status_effect;
|
||||
G_6x70_StatusEffectState unused_status_effect;
|
||||
uint32_t language = 0;
|
||||
uint32_t player_tag = 0;
|
||||
uint32_t guild_card_number = 0;
|
||||
uint32_t unknown_a6 = 0;
|
||||
uint32_t battle_team_number = 0;
|
||||
Telepipe6x70 telepipe;
|
||||
G_6x70_Sub_Telepipe telepipe;
|
||||
uint32_t unknown_a8 = 0;
|
||||
parray<uint8_t, 0x10> unknown_a9_nte_112000;
|
||||
G_Unknown_6x70_SubA1 unknown_a9_final;
|
||||
G_6x70_Sub_UnknownA1 unknown_a9_final;
|
||||
uint32_t area = 0;
|
||||
uint32_t flags2 = 0;
|
||||
parray<uint8_t, 0x14> technique_levels_v1 = 0xFF;
|
||||
@@ -116,9 +120,11 @@ public:
|
||||
|
||||
protected:
|
||||
Parsed6x70Data(
|
||||
const G_SyncPlayerDispAndInventory_BaseV1& base,
|
||||
const G_6x70_Base_V1& base,
|
||||
uint32_t guild_card_number,
|
||||
Version from_version,
|
||||
bool from_client_customization);
|
||||
G_SyncPlayerDispAndInventory_BaseV1 base_v1() const;
|
||||
G_6x70_Base_V1 base_v1() const;
|
||||
};
|
||||
|
||||
bool validate_6xBB(G_SyncCardTradeServerState_Ep3_6xBB& cmd);
|
||||
|
||||
+74
-7
@@ -143,6 +143,16 @@ bool PSOVMSFileHeader::checksum_correct() const {
|
||||
return (crc == this->crc);
|
||||
}
|
||||
|
||||
void PSOVMSFileHeader::check() const {
|
||||
if (!this->checksum_correct()) {
|
||||
throw runtime_error("VMS file unencrypted header checksum is incorrect");
|
||||
}
|
||||
}
|
||||
|
||||
bool PSOVMSFileHeader::is_v2() const {
|
||||
return !memcmp(this->short_desc.data, "PSOV2", 5);
|
||||
}
|
||||
|
||||
bool PSOGCIFileHeader::checksum_correct() const {
|
||||
uint32_t cs = phosg::crc32(&this->game_name, this->game_name.bytes());
|
||||
cs = phosg::crc32(&this->embedded_seed, sizeof(this->embedded_seed), cs);
|
||||
@@ -260,7 +270,7 @@ phosg::Image PSOGCSnapshotFile::decode_image() const {
|
||||
PSOGCEp3CharacterFile::Character::Character(const PSOGCEp3NTECharacter& nte)
|
||||
: inventory(nte.inventory),
|
||||
disp(nte.disp),
|
||||
flags(nte.flags),
|
||||
validation_flags(nte.validation_flags),
|
||||
creation_timestamp(nte.creation_timestamp),
|
||||
signature(nte.signature),
|
||||
play_time_seconds(nte.play_time_seconds),
|
||||
@@ -290,7 +300,7 @@ PSOGCEp3CharacterFile::Character::operator PSOGCEp3NTECharacter() const {
|
||||
PSOGCEp3NTECharacter ret;
|
||||
ret.inventory = this->inventory;
|
||||
ret.disp = this->disp;
|
||||
ret.flags = this->flags;
|
||||
ret.validation_flags = this->validation_flags;
|
||||
ret.creation_timestamp = this->creation_timestamp;
|
||||
ret.signature = this->signature;
|
||||
ret.play_time_seconds = this->play_time_seconds;
|
||||
@@ -327,7 +337,7 @@ uint32_t PSOBBGuildCardFile::checksum() const {
|
||||
|
||||
PSOBBBaseSystemFile::PSOBBBaseSystemFile() {
|
||||
// This field is based on 1/1/2000, not 1/1/1970, so adjust appropriately
|
||||
this->base.creation_timestamp = (phosg::now() - 946684800000000ULL) / 1000000;
|
||||
this->creation_timestamp = (phosg::now() - 946684800000000ULL) / 1000000;
|
||||
for (size_t z = 0; z < DEFAULT_KEY_CONFIG.size(); z++) {
|
||||
this->key_config[z] = DEFAULT_KEY_CONFIG[z];
|
||||
}
|
||||
@@ -543,12 +553,13 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
|
||||
guild_card_number, language, preview.visual, preview.name.decode(language), level_table);
|
||||
}
|
||||
|
||||
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_dc_v2(const PSODCV2CharacterFile& dc) {
|
||||
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_dc_v2(const PSODCV2CharacterFile::Character& dc) {
|
||||
auto ret = make_shared<PSOBBCharacterFile>();
|
||||
ret->inventory = dc.inventory;
|
||||
ret->inventory.decode_from_client(Version::DC_V2);
|
||||
uint8_t language = ret->inventory.language;
|
||||
ret->disp = dc.disp.to_bb(language, language);
|
||||
ret->validation_flags = dc.validation_flags;
|
||||
ret->creation_timestamp = dc.creation_timestamp;
|
||||
ret->play_time_seconds = dc.play_time_seconds;
|
||||
ret->option_flags = dc.option_flags;
|
||||
@@ -586,6 +597,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_gc_nte(const PSOG
|
||||
// not do this, so the data2 fields are already in the correct order here.
|
||||
uint8_t language = ret->inventory.language;
|
||||
ret->disp = gc_nte.disp.to_bb(language, language);
|
||||
ret->validation_flags = gc_nte.validation_flags.load();
|
||||
ret->creation_timestamp = gc_nte.creation_timestamp.load();
|
||||
ret->play_time_seconds = gc_nte.play_time_seconds.load();
|
||||
ret->option_flags = gc_nte.option_flags.load();
|
||||
@@ -621,6 +633,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_gc(const PSOGCCha
|
||||
// not do this, so the data2 fields are already in the correct order here.
|
||||
uint8_t language = ret->inventory.language;
|
||||
ret->disp = gc.disp.to_bb(language, language);
|
||||
ret->validation_flags = gc.validation_flags.load();
|
||||
ret->creation_timestamp = gc.creation_timestamp.load();
|
||||
ret->play_time_seconds = gc.play_time_seconds.load();
|
||||
ret->option_flags = gc.option_flags.load();
|
||||
@@ -662,6 +675,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_ep3(const PSOGCEp
|
||||
ret->inventory = ep3.inventory;
|
||||
uint8_t language = ret->inventory.language;
|
||||
ret->disp = ep3.disp.to_bb(language, language);
|
||||
ret->validation_flags = ep3.validation_flags.load();
|
||||
ret->creation_timestamp = ep3.creation_timestamp.load();
|
||||
ret->play_time_seconds = ep3.play_time_seconds.load();
|
||||
ret->option_flags = ep3.option_flags.load();
|
||||
@@ -705,6 +719,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_xb(const PSOXBCha
|
||||
ret->inventory.decode_from_client(Version::XB_V3);
|
||||
uint8_t language = ret->inventory.language;
|
||||
ret->disp = xb.disp.to_bb(language, language);
|
||||
ret->validation_flags = xb.validation_flags;
|
||||
ret->creation_timestamp = xb.creation_timestamp.load();
|
||||
ret->play_time_seconds = xb.play_time_seconds.load();
|
||||
ret->option_flags = xb.option_flags.load();
|
||||
@@ -742,16 +757,62 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_xb(const PSOXBCha
|
||||
return ret;
|
||||
}
|
||||
|
||||
PSODCV2CharacterFile PSOBBCharacterFile::to_dc_v2() const {
|
||||
LoadedPSOCHARFile load_psochar(const string& filename, bool load_system) {
|
||||
auto f = phosg::fopen_unique(filename, "rb");
|
||||
auto header = phosg::freadx<PSOCommandHeaderBB>(f.get());
|
||||
if (header.size != 0x399C) {
|
||||
throw runtime_error("incorrect size in character file header");
|
||||
}
|
||||
if (header.command != 0x00E7) {
|
||||
throw runtime_error("incorrect command in character file header");
|
||||
}
|
||||
if (header.flag != 0x00000000) {
|
||||
throw runtime_error("incorrect flag in character file header");
|
||||
}
|
||||
static_assert(sizeof(PSOBBCharacterFile) + sizeof(PSOBBBaseSystemFile) + sizeof(PSOBBTeamMembership) == 0x3994, ".psochar size is incorrect");
|
||||
|
||||
LoadedPSOCHARFile ret;
|
||||
ret.character_file = make_shared<PSOBBCharacterFile>(phosg::freadx<PSOBBCharacterFile>(f.get()));
|
||||
if (load_system) {
|
||||
ret.system_file = make_shared<PSOBBBaseSystemFile>(phosg::freadx<PSOBBBaseSystemFile>(f.get()));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void save_psochar(
|
||||
const std::string& filename,
|
||||
std::shared_ptr<const PSOBBBaseSystemFile> system,
|
||||
std::shared_ptr<const PSOBBCharacterFile> character) {
|
||||
auto f = phosg::fopen_unique(filename, "wb");
|
||||
PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBBaseSystemFile) + sizeof(PSOBBTeamMembership), 0x00E7, 0x00000000};
|
||||
phosg::fwritex(f.get(), header);
|
||||
phosg::fwritex(f.get(), *character);
|
||||
phosg::fwritex(f.get(), *system);
|
||||
// TODO: Technically, we should write the actual team membership struct to
|
||||
// the file here, but that would cause Client to depend on Account, which it
|
||||
// currently does not. This data doesn't matter at all for correctness within
|
||||
// newserv, since it ignores this data entirely and instead generates the
|
||||
// membership struct from the team ID in the Account and the team's state.
|
||||
// So, writing correct data here would mostly be for compatibility with other
|
||||
// PSO servers. But if the other server is newserv, then this data wouldn't
|
||||
// be used anyway, and if it's not, then it would presumably have a different
|
||||
// set of teams with a different set of team IDs anyway, so the membership
|
||||
// struct here would be useless either way.
|
||||
static const PSOBBTeamMembership empty_membership;
|
||||
phosg::fwritex(f.get(), empty_membership);
|
||||
}
|
||||
|
||||
PSODCV2CharacterFile::Character PSOBBCharacterFile::to_dc_v2() const {
|
||||
uint8_t language = this->inventory.language;
|
||||
|
||||
PSODCV2CharacterFile ret;
|
||||
PSODCV2CharacterFile::Character ret;
|
||||
ret.inventory = this->inventory;
|
||||
// We don't need to do the v1-compatible encoding (hence it is OK to pass
|
||||
// nullptr here) but we do need to encode mag stats in the v2 format
|
||||
ret.inventory.encode_for_client(Version::DC_V2, nullptr);
|
||||
ret.disp = this->disp.to_dcpcv3<false>(language, language);
|
||||
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2);
|
||||
ret.validation_flags = this->validation_flags.load();
|
||||
ret.creation_timestamp = this->creation_timestamp.load();
|
||||
ret.play_time_seconds = this->play_time_seconds.load();
|
||||
ret.option_flags = this->option_flags.load();
|
||||
@@ -793,6 +854,7 @@ PSOGCNTECharacterFileCharacter PSOBBCharacterFile::to_gc_nte() const {
|
||||
// not do this, so the data2 fields are already in the correct order here.
|
||||
ret.disp = this->disp.to_dcpcv3<true>(language, language);
|
||||
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::GC_V3);
|
||||
ret.validation_flags = this->validation_flags.load();
|
||||
ret.creation_timestamp = this->creation_timestamp.load();
|
||||
ret.play_time_seconds = this->play_time_seconds.load();
|
||||
ret.option_flags = this->option_flags.load();
|
||||
@@ -830,6 +892,7 @@ PSOGCCharacterFile::Character PSOBBCharacterFile::to_gc() const {
|
||||
// not do this, so the data2 fields are already in the correct order here.
|
||||
ret.disp = this->disp.to_dcpcv3<true>(language, language);
|
||||
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::GC_V3);
|
||||
ret.validation_flags = this->validation_flags.load();
|
||||
ret.creation_timestamp = this->creation_timestamp.load();
|
||||
ret.play_time_seconds = this->play_time_seconds.load();
|
||||
ret.option_flags = this->option_flags.load();
|
||||
@@ -874,6 +937,7 @@ PSOXBCharacterFileCharacter PSOBBCharacterFile::to_xb(uint64_t xb_user_id) const
|
||||
ret.inventory.encode_for_client(Version::XB_V3, nullptr);
|
||||
ret.disp = this->disp.to_dcpcv3<false>(language, language);
|
||||
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::XB_V3);
|
||||
ret.validation_flags = this->validation_flags.load();
|
||||
ret.creation_timestamp = this->creation_timestamp.load();
|
||||
ret.play_time_seconds = this->play_time_seconds.load();
|
||||
ret.option_flags = this->option_flags.load();
|
||||
@@ -1133,14 +1197,17 @@ void PSOBBCharacterFile::import_tethealla_material_usage(std::shared_ptr<const L
|
||||
|
||||
void PSOBBCharacterFile::recompute_stats(std::shared_ptr<const LevelTable> level_table) {
|
||||
uint32_t level = this->disp.stats.level;
|
||||
uint32_t exp = this->disp.stats.experience;
|
||||
level_table->reset_to_base(this->disp.stats, this->disp.visual.char_class);
|
||||
level_table->advance_to_level(this->disp.stats, level, this->disp.visual.char_class);
|
||||
this->disp.stats.experience = exp;
|
||||
|
||||
this->disp.stats.char_stats.atp += (this->get_material_usage(MaterialType::POWER) * 2);
|
||||
this->disp.stats.char_stats.mst += (this->get_material_usage(MaterialType::MIND) * 2);
|
||||
this->disp.stats.char_stats.evp += (this->get_material_usage(MaterialType::EVADE) * 2);
|
||||
this->disp.stats.char_stats.dfp += (this->get_material_usage(MaterialType::DEF) * 2);
|
||||
this->disp.stats.char_stats.lck += (this->get_material_usage(MaterialType::LUCK) * 2);
|
||||
this->disp.stats.char_stats.hp += (this->get_material_usage(MaterialType::HP) * 2);
|
||||
// Note: HP in this structure is unaffected by material usage
|
||||
}
|
||||
|
||||
static uint16_t crc16(const void* data, size_t size) {
|
||||
|
||||
+234
-115
@@ -14,6 +14,7 @@
|
||||
#include "Episode3/DataIndexes.hh"
|
||||
#include "ItemNameIndex.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "PlayerSubordinates.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
@@ -35,6 +36,12 @@ struct PSOVMSFileHeader {
|
||||
/* 0080 */ // parray<parray<uint8_t, 0x200>, num_icons> icon;
|
||||
|
||||
bool checksum_correct() const;
|
||||
void check() const;
|
||||
inline size_t icon_data_size() const {
|
||||
return this->num_icons * 0x200;
|
||||
}
|
||||
|
||||
bool is_v2() const;
|
||||
} __packed_ws__(PSOVMSFileHeader, 0x80);
|
||||
|
||||
struct PSOGCIFileHeader {
|
||||
@@ -100,11 +107,11 @@ struct ShuffleTables {
|
||||
|
||||
template <bool BE, TextEncoding Encoding, size_t NameLength>
|
||||
struct SaveFileSymbolChatEntryT {
|
||||
/* PC:GC:XB:BB */
|
||||
/* 00:00:00:00 */ U32T<BE> present;
|
||||
/* 04:04:04:04 */ pstring<Encoding, NameLength> name;
|
||||
/* 34:1C:1C:2C */ SymbolChatT<BE> spec;
|
||||
/* 70:58:58:68 */
|
||||
/* DC:PC:GC:XB:BB */
|
||||
/* 00:00:00:00:00 */ U32T<BE> present;
|
||||
/* 04:04:04:04:04 */ pstring<Encoding, NameLength> name;
|
||||
/* 1C:34:1C:1C:2C */ SymbolChatT<BE> spec;
|
||||
/* 58:70:58:58:68 */
|
||||
} __packed__;
|
||||
using SaveFileSymbolChatEntryPC = SaveFileSymbolChatEntryT<false, TextEncoding::UTF16, 0x18>;
|
||||
using SaveFileSymbolChatEntryGC = SaveFileSymbolChatEntryT<true, TextEncoding::MARKED, 0x18>;
|
||||
@@ -279,8 +286,7 @@ struct PSOBBMinimalSystemFile {
|
||||
/* 0114 */
|
||||
} __packed_ws__(PSOBBMinimalSystemFile, 0x114);
|
||||
|
||||
struct PSOBBBaseSystemFile {
|
||||
/* 0000 */ PSOBBMinimalSystemFile base;
|
||||
struct PSOBBBaseSystemFile : PSOBBMinimalSystemFile {
|
||||
/* 0114 */ parray<uint8_t, 0x016C> key_config;
|
||||
/* 0280 */ parray<uint8_t, 0x0038> joystick_config;
|
||||
/* 02B8 */
|
||||
@@ -288,90 +294,143 @@ struct PSOBBBaseSystemFile {
|
||||
PSOBBBaseSystemFile();
|
||||
} __packed_ws__(PSOBBBaseSystemFile, 0x2B8);
|
||||
|
||||
struct PSOBBFullSystemFile {
|
||||
/* 0000 */ PSOBBBaseSystemFile base;
|
||||
/* 02B8 */ PSOBBTeamMembership team_membership;
|
||||
/* 0AF0 */
|
||||
|
||||
PSOBBFullSystemFile() = default;
|
||||
} __packed_ws__(PSOBBFullSystemFile, 0xAF0);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Character files
|
||||
|
||||
struct PSODCNTECharacterFile {
|
||||
/* 0000 */ le_uint32_t checksum = 0;
|
||||
struct Character {
|
||||
// See PSOGCCharacterFile::Character for descriptions of fields' meanings.
|
||||
/* 0000:---- */ PlayerInventory inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C:0000 */ le_uint32_t validation_flags = 0;
|
||||
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ le_uint32_t signature = 0xBB40711D;
|
||||
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
|
||||
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
|
||||
/* 0430:0014 */ le_uint16_t save_count_since_last_inventory_erasure = 1;
|
||||
/* 0432:0016 */ le_uint16_t inventory_erasure_count = 0;
|
||||
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
|
||||
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
|
||||
// TODO: Figure out how quest flags work; it's obviously different from 0x80
|
||||
// bytes per difficulty like in v1. Is it just 2048 flags shared across all
|
||||
// difficulties, instead of 1024 in each difficulty?
|
||||
/* 0460:0044 */ parray<uint8_t, 0x100> quest_flags;
|
||||
/* 0560:0144 */ le_uint16_t bank_meseta;
|
||||
/* 0562:0146 */ le_uint16_t num_bank_items;
|
||||
/* 0564:0148 */ parray<ItemData, 60> bank_items;
|
||||
/* 0A14:05F8 */ GuildCardDCNTE guild_card;
|
||||
/* 0A8F:0673 */ uint8_t unknown_s1; // Probably actually unused
|
||||
/* 0A90:0674 */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
|
||||
/* 0AA0:0684 */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
|
||||
/* 0AB0:0694 */
|
||||
} __packed_ws__(Character, 0xAB0);
|
||||
/* 0004 */ Character character;
|
||||
/* 0AB4 */ le_uint32_t round2_seed = 0;
|
||||
/* 0AB8 */
|
||||
} __packed_ws__(PSODCNTECharacterFile, 0xAB8);
|
||||
|
||||
struct PSODC112000CharacterFile {
|
||||
// See PSOGCCharacterFile::Character for descriptions of fields' meanings.
|
||||
/* 0000:---- */ PlayerInventory inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C:0000 */ le_uint32_t flags = 0;
|
||||
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ le_uint32_t signature = 0xA205B064;
|
||||
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
|
||||
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
|
||||
/* 0430:0014 */ le_uint16_t save_count_since_last_inventory_erasure = 1;
|
||||
/* 0432:0016 */ le_uint16_t inventory_erasure_count = 0;
|
||||
// TODO: Fill out the rest of this structure.
|
||||
/* 0434:0018 */ parray<uint8_t, 0xFA0> unknown_a1;
|
||||
/* 13D4:0FB8 */
|
||||
} __packed_ws__(PSODC112000CharacterFile, 0x13D4);
|
||||
/* 0000 */ le_uint32_t checksum = 0;
|
||||
struct Character {
|
||||
// See PSOGCCharacterFile::Character for descriptions of fields' meanings.
|
||||
/* 0000:---- */ PlayerInventory inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C:0000 */ le_uint32_t validation_flags = 0;
|
||||
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ le_uint32_t signature = 0xBB40711D;
|
||||
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
|
||||
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
|
||||
/* 0430:0014 */ le_uint16_t save_count_since_last_inventory_erasure = 1;
|
||||
/* 0432:0016 */ le_uint16_t inventory_erasure_count = 0;
|
||||
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
|
||||
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
|
||||
// TODO: Figure out how quest flags work; it's obviously different from 0x80
|
||||
// bytes per difficulty like in v1. Is it just 2048 flags shared across all
|
||||
// difficulties, instead of 1024 in each difficulty?
|
||||
/* 0460:0044 */ parray<uint8_t, 0x100> quest_flags;
|
||||
/* 0560:0144 */ le_uint16_t bank_meseta;
|
||||
/* 0562:0146 */ le_uint16_t num_bank_items;
|
||||
/* 0564:0148 */ parray<ItemData, 60> bank_items;
|
||||
/* 0A14:05F8 */ GuildCardDC guild_card;
|
||||
/* 0A91:0675 */ parray<uint8_t, 3> unknown_s1; // Probably actually unused
|
||||
/* 0A94:0678 */ parray<SaveFileSymbolChatEntryDCXB, 12> symbol_chats;
|
||||
/* 0EB4:0A98 */ parray<SaveFileShortcutEntryDC, 20> shortcuts;
|
||||
/* 13B4:0F98 */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
|
||||
/* 13C4:0FA8 */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
|
||||
/* 13D4:0FB8 */
|
||||
} __packed_ws__(Character, 0x13D4);
|
||||
/* 0004 */ Character character;
|
||||
/* 13D8 */ le_uint32_t round2_seed = 0;
|
||||
} __packed_ws__(PSODC112000CharacterFile, 0x13DC);
|
||||
|
||||
struct PSODCV1CharacterFile {
|
||||
// See PSOGCCharacterFile::Character for descriptions of fields' meanings.
|
||||
/* 0000:---- */ PlayerInventory inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C:0000 */ le_uint32_t flags = 0;
|
||||
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ le_uint32_t signature = 0xA205B064;
|
||||
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
|
||||
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
|
||||
/* 0430:0014 */ le_uint32_t save_count = 1;
|
||||
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
|
||||
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
|
||||
/* 0460:0044 */ QuestFlagsV1 quest_flags;
|
||||
/* 05E0:01C4 */ PlayerBank60 bank;
|
||||
/* 0B88:076C */ GuildCardDC guild_card;
|
||||
/* 0C05:07E9 */ parray<uint8_t, 3> unknown_s1; // Probably actually unused
|
||||
/* 0C08:07EC */ parray<SaveFileSymbolChatEntryDCXB, 12> symbol_chats;
|
||||
/* 1028:0C0C */ parray<SaveFileShortcutEntryDC, 20> shortcuts;
|
||||
/* 1528:110C */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
|
||||
/* 1538:111C */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
|
||||
/* 1548:112C */
|
||||
} __packed_ws__(PSODCV1CharacterFile, 0x1548);
|
||||
/* 0000 */ le_uint32_t checksum = 0;
|
||||
struct Character {
|
||||
// See PSOGCCharacterFile::Character for descriptions of fields' meanings.
|
||||
/* 0000:---- */ PlayerInventory inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C:0000 */ le_uint32_t validation_flags = 0;
|
||||
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ le_uint32_t signature = 0xA205B064;
|
||||
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
|
||||
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
|
||||
/* 0430:0014 */ le_uint32_t save_count = 1;
|
||||
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
|
||||
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
|
||||
/* 0460:0044 */ QuestFlagsV1 quest_flags;
|
||||
/* 05E0:01C4 */ PlayerBank60 bank;
|
||||
/* 0B88:076C */ GuildCardDC guild_card;
|
||||
/* 0C05:07E9 */ parray<uint8_t, 3> unknown_s1; // Probably actually unused
|
||||
/* 0C08:07EC */ parray<SaveFileSymbolChatEntryDCXB, 12> symbol_chats;
|
||||
/* 1028:0C0C */ parray<SaveFileShortcutEntryDC, 20> shortcuts;
|
||||
/* 1528:110C */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
|
||||
/* 1538:111C */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
|
||||
/* 1548:112C */
|
||||
} __packed_ws__(Character, 0x1548);
|
||||
/* 0004 */ Character character;
|
||||
/* 154C */ le_uint32_t round2_seed = 0;
|
||||
} __packed_ws__(PSODCV1CharacterFile, 0x1550);
|
||||
|
||||
struct PSODCV2CharacterFile {
|
||||
// See PSOGCCharacterFile::Character for descriptions of fields' meanings.
|
||||
/* 0000:---- */ PlayerInventory inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C:0000 */ le_uint32_t flags = 0;
|
||||
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ le_uint32_t signature = 0xA205B064;
|
||||
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
|
||||
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
|
||||
/* 0430:0014 */ le_uint32_t save_count = 1;
|
||||
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
|
||||
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
|
||||
/* 0460:0044 */ QuestFlags quest_flags;
|
||||
/* 0660:0244 */ PlayerBank60 bank;
|
||||
/* 0C08:07EC */ GuildCardDC guild_card;
|
||||
/* 0C85:0869 */ parray<uint8_t, 3> unknown_s1; // Probably actually unused
|
||||
/* 0C88:086C */ parray<SaveFileSymbolChatEntryDCXB, 12> symbol_chats;
|
||||
/* 10A8:0C8C */ parray<SaveFileShortcutEntryDC, 20> shortcuts;
|
||||
/* 15A8:118C */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
|
||||
/* 15B8:119C */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
|
||||
/* 15C8:11AC */ PlayerRecordsBattle battle_records;
|
||||
/* 15E0:11C4 */ PlayerRecordsChallengeDC challenge_records;
|
||||
/* 1680:1264 */ parray<le_uint16_t, 20> tech_menu_shortcut_entries;
|
||||
// The Choice Search config is stored here as 32-bit integers, even though
|
||||
// it's represented with 16-bit integers in the various commands that send it
|
||||
// to and from the server. The order of the entries here is the same (that
|
||||
// is, the first two of these ints are entries[0], the second two are
|
||||
// entries[1], etc.).
|
||||
/* 16A8:128C */ parray<le_uint32_t, 10> choice_search_config;
|
||||
/* 16D0:12B4 */ parray<uint8_t, 4> unknown_a2;
|
||||
/* 16D4:12B8 */ pstring<TextEncoding::ASCII, 0x10> v2_serial_number;
|
||||
/* 16E4:12C8 */ pstring<TextEncoding::ASCII, 0x10> v2_access_key;
|
||||
/* 16F4:12D8 */
|
||||
} __packed_ws__(PSODCV2CharacterFile, 0x16F4);
|
||||
/* 0000 */ le_uint32_t checksum = 0;
|
||||
struct Character {
|
||||
// See PSOGCCharacterFile::Character for descriptions of fields' meanings.
|
||||
/* 0000:---- */ PlayerInventory inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C:0000 */ le_uint32_t validation_flags = 0;
|
||||
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ le_uint32_t signature = 0xA205B064;
|
||||
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
|
||||
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
|
||||
/* 0430:0014 */ le_uint32_t save_count = 1;
|
||||
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
|
||||
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
|
||||
/* 0460:0044 */ QuestFlags quest_flags;
|
||||
/* 0660:0244 */ PlayerBank60 bank;
|
||||
/* 0C08:07EC */ GuildCardDC guild_card;
|
||||
/* 0C85:0869 */ parray<uint8_t, 3> unknown_s1; // Probably actually unused
|
||||
/* 0C88:086C */ parray<SaveFileSymbolChatEntryDCXB, 12> symbol_chats;
|
||||
/* 10A8:0C8C */ parray<SaveFileShortcutEntryDC, 20> shortcuts;
|
||||
/* 15A8:118C */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
|
||||
/* 15B8:119C */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
|
||||
/* 15C8:11AC */ PlayerRecordsBattle battle_records;
|
||||
/* 15E0:11C4 */ PlayerRecordsChallengeDC challenge_records;
|
||||
/* 1680:1264 */ parray<le_uint16_t, 20> tech_menu_shortcut_entries;
|
||||
// The Choice Search config is stored here as 32-bit integers, even though
|
||||
// it's represented with 16-bit integers in the various commands that send it
|
||||
// to and from the server. The order of the entries here is the same (that
|
||||
// is, the first two of these ints are entries[0], the second two are
|
||||
// entries[1], etc.).
|
||||
/* 16A8:128C */ parray<le_uint32_t, 10> choice_search_config;
|
||||
/* 16D0:12B4 */ parray<uint8_t, 4> unknown_a2;
|
||||
/* 16D4:12B8 */ pstring<TextEncoding::ASCII, 0x10> v2_serial_number;
|
||||
/* 16E4:12C8 */ pstring<TextEncoding::ASCII, 0x10> v2_access_key;
|
||||
/* 16F4:12D8 */
|
||||
} __packed_ws__(Character, 0x16F4);
|
||||
/* 0004 */ Character character;
|
||||
/* 16F0 */ le_uint32_t round2_seed = 0;
|
||||
} __packed_ws__(PSODCV2CharacterFile, 0x16FC);
|
||||
|
||||
struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD
|
||||
// See PSOGCCharacterFile::Character for descriptions of fields' meanings.
|
||||
@@ -387,7 +446,7 @@ struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD
|
||||
struct Character {
|
||||
/* 0000 */ PlayerInventory inventory;
|
||||
/* 034C */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C */ be_uint32_t flags = 0;
|
||||
/* 041C */ be_uint32_t validation_flags = 0;
|
||||
/* 0420 */ be_uint32_t creation_timestamp = 0;
|
||||
/* 0424 */ be_uint32_t signature = 0x6C5D889E;
|
||||
/* 0428 */ be_uint32_t play_time_seconds = 0;
|
||||
@@ -424,7 +483,7 @@ struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD
|
||||
struct PSOGCNTECharacterFileCharacter {
|
||||
/* 0000:---- */ PlayerInventoryBE inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3BE disp;
|
||||
/* 041C:0000 */ be_uint32_t flags = 0;
|
||||
/* 041C:0000 */ be_uint32_t validation_flags = 0;
|
||||
/* 0420:0004 */ be_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ be_uint32_t signature = 0xA205B064;
|
||||
/* 0428:000C */ be_uint32_t play_time_seconds = 0;
|
||||
@@ -456,19 +515,28 @@ struct PSOGCCharacterFile {
|
||||
// to the start of the second internal structure (second column).
|
||||
/* 0000:---- */ PlayerInventoryBE inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3BE disp;
|
||||
// Known bits in the flags field:
|
||||
// Known bits in the validation_flags field:
|
||||
// 00000001: Character was not saved after disconnecting (and the message
|
||||
// about items being deleted is shown in the select menu)
|
||||
// 00000002: Used, but purpose is unknown
|
||||
// 00000008: Used, but purpose is unknown
|
||||
// 00000002: Character has level out of range (< 0 or > max)
|
||||
// 00000004: Character has EXP out of range for their current level
|
||||
// 00000008: Character has one or more stats out of range (< 0 or > max)
|
||||
// 00000010: Character has ever possessed a hacked item, according to the
|
||||
// check_for_hacked_item function in DCv2 (TODO: Does this exist in V3+
|
||||
// also? If so, is the logic the same?)
|
||||
// 00000040: Used, but purpose is unknown
|
||||
/* 041C:0000 */ be_uint32_t flags = 0;
|
||||
// 00000020: Character has meseta out of range (< 0 or > 999999)
|
||||
// 00000040: Character was loaded on a client that has "important" files
|
||||
// modified (on GC, these files are ending_normal.sfd, psogc_j.sfd,
|
||||
// psogc_j2.sfd, ult01.sfd, ult02.sfd, ult03.sfd, ult04.sfd,
|
||||
// ItemPMT.prs, itemrt.gsl, itempt.gsl, and PlyLevelTbl.cpt). For files
|
||||
// larger than 1000000 bytes (decimal), the game only checks the file's
|
||||
// size and skips checksumming its contents.
|
||||
/* 041C:0000 */ be_uint32_t validation_flags = 0;
|
||||
/* 0420:0004 */ be_uint32_t creation_timestamp = 0;
|
||||
// The signature field holds the value 0xA205B064, which is 2718281828 in
|
||||
// decimal - approximately e * 10^9. It's unknown why Sega chose this value.
|
||||
// decimal - approximately e * 10^9. It's unknown why Sega chose this
|
||||
// value. On some other versions, this field has a different value; see the
|
||||
// defaults in the other versions' structures.
|
||||
/* 0424:0008 */ be_uint32_t signature = 0xA205B064;
|
||||
/* 0428:000C */ be_uint32_t play_time_seconds = 0;
|
||||
// This field is a collection of several flags and small values. The known
|
||||
@@ -476,7 +544,7 @@ struct PSOGCCharacterFile {
|
||||
// ------zA BCDEFG-- HHHIIIJJ KLMNOPQR
|
||||
// z = Function key setting (BB; 0 = menu shortcuts; 1 = chat shortcuts).
|
||||
// This bit is unused by PSO GC.
|
||||
// A = Keyboard controls (BB; 0 = on; 1 = off). Note that A is also used
|
||||
// A = Keyboard controls (BB; 0 = on; 1 = off). This field is also used
|
||||
// by PSO GC, but its function is currently unknown.
|
||||
// G = Choice search setting (0 = enabled; 1 = disabled)
|
||||
// H = Player lobby labels (0 = name; 1 = name, language, and level;
|
||||
@@ -514,7 +582,7 @@ struct PSOGCCharacterFile {
|
||||
/* 2794:2378 */ be_uint32_t unknown_f8 = 0;
|
||||
/* 2798:237C */
|
||||
} __packed_ws__(Character, 0x2798);
|
||||
/* 00004 */ parray<Character, 7> characters;
|
||||
/* 00004 */ parray<Character, 7> characters; // 0-3: main chars, 4-6: temps
|
||||
/* 1152C */ pstring<TextEncoding::ASCII, 0x10> serial_number; // As %08X (not decimal)
|
||||
/* 1153C */ pstring<TextEncoding::ASCII, 0x10> access_key;
|
||||
/* 1154C */ pstring<TextEncoding::ASCII, 0x10> password;
|
||||
@@ -530,7 +598,7 @@ struct PSOGCEp3NTECharacter {
|
||||
// to the start of the second internal structure (second column).
|
||||
/* 0000:---- */ PlayerInventoryBE inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3BE disp;
|
||||
/* 041C:0000 */ be_uint32_t flags = 0;
|
||||
/* 041C:0000 */ be_uint32_t validation_flags = 0;
|
||||
/* 0420:0004 */ be_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ be_uint32_t signature = 0xA205B064;
|
||||
/* 0428:000C */ be_uint32_t play_time_seconds = 0;
|
||||
@@ -538,9 +606,6 @@ struct PSOGCEp3NTECharacter {
|
||||
/* 0430:0014 */ be_uint32_t save_count = 1;
|
||||
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
|
||||
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
|
||||
// seq_vars is an array of 8192 bits, which contain all the Episode 3 quest
|
||||
// progress flags. This includes things like which maps are unlocked, which
|
||||
// NPC decks are unlocked, and whether the player has a VIP card or not.
|
||||
/* 0460:0044 */ parray<uint8_t, 0x400> seq_vars;
|
||||
/* 0860:0444 */ be_uint32_t death_count = 0;
|
||||
/* 0864:0448 */ PlayerBank200BE bank;
|
||||
@@ -549,7 +614,6 @@ struct PSOGCEp3NTECharacter {
|
||||
/* 1FDC:1BC0 */ parray<SaveFileShortcutEntryGC, 20> chat_shortcuts;
|
||||
/* 266C:2250 */ pstring<TextEncoding::MARKED, 0xAC> auto_reply;
|
||||
/* 2718:22FC */ pstring<TextEncoding::MARKED, 0xAC> info_board;
|
||||
// // In this struct, place_counts[0] is win_count and [1] is loss_count
|
||||
/* 27C4:23A8 */ PlayerRecordsBattleBE battle_records;
|
||||
/* 27DC:23C0 */ parray<uint8_t, 4> unknown_a10;
|
||||
/* 27E0:23C4 */ PlayerRecordsChallengeV3BE::Stats challenge_record_stats;
|
||||
@@ -570,7 +634,7 @@ struct PSOGCEp3CharacterFile {
|
||||
// to the start of the second internal structure (second column).
|
||||
/* 0000:---- */ PlayerInventoryBE inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3BE disp;
|
||||
/* 041C:0000 */ be_uint32_t flags = 0;
|
||||
/* 041C:0000 */ be_uint32_t validation_flags = 0;
|
||||
/* 0420:0004 */ be_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ be_uint32_t signature = 0xA205B064;
|
||||
/* 0428:000C */ be_uint32_t play_time_seconds = 0;
|
||||
@@ -581,11 +645,13 @@ struct PSOGCEp3CharacterFile {
|
||||
// seq_vars is an array of 8192 bits, which contain all the Episode 3 quest
|
||||
// progress flags. This includes things like which maps are unlocked, which
|
||||
// NPC decks are unlocked, and whether the player has a VIP card or not.
|
||||
// Logically, this structure maps to quest_flags in other versions, but is
|
||||
// a different size.
|
||||
/* 0460:0044 */ parray<uint8_t, 0x400> seq_vars;
|
||||
/* 0860:0444 */ be_uint32_t death_count = 0;
|
||||
// Curiously, Episode 3 characters do have item banks, but there are only 4
|
||||
// item slots. Sega presumably didn't completely remove the bank in Ep3
|
||||
// because they would have to change too much code.
|
||||
// item slots. Presumably Sega didn't completely remove the bank in Ep3
|
||||
// because they would have had to change too much code.
|
||||
/* 0864:0448 */ PlayerBankT<4, true> bank;
|
||||
/* 08CC:04B0 */ GuildCardGCBE guild_card;
|
||||
/* 095C:0540 */ parray<SaveFileSymbolChatEntryGC, 12> symbol_chats;
|
||||
@@ -637,7 +703,7 @@ struct PSOXBCharacterFileCharacter {
|
||||
// Most fields have the same meanings as in PSOGCCharacterFile::Character.
|
||||
/* 0000:---- */ PlayerInventory inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C:0000 */ le_uint32_t flags = 0;
|
||||
/* 041C:0000 */ le_uint32_t validation_flags = 0;
|
||||
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 0424:0008 */ le_uint32_t signature = 0xC87ED5B1;
|
||||
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
|
||||
@@ -680,7 +746,7 @@ struct PSOBBCharacterFile {
|
||||
|
||||
/* 0000 */ PlayerInventory inventory;
|
||||
/* 034C */ PlayerDispDataBB disp;
|
||||
/* 04DC */ le_uint32_t flags = 0;
|
||||
/* 04DC */ le_uint32_t validation_flags = 0;
|
||||
/* 04E0 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 04E4 */ le_uint32_t signature = 0xC87ED5B1;
|
||||
/* 04E8 */ le_uint32_t play_time_seconds = 0;
|
||||
@@ -721,13 +787,13 @@ struct PSOBBCharacterFile {
|
||||
uint8_t language,
|
||||
const PlayerDispDataBBPreview& preview,
|
||||
std::shared_ptr<const LevelTable> level_table);
|
||||
static std::shared_ptr<PSOBBCharacterFile> create_from_dc_v2(const PSODCV2CharacterFile& dc);
|
||||
static std::shared_ptr<PSOBBCharacterFile> create_from_dc_v2(const PSODCV2CharacterFile::Character& dc);
|
||||
static std::shared_ptr<PSOBBCharacterFile> create_from_gc_nte(const PSOGCNTECharacterFileCharacter& gc_nte);
|
||||
static std::shared_ptr<PSOBBCharacterFile> create_from_gc(const PSOGCCharacterFile::Character& gc);
|
||||
static std::shared_ptr<PSOBBCharacterFile> create_from_ep3(const PSOGCEp3CharacterFile::Character& ep3);
|
||||
static std::shared_ptr<PSOBBCharacterFile> create_from_xb(const PSOXBCharacterFileCharacter& xb);
|
||||
|
||||
PSODCV2CharacterFile to_dc_v2() const;
|
||||
PSODCV2CharacterFile::Character to_dc_v2() const;
|
||||
PSOGCNTECharacterFileCharacter to_gc_nte() const;
|
||||
PSOGCCharacterFile::Character to_gc() const;
|
||||
PSOXBCharacterFileCharacter to_xb(uint64_t xb_user_id) const;
|
||||
@@ -757,9 +823,61 @@ struct PSOBBCharacterFile {
|
||||
void recompute_stats(std::shared_ptr<const LevelTable> level_table);
|
||||
} __packed_ws__(PSOBBCharacterFile, 0x2EA4);
|
||||
|
||||
struct LoadedPSOCHARFile {
|
||||
std::shared_ptr<PSOBBBaseSystemFile> system_file; // Null if load_system is false
|
||||
std::shared_ptr<PSOBBCharacterFile> character_file; // Never null
|
||||
// Team membership is present in the file, but ignored by newserv
|
||||
};
|
||||
|
||||
LoadedPSOCHARFile load_psochar(const std::string& filename, bool load_system);
|
||||
void save_psochar(
|
||||
const std::string& filename,
|
||||
std::shared_ptr<const PSOBBBaseSystemFile> system,
|
||||
std::shared_ptr<const PSOBBCharacterFile> character);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Guild Card files
|
||||
|
||||
struct PSODCNTEGuildCardFile {
|
||||
// Note: DC NTE does not encrypt the Guild Card file
|
||||
struct Entry {
|
||||
/* 0000 */ GuildCardDCNTE guild_card;
|
||||
/* 007B */ uint8_t unknown_a1 = 0; // Probably actually unused
|
||||
/* 007C */
|
||||
} __packed_ws__(Entry, 0x7C);
|
||||
/* 0000 */ parray<Entry, 100> entries;
|
||||
/* 3070 */
|
||||
} __packed_ws__(PSODCNTEGuildCardFile, 0x3070);
|
||||
|
||||
struct PSODCGuildCardFileEntry {
|
||||
/* 0000 */ GuildCardDC guild_card;
|
||||
/* 007D */ parray<uint8_t, 3> unknown_a1 = 0; // Probably actually unused
|
||||
/* 0080 */
|
||||
} __packed_ws__(PSODCGuildCardFileEntry, 0x80);
|
||||
|
||||
struct PSODC112000GuildCardFile {
|
||||
// Note: 11/2000 does not encrypt the Guild Card file
|
||||
/* 0000 */ parray<PSODCGuildCardFileEntry, 100> entries;
|
||||
/* 3200 */
|
||||
} __packed_ws__(PSODC112000GuildCardFile, 0x3200);
|
||||
|
||||
struct PSODCV1V2GuildCardFile {
|
||||
struct EncryptedSection {
|
||||
/* 0000 */ le_uint32_t checksum = 0;
|
||||
/* 0004 */ parray<PSODCGuildCardFileEntry, 100> entries;
|
||||
/* 3204 */ le_int16_t music_volume = 0;
|
||||
/* 3206 */ int8_t sound_volume = 0;
|
||||
/* 3207 */ uint8_t language = 1;
|
||||
/* 3208 */ le_int32_t server_time_delta_frames = 540000; // 648000 on DCv1
|
||||
/* 320C */ le_uint32_t creation_timestamp = 0;
|
||||
/* 3210 */ le_uint32_t round2_seed = 0;
|
||||
/* 3214 */
|
||||
} __packed_ws__(EncryptedSection, 0x3214);
|
||||
/* 0000 */ EncryptedSection encrypted_section;
|
||||
/* 3214 */ parray<uint8_t, 0x100> event_flags;
|
||||
/* 3314 */
|
||||
} __packed_ws__(PSODCV1V2GuildCardFile, 0x3314);
|
||||
|
||||
struct PSOPCGuildCardFile { // PSO______GUD
|
||||
/* 0000 */ le_uint32_t checksum = 0;
|
||||
// TODO: Figure out the PC guild card format.
|
||||
@@ -864,7 +982,8 @@ struct LegacySavedAccountDataBB { // .nsa file format
|
||||
/* 0000 */ pstring<TextEncoding::ASCII, 0x40> signature;
|
||||
/* 0040 */ parray<le_uint32_t, 0x001E> blocked_senders;
|
||||
/* 00B8 */ PSOBBGuildCardFile guild_card_file;
|
||||
/* D648 */ PSOBBFullSystemFile system_file;
|
||||
/* D648 */ PSOBBBaseSystemFile system_file;
|
||||
/* D880 */ PSOBBTeamMembership team_membership;
|
||||
/* E138 */ le_uint32_t unused = 0;
|
||||
/* E13C */ le_uint32_t option_flags = 0x00040058;
|
||||
/* E140 */ parray<SaveFileShortcutEntryBB, 0x10> shortcuts;
|
||||
@@ -921,14 +1040,14 @@ std::string decrypt_fixed_size_data_section_s(
|
||||
size_t size,
|
||||
uint32_t round1_seed,
|
||||
bool skip_checksum = false,
|
||||
uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF) {
|
||||
int64_t override_round2_seed = -1) {
|
||||
if (size < 2 * sizeof(U32T<BE>)) {
|
||||
throw std::runtime_error("data size is too small");
|
||||
}
|
||||
std::string decrypted = decrypt_data_section<BE>(data_section, size, round1_seed);
|
||||
|
||||
uint32_t round2_seed = override_round2_seed < 0x100000000
|
||||
? static_cast<uint32_t>(override_round2_seed)
|
||||
uint32_t round2_seed = (override_round2_seed >= 0)
|
||||
? override_round2_seed
|
||||
: reinterpret_cast<const U32T<BE>*>(decrypted.data() + decrypted.size() - sizeof(U32T<BE>))->load();
|
||||
PSOV2Encryption round2_crypt(round2_seed);
|
||||
if (BE) {
|
||||
@@ -953,13 +1072,13 @@ std::string decrypt_fixed_size_data_section_s(
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
template <typename StructT, bool BE>
|
||||
template <typename StructT, bool BE, size_t ChecksumLength = sizeof(StructT)>
|
||||
StructT decrypt_fixed_size_data_section_t(
|
||||
const void* data_section,
|
||||
size_t size,
|
||||
uint32_t round1_seed,
|
||||
bool skip_checksum = false,
|
||||
uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF) {
|
||||
int64_t override_round2_seed = -1) {
|
||||
|
||||
std::string decrypted = decrypt_data_section<BE>(data_section, size, round1_seed);
|
||||
if (decrypted.size() < sizeof(StructT)) {
|
||||
@@ -967,7 +1086,7 @@ StructT decrypt_fixed_size_data_section_t(
|
||||
}
|
||||
StructT ret = *reinterpret_cast<const StructT*>(decrypted.data());
|
||||
|
||||
PSOV2Encryption round2_crypt(override_round2_seed < 0x100000000 ? override_round2_seed : ret.round2_seed.load());
|
||||
PSOV2Encryption round2_crypt((override_round2_seed >= 0) ? override_round2_seed : ret.round2_seed.load());
|
||||
if (BE) {
|
||||
round2_crypt.encrypt_big_endian(&ret, offsetof(StructT, round2_seed));
|
||||
} else {
|
||||
@@ -977,7 +1096,7 @@ StructT decrypt_fixed_size_data_section_t(
|
||||
if (!skip_checksum) {
|
||||
uint32_t expected_crc = ret.checksum;
|
||||
ret.checksum = 0;
|
||||
uint32_t actual_crc = phosg::crc32(&ret, sizeof(ret));
|
||||
uint32_t actual_crc = phosg::crc32(&ret, ChecksumLength);
|
||||
ret.checksum = expected_crc;
|
||||
if (expected_crc != actual_crc) {
|
||||
throw std::runtime_error(phosg::string_printf(
|
||||
@@ -1012,12 +1131,12 @@ std::string encrypt_fixed_size_data_section_s(const void* data, size_t size, uin
|
||||
return encrypt_data_section<BE>(encrypted.data(), encrypted.size(), round1_seed);
|
||||
}
|
||||
|
||||
template <typename StructT, bool BE>
|
||||
template <typename StructT, bool BE, size_t ChecksumLength = sizeof(StructT)>
|
||||
std::string encrypt_fixed_size_data_section_t(const StructT& s, uint32_t round1_seed) {
|
||||
StructT encrypted = s;
|
||||
encrypted.checksum = 0;
|
||||
encrypted.round2_seed = phosg::random_object<uint32_t>();
|
||||
encrypted.checksum = phosg::crc32(&encrypted, sizeof(encrypted));
|
||||
encrypted.checksum = phosg::crc32(&encrypted, ChecksumLength);
|
||||
|
||||
PSOV2Encryption round2_crypt(encrypted.round2_seed);
|
||||
if (BE) {
|
||||
|
||||
+42
-50
@@ -64,10 +64,6 @@ const unordered_set<uint32_t> v3_crypt_initial_client_commands({
|
||||
0x00EC019E, // (02) GC login (UDP off)
|
||||
0x0150009E, // (02) GC extended login
|
||||
0x0150019E, // (02) GC extended login (UDP off)
|
||||
0x0130009E, // (02) XB login
|
||||
0x0130019E, // (02) XB login (UDP off)
|
||||
0x0194009E, // (02) XB extended login
|
||||
0x0194019E, // (02) XB extended login (UDP off)
|
||||
});
|
||||
|
||||
const unordered_set<string> bb_crypt_initial_client_commands({
|
||||
@@ -619,8 +615,8 @@ void send_client_init_bb(shared_ptr<Client> c, uint32_t error_code) {
|
||||
void send_system_file_bb(shared_ptr<Client> c) {
|
||||
auto team = c->team();
|
||||
|
||||
PSOBBFullSystemFile cmd;
|
||||
cmd.base = *c->system_file();
|
||||
S_SyncSystemFile_BB_E2 cmd;
|
||||
cmd.system_file = *c->system_file();
|
||||
if (team) {
|
||||
cmd.team_membership = team->membership_for_member(c->login->account->account_id);
|
||||
}
|
||||
@@ -674,22 +670,15 @@ static const vector<string> stream_file_entries = {
|
||||
"BattleParamEntry_ep4_on.dat",
|
||||
"PlyLevelTbl.prs",
|
||||
};
|
||||
static FileContentsCache bb_stream_files_cache(3600000000ULL);
|
||||
|
||||
void send_stream_file_index_bb(shared_ptr<Client> c) {
|
||||
|
||||
struct S_StreamFileIndexEntry_BB_01EB {
|
||||
le_uint32_t size;
|
||||
le_uint32_t checksum; // crc32 of file data
|
||||
le_uint32_t offset; // offset in stream (== sum of all previous files' sizes)
|
||||
pstring<TextEncoding::ASCII, 0x40> filename;
|
||||
};
|
||||
auto s = c->require_server_state();
|
||||
|
||||
vector<S_StreamFileIndexEntry_BB_01EB> entries;
|
||||
size_t offset = 0;
|
||||
for (const string& filename : stream_file_entries) {
|
||||
string key = "system/blueburst/" + filename;
|
||||
auto cache_res = bb_stream_files_cache.get_or_load(key);
|
||||
auto cache_res = s->bb_stream_files_cache->get_or_load(key);
|
||||
auto& e = entries.emplace_back();
|
||||
e.size = cache_res.file->data->size();
|
||||
// Computing the checksum can be slow, so we cache it along with the file
|
||||
@@ -697,12 +686,12 @@ void send_stream_file_index_bb(shared_ptr<Client> c) {
|
||||
// so we always recompute the checksum in that case.
|
||||
if (cache_res.generate_called) {
|
||||
e.checksum = crc32(cache_res.file->data->data(), e.size);
|
||||
bb_stream_files_cache.replace_obj<uint32_t>(key + ".crc32", e.checksum);
|
||||
s->bb_stream_files_cache->replace_obj<uint32_t>(key + ".crc32", e.checksum);
|
||||
} else {
|
||||
auto compute_checksum = [&](const string&) -> uint32_t {
|
||||
return crc32(cache_res.file->data->data(), e.size);
|
||||
};
|
||||
e.checksum = bb_stream_files_cache.get_obj<uint32_t>(key + ".crc32", compute_checksum).obj;
|
||||
e.checksum = s->bb_stream_files_cache->get_obj<uint32_t>(key + ".crc32", compute_checksum).obj;
|
||||
}
|
||||
e.offset = offset;
|
||||
e.filename.encode(filename);
|
||||
@@ -712,17 +701,19 @@ void send_stream_file_index_bb(shared_ptr<Client> c) {
|
||||
}
|
||||
|
||||
void send_stream_file_chunk_bb(shared_ptr<Client> c, uint32_t chunk_index) {
|
||||
auto cache_result = bb_stream_files_cache.get(
|
||||
"<BB stream file>", +[](const string&) -> string {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
auto cache_result = s->bb_stream_files_cache->get(
|
||||
"<BB stream file>", [&](const string&) -> string {
|
||||
size_t bytes = 0;
|
||||
for (const auto& name : stream_file_entries) {
|
||||
bytes += bb_stream_files_cache.get_or_load("system/blueburst/" + name).file->data->size();
|
||||
bytes += s->bb_stream_files_cache->get_or_load("system/blueburst/" + name).file->data->size();
|
||||
}
|
||||
|
||||
string ret;
|
||||
ret.reserve(bytes);
|
||||
for (const auto& name : stream_file_entries) {
|
||||
ret += *bb_stream_files_cache.get_or_load("system/blueburst/" + name).file->data;
|
||||
ret += *s->bb_stream_files_cache->get_or_load("system/blueburst/" + name).file->data;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
@@ -754,14 +745,14 @@ void send_complete_player_bb(shared_ptr<Client> c) {
|
||||
if (c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) {
|
||||
p->inventory.language = 1;
|
||||
p->guild_card.language = 1;
|
||||
sys->base.language = 1;
|
||||
sys->language = 1;
|
||||
}
|
||||
|
||||
SC_SyncSaveFiles_BB_E7 cmd;
|
||||
cmd.char_file = *p;
|
||||
cmd.system_file.base = *sys;
|
||||
cmd.system_file = *sys;
|
||||
if (team) {
|
||||
cmd.system_file.team_membership = team->membership_for_member(c->login->account->account_id);
|
||||
cmd.team_membership = team->membership_for_member(c->login->account->account_id);
|
||||
}
|
||||
send_command_t(c, 0x00E7, 0x00000000, cmd);
|
||||
}
|
||||
@@ -962,6 +953,14 @@ void send_text_or_scrolling_message(
|
||||
}
|
||||
}
|
||||
|
||||
void send_text_or_scrolling_message(shared_ptr<ServerState> s, const std::string& text, const std::string& scrolling) {
|
||||
for (const auto& it : s->channel_to_client) {
|
||||
if (it.second->login && !is_patch(it.second->version())) {
|
||||
send_text_or_scrolling_message(it.second, text, scrolling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string prepare_chat_data(
|
||||
Version version,
|
||||
uint8_t language,
|
||||
@@ -1586,7 +1585,7 @@ void send_quest_menu_t(
|
||||
}
|
||||
|
||||
auto& e = entries.emplace_back();
|
||||
e.menu_id = (it.second->episode == Episode::EP1) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2;
|
||||
e.menu_id = ((it.second->episode == Episode::EP1) || (it.second->episode == Episode::EP3)) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2;
|
||||
e.item_id = it.second->quest_number;
|
||||
e.name.encode(vq->name, c->language());
|
||||
e.short_description.encode(add_color(vq->short_description), c->language());
|
||||
@@ -2546,26 +2545,20 @@ void send_player_stats_change(Channel& ch, uint16_t client_id, PlayerStatsChange
|
||||
send_command_vt(ch, (subs.size() > 0x400 / sizeof(G_UpdatePlayerStat_6x9A)) ? 0x6C : 0x60, 0x00, subs);
|
||||
}
|
||||
|
||||
void send_remove_conditions(shared_ptr<Client> c) {
|
||||
parray<G_AddOrRemoveCondition_6x0C_6x0D, 4> cmds;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
auto& cmd = cmds[z];
|
||||
cmd.header = {0x0D, sizeof(G_AddOrRemoveCondition_6x0C_6x0D) >> 2, c->lobby_client_id};
|
||||
cmd.unknown_a1 = z;
|
||||
cmd.unknown_a2 = 0;
|
||||
}
|
||||
send_protected_command(c, &cmds, sizeof(cmds), true);
|
||||
void send_remove_negative_conditions(shared_ptr<Client> c) {
|
||||
G_AddStatusEffect_6x0C cmd;
|
||||
cmd.header = {0x0C, sizeof(G_AddStatusEffect_6x0C) >> 2, c->lobby_client_id};
|
||||
cmd.effect_type = 7; // Healing ring
|
||||
cmd.level = 0;
|
||||
send_protected_command(c, &cmd, sizeof(cmd), true);
|
||||
}
|
||||
|
||||
void send_remove_conditions(Channel& ch, uint16_t client_id) {
|
||||
parray<G_AddOrRemoveCondition_6x0C_6x0D, 4> cmds;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
auto& cmd = cmds[z];
|
||||
cmd.header = {0x0D, sizeof(G_AddOrRemoveCondition_6x0C_6x0D) >> 2, client_id};
|
||||
cmd.unknown_a1 = z;
|
||||
cmd.unknown_a2 = 0;
|
||||
}
|
||||
ch.send(0x60, 0x00, &cmds, sizeof(cmds));
|
||||
void send_remove_negative_conditions(Channel& ch, uint16_t client_id) {
|
||||
G_AddStatusEffect_6x0C cmd;
|
||||
cmd.header = {0x0C, sizeof(G_AddStatusEffect_6x0C) >> 2, client_id};
|
||||
cmd.effect_type = 7; // Healing ring
|
||||
cmd.level = 0;
|
||||
ch.send(0x60, 0x00, &cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
void send_warp(Channel& ch, uint8_t client_id, uint32_t floor, bool is_private) {
|
||||
@@ -2576,7 +2569,6 @@ void send_warp(Channel& ch, uint8_t client_id, uint32_t floor, bool is_private)
|
||||
void send_warp(shared_ptr<Client> c, uint32_t floor, bool is_private) {
|
||||
send_warp(c->channel, c->lobby_client_id, floor, is_private);
|
||||
c->floor = floor;
|
||||
c->recent_switch_flags.clear();
|
||||
}
|
||||
|
||||
void send_warp(shared_ptr<Lobby> l, uint32_t floor, bool is_private) {
|
||||
@@ -3093,8 +3085,8 @@ void send_level_up(shared_ptr<Client> c) {
|
||||
}
|
||||
|
||||
uint8_t subcommand = get_pre_v1_subcommand(c->version(), 0x2C, 0x2E, 0x30);
|
||||
G_LevelUp_6x30 cmd = {
|
||||
{subcommand, sizeof(G_LevelUp_6x30) / 4, c->lobby_client_id},
|
||||
G_ChangePlayerLevel_6x30 cmd = {
|
||||
{subcommand, sizeof(G_ChangePlayerLevel_6x30) / 4, c->lobby_client_id},
|
||||
stats.atp + (mag ? ((mag->data1w[3] / 100) * 2) : 0),
|
||||
stats.mst + (mag ? ((mag->data1w[5] / 100) * 2) : 0),
|
||||
stats.evp,
|
||||
@@ -3141,14 +3133,14 @@ void send_rare_enemy_index_list(shared_ptr<Client> c, const vector<size_t>& inde
|
||||
send_command_t(c, 0xDE, 0x00, cmd);
|
||||
}
|
||||
|
||||
void send_quest_function_call(Channel& ch, uint16_t function_id) {
|
||||
void send_quest_function_call(Channel& ch, uint16_t label) {
|
||||
S_CallQuestFunction_V3_BB_AB cmd;
|
||||
cmd.function_id = function_id;
|
||||
cmd.label = label;
|
||||
ch.send(0xAB, 0x00, &cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
void send_quest_function_call(shared_ptr<Client> c, uint16_t function_id) {
|
||||
send_quest_function_call(c->channel, function_id);
|
||||
void send_quest_function_call(shared_ptr<Client> c, uint16_t label) {
|
||||
send_quest_function_call(c->channel, label);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
+5
-4
@@ -214,6 +214,7 @@ void send_scrolling_message_bb(std::shared_ptr<Client> c, const std::string& tex
|
||||
void send_text_or_scrolling_message(std::shared_ptr<Client> c, const std::string& text, const std::string& scrolling);
|
||||
void send_text_or_scrolling_message(
|
||||
std::shared_ptr<Lobby> l, std::shared_ptr<Client> exclude_c, const std::string& text, const std::string& scrolling);
|
||||
void send_text_or_scrolling_message(std::shared_ptr<ServerState> s, const std::string& text, const std::string& scrolling);
|
||||
|
||||
std::string prepare_chat_data(
|
||||
Version version,
|
||||
@@ -325,8 +326,8 @@ enum PlayerStatsChange {
|
||||
|
||||
void send_player_stats_change(std::shared_ptr<Client> c, PlayerStatsChange stat, uint32_t amount);
|
||||
void send_player_stats_change(Channel& ch, uint16_t client_id, PlayerStatsChange stat, uint32_t amount);
|
||||
void send_remove_conditions(std::shared_ptr<Client> c);
|
||||
void send_remove_conditions(Channel& ch, uint16_t client_id);
|
||||
void send_remove_negative_conditions(std::shared_ptr<Client> c);
|
||||
void send_remove_negative_conditions(Channel& ch, uint16_t client_id);
|
||||
void send_warp(Channel& ch, uint8_t client_id, uint32_t floor, bool is_private);
|
||||
void send_warp(std::shared_ptr<Client> c, uint32_t floor, bool is_private);
|
||||
void send_warp(std::shared_ptr<Lobby> l, uint32_t floor, bool is_private);
|
||||
@@ -373,8 +374,8 @@ void send_give_experience(std::shared_ptr<Client> c, uint32_t amount);
|
||||
void send_set_exp_multiplier(std::shared_ptr<Lobby> l);
|
||||
void send_rare_enemy_index_list(std::shared_ptr<Client> c, const std::vector<size_t>& indexes);
|
||||
|
||||
void send_quest_function_call(Channel& ch, uint16_t function_id);
|
||||
void send_quest_function_call(std::shared_ptr<Client> c, uint16_t function_id);
|
||||
void send_quest_function_call(Channel& ch, uint16_t label);
|
||||
void send_quest_function_call(std::shared_ptr<Client> c, uint16_t label);
|
||||
|
||||
void send_ep3_card_list_update(std::shared_ptr<Client> c);
|
||||
void send_ep3_media_update(
|
||||
|
||||
+98
-4
@@ -7,6 +7,7 @@
|
||||
#include <phosg/Random.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
#include "ChatCommands.hh"
|
||||
#include "ReceiveCommands.hh"
|
||||
#include "SendCommands.hh"
|
||||
#include "ServerState.hh"
|
||||
@@ -214,6 +215,7 @@ CommandDefinition c_reload(
|
||||
accounts - reindex user accounts\n\
|
||||
battle-params - reload the BB enemy stats files\n\
|
||||
bb-keys - reload BB private keys\n\
|
||||
caches - clear all cached files\n\
|
||||
config - reload most fields from config.json\n\
|
||||
dol-files - reindex all DOL files\n\
|
||||
drop-tables - reload drop tables\n\
|
||||
@@ -230,6 +232,7 @@ CommandDefinition c_reload(
|
||||
teams - reindex all BB teams\n\
|
||||
text-index - reload in-game text\n\
|
||||
word-select - regenerate the Word Select translation table\n\
|
||||
all - do all of the above\n\
|
||||
Reloading will not affect items that are in use; for example, if an Episode\n\
|
||||
3 battle is in progress, it will continue to use the previous map and card\n\
|
||||
definitions. Similarly, BB clients are not forced to disconnect or reload\n\
|
||||
@@ -240,10 +243,21 @@ CommandDefinition c_reload(
|
||||
+[](CommandArgs& args) {
|
||||
auto types = phosg::split(args.args, ' ');
|
||||
for (const auto& type : types) {
|
||||
if (type == "bb-keys") {
|
||||
if (type == "all") {
|
||||
args.s->forward_to_event_thread([s = args.s]() {
|
||||
try {
|
||||
s->load_all();
|
||||
} catch (const exception& e) {
|
||||
fprintf(stderr, "FAILED: %s\n", e.what());
|
||||
fprintf(stderr, "Some configuration may have been reloaded. Fix the underlying issue and try again.\n");
|
||||
}
|
||||
});
|
||||
} else if (type == "bb-keys") {
|
||||
args.s->load_bb_private_keys(true);
|
||||
} else if (type == "accounts") {
|
||||
args.s->load_accounts(true);
|
||||
} else if (type == "caches") {
|
||||
args.s->clear_file_caches(true);
|
||||
} else if (type == "patch-files") {
|
||||
args.s->load_patch_indexes(true);
|
||||
} else if (type == "ep3-cards") {
|
||||
@@ -358,11 +372,37 @@ uint32_t parse_account_flags(const string& flags_str) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint32_t parse_account_user_flags(const string& user_flags_str) {
|
||||
try {
|
||||
size_t end_pos = 0;
|
||||
uint32_t ret = stoul(user_flags_str, &end_pos, 16);
|
||||
if (end_pos == user_flags_str.size()) {
|
||||
return ret;
|
||||
}
|
||||
} catch (const exception&) {
|
||||
}
|
||||
|
||||
uint32_t ret = 0;
|
||||
auto tokens = phosg::split(user_flags_str, ',');
|
||||
for (const auto& token : tokens) {
|
||||
string token_upper = phosg::toupper(token);
|
||||
if (token_upper == "NONE") {
|
||||
// Nothing to do
|
||||
} else if (token_upper == "DISABLE_DROP_NOTIFICATION_BROADCAST") {
|
||||
ret |= static_cast<uint32_t>(Account::UserFlag::DISABLE_DROP_NOTIFICATION_BROADCAST);
|
||||
} else {
|
||||
throw runtime_error("invalid user flag name: " + token_upper);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
CommandDefinition c_add_account(
|
||||
"add-account", "add-account [PARAMETERS...]\n\
|
||||
Add an account to the server. <parameters> is some subset of:\n\
|
||||
id=ACCOUNT-ID: preferred account ID in hex (optional)\n\
|
||||
flags=FLAGS: behaviors and permissions for the account (see below)\n\
|
||||
user-flags=FLAGS: user-set behaviors for the account\n\
|
||||
ep3-current-meseta=MESETA: Episode 3 Meseta value\n\
|
||||
ep3-total-meseta=MESETA: Episode 3 total Meseta ever earned\n\
|
||||
temporary: marks the account as temporary; it is not saved to disk and\n\
|
||||
@@ -401,6 +441,8 @@ CommandDefinition c_add_account(
|
||||
account->is_temporary = true;
|
||||
} else if (phosg::starts_with(token, "flags=")) {
|
||||
account->flags = parse_account_flags(token.substr(6));
|
||||
} else if (phosg::starts_with(token, "user-flags=")) {
|
||||
account->user_flags = parse_account_user_flags(token.substr(11));
|
||||
} else {
|
||||
throw invalid_argument("invalid account field: " + token);
|
||||
}
|
||||
@@ -414,7 +456,8 @@ CommandDefinition c_update_account(
|
||||
Update an existing license. ACCOUNT-ID (8 hex digits) specifies which\n\
|
||||
account to update. The options are similar to the add-account command:\n\
|
||||
flags=FLAGS: sets behaviors and permissions for the account (same as\n\
|
||||
with add-account)\n\
|
||||
add-account)\n\
|
||||
user-flags=FLAGS: sets behaviors for the account (same as add-account)\n\
|
||||
ban-duration=DURATION: bans this account for the specified duration; the\n\
|
||||
duration should be of the form 3d, 2w, 1mo, or 1y\n\
|
||||
unban: clears any existing ban from this account\n\
|
||||
@@ -437,6 +480,7 @@ CommandDefinition c_update_account(
|
||||
int64_t new_ep3_current_meseta = -1;
|
||||
int64_t new_ep3_total_meseta = -1;
|
||||
int64_t new_flags = -1;
|
||||
int64_t new_user_flags = -1;
|
||||
uint8_t new_is_temporary = 0xFF;
|
||||
int64_t new_ban_duration = -1;
|
||||
for (const string& token : tokens) {
|
||||
@@ -450,6 +494,8 @@ CommandDefinition c_update_account(
|
||||
new_is_temporary = 0;
|
||||
} else if (phosg::starts_with(token, "flags=")) {
|
||||
new_flags = parse_account_flags(token.substr(6));
|
||||
} else if (phosg::starts_with(token, "user-flags=")) {
|
||||
new_user_flags = parse_account_user_flags(token.substr(11));
|
||||
} else if (token == "unban") {
|
||||
new_ban_duration = 0;
|
||||
} else if (phosg::starts_with(token, "ban-duration=")) {
|
||||
@@ -488,6 +534,9 @@ CommandDefinition c_update_account(
|
||||
if (new_flags >= 0) {
|
||||
account->flags = new_flags;
|
||||
}
|
||||
if (new_user_flags >= 0) {
|
||||
account->user_flags = new_user_flags;
|
||||
}
|
||||
if (new_is_temporary != 0xFF) {
|
||||
account->is_temporary = new_is_temporary;
|
||||
}
|
||||
@@ -873,6 +922,47 @@ CommandDefinition c_describe_tournament(
|
||||
}
|
||||
});
|
||||
|
||||
CommandDefinition c_cc(
|
||||
"cc", "cc COMMAND\n\
|
||||
Execute a chat command as if a client had sent it in-game. The command\n\
|
||||
should be specified exactly as it would be typed in-game; for example:\n\
|
||||
cc $itemnotifs on\n\
|
||||
This command cannot send chat messages to other players or to the server\n\
|
||||
(in proxy sessions); it can only execute chat commands.",
|
||||
true,
|
||||
+[](CommandArgs& args) {
|
||||
shared_ptr<ProxyServer::LinkedSession> ses;
|
||||
try {
|
||||
ses = args.shell->get_proxy_session(args.session_name);
|
||||
} catch (const exception&) {
|
||||
}
|
||||
|
||||
if (ses.get()) {
|
||||
on_chat_command(ses, args.args);
|
||||
} else {
|
||||
shared_ptr<Client> c;
|
||||
if (args.session_name.empty()) {
|
||||
c = args.s->game_server->get_client();
|
||||
} else {
|
||||
auto clients = args.s->game_server->get_clients_by_identifier(args.session_name);
|
||||
if (clients.empty()) {
|
||||
throw runtime_error("no such client");
|
||||
}
|
||||
if (clients.size() > 1) {
|
||||
throw runtime_error("multiple clients found");
|
||||
}
|
||||
c = std::move(clients[0]);
|
||||
}
|
||||
|
||||
if (c) {
|
||||
on_chat_command(c, args.args);
|
||||
} else {
|
||||
throw runtime_error("no client available");
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
|
||||
void f_sc_ss(CommandArgs& args) {
|
||||
string data = phosg::parse_data_string(args.args, nullptr, phosg::ParseDataFlags::ALLOW_FILES);
|
||||
if (data.size() == 0) {
|
||||
@@ -1101,6 +1191,10 @@ CommandDefinition c_close_idle_sessions(
|
||||
"close-idle-sessions", "close-idle-sessions\n\
|
||||
Close all proxy sessions that don\'t have a client and server connected.",
|
||||
true, +[](CommandArgs& args) {
|
||||
size_t count = args.s->proxy_server->delete_disconnected_sessions();
|
||||
fprintf(stderr, "%zu sessions closed\n", count);
|
||||
if (args.s->proxy_server) {
|
||||
size_t count = args.s->proxy_server->delete_disconnected_sessions();
|
||||
fprintf(stderr, "%zu sessions closed\n", count);
|
||||
} else {
|
||||
throw runtime_error("the proxy server is disabled");
|
||||
}
|
||||
});
|
||||
|
||||
+128
-38
@@ -5,6 +5,7 @@
|
||||
#include <memory>
|
||||
#include <phosg/Image.hh>
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Platform.hh>
|
||||
|
||||
#include "Compression.hh"
|
||||
#include "EventUtils.hh"
|
||||
@@ -19,6 +20,33 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
#ifdef PHOSG_WINDOWS
|
||||
static constexpr bool IS_WINDOWS = true;
|
||||
#else
|
||||
static constexpr bool IS_WINDOWS = false;
|
||||
#endif
|
||||
|
||||
CheatFlags::CheatFlags(const phosg::JSON& json) : CheatFlags() {
|
||||
unordered_set<std::string> enabled_keys;
|
||||
for (const auto& it : json.as_list()) {
|
||||
enabled_keys.emplace(it->as_string());
|
||||
}
|
||||
|
||||
this->create_items = enabled_keys.count("CreateItems");
|
||||
this->edit_section_id = enabled_keys.count("EditSectionID");
|
||||
this->edit_stats = enabled_keys.count("EditStats");
|
||||
this->ep3_replace_assist = enabled_keys.count("Ep3ReplaceAssist");
|
||||
this->ep3_unset_field_character = enabled_keys.count("Ep3UnsetFieldCharacter");
|
||||
this->infinite_hp_tp = enabled_keys.count("InfiniteHPTP");
|
||||
this->insufficient_minimum_level = enabled_keys.count("InsufficientMinimumLevel");
|
||||
this->override_random_seed = enabled_keys.count("OverrideRandomSeed");
|
||||
this->override_section_id = enabled_keys.count("OverrideSectionID");
|
||||
this->override_variations = enabled_keys.count("OverrideVariations");
|
||||
this->proxy_override_drops = enabled_keys.count("ProxyOverrideDrops");
|
||||
this->reset_materials = enabled_keys.count("ResetMaterials");
|
||||
this->warp = enabled_keys.count("Warp");
|
||||
}
|
||||
|
||||
ServerState::QuestF960Result::QuestF960Result(const phosg::JSON& json, shared_ptr<const ItemNameIndex> name_index) {
|
||||
static const array<string, 7> day_names = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
|
||||
this->meseta_cost = json.get_int("MesetaCost", 0);
|
||||
@@ -44,6 +72,9 @@ ServerState::ServerState(shared_ptr<struct event_base> base, const string& confi
|
||||
base(base),
|
||||
config_filename(config_filename),
|
||||
is_replay(is_replay),
|
||||
bb_stream_files_cache(new FileContentsCache(3600000000ULL)),
|
||||
bb_system_cache(new FileContentsCache(3600000000ULL)),
|
||||
gba_files_cache(new FileContentsCache(3600000000ULL)),
|
||||
player_files_manager(this->base ? make_shared<PlayerFilesManager>(base) : nullptr),
|
||||
destroy_lobbies_event(this->base ? event_new(base.get(), -1, EV_TIMEOUT, &ServerState::dispatch_destroy_lobbies, this) : nullptr, event_free) {}
|
||||
|
||||
@@ -257,15 +288,21 @@ uint32_t ServerState::connect_address_for_client(shared_ptr<Client> c) const {
|
||||
}
|
||||
const auto* sin = reinterpret_cast<const sockaddr_in*>(&c->channel.remote_addr);
|
||||
return IPStackSimulator::connect_address_for_remote_address(ntohl(sin->sin_addr.s_addr));
|
||||
} else {
|
||||
// TODO: we can do something smarter here, like use the sockname to find
|
||||
// out which interface the client is connected to, and return that address
|
||||
if (is_local_address(c->channel.remote_addr)) {
|
||||
return this->local_address;
|
||||
} else {
|
||||
return this->external_address;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ret = is_local_address(c->channel.remote_addr) ? this->local_address : this->external_address;
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct sockaddr_storage addr;
|
||||
phosg::get_socket_addresses(bufferevent_getfd(c->channel.bev.get()), &addr, nullptr);
|
||||
if (addr.ss_family == AF_INET) {
|
||||
const sockaddr_in* sin = reinterpret_cast<const sockaddr_in*>(&addr);
|
||||
return ntohl(sin->sin_addr.s_addr);
|
||||
}
|
||||
|
||||
throw runtime_error("no connect address available");
|
||||
}
|
||||
|
||||
shared_ptr<const Menu> ServerState::information_menu(Version version) const {
|
||||
@@ -517,9 +554,8 @@ shared_ptr<const string> ServerState::load_bb_file(
|
||||
|
||||
// Finally, look in system/blueburst
|
||||
const string& effective_bb_directory_filename = bb_directory_filename.empty() ? patch_index_filename : bb_directory_filename;
|
||||
static FileContentsCache cache(10 * 60 * 1000 * 1000); // 10 minutes
|
||||
try {
|
||||
auto ret = cache.get_or_load("system/blueburst/" + effective_bb_directory_filename);
|
||||
auto ret = this->bb_system_cache->get_or_load("system/blueburst/" + effective_bb_directory_filename);
|
||||
return ret.file->data;
|
||||
} catch (const exception& e) {
|
||||
throw phosg::cannot_open_file(patch_index_filename);
|
||||
@@ -644,8 +680,10 @@ void ServerState::load_config_early() {
|
||||
for (const auto& item : this->config_json->at("IPStackListen").as_list()) {
|
||||
if (item->is_int()) {
|
||||
this->ip_stack_addresses.emplace_back(phosg::string_printf("0.0.0.0:%" PRId64, item->as_int()));
|
||||
} else {
|
||||
} else if (!IS_WINDOWS) {
|
||||
this->ip_stack_addresses.emplace_back(item->as_string());
|
||||
} else {
|
||||
config_log.warning("Unix sockets are not supported on Windows; skipping address %s", item->as_string().c_str());
|
||||
}
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
@@ -654,8 +692,10 @@ void ServerState::load_config_early() {
|
||||
for (const auto& item : this->config_json->at("PPPStackListen").as_list()) {
|
||||
if (item->is_int()) {
|
||||
this->ppp_stack_addresses.emplace_back(phosg::string_printf("0.0.0.0:%" PRId64, item->as_int()));
|
||||
} else {
|
||||
} else if (!IS_WINDOWS) {
|
||||
this->ppp_stack_addresses.emplace_back(item->as_string());
|
||||
} else {
|
||||
config_log.warning("Unix sockets are not supported on Windows; skipping address %s", item->as_string().c_str());
|
||||
}
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
@@ -664,8 +704,10 @@ void ServerState::load_config_early() {
|
||||
for (const auto& item : this->config_json->at("PPPRawListen").as_list()) {
|
||||
if (item->is_int()) {
|
||||
this->ppp_raw_addresses.emplace_back(phosg::string_printf("0.0.0.0:%" PRId64, item->as_int()));
|
||||
} else {
|
||||
} else if (!IS_WINDOWS) {
|
||||
this->ppp_raw_addresses.emplace_back(item->as_string());
|
||||
} else {
|
||||
config_log.warning("Unix sockets are not supported on Windows; skipping address %s", item->as_string().c_str());
|
||||
}
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
@@ -674,8 +716,10 @@ void ServerState::load_config_early() {
|
||||
for (const auto& item : this->config_json->at("HTTPListen").as_list()) {
|
||||
if (item->is_int()) {
|
||||
this->http_addresses.emplace_back(phosg::string_printf("0.0.0.0:%" PRId64, item->as_int()));
|
||||
} else {
|
||||
} else if (!IS_WINDOWS) {
|
||||
this->http_addresses.emplace_back(item->as_string());
|
||||
} else {
|
||||
config_log.warning("Unix sockets are not supported on Windows; skipping address %s", item->as_string().c_str());
|
||||
}
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
@@ -684,31 +728,62 @@ void ServerState::load_config_early() {
|
||||
this->one_time_config_loaded = true;
|
||||
}
|
||||
|
||||
auto local_address_str = this->config_json->at("LocalAddress").as_string();
|
||||
try {
|
||||
this->local_address = this->all_addresses.at(local_address_str);
|
||||
string addr_str = string_for_address(this->local_address);
|
||||
config_log.info("Added local address: %s (%s)", addr_str.c_str(),
|
||||
local_address_str.c_str());
|
||||
auto local_address_str = this->config_json->at("LocalAddress").as_string();
|
||||
try {
|
||||
this->local_address = this->all_addresses.at(local_address_str);
|
||||
string addr_str = string_for_address(this->local_address);
|
||||
config_log.info("Added local address: %s (%s)", addr_str.c_str(),
|
||||
local_address_str.c_str());
|
||||
} catch (const out_of_range&) {
|
||||
this->local_address = address_for_string(local_address_str.c_str());
|
||||
config_log.info("Added local address: %s", local_address_str.c_str());
|
||||
}
|
||||
this->all_addresses.erase("<local>");
|
||||
this->all_addresses.emplace("<local>", this->local_address);
|
||||
} catch (const out_of_range&) {
|
||||
this->local_address = address_for_string(local_address_str.c_str());
|
||||
config_log.info("Added local address: %s", local_address_str.c_str());
|
||||
for (const auto& it : this->all_addresses) {
|
||||
// Choose any local interface except the loopback interface
|
||||
if (!is_loopback_address(it.second) && is_local_address(it.second)) {
|
||||
this->local_address = it.second;
|
||||
}
|
||||
}
|
||||
if (this->local_address) {
|
||||
string addr_str = string_for_address(this->local_address);
|
||||
config_log.warning("Local address not specified; using %s as default", addr_str.c_str());
|
||||
} else {
|
||||
config_log.warning("Local address not specified and no default is available");
|
||||
}
|
||||
}
|
||||
this->all_addresses.erase("<local>");
|
||||
this->all_addresses.emplace("<local>", this->local_address);
|
||||
|
||||
auto external_address_str = this->config_json->at("ExternalAddress").as_string();
|
||||
try {
|
||||
this->external_address = this->all_addresses.at(external_address_str);
|
||||
string addr_str = string_for_address(this->external_address);
|
||||
config_log.info("Added external address: %s (%s)", addr_str.c_str(),
|
||||
external_address_str.c_str());
|
||||
auto external_address_str = this->config_json->at("ExternalAddress").as_string();
|
||||
try {
|
||||
this->external_address = this->all_addresses.at(external_address_str);
|
||||
string addr_str = string_for_address(this->external_address);
|
||||
config_log.info("Added external address: %s (%s)", addr_str.c_str(),
|
||||
external_address_str.c_str());
|
||||
} catch (const out_of_range&) {
|
||||
this->external_address = address_for_string(external_address_str.c_str());
|
||||
config_log.info("Added external address: %s", external_address_str.c_str());
|
||||
}
|
||||
this->all_addresses.erase("<external>");
|
||||
this->all_addresses.emplace("<external>", this->external_address);
|
||||
} catch (const out_of_range&) {
|
||||
this->external_address = address_for_string(external_address_str.c_str());
|
||||
config_log.info("Added external address: %s", external_address_str.c_str());
|
||||
for (const auto& it : this->all_addresses) {
|
||||
// Choose any non-local address, if any exist
|
||||
if (!is_local_address(it.second)) {
|
||||
this->external_address = it.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this->external_address) {
|
||||
string addr_str = string_for_address(this->external_address);
|
||||
config_log.warning("External address not specified; using %s as default", addr_str.c_str());
|
||||
} else {
|
||||
config_log.warning("External address not specified and no default is available; only local clients will be able to connect");
|
||||
}
|
||||
}
|
||||
this->all_addresses.erase("<external>");
|
||||
this->all_addresses.emplace("<external>", this->external_address);
|
||||
|
||||
try {
|
||||
this->banned_ipv4_ranges = make_shared<IPV4RangeSet>(this->config_json->at("BannedIPV4Ranges"));
|
||||
@@ -1191,6 +1266,12 @@ void ServerState::load_config_early() {
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
try {
|
||||
this->cheat_flags = CheatFlags(this->config_json->at("CheatingBehaviors"));
|
||||
} catch (const out_of_range&) {
|
||||
this->cheat_flags = CheatFlags();
|
||||
}
|
||||
|
||||
this->update_dependent_server_configs();
|
||||
}
|
||||
|
||||
@@ -1451,11 +1532,20 @@ void ServerState::load_patch_indexes(bool from_non_event_thread) {
|
||||
this->forward_or_call(from_non_event_thread, std::move(set));
|
||||
}
|
||||
|
||||
void ServerState::clear_map_file_caches() {
|
||||
config_log.info("Clearing map file caches");
|
||||
for (auto& cache : this->map_file_caches) {
|
||||
cache = make_shared<ThreadSafeFileCache>();
|
||||
}
|
||||
void ServerState::clear_file_caches(bool from_non_event_thread) {
|
||||
auto set = [s = this->shared_from_this()]() {
|
||||
config_log.info("Clearing map file caches");
|
||||
for (auto& cache : s->map_file_caches) {
|
||||
cache = make_shared<ThreadSafeFileCache>();
|
||||
}
|
||||
config_log.info("Clearing BB stream file cache");
|
||||
s->bb_stream_files_cache.reset(new FileContentsCache(3600000000ULL));
|
||||
config_log.info("Clearing BB system cache");
|
||||
s->bb_system_cache.reset(new FileContentsCache(3600000000ULL));
|
||||
config_log.info("Clearing GBA file cache");
|
||||
s->gba_files_cache.reset(new FileContentsCache(300 * 1000 * 1000));
|
||||
};
|
||||
this->forward_or_call(from_non_event_thread, std::move(set));
|
||||
}
|
||||
|
||||
void ServerState::load_set_data_tables(bool from_non_event_thread) {
|
||||
@@ -1942,7 +2032,7 @@ void ServerState::load_all() {
|
||||
this->load_bb_private_keys(false);
|
||||
this->load_bb_system_defaults(false);
|
||||
this->load_accounts(false);
|
||||
this->clear_map_file_caches();
|
||||
this->clear_file_caches(false);
|
||||
this->load_patch_indexes(false);
|
||||
this->load_ep3_cards(false);
|
||||
this->load_ep3_maps(false);
|
||||
|
||||
+29
-2
@@ -46,6 +46,29 @@ struct PortConfiguration {
|
||||
ServerBehavior behavior;
|
||||
};
|
||||
|
||||
struct CheatFlags {
|
||||
// This structure describes which behaviors are considered cheating (that is,
|
||||
// require cheat mode to be enabled or the user to have the CHEAT_ANYWHERE
|
||||
// account flag). A false value here means that that particular behavior is
|
||||
// NOT cheating, so cheat mode is NOT required.
|
||||
bool create_items = true;
|
||||
bool edit_section_id = true;
|
||||
bool edit_stats = true;
|
||||
bool ep3_replace_assist = true;
|
||||
bool ep3_unset_field_character = true;
|
||||
bool infinite_hp_tp = true;
|
||||
bool insufficient_minimum_level = true;
|
||||
bool override_random_seed = true;
|
||||
bool override_section_id = true;
|
||||
bool override_variations = true;
|
||||
bool proxy_override_drops = true;
|
||||
bool reset_materials = false;
|
||||
bool warp = true;
|
||||
|
||||
CheatFlags() = default;
|
||||
explicit CheatFlags(const phosg::JSON& json);
|
||||
};
|
||||
|
||||
struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
enum class RunShellBehavior {
|
||||
DEFAULT = 0,
|
||||
@@ -151,6 +174,9 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::shared_ptr<const PatchFileIndex> pc_patch_file_index;
|
||||
std::shared_ptr<const PatchFileIndex> bb_patch_file_index;
|
||||
std::array<std::shared_ptr<ThreadSafeFileCache>, NUM_VERSIONS> map_file_caches;
|
||||
std::shared_ptr<FileContentsCache> bb_stream_files_cache;
|
||||
std::shared_ptr<FileContentsCache> bb_system_cache;
|
||||
std::shared_ptr<FileContentsCache> gba_files_cache;
|
||||
std::shared_ptr<const DOLFileIndex> dol_file_index;
|
||||
std::shared_ptr<const Episode3::CardIndex> ep3_card_index;
|
||||
std::shared_ptr<const Episode3::CardIndex> ep3_card_index_trial;
|
||||
@@ -162,7 +188,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::shared_ptr<const QuestCategoryIndex> quest_category_index;
|
||||
std::shared_ptr<const QuestIndex> default_quest_index;
|
||||
std::shared_ptr<const QuestIndex> ep3_download_quest_index;
|
||||
std::shared_ptr<const LevelTable> level_table_v1_v2;
|
||||
std::shared_ptr<const LevelTableV2> level_table_v1_v2;
|
||||
std::shared_ptr<const LevelTable> level_table_v3;
|
||||
std::shared_ptr<const LevelTable> level_table_v4;
|
||||
std::shared_ptr<const BattleParamsIndex> battle_params;
|
||||
@@ -188,6 +214,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::shared_ptr<const Map::RareEnemyRates> rare_enemy_rates_challenge;
|
||||
std::array<std::array<size_t, 4>, 3> min_levels_v4; // Indexed as [episode][difficulty]
|
||||
std::vector<std::string> bb_required_patches;
|
||||
CheatFlags cheat_flags;
|
||||
|
||||
struct QuestF960Result {
|
||||
uint32_t meseta_cost = 0;
|
||||
@@ -386,7 +413,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
void load_accounts(bool from_non_event_thread);
|
||||
void load_teams(bool from_non_event_thread);
|
||||
void load_patch_indexes(bool from_non_event_thread);
|
||||
void clear_map_file_caches();
|
||||
void clear_file_caches(bool from_non_event_thread);
|
||||
void load_battle_params(bool from_non_event_thread);
|
||||
void load_level_tables(bool from_non_event_thread);
|
||||
void load_text_index(bool from_non_event_thread);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "SignalWatcher.hh"
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Random.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
#include "ReceiveCommands.hh"
|
||||
#include "SendCommands.hh"
|
||||
#include "ServerState.hh"
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
SignalWatcher::SignalWatcher(shared_ptr<ServerState> state)
|
||||
: log("[SignalWatcher] "),
|
||||
state(state),
|
||||
sigusr1_event(evsignal_new(this->state->base.get(), SIGUSR1, &SignalWatcher::dispatch_on_signal, this), event_free),
|
||||
sigusr2_event(evsignal_new(this->state->base.get(), SIGUSR2, &SignalWatcher::dispatch_on_signal, this), event_free) {
|
||||
event_add(this->sigusr1_event.get(), nullptr);
|
||||
event_add(this->sigusr2_event.get(), nullptr);
|
||||
}
|
||||
|
||||
void SignalWatcher::dispatch_on_signal(evutil_socket_t signal, short what, void* ctx) {
|
||||
if (what != EV_SIGNAL) {
|
||||
throw logic_error("dispatch_on_signal called for non-signal event");
|
||||
}
|
||||
reinterpret_cast<SignalWatcher*>(ctx)->on_signal(signal);
|
||||
}
|
||||
|
||||
void SignalWatcher::on_signal(int signum) {
|
||||
switch (signum) {
|
||||
case SIGUSR1:
|
||||
this->log.info("Received SIGUSR1; reloading config.json");
|
||||
this->state->forward_to_event_thread([s = this->state]() {
|
||||
try {
|
||||
s->load_config_early();
|
||||
s->load_config_late();
|
||||
fprintf(stderr, "Configuration update complete\n");
|
||||
} catch (const exception& e) {
|
||||
fprintf(stderr, "FAILED: %s\n", e.what());
|
||||
fprintf(stderr, "Some configuration may have been reloaded. Fix the underlying issue and try again.\n");
|
||||
}
|
||||
});
|
||||
break;
|
||||
case SIGUSR2:
|
||||
this->log.info("Received SIGUSR2; reloading config.json and all dependencies");
|
||||
this->state->forward_to_event_thread([s = this->state]() {
|
||||
try {
|
||||
s->load_all();
|
||||
fprintf(stderr, "Configuration update complete\n");
|
||||
} catch (const exception& e) {
|
||||
fprintf(stderr, "FAILED: %s\n", e.what());
|
||||
fprintf(stderr, "Some configuration may have been reloaded. Fix the underlying issue and try again.\n");
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this->log.warning("Unknown signal received: %d", signum);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include "ServerState.hh"
|
||||
|
||||
class SignalWatcher : public std::enable_shared_from_this<SignalWatcher> {
|
||||
public:
|
||||
explicit SignalWatcher(std::shared_ptr<ServerState> state);
|
||||
SignalWatcher(const SignalWatcher&) = delete;
|
||||
SignalWatcher(SignalWatcher&&) = delete;
|
||||
SignalWatcher& operator=(const SignalWatcher&) = delete;
|
||||
SignalWatcher& operator=(SignalWatcher&&) = delete;
|
||||
~SignalWatcher() = default;
|
||||
|
||||
protected:
|
||||
phosg::PrefixedLogger log;
|
||||
std::shared_ptr<ServerState> state;
|
||||
std::unique_ptr<struct event, void (*)(struct event*)> sigusr1_event;
|
||||
std::unique_ptr<struct event, void (*)(struct event*)> sigusr2_event;
|
||||
|
||||
static void dispatch_on_signal(evutil_socket_t, short, void* ctx);
|
||||
void on_signal(int signum);
|
||||
};
|
||||
+57
-11
@@ -220,10 +220,56 @@ const unordered_map<string, uint8_t> name_to_lobby_type({
|
||||
{"morgue", 0xFF},
|
||||
});
|
||||
|
||||
const vector<string> npc_id_to_name({"ninja", "rico", "sonic", "knuckles", "tails", "flowen", "elly"});
|
||||
const vector<string> npc_id_to_name({
|
||||
"ninja",
|
||||
"rico",
|
||||
"sonic",
|
||||
"knuckles",
|
||||
"tails",
|
||||
"flowen",
|
||||
"elly",
|
||||
"momoka",
|
||||
"irene",
|
||||
"guild",
|
||||
"nurse",
|
||||
});
|
||||
|
||||
const unordered_map<string, uint8_t> name_to_npc_id = {
|
||||
{"ninja", 0}, {"rico", 1}, {"sonic", 2}, {"knuckles", 3}, {"tails", 4}, {"flowen", 5}, {"elly", 6}};
|
||||
{"ninja", 0},
|
||||
{"rico", 1},
|
||||
{"sonic", 2},
|
||||
{"knuckles", 3},
|
||||
{"tails", 4},
|
||||
{"flowen", 5},
|
||||
{"elly", 6},
|
||||
{"momoka", 7},
|
||||
{"irene", 8},
|
||||
{"guild", 9},
|
||||
{"nurse", 10},
|
||||
};
|
||||
|
||||
bool npc_valid_for_version(uint8_t npc, Version version) {
|
||||
switch (version) {
|
||||
case Version::DC_NTE:
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
case Version::DC_V1:
|
||||
return false;
|
||||
case Version::DC_V2:
|
||||
case Version::PC_NTE:
|
||||
case Version::PC_V2:
|
||||
return (npc < 5);
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3:
|
||||
return (npc < 7);
|
||||
case Version::BB_V4:
|
||||
return (npc < 11);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const char* abbreviation_for_section_id(uint8_t section_id) {
|
||||
if (section_id < section_id_to_abbreviation.size()) {
|
||||
@@ -317,20 +363,20 @@ const string& name_for_npc(uint8_t npc) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t npc_for_name(const string& name) {
|
||||
uint8_t npc_for_name(const string& name, Version version) {
|
||||
uint8_t npc_id = 0xFF;
|
||||
try {
|
||||
return name_to_npc_id.at(name);
|
||||
npc_id = name_to_npc_id.at(name);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
uint64_t x = stoul(name);
|
||||
if (x < npc_id_to_name.size()) {
|
||||
return x;
|
||||
if (npc_id == 0xFF) {
|
||||
try {
|
||||
npc_id = stoul(name);
|
||||
} catch (const invalid_argument&) {
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
} catch (const invalid_argument&) {
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
return 0xFF;
|
||||
return npc_valid_for_version(npc_id, version) ? npc_id : 0xFF;
|
||||
}
|
||||
|
||||
const char* name_for_char_class(uint8_t cls) {
|
||||
|
||||
@@ -50,7 +50,8 @@ const std::string& name_for_lobby_type(uint8_t type);
|
||||
uint8_t lobby_type_for_name(const std::string& name);
|
||||
|
||||
const std::string& name_for_npc(uint8_t npc);
|
||||
uint8_t npc_for_name(const std::string& name);
|
||||
uint8_t npc_for_name(const std::string& name, Version version);
|
||||
bool npc_valid_for_version(uint8_t npc, Version version);
|
||||
|
||||
const char* name_for_char_class(uint8_t cls);
|
||||
const char* abbreviation_for_char_class(uint8_t cls);
|
||||
|
||||
+1
-1
@@ -610,7 +610,7 @@ struct pstring {
|
||||
if (this->data[1] == 'J') {
|
||||
client_language = 0;
|
||||
offset = 2;
|
||||
} else {
|
||||
} else if (this->data[1] != 'C') {
|
||||
client_language = 1;
|
||||
offset = 2;
|
||||
}
|
||||
|
||||
@@ -280,6 +280,20 @@ void WordSelectTable::print_index(FILE* stream, Version v) const {
|
||||
}
|
||||
}
|
||||
|
||||
void WordSelectTable::validate(const WordSelectMessage& msg, Version version) const {
|
||||
const auto& index = this->tokens_for_version(version);
|
||||
|
||||
for (size_t z = 0; z < msg.tokens.size(); z++) {
|
||||
if (msg.tokens[z] == 0xFFFF) {
|
||||
continue;
|
||||
}
|
||||
const auto& token = index.at(msg.tokens[z]);
|
||||
if (!token) {
|
||||
throw runtime_error(phosg::string_printf("token %04hX does not exist in the index", msg.tokens[z].load()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WordSelectMessage WordSelectTable::translate(
|
||||
const WordSelectMessage& msg,
|
||||
Version from_version,
|
||||
|
||||
@@ -81,10 +81,8 @@ public:
|
||||
void print(FILE* stream) const;
|
||||
void print_index(FILE* stream, Version v) const;
|
||||
|
||||
WordSelectMessage translate(
|
||||
const WordSelectMessage& msg,
|
||||
Version from_version,
|
||||
Version to_version) const;
|
||||
void validate(const WordSelectMessage& msg, Version version) const; // Throws runtime_error if invalid
|
||||
WordSelectMessage translate(const WordSelectMessage& msg, Version from_version, Version to_version) const;
|
||||
|
||||
private:
|
||||
struct Token {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# It would be a bad idea to remove `.meta hide_from_patches_menu` to make this
|
||||
# patch an option for players to be able to select; either all players on the
|
||||
# server should have this patch, or none should have it.
|
||||
|
||||
# This patch clears the list of unreleased items on the client, so the client
|
||||
# never creates buggy items when the server generates an item that wasn't
|
||||
# released on the official servers.
|
||||
|
||||
.meta name="Clear unreleased item list"
|
||||
.meta description=""
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
xor eax, eax
|
||||
mov edx, esp
|
||||
mov esp, 0x009F81B0
|
||||
mov ecx, 0x3C
|
||||
again:
|
||||
push 0
|
||||
dec ecx
|
||||
jnz again
|
||||
mov esp, edx
|
||||
ret
|
||||
+3
-3
@@ -66,13 +66,13 @@ max_stack_size_for_tool_start:
|
||||
# e.g. tech disks this would be 02). For classes beyond 15, the value for 15
|
||||
# is used.
|
||||
# Index: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15
|
||||
.binary 0A 0A 01 0A 0A 0A 0A 0A 0A 01 63 63 01 01 01 01 63 01 01 01 01 01
|
||||
.binary 0A 0A 01 0A 0A 0A 0A 0A 0A 01 01 01 01 01 01 01 63 01 01 01 01 01
|
||||
data_end:
|
||||
|
||||
# eax = min<uint8_t>(data1[1], 0x11)
|
||||
# eax = min<uint8_t>(data1[1], 0x15)
|
||||
mov al, [ecx + 1]
|
||||
xor edx, edx
|
||||
mov dl, 0x11
|
||||
mov dl, 0x15
|
||||
cmp eax, edx
|
||||
cmovg eax, edx
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Bug fixes"
|
||||
.meta description="Fix many minor\ngameplay, sound,\nand graphical bugs"
|
||||
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Bug fixes"
|
||||
.meta description="Fix many minor\ngameplay, sound,\nand graphical bugs"
|
||||
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Bug fixes"
|
||||
.meta description="Fix many minor\ngameplay, sound,\nand graphical bugs"
|
||||
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Bug fixes"
|
||||
.meta description="Fix many minor\ngameplay, sound,\nand graphical bugs"
|
||||
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Bug fixes"
|
||||
.meta description="Fix many minor\ngameplay, sound,\nand graphical bugs"
|
||||
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Bug fixes"
|
||||
.meta description="Fix many minor\ngameplay, sound,\nand graphical bugs"
|
||||
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Bug fixes"
|
||||
.meta description="Fix many minor\ngameplay, sound,\nand graphical bugs"
|
||||
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Bug fixes"
|
||||
.meta description="Fix many minor\ngameplay, sound,\nand graphical bugs"
|
||||
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Bug fixes (WIP)"
|
||||
.meta description="Fix many minor\ngameplay, sound,\nand graphical bugs"
|
||||
.meta description="Fixes many minor\ngameplay, sound,\nand graphical bugs"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enable extended\nWord Select and\nstop the Log Window\nfrom scrolling with L+R"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enable extended\nWord Select and\nstop the Log Window\nfrom scrolling with L+R"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enable extended\nWord Select and\nstop the Log Window\nfrom scrolling with L+R"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enable extended\nWord Select and\nstop the Log Window\nfrom scrolling with L+R"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enable extended\nWord Select and\nstop the Log Window\nfrom scrolling with L+R"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enable extended\nWord Select and\nstop the Log Window\nfrom scrolling with L+R"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enable extended\nWord Select and\nstop the Log Window\nfrom scrolling with L+R"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enable extended\nWord Select and\nstop the Log Window\nfrom scrolling with L+R"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
.data 0x8000D6A0
|
||||
.data 0x0000001C
|
||||
lis r3, 0x804A
|
||||
lhz r3, [r3 + 0x0098]
|
||||
andi. r0, r3, 0x0003
|
||||
cmplwi r0, 3
|
||||
beqlr
|
||||
stfs [r28 + 0x0084], f1
|
||||
blr
|
||||
|
||||
.data 0x8016FC9C
|
||||
.data 0x00000004
|
||||
.data 0x4BE9DA05
|
||||
|
||||
.data 0x801C7D88
|
||||
.data 0x00000004
|
||||
li r3, 0x0000
|
||||
|
||||
.data 0x00000000
|
||||
.data 0x00000000
|
||||
@@ -0,0 +1,32 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
.data 0x8000D6A0
|
||||
.data 0x0000001C
|
||||
lis r3, 0x804A
|
||||
lhz r3, [r3 - 0x0D88]
|
||||
andi. r0, r3, 0x0003
|
||||
cmplwi r0, 3
|
||||
beqlr
|
||||
stfs [r28 + 0x0084], f1
|
||||
blr
|
||||
|
||||
.data 0x8016FDE8
|
||||
.data 0x00000004
|
||||
.data 0x4BE9D8B9
|
||||
|
||||
.data 0x801C7CFC
|
||||
.data 0x00000004
|
||||
li r3, 0x0000
|
||||
|
||||
.data 0x00000000
|
||||
.data 0x00000000
|
||||
@@ -0,0 +1,32 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
.data 0x8000D6A0
|
||||
.data 0x0000001C
|
||||
lis r3, 0x8048
|
||||
lhz r3, [r3 + 0x1238]
|
||||
andi. r0, r3, 0x0003
|
||||
cmplwi r0, 3
|
||||
beqlr
|
||||
stfs [r28 + 0x0084], f1
|
||||
blr
|
||||
|
||||
.data 0x8017F51C
|
||||
.data 0x00000004
|
||||
.data 0x4BE8E185
|
||||
|
||||
.data 0x801D9B30
|
||||
.data 0x00000004
|
||||
li r3, 0x0000
|
||||
|
||||
.data 0x00000000
|
||||
.data 0x00000000
|
||||
@@ -0,0 +1,32 @@
|
||||
.meta name="Chat"
|
||||
.meta description="Enables extended\nWord Select and\nstops the Log\nWindow from\nscrolling with L+R"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
.data 0x8000D6A0
|
||||
.data 0x0000001C
|
||||
lis r3, 0x804A
|
||||
lhz r3, [r3 + 0x24B8]
|
||||
andi. r0, r3, 0x0003
|
||||
cmplwi r0, 3
|
||||
beqlr
|
||||
stfs [r28 + 0x0084], f1
|
||||
blr
|
||||
|
||||
.data 0x80170148
|
||||
.data 0x00000004
|
||||
.data 0x4BE9D559
|
||||
|
||||
.data 0x801C83FC
|
||||
.data 0x00000004
|
||||
li r3, 0x0000
|
||||
|
||||
.data 0x00000000
|
||||
.data 0x00000000
|
||||
@@ -1,5 +1,5 @@
|
||||
.meta name="Decoction"
|
||||
.meta description="Make the Decoction\nitem reset your\nmaterial usage"
|
||||
.meta description="Makes the Decoction\nitem reset your\nmaterial usage"
|
||||
# Original code by Ralf @ GC-Forever and Aleron Ives
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2050
|
||||
# https://www.gc-forever.com/forums/viewtopic.php?t=2049
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user