Compare commits

..

88 Commits

Author SHA1 Message Date
Martin Michelsen 2dc2f85b1a don't disconnect on duplicate EXP requests 2023-12-10 21:45:13 -08:00
Martin Michelsen 6ef5b59724 fix log level on ItemCreator 2023-12-10 21:31:51 -08:00
Martin Michelsen 2f32e8ab7d fix BB inventory desync when buying consumables from shop 2023-12-10 21:16:42 -08:00
Martin Michelsen 60c1aa71dc fix ToolV4 structure 2023-12-10 17:35:57 -08:00
Martin Michelsen a315f6d011 expand metadata comments in q058 2023-12-10 15:22:40 -08:00
Martin Michelsen a42594afd3 fix implicit signed conversion 2023-12-10 14:54:30 -08:00
Martin Michelsen 04445dabd0 fix default BB key config 2023-12-10 14:52:17 -08:00
Martin Michelsen 16cddd28b2 add quest script compiler 2023-12-10 14:24:30 -08:00
Martin Michelsen b53bde9046 expand comment on expand_rate 2023-12-10 09:16:59 -08:00
Martin Michelsen afd63ca1dd make ep2 quests have orange icon on non-BB versions 2023-12-09 19:21:31 -08:00
Martin Michelsen 8ae7696b51 fix $exit when v3 quests are loaded in ep3 games 2023-12-09 19:21:31 -08:00
Martin Michelsen 81d03738da enable $quest to load v3 quests on ep3 2023-12-09 19:10:54 -08:00
Martin Michelsen beb87b546f clean up map logging 2023-12-09 18:32:17 -08:00
Martin Michelsen 12572ed2d4 hide inventory/bank log messages if disabled in config.json 2023-12-09 10:56:58 -08:00
Martin Michelsen bb3d4ac847 disable $bank when character overlay is present 2023-12-09 10:38:48 -08:00
Martin Michelsen 0ded423c84 treat enemy type 0100 as NPC 2023-12-08 23:34:36 -08:00
Martin Michelsen 414ef0d825 don't send card search results if searcher is blocked 2023-12-08 23:34:32 -08:00
Martin Michelsen b54b32b461 reformat DAT constructor tables 2023-12-08 20:39:50 -08:00
Martin Michelsen 67e2428daa don't load Challenge map data on quest start 2023-12-08 20:22:04 -08:00
Martin Michelsen 8654555777 recreate map on challenge mode restart 2023-12-08 18:08:47 -08:00
Martin Michelsen 83166f1eff fix multi-area challenge enemy generation 2023-12-08 17:39:00 -08:00
Martin Michelsen fbf170ef12 fix slime enemy types 2023-12-08 17:38:42 -08:00
Martin Michelsen b7bc148e09 implement solo quest progression flags 2023-12-08 17:01:11 -08:00
Martin Michelsen 009a0fc93d fix Monest child_type 2023-12-08 10:02:36 -08:00
Martin Michelsen 5a98b48521 don't set floor if it's negative 2023-12-08 10:01:01 -08:00
Martin Michelsen bf17ec0943 add unknown enemy type 0118 2023-12-08 09:51:57 -08:00
Martin Michelsen e901f5e681 don't save licenses for replay sessions 2023-12-07 20:27:46 -08:00
Martin Michelsen 6136f8dfb3 implement $edit on v1/v2 2023-12-07 20:08:46 -08:00
Martin Michelsen 0c18656e03 update note about get_game_version 2023-12-07 17:05:26 -08:00
Martin Michelsen 317c9fd616 implement Simple Mail auto-reply when recipient is offline 2023-12-07 12:46:01 -08:00
Martin Michelsen 6d16f8095a factor ClientGameData into Client to reduce data duplication 2023-12-07 12:23:21 -08:00
Martin Michelsen 072ebe81bf add $savechar and $loadchar commands 2023-12-06 23:54:53 -08:00
Martin Michelsen 7db761f181 fix attribute upgrade in Gallon's Shop 2023-12-06 15:57:37 -08:00
Martin Michelsen 7211205e55 fix meseta and material import in $bbchar 2023-12-06 09:55:33 -08:00
Martin Michelsen 85d0bac5cb assign bank item IDs at game join time 2023-12-06 09:46:57 -08:00
Martin Michelsen 713327b0ae fix double language marker on BB 2023-12-05 23:31:16 -08:00
Martin Michelsen 0ce5210c22 add v4 ItemPT data 2023-12-05 23:31:16 -08:00
Martin Michelsen 4ccbb2f683 don't disconnect when no combinations apply 2023-12-05 23:31:16 -08:00
Martin Michelsen 3075370975 add $qcheck command 2023-12-05 23:31:16 -08:00
Matt Swift 9dfdbc624b Add fixed BB Gallon's Shop 2023-12-05 23:07:07 -08:00
Martin Michelsen 027956876d fix episode 3 tests 2023-12-04 22:40:07 -08:00
Martin Michelsen cd1cc43cb3 update to-do list 2023-12-04 22:40:07 -08:00
Martin Michelsen 77798e09be prevent player from joining game if a quest they don't have access to is in progress 2023-12-04 22:40:07 -08:00
Martin Michelsen da0ffea7e0 prevent player from joining game if a quest they don't have access to is in progress 2023-12-04 21:42:51 -08:00
Martin Michelsen 330dbecada rewrite $bbchar implementation 2023-12-04 19:38:42 -08:00
Martin Michelsen 2360beb77b sort bank contents before sending to client 2023-12-04 18:43:37 -08:00
Martin Michelsen 33bbb15bf0 fix stacked item bank deposit bug 2023-12-04 18:23:41 -08:00
Martin Michelsen c25569c688 implement shared bank 2023-12-04 16:59:03 -08:00
Martin Michelsen 01b83044dc cache loaded player files between sessions 2023-12-04 12:38:26 -08:00
Martin Michelsen e05dcb6e70 update rare enemy rates 2023-12-04 10:37:02 -08:00
Martin Michelsen 501a048af0 allow specifying minimum levels for difficulties 2023-12-03 23:35:38 -08:00
Martin Michelsen f605a21c1a add option to specify BB rare enemy rates 2023-12-03 21:44:56 -08:00
Martin Michelsen 8e1edbc34e update notes about DC prototypes 2023-12-03 21:44:56 -08:00
Martin Michelsen 83549fe8e4 update compatibility table 2023-12-03 16:59:05 -08:00
Martin Michelsen fbda7a2a48 fix GC NTE proxy behavior 2023-12-03 16:34:09 -08:00
Martin Michelsen b8d4ab589e update compatibility table 2023-12-03 00:06:32 -08:00
Martin Michelsen e49e159eee implement HDLC/PPP in IPStackSimulator 2023-12-03 00:06:32 -08:00
Martin Michelsen 0442f6e579 fix Ep3 card trade sequence 2023-12-02 18:24:07 -08:00
Martin Michelsen 3160d86eaa only use language from quest header if it's valid 2023-12-01 22:27:58 -08:00
Martin Michelsen 4cd82caa5f fix GC NTE episode number when creating games 2023-12-01 21:34:51 -08:00
Martin Michelsen d943364c29 use DC quest download semantics for GC NTE 2023-12-01 21:00:10 -08:00
Martin Michelsen cddd8007c7 fix 6x75 on GC NTE 2023-12-01 20:29:11 -08:00
Martin Michelsen 039786b2f8 handle GC NTE character data struct 2023-12-01 20:05:12 -08:00
Martin Michelsen 5de53391db disable rare mag evolution logic on v1/v2 2023-12-01 17:34:16 -08:00
Martin Michelsen 5f8e0bd6bd add XB voice chat command structs 2023-12-01 17:32:55 -08:00
Martin Michelsen 3e83669138 update to-do list 2023-12-01 17:32:45 -08:00
Martin Michelsen 5593dc0ecd add PSOBB connection instructions 2023-11-30 10:59:31 -08:00
Martin Michelsen 956e890ad6 use make_shared where appropriate 2023-11-30 10:24:27 -08:00
Martin Michelsen c833b575e4 fix item table reloading 2023-11-30 10:24:11 -08:00
Martin Michelsen 4b3be7eee3 add fallback for undecodable text in quest disassembler 2023-11-30 09:48:40 -08:00
Martin Michelsen f7b6f602cd update to-do list 2023-11-30 09:48:24 -08:00
Martin Michelsen 695f14e3fb implement 6xD8 subcommand 2023-11-30 09:48:12 -08:00
Martin Michelsen 77906b7a57 update example config 2023-11-29 22:51:02 -08:00
Martin Michelsen 14fc0996bd fix team reward keys on quests 2023-11-29 22:48:20 -08:00
Martin Michelsen 3743d0a156 implement quest unlock flags 2023-11-29 22:22:19 -08:00
Martin Michelsen 3d2d96eb7e fix BB bank withdraw conditions 2023-11-29 18:45:30 -08:00
Martin Michelsen ba8917e50d implement team item rewards 2023-11-29 18:28:54 -08:00
Martin Michelsen b09269eabc update to-do list 2023-11-29 17:19:48 -08:00
Martin Michelsen d1ce010d06 make user flag that allows cheating even if cheats are disabled 2023-11-29 17:19:37 -08:00
Martin Michelsen 1e3ca4111a add TODO item about F94D quest opcode 2023-11-29 16:45:02 -08:00
Martin Michelsen 6a052722c9 load V1 rare tables 2023-11-29 16:44:53 -08:00
Martin Michelsen acb9c656c5 implement 6xCB subcommand 2023-11-29 16:42:24 -08:00
Martin Michelsen f5ebf6fdcd fix comment in rare-table-v1.json 2023-11-29 12:55:36 -08:00
Martin Michelsen 9ea84d7101 implement most remaining BB team functions 2023-11-29 11:35:15 -08:00
Martin Michelsen 556360c993 implement choice search 2023-11-28 18:38:43 -08:00
Martin Michelsen 4008d7f4ff remove history/future sections from readme 2023-11-28 18:38:43 -08:00
Matt e47b72dd72 Add Chinese/Korean download quests for PCv2 2023-11-28 14:06:21 -08:00
Martin Michelsen 613d0c6d36 update windows build instructions 2023-11-27 22:54:42 -08:00
308 changed files with 7135 additions and 3414 deletions
+3 -1
View File
@@ -47,6 +47,7 @@ set(SOURCES
src/CatSession.cc
src/Channel.cc
src/ChatCommands.cc
src/ChoiceSearch.cc
src/Client.cc
src/CommonItemSet.cc
src/Compression.cc
@@ -85,7 +86,7 @@ set(SOURCES
src/Menu.cc
src/NetworkAddresses.cc
src/PatchFileIndex.cc
src/Player.cc
src/PlayerFilesManager.cc
src/PlayerSubordinates.cc
src/ProxyCommands.cc
src/ProxyServer.cc
@@ -93,6 +94,7 @@ set(SOURCES
src/PSOGCObjectGraph.cc
src/PSOProtocol.cc
src/Quest.cc
src/QuestAvailabilityExpression.cc
src/QuestScript.cc
src/RareItemSet.cc
src/ReceiveCommands.cc
+48 -56
View File
@@ -4,9 +4,11 @@ newserv is a game server, proxy, and reverse-engineering tool for Phantasy Star
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.
* Background
* [History](#history)
* [Future (and to-do list)](#future)
Feel free to submit GitHub issues if you find bugs or have feature requests. I'd like to make the server as stable and complete as possible, but I can't promise that I'll respond to issues in a timely manner, because this is a personal project undertaken primarily for the fun of reverse-engineering. If you want to contribute to newserv yourself, pull requests are welcome as well.
See TODO.md for a list of known issues and future work I've curated, or go to the GitHub issue tracker for issues and requests submitted by the community.
**Table of contents**
* [Compatibility](#compatibility)
* Setup
* [Configuration](#configuration)
@@ -23,61 +25,35 @@ This project includes code that was reverse-engineered by the community in ages
* [PSO PC](#pso-pc)
* [PSO GC on a real GameCube](#pso-gc-on-a-real-gamecube)
* [PSO GC on Dolphin](#pso-gc-on-dolphin)
* [PSO BB](#pso-bb)
* [Connecting external clients](#connecting-external-clients)
* [Non-server features](#non-server-features)
## History
The history of this project essentially mirrors my development as a software engineer from the beginning of my hobby until now. If you don't care about the story, skip to the "Compatibility" or "Setup" sections below.
I originally purchased PSO GC when I heard about PSUL, and wanted to play around with running homebrew on my GameCube. This pathway eventually led to [GCARS-CS](https://github.com/fuzziqersoftware/gcars-cs), but that's another story.
<img align="left" src="s-khyps.png" /> After playing PSO for a while, both offline and online, I wrote a proxy called Khyps sometime in 2003. This was back in the days of the official Sega servers, where vulnerabilities weren't addressed in a timely manner or at all. It was common for malicious players using their own proxies or Action Replay codes (a story for another time) to send invalid commands that the servers would blindly forward, and cause the receiving clients to crash. These crashes were more than simply inconvenient; they could also corrupt your save data, destroying the hours of work you may have put into hunting items and leveling up your character.
For a while it was essentially necessary to use a proxy to go online at all, so the proxy could block these invalid commands. Khyps was designed primarily with this function in mind, though it also implemented some convenient cheats, like the ability to give yourself or other players infinite HP and allow you to teleport to different places without using an in-game teleporter.
<img align="left" src="s-khyller.png" /> After Khyps I took on the larger challenge of writing a server, which resulted in Khyller sometime in 2005. This was the first server of any type I had ever written. This project eventually evolved into a full-featured environment supporting all versions of the game that I had access to - at the time, PC, GC, and BB. (However, I suspect from reading the ancient source files that Khyller's BB support was very buggy.) As Khyller evolved, the code became increasingly cumbersome, littered with debugging filth that I never cleaned up and odd coding patterns I had picked up over the years. My understanding of the C++ language was woefully incomplete as well (as opposed to now, when it is still incomplete but not woefully so), which resulted in Khyller being essentially a C project that had a couple of classes in it.
<img align="left" src="s-aeon.png" /> Sometime in 2006 or 2007, I abandoned Khyller and rebuilt the entire thing from scratch, resulting in Aeon. Aeon was substantially cleaner in code than Khyller but still fairly hard to work with, and it lacked a few of the more arcane features I had originally written (for example, the ability to convert any quest into a download quest). In addition, the code still had some stability problems... it turns out that Aeon's concurrency primitives were simply incorrect. I had derived the concept of a mutex myself, before taking any real computer engineering classes, but had implemented it incorrectly. I made the race window as small as possible, but Aeon would still randomly crash after running seemingly fine for a few days.
At the time of its inception, Aeon was also called newserv, and you may find some beta releases floating around the Internet with filenames like `newserv-b3.zip`. I had released betas 1, 2, and 3 before I released the entire source of beta 5 and stopped working on the project when I went to college. This was around the time when I switched from writing software primarily on Windows to primarily on macOS and Linux, so Aeon beta 5 was the last server I wrote that specifically targeted Windows. (newserv, which you're looking at now, is a bit tedious to compile on Windows but does work.)
<img align="left" src="s-newserv.png" /> After a long hiatus from PSO and much professional and personal development in my technical abilities, I was reminiscing sometime in October 2018 by reading my old code archives. Somehow inspired when I came across Aeon, I spent a weekend and a couple more evenings rewriting the entire project again, cleaning up ancient patterns I had used eleven years ago, replacing entire modules with simple STL containers, and eliminating even more support files in favor of configuration autodetection. The code is now suitably modern and stable, and I'm not embarrassed by its existence, as I am by Aeon beta 5's source code and my archive of Khyller (which, thankfully, no one else ever saw).
## Future
newserv is many things - a server, a proxy, an encryption and decryption tool, a decoder of various PSO-related formats, and more. Primarily, it's a reverse-engineering project in which I try to unravel the secrets of a 20-year-old video game, for honestly no reason. Solving these problems and documenting them in code has been fun, and I'll continue to do it when my time allows.
With that said, I offer no guarantees on how or when this project will advance. Feel free to submit GitHub issues if you find bugs or have feature requests; I'd like to make the server as stable and complete as possible, but I can't promise that I'll respond to issues in a timely manner. If you feel like contributing to newserv yourself, pull requests are welcome as well.
See TODO.md for a list of known issues and future work.
## Compatibility
newserv supports several versions of PSO. Specifically:
| Version | Login | Lobbies | Games | Proxy |
|----------------|--------------|--------------|--------------|--------------|
| DC Trial | Yes | Yes | Yes | No |
| DC 11/2000 | Yes | Yes | Yes | No |
| DC 12/2000 | Yes | Yes | Yes | Yes |
| DC 01/2001 | Yes | Yes | Yes | Yes |
| DC V1 | Yes | Yes | Yes | Yes |
| DC 08/2001 | Untested (1) | Untested (1) | Untested (1) | Untested (1) |
| DC V2 | Yes | Yes | Yes | Yes |
| PC | Yes | Yes | Yes | Yes |
| GC Ep1&2 Trial | Untested (1) | Untested (1) | Untested (1) | Untested (1) |
| GC Ep1&2 | Yes | Yes | Yes | Yes |
| GC Ep1&2 Plus | Yes | Yes | Yes | Yes |
| GC Ep3 Trial | Yes | Yes | Partial (3) | Yes |
| GC Ep3 | Yes | Yes | Yes | Yes |
| Xbox Ep1&2 | Yes | Yes | Yes | Yes |
| BB (vanilla) | Yes | Yes | Yes (2) | Yes |
| BB (Tethealla) | Yes | Yes | Yes (2) | Yes |
newserv supports several versions of PSO, including various development prototypes. Specifically:
| Version | Lobbies | Games | Proxy |
|----------------|--------------|--------------|--------------|
| DC Trial | Yes | Yes | No |
| DC 11/2000 | Yes | Yes | No |
| DC 12/2000 | Yes | Yes | Yes |
| DC 01/2001 | Yes | Yes | Yes |
| DC V1 | Yes | Yes | Yes |
| DC 08/2001 | Yes | Yes | Yes |
| DC V2 | Yes | Yes | Yes |
| PC | Yes | Yes | Yes |
| GC Ep1&2 Trial | Yes | Yes | Yes |
| GC Ep1&2 | Yes | Yes | Yes |
| GC Ep1&2 Plus | Yes | Yes | Yes |
| GC Ep3 Trial | Yes | Partial (1) | Yes |
| GC Ep3 | Yes | Yes | Yes |
| Xbox Ep1&2 | Yes | Yes | Yes |
| BB (vanilla) | Yes | Yes (2) | Yes |
| BB (Tethealla) | Yes | Yes (2) | Yes |
*Notes:*
1. *newserv's implementations of these versions are based on disassembly of the client executables and have never been tested.*
2. *BB games are mostly playable, but there are still some unimplemented features (for example, some quests that use rare commands may not work). Please submit a GitHub issue if you find something that doesn't work.*
3. *Creating a game works and battle setup behaves mostly normally, but starting a battle doesn't work.*
1. *Players can create games, edit decks, trade cards, and participate in auctions, but CARD battles don't work on Episode 3 Trial Edition on newserv.*
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.*
## Setup
@@ -90,7 +66,7 @@ There is a fairly recent macOS ARM64 release on the newserv GitHub repository. Y
There is a fairly recent Windows release on the newserv GitHub repository also. It's built with Cygwin, and all the necessary DLL files should be included. That said, I've only tested it on my own machine and there is no CI for Windows builds like there is for macOS and Linux, so if it doesn't work for you, please open a GitHub issue to let me know.
If you're not using a release from the GitHub repository, do this to build newserv:
1. If you're on Windows, install Cygwin. While doing so, install the `cmake`, `gcc-core`, `gcc-g++`, `git`, `libevent2.1_7`, `make`, `libiconv`, and `zlib` packages. Do the rest of these steps inside a Cygwin shell (not a Windows cmd shell or PowerShell).
1. If you're on Windows, install Cygwin. While doing so, install the `cmake`, `gcc-core`, `gcc-g++`, `git`, `libevent2.1_7`, `make`, `libiconv-devel`, and `zlib` packages. Do the rest of these steps inside a Cygwin shell (not a Windows cmd shell or PowerShell).
2. Make sure you have CMake, libevent, and libiconv installed. (On macOS, `brew install cmake libevent libiconv`; on most Linuxes, `sudo apt-get install cmake libevent-dev`; on Windows, you already did this in step 1.)
3. Build and install phosg (https://github.com/fuzziqersoftware/phosg).
4. Optionally, install resource_dasm (https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to send memory patches and load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this.
@@ -117,7 +93,7 @@ Within the category directories, quest files should be named like `q###-VERSION-
For .dat files, the `LANGUAGE` token may be omitted. If it's present, then that .dat file will only be used for that language of the quest; if omitted, then that .dat file will be used for all languages of the quest.
Some quests (mostly battle and challenge mode quests) have additional JSON metadata files that describe how the server should handle them. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all versions of the quest on all PSO versions.
Some quests (mostly battle and challenge mode quests) have additional JSON metadata files that describe how the server should handle them. This includes flags that can be used to hide the quest unless a preceding quest has been cleared, or to hide the quest unless purchased as a team reward. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See system/quests/battle/b88001.json for documentation on the exact format of the JSON file.
Some quests may also include a .pvr file, which contains an image used in the quest. These files are named similarly to their .bin and .dat counterparts.
@@ -133,6 +109,7 @@ There are multiple PSO quest formats out there; newserv supports all of them. It
| Compressed Ep3 | .bin or .mnm | Yes (4) | None (1) |
| Uncompressed | .bind and .datd | Yes | compress-prs (2) |
| Uncompressed Ep3 | .bind or .mnmd | Yes (4) | compress-prs (2) |
| Source | .bin.txt and .dat | Yes | None (5) |
| VMS (DCv1) | .bin.vms and .dat.vms | Yes | decode-vms |
| VMS (DCv2) | .bin.vms and .dat.vms | Decode (3) | decode-vms (3) |
| GCI (decrypted) | .bin.gci and .dat.gci | Yes | decode-gci |
@@ -150,6 +127,7 @@ There are multiple PSO quest formats out there; newserv supports all of them. It
2. *Similar to (1), to compress an uncompressed quest file: `newserv compress-prs FILENAME.bind FILENAME.bin` (and likewise for .datd -> .dat)*
3. *Use the decode action to convert these quests to .bin/.dat format before putting them into the server's quests directory. If you know the encryption seed (serial number), pass it in as a hex string with the `--seed=` option. If you don't know the encryption seed, newserv will find it for you, which will likely take a long time.*
4. *Episode 3 quests don't go in the system/quests directory. See the Episode 3 section below.*
5. *Quest source can be assembled into a .bin or .bind file with `newserv assemble-quest-script FILENAME.txt`. See system/quests/retrieval/q058-gc-e.bin.txt for an annotated example; this is the English GameCube version of Lost HEAT SWORD.*
Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/). These files can be encoded in any of the formats described above, except .qst.
@@ -282,6 +260,7 @@ Some commands only work on the game server and not on the proxy server. The chat
* `$debug` (game server only): Enable or disable debug. You need the DEBUG permission in your user license to use this command. When debug is enabled, you'll see in-game messages from the server when you take certain actions. You'll also be placed into the highest available slot in lobbies and games instead of the lowest, which is useful for finding commands for which newserv doesn't handle client IDs properly. This setting also disables certain safeguards and allows you to do some things that might crash your client.
* `$quest <number>`: Load a quest by quest number. Can be used to load battle or challenge quests with only one player present.
* `$qcall <function-id>`: Call a quest function on your client.
* `$qcheck <flag-num>`: Show the value of a quest flag.
* `$qset <flag-num>` or `$qclear <flag-num>`: Set or clear a global quest flag for everyone in the game.
* `$qsync <reg-num> <value>`: Set a quest register's value on your client. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
* `$gc` (game server only): Send your own Guild Card to yourself.
@@ -297,9 +276,14 @@ Some commands only work on the game server and not on the proxy server. The chat
* `$exit`: If you're in a lobby, sends you to the main menu (which ends your proxy session, if you're in one). If you're in a game or spectator team, sends you to the lobby (but does not end your proxy session if you're in one). Does nothing if you're in a non-Episode 3 game and no quest is in progress.
* `$patch <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
* Character data commands
* `$savechar <slot>`: Saves your current character data on the server in the specified slot (each serial number has 4 slots, numbered 1-4). These slots are separate from BB character slots; using this command does not affect BB characters.
* `$loadchar <slot>` (v1 and v2 only): Loads your character data from the specified slot. The changes will be undone if you join a game - to save your changes, disconnect from the lobby.
* `$bbchar <username> <password> <slot>`: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot (1-4). Any character already in that slot is overwritten. (This command is similar to `$savechar`, except it overwrites a BB character slot, and can transfer characters across accounts.) Note that the character's chat data, quick menu config, and bank contents are not copied, since there is no way for the server to request those types of data.
* `$edit <stat> <value>`: Modifies your character data. If you are on V3 (GameCube/Xbox), or if the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing. If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby.
* Blue Burst player commands (game server only)
* `$bbchar <username> <password> <1-4>`: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot. Any character already in that slot is overwritten.
* `$edit <stat> <value>`: Modifies your character data. If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing.
* `$bank [number]`: Switches your current bank, so you can access your other character's banks (if `number` is 1-4) or your shared account bank (if `number` is 0). If `number` is not given, switches back to your current character's bank.
* `$save`: Saves your character, system, and Guild Card data immediately. (By default, your character is saved every 60 seconds while online, and your account and Guild Card data are saved whenever they change.)
* Game state commands (game server only)
@@ -389,6 +373,14 @@ If you're using a version of Dolphin with tapserver support, you can make it con
3. In PSO's network settings, enable DHCP ("Automatically obtain an IP address"), set DNS server address to "Automatic", and leave DHCP Hostname as "Not set". Leave the proxy server settings blank.
4. Start an online game.
#### PSO BB
The PSO BB client has been modified and distributed in many different forms. newserv supports most, but not all, of the common distributions. Unlike other versions, it's important that the client and server have the same map files, so make sure to set up the patch directory based on the client you'll be using with newserv. (See the "Client patch directories" section for instructions on setting this up.)
The original Japanese and US versions of PSO BB should work, but you'll have to modify your hosts file or edit psobb.exe to point to your newserv instance. The original versions are packed, so this is a more involved process than simply opening the executable in a hex editor and finding/replacing some strings.
Alternatively, you can use the Tethealla client (https://archive.org/details/psobb-tethealla-client); you can find the connection addresses starting at 0x56D724 in psobb.exe. Overwrite these addresses with your server's hostname or IP address, and you should be able to connect.
### Connecting external clients
If you want to accept connections from outside your local network, you'll need to set ExternalAddress to your public IP address in the configuration file, and you'll likely need to open some ports in your router's NAT configuration - specifically, all the TCP ports listed in PortConfiguration in config.json.
@@ -411,7 +403,7 @@ newserv has many CLI options, which can be used to access functionality other th
* Convert quests in .gci, .vms, .dlq, or .qst format to .bin/.dat format (`decode-gci`, `decode-vms`, `decode-dlq`, `decode-qst`)
* Convert quests in .bin/.dat to .qst format (`encode-qst`)
* Convert text archives (e.g. TextEnglish.pr2) to JSON and vice versa (`decode-text-archive`, `encode-text-archive`)
* Disassemble quest scripts (`disassemble-quest-script`)
* Compile or disassemble quest scripts (`assemble-quest-script`, `disassemble-quest-script`)
* Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`)
* Convert item data to a human-readable description, or vice versa (`describe-item`, `encode-item`)
* Connect to another PSO server and pretend to be a client (`cat-client`)
+5 -17
View File
@@ -1,41 +1,29 @@
## General
- Find a way to silence audio in RunDOL.s
- Encapsulate BB server-side random state and make replays deterministic
- Implement choice search
- Write a simple status API
- Implement per-game logging
- Build an exception-handling abstraction in ChatCommands that shows formatted error messages in all cases
- Make reloading happen on separate threads so compression doesn't block active clients
- Implement decrypt/encrypt actions for VMS files
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
- Figure out what causes the corruption message on PC proxy sessions and fix it
- Make $edit for DC/PC
- Add an idle connection timeout for proxy sessions
## Episode 3
- Make disconnecting during a tournament match cause you to forfeit the match
- Enforce tournament deck restrictions (e.g. rank checks, No Assist option) when populating COMs at tournament start time
- It may be possible to send spectators back to the waiting room after a non-tournament battle by sending 6xB4x05 with environment 0x19, then 6xB4x3B again; try this
- Add support for recording battles on the proxy server (both in primary and spectator teams)
- When `reload ep3` happens and the defs file is changed, send the new defs file to all connected players who aren't in a game (if this even works - when exactly does the client decompress the defs file from the server?)
- Make `reload licenses` not vulnerable to online players' licenses overwriting licenses on disk somehow
- Implement ranks (based on total Meseta earned)
## PSO XBOX
- Fix receiving Guild Cards from non-Xbox players
- Make the Guild Card description field in SavedPlayerDataBB longer to accommodate XB descriptions (0x200 bytes)
- Research the F94D quest opcode
## PSOBB
- Find any remaining mismatches in enemy indexes / experience
- Fix some edge cases on the BB proxy server (e.g. Change Ship)
- Implement less-common subcommands
- 6xD8: Add S-rank weapon special
- Test team commands
- Test all EA subcommands (a few are still not implemented)
- 6xC1, 6xC2, 6xCD, 6xCE: Team invites/administration (not implemented)
- Fix invite member menu
- Implement story progress flags for unlocking quests
- Test all quest item subcommands
- Check if Commander Blade effect works and implement it if not
- Figure out which quest flags are required for solo quests and write appropriate JSON files
- Figure out why Pouilly Slime EXP doesn't work
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -2,4 +2,4 @@ DC NTE: pso02.dricas.ne.jp
Nov 2000 proto: test1.st-pso.games.sega.net
Dec 2000 proto: sg107634.csrd.sega.co.jp OR master.pso.dream-key.com
Jan 2001 proto: master.pso.dream-key.com
Aug 2001 proto (v2): ???
Aug 2001 proto (v2): game01.st-pso.games.sega.net
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

+1 -1
View File
@@ -18,7 +18,7 @@ void run_ar_code_translator(const std::string& initial_directory, const std::str
if (ends_with(filename, ".dol")) {
string name = filename.substr(0, filename.size() - 4);
string path = directory + "/" + filename;
files.emplace(name, new DOLFile(path.c_str()));
files.emplace(name, make_shared<DOLFile>(path.c_str()));
log.info("Loaded %s", name.c_str());
}
}
+6 -6
View File
@@ -78,13 +78,13 @@ void CatSession::on_channel_input(
if (command == 0x02 || command == 0x17 || command == 0x91 || command == 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.reset(new PSOV3Encryption(cmd.server_key));
this->channel.crypt_out.reset(new PSOV3Encryption(cmd.client_key));
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 { // PC, DC, or patch server
this->channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key));
this->channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key));
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());
}
@@ -95,8 +95,8 @@ void CatSession::on_channel_input(
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.reset(new PSOBBEncryption(*this->bb_key_file, &cmd.server_key[0], sizeof(cmd.server_key)));
this->channel.crypt_out.reset(new PSOBBEncryption(*this->bb_key_file, &cmd.client_key[0], sizeof(cmd.client_key)));
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");
}
}
+163 -94
View File
@@ -52,12 +52,6 @@ static void check_version(shared_ptr<Client> c, Version version) {
}
}
static void check_not_version(shared_ptr<Client> c, Version version) {
if (c->version() == version) {
throw precondition_failed("$C6This command cannot\nbe used for your\nversion of PSO.");
}
}
static void check_is_game(shared_ptr<Lobby> l, bool is_game) {
if (l->is_game() != is_game) {
throw precondition_failed(is_game ? "$C6This command cannot\nbe used in lobbies." : "$C6This command cannot\nbe used in games.");
@@ -70,20 +64,20 @@ static void check_is_ep3(shared_ptr<Client> c, bool is_ep3) {
}
}
static void check_cheats_enabled(shared_ptr<Lobby> l) {
if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED)) {
static void check_cheats_enabled(shared_ptr<Lobby> l, shared_ptr<Client> c) {
if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) && !(c->license->flags & License::Flag::CHEAT_ANYWHERE)) {
throw precondition_failed("$C6This command can\nonly be used in\ncheat mode.");
}
}
static void check_cheats_allowed(shared_ptr<ServerState> s) {
if (s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) {
static void check_cheats_allowed(shared_ptr<ServerState> s, shared_ptr<Client> c) {
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && !(c->license->flags & License::Flag::CHEAT_ANYWHERE)) {
throw precondition_failed("$C6Cheats are disabled\non this server.");
}
}
static void check_proxy_cheats_allowed(shared_ptr<ServerState> s) {
if (s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) {
static void check_cheats_allowed(shared_ptr<ServerState> s, shared_ptr<ProxyServer::LinkedSession> ses) {
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && (!ses->license || !(ses->license->flags & License::Flag::CHEAT_ANYWHERE))) {
throw precondition_failed("$C6Cheats are disabled\non this proxy.");
}
}
@@ -112,7 +106,7 @@ static void server_command_lobby_info(shared_ptr<Client> c, const std::string&)
} else {
lines.emplace_back(string_printf("$C6%08X$C7 L$C6%d-%d$C7", l->lobby_id, l->min_level + 1, l->max_level + 1));
}
lines.emplace_back(string_printf("$C7Section ID: $C6%s$C7", name_for_section_id(l->section_id).c_str()));
lines.emplace_back(string_printf("$C7Section ID: $C6%s$C7", name_for_section_id(l->section_id)));
if (l->check_flag(Lobby::Flag::DROPS_ENABLED)) {
if (l->item_creator) {
@@ -263,10 +257,26 @@ static void server_command_quest(shared_ptr<Client> c, const std::string& args)
return;
}
Version effective_version = is_ep3(c->version()) ? Version::GC_V3 : c->version();
auto s = c->require_server_state();
auto l = c->require_lobby();
auto q = s->quest_index_for_client(c)->get(stoul(args));
set_lobby_quest(c->require_lobby(), q);
auto q = s->quest_index_for_version(effective_version)->get(stoul(args));
if (!q) {
send_text_message(c, "$C6Quest not found");
} else {
set_lobby_quest(c->require_lobby(), q, true);
}
}
static void server_command_qcheck(shared_ptr<Client> c, const std::string& args) {
auto l = c->require_lobby();
uint16_t flag_num = stoul(args, nullptr, 0);
send_text_message_printf(c, "$C7Quest flag 0x%hX (%hu)\nis %s on %s",
flag_num, flag_num,
c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
name_for_difficulty(l->difficulty));
}
static void server_command_qset_qclear(shared_ptr<Client> c, const std::string& args, bool should_set) {
@@ -282,11 +292,17 @@ static void server_command_qset_qclear(shared_ptr<Client> c, const std::string&
uint16_t flag_num = stoul(args, nullptr, 0);
if (should_set) {
c->character()->quest_flags.set(l->difficulty, flag_num);
} else {
c->character()->quest_flags.clear(l->difficulty, flag_num);
}
if (is_v1_or_v2(c->version())) {
G_SetQuestFlag_DC_PC_6x75 cmd = {{0x75, 0x02, 0x0000}, flag_num, should_set ? 0 : 1};
send_command_t(l, 0x60, 0x00, cmd);
} else {
G_SetQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x02, 0x0000}, flag_num, should_set ? 0 : 1}, l->difficulty, 0x0000};
G_SetQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x03, 0x0000}, flag_num, should_set ? 0 : 1}, l->difficulty, 0x0000};
send_command_t(l, 0x60, 0x00, cmd);
}
}
@@ -354,7 +370,7 @@ static void proxy_command_qcall(shared_ptr<ProxyServer::LinkedSession> ses, cons
}
static void server_command_show_material_counts(shared_ptr<Client> c, const std::string&) {
auto p = c->game_data.character();
auto p = c->character();
if (is_v1_or_v2(c->version())) {
send_text_message_printf(c, "%hhu HP, %hhu TP",
p->get_material_usage(PSOBBCharacterFile::MaterialType::HP),
@@ -475,12 +491,12 @@ static void server_command_persist(shared_ptr<Client> c, const std::string&) {
static void server_command_exit(shared_ptr<Client> c, const std::string&) {
auto l = c->require_lobby();
if (l->is_game()) {
if (is_ep3(c->version())) {
c->channel.send(0xED, 0x00);
} else if (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
if (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
G_UnusedHeader cmd = {0x73, 0x01, 0x0000};
c->channel.send(0x60, 0x00, cmd);
c->floor = 0;
} else if (is_ep3(c->version())) {
c->channel.send(0xED, 0x00);
} else {
send_text_message(c, "$C6You must return to\nthe lobby first");
}
@@ -702,9 +718,8 @@ static void server_command_playrec(shared_ptr<Client> c, const std::string& args
send_text_message(c, "$C4The recording does\nnot exist");
return;
}
shared_ptr<Episode3::BattleRecord> record(new Episode3::BattleRecord(data));
shared_ptr<Episode3::BattleRecordPlayer> battle_player(
new Episode3::BattleRecordPlayer(record, s->game_server->get_base()));
auto record = make_shared<Episode3::BattleRecord>(data);
auto battle_player = make_shared<Episode3::BattleRecordPlayer>(record, s->game_server->get_base());
auto game = create_game_generic(
s, c, args, "", Episode::EP3, GameMode::NORMAL, 0, false, nullptr, battle_player);
if (game) {
@@ -740,7 +755,7 @@ 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_is_game(l, false);
check_cheats_allowed(c->require_server_state());
check_cheats_allowed(c->require_server_state(), c);
if (!args[0]) {
c->config.override_section_id = 0xFF;
@@ -751,14 +766,13 @@ static void server_command_secid(shared_ptr<Client> c, const std::string& args)
send_text_message(c, "$C6Invalid section ID");
} else {
c->config.override_section_id = new_secid;
string name = name_for_section_id(new_secid);
send_text_message_printf(c, "$C6Override section ID\nset to %s", name.c_str());
send_text_message_printf(c, "$C6Override section ID\nset to %s", name_for_section_id(new_secid));
}
}
}
static void proxy_command_secid(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
check_cheats_allowed(ses->require_server_state());
check_cheats_allowed(ses->require_server_state(), ses);
if (!args[0]) {
ses->config.override_section_id = 0xFF;
send_text_message(ses->client_channel, "$C6Override section ID\nremoved");
@@ -768,8 +782,7 @@ static void proxy_command_secid(shared_ptr<ProxyServer::LinkedSession> ses, cons
send_text_message(ses->client_channel, "$C6Invalid section ID");
} else {
ses->config.override_section_id = new_secid;
string name = name_for_section_id(new_secid);
send_text_message(ses->client_channel, "$C6Override section ID\nset to " + name);
send_text_message_printf(ses->client_channel, "$C6Override section ID\nset to %s", name_for_section_id(new_secid));
}
}
}
@@ -778,7 +791,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);
check_cheats_allowed(s, c);
if (!args[0]) {
c->config.clear_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED);
@@ -792,7 +805,7 @@ 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_proxy_cheats_allowed(ses->require_server_state());
check_cheats_allowed(ses->require_server_state(), ses);
if (!args[0]) {
ses->config.clear_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED);
ses->config.override_random_seed = 0;
@@ -885,9 +898,11 @@ static void server_command_edit(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_version(c, Version::BB_V4);
if (!is_v1_or_v2(c->version()) && (c->version() != Version::BB_V4)) {
throw precondition_failed("$C6This command cannot\nbe used for your\nversion of PSO.");
}
if (s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) {
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && !(c->license->flags & License::Flag::CHEAT_ANYWHERE)) {
send_text_message(l, "$C6Cheats are disabled\non this server");
return;
}
@@ -896,7 +911,7 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
vector<string> tokens = split(encoded_args, ' ');
try {
auto p = c->game_data.character();
auto p = c->character();
if (tokens.at(0) == "atp") {
p->disp.stats.char_stats.atp = stoul(tokens.at(1));
} else if (tokens.at(0) == "mst") {
@@ -979,69 +994,119 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
// Reload the client in the lobby
send_player_leave_notification(l, c->lobby_client_id);
send_complete_player_bb(c);
if (c->version() == Version::BB_V4) {
send_complete_player_bb(c);
}
c->v1_v2_last_reported_disp.reset();
s->send_lobby_join_notifications(l, c);
}
// TODO: implement this (and make sure the bank name is filesystem-safe)
/* static void server_command_change_bank(shared_ptr<Client> c, const std::string&) {
static void server_command_change_bank(shared_ptr<Client> c, const std::string& args) {
check_version(c, Version::BB_V4);
...
} */
// TODO: This can be implemented on the proxy server too.
static void server_command_convert_char_to_bb(shared_ptr<Client> c, const std::string& args) {
if (c->config.check_flag(Client::Flag::AT_BANK_COUNTER)) {
throw runtime_error("cannot change banks while at the bank counter");
}
if (c->has_overlay()) {
throw runtime_error("cannot change banks while Battle or Challenge is in progress");
}
ssize_t new_char_index = args.empty() ? (c->bb_character_index + 1) : stol(args, nullptr, 0);
if (new_char_index == 0) {
if (c->use_shared_bank()) {
send_text_message_printf(c, "$C6Using shared bank (0)");
} else {
send_text_message_printf(c, "$C6Created shared bank (0)");
}
} else if (new_char_index <= 4) {
c->use_character_bank(new_char_index - 1);
auto bp = c->current_bank_character();
auto name = bp->disp.name.decode(c->language());
send_text_message_printf(c, "$C6Using %s\'s bank (%zu)", name.c_str(), new_char_index);
} else {
throw runtime_error("invalid bank number");
}
const auto& bank = c->current_bank();
send_text_message_printf(c, "%" PRIu32 " items\n%" PRIu32 " Meseta", bank.num_items.load(), bank.meseta.load());
}
static void server_command_bbchar_savechar(shared_ptr<Client> c, const std::string& args, bool is_bb_conversion) {
auto s = c->require_server_state();
auto l = c->require_lobby();
check_is_game(l, false);
check_not_version(c, Version::BB_V4);
vector<string> tokens = split(args, ' ');
if (tokens.size() != 3) {
send_text_message(c, "$C6Incorrect argument count");
return;
auto pending_export = make_unique<Client::PendingCharacterExport>();
pending_export->is_bb_conversion = is_bb_conversion;
if (is_bb_conversion) {
vector<string> tokens = split(args, ' ');
if (tokens.size() != 3) {
send_text_message(c, "$C6Incorrect argument count");
return;
}
// username/password are tokens[0] and [1]
pending_export->character_index = stoll(tokens[2]) - 1;
if ((pending_export->character_index > 3) || (pending_export->character_index < 0)) {
send_text_message(c, "$C6Player index must\nbe in range 1-4");
return;
}
try {
c->pending_character_export->license = s->license_index->verify_bb(tokens[0].c_str(), tokens[1].c_str());
} catch (const exception& e) {
send_text_message_printf(c, "$C6Login failed: %s", e.what());
return;
}
} else {
pending_export->character_index = stoll(args) - 1;
if ((pending_export->character_index > 3) || (pending_export->character_index < 0)) {
send_text_message(c, "$C6Player index must\nbe in range 1-4");
return;
}
pending_export->license = c->license;
}
// username/password are tokens[0] and [1]
c->pending_bb_save_character_index = stoul(tokens[2]) - 1;
if (c->pending_bb_save_character_index > 3) {
send_text_message(c, "$C6Player index must be 1-4");
return;
}
try {
s->license_index->verify_bb(tokens[0].c_str(), tokens[1].c_str());
} catch (const exception& e) {
send_text_message_printf(c, "$C6Login failed: %s", e.what());
return;
}
c->pending_bb_save_username = tokens[0];
c->pending_character_export = std::move(pending_export);
// Request the player data. The client will respond with a 61, and the handler
// for that command will execute the conversion
send_get_player_info(c);
}
static void server_command_bbchar(shared_ptr<Client> c, const std::string& args) {
server_command_bbchar_savechar(c, args, true);
}
static void server_command_savechar(shared_ptr<Client> c, const std::string& args) {
server_command_bbchar_savechar(c, args, false);
}
static void server_command_loadchar(shared_ptr<Client> c, const std::string& args) {
if (!is_v1_or_v2(c->version())) {
send_text_message(c, "$C7This command can only\nbe used on v1 or v2");
return;
}
auto l = c->require_lobby();
check_is_game(l, false);
size_t index = stoull(args, nullptr, 0);
c->load_backup_character(c->license->serial_number, index);
auto s = c->require_server_state();
send_player_leave_notification(l, c->lobby_client_id);
s->send_lobby_join_notifications(l, c);
}
static void server_command_save(shared_ptr<Client> c, const std::string&) {
check_version(c, Version::BB_V4);
try {
c->game_data.save_character_file();
send_text_message(c, "Character data saved");
c->save_all();
send_text_message(c, "All data saved");
} catch (const exception& e) {
send_text_message_printf(c, "Can\'t save character:\n%s", e.what());
}
try {
c->game_data.save_system_file();
send_text_message(c, "System data saved");
} catch (const exception& e) {
send_text_message_printf(c, "Can\'t save system data:\n%s", e.what());
}
try {
c->game_data.save_guild_card_file();
send_text_message(c, "Guild Card data saved");
} catch (const exception& e) {
send_text_message_printf(c, "Can\'t save Guild Cards:\n%s", e.what());
send_text_message_printf(c, "Can\'t save data:\n%s", e.what());
}
c->reschedule_save_game_data_event();
}
@@ -1050,7 +1115,7 @@ static void server_command_save(shared_ptr<Client> c, const std::string&) {
// Administration commands
static string name_for_client(shared_ptr<Client> c) {
auto player = c->game_data.character(false);
auto player = c->character(false);
if (player.get()) {
return player->disp.name.decode(player->inventory.language);
}
@@ -1166,7 +1231,7 @@ 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);
check_cheats_enabled(l, c);
uint32_t floor = stoul(args, nullptr, 0);
if (c->floor == floor) {
@@ -1198,7 +1263,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_proxy_cheats_allowed(s);
check_cheats_allowed(s, ses);
if (!ses->is_in_game) {
send_text_message(ses->client_channel, "$C6You must be in a\ngame to use this\ncommand");
return;
@@ -1223,7 +1288,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);
check_cheats_enabled(l, c);
size_t limit = floor_limit_for_episode(l->episode);
if (limit == 0) {
@@ -1234,7 +1299,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_proxy_cheats_allowed(s);
check_cheats_allowed(s, ses);
if (!ses->is_in_game) {
send_text_message(ses->client_channel, "$C6You must be in a\ngame to use this\ncommand");
return;
@@ -1300,7 +1365,7 @@ 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);
check_cheats_enabled(l, c);
c->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED);
send_text_message_printf(c, "$C6Infinite HP %s", c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED) ? "enabled" : "disabled");
@@ -1308,7 +1373,7 @@ static void server_command_infinite_hp(shared_ptr<Client> c, const std::string&)
static void proxy_command_infinite_hp(shared_ptr<ProxyServer::LinkedSession> ses, const std::string&) {
auto s = ses->require_server_state();
check_proxy_cheats_allowed(s);
check_cheats_allowed(s, ses);
ses->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED);
send_text_message_printf(ses->client_channel, "$C6Infinite HP %s",
ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED) ? "enabled" : "disabled");
@@ -1318,7 +1383,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);
check_cheats_enabled(l, c);
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");
@@ -1326,7 +1391,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_proxy_cheats_allowed(s);
check_cheats_allowed(s, ses);
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");
@@ -1336,7 +1401,7 @@ static void server_command_switch_assist(shared_ptr<Client> c, const std::string
auto s = c->require_server_state();
auto l = c->require_lobby();
check_is_game(l, true);
check_cheats_enabled(l);
check_cheats_enabled(l, c);
c->config.toggle_flag(Client::Flag::SWITCH_ASSIST_ENABLED);
send_text_message_printf(c, "$C6Switch assist %s",
@@ -1345,7 +1410,7 @@ static void server_command_switch_assist(shared_ptr<Client> c, const std::string
static void proxy_command_switch_assist(shared_ptr<ProxyServer::LinkedSession> ses, const std::string&) {
auto s = ses->require_server_state();
check_proxy_cheats_allowed(s);
check_cheats_allowed(s, ses);
ses->config.toggle_flag(Client::Flag::SWITCH_ASSIST_ENABLED);
send_text_message_printf(ses->client_channel, "$C6Switch assist %s",
ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? "enabled" : "disabled");
@@ -1387,7 +1452,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);
check_cheats_enabled(l, c);
ItemData item = s->item_name_index->parse_item_description(c->version(), args);
item.id = l->generate_item_id(c->lobby_client_id);
@@ -1401,7 +1466,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_proxy_cheats_allowed(s);
check_cheats_allowed(s, ses);
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;
@@ -1540,7 +1605,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);
check_cheats_enabled(l, c);
if (l->episode != Episode::EP3) {
throw logic_error("non-Ep3 client in Ep3 game");
@@ -1573,7 +1638,7 @@ static void server_command_surrender(shared_ptr<Client> c, const std::string&) {
send_text_message(c, "$C6Battle has not\nyet started");
return;
}
const string& name = c->game_data.character()->disp.name.decode(c->language());
const string& name = c->character()->disp.name.decode(c->language());
send_text_message_printf(l, "$C6%s has\nsurrendered", name.c_str());
for (const auto& watcher_l : l->watcher_lobbies) {
send_text_message_printf(watcher_l, "$C6%s has\nsurrendered", name.c_str());
@@ -1680,12 +1745,14 @@ static const unordered_map<string, ChatCommandDefinition> chat_commands({
{"$auction", {server_command_auction, proxy_command_auction}},
{"$ax", {server_command_ax, nullptr}},
{"$ban", {server_command_ban, nullptr}},
{"$bbchar", {server_command_convert_char_to_bb, nullptr}},
{"$bank", {server_command_change_bank, nullptr}},
{"$bbchar", {server_command_bbchar, nullptr}},
{"$cheat", {server_command_cheat, nullptr}},
{"$debug", {server_command_debug, nullptr}},
{"$defrange", {server_command_ep3_set_def_dice_range, nullptr}},
{"$drop", {server_command_drop, nullptr}},
{"$edit", {server_command_edit, nullptr}},
{"$ep3battledebug", {server_command_enable_ep3_battle_debug_menu, nullptr}},
{"$event", {server_command_lobby_event, proxy_command_lobby_event}},
{"$exit", {server_command_exit, proxy_command_exit}},
{"$gc", {server_command_get_self_card, proxy_command_get_player_card}},
@@ -1698,7 +1765,7 @@ static const unordered_map<string, ChatCommandDefinition> chat_commands({
{"$kick", {server_command_kick, nullptr}},
{"$li", {server_command_lobby_info, proxy_command_lobby_info}},
{"$ln", {server_command_lobby_type, proxy_command_lobby_type}},
{"$ep3battledebug", {server_command_enable_ep3_battle_debug_menu, nullptr}},
{"$loadchar", {server_command_loadchar, nullptr}},
{"$matcount", {server_command_show_material_counts, nullptr}},
{"$maxlevel", {server_command_max_level, nullptr}},
{"$meseta", {server_command_meseta, nullptr}},
@@ -1710,12 +1777,14 @@ static const unordered_map<string, ChatCommandDefinition> chat_commands({
{"$ping", {server_command_ping, nullptr}},
{"$playrec", {server_command_playrec, nullptr}},
{"$qcall", {server_command_qcall, proxy_command_qcall}},
{"$qcheck", {server_command_qcheck, nullptr}},
{"$qclear", {server_command_qclear, nullptr}},
{"$qset", {server_command_qset, nullptr}},
{"$qsync", {server_command_qsync, nullptr}},
{"$quest", {server_command_quest, nullptr}},
{"$rand", {server_command_rand, proxy_command_rand}},
{"$save", {server_command_save, nullptr}},
{"$savechar", {server_command_savechar, nullptr}},
{"$saverec", {server_command_saverec, nullptr}},
{"$sc", {server_command_send_client, proxy_command_send_client}},
{"$secid", {server_command_secid, proxy_command_secid}},
+149
View File
@@ -0,0 +1,149 @@
#include "ChoiceSearch.hh"
#include <inttypes.h>
#include <string.h>
#include "Client.hh"
using namespace std;
const vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
ChoiceSearchCategory{
.id = 0x0001,
.name = "Level",
.choices = {
{0x0000, "Any"},
{0x0001, "Own level +/- 5"},
{0x0002, "Level 1-10"},
{0x0003, "Level 11-20"},
{0x0004, "Level 21-40"},
{0x0005, "Level 41-60"},
{0x0006, "Level 61-80"},
{0x0007, "Level 81-100"},
{0x0008, "Level 101-120"},
{0x0009, "Level 121-160"},
{0x000A, "Level 161-200"},
},
.client_matches = +[](shared_ptr<Client> searcher_c, shared_ptr<Client> target_c, uint16_t choice_id) -> bool {
if (choice_id == 0x0000) {
return true;
}
uint32_t target_level = target_c->character()->disp.stats.level + 1;
switch (choice_id) {
case 0x0001:
return (labs(static_cast<int32_t>(target_level - searcher_c->character()->disp.stats.level)) <= 5);
case 0x0002:
return (target_level <= 10);
case 0x0003:
return (target_level > 10) && (target_level <= 20);
case 0x0004:
return (target_level > 20) && (target_level <= 40);
case 0x0005:
return (target_level > 40) && (target_level <= 60);
case 0x0006:
return (target_level > 60) && (target_level <= 80);
case 0x0007:
return (target_level > 80) && (target_level <= 100);
case 0x0008:
return (target_level > 100) && (target_level <= 120);
case 0x0009:
return (target_level > 120) && (target_level <= 160);
case 0x000A:
return (target_level > 160) && (target_level <= 200);
}
return false;
},
},
ChoiceSearchCategory{
.id = 0x0002,
.name = "Class",
.choices = {
{0x0000, "Any"},
{0x0010, "Hunter"},
{0x0001, "HUmar"},
{0x0002, "HUnewearl"},
{0x0003, "HUcast"},
{0x000A, "HUcaseal"},
{0x0011, "Ranger"},
{0x0004, "RAmar"},
{0x000C, "RAmarl"},
{0x0005, "RAcast"},
{0x0006, "RAcaseal"},
{0x0012, "Force"},
{0x000B, "FOmar"},
{0x0007, "FOmarl"},
{0x0008, "FOnewm"},
{0x0009, "FOnewearl"},
},
.client_matches = +[](shared_ptr<Client>, shared_ptr<Client> target_c, uint16_t choice_id) -> bool {
switch (choice_id) {
case 0x0000:
return true;
case 0x0010:
return target_c->character()->disp.visual.class_flags & 0x20;
case 0x0011:
return target_c->character()->disp.visual.class_flags & 0x40;
case 0x0012:
return target_c->character()->disp.visual.class_flags & 0x80;
default:
return ((choice_id - 1) == target_c->character()->disp.visual.char_class);
}
},
},
ChoiceSearchCategory{
.id = 0x0003,
.name = "Platform",
.choices = {
{0x0000, "Any"},
{0x0001, "DC betas"},
{0x0002, "DC V1"},
{0x0003, "DC V2 / PC"},
{0x0004, "GC / Xbox Episodes 1&2"},
{0x0005, "GC Episode 3"},
{0x0006, "BB"},
},
.client_matches = +[](shared_ptr<Client>, shared_ptr<Client> target_c, uint16_t choice_id) -> bool {
if (choice_id == 0x0000) {
return true;
}
switch (target_c->version()) {
case Version::DC_NTE:
case Version::DC_V1_11_2000_PROTOTYPE:
return (choice_id == 0x0001);
case Version::DC_V1:
return (choice_id == 0x0002);
case Version::DC_V2:
case Version::PC_V2:
return (choice_id == 0x0003);
case Version::GC_NTE:
case Version::GC_V3:
case Version::XB_V3:
return (choice_id == 0x0004);
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
return (choice_id == 0x0005);
case Version::BB_V4:
return (choice_id == 0x0006);
default:
return false;
}
},
},
ChoiceSearchCategory{
.id = 0x0204,
.name = "Game mode",
.choices = {
{0x0000, "Any"},
{0x0001, "Normal"},
{0x0002, "Hard"},
{0x0003, "Very Hard"},
{0x0004, "Ultimate"},
{0x0005, "Battle"},
{0x0006, "Challenge"},
},
.client_matches = +[](shared_ptr<Client>, shared_ptr<Client> target_c, uint16_t choice_id) -> bool {
uint16_t target_choice_id = target_c->character()->choice_search_config.get_setting(0x0204);
return (choice_id == 0) || (target_choice_id == 0) || (choice_id == target_choice_id);
},
},
});
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <functional>
#include <memory>
#include <phosg/Encoding.hh>
#include <string>
#include <vector>
#include "Text.hh"
class Client;
struct ChoiceSearchConfig {
le_uint32_t disabled = 1; // 0 = enabled, 1 = disabled. Unused in command C3
struct Entry {
le_uint16_t parent_choice_id = 0;
le_uint16_t choice_id = 0;
} __attribute__((packed));
parray<Entry, 5> entries;
int32_t get_setting(uint16_t parent_choice_id) const {
for (size_t z = 0; z < this->entries.size(); z++) {
if (this->entries[z].parent_choice_id == parent_choice_id) {
return this->entries[z].choice_id;
}
}
return -1;
}
} __attribute__((packed));
struct ChoiceSearchCategory {
struct Choice {
uint16_t id;
const char* name;
};
uint16_t id;
const char* name;
std::vector<Choice> choices;
std::function<bool(std::shared_ptr<Client> searcher_c, std::shared_ptr<Client> target_c, uint16_t choice_id)> client_matches;
};
extern const std::vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES;
+654 -12
View File
@@ -171,11 +171,13 @@ Client::Client(
card_battle_table_number(-1),
card_battle_table_seat_number(0),
card_battle_table_seat_state(0),
should_update_play_time(false),
bb_character_index(-1),
next_exp_value(0),
can_chat(true),
pending_bb_save_character_index(0),
dol_base_addr(0) {
dol_base_addr(0),
external_bank_character_index(-1),
last_play_time_update(0) {
this->config.set_flags_for_version(version, -1);
this->config.specific_version = default_specific_version_for_version(version, -1);
@@ -204,6 +206,9 @@ Client::~Client() {
}
}
if ((this->version() == Version::BB_V4) && (this->character_data.get())) {
this->save_all();
}
this->log.info("Deleted");
}
@@ -222,11 +227,15 @@ void Client::reschedule_ping_and_timeout_events() {
}
void Client::set_license(shared_ptr<License> l) {
this->license = l;
this->game_data.guild_card_number = this->license->serial_number;
if (this->version() == Version::BB_V4) {
this->game_data.bb_username = this->license->bb_username;
// Make sure bb_username is filename-safe
for (char ch : l->bb_username) {
if (!isalnum(ch) && (ch != '-') && (ch != '_')) {
throw runtime_error("invalid characters in username");
}
}
}
this->license = l;
}
shared_ptr<ServerState> Client::require_server_state() const {
@@ -245,7 +254,7 @@ shared_ptr<Lobby> Client::require_lobby() const {
return l;
}
shared_ptr<TeamIndex::Team> Client::team() {
shared_ptr<const TeamIndex::Team> Client::team() const {
if (!this->license) {
throw logic_error("Client::team called on client with no license");
}
@@ -254,10 +263,11 @@ shared_ptr<TeamIndex::Team> Client::team() {
return nullptr;
}
auto p = this->game_data.character(false);
auto p = this->character(false);
auto s = this->require_server_state();
auto team = s->team_index->get_by_id(this->license->bb_team_id);
if (!team) {
this->log.info("License contains a team ID, but the team does not exist; clearing team ID from license");
this->license->bb_team_id = 0;
this->license->save();
return nullptr;
@@ -265,6 +275,7 @@ shared_ptr<TeamIndex::Team> Client::team() {
auto member_it = team->members.find(this->license->serial_number);
if (member_it == team->members.end()) {
this->log.info("License contains a team ID, but the team does not contain this member; clearing team ID from license");
this->license->bb_team_id = 0;
this->license->save();
return nullptr;
@@ -277,14 +288,39 @@ shared_ptr<TeamIndex::Team> Client::team() {
string name = p->disp.name.decode(this->language());
if (m.name != name) {
this->log.info("Updating player name in team config");
m.name = name;
team->save_config();
s->team_index->update_member_name(this->license->serial_number, name);
}
}
return team;
}
bool Client::can_see_quest(shared_ptr<const Quest> q, uint8_t difficulty) const {
if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
return true;
}
if (!q->available_expression) {
return true;
}
string expr = q->available_expression->str();
bool ret = q->available_expression->evaluate(this->character()->quest_flags.data.at(difficulty), this->team());
this->log.info("Evaluating quest availability expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE");
return ret;
}
bool Client::can_play_quest(shared_ptr<const Quest> q, uint8_t difficulty) const {
if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
return true;
}
if (!q->enabled_expression) {
return true;
}
string expr = q->enabled_expression->str();
bool ret = q->enabled_expression->evaluate(this->character()->quest_flags.data.at(difficulty), this->team());
this->log.info("Evaluating quest enabled expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE");
return ret;
}
void Client::dispatch_save_game_data(evutil_socket_t, short, void* ctx) {
reinterpret_cast<Client*>(ctx)->save_game_data();
}
@@ -293,8 +329,8 @@ void Client::save_game_data() {
if (this->version() != Version::BB_V4) {
throw logic_error("save_game_data called for non-BB client");
}
if (this->game_data.character(false)) {
this->game_data.save_character_file();
if (this->character(false)) {
this->save_all();
}
}
@@ -335,3 +371,609 @@ void Client::suspend_timeouts() {
event_del(this->idle_timeout_event.get());
this->log.info("Timeouts suspended");
}
void Client::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_ptr<const LevelTable> level_table) {
this->overlay_character_data = make_shared<PSOBBCharacterFile>(*this->character(true, false));
if (rules->weapon_and_armor_mode != BattleRules::WeaponAndArmorMode::ALLOW) {
this->overlay_character_data->inventory.remove_all_items_of_type(0);
this->overlay_character_data->inventory.remove_all_items_of_type(1);
}
if (rules->mag_mode == BattleRules::MagMode::FORBID_ALL) {
this->overlay_character_data->inventory.remove_all_items_of_type(2);
}
if (rules->tool_mode != BattleRules::ToolMode::ALLOW) {
this->overlay_character_data->inventory.remove_all_items_of_type(3);
}
if (rules->replace_char) {
// TODO: Shouldn't we clear other material usage here? It looks like the
// original code doesn't, but that seems wrong.
this->overlay_character_data->inventory.hp_from_materials = 0;
this->overlay_character_data->inventory.tp_from_materials = 0;
uint32_t target_level = clamp<uint32_t>(rules->char_level, 0, 199);
uint8_t char_class = this->overlay_character_data->disp.visual.char_class;
auto& stats = this->overlay_character_data->disp.stats;
stats.reset_to_base(char_class, level_table);
stats.advance_to_level(char_class, target_level, level_table);
stats.unknown_a1 = 40;
stats.meseta = 300;
}
if (rules->tech_disk_mode == BattleRules::TechDiskMode::LIMIT_LEVEL) {
// TODO: Verify this is what the game actually does.
for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) {
uint8_t existing_level = this->overlay_character_data->get_technique_level(tech_num);
if ((existing_level != 0xFF) && (existing_level > rules->max_tech_level)) {
this->overlay_character_data->set_technique_level(tech_num, rules->max_tech_level);
}
}
} else if (rules->tech_disk_mode == BattleRules::TechDiskMode::FORBID_ALL) {
for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) {
this->overlay_character_data->set_technique_level(tech_num, 0xFF);
}
}
if (rules->meseta_mode != BattleRules::MesetaMode::ALLOW) {
this->overlay_character_data->disp.stats.meseta = 0;
}
if (rules->forbid_scape_dolls) {
this->overlay_character_data->inventory.remove_all_items_of_type(3, 9);
}
}
void Client::create_challenge_overlay(Version version, size_t template_index, shared_ptr<const LevelTable> level_table) {
auto p = this->character(true, false);
const auto& tpl = get_challenge_template_definition(version, p->disp.visual.class_flags, template_index);
this->overlay_character_data = make_shared<PSOBBCharacterFile>(*p);
auto overlay = this->overlay_character_data;
for (size_t z = 0; z < overlay->inventory.items.size(); z++) {
auto& i = overlay->inventory.items[z];
i.present = 0;
i.unknown_a1 = 0;
i.extension_data1 = 0;
i.extension_data2 = 0;
i.flags = 0;
i.data = ItemData();
}
overlay->inventory.items[13].extension_data2 = 1;
overlay->disp.stats.reset_to_base(overlay->disp.visual.char_class, level_table);
overlay->disp.stats.advance_to_level(overlay->disp.visual.char_class, tpl.level, level_table);
overlay->disp.stats.unknown_a1 = 40;
overlay->disp.stats.unknown_a3 = 10.0;
overlay->disp.stats.experience = level_table->stats_delta_for_level(overlay->disp.visual.char_class, overlay->disp.stats.level).experience;
overlay->disp.stats.meseta = 0;
overlay->clear_all_material_usage();
for (size_t z = 0; z < 0x13; z++) {
overlay->set_technique_level(z, 0xFF);
}
for (size_t z = 0; z < tpl.items.size(); z++) {
auto& inv_item = overlay->inventory.items[z];
inv_item.present = tpl.items[z].present;
inv_item.unknown_a1 = tpl.items[z].unknown_a1;
inv_item.flags = tpl.items[z].flags;
inv_item.data = tpl.items[z].data;
}
overlay->inventory.num_items = tpl.items.size();
for (const auto& tech_level : tpl.tech_levels) {
overlay->set_technique_level(tech_level.tech_num, tech_level.level);
}
}
void Client::import_blocked_senders(const parray<le_uint32_t, 30>& blocked_senders) {
this->blocked_senders.clear();
for (size_t z = 0; z < blocked_senders.size(); z++) {
if (blocked_senders[z]) {
this->blocked_senders.emplace(blocked_senders[z]);
}
}
}
shared_ptr<PSOBBBaseSystemFile> Client::system_file(bool allow_load) {
if (!this->system_data && allow_load) {
this->load_all_files();
}
return this->system_data;
}
shared_ptr<const PSOBBBaseSystemFile> Client::system_file(bool allow_load) const {
if (!this->system_data.get() && allow_load) {
throw runtime_error("system data is not loaded");
}
return this->system_data;
}
shared_ptr<PSOBBCharacterFile> Client::character(bool allow_load, bool allow_overlay) {
if (this->overlay_character_data && allow_overlay) {
return this->overlay_character_data;
}
if (!this->character_data && allow_load) {
if ((this->version() == Version::BB_V4) && (this->bb_character_index < 0)) {
throw runtime_error("character index not specified");
}
this->load_all_files();
}
return this->character_data;
}
shared_ptr<const PSOBBCharacterFile> Client::character(bool allow_load, bool allow_overlay) const {
if (allow_overlay && this->overlay_character_data) {
return this->overlay_character_data;
}
if (!this->character_data && allow_load) {
throw runtime_error("character data is not loaded");
}
return this->character_data;
}
shared_ptr<PSOBBGuildCardFile> Client::guild_card_file(bool allow_load) {
if (!this->guild_card_data && allow_load) {
this->load_all_files();
}
return this->guild_card_data;
}
shared_ptr<const PSOBBGuildCardFile> Client::guild_card_file(bool allow_load) const {
if (!this->guild_card_data && allow_load) {
throw runtime_error("account data is not loaded");
}
return this->guild_card_data;
}
string Client::system_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have system data");
}
if (!this->license) {
throw logic_error("client is not logged in");
}
return string_printf("system/players/system_%s.psosys", this->license->bb_username.c_str());
}
string Client::character_filename(const std::string& bb_username, int8_t index) {
if (bb_username.empty()) {
throw logic_error("non-BB players do not have character data");
}
if (index < 0) {
throw logic_error("character index is not set");
}
return string_printf("system/players/player_%s_%hhd.psochar", bb_username.c_str(), index);
}
string Client::backup_character_filename(uint32_t serial_number, size_t index) {
return string_printf("system/players/backup_player_%" PRIu32 "_%zu.psochar", serial_number, index);
}
string Client::character_filename(int8_t index) const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have character data");
}
if (!this->license) {
throw logic_error("client is not logged in");
}
return this->character_filename(this->license->bb_username, (index < 0) ? this->bb_character_index : index);
}
string Client::guild_card_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have character data");
}
if (!this->license) {
throw logic_error("client is not logged in");
}
return string_printf("system/players/guild_cards_%s.psocard", this->license->bb_username.c_str());
}
string Client::shared_bank_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have character data");
}
if (!this->license) {
throw logic_error("client is not logged in");
}
return string_printf("system/players/shared_bank_%s.psobank", this->license->bb_username.c_str());
}
string Client::legacy_account_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have character data");
}
if (!this->license) {
throw logic_error("client is not logged in");
}
return string_printf("system/players/account_%s.nsa", this->license->bb_username.c_str());
}
string Client::legacy_player_filename() const {
if (this->version() != Version::BB_V4) {
throw logic_error("non-BB players do not have character data");
}
if (!this->license) {
throw logic_error("client is not logged in");
}
if (this->bb_character_index < 0) {
throw logic_error("character index is not set");
}
return string_printf(
"system/players/player_%s_%hhd.nsc",
this->license->bb_username.c_str(),
static_cast<int8_t>(this->bb_character_index + 1));
}
void Client::create_character_file(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
shared_ptr<const LevelTable> level_table) {
this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table);
this->save_character_file();
}
void Client::load_all_files() {
if (this->version() != Version::BB_V4) {
this->system_data = make_shared<PSOBBBaseSystemFile>();
this->character_data = make_shared<PSOBBCharacterFile>();
this->guild_card_data = make_shared<PSOBBGuildCardFile>();
return;
}
if (!this->license) {
throw logic_error("cannot load BB player data until client is logged in");
}
this->system_data.reset();
this->character_data.reset();
this->guild_card_data.reset();
auto files_manager = this->require_server_state()->player_files_manager;
string sys_filename = this->system_filename();
this->system_data = files_manager->get_system(sys_filename);
if (this->system_data) {
player_data_log.info("Using loaded system file %s", sys_filename.c_str());
} else if (isfile(sys_filename)) {
this->system_data = make_shared<PSOBBBaseSystemFile>(load_object_file<PSOBBBaseSystemFile>(sys_filename, true));
files_manager->set_system(sys_filename, this->system_data);
player_data_log.info("Loaded system data from %s", sys_filename.c_str());
} else {
player_data_log.info("System file is missing: %s", sys_filename.c_str());
}
if (this->bb_character_index >= 0) {
string char_filename = this->character_filename();
this->character_data = files_manager->get_character(char_filename);
if (this->character_data) {
player_data_log.info("Using loaded character file %s", char_filename.c_str());
} else if (isfile(char_filename)) {
auto f = fopen_unique(char_filename, "rb");
auto header = 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>(freadx<PSOBBCharacterFile>(f.get()));
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
// file instead
if (!this->system_data) {
this->system_data = make_shared<PSOBBBaseSystemFile>(freadx<PSOBBBaseSystemFile>(f.get()));
files_manager->set_system(sys_filename, this->system_data);
player_data_log.info("Loaded system data from %s", char_filename.c_str());
}
} else {
player_data_log.info("Character file is missing: %s", char_filename.c_str());
}
}
string card_filename = this->guild_card_filename();
this->guild_card_data = files_manager->get_guild_card(card_filename);
if (this->guild_card_data) {
player_data_log.info("Using loaded Guild Card file %s", card_filename.c_str());
} else if (isfile(card_filename)) {
this->guild_card_data = make_shared<PSOBBGuildCardFile>(load_object_file<PSOBBGuildCardFile>(card_filename));
files_manager->set_guild_card(card_filename, this->guild_card_data);
player_data_log.info("Loaded Guild Card data from %s", card_filename.c_str());
} else {
player_data_log.info("Guild Card file is missing: %s", card_filename.c_str());
}
// If any of the above files were missing, try to load from .nsa/.nsc files instead
if (!this->system_data || (!this->character_data && (this->bb_character_index >= 0)) || !this->guild_card_data) {
string nsa_filename = this->legacy_account_filename();
shared_ptr<LegacySavedAccountDataBB> nsa_data;
if (isfile(nsa_filename)) {
nsa_data = make_shared<LegacySavedAccountDataBB>(load_object_file<LegacySavedAccountDataBB>(nsa_filename));
if (!nsa_data->signature.eq(LegacySavedAccountDataBB::SIGNATURE)) {
throw runtime_error("account data header is incorrect");
}
if (!this->system_data) {
this->system_data = make_shared<PSOBBBaseSystemFile>(nsa_data->system_file.base);
files_manager->set_system(sys_filename, this->system_data);
player_data_log.info("Loaded legacy system data from %s", nsa_filename.c_str());
}
if (!this->guild_card_data) {
this->guild_card_data = make_shared<PSOBBGuildCardFile>(nsa_data->guild_card_file);
files_manager->set_guild_card(card_filename, this->guild_card_data);
player_data_log.info("Loaded legacy Guild Card data from %s", nsa_filename.c_str());
}
}
if (!this->system_data) {
this->system_data = make_shared<PSOBBBaseSystemFile>();
files_manager->set_system(sys_filename, this->system_data);
player_data_log.info("Created new system data");
}
if (!this->guild_card_data) {
this->guild_card_data = make_shared<PSOBBGuildCardFile>();
files_manager->set_guild_card(card_filename, this->guild_card_data);
player_data_log.info("Created new Guild Card data");
}
if (!this->character_data && (this->bb_character_index >= 0)) {
string nsc_filename = this->legacy_player_filename();
auto nsc_data = load_object_file<LegacySavedPlayerDataBB>(nsc_filename);
if (nsc_data.signature == LegacySavedPlayerDataBB::SIGNATURE_V0) {
nsc_data.signature = LegacySavedPlayerDataBB::SIGNATURE_V0;
nsc_data.unused.clear();
nsc_data.battle_records.place_counts.clear(0);
nsc_data.battle_records.disconnect_count = 0;
nsc_data.battle_records.unknown_a1.clear(0);
} else if (nsc_data.signature != LegacySavedPlayerDataBB::SIGNATURE_V1) {
throw runtime_error("legacy player data has incorrect signature");
}
this->character_data = make_shared<PSOBBCharacterFile>();
files_manager->set_character(this->character_filename(), this->character_data);
this->character_data->inventory = nsc_data.inventory;
this->character_data->disp = nsc_data.disp;
this->character_data->play_time_seconds = nsc_data.disp.play_time;
this->character_data->unknown_a2 = nsc_data.unknown_a2;
this->character_data->quest_flags = nsc_data.quest_flags;
this->character_data->death_count = nsc_data.death_count;
this->character_data->bank = nsc_data.bank;
this->character_data->guild_card.guild_card_number = this->license->serial_number;
this->character_data->guild_card.name = nsc_data.disp.name;
this->character_data->guild_card.description = nsc_data.guild_card_description;
this->character_data->guild_card.present = 1;
this->character_data->guild_card.language = nsc_data.inventory.language;
this->character_data->guild_card.section_id = nsc_data.disp.visual.section_id;
this->character_data->guild_card.char_class = nsc_data.disp.visual.char_class;
this->character_data->auto_reply = nsc_data.auto_reply;
this->character_data->info_board = nsc_data.info_board;
this->character_data->battle_records = nsc_data.battle_records;
this->character_data->challenge_records = nsc_data.challenge_records;
this->character_data->tech_menu_config = nsc_data.tech_menu_config;
this->character_data->quest_global_flags = nsc_data.quest_global_flags;
if (nsa_data) {
this->character_data->option_flags = nsa_data->option_flags;
this->character_data->symbol_chats = nsa_data->symbol_chats;
this->character_data->shortcuts = nsa_data->shortcuts;
player_data_log.info("Loaded legacy player data from %s and %s", nsa_filename.c_str(), nsc_filename.c_str());
} else {
player_data_log.info("Loaded legacy player data from %s", nsc_filename.c_str());
}
}
}
if (this->character_data) {
this->license->auto_reply_message = this->character_data->auto_reply.decode();
this->license->save();
}
this->blocked_senders.clear();
for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) {
if (this->guild_card_data->blocked[z].present) {
this->blocked_senders.emplace(this->guild_card_data->blocked[z].guild_card_number);
}
}
if (this->character_data) {
this->last_play_time_update = now();
}
}
void Client::save_all() {
if (this->system_data) {
this->save_system_file();
}
if (this->character_data) {
this->save_character_file();
}
if (this->guild_card_data) {
this->save_guild_card_file();
}
if (this->external_bank) {
string filename = this->shared_bank_filename();
save_object_file<PlayerBank>(filename, *this->external_bank);
player_data_log.info("Saved shared bank file %s", filename.c_str());
}
if (this->external_bank_character) {
this->save_character_file(
this->character_filename(this->external_bank_character_index),
this->system_data,
this->external_bank_character);
}
}
void Client::save_system_file() const {
if (!this->system_data) {
throw logic_error("no system file loaded");
}
string filename = this->system_filename();
save_object_file(filename, *this->system_data);
player_data_log.info("Saved system file %s", filename.c_str());
}
void Client::save_character_file(
const string& filename,
shared_ptr<const PSOBBBaseSystemFile> system,
shared_ptr<const PSOBBCharacterFile> character) {
auto f = fopen_unique(filename, "wb");
PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBBaseSystemFile) + sizeof(PSOBBTeamMembership), 0x00E7, 0x00000000};
fwritex(f.get(), header);
fwritex(f.get(), *character);
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 License, 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 License 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;
fwritex(f.get(), empty_membership);
player_data_log.info("Saved character file %s", filename.c_str());
}
void Client::save_character_file() {
if (!this->system_data.get()) {
throw logic_error("no system file loaded");
}
if (!this->character_data.get()) {
throw logic_error("no character file loaded");
}
if (this->should_update_play_time) {
// This is slightly inaccurate, since fractions of a second are truncated
// off each time we save. I'm lazy, so insert shrug emoji here.
uint64_t t = now();
uint64_t seconds = (t - this->last_play_time_update) / 1000000;
this->character_data->disp.play_time += seconds;
this->character_data->play_time_seconds = this->character_data->disp.play_time;
player_data_log.info("Added %" PRIu64 " seconds to play time", seconds);
this->last_play_time_update = t;
}
this->save_character_file(this->character_filename(), this->system_data, this->character_data);
}
void Client::save_guild_card_file() const {
if (!this->guild_card_data.get()) {
throw logic_error("no Guild Card file loaded");
}
string filename = this->guild_card_filename();
save_object_file(filename, *this->guild_card_data);
player_data_log.info("Saved Guild Card file %s", filename.c_str());
}
void Client::load_backup_character(uint32_t serial_number, size_t index) {
string filename = this->backup_character_filename(serial_number, index);
auto f = fopen_unique(filename, "rb");
auto header = 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>(freadx<PSOBBCharacterFile>(f.get()));
this->v1_v2_last_reported_disp.reset();
}
void Client::save_and_unload_character() {
if (this->character_data) {
this->save_character_file();
this->character_data.reset();
this->log.info("Unloaded character");
}
}
PlayerBank& Client::current_bank() {
if (this->external_bank) {
return *this->external_bank;
} else if (this->external_bank_character) {
return this->external_bank_character->bank;
}
return this->character()->bank;
}
std::shared_ptr<PSOBBCharacterFile> Client::current_bank_character() {
return this->external_bank_character ? this->external_bank_character : this->character();
}
void Client::use_default_bank() {
if (this->external_bank) {
string filename = this->shared_bank_filename();
save_object_file<PlayerBank>(filename, *this->external_bank);
this->external_bank.reset();
player_data_log.info("Detached shared bank %s", filename.c_str());
}
if (this->external_bank_character) {
string filename = this->character_filename(this->external_bank_character_index);
this->save_character_file(filename, this->system_data, this->external_bank_character);
this->external_bank_character.reset();
player_data_log.info("Detached character %s from bank", filename.c_str());
}
}
bool Client::use_shared_bank() {
this->use_default_bank();
string filename = this->shared_bank_filename();
auto files_manager = this->require_server_state()->player_files_manager;
this->external_bank = files_manager->get_bank(filename);
if (this->external_bank) {
player_data_log.info("Using loaded shared bank %s", filename.c_str());
return true;
} else if (isfile(filename)) {
this->external_bank = make_shared<PlayerBank>(load_object_file<PlayerBank>(filename));
files_manager->set_bank(filename, this->external_bank);
player_data_log.info("Loaded shared bank %s", filename.c_str());
return true;
} else {
this->external_bank = make_shared<PlayerBank>();
files_manager->set_bank(filename, this->external_bank);
player_data_log.info("Created shared bank for %s", filename.c_str());
return false;
}
}
void Client::use_character_bank(int8_t index) {
this->use_default_bank();
if (index != this->bb_character_index) {
auto files_manager = this->require_server_state()->player_files_manager;
string filename = this->character_filename(index);
this->external_bank_character = files_manager->get_character(filename);
if (this->external_bank_character) {
this->external_bank_character_index = index;
player_data_log.info("Using loaded character file %s for external bank", filename.c_str());
} else if (isfile(filename)) {
auto f = fopen_unique(filename, "rb");
auto header = 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>(freadx<PSOBBCharacterFile>(f.get()));
this->external_bank_character_index = index;
files_manager->set_character(filename, this->external_bank_character);
player_data_log.info("Loaded character data from %s for external bank", filename.c_str());
} else {
throw runtime_error("character does not exist");
}
}
}
+110 -8
View File
@@ -15,7 +15,7 @@
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "PatchFileIndex.hh"
#include "Player.hh"
#include "Quest.hh"
#include "QuestScript.hh"
#include "TeamIndex.hh"
#include "Text.hh"
@@ -25,7 +25,8 @@ extern const uint64_t CLIENT_CONFIG_MAGIC;
class Server;
struct Lobby;
struct Client : public std::enable_shared_from_this<Client> {
class Client : public std::enable_shared_from_this<Client> {
public:
enum class Flag : uint64_t {
// clang-format off
@@ -54,7 +55,7 @@ struct Client : public std::enable_shared_from_this<Client> {
HAS_EP3_MEDIA_UPDATES = 0x0000000010000000,
USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000,
HAS_GUILD_CARD_NUMBER = 0x0000000040000000,
ACCEPTED_TEAM_INVITATION = 0x0000000080000000,
AT_BANK_COUNTER = 0x0000000080000000,
// Cheat mode flags
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
@@ -147,7 +148,6 @@ struct Client : public std::enable_shared_from_this<Client> {
};
std::weak_ptr<Server> server;
std::weak_ptr<ServerState> server_state;
uint64_t id;
PrefixedLogger log;
@@ -180,7 +180,7 @@ struct Client : public std::enable_shared_from_this<Client> {
uint8_t lobby_client_id;
uint8_t lobby_arrow_color;
int64_t preferred_lobby_id; // <0 = no preference
ClientGameData game_data;
std::unique_ptr<struct event, void (*)(struct event*)> save_game_data_event;
std::unique_ptr<struct event, void (*)(struct event*)> send_ping_event;
std::unique_ptr<struct event, void (*)(struct event*)> idle_timeout_event;
@@ -197,12 +197,38 @@ struct Client : public std::enable_shared_from_this<Client> {
};
std::unique_ptr<std::deque<JoinCommand>> game_join_command_queue;
// Character / game data
struct PendingItemTrade {
uint8_t other_client_id;
bool confirmed; // true if client has sent a D2 command
std::vector<ItemData> items;
};
struct PendingCardTrade {
uint8_t other_client_id;
bool confirmed; // true if client has sent an EE D2 command
std::vector<std::pair<uint32_t, uint32_t>> card_to_count;
};
bool should_update_play_time;
std::unordered_set<uint32_t> blocked_senders;
std::unique_ptr<PlayerDispDataDCPCV3> v1_v2_last_reported_disp;
// These are null unless the client is within the trade sequence (D0-D4 or EE commands)
std::unique_ptr<PendingItemTrade> pending_item_trade;
std::unique_ptr<PendingCardTrade> pending_card_trade;
std::shared_ptr<Episode3::PlayerConfig> ep3_config; // Null for non-Ep3
int8_t bb_character_index;
ItemData bb_identify_result;
std::array<std::vector<ItemData>, 3> bb_shop_contents;
// Miscellaneous (used by chat commands)
uint32_t next_exp_value; // next EXP value to give
G_SwitchStateChanged_6x05 last_switch_enabled_command;
bool can_chat;
std::string pending_bb_save_username;
uint8_t pending_bb_save_character_index;
struct PendingCharacterExport {
std::shared_ptr<const License> license;
ssize_t character_index = -1;
bool is_bb_conversion = false;
};
std::unique_ptr<PendingCharacterExport> pending_character_export;
std::deque<std::function<void(uint32_t, uint32_t)>> function_call_response_queue;
// File loading state
@@ -232,7 +258,10 @@ struct Client : public std::enable_shared_from_this<Client> {
std::shared_ptr<ServerState> require_server_state() const;
std::shared_ptr<Lobby> require_lobby() const;
std::shared_ptr<TeamIndex::Team> team();
std::shared_ptr<const TeamIndex::Team> team() const;
bool can_see_quest(std::shared_ptr<const Quest> q, uint8_t difficulty) const;
bool can_play_quest(std::shared_ptr<const Quest> q, uint8_t difficulty) const;
static void dispatch_save_game_data(evutil_socket_t, short, void* ctx);
void save_game_data();
@@ -242,4 +271,77 @@ struct Client : public std::enable_shared_from_this<Client> {
void idle_timeout();
void suspend_timeouts();
const std::string& get_bb_username() const;
void set_bb_username(const std::string& bb_username);
void create_battle_overlay(std::shared_ptr<const BattleRules> rules, std::shared_ptr<const LevelTable> level_table);
void create_challenge_overlay(Version version, size_t template_index, std::shared_ptr<const LevelTable> level_table);
inline void delete_overlay() {
this->overlay_character_data.reset();
}
inline bool has_overlay() const {
return this->overlay_character_data.get() != nullptr;
}
void import_blocked_senders(const parray<le_uint32_t, 30>& blocked_senders);
std::shared_ptr<PSOBBBaseSystemFile> system_file(bool allow_load = true);
std::shared_ptr<PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true);
std::shared_ptr<PSOBBGuildCardFile> guild_card_file(bool allow_load = true);
std::shared_ptr<const PSOBBBaseSystemFile> system_file(bool allow_load = true) const;
std::shared_ptr<const PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true) const;
std::shared_ptr<const PSOBBGuildCardFile> guild_card_file(bool allow_load = true) const;
void create_character_file(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
std::shared_ptr<const LevelTable> level_table);
std::string system_filename() const;
static std::string character_filename(const std::string& bb_username, int8_t index);
static std::string backup_character_filename(uint32_t serial_number, size_t index);
std::string character_filename(int8_t index = -1) const;
std::string guild_card_filename() const;
std::string shared_bank_filename() const;
std::string legacy_player_filename() const;
std::string legacy_account_filename() const;
void save_all();
void save_system_file() const;
static void save_character_file(
const std::string& filename,
std::shared_ptr<const PSOBBBaseSystemFile> sys,
std::shared_ptr<const PSOBBCharacterFile> character);
// Note: This function is not const because it updates the player's play time.
void save_character_file();
void save_guild_card_file() const;
void load_backup_character(uint32_t serial_number, size_t index);
void save_and_unload_character();
PlayerBank& current_bank();
std::shared_ptr<PSOBBCharacterFile> current_bank_character();
bool use_shared_bank(); // Returns true if the bank exists; false if it was created
void use_character_bank(int8_t bb_character_index);
void use_default_bank();
private:
// The overlay character data is used in battle and challenge modes, when
// character data is temporarily replaced in-game. In other play modes and in
// lobbies, overlay_character_data is null.
std::shared_ptr<PSOBBBaseSystemFile> system_data;
std::shared_ptr<PSOBBCharacterFile> overlay_character_data;
std::shared_ptr<PSOBBCharacterFile> character_data;
std::shared_ptr<PSOBBGuildCardFile> guild_card_data;
std::shared_ptr<PlayerBank> external_bank;
std::shared_ptr<PSOBBCharacterFile> external_bank_character;
int8_t external_bank_character_index;
uint64_t last_play_time_update;
void save_and_clear_external_bank();
void load_all_files();
};
+139 -83
View File
@@ -10,7 +10,7 @@
#include "Episode3/MapState.hh"
#include "Episode3/PlayerStateSubordinates.hh"
#include "PSOProtocol.hh"
#include "Player.hh"
#include "PlayerSubordinates.hh"
#include "SaveFileFormats.hh"
#include "Text.hh"
@@ -605,9 +605,10 @@ struct SC_MeetUserExtension {
le_uint32_t menu_id = 0;
le_uint32_t item_id = 0;
} __packed__;
parray<LobbyReference, 8> lobby_refs;
le_uint32_t unknown_a2 = 0;
pstring<Encoding, 0x20> player_name;
/* 00 */ parray<LobbyReference, 8> lobby_refs;
/* 40 */ le_uint32_t unknown_a2 = 0;
/* 44 */ pstring<Encoding, 0x20> player_name;
/* 64 (or 84 on UTF16 versions) */
} __packed__;
struct S_LegacyJoinGame_PC_0E {
@@ -1052,7 +1053,6 @@ struct C_OpenFileConfirmation_44_A6 {
// 61 (C->S): Player data
// Internal name: SndCharaDataV2 (SndCharaData in DCv1)
// See the PSOPlayerData structs in Player.hh for this command's format.
// header.flag specifies the format version, which is related to (but not
// identical to) the game's major version. For example, the format version is 01
// on DC v1, 02 on PSO PC, 03 on PSO GC, XB, and BB, and 04 on Episode 3.
@@ -1103,7 +1103,7 @@ struct C_CharacterData_DCv2_61_98 {
/* 0000 */ PlayerInventory inventory;
/* 034C */ PlayerDispDataDCPCV3 disp;
/* 041C */ PlayerRecordsEntry_DC records;
/* 04D8 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
/* 04D8 */ ChoiceSearchConfig choice_search_config;
/* 04F0 */
} __attribute__((packed));
@@ -1111,7 +1111,7 @@ struct C_CharacterData_PC_61_98 {
/* 0000 */ PlayerInventory inventory;
/* 034C */ PlayerDispDataDCPCV3 disp;
/* 041C */ PlayerRecordsEntry_PC records;
/* 0510 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
/* 0510 */ ChoiceSearchConfig choice_search_config;
/* 0528 */ parray<le_uint32_t, 0x1E> blocked_senders;
/* 05A0 */ le_uint32_t auto_reply_enabled = 0;
// The auto-reply message can be up to 0x200 characters. If it's shorter than
@@ -1120,11 +1120,24 @@ struct C_CharacterData_PC_61_98 {
/* 05A4 */ // uint16_t auto_reply[...EOF];
} __attribute__((packed));
struct C_CharacterData_GCNTE_61_98 {
/* 0000 */ PlayerInventory inventory;
/* 034C */ PlayerDispDataDCPCV3 disp;
/* 041C */ PlayerRecordsEntry_DC records;
/* 04D8 */ ChoiceSearchConfig choice_search_config;
/* 04F0 */ parray<le_uint32_t, 0x1E> blocked_senders;
/* 0468 */ le_uint32_t auto_reply_enabled = 0;
// The auto-reply message can be up to 0x200 bytes. If it's shorter than that,
// the client truncates the command after the first zero byte (rounded up to
// the next 4-byte boundary).
/* 046C */ // char auto_reply[...EOF];
} __attribute__((packed));
struct C_CharacterData_V3_61_98 {
/* 0000 */ PlayerInventory inventory;
/* 034C */ PlayerDispDataDCPCV3 disp;
/* 041C */ PlayerRecordsEntry_V3 records;
/* 0538 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
/* 0538 */ ChoiceSearchConfig choice_search_config;
/* 0550 */ pstring<TextEncoding::MARKED, 0xAC> info_board;
/* 05FC */ parray<le_uint32_t, 0x1E> blocked_senders;
/* 0674 */ le_uint32_t auto_reply_enabled = 0;
@@ -1138,7 +1151,7 @@ struct C_CharacterData_GC_Ep3_61_98 {
/* 0000 */ PlayerInventory inventory;
/* 034C */ PlayerDispDataDCPCV3 disp;
/* 041C */ PlayerRecordsEntry_V3 records;
/* 0538 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
/* 0538 */ ChoiceSearchConfig choice_search_config;
/* 0550 */ pstring<TextEncoding::MARKED, 0xAC> info_board;
/* 05FC */ parray<le_uint32_t, 0x1E> blocked_senders;
/* 0674 */ le_uint32_t auto_reply_enabled = 0;
@@ -1151,7 +1164,7 @@ struct C_CharacterData_BB_61_98 {
/* 0000 */ PlayerInventory inventory;
/* 034C */ PlayerDispDataBB disp;
/* 04DC */ PlayerRecordsEntry_BB records;
/* 0638 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
/* 0638 */ ChoiceSearchConfig choice_search_config;
/* 0650 */ pstring<TextEncoding::UTF16, 0xAC> info_board;
/* 07A8 */ parray<le_uint32_t, 0x1E> blocked_senders;
/* 0820 */ le_uint32_t auto_reply_enabled = 0;
@@ -1983,6 +1996,15 @@ struct C_ChangeShipOrBlock_A0_A1 {
template <TextEncoding Encoding, size_t ShortDescLength>
struct S_QuestMenuEntry {
// Note: The game treats menu_id as two 8-bit fields followed by a 16-bit
// field. In most situations, this is opaque to the server, so we treat it as
// a single 32-bit field, but in the case of the quest menu, the second byte
// is used to determine the icon that appears to the left of the quest name.
// Specifically:
// 0 = online quest icon (green diamond)
// 1 = download quest icon (green square with outlined diamond)
// 2 = completed download quest icon (orange square with outlined diamond)
// Anything else = same as 1
le_uint32_t menu_id = 0;
le_uint32_t item_id = 0;
pstring<Encoding, 0x20> name;
@@ -1994,7 +2016,17 @@ struct S_QuestMenuEntry_DC_GC_A2_A4 : S_QuestMenuEntry<TextEncoding::MARKED, 0x7
} __packed__;
struct S_QuestMenuEntry_XB_A2_A4 : S_QuestMenuEntry<TextEncoding::MARKED, 0x80> {
} __packed__;
struct S_QuestMenuEntry_BB_A2_A4 : S_QuestMenuEntry<TextEncoding::UTF16, 0x7A> {
struct S_QuestMenuEntry_BB_A2_A4 {
le_uint32_t menu_id = 0;
le_uint32_t item_id = 0;
pstring<TextEncoding::UTF16, 0x20> name;
pstring<TextEncoding::UTF16, 0x78> short_description;
// If this field is set, a yellow hex icon is displayed instead of the green
// or orange diamond icon, and the quest is grayed out and cannot be selected.
// This field is ignored if the icon type (see S_QuestMenuEntry) isn't 1 or 2.
uint8_t disabled = 0;
parray<uint8_t, 3> unused;
} __packed__;
// A3 (S->C): Quest information
@@ -2359,19 +2391,17 @@ struct S_TournamentMatchInformation_GC_Ep3_BB {
// Internal name: RcvChoiceList
// Command is a list of these; header.flag is the entry count (incl. top-level).
template <typename ItemIDT, TextEncoding Encoding>
template <TextEncoding Encoding>
struct S_ChoiceSearchEntry {
// Category IDs are nonzero; if the high byte of the ID is nonzero then the
// category can be set by the user at any time; otherwise it can't.
ItemIDT parent_category_id = 0; // 0 for top-level categories
ItemIDT category_id = 0;
le_uint16_t parent_choice_id = 0; // 0 for top-level categories
le_uint16_t choice_id = 0;
pstring<Encoding, 0x1C> text;
} __packed__;
struct S_ChoiceSearchEntry_DC_C0 : S_ChoiceSearchEntry<le_uint32_t, TextEncoding::MARKED> {
struct S_ChoiceSearchEntry_DC_V3_C0 : S_ChoiceSearchEntry<TextEncoding::MARKED> {
} __packed__;
struct S_ChoiceSearchEntry_V3_C0 : S_ChoiceSearchEntry<le_uint16_t, TextEncoding::MARKED> {
} __packed__;
struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry<le_uint16_t, TextEncoding::UTF16> {
struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry<TextEncoding::UTF16> {
} __packed__;
// Top-level categories are things like "Level", "Class", etc.
@@ -2425,12 +2455,7 @@ struct C_CreateGame_BB_C1 : C_CreateGame<TextEncoding::UTF16> {
// C2 (C->S): Set choice search parameters (DCv2 and later versions)
// Internal name: PutChoiceList
// Server does not respond.
// The ChoiceSearchConfig structure is defined in PlayerSubordinates.hh.
struct C_ChoiceSearchSelections_DC_C2_C3 : ChoiceSearchConfig<le_uint32_t> {
} __packed__;
struct C_ChoiceSearchSelections_PC_V3_BB_C2_C3 : ChoiceSearchConfig<le_uint16_t> {
} __packed__;
// Contents is a ChoiceSearchConfig, which is defined in PlayerSubordinates.hh.
// C3 (C->S): Execute choice search (DCv2 and later versions)
// Internal name: SndChoiceSeq
@@ -2441,22 +2466,25 @@ struct C_ChoiceSearchSelections_PC_V3_BB_C2_C3 : ChoiceSearchConfig<le_uint16_t>
// Internal name: RcvChoiceAns
// Command is a list of these; header.flag is the entry count
struct S_ChoiceSearchResultEntry_V3_C4 {
template <typename HeaderT, TextEncoding NameEncoding, TextEncoding DescEncoding, TextEncoding LocatorEncoding>
struct S_ChoiceSearchResultEntry_C4 {
le_uint32_t guild_card_number = 0;
pstring<TextEncoding::ASCII, 0x10> name; // No language marker, as usual on V3
pstring<TextEncoding::MARKED, 0x20> info_string; // Usually something like "<class> Lvl <level>"
pstring<NameEncoding, 0x10> name;
pstring<DescEncoding, 0x20> info_string; // Usually something like "<class> Lvl <level>"
// Format is stricter here; this is "LOBBYNAME,BLOCKNUM,SHIPNAME"
// If target is in game, for example, "Game Name,BLOCK01,Alexandria"
// If target is in lobby, for example, "BLOCK01-1,BLOCK01,Alexandria"
pstring<TextEncoding::MARKED, 0x34> locator_string;
// Server IP and port for "meet user" option
le_uint32_t server_ip = 0;
le_uint16_t server_port = 0;
le_uint16_t unused1 = 0;
le_uint32_t menu_id = 0;
le_uint32_t lobby_id = 0; // These two are guesses
le_uint32_t game_id = 0; // Zero if target is in a lobby rather than a game
parray<uint8_t, 0x58> unused2;
pstring<LocatorEncoding, 0x30> location_string;
HeaderT reconnect_command_header; // Ignored by the client
S_Reconnect_19 reconnect_command;
SC_MeetUserExtension<NameEncoding> meet_user;
} __packed__;
struct S_ChoiceSearchResultEntry_DC_V3_C4 : S_ChoiceSearchResultEntry_C4<PSOCommandHeaderDCV3, TextEncoding::ASCII, TextEncoding::MARKED, TextEncoding::ASCII> {
} __packed__;
struct S_ChoiceSearchResultEntry_PC_C4 : S_ChoiceSearchResultEntry_C4<PSOCommandHeaderPC, TextEncoding::UTF16, TextEncoding::UTF16, TextEncoding::UTF16> {
} __packed__;
struct S_ChoiceSearchResultEntry_BB_C4 : S_ChoiceSearchResultEntry_C4<PSOCommandHeaderBB, TextEncoding::UTF16, TextEncoding::UTF16, TextEncoding::UTF16> {
} __packed__;
// C5 (S->C): Player records update (DCv2 and later versions)
@@ -3257,9 +3285,8 @@ struct C_AddOrRemoveTeamMember_BB_03EA_05EA {
struct SC_TeamChat_BB_07EA {
pstring<TextEncoding::UTF16, 0x10> sender_name;
// It seems there are no real limits on the message length, other than the
// overall command length limit of 0x7C00 bytes.
// Text follows here
// Text follows here. The message is truncated by the client if it is longer
// than 0x8F wchar_ts.
} __packed__;
// 08EA (C->S): Get team member list
@@ -3270,8 +3297,8 @@ struct SC_TeamChat_BB_07EA {
struct S_TeamMemberList_BB_09EA {
le_uint32_t entry_count = 0;
struct Entry {
// This is displayed as "<%04d> %s" % (value, message)
le_uint32_t index = 0;
// This is displayed as "<%04d> %s" % (rank, name)
le_uint32_t rank = 0;
le_uint32_t privilege_level = 0; // 0x10 or 0x20 = green, 0x30 = blue, 0x40 = red, anything else = white
le_uint32_t guild_card_number = 0;
pstring<TextEncoding::UTF16, 0x10> name;
@@ -3281,18 +3308,19 @@ struct S_TeamMemberList_BB_09EA {
} __packed__;
// 0CEA (S->C): Unknown
// The client appears to ignore this command.
struct S_Unknown_BB_0CEA {
parray<uint8_t, 0x20> unknown_a1;
// Text follows here
} __packed__;
// 0DEA (C->S): Unknown
// 0DEA (C->S): Get team name
// No arguments
// 0EEA (S->C): Unknown
// 0EEA (S->C): Team name
struct S_Unknown_BB_0EEA {
struct S_TeamName_BB_0EEA {
parray<uint8_t, 0x10> unused;
pstring<TextEncoding::UTF16, 0x10> team_name;
} __packed__;
@@ -3309,8 +3337,10 @@ struct C_SetTeamFlag_BB_0FEA {
// 11EA: Change team member privilege level
// The format below is used only when the client sends this command; when the
// server sends it, only header.flag is used.
// server sends it, only header.flag is used. As with various other team
// commands, header.flag specifies the error code in this case.
// header.flag specifies the new privilege level for the specified team member.
// Known values: 0 = normal, 0x30 = leader, 0x40 = master
struct C_ChangeTeamMemberPrivilegeLevel_BB_11EA {
le_uint32_t guild_card_number = 0;
@@ -3336,6 +3366,8 @@ struct S_TeamMembershipInformation_BB_12EA {
// header.flag specifies the number of entries.
struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry {
// The client uses the first four of these to determine if the player is in a
// team or not - if they are all zero, the player is not in a team.
le_uint32_t guild_card_number = 0;
le_uint32_t team_id = 0;
le_uint32_t unknown_a3 = 0;
@@ -3348,20 +3380,21 @@ struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry {
parray<le_uint16_t, 0x20 * 0x20> flag_data;
} __packed__;
// 14EA (C->S): Unknown
// 14EA (C->S): Get team info for lobby players
// No arguments. Client always sends 1 in the header.flag field.
// 15EA (S->C): Unknown
// 15EA (S->C): Team info for lobby players
// header.flag specifies the number of entries. The entry format appears to be
// the same as for the 13EA command.
// 16EA (S->C): Unknown
// No arguments except header.flag.
// 16EA (S->C): Transfer item via Simple Mail result
// No arguments except header.flag, which is 0 if the transfer failed and
// nonzero if it succeeded.
// 18EA: Team ranking information
// 18EA: Intra-team ranking information
// No arguments (C->S)
struct S_TeamRankingInformation_BB_18EA {
struct S_IntraTeamRanking_BB_18EA {
/* 0000 */ le_uint32_t ranking_points = 0;
/* 0004 */ le_uint32_t unknown_a2 = 0;
/* 0008 */ le_uint32_t points_remaining = 0;
@@ -3381,13 +3414,7 @@ struct S_TeamRankingInformation_BB_18EA {
// 19EA: Team reward list
// No arguments (C->S)
struct S_TeamRewardList_BB_19EA {
le_uint32_t num_rewards_unlocked = 0;
} __packed__;
// 1AEA: Team rewards available for purchase
struct S_TeamRewardsAvailableForPurchase_BB_1AEA {
struct S_TeamRewardList_BB_19EA_1AEA {
le_uint32_t num_entries;
struct Entry {
/* 0000 */ pstring<TextEncoding::UTF16, 0x40> name;
@@ -3400,13 +3427,28 @@ struct S_TeamRewardsAvailableForPurchase_BB_1AEA {
// Entry entries[num_entries];
} __packed__;
// 1AEA: Team rewards available for purchase
// Same format as 19EA.
// 1BEA (C->S): Buy team reward
// No arguments except header.flag, which specifies a reward_id from a preceding
// 1AEA command.
// 1CEA: Ranking information
// 1CEA: Cross-team ranking information
// No arguments when sent by the client.
struct S_CrossTeamRanking_BB_1CEA {
le_uint32_t num_entries;
struct Entry {
/* 00 */ pstring<TextEncoding::UTF16, 0x10> team_name;
/* 20 */ le_uint32_t team_points = 0;
/* 24 */ le_uint32_t unknown_a1 = 0;
/* 28 */
} __packed__;
// Variable length field:
// Entry entries[num_entries];
} __packed__;
// 1DEA (S->C): Update team rewards bitmask
// header.flag specifies the new rewards bitmask.
@@ -3418,10 +3460,13 @@ struct C_Unknown_BB_1EEA {
} __packed__;
// 1FEA (S->C): Action result
// This command behaves exactly like 02EA.
// This command behaves exactly like 02EA. This command is presumably the
// response to whatever 1EEA does.
// 20EA: Unknown
// header.flag is used, but no other arguments
// header.flag is used, but no other arguments. When sent by the server,
// header.flag is an error code, similar to various other result commands in
// this section.
// EB (S->C): Add player to spectator team (Episode 3)
// Same format and usage as 65 and 68 commands, but sent to spectators in a
@@ -3910,9 +3955,9 @@ struct G_DestroyNPC_6x1C {
// 6x1F: Set player floor
struct G_SetPlayerArea_6x1F {
struct G_SetPlayerFloor_6x1F {
G_ClientIDHeader header;
le_uint32_t floor = 0;
le_int32_t floor = 0;
} __packed__;
// 6x20: Set position
@@ -3921,7 +3966,7 @@ struct G_SetPlayerArea_6x1F {
struct G_SetPosition_6x20 {
G_ClientIDHeader header;
le_uint32_t floor = 0;
le_int32_t floor = 0;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
@@ -3932,7 +3977,7 @@ struct G_SetPosition_6x20 {
struct G_InterLevelWarp_6x21 {
G_ClientIDHeader header;
le_uint32_t floor = 0;
le_int32_t floor = 0;
} __packed__;
// 6x22: Set player invisible
@@ -4138,9 +4183,9 @@ struct G_Unknown_6x3B {
struct G_StopAtPosition_6x3E {
G_ClientIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t unknown_a2 = 0;
le_uint16_t floor = 0;
le_uint16_t unknown_a3 = 0;
le_uint16_t angle = 0;
le_int16_t floor = 0;
le_int16_t room = 0;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
@@ -4152,8 +4197,8 @@ struct G_SetPosition_6x3F {
G_ClientIDHeader header;
le_uint16_t unknown_a1 = 0;
le_uint16_t angle = 0;
le_uint16_t floor = 0;
le_uint16_t room = 0;
le_int16_t floor = 0;
le_int16_t room = 0;
le_float x = 0.0f;
le_float y = 0.0f;
le_float z = 0.0f;
@@ -4612,7 +4657,7 @@ struct G_SyncFlagState_6x6E_Decompressed {
struct G_SetQuestFlags_6x6F {
G_UnusedHeader header;
parray<parray<uint8_t, 0x80>, 4> quest_flags_by_difficulty;
QuestFlags quest_flags;
} __packed__;
// 6x70: Sync player disp data and inventory (used while loading into game)
@@ -5257,6 +5302,12 @@ struct G_Unknown_6xB2 {
// 6xB3: Unknown (Xbox; voice chat)
struct G_Unknown_XB_6xB3 {
G_ClientIDHeader header;
le_uint32_t num_frames;
// (0x0A * num_frames) bytes of data follows here.
} __packed__;
// 6xB3: CARD battle server data request (Episode 3)
// CARD battle subcommands have multiple subsubcommands, which we name 6xBYxZZ,
@@ -5303,6 +5354,11 @@ struct G_CardServerDataCommandHeader {
// 6xB4: Unknown (Xbox; voice chat)
struct G_Unknown_XB_6xB4 {
G_ClientIDHeader header;
le_uint32_t unknown_a1;
} __packed__;
// 6xB4: CARD battle server response (Episode 3) - see 6xB3 above
// 6xB5: CARD battle client command (Episode 3) - see 6xB3 above
@@ -5473,11 +5529,11 @@ struct G_WordSelectDuringBattle_GC_Ep3_6xBD {
struct G_BankAction_BB_6xBD {
G_UnusedHeader header;
le_uint32_t item_id = 0; // 0xFFFFFFFF = meseta; anything else = item
le_uint32_t item_id = 0;
le_uint32_t meseta_amount = 0;
uint8_t action = 0; // 0 = deposit, 1 = take
uint8_t action = 0; // 0 = deposit, 1 = take, 3 = done (close bank window)
uint8_t item_amount = 0;
le_uint16_t unused2 = 0;
le_uint16_t item_index = 0; // 0xFFFF = meseta
} __packed__;
// 6xBE: Sound chat (Episode 3; not Trial Edition)
@@ -5597,13 +5653,13 @@ struct G_ItemRewardRequest_BB_6xCA {
ItemData item_data;
} __packed__;
// 6xCB: Request to transfer item (BB)
// 6xCB: Transfer item via mail message (BB)
struct G_ItemTransferRequest_BB_6xCB {
struct G_TransferItemViaMailMessage_BB_6xCB {
G_ClientIDHeader header;
le_uint32_t unknown_a1 = 0;
le_uint32_t unknown_a2 = 0;
le_uint32_t unknown_a3 = 0;
le_uint32_t item_id = 0;
le_uint32_t amount = 0;
le_uint32_t target_guild_card_number = 0;
} __packed__;
// 6xCC: Exchange item for team points (BB)
@@ -5701,10 +5757,10 @@ struct G_PaganiniPhotonDropExchange_BB_6xD7 {
struct G_AddSRankWeaponSpecial_BB_6xD8 {
G_ClientIDHeader header;
ItemData unknown_a1; // Only data1[0]-[2] are used
le_uint32_t unknown_a2 = 0;
le_uint32_t unknown_a3 = 0;
le_uint16_t unknown_a4 = 0;
le_uint16_t unknown_a5 = 0;
le_uint32_t item_id = 0;
le_uint32_t special_type = 0;
le_uint16_t success_function_id = 0;
le_uint16_t failure_function_id = 0;
} __packed__;
// 6xD9: Momoka item exchange (BB; handled by server)
+136 -181
View File
@@ -6,176 +6,107 @@
using namespace std;
CommonItemSet::Table::Table(
shared_ptr<const string> owned_data, const StringReader& r, bool is_big_endian, bool is_v3)
: owned_data(owned_data),
r(r),
is_big_endian(is_big_endian),
is_v3(is_v3) {
CommonItemSet::Table::Table(const StringReader& r, bool is_big_endian, bool is_v3) {
if (is_big_endian) {
const auto& be_offsets = r.pget<Offsets<true>>(r.pget_u32b(this->r.size() - 0x10));
this->offsets.base_weapon_type_prob_table_offset = be_offsets.base_weapon_type_prob_table_offset.load();
this->offsets.subtype_base_table_offset = be_offsets.subtype_base_table_offset.load();
this->offsets.subtype_area_length_table_offset = be_offsets.subtype_area_length_table_offset.load();
this->offsets.grind_prob_table_offset = be_offsets.grind_prob_table_offset.load();
this->offsets.armor_shield_type_index_prob_table_offset = be_offsets.armor_shield_type_index_prob_table_offset.load();
this->offsets.armor_slot_count_prob_table_offset = be_offsets.armor_slot_count_prob_table_offset.load();
this->offsets.enemy_meseta_ranges_offset = be_offsets.enemy_meseta_ranges_offset.load();
this->offsets.enemy_type_drop_probs_offset = be_offsets.enemy_type_drop_probs_offset.load();
this->offsets.enemy_item_classes_offset = be_offsets.enemy_item_classes_offset.load();
this->offsets.box_meseta_ranges_offset = be_offsets.box_meseta_ranges_offset.load();
this->offsets.bonus_value_prob_table_offset = be_offsets.bonus_value_prob_table_offset.load();
this->offsets.nonrare_bonus_prob_spec_offset = be_offsets.nonrare_bonus_prob_spec_offset.load();
this->offsets.bonus_type_prob_table_offset = be_offsets.bonus_type_prob_table_offset.load();
this->offsets.special_mult_offset = be_offsets.special_mult_offset.load();
this->offsets.special_percent_offset = be_offsets.special_percent_offset.load();
this->offsets.tool_class_prob_table_offset = be_offsets.tool_class_prob_table_offset.load();
this->offsets.technique_index_prob_table_offset = be_offsets.technique_index_prob_table_offset.load();
this->offsets.technique_level_ranges_offset = be_offsets.technique_level_ranges_offset.load();
this->offsets.armor_or_shield_type_bias = be_offsets.armor_or_shield_type_bias;
this->offsets.unit_max_stars_offset = be_offsets.unit_max_stars_offset.load();
this->offsets.box_item_class_prob_table_offset = be_offsets.box_item_class_prob_table_offset.load();
this->parse_itempt_t<true>(r, is_v3);
} else {
this->offsets = r.pget<Offsets<false>>(r.pget_u32l(this->r.size() - 0x10));
this->parse_itempt_t<false>(r, is_v3);
}
}
const parray<uint8_t, 0x0C>& CommonItemSet::Table::base_weapon_type_prob_table() const {
return this->r.pget<parray<uint8_t, 0x0C>>(this->offsets.base_weapon_type_prob_table_offset);
}
const parray<int8_t, 0x0C>& CommonItemSet::Table::subtype_base_table() const {
return this->r.pget<parray<int8_t, 0x0C>>(this->offsets.subtype_base_table_offset);
}
const parray<uint8_t, 0x0C>& CommonItemSet::Table::subtype_area_length_table() const {
return this->r.pget<parray<uint8_t, 0x0C>>(this->offsets.subtype_area_length_table_offset);
}
const parray<parray<uint8_t, 4>, 9>& CommonItemSet::Table::grind_prob_table() const {
return this->r.pget<parray<parray<uint8_t, 4>, 9>>(this->offsets.grind_prob_table_offset);
}
const parray<uint8_t, 0x05>& CommonItemSet::Table::armor_shield_type_index_prob_table() const {
return this->r.pget<parray<uint8_t, 0x05>>(this->offsets.armor_shield_type_index_prob_table_offset);
}
const parray<uint8_t, 0x05>& CommonItemSet::Table::armor_slot_count_prob_table() const {
return this->r.pget<parray<uint8_t, 0x05>>(this->offsets.armor_slot_count_prob_table_offset);
}
const parray<CommonItemSet::Table::Range<uint16_t>, 0x64>& CommonItemSet::Table::enemy_meseta_ranges() const {
if (!this->parsed_enemy_meseta_ranges_populated) {
if (this->is_big_endian) {
const auto& data = r.pget<parray<Range<be_uint16_t>, 0x64>>(this->offsets.enemy_meseta_ranges_offset);
for (size_t z = 0; z < data.size(); z++) {
this->parsed_enemy_meseta_ranges[z] = Range<uint16_t>{data[z].min, data[z].max};
}
} else {
const auto& data = r.pget<parray<Range<le_uint16_t>, 0x64>>(this->offsets.enemy_meseta_ranges_offset);
for (size_t z = 0; z < data.size(); z++) {
this->parsed_enemy_meseta_ranges[z] = Range<uint16_t>{data[z].min, data[z].max};
template <bool IsBigEndian>
void CommonItemSet::Table::parse_itempt_t(const StringReader& r, bool is_v3) {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
const auto& offsets = r.pget<Offsets<IsBigEndian>>(r.pget<U32T>(r.size() - 0x10));
this->base_weapon_type_prob_table = r.pget<parray<uint8_t, 0x0C>>(offsets.base_weapon_type_prob_table_offset);
this->subtype_base_table = r.pget<parray<int8_t, 0x0C>>(offsets.subtype_base_table_offset);
this->subtype_area_length_table = r.pget<parray<uint8_t, 0x0C>>(offsets.subtype_area_length_table_offset);
this->grind_prob_table = r.pget<parray<parray<uint8_t, 4>, 9>>(offsets.grind_prob_table_offset);
this->armor_shield_type_index_prob_table = r.pget<parray<uint8_t, 0x05>>(offsets.armor_shield_type_index_prob_table_offset);
this->armor_slot_count_prob_table = r.pget<parray<uint8_t, 0x05>>(offsets.armor_slot_count_prob_table_offset);
const auto& data = r.pget<parray<Range<U16T>, 0x64>>(offsets.enemy_meseta_ranges_offset);
for (size_t z = 0; z < data.size(); z++) {
this->enemy_meseta_ranges[z] = Range<uint16_t>{data[z].min, data[z].max};
}
this->enemy_type_drop_probs = r.pget<parray<uint8_t, 0x64>>(offsets.enemy_type_drop_probs_offset);
this->enemy_item_classes = r.pget<parray<uint8_t, 0x64>>(offsets.enemy_item_classes_offset);
{
const auto& data = r.pget<parray<Range<U16T>, 0x0A>>(offsets.box_meseta_ranges_offset);
for (size_t z = 0; z < data.size(); z++) {
this->box_meseta_ranges[z] = Range<uint16_t>{data[z].min, data[z].max};
}
}
this->has_rare_bonus_value_prob_table = is_v3;
if (!this->has_rare_bonus_value_prob_table) { // V2
const auto& data = r.pget<parray<parray<uint8_t, 5>, 0x17>>(offsets.bonus_value_prob_table_offset);
for (size_t z = 0; z < data.size(); z++) {
for (size_t x = 0; x < data[z].size(); x++) {
this->bonus_value_prob_table[z][x] = data[z][x];
}
}
this->parsed_enemy_meseta_ranges_populated = true;
}
return this->parsed_enemy_meseta_ranges;
}
const parray<uint8_t, 0x64>& CommonItemSet::Table::enemy_type_drop_probs() const {
return this->r.pget<parray<uint8_t, 0x64>>(this->offsets.enemy_type_drop_probs_offset);
}
const parray<uint8_t, 0x64>& CommonItemSet::Table::enemy_item_classes() const {
return this->r.pget<parray<uint8_t, 0x64>>(this->offsets.enemy_item_classes_offset);
}
const parray<CommonItemSet::Table::Range<uint16_t>, 0x0A>& CommonItemSet::Table::box_meseta_ranges() const {
if (!this->parsed_box_meseta_ranges_populated) {
if (this->is_big_endian) {
const auto& data = r.pget<parray<Range<be_uint16_t>, 0x0A>>(this->offsets.box_meseta_ranges_offset);
for (size_t z = 0; z < data.size(); z++) {
this->parsed_box_meseta_ranges[z] = Range<uint16_t>{data[z].min, data[z].max};
}
} else {
const auto& data = r.pget<parray<Range<le_uint16_t>, 0x0A>>(this->offsets.box_meseta_ranges_offset);
for (size_t z = 0; z < data.size(); z++) {
this->parsed_box_meseta_ranges[z] = Range<uint16_t>{data[z].min, data[z].max};
} else { // V3
const auto& data = r.pget<parray<parray<U16T, 6>, 0x17>>(offsets.bonus_value_prob_table_offset);
for (size_t z = 0; z < data.size(); z++) {
for (size_t x = 0; x < data[z].size(); x++) {
this->bonus_value_prob_table[z][x] = data[z][x];
}
}
this->parsed_box_meseta_ranges_populated = true;
}
return this->parsed_box_meseta_ranges;
}
bool CommonItemSet::Table::has_rare_bonus_value_prob_table() const {
return this->is_v3;
}
const parray<parray<uint16_t, 6>, 0x17>& CommonItemSet::Table::bonus_value_prob_table() const {
if (!this->parsed_bonus_value_prob_table_populated) {
if (!this->is_v3) { // V2
const auto& data = r.pget<parray<parray<uint8_t, 5>, 0x17>>(this->offsets.bonus_value_prob_table_offset);
for (size_t z = 0; z < data.size(); z++) {
for (size_t x = 0; x < data[z].size(); x++) {
this->parsed_bonus_value_prob_table[z][x] = data[z][x];
}
}
} else if (this->is_big_endian) { // BE V3
const auto& data = r.pget<parray<parray<be_uint16_t, 6>, 0x17>>(this->offsets.bonus_value_prob_table_offset);
for (size_t z = 0; z < data.size(); z++) {
for (size_t x = 0; x < data[z].size(); x++) {
this->parsed_bonus_value_prob_table[z][x] = data[z][x];
}
}
} else { // LE V3
const auto& data = r.pget<parray<parray<le_uint16_t, 6>, 0x17>>(this->offsets.bonus_value_prob_table_offset);
for (size_t z = 0; z < data.size(); z++) {
for (size_t x = 0; x < data[z].size(); x++) {
this->parsed_bonus_value_prob_table[z][x] = data[z][x];
}
this->nonrare_bonus_prob_spec = r.pget<parray<parray<uint8_t, 10>, 3>>(offsets.nonrare_bonus_prob_spec_offset);
this->bonus_type_prob_table = r.pget<parray<parray<uint8_t, 10>, 6>>(offsets.bonus_type_prob_table_offset);
this->special_mult = r.pget<parray<uint8_t, 0x0A>>(offsets.special_mult_offset);
this->special_percent = r.pget<parray<uint8_t, 0x0A>>(offsets.special_percent_offset);
{
const auto& data = r.pget<parray<parray<U16T, 0x0A>, 0x1C>>(offsets.tool_class_prob_table_offset);
for (size_t z = 0; z < data.size(); z++) {
for (size_t x = 0; x < data[z].size(); x++) {
this->tool_class_prob_table[z][x] = data[z][x];
}
}
this->parsed_bonus_value_prob_table_populated = true;
}
return this->parsed_bonus_value_prob_table;
this->technique_index_prob_table = r.pget<parray<parray<uint8_t, 0x0A>, 0x13>>(offsets.technique_index_prob_table_offset);
this->technique_level_ranges = r.pget<parray<parray<Range<uint8_t>, 0x0A>, 0x13>>(offsets.technique_level_ranges_offset);
this->armor_or_shield_type_bias = offsets.armor_or_shield_type_bias;
this->unit_max_stars_table = r.pget<parray<uint8_t, 0x0A>>(offsets.unit_max_stars_offset);
this->box_item_class_prob_table = r.pget<parray<parray<uint8_t, 10>, 7>>(offsets.box_item_class_prob_table_offset);
}
const parray<parray<uint8_t, 10>, 3>& CommonItemSet::Table::nonrare_bonus_prob_spec() const {
return this->r.pget<parray<parray<uint8_t, 10>, 3>>(this->offsets.nonrare_bonus_prob_spec_offset);
}
const parray<parray<uint8_t, 10>, 6>& CommonItemSet::Table::bonus_type_prob_table() const {
return this->r.pget<parray<parray<uint8_t, 10>, 6>>(this->offsets.bonus_type_prob_table_offset);
}
const parray<uint8_t, 0x0A>& CommonItemSet::Table::special_mult() const {
return this->r.pget<parray<uint8_t, 0x0A>>(this->offsets.special_mult_offset);
}
const parray<uint8_t, 0x0A>& CommonItemSet::Table::special_percent() const {
return this->r.pget<parray<uint8_t, 0x0A>>(this->offsets.special_percent_offset);
}
const parray<parray<uint16_t, 0x0A>, 0x1C>& CommonItemSet::Table::tool_class_prob_table() const {
if (!this->parsed_tool_class_prob_table_populated) {
if (this->is_big_endian) {
const auto& data = r.pget<parray<parray<be_uint16_t, 0x0A>, 0x1C>>(this->offsets.tool_class_prob_table_offset);
for (size_t z = 0; z < data.size(); z++) {
for (size_t x = 0; x < data[z].size(); x++) {
this->parsed_tool_class_prob_table[z][x] = data[z][x];
}
}
} else {
const auto& data = r.pget<parray<parray<le_uint16_t, 0x0A>, 0x1C>>(this->offsets.tool_class_prob_table_offset);
for (size_t z = 0; z < data.size(); z++) {
for (size_t x = 0; x < data[z].size(); x++) {
this->parsed_tool_class_prob_table[z][x] = data[z][x];
}
}
void CommonItemSet::Table::print_enemy_table(FILE* stream) const {
const auto& meseta_ranges = this->enemy_meseta_ranges;
const auto& drop_probs = this->enemy_type_drop_probs;
const auto& item_classes = this->enemy_item_classes;
// const parray<Range<uint16_t>, 0x64>& enemy_meseta_ranges() const;
// const parray<uint8_t, 0x64>& enemy_type_drop_probs() const;
// const parray<uint8_t, 0x64>& enemy_item_classes() const;
fprintf(stream, "## $_LOW $_HIGH DAR ITEM\n");
for (size_t z = 0; z < 0x64; z++) {
const char* item_class_name = "__UNKNOWN__";
switch (item_classes[z]) {
case 0x00:
item_class_name = "WEAPON";
break;
case 0x01:
item_class_name = "ARMOR";
break;
case 0x02:
item_class_name = "SHIELD";
break;
case 0x03:
item_class_name = "UNIT";
break;
case 0x04:
item_class_name = "TOOL";
break;
case 0x05:
item_class_name = "MESETA";
break;
}
this->parsed_tool_class_prob_table_populated = true;
fprintf(stream, "%02zX %5hu %5hu %3hhu %02hX (%s)\n",
z, meseta_ranges[z].min, meseta_ranges[z].max, drop_probs[z], item_classes[z], item_class_name);
}
return this->parsed_tool_class_prob_table;
}
const parray<parray<uint8_t, 0x0A>, 0x13>& CommonItemSet::Table::technique_index_prob_table() const {
return this->r.pget<parray<parray<uint8_t, 0x0A>, 0x13>>(this->offsets.technique_index_prob_table_offset);
}
const parray<parray<CommonItemSet::Table::Range<uint8_t>, 0x0A>, 0x13>& CommonItemSet::Table::technique_level_ranges() const {
return this->r.pget<parray<parray<Range<uint8_t>, 0x0A>, 0x13>>(this->offsets.technique_level_ranges_offset);
}
uint8_t CommonItemSet::Table::armor_or_shield_type_bias() const {
return this->offsets.armor_or_shield_type_bias;
}
const parray<uint8_t, 0x0A>& CommonItemSet::Table::unit_max_stars_table() const {
return this->r.pget<parray<uint8_t, 0x0A>>(this->offsets.unit_max_stars_offset);
}
const parray<parray<uint8_t, 10>, 7>& CommonItemSet::Table::box_item_class_prob_table() const {
return this->r.pget<parray<parray<uint8_t, 10>, 7>>(this->offsets.box_item_class_prob_table_offset);
}
uint16_t CommonItemSet::key_for_table(Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) {
@@ -205,7 +136,7 @@ AFSV2CommonItemSet::AFSV2CommonItemSet(
for (size_t section_id = 0; section_id < 10; section_id++) {
auto entry = pt_afs.get(difficulty * 10 + section_id);
StringReader r(entry.first, entry.second);
shared_ptr<Table> table(new Table(pt_afs_data, r, false, false));
auto table = make_shared<Table>(r, false, false);
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::NORMAL, difficulty, section_id), table);
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::BATTLE, difficulty, section_id), table);
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::SOLO, difficulty, section_id), table);
@@ -217,48 +148,72 @@ AFSV2CommonItemSet::AFSV2CommonItemSet(
AFSArchive ct_afs(ct_afs_data);
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
auto r = ct_afs.get_reader(difficulty * 10);
shared_ptr<Table> table(new Table(ct_afs_data, r, false, false));
auto table = make_shared<Table>(r, false, false);
for (size_t section_id = 0; section_id < 10; section_id++) {
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::CHALLENGE, difficulty, section_id), table);
}
}
}
GSLV3CommonItemSet::GSLV3CommonItemSet(std::shared_ptr<const std::string> gsl_data, bool is_big_endian) {
GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gsl_data, bool is_big_endian) {
GSLArchive gsl(gsl_data, is_big_endian);
vector<Episode> episodes = {Episode::EP1, Episode::EP2};
auto filename_for_table = +[](Episode episode, uint8_t difficulty, uint8_t section_id, bool is_challenge) -> string {
const char* episode_token = "";
switch (episode) {
case Episode::EP1:
episode_token = "";
break;
case Episode::EP2:
episode_token = "l";
break;
case Episode::EP4:
episode_token = "s";
break;
default:
throw runtime_error("invalid episode");
}
return string_printf(
"ItemPT%s%s%c%1hhu.rel",
is_challenge ? "c" : "",
episode_token,
tolower(abbreviation_for_difficulty(difficulty)),
section_id);
};
vector<Episode> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
for (size_t section_id = 0; section_id < 10; section_id++) {
string filename = string_printf(
"ItemPT%s%c%1zu.rel",
((episode == Episode::EP2) ? "l" : ""),
tolower(abbreviation_for_difficulty(difficulty)),
section_id);
auto r = gsl.get_reader(filename);
shared_ptr<Table> table(new Table(gsl_data, r, is_big_endian, true));
StringReader r;
try {
r = gsl.get_reader(filename_for_table(episode, difficulty, section_id, false));
} catch (const exception&) {
// Fall back to Episode 1 if Episode 4 data is missing
if (episode == Episode::EP4) {
auto ep1_table = this->tables.at(this->key_for_table(Episode::EP1, GameMode::NORMAL, difficulty, section_id));
this->tables.emplace(this->key_for_table(episode, GameMode::NORMAL, difficulty, section_id), ep1_table);
this->tables.emplace(this->key_for_table(episode, GameMode::BATTLE, difficulty, section_id), ep1_table);
this->tables.emplace(this->key_for_table(episode, GameMode::SOLO, difficulty, section_id), ep1_table);
continue;
} else {
throw;
}
}
auto table = make_shared<Table>(r, is_big_endian, true);
this->tables.emplace(this->key_for_table(episode, GameMode::NORMAL, difficulty, section_id), table);
this->tables.emplace(this->key_for_table(episode, GameMode::BATTLE, difficulty, section_id), table);
this->tables.emplace(this->key_for_table(episode, GameMode::SOLO, difficulty, section_id), table);
// TODO: These tables don't exist for Episode 4, and the GC version is
// the closest we have, so we use the Ep1 data from GC for Ep4.
if (episode == Episode::EP1) {
this->tables.emplace(this->key_for_table(Episode::EP4, GameMode::NORMAL, difficulty, section_id), table);
this->tables.emplace(this->key_for_table(Episode::EP4, GameMode::BATTLE, difficulty, section_id), table);
this->tables.emplace(this->key_for_table(Episode::EP4, GameMode::SOLO, difficulty, section_id), table);
}
}
}
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
string filename = string_printf(
"ItemPTc%s%c0.rel",
((episode == Episode::EP2) ? "l" : ""),
tolower(abbreviation_for_difficulty(difficulty)));
auto r = gsl.get_reader(filename);
shared_ptr<Table> table(new Table(gsl_data, r, is_big_endian, true));
for (size_t section_id = 0; section_id < 10; section_id++) {
this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table);
if (episode != Episode::EP4) {
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
auto table = make_shared<Table>(r, is_big_endian, true);
for (size_t section_id = 0; section_id < 10; section_id++) {
this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table);
}
}
}
}
+30 -41
View File
@@ -13,7 +13,7 @@ public:
class Table {
public:
Table() = delete;
Table(std::shared_ptr<const std::string> owned_data, const StringReader& r, bool big_endian, bool is_v3);
Table(const StringReader& r, bool big_endian, bool is_v3);
template <typename IntT>
struct Range {
@@ -21,30 +21,35 @@ public:
IntT max;
} __attribute__((packed));
const parray<uint8_t, 0x0C>& base_weapon_type_prob_table() const;
const parray<int8_t, 0x0C>& subtype_base_table() const;
const parray<uint8_t, 0x0C>& subtype_area_length_table() const;
const parray<parray<uint8_t, 4>, 9>& grind_prob_table() const;
const parray<uint8_t, 0x05>& armor_shield_type_index_prob_table() const;
const parray<uint8_t, 0x05>& armor_slot_count_prob_table() const;
const parray<Range<uint16_t>, 0x64>& enemy_meseta_ranges() const;
const parray<uint8_t, 0x64>& enemy_type_drop_probs() const;
const parray<uint8_t, 0x64>& enemy_item_classes() const;
const parray<Range<uint16_t>, 0x0A>& box_meseta_ranges() const;
bool has_rare_bonus_value_prob_table() const;
const parray<parray<uint16_t, 6>, 0x17>& bonus_value_prob_table() const;
const parray<parray<uint8_t, 10>, 3>& nonrare_bonus_prob_spec() const;
const parray<parray<uint8_t, 10>, 6>& bonus_type_prob_table() const;
const parray<uint8_t, 0x0A>& special_mult() const;
const parray<uint8_t, 0x0A>& special_percent() const;
const parray<parray<uint16_t, 0x0A>, 0x1C>& tool_class_prob_table() const;
const parray<parray<uint8_t, 0x0A>, 0x13>& technique_index_prob_table() const;
const parray<parray<Range<uint8_t>, 0x0A>, 0x13>& technique_level_ranges() const;
uint8_t armor_or_shield_type_bias() const;
const parray<uint8_t, 0x0A>& unit_max_stars_table() const;
const parray<parray<uint8_t, 10>, 7>& box_item_class_prob_table() const;
parray<uint8_t, 0x0C> base_weapon_type_prob_table;
parray<int8_t, 0x0C> subtype_base_table;
parray<uint8_t, 0x0C> subtype_area_length_table;
parray<parray<uint8_t, 4>, 9> grind_prob_table;
parray<uint8_t, 0x05> armor_shield_type_index_prob_table;
parray<uint8_t, 0x05> armor_slot_count_prob_table;
parray<Range<uint16_t>, 0x64> enemy_meseta_ranges;
parray<uint8_t, 0x64> enemy_type_drop_probs;
parray<uint8_t, 0x64> enemy_item_classes;
parray<Range<uint16_t>, 0x0A> box_meseta_ranges;
bool has_rare_bonus_value_prob_table;
parray<parray<uint16_t, 6>, 0x17> bonus_value_prob_table;
parray<parray<uint8_t, 10>, 3> nonrare_bonus_prob_spec;
parray<parray<uint8_t, 10>, 6> bonus_type_prob_table;
parray<uint8_t, 0x0A> special_mult;
parray<uint8_t, 0x0A> special_percent;
parray<parray<uint16_t, 0x0A>, 0x1C> tool_class_prob_table;
parray<parray<uint8_t, 0x0A>, 0x13> technique_index_prob_table;
parray<parray<Range<uint8_t>, 0x0A>, 0x13> technique_level_ranges;
uint8_t armor_or_shield_type_bias;
parray<uint8_t, 0x0A> unit_max_stars_table;
parray<parray<uint8_t, 10>, 7> box_item_class_prob_table;
void print_enemy_table(FILE* stream) const;
private:
template <bool IsBigEndian>
void parse_itempt_t(const StringReader& r, bool is_v3);
template <bool IsBigEndian>
struct Offsets {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
@@ -248,22 +253,6 @@ public:
// There are several unused fields here.
} __attribute__((packed));
std::shared_ptr<const std::string> owned_data;
StringReader r;
bool is_big_endian;
bool is_v3;
Offsets<false> offsets;
mutable parray<Range<uint16_t>, 0x64> parsed_enemy_meseta_ranges;
mutable bool parsed_enemy_meseta_ranges_populated = false;
mutable parray<Range<uint16_t>, 0x0A> parsed_box_meseta_ranges;
mutable bool parsed_box_meseta_ranges_populated = false;
mutable parray<parray<uint16_t, 6>, 0x17> parsed_bonus_value_prob_table;
mutable bool parsed_bonus_value_prob_table_populated = false;
mutable parray<parray<uint16_t, 0x0A>, 0x1C> parsed_tool_class_prob_table;
mutable bool parsed_tool_class_prob_table_populated = false;
};
std::shared_ptr<const Table> get_table(Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) const;
@@ -281,9 +270,9 @@ public:
AFSV2CommonItemSet(std::shared_ptr<const std::string> pt_afs_data, std::shared_ptr<const std::string> ct_afs_data);
};
class GSLV3CommonItemSet : public CommonItemSet {
class GSLV3V4CommonItemSet : public CommonItemSet {
public:
GSLV3CommonItemSet(std::shared_ptr<const std::string> gsl_data, bool is_big_endian);
GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gsl_data, bool is_big_endian);
};
// Note: There are clearly better ways of doing this, but this implementation
+1 -2
View File
@@ -31,7 +31,6 @@ private:
uint32_t local_connect_address;
uint32_t external_connect_address;
static void dispatch_on_receive_message(evutil_socket_t fd, short events,
void* ctx);
static void dispatch_on_receive_message(evutil_socket_t fd, short events, void* ctx);
void on_receive_message(int fd, short event);
};
+29 -23
View File
@@ -618,10 +618,10 @@ uint8_t battle_param_index_for_enemy_type(Episode episode, EnemyType enemy_type)
return 0x20;
case EnemyType::VOL_OPT_2:
return 0x25;
case EnemyType::POUILLY_SLIME:
return 0x2F;
case EnemyType::POFUILLY_SLIME:
return 0x30;
case EnemyType::POUILLY_SLIME:
return 0x2F;
case EnemyType::PAN_ARMS:
return 0x31;
case EnemyType::HIDOOM:
@@ -852,9 +852,9 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::AL_RAPPY:
return 0x06;
case EnemyType::ASTARK:
return 0x01;
return 0x41;
case EnemyType::BA_BOOTA:
return 0x0B;
return 0x4F;
case EnemyType::BARBA_RAY:
return 0x49;
case EnemyType::BARBAROUS_WOLF:
@@ -862,7 +862,7 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::BOOMA:
return 0x09;
case EnemyType::BOOTA:
return 0x09;
return 0x4D;
case EnemyType::BULCLAW:
return 0x28;
case EnemyType::CANADINE:
@@ -889,8 +889,9 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::DEL_LILY:
return 0x53;
case EnemyType::DEL_RAPPY:
return 0x57;
case EnemyType::DEL_RAPPY_ALT:
return 0x12;
return 0x58;
case EnemyType::DELBITER:
return 0x48;
case EnemyType::DELDEPTH:
@@ -904,9 +905,9 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::DOLMOLM:
return 0x40;
case EnemyType::DORPHON:
return 0x0C;
return 0x50;
case EnemyType::DORPHON_ECLAIR:
return 0x0D;
return 0x51;
case EnemyType::DRAGON:
return 0x2C;
case EnemyType::DUBCHIC:
@@ -932,15 +933,15 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::GILLCHIC:
return 0x32;
case EnemyType::GIRTABLULU:
return 0x06;
return 0x48;
case EnemyType::GOBOOMA:
return 0x0A;
case EnemyType::GOL_DRAGON:
return 0x4C;
case EnemyType::GORAN:
return 0x0E;
return 0x52;
case EnemyType::GORAN_DETONATOR:
return 0x0F;
return 0x53;
case EnemyType::GRASS_ASSASSIN:
return 0x0C;
case EnemyType::GUIL_SHARK:
@@ -956,7 +957,7 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::ILL_GILL:
return 0x52;
case EnemyType::KONDRIEU:
return 0x15;
return 0x5B;
case EnemyType::LA_DIMENIAN:
return 0x2A;
case EnemyType::LOVE_RAPPY:
@@ -972,9 +973,9 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::MERILTAS:
return 0x35;
case EnemyType::MERISSA_A:
return 0x04;
return 0x46;
case EnemyType::MERISSA_AA:
return 0x05;
return 0x47;
case EnemyType::MIGIUM:
return 0x16;
case EnemyType::MONEST:
@@ -994,8 +995,9 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::PAN_ARMS:
return 0x15;
case EnemyType::PAZUZU:
return 0x4B;
case EnemyType::PAZUZU_ALT:
return 0x08;
return 0x4C;
case EnemyType::POFUILLY_SLIME:
return 0x13;
case EnemyType::POUILLY_SLIME:
@@ -1003,7 +1005,7 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::POISON_LILY:
return 0x0D;
case EnemyType::PYRO_GORAN:
return 0x10;
return 0x54;
case EnemyType::RAG_RAPPY:
return 0x05;
case EnemyType::RECOBOX:
@@ -1013,17 +1015,19 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::SAINT_RAPPY:
return 0x4F;
case EnemyType::SAINT_MILLION:
return 0x13;
return 0x59;
case EnemyType::SAND_RAPPY:
return 0x55;
case EnemyType::SAND_RAPPY_ALT:
return 0x11;
return 0x56;
case EnemyType::SATELLITE_LIZARD:
return 0x44;
case EnemyType::SATELLITE_LIZARD_ALT:
return 0x03;
return 0x45;
case EnemyType::SAVAGE_WOLF:
return 0x07;
case EnemyType::SHAMBERTIN:
return 0x14;
return 0x5A;
case EnemyType::SINOW_BEAT:
return 0x1A;
case EnemyType::SINOW_BERILL:
@@ -1043,15 +1047,17 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) {
case EnemyType::VOL_OPT_2:
return 0x2E;
case EnemyType::YOWIE:
return 0x42;
case EnemyType::YOWIE_ALT:
return 0x02;
return 0x43;
case EnemyType::ZE_BOOTA:
return 0x0A;
return 0x4E;
case EnemyType::ZOL_GIBBON:
return 0x3C;
case EnemyType::ZU:
return 0x49;
case EnemyType::ZU_ALT:
return 0x07;
return 0x4A;
default:
throw runtime_error(string_printf("%s does not have a rare table entry", name_for_enum(enemy_type)));
}
+1 -1
View File
@@ -9,7 +9,7 @@
#include <string>
#include <variant>
#include "../Player.hh"
#include "../PlayerSubordinates.hh"
struct Lobby;
+8 -8
View File
@@ -2261,7 +2261,7 @@ CardIndex::CardIndex(
continue;
}
shared_ptr<CardEntry> entry(new CardEntry({defs[x], "", "", "", {}}));
auto entry = make_shared<CardEntry>(CardEntry{defs[x], "", "", "", {}});
if (!this->card_definitions.emplace(entry->def.card_id, entry).second) {
throw runtime_error(string_printf(
"duplicate card id: %08" PRIX32, entry->def.card_id.load()));
@@ -2391,12 +2391,12 @@ MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t lang
"decompressed data size is incorrect (expected %zu bytes, read %zu bytes)",
sizeof(MapDefinition), decompressed.size()));
}
this->map.reset(new MapDefinition(*reinterpret_cast<const MapDefinition*>(decompressed.data())));
this->map = make_shared<MapDefinition>(*reinterpret_cast<const MapDefinition*>(decompressed.data()));
}
shared_ptr<const MapDefinitionTrial> MapIndex::VersionedMap::trial() const {
if (!this->trial_map) {
this->trial_map.reset(new MapDefinitionTrial(*this->map));
this->trial_map = make_shared<MapDefinitionTrial>(*this->map);
}
return this->trial_map;
}
@@ -2465,7 +2465,7 @@ MapIndex::MapIndex(const string& directory) {
string compressed_data;
shared_ptr<MapDefinition> decompressed_data;
if (ends_with(filename, ".mnmd") || ends_with(filename, ".bind")) {
decompressed_data.reset(new MapDefinition(load_object_file<MapDefinition>(directory + "/" + filename)));
decompressed_data = make_shared<MapDefinition>(load_object_file<MapDefinition>(directory + "/" + filename));
base_filename = filename.substr(0, filename.size() - 5);
} else if (ends_with(filename, ".mnm") || ends_with(filename, ".bin")) {
compressed_data = load_file(directory + "/" + filename);
@@ -2502,9 +2502,9 @@ MapIndex::MapIndex(const string& directory) {
shared_ptr<VersionedMap> vm;
if (decompressed_data) {
vm.reset(new VersionedMap(decompressed_data, language));
vm = make_shared<VersionedMap>(decompressed_data, language);
} else if (!compressed_data.empty()) {
vm.reset(new VersionedMap(std::move(compressed_data), language));
vm = make_shared<VersionedMap>(std::move(compressed_data), language);
} else {
throw runtime_error("unknown map file format");
}
@@ -2512,7 +2512,7 @@ MapIndex::MapIndex(const string& directory) {
string name = vm->map->name.decode(vm->language);
auto map_it = this->maps.find(vm->map->map_number);
if (map_it == this->maps.end()) {
map_it = this->maps.emplace(vm->map->map_number, new Map(vm)).first;
map_it = this->maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first;
static_game_data_log.info("(%s) Created Episode 3 map %08" PRIX32 " %c (%s; %s)",
filename.c_str(),
vm->map->map_number.load(),
@@ -2640,7 +2640,7 @@ COMDeckIndex::COMDeckIndex(const string& filename) {
try {
auto json = JSON::parse(load_file(filename));
for (const auto& def_json : json.as_list()) {
auto& def = this->decks.emplace_back(new COMDeckDefinition());
auto& def = this->decks.emplace_back(make_shared<COMDeckDefinition>());
def->index = this->decks.size() - 1;
def->player_name = def_json->at(0).as_string();
def->deck_name = def_json->at(1).as_string();
+9 -22
View File
@@ -48,10 +48,7 @@ void PlayerState::init() {
throw logic_error("replacing a player state object is not permitted");
}
this->deck_state.reset(new DeckState(
this->client_id,
s->deck_entries[client_id]->card_ids,
s->options.random_crypt));
this->deck_state = make_shared<DeckState>(this->client_id, s->deck_entries[client_id]->card_ids, s->options.random_crypt);
if (s->map_and_rules->rules.disable_deck_shuffle) {
this->deck_state->disable_shuffle();
}
@@ -77,10 +74,10 @@ void PlayerState::init() {
throw runtime_error("SC card is not a Hunters or Arkz SC");
}
this->hand_and_equip.reset(new HandAndEquipState());
this->card_short_statuses.reset(new parray<CardShortStatus, 0x10>());
this->set_card_action_chains.reset(new parray<ActionChainWithConds, 9>());
this->set_card_action_metadatas.reset(new parray<ActionMetadata, 9>());
this->hand_and_equip = make_shared<HandAndEquipState>();
this->card_short_statuses = make_shared<parray<CardShortStatus, 0x10>>();
this->set_card_action_chains = make_shared<parray<ActionChainWithConds, 9>>();
this->set_card_action_metadatas = make_shared<parray<ActionMetadata, 9>>();
this->hand_and_equip->clear_FF();
for (size_t z = 0; z < 0x10; z++) {
@@ -91,11 +88,7 @@ void PlayerState::init() {
this->set_card_action_metadatas->at(z).clear_FF();
}
this->sc_card.reset(new Card(
this->deck_state->sc_card_id(),
this->sc_card_ref,
this->client_id,
s));
this->sc_card = make_shared<Card>(this->deck_state->sc_card_id(), this->sc_card_ref, this->client_id, s);
this->sc_card->init();
this->draw_initial_hand();
@@ -1232,8 +1225,7 @@ bool PlayerState::set_card_from_hand(
auto s = this->server();
if (!skip_error_checks_and_atk_sub) {
int32_t code = this->error_code_for_client_setting_card(
card_ref, card_index, loc, assist_target_client_id);
int32_t code = this->error_code_for_client_setting_card(card_ref, card_index, loc, assist_target_client_id);
if (code) {
s->ruler_server->error_code1 = code;
this->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
@@ -1248,8 +1240,7 @@ bool PlayerState::set_card_from_hand(
}
if (!skip_error_checks_and_atk_sub) {
int16_t cost = s->ruler_server->set_cost_for_card(
this->client_id, card_ref);
int16_t cost = s->ruler_server->set_cost_for_card(this->client_id, card_ref);
this->subtract_atk_points(cost);
}
@@ -1261,11 +1252,7 @@ bool PlayerState::set_card_from_hand(
return 0;
}
this->card_refs[card_index + 1] = card_ref;
this->set_cards[card_index - 7].reset(new Card(
s->card_id_for_card_ref(card_ref),
card_ref,
this->client_id,
s));
this->set_cards[card_index - 7] = make_shared<Card>(s->card_id_for_card_ref(card_ref), card_ref, this->client_id, s);
auto new_card = this->set_cards[card_index - 7];
new_card->init();
+8 -8
View File
@@ -75,32 +75,32 @@ Server::~Server() noexcept(false) {
}
void Server::init() {
this->map_and_rules.reset(new MapAndRulesState());
this->map_and_rules = make_shared<MapAndRulesState>();
this->num_clients_present = 0;
this->overlay_state.clear();
for (size_t z = 0; z < 4; z++) {
this->presence_entries[z].clear();
this->deck_entries[z].reset(new DeckEntry());
this->deck_entries[z] = make_shared<DeckEntry>();
this->name_entries[z].clear();
this->name_entries_valid[z] = false;
}
this->card_special.reset(new CardSpecial(this->shared_from_this()));
this->card_special = make_shared<CardSpecial>(this->shared_from_this());
// Note: The original implementation calls the default PSOV2Encryption
// constructor for random_crypt, which just uses 0 as the seed. It then
// re-seeds the generator later. We instead expect the caller to provide a
// seeded generator, and we don't re-seed it at all.
// this->random_crypt.reset(new PSOV2Encryption(0));
// this->random_crypt = make_shared<PSOV2Encryption>(0);
this->state_flags.reset(new StateFlags());
this->state_flags = make_shared<StateFlags>();
this->clear_player_flags_after_dice_phase();
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
this->assist_server.reset(new AssistServer(this->shared_from_this()));
this->ruler_server.reset(new RulerServer(this->shared_from_this()));
this->assist_server = make_shared<AssistServer>(this->shared_from_this());
this->ruler_server = make_shared<RulerServer>(this->shared_from_this());
this->ruler_server->link_objects(this->map_and_rules, this->state_flags, this->assist_server);
this->send_6xB4x46();
@@ -1403,7 +1403,7 @@ void Server::setup_and_start_battle() {
if (!this->check_presence_entry(z)) {
this->name_entries[z].clear();
} else {
this->player_states[z].reset(new PlayerState(z, this->shared_from_this()));
this->player_states[z] = make_shared<PlayerState>(z, this->shared_from_this());
this->player_states[z]->init();
}
}
+5 -7
View File
@@ -16,7 +16,7 @@ Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number, const string& playe
Tournament::PlayerEntry::PlayerEntry(shared_ptr<Client> c)
: serial_number(c->license->serial_number),
client(c),
player_name(c->game_data.character()->disp.name.decode(c->language())) {}
player_name(c->character()->disp.name.decode(c->language())) {}
Tournament::PlayerEntry::PlayerEntry(
shared_ptr<const COMDeckDefinition> com_deck)
@@ -364,10 +364,8 @@ void Tournament::init() {
is_registration_complete = this->source_json.get_bool("is_registration_complete");
for (const auto& team_json : this->source_json.get_list("teams")) {
auto& team = this->teams.emplace_back(new Team(
this->shared_from_this(),
this->teams.size(),
team_json->get_int("max_players")));
auto& team = this->teams.emplace_back(make_shared<Team>(
this->shared_from_this(), this->teams.size(), team_json->get_int("max_players")));
team->name = team_json->get_string("name");
team->password = team_json->get_string("password");
team_index_to_rounds_cleared.emplace_back(team_json->get_int("num_rounds_cleared"));
@@ -806,7 +804,7 @@ TournamentIndex::TournamentIndex(
}
for (size_t z = 0; z < min<size_t>(json.size(), 0x20); z++) {
if (!json.at(z).is_null()) {
shared_ptr<Tournament> tourn(new Tournament(this->map_index, this->com_deck_index, json.at(z)));
auto tourn = make_shared<Tournament>(this->map_index, this->com_deck_index, json.at(z));
tourn->init();
if (!this->name_to_tournament.emplace(tourn->get_name(), tourn).second) {
throw runtime_error("multiple tournaments have the same name: " + tourn->get_name());
@@ -820,7 +818,7 @@ TournamentIndex::TournamentIndex(
throw runtime_error("tournament JSON dict length is incorrect");
}
for (const auto& it : json.as_dict()) {
shared_ptr<Tournament> tourn(new Tournament(this->map_index, this->com_deck_index, *it.second));
auto tourn = make_shared<Tournament>(this->map_index, this->com_deck_index, *it.second);
tourn->init();
if (!this->name_to_tournament.emplace(tourn->get_name(), tourn).second) {
// This is logic_error instead of runtime_error because JSON dicts are
+2 -2
View File
@@ -10,10 +10,10 @@
#include <unordered_set>
#include <vector>
#include "../Player.hh"
#include "DataIndexes.hh"
struct Lobby;
struct Client;
class Client;
struct ServerState;
namespace Episode3 {
+2 -2
View File
@@ -14,7 +14,7 @@ FileContentsCache::File::File(
string&& data,
uint64_t load_time)
: name(name),
data(new string(std::move(data))),
data(make_shared<string>(std::move(data))),
load_time(load_time) {}
shared_ptr<const FileContentsCache::File> FileContentsCache::replace(
@@ -22,7 +22,7 @@ shared_ptr<const FileContentsCache::File> FileContentsCache::replace(
if (t == 0) {
t = now();
}
shared_ptr<File> new_file(new File(name, std::move(data), t));
auto new_file = make_shared<File>(name, std::move(data), t);
auto emplace_ret = this->name_to_file.emplace(name, new_file);
if (!emplace_ret.second) {
emplace_ret.first->second = new_file;
+4 -4
View File
@@ -127,7 +127,7 @@ shared_ptr<CompiledFunctionCode> compile_function_code(
throw runtime_error("function compiler is not available");
#else
shared_ptr<CompiledFunctionCode> ret(new CompiledFunctionCode());
auto ret = make_shared<CompiledFunctionCode>();
ret->arch = arch;
ret->name = name;
ret->index = 0;
@@ -236,7 +236,7 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
shared_ptr<const Menu> FunctionCodeIndex::patch_menu(uint32_t specific_version) const {
auto suffix = string_printf("-%08" PRIX32, specific_version);
shared_ptr<Menu> ret(new Menu(MenuID::PATCHES, "Patches"));
auto ret = make_shared<Menu>(MenuID::PATCHES, "Patches");
ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0);
for (const auto& it : this->name_and_specific_version_to_patch_function) {
const auto& fn = it.second;
@@ -266,7 +266,7 @@ DOLFileIndex::DOLFileIndex(const string& directory) {
return;
}
shared_ptr<Menu> menu(new Menu(MenuID::PROGRAMS, "Programs"));
auto menu = make_shared<Menu>(MenuID::PROGRAMS, "Programs");
this->menu = menu;
menu->items.emplace_back(ProgramsMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0);
@@ -280,7 +280,7 @@ DOLFileIndex::DOLFileIndex(const string& directory) {
string name = filename.substr(0, filename.size() - (is_compressed_dol ? 8 : 4));
try {
shared_ptr<File> dol(new File());
auto dol = make_shared<File>();
dol->menu_item_id = next_menu_item_id++;
dol->name = name;
+137 -100
View File
@@ -13,120 +13,130 @@ static inline uint16_t collapse_checksum(uint32_t sum) {
return (sum & 0xFFFF) + (sum >> 16);
}
FrameInfo::FrameInfo()
: ether(nullptr),
ether_protocol(0),
ipv4(nullptr),
arp(nullptr),
udp(nullptr),
tcp(nullptr),
header_start(nullptr),
payload(nullptr),
total_size(0),
tcp_options_size(0),
payload_size(0) {}
FrameInfo::FrameInfo(LinkType link_type, const string& data)
: FrameInfo(link_type, data.data(), data.size()) {}
FrameInfo::FrameInfo(const string& data) : FrameInfo(data.data(), data.size()) {}
FrameInfo::FrameInfo(LinkType link_type, const void* header_start, size_t size)
: FrameInfo() {
this->link_type = link_type;
this->header_start = header_start;
this->total_size = size;
this->payload_size = size;
FrameInfo::FrameInfo(const void* header_start, size_t size)
: ether(nullptr),
ether_protocol(0),
ipv4(nullptr),
arp(nullptr),
udp(nullptr),
tcp(nullptr),
header_start(header_start),
payload(nullptr),
total_size(size),
tcp_options_size(0),
payload_size(size) {
StringReader r(header_start, size);
// Parse ethernet header
if (this->payload_size < sizeof(EthernetHeader)) {
throw invalid_argument("frame is too small for ethernet");
}
this->payload_size -= sizeof(EthernetHeader);
this->ether = reinterpret_cast<const EthernetHeader*>(header_start);
this->ether_protocol = this->ether->protocol;
// Parse link-layer header
Protocol proto = Protocol::NONE;
switch (this->link_type) {
case LinkType::ETHERNET:
this->payload_size -= sizeof(EthernetHeader);
this->ether = &r.get<EthernetHeader>();
this->ether_protocol = this->ether->protocol;
// Unwrap VLAN tags if necessary
while ((this->ether_protocol == 0x8100) || (this->ether_protocol == 0x88A8)) {
r.skip(2);
this->ether_protocol = r.get_u16b();
this->payload_size -= 4;
}
switch (this->ether_protocol) {
case 0x0800:
proto = Protocol::IPV4;
break;
case 0x0806:
proto = Protocol::ARP;
break;
}
break;
// Figure out the protocol
const be_uint16_t* u16data = reinterpret_cast<const be_uint16_t*>(this->ether + 1);
while ((this->ether_protocol == 0x8100) || (this->ether_protocol == 0x88A8)) {
if (this->payload_size < 4) {
throw invalid_argument("VLAN tags exceed frame size");
}
this->ether_protocol = u16data[1];
u16data += 2;
this->payload_size -= 4;
case LinkType::HDLC:
this->payload_size -= (sizeof(HDLCHeader) + 3); // Trim off checksum and end sentinel
this->hdlc = &r.get<HDLCHeader>();
this->hdlc_checksum = r.pget_u16b(r.where() + this->payload_size);
switch (this->hdlc->protocol) {
case 0xC021:
proto = Protocol::LCP;
break;
case 0xC023:
proto = Protocol::PAP;
break;
case 0x8021:
proto = Protocol::IPCP;
break;
case 0x0021:
proto = Protocol::IPV4;
break;
}
break;
default:
throw logic_error("invalid link type");
}
// TODO: Some less-common protocols that we might want to support:
// 0x8035 = RARP
// 0x809B = AppleTalk
// 0x80F3 = AppleTalk ARP
// 0x8137 = IPX
// 0x9000 = loopback
// Parse protocol headers if possible
if (this->ether_protocol == 0x0800) { // IPv4
if (this->payload_size < sizeof(IPv4Header)) {
throw invalid_argument("frame is too small for ipv4 header");
}
this->ipv4 = reinterpret_cast<const IPv4Header*>(u16data);
if (this->payload_size < this->ipv4->size) {
throw invalid_argument("ipv4 header specifies size larger than frame");
}
this->payload_size = this->ipv4->size - sizeof(IPv4Header);
if (this->ipv4->protocol == 0x06) {
if (this->payload_size < sizeof(TCPHeader)) {
throw invalid_argument("frame is too small for tcp4 header");
// Parse inner protocol headers
switch (proto) {
case Protocol::NONE:
throw runtime_error("unknown protocol");
case Protocol::LCP:
this->payload_size -= sizeof(LCPHeader);
this->lcp = &r.get<LCPHeader>();
break;
case Protocol::PAP:
this->payload_size -= sizeof(PAPHeader);
this->pap = &r.get<PAPHeader>();
break;
case Protocol::IPCP:
this->payload_size -= sizeof(IPCPHeader);
this->ipcp = &r.get<IPCPHeader>();
break;
case Protocol::IPV4:
this->ipv4 = &r.get<IPv4Header>();
if (this->payload_size < this->ipv4->size) {
throw invalid_argument("ipv4 header specifies size larger than frame");
}
this->tcp = reinterpret_cast<const TCPHeader*>(this->ipv4 + 1);
size_t tcp_header_size = (this->tcp->flags >> 12) * 4;
if (tcp_header_size < sizeof(TCPHeader) || tcp_header_size > this->payload_size) {
throw invalid_argument("frame is too small for tcp4 header with options");
this->payload_size = this->ipv4->size - sizeof(IPv4Header);
if (this->ipv4->protocol == 0x06) {
this->tcp = &r.get<TCPHeader>();
size_t tcp_header_size = (this->tcp->flags >> 12) * 4;
if (tcp_header_size < sizeof(TCPHeader) || tcp_header_size > this->payload_size) {
throw invalid_argument("frame is too small for tcp4 header with options");
}
this->tcp_options_size = tcp_header_size - sizeof(TCPHeader);
this->payload_size -= tcp_header_size;
r.skip(tcp_header_size - sizeof(TCPHeader));
} else if (this->ipv4->protocol == 0x11) {
this->payload_size -= sizeof(UDPHeader);
this->udp = &r.get<UDPHeader>();
}
this->tcp_options_size = tcp_header_size - sizeof(TCPHeader);
this->payload_size -= tcp_header_size;
this->payload = reinterpret_cast<const uint8_t*>(this->tcp) + tcp_header_size;
} else if (this->ipv4->protocol == 0x11) {
if (this->payload_size < sizeof(UDPHeader)) {
throw invalid_argument("frame is too small for udp4 header");
}
this->payload_size -= sizeof(UDPHeader);
this->udp = reinterpret_cast<const UDPHeader*>(this->ipv4 + 1);
this->payload = this->udp + 1;
} else {
this->payload = this->ipv4 + 1;
}
} else if (this->ether_protocol == 0x0806) { // ARP
if (this->payload_size < sizeof(const ARPHeader)) {
throw invalid_argument("frame is too small for arp header");
}
this->payload_size -= sizeof(ARPHeader);
this->arp = reinterpret_cast<const ARPHeader*>(u16data);
this->payload = this->arp + 1;
} else {
throw runtime_error("unknown protocol");
break;
case Protocol::ARP:
this->payload_size -= sizeof(ARPHeader);
this->arp = &r.get<ARPHeader>();
break;
}
this->payload = r.getv(this->payload_size);
}
string FrameInfo::header_str() const {
if (!this->ether) {
if (!this->ether && !this->hdlc) {
return "<invalid-frame-info>";
}
string ret = string_printf(
"%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX->%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX",
this->ether->src_mac[0], this->ether->src_mac[1], this->ether->src_mac[2],
this->ether->src_mac[3], this->ether->src_mac[4], this->ether->src_mac[5],
this->ether->dest_mac[0], this->ether->dest_mac[1], this->ether->dest_mac[2],
this->ether->dest_mac[3], this->ether->dest_mac[4], this->ether->dest_mac[5]);
string ret;
if (this->ether) {
ret = string_printf(
"ETHER:%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX->%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX",
this->ether->src_mac[0], this->ether->src_mac[1], this->ether->src_mac[2],
this->ether->src_mac[3], this->ether->src_mac[4], this->ether->src_mac[5],
this->ether->dest_mac[0], this->ether->dest_mac[1], this->ether->dest_mac[2],
this->ether->dest_mac[3], this->ether->dest_mac[4], this->ether->dest_mac[5]);
} else if (this->hdlc) {
ret = string_printf("HDLC:%02hhX/%02hhX", this->hdlc->address, this->hdlc->control);
} else {
return "<invalid-frame-info>";
}
if (this->arp) {
ret += string_printf(
@@ -169,7 +179,11 @@ string FrameInfo::header_str() const {
}
} else {
ret += string_printf(",proto=%04hX", this->ether->protocol.load());
if (this->ether) {
ret += string_printf(",proto=%04hX", this->ether->protocol.load());
} else if (this->hdlc) {
ret += string_printf(",proto=%04hX", this->hdlc->protocol.load());
}
}
return ret;
@@ -292,3 +306,26 @@ uint16_t FrameInfo::computed_tcp4_checksum() const {
*this->ipv4, *this->tcp, this->tcp + 1,
this->payload_size + this->tcp_options_size);
}
uint16_t FrameInfo::computed_hdlc_checksum(const void* vdata, size_t size) {
const uint8_t* data = reinterpret_cast<const uint8_t*>(vdata);
uint16_t crc = 0xFFFF;
for (size_t z = 0; z < size; z++) {
crc ^= data[z];
for (size_t b = 0; b < 8; b++) {
crc = (crc & 1) ? ((crc >> 1) ^ 0x8408) : (crc >> 1);
}
}
return ~crc;
}
uint16_t FrameInfo::computed_hdlc_checksum() const {
if (!this->hdlc) {
throw logic_error("cannot compute HDLC checksum for non-HDLC frame");
}
return this->computed_hdlc_checksum(&this->hdlc->address, this->total_size - 4);
}
uint16_t FrameInfo::stored_hdlc_checksum() const {
return *reinterpret_cast<const le_uint16_t*>(reinterpret_cast<const uint8_t*>(this->header_start) + (this->total_size - 3));
}
+80 -19
View File
@@ -3,9 +3,35 @@
#include <stdint.h>
#include <phosg/Encoding.hh>
#include <phosg/Strings.hh>
#include "Text.hh"
struct HDLCHeader {
uint8_t start_sentinel1; // 0x7E
uint8_t address; // 0xFF usually
uint8_t control; // 0x03 for PPP
be_uint16_t protocol;
} __attribute__((packed));
struct LCPHeader {
uint8_t command;
uint8_t request_id;
be_uint16_t size;
} __attribute__((packed));
struct PAPHeader {
uint8_t command;
uint8_t request_id;
be_uint16_t size;
} __attribute__((packed));
struct IPCPHeader {
uint8_t command;
uint8_t request_id;
be_uint16_t size;
} __attribute__((packed));
struct EthernetHeader {
parray<uint8_t, 6> dest_mac;
parray<uint8_t, 6> src_mac;
@@ -82,40 +108,75 @@ struct DHCPHeader {
} __attribute__((packed));
struct FrameInfo {
// This is always valid
const EthernetHeader* ether;
uint16_t ether_protocol;
enum class LinkType {
ETHERNET = 0,
HDLC,
};
enum class Protocol {
NONE = 0,
LCP,
PAP,
IPCP,
IPV4,
ARP,
// TODO: Some less-common protocols that we might want to support:
// Ether / HDLC = proto
// 0x8035 / ?????? = RARP
// 0x809B / 0x0029 = AppleTalk
// 0x80F3 / ?????? = AppleTalk ARP
// 0x8137 / 0x002B = IPX
};
LinkType link_type = LinkType::ETHERNET;
// Exactly one of these headers is valid
const EthernetHeader* ether = nullptr;
uint16_t ether_protocol = 0;
const HDLCHeader* hdlc = nullptr;
uint16_t hdlc_checksum = 0;
// One of these may be non-null if hdlc is valid
const LCPHeader* lcp = nullptr;
const PAPHeader* pap = nullptr;
const IPCPHeader* ipcp = nullptr;
// At most one of these is not null
const IPv4Header* ipv4;
const ARPHeader* arp;
const IPv4Header* ipv4 = nullptr;
const ARPHeader* arp = nullptr;
// One of these may be not null if this->ipv4 is not null
const UDPHeader* udp;
const TCPHeader* tcp;
const UDPHeader* udp = nullptr;
const TCPHeader* tcp = nullptr;
const void* header_start;
const void* payload;
size_t total_size;
size_t tcp_options_size;
size_t payload_size;
const void* header_start = nullptr;
const void* payload = nullptr;
size_t total_size = 0;
size_t tcp_options_size = 0;
size_t payload_size = 0;
FrameInfo();
FrameInfo(const std::string& data);
FrameInfo(const void* data, size_t size);
FrameInfo() = default;
FrameInfo(LinkType link_type, const std::string& data);
FrameInfo(LinkType link_type, const void* data, size_t size);
std::string header_str() const;
inline StringReader read_payload() const {
return StringReader(this->payload, this->payload_size);
}
void truncate(size_t new_total_size);
size_t size_from_header() const;
static uint16_t computed_ipv4_header_checksum(const IPv4Header& ipv4);
uint16_t computed_ipv4_header_checksum() const;
static uint16_t computed_udp4_checksum(
const IPv4Header& ipv4, const UDPHeader& udp, const void* data, size_t size);
static uint16_t computed_udp4_checksum(const IPv4Header& ipv4, const UDPHeader& udp, const void* data, size_t size);
uint16_t computed_udp4_checksum() const;
static uint16_t computed_tcp4_checksum(
const IPv4Header& ip, const TCPHeader& tcp, const void* data, size_t size);
static uint16_t computed_tcp4_checksum(const IPv4Header& ip, const TCPHeader& tcp, const void* data, size_t size);
uint16_t computed_tcp4_checksum() const;
static uint16_t computed_hdlc_checksum(const void* data, size_t size);
uint16_t computed_hdlc_checksum() const;
uint16_t stored_hdlc_checksum() const;
};
+494 -115
View File
@@ -21,6 +21,65 @@ using namespace std;
static const size_t DEFAULT_RESEND_PUSH_USECS = 200000; // 200ms
static string unescape_hdlc_frame(const void* data, size_t size) {
StringReader r(data, size);
if (r.get_u8(data) != 0x7E) {
throw runtime_error("HDLC frame does not begin with 7E");
}
string ret("\x7E", 1);
while (r.get_u8(false) != 0x7E) {
uint8_t ch = r.get_u8();
if (ch == 0x7D) {
ch = r.get_u8();
if (ch == 0x7E) {
throw runtime_error("abort sequence received");
}
ret.push_back(ch ^ 0x20);
} else {
ret.push_back(ch);
}
}
ret.push_back(0x7E);
return ret;
}
static string unescape_hdlc_frame(const string& data) {
return unescape_hdlc_frame(data.data(), data.size());
}
static string escape_hdlc_frame(const void* data, size_t size, uint32_t escape_control_character_flags = 0xFFFFFFFF) {
if (size < 2) {
throw runtime_error("HDLC frame too small for start and end sentinels");
}
StringReader r(data, size);
if (r.pget_u8(size - 1) != 0x7E) {
throw runtime_error("HDLC frame does not end with 7E");
}
r.truncate(size - 1);
if (r.get_u8() != 0x7E) {
throw runtime_error("HDLC frame does not begin with 7E");
}
string ret("\x7E", 1);
while (!r.eof()) {
uint8_t ch = r.get_u8();
if ((ch == 0x7D) || (ch == 0x7E) || ((ch < 0x20) && ((escape_control_character_flags >> ch) & 1))) {
ret.push_back(0x7D);
ret.push_back(ch ^ 0x20);
} else {
ret.push_back(ch);
}
}
ret.push_back(0x7E);
return ret;
}
static string escape_hdlc_frame(const string& data, uint32_t escape_control_character_flags = 0xFFFFFFFF) {
return escape_hdlc_frame(data.data(), data.size(), escape_control_character_flags);
}
// Note: these functions exist because seq nums are allowed to wrap around the
// 32-bit integer space by design. We have to do the subtraction before the
// comparison to allow integer overflow to occur if needed.
@@ -51,8 +110,7 @@ string IPStackSimulator::str_for_ipv4_netloc(uint32_t addr, uint16_t port) {
}
}
string IPStackSimulator::str_for_tcp_connection(shared_ptr<const IPClient> c,
const IPClient::TCPConnection& conn) {
string IPStackSimulator::str_for_tcp_connection(shared_ptr<const IPClient> c, const IPClient::TCPConnection& conn) {
uint64_t key = IPStackSimulator::tcp_conn_key_for_connection(conn);
string server_netloc_str = str_for_ipv4_netloc(conn.server_addr, conn.server_port);
string client_netloc_str = str_for_ipv4_netloc(c->ipv4_addr, conn.client_port);
@@ -77,28 +135,28 @@ IPStackSimulator::~IPStackSimulator() {
}
}
void IPStackSimulator::listen(const string& name, const string& socket_path) {
void IPStackSimulator::listen(const string& name, const string& socket_path, FrameInfo::LinkType link_type) {
int fd = ::listen(socket_path, 0, SOMAXCONN);
ip_stack_simulator_log.info("Listening on Unix socket %s on fd %d as %s", socket_path.c_str(), fd, name.c_str());
this->add_socket(name, fd);
this->add_socket(name, fd, link_type);
}
void IPStackSimulator::listen(const string& name, const string& addr, int port) {
void IPStackSimulator::listen(const string& name, const string& addr, int port, FrameInfo::LinkType link_type) {
if (port == 0) {
this->listen(name, addr);
this->listen(name, addr, link_type);
} else {
int fd = ::listen(addr, port, SOMAXCONN);
string netloc_str = render_netloc(addr, port);
ip_stack_simulator_log.info("Listening on TCP interface %s on fd %d as %s", netloc_str.c_str(), fd, name.c_str());
this->add_socket(name, fd);
this->add_socket(name, fd, link_type);
}
}
void IPStackSimulator::listen(const string& name, int port) {
this->listen(name, "", port);
void IPStackSimulator::listen(const string& name, int port, FrameInfo::LinkType link_type) {
this->listen(name, "", port, link_type);
}
void IPStackSimulator::add_socket(const string& name, int fd) {
void IPStackSimulator::add_socket(const string& name, int fd, FrameInfo::LinkType link_type) {
unique_listener l(
evconnlistener_new(
this->base.get(),
@@ -108,7 +166,7 @@ void IPStackSimulator::add_socket(const string& name, int fd) {
0,
fd),
evconnlistener_free);
this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd), forward_as_tuple(name, std::move(l)));
this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd), forward_as_tuple(name, link_type, std::move(l)));
}
uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_addr) {
@@ -122,9 +180,10 @@ uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_ad
}
}
IPStackSimulator::IPClient::IPClient(shared_ptr<IPStackSimulator> sim, struct bufferevent* bev)
IPStackSimulator::IPClient::IPClient(shared_ptr<IPStackSimulator> sim, FrameInfo::LinkType link_type, struct bufferevent* bev)
: sim(sim),
bev(bev, bufferevent_free),
link_type(link_type),
mac_addr(0),
ipv4_addr(0),
idle_timeout_event(event_new(sim->base.get(), -1, EV_TIMEOUT, &IPStackSimulator::IPClient::dispatch_on_idle_timeout, this), event_free) {
@@ -196,7 +255,7 @@ void IPStackSimulator::on_listen_accept(struct evconnlistener* listener,
struct bufferevent* bev = bufferevent_socket_new(this->base.get(), fd,
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
shared_ptr<IPClient> c(new IPClient(this->shared_from_this(), bev));
auto c = make_shared<IPClient>(this->shared_from_this(), listening_socket->link_type, bev);
this->bev_to_client.emplace(make_pair(bev, c));
bufferevent_setcb(bev, &IPStackSimulator::dispatch_on_client_input, nullptr,
@@ -274,21 +333,143 @@ void IPStackSimulator::on_client_error(struct bufferevent* bev, short events) {
}
}
void IPStackSimulator::on_client_frame(
shared_ptr<IPClient> c, const string& frame) {
if (ip_stack_simulator_log.debug("Virtual network sent frame")) {
print_data(stderr, frame);
fputc('\n', stderr);
}
this->log_frame(frame);
void IPStackSimulator::send_layer3_frame(shared_ptr<IPClient> c, FrameInfo::Protocol proto, const string& data) const {
this->send_layer3_frame(c, proto, data.data(), data.size());
}
FrameInfo fi(frame);
void IPStackSimulator::send_layer3_frame(shared_ptr<IPClient> c, FrameInfo::Protocol proto, const void* data, size_t size) const {
struct evbuffer* out_buf = bufferevent_get_output(c->bev.get());
switch (c->link_type) {
case FrameInfo::LinkType::ETHERNET: {
EthernetHeader ether;
ether.dest_mac = c->mac_addr;
ether.src_mac = this->host_mac_address_bytes;
switch (proto) {
case FrameInfo::Protocol::NONE:
throw logic_error("layer 3 protocol not specified");
case FrameInfo::Protocol::LCP:
throw logic_error("cannot send LCP frame over Ethernet");
case FrameInfo::Protocol::IPV4:
ether.protocol = 0x0800;
break;
case FrameInfo::Protocol::ARP:
ether.protocol = 0x0806;
break;
default:
throw logic_error("unknown layer 3 protocol");
}
le_uint16_t frame_size = size + sizeof(EthernetHeader);
evbuffer_add(out_buf, &frame_size, 2);
evbuffer_add(out_buf, &ether, sizeof(ether));
evbuffer_add(out_buf, data, size);
if (this->pcap_text_log_file) {
StringWriter w;
w.write(&ether, sizeof(ether));
w.write(data, size);
this->log_frame(w.str());
}
break;
}
case FrameInfo::LinkType::HDLC: {
HDLCHeader hdlc;
hdlc.start_sentinel1 = 0x7E;
hdlc.address = 0xFF;
hdlc.control = 0x03;
switch (proto) {
case FrameInfo::Protocol::NONE:
throw logic_error("layer 3 protocol not specified");
case FrameInfo::Protocol::LCP:
hdlc.protocol = 0xC021;
break;
case FrameInfo::Protocol::PAP:
hdlc.protocol = 0xC023;
break;
case FrameInfo::Protocol::IPCP:
hdlc.protocol = 0x8021;
break;
case FrameInfo::Protocol::IPV4:
hdlc.protocol = 0x0021;
break;
case FrameInfo::Protocol::ARP:
throw runtime_error("cannot send ARP packets over HDLC");
default:
throw logic_error("unknown layer 3 protocol");
}
StringWriter w;
w.put(hdlc);
w.write(data, size);
w.put_u16l(FrameInfo::computed_hdlc_checksum(w.str().data() + 1, w.size() - 1));
w.put_u8(0x7E);
string escaped = escape_hdlc_frame(w.str(), c->hdlc_escape_control_character_flags);
if (ip_stack_simulator_log.debug("Sending HDLC frame to virtual network (escaped to %zX bytes)", escaped.size())) {
print_data(stderr, w.str());
}
le_uint16_t frame_size = escaped.size();
evbuffer_add(out_buf, &frame_size, 2);
evbuffer_add(out_buf, escaped.data(), escaped.size());
if (this->pcap_text_log_file) {
this->log_frame(escaped);
}
break;
}
default:
throw logic_error("unknown link type");
}
}
void IPStackSimulator::on_client_frame(shared_ptr<IPClient> c, const string& frame) {
const string* effective_data = &frame;
string hdlc_unescaped_data;
if (c->link_type == FrameInfo::LinkType::HDLC) {
hdlc_unescaped_data = unescape_hdlc_frame(frame);
effective_data = &hdlc_unescaped_data;
}
if (ip_stack_simulator_log.debug("Virtual network sent frame")) {
print_data(stderr, *effective_data);
}
this->log_frame(*effective_data);
FrameInfo fi(c->link_type, *effective_data);
if (ip_stack_simulator_log.should_log(LogLevel::DEBUG)) {
string fi_header = fi.header_str();
ip_stack_simulator_log.debug("Frame header: %s", fi_header.c_str());
}
if (fi.arp) {
if (fi.ether) {
if (c->mac_addr.is_filled_with(0)) {
c->mac_addr = fi.ether->src_mac;
} else if ((fi.ether->src_mac != c->mac_addr) && (fi.ether->src_mac != this->broadcast_mac_address_bytes)) {
throw runtime_error("client sent IPv4 packet from different MAC address");
}
} else if (fi.hdlc) {
uint16_t expected_checksum = fi.computed_hdlc_checksum();
uint16_t stored_checksum = fi.stored_hdlc_checksum();
if (expected_checksum != stored_checksum) {
throw runtime_error(string_printf(
"HDLC checksum is incorrect (%04hX expected, %04hX received)",
expected_checksum, stored_checksum));
}
} else {
throw runtime_error("frame is not Ethernet or HDLC");
}
if (fi.lcp) {
this->on_client_lcp_frame(c, fi);
} else if (fi.pap) {
this->on_client_pap_frame(c, fi);
} else if (fi.ipcp) {
this->on_client_ipcp_frame(c, fi);
} else if (fi.arp) {
this->on_client_arp_frame(c, fi);
} else if (fi.ipv4) {
@@ -299,12 +480,6 @@ void IPStackSimulator::on_client_frame(
expected_ipv4_checksum, fi.ipv4->checksum.load()));
}
// Populate the client's addresses if needed
if (c->mac_addr.is_filled_with(0)) {
c->mac_addr = fi.ether->src_mac;
} else if ((fi.ether->src_mac != c->mac_addr) && (fi.ether->src_mac != this->broadcast_mac_address_bytes)) {
throw runtime_error("client sent IPv4 packet from different MAC address");
}
if ((fi.ipv4->src_addr != c->ipv4_addr) && (fi.ipv4->src_addr != 0)) {
throw runtime_error("client sent IPv4 packet from different IPv4 address");
}
@@ -336,6 +511,261 @@ void IPStackSimulator::on_client_frame(
}
}
void IPStackSimulator::on_client_lcp_frame(shared_ptr<IPClient> c, const FrameInfo& fi) {
switch (fi.lcp->command) {
case 0x01: { // Configure-Request
auto opts_r = fi.read_payload();
while (!opts_r.eof()) {
uint8_t opt = opts_r.get_u8();
string opt_data = opts_r.read(opts_r.get_u8() - 2);
StringReader opt_data_r(opt_data);
switch (opt) {
case 0x01: // Maximum receive unit
// TODO: Currently we ignore this, but we probably should use it.
opt_data_r.get_u16b();
break;
case 0x02: // Escaped control character flags
c->hdlc_escape_control_character_flags = opt_data_r.get_u32b();
break;
case 0x05: // Magic-Number
c->hdlc_remote_magic_number = opt_data_r.get_u32b();
break;
case 0x00: // RESERVED
case 0x03: // Authentication protocol
case 0x04: // Quality protocol
case 0x07: // Protocol field compression
case 0x08: // Address and control field compression
throw runtime_error(string_printf("unimplemented LCP option %02hhX (%zu bytes)", opt, opt_data.size()));
default:
throw runtime_error("unknown LCP option");
}
}
// Technically, we should implement the LCP state machine, but I'm too
// lazy to do this right now. In our situation, it should suffice to
// simply always send a Configure-Request to the client with a magic
// number not equal to the one we received.
StringWriter opts_w;
opts_w.put_u8(0x01); // Maximum receive unit
opts_w.put_u8(0x04);
opts_w.put_u16b(1500);
opts_w.put_u8(0x02); // Escaped control character flags (we don't require any)
opts_w.put_u8(0x06);
opts_w.put_u32(0);
opts_w.put_u8(0x03); // Authentication protocol
opts_w.put_u8(0x04);
opts_w.put_u16b(0xC023); // Password authentication protocol
opts_w.put_u8(0x05); // Magic number (bitwise inverse of the remote end's)
opts_w.put_u8(0x06);
opts_w.put_u32b(~c->hdlc_remote_magic_number);
StringWriter request_w;
request_w.put<LCPHeader>(LCPHeader{
.command = 0x01, // Configure-Request
.request_id = fi.lcp->request_id,
.size = static_cast<uint16_t>(sizeof(LCPHeader) + opts_w.size()),
});
request_w.write(opts_w.str());
this->send_layer3_frame(c, FrameInfo::Protocol::LCP, request_w.str());
StringWriter ack_w;
ack_w.put<LCPHeader>(LCPHeader{
.command = 0x02, // Configure-Ack
.request_id = fi.lcp->request_id,
.size = fi.lcp->size,
});
ack_w.write(fi.payload, fi.payload_size);
this->send_layer3_frame(c, FrameInfo::Protocol::LCP, ack_w.str());
break;
}
case 0x05: { // Terminate-Request
c->ipv4_addr = 0;
c->tcp_connections.clear();
string response(reinterpret_cast<const char*>(fi.payload), fi.payload_size);
response.at(0) = 0x06; // Terminate-Ack
this->send_layer3_frame(c, FrameInfo::Protocol::LCP, response);
break;
}
case 0x09: { // Echo-Request
string response(reinterpret_cast<const char*>(fi.payload), fi.payload_size);
response.at(0) = 0x0A; // Echo-Reply
this->send_layer3_frame(c, FrameInfo::Protocol::LCP, response);
break;
}
case 0x0B: // Discard-Request
case 0x02: // Configure-Ack
break;
case 0x03: // Configure-Nak
case 0x04: // Configure-Reject
case 0x06: // Terminate-Ack
case 0x07: // Code-Reject
case 0x08: // Protocol-Reject
case 0x0A: // Echo-Reply
throw runtime_error("unimplemented LCP command");
default:
throw runtime_error("unknown LCP command");
}
}
void IPStackSimulator::on_client_pap_frame(shared_ptr<IPClient> c, const FrameInfo& fi) {
if (fi.pap->command != 0x01) { // Authenticate-Request
throw runtime_error("client sent incorrect PAP command");
}
auto r = fi.read_payload();
string username = r.read(r.get_u8());
string password = r.read(r.get_u8());
ip_stack_simulator_log.info("Client logged in with username \"%s\" and password", username.c_str());
static const string login_message = "newserv PPP simulator";
StringWriter w;
w.put<PAPHeader>(PAPHeader{
.command = 0x02, // Authenticate-Ack
.request_id = fi.pap->request_id,
.size = login_message.size() + sizeof(PAPHeader) + 1,
});
w.put_u8(login_message.size());
w.write(login_message);
this->send_layer3_frame(c, FrameInfo::Protocol::PAP, w.str());
}
void IPStackSimulator::on_client_ipcp_frame(shared_ptr<IPClient> c, const FrameInfo& fi) {
switch (fi.ipcp->command) {
case 0x01: { // Configure-Request
auto opts_r = fi.read_payload();
uint32_t remote_ip = 0;
uint32_t remote_primary_dns = 0;
uint32_t remote_secondary_dns = 0;
StringWriter rejected_opts_w;
while (!opts_r.eof()) {
uint8_t opt = opts_r.get_u8();
string opt_data = opts_r.read(opts_r.get_u8() - 2);
StringReader opt_data_r(opt_data);
switch (opt) {
case 0x01: // IP addresses (deprecated as of 1992; we don't support it at all)
throw runtime_error("IPCP client sent IP-Addresses option");
case 0x02: // IP compression protocol
rejected_opts_w.put_u8(0x02);
rejected_opts_w.put_u8(opt_data_r.size() + 2);
rejected_opts_w.write(opt_data);
break;
case 0x03: // IP address
remote_ip = opt_data_r.get_u32b();
break;
case 0x81: // Primary DNS server address
remote_primary_dns = opt_data_r.get_u32b();
break;
case 0x83: // Secondary DNS server address
remote_secondary_dns = opt_data_r.get_u32b();
break;
case 0x82: // Primary NBNS server address
case 0x84: // Secondary NBNS server address
case 0x04: // Mobile IP address
throw runtime_error(string_printf("unimplemented IPCP option %02hhX (%zu bytes)", opt, opt_data.size()));
default:
throw runtime_error("unknown IPCP option");
}
}
if (!rejected_opts_w.str().empty()) {
// Send a Configure-Reject if the client specified IP header compression
StringWriter reject_w;
reject_w.put<IPCPHeader>(IPCPHeader{
.command = 0x04, // Configure-Reject
.request_id = fi.ipcp->request_id,
.size = sizeof(IPCPHeader) + rejected_opts_w.size(),
});
reject_w.write(rejected_opts_w.str());
this->send_layer3_frame(c, FrameInfo::Protocol::IPCP, reject_w.str());
} else if ((remote_ip != 0x1E1E1E1E) ||
(remote_primary_dns != 0x23232323) ||
(remote_secondary_dns != 0x24242424)) {
// Send a Configure-Nak if the client's request doesn't exactly match
// what we want them to use.
StringWriter opts_w;
opts_w.put_u8(0x03); // IP address
opts_w.put_u8(0x06);
opts_w.put_u32b(0x1E1E1E1E);
opts_w.put_u8(0x81); // Primary DNS server address
opts_w.put_u8(0x06);
opts_w.put_u32b(0x23232323);
opts_w.put_u8(0x83); // Secondary DNS server address
opts_w.put_u8(0x06);
opts_w.put_u32b(0x24242424);
StringWriter nak_w;
nak_w.put<IPCPHeader>(IPCPHeader{
.command = 0x03, // Configure-Nak
.request_id = fi.ipcp->request_id,
.size = static_cast<uint16_t>(opts_w.size() + sizeof(IPCPHeader)),
});
nak_w.write(opts_w.str());
this->send_layer3_frame(c, FrameInfo::Protocol::IPCP, nak_w.str());
} else { // Options OK
c->ipv4_addr = remote_ip;
// As with LCP, we technically should implement the state machine, but I
// continue to be lazy.
StringWriter opts_w;
opts_w.put_u8(0x03); // IP address
opts_w.put_u8(0x06);
opts_w.put_u32b(0x39393939);
opts_w.put_u8(0x81); // Primary DNS server address
opts_w.put_u8(0x06);
opts_w.put_u32b(0x23232323);
opts_w.put_u8(0x83); // Secondary DNS server address
opts_w.put_u8(0x06);
opts_w.put_u32b(0x24242424);
StringWriter request_w;
request_w.put<IPCPHeader>(IPCPHeader{
.command = 0x01, // Configure-Request
.request_id = fi.ipcp->request_id,
.size = static_cast<uint16_t>(opts_w.size() + sizeof(IPCPHeader)),
});
request_w.write(opts_w.str());
this->send_layer3_frame(c, FrameInfo::Protocol::IPCP, request_w.str());
StringWriter ack_w;
ack_w.put<IPCPHeader>(IPCPHeader{
.command = 0x02, // Configure-Ack
.request_id = fi.ipcp->request_id,
.size = fi.ipcp->size,
});
ack_w.write(fi.payload, fi.payload_size);
this->send_layer3_frame(c, FrameInfo::Protocol::IPCP, ack_w.str());
}
break;
}
case 0x05: { // Terminate-Request
c->ipv4_addr = 0;
c->tcp_connections.clear();
string response(reinterpret_cast<const char*>(fi.payload), fi.payload_size);
response.at(0) = 0x06; // Terminate-Ack
this->send_layer3_frame(c, FrameInfo::Protocol::LCP, response);
break;
}
case 0x02: // Configure-Ack
break;
case 0x03: // Configure-Nak
case 0x04: // Configure-Reject
case 0x06: // Terminate-Ack
case 0x07: // Code-Reject
throw runtime_error("unimplemented IPCP command");
default:
throw runtime_error("unknown LCP command");
}
}
void IPStackSimulator::on_client_arp_frame(
shared_ptr<IPClient> c, const FrameInfo& fi) {
if (fi.arp->hwaddr_len != 6 ||
@@ -353,17 +783,14 @@ void IPStackSimulator::on_client_arp_frame(
reinterpret_cast<const uint8_t*>(fi.payload) + 6);
}
EthernetHeader r_ether;
r_ether.dest_mac = fi.ether->src_mac;
r_ether.src_mac = this->host_mac_address_bytes;
r_ether.protocol = fi.ether->protocol;
ARPHeader r_arp;
r_arp.hardware_type = fi.arp->hardware_type;
r_arp.protocol_type = fi.arp->protocol_type;
r_arp.hwaddr_len = 6;
r_arp.paddr_len = 4;
r_arp.operation = 0x0002;
StringWriter w;
w.put<ARPHeader>(ARPHeader{
.hardware_type = fi.arp->hardware_type,
.protocol_type = fi.arp->protocol_type,
.hwaddr_len = 6,
.paddr_len = 4,
.operation = 0x0002,
});
// The incoming payload is:
// uint8_t src_mac[6]; // MAC address of client
@@ -375,43 +802,19 @@ void IPStackSimulator::on_client_arp_frame(
// uint8_t dest_ip[4]; // IP address of host
// uint8_t src_mac[6]; // MAC address of client
// uint8_t src_ip[4]; // IP address of client
const char* payload_bytes = reinterpret_cast<const char*>(fi.payload);
w.write(this->host_mac_address_bytes.data(), 6);
w.write(payload_bytes + 16, 4);
w.write(payload_bytes, 10);
uint8_t r_payload[20];
memcpy(&r_payload[0], this->host_mac_address_bytes.data(), 6);
memcpy(&r_payload[6], payload_bytes + 16, 4);
memcpy(&r_payload[10], payload_bytes, 10);
struct evbuffer* out_buf = bufferevent_get_output(c->bev.get());
uint16_t frame_size = sizeof(r_ether) + sizeof(r_arp) + sizeof(r_payload);
evbuffer_add(out_buf, &frame_size, 2);
evbuffer_add(out_buf, &r_ether, sizeof(r_ether));
evbuffer_add(out_buf, &r_arp, sizeof(r_arp));
evbuffer_add(out_buf, r_payload, sizeof(r_payload));
ip_stack_simulator_log.debug("Sending ARP response");
if (this->pcap_text_log_file) {
StringWriter w;
w.write(&r_ether, sizeof(r_ether));
w.write(&r_arp, sizeof(r_arp));
w.write(r_payload, sizeof(r_payload));
this->log_frame(w.str());
}
this->send_layer3_frame(c, FrameInfo::Protocol::ARP, w.str());
}
void IPStackSimulator::on_client_udp_frame(
shared_ptr<IPClient> c, const FrameInfo& fi) {
// We only implement DHCP and newserv's DNS server here
void IPStackSimulator::on_client_udp_frame(shared_ptr<IPClient> c, const FrameInfo& fi) {
// We only implement DHCP and newserv's DNS server here.
// Every received UDP packet will elicit exactly one UDP response from
// newserv, so we prepare the response headers in advance
EthernetHeader r_ether;
r_ether.dest_mac = fi.ether->src_mac;
r_ether.src_mac = this->host_mac_address_bytes;
r_ether.protocol = fi.ether->protocol;
IPv4Header r_ipv4;
r_ipv4.version_ihl = 0x45;
@@ -433,7 +836,7 @@ void IPStackSimulator::on_client_udp_frame(
string r_data;
if (fi.udp->dest_port == 67) { // DHCP
StringReader r(fi.payload, fi.payload_size);
auto r = fi.read_payload();
const auto& dhcp = r.get<DHCPHeader>();
if (dhcp.hardware_type != 1) {
throw runtime_error("unknown DHCP hardware type");
@@ -566,29 +969,18 @@ void IPStackSimulator::on_client_udp_frame(
r_udp.checksum = FrameInfo::computed_udp4_checksum(
r_ipv4, r_udp, r_data.data(), r_data.size());
struct evbuffer* out_buf = bufferevent_get_output(c->bev.get());
if (ip_stack_simulator_log.should_log(LogLevel::DEBUG)) {
string remote_str = this->str_for_ipv4_netloc(fi.ipv4->src_addr, fi.udp->src_port);
ip_stack_simulator_log.debug("Sending UDP response to %s", remote_str.c_str());
print_data(stderr, r_data);
}
uint16_t frame_size = sizeof(r_ether) + sizeof(r_ipv4) + sizeof(r_udp) + r_data.size();
evbuffer_add(out_buf, &frame_size, 2);
evbuffer_add(out_buf, &r_ether, sizeof(r_ether));
evbuffer_add(out_buf, &r_ipv4, sizeof(r_ipv4));
evbuffer_add(out_buf, &r_udp, sizeof(r_udp));
evbuffer_add(out_buf, r_data.data(), r_data.size());
StringWriter w;
w.put(r_ipv4);
w.put(r_udp);
w.write(r_data);
if (this->pcap_text_log_file) {
StringWriter w;
w.write(&r_ether, sizeof(r_ether));
w.write(&r_ipv4, sizeof(r_ipv4));
w.write(&r_udp, sizeof(r_udp));
w.write(r_data.data(), r_data.size());
this->log_frame(w.str());
}
this->send_layer3_frame(c, FrameInfo::Protocol::IPV4, w.str());
}
}
@@ -863,8 +1255,7 @@ void IPStackSimulator::on_client_tcp_frame(
}
}
void IPStackSimulator::open_server_connection(
shared_ptr<IPClient> c, IPClient::TCPConnection& conn) {
void IPStackSimulator::open_server_connection(shared_ptr<IPClient> c, IPClient::TCPConnection& conn) {
if (conn.server_bev.get()) {
throw logic_error("server connection is already open");
}
@@ -913,20 +1304,25 @@ void IPStackSimulator::open_server_connection(
}
}
void IPStackSimulator::send_pending_push_frame(
shared_ptr<IPClient> c, IPClient::TCPConnection& conn) {
void IPStackSimulator::send_pending_push_frame(shared_ptr<IPClient> c, IPClient::TCPConnection& conn) {
size_t pending_bytes = evbuffer_get_length(conn.pending_data.get());
if (!pending_bytes) {
return;
}
size_t bytes_to_send = min<size_t>(pending_bytes, conn.next_push_max_frame_size);
if ((c->link_type == FrameInfo::LinkType::HDLC) && (bytes_to_send > 200)) {
// There is a bug in Dolphin's modem implementation (which I wrote, so it's
// my fault) that causes commands to be dropped when too much data is sent
// at once. To work around this, we only send up to 200 bytes in each push
// frame.
bytes_to_send = 200;
}
ip_stack_simulator_log.debug("Sending PSH frame with seq_num %08" PRIX32 ", 0x%zX/0x%zX data bytes",
conn.acked_server_seq, bytes_to_send, pending_bytes);
this->send_tcp_frame(c, conn, TCPHeader::Flag::PSH, conn.pending_data.get(),
bytes_to_send);
this->send_tcp_frame(c, conn, TCPHeader::Flag::PSH, conn.pending_data.get(), bytes_to_send);
struct timeval resend_push_timeout = usecs_to_timeval(conn.resend_push_usecs);
event_add(conn.resend_push_event.get(), &resend_push_timeout);
@@ -953,11 +1349,6 @@ void IPStackSimulator::send_tcp_frame(
throw logic_error("data should be given if and only if PSH is given");
}
EthernetHeader ether;
ether.dest_mac = c->mac_addr;
ether.src_mac = this->host_mac_address_bytes;
ether.protocol = 0x0800; // IPv4
IPv4Header ipv4;
ipv4.version_ihl = 0x45;
ipv4.tos = 0;
@@ -984,28 +1375,16 @@ void IPStackSimulator::send_tcp_frame(
ipv4.checksum = FrameInfo::computed_ipv4_header_checksum(ipv4);
const void* linear_data = src_bytes ? evbuffer_pullup(src_buf, src_bytes) : nullptr;
tcp.checksum = FrameInfo::computed_tcp4_checksum(
ipv4, tcp, linear_data, src_bytes);
tcp.checksum = FrameInfo::computed_tcp4_checksum(ipv4, tcp, linear_data, src_bytes);
struct evbuffer* out_buf = bufferevent_get_output(c->bev.get());
uint16_t frame_size = sizeof(ether) + sizeof(ipv4) + sizeof(tcp) + src_bytes;
evbuffer_add(out_buf, &frame_size, 2);
evbuffer_add(out_buf, &ether, sizeof(ether));
evbuffer_add(out_buf, &ipv4, sizeof(ipv4));
evbuffer_add(out_buf, &tcp, sizeof(tcp));
StringWriter w;
w.put(ipv4);
w.put(tcp);
if (src_bytes) {
evbuffer_add(out_buf, linear_data, src_bytes);
w.write(linear_data, src_bytes);
}
if (this->pcap_text_log_file) {
StringWriter w;
w.write(&ether, sizeof(ether));
w.write(&ipv4, sizeof(ipv4));
w.write(&tcp, sizeof(tcp));
w.write(linear_data, src_bytes);
this->log_frame(w.str());
}
this->send_layer3_frame(c, FrameInfo::Protocol::IPV4, w.str());
}
void IPStackSimulator::dispatch_on_resend_push(evutil_socket_t, short, void* ctx) {
+20 -7
View File
@@ -1,3 +1,5 @@
#pragma once
#include <netinet/in.h>
#include <stdint.h>
@@ -19,10 +21,10 @@ public:
std::shared_ptr<ServerState> state);
~IPStackSimulator();
void listen(const std::string& name, const std::string& socket_path);
void listen(const std::string& name, const std::string& addr, int port);
void listen(const std::string& name, int port);
void add_socket(const std::string& name, int fd);
void listen(const std::string& name, const std::string& socket_path, FrameInfo::LinkType link_type);
void listen(const std::string& name, const std::string& addr, int port, FrameInfo::LinkType link_type);
void listen(const std::string& name, int port, FrameInfo::LinkType link_type);
void add_socket(const std::string& name, int fd, FrameInfo::LinkType link_type);
static uint32_t connect_address_for_remote_address(uint32_t remote_addr);
@@ -39,7 +41,10 @@ private:
std::weak_ptr<IPStackSimulator> sim;
unique_bufferevent bev;
parray<uint8_t, 6> mac_addr;
FrameInfo::LinkType link_type;
uint32_t hdlc_escape_control_character_flags = 0xFFFFFFFF;
uint32_t hdlc_remote_magic_number = 0;
parray<uint8_t, 6> mac_addr; // Only used for LinkType::ETHERNET
uint32_t ipv4_addr;
struct TCPConnection {
@@ -75,7 +80,7 @@ private:
unique_event idle_timeout_event;
IPClient(std::shared_ptr<IPStackSimulator> sim, struct bufferevent* bev);
IPClient(std::shared_ptr<IPStackSimulator> sim, FrameInfo::LinkType link_type, struct bufferevent* bev);
static void dispatch_on_idle_timeout(evutil_socket_t fd, short events, void* ctx);
void on_idle_timeout();
@@ -83,10 +88,12 @@ private:
struct ListeningSocket {
std::string name;
FrameInfo::LinkType link_type;
unique_listener listener;
ListeningSocket(const std::string& name, unique_listener&& l)
ListeningSocket(const std::string& name, FrameInfo::LinkType link_type, unique_listener&& l)
: name(name),
link_type(link_type),
listener(std::move(l)) {}
};
@@ -120,7 +127,13 @@ private:
static void dispatch_on_client_error(struct bufferevent* bev, short events, void* ctx);
void on_client_error(struct bufferevent* bev, short events);
void send_layer3_frame(std::shared_ptr<IPClient> c, FrameInfo::Protocol proto, const std::string& data) const;
void send_layer3_frame(std::shared_ptr<IPClient> c, FrameInfo::Protocol proto, const void* data, size_t size) const;
void on_client_frame(std::shared_ptr<IPClient> c, const std::string& frame);
void on_client_lcp_frame(std::shared_ptr<IPClient> c, const FrameInfo& fi);
void on_client_pap_frame(std::shared_ptr<IPClient> c, const FrameInfo& fi);
void on_client_ipcp_frame(std::shared_ptr<IPClient> c, const FrameInfo& fi);
void on_client_arp_frame(std::shared_ptr<IPClient> c, const FrameInfo& fi);
void on_client_udp_frame(std::shared_ptr<IPClient> c, const FrameInfo& fi);
void on_client_tcp_frame(std::shared_ptr<IPClient> c, const FrameInfo& fi);
+48 -31
View File
@@ -3,6 +3,8 @@
#include <algorithm>
#include <array>
#include "Loggers.hh"
using namespace std;
static const array<uint8_t, 10> favored_weapon_by_section_id = {
@@ -23,7 +25,7 @@ ItemCreator::ItemCreator(
uint8_t section_id,
uint32_t random_seed,
shared_ptr<const BattleRules> restrictions)
: log(string_printf("[ItemCreator:%s/%s/%s/%c/%hhu] ", name_for_enum(version), abbreviation_for_episode(episode), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id)),
: log(string_printf("[ItemCreator:%s/%s/%s/%c/%hhu] ", name_for_enum(version), abbreviation_for_episode(episode), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id), lobby_log.min_level),
version(version),
episode(episode),
mode(mode),
@@ -41,6 +43,15 @@ ItemCreator::ItemCreator(
this->generate_unit_stars_tables();
}
void ItemCreator::set_random_state(uint32_t seed, uint32_t absolute_offset) {
if ((this->random_crypt.seed() != seed) || (this->random_crypt.absolute_offset() > absolute_offset)) {
this->random_crypt = PSOV2Encryption(seed);
}
while (this->random_crypt.absolute_offset() < absolute_offset) {
this->random_crypt.next();
}
}
void ItemCreator::clear_destroyed_entities() {
this->destroyed_monsters.clear();
this->destroyed_boxes.clear();
@@ -132,7 +143,7 @@ ItemData ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
area_norm, this->random_crypt.seed(), this->random_crypt.absolute_offset());
ItemData item = this->check_rare_specs_and_create_rare_box_item(area_norm);
if (item.empty()) {
uint8_t item_class = this->get_rand_from_weighted_tables_2d_vertical(this->pt->box_item_class_prob_table(), area_norm);
uint8_t item_class = this->get_rand_from_weighted_tables_2d_vertical(this->pt->box_item_class_prob_table, area_norm);
this->log.info("Item class is %02hhX", item_class);
switch (item_class) {
case 0: // Weapon
@@ -175,11 +186,13 @@ ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, u
}
this->log.info("Enemy type: %" PRIX32 "; random state: %08" PRIX32 " %08" PRIX32, enemy_type, this->random_crypt.seed(), this->random_crypt.absolute_offset());
uint8_t type_drop_prob = this->pt->enemy_type_drop_probs().at(enemy_type);
uint8_t type_drop_prob = this->pt->enemy_type_drop_probs.at(enemy_type);
uint8_t drop_sample = this->rand_int(100);
if (drop_sample >= type_drop_prob) {
this->log.info("Drop not chosen (%hhu >= %hhu)", drop_sample, type_drop_prob);
return ItemData();
} else {
this->log.info("Drop chosen (%hhu < %hhu)", drop_sample, type_drop_prob);
}
ItemData item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area_norm);
@@ -198,7 +211,7 @@ ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, u
item_class = 4;
break;
case 2:
item_class = this->pt->enemy_item_classes().at(enemy_type);
item_class = this->pt->enemy_item_classes.at(enemy_type);
break;
default:
throw logic_error("invalid item class determinant");
@@ -224,7 +237,7 @@ ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, u
break;
case 5: // Meseta
item.data1[0] = 0x04;
item.data2d = this->choose_meseta_amount(this->pt->enemy_meseta_ranges(), enemy_type) & 0xFFFF;
item.data2d = this->choose_meseta_amount(this->pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
break;
default:
return item;
@@ -277,12 +290,16 @@ uint32_t ItemCreator::choose_meseta_amount(
// Note: The original code seems like it has a bug here: it compares to 0xFF
// instead of 0xFFFF (and returns 0xFF if either limit matches 0xFF).
uint32_t ret = 0;
if (((min == 0xFFFF) || (max == 0xFFFF)) || (max < min)) {
return 0xFFFF;
ret = 0xFFFF;
} else if (min != max) {
return this->rand_int((max - min) + 1) + min;
ret = this->rand_int((max - min) + 1) + min;
} else {
ret = min;
}
return min;
this->log.info("Chose %" PRIu32 " Meseta from range [%hu, %hu]", ret, min, max);
return ret;
}
bool ItemCreator::should_allow_meseta_drops() const {
@@ -329,7 +346,7 @@ ItemData ItemCreator::check_rate_and_create_rare_item(const RareItemSet::Expande
item.data1[2] = drop.item_code[2];
switch (item.data1[0]) {
case 0:
if (this->pt->has_rare_bonus_value_prob_table()) {
if (this->pt->has_rare_bonus_value_prob_table) {
this->generate_rare_weapon_bonuses(item, this->rand_int(10));
} else {
this->generate_common_weapon_bonuses(item, area_norm);
@@ -362,13 +379,13 @@ void ItemCreator::generate_rare_weapon_bonuses(ItemData& item, uint32_t random_s
return;
}
if (!this->pt->has_rare_bonus_value_prob_table()) {
if (!this->pt->has_rare_bonus_value_prob_table) {
throw logic_error("generate_rare_weapon_bonuses called for common item table without rare bonus value probability table");
}
for (size_t z = 0; z < 6; z += 2) {
uint8_t bonus_type = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_type_prob_table(), random_sample);
int16_t bonus_value = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_value_prob_table(), 5);
uint8_t bonus_type = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_type_prob_table, random_sample);
int16_t bonus_value = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_value_prob_table, 5);
item.data1[z + 6] = bonus_type;
item.data1[z + 7] = bonus_value * 5 - 10;
// Note: The original code has a special case here, which divides
@@ -386,12 +403,12 @@ void ItemCreator::generate_common_weapon_bonuses(ItemData& item, uint8_t area_no
}
for (size_t row = 0; row < 3; row++) {
uint8_t spec = this->pt->nonrare_bonus_prob_spec().at(row).at(area_norm);
uint8_t spec = this->pt->nonrare_bonus_prob_spec.at(row).at(area_norm);
if (spec == 0xFF) {
this->log.info("Bonus %zu is forbidden", row);
} else {
item.data1[(row * 2) + 6] = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_type_prob_table(), area_norm);
int16_t amount = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_value_prob_table(), spec);
item.data1[(row * 2) + 6] = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_type_prob_table, area_norm);
int16_t amount = this->get_rand_from_weighted_tables_2d_vertical(this->pt->bonus_value_prob_table, spec);
item.data1[(row * 2) + 7] = amount * 5 - 10;
this->log.info("Bonus %zu generated as %02hhX %02hhX from area_norm %02hhX and spec %02hhX", row, item.data1[(row * 2) + 6], item.data1[(row * 2) + 7], area_norm, spec);
}
@@ -560,7 +577,7 @@ void ItemCreator::generate_common_item_variances(uint32_t area_norm, ItemData& i
break;
case 1:
if (item.data1[1] == 3) {
float f1 = 1.0 + this->pt->unit_max_stars_table().at(area_norm);
float f1 = 1.0 + this->pt->unit_max_stars_table.at(area_norm);
float f2 = this->rand_float_0_1_from_crypt();
uint8_t stars = static_cast<uint32_t>(f1 * f2) & 0xFF;
this->log.info("Unit stars: %g * %g = %" PRIu32, f1, f2, stars);
@@ -580,7 +597,7 @@ void ItemCreator::generate_common_item_variances(uint32_t area_norm, ItemData& i
this->generate_common_tool_variances(area_norm, item);
break;
case 4:
item.data2d = this->choose_meseta_amount(this->pt->box_meseta_ranges(), area_norm) & 0xFFFF;
item.data2d = this->choose_meseta_amount(this->pt->box_meseta_ranges, area_norm) & 0xFFFF;
break;
default:
// Note: The original code does the following here:
@@ -596,15 +613,15 @@ void ItemCreator::generate_common_item_variances(uint32_t area_norm, ItemData& i
void ItemCreator::generate_common_armor_or_shield_type_and_variances(char area_norm, ItemData& item) {
this->generate_common_armor_slots_and_bonuses(item);
uint8_t type = this->get_rand_from_weighted_tables_1d(this->pt->armor_shield_type_index_prob_table());
item.data1[2] = area_norm + type + this->pt->armor_or_shield_type_bias();
uint8_t type = this->get_rand_from_weighted_tables_1d(this->pt->armor_shield_type_index_prob_table);
item.data1[2] = area_norm + type + this->pt->armor_or_shield_type_bias;
if (item.data1[2] < 3) {
item.data1[2] = 0;
} else {
item.data1[2] -= 3;
}
this->log.info("Armor/shield type: max(%02hhX + %02hhX + %02hhX - 3, 0) = %02hhX",
area_norm, type, this->pt->armor_or_shield_type_bias(), item.data1[2]);
area_norm, type, this->pt->armor_or_shield_type_bias, item.data1[2]);
}
void ItemCreator::generate_common_armor_slots_and_bonuses(ItemData& item) {
@@ -622,13 +639,13 @@ void ItemCreator::generate_common_armor_slots_and_bonuses(ItemData& item) {
}
void ItemCreator::generate_common_armor_slot_count(ItemData& item) {
item.data1[5] = this->get_rand_from_weighted_tables_1d(this->pt->armor_slot_count_prob_table());
item.data1[5] = this->get_rand_from_weighted_tables_1d(this->pt->armor_slot_count_prob_table);
}
void ItemCreator::generate_common_tool_variances(uint32_t area_norm, ItemData& item) {
item.clear();
uint8_t tool_class = this->get_rand_from_weighted_tables_2d_vertical(this->pt->tool_class_prob_table(), area_norm);
uint8_t tool_class = this->get_rand_from_weighted_tables_2d_vertical(this->pt->tool_class_prob_table, area_norm);
if (this->is_v3() && (tool_class == 0x1A)) {
tool_class = 0x73;
}
@@ -653,7 +670,7 @@ void ItemCreator::generate_common_tool_variances(uint32_t area_norm, ItemData& i
}
if (item.data1[1] == 0x02) { // Tech disk
item.data1[4] = this->get_rand_from_weighted_tables_2d_vertical(this->pt->technique_index_prob_table(), area_norm);
item.data1[4] = this->get_rand_from_weighted_tables_2d_vertical(this->pt->technique_index_prob_table, area_norm);
item.data1[2] = this->generate_tech_disk_level(item.data1[4], area_norm);
this->clear_tool_item_if_invalid(item);
}
@@ -661,7 +678,7 @@ void ItemCreator::generate_common_tool_variances(uint32_t area_norm, ItemData& i
}
uint8_t ItemCreator::generate_tech_disk_level(uint32_t tech_num, uint32_t area_norm) {
const auto& range = this->pt->technique_level_ranges().at(tech_num).at(area_norm);
const auto& range = this->pt->technique_level_ranges.at(tech_num).at(area_norm);
if (((range.min == 0xFF) || (range.max == 0xFF)) || (range.max < range.min)) {
return 0xFF;
} else if (range.min != range.max) {
@@ -685,12 +702,12 @@ void ItemCreator::generate_common_weapon_variances(uint8_t area_norm, ItemData&
weapon_type_prob_table[0] = 0;
memmove(
weapon_type_prob_table.data() + 1,
this->pt->base_weapon_type_prob_table().data(),
this->pt->base_weapon_type_prob_table.data(),
0x0C);
for (size_t z = 1; z < 13; z++) {
// Technically this should be `if (... < 0)`, but whatever
if ((area_norm + this->pt->subtype_base_table().at(z - 1)) & 0x80) {
if ((area_norm + this->pt->subtype_base_table.at(z - 1)) & 0x80) {
weapon_type_prob_table[z] = 0;
}
}
@@ -706,8 +723,8 @@ void ItemCreator::generate_common_weapon_variances(uint8_t area_norm, ItemData&
this->log.info("00 chosen from subtype table; skipping item");
item.clear();
} else {
int8_t subtype_base = this->pt->subtype_base_table().at(item.data1[1] - 1);
uint8_t area_length = this->pt->subtype_area_length_table().at(item.data1[1] - 1);
int8_t subtype_base = this->pt->subtype_base_table.at(item.data1[1] - 1);
uint8_t area_length = this->pt->subtype_area_length_table.at(item.data1[1] - 1);
this->log.info("Subtype table yielded %02hhX; subtype base is %hhd with area length %hhu", item.data1[1], subtype_base, area_length);
if (subtype_base < 0) {
item.data1[2] = (area_norm + subtype_base) / area_length;
@@ -727,7 +744,7 @@ void ItemCreator::generate_common_weapon_variances(uint8_t area_norm, ItemData&
void ItemCreator::generate_common_weapon_grind(ItemData& item, uint8_t offset_within_subtype_range) {
if (item.data1[0] == 0) {
uint8_t offset = clamp<uint8_t>(offset_within_subtype_range, 0, 3);
item.data1[3] = this->get_rand_from_weighted_tables_2d_vertical(this->pt->grind_prob_table(), offset);
item.data1[3] = this->get_rand_from_weighted_tables_2d_vertical(this->pt->grind_prob_table, offset);
this->log.info("Generated grind %02hhX from offset within subtype range %02hhX", item.data1[3], offset_within_subtype_range);
}
}
@@ -740,13 +757,13 @@ void ItemCreator::generate_common_weapon_special(ItemData& item, uint8_t area_no
this->log.info("Item is rare; skipping special generation");
return;
}
uint8_t special_mult = this->pt->special_mult().at(area_norm);
uint8_t special_mult = this->pt->special_mult.at(area_norm);
if (special_mult == 0) {
this->log.info("Special multiplier is zero for area_norm %02hhX; skipping special generation", area_norm);
return;
}
uint8_t det = this->rand_int(100);
uint8_t prob = this->pt->special_percent().at(area_norm);
uint8_t prob = this->pt->special_percent.at(area_norm);
if (det >= prob) {
this->log.info("Special not chosen (%02hhX > %02hhX)", det, prob);
return;
+1
View File
@@ -28,6 +28,7 @@ public:
std::shared_ptr<const BattleRules> restrictions = nullptr);
~ItemCreator() = default;
void set_random_state(uint32_t seed, uint32_t absolute_offset);
void clear_destroyed_entities();
ItemData on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area);
+11
View File
@@ -43,6 +43,17 @@ bool ItemData::operator!=(const ItemData& other) const {
return !this->operator==(other);
}
bool ItemData::operator<(const ItemData& other) const {
for (size_t z = 0; z < 3; z++) {
if (this->data1db[z] < other.data1db[z]) {
return true;
} else if (this->data1db[z] > other.data1db[z]) {
return false;
}
}
return (this->data2db < other.data2db);
}
void ItemData::clear() {
this->data1d.clear(0);
this->id = 0xFFFFFFFF;
+6
View File
@@ -111,13 +111,17 @@ struct ItemData { // 0x14 bytes
union {
parray<uint8_t, 12> data1;
parray<le_uint16_t, 6> data1w;
parray<be_uint16_t, 6> data1wb;
parray<le_uint32_t, 3> data1d;
parray<be_uint32_t, 3> data1db;
} __attribute__((packed));
le_uint32_t id;
union {
parray<uint8_t, 4> data2;
parray<le_uint16_t, 2> data2w;
parray<be_uint16_t, 2> data2wb;
le_uint32_t data2d;
be_uint32_t data2db;
} __attribute__((packed));
ItemData();
@@ -128,6 +132,8 @@ struct ItemData { // 0x14 bytes
bool operator==(const ItemData& other) const;
bool operator!=(const ItemData& other) const;
bool operator<(const ItemData& other) const;
void clear();
static ItemData from_data(const std::string& data);
+1 -1
View File
@@ -381,7 +381,7 @@ const ItemParameterTable::ToolV4& ItemParameterTable::get_tool(uint8_t data1_1,
def_v4.amount = def_v3.amount.load();
def_v4.tech = def_v3.tech.load();
def_v4.cost = def_v3.cost.load();
def_v4.item_flag = def_v3.item_flag;
def_v4.item_flag = def_v3.item_flag.load();
} else {
throw logic_error("table is not v2, v3, or v4");
+2 -2
View File
@@ -207,12 +207,12 @@ public:
struct Tool {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
using S32T = typename std::conditional<IsBigEndian, be_int32_t, le_int32_t>::type;
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
BaseT base;
U16T amount = 0;
U16T tech = 0;
S32T cost = 0;
uint8_t item_flag = 0;
parray<uint8_t, 3> unused;
U32T item_flag = 0;
} __attribute__((packed));
struct ToolV2 : Tool<ItemBaseV2<false>, false> {
} __attribute__((packed));
+4 -8
View File
@@ -17,7 +17,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
bool is_v3_or_later = is_v3(c->version()) || is_v4(c->version());
bool should_delete_item = is_v3_or_later;
auto player = c->game_data.character();
auto player = c->character();
auto& item = player->inventory.items[item_index];
uint32_t item_identifier = item.data.primary_identifier();
@@ -54,7 +54,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
weapon.data.data1[3] += (item.data.data1[2] + 1);
} else if ((item_identifier & 0xFFFF00) == 0x030B00) { // Material
auto p = c->game_data.character();
auto p = c->character();
using Type = PSOBBCharacterFile::MaterialType;
Type type;
@@ -247,10 +247,6 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
} catch (const out_of_range&) {
}
}
if (!combo_applied) {
throw runtime_error("no combinations apply");
}
}
if (should_delete_item) {
@@ -276,7 +272,7 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
});
auto s = c->require_server_state();
auto player = c->game_data.character();
auto player = c->character();
auto& fed_item = player->inventory.items[fed_item_index];
auto& mag_item = player->inventory.items[mag_item_index];
@@ -376,7 +372,7 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
} else if ((mag_level % 5) == 0) { // Level 50 (and beyond) evolutions
if (evolution_number < 4) {
if (mag_level >= 100) {
if ((mag_level >= 100) && !is_v1_or_v2(c->version())) {
uint8_t section_id_group = player->disp.visual.section_id % 3;
uint16_t def = mag_item.data.data1w[2] / 100;
uint16_t pow = mag_item.data.data1w[3] / 100;
+1 -1
View File
@@ -33,7 +33,7 @@ void PlayerStats::advance_to_level(uint8_t char_class, uint32_t level, shared_pt
LevelTable::LevelTable(shared_ptr<const string> data, bool compressed) {
if (compressed) {
this->data.reset(new string(prs_decompress(*data)));
this->data = make_shared<string>(prs_decompress(*data));
} else {
this->data = data;
}
+72 -55
View File
@@ -26,6 +26,8 @@ License::License(const JSON& json)
this->bb_password = json.get_string("BBPassword", "");
this->flags = json.get_int("Flags", 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", "");
this->ep3_current_meseta = json.get_int("Ep3CurrentMeseta", 0);
this->ep3_total_meseta_earned = json.get_int("Ep3TotalMesetaEarned", 0);
this->bb_team_id = json.get_int("BBTeamID", 0);
@@ -43,23 +45,16 @@ JSON License::json() const {
{"BBPassword", this->bb_password},
{"Flags", this->flags},
{"BanEndTime", this->ban_end_time},
{"LastPlayerName", this->last_player_name},
{"AutoReplyMessage", this->auto_reply_message},
{"Ep3CurrentMeseta", this->ep3_current_meseta},
{"Ep3TotalMesetaEarned", this->ep3_total_meseta_earned},
{"BBTeamID", this->bb_team_id},
});
}
void License::save() const {
auto json = this->json();
string json_data = json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS);
string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->serial_number);
save_file(filename, json_data);
}
void License::delete_file() const {
string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->serial_number);
remove(filename.c_str());
}
void License::save() const {}
void License::delete_file() const {}
string License::str() const {
vector<string> tokens;
@@ -98,53 +93,22 @@ string License::str() const {
return "[License: " + join(tokens, ", ") + "]";
}
struct BinaryLicense {
pstring<TextEncoding::ASCII, 0x14> username; // BB username (max. 16 chars; should technically be Unicode)
pstring<TextEncoding::ASCII, 0x14> bb_password; // BB password (max. 16 chars)
uint32_t serial_number; // PC/GC serial number. MUST BE PRESENT FOR BB LICENSES TOO; this is also the player's guild card number.
pstring<TextEncoding::ASCII, 0x10> access_key; // PC/GC access key. (to log in using PC on a GC license, just enter the first 8 characters of the GC access key)
pstring<TextEncoding::ASCII, 0x0C> gc_password; // GC password
uint32_t privileges; // privilege level
uint64_t ban_end_time; // end time of ban (zero = not banned)
} __attribute__((packed));
DiskLicense::DiskLicense(const JSON& json) : License(json) {}
LicenseIndex::LicenseIndex() {
if (!isdir("system/licenses")) {
mkdir("system/licenses", 0755);
}
void DiskLicense::save() const {
auto json = this->json();
string json_data = json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS);
string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->serial_number);
save_file(filename, json_data);
}
// Convert binary licenses to JSON licenses and save them
if (isfile("system/licenses.nsi")) {
auto bin_licenses = load_vector_file<BinaryLicense>("system/licenses.nsi");
for (const auto& bin_license : bin_licenses) {
// Only add licenses from the binary file if there isn't a JSON version of
// the same license
try {
this->get(bin_license.serial_number);
} catch (const missing_license&) {
License license;
license.serial_number = bin_license.serial_number;
license.access_key = bin_license.access_key.decode();
license.gc_password = bin_license.gc_password.decode();
license.bb_username = bin_license.username.decode();
license.bb_password = bin_license.bb_password.decode();
license.flags = bin_license.privileges;
license.ban_end_time = bin_license.ban_end_time;
license.ep3_current_meseta = 0;
license.ep3_total_meseta_earned = 0;
license.save();
}
}
::remove("system/licenses.nsi");
}
void DiskLicense::delete_file() const {
string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->serial_number);
remove(filename.c_str());
}
for (const auto& item : list_directory("system/licenses")) {
if (ends_with(item, ".json")) {
JSON json = JSON::parse(load_file("system/licenses/" + item));
shared_ptr<License> license(new License(json));
this->add(license);
}
}
shared_ptr<License> LicenseIndex::create_license() const {
return make_shared<License>();
}
size_t LicenseIndex::count() const {
@@ -284,3 +248,56 @@ shared_ptr<License> LicenseIndex::verify_bb(const string& username, const string
throw missing_license();
}
}
DiskLicenseIndex::DiskLicenseIndex() {
struct BinaryLicense {
pstring<TextEncoding::ASCII, 0x14> username; // BB username (max. 16 chars; should technically be Unicode)
pstring<TextEncoding::ASCII, 0x14> bb_password; // BB password (max. 16 chars)
uint32_t serial_number; // PC/GC serial number. MUST BE PRESENT FOR BB LICENSES TOO; this is also the player's guild card number.
pstring<TextEncoding::ASCII, 0x10> access_key; // PC/GC access key. (to log in using PC on a GC license, just enter the first 8 characters of the GC access key)
pstring<TextEncoding::ASCII, 0x0C> gc_password; // GC password
uint32_t privileges; // privilege level
uint64_t ban_end_time; // end time of ban (zero = not banned)
} __attribute__((packed));
if (!isdir("system/licenses")) {
mkdir("system/licenses", 0755);
}
// Convert binary licenses to JSON licenses and save them
if (isfile("system/licenses.nsi")) {
auto bin_licenses = load_vector_file<BinaryLicense>("system/licenses.nsi");
for (const auto& bin_license : bin_licenses) {
// Only add licenses from the binary file if there isn't a JSON version of
// the same license
try {
this->get(bin_license.serial_number);
} catch (const missing_license&) {
License license;
license.serial_number = bin_license.serial_number;
license.access_key = bin_license.access_key.decode();
license.gc_password = bin_license.gc_password.decode();
license.bb_username = bin_license.username.decode();
license.bb_password = bin_license.bb_password.decode();
license.flags = bin_license.privileges;
license.ban_end_time = bin_license.ban_end_time;
license.ep3_current_meseta = 0;
license.ep3_total_meseta_earned = 0;
license.save();
}
}
::remove("system/licenses.nsi");
}
for (const auto& item : list_directory("system/licenses")) {
if (ends_with(item, ".json")) {
JSON json = JSON::parse(load_file("system/licenses/" + item));
auto license = make_shared<DiskLicense>(json);
this->add(license);
}
}
}
shared_ptr<License> DiskLicenseIndex::create_license() const {
return make_shared<DiskLicense>();
}
+44 -18
View File
@@ -10,23 +10,26 @@
class LicenseIndex;
struct License {
class License {
public:
enum Flag : uint32_t {
// clang-format off
KICK_USER = 0x00000001,
BAN_USER = 0x00000002,
SILENCE_USER = 0x00000004,
CHANGE_LOBBY_INFO = 0x00000008,
CHANGE_EVENT = 0x00000010,
ANNOUNCE = 0x00000020,
FREE_JOIN_GAMES = 0x00000040,
UNLOCK_GAMES = 0x00000080,
DEBUG = 0x01000000,
MODERATOR = 0x00000007,
ADMINISTRATOR = 0x000000FF,
ROOT = 0x7FFFFFFF,
KICK_USER = 0x00000001,
BAN_USER = 0x00000002,
SILENCE_USER = 0x00000004,
CHANGE_LOBBY_INFO = 0x00000008,
CHANGE_EVENT = 0x00000010,
ANNOUNCE = 0x00000020,
FREE_JOIN_GAMES = 0x00000040,
UNLOCK_GAMES = 0x00000080,
DEBUG = 0x01000000,
CHEAT_ANYWHERE = 0x02000000,
DISABLE_QUEST_REQUIREMENTS = 0x04000000,
MODERATOR = 0x00000007,
ADMINISTRATOR = 0x000000FF,
ROOT = 0x7FFFFFFF,
UNUSED_BITS = 0x7EFFFF00,
UNUSED_BITS = 0x78FFFF00,
// clang-format on
};
@@ -41,6 +44,8 @@ struct License {
uint32_t flags = 0;
uint64_t ban_end_time = 0; // 0 = not banned
std::string last_player_name;
std::string auto_reply_message;
uint32_t ep3_current_meseta = 0;
uint32_t ep3_total_meseta_earned = 0;
@@ -49,14 +54,25 @@ struct License {
License() = default;
explicit License(const JSON& json);
virtual ~License() = default;
JSON json() const;
void save() const;
void delete_file() const;
virtual void save() const;
virtual void delete_file() const;
std::string str() const;
};
class DiskLicense : public License {
public:
DiskLicense() = default;
explicit DiskLicense(const JSON& json);
virtual ~DiskLicense() = default;
virtual void save() const;
virtual void delete_file() const;
};
class LicenseIndex {
public:
class no_username : public std::invalid_argument {
@@ -76,8 +92,10 @@ public:
missing_license() : invalid_argument("missing license") {}
};
LicenseIndex();
~LicenseIndex() = default;
LicenseIndex() = default;
virtual ~LicenseIndex() = default;
virtual std::shared_ptr<License> create_license() const;
size_t count() const;
std::shared_ptr<License> get(uint32_t serial_number) const;
@@ -97,3 +115,11 @@ protected:
std::unordered_map<std::string, std::shared_ptr<License>> xb_gamertag_to_license;
std::unordered_map<uint32_t, std::shared_ptr<License>> serial_number_to_license;
};
class DiskLicenseIndex : public LicenseIndex {
public:
DiskLicenseIndex();
virtual ~DiskLicenseIndex() = default;
virtual std::shared_ptr<License> create_license() const;
};
+35 -12
View File
@@ -77,17 +77,17 @@ void Lobby::create_item_creator() {
case Version::GC_NTE:
case Version::GC_V3:
case Version::XB_V3:
common_item_set = s->common_item_set_v3;
common_item_set = s->common_item_set_v3_v4;
rare_item_set = s->rare_item_sets.at("rare-table-v3");
break;
case Version::BB_V4:
common_item_set = s->common_item_set_v3;
common_item_set = s->common_item_set_v3_v4;
rare_item_set = s->rare_item_sets.at("rare-table-v4");
break;
default:
throw logic_error("invalid lobby base version");
}
this->item_creator.reset(new ItemCreator(
this->item_creator = make_shared<ItemCreator>(
common_item_set,
rare_item_set,
s->armor_random_set,
@@ -101,7 +101,7 @@ void Lobby::create_item_creator() {
this->difficulty,
this->section_id,
this->random_seed,
this->quest ? this->quest->battle_rules : nullptr));
this->quest ? this->quest->battle_rules : nullptr);
}
void Lobby::create_ep3_server() {
@@ -214,12 +214,12 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
// If the lobby is a game and item tracking is enabled, assign the inventory's
// item IDs
if (this->is_game() && this->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
this->assign_inventory_item_ids(c);
this->assign_inventory_and_bank_item_ids(c);
}
// If the lobby is recording a battle record, add the player join event
if (this->battle_record) {
auto p = c->game_data.character();
auto p = c->character();
PlayerLobbyDataDCGC lobby_data;
lobby_data.player_tag = 0x00010000;
lobby_data.guild_card_number = c->license->serial_number;
@@ -228,7 +228,7 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
lobby_data,
p->inventory,
p->disp.to_dcpcv3(c->language(), c->language()),
c->game_data.ep3_config ? (c->game_data.ep3_config->online_clv_exp / 100) : 0);
c->ep3_config ? (c->ep3_config->online_clv_exp / 100) : 0);
}
// Send spectator count notifications if needed
@@ -318,7 +318,7 @@ shared_ptr<Client> Lobby::find_client(const string* identifier, uint64_t serial_
(lc->license->serial_number == serial_number)) {
return lc;
}
if (identifier && (lc->game_data.character()->disp.name.eq(*identifier, lc->language()))) {
if (identifier && (lc->character()->disp.name.eq(*identifier, lc->language()))) {
return lc;
}
}
@@ -383,13 +383,21 @@ void Lobby::on_item_id_generated_externally(uint32_t item_id) {
}
}
void Lobby::assign_inventory_item_ids(shared_ptr<Client> c) {
auto p = c->game_data.character();
void Lobby::assign_inventory_and_bank_item_ids(shared_ptr<Client> c) {
auto p = c->character();
for (size_t z = 0; z < p->inventory.num_items; z++) {
p->inventory.items[z].data.id = this->generate_item_id(c->lobby_client_id);
}
c->log.info("Assigned item IDs");
p->print_inventory(stderr, c->version(), c->require_server_state()->item_name_index);
if (c->log.info("Assigned inventory item IDs")) {
p->print_inventory(stderr, c->version(), c->require_server_state()->item_name_index);
if (p->bank.num_items) {
p->bank.assign_ids(0x99000000 + (c->lobby_client_id << 20));
c->log.info("Assigned bank item IDs");
p->print_bank(stderr, c->version(), c->require_server_state()->item_name_index);
} else {
c->log.info("Bank is empty");
}
}
}
unordered_map<uint32_t, shared_ptr<Client>> Lobby::clients_by_serial_number() const {
@@ -401,3 +409,18 @@ unordered_map<uint32_t, shared_ptr<Client>> Lobby::clients_by_serial_number() co
}
return ret;
}
QuestIndex::IncludeCondition Lobby::quest_include_condition() const {
return [this](shared_ptr<const Quest> q) -> QuestIndex::IncludeState {
bool is_enabled = true;
for (const auto& lc : this->clients) {
if (lc && !lc->can_see_quest(q, this->difficulty)) {
return QuestIndex::IncludeState::HIDDEN;
}
if (lc && !lc->can_play_quest(q, this->difficulty)) {
is_enabled = false;
}
}
return is_enabled ? QuestIndex::IncludeState::AVAILABLE : QuestIndex::IncludeState::DISABLED;
};
}
+4 -2
View File
@@ -16,7 +16,6 @@
#include "Episode3/Server.hh"
#include "ItemCreator.hh"
#include "Map.hh"
#include "Player.hh"
#include "Quest.hh"
#include "StaticGameData.hh"
#include "Text.hh"
@@ -63,6 +62,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
float z;
uint8_t floor;
};
std::shared_ptr<const Map::RareEnemyRates> rare_enemy_rates;
std::shared_ptr<Map> map;
std::array<uint32_t, 12> next_item_id;
uint32_t next_game_item_id;
@@ -190,7 +190,9 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
ItemData remove_item(uint32_t item_id);
uint32_t generate_item_id(uint8_t client_id);
void on_item_id_generated_externally(uint32_t item_id);
void assign_inventory_item_ids(std::shared_ptr<Client> c);
void assign_inventory_and_bank_item_ids(std::shared_ptr<Client> c);
QuestIndex::IncludeCondition quest_include_condition() const;
static uint8_t game_event_for_lobby_event(uint8_t lobby_event);
+83 -36
View File
@@ -363,13 +363,13 @@ static void a_encrypt_decrypt_fn(Arguments& args) {
case Version::DC_V2:
case Version::PC_V2:
case Version::GC_NTE:
crypt.reset(new PSOV2Encryption(stoul(seed, nullptr, 16)));
crypt = make_shared<PSOV2Encryption>(stoul(seed, nullptr, 16));
break;
case Version::GC_V3:
case Version::XB_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
crypt.reset(new PSOV3Encryption(stoul(seed, nullptr, 16)));
crypt = make_shared<PSOV3Encryption>(stoul(seed, nullptr, 16));
break;
case Version::BB_V4: {
string key_name = args.get<string>("key");
@@ -378,7 +378,7 @@ static void a_encrypt_decrypt_fn(Arguments& args) {
}
seed = parse_data_string(seed, nullptr, ParseDataFlags::ALLOW_FILES);
auto key = load_object_file<PSOBBEncryption::KeyFile>("system/blueburst/keys/" + key_name + ".nsk");
crypt.reset(new PSOBBEncryption(key, seed.data(), seed.size()));
crypt = make_shared<PSOBBEncryption>(key, seed.data(), seed.size());
break;
}
default:
@@ -1003,14 +1003,14 @@ Action a_encode_qst(
string pvr_filename = ends_with(bin_filename, ".bin")
? (bin_filename.substr(0, bin_filename.size() - 3) + "pvr")
: (bin_filename + ".pvr");
shared_ptr<string> bin_data(new string(load_file(bin_filename)));
shared_ptr<string> dat_data(new string(load_file(dat_filename)));
auto bin_data = make_shared<string>(load_file(bin_filename));
auto dat_data = make_shared<string>(load_file(dat_filename));
shared_ptr<string> pvr_data;
try {
pvr_data.reset(new string(load_file(pvr_filename)));
pvr_data = make_shared<string>(load_file(pvr_filename));
} catch (const cannot_open_file&) {
}
shared_ptr<VersionedQuest> vq(new VersionedQuest(0, 0, version, 0, bin_data, dat_data, pvr_data));
auto vq = make_shared<VersionedQuest>(0, 0, version, 0, bin_data, dat_data, pvr_data);
if (download) {
vq = vq->create_download_quest();
}
@@ -1040,8 +1040,7 @@ Action a_disassemble_quest_map(
"disassemble-quest-map", "\
disassemble-quest-map [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Disassemble the input quest map (.dat file) into a text representation of\n\
the data it contains. Specify the quest\'s game version with one of the\n\
--dc-nte, --dc-v1, --dc-v2, --pc, --gc-nte, --gc, --xb, or --bb options.\n",
the data it contains.\n",
+[](Arguments& args) {
string data = read_input_data(args);
if (!args.get<bool>("decompressed")) {
@@ -1051,6 +1050,22 @@ Action a_disassemble_quest_map(
write_output_data(args, result.data(), result.size(), "txt");
});
Action a_assemble_quest_script(
"assemble-quest-script", "\
assemble-quest-script [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Assemble the input quest script (.txt file) into a compressed .bin file\n\
usable as an online quest script. If --decompressed is given, produces an\n\
uncompressed .bind file instead.\n",
+[](Arguments& args) {
string text = read_input_data(args);
string result = assemble_quest_script(text);
bool compress = !args.get<bool>("decompressed");
if (compress) {
result = prs_compress_optimal(result);
}
write_output_data(args, result.data(), result.size(), compress ? "bin" : "bind");
});
void a_extract_archive_fn(Arguments& args) {
string output_prefix = args.get<string>(2, false);
if (output_prefix == "-") {
@@ -1064,7 +1079,7 @@ void a_extract_archive_fn(Arguments& args) {
}
string data = read_input_data(args);
shared_ptr<string> data_shared(new string(std::move(data)));
auto data_shared = make_shared<string>(std::move(data));
if (args.get<string>(0) == "extract-afs") {
AFSArchive arch(data_shared);
@@ -1213,8 +1228,8 @@ Action a_cat_client(
if (key_file_name.empty()) {
throw runtime_error("a key filename is required for BB client emulation");
}
key.reset(new PSOBBEncryption::KeyFile(
load_object_file<PSOBBEncryption::KeyFile>("system/blueburst/keys/" + key_file_name + ".nsk")));
key = make_shared<PSOBBEncryption::KeyFile>(
load_object_file<PSOBBEncryption::KeyFile>("system/blueburst/keys/" + key_file_name + ".nsk"));
}
shared_ptr<struct event_base> base(event_base_new(), event_base_free);
auto cat_client_remote = make_sockaddr_storage(parse_netloc(args.get<string>(1))).first;
@@ -1246,18 +1261,18 @@ Action a_convert_rare_item_set(
}
auto version = get_cli_version(args);
shared_ptr<string> data(new string(read_input_data(args)));
auto data = make_shared<string>(read_input_data(args));
shared_ptr<RareItemSet> rs;
if (ends_with(input_filename, ".json")) {
rs.reset(new RareItemSet(JSON::parse(*data), version, name_index));
rs = make_shared<RareItemSet>(JSON::parse(*data), version, name_index);
} else if (ends_with(input_filename, ".gsl")) {
rs.reset(new RareItemSet(GSLArchive(data, false), false));
rs = make_shared<RareItemSet>(GSLArchive(data, false), false);
} else if (ends_with(input_filename, ".gslb")) {
rs.reset(new RareItemSet(GSLArchive(data, true), true));
rs = make_shared<RareItemSet>(GSLArchive(data, true), true);
} else if (ends_with(input_filename, ".afs")) {
rs.reset(new RareItemSet(AFSArchive(data), is_v1(version)));
rs = make_shared<RareItemSet>(AFSArchive(data), is_v1(version));
} else if (ends_with(input_filename, ".rel")) {
rs.reset(new RareItemSet(*data, true));
rs = make_shared<RareItemSet>(*data, true);
} else {
throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, .afs, or .rel");
}
@@ -1296,11 +1311,11 @@ Action a_describe_item(
JSON::parse(load_file("system/item-tables/names-v2.json")),
JSON::parse(load_file("system/item-tables/names-v3.json")),
JSON::parse(load_file("system/item-tables/names-v4.json")));
shared_ptr<string> pmt_data_v2(new string(prs_decompress(load_file("system/item-tables/ItemPMT-v2.prs"))));
auto pmt_data_v2 = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemPMT-v2.prs")));
auto pmt_v2 = make_shared<ItemParameterTable>(pmt_data_v2, ItemParameterTable::Version::V2);
shared_ptr<string> pmt_data_v3(new string(prs_decompress(load_file("system/item-tables/ItemPMT-gc.prs"))));
auto pmt_data_v3 = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemPMT-gc.prs")));
auto pmt_v3 = make_shared<ItemParameterTable>(pmt_data_v3, ItemParameterTable::Version::V3);
shared_ptr<string> pmt_data_v4(new string(prs_decompress(load_file("system/item-tables/ItemPMT-bb.prs"))));
auto pmt_data_v4 = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemPMT-bb.prs")));
auto pmt_v4 = make_shared<ItemParameterTable>(pmt_data_v4, ItemParameterTable::Version::V4);
ItemData item = name_index->parse_item_description(version, description);
@@ -1375,7 +1390,7 @@ Action a_show_ep3_cards(
unique_ptr<TextArchive> text_english;
try {
JSON json = JSON::parse(load_file("system/ep3/text-english.json"));
text_english.reset(new TextArchive(json));
text_english = make_unique<TextArchive>(json);
} catch (const exception& e) {
}
@@ -1426,7 +1441,7 @@ Action a_generate_ep3_cards_html(
unique_ptr<TextArchive> text_english;
try {
JSON json = JSON::parse(load_file("system/ep3/text-english.json"));
text_english.reset(new TextArchive(json));
text_english = make_unique<TextArchive>(json);
} catch (const exception& e) {
}
@@ -1574,6 +1589,34 @@ Action a_show_ep3_maps(
}
});
Action a_show_battle_params(
"show-battle-params", "\
show-battle-params\n\
Print the Blue Burst battle parameters from the system/blueburst directory\n\
in a human-readable format.\n",
+[](Arguments&) {
BattleParamsIndex index(
make_shared<string>(load_file("system/blueburst/BattleParamEntry_on.dat")),
make_shared<string>(load_file("system/blueburst/BattleParamEntry_lab_on.dat")),
make_shared<string>(load_file("system/blueburst/BattleParamEntry_ep4_on.dat")),
make_shared<string>(load_file("system/blueburst/BattleParamEntry.dat")),
make_shared<string>(load_file("system/blueburst/BattleParamEntry_lab.dat")),
make_shared<string>(load_file("system/blueburst/BattleParamEntry_ep4.dat")));
fprintf(stdout, "Episode 1 multi\n");
index.get_table(false, Episode::EP1).print(stdout);
fprintf(stdout, "Episode 1 solo\n");
index.get_table(true, Episode::EP1).print(stdout);
fprintf(stdout, "Episode 2 multi\n");
index.get_table(false, Episode::EP2).print(stdout);
fprintf(stdout, "Episode 2 solo\n");
index.get_table(true, Episode::EP2).print(stdout);
fprintf(stdout, "Episode 4 multi\n");
index.get_table(false, Episode::EP4).print(stdout);
fprintf(stdout, "Episode 4 solo\n");
index.get_table(true, Episode::EP4).print(stdout);
});
Action a_parse_object_graph(
"parse-object-graph", nullptr, +[](Arguments& args) {
uint32_t root_object_address = args.get<uint32_t>("root", Arguments::IntFormat::HEX);
@@ -1698,14 +1741,13 @@ Action a_run_server_replay_log(
}
shared_ptr<struct event_base> base(event_base_new(), event_base_free);
shared_ptr<ServerState> state(new ServerState(config_filename, is_replay));
auto state = make_shared<ServerState>(base, config_filename, is_replay);
state->init();
shared_ptr<DNSServer> dns_server;
if (state->dns_server_port && !is_replay) {
config_log.info("Starting DNS server on port %hu", state->dns_server_port);
dns_server.reset(new DNSServer(base, state->local_address,
state->external_address));
dns_server = make_shared<DNSServer>(base, state->local_address, state->external_address);
dns_server->listen("", state->dns_server_port);
} else {
config_log.info("DNS server is disabled");
@@ -1716,9 +1758,9 @@ Action a_run_server_replay_log(
shared_ptr<IPStackSimulator> ip_stack_simulator;
if (is_replay) {
config_log.info("Starting proxy server");
state->proxy_server.reset(new ProxyServer(base, state));
state->proxy_server = make_shared<ProxyServer>(base, state);
config_log.info("Starting game server");
state->game_server.reset(new Server(base, state));
state->game_server = make_shared<Server>(base, state);
auto nop_destructor = +[](FILE*) {};
shared_ptr<FILE> log_f(stdin, nop_destructor);
@@ -1726,7 +1768,7 @@ Action a_run_server_replay_log(
log_f = fopen_shared(replay_log_filename, "rt");
}
replay_session.reset(new ReplaySession(base, log_f.get(), state, args.get<bool>("require-basic-credentials")));
replay_session = make_shared<ReplaySession>(base, log_f.get(), state, args.get<bool>("require-basic-credentials"));
replay_session->start();
} else {
@@ -1736,7 +1778,7 @@ Action a_run_server_replay_log(
if (pc->behavior == ServerBehavior::PROXY_SERVER) {
if (!state->proxy_server.get()) {
config_log.info("Starting proxy server");
state->proxy_server.reset(new ProxyServer(base, state));
state->proxy_server = make_shared<ProxyServer>(base, state);
}
if (state->proxy_server.get()) {
// For PC and GC, proxy sessions are dynamically created when a client
@@ -1761,20 +1803,25 @@ Action a_run_server_replay_log(
} else {
if (!state->game_server.get()) {
config_log.info("Starting game server");
state->game_server.reset(new Server(base, state));
state->game_server = make_shared<Server>(base, state);
}
string spec = string_printf("T-%hu-%s-%s-%s", pc->port, name_for_enum(pc->version), pc->name.c_str(), name_for_enum(pc->behavior));
state->game_server->listen(spec, "", pc->port, pc->version, pc->behavior);
}
}
if (!state->ip_stack_addresses.empty()) {
config_log.info("Starting IP stack simulator");
ip_stack_simulator.reset(new IPStackSimulator(base, state));
if (!state->ip_stack_addresses.empty() || !state->ppp_stack_addresses.empty()) {
config_log.info("Starting IP/PPP stack simulator");
ip_stack_simulator = make_shared<IPStackSimulator>(base, state);
for (const auto& it : state->ip_stack_addresses) {
auto netloc = parse_netloc(it);
string spec = (netloc.second == 0) ? ("T-IPS-" + netloc.first) : string_printf("T-IPS-%hu", netloc.second);
ip_stack_simulator->listen(spec, netloc.first, netloc.second);
ip_stack_simulator->listen(spec, netloc.first, netloc.second, FrameInfo::LinkType::ETHERNET);
}
for (const auto& it : state->ppp_stack_addresses) {
auto netloc = parse_netloc(it);
string spec = (netloc.second == 0) ? ("T-PPPS-" + netloc.first) : string_printf("T-PPPS-%hu", netloc.second);
ip_stack_simulator->listen(spec, netloc.first, netloc.second, FrameInfo::LinkType::HDLC);
}
}
}
@@ -1796,7 +1843,7 @@ Action a_run_server_replay_log(
should_run_shell = !replay_session.get();
}
if (should_run_shell) {
shell.reset(new ServerShell(base, state));
shell = make_shared<ServerShell>(base, state);
}
config_log.info("Ready");
+139 -138
View File
@@ -14,6 +14,39 @@ using namespace std;
static constexpr float UINT32_MAX_AS_FLOAT = 4294967296.0f;
Map::RareEnemyRates::RareEnemyRates(uint32_t enemy_rate, uint32_t boss_rate)
: hildeblue(enemy_rate),
rappy(enemy_rate),
nar_lily(enemy_rate),
pouilly_slime(enemy_rate),
merissa_aa(enemy_rate),
pazuzu(enemy_rate),
dorphon_eclair(enemy_rate),
kondrieu(boss_rate) {}
Map::RareEnemyRates::RareEnemyRates(const JSON& json)
: hildeblue(json.get_int("Hildeblue")),
rappy(json.get_int("Rappy")),
nar_lily(json.get_int("NarLily")),
pouilly_slime(json.get_int("PouillySlime")),
merissa_aa(json.get_int("MerissaAA")),
pazuzu(json.get_int("Pazuzu")),
dorphon_eclair(json.get_int("DorphonEclair")),
kondrieu(json.get_int("Kondrieu")) {}
JSON Map::RareEnemyRates::json() const {
return JSON::dict({
{"Hildeblue", this->hildeblue},
{"Rappy", this->rappy},
{"NarLily", this->nar_lily},
{"PouillySlime", this->pouilly_slime},
{"MerissaAA", this->merissa_aa},
{"Pazuzu", this->pazuzu},
{"DorphonEclair", this->dorphon_eclair},
{"Kondrieu", this->kondrieu},
});
}
string Map::ObjectEntry::str() const {
return string_printf("[ObjectEntry type=%04hX flags=%04hX index=%04hX a2=%04hX entity_id=%04hX group=%04hX section=%04hX a3=%04hX x=%g y=%g z=%g x_angle=%08" PRIX32 " y_angle=%08" PRIX32 " z_angle=%08" PRIX32 " params=[%g %g %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] unused=%08" PRIX32 "]",
this->base_type.load(),
@@ -76,8 +109,8 @@ Map::Enemy::Enemy(size_t source_index, uint8_t floor, EnemyType type)
}
string Map::Enemy::str() const {
return string_printf("[Map::Enemy source %zX %s flags=%02hhX last_hit_by_client_id=%hu]",
this->source_index, name_for_enum(this->type), this->state_flags, this->last_hit_by_client_id);
return string_printf("[Map::Enemy source %zX %s floor=%02hhX flags=%02hhX last_hit_by_client_id=%hu]",
this->source_index, name_for_enum(this->type), this->floor, this->state_flags, this->last_hit_by_client_id);
}
string Map::Object::str(shared_ptr<const ItemNameIndex> name_index) const {
@@ -98,6 +131,9 @@ string Map::Object::str(shared_ptr<const ItemNameIndex> name_index) const {
}
}
Map::Map(uint32_t lobby_id)
: log(string_printf("[Lobby:%08" PRIX32 ":map] ", lobby_id), lobby_log.min_level) {}
void Map::clear() {
this->objects.clear();
this->enemies.clear();
@@ -145,7 +181,7 @@ void Map::add_enemy(
uint8_t floor,
size_t index,
const EnemyEntry& e,
const RareEnemyRates& rare_rates) {
std::shared_ptr<const RareEnemyRates> rare_rates) {
auto add = [&](EnemyType type) -> void {
this->enemies.emplace_back(index, floor, type);
};
@@ -213,17 +249,18 @@ void Map::add_enemy(
case 0x00FD: // TObjNpcNgcBase
case 0x00FE: // TObjNpcNgcBase
case 0x00FF: // TObjNpcNgcBase
case 0x0100: // Unknown NPC
// All of these have a default child count of zero
add(EnemyType::NON_ENEMY_NPC);
break;
case 0x0040: // TObjEneMoja
add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.hildeblue)
add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->hildeblue)
? EnemyType::HILDEBLUE
: EnemyType::HILDEBEAR);
break;
case 0x0041: { // TObjEneLappy
bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.rappy);
bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->rappy);
switch (episode) {
case Episode::EP1:
add(is_rare ? EnemyType::AL_RAPPY : EnemyType::RAG_RAPPY);
@@ -261,6 +298,7 @@ void Map::add_enemy(
}
case 0x0042: // TObjEneBm3FlyNest
add(EnemyType::MONEST);
child_type = EnemyType::MOTHMANT;
default_num_children = 30;
break;
case 0x0043: // TObjEneBm5Wolf
@@ -278,7 +316,7 @@ void Map::add_enemy(
if ((episode == Episode::EP2) && (e.floor > 0x0F)) {
add(EnemyType::DEL_LILY);
} else {
add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.nar_lily)
add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->nar_lily)
? EnemyType::NAR_LILY
: EnemyType::POISON_LILY);
}
@@ -292,14 +330,14 @@ void Map::add_enemy(
break;
}
case 0x0064: // TObjEneSlime
add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.pouilly_slime)
? EnemyType::POFUILLY_SLIME
: EnemyType::POUILLY_SLIME);
add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->pouilly_slime)
? EnemyType::POUILLY_SLIME
: EnemyType::POFUILLY_SLIME);
default_num_children = 4;
break;
case 0x0065: // TObjEnePanarms
if ((e.num_children != 0) && (e.num_children != 2)) {
static_game_data_log.warning("PAN_ARMS has an unusual num_children (0x%hX)", e.num_children.load());
this->log.warning("PAN_ARMS has an unusual num_children (0x%hX)", e.num_children.load());
}
default_num_children = -1; // Skip adding children (because we do it here)
add(EnemyType::PAN_ARMS);
@@ -332,7 +370,7 @@ void Map::add_enemy(
break;
case 0x00A1: // TObjEneRe4Sorcerer
if ((e.num_children != 0) && (e.num_children != 2)) {
static_game_data_log.warning("CHAOS_SORCERER has an unusual num_children (0x%hX)", e.num_children.load());
this->log.warning("CHAOS_SORCERER has an unusual num_children (0x%hX)", e.num_children.load());
}
default_num_children = -1; // Skip adding children (because we do it here)
add(EnemyType::CHAOS_SORCERER);
@@ -375,7 +413,7 @@ void Map::add_enemy(
break;
case 0x00C1: // TBoss2DeRolLe
if ((e.num_children != 0) && (e.num_children != 0x13)) {
static_game_data_log.warning("DE_ROL_LE has an unusual num_children (0x%hX)", e.num_children.load());
this->log.warning("DE_ROL_LE has an unusual num_children (0x%hX)", e.num_children.load());
}
default_num_children = -1; // Skip adding children (because we do it here)
add(EnemyType::DE_ROL_LE);
@@ -388,7 +426,7 @@ void Map::add_enemy(
break;
case 0x00C2: // TBoss3Volopt
if ((e.num_children != 0) && (e.num_children != 0x23)) {
static_game_data_log.warning("VOL_OPT has an unusual num_children (0x%hX)", e.num_children.load());
this->log.warning("VOL_OPT has an unusual num_children (0x%hX)", e.num_children.load());
}
default_num_children = -1; // Skip adding children (because we do it here)
add(EnemyType::VOL_OPT_1);
@@ -410,7 +448,7 @@ void Map::add_enemy(
break;
case 0x00C8: // TBoss4DarkFalz
if ((e.num_children != 0) && (e.num_children != 0x200)) {
static_game_data_log.warning("DARK_FALZ has an unusual num_children (0x%hX)", e.num_children.load());
this->log.warning("DARK_FALZ has an unusual num_children (0x%hX)", e.num_children.load());
}
default_num_children = -1; // Skip adding children (because we do it here)
if (difficulty) {
@@ -503,7 +541,7 @@ void Map::add_enemy(
}
break;
case 0x0112:
add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.merissa_aa)
add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->merissa_aa)
? EnemyType::MERISSA_AA
: EnemyType::MERISSA_A);
break;
@@ -511,7 +549,7 @@ void Map::add_enemy(
add(EnemyType::GIRTABLULU);
break;
case 0x0114: {
bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.pazuzu);
bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->pazuzu);
if (e.floor > 0x05) {
add(is_rare ? EnemyType::PAZUZU_ALT : EnemyType::ZU_ALT);
} else {
@@ -527,7 +565,7 @@ void Map::add_enemy(
}
break;
case 0x0116:
add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates.dorphon_eclair)
add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->dorphon_eclair)
? EnemyType::DORPHON_ECLAIR
: EnemyType::DORPHON);
break;
@@ -537,7 +575,7 @@ void Map::add_enemy(
break;
}
case 0x0119: {
bool is_rare = this->check_and_log_rare_enemy((e.fparam2 != 0.0f), rare_rates.kondrieu);
bool is_rare = this->check_and_log_rare_enemy((e.fparam2 != 0.0f), rare_rates->kondrieu);
if (is_rare) {
add(EnemyType::KONDRIEU);
} else {
@@ -551,16 +589,16 @@ void Map::add_enemy(
case 0x00C4: // TBoss3VoloptCore or subclass
case 0x00C6: // TBoss3VoloptMonitor
case 0x00C7: // TBoss3VoloptHiraisin
case 0x0100:
case 0x0118:
add(EnemyType::UNKNOWN);
static_game_data_log.warning(
this->log.warning(
"(Entry %zu, offset %zX in file) Unknown enemy type %04hX",
index, index * sizeof(EnemyEntry), e.base_type.load());
break;
default:
add(EnemyType::UNKNOWN);
static_game_data_log.warning(
this->log.warning(
"(Entry %zu, offset %zX in file) Invalid enemy type %04hX",
index, index * sizeof(EnemyEntry), e.base_type.load());
break;
@@ -584,7 +622,7 @@ void Map::add_enemies_from_map_data(
uint8_t floor,
const void* data,
size_t size,
const RareEnemyRates& rare_rates) {
std::shared_ptr<const RareEnemyRates> rare_rates) {
size_t entry_count = size / sizeof(EnemyEntry);
if (size != entry_count * sizeof(EnemyEntry)) {
throw runtime_error("data size is not a multiple of entry size");
@@ -596,78 +634,70 @@ void Map::add_enemies_from_map_data(
}
}
struct DATParserRandomState {
PSOV2Encryption random;
PSOV2Encryption location_table_random;
std::array<uint32_t, 0x20> location_index_table;
uint32_t location_indexes_populated;
uint32_t location_indexes_used;
uint32_t location_entries_base_offset;
Map::DATParserRandomState::DATParserRandomState(uint32_t rare_seed)
: random(rare_seed),
location_table_random(0),
location_indexes_populated(0),
location_indexes_used(0),
location_entries_base_offset(0) {
this->location_index_table.fill(0);
}
DATParserRandomState(uint32_t rare_seed)
: random(rare_seed),
location_table_random(0),
location_indexes_populated(0),
location_indexes_used(0),
location_entries_base_offset(0) {
this->location_index_table.fill(0);
size_t Map::DATParserRandomState::rand_int_biased(size_t min_v, size_t max_v) {
float max_f = static_cast<float>(max_v + 1);
uint32_t crypt_v = this->random.next();
float det_f = static_cast<float>(crypt_v);
return max<size_t>(floorf((max_f * det_f) / UINT32_MAX_AS_FLOAT), min_v);
}
uint32_t Map::DATParserRandomState::next_location_index() {
if (this->location_indexes_used < this->location_indexes_populated) {
return this->location_index_table.at(this->location_indexes_used++);
}
return 0;
}
void Map::DATParserRandomState::generate_shuffled_location_table(
const Map::RandomEnemyLocationsHeader& header, StringReader r, uint16_t section) {
if (header.num_sections == 0) {
throw runtime_error("no locations defined");
}
size_t rand_int_biased(size_t min_v, size_t max_v) {
float max_f = static_cast<float>(max_v + 1);
uint32_t crypt_v = this->random.next();
float det_f = static_cast<float>(crypt_v);
return max<size_t>(floorf((max_f * det_f) / UINT32_MAX_AS_FLOAT), min_v);
StringReader sections_r = r.sub(header.section_table_offset, header.num_sections * sizeof(Map::RandomEnemyLocationSection));
size_t bs_min = 0;
size_t bs_max = header.num_sections - 1;
do {
size_t bs_mid = (bs_min + bs_max) / 2;
if (sections_r.pget<Map::RandomEnemyLocationSection>(bs_mid * sizeof(Map::RandomEnemyLocationSection)).section < section) {
bs_min = bs_mid + 1;
} else {
bs_max = bs_mid;
}
} while (bs_min < bs_max);
const auto& sec = sections_r.pget<Map::RandomEnemyLocationSection>(bs_min * sizeof(Map::RandomEnemyLocationSection));
if (section != sec.section) {
return;
}
uint32_t next_location_index() {
if (this->location_indexes_used < this->location_indexes_populated) {
return this->location_index_table.at(this->location_indexes_used++);
}
return 0;
this->location_indexes_populated = sec.count;
this->location_indexes_used = 0;
this->location_entries_base_offset = sec.offset;
for (size_t z = 0; z < sec.count; z++) {
this->location_index_table.at(z) = z;
}
void generate_shuffled_location_table(const Map::RandomEnemyLocationsHeader& header, StringReader r, uint16_t section) {
if (header.num_sections == 0) {
throw runtime_error("no locations defined");
}
StringReader sections_r = r.sub(header.section_table_offset, header.num_sections * sizeof(Map::RandomEnemyLocationSection));
size_t bs_min = 0;
size_t bs_max = header.num_sections - 1;
do {
size_t bs_mid = (bs_min + bs_max) / 2;
if (sections_r.pget<Map::RandomEnemyLocationSection>(bs_mid * sizeof(Map::RandomEnemyLocationSection)).section < section) {
bs_min = bs_mid + 1;
} else {
bs_max = bs_mid;
}
} while (bs_min < bs_max);
const auto& sec = sections_r.pget<Map::RandomEnemyLocationSection>(bs_min * sizeof(Map::RandomEnemyLocationSection));
if (section != sec.section) {
return;
}
this->location_indexes_populated = sec.count;
this->location_indexes_used = 0;
this->location_entries_base_offset = sec.offset;
for (size_t z = 0; z < sec.count; z++) {
this->location_index_table.at(z) = z;
}
for (size_t z = 0; z < 4; z++) {
for (size_t x = 0; x < sec.count; x++) {
uint32_t crypt_v = this->location_table_random.next();
size_t choice = floorf((static_cast<float>(sec.count) * static_cast<float>(crypt_v)) / UINT32_MAX_AS_FLOAT);
uint32_t t = this->location_index_table[x];
this->location_index_table[x] = this->location_index_table[choice];
this->location_index_table[choice] = t;
}
for (size_t z = 0; z < 4; z++) {
for (size_t x = 0; x < sec.count; x++) {
uint32_t crypt_v = this->location_table_random.next();
size_t choice = floorf((static_cast<float>(sec.count) * static_cast<float>(crypt_v)) / UINT32_MAX_AS_FLOAT);
uint32_t t = this->location_index_table[x];
this->location_index_table[x] = this->location_index_table[choice];
this->location_index_table[choice] = t;
}
}
};
}
void Map::add_random_enemies_from_map_data(
Episode episode,
@@ -677,8 +707,8 @@ void Map::add_random_enemies_from_map_data(
StringReader wave_events_segment_r,
StringReader locations_segment_r,
StringReader definitions_segment_r,
uint32_t rare_seed,
const RareEnemyRates& rare_rates) {
std::shared_ptr<DATParserRandomState> random_state,
std::shared_ptr<const RareEnemyRates> rare_rates) {
static const array<uint32_t, 41> rand_enemy_base_types = {
0x44, 0x43, 0x41, 0x42, 0x40, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x80,
@@ -701,15 +731,11 @@ void Map::add_random_enemies_from_map_data(
definitions_header.weight_entries_offset,
definitions_header.weight_entry_count * sizeof(RandomEnemyWeight));
DATParserRandomState random(rare_seed);
for (size_t wave_entry_index = 0; wave_entry_index < wave_events_header.entry_count; wave_entry_index++) {
auto entry_log = static_game_data_log.sub(string_printf("(Entry %zu/%" PRIu32 ") ", wave_entry_index, wave_events_header.entry_count.load()));
entry_log.info("Start");
auto entry_log = this->log.sub(string_printf("(Entry %zu/%" PRIu32 ") ", wave_entry_index, wave_events_header.entry_count.load()));
const auto& entry = wave_events_segment_r.get<Event2Entry>();
size_t remaining_waves = random.rand_int_biased(1, entry.max_waves);
entry_log.info("Chose %zu waves (max=%hu)", remaining_waves, entry.max_waves.load());
size_t remaining_waves = random_state->rand_int_biased(1, entry.max_waves);
// Trace: at 0080E125 EAX is wave count
uint32_t wave_number = entry.wave_number;
@@ -717,15 +743,10 @@ void Map::add_random_enemies_from_map_data(
remaining_waves--;
auto wave_log = entry_log.sub(string_printf("(Wave %zu) ", remaining_waves));
size_t remaining_enemies = random.rand_int_biased(entry.min_enemies, entry.max_enemies);
wave_log.info("Chose %zu enemies (range=[%hhu, %hhu])", remaining_enemies, entry.min_enemies, entry.max_enemies);
size_t remaining_enemies = random_state->rand_int_biased(entry.min_enemies, entry.max_enemies);
// Trace: at 0080E208 EDI is enemy count
random.generate_shuffled_location_table(locations_header, locations_segment_r, entry.section);
wave_log.info("Generated shuffled location table");
for (size_t z = 0; z < random.location_indexes_populated; z++) {
wave_log.info(" table[%zX] = %" PRIX32, z, random.location_index_table[z]);
}
random_state->generate_shuffled_location_table(locations_header, locations_segment_r, entry.section);
// Trace: at 0080EBB0 *(EBP + 4) points to table (0x20 uint32_ts)
while (remaining_enemies) {
@@ -740,8 +761,7 @@ void Map::add_random_enemies_from_map_data(
}
// Trace: at 0080E2C2 EBX is weight_total
size_t det = random.rand_int_biased(0, weight_total - 1);
enemy_log.info("weight_total=%zX, det=%zX", weight_total, det);
size_t det = random_state->rand_int_biased(0, weight_total - 1);
// Trace: at 0080E300 EDX is det
weights_r.go(0);
@@ -778,13 +798,13 @@ void Map::add_random_enemies_from_map_data(
e.fparam5 = def.fparam5;
e.uparam1 = def.uparam1;
e.uparam2 = def.uparam2;
e.num_children = random.rand_int_biased(def.min_children, def.max_children);
e.num_children = random_state->rand_int_biased(def.min_children, def.max_children);
} else {
throw runtime_error("random enemy definition not found");
}
const auto& loc = locations_segment_r.pget<RandomEnemyLocationEntry>(
locations_header.entries_offset + sizeof(RandomEnemyLocationEntry) * random.next_location_index());
locations_header.entries_offset + sizeof(RandomEnemyLocationEntry) * random_state->next_location_index());
e.x = loc.x;
e.y = loc.y;
e.z = loc.z;
@@ -792,11 +812,10 @@ void Map::add_random_enemies_from_map_data(
e.y_angle = loc.y_angle;
e.z_angle = loc.z_angle;
enemy_log.info("Creating enemy with base_type %04hX fparam2 %g uparam1 %04hX", e.base_type.load(), e.fparam2.load(), e.uparam1.load());
// Trace: at 0080E6FE CX is base_type
this->add_enemy(episode, difficulty, event, floor, 0, e, rare_rates);
} else {
enemy_log.info("Cannot create enemy: parameters are missing");
enemy_log.warning("Cannot create enemy: parameters are missing");
}
break;
} else {
@@ -809,13 +828,13 @@ void Map::add_random_enemies_from_map_data(
// doing so, it uses one value from random to determine the delay
// parameter of the event. To keep our state in sync with what the
// client would do, we skip a random value here.
random.random.next();
random_state->random.next();
wave_number++;
}
}
// For the same reason as above, we need to skip another random value here.
random.random.next();
random_state->random.next();
}
}
@@ -825,8 +844,6 @@ vector<Map::DATSectionsForFloor> Map::collect_quest_map_data_sections(const void
while (!r.eof()) {
size_t header_offset = r.where();
const auto& header = r.get<SectionHeader>();
static_game_data_log.info("(DAT:%08zX) type=%08" PRIX32 " floor=%08" PRIX32 " data_size=%08" PRIX32,
header_offset, header.le_type.load(), header.floor.load(), header.data_size.load());
if (header.type() == SectionHeader::Type::END && header.section_size == 0) {
break;
@@ -889,10 +906,11 @@ void Map::add_enemies_and_objects_from_quest_data(
const void* data,
size_t size,
uint32_t rare_seed,
const RareEnemyRates& rare_rates) {
std::shared_ptr<const RareEnemyRates> rare_rates) {
auto all_floor_sections = this->collect_quest_map_data_sections(data, size);
StringReader r(data, size);
shared_ptr<DATParserRandomState> random_state;
for (size_t floor = 0; floor < all_floor_sections.size(); floor++) {
const auto& floor_sections = all_floor_sections[floor];
@@ -901,7 +919,7 @@ void Map::add_enemies_and_objects_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");
}
static_game_data_log.info("(Floor %02zX) Adding objects", floor);
this->log.info("(Floor %02zX) Adding objects", floor);
this->add_objects_from_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size);
}
@@ -910,7 +928,7 @@ void Map::add_enemies_and_objects_from_quest_data(
if (header.data_size % sizeof(EnemyEntry)) {
throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size");
}
static_game_data_log.info("(Floor %02zX) Adding enemies", floor);
this->log.info("(Floor %02zX) Adding enemies", floor);
this->add_enemies_from_map_data(
episode,
difficulty,
@@ -923,10 +941,13 @@ void Map::add_enemies_and_objects_from_quest_data(
} else if ((floor_sections.wave_events != 0xFFFFFFFF) &&
(floor_sections.random_enemy_locations != 0xFFFFFFFF) &&
(floor_sections.random_enemy_definitions != 0xFFFFFFFF)) {
static_game_data_log.info("(Floor %02zX) Adding random enemies", floor);
this->log.info("(Floor %02zX) Adding random enemies", floor);
const auto& wave_events_header = r.pget<SectionHeader>(floor_sections.wave_events);
const auto& random_enemy_locations_header = r.pget<SectionHeader>(floor_sections.random_enemy_locations);
const auto& random_enemy_definitions_header = r.pget<SectionHeader>(floor_sections.random_enemy_definitions);
if (!random_state) {
random_state = make_shared<DATParserRandomState>(rare_seed);
}
this->add_random_enemies_from_map_data(
episode,
difficulty,
@@ -935,7 +956,7 @@ void Map::add_enemies_and_objects_from_quest_data(
r.sub(floor_sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size),
r.sub(floor_sections.random_enemy_locations + sizeof(SectionHeader), random_enemy_locations_header.data_size),
r.sub(floor_sections.random_enemy_definitions + sizeof(SectionHeader), random_enemy_definitions_header.data_size),
rare_seed,
random_state,
rare_rates);
}
}
@@ -1007,7 +1028,7 @@ string Map::disassemble_quest_data(const void* data, size_t size) {
}
}
return join(ret, "\n");
return join(ret, "\n") + "\n";
}
SetDataTable::SetDataTable(shared_ptr<const string> data, bool big_endian) {
@@ -1311,25 +1332,5 @@ vector<string> map_filenames_for_variation(
return ret;
}
const Map::RareEnemyRates Map::NO_RARE_ENEMIES = {
.hildeblue = 0x00000000,
.rappy = 0x00000000,
.nar_lily = 0x00000000,
.pouilly_slime = 0x00000000,
.merissa_aa = 0x00000000,
.pazuzu = 0x00000000,
.dorphon_eclair = 0x00000000,
.kondrieu = 0x00000000,
};
const Map::RareEnemyRates Map::DEFAULT_RARE_ENEMIES = {
// All 1/512 except Kondrieu, which is 1/10
.hildeblue = 0x00800000,
.rappy = 0x00800000,
.nar_lily = 0x00800000,
.pouilly_slime = 0x00800000,
.merissa_aa = 0x00800000,
.pazuzu = 0x00800000,
.dorphon_eclair = 0x00800000,
.kondrieu = 0x1999999A,
};
const shared_ptr<const Map::RareEnemyRates> Map::NO_RARE_ENEMIES = make_shared<Map::RareEnemyRates>(0, 0);
const shared_ptr<const Map::RareEnemyRates> Map::DEFAULT_RARE_ENEMIES = make_shared<Map::RareEnemyRates>(0x0083126E, 0x1999999A);
+34 -11
View File
@@ -4,6 +4,7 @@
#include <memory>
#include <phosg/Encoding.hh>
#include <phosg/JSON.hh>
#include <random>
#include <string>
#include <vector>
@@ -192,10 +193,15 @@ struct Map {
uint32_t pazuzu; // ZU -> PAZUZU (and _ALT variants)
uint32_t dorphon_eclair; // DORPHON -> DORPHON_ECLAIR
uint32_t kondrieu; // {SAINT_MILLION, SHAMBERTIN} -> KONDRIEU
RareEnemyRates(uint32_t enemy_rate, uint32_t boss_rate);
explicit RareEnemyRates(const JSON& json);
JSON json() const;
};
static const RareEnemyRates NO_RARE_ENEMIES;
static const RareEnemyRates DEFAULT_RARE_ENEMIES;
static const std::shared_ptr<const RareEnemyRates> NO_RARE_ENEMIES;
static const std::shared_ptr<const RareEnemyRates> DEFAULT_RARE_ENEMIES;
struct Object {
// TODO: Add more fields in here if we ever care about them. Currently we
@@ -233,9 +239,22 @@ struct Map {
std::string str() const;
} __attribute__((packed));
std::vector<Object> objects;
std::vector<Enemy> enemies;
std::vector<size_t> rare_enemy_indexes;
struct DATParserRandomState {
PSOV2Encryption random;
PSOV2Encryption location_table_random;
std::array<uint32_t, 0x20> location_index_table;
uint32_t location_indexes_populated;
uint32_t location_indexes_used;
uint32_t location_entries_base_offset;
DATParserRandomState(uint32_t rare_seed);
size_t rand_int_biased(size_t min_v, size_t max_v);
uint32_t next_location_index();
void generate_shuffled_location_table(const Map::RandomEnemyLocationsHeader& header, StringReader r, uint16_t section);
};
explicit Map(uint32_t lobby_id);
~Map() = default;
void clear();
@@ -249,7 +268,7 @@ struct Map {
uint8_t floor,
size_t index,
const EnemyEntry& e,
const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES);
std::shared_ptr<const RareEnemyRates> rare_rates = DEFAULT_RARE_ENEMIES);
void add_enemies_from_map_data(
Episode episode,
uint8_t difficulty,
@@ -257,7 +276,7 @@ struct Map {
uint8_t floor,
const void* data,
size_t size,
const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES);
std::shared_ptr<const RareEnemyRates> rare_rates = DEFAULT_RARE_ENEMIES);
void add_random_enemies_from_map_data(
Episode episode,
uint8_t difficulty,
@@ -266,8 +285,8 @@ struct Map {
StringReader wave_events_r,
StringReader random_enemy_locations_r,
StringReader random_enemy_definitions_r,
uint32_t rare_seed,
const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES);
std::shared_ptr<DATParserRandomState> random_state,
std::shared_ptr<const RareEnemyRates> rare_rates = DEFAULT_RARE_ENEMIES);
struct DATSectionsForFloor {
uint32_t objects = 0xFFFFFFFF;
@@ -285,9 +304,14 @@ struct Map {
const void* data,
size_t size,
uint32_t rare_seed,
const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES);
std::shared_ptr<const RareEnemyRates> rare_rates = Map::DEFAULT_RARE_ENEMIES);
static std::string disassemble_quest_data(const void* data, size_t size);
PrefixedLogger log;
std::vector<Object> objects;
std::vector<Enemy> enemies;
std::vector<size_t> rare_enemy_indexes;
};
// TODO: This class is currently unused. It would be nice if we could use this
@@ -329,4 +353,3 @@ void generate_variations_dc_nte(
std::shared_ptr<PSOLFGEncryption> random);
std::vector<std::string> map_filenames_for_variation(
Episode episode, bool is_solo, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies);
void load_map_files();
+3 -2
View File
@@ -17,8 +17,9 @@ constexpr uint32_t MAIN = 0x11000011;
constexpr uint32_t INFORMATION = 0x22000022;
constexpr uint32_t LOBBY = 0x33000033;
constexpr uint32_t GAME = 0x44000044;
constexpr uint32_t QUEST = 0x55000055;
constexpr uint32_t QUEST_CATEGORIES = 0x66000066;
constexpr uint32_t QUEST_EP1 = 0x55010155;
constexpr uint32_t QUEST_EP2 = 0x55020255;
constexpr uint32_t QUEST_CATEGORIES = 0x66010166;
constexpr uint32_t PROXY_DESTINATIONS = 0x77000077;
constexpr uint32_t PROGRAMS = 0x88000088;
constexpr uint32_t PATCHES = 0x99000099;
+7 -10
View File
@@ -718,11 +718,11 @@ void PSOV2OrV3DetectorEncryption::encrypt(void* data, size_t size, bool advance)
le_uint32_t encrypted = *reinterpret_cast<le_uint32_t*>(data);
le_uint32_t decrypted_v2 = encrypted;
unique_ptr<PSOEncryption> v2_crypt(new PSOV2Encryption(this->key));
auto v2_crypt = make_unique<PSOV2Encryption>(this->key);
v2_crypt->decrypt(&decrypted_v2, sizeof(decrypted_v2), false);
le_uint32_t decrypted_v3 = encrypted;
unique_ptr<PSOEncryption> v3_crypt(new PSOV3Encryption(this->key));
auto v3_crypt = make_unique<PSOV3Encryption>(this->key);
v3_crypt->decrypt(&decrypted_v3, sizeof(decrypted_v3), false);
bool v2_match = this->v2_matches.count(decrypted_v2);
@@ -760,9 +760,9 @@ void PSOV2OrV3ImitatorEncryption::encrypt(void* data, size_t size, bool advance)
if (!this->active_crypt) {
auto t = this->detector_crypt->type();
if (t == Type::V2) {
this->active_crypt.reset(new PSOV2Encryption(this->key));
this->active_crypt = make_shared<PSOV2Encryption>(this->key);
} else if (t == Type::V3) {
this->active_crypt.reset(new PSOV3Encryption(this->key));
this->active_crypt = make_shared<PSOV3Encryption>(this->key);
} else {
throw logic_error("detector crypt is not V2 or V3");
}
@@ -801,8 +801,7 @@ void PSOBBMultiKeyDetectorEncryption::decrypt(void* data, size_t size, bool adva
for (const auto& key : this->possible_keys) {
this->active_key = key;
this->active_crypt.reset(new PSOBBEncryption(
*this->active_key, this->seed.data(), this->seed.size()));
this->active_crypt = make_shared<PSOBBEncryption>(*this->active_key, this->seed.data(), this->seed.size());
string test_data(reinterpret_cast<const char*>(data), size);
this->active_crypt->decrypt(test_data.data(), test_data.size(), false);
if (this->expected_first_data.count(test_data)) {
@@ -854,11 +853,9 @@ shared_ptr<PSOBBEncryption> PSOBBMultiKeyImitatorEncryption::ensure_crypt() {
// To handle this, we use the other crypt's seed if the type is JSD1.
if ((key->subtype == PSOBBEncryption::Subtype::JSD1) && this->jsd1_use_detector_seed) {
const auto& detector_seed = this->detector_crypt->get_seed();
this->active_crypt.reset(new PSOBBEncryption(
*key, detector_seed.data(), detector_seed.size()));
this->active_crypt = make_shared<PSOBBEncryption>(*key, detector_seed.data(), detector_seed.size());
} else {
this->active_crypt.reset(new PSOBBEncryption(
*key, this->seed.data(), this->seed.size()));
this->active_crypt = make_shared<PSOBBEncryption>(*key, this->seed.data(), this->seed.size());
}
}
return this->active_crypt;
+2 -2
View File
@@ -38,7 +38,7 @@ shared_ptr<PSOGCObjectGraph::VTable> PSOGCObjectGraph::parse_vtable_memo(
}
const auto& vt = r.pget<TObjectVTable>(addr & 0x01FFFFFF);
auto ret = this->all_vtables.emplace(addr, new VTable()).first->second;
auto ret = this->all_vtables.emplace(addr, make_shared<VTable>()).first->second;
ret->address = addr;
ret->destroy_addr = vt.destroy;
ret->update_addr = vt.update;
@@ -57,7 +57,7 @@ shared_ptr<PSOGCObjectGraph::Object> PSOGCObjectGraph::parse_object_memo(
const auto& obj = r.pget<TObject>(addr & 0x01FFFFFF);
string type_name = r.pget_cstr(obj.type_name_addr & 0x01FFFFFF);
auto ret = this->all_objects.emplace(addr, new Object()).first->second;
auto ret = this->all_objects.emplace(addr, make_shared<Object>()).first->second;
ret->address = addr;
ret->flags = obj.flags;
ret->type_name = std::move(type_name);
+2 -2
View File
@@ -23,7 +23,7 @@ std::shared_ptr<const std::string> PatchFileIndex::File::load_data() {
string relative_path = join(this->path_directories, "/") + "/" + this->name;
string full_path = this->index->root_dir + "/" + relative_path;
patch_index_log.info("Loading data for %s", relative_path.c_str());
this->loaded_data.reset(new string(load_file(full_path)));
this->loaded_data = make_shared<string>(load_file(full_path));
this->size = this->loaded_data->size();
}
return this->loaded_data;
@@ -70,7 +70,7 @@ PatchFileIndex::PatchFileIndex(const string& root_dir)
auto st = stat(full_item_path);
shared_ptr<File> f(new File(this));
auto f = make_shared<File>(this);
f->path_directories = path_directories;
f->name = item;
-427
View File
@@ -1,427 +0,0 @@
#include "Player.hh"
#include <stdio.h>
#include <string.h>
#include <wchar.h>
#include <phosg/Filesystem.hh>
#include <phosg/Hash.hh>
#include <stdexcept>
#include "FileContentsCache.hh"
#include "ItemData.hh"
#include "Loggers.hh"
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "StaticGameData.hh"
#include "Text.hh"
#include "Version.hh"
using namespace std;
ClientGameData::ClientGameData()
: guild_card_number(0),
should_update_play_time(false),
bb_character_index(-1),
last_play_time_update(0) {
for (size_t z = 0; z < this->blocked_senders.size(); z++) {
this->blocked_senders[z] = 0;
}
}
ClientGameData::~ClientGameData() {
if (!this->bb_username.empty() && this->character_data.get()) {
this->save_character_file();
}
}
void ClientGameData::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_ptr<const LevelTable> level_table) {
this->overlay_character_data.reset(new PSOBBCharacterFile(*this->character(true, false)));
if (rules->weapon_and_armor_mode != BattleRules::WeaponAndArmorMode::ALLOW) {
this->overlay_character_data->inventory.remove_all_items_of_type(0);
this->overlay_character_data->inventory.remove_all_items_of_type(1);
}
if (rules->mag_mode == BattleRules::MagMode::FORBID_ALL) {
this->overlay_character_data->inventory.remove_all_items_of_type(2);
}
if (rules->tool_mode != BattleRules::ToolMode::ALLOW) {
this->overlay_character_data->inventory.remove_all_items_of_type(3);
}
if (rules->replace_char) {
// TODO: Shouldn't we clear other material usage here? It looks like the
// original code doesn't, but that seems wrong.
this->overlay_character_data->inventory.hp_from_materials = 0;
this->overlay_character_data->inventory.tp_from_materials = 0;
uint32_t target_level = clamp<uint32_t>(rules->char_level, 0, 199);
uint8_t char_class = this->overlay_character_data->disp.visual.char_class;
auto& stats = this->overlay_character_data->disp.stats;
stats.reset_to_base(char_class, level_table);
stats.advance_to_level(char_class, target_level, level_table);
stats.unknown_a1 = 40;
stats.meseta = 300;
}
if (rules->tech_disk_mode == BattleRules::TechDiskMode::LIMIT_LEVEL) {
// TODO: Verify this is what the game actually does.
for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) {
uint8_t existing_level = this->overlay_character_data->get_technique_level(tech_num);
if ((existing_level != 0xFF) && (existing_level > rules->max_tech_level)) {
this->overlay_character_data->set_technique_level(tech_num, rules->max_tech_level);
}
}
} else if (rules->tech_disk_mode == BattleRules::TechDiskMode::FORBID_ALL) {
for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) {
this->overlay_character_data->set_technique_level(tech_num, 0xFF);
}
}
if (rules->meseta_mode != BattleRules::MesetaMode::ALLOW) {
this->overlay_character_data->disp.stats.meseta = 0;
}
if (rules->forbid_scape_dolls) {
this->overlay_character_data->inventory.remove_all_items_of_type(3, 9);
}
}
void ClientGameData::create_challenge_overlay(Version version, size_t template_index, shared_ptr<const LevelTable> level_table) {
auto p = this->character(true, false);
const auto& tpl = get_challenge_template_definition(version, p->disp.visual.class_flags, template_index);
this->overlay_character_data.reset(new PSOBBCharacterFile(*p));
auto overlay = this->overlay_character_data;
for (size_t z = 0; z < overlay->inventory.items.size(); z++) {
auto& i = overlay->inventory.items[z];
i.present = 0;
i.unknown_a1 = 0;
i.extension_data1 = 0;
i.extension_data2 = 0;
i.flags = 0;
i.data = ItemData();
}
overlay->inventory.items[13].extension_data2 = 1;
overlay->disp.stats.reset_to_base(overlay->disp.visual.char_class, level_table);
overlay->disp.stats.advance_to_level(overlay->disp.visual.char_class, tpl.level, level_table);
overlay->disp.stats.unknown_a1 = 40;
overlay->disp.stats.unknown_a3 = 10.0;
overlay->disp.stats.experience = level_table->stats_delta_for_level(overlay->disp.visual.char_class, overlay->disp.stats.level).experience;
overlay->disp.stats.meseta = 0;
overlay->clear_all_material_usage();
for (size_t z = 0; z < 0x13; z++) {
overlay->set_technique_level(z, 0xFF);
}
for (size_t z = 0; z < tpl.items.size(); z++) {
auto& inv_item = overlay->inventory.items[z];
inv_item.present = tpl.items[z].present;
inv_item.unknown_a1 = tpl.items[z].unknown_a1;
inv_item.flags = tpl.items[z].flags;
inv_item.data = tpl.items[z].data;
}
overlay->inventory.num_items = tpl.items.size();
for (const auto& tech_level : tpl.tech_levels) {
overlay->set_technique_level(tech_level.tech_num, tech_level.level);
}
}
shared_ptr<PSOBBBaseSystemFile> ClientGameData::system(bool allow_load) {
if (!this->system_data && allow_load) {
this->load_all_files();
}
return this->system_data;
}
shared_ptr<const PSOBBBaseSystemFile> ClientGameData::system(bool allow_load) const {
if (!this->system_data.get() && allow_load) {
throw runtime_error("system data is not loaded");
}
return this->system_data;
}
shared_ptr<PSOBBCharacterFile> ClientGameData::character(bool allow_load, bool allow_overlay) {
if (this->overlay_character_data && allow_overlay) {
return this->overlay_character_data;
}
if (!this->character_data && allow_load) {
if (!this->bb_username.empty() && (this->bb_character_index < 0)) {
throw runtime_error("character index not specified");
}
this->load_all_files();
}
return this->character_data;
}
shared_ptr<const PSOBBCharacterFile> ClientGameData::character(bool allow_load, bool allow_overlay) const {
if (allow_overlay && this->overlay_character_data) {
return this->overlay_character_data;
}
if (!this->character_data && allow_load) {
throw runtime_error("character data is not loaded");
}
return this->character_data;
}
shared_ptr<PSOBBGuildCardFile> ClientGameData::guild_cards(bool allow_load) {
if (!this->guild_card_data && allow_load) {
this->load_all_files();
}
return this->guild_card_data;
}
shared_ptr<const PSOBBGuildCardFile> ClientGameData::guild_cards(bool allow_load) const {
if (!this->guild_card_data && allow_load) {
throw runtime_error("account data is not loaded");
}
return this->guild_card_data;
}
string ClientGameData::system_filename() const {
if (this->bb_username.empty()) {
throw logic_error("non-BB players do not have system data");
}
return string_printf("system/players/system_%s.psosys", this->bb_username.c_str());
}
string ClientGameData::character_filename() const {
if (this->bb_username.empty()) {
throw logic_error("non-BB players do not have character data");
}
if (this->bb_character_index < 0) {
throw logic_error("character index is not set");
}
return string_printf("system/players/player_%s_%hhd.psochar", this->bb_username.c_str(), this->bb_character_index);
}
string ClientGameData::guild_card_filename() const {
if (this->bb_username.empty()) {
throw logic_error("non-BB players do not have Guild Card files");
}
return string_printf("system/players/guild_cards_%s.psocard", this->bb_username.c_str());
}
string ClientGameData::legacy_account_filename() const {
if (this->bb_username.empty()) {
throw logic_error("non-BB players do not have legacy account data");
}
return string_printf("system/players/account_%s.nsa", this->bb_username.c_str());
}
string ClientGameData::legacy_player_filename() const {
if (this->bb_username.empty()) {
throw logic_error("non-BB players do not have legacy player data");
}
if (this->bb_character_index < 0) {
throw logic_error("character index is not set");
}
return string_printf(
"system/players/player_%s_%hhd.nsc",
this->bb_username.c_str(),
static_cast<int8_t>(this->bb_character_index + 1));
}
void ClientGameData::create_character_file(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
shared_ptr<const LevelTable> level_table) {
this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table);
this->save_character_file();
}
void ClientGameData::load_all_files() {
if (this->bb_username.empty()) {
this->system_data.reset(new PSOBBBaseSystemFile());
this->character_data.reset(new PSOBBCharacterFile());
this->guild_card_data.reset(new PSOBBGuildCardFile());
return;
}
this->system_data.reset();
this->character_data.reset();
this->guild_card_data.reset();
string sys_filename = this->system_filename();
if (isfile(sys_filename)) {
this->system_data.reset(new PSOBBBaseSystemFile(load_object_file<PSOBBBaseSystemFile>(sys_filename, true)));
player_data_log.info("Loaded system data from %s", sys_filename.c_str());
}
if (this->bb_character_index >= 0) {
string char_filename = this->character_filename();
if (isfile(char_filename)) {
auto f = fopen_unique(char_filename, "rb");
auto header = 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.reset(new PSOBBCharacterFile(freadx<PSOBBCharacterFile>(f.get())));
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
// file instead
if (!this->system_data) {
this->system_data.reset(new PSOBBBaseSystemFile(freadx<PSOBBBaseSystemFile>(f.get())));
player_data_log.info("Loaded system data from %s", char_filename.c_str());
}
}
}
string card_filename = this->guild_card_filename();
if (isfile(card_filename)) {
this->guild_card_data.reset(new PSOBBGuildCardFile(load_object_file<PSOBBGuildCardFile>(card_filename)));
player_data_log.info("Loaded Guild Card data from %s", card_filename.c_str());
}
// If any of the above files were missing, try to load from .nsa/.nsc files instead
if (!this->system_data || (!this->character_data && (this->bb_character_index >= 0)) || !this->guild_card_data) {
string nsa_filename = this->legacy_account_filename();
shared_ptr<LegacySavedAccountDataBB> nsa_data;
if (isfile(nsa_filename)) {
nsa_data.reset(new LegacySavedAccountDataBB(load_object_file<LegacySavedAccountDataBB>(nsa_filename)));
if (!nsa_data->signature.eq(LegacySavedAccountDataBB::SIGNATURE)) {
throw runtime_error("account data header is incorrect");
}
if (!this->system_data) {
this->system_data.reset(new PSOBBBaseSystemFile(nsa_data->system_file.base));
player_data_log.info("Loaded legacy system data from %s", nsa_filename.c_str());
}
if (!this->guild_card_data) {
this->guild_card_data.reset(new PSOBBGuildCardFile(nsa_data->guild_card_file));
player_data_log.info("Loaded legacy Guild Card data from %s", nsa_filename.c_str());
}
}
if (!this->system_data) {
this->system_data.reset(new PSOBBBaseSystemFile());
player_data_log.info("Created new system data");
}
if (!this->guild_card_data) {
this->guild_card_data.reset(new PSOBBGuildCardFile());
player_data_log.info("Created new Guild Card data");
}
if (!this->character_data && (this->bb_character_index >= 0)) {
string nsc_filename = this->legacy_player_filename();
auto nsc_data = load_object_file<LegacySavedPlayerDataBB>(nsc_filename);
if (nsc_data.signature == LegacySavedPlayerDataBB::SIGNATURE_V0) {
nsc_data.signature = LegacySavedPlayerDataBB::SIGNATURE_V0;
nsc_data.unused.clear();
nsc_data.battle_records.place_counts.clear(0);
nsc_data.battle_records.disconnect_count = 0;
nsc_data.battle_records.unknown_a1.clear(0);
} else if (nsc_data.signature != LegacySavedPlayerDataBB::SIGNATURE_V1) {
throw runtime_error("legacy player data has incorrect signature");
}
this->character_data.reset(new PSOBBCharacterFile());
this->character_data->inventory = nsc_data.inventory;
this->character_data->disp = nsc_data.disp;
this->character_data->play_time_seconds = nsc_data.disp.play_time;
this->character_data->unknown_a2 = nsc_data.unknown_a2;
this->character_data->quest_flags = nsc_data.quest_flags;
this->character_data->death_count = nsc_data.death_count;
this->character_data->bank = nsc_data.bank;
this->character_data->guild_card.guild_card_number = this->guild_card_number;
this->character_data->guild_card.name = nsc_data.disp.name;
this->character_data->guild_card.description = nsc_data.guild_card_description;
this->character_data->guild_card.present = 1;
this->character_data->guild_card.language = nsc_data.inventory.language;
this->character_data->guild_card.section_id = nsc_data.disp.visual.section_id;
this->character_data->guild_card.char_class = nsc_data.disp.visual.char_class;
this->character_data->auto_reply = nsc_data.auto_reply;
this->character_data->info_board = nsc_data.info_board;
this->character_data->battle_records = nsc_data.battle_records;
this->character_data->challenge_records = nsc_data.challenge_records;
this->character_data->tech_menu_config = nsc_data.tech_menu_config;
this->character_data->quest_global_flags = nsc_data.quest_global_flags;
if (nsa_data) {
this->character_data->option_flags = nsa_data->option_flags;
this->character_data->symbol_chats = nsa_data->symbol_chats;
this->character_data->shortcuts = nsa_data->shortcuts;
player_data_log.info("Loaded legacy player data from %s and %s", nsa_filename.c_str(), nsc_filename.c_str());
} else {
player_data_log.info("Loaded legacy player data from %s", nsc_filename.c_str());
}
}
}
this->blocked_senders.fill(0);
for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) {
if (this->guild_card_data->blocked[z].present) {
this->blocked_senders[z] = this->guild_card_data->blocked[z].guild_card_number;
}
}
if (this->character_data) {
this->last_play_time_update = now();
}
}
void ClientGameData::save_system_file() const {
if (!this->system_data) {
throw logic_error("no system file loaded");
}
string filename = this->system_filename();
save_object_file(filename, *this->system_data);
player_data_log.info("Saved system file %s", filename.c_str());
}
void ClientGameData::save_character_file() {
if (!this->system_data.get()) {
throw logic_error("no system file loaded");
}
if (!this->character_data.get()) {
throw logic_error("no character file loaded");
}
if (this->should_update_play_time) {
// This is slightly inaccurate, since fractions of a second are truncated
// off each time we save. I'm lazy, so insert shrug emoji here.
uint64_t t = now();
uint64_t seconds = (t - this->last_play_time_update) / 1000000;
this->character_data->disp.play_time += seconds;
this->character_data->play_time_seconds = this->character_data->disp.play_time;
player_data_log.info("Added %" PRIu64 " seconds to play time", seconds);
this->last_play_time_update = t;
}
string filename = this->character_filename();
auto f = fopen_unique(filename, "wb");
PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBBaseSystemFile) + sizeof(PSOBBTeamMembership), 0x00E7, 0x00000000};
fwritex(f.get(), header);
fwritex(f.get(), *this->character_data);
fwritex(f.get(), *this->system_data);
// TODO: Technically, we should write the actual team membership struct to the
// file here, but that would cause ClientGameData to depend on License, 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 License 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;
fwritex(f.get(), empty_membership);
player_data_log.info("Saved character file %s", filename.c_str());
}
void ClientGameData::save_guild_card_file() const {
if (!this->guild_card_data.get()) {
throw logic_error("no Guild Card file loaded");
}
string filename = this->guild_card_filename();
save_object_file(filename, *this->guild_card_data);
player_data_log.info("Saved Guild Card file %s", filename.c_str());
}
-108
View File
@@ -1,108 +0,0 @@
#pragma once
#include <inttypes.h>
#include <stddef.h>
#include <array>
#include <phosg/Encoding.hh>
#include <string>
#include <utility>
#include <vector>
#include "Episode3/DataIndexes.hh"
#include "ItemCreator.hh"
#include "ItemNameIndex.hh"
#include "LevelTable.hh"
#include "PlayerSubordinates.hh"
#include "SaveFileFormats.hh"
#include "Text.hh"
#include "Version.hh"
struct PendingItemTrade {
uint8_t other_client_id;
bool confirmed; // true if client has sent a D2 command
std::vector<ItemData> items;
};
struct PendingCardTrade {
uint8_t other_client_id;
bool confirmed; // true if client has sent an EE D2 command
std::vector<std::pair<uint32_t, uint32_t>> card_to_count;
};
class ClientGameData {
public:
uint32_t guild_card_number;
bool should_update_play_time;
// The following fields are not saved, and are only used in certain situations
std::array<uint32_t, 30> blocked_senders;
// This is only used if the client is v1 or v2
std::unique_ptr<PlayerDispDataDCPCV3> last_reported_disp_v1_v2;
// Null unless the client is within the trade sequence (D0-D4 or EE commands)
std::unique_ptr<PendingItemTrade> pending_item_trade;
std::unique_ptr<PendingCardTrade> pending_card_trade;
// Null unless the client is Episode 3 and has sent its config already
std::shared_ptr<Episode3::PlayerConfig> ep3_config;
// These are only used if the client is BB
std::string bb_username;
int8_t bb_character_index;
ItemData identify_result;
std::array<std::vector<ItemData>, 3> shop_contents;
ClientGameData();
~ClientGameData();
void create_battle_overlay(std::shared_ptr<const BattleRules> rules, std::shared_ptr<const LevelTable> level_table);
void create_challenge_overlay(Version version, size_t template_index, std::shared_ptr<const LevelTable> level_table);
inline void delete_overlay() {
this->overlay_character_data.reset();
}
inline bool has_overlay() const {
return this->overlay_character_data.get() != nullptr;
}
std::shared_ptr<PSOBBBaseSystemFile> system(bool allow_load = true);
std::shared_ptr<const PSOBBBaseSystemFile> system(bool allow_load = true) const;
std::shared_ptr<PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true);
std::shared_ptr<const PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true) const;
std::shared_ptr<PSOBBGuildCardFile> guild_cards(bool allow_load = true);
std::shared_ptr<const PSOBBGuildCardFile> guild_cards(bool allow_load = true) const;
void create_character_file(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
std::shared_ptr<const LevelTable> level_table);
void save_system_file() const;
// Note: This function is not const because it updates the player's play time.
void save_character_file();
void save_guild_card_file() const;
private:
// The overlay character data is used in battle and challenge modes, when
// character data is temporarily replaced in-game. In other play modes and in
// lobbies, overlay_character_data is null.
std::shared_ptr<PSOBBBaseSystemFile> system_data;
std::shared_ptr<PSOBBCharacterFile> overlay_character_data;
std::shared_ptr<PSOBBCharacterFile> character_data;
std::shared_ptr<PSOBBGuildCardFile> guild_card_data;
uint64_t last_play_time_update;
void load_all_files();
std::string system_filename() const;
std::string character_filename() const;
std::string guild_card_filename() const;
std::string legacy_player_filename() const;
std::string legacy_account_filename() const;
};
+119
View File
@@ -0,0 +1,119 @@
#include "PlayerFilesManager.hh"
#include <stdio.h>
#include <string.h>
#include <wchar.h>
#include <phosg/Filesystem.hh>
#include <phosg/Hash.hh>
#include <stdexcept>
#include "FileContentsCache.hh"
#include "ItemData.hh"
#include "Loggers.hh"
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "StaticGameData.hh"
#include "Text.hh"
#include "Version.hh"
using namespace std;
PlayerFilesManager::PlayerFilesManager(std::shared_ptr<struct event_base> base)
: base(base),
clear_expired_files_event(
event_new(this->base.get(), -1, EV_TIMEOUT | EV_PERSIST, &PlayerFilesManager::clear_expired_files, this),
event_free) {
auto tv = usecs_to_timeval(30 * 1000 * 1000);
event_add(this->clear_expired_files_event.get(), &tv);
}
template <typename KeyT, typename ValueT>
size_t erase_unused(std::unordered_map<KeyT, std::shared_ptr<ValueT>>& m) {
size_t ret = 0;
for (auto it = m.begin(); it != m.end();) {
if (it->second.use_count() <= 1) {
it = m.erase(it);
ret++;
} else {
it++;
}
}
return ret;
}
std::shared_ptr<PSOBBBaseSystemFile> PlayerFilesManager::get_system(const std::string& filename) {
try {
return this->loaded_system_files.at(filename);
} catch (const out_of_range&) {
return nullptr;
}
}
std::shared_ptr<PSOBBCharacterFile> PlayerFilesManager::get_character(const std::string& filename) {
try {
return this->loaded_character_files.at(filename);
} catch (const out_of_range&) {
return nullptr;
}
}
std::shared_ptr<PSOBBGuildCardFile> PlayerFilesManager::get_guild_card(const std::string& filename) {
try {
return this->loaded_guild_card_files.at(filename);
} catch (const out_of_range&) {
return nullptr;
}
}
std::shared_ptr<PlayerBank> PlayerFilesManager::get_bank(const std::string& filename) {
try {
return this->loaded_bank_files.at(filename);
} catch (const out_of_range&) {
return nullptr;
}
}
void PlayerFilesManager::set_system(const std::string& filename, std::shared_ptr<PSOBBBaseSystemFile> file) {
if (!this->loaded_system_files.emplace(filename, file).second) {
throw runtime_error("Guild Card file already loaded: " + filename);
}
}
void PlayerFilesManager::set_character(const std::string& filename, std::shared_ptr<PSOBBCharacterFile> file) {
if (!this->loaded_character_files.emplace(filename, file).second) {
throw runtime_error("character file already loaded: " + filename);
}
}
void PlayerFilesManager::set_guild_card(const std::string& filename, std::shared_ptr<PSOBBGuildCardFile> file) {
if (!this->loaded_guild_card_files.emplace(filename, file).second) {
throw runtime_error("Guild Card file already loaded: " + filename);
}
}
void PlayerFilesManager::set_bank(const std::string& filename, std::shared_ptr<PlayerBank> file) {
if (!this->loaded_bank_files.emplace(filename, file).second) {
throw runtime_error("bank file already loaded: " + filename);
}
}
void PlayerFilesManager::clear_expired_files(evutil_socket_t, short, void* ctx) {
auto* self = reinterpret_cast<PlayerFilesManager*>(ctx);
size_t num_deleted = erase_unused(self->loaded_system_files);
if (num_deleted) {
player_data_log.info("Cleared %zu expired system file(s)", num_deleted);
}
num_deleted = erase_unused(self->loaded_character_files);
if (num_deleted) {
player_data_log.info("Cleared %zu expired character file(s)", num_deleted);
}
num_deleted = erase_unused(self->loaded_guild_card_files);
if (num_deleted) {
player_data_log.info("Cleared %zu expired Guild Card file(s)", num_deleted);
}
num_deleted = erase_unused(self->loaded_bank_files);
if (num_deleted) {
player_data_log.info("Cleared %zu expired bank file(s)", num_deleted);
}
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <event2/event.h>
#include <inttypes.h>
#include <stddef.h>
#include <array>
#include <phosg/Encoding.hh>
#include <string>
#include <utility>
#include <vector>
#include "Episode3/DataIndexes.hh"
#include "ItemCreator.hh"
#include "ItemNameIndex.hh"
#include "LevelTable.hh"
#include "PlayerSubordinates.hh"
#include "SaveFileFormats.hh"
#include "Text.hh"
#include "Version.hh"
class PlayerFilesManager {
public:
explicit PlayerFilesManager(std::shared_ptr<struct event_base> base);
~PlayerFilesManager() = default;
std::shared_ptr<PSOBBBaseSystemFile> get_system(const std::string& filename);
std::shared_ptr<PSOBBCharacterFile> get_character(const std::string& filename);
std::shared_ptr<PSOBBGuildCardFile> get_guild_card(const std::string& filename);
std::shared_ptr<PlayerBank> get_bank(const std::string& filename);
void set_system(const std::string& filename, std::shared_ptr<PSOBBBaseSystemFile> file);
void set_character(const std::string& filename, std::shared_ptr<PSOBBCharacterFile> file);
void set_guild_card(const std::string& filename, std::shared_ptr<PSOBBGuildCardFile> file);
void set_bank(const std::string& filename, std::shared_ptr<PlayerBank> file);
private:
std::shared_ptr<struct event_base> base;
std::unique_ptr<struct event, void (*)(struct event*)> clear_expired_files_event;
std::unordered_map<std::string, std::shared_ptr<PSOBBBaseSystemFile>> loaded_system_files;
std::unordered_map<std::string, std::shared_ptr<PSOBBCharacterFile>> loaded_character_files;
std::unordered_map<std::string, std::shared_ptr<PSOBBGuildCardFile>> loaded_guild_card_files;
std::unordered_map<std::string, std::shared_ptr<PlayerBank>> loaded_bank_files;
static void clear_expired_files(evutil_socket_t fd, short events, void* ctx);
};
+63 -70
View File
@@ -194,13 +194,6 @@ PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const {
return pre;
}
void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) {
this->stats.level = pre.level;
this->stats.experience = pre.experience;
this->visual = pre.visual;
this->name = pre.name;
}
void PlayerDispDataBB::apply_dressing_room(const PlayerDispDataBBPreview& pre) {
this->visual.name_color = pre.visual.name_color;
this->visual.extra_model = pre.visual.extra_model;
@@ -466,17 +459,18 @@ void PlayerBank::add_item(const ItemData& item) {
}
if (y < this->num_items) {
this->items[y].data.data1[5] += item.data1[5];
if (this->items[y].data.data1[5] > combine_max) {
this->items[y].data.data1[5] = combine_max;
uint8_t new_count = this->items[y].data.data1[5] + item.data1[5];
if (new_count > combine_max) {
throw runtime_error("stack size would exceed limit");
}
this->items[y].amount = this->items[y].data.data1[5];
this->items[y].data.data1[5] = new_count;
this->items[y].amount = new_count;
return;
}
}
if (this->num_items >= 200) {
throw runtime_error("bank is full");
throw runtime_error("no free space in bank");
}
auto& last_item = this->items[this->num_items];
last_item.data = item;
@@ -486,21 +480,10 @@ void PlayerBank::add_item(const ItemData& item) {
}
ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount) {
ItemData ret;
if (item_id == 0xFFFFFFFF) {
if (amount > this->meseta) {
throw out_of_range("player does not have enough meseta");
}
ret.data1[0] = 0x04;
ret.data2d = amount;
this->meseta -= amount;
return ret;
}
size_t index = this->find_item(item_id);
auto& bank_item = this->items[index];
ItemData ret;
if (amount && (bank_item.data.stack_size() > 1) && (amount < bank_item.data.data1[5])) {
ret = bank_item.data;
ret.data1[5] = amount;
@@ -694,62 +677,72 @@ size_t PlayerBank::find_item(uint32_t item_id) {
throw out_of_range("item not present");
}
void PlayerBank::sort() {
std::sort(this->items.data(), this->items.data() + this->num_items);
}
void PlayerBank::assign_ids(uint32_t base_id) {
for (size_t z = 0; z < this->num_items; z++) {
this->items[z].data.id = base_id + z;
}
}
BattleRules::BattleRules(const JSON& json) {
static const JSON empty_list = JSON::list();
this->tech_disk_mode = json.get_enum("tech_disk_mode", this->tech_disk_mode);
this->weapon_and_armor_mode = json.get_enum("weapon_and_armor_mode", this->weapon_and_armor_mode);
this->mag_mode = json.get_enum("mag_mode", this->mag_mode);
this->tool_mode = json.get_enum("tool_mode", this->tool_mode);
this->trap_mode = json.get_enum("trap_mode", this->trap_mode);
this->unused_F817 = json.get_int("unused_F817", this->unused_F817);
this->respawn_mode = json.get_int("respawn_mode", this->respawn_mode);
this->replace_char = json.get_int("replace_char", this->replace_char);
this->drop_weapon = json.get_int("drop_weapon", this->drop_weapon);
this->is_teams = json.get_int("is_teams", this->is_teams);
this->hide_target_reticle = json.get_int("hide_target_reticle", this->hide_target_reticle);
this->meseta_mode = json.get_enum("meseta_mode", this->meseta_mode);
this->death_level_up = json.get_int("death_level_up", this->death_level_up);
const JSON& trap_counts_json = json.get("trap_counts", empty_list);
this->tech_disk_mode = json.get_enum("TechDiskMode", this->tech_disk_mode);
this->weapon_and_armor_mode = json.get_enum("WeaponAndArmorMode", this->weapon_and_armor_mode);
this->mag_mode = json.get_enum("MagMode", this->mag_mode);
this->tool_mode = json.get_enum("ToolMode", this->tool_mode);
this->trap_mode = json.get_enum("TrapMode", this->trap_mode);
this->unused_F817 = json.get_int("UnusedF817", this->unused_F817);
this->respawn_mode = json.get_int("RespawnMode", this->respawn_mode);
this->replace_char = json.get_int("ReplaceChar", this->replace_char);
this->drop_weapon = json.get_int("DropWeapon", this->drop_weapon);
this->is_teams = json.get_int("IsTeams", this->is_teams);
this->hide_target_reticle = json.get_int("HideTargetReticle", this->hide_target_reticle);
this->meseta_mode = json.get_enum("MesetaMode", this->meseta_mode);
this->death_level_up = json.get_int("DeathLevelUp", this->death_level_up);
const JSON& trap_counts_json = json.get("TrapCounts", empty_list);
for (size_t z = 0; z < trap_counts_json.size(); z++) {
this->trap_counts[z] = trap_counts_json.at(z).as_int();
}
this->enable_sonar = json.get_int("enable_sonar", this->enable_sonar);
this->sonar_count = json.get_int("sonar_count", this->sonar_count);
this->forbid_scape_dolls = json.get_int("forbid_scape_dolls", this->forbid_scape_dolls);
this->lives = json.get_int("lives", this->lives);
this->max_tech_level = json.get_int("max_tech_level", this->max_tech_level);
this->char_level = json.get_int("char_level", this->char_level);
this->time_limit = json.get_int("time_limit", this->time_limit);
this->death_tech_level_up = json.get_int("death_tech_level_up", this->death_tech_level_up);
this->box_drop_area = json.get_int("box_drop_area", this->box_drop_area);
this->enable_sonar = json.get_int("EnableSonar", this->enable_sonar);
this->sonar_count = json.get_int("SonarCount", this->sonar_count);
this->forbid_scape_dolls = json.get_int("ForbidScapeDolls", this->forbid_scape_dolls);
this->lives = json.get_int("Lives", this->lives);
this->max_tech_level = json.get_int("MaxTechLevel", this->max_tech_level);
this->char_level = json.get_int("CharLevel", this->char_level);
this->time_limit = json.get_int("TimeLimit", this->time_limit);
this->death_tech_level_up = json.get_int("DeathTechLevelUp", this->death_tech_level_up);
this->box_drop_area = json.get_int("BoxDropArea", this->box_drop_area);
}
JSON BattleRules::json() const {
return JSON::dict({
{"tech_disk_mode", this->tech_disk_mode},
{"weapon_and_armor_mode", this->weapon_and_armor_mode},
{"mag_mode", this->mag_mode},
{"tool_mode", this->tool_mode},
{"trap_mode", this->trap_mode},
{"unused_F817", this->unused_F817},
{"respawn_mode", this->respawn_mode},
{"replace_char", this->replace_char},
{"drop_weapon", this->drop_weapon},
{"is_teams", this->is_teams},
{"hide_target_reticle", this->hide_target_reticle},
{"meseta_mode", this->meseta_mode},
{"death_level_up", this->death_level_up},
{"trap_counts", JSON::list({this->trap_counts[0], this->trap_counts[1], this->trap_counts[2], this->trap_counts[3]})},
{"enable_sonar", this->enable_sonar},
{"sonar_count", this->sonar_count},
{"forbid_scape_dolls", this->forbid_scape_dolls},
{"lives", this->lives.load()},
{"max_tech_level", this->max_tech_level.load()},
{"char_level", this->char_level.load()},
{"time_limit", this->time_limit.load()},
{"death_tech_level_up", this->death_tech_level_up.load()},
{"box_drop_area", this->box_drop_area.load()},
{"TechDiskMode", this->tech_disk_mode},
{"WeaponAndArmorMode", this->weapon_and_armor_mode},
{"MagMode", this->mag_mode},
{"ToolMode", this->tool_mode},
{"TrapMode", this->trap_mode},
{"UnusedF817", this->unused_F817},
{"RespawnMode", this->respawn_mode},
{"ReplaceChar", this->replace_char},
{"DropWeapon", this->drop_weapon},
{"IsTeams", this->is_teams},
{"HideTargetReticle", this->hide_target_reticle},
{"MesetaMode", this->meseta_mode},
{"DeathLevelUp", this->death_level_up},
{"TrapCounts", JSON::list({this->trap_counts[0], this->trap_counts[1], this->trap_counts[2], this->trap_counts[3]})},
{"EnableSonar", this->enable_sonar},
{"SonarCount", this->sonar_count},
{"ForbidScapeDolls", this->forbid_scape_dolls},
{"Lives", this->lives.load()},
{"MaxTechLevel", this->max_tech_level.load()},
{"CharLevel", this->char_level.load()},
{"TimeLimit", this->time_limit.load()},
{"DeathTechLevelUp", this->death_tech_level_up.load()},
{"BoxDropArea", this->box_drop_area.load()},
});
}
+44 -13
View File
@@ -10,13 +10,14 @@
#include <utility>
#include <vector>
#include "ChoiceSearch.hh"
#include "FileContentsCache.hh"
#include "ItemData.hh"
#include "LevelTable.hh"
#include "Text.hh"
#include "Version.hh"
struct Client;
class Client;
class ItemParameterTable;
// PSO V2 stored some extra data in the character structs in a format that I'm
@@ -62,6 +63,10 @@ struct PlayerBankItem {
/* 14 */ le_uint16_t amount = 0;
/* 16 */ le_uint16_t present = 0;
/* 18 */
inline bool operator<(const PlayerBankItem& other) const {
return this->data < other.data;
}
} __attribute__((packed));
struct PlayerInventory {
@@ -98,6 +103,9 @@ struct PlayerBank {
void add_item(const ItemData& item);
ItemData remove_item(uint32_t item_id, uint32_t amount);
size_t find_item(uint32_t item_id);
void sort();
void assign_ids(uint32_t base_id);
} __attribute__((packed));
struct PlayerDispDataBB;
@@ -105,7 +113,7 @@ struct PlayerDispDataBB;
struct PlayerVisualConfig {
/* 00 */ pstring<TextEncoding::ASCII, 0x10> name;
/* 10 */ parray<uint8_t, 8> unknown_a2;
/* 18 */ le_uint32_t name_color = 0x00000000; // ARGB
/* 18 */ le_uint32_t name_color = 0xFFFFFFFF; // ARGB
/* 1C */ uint8_t extra_model = 0;
/* 1D */ parray<uint8_t, 0x0F> unused;
// See compute_name_color_checksum for details on how this is computed. This
@@ -453,17 +461,6 @@ struct PlayerRecords_Battle {
/* 18 */
} __attribute__((packed));
template <typename ItemIDT>
struct ChoiceSearchConfig {
// 0 = enabled, 1 = disabled. Unused for command C3
le_uint32_t disabled = 1;
struct Entry {
ItemIDT parent_category_id = 0;
ItemIDT category_id = 0;
} __attribute__((packed));
parray<Entry, 5> entries;
} __attribute__((packed));
template <typename DestT, typename SrcT = DestT>
DestT convert_player_disp_data(const SrcT&, uint8_t, uint8_t) {
static_assert(always_false<DestT, SrcT>::v,
@@ -493,6 +490,40 @@ inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
return src;
}
struct QuestFlagsForDifficulty {
parray<uint8_t, 0x80> data;
inline bool get(uint16_t flag_index) const {
size_t byte_index = flag_index >> 3;
uint8_t mask = 0x80 >> (flag_index & 7);
return !!(this->data[byte_index] & mask);
}
inline void set(uint16_t flag_index) {
size_t byte_index = flag_index >> 3;
uint8_t mask = 0x80 >> (flag_index & 7);
this->data[byte_index] |= mask;
}
inline void clear(uint16_t flag_index) {
size_t byte_index = flag_index >> 3;
uint8_t mask = 0x80 >> (flag_index & 7);
this->data[byte_index] &= (~mask);
}
} __attribute__((packed));
struct QuestFlags {
parray<QuestFlagsForDifficulty, 4> data;
inline bool get(uint8_t difficulty, uint16_t flag_index) const {
return this->data[difficulty].get(flag_index);
}
inline void set(uint8_t difficulty, uint16_t flag_index) {
this->data[difficulty].set(flag_index);
}
inline void clear(uint8_t difficulty, uint16_t flag_index) {
this->data[difficulty].clear(flag_index);
}
} __attribute__((packed));
struct BattleRules {
enum class TechDiskMode : uint8_t {
ALLOW = 0,
+30 -27
View File
@@ -229,15 +229,15 @@ static HandlerResult S_V123P_02_17(
forward_command(ses, false, command, flag, data);
if (uses_v3_encryption(ses->version())) {
ses->server_channel.crypt_in.reset(new PSOV3Encryption(cmd.server_key));
ses->server_channel.crypt_out.reset(new PSOV3Encryption(cmd.client_key));
ses->client_channel.crypt_in.reset(new PSOV3Encryption(cmd.client_key));
ses->client_channel.crypt_out.reset(new PSOV3Encryption(cmd.server_key));
ses->server_channel.crypt_in = make_shared<PSOV3Encryption>(cmd.server_key);
ses->server_channel.crypt_out = make_shared<PSOV3Encryption>(cmd.client_key);
ses->client_channel.crypt_in = make_shared<PSOV3Encryption>(cmd.client_key);
ses->client_channel.crypt_out = make_shared<PSOV3Encryption>(cmd.server_key);
} else { // DC, PC, or patch server (they all use V2 encryption)
ses->server_channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key));
ses->server_channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key));
ses->client_channel.crypt_in.reset(new PSOV2Encryption(cmd.client_key));
ses->client_channel.crypt_out.reset(new PSOV2Encryption(cmd.server_key));
ses->server_channel.crypt_in = make_shared<PSOV2Encryption>(cmd.server_key);
ses->server_channel.crypt_out = make_shared<PSOV2Encryption>(cmd.client_key);
ses->client_channel.crypt_in = make_shared<PSOV2Encryption>(cmd.client_key);
ses->client_channel.crypt_out = make_shared<PSOV2Encryption>(cmd.server_key);
}
return HandlerResult::Type::SUPPRESS;
@@ -247,11 +247,11 @@ static HandlerResult S_V123P_02_17(
// This isn't forwarded to the client, so don't recreate the client's crypts
if (uses_v3_encryption(ses->version())) {
ses->server_channel.crypt_in.reset(new PSOV3Encryption(cmd.server_key));
ses->server_channel.crypt_out.reset(new PSOV3Encryption(cmd.client_key));
ses->server_channel.crypt_in = make_shared<PSOV3Encryption>(cmd.server_key);
ses->server_channel.crypt_out = make_shared<PSOV3Encryption>(cmd.client_key);
} else {
ses->server_channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key));
ses->server_channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key));
ses->server_channel.crypt_in = make_shared<PSOV2Encryption>(cmd.server_key);
ses->server_channel.crypt_out = make_shared<PSOV2Encryption>(cmd.client_key);
}
// Respond with an appropriate login command. We don't let the client do this
@@ -466,10 +466,10 @@ static HandlerResult S_B_03(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t
// being able to try all the crypts it knows to detect what type the client
// uses, but the client can't do this since it sends the first encrypted
// data on the connection.
ses->server_channel.crypt_in.reset(new PSOBBMultiKeyImitatorEncryption(
ses->detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), false));
ses->server_channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
ses->detector_crypt, cmd.client_key.data(), sizeof(cmd.client_key), false));
ses->server_channel.crypt_in = make_shared<PSOBBMultiKeyImitatorEncryption>(
ses->detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), false);
ses->server_channel.crypt_out = make_shared<PSOBBMultiKeyImitatorEncryption>(
ses->detector_crypt, cmd.client_key.data(), sizeof(cmd.client_key), false);
// Forward the login command we saved during the unlinked ses->
if (ses->enable_remote_ip_crc_patch && (ses->login_command_bb.size() >= 0x98)) {
@@ -488,18 +488,18 @@ static HandlerResult S_B_03(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t
// client receives the unencrypted data
ses->client_channel.send(0x03, 0x00, data);
ses->detector_crypt.reset(new PSOBBMultiKeyDetectorEncryption(
ses->detector_crypt = make_shared<PSOBBMultiKeyDetectorEncryption>(
ses->require_server_state()->bb_private_keys,
bb_crypt_initial_client_commands,
cmd.client_key.data(),
sizeof(cmd.client_key)));
sizeof(cmd.client_key));
ses->client_channel.crypt_in = ses->detector_crypt;
ses->client_channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
ses->detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), true));
ses->server_channel.crypt_in.reset(new PSOBBMultiKeyImitatorEncryption(
ses->detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), false));
ses->server_channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
ses->detector_crypt, cmd.client_key.data(), sizeof(cmd.client_key), false));
ses->client_channel.crypt_out = make_shared<PSOBBMultiKeyImitatorEncryption>(
ses->detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), true);
ses->server_channel.crypt_in = make_shared<PSOBBMultiKeyImitatorEncryption>(
ses->detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), false);
ses->server_channel.crypt_out = make_shared<PSOBBMultiKeyImitatorEncryption>(
ses->detector_crypt, cmd.client_key.data(), sizeof(cmd.client_key), false);
// We already forwarded the command, so don't do so again
return HandlerResult::Type::SUPPRESS;
@@ -787,7 +787,9 @@ static HandlerResult S_C4(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
return modified ? HandlerResult::Type::MODIFIED : HandlerResult::Type::FORWARD;
}
constexpr on_command_t S_V3_C4 = &S_C4<S_ChoiceSearchResultEntry_V3_C4>;
constexpr on_command_t S_DGX_C4 = &S_C4<S_ChoiceSearchResultEntry_DC_V3_C4>;
constexpr on_command_t S_P_C4 = &S_C4<S_ChoiceSearchResultEntry_PC_C4>;
constexpr on_command_t S_B_C4 = &S_C4<S_ChoiceSearchResultEntry_BB_C4>;
static HandlerResult S_G_E4(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
auto& cmd = check_size_t<S_CardBattleTableState_GC_Ep3_E4>(data);
@@ -1070,6 +1072,7 @@ static HandlerResult C_GXB_61(shared_ptr<ProxyServer::LinkedSession> ses, uint16
pd = reinterpret_cast<C_CharacterData_V3_61_98*>(&ep3_pd);
} else {
if (is_ep3(ses->version())) {
ses->log.info("Version changed to GC_EP3_TRIAL_EDITION");
ses->set_version(Version::GC_EP3_TRIAL_EDITION);
}
pd = &check_size_t<C_CharacterData_V3_61_98>(data, 0xFFFF);
@@ -1962,8 +1965,8 @@ static on_command_t handlers[0x100][13][2] = {
/* C1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* C2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* C3 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* C4 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_V3_C4, nullptr}, {S_V3_C4, nullptr}, {S_V3_C4, nullptr}, {S_V3_C4, nullptr}, {nullptr, nullptr}},
/* C5 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
/* C4 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_DGX_C4, nullptr}, {S_P_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_B_C4, nullptr}},
/* C5 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
/* C6 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* C7 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
/* C8 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
+48 -52
View File
@@ -44,8 +44,7 @@ ProxyServer::ProxyServer(
next_unlicensed_session_id(0xFF00000000000001) {}
void ProxyServer::listen(uint16_t port, Version version, const struct sockaddr_storage* default_destination) {
shared_ptr<ListeningSocket> socket_obj(new ListeningSocket(
this, port, version, default_destination));
auto socket_obj = make_shared<ListeningSocket>(this, port, version, default_destination);
auto l = this->listeners.emplace(port, socket_obj).first->second;
}
@@ -143,7 +142,7 @@ void ProxyServer::on_client_connect(
this->next_unlicensed_session_id = 0xFF00000000000001;
}
auto emplace_ret = this->id_to_session.emplace(session_id, new LinkedSession(this->shared_from_this(), session_id, listen_port, version, *default_destination));
auto emplace_ret = this->id_to_session.emplace(session_id, make_shared<LinkedSession>(this->shared_from_this(), session_id, listen_port, version, *default_destination));
if (!emplace_ret.second) {
throw logic_error("linked session already exists for unlicensed client");
}
@@ -157,7 +156,7 @@ void ProxyServer::on_client_connect(
// create an unlinked session - we'll have to get the destination from the
// client's config, which we'll get via a 9E command soon.
} else {
auto emplace_ret = this->bev_to_unlinked_session.emplace(bev, new UnlinkedSession(this->shared_from_this(), bev, listen_port, version));
auto emplace_ret = this->bev_to_unlinked_session.emplace(bev, make_shared<UnlinkedSession>(this->shared_from_this(), bev, listen_port, version));
if (!emplace_ret.second) {
throw logic_error("stale unlinked session exists");
}
@@ -189,11 +188,11 @@ void ProxyServer::on_client_connect(
auto cmd = prepare_server_init_contents_console(server_key, client_key, 0);
ses->channel.send(0x02, 0x00, &cmd, sizeof(cmd));
if (uses_v2_encryption(version)) {
ses->channel.crypt_out.reset(new PSOV2Encryption(server_key));
ses->channel.crypt_in.reset(new PSOV2Encryption(client_key));
ses->channel.crypt_out = make_shared<PSOV2Encryption>(server_key);
ses->channel.crypt_in = make_shared<PSOV2Encryption>(client_key);
} else {
ses->channel.crypt_out.reset(new PSOV3Encryption(server_key));
ses->channel.crypt_in.reset(new PSOV3Encryption(client_key));
ses->channel.crypt_out = make_shared<PSOV3Encryption>(server_key);
ses->channel.crypt_in = make_shared<PSOV3Encryption>(client_key);
}
break;
}
@@ -204,17 +203,17 @@ void ProxyServer::on_client_connect(
random_data(client_key.data(), client_key.bytes());
auto cmd = prepare_server_init_contents_bb(server_key, client_key, 0);
ses->channel.send(0x03, 0x00, &cmd, sizeof(cmd));
ses->detector_crypt.reset(new PSOBBMultiKeyDetectorEncryption(
ses->detector_crypt = make_shared<PSOBBMultiKeyDetectorEncryption>(
this->state->bb_private_keys,
bb_crypt_initial_client_commands,
cmd.basic_cmd.client_key.data(),
sizeof(cmd.basic_cmd.client_key)));
sizeof(cmd.basic_cmd.client_key));
ses->channel.crypt_in = ses->detector_crypt;
ses->channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
ses->channel.crypt_out = make_shared<PSOBBMultiKeyImitatorEncryption>(
ses->detector_crypt,
cmd.basic_cmd.server_key.data(),
sizeof(cmd.basic_cmd.server_key),
true));
true);
break;
}
default:
@@ -240,8 +239,7 @@ ProxyServer::UnlinkedSession::UnlinkedSession(
string_printf("UnlinkedSession:%p", bev),
TerminalFormat::FG_YELLOW,
TerminalFormat::FG_GREEN),
local_port(local_port),
version(version) {
local_port(local_port) {
memset(&this->next_destination, 0, sizeof(this->next_destination));
}
@@ -265,40 +263,46 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
bool should_close_unlinked_session = false;
try {
switch (ses->version) {
case Version::DC_NTE: {
// We should only get an 8B while the session is unlinked
if (command != 0x8B) {
throw runtime_error("command is not 8B");
}
const auto& cmd = check_size_t<C_Login_DCNTE_8B>(data, sizeof(C_LoginExtended_DCNTE_8B));
ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
ses->sub_version = cmd.sub_version;
ses->channel.language = cmd.language;
ses->character_name = cmd.name.decode(ses->channel.language);
// TODO: Parse cmd.hardware_id
ses->version = Version::DC_NTE;
break;
}
switch (ses->version()) {
case Version::DC_NTE:
case Version::DC_V1_11_2000_PROTOTYPE:
case Version::DC_V1:
case Version::DC_V2:
// We should only get a 93 or 9D while the session is unlinked
if (command == 0x93) {
case Version::GC_NTE:
// We should only get an 8B, 93 or 9D while the session is unlinked
if (command == 0x8B) {
ses->channel.version = Version::DC_NTE;
ses->log.info("Version changed to DC_NTE");
const auto& cmd = check_size_t<C_Login_DCNTE_8B>(data, sizeof(C_LoginExtended_DCNTE_8B));
ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
ses->sub_version = cmd.sub_version;
ses->channel.language = cmd.language;
ses->character_name = cmd.name.decode(ses->channel.language);
// TODO: Parse cmd.hardware_id
} else if (command == 0x93) { // 11/2000 proto through DC V1
ses->channel.version = Version::DC_V1;
ses->log.info("Version changed to DC_V1");
const auto& cmd = check_size_t<C_LoginV1_DC_93>(data);
ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
ses->sub_version = cmd.sub_version;
ses->channel.language = cmd.language;
ses->character_name = cmd.name.decode(ses->channel.language);
ses->hardware_id = cmd.hardware_id.decode();
ses->version = Version::DC_V1;
} else if (command == 0x9D) {
const auto& cmd = check_size_t<C_Login_DC_PC_GC_9D>(data, sizeof(C_LoginExtended_DC_GC_9D));
ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
if (cmd.sub_version >= 0x30) {
ses->log.info("Version changed to GC_NTE");
ses->channel.version = Version::GC_NTE;
ses->license = s->license_index->verify_gc(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
} else { // DC V2
ses->log.info("Version changed to DC_V2");
ses->channel.version = Version::DC_V2;
ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
}
ses->sub_version = cmd.sub_version;
ses->channel.language = cmd.language;
ses->character_name = cmd.name.decode(ses->channel.language);
ses->config.set_flags_for_version(ses->version(), cmd.sub_version);
} else {
throw runtime_error("command is not 93 or 9D");
}
@@ -317,20 +321,11 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
break;
}
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
// We should only get a 9D or 9E while the session is unlinked
if (command == 0x9D) {
const auto& cmd = check_size_t<C_Login_DC_PC_GC_9D>(data, sizeof(C_LoginExtended_DC_GC_9D));
ses->license = s->license_index->verify_gc(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
ses->sub_version = cmd.sub_version;
ses->channel.language = cmd.language;
ses->character_name = cmd.name.decode(ses->channel.language);
ses->version = Version::GC_NTE;
ses->config.set_flags_for_version(ses->version, cmd.sub_version);
} else if (command == 0x9E) {
// We should only get a 9E while the session is unlinked
if (command == 0x9E) {
const auto& cmd = check_size_t<C_Login_GC_9E>(data, sizeof(C_LoginExtended_GC_9E));
ses->license = s->license_index->verify_gc(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode());
ses->sub_version = cmd.sub_version;
@@ -338,7 +333,8 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
ses->character_name = cmd.name.decode(ses->channel.language);
ses->config.parse_from(cmd.client_config);
if (cmd.sub_version >= 0x40) {
ses->version = Version::GC_EP3;
ses->log.info("Version changed to GC_EP3");
ses->channel.version = Version::GC_EP3;
}
} else {
throw runtime_error("command is not 9D or 9E");
@@ -381,7 +377,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
if (!s->allow_unregistered_users) {
throw;
}
shared_ptr<License> l(new License());
auto l = s->license_index->create_license();
l->serial_number = fnv1a32(cmd.username.decode()) & 0x7FFFFFFF;
l->bb_username = cmd.username.decode();
l->bb_password = cmd.password.decode();
@@ -426,10 +422,10 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
// destination somewhere - either in the client config or in the unlinked
// session
if (ses->config.proxy_destination_address != 0) {
linked_ses.reset(new LinkedSession(server, ses->local_port, ses->version, ses->license, ses->config));
linked_ses = make_shared<LinkedSession>(server, ses->local_port, ses->version(), ses->license, ses->config);
linked_ses->log.info("Opened licensed session for unlinked session based on client config");
} else if (ses->next_destination.ss_family == AF_INET) {
linked_ses.reset(new LinkedSession(server, ses->local_port, ses->version, ses->license, ses->next_destination));
linked_ses = make_shared<LinkedSession>(server, ses->local_port, ses->version(), ses->license, ses->next_destination);
linked_ses->log.info("Opened licensed session for unlinked session based on unlinked default destination");
} else {
ses->log.error("Cannot open linked session: no valid destination in client config or unlinked session");
@@ -438,12 +434,12 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
if (linked_ses.get()) {
server->id_to_session.emplace(ses->license->serial_number, linked_ses);
if (linked_ses->version() != ses->version) {
if (linked_ses->version() != ses->version()) {
linked_ses->log.error("Linked session has different game version");
} else {
// Resume the linked session using the unlinked session
try {
if (ses->version == Version::BB_V4) {
if (ses->version() == Version::BB_V4) {
linked_ses->resume(
std::move(ses->channel),
ses->detector_crypt,
@@ -876,7 +872,7 @@ shared_ptr<ProxyServer::LinkedSession> ProxyServer::create_licensed_session(
uint16_t local_port,
Version version,
const Client::Config& config) {
shared_ptr<LinkedSession> session(new LinkedSession(this->shared_from_this(), local_port, version, l, config));
auto session = make_shared<LinkedSession>(this->shared_from_this(), local_port, version, l, config);
auto emplace_ret = this->id_to_session.emplace(session->id, session);
if (!emplace_ret.second) {
throw runtime_error("session already exists for this license");
+4 -1
View File
@@ -219,7 +219,6 @@ private:
PrefixedLogger log;
Channel channel;
uint16_t local_port;
Version version;
struct sockaddr_storage next_destination;
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt;
@@ -242,6 +241,10 @@ private:
std::shared_ptr<ProxyServer> require_server() const;
std::shared_ptr<ServerState> require_server_state() const;
inline Version version() const {
return this->channel.version;
}
void receive_and_process_commands();
static void on_input(Channel& ch, uint16_t command, uint32_t flag, std::string& msg);
+87 -41
View File
@@ -32,7 +32,7 @@ QuestCategoryIndex::Category::Category(uint32_t category_id, const JSON& json)
QuestCategoryIndex::QuestCategoryIndex(const JSON& json) {
uint32_t next_category_id = 1;
for (const auto& it : json.as_list()) {
this->categories.emplace_back(new Category(next_category_id++, *it));
this->categories.emplace_back(make_shared<Category>(next_category_id++, *it));
}
}
@@ -204,7 +204,9 @@ VersionedQuest::VersionedQuest(
std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules,
ssize_t challenge_template_index)
ssize_t challenge_template_index,
std::shared_ptr<const QuestAvailabilityExpression> available_expression,
std::shared_ptr<const QuestAvailabilityExpression> enabled_expression)
: quest_number(quest_number),
category_id(category_id),
episode(Episode::NONE),
@@ -216,7 +218,9 @@ VersionedQuest::VersionedQuest(
dat_contents(dat_contents),
pvr_contents(pvr_contents),
battle_rules(battle_rules),
challenge_template_index(challenge_template_index) {
challenge_template_index(challenge_template_index),
available_expression(available_expression),
enabled_expression(enabled_expression) {
auto bin_decompressed = prs_decompress(*this->bin_contents);
@@ -315,7 +319,7 @@ VersionedQuest::VersionedQuest(
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderBB*>(bin_decompressed.data());
this->joinable = header->joinable_in_progress;
this->joinable = header->joinable;
this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version);
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = header->quest_number;
@@ -373,7 +377,9 @@ Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
joinable(initial_version->joinable),
name(initial_version->name),
battle_rules(initial_version->battle_rules),
challenge_template_index(initial_version->challenge_template_index) {
challenge_template_index(initial_version->challenge_template_index),
available_expression(initial_version->available_expression),
enabled_expression(initial_version->enabled_expression) {
this->versions.emplace(this->versions_key(initial_version->version, initial_version->language), initial_version);
}
@@ -403,6 +409,18 @@ void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
if (this->challenge_template_index != vq->challenge_template_index) {
throw runtime_error("quest version has different challenge template index");
}
if (!this->available_expression != !vq->available_expression) {
throw runtime_error("quest version has available expression but root quest does not, or vice versa");
}
if (this->available_expression && *this->available_expression != *vq->available_expression) {
throw runtime_error("quest version has a different available expression");
}
if (!this->enabled_expression != !vq->enabled_expression) {
throw runtime_error("quest version has enabled expression but root quest does not, or vice versa");
}
if (this->enabled_expression && *this->enabled_expression != *vq->enabled_expression) {
throw runtime_error("quest version has a different enabled expression");
}
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
}
@@ -459,7 +477,7 @@ QuestIndex::QuestIndex(
if (categories.emplace(name, cat->category_id).first->second != cat->category_id) {
throw runtime_error("file " + name + " exists in multiple categories");
}
shared_ptr<const string> data_ptr(new string(std::move(value)));
auto data_ptr = make_shared<string>(std::move(value));
if (!files.emplace(name, data_ptr).second) {
throw runtime_error("file " + name + " already exists");
}
@@ -483,6 +501,12 @@ QuestIndex::QuestIndex(
} else if (ends_with(filename, ".dlq")) {
file_data = decode_dlq_data(load_file(file_path));
filename.resize(filename.size() - 4);
} else if (ends_with(filename, ".txt")) {
file_data = assemble_quest_script(load_file(file_path));
filename.resize(filename.size() - 4);
if (ends_with(filename, ".bin")) {
filename.push_back('d');
}
} else {
file_data = load_file(file_path);
}
@@ -627,6 +651,8 @@ QuestIndex::QuestIndex(
JSON metadata_json = nullptr;
shared_ptr<BattleRules> battle_rules;
ssize_t challenge_template_index = -1;
shared_ptr<const QuestAvailabilityExpression> available_expression;
shared_ptr<const QuestAvailabilityExpression> enabled_expression;
try {
json_filename = basename;
metadata_json = JSON::parse(*json_files.at(json_filename));
@@ -644,16 +670,24 @@ QuestIndex::QuestIndex(
}
if (!metadata_json.is_null()) {
try {
battle_rules.reset(new BattleRules(metadata_json.at("battle_rules")));
battle_rules = make_shared<BattleRules>(metadata_json.at("BattleRules"));
} catch (const out_of_range&) {
}
try {
challenge_template_index = metadata_json.at("challenge_template_index").as_int();
challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int();
} catch (const out_of_range&) {
}
try {
available_expression = make_shared<QuestAvailabilityExpression>(metadata_json.get_string("AvailableIf"));
} catch (const out_of_range&) {
}
try {
enabled_expression = make_shared<QuestAvailabilityExpression>(metadata_json.get_string("EnabledIf"));
} catch (const out_of_range&) {
}
}
shared_ptr<VersionedQuest> vq(new VersionedQuest(
auto vq = make_shared<VersionedQuest>(
quest_number,
category_id,
version,
@@ -662,7 +696,9 @@ QuestIndex::QuestIndex(
dat_contents,
pvr_contents,
battle_rules,
challenge_template_index));
challenge_template_index,
available_expression,
enabled_expression);
auto category_name = this->category_index->at(vq->category_id)->name;
string dat_str = dat_filename.empty() ? "" : (" with layout from " + dat_filename + ".dat");
@@ -681,7 +717,9 @@ QuestIndex::QuestIndex(
battle_rules_str.c_str(),
challenge_template_str.c_str());
} else {
this->quests_by_number.emplace(vq->quest_number, new Quest(vq));
auto q = make_shared<Quest>(vq);
this->quests_by_number.emplace(vq->quest_number, q);
this->quests_by_category_id_and_number[q->category_id].emplace(vq->quest_number, q);
static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " (%s) (%s, %s (%" PRIu32 "), %s)%s%s%s",
basename.c_str(),
name_for_enum(vq->version),
@@ -710,47 +748,55 @@ shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
}
}
const vector<shared_ptr<const QuestCategoryIndex::Category>>& QuestIndex::categories(
QuestMenuType menu_type, Episode episode, Version version) const {
vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories(
QuestMenuType menu_type,
Episode episode,
Version version,
IncludeCondition include_condition) const {
// The episode filter should apply in normal or solo mode
if ((menu_type != QuestMenuType::NORMAL) && (menu_type != QuestMenuType::SOLO)) {
episode = Episode::NONE;
}
uint64_t key = (static_cast<uint32_t>(menu_type) << 20) | (static_cast<uint32_t>(episode) << 16) | static_cast<uint32_t>(version);
try {
return this->category_filter_results_cache.at(key);
} catch (const out_of_range&) {
auto& ret = this->category_filter_results_cache[key];
for (const auto& cat : this->category_index->categories) {
if (cat->check_flag(menu_type) && !this->filter(menu_type, episode, version, cat->category_id).empty()) {
ret.emplace_back(cat);
}
vector<shared_ptr<const QuestCategoryIndex::Category>> ret;
for (const auto& cat : this->category_index->categories) {
if (cat->check_flag(menu_type) && !this->filter(menu_type, episode, version, cat->category_id, include_condition, 1).empty()) {
ret.emplace_back(cat);
}
return ret;
}
return ret;
}
const vector<shared_ptr<const Quest>>& QuestIndex::filter(
QuestMenuType menu_type, Episode episode, Version version, uint32_t category_id) const {
vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>> QuestIndex::filter(
QuestMenuType menu_type,
Episode episode,
Version version,
uint32_t category_id,
IncludeCondition include_condition,
size_t limit) const {
if ((menu_type != QuestMenuType::NORMAL) && (menu_type != QuestMenuType::SOLO)) {
episode = Episode::NONE;
}
uint64_t key = (static_cast<uint64_t>(episode) << 48) | (static_cast<uint64_t>(version) << 32) | category_id;
try {
return this->quest_filter_results_cache.at(key);
} catch (const out_of_range&) {
vector<shared_ptr<const Quest>>& ret = this->quest_filter_results_cache[key];
for (auto it : this->quests_by_number) {
if (((episode == Episode::NONE) || (it.second->episode == episode)) &&
(it.second->category_id == category_id) &&
it.second->has_version_any_language(version)) {
ret.emplace_back(it.second);
}
}
vector<pair<IncludeState, shared_ptr<const Quest>>> ret;
auto category_it = this->quests_by_category_id_and_number.find(category_id);
if (category_it == this->quests_by_category_id_and_number.end()) {
return ret;
}
for (auto it : category_it->second) {
if (((episode == Episode::NONE) || (it.second->episode == episode)) &&
it.second->has_version_any_language(version)) {
IncludeState state = include_condition ? include_condition(it.second) : IncludeState::AVAILABLE;
if (state == IncludeState::HIDDEN) {
continue;
}
ret.emplace_back(make_pair(state, it.second));
if (limit && (ret.size() >= limit)) {
break;
}
}
}
return ret;
}
string encode_download_quest_data(const string& compressed_data, size_t decompressed_size, uint32_t encryption_seed) {
@@ -845,11 +891,11 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
// Return a new VersionedQuest object with appropriately-processed .bin and
// .dat file contents
shared_ptr<VersionedQuest> dlq(new VersionedQuest(*this));
dlq->bin_contents.reset(new string(encode_download_quest_data(compressed_bin, decompressed_bin.size())));
dlq->dat_contents.reset(new string(encode_download_quest_data(*this->dat_contents)));
auto dlq = make_shared<VersionedQuest>(*this);
dlq->bin_contents = make_shared<string>(encode_download_quest_data(compressed_bin, decompressed_bin.size()));
dlq->dat_contents = make_shared<string>(encode_download_quest_data(*this->dat_contents));
if (this->pvr_contents) {
dlq->pvr_contents.reset(new string(encode_download_quest_data(*this->pvr_contents)));
dlq->pvr_contents = make_shared<string>(encode_download_quest_data(*this->pvr_contents));
}
dlq->is_dlq_encoded = true;
return dlq;
+29 -8
View File
@@ -9,8 +9,10 @@
#include <vector>
#include "PlayerSubordinates.hh"
#include "QuestAvailabilityExpression.hh"
#include "QuestScript.hh"
#include "StaticGameData.hh"
#include "TeamIndex.hh"
enum class QuestFileFormat {
BIN_DAT = 0,
@@ -69,6 +71,8 @@ struct VersionedQuest {
std::shared_ptr<const std::string> pvr_contents;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
std::shared_ptr<const QuestAvailabilityExpression> available_expression;
std::shared_ptr<const QuestAvailabilityExpression> enabled_expression;
VersionedQuest(
uint32_t quest_number,
@@ -79,7 +83,9 @@ struct VersionedQuest {
std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules = nullptr,
ssize_t challenge_template_index = -1);
ssize_t challenge_template_index = -1,
std::shared_ptr<const QuestAvailabilityExpression> available_expression = nullptr,
std::shared_ptr<const QuestAvailabilityExpression> enabled_expression = nullptr);
std::string bin_filename() const;
std::string dat_filename() const;
@@ -112,26 +118,41 @@ public:
std::string name;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
std::shared_ptr<const QuestAvailabilityExpression> available_expression;
std::shared_ptr<const QuestAvailabilityExpression> enabled_expression;
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
};
struct QuestIndex {
enum class IncludeState {
HIDDEN = 0,
AVAILABLE,
DISABLED,
};
using IncludeCondition = std::function<IncludeState(std::shared_ptr<const Quest>)>;
std::string directory;
std::shared_ptr<const QuestCategoryIndex> category_index;
std::map<uint32_t, std::shared_ptr<Quest>> quests_by_number;
mutable std::unordered_map<uint32_t, std::vector<std::shared_ptr<const QuestCategoryIndex::Category>>> category_filter_results_cache;
mutable std::unordered_map<uint64_t, std::vector<std::shared_ptr<const Quest>>> quest_filter_results_cache;
std::map<uint32_t, std::map<uint32_t, std::shared_ptr<Quest>>> quests_by_category_id_and_number;
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index, bool is_ep3);
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
const std::vector<std::shared_ptr<const QuestCategoryIndex::Category>>& categories(
QuestMenuType menu_type, Episode episode, Version version) const;
const std::vector<std::shared_ptr<const Quest>>& filter(
QuestMenuType menu_type, Episode episode, Version version, uint32_t category_id) const;
std::vector<std::shared_ptr<const QuestCategoryIndex::Category>> categories(
QuestMenuType menu_type,
Episode episode,
Version version,
IncludeCondition include_condition = nullptr) const;
std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>> filter(
QuestMenuType menu_type,
Episode episode,
Version version,
uint32_t category_id,
IncludeCondition include_condition = nullptr,
size_t limit = 0) const;
};
std::string encode_download_quest_data(
+248
View File
@@ -0,0 +1,248 @@
#include "QuestAvailabilityExpression.hh"
#include <algorithm>
#include <mutex>
#include <phosg/Encoding.hh>
#include <phosg/Filesystem.hh>
#include <phosg/Hash.hh>
#include <phosg/Random.hh>
#include <phosg/Strings.hh>
#include <phosg/Tools.hh>
#include <string>
#include <unordered_map>
#include "CommandFormats.hh"
#include "Compression.hh"
#include "Loggers.hh"
#include "PSOEncryption.hh"
#include "QuestScript.hh"
#include "SaveFileFormats.hh"
#include "Text.hh"
using namespace std;
QuestAvailabilityExpression::QuestAvailabilityExpression(const string& text)
: root(this->parse_expr(text)) {}
QuestAvailabilityExpression::OrNode::OrNode(unique_ptr<const Node>&& left, unique_ptr<const Node>&& right)
: left(std::move(left)),
right(std::move(right)) {}
bool QuestAvailabilityExpression::OrNode::operator==(const Node& other) const {
try {
const OrNode& other_or = dynamic_cast<const OrNode&>(other);
return *other_or.left == *this->left && *other_or.right == *this->right;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::OrNode::evaluate(
const QuestFlagsForDifficulty& flags, shared_ptr<const TeamIndex::Team> team) const {
return this->left->evaluate(flags, team) || this->right->evaluate(flags, team);
}
string QuestAvailabilityExpression::OrNode::str() const {
return "(" + this->left->str() + ") || (" + this->right->str() + ")";
}
QuestAvailabilityExpression::AndNode::AndNode(unique_ptr<const Node>&& left, unique_ptr<const Node>&& right)
: left(std::move(left)),
right(std::move(right)) {}
bool QuestAvailabilityExpression::AndNode::operator==(const Node& other) const {
try {
const AndNode& other_and = dynamic_cast<const AndNode&>(other);
return *other_and.left == *this->left && *other_and.right == *this->right;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::AndNode::evaluate(
const QuestFlagsForDifficulty& flags, shared_ptr<const TeamIndex::Team> team) const {
return this->left->evaluate(flags, team) && this->right->evaluate(flags, team);
}
string QuestAvailabilityExpression::AndNode::str() const {
return "(" + this->left->str() + ") && (" + this->right->str() + ")";
}
QuestAvailabilityExpression::NotNode::NotNode(unique_ptr<const Node>&& sub)
: sub(std::move(sub)) {}
bool QuestAvailabilityExpression::NotNode::operator==(const Node& other) const {
try {
const NotNode& other_not = dynamic_cast<const NotNode&>(other);
return *other_not.sub == *this->sub;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::NotNode::evaluate(
const QuestFlagsForDifficulty& flags, shared_ptr<const TeamIndex::Team> team) const {
return !this->sub->evaluate(flags, team);
}
string QuestAvailabilityExpression::NotNode::str() const {
return "!(" + this->sub->str() + ")";
}
QuestAvailabilityExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index)
: flag_index(flag_index) {}
bool QuestAvailabilityExpression::FlagLookupNode::operator==(const Node& other) const {
try {
const FlagLookupNode& other_flag = dynamic_cast<const FlagLookupNode&>(other);
return other_flag.flag_index == this->flag_index;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::FlagLookupNode::evaluate(
const QuestFlagsForDifficulty& flags, shared_ptr<const TeamIndex::Team>) const {
return flags.get(this->flag_index);
}
string QuestAvailabilityExpression::FlagLookupNode::str() const {
return string_printf("F_%04hX", this->flag_index);
}
QuestAvailabilityExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name)
: reward_name(reward_name) {}
bool QuestAvailabilityExpression::TeamRewardLookupNode::operator==(const Node& other) const {
try {
const TeamRewardLookupNode& other_team_reward = dynamic_cast<const TeamRewardLookupNode&>(other);
return other_team_reward.reward_name == this->reward_name;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::TeamRewardLookupNode::evaluate(
const QuestFlagsForDifficulty&, shared_ptr<const TeamIndex::Team> team) const {
return team && team->has_reward(this->reward_name);
}
string QuestAvailabilityExpression::TeamRewardLookupNode::str() const {
return "T_" + this->reward_name;
}
QuestAvailabilityExpression::ConstantNode::ConstantNode(bool value)
: value(value) {}
bool QuestAvailabilityExpression::ConstantNode::operator==(const Node& other) const {
try {
const ConstantNode& other_const = dynamic_cast<const ConstantNode&>(other);
return other_const.value == this->value;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::ConstantNode::evaluate(
const QuestFlagsForDifficulty&, shared_ptr<const TeamIndex::Team>) const {
return this->value;
}
string QuestAvailabilityExpression::ConstantNode::str() const {
return this->value ? "true" : "false";
}
unique_ptr<const QuestAvailabilityExpression::Node> QuestAvailabilityExpression::parse_expr(string_view text) {
// Strip off spaces and fully-enclosing parentheses
for (;;) {
size_t starting_size = text.size();
while (text.at(0) == ' ') {
text = text.substr(1);
}
while (text.at(text.size() - 1) == ' ') {
text = text.substr(0, text.size() - 1);
}
if (text.at(0) == '(' && text.at(text.size() - 1) == ')') {
// It doesn't suffice to just check the first ant last characters, since
// text could be like "(a) && (b)". Instead, we ignore the first and last
// characters, and don't strip anything if the internal parentheses are
// unbalanced.
size_t paren_level = 1;
for (size_t z = 1; z < text.size() - 1; z++) {
if (text[z] == '(') {
paren_level++;
} else if (text[z] == ')') {
paren_level--;
if (paren_level == 0) {
break;
}
}
}
if (paren_level > 0) {
text = text.substr(1, text.size() - 2);
}
}
if (text.size() == starting_size) {
break;
}
}
// Check for binary operators at the root level
size_t paren_level = 0;
size_t and_pos = 0;
size_t or_pos = 0;
for (size_t z = 0; z < text.size() - 1; z++) {
if (text[z] == '(') {
paren_level++;
} else if (text[z] == ')') {
paren_level--;
} else if ((text[z] == '&') && (text[z + 1] == '&') && !paren_level) {
and_pos = z;
} else if ((text[z] == '|') && (text[z + 1] == '|') && !paren_level) {
or_pos = z;
}
}
if ((or_pos && (!and_pos || (and_pos > or_pos)))) {
auto left = QuestAvailabilityExpression::parse_expr(text.substr(0, or_pos));
auto right = QuestAvailabilityExpression::parse_expr(text.substr(or_pos + 2));
return make_unique<OrNode>(std::move(left), std::move(right));
}
if ((and_pos && (!or_pos || (or_pos > and_pos)))) {
auto left = QuestAvailabilityExpression::parse_expr(text.substr(0, and_pos));
auto right = QuestAvailabilityExpression::parse_expr(text.substr(and_pos + 2));
return make_unique<AndNode>(std::move(left), std::move(right));
}
// Check for not operator
if (text.at(0) == '!') {
auto sub = QuestAvailabilityExpression::parse_expr(text.substr(1));
return make_unique<NotNode>(std::move(sub));
}
// Check for constants
if (text == "true") {
return make_unique<ConstantNode>(true);
}
if (text == "false") {
return make_unique<ConstantNode>(false);
}
// Check for flag lookups
if (text.starts_with("F_")) {
char* endptr = nullptr;
uint64_t flag = strtoul(text.data() + 2, &endptr, 16);
if (endptr != text.data() + text.size()) {
throw runtime_error("invalid flag lookup token");
}
if (flag >= 0x400) {
throw runtime_error("invalid flag index");
}
return make_unique<FlagLookupNode>(flag);
}
if (text.starts_with("T_")) {
return make_unique<TeamRewardLookupNode>(string(text.substr(2)));
}
throw runtime_error("unparseable expression");
}
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include <stdint.h>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "PlayerSubordinates.hh"
#include "QuestScript.hh"
#include "StaticGameData.hh"
#include "TeamIndex.hh"
class QuestAvailabilityExpression {
public:
QuestAvailabilityExpression(const std::string& text);
~QuestAvailabilityExpression() = default;
inline bool operator==(const QuestAvailabilityExpression& other) const {
return this->root->operator==(*other.root);
}
inline bool operator!=(const QuestAvailabilityExpression& other) const {
return !this->operator==(other);
}
inline bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const {
return this->root->evaluate(flags, team);
}
inline std::string str() const {
return this->root->str();
}
protected:
class Node {
public:
virtual ~Node() = default;
virtual bool operator==(const Node& other) const = 0;
inline bool operator!=(const Node& other) const {
return !this->operator==(other);
}
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const = 0;
virtual std::string str() const = 0;
protected:
Node() = default;
};
class OrNode : public Node {
public:
OrNode(std::unique_ptr<const Node>&& left, std::unique_ptr<const Node>&& right);
virtual ~OrNode() = default;
virtual bool operator==(const Node& other) const;
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
std::unique_ptr<const Node> left;
std::unique_ptr<const Node> right;
};
class AndNode : public Node {
public:
AndNode(std::unique_ptr<const Node>&& left, std::unique_ptr<const Node>&& right);
virtual ~AndNode() = default;
virtual bool operator==(const Node& other) const;
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
std::unique_ptr<const Node> left;
std::unique_ptr<const Node> right;
};
class NotNode : public Node {
public:
NotNode(std::unique_ptr<const Node>&& sub);
virtual ~NotNode() = default;
virtual bool operator==(const Node& other) const;
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
std::unique_ptr<const Node> sub;
};
class FlagLookupNode : public Node {
public:
FlagLookupNode(uint16_t flag_index);
virtual ~FlagLookupNode() = default;
virtual bool operator==(const Node& other) const;
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
uint16_t flag_index;
};
class TeamRewardLookupNode : public Node {
public:
TeamRewardLookupNode(const std::string& reward_name);
virtual ~TeamRewardLookupNode() = default;
virtual bool operator==(const Node& other) const;
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
std::string reward_name;
};
class ConstantNode : public Node {
public:
ConstantNode(bool value);
virtual ~ConstantNode() = default;
virtual bool operator==(const Node& other) const;
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
bool value;
};
std::unique_ptr<const Node> parse_expr(std::string_view text);
std::unique_ptr<const Node> root;
};
+604 -40
View File
@@ -40,24 +40,28 @@ static TextEncoding encoding_for_language(uint8_t language) {
static string escape_string(const string& data, TextEncoding encoding = TextEncoding::UTF8) {
string decoded;
switch (encoding) {
case TextEncoding::UTF8:
decoded = data;
break;
case TextEncoding::UTF16:
decoded = tt_utf16_to_utf8(data);
break;
case TextEncoding::SJIS:
decoded = tt_sjis_to_utf8(data);
break;
case TextEncoding::ISO8859:
decoded = tt_8859_to_utf8(data);
break;
case TextEncoding::ASCII:
decoded = tt_ascii_to_utf8(data);
break;
default:
return format_data_string(data);
try {
switch (encoding) {
case TextEncoding::UTF8:
decoded = data;
break;
case TextEncoding::UTF16:
decoded = tt_utf16_to_utf8(data);
break;
case TextEncoding::SJIS:
decoded = tt_sjis_to_utf8(data);
break;
case TextEncoding::ISO8859:
decoded = tt_8859_to_utf8(data);
break;
case TextEncoding::ASCII:
decoded = tt_ascii_to_utf8(data);
break;
default:
return format_data_string(data);
}
} catch (const runtime_error&) {
return format_data_string(data);
}
string ret = "\"";
@@ -489,7 +493,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0x00ED, "create_bgmctrl", {}, F_V1_V4},
{0x00EE, "pl_add_meseta2", {INT32}, F_V1_V4 | F_ARGS},
{0x00EF, "sync_register2", {INT32, REG32}, F_V1_V2},
{0x00EF, "sync_register2", {INT32, INT32}, F_V3_V4 | F_ARGS},
{0x00EF, "sync_register2", {REG, INT32}, F_V3_V4 | F_ARGS},
{0x00F0, "send_regwork", {INT32, REG32}, F_V1_V2},
{0x00F1, "leti_fixed_camera", {{REG32_SET_FIXED, 6}}, F_V2},
{0x00F1, "leti_fixed_camera", {{REG_SET_FIXED, 6}}, F_V3_V4},
@@ -618,7 +622,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF889, "disable_win_pfx", {}, F_V2_V4},
{0xF88A, "get_player_status", {REG, REG}, F_V2_V4},
{0xF88B, "send_mail", {REG, CSTRING}, F_V2_V4 | F_ARGS},
{0xF88C, "get_game_version", {REG}, F_V2_V4}, // Returns 2 on DCv2/PC, 3 on GC, 4 on BB
{0xF88C, "get_game_version", {REG}, F_V2_V4}, // Returns 2 on DCv2/PC, 3 on GC, 4 on XB and BB
{0xF88D, "chl_set_timerecord", {REG}, F_V2 | F_V3},
{0xF88D, "chl_set_timerecord", {REG, REG}, F_V4},
{0xF88E, "chl_get_timerecord", {REG}, F_V2_V4},
@@ -668,7 +672,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF8B5, "write4", {REG, REG}, F_V2},
{0xF8B5, "write4", {INT32, INT32}, F_V3_V4 | F_ARGS},
{0xF8B6, "check_for_hacking", {REG}, F_V2}, // Returns a bitmask of 5 different types of detectable hacking. But it only works on DCv2 - it crashes on all other versions.
{0xF8B7, nullptr, {REG}, F_V2_V4}, // TODO (DX) - Challenge mode. Appears to be timing-related; regA is expected to be in [60, 3600]. Encodes the value with encrypt_challenge_time even though it's never sent over the network and is only decrypted locally.
{0xF8B7, "unknown_F8B7", {REG}, F_V2_V4}, // TODO (DX) - Challenge mode. Appears to be timing-related; regA is expected to be in [60, 3600]. Encodes the value with encrypt_challenge_time even though it's never sent over the network and is only decrypted locally.
{0xF8B8, "disable_retry_menu", {}, F_V2_V4},
{0xF8B9, "chl_recovery", {}, F_V2_V4},
{0xF8BA, "load_guild_card_file_creation_time_to_flag_buf", {}, F_V2_V4},
@@ -731,7 +735,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF8EF, "nop_F8EF", {}, F_V3_V4},
{0xF8F0, "turn_off_bgm_p2", {}, F_V3_V4},
{0xF8F1, "turn_on_bgm_p2", {}, F_V3_V4},
{0xF8F2, nullptr, {INT32, FLOAT32, FLOAT32, INT32, {REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::UNKNOWN_F8F2_DATA}}, F_V3_V4 | F_ARGS}, // TODO (DX)
{0xF8F2, "unknown_F8F2", {INT32, FLOAT32, FLOAT32, INT32, {REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::UNKNOWN_F8F2_DATA}}, F_V3_V4 | F_ARGS}, // TODO (DX)
{0xF8F3, "particle2", {{REG_SET_FIXED, 3}, INT32, FLOAT32}, F_V3_V4 | F_ARGS},
{0xF901, "dec2float", {REG, REG}, F_V3_V4},
{0xF902, "float2dec", {REG, REG}, F_V3_V4},
@@ -813,7 +817,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF94B, "particle_effect_nc", {{REG_SET_FIXED, 4}}, F_V3_V4},
{0xF94C, "player_effect_nc", {{REG_SET_FIXED, 4}}, F_V3_V4},
{0xF94D, "give_or_take_card", {{REG_SET_FIXED, 2}}, F_GC_EP3}, // regsA[0] is card_id; card is given if regsA[1] >= 0, otherwise it's taken
{0xF94D, nullptr, {INT32, REG}, F_XB_V3 | F_ARGS}, // Related to voice chat. argA is a client ID; a value is read from that player's TVoiceChatClient object and (!!value) is placed in regB. This value is set by the 6xB3 command; TODO: figure out what that value represents and name this opcode appropriately
{0xF94D, "unknown_F94D", {INT32, REG}, F_XB_V3 | F_ARGS}, // Related to voice chat. argA is a client ID; a value is read from that player's TVoiceChatClient object and (!!value) is placed in regB. This value is set by the 6xB3 command; TODO: figure out what that value represents and name this opcode appropriately
{0xF94D, "nop_F94D", {}, F_V4},
{0xF94E, "nop_F94E", {}, F_V4},
{0xF94F, "nop_F94F", {}, F_V4},
@@ -823,7 +827,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
{0xF953, "bb_swap_item", {INT32, INT32, INT32, INT32, INT32, INT32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS}, // Sends 6xD5
{0xF954, "bb_check_wrap", {INT32, REG}, F_V4 | F_ARGS},
{0xF955, "bb_exchange_pd_item", {INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xD7
{0xF956, "bb_exchange_pd_srank", {INT32, INT32, INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xD8
{0xF956, "bb_exchange_pd_srank", {INT32, INT32, INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xD8; argsA[0] is item ID; argsA[1]-[3] are item.data1[0]-[2]; argsA[4] is special type; argsA[5] is success label; argsA[6] is failure label
{0xF957, "bb_exchange_pd_percent", {INT32, INT32, INT32, INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xDA
{0xF958, "bb_exchange_ps_percent", {INT32, INT32, INT32, INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xDA
{0xF959, "bb_set_ep4_boss_can_escape", {INT32}, F_V4 | F_ARGS},
@@ -860,9 +864,36 @@ opcodes_for_version(Version v) {
return index;
}
static const unordered_map<string, const QuestScriptOpcodeDefinition*>&
opcodes_by_name_for_version(Version v) {
static array<
unordered_map<string, const QuestScriptOpcodeDefinition*>,
static_cast<size_t>(Version::BB_V4) + 1>
indexes;
auto& index = indexes.at(static_cast<size_t>(v));
if (index.empty()) {
uint16_t vf = v_flag(v);
for (size_t z = 0; z < sizeof(opcode_defs) / sizeof(opcode_defs[0]); z++) {
const auto& def = opcode_defs[z];
if (!(def.flags & vf)) {
continue;
}
if (!def.name) {
continue;
}
if (!index.emplace(def.name, &def).second) {
throw logic_error(string_printf("duplicate definition for opcode %04hX", def.opcode));
}
}
}
return index;
}
std::string disassemble_quest_script(const void* data, size_t size, Version version, uint8_t language) {
StringReader r(data, size);
deque<string> lines;
lines.emplace_back(string_printf(".version %s", name_for_enum(version)));
bool use_wstrs = false;
size_t code_offset = 0;
@@ -882,7 +913,9 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
const auto& header = r.get<PSOQuestHeaderDC>();
code_offset = header.code_offset;
function_table_offset = header.function_table_offset;
language = header.language;
if (header.language < 5) {
language = header.language;
}
lines.emplace_back(string_printf(".quest_num %hu", header.quest_number.load()));
lines.emplace_back(string_printf(".language %hhu", header.language));
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
@@ -895,7 +928,9 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
const auto& header = r.get<PSOQuestHeaderPC>();
code_offset = header.code_offset;
function_table_offset = header.function_table_offset;
language = header.language;
if (header.language < 8) {
language = header.language;
}
lines.emplace_back(string_printf(".quest_num %hu", header.quest_number.load()));
lines.emplace_back(string_printf(".language %hhu", header.language));
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
@@ -911,7 +946,9 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
const auto& header = r.get<PSOQuestHeaderGC>();
code_offset = header.code_offset;
function_table_offset = header.function_table_offset;
language = header.language;
if (header.language < 5) {
language = header.language;
}
lines.emplace_back(string_printf(".quest_num %hhu", header.quest_number));
lines.emplace_back(string_printf(".language %hhu", header.language));
lines.emplace_back(string_printf(".episode %hhu", header.episode));
@@ -928,8 +965,8 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
lines.emplace_back(string_printf(".quest_num %hu", header.quest_number.load()));
lines.emplace_back(string_printf(".episode %hhu", header.episode));
lines.emplace_back(string_printf(".max_players %hhu", header.episode));
if (header.joinable_in_progress) {
lines.emplace_back(".joinable_in_progress");
if (header.joinable) {
lines.emplace_back(".joinable");
}
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
@@ -971,7 +1008,7 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
uint32_t function_id = function_table.size();
string name = string_printf("label%04" PRIX32, function_id);
uint32_t offset = function_table_r.get_u32l();
shared_ptr<Label> l(new Label(name, offset, function_id));
auto l = make_shared<Label>(name, offset, function_id);
if (function_id == 0) {
l->add_data_type(Arg::DataType::SCRIPT);
}
@@ -1081,7 +1118,7 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
}
uint8_t num_functions = cmd_r.get_u8();
for (size_t z = 0; z < num_functions; z++) {
dasm_arg += (dasm_arg.empty() ? "(" : ", ");
dasm_arg += (dasm_arg.empty() ? "[" : ", ");
uint32_t label_id = cmd_r.get_u16l();
if (label_id >= function_table.size()) {
dasm_arg += string_printf("function%04" PRIX32 " /* invalid */", label_id);
@@ -1096,9 +1133,9 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
}
}
if (dasm_arg.empty()) {
dasm_arg = "()";
dasm_arg = "[]";
} else {
dasm_arg += ")";
dasm_arg += "]";
}
break;
}
@@ -1116,12 +1153,12 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
}
uint8_t num_regs = cmd_r.get_u8();
for (size_t z = 0; z < num_regs; z++) {
dasm_arg += string_printf("%sr%hhu", (dasm_arg.empty() ? "(" : ", "), cmd_r.get_u8());
dasm_arg += string_printf("%sr%hhu", (dasm_arg.empty() ? "[" : ", "), cmd_r.get_u8());
}
if (dasm_arg.empty()) {
dasm_arg = "()";
dasm_arg = "[]";
} else {
dasm_arg += ")";
dasm_arg += "]";
}
break;
}
@@ -1283,7 +1320,7 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
case Arg::Type::FLOAT32:
switch (arg_value.type) {
case ArgStackValue::Type::REG:
dasm_arg = string_printf("(float)r%" PRIu32, arg_value.as_int);
dasm_arg = string_printf("f%" PRIu32, arg_value.as_int);
break;
case ArgStackValue::Type::INT:
dasm_arg = string_printf("%g", as_type<float>(arg_value.as_int));
@@ -1412,8 +1449,7 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
string unused = format_data_string(visual.unused.data(), visual.unused.bytes());
lines.emplace_back(string_printf(" %04zX unused %s", l->offset + offsetof(PlayerVisualConfig, unused), unused.c_str()));
lines.emplace_back(string_printf(" %04zX name_color_checksum %08" PRIX32, l->offset + offsetof(PlayerVisualConfig, name_color_checksum), visual.name_color_checksum.load()));
string secid_name = name_for_section_id(visual.section_id);
lines.emplace_back(string_printf(" %04zX section_id %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, section_id), visual.section_id, secid_name.c_str()));
lines.emplace_back(string_printf(" %04zX section_id %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, section_id), visual.section_id, name_for_section_id(visual.section_id)));
lines.emplace_back(string_printf(" %04zX char_class %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, char_class), visual.char_class, name_for_char_class(visual.char_class)));
lines.emplace_back(string_printf(" %04zX validation_flags %02hhX", l->offset + offsetof(PlayerVisualConfig, validation_flags), visual.validation_flags));
lines.emplace_back(string_printf(" %04zX version %02hhX", l->offset + offsetof(PlayerVisualConfig, version), visual.version));
@@ -1589,7 +1625,7 @@ Episode find_quest_episode_from_script(const void* data, size_t size, Version ve
}
if (def == nullptr) {
throw runtime_error("unknown quest opcode");
throw runtime_error(string_printf("unknown quest opcode %04hX", opcode));
}
if (def->flags & F_RET) {
@@ -1688,3 +1724,531 @@ Episode episode_for_quest_episode_number(uint8_t episode_number) {
throw runtime_error(string_printf("invalid episode number %02hhX", episode_number));
}
}
std::string assemble_quest_script(const std::string& text) {
auto lines = split(text, '\n');
// Strip comments and whitespace
for (auto& line : lines) {
size_t comment_start = line.find("/*");
while (comment_start != string::npos) {
size_t comment_end = line.find("*/", comment_start + 2);
if (comment_end == string::npos) {
throw runtime_error("unterminated inline comment");
}
line.erase(comment_start, comment_end + 2 - comment_start);
comment_start = line.find("/*");
}
comment_start = line.find("//");
if (comment_start != string::npos) {
line.resize(comment_start);
}
strip_trailing_whitespace(line);
strip_leading_whitespace(line);
}
// Collect metadata directives
Version quest_version = Version::UNKNOWN;
string quest_name;
string quest_short_desc;
string quest_long_desc;
int64_t quest_num = -1;
uint8_t quest_language = 1;
Episode quest_episode = Episode::EP1;
uint8_t quest_max_players = 4;
bool quest_joinable = false;
for (const auto& line : lines) {
if (line.empty()) {
continue;
}
if (line[0] == '.') {
if (starts_with(line, ".version ")) {
string name = line.substr(9);
quest_version = enum_for_name<Version>(name.c_str());
} else if (starts_with(line, ".name ")) {
quest_name = parse_data_string(line.substr(6));
} else if (starts_with(line, ".short_desc ")) {
quest_short_desc = parse_data_string(line.substr(12));
} else if (starts_with(line, ".long_desc ")) {
quest_long_desc = parse_data_string(line.substr(11));
} else if (starts_with(line, ".quest_num ")) {
quest_num = stoul(line.substr(11), nullptr, 0);
} else if (starts_with(line, ".language ")) {
quest_language = stoul(line.substr(10), nullptr, 0);
} else if (starts_with(line, ".episode ")) {
quest_episode = episode_for_token_name(line.substr(9));
} else if (starts_with(line, ".max_players ")) {
quest_max_players = stoul(line.substr(12), nullptr, 0);
} else if (starts_with(line, ".joinable ")) {
quest_joinable = true;
}
}
}
if (quest_version == Version::PC_PATCH || quest_version == Version::BB_PATCH || quest_version == Version::UNKNOWN) {
throw runtime_error(".version directive is missing or invalid");
}
if (quest_num < 0) {
throw runtime_error(".quest_num directive is missing or invalid");
}
if (quest_name.empty()) {
throw runtime_error(".name directive is missing or invalid");
}
// Find all label names
struct Label {
std::string name;
ssize_t index = -1;
ssize_t offset = -1;
};
unordered_map<string, shared_ptr<Label>> labels_by_name;
map<ssize_t, shared_ptr<Label>> labels_by_index;
for (size_t line_num = 1; line_num <= lines.size(); line_num++) {
const auto& line = lines[line_num - 1];
if (ends_with(line, ":")) {
auto label = make_shared<Label>();
label->name = line.substr(0, line.size() - 1);
size_t at_offset = label->name.find('@');
if (at_offset != string::npos) {
label->index = stoul(label->name.substr(at_offset + 1), nullptr, 0);
label->name.resize(at_offset);
if (label->name == "start" && label->index != 0) {
throw runtime_error("start label cannot have a nonzero label ID");
}
} else if (label->name == "start") {
label->index = 0;
}
if (!labels_by_name.emplace(label->name, label).second) {
throw runtime_error(string_printf("(line %zu) duplicate label name: %s", line_num, label->name.c_str()));
}
if (label->index >= 0) {
auto index_emplace_ret = labels_by_index.emplace(label->index, label);
if (label->index >= 0 && !index_emplace_ret.second) {
throw runtime_error(string_printf("(line %zu) duplicate label index: %zd (0x%zX) from %s and %s", line_num, label->index, label->index, label->name.c_str(), index_emplace_ret.first->second->name.c_str()));
}
}
}
}
if (!labels_by_name.count("start")) {
throw runtime_error("start label is not defined");
}
// Assign indexes to labels without explicit indexes
{
size_t next_index = 0;
for (auto& it : labels_by_name) {
if (it.second->index >= 0) {
continue;
}
while (labels_by_index.count(next_index)) {
next_index++;
}
it.second->index = next_index++;
labels_by_index.emplace(it.second->index, it.second);
}
}
// Assemble code segment
const auto& opcodes = opcodes_by_name_for_version(quest_version);
StringWriter code_w;
for (size_t line_num = 1; line_num <= lines.size(); line_num++) {
try {
const auto& line = lines[line_num - 1];
if (line.empty()) {
continue;
}
if (ends_with(line, ":")) {
size_t at_offset = line.find('@');
string label_name = line.substr(0, (at_offset == string::npos) ? (line.size() - 1) : at_offset);
labels_by_name.at(label_name)->offset = code_w.size();
continue;
}
if (line[0] == '.') {
if (starts_with(line, ".data ")) {
code_w.write(parse_data_string(line.substr(6)));
} else if (starts_with(line, ".zero ")) {
size_t size = stoull(line.substr(6), nullptr, 0);
code_w.extend_by(size, 0x00);
}
continue;
}
auto line_tokens = split(line, ' ', 1);
const auto& opcode_def = opcodes.at(line_tokens.at(0));
if (!(opcode_def->flags & F_ARGS)) {
if ((opcode_def->opcode & 0xFF00) == 0x0000) {
code_w.put_u8(opcode_def->opcode);
} else {
code_w.put_u16b(opcode_def->opcode);
}
}
if (opcode_def->args.empty()) {
if (line_tokens.size() > 1) {
throw runtime_error(string_printf("(line %zu) arguments not allowed for %s", line_num, opcode_def->name));
}
continue;
}
if (line_tokens.size() < 2) {
throw runtime_error(string_printf("(line %zu) arguments required for %s", line_num, opcode_def->name));
}
auto args = split_context(line_tokens[1], ',');
if (args.size() != opcode_def->args.size()) {
throw runtime_error(string_printf("(line %zu) incorrect argument count for %s", line_num, opcode_def->name));
}
for (size_t z = 0; z < args.size(); z++) {
using Type = QuestScriptOpcodeDefinition::Argument::Type;
string& arg = args[z];
const auto& arg_def = opcode_def->args[z];
strip_trailing_whitespace(arg);
strip_leading_whitespace(arg);
try {
auto parse_reg = +[](const string& name) -> uint8_t {
if ((name[0] != 'r') && (name[0] != 'f')) {
throw runtime_error("a register is required");
}
size_t reg_num = stoull(name.substr(1), nullptr, 0);
if (reg_num > 0xFF) {
throw runtime_error("invalid register number");
}
return reg_num;
};
auto add_cstr = [&](const string& text) -> void {
switch (quest_version) {
case Version::DC_NTE:
code_w.write(tt_utf8_to_sjis(text));
code_w.put_u8(0);
break;
case Version::DC_V1_11_2000_PROTOTYPE:
case Version::DC_V1:
case Version::DC_V2:
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
case Version::XB_V3:
code_w.write(quest_language ? tt_utf8_to_8859(text) : tt_utf8_to_sjis(text));
code_w.put_u8(0);
break;
case Version::PC_V2:
case Version::BB_V4:
code_w.write(tt_utf8_to_utf16(text));
code_w.put_u16(0);
break;
default:
throw logic_error("invalid game version");
}
};
if (opcode_def->flags & F_ARGS) {
auto label_it = labels_by_name.find(arg);
if (starts_with(line_tokens[1], "...")) {
// Args were specified by preceding arg_push calls; nothing to do here
} else if (arg.empty()) {
throw runtime_error("argument is empty");
} else if (label_it != labels_by_name.end()) {
code_w.put_u8(0x4B); // arg_pushw
code_w.put_u16l(label_it->second->index);
} else if ((arg[0] == 'r') || (arg[0] == 'f')) {
// If the corresponding argument is a REG or REG_SET_FIXED, push
// the register number, not the register's value, since it's an
// out-param
if ((arg_def.type == Type::REG) || (arg_def.type == Type::REG32)) {
code_w.put_u8(0x4A); // arg_pushb
code_w.put_u8(parse_reg(arg));
} else if (
(arg_def.type == Type::REG_SET_FIXED) ||
(arg_def.type == Type::REG32_SET_FIXED)) {
auto tokens = split(arg, '-');
uint8_t start_reg;
if (tokens.size() == 1) {
start_reg = parse_reg(tokens[0]);
} else if (tokens.size() == 2) {
start_reg = parse_reg(tokens[0]);
if (static_cast<size_t>(parse_reg(tokens[1]) - start_reg + 1) != arg_def.count) {
throw runtime_error("incorrect number of registers used");
}
} else {
throw runtime_error("invalid fixed register set syntax");
}
code_w.put_u8(0x4A); // arg_pushb
code_w.put_u8(start_reg);
} else {
code_w.put_u8(0x48); // arg_pushr
code_w.put_u8(parse_reg(arg));
}
} else if ((arg[0] == '@') && ((arg[1] == 'r') || (arg[1] == 'f'))) {
code_w.put_u8(0x4C); // arg_pusha
code_w.put_u8(parse_reg(arg.substr(1)));
} else if ((arg[0] == '@') && labels_by_name.count(arg.substr(1))) {
code_w.put_u8(0x4D); // arg_pusho
code_w.put_u16(labels_by_name.at(arg.substr(1))->index);
} else {
bool write_as_str = false;
try {
size_t end_offset;
uint64_t value = stoll(arg, &end_offset, 0);
if (end_offset != arg.size()) {
write_as_str = true;
} else if (value > 0xFFFF) {
code_w.put_u8(0x49); // arg_pushl
code_w.put_u32l(value);
} else if (value > 0xFF) {
code_w.put_u8(0x4B); // arg_pushw
code_w.put_u16l(value);
} else {
code_w.put_u8(0x4A); // arg_pushb
code_w.put_u8(value);
}
} catch (const exception&) {
write_as_str = true;
}
if (write_as_str) {
if (arg[0] == '\"') {
code_w.put_u8(0x4E); // arg_pushs
add_cstr(parse_data_string(arg));
} else {
throw runtime_error("invalid argument syntax");
}
}
}
} else { // Not F_ARGS
auto add_label = [&](const string& name, bool is32) -> void {
if (!labels_by_name.count(name)) {
throw runtime_error("label not defined: " + name);
}
if (is32) {
code_w.put_u32(labels_by_name.at(name)->index);
} else {
code_w.put_u16(labels_by_name.at(name)->index);
}
};
auto add_reg = [&](const string& name, bool is32) -> void {
if (is32) {
code_w.put_u32l(parse_reg(name));
} else {
code_w.put_u8(parse_reg(name));
}
};
auto split_set = [&](const string& text) -> vector<string> {
if (!starts_with(text, "[") || !ends_with(text, "]")) {
throw runtime_error("incorrect syntax for set-valued argument");
}
auto values = split(text.substr(1, text.size() - 2), ',');
if (values.size() > 0xFF) {
throw runtime_error("too many labels in set-valued argument");
}
return values;
};
switch (arg_def.type) {
case Type::LABEL16:
case Type::LABEL32:
add_label(arg, arg_def.type == Type::LABEL32);
break;
case Type::LABEL16_SET: {
auto label_names = split_set(arg);
code_w.put_u8(label_names.size());
for (auto name : label_names) {
strip_trailing_whitespace(name);
strip_leading_whitespace(name);
add_label(name, false);
}
break;
}
case Type::REG:
case Type::REG32:
add_reg(arg, arg_def.type == Type::REG32);
break;
case Type::REG_SET_FIXED:
case Type::REG32_SET_FIXED: {
auto tokens = split(arg, '-');
if (tokens.size() == 1) {
add_reg(tokens[0], arg_def.type == Type::REG32_SET_FIXED);
} else if (tokens.size() == 2) {
if (static_cast<size_t>(parse_reg(tokens[1]) - parse_reg(tokens[0]) + 1) != arg_def.count) {
throw runtime_error("incorrect number of registers used");
}
add_reg(tokens[0], arg_def.type == Type::REG32_SET_FIXED);
} else {
throw runtime_error("invalid fixed register set syntax");
}
break;
}
case Type::REG_SET: {
auto regs = split_set(arg);
code_w.put_u8(regs.size());
for (auto reg : regs) {
strip_trailing_whitespace(reg);
strip_leading_whitespace(reg);
add_reg(reg, false);
}
break;
}
case Type::INT8:
code_w.put_u8(stol(arg, nullptr, 0));
break;
case Type::INT16:
code_w.put_u16l(stol(arg, nullptr, 0));
break;
case Type::INT32:
code_w.put_u32l(stol(arg, nullptr, 0));
break;
case Type::FLOAT32:
code_w.put_u32l(stof(arg, nullptr));
break;
case Type::CSTRING:
add_cstr(parse_data_string(arg));
break;
default:
throw logic_error("unknown argument type");
}
}
} catch (const exception& e) {
throw runtime_error(string_printf("(arg %zu) %s", z + 1, e.what()));
}
}
if (opcode_def->flags & F_ARGS) {
if ((opcode_def->opcode & 0xFF00) == 0x0000) {
code_w.put_u8(opcode_def->opcode);
} else {
code_w.put_u16b(opcode_def->opcode);
}
}
} catch (const exception& e) {
throw runtime_error(string_printf("(line %zu) %s", line_num, e.what()));
}
}
while (code_w.size() & 3) {
code_w.put_u8(0);
}
// Generate function table
ssize_t function_table_size = labels_by_index.rbegin()->first + 1;
vector<le_uint32_t> function_table;
function_table.reserve(function_table_size);
{
auto it = labels_by_index.begin();
for (ssize_t z = 0; z < function_table_size; z++) {
if (it == labels_by_index.end()) {
throw logic_error("function table size exceeds maximum function ID");
} else if (it->first > z) {
function_table.emplace_back(0xFFFFFFFF);
} else if (it->first == z) {
if (it->second->offset < 0) {
throw runtime_error("label " + it->second->name + " does not have a valid offset");
}
function_table.emplace_back(it->second->offset);
it++;
} else if (it->first < z) {
throw logic_error("missed label " + it->second->name + " when compiling function table");
}
}
}
// Generate header
StringWriter w;
switch (quest_version) {
case Version::DC_NTE: {
PSOQuestHeaderDCNTE header;
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.unused = 0;
header.name.encode(quest_name, 0);
w.put(header);
break;
}
case Version::DC_V1_11_2000_PROTOTYPE:
case Version::DC_V1:
case Version::DC_V2: {
PSOQuestHeaderDC header;
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.unused = 0;
header.language = quest_language;
header.unknown1 = 0;
header.quest_number = quest_num;
header.name.encode(quest_name, quest_language);
header.short_description.encode(quest_short_desc, quest_language);
header.long_description.encode(quest_long_desc, quest_language);
w.put(header);
break;
}
case Version::PC_V2: {
PSOQuestHeaderPC header;
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.unused = 0;
header.language = quest_language;
header.unknown1 = 0;
header.quest_number = quest_num;
header.name.encode(quest_name, quest_language);
header.short_description.encode(quest_short_desc, quest_language);
header.long_description.encode(quest_long_desc, quest_language);
w.put(header);
break;
}
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
case Version::XB_V3: {
PSOQuestHeaderGC header;
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.unused = 0;
header.language = quest_language;
header.unknown1 = 0;
header.quest_number = quest_num;
header.episode = (quest_episode == Episode::EP2) ? 1 : 0;
header.name.encode(quest_name, quest_language);
header.short_description.encode(quest_short_desc, quest_language);
header.long_description.encode(quest_long_desc, quest_language);
w.put(header);
break;
}
case Version::BB_V4: {
PSOQuestHeaderBB header;
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.unused = 0;
header.quest_number = quest_num;
header.unused2 = 0;
if (quest_episode == Episode::EP4) {
header.episode = 2;
} else if (quest_episode == Episode::EP2) {
header.episode = 1;
} else {
header.episode = 0;
}
header.max_players = quest_max_players;
header.joinable = quest_joinable ? 1 : 0;
header.unknown = 0;
header.name.encode(quest_name, quest_language);
header.short_description.encode(quest_short_desc, quest_language);
header.long_description.encode(quest_long_desc, quest_language);
w.put(header);
break;
}
default:
throw logic_error("invalid quest version");
}
w.write(code_w.str());
w.write(function_table.data(), function_table.size() * sizeof(function_table[0]));
return std::move(w.str());
}
+2 -1
View File
@@ -72,7 +72,7 @@ struct PSOQuestHeaderBB {
/* 0012 */ le_uint16_t unused2;
/* 0014 */ uint8_t episode; // 0 = Ep1, 1 = Ep2, 2 = Ep4
/* 0015 */ uint8_t max_players;
/* 0016 */ uint8_t joinable_in_progress;
/* 0016 */ uint8_t joinable;
/* 0017 */ uint8_t unknown;
/* 0018 */ pstring<TextEncoding::UTF16, 0x20> name;
/* 0058 */ pstring<TextEncoding::UTF16, 0x80> short_description;
@@ -83,5 +83,6 @@ struct PSOQuestHeaderBB {
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 language);
std::string assemble_quest_script(const std::string& text);
Episode find_quest_episode_from_script(const void* data, size_t size, Version version);
+10 -8
View File
@@ -31,12 +31,15 @@ string RareItemSet::ExpandedDrop::str(Version version, shared_ptr<const ItemName
}
uint32_t RareItemSet::expand_rate(uint8_t pc) {
// pc = bits SSSSS VVV
// S = shift + 4 (so actual shift is 0-27)
// V = value - 7 (so actual value is 7-14)
// take the bits 00000000 00000000 00000000 00000010
// shift left by shift (0-27)
// multiply by value
// To compute the actual drop rare drop rate from pc, first decode pc into
// shift and value:
// pc = bits SSSSSVVV
// shift = S - 4 (so shift is 0-27)
// value = V + 7 (so value is 7-14)
// Then, take the value 0x00000002, shift it left by shift (0-27), and
// multiply the result by value (7-14) to get the actual drop rate. The result
// is a probability out of 0xFFFFFFFF (so 0x40000000 means the item will drop
// 25% of the time, for example).
int8_t shift = ((pc >> 3) & 0x1F) - 4;
if (shift < 0) {
shift = 0;
@@ -518,12 +521,11 @@ void RareItemSet::print_collection(
return;
}
string secid_name = name_for_section_id(section_id);
fprintf(stream, "%s %s %s %s\n",
name_for_mode(mode),
name_for_episode(episode),
name_for_difficulty(difficulty),
secid_name.c_str());
name_for_section_id(section_id));
fprintf(stream, " Monster rares:\n");
for (size_t z = 0; z < collection->rt_index_to_specs.size(); z++) {
+446 -304
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -1,3 +1,5 @@
#pragma once
#include <memory>
#include <string>
@@ -15,7 +17,7 @@ std::shared_ptr<Lobby> create_game_generic(
bool allow_v1 = false,
std::shared_ptr<Lobby> watched_lobby = nullptr,
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player = nullptr);
void set_lobby_quest(std::shared_ptr<Lobby> l, std::shared_ptr<const Quest> q);
void set_lobby_quest(std::shared_ptr<Lobby> l, std::shared_ptr<const Quest> q, bool substitute_v3_for_ep3 = false);
void on_connect(std::shared_ptr<Client> c);
void on_disconnect(std::shared_ptr<Client> c);
+362 -293
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -1,3 +1,5 @@
#pragma once
#include <stdint.h>
#include "Client.hh"
+13 -13
View File
@@ -58,7 +58,7 @@ string ReplaySession::Client::str() const {
shared_ptr<ReplaySession::Event> ReplaySession::create_event(
Event::Type type, shared_ptr<Client> c, size_t line_num) {
shared_ptr<Event> event(new Event(type, c->id, line_num));
auto event = make_shared<Event>(type, c->id, line_num);
if (!this->last_event.get()) {
this->first_event = event;
} else {
@@ -495,11 +495,11 @@ ReplaySession::ReplaySession(
throw runtime_error(string_printf("(ev-line %zu) client connection message listening socket token format is incorrect", line_num));
}
shared_ptr<Client> c(new Client(
auto c = make_shared<Client>(
this,
stoull(tokens[8].substr(2), nullptr, 16),
stoul(listen_tokens[1], nullptr, 10),
enum_for_name<Version>(listen_tokens[2].c_str())));
enum_for_name<Version>(listen_tokens[2].c_str()));
if (!this->clients.emplace(c->id, c).second) {
throw runtime_error(string_printf("(ev-line %zu) duplicate client ID in input log", line_num));
}
@@ -723,8 +723,8 @@ void ReplaySession::on_command_received(
case Version::BB_PATCH:
if (command == 0x02) {
auto& cmd = check_size_t<S_ServerInit_Patch_02>(data);
c->channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key));
c->channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key));
c->channel.crypt_in = make_shared<PSOV2Encryption>(cmd.server_key);
c->channel.crypt_out = make_shared<PSOV2Encryption>(cmd.client_key);
}
break;
case Version::DC_NTE:
@@ -740,11 +740,11 @@ void ReplaySession::on_command_received(
if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) {
auto& cmd = check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>(data, 0xFFFF);
if (is_v1_or_v2(c->version)) {
c->channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key));
c->channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key));
c->channel.crypt_in = make_shared<PSOV2Encryption>(cmd.server_key);
c->channel.crypt_out = make_shared<PSOV2Encryption>(cmd.client_key);
} else { // V3
c->channel.crypt_in.reset(new PSOV3Encryption(cmd.server_key));
c->channel.crypt_out.reset(new PSOV3Encryption(cmd.client_key));
c->channel.crypt_in = make_shared<PSOV3Encryption>(cmd.server_key);
c->channel.crypt_out = make_shared<PSOV3Encryption>(cmd.client_key);
}
}
break;
@@ -753,10 +753,10 @@ void ReplaySession::on_command_received(
auto& cmd = check_size_t<S_ServerInitDefault_BB_03_9B>(data, 0xFFFF);
// TODO: At some point it may matter which BB private key file we use.
// Don't just blindly use the first one here.
c->channel.crypt_in.reset(new PSOBBEncryption(
*this->state->bb_private_keys[0], cmd.server_key.data(), cmd.server_key.size()));
c->channel.crypt_out.reset(new PSOBBEncryption(
*this->state->bb_private_keys[0], cmd.client_key.data(), cmd.client_key.size()));
c->channel.crypt_in = make_shared<PSOBBEncryption>(
*this->state->bb_private_keys[0], cmd.server_key.data(), cmd.server_key.size());
c->channel.crypt_out = make_shared<PSOBBEncryption>(
*this->state->bb_private_keys[0], cmd.client_key.data(), cmd.client_key.size());
}
break;
default:
+35 -12
View File
@@ -208,10 +208,11 @@ PSOBBBaseSystemFile::PSOBBBaseSystemFile() {
}
}
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_config(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
const PlayerVisualConfig& visual,
const std::string& name,
shared_ptr<const LevelTable> level_table) {
static const array<array<PlayerInventoryItem, 5>, 12> initial_inventory{{
{
@@ -300,7 +301,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
},
}};
array<uint8_t, 0xE8> config_hunter_ranger{
static const array<uint8_t, 0xE8> config_hunter_ranger{
{0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -316,7 +317,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
array<uint8_t, 0xE8> config_force{
static const array<uint8_t, 0xE8> config_force{
{0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -333,9 +334,11 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
shared_ptr<PSOBBCharacterFile> ret(new PSOBBCharacterFile());
auto ret = make_shared<PSOBBCharacterFile>();
ret->disp.visual = visual;
ret->disp.name.encode(name, language);
const auto& initial_items = initial_inventory.at(preview.visual.char_class);
const auto& initial_items = initial_inventory.at(visual.char_class);
ret->inventory.num_items = initial_items.size();
for (size_t z = 0; z < initial_items.size(); z++) {
ret->inventory.items[z] = initial_items[z];
@@ -347,7 +350,6 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
ret->disp.config[z] = config[z];
}
ret->disp.apply_preview(preview);
ret->disp.stats.reset_to_base(ret->disp.visual.char_class, level_table);
ret->disp.technique_levels_v1.clear(0xFF);
if (ret->disp.visual.class_flags & 0x80) {
@@ -369,6 +371,15 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
return ret;
}
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
uint32_t guild_card_number,
uint8_t language,
const PlayerDispDataBBPreview& preview,
shared_ptr<const LevelTable> level_table) {
return PSOBBCharacterFile::create_from_config(
guild_card_number, language, preview.visual, preview.name.decode(language), level_table);
}
PSOBBCharacterFile::SymbolChatEntry PSOBBCharacterFile::DefaultSymbolChatEntry::to_entry() const {
SymbolChatEntry ret;
ret.present = 1;
@@ -573,6 +584,18 @@ void PSOBBCharacterFile::print_inventory(FILE* stream, Version version, shared_p
}
}
void PSOBBCharacterFile::print_bank(FILE* stream, Version version, shared_ptr<const ItemNameIndex> name_index) const {
fprintf(stream, "[PlayerBank] Meseta: %" PRIu32 "\n", this->bank.meseta.load());
fprintf(stream, "[PlayerBank] %" PRIu32 " items\n", this->bank.num_items.load());
for (size_t x = 0; x < this->bank.num_items; x++) {
const auto& item = this->bank.items[x];
const char* present_token = item.present ? "" : " (missing present flag)";
auto name = name_index->describe_item(version, item.data);
auto hex = item.data.hex();
fprintf(stream, "[PlayerBank] %3zu: %s (%s) (x%hu) %s\n", x, hex.c_str(), name.c_str(), item.amount.load(), present_token);
}
}
const array<PSOBBCharacterFile::DefaultSymbolChatEntry, 6> PSOBBCharacterFile::DEFAULT_SYMBOL_CHATS = {
DefaultSymbolChatEntry{"\tEHello", 0x28, {0xFFFF, 0x000D, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x05, 0x18, 0x1D, 0x00}, {0x05, 0x28, 0x1D, 0x01}, {0x36, 0x20, 0x2A, 0x00}, {0x3C, 0x00, 0x32, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}},
DefaultSymbolChatEntry{"\tEGood-bye", 0x74, {0x0476, 0x000C, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x06, 0x15, 0x14, 0x00}, {0x06, 0x2B, 0x14, 0x01}, {0x05, 0x18, 0x1F, 0x00}, {0x05, 0x28, 0x1F, 0x01}, {0x36, 0x20, 0x2A, 0x00}, {0x3C, 0x00, 0x32, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}},
@@ -587,16 +610,16 @@ const array<uint16_t, 20> PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG = {
0x0012, 0x000F, 0x0010, 0x0011, 0x000D, 0x000A, 0x000B, 0x000C, 0x000E, 0x0000};
const array<uint8_t, 0x016C> PSOBBBaseSystemFile::DEFAULT_KEY_CONFIG = {
0x00, 0x00, 0x00, 0x00, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00,
+12 -5
View File
@@ -10,6 +10,7 @@
#include <phosg/Strings.hh>
#include <string>
#include "ChoiceSearch.hh"
#include "Episode3/DataIndexes.hh"
#include "ItemNameIndex.hh"
#include "PSOEncryption.hh"
@@ -194,7 +195,7 @@ struct PSOBBCharacterFile {
/* 04E8 */ le_uint32_t play_time_seconds = 0;
/* 04EC */ le_uint32_t option_flags = 0x00040058;
/* 04F0 */ parray<uint8_t, 4> unknown_a2;
/* 04F4 */ parray<parray<uint8_t, 0x80>, 4> quest_flags;
/* 04F4 */ QuestFlags quest_flags;
/* 06F4 */ le_uint32_t death_count = 0;
/* 06F8 */ PlayerBank bank;
/* 19C0 */ GuildCardBB guild_card;
@@ -207,7 +208,7 @@ struct PSOBBCharacterFile {
/* 2CB4 */ parray<uint8_t, 4> unknown_a4;
/* 2CB8 */ PlayerRecordsBB_Challenge challenge_records;
/* 2DF8 */ parray<le_uint16_t, 0x0014> tech_menu_config;
/* 2E20 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
/* 2E20 */ ChoiceSearchConfig choice_search_config;
/* 2E38 */ parray<uint8_t, 0x0010> unknown_a6;
/* 2E48 */ parray<le_uint32_t, 0x0010> quest_global_flags;
/* 2E88 */ parray<uint8_t, 0x1C> unknown_a7;
@@ -218,6 +219,12 @@ struct PSOBBCharacterFile {
PSOBBCharacterFile() = default;
static std::shared_ptr<PSOBBCharacterFile> create_from_config(
uint32_t guild_card_number,
uint8_t language,
const PlayerVisualConfig& visual,
const std::string& name,
std::shared_ptr<const LevelTable> level_table);
static std::shared_ptr<PSOBBCharacterFile> create_from_preview(
uint32_t guild_card_number,
uint8_t language,
@@ -247,6 +254,7 @@ struct PSOBBCharacterFile {
void clear_all_material_usage();
void print_inventory(FILE* stream, Version version, std::shared_ptr<const ItemNameIndex> name_index) const;
void print_bank(FILE* stream, Version version, std::shared_ptr<const ItemNameIndex> name_index) const;
} __attribute__((packed));
struct PSOBBGuildCardFile {
@@ -342,8 +350,7 @@ struct PSOGCCharacterFile {
/* 0430:0014 */ be_uint32_t save_count;
/* 0434:0018 */ parray<uint8_t, 0x1C> unknown_a4;
/* 0450:0034 */ parray<uint8_t, 0x10> unknown_a5;
// 1024 bits (flags) per difficulty
/* 0460:0044 */ parray<parray<uint8_t, 0x80>, 4> quest_flags;
/* 0460:0044 */ QuestFlags quest_flags;
/* 0660:0244 */ be_uint32_t death_count;
/* 0664:0248 */ PlayerBank bank;
/* 192C:1510 */ GuildCardGC guild_card;
@@ -760,7 +767,7 @@ struct LegacySavedPlayerDataBB { // .nsc file format
/* 185C */ pstring<TextEncoding::UTF16, 0x00AC> info_board;
/* 19B4 */ PlayerInventory inventory;
/* 1D00 */ parray<uint8_t, 4> unknown_a2;
/* 1D04 */ parray<parray<uint8_t, 0x80>, 4> quest_flags;
/* 1D04 */ QuestFlags quest_flags;
/* 1F04 */ le_uint32_t death_count;
/* 1F08 */ parray<le_uint32_t, 0x0016> quest_global_flags;
/* 1F60 */ parray<le_uint16_t, 0x0014> tech_menu_config;
+262 -204
View File
@@ -165,8 +165,8 @@ void send_server_init_dc_pc_v3(shared_ptr<Client> c, uint8_t flags) {
switch (c->version()) {
case Version::PC_V2:
c->channel.crypt_in.reset(new PSOV2Encryption(client_key));
c->channel.crypt_out.reset(new PSOV2Encryption(server_key));
c->channel.crypt_in = make_shared<PSOV2Encryption>(client_key);
c->channel.crypt_out = make_shared<PSOV2Encryption>(server_key);
break;
case Version::DC_NTE:
case Version::DC_V1_11_2000_PROTOTYPE:
@@ -176,15 +176,15 @@ void send_server_init_dc_pc_v3(shared_ptr<Client> c, uint8_t flags) {
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3: {
shared_ptr<PSOV2OrV3DetectorEncryption> det_crypt(new PSOV2OrV3DetectorEncryption(
client_key, v2_crypt_initial_client_commands, v3_crypt_initial_client_commands));
auto det_crypt = make_shared<PSOV2OrV3DetectorEncryption>(
client_key, v2_crypt_initial_client_commands, v3_crypt_initial_client_commands);
c->channel.crypt_in = det_crypt;
c->channel.crypt_out.reset(new PSOV2OrV3ImitatorEncryption(server_key, det_crypt));
c->channel.crypt_out = make_shared<PSOV2OrV3ImitatorEncryption>(server_key, det_crypt);
break;
}
case Version::XB_V3:
c->channel.crypt_in.reset(new PSOV3Encryption(client_key));
c->channel.crypt_out.reset(new PSOV3Encryption(server_key));
c->channel.crypt_in = make_shared<PSOV3Encryption>(client_key);
c->channel.crypt_out = make_shared<PSOV3Encryption>(server_key);
break;
default:
throw invalid_argument("incorrect client version");
@@ -216,15 +216,15 @@ void send_server_init_bb(shared_ptr<Client> c, uint8_t flags) {
static const string primary_expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8);
static const string secondary_expected_first_data("\xDC\x00\xDB\x00\x00\x00\x00\x00", 8);
shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt(new PSOBBMultiKeyDetectorEncryption(
auto detector_crypt = make_shared<PSOBBMultiKeyDetectorEncryption>(
c->require_server_state()->bb_private_keys,
bb_crypt_initial_client_commands,
cmd.basic_cmd.client_key.data(),
sizeof(cmd.basic_cmd.client_key)));
sizeof(cmd.basic_cmd.client_key));
c->channel.crypt_in = detector_crypt;
c->channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
c->channel.crypt_out = make_shared<PSOBBMultiKeyImitatorEncryption>(
detector_crypt, cmd.basic_cmd.server_key.data(),
sizeof(cmd.basic_cmd.server_key), true));
sizeof(cmd.basic_cmd.server_key), true);
}
void send_server_init_patch(shared_ptr<Client> c) {
@@ -237,8 +237,8 @@ void send_server_init_patch(shared_ptr<Client> c) {
cmd.client_key = client_key;
send_command_t(c, 0x02, 0x00, cmd);
c->channel.crypt_out.reset(new PSOV2Encryption(server_key));
c->channel.crypt_in.reset(new PSOV2Encryption(client_key));
c->channel.crypt_out = make_shared<PSOV2Encryption>(server_key);
c->channel.crypt_in = make_shared<PSOV2Encryption>(client_key);
}
void send_server_init(shared_ptr<Client> c, uint8_t flags) {
@@ -498,7 +498,7 @@ void send_system_file_bb(shared_ptr<Client> c) {
auto team = c->team();
PSOBBFullSystemFile cmd;
cmd.base = *c->game_data.system();
cmd.base = *c->system_file();
if (team) {
cmd.team_membership = team->membership_for_member(c->license->serial_number);
}
@@ -518,7 +518,7 @@ void send_player_preview_bb(shared_ptr<Client> c, int8_t character_index, const
}
void send_guild_card_header_bb(shared_ptr<Client> c) {
uint32_t checksum = c->game_data.guild_cards()->checksum();
uint32_t checksum = c->guild_card_file()->checksum();
S_GuildCardHeader_BB_01DC cmd = {1, sizeof(PSOBBGuildCardFile), checksum};
send_command_t(c, 0x01DC, 0x00000000, cmd);
}
@@ -535,7 +535,7 @@ void send_guild_card_chunk_bb(shared_ptr<Client> c, size_t chunk_index) {
cmd.unknown = 0;
cmd.chunk_index = chunk_index;
cmd.data.assign_range(
reinterpret_cast<const uint8_t*>(c->game_data.guild_cards().get()) + chunk_offset,
reinterpret_cast<const uint8_t*>(c->guild_card_file().get()) + chunk_offset,
data_size, 0);
send_command(c, 0x02DC, 0x00000000, &cmd, sizeof(cmd) - sizeof(cmd.data) + data_size);
@@ -621,13 +621,13 @@ void send_stream_file_chunk_bb(shared_ptr<Client> c, uint32_t chunk_index) {
}
void send_approve_player_choice_bb(shared_ptr<Client> c) {
S_ApprovePlayerChoice_BB_00E4 cmd = {c->game_data.bb_character_index, 1};
S_ApprovePlayerChoice_BB_00E4 cmd = {c->bb_character_index, 1};
send_command_t(c, 0x00E4, 0x00000000, cmd);
}
void send_complete_player_bb(shared_ptr<Client> c) {
auto p = c->game_data.character(true, false);
auto sys = c->game_data.system(true);
auto p = c->character(true, false);
auto sys = c->system_file(true);
auto team = c->team();
if (c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) {
p->inventory.language = 1;
@@ -969,7 +969,7 @@ void send_info_board_t(shared_ptr<Client> c) {
if (!other_c.get()) {
continue;
}
auto other_p = other_c->game_data.character(true, false);
auto other_p = other_c->character(true, false);
auto& e = entries.emplace_back();
e.name.encode(other_p->disp.name.decode(other_p->inventory.language), c->language());
e.message.encode(add_color(other_p->info_board.decode(other_p->inventory.language)), c->language());
@@ -987,6 +987,44 @@ void send_info_board(shared_ptr<Client> c) {
}
}
template <typename CmdT>
void send_choice_search_choices_t(shared_ptr<Client> c) {
vector<CmdT> entries;
for (const auto& cat : CHOICE_SEARCH_CATEGORIES) {
auto& cat_e = entries.emplace_back();
cat_e.parent_choice_id = 0;
cat_e.choice_id = cat.id;
cat_e.text.encode(cat.name, c->language());
for (const auto& choice : cat.choices) {
auto& e = entries.emplace_back();
e.parent_choice_id = cat.id;
e.choice_id = choice.id;
e.text.encode(choice.name, c->language());
}
}
send_command_vt(c, 0xC0, entries.size(), entries);
}
void send_choice_search_choices(shared_ptr<Client> c) {
switch (c->version()) {
// DC V1 and the prototypes do not support this command
case Version::DC_V2:
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
case Version::XB_V3:
send_choice_search_choices_t<S_ChoiceSearchEntry_DC_V3_C0>(c);
break;
case Version::PC_V2:
case Version::BB_V4:
send_choice_search_choices_t<S_ChoiceSearchEntry_PC_BB_C0>(c);
break;
default:
throw logic_error("unimplemented versioned command");
}
}
template <typename CommandHeaderT, TextEncoding Encoding>
void send_card_search_result_t(
shared_ptr<Client> c,
@@ -1017,7 +1055,7 @@ void send_card_search_result_t(
cmd.location_string.encode(location_string, c->language());
cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY;
cmd.extension.lobby_refs[0].item_id = result_lobby->lobby_id;
auto rp = result->game_data.character(true, false);
auto rp = result->character(true, false);
cmd.extension.player_name.encode(rp->disp.name.decode(rp->inventory.language), c->language());
send_command_t(c, 0x41, 0x00, cmd);
@@ -1171,7 +1209,7 @@ void send_guild_card(shared_ptr<Client> c, shared_ptr<Client> source) {
throw runtime_error("source player does not have a license");
}
auto source_p = source->game_data.character(true, false);
auto source_p = source->character(true, false);
auto source_team = source->team();
uint64_t xb_user_id = source->license->xb_user_id
@@ -1375,37 +1413,63 @@ void send_game_menu(
template <typename EntryT>
void send_quest_menu_t(
shared_ptr<Client> c,
uint32_t menu_id,
const vector<shared_ptr<const Quest>>& quests,
const vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>>& quests,
bool is_download_menu) {
auto v = c->version();
vector<EntryT> entries;
for (const auto& quest : quests) {
auto vq = quest->version(v, c->language());
for (const auto& it : quests) {
auto vq = it.second->version(v, c->language());
if (!vq) {
continue;
}
auto& e = entries.emplace_back();
e.menu_id = menu_id;
e.item_id = quest->quest_number;
e.menu_id = (it.second->episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1;
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());
}
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
}
void send_quest_menu_bb(
shared_ptr<Client> c,
const vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>>& quests,
bool is_download_menu) {
auto v = c->version();
vector<S_QuestMenuEntry_BB_A2_A4> entries;
for (const auto& it : quests) {
auto vq = it.second->version(v, c->language());
if (!vq) {
continue;
}
auto& e = entries.emplace_back();
e.menu_id = MenuID::QUEST_EP1;
e.item_id = it.second->quest_number;
e.name.encode(vq->name, c->language());
e.short_description.encode(add_color(vq->short_description), c->language());
e.disabled = (it.first == QuestIndex::IncludeState::DISABLED) ? 1 : 0;
}
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
}
template <typename EntryT>
void send_quest_categories_menu_t(
shared_ptr<Client> c,
uint32_t menu_id,
shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode) {
QuestIndex::IncludeCondition include_condition = nullptr;
if (!(c->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
auto l = c->lobby.lock();
include_condition = l ? l->quest_include_condition() : nullptr;
}
vector<EntryT> entries;
for (const auto& cat : quest_index->categories(menu_type, episode, c->version())) {
for (const auto& cat : quest_index->categories(menu_type, episode, c->version(), include_condition)) {
auto& e = entries.emplace_back();
e.menu_id = menu_id;
e.menu_id = MenuID::QUEST_CATEGORIES;
e.item_id = cat->category_id;
e.name.encode(cat->name, c->language());
e.short_description.encode(add_color(cat->description), c->language());
@@ -1415,11 +1479,13 @@ void send_quest_categories_menu_t(
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
}
void send_quest_menu(shared_ptr<Client> c, uint32_t menu_id,
const vector<shared_ptr<const Quest>>& quests, bool is_download_menu) {
void send_quest_menu(
shared_ptr<Client> c,
const vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>>& quests,
bool is_download_menu) {
switch (c->version()) {
case Version::PC_V2:
send_quest_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_id, quests, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, quests, is_download_menu);
break;
case Version::DC_NTE:
case Version::DC_V1_11_2000_PROTOTYPE:
@@ -1429,24 +1495,27 @@ void send_quest_menu(shared_ptr<Client> c, uint32_t menu_id,
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
send_quest_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_id, quests, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, quests, is_download_menu);
break;
case Version::XB_V3:
send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_id, quests, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, quests, is_download_menu);
break;
case Version::BB_V4:
send_quest_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, menu_id, quests, is_download_menu);
send_quest_menu_bb(c, quests, is_download_menu);
break;
default:
throw logic_error("unimplemented versioned command");
}
}
void send_quest_categories_menu(shared_ptr<Client> c, uint32_t menu_id,
shared_ptr<const QuestIndex> quest_index, QuestMenuType menu_type, Episode episode) {
void send_quest_categories_menu(
shared_ptr<Client> c,
shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode) {
switch (c->version()) {
case Version::PC_V2:
send_quest_categories_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_id, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, quest_index, menu_type, episode);
break;
case Version::DC_NTE:
case Version::DC_V1_11_2000_PROTOTYPE:
@@ -1456,13 +1525,13 @@ void send_quest_categories_menu(shared_ptr<Client> c, uint32_t menu_id,
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
send_quest_categories_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_id, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, quest_index, menu_type, episode);
break;
case Version::XB_V3:
send_quest_categories_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_id, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, quest_index, menu_type, episode);
break;
case Version::BB_V4:
send_quest_categories_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, menu_id, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, quest_index, menu_type, episode);
break;
default:
throw logic_error("unimplemented versioned command");
@@ -1499,7 +1568,7 @@ template <typename EntryT>
void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client) {
vector<EntryT> entries;
auto add_client = [&](shared_ptr<Client> lc) -> void {
auto lp = lc->game_data.character(true, false);
auto lp = lc->character(true, false);
auto& e = entries.emplace_back();
e.client_id = lc->lobby_client_id;
e.challenge = lp->challenge_records;
@@ -1551,7 +1620,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
if (!wc) {
continue;
}
auto wc_p = wc->game_data.character();
auto wc_p = wc->character();
auto& p = cmd.players[z];
p.lobby_data.player_tag = 0x00010000;
p.lobby_data.guild_card_number = wc->license->serial_number;
@@ -1567,8 +1636,8 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
e.guild_card_number = wc->license->serial_number;
e.name.encode(wc_p->disp.name.decode(wc_p->inventory.language), c->language());
e.present = 1;
e.level = wc->game_data.ep3_config
? (wc->game_data.ep3_config->online_clv_exp / 100)
e.level = wc->ep3_config
? (wc->ep3_config->online_clv_exp / 100)
: wc_p->disp.stats.level.load();
e.name_color = wc_p->disp.visual.name_color;
@@ -1618,7 +1687,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
for (size_t z = 4; z < 12; z++) {
if (l->clients[z]) {
auto other_c = l->clients[z];
auto other_p = other_c->game_data.character();
auto other_p = other_c->character();
auto& cmd_p = cmd.spectator_players[z - 4];
auto& cmd_e = cmd.entries[z];
cmd_p.lobby_data.player_tag = 0x00010000;
@@ -1633,8 +1702,8 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
cmd_e.guild_card_number = other_c->license->serial_number;
cmd_e.name = cmd_p.lobby_data.name;
cmd_e.present = 1;
cmd_e.level = other_c->game_data.ep3_config
? (other_c->game_data.ep3_config->online_clv_exp / 100)
cmd_e.level = other_c->ep3_config
? (other_c->ep3_config->online_clv_exp / 100)
: other_p->disp.stats.level.load();
cmd_e.name_color = other_p->disp.visual.name_color;
@@ -1660,7 +1729,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
cmd.lobby_data[x].player_tag = 0x00010000;
cmd.lobby_data[x].guild_card_number = lc->license->serial_number;
cmd.lobby_data[x].client_id = lc->lobby_client_id;
cmd.lobby_data[x].name.encode(lc->game_data.character()->disp.name.decode(lc->language()), c->language());
cmd.lobby_data[x].name.encode(lc->character()->disp.name.decode(lc->language()), c->language());
player_count++;
} else {
cmd.lobby_data[x].clear();
@@ -1740,7 +1809,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
auto s = c->require_server_state();
for (size_t x = 0; x < 4; x++) {
if (l->clients[x]) {
auto other_p = l->clients[x]->game_data.character();
auto other_p = l->clients[x]->character();
cmd.players_ep3[x].inventory = other_p->inventory;
cmd.players_ep3[x].inventory.encode_for_client(c);
cmd.players_ep3[x].disp = convert_player_disp_data<PlayerDispDataDCPCV3>(other_p->disp, c->language(), other_p->inventory.language);
@@ -1780,7 +1849,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
}
c->log.info("Creating game join command queue");
c->game_join_command_queue.reset(new deque<Client::JoinCommand>());
c->game_join_command_queue = make_unique<deque<Client::JoinCommand>>();
send_command(c, 0x1D, 0x00);
}
@@ -1856,20 +1925,23 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
size_t used_entries = 0;
for (const auto& lc : lobby_clients) {
auto lp = lc->game_data.character();
auto lp = lc->character();
auto& e = cmd.entries[used_entries++];
e.lobby_data.player_tag = 0x00010000;
e.lobby_data.guild_card_number = lc->license->serial_number;
e.lobby_data.client_id = lc->lobby_client_id;
if (UseLanguageMarkerInName) {
e.lobby_data.name.encode("\tJ" + lp->disp.name.decode(lp->inventory.language), c->language());
string name = lp->disp.name.decode(lp->inventory.language);
bool name_has_marker = (name.size() >= 2) && (name[0] == '\t') && (name[1] != 'C');
if (UseLanguageMarkerInName && !name_has_marker) {
const char* marker = c->language() ? "\tE" : "\tJ";
e.lobby_data.name.encode(marker + name, c->language());
} else {
e.lobby_data.name.encode(lp->disp.name.decode(lp->inventory.language), c->language());
e.lobby_data.name.encode(name, c->language());
}
e.inventory = lp->inventory;
e.inventory.encode_for_client(c);
if ((lc == c) && is_v1_or_v2(c->version()) && lc->game_data.last_reported_disp_v1_v2) {
e.disp = convert_player_disp_data<DispDataT>(*lc->game_data.last_reported_disp_v1_v2, c->language(), lp->inventory.language);
if ((lc == c) && is_v1_or_v2(c->version()) && lc->v1_v2_last_reported_disp) {
e.disp = convert_player_disp_data<DispDataT>(*lc->v1_v2_last_reported_disp, c->language(), lp->inventory.language);
} else {
e.disp = convert_player_disp_data<DispDataT>(lp->disp, c->language(), lp->inventory.language);
e.disp.enforce_lobby_join_limits_for_client(c);
@@ -1932,7 +2004,7 @@ void send_join_lobby_xb(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cl
size_t used_entries = 0;
for (const auto& lc : lobby_clients) {
auto lp = lc->game_data.character();
auto lp = lc->character();
auto& e = cmd.entries[used_entries++];
e.lobby_data.player_tag = 0x00010000;
e.lobby_data.guild_card_number = lc->license->serial_number;
@@ -1985,7 +2057,7 @@ void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l,
size_t used_entries = 0;
for (const auto& lc : lobby_clients) {
auto lp = lc->game_data.character();
auto lp = lc->character();
auto& e = cmd.entries[used_entries++];
e.lobby_data.player_tag = 0x00010000;
e.lobby_data.guild_card_number = lc->license->serial_number;
@@ -2115,7 +2187,7 @@ void send_execute_item_trade(shared_ptr<Client> c, const vector<ItemData>& items
}
void send_execute_card_trade(shared_ptr<Client> c, const vector<pair<uint32_t, uint32_t>>& card_to_count) {
if (is_ep3(c->version())) {
if (!is_ep3(c->version())) {
throw logic_error("cannot send trade cards command to non-Ep3 client");
}
@@ -2277,21 +2349,29 @@ void send_pick_up_item(shared_ptr<Client> c, uint32_t item_id, uint8_t floor) {
send_command_t(l, 0x60, 0x00, cmd);
}
void send_create_inventory_item(shared_ptr<Client> c, const ItemData& item) {
void send_create_inventory_item(shared_ptr<Client> c, const ItemData& item, bool exclude_c) {
auto l = c->require_lobby();
if (c->version() != Version::BB_V4) {
throw logic_error("6xBE can only be sent to BB clients");
}
uint16_t client_id = c->lobby_client_id;
G_CreateInventoryItem_BB_6xBE cmd = {{0xBE, 0x07, client_id}, item, 0};
send_command_t(l, 0x60, 0x00, cmd);
if (exclude_c) {
send_command_excluding_client(l, c, 0x60, 0x00, &cmd, sizeof(cmd));
} else {
send_command_t(l, 0x60, 0x00, cmd);
}
}
void send_destroy_item(shared_ptr<Client> c, uint32_t item_id, uint32_t amount) {
void send_destroy_item(shared_ptr<Client> c, uint32_t item_id, uint32_t amount, bool exclude_c) {
auto l = c->require_lobby();
uint16_t client_id = c->lobby_client_id;
G_DeleteInventoryItem_6x29 cmd = {{0x29, 0x03, client_id}, item_id, amount};
send_command_t(l, 0x60, 0x00, cmd);
if (exclude_c) {
send_command_excluding_client(l, c, 0x60, 0x00, &cmd, sizeof(cmd));
} else {
send_command_t(l, 0x60, 0x00, cmd);
}
}
void send_item_identify_result(shared_ptr<Client> c) {
@@ -2303,7 +2383,7 @@ void send_item_identify_result(shared_ptr<Client> c) {
res.header.subcommand = 0xB9;
res.header.size = sizeof(res) / 4;
res.header.client_id = c->lobby_client_id;
res.item_data = c->game_data.identify_result;
res.item_data = c->bb_identify_result;
send_command_t(l, 0x60, 0x00, res);
}
@@ -2312,15 +2392,17 @@ void send_bank(shared_ptr<Client> c) {
throw logic_error("6xBC can only be sent to BB clients");
}
auto p = c->game_data.character();
const auto* items_it = p->bank.items.data();
vector<PlayerBankItem> items(items_it, items_it + p->bank.num_items);
auto p = c->character();
auto& bank = c->current_bank();
bank.sort();
const auto* items_it = bank.items.data();
vector<PlayerBankItem> items(items_it, items_it + bank.num_items);
G_BankContentsHeader_BB_6xBC cmd = {
{{0xBC, 0, 0}, sizeof(G_BankContentsHeader_BB_6xBC) + items.size() * sizeof(PlayerBankItem)},
random_object<uint32_t>(),
p->bank.num_items,
p->bank.meseta};
bank.num_items,
bank.meseta};
send_command_t_vt(c, 0x6C, 0x00, cmd, items);
}
@@ -2330,7 +2412,7 @@ void send_shop(shared_ptr<Client> c, uint8_t shop_type) {
throw logic_error("6xB6 can only be sent to BB clients");
}
const auto& contents = c->game_data.shop_contents.at(shop_type);
const auto& contents = c->bb_shop_contents.at(shop_type);
G_ShopContents_BB_6xB6 cmd = {
{0xB6, static_cast<uint8_t>(2 + (sizeof(ItemData) >> 2) * contents.size()), 0x0000},
@@ -2348,7 +2430,7 @@ void send_shop(shared_ptr<Client> c, uint8_t shop_type) {
void send_level_up(shared_ptr<Client> c) {
auto l = c->require_lobby();
auto p = c->game_data.character();
auto p = c->character();
CharacterStats stats = p->disp.stats.char_stats;
const ItemData* mag = nullptr;
@@ -2614,7 +2696,7 @@ string ep3_description_for_client(shared_ptr<Client> c) {
if (!is_ep3(c->version())) {
throw runtime_error("client is not Episode 3");
}
auto p = c->game_data.character();
auto p = c->character();
return string_printf(
"%s CLv%" PRIu32 " %c",
name_for_char_class(p->disp.visual.char_class),
@@ -2662,7 +2744,7 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
if (player.is_human()) {
try {
auto other_c = serial_number_to_client.at(player.serial_number);
entry.name.encode(other_c->game_data.character()->disp.name.decode(other_c->language()), c->language());
entry.name.encode(other_c->character()->disp.name.decode(other_c->language()), c->language());
entry.description.encode(ep3_description_for_client(other_c), c->language());
} catch (const out_of_range&) {
entry.name.encode(player.player_name, c->language());
@@ -2683,7 +2765,7 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
for (auto spec_c : l->clients) {
if (spec_c) {
auto& entry = cmd.spectator_entries[cmd.num_spectators++];
entry.name.encode(spec_c->game_data.character()->disp.name.decode(spec_c->language()), c->language());
entry.name.encode(spec_c->character()->disp.name.decode(spec_c->language()), c->language());
entry.description.encode(ep3_description_for_client(spec_c), c->language());
}
}
@@ -2700,7 +2782,7 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
size_t num_players = 0;
for (const auto& opp_c : primary_lobby->clients) {
if (opp_c) {
cmd.player_entries[num_players].name.encode(opp_c->game_data.character()->disp.name.decode(opp_c->language()), c->language());
cmd.player_entries[num_players].name.encode(opp_c->character()->disp.name.decode(opp_c->language()), c->language());
cmd.player_entries[num_players].description.encode(ep3_description_for_client(opp_c), c->language());
num_players++;
}
@@ -2713,7 +2795,7 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
for (auto spec_c : l->clients) {
if (spec_c) {
auto& entry = cmd.spectator_entries[num_spectators++];
entry.name.encode(spec_c->game_data.character()->disp.name.decode(spec_c->language()), c->language());
entry.name.encode(spec_c->character()->disp.name.decode(spec_c->language()), c->language());
entry.description.encode(ep3_description_for_client(spec_c), c->language());
}
}
@@ -2824,7 +2906,7 @@ void send_ep3_tournament_match_result(shared_ptr<Lobby> l, uint32_t meseta_rewar
if (player.is_human()) {
try {
auto pc = serial_number_to_client.at(player.serial_number);
entry.player_names[z].encode(pc->game_data.character()->disp.name.decode(pc->language()), lc->language());
entry.player_names[z].encode(pc->character()->disp.name.decode(pc->language()), lc->language());
} catch (const out_of_range&) {
entry.player_names[z].encode(player.player_name, lc->language());
}
@@ -2981,7 +3063,8 @@ void send_quest_file_chunk(
}
cmd.data_size = size;
send_command_t(c, is_download_quest ? 0xA7 : 0x13, chunk_index, cmd);
c->log.info("Sending quest file chunk %s:%zu", filename.c_str(), chunk_index);
c->channel.send(is_download_quest ? 0xA7 : 0x13, chunk_index, &cmd, sizeof(cmd), true);
}
template <typename CommandT>
@@ -3057,10 +3140,10 @@ void send_open_quest_file(
case Version::DC_V1_11_2000_PROTOTYPE:
case Version::DC_V1:
case Version::DC_V2:
case Version::GC_NTE:
send_open_quest_file_t<S_OpenFile_DC_44_A6>(c, quest_name, filename, xb_filename, contents->size(), quest_number, type);
break;
case Version::PC_V2:
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
@@ -3078,7 +3161,7 @@ void send_open_quest_file(
// For GC/XB/BB, we wait for acknowledgement commands before sending each
// chunk. For DC/PC, we send the entire quest all at once.
if (is_v1_or_v2(c->version())) {
if (is_v1_or_v2(c->version()) && (c->version() != Version::GC_NTE)) {
for (size_t offset = 0; offset < contents->size(); offset += 0x400) {
size_t chunk_bytes = contents->size() - offset;
if (chunk_bytes > 0x400) {
@@ -3274,10 +3357,10 @@ static S_TeamInfoForPlayer_BB_13EA_15EA_Entry team_metadata_for_client(shared_pt
auto team = c->team();
S_TeamInfoForPlayer_BB_13EA_15EA_Entry cmd;
cmd.lobby_client_id = c->lobby_client_id;
cmd.guild_card_number = c->license->serial_number;
cmd.guild_card_number2 = c->license->serial_number;
cmd.player_name = c->game_data.character()->disp.name;
cmd.player_name = c->character()->disp.name;
if (team) {
cmd.guild_card_number = c->license->serial_number;
cmd.team_id = team->team_id;
cmd.privilege_level = team->members.at(c->license->serial_number).privilege_level();
cmd.team_name.encode(team->name);
@@ -3317,29 +3400,6 @@ void send_team_member_list(shared_ptr<Client> c) {
throw runtime_error("client is not in a team");
}
S_TeamMemberList_BB_09EA header;
header.entry_count = team->members.size();
vector<S_TeamMemberList_BB_09EA::Entry> entries;
entries.reserve(header.entry_count);
for (auto& it : team->members) {
auto& m = it.second;
auto& e = entries.emplace_back();
e.index = entries.size();
e.privilege_level = m.privilege_level();
e.guild_card_number = m.serial_number;
e.name.encode(m.name, c->language());
}
send_command_t_vt(c, 0x09EA, 0x00000000, header, entries);
}
void send_team_rank_info(std::shared_ptr<Client> c) {
auto team = c->team();
if (!team) {
throw runtime_error("client is not in a team");
}
vector<const TeamIndex::Team::Member*> members;
for (const auto& it : team->members) {
members.emplace_back(&it.second);
@@ -3349,10 +3409,45 @@ void send_team_rank_info(std::shared_ptr<Client> c) {
};
sort(members.begin(), members.end(), rank_fn);
S_TeamRankingInformation_BB_18EA cmd;
cmd.points_remaining = 0;
S_TeamMemberList_BB_09EA header;
header.entry_count = members.size();
vector<S_TeamRankingInformation_BB_18EA::Entry> entries;
vector<S_TeamMemberList_BB_09EA::Entry> entries;
entries.reserve(header.entry_count);
for (size_t z = 0; z < members.size(); z++) {
const auto* m = members[z];
auto& e = entries.emplace_back();
e.rank = z + 1;
e.privilege_level = m->privilege_level();
e.guild_card_number = m->serial_number;
e.name.encode(m->name, c->language());
}
send_command_t_vt(c, 0x09EA, 0x00000000, header, entries);
}
void send_intra_team_ranking(std::shared_ptr<Client> c) {
auto team = c->team();
if (!team) {
throw runtime_error("client is not in a team");
}
// TODO: At some point we should maintain a sorted index instead of sorting
// these on-demand.
vector<const TeamIndex::Team::Member*> members;
for (const auto& it : team->members) {
members.emplace_back(&it.second);
}
auto rank_fn = +[](const TeamIndex::Team::Member* a, const TeamIndex::Team::Member* b) {
return a->points > b->points;
};
sort(members.begin(), members.end(), rank_fn);
S_IntraTeamRanking_BB_18EA cmd;
cmd.points_remaining = team->points - team->spent_points;
cmd.num_entries = members.size();
vector<S_IntraTeamRanking_BB_18EA::Entry> entries;
for (size_t z = 0; z < members.size(); z++) {
const auto* m = members[z];
cmd.ranking_points += m->points;
@@ -3363,111 +3458,74 @@ void send_team_rank_info(std::shared_ptr<Client> c) {
e.player_name.encode(m->name);
e.points = m->points;
}
cmd.num_entries = entries.size();
send_command_t_vt(c, 0x18EA, 0x00000000, cmd, entries);
}
void send_team_rewards_available_for_purchase(std::shared_ptr<Client> c) {
void send_cross_team_ranking(std::shared_ptr<Client> c) {
auto s = c->require_server_state();
// TODO: At some point we should maintain a sorted index instead of sorting
// these on-demand.
auto teams = s->team_index->all();
auto rank_fn = +[](const shared_ptr<const TeamIndex::Team>& a, const shared_ptr<const TeamIndex::Team>& b) {
return a->points > b->points;
};
sort(teams.begin(), teams.end(), rank_fn);
size_t num_to_send = min<size_t>(teams.size(), 0x300);
S_CrossTeamRanking_BB_1CEA cmd;
cmd.num_entries = num_to_send;
vector<S_CrossTeamRanking_BB_1CEA::Entry> entries;
for (size_t z = 0; z < num_to_send; z++) {
auto t = teams[z];
auto& e = entries.emplace_back();
e.team_name.encode(t->name, c->language());
e.team_points = t->points;
e.unknown_a1 = 0x01020304;
}
send_command_t_vt(c, 0x1CEA, 0x00000000, cmd, entries);
}
void send_team_reward_list(std::shared_ptr<Client> c, bool show_purchased) {
auto team = c->team();
if (!team) {
throw runtime_error("user is not in a team");
}
auto s = c->require_server_state();
vector<S_TeamRewardsAvailableForPurchase_BB_1AEA::Entry> entries;
if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::TEAM_FLAG)) {
bool show_item_rewards = show_purchased || (c->current_bank().num_items < 200);
vector<S_TeamRewardList_BB_19EA_1AEA::Entry> entries;
for (const auto& reward : s->team_index->reward_definitions()) {
if (team->has_reward(reward.key) != show_purchased) {
continue;
}
if (!show_item_rewards && !reward.reward_item.empty()) {
continue;
}
bool has_all_prerequisites = true;
for (const auto& key : reward.prerequisite_keys) {
if (!team->has_reward(key)) {
has_all_prerequisites = false;
break;
}
}
if (!has_all_prerequisites) {
continue;
}
auto& e = entries.emplace_back();
e.name.encode("Team flag");
e.description.encode("Show a custom banner\nabove your team\'s\nplayers in the lobby");
e.reward_id = TeamRewardMenuItemID::TEAM_FLAG;
e.team_points = 2500;
}
if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::DRESSING_ROOM)) {
auto& e = entries.emplace_back();
e.name.encode("Dressing room");
e.description.encode("Unlock the ability to\nchange your character\'s\nappearance");
e.reward_id = TeamRewardMenuItemID::DRESSING_ROOM;
e.team_points = 3000;
}
if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_20_LEADERS_3)) {
auto& e = entries.emplace_back();
e.name.encode("20 team members");
e.description.encode("Increase your team\'s\nsize limit to 30 members\nand 3 leaders");
e.reward_id = TeamRewardMenuItemID::MEMBERS_20_LEADERS_3;
e.team_points = 1500;
} else if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_40_LEADERS_5)) {
auto& e = entries.emplace_back();
e.name.encode("40 team members");
e.description.encode("Increase your team\'s\nsize limit to 40 members\nand 3 leaders");
e.reward_id = TeamRewardMenuItemID::MEMBERS_40_LEADERS_5;
e.team_points = 4000;
} else if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_70_LEADERS_8)) {
auto& e = entries.emplace_back();
e.name.encode("70 team members");
e.description.encode("Increase your team\'s\nsize limit to 70 members\nand 8 leaders");
e.reward_id = TeamRewardMenuItemID::MEMBERS_70_LEADERS_8;
e.team_points = 9000;
} else if (!team->check_reward_flag(TeamIndex::Team::RewardFlag::MEMBERS_100_LEADERS_10)) {
auto& e = entries.emplace_back();
e.name.encode("100 team members");
e.description.encode("Increase your team\'s\nsize limit to 100 members\nand 10 leaders");
e.reward_id = TeamRewardMenuItemID::MEMBERS_100_LEADERS_10;
e.team_points = 18000;
e.name.encode(reward.name, c->language());
e.description.encode(reward.description, c->language());
e.reward_id = reward.menu_item_id;
e.team_points = reward.team_points;
}
// TODO: Implement these. Currently we don't have a good way to conditionally
// unlock quests, and especially not from a team reward flag.
// if (!point_of_disaster_unlocked) {
// auto& e = entries.emplace_back();
// e.name.encode("Quest: Point of Disaster");
// e.description.encode("Unlock the quest\nPoint of Disaster\nfor your team");
// e.team_points = 1000;
// e.reward_id = TeamRewardMenuItemID::POINT_OF_DISASTER;
// }
// if (!toys_twilight_unlocked) {
// auto& e = entries.emplace_back();
// e.name.encode("Quest: Toys Twilight");
// e.description.encode("Unlock the quest\nToys Twilight\nfor your team");
// e.team_points = 1000;
// e.reward_id = TeamRewardMenuItemID::TOYS_TWILIGHT;
// }
// TODO: How should these be implemented? There has to be a way to create
// items in the lobby, presumably...
// auto& e = entries.emplace_back();
// e.name.encode("Commander Blade");
// e.description.encode("Create a Commander\nBlade weapon");
// e.team_points = 8000;
// e.reward_id = TeamRewardMenuItemID::COMMANDER_BLADE;
// auto& e = entries.emplace_back();
// e.name.encode("Union Guard");
// e.description.encode("Create a Union Guard\nshield");
// e.team_points = 100;
// e.reward_id = TeamRewardMenuItemID::UNION_GUARD;
// auto& e = entries.emplace_back();
// e.name.encode("Team Points Ticket 500");
// e.description.encode("Create a 500-point ticket");
// e.team_points = 500;
// e.reward_id = TeamRewardMenuItemID::TEAM_POINTS_500;
// auto& e = entries.emplace_back();
// e.name.encode("Team Points Ticket 1000");
// e.description.encode("Create a 1000-point ticket");
// e.team_points = 1000;
// e.reward_id = TeamRewardMenuItemID::TEAM_POINTS_1000;
// auto& e = entries.emplace_back();
// e.name.encode("Team Points Ticket 5000");
// e.description.encode("Create a 5000-point ticket");
// e.team_points = 5000;
// e.reward_id = TeamRewardMenuItemID::TEAM_POINTS_5000;
// auto& e = entries.emplace_back();
// e.name.encode("Team Points Ticket 10000");
// e.description.encode("Create a 10000-point ticket");
// e.team_points = 10000;
// e.reward_id = TeamRewardMenuItemID::TEAM_POINTS_10000;
S_TeamRewardsAvailableForPurchase_BB_1AEA cmd;
S_TeamRewardList_BB_19EA_1AEA cmd;
cmd.num_entries = entries.size();
send_command_t_vt(c, 0x1AEA, 0x00000000, cmd, entries);
send_command_t_vt(c, show_purchased ? 0x19EA : 0x1AEA, 0x00000000, cmd, entries);
}
+8 -7
View File
@@ -232,6 +232,8 @@ __attribute__((format(printf, 2, 3))) void send_ep3_text_message_printf(
void send_info_board(std::shared_ptr<Client> c);
void send_choice_search_choices(std::shared_ptr<Client> c);
void send_card_search_result(
std::shared_ptr<Client> c,
std::shared_ptr<Client> result,
@@ -255,12 +257,10 @@ void send_game_menu(
bool is_tournament_game_list);
void send_quest_menu(
std::shared_ptr<Client> c,
uint32_t menu_id,
const std::vector<std::shared_ptr<const Quest>>& quests,
const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests,
bool is_download_menu);
void send_quest_categories_menu(
std::shared_ptr<Client> c,
uint32_t menu_id,
std::shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode);
@@ -305,8 +305,8 @@ void send_drop_item(std::shared_ptr<Lobby> l, const ItemData& item,
void send_drop_stacked_item(std::shared_ptr<ServerState> s, Channel& ch, const ItemData& item, uint8_t floor, float x, float z);
void send_drop_stacked_item(std::shared_ptr<Lobby> l, const ItemData& item, uint8_t floor, float x, float z);
void send_pick_up_item(std::shared_ptr<Client> c, uint32_t id, uint8_t floor);
void send_create_inventory_item(std::shared_ptr<Client> c, const ItemData& item);
void send_destroy_item(std::shared_ptr<Client> c, uint32_t item_id, uint32_t amount);
void send_create_inventory_item(std::shared_ptr<Client> c, const ItemData& item, bool exclude_c = false);
void send_destroy_item(std::shared_ptr<Client> c, uint32_t item_id, uint32_t amount, bool exclude_c = false);
void send_item_identify_result(std::shared_ptr<Client> c);
void send_bank(std::shared_ptr<Client> c);
void send_shop(std::shared_ptr<Client> c, uint8_t shop_type);
@@ -393,5 +393,6 @@ void send_update_team_metadata_for_client(std::shared_ptr<Client> c); // 15EA (t
void send_all_nearby_team_metadatas_to_client(std::shared_ptr<Client> c, bool is_13EA); // 13EA/15EA (to only c, with all lobby clients' data)
void send_update_team_reward_flags(std::shared_ptr<Client> c); // 1DEA
void send_team_member_list(std::shared_ptr<Client> c); // 09EA
void send_team_rank_info(std::shared_ptr<Client> c); // 18EA
void send_team_rewards_available_for_purchase(std::shared_ptr<Client> c); // 1AEA
void send_intra_team_ranking(std::shared_ptr<Client> c); // 18EA
void send_team_reward_list(std::shared_ptr<Client> c, bool show_purchased); // 19EA, 1AEA
void send_cross_team_ranking(std::shared_ptr<Client> c); // 1CEA
+3 -4
View File
@@ -110,8 +110,7 @@ void Server::on_listen_accept(
struct bufferevent* bev = bufferevent_socket_new(this->base.get(), fd,
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
shared_ptr<Client> c(new Client(
this->shared_from_this(), bev, listening_socket->version, listening_socket->behavior));
auto c = make_shared<Client>(this->shared_from_this(), bev, listening_socket->version, listening_socket->behavior);
c->channel.on_command_received = Server::on_client_input;
c->channel.on_error = Server::on_client_error;
c->channel.context_obj = this;
@@ -131,7 +130,7 @@ void Server::on_listen_accept(
void Server::connect_client(
struct bufferevent* bev, uint32_t address, uint16_t client_port,
uint16_t server_port, Version version, ServerBehavior initial_state) {
shared_ptr<Client> c(new Client(this->shared_from_this(), bev, version, initial_state));
auto c = make_shared<Client>(this->shared_from_this(), bev, version, initial_state);
c->channel.on_command_received = Server::on_client_input;
c->channel.on_error = Server::on_client_error;
c->channel.context_obj = this;
@@ -316,7 +315,7 @@ vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident
continue;
}
auto p = c->game_data.character(false, false);
auto p = c->character(false, false);
if (p && p->disp.name.eq(ident, p->inventory.language)) {
results.emplace_back(std::move(c));
continue;
+6 -4
View File
@@ -314,13 +314,14 @@ Proxy session commands:\n\
auto config_json = this->state->load_config();
this->state->parse_config(config_json, true);
this->state->resolve_ep3_card_names();
this->state->load_teams();
} else {
throw invalid_argument("incorrect data type");
}
}
} else if (command_name == "add-license") {
shared_ptr<License> l(new License());
auto l = this->state->license_index->create_license();
for (const string& token : split(command_args, ' ')) {
if (starts_with(token, "bb-username=")) {
@@ -375,7 +376,8 @@ Proxy session commands:\n\
uint32_t serial_number = stoul(tokens[0]);
tokens.erase(tokens.begin());
auto orig_l = this->state->license_index->get(serial_number);
shared_ptr<License> l(new License(*orig_l));
auto l = this->state->license_index->create_license();
*l = *orig_l;
this->state->license_index->remove(orig_l->serial_number);
try {
@@ -656,11 +658,11 @@ Proxy session commands:\n\
for (size_t z = 0; z < ses->lobby_players.size(); z++) {
const auto& player = ses->lobby_players[z];
if (player.guild_card_number) {
auto secid_name = name_for_section_id(player.section_id);
fprintf(stderr, " %zu: %" PRIu32 " => %s (%c, %s, %s)\n",
z, player.guild_card_number, player.name.c_str(),
char_for_language_code(player.language),
name_for_char_class(player.char_class), secid_name.c_str());
name_for_char_class(player.char_class),
name_for_section_id(player.section_id));
} else {
fprintf(stderr, " %zu: (no player)\n", z);
}
+147 -89
View File
@@ -17,7 +17,7 @@
using namespace std;
ServerState::ServerState(const string& config_filename, bool is_replay)
ServerState::ServerState(shared_ptr<struct event_base> base, const string& config_filename, bool is_replay)
: config_filename(config_filename),
is_replay(is_replay),
dns_server_port(0),
@@ -42,6 +42,7 @@ ServerState::ServerState(const string& config_filename, bool is_replay)
ep3_card_auction_points(0),
ep3_card_auction_min_size(0),
ep3_card_auction_max_size(0),
player_files_manager(make_shared<PlayerFilesManager>(base)),
next_lobby_id(1),
pre_lobby_event(0),
ep3_menu_song(-1),
@@ -253,7 +254,7 @@ shared_ptr<Lobby> ServerState::create_lobby() {
while (this->id_to_lobby.count(this->next_lobby_id)) {
this->next_lobby_id++;
}
shared_ptr<Lobby> l(new Lobby(this->shared_from_this(), this->next_lobby_id++));
auto l = make_shared<Lobby>(this->shared_from_this(), this->next_lobby_id++);
this->id_to_lobby.emplace(l->lobby_id, l);
l->log.info("Created lobby");
return l;
@@ -287,7 +288,7 @@ void ServerState::remove_lobby(uint32_t lobby_id) {
this->id_to_lobby.erase(lobby_it);
}
shared_ptr<Client> ServerState::find_client(const std::string* identifier, uint64_t serial_number, shared_ptr<Lobby> l) {
shared_ptr<Client> ServerState::find_client(const string* identifier, uint64_t serial_number, shared_ptr<Lobby> l) {
if ((serial_number == 0) && identifier) {
try {
serial_number = stoull(*identifier, nullptr, 0);
@@ -315,7 +316,7 @@ shared_ptr<Client> ServerState::find_client(const std::string* identifier, uint6
throw out_of_range("client not found");
}
uint32_t ServerState::connect_address_for_client(std::shared_ptr<Client> c) const {
uint32_t ServerState::connect_address_for_client(shared_ptr<Client> c) const {
if (c->channel.is_virtual_connection) {
if (c->channel.remote_addr.ss_family != AF_INET) {
throw logic_error("virtual connection is missing remote IPv4 address");
@@ -334,7 +335,7 @@ uint32_t ServerState::connect_address_for_client(std::shared_ptr<Client> c) cons
}
}
std::shared_ptr<const Menu> ServerState::information_menu_for_version(Version version) const {
shared_ptr<const Menu> ServerState::information_menu_for_version(Version version) const {
if (is_v1_or_v2(version)) {
return this->information_menu_v2;
} else if (is_v3(version)) {
@@ -370,10 +371,10 @@ const vector<pair<string, uint16_t>>& ServerState::proxy_destinations_for_versio
case Version::DC_V1_11_2000_PROTOTYPE:
case Version::DC_V1:
case Version::DC_V2:
case Version::GC_NTE:
return this->proxy_destinations_dc;
case Version::PC_V2:
return this->proxy_destinations_pc;
case Version::GC_NTE:
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
@@ -385,7 +386,7 @@ const vector<pair<string, uint16_t>>& ServerState::proxy_destinations_for_versio
}
}
std::shared_ptr<const ItemParameterTable> ServerState::item_parameter_table_for_version(Version version) const {
shared_ptr<const ItemParameterTable> ServerState::item_parameter_table_for_version(Version version) const {
switch (version) {
case Version::DC_NTE:
case Version::DC_V1_11_2000_PROTOTYPE:
@@ -406,7 +407,7 @@ std::shared_ptr<const ItemParameterTable> ServerState::item_parameter_table_for_
}
}
std::string ServerState::describe_item(Version version, const ItemData& item, bool include_color_codes) const {
string ServerState::describe_item(Version version, const ItemData& item, bool include_color_codes) const {
return this->item_name_index->describe_item(
version,
item,
@@ -420,7 +421,7 @@ void ServerState::set_port_configuration(
bool any_port_is_pc_console_detect = false;
for (const auto& pc : port_configs) {
shared_ptr<PortConfiguration> spc(new PortConfiguration(pc));
auto spc = make_shared<PortConfiguration>(pc);
if (!this->name_to_port_config.emplace(spc->name, spc).second) {
// Note: This is a logic_error instead of a runtime_error because
// port_configs comes from a JSON map, so the names should already all be
@@ -448,9 +449,9 @@ void ServerState::set_port_configuration(
}
shared_ptr<const string> ServerState::load_bb_file(
const std::string& patch_index_filename,
const std::string& gsl_filename,
const std::string& bb_directory_filename) const {
const string& patch_index_filename,
const string& gsl_filename,
const string& bb_directory_filename) const {
if (this->bb_patch_file_index) {
// First, look in the patch tree's data directory
@@ -470,7 +471,7 @@ shared_ptr<const string> ServerState::load_bb_file(
try {
// TODO: It's kinda not great that we copy the data here; find a way to
// avoid doing this (also in the below case)
shared_ptr<string> ret(new string(this->bb_data_gsl->get_copy(effective_gsl_filename)));
auto ret = make_shared<string>(this->bb_data_gsl->get_copy(effective_gsl_filename));
static_game_data_log.info("Loaded %s from data.gsl in BB patch tree", effective_gsl_filename.c_str());
return ret;
} catch (const out_of_range&) {
@@ -482,7 +483,7 @@ shared_ptr<const string> ServerState::load_bb_file(
if (dot_offset != string::npos) {
string no_ext_gsl_filename = effective_gsl_filename.substr(0, dot_offset);
try {
shared_ptr<string> ret(new string(this->bb_data_gsl->get_copy(no_ext_gsl_filename)));
auto ret = make_shared<string>(this->bb_data_gsl->get_copy(no_ext_gsl_filename));
static_game_data_log.info("Loaded %s from data.gsl in BB patch tree", no_ext_gsl_filename.c_str());
return ret;
} catch (const out_of_range&) {
@@ -581,6 +582,16 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
}
} catch (const out_of_range&) {
}
try {
for (const auto& item : json.at("PPPStackListen").as_list()) {
if (item->is_int()) {
this->ppp_stack_addresses.emplace_back(string_printf("0.0.0.0:%" PRId64, item->as_int()));
} else {
this->ppp_stack_addresses.emplace_back(item->as_string());
}
}
} catch (const out_of_range&) {
}
}
auto local_address_str = json.at("LocalAddress").as_string();
@@ -700,7 +711,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
{
auto parse_ep3_ex_result_cmd = [&](const JSON& src) -> shared_ptr<G_SetEXResultValues_GC_Ep3_6xB4x4B> {
shared_ptr<G_SetEXResultValues_GC_Ep3_6xB4x4B> ret(new G_SetEXResultValues_GC_Ep3_6xB4x4B());
auto ret = make_shared<G_SetEXResultValues_GC_Ep3_6xB4x4B>();
const auto& win_json = src.at("Win");
for (size_t z = 0; z < min<size_t>(win_json.size(), 10); z++) {
ret->win_entries[z].threshold = win_json.at(z).at(0).as_int();
@@ -790,7 +801,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
if (!is_reload) {
try {
this->quest_category_index.reset(new QuestCategoryIndex(json.at("QuestCategories")));
this->quest_category_index = make_shared<QuestCategoryIndex>(json.at("QuestCategories"));
} catch (const exception& e) {
throw runtime_error(string_printf(
"QuestCategories is missing or invalid in config.json (%s) - see config.example.json for an example", e.what()));
@@ -799,10 +810,10 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
config_log.info("Creating menus");
shared_ptr<Menu> information_menu_v2(new Menu(MenuID::INFORMATION, "Information"));
shared_ptr<Menu> information_menu_v3(new Menu(MenuID::INFORMATION, "Information"));
shared_ptr<vector<string>> information_contents_v2(new vector<string>());
shared_ptr<vector<string>> information_contents_v3(new vector<string>());
auto information_menu_v2 = make_shared<Menu>(MenuID::INFORMATION, "Information");
auto information_menu_v3 = make_shared<Menu>(MenuID::INFORMATION, "Information");
shared_ptr<vector<string>> information_contents_v2 = make_shared<vector<string>>();
shared_ptr<vector<string>> information_contents_v3 = make_shared<vector<string>>();
information_menu_v2->items.emplace_back(InformationMenuItemID::GO_BACK, "Go back",
"Return to the\nmain menu", MenuItem::Flag::INVISIBLE_IN_INFO_MENU);
@@ -838,7 +849,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
this->information_contents_v3 = information_contents_v3;
auto generate_proxy_destinations_menu = [&](vector<pair<string, uint16_t>>& ret_pds, const char* key) -> shared_ptr<const Menu> {
shared_ptr<Menu> ret(new Menu(MenuID::PROXY_DESTINATIONS, "Proxy server"));
auto ret = make_shared<Menu>(MenuID::PROXY_DESTINATIONS, "Proxy server");
ret_pds.clear();
try {
@@ -898,6 +909,54 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
this->welcome_message = json.get_string("WelcomeMessage", "");
this->pc_patch_server_message = json.get_string("PCPatchServerMessage", "");
this->bb_patch_server_message = json.get_string("BBPatchServerMessage", "");
try {
this->team_reward_defs_json = std::move(json.at("TeamRewards"));
} catch (const out_of_range&) {
}
for (size_t z = 0; z < 4; z++) {
shared_ptr<const Map::RareEnemyRates> prev = Map::DEFAULT_RARE_ENEMIES;
try {
string key = "RareEnemyRates-";
key += token_name_for_difficulty(z);
this->rare_enemy_rates_by_difficulty[z] = make_shared<Map::RareEnemyRates>(json.at(key));
prev = this->rare_enemy_rates_by_difficulty[z];
} catch (const out_of_range&) {
this->rare_enemy_rates_by_difficulty[z] = prev;
}
}
try {
this->rare_enemy_rates_challenge = make_shared<Map::RareEnemyRates>(json.at("RareEnemyRates-Challenge"));
} catch (const out_of_range&) {
this->rare_enemy_rates_challenge = Map::DEFAULT_RARE_ENEMIES;
}
this->min_levels_v4[0] = DEFAULT_MIN_LEVELS_EP1;
this->min_levels_v4[1] = DEFAULT_MIN_LEVELS_EP2;
this->min_levels_v4[2] = DEFAULT_MIN_LEVELS_EP4;
try {
for (const auto& ep_it : json.get_dict("BBMinimumLevels")) {
array<size_t, 4> levels({0, 0, 0, 0});
for (size_t z = 0; z < 4; z++) {
levels[z] = ep_it.second->get_int(z) - 1;
}
switch (episode_for_token_name(ep_it.first)) {
case Episode::EP1:
this->min_levels_v4[0] = levels;
break;
case Episode::EP2:
this->min_levels_v4[1] = levels;
break;
case Episode::EP4:
this->min_levels_v4[2] = levels;
break;
default:
throw runtime_error("unknown episode");
}
}
} catch (const out_of_range&) {
}
}
void ServerState::load_bb_private_keys() {
@@ -905,7 +964,7 @@ void ServerState::load_bb_private_keys() {
if (!ends_with(filename, ".nsk")) {
continue;
}
this->bb_private_keys.emplace_back(new PSOBBEncryption::KeyFile(
this->bb_private_keys.emplace_back(make_shared<PSOBBEncryption::KeyFile>(
load_object_file<PSOBBEncryption::KeyFile>("system/blueburst/keys/" + filename)));
config_log.info("Loaded Blue Burst key file: %s", filename.c_str());
}
@@ -914,27 +973,28 @@ void ServerState::load_bb_private_keys() {
void ServerState::load_licenses() {
config_log.info("Indexing licenses");
this->license_index.reset(new LicenseIndex());
this->license_index = this->is_replay ? make_shared<LicenseIndex>() : make_shared<DiskLicenseIndex>();
}
void ServerState::load_teams() {
config_log.info("Indexing teams");
this->team_index.reset(new TeamIndex("system/teams"));
this->team_index = make_shared<TeamIndex>("system/teams", this->team_reward_defs_json);
this->team_reward_defs_json = nullptr;
}
void ServerState::load_patch_indexes() {
if (isdir("system/patch-pc")) {
config_log.info("Indexing PSO PC patch files");
this->pc_patch_file_index.reset(new PatchFileIndex("system/patch-pc"));
this->pc_patch_file_index = make_shared<PatchFileIndex>("system/patch-pc");
} else {
config_log.info("PSO PC patch files not present");
}
if (isdir("system/patch-bb")) {
config_log.info("Indexing PSO BB patch files");
this->bb_patch_file_index.reset(new PatchFileIndex("system/patch-bb"));
this->bb_patch_file_index = make_shared<PatchFileIndex>("system/patch-bb");
try {
auto gsl_file = this->bb_patch_file_index->get("./data/data.gsl");
this->bb_data_gsl.reset(new GSLArchive(gsl_file->load_data(), false));
this->bb_data_gsl = make_shared<GSLArchive>(gsl_file->load_data(), false);
config_log.info("data.gsl found in BB patch files");
} catch (const out_of_range&) {
config_log.info("data.gsl is not present in BB patch files");
@@ -946,33 +1006,34 @@ void ServerState::load_patch_indexes() {
void ServerState::load_battle_params() {
config_log.info("Loading battle parameters");
this->battle_params.reset(new BattleParamsIndex(
this->battle_params = make_shared<BattleParamsIndex>(
this->load_bb_file("BattleParamEntry_on.dat"),
this->load_bb_file("BattleParamEntry_lab_on.dat"),
this->load_bb_file("BattleParamEntry_ep4_on.dat"),
this->load_bb_file("BattleParamEntry.dat"),
this->load_bb_file("BattleParamEntry_lab.dat"),
this->load_bb_file("BattleParamEntry_ep4.dat")));
this->load_bb_file("BattleParamEntry_ep4.dat"));
}
void ServerState::load_level_table() {
config_log.info("Loading level table");
this->level_table.reset(new LevelTable(this->load_bb_file("PlyLevelTbl.prs"), true));
this->level_table = make_shared<LevelTable>(this->load_bb_file("PlyLevelTbl.prs"), true);
}
void ServerState::load_word_select_table() {
config_log.info("Loading Word Select table");
this->word_select_table.reset(new WordSelectTable(JSON::parse(load_file("system/word-select-table.json"))));
this->word_select_table = make_shared<WordSelectTable>(JSON::parse(load_file("system/word-select-table.json")));
}
void ServerState::load_item_tables() {
config_log.info("Loading item name index");
this->item_name_index.reset(new ItemNameIndex(
this->item_name_index = make_shared<ItemNameIndex>(
JSON::parse(load_file("system/item-tables/names-v2.json")),
JSON::parse(load_file("system/item-tables/names-v3.json")),
JSON::parse(load_file("system/item-tables/names-v4.json"))));
JSON::parse(load_file("system/item-tables/names-v4.json")));
config_log.info("Loading rare item sets");
unordered_map<string, shared_ptr<const RareItemSet>> new_rare_item_sets;
for (const auto& filename : list_directory_sorted("system/item-tables")) {
if (!starts_with(filename, "rare-table-")) {
continue;
@@ -982,62 +1043,61 @@ void ServerState::load_item_tables() {
size_t ext_offset = filename.rfind('.');
string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset);
if (ends_with(filename, "-v2.json")) {
if (ends_with(filename, "-v1.json")) {
config_log.info("Loading v1 JSON rare item table %s", filename.c_str());
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), Version::DC_V1, this->item_name_index));
} else if (ends_with(filename, "-v2.json")) {
config_log.info("Loading v2 JSON rare item table %s", filename.c_str());
this->rare_item_sets.emplace(basename, new RareItemSet(JSON::parse(load_file(path)), Version::PC_V2, this->item_name_index));
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), Version::PC_V2, this->item_name_index));
} else if (ends_with(filename, "-v3.json")) {
config_log.info("Loading v3 JSON rare item table %s", filename.c_str());
this->rare_item_sets.emplace(basename, new RareItemSet(JSON::parse(load_file(path)), Version::GC_V3, this->item_name_index));
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), Version::GC_V3, this->item_name_index));
} else if (ends_with(filename, "-v4.json")) {
config_log.info("Loading v4 JSON rare item table %s", filename.c_str());
this->rare_item_sets.emplace(basename, new RareItemSet(JSON::parse(load_file(path)), Version::BB_V4, this->item_name_index));
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(JSON::parse(load_file(path)), Version::BB_V4, this->item_name_index));
} else if (ends_with(filename, ".afs")) {
config_log.info("Loading AFS rare item table %s", filename.c_str());
shared_ptr<string> data(new string(load_file(path)));
this->rare_item_sets.emplace(basename, new RareItemSet(AFSArchive(data), false));
auto data = make_shared<string>(load_file(path));
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(AFSArchive(data), false));
} else if (ends_with(filename, ".gsl")) {
config_log.info("Loading GSL rare item table %s", filename.c_str());
shared_ptr<string> data(new string(load_file(path)));
this->rare_item_sets.emplace(basename, new RareItemSet(GSLArchive(data, false), false));
auto data = make_shared<string>(load_file(path));
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(GSLArchive(data, false), false));
} else if (ends_with(filename, ".gslb")) {
config_log.info("Loading GSL rare item table %s", filename.c_str());
shared_ptr<string> data(new string(load_file(path)));
this->rare_item_sets.emplace(basename, new RareItemSet(GSLArchive(data, true), true));
auto data = make_shared<string>(load_file(path));
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(GSLArchive(data, true), true));
} else if (ends_with(filename, ".rel")) {
config_log.info("Loading REL rare item table %s", filename.c_str());
this->rare_item_sets.emplace(basename, new RareItemSet(load_file(path), true));
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(load_file(path), true));
}
}
if (!this->rare_item_sets.count("rare-table-v4")) {
if (!new_rare_item_sets.count("rare-table-v4")) {
config_log.info("rare-table-v4 rare item set is not available; loading from BB data");
this->rare_item_sets.emplace("rare-table-v4", new RareItemSet(load_file("system/blueburst/ItemRT.rel"), true));
new_rare_item_sets.emplace("rare-table-v4", make_shared<RareItemSet>(load_file("system/blueburst/ItemRT.rel"), true));
}
this->rare_item_sets.swap(new_rare_item_sets);
config_log.info("Loading v2 common item table");
shared_ptr<string> ct_data_v2(new string(load_file("system/item-tables/ItemCT-v2.afs")));
shared_ptr<string> pt_data_v2(new string(load_file("system/item-tables/ItemPT-v2.afs")));
this->common_item_set_v2.reset(new AFSV2CommonItemSet(pt_data_v2, ct_data_v2));
config_log.info("Loading v3 common item table");
shared_ptr<string> pt_data_v3(new string(load_file("system/item-tables/ItemPT-gc.gsl")));
this->common_item_set_v3.reset(new GSLV3CommonItemSet(pt_data_v3, true));
// Note: The ItemPT files don't exist in BB, so we use the GC versions of them
// instead. This doesn't include Episode 4 of course, so we use Episode 1
// parameters for Episode 4 implicitly.
auto ct_data_v2 = make_shared<string>(load_file("system/item-tables/ItemCT-v2.afs"));
auto pt_data_v2 = make_shared<string>(load_file("system/item-tables/ItemPT-v2.afs"));
this->common_item_set_v2 = make_shared<AFSV2CommonItemSet>(pt_data_v2, ct_data_v2);
config_log.info("Loading v3+v4 common item table");
auto pt_data_v3_v4 = make_shared<string>(load_file("system/item-tables/ItemPT-gc-v4.gsl"));
this->common_item_set_v3_v4 = make_shared<GSLV3V4CommonItemSet>(pt_data_v3_v4, true);
config_log.info("Loading armor table");
shared_ptr<string> armor_data(new string(load_file(
"system/item-tables/ArmorRandom-gc.rel")));
this->armor_random_set.reset(new ArmorRandomSet(armor_data));
auto armor_data = make_shared<string>(load_file("system/item-tables/ArmorRandom-gc.rel"));
this->armor_random_set = make_shared<ArmorRandomSet>(armor_data);
config_log.info("Loading tool table");
shared_ptr<string> tool_data(new string(load_file(
"system/item-tables/ToolRandom-gc.rel")));
this->tool_random_set.reset(new ToolRandomSet(tool_data));
auto tool_data = make_shared<string>(load_file("system/item-tables/ToolRandom-gc.rel"));
this->tool_random_set = make_shared<ToolRandomSet>(tool_data);
config_log.info("Loading weapon tables");
const char* filenames[4] = {
@@ -1047,54 +1107,52 @@ void ServerState::load_item_tables() {
"system/item-tables/WeaponRandomUltimate-gc.rel",
};
for (size_t z = 0; z < 4; z++) {
shared_ptr<string> weapon_data(new string(load_file(filenames[z])));
this->weapon_random_sets[z].reset(new WeaponRandomSet(weapon_data));
auto weapon_data = make_shared<string>(load_file(filenames[z]));
this->weapon_random_sets[z] = make_shared<WeaponRandomSet>(weapon_data);
}
config_log.info("Loading tekker adjustment table");
shared_ptr<string> tekker_data(new string(load_file(
"system/item-tables/JudgeItem-gc.rel")));
this->tekker_adjustment_set.reset(new TekkerAdjustmentSet(tekker_data));
auto tekker_data = make_shared<string>(load_file("system/item-tables/JudgeItem-gc.rel"));
this->tekker_adjustment_set = make_shared<TekkerAdjustmentSet>(tekker_data);
config_log.info("Loading item definition tables");
shared_ptr<string> pmt_data_v2(new string(prs_decompress(load_file("system/item-tables/ItemPMT-v2.prs"))));
this->item_parameter_table_v2.reset(new ItemParameterTable(pmt_data_v2, ItemParameterTable::Version::V2));
shared_ptr<string> pmt_data_v3(new string(prs_decompress(load_file("system/item-tables/ItemPMT-gc.prs"))));
this->item_parameter_table_v3.reset(new ItemParameterTable(pmt_data_v3, ItemParameterTable::Version::V3));
shared_ptr<string> pmt_data_v4(new string(prs_decompress(load_file("system/item-tables/ItemPMT-bb.prs"))));
this->item_parameter_table_v4.reset(new ItemParameterTable(pmt_data_v4, ItemParameterTable::Version::V4));
auto pmt_data_v2 = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemPMT-v2.prs")));
this->item_parameter_table_v2 = make_shared<ItemParameterTable>(pmt_data_v2, ItemParameterTable::Version::V2);
auto pmt_data_v3 = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemPMT-gc.prs")));
this->item_parameter_table_v3 = make_shared<ItemParameterTable>(pmt_data_v3, ItemParameterTable::Version::V3);
auto pmt_data_v4 = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemPMT-bb.prs")));
this->item_parameter_table_v4 = make_shared<ItemParameterTable>(pmt_data_v4, ItemParameterTable::Version::V4);
config_log.info("Loading mag evolution table");
shared_ptr<string> mag_data(new string(prs_decompress(load_file(
"system/item-tables/ItemMagEdit-bb.prs"))));
this->mag_evolution_table.reset(new MagEvolutionTable(mag_data));
auto mag_data = make_shared<string>(prs_decompress(load_file("system/item-tables/ItemMagEdit-bb.prs")));
this->mag_evolution_table = make_shared<MagEvolutionTable>(mag_data);
}
void ServerState::load_ep3_data() {
config_log.info("Collecting Episode 3 maps");
this->ep3_map_index.reset(new Episode3::MapIndex("system/ep3/maps"));
this->ep3_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps");
config_log.info("Loading Episode 3 card definitions");
this->ep3_card_index.reset(new Episode3::CardIndex(
this->ep3_card_index = make_shared<Episode3::CardIndex>(
"system/ep3/card-definitions.mnr",
"system/ep3/card-definitions.mnrd",
"system/ep3/card-text.mnr",
"system/ep3/card-text.mnrd",
"system/ep3/card-dice-text.mnr",
"system/ep3/card-dice-text.mnrd"));
"system/ep3/card-dice-text.mnrd");
config_log.info("Loading Episode 3 trial card definitions");
this->ep3_card_index_trial.reset(new Episode3::CardIndex(
this->ep3_card_index_trial = make_shared<Episode3::CardIndex>(
"system/ep3/card-definitions-trial.mnr",
"system/ep3/card-definitions-trial.mnrd",
"system/ep3/card-text-trial.mnr",
"system/ep3/card-text-trial.mnrd",
"system/ep3/card-dice-text-trial.mnr",
"system/ep3/card-dice-text-trial.mnrd"));
"system/ep3/card-dice-text-trial.mnrd");
config_log.info("Loading Episode 3 COM decks");
this->ep3_com_deck_index.reset(new Episode3::COMDeckIndex("system/ep3/com-decks.json"));
this->ep3_com_deck_index = make_shared<Episode3::COMDeckIndex>("system/ep3/com-decks.json");
const string& tournament_state_filename = "system/ep3/tournament-state.json";
this->ep3_tournament_index.reset(new Episode3::TournamentIndex(
this->ep3_map_index, this->ep3_com_deck_index, tournament_state_filename));
this->ep3_tournament_index = make_shared<Episode3::TournamentIndex>(
this->ep3_map_index, this->ep3_com_deck_index, tournament_state_filename);
this->ep3_tournament_index->link_all_clients(this->shared_from_this());
config_log.info("Loaded Episode 3 tournament state");
}
@@ -1132,25 +1190,25 @@ void ServerState::resolve_ep3_card_names() {
void ServerState::load_quest_index() {
config_log.info("Collecting quests");
this->default_quest_index.reset(new QuestIndex("system/quests", this->quest_category_index, false));
this->default_quest_index = make_shared<QuestIndex>("system/quests", this->quest_category_index, false);
config_log.info("Collecting Episode 3 download quests");
this->ep3_download_quest_index.reset(new QuestIndex("system/ep3/maps-download", this->quest_category_index, true));
this->ep3_download_quest_index = make_shared<QuestIndex>("system/ep3/maps-download", this->quest_category_index, true);
}
void ServerState::compile_functions() {
config_log.info("Compiling client functions");
this->function_code_index.reset(new FunctionCodeIndex("system/ppc"));
this->function_code_index = make_shared<FunctionCodeIndex>("system/ppc");
}
void ServerState::load_dol_files() {
config_log.info("Loading DOL files");
this->dol_file_index.reset(new DOLFileIndex("system/dol"));
this->dol_file_index = make_shared<DOLFileIndex>("system/dol");
}
shared_ptr<const vector<string>> ServerState::information_contents_for_client(shared_ptr<const Client> c) const {
return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3;
}
shared_ptr<const QuestIndex> ServerState::quest_index_for_client(shared_ptr<const Client> c) const {
return is_ep3(c->version()) ? this->ep3_download_quest_index : this->default_quest_index;
shared_ptr<const QuestIndex> ServerState::quest_index_for_version(Version version) const {
return is_ep3(version) ? this->ep3_download_quest_index : this->default_quest_index;
}
+12 -3
View File
@@ -1,5 +1,7 @@
#pragma once
#include <event2/event.h>
#include <atomic>
#include <map>
#include <memory>
@@ -21,6 +23,7 @@
#include "License.hh"
#include "Lobby.hh"
#include "Menu.hh"
#include "PlayerFilesManager.hh"
#include "Quest.hh"
#include "TeamIndex.hh"
#include "WordSelectTable.hh"
@@ -65,6 +68,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::string username;
uint16_t dns_server_port;
std::vector<std::string> ip_stack_addresses;
std::vector<std::string> ppp_stack_addresses;
bool ip_stack_debug;
bool allow_unregistered_users;
bool allow_dc_pc_games;
@@ -102,7 +106,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const GSLArchive> bb_data_gsl;
std::unordered_map<std::string, std::shared_ptr<const RareItemSet>> rare_item_sets;
std::shared_ptr<const CommonItemSet> common_item_set_v2;
std::shared_ptr<const CommonItemSet> common_item_set_v3;
std::shared_ptr<const CommonItemSet> common_item_set_v3_v4;
std::shared_ptr<const ArmorRandomSet> armor_random_set;
std::shared_ptr<const ToolRandomSet> tool_random_set;
std::array<std::shared_ptr<const WeaponRandomSet>, 4> weapon_random_sets;
@@ -113,6 +117,9 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const MagEvolutionTable> mag_evolution_table;
std::shared_ptr<const ItemNameIndex> item_name_index;
std::shared_ptr<const WordSelectTable> word_select_table;
std::array<std::shared_ptr<const Map::RareEnemyRates>, 4> rare_enemy_rates_by_difficulty;
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]
// Indexed as [type][difficulty][random_choice]
std::vector<std::vector<std::vector<ItemData>>> quest_F95E_results;
@@ -143,6 +150,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<LicenseIndex> license_index;
std::shared_ptr<TeamIndex> team_index;
JSON team_reward_defs_json;
std::shared_ptr<const Menu> information_menu_v2;
std::shared_ptr<const Menu> information_menu_v3;
@@ -162,6 +170,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::string pc_patch_server_message;
std::string bb_patch_server_message;
std::shared_ptr<PlayerFilesManager> player_files_manager;
std::unordered_map<Channel*, std::shared_ptr<Client>> channel_to_client;
std::map<int64_t, std::shared_ptr<Lobby>> id_to_lobby;
std::vector<std::shared_ptr<Lobby>> public_lobby_search_order;
@@ -179,7 +188,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<ProxyServer> proxy_server;
std::shared_ptr<Server> game_server;
ServerState(const std::string& config_filename, bool is_replay);
ServerState(std::shared_ptr<struct event_base> base, const std::string& config_filename, bool is_replay);
ServerState(const ServerState&) = delete;
ServerState(ServerState&&) = delete;
ServerState& operator=(const ServerState&) = delete;
@@ -218,7 +227,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::string describe_item(Version version, const ItemData& item, bool include_color_codes) const;
std::shared_ptr<const std::vector<std::string>> information_contents_for_client(std::shared_ptr<const Client> c) const;
std::shared_ptr<const QuestIndex> quest_index_for_client(std::shared_ptr<const Client> c) const;
std::shared_ptr<const QuestIndex> quest_index_for_version(Version version) const;
void set_port_configuration(const std::vector<PortConfiguration>& port_configs);
+23 -4
View File
@@ -42,6 +42,22 @@ const char* token_name_for_episode(Episode ep) {
}
}
Episode episode_for_token_name(const string& name) {
if (name == "Episode1") {
return Episode::EP1;
}
if (name == "Episode2") {
return Episode::EP2;
}
if (name == "Episode3") {
return Episode::EP3;
}
if (name == "Episode4") {
return Episode::EP4;
}
throw runtime_error("unknown episode");
}
const char* abbreviation_for_episode(Episode ep) {
switch (ep) {
case Episode::NONE:
@@ -89,7 +105,7 @@ const char* abbreviation_for_mode(GameMode mode) {
}
}
const vector<string> section_id_to_name = {
static const array<const char*, 10> section_id_to_name = {
"Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum",
"Pinkal", "Redria", "Oran", "Yellowboze", "Whitill"};
@@ -205,12 +221,11 @@ const vector<string> npc_id_to_name({"ninja", "rico", "sonic", "knuckles", "tail
const unordered_map<string, uint8_t> name_to_npc_id = {
{"ninja", 0}, {"rico", 1}, {"sonic", 2}, {"knuckles", 3}, {"tails", 4}, {"flowen", 5}, {"elly", 6}};
const string& name_for_section_id(uint8_t section_id) {
const char* name_for_section_id(uint8_t section_id) {
if (section_id < section_id_to_name.size()) {
return section_id_to_name[section_id];
} else {
static const string ret = "<Unknown section id>";
return ret;
return "<Unknown>";
}
}
@@ -765,3 +780,7 @@ char char_for_challenge_rank(uint8_t rank) {
}
return "BAS"[rank];
}
const array<size_t, 4> DEFAULT_MIN_LEVELS_EP1({0, 19, 39, 79});
const array<size_t, 4> DEFAULT_MIN_LEVELS_EP2({0, 29, 49, 89});
const array<size_t, 4> DEFAULT_MIN_LEVELS_EP4({0, 39, 79, 109});
+6 -1
View File
@@ -20,6 +20,7 @@ bool episode_has_arpg_semantics(Episode ep);
const char* name_for_episode(Episode ep);
const char* token_name_for_episode(Episode ep);
const char* abbreviation_for_episode(Episode ep);
Episode episode_for_token_name(const std::string& name);
enum class GameMode {
NORMAL = 0,
@@ -39,7 +40,7 @@ extern const std::unordered_map<std::string, uint8_t> name_to_tech_id;
const std::string& name_for_technique(uint8_t tech);
uint8_t technique_for_name(const std::string& name);
const std::string& name_for_section_id(uint8_t section_id);
const char* name_for_section_id(uint8_t section_id);
uint8_t section_id_for_name(const std::string& name);
const std::string& name_for_event(uint8_t event);
@@ -78,3 +79,7 @@ const char* name_for_floor(Episode episode, uint8_t floor);
uint32_t class_flags_for_class(uint8_t char_class);
char char_for_challenge_rank(uint8_t rank);
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_EP1;
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_EP2;
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_EP4;
+136 -9
View File
@@ -52,11 +52,20 @@ string TeamIndex::Team::flag_filename() const {
void TeamIndex::Team::load_config() {
auto json = JSON::parse(load_file(this->json_filename()));
this->name = json.get_string("Name");
this->spent_points = json.get_int("SpentPoints");
this->points = 0;
for (const auto& member_it : json.get_list("Members")) {
Member m(*member_it);
this->points += m.points;
uint32_t serial_number = m.serial_number;
this->members.emplace(serial_number, std::move(m));
}
try {
for (const auto& it : json.get_list("RewardKeys")) {
this->reward_keys.emplace(it->as_string());
}
} catch (const out_of_range&) {
}
this->reward_flags = json.get_int("RewardFlags");
}
@@ -65,9 +74,15 @@ void TeamIndex::Team::save_config() const {
for (const auto& it : this->members) {
members_json.emplace_back(it.second.json());
}
JSON reward_keys_json = JSON::list();
for (const auto& it : this->reward_keys) {
reward_keys_json.emplace_back(it);
}
JSON root = JSON::dict({
{"Name", this->name},
{"SpentPoints", this->spent_points},
{"Members", std::move(members_json)},
{"RewardKeys", std::move(reward_keys_json)},
{"RewardFlags", this->reward_flags},
});
save_file(this->json_filename(), root.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS));
@@ -128,6 +143,10 @@ PSOBBTeamMembership TeamIndex::Team::membership_for_member(uint32_t serial_numbe
return ret;
}
bool TeamIndex::Team::has_reward(const string& key) const {
return this->reward_keys.count(key);
}
size_t TeamIndex::Team::num_members() const {
return this->members.size();
}
@@ -178,9 +197,37 @@ bool TeamIndex::Team::can_promote_leader() const {
return this->num_leaders() < this->max_leaders();
}
TeamIndex::TeamIndex(const string& directory)
TeamIndex::Reward::Reward(uint32_t menu_item_id, const JSON& def_json)
: menu_item_id(menu_item_id),
key(def_json.get_string("Key")),
name(def_json.get_string("Name")),
description(def_json.get_string("Description")),
is_unique(def_json.get_bool("IsUnique", true)),
team_points(def_json.get_int("Points")) {
try {
for (const auto& it : def_json.get_list("PrerequisiteKeys")) {
this->prerequisite_keys.emplace(it->as_string());
}
} catch (const out_of_range&) {
}
try {
this->reward_flag = static_cast<Team::RewardFlag>(def_json.get_int("RewardFlag"));
} catch (const out_of_range&) {
}
try {
this->reward_item = ItemData::from_data(parse_data_string(def_json.get_string("RewardItem")));
} catch (const out_of_range&) {
}
}
TeamIndex::TeamIndex(const string& directory, const JSON& reward_defs_json)
: directory(directory),
next_team_id(1) {
uint32_t reward_menu_item_id = 0;
for (const auto& it : reward_defs_json.as_list()) {
this->reward_defs.emplace_back(reward_menu_item_id++, *it);
}
if (!isdir(this->directory)) {
mkdir(this->directory.c_str(), 0755);
return;
@@ -194,7 +241,7 @@ TeamIndex::TeamIndex(const string& directory)
if (ends_with(filename, ".json")) {
try {
uint32_t team_id = stoul(filename.substr(0, filename.size() - 5), nullptr, 16);
shared_ptr<Team> team(new Team(team_id));
auto team = make_shared<Team>(team_id);
team->load_config();
try {
team->load_flag();
@@ -214,7 +261,7 @@ size_t TeamIndex::count() const {
return this->id_to_team.size();
}
shared_ptr<TeamIndex::Team> TeamIndex::get_by_id(uint32_t team_id) {
shared_ptr<const TeamIndex::Team> TeamIndex::get_by_id(uint32_t team_id) const {
try {
return this->id_to_team.at(team_id);
} catch (const out_of_range&) {
@@ -222,7 +269,7 @@ shared_ptr<TeamIndex::Team> TeamIndex::get_by_id(uint32_t team_id) {
}
}
shared_ptr<TeamIndex::Team> TeamIndex::get_by_name(const string& name) {
shared_ptr<const TeamIndex::Team> TeamIndex::get_by_name(const string& name) const {
try {
return this->name_to_team.at(name);
} catch (const out_of_range&) {
@@ -230,7 +277,7 @@ shared_ptr<TeamIndex::Team> TeamIndex::get_by_name(const string& name) {
}
}
shared_ptr<TeamIndex::Team> TeamIndex::get_by_serial_number(uint32_t serial_number) {
shared_ptr<const TeamIndex::Team> TeamIndex::get_by_serial_number(uint32_t serial_number) const {
try {
return this->serial_number_to_team.at(serial_number);
} catch (const out_of_range&) {
@@ -238,16 +285,16 @@ shared_ptr<TeamIndex::Team> TeamIndex::get_by_serial_number(uint32_t serial_numb
}
}
vector<shared_ptr<TeamIndex::Team>> TeamIndex::all() {
vector<shared_ptr<Team>> ret;
vector<shared_ptr<const TeamIndex::Team>> TeamIndex::all() const {
vector<shared_ptr<const Team>> ret;
for (const auto& it : this->id_to_team) {
ret.emplace_back(it.second);
}
return ret;
}
shared_ptr<TeamIndex::Team> TeamIndex::create(string& name, uint32_t master_serial_number, const string& master_name) {
shared_ptr<Team> team(new Team(this->next_team_id++));
shared_ptr<const TeamIndex::Team> TeamIndex::create(string& name, uint32_t master_serial_number, const string& master_name) {
auto team = make_shared<Team>(this->next_team_id++);
save_file(this->directory + "/base.json", JSON::dict({{"NextTeamID", this->next_team_id}}).serialize());
Team::Member m;
@@ -301,6 +348,86 @@ void TeamIndex::remove_member(uint32_t serial_number) {
}
}
void TeamIndex::update_member_name(uint32_t serial_number, const std::string& name) {
auto team = this->serial_number_to_team.at(serial_number);
auto& m = team->members.at(serial_number);
m.name = name;
team->save_config();
}
void TeamIndex::add_member_points(uint32_t serial_number, uint32_t points) {
auto team = this->serial_number_to_team.at(serial_number);
team->members.at(serial_number).points += points;
team->points += points;
team->save_config();
}
void TeamIndex::set_flag_data(uint32_t team_id, const parray<le_uint16_t, 0x20 * 0x20>& flag_data) {
auto team = this->id_to_team.at(team_id);
team->flag_data.reset(new parray<le_uint16_t, 0x20 * 0x20>(flag_data));
team->save_flag();
}
bool TeamIndex::promote_leader(uint32_t master_serial_number, uint32_t leader_serial_number) {
auto team = this->serial_number_to_team.at(master_serial_number);
auto& master_m = team->members.at(master_serial_number);
if (!master_m.check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) {
throw runtime_error("incorrect master serial number");
}
auto& other_m = team->members.at(leader_serial_number);
if (other_m.check_flag(TeamIndex::Team::Member::Flag::IS_LEADER) || !team->can_promote_leader()) {
return false;
}
other_m.set_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
team->save_config();
return true;
}
bool TeamIndex::demote_leader(uint32_t master_serial_number, uint32_t leader_serial_number) {
auto team = this->serial_number_to_team.at(master_serial_number);
auto& master_m = team->members.at(master_serial_number);
if (!master_m.check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) {
throw runtime_error("incorrect master serial number");
}
auto& other_m = team->members.at(leader_serial_number);
if (!other_m.check_flag(TeamIndex::Team::Member::Flag::IS_LEADER)) {
return false;
}
other_m.clear_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
team->save_config();
return true;
}
void TeamIndex::change_master(uint32_t master_serial_number, uint32_t new_master_serial_number) {
auto team = this->serial_number_to_team.at(master_serial_number);
auto& master_m = team->members.at(master_serial_number);
if (!master_m.check_flag(TeamIndex::Team::Member::Flag::IS_MASTER)) {
throw runtime_error("incorrect master serial number");
}
auto& new_master_m = team->members.at(new_master_serial_number);
master_m.clear_flag(TeamIndex::Team::Member::Flag::IS_MASTER);
master_m.set_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
new_master_m.clear_flag(TeamIndex::Team::Member::Flag::IS_LEADER);
new_master_m.set_flag(TeamIndex::Team::Member::Flag::IS_MASTER);
team->save_config();
}
void TeamIndex::buy_reward(uint32_t team_id, const string& key, uint32_t points, Team::RewardFlag reward_flag) {
auto team = this->id_to_team.at(team_id);
if (team->spent_points + points > team->points) {
throw runtime_error("not enough points available");
}
team->reward_keys.emplace(key);
team->spent_points += points;
if (reward_flag != Team::RewardFlag::NONE) {
team->set_reward_flag(reward_flag);
}
team->save_config();
}
void TeamIndex::add_to_indexes(shared_ptr<Team> team) {
if (!this->id_to_team.emplace(team->team_id, team).second) {
throw runtime_error("team ID is already in use");
+39 -7
View File
@@ -47,6 +47,7 @@ public:
enum class RewardFlag {
// Only 0x00000001 and 0x00000002 are used by the client; the rest are
// free to be used however the server chooses.
NONE = 0x00000000,
TEAM_FLAG = 0x00000001,
DRESSING_ROOM = 0x00000002,
MEMBERS_20_LEADERS_3 = 0x00000004,
@@ -56,9 +57,12 @@ public:
};
uint32_t team_id = 0;
uint32_t points = 0;
uint32_t spent_points = 0;
std::string name;
std::unordered_map<uint32_t, Member> members;
uint32_t reward_flags = 0;
std::unordered_set<std::string> reward_keys;
std::shared_ptr<parray<le_uint16_t, 0x20 * 0x20>> flag_data;
Team() = default;
@@ -86,6 +90,8 @@ public:
this->reward_flags &= (~static_cast<uint8_t>(flag));
}
[[nodiscard]] bool has_reward(const std::string& key) const;
size_t num_members() const;
size_t num_leaders() const;
size_t max_members() const;
@@ -94,20 +100,45 @@ public:
bool can_promote_leader() const;
};
explicit TeamIndex(const std::string& directory);
struct Reward {
uint32_t menu_item_id = 0;
std::string key;
std::string name;
std::string description;
std::unordered_set<std::string> prerequisite_keys;
bool is_unique = true;
uint32_t team_points = 0;
Team::RewardFlag reward_flag = Team::RewardFlag::NONE;
ItemData reward_item;
Reward(uint32_t menu_item_id, const JSON& def_json);
};
TeamIndex(const std::string& directory, const JSON& reward_defs_json);
~TeamIndex() = default;
size_t count() const;
std::shared_ptr<Team> get_by_id(uint32_t team_id);
std::shared_ptr<Team> get_by_name(const std::string& name);
std::shared_ptr<Team> get_by_serial_number(uint32_t serial_number);
std::vector<std::shared_ptr<Team>> all();
inline const std::vector<Reward>& reward_definitions() const {
return this->reward_defs;
}
std::shared_ptr<Team> create(std::string& name, uint32_t master_serial_number, const std::string& master_name);
size_t count() const;
std::shared_ptr<const Team> get_by_id(uint32_t team_id) const;
std::shared_ptr<const Team> get_by_name(const std::string& name) const;
std::shared_ptr<const Team> get_by_serial_number(uint32_t serial_number) const;
std::vector<std::shared_ptr<const Team>> all() const;
std::shared_ptr<const Team> create(std::string& name, uint32_t master_serial_number, const std::string& master_name);
void disband(uint32_t team_id);
void add_member(uint32_t team_id, uint32_t serial_number, const std::string& name);
void remove_member(uint32_t serial_number);
void update_member_name(uint32_t serial_number, const std::string& name);
void add_member_points(uint32_t serial_number, uint32_t points);
void set_flag_data(uint32_t team_id, const parray<le_uint16_t, 0x20 * 0x20>& flag_data);
bool promote_leader(uint32_t master_serial_number, uint32_t leader_serial_number);
bool demote_leader(uint32_t master_serial_number, uint32_t leader_serial_number);
void change_master(uint32_t master_serial_number, uint32_t new_master_serial_number);
void buy_reward(uint32_t team_id, const std::string& key, uint32_t points, Team::RewardFlag reward_flag);
protected:
std::string directory;
@@ -115,6 +146,7 @@ protected:
std::unordered_map<uint32_t, std::shared_ptr<Team>> id_to_team;
std::unordered_map<std::string, std::shared_ptr<Team>> name_to_team;
std::unordered_map<uint32_t, std::shared_ptr<Team>> serial_number_to_team;
std::vector<Reward> reward_defs;
void add_to_indexes(std::shared_ptr<Team> team);
void remove_from_indexes(std::shared_ptr<Team> team);
+3 -3
View File
@@ -29,7 +29,7 @@ TextArchive::TextArchive(const JSON& json) {
}
for (const auto& keyboard_json : json.at("keyboards").as_list()) {
auto& keyboard = this->keyboards.emplace_back(new Keyboard());
auto& keyboard = this->keyboards.emplace_back(make_unique<Keyboard>());
for (size_t y = 0; y < keyboard->size(); y++) {
auto& row = keyboard->at(y);
const auto& row_json = keyboard_json->at(y);
@@ -116,7 +116,7 @@ void TextArchive::set_keyboard(size_t kb_index, const Keyboard& kb) {
if (kb_index >= this->keyboards.size()) {
this->keyboards.resize(kb_index + 1);
}
this->keyboards[kb_index].reset(new Keyboard(kb));
this->keyboards[kb_index] = make_unique<Keyboard>(kb);
}
void TextArchive::resize_keyboards(size_t num_keyboards) {
@@ -171,7 +171,7 @@ void TextArchive::load_t(const string& pr2_data) {
while (this->keyboards.size() < num_keyboards) {
uint32_t keyboard_offset = r.pget<U32T>(keyboards_offset + 4 * this->keyboards.size());
used_offsets.emplace(keyboard_offset);
auto& kb = this->keyboards.emplace_back(new Keyboard());
auto& kb = this->keyboards.emplace_back(make_unique<Keyboard>());
auto key_r = r.sub(keyboard_offset, sizeof(Keyboard));
for (size_t y = 0; y < kb->size(); y++) {
auto& row = kb->at(y);
+1 -1
View File
@@ -70,8 +70,8 @@ const char* proxy_port_name_for_version(Version v) {
case Version::DC_V1_11_2000_PROTOTYPE:
case Version::DC_V1:
case Version::DC_V2:
return "dc-proxy";
case Version::GC_NTE:
return "dc-proxy";
case Version::GC_V3:
case Version::GC_EP3_TRIAL_EDITION:
case Version::GC_EP3:
+3 -2
View File
@@ -84,8 +84,9 @@ inline bool uses_v2_encryption(Version version) {
}
inline bool uses_v3_encryption(Version version) {
return (version == Version::GC_V3) ||
(version == Version::XB_V3) ||
(version == Version::GC_EP3);
(version == Version::GC_EP3_TRIAL_EDITION) ||
(version == Version::GC_EP3) ||
(version == Version::XB_V3);
}
inline bool uses_v4_encryption(Version version) {
return (version == Version::BB_V4);
+159 -8
View File
@@ -128,24 +128,28 @@
"bb-data2": [12005, "bb", "login_server"],
},
// Where to listen for IP stack clients. This exists to interface with PSO GC
// clients running in a local Dolphin emulator. To enable local Dolphin
// clients to connect, set this to ["/tmp/dolphin-tap"] and configure Dolphin
// to use the tapserver type of broadband adapter. You do not need to install
// or run tapserver. See README.md for details on how to get PSO to connect
// via this interface. You can also add numbers or "address:port" strings to
// this list to listen for tapserver connections on a TCP port.
// Where to listen for IP and PPP stack clients. This exists to interface with
// PSO GC clients running in a local Dolphin emulator. To enable local Dolphin
// clients to connect, set IPStackListen to ["/tmp/dolphin-tap"] and configure
// Dolphin to use the tapserver type of broadband adapter. You do not need to
// install or run tapserver. See README.md for details on how to get PSO to
// connect via this interface. You can also add numbers or "address:port"
// strings to these lists to listen for tapserver connections on TCP ports.
// On Windows, Unix sockets are not available, so you can only use TCP sockets
// here. You can get Dolphin to connect locally by adding a port to this list
// and configuring Dolphin to connect to the same port. For example, you could
// set this to ["127.0.0.1:5059"], and configure Dolphin's tapserver BBA to
// connect to 127.0.0.1:5059.
"IPStackListen": [],
"PPPStackListen": [],
// Other servers to support proxying to. If this is empty for any game
// version, the proxy server is disabled for that version. Entries in these
// dictionaries should be of the form "name": "address:port"; the names are
// used in the proxy server menu.
// Note that PSO GameCube Episodes 1&2 Trial Edition uses the DC's
// ProxyDestinations dictionary here. This is because other servers that
// support that version treat it as PSO DC v2.
"ProxyDestinations-DC": {},
"ProxyDestinations-PC": {},
"ProxyDestinations-GC": {},
@@ -497,7 +501,8 @@
[0x02, "battle", "Battle", "$E$C6Battle mode rule\nsets"],
[0x04, "challenge-ep1", "Challenge (Episode 1)", "$E$C6Challenge mode\nquests in Episode 1"],
[0x84, "challenge-ep2", "Challenge (Episode 2)", "$E$C6Challenge mode\nquests in Episode 2"],
[0x08, "solo", "Solo", "$E$C6Quests that require\na single player"],
[0x08, "solo-story", "Story", "$E$C6Quests that follow\nthe Episode 1 story"],
[0x08, "solo-extra", "Solo", "$E$C6Quests that require\na single player"],
[0x10, "government-ep1", "Hero in Red", "$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline"],
[0x10, "government-ep2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"],
[0x10, "government-ep4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"],
@@ -559,6 +564,117 @@
// limitation, and must be at least 1.
"BBGlobalEXPMultiplier": 1,
// BB team reward definitions. Team rewards have the following fields:
// Key: Internal name of the reward. Must be unique across all rewards.
// Name: Reward name shown to the player.
// Description: Reward description shown to the player.
// Points: Cost in team points.
// PrerequisiteKeys: List of reward keys required to be purchased before
// this reward can be purchased.
// RewardFlag: Flag in the client's team rewards field. Not used for most
// rewards; only rewards that change client behavior need this.
// RewardItem: Item to be given to the team master when this reward is
// purchased. If the master's bank is full, item rewards do not appear in
// the purchase list.
// IsUnique: If false, the reward can be purchased multiple times (this only
// really makes sense for item rewards). If true or omitted, the reward
// can only be purchased once.
"TeamRewards": [
{
"Key": "TeamFlag",
"Name": "Team flag",
"Description": "Show a custom banner\nabove your team's\nplayers in the lobby",
"Points": 2500,
"RewardFlag": 0x00000001,
}, {
"Key": "DressingRoom",
"Name": "Dressing room",
"Description": "Unlock the ability to\nchange your character's\nappearance",
"Points": 3000,
"RewardFlag": 0x00000002,
}, {
"Key": "Members20Leaders3",
"Name": "20 team members",
"Description": "Increase your team's\nsize limit to 30 members\nand 3 leaders",
"Points": 1500,
"RewardFlag": 0x00000004,
}, {
"Key": "Members40Leaders5",
"Name": "40 team members",
"Description": "Increase your team's\nsize limit to 40 members\nand 5 leaders",
"Points": 4000,
"PrerequisiteKeys": ["Members20Leaders3"],
"RewardFlag": 0x00000008,
}, {
"Key": "Members70Leaders8",
"Name": "70 team members",
"Description": "Increase your team's\nsize limit to 70 members\nand 8 leaders",
"Points": 9000,
"PrerequisiteKeys": ["Members40Leaders5"],
"RewardFlag": 0x00000010,
}, {
"Key": "Members100Leaders10",
"Name": "100 team members",
"Description": "Increase your team's\nsize limit to 100 members\nand 10 leaders",
"Points": 18000,
"PrerequisiteKeys": ["Members70Leaders8"],
"RewardFlag": 0x00000020,
}, {
"Key": "PointOfDisasterQuest",
"Name": "Quest: Point of Disaster",
"Description": "Unlock the quest\nPoint of Disaster\nfor your team",
"Points": 1000,
}, {
"Key": "TheRobotsReckoningQuest",
"Name": "Quest: The Robots' Reckoning",
"Description": "Unlock the quest\nThe Robots' Reckoning\nfor your team",
"Points": 1000,
}, {
"Key": "CommanderBlade",
"Name": "Commander Blade",
"Description": "Create a Commander\nBlade weapon",
"IsUnique": false,
"Points": 8000,
"RewardItem": "00B200",
}, {
"Key": "UnionGuard",
"Name": "Union Guard",
"Description": "Create a Union Guard\nshield",
"IsUnique": false,
"Points": 100,
// TODO: There are 4 of these in names-v4.json; which should we use?
"RewardItem": "010295",
}, {
"Key": "Ticket500",
"Name": "Team Points Ticket 500",
"Description": "Create a 500-point ticket",
"IsUnique": false,
"Points": 500,
"RewardItem": "031900",
}, {
"Key": "Ticket1000",
"Name": "Team Points Ticket 1000",
"Description": "Create a 1000-point ticket",
"IsUnique": false,
"Points": 1000,
"RewardItem": "031901",
}, {
"Key": "Ticket5000",
"Name": "Team Points Ticket 5000",
"Description": "Create a 5000-point ticket",
"IsUnique": false,
"Points": 5000,
"RewardItem": "031902",
}, {
"Key": "Ticket10000",
"Name": "Team Points Ticket 10000",
"Description": "Create a 10000-point ticket",
"IsUnique": false,
"Points": 10000,
"RewardItem": "031903",
},
],
// Cheat mode behavior. There are three values:
// "Off": Cheat mode is disabled on the entire server. Cheat mode cannot be
// enabled in games, and the $cheat command does nothing. This also
@@ -609,6 +725,41 @@
"ItemDropMode": "OnByDefault",
"UseServerItemTables": "OffByDefault",
// Rare enemy rates for BB games. The default rates specified here match the
// original rates on the official servers. There is a hard limit of 16 rare
// enemies per room or quest, which you may run into if you increase these
// rates significantly.
// If no rates are specified for a difficulty, the previous difficulty's rates
// will be used. (In the default configuration, only Normal is specified, so
// all difficulties use the same rates.) If the Challenge set is not
// specified, the default rates are used for Challenge mode, which is 1/500
// for all enemies.
"RareEnemyRates-Normal": {
// These are probabilities out of 0xFFFFFFFF - so 0 means that rare enemy
// will never appear, and 0xFFFFFFFF means it will always appear (until 16
// rare enemies have been assigned).
"Hildeblue": 0x0083126E,
"Rappy": 0x0083126E,
"NarLily": 0x0083126E,
"PouillySlime": 0x0083126E,
"MerissaAA": 0x0083126E,
"Pazuzu": 0x0083126E,
"DorphonEclair": 0x0083126E,
"Kondrieu": 0x1999999A,
},
// "RareEnemyRates-Hard": {...},
// "RareEnemyRates-VeryHard": {...},
// "RareEnemyRates-Ultimate": {...},
// "RareEnemyRates-Challenge": {...},
// You can override the minimum character levels required to make BB games in
// each episode and difficulty level here.
"BBMinimumLevels": {
"Episode1": [1, 20, 50, 90],
"Episode2": [1, 30, 60, 100],
"Episode4": [1, 40, 70, 110],
},
// Whether to enable certain exception handling. Disabling this causes
// newserv to abort when any client causes an exception, which is generally
// only useful for debugging newserv itself. This setting should usually be
+1 -1
View File
@@ -1,5 +1,5 @@
{
// Rare table for PSO DCv1 and PC.
// Rare table for PSO DCv1.
// See rare-table-v4.json for a description of how this file works.
// This file is only used when client rare tables are overridden. By default,
// the client controls rare drops and this table is ignored.
File diff suppressed because it is too large Load Diff
+48 -21
View File
@@ -1,23 +1,50 @@
{
"battle_rules": {
"tech_disk_mode": "ALLOW",
"weapon_and_armor_mode": "ALLOW",
"mag_mode": "ALLOW",
"tool_mode": "ALLOW",
"trap_mode": "ALL_PLAYERS",
"respawn_mode": 0,
"replace_char": 0,
"drop_weapon": 1,
"is_teams": 1,
"hide_target_reticle": 1,
"death_level_up": 3,
"meseta_mode": "ALLOW",
"enable_sonar": 1,
"time_limit": 10,
"forbid_scape_dolls": 1,
"death_tech_level_up": 1,
"trap_counts": [5, 5, 5, 5],
"sonar_count": 5,
"box_drop_area": 10
}
// Each quest may have an optional JSON file (like this one) that defines
// server-side behaviors for the quest.
// For battle quests, the BattleRules field should be defined to match the
// rules that the quest defines internally. These are the rules for Battle 1.
"BattleRules": {
"TechDiskMode": "ALLOW",
"WeaponAndArmorMode": "ALLOW",
"MagMode": "ALLOW",
"ToolMode": "ALLOW",
"TrapMode": "ALL_PLAYERS",
"RespawnMode": 0,
"ReplaceChar": 0,
"DropWeapon": 1,
"IsTeams": 1,
"HideTargetReticle": 1,
"DeathLevelUp": 3,
"MesetaMode": "ALLOW",
"EnableSonar": 1,
"TimeLimit": 10,
"ForbidScapeDolls": 1,
"DeathTechLevelUp": 1,
"TrapCounts": [5, 5, 5, 5],
"SonarCount": 5,
"BoxDropArea": 10,
// These rules are used by other battles, but not by Battle 1:
// "Lives": 10,
// "MaxTechLevel": 15,
// "CharLevel": 1,
},
// Challenge quests should specify the ChallengeTemplateIndex field, which
// should match the template that the quest uses to replace player characters.
// "ChallengeTemplateIndex": 0,
// Quests may be set to be unavailable until a preceding quest has been
// cleared or a team reward has been purchased. To enable this feature, set a
// value for AvailableIf in the quest's JSON file. This field's value should
// be a boolean expression that tests one or more flags or team rewards. An
// example with random values is shown below. This field is ignored if the
// player has the DISABLE_QUEST_REQUIREMENTS flag in their license.
// "AvailableIf": "(F_016D || F_0171 || T_EpicCustomQuest) && !F_0173",
// On BB, quests may be disabled but still visible to the player. This
// expression controls when that should be the case. If AvailableIf evaluates
// to false, this is ignored. This field is also ignored if the player has
// the DISABLE_QUEST_REQUIREMENTS flag in their license.
// "EnabledIf": "!F_0169",
}
+22 -22
View File
@@ -1,25 +1,25 @@
{
"battle_rules": {
"tech_disk_mode": "LIMIT_LEVEL",
"weapon_and_armor_mode": "CLEAR_AND_ALLOW",
"mag_mode": "FORBID_ALL",
"tool_mode": "CLEAR_AND_ALLOW",
"trap_mode": "ALL_PLAYERS",
"respawn_mode": 0,
"replace_char": 1,
"drop_weapon": 1,
"is_teams": 0,
"hide_target_reticle": 1,
"death_level_up": 5,
"meseta_mode": "CLEAR_AND_ALLOW",
"enable_sonar": 1,
"max_tech_level": 0,
"char_level": 0,
"time_limit": 10,
"forbid_scape_dolls": 1,
"death_tech_level_up": 1,
"trap_counts": [0, 5, 5, 5],
"sonar_count": 5,
"box_drop_area": 1
"BattleRules": {
"TechDiskMode": "LIMIT_LEVEL",
"WeaponAndArmorMode": "CLEAR_AND_ALLOW",
"MagMode": "FORBID_ALL",
"ToolMode": "CLEAR_AND_ALLOW",
"TrapMode": "ALL_PLAYERS",
"RespawnMode": 0,
"ReplaceChar": 1,
"DropWeapon": 1,
"IsTeams": 0,
"HideTargetReticle": 1,
"DeathLevelUp": 5,
"MesetaMode": "CLEAR_AND_ALLOW",
"EnableSonar": 1,
"MaxTechLevel": 0,
"CharLevel": 0,
"TimeLimit": 10,
"ForbidScapeDolls": 1,
"DeathTechLevelUp": 1,
"TrapCounts": [0, 5, 5, 5],
"SonarCount": 5,
"BoxDropArea": 1
}
}
+22 -22
View File
@@ -1,25 +1,25 @@
{
"battle_rules": {
"tech_disk_mode": "LIMIT_LEVEL",
"weapon_and_armor_mode": "CLEAR_AND_ALLOW",
"mag_mode": "FORBID_ALL",
"tool_mode": "CLEAR_AND_ALLOW",
"trap_mode": "ALL_PLAYERS",
"respawn_mode": 2,
"replace_char": 1,
"drop_weapon": 0,
"is_teams": 0,
"hide_target_reticle": 1,
"death_level_up": 3,
"meseta_mode": "FORBID_ALL",
"enable_sonar": 0,
"lives": 10,
"max_tech_level": 0,
"char_level": 4,
"time_limit": 10,
"forbid_scape_dolls": 1,
"death_tech_level_up": 1,
"trap_counts": [0, 10, 10, 10],
"box_drop_area": 3
"BattleRules": {
"TechDiskMode": "LIMIT_LEVEL",
"WeaponAndArmorMode": "CLEAR_AND_ALLOW",
"MagMode": "FORBID_ALL",
"ToolMode": "CLEAR_AND_ALLOW",
"TrapMode": "ALL_PLAYERS",
"RespawnMode": 2,
"ReplaceChar": 1,
"DropWeapon": 0,
"IsTeams": 0,
"HideTargetReticle": 1,
"DeathLevelUp": 3,
"MesetaMode": "FORBID_ALL",
"EnableSonar": 0,
"Lives": 10,
"MaxTechLevel": 0,
"CharLevel": 4,
"TimeLimit": 10,
"ForbidScapeDolls": 1,
"DeathTechLevelUp": 1,
"TrapCounts": [0, 10, 10, 10],
"BoxDropArea": 3
}
}
+23 -23
View File
@@ -1,26 +1,26 @@
{
"battle_rules": {
"tech_disk_mode": "LIMIT_LEVEL",
"weapon_and_armor_mode": "CLEAR_AND_ALLOW",
"mag_mode": "FORBID_ALL",
"tool_mode": "CLEAR_AND_ALLOW",
"trap_mode": "ALL_PLAYERS",
"respawn_mode": 2,
"replace_char": 1,
"drop_weapon": 1,
"is_teams": 0,
"hide_target_reticle": 1,
"death_level_up": 5,
"meseta_mode": "CLEAR_AND_ALLOW",
"enable_sonar": 1,
"lives": 10,
"max_tech_level": 1,
"char_level": 1,
"time_limit": 10,
"forbid_scape_dolls": 1,
"death_tech_level_up": 1,
"trap_counts": [5, 5, 5, 5],
"sonar_count": 5,
"box_drop_area": 1
"BattleRules": {
"TechDiskMode": "LIMIT_LEVEL",
"WeaponAndArmorMode": "CLEAR_AND_ALLOW",
"MagMode": "FORBID_ALL",
"ToolMode": "CLEAR_AND_ALLOW",
"TrapMode": "ALL_PLAYERS",
"RespawnMode": 2,
"ReplaceChar": 1,
"DropWeapon": 1,
"IsTeams": 0,
"HideTargetReticle": 1,
"DeathLevelUp": 5,
"MesetaMode": "CLEAR_AND_ALLOW",
"EnableSonar": 1,
"Lives": 10,
"MaxTechLevel": 1,
"CharLevel": 1,
"TimeLimit": 10,
"ForbidScapeDolls": 1,
"DeathTechLevelUp": 1,
"TrapCounts": [5, 5, 5, 5],
"SonarCount": 5,
"BoxDropArea": 1
}
}
+20 -20
View File
@@ -1,23 +1,23 @@
{
"battle_rules": {
"tech_disk_mode": "ALLOW",
"weapon_and_armor_mode": "ALLOW",
"mag_mode": "FORBID_ALL",
"tool_mode": "ALLOW",
"trap_mode": "ALL_PLAYERS",
"respawn_mode": 1,
"replace_char": 0,
"drop_weapon": 1,
"is_teams": 1,
"hide_target_reticle": 1,
"death_level_up": 5,
"meseta_mode": "CLEAR_AND_ALLOW",
"enable_sonar": 1,
"time_limit": 10,
"forbid_scape_dolls": 1,
"death_tech_level_up": 1,
"trap_counts": [5, 5, 5, 5],
"sonar_count": 5,
"box_drop_area": 10
"BattleRules": {
"TechDiskMode": "ALLOW",
"WeaponAndArmorMode": "ALLOW",
"MagMode": "FORBID_ALL",
"ToolMode": "ALLOW",
"TrapMode": "ALL_PLAYERS",
"RespawnMode": 1,
"ReplaceChar": 0,
"DropWeapon": 1,
"IsTeams": 1,
"HideTargetReticle": 1,
"DeathLevelUp": 5,
"MesetaMode": "CLEAR_AND_ALLOW",
"EnableSonar": 1,
"TimeLimit": 10,
"ForbidScapeDolls": 1,
"DeathTechLevelUp": 1,
"TrapCounts": [5, 5, 5, 5],
"SonarCount": 5,
"BoxDropArea": 10
}
}
+22 -22
View File
@@ -1,25 +1,25 @@
{
"battle_rules": {
"tech_disk_mode": "LIMIT_LEVEL",
"weapon_and_armor_mode": "CLEAR_AND_ALLOW",
"mag_mode": "FORBID_ALL",
"tool_mode": "CLEAR_AND_ALLOW",
"trap_mode": "ALL_PLAYERS",
"respawn_mode": 2,
"replace_char": 1,
"drop_weapon": 1,
"is_teams": 1,
"hide_target_reticle": 1,
"death_level_up": 3,
"meseta_mode": "CLEAR_AND_ALLOW",
"enable_sonar": 0,
"lives": 10,
"max_tech_level": 4,
"char_level": 19,
"time_limit": 10,
"forbid_scape_dolls": 1,
"death_tech_level_up": 1,
"trap_counts": [5, 5, 0, 0],
"box_drop_area": 6
"BattleRules": {
"TechDiskMode": "LIMIT_LEVEL",
"WeaponAndArmorMode": "CLEAR_AND_ALLOW",
"MagMode": "FORBID_ALL",
"ToolMode": "CLEAR_AND_ALLOW",
"TrapMode": "ALL_PLAYERS",
"RespawnMode": 2,
"ReplaceChar": 1,
"DropWeapon": 1,
"IsTeams": 1,
"HideTargetReticle": 1,
"DeathLevelUp": 3,
"MesetaMode": "CLEAR_AND_ALLOW",
"EnableSonar": 0,
"Lives": 10,
"MaxTechLevel": 4,
"CharLevel": 19,
"TimeLimit": 10,
"ForbidScapeDolls": 1,
"DeathTechLevelUp": 1,
"TrapCounts": [5, 5, 0, 0],
"BoxDropArea": 6
}
}
+23 -23
View File
@@ -1,26 +1,26 @@
{
"battle_rules": {
"tech_disk_mode": "LIMIT_LEVEL",
"weapon_and_armor_mode": "CLEAR_AND_ALLOW",
"mag_mode": "FORBID_ALL",
"tool_mode": "CLEAR_AND_ALLOW",
"trap_mode": "ALL_PLAYERS",
"respawn_mode": 2,
"replace_char": 1,
"drop_weapon": 1,
"is_teams": 0,
"hide_target_reticle": 1,
"death_level_up": 1,
"meseta_mode": "CLEAR_AND_ALLOW",
"enable_sonar": 1,
"lives": 15,
"max_tech_level": 0,
"char_level": 0,
"time_limit": 10,
"forbid_scape_dolls": 0,
"death_tech_level_up": 0,
"trap_counts": [0, 0, 1, 0],
"sonar_count": 10,
"box_drop_area": 2
"BattleRules": {
"TechDiskMode": "LIMIT_LEVEL",
"WeaponAndArmorMode": "CLEAR_AND_ALLOW",
"MagMode": "FORBID_ALL",
"ToolMode": "CLEAR_AND_ALLOW",
"TrapMode": "ALL_PLAYERS",
"RespawnMode": 2,
"ReplaceChar": 1,
"DropWeapon": 1,
"IsTeams": 0,
"HideTargetReticle": 1,
"DeathLevelUp": 1,
"MesetaMode": "CLEAR_AND_ALLOW",
"EnableSonar": 1,
"Lives": 15,
"MaxTechLevel": 0,
"CharLevel": 0,
"TimeLimit": 10,
"ForbidScapeDolls": 0,
"DeathTechLevelUp": 0,
"TrapCounts": [0, 0, 1, 0],
"SonarCount": 10,
"BoxDropArea": 2
}
}
+23 -23
View File
@@ -1,26 +1,26 @@
{
"battle_rules": {
"tech_disk_mode": "LIMIT_LEVEL",
"weapon_and_armor_mode": "CLEAR_AND_ALLOW",
"mag_mode": "FORBID_ALL",
"tool_mode": "CLEAR_AND_ALLOW",
"trap_mode": "ALL_PLAYERS",
"respawn_mode": 2,
"replace_char": 1,
"drop_weapon": 0,
"is_teams": 0,
"hide_target_reticle": 1,
"death_level_up": 5,
"meseta_mode": "FORBID_ALL",
"enable_sonar": 1,
"lives": 10,
"max_tech_level": 0,
"char_level": 19,
"time_limit": 10,
"forbid_scape_dolls": 1,
"death_tech_level_up": 0,
"trap_counts": [0, 10, 10, 10],
"sonar_count": 10,
"box_drop_area": 1
"BattleRules": {
"TechDiskMode": "LIMIT_LEVEL",
"WeaponAndArmorMode": "CLEAR_AND_ALLOW",
"MagMode": "FORBID_ALL",
"ToolMode": "CLEAR_AND_ALLOW",
"TrapMode": "ALL_PLAYERS",
"RespawnMode": 2,
"ReplaceChar": 1,
"DropWeapon": 0,
"IsTeams": 0,
"HideTargetReticle": 1,
"DeathLevelUp": 5,
"MesetaMode": "FORBID_ALL",
"EnableSonar": 1,
"Lives": 10,
"MaxTechLevel": 0,
"CharLevel": 19,
"TimeLimit": 10,
"ForbidScapeDolls": 1,
"DeathTechLevelUp": 0,
"TrapCounts": [0, 10, 10, 10],
"SonarCount": 10,
"BoxDropArea": 1
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"challenge_template_index": 0
"ChallengeTemplateIndex": 0
}

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