Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d08aaef0f8 | |||
| 245df782b9 | |||
| 9ffe429a1f | |||
| 673c767a42 | |||
| de42135532 | |||
| 79bf6b3fa9 | |||
| 741456d1da | |||
| c95b158e4e | |||
| d40c260d18 | |||
| 454e0e558b | |||
| 5ea49425c7 | |||
| 08ea9403e9 | |||
| f01882db39 | |||
| 1870273f89 | |||
| d6edf1b24d | |||
| 8ecbe6798d | |||
| 587ad1933d | |||
| 70548aef04 | |||
| 43663cbe79 | |||
| 5f2e7e543b | |||
| c98d1081a3 | |||
| 0b2272bfa7 | |||
| 04982d919c | |||
| 34751f99e9 | |||
| 40d5c6ee64 | |||
| be0b70f903 | |||
| 76aeacfdfd | |||
| dec979fb52 | |||
| 1c85d46436 | |||
| f05dc6d9f9 | |||
| e141642dd6 | |||
| af4d3a3325 | |||
| 91131f8b36 | |||
| b2ea059fd8 | |||
| 150acda1ea | |||
| 3e1449bb80 | |||
| 4c104443bc | |||
| de8a210d0f | |||
| 9d2b36b787 | |||
| 03b78c3825 | |||
| 3c8674dcc7 | |||
| 95919b8b01 | |||
| 1712b13106 | |||
| 50a32429be | |||
| 6f0124f7ec | |||
| acbebaeb70 | |||
| d44b0b3d62 | |||
| 4a3b0118a8 | |||
| 7c7df39e6d | |||
| dba49be1e3 | |||
| 33483bbfbf | |||
| 9630b06284 | |||
| e6acea8247 | |||
| 2cd4c733ef | |||
| 05e5705537 | |||
| 24e48b1abd | |||
| 6d73cae91b | |||
| dd9bc51457 | |||
| dce0f91678 | |||
| eb5701ece9 | |||
| 6f99b3b1c8 | |||
| da9765f1aa | |||
| b7897cddf2 | |||
| ce2300b116 | |||
| cb05dce764 | |||
| a762c0f8f8 | |||
| cd008ab0ba | |||
| 53b36d7074 | |||
| 5a1880bd65 | |||
| 8e280a1464 | |||
| 0bcdd9997e | |||
| d5351c4580 | |||
| 76bc2385ca | |||
| 325f7c6efc | |||
| 93d97d3e5b | |||
| 66b64603a0 | |||
| 7405eaea0b | |||
| 477e433361 | |||
| 7ca2012bc4 | |||
| dace165ef2 | |||
| f6df2b5b45 | |||
| 1a310df17e | |||
| 31edec701b | |||
| dc36d2ae8d | |||
| 4e733b0dc6 | |||
| 6eadaaca66 | |||
| d778340999 |
+3
-2
@@ -58,6 +58,7 @@ add_custom_target(
|
||||
|
||||
set(SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Revision.cc
|
||||
src/Account.cc
|
||||
src/AFSArchive.cc
|
||||
src/BattleParamsIndex.cc
|
||||
src/BMLArchive.cc
|
||||
@@ -91,13 +92,13 @@ set(SOURCES
|
||||
src/HTTPServer.cc
|
||||
src/IPFrameInfo.cc
|
||||
src/IPStackSimulator.cc
|
||||
src/IPV4RangeSet.cc
|
||||
src/ItemCreator.cc
|
||||
src/ItemData.cc
|
||||
src/ItemNameIndex.cc
|
||||
src/ItemParameterTable.cc
|
||||
src/Items.cc
|
||||
src/LevelTable.cc
|
||||
src/License.cc
|
||||
src/Lobby.cc
|
||||
src/Loggers.cc
|
||||
src/Main.cc
|
||||
@@ -114,7 +115,7 @@ set(SOURCES
|
||||
src/PSOGCObjectGraph.cc
|
||||
src/PSOProtocol.cc
|
||||
src/Quest.cc
|
||||
src/QuestAvailabilityExpression.cc
|
||||
src/IntegralExpression.cc
|
||||
src/QuestScript.cc
|
||||
src/RareItemSet.cc
|
||||
src/ReceiveCommands.cc
|
||||
|
||||
@@ -18,6 +18,7 @@ See TODO.md for a list of known issues and future work I've curated, or go to th
|
||||
* [Client patch directories for PC and BB](#client-patch-directories)
|
||||
* [How to connect](#how-to-connect)
|
||||
* Features and configuration
|
||||
* [User accounts](#user-accounts)
|
||||
* [Installing quests](#installing-quests)
|
||||
* [Item tables and drop modes](#item-tables-and-drop-modes)
|
||||
* [Cross-version play](#cross-version-play)
|
||||
@@ -131,7 +132,10 @@ newserv implements a patch server for PSO PC and PSO BB game data. Any file or d
|
||||
For Blue Burst set up, the below is mandatory for a smooth experience:
|
||||
|
||||
1. Browse to your chosen client's data directory.
|
||||
2. Copy all the map_*.dat files and the data.gsl file and place them in `system/patch-bb/data`
|
||||
2. Copy all the map_*.dat files, unitxt_* files and the data.gsl file and place them in `system/patch-bb/data`.
|
||||
3. If you're using game files from the Tethealla client, make a copy of unitxt_j.prs inside system/patch-bb/data and name it unitxt_e.prs. (If unitxt_e.prs already exists, replace it with the copied file.)
|
||||
|
||||
If you do not have a BB client, or using a Tethealla client from another source, Tethealla clients that are compatible with newserv can be found here: [English](https://web.archive.org/web/20240402011115/https://ragol.org/files/bb/TethVer12513_English.zip) / [Japanese](https://web.archive.org/web/20240402013127/https://ragol.org/files/bb/TethVer12513_Japanese.zip). These clients connect to 127.0.0.1 (localhost) automatically.
|
||||
|
||||
For BB clients, newserv reads some files out of the patch data to implement game logic, so it's important that certain game files are synchronized between the server and the client. newserv contains defaults for these files in the system/maps/bb-v4 directory, but if these don't match the client's copies of the files, odd behavior will occur in games.
|
||||
|
||||
@@ -210,9 +214,9 @@ If you're using a version of Dolphin with tapserver support, you can make it con
|
||||
|
||||
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.
|
||||
The original Japanese and US versions of PSO BB should work (the last Japanese release can be found [here](https://archive.org/details/psobb_jp_setup_12511_20240109/)), but you'll have to modify your hosts file or edit psobb.exe to point to your newserv instance. The original versions are packed with various versions of ASProtect, 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.
|
||||
Alternatively, you can use the Tethealla client ([English](https://web.archive.org/web/20240402011115/https://ragol.org/files/bb/TethVer12513_English.zip) / [Japanese](https://web.archive.org/web/20240402013127/https://ragol.org/files/bb/TethVer12513_Japanese.zip)); 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. If the server is on the same PC as the client and you don't plan to have any external players, these Tethealla clients will automatically connect to the server without any modifications.
|
||||
|
||||
### Connecting external clients
|
||||
|
||||
@@ -222,6 +226,23 @@ For GC clients, you'll have to use newserv's built-in DNS server or set up your
|
||||
|
||||
# Server feature configuration
|
||||
|
||||
## User accounts
|
||||
|
||||
By default, newserv does not require users to pre-register before playing; the server will instead automatically create an account the first time each player connects. These accounts have no special permissions. You can view, create, edit, and delete user accounts in the server's shell (run `help` in the shell to see how to do this).
|
||||
|
||||
A license is a set of credentials that a player can use to log in. There are six types of licenses:
|
||||
* *DC NTE licenses* consist of a 16-character serial number and 16-character access key.
|
||||
* *DC licenses* consist of an 8-character hex serial number and an 8-character access key.
|
||||
* *PC licenses* are the same format as DC licenses, but are used for PC v2.
|
||||
* *GC licenses* consist of a 10-digit decimal serial number, a 12-character access key, and a password of up to 8 characters.
|
||||
* *XB licenses* consist of a gamertag of up to 16 characters, a 16-character hex user ID, and a 16-character hex account ID.
|
||||
* *BB licenses* consist of a username of up to 16 characters and a password of up to 16 characters.
|
||||
Each account may have multiple licenses. To add a license to an account, use `add-license` in the shell.
|
||||
|
||||
On BB, character data is scoped to the license, but system and Guild Card data is scoped to the account. That is, an account with multiple BB licenses can have more than 4 characters (up to 4 per license), but they will all share the same team membership and Guild Card lists.
|
||||
|
||||
You may want to give your account elevated privileges. To do so, run `update-account ACCOUNT-ID flags=root` (replacing ACCOUNT-ID with your actual account-id). You can also use update-account to edit other parts of the account; see the help text for more information.
|
||||
|
||||
## Installing quests
|
||||
|
||||
newserv automatically finds quests in the subdirectories of the system/quests/ directory. To install your own quests, or to use quests you've saved using the proxy's Save Files option, just put them in one of the subdirectories there and name them appropriately. The subdirectories and their behaviors (e.g. in which game modes they should appear and for which PSO versions) is defined in the QuestCategories field in config.json.
|
||||
@@ -344,23 +365,52 @@ Like quests, Episode 3 card definitions, maps, and quests are cached in memory.
|
||||
|
||||
## Memory patches, client functions, and DOL files
|
||||
|
||||
Everything in this section requires resource_dasm to be installed, so newserv can use the assemblers and disassemblers from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available.
|
||||
*Everything in this section requires resource_dasm to be installed, so newserv can use the assemblers and disassemblers from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available.*
|
||||
|
||||
In addition, these features are only supported for the following game versions:
|
||||
* PSO GameCube Episodes 1&2 Trial Edition
|
||||
* PSO GameCube Episodes 1&2 JP, USA, and EU but not Plus
|
||||
* PSO GameCube Episodes 1&2 Plus JP v1.4 but not v1.5
|
||||
* PSO GameCube Episode 3 Trial Edition
|
||||
* PSO GameCube Episode 3 JP
|
||||
* PSO GameCube Episode 3 USA (experimental; must be manually enabled in config.json)
|
||||
* PSO Xbox (all versions)
|
||||
* PSO BB
|
||||
You can put assembly files in the system/client-functions directory with filenames like PatchName.VERS.patch.s and they will appear in the Patches menu for clients that support client functions. Client functions are written in SH-4, PowerPC, or x86 assembly and are compiled when newserv is started. The assembly system's features are documented in the comments in system/client-functions/System/WriteMemory.ppc.s.
|
||||
|
||||
The VERS token in client function filenames refers to the specific version of the game that the client function applies to. Some versions do not support receiving client functions at all. The specific versions are:
|
||||
|
||||
| Game | VERS | Supported |
|
||||
|-------------------|------|-----------|
|
||||
| PSO DC NTE | 1OJ1 | No |
|
||||
| PSO DC 11/2000 | 1OJ2 | No |
|
||||
| PSO DC 12/2000 | 1OJ3 | No |
|
||||
| PSO DC 01/2001 | 1OJ4 | No |
|
||||
| PSO DC v1 JP | 1OJF | No |
|
||||
| PSO DC v1 US | 1OEF | No |
|
||||
| PSO DC v1 EU | 1OPF | No |
|
||||
| PSO DC 08/2001 | 2OJ5 | Yes |
|
||||
| PSO DC v2 JP | 2OJF | Yes |
|
||||
| PSO DC v2 US | 2OEF | Yes |
|
||||
| PSO DC v2 EU | 2OPF | Yes |
|
||||
| PSO PC (v2) | 2OJW | No |
|
||||
| PSO GC NTE | 3OJT | Yes |
|
||||
| PSO GC v1.2 JP | 3OJ2 | Yes |
|
||||
| PSO GC v1.3 JP | 3OJ3 | Yes |
|
||||
| PSO GC v1.4 JP | 3OJ4 | Yes |
|
||||
| PSO GC v1.5 JP | 3OJ5 | No |
|
||||
| PSO GC v1.0 US | 3OE0 | Yes |
|
||||
| PSO GC v1.1 US | 3OE1 | Yes |
|
||||
| PSO GC v1.2 US | 3OE2 | No |
|
||||
| PSO GC v1.0 EU | 3OP0 | Yes |
|
||||
| PSO GC Ep3 NTE | 3SJT | Yes |
|
||||
| PSO GC Ep3 JP | 3SJ0 | Yes |
|
||||
| PSO GC Ep3 US | 3SE0 | No |
|
||||
| PSO GC Ep3 EU | 3SP0 | No |
|
||||
| PSO Xbox Beta | 4OJB | Yes |
|
||||
| PSO Xbox JP Disc | 4OJD | Yes |
|
||||
| PSO Xbox JP TU | 4OJU | Yes |
|
||||
| PSO Xbox US Disc | 4OED | Yes |
|
||||
| PSO Xbox US TU | 4OEU | Yes |
|
||||
| PSO Xbox EU Disc | 4OPD | Yes |
|
||||
| PSO Xbox EU TU | 4OPU | Yes |
|
||||
| PSO BB JP 1.25.13 | 51OC | Yes |
|
||||
| PSO BB Tethealla | 51OC | Yes |
|
||||
|
||||
*Note: newserv uses the shorter GameCube versioning convention, where discs labeled DOL-XXXX-0-0Y are version 1.Y. The PSO community seems to use the convention 1.0Y in some places instead, but these are the same version. For example, the version that newserv calls v1.4 is the same as v1.04, and is labeled DOL-GPOJ-0-04 on the underside of the disc.*
|
||||
|
||||
You can put memory patches in the system/client-functions directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC, XB, and BB clients that support patching. Memory patches are written in PowerPC or x86 assembly and are compiled when newserv is started. The assembly system's features are documented in the comments in system/client-functions/WriteMemory.ppc.s.
|
||||
|
||||
newserv comes with a set of patches for GC Episodes 1&2 based on AR codes originally made by Ralf at GC-Forever and Aleron Ives. Many of them were originally posted in [this thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050).
|
||||
newserv comes with a set of patches for some of the above versions, based on AR codes originally made by Ralf at GC-Forever and Aleron Ives. Many of them were originally posted in [this thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050).
|
||||
|
||||
You can also put DOL files in the system/dol directory, and they will appear in the Programs menu for GC clients. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWord.ppc.s, WriteMemory.ppc.s, and RunDOL.ppc.s must be present in the system/client-functions directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
|
||||
|
||||
@@ -423,7 +473,7 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$where` (game server only): Shows your current floor number and coordinates. Mainly useful for debugging.
|
||||
|
||||
* Debugging commands
|
||||
* `$debug` (game server only): Enable or disable debug. You need the DEBUG permission in your user license to use this command. Enabling debug does a few things:
|
||||
* `$debug` (game server only): Enable or disable debug. You need the DEBUG flag in your user account to use this command. Enabling debug does a few things:
|
||||
* You'll see in-game messages from the server when you take certain actions, like killing an enemy in BB.
|
||||
* You'll see the rare seed value and floor variations when you join a game.
|
||||
* You'll be placed into the highest available slot in lobbies and games instead of the lowest, unless you're joining a BB solo-mode game.
|
||||
@@ -437,9 +487,12 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$qgwrite <flag-num> <value>` (game server only): Set the value of a quest counter ("global flag") for yourself.
|
||||
* `$qsync <reg-num> <value>`: Set a quest register's value for yourself only. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
|
||||
* `$qsyncall <reg-num> <value>`: Set a quest register's value for everyone in the game. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
|
||||
* `$swset [floor] <flag-num>` and `$swclear [floor] <flag-num>`: Set or clear a switch flag. If floor is not given, sets or clears the flag on your current floor.
|
||||
* `$swsetall`: Sets all switch flags on your current floor. This unlocks all doors, disables all laser fences, triggers all light/poison switches, etc.
|
||||
* `$gc` (game server only): Send your own Guild Card to yourself.
|
||||
* `$sc <data>`: Send a command to yourself.
|
||||
* `$ss <data>` (proxy server only): Send a command to the remote server.
|
||||
* `$ss <data>`: Send a command to the remote server (if in a proxy session) or to the game server.
|
||||
* `$sb <data>`: Send a command to yourself, and to the remote server or game server.
|
||||
* `$meseta <amount>` (game server only; Episode 3 only): Add the given amount to your Meseta total.
|
||||
* `$auction` (Episode 3 only): Bring up the CARD Auction menu, regardless of how many players are in the game or if you have a VIP card.
|
||||
* `$ep3battledebug` (game server only; Episode 3 only): Enable or disable TCard00_Select. If enabled, the game will enter the debug menu when you start a battle.
|
||||
@@ -457,7 +510,7 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$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), this command does nothing. If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby. If cheats are allowed on the server, `<stat>` can be any of `atp`, `mst`, `evp`, `hp`, `dfp`, `ata`, `lck`, `meseta`, `exp`, `level`, `namecolor`, `secid`, `name`, `npc`, or `tech`. If cheats are not allowed, only `namecolor`, `name`, and `npc` can be used.
|
||||
* `$edit <stat> <value>`: Modifies your character data. If you are on V3 (GameCube/Xbox), this command does nothing. If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby. If cheats are allowed on the server, `<stat>` can be any of `atp`, `mst`, `evp`, `hp`, `dfp`, `ata`, `lck`, `meseta`, `exp`, `level`, `namecolor`, `secid`, `name`, `language`, `npc`, or `tech`. If cheats are not allowed, only `namecolor`, `name`, `language`, and `npc` can be used. Changing your character's language is only useful on BB; to do so, use a single-character language code (e.g. to switch your character to English, use `$edit language E`; for Japanese, use `$edit language J`).
|
||||
|
||||
* Blue Burst player commands (game server only)
|
||||
* `$bank [number]`: Switches your current bank, so you can access your other character's banks (if `number` is 1-4) or your shared account bank (if `number` is 0). If `number` is not given, switches back to your current character's bank.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
## Episode 3
|
||||
|
||||
- Enforce tournament deck restrictions (e.g. rank checks, No Assist option) when populating COMs at tournament start time
|
||||
- Make `reload licenses` not vulnerable to online players' licenses overwriting licenses on disk somehow
|
||||
- Make `reload accounts` not vulnerable to online players' accounts overwriting accounts on disk somehow
|
||||
- Implement ranks (based on total Meseta earned)
|
||||
- Make an AR code that gets rid of the SAMPLE overlays on NTE
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
- Fix receiving Guild Cards from non-Xbox players
|
||||
- Research the F94D quest opcode
|
||||
- Finish porting the remaining GC patches
|
||||
|
||||
## PSOBB
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
struct AITalkBin {
|
||||
be_uint32_t num_scs;
|
||||
be_uint32_t sc_offsets[num_scs];
|
||||
|
||||
struct SCDialogueEntry {
|
||||
be_uint32_t num_entries;
|
||||
be_uint32_t unknown_a1;
|
||||
be_uint32_t size; // in bytes
|
||||
struct WhenEntry {
|
||||
be_uint32_t when;
|
||||
be_uint32_t percent_chance; // 0-100
|
||||
be_uint32_t count;
|
||||
be_uint32_t string_ids[count];
|
||||
} __attribute__((packed));
|
||||
} __attribute__((packed));
|
||||
} __attribute__((packed));
|
||||
-2785
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
star value tables
|
||||
|
||||
psobb [B1-437]
|
||||
00010203 04090909 01020304 05090909 01020304 05090909 01020304 05090909 01020304 05090909 00010203 04090909 01020304 05090909 01020304 05090909 01020304 05090909 00010203 09090901 02030409 09090102 03040909 09090A0A 090A0A09 0A0A090C 0B0A0A0A 0A0A0A0B 0A090A0A 0A0A0A09 0A0A0A0A 0A0A0A0A 0A0A0B0A 0C0C0B0A 0A090A09 090A0A0A 0A0C090C 0B0A090A 090C0A0B 0A0A0A0A 0A0A0A0B 0B0A0A0A 09090A09 0C0A0A0A 0B0A0B09 0A0A090A 0A0B090B 0A0B0B0A 090A090A 0B090A0A 0A0A0A0A 0A0A0A09 090C0C0C 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0009 0A0A0A0B 090A0A09 0A0A0B0A 0A0A0A0A 0A0A0A0A 0A0A0A0A 0A0A0A0A 0A0A0A0A 0B0B0B0B 0B0B0B0B 0B0B0B0A 0C0A0C0B 0A0A0A0A 0A0B0A0B 0B0B0B0B 0A0A090A 0A0A090B 0B0B0B0C 0C0C0C0C 0A0A0C0A 090A0C09 0A0B0A0A 0A0A0C0A 0A0A0A09 0A0C0A09 0A0A0A0A 0A090C0B 09090909 09090909 09090909 09090909 0909090B 0A0C0A0B 0B0C0A0A 0A090A0A 0A0A0B0A 0A0A0A0A 0909090A 0A090C0A 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0102 03040102 03040203 04020304 01020304 01020304 01020304 01020304 01020304 01020304 03040000 00010102 02030304 04050506 06070707 07080808 08080809 09090A0A 0A0A0A0A 0B0C0A0A 0A0A0A0A 0B0B0B0A 0B0B0C0B 0B0B0B0B 0A0A0A0A 0A0A0C09 0909090A 0A0B0C09 0B0A0A0A 0A0A0A0A 0A0A0A0B 0A0A0A0A 0A0A0A00 00010203 03040405 05050606 07070808 08080808 0A0A0A0A 0A0A0909 090A0A0A 0A0A0A0A 0A0A0B0A 0A0B0A09 0909090A 0B0B0000 0B000000 00080808 08080808 09080808 09080808 09070707 07070909 090C0909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 0A0A0B0A 0B0A0909 0B0B0B0C 0A0A0A09 0A0A0A0A 090A0A0A 0A0A0A0A 0A0A0A0A 0203050B 0203050B 0203050B 0203050B 0204060B 0204060B 0203050B 080B080A 0B020305 02030502 03050304 06030405 07080B04 06090406 09040609 06090B06 090B0909 09090909 0A0B0B0B 0B0B0B0B 0B0B0B0B 0B0B0B0B 0B0B0B0B 0A0B0B0B 0B0B0B0B
|
||||
|
||||
psogc [94-2F7]
|
||||
00010203 04090909 01020304 05090909 01020304 05090909 01020304 05090909 01020304 05090909 00010203 04090909 01020304 05090909 01020304 05090909 01020304 05090909 00010203 09090901 02030409 09090102 03040909 09090A0A 090A0A09 0A0A090C 0B0A0A0A 0A0A0A0B 0A090A0A 0A0A0A09 0A0A0A0A 0A0A0A0A 0A0A0B0A 0C0C0B0A 0A090A09 090A0A0A 0A0C090C 0B0A090A 090C0A0B 0A0A0A0A 0A0A0A0B 0B0A0A0A 09090A09 0C0A0A0A 0B0A0B09 0A0A090A 0A0B090B 0A0B0B0A 090A090A 0B090A0A 0A0A0A0A 0A0A0A09 090C0C0C 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0C0C 0C0C0009 0A0A0A0B 090A0A09 0A0A0B0A 0A0A0A0A 0A0A0A0A 0A0A0A0A 0A0A0A0A 0A0A0A0A 0B0B0B0B 0B0B0B0B 0B0B0B0A 0C0A0C0B 0A0A0A0A 0A0B0A0B 0B0B0B0B 0A0A090A 0A0A090B 0B0B0B0C 0C0C0C0C 01020304 01020304 02030402 03040102 03040102 03040102 03040102 03040102 03040102 03040304 00000001 01020203 03040405 05060607 07070708 08080808 08090909 0A0A0A0A 0A0A0B0C 0A0A0A0A 0A0A0B0B 0B0A0B0B 0C0B0B0B 0B0B0000 01020303 04040505 05060607 07080808 0808080A 0A0A0A0A 0A090909 0A0A0A0A 0A0A0A0A 0A0B0A0A 0B0A0909 09090A0B 0B000000 00000000 08080808 08080809 08080809 08080809 07070707 07090909 0C090909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 09090909 09090902 03050B02 03050B02 03050B02 03050B02 04060B02 04060B02 03050B08 0B080A0B 02030502 03050203 05030406 03040507 080B0406 09040609 04060906 090B0609 0B090909 090909
|
||||
|
||||
|
||||
|
||||
0203050B0203050B0203050B0203050B
|
||||
+4
-2
@@ -7,6 +7,8 @@
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
#include "Text.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
AFSArchive::AFSArchive(shared_ptr<const string> data)
|
||||
@@ -14,12 +16,12 @@ AFSArchive::AFSArchive(shared_ptr<const string> data)
|
||||
struct FileHeader {
|
||||
be_uint32_t magic;
|
||||
le_uint32_t num_files;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(FileHeader, 8);
|
||||
|
||||
struct FileEntry {
|
||||
le_uint32_t offset;
|
||||
le_uint32_t size;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(FileEntry, 8);
|
||||
|
||||
StringReader r(*this->data);
|
||||
const auto& header = r.get<FileHeader>();
|
||||
|
||||
+1003
File diff suppressed because it is too large
Load Diff
+263
@@ -0,0 +1,263 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <phosg/JSON.hh>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Text.hh"
|
||||
|
||||
class LicenseIndex;
|
||||
|
||||
struct DCNTELicense {
|
||||
std::string serial_number;
|
||||
std::string access_key;
|
||||
|
||||
static std::shared_ptr<DCNTELicense> from_json(const JSON& json);
|
||||
JSON json() const;
|
||||
};
|
||||
|
||||
struct V1V2License {
|
||||
uint32_t serial_number = 0;
|
||||
std::string access_key;
|
||||
|
||||
static std::shared_ptr<V1V2License> from_json(const JSON& json);
|
||||
JSON json() const;
|
||||
};
|
||||
|
||||
struct GCLicense {
|
||||
uint32_t serial_number = 0;
|
||||
std::string access_key;
|
||||
std::string password;
|
||||
|
||||
static std::shared_ptr<GCLicense> from_json(const JSON& json);
|
||||
JSON json() const;
|
||||
};
|
||||
|
||||
struct XBLicense {
|
||||
std::string gamertag;
|
||||
uint64_t user_id = 0;
|
||||
uint64_t account_id = 0;
|
||||
|
||||
static std::shared_ptr<XBLicense> from_json(const JSON& json);
|
||||
JSON json() const;
|
||||
};
|
||||
|
||||
struct BBLicense {
|
||||
std::string username;
|
||||
std::string password;
|
||||
|
||||
static std::shared_ptr<BBLicense> from_json(const JSON& json);
|
||||
JSON json() const;
|
||||
};
|
||||
|
||||
struct Account {
|
||||
enum class Flag : uint32_t {
|
||||
// clang-format off
|
||||
KICK_USER = 0x00000001,
|
||||
BAN_USER = 0x00000002,
|
||||
SILENCE_USER = 0x00000004,
|
||||
CHANGE_EVENT = 0x00000010,
|
||||
ANNOUNCE = 0x00000020,
|
||||
FREE_JOIN_GAMES = 0x00000040,
|
||||
DEBUG = 0x01000000,
|
||||
CHEAT_ANYWHERE = 0x02000000,
|
||||
DISABLE_QUEST_REQUIREMENTS = 0x04000000,
|
||||
ALWAYS_ENABLE_CHAT_COMMANDS = 0x08000000,
|
||||
MODERATOR = 0x00000007,
|
||||
ADMINISTRATOR = 0x000000FF,
|
||||
ROOT = 0x7FFFFFFF,
|
||||
IS_SHARED_ACCOUNT = 0x80000000,
|
||||
// NOTE: When adding or changing license flags, don't forget to change the
|
||||
// documentation in the shell's help text.
|
||||
UNUSED_BITS = 0x70FFFF00,
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
// account_id is also the account's guild card number
|
||||
uint32_t account_id = 0;
|
||||
|
||||
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;
|
||||
|
||||
uint32_t bb_team_id = 0;
|
||||
bool is_temporary = false; // If true, isn't saved to disk
|
||||
|
||||
std::unordered_set<std::string> auto_patches_enabled;
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<DCNTELicense>> dc_nte_licenses;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<V1V2License>> dc_licenses;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<V1V2License>> pc_licenses;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<GCLicense>> gc_licenses;
|
||||
std::unordered_map<std::string, std::shared_ptr<XBLicense>> xb_licenses;
|
||||
std::unordered_map<std::string, std::shared_ptr<BBLicense>> bb_licenses;
|
||||
|
||||
Account() = default;
|
||||
explicit Account(const JSON& json);
|
||||
virtual ~Account() = default;
|
||||
|
||||
JSON json() const;
|
||||
virtual void save() const;
|
||||
virtual void delete_file() const;
|
||||
|
||||
[[nodiscard]] inline bool check_flag(Flag flag) const {
|
||||
return !!(this->flags & static_cast<uint32_t>(flag));
|
||||
}
|
||||
inline void set_flag(Flag flag) {
|
||||
this->flags |= static_cast<uint32_t>(flag);
|
||||
}
|
||||
inline void clear_flag(Flag flag) {
|
||||
this->flags &= (~static_cast<uint32_t>(flag));
|
||||
}
|
||||
inline void toggle_flag(Flag flag) {
|
||||
this->flags ^= static_cast<uint32_t>(flag);
|
||||
}
|
||||
inline void replace_all_flags(Flag mask) {
|
||||
this->flags = static_cast<uint32_t>(mask);
|
||||
}
|
||||
|
||||
void print(FILE* stream) const;
|
||||
};
|
||||
|
||||
struct Login {
|
||||
bool account_was_created = false;
|
||||
// This field will never be null
|
||||
std::shared_ptr<Account> account;
|
||||
// Exactly one of the following will be non-null, representing the license
|
||||
// that the client logged in with
|
||||
std::shared_ptr<DCNTELicense> dc_nte_license;
|
||||
std::shared_ptr<V1V2License> dc_license;
|
||||
std::shared_ptr<V1V2License> pc_license;
|
||||
std::shared_ptr<GCLicense> gc_license;
|
||||
std::shared_ptr<XBLicense> xb_license;
|
||||
std::shared_ptr<BBLicense> bb_license;
|
||||
};
|
||||
|
||||
class AccountIndex {
|
||||
public:
|
||||
class no_username : public std::invalid_argument {
|
||||
public:
|
||||
no_username() : invalid_argument("serial number is zero or username is missing") {}
|
||||
};
|
||||
class incorrect_password : public std::invalid_argument {
|
||||
public:
|
||||
incorrect_password() : invalid_argument("incorrect password") {}
|
||||
};
|
||||
class incorrect_access_key : public std::invalid_argument {
|
||||
public:
|
||||
incorrect_access_key() : invalid_argument("incorrect access key") {}
|
||||
};
|
||||
class missing_account : public std::invalid_argument {
|
||||
public:
|
||||
missing_account() : invalid_argument("missing account") {}
|
||||
};
|
||||
|
||||
explicit AccountIndex(bool force_all_temporary);
|
||||
virtual ~AccountIndex() = default;
|
||||
|
||||
std::shared_ptr<Account> create_account(bool is_temporary) const;
|
||||
|
||||
size_t count() const;
|
||||
std::vector<std::shared_ptr<Account>> all() const;
|
||||
|
||||
void add(std::shared_ptr<Account> a);
|
||||
void remove(uint32_t serial_number);
|
||||
|
||||
void add_dc_nte_license(std::shared_ptr<Account> account, std::shared_ptr<DCNTELicense> license);
|
||||
void add_dc_license(std::shared_ptr<Account> account, std::shared_ptr<V1V2License> license);
|
||||
void add_pc_license(std::shared_ptr<Account> account, std::shared_ptr<V1V2License> license);
|
||||
void add_gc_license(std::shared_ptr<Account> account, std::shared_ptr<GCLicense> license);
|
||||
void add_xb_license(std::shared_ptr<Account> account, std::shared_ptr<XBLicense> license);
|
||||
void add_bb_license(std::shared_ptr<Account> account, std::shared_ptr<BBLicense> license);
|
||||
void remove_dc_nte_license(std::shared_ptr<Account> account, const std::string& serial_number);
|
||||
void remove_dc_license(std::shared_ptr<Account> account, uint32_t serial_number);
|
||||
void remove_pc_license(std::shared_ptr<Account> account, uint32_t serial_number);
|
||||
void remove_gc_license(std::shared_ptr<Account> account, uint32_t serial_number);
|
||||
void remove_xb_license(std::shared_ptr<Account> account, const std::string& gamertag);
|
||||
void remove_bb_license(std::shared_ptr<Account> account, const std::string& username);
|
||||
|
||||
std::shared_ptr<Account> from_account_id(uint32_t account_id) const;
|
||||
std::shared_ptr<Login> from_dc_nte_credentials(
|
||||
const std::string& serial_number,
|
||||
const std::string& access_key,
|
||||
bool allow_create);
|
||||
std::shared_ptr<Login> from_dc_credentials(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name,
|
||||
bool allow_create);
|
||||
std::shared_ptr<Login> from_pc_nte_credentials(
|
||||
uint32_t guild_card_number,
|
||||
bool allow_create);
|
||||
std::shared_ptr<Login> from_pc_credentials(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name,
|
||||
bool allow_create);
|
||||
std::shared_ptr<Login> from_gc_credentials(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string* password,
|
||||
const std::string& character_name,
|
||||
bool allow_create);
|
||||
std::shared_ptr<Login> from_xb_credentials(
|
||||
const std::string& gamertag,
|
||||
uint64_t user_id,
|
||||
uint64_t account_id,
|
||||
bool allow_create);
|
||||
std::shared_ptr<Login> from_bb_credentials(
|
||||
const std::string& username,
|
||||
const std::string* password,
|
||||
bool allow_create);
|
||||
|
||||
std::shared_ptr<Account> create_temporary_account_for_shared_account(
|
||||
std::shared_ptr<const Account> src_a, const std::string& variation_data) const;
|
||||
|
||||
protected:
|
||||
bool force_all_temporary;
|
||||
|
||||
// This class must be thread-safe because it's used by both the patch server
|
||||
// and game server threads
|
||||
mutable std::shared_mutex lock;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<Account>> by_account_id;
|
||||
std::unordered_map<std::string, std::shared_ptr<Account>> by_dc_nte_serial_number;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<Account>> by_dc_serial_number;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<Account>> by_pc_serial_number;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<Account>> by_gc_serial_number;
|
||||
std::unordered_map<std::string, std::shared_ptr<Account>> by_xb_gamertag;
|
||||
std::unordered_map<std::string, std::shared_ptr<Account>> by_bb_username;
|
||||
|
||||
void add_locked(std::shared_ptr<Account> a);
|
||||
|
||||
std::shared_ptr<Login> from_dc_nte_credentials_locked(
|
||||
const std::string& serial_number,
|
||||
const std::string& access_key);
|
||||
std::shared_ptr<Login> from_dc_credentials_locked(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name);
|
||||
std::shared_ptr<Login> from_pc_credentials_locked(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name);
|
||||
std::shared_ptr<Login> from_gc_credentials_locked(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string* password,
|
||||
const std::string& character_name);
|
||||
std::shared_ptr<Login> from_xb_credentials_locked(
|
||||
const std::string& gamertag,
|
||||
uint64_t user_id,
|
||||
uint64_t account_id);
|
||||
std::shared_ptr<Login> from_bb_credentials_locked(
|
||||
const std::string& username,
|
||||
const std::string* password);
|
||||
};
|
||||
+16
-6
@@ -9,16 +9,21 @@
|
||||
using namespace std;
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct BMLHeader {
|
||||
struct BMLHeaderT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
parray<uint8_t, 0x04> unknown_a1;
|
||||
U32T num_entries;
|
||||
parray<uint8_t, 0x38> unknown_a2;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
using BMLHeader = BMLHeaderT<false>;
|
||||
using BMLHeaderBE = BMLHeaderT<true>;
|
||||
check_struct_size(BMLHeader, 0x40);
|
||||
check_struct_size(BMLHeaderBE, 0x40);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct BMLHeaderEntry {
|
||||
struct BMLHeaderEntryT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
pstring<TextEncoding::ASCII, 0x20> filename;
|
||||
@@ -28,17 +33,22 @@ struct BMLHeaderEntry {
|
||||
U32T compressed_gvm_size;
|
||||
U32T decompressed_gvm_size;
|
||||
parray<uint8_t, 0x0C> unknown_a2;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
using BMLHeaderEntry = BMLHeaderEntryT<false>;
|
||||
using BMLHeaderEntryBE = BMLHeaderEntryT<true>;
|
||||
check_struct_size(BMLHeaderEntry, 0x40);
|
||||
check_struct_size(BMLHeaderEntryBE, 0x40);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
void BMLArchive::load_t() {
|
||||
StringReader r(*this->data);
|
||||
|
||||
const auto& header = r.get<BMLHeader<IsBigEndian>>();
|
||||
const auto& header = r.get<BMLHeaderT<IsBigEndian>>();
|
||||
|
||||
size_t offset = 0x800;
|
||||
while (this->entries.size() < header.num_entries) {
|
||||
const auto& entry = r.get<BMLHeaderEntry<IsBigEndian>>();
|
||||
const auto& entry = r.get<BMLHeaderEntryT<IsBigEndian>>();
|
||||
|
||||
if (offset + entry.compressed_size > this->data->size()) {
|
||||
throw runtime_error("BML data entry extends beyond end of data");
|
||||
|
||||
@@ -36,7 +36,7 @@ public:
|
||||
/* 28 */ le_uint32_t unknown_a15;
|
||||
/* 2C */ le_uint32_t unknown_a16;
|
||||
/* 30 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(AttackData, 0x30);
|
||||
|
||||
struct ResistData {
|
||||
/* 00 */ le_int16_t evp_bonus;
|
||||
@@ -51,7 +51,7 @@ public:
|
||||
/* 18 */ le_uint32_t unknown_a9;
|
||||
/* 1C */ le_int32_t dfp_bonus;
|
||||
/* 20 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ResistData, 0x20);
|
||||
|
||||
struct MovementData {
|
||||
/* 00 */ le_float idle_move_speed;
|
||||
@@ -67,7 +67,7 @@ public:
|
||||
/* 28 */ le_uint32_t unknown_a7;
|
||||
/* 2C */ le_uint32_t unknown_a8;
|
||||
/* 30 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(MovementData, 0x30);
|
||||
|
||||
struct Table {
|
||||
/* 0000 */ parray<parray<PlayerStats, 0x60>, 4> stats;
|
||||
@@ -77,7 +77,7 @@ public:
|
||||
/* F600 */
|
||||
|
||||
void print(FILE* stream) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Table, 0xF600);
|
||||
|
||||
BattleParamsIndex(
|
||||
std::shared_ptr<const std::string> data_on_ep1, // BattleParamEntry_on.dat
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ CatSession::CatSession(
|
||||
if (!bev) {
|
||||
throw runtime_error(string_printf("failed to open socket (%d)", EVUTIL_SOCKET_ERROR()));
|
||||
}
|
||||
this->channel.set_bufferevent(bev);
|
||||
this->channel.set_bufferevent(bev, 0);
|
||||
|
||||
if (bufferevent_socket_connect(this->channel.bev.get(),
|
||||
reinterpret_cast<const sockaddr*>(&remote), sizeof(struct sockaddr_in)) != 0) {
|
||||
|
||||
+8
-11
@@ -32,6 +32,7 @@ Channel::Channel(
|
||||
TerminalFormat terminal_send_color,
|
||||
TerminalFormat terminal_recv_color)
|
||||
: bev(nullptr, flush_and_free_bufferevent),
|
||||
virtual_network_id(0),
|
||||
version(version),
|
||||
language(language),
|
||||
name(name),
|
||||
@@ -44,6 +45,7 @@ Channel::Channel(
|
||||
|
||||
Channel::Channel(
|
||||
struct bufferevent* bev,
|
||||
uint64_t virtual_network_id,
|
||||
Version version,
|
||||
uint8_t language,
|
||||
on_command_received_t on_command_received,
|
||||
@@ -61,7 +63,7 @@ Channel::Channel(
|
||||
on_command_received(on_command_received),
|
||||
on_error(on_error),
|
||||
context_obj(context_obj) {
|
||||
this->set_bufferevent(bev);
|
||||
this->set_bufferevent(bev, virtual_network_id);
|
||||
}
|
||||
|
||||
void Channel::replace_with(
|
||||
@@ -70,10 +72,9 @@ void Channel::replace_with(
|
||||
on_error_t on_error,
|
||||
void* context_obj,
|
||||
const std::string& name) {
|
||||
this->set_bufferevent(other.bev.release());
|
||||
this->set_bufferevent(other.bev.release(), other.virtual_network_id);
|
||||
this->local_addr = other.local_addr;
|
||||
this->remote_addr = other.remote_addr;
|
||||
this->is_virtual_connection = other.is_virtual_connection;
|
||||
this->version = other.version;
|
||||
this->language = other.language;
|
||||
this->crypt_in = other.crypt_in;
|
||||
@@ -87,27 +88,23 @@ void Channel::replace_with(
|
||||
other.disconnect(); // Clears crypts, addrs, etc.
|
||||
}
|
||||
|
||||
void Channel::set_bufferevent(struct bufferevent* bev) {
|
||||
void Channel::set_bufferevent(struct bufferevent* bev, uint64_t virtual_network_id) {
|
||||
this->bev.reset(bev);
|
||||
this->virtual_network_id = virtual_network_id;
|
||||
|
||||
if (this->bev.get()) {
|
||||
int fd = bufferevent_getfd(this->bev.get());
|
||||
if (fd < 0) {
|
||||
this->is_virtual_connection = true;
|
||||
memset(&this->local_addr, 0, sizeof(this->local_addr));
|
||||
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
|
||||
} else {
|
||||
this->is_virtual_connection = false;
|
||||
get_socket_addresses(fd, &this->local_addr, &this->remote_addr);
|
||||
}
|
||||
|
||||
bufferevent_setcb(this->bev.get(),
|
||||
&Channel::dispatch_on_input, nullptr,
|
||||
&Channel::dispatch_on_error, this);
|
||||
bufferevent_setcb(this->bev.get(), &Channel::dispatch_on_input, nullptr, &Channel::dispatch_on_error, this);
|
||||
bufferevent_enable(this->bev.get(), EV_READ | EV_WRITE);
|
||||
|
||||
} else {
|
||||
this->is_virtual_connection = false;
|
||||
memset(&this->local_addr, 0, sizeof(this->local_addr));
|
||||
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
|
||||
}
|
||||
@@ -149,7 +146,7 @@ void Channel::disconnect() {
|
||||
|
||||
memset(&this->local_addr, 0, sizeof(this->local_addr));
|
||||
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
|
||||
this->is_virtual_connection = false;
|
||||
this->virtual_network_id = false;
|
||||
this->crypt_in.reset();
|
||||
this->crypt_out.reset();
|
||||
}
|
||||
|
||||
+3
-2
@@ -13,7 +13,7 @@ struct Channel {
|
||||
std::unique_ptr<struct bufferevent, void (*)(struct bufferevent*)> bev;
|
||||
struct sockaddr_storage local_addr;
|
||||
struct sockaddr_storage remote_addr;
|
||||
bool is_virtual_connection;
|
||||
uint64_t virtual_network_id; // 0 = normal TCP connection
|
||||
|
||||
Version version;
|
||||
uint8_t language;
|
||||
@@ -50,6 +50,7 @@ struct Channel {
|
||||
// Creates a connected channel
|
||||
Channel(
|
||||
struct bufferevent* bev,
|
||||
uint64_t virtual_network_id,
|
||||
Version version,
|
||||
uint8_t language,
|
||||
on_command_received_t on_command_received,
|
||||
@@ -70,7 +71,7 @@ struct Channel {
|
||||
void* context_obj,
|
||||
const std::string& name = "");
|
||||
|
||||
void set_bufferevent(struct bufferevent* bev);
|
||||
void set_bufferevent(struct bufferevent* bev, uint64_t virtual_network_id);
|
||||
|
||||
inline bool connected() const {
|
||||
return this->bev.get() != nullptr;
|
||||
|
||||
+218
-78
@@ -38,11 +38,11 @@ private:
|
||||
std::string user_msg;
|
||||
};
|
||||
|
||||
static void check_license_flag(shared_ptr<Client> c, License::Flag flag) {
|
||||
if (!c->license) {
|
||||
static void check_account_flag(shared_ptr<Client> c, Account::Flag flag) {
|
||||
if (!c->login) {
|
||||
throw precondition_failed("$C6You are not\nlogged in.");
|
||||
}
|
||||
if (!c->license->check_flag(flag)) {
|
||||
if (!c->login->account->check_flag(flag)) {
|
||||
throw precondition_failed("$C6You do not have\npermission to\nrun this command.");
|
||||
}
|
||||
}
|
||||
@@ -65,21 +65,29 @@ static void check_is_ep3(shared_ptr<Client> c, bool is_ep3) {
|
||||
}
|
||||
}
|
||||
|
||||
static void check_debug_enabled(shared_ptr<Client> c) {
|
||||
if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
throw precondition_failed("$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_cheats_enabled(shared_ptr<Lobby> l, shared_ptr<Client> c) {
|
||||
if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) && !c->license->check_flag(License::Flag::CHEAT_ANYWHERE)) {
|
||||
if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) &&
|
||||
!c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) {
|
||||
throw precondition_failed("$C6This command can\nonly be used in\ncheat mode.");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_cheats_allowed(shared_ptr<ServerState> s, shared_ptr<Client> c) {
|
||||
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && !c->license->check_flag(License::Flag::CHEAT_ANYWHERE)) {
|
||||
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) &&
|
||||
!c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) {
|
||||
throw precondition_failed("$C6Cheats are disabled\non this server.");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_cheats_allowed(shared_ptr<ServerState> s, shared_ptr<ProxyServer::LinkedSession> ses) {
|
||||
if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) &&
|
||||
(!ses->license || !ses->license->check_flag(License::Flag::CHEAT_ANYWHERE))) {
|
||||
(!ses->login || !ses->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE))) {
|
||||
throw precondition_failed("$C6Cheats are disabled\non this proxy.");
|
||||
}
|
||||
}
|
||||
@@ -255,19 +263,19 @@ static void proxy_command_lobby_info(shared_ptr<ProxyServer::LinkedSession> ses,
|
||||
}
|
||||
|
||||
static void server_command_ax(shared_ptr<Client> c, const std::string& args) {
|
||||
check_license_flag(c, License::Flag::ANNOUNCE);
|
||||
check_account_flag(c, Account::Flag::ANNOUNCE);
|
||||
ax_messages_log.info("%s", args.c_str());
|
||||
}
|
||||
|
||||
static void server_command_announce(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
check_license_flag(c, License::Flag::ANNOUNCE);
|
||||
check_account_flag(c, Account::Flag::ANNOUNCE);
|
||||
send_text_message(s, args);
|
||||
}
|
||||
|
||||
static void server_command_announce_mail(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
check_license_flag(c, License::Flag::ANNOUNCE);
|
||||
check_account_flag(c, Account::Flag::ANNOUNCE);
|
||||
send_simple_mail(s, 0, s->name, args);
|
||||
}
|
||||
|
||||
@@ -284,16 +292,13 @@ static void proxy_command_arrow(shared_ptr<ProxyServer::LinkedSession> ses, cons
|
||||
}
|
||||
|
||||
static void server_command_debug(shared_ptr<Client> c, const std::string&) {
|
||||
check_license_flag(c, License::Flag::DEBUG);
|
||||
check_account_flag(c, Account::Flag::DEBUG);
|
||||
c->config.toggle_flag(Client::Flag::DEBUG_ENABLED);
|
||||
send_text_message_printf(c, "Debug %s", (c->config.check_flag(Client::Flag::DEBUG_ENABLED) ? "enabled" : "disabled"));
|
||||
}
|
||||
|
||||
static void server_command_quest(shared_ptr<Client> c, const std::string& args) {
|
||||
if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message(c, "$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
||||
return;
|
||||
}
|
||||
check_debug_enabled(c);
|
||||
|
||||
Version effective_version = is_ep3(c->version()) ? Version::GC_V3 : c->version();
|
||||
|
||||
@@ -329,11 +334,124 @@ static void server_command_qcheck(shared_ptr<Client> c, const std::string& args)
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_qset_qclear(shared_ptr<Client> c, const std::string& args, bool should_set) {
|
||||
if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message(c, "$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
||||
static void server_command_swset_swclear(shared_ptr<Client> c, const std::string& args, bool should_set) {
|
||||
check_debug_enabled(c);
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game()) {
|
||||
send_text_message(c, "$C6This command cannot\nbe used in the lobby");
|
||||
return;
|
||||
}
|
||||
|
||||
auto tokens = split(args, ' ');
|
||||
uint8_t floor, flag_num;
|
||||
if (tokens.size() == 1) {
|
||||
floor = c->floor;
|
||||
flag_num = stoul(tokens[0], nullptr, 0);
|
||||
} else if (tokens.size() == 2) {
|
||||
floor = stoul(tokens[0], nullptr, 0);
|
||||
flag_num = stoul(tokens[1], nullptr, 0);
|
||||
} else {
|
||||
send_text_message(c, "$C4Incorrect parameters");
|
||||
return;
|
||||
}
|
||||
|
||||
if (should_set) {
|
||||
l->switch_flags->set(floor, flag_num);
|
||||
} else {
|
||||
l->switch_flags->clear(floor, flag_num);
|
||||
}
|
||||
|
||||
uint8_t cmd_flags = should_set ? 0x01 : 0x00;
|
||||
G_SwitchStateChanged_6x05 cmd = {{0x05, 0x03, 0xFFFF}, 0, 0, flag_num, floor, cmd_flags};
|
||||
send_command_t(l, 0x60, 0x00, cmd);
|
||||
}
|
||||
|
||||
static void server_command_swset(shared_ptr<Client> c, const std::string& args) {
|
||||
return server_command_swset_swclear(c, args, true);
|
||||
}
|
||||
|
||||
static void server_command_swclear(shared_ptr<Client> c, const std::string& args) {
|
||||
return server_command_swset_swclear(c, args, false);
|
||||
}
|
||||
|
||||
static void proxy_command_swset_swclear(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args, bool should_set) {
|
||||
if (!ses->is_in_game) {
|
||||
send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby");
|
||||
return;
|
||||
}
|
||||
|
||||
auto tokens = split(args, ' ');
|
||||
uint8_t floor, flag_num;
|
||||
if (tokens.size() == 1) {
|
||||
floor = ses->floor;
|
||||
flag_num = stoul(tokens[0], nullptr, 0);
|
||||
} else if (tokens.size() == 2) {
|
||||
floor = stoul(tokens[0], nullptr, 0);
|
||||
flag_num = stoul(tokens[1], nullptr, 0);
|
||||
} else {
|
||||
send_text_message(ses->client_channel, "$C4Incorrect parameters");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t cmd_flags = should_set ? 0x01 : 0x00;
|
||||
G_SwitchStateChanged_6x05 cmd = {{0x05, 0x03, 0xFFFF}, 0, 0, flag_num, floor, cmd_flags};
|
||||
ses->client_channel.send(0x60, 0x00, &cmd, sizeof(cmd));
|
||||
ses->server_channel.send(0x60, 0x00, &cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
static void proxy_command_swset(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
return proxy_command_swset_swclear(ses, args, true);
|
||||
}
|
||||
|
||||
static void proxy_command_swclear(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
return proxy_command_swset_swclear(ses, args, false);
|
||||
}
|
||||
|
||||
static void server_command_swsetall(shared_ptr<Client> c, const std::string&) {
|
||||
check_debug_enabled(c);
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game()) {
|
||||
send_text_message(c, "$C6This command cannot\nbe used in the lobby");
|
||||
return;
|
||||
}
|
||||
|
||||
l->switch_flags->data[c->floor].clear(0xFF);
|
||||
|
||||
parray<G_SwitchStateChanged_6x05, 0x100> cmds;
|
||||
for (size_t z = 0; z < cmds.size(); z++) {
|
||||
auto& cmd = cmds[z];
|
||||
cmd.header.subcommand = 0x05;
|
||||
cmd.header.size = 0x03;
|
||||
cmd.header.object_id = 0xFFFF;
|
||||
cmd.switch_flag_floor = c->floor;
|
||||
cmd.switch_flag_num = z;
|
||||
cmd.flags = 0x01;
|
||||
}
|
||||
send_command_t(l, 0x6C, 0x00, cmds);
|
||||
}
|
||||
|
||||
static void proxy_command_swsetall(shared_ptr<ProxyServer::LinkedSession> ses, const std::string&) {
|
||||
if (!ses->is_in_game) {
|
||||
send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby");
|
||||
return;
|
||||
}
|
||||
|
||||
parray<G_SwitchStateChanged_6x05, 0x100> cmds;
|
||||
for (size_t z = 0; z < cmds.size(); z++) {
|
||||
auto& cmd = cmds[z];
|
||||
cmd.header.subcommand = 0x05;
|
||||
cmd.header.size = 0x03;
|
||||
cmd.header.object_id = 0xFFFF;
|
||||
cmd.switch_flag_floor = ses->floor;
|
||||
cmd.switch_flag_num = z;
|
||||
cmd.flags = 0x01;
|
||||
}
|
||||
ses->client_channel.send(0x6C, 0x00, &cmds, sizeof(cmds));
|
||||
ses->server_channel.send(0x6C, 0x00, &cmds, sizeof(cmds));
|
||||
}
|
||||
|
||||
static void server_command_qset_qclear(shared_ptr<Client> c, const std::string& args, bool should_set) {
|
||||
check_debug_enabled(c);
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game()) {
|
||||
send_text_message(c, "$C6This command cannot\nbe used in the lobby");
|
||||
@@ -419,10 +537,7 @@ static void server_command_qgwrite(shared_ptr<Client> c, const std::string& args
|
||||
send_text_message(c, "$C6This command can\nonly be used on BB");
|
||||
return;
|
||||
}
|
||||
if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message(c, "$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
||||
return;
|
||||
}
|
||||
check_debug_enabled(c);
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game()) {
|
||||
send_text_message(c, "$C6This command cannot\nbe used in the lobby");
|
||||
@@ -449,10 +564,7 @@ static void server_command_qgwrite(shared_ptr<Client> c, const std::string& args
|
||||
}
|
||||
|
||||
static void server_command_qsync_qsyncall(shared_ptr<Client> c, const std::string& args, bool send_to_lobby) {
|
||||
if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message(c, "$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
||||
return;
|
||||
}
|
||||
check_debug_enabled(c);
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game()) {
|
||||
send_text_message(c, "$C6This command cannot\nbe used in the lobby");
|
||||
@@ -531,10 +643,7 @@ static void proxy_command_qsyncall(shared_ptr<ProxyServer::LinkedSession> ses, c
|
||||
}
|
||||
|
||||
static void server_command_qcall(shared_ptr<Client> c, const std::string& args) {
|
||||
if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message(c, "$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
||||
return;
|
||||
}
|
||||
check_debug_enabled(c);
|
||||
|
||||
auto l = c->require_lobby();
|
||||
if (l->is_game() && l->quest) {
|
||||
@@ -571,7 +680,7 @@ static void server_command_show_material_counts(shared_ptr<Client> c, const std:
|
||||
}
|
||||
|
||||
static void server_command_auction(shared_ptr<Client> c, const std::string&) {
|
||||
check_license_flag(c, License::Flag::DEBUG);
|
||||
check_account_flag(c, Account::Flag::DEBUG);
|
||||
auto l = c->require_lobby();
|
||||
if (l->is_game() && l->is_ep3()) {
|
||||
G_InitiateCardAuction_Ep3_6xB5x42 cmd;
|
||||
@@ -742,11 +851,19 @@ static void proxy_command_get_player_card(shared_ptr<ProxyServer::LinkedSession>
|
||||
}
|
||||
|
||||
static void server_command_send_client(shared_ptr<Client> c, const std::string& args) {
|
||||
check_debug_enabled(c);
|
||||
string data = parse_data_string(args);
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
c->channel.send(data);
|
||||
}
|
||||
|
||||
static void server_command_send_server(shared_ptr<Client> c, const std::string& args) {
|
||||
check_debug_enabled(c);
|
||||
string data = parse_data_string(args);
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
on_command_with_header(c, data);
|
||||
}
|
||||
|
||||
static void proxy_command_send_client(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
string data = parse_data_string(args);
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
@@ -759,6 +876,16 @@ static void proxy_command_send_server(shared_ptr<ProxyServer::LinkedSession> ses
|
||||
ses->server_channel.send(data);
|
||||
}
|
||||
|
||||
static void server_command_send_both(shared_ptr<Client> c, const std::string& args) {
|
||||
server_command_send_client(c, args);
|
||||
server_command_send_server(c, args);
|
||||
}
|
||||
|
||||
static void proxy_command_send_both(shared_ptr<ProxyServer::LinkedSession> ses, const std::string& args) {
|
||||
proxy_command_send_client(ses, args);
|
||||
proxy_command_send_server(ses, args);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Lobby commands
|
||||
|
||||
@@ -773,7 +900,7 @@ static void server_command_cheat(shared_ptr<Client> c, const std::string&) {
|
||||
l->toggle_flag(Lobby::Flag::CHEATS_ENABLED);
|
||||
send_text_message_printf(l, "Cheat mode %s", l->check_flag(Lobby::Flag::CHEATS_ENABLED) ? "enabled" : "disabled");
|
||||
|
||||
if (!c->license->check_flag(License::Flag::CHEAT_ANYWHERE)) {
|
||||
if (!c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) {
|
||||
size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty);
|
||||
if (l->min_level < default_min_level) {
|
||||
l->min_level = default_min_level;
|
||||
@@ -786,7 +913,7 @@ static void server_command_cheat(shared_ptr<Client> c, const std::string&) {
|
||||
static void server_command_lobby_event(shared_ptr<Client> c, const std::string& args) {
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, false);
|
||||
check_license_flag(c, License::Flag::CHANGE_EVENT);
|
||||
check_account_flag(c, Account::Flag::CHANGE_EVENT);
|
||||
|
||||
uint8_t new_event = event_for_name(args);
|
||||
if (new_event == 0xFF) {
|
||||
@@ -815,7 +942,7 @@ static void proxy_command_lobby_event(shared_ptr<ProxyServer::LinkedSession> ses
|
||||
}
|
||||
|
||||
static void server_command_lobby_event_all(shared_ptr<Client> c, const std::string& args) {
|
||||
check_license_flag(c, License::Flag::CHANGE_EVENT);
|
||||
check_account_flag(c, Account::Flag::CHANGE_EVENT);
|
||||
|
||||
uint8_t new_event = event_for_name(args);
|
||||
if (new_event == 0xFF) {
|
||||
@@ -867,13 +994,13 @@ static void proxy_command_lobby_type(shared_ptr<ProxyServer::LinkedSession> ses,
|
||||
ses->config.override_lobby_number = new_type;
|
||||
}
|
||||
|
||||
static string file_path_for_recording(const std::string& args, uint32_t serial_number) {
|
||||
static string file_path_for_recording(const std::string& args, uint32_t account_id) {
|
||||
for (char ch : args) {
|
||||
if (ch <= 0x20 || ch > 0x7E || ch == '/') {
|
||||
throw runtime_error("invalid recording name");
|
||||
}
|
||||
}
|
||||
return string_printf("system/ep3/battle-records/%010" PRIu32 "_%s.mzrd", serial_number, args.c_str());
|
||||
return string_printf("system/ep3/battle-records/%010" PRIu32 "_%s.mzrd", account_id, args.c_str());
|
||||
}
|
||||
|
||||
static void server_command_saverec(shared_ptr<Client> c, const std::string& args) {
|
||||
@@ -881,7 +1008,7 @@ static void server_command_saverec(shared_ptr<Client> c, const std::string& args
|
||||
send_text_message(c, "$C4No finished\nrecording is\npresent");
|
||||
return;
|
||||
}
|
||||
string file_path = file_path_for_recording(args, c->license->serial_number);
|
||||
string file_path = file_path_for_recording(args, c->login->account->account_id);
|
||||
string data = c->ep3_prev_battle_record->serialize();
|
||||
save_file(file_path, data);
|
||||
send_text_message(c, "$C7Recording saved");
|
||||
@@ -898,7 +1025,7 @@ static void server_command_playrec(shared_ptr<Client> c, const std::string& args
|
||||
if (l->is_game() && l->battle_player) {
|
||||
l->battle_player->start();
|
||||
} else if (!l->is_game()) {
|
||||
string file_path = file_path_for_recording(args, c->license->serial_number);
|
||||
string file_path = file_path_for_recording(args, c->login->account->account_id);
|
||||
|
||||
auto s = c->require_server_state();
|
||||
string filename = args;
|
||||
@@ -932,17 +1059,14 @@ static void server_command_playrec(shared_ptr<Client> c, const std::string& args
|
||||
|
||||
static void server_command_meseta(shared_ptr<Client> c, const std::string& args) {
|
||||
check_is_ep3(c, true);
|
||||
if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message(c, "$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
||||
return;
|
||||
}
|
||||
check_debug_enabled(c);
|
||||
|
||||
uint32_t amount = stoul(args, nullptr, 0);
|
||||
c->license->ep3_current_meseta += amount;
|
||||
c->license->ep3_total_meseta_earned += amount;
|
||||
c->license->save();
|
||||
c->login->account->ep3_current_meseta += amount;
|
||||
c->login->account->ep3_total_meseta_earned += amount;
|
||||
c->login->account->save();
|
||||
send_ep3_rank_update(c);
|
||||
send_text_message_printf(c, "You now have\n$C6%" PRIu32 "$C7 Meseta", c->license->ep3_current_meseta);
|
||||
send_text_message_printf(c, "You now have\n$C6%" PRIu32 "$C7 Meseta", c->login->account->ep3_current_meseta);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -1093,7 +1217,8 @@ static void server_command_min_level(shared_ptr<Client> c, const std::string& ar
|
||||
size_t new_min_level = stoull(args) - 1;
|
||||
|
||||
auto s = c->require_server_state();
|
||||
bool cheats_allowed = (l->check_flag(Lobby::Flag::CHEATS_ENABLED) || c->license->check_flag(License::Flag::CHEAT_ANYWHERE));
|
||||
bool cheats_allowed = (l->check_flag(Lobby::Flag::CHEATS_ENABLED) ||
|
||||
c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE));
|
||||
if (!cheats_allowed) {
|
||||
size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty);
|
||||
if (new_min_level < default_min_level) {
|
||||
@@ -1135,7 +1260,7 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
|
||||
}
|
||||
|
||||
bool cheats_allowed = ((s->cheat_mode_behavior != ServerState::BehaviorSwitch::OFF) ||
|
||||
c->license->check_flag(License::Flag::CHEAT_ANYWHERE));
|
||||
c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE));
|
||||
|
||||
string encoded_args = tolower(args);
|
||||
vector<string> tokens = split(encoded_args, ' ');
|
||||
@@ -1162,12 +1287,24 @@ static void server_command_edit(shared_ptr<Client> c, const std::string& args) {
|
||||
p->disp.stats.experience = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "level" && cheats_allowed) {
|
||||
uint32_t level = stoul(tokens.at(1)) - 1;
|
||||
p->disp.stats.reset_to_base(p->disp.visual.char_class, s->level_table);
|
||||
p->disp.stats.advance_to_level(p->disp.visual.char_class, level, s->level_table);
|
||||
s->level_table->reset_to_base(p->disp.stats, p->disp.visual.char_class);
|
||||
s->level_table->advance_to_level(p->disp.stats, level, p->disp.visual.char_class);
|
||||
} else if (tokens.at(0) == "namecolor") {
|
||||
uint32_t new_color;
|
||||
sscanf(tokens.at(1).c_str(), "%8X", &new_color);
|
||||
p->disp.visual.name_color = new_color;
|
||||
} else if (tokens.at(0) == "language" || tokens.at(0) == "lang") {
|
||||
if (tokens.at(1).size() != 1) {
|
||||
throw runtime_error("invalid language");
|
||||
}
|
||||
uint8_t new_language = language_code_for_char(tokens.at(1).at(0));
|
||||
c->channel.language = new_language;
|
||||
p->inventory.language = new_language;
|
||||
p->guild_card.language = new_language;
|
||||
auto sys = c->system_file(false);
|
||||
if (sys) {
|
||||
sys->base.language = new_language;
|
||||
}
|
||||
} else if (tokens.at(0) == "secid" && cheats_allowed) {
|
||||
uint8_t secid = section_id_for_name(tokens.at(1));
|
||||
if (secid == 0xFF) {
|
||||
@@ -1267,7 +1404,6 @@ static void server_command_bbchar_savechar(shared_ptr<Client> c, const std::stri
|
||||
check_is_game(l, false);
|
||||
|
||||
auto pending_export = make_unique<Client::PendingCharacterExport>();
|
||||
pending_export->is_bb_conversion = is_bb_conversion;
|
||||
|
||||
if (is_bb_conversion) {
|
||||
vector<string> tokens = split(args, ' ');
|
||||
@@ -1284,7 +1420,9 @@ static void server_command_bbchar_savechar(shared_ptr<Client> c, const std::stri
|
||||
}
|
||||
|
||||
try {
|
||||
c->pending_character_export->license = s->license_index->verify_bb(tokens[0].c_str(), tokens[1].c_str());
|
||||
auto dest_login = s->account_index->from_bb_credentials(tokens[0], &tokens[1], false);
|
||||
pending_export->dest_account = dest_login->account;
|
||||
pending_export->dest_bb_license = dest_login->bb_license;
|
||||
} catch (const exception& e) {
|
||||
send_text_message_printf(c, "$C6Login failed: %s", e.what());
|
||||
return;
|
||||
@@ -1296,13 +1434,14 @@ static void server_command_bbchar_savechar(shared_ptr<Client> c, const std::stri
|
||||
send_text_message(c, "$C6Player index must\nbe in range 1-4");
|
||||
return;
|
||||
}
|
||||
pending_export->license = c->license;
|
||||
pending_export->dest_account = c->login->account;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Request the player data. The client will respond with a 61 or 30, and the
|
||||
// handler for either of those commands will execute the conversion
|
||||
send_get_player_info(c, true);
|
||||
}
|
||||
|
||||
static void server_command_bbchar(shared_ptr<Client> c, const std::string& args) {
|
||||
@@ -1310,8 +1449,8 @@ static void server_command_bbchar(shared_ptr<Client> c, const std::string& args)
|
||||
}
|
||||
|
||||
static void server_command_savechar(shared_ptr<Client> c, const std::string& args) {
|
||||
if (c->license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
send_text_message(c, "$C7This command cannot\nbe used on a shared\nserial number");
|
||||
if (c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) {
|
||||
send_text_message(c, "$C7This command cannot\nbe used on a shared\naccount");
|
||||
return;
|
||||
}
|
||||
server_command_bbchar_savechar(c, args, false);
|
||||
@@ -1322,8 +1461,8 @@ static void server_command_loadchar(shared_ptr<Client> c, const std::string& arg
|
||||
send_text_message(c, "$C7This command can only\nbe used on v1 or v2");
|
||||
return;
|
||||
}
|
||||
if (c->license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
send_text_message(c, "$C7This command cannot\nbe used on a shared\nserial number");
|
||||
if (c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) {
|
||||
send_text_message(c, "$C7This command cannot\nbe used on a shared\naccount");
|
||||
return;
|
||||
}
|
||||
auto l = c->require_lobby();
|
||||
@@ -1334,7 +1473,7 @@ static void server_command_loadchar(shared_ptr<Client> c, const std::string& arg
|
||||
send_text_message(c, "$C6Player index must\nbe in range 1-4");
|
||||
return;
|
||||
}
|
||||
c->load_backup_character(c->license->serial_number, index);
|
||||
c->load_backup_character(c->login->account->account_id, index);
|
||||
|
||||
auto s = c->require_server_state();
|
||||
send_player_leave_notification(l, c->lobby_client_id);
|
||||
@@ -1361,8 +1500,8 @@ static string name_for_client(shared_ptr<Client> c) {
|
||||
return escape_player_name(player->disp.name.decode(player->inventory.language));
|
||||
}
|
||||
|
||||
if (c->license.get()) {
|
||||
return string_printf("SN:%" PRIu32, c->license->serial_number);
|
||||
if (c->login) {
|
||||
return string_printf("SN:%" PRIu32, c->login->account->account_id);
|
||||
}
|
||||
|
||||
return "Player";
|
||||
@@ -1371,16 +1510,16 @@ static string name_for_client(shared_ptr<Client> c) {
|
||||
static void server_command_silence(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_license_flag(c, License::Flag::SILENCE_USER);
|
||||
check_account_flag(c, Account::Flag::SILENCE_USER);
|
||||
|
||||
auto target = s->find_client(&args);
|
||||
if (!target->license) {
|
||||
if (!target->login) {
|
||||
// this should be impossible, but I'll bet it's not actually
|
||||
send_text_message(c, "$C6Client not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target->license->check_flag(License::Flag::SILENCE_USER)) {
|
||||
if (target->login->account->check_flag(Account::Flag::SILENCE_USER)) {
|
||||
send_text_message(c, "$C6You do not have\nsufficient privileges.");
|
||||
return;
|
||||
}
|
||||
@@ -1394,16 +1533,16 @@ static void server_command_silence(shared_ptr<Client> c, const std::string& args
|
||||
static void server_command_kick(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_license_flag(c, License::Flag::KICK_USER);
|
||||
check_account_flag(c, Account::Flag::KICK_USER);
|
||||
|
||||
auto target = s->find_client(&args);
|
||||
if (!target->license) {
|
||||
if (!target->login) {
|
||||
// This should be impossible, but I'll bet it's not actually
|
||||
send_text_message(c, "$C6Client not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target->license->check_flag(License::Flag::KICK_USER)) {
|
||||
if (target->login->account->check_flag(Account::Flag::KICK_USER)) {
|
||||
send_text_message(c, "$C6You do not have\nsufficient privileges.");
|
||||
return;
|
||||
}
|
||||
@@ -1417,7 +1556,7 @@ static void server_command_kick(shared_ptr<Client> c, const std::string& args) {
|
||||
static void server_command_ban(shared_ptr<Client> c, const std::string& args) {
|
||||
auto s = c->require_server_state();
|
||||
auto l = c->require_lobby();
|
||||
check_license_flag(c, License::Flag::BAN_USER);
|
||||
check_account_flag(c, Account::Flag::BAN_USER);
|
||||
|
||||
size_t space_pos = args.find(' ');
|
||||
if (space_pos == string::npos) {
|
||||
@@ -1427,13 +1566,13 @@ static void server_command_ban(shared_ptr<Client> c, const std::string& args) {
|
||||
|
||||
string identifier = args.substr(space_pos + 1);
|
||||
auto target = s->find_client(&identifier);
|
||||
if (!target->license) {
|
||||
if (!target->login) {
|
||||
// This should be impossible, but I'll bet it's not actually
|
||||
send_text_message(c, "$C6Client not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target->license->check_flag(License::Flag::BAN_USER)) {
|
||||
if (target->login->account->check_flag(Account::Flag::BAN_USER)) {
|
||||
send_text_message(c, "$C6You do not have\nsufficient privileges.");
|
||||
return;
|
||||
}
|
||||
@@ -1457,8 +1596,8 @@ static void server_command_ban(shared_ptr<Client> c, const std::string& args) {
|
||||
usecs *= 60 * 60 * 24 * 365;
|
||||
}
|
||||
|
||||
target->license->ban_end_time = now() + usecs;
|
||||
target->license->save();
|
||||
target->login->account->ban_end_time = now() + usecs;
|
||||
target->login->account->save();
|
||||
send_message_box(target, "$C6You were banned by a moderator.");
|
||||
target->should_disconnect = true;
|
||||
string target_name = name_for_client(target);
|
||||
@@ -1874,10 +2013,7 @@ static void proxy_command_item(shared_ptr<ProxyServer::LinkedSession> ses, const
|
||||
}
|
||||
|
||||
static void server_command_enable_ep3_battle_debug_menu(shared_ptr<Client> c, const std::string& args) {
|
||||
if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message(c, "$C6This command can only\nbe run in debug mode\n(run %sdebug first)");
|
||||
return;
|
||||
}
|
||||
check_debug_enabled(c);
|
||||
|
||||
auto l = c->require_lobby();
|
||||
check_is_game(l, true);
|
||||
@@ -2242,6 +2378,7 @@ static const unordered_map<string, ChatCommandDefinition> chat_commands({
|
||||
{"$save", {server_command_save, nullptr}},
|
||||
{"$savechar", {server_command_savechar, nullptr}},
|
||||
{"$saverec", {server_command_saverec, nullptr}},
|
||||
{"$sb", {server_command_send_both, proxy_command_send_both}},
|
||||
{"$sc", {server_command_send_client, proxy_command_send_client}},
|
||||
{"$secid", {server_command_secid, proxy_command_secid}},
|
||||
{"$setassist", {server_command_ep3_replace_assist_card, nullptr}},
|
||||
@@ -2249,10 +2386,13 @@ static const unordered_map<string, ChatCommandDefinition> chat_commands({
|
||||
{"$silence", {server_command_silence, nullptr}},
|
||||
{"$song", {server_command_song, proxy_command_song}},
|
||||
{"$spec", {server_command_toggle_spectator_flag, nullptr}},
|
||||
{"$ss", {nullptr, proxy_command_send_server}},
|
||||
{"$ss", {server_command_send_server, proxy_command_send_server}},
|
||||
{"$stat", {server_command_get_ep3_battle_stat, nullptr}},
|
||||
{"$surrender", {server_command_surrender, nullptr}},
|
||||
{"$swa", {server_command_switch_assist, proxy_command_switch_assist}},
|
||||
{"$swclear", {server_command_swclear, proxy_command_swclear}},
|
||||
{"$swset", {server_command_swset, proxy_command_swset}},
|
||||
{"$swsetall", {server_command_swsetall, proxy_command_swsetall}},
|
||||
{"$unset", {server_command_ep3_unset_field_character, nullptr}},
|
||||
{"$variations", {server_command_variations, nullptr}},
|
||||
{"$warp", {server_command_warpme, proxy_command_warpme}},
|
||||
|
||||
+27
-6
@@ -10,12 +10,16 @@
|
||||
|
||||
class Client;
|
||||
|
||||
struct ChoiceSearchConfig {
|
||||
le_uint32_t disabled = 1; // 0 = enabled, 1 = disabled. Unused in command C3
|
||||
template <bool IsBigEndian>
|
||||
struct ChoiceSearchConfigT {
|
||||
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;
|
||||
|
||||
U32T 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));
|
||||
U16T parent_choice_id = 0;
|
||||
U16T choice_id = 0;
|
||||
} __packed_ws__(Entry, 4);
|
||||
parray<Entry, 5> entries;
|
||||
|
||||
int32_t get_setting(uint16_t parent_choice_id) const {
|
||||
@@ -26,7 +30,24 @@ struct ChoiceSearchConfig {
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
|
||||
operator ChoiceSearchConfigT<!IsBigEndian>() const {
|
||||
ChoiceSearchConfigT<!IsBigEndian> ret;
|
||||
ret.disabled = this->disabled.load();
|
||||
for (size_t z = 0; z < this->entries.size(); z++) {
|
||||
auto& ret_e = ret.entries[z];
|
||||
const auto& this_e = this->entries[z];
|
||||
ret_e.parent_choice_id = this_e.parent_choice_id.load();
|
||||
ret_e.choice_id = this_e.choice_id.load();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
|
||||
using ChoiceSearchConfig = ChoiceSearchConfigT<false>;
|
||||
using ChoiceSearchConfigBE = ChoiceSearchConfigT<true>;
|
||||
check_struct_size(ChoiceSearchConfig, 0x18);
|
||||
check_struct_size(ChoiceSearchConfigBE, 0x18);
|
||||
|
||||
struct ChoiceSearchCategory {
|
||||
struct Choice {
|
||||
|
||||
+101
-80
@@ -11,6 +11,7 @@
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "IPStackSimulator.hh"
|
||||
#include "Loggers.hh"
|
||||
#include "Server.hh"
|
||||
#include "Version.hh"
|
||||
@@ -44,6 +45,9 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
case Version::DC_NTE:
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
case Version::DC_V1:
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::NO_SEND_FUNCTION_CALL);
|
||||
break;
|
||||
case Version::DC_V2:
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
@@ -74,13 +78,15 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::NO_SEND_FUNCTION_CALL);
|
||||
break;
|
||||
case 0x23: // DCv1 EU
|
||||
case 0x22: // DCv1 EU 50Hz (presumably)
|
||||
case 0x23: // DCv1 EU 60Hz (presumably)
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::NO_SEND_FUNCTION_CALL);
|
||||
break;
|
||||
case 0x25: // DCv2 JP
|
||||
case 0x26: // DCv2 US
|
||||
case 0x28: // DCv2 EU
|
||||
case 0x27: // DCv2 EU 50Hz (presumably)
|
||||
case 0x28: // DCv2 EU 60Hz (presumably)
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
break;
|
||||
@@ -164,12 +170,13 @@ bool Client::Config::should_update_vs(const Config& other) const {
|
||||
Client::Client(
|
||||
shared_ptr<Server> server,
|
||||
struct bufferevent* bev,
|
||||
uint64_t virtual_network_id,
|
||||
Version version,
|
||||
ServerBehavior server_behavior)
|
||||
: server(server),
|
||||
id(next_id++),
|
||||
log(string_printf("[C-%" PRIX64 "] ", this->id), client_log.min_level),
|
||||
channel(bev, version, 1, nullptr, nullptr, this, string_printf("C-%" PRIX64, this->id), TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN),
|
||||
channel(bev, virtual_network_id, version, 1, nullptr, nullptr, this, "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN),
|
||||
server_behavior(server_behavior),
|
||||
should_disconnect(false),
|
||||
should_send_to_lobby_server(false),
|
||||
@@ -201,6 +208,7 @@ Client::Client(
|
||||
card_battle_table_number(-1),
|
||||
card_battle_table_seat_number(0),
|
||||
card_battle_table_seat_state(0),
|
||||
last_game_info_requested(0),
|
||||
should_update_play_time(false),
|
||||
bb_character_index(-1),
|
||||
next_exp_value(0),
|
||||
@@ -208,6 +216,8 @@ Client::Client(
|
||||
dol_base_addr(0),
|
||||
external_bank_character_index(-1),
|
||||
last_play_time_update(0) {
|
||||
this->update_channel_name();
|
||||
|
||||
this->config.set_flags_for_version(version, -1);
|
||||
auto s = server->get_state();
|
||||
if (is_v1_or_v2(this->version()) ? s->default_rare_notifs_enabled_v1_v2 : s->default_rare_notifs_enabled_v3_v4) {
|
||||
@@ -246,6 +256,19 @@ Client::~Client() {
|
||||
this->log.info("Deleted");
|
||||
}
|
||||
|
||||
void Client::update_channel_name() {
|
||||
string ip_str = this->require_server_state()->format_address_for_channel_name(
|
||||
this->channel.remote_addr, this->channel.virtual_network_id);
|
||||
|
||||
auto player = this->character(false, false);
|
||||
if (player) {
|
||||
string name_str = player->disp.name.decode(this->language());
|
||||
this->channel.name = string_printf("C-%" PRIX64 " (%s) @ %s", this->id, name_str.c_str(), ip_str.c_str());
|
||||
} else {
|
||||
this->channel.name = string_printf("C-%" PRIX64 " @ %s", this->id, ip_str.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Client::reschedule_save_game_data_event() {
|
||||
if (this->version() == Version::BB_V4) {
|
||||
struct timeval tv = usecs_to_timeval(60000000); // 1 minute
|
||||
@@ -261,33 +284,16 @@ void Client::reschedule_ping_and_timeout_events() {
|
||||
event_add(this->idle_timeout_event.get(), &idle_tv);
|
||||
}
|
||||
|
||||
void Client::set_license(shared_ptr<License> l) {
|
||||
if (this->version() == Version::BB_V4) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
void Client::convert_license_to_temporary_if_nte() {
|
||||
// If the session is a prototype version and the license was created and we
|
||||
// should use a temporary license instead, delete the permanent license and
|
||||
// replace it with a temporary license.
|
||||
void Client::convert_account_to_temporary_if_nte() {
|
||||
// If the session is a prototype version and the account was created and we
|
||||
// should use a temporary account instead, delete the permanent account and
|
||||
// replace it with a temporary account.
|
||||
auto s = this->require_server_state();
|
||||
if (s->use_temp_licenses_for_prototypes &&
|
||||
this->config.check_flag(Client::Flag::LICENSE_WAS_CREATED) &&
|
||||
is_any_nte(this->version())) {
|
||||
this->log.info("Client is a prototype version and the license was created during this session; converting permanent license to temporary license");
|
||||
s->license_index->remove(this->license->serial_number);
|
||||
auto new_l = s->license_index->create_temporary_license();
|
||||
this->license->delete_file();
|
||||
*new_l = std::move(*this->license);
|
||||
this->set_license(new_l);
|
||||
this->config.clear_flag(Client::Flag::LICENSE_WAS_CREATED);
|
||||
if (s->use_temp_accounts_for_prototypes && this->login->account_was_created && is_any_nte(this->version())) {
|
||||
this->log.info("Client is a prototype version and the account was created during this session; converting permanent account to temporary account");
|
||||
this->login->account->is_temporary = true;
|
||||
this->login->account->delete_file();
|
||||
this->login->account_was_created = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,29 +314,29 @@ shared_ptr<Lobby> Client::require_lobby() const {
|
||||
}
|
||||
|
||||
shared_ptr<const TeamIndex::Team> Client::team() const {
|
||||
if (!this->license) {
|
||||
throw logic_error("Client::team called on client with no license");
|
||||
if (!this->login) {
|
||||
throw logic_error("Client::team called on client with no account");
|
||||
}
|
||||
|
||||
if (this->license->bb_team_id == 0) {
|
||||
if (this->login->account->bb_team_id == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto p = this->character(false);
|
||||
auto s = this->require_server_state();
|
||||
auto team = s->team_index->get_by_id(this->license->bb_team_id);
|
||||
auto team = s->team_index->get_by_id(this->login->account->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();
|
||||
this->log.info("Account contains a team ID, but the team does not exist; clearing team ID from account");
|
||||
this->login->account->bb_team_id = 0;
|
||||
this->login->account->save();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto member_it = team->members.find(this->license->serial_number);
|
||||
auto member_it = team->members.find(this->login->account->account_id);
|
||||
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();
|
||||
this->log.info("Account contains a team ID, but the team does not contain this member; clearing team ID from account");
|
||||
this->login->account->bb_team_id = 0;
|
||||
this->login->account->save();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -341,7 +347,7 @@ shared_ptr<const TeamIndex::Team> Client::team() const {
|
||||
string name = p->disp.name.decode(this->language());
|
||||
if (m.name != name) {
|
||||
this->log.info("Updating player name in team config");
|
||||
s->team_index->update_member_name(this->license->serial_number, name);
|
||||
s->team_index->update_member_name(this->login->account->account_id, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,21 +355,24 @@ shared_ptr<const TeamIndex::Team> Client::team() const {
|
||||
}
|
||||
|
||||
bool Client::evaluate_quest_availability_expression(
|
||||
shared_ptr<const QuestAvailabilityExpression> expr,
|
||||
shared_ptr<const IntegralExpression> expr,
|
||||
shared_ptr<const Lobby> game,
|
||||
uint8_t event,
|
||||
uint8_t difficulty,
|
||||
size_t num_players,
|
||||
bool v1_present) const {
|
||||
if (this->license && this->license->check_flag(License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
if (this->login && this->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
return true;
|
||||
}
|
||||
if (!expr) {
|
||||
return true;
|
||||
}
|
||||
auto l = this->lobby.lock();
|
||||
if (game && !game->quest_flag_values) {
|
||||
throw logic_error("quest flags are missing from game");
|
||||
}
|
||||
auto p = this->character();
|
||||
QuestAvailabilityExpression::Env env = {
|
||||
.flags = (l && !l->quest_flags_known) ? &l->quest_flag_values->data.at(difficulty) : &p->quest_flags.data.at(difficulty),
|
||||
IntegralExpression::Env env = {
|
||||
.flags = &p->quest_flags.data.at(difficulty),
|
||||
.challenge_records = &p->challenge_records,
|
||||
.team = this->team(),
|
||||
.num_players = num_players,
|
||||
@@ -378,19 +387,31 @@ bool Client::evaluate_quest_availability_expression(
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool Client::can_see_quest(shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players, bool v1_present) const {
|
||||
return this->evaluate_quest_availability_expression(q->available_expression, event, difficulty, num_players, v1_present);
|
||||
bool Client::can_see_quest(
|
||||
shared_ptr<const Quest> q,
|
||||
shared_ptr<const Lobby> game,
|
||||
uint8_t event,
|
||||
uint8_t difficulty,
|
||||
size_t num_players,
|
||||
bool v1_present) const {
|
||||
return this->evaluate_quest_availability_expression(q->available_expression, game, event, difficulty, num_players, v1_present);
|
||||
}
|
||||
|
||||
bool Client::can_play_quest(shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players, bool v1_present) const {
|
||||
return this->evaluate_quest_availability_expression(q->enabled_expression, event, difficulty, num_players, v1_present);
|
||||
bool Client::can_play_quest(
|
||||
shared_ptr<const Quest> q,
|
||||
shared_ptr<const Lobby> game,
|
||||
uint8_t event,
|
||||
uint8_t difficulty,
|
||||
size_t num_players,
|
||||
bool v1_present) const {
|
||||
return this->evaluate_quest_availability_expression(q->enabled_expression, game, event, difficulty, num_players, v1_present);
|
||||
}
|
||||
|
||||
bool Client::can_use_chat_commands() const {
|
||||
if (!this->license) {
|
||||
if (!this->login) {
|
||||
return false;
|
||||
}
|
||||
if (this->license->check_flag(License::Flag::ALWAYS_ENABLE_CHAT_COMMANDS)) {
|
||||
if (this->login->account->check_flag(Account::Flag::ALWAYS_ENABLE_CHAT_COMMANDS)) {
|
||||
return true;
|
||||
}
|
||||
return this->require_server_state()->enable_chat_commands;
|
||||
@@ -470,8 +491,8 @@ void Client::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_p
|
||||
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);
|
||||
level_table->reset_to_base(stats, char_class);
|
||||
level_table->advance_to_level(stats, target_level, char_class);
|
||||
|
||||
stats.esp = 40;
|
||||
stats.meseta = 300;
|
||||
@@ -516,8 +537,8 @@ void Client::create_challenge_overlay(Version version, size_t template_index, sh
|
||||
|
||||
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);
|
||||
level_table->reset_to_base(overlay->disp.stats, overlay->disp.visual.char_class);
|
||||
level_table->advance_to_level(overlay->disp.stats, tpl.level, overlay->disp.visual.char_class);
|
||||
|
||||
overlay->disp.stats.esp = 40;
|
||||
overlay->disp.stats.unknown_a3 = 10.0;
|
||||
@@ -606,10 +627,10 @@ 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) {
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return string_printf("system/players/system_%s.psosys", this->license->bb_username.c_str());
|
||||
return string_printf("system/players/system_%s.psosys", this->login->bb_license->username.c_str());
|
||||
}
|
||||
|
||||
string Client::character_filename(const std::string& bb_username, int8_t index) {
|
||||
@@ -622,55 +643,55 @@ string Client::character_filename(const std::string& bb_username, int8_t index)
|
||||
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::backup_character_filename(uint32_t account_id, size_t index) {
|
||||
return string_printf("system/players/backup_player_%" PRIu32 "_%zu.psochar", account_id, 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) {
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return this->character_filename(this->license->bb_username, (index < 0) ? this->bb_character_index : index);
|
||||
return this->character_filename(this->login->bb_license->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) {
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return string_printf("system/players/guild_cards_%s.psocard", this->license->bb_username.c_str());
|
||||
return string_printf("system/players/guild_cards_%s.psocard", this->login->bb_license->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) {
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return string_printf("system/players/shared_bank_%s.psobank", this->license->bb_username.c_str());
|
||||
return string_printf("system/players/shared_bank_%s.psobank", this->login->bb_license->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) {
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return string_printf("system/players/account_%s.nsa", this->license->bb_username.c_str());
|
||||
return string_printf("system/players/account_%s.nsa", this->login->bb_license->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) {
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
if (this->bb_character_index < 0) {
|
||||
@@ -678,7 +699,7 @@ string Client::legacy_player_filename() const {
|
||||
}
|
||||
return string_printf(
|
||||
"system/players/player_%s_%hhd.nsc",
|
||||
this->license->bb_username.c_str(),
|
||||
this->login->bb_license->username.c_str(),
|
||||
static_cast<int8_t>(this->bb_character_index + 1));
|
||||
}
|
||||
|
||||
@@ -698,7 +719,7 @@ void Client::load_all_files() {
|
||||
this->guild_card_data = make_shared<PSOBBGuildCardFile>();
|
||||
return;
|
||||
}
|
||||
if (!this->license) {
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("cannot load BB player data until client is logged in");
|
||||
}
|
||||
|
||||
@@ -737,6 +758,7 @@ void Client::load_all_files() {
|
||||
if (header.flag != 0x00000000) {
|
||||
throw runtime_error("incorrect flag in character file header");
|
||||
}
|
||||
static_assert(sizeof(PSOBBCharacterFile) + sizeof(PSOBBFullSystemFile) == 0x3994, ".psochar size is incorrect");
|
||||
this->character_data = make_shared<PSOBBCharacterFile>(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());
|
||||
@@ -815,11 +837,10 @@ void Client::load_all_files() {
|
||||
this->character_data->inventory = nsc_data.inventory;
|
||||
this->character_data->disp = nsc_data.disp;
|
||||
this->character_data->play_time_seconds = 0;
|
||||
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.guild_card_number = this->login->account->account_id;
|
||||
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;
|
||||
@@ -830,7 +851,7 @@ void Client::load_all_files() {
|
||||
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->tech_menu_shortcut_entries = nsc_data.tech_menu_shortcut_entries;
|
||||
this->character_data->quest_counters = nsc_data.quest_counters;
|
||||
if (nsa_data) {
|
||||
this->character_data->option_flags = nsa_data->option_flags;
|
||||
@@ -853,8 +874,8 @@ void Client::load_all_files() {
|
||||
if (this->character_data) {
|
||||
// Clear legacy play_time field
|
||||
this->character_data->disp.name.clear_after_bytes(0x18);
|
||||
this->license->auto_reply_message = this->character_data->auto_reply.decode();
|
||||
this->license->save();
|
||||
this->login->account->auto_reply_message = this->character_data->auto_reply.decode();
|
||||
this->login->account->save();
|
||||
this->last_play_time_update = now();
|
||||
}
|
||||
}
|
||||
@@ -901,10 +922,10 @@ void Client::save_character_file(
|
||||
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
|
||||
// file here, but that would cause Client to depend on Account, which
|
||||
// it currently does not. This data doesn't matter at all for correctness
|
||||
// within newserv, since it ignores this data entirely and instead generates
|
||||
// the membership struct from the team ID in the License and the team's state.
|
||||
// the membership struct from the team ID in the Account and the team's state.
|
||||
// So, writing correct data here would mostly be for compatibility with other
|
||||
// PSO servers. But if the other server is newserv, then this data would be
|
||||
// used anyway, and if it's not, then it would presumably have a different set
|
||||
@@ -944,8 +965,8 @@ void Client::save_guild_card_file() const {
|
||||
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);
|
||||
void Client::load_backup_character(uint32_t account_id, size_t index) {
|
||||
string filename = this->backup_character_filename(account_id, index);
|
||||
auto f = fopen_unique(filename, "rb");
|
||||
auto header = freadx<PSOCommandHeaderBB>(f.get());
|
||||
if (header.size != 0x399C) {
|
||||
|
||||
+29
-14
@@ -5,13 +5,13 @@
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "Account.hh"
|
||||
#include "Channel.hh"
|
||||
#include "CommandFormats.hh"
|
||||
#include "Episode3/BattleRecord.hh"
|
||||
#include "Episode3/Tournament.hh"
|
||||
#include "FileContentsCache.hh"
|
||||
#include "FunctionCompiler.hh"
|
||||
#include "License.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "PatchFileIndex.hh"
|
||||
@@ -38,7 +38,6 @@ public:
|
||||
|
||||
// Version-related flags
|
||||
CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002,
|
||||
LICENSE_WAS_CREATED = 0x0000000000000004, // Server-side only
|
||||
NO_D6_AFTER_LOBBY = 0x0000000000000100,
|
||||
NO_D6 = 0x0000000000000200,
|
||||
FORCE_ENGLISH_LANGUAGE_BB = 0x0000000000000400,
|
||||
@@ -62,6 +61,7 @@ public:
|
||||
HAS_EP3_MEDIA_UPDATES = 0x0000000010000000,
|
||||
USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000,
|
||||
HAS_GUILD_CARD_NUMBER = 0x0000000040000000,
|
||||
HAS_AUTO_PATCHES = 0x0000004000000000,
|
||||
AT_BANK_COUNTER = 0x0000000080000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE = 0x0040000000000000, // Server-side only
|
||||
@@ -181,8 +181,7 @@ public:
|
||||
uint64_t id;
|
||||
PrefixedLogger log;
|
||||
|
||||
// License & account
|
||||
std::shared_ptr<License> license;
|
||||
std::shared_ptr<Login> login;
|
||||
|
||||
// Network
|
||||
Channel channel;
|
||||
@@ -217,8 +216,9 @@ public:
|
||||
uint16_t card_battle_table_seat_number;
|
||||
uint16_t card_battle_table_seat_state;
|
||||
std::weak_ptr<Episode3::Tournament::Team> ep3_tournament_team;
|
||||
std::shared_ptr<Episode3::BattleRecord> ep3_prev_battle_record;
|
||||
std::shared_ptr<const Episode3::BattleRecord> ep3_prev_battle_record;
|
||||
std::shared_ptr<const Menu> last_menu_sent;
|
||||
uint32_t last_game_info_requested;
|
||||
struct JoinCommand {
|
||||
uint16_t command;
|
||||
uint32_t flag;
|
||||
@@ -253,9 +253,9 @@ public:
|
||||
RecentSwitchFlags recent_switch_flags; // used for switch assist
|
||||
bool can_chat;
|
||||
struct PendingCharacterExport {
|
||||
std::shared_ptr<const License> license;
|
||||
std::shared_ptr<const Account> dest_account;
|
||||
ssize_t character_index = -1;
|
||||
bool is_bb_conversion = false;
|
||||
std::shared_ptr<const BBLicense> dest_bb_license; // Only used for $bbchar; null for $savechar
|
||||
};
|
||||
std::unique_ptr<PendingCharacterExport> pending_character_export;
|
||||
std::deque<std::function<void(uint32_t, uint32_t)>> function_call_response_queue;
|
||||
@@ -268,10 +268,13 @@ public:
|
||||
Client(
|
||||
std::shared_ptr<Server> server,
|
||||
struct bufferevent* bev,
|
||||
uint64_t virtual_network_id,
|
||||
Version version,
|
||||
ServerBehavior server_behavior);
|
||||
~Client();
|
||||
|
||||
void update_channel_name();
|
||||
|
||||
void reschedule_save_game_data_event();
|
||||
void reschedule_ping_and_timeout_events();
|
||||
|
||||
@@ -282,8 +285,7 @@ public:
|
||||
return this->channel.language;
|
||||
}
|
||||
|
||||
void set_license(std::shared_ptr<License> l);
|
||||
void convert_license_to_temporary_if_nte();
|
||||
void convert_account_to_temporary_if_nte();
|
||||
|
||||
void sync_config();
|
||||
|
||||
@@ -293,13 +295,26 @@ public:
|
||||
std::shared_ptr<const TeamIndex::Team> team() const;
|
||||
|
||||
bool evaluate_quest_availability_expression(
|
||||
std::shared_ptr<const QuestAvailabilityExpression> expr,
|
||||
std::shared_ptr<const IntegralExpression> expr,
|
||||
std::shared_ptr<const Lobby> game,
|
||||
uint8_t event,
|
||||
uint8_t difficulty,
|
||||
size_t num_players,
|
||||
bool v1_present) const;
|
||||
bool can_see_quest(
|
||||
std::shared_ptr<const Quest> q,
|
||||
std::shared_ptr<const Lobby> game,
|
||||
uint8_t event,
|
||||
uint8_t difficulty,
|
||||
size_t num_players,
|
||||
bool v1_present) const;
|
||||
bool can_play_quest(
|
||||
std::shared_ptr<const Quest> q,
|
||||
std::shared_ptr<const Lobby> game,
|
||||
uint8_t event,
|
||||
uint8_t difficulty,
|
||||
size_t num_players,
|
||||
bool v1_present) const;
|
||||
bool can_see_quest(std::shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players, bool v1_present) const;
|
||||
bool can_play_quest(std::shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players, bool v1_present) const;
|
||||
|
||||
bool can_use_chat_commands() const;
|
||||
|
||||
@@ -341,7 +356,7 @@ public:
|
||||
|
||||
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);
|
||||
static std::string backup_character_filename(uint32_t account_id, size_t index);
|
||||
std::string character_filename(int8_t index = -1) const;
|
||||
std::string guild_card_filename() const;
|
||||
std::string shared_bank_filename() const;
|
||||
@@ -359,7 +374,7 @@ public:
|
||||
void save_character_file();
|
||||
void save_guild_card_file() const;
|
||||
|
||||
void load_backup_character(uint32_t serial_number, size_t index);
|
||||
void load_backup_character(uint32_t account_id, size_t index);
|
||||
void save_and_unload_character();
|
||||
|
||||
PlayerBank& current_bank();
|
||||
|
||||
+880
-841
File diff suppressed because it is too large
Load Diff
+5
-12
@@ -50,19 +50,12 @@ JSON CommonItemSet::Table::json() const {
|
||||
for (size_t z = 0; z < 0x64; z++) {
|
||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
||||
for (Episode episode : episodes) {
|
||||
JSON enemy_meseta_ranges_episode_json = JSON::dict();
|
||||
JSON enemy_type_drop_probs_episode_json = JSON::dict();
|
||||
JSON enemy_item_classes_episode_json = JSON::dict();
|
||||
for (auto type : enemy_types_for_rare_table_index(episode, z)) {
|
||||
string type_str = name_for_enum(type);
|
||||
enemy_meseta_ranges_episode_json.emplace(type_str, to_json(this->enemy_meseta_ranges[z]));
|
||||
enemy_type_drop_probs_episode_json.emplace(type_str, this->enemy_type_drop_probs[z]);
|
||||
enemy_item_classes_episode_json.emplace(type_str, this->enemy_item_classes[z]);
|
||||
string name = string_printf("%s:%s", abbreviation_for_episode(episode), name_for_enum(type));
|
||||
enemy_meseta_ranges_json.emplace(name, to_json(this->enemy_meseta_ranges[z]));
|
||||
enemy_type_drop_probs_json.emplace(name, this->enemy_type_drop_probs[z]);
|
||||
enemy_item_classes_json.emplace(name, this->enemy_item_classes[z]);
|
||||
}
|
||||
string name = name_for_episode(episode);
|
||||
enemy_meseta_ranges_json.emplace(name, std::move(enemy_meseta_ranges_episode_json));
|
||||
enemy_type_drop_probs_json.emplace(name, std::move(enemy_type_drop_probs_episode_json));
|
||||
enemy_item_classes_json.emplace(name, std::move(enemy_item_classes_episode_json));
|
||||
}
|
||||
}
|
||||
return JSON::dict({
|
||||
@@ -131,7 +124,7 @@ 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));
|
||||
const auto& offsets = r.pget<OffsetsT<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);
|
||||
|
||||
+17
-11
@@ -20,7 +20,7 @@ public:
|
||||
struct Range {
|
||||
IntT min;
|
||||
IntT max;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
parray<uint8_t, 0x0C> base_weapon_type_prob_table;
|
||||
parray<int8_t, 0x0C> subtype_base_table;
|
||||
@@ -53,7 +53,7 @@ public:
|
||||
void parse_itempt_t(const StringReader& r, bool is_v3);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct Offsets {
|
||||
struct OffsetsT {
|
||||
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;
|
||||
|
||||
@@ -254,7 +254,11 @@ public:
|
||||
/* 50 */ U32T box_item_class_prob_table_offset;
|
||||
|
||||
// There are several unused fields here.
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using Offsets = OffsetsT<false>;
|
||||
using OffsetsBE = OffsetsT<true>;
|
||||
check_struct_size(Offsets, 0x54);
|
||||
check_struct_size(OffsetsBE, 0x54);
|
||||
};
|
||||
|
||||
std::shared_ptr<const Table> get_table(Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) const;
|
||||
@@ -327,10 +331,12 @@ public:
|
||||
struct WeightTableEntry {
|
||||
ValueT value;
|
||||
WeightT weight;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
using WeightTableEntry8 = WeightTableEntry<uint8_t>;
|
||||
using WeightTableEntry32 = WeightTableEntry<be_uint32_t>;
|
||||
check_struct_size(WeightTableEntry8, 2);
|
||||
check_struct_size(WeightTableEntry32, 8);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<const std::string> data;
|
||||
@@ -340,7 +346,7 @@ protected:
|
||||
be_uint32_t offset;
|
||||
uint8_t entries_per_table;
|
||||
parray<uint8_t, 3> unused;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(TableSpec, 8);
|
||||
|
||||
RELFileSet(std::shared_ptr<const std::string> data);
|
||||
|
||||
@@ -381,7 +387,7 @@ public:
|
||||
Mode mode;
|
||||
uint8_t player_level_divisor_or_min_level;
|
||||
uint8_t max_level;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(TechDiskLevelEntry, 3);
|
||||
|
||||
std::pair<const uint8_t*, size_t> get_common_recovery_table(size_t index) const;
|
||||
std::pair<const WeightTableEntry8*, size_t> get_rare_recovery_table(size_t index) const;
|
||||
@@ -403,7 +409,7 @@ public:
|
||||
struct RangeTableEntry {
|
||||
be_uint32_t min;
|
||||
be_uint32_t max;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(RangeTableEntry, 8);
|
||||
|
||||
std::pair<const WeightTableEntry8*, size_t> get_weapon_type_table(size_t index) const;
|
||||
const parray<WeightTableEntry32, 6>* get_bonus_type_table(size_t which, size_t index) const;
|
||||
@@ -422,7 +428,7 @@ private:
|
||||
be_uint32_t special_mode_table; // [[{u32 value, u32 weight}](3)](8)
|
||||
be_uint32_t standard_grind_range_table; // [{u32 min, u32 max}](6)
|
||||
be_uint32_t favored_grind_range_table; // [{u32 min, u32 max}](6)
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Offsets, 0x20);
|
||||
|
||||
const Offsets* offsets;
|
||||
};
|
||||
@@ -455,11 +461,11 @@ private:
|
||||
uint8_t delta_index;
|
||||
uint8_t count_default;
|
||||
uint8_t count_favored;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(DeltaProbabilityEntry, 3);
|
||||
struct LuckTableEntry {
|
||||
uint8_t delta_index;
|
||||
int8_t luck;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(LuckTableEntry, 2);
|
||||
|
||||
struct Offsets {
|
||||
// Each section ID's favored weapon class has different probabilities than
|
||||
@@ -565,7 +571,7 @@ private:
|
||||
// In PSO V3, the bonus delta luck table is:
|
||||
// +10 => +15, +5 => +8, 0 => 0, -5 => -8, -10 => -15
|
||||
be_uint32_t bonus_delta_luck_offset; // LuckTableEntry[...]; ending with FF FF
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Offsets, 0x18);
|
||||
|
||||
const Offsets* offsets;
|
||||
|
||||
|
||||
+64
-18
@@ -438,6 +438,45 @@ string prs_compress_optimal(const string& data, ProgressCallback progress_fn) {
|
||||
return prs_compress_optimal(data.data(), data.size(), progress_fn);
|
||||
}
|
||||
|
||||
string prs_compress_pessimal(const void* vdata, size_t size) {
|
||||
const uint8_t* in_data = reinterpret_cast<const uint8_t*>(vdata);
|
||||
|
||||
// The worst possible encoding we can do is a literal byte when no byte with
|
||||
// the same value is within the window, or an extended copy if there is a byte
|
||||
// with the same value in the window.
|
||||
WindowIndex<0x1FFF, 1> window(in_data, size);
|
||||
LZSSInterleavedWriter w;
|
||||
for (size_t z = 0; z < size; z++) {
|
||||
auto match = window.get_best_match();
|
||||
if (match.second >= 1) {
|
||||
// Write extended copy
|
||||
int16_t offset = match.first - window.offset;
|
||||
w.write_control(false);
|
||||
w.flush_if_ready();
|
||||
w.write_control(true);
|
||||
uint16_t a = (offset << 3);
|
||||
w.write_data(a & 0xFF);
|
||||
w.write_data(a >> 8);
|
||||
w.write_data(0);
|
||||
} else {
|
||||
// Write literal
|
||||
w.write_control(true);
|
||||
w.write_data(in_data[z]);
|
||||
}
|
||||
w.flush_if_ready();
|
||||
window.advance();
|
||||
}
|
||||
|
||||
// Write stop command
|
||||
w.write_control(false);
|
||||
w.flush_if_ready();
|
||||
w.write_control(true);
|
||||
w.write_data(0);
|
||||
w.write_data(0);
|
||||
|
||||
return std::move(w.close());
|
||||
}
|
||||
|
||||
PRSCompressor::PRSCompressor(
|
||||
ssize_t compression_level, ProgressCallback progress_fn)
|
||||
: compression_level(compression_level),
|
||||
@@ -976,41 +1015,48 @@ void prs_disassemble(FILE* stream, const void* data, size_t size) {
|
||||
ControlStreamReader cr(r);
|
||||
|
||||
while (!r.eof()) {
|
||||
uint8_t buffered_bits = cr.buffered_bits();
|
||||
if (cr.read()) {
|
||||
fprintf(stream, "[%zX] literal %02hhX\n", output_bytes, r.get_u8());
|
||||
uint8_t literal_value = r.get_u8();
|
||||
fprintf(stream, "[%zX] %hhu> 1 %02hhX literal %02hhX\n",
|
||||
output_bytes, buffered_bits, literal_value, literal_value);
|
||||
output_bytes++;
|
||||
|
||||
} else {
|
||||
ssize_t offset;
|
||||
size_t count;
|
||||
const char* copy_type;
|
||||
|
||||
size_t count, read_offset;
|
||||
if (cr.read()) {
|
||||
uint16_t a = r.get_u8();
|
||||
a |= (r.get_u8() << 8);
|
||||
offset = (a >> 3) | (~0x1FFF);
|
||||
uint8_t a_low = r.get_u8();
|
||||
uint8_t a_high = r.get_u8();
|
||||
uint16_t a = (a_high << 8) | a_low;
|
||||
ssize_t offset = (a >> 3) | (~0x1FFF);
|
||||
if (offset == ~0x1FFF) {
|
||||
fprintf(stream, "[%zX] end\n", output_bytes);
|
||||
break;
|
||||
}
|
||||
if (a & 7) {
|
||||
copy_type = "long";
|
||||
count = (a & 7) + 2;
|
||||
read_offset = output_bytes + offset;
|
||||
fprintf(stream, "[%zX] %hhu> 01 %02hhX %02hhX long copy from %zd (offset=%zX) size=%zX\n",
|
||||
output_bytes, buffered_bits, a_low, a_high, offset, read_offset, count);
|
||||
} else {
|
||||
copy_type = "extended";
|
||||
count = r.get_u8() + 1;
|
||||
uint8_t count_u8 = r.get_u8();
|
||||
count = count_u8 + 1;
|
||||
read_offset = output_bytes + offset;
|
||||
fprintf(stream, "[%zX] %hhu> 01 %02hhX %02hhX %02hhX extended copy from %zd (offset=%zX) size=%zX\n",
|
||||
output_bytes, buffered_bits, a_low, a_high, count_u8, offset, read_offset, count);
|
||||
}
|
||||
|
||||
} else {
|
||||
copy_type = "short";
|
||||
count = cr.read() << 1;
|
||||
count = (count | cr.read()) + 2;
|
||||
offset = r.get_u8() | (~0xFF);
|
||||
bool first_bit = cr.read();
|
||||
bool second_bit = cr.read();
|
||||
uint8_t offset_u8 = r.get_u8();
|
||||
count = ((first_bit ? 2 : 0) | (second_bit ? 1 : 0)) + 2;
|
||||
ssize_t offset = offset_u8 | (~0xFF);
|
||||
read_offset = output_bytes + offset;
|
||||
fprintf(stream, "[%zX] %hhu> 00%c%c %02hhX short copy from %zd (offset=%zX) size=%zX\n",
|
||||
output_bytes, buffered_bits, first_bit ? '1' : '0', second_bit ? '1' : '0', offset_u8, offset, read_offset, count);
|
||||
}
|
||||
|
||||
size_t read_offset = output_bytes + offset;
|
||||
fprintf(stream, "[%zX] %s copy %zX\n", output_bytes, copy_type, count);
|
||||
|
||||
if (read_offset >= output_bytes) {
|
||||
throw runtime_error("backreference offset beyond beginning of output");
|
||||
}
|
||||
|
||||
@@ -177,6 +177,10 @@ std::string prs_compress_indexed(
|
||||
std::string prs_compress_optimal(const void* vdata, size_t size, ProgressCallback progress_fn = nullptr);
|
||||
std::string prs_compress_optimal(const std::string& data, ProgressCallback progress_fn = nullptr);
|
||||
|
||||
// Compresses data using PRS to the LARGEST possible output size. There is no
|
||||
// practical use for this function except for amusement.
|
||||
std::string prs_compress_pessimal(const void* vdata, size_t size);
|
||||
|
||||
// Decompresses PRS-compressed data.
|
||||
struct PRSDecompressResult {
|
||||
std::string data;
|
||||
|
||||
@@ -1386,3 +1386,74 @@ void dc_serial_number_speed_test(uint64_t seed) {
|
||||
fprintf(stderr, "Fast vs. slow speedup: %zux\n", static_cast<size_t>(time_slow / time_fast));
|
||||
fprintf(stderr, "Disagreements: %zu\n", num_disagreements);
|
||||
}
|
||||
|
||||
string decrypt_dp_address_jpn(
|
||||
const string& executable,
|
||||
const string& values,
|
||||
const string& indexes) {
|
||||
StringReader values_r(values);
|
||||
StringReader indexes_r(indexes);
|
||||
|
||||
size_t fixup_values_offset = values_r.pget_u32l(0x3FFC) - 0x8C004000;
|
||||
size_t fixup_steps_offset = indexes_r.pget_u32l(0x3BFC) - 0x8C008400;
|
||||
StringReader fixup_values_r = values_r.sub(fixup_values_offset);
|
||||
StringReader fixup_steps_r = indexes_r.sub(fixup_steps_offset);
|
||||
|
||||
auto decrypted = decrypt_pr2_data<false>(executable);
|
||||
size_t fixup_offset = 0;
|
||||
while (fixup_steps_r.get_u8(false)) {
|
||||
fixup_offset += (fixup_steps_r.get_u8() << 2);
|
||||
fixup_steps_r.skip(1);
|
||||
if (fixup_offset + 4 > decrypted.compressed_data.size()) {
|
||||
throw runtime_error("fixup beyond end of compressed data");
|
||||
}
|
||||
*reinterpret_cast<le_uint32_t*>(decrypted.compressed_data.data() + fixup_offset) = fixup_values_r.get_u32l();
|
||||
}
|
||||
|
||||
return prs_decompress(decrypted.compressed_data);
|
||||
}
|
||||
|
||||
EncryptedDCv2Executables encrypt_dp_address_jpn(const string& executable, const string& indexes) {
|
||||
EncryptedDCv2Executables ret;
|
||||
|
||||
string compressed = prs_compress(executable);
|
||||
ret.executable = encrypt_pr2_data<false>(compressed, executable.size(), random_object<uint32_t>() & 0x7FFFFF7F);
|
||||
|
||||
StringReader indexes_r(indexes);
|
||||
size_t fixup_steps_offset = indexes_r.pget_u32l(0x3BFC) - 0x8C008400;
|
||||
ret.indexes = indexes;
|
||||
ret.indexes.at(fixup_steps_offset) = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string crypt_dp_address_jpn_simple(const std::string& data, int64_t mask_key) {
|
||||
if (data.size() & 3) {
|
||||
throw runtime_error("size is not a multiple of 4");
|
||||
}
|
||||
|
||||
StringReader r(data);
|
||||
if (mask_key < 0) {
|
||||
unordered_map<uint32_t, size_t> key_freq;
|
||||
while (!r.eof()) {
|
||||
key_freq[r.get_u32l()] += 1;
|
||||
}
|
||||
size_t max_v = 0;
|
||||
for (const auto& it : key_freq) {
|
||||
if (it.second > max_v) {
|
||||
max_v = it.second;
|
||||
mask_key = it.first;
|
||||
}
|
||||
}
|
||||
if (mask_key < 0) {
|
||||
throw runtime_error("cannot determine mask key");
|
||||
}
|
||||
log_info("Determined %08" PRIX64 " to be the most likely mask key", mask_key);
|
||||
r.go(0);
|
||||
}
|
||||
|
||||
StringWriter w;
|
||||
while (!r.eof()) {
|
||||
w.put_u32l(r.get_u32l() ^ mask_key);
|
||||
}
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
@@ -21,3 +21,16 @@ std::string generate_dc_serial_number(uint8_t domain, uint8_t subdomain = 0xFF);
|
||||
std::unordered_map<uint32_t, std::string> generate_all_dc_serial_numbers(uint8_t domain = 0xFF, uint8_t subdomain = 0xFF);
|
||||
|
||||
void dc_serial_number_speed_test(uint64_t seed = 0xFFFFFFFFFFFFFFFF);
|
||||
|
||||
struct EncryptedDCv2Executables {
|
||||
std::string executable;
|
||||
std::string indexes;
|
||||
};
|
||||
|
||||
std::string decrypt_dp_address_jpn(
|
||||
const std::string& dp_address_jpn_data,
|
||||
const std::string& iwashi_sea_data,
|
||||
const std::string& katsuo_sea_data);
|
||||
EncryptedDCv2Executables encrypt_dp_address_jpn(const std::string& executable, const std::string& indexes);
|
||||
|
||||
std::string crypt_dp_address_jpn_simple(const std::string& data, int64_t seed = -1);
|
||||
|
||||
+10
-7
@@ -19,10 +19,13 @@ using namespace std;
|
||||
|
||||
DNSServer::DNSServer(
|
||||
shared_ptr<struct event_base> base,
|
||||
uint32_t local_connect_address, uint32_t external_connect_address)
|
||||
uint32_t local_connect_address,
|
||||
uint32_t external_connect_address,
|
||||
shared_ptr<const IPV4RangeSet> banned_ipv4_ranges)
|
||||
: base(base),
|
||||
local_connect_address(local_connect_address),
|
||||
external_connect_address(external_connect_address) {}
|
||||
external_connect_address(external_connect_address),
|
||||
banned_ipv4_ranges(banned_ipv4_ranges) {}
|
||||
|
||||
DNSServer::~DNSServer() {
|
||||
for (const auto& it : this->fd_to_receive_event) {
|
||||
@@ -55,8 +58,7 @@ void DNSServer::dispatch_on_receive_message(evutil_socket_t fd,
|
||||
reinterpret_cast<DNSServer*>(ctx)->on_receive_message(fd, events);
|
||||
}
|
||||
|
||||
string DNSServer::response_for_query(
|
||||
const void* vdata, size_t size, uint32_t resolved_address) {
|
||||
string DNSServer::response_for_query(const void* vdata, size_t size, uint32_t resolved_address) {
|
||||
if (size < 0x0C) {
|
||||
throw invalid_argument("query too small");
|
||||
}
|
||||
@@ -82,7 +84,7 @@ string DNSServer::response_for_query(
|
||||
|
||||
void DNSServer::on_receive_message(int fd, short) {
|
||||
for (;;) {
|
||||
sockaddr_in remote;
|
||||
struct sockaddr_storage remote;
|
||||
socklen_t remote_size = sizeof(sockaddr_in);
|
||||
memset(&remote, 0, remote_size);
|
||||
|
||||
@@ -104,9 +106,10 @@ void DNSServer::on_receive_message(int fd, short) {
|
||||
dns_server_log.warning("input query too small");
|
||||
print_data(stderr, input.data(), bytes);
|
||||
|
||||
} else {
|
||||
} else if (!this->banned_ipv4_ranges->check(remote)) {
|
||||
input.resize(bytes);
|
||||
uint32_t remote_address = ntohl(remote.sin_addr.s_addr);
|
||||
const sockaddr_in* remote_sin = reinterpret_cast<const sockaddr_in*>(&remote);
|
||||
uint32_t remote_address = ntohl(remote_sin->sin_addr.s_addr);
|
||||
uint32_t connect_address = is_local_address(remote_address)
|
||||
? this->local_connect_address
|
||||
: this->external_connect_address;
|
||||
|
||||
+14
-6
@@ -7,29 +7,37 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "IPV4RangeSet.hh"
|
||||
|
||||
class DNSServer {
|
||||
public:
|
||||
DNSServer(std::shared_ptr<struct event_base> base,
|
||||
uint32_t local_connect_address, uint32_t external_connect_address);
|
||||
DNSServer(
|
||||
std::shared_ptr<struct event_base> base,
|
||||
uint32_t local_connect_address,
|
||||
uint32_t external_connect_address,
|
||||
std::shared_ptr<const IPV4RangeSet> banned_ipv4_ranges);
|
||||
DNSServer(const DNSServer&) = delete;
|
||||
DNSServer(DNSServer&&) = delete;
|
||||
virtual ~DNSServer();
|
||||
|
||||
inline void set_banned_ipv4_ranges(std::shared_ptr<const IPV4RangeSet> banned_ipv4_ranges) {
|
||||
this->banned_ipv4_ranges = banned_ipv4_ranges;
|
||||
}
|
||||
|
||||
void listen(const std::string& socket_path);
|
||||
void listen(const std::string& addr, int port);
|
||||
void listen(int port);
|
||||
void add_socket(int fd);
|
||||
|
||||
static std::string response_for_query(
|
||||
const void* vdata, size_t size, uint32_t resolved_address);
|
||||
static std::string response_for_query(
|
||||
const std::string& query, uint32_t resolved_address);
|
||||
static std::string response_for_query(const void* vdata, size_t size, uint32_t resolved_address);
|
||||
static std::string response_for_query(const std::string& query, uint32_t resolved_address);
|
||||
|
||||
private:
|
||||
std::shared_ptr<struct event_base> base;
|
||||
std::unordered_map<int, std::unique_ptr<struct event, void (*)(struct event*)>> fd_to_receive_event;
|
||||
uint32_t local_connect_address;
|
||||
uint32_t external_connect_address;
|
||||
std::shared_ptr<const IPV4RangeSet> banned_ipv4_ranges;
|
||||
|
||||
static void dispatch_on_receive_message(evutil_socket_t fd, short events, void* ctx);
|
||||
void on_receive_message(int fd, short event);
|
||||
|
||||
@@ -9,6 +9,12 @@ using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
void BattleRecord::PlayerEntry::print(FILE* stream) const {
|
||||
// TODO: Format this nicely somehow. Maybe factor out the functions in
|
||||
// QuestScript that format some of these structures
|
||||
print_data(stream, this, sizeof(this));
|
||||
}
|
||||
|
||||
BattleRecord::Event::Event(StringReader& r) {
|
||||
this->type = r.get<Event::Type>();
|
||||
this->timestamp = r.get_u64l();
|
||||
@@ -32,6 +38,7 @@ BattleRecord::Event::Event(StringReader& r) {
|
||||
case Event::Type::GAME_COMMAND:
|
||||
case Event::Type::BATTLE_COMMAND:
|
||||
case Event::Type::EP3_GAME_COMMAND:
|
||||
case Event::Type::SERVER_DATA_COMMAND:
|
||||
this->data = r.read(r.get_u16l());
|
||||
break;
|
||||
default:
|
||||
@@ -64,6 +71,7 @@ void BattleRecord::Event::serialize(StringWriter& w) const {
|
||||
case Event::Type::GAME_COMMAND:
|
||||
case Event::Type::BATTLE_COMMAND:
|
||||
case Event::Type::EP3_GAME_COMMAND:
|
||||
case Event::Type::SERVER_DATA_COMMAND:
|
||||
w.put_u16l(this->data.size());
|
||||
w.write(this->data);
|
||||
break;
|
||||
@@ -72,6 +80,51 @@ void BattleRecord::Event::serialize(StringWriter& w) const {
|
||||
}
|
||||
}
|
||||
|
||||
void BattleRecord::Event::print(FILE* stream) const {
|
||||
string time_str = format_time(this->timestamp);
|
||||
fprintf(stream, "Event @%016" PRIX64 " (%s) ", this->timestamp, time_str.c_str());
|
||||
switch (this->type) {
|
||||
case Type::PLAYER_JOIN:
|
||||
fprintf(stream, "PLAYER_JOIN %02" PRIX32 "\n", this->players[0].lobby_data.client_id.load());
|
||||
this->players[0].print(stream);
|
||||
break;
|
||||
case Type::PLAYER_LEAVE:
|
||||
fprintf(stream, "PLAYER_LEAVE %02hhu\n", this->leaving_client_id);
|
||||
break;
|
||||
case Type::SET_INITIAL_PLAYERS:
|
||||
fprintf(stream, "SET_INITIAL_PLAYERS");
|
||||
for (const auto& player : this->players) {
|
||||
fprintf(stream, " %02" PRIX32, player.lobby_data.client_id.load());
|
||||
}
|
||||
for (const auto& player : this->players) {
|
||||
player.print(stream);
|
||||
}
|
||||
break;
|
||||
case Type::BATTLE_COMMAND:
|
||||
fprintf(stream, "BATTLE_COMMAND\n");
|
||||
print_data(stream, this->data);
|
||||
break;
|
||||
case Type::GAME_COMMAND:
|
||||
fprintf(stream, "GAME_COMMAND\n");
|
||||
print_data(stream, this->data);
|
||||
break;
|
||||
case Type::EP3_GAME_COMMAND:
|
||||
fprintf(stream, "EP3_GAME_COMMAND\n");
|
||||
print_data(stream, this->data);
|
||||
break;
|
||||
case Type::CHAT_MESSAGE:
|
||||
fprintf(stream, "CHAT_MESSAGE %08" PRIX32 "\n", this->guild_card_number);
|
||||
print_data(stream, this->data);
|
||||
break;
|
||||
case Type::SERVER_DATA_COMMAND:
|
||||
fprintf(stream, "SERVER_DATA_COMMAND\n");
|
||||
print_data(stream, this->data);
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("unknown event type in battle record");
|
||||
}
|
||||
}
|
||||
|
||||
BattleRecord::BattleRecord(uint32_t behavior_flags)
|
||||
: is_writable(true),
|
||||
behavior_flags(behavior_flags),
|
||||
@@ -84,14 +137,23 @@ BattleRecord::BattleRecord(const string& data)
|
||||
battle_start_timestamp(0),
|
||||
battle_end_timestamp(0) {
|
||||
StringReader r(data);
|
||||
|
||||
uint64_t signature = r.get_u64l();
|
||||
if (signature != this->SIGNATURE) {
|
||||
bool has_random_stream;
|
||||
if (signature == this->SIGNATURE_V1) {
|
||||
has_random_stream = false;
|
||||
} else if (signature == this->SIGNATURE_V2) {
|
||||
has_random_stream = true;
|
||||
} else {
|
||||
throw runtime_error("incorrect battle record signature");
|
||||
}
|
||||
|
||||
this->battle_start_timestamp = r.get_u64l();
|
||||
this->battle_end_timestamp = r.get_u64l();
|
||||
this->behavior_flags = r.get_u32l();
|
||||
if (has_random_stream) {
|
||||
this->random_stream = r.read(r.get_u32l());
|
||||
}
|
||||
while (!r.eof()) {
|
||||
this->events.emplace_back(r);
|
||||
}
|
||||
@@ -99,10 +161,12 @@ BattleRecord::BattleRecord(const string& data)
|
||||
|
||||
string BattleRecord::serialize() const {
|
||||
StringWriter w;
|
||||
w.put_u64l(this->SIGNATURE);
|
||||
w.put_u64l(this->SIGNATURE_V2);
|
||||
w.put_u64l(this->battle_start_timestamp);
|
||||
w.put_u64l(this->battle_end_timestamp);
|
||||
w.put_u32l(this->behavior_flags);
|
||||
w.put_u32l(this->random_stream.size());
|
||||
w.write(this->random_stream);
|
||||
for (const auto& ev : this->events) {
|
||||
ev.serialize(w);
|
||||
}
|
||||
@@ -187,6 +251,10 @@ void BattleRecord::add_chat_message(
|
||||
ev.data = std::move(data);
|
||||
}
|
||||
|
||||
void BattleRecord::add_random_data(const void* data, size_t size) {
|
||||
this->random_stream.append(reinterpret_cast<const char*>(data), size);
|
||||
}
|
||||
|
||||
bool BattleRecord::is_map_definition_event(const Event& ev) {
|
||||
if (ev.type == Event::Type::BATTLE_COMMAND) {
|
||||
auto& header = check_size_t<G_CardBattleCommandHeader>(ev.data, 0xFFFF);
|
||||
@@ -268,12 +336,30 @@ void BattleRecord::set_battle_start_timestamp() {
|
||||
}
|
||||
}
|
||||
this->events = std::move(new_events);
|
||||
|
||||
// Clear any existing random data (there shouldn't be any)
|
||||
this->random_stream.clear();
|
||||
}
|
||||
|
||||
void BattleRecord::set_battle_end_timestamp() {
|
||||
this->battle_end_timestamp = now();
|
||||
}
|
||||
|
||||
void BattleRecord::print(FILE* stream) const {
|
||||
string start_str = format_time(this->battle_start_timestamp);
|
||||
string end_str = format_time(this->battle_end_timestamp);
|
||||
fprintf(stream, "BattleRecord %s behavior_flags=%08" PRIX32 " start=%016" PRIX64 " (%s) end=%016" PRIX64 " (%s); %zu events\n",
|
||||
this->is_writable ? "writable" : "read-only",
|
||||
this->behavior_flags,
|
||||
this->battle_start_timestamp,
|
||||
start_str.c_str(),
|
||||
this->battle_end_timestamp,
|
||||
end_str.c_str(), this->events.size());
|
||||
for (const auto& event : this->events) {
|
||||
event.print(stream);
|
||||
}
|
||||
}
|
||||
|
||||
BattleRecordPlayer::BattleRecordPlayer(
|
||||
shared_ptr<const BattleRecord> rec,
|
||||
shared_ptr<struct event_base> base)
|
||||
@@ -356,6 +442,10 @@ void BattleRecordPlayer::schedule_events() {
|
||||
case BattleRecord::Event::Type::CHAT_MESSAGE:
|
||||
send_prepared_chat_message(l, ev.guild_card_number, ev.data);
|
||||
break;
|
||||
case BattleRecord::Event::Type::SERVER_DATA_COMMAND:
|
||||
// These are not replayed, since the battle record also contains
|
||||
// the results of these commands.
|
||||
break;
|
||||
}
|
||||
this->event_it++;
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ public:
|
||||
PlayerInventory inventory;
|
||||
PlayerDispDataDCPCV3 disp;
|
||||
le_uint32_t level;
|
||||
} __attribute__((packed));
|
||||
|
||||
void print(FILE* stream) const;
|
||||
} __packed_ws__(PlayerEntry, 0x440);
|
||||
|
||||
struct Event {
|
||||
enum class Type : uint8_t {
|
||||
@@ -35,6 +37,7 @@ public:
|
||||
GAME_COMMAND = 4,
|
||||
EP3_GAME_COMMAND = 5,
|
||||
CHAT_MESSAGE = 6,
|
||||
SERVER_DATA_COMMAND = 7,
|
||||
};
|
||||
|
||||
// Fields used for all events
|
||||
@@ -52,6 +55,7 @@ public:
|
||||
Event() = default;
|
||||
explicit Event(StringReader& r);
|
||||
void serialize(StringWriter& w) const;
|
||||
void print(FILE* stream) const;
|
||||
};
|
||||
|
||||
explicit BattleRecord(uint32_t behavior_flags);
|
||||
@@ -72,6 +76,7 @@ public:
|
||||
void add_command(Event::Type type, const void* data, size_t size);
|
||||
void add_command(Event::Type type, std::string&& data);
|
||||
void add_chat_message(uint32_t guild_card_number, std::string&& data);
|
||||
void add_random_data(const void* data, size_t size);
|
||||
// This function collapses all the existing player join/leave events into a
|
||||
// single SET_INITIAL_PLAYERS event, and deletes all events before the latest
|
||||
// BATTLE_COMMAND command that specifies the battle map. This should provide a
|
||||
@@ -79,8 +84,11 @@ public:
|
||||
void set_battle_start_timestamp();
|
||||
void set_battle_end_timestamp();
|
||||
|
||||
void print(FILE* stream) const;
|
||||
|
||||
private:
|
||||
static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC50;
|
||||
static constexpr uint64_t SIGNATURE_V1 = 0x14C946D56D1DAC50;
|
||||
static constexpr uint64_t SIGNATURE_V2 = 0xD01E5EC12853C377;
|
||||
|
||||
static bool is_map_definition_event(const Event& ev);
|
||||
|
||||
@@ -90,6 +98,7 @@ private:
|
||||
uint64_t battle_start_timestamp;
|
||||
uint64_t battle_end_timestamp;
|
||||
std::deque<Event> events;
|
||||
std::string random_stream;
|
||||
|
||||
friend class BattleRecordPlayer;
|
||||
};
|
||||
@@ -114,6 +123,7 @@ private:
|
||||
std::shared_ptr<struct event_base> base;
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
std::shared_ptr<struct event> next_command_ev;
|
||||
StringReader random_r;
|
||||
};
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
@@ -3859,8 +3859,10 @@ void CardSpecial::check_for_defense_interference(
|
||||
shared_ptr<const Card> attacker_card,
|
||||
shared_ptr<Card> target_card,
|
||||
int16_t* inout_unknown_p4) {
|
||||
auto s = this->server();
|
||||
|
||||
// Note: This check is not part of the original implementation.
|
||||
if (this->server()->options.behavior_flags & BehaviorFlag::DISABLE_INTERFERENCE) {
|
||||
if (s->options.behavior_flags & BehaviorFlag::DISABLE_INTERFERENCE) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3871,13 +3873,13 @@ void CardSpecial::check_for_defense_interference(
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t ally_sc_card_ref = this->server()->ruler_server->get_ally_sc_card_ref(
|
||||
uint16_t ally_sc_card_ref = s->ruler_server->get_ally_sc_card_ref(
|
||||
target_card->get_card_ref());
|
||||
if (ally_sc_card_ref == 0xFFFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto ally_sc = this->server()->card_for_set_card_ref(ally_sc_card_ref);
|
||||
auto ally_sc = s->card_for_set_card_ref(ally_sc_card_ref);
|
||||
if (!ally_sc || (ally_sc->card_flags & 2)) {
|
||||
return;
|
||||
}
|
||||
@@ -3892,17 +3894,17 @@ void CardSpecial::check_for_defense_interference(
|
||||
return;
|
||||
}
|
||||
|
||||
auto ally_hes = this->server()->ruler_server->get_hand_and_equip_state_for_client_id(target_ally_client_id);
|
||||
if (!ally_hes || (!(this->server()->options.behavior_flags & BehaviorFlag::ALLOW_NON_COM_INTERFERENCE) && !ally_hes->is_cpu_player)) {
|
||||
auto ally_hes = s->ruler_server->get_hand_and_equip_state_for_client_id(target_ally_client_id);
|
||||
if (!ally_hes || (!(s->options.behavior_flags & BehaviorFlag::ALLOW_NON_COM_INTERFERENCE) && !ally_hes->is_cpu_player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t target_card_id = this->server()->card_id_for_card_ref(target_card->get_card_ref());
|
||||
uint16_t target_card_id = s->card_id_for_card_ref(target_card->get_card_ref());
|
||||
if (target_card_id == 0xFFFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t ally_sc_card_id = this->server()->card_id_for_card_ref(ally_sc_card_ref);
|
||||
uint16_t ally_sc_card_id = s->card_id_for_card_ref(ally_sc_card_ref);
|
||||
if (ally_sc_card_id == 0xFFFF) {
|
||||
return;
|
||||
}
|
||||
@@ -3916,7 +3918,7 @@ void CardSpecial::check_for_defense_interference(
|
||||
}
|
||||
auto entry = get_interference_probability_entry(
|
||||
target_card_id, ally_sc_card_id, false);
|
||||
if (!entry || (this->server()->get_random(99) >= entry->defense_probability)) {
|
||||
if (!entry || (s->get_random(99) >= entry->defense_probability)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3928,7 +3930,7 @@ void CardSpecial::check_for_defense_interference(
|
||||
cmd.effect.target_card_ref = target_card->get_card_ref();
|
||||
cmd.effect.value = 0;
|
||||
cmd.effect.operation = 0x7D;
|
||||
this->server()->send(cmd);
|
||||
s->send(cmd);
|
||||
if (inout_unknown_p4) {
|
||||
*inout_unknown_p4 = 0;
|
||||
target_card->action_metadata.set_flags(0x10);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "../Text.hh"
|
||||
#include "DataIndexes.hh"
|
||||
#include "Server.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
@@ -94,7 +95,7 @@ public:
|
||||
void print(FILE* stream) const;
|
||||
|
||||
uint32_t at(size_t index) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(AttackEnvStats, 0x9C);
|
||||
|
||||
CardSpecial(std::shared_ptr<Server> server);
|
||||
std::shared_ptr<Server> server();
|
||||
|
||||
+22
-22
@@ -447,7 +447,7 @@ struct Location {
|
||||
|
||||
void clear();
|
||||
void clear_FF();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Location, 4);
|
||||
|
||||
struct CardDefinition {
|
||||
struct Stat {
|
||||
@@ -470,7 +470,7 @@ struct CardDefinition {
|
||||
void decode_code();
|
||||
std::string str() const;
|
||||
JSON json() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Stat, 4);
|
||||
|
||||
struct Effect {
|
||||
// effect_num is the 1-based index of this effect within the card definition
|
||||
@@ -507,7 +507,7 @@ struct CardDefinition {
|
||||
static std::string str_for_arg(const std::string& arg);
|
||||
std::string str(const char* separator = ", ", const TextSet* text_archive = nullptr) const;
|
||||
JSON json() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Effect, 0x20);
|
||||
|
||||
/* 0000 */ be_uint32_t card_id;
|
||||
/* 0004 */ pstring<TextEncoding::SJIS, 0x40> jp_name;
|
||||
@@ -774,7 +774,7 @@ struct CardDefinition {
|
||||
void decode_range();
|
||||
std::string str(bool single_line = true, const TextSet* text_archive = nullptr) const;
|
||||
JSON json() const;
|
||||
} __attribute__((packed)); // 0x128 bytes in total
|
||||
} __packed_ws__(CardDefinition, 0x128);
|
||||
|
||||
struct CardDefinitionsFooter {
|
||||
// Technically the card definitions file is a REL file, so the last 0x20 bytes
|
||||
@@ -790,7 +790,7 @@ struct CardDefinitionsFooter {
|
||||
/* 48 */ be_uint32_t footer_offset;
|
||||
/* 4C */ parray<be_uint32_t, 3> unused2;
|
||||
/* 58 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(CardDefinitionsFooter, 0x58);
|
||||
|
||||
struct DeckDefinition {
|
||||
/* 00 */ pstring<TextEncoding::MARKED, 0x10> name;
|
||||
@@ -810,7 +810,7 @@ struct DeckDefinition {
|
||||
/* 82 */ uint8_t second;
|
||||
/* 83 */ uint8_t unknown_a2;
|
||||
/* 84 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(DeckDefinition, 0x84);
|
||||
|
||||
struct PlayerConfig {
|
||||
// The game splits this internally into two structures. The first column of
|
||||
@@ -828,7 +828,7 @@ struct PlayerConfig {
|
||||
// earlier version, this was the offline records structure, but they later
|
||||
// decided to just count online and offline records together in the main
|
||||
// records structure and didn't remove the codepath that reads from this.
|
||||
/* 0138:---- */ PlayerRecords_Battle<true> unused_offline_records;
|
||||
/* 0138:---- */ PlayerRecordsBattleBE unused_offline_records;
|
||||
/* 0150:---- */ parray<uint8_t, 4> unknown_a4;
|
||||
// The PlayerDataSegment structure begins here. In newserv, we combine this
|
||||
// structure into PlayerConfig since the two are always used together.
|
||||
@@ -879,7 +879,7 @@ struct PlayerConfig {
|
||||
struct PlayerReference {
|
||||
/* 00 */ be_uint32_t guild_card_number;
|
||||
/* 04 */ pstring<TextEncoding::MARKED, 0x18> name;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PlayerReference, 0x1C);
|
||||
// This array is updated when a battle is started (via a 6xB4x05 command). The
|
||||
// client adds the opposing players' info to ths first two entries here if the
|
||||
// opponents are human. (The existing entries are always moved back by two
|
||||
@@ -902,7 +902,7 @@ struct PlayerConfig {
|
||||
|
||||
void decrypt();
|
||||
void encrypt(uint8_t basis);
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PlayerConfig, 0x2350);
|
||||
|
||||
enum class HPType : uint8_t {
|
||||
DEFEAT_PLAYER = 0,
|
||||
@@ -972,7 +972,7 @@ struct Rules {
|
||||
std::pair<uint8_t, uint8_t> def_dice_range(bool is_1p_2v1) const;
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Rules, 0x14);
|
||||
|
||||
struct RulesTrial {
|
||||
// Most fields here have the same meanings as in the final version.
|
||||
@@ -996,7 +996,7 @@ struct RulesTrial {
|
||||
RulesTrial() = default;
|
||||
RulesTrial(const Rules&);
|
||||
operator Rules() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(RulesTrial, 0x0C);
|
||||
|
||||
struct StateFlags {
|
||||
/* 00 */ le_uint16_t turn_num;
|
||||
@@ -1018,7 +1018,7 @@ struct StateFlags {
|
||||
bool operator!=(const StateFlags& other) const;
|
||||
void clear();
|
||||
void clear_FF();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(StateFlags, 0x18);
|
||||
|
||||
struct MapList {
|
||||
be_uint32_t num_maps;
|
||||
@@ -1046,18 +1046,18 @@ struct MapList {
|
||||
/* 021C */ uint8_t map_category;
|
||||
/* 021D */ parray<uint8_t, 3> unused;
|
||||
/* 0220 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Entry, 0x220);
|
||||
|
||||
// Variable-length fields:
|
||||
// Entry entries[num_maps];
|
||||
// char strings[...EOF]; // Null-terminated strings, pointed to by offsets in Entry structs
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(MapList, 0x10);
|
||||
|
||||
struct CompressedMapHeader { // .mnm file format
|
||||
le_uint32_t map_number;
|
||||
le_uint32_t compressed_data_size;
|
||||
// Compressed data immediately follows (which decompresses to a MapDefinition)
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(CompressedMapHeader, 8);
|
||||
|
||||
struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
// If tag is not 0x00000100, the game considers the map to be corrupt in
|
||||
@@ -1155,7 +1155,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
|
||||
std::string str() const;
|
||||
JSON json() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(CameraSpec, 0x48);
|
||||
|
||||
// This array specifies the camera zone maps. A camera zone map is a subset of
|
||||
// the main map (specified in map_tiles). Tiles that are part of each camera
|
||||
@@ -1213,7 +1213,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 18 */ parray<be_uint16_t, 0x20> card_ids; // Last one appears to always be FFFF
|
||||
/* 58 */
|
||||
JSON json(uint8_t language) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(NPCDeck, 0x58);
|
||||
/* 1FE8 */ parray<NPCDeck, 3> npc_decks; // Unused if name[0] == 0
|
||||
|
||||
// These are almost (but not quite) the same format as the entries in
|
||||
@@ -1229,7 +1229,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 0018 */ parray<be_uint16_t, 0x7E> params;
|
||||
/* 0114 */
|
||||
JSON json(uint8_t language) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(AIParams, 0x114);
|
||||
/* 20F0 */ parray<AIParams, 3> npc_ai_params; // Unused if name[0] == 0
|
||||
|
||||
/* 242C */ parray<uint8_t, 8> unknown_a7;
|
||||
@@ -1282,7 +1282,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 0004 */ parray<pstring<TextEncoding::MARKED, 0x40>, 4> strings;
|
||||
/* 0104 */
|
||||
JSON json(uint8_t language) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(DialogueSet, 0x104);
|
||||
|
||||
// There are up to 0x10 of these per valid NPC, but only the first 13 of them
|
||||
// are used, since each one must have a unique value for .when and the values
|
||||
@@ -1365,7 +1365,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
bool operator==(const EntryState& other) const = default;
|
||||
bool operator!=(const EntryState& other) const = default;
|
||||
JSON json() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(EntryState, 2);
|
||||
/* 5A10 */ parray<EntryState, 4> entry_states;
|
||||
/* 5A18 */
|
||||
|
||||
@@ -1381,7 +1381,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
|
||||
std::string str(const CardIndex* card_index, uint8_t language) const;
|
||||
JSON json(uint8_t language) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(MapDefinition, 0x5A18);
|
||||
|
||||
struct MapDefinitionTrial {
|
||||
// This is the format of Episode 3 Trial Edition maps. See the comments in
|
||||
@@ -1430,7 +1430,7 @@ struct MapDefinitionTrial {
|
||||
|
||||
MapDefinitionTrial(const MapDefinition& map);
|
||||
operator MapDefinition() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(MapDefinitionTrial, 0x41A0);
|
||||
|
||||
struct COMDeckDefinition {
|
||||
size_t index;
|
||||
|
||||
+46
-32
@@ -1,5 +1,7 @@
|
||||
#include "DeckState.hh"
|
||||
|
||||
#include "Server.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
@@ -84,43 +86,45 @@ bool DeckState::draw_card_by_ref(uint16_t card_ref) {
|
||||
}
|
||||
|
||||
uint8_t index = index_for_card_ref(card_ref);
|
||||
if (index > this->entries.size()) {
|
||||
if (index >= this->entries.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->entries[index].state == CardState::DISCARDED) {
|
||||
auto& entry = this->entries[index];
|
||||
if (entry.state == CardState::DISCARDED) {
|
||||
// If the card is discarded, then it should be before the draw index, and we
|
||||
// can just change its state.
|
||||
this->entries[index].state = CardState::IN_HAND;
|
||||
entry.state = CardState::IN_HAND;
|
||||
return true;
|
||||
}
|
||||
|
||||
} else if (this->entries[index].state == CardState::DRAWABLE) {
|
||||
// If the card is still drawable, we need to move it so it's just in front
|
||||
// of the draw index, then immediately draw it. Ep3 NTE does not handle this
|
||||
// case, but we do even when playing NTE.
|
||||
ssize_t ref_index;
|
||||
for (ref_index = this->card_refs.size(); ref_index >= 0; ref_index--) {
|
||||
if (this->card_refs[ref_index] == card_ref) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ref_index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t ref_uindex = ref_index;
|
||||
for (; ref_uindex > this->draw_index; ref_uindex--) {
|
||||
// Note: draw_index is also unsigned, so ref_uindex cannot be zero here
|
||||
this->card_refs[ref_uindex] = this->card_refs[ref_uindex - 1];
|
||||
}
|
||||
this->card_refs[this->draw_index] = card_ref;
|
||||
this->entries[index].state = CardState::IN_HAND;
|
||||
this->draw_index++;
|
||||
return true;
|
||||
|
||||
} else {
|
||||
if (entry.state != CardState::DRAWABLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the card is still drawable, we need to move it so it's just in front of
|
||||
// the draw index, then immediately draw it. Ep3 NTE does not handle this
|
||||
// case, but we do even when playing NTE.
|
||||
size_t ref_index;
|
||||
for (ref_index = 0; ref_index < this->card_refs.size(); ref_index++) {
|
||||
if (this->card_refs[ref_index] == card_ref) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ref_index >= this->card_refs.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (; ref_index > this->draw_index; ref_index--) {
|
||||
// this->draw_index is also unsigned, so ref_index cannot be zero here
|
||||
this->card_refs[ref_index] = this->card_refs[ref_index - 1];
|
||||
}
|
||||
this->card_refs[this->draw_index] = card_ref;
|
||||
|
||||
// Draw the card
|
||||
entry.state = CardState::IN_HAND;
|
||||
this->draw_index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t DeckState::card_id_for_card_ref(uint16_t card_ref) const {
|
||||
@@ -201,12 +205,17 @@ void DeckState::do_mulligan(bool is_nte) {
|
||||
this->card_refs[index + 5] = temp_ref;
|
||||
}
|
||||
|
||||
auto s = this->server.lock();
|
||||
if (!s) {
|
||||
throw runtime_error("server is missing");
|
||||
}
|
||||
|
||||
// Shuffle the deck, except the first 5 cards (which are about to be drawn).
|
||||
size_t max = this->num_drawable_cards() - 5;
|
||||
uint8_t base_index = this->draw_index + 5;
|
||||
for (size_t z = 0; z < this->card_refs.size(); z++) {
|
||||
uint8_t index1 = random_from_optional_crypt(this->opt_rand_crypt) % max;
|
||||
uint8_t index2 = random_from_optional_crypt(this->opt_rand_crypt) % max;
|
||||
uint8_t index1 = s->get_random(max);
|
||||
uint8_t index2 = s->get_random(max);
|
||||
uint16_t temp_ref = this->card_refs[base_index + index1];
|
||||
this->card_refs[base_index + index1] = this->card_refs[base_index + index2];
|
||||
this->card_refs[base_index + index2] = temp_ref;
|
||||
@@ -258,6 +267,11 @@ void DeckState::set_card_discarded(uint16_t card_ref) {
|
||||
|
||||
void DeckState::shuffle() {
|
||||
if (this->shuffle_enabled) {
|
||||
auto s = this->server.lock();
|
||||
if (!s) {
|
||||
throw runtime_error("server is missing");
|
||||
}
|
||||
|
||||
size_t max = this->num_drawable_cards();
|
||||
for (size_t z = 0; z < this->card_refs.size(); z++) {
|
||||
// Note: This is the way Sega originally implemented shuffling - they just
|
||||
@@ -265,8 +279,8 @@ void DeckState::shuffle() {
|
||||
// instead swap each item with another random item (possibly itself) that
|
||||
// doesn't appear earlier than it in the array, but this is not what Sega
|
||||
// did.
|
||||
uint8_t index1 = this->draw_index + random_from_optional_crypt(this->opt_rand_crypt) % max;
|
||||
uint8_t index2 = this->draw_index + random_from_optional_crypt(this->opt_rand_crypt) % max;
|
||||
uint8_t index1 = this->draw_index + s->get_random(max);
|
||||
uint8_t index2 = this->draw_index + s->get_random(max);
|
||||
uint16_t temp_ref = this->card_refs[index1];
|
||||
this->card_refs[index1] = this->card_refs[index2];
|
||||
this->card_refs[index2] = temp_ref;
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
class Server;
|
||||
|
||||
struct NameEntry {
|
||||
/* 00 */ pstring<TextEncoding::MARKED, 0x10> name;
|
||||
/* 10 */ uint8_t client_id;
|
||||
@@ -20,7 +22,7 @@ struct NameEntry {
|
||||
|
||||
NameEntry();
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(NameEntry, 0x14);
|
||||
|
||||
struct DeckEntry {
|
||||
/* 00 */ pstring<TextEncoding::MARKED, 0x10> name;
|
||||
@@ -37,7 +39,7 @@ struct DeckEntry {
|
||||
|
||||
DeckEntry();
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(DeckEntry, 0x58);
|
||||
|
||||
uint8_t index_for_card_ref(uint16_t card_ref);
|
||||
uint8_t client_id_for_card_ref(uint16_t card_ref);
|
||||
@@ -57,13 +59,13 @@ public:
|
||||
DeckState(
|
||||
uint8_t client_id,
|
||||
const parray<CardIDT, 0x1F>& card_ids,
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt)
|
||||
: client_id(client_id),
|
||||
std::shared_ptr<Server> server)
|
||||
: server(server),
|
||||
client_id(client_id),
|
||||
draw_index(1),
|
||||
card_ref_base(this->client_id << 8),
|
||||
shuffle_enabled(true),
|
||||
loop_enabled(true),
|
||||
opt_rand_crypt(opt_rand_crypt) {
|
||||
loop_enabled(true) {
|
||||
for (size_t z = 0; z < card_ids.size(); z++) {
|
||||
auto& e = this->entries[z];
|
||||
e.card_id = card_ids[z];
|
||||
@@ -99,6 +101,8 @@ public:
|
||||
void print(FILE* stream, std::shared_ptr<const CardIndex> card_index = nullptr) const;
|
||||
|
||||
private:
|
||||
std::weak_ptr<Server> server;
|
||||
|
||||
struct CardEntry {
|
||||
uint16_t card_id;
|
||||
uint8_t deck_index;
|
||||
@@ -111,8 +115,6 @@ private:
|
||||
bool loop_enabled;
|
||||
parray<CardEntry, 31> entries;
|
||||
parray<uint16_t, 31> card_refs;
|
||||
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt;
|
||||
};
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
@@ -20,7 +20,7 @@ struct MapState {
|
||||
void clear();
|
||||
|
||||
void print(FILE* stream) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(MapState, 0x110);
|
||||
|
||||
struct MapAndRulesState {
|
||||
/* 0000 */ MapState map;
|
||||
@@ -45,7 +45,7 @@ struct MapAndRulesState {
|
||||
|
||||
void set_occupied_bit_for_tile(uint8_t x, uint8_t y);
|
||||
void clear_occupied_bit_for_tile(uint8_t x, uint8_t y);
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(MapAndRulesState, 0x138);
|
||||
|
||||
struct MapAndRulesStateTrial {
|
||||
/* 0000 */ MapState map;
|
||||
@@ -65,7 +65,7 @@ struct MapAndRulesStateTrial {
|
||||
MapAndRulesStateTrial() = default;
|
||||
MapAndRulesStateTrial(const MapAndRulesState& state);
|
||||
operator MapAndRulesState() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(MapAndRulesStateTrial, 0x130);
|
||||
|
||||
struct OverlayState {
|
||||
parray<parray<uint8_t, 0x10>, 0x10> tiles;
|
||||
@@ -75,6 +75,6 @@ struct OverlayState {
|
||||
|
||||
OverlayState();
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(OverlayState, 0x174);
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
@@ -49,7 +49,7 @@ void PlayerState::init() {
|
||||
throw logic_error("replacing a player state object is not permitted");
|
||||
}
|
||||
|
||||
this->deck_state = make_shared<DeckState>(this->client_id, s->deck_entries[client_id]->card_ids, s->options.opt_rand_crypt);
|
||||
this->deck_state = make_shared<DeckState>(this->client_id, s->deck_entries[client_id]->card_ids, s);
|
||||
if (s->map_and_rules->rules.disable_deck_shuffle) {
|
||||
this->deck_state->disable_shuffle();
|
||||
}
|
||||
@@ -224,8 +224,7 @@ void PlayerState::apply_assist_card_effect_on_set(
|
||||
|
||||
size_t log_index;
|
||||
for (log_index = 0; log_index < 0x10; log_index++) {
|
||||
auto ce = s->definition_for_card_ref(
|
||||
this->discard_log_card_refs[log_index]);
|
||||
auto ce = s->definition_for_card_ref(this->discard_log_card_refs[log_index]);
|
||||
if (ce && ((ce->def.type == CardType::ITEM || ce->def.type == CardType::CREATURE))) {
|
||||
break;
|
||||
}
|
||||
@@ -1416,7 +1415,7 @@ bool PlayerState::set_card_from_hand(
|
||||
s->send_6xB4x05();
|
||||
|
||||
if (!is_nte) {
|
||||
G_Unknown_Ep3_6xB4x4A cmd;
|
||||
G_AddToSetCardlog_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.card_refs[0] = card_ref;
|
||||
cmd.client_id = this->client_id;
|
||||
@@ -1783,7 +1782,7 @@ bool PlayerState::set_action_cards_for_action_state(const ActionState& pa) {
|
||||
card->loc.direction = pa.facing_direction;
|
||||
log.debug("set facing direction to %s", name_for_enum(card->loc.direction));
|
||||
|
||||
G_Unknown_Ep3_6xB4x4A cmd;
|
||||
G_AddToSetCardlog_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.round_num = s->get_round_num();
|
||||
@@ -1843,7 +1842,7 @@ bool PlayerState::set_action_cards_for_action_state(const ActionState& pa) {
|
||||
}
|
||||
}
|
||||
if (!is_nte) {
|
||||
G_Unknown_Ep3_6xB4x4A cmd;
|
||||
G_AddToSetCardlog_Ep3_6xB4x4A cmd;
|
||||
cmd.card_refs.clear(0xFFFF);
|
||||
cmd.client_id = this->client_id;
|
||||
cmd.round_num = s->get_round_num();
|
||||
|
||||
@@ -36,7 +36,7 @@ struct Condition {
|
||||
void clear_FF();
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Condition, 0x10);
|
||||
|
||||
struct EffectResult {
|
||||
/* 00 */ le_uint16_t attacker_card_ref;
|
||||
@@ -58,7 +58,7 @@ struct EffectResult {
|
||||
void clear();
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(EffectResult, 0x0C);
|
||||
|
||||
struct CardShortStatus {
|
||||
/* 00 */ le_uint16_t card_ref;
|
||||
@@ -78,7 +78,7 @@ struct CardShortStatus {
|
||||
void clear_FF();
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(CardShortStatus, 0x10);
|
||||
|
||||
struct ActionState {
|
||||
/* 00 */ le_uint16_t client_id;
|
||||
@@ -99,7 +99,7 @@ struct ActionState {
|
||||
void clear();
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ActionState, 0x64);
|
||||
|
||||
struct ActionChain {
|
||||
// Note: Episode 3 Trial Edition has a different format for this structure.
|
||||
@@ -135,7 +135,7 @@ struct ActionChain {
|
||||
void clear_FF();
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ActionChain, 0x70);
|
||||
|
||||
struct ActionChainWithConds {
|
||||
/* 0000 */ ActionChain chain;
|
||||
@@ -173,7 +173,7 @@ struct ActionChainWithConds {
|
||||
uint8_t get_adjusted_move_ability_nte(uint8_t ability) const;
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ActionChainWithConds, 0x100);
|
||||
|
||||
struct ActionChainWithCondsTrial {
|
||||
/* 0000 */ int8_t effective_ap;
|
||||
@@ -205,7 +205,7 @@ struct ActionChainWithCondsTrial {
|
||||
ActionChainWithCondsTrial() = default;
|
||||
ActionChainWithCondsTrial(const ActionChainWithConds& src);
|
||||
operator ActionChainWithConds() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ActionChainWithCondsTrial, 0x100);
|
||||
|
||||
struct ActionMetadata {
|
||||
/* 00 */ le_uint16_t card_ref;
|
||||
@@ -241,7 +241,7 @@ struct ActionMetadata {
|
||||
uint16_t original_attacker_card_ref);
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ActionMetadata, 0x74);
|
||||
|
||||
struct HandAndEquipState {
|
||||
/* 00 */ parray<uint8_t, 2> dice_results;
|
||||
@@ -276,7 +276,7 @@ struct HandAndEquipState {
|
||||
void clear_FF();
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(HandAndEquipState, 0x54);
|
||||
|
||||
struct PlayerBattleStats {
|
||||
/* 00 */ le_uint16_t damage_given;
|
||||
@@ -310,7 +310,7 @@ struct PlayerBattleStats {
|
||||
|
||||
static uint8_t rank_for_score(float score);
|
||||
static const char* name_for_rank(uint8_t rank);
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PlayerBattleStats, 0x28);
|
||||
|
||||
struct PlayerBattleStatsTrial {
|
||||
/* 00 */ le_uint32_t damage_given = 0;
|
||||
@@ -323,7 +323,7 @@ struct PlayerBattleStatsTrial {
|
||||
PlayerBattleStatsTrial() = default;
|
||||
PlayerBattleStatsTrial(const PlayerBattleStats& data);
|
||||
operator PlayerBattleStats() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PlayerBattleStatsTrial, 0x14);
|
||||
|
||||
std::vector<uint16_t> get_card_refs_within_range(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
|
||||
+38
-31
@@ -4,6 +4,7 @@
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "../Loggers.hh"
|
||||
#include "../Revision.hh"
|
||||
#include "../SendCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
@@ -29,6 +30,7 @@ void Server::PresenceEntry::clear() {
|
||||
|
||||
Server::Server(shared_ptr<Lobby> lobby, Options&& options)
|
||||
: lobby(lobby),
|
||||
battle_record(lobby->battle_record),
|
||||
has_lobby(lobby != nullptr),
|
||||
options(std::move(options)),
|
||||
last_chosen_map(this->options.tournament ? this->options.tournament->get_map() : nullptr),
|
||||
@@ -99,10 +101,10 @@ void Server::init() {
|
||||
this->card_special = make_shared<CardSpecial>(this->shared_from_this());
|
||||
|
||||
// Note: The original implementation calls the default PSOV2Encryption
|
||||
// constructor for opt_rand_crypt, which just uses 0 as the seed. It then
|
||||
// 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->opt_rand_crypt = make_shared<PSOV2Encryption>(0);
|
||||
// this->random_crypt = make_shared<PSOV2Encryption>(0);
|
||||
|
||||
this->state_flags = make_shared<StateFlags>();
|
||||
|
||||
@@ -252,8 +254,8 @@ void Server::send(const void* data, size_t size, uint8_t command, bool enable_ma
|
||||
for (auto watcher_l : l->watcher_lobbies) {
|
||||
send_command_if_not_loading(watcher_l, command, 0x00, data, size);
|
||||
}
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
l->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
|
||||
if (this->battle_record && this->battle_record->writable()) {
|
||||
this->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
|
||||
}
|
||||
|
||||
} else if ((this->options.behavior_flags & BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING) &&
|
||||
@@ -271,19 +273,8 @@ void Server::send_6xB4x46() const {
|
||||
G_ServerVersionStrings_Ep3_6xB4x46 cmd;
|
||||
cmd.version_signature.encode(this->options.is_nte() ? VERSION_SIGNATURE_NTE : VERSION_SIGNATURE, 1);
|
||||
cmd.date_str1.encode(format_time(this->options.card_index->definitions_mtime() * 1000000), 1);
|
||||
string date_str2;
|
||||
if (this->options.opt_rand_crypt) {
|
||||
date_str2 = string_printf(
|
||||
"Random:%08" PRIX32 "+%08" PRIX32,
|
||||
this->options.opt_rand_crypt->seed(),
|
||||
this->options.opt_rand_crypt->absolute_offset());
|
||||
} else {
|
||||
date_str2 = "Random:<SYS>";
|
||||
}
|
||||
if (this->last_chosen_map) {
|
||||
date_str2 += string_printf(" Map:%08" PRIX32, this->last_chosen_map->map_number);
|
||||
}
|
||||
cmd.date_str2.encode(date_str2, 1);
|
||||
string build_date = format_time(BUILD_TIMESTAMP);
|
||||
cmd.date_str2.encode(string_printf("newserv %s compiled at %s", GIT_REVISION_HASH, build_date.c_str()), 1);
|
||||
this->send(cmd);
|
||||
}
|
||||
|
||||
@@ -783,6 +774,9 @@ void Server::destroy_cards_with_zero_hp() {
|
||||
void Server::determine_first_team_turn() {
|
||||
this->team_client_count[0] = this->map_and_rules->num_team0_players;
|
||||
this->team_client_count[1] = this->map_and_rules->num_players - this->team_client_count[0];
|
||||
if (this->team_client_count[0] == 0 || this->team_client_count[1] == 0) {
|
||||
throw runtime_error("one or both teams have no players");
|
||||
}
|
||||
this->first_team_turn = 0xFF;
|
||||
while (this->first_team_turn == 0xFF) {
|
||||
uint8_t results[2] = {0, 0};
|
||||
@@ -1080,16 +1074,24 @@ shared_ptr<const PlayerState> Server::get_player_state(uint8_t client_id) const
|
||||
return this->player_states[client_id];
|
||||
}
|
||||
|
||||
uint32_t Server::get_random_raw() {
|
||||
le_uint32_t ret = random_from_optional_crypt(this->options.opt_rand_crypt);
|
||||
if (this->battle_record && this->battle_record->writable()) {
|
||||
this->battle_record->add_random_data(&ret, sizeof(ret));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint32_t Server::get_random(uint32_t max) {
|
||||
// The original implementation was essentially:
|
||||
// return (static_cast<double>(this->opt_rand_crypt->next() >> 16) / 65536.0) * max
|
||||
// return (static_cast<double>(this->random_source->next() >> 16) / 65536.0) * max
|
||||
// This is unnecessarily complicated and imprecise, so we instead just do:
|
||||
return random_from_optional_crypt(this->options.opt_rand_crypt) % max;
|
||||
return this->get_random_raw() % max;
|
||||
}
|
||||
|
||||
float Server::get_random_float_0_1() {
|
||||
// This lacks some precision, but matches the original implementation.
|
||||
return (static_cast<double>(random_from_optional_crypt(this->options.opt_rand_crypt) >> 16) / 65536.0);
|
||||
return (static_cast<double>(this->get_random_raw() >> 16) / 65536.0);
|
||||
}
|
||||
|
||||
uint32_t Server::get_round_num() const {
|
||||
@@ -1546,8 +1548,8 @@ void Server::setup_and_start_battle() {
|
||||
|
||||
this->setup_phase = SetupPhase::STARTER_ROLLS;
|
||||
|
||||
// Note: This is where original implementation re-seeds opt_rand_crypt (it
|
||||
// uses time() as the seed value).
|
||||
// Note: This is where original implementation re-seeds its random generator
|
||||
// (it uses time() as the seed value).
|
||||
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
if (!this->check_presence_entry(z)) {
|
||||
@@ -1837,7 +1839,11 @@ void Server::on_server_data_input(shared_ptr<Client> sender_c, const string& dat
|
||||
throw runtime_error("unknown CAx subsubcommand");
|
||||
}
|
||||
|
||||
if ((sender_c->version() == Version::GC_EP3_NTE) || !header.mask_key) {
|
||||
if (this->battle_record && this->battle_record->writable()) {
|
||||
this->battle_record->add_command(BattleRecord::Event::Type::SERVER_DATA_COMMAND, data.data(), data.size());
|
||||
}
|
||||
|
||||
if ((sender_c && (sender_c->version() == Version::GC_EP3_NTE)) || !header.mask_key) {
|
||||
(this->*handler)(sender_c, data);
|
||||
} else {
|
||||
string unmasked_data = data;
|
||||
@@ -2167,7 +2173,8 @@ void Server::handle_CAx13_update_map_during_setup_t(shared_ptr<Client> c, const
|
||||
// in the case of NTE, no values at all, since the Rules structure is
|
||||
// smaller). So, use the values from the last chosen map if applicable, or
|
||||
// the values from the $dicerange command if available.
|
||||
const Rules* map_rules = this->last_chosen_map ? &this->last_chosen_map->version(c->language())->map->default_rules : nullptr;
|
||||
uint8_t language = c ? c->language() : 1;
|
||||
const Rules* map_rules = this->last_chosen_map ? &this->last_chosen_map->version(language)->map->default_rules : nullptr;
|
||||
auto& server_rules = this->map_and_rules->rules;
|
||||
// NTE can specify the DEF dice value range in its Rules struct, so we use
|
||||
// that unless the map or $dicerange overrides it.
|
||||
@@ -2347,11 +2354,12 @@ void Server::handle_CAx1D_start_battle(shared_ptr<Client>, const string& data) {
|
||||
}
|
||||
|
||||
if (should_start) {
|
||||
if (this->battle_record && this->battle_record->writable()) {
|
||||
this->battle_record->set_battle_start_timestamp();
|
||||
}
|
||||
|
||||
auto l = this->lobby.lock();
|
||||
if (l) {
|
||||
if (l->battle_record) {
|
||||
l->battle_record->set_battle_start_timestamp();
|
||||
}
|
||||
// Note: Sega's implementation doesn't set EX results values here; they
|
||||
// did it at game join time instead. We do it here for code simplicity.
|
||||
if ((l->base_version != Version::GC_EP3_NTE) && l->ep3_ex_result_values) {
|
||||
@@ -2554,8 +2562,7 @@ void Server::handle_CAx3A_time_limit_expired(shared_ptr<Client>, const string& d
|
||||
|
||||
void Server::handle_CAx40_map_list_request(shared_ptr<Client> sender_c, const string& data) {
|
||||
const auto& in_cmd = check_size_t<G_MapListRequest_Ep3_CAx40>(data);
|
||||
this->send_debug_command_received_message(
|
||||
in_cmd.header.subsubcommand, "MAP LIST");
|
||||
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "MAP LIST");
|
||||
|
||||
auto l = this->lobby.lock();
|
||||
if (!l) {
|
||||
@@ -2609,13 +2616,13 @@ void Server::send_6xB6x41_to_all_clients() const {
|
||||
}
|
||||
}
|
||||
|
||||
if (l->battle_record && l->battle_record->writable()) {
|
||||
if (this->battle_record && this->battle_record->writable()) {
|
||||
// TODO: It's not great that we just pick the first one; ideally we'd put
|
||||
// all of them in the recording and send the appropriate one to the client
|
||||
// in the playback lobby
|
||||
for (string& data : map_commands_by_language) {
|
||||
if (!data.empty()) {
|
||||
l->battle_record->add_command(
|
||||
this->battle_record->add_command(
|
||||
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "../CommandFormats.hh"
|
||||
#include "../Text.hh"
|
||||
#include "AssistServer.hh"
|
||||
#include "BattleRecord.hh"
|
||||
#include "CardSpecial.hh"
|
||||
#include "MapState.hh"
|
||||
#include "PlayerState.hh"
|
||||
@@ -191,6 +192,7 @@ public:
|
||||
uint8_t get_current_team_turn() const;
|
||||
std::shared_ptr<PlayerState> get_player_state(uint8_t client_id);
|
||||
std::shared_ptr<const PlayerState> get_player_state(uint8_t client_id) const;
|
||||
uint32_t get_random_raw();
|
||||
uint32_t get_random(uint32_t max);
|
||||
float get_random_float_0_1();
|
||||
uint32_t get_round_num() const;
|
||||
@@ -273,6 +275,7 @@ private:
|
||||
public:
|
||||
// These fields are not part of the original implementation
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
std::shared_ptr<BattleRecord> battle_record;
|
||||
bool has_lobby;
|
||||
Options options;
|
||||
std::shared_ptr<const MapIndex::Map> last_chosen_map;
|
||||
@@ -290,7 +293,8 @@ public:
|
||||
uint8_t is_cpu_player;
|
||||
PresenceEntry();
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PresenceEntry, 3);
|
||||
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules;
|
||||
bcarray<std::shared_ptr<DeckEntry>, 4> deck_entries;
|
||||
parray<PresenceEntry, 4> presence_entries;
|
||||
|
||||
+31
-31
@@ -9,18 +9,18 @@ using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number, const string& player_name)
|
||||
: serial_number(serial_number),
|
||||
Tournament::PlayerEntry::PlayerEntry(uint32_t account_id, const string& player_name)
|
||||
: account_id(account_id),
|
||||
player_name(player_name) {}
|
||||
|
||||
Tournament::PlayerEntry::PlayerEntry(shared_ptr<Client> c)
|
||||
: serial_number(c->license->serial_number),
|
||||
: account_id(c->login->account->account_id),
|
||||
client(c),
|
||||
player_name(c->character()->disp.name.decode(c->language())) {}
|
||||
|
||||
Tournament::PlayerEntry::PlayerEntry(
|
||||
shared_ptr<const COMDeckDefinition> com_deck)
|
||||
: serial_number(0),
|
||||
: account_id(0),
|
||||
com_deck(com_deck) {}
|
||||
|
||||
bool Tournament::PlayerEntry::is_com() const {
|
||||
@@ -28,7 +28,7 @@ bool Tournament::PlayerEntry::is_com() const {
|
||||
}
|
||||
|
||||
bool Tournament::PlayerEntry::is_human() const {
|
||||
return (this->serial_number != 0);
|
||||
return (this->account_id != 0);
|
||||
}
|
||||
|
||||
Tournament::Team::Team(
|
||||
@@ -56,9 +56,9 @@ string Tournament::Team::str() const {
|
||||
for (const auto& player : this->players) {
|
||||
if (player.is_human()) {
|
||||
if (player.player_name.empty()) {
|
||||
ret += string_printf(" %08" PRIX32, player.serial_number);
|
||||
ret += string_printf(" %08" PRIX32, player.account_id);
|
||||
} else {
|
||||
ret += string_printf(" %08" PRIX32 " (%s)", player.serial_number, player.player_name.c_str());
|
||||
ret += string_printf(" %08" PRIX32 " (%s)", player.account_id, player.player_name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,12 +81,12 @@ void Tournament::Team::register_player(
|
||||
if (!tournament) {
|
||||
throw runtime_error("tournament has been deleted");
|
||||
}
|
||||
if (!tournament->all_player_serial_numbers.emplace(c->license->serial_number).second) {
|
||||
if (!tournament->all_player_account_ids.emplace(c->login->account->account_id).second) {
|
||||
throw runtime_error("player already registered in same tournament");
|
||||
}
|
||||
|
||||
for (const auto& player : this->players) {
|
||||
if (player.is_human() && (player.serial_number == c->license->serial_number)) {
|
||||
if (player.is_human() && (player.account_id == c->login->account->account_id)) {
|
||||
throw logic_error("player already registered in team but not in tournament");
|
||||
}
|
||||
}
|
||||
@@ -99,11 +99,11 @@ void Tournament::Team::register_player(
|
||||
}
|
||||
}
|
||||
|
||||
bool Tournament::Team::unregister_player(uint32_t serial_number) {
|
||||
bool Tournament::Team::unregister_player(uint32_t account_id) {
|
||||
size_t index;
|
||||
for (index = 0; index < this->players.size(); index++) {
|
||||
if (this->players[index].is_human() &&
|
||||
(this->players[index].serial_number == serial_number)) {
|
||||
(this->players[index].account_id == account_id)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ bool Tournament::Team::unregister_player(uint32_t serial_number) {
|
||||
// If the tournament has not started yet, just remove the player from the
|
||||
// team
|
||||
} else {
|
||||
if (!tournament->all_player_serial_numbers.erase(serial_number)) {
|
||||
if (!tournament->all_player_account_ids.erase(account_id)) {
|
||||
throw logic_error("player removed from team but not from tournament");
|
||||
}
|
||||
}
|
||||
@@ -371,13 +371,13 @@ void Tournament::init() {
|
||||
team_index_to_rounds_cleared.emplace_back(team_json->get_int("num_rounds_cleared"));
|
||||
for (const auto& player_json : team_json->get_list("player_specs")) {
|
||||
if (player_json->is_list()) {
|
||||
uint32_t serial_number = player_json->at(0).as_int();
|
||||
team->players.emplace_back(serial_number, player_json->at(1).as_string());
|
||||
this->all_player_serial_numbers.emplace(serial_number);
|
||||
uint32_t account_id = player_json->at(0).as_int();
|
||||
team->players.emplace_back(account_id, player_json->at(1).as_string());
|
||||
this->all_player_account_ids.emplace(account_id);
|
||||
} else if (player_json->is_int()) {
|
||||
uint32_t serial_number = player_json->as_int();
|
||||
team->players.emplace_back(serial_number);
|
||||
this->all_player_serial_numbers.emplace(serial_number);
|
||||
uint32_t account_id = player_json->as_int();
|
||||
team->players.emplace_back(account_id);
|
||||
this->all_player_account_ids.emplace(account_id);
|
||||
} else if (player_json->is_string()) {
|
||||
team->players.emplace_back(this->com_deck_index->deck_for_name(player_json->as_string()));
|
||||
} else {
|
||||
@@ -511,9 +511,9 @@ JSON Tournament::json() const {
|
||||
for (const auto& player : team->players) {
|
||||
if (player.is_human()) {
|
||||
if (!player.player_name.empty()) {
|
||||
players_list.emplace_back(JSON::list({player.serial_number, player.player_name}));
|
||||
players_list.emplace_back(JSON::list({player.account_id, player.player_name}));
|
||||
} else {
|
||||
players_list.emplace_back(player.serial_number);
|
||||
players_list.emplace_back(player.account_id);
|
||||
}
|
||||
} else {
|
||||
players_list.emplace_back(player.com_deck->deck_name);
|
||||
@@ -571,25 +571,25 @@ shared_ptr<Tournament::Match> Tournament::get_final_match() const {
|
||||
return this->final_match;
|
||||
}
|
||||
|
||||
shared_ptr<Tournament::Team> Tournament::team_for_serial_number(
|
||||
uint32_t serial_number) const {
|
||||
if (!this->all_player_serial_numbers.count(serial_number)) {
|
||||
shared_ptr<Tournament::Team> Tournament::team_for_account_id(
|
||||
uint32_t account_id) const {
|
||||
if (!this->all_player_account_ids.count(account_id)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (auto team : this->teams) {
|
||||
for (const auto& player : team->players) {
|
||||
if (player.serial_number == serial_number) {
|
||||
if (player.account_id == account_id) {
|
||||
return team->is_active ? team : nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw logic_error("serial number registered in tournament but not in any team");
|
||||
throw logic_error("account ID registered in tournament but not in any team");
|
||||
}
|
||||
|
||||
const set<uint32_t>& Tournament::get_all_player_serial_numbers() const {
|
||||
return this->all_player_serial_numbers;
|
||||
const set<uint32_t>& Tournament::get_all_player_account_ids() const {
|
||||
return this->all_player_account_ids;
|
||||
}
|
||||
|
||||
void Tournament::start() {
|
||||
@@ -896,10 +896,10 @@ bool TournamentIndex::delete_tournament(const string& name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
shared_ptr<Tournament::Team> TournamentIndex::team_for_serial_number(uint32_t serial_number) const {
|
||||
shared_ptr<Tournament::Team> TournamentIndex::team_for_account_id(uint32_t account_id) const {
|
||||
for (const auto& it : this->name_to_tournament) {
|
||||
const auto& tourn = it.second;
|
||||
auto team = tourn->team_for_serial_number(serial_number);
|
||||
auto team = tourn->team_for_account_id(account_id);
|
||||
if (team) {
|
||||
return team;
|
||||
}
|
||||
@@ -912,11 +912,11 @@ void TournamentIndex::link_client(shared_ptr<Client> c) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto team = this->team_for_serial_number(c->license->serial_number);
|
||||
auto team = this->team_for_account_id(c->login->account->account_id);
|
||||
auto tourn = team ? team->tournament.lock() : nullptr;
|
||||
if (team && team->is_active && tourn) {
|
||||
for (auto& player : team->players) {
|
||||
if (player.serial_number == c->license->serial_number) {
|
||||
if (player.account_id == c->login->account->account_id) {
|
||||
c->ep3_tournament_team = team;
|
||||
player.client = c;
|
||||
if (c->version() == Version::GC_EP3) {
|
||||
|
||||
@@ -33,16 +33,16 @@ public:
|
||||
};
|
||||
|
||||
struct PlayerEntry {
|
||||
// Invariant: (serial_number == 0) != (com_deck == nullptr)
|
||||
// Invariant: (account_id == 0) != (com_deck == nullptr)
|
||||
// (that is, exactly one of the following must be valid)
|
||||
uint32_t serial_number;
|
||||
uint32_t account_id;
|
||||
std::shared_ptr<const COMDeckDefinition> com_deck;
|
||||
|
||||
// client is valid if serial_number is nonzero and the client is connected
|
||||
// client is valid if account_id is nonzero and the client is connected
|
||||
std::weak_ptr<Client> client;
|
||||
std::string player_name; // Not used for COM decks
|
||||
|
||||
explicit PlayerEntry(uint32_t serial_number, const std::string& player_name = "");
|
||||
explicit PlayerEntry(uint32_t account_id, const std::string& player_name = "");
|
||||
explicit PlayerEntry(std::shared_ptr<Client> c);
|
||||
explicit PlayerEntry(std::shared_ptr<const COMDeckDefinition> com_deck);
|
||||
|
||||
@@ -73,7 +73,7 @@ public:
|
||||
std::shared_ptr<Client> c,
|
||||
const std::string& team_name,
|
||||
const std::string& password);
|
||||
bool unregister_player(uint32_t serial_number);
|
||||
bool unregister_player(uint32_t account_id);
|
||||
|
||||
bool has_any_human_players() const;
|
||||
size_t num_human_players() const;
|
||||
@@ -152,8 +152,8 @@ public:
|
||||
std::shared_ptr<Team> get_winner_team() const;
|
||||
std::shared_ptr<Match> next_match_for_team(std::shared_ptr<Team> team) const;
|
||||
std::shared_ptr<Match> get_final_match() const;
|
||||
std::shared_ptr<Team> team_for_serial_number(uint32_t serial_number) const;
|
||||
const std::set<uint32_t>& get_all_player_serial_numbers() const;
|
||||
std::shared_ptr<Team> team_for_account_id(uint32_t account_id) const;
|
||||
const std::set<uint32_t>& get_all_player_account_ids() const;
|
||||
|
||||
void start();
|
||||
|
||||
@@ -178,7 +178,7 @@ private:
|
||||
State current_state;
|
||||
uint32_t menu_item_id;
|
||||
|
||||
std::set<uint32_t> all_player_serial_numbers;
|
||||
std::set<uint32_t> all_player_account_ids;
|
||||
std::unordered_set<std::shared_ptr<Match>> pending_matches;
|
||||
|
||||
// This vector contains all teams in the original starting order of the
|
||||
@@ -231,7 +231,7 @@ public:
|
||||
uint8_t flags);
|
||||
bool delete_tournament(const std::string& name);
|
||||
|
||||
std::shared_ptr<Tournament::Team> team_for_serial_number(uint32_t serial_number) const;
|
||||
std::shared_ptr<Tournament::Team> team_for_account_id(uint32_t account_id) const;
|
||||
|
||||
void link_client(std::shared_ptr<Client> c);
|
||||
void link_all_clients(std::shared_ptr<ServerState> s);
|
||||
|
||||
+130
-79
@@ -10,6 +10,7 @@
|
||||
|
||||
#ifdef HAVE_RESOURCE_FILE
|
||||
#include <resource_file/Emulators/PPC32Emulator.hh>
|
||||
#include <resource_file/Emulators/SH4Emulator.hh>
|
||||
#include <resource_file/Emulators/X86Emulator.hh>
|
||||
#endif
|
||||
|
||||
@@ -39,6 +40,8 @@ const char* name_for_architecture(CompiledFunctionCode::Architecture arch) {
|
||||
return "PowerPC";
|
||||
case CompiledFunctionCode::Architecture::X86:
|
||||
return "x86";
|
||||
case CompiledFunctionCode::Architecture::SH4:
|
||||
return "SH-4";
|
||||
default:
|
||||
throw logic_error("invalid architecture");
|
||||
}
|
||||
@@ -117,12 +120,14 @@ bool CompiledFunctionCode::is_big_endian() const {
|
||||
|
||||
shared_ptr<CompiledFunctionCode> compile_function_code(
|
||||
CompiledFunctionCode::Architecture arch,
|
||||
const string& directory,
|
||||
const string& function_directory,
|
||||
const string& system_directory,
|
||||
const string& name,
|
||||
const string& text) {
|
||||
#ifndef HAVE_RESOURCE_FILE
|
||||
(void)arch;
|
||||
(void)directory;
|
||||
(void)function_directory;
|
||||
(void)system_directory;
|
||||
(void)name;
|
||||
(void)text;
|
||||
throw runtime_error("function compiler is not available");
|
||||
@@ -151,7 +156,11 @@ shared_ptr<CompiledFunctionCode> compile_function_code(
|
||||
throw runtime_error("unknown architecture");
|
||||
}
|
||||
|
||||
string asm_filename = string_printf("%s/%s.%s.inc.s", directory.c_str(), name.c_str(), arch_name_token);
|
||||
// Look in the function directory first, then the system directory
|
||||
string asm_filename = string_printf("%s/%s.%s.inc.s", function_directory.c_str(), name.c_str(), arch_name_token);
|
||||
if (!isfile(asm_filename)) {
|
||||
asm_filename = string_printf("%s/%s.%s.inc.s", system_directory.c_str(), name.c_str(), arch_name_token);
|
||||
}
|
||||
if (isfile(asm_filename)) {
|
||||
if (!get_include_stack.emplace(name).second) {
|
||||
throw runtime_error("mutual recursion between includes: " + name);
|
||||
@@ -165,14 +174,20 @@ shared_ptr<CompiledFunctionCode> compile_function_code(
|
||||
ret = X86Emulator::assemble(load_file(asm_filename), get_include);
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::SH4:
|
||||
throw runtime_error("cannot compuile SH-4 assembly");
|
||||
ret = SH4Emulator::assemble(load_file(asm_filename), get_include);
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("unknown architecture");
|
||||
}
|
||||
get_include_stack.erase(name);
|
||||
return ret.code;
|
||||
}
|
||||
string bin_filename = directory + "/" + name + ".inc.bin";
|
||||
|
||||
string bin_filename = function_directory + "/" + name + ".inc.bin";
|
||||
if (isfile(bin_filename)) {
|
||||
return load_file(bin_filename);
|
||||
}
|
||||
bin_filename = system_directory + "/" + name + ".inc.bin";
|
||||
if (isfile(bin_filename)) {
|
||||
return load_file(bin_filename);
|
||||
}
|
||||
@@ -184,6 +199,10 @@ shared_ptr<CompiledFunctionCode> compile_function_code(
|
||||
assembled = PPC32Emulator::assemble(text, get_include);
|
||||
} else if (arch == CompiledFunctionCode::Architecture::X86) {
|
||||
assembled = X86Emulator::assemble(text, get_include);
|
||||
} else if (arch == CompiledFunctionCode::Architecture::SH4) {
|
||||
assembled = SH4Emulator::assemble(text, get_include);
|
||||
} else {
|
||||
throw runtime_error("invalid architecture");
|
||||
}
|
||||
ret->code = std::move(assembled.code);
|
||||
ret->label_offsets = std::move(assembled.label_offsets);
|
||||
@@ -237,83 +256,95 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t next_menu_item_id = 0;
|
||||
for (const auto& filename : list_directory_sorted(directory)) {
|
||||
try {
|
||||
if (!ends_with(filename, ".s")) {
|
||||
continue;
|
||||
}
|
||||
string system_dir_path = ends_with(directory, "/") ? (directory + "System") : (directory + "/System");
|
||||
|
||||
string name = filename.substr(0, filename.size() - 2);
|
||||
if (ends_with(name, ".inc")) {
|
||||
continue;
|
||||
}
|
||||
uint32_t next_menu_item_id = 1;
|
||||
for (const auto& subdir_name : list_directory_sorted(directory)) {
|
||||
string subdir_path = ends_with(directory, "/") ? (directory + subdir_name) : (directory + "/" + subdir_name);
|
||||
if (!isdir(subdir_path)) {
|
||||
function_compiler_log.warning("Skipping %s (not a directory)", subdir_name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
bool is_patch = ends_with(name, ".patch");
|
||||
if (is_patch) {
|
||||
name.resize(name.size() - 6);
|
||||
}
|
||||
for (const auto& filename : list_directory_sorted(subdir_path)) {
|
||||
try {
|
||||
if (!ends_with(filename, ".s")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Figure out the version or specific_version
|
||||
CompiledFunctionCode::Architecture arch = CompiledFunctionCode::Architecture::UNKNOWN;
|
||||
uint32_t specific_version = 0;
|
||||
string short_name = name;
|
||||
if (ends_with(name, ".ppc")) {
|
||||
arch = CompiledFunctionCode::Architecture::POWERPC;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (ends_with(name, ".x86")) {
|
||||
arch = CompiledFunctionCode::Architecture::X86;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (ends_with(name, ".sh4")) {
|
||||
arch = CompiledFunctionCode::Architecture::SH4;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (is_patch && (name.size() >= 5) && (name[name.size() - 5] == '.')) {
|
||||
specific_version = (name[name.size() - 4] << 24) | (name[name.size() - 3] << 16) | (name[name.size() - 2] << 8) | name[name.size() - 1];
|
||||
if (specific_version_is_gc(specific_version)) {
|
||||
string name = filename.substr(0, filename.size() - 2);
|
||||
if (ends_with(name, ".inc")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool is_patch = ends_with(name, ".patch");
|
||||
if (is_patch) {
|
||||
name.resize(name.size() - 6);
|
||||
}
|
||||
|
||||
// Figure out the version or specific_version
|
||||
CompiledFunctionCode::Architecture arch = CompiledFunctionCode::Architecture::UNKNOWN;
|
||||
uint32_t specific_version = 0;
|
||||
string short_name = name;
|
||||
if (ends_with(name, ".ppc")) {
|
||||
arch = CompiledFunctionCode::Architecture::POWERPC;
|
||||
} else if (specific_version_is_xb(specific_version) || specific_version_is_bb(specific_version)) {
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (ends_with(name, ".x86")) {
|
||||
arch = CompiledFunctionCode::Architecture::X86;
|
||||
} else {
|
||||
throw runtime_error("unable to determine architecture from specific_version");
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (ends_with(name, ".sh4")) {
|
||||
arch = CompiledFunctionCode::Architecture::SH4;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (is_patch && (name.size() >= 5) && (name[name.size() - 5] == '.')) {
|
||||
specific_version = (name[name.size() - 4] << 24) | (name[name.size() - 3] << 16) | (name[name.size() - 2] << 8) | name[name.size() - 1];
|
||||
if (specific_version_is_dc(specific_version)) {
|
||||
arch = CompiledFunctionCode::Architecture::SH4;
|
||||
} else if (specific_version_is_gc(specific_version)) {
|
||||
arch = CompiledFunctionCode::Architecture::POWERPC;
|
||||
} else if (specific_version_is_xb(specific_version) || specific_version_is_bb(specific_version)) {
|
||||
arch = CompiledFunctionCode::Architecture::X86;
|
||||
} else {
|
||||
throw runtime_error("unable to determine architecture from specific_version");
|
||||
}
|
||||
short_name = name.substr(0, name.size() - 5);
|
||||
}
|
||||
short_name = name.substr(0, name.size() - 5);
|
||||
}
|
||||
|
||||
if (arch == CompiledFunctionCode::Architecture::UNKNOWN) {
|
||||
throw runtime_error("unable to determine architecture");
|
||||
}
|
||||
|
||||
string path = directory + "/" + filename;
|
||||
string text = load_file(path);
|
||||
auto code = compile_function_code(arch, directory, name, text);
|
||||
if (code->index != 0) {
|
||||
if (!this->index_to_function.emplace(code->index, code).second) {
|
||||
throw runtime_error(string_printf(
|
||||
"duplicate function index: %08" PRIX32, code->index));
|
||||
if (arch == CompiledFunctionCode::Architecture::UNKNOWN) {
|
||||
throw runtime_error("unable to determine architecture");
|
||||
}
|
||||
}
|
||||
code->specific_version = specific_version;
|
||||
code->source_path = path;
|
||||
code->short_name = short_name;
|
||||
this->name_to_function.emplace(name, code);
|
||||
if (is_patch) {
|
||||
code->menu_item_id = next_menu_item_id++;
|
||||
this->menu_item_id_and_specific_version_to_patch_function.emplace(
|
||||
static_cast<uint64_t>(code->menu_item_id) << 32 | specific_version, code);
|
||||
this->name_and_specific_version_to_patch_function.emplace(
|
||||
string_printf("%s-%08" PRIX32, short_name.c_str(), specific_version), code);
|
||||
}
|
||||
|
||||
string index_prefix = code->index ? string_printf("%02X => ", code->index) : "";
|
||||
string patch_prefix = is_patch ? string_printf("[%08" PRIX32 "/%08" PRIX32 "] ", code->menu_item_id, code->specific_version) : "";
|
||||
function_compiler_log.info("Compiled function %s%s%s (%s)",
|
||||
index_prefix.c_str(), patch_prefix.c_str(), name.c_str(), name_for_architecture(code->arch));
|
||||
string path = subdir_path + "/" + filename;
|
||||
string text = load_file(path);
|
||||
auto code = compile_function_code(arch, subdir_path, system_dir_path, name, text);
|
||||
if (code->index != 0) {
|
||||
if (!this->index_to_function.emplace(code->index, code).second) {
|
||||
throw runtime_error(string_printf(
|
||||
"duplicate function index: %08" PRIX32, code->index));
|
||||
}
|
||||
}
|
||||
code->specific_version = specific_version;
|
||||
code->source_path = path;
|
||||
code->short_name = short_name;
|
||||
this->name_to_function.emplace(name, code);
|
||||
if (is_patch) {
|
||||
code->menu_item_id = next_menu_item_id++;
|
||||
this->menu_item_id_and_specific_version_to_patch_function.emplace(
|
||||
static_cast<uint64_t>(code->menu_item_id) << 32 | specific_version, code);
|
||||
this->name_and_specific_version_to_patch_function.emplace(
|
||||
string_printf("%s-%08" PRIX32, short_name.c_str(), specific_version), code);
|
||||
}
|
||||
|
||||
} catch (const exception& e) {
|
||||
function_compiler_log.warning("Failed to compile function %s: %s", filename.c_str(), e.what());
|
||||
string index_prefix = code->index ? string_printf("%02X => ", code->index) : "";
|
||||
string patch_prefix = is_patch ? string_printf("[%08" PRIX32 "/%08" PRIX32 "] ", code->menu_item_id, code->specific_version) : "";
|
||||
function_compiler_log.info("Compiled function %s%s%s (%s)",
|
||||
index_prefix.c_str(), patch_prefix.c_str(), name.c_str(), name_for_architecture(code->arch));
|
||||
|
||||
} catch (const exception& e) {
|
||||
function_compiler_log.warning("Failed to compile function %s: %s", filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,13 +356,33 @@ shared_ptr<const Menu> FunctionCodeIndex::patch_menu(uint32_t specific_version)
|
||||
ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0);
|
||||
for (const auto& it : this->name_and_specific_version_to_patch_function) {
|
||||
const auto& fn = it.second;
|
||||
if (!fn->hide_from_patches_menu && ends_with(it.first, suffix)) {
|
||||
ret->items.emplace_back(
|
||||
fn->menu_item_id,
|
||||
fn->long_name.empty() ? fn->short_name : fn->long_name,
|
||||
fn->description,
|
||||
MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL);
|
||||
if (fn->hide_from_patches_menu || !ends_with(it.first, suffix)) {
|
||||
continue;
|
||||
}
|
||||
ret->items.emplace_back(
|
||||
fn->menu_item_id,
|
||||
fn->long_name.empty() ? fn->short_name : fn->long_name,
|
||||
fn->description,
|
||||
MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
shared_ptr<const Menu> FunctionCodeIndex::patch_switches_menu(
|
||||
uint32_t specific_version, const std::unordered_set<std::string>& auto_patches_enabled) const {
|
||||
auto suffix = string_printf("-%08" PRIX32, specific_version);
|
||||
|
||||
auto ret = make_shared<Menu>(MenuID::PATCH_SWITCHES, "Patch switches");
|
||||
ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0);
|
||||
for (const auto& it : this->name_and_specific_version_to_patch_function) {
|
||||
const auto& fn = it.second;
|
||||
if (fn->hide_from_patches_menu || !ends_with(it.first, suffix)) {
|
||||
continue;
|
||||
}
|
||||
string name;
|
||||
name.push_back(auto_patches_enabled.count(fn->short_name) ? '*' : '-');
|
||||
name += fn->long_name.empty() ? fn->short_name : fn->long_name;
|
||||
ret->items.emplace_back(fn->menu_item_id, name, fn->description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -440,7 +491,7 @@ uint32_t specific_version_for_gc_header_checksum(uint32_t header_checksum) {
|
||||
char developer_code2 = 'P';
|
||||
uint8_t disc_number = 0;
|
||||
uint8_t version_code;
|
||||
} __attribute__((packed)) data;
|
||||
} __packed__ data;
|
||||
for (const char* game_code2 = "OS"; *game_code2; game_code2++) {
|
||||
data.game_code2 = *game_code2;
|
||||
for (const char* region_code = "JEP"; *region_code; region_code++) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "Menu.hh"
|
||||
@@ -69,6 +70,7 @@ struct FunctionCodeIndex {
|
||||
std::map<std::string, std::shared_ptr<CompiledFunctionCode>> name_and_specific_version_to_patch_function;
|
||||
|
||||
std::shared_ptr<const Menu> patch_menu(uint32_t specific_version) const;
|
||||
std::shared_ptr<const Menu> patch_switches_menu(uint32_t specific_version, const std::unordered_set<std::string>& auto_patches_enabled) const;
|
||||
bool patch_menu_empty(uint32_t specific_version) const;
|
||||
|
||||
std::shared_ptr<const CompiledFunctionCode> get_patch(const std::string& name, uint32_t specific_version) const;
|
||||
|
||||
+10
-5
@@ -9,21 +9,26 @@
|
||||
using namespace std;
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct GSLHeaderEntry {
|
||||
struct GSLHeaderEntryT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
pstring<TextEncoding::ASCII, 0x20> filename;
|
||||
U32T offset; // In pages, so actual offset is this * 0x800
|
||||
U32T size;
|
||||
uint64_t unused;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
using GSLHeaderEntry = GSLHeaderEntryT<false>;
|
||||
using GSLHeaderEntryBE = GSLHeaderEntryT<true>;
|
||||
check_struct_size(GSLHeaderEntry, 0x30);
|
||||
check_struct_size(GSLHeaderEntryBE, 0x30);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
void GSLArchive::load_t() {
|
||||
StringReader r(*this->data);
|
||||
uint64_t min_data_offset = 0xFFFFFFFFFFFFFFFF;
|
||||
while (r.where() < min_data_offset) {
|
||||
const auto& entry = r.get<GSLHeaderEntry<IsBigEndian>>();
|
||||
const auto& entry = r.get<GSLHeaderEntryT<IsBigEndian>>();
|
||||
if (entry.filename.empty()) {
|
||||
break;
|
||||
}
|
||||
@@ -85,10 +90,10 @@ string GSLArchive::generate_t(const unordered_map<string, string>& files) {
|
||||
|
||||
// Make sure there's enough space for a blank header entry before any file's
|
||||
// data pages begin
|
||||
uint32_t data_start_offset = ((sizeof(GSLHeaderEntry<IsBigEndian>) * (files.size() + 1)) + 0x7FF) & (~0x7FF);
|
||||
uint32_t data_start_offset = ((sizeof(GSLHeaderEntryT<IsBigEndian>) * (files.size() + 1)) + 0x7FF) & (~0x7FF);
|
||||
uint32_t data_offset = data_start_offset;
|
||||
for (const auto& file : files) {
|
||||
GSLHeaderEntry<IsBigEndian> entry;
|
||||
GSLHeaderEntryT<IsBigEndian> entry;
|
||||
entry.filename.encode(file.first);
|
||||
entry.offset = data_offset >> 11;
|
||||
entry.size = file.second.size();
|
||||
|
||||
+32
-21
@@ -11,15 +11,18 @@ using namespace std;
|
||||
struct GVMFileEntry {
|
||||
be_uint16_t file_num;
|
||||
pstring<TextEncoding::ASCII, 0x1C> name;
|
||||
parray<be_uint32_t, 2> unknown_a1;
|
||||
} __attribute__((packed));
|
||||
uint8_t format_flags; // Same as in GVRHeader
|
||||
GVRDataFormat data_format; // Same as in GVRHeader
|
||||
be_uint16_t dimensions; // As powers of two in low nybbles (so e.g. 128x128 = 0x0055)
|
||||
be_uint32_t global_index;
|
||||
} __packed_ws__(GVMFileEntry, 0x26);
|
||||
|
||||
struct GVMFileHeader {
|
||||
be_uint32_t magic; // 'GVMH'
|
||||
be_uint32_t signature; // 'GVMH'
|
||||
le_uint32_t header_size;
|
||||
be_uint16_t flags;
|
||||
be_uint16_t flags; // Specifies which fields are present in GVMFileEntries; we always use 0xF (all fields present)
|
||||
be_uint16_t num_files;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(GVMFileHeader, 0x0C);
|
||||
|
||||
struct GVRHeader {
|
||||
be_uint32_t magic; // 'GVRT'
|
||||
@@ -29,21 +32,26 @@ struct GVRHeader {
|
||||
GVRDataFormat data_format;
|
||||
be_uint16_t width;
|
||||
be_uint16_t height;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(GVRHeader, 0x10);
|
||||
|
||||
string encode_gvm(const Image& img, GVRDataFormat data_format) {
|
||||
if (img.get_width() > 0xFFFF) {
|
||||
throw runtime_error("image is too wide to be encoded as a GVR texture");
|
||||
}
|
||||
if (img.get_height() > 0xFFFF) {
|
||||
throw runtime_error("image is too tall to be encoded as a GVR texture");
|
||||
}
|
||||
if (img.get_width() & 3) {
|
||||
throw runtime_error("image width is not a multiple of 4");
|
||||
}
|
||||
if (img.get_height() & 3) {
|
||||
throw runtime_error("image height is not a multiple of 4");
|
||||
string encode_gvm(const Image& img, GVRDataFormat data_format, const std::string& internal_name, uint32_t global_index) {
|
||||
int8_t dimensions_field = -2;
|
||||
{
|
||||
size_t h = img.get_height();
|
||||
size_t w = img.get_width();
|
||||
if ((h != w) || (w & (w - 1)) || (h & (h - 1))) {
|
||||
throw runtime_error("image must be square and dimensions must be powers of 2");
|
||||
}
|
||||
for (w >>= 1; w; w >>= 1, dimensions_field++) {
|
||||
}
|
||||
if (dimensions_field < 1) {
|
||||
throw runtime_error("image is too small");
|
||||
}
|
||||
if (dimensions_field > 0xF) {
|
||||
throw runtime_error("image is too large");
|
||||
}
|
||||
}
|
||||
|
||||
size_t pixel_count = img.get_width() * img.get_height();
|
||||
size_t pixel_bytes = 0;
|
||||
switch (data_format) {
|
||||
@@ -59,11 +67,14 @@ string encode_gvm(const Image& img, GVRDataFormat data_format) {
|
||||
}
|
||||
|
||||
StringWriter w;
|
||||
w.put<GVMFileHeader>({.magic = 0x47564D48, .header_size = 0x48, .flags = 0x010F, .num_files = 1});
|
||||
w.put<GVMFileHeader>({.signature = 0x47564D48, .header_size = 0x48, .flags = 0x000F, .num_files = 1});
|
||||
GVMFileEntry file_entry;
|
||||
file_entry.file_num = 0;
|
||||
file_entry.name.encode("img", 1);
|
||||
file_entry.unknown_a1.clear(0);
|
||||
file_entry.name.encode(internal_name, 1);
|
||||
file_entry.data_format = data_format;
|
||||
file_entry.format_flags = 0;
|
||||
file_entry.dimensions = (dimensions_field << 4) | dimensions_field;
|
||||
file_entry.global_index = global_index;
|
||||
w.put(file_entry);
|
||||
w.extend_to(0x50, 0x00);
|
||||
w.put<GVRHeader>({.magic = 0x47565254,
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ enum class GVRDataFormat : uint8_t {
|
||||
DXT1 = 0x0E,
|
||||
};
|
||||
|
||||
std::string encode_gvm(const Image& img, GVRDataFormat data_format);
|
||||
std::string encode_gvm(const Image& img, GVRDataFormat data_format, const std::string& internal_name, uint32_t global_index);
|
||||
|
||||
constexpr uint16_t encode_rgb565(uint8_t r, uint8_t g, uint8_t b) {
|
||||
return ((r << 8) & 0xF800) | ((g << 3) & 0x07E0) | ((b >> 3) & 0x001F);
|
||||
|
||||
+57
-26
@@ -266,19 +266,53 @@ JSON HTTPServer::generate_client_config_json_st(const Client::Config& config) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_license_json_st(shared_ptr<const License> l) {
|
||||
auto ret = JSON::dict({
|
||||
{"SerialNumber", l->serial_number},
|
||||
{"Flags", l->flags},
|
||||
{"Ep3CurrentMeseta", l->ep3_current_meseta},
|
||||
{"Ep3TotalMesetaEarned", l->ep3_total_meseta_earned},
|
||||
{"BBTeamID", l->bb_team_id},
|
||||
JSON HTTPServer::generate_account_json_st(shared_ptr<const Account> a) {
|
||||
auto dc_nte_licenses_json = JSON::list();
|
||||
for (const auto& it : a->dc_nte_licenses) {
|
||||
dc_nte_licenses_json.emplace_back(it.first);
|
||||
}
|
||||
auto dc_licenses_json = JSON::list();
|
||||
for (const auto& it : a->dc_licenses) {
|
||||
dc_licenses_json.emplace_back(it.first);
|
||||
}
|
||||
auto pc_licenses_json = JSON::list();
|
||||
for (const auto& it : a->pc_licenses) {
|
||||
pc_licenses_json.emplace_back(it.first);
|
||||
}
|
||||
auto gc_licenses_json = JSON::list();
|
||||
for (const auto& it : a->gc_licenses) {
|
||||
gc_licenses_json.emplace_back(it.first);
|
||||
}
|
||||
auto xb_licenses_json = JSON::list();
|
||||
for (const auto& it : a->xb_licenses) {
|
||||
xb_licenses_json.emplace_back(it.first);
|
||||
}
|
||||
auto bb_licenses_json = JSON::list();
|
||||
for (const auto& it : a->bb_licenses) {
|
||||
bb_licenses_json.emplace_back(it.first);
|
||||
}
|
||||
auto auto_patches_json = JSON::list();
|
||||
for (const auto& it : a->auto_patches_enabled) {
|
||||
auto_patches_json.emplace_back(it);
|
||||
}
|
||||
return JSON::dict({
|
||||
{"AccountID", a->account_id},
|
||||
{"Flags", a->flags},
|
||||
{"BanEndTime", a->ban_end_time ? a->ban_end_time : JSON(nullptr)},
|
||||
{"Ep3CurrentMeseta", a->ep3_current_meseta},
|
||||
{"Ep3TotalMesetaEarned", a->ep3_total_meseta_earned},
|
||||
{"BBTeamID", a->bb_team_id},
|
||||
{"LastPlayerName", a->last_player_name},
|
||||
{"AutoReplyMessage", a->auto_reply_message},
|
||||
{"IsTemporary", a->is_temporary},
|
||||
{"DCNTELicenses", std::move(dc_nte_licenses_json)},
|
||||
{"DCLicenses", std::move(dc_licenses_json)},
|
||||
{"PCLicenses", std::move(pc_licenses_json)},
|
||||
{"GCLicenses", std::move(gc_licenses_json)},
|
||||
{"XBLicenses", std::move(xb_licenses_json)},
|
||||
{"BBLicenses", std::move(bb_licenses_json)},
|
||||
{"AutoPatchesEnabled", std::move(auto_patches_json)},
|
||||
});
|
||||
ret.emplace("BanEndTime", l->ban_end_time ? l->ban_end_time : JSON(nullptr));
|
||||
ret.emplace("XBGamertag", !l->xb_gamertag.empty() ? l->xb_gamertag : JSON(nullptr));
|
||||
ret.emplace("XBUserID", l->xb_user_id ? l->xb_user_id : JSON(nullptr));
|
||||
ret.emplace("BBUsername", !l->bb_username.empty() ? l->bb_username : JSON(nullptr));
|
||||
return ret;
|
||||
};
|
||||
|
||||
JSON HTTPServer::generate_game_client_json_st(shared_ptr<const Client> c, shared_ptr<const ItemNameIndex> item_name_index) {
|
||||
@@ -294,7 +328,7 @@ JSON HTTPServer::generate_game_client_json_st(shared_ptr<const Client> c, shared
|
||||
{"LocationFloor", c->floor},
|
||||
{"CanChat", c->can_chat},
|
||||
});
|
||||
ret.emplace("license", c->license ? HTTPServer::generate_license_json_st(c->license) : JSON(nullptr));
|
||||
ret.emplace("Account", c->login ? HTTPServer::generate_account_json_st(c->login->account) : JSON(nullptr));
|
||||
auto l = c->lobby.lock();
|
||||
if (l) {
|
||||
ret.emplace("LobbyID", l->lobby_id);
|
||||
@@ -380,10 +414,10 @@ JSON HTTPServer::generate_game_client_json_st(shared_ptr<const Client> c, shared
|
||||
ret.emplace("BattleDisconnectCount", p->battle_records.disconnect_count.load());
|
||||
|
||||
if (!is_ep3(c->version())) {
|
||||
auto json_for_challenge_times = []<size_t Count>(const parray<ChallengeTime<false>, Count>& times) -> JSON {
|
||||
auto json_for_challenge_times = []<size_t Count>(const parray<ChallengeTime, Count>& times) -> JSON {
|
||||
auto times_json = JSON::list();
|
||||
for (size_t z = 0; z < times.size(); z++) {
|
||||
times_json.emplace_back(times[z].load());
|
||||
times_json.emplace_back(times[z].decode());
|
||||
}
|
||||
return times_json;
|
||||
};
|
||||
@@ -419,11 +453,11 @@ JSON HTTPServer::generate_game_client_json_st(shared_ptr<const Client> c, shared
|
||||
ret.emplace("ChallengeGraveTeam", p->challenge_records.grave_team.decode());
|
||||
ret.emplace("ChallengeGraveMessage", p->challenge_records.grave_message.decode());
|
||||
ret.emplace("ChallengeAwardStateEp1OnlineFlags", p->challenge_records.ep1_online_award_state.rank_award_flags.load());
|
||||
ret.emplace("ChallengeAwardStateEp1OnlineMaxRank", p->challenge_records.ep1_online_award_state.maximum_rank.load());
|
||||
ret.emplace("ChallengeAwardStateEp1OnlineMaxRank", p->challenge_records.ep1_online_award_state.maximum_rank.decode());
|
||||
ret.emplace("ChallengeAwardStateEp2OnlineFlags", p->challenge_records.ep2_online_award_state.rank_award_flags.load());
|
||||
ret.emplace("ChallengeAwardStateEp2OnlineMaxRank", p->challenge_records.ep2_online_award_state.maximum_rank.load());
|
||||
ret.emplace("ChallengeAwardStateEp2OnlineMaxRank", p->challenge_records.ep2_online_award_state.maximum_rank.decode());
|
||||
ret.emplace("ChallengeAwardStateEp1OfflineFlags", p->challenge_records.ep1_offline_award_state.rank_award_flags.load());
|
||||
ret.emplace("ChallengeAwardStateEp1OfflineMaxRank", p->challenge_records.ep1_offline_award_state.maximum_rank.load());
|
||||
ret.emplace("ChallengeAwardStateEp1OfflineMaxRank", p->challenge_records.ep1_offline_award_state.maximum_rank.decode());
|
||||
ret.emplace("ChallengeRankTitle", p->challenge_records.rank_title.decode());
|
||||
}
|
||||
}
|
||||
@@ -498,7 +532,7 @@ JSON HTTPServer::generate_proxy_client_json_st(shared_ptr<const ProxyServer::Lin
|
||||
ret.emplace("DropMode", "proxy");
|
||||
break;
|
||||
}
|
||||
ret.emplace("License", ses->license ? HTTPServer::generate_license_json_st(ses->license) : JSON(nullptr));
|
||||
ret.emplace("Account", ses->login ? HTTPServer::generate_account_json_st(ses->login->account) : JSON(nullptr));
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -585,7 +619,7 @@ JSON HTTPServer::generate_lobby_json_st(shared_ptr<const Lobby> l, shared_ptr<co
|
||||
{"LocationX", item->x},
|
||||
{"LocationZ", item->z},
|
||||
{"DropNumber", item->drop_number},
|
||||
{"VisibilityFlags", item->visibility_flags},
|
||||
{"Flags", item->flags},
|
||||
{"Data", item->data.hex()},
|
||||
{"ItemID", item->data.id.load()},
|
||||
});
|
||||
@@ -766,7 +800,7 @@ JSON HTTPServer::generate_summary_json() const {
|
||||
auto l = c->lobby.lock();
|
||||
clients_json.emplace_back(JSON::dict({
|
||||
{"ID", c->id},
|
||||
{"SerialNumber", c->license ? c->license->serial_number : JSON(nullptr)},
|
||||
{"AccountID", c->login ? c->login->account->account_id : JSON(nullptr)},
|
||||
{"Name", p ? p->disp.name.decode(it.second->language()) : JSON(nullptr)},
|
||||
{"Version", name_for_enum(it.second->version())},
|
||||
{"Language", name_for_language_code(it.second->language())},
|
||||
@@ -780,7 +814,7 @@ JSON HTTPServer::generate_summary_json() const {
|
||||
auto proxy_clients_json = JSON::list();
|
||||
for (const auto& it : this->state->proxy_server->all_sessions()) {
|
||||
proxy_clients_json.emplace_back(JSON::dict({
|
||||
{"SerialNumber", it.second->license ? it.second->license->serial_number : JSON(nullptr)},
|
||||
{"AccountID", it.second->login ? it.second->login->account->account_id : JSON(nullptr)},
|
||||
{"Name", it.second->character_name},
|
||||
{"Version", name_for_enum(it.second->version())},
|
||||
{"Language", name_for_language_code(it.second->language())},
|
||||
@@ -848,10 +882,7 @@ JSON HTTPServer::generate_common_tables_json() const {
|
||||
auto [set_v2, set_v3_v4] = call_on_event_thread<pair<shared_ptr<const CommonItemSet>, shared_ptr<const CommonItemSet>>>(this->state->base, [&]() {
|
||||
return make_pair(this->state->common_item_set_v2, this->state->common_item_set_v3_v4);
|
||||
});
|
||||
return JSON::dict({
|
||||
{"v1_v2", set_v2->json()},
|
||||
{"v3_v4", set_v3_v4->json()},
|
||||
});
|
||||
return JSON::dict({{"v1_v2", set_v2->json()}, {"v3_v4", set_v3_v4->json()}});
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_rare_tables_json() const {
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ protected:
|
||||
|
||||
static JSON generate_quest_json_st(std::shared_ptr<const Quest> q);
|
||||
static JSON generate_client_config_json_st(const Client::Config& config);
|
||||
static JSON generate_license_json_st(std::shared_ptr<const License> l);
|
||||
static JSON generate_account_json_st(std::shared_ptr<const Account> a);
|
||||
static JSON generate_game_client_json_st(std::shared_ptr<const Client> c, std::shared_ptr<const ItemNameIndex> item_name_index);
|
||||
static JSON generate_proxy_client_json_st(std::shared_ptr<const ProxyServer::LinkedSession> ses);
|
||||
static JSON generate_lobby_json_st(std::shared_ptr<const Lobby> l, std::shared_ptr<const ItemNameIndex> item_name_index);
|
||||
|
||||
+10
-10
@@ -12,31 +12,31 @@ struct HDLCHeader {
|
||||
uint8_t address; // 0xFF usually
|
||||
uint8_t control; // 0x03 for PPP
|
||||
be_uint16_t protocol;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(HDLCHeader, 5);
|
||||
|
||||
struct LCPHeader {
|
||||
uint8_t command;
|
||||
uint8_t request_id;
|
||||
be_uint16_t size;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(LCPHeader, 4);
|
||||
|
||||
struct PAPHeader {
|
||||
uint8_t command;
|
||||
uint8_t request_id;
|
||||
be_uint16_t size;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PAPHeader, 4);
|
||||
|
||||
struct IPCPHeader {
|
||||
uint8_t command;
|
||||
uint8_t request_id;
|
||||
be_uint16_t size;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(IPCPHeader, 4);
|
||||
|
||||
struct EthernetHeader {
|
||||
parray<uint8_t, 6> dest_mac;
|
||||
parray<uint8_t, 6> src_mac;
|
||||
be_uint16_t protocol;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(EthernetHeader, 0x0E);
|
||||
|
||||
struct ARPHeader {
|
||||
be_uint16_t hardware_type;
|
||||
@@ -44,7 +44,7 @@ struct ARPHeader {
|
||||
uint8_t hwaddr_len;
|
||||
uint8_t paddr_len;
|
||||
be_uint16_t operation;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ARPHeader, 8);
|
||||
|
||||
struct IPv4Header {
|
||||
uint8_t version_ihl;
|
||||
@@ -57,14 +57,14 @@ struct IPv4Header {
|
||||
be_uint16_t checksum;
|
||||
be_uint32_t src_addr;
|
||||
be_uint32_t dest_addr;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(IPv4Header, 0x14);
|
||||
|
||||
struct UDPHeader {
|
||||
be_uint16_t src_port;
|
||||
be_uint16_t dest_port;
|
||||
be_uint16_t size;
|
||||
be_uint16_t checksum;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(UDPHeader, 8);
|
||||
|
||||
struct TCPHeader {
|
||||
enum Flag {
|
||||
@@ -87,7 +87,7 @@ struct TCPHeader {
|
||||
be_uint16_t window;
|
||||
be_uint16_t checksum;
|
||||
be_uint16_t urgent_ptr;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(TCPHeader, 0x14);
|
||||
|
||||
struct DHCPHeader {
|
||||
uint8_t opcode = 0;
|
||||
@@ -105,7 +105,7 @@ struct DHCPHeader {
|
||||
parray<uint8_t, 0xC0> unused_bootp_legacy;
|
||||
be_uint32_t magic = 0x63825363;
|
||||
// Options follow here, terminated with FF
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(DHCPHeader, 0xF0);
|
||||
|
||||
struct FrameInfo {
|
||||
enum class LinkType {
|
||||
|
||||
+55
-51
@@ -124,6 +124,7 @@ IPStackSimulator::IPStackSimulator(
|
||||
shared_ptr<ServerState> state)
|
||||
: base(base),
|
||||
state(state),
|
||||
next_network_id(1),
|
||||
pcap_text_log_file(state->ip_stack_debug ? fopen("IPStackSimulator-Log.txt", "wt") : nullptr) {
|
||||
this->host_mac_address_bytes.clear(0x90);
|
||||
this->broadcast_mac_address_bytes.clear(0xFF);
|
||||
@@ -169,6 +170,10 @@ void IPStackSimulator::add_socket(const string& name, int fd, Protocol proto) {
|
||||
this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd), forward_as_tuple(name, proto, std::move(l)));
|
||||
}
|
||||
|
||||
shared_ptr<IPStackSimulator::IPClient> IPStackSimulator::get_network(uint64_t network_id) const {
|
||||
return this->network_id_to_client.at(network_id);
|
||||
}
|
||||
|
||||
uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_addr) {
|
||||
// Use an address not on the same subnet as the client, so that PSO Plus and
|
||||
// Episode III will think they're talking to a remote network and won't reject
|
||||
@@ -180,8 +185,10 @@ uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_ad
|
||||
}
|
||||
}
|
||||
|
||||
IPStackSimulator::IPClient::IPClient(shared_ptr<IPStackSimulator> sim, Protocol protocol, struct bufferevent* bev)
|
||||
IPStackSimulator::IPClient::IPClient(
|
||||
shared_ptr<IPStackSimulator> sim, uint64_t network_id, Protocol protocol, struct bufferevent* bev)
|
||||
: sim(sim),
|
||||
network_id(network_id),
|
||||
bev(bev, bufferevent_free),
|
||||
protocol(protocol),
|
||||
mac_addr(0),
|
||||
@@ -200,7 +207,7 @@ void IPStackSimulator::IPClient::on_idle_timeout() {
|
||||
auto sim = this->sim.lock();
|
||||
if (sim) {
|
||||
ip_stack_simulator_log.info("Idle timeout expired on virtual network %d", bufferevent_getfd(this->bev.get()));
|
||||
sim->disconnect_client(this->bev.get());
|
||||
sim->disconnect_client(this->network_id);
|
||||
} else {
|
||||
ip_stack_simulator_log.info("Idle timeout expired on virtual network %d, but simulator is missing", bufferevent_getfd(this->bev.get()));
|
||||
}
|
||||
@@ -227,40 +234,45 @@ IPStackSimulator::IPClient::TCPConnection::TCPConnection()
|
||||
bytes_received(0),
|
||||
bytes_sent(0) {}
|
||||
|
||||
void IPStackSimulator::disconnect_client(struct bufferevent* bev) {
|
||||
ip_stack_simulator_log.info("Virtual network %d disconnected", bufferevent_getfd(bev));
|
||||
this->bev_to_client.erase(bev);
|
||||
void IPStackSimulator::disconnect_client(uint64_t network_id) {
|
||||
ip_stack_simulator_log.info("Virtual network N-%" PRIu64 " disconnected", network_id);
|
||||
this->network_id_to_client.erase(network_id);
|
||||
}
|
||||
|
||||
void IPStackSimulator::dispatch_on_listen_accept(
|
||||
struct evconnlistener* listener, evutil_socket_t fd,
|
||||
struct sockaddr* address, int socklen, void* ctx) {
|
||||
reinterpret_cast<IPStackSimulator*>(ctx)->on_listen_accept(
|
||||
listener, fd, address, socklen);
|
||||
reinterpret_cast<IPStackSimulator*>(ctx)->on_listen_accept(listener, fd, address, socklen);
|
||||
}
|
||||
|
||||
void IPStackSimulator::on_listen_accept(struct evconnlistener* listener,
|
||||
evutil_socket_t fd, struct sockaddr*, int) {
|
||||
void IPStackSimulator::on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr*, int) {
|
||||
struct sockaddr_storage remote_addr;
|
||||
get_socket_addresses(fd, nullptr, &remote_addr);
|
||||
if (this->state->banned_ipv4_ranges->check(remote_addr)) {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
int listen_fd = evconnlistener_get_fd(listener);
|
||||
|
||||
const ListeningSocket* listening_socket;
|
||||
try {
|
||||
listening_socket = &this->listening_sockets.at(listen_fd);
|
||||
} catch (const out_of_range&) {
|
||||
ip_stack_simulator_log.info("Virtual network %d connected via unknown listener %d; disconnecting", fd, listen_fd);
|
||||
ip_stack_simulator_log.info("Virtual network fd %d connected via unknown listener %d; disconnecting", fd, listen_fd);
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
ip_stack_simulator_log.info("Virtual network %d connected via %s", fd, listening_socket->name.c_str());
|
||||
uint64_t network_id = this->next_network_id++;
|
||||
ip_stack_simulator_log.info("Virtual network N-%" PRIu64 " connected via %s", network_id, listening_socket->name.c_str());
|
||||
|
||||
struct bufferevent* bev = bufferevent_socket_new(this->base.get(), fd,
|
||||
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
auto c = make_shared<IPClient>(this->shared_from_this(), listening_socket->protocol, bev);
|
||||
this->bev_to_client.emplace(make_pair(bev, c));
|
||||
struct bufferevent* bev = bufferevent_socket_new(this->base.get(), fd, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
auto c = make_shared<IPClient>(this->shared_from_this(), network_id, listening_socket->protocol, bev);
|
||||
this->network_id_to_client.emplace(c->network_id, c);
|
||||
|
||||
bufferevent_setcb(bev, &IPStackSimulator::dispatch_on_client_input, nullptr,
|
||||
&IPStackSimulator::dispatch_on_client_error, this);
|
||||
bufferevent_setcb(bev, &IPStackSimulator::IPClient::dispatch_on_client_input, nullptr,
|
||||
&IPStackSimulator::IPClient::dispatch_on_client_error, c.get());
|
||||
bufferevent_enable(bev, EV_READ | EV_WRITE);
|
||||
}
|
||||
|
||||
@@ -276,31 +288,26 @@ void IPStackSimulator::on_listen_error(struct evconnlistener* listener) {
|
||||
event_base_loopexit(this->base.get(), nullptr);
|
||||
}
|
||||
|
||||
void IPStackSimulator::dispatch_on_client_input(
|
||||
struct bufferevent* bev, void* ctx) {
|
||||
reinterpret_cast<IPStackSimulator*>(ctx)->on_client_input(bev);
|
||||
void IPStackSimulator::IPClient::dispatch_on_client_input(struct bufferevent* bev, void* ctx) {
|
||||
reinterpret_cast<IPClient*>(ctx)->on_client_input(bev);
|
||||
}
|
||||
|
||||
void IPStackSimulator::on_client_input(struct bufferevent* bev) {
|
||||
void IPStackSimulator::IPClient::on_client_input(struct bufferevent* bev) {
|
||||
struct evbuffer* buf = bufferevent_get_input(bev);
|
||||
|
||||
shared_ptr<IPClient> c;
|
||||
try {
|
||||
c = this->bev_to_client.at(bev);
|
||||
} catch (const out_of_range&) {
|
||||
auto sim = this->sim.lock();
|
||||
if (!sim) {
|
||||
size_t bytes = evbuffer_get_length(buf);
|
||||
ip_stack_simulator_log.warning("Ignoring data received from unregistered virtual network (0x%zX bytes)",
|
||||
bytes);
|
||||
ip_stack_simulator_log.warning("Ignoring data from unregistered virtual network (0x%zX bytes)", bytes);
|
||||
evbuffer_drain(buf, bytes);
|
||||
return;
|
||||
}
|
||||
|
||||
auto sim = c->sim.lock();
|
||||
uint64_t idle_timeout_usecs = sim ? sim->state->client_idle_timeout_usecs : 60000000;
|
||||
struct timeval tv = usecs_to_timeval(idle_timeout_usecs);
|
||||
event_add(c->idle_timeout_event.get(), &tv);
|
||||
event_add(this->idle_timeout_event.get(), &tv);
|
||||
|
||||
switch (c->protocol) {
|
||||
switch (this->protocol) {
|
||||
case Protocol::ETHERNET_TAPSERVER:
|
||||
case Protocol::HDLC_TAPSERVER:
|
||||
while (evbuffer_get_length(buf) >= 2) {
|
||||
@@ -315,7 +322,7 @@ void IPStackSimulator::on_client_input(struct bufferevent* bev) {
|
||||
evbuffer_remove(buf, frame.data(), frame.size());
|
||||
|
||||
try {
|
||||
this->on_client_frame(c, frame);
|
||||
sim->on_client_frame(this->shared_from_this(), frame);
|
||||
} catch (const exception& e) {
|
||||
if (ip_stack_simulator_log.warning("Failed to process frame: %s", e.what())) {
|
||||
print_data(stderr, frame);
|
||||
@@ -350,7 +357,7 @@ void IPStackSimulator::on_client_input(struct bufferevent* bev) {
|
||||
evbuffer_remove(buf, frame.data(), frame.size());
|
||||
|
||||
try {
|
||||
this->on_client_frame(c, frame);
|
||||
sim->on_client_frame(this->shared_from_this(), frame);
|
||||
} catch (const exception& e) {
|
||||
if (ip_stack_simulator_log.warning("Failed to process frame: %s", e.what())) {
|
||||
print_data(stderr, frame);
|
||||
@@ -361,18 +368,19 @@ void IPStackSimulator::on_client_input(struct bufferevent* bev) {
|
||||
}
|
||||
}
|
||||
|
||||
void IPStackSimulator::dispatch_on_client_error(
|
||||
struct bufferevent* bev, short events, void* ctx) {
|
||||
reinterpret_cast<IPStackSimulator*>(ctx)->on_client_error(bev, events);
|
||||
void IPStackSimulator::IPClient::dispatch_on_client_error(struct bufferevent* bev, short events, void* ctx) {
|
||||
reinterpret_cast<IPClient*>(ctx)->on_client_error(bev, events);
|
||||
}
|
||||
void IPStackSimulator::on_client_error(struct bufferevent* bev, short events) {
|
||||
void IPStackSimulator::IPClient::on_client_error(struct bufferevent*, short events) {
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
ip_stack_simulator_log.warning("Virtual network caused error %d (%s)", err,
|
||||
evutil_socket_error_to_string(err));
|
||||
ip_stack_simulator_log.warning("Virtual network caused error %d (%s)", err, evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
|
||||
this->disconnect_client(bev);
|
||||
auto sim = this->sim.lock();
|
||||
if (sim) {
|
||||
sim->disconnect_client(this->network_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1335,23 +1343,19 @@ void IPStackSimulator::open_server_connection(shared_ptr<IPClient> c, IPClient::
|
||||
string conn_str = this->str_for_tcp_connection(c, conn);
|
||||
if (port_config->behavior == ServerBehavior::PROXY_SERVER) {
|
||||
if (!this->state->proxy_server.get()) {
|
||||
ip_stack_simulator_log.error("TCP connection %s is to non-running proxy server",
|
||||
conn_str.c_str());
|
||||
ip_stack_simulator_log.error("TCP connection %s is to non-running proxy server", conn_str.c_str());
|
||||
flush_and_free_bufferevent(bevs[1]);
|
||||
} else {
|
||||
this->state->proxy_server->connect_client(bevs[1], conn.server_port);
|
||||
ip_stack_simulator_log.info("Connected TCP connection %s to proxy server",
|
||||
conn_str.c_str());
|
||||
this->state->proxy_server->connect_virtual_client(bevs[1], c->network_id, conn.server_port);
|
||||
ip_stack_simulator_log.info("Connected TCP connection %s to proxy server", conn_str.c_str());
|
||||
}
|
||||
} else if (this->state->game_server.get()) {
|
||||
this->state->game_server->connect_client(bevs[1], c->ipv4_addr,
|
||||
conn.client_port, conn.server_port, port_config->version,
|
||||
port_config->behavior);
|
||||
ip_stack_simulator_log.info("Connected TCP connection %s to game server",
|
||||
conn_str.c_str());
|
||||
this->state->game_server->connect_virtual_client(
|
||||
bevs[1], c->network_id, c->ipv4_addr, conn.client_port,
|
||||
conn.server_port, port_config->version, port_config->behavior);
|
||||
ip_stack_simulator_log.info("Connected TCP connection %s to game server", conn_str.c_str());
|
||||
} else {
|
||||
ip_stack_simulator_log.error("No server available for TCP connection %s",
|
||||
conn_str.c_str());
|
||||
ip_stack_simulator_log.error("No server available for TCP connection %s", conn_str.c_str());
|
||||
flush_and_free_bufferevent(bevs[1]);
|
||||
}
|
||||
}
|
||||
|
||||
+36
-32
@@ -22,29 +22,14 @@ public:
|
||||
HDLC_RAW,
|
||||
};
|
||||
|
||||
IPStackSimulator(
|
||||
std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<ServerState> state);
|
||||
~IPStackSimulator();
|
||||
|
||||
void listen(const std::string& name, const std::string& socket_path, Protocol protocol);
|
||||
void listen(const std::string& name, const std::string& addr, int port, Protocol protocol);
|
||||
void listen(const std::string& name, int port, Protocol protocol);
|
||||
void add_socket(const std::string& name, int fd, Protocol protocol);
|
||||
|
||||
static uint32_t connect_address_for_remote_address(uint32_t remote_addr);
|
||||
|
||||
private:
|
||||
std::shared_ptr<struct event_base> base;
|
||||
std::shared_ptr<ServerState> state;
|
||||
|
||||
using unique_listener = std::unique_ptr<struct evconnlistener, void (*)(struct evconnlistener*)>;
|
||||
using unique_bufferevent = std::unique_ptr<struct bufferevent, void (*)(struct bufferevent*)>;
|
||||
using unique_evbuffer = std::unique_ptr<struct evbuffer, void (*)(struct evbuffer*)>;
|
||||
using unique_event = std::unique_ptr<struct event, void (*)(struct event*)>;
|
||||
|
||||
struct IPClient {
|
||||
struct IPClient : std::enable_shared_from_this<IPClient> {
|
||||
std::weak_ptr<IPStackSimulator> sim;
|
||||
uint64_t network_id;
|
||||
|
||||
unique_bufferevent bev;
|
||||
Protocol protocol;
|
||||
@@ -86,12 +71,41 @@ private:
|
||||
|
||||
unique_event idle_timeout_event;
|
||||
|
||||
IPClient(std::shared_ptr<IPStackSimulator> sim, Protocol protocol, struct bufferevent* bev);
|
||||
IPClient(std::shared_ptr<IPStackSimulator> sim, uint64_t network_id, Protocol protocol, struct bufferevent* bev);
|
||||
|
||||
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
|
||||
void on_client_input(struct bufferevent* bev);
|
||||
static void dispatch_on_client_error(struct bufferevent* bev, short events, void* ctx);
|
||||
void on_client_error(struct bufferevent* bev, short events);
|
||||
|
||||
static void dispatch_on_idle_timeout(evutil_socket_t fd, short events, void* ctx);
|
||||
void on_idle_timeout();
|
||||
};
|
||||
|
||||
IPStackSimulator(
|
||||
std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<ServerState> state);
|
||||
~IPStackSimulator();
|
||||
|
||||
void listen(const std::string& name, const std::string& socket_path, Protocol protocol);
|
||||
void listen(const std::string& name, const std::string& addr, int port, Protocol protocol);
|
||||
void listen(const std::string& name, int port, Protocol protocol);
|
||||
void add_socket(const std::string& name, int fd, Protocol protocol);
|
||||
|
||||
static uint32_t connect_address_for_remote_address(uint32_t remote_addr);
|
||||
|
||||
std::shared_ptr<IPClient> get_network(uint64_t network_id) const;
|
||||
inline const std::unordered_map<uint64_t, std::shared_ptr<IPClient>>& all_networks() const {
|
||||
return this->network_id_to_client;
|
||||
}
|
||||
|
||||
void disconnect_client(uint64_t network_id);
|
||||
|
||||
private:
|
||||
std::shared_ptr<struct event_base> base;
|
||||
std::shared_ptr<ServerState> state;
|
||||
uint64_t next_network_id;
|
||||
|
||||
struct ListeningSocket {
|
||||
std::string name;
|
||||
Protocol protocol;
|
||||
@@ -104,35 +118,26 @@ private:
|
||||
};
|
||||
|
||||
std::unordered_map<int, ListeningSocket> listening_sockets;
|
||||
std::unordered_map<struct bufferevent*, std::shared_ptr<IPClient>> bev_to_client;
|
||||
std::unordered_map<uint64_t, std::shared_ptr<IPClient>> network_id_to_client;
|
||||
|
||||
parray<uint8_t, 6> host_mac_address_bytes;
|
||||
parray<uint8_t, 6> broadcast_mac_address_bytes;
|
||||
|
||||
FILE* pcap_text_log_file;
|
||||
|
||||
void disconnect_client(struct bufferevent* bev);
|
||||
|
||||
static uint64_t tcp_conn_key_for_connection(const IPClient::TCPConnection& conn);
|
||||
static uint64_t tcp_conn_key_for_client_frame(const IPv4Header& ipv4, const TCPHeader& tcp);
|
||||
static uint64_t tcp_conn_key_for_client_frame(const FrameInfo& fi);
|
||||
|
||||
static std::string str_for_ipv4_netloc(uint32_t addr, uint16_t port);
|
||||
static std::string str_for_tcp_connection(std::shared_ptr<const IPClient> c,
|
||||
const IPClient::TCPConnection& conn);
|
||||
static std::string str_for_tcp_connection(std::shared_ptr<const IPClient> c, const IPClient::TCPConnection& conn);
|
||||
|
||||
static void dispatch_on_listen_accept(struct evconnlistener* listener,
|
||||
evutil_socket_t fd, struct sockaddr* address, int socklen, void* ctx);
|
||||
void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd,
|
||||
struct sockaddr* address, int socklen);
|
||||
void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr* address, int socklen);
|
||||
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
|
||||
void on_listen_error(struct evconnlistener* listener);
|
||||
|
||||
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
|
||||
void on_client_input(struct bufferevent* bev);
|
||||
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;
|
||||
|
||||
@@ -158,8 +163,7 @@ private:
|
||||
struct evbuffer* src_buf = nullptr,
|
||||
size_t src_bytes = 0);
|
||||
|
||||
void open_server_connection(
|
||||
std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn);
|
||||
void open_server_connection(std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn);
|
||||
|
||||
void log_frame(const std::string& data) const;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "IPV4RangeSet.hh"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
IPV4RangeSet::IPV4RangeSet(const JSON& json) {
|
||||
for (const auto& it : json.as_list()) {
|
||||
// String should be of the form a.b.c.d or a.b.c.d/e
|
||||
auto tokens = split(it->as_string(), '/');
|
||||
|
||||
size_t mask_bits;
|
||||
if (tokens.size() == 1) {
|
||||
mask_bits = 32;
|
||||
} else if (tokens.size() == 2) {
|
||||
mask_bits = stoul(tokens[1], nullptr, 10);
|
||||
if (mask_bits > 32) {
|
||||
throw runtime_error("invalid IPv4 address range");
|
||||
}
|
||||
} else {
|
||||
throw runtime_error("invalid IPv4 address range");
|
||||
}
|
||||
|
||||
auto addr_tokens = split(tokens[0], '.');
|
||||
if (addr_tokens.size() != 4) {
|
||||
throw runtime_error("invalid IPv4 address");
|
||||
}
|
||||
uint32_t addr = 0;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
size_t end_pos = 0;
|
||||
size_t new_byte = stoul(addr_tokens[z], &end_pos, 10);
|
||||
if (end_pos != addr_tokens[z].size() || new_byte > 0xFF) {
|
||||
throw runtime_error("invalid IPv4 address");
|
||||
}
|
||||
addr = (addr << 8) | new_byte;
|
||||
}
|
||||
addr &= (0xFFFFFFFF << (32 - mask_bits));
|
||||
|
||||
this->ranges.emplace(addr, mask_bits);
|
||||
}
|
||||
}
|
||||
|
||||
JSON IPV4RangeSet::json() const {
|
||||
auto ret = JSON::list();
|
||||
for (const auto& it : this->ranges) {
|
||||
uint32_t addr = it.first;
|
||||
uint8_t mask_bits = it.second;
|
||||
ret.emplace_back(string_printf("%hhu.%hhu.%hhu.%hhu/%hhu",
|
||||
static_cast<uint8_t>((addr >> 24) & 0xFF),
|
||||
static_cast<uint8_t>((addr >> 16) & 0xFF),
|
||||
static_cast<uint8_t>((addr >> 8) & 0xFF),
|
||||
static_cast<uint8_t>(addr & 0xFF),
|
||||
mask_bits));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool IPV4RangeSet::check(uint32_t addr) const {
|
||||
auto it = this->ranges.upper_bound(addr);
|
||||
if (it == this->ranges.begin()) {
|
||||
return false; // addr is before any range
|
||||
}
|
||||
const auto& range = *(--it);
|
||||
return (((range.first ^ addr) & (0xFFFFFFFF << (32 - range.second))) == 0);
|
||||
}
|
||||
|
||||
bool IPV4RangeSet::check(const struct sockaddr_storage& ss) const {
|
||||
if (ss.ss_family != AF_INET) {
|
||||
return false;
|
||||
}
|
||||
const sockaddr_in* sin = reinterpret_cast<const sockaddr_in*>(&ss);
|
||||
return this->check(ntohl(sin->sin_addr.s_addr));
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <phosg/JSON.hh>
|
||||
#include <set>
|
||||
|
||||
class IPV4RangeSet {
|
||||
public:
|
||||
IPV4RangeSet() = default;
|
||||
explicit IPV4RangeSet(const JSON& json);
|
||||
|
||||
JSON json() const;
|
||||
|
||||
bool check(uint32_t addr) const;
|
||||
bool check(const struct sockaddr_storage& ss) const;
|
||||
|
||||
protected:
|
||||
std::map<uint32_t, uint8_t> ranges; // {addr: mask_bits}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "QuestAvailabilityExpression.hh"
|
||||
#include "IntegralExpression.hh"
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
@@ -21,16 +21,16 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
QuestAvailabilityExpression::QuestAvailabilityExpression(const string& text)
|
||||
IntegralExpression::IntegralExpression(const string& text)
|
||||
: root(this->parse_expr(text)) {}
|
||||
|
||||
QuestAvailabilityExpression::BinaryOperatorNode::BinaryOperatorNode(
|
||||
IntegralExpression::BinaryOperatorNode::BinaryOperatorNode(
|
||||
Type type, unique_ptr<const Node>&& left, unique_ptr<const Node>&& right)
|
||||
: type(type),
|
||||
left(std::move(left)),
|
||||
right(std::move(right)) {}
|
||||
|
||||
bool QuestAvailabilityExpression::BinaryOperatorNode::operator==(const Node& other) const {
|
||||
bool IntegralExpression::BinaryOperatorNode::operator==(const Node& other) const {
|
||||
try {
|
||||
const BinaryOperatorNode& other_bin = dynamic_cast<const BinaryOperatorNode&>(other);
|
||||
return other_bin.type == this->type && *other_bin.left == *this->left && *other_bin.right == *this->right;
|
||||
@@ -39,7 +39,7 @@ bool QuestAvailabilityExpression::BinaryOperatorNode::operator==(const Node& oth
|
||||
}
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::BinaryOperatorNode::evaluate(const Env& env) const {
|
||||
int64_t IntegralExpression::BinaryOperatorNode::evaluate(const Env& env) const {
|
||||
switch (this->type) {
|
||||
case Type::LOGICAL_OR:
|
||||
return this->left->evaluate(env) || this->right->evaluate(env);
|
||||
@@ -82,7 +82,7 @@ int64_t QuestAvailabilityExpression::BinaryOperatorNode::evaluate(const Env& env
|
||||
}
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::BinaryOperatorNode::str() const {
|
||||
string IntegralExpression::BinaryOperatorNode::str() const {
|
||||
switch (this->type) {
|
||||
case Type::LOGICAL_OR:
|
||||
return "(" + this->left->str() + ") || (" + this->right->str() + ")";
|
||||
@@ -125,11 +125,11 @@ string QuestAvailabilityExpression::BinaryOperatorNode::str() const {
|
||||
}
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::UnaryOperatorNode::UnaryOperatorNode(Type type, unique_ptr<const Node>&& sub)
|
||||
IntegralExpression::UnaryOperatorNode::UnaryOperatorNode(Type type, unique_ptr<const Node>&& sub)
|
||||
: type(type),
|
||||
sub(std::move(sub)) {}
|
||||
|
||||
bool QuestAvailabilityExpression::UnaryOperatorNode::operator==(const Node& other) const {
|
||||
bool IntegralExpression::UnaryOperatorNode::operator==(const Node& other) const {
|
||||
try {
|
||||
const UnaryOperatorNode& other_un = dynamic_cast<const UnaryOperatorNode&>(other);
|
||||
return other_un.type == this->type && *other_un.sub == *this->sub;
|
||||
@@ -138,7 +138,7 @@ bool QuestAvailabilityExpression::UnaryOperatorNode::operator==(const Node& othe
|
||||
}
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::UnaryOperatorNode::evaluate(const Env& env) const {
|
||||
int64_t IntegralExpression::UnaryOperatorNode::evaluate(const Env& env) const {
|
||||
switch (this->type) {
|
||||
case Type::LOGICAL_NOT:
|
||||
return !this->sub->evaluate(env);
|
||||
@@ -151,7 +151,7 @@ int64_t QuestAvailabilityExpression::UnaryOperatorNode::evaluate(const Env& env)
|
||||
}
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::UnaryOperatorNode::str() const {
|
||||
string IntegralExpression::UnaryOperatorNode::str() const {
|
||||
switch (this->type) {
|
||||
case Type::LOGICAL_NOT:
|
||||
return "!(" + this->sub->str() + ")";
|
||||
@@ -164,10 +164,10 @@ string QuestAvailabilityExpression::UnaryOperatorNode::str() const {
|
||||
}
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index)
|
||||
IntegralExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index)
|
||||
: flag_index(flag_index) {}
|
||||
|
||||
bool QuestAvailabilityExpression::FlagLookupNode::operator==(const Node& other) const {
|
||||
bool IntegralExpression::FlagLookupNode::operator==(const Node& other) const {
|
||||
try {
|
||||
const FlagLookupNode& other_flag = dynamic_cast<const FlagLookupNode&>(other);
|
||||
return other_flag.flag_index == this->flag_index;
|
||||
@@ -176,20 +176,23 @@ bool QuestAvailabilityExpression::FlagLookupNode::operator==(const Node& other)
|
||||
}
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::FlagLookupNode::evaluate(const Env& env) const {
|
||||
int64_t IntegralExpression::FlagLookupNode::evaluate(const Env& env) const {
|
||||
if (!env.flags) {
|
||||
throw runtime_error("quest flags not available");
|
||||
}
|
||||
return env.flags->get(this->flag_index) ? 1 : 0;
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::FlagLookupNode::str() const {
|
||||
string IntegralExpression::FlagLookupNode::str() const {
|
||||
return string_printf("F_%04hX", this->flag_index);
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode(
|
||||
IntegralExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode(
|
||||
Episode episode, uint8_t stage_index)
|
||||
: episode(episode),
|
||||
stage_index(stage_index) {}
|
||||
|
||||
bool QuestAvailabilityExpression::ChallengeCompletionLookupNode::operator==(const Node& other) const {
|
||||
bool IntegralExpression::ChallengeCompletionLookupNode::operator==(const Node& other) const {
|
||||
try {
|
||||
const ChallengeCompletionLookupNode& other_cc = dynamic_cast<const ChallengeCompletionLookupNode&>(other);
|
||||
return other_cc.episode == this->episode && other_cc.stage_index == this->stage_index;
|
||||
@@ -198,7 +201,10 @@ bool QuestAvailabilityExpression::ChallengeCompletionLookupNode::operator==(cons
|
||||
}
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::ChallengeCompletionLookupNode::evaluate(const Env& env) const {
|
||||
int64_t IntegralExpression::ChallengeCompletionLookupNode::evaluate(const Env& env) const {
|
||||
if (!env.challenge_records) {
|
||||
throw runtime_error("challenge records not available");
|
||||
}
|
||||
if (this->episode == Episode::EP1) {
|
||||
return env.challenge_records->times_ep1_online.at(this->stage_index).has_value();
|
||||
} else if (this->episode == Episode::EP2) {
|
||||
@@ -207,14 +213,14 @@ int64_t QuestAvailabilityExpression::ChallengeCompletionLookupNode::evaluate(con
|
||||
return false;
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::ChallengeCompletionLookupNode::str() const {
|
||||
string IntegralExpression::ChallengeCompletionLookupNode::str() const {
|
||||
return string_printf("CC_%s_%hhu", abbreviation_for_episode(this->episode), static_cast<uint8_t>(this->stage_index + 1));
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name)
|
||||
IntegralExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name)
|
||||
: reward_name(reward_name) {}
|
||||
|
||||
bool QuestAvailabilityExpression::TeamRewardLookupNode::operator==(const Node& other) const {
|
||||
bool IntegralExpression::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;
|
||||
@@ -223,60 +229,60 @@ bool QuestAvailabilityExpression::TeamRewardLookupNode::operator==(const Node& o
|
||||
}
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::TeamRewardLookupNode::evaluate(const Env& env) const {
|
||||
int64_t IntegralExpression::TeamRewardLookupNode::evaluate(const Env& env) const {
|
||||
return (env.team && env.team->has_reward(this->reward_name)) ? 1 : 0;
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::TeamRewardLookupNode::str() const {
|
||||
string IntegralExpression::TeamRewardLookupNode::str() const {
|
||||
return "T_" + this->reward_name;
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::NumPlayersLookupNode::NumPlayersLookupNode() {}
|
||||
IntegralExpression::NumPlayersLookupNode::NumPlayersLookupNode() {}
|
||||
|
||||
bool QuestAvailabilityExpression::NumPlayersLookupNode::operator==(const Node& other) const {
|
||||
bool IntegralExpression::NumPlayersLookupNode::operator==(const Node& other) const {
|
||||
return dynamic_cast<const NumPlayersLookupNode*>(&other) != nullptr;
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::NumPlayersLookupNode::evaluate(const Env& env) const {
|
||||
int64_t IntegralExpression::NumPlayersLookupNode::evaluate(const Env& env) const {
|
||||
return env.num_players;
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::NumPlayersLookupNode::str() const {
|
||||
string IntegralExpression::NumPlayersLookupNode::str() const {
|
||||
return "V_NumPlayers";
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::EventLookupNode::EventLookupNode() {}
|
||||
IntegralExpression::EventLookupNode::EventLookupNode() {}
|
||||
|
||||
bool QuestAvailabilityExpression::EventLookupNode::operator==(const Node& other) const {
|
||||
bool IntegralExpression::EventLookupNode::operator==(const Node& other) const {
|
||||
return dynamic_cast<const EventLookupNode*>(&other) != nullptr;
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::EventLookupNode::evaluate(const Env& env) const {
|
||||
int64_t IntegralExpression::EventLookupNode::evaluate(const Env& env) const {
|
||||
return env.event;
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::EventLookupNode::str() const {
|
||||
string IntegralExpression::EventLookupNode::str() const {
|
||||
return "V_Event";
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::V1PresenceLookupNode::V1PresenceLookupNode() {}
|
||||
IntegralExpression::V1PresenceLookupNode::V1PresenceLookupNode() {}
|
||||
|
||||
bool QuestAvailabilityExpression::V1PresenceLookupNode::operator==(const Node& other) const {
|
||||
bool IntegralExpression::V1PresenceLookupNode::operator==(const Node& other) const {
|
||||
return dynamic_cast<const V1PresenceLookupNode*>(&other) != nullptr;
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::V1PresenceLookupNode::evaluate(const Env& env) const {
|
||||
int64_t IntegralExpression::V1PresenceLookupNode::evaluate(const Env& env) const {
|
||||
return env.v1_present ? 1 : 0;
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::V1PresenceLookupNode::str() const {
|
||||
string IntegralExpression::V1PresenceLookupNode::str() const {
|
||||
return "V_V1Present";
|
||||
}
|
||||
|
||||
QuestAvailabilityExpression::ConstantNode::ConstantNode(int64_t value)
|
||||
IntegralExpression::ConstantNode::ConstantNode(int64_t value)
|
||||
: value(value) {}
|
||||
|
||||
bool QuestAvailabilityExpression::ConstantNode::operator==(const Node& other) const {
|
||||
bool IntegralExpression::ConstantNode::operator==(const Node& other) const {
|
||||
try {
|
||||
const ConstantNode& other_const = dynamic_cast<const ConstantNode&>(other);
|
||||
return other_const.value == this->value;
|
||||
@@ -285,15 +291,15 @@ bool QuestAvailabilityExpression::ConstantNode::operator==(const Node& other) co
|
||||
}
|
||||
}
|
||||
|
||||
int64_t QuestAvailabilityExpression::ConstantNode::evaluate(const Env&) const {
|
||||
int64_t IntegralExpression::ConstantNode::evaluate(const Env&) const {
|
||||
return this->value;
|
||||
}
|
||||
|
||||
string QuestAvailabilityExpression::ConstantNode::str() const {
|
||||
string IntegralExpression::ConstantNode::str() const {
|
||||
return string_printf("%" PRId64, this->value);
|
||||
}
|
||||
|
||||
unique_ptr<const QuestAvailabilityExpression::Node> QuestAvailabilityExpression::parse_expr(string_view text) {
|
||||
unique_ptr<const IntegralExpression::Node> IntegralExpression::parse_expr(string_view text) {
|
||||
// Strip off spaces and fully-enclosing parentheses
|
||||
for (;;) {
|
||||
size_t starting_size = text.size();
|
||||
@@ -334,13 +340,13 @@ unique_ptr<const QuestAvailabilityExpression::Node> QuestAvailabilityExpression:
|
||||
// Check for unary operators
|
||||
if (text[0] == '!') {
|
||||
return make_unique<UnaryOperatorNode>(UnaryOperatorNode::Type::LOGICAL_NOT,
|
||||
QuestAvailabilityExpression::parse_expr(text.substr(1)));
|
||||
IntegralExpression::parse_expr(text.substr(1)));
|
||||
} else if (text[0] == '~') {
|
||||
return make_unique<UnaryOperatorNode>(UnaryOperatorNode::Type::BITWISE_NOT,
|
||||
QuestAvailabilityExpression::parse_expr(text.substr(1)));
|
||||
IntegralExpression::parse_expr(text.substr(1)));
|
||||
} else if (text[0] == '-') {
|
||||
return make_unique<UnaryOperatorNode>(UnaryOperatorNode::Type::NEGATIVE,
|
||||
QuestAvailabilityExpression::parse_expr(text.substr(1)));
|
||||
IntegralExpression::parse_expr(text.substr(1)));
|
||||
}
|
||||
|
||||
// Check for binary operators at the root level
|
||||
@@ -377,8 +383,8 @@ unique_ptr<const QuestAvailabilityExpression::Node> QuestAvailabilityExpression:
|
||||
((z < oper.first.size()) || (text.compare(z - oper.first.size(), oper.first.size(), oper.first) != 0)) &&
|
||||
(text.compare(z, oper.first.size(), oper.first) == 0) &&
|
||||
(text.compare(z + oper.first.size(), oper.first.size(), oper.first) != 0)) {
|
||||
auto left = QuestAvailabilityExpression::parse_expr(text.substr(0, z));
|
||||
auto right = QuestAvailabilityExpression::parse_expr(text.substr(z + oper.first.size()));
|
||||
auto left = IntegralExpression::parse_expr(text.substr(0, z));
|
||||
auto right = IntegralExpression::parse_expr(text.substr(z + oper.first.size()));
|
||||
return make_unique<BinaryOperatorNode>(oper.second, std::move(left), std::move(right));
|
||||
}
|
||||
}
|
||||
@@ -13,23 +13,23 @@
|
||||
#include "StaticGameData.hh"
|
||||
#include "TeamIndex.hh"
|
||||
|
||||
class QuestAvailabilityExpression {
|
||||
class IntegralExpression {
|
||||
public:
|
||||
struct Env {
|
||||
const QuestFlagsForDifficulty* flags;
|
||||
const PlayerRecordsBB_Challenge* challenge_records;
|
||||
const PlayerRecordsChallengeBB* challenge_records;
|
||||
std::shared_ptr<const TeamIndex::Team> team;
|
||||
size_t num_players;
|
||||
uint8_t event;
|
||||
bool v1_present;
|
||||
};
|
||||
|
||||
QuestAvailabilityExpression(const std::string& text);
|
||||
~QuestAvailabilityExpression() = default;
|
||||
inline bool operator==(const QuestAvailabilityExpression& other) const {
|
||||
IntegralExpression(const std::string& text);
|
||||
~IntegralExpression() = default;
|
||||
inline bool operator==(const IntegralExpression& other) const {
|
||||
return this->root->operator==(*other.root);
|
||||
}
|
||||
inline bool operator!=(const QuestAvailabilityExpression& other) const {
|
||||
inline bool operator!=(const IntegralExpression& other) const {
|
||||
return !this->operator==(other);
|
||||
}
|
||||
inline int64_t evaluate(const Env& env) const {
|
||||
+3
-2
@@ -66,8 +66,9 @@ bool ItemCreator::are_rare_drops_allowed() const {
|
||||
// Note: The client has an additional check here, which appears to be a subtle
|
||||
// anti-cheating measure. There is a flag on the client, initially zero, which
|
||||
// is set to 1 when certain unexpected item-related things happen (for
|
||||
// example, a player possessing a mag with a level above 200). When the flag
|
||||
// is set, this function returns false, which prevents all rare item drops.
|
||||
// example, a player possessing a mag with a level above 200, or a stack of
|
||||
// consumables with an amount above the stack size limit). When the flag is
|
||||
// set, this function returns false, which prevents all rare item drops.
|
||||
// newserv intentionally does not implement this flag.
|
||||
return (this->mode != GameMode::CHALLENGE);
|
||||
}
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ private:
|
||||
struct UnitResult {
|
||||
uint8_t unit;
|
||||
int8_t modifier;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(UnitResult, 2);
|
||||
std::array<std::vector<UnitResult>, 13> unit_results_by_star_count;
|
||||
|
||||
// Note: The original implementation uses 17 different random states for some
|
||||
|
||||
@@ -101,6 +101,14 @@ bool ItemData::empty() const {
|
||||
}
|
||||
|
||||
uint32_t ItemData::primary_identifier() const {
|
||||
// Primary identifiers are like:
|
||||
// - 00TTSS00 = weapon (T = type, S = subtype; subtype is 0 for ES weapons)
|
||||
// - 01TTSS00 = armor/shield/unit
|
||||
// - 02TT0000 = mag
|
||||
// - 0302ZZLL = tech disk (Z = tech number, L = level)
|
||||
// - 03TTSS00 = tool
|
||||
// - 04000000 = meseta
|
||||
|
||||
// The game treats any item starting with 04 as Meseta, and ignores the rest
|
||||
// of data1 (the value is in data2)
|
||||
if (this->data1[0] == 0x04) {
|
||||
|
||||
+3
-3
@@ -121,7 +121,7 @@ struct ItemData {
|
||||
parray<be_uint16_t, 6> data1wb;
|
||||
parray<le_uint32_t, 3> data1d;
|
||||
parray<be_uint32_t, 3> data1db;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
le_uint32_t id;
|
||||
union {
|
||||
parray<uint8_t, 4> data2;
|
||||
@@ -129,7 +129,7 @@ struct ItemData {
|
||||
parray<be_uint16_t, 2> data2wb;
|
||||
le_uint32_t data2d;
|
||||
be_uint32_t data2db;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
ItemData();
|
||||
ItemData(const ItemData& other);
|
||||
@@ -195,4 +195,4 @@ struct ItemData {
|
||||
bool empty() const;
|
||||
|
||||
static bool compare_for_sort(const ItemData& a, const ItemData& b);
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ItemData, 0x14);
|
||||
|
||||
+14
-6
@@ -166,7 +166,7 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
|
||||
}
|
||||
}
|
||||
|
||||
// For weapons, add the grind and percentages, or S-rank name if applicable
|
||||
// For weapons, add the grind and bonuses, or S-rank name if applicable
|
||||
if (item.data1[0] == 0x00) {
|
||||
if (item.data1[3] > 0) {
|
||||
ret_tokens.emplace_back(string_printf("+%hhu", item.data1[3]));
|
||||
@@ -201,7 +201,7 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
|
||||
}
|
||||
|
||||
} else { // Not S-rank (extended name bits not set)
|
||||
parray<int8_t, 5> percentages(0);
|
||||
parray<int8_t, 5> bonuses(0);
|
||||
for (size_t x = 0; x < 3; x++) {
|
||||
uint8_t which = item.data1[6 + 2 * x];
|
||||
uint8_t value = item.data1[7 + 2 * x];
|
||||
@@ -211,12 +211,20 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo
|
||||
if (which > 5) {
|
||||
ret_tokens.emplace_back(string_printf("!PC:%02hhX%02hhX", which, value));
|
||||
} else {
|
||||
percentages[which - 1] = value;
|
||||
bonuses[which - 1] = value;
|
||||
}
|
||||
}
|
||||
if (!percentages.is_filled_with(0)) {
|
||||
ret_tokens.emplace_back(string_printf("%hhd/%hhd/%hhd/%hhd/%hhd",
|
||||
percentages[0], percentages[1], percentages[2], percentages[3], percentages[4]));
|
||||
if (!bonuses.is_filled_with(0)) {
|
||||
bool should_include_hit = (bonuses[4] != 0);
|
||||
bool should_highlight_hit = include_color_escapes && (bonuses[4] > 0);
|
||||
if (should_include_hit) {
|
||||
ret_tokens.emplace_back(string_printf("%hhd/%hhd/%hhd/%hhd/%s%hhd",
|
||||
bonuses[0], bonuses[1], bonuses[2], bonuses[3],
|
||||
(should_highlight_hit ? "$CG" : ""), bonuses[4]));
|
||||
} else {
|
||||
ret_tokens.emplace_back(string_printf("%hhd/%hhd/%hhd/%hhd",
|
||||
bonuses[0], bonuses[1], bonuses[2], bonuses[3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+57
-56
@@ -79,9 +79,9 @@ ItemParameterTable::ItemParameterTable(shared_ptr<const string> data, Version ve
|
||||
case Version::GC_V3:
|
||||
case Version::XB_V3: {
|
||||
if (is_big_endian(this->version)) {
|
||||
this->offsets_v3_be = &this->r.pget<TableOffsetsV3V4<true>>(offset_table_offset);
|
||||
this->offsets_v3_be = &this->r.pget<TableOffsetsV3V4BE>(offset_table_offset);
|
||||
} else {
|
||||
this->offsets_v3_le = &this->r.pget<TableOffsetsV3V4<false>>(offset_table_offset);
|
||||
this->offsets_v3_le = &this->r.pget<TableOffsetsV3V4>(offset_table_offset);
|
||||
}
|
||||
this->num_weapon_classes = 0xAA;
|
||||
this->num_tool_classes = 0x18;
|
||||
@@ -93,7 +93,7 @@ ItemParameterTable::ItemParameterTable(shared_ptr<const string> data, Version ve
|
||||
}
|
||||
|
||||
case Version::BB_V4: {
|
||||
this->offsets_v4 = &this->r.pget<TableOffsetsV3V4<false>>(offset_table_offset);
|
||||
this->offsets_v4 = &this->r.pget<TableOffsetsV3V4>(offset_table_offset);
|
||||
this->num_weapon_classes = 0xED;
|
||||
this->num_tool_classes = 0x1B;
|
||||
this->item_stars_first_id = 0xB1;
|
||||
@@ -207,7 +207,7 @@ ItemParameterTable::WeaponV4 ItemParameterTable::WeaponGCNTE::to_v4() const {
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
ItemParameterTable::WeaponV4 ItemParameterTable::WeaponV3<IsBigEndian>::to_v4() const {
|
||||
ItemParameterTable::WeaponV4 ItemParameterTable::WeaponV3T<IsBigEndian>::to_v4() const {
|
||||
WeaponV4 ret;
|
||||
ret.base.id = this->base.id.load();
|
||||
ret.base.type = this->base.type.load();
|
||||
@@ -282,7 +282,7 @@ ItemParameterTable::ArmorOrShieldV4 ItemParameterTable::ArmorOrShieldV1V2::to_v4
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
ItemParameterTable::ArmorOrShieldV4 ItemParameterTable::ArmorOrShieldV3<IsBigEndian>::to_v4() const {
|
||||
ItemParameterTable::ArmorOrShieldV4 ItemParameterTable::ArmorOrShieldV3T<IsBigEndian>::to_v4() const {
|
||||
ArmorOrShieldV4 ret;
|
||||
ret.base.id = this->base.id.load();
|
||||
ret.base.type = this->base.type.load();
|
||||
@@ -324,7 +324,7 @@ ItemParameterTable::UnitV4 ItemParameterTable::UnitV1V2::to_v4() const {
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
ItemParameterTable::UnitV4 ItemParameterTable::UnitV3<IsBigEndian>::to_v4() const {
|
||||
ItemParameterTable::UnitV4 ItemParameterTable::UnitV3T<IsBigEndian>::to_v4() const {
|
||||
UnitV4 ret;
|
||||
ret.base.id = this->base.id.load();
|
||||
ret.base.type = this->base.type.load();
|
||||
@@ -371,7 +371,7 @@ ItemParameterTable::MagV4 ItemParameterTable::MagV2::to_v4() const {
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
ItemParameterTable::MagV4 ItemParameterTable::MagV3<IsBigEndian>::to_v4() const {
|
||||
ItemParameterTable::MagV4 ItemParameterTable::MagV3T<IsBigEndian>::to_v4() const {
|
||||
MagV4 ret;
|
||||
ret.base.id = this->base.id.load();
|
||||
ret.base.type = this->base.type.load();
|
||||
@@ -402,7 +402,7 @@ ItemParameterTable::ToolV4 ItemParameterTable::ToolV1V2::to_v4() const {
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
ItemParameterTable::ToolV4 ItemParameterTable::ToolV3<IsBigEndian>::to_v4() const {
|
||||
ItemParameterTable::ToolV4 ItemParameterTable::ToolV3T<IsBigEndian>::to_v4() const {
|
||||
ToolV4 ret;
|
||||
ret.base.id = this->base.id.load();
|
||||
ret.base.type = this->base.type.load();
|
||||
@@ -416,13 +416,13 @@ ItemParameterTable::ToolV4 ItemParameterTable::ToolV3<IsBigEndian>::to_v4() cons
|
||||
|
||||
template <bool IsBigEndian>
|
||||
size_t indirect_lookup_2d_count(const StringReader& r, size_t root_offset, size_t co_index) {
|
||||
using ArrayRefT = typename std::conditional_t<IsBigEndian, ItemParameterTable::ArrayRefBE, ItemParameterTable::ArrayRefLE>;
|
||||
using ArrayRefT = typename std::conditional_t<IsBigEndian, ItemParameterTable::ArrayRefBE, ItemParameterTable::ArrayRef>;
|
||||
return r.pget<ArrayRefT>(root_offset + sizeof(ArrayRefT) * co_index).count;
|
||||
}
|
||||
|
||||
template <typename T, bool IsBigEndian>
|
||||
const T& indirect_lookup_2d(const StringReader& r, size_t root_offset, size_t co_index, size_t item_index) {
|
||||
using ArrayRefT = typename std::conditional_t<IsBigEndian, ItemParameterTable::ArrayRefBE, ItemParameterTable::ArrayRefLE>;
|
||||
using ArrayRefT = typename std::conditional_t<IsBigEndian, ItemParameterTable::ArrayRefBE, ItemParameterTable::ArrayRef>;
|
||||
|
||||
const auto& co = r.pget<ArrayRefT>(root_offset + sizeof(ArrayRefT) * co_index);
|
||||
if (item_index >= co.count) {
|
||||
@@ -473,9 +473,9 @@ const ItemParameterTable::WeaponV4& ItemParameterTable::get_weapon(uint8_t data1
|
||||
} else if (this->offsets_gc_nte) {
|
||||
def_v4 = indirect_lookup_2d<WeaponGCNTE, true>(this->r, this->offsets_gc_nte->weapon_table, data1_1, data1_2).to_v4();
|
||||
} else if (this->offsets_v3_le) {
|
||||
def_v4 = indirect_lookup_2d<WeaponV3<false>, false>(this->r, this->offsets_v3_le->weapon_table, data1_1, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<WeaponV3, false>(this->r, this->offsets_v3_le->weapon_table, data1_1, data1_2).to_v4();
|
||||
} else if (this->offsets_v3_be) {
|
||||
def_v4 = indirect_lookup_2d<WeaponV3<true>, true>(this->r, this->offsets_v3_be->weapon_table, data1_1, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<WeaponV3BE, true>(this->r, this->offsets_v3_be->weapon_table, data1_1, data1_2).to_v4();
|
||||
} else {
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
@@ -528,11 +528,11 @@ const ItemParameterTable::ArmorOrShieldV4& ItemParameterTable::get_armor_or_shie
|
||||
} else if (this->offsets_v1_v2) {
|
||||
def_v4 = indirect_lookup_2d<ArmorOrShieldV1V2, false>(this->r, this->offsets_v1_v2->armor_table, data1_1 - 1, data1_2).to_v4();
|
||||
} else if (this->offsets_gc_nte) {
|
||||
def_v4 = indirect_lookup_2d<ArmorOrShieldV3<true>, true>(this->r, this->offsets_gc_nte->armor_table, data1_1 - 1, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<ArmorOrShieldV3BE, true>(this->r, this->offsets_gc_nte->armor_table, data1_1 - 1, data1_2).to_v4();
|
||||
} else if (this->offsets_v3_le) {
|
||||
def_v4 = indirect_lookup_2d<ArmorOrShieldV3<false>, false>(this->r, this->offsets_v3_le->armor_table, data1_1 - 1, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<ArmorOrShieldV3, false>(this->r, this->offsets_v3_le->armor_table, data1_1 - 1, data1_2).to_v4();
|
||||
} else if (this->offsets_v3_be) {
|
||||
def_v4 = indirect_lookup_2d<ArmorOrShieldV3<true>, true>(this->r, this->offsets_v3_be->armor_table, data1_1 - 1, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<ArmorOrShieldV3BE, true>(this->r, this->offsets_v3_be->armor_table, data1_1 - 1, data1_2).to_v4();
|
||||
} else {
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
@@ -580,11 +580,11 @@ const ItemParameterTable::UnitV4& ItemParameterTable::get_unit(uint8_t data1_2)
|
||||
} else if (this->offsets_v1_v2) {
|
||||
def_v4 = indirect_lookup_2d<UnitV1V2, false>(this->r, this->offsets_v1_v2->unit_table, 0, data1_2).to_v4();
|
||||
} else if (this->offsets_gc_nte) {
|
||||
def_v4 = indirect_lookup_2d<UnitV3<true>, true>(this->r, this->offsets_gc_nte->unit_table, 0, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<UnitV3BE, true>(this->r, this->offsets_gc_nte->unit_table, 0, data1_2).to_v4();
|
||||
} else if (this->offsets_v3_le) {
|
||||
def_v4 = indirect_lookup_2d<UnitV3<false>, false>(this->r, this->offsets_v3_le->unit_table, 0, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<UnitV3, false>(this->r, this->offsets_v3_le->unit_table, 0, data1_2).to_v4();
|
||||
} else if (this->offsets_v3_be) {
|
||||
def_v4 = indirect_lookup_2d<UnitV3<true>, true>(this->r, this->offsets_v3_be->unit_table, 0, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<UnitV3BE, true>(this->r, this->offsets_v3_be->unit_table, 0, data1_2).to_v4();
|
||||
} else {
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
@@ -636,11 +636,11 @@ const ItemParameterTable::MagV4& ItemParameterTable::get_mag(uint8_t data1_1) co
|
||||
def_v4 = indirect_lookup_2d<MagV2, false>(this->r, this->offsets_v1_v2->mag_table, 0, data1_1).to_v4();
|
||||
}
|
||||
} else if (this->offsets_gc_nte) {
|
||||
def_v4 = indirect_lookup_2d<MagV3<true>, true>(this->r, this->offsets_gc_nte->mag_table, 0, data1_1).to_v4();
|
||||
def_v4 = indirect_lookup_2d<MagV3BE, true>(this->r, this->offsets_gc_nte->mag_table, 0, data1_1).to_v4();
|
||||
} else if (this->offsets_v3_le) {
|
||||
def_v4 = indirect_lookup_2d<MagV3<false>, false>(this->r, this->offsets_v3_le->mag_table, 0, data1_1).to_v4();
|
||||
def_v4 = indirect_lookup_2d<MagV3, false>(this->r, this->offsets_v3_le->mag_table, 0, data1_1).to_v4();
|
||||
} else if (this->offsets_v3_be) {
|
||||
def_v4 = indirect_lookup_2d<MagV3<true>, true>(this->r, this->offsets_v3_be->mag_table, 0, data1_1).to_v4();
|
||||
def_v4 = indirect_lookup_2d<MagV3BE, true>(this->r, this->offsets_v3_be->mag_table, 0, data1_1).to_v4();
|
||||
} else {
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
@@ -693,11 +693,11 @@ const ItemParameterTable::ToolV4& ItemParameterTable::get_tool(uint8_t data1_1,
|
||||
} else if (this->offsets_v1_v2) {
|
||||
def_v4 = indirect_lookup_2d<ToolV1V2, false>(this->r, this->offsets_v1_v2->tool_table, data1_1, data1_2).to_v4();
|
||||
} else if (this->offsets_gc_nte) {
|
||||
def_v4 = indirect_lookup_2d<ToolV3<true>, true>(this->r, this->offsets_gc_nte->tool_table, data1_1, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<ToolV3BE, true>(this->r, this->offsets_gc_nte->tool_table, data1_1, data1_2).to_v4();
|
||||
} else if (this->offsets_v3_le) {
|
||||
def_v4 = indirect_lookup_2d<ToolV3<false>, false>(this->r, this->offsets_v3_le->tool_table, data1_1, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<ToolV3, false>(this->r, this->offsets_v3_le->tool_table, data1_1, data1_2).to_v4();
|
||||
} else if (this->offsets_v3_be) {
|
||||
def_v4 = indirect_lookup_2d<ToolV3<true>, true>(this->r, this->offsets_v3_be->tool_table, data1_1, data1_2).to_v4();
|
||||
def_v4 = indirect_lookup_2d<ToolV3BE, true>(this->r, this->offsets_v3_be->tool_table, data1_1, data1_2).to_v4();
|
||||
} else {
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
@@ -706,13 +706,13 @@ const ItemParameterTable::ToolV4& ItemParameterTable::get_tool(uint8_t data1_1,
|
||||
}
|
||||
}
|
||||
|
||||
template <typename ToolT, bool IsBigEndian>
|
||||
template <typename ToolDefT, bool IsBigEndian>
|
||||
pair<uint8_t, uint8_t> ItemParameterTable::find_tool_by_id_t(uint32_t tool_table_offset, uint32_t item_id) const {
|
||||
const auto* cos = &this->r.pget<ArrayRef<IsBigEndian>>(
|
||||
tool_table_offset, this->num_tool_classes * sizeof(ArrayRef<IsBigEndian>));
|
||||
const auto* cos = &this->r.pget<ArrayRefT<IsBigEndian>>(
|
||||
tool_table_offset, this->num_tool_classes * sizeof(ArrayRefT<IsBigEndian>));
|
||||
for (size_t z = 0; z < this->num_tool_classes; z++) {
|
||||
const auto& co = cos[z];
|
||||
const auto* defs = &this->r.pget<ToolT>(co.offset, sizeof(ToolT) * co.count);
|
||||
const auto* defs = &this->r.pget<ToolDefT>(co.offset, sizeof(ToolDefT) * co.count);
|
||||
for (size_t y = 0; y < co.count; y++) {
|
||||
if (defs[y].base.id == item_id) {
|
||||
return make_pair(z, y);
|
||||
@@ -728,11 +728,11 @@ pair<uint8_t, uint8_t> ItemParameterTable::find_tool_by_id(uint32_t item_id) con
|
||||
} else if (this->offsets_v1_v2) {
|
||||
return this->find_tool_by_id_t<ToolV1V2, false>(this->offsets_v1_v2->tool_table, item_id);
|
||||
} else if (this->offsets_gc_nte) {
|
||||
return this->find_tool_by_id_t<ToolV3<true>, true>(this->offsets_gc_nte->tool_table, item_id);
|
||||
return this->find_tool_by_id_t<ToolV3BE, true>(this->offsets_gc_nte->tool_table, item_id);
|
||||
} else if (this->offsets_v3_le) {
|
||||
return this->find_tool_by_id_t<ToolV3<false>, false>(this->offsets_v3_le->tool_table, item_id);
|
||||
return this->find_tool_by_id_t<ToolV3, false>(this->offsets_v3_le->tool_table, item_id);
|
||||
} else if (this->offsets_v3_be) {
|
||||
return this->find_tool_by_id_t<ToolV3<true>, true>(this->offsets_v3_be->tool_table, item_id);
|
||||
return this->find_tool_by_id_t<ToolV3BE, true>(this->offsets_v3_be->tool_table, item_id);
|
||||
} else if (this->offsets_v4) {
|
||||
return this->find_tool_by_id_t<ToolV4, false>(this->offsets_v4->tool_table, item_id);
|
||||
} else {
|
||||
@@ -752,7 +752,7 @@ float ItemParameterTable::get_sale_divisor_t(const OffsetsT* offsets, uint8_t da
|
||||
return this->r.pget<FloatT>(offsets->weapon_sale_divisor_table + data1_1 * sizeof(FloatT));
|
||||
|
||||
case 1: {
|
||||
const auto& divisors = this->r.pget<NonWeaponSaleDivisors<IsBigEndian>>(offsets->sale_divisor_table);
|
||||
const auto& divisors = this->r.pget<NonWeaponSaleDivisorsT<IsBigEndian>>(offsets->sale_divisor_table);
|
||||
switch (data1_1) {
|
||||
case 1:
|
||||
return divisors.armor_divisor;
|
||||
@@ -765,7 +765,7 @@ float ItemParameterTable::get_sale_divisor_t(const OffsetsT* offsets, uint8_t da
|
||||
}
|
||||
|
||||
case 2: {
|
||||
const auto& divisors = this->r.pget<NonWeaponSaleDivisors<IsBigEndian>>(offsets->sale_divisor_table);
|
||||
const auto& divisors = this->r.pget<NonWeaponSaleDivisorsT<IsBigEndian>>(offsets->sale_divisor_table);
|
||||
return divisors.mag_divisor;
|
||||
}
|
||||
|
||||
@@ -803,22 +803,22 @@ const ItemParameterTable::MagFeedResult& ItemParameterTable::get_mag_feed_result
|
||||
|
||||
uint32_t offset;
|
||||
if (this->offsets_dc_protos) {
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsets<false>>(this->offsets_dc_protos->mag_feed_table);
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsets>(this->offsets_dc_protos->mag_feed_table);
|
||||
offset = table_offsets.offsets[table_index];
|
||||
} else if (this->offsets_v1_v2) {
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsets<false>>(this->offsets_v1_v2->mag_feed_table);
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsets>(this->offsets_v1_v2->mag_feed_table);
|
||||
offset = table_offsets.offsets[table_index];
|
||||
} else if (this->offsets_gc_nte) {
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsets<true>>(this->offsets_gc_nte->mag_feed_table);
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsetsBE>(this->offsets_gc_nte->mag_feed_table);
|
||||
offset = table_offsets.offsets[table_index];
|
||||
} else if (this->offsets_v3_le) {
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsets<false>>(this->offsets_v3_le->mag_feed_table);
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsets>(this->offsets_v3_le->mag_feed_table);
|
||||
offset = table_offsets.offsets[table_index];
|
||||
} else if (this->offsets_v3_be) {
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsets<true>>(this->offsets_v3_be->mag_feed_table);
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsetsBE>(this->offsets_v3_be->mag_feed_table);
|
||||
offset = table_offsets.offsets[table_index];
|
||||
} else if (this->offsets_v4) {
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsets<false>>(this->offsets_v4->mag_feed_table);
|
||||
const auto& table_offsets = this->r.pget<MagFeedResultsListOffsets>(this->offsets_v4->mag_feed_table);
|
||||
offset = table_offsets.offsets[table_index];
|
||||
} else {
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
@@ -856,24 +856,24 @@ uint8_t ItemParameterTable::get_special_stars(uint8_t special) const {
|
||||
: 0;
|
||||
}
|
||||
|
||||
const ItemParameterTable::Special<false>& ItemParameterTable::get_special(uint8_t special) const {
|
||||
const ItemParameterTable::Special& ItemParameterTable::get_special(uint8_t special) const {
|
||||
special &= 0x3F;
|
||||
if (special >= this->num_specials) {
|
||||
throw out_of_range("invalid special index");
|
||||
}
|
||||
|
||||
if (this->offsets_dc_protos) {
|
||||
return this->r.pget<Special<false>>(this->offsets_dc_protos->special_data_table + sizeof(Special<false>) * special);
|
||||
return this->r.pget<Special>(this->offsets_dc_protos->special_data_table + sizeof(Special) * special);
|
||||
} else if (this->offsets_v1_v2) {
|
||||
return this->r.pget<Special<false>>(this->offsets_v1_v2->special_data_table + sizeof(Special<false>) * special);
|
||||
return this->r.pget<Special>(this->offsets_v1_v2->special_data_table + sizeof(Special) * special);
|
||||
} else if (this->offsets_v3_le) {
|
||||
return this->r.pget<Special<false>>(this->offsets_v3_le->special_data_table + sizeof(Special<false>) * special);
|
||||
return this->r.pget<Special>(this->offsets_v3_le->special_data_table + sizeof(Special) * special);
|
||||
} else if (this->offsets_gc_nte) {
|
||||
if ((special >= this->parsed_specials.size()) || (this->parsed_specials[special].type != 0xFFFF)) {
|
||||
if (special >= this->parsed_specials.size()) {
|
||||
this->parsed_specials.resize(special + 1);
|
||||
}
|
||||
const auto& sp_be = this->r.pget<Special<true>>(this->offsets_gc_nte->special_data_table + sizeof(Special<true>) * special);
|
||||
const auto& sp_be = this->r.pget<SpecialBE>(this->offsets_gc_nte->special_data_table + sizeof(SpecialBE) * special);
|
||||
this->parsed_specials[special].type = sp_be.type.load();
|
||||
this->parsed_specials[special].amount = sp_be.amount.load();
|
||||
}
|
||||
@@ -883,13 +883,13 @@ const ItemParameterTable::Special<false>& ItemParameterTable::get_special(uint8_
|
||||
if (special >= this->parsed_specials.size()) {
|
||||
this->parsed_specials.resize(special + 1);
|
||||
}
|
||||
const auto& sp_be = this->r.pget<Special<true>>(this->offsets_v3_be->special_data_table + sizeof(Special<true>) * special);
|
||||
const auto& sp_be = this->r.pget<SpecialBE>(this->offsets_v3_be->special_data_table + sizeof(SpecialBE) * special);
|
||||
this->parsed_specials[special].type = sp_be.type.load();
|
||||
this->parsed_specials[special].amount = sp_be.amount.load();
|
||||
}
|
||||
return this->parsed_specials[special];
|
||||
} else if (this->offsets_v4) {
|
||||
return this->r.pget<Special<false>>(this->offsets_v4->special_data_table + sizeof(Special<false>) * special);
|
||||
return this->r.pget<Special>(this->offsets_v4->special_data_table + sizeof(Special) * special);
|
||||
} else {
|
||||
throw logic_error("table is not v2, v3, or v4");
|
||||
}
|
||||
@@ -1014,14 +1014,15 @@ uint8_t ItemParameterTable::get_item_base_stars(const ItemData& item) const {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t ItemParameterTable::get_item_adjusted_stars(const ItemData& item) const {
|
||||
uint8_t ItemParameterTable::get_item_adjusted_stars(const ItemData& item, bool ignore_unidentified) const {
|
||||
uint8_t ret = this->get_item_base_stars(item);
|
||||
if (item.data1[0] == 0) {
|
||||
bool is_unidentified = (!ignore_unidentified) && (item.data1[4] & 0x80);
|
||||
if (ret < 9) {
|
||||
if (!(item.data1[4] & 0x80)) {
|
||||
if (!is_unidentified) {
|
||||
ret += this->get_special_stars(item.data1[4]);
|
||||
}
|
||||
} else if (item.data1[4] & 0x80) {
|
||||
} else if (is_unidentified) {
|
||||
ret = 0;
|
||||
}
|
||||
} else if (item.data1[0] == 1) {
|
||||
@@ -1050,7 +1051,7 @@ bool ItemParameterTable::is_unsealable_item(uint8_t data1_0, uint8_t data1_1, ui
|
||||
if (this->offsets_dc_protos || this->offsets_v1_v2 || this->offsets_gc_nte) {
|
||||
return false;
|
||||
} else if (this->offsets_v3_le) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v3_le->unsealable_table);
|
||||
const auto& co = this->r.pget<ArrayRef>(this->offsets_v3_le->unsealable_table);
|
||||
offset = co.offset;
|
||||
count = co.count;
|
||||
} else if (this->offsets_v3_be) {
|
||||
@@ -1058,7 +1059,7 @@ bool ItemParameterTable::is_unsealable_item(uint8_t data1_0, uint8_t data1_1, ui
|
||||
offset = co.offset;
|
||||
count = co.count;
|
||||
} else if (this->offsets_v4) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v4->unsealable_table);
|
||||
const auto& co = this->r.pget<ArrayRef>(this->offsets_v4->unsealable_table);
|
||||
offset = co.offset;
|
||||
count = co.count;
|
||||
} else {
|
||||
@@ -1110,7 +1111,7 @@ const std::map<uint32_t, std::vector<ItemParameterTable::ItemCombination>>& Item
|
||||
static const std::map<uint32_t, std::vector<ItemParameterTable::ItemCombination>> empty_map;
|
||||
return empty_map;
|
||||
} else if (this->offsets_v3_le) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v3_le->combination_table);
|
||||
const auto& co = this->r.pget<ArrayRef>(this->offsets_v3_le->combination_table);
|
||||
offset = co.offset;
|
||||
count = co.count;
|
||||
} else if (this->offsets_v3_be) {
|
||||
@@ -1118,7 +1119,7 @@ const std::map<uint32_t, std::vector<ItemParameterTable::ItemCombination>>& Item
|
||||
offset = co.offset;
|
||||
count = co.count;
|
||||
} else if (this->offsets_v4) {
|
||||
const auto& co = this->r.pget<ArrayRefLE>(this->offsets_v4->combination_table);
|
||||
const auto& co = this->r.pget<ArrayRef>(this->offsets_v4->combination_table);
|
||||
offset = co.offset;
|
||||
count = co.count;
|
||||
} else {
|
||||
@@ -1137,7 +1138,7 @@ const std::map<uint32_t, std::vector<ItemParameterTable::ItemCombination>>& Item
|
||||
|
||||
template <bool IsBigEndian>
|
||||
size_t ItemParameterTable::num_events_t(uint32_t base_offset) const {
|
||||
return this->r.pget<ArrayRef<IsBigEndian>>(base_offset).count;
|
||||
return this->r.pget<ArrayRefT<IsBigEndian>>(base_offset).count;
|
||||
}
|
||||
|
||||
size_t ItemParameterTable::num_events() const {
|
||||
@@ -1157,11 +1158,11 @@ size_t ItemParameterTable::num_events() const {
|
||||
template <bool IsBigEndian>
|
||||
std::pair<const ItemParameterTable::EventItem*, size_t> ItemParameterTable::get_event_items_t(
|
||||
uint32_t base_offset, uint8_t event_number) const {
|
||||
const auto& co = this->r.pget<ArrayRef<IsBigEndian>>(base_offset);
|
||||
const auto& co = this->r.pget<ArrayRefT<IsBigEndian>>(base_offset);
|
||||
if (event_number >= co.count) {
|
||||
throw out_of_range("invalid event number");
|
||||
}
|
||||
const auto& event_co = this->r.pget<ArrayRef<IsBigEndian>>(co.offset + sizeof(ArrayRef<IsBigEndian>) * event_number);
|
||||
const auto& event_co = this->r.pget<ArrayRefT<IsBigEndian>>(co.offset + sizeof(ArrayRefT<IsBigEndian>) * event_number);
|
||||
const auto* defs = &this->r.pget<EventItem>(event_co.offset, event_co.count * sizeof(EventItem));
|
||||
return make_pair(defs, event_co.count);
|
||||
}
|
||||
|
||||
+148
-106
@@ -19,42 +19,43 @@ public:
|
||||
// being null or not in each public function. Rewrite this and make it better.
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct ArrayRef {
|
||||
struct ArrayRefT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
/* 00 */ U32T count;
|
||||
/* 04 */ U32T offset;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
struct ArrayRefLE : ArrayRef<false> {
|
||||
} __attribute__((packed));
|
||||
struct ArrayRefBE : ArrayRef<true> {
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
using ArrayRef = ArrayRefT<false>;
|
||||
using ArrayRefBE = ArrayRefT<true>;
|
||||
check_struct_size(ArrayRef, 8);
|
||||
check_struct_size(ArrayRefBE, 8);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct ItemBaseV2 {
|
||||
struct ItemBaseV2T {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
// id specifies several things; notably, it doubles as the index of the
|
||||
// item's name in the text archive (e.g. TextEnglish) collection 0.
|
||||
/* 00 */ U32T id = 0xFFFFFFFF;
|
||||
/* 04 */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
template <bool IsBigEndian>
|
||||
struct ItemBaseV3 : ItemBaseV2<IsBigEndian> {
|
||||
struct ItemBaseV3T : ItemBaseV2T<IsBigEndian> {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
/* 04 */ U16T type = 0;
|
||||
/* 06 */ U16T skin = 0;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
template <bool IsBigEndian>
|
||||
struct ItemBaseV4 : ItemBaseV3<IsBigEndian> {
|
||||
struct ItemBaseV4T : ItemBaseV3T<IsBigEndian> {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
/* 08 */ U32T team_points = 0;
|
||||
/* 0C */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
struct WeaponV4;
|
||||
struct WeaponDCProtos {
|
||||
/* 00 */ ItemBaseV2<false> base;
|
||||
/* 00 */ ItemBaseV2T<false> base;
|
||||
/* 04 */ le_uint16_t class_flags = 0;
|
||||
/* 06 */ le_uint16_t atp_min = 0;
|
||||
/* 08 */ le_uint16_t atp_max = 0;
|
||||
@@ -68,10 +69,10 @@ public:
|
||||
/* 14 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(WeaponDCProtos, 0x14);
|
||||
|
||||
struct WeaponV1V2 {
|
||||
/* 00 */ ItemBaseV2<false> base;
|
||||
/* 00 */ ItemBaseV2T<false> base;
|
||||
/* 04 */ le_uint16_t class_flags = 0;
|
||||
/* 06 */ le_uint16_t atp_min = 0;
|
||||
/* 08 */ le_uint16_t atp_max = 0;
|
||||
@@ -87,10 +88,10 @@ public:
|
||||
/* 18 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(WeaponV1V2, 0x18);
|
||||
|
||||
struct WeaponGCNTE {
|
||||
/* 00 */ ItemBaseV3<true> base;
|
||||
/* 00 */ ItemBaseV3T<true> base;
|
||||
/* 08 */ be_uint16_t class_flags = 0;
|
||||
/* 0A */ be_uint16_t atp_min = 0;
|
||||
/* 0C */ be_uint16_t atp_max = 0;
|
||||
@@ -115,12 +116,12 @@ public:
|
||||
/* 24 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(WeaponGCNTE, 0x24);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct WeaponV3 {
|
||||
struct WeaponV3T {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
/* 00 */ ItemBaseV3<IsBigEndian> base;
|
||||
/* 00 */ ItemBaseV3T<IsBigEndian> base;
|
||||
/* 08 */ U16T class_flags = 0;
|
||||
/* 0A */ U16T atp_min = 0;
|
||||
/* 0C */ U16T atp_max = 0;
|
||||
@@ -149,10 +150,15 @@ public:
|
||||
/* 28 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
using WeaponV3 = WeaponV3T<false>;
|
||||
using WeaponV3BE = WeaponV3T<true>;
|
||||
check_struct_size(WeaponV3, 0x28);
|
||||
check_struct_size(WeaponV3BE, 0x28);
|
||||
|
||||
struct WeaponV4 {
|
||||
/* 00 */ ItemBaseV4<false> base;
|
||||
/* 00 */ ItemBaseV4T<false> base;
|
||||
/* 0C */ le_uint16_t class_flags = 0x00FF;
|
||||
/* 0E */ le_uint16_t atp_min = 0;
|
||||
/* 10 */ le_uint16_t atp_max = 0;
|
||||
@@ -179,10 +185,10 @@ public:
|
||||
/* 2A */ uint8_t tech_boost = 0;
|
||||
/* 2B */ uint8_t combo_type = 0;
|
||||
/* 2C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(WeaponV4, 0x2C);
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct ArmorOrShield {
|
||||
struct ArmorOrShieldT {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
/* V1/V2 offsets */
|
||||
/* 00 */ BaseT base;
|
||||
@@ -200,33 +206,36 @@ public:
|
||||
/* 12 */ uint8_t dfp_range = 0;
|
||||
/* 13 */ uint8_t evp_range = 0;
|
||||
/* 14 */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
struct ArmorOrShieldV4;
|
||||
struct ArmorOrShieldDCProtos : ArmorOrShield<ItemBaseV2<false>, false> {
|
||||
ArmorOrShieldV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct ArmorOrShieldFinal : ArmorOrShield<BaseT, IsBigEndian> {
|
||||
struct ArmorOrShieldFinalT : ArmorOrShieldT<BaseT, IsBigEndian> {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
/* 14 */ uint8_t stat_boost = 0;
|
||||
/* 15 */ uint8_t tech_boost = 0;
|
||||
/* 16 */ U16T unknown_a2 = 0;
|
||||
/* 18 */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using ArmorOrShieldV4 = ArmorOrShieldFinalT<ItemBaseV4T<false>, false>;
|
||||
check_struct_size(ArmorOrShieldV4, 0x20);
|
||||
struct ArmorOrShieldDCProtos : ArmorOrShieldT<ItemBaseV2T<false>, false> {
|
||||
ArmorOrShieldV4 to_v4() const;
|
||||
} __packed_ws__(ArmorOrShieldDCProtos, 0x14);
|
||||
|
||||
struct ArmorOrShieldV1V2 : ArmorOrShieldFinal<ItemBaseV2<false>, false> {
|
||||
struct ArmorOrShieldV1V2 : ArmorOrShieldFinalT<ItemBaseV2T<false>, false> {
|
||||
ArmorOrShieldV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ArmorOrShieldV1V2, 0x18);
|
||||
template <bool IsBigEndian>
|
||||
struct ArmorOrShieldV3 : ArmorOrShieldFinal<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
struct ArmorOrShieldV3T : ArmorOrShieldFinalT<ItemBaseV3T<IsBigEndian>, IsBigEndian> {
|
||||
ArmorOrShieldV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct ArmorOrShieldV4 : ArmorOrShieldFinal<ItemBaseV4<false>, false> {
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using ArmorOrShieldV3 = ArmorOrShieldV3T<false>;
|
||||
using ArmorOrShieldV3BE = ArmorOrShieldV3T<true>;
|
||||
check_struct_size(ArmorOrShieldV3, 0x1C);
|
||||
check_struct_size(ArmorOrShieldV3BE, 0x1C);
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct Unit {
|
||||
struct UnitT {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
using S16T = typename std::conditional<IsBigEndian, be_int16_t, le_int16_t>::type;
|
||||
/* V1/V2 offsets */
|
||||
@@ -234,31 +243,34 @@ public:
|
||||
/* 04 */ U16T stat = 0;
|
||||
/* 06 */ U16T stat_amount = 0;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
struct UnitV4;
|
||||
struct UnitDCProtos : Unit<ItemBaseV2<false>, false> {
|
||||
UnitV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct UnitFinal : Unit<BaseT, IsBigEndian> {
|
||||
struct UnitFinalT : UnitT<BaseT, IsBigEndian> {
|
||||
using S16T = typename std::conditional<IsBigEndian, be_int16_t, le_int16_t>::type;
|
||||
/* 08 */ S16T modifier_amount = 0;
|
||||
/* 0A */ parray<uint8_t, 2> unused;
|
||||
/* 0C */
|
||||
} __attribute__((packed));
|
||||
struct UnitV1V2 : UnitFinal<ItemBaseV2<false>, false> {
|
||||
} __packed__;
|
||||
using UnitV4 = UnitFinalT<ItemBaseV4T<false>, false>;
|
||||
check_struct_size(UnitV4, 0x14);
|
||||
struct UnitDCProtos : UnitT<ItemBaseV2T<false>, false> {
|
||||
UnitV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(UnitDCProtos, 0x08);
|
||||
struct UnitV1V2 : UnitFinalT<ItemBaseV2T<false>, false> {
|
||||
UnitV4 to_v4() const;
|
||||
} __packed_ws__(UnitV1V2, 0x0C);
|
||||
template <bool IsBigEndian>
|
||||
struct UnitV3 : UnitFinal<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
struct UnitV3T : UnitFinalT<ItemBaseV3T<IsBigEndian>, IsBigEndian> {
|
||||
UnitV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct UnitV4 : UnitFinal<ItemBaseV4<false>, false> {
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using UnitV3 = UnitV3T<false>;
|
||||
using UnitV3BE = UnitV3T<true>;
|
||||
check_struct_size(UnitV3, 0x10);
|
||||
check_struct_size(UnitV3BE, 0x10);
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct Mag {
|
||||
struct MagT {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
/* V1/V2 offsets */
|
||||
/* 00 */ BaseT base;
|
||||
@@ -289,36 +301,38 @@ public:
|
||||
/* 0E */ uint8_t on_death_flag = 0;
|
||||
/* 0F */ uint8_t on_boss_flag = 0;
|
||||
/* 10 */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
struct MagV4;
|
||||
struct MagV1 : Mag<ItemBaseV2<false>, false> {
|
||||
struct MagV4 : MagT<ItemBaseV4T<false>, false> {
|
||||
le_uint16_t class_flags = 0x00FF;
|
||||
parray<uint8_t, 2> unused;
|
||||
} __packed_ws__(MagV4, 0x1C);
|
||||
struct MagV1 : MagT<ItemBaseV2T<false>, false> {
|
||||
MagV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct MagV2 : Mag<ItemBaseV2<false>, false> {
|
||||
} __packed_ws__(MagV1, 0x10);
|
||||
struct MagV2 : MagT<ItemBaseV2T<false>, false> {
|
||||
/* 10 */ le_uint16_t class_flags = 0x00FF;
|
||||
/* 12 */ parray<uint8_t, 2> unused;
|
||||
/* 14 */
|
||||
|
||||
MagV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(MagV2, 0x14);
|
||||
template <bool IsBigEndian>
|
||||
struct MagV3 : Mag<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
struct MagV3T : MagT<ItemBaseV3T<IsBigEndian>, IsBigEndian> {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
/* 10 */ U16T class_flags = 0x00FF;
|
||||
/* 12 */ parray<uint8_t, 2> unused;
|
||||
/* 14 */
|
||||
|
||||
MagV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct MagV4 : Mag<ItemBaseV4<false>, false> {
|
||||
/* 10 */ le_uint16_t class_flags = 0x00FF;
|
||||
/* 12 */ parray<uint8_t, 2> unused;
|
||||
/* 14 */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using MagV3 = MagV3T<false>;
|
||||
using MagV3BE = MagV3T<true>;
|
||||
check_struct_size(MagV3, 0x18);
|
||||
check_struct_size(MagV3BE, 0x18);
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct Tool {
|
||||
struct ToolT {
|
||||
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;
|
||||
@@ -329,18 +343,21 @@ public:
|
||||
/* 08 */ S32T cost = 0;
|
||||
/* 0C */ U32T item_flag = 0;
|
||||
/* 10 */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
|
||||
struct ToolV4;
|
||||
struct ToolV1V2 : Tool<ItemBaseV2<false>, false> {
|
||||
using ToolV4 = ToolT<ItemBaseV4T<false>, false>;
|
||||
check_struct_size(ToolV4, 0x18);
|
||||
struct ToolV1V2 : ToolT<ItemBaseV2T<false>, false> {
|
||||
ToolV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ToolV1V2, 0x10);
|
||||
template <bool IsBigEndian>
|
||||
struct ToolV3 : Tool<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
struct ToolV3T : ToolT<ItemBaseV3T<IsBigEndian>, IsBigEndian> {
|
||||
ToolV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct ToolV4 : Tool<ItemBaseV4<false>, false> {
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using ToolV3 = ToolV3T<false>;
|
||||
using ToolV3BE = ToolV3T<true>;
|
||||
check_struct_size(ToolV3, 0x14);
|
||||
check_struct_size(ToolV3BE, 0x14);
|
||||
|
||||
struct MagFeedResult {
|
||||
int8_t def = 0;
|
||||
@@ -350,31 +367,43 @@ public:
|
||||
int8_t iq = 0;
|
||||
int8_t synchro = 0;
|
||||
parray<uint8_t, 2> unused;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(MagFeedResult, 8);
|
||||
|
||||
using MagFeedResultsList = parray<MagFeedResult, 11>;
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct MagFeedResultsListOffsets {
|
||||
struct MagFeedResultsListOffsetsT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
parray<U32T, 8> offsets; // Offsets of MagFeedResultsList objects
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using MagFeedResultsListOffsets = MagFeedResultsListOffsetsT<false>;
|
||||
using MagFeedResultsListOffsetsBE = MagFeedResultsListOffsetsT<true>;
|
||||
check_struct_size(MagFeedResultsListOffsets, 0x20);
|
||||
check_struct_size(MagFeedResultsListOffsetsBE, 0x20);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct Special {
|
||||
struct SpecialT {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
U16T type = 0xFFFF;
|
||||
U16T amount = 0;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using Special = SpecialT<false>;
|
||||
using SpecialBE = SpecialT<true>;
|
||||
check_struct_size(Special, 4);
|
||||
check_struct_size(SpecialBE, 4);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct StatBoost {
|
||||
struct StatBoostT {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
uint8_t stat1 = 0;
|
||||
uint8_t stat2 = 0;
|
||||
U16T amount1 = 0;
|
||||
U16T amount2 = 0;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using StatBoost = StatBoostT<false>;
|
||||
using StatBoostBE = StatBoostT<true>;
|
||||
check_struct_size(StatBoost, 6);
|
||||
check_struct_size(StatBoostBE, 6);
|
||||
|
||||
// Indexed as [tech_num][char_class]
|
||||
using MaxTechniqueLevels = parray<parray<uint8_t, 12>, 19>;
|
||||
@@ -388,10 +417,10 @@ public:
|
||||
uint8_t level = 0;
|
||||
uint8_t char_class = 0;
|
||||
parray<uint8_t, 3> unused;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ItemCombination, 0x10);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct TechniqueBoost {
|
||||
struct TechniqueBoostT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
using FloatT = typename std::conditional<IsBigEndian, be_float, le_float>::type;
|
||||
U32T tech1 = 0;
|
||||
@@ -400,26 +429,34 @@ public:
|
||||
FloatT boost2 = 0.0f;
|
||||
U32T tech3 = 0;
|
||||
FloatT boost3 = 0.0f;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using TechniqueBoost = TechniqueBoostT<false>;
|
||||
using TechniqueBoostBE = TechniqueBoostT<true>;
|
||||
check_struct_size(TechniqueBoost, 0x18);
|
||||
check_struct_size(TechniqueBoostBE, 0x18);
|
||||
|
||||
struct EventItem {
|
||||
parray<uint8_t, 3> item;
|
||||
uint8_t probability = 0;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(EventItem, 4);
|
||||
|
||||
struct UnsealableItem {
|
||||
parray<uint8_t, 3> item;
|
||||
uint8_t unused = 0;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(UnsealableItem, 4);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct NonWeaponSaleDivisors {
|
||||
struct NonWeaponSaleDivisorsT {
|
||||
using FloatT = typename std::conditional<IsBigEndian, be_float, le_float>::type;
|
||||
FloatT armor_divisor = 0.0f;
|
||||
FloatT shield_divisor = 0.0f;
|
||||
FloatT unit_divisor = 0.0f;
|
||||
FloatT mag_divisor = 0.0f;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using NonWeaponSaleDivisors = NonWeaponSaleDivisorsT<false>;
|
||||
using NonWeaponSaleDivisorsBE = NonWeaponSaleDivisorsT<true>;
|
||||
check_struct_size(NonWeaponSaleDivisors, 0x10);
|
||||
check_struct_size(NonWeaponSaleDivisorsBE, 0x10);
|
||||
|
||||
ItemParameterTable(std::shared_ptr<const std::string> data, Version version);
|
||||
~ItemParameterTable() = default;
|
||||
@@ -443,14 +480,14 @@ public:
|
||||
const MagFeedResult& get_mag_feed_result(uint8_t table_index, uint8_t which) const;
|
||||
uint8_t get_item_stars(uint32_t id) const;
|
||||
uint8_t get_special_stars(uint8_t special) const;
|
||||
const Special<false>& get_special(uint8_t special) const;
|
||||
const Special& get_special(uint8_t special) const;
|
||||
uint8_t get_max_tech_level(uint8_t char_class, uint8_t tech_num) const;
|
||||
uint8_t get_weapon_v1_replacement(uint8_t data1_1) const;
|
||||
|
||||
uint32_t get_item_id(const ItemData& item) const;
|
||||
uint32_t get_item_team_points(const ItemData& item) const;
|
||||
uint8_t get_item_base_stars(const ItemData& item) const;
|
||||
uint8_t get_item_adjusted_stars(const ItemData& item) const;
|
||||
uint8_t get_item_adjusted_stars(const ItemData& item, bool ignore_unidentified = false) const;
|
||||
bool is_item_rare(const ItemData& item) const;
|
||||
bool is_unsealable_item(uint8_t data1_0, uint8_t data1_1, uint8_t data1_2) const;
|
||||
bool is_unsealable_item(const ItemData& param_1) const;
|
||||
@@ -493,7 +530,7 @@ private:
|
||||
/* 44 / 0668 / 0668 */ le_uint32_t unknown_a2;
|
||||
/* 48 / 030C / 030C */ le_uint32_t unknown_a3;
|
||||
/* 4C / 2CE4 / 2D78 */ le_uint32_t unknown_a4;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(TableOffsetsDCProtos, 0x50);
|
||||
|
||||
struct TableOffsetsV1V2 {
|
||||
// TODO: Is weapon count 0x89 or 0x8A? It could be that the last entry in
|
||||
@@ -516,7 +553,7 @@ private:
|
||||
/* 38 / 2C12 / 4540 */ le_uint32_t special_data_table; // -> [Special](0x29)
|
||||
/* 3C / 2CB8 / 58DC */ le_uint32_t stat_boost_table; // -> [StatBoost]
|
||||
/* 40 / 3198 / 5704 */ le_uint32_t shield_effect_table; // -> [8-byte structs]
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(TableOffsetsV1V2, 0x44);
|
||||
|
||||
struct TableOffsetsGCNTE {
|
||||
/* 00 / 6F0C */ be_uint32_t weapon_table; // -> [{count, offset -> [WeaponV3/WeaponV4]}](0xED)
|
||||
@@ -539,10 +576,10 @@ private:
|
||||
/* 44 / 737C */ be_uint32_t combination_table; // -> {count, offset -> [ItemCombination]}
|
||||
/* 48 / 68B0 */ be_uint32_t unknown_a1;
|
||||
/* 4C / 6B1C */ be_uint32_t tech_boost_table; // -> [TechniqueBoost] (always 0x2C of them? from counts struct?)
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(TableOffsetsGCNTE, 0x50);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct TableOffsetsV3V4 {
|
||||
struct TableOffsetsV3V4T {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
/* ## / GC / BB */
|
||||
/* 00 / F078 / 14884 */ U32T weapon_table; // -> [{count, offset -> [WeaponV3/WeaponV4]}](0xED)
|
||||
@@ -568,7 +605,11 @@ private:
|
||||
/* 50 / F5F0 / 15014 */ U32T unwrap_table; // -> {count, offset -> [{count, offset -> [EventItem]}]}
|
||||
/* 54 / F5F8 / 1501C */ U32T unsealable_table; // -> {count, offset -> [UnsealableItem]}
|
||||
/* 58 / F600 / 15024 */ U32T ranged_special_table; // -> {count, offset -> [4-byte structs]}
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using TableOffsetsV3V4 = TableOffsetsV3V4T<false>;
|
||||
using TableOffsetsV3V4BE = TableOffsetsV3V4T<true>;
|
||||
check_struct_size(TableOffsetsV3V4, 0x5C);
|
||||
check_struct_size(TableOffsetsV3V4BE, 0x5C);
|
||||
|
||||
Version version;
|
||||
std::shared_ptr<const std::string> data;
|
||||
@@ -576,9 +617,9 @@ private:
|
||||
const TableOffsetsDCProtos* offsets_dc_protos;
|
||||
const TableOffsetsV1V2* offsets_v1_v2;
|
||||
const TableOffsetsGCNTE* offsets_gc_nte;
|
||||
const TableOffsetsV3V4<false>* offsets_v3_le;
|
||||
const TableOffsetsV3V4<true>* offsets_v3_be;
|
||||
const TableOffsetsV3V4<false>* offsets_v4;
|
||||
const TableOffsetsV3V4* offsets_v3_le;
|
||||
const TableOffsetsV3V4BE* offsets_v3_be;
|
||||
const TableOffsetsV3V4* offsets_v4;
|
||||
|
||||
// These are unused if offsets_v4 is not null (in that case, we just return
|
||||
// references pointing inside the data string)
|
||||
@@ -588,13 +629,13 @@ private:
|
||||
mutable std::vector<UnitV4> parsed_units;
|
||||
mutable std::vector<MagV4> parsed_mags;
|
||||
mutable std::unordered_map<uint16_t, ToolV4> parsed_tools;
|
||||
mutable std::vector<Special<false>> parsed_specials;
|
||||
mutable std::vector<Special> parsed_specials;
|
||||
|
||||
// Key is used_item. We can't index on (used_item, equipped_item) because
|
||||
// equipped_item may contain wildcards, and the matching order matters.
|
||||
mutable std::map<uint32_t, std::vector<ItemCombination>> item_combination_index;
|
||||
|
||||
template <typename ToolT, bool IsBigEndian>
|
||||
template <typename ToolDefT, bool IsBigEndian>
|
||||
std::pair<uint8_t, uint8_t> find_tool_by_id_t(uint32_t tool_table_offset, uint32_t id) const;
|
||||
template <bool IsBigEndian, typename OffsetsT>
|
||||
float get_sale_divisor_t(const OffsetsT* offsets, uint8_t data1_0, uint8_t data1_1) const;
|
||||
@@ -607,17 +648,18 @@ private:
|
||||
class MagEvolutionTable {
|
||||
public:
|
||||
struct TableOffsets {
|
||||
/* 00 / 0400 */ le_uint32_t unknown_a1; // -> [offset -> (0xC-byte struct)[0x53], offset -> (same as first offset)]
|
||||
/* 04 / 0408 */ le_uint32_t unknown_a2; // -> (2-byte struct, or single word)[0x53]
|
||||
// num_mags = 0x53 in BB, 0x43 in V3
|
||||
/* 00 / 0400 */ le_uint32_t unknown_a1; // -> [offset -> (0xC-byte struct)[num_mags], offset -> (same as first offset)]
|
||||
/* 04 / 0408 */ le_uint32_t unknown_a2; // -> (2-byte struct, or single word)[num_mags]
|
||||
/* 08 / 04AE */ le_uint32_t unknown_a3; // -> (0xA8 bytes; possibly (8-byte struct)[0x15])
|
||||
/* 0C / 0556 */ le_uint32_t unknown_a4; // -> (uint8_t)[0x53]
|
||||
/* 0C / 0556 */ le_uint32_t unknown_a4; // -> (uint8_t)[num_mags]
|
||||
/* 10 / 05AC */ le_uint32_t unknown_a5; // -> (float)[0x48]
|
||||
/* 14 / 06CC */ le_uint32_t evolution_number; // -> (uint8_t)[0x53]
|
||||
} __attribute__((packed));
|
||||
/* 14 / 06CC */ le_uint32_t evolution_number; // -> (uint8_t)[num_mags]
|
||||
} __packed_ws__(TableOffsets, 0x18);
|
||||
|
||||
struct EvolutionNumberTable {
|
||||
parray<uint8_t, 0x53> values;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(EvolutionNumberTable, 0x53);
|
||||
|
||||
MagEvolutionTable(std::shared_ptr<const std::string> data);
|
||||
~MagEvolutionTable() = default;
|
||||
|
||||
+16
-16
@@ -9,26 +9,26 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
void PlayerStats::reset_to_base(uint8_t char_class, shared_ptr<const LevelTable> level_table) {
|
||||
this->level = 0;
|
||||
this->experience = 0;
|
||||
this->char_stats = level_table->base_stats_for_class(char_class);
|
||||
void LevelTable::reset_to_base(PlayerStats& stats, uint8_t char_class) const {
|
||||
stats.level = 0;
|
||||
stats.experience = 0;
|
||||
stats.char_stats = this->base_stats_for_class(char_class);
|
||||
}
|
||||
|
||||
void PlayerStats::advance_to_level(uint8_t char_class, uint32_t level, shared_ptr<const LevelTable> level_table) {
|
||||
for (; this->level < level; this->level++) {
|
||||
const auto& level_stats = level_table->stats_delta_for_level(char_class, this->level + 1);
|
||||
void LevelTable::advance_to_level(PlayerStats& stats, uint32_t level, uint8_t char_class) const {
|
||||
for (; stats.level < level; stats.level++) {
|
||||
const auto& level_stats = this->stats_delta_for_level(char_class, stats.level + 1);
|
||||
// The original code clamps the resulting stat values to [0, max_stat]; we
|
||||
// don't have max_stat handy so we just allow them to be unbounded
|
||||
this->char_stats.atp += level_stats.atp;
|
||||
this->char_stats.mst += level_stats.mst;
|
||||
this->char_stats.evp += level_stats.evp;
|
||||
this->char_stats.hp += level_stats.hp;
|
||||
this->char_stats.dfp += level_stats.dfp;
|
||||
this->char_stats.ata += level_stats.ata;
|
||||
stats.char_stats.atp += level_stats.atp;
|
||||
stats.char_stats.mst += level_stats.mst;
|
||||
stats.char_stats.evp += level_stats.evp;
|
||||
stats.char_stats.hp += level_stats.hp;
|
||||
stats.char_stats.dfp += level_stats.dfp;
|
||||
stats.char_stats.ata += level_stats.ata;
|
||||
// Note: It is not a bug that lck is ignored here; the original code
|
||||
// ignores it too.
|
||||
this->experience = level_stats.experience;
|
||||
stats.experience = level_stats.experience;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ LevelTableV2::LevelTableV2(const string& data, bool compressed) {
|
||||
le_uint32_t unknown_a10; // -> u32[3] -> (0x10-byte struct)[0x0C]
|
||||
le_uint32_t unknown_a11; // -> u32[3] -> (0x30-bytes)
|
||||
le_uint32_t unknown_a12; // -> u32[3] -> (0x14-byte struct)[0x0F]
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Offsets, 0x40);
|
||||
|
||||
StringReader r;
|
||||
string decompressed_data;
|
||||
@@ -158,7 +158,7 @@ LevelTableV4::LevelTableV4(const string& data, bool compressed) {
|
||||
struct Offsets {
|
||||
le_uint32_t base_stats; // -> u32[12] -> CharacterStats
|
||||
le_uint32_t level_deltas; // -> u32[12] -> LevelStatsDelta[200]
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Offsets, 8);
|
||||
|
||||
StringReader r;
|
||||
string decompressed_data;
|
||||
|
||||
+71
-30
@@ -7,35 +7,74 @@
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <string>
|
||||
|
||||
#include "Text.hh"
|
||||
|
||||
class LevelTable;
|
||||
|
||||
struct CharacterStats {
|
||||
/* 00 */ le_uint16_t atp = 0;
|
||||
/* 02 */ le_uint16_t mst = 0;
|
||||
/* 04 */ le_uint16_t evp = 0;
|
||||
/* 06 */ le_uint16_t hp = 0;
|
||||
/* 08 */ le_uint16_t dfp = 0;
|
||||
/* 0A */ le_uint16_t ata = 0;
|
||||
/* 0C */ le_uint16_t lck = 0;
|
||||
template <bool IsBigEndian>
|
||||
struct CharacterStatsT {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
|
||||
/* 00 */ U16T atp = 0;
|
||||
/* 02 */ U16T mst = 0;
|
||||
/* 04 */ U16T evp = 0;
|
||||
/* 06 */ U16T hp = 0;
|
||||
/* 08 */ U16T dfp = 0;
|
||||
/* 0A */ U16T ata = 0;
|
||||
/* 0C */ U16T lck = 0;
|
||||
/* 0E */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerStats {
|
||||
/* 00 */ CharacterStats char_stats;
|
||||
/* 0E */ le_uint16_t esp = 0;
|
||||
/* 10 */ le_float height = 0.0;
|
||||
/* 14 */ le_float unknown_a3 = 0.0;
|
||||
/* 18 */ le_uint32_t level = 0;
|
||||
/* 1C */ le_uint32_t experience = 0;
|
||||
/* 20 */ le_uint32_t meseta = 0;
|
||||
/* 24 */
|
||||
|
||||
void reset_to_base(uint8_t char_class, std::shared_ptr<const LevelTable> level_table);
|
||||
void advance_to_level(uint8_t char_class, uint32_t level, std::shared_ptr<const LevelTable> level_table);
|
||||
} __attribute__((packed));
|
||||
operator CharacterStatsT<!IsBigEndian>() const {
|
||||
CharacterStatsT<!IsBigEndian> ret;
|
||||
ret.atp = this->atp.load();
|
||||
ret.mst = this->mst.load();
|
||||
ret.evp = this->evp.load();
|
||||
ret.hp = this->hp.load();
|
||||
ret.dfp = this->dfp.load();
|
||||
ret.ata = this->ata.load();
|
||||
ret.lck = this->lck.load();
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using CharacterStats = CharacterStatsT<false>;
|
||||
using CharacterStatsBE = CharacterStatsT<true>;
|
||||
check_struct_size(CharacterStats, 0x0E);
|
||||
check_struct_size(CharacterStatsBE, 0x0E);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct LevelStatsDeltaBase {
|
||||
struct PlayerStatsT {
|
||||
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;
|
||||
using F32T = typename std::conditional<IsBigEndian, be_float, le_float>::type;
|
||||
|
||||
/* 00 */ CharacterStatsT<IsBigEndian> char_stats;
|
||||
/* 0E */ U16T esp = 0;
|
||||
/* 10 */ F32T height = 0.0;
|
||||
/* 14 */ F32T unknown_a3 = 0.0;
|
||||
/* 18 */ U32T level = 0;
|
||||
/* 1C */ U32T experience = 0;
|
||||
/* 20 */ U32T meseta = 0;
|
||||
/* 24 */
|
||||
|
||||
operator PlayerStatsT<!IsBigEndian>() const {
|
||||
PlayerStatsT<!IsBigEndian> ret;
|
||||
ret.char_stats = this->char_stats;
|
||||
ret.esp = this->esp.load();
|
||||
ret.height = this->height.load();
|
||||
ret.unknown_a3 = this->unknown_a3.load();
|
||||
ret.level = this->level.load();
|
||||
ret.experience = this->experience.load();
|
||||
ret.meseta = this->meseta.load();
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using PlayerStats = PlayerStatsT<false>;
|
||||
using PlayerStatsBE = PlayerStatsT<true>;
|
||||
check_struct_size(PlayerStats, 0x24);
|
||||
check_struct_size(PlayerStatsBE, 0x24);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct LevelStatsDeltaT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
/* 00 */ uint8_t atp;
|
||||
@@ -58,12 +97,11 @@ struct LevelStatsDeltaBase {
|
||||
ps.mst += this->mst;
|
||||
ps.lck += this->lck;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
|
||||
struct LevelStatsDelta : LevelStatsDeltaBase<false> {
|
||||
} __attribute__((packed));
|
||||
struct LevelStatsDeltaBE : LevelStatsDeltaBase<true> {
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using LevelStatsDelta = LevelStatsDeltaT<false>;
|
||||
using LevelStatsDeltaBE = LevelStatsDeltaT<true>;
|
||||
check_struct_size(LevelStatsDelta, 0x0C);
|
||||
check_struct_size(LevelStatsDeltaBE, 0x0C);
|
||||
|
||||
class LevelTable {
|
||||
// This is the base class for all the LevelTable implementations. The public
|
||||
@@ -76,6 +114,9 @@ public:
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const = 0;
|
||||
virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const = 0;
|
||||
|
||||
void reset_to_base(PlayerStats& stats, uint8_t char_class) const;
|
||||
void advance_to_level(PlayerStats& stats, uint32_t level, uint8_t char_class) const;
|
||||
|
||||
protected:
|
||||
LevelTable() = default;
|
||||
};
|
||||
@@ -89,7 +130,7 @@ public:
|
||||
/* 14 */ le_float unknown_a3 = 0.0;
|
||||
/* 18 */ le_uint32_t level = 0;
|
||||
/* 1C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Level100Entry, 0x1C);
|
||||
|
||||
LevelTableV2(const std::string& data, bool compressed);
|
||||
virtual ~LevelTableV2() = default;
|
||||
|
||||
-392
@@ -1,392 +0,0 @@
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Hash.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "License.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
License::License(const JSON& json)
|
||||
: serial_number(0),
|
||||
flags(0),
|
||||
ban_end_time(0),
|
||||
ep3_current_meseta(0),
|
||||
ep3_total_meseta_earned(0),
|
||||
bb_team_id(0) {
|
||||
this->serial_number = json.get_int("SerialNumber");
|
||||
this->access_key = json.get_string("AccessKey", "");
|
||||
this->dc_nte_serial_number = json.get_string("DCNTESerialNumber", "");
|
||||
this->dc_nte_access_key = json.get_string("DCNTEAccessKey", "");
|
||||
this->gc_password = json.get_string("GCPassword", "");
|
||||
this->xb_gamertag = json.get_string("XBGamerTag", "");
|
||||
this->xb_user_id = json.get_int("XBUserID", 0);
|
||||
this->xb_account_id = json.get_int("XBAccountID", 0);
|
||||
this->bb_username = json.get_string("BBUsername", "");
|
||||
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);
|
||||
}
|
||||
|
||||
JSON License::json() const {
|
||||
return JSON::dict({
|
||||
{"SerialNumber", this->serial_number},
|
||||
{"AccessKey", this->access_key},
|
||||
{"DCNTESerialNumber", this->dc_nte_serial_number},
|
||||
{"DCNTEAccessKey", this->dc_nte_access_key},
|
||||
{"GCPassword", this->gc_password},
|
||||
{"XBGamerTag", this->xb_gamertag},
|
||||
{"XBUserID", this->xb_user_id},
|
||||
{"XBAccountID", this->xb_account_id},
|
||||
{"BBUsername", this->bb_username},
|
||||
{"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 {}
|
||||
void License::delete_file() const {}
|
||||
|
||||
string License::str() const {
|
||||
vector<string> tokens;
|
||||
tokens.emplace_back(string_printf("serial_number=%010" PRIu32 "/%08" PRIX32, this->serial_number, this->serial_number));
|
||||
if (!this->access_key.empty()) {
|
||||
tokens.emplace_back("access_key=" + this->access_key);
|
||||
}
|
||||
if (!this->dc_nte_serial_number.empty()) {
|
||||
tokens.emplace_back("dc_nte_serial_number=" + this->dc_nte_serial_number);
|
||||
}
|
||||
if (!this->dc_nte_access_key.empty()) {
|
||||
tokens.emplace_back("dc_nte_access_key=" + this->dc_nte_access_key);
|
||||
}
|
||||
if (!this->gc_password.empty()) {
|
||||
tokens.emplace_back("gc_password=" + this->gc_password);
|
||||
}
|
||||
if (!this->xb_gamertag.empty()) {
|
||||
tokens.emplace_back("xb_gamertag=" + this->xb_gamertag);
|
||||
}
|
||||
if (this->xb_user_id != 0) {
|
||||
tokens.emplace_back(string_printf("xb_user_id=%016" PRIX64, this->xb_user_id));
|
||||
}
|
||||
if (this->xb_account_id != 0) {
|
||||
tokens.emplace_back(string_printf("xb_account_id=%016" PRIX64, this->xb_account_id));
|
||||
}
|
||||
if (!this->bb_username.empty()) {
|
||||
tokens.emplace_back("bb_username=" + this->bb_username);
|
||||
}
|
||||
if (!this->bb_password.empty()) {
|
||||
tokens.emplace_back("bb_password=" + this->bb_password);
|
||||
}
|
||||
tokens.emplace_back(string_printf("flags=%08" PRIX32, this->flags));
|
||||
if (this->ban_end_time) {
|
||||
tokens.emplace_back(string_printf("ban_end_time=%016" PRIX64, this->ban_end_time));
|
||||
}
|
||||
if (this->ep3_current_meseta) {
|
||||
tokens.emplace_back(string_printf("ep3_current_meseta=%" PRIu32, this->ep3_current_meseta));
|
||||
}
|
||||
if (this->ep3_total_meseta_earned) {
|
||||
tokens.emplace_back(string_printf("ep3_total_meseta_earned=%" PRIu32, this->ep3_total_meseta_earned));
|
||||
}
|
||||
return "[License: " + join(tokens, ", ") + "]";
|
||||
}
|
||||
|
||||
DiskLicense::DiskLicense(const JSON& json) : License(json) {}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void DiskLicense::delete_file() const {
|
||||
string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->serial_number);
|
||||
remove(filename.c_str());
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::create_license() const {
|
||||
return make_shared<License>();
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::create_temporary_license() const {
|
||||
return make_shared<License>();
|
||||
}
|
||||
|
||||
size_t LicenseIndex::count() const {
|
||||
return this->serial_number_to_license.size();
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::get(uint32_t serial_number) const {
|
||||
try {
|
||||
return this->serial_number_to_license.at(serial_number);
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::get_by_bb_username(const string& bb_username) const {
|
||||
try {
|
||||
return this->bb_username_to_license.at(bb_username);
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
vector<shared_ptr<License>> LicenseIndex::all() const {
|
||||
vector<shared_ptr<License>> ret;
|
||||
ret.reserve(this->serial_number_to_license.size());
|
||||
for (const auto& it : this->serial_number_to_license) {
|
||||
ret.emplace_back(it.second);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void LicenseIndex::add(shared_ptr<License> l) {
|
||||
this->serial_number_to_license[l->serial_number] = l;
|
||||
if (!l->dc_nte_serial_number.empty()) {
|
||||
this->dc_nte_serial_number_to_license[l->dc_nte_serial_number] = l;
|
||||
}
|
||||
if (!l->xb_gamertag.empty()) {
|
||||
this->xb_gamertag_to_license[l->xb_gamertag] = l;
|
||||
}
|
||||
if (!l->bb_username.empty()) {
|
||||
this->bb_username_to_license[l->bb_username] = l;
|
||||
}
|
||||
}
|
||||
|
||||
void LicenseIndex::remove(uint32_t serial_number) {
|
||||
auto l = this->serial_number_to_license.at(serial_number);
|
||||
this->serial_number_to_license.erase(l->serial_number);
|
||||
if (!l->dc_nte_serial_number.empty()) {
|
||||
this->dc_nte_serial_number_to_license.erase(l->dc_nte_serial_number);
|
||||
}
|
||||
if (!l->xb_gamertag.empty()) {
|
||||
this->xb_gamertag_to_license.erase(l->xb_gamertag);
|
||||
}
|
||||
if (!l->bb_username.empty()) {
|
||||
this->bb_username_to_license.erase(l->bb_username);
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_dc_nte(const string& serial_number, const string& access_key) const {
|
||||
if (serial_number.empty()) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->dc_nte_serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->dc_nte_access_key != access_key) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_v1_v2(
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& character_name) const {
|
||||
if (serial_number == 0) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
return this->create_temporary_license_for_shared_license(license->flags, serial_number, access_key, "", character_name);
|
||||
}
|
||||
if (license->access_key.compare(0, 8, access_key) != 0) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_gc_no_password(
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& character_name) const {
|
||||
if (serial_number == 0) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
return this->create_temporary_license_for_shared_license(license->flags, serial_number, access_key, "", character_name);
|
||||
}
|
||||
if (license->access_key != access_key) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_gc_with_password(
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& password,
|
||||
const string& character_name) const {
|
||||
if (serial_number == 0) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
return this->create_temporary_license_for_shared_license(license->flags, serial_number, access_key, password, character_name);
|
||||
}
|
||||
if (license->access_key != access_key) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->gc_password != password) {
|
||||
throw incorrect_password();
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_xb(const string& gamertag, uint64_t user_id, uint64_t account_id) const {
|
||||
if (user_id == 0 || account_id == 0) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
try {
|
||||
auto& license = this->xb_gamertag_to_license.at(gamertag);
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
throw missing_license(); // XB users cannot use shared serials
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->xb_user_id && (license->xb_user_id != user_id)) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->xb_account_id && (license->xb_account_id != account_id)) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_bb(const string& username, const string& password) const {
|
||||
if (username.empty() || password.empty()) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->bb_username_to_license.at(username);
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
throw missing_license(); // BB users cannot use shared serials
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->bb_password != password) {
|
||||
throw incorrect_password();
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::create_temporary_license_for_shared_license(
|
||||
uint32_t base_flags,
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& password,
|
||||
const string& character_name) const {
|
||||
uint32_t temp_serial_number = fnv1a32(&serial_number, sizeof(serial_number));
|
||||
temp_serial_number = fnv1a32(access_key, temp_serial_number);
|
||||
temp_serial_number = fnv1a32(password, temp_serial_number);
|
||||
temp_serial_number = fnv1a32(character_name, temp_serial_number);
|
||||
auto ret = this->create_temporary_license();
|
||||
ret->serial_number = temp_serial_number & 0x7FFFFFFF;
|
||||
ret->flags = base_flags;
|
||||
ret->set_flag(License::Flag::IS_SHARED_SERIAL);
|
||||
return ret;
|
||||
}
|
||||
|
||||
DiskLicenseIndex::DiskLicenseIndex() {
|
||||
struct BinaryLicense {
|
||||
pstring<TextEncoding::ASCII, 0x14> username; // BB username (max. 16 chars; should technically be Unicode)
|
||||
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>();
|
||||
}
|
||||
-165
@@ -1,165 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <phosg/JSON.hh>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Text.hh"
|
||||
|
||||
class LicenseIndex;
|
||||
|
||||
class License {
|
||||
public:
|
||||
enum class Flag : uint32_t {
|
||||
// clang-format off
|
||||
KICK_USER = 0x00000001,
|
||||
BAN_USER = 0x00000002,
|
||||
SILENCE_USER = 0x00000004,
|
||||
CHANGE_EVENT = 0x00000010,
|
||||
ANNOUNCE = 0x00000020,
|
||||
FREE_JOIN_GAMES = 0x00000040,
|
||||
DEBUG = 0x01000000,
|
||||
CHEAT_ANYWHERE = 0x02000000,
|
||||
DISABLE_QUEST_REQUIREMENTS = 0x04000000,
|
||||
ALWAYS_ENABLE_CHAT_COMMANDS = 0x08000000,
|
||||
MODERATOR = 0x00000007,
|
||||
ADMINISTRATOR = 0x000000FF,
|
||||
ROOT = 0x7FFFFFFF,
|
||||
IS_SHARED_SERIAL = 0x80000000,
|
||||
// NOTE: When adding or changing license flags, don't forget to change the
|
||||
// documentation in the shell's help text.
|
||||
UNUSED_BITS = 0x70FFFF00,
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
uint32_t serial_number = 0;
|
||||
std::string dc_nte_serial_number;
|
||||
std::string dc_nte_access_key;
|
||||
std::string access_key;
|
||||
std::string gc_password;
|
||||
std::string xb_gamertag;
|
||||
uint64_t xb_user_id = 0;
|
||||
uint64_t xb_account_id = 0;
|
||||
std::string bb_username;
|
||||
std::string bb_password;
|
||||
|
||||
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;
|
||||
|
||||
uint32_t bb_team_id = 0;
|
||||
|
||||
License() = default;
|
||||
explicit License(const JSON& json);
|
||||
virtual ~License() = default;
|
||||
|
||||
JSON json() const;
|
||||
virtual void save() const;
|
||||
virtual void delete_file() const;
|
||||
|
||||
[[nodiscard]] inline bool check_flag(Flag flag) const {
|
||||
return !!(this->flags & static_cast<uint32_t>(flag));
|
||||
}
|
||||
inline void set_flag(Flag flag) {
|
||||
this->flags |= static_cast<uint32_t>(flag);
|
||||
}
|
||||
inline void clear_flag(Flag flag) {
|
||||
this->flags &= (~static_cast<uint32_t>(flag));
|
||||
}
|
||||
inline void toggle_flag(Flag flag) {
|
||||
this->flags ^= static_cast<uint32_t>(flag);
|
||||
}
|
||||
inline void replace_all_flags(Flag mask) {
|
||||
this->flags = static_cast<uint32_t>(mask);
|
||||
}
|
||||
|
||||
std::string str() const;
|
||||
};
|
||||
|
||||
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 {
|
||||
public:
|
||||
no_username() : invalid_argument("serial number is zero or username is missing") {}
|
||||
};
|
||||
class incorrect_password : public std::invalid_argument {
|
||||
public:
|
||||
incorrect_password() : invalid_argument("incorrect password") {}
|
||||
};
|
||||
class incorrect_access_key : public std::invalid_argument {
|
||||
public:
|
||||
incorrect_access_key() : invalid_argument("incorrect access key") {}
|
||||
};
|
||||
class missing_license : public std::invalid_argument {
|
||||
public:
|
||||
missing_license() : invalid_argument("missing license") {}
|
||||
};
|
||||
|
||||
LicenseIndex() = default;
|
||||
virtual ~LicenseIndex() = default;
|
||||
|
||||
virtual std::shared_ptr<License> create_license() const;
|
||||
virtual std::shared_ptr<License> create_temporary_license() const;
|
||||
|
||||
size_t count() const;
|
||||
std::shared_ptr<License> get(uint32_t serial_number) const;
|
||||
std::shared_ptr<License> get_by_bb_username(const std::string& bb_username) const;
|
||||
std::vector<std::shared_ptr<License>> all() const;
|
||||
|
||||
void add(std::shared_ptr<License> l);
|
||||
void remove(uint32_t serial_number);
|
||||
|
||||
std::shared_ptr<License> verify_dc_nte(const std::string& serial_number, const std::string& access_key) const;
|
||||
std::shared_ptr<License> verify_v1_v2(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name) const;
|
||||
std::shared_ptr<License> verify_gc_no_password(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name) const;
|
||||
std::shared_ptr<License> verify_gc_with_password(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& password,
|
||||
const std::string& character_name) const;
|
||||
std::shared_ptr<License> verify_xb(const std::string& gamertag, uint64_t user_id, uint64_t account_id) const;
|
||||
std::shared_ptr<License> verify_bb(const std::string& username, const std::string& password) const;
|
||||
|
||||
protected:
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> dc_nte_serial_number_to_license;
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> xb_gamertag_to_license;
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> bb_username_to_license;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<License>> serial_number_to_license;
|
||||
|
||||
std::shared_ptr<License> create_temporary_license_for_shared_license(
|
||||
uint32_t base_flags,
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& password,
|
||||
const std::string& character_name) const;
|
||||
};
|
||||
|
||||
class DiskLicenseIndex : public LicenseIndex {
|
||||
public:
|
||||
DiskLicenseIndex();
|
||||
virtual ~DiskLicenseIndex() = default;
|
||||
|
||||
virtual std::shared_ptr<License> create_license() const;
|
||||
};
|
||||
+30
-25
@@ -12,7 +12,7 @@
|
||||
using namespace std;
|
||||
|
||||
bool Lobby::FloorItem::visible_to_client(uint8_t client_id) const {
|
||||
return this->visibility_flags & (1 << client_id);
|
||||
return this->flags & (1 << client_id);
|
||||
}
|
||||
|
||||
Lobby::FloorItemManager::FloorItemManager(uint32_t lobby_id, uint8_t floor)
|
||||
@@ -27,18 +27,18 @@ shared_ptr<Lobby::FloorItem> Lobby::FloorItemManager::find(uint32_t item_id) con
|
||||
return this->items.at(item_id);
|
||||
}
|
||||
|
||||
void Lobby::FloorItemManager::add(const ItemData& item, float x, float z, uint16_t visibility_flags) {
|
||||
void Lobby::FloorItemManager::add(const ItemData& item, float x, float z, uint16_t flags) {
|
||||
auto fi = make_shared<FloorItem>();
|
||||
fi->data = item;
|
||||
fi->x = x;
|
||||
fi->z = z;
|
||||
fi->drop_number = this->next_drop_number++;
|
||||
fi->visibility_flags = visibility_flags & 0x0FFF;
|
||||
fi->flags = flags;
|
||||
this->add(fi);
|
||||
}
|
||||
|
||||
void Lobby::FloorItemManager::add(shared_ptr<Lobby::FloorItem> fi) {
|
||||
if (fi->visibility_flags == 0) {
|
||||
if (fi->flags == 0) {
|
||||
throw logic_error("floor item is not visible to any player");
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ void Lobby::FloorItemManager::add(shared_ptr<Lobby::FloorItem> fi) {
|
||||
this->queue_for_client[z].emplace(fi->drop_number, fi);
|
||||
}
|
||||
}
|
||||
this->log.info("Added floor item %08" PRIX32 " at %g, %g with drop number %" PRIu64 " visible to %03hX",
|
||||
fi->data.id.load(), fi->x, fi->z, fi->drop_number, fi->visibility_flags);
|
||||
this->log.info("Added floor item %08" PRIX32 " at %g, %g with drop number %" PRIu64 " with flags %03hX",
|
||||
fi->data.id.load(), fi->x, fi->z, fi->drop_number, fi->flags);
|
||||
}
|
||||
|
||||
std::shared_ptr<Lobby::FloorItem> Lobby::FloorItemManager::remove(uint32_t item_id, uint8_t client_id) {
|
||||
@@ -70,8 +70,8 @@ std::shared_ptr<Lobby::FloorItem> Lobby::FloorItemManager::remove(uint32_t item_
|
||||
}
|
||||
}
|
||||
this->items.erase(item_it);
|
||||
this->log.info("Removed floor item %08" PRIX32 " at %g, %g with drop number %" PRIu64 " visible to %03hX",
|
||||
fi->data.id.load(), fi->x, fi->z, fi->drop_number, fi->visibility_flags);
|
||||
this->log.info("Removed floor item %08" PRIX32 " at %g, %g with drop number %" PRIu64 " with flags %03hX",
|
||||
fi->data.id.load(), fi->x, fi->z, fi->drop_number, fi->flags);
|
||||
return fi;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ std::unordered_set<std::shared_ptr<Lobby::FloorItem>> Lobby::FloorItemManager::e
|
||||
void Lobby::FloorItemManager::clear_inaccessible(uint16_t remaining_clients_mask) {
|
||||
unordered_set<uint32_t> item_ids_to_delete;
|
||||
for (const auto& it : this->items) {
|
||||
if ((it.second->visibility_flags & remaining_clients_mask) == 0) {
|
||||
if ((it.second->flags & remaining_clients_mask) == 0) {
|
||||
item_ids_to_delete.emplace(it.first);
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ void Lobby::FloorItemManager::clear_inaccessible(uint16_t remaining_clients_mask
|
||||
void Lobby::FloorItemManager::clear_private() {
|
||||
unordered_set<uint32_t> item_ids_to_delete;
|
||||
for (const auto& it : this->items) {
|
||||
if ((it.second->visibility_flags & 0x00F) != 0x00F) {
|
||||
if ((it.second->flags & 0x00F) != 0x00F) {
|
||||
item_ids_to_delete.emplace(it.first);
|
||||
}
|
||||
}
|
||||
@@ -566,6 +566,10 @@ bool Lobby::any_v1_clients_present() const {
|
||||
}
|
||||
|
||||
void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
|
||||
if (!c->login) {
|
||||
throw runtime_error("client is not logged in");
|
||||
}
|
||||
|
||||
ssize_t index;
|
||||
ssize_t min_client_id = this->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0;
|
||||
|
||||
@@ -645,12 +649,12 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
|
||||
auto p = c->character();
|
||||
PlayerLobbyDataDCGC lobby_data;
|
||||
lobby_data.player_tag = 0x00010000;
|
||||
lobby_data.guild_card_number = c->license->serial_number;
|
||||
lobby_data.guild_card_number = c->login->account->account_id;
|
||||
lobby_data.name.encode(p->disp.name.decode(c->language()), c->language());
|
||||
this->battle_record->add_player(
|
||||
lobby_data,
|
||||
p->inventory,
|
||||
p->disp.to_dcpcv3(c->language(), c->language()),
|
||||
p->disp.to_dcpcv3<false>(c->language(), c->language()),
|
||||
c->ep3_config ? (c->ep3_config->online_clv_exp / 100) : 0);
|
||||
}
|
||||
|
||||
@@ -767,14 +771,13 @@ void Lobby::move_client_to_lobby(
|
||||
dest_lobby->add_client(c, required_client_id);
|
||||
}
|
||||
|
||||
shared_ptr<Client> Lobby::find_client(const string* identifier, uint64_t serial_number) {
|
||||
shared_ptr<Client> Lobby::find_client(const string* identifier, uint64_t account_id) {
|
||||
for (size_t x = 0; x < this->max_clients; x++) {
|
||||
auto lc = this->clients[x];
|
||||
if (!lc) {
|
||||
continue;
|
||||
}
|
||||
if (serial_number && lc->license &&
|
||||
(lc->license->serial_number == serial_number)) {
|
||||
if (account_id && lc->login && (lc->login->account->account_id == account_id)) {
|
||||
return lc;
|
||||
}
|
||||
if (identifier && (lc->character()->disp.name.eq(*identifier, lc->language()))) {
|
||||
@@ -802,7 +805,7 @@ Lobby::JoinError Lobby::join_error_for_client(std::shared_ptr<Client> c, const s
|
||||
if (this->mode == GameMode::SOLO) {
|
||||
return JoinError::SOLO;
|
||||
}
|
||||
if (!c->license->check_flag(License::Flag::FREE_JOIN_GAMES)) {
|
||||
if (!c->login->account->check_flag(Account::Flag::FREE_JOIN_GAMES)) {
|
||||
if (password && !this->password.empty() && (*password != this->password)) {
|
||||
return JoinError::INCORRECT_PASSWORD;
|
||||
}
|
||||
@@ -816,8 +819,9 @@ Lobby::JoinError Lobby::join_error_for_client(std::shared_ptr<Client> c, const s
|
||||
if (this->quest) {
|
||||
size_t num_clients = this->count_clients() + 1;
|
||||
bool v1_present = is_v1(c->version()) || this->any_v1_clients_present();
|
||||
if (!c->can_see_quest(this->quest, this->event, this->difficulty, num_clients, v1_present) ||
|
||||
!c->can_play_quest(this->quest, this->event, this->difficulty, num_clients, v1_present)) {
|
||||
auto this_sh = this->shared_from_this();
|
||||
if (!c->can_see_quest(this->quest, this_sh, this->event, this->difficulty, num_clients, v1_present) ||
|
||||
!c->can_play_quest(this->quest, this_sh, this->event, this->difficulty, num_clients, v1_present)) {
|
||||
return JoinError::NO_ACCESS_TO_QUEST;
|
||||
}
|
||||
}
|
||||
@@ -842,9 +846,9 @@ shared_ptr<Lobby::FloorItem> Lobby::find_item(uint8_t floor, uint32_t item_id) c
|
||||
return this->floor_item_managers.at(floor).find(item_id);
|
||||
}
|
||||
|
||||
void Lobby::add_item(uint8_t floor, const ItemData& data, float x, float z, uint16_t visibility_flags) {
|
||||
void Lobby::add_item(uint8_t floor, const ItemData& data, float x, float z, uint16_t flags) {
|
||||
auto& m = this->floor_item_managers.at(floor);
|
||||
m.add(data, x, z, visibility_flags);
|
||||
m.add(data, x, z, flags);
|
||||
this->evict_items_from_floor(floor);
|
||||
}
|
||||
|
||||
@@ -914,11 +918,11 @@ void Lobby::assign_inventory_and_bank_item_ids(shared_ptr<Client> c, bool consum
|
||||
}
|
||||
}
|
||||
|
||||
unordered_map<uint32_t, shared_ptr<Client>> Lobby::clients_by_serial_number() const {
|
||||
unordered_map<uint32_t, shared_ptr<Client>> Lobby::clients_by_account_id() const {
|
||||
unordered_map<uint32_t, shared_ptr<Client>> ret;
|
||||
for (auto c : this->clients) {
|
||||
if (c) {
|
||||
ret.emplace(c->license->serial_number, c);
|
||||
if (c && c->login) {
|
||||
ret.emplace(c->login->account->account_id, c);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
@@ -930,10 +934,11 @@ QuestIndex::IncludeCondition Lobby::quest_include_condition() const {
|
||||
return [this, num_players, v1_present](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->event, this->difficulty, num_players, v1_present)) {
|
||||
auto this_sh = this->shared_from_this();
|
||||
if (lc && !lc->can_see_quest(q, this_sh, this->event, this->difficulty, num_players, v1_present)) {
|
||||
return QuestIndex::IncludeState::HIDDEN;
|
||||
}
|
||||
if (lc && !lc->can_play_quest(q, this->event, this->difficulty, num_players, v1_present)) {
|
||||
if (lc && !lc->can_play_quest(q, this_sh, this->event, this->difficulty, num_players, v1_present)) {
|
||||
is_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-7
@@ -29,7 +29,13 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
float x;
|
||||
float z;
|
||||
uint64_t drop_number;
|
||||
uint16_t visibility_flags;
|
||||
// The low 12 bits of flags are visibility flags, specifying which clients
|
||||
// can see the item. (In practice, only the lowest 4 of these bits are used,
|
||||
// but the game has fields for 12 players so we do too.)
|
||||
// The 13th bit (0x1000) specifies whether a rare item notification should
|
||||
// be sent to all players when the item is picked up. This has no effect for
|
||||
// non-rare items.
|
||||
uint16_t flags;
|
||||
|
||||
bool visible_to_client(uint8_t client_id) const;
|
||||
};
|
||||
@@ -46,7 +52,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
|
||||
bool exists(uint32_t item_id) const;
|
||||
std::shared_ptr<FloorItem> find(uint32_t item_id) const;
|
||||
void add(const ItemData& item, float x, float z, uint16_t visibility_flags);
|
||||
void add(const ItemData& item, float x, float z, uint16_t flags);
|
||||
void add(std::shared_ptr<FloorItem> fi);
|
||||
std::shared_ptr<FloorItem> remove(uint32_t item_id, uint8_t client_id);
|
||||
std::unordered_set<std::shared_ptr<FloorItem>> evict();
|
||||
@@ -269,9 +275,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
std::shared_ptr<Client> c,
|
||||
ssize_t required_client_id = -1);
|
||||
|
||||
std::shared_ptr<Client> find_client(
|
||||
const std::string* identifier = nullptr,
|
||||
uint64_t serial_number = 0);
|
||||
std::shared_ptr<Client> find_client(const std::string* identifier = nullptr, uint64_t account_id = 0);
|
||||
|
||||
enum class JoinError {
|
||||
ALLOWED = 0,
|
||||
@@ -290,7 +294,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
|
||||
bool item_exists(uint8_t floor, uint32_t item_id) const;
|
||||
std::shared_ptr<FloorItem> find_item(uint8_t floor, uint32_t item_id) const;
|
||||
void add_item(uint8_t floor, const ItemData& item, float x, float z, uint16_t visibility_flags);
|
||||
void add_item(uint8_t floor, const ItemData& item, float x, float z, uint16_t flags);
|
||||
void add_item(uint8_t floor, std::shared_ptr<FloorItem>);
|
||||
void evict_items_from_floor(uint8_t floor);
|
||||
std::shared_ptr<FloorItem> remove_item(uint8_t floor, uint32_t item_id, uint8_t requesting_client_id);
|
||||
@@ -301,7 +305,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
|
||||
QuestIndex::IncludeCondition quest_include_condition() const;
|
||||
|
||||
std::unordered_map<uint32_t, std::shared_ptr<Client>> clients_by_serial_number() const;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<Client>> clients_by_account_id() const;
|
||||
|
||||
static void dispatch_on_idle_timeout(evutil_socket_t, short, void* ctx);
|
||||
|
||||
|
||||
+154
-45
@@ -56,6 +56,11 @@ bool use_terminal_colors = false;
|
||||
void print_version_info();
|
||||
void print_usage();
|
||||
|
||||
std::string get_config_filename(Arguments& args) {
|
||||
string config_filename = args.get<string>("config");
|
||||
return config_filename.empty() ? "system/config.json" : config_filename;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
vector<T> parse_int_vector(const JSON& o) {
|
||||
vector<T> ret;
|
||||
@@ -266,6 +271,7 @@ static void a_compress_decompress_fn(Arguments& args) {
|
||||
bool is_decompress = starts_with(action, "decompress-");
|
||||
bool is_big_endian = args.get<bool>("big-endian");
|
||||
bool is_optimal = args.get<bool>("optimal");
|
||||
bool is_pessimal = args.get<bool>("pessimal");
|
||||
int8_t compression_level = args.get<int8_t>("compression-level", 0);
|
||||
size_t bytes = args.get<size_t>("bytes", 0);
|
||||
string seed = args.get<string>("seed");
|
||||
@@ -298,6 +304,8 @@ static void a_compress_decompress_fn(Arguments& args) {
|
||||
if (!is_decompress && (is_prs || is_pr2 || is_prc)) {
|
||||
if (is_optimal) {
|
||||
data = prs_compress_optimal(data.data(), data.size(), optimal_progress_fn);
|
||||
} else if (is_pessimal) {
|
||||
data = prs_compress_pessimal(data.data(), data.size());
|
||||
} else {
|
||||
data = prs_compress(data, compression_level, progress_fn);
|
||||
}
|
||||
@@ -746,6 +754,62 @@ static void a_encrypt_decrypt_save_data_fn(Arguments& args) {
|
||||
Action a_decrypt_save_data("decrypt-save-data", nullptr, a_encrypt_decrypt_save_data_fn);
|
||||
Action a_encrypt_save_data("encrypt-save-data", nullptr, a_encrypt_decrypt_save_data_fn);
|
||||
|
||||
Action a_decrypt_dcv2_executable(
|
||||
"decrypt-dcv2-executable", "\
|
||||
decrypt-dcv2-executable --executable=EXEC --indexes=INDEXES --values=VALUES\n\
|
||||
decrypt-dcv2-executable --executable=EXEC --simple [--seed=SEED]\n\
|
||||
Decrypt a PSO DC v2 executable file. EXEC should be the path to the\n\
|
||||
executable (DP_ADDRESS.JPN), INDEXES should be the path to the index fixup\n\
|
||||
table (KATSUO.SEA), and VALUES should be the path to the value fixup table\n\
|
||||
(IWASHI.SEA). The output is written to EXEC.dec.\n\
|
||||
If --simple is given, uses the simpler encryption method used in some\n\
|
||||
community modifications of the game. In this case, --seed is not required;\n\
|
||||
if not given, finds the seed automatically, and prints it to stderr so you\n\
|
||||
will be able to use it when re-encrypting.",
|
||||
+[](Arguments& args) {
|
||||
string executable_filename = args.get<string>("executable", true);
|
||||
string executable_data = load_file(executable_filename);
|
||||
string decrypted;
|
||||
if (args.get<bool>("simple")) {
|
||||
string seed_str = args.get<string>("seed");
|
||||
int64_t seed = seed_str.empty() ? -1 : stoull(seed_str, nullptr, 16);
|
||||
decrypted = crypt_dp_address_jpn_simple(executable_data, seed);
|
||||
} else {
|
||||
string values_filename = args.get<string>("values", true);
|
||||
string indexes_filename = args.get<string>("indexes", true);
|
||||
string values_data = load_file(values_filename);
|
||||
string indexes_data = load_file(indexes_filename);
|
||||
decrypted = decrypt_dp_address_jpn(executable_data, values_data, indexes_data);
|
||||
}
|
||||
save_file(executable_filename + ".dec", decrypted);
|
||||
});
|
||||
Action a_encrypt_dcv2_executable(
|
||||
"encrypt-dcv2-executable", "\
|
||||
decrypt-dcv2-executable --executable=EXEC --indexes=INDEXES\n\
|
||||
decrypt-dcv2-executable --executable=EXEC --simple --seed=SEED\n\
|
||||
Encrypt a PSO DC v2 executable file. EXEC should be the path to the\n\
|
||||
executable (DP_ADDRESS.JPN) and INDEXES should be the path to the index\n\
|
||||
fixup table (KATSUO.SEA). The output is written to EXEC.enc and\n\
|
||||
INDEXES.enc.\n\
|
||||
If --simple is given, uses the simpler encryption method used in some\n\
|
||||
community modifications of the game. In this case, --seed is required.",
|
||||
+[](Arguments& args) {
|
||||
string executable_filename = args.get<string>("executable", true);
|
||||
string executable_data = load_file(executable_filename);
|
||||
string encrypted_executable;
|
||||
if (args.get<bool>("simple")) {
|
||||
int64_t seed = stoull(args.get<string>("seed", true), nullptr, 16);
|
||||
encrypted_executable = crypt_dp_address_jpn_simple(executable_data, seed);
|
||||
} else {
|
||||
string indexes_filename = args.get<string>("indexes", true);
|
||||
string indexes_data = load_file(indexes_filename);
|
||||
auto encrypted = encrypt_dp_address_jpn(executable_data, indexes_data);
|
||||
save_file(indexes_filename + ".enc", encrypted.indexes);
|
||||
encrypted_executable = std::move(encrypted.executable);
|
||||
}
|
||||
save_file(executable_filename + ".enc", encrypted_executable);
|
||||
});
|
||||
|
||||
Action a_decode_gci_snapshot(
|
||||
"decode-gci-snapshot", "\
|
||||
decode-gci-snapshot [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
||||
@@ -782,7 +846,7 @@ Action a_encode_gvm(
|
||||
} else {
|
||||
img = Image(stdin);
|
||||
}
|
||||
string encoded = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565);
|
||||
string encoded = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565, "image.gvr", 0);
|
||||
write_output_data(args, encoded.data(), encoded.size(), "gvm");
|
||||
});
|
||||
|
||||
@@ -1189,8 +1253,7 @@ Action a_assemble_all_patches(
|
||||
versions, and one encrypted, for PSO GC JP v1.4, JP Ep3, and Ep3 Trial\n\
|
||||
Edition). The output files are saved in system/client-functions.\n",
|
||||
+[](Arguments&) {
|
||||
auto s = make_shared<ServerState>();
|
||||
s->compile_functions(false);
|
||||
auto fci = make_shared<FunctionCodeIndex>("system/client-functions");
|
||||
|
||||
auto process_code = +[](shared_ptr<const CompiledFunctionCode> code,
|
||||
uint32_t checksum_addr,
|
||||
@@ -1207,23 +1270,27 @@ Action a_assemble_all_patches(
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& it : s->function_code_index->name_and_specific_version_to_patch_function) {
|
||||
for (const auto& it : fci->name_and_specific_version_to_patch_function) {
|
||||
process_code(it.second, 0, 0, 0);
|
||||
}
|
||||
try {
|
||||
process_code(s->function_code_index->name_to_function.at("VersionDetectGC"), 0, 0, 0);
|
||||
process_code(fci->name_to_function.at("VersionDetectDC"), 0, 0, 0);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
process_code(s->function_code_index->name_to_function.at("VersionDetectXB"), 0, 0, 0);
|
||||
process_code(fci->name_to_function.at("VersionDetectGC"), 0, 0, 0);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
process_code(s->function_code_index->name_to_function.at("CacheClearFix-Phase1"), 0x80000000, 8, 0x7F2734EC);
|
||||
process_code(fci->name_to_function.at("VersionDetectXB"), 0, 0, 0);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
process_code(s->function_code_index->name_to_function.at("CacheClearFix-Phase2"), 0, 0, 0);
|
||||
process_code(fci->name_to_function.at("CacheClearFix-Phase1"), 0x80000000, 8, 0x7F2734EC);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
process_code(fci->name_to_function.at("CacheClearFix-Phase2"), 0, 0, 0);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
});
|
||||
@@ -1298,6 +1365,12 @@ Action a_extract_bml("extract-bml", "\
|
||||
PC/BB format.\n",
|
||||
a_extract_archive_fn);
|
||||
|
||||
Action a_encode_sjis(
|
||||
"encode-sjis", nullptr, +[](Arguments& args) {
|
||||
string data = read_input_data(args);
|
||||
string result = tt_utf8_to_sega_sjis(data);
|
||||
write_output_data(args, result.data(), result.size(), "txt");
|
||||
});
|
||||
Action a_decode_sjis(
|
||||
"decode-sjis", nullptr, +[](Arguments& args) {
|
||||
string data = read_input_data(args);
|
||||
@@ -1415,7 +1488,7 @@ Action a_print_word_select_table(
|
||||
given, prints the table sorted by token ID for that version. If no version\n\
|
||||
option is given, prints the token table sorted by canonical name.\n",
|
||||
+[](Arguments& args) {
|
||||
auto s = make_shared<ServerState>();
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_patch_indexes(false);
|
||||
s->load_text_index(false);
|
||||
s->load_word_select_table(false);
|
||||
@@ -1477,7 +1550,7 @@ Action a_convert_rare_item_set(
|
||||
auto version = get_cli_version(args);
|
||||
|
||||
double rate_factor = args.get<double>("multiply", 1.0);
|
||||
auto s = make_shared<ServerState>("system/config.json");
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_config_early();
|
||||
s->load_patch_indexes(false);
|
||||
s->load_text_index(false);
|
||||
@@ -1541,7 +1614,7 @@ Action a_describe_item(
|
||||
string description = args.get<string>(1);
|
||||
auto version = get_cli_version(args);
|
||||
|
||||
auto s = make_shared<ServerState>("system/config.json");
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_config_early();
|
||||
s->load_patch_indexes(false);
|
||||
s->load_text_index(false);
|
||||
@@ -1556,6 +1629,8 @@ Action a_describe_item(
|
||||
}
|
||||
|
||||
string desc = name_index->describe_item(item);
|
||||
string desc_colored = name_index->describe_item(item, true);
|
||||
|
||||
log_info("Data (decoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX",
|
||||
item.data1[0], item.data1[1], item.data1[2], item.data1[3],
|
||||
item.data1[4], item.data1[5], item.data1[6], item.data1[7],
|
||||
@@ -1601,6 +1676,7 @@ Action a_describe_item(
|
||||
}
|
||||
|
||||
log_info("Description: %s", desc.c_str());
|
||||
log_info("Description (in-game): %s", desc_colored.c_str());
|
||||
|
||||
size_t purchase_price = s->item_parameter_table(Version::BB_V4)->price_for_item(item);
|
||||
size_t sale_price = purchase_price >> 3;
|
||||
@@ -1608,8 +1684,8 @@ Action a_describe_item(
|
||||
});
|
||||
|
||||
Action a_name_all_items(
|
||||
"name-all-items", nullptr, +[](Arguments&) {
|
||||
auto s = make_shared<ServerState>("system/config.json");
|
||||
"name-all-items", nullptr, +[](Arguments& args) {
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_config_early();
|
||||
s->load_patch_indexes(false);
|
||||
s->load_text_index(false);
|
||||
@@ -1658,8 +1734,8 @@ Action a_name_all_items(
|
||||
});
|
||||
|
||||
Action a_print_item_parameter_tables(
|
||||
"print-item-tables", nullptr, +[](Arguments&) {
|
||||
auto s = make_shared<ServerState>();
|
||||
"print-item-tables", nullptr, +[](Arguments& args) {
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_patch_indexes(false);
|
||||
s->load_text_index(false);
|
||||
s->load_item_definitions(false);
|
||||
@@ -1682,7 +1758,7 @@ Action a_show_ep3_cards(
|
||||
+[](Arguments& args) {
|
||||
bool one_line = args.get<bool>("one-line");
|
||||
|
||||
auto s = make_shared<ServerState>();
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_ep3_cards(false);
|
||||
|
||||
unique_ptr<BinaryTextSet> text_english;
|
||||
@@ -1732,7 +1808,7 @@ Action a_generate_ep3_cards_html(
|
||||
|
||||
bool is_nte = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE);
|
||||
|
||||
auto s = make_shared<ServerState>();
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_patch_indexes(false);
|
||||
s->load_text_index(false);
|
||||
s->load_ep3_cards(false);
|
||||
@@ -1894,10 +1970,10 @@ Action a_show_ep3_maps(
|
||||
show-ep3-maps\n\
|
||||
Print the Episode 3 maps from the system/ep3 directory in a (sort of)\n\
|
||||
human-readable format.\n",
|
||||
+[](Arguments&) {
|
||||
+[](Arguments& args) {
|
||||
config_log.info("Collecting Episode 3 data");
|
||||
|
||||
auto s = make_shared<ServerState>();
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_ep3_cards(false);
|
||||
s->load_ep3_maps(false);
|
||||
|
||||
@@ -1921,8 +1997,8 @@ Action a_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&) {
|
||||
auto s = make_shared<ServerState>();
|
||||
+[](Arguments& args) {
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_patch_indexes(false);
|
||||
s->load_battle_params(false);
|
||||
|
||||
@@ -1963,7 +2039,7 @@ Action a_find_rare_enemy_seeds(
|
||||
size_t min_count = args.get<size_t>("min-count", 1);
|
||||
string quest_name = args.get<string>("quest", false);
|
||||
|
||||
auto s = make_shared<ServerState>("system/config.json");
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
shared_ptr<const VersionedQuest> vq;
|
||||
if (!quest_name.empty()) {
|
||||
s->load_config_early();
|
||||
@@ -2046,9 +2122,9 @@ Action a_find_rare_enemy_seeds(
|
||||
});
|
||||
|
||||
Action a_load_maps_test(
|
||||
"load-maps-test", nullptr, +[](Arguments&) {
|
||||
"load-maps-test", nullptr, +[](Arguments& args) {
|
||||
using SDT = SetDataTable;
|
||||
auto s = make_shared<ServerState>("system/config.json");
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_config_early();
|
||||
s->clear_map_file_caches();
|
||||
s->load_patch_indexes(false);
|
||||
@@ -2285,9 +2361,25 @@ Action a_diff_dol_files(
|
||||
}
|
||||
});
|
||||
|
||||
Action a_generate_hangame_creds(
|
||||
"generate-hangame-creds", nullptr, +[](Arguments& args) {
|
||||
const string& user_id = args.get<string>(1);
|
||||
const string& token = args.get<string>(2);
|
||||
const string& unused = args.get<string>(3, false);
|
||||
string hex = format_data_string(encode_psobb_hangame_credentials(user_id, token, unused));
|
||||
fprintf(stdout, "psobb.exe 1196310600 %s\n", hex.c_str());
|
||||
});
|
||||
|
||||
Action a_format_ep3_battle_record(
|
||||
"format-ep3-battle-record", nullptr, +[](Arguments& args) {
|
||||
string data = read_input_data(args);
|
||||
Episode3::BattleRecord rec(data);
|
||||
rec.print(stdout);
|
||||
});
|
||||
|
||||
Action a_replay_ep3_battle_commands(
|
||||
"replay-ep3-battle-commands", nullptr, +[](Arguments& args) {
|
||||
auto s = make_shared<ServerState>();
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_ep3_cards(false);
|
||||
s->load_ep3_maps(false);
|
||||
|
||||
@@ -2341,13 +2433,8 @@ Action a_run_server_replay_log(
|
||||
config_log.info("newserv %s compiled at %s", GIT_REVISION_HASH, build_date.c_str());
|
||||
}
|
||||
|
||||
#ifdef PHOSG_WINDOWS
|
||||
int evthread_ret = evthread_use_windows_threads();
|
||||
#else
|
||||
int evthread_ret = evthread_use_pthreads();
|
||||
#endif
|
||||
if (evthread_ret) {
|
||||
throw runtime_error("failed to setup libevent threads");
|
||||
if (evthread_use_pthreads()) {
|
||||
throw runtime_error("failed to set up libevent threads");
|
||||
}
|
||||
|
||||
if (!isdir("system/players")) {
|
||||
@@ -2355,12 +2442,8 @@ Action a_run_server_replay_log(
|
||||
mkdir("system/players", 0755);
|
||||
}
|
||||
|
||||
string config_filename = args.get<string>("config");
|
||||
const string& replay_log_filename = args.get<string>("replay-log");
|
||||
bool is_replay = !replay_log_filename.empty();
|
||||
if (config_filename.empty()) {
|
||||
config_filename = "system/config.json";
|
||||
}
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
if (isatty(fileno(stderr))) {
|
||||
@@ -2372,25 +2455,24 @@ Action a_run_server_replay_log(
|
||||
}
|
||||
|
||||
shared_ptr<struct event_base> base(event_base_new(), event_base_free);
|
||||
auto state = make_shared<ServerState>(base, config_filename, is_replay);
|
||||
auto state = make_shared<ServerState>(base, get_config_filename(args), is_replay);
|
||||
state->load_all();
|
||||
|
||||
shared_ptr<DNSServer> dns_server;
|
||||
if (state->dns_server_port && !is_replay) {
|
||||
if (!state->dns_server_addr.empty()) {
|
||||
config_log.info("Starting DNS server on %s:%hu", state->dns_server_addr.c_str(), state->dns_server_port);
|
||||
} else {
|
||||
config_log.info("Starting DNS server on port %hu", state->dns_server_port);
|
||||
}
|
||||
dns_server = make_shared<DNSServer>(base, state->local_address, state->external_address);
|
||||
dns_server->listen(state->dns_server_addr, state->dns_server_port);
|
||||
state->dns_server = make_shared<DNSServer>(
|
||||
base, state->local_address, state->external_address, state->banned_ipv4_ranges);
|
||||
state->dns_server->listen(state->dns_server_addr, state->dns_server_port);
|
||||
} else {
|
||||
config_log.info("DNS server is disabled");
|
||||
}
|
||||
|
||||
shared_ptr<ServerShell> shell;
|
||||
shared_ptr<ReplaySession> replay_session;
|
||||
shared_ptr<IPStackSimulator> ip_stack_simulator;
|
||||
shared_ptr<HTTPServer> http_server;
|
||||
if (is_replay) {
|
||||
config_log.info("Starting proxy server");
|
||||
@@ -2465,21 +2547,24 @@ Action a_run_server_replay_log(
|
||||
|
||||
if (!state->ip_stack_addresses.empty() || !state->ppp_stack_addresses.empty() || !state->ppp_raw_addresses.empty()) {
|
||||
config_log.info("Starting IP/PPP stack simulator");
|
||||
ip_stack_simulator = make_shared<IPStackSimulator>(base, state);
|
||||
state->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, IPStackSimulator::Protocol::ETHERNET_TAPSERVER);
|
||||
state->ip_stack_simulator->listen(
|
||||
spec, netloc.first, netloc.second, IPStackSimulator::Protocol::ETHERNET_TAPSERVER);
|
||||
}
|
||||
for (const auto& it : state->ppp_stack_addresses) {
|
||||
auto netloc = parse_netloc(it);
|
||||
string spec = (netloc.second == 0) ? ("T-PPPST-" + netloc.first) : string_printf("T-PPPST-%hu", netloc.second);
|
||||
ip_stack_simulator->listen(spec, netloc.first, netloc.second, IPStackSimulator::Protocol::HDLC_TAPSERVER);
|
||||
state->ip_stack_simulator->listen(
|
||||
spec, netloc.first, netloc.second, IPStackSimulator::Protocol::HDLC_TAPSERVER);
|
||||
}
|
||||
for (const auto& it : state->ppp_raw_addresses) {
|
||||
auto netloc = parse_netloc(it);
|
||||
string spec = (netloc.second == 0) ? ("T-PPPSR-" + netloc.first) : string_printf("T-PPPSR-%hu", netloc.second);
|
||||
ip_stack_simulator->listen(spec, netloc.first, netloc.second, IPStackSimulator::Protocol::HDLC_RAW);
|
||||
state->ip_stack_simulator->listen(
|
||||
spec, netloc.first, netloc.second, IPStackSimulator::Protocol::HDLC_RAW);
|
||||
if (netloc.second) {
|
||||
if (state->local_address == state->external_address) {
|
||||
config_log.info(
|
||||
@@ -2636,6 +2721,30 @@ int main(int argc, char** argv) {
|
||||
log_error("Unknown or invalid action; try --help");
|
||||
return 1;
|
||||
}
|
||||
#ifdef PHOSG_WINDOWS
|
||||
// Cygwin just gives a stackdump when an exception falls out of main(), so
|
||||
// unlike Linux and macOS, we have to manually catch exceptions here just to
|
||||
// see what the exception message was.
|
||||
try {
|
||||
a->run(args);
|
||||
} catch (const cannot_open_file& e) {
|
||||
log_error("Top-level exception (cannot_open_file): %s", e.what());
|
||||
throw;
|
||||
} catch (const invalid_argument& e) {
|
||||
log_error("Top-level exception (invalid_argument): %s", e.what());
|
||||
throw;
|
||||
} catch (const out_of_range& e) {
|
||||
log_error("Top-level exception (out_of_range): %s", e.what());
|
||||
throw;
|
||||
} catch (const runtime_error& e) {
|
||||
log_error("Top-level exception (runtime_error): %s", e.what());
|
||||
throw;
|
||||
} catch (const exception& e) {
|
||||
log_error("Top-level exception: %s", e.what());
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
a->run(args);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
+36
-26
@@ -607,8 +607,10 @@ JSON Map::RareEnemyRates::json() const {
|
||||
}
|
||||
|
||||
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 "]",
|
||||
string name_str = Map::name_for_object_type(this->base_type);
|
||||
return string_printf("[ObjectEntry type=%04hX \"%s\" 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(),
|
||||
name_str.c_str(),
|
||||
this->flags.load(),
|
||||
this->index.load(),
|
||||
this->unknown_a2.load(),
|
||||
@@ -1478,23 +1480,29 @@ void Map::add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint16_t s
|
||||
ev.flags = flags;
|
||||
ev.floor = floor;
|
||||
ev.action_stream_offset = action_stream_offset;
|
||||
uint64_t k = (static_cast<uint64_t>(floor) << 32) | event_id;
|
||||
if (!this->floor_and_event_id_to_index.emplace(k, index).second) {
|
||||
this->log.warning("Duplicate event ID: W-%02hhX-%" PRIX32, floor, event_id);
|
||||
}
|
||||
|
||||
uint64_t k = (static_cast<uint64_t>(floor) << 32) | event_id;
|
||||
this->floor_and_event_id_to_index.emplace(k, index);
|
||||
k = section_index_key(floor, section, wave_number);
|
||||
this->floor_section_and_wave_number_to_event_index.emplace(k, index);
|
||||
}
|
||||
|
||||
Map::Event& Map::get_event(uint8_t floor, uint32_t event_id) {
|
||||
vector<Map::Event*> Map::get_events(uint8_t floor, uint32_t event_id) {
|
||||
uint64_t k = (static_cast<uint64_t>(floor) << 32) | event_id;
|
||||
return this->events.at(this->floor_and_event_id_to_index.at(k));
|
||||
vector<Event*> ret;
|
||||
for (auto its = this->floor_and_event_id_to_index.equal_range(k); its.first != its.second; its.first++) {
|
||||
ret.emplace_back(&this->events.at(its.first->second));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const Map::Event& Map::get_event(uint8_t floor, uint32_t event_id) const {
|
||||
vector<const Map::Event*> Map::get_events(uint8_t floor, uint32_t event_id) const {
|
||||
uint64_t k = (static_cast<uint64_t>(floor) << 32) | event_id;
|
||||
return this->events.at(this->floor_and_event_id_to_index.at(k));
|
||||
vector<const Event*> ret;
|
||||
for (auto its = this->floor_and_event_id_to_index.equal_range(k); its.first != its.second; its.first++) {
|
||||
ret.emplace_back(&this->events.at(its.first->second));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Map::add_events_from_map_data(uint8_t floor, const void* data, size_t size) {
|
||||
@@ -1932,7 +1940,8 @@ void SetDataTable::load_table_t(const string& data) {
|
||||
U32T unknown_a6; // == 0
|
||||
U32T unknown_a7; // == 0
|
||||
U32T unknown_a8; // == 0
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Footer, 0x20);
|
||||
|
||||
if (r.size() < sizeof(Footer)) {
|
||||
throw runtime_error("set data table is too small");
|
||||
}
|
||||
@@ -2175,24 +2184,25 @@ pair<uint32_t, uint32_t> SetDataTableDC112000::num_free_roam_variations_for_floo
|
||||
|
||||
string SetDataTableDC112000::map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode, GameMode, FilenameType type) const {
|
||||
if (floor >= this->NAMES.size()) {
|
||||
try {
|
||||
string basename = this->NAMES.at(floor).at(var1).at(var2);
|
||||
switch (type) {
|
||||
case FilenameType::ENEMIES:
|
||||
basename += "e.dat";
|
||||
break;
|
||||
case FilenameType::OBJECTS:
|
||||
basename += "o.dat";
|
||||
break;
|
||||
case FilenameType::EVENTS:
|
||||
basename += ".evt";
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid map filename type");
|
||||
}
|
||||
return basename;
|
||||
} catch (const out_of_range&) {
|
||||
return "";
|
||||
}
|
||||
string basename = this->NAMES.at(floor).at(var1).at(var2);
|
||||
switch (type) {
|
||||
case FilenameType::ENEMIES:
|
||||
basename += "e.dat";
|
||||
break;
|
||||
case FilenameType::OBJECTS:
|
||||
basename += "o.dat";
|
||||
break;
|
||||
case FilenameType::EVENTS:
|
||||
basename += ".evt";
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid map filename type");
|
||||
}
|
||||
return basename;
|
||||
}
|
||||
|
||||
static const vector<AreaMapFileInfo> map_file_info_dc_nte = {
|
||||
|
||||
+15
-15
@@ -34,7 +34,7 @@ struct Map {
|
||||
inline Type type() const {
|
||||
return static_cast<Type>(this->le_type.load());
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(SectionHeader, 0x10);
|
||||
|
||||
struct ObjectEntry { // Section type 1 (OBJECTS)
|
||||
/* 00 */ le_uint16_t base_type;
|
||||
@@ -61,7 +61,7 @@ struct Map {
|
||||
/* 44 */
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(ObjectEntry, 0x44);
|
||||
|
||||
struct EnemyEntry { // Section type 2 (ENEMIES)
|
||||
/* 00 */ le_uint16_t base_type;
|
||||
@@ -91,7 +91,7 @@ struct Map {
|
||||
/* 48 */
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(EnemyEntry, 0x48);
|
||||
|
||||
struct EventsSectionHeader { // Section type 3 (WAVE_EVENTS)
|
||||
/* 00 */ le_uint32_t action_stream_offset;
|
||||
@@ -99,7 +99,7 @@ struct Map {
|
||||
/* 08 */ le_uint32_t entry_count;
|
||||
/* 0C */ be_uint32_t format; // 0 or 'evt2'
|
||||
/* 10 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(EventsSectionHeader, 0x10);
|
||||
|
||||
struct Event1Entry { // Section type 3 (WAVE_EVENTS) if format == 0
|
||||
/* 00 */ le_uint32_t event_id;
|
||||
@@ -114,7 +114,7 @@ struct Map {
|
||||
/* 0C */ le_uint32_t delay;
|
||||
/* 10 */ le_uint32_t action_stream_offset;
|
||||
/* 14 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Event1Entry, 0x14);
|
||||
|
||||
struct Event2Entry { // Section type 3 (WAVE_EVENTS) if format == 'evt2'
|
||||
/* 00 */ le_uint32_t event_id;
|
||||
@@ -129,21 +129,21 @@ struct Map {
|
||||
/* 12 */ le_uint16_t max_waves;
|
||||
/* 14 */ le_uint32_t action_stream_offset;
|
||||
/* 18 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Event2Entry, 0x18);
|
||||
|
||||
struct RandomEnemyLocationsHeader { // Section type 4 (RANDOM_ENEMY_LOCATIONS)
|
||||
/* 00 */ le_uint32_t section_table_offset; // Offset to RandomEnemyLocationSegment structs, from start of this struct
|
||||
/* 04 */ le_uint32_t entries_offset; // Offset to RandomEnemyLocationEntry structs, from start of this struct
|
||||
/* 08 */ le_uint32_t num_sections;
|
||||
/* 0C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(RandomEnemyLocationsHeader, 0x0C);
|
||||
|
||||
struct RandomEnemyLocationSection { // Section type 4 (RANDOM_ENEMY_LOCATIONS)
|
||||
/* 00 */ le_uint16_t section;
|
||||
/* 02 */ le_uint16_t count;
|
||||
/* 04 */ le_uint32_t offset;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(RandomEnemyLocationSection, 8);
|
||||
|
||||
struct RandomEnemyLocationEntry { // Section type 4 (RANDOM_ENEMY_LOCATIONS)
|
||||
/* 00 */ le_float x;
|
||||
@@ -155,7 +155,7 @@ struct Map {
|
||||
/* 18 */ uint16_t unknown_a9;
|
||||
/* 1A */ uint16_t unknown_a10;
|
||||
/* 1C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(RandomEnemyLocationEntry, 0x1C);
|
||||
|
||||
struct RandomEnemyDefinitionsHeader { // Section type 5 (RANDOM_ENEMY_DEFINITIONS)
|
||||
/* 00 */ le_uint32_t entries_offset; // Offset to RandomEnemyDefinition structs, from start of this struct
|
||||
@@ -163,7 +163,7 @@ struct Map {
|
||||
/* 08 */ le_uint32_t entry_count;
|
||||
/* 0C */ le_uint32_t weight_entry_count;
|
||||
/* 10 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(RandomEnemyDefinitionsHeader, 0x10);
|
||||
|
||||
struct RandomEnemyDefinition { // Section type 5 (RANDOM_ENEMY_DEFINITIONS)
|
||||
// All fields through entry_num map to the corresponding fields in
|
||||
@@ -179,7 +179,7 @@ struct Map {
|
||||
/* 1C */ le_uint16_t min_children;
|
||||
/* 1E */ le_uint16_t max_children;
|
||||
/* 20 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(RandomEnemyDefinition, 0x20);
|
||||
|
||||
struct RandomEnemyWeight { // Section type 5 (RANDOM_ENEMY_DEFINITIONS)
|
||||
/* 00 */ uint8_t base_type_index;
|
||||
@@ -187,7 +187,7 @@ struct Map {
|
||||
/* 02 */ uint8_t weight;
|
||||
/* 03 */ uint8_t unknown_a4;
|
||||
/* 04 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(RandomEnemyWeight, 4);
|
||||
|
||||
struct RareEnemyRates {
|
||||
uint32_t hildeblue; // HILDEBEAR -> HILDEBLUE
|
||||
@@ -330,8 +330,8 @@ struct Map {
|
||||
uint16_t section,
|
||||
uint16_t wave_number,
|
||||
uint32_t action_stream_offset);
|
||||
Event& get_event(uint8_t floor, uint32_t event_id);
|
||||
const Event& get_event(uint8_t floor, uint32_t event_id) const;
|
||||
std::vector<Event*> get_events(uint8_t floor, uint32_t event_id);
|
||||
std::vector<const Event*> get_events(uint8_t floor, uint32_t event_id) const;
|
||||
void add_events_from_map_data(uint8_t floor, const void* data, size_t size);
|
||||
|
||||
struct DATSectionsForFloor {
|
||||
@@ -373,7 +373,7 @@ struct Map {
|
||||
std::vector<size_t> rare_enemy_indexes;
|
||||
std::vector<Event> events;
|
||||
std::string event_action_stream;
|
||||
std::map<uint64_t, size_t> floor_and_event_id_to_index;
|
||||
std::multimap<uint64_t, size_t> floor_and_event_id_to_index;
|
||||
std::unordered_multimap<uint64_t, size_t> floor_section_and_group_to_object_index;
|
||||
std::unordered_multimap<uint64_t, size_t> floor_section_and_wave_number_to_enemy_index;
|
||||
std::unordered_multimap<uint64_t, size_t> floor_section_and_wave_number_to_event_index;
|
||||
|
||||
@@ -24,6 +24,7 @@ constexpr uint32_t QUEST_CATEGORIES = 0x66010166;
|
||||
constexpr uint32_t PROXY_DESTINATIONS = 0x77000077;
|
||||
constexpr uint32_t PROGRAMS = 0x88000088;
|
||||
constexpr uint32_t PATCHES = 0x99000099;
|
||||
constexpr uint32_t PATCH_SWITCHES = 0x99010199;
|
||||
constexpr uint32_t PROXY_OPTIONS = 0xAA0000AA;
|
||||
constexpr uint32_t TOURNAMENTS = 0xBB0000BB;
|
||||
constexpr uint32_t TOURNAMENTS_FOR_SPEC = 0xBB1111BB;
|
||||
@@ -36,6 +37,7 @@ constexpr uint32_t INFORMATION = 0x11333311;
|
||||
constexpr uint32_t DOWNLOAD_QUESTS = 0x11444411;
|
||||
constexpr uint32_t PROXY_DESTINATIONS = 0x11555511;
|
||||
constexpr uint32_t PATCHES = 0x11666611;
|
||||
constexpr uint32_t PATCH_SWITCHES = 0x11676711;
|
||||
constexpr uint32_t PROGRAMS = 0x11777711;
|
||||
constexpr uint32_t DISCONNECT = 0x11888811;
|
||||
constexpr uint32_t CLEAR_LICENSE = 0x11999911;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -23,8 +24,7 @@ uint32_t resolve_address(const char* address) {
|
||||
"can\'t resolve hostname %s: %s", address, e.c_str()));
|
||||
}
|
||||
|
||||
std::unique_ptr<struct addrinfo, void (*)(struct addrinfo*)> res0_unique(
|
||||
res0, freeaddrinfo);
|
||||
std::unique_ptr<struct addrinfo, void (*)(struct addrinfo*)> res0_unique(res0, freeaddrinfo);
|
||||
struct addrinfo* res4 = nullptr;
|
||||
for (struct addrinfo* res = res0; res; res = res->ai_next) {
|
||||
if (res->ai_family == AF_INET) {
|
||||
|
||||
+30
-20
@@ -114,13 +114,13 @@ public:
|
||||
parray<le_uint32_t, 0x12> as32;
|
||||
InitialKeys() : as32() {}
|
||||
InitialKeys(const InitialKeys& other) : as32(other.as32) {}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(InitialKeys, 0x48);
|
||||
union PrivateKeys {
|
||||
parray<uint8_t, 0x1000> as8;
|
||||
parray<le_uint32_t, 0x400> as32;
|
||||
PrivateKeys() : as32() {}
|
||||
PrivateKeys(const PrivateKeys& other) : as32(other.as32) {}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PrivateKeys, 0x1000);
|
||||
InitialKeys initial_keys;
|
||||
PrivateKeys private_keys;
|
||||
// This field only really needs to be one byte, but annoyingly, some
|
||||
@@ -129,7 +129,7 @@ public:
|
||||
// this structure's size from not matching the .nsk files' sizes, we use
|
||||
// an unnecessarily large size for this field.
|
||||
le_uint64_t subtype;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(KeyFile, 0x1050);
|
||||
|
||||
PSOBBEncryption(const KeyFile& key, const void* seed, size_t seed_size);
|
||||
|
||||
@@ -254,39 +254,49 @@ uint32_t encrypt_challenge_time(uint16_t value);
|
||||
uint16_t decrypt_challenge_time(uint32_t value);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
class ChallengeTime {
|
||||
class ChallengeTimeT {
|
||||
private:
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
U32T value;
|
||||
|
||||
public:
|
||||
ChallengeTime() = default;
|
||||
ChallengeTime(uint16_t v) {
|
||||
this->store(v);
|
||||
ChallengeTimeT() = default;
|
||||
ChallengeTimeT(uint16_t v) {
|
||||
this->encode(v);
|
||||
}
|
||||
ChallengeTime(const ChallengeTime& other) = default;
|
||||
ChallengeTime(ChallengeTime&& other) = default;
|
||||
ChallengeTime& operator=(const ChallengeTime& other) = default;
|
||||
ChallengeTime& operator=(ChallengeTime&& other) = default;
|
||||
ChallengeTimeT(const ChallengeTimeT& other) = default;
|
||||
ChallengeTimeT(ChallengeTimeT&& other) = default;
|
||||
ChallengeTimeT& operator=(const ChallengeTimeT& other) = default;
|
||||
ChallengeTimeT& operator=(ChallengeTimeT&& other) = default;
|
||||
|
||||
bool has_value() const {
|
||||
return this->value != 0;
|
||||
}
|
||||
|
||||
uint16_t load() const {
|
||||
uint32_t load_raw() const {
|
||||
return this->value;
|
||||
}
|
||||
void store_raw(uint32_t value) {
|
||||
this->value = value;
|
||||
}
|
||||
|
||||
uint16_t decode() const {
|
||||
return decrypt_challenge_time(this->value);
|
||||
}
|
||||
operator uint16_t() const {
|
||||
return this->load();
|
||||
}
|
||||
void store(uint16_t v) {
|
||||
void encode(uint16_t v) {
|
||||
this->value = ((v == 0) || (v == 0xFFFF)) ? 0 : encrypt_challenge_time(v);
|
||||
}
|
||||
ChallengeTime& operator=(uint16_t v) {
|
||||
this->store(v);
|
||||
return *this;
|
||||
|
||||
operator ChallengeTimeT<!IsBigEndian>() const {
|
||||
ChallengeTimeT<!IsBigEndian> ret;
|
||||
ret.store_raw(this->value);
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using ChallengeTime = ChallengeTimeT<false>;
|
||||
using ChallengeTimeBE = ChallengeTimeT<true>;
|
||||
check_struct_size(ChallengeTime, 4);
|
||||
check_struct_size(ChallengeTimeBE, 4);
|
||||
|
||||
std::string decrypt_v2_registry_value(const void* data, size_t size);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ struct TObjectVTable {
|
||||
be_uint32_t update;
|
||||
be_uint32_t render;
|
||||
be_uint32_t render_shadow;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(TObjectVTable, 0x18);
|
||||
|
||||
struct TObject {
|
||||
be_uint32_t type_name_addr;
|
||||
@@ -22,7 +22,7 @@ struct TObject {
|
||||
be_uint32_t parent_addr;
|
||||
be_uint32_t children_head_addr;
|
||||
be_uint32_t vtable_addr;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(TObject, 0x1C);
|
||||
|
||||
PSOGCObjectGraph::PSOGCObjectGraph(
|
||||
const string& memory_data, uint32_t root_address) {
|
||||
|
||||
+4
-4
@@ -13,19 +13,19 @@ struct PSOCommandHeaderPC {
|
||||
le_uint16_t size;
|
||||
uint8_t command;
|
||||
uint8_t flag;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOCommandHeaderPC, 4);
|
||||
|
||||
struct PSOCommandHeaderDCV3 {
|
||||
uint8_t command;
|
||||
uint8_t flag;
|
||||
le_uint16_t size;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOCommandHeaderDCV3, 4);
|
||||
|
||||
struct PSOCommandHeaderBB {
|
||||
le_uint16_t size;
|
||||
le_uint16_t command;
|
||||
le_uint32_t flag;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOCommandHeaderBB, 8);
|
||||
|
||||
union PSOCommandHeader {
|
||||
PSOCommandHeaderDCV3 dc;
|
||||
@@ -45,7 +45,7 @@ union PSOCommandHeader {
|
||||
}
|
||||
|
||||
PSOCommandHeader();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOCommandHeader, 8);
|
||||
|
||||
// This function is used in a lot of places to check received command sizes and
|
||||
// cast them to the appropriate type
|
||||
|
||||
+46
-21
@@ -38,7 +38,7 @@ PatchServer::Client::Client(
|
||||
: server(server),
|
||||
id(next_id++),
|
||||
log(string_printf("[C-%" PRIX64 "] ", this->id), client_log.min_level),
|
||||
channel(bev, version, 1, nullptr, nullptr, this, string_printf("C-%" PRIX64, this->id), TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN),
|
||||
channel(bev, 0, version, 1, nullptr, nullptr, this, string_printf("C-%" PRIX64, this->id), TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN),
|
||||
idle_timeout_usecs(idle_timeout_usecs),
|
||||
idle_timeout_event(
|
||||
event_new(bufferevent_get_base(bev), -1, EV_TIMEOUT, &PatchServer::Client::dispatch_idle_timeout, this),
|
||||
@@ -160,19 +160,19 @@ void PatchServer::on_04(shared_ptr<Client> c, string& data) {
|
||||
string password = cmd.password.decode();
|
||||
|
||||
// There are 3 cases here:
|
||||
// - No login information at all: just proceed without checking license
|
||||
// - Username only: check that license exists if allow_unregistered_users is off
|
||||
// - No login information at all: just proceed without checking credentials
|
||||
// - Username: check that account exists if allow_unregistered_users is off
|
||||
// - Username and password: call verify_bb
|
||||
if (!username.empty() && !password.empty()) {
|
||||
try {
|
||||
this->config->license_index->verify_bb(username, password);
|
||||
this->config->account_index->from_bb_credentials(username, &password, false);
|
||||
|
||||
} catch (const LicenseIndex::incorrect_password& e) {
|
||||
} catch (const AccountIndex::incorrect_password& e) {
|
||||
this->send_message_box(c, string_printf("Login failed: %s", e.what()));
|
||||
this->disconnect_client(c);
|
||||
return;
|
||||
|
||||
} catch (const LicenseIndex::missing_license& e) {
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
if (!this->config->allow_unregistered_users) {
|
||||
this->send_message_box(c, string_printf("Login failed: %s", e.what()));
|
||||
this->disconnect_client(c);
|
||||
@@ -182,8 +182,8 @@ void PatchServer::on_04(shared_ptr<Client> c, string& data) {
|
||||
|
||||
} else if (!username.empty() && !this->config->allow_unregistered_users) {
|
||||
try {
|
||||
this->config->license_index->get_by_bb_username(username);
|
||||
} catch (const LicenseIndex::missing_license& e) {
|
||||
this->config->account_index->from_bb_credentials(username, nullptr, false);
|
||||
} catch (const AccountIndex::missing_account& e) {
|
||||
this->send_message_box(c, string_printf("Login failed: %s", e.what()));
|
||||
this->disconnect_client(c);
|
||||
return;
|
||||
@@ -279,12 +279,12 @@ void PatchServer::on_10(shared_ptr<Client> c, string&) {
|
||||
}
|
||||
|
||||
void PatchServer::disconnect_client(shared_ptr<Client> c) {
|
||||
if (c->channel.is_virtual_connection) {
|
||||
server_log.info("Client disconnected: C-%" PRIX64 " on virtual connection %p", c->id, c->channel.bev.get());
|
||||
if (c->channel.virtual_network_id) {
|
||||
server_log.info("Client disconnected: C-%" PRIX64 " on N-%" PRIu64, c->id, c->channel.virtual_network_id);
|
||||
} else if (c->channel.bev) {
|
||||
server_log.info("Client disconnected: C-%" PRIX64 " on fd %d", c->id, bufferevent_getfd(c->channel.bev.get()));
|
||||
server_log.info("Client disconnected: C-%" PRIX64, c->id);
|
||||
} else {
|
||||
server_log.info("Client C-%" PRIX64 " removed from game server", c->id);
|
||||
server_log.info("Client C-%" PRIX64 " removed from patch server", c->id);
|
||||
}
|
||||
|
||||
this->channel_to_client.erase(&c->channel);
|
||||
@@ -320,6 +320,13 @@ void PatchServer::dispatch_on_listen_error(
|
||||
}
|
||||
|
||||
void PatchServer::on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr*, int) {
|
||||
struct sockaddr_storage remote_addr;
|
||||
get_socket_addresses(fd, nullptr, &remote_addr);
|
||||
if (this->config->banned_ipv4_ranges->check(remote_addr)) {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
int listen_fd = evconnlistener_get_fd(listener);
|
||||
ListeningSocket* listening_socket;
|
||||
try {
|
||||
@@ -395,17 +402,31 @@ void PatchServer::on_client_error(Channel& ch, short events) {
|
||||
}
|
||||
|
||||
PatchServer::PatchServer(shared_ptr<const Config> config)
|
||||
: base(event_base_new(), event_base_free),
|
||||
config(config),
|
||||
destroy_clients_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &PatchServer::dispatch_destroy_clients, this), event_free),
|
||||
th(&PatchServer::thread_fn, this) {}
|
||||
: config(config) {
|
||||
if (config->shared_base) {
|
||||
this->base = config->shared_base;
|
||||
this->base_is_shared = true;
|
||||
} else {
|
||||
this->base.reset(event_base_new(), event_base_free);
|
||||
this->base_is_shared = false;
|
||||
}
|
||||
this->destroy_clients_ev.reset(
|
||||
event_new(this->base.get(), -1, EV_TIMEOUT, &PatchServer::dispatch_destroy_clients, this), event_free);
|
||||
if (!this->base_is_shared) {
|
||||
this->th = thread(&PatchServer::thread_fn, this);
|
||||
}
|
||||
}
|
||||
|
||||
void PatchServer::schedule_stop() {
|
||||
event_base_loopexit(this->base.get(), nullptr);
|
||||
if (!this->base_is_shared) {
|
||||
event_base_loopexit(this->base.get(), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void PatchServer::wait_for_stop() {
|
||||
this->th.join();
|
||||
if (!this->base_is_shared) {
|
||||
this->th.join();
|
||||
}
|
||||
}
|
||||
|
||||
void PatchServer::listen(const std::string& addr_str, const string& socket_path, Version version) {
|
||||
@@ -447,7 +468,11 @@ void PatchServer::thread_fn() {
|
||||
}
|
||||
|
||||
void PatchServer::set_config(std::shared_ptr<const Config> config) {
|
||||
forward_to_event_thread(this->base, [s = this->shared_from_this(), config = std::move(config)]() {
|
||||
s->config = config;
|
||||
});
|
||||
if (this->base_is_shared) {
|
||||
this->config = config;
|
||||
} else {
|
||||
forward_to_event_thread(this->base, [s = this->shared_from_this(), config = std::move(config)]() {
|
||||
s->config = config;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -8,8 +8,9 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "Account.hh"
|
||||
#include "Channel.hh"
|
||||
#include "License.hh"
|
||||
#include "IPV4RangeSet.hh"
|
||||
#include "PatchFileIndex.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
@@ -20,8 +21,10 @@ public:
|
||||
bool hide_data_from_logs;
|
||||
uint64_t idle_timeout_usecs;
|
||||
std::string message;
|
||||
std::shared_ptr<const LicenseIndex> license_index;
|
||||
std::shared_ptr<AccountIndex> account_index;
|
||||
std::shared_ptr<const PatchFileIndex> patch_file_index;
|
||||
std::shared_ptr<const IPV4RangeSet> banned_ipv4_ranges;
|
||||
std::shared_ptr<struct event_base> shared_base;
|
||||
};
|
||||
|
||||
PatchServer() = delete;
|
||||
@@ -86,6 +89,7 @@ private:
|
||||
};
|
||||
|
||||
std::shared_ptr<struct event_base> base;
|
||||
bool base_is_shared;
|
||||
std::shared_ptr<const Config> config;
|
||||
|
||||
std::unordered_set<std::shared_ptr<Client>> clients_to_destroy;
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include <array>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/JSON.hh>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ChoiceSearch.hh"
|
||||
#include "FileContentsCache.hh"
|
||||
#include "ItemData.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
class Client;
|
||||
class ItemParameterTable;
|
||||
|
||||
// PSO V2 stored some extra data in the character structs in a format that I'm
|
||||
// sure Sega thought was very clever for backward compatibility, but for us is
|
||||
// just plain annoying. Specifically, they used the third and fourth bytes of
|
||||
// the InventoryItem struct to store some things not present in V1. The game
|
||||
// stores arrays of bytes striped across these structures. In newserv, we call
|
||||
// those fields extension_data. They contain:
|
||||
// items[0].extension_data1 through items[19].extension_data1:
|
||||
// Extended technique levels. The values in the technique_levels_v1 array
|
||||
// only go up to 14 (tech level 15); if the player has a technique above
|
||||
// level 15, the corresponding extension_data1 field holds the remaining
|
||||
// levels (so a level 20 tech would have 14 in technique_levels_v1 and 5
|
||||
// in the corresponding item's extension_data1 field).
|
||||
// items[0].extension_data2 through items[3].extension_data2:
|
||||
// The flags field from the PSOGCCharacterFile::Character struct; see
|
||||
// SaveFileFormats.hh for details.
|
||||
// items[4].extension_data2 through items[7].extension_data2:
|
||||
// The timestamp when the character was last saved, in seconds since
|
||||
// January 1, 2000. Stored little-endian, so items[4] contains the LSB.
|
||||
// items[8].extension_data2 through items[12].extension_data2:
|
||||
// Number of power materials, mind materials, evade materials, def
|
||||
// materials, and luck materials (respectively) used by the player.
|
||||
// items[13].extension_data2 through items[15].extension_data2:
|
||||
// Unknown. These are not an array, but do appear to be related.
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct PlayerInventoryItemT {
|
||||
/* 00 */ uint8_t present = 0;
|
||||
/* 01 */ uint8_t unknown_a1 = 0;
|
||||
// See note above about these fields
|
||||
/* 02 */ uint8_t extension_data1 = 0;
|
||||
/* 03 */ uint8_t extension_data2 = 0;
|
||||
/* 04 */ le_uint32_t flags = 0; // 8 = equipped
|
||||
/* 08 */ ItemData data;
|
||||
/* 1C */
|
||||
|
||||
PlayerInventoryItemT() = default;
|
||||
|
||||
PlayerInventoryItemT(const ItemData& item, bool equipped)
|
||||
: present(1),
|
||||
unknown_a1(0),
|
||||
extension_data1(0),
|
||||
extension_data2(0),
|
||||
flags(equipped ? 8 : 0),
|
||||
data(item) {}
|
||||
|
||||
operator PlayerInventoryItemT<!IsBigEndian>() const {
|
||||
PlayerInventoryItemT<!IsBigEndian> ret;
|
||||
ret.present = this->present;
|
||||
ret.unknown_a1 = this->unknown_a1;
|
||||
ret.extension_data1 = this->extension_data1;
|
||||
ret.extension_data2 = this->extension_data2;
|
||||
ret.flags = this->flags.load();
|
||||
ret.data = this->data;
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using PlayerInventoryItem = PlayerInventoryItemT<false>;
|
||||
using PlayerInventoryItemBE = PlayerInventoryItemT<true>;
|
||||
check_struct_size(PlayerInventoryItem, 0x1C);
|
||||
check_struct_size(PlayerInventoryItemBE, 0x1C);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct PlayerBankItemT {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
|
||||
/* 00 */ ItemData data;
|
||||
/* 14 */ U16T amount = 0;
|
||||
/* 16 */ U16T present = 0;
|
||||
/* 18 */
|
||||
|
||||
inline bool operator<(const PlayerBankItemT<IsBigEndian>& other) const {
|
||||
return this->data < other.data;
|
||||
}
|
||||
|
||||
operator PlayerBankItemT<!IsBigEndian>() const {
|
||||
PlayerBankItemT<!IsBigEndian> ret;
|
||||
ret.data = this->data;
|
||||
ret.amount = this->amount.load();
|
||||
ret.present = this->present.load();
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using PlayerBankItem = PlayerBankItemT<false>;
|
||||
using PlayerBankItemBE = PlayerBankItemT<true>;
|
||||
check_struct_size(PlayerBankItem, 0x18);
|
||||
check_struct_size(PlayerBankItemBE, 0x18);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct PlayerInventoryT {
|
||||
/* 0000 */ uint8_t num_items = 0;
|
||||
/* 0001 */ uint8_t hp_from_materials = 0;
|
||||
/* 0002 */ uint8_t tp_from_materials = 0;
|
||||
/* 0003 */ uint8_t language = 0;
|
||||
/* 0004 */ parray<PlayerInventoryItemT<IsBigEndian>, 30> items;
|
||||
/* 034C */
|
||||
|
||||
size_t find_item(uint32_t item_id) const {
|
||||
for (size_t x = 0; x < this->num_items; x++) {
|
||||
if (this->items[x].data.id == item_id) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw std::out_of_range("item not present");
|
||||
}
|
||||
|
||||
size_t find_item_by_primary_identifier(uint32_t primary_identifier) const {
|
||||
for (size_t x = 0; x < this->num_items; x++) {
|
||||
if (this->items[x].data.primary_identifier() == primary_identifier) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw std::out_of_range("item not present");
|
||||
}
|
||||
|
||||
size_t find_equipped_item(EquipSlot slot) const {
|
||||
ssize_t ret = -1;
|
||||
for (size_t y = 0; y < this->num_items; y++) {
|
||||
const auto& i = this->items[y];
|
||||
if (!(i.flags & 0x00000008)) {
|
||||
continue;
|
||||
}
|
||||
if (!i.data.can_be_equipped_in_slot(slot)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Units can be equipped in multiple slots, so the currently-equipped slot
|
||||
// is stored in the item data itself.
|
||||
if (((slot == EquipSlot::UNIT_1) && (i.data.data1[4] != 0x00)) ||
|
||||
((slot == EquipSlot::UNIT_2) && (i.data.data1[4] != 0x01)) ||
|
||||
((slot == EquipSlot::UNIT_3) && (i.data.data1[4] != 0x02)) ||
|
||||
((slot == EquipSlot::UNIT_4) && (i.data.data1[4] != 0x03))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ret < 0) {
|
||||
ret = y;
|
||||
} else {
|
||||
throw std::runtime_error("multiple items are equipped in the same slot");
|
||||
}
|
||||
}
|
||||
if (ret < 0) {
|
||||
throw std::out_of_range("no item is equipped in this slot");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool has_equipped_item(EquipSlot slot) const {
|
||||
try {
|
||||
this->find_equipped_item(slot);
|
||||
return true;
|
||||
} catch (const std::out_of_range&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void equip_item_id(uint32_t item_id, EquipSlot slot, bool allow_overwrite) {
|
||||
this->equip_item_index(this->find_item(item_id), slot, allow_overwrite);
|
||||
}
|
||||
|
||||
void equip_item_index(size_t index, EquipSlot slot, bool allow_overwrite) {
|
||||
auto& item = this->items[index];
|
||||
|
||||
if ((slot == EquipSlot::UNKNOWN) || !item.data.can_be_equipped_in_slot(slot)) {
|
||||
slot = item.data.default_equip_slot();
|
||||
}
|
||||
|
||||
if (this->has_equipped_item(slot)) {
|
||||
if (allow_overwrite) {
|
||||
this->unequip_item_slot(slot);
|
||||
} else {
|
||||
throw std::runtime_error("equip slot is already in use");
|
||||
}
|
||||
}
|
||||
|
||||
item.flags |= 0x00000008;
|
||||
// Units store which slot they're equipped in within the item data itself
|
||||
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x03)) {
|
||||
item.data.data1[4] = static_cast<uint8_t>(slot) - 9;
|
||||
}
|
||||
}
|
||||
|
||||
void unequip_item_id(uint32_t item_id) {
|
||||
this->unequip_item_index(this->find_item(item_id));
|
||||
}
|
||||
|
||||
void unequip_item_slot(EquipSlot slot) {
|
||||
this->unequip_item_index(this->find_equipped_item(slot));
|
||||
}
|
||||
|
||||
void unequip_item_index(size_t index) {
|
||||
auto& item = this->items[index];
|
||||
|
||||
item.flags &= (~0x00000008);
|
||||
// Units store which slot they're equipped in within the item data itself
|
||||
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x03)) {
|
||||
item.data.data1[4] = 0x00;
|
||||
}
|
||||
// If the item is an armor, remove all units too
|
||||
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x01)) {
|
||||
for (size_t z = 0; z < 30; z++) {
|
||||
auto& unit = this->items[z];
|
||||
if ((unit.data.data1[0] == 0x01) && (unit.data.data1[1] == 0x03)) {
|
||||
unit.flags &= (~0x00000008);
|
||||
unit.data.data1[4] = 0x00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t remove_all_items_of_type(uint8_t data1_0, int16_t data1_1 = -1) {
|
||||
size_t write_offset = 0;
|
||||
for (size_t read_offset = 0; read_offset < this->num_items; read_offset++) {
|
||||
bool should_delete = ((this->items[read_offset].data.data1[0] == data1_0) &&
|
||||
((data1_1 < 0) || (this->items[read_offset].data.data1[1] == static_cast<uint8_t>(data1_1))));
|
||||
if (!should_delete) {
|
||||
if (read_offset != write_offset) {
|
||||
this->items[write_offset].present = this->items[read_offset].present;
|
||||
this->items[write_offset].unknown_a1 = this->items[read_offset].unknown_a1;
|
||||
this->items[write_offset].flags = this->items[read_offset].flags;
|
||||
this->items[write_offset].data = this->items[read_offset].data;
|
||||
}
|
||||
write_offset++;
|
||||
}
|
||||
}
|
||||
size_t ret = this->num_items - write_offset;
|
||||
this->num_items = write_offset;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void decode_from_client(Version v) {
|
||||
for (size_t z = 0; z < this->items.size(); z++) {
|
||||
this->items[z].data.decode_for_version(v);
|
||||
}
|
||||
}
|
||||
|
||||
void encode_for_client(Version v, std::shared_ptr<const ItemParameterTable> item_parameter_table) {
|
||||
if (v == Version::DC_NTE) {
|
||||
// DC NTE has the item count as a 32-bit value here, whereas every other
|
||||
// version uses a single byte. To stop DC NTE from crashing by trying to
|
||||
// construct far more than 30 TItem objects, we clear the fields DC NTE
|
||||
// doesn't know about. Note that the 11/2000 prototype does not have this
|
||||
// issue - its inventory format matches the rest of the versions.
|
||||
this->hp_from_materials = 0;
|
||||
this->tp_from_materials = 0;
|
||||
this->language = 0;
|
||||
} else if ((v != Version::PC_NTE) && (v != Version::PC_V2)) {
|
||||
if (this->language > 4) {
|
||||
this->language = 0;
|
||||
}
|
||||
} else {
|
||||
if (this->language > 7) {
|
||||
this->language = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// For pre-V2 clients, use the V2 parameter table, since the V1 table
|
||||
// doesn't have correct encodings for backward-compatible V2 items.
|
||||
for (size_t z = 0; z < this->items.size(); z++) {
|
||||
this->items[z].data.encode_for_version(v, item_parameter_table);
|
||||
}
|
||||
}
|
||||
|
||||
operator PlayerInventoryT<!IsBigEndian>() const {
|
||||
PlayerInventoryT<!IsBigEndian> ret;
|
||||
ret.num_items = this->num_items;
|
||||
ret.hp_from_materials = this->hp_from_materials;
|
||||
ret.tp_from_materials = this->tp_from_materials;
|
||||
ret.language = this->language;
|
||||
ret.items = this->items;
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using PlayerInventory = PlayerInventoryT<false>;
|
||||
using PlayerInventoryBE = PlayerInventoryT<true>;
|
||||
check_struct_size(PlayerInventory, 0x34C);
|
||||
check_struct_size(PlayerInventoryBE, 0x34C);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct PlayerBankT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
/* 0000 */ U32T num_items = 0;
|
||||
/* 0004 */ U32T meseta = 0;
|
||||
/* 0008 */ parray<PlayerBankItemT<IsBigEndian>, 200> items;
|
||||
/* 12C8 */
|
||||
|
||||
void add_item(const ItemData& item, const ItemData::StackLimits& limits) {
|
||||
uint32_t primary_identifier = item.primary_identifier();
|
||||
|
||||
if (primary_identifier == 0x04000000) {
|
||||
this->meseta += item.data2d;
|
||||
if (this->meseta > 999999) {
|
||||
this->meseta = 999999;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
size_t combine_max = item.max_stack_size(limits);
|
||||
if (combine_max > 1) {
|
||||
size_t y;
|
||||
for (y = 0; y < this->num_items; y++) {
|
||||
if (this->items[y].data.primary_identifier() == primary_identifier) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (y < this->num_items) {
|
||||
uint8_t new_count = this->items[y].data.data1[5] + item.data1[5];
|
||||
if (new_count > combine_max) {
|
||||
throw std::runtime_error("stack size would exceed limit");
|
||||
}
|
||||
this->items[y].data.data1[5] = new_count;
|
||||
this->items[y].amount = new_count;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->num_items >= 200) {
|
||||
throw std::runtime_error("no free space in bank");
|
||||
}
|
||||
auto& last_item = this->items[this->num_items];
|
||||
last_item.data = item;
|
||||
last_item.amount = (item.max_stack_size(limits) > 1) ? item.data1[5] : 1;
|
||||
last_item.present = 1;
|
||||
this->num_items++;
|
||||
}
|
||||
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) {
|
||||
size_t index = this->find_item(item_id);
|
||||
auto& bank_item = this->items[index];
|
||||
|
||||
ItemData ret;
|
||||
if (amount && (bank_item.data.stack_size(limits) > 1) && (amount < bank_item.data.data1[5])) {
|
||||
ret = bank_item.data;
|
||||
ret.data1[5] = amount;
|
||||
bank_item.data.data1[5] -= amount;
|
||||
bank_item.amount -= amount;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = bank_item.data;
|
||||
this->num_items--;
|
||||
for (size_t x = index; x < this->num_items; x++) {
|
||||
this->items[x] = this->items[x + 1];
|
||||
}
|
||||
auto& last_item = this->items[this->num_items];
|
||||
last_item.amount = 0;
|
||||
last_item.present = 0;
|
||||
last_item.data.clear();
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t find_item(uint32_t item_id) {
|
||||
for (size_t x = 0; x < this->num_items; x++) {
|
||||
if (this->items[x].data.id == item_id) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw std::out_of_range("item not present");
|
||||
}
|
||||
|
||||
void sort() {
|
||||
std::sort(this->items.data(), this->items.data() + this->num_items);
|
||||
}
|
||||
|
||||
void assign_ids(uint32_t base_id) {
|
||||
for (size_t z = 0; z < this->num_items; z++) {
|
||||
this->items[z].data.id = base_id + z;
|
||||
}
|
||||
}
|
||||
|
||||
operator PlayerBankT<!IsBigEndian>() const {
|
||||
PlayerBankT<!IsBigEndian> ret;
|
||||
ret.num_items = this->num_items.load();
|
||||
ret.meseta = this->meseta.load();
|
||||
for (size_t z = 0; z < this->items.size(); z++) {
|
||||
ret.items[z] = this->items[z];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using PlayerBank = PlayerBankT<false>;
|
||||
using PlayerBankBE = PlayerBankT<true>;
|
||||
check_struct_size(PlayerBank, 0x12C8);
|
||||
check_struct_size(PlayerBankBE, 0x12C8);
|
||||
+150
-499
@@ -22,184 +22,6 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
PlayerInventoryItem::PlayerInventoryItem(const ItemData& item, bool equipped)
|
||||
: present(1),
|
||||
unknown_a1(0),
|
||||
extension_data1(0),
|
||||
extension_data2(0),
|
||||
flags(equipped ? 8 : 0),
|
||||
data(item) {}
|
||||
|
||||
uint32_t PlayerVisualConfig::compute_name_color_checksum(uint32_t name_color) {
|
||||
uint8_t x = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
uint8_t y = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
// name_color (ARGB) = ABCDEFGHabcdefghIJKLMNOPijklmnop
|
||||
// name_color_checksum = 000000000ijklmabcdeIJKLM00000000 ^ xxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyy
|
||||
uint32_t xbrgx95558 = ((name_color << 15) & 0x007C0000) | ((name_color >> 6) & 0x0003E000) | ((name_color >> 3) & 0x00001F00);
|
||||
uint32_t mask = (x << 24) | (y << 16) | (x << 8) | y;
|
||||
return xbrgx95558 ^ mask;
|
||||
}
|
||||
|
||||
void PlayerVisualConfig::compute_name_color_checksum() {
|
||||
this->name_color_checksum = this->compute_name_color_checksum(this->name_color);
|
||||
}
|
||||
|
||||
void PlayerVisualConfig::enforce_lobby_join_limits_for_version(Version v) {
|
||||
struct ClassMaxes {
|
||||
uint16_t costume;
|
||||
uint16_t skin;
|
||||
uint16_t face;
|
||||
uint16_t head;
|
||||
uint16_t hair;
|
||||
};
|
||||
static constexpr ClassMaxes v1_v2_class_maxes[14] = {
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x0007},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0009, 0x0000, 0x0005, 0x0000},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x0007},
|
||||
{0x0000, 0x0009, 0x0000, 0x0005, 0x0000},
|
||||
{0x0000, 0x0009, 0x0000, 0x0005, 0x0000},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x0007},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
};
|
||||
static constexpr ClassMaxes v3_v4_class_maxes[19] = {
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0001},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0001},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000}};
|
||||
|
||||
const ClassMaxes* maxes;
|
||||
if (v == Version::GC_NTE) {
|
||||
// GC NTE has HUcaseal, FOmar, and RAmarl, but missing others
|
||||
if (this->char_class >= 12) {
|
||||
this->char_class = 0; // Invalid classes -> HUmar
|
||||
}
|
||||
|
||||
// GC NTE is basically v2, but uses v3 maxes
|
||||
this->version = min<uint8_t>(this->version, 2);
|
||||
maxes = &v3_v4_class_maxes[this->char_class];
|
||||
|
||||
// Prevent GC NTE from crashing from extra models
|
||||
this->extra_model = 0;
|
||||
this->validation_flags &= 0xFD;
|
||||
} else if (is_v1_or_v2(v)) {
|
||||
// V1/V2 have fewer classes, so we'll substitute some here
|
||||
switch (this->char_class) {
|
||||
case 0: // HUmar
|
||||
case 1: // HUnewearl
|
||||
case 2: // HUcast
|
||||
case 3: // RAmar
|
||||
case 4: // RAcast
|
||||
case 5: // RAcaseal
|
||||
case 6: // FOmarl
|
||||
case 7: // FOnewm
|
||||
case 8: // FOnewearl
|
||||
case 12: // V3 custom 1
|
||||
case 13: // V3 custom 2
|
||||
break;
|
||||
case 9: // HUcaseal
|
||||
this->char_class = 5; // HUcaseal -> RAcaseal
|
||||
break;
|
||||
case 10: // FOmar
|
||||
this->char_class = 0; // FOmar -> HUmar
|
||||
break;
|
||||
case 11: // RAmarl
|
||||
this->char_class = 1; // RAmarl -> HUnewearl
|
||||
break;
|
||||
case 14: // V2 custom 1 / V3 custom 3
|
||||
case 15: // V2 custom 2 / V3 custom 4
|
||||
case 16: // V2 custom 3 / V3 custom 5
|
||||
case 17: // V2 custom 4 / V3 custom 6
|
||||
case 18: // V2 custom 5 / V3 custom 7
|
||||
this->char_class -= 5;
|
||||
break;
|
||||
default:
|
||||
this->char_class = 0; // Invalid classes -> HUmar
|
||||
}
|
||||
|
||||
this->version = min<uint8_t>(this->version, is_v1(v) ? 0 : 2);
|
||||
maxes = &v1_v2_class_maxes[this->char_class];
|
||||
|
||||
} else {
|
||||
if (this->char_class >= 19) {
|
||||
this->char_class = 0; // Invalid classes -> HUmar
|
||||
}
|
||||
this->version = min<uint8_t>(this->version, 3);
|
||||
maxes = &v3_v4_class_maxes[this->char_class];
|
||||
}
|
||||
|
||||
// V1/V2 has fewer costumes and android skins, so substitute them here
|
||||
this->costume = maxes->costume ? (this->costume % maxes->costume) : 0;
|
||||
this->skin = maxes->skin ? (this->skin % maxes->skin) : 0;
|
||||
this->face = maxes->face ? (this->face % maxes->face) : 0;
|
||||
this->head = maxes->head ? (this->head % maxes->head) : 0;
|
||||
this->hair = maxes->hair ? (this->hair % maxes->hair) : 0;
|
||||
|
||||
if (this->name_color == 0) {
|
||||
this->name_color = 0xFFFFFFFF;
|
||||
}
|
||||
if (is_v1_or_v2(v)) {
|
||||
this->compute_name_color_checksum();
|
||||
} else {
|
||||
this->name_color_checksum = 0;
|
||||
}
|
||||
this->class_flags = class_flags_for_class(this->char_class);
|
||||
this->name.clear_after_bytes(0x0C);
|
||||
}
|
||||
|
||||
void PlayerDispDataDCPCV3::enforce_lobby_join_limits_for_version(Version v) {
|
||||
this->visual.enforce_lobby_join_limits_for_version(v);
|
||||
}
|
||||
|
||||
void PlayerDispDataBB::enforce_lobby_join_limits_for_version(Version v) {
|
||||
this->visual.enforce_lobby_join_limits_for_version(v);
|
||||
this->name.clear_after_bytes(0x18); // 12 characters
|
||||
}
|
||||
|
||||
PlayerDispDataBB PlayerDispDataDCPCV3::to_bb(uint8_t to_language, uint8_t from_language) const {
|
||||
PlayerDispDataBB bb;
|
||||
bb.stats = this->stats;
|
||||
bb.visual = this->visual;
|
||||
bb.visual.name.encode(" 0");
|
||||
string decoded_name = this->visual.name.decode(from_language);
|
||||
bb.name.encode(decoded_name, to_language);
|
||||
bb.config = this->config;
|
||||
bb.technique_levels_v1 = this->technique_levels_v1;
|
||||
return bb;
|
||||
}
|
||||
|
||||
PlayerDispDataDCPCV3 PlayerDispDataBB::to_dcpcv3(uint8_t to_language, uint8_t from_language) const {
|
||||
PlayerDispDataDCPCV3 ret;
|
||||
ret.stats = this->stats;
|
||||
ret.visual = this->visual;
|
||||
string decoded_name = this->name.decode(from_language);
|
||||
ret.visual.name.encode(decoded_name, to_language);
|
||||
ret.config = this->config;
|
||||
ret.technique_levels_v1 = this->technique_levels_v1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PlayerDispDataBB::apply_dressing_room(const PlayerDispDataBBPreview& pre) {
|
||||
this->visual.name_color = pre.visual.name_color;
|
||||
this->visual.extra_model = pre.visual.extra_model;
|
||||
@@ -233,6 +55,106 @@ void GuildCardBB::clear() {
|
||||
this->char_class = 0;
|
||||
}
|
||||
|
||||
GuildCardDCNTE::operator GuildCardBB() const {
|
||||
GuildCardBB ret;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.name.encode(this->name.decode(this->language), this->language);
|
||||
ret.description.encode(this->description.decode(this->language), this->language);
|
||||
ret.present = this->present;
|
||||
ret.language = this->language;
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
return ret;
|
||||
}
|
||||
|
||||
GuildCardDC::operator GuildCardBB() const {
|
||||
GuildCardBB ret;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.name.encode(this->name.decode(this->language), this->language);
|
||||
ret.description.encode(this->description.decode(this->language), this->language);
|
||||
ret.present = this->present;
|
||||
ret.language = this->language;
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
return ret;
|
||||
}
|
||||
|
||||
GuildCardPC::operator GuildCardBB() const {
|
||||
GuildCardBB ret;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.name.encode(this->name.decode(this->language), this->language);
|
||||
ret.description.encode(this->description.decode(this->language), this->language);
|
||||
ret.present = this->present;
|
||||
ret.language = this->language;
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
return ret;
|
||||
}
|
||||
|
||||
GuildCardXB::operator GuildCardBB() const {
|
||||
GuildCardBB ret;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.name.encode(this->name.decode(this->language), this->language);
|
||||
ret.description.encode(this->description.decode(this->language), this->language);
|
||||
ret.present = this->present;
|
||||
ret.language = this->language;
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
return ret;
|
||||
}
|
||||
|
||||
GuildCardBB::operator GuildCardDCNTE() const {
|
||||
GuildCardDCNTE ret;
|
||||
ret.player_tag = 0x00010000;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.name.encode(this->name.decode(this->language), this->language);
|
||||
ret.description.encode(this->description.decode(this->language), this->language);
|
||||
ret.present = this->present;
|
||||
ret.language = this->language;
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
return ret;
|
||||
}
|
||||
|
||||
GuildCardBB::operator GuildCardDC() const {
|
||||
GuildCardDC ret;
|
||||
ret.player_tag = 0x00010000;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.name.encode(this->name.decode(this->language), this->language);
|
||||
ret.description.encode(this->description.decode(this->language), this->language);
|
||||
ret.present = this->present;
|
||||
ret.language = this->language;
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
return ret;
|
||||
}
|
||||
|
||||
GuildCardBB::operator GuildCardPC() const {
|
||||
GuildCardPC ret;
|
||||
ret.player_tag = 0x00010000;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.name.encode(this->name.decode(this->language), this->language);
|
||||
ret.description.encode(this->description.decode(this->language), this->language);
|
||||
ret.present = this->present;
|
||||
ret.language = this->language;
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
return ret;
|
||||
}
|
||||
|
||||
GuildCardBB::operator GuildCardXB() const {
|
||||
GuildCardXB ret;
|
||||
ret.player_tag = 0x00010000;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
ret.name.encode(this->name.decode(this->language), this->language);
|
||||
ret.description.encode(this->description.decode(this->language), this->language);
|
||||
ret.present = this->present;
|
||||
ret.language = this->language;
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PlayerLobbyDataPC::clear() {
|
||||
this->player_tag = 0;
|
||||
this->guild_card_number = 0;
|
||||
@@ -279,7 +201,7 @@ void PlayerLobbyDataBB::clear() {
|
||||
this->hide_help_prompt = 0;
|
||||
}
|
||||
|
||||
PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec)
|
||||
PlayerRecordsChallengeBB::PlayerRecordsChallengeBB(const PlayerRecordsChallengeDC& rec)
|
||||
: title_color(rec.title_color),
|
||||
unknown_u0(rec.unknown_u0),
|
||||
times_ep1_online(rec.times_ep1_online),
|
||||
@@ -303,7 +225,7 @@ PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsDC_Chall
|
||||
rank_title(rec.rank_title.decode(), 1),
|
||||
unknown_l7(0) {}
|
||||
|
||||
PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec)
|
||||
PlayerRecordsChallengeBB::PlayerRecordsChallengeBB(const PlayerRecordsChallengePC& rec)
|
||||
: title_color(rec.title_color),
|
||||
unknown_u0(rec.unknown_u0),
|
||||
times_ep1_online(rec.times_ep1_online),
|
||||
@@ -327,35 +249,8 @@ PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsPC_Chall
|
||||
rank_title(rec.rank_title.decode(), 1),
|
||||
unknown_l7(0) {}
|
||||
|
||||
PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge<false>& rec)
|
||||
: title_color(rec.stats.title_color),
|
||||
unknown_u0(rec.stats.unknown_u0),
|
||||
times_ep1_online(rec.stats.times_ep1_online),
|
||||
times_ep2_online(rec.stats.times_ep2_online),
|
||||
times_ep1_offline(rec.stats.times_ep1_offline),
|
||||
grave_is_ep2(rec.stats.grave_is_ep2),
|
||||
grave_stage_num(rec.stats.grave_stage_num),
|
||||
grave_floor(rec.stats.grave_floor),
|
||||
unknown_g0(rec.stats.unknown_g0),
|
||||
grave_deaths(rec.stats.grave_deaths),
|
||||
unknown_u4(rec.stats.unknown_u4),
|
||||
grave_time(rec.stats.grave_time),
|
||||
grave_defeated_by_enemy_rt_index(rec.stats.grave_defeated_by_enemy_rt_index),
|
||||
grave_x(rec.stats.grave_x),
|
||||
grave_y(rec.stats.grave_y),
|
||||
grave_z(rec.stats.grave_z),
|
||||
grave_team(rec.stats.grave_team.decode(), 1),
|
||||
grave_message(rec.stats.grave_message.decode(), 1),
|
||||
unknown_m5(rec.stats.unknown_m5),
|
||||
unknown_t6(rec.stats.unknown_t6),
|
||||
ep1_online_award_state(rec.stats.ep1_online_award_state),
|
||||
ep2_online_award_state(rec.stats.ep2_online_award_state),
|
||||
ep1_offline_award_state(rec.stats.ep1_offline_award_state),
|
||||
rank_title(rec.rank_title.decode(), 1),
|
||||
unknown_l7(rec.unknown_l7) {}
|
||||
|
||||
PlayerRecordsBB_Challenge::operator PlayerRecordsDC_Challenge() const {
|
||||
PlayerRecordsDC_Challenge ret;
|
||||
PlayerRecordsChallengeBB::operator PlayerRecordsChallengeDC() const {
|
||||
PlayerRecordsChallengeDC ret;
|
||||
ret.title_color = this->title_color;
|
||||
ret.unknown_u0 = this->unknown_u0;
|
||||
ret.rank_title.encode(this->rank_title.decode());
|
||||
@@ -384,8 +279,8 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsDC_Challenge() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
PlayerRecordsBB_Challenge::operator PlayerRecordsPC_Challenge() const {
|
||||
PlayerRecordsPC_Challenge ret;
|
||||
PlayerRecordsChallengeBB::operator PlayerRecordsChallengePC() const {
|
||||
PlayerRecordsChallengePC ret;
|
||||
ret.title_color = this->title_color;
|
||||
ret.unknown_u0 = this->unknown_u0;
|
||||
ret.rank_title.encode(this->rank_title.decode());
|
||||
@@ -414,289 +309,6 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsPC_Challenge() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge<false>() const {
|
||||
PlayerRecordsV3_Challenge<false> ret;
|
||||
ret.stats.title_color = this->title_color;
|
||||
ret.stats.unknown_u0 = this->unknown_u0;
|
||||
ret.stats.times_ep1_online = this->times_ep1_online;
|
||||
ret.stats.times_ep2_online = this->times_ep2_online;
|
||||
ret.stats.times_ep1_offline = this->times_ep1_offline;
|
||||
ret.stats.grave_is_ep2 = this->grave_is_ep2;
|
||||
ret.stats.grave_stage_num = this->grave_stage_num;
|
||||
ret.stats.grave_floor = this->grave_floor;
|
||||
ret.stats.unknown_g0 = this->unknown_g0;
|
||||
ret.stats.grave_deaths = this->grave_deaths;
|
||||
ret.stats.unknown_u4 = this->unknown_u4;
|
||||
ret.stats.grave_time = this->grave_time;
|
||||
ret.stats.grave_defeated_by_enemy_rt_index = this->grave_defeated_by_enemy_rt_index;
|
||||
ret.stats.grave_x = this->grave_x;
|
||||
ret.stats.grave_y = this->grave_y;
|
||||
ret.stats.grave_z = this->grave_z;
|
||||
ret.stats.grave_team.encode(this->grave_team.decode(), 1);
|
||||
ret.stats.grave_message.encode(this->grave_message.decode(), 1);
|
||||
ret.stats.unknown_m5 = this->unknown_m5;
|
||||
ret.stats.unknown_t6 = this->unknown_t6;
|
||||
ret.stats.ep1_online_award_state = this->ep1_online_award_state;
|
||||
ret.stats.ep2_online_award_state = this->ep2_online_award_state;
|
||||
ret.stats.ep1_offline_award_state = this->ep1_offline_award_state;
|
||||
ret.rank_title.encode(this->rank_title.decode(), 1);
|
||||
ret.unknown_l7 = this->unknown_l7;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PlayerBank::add_item(const ItemData& item, const ItemData::StackLimits& limits) {
|
||||
uint32_t primary_identifier = item.primary_identifier();
|
||||
|
||||
if (primary_identifier == 0x04000000) {
|
||||
this->meseta += item.data2d;
|
||||
if (this->meseta > 999999) {
|
||||
this->meseta = 999999;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
size_t combine_max = item.max_stack_size(limits);
|
||||
if (combine_max > 1) {
|
||||
size_t y;
|
||||
for (y = 0; y < this->num_items; y++) {
|
||||
if (this->items[y].data.primary_identifier() == primary_identifier) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (y < this->num_items) {
|
||||
uint8_t new_count = this->items[y].data.data1[5] + item.data1[5];
|
||||
if (new_count > combine_max) {
|
||||
throw runtime_error("stack size would exceed limit");
|
||||
}
|
||||
this->items[y].data.data1[5] = new_count;
|
||||
this->items[y].amount = new_count;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->num_items >= 200) {
|
||||
throw runtime_error("no free space in bank");
|
||||
}
|
||||
auto& last_item = this->items[this->num_items];
|
||||
last_item.data = item;
|
||||
last_item.amount = (item.max_stack_size(limits) > 1) ? item.data1[5] : 1;
|
||||
last_item.present = 1;
|
||||
this->num_items++;
|
||||
}
|
||||
|
||||
ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) {
|
||||
size_t index = this->find_item(item_id);
|
||||
auto& bank_item = this->items[index];
|
||||
|
||||
ItemData ret;
|
||||
if (amount && (bank_item.data.stack_size(limits) > 1) && (amount < bank_item.data.data1[5])) {
|
||||
ret = bank_item.data;
|
||||
ret.data1[5] = amount;
|
||||
bank_item.data.data1[5] -= amount;
|
||||
bank_item.amount -= amount;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = bank_item.data;
|
||||
this->num_items--;
|
||||
for (size_t x = index; x < this->num_items; x++) {
|
||||
this->items[x] = this->items[x + 1];
|
||||
}
|
||||
auto& last_item = this->items[this->num_items];
|
||||
last_item.amount = 0;
|
||||
last_item.present = 0;
|
||||
last_item.data.clear();
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t PlayerInventory::find_item(uint32_t item_id) const {
|
||||
for (size_t x = 0; x < this->num_items; x++) {
|
||||
if (this->items[x].data.id == item_id) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw out_of_range("item not present");
|
||||
}
|
||||
|
||||
size_t PlayerInventory::find_item_by_primary_identifier(uint32_t primary_identifier) const {
|
||||
for (size_t x = 0; x < this->num_items; x++) {
|
||||
if (this->items[x].data.primary_identifier() == primary_identifier) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw out_of_range("item not present");
|
||||
}
|
||||
|
||||
size_t PlayerInventory::find_equipped_item(EquipSlot slot) const {
|
||||
ssize_t ret = -1;
|
||||
for (size_t y = 0; y < this->num_items; y++) {
|
||||
const auto& i = this->items[y];
|
||||
if (!(i.flags & 0x00000008)) {
|
||||
continue;
|
||||
}
|
||||
if (!i.data.can_be_equipped_in_slot(slot)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Units can be equipped in multiple slots, so the currently-equipped slot
|
||||
// is stored in the item data itself.
|
||||
if (((slot == EquipSlot::UNIT_1) && (i.data.data1[4] != 0x00)) ||
|
||||
((slot == EquipSlot::UNIT_2) && (i.data.data1[4] != 0x01)) ||
|
||||
((slot == EquipSlot::UNIT_3) && (i.data.data1[4] != 0x02)) ||
|
||||
((slot == EquipSlot::UNIT_4) && (i.data.data1[4] != 0x03))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ret < 0) {
|
||||
ret = y;
|
||||
} else {
|
||||
throw runtime_error("multiple items are equipped in the same slot");
|
||||
}
|
||||
}
|
||||
if (ret < 0) {
|
||||
throw out_of_range("no item is equipped in this slot");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool PlayerInventory::has_equipped_item(EquipSlot slot) const {
|
||||
try {
|
||||
this->find_equipped_item(slot);
|
||||
return true;
|
||||
} catch (const out_of_range&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerInventory::equip_item_id(uint32_t item_id, EquipSlot slot, bool allow_overwrite) {
|
||||
this->equip_item_index(this->find_item(item_id), slot, allow_overwrite);
|
||||
}
|
||||
|
||||
void PlayerInventory::equip_item_index(size_t index, EquipSlot slot, bool allow_overwrite) {
|
||||
auto& item = this->items[index];
|
||||
|
||||
if ((slot == EquipSlot::UNKNOWN) || !item.data.can_be_equipped_in_slot(slot)) {
|
||||
slot = item.data.default_equip_slot();
|
||||
}
|
||||
|
||||
if (this->has_equipped_item(slot)) {
|
||||
if (allow_overwrite) {
|
||||
this->unequip_item_slot(slot);
|
||||
} else {
|
||||
throw runtime_error("equip slot is already in use");
|
||||
}
|
||||
}
|
||||
|
||||
item.flags |= 0x00000008;
|
||||
// Units store which slot they're equipped in within the item data itself
|
||||
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x03)) {
|
||||
item.data.data1[4] = static_cast<uint8_t>(slot) - 9;
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerInventory::unequip_item_id(uint32_t item_id) {
|
||||
this->unequip_item_index(this->find_item(item_id));
|
||||
}
|
||||
|
||||
void PlayerInventory::unequip_item_slot(EquipSlot slot) {
|
||||
this->unequip_item_index(this->find_equipped_item(slot));
|
||||
}
|
||||
|
||||
void PlayerInventory::unequip_item_index(size_t index) {
|
||||
auto& item = this->items[index];
|
||||
|
||||
item.flags &= (~0x00000008);
|
||||
// Units store which slot they're equipped in within the item data itself
|
||||
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x03)) {
|
||||
item.data.data1[4] = 0x00;
|
||||
}
|
||||
// If the item is an armor, remove all units too
|
||||
if ((item.data.data1[0] == 0x01) && (item.data.data1[1] == 0x01)) {
|
||||
for (size_t z = 0; z < 30; z++) {
|
||||
auto& unit = this->items[z];
|
||||
if ((unit.data.data1[0] == 0x01) && (unit.data.data1[1] == 0x03)) {
|
||||
unit.flags &= (~0x00000008);
|
||||
unit.data.data1[4] = 0x00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t PlayerInventory::remove_all_items_of_type(uint8_t data1_0, int16_t data1_1) {
|
||||
size_t write_offset = 0;
|
||||
for (size_t read_offset = 0; read_offset < this->num_items; read_offset++) {
|
||||
bool should_delete = ((this->items[read_offset].data.data1[0] == data1_0) &&
|
||||
((data1_1 < 0) || (this->items[read_offset].data.data1[1] == static_cast<uint8_t>(data1_1))));
|
||||
if (!should_delete) {
|
||||
if (read_offset != write_offset) {
|
||||
this->items[write_offset].present = this->items[read_offset].present;
|
||||
this->items[write_offset].unknown_a1 = this->items[read_offset].unknown_a1;
|
||||
this->items[write_offset].flags = this->items[read_offset].flags;
|
||||
this->items[write_offset].data = this->items[read_offset].data;
|
||||
}
|
||||
write_offset++;
|
||||
}
|
||||
}
|
||||
size_t ret = this->num_items - write_offset;
|
||||
this->num_items = write_offset;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PlayerInventory::decode_from_client(shared_ptr<Client> c) {
|
||||
for (size_t z = 0; z < this->items.size(); z++) {
|
||||
this->items[z].data.decode_for_version(c->version());
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerInventory::encode_for_client(shared_ptr<Client> c) {
|
||||
Version v = c->version();
|
||||
if (v == Version::DC_NTE) {
|
||||
// DC NTE has the item count as a 32-bit value here, whereas every other
|
||||
// version uses a single byte. To stop DC NTE from crashing by trying to
|
||||
// construct far more than 30 TItem objects, we clear the fields DC NTE
|
||||
// doesn't know about. Note that the 11/2000 prototype does not have this
|
||||
// issue - its inventory format matches the rest of the versions.
|
||||
this->hp_from_materials = 0;
|
||||
this->tp_from_materials = 0;
|
||||
this->language = 0;
|
||||
} else if ((v != Version::PC_NTE) && (v != Version::PC_V2)) {
|
||||
if (this->language > 4) {
|
||||
this->language = 0;
|
||||
}
|
||||
} else {
|
||||
if (this->language > 7) {
|
||||
this->language = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// For pre-V2 clients, use the V2 parameter table, since the V1 table doesn't
|
||||
// have correct encodings for backward-compatible V2 items.
|
||||
auto item_parameter_table = c->require_server_state()->item_parameter_table_for_encode(v);
|
||||
for (size_t z = 0; z < this->items.size(); z++) {
|
||||
this->items[z].data.encode_for_version(v, item_parameter_table);
|
||||
}
|
||||
}
|
||||
|
||||
size_t PlayerBank::find_item(uint32_t item_id) {
|
||||
for (size_t x = 0; x < this->num_items; x++) {
|
||||
if (this->items[x].data.id == item_id) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw 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;
|
||||
}
|
||||
}
|
||||
|
||||
QuestFlagsV1& QuestFlagsV1::operator=(const QuestFlags& other) {
|
||||
this->data[0] = other.data[0];
|
||||
this->data[1] = other.data[1];
|
||||
@@ -1087,11 +699,6 @@ const ChallengeTemplateDefinition& get_challenge_template_definition(Version ver
|
||||
}
|
||||
}
|
||||
|
||||
SymbolChat::SymbolChat()
|
||||
: spec(0),
|
||||
corner_objects(0x00FF),
|
||||
face_parts() {}
|
||||
|
||||
void RecentSwitchFlags::add(uint16_t flag_num) {
|
||||
if ((flag_num != ((this->flag_nums >> 48) & 0xFFFF)) &&
|
||||
(flag_num != ((this->flag_nums >> 32) & 0xFFFF)) &&
|
||||
@@ -1114,3 +721,47 @@ string RecentSwitchFlags::enable_commands(uint8_t floor) const {
|
||||
}
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
const QuestFlagsForDifficulty bb_quest_flag_apply_mask{{
|
||||
// clang-format off
|
||||
/* 0000 */ 0x00, 0x3F, 0xFF, 0xE3, 0xE0, 0xFF, 0xFF, 0x00,
|
||||
/* 0040 */ 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00,
|
||||
/* 0080 */ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x00,
|
||||
/* 00C0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
/* 0100 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFC, 0x00,
|
||||
/* 0140 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
/* 0180 */ 0xFE, 0x00, 0x7F, 0xFE, 0x0F, 0xFF, 0xFF, 0x80,
|
||||
/* 01C0 */ 0x3F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x0F, 0xFF,
|
||||
/* 0200 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00,
|
||||
/* 0240 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
/* 0280 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
|
||||
/* 02C0 */ 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
/* 0300 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
/* 0340 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
/* 0380 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
/* 03C0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// clang-format on
|
||||
|
||||
// The flags in the above mask are:
|
||||
// 000A 000B 000C 000D 000E 000F 0010 0011 0012 0013 0014 0015 0016 0017
|
||||
// 0018 0019 001A 001E 001F 0020 0021 0022 0028 0029 002A 002B 002C 002D
|
||||
// 002E 002F 0030 0031 0032 0033 0034 0035 0036 0037 0046 0047 0048 0049
|
||||
// 004A 004B 004C 004D 004E 004F 0050 0051 0052 0053 0054 0055 0056 0057
|
||||
// 0058 0059 005A 005B 005C 005D 005E 005F 0060 0061 0062 0063 0097 0098
|
||||
// 0099 009A 012D 012E 012F 0130 0131 0132 0133 0134 0135 0140 0141 0142
|
||||
// 0143 0144 0145 0146 0147 0148 0149 014A 014B 014C 014D 014E 014F 0150
|
||||
// 0151 0152 0153 0154 0155 0156 0157 0158 0159 015A 015B 015C 015D 015E
|
||||
// 015F 0160 0161 0162 0163 0164 0165 0166 0167 0168 0169 016A 016B 016C
|
||||
// 016D 016E 016F 0170 0171 0172 0173 0174 0175 0176 0177 0178 0179 017A
|
||||
// 017B 017C 017D 017E 017F 0180 0181 0182 0183 0184 0185 0186 0191 0192
|
||||
// 0193 0194 0195 0196 0197 0198 0199 019A 019B 019C 019D 019E 01A4 01A5
|
||||
// 01A6 01A7 01A8 01A9 01AA 01AB 01AC 01AD 01AE 01AF 01B0 01B1 01B2 01B3
|
||||
// 01B4 01B5 01B6 01B7 01B8 01C2 01C3 01C4 01C5 01C6 01C7 01C8 01C9 01CA
|
||||
// 01CB 01CC 01CD 01CE 01CF 01D0 01D1 01D2 01D3 01D4 01D5 01D6 01F4 01F5
|
||||
// 01F6 01F7 01F8 01F9 01FA 01FB 01FC 01FD 01FE 01FF 0200 0201 0202 0203
|
||||
// 0204 0205 0206 0207 0208 0209 020A 020B 020C 020D 020E 020F 0210 0211
|
||||
// 0212 0213 0214 0215 0216 0217 0218 0219 021A 021B 021C 021D 021E 021F
|
||||
// 0220 0221 0222 0223 0224 0225 0226 0227 0228 0229 022A 022B 022C 022D
|
||||
// 022E 022F 0230 0231 0232 0233 0234 0235 02BD 02BE 02BF 02C0 02C1 02C2
|
||||
// 02C3 02C4
|
||||
}};
|
||||
|
||||
+493
-207
@@ -15,106 +15,25 @@
|
||||
#include "ItemData.hh"
|
||||
#include "LevelTable.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "PlayerInventory.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
class Client;
|
||||
class ItemParameterTable;
|
||||
|
||||
// PSO V2 stored some extra data in the character structs in a format that I'm
|
||||
// sure Sega thought was very clever for backward compatibility, but for us is
|
||||
// just plain annoying. Specifically, they used the third and fourth bytes of
|
||||
// the InventoryItem struct to store some things not present in V1. The game
|
||||
// stores arrays of bytes striped across these structures. In newserv, we call
|
||||
// those fields extension_data. They contain:
|
||||
// items[0].extension_data1 through items[19].extension_data1:
|
||||
// Extended technique levels. The values in the technique_levels_v1 array
|
||||
// only go up to 14 (tech level 15); if the player has a technique above
|
||||
// level 15, the corresponding extension_data1 field holds the remaining
|
||||
// levels (so a level 20 tech would have 14 in technique_levels_v1 and 5
|
||||
// in the corresponding item's extension_data1 field).
|
||||
// items[0].extension_data2 through items[3].extension_data2:
|
||||
// The flags field from the PSOGCCharacterFile::Character struct; see
|
||||
// SaveFileFormats.hh for details.
|
||||
// items[4].extension_data2 through items[7].extension_data2:
|
||||
// The timestamp when the character was last saved, in seconds since
|
||||
// January 1, 2000. Stored little-endian, so items[4] contains the LSB.
|
||||
// items[8].extension_data2 through items[12].extension_data2:
|
||||
// Number of power materials, mind materials, evade materials, def
|
||||
// materials, and luck materials (respectively) used by the player.
|
||||
// items[13].extension_data2 through items[15].extension_data2:
|
||||
// Unknown. These are not an array, but do appear to be related.
|
||||
|
||||
struct PlayerInventoryItem {
|
||||
/* 00 */ uint8_t present = 0;
|
||||
/* 01 */ uint8_t unknown_a1 = 0;
|
||||
// See note above about these fields
|
||||
/* 02 */ uint8_t extension_data1 = 0;
|
||||
/* 03 */ uint8_t extension_data2 = 0;
|
||||
/* 04 */ le_uint32_t flags = 0; // 8 = equipped
|
||||
/* 08 */ ItemData data;
|
||||
/* 1C */
|
||||
|
||||
PlayerInventoryItem() = default;
|
||||
explicit PlayerInventoryItem(const ItemData& item, bool equipped = false);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerBankItem {
|
||||
/* 00 */ ItemData data;
|
||||
/* 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 {
|
||||
/* 0000 */ uint8_t num_items = 0;
|
||||
/* 0001 */ uint8_t hp_from_materials = 0;
|
||||
/* 0002 */ uint8_t tp_from_materials = 0;
|
||||
/* 0003 */ uint8_t language = 0;
|
||||
/* 0004 */ parray<PlayerInventoryItem, 30> items;
|
||||
/* 034C */
|
||||
|
||||
size_t find_item(uint32_t item_id) const;
|
||||
size_t find_item_by_primary_identifier(uint32_t primary_identifier) const;
|
||||
|
||||
size_t find_equipped_item(EquipSlot slot) const;
|
||||
bool has_equipped_item(EquipSlot slot) const;
|
||||
void equip_item_id(uint32_t item_id, EquipSlot slot, bool allow_overwrite);
|
||||
void equip_item_index(size_t index, EquipSlot slot, bool allow_overwrite);
|
||||
void unequip_item_id(uint32_t item_id);
|
||||
void unequip_item_slot(EquipSlot slot);
|
||||
void unequip_item_index(size_t index);
|
||||
|
||||
size_t remove_all_items_of_type(uint8_t data0, int16_t data1 = -1);
|
||||
|
||||
void decode_from_client(std::shared_ptr<Client> c);
|
||||
void encode_for_client(std::shared_ptr<Client> c);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerBank {
|
||||
/* 0000 */ le_uint32_t num_items = 0;
|
||||
/* 0004 */ le_uint32_t meseta = 0;
|
||||
/* 0008 */ parray<PlayerBankItem, 200> items;
|
||||
/* 12C8 */
|
||||
|
||||
void add_item(const ItemData& item, const ItemData::StackLimits& limits);
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits);
|
||||
size_t find_item(uint32_t item_id);
|
||||
|
||||
void sort();
|
||||
void assign_ids(uint32_t base_id);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerDispDataBB;
|
||||
|
||||
struct PlayerVisualConfig {
|
||||
template <bool IsBigEndian>
|
||||
struct PlayerVisualConfigT {
|
||||
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;
|
||||
using F32T = typename std::conditional<IsBigEndian, be_float, le_float>::type;
|
||||
|
||||
/* 00 */ pstring<TextEncoding::ASCII, 0x10> name;
|
||||
/* 10 */ parray<uint8_t, 8> unknown_a2;
|
||||
/* 18 */ le_uint32_t name_color = 0xFFFFFFFF; // ARGB
|
||||
/* 18 */ U32T 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. If the
|
||||
@@ -122,7 +41,7 @@ struct PlayerVisualConfig {
|
||||
// default color instead. This field is ignored on GC; on BB (and presumably
|
||||
// Xbox), if this has a nonzero value, the "Change Name" option appears in the
|
||||
// character selection menu.
|
||||
/* 2C */ le_uint32_t name_color_checksum = 0;
|
||||
/* 2C */ U32T name_color_checksum = 0;
|
||||
/* 30 */ uint8_t section_id = 0;
|
||||
/* 31 */ uint8_t char_class = 0;
|
||||
// validation_flags specifies that some parts of this structure are not valid
|
||||
@@ -138,35 +57,206 @@ struct PlayerVisualConfig {
|
||||
// F = force, R = ranger, H = hunter
|
||||
// A = android, N = newman, M = human
|
||||
// f = female, m = male
|
||||
/* 34 */ le_uint32_t class_flags = 0;
|
||||
/* 38 */ le_uint16_t costume = 0;
|
||||
/* 3A */ le_uint16_t skin = 0;
|
||||
/* 3C */ le_uint16_t face = 0;
|
||||
/* 3E */ le_uint16_t head = 0;
|
||||
/* 40 */ le_uint16_t hair = 0;
|
||||
/* 42 */ le_uint16_t hair_r = 0;
|
||||
/* 44 */ le_uint16_t hair_g = 0;
|
||||
/* 46 */ le_uint16_t hair_b = 0;
|
||||
/* 48 */ le_float proportion_x = 0.0;
|
||||
/* 4C */ le_float proportion_y = 0.0;
|
||||
/* 34 */ U32T class_flags = 0;
|
||||
/* 38 */ U16T costume = 0;
|
||||
/* 3A */ U16T skin = 0;
|
||||
/* 3C */ U16T face = 0;
|
||||
/* 3E */ U16T head = 0;
|
||||
/* 40 */ U16T hair = 0;
|
||||
/* 42 */ U16T hair_r = 0;
|
||||
/* 44 */ U16T hair_g = 0;
|
||||
/* 46 */ U16T hair_b = 0;
|
||||
/* 48 */ F32T proportion_x = 0.0;
|
||||
/* 4C */ F32T proportion_y = 0.0;
|
||||
/* 50 */
|
||||
|
||||
static uint32_t compute_name_color_checksum(uint32_t name_color);
|
||||
void compute_name_color_checksum();
|
||||
static uint32_t compute_name_color_checksum(uint32_t name_color) {
|
||||
uint8_t x = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
uint8_t y = (random_object<uint32_t>() % 0xFF) + 1;
|
||||
// name_color (ARGB) = ABCDEFGHabcdefghIJKLMNOPijklmnop
|
||||
// name_color_checksum = 000000000ijklmabcdeIJKLM00000000 ^ xxxxxxxxyyyyyyyyxxxxxxxxyyyyyyyy
|
||||
uint32_t xbrgx95558 = ((name_color << 15) & 0x007C0000) | ((name_color >> 6) & 0x0003E000) | ((name_color >> 3) & 0x00001F00);
|
||||
uint32_t mask = (x << 24) | (y << 16) | (x << 8) | y;
|
||||
return xbrgx95558 ^ mask;
|
||||
}
|
||||
|
||||
void enforce_lobby_join_limits_for_version(Version v);
|
||||
} __attribute__((packed));
|
||||
void compute_name_color_checksum() {
|
||||
this->name_color_checksum = this->compute_name_color_checksum(this->name_color);
|
||||
}
|
||||
|
||||
struct PlayerDispDataDCPCV3 {
|
||||
/* 00 */ PlayerStats stats;
|
||||
/* 24 */ PlayerVisualConfig visual;
|
||||
void enforce_lobby_join_limits_for_version(Version v) {
|
||||
struct ClassMaxes {
|
||||
uint16_t costume;
|
||||
uint16_t skin;
|
||||
uint16_t face;
|
||||
uint16_t head;
|
||||
uint16_t hair;
|
||||
};
|
||||
static constexpr ClassMaxes v1_v2_class_maxes[14] = {
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x0007},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0009, 0x0000, 0x0005, 0x0000},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x0007},
|
||||
{0x0000, 0x0009, 0x0000, 0x0005, 0x0000},
|
||||
{0x0000, 0x0009, 0x0000, 0x0005, 0x0000},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x0007},
|
||||
{0x0009, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
};
|
||||
static constexpr ClassMaxes v3_v4_class_maxes[19] = {
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0019, 0x0000, 0x0005, 0x0000},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0012, 0x0004, 0x0005, 0x0000, 0x000A},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0001},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0001},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000},
|
||||
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000}};
|
||||
|
||||
const ClassMaxes* maxes;
|
||||
if (v == Version::GC_NTE) {
|
||||
// GC NTE has HUcaseal, FOmar, and RAmarl, but missing others
|
||||
if (this->char_class >= 12) {
|
||||
this->char_class = 0; // Invalid classes -> HUmar
|
||||
}
|
||||
|
||||
// GC NTE is basically v2, but uses v3 maxes
|
||||
this->version = std::min<uint8_t>(this->version, 2);
|
||||
maxes = &v3_v4_class_maxes[this->char_class];
|
||||
|
||||
// Prevent GC NTE from crashing from extra models
|
||||
this->extra_model = 0;
|
||||
this->validation_flags &= 0xFD;
|
||||
} else if (is_v1_or_v2(v)) {
|
||||
// V1/V2 have fewer classes, so we'll substitute some here
|
||||
switch (this->char_class) {
|
||||
case 0: // HUmar
|
||||
case 1: // HUnewearl
|
||||
case 2: // HUcast
|
||||
case 3: // RAmar
|
||||
case 4: // RAcast
|
||||
case 5: // RAcaseal
|
||||
case 6: // FOmarl
|
||||
case 7: // FOnewm
|
||||
case 8: // FOnewearl
|
||||
case 12: // V3 custom 1
|
||||
case 13: // V3 custom 2
|
||||
break;
|
||||
case 9: // HUcaseal
|
||||
this->char_class = 5; // HUcaseal -> RAcaseal
|
||||
break;
|
||||
case 10: // FOmar
|
||||
this->char_class = 0; // FOmar -> HUmar
|
||||
break;
|
||||
case 11: // RAmarl
|
||||
this->char_class = 1; // RAmarl -> HUnewearl
|
||||
break;
|
||||
case 14: // V2 custom 1 / V3 custom 3
|
||||
case 15: // V2 custom 2 / V3 custom 4
|
||||
case 16: // V2 custom 3 / V3 custom 5
|
||||
case 17: // V2 custom 4 / V3 custom 6
|
||||
case 18: // V2 custom 5 / V3 custom 7
|
||||
this->char_class -= 5;
|
||||
break;
|
||||
default:
|
||||
this->char_class = 0; // Invalid classes -> HUmar
|
||||
}
|
||||
|
||||
this->version = std::min<uint8_t>(this->version, is_v1(v) ? 0 : 2);
|
||||
maxes = &v1_v2_class_maxes[this->char_class];
|
||||
|
||||
} else {
|
||||
if (this->char_class >= 19) {
|
||||
this->char_class = 0; // Invalid classes -> HUmar
|
||||
}
|
||||
this->version = std::min<uint8_t>(this->version, 3);
|
||||
maxes = &v3_v4_class_maxes[this->char_class];
|
||||
}
|
||||
|
||||
// V1/V2 has fewer costumes and android skins, so substitute them here
|
||||
this->costume = maxes->costume ? (this->costume % maxes->costume) : 0;
|
||||
this->skin = maxes->skin ? (this->skin % maxes->skin) : 0;
|
||||
this->face = maxes->face ? (this->face % maxes->face) : 0;
|
||||
this->head = maxes->head ? (this->head % maxes->head) : 0;
|
||||
this->hair = maxes->hair ? (this->hair % maxes->hair) : 0;
|
||||
|
||||
if (this->name_color == 0) {
|
||||
this->name_color = 0xFFFFFFFF;
|
||||
}
|
||||
if (is_v1_or_v2(v)) {
|
||||
this->compute_name_color_checksum();
|
||||
} else {
|
||||
this->name_color_checksum = 0;
|
||||
}
|
||||
this->class_flags = class_flags_for_class(this->char_class);
|
||||
this->name.clear_after_bytes(0x0C);
|
||||
}
|
||||
|
||||
operator PlayerVisualConfigT<!IsBigEndian>() const {
|
||||
PlayerVisualConfigT<!IsBigEndian> ret;
|
||||
ret.name = this->name;
|
||||
ret.unknown_a2 = this->unknown_a2;
|
||||
ret.name_color = this->name_color.load();
|
||||
ret.extra_model = this->extra_model;
|
||||
ret.unused = this->unused;
|
||||
ret.name_color_checksum = this->name_color_checksum.load();
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
ret.validation_flags = this->validation_flags;
|
||||
ret.version = this->version;
|
||||
ret.class_flags = this->class_flags.load();
|
||||
ret.costume = this->costume.load();
|
||||
ret.skin = this->skin.load();
|
||||
ret.face = this->face.load();
|
||||
ret.head = this->head.load();
|
||||
ret.hair = this->hair.load();
|
||||
ret.hair_r = this->hair_r.load();
|
||||
ret.hair_g = this->hair_g.load();
|
||||
ret.hair_b = this->hair_b.load();
|
||||
ret.proportion_x = this->proportion_x.load();
|
||||
ret.proportion_y = this->proportion_y.load();
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using PlayerVisualConfig = PlayerVisualConfigT<false>;
|
||||
using PlayerVisualConfigBE = PlayerVisualConfigT<true>;
|
||||
check_struct_size(PlayerVisualConfig, 0x50);
|
||||
check_struct_size(PlayerVisualConfigBE, 0x50);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct PlayerDispDataDCPCV3T {
|
||||
/* 00 */ PlayerStatsT<IsBigEndian> stats;
|
||||
/* 24 */ PlayerVisualConfigT<IsBigEndian> visual;
|
||||
/* 74 */ parray<uint8_t, 0x48> config;
|
||||
/* BC */ parray<uint8_t, 0x14> technique_levels_v1;
|
||||
/* D0 */
|
||||
|
||||
void enforce_lobby_join_limits_for_version(Version v);
|
||||
void enforce_lobby_join_limits_for_version(Version v) {
|
||||
this->visual.enforce_lobby_join_limits_for_version(v);
|
||||
}
|
||||
|
||||
PlayerDispDataBB to_bb(uint8_t to_language, uint8_t from_language) const;
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using PlayerDispDataDCPCV3 = PlayerDispDataDCPCV3T<false>;
|
||||
using PlayerDispDataDCPCV3BE = PlayerDispDataDCPCV3T<true>;
|
||||
check_struct_size(PlayerDispDataDCPCV3, 0xD0);
|
||||
check_struct_size(PlayerDispDataDCPCV3BE, 0xD0);
|
||||
|
||||
struct PlayerDispDataBBPreview {
|
||||
/* 00 */ le_uint32_t experience = 0;
|
||||
@@ -177,7 +267,7 @@ struct PlayerDispDataBBPreview {
|
||||
/* 58 */ pstring<TextEncoding::UTF16_ALWAYS_MARKED, 0x10> name;
|
||||
/* 78 */ uint32_t play_time_seconds = 0;
|
||||
/* 7C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PlayerDispDataBBPreview, 0x7C);
|
||||
|
||||
// BB player appearance and stats data
|
||||
struct PlayerDispDataBB {
|
||||
@@ -188,14 +278,44 @@ struct PlayerDispDataBB {
|
||||
/* 017C */ parray<uint8_t, 0x14> technique_levels_v1;
|
||||
/* 0190 */
|
||||
|
||||
void enforce_lobby_join_limits_for_version(Version v);
|
||||
PlayerDispDataDCPCV3 to_dcpcv3(uint8_t to_language, uint8_t from_language) const;
|
||||
void enforce_lobby_join_limits_for_version(Version v) {
|
||||
this->visual.enforce_lobby_join_limits_for_version(v);
|
||||
this->name.clear_after_bytes(0x18); // 12 characters
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
PlayerDispDataDCPCV3T<IsBigEndian> to_dcpcv3(uint8_t to_language, uint8_t from_language) const {
|
||||
PlayerDispDataDCPCV3T<IsBigEndian> ret;
|
||||
ret.stats = this->stats;
|
||||
ret.visual = this->visual;
|
||||
std::string decoded_name = this->name.decode(from_language);
|
||||
ret.visual.name.encode(decoded_name, to_language);
|
||||
ret.config = this->config;
|
||||
ret.technique_levels_v1 = this->technique_levels_v1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void apply_preview(const PlayerDispDataBBPreview&);
|
||||
void apply_dressing_room(const PlayerDispDataBBPreview&);
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PlayerDispDataBB, 0x190);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
PlayerDispDataBB PlayerDispDataDCPCV3T<IsBigEndian>::to_bb(uint8_t to_language, uint8_t from_language) const {
|
||||
PlayerDispDataBB bb;
|
||||
bb.stats = this->stats;
|
||||
bb.visual = this->visual;
|
||||
bb.visual.name.encode(" 0");
|
||||
std::string decoded_name = this->visual.name.decode(from_language);
|
||||
bb.name.encode(decoded_name, to_language);
|
||||
bb.config = this->config;
|
||||
bb.technique_levels_v1 = this->technique_levels_v1;
|
||||
return bb;
|
||||
}
|
||||
|
||||
struct GuildCardBB;
|
||||
|
||||
struct GuildCardDCNTE {
|
||||
/* 00 */ le_uint32_t player_tag = 0;
|
||||
/* 00 */ le_uint32_t player_tag = 0x00010000;
|
||||
/* 04 */ le_uint32_t guild_card_number = 0;
|
||||
/* 08 */ pstring<TextEncoding::ASCII, 0x18> name;
|
||||
/* 20 */ pstring<TextEncoding::MARKED, 0x48> description;
|
||||
@@ -205,10 +325,12 @@ struct GuildCardDCNTE {
|
||||
/* 79 */ uint8_t section_id = 0;
|
||||
/* 7A */ uint8_t char_class = 0;
|
||||
/* 7B */
|
||||
} __attribute__((packed));
|
||||
|
||||
operator GuildCardBB() const;
|
||||
} __packed_ws__(GuildCardDCNTE, 0x7B);
|
||||
|
||||
struct GuildCardDC {
|
||||
/* 00 */ le_uint32_t player_tag = 0;
|
||||
/* 00 */ le_uint32_t player_tag = 0x00010000;
|
||||
/* 04 */ le_uint32_t guild_card_number = 0;
|
||||
/* 08 */ pstring<TextEncoding::ASCII, 0x18> name;
|
||||
/* 20 */ pstring<TextEncoding::MARKED, 0x48> description;
|
||||
@@ -218,10 +340,12 @@ struct GuildCardDC {
|
||||
/* 7B */ uint8_t section_id = 0;
|
||||
/* 7C */ uint8_t char_class = 0;
|
||||
/* 7D */
|
||||
} __attribute__((packed));
|
||||
|
||||
operator GuildCardBB() const;
|
||||
} __packed_ws__(GuildCardDC, 0x7D);
|
||||
|
||||
struct GuildCardPC {
|
||||
/* 00 */ le_uint32_t player_tag = 0;
|
||||
/* 00 */ le_uint32_t player_tag = 0x00010000;
|
||||
/* 04 */ le_uint32_t guild_card_number = 0;
|
||||
// TODO: Is the length of the name field correct here?
|
||||
/* 08 */ pstring<TextEncoding::UTF16, 0x18> name;
|
||||
@@ -231,11 +355,16 @@ struct GuildCardPC {
|
||||
/* EE */ uint8_t section_id = 0;
|
||||
/* EF */ uint8_t char_class = 0;
|
||||
/* F0 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct GuildCardGC {
|
||||
/* 00 */ le_uint32_t player_tag = 0;
|
||||
/* 04 */ le_uint32_t guild_card_number = 0;
|
||||
operator GuildCardBB() const;
|
||||
} __packed_ws__(GuildCardPC, 0xF0);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct GuildCardGCT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
/* 00 */ U32T player_tag = 0x00010000;
|
||||
/* 04 */ U32T guild_card_number = 0;
|
||||
/* 08 */ pstring<TextEncoding::ASCII, 0x18> name;
|
||||
/* 20 */ pstring<TextEncoding::MARKED, 0x6C> description;
|
||||
/* 8C */ uint8_t present = 0;
|
||||
@@ -243,10 +372,16 @@ struct GuildCardGC {
|
||||
/* 8E */ uint8_t section_id = 0;
|
||||
/* 8F */ uint8_t char_class = 0;
|
||||
/* 90 */
|
||||
} __attribute__((packed));
|
||||
|
||||
operator GuildCardBB() const;
|
||||
} __packed__;
|
||||
using GuildCardGC = GuildCardGCT<false>;
|
||||
using GuildCardGCBE = GuildCardGCT<true>;
|
||||
check_struct_size(GuildCardGC, 0x90);
|
||||
check_struct_size(GuildCardGCBE, 0x90);
|
||||
|
||||
struct GuildCardXB {
|
||||
/* 0000 */ le_uint32_t player_tag = 0;
|
||||
/* 0000 */ le_uint32_t player_tag = 0x00010000;
|
||||
/* 0004 */ le_uint32_t guild_card_number = 0;
|
||||
/* 0008 */ le_uint32_t xb_user_id_high = 0;
|
||||
/* 000C */ le_uint32_t xb_user_id_low = 0;
|
||||
@@ -257,7 +392,9 @@ struct GuildCardXB {
|
||||
/* 022A */ uint8_t section_id = 0;
|
||||
/* 022B */ uint8_t char_class = 0;
|
||||
/* 022C */
|
||||
} __attribute__((packed));
|
||||
|
||||
operator GuildCardBB() const;
|
||||
} __packed_ws__(GuildCardXB, 0x22C);
|
||||
|
||||
struct GuildCardBB {
|
||||
/* 0000 */ le_uint32_t guild_card_number = 0;
|
||||
@@ -271,7 +408,38 @@ struct GuildCardBB {
|
||||
/* 0108 */
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
operator GuildCardDCNTE() const;
|
||||
operator GuildCardDC() const;
|
||||
operator GuildCardPC() const;
|
||||
template <bool IsBigEndian>
|
||||
operator GuildCardGCT<IsBigEndian>() const {
|
||||
GuildCardGCT<IsBigEndian> ret;
|
||||
ret.player_tag = 0x00010000;
|
||||
ret.guild_card_number = this->guild_card_number.load();
|
||||
ret.name.encode(this->name.decode(this->language), this->language);
|
||||
ret.description.encode(this->description.decode(this->language), this->language);
|
||||
ret.present = this->present;
|
||||
ret.language = this->language;
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
return ret;
|
||||
}
|
||||
operator GuildCardXB() const;
|
||||
} __packed_ws__(GuildCardBB, 0x108);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
GuildCardGCT<IsBigEndian>::operator GuildCardBB() const {
|
||||
GuildCardBB ret;
|
||||
ret.guild_card_number = this->guild_card_number.load();
|
||||
ret.name.encode(this->name.decode(this->language), this->language);
|
||||
ret.description.encode(this->description.decode(this->language), this->language);
|
||||
ret.present = this->present;
|
||||
ret.language = this->language;
|
||||
ret.section_id = this->section_id;
|
||||
ret.char_class = this->char_class;
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct PlayerLobbyDataPC {
|
||||
le_uint32_t player_tag = 0;
|
||||
@@ -286,7 +454,7 @@ struct PlayerLobbyDataPC {
|
||||
pstring<TextEncoding::UTF16, 0x10> name;
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PlayerLobbyDataPC, 0x30);
|
||||
|
||||
struct PlayerLobbyDataDCGC {
|
||||
le_uint32_t player_tag = 0;
|
||||
@@ -296,7 +464,7 @@ struct PlayerLobbyDataDCGC {
|
||||
pstring<TextEncoding::ASCII, 0x10> name;
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PlayerLobbyDataDCGC, 0x20);
|
||||
|
||||
struct XBNetworkLocation {
|
||||
/* 00 */ le_uint32_t internal_ipv4_address = 0x0A0A0A0A;
|
||||
@@ -307,21 +475,21 @@ struct XBNetworkLocation {
|
||||
/* 14 */ le_uint32_t unknown_a2;
|
||||
/* 18 */ le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
|
||||
/* 20 */ parray<le_uint32_t, 4> unknown_a3;
|
||||
/* 24 */
|
||||
/* 30 */
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(XBNetworkLocation, 0x30);
|
||||
|
||||
struct PlayerLobbyDataXB {
|
||||
/* 00 */ le_uint32_t player_tag = 0;
|
||||
/* 04 */ le_uint32_t guild_card_number = 0;
|
||||
/* 08 */ XBNetworkLocation netloc;
|
||||
/* 2C */ le_uint32_t client_id = 0;
|
||||
/* 30 */ pstring<TextEncoding::ASCII, 0x10> name;
|
||||
/* 40 */
|
||||
/* 38 */ le_uint32_t client_id = 0;
|
||||
/* 3C */ pstring<TextEncoding::ASCII, 0x10> name;
|
||||
/* 4C */
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PlayerLobbyDataXB, 0x4C);
|
||||
|
||||
struct PlayerLobbyDataBB {
|
||||
/* 00 */ le_uint32_t player_tag = 0;
|
||||
@@ -334,25 +502,36 @@ struct PlayerLobbyDataBB {
|
||||
// If this field is zero, the "Press F1 for help" prompt appears in the corner
|
||||
// of the screen in the lobby and on Pioneer 2.
|
||||
/* 40 */ le_uint32_t hide_help_prompt = 1;
|
||||
/* 44 */
|
||||
/* 44 */
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PlayerLobbyDataBB, 0x44);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct ChallengeAwardState {
|
||||
struct ChallengeAwardStateT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
U32T rank_award_flags = 0;
|
||||
ChallengeTime<IsBigEndian> maximum_rank;
|
||||
} __attribute__((packed));
|
||||
ChallengeTimeT<IsBigEndian> maximum_rank;
|
||||
|
||||
operator ChallengeAwardStateT<!IsBigEndian>() const {
|
||||
ChallengeAwardStateT<!IsBigEndian> ret;
|
||||
ret.rank_award_flags = this->rank_award_flags.load();
|
||||
ret.maximum_rank = this->maximum_rank;
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using ChallengeAwardState = ChallengeAwardStateT<false>;
|
||||
using ChallengeAwardStateBE = ChallengeAwardStateT<true>;
|
||||
check_struct_size(ChallengeAwardState, 8);
|
||||
check_struct_size(ChallengeAwardStateBE, 8);
|
||||
|
||||
template <TextEncoding UnencryptedEncoding, TextEncoding EncryptedEncoding>
|
||||
struct PlayerRecordsDCPC_Challenge {
|
||||
struct PlayerRecordsChallengeDCPCT {
|
||||
/* DC:PC */
|
||||
/* 00:00 */ le_uint16_t title_color = 0x7FFF;
|
||||
/* 02:02 */ parray<uint8_t, 2> unknown_u0;
|
||||
/* 04:04 */ pstring<EncryptedEncoding, 0x0C> rank_title;
|
||||
/* 10:1C */ parray<ChallengeTime<false>, 9> times_ep1_online; // TODO: This might be offline times
|
||||
/* 10:1C */ parray<ChallengeTimeT<false>, 9> times_ep1_online; // TODO: This might be offline times
|
||||
/* 34:40 */ uint8_t grave_stage_num = 0;
|
||||
/* 35:41 */ uint8_t grave_floor = 0;
|
||||
/* 36:42 */ le_uint16_t grave_deaths = 0;
|
||||
@@ -370,51 +549,49 @@ struct PlayerRecordsDCPC_Challenge {
|
||||
/* 48:54 */ le_float grave_z = 0.0f;
|
||||
/* 4C:58 */ pstring<UnencryptedEncoding, 0x14> grave_team;
|
||||
/* 60:80 */ pstring<UnencryptedEncoding, 0x18> grave_message;
|
||||
/* 78:B0 */ parray<ChallengeTime<false>, 9> times_ep1_offline; // TODO: This might be online times
|
||||
/* 78:B0 */ parray<ChallengeTimeT<false>, 9> times_ep1_offline; // TODO: This might be online times
|
||||
/* 9C:D4 */ parray<uint8_t, 4> unknown_l4;
|
||||
/* A0:D8 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerRecordsDC_Challenge : PlayerRecordsDCPC_Challenge<TextEncoding::ASCII, TextEncoding::CHALLENGE8> {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerRecordsPC_Challenge : PlayerRecordsDCPC_Challenge<TextEncoding::UTF16, TextEncoding::CHALLENGE16> {
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using PlayerRecordsChallengeDC = PlayerRecordsChallengeDCPCT<TextEncoding::ASCII, TextEncoding::CHALLENGE8>;
|
||||
using PlayerRecordsChallengePC = PlayerRecordsChallengeDCPCT<TextEncoding::UTF16, TextEncoding::CHALLENGE16>;
|
||||
check_struct_size(PlayerRecordsChallengeDC, 0xA0);
|
||||
check_struct_size(PlayerRecordsChallengePC, 0xD8);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct PlayerRecordsV3_Challenge {
|
||||
struct PlayerRecordsChallengeV3T {
|
||||
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;
|
||||
using FloatT = typename std::conditional<IsBigEndian, be_float, le_float>::type;
|
||||
using F32T = typename std::conditional<IsBigEndian, be_float, le_float>::type;
|
||||
|
||||
// Offsets are (1) relative to start of C5 entry, and (2) relative to start
|
||||
// of save file structure
|
||||
struct Stats {
|
||||
/* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555
|
||||
/* 02:1E */ parray<uint8_t, 2> unknown_u0;
|
||||
/* 04:20 */ parray<ChallengeTime<IsBigEndian>, 9> times_ep1_online;
|
||||
/* 28:44 */ parray<ChallengeTime<IsBigEndian>, 5> times_ep2_online;
|
||||
/* 3C:58 */ parray<ChallengeTime<IsBigEndian>, 9> times_ep1_offline;
|
||||
/* 04:20 */ parray<ChallengeTimeT<IsBigEndian>, 9> times_ep1_online;
|
||||
/* 28:44 */ parray<ChallengeTimeT<IsBigEndian>, 5> times_ep2_online;
|
||||
/* 3C:58 */ parray<ChallengeTimeT<IsBigEndian>, 9> times_ep1_offline;
|
||||
/* 60:7C */ uint8_t grave_is_ep2 = 0;
|
||||
/* 61:7D */ uint8_t grave_stage_num = 0;
|
||||
/* 62:7E */ uint8_t grave_floor = 0;
|
||||
/* 63:7F */ uint8_t unknown_g0 = 0;
|
||||
/* 64:80 */ U16T grave_deaths = 0;
|
||||
/* 66:82 */ parray<uint8_t, 2> unknown_u4;
|
||||
/* 68:84 */ U32T grave_time = 0; // Encoded as in PlayerRecordsDCPC_Challenge
|
||||
/* 68:84 */ U32T grave_time = 0; // Encoded as in PlayerRecordsChallengeDCPC
|
||||
/* 6C:88 */ U32T grave_defeated_by_enemy_rt_index = 0;
|
||||
/* 70:8C */ FloatT grave_x = 0.0f;
|
||||
/* 74:90 */ FloatT grave_y = 0.0f;
|
||||
/* 78:94 */ FloatT grave_z = 0.0f;
|
||||
/* 70:8C */ F32T grave_x = 0.0f;
|
||||
/* 74:90 */ F32T grave_y = 0.0f;
|
||||
/* 78:94 */ F32T grave_z = 0.0f;
|
||||
/* 7C:98 */ pstring<TextEncoding::ASCII, 0x14> grave_team;
|
||||
/* 90:AC */ pstring<TextEncoding::ASCII, 0x20> grave_message;
|
||||
/* B0:CC */ parray<uint8_t, 4> unknown_m5;
|
||||
/* B4:D0 */ parray<U32T, 3> unknown_t6;
|
||||
/* C0:DC */ ChallengeAwardState<IsBigEndian> ep1_online_award_state;
|
||||
/* C8:E4 */ ChallengeAwardState<IsBigEndian> ep2_online_award_state;
|
||||
/* D0:EC */ ChallengeAwardState<IsBigEndian> ep1_offline_award_state;
|
||||
/* C0:DC */ ChallengeAwardStateT<IsBigEndian> ep1_online_award_state;
|
||||
/* C8:E4 */ ChallengeAwardStateT<IsBigEndian> ep2_online_award_state;
|
||||
/* D0:EC */ ChallengeAwardStateT<IsBigEndian> ep1_offline_award_state;
|
||||
/* D8:F4 */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
/* 0000:001C */ Stats stats;
|
||||
// On Episode 3, there are special cases that apply to this field - if the
|
||||
// text ends with certain strings, the player will have particle effects
|
||||
@@ -426,21 +603,25 @@ struct PlayerRecordsV3_Challenge {
|
||||
/* 00D8:00F4 */ pstring<TextEncoding::CHALLENGE8, 0x0C> rank_title;
|
||||
/* 00E4:0100 */ parray<uint8_t, 0x1C> unknown_l7;
|
||||
/* 0100:011C */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using PlayerRecordsChallengeV3 = PlayerRecordsChallengeV3T<false>;
|
||||
using PlayerRecordsChallengeV3BE = PlayerRecordsChallengeV3T<true>;
|
||||
check_struct_size(PlayerRecordsChallengeV3, 0x100);
|
||||
check_struct_size(PlayerRecordsChallengeV3BE, 0x100);
|
||||
|
||||
struct PlayerRecordsBB_Challenge {
|
||||
struct PlayerRecordsChallengeBB {
|
||||
/* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555
|
||||
/* 0002 */ parray<uint8_t, 2> unknown_u0;
|
||||
/* 0004 */ parray<ChallengeTime<false>, 9> times_ep1_online;
|
||||
/* 0028 */ parray<ChallengeTime<false>, 5> times_ep2_online;
|
||||
/* 003C */ parray<ChallengeTime<false>, 9> times_ep1_offline;
|
||||
/* 0004 */ parray<ChallengeTimeT<false>, 9> times_ep1_online;
|
||||
/* 0028 */ parray<ChallengeTimeT<false>, 5> times_ep2_online;
|
||||
/* 003C */ parray<ChallengeTimeT<false>, 9> times_ep1_offline;
|
||||
/* 0060 */ uint8_t grave_is_ep2 = 0;
|
||||
/* 0061 */ uint8_t grave_stage_num = 0;
|
||||
/* 0062 */ uint8_t grave_floor = 0;
|
||||
/* 0063 */ uint8_t unknown_g0 = 0;
|
||||
/* 0064 */ le_uint16_t grave_deaths = 0;
|
||||
/* 0066 */ parray<uint8_t, 2> unknown_u4;
|
||||
/* 0068 */ le_uint32_t grave_time = 0; // Encoded as in PlayerRecordsDCPC_Challenge
|
||||
/* 0068 */ le_uint32_t grave_time = 0; // Encoded as in PlayerRecordsChallengeDCPC
|
||||
/* 006C */ le_uint32_t grave_defeated_by_enemy_rt_index = 0;
|
||||
/* 0070 */ le_float grave_x = 0.0f;
|
||||
/* 0074 */ le_float grave_y = 0.0f;
|
||||
@@ -449,36 +630,118 @@ struct PlayerRecordsBB_Challenge {
|
||||
/* 00A4 */ pstring<TextEncoding::UTF16, 0x20> grave_message;
|
||||
/* 00E4 */ parray<uint8_t, 4> unknown_m5;
|
||||
/* 00E8 */ parray<le_uint32_t, 3> unknown_t6;
|
||||
/* 00F4 */ ChallengeAwardState<false> ep1_online_award_state;
|
||||
/* 00FC */ ChallengeAwardState<false> ep2_online_award_state;
|
||||
/* 0104 */ ChallengeAwardState<false> ep1_offline_award_state;
|
||||
/* 00F4 */ ChallengeAwardStateT<false> ep1_online_award_state;
|
||||
/* 00FC */ ChallengeAwardStateT<false> ep2_online_award_state;
|
||||
/* 0104 */ ChallengeAwardStateT<false> ep1_offline_award_state;
|
||||
/* 010C */ pstring<TextEncoding::CHALLENGE16, 0x0C> rank_title;
|
||||
/* 0124 */ parray<uint8_t, 0x1C> unknown_l7;
|
||||
/* 0140 */
|
||||
|
||||
PlayerRecordsBB_Challenge() = default;
|
||||
PlayerRecordsBB_Challenge(const PlayerRecordsBB_Challenge& other) = default;
|
||||
PlayerRecordsBB_Challenge& operator=(const PlayerRecordsBB_Challenge& other) = default;
|
||||
PlayerRecordsChallengeBB() = default;
|
||||
PlayerRecordsChallengeBB(const PlayerRecordsChallengeBB& other) = default;
|
||||
PlayerRecordsChallengeBB& operator=(const PlayerRecordsChallengeBB& other) = default;
|
||||
|
||||
PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec);
|
||||
PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec);
|
||||
PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge<false>& rec);
|
||||
PlayerRecordsChallengeBB(const PlayerRecordsChallengeDC& rec);
|
||||
PlayerRecordsChallengeBB(const PlayerRecordsChallengePC& rec);
|
||||
|
||||
operator PlayerRecordsDC_Challenge() const;
|
||||
operator PlayerRecordsPC_Challenge() const;
|
||||
operator PlayerRecordsV3_Challenge<false>() const;
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian>
|
||||
PlayerRecordsChallengeBB(const PlayerRecordsChallengeV3T<IsBigEndian>& rec)
|
||||
: title_color(rec.stats.title_color.load()),
|
||||
unknown_u0(rec.stats.unknown_u0),
|
||||
times_ep1_online(rec.stats.times_ep1_online),
|
||||
times_ep2_online(rec.stats.times_ep2_online),
|
||||
times_ep1_offline(rec.stats.times_ep1_offline),
|
||||
grave_is_ep2(rec.stats.grave_is_ep2),
|
||||
grave_stage_num(rec.stats.grave_stage_num),
|
||||
grave_floor(rec.stats.grave_floor),
|
||||
unknown_g0(rec.stats.unknown_g0),
|
||||
grave_deaths(rec.stats.grave_deaths.load()),
|
||||
unknown_u4(rec.stats.unknown_u4),
|
||||
grave_time(rec.stats.grave_time.load()),
|
||||
grave_defeated_by_enemy_rt_index(rec.stats.grave_defeated_by_enemy_rt_index.load()),
|
||||
grave_x(rec.stats.grave_x.load()),
|
||||
grave_y(rec.stats.grave_y.load()),
|
||||
grave_z(rec.stats.grave_z.load()),
|
||||
grave_team(rec.stats.grave_team.decode(), 1),
|
||||
grave_message(rec.stats.grave_message.decode(), 1),
|
||||
unknown_m5(rec.stats.unknown_m5),
|
||||
ep1_online_award_state(rec.stats.ep1_online_award_state),
|
||||
ep2_online_award_state(rec.stats.ep2_online_award_state),
|
||||
ep1_offline_award_state(rec.stats.ep1_offline_award_state),
|
||||
rank_title(rec.rank_title.decode(), 1),
|
||||
unknown_l7(rec.unknown_l7) {
|
||||
for (size_t z = 0; z < std::min<size_t>(this->unknown_t6.size(), rec.stats.unknown_t6.size()); z++) {
|
||||
this->unknown_t6[z] = rec.stats.unknown_t6[z].load();
|
||||
}
|
||||
}
|
||||
|
||||
operator PlayerRecordsChallengeDC() const;
|
||||
operator PlayerRecordsChallengePC() const;
|
||||
template <bool IsBigEndian>
|
||||
operator PlayerRecordsChallengeV3T<IsBigEndian>() const {
|
||||
PlayerRecordsChallengeV3T<IsBigEndian> ret;
|
||||
ret.stats.title_color = this->title_color.load();
|
||||
ret.stats.unknown_u0 = this->unknown_u0;
|
||||
ret.stats.times_ep1_online = this->times_ep1_online;
|
||||
ret.stats.times_ep2_online = this->times_ep2_online;
|
||||
ret.stats.times_ep1_offline = this->times_ep1_offline;
|
||||
ret.stats.grave_is_ep2 = this->grave_is_ep2;
|
||||
ret.stats.grave_stage_num = this->grave_stage_num;
|
||||
ret.stats.grave_floor = this->grave_floor;
|
||||
ret.stats.unknown_g0 = this->unknown_g0;
|
||||
ret.stats.grave_deaths = this->grave_deaths.load();
|
||||
ret.stats.unknown_u4 = this->unknown_u4;
|
||||
ret.stats.grave_time = this->grave_time.load();
|
||||
ret.stats.grave_defeated_by_enemy_rt_index = this->grave_defeated_by_enemy_rt_index.load();
|
||||
ret.stats.grave_x = this->grave_x.load();
|
||||
ret.stats.grave_y = this->grave_y.load();
|
||||
ret.stats.grave_z = this->grave_z.load();
|
||||
ret.stats.grave_team.encode(this->grave_team.decode(), 1);
|
||||
ret.stats.grave_message.encode(this->grave_message.decode(), 1);
|
||||
ret.stats.unknown_m5 = this->unknown_m5;
|
||||
for (size_t z = 0; z < std::min<size_t>(ret.stats.unknown_t6.size(), this->unknown_t6.size()); z++) {
|
||||
ret.stats.unknown_t6[z] = this->unknown_t6[z].load();
|
||||
}
|
||||
ret.stats.ep1_online_award_state = this->ep1_online_award_state;
|
||||
ret.stats.ep2_online_award_state = this->ep2_online_award_state;
|
||||
ret.stats.ep1_offline_award_state = this->ep1_offline_award_state;
|
||||
ret.rank_title.encode(this->rank_title.decode(), 1);
|
||||
ret.unknown_l7 = this->unknown_l7;
|
||||
return ret;
|
||||
}
|
||||
} __packed_ws__(PlayerRecordsChallengeBB, 0x140);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct PlayerRecords_Battle {
|
||||
struct PlayerRecordsBattleT {
|
||||
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;
|
||||
|
||||
// On Episode 3, place_counts[0] is win count and [1] is loss count
|
||||
/* 00 */ parray<U16T, 4> place_counts;
|
||||
/* 08 */ U16T disconnect_count = 0;
|
||||
/* 0A */ parray<uint16_t, 3> unknown_a1;
|
||||
/* 10 */ parray<uint32_t, 2> unknown_a2;
|
||||
/* 0A */ parray<U16T, 3> unknown_a1;
|
||||
/* 10 */ parray<U32T, 2> unknown_a2;
|
||||
/* 18 */
|
||||
} __attribute__((packed));
|
||||
|
||||
operator PlayerRecordsBattleT<!IsBigEndian>() const {
|
||||
PlayerRecordsBattleT<!IsBigEndian> ret;
|
||||
for (size_t z = 0; z < this->place_counts.size(); z++) {
|
||||
ret.place_counts[z] = this->place_counts[z].load();
|
||||
}
|
||||
ret.disconnect_count = this->disconnect_count.load();
|
||||
for (size_t z = 0; z < this->unknown_a1.size(); z++) {
|
||||
ret.unknown_a1[z] = this->unknown_a1[z].load();
|
||||
}
|
||||
for (size_t z = 0; z < this->unknown_a2.size(); z++) {
|
||||
ret.unknown_a2[z] = this->unknown_a2[z].load();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using PlayerRecordsBattle = PlayerRecordsBattleT<false>;
|
||||
using PlayerRecordsBattleBE = PlayerRecordsBattleT<true>;
|
||||
check_struct_size(PlayerRecordsBattle, 0x18);
|
||||
check_struct_size(PlayerRecordsBattleBE, 0x18);
|
||||
|
||||
template <typename DestT, typename SrcT = DestT>
|
||||
DestT convert_player_disp_data(const SrcT&, uint8_t, uint8_t) {
|
||||
@@ -494,7 +757,7 @@ inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3>(const
|
||||
template <>
|
||||
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(
|
||||
const PlayerDispDataBB& src, uint8_t to_language, uint8_t from_language) {
|
||||
return src.to_dcpcv3(to_language, from_language);
|
||||
return src.to_dcpcv3<false>(to_language, from_language);
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -534,7 +797,7 @@ struct QuestFlagsForDifficulty {
|
||||
this->data.clear(0x00);
|
||||
}
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(QuestFlagsForDifficulty, 0x80);
|
||||
|
||||
struct QuestFlags {
|
||||
parray<QuestFlagsForDifficulty, 4> data;
|
||||
@@ -556,14 +819,14 @@ struct QuestFlags {
|
||||
this->update_all(z, set);
|
||||
}
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(QuestFlags, 0x200);
|
||||
|
||||
struct QuestFlagsV1 {
|
||||
parray<QuestFlagsForDifficulty, 3> data;
|
||||
|
||||
QuestFlagsV1& operator=(const QuestFlags& other);
|
||||
operator QuestFlags() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(QuestFlagsV1, 0x180);
|
||||
|
||||
struct SwitchFlags {
|
||||
parray<parray<uint8_t, 0x20>, 0x12> data;
|
||||
@@ -577,7 +840,7 @@ struct SwitchFlags {
|
||||
inline void clear(uint8_t floor, uint16_t flag_num) {
|
||||
this->data[floor][flag_num >> 3] &= ~(0x80 >> (flag_num & 7));
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(SwitchFlags, 0x240);
|
||||
|
||||
struct BattleRules {
|
||||
enum class TechDiskMode : uint8_t {
|
||||
@@ -696,7 +959,7 @@ struct BattleRules {
|
||||
|
||||
bool operator==(const BattleRules& other) const = default;
|
||||
bool operator!=(const BattleRules& other) const = default;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(BattleRules, 0x30);
|
||||
|
||||
struct ChallengeTemplateDefinition {
|
||||
uint32_t level;
|
||||
@@ -710,29 +973,50 @@ struct ChallengeTemplateDefinition {
|
||||
|
||||
const ChallengeTemplateDefinition& get_challenge_template_definition(Version version, uint32_t class_flags, size_t index);
|
||||
|
||||
struct SymbolChat {
|
||||
struct SymbolChatFacePart {
|
||||
uint8_t type = 0xFF; // FF = no part in this slot
|
||||
uint8_t x = 0;
|
||||
uint8_t y = 0;
|
||||
// Bits: ------VH (V = reverse vertical, H = reverse horizontal)
|
||||
uint8_t flags = 0;
|
||||
} __packed_ws__(SymbolChatFacePart, 4);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct SymbolChatT {
|
||||
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;
|
||||
|
||||
// Bits: ----------------------DMSSSCCCFF
|
||||
// S = sound, C = face color, F = face shape, D = capture, M = mute sound
|
||||
/* 00 */ le_uint32_t spec = 0;
|
||||
/* 00 */ U32T spec = 0;
|
||||
|
||||
// Corner objects are specified in reading order ([0] is the top-left one).
|
||||
// Bits (each entry): ---VHCCCZZZZZZZZ
|
||||
// V = reverse vertical, H = reverse horizontal, C = color, Z = object
|
||||
// If Z is all 1 bits (0xFF), no corner object is rendered.
|
||||
/* 04 */ parray<le_uint16_t, 4> corner_objects;
|
||||
|
||||
struct FacePart {
|
||||
uint8_t type = 0xFF; // FF = no part in this slot
|
||||
uint8_t x = 0;
|
||||
uint8_t y = 0;
|
||||
// Bits: ------VH (V = reverse vertical, H = reverse horizontal)
|
||||
uint8_t flags = 0;
|
||||
} __attribute__((packed));
|
||||
/* 0C */ parray<FacePart, 12> face_parts;
|
||||
/* 04 */ parray<U16T, 4> corner_objects;
|
||||
/* 0C */ parray<SymbolChatFacePart, 12> face_parts;
|
||||
/* 3C */
|
||||
|
||||
SymbolChat();
|
||||
} __attribute__((packed));
|
||||
SymbolChatT()
|
||||
: spec(0),
|
||||
corner_objects(0x00FF),
|
||||
face_parts() {}
|
||||
|
||||
operator SymbolChatT<!IsBigEndian>() const {
|
||||
SymbolChatT<!IsBigEndian> ret;
|
||||
ret.spec = this->spec.load();
|
||||
for (size_t z = 0; z < this->corner_objects.size(); z++) {
|
||||
ret.corner_objects[z] = this->corner_objects[z].load();
|
||||
}
|
||||
ret.face_parts = this->face_parts;
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using SymbolChat = SymbolChatT<false>;
|
||||
using SymbolChatBE = SymbolChatT<true>;
|
||||
check_struct_size(SymbolChat, 0x3C);
|
||||
check_struct_size(SymbolChatBE, 0x3C);
|
||||
|
||||
struct RecentSwitchFlags {
|
||||
uint64_t flag_nums = 0xFFFFFFFFFFFFFFFF;
|
||||
@@ -745,3 +1029,5 @@ struct RecentSwitchFlags {
|
||||
|
||||
std::string enable_commands(uint8_t floor) const;
|
||||
};
|
||||
|
||||
extern const QuestFlagsForDifficulty bb_quest_flag_apply_mask;
|
||||
|
||||
+104
-70
@@ -24,6 +24,7 @@
|
||||
#include <phosg/Time.hh>
|
||||
#ifdef HAVE_RESOURCE_FILE
|
||||
#include <resource_file/Emulators/PPC32Emulator.hh>
|
||||
#include <resource_file/Emulators/SH4Emulator.hh>
|
||||
#include <resource_file/Emulators/X86Emulator.hh>
|
||||
#endif
|
||||
|
||||
@@ -136,13 +137,13 @@ static HandlerResult S_97(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
}
|
||||
|
||||
static HandlerResult C_G_9E(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string&) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN)) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN) && ses->login) {
|
||||
le_uint64_t checksum = random_object<uint64_t>() & 0x0000FFFFFFFFFFFF;
|
||||
ses->server_channel.send(0x96, 0x00, &checksum, sizeof(checksum));
|
||||
|
||||
S_UpdateClientConfig_V3_04 cmd;
|
||||
cmd.player_tag = 0x00010000;
|
||||
cmd.guild_card_number = ses->license->serial_number;
|
||||
cmd.guild_card_number = ses->login->account->account_id;
|
||||
cmd.client_config.clear(0xFF);
|
||||
ses->client_channel.send(0x04, 0x00, &cmd, sizeof(cmd));
|
||||
|
||||
@@ -154,7 +155,7 @@ static HandlerResult C_G_9E(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t
|
||||
}
|
||||
|
||||
static HandlerResult S_G_9A(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string&) {
|
||||
if (!ses->license || ses->config.check_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN)) {
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN) || !ses->login || !ses->login->gc_license) {
|
||||
return HandlerResult::Type::FORWARD;
|
||||
}
|
||||
|
||||
@@ -171,8 +172,8 @@ static HandlerResult S_G_9A(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t
|
||||
cmd.sub_version = ses->sub_version;
|
||||
cmd.is_extended = (ses->remote_guild_card_number < 0) ? 1 : 0;
|
||||
cmd.language = ses->language();
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
|
||||
cmd.access_key.encode(ses->license->access_key);
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.access_key.encode(ses->login->gc_license->access_key);
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) {
|
||||
@@ -204,8 +205,8 @@ static HandlerResult S_V123P_02_17(
|
||||
// after_message than newserv does, so don't require it
|
||||
const auto& cmd = check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>(data, 0xFFFF);
|
||||
|
||||
if (!ses->license) {
|
||||
ses->log.info("No license in linked session");
|
||||
if (!ses->login) {
|
||||
ses->log.info("Linked session is not logged in");
|
||||
|
||||
// We have to forward the command BEFORE setting up encryption, so the
|
||||
// client will be able to understand what we sent.
|
||||
@@ -226,7 +227,7 @@ static HandlerResult S_V123P_02_17(
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
|
||||
ses->log.info("Existing license in linked session");
|
||||
ses->log.info("Linked session is logged in");
|
||||
|
||||
// This isn't forwarded to the client, so don't recreate the client's crypts
|
||||
if (uses_v3_encryption(ses->version())) {
|
||||
@@ -253,10 +254,13 @@ static HandlerResult S_V123P_02_17(
|
||||
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
case Version::DC_V1:
|
||||
if (!ses->login->dc_license) {
|
||||
throw runtime_error("DC license missing from login");
|
||||
}
|
||||
if (command == 0x17) {
|
||||
C_LoginV1_DC_PC_V3_90 cmd;
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
|
||||
cmd.access_key.encode(ses->license->access_key);
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.access_key.encode(ses->login->dc_license->access_key);
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
ses->server_channel.send(0x90, 0x00, &cmd, sizeof(cmd));
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
@@ -274,8 +278,8 @@ static HandlerResult S_V123P_02_17(
|
||||
cmd.sub_version = ses->sub_version;
|
||||
cmd.is_extended = 0;
|
||||
cmd.language = ses->language();
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
|
||||
cmd.access_key.encode(ses->license->access_key);
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.access_key.encode(ses->login->dc_license->access_key);
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
cmd.hardware_id.encode(ses->hardware_id);
|
||||
cmd.name.encode(ses->character_name);
|
||||
@@ -288,6 +292,18 @@ static HandlerResult S_V123P_02_17(
|
||||
case Version::PC_NTE:
|
||||
case Version::PC_V2:
|
||||
case Version::GC_NTE:
|
||||
const string* access_key;
|
||||
if (ses->version() == Version::DC_V2) {
|
||||
access_key = ses->login->dc_license ? &ses->login->dc_license->access_key : nullptr;
|
||||
} else if (ses->version() != Version::GC_NTE) {
|
||||
access_key = ses->login->pc_license ? &ses->login->pc_license->access_key : nullptr;
|
||||
} else {
|
||||
access_key = ses->login->gc_license ? &ses->login->gc_license->access_key : nullptr;
|
||||
}
|
||||
if (!access_key) {
|
||||
throw runtime_error("incorrect login type");
|
||||
}
|
||||
|
||||
if (command == 0x17) {
|
||||
C_Login_DC_PC_V3_9A cmd;
|
||||
if (ses->remote_guild_card_number < 0) {
|
||||
@@ -298,8 +314,8 @@ static HandlerResult S_V123P_02_17(
|
||||
cmd.guild_card_number = ses->remote_guild_card_number;
|
||||
}
|
||||
cmd.sub_version = ses->sub_version;
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
|
||||
cmd.access_key.encode(ses->license->access_key);
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.access_key.encode(*access_key);
|
||||
if (ses->version() != Version::GC_NTE) {
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
}
|
||||
@@ -307,7 +323,7 @@ static HandlerResult S_V123P_02_17(
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
// TODO: We probably should set email_address, but we currently don't
|
||||
// keep that value anywhere in the session object, nor is it saved in
|
||||
// the License object.
|
||||
// the Account object.
|
||||
ses->server_channel.send(0x9A, 0x00, &cmd, sizeof(cmd));
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
} else {
|
||||
@@ -324,8 +340,8 @@ static HandlerResult S_V123P_02_17(
|
||||
cmd.sub_version = ses->sub_version;
|
||||
cmd.is_extended = 0;
|
||||
cmd.language = ses->language();
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
|
||||
cmd.access_key.encode(ses->license->access_key);
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.access_key.encode(*access_key);
|
||||
if (ses->version() != Version::GC_NTE) {
|
||||
cmd.access_key.clear_after_bytes(8);
|
||||
}
|
||||
@@ -344,14 +360,17 @@ static HandlerResult S_V123P_02_17(
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
if (!ses->login->gc_license) {
|
||||
throw runtime_error("GC license missing from login");
|
||||
}
|
||||
if (command == 0x17) {
|
||||
C_VerifyLicense_V3_DB cmd;
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->license->serial_number));
|
||||
cmd.access_key.encode(ses->license->access_key);
|
||||
C_VerifyAccount_V3_DB cmd;
|
||||
cmd.serial_number.encode(string_printf("%08" PRIX32 "", ses->login->account->account_id));
|
||||
cmd.access_key.encode(ses->login->gc_license->access_key);
|
||||
cmd.sub_version = ses->sub_version;
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
cmd.password.encode(ses->license->gc_password);
|
||||
cmd.password.encode(ses->login->gc_license->password);
|
||||
ses->server_channel.send(0xDB, 0x00, &cmd, sizeof(cmd));
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
|
||||
@@ -400,6 +419,9 @@ static HandlerResult S_V123P_02_17(
|
||||
throw logic_error("GC init command not handled");
|
||||
|
||||
case Version::XB_V3: {
|
||||
if (!ses->login->xb_license) {
|
||||
throw runtime_error("XB license missing from login");
|
||||
}
|
||||
C_LoginExtended_XB_9E cmd;
|
||||
if (ses->remote_guild_card_number < 0) {
|
||||
cmd.player_tag = 0xFFFF0000;
|
||||
@@ -413,8 +435,8 @@ static HandlerResult S_V123P_02_17(
|
||||
cmd.sub_version = ses->sub_version;
|
||||
cmd.is_extended = (ses->remote_guild_card_number < 0) ? 1 : 0;
|
||||
cmd.language = ses->language();
|
||||
cmd.serial_number.encode(ses->license->xb_gamertag);
|
||||
cmd.access_key.encode(string_printf("%016" PRIX64, ses->license->xb_user_id));
|
||||
cmd.serial_number.encode(ses->login->xb_license->gamertag);
|
||||
cmd.access_key.encode(string_printf("%016" PRIX64, ses->login->xb_license->user_id));
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
if (ses->config.check_flag(Client::Flag::PROXY_BLANK_NAME_ENABLED)) {
|
||||
@@ -424,8 +446,8 @@ static HandlerResult S_V123P_02_17(
|
||||
}
|
||||
cmd.netloc = ses->xb_netloc;
|
||||
cmd.unknown_a1a = ses->xb_9E_unknown_a1a;
|
||||
cmd.xb_user_id_high = (ses->license->xb_user_id >> 32) & 0xFFFFFFFF;
|
||||
cmd.xb_user_id_low = ses->license->xb_user_id & 0xFFFFFFFF;
|
||||
cmd.xb_user_id_high = (ses->login->xb_license->user_id >> 32) & 0xFFFFFFFF;
|
||||
cmd.xb_user_id_low = ses->login->xb_license->user_id & 0xFFFFFFFF;
|
||||
ses->server_channel.send(0x9E, 0x01, &cmd, sizeof(C_LoginExtended_XB_9E));
|
||||
return HandlerResult::Type::SUPPRESS;
|
||||
}
|
||||
@@ -508,8 +530,8 @@ static HandlerResult S_V123_04(shared_ptr<ProxyServer::LinkedSession> ses, uint1
|
||||
offsetof(S_UpdateClientConfig_V3_04, client_config),
|
||||
sizeof(S_UpdateClientConfig_V3_04));
|
||||
|
||||
// If this is a licensed session, hide the guild card number assigned by the
|
||||
// remote server so the client doesn't see it change. If this is an unlicensed
|
||||
// If this is a logged-in session, hide the guild card number assigned by the
|
||||
// remote server so the client doesn't see it change. If this is a logged-out
|
||||
// session, then the client never received a guild card number from newserv
|
||||
// anyway, so we can let the client see the number from the remote server.
|
||||
bool had_guild_card_number = (ses->remote_guild_card_number >= 0);
|
||||
@@ -522,8 +544,8 @@ static HandlerResult S_V123_04(shared_ptr<ProxyServer::LinkedSession> ses, uint1
|
||||
ses->remote_guild_card_number);
|
||||
send_ship_info(ses->client_channel, message);
|
||||
}
|
||||
if (ses->license) {
|
||||
cmd.guild_card_number = ses->license->serial_number;
|
||||
if (ses->login) {
|
||||
cmd.guild_card_number = ses->login->account->account_id;
|
||||
}
|
||||
|
||||
// It seems the client ignores the length of the 04 command, and always copies
|
||||
@@ -549,15 +571,15 @@ static HandlerResult S_V123_04(shared_ptr<ProxyServer::LinkedSession> ses, uint1
|
||||
ses->server_channel.send(0x96, 0x00, &checksum, sizeof(checksum));
|
||||
}
|
||||
|
||||
return ses->license ? HandlerResult::Type::MODIFIED : HandlerResult::Type::FORWARD;
|
||||
return ses->login ? HandlerResult::Type::MODIFIED : HandlerResult::Type::FORWARD;
|
||||
}
|
||||
|
||||
static HandlerResult S_V123_06(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
bool modified = false;
|
||||
if (ses->license) {
|
||||
if (ses->login) {
|
||||
auto& cmd = check_size_t<SC_TextHeader_01_06_11_B0_EE>(data, 0xFFFF);
|
||||
if (cmd.guild_card_number == ses->remote_guild_card_number) {
|
||||
cmd.guild_card_number = ses->license->serial_number;
|
||||
cmd.guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
@@ -579,7 +601,7 @@ static HandlerResult S_V123_06(shared_ptr<ProxyServer::LinkedSession> ses, uint1
|
||||
|
||||
template <typename CmdT>
|
||||
static HandlerResult S_41(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
if (ses->license) {
|
||||
if (ses->login) {
|
||||
auto& cmd = check_size_t<CmdT>(data);
|
||||
if ((cmd.searcher_guild_card_number == ses->remote_guild_card_number) &&
|
||||
(cmd.result_guild_card_number == ses->remote_guild_card_number) &&
|
||||
@@ -593,11 +615,11 @@ static HandlerResult S_41(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
} else {
|
||||
bool modified = false;
|
||||
if (cmd.searcher_guild_card_number == ses->remote_guild_card_number) {
|
||||
cmd.searcher_guild_card_number = ses->license->serial_number;
|
||||
cmd.searcher_guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
if (cmd.result_guild_card_number == ses->remote_guild_card_number) {
|
||||
cmd.result_guild_card_number = ses->license->serial_number;
|
||||
cmd.result_guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
return modified ? HandlerResult::Type::MODIFIED : HandlerResult::Type::FORWARD;
|
||||
@@ -614,14 +636,14 @@ constexpr on_command_t S_B_41 = &S_41<S_GuildCardSearchResult_BB_41>;
|
||||
template <typename CmdT>
|
||||
static HandlerResult S_81(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
bool modified = false;
|
||||
if (ses->license) {
|
||||
if (ses->login) {
|
||||
auto& cmd = check_size_t<CmdT>(data);
|
||||
if (cmd.from_guild_card_number == ses->remote_guild_card_number) {
|
||||
cmd.from_guild_card_number = ses->license->serial_number;
|
||||
cmd.from_guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
if (cmd.to_guild_card_number == ses->remote_guild_card_number) {
|
||||
cmd.to_guild_card_number = ses->license->serial_number;
|
||||
cmd.to_guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
@@ -634,13 +656,13 @@ constexpr on_command_t S_B_81 = &S_81<SC_SimpleMail_BB_81>;
|
||||
|
||||
static HandlerResult S_88(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t flag, string& data) {
|
||||
bool modified = false;
|
||||
if (ses->license) {
|
||||
if (ses->login) {
|
||||
size_t expected_size = sizeof(S_ArrowUpdateEntry_88) * flag;
|
||||
auto* entries = &check_size_t<S_ArrowUpdateEntry_88>(
|
||||
data, expected_size, expected_size);
|
||||
for (size_t x = 0; x < flag; x++) {
|
||||
if (entries[x].guild_card_number == ses->remote_guild_card_number) {
|
||||
entries[x].guild_card_number = ses->license->serial_number;
|
||||
entries[x].guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
@@ -701,13 +723,14 @@ static HandlerResult S_B2(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
ses->log.info("Wrote code from server to file %s", output_filename.c_str());
|
||||
|
||||
#ifdef HAVE_RESOURCE_FILE
|
||||
using FooterT = S_ExecuteCode_Footer_B2<IsBigEndian>;
|
||||
using FooterT = S_ExecuteCode_FooterT_B2<IsBigEndian>;
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
// TODO: Support SH-4 disassembly too
|
||||
bool is_ppc = ::is_ppc(ses->version());
|
||||
bool is_x86 = ::is_x86(ses->version());
|
||||
if (is_ppc || is_x86) {
|
||||
bool is_sh4 = ::is_sh4(ses->version());
|
||||
if (is_ppc || is_x86 || is_sh4) {
|
||||
try {
|
||||
if (code.size() < sizeof(FooterT)) {
|
||||
throw runtime_error("code section is too small");
|
||||
@@ -742,6 +765,12 @@ static HandlerResult S_B2(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
code.size(),
|
||||
0,
|
||||
&labels);
|
||||
} else if (is_sh4) {
|
||||
disassembly = SH4Emulator::disassemble(
|
||||
&r.pget<uint8_t>(0, code.size()),
|
||||
code.size(),
|
||||
0,
|
||||
&labels);
|
||||
} else {
|
||||
// We shouldn't have entered the outer if statement if this happens
|
||||
throw logic_error("unsupported architecture");
|
||||
@@ -806,14 +835,14 @@ static HandlerResult S_B_E7(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t
|
||||
template <typename CmdT>
|
||||
static HandlerResult S_C4(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t flag, string& data) {
|
||||
bool modified = false;
|
||||
if (ses->license) {
|
||||
if (ses->login) {
|
||||
size_t expected_size = sizeof(CmdT) * flag;
|
||||
// Some servers (e.g. Schtserv) send extra data on the end of this command;
|
||||
// the client ignores it so we can ignore it too
|
||||
auto* entries = &check_size_t<CmdT>(data, expected_size, 0xFFFF);
|
||||
for (size_t x = 0; x < flag; x++) {
|
||||
if (entries[x].guild_card_number == ses->remote_guild_card_number) {
|
||||
entries[x].guild_card_number = ses->license->serial_number;
|
||||
entries[x].guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
@@ -830,7 +859,7 @@ static HandlerResult S_G_E4(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t
|
||||
bool modified = false;
|
||||
for (size_t x = 0; x < 4; x++) {
|
||||
if (cmd.entries[x].guild_card_number == ses->remote_guild_card_number) {
|
||||
cmd.entries[x].guild_card_number = ses->license->serial_number;
|
||||
cmd.entries[x].guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
@@ -1192,6 +1221,7 @@ static HandlerResult C_GXB_61(shared_ptr<ProxyServer::LinkedSession> ses, uint16
|
||||
if (is_ep3(ses->version()) && (ses->version() != Version::GC_EP3_NTE)) {
|
||||
ses->log.info("Version changed to GC_EP3_NTE");
|
||||
ses->set_version(Version::GC_EP3_NTE);
|
||||
ses->config.specific_version = SPECIFIC_VERSION_GC_EP3_NTE;
|
||||
}
|
||||
pd = &check_size_t<C_CharacterData_V3_61_98>(data, 0xFFFF);
|
||||
}
|
||||
@@ -1345,16 +1375,20 @@ static HandlerResult S_13_A7(shared_ptr<ProxyServer::LinkedSession> ses, uint16_
|
||||
|
||||
if (!sf->is_download && ends_with(sf->basename, ".dat")) {
|
||||
auto quest_dat_data = make_shared<std::string>(join(sf->blocks));
|
||||
ses->map = Lobby::load_maps(
|
||||
ses->version(),
|
||||
ses->lobby_episode,
|
||||
ses->lobby_difficulty,
|
||||
ses->lobby_event,
|
||||
ses->id,
|
||||
Map::DEFAULT_RARE_ENEMIES,
|
||||
ses->lobby_random_seed,
|
||||
make_shared<PSOV2Encryption>(ses->lobby_random_seed),
|
||||
quest_dat_data);
|
||||
try {
|
||||
ses->map = Lobby::load_maps(
|
||||
ses->version(),
|
||||
ses->lobby_episode,
|
||||
ses->lobby_difficulty,
|
||||
ses->lobby_event,
|
||||
ses->id,
|
||||
Map::DEFAULT_RARE_ENEMIES,
|
||||
ses->lobby_random_seed,
|
||||
make_shared<PSOV2Encryption>(ses->lobby_random_seed),
|
||||
quest_dat_data);
|
||||
} catch (const exception& e) {
|
||||
ses->log.warning("Failed to load quest map: %s", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
ses->saving_files.erase(cmd.filename.decode());
|
||||
@@ -1521,8 +1555,8 @@ static HandlerResult S_65_67_68_EB(shared_ptr<ProxyServer::LinkedSession> ses, u
|
||||
ses->log.warning("Ignoring invalid player index %zu at position %zu", index, x);
|
||||
} else {
|
||||
string name = escape_player_name(entry.disp.visual.name.decode(entry.inventory.language));
|
||||
if (ses->license && (entry.lobby_data.guild_card_number == ses->remote_guild_card_number)) {
|
||||
entry.lobby_data.guild_card_number = ses->license->serial_number;
|
||||
if (ses->login && (entry.lobby_data.guild_card_number == ses->remote_guild_card_number)) {
|
||||
entry.lobby_data.guild_card_number = ses->login->account->account_id;
|
||||
num_replacements++;
|
||||
modified = true;
|
||||
} else if (ses->config.check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) &&
|
||||
@@ -1675,7 +1709,7 @@ static HandlerResult S_64(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
update_leader_id(ses, cmd->leader_id);
|
||||
for (size_t x = 0; x < flag; x++) {
|
||||
if (cmd->lobby_data[x].guild_card_number == ses->remote_guild_card_number) {
|
||||
cmd->lobby_data[x].guild_card_number = ses->license->serial_number;
|
||||
cmd->lobby_data[x].guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
auto& p = ses->lobby_players[x];
|
||||
@@ -1740,11 +1774,11 @@ static HandlerResult S_E8(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
auto& spec_entry = cmd.entries[x];
|
||||
|
||||
if (player_entry.lobby_data.guild_card_number == ses->remote_guild_card_number) {
|
||||
player_entry.lobby_data.guild_card_number = ses->license->serial_number;
|
||||
player_entry.lobby_data.guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
if (spec_entry.guild_card_number == ses->remote_guild_card_number) {
|
||||
spec_entry.guild_card_number = ses->license->serial_number;
|
||||
spec_entry.guild_card_number = ses->login->account->account_id;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
@@ -1879,13 +1913,13 @@ static HandlerResult C_06(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
|
||||
static HandlerResult C_40(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
bool modified = false;
|
||||
if (ses->license) {
|
||||
if (ses->login) {
|
||||
auto& cmd = check_size_t<C_GuildCardSearch_40>(data);
|
||||
if (cmd.searcher_guild_card_number == ses->license->serial_number) {
|
||||
if (cmd.searcher_guild_card_number == ses->login->account->account_id) {
|
||||
cmd.searcher_guild_card_number = ses->remote_guild_card_number;
|
||||
modified = true;
|
||||
}
|
||||
if (cmd.target_guild_card_number == ses->license->serial_number) {
|
||||
if (cmd.target_guild_card_number == ses->login->account->account_id) {
|
||||
cmd.target_guild_card_number = ses->remote_guild_card_number;
|
||||
modified = true;
|
||||
}
|
||||
@@ -1896,11 +1930,11 @@ static HandlerResult C_40(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
template <typename CmdT>
|
||||
static HandlerResult C_81(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
auto& cmd = check_size_t<CmdT>(data);
|
||||
if (ses->license) {
|
||||
if (cmd.from_guild_card_number == ses->license->serial_number) {
|
||||
if (ses->login) {
|
||||
if (cmd.from_guild_card_number == ses->login->account->account_id) {
|
||||
cmd.from_guild_card_number = ses->remote_guild_card_number;
|
||||
}
|
||||
if (cmd.to_guild_card_number == ses->license->serial_number) {
|
||||
if (cmd.to_guild_card_number == ses->login->account->account_id) {
|
||||
cmd.to_guild_card_number = ses->remote_guild_card_number;
|
||||
}
|
||||
}
|
||||
@@ -1922,12 +1956,12 @@ void C_6x_movement(shared_ptr<ProxyServer::LinkedSession> ses, const string& dat
|
||||
|
||||
template <typename SendGuildCardCmdT>
|
||||
static HandlerResult C_6x(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t command, uint32_t flag, string& data) {
|
||||
if (ses->license && !data.empty()) {
|
||||
if (ses->login && !data.empty()) {
|
||||
// On BB, the 6x06 command is blank - the server generates the actual Guild
|
||||
// Card contents and sends it to the target client.
|
||||
if ((data[0] == 0x06) && !is_v4(ses->version())) {
|
||||
auto& cmd = check_size_t<SendGuildCardCmdT>(data);
|
||||
if (cmd.guild_card.guild_card_number == ses->license->serial_number) {
|
||||
if (cmd.guild_card.guild_card_number == ses->login->account->account_id) {
|
||||
cmd.guild_card.guild_card_number = ses->remote_guild_card_number;
|
||||
}
|
||||
}
|
||||
@@ -2006,11 +2040,11 @@ HandlerResult C_6x<void>(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, u
|
||||
}
|
||||
|
||||
static HandlerResult C_V123_A0_A1(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string&) {
|
||||
if (!ses->license) {
|
||||
if (!ses->login) {
|
||||
return HandlerResult::Type::FORWARD;
|
||||
}
|
||||
|
||||
// For licensed sessions, send them back to newserv's main menu instead of
|
||||
// For logged-in sessions, send them back to newserv's main menu instead of
|
||||
// going to the remote server's ship/block select menu
|
||||
ses->send_to_game_server();
|
||||
ses->disconnect_action = ProxyServer::LinkedSession::DisconnectAction::CLOSE_IMMEDIATELY;
|
||||
|
||||
+144
-112
@@ -41,7 +41,8 @@ ProxyServer::ProxyServer(
|
||||
: base(base),
|
||||
destroy_sessions_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &ProxyServer::dispatch_destroy_sessions, this), event_free),
|
||||
state(state),
|
||||
next_unlicensed_session_id(0xFF00000000000001) {}
|
||||
next_unlinked_session_id(this->MIN_UNLINKED_SESSION_ID),
|
||||
next_logged_out_session_id(this->MIN_LINKED_LOGGED_OUT_SESSION_ID) {}
|
||||
|
||||
void ProxyServer::listen(const std::string& addr, uint16_t port, Version version, const struct sockaddr_storage* default_destination) {
|
||||
auto socket_obj = make_shared<ListeningSocket>(this, addr, port, version, default_destination);
|
||||
@@ -57,7 +58,7 @@ ProxyServer::ListeningSocket::ListeningSocket(
|
||||
Version version,
|
||||
const struct sockaddr_storage* default_destination)
|
||||
: server(server),
|
||||
log(string_printf("[ProxyServer:ListeningSocket:%hu] ", port), proxy_server_log.min_level),
|
||||
log(string_printf("[ProxyServer:T-%hu] ", port), proxy_server_log.min_level),
|
||||
port(port),
|
||||
fd(::listen(addr, port, SOMAXCONN)),
|
||||
listener(nullptr, evconnlistener_free),
|
||||
@@ -98,9 +99,16 @@ void ProxyServer::ListeningSocket::dispatch_on_listen_error(
|
||||
}
|
||||
|
||||
void ProxyServer::ListeningSocket::on_listen_accept(int fd) {
|
||||
struct sockaddr_storage remote_addr;
|
||||
get_socket_addresses(fd, nullptr, &remote_addr);
|
||||
if (this->server->state->banned_ipv4_ranges->check(remote_addr)) {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
this->log.info("Client connected on fd %d (port %hu, version %s)", fd, this->port, name_for_enum(this->version));
|
||||
auto* bev = bufferevent_socket_new(this->server->base.get(), fd, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
this->server->on_client_connect(bev, this->port, this->version,
|
||||
this->server->on_client_connect(bev, 0, this->port, this->version,
|
||||
(this->default_destination.ss_family == AF_INET) ? &this->default_destination : nullptr);
|
||||
}
|
||||
|
||||
@@ -112,27 +120,26 @@ void ProxyServer::ListeningSocket::on_listen_error() {
|
||||
event_base_loopexit(this->server->base.get(), nullptr);
|
||||
}
|
||||
|
||||
void ProxyServer::connect_client(struct bufferevent* bev, uint16_t server_port) {
|
||||
void ProxyServer::connect_virtual_client(struct bufferevent* bev, uint64_t virtual_network_id, uint16_t server_port) {
|
||||
// Look up the listening socket for the given port, and use that game version.
|
||||
// We don't support default-destination proxying for virtual connections (yet)
|
||||
Version version;
|
||||
try {
|
||||
version = this->listeners.at(server_port)->version;
|
||||
} catch (const out_of_range&) {
|
||||
proxy_server_log.info("Virtual connection received on unregistered port %hu; closing it",
|
||||
server_port);
|
||||
proxy_server_log.info("Virtual connection received on unregistered port %hu; closing it", server_port);
|
||||
bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_FINISHED);
|
||||
bufferevent_free(bev);
|
||||
return;
|
||||
}
|
||||
|
||||
proxy_server_log.info("Client connected on virtual connection %p (port %hu)", bev,
|
||||
server_port);
|
||||
this->on_client_connect(bev, server_port, version, nullptr);
|
||||
proxy_server_log.info("Client connected on virtual connection %p (port %hu)", bev, server_port);
|
||||
this->on_client_connect(bev, virtual_network_id, server_port, version, nullptr);
|
||||
}
|
||||
|
||||
void ProxyServer::on_client_connect(
|
||||
struct bufferevent* bev,
|
||||
uint64_t virtual_network_id,
|
||||
uint16_t listen_port,
|
||||
Version version,
|
||||
const struct sockaddr_storage* default_destination) {
|
||||
@@ -140,26 +147,32 @@ void ProxyServer::on_client_connect(
|
||||
// client, create a linked session immediately and connect to the remote
|
||||
// server. This creates a direct session.
|
||||
if (default_destination && is_patch(version)) {
|
||||
uint64_t session_id = this->next_unlicensed_session_id++;
|
||||
if (this->next_unlicensed_session_id == 0) {
|
||||
this->next_unlicensed_session_id = 0xFF00000000000001;
|
||||
uint64_t session_id = this->next_logged_out_session_id++;
|
||||
if (this->next_logged_out_session_id == this->MIN_UNLINKED_SESSION_ID) {
|
||||
this->next_logged_out_session_id = this->MIN_LINKED_LOGGED_OUT_SESSION_ID;
|
||||
}
|
||||
|
||||
auto emplace_ret = this->id_to_session.emplace(session_id, make_shared<LinkedSession>(this->shared_from_this(), session_id, listen_port, version, *default_destination));
|
||||
auto emplace_ret = this->id_to_linked_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");
|
||||
throw logic_error("linked session already exists for logged-out client");
|
||||
}
|
||||
auto ses = emplace_ret.first->second;
|
||||
ses->log.info("Opened linked session");
|
||||
|
||||
Channel ch(bev, version, 1, nullptr, nullptr, ses.get(), "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN);
|
||||
Channel ch(bev, virtual_network_id, version, 1, nullptr, nullptr, ses.get(), "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN);
|
||||
ses->resume(std::move(ch));
|
||||
|
||||
} else {
|
||||
// If no default destination exists, or the client is not a patch client,
|
||||
// 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, make_shared<UnlinkedSession>(this->shared_from_this(), bev, listen_port, version));
|
||||
uint64_t session_id = this->next_unlinked_session_id++;
|
||||
if (this->next_unlinked_session_id == 0) {
|
||||
this->next_unlinked_session_id = this->MIN_UNLINKED_SESSION_ID;
|
||||
}
|
||||
|
||||
auto emplace_ret = this->id_to_unlinked_session.emplace(
|
||||
session_id, make_shared<UnlinkedSession>(this->shared_from_this(), session_id, bev, virtual_network_id, listen_port, version));
|
||||
if (!emplace_ret.second) {
|
||||
throw logic_error("stale unlinked session exists");
|
||||
}
|
||||
@@ -228,22 +241,28 @@ void ProxyServer::on_client_connect(
|
||||
|
||||
ProxyServer::UnlinkedSession::UnlinkedSession(
|
||||
shared_ptr<ProxyServer> server,
|
||||
uint64_t id,
|
||||
struct bufferevent* bev,
|
||||
uint64_t virtual_network_id,
|
||||
uint16_t local_port,
|
||||
Version version)
|
||||
: server(server),
|
||||
log(string_printf("[ProxyServer:UnlinkedSession:%p] ", bev), proxy_server_log.min_level),
|
||||
id(id),
|
||||
log(string_printf("[ProxyServer:US-%" PRIX64 "] ", this->id), proxy_server_log.min_level),
|
||||
channel(
|
||||
bev,
|
||||
virtual_network_id,
|
||||
version,
|
||||
1,
|
||||
ProxyServer::UnlinkedSession::on_input,
|
||||
ProxyServer::UnlinkedSession::on_error,
|
||||
this,
|
||||
string_printf("UnlinkedSession:%p", bev),
|
||||
"",
|
||||
TerminalFormat::FG_YELLOW,
|
||||
TerminalFormat::FG_GREEN),
|
||||
local_port(local_port) {
|
||||
string ip_str = server->state->format_address_for_channel_name(this->channel.remote_addr, this->channel.virtual_network_id);
|
||||
this->channel.name = string_printf("US-%" PRIX64 " @ %s", this->id, ip_str.c_str());
|
||||
memset(&this->next_destination, 0, sizeof(this->next_destination));
|
||||
}
|
||||
|
||||
@@ -277,9 +296,9 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
if (command == 0x8B) {
|
||||
ses->channel.version = Version::DC_NTE;
|
||||
ses->log.info("Version changed to DC_NTE");
|
||||
ses->config.specific_version = SPECIFIC_VERSION_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(), cmd.name.decode());
|
||||
ses->login = s->account_index->from_dc_nte_credentials(cmd.serial_number.decode(), cmd.access_key.decode(), false);
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
ses->character_name = cmd.name.decode(ses->channel.language);
|
||||
@@ -287,9 +306,12 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
} else if (command == 0x93) { // 11/2000 proto through DC V1
|
||||
ses->channel.version = Version::DC_V1;
|
||||
ses->log.info("Version changed to DC_V1");
|
||||
if (specific_version_is_indeterminate(ses->config.specific_version)) {
|
||||
ses->config.specific_version = SPECIFIC_VERSION_DC_V1_INDETERMINATE;
|
||||
}
|
||||
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(), cmd.name.decode());
|
||||
ses->login = s->account_index->from_dc_credentials(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode(), false);
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
ses->character_name = cmd.name.decode(ses->channel.language);
|
||||
@@ -299,13 +321,17 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
if (cmd.sub_version >= 0x30) {
|
||||
ses->log.info("Version changed to GC_NTE");
|
||||
ses->channel.version = Version::GC_NTE;
|
||||
ses->license = s->license_index->verify_gc_no_password(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode());
|
||||
ses->config.specific_version = SPECIFIC_VERSION_GC_NTE;
|
||||
ses->login = s->account_index->from_gc_credentials(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), nullptr, cmd.name.decode(), false);
|
||||
} 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(), cmd.name.decode());
|
||||
if (specific_version_is_indeterminate(ses->config.specific_version)) {
|
||||
ses->config.specific_version = SPECIFIC_VERSION_DC_V2_INDETERMINATE;
|
||||
}
|
||||
ses->login = s->account_index->from_dc_credentials(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode(), false);
|
||||
}
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
@@ -323,8 +349,8 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
throw runtime_error("command is not 9D");
|
||||
}
|
||||
const auto& cmd = check_size_t<C_Login_DC_PC_GC_9D>(data, sizeof(C_LoginExtended_PC_9D));
|
||||
ses->license = s->license_index->verify_v1_v2(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode());
|
||||
ses->login = s->account_index->from_pc_credentials(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode(), false);
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
ses->character_name = cmd.name.decode(ses->channel.language);
|
||||
@@ -337,8 +363,8 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
// 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_no_password(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), cmd.name.decode());
|
||||
ses->login = s->account_index->from_gc_credentials(
|
||||
stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode(), nullptr, cmd.name.decode(), false);
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
ses->character_name = cmd.name.decode(ses->channel.language);
|
||||
@@ -346,6 +372,9 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
if (cmd.sub_version >= 0x40) {
|
||||
ses->log.info("Version changed to GC_EP3");
|
||||
ses->channel.version = Version::GC_EP3;
|
||||
if (specific_version_is_indeterminate(ses->config.specific_version)) {
|
||||
ses->config.specific_version = SPECIFIC_VERSION_GC_EP3_INDETERMINATE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw runtime_error("command is not 9D or 9E");
|
||||
@@ -359,7 +388,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
string xb_gamertag = cmd.serial_number.decode();
|
||||
uint64_t xb_user_id = stoull(cmd.access_key.decode(), nullptr, 16);
|
||||
uint64_t xb_account_id = cmd.netloc.account_id;
|
||||
ses->license = s->license_index->verify_xb(xb_gamertag, xb_user_id, xb_account_id);
|
||||
ses->login = s->account_index->from_xb_credentials(xb_gamertag, xb_user_id, xb_account_id, false);
|
||||
ses->sub_version = cmd.sub_version;
|
||||
ses->channel.language = cmd.language;
|
||||
ses->character_name = cmd.name.decode(ses->channel.language);
|
||||
@@ -382,22 +411,8 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
throw runtime_error("command is not 93");
|
||||
}
|
||||
const auto& cmd = check_size_t<C_LoginBase_BB_93>(data, 0xFFFF);
|
||||
try {
|
||||
ses->license = s->license_index->verify_bb(cmd.username.decode(), cmd.password.decode());
|
||||
} catch (const LicenseIndex::missing_license&) {
|
||||
if (!s->allow_unregistered_users) {
|
||||
throw;
|
||||
}
|
||||
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();
|
||||
s->license_index->add(l);
|
||||
l->save();
|
||||
ses->license = l;
|
||||
string l_str = l->str();
|
||||
ses->log.info("Created license %s", l_str.c_str());
|
||||
}
|
||||
string password = cmd.password.decode();
|
||||
ses->login = s->account_index->from_bb_credentials(cmd.username.decode(), &password, s->allow_unregistered_users);
|
||||
ses->login_command_bb = std::move(data);
|
||||
break;
|
||||
}
|
||||
@@ -410,41 +425,38 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
should_close_unlinked_session = true;
|
||||
}
|
||||
|
||||
// Note that ch.bev will be moved from when the linked session is resumed, so
|
||||
// we need to retain a copy of it in order to close the unlinked session
|
||||
// afterward.
|
||||
struct bufferevent* session_key = ch.bev.get();
|
||||
uint64_t unlinked_session_id = ses->id;
|
||||
|
||||
// If license is non-null, then the client has a password and can be connected
|
||||
// If login is present, then the client has credentials and can be connected
|
||||
// to the remote lobby server.
|
||||
if (ses->license.get()) {
|
||||
if (ses->login) {
|
||||
// At this point, we will always close the unlinked session, even if it
|
||||
// doesn't get converted/merged to a linked session
|
||||
should_close_unlinked_session = true;
|
||||
|
||||
// Look up the linked session for this license (if any)
|
||||
// Look up the linked session for this account (if any)
|
||||
shared_ptr<LinkedSession> linked_ses;
|
||||
try {
|
||||
linked_ses = server->id_to_session.at(ses->license->serial_number);
|
||||
linked_ses = server->id_to_linked_session.at(ses->login->account->account_id);
|
||||
linked_ses->log.info("Resuming linked session from unlinked session");
|
||||
|
||||
} catch (const out_of_range&) {
|
||||
// If there's no open session for this license, then there must be a valid
|
||||
// If there's no open session for this account, then there must be a valid
|
||||
// destination somewhere - either in the client config or in the unlinked
|
||||
// session
|
||||
if (ses->config.proxy_destination_address != 0) {
|
||||
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");
|
||||
linked_ses = make_shared<LinkedSession>(server, ses->local_port, ses->version(), ses->login, ses->config);
|
||||
linked_ses->log.info("Opened logged-in session for unlinked session based on client config");
|
||||
} else if (ses->next_destination.ss_family == AF_INET) {
|
||||
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");
|
||||
linked_ses = make_shared<LinkedSession>(server, ses->local_port, ses->version(), ses->login, ses->next_destination);
|
||||
linked_ses->log.info("Opened logged-in 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");
|
||||
}
|
||||
}
|
||||
|
||||
if (linked_ses.get()) {
|
||||
server->id_to_session.emplace(ses->license->serial_number, linked_ses);
|
||||
server->id_to_linked_session.emplace(ses->login->account->account_id, linked_ses);
|
||||
// Resume the linked session using the unlinked session
|
||||
try {
|
||||
if (ses->version() == Version::BB_V4) {
|
||||
@@ -469,7 +481,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
|
||||
}
|
||||
|
||||
if (should_close_unlinked_session) {
|
||||
server->delete_session(session_key);
|
||||
server->delete_session(unlinked_session_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,7 +495,7 @@ void ProxyServer::UnlinkedSession::on_error(Channel& ch, short events) {
|
||||
}
|
||||
if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) {
|
||||
ses->log.info("Client has disconnected");
|
||||
ses->require_server()->delete_session(ses->channel.bev.get());
|
||||
ses->require_server()->delete_session(ses->id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,16 +506,16 @@ ProxyServer::LinkedSession::LinkedSession(
|
||||
Version version)
|
||||
: server(server),
|
||||
id(id),
|
||||
log(string_printf("[ProxyServer:LinkedSession:%08" PRIX64 "] ", this->id), proxy_server_log.min_level),
|
||||
log(string_printf("[ProxyServer:LS-%" PRIX64 "] ", this->id), proxy_server_log.min_level),
|
||||
timeout_event(event_new(server->base.get(), -1, EV_TIMEOUT, &LinkedSession::dispatch_on_timeout, this), event_free),
|
||||
license(nullptr),
|
||||
login(nullptr),
|
||||
client_channel(
|
||||
version,
|
||||
1,
|
||||
nullptr,
|
||||
nullptr,
|
||||
this,
|
||||
string_printf("LinkedSession:%08" PRIX64 ":client", this->id),
|
||||
string_printf("LS-%" PRIX64 "-C", this->id),
|
||||
TerminalFormat::FG_YELLOW,
|
||||
TerminalFormat::FG_GREEN),
|
||||
server_channel(
|
||||
@@ -512,7 +524,7 @@ ProxyServer::LinkedSession::LinkedSession(
|
||||
nullptr,
|
||||
nullptr,
|
||||
this,
|
||||
string_printf("LinkedSession:%08" PRIX64 ":server", this->id),
|
||||
string_printf("LS-%" PRIX64 "-S", this->id),
|
||||
TerminalFormat::FG_YELLOW,
|
||||
TerminalFormat::FG_RED),
|
||||
local_port(local_port),
|
||||
@@ -544,10 +556,10 @@ ProxyServer::LinkedSession::LinkedSession(
|
||||
shared_ptr<ProxyServer> server,
|
||||
uint16_t local_port,
|
||||
Version version,
|
||||
shared_ptr<License> license,
|
||||
shared_ptr<Login> login,
|
||||
const Client::Config& config)
|
||||
: LinkedSession(server, license->serial_number, local_port, version) {
|
||||
this->license = license;
|
||||
: LinkedSession(server, login->account->account_id, local_port, version) {
|
||||
this->login = login;
|
||||
this->config = config;
|
||||
memset(&this->next_destination, 0, sizeof(this->next_destination));
|
||||
struct sockaddr_in* dest_sin = reinterpret_cast<struct sockaddr_in*>(&this->next_destination);
|
||||
@@ -560,10 +572,10 @@ ProxyServer::LinkedSession::LinkedSession(
|
||||
shared_ptr<ProxyServer> server,
|
||||
uint16_t local_port,
|
||||
Version version,
|
||||
std::shared_ptr<License> license,
|
||||
std::shared_ptr<Login> login,
|
||||
const struct sockaddr_storage& next_destination)
|
||||
: LinkedSession(server, license->serial_number, local_port, version) {
|
||||
this->license = license;
|
||||
: LinkedSession(server, login->account->account_id, local_port, version) {
|
||||
this->login = login;
|
||||
this->next_destination = next_destination;
|
||||
}
|
||||
|
||||
@@ -631,7 +643,12 @@ void ProxyServer::LinkedSession::resume_inner(
|
||||
throw runtime_error("client connection is already open for this session");
|
||||
}
|
||||
if (this->next_destination.ss_family != AF_INET) {
|
||||
throw logic_error("attempted to resume an unlicensed linked session without destination set");
|
||||
throw logic_error("attempted to resume an logged-out linked session without destination set");
|
||||
}
|
||||
|
||||
auto s = this->server.lock();
|
||||
if (!s) {
|
||||
throw logic_error("ProxyServer is missing during LinkedSession resume");
|
||||
}
|
||||
|
||||
this->client_channel.replace_with(
|
||||
@@ -639,7 +656,7 @@ void ProxyServer::LinkedSession::resume_inner(
|
||||
ProxyServer::LinkedSession::on_input,
|
||||
ProxyServer::LinkedSession::on_error,
|
||||
this,
|
||||
string_printf("LinkedSession:%08" PRIX64 ":client", this->id));
|
||||
"");
|
||||
this->server_channel.language = this->client_channel.language;
|
||||
this->server_channel.version = this->client_channel.version;
|
||||
|
||||
@@ -662,8 +679,8 @@ void ProxyServer::LinkedSession::connect() {
|
||||
string netloc_str = render_sockaddr_storage(this->next_destination);
|
||||
this->log.info("Connecting to %s", netloc_str.c_str());
|
||||
|
||||
this->server_channel.set_bufferevent(bufferevent_socket_new(
|
||||
this->require_server()->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS));
|
||||
this->server_channel.set_bufferevent(
|
||||
bufferevent_socket_new(this->require_server()->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS), 0);
|
||||
if (bufferevent_socket_connect(this->server_channel.bev.get(),
|
||||
reinterpret_cast<const sockaddr*>(dest_sin), sizeof(*dest_sin)) != 0) {
|
||||
throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR()));
|
||||
@@ -673,10 +690,21 @@ void ProxyServer::LinkedSession::connect() {
|
||||
this->server_channel.on_error = ProxyServer::LinkedSession::on_error;
|
||||
this->server_channel.context_obj = this;
|
||||
|
||||
this->update_channel_names();
|
||||
|
||||
// Cancel the session delete timeout
|
||||
event_del(this->timeout_event.get());
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::update_channel_names() {
|
||||
auto s = this->require_server_state();
|
||||
auto client_ip_str = s->format_address_for_channel_name(
|
||||
this->client_channel.remote_addr, this->client_channel.virtual_network_id);
|
||||
auto server_ip_str = s->format_address_for_channel_name(this->server_channel.remote_addr, 0);
|
||||
this->client_channel.name = string_printf("LS-%08" PRIX64 "-C @ %s", this->id, client_ip_str.c_str());
|
||||
this->server_channel.name = string_printf("LS-%08" PRIX64 "-S @ %s", this->id, server_ip_str.c_str());
|
||||
}
|
||||
|
||||
ProxyServer::LinkedSession::SavingFile::SavingFile(
|
||||
const string& basename,
|
||||
const string& output_filename,
|
||||
@@ -701,6 +729,10 @@ void ProxyServer::LinkedSession::on_error(Channel& ch, short events) {
|
||||
|
||||
if (events & BEV_EVENT_CONNECTED) {
|
||||
ses->log.info("%s channel connected", is_server_stream ? "Server" : "Client");
|
||||
if (is_server_stream) {
|
||||
get_socket_addresses(bufferevent_getfd(ch.bev.get()), &ch.local_addr, &ch.remote_addr);
|
||||
ses->update_channel_names();
|
||||
}
|
||||
|
||||
if (is_server_stream && (ses->config.override_lobby_event != 0xFF) && (is_v3(ses->version()) || is_v4(ses->version()))) {
|
||||
ses->client_channel.send(0xDA, ses->config.override_lobby_event);
|
||||
@@ -795,9 +827,9 @@ void ProxyServer::LinkedSession::set_drop_mode(DropMode new_mode) {
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::send_to_game_server(const char* error_message) {
|
||||
// If there is no license, do nothing - we can't return to the game server
|
||||
// from unlicensed sessions
|
||||
if (!this->license) {
|
||||
// If there is no account, do nothing - we can't return to the game server
|
||||
// from logged-out sessions
|
||||
if (!this->login) {
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
@@ -831,17 +863,17 @@ void ProxyServer::LinkedSession::send_to_game_server(const char* error_message)
|
||||
if (is_v3(this->version())) {
|
||||
S_UpdateClientConfig_V3_04 update_client_config_cmd;
|
||||
update_client_config_cmd.player_tag = 0x00010000;
|
||||
update_client_config_cmd.guild_card_number = this->license->serial_number;
|
||||
update_client_config_cmd.guild_card_number = this->login->account->account_id;
|
||||
this->config.serialize_into(update_client_config_cmd.client_config);
|
||||
this->client_channel.send(0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd));
|
||||
}
|
||||
|
||||
string port_name = login_port_name_for_version(this->version());
|
||||
S_Reconnect_19 reconnect_cmd = {{0, s->name_to_port_config.at(port_name)->port, 0}};
|
||||
S_Reconnect_19 reconnect_cmd = {0, s->name_to_port_config.at(port_name)->port, 0};
|
||||
|
||||
// If the client is on a virtual connection, we can use any address
|
||||
// here and they should be able to connect back to the game server
|
||||
if (this->client_channel.is_virtual_connection) {
|
||||
if (this->client_channel.virtual_network_id) {
|
||||
struct sockaddr_in* dest_sin = reinterpret_cast<struct sockaddr_in*>(&this->next_destination);
|
||||
if (dest_sin->sin_family != AF_INET) {
|
||||
throw logic_error("ss not AF_INET");
|
||||
@@ -914,19 +946,19 @@ void ProxyServer::LinkedSession::on_input(Channel& ch, uint16_t command, uint32_
|
||||
}
|
||||
|
||||
shared_ptr<ProxyServer::LinkedSession> ProxyServer::get_session() const {
|
||||
if (this->id_to_session.empty()) {
|
||||
if (this->id_to_linked_session.empty()) {
|
||||
throw runtime_error("no sessions exist");
|
||||
}
|
||||
if (this->id_to_session.size() > 1) {
|
||||
if (this->id_to_linked_session.size() > 1) {
|
||||
throw runtime_error("multiple sessions exist");
|
||||
}
|
||||
return this->id_to_session.begin()->second;
|
||||
return this->id_to_linked_session.begin()->second;
|
||||
}
|
||||
|
||||
shared_ptr<ProxyServer::LinkedSession> ProxyServer::get_session_by_name(const std::string& name) const {
|
||||
try {
|
||||
uint64_t session_id = stoull(name, nullptr, 16);
|
||||
return this->id_to_session.at(session_id);
|
||||
return this->id_to_linked_session.at(session_id);
|
||||
} catch (const invalid_argument&) {
|
||||
throw runtime_error("invalid session name");
|
||||
} catch (const out_of_range&) {
|
||||
@@ -935,40 +967,40 @@ shared_ptr<ProxyServer::LinkedSession> ProxyServer::get_session_by_name(const st
|
||||
}
|
||||
|
||||
const unordered_map<uint64_t, shared_ptr<ProxyServer::LinkedSession>>& ProxyServer::all_sessions() const {
|
||||
return this->id_to_session;
|
||||
return this->id_to_linked_session;
|
||||
}
|
||||
|
||||
shared_ptr<ProxyServer::LinkedSession> ProxyServer::create_licensed_session(
|
||||
shared_ptr<License> l,
|
||||
shared_ptr<ProxyServer::LinkedSession> ProxyServer::create_logged_in_session(
|
||||
shared_ptr<Login> login,
|
||||
uint16_t local_port,
|
||||
Version version,
|
||||
const Client::Config& 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);
|
||||
auto session = make_shared<LinkedSession>(this->shared_from_this(), local_port, version, login, config);
|
||||
auto emplace_ret = this->id_to_linked_session.emplace(session->id, session);
|
||||
if (!emplace_ret.second) {
|
||||
throw runtime_error("session already exists for this license");
|
||||
throw runtime_error("session already exists for this account");
|
||||
}
|
||||
session->log.info("Opening licensed session");
|
||||
session->log.info("Opening logged-in session");
|
||||
return emplace_ret.first->second;
|
||||
}
|
||||
|
||||
void ProxyServer::delete_session(uint64_t id) {
|
||||
if (this->id_to_session.erase(id)) {
|
||||
proxy_server_log.info("Closed LinkedSession:%08" PRIX64, id);
|
||||
}
|
||||
}
|
||||
if (id < this->MIN_UNLINKED_SESSION_ID) {
|
||||
if (this->id_to_linked_session.erase(id)) {
|
||||
proxy_server_log.info("Closed LS-%08" PRIX64, id);
|
||||
}
|
||||
} else {
|
||||
auto it = this->id_to_unlinked_session.find(id);
|
||||
if (it == this->id_to_unlinked_session.end()) {
|
||||
throw logic_error("unlinked session exists but is not registered");
|
||||
}
|
||||
it->second->log.info("Closing session");
|
||||
this->unlinked_sessions_to_destroy.emplace(std::move(it->second));
|
||||
this->id_to_unlinked_session.erase(it);
|
||||
|
||||
void ProxyServer::delete_session(struct bufferevent* bev) {
|
||||
auto it = this->bev_to_unlinked_session.find(bev);
|
||||
if (it == this->bev_to_unlinked_session.end()) {
|
||||
throw logic_error("unlinked session exists but is not registered");
|
||||
auto tv = usecs_to_timeval(0);
|
||||
event_add(this->destroy_sessions_ev.get(), &tv);
|
||||
}
|
||||
it->second->log.info("Closing session");
|
||||
this->unlinked_sessions_to_destroy.emplace(std::move(it->second));
|
||||
this->bev_to_unlinked_session.erase(it);
|
||||
|
||||
auto tv = usecs_to_timeval(0);
|
||||
event_add(this->destroy_sessions_ev.get(), &tv);
|
||||
}
|
||||
|
||||
void ProxyServer::dispatch_destroy_sessions(evutil_socket_t, short, void* ctx) {
|
||||
@@ -980,14 +1012,14 @@ void ProxyServer::destroy_sessions() {
|
||||
}
|
||||
|
||||
size_t ProxyServer::num_sessions() const {
|
||||
return this->id_to_session.size();
|
||||
return this->id_to_linked_session.size();
|
||||
}
|
||||
|
||||
size_t ProxyServer::delete_disconnected_sessions() {
|
||||
size_t count = 0;
|
||||
for (auto it = this->id_to_session.begin(); it != this->id_to_session.end();) {
|
||||
for (auto it = this->id_to_linked_session.begin(); it != this->id_to_linked_session.end();) {
|
||||
if (!it->second->is_connected()) {
|
||||
it = this->id_to_session.erase(it);
|
||||
it = this->id_to_linked_session.erase(it);
|
||||
count++;
|
||||
} else {
|
||||
it++;
|
||||
|
||||
+25
-12
@@ -28,7 +28,7 @@ public:
|
||||
|
||||
void listen(const std::string& addr, uint16_t port, Version version, const struct sockaddr_storage* default_destination = nullptr);
|
||||
|
||||
void connect_client(struct bufferevent* bev, uint16_t server_port);
|
||||
void connect_virtual_client(struct bufferevent* bev, uint64_t virtual_network_id, uint16_t server_port);
|
||||
|
||||
struct LinkedSession : std::enable_shared_from_this<LinkedSession> {
|
||||
std::weak_ptr<ProxyServer> server;
|
||||
@@ -37,7 +37,7 @@ public:
|
||||
|
||||
std::unique_ptr<struct event, void (*)(struct event*)> timeout_event;
|
||||
|
||||
std::shared_ptr<License> license;
|
||||
std::shared_ptr<Login> login;
|
||||
|
||||
Channel client_channel;
|
||||
Channel server_channel;
|
||||
@@ -136,13 +136,13 @@ public:
|
||||
std::shared_ptr<ProxyServer> server,
|
||||
uint16_t local_port,
|
||||
Version version,
|
||||
std::shared_ptr<License> license,
|
||||
std::shared_ptr<Login> login,
|
||||
const Client::Config& config);
|
||||
LinkedSession(
|
||||
std::shared_ptr<ProxyServer> server,
|
||||
uint16_t local_port,
|
||||
Version version,
|
||||
std::shared_ptr<License> license,
|
||||
std::shared_ptr<Login> login,
|
||||
const struct sockaddr_storage& next_destination);
|
||||
LinkedSession(
|
||||
std::shared_ptr<ProxyServer> server,
|
||||
@@ -186,6 +186,8 @@ public:
|
||||
static void on_error(Channel& ch, short events);
|
||||
void on_timeout();
|
||||
|
||||
void update_channel_names();
|
||||
|
||||
void clear_lobby_players(size_t num_slots);
|
||||
|
||||
void set_drop_mode(DropMode new_mode);
|
||||
@@ -199,13 +201,12 @@ public:
|
||||
std::shared_ptr<LinkedSession> get_session_by_name(const std::string& name) const;
|
||||
const std::unordered_map<uint64_t, std::shared_ptr<LinkedSession>>& all_sessions() const;
|
||||
|
||||
std::shared_ptr<LinkedSession> create_licensed_session(
|
||||
std::shared_ptr<License> l,
|
||||
std::shared_ptr<LinkedSession> create_logged_in_session(
|
||||
std::shared_ptr<Login> login,
|
||||
uint16_t local_port,
|
||||
Version version,
|
||||
const Client::Config& config);
|
||||
void delete_session(uint64_t id);
|
||||
void delete_session(struct bufferevent* bev);
|
||||
|
||||
size_t num_sessions() const;
|
||||
|
||||
@@ -238,6 +239,7 @@ private:
|
||||
|
||||
struct UnlinkedSession {
|
||||
std::weak_ptr<ProxyServer> server;
|
||||
uint64_t id;
|
||||
|
||||
PrefixedLogger log;
|
||||
Channel channel;
|
||||
@@ -250,7 +252,7 @@ private:
|
||||
// just local variables inside on_input because XB requires two commands to
|
||||
// get started (9E and 9F), so we need to store this state somewhere between
|
||||
// those commands.
|
||||
std::shared_ptr<License> license;
|
||||
std::shared_ptr<Login> login;
|
||||
uint32_t sub_version = 0;
|
||||
std::string character_name;
|
||||
Client::Config config;
|
||||
@@ -259,7 +261,13 @@ private:
|
||||
XBNetworkLocation xb_netloc;
|
||||
parray<le_uint32_t, 3> xb_9E_unknown_a1a;
|
||||
|
||||
UnlinkedSession(std::shared_ptr<ProxyServer> server, struct bufferevent* bev, uint16_t port, Version version);
|
||||
UnlinkedSession(
|
||||
std::shared_ptr<ProxyServer> server,
|
||||
uint64_t id,
|
||||
struct bufferevent* bev,
|
||||
uint64_t virtual_network_id,
|
||||
uint16_t port,
|
||||
Version version);
|
||||
|
||||
std::shared_ptr<ProxyServer> require_server() const;
|
||||
std::shared_ptr<ServerState> require_server_state() const;
|
||||
@@ -278,17 +286,22 @@ private:
|
||||
std::shared_ptr<struct event> destroy_sessions_ev;
|
||||
std::shared_ptr<ServerState> state;
|
||||
std::map<int, std::shared_ptr<ListeningSocket>> listeners;
|
||||
std::unordered_map<struct bufferevent*, std::shared_ptr<UnlinkedSession>> bev_to_unlinked_session;
|
||||
std::unordered_map<uint64_t, std::shared_ptr<UnlinkedSession>> id_to_unlinked_session;
|
||||
std::unordered_set<std::shared_ptr<UnlinkedSession>> unlinked_sessions_to_destroy;
|
||||
std::unordered_map<uint64_t, std::shared_ptr<LinkedSession>> id_to_session;
|
||||
uint64_t next_unlicensed_session_id;
|
||||
std::unordered_map<uint64_t, std::shared_ptr<LinkedSession>> id_to_linked_session;
|
||||
uint64_t next_unlinked_session_id;
|
||||
uint64_t next_logged_out_session_id;
|
||||
|
||||
static void dispatch_destroy_sessions(evutil_socket_t, short, void* ctx);
|
||||
void destroy_sessions();
|
||||
|
||||
void on_client_connect(
|
||||
struct bufferevent* bev,
|
||||
uint64_t virtual_network_id,
|
||||
uint16_t listen_port,
|
||||
Version version,
|
||||
const struct sockaddr_storage* default_destination);
|
||||
|
||||
static constexpr uint64_t MIN_UNLINKED_SESSION_ID = 0xC000000000000000;
|
||||
static constexpr uint64_t MIN_LINKED_LOGGED_OUT_SESSION_ID = 0x1000000000000000;
|
||||
};
|
||||
|
||||
+30
-25
@@ -43,7 +43,7 @@ shared_ptr<const QuestCategoryIndex::Category> QuestCategoryIndex::at(uint32_t c
|
||||
// GCI decoding logic
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct PSOMemCardDLQFileEncryptedHeader {
|
||||
struct PSOMemCardDLQFileEncryptedHeaderT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
U32T round2_seed;
|
||||
@@ -54,12 +54,11 @@ struct PSOMemCardDLQFileEncryptedHeader {
|
||||
le_uint32_t decompressed_size;
|
||||
le_uint32_t round3_seed;
|
||||
// Data follows here.
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOVMSDLQFileEncryptedHeader : PSOMemCardDLQFileEncryptedHeader<false> {
|
||||
} __attribute__((packed));
|
||||
struct PSOGCIDLQFileEncryptedHeader : PSOMemCardDLQFileEncryptedHeader<true> {
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using PSOVMSDLQFileEncryptedHeader = PSOMemCardDLQFileEncryptedHeaderT<false>;
|
||||
using PSOGCIDLQFileEncryptedHeader = PSOMemCardDLQFileEncryptedHeaderT<true>;
|
||||
check_struct_size(PSOVMSDLQFileEncryptedHeader, 0x10);
|
||||
check_struct_size(PSOGCIDLQFileEncryptedHeader, 0x10);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
string decrypt_download_quest_data_section(
|
||||
@@ -73,7 +72,7 @@ string decrypt_download_quest_data_section(
|
||||
// not at the beginning. Presumably they did this because the system,
|
||||
// character, and Guild Card files are a constant size, but download quest
|
||||
// files can vary in size.
|
||||
using HeaderT = PSOMemCardDLQFileEncryptedHeader<IsBigEndian>;
|
||||
using HeaderT = PSOMemCardDLQFileEncryptedHeaderT<IsBigEndian>;
|
||||
auto* header = reinterpret_cast<HeaderT*>(decrypted.data());
|
||||
PSOV2Encryption round2_crypt(header->round2_seed);
|
||||
round2_crypt.encrypt_t<IsBigEndian>(
|
||||
@@ -193,7 +192,7 @@ string find_seed_and_decrypt_download_quest_data_section(
|
||||
struct PSODownloadQuestHeader {
|
||||
le_uint32_t size;
|
||||
le_uint32_t encryption_seed;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSODownloadQuestHeader, 8);
|
||||
|
||||
VersionedQuest::VersionedQuest(
|
||||
uint32_t quest_number,
|
||||
@@ -205,8 +204,8 @@ VersionedQuest::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)
|
||||
std::shared_ptr<const IntegralExpression> available_expression,
|
||||
std::shared_ptr<const IntegralExpression> enabled_expression)
|
||||
: quest_number(quest_number),
|
||||
category_id(category_id),
|
||||
episode(Episode::NONE),
|
||||
@@ -490,7 +489,7 @@ QuestIndex::QuestIndex(
|
||||
continue;
|
||||
}
|
||||
|
||||
auto add_file = [&](map<string, FileData>& files, const string& basename, const string& filename, string&& value) {
|
||||
auto add_file = [&](map<string, FileData>& files, const string& basename, const string& filename, string&& value, bool check_chunk_size) {
|
||||
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
|
||||
throw runtime_error("file " + basename + " exists in multiple categories");
|
||||
}
|
||||
@@ -498,6 +497,12 @@ QuestIndex::QuestIndex(
|
||||
if (!files.emplace(basename, FileData{filename, data_ptr}).second) {
|
||||
throw runtime_error("file " + basename + " already exists");
|
||||
}
|
||||
// There is a bug in the client that prevents quests from loading properly
|
||||
// if any file's size is a multiple of 0x400. See the comments on the 13
|
||||
// command in CommandFormats.hh for more details.
|
||||
if (check_chunk_size && !(data_ptr->size() & 0x3FF)) {
|
||||
data_ptr->push_back(0x00);
|
||||
}
|
||||
};
|
||||
|
||||
string cat_path = directory + "/" + cat->directory_name;
|
||||
@@ -544,26 +549,26 @@ QuestIndex::QuestIndex(
|
||||
}
|
||||
|
||||
if (extension == "json") {
|
||||
add_file(json_files, file_basename, orig_filename, std::move(file_data));
|
||||
add_file(json_files, file_basename, orig_filename, std::move(file_data), false);
|
||||
} else if (extension == "bin" || extension == "mnm") {
|
||||
add_file(bin_files, file_basename, orig_filename, std::move(file_data));
|
||||
add_file(bin_files, file_basename, orig_filename, std::move(file_data), true);
|
||||
} else if (extension == "bind" || extension == "mnmd") {
|
||||
add_file(bin_files, file_basename, orig_filename, prs_compress_optimal(file_data));
|
||||
add_file(bin_files, file_basename, orig_filename, prs_compress_optimal(file_data), true);
|
||||
} else if (extension == "dat") {
|
||||
add_file(dat_files, file_basename, orig_filename, std::move(file_data));
|
||||
add_file(dat_files, file_basename, orig_filename, std::move(file_data), true);
|
||||
} else if (extension == "datd") {
|
||||
add_file(dat_files, file_basename, orig_filename, prs_compress_optimal(file_data));
|
||||
add_file(dat_files, file_basename, orig_filename, prs_compress_optimal(file_data), true);
|
||||
} else if (extension == "pvr") {
|
||||
add_file(pvr_files, file_basename, orig_filename, std::move(file_data));
|
||||
add_file(pvr_files, file_basename, orig_filename, std::move(file_data), true);
|
||||
} else if (extension == "qst") {
|
||||
auto files = decode_qst_data(file_data);
|
||||
for (auto& it : files) {
|
||||
if (ends_with(it.first, ".bin")) {
|
||||
add_file(bin_files, file_basename, orig_filename, std::move(it.second));
|
||||
add_file(bin_files, file_basename, orig_filename, std::move(it.second), true);
|
||||
} else if (ends_with(it.first, ".dat")) {
|
||||
add_file(dat_files, file_basename, orig_filename, std::move(it.second));
|
||||
add_file(dat_files, file_basename, orig_filename, std::move(it.second), true);
|
||||
} else if (ends_with(it.first, ".pvr")) {
|
||||
add_file(pvr_files, file_basename, orig_filename, std::move(it.second));
|
||||
add_file(pvr_files, file_basename, orig_filename, std::move(it.second), true);
|
||||
} else {
|
||||
throw runtime_error("qst file contains unsupported file type: " + it.first);
|
||||
}
|
||||
@@ -667,8 +672,8 @@ QuestIndex::QuestIndex(
|
||||
const FileData* json_filedata = nullptr;
|
||||
shared_ptr<BattleRules> battle_rules;
|
||||
ssize_t challenge_template_index = -1;
|
||||
shared_ptr<const QuestAvailabilityExpression> available_expression;
|
||||
shared_ptr<const QuestAvailabilityExpression> enabled_expression;
|
||||
shared_ptr<const IntegralExpression> available_expression;
|
||||
shared_ptr<const IntegralExpression> enabled_expression;
|
||||
try {
|
||||
json_filedata = &json_files.at(basename);
|
||||
} catch (const out_of_range&) {
|
||||
@@ -692,11 +697,11 @@ QuestIndex::QuestIndex(
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
available_expression = make_shared<QuestAvailabilityExpression>(metadata_json.get_string("AvailableIf"));
|
||||
available_expression = make_shared<IntegralExpression>(metadata_json.get_string("AvailableIf"));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
enabled_expression = make_shared<QuestAvailabilityExpression>(metadata_json.get_string("EnabledIf"));
|
||||
enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -8,8 +8,8 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "IntegralExpression.hh"
|
||||
#include "PlayerSubordinates.hh"
|
||||
#include "QuestAvailabilityExpression.hh"
|
||||
#include "QuestScript.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "TeamIndex.hh"
|
||||
@@ -76,8 +76,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;
|
||||
std::shared_ptr<const IntegralExpression> available_expression;
|
||||
std::shared_ptr<const IntegralExpression> enabled_expression;
|
||||
|
||||
VersionedQuest(
|
||||
uint32_t quest_number,
|
||||
@@ -89,8 +89,8 @@ struct VersionedQuest {
|
||||
std::shared_ptr<const std::string> pvr_contents,
|
||||
std::shared_ptr<const BattleRules> battle_rules = nullptr,
|
||||
ssize_t challenge_template_index = -1,
|
||||
std::shared_ptr<const QuestAvailabilityExpression> available_expression = nullptr,
|
||||
std::shared_ptr<const QuestAvailabilityExpression> enabled_expression = nullptr);
|
||||
std::shared_ptr<const IntegralExpression> available_expression = nullptr,
|
||||
std::shared_ptr<const IntegralExpression> enabled_expression = nullptr);
|
||||
|
||||
std::string bin_filename() const;
|
||||
std::string dat_filename() const;
|
||||
@@ -124,8 +124,8 @@ 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::shared_ptr<const IntegralExpression> available_expression;
|
||||
std::shared_ptr<const IntegralExpression> enabled_expression;
|
||||
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ static string format_and_indent_data(const void* data, size_t size, uint64_t sta
|
||||
|
||||
struct UnknownF8F2Entry {
|
||||
parray<le_float, 4> unknown_a1;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(UnknownF8F2Entry, 0x10);
|
||||
|
||||
struct QuestScriptOpcodeDefinition {
|
||||
struct Argument {
|
||||
|
||||
+5
-5
@@ -16,7 +16,7 @@ struct PSOQuestHeaderDCNTE {
|
||||
/* 000C */ le_uint32_t unused;
|
||||
/* 0010 */ pstring<TextEncoding::SJIS, 0x10> name;
|
||||
/* 0020 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOQuestHeaderDCNTE, 0x20);
|
||||
|
||||
struct PSOQuestHeaderDC { // Same format for DC v1 and v2
|
||||
/* 0000 */ le_uint32_t code_offset;
|
||||
@@ -30,7 +30,7 @@ struct PSOQuestHeaderDC { // Same format for DC v1 and v2
|
||||
/* 0034 */ pstring<TextEncoding::MARKED, 0x80> short_description;
|
||||
/* 00B4 */ pstring<TextEncoding::MARKED, 0x120> long_description;
|
||||
/* 01D4 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOQuestHeaderDC, 0x1D4);
|
||||
|
||||
struct PSOQuestHeaderPC {
|
||||
/* 0000 */ le_uint32_t code_offset;
|
||||
@@ -44,7 +44,7 @@ struct PSOQuestHeaderPC {
|
||||
/* 0054 */ pstring<TextEncoding::UTF16, 0x80> short_description;
|
||||
/* 0154 */ pstring<TextEncoding::UTF16, 0x120> long_description;
|
||||
/* 0394 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOQuestHeaderPC, 0x394);
|
||||
|
||||
// TODO: Is the XB quest header format the same as on GC? If not, make a
|
||||
// separate struct; if so, rename this struct to V3.
|
||||
@@ -61,7 +61,7 @@ struct PSOQuestHeaderGC {
|
||||
/* 0034 */ pstring<TextEncoding::MARKED, 0x80> short_description;
|
||||
/* 00B4 */ pstring<TextEncoding::MARKED, 0x120> long_description;
|
||||
/* 01D4 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOQuestHeaderGC, 0x1D4);
|
||||
|
||||
struct PSOQuestHeaderBB {
|
||||
/* 0000 */ le_uint32_t code_offset;
|
||||
@@ -78,7 +78,7 @@ struct PSOQuestHeaderBB {
|
||||
/* 0058 */ pstring<TextEncoding::UTF16, 0x80> short_description;
|
||||
/* 0158 */ pstring<TextEncoding::UTF16, 0x120> long_description;
|
||||
/* 0398 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOQuestHeaderBB, 0x398);
|
||||
|
||||
Episode episode_for_quest_episode_number(uint8_t episode_number);
|
||||
|
||||
|
||||
+2
-2
@@ -97,7 +97,7 @@ void RareItemSet::ParsedRELData::parse_t(StringReader r, bool is_v1) {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
uint32_t root_offset = r.pget<U32T>(r.size() - 0x10);
|
||||
const auto& root = r.pget<Offsets<IsBigEndian>>(root_offset);
|
||||
const auto& root = r.pget<OffsetsT<IsBigEndian>>(root_offset);
|
||||
|
||||
StringReader monsters_r = r.sub(root.monster_rares_offset);
|
||||
for (size_t z = 0; z < (is_v1 ? 0x33 : 0x65); z++) {
|
||||
@@ -125,7 +125,7 @@ std::string RareItemSet::ParsedRELData::serialize_t(bool is_v1) const {
|
||||
|
||||
static const PackedDrop empty_drop;
|
||||
|
||||
Offsets<IsBigEndian> root;
|
||||
OffsetsT<IsBigEndian> root;
|
||||
root.box_count = this->box_rares.size();
|
||||
|
||||
StringWriter w;
|
||||
|
||||
+7
-3
@@ -64,17 +64,21 @@ protected:
|
||||
PackedDrop() = default;
|
||||
explicit PackedDrop(const ExpandedDrop&);
|
||||
ExpandedDrop expand() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PackedDrop, 4);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct Offsets {
|
||||
struct OffsetsT {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
/* 00 */ U32T monster_rares_offset; // -> parray<PackedDrop, 0x65> (or 0x33 on v1)
|
||||
/* 04 */ U32T box_count; // Usually 30 (0x1E)
|
||||
/* 08 */ U32T box_areas_offset; // -> parray<uint8_t, 0x1E>
|
||||
/* 0C */ U32T box_rares_offset; // -> parray<PackedDrop, 0x1E>
|
||||
/* 10 */
|
||||
} __attribute__((packed));
|
||||
} __packed__;
|
||||
using Offsets = OffsetsT<false>;
|
||||
using OffsetsBE = OffsetsT<true>;
|
||||
check_struct_size(Offsets, 0x10);
|
||||
check_struct_size(OffsetsBE, 0x10);
|
||||
|
||||
struct BoxRare {
|
||||
uint8_t area;
|
||||
|
||||
+642
-662
File diff suppressed because it is too large
Load Diff
+137
-61
@@ -644,9 +644,11 @@ static void on_sync_joining_player_quest_flags_t(shared_ptr<Client> c, uint8_t c
|
||||
|
||||
static void on_sync_joining_player_quest_flags(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
if (is_v1(c->version())) {
|
||||
on_sync_joining_player_quest_flags_t<G_SetQuestFlagsV1_6x6F>(c, command, flag, data, size);
|
||||
on_sync_joining_player_quest_flags_t<G_SetQuestFlags_DCv1_6x6F>(c, command, flag, data, size);
|
||||
} else if (!is_v4(c->version())) {
|
||||
on_sync_joining_player_quest_flags_t<G_SetQuestFlags_V2_V3_6x6F>(c, command, flag, data, size);
|
||||
} else {
|
||||
on_sync_joining_player_quest_flags_t<G_SetQuestFlagsV2V3V4_6x6F>(c, command, flag, data, size);
|
||||
on_sync_joining_player_quest_flags_t<G_SetQuestFlags_BB_6x6F>(c, command, flag, data, size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -989,13 +991,13 @@ static void on_sync_joining_player_disp_and_inventory(
|
||||
case Version::DC_NTE:
|
||||
parsed = make_unique<Parsed6x70Data>(
|
||||
check_size_t<G_SyncPlayerDispAndInventory_DCNTE_6x70>(data, size),
|
||||
c->license->serial_number);
|
||||
c->login->account->account_id);
|
||||
parsed->clear_dc_protos_unused_item_fields();
|
||||
break;
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
parsed = make_unique<Parsed6x70Data>(
|
||||
check_size_t<G_SyncPlayerDispAndInventory_DC112000_6x70>(data, size),
|
||||
c->license->serial_number,
|
||||
c->login->account->account_id,
|
||||
c->language());
|
||||
parsed->clear_dc_protos_unused_item_fields();
|
||||
break;
|
||||
@@ -1005,7 +1007,7 @@ static void on_sync_joining_player_disp_and_inventory(
|
||||
case Version::PC_V2:
|
||||
parsed = make_unique<Parsed6x70Data>(
|
||||
check_size_t<G_SyncPlayerDispAndInventory_DC_PC_6x70>(data, size),
|
||||
c->license->serial_number);
|
||||
c->login->account->account_id);
|
||||
if (c_v == Version::DC_V1) {
|
||||
parsed->clear_v1_unused_item_fields();
|
||||
}
|
||||
@@ -1016,17 +1018,17 @@ static void on_sync_joining_player_disp_and_inventory(
|
||||
case Version::GC_EP3:
|
||||
parsed = make_unique<Parsed6x70Data>(
|
||||
check_size_t<G_SyncPlayerDispAndInventory_GC_6x70>(data, size),
|
||||
c->license->serial_number);
|
||||
c->login->account->account_id);
|
||||
break;
|
||||
case Version::XB_V3:
|
||||
parsed = make_unique<Parsed6x70Data>(
|
||||
check_size_t<G_SyncPlayerDispAndInventory_XB_6x70>(data, size),
|
||||
c->license->serial_number);
|
||||
c->login->account->account_id);
|
||||
break;
|
||||
case Version::BB_V4:
|
||||
parsed = make_unique<Parsed6x70Data>(
|
||||
check_size_t<G_SyncPlayerDispAndInventory_BB_6x70>(data, size),
|
||||
c->license->serial_number);
|
||||
c->login->account->account_id);
|
||||
break;
|
||||
default:
|
||||
throw logic_error("6x70 command from unknown game version");
|
||||
@@ -1229,7 +1231,7 @@ static void on_symbol_chat(shared_ptr<Client> c, uint8_t command, uint8_t flag,
|
||||
|
||||
template <bool SenderIsBigEndian>
|
||||
static void on_word_select_t(shared_ptr<Client> c, uint8_t command, uint8_t, void* data, size_t size) {
|
||||
const auto& cmd = check_size_t<G_WordSelect_6x74<SenderIsBigEndian>>(data, size);
|
||||
const auto& cmd = check_size_t<G_WordSelectT_6x74<SenderIsBigEndian>>(data, size);
|
||||
if (c->can_chat && (cmd.client_id == c->lobby_client_id)) {
|
||||
if (command_is_private(command)) {
|
||||
return;
|
||||
@@ -1280,12 +1282,12 @@ static void on_word_select_t(shared_ptr<Client> c, uint8_t command, uint8_t, voi
|
||||
}
|
||||
|
||||
if (is_big_endian(lc->version())) {
|
||||
G_WordSelect_6x74<true> out_cmd = {
|
||||
G_WordSelectBE_6x74 out_cmd = {
|
||||
subcommand, cmd.size, cmd.client_id.load(),
|
||||
s->word_select_table->translate(cmd.message, from_version, lc_version)};
|
||||
send_command_t(lc, 0x60, 0x00, out_cmd);
|
||||
} else {
|
||||
G_WordSelect_6x74<false> out_cmd = {
|
||||
G_WordSelect_6x74 out_cmd = {
|
||||
subcommand, cmd.size, cmd.client_id.load(),
|
||||
s->word_select_table->translate(cmd.message, from_version, lc_version)};
|
||||
send_command_t(lc, 0x60, 0x00, out_cmd);
|
||||
@@ -1412,21 +1414,25 @@ static void on_player_revivable(shared_ptr<Client> c, uint8_t command, uint8_t f
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
|
||||
// Revive if infinite HP is enabled
|
||||
bool player_cheats_enabled = l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->license->check_flag(License::Flag::CHEAT_ANYWHERE));
|
||||
bool player_cheats_enabled = l->check_flag(Lobby::Flag::CHEATS_ENABLED) ||
|
||||
(c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE));
|
||||
if (player_cheats_enabled && c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
||||
G_UseMedicalCenter_6x31 v2_cmd = {0x31, 0x01, c->lobby_client_id};
|
||||
G_RevivePlayer_V3_BB_6xA1 v3_cmd = {0xA1, 0x01, c->lobby_client_id};
|
||||
for (auto lc : l->clients) {
|
||||
if (!lc) {
|
||||
continue;
|
||||
}
|
||||
bool use_v3 = !is_v1_or_v2(lc->version()) || (lc->version() == Version::GC_NTE);
|
||||
const void* data = use_v3 ? static_cast<const void*>(&v3_cmd) : static_cast<const void*>(&v2_cmd);
|
||||
size_t size = use_v3 ? sizeof(v3_cmd) : sizeof(v2_cmd);
|
||||
if (lc == c) {
|
||||
send_protected_command(lc, data, size, false);
|
||||
} else {
|
||||
send_command(lc, 0x60, 0x00, data, size);
|
||||
static_assert(sizeof(v2_cmd) == sizeof(v3_cmd), "Command sizes do not match");
|
||||
|
||||
const void* c_data = (!is_v1_or_v2(c->version()) || (c->version() == Version::GC_NTE))
|
||||
? static_cast<const void*>(&v3_cmd)
|
||||
: static_cast<const void*>(&v2_cmd);
|
||||
if (send_protected_command(c, c_data, sizeof(v3_cmd), false)) {
|
||||
for (auto lc : l->clients) {
|
||||
if (!lc || (lc == c)) {
|
||||
continue;
|
||||
}
|
||||
const void* lc_data = (!is_v1_or_v2(lc->version()) || (lc->version() == Version::GC_NTE))
|
||||
? static_cast<const void*>(&v3_cmd)
|
||||
: static_cast<const void*>(&v2_cmd);
|
||||
send_command(lc, 0x60, 0x00, lc_data, sizeof(v3_cmd));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1439,7 +1445,7 @@ void on_player_revived(shared_ptr<Client> c, uint8_t command, uint8_t flag, void
|
||||
if (l->is_game()) {
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
bool player_cheats_enabled = !is_v1(c->version()) &&
|
||||
(l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->license->check_flag(License::Flag::CHEAT_ANYWHERE)));
|
||||
(l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)));
|
||||
if (player_cheats_enabled && c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
||||
send_player_stats_change(c, PlayerStatsChange::ADD_HP, 2550);
|
||||
}
|
||||
@@ -1453,7 +1459,7 @@ static void on_received_condition(shared_ptr<Client> c, uint8_t command, uint8_t
|
||||
if (l->is_game()) {
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
if (cmd.client_id == c->lobby_client_id) {
|
||||
bool player_cheats_enabled = l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->license->check_flag(License::Flag::CHEAT_ANYWHERE));
|
||||
bool player_cheats_enabled = l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE));
|
||||
if (player_cheats_enabled && c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
||||
send_remove_conditions(c);
|
||||
}
|
||||
@@ -1469,7 +1475,7 @@ static void on_change_hp(shared_ptr<Client> c, uint8_t command, uint8_t flag, vo
|
||||
if (l->is_game() && (cmd.client_id == c->lobby_client_id)) {
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
bool player_cheats_enabled = !is_v1(c->version()) &&
|
||||
(l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->license->check_flag(License::Flag::CHEAT_ANYWHERE)));
|
||||
(l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)));
|
||||
if (player_cheats_enabled && c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
||||
send_player_stats_change(c, PlayerStatsChange::ADD_HP, 2550);
|
||||
}
|
||||
@@ -1483,7 +1489,7 @@ static void on_cast_technique_finished(shared_ptr<Client> c, uint8_t command, ui
|
||||
if (l->is_game() && (cmd.header.client_id == c->lobby_client_id)) {
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
bool player_cheats_enabled = !is_v1(c->version()) &&
|
||||
(l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->license->check_flag(License::Flag::CHEAT_ANYWHERE)));
|
||||
(l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)));
|
||||
if (player_cheats_enabled && c->config.check_flag(Client::Flag::INFINITE_TP_ENABLED)) {
|
||||
send_player_stats_change(c, PlayerStatsChange::ADD_TP, 255);
|
||||
}
|
||||
@@ -1548,17 +1554,20 @@ static void on_switch_state_changed(shared_ptr<Client> c, uint8_t command, uint8
|
||||
if (l->switch_flags) {
|
||||
if (cmd.flags & 1) {
|
||||
l->switch_flags->set(cmd.switch_flag_floor, cmd.switch_flag_num);
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message_printf(c, "$C5SW-%02hhX-%02hX ON", cmd.switch_flag_floor, cmd.switch_flag_num.load());
|
||||
}
|
||||
} else {
|
||||
l->switch_flags->clear(cmd.switch_flag_floor, cmd.switch_flag_num);
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message_printf(c, "$C5SW-%02hhX-%02hX OFF", cmd.switch_flag_floor, cmd.switch_flag_num.load());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((cmd.flags & 1) && cmd.header.object_id != 0xFFFF) {
|
||||
c->recent_switch_flags.add(cmd.switch_flag_num);
|
||||
if (!l->quest && c->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) {
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message(c, "$C5Switch assist");
|
||||
}
|
||||
string commands = c->recent_switch_flags.enable_commands(c->floor);
|
||||
if (!commands.empty()) {
|
||||
send_command(c, 0x60, 0x00, commands);
|
||||
@@ -1894,7 +1903,7 @@ static void on_box_or_enemy_item_drop_t(shared_ptr<Client> c, uint8_t command, u
|
||||
ItemData item = cmd.item.item;
|
||||
item.decode_for_version(c->version());
|
||||
l->on_item_id_generated_externally(item.id);
|
||||
l->add_item(cmd.item.floor, item, cmd.item.x, cmd.item.z, 0x00F);
|
||||
l->add_item(cmd.item.floor, item, cmd.item.x, cmd.item.z, 0x100F);
|
||||
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hhu (leader) created floor item %08" PRIX32 " (%s) at %hhu:(%g, %g)",
|
||||
@@ -1991,6 +2000,40 @@ static void on_pick_up_item_generic(
|
||||
send_create_inventory_item_to_client(lc, client_id, fi->data);
|
||||
}
|
||||
}
|
||||
|
||||
if (fi->flags & 0x1000) {
|
||||
uint32_t pi = fi->data.primary_identifier();
|
||||
bool should_send_game_notif, should_send_global_notif;
|
||||
if (is_v1_or_v2(c->version()) && (c->version() != Version::GC_NTE)) {
|
||||
should_send_game_notif = s->notify_game_for_item_primary_identifiers_v1_v2.count(pi);
|
||||
should_send_global_notif = s->notify_server_for_item_primary_identifiers_v1_v2.count(pi);
|
||||
} else if (!is_v4(c->version())) {
|
||||
should_send_game_notif = s->notify_game_for_item_primary_identifiers_v3.count(pi);
|
||||
should_send_global_notif = s->notify_server_for_item_primary_identifiers_v3.count(pi);
|
||||
} else {
|
||||
should_send_game_notif = s->notify_game_for_item_primary_identifiers_v4.count(pi);
|
||||
should_send_global_notif = s->notify_server_for_item_primary_identifiers_v4.count(pi);
|
||||
}
|
||||
|
||||
if (should_send_game_notif || should_send_global_notif) {
|
||||
string p_name = p->disp.name.decode();
|
||||
string desc = s->describe_item(c->version(), fi->data, true);
|
||||
string message = string_printf("$C6%s$C7 found\n%s", p_name.c_str(), desc.c_str());
|
||||
string bb_message = string_printf("$C6%s$C7 has found %s", p_name.c_str(), desc.c_str());
|
||||
if (should_send_global_notif) {
|
||||
for (auto& it : s->channel_to_client) {
|
||||
if (it.second->login &&
|
||||
!is_patch(it.second->version()) &&
|
||||
!is_ep3(it.second->version()) &&
|
||||
it.second->lobby.lock()) {
|
||||
send_text_or_scrolling_message(it.second, message, bb_message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
send_text_or_scrolling_message(l, nullptr, message, bb_message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2603,7 +2646,7 @@ static void on_entity_drop_item_request(shared_ptr<Client> c, uint8_t command, u
|
||||
res.item.id = l->generate_item_id(0xFF);
|
||||
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s",
|
||||
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str());
|
||||
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, (1 << lc->lobby_client_id));
|
||||
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, 0x1000 | (1 << lc->lobby_client_id));
|
||||
send_drop_item_to_channel(s, lc->channel, res.item, !rec.is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
|
||||
send_item_notification_if_needed(s, lc->channel, lc->config, res.item, res.is_from_rare_table);
|
||||
}
|
||||
@@ -2613,7 +2656,7 @@ static void on_entity_drop_item_request(shared_ptr<Client> c, uint8_t command, u
|
||||
res.item.id = l->generate_item_id(0xFF);
|
||||
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for all clients",
|
||||
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load());
|
||||
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, 0x00F);
|
||||
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, 0x100F);
|
||||
send_drop_item_to_lobby(l, res.item, !rec.is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
|
||||
for (auto lc : l->clients) {
|
||||
if (lc) {
|
||||
@@ -2636,7 +2679,7 @@ static void on_entity_drop_item_request(shared_ptr<Client> c, uint8_t command, u
|
||||
res.item.id = l->generate_item_id(0xFF);
|
||||
l->log.info("Creating item %08" PRIX32 " at %02hhX:%g,%g for %s",
|
||||
res.item.id.load(), cmd.floor, cmd.x.load(), cmd.z.load(), lc->channel.name.c_str());
|
||||
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, (1 << lc->lobby_client_id));
|
||||
l->add_item(cmd.floor, res.item, cmd.x, cmd.z, 0x1000 | (1 << lc->lobby_client_id));
|
||||
send_drop_item_to_channel(s, lc->channel, res.item, !rec.is_box, cmd.floor, cmd.x, cmd.z, cmd.entity_id);
|
||||
send_item_notification_if_needed(s, lc->channel, lc->config, res.item, res.is_from_rare_table);
|
||||
}
|
||||
@@ -2689,16 +2732,12 @@ static void on_set_quest_flag(shared_ptr<Client> c, uint8_t command, uint8_t fla
|
||||
auto s = c->require_server_state();
|
||||
// TODO: Should we allow overlays here?
|
||||
auto p = c->character(true, false);
|
||||
if (s->quest_flag_persist_mask.get(flag_num)) {
|
||||
if (should_set) {
|
||||
c->log.info("Setting quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num);
|
||||
p->quest_flags.set(difficulty, flag_num);
|
||||
} else {
|
||||
c->log.info("Clearing quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num);
|
||||
p->quest_flags.clear(difficulty, flag_num);
|
||||
}
|
||||
if (should_set) {
|
||||
c->log.info("Setting quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num);
|
||||
p->quest_flags.set(difficulty, flag_num);
|
||||
} else {
|
||||
c->log.info("Quest flag %s:%03hX cannot be modified", name_for_difficulty(difficulty), flag_num);
|
||||
c->log.info("Clearing quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num);
|
||||
p->quest_flags.clear(difficulty, flag_num);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2896,11 +2935,14 @@ static void on_trigger_set_event(shared_ptr<Client> c, uint8_t command, uint8_t
|
||||
|
||||
const auto& cmd = check_size_t<G_TriggerSetEvent_6x67>(data, size);
|
||||
if (l->map) {
|
||||
try {
|
||||
l->map->get_event(cmd.floor, cmd.event_id).flags |= 0x04;
|
||||
l->log.info("Client triggered set event W-%02" PRIX32 "-%" PRIX32, cmd.floor.load(), cmd.event_id.load());
|
||||
} catch (const out_of_range&) {
|
||||
l->log.warning("Client triggered missing set event W-%02" PRIX32 "-%" PRIX32, cmd.floor.load(), cmd.event_id.load());
|
||||
auto events = l->map->get_events(cmd.floor, cmd.event_id);
|
||||
for (auto* event : events) {
|
||||
event->flags |= 0x04;
|
||||
}
|
||||
l->log.info("Client triggered set event W-%02" PRIX32 "-%" PRIX32 " (%zu events)",
|
||||
cmd.floor.load(), cmd.event_id.load(), events.size());
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message_printf(c, "$C5W-%02" PRIX32 "-%" PRIX32 " START (%zu)", cmd.floor.load(), cmd.event_id.load(), events.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2941,7 +2983,7 @@ static void on_activate_timed_switch(shared_ptr<Client> c, uint8_t command, uint
|
||||
}
|
||||
|
||||
static void on_battle_scores(shared_ptr<Client> c, uint8_t command, uint8_t, void* data, size_t size) {
|
||||
const auto& cmd = check_size_t<G_BattleScores_6x7F<false>>(data, size);
|
||||
const auto& cmd = check_size_t<G_BattleScores_6x7F>(data, size);
|
||||
|
||||
if (command_is_private(command)) {
|
||||
return;
|
||||
@@ -2951,7 +2993,7 @@ static void on_battle_scores(shared_ptr<Client> c, uint8_t command, uint8_t, voi
|
||||
return;
|
||||
}
|
||||
|
||||
G_BattleScores_6x7F<true> sw_cmd;
|
||||
G_BattleScoresBE_6x7F sw_cmd;
|
||||
sw_cmd.header.subcommand = 0x7F;
|
||||
sw_cmd.header.size = cmd.header.size;
|
||||
sw_cmd.header.unused = 0;
|
||||
@@ -2985,8 +3027,8 @@ static void on_dragon_actions(shared_ptr<Client> c, uint8_t command, uint8_t, vo
|
||||
return;
|
||||
}
|
||||
|
||||
G_DragonBossActions_GC_6x12 sw_cmd = {{{cmd.header.subcommand, cmd.header.size, cmd.header.enemy_id},
|
||||
cmd.unknown_a2, cmd.unknown_a3, cmd.unknown_a4, cmd.x.load(), cmd.z.load()}};
|
||||
G_DragonBossActions_GC_6x12 sw_cmd = {{cmd.header.subcommand, cmd.header.size, cmd.header.enemy_id},
|
||||
cmd.unknown_a2, cmd.unknown_a3, cmd.unknown_a4, cmd.x.load(), cmd.z.load()};
|
||||
bool sender_is_be = is_big_endian(c->version());
|
||||
for (auto lc : l->clients) {
|
||||
if (lc && (lc != c)) {
|
||||
@@ -3010,14 +3052,14 @@ static void on_gol_dragon_actions(shared_ptr<Client> c, uint8_t command, uint8_t
|
||||
return;
|
||||
}
|
||||
|
||||
G_GolDragonBossActions_GC_6xA8 sw_cmd = {{{cmd.header.subcommand, cmd.header.size, cmd.header.enemy_id},
|
||||
G_GolDragonBossActions_GC_6xA8 sw_cmd = {{cmd.header.subcommand, cmd.header.size, cmd.header.enemy_id},
|
||||
cmd.unknown_a2,
|
||||
cmd.unknown_a3,
|
||||
cmd.unknown_a4,
|
||||
cmd.x.load(),
|
||||
cmd.z.load(),
|
||||
cmd.unknown_a5,
|
||||
0}};
|
||||
0};
|
||||
bool sender_is_be = is_big_endian(c->version());
|
||||
for (auto lc : l->clients) {
|
||||
if (lc && (lc != c)) {
|
||||
@@ -3054,7 +3096,7 @@ static void on_update_enemy_state(shared_ptr<Client> c, uint8_t command, uint8_t
|
||||
l->log.info("E-%hX updated to damage=%hu game_flags=%08" PRIX32, cmd.enemy_index.load(), enemy.total_damage, enemy.game_flags);
|
||||
}
|
||||
|
||||
G_UpdateEnemyState_GC_6x0A sw_cmd = {{{cmd.header.subcommand, cmd.header.size, cmd.header.enemy_id}, cmd.enemy_index, cmd.total_damage, cmd.flags.load()}};
|
||||
G_UpdateEnemyState_GC_6x0A sw_cmd = {{cmd.header.subcommand, cmd.header.size, cmd.header.enemy_id}, cmd.enemy_index, cmd.total_damage, cmd.flags.load()};
|
||||
bool sender_is_be = is_big_endian(c->version());
|
||||
for (auto lc : l->clients) {
|
||||
if (lc && (lc != c)) {
|
||||
@@ -3105,6 +3147,37 @@ static void on_charge_attack_bb(shared_ptr<Client> c, uint8_t command, uint8_t f
|
||||
}
|
||||
}
|
||||
|
||||
static void send_max_level_notification_if_needed(shared_ptr<Client> c) {
|
||||
auto s = c->require_server_state();
|
||||
if (!s->notify_server_for_max_level_achieved) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t max_level;
|
||||
if (is_v1(c->version())) {
|
||||
max_level = 99;
|
||||
} else if (!is_ep3(c->version())) {
|
||||
max_level = 199;
|
||||
} else {
|
||||
max_level = 998;
|
||||
}
|
||||
|
||||
auto p = c->character();
|
||||
if (p->disp.stats.level == max_level) {
|
||||
string name = p->disp.name.decode(c->language());
|
||||
size_t level_for_str = max_level + 1;
|
||||
string message = string_printf("$CG%s$C6\nGC: %" PRIu32 "\nhas reached Level $CG%zu",
|
||||
name.c_str(), c->login->account->account_id, level_for_str);
|
||||
string bb_message = string_printf("$CG%s$C6 (GC: %" PRIu32 ") has reached Level $CG%zu",
|
||||
name.c_str(), c->login->account->account_id, level_for_str);
|
||||
for (auto& it : s->channel_to_client) {
|
||||
if ((it.second != c) && it.second->login && !is_patch(it.second->version()) && it.second->lobby.lock()) {
|
||||
send_text_or_scrolling_message(it.second, message, bb_message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void on_level_up(shared_ptr<Client> c, uint8_t command, uint8_t flag, void* data, size_t size) {
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game()) {
|
||||
@@ -3137,6 +3210,7 @@ static void on_level_up(shared_ptr<Client> c, uint8_t command, uint8_t flag, voi
|
||||
p->disp.stats.level = cmd.level.load();
|
||||
}
|
||||
|
||||
send_max_level_notification_if_needed(c);
|
||||
forward_subcommand(c, command, flag, data, size);
|
||||
}
|
||||
|
||||
@@ -3159,7 +3233,9 @@ static void add_player_exp(shared_ptr<Client> c, uint32_t exp) {
|
||||
break;
|
||||
}
|
||||
} while (p->disp.stats.level < 199);
|
||||
|
||||
if (leveled_up) {
|
||||
send_max_level_notification_if_needed(c);
|
||||
send_level_up(c);
|
||||
}
|
||||
}
|
||||
@@ -3433,7 +3509,7 @@ void on_exchange_item_for_team_points_bb(shared_ptr<Client> c, uint8_t command,
|
||||
auto item = p->remove_item(cmd.item_id, cmd.amount, *s->item_stack_limits(c->version()));
|
||||
|
||||
size_t points = s->item_parameter_table(Version::BB_V4)->get_item_team_points(item);
|
||||
s->team_index->add_member_points(c->license->serial_number, points);
|
||||
s->team_index->add_member_points(c->login->account->account_id, points);
|
||||
|
||||
if (l->log.should_log(LogLevel::INFO)) {
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
@@ -3677,7 +3753,7 @@ static void on_battle_level_up_bb(shared_ptr<Client> c, uint8_t, uint8_t, void*
|
||||
auto lp = lc->character();
|
||||
uint32_t target_level = lp->disp.stats.level + cmd.num_levels;
|
||||
uint32_t before_exp = lp->disp.stats.experience;
|
||||
lp->disp.stats.advance_to_level(lp->disp.visual.char_class, target_level, s->level_table);
|
||||
s->level_table->advance_to_level(lp->disp.stats, target_level, lp->disp.visual.char_class);
|
||||
send_give_experience(lc, lp->disp.stats.experience - before_exp);
|
||||
send_level_up(lc);
|
||||
}
|
||||
@@ -3701,7 +3777,7 @@ static void on_request_challenge_grave_recovery_item_bb(shared_ptr<Client> c, ui
|
||||
};
|
||||
ItemData item = items.at(cmd.item_type);
|
||||
item.id = l->generate_item_id(cmd.header.client_id);
|
||||
l->add_item(cmd.floor, item, cmd.x, cmd.z, 0x00F);
|
||||
l->add_item(cmd.floor, item, cmd.x, cmd.z, 0x100F);
|
||||
send_drop_stacked_item_to_lobby(l, item, cmd.floor, cmd.x, cmd.z);
|
||||
}
|
||||
}
|
||||
@@ -4070,7 +4146,7 @@ static void on_quest_F95E_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, void
|
||||
}
|
||||
|
||||
item.id = l->generate_item_id(0xFF);
|
||||
l->add_item(cmd.floor, item, cmd.x, cmd.z, 0x00F);
|
||||
l->add_item(cmd.floor, item, cmd.x, cmd.z, 0x100F);
|
||||
|
||||
send_drop_stacked_item_to_lobby(l, item, cmd.floor, cmd.x, cmd.z);
|
||||
}
|
||||
@@ -4551,10 +4627,10 @@ void on_subcommand_multi(shared_ptr<Client> c, uint8_t command, uint8_t flag, st
|
||||
if (header->size != 0) {
|
||||
cmd_size = header->size << 2;
|
||||
} else {
|
||||
if (offset + sizeof(G_ExtendedHeader<G_UnusedHeader>) > data.size()) {
|
||||
if (offset + sizeof(G_ExtendedHeaderT<G_UnusedHeader>) > data.size()) {
|
||||
throw runtime_error("insufficient data remaining for next extended subcommand header");
|
||||
}
|
||||
const auto* ext_header = reinterpret_cast<const G_ExtendedHeader<G_UnusedHeader>*>(data.data() + offset);
|
||||
const auto* ext_header = reinterpret_cast<const G_ExtendedHeaderT<G_UnusedHeader>*>(data.data() + offset);
|
||||
cmd_size = ext_header->size;
|
||||
if (cmd_size < 8) {
|
||||
throw runtime_error("extended subcommand header has size < 8");
|
||||
|
||||
@@ -189,7 +189,7 @@ void ReplaySession::check_for_password(shared_ptr<const Event> ev) const {
|
||||
check_ak(cmd.access_key2.decode());
|
||||
}
|
||||
} else if (header.command == 0xDB) {
|
||||
const auto& cmd = check_size_t<C_VerifyLicense_V3_DB>(cmd_data, cmd_size);
|
||||
const auto& cmd = check_size_t<C_VerifyAccount_V3_DB>(cmd_data, cmd_size);
|
||||
check_ak(cmd.access_key.decode());
|
||||
check_ak(cmd.access_key2.decode());
|
||||
check_pw(cmd.password.decode());
|
||||
@@ -208,7 +208,7 @@ void ReplaySession::check_for_password(shared_ptr<const Event> ev) const {
|
||||
} else if (header.command == 0x9E) {
|
||||
check_pw(check_size_t<C_LoginExtended_BB_9E>(cmd_data, cmd_size).password.decode());
|
||||
} else if (header.command == 0xDB) {
|
||||
check_pw(check_size_t<C_VerifyLicense_BB_DB>(cmd_data, cmd_size).password.decode());
|
||||
check_pw(check_size_t<C_VerifyAccount_V3_DB>(cmd_data, cmd_size).password.decode());
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -630,7 +630,7 @@ void ReplaySession::execute_pending_events() {
|
||||
struct bufferevent* bevs[2];
|
||||
bufferevent_pair_new(this->base.get(), 0, bevs);
|
||||
|
||||
c->channel.set_bufferevent(bevs[0]);
|
||||
c->channel.set_bufferevent(bevs[0], 0);
|
||||
this->channel_to_client.emplace(&c->channel, c);
|
||||
|
||||
shared_ptr<const PortConfiguration> port_config;
|
||||
@@ -645,7 +645,7 @@ void ReplaySession::execute_pending_events() {
|
||||
// TODO: We should support this at some point in the future
|
||||
throw runtime_error(string_printf("(ev-line %zu) client connected to proxy server", this->first_event->line_num));
|
||||
} else if (this->state->game_server.get()) {
|
||||
this->state->game_server->connect_client(bevs[1], 0x20202020,
|
||||
this->state->game_server->connect_virtual_client(bevs[1], 0, 0x20202020,
|
||||
1025, c->port, port_config->version, port_config->behavior);
|
||||
} else {
|
||||
throw runtime_error(string_printf("(ev-line %zu) no server available for connection", this->first_event->line_num));
|
||||
|
||||
+228
-13
@@ -1,5 +1,6 @@
|
||||
#include "SaveFileFormats.hh"
|
||||
|
||||
#include <phosg/Hash.hh>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
@@ -355,6 +356,31 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_config(
|
||||
for (size_t z = 0; z < initial_items.size(); z++) {
|
||||
ret->inventory.items[z] = initial_items[z];
|
||||
}
|
||||
|
||||
// Set mag color based on initial costume
|
||||
static const array<array<uint8_t, 25>, 12> mag_colors = {{
|
||||
{0x09, 0x01, 0x02, 0x11, 0x0A, 0x05, 0x06, 0x0B, 0x05, 0x00, 0x07, 0x0B, 0x0C, 0x04, 0x05, 0x06, 0x0E, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
{0x00, 0x01, 0x02, 0x11, 0x04, 0x05, 0x06, 0x08, 0x11, 0x0D, 0x01, 0x02, 0x0C, 0x04, 0x05, 0x06, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
{0x00, 0x01, 0x02, 0x11, 0x04, 0x0E, 0x06, 0x01, 0x0E, 0x09, 0x07, 0x02, 0x11, 0x04, 0x05, 0x06, 0x04, 0x11, 0x0D, 0x01, 0x0B, 0x11, 0x0D, 0x05, 0x06},
|
||||
{0x00, 0x01, 0x0B, 0x11, 0x04, 0x05, 0x06, 0x0F, 0x05, 0x09, 0x07, 0x02, 0x11, 0x04, 0x05, 0x0F, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
{0x00, 0x01, 0x0B, 0x11, 0x0A, 0x05, 0x06, 0x06, 0x09, 0x09, 0x01, 0x02, 0x11, 0x0A, 0x0E, 0x06, 0x01, 0x04, 0x0D, 0x07, 0x01, 0x0C, 0x0A, 0x05, 0x06},
|
||||
{0x10, 0x07, 0x02, 0x11, 0x0A, 0x05, 0x0A, 0x00, 0x07, 0x00, 0x01, 0x08, 0x11, 0x04, 0x09, 0x0F, 0x0D, 0x02, 0x0A, 0x07, 0x02, 0x0C, 0x04, 0x0E, 0x0E},
|
||||
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x10, 0x01, 0x00, 0x07, 0x02, 0x0C, 0x04, 0x05, 0x06, 0x10, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
{0x0D, 0x01, 0x02, 0x11, 0x04, 0x05, 0x06, 0x00, 0x11, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x04, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x10, 0x05, 0x09, 0x01, 0x0B, 0x0C, 0x04, 0x05, 0x06, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
{0x00, 0x01, 0x02, 0x0C, 0x04, 0x05, 0x0F, 0x0A, 0x04, 0x0D, 0x01, 0x08, 0x11, 0x04, 0x05, 0x0F, 0x05, 0x10, 0x10, 0x07, 0x02, 0x0B, 0x0A, 0x0A, 0x0F},
|
||||
{0x00, 0x01, 0x0B, 0x0C, 0x04, 0x05, 0x06, 0x08, 0x0A, 0x0D, 0x07, 0x02, 0x11, 0x0A, 0x05, 0x06, 0x01, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
{0x00, 0x07, 0x02, 0x11, 0x04, 0x05, 0x06, 0x09, 0x0C, 0x00, 0x01, 0x02, 0x11, 0x0D, 0x05, 0x10, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
}};
|
||||
uint8_t char_class = (visual.char_class > 0x0B) ? 0 : visual.char_class;
|
||||
uint8_t mag_color_index;
|
||||
if (char_class == 2 || char_class == 4 || char_class == 5 || char_class == 9) {
|
||||
mag_color_index = (visual.skin >= 25) ? 0 : visual.skin.load();
|
||||
} else {
|
||||
mag_color_index = (visual.costume >= 18) ? 0 : visual.costume.load();
|
||||
}
|
||||
ret->inventory.items[2].data.data2[3] = mag_colors.at(char_class).at(mag_color_index);
|
||||
|
||||
ret->inventory.items[13].extension_data2 = 1;
|
||||
|
||||
const auto& config = (ret->disp.visual.class_flags & 0x80) ? config_force : config_hunter_ranger;
|
||||
@@ -362,7 +388,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_config(
|
||||
ret->disp.config[z] = config[z];
|
||||
}
|
||||
|
||||
ret->disp.stats.reset_to_base(ret->disp.visual.char_class, level_table);
|
||||
level_table->reset_to_base(ret->disp.stats, ret->disp.visual.char_class);
|
||||
ret->disp.technique_levels_v1.clear(0xFF);
|
||||
if (ret->disp.visual.class_flags & 0x80) {
|
||||
ret->disp.technique_levels_v1[0] = 0x00; // Forces start with Foie Lv.1
|
||||
@@ -378,7 +404,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_config(
|
||||
ret->symbol_chats[z] = PSOBBCharacterFile::DEFAULT_SYMBOL_CHATS[z].to_entry(language);
|
||||
}
|
||||
for (size_t z = 0; z < PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG.size(); z++) {
|
||||
ret->tech_menu_config[z] = PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG[z];
|
||||
ret->tech_menu_shortcut_entries[z] = PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG[z];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -392,16 +418,99 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
|
||||
guild_card_number, language, preview.visual, preview.name.decode(language), level_table);
|
||||
}
|
||||
|
||||
PSOBBCharacterFile::SymbolChatEntry PSOBBCharacterFile::DefaultSymbolChatEntry::to_entry(uint8_t language) const {
|
||||
SymbolChatEntry ret;
|
||||
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_gc(const PSOGCCharacterFile::Character& gc) {
|
||||
auto ret = make_shared<PSOBBCharacterFile>();
|
||||
ret->inventory = gc.inventory;
|
||||
uint8_t language = ret->inventory.language;
|
||||
ret->disp = gc.disp.to_bb(language, language);
|
||||
ret->creation_timestamp = gc.creation_timestamp.load();
|
||||
ret->play_time_seconds = gc.play_time_seconds.load();
|
||||
ret->option_flags = gc.option_flags.load();
|
||||
ret->save_count = gc.save_count.load();
|
||||
ret->quest_flags = gc.quest_flags;
|
||||
ret->death_count = gc.death_count.load();
|
||||
ret->bank = gc.bank;
|
||||
ret->guild_card = gc.guild_card;
|
||||
for (size_t z = 0; z < std::min<size_t>(ret->symbol_chats.size(), gc.symbol_chats.size()); z++) {
|
||||
auto& ret_sc = ret->symbol_chats[z];
|
||||
const auto& gc_sc = gc.symbol_chats[z];
|
||||
ret_sc.present = gc_sc.present.load();
|
||||
ret_sc.name.encode(gc_sc.name.decode(language), language);
|
||||
ret_sc.spec = gc_sc.spec;
|
||||
}
|
||||
for (size_t z = 0; z < std::min<size_t>(ret->shortcuts.size(), gc.shortcuts.size()); z++) {
|
||||
ret->shortcuts[z] = gc.shortcuts[z].convert<false, TextEncoding::UTF16>(language);
|
||||
}
|
||||
ret->auto_reply.encode(gc.auto_reply.decode(language), language);
|
||||
ret->info_board.encode(gc.info_board.decode(language), language);
|
||||
ret->battle_records = gc.battle_records;
|
||||
ret->unknown_a4 = gc.unknown_a4;
|
||||
ret->challenge_records = gc.challenge_records;
|
||||
for (size_t z = 0; z < std::min<size_t>(ret->tech_menu_shortcut_entries.size(), gc.tech_menu_shortcut_entries.size()); z++) {
|
||||
ret->tech_menu_shortcut_entries[z] = gc.tech_menu_shortcut_entries[z].load();
|
||||
}
|
||||
ret->choice_search_config = gc.choice_search_config;
|
||||
ret->unknown_a6 = gc.unknown_a6;
|
||||
for (size_t z = 0; z < std::min<size_t>(ret->quest_counters.size(), gc.quest_counters.size()); z++) {
|
||||
ret->quest_counters[z] = gc.quest_counters[z].load();
|
||||
}
|
||||
ret->offline_battle_records = gc.offline_battle_records;
|
||||
ret->unknown_a7 = gc.unknown_a7;
|
||||
return ret;
|
||||
}
|
||||
|
||||
PSOGCCharacterFile::Character PSOBBCharacterFile::to_gc() const {
|
||||
uint8_t language = this->inventory.language;
|
||||
|
||||
PSOGCCharacterFile::Character ret;
|
||||
ret.inventory = this->inventory;
|
||||
ret.disp = this->disp.to_dcpcv3<true>(language, language);
|
||||
ret.creation_timestamp = this->creation_timestamp.load();
|
||||
ret.play_time_seconds = this->play_time_seconds.load();
|
||||
ret.option_flags = this->option_flags.load();
|
||||
ret.save_count = this->save_count.load();
|
||||
ret.quest_flags = this->quest_flags;
|
||||
ret.death_count = this->death_count.load();
|
||||
ret.bank = this->bank;
|
||||
ret.guild_card = this->guild_card;
|
||||
for (size_t z = 0; z < std::min<size_t>(ret.symbol_chats.size(), this->symbol_chats.size()); z++) {
|
||||
auto& ret_sc = ret.symbol_chats[z];
|
||||
const auto& gc_sc = this->symbol_chats[z];
|
||||
ret_sc.present = gc_sc.present.load();
|
||||
ret_sc.name.encode(gc_sc.name.decode(language), language);
|
||||
ret_sc.spec = gc_sc.spec;
|
||||
}
|
||||
for (size_t z = 0; z < std::min<size_t>(ret.shortcuts.size(), this->shortcuts.size()); z++) {
|
||||
ret.shortcuts[z] = this->shortcuts[z].convert<true, TextEncoding::MARKED>(language);
|
||||
}
|
||||
ret.auto_reply.encode(this->auto_reply.decode(language), language);
|
||||
ret.info_board.encode(this->info_board.decode(language), language);
|
||||
ret.battle_records = this->battle_records;
|
||||
ret.unknown_a4 = this->unknown_a4;
|
||||
ret.challenge_records = this->challenge_records;
|
||||
for (size_t z = 0; z < std::min<size_t>(ret.tech_menu_shortcut_entries.size(), this->tech_menu_shortcut_entries.size()); z++) {
|
||||
ret.tech_menu_shortcut_entries[z] = this->tech_menu_shortcut_entries[z].load();
|
||||
}
|
||||
ret.choice_search_config = this->choice_search_config;
|
||||
ret.unknown_a6 = this->unknown_a6;
|
||||
for (size_t z = 0; z < std::min<size_t>(ret.quest_counters.size(), this->quest_counters.size()); z++) {
|
||||
ret.quest_counters[z] = this->quest_counters[z].load();
|
||||
}
|
||||
ret.offline_battle_records = this->offline_battle_records;
|
||||
ret.unknown_a7 = this->unknown_a7;
|
||||
return ret;
|
||||
}
|
||||
|
||||
SaveFileSymbolChatEntryBB PSOBBCharacterFile::DefaultSymbolChatEntry::to_entry(uint8_t language) const {
|
||||
SaveFileSymbolChatEntryBB ret;
|
||||
ret.present = 1;
|
||||
ret.name.encode(this->language_to_name.at(language), language);
|
||||
ret.data.spec = this->spec;
|
||||
ret.spec.spec = this->spec;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
ret.data.corner_objects[z] = this->corner_objects[z];
|
||||
ret.spec.corner_objects[z] = this->corner_objects[z];
|
||||
}
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
ret.data.face_parts[z] = this->face_parts[z];
|
||||
ret.spec.face_parts[z] = this->face_parts[z];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -591,12 +700,12 @@ void PSOBBCharacterFile::clear_all_material_usage() {
|
||||
}
|
||||
|
||||
const array<PSOBBCharacterFile::DefaultSymbolChatEntry, 6> PSOBBCharacterFile::DEFAULT_SYMBOL_CHATS = {
|
||||
DefaultSymbolChatEntry{{"\tJ\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF", "\tEHello", "\tEHallo", "\tESalut", "\tEHola", "\tB\xE4\xBD\xA0\xE5\xA5\xBD", "\tT\xE4\xBD\xA0\xE5\xA5\xBD", "\tK\xEC\x95\x88\xEB\x85\x95"}, 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{{"\tJ\xE3\x81\x95\xE3\x82\x88\xE3\x81\x86\xE3\x81\xAA\xE3\x82\x89", "\tEGood-bye", "\tETschus", "\tEAu revoir", "\tEAdios", "\tB\xE5\x86\x8D\xE8\xA7\x81", "\tT\xE5\x86\x8D\xE8\xA6\x8B", "\tK\xEC\x9E\x98\xEA\xB0\x80"}, 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}}},
|
||||
DefaultSymbolChatEntry{{"\tJ\xE3\x81\xB0\xE3\x82\x93\xE3\x81\x96\xE3\x83\xBC\xE3\x81\x84", "\tEHurrah!", "\tEHurra!", "\tEHourra !", "\tEHurra", "\tB\xE4\xB8\x87\xE5\xB2\x81", "\tT\xE8\x90\xAC\xE6\xAD\xB2", "\tK\xEB\xA7\x8C\xEC\x84\xB8"}, 0x28, {0x0362, 0x0362, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x09, 0x16, 0x1B, 0x00}, {0x09, 0x2B, 0x1B, 0x01}, {0x37, 0x20, 0x2C, 0x00}, {0xFF, 0x00, 0x00, 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{{"\tJ\xE3\x81\x86\xE3\x81\x87\xEF\xBD\x9E\xE3\x82\x93", "\tECrying", "\tEIch bin sauer!", "\tEJe suis triste", "\tELlanto", "\tB\xE5\x96\x82\xEF\xBD\x9E", "\tT\xE5\x96\x82\xEF\xBD\x9E", "\tK\xEC\x9D\x91~"}, 0x74, {0x074F, 0xFFFF, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x06, 0x15, 0x14, 0x00}, {0x06, 0x2B, 0x14, 0x01}, {0x05, 0x18, 0x1F, 0x00}, {0x05, 0x28, 0x1F, 0x01}, {0x21, 0x20, 0x2E, 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{{"\tJ\xE3\x81\x8A\xE3\x81\x93\xE3\x81\xA3\xE3\x81\x9F\xEF\xBC\x81", "\tEI'm angry!", "\tEWeinen", "\tEJe suis en colere !", "\tEEnfado", "\tB\xE7\x94\x9F\xE6\xB0\x94\xE4\xBA\x86\xEF\xBC\x81", "\tT\xE7\x94\x9F\xE6\xB0\xA3\xE4\xBA\x86\xEF\xBC\x81", "\tK\xEB\x82\x98\xEC\x99\x94\xEB\x8B\xA4\xEF\xBC\x81"}, 0x5C, {0x0116, 0x0001, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x0B, 0x18, 0x1B, 0x01}, {0x0B, 0x28, 0x1B, 0x00}, {0x33, 0x20, 0x2A, 0x00}, {0xFF, 0x00, 0x00, 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{{"\tJ\xE3\x81\x9F\xE3\x81\x99\xE3\x81\x91\xE3\x81\xA6\xEF\xBC\x81", "\tEHelp me!", "\tEHilf mir!", "\tEAide-moi !", "\tEAyuda", "\tB\xE6\x95\x91\xE5\x91\xBD\xE5\x95\x8A\xEF\xBC\x81", "\tT\xE6\x95\x91\xE5\x91\xBD\xE5\x95\x8A\xEF\xBC\x81", "\tK\xEB\x8F\x84\xEC\x99\x80\xEC\xA4\x98\xEF\xBC\x81"}, 0xEC, {0x065E, 0x0138, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x02, 0x17, 0x1B, 0x01}, {0x02, 0x2A, 0x1B, 0x00}, {0x31, 0x20, 0x2C, 0x00}, {0xFF, 0x00, 0x00, 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{{"\tJ\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF", "\tEHello", "\tEHallo", "\tESalut", "\tEHola", "\tB\xE4\xBD\xA0\xE5\xA5\xBD", "\tT\xE4\xBD\xA0\xE5\xA5\xBD", "\tK\xEC\x95\x88\xEB\x85\x95"}, 0x28, {0xFFFF, 0x000D, 0xFFFF, 0xFFFF}, {SymbolChatFacePart{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{{"\tJ\xE3\x81\x95\xE3\x82\x88\xE3\x81\x86\xE3\x81\xAA\xE3\x82\x89", "\tEGood-bye", "\tETschus", "\tEAu revoir", "\tEAdios", "\tB\xE5\x86\x8D\xE8\xA7\x81", "\tT\xE5\x86\x8D\xE8\xA6\x8B", "\tK\xEC\x9E\x98\xEA\xB0\x80"}, 0x74, {0x0476, 0x000C, 0xFFFF, 0xFFFF}, {SymbolChatFacePart{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}}},
|
||||
DefaultSymbolChatEntry{{"\tJ\xE3\x81\xB0\xE3\x82\x93\xE3\x81\x96\xE3\x83\xBC\xE3\x81\x84", "\tEHurrah!", "\tEHurra!", "\tEHourra !", "\tEHurra", "\tB\xE4\xB8\x87\xE5\xB2\x81", "\tT\xE8\x90\xAC\xE6\xAD\xB2", "\tK\xEB\xA7\x8C\xEC\x84\xB8"}, 0x28, {0x0362, 0x0362, 0xFFFF, 0xFFFF}, {SymbolChatFacePart{0x09, 0x16, 0x1B, 0x00}, {0x09, 0x2B, 0x1B, 0x01}, {0x37, 0x20, 0x2C, 0x00}, {0xFF, 0x00, 0x00, 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{{"\tJ\xE3\x81\x86\xE3\x81\x87\xEF\xBD\x9E\xE3\x82\x93", "\tECrying", "\tEIch bin sauer!", "\tEJe suis triste", "\tELlanto", "\tB\xE5\x96\x82\xEF\xBD\x9E", "\tT\xE5\x96\x82\xEF\xBD\x9E", "\tK\xEC\x9D\x91~"}, 0x74, {0x074F, 0xFFFF, 0xFFFF, 0xFFFF}, {SymbolChatFacePart{0x06, 0x15, 0x14, 0x00}, {0x06, 0x2B, 0x14, 0x01}, {0x05, 0x18, 0x1F, 0x00}, {0x05, 0x28, 0x1F, 0x01}, {0x21, 0x20, 0x2E, 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{{"\tJ\xE3\x81\x8A\xE3\x81\x93\xE3\x81\xA3\xE3\x81\x9F\xEF\xBC\x81", "\tEI'm angry!", "\tEWeinen", "\tEJe suis en colere !", "\tEEnfado", "\tB\xE7\x94\x9F\xE6\xB0\x94\xE4\xBA\x86\xEF\xBC\x81", "\tT\xE7\x94\x9F\xE6\xB0\xA3\xE4\xBA\x86\xEF\xBC\x81", "\tK\xEB\x82\x98\xEC\x99\x94\xEB\x8B\xA4\xEF\xBC\x81"}, 0x5C, {0x0116, 0x0001, 0xFFFF, 0xFFFF}, {SymbolChatFacePart{0x0B, 0x18, 0x1B, 0x01}, {0x0B, 0x28, 0x1B, 0x00}, {0x33, 0x20, 0x2A, 0x00}, {0xFF, 0x00, 0x00, 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{{"\tJ\xE3\x81\x9F\xE3\x81\x99\xE3\x81\x91\xE3\x81\xA6\xEF\xBC\x81", "\tEHelp me!", "\tEHilf mir!", "\tEAide-moi !", "\tEAyuda", "\tB\xE6\x95\x91\xE5\x91\xBD\xE5\x95\x8A\xEF\xBC\x81", "\tT\xE6\x95\x91\xE5\x91\xBD\xE5\x95\x8A\xEF\xBC\x81", "\tK\xEB\x8F\x84\xEC\x99\x80\xEC\xA4\x98\xEF\xBC\x81"}, 0xEC, {0x065E, 0x0138, 0xFFFF, 0xFFFF}, {SymbolChatFacePart{0x02, 0x17, 0x1B, 0x01}, {0x02, 0x2A, 0x1B, 0x00}, {0x31, 0x20, 0x2C, 0x00}, {0xFF, 0x00, 0x00, 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}}},
|
||||
};
|
||||
|
||||
const array<uint16_t, 20> PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG = {
|
||||
@@ -633,3 +742,109 @@ const array<uint8_t, 0x0038> PSOBBBaseSystemFile::DEFAULT_JOYSTICK_CONFIG = {
|
||||
0x00, 0x00, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
0x08, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00};
|
||||
|
||||
static uint16_t crc16(const void* data, size_t size) {
|
||||
static const uint16_t table[0x100] = {
|
||||
// clang-format off
|
||||
/* 00 */ 0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF,
|
||||
/* 08 */ 0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7,
|
||||
/* 10 */ 0x1081, 0x0108, 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E,
|
||||
/* 18 */ 0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876,
|
||||
/* 20 */ 0x2102, 0x308B, 0x0210, 0x1399, 0x6726, 0x76AF, 0x4434, 0x55BD,
|
||||
/* 28 */ 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5,
|
||||
/* 30 */ 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, 0x54B5, 0x453C,
|
||||
/* 38 */ 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, 0xC974,
|
||||
/* 40 */ 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB,
|
||||
/* 48 */ 0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3,
|
||||
/* 50 */ 0x5285, 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A,
|
||||
/* 58 */ 0xDECD, 0xCF44, 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72,
|
||||
/* 60 */ 0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9,
|
||||
/* 68 */ 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, 0xA96A, 0xB8E3, 0x8A78, 0x9BF1,
|
||||
/* 70 */ 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738,
|
||||
/* 78 */ 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, 0x9AF9, 0x8B70,
|
||||
/* 80 */ 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, 0xF0B7,
|
||||
/* 88 */ 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF,
|
||||
/* 90 */ 0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036,
|
||||
/* 98 */ 0x18C1, 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E,
|
||||
/* A0 */ 0xA50A, 0xB483, 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5,
|
||||
/* A8 */ 0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD,
|
||||
/* B0 */ 0xB58B, 0xA402, 0x9699, 0x8710, 0xF3AF, 0xE226, 0xD0BD, 0xC134,
|
||||
/* B8 */ 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C,
|
||||
/* C0 */ 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, 0xA33A, 0xB2B3,
|
||||
/* C8 */ 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, 0x3EFB,
|
||||
/* D0 */ 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232,
|
||||
/* D8 */ 0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A,
|
||||
/* E0 */ 0xE70E, 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1,
|
||||
/* E8 */ 0x6B46, 0x7ACF, 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9,
|
||||
/* F0 */ 0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330,
|
||||
/* F8 */ 0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78,
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
uint16_t ret = 0xFFFF;
|
||||
StringReader r(data, size);
|
||||
while (!r.eof()) {
|
||||
ret = (ret >> 8) ^ table[r.get_u8() ^ (ret & 0xFF)];
|
||||
}
|
||||
return ret ^ 0xFFFF;
|
||||
}
|
||||
|
||||
string encode_psobb_hangame_credentials(const string& user_id, const string& token, const string& unused) {
|
||||
if (user_id.size() < 4) {
|
||||
throw runtime_error("user_id must be at least 4 characters");
|
||||
}
|
||||
if (user_id.size() > 12) {
|
||||
throw runtime_error("user_id must be at most 12 characters");
|
||||
}
|
||||
if (!ends_with(user_id, "@HG")) {
|
||||
throw runtime_error("user_id must end with \"@HG\"");
|
||||
}
|
||||
if (token.empty()) {
|
||||
throw runtime_error("token must not be empty");
|
||||
}
|
||||
if (token.size() > 8) {
|
||||
throw runtime_error("token must be at most 8 characters");
|
||||
}
|
||||
for (char ch : token) {
|
||||
if (!isdigit(ch)) {
|
||||
throw runtime_error("token must contain only decimal digits");
|
||||
}
|
||||
}
|
||||
if (unused.size() > 0xFF) {
|
||||
throw runtime_error("unused must be at most 255 characters");
|
||||
}
|
||||
|
||||
// The encoded format is:
|
||||
// parray<uint8_t, 4> mask_key; // xor this with all bytes starting with checksum
|
||||
// le_uint16_t checksum; // crc16(&unused, EOF - &unused)
|
||||
// uint8_t unused;
|
||||
// uint8_t user_id_size;
|
||||
// char user_id[user_id_size]; // Length must be in [4, 12] and must end with "@HG"
|
||||
// uint8_t token_size;
|
||||
// char token[token_size]; // Length must be in [1, 8] and must be all decimal digits
|
||||
// uint8_t unused_size;
|
||||
// char unused[unused_size]; // Ignored (possibly email address?)
|
||||
// We'll fill in mask_key and checksum after all the other fields.
|
||||
string data(7, '\0'); // mask_key, checksum, unused
|
||||
data.push_back(user_id.size());
|
||||
data += user_id;
|
||||
data.push_back(token.size());
|
||||
data += token;
|
||||
data.push_back(unused.size());
|
||||
data += unused;
|
||||
|
||||
uint16_t checksum = crc16(data.data() + 6, data.size() - 6);
|
||||
uint32_t timestamp = time(nullptr);
|
||||
data[0] = (timestamp & 0xFF);
|
||||
data[1] = ((timestamp >> 8) & 0xFF);
|
||||
data[2] = ((timestamp >> 16) & 0xFF);
|
||||
data[3] = ((timestamp >> 24) & 0xFF);
|
||||
data[4] = checksum & 0xFF;
|
||||
data[5] = (checksum >> 8) & 0xFF;
|
||||
|
||||
for (size_t z = 0; z < data.size() - 4; z++) {
|
||||
data[z + 4] ^= data[z % 3];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
+306
-259
@@ -29,9 +29,9 @@ struct ShuffleTables {
|
||||
};
|
||||
|
||||
struct PSOVMSFileHeader {
|
||||
/* 0000 */ pstring<TextEncoding::SJIS, 0x10> short_desc;
|
||||
/* 0010 */ pstring<TextEncoding::SJIS, 0x20> long_desc;
|
||||
/* 0030 */ pstring<TextEncoding::SJIS, 0x10> creator_id;
|
||||
/* 0000 */ pstring<TextEncoding::MARKED, 0x10> short_desc;
|
||||
/* 0010 */ pstring<TextEncoding::MARKED, 0x20> long_desc;
|
||||
/* 0030 */ pstring<TextEncoding::MARKED, 0x10> creator_id;
|
||||
/* 0040 */ le_uint16_t num_icons;
|
||||
/* 0042 */ le_uint16_t animation_speed;
|
||||
/* 0044 */ le_uint16_t eyecatch_type;
|
||||
@@ -43,7 +43,7 @@ struct PSOVMSFileHeader {
|
||||
/* 0080 */ // parray<uint8_t, num_icons> icon;
|
||||
|
||||
bool checksum_correct() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOVMSFileHeader, 0x80);
|
||||
|
||||
struct PSOGCIFileHeader {
|
||||
// Every PSOGC save file begins with a PSOGCIFileHeader. The first 0x40 bytes
|
||||
@@ -57,7 +57,7 @@ struct PSOGCIFileHeader {
|
||||
// There is a structure for this part of the header, but we don't use it.
|
||||
/* 0006 */ uint8_t unused;
|
||||
/* 0007 */ uint8_t image_flags;
|
||||
/* 0008 */ pstring<TextEncoding::SJIS, 0x20> internal_file_name;
|
||||
/* 0008 */ pstring<TextEncoding::MARKED, 0x20> internal_file_name;
|
||||
/* 0028 */ be_uint32_t modification_time;
|
||||
/* 002C */ be_uint32_t image_data_offset;
|
||||
/* 0030 */ be_uint16_t icon_formats;
|
||||
@@ -70,9 +70,9 @@ struct PSOGCIFileHeader {
|
||||
/* 003C */ be_uint32_t comment_offset;
|
||||
// GCI header ends here (and memcard file data begins here)
|
||||
// game_name is e.g. "PSO EPISODE I & II" or "PSO EPISODE III"
|
||||
/* 0040 */ pstring<TextEncoding::SJIS, 0x1C> game_name;
|
||||
/* 0040 */ pstring<TextEncoding::MARKED, 0x1C> game_name;
|
||||
/* 005C */ be_uint32_t embedded_seed; // Used in some of Ralf's quest packs
|
||||
/* 0060 */ pstring<TextEncoding::SJIS, 0x20> file_name;
|
||||
/* 0060 */ pstring<TextEncoding::MARKED, 0x20> file_name;
|
||||
/* 0080 */ parray<uint8_t, 0x1800> banner;
|
||||
/* 1880 */ parray<uint8_t, 0x800> icon;
|
||||
// data_size specifies the number of bytes remaining in the file. In all cases
|
||||
@@ -90,7 +90,7 @@ struct PSOGCIFileHeader {
|
||||
bool is_ep12() const;
|
||||
bool is_ep3() const;
|
||||
bool is_nte() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOGCIFileHeader, 0x2088);
|
||||
|
||||
struct PSOGCSystemFile {
|
||||
/* 0000 */ be_uint32_t checksum;
|
||||
@@ -110,7 +110,7 @@ struct PSOGCSystemFile {
|
||||
// Guild Card files.
|
||||
/* 0118 */ be_uint32_t creation_timestamp;
|
||||
/* 011C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOGCSystemFile, 0x11C);
|
||||
|
||||
struct PSOGCEp3SystemFile {
|
||||
/* 0000 */ PSOGCSystemFile base;
|
||||
@@ -118,196 +118,99 @@ struct PSOGCEp3SystemFile {
|
||||
/* 011D */ parray<uint8_t, 11> unknown_a2;
|
||||
/* 0128 */ be_uint32_t unknown_a3;
|
||||
/* 012C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOGCEp3SystemFile, 0x12C);
|
||||
|
||||
struct PSOBBMinimalSystemFile {
|
||||
/* 0000 */ be_uint32_t checksum = 0;
|
||||
/* 0004 */ be_int16_t music_volume = 0;
|
||||
/* 0006 */ int8_t sound_volume = 0;
|
||||
/* 0007 */ uint8_t language = 0;
|
||||
/* 0008 */ be_int32_t server_time_delta_frames = 1728000;
|
||||
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
|
||||
/* 000E */ be_uint16_t surround_sound_enabled = 0;
|
||||
/* 0010 */ parray<uint8_t, 0x0100> event_flags;
|
||||
/* 0110 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 0114 */
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian, TextEncoding Encoding, size_t NameLength>
|
||||
struct SaveFileSymbolChatEntryT {
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
|
||||
struct PSOBBTeamMembership {
|
||||
/* 0000 */ le_uint32_t team_master_guild_card_number = 0;
|
||||
/* 0004 */ le_uint32_t team_id = 0;
|
||||
/* 0008 */ le_uint32_t unknown_a5 = 0;
|
||||
/* 000C */ le_uint32_t unknown_a6 = 0;
|
||||
/* 0010 */ uint8_t privilege_level = 0;
|
||||
/* 0011 */ uint8_t unknown_a7 = 0;
|
||||
/* 0012 */ uint8_t unknown_a8 = 0;
|
||||
/* 0013 */ uint8_t unknown_a9 = 0;
|
||||
/* 0014 */ pstring<TextEncoding::UTF16_ALWAYS_MARKED, 0x10> team_name;
|
||||
/* 0034 */ parray<le_uint16_t, 0x20 * 0x20> flag_data;
|
||||
/* 0834 */ le_uint32_t reward_flags = 0;
|
||||
/* 0838 */
|
||||
/* PC:GC:BB */
|
||||
/* 00:00:00 */ U32T present;
|
||||
/* 04:04:04 */ pstring<Encoding, NameLength> name;
|
||||
/* 34:1C:2C */ SymbolChatT<IsBigEndian> spec;
|
||||
/* 70:58:68 */
|
||||
} __packed__;
|
||||
using SaveFileSymbolChatEntryPC = SaveFileSymbolChatEntryT<false, TextEncoding::UTF16, 0x18>;
|
||||
using SaveFileSymbolChatEntryGC = SaveFileSymbolChatEntryT<true, TextEncoding::MARKED, 0x18>;
|
||||
using SaveFileSymbolChatEntryBB = SaveFileSymbolChatEntryT<false, TextEncoding::UTF16, 0x14>;
|
||||
check_struct_size(SaveFileSymbolChatEntryPC, 0x70);
|
||||
check_struct_size(SaveFileSymbolChatEntryGC, 0x58);
|
||||
check_struct_size(SaveFileSymbolChatEntryBB, 0x68);
|
||||
|
||||
PSOBBTeamMembership() = default;
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian>
|
||||
struct WordSelectMessageT {
|
||||
using U16T = std::conditional_t<IsBigEndian, be_uint16_t, le_uint16_t>;
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
|
||||
struct PSOBBBaseSystemFile {
|
||||
/* 0000 */ PSOBBMinimalSystemFile base;
|
||||
/* 0114 */ parray<uint8_t, 0x016C> key_config;
|
||||
/* 0280 */ parray<uint8_t, 0x0038> joystick_config;
|
||||
/* 02B8 */
|
||||
U16T num_tokens = 0;
|
||||
U16T target_type = 0;
|
||||
parray<U16T, 8> tokens;
|
||||
U32T numeric_parameter = 0;
|
||||
U32T unknown_a4 = 0;
|
||||
|
||||
static const std::array<uint8_t, 0x016C> DEFAULT_KEY_CONFIG;
|
||||
static const std::array<uint8_t, 0x0038> DEFAULT_JOYSTICK_CONFIG;
|
||||
operator WordSelectMessageT<!IsBigEndian>() const {
|
||||
WordSelectMessageT<!IsBigEndian> ret;
|
||||
ret.num_tokens = this->num_tokens.load();
|
||||
ret.target_type = this->target_type.load();
|
||||
for (size_t z = 0; z < this->tokens.size(); z++) {
|
||||
ret.tokens[z] = this->tokens[z].load();
|
||||
}
|
||||
ret.numeric_parameter = this->numeric_parameter.load();
|
||||
ret.unknown_a4 = this->unknown_a4.load();
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using WordSelectMessage = WordSelectMessageT<false>;
|
||||
using WordSelectMessageBE = WordSelectMessageT<true>;
|
||||
check_struct_size(WordSelectMessage, 0x1C);
|
||||
check_struct_size(WordSelectMessageBE, 0x1C);
|
||||
|
||||
PSOBBBaseSystemFile();
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian, TextEncoding Encoding>
|
||||
struct SaveFileChatShortcutEntryT {
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
|
||||
struct PSOBBFullSystemFile {
|
||||
/* 0000 */ PSOBBBaseSystemFile base;
|
||||
/* 02B8 */ PSOBBTeamMembership team_membership;
|
||||
/* 0AF0 */
|
||||
union Definition {
|
||||
pstring<Encoding, 0x50> text;
|
||||
WordSelectMessageT<IsBigEndian> word_select;
|
||||
SymbolChatT<IsBigEndian> symbol_chat;
|
||||
|
||||
PSOBBFullSystemFile() = default;
|
||||
} __attribute__((packed));
|
||||
Definition() : text() {}
|
||||
Definition(const Definition& other) : text(other.text) {}
|
||||
Definition& operator=(const Definition& other) {
|
||||
this->text = other.text;
|
||||
return *this;
|
||||
}
|
||||
} __packed__;
|
||||
|
||||
struct PSOBBCharacterFile {
|
||||
struct SymbolChatEntry {
|
||||
/* 00 */ le_uint32_t present = 0;
|
||||
/* 04 */ pstring<TextEncoding::UTF16_ALWAYS_MARKED, 0x14> name;
|
||||
/* 2C */ SymbolChat data;
|
||||
/* 68 */
|
||||
} __attribute__((packed));
|
||||
/* GC:BB */
|
||||
/* 00:00 */ U32T type; // 1 = text, 2 = word select, 3 = symbol chat
|
||||
/* 04:04 */ Definition definition;
|
||||
/* 54:A4 */
|
||||
|
||||
struct DefaultSymbolChatEntry {
|
||||
std::array<const char*, 8> language_to_name;
|
||||
uint32_t spec;
|
||||
std::array<uint16_t, 4> corner_objects;
|
||||
std::array<SymbolChat::FacePart, 12> face_parts;
|
||||
|
||||
SymbolChatEntry to_entry(uint8_t language) const;
|
||||
};
|
||||
|
||||
/* 0000 */ PlayerInventory inventory;
|
||||
/* 034C */ PlayerDispDataBB disp;
|
||||
/* 04DC */ le_uint32_t flags = 0;
|
||||
/* 04E0 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 04E4 */ le_uint32_t signature = 0xC87ED5B1;
|
||||
/* 04E8 */ le_uint32_t play_time_seconds = 0;
|
||||
/* 04EC */ le_uint32_t option_flags = 0x00040058;
|
||||
/* 04F0 */ parray<uint8_t, 4> unknown_a2;
|
||||
/* 04F4 */ QuestFlags quest_flags;
|
||||
/* 06F4 */ le_uint32_t death_count = 0;
|
||||
/* 06F8 */ PlayerBank bank;
|
||||
/* 19C0 */ GuildCardBB guild_card;
|
||||
/* 1AC8 */ le_uint32_t unknown_a3 = 0;
|
||||
/* 1ACC */ parray<SymbolChatEntry, 12> symbol_chats;
|
||||
/* 1FAC */ parray<uint8_t, 0x0A40> shortcuts;
|
||||
/* 29EC */ pstring<TextEncoding::UTF16, 0x00AC> auto_reply;
|
||||
/* 2B44 */ pstring<TextEncoding::UTF16, 0x00AC> info_board;
|
||||
/* 2C9C */ PlayerRecords_Battle<false> battle_records;
|
||||
/* 2CB4 */ parray<uint8_t, 4> unknown_a4;
|
||||
/* 2CB8 */ PlayerRecordsBB_Challenge challenge_records;
|
||||
/* 2DF8 */ parray<le_uint16_t, 0x0014> tech_menu_config;
|
||||
/* 2E20 */ ChoiceSearchConfig choice_search_config;
|
||||
/* 2E38 */ parray<uint8_t, 0x0010> unknown_a6;
|
||||
/* 2E48 */ parray<le_uint32_t, 0x0010> quest_counters;
|
||||
/* 2E88 */ parray<uint8_t, 0x1C> unknown_a7;
|
||||
/* 2EA4 */
|
||||
|
||||
static const std::array<DefaultSymbolChatEntry, 6> DEFAULT_SYMBOL_CHATS;
|
||||
static const std::array<uint16_t, 20> DEFAULT_TECH_MENU_CONFIG;
|
||||
|
||||
PSOBBCharacterFile() = default;
|
||||
|
||||
PlayerDispDataBBPreview to_preview() const;
|
||||
|
||||
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,
|
||||
const PlayerDispDataBBPreview& preview,
|
||||
std::shared_ptr<const LevelTable> level_table);
|
||||
|
||||
void add_item(const ItemData& item, const ItemData::StackLimits& limits);
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits);
|
||||
void add_meseta(uint32_t amount);
|
||||
void remove_meseta(uint32_t amount, bool allow_overdraft);
|
||||
|
||||
uint8_t get_technique_level(uint8_t which) const; // Returns FF or 00-1D
|
||||
void set_technique_level(uint8_t which, uint8_t level);
|
||||
|
||||
enum class MaterialType : int8_t {
|
||||
HP = -2,
|
||||
TP = -1,
|
||||
POWER = 0,
|
||||
MIND = 1,
|
||||
EVADE = 2,
|
||||
DEF = 3,
|
||||
LUCK = 4,
|
||||
};
|
||||
|
||||
uint8_t get_material_usage(MaterialType which) const;
|
||||
void set_material_usage(MaterialType which, uint8_t usage);
|
||||
void clear_all_material_usage();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOBBGuildCardFile {
|
||||
struct Entry {
|
||||
/* 0000 */ GuildCardBB data;
|
||||
/* 0108 */ pstring<TextEncoding::UTF16, 0x58> comment;
|
||||
/* 01B8 */ parray<uint8_t, 0x4> unknown_a1;
|
||||
/* 01BC */
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
/* 0000 */ PSOBBMinimalSystemFile system_file;
|
||||
/* 0114 */ parray<GuildCardBB, 0x1C> blocked;
|
||||
/* 1DF4 */ parray<uint8_t, 0x180> unknown_a2;
|
||||
/* 1F74 */ parray<Entry, 0x69> entries;
|
||||
/* D590 */
|
||||
|
||||
PSOBBGuildCardFile() = default;
|
||||
|
||||
uint32_t checksum() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOGCSaveFileSymbolChatEntry {
|
||||
/* 00 */ be_uint32_t present;
|
||||
/* 04 */ pstring<TextEncoding::SJIS, 0x18> name;
|
||||
/* 1C */ be_uint32_t spec;
|
||||
struct CornerObject {
|
||||
uint8_t type;
|
||||
uint8_t flags_color;
|
||||
} __attribute__((packed));
|
||||
/* 20 */ parray<CornerObject, 4> corner_objects;
|
||||
struct FacePart {
|
||||
uint8_t type;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t flags;
|
||||
} __attribute__((packed));
|
||||
/* 28 */ parray<FacePart, 12> face_parts;
|
||||
/* 58 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOPCSaveFileSymbolChatEntry {
|
||||
/* 00 */ le_uint32_t present;
|
||||
/* 04 */ pstring<TextEncoding::UTF16, 0x18> name;
|
||||
/* 34 */ SymbolChat data;
|
||||
/* 70 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOGCSaveFileChatShortcutEntry {
|
||||
/* 00 */ be_uint32_t present_type;
|
||||
/* 04 */ parray<uint8_t, 0x50> definition;
|
||||
/* 54 */
|
||||
} __attribute__((packed));
|
||||
template <bool RetIsBigEndian, TextEncoding RetEncoding>
|
||||
SaveFileChatShortcutEntryT<RetIsBigEndian, RetEncoding> convert(uint8_t language) const {
|
||||
SaveFileChatShortcutEntryT<RetIsBigEndian, RetEncoding> ret;
|
||||
ret.type = this->type.load();
|
||||
switch (ret.type) {
|
||||
case 1:
|
||||
ret.definition.text.encode(this->definition.text.decode(language), language);
|
||||
break;
|
||||
case 2:
|
||||
// TODO: We should translate the message across PSO versions if
|
||||
// possible, but this is a lossy process :|
|
||||
ret.definition.word_select = this->definition.word_select;
|
||||
break;
|
||||
case 3:
|
||||
ret.definition.symbol_chat = this->definition.symbol_chat;
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
} __packed__;
|
||||
using SaveFileShortcutEntryGC = SaveFileChatShortcutEntryT<true, TextEncoding::MARKED>;
|
||||
using SaveFileShortcutEntryBB = SaveFileChatShortcutEntryT<false, TextEncoding::UTF16>;
|
||||
check_struct_size(SaveFileShortcutEntryGC, 0x54);
|
||||
check_struct_size(SaveFileShortcutEntryBB, 0xA4);
|
||||
|
||||
struct PSOGCCharacterFile {
|
||||
/* 00000 */ be_uint32_t checksum;
|
||||
@@ -315,18 +218,18 @@ struct PSOGCCharacterFile {
|
||||
// This structure is internally split into two by the game. The offsets here
|
||||
// are relative to the start of this structure (first column), and relative
|
||||
// to the start of the second internal structure (second column).
|
||||
/* 0000:---- */ PlayerInventory inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
|
||||
/* 0000:---- */ PlayerInventoryBE inventory;
|
||||
/* 034C:---- */ PlayerDispDataDCPCV3BE disp;
|
||||
// Known bits in the flags field:
|
||||
// 00000001: Character was not saved after disconnecting (and the message
|
||||
// about items being deleted is shown in the select menu)
|
||||
// 00000002: Used for something, but it's not known what it does
|
||||
/* 041C:0000 */ be_uint32_t flags;
|
||||
/* 0420:0004 */ be_uint32_t creation_timestamp;
|
||||
/* 041C:0000 */ be_uint32_t flags = 0;
|
||||
/* 0420:0004 */ be_uint32_t creation_timestamp = 0;
|
||||
// The signature field holds the value 0xA205B064, which is 2718281828 in
|
||||
// decimal - approximately e * 10^9. It's unknown why Sega chose this value.
|
||||
/* 0424:0008 */ be_uint32_t signature;
|
||||
/* 0428:000C */ be_uint32_t play_time_seconds;
|
||||
/* 0424:0008 */ be_uint32_t signature = 0xA205B064;
|
||||
/* 0428:000C */ be_uint32_t play_time_seconds = 0;
|
||||
// This field is a collection of several flags and small values. The known
|
||||
// fields are:
|
||||
// ------zA BCDEFG-- HHHIIIJJ KLMNOPQR
|
||||
@@ -344,31 +247,32 @@ struct PSOGCCharacterFile {
|
||||
// P = Cursor position (0 = saved; 1 = non-saved)
|
||||
// Q = Button config (0 = normal; 1 = L/R reversed)
|
||||
// R = Map direction (0 = non-fixed; 1 = fixed)
|
||||
/* 042C:0010 */ be_uint32_t option_flags;
|
||||
/* 0430:0014 */ be_uint32_t save_count;
|
||||
/* 0434:0018 */ parray<uint8_t, 0x1C> unknown_a4;
|
||||
/* 0450:0034 */ parray<uint8_t, 0x10> unknown_a5;
|
||||
/* 042C:0010 */ be_uint32_t option_flags = 0x00040058;
|
||||
/* 0430:0014 */ be_uint32_t save_count = 0;
|
||||
/* 0434:0018 */ parray<uint8_t, 0x1C> unknown_a2;
|
||||
/* 0450:0034 */ parray<uint8_t, 0x10> unknown_a3;
|
||||
/* 0460:0044 */ QuestFlags quest_flags;
|
||||
/* 0660:0244 */ be_uint32_t death_count;
|
||||
/* 0664:0248 */ PlayerBank bank;
|
||||
/* 192C:1510 */ GuildCardGC guild_card;
|
||||
/* 19BC:15A0 */ parray<PSOGCSaveFileSymbolChatEntry, 12> symbol_chats;
|
||||
/* 1DDC:19C0 */ parray<PSOGCSaveFileChatShortcutEntry, 20> chat_shortcuts;
|
||||
/* 246C:2050 */ pstring<TextEncoding::SJIS, 0xAC> auto_reply;
|
||||
/* 2518:20FC */ pstring<TextEncoding::SJIS, 0xAC> info_board;
|
||||
/* 25C4:21A8 */ PlayerRecords_Battle<true> battle_records;
|
||||
/* 25DC:21C0 */ parray<uint8_t, 4> unknown_a2;
|
||||
/* 25E0:21C4 */ PlayerRecordsV3_Challenge<true> challenge_records;
|
||||
/* 0660:0244 */ be_uint32_t death_count = 0;
|
||||
/* 0664:0248 */ PlayerBankBE bank;
|
||||
/* 192C:1510 */ GuildCardGCBE guild_card;
|
||||
/* 19BC:15A0 */ parray<SaveFileSymbolChatEntryGC, 12> symbol_chats;
|
||||
/* 1DDC:19C0 */ parray<SaveFileShortcutEntryGC, 20> shortcuts;
|
||||
/* 246C:2050 */ pstring<TextEncoding::MARKED, 0xAC> auto_reply;
|
||||
/* 2518:20FC */ pstring<TextEncoding::MARKED, 0xAC> info_board;
|
||||
/* 25C4:21A8 */ PlayerRecordsBattleBE battle_records;
|
||||
/* 25DC:21C0 */ parray<uint8_t, 4> unknown_a4;
|
||||
/* 25E0:21C4 */ PlayerRecordsChallengeV3BE challenge_records;
|
||||
/* 26E0:22C4 */ parray<be_uint16_t, 20> tech_menu_shortcut_entries;
|
||||
/* 2708:22EC */ parray<uint8_t, 0x28> unknown_a6;
|
||||
/* 2708:22EC */ ChoiceSearchConfigBE choice_search_config;
|
||||
/* 2720:2304 */ parray<uint8_t, 0x10> unknown_a6;
|
||||
/* 2730:2314 */ parray<be_uint32_t, 0x10> quest_counters;
|
||||
/* 2770:2354 */ PlayerRecords_Battle<true> offline_battle_records;
|
||||
/* 2788:236C */ parray<uint8_t, 4> unknown_f5;
|
||||
/* 278C:2370 */ be_uint32_t unknown_f6;
|
||||
/* 2790:2374 */ be_uint32_t unknown_f7;
|
||||
/* 2794:2378 */ be_uint32_t unknown_f8;
|
||||
/* 2770:2354 */ PlayerRecordsBattleBE offline_battle_records;
|
||||
/* 2788:236C */ parray<uint8_t, 4> unknown_a7;
|
||||
/* 278C:2370 */ be_uint32_t unknown_f6 = 0;
|
||||
/* 2790:2374 */ be_uint32_t unknown_f7 = 0;
|
||||
/* 2794:2378 */ be_uint32_t unknown_f8 = 0;
|
||||
/* 2798:237C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Character, 0x2798);
|
||||
/* 00004 */ parray<Character, 7> characters;
|
||||
/* 1152C */ pstring<TextEncoding::ASCII, 0x10> serial_number; // As %08X (not decimal)
|
||||
/* 1153C */ pstring<TextEncoding::ASCII, 0x10> access_key;
|
||||
@@ -377,7 +281,7 @@ struct PSOGCCharacterFile {
|
||||
/* 11564 */ be_uint32_t save_count;
|
||||
/* 11568 */ be_uint32_t round2_seed;
|
||||
/* 1156C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOGCCharacterFile, 0x1156C);
|
||||
|
||||
struct PSOGCEp3CharacterFile {
|
||||
/* 00000 */ be_uint32_t checksum; // crc32 of this field (as 0) through end of struct
|
||||
@@ -407,22 +311,22 @@ struct PSOGCEp3CharacterFile {
|
||||
// remove the bank in Ep3 because they would have to change too much code.
|
||||
/* 0864:0448 */ be_uint32_t num_bank_items;
|
||||
/* 0868:044C */ be_uint32_t bank_meseta;
|
||||
/* 086C:0450 */ parray<PlayerBankItem, 4> bank_items;
|
||||
/* 08CC:04B0 */ GuildCardGC guild_card;
|
||||
/* 095C:0540 */ parray<PSOGCSaveFileSymbolChatEntry, 12> symbol_chats;
|
||||
/* 0D7C:0960 */ parray<PSOGCSaveFileChatShortcutEntry, 20> chat_shortcuts;
|
||||
/* 140C:0FF0 */ pstring<TextEncoding::SJIS, 0xAC> auto_reply;
|
||||
/* 14B8:109C */ pstring<TextEncoding::SJIS, 0xAC> info_board;
|
||||
/* 086C:0450 */ parray<PlayerBankItemBE, 4> bank_items;
|
||||
/* 08CC:04B0 */ GuildCardGCBE guild_card;
|
||||
/* 095C:0540 */ parray<SaveFileSymbolChatEntryGC, 12> symbol_chats;
|
||||
/* 0D7C:0960 */ parray<SaveFileShortcutEntryGC, 20> chat_shortcuts;
|
||||
/* 140C:0FF0 */ pstring<TextEncoding::MARKED, 0xAC> auto_reply;
|
||||
/* 14B8:109C */ pstring<TextEncoding::MARKED, 0xAC> info_board;
|
||||
// In this struct, place_counts[0] is win_count and [1] is loss_count
|
||||
/* 1564:1148 */ PlayerRecords_Battle<true> battle_records;
|
||||
/* 1564:1148 */ PlayerRecordsBattleBE battle_records;
|
||||
/* 157C:1160 */ parray<uint8_t, 4> unknown_a10;
|
||||
/* 1580:1164 */ PlayerRecordsV3_Challenge<true>::Stats challenge_record_stats;
|
||||
/* 1580:1164 */ PlayerRecordsChallengeV3BE::Stats challenge_record_stats;
|
||||
/* 1658:123C */ Episode3::PlayerConfig ep3_config;
|
||||
/* 39A8:358C */ be_uint32_t unknown_a11;
|
||||
/* 39AC:3590 */ be_uint32_t unknown_a12;
|
||||
/* 39B0:3594 */ be_uint32_t unknown_a13;
|
||||
/* 39B4:3598 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Character, 0x39B4);
|
||||
/* 00004 */ parray<Character, 7> characters;
|
||||
/* 193F0 */ pstring<TextEncoding::ASCII, 0x10> serial_number; // As %08X (not decimal)
|
||||
/* 19400 */ pstring<TextEncoding::ASCII, 0x10> access_key; // As 12 ASCII characters (decimal)
|
||||
@@ -445,39 +349,26 @@ struct PSOGCEp3CharacterFile {
|
||||
/* 1942C */ parray<uint8_t, 0x80> card_rank_override_flags;
|
||||
/* 194AC */ be_uint32_t round2_seed;
|
||||
/* 194B0 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOGCEp3CharacterFile, 0x194B0);
|
||||
|
||||
struct PSOGCGuildCardFile {
|
||||
/* 0000 */ be_uint32_t checksum;
|
||||
/* 0004 */ parray<uint8_t, 0xC0> unknown_a1;
|
||||
struct GuildCardBE {
|
||||
// Note: This struct (up through offset 0x90) is identical to GuildCardGC
|
||||
// except for the 32-bit fields, which are big-endian here.
|
||||
/* 0000 */ be_uint32_t player_tag; // == 0x00000001 (not 0x00010000)
|
||||
/* 0004 */ be_uint32_t guild_card_number;
|
||||
/* 0008 */ pstring<TextEncoding::ASCII, 0x18> name;
|
||||
/* 0020 */ pstring<TextEncoding::MARKED, 0x6C> description;
|
||||
/* 008C */ uint8_t present;
|
||||
/* 008D */ uint8_t language;
|
||||
/* 008E */ uint8_t section_id;
|
||||
/* 008F */ uint8_t char_class;
|
||||
/* 0090 */
|
||||
} __attribute__((packed));
|
||||
struct GuildCardEntry {
|
||||
/* 0000 */ GuildCardBE base;
|
||||
/* 0000 */ GuildCardGCBE base;
|
||||
/* 0090 */ uint8_t unknown_a1;
|
||||
/* 0091 */ uint8_t unknown_a2;
|
||||
/* 0092 */ uint8_t unknown_a3;
|
||||
/* 0093 */ uint8_t unknown_a4;
|
||||
/* 0094 */ pstring<TextEncoding::MARKED, 0x6C> comment;
|
||||
/* 0100 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(GuildCardEntry, 0x100);
|
||||
/* 00C4 */ parray<GuildCardEntry, 0xD2> entries;
|
||||
/* D2C4 */ parray<GuildCardBE, 0x1C> blocked_senders;
|
||||
/* D2C4 */ parray<GuildCardGCBE, 0x1C> blocked_senders;
|
||||
/* E284 */ be_uint32_t creation_timestamp;
|
||||
/* E288 */ be_uint32_t round2_seed;
|
||||
/* E28C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOGCGuildCardFile, 0xE28C);
|
||||
|
||||
struct PSOGCSnapshotFile {
|
||||
/* 00000 */ be_uint32_t checksum;
|
||||
@@ -496,7 +387,7 @@ struct PSOGCSnapshotFile {
|
||||
|
||||
bool checksum_correct() const;
|
||||
Image decode_image() const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOGCSnapshotFile, 0x1818C);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
std::string decrypt_data_section(const void* data_section, size_t size, uint32_t round1_seed, size_t max_decrypt_bytes = 0) {
|
||||
@@ -680,7 +571,7 @@ struct PSOPCCreationTimeFile { // PSO______FLS
|
||||
/* 0624 */ le_uint32_t creation_timestamp;
|
||||
/* 0628 */ parray<uint8_t, 0xDD8> unused2;
|
||||
/* 1400 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOPCCreationTimeFile, 0x1400);
|
||||
|
||||
struct PSOPCSystemFile { // PSO______COM
|
||||
/* 0000 */ le_uint32_t checksum;
|
||||
@@ -695,7 +586,7 @@ struct PSOPCSystemFile { // PSO______COM
|
||||
/* 012C */ le_uint32_t round1_seed;
|
||||
/* 0130 */ parray<uint8_t, 0xD0> end_padding;
|
||||
/* 0200 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOPCSystemFile, 0x200);
|
||||
|
||||
struct PSOPCGuildCardFile { // PSO______GUD
|
||||
/* 0000 */ le_uint32_t checksum;
|
||||
@@ -705,7 +596,7 @@ struct PSOPCGuildCardFile { // PSO______GUD
|
||||
/* 7988 */ le_uint32_t round2_seed;
|
||||
/* 798C */ parray<uint8_t, 0x74> end_padding;
|
||||
/* 7A00 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOPCGuildCardFile, 0x7A00);
|
||||
|
||||
struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD
|
||||
/* 00000 */ le_uint32_t signature; // 'CAEN' (stored as 4E 45 41 43)
|
||||
@@ -728,7 +619,7 @@ struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD
|
||||
// TODO: Figure out what this is. On GC, this is where the bank data goes.
|
||||
/* 0438 */ parray<uint8_t, 0x7D4> unknown_a2;
|
||||
/* 0C0C */ GuildCardPC guild_card;
|
||||
/* 0CFC */ parray<PSOPCSaveFileSymbolChatEntry, 12> symbol_chats;
|
||||
/* 0CFC */ parray<SaveFileSymbolChatEntryPC, 12> symbol_chats;
|
||||
// TODO: Figure out what this is. On GC, this is where chat shortcuts and
|
||||
// challenge/battle records go.
|
||||
/* 123C */ parray<uint8_t, 0xAA0> unknown_a3;
|
||||
@@ -738,14 +629,168 @@ struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD
|
||||
/* 1D40 */ pstring<TextEncoding::ASCII, 0x10> access_key; // As decimal
|
||||
/* 1D50 */ le_uint32_t round2_seed;
|
||||
/* 1D54 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(Character, 0x1D54);
|
||||
/* 0004 */ Character character;
|
||||
/* 1D58 */ parray<uint8_t, 0x3C> unused;
|
||||
/* 1D94 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(CharacterEntry, 0x1D94);
|
||||
/* 00440 */ parray<CharacterEntry, 0x80> entries;
|
||||
/* ECE40 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(PSOPCCharacterFile, 0xECE40);
|
||||
|
||||
struct PSOBBMinimalSystemFile {
|
||||
/* 0000 */ be_uint32_t checksum = 0;
|
||||
/* 0004 */ be_int16_t music_volume = 0;
|
||||
/* 0006 */ int8_t sound_volume = 0;
|
||||
/* 0007 */ uint8_t language = 0;
|
||||
/* 0008 */ be_int32_t server_time_delta_frames = 1728000;
|
||||
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
|
||||
/* 000E */ be_uint16_t surround_sound_enabled = 0;
|
||||
/* 0010 */ parray<uint8_t, 0x0100> event_flags;
|
||||
/* 0110 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 0114 */
|
||||
} __packed_ws__(PSOBBMinimalSystemFile, 0x114);
|
||||
|
||||
struct PSOBBTeamMembership {
|
||||
/* 0000 */ le_uint32_t team_master_guild_card_number = 0;
|
||||
/* 0004 */ le_uint32_t team_id = 0;
|
||||
/* 0008 */ le_uint32_t unknown_a5 = 0;
|
||||
/* 000C */ le_uint32_t unknown_a6 = 0;
|
||||
/* 0010 */ uint8_t privilege_level = 0;
|
||||
/* 0011 */ uint8_t unknown_a7 = 0;
|
||||
/* 0012 */ uint8_t unknown_a8 = 0;
|
||||
/* 0013 */ uint8_t unknown_a9 = 0;
|
||||
/* 0014 */ pstring<TextEncoding::UTF16_ALWAYS_MARKED, 0x10> team_name;
|
||||
/* 0034 */ parray<le_uint16_t, 0x20 * 0x20> flag_data;
|
||||
/* 0834 */ le_uint32_t reward_flags = 0;
|
||||
/* 0838 */
|
||||
|
||||
PSOBBTeamMembership() = default;
|
||||
} __packed_ws__(PSOBBTeamMembership, 0x838);
|
||||
|
||||
struct PSOBBBaseSystemFile {
|
||||
/* 0000 */ PSOBBMinimalSystemFile base;
|
||||
/* 0114 */ parray<uint8_t, 0x016C> key_config;
|
||||
/* 0280 */ parray<uint8_t, 0x0038> joystick_config;
|
||||
/* 02B8 */
|
||||
|
||||
static const std::array<uint8_t, 0x016C> DEFAULT_KEY_CONFIG;
|
||||
static const std::array<uint8_t, 0x0038> DEFAULT_JOYSTICK_CONFIG;
|
||||
|
||||
PSOBBBaseSystemFile();
|
||||
} __packed_ws__(PSOBBBaseSystemFile, 0x2B8);
|
||||
|
||||
struct PSOBBFullSystemFile {
|
||||
/* 0000 */ PSOBBBaseSystemFile base;
|
||||
/* 02B8 */ PSOBBTeamMembership team_membership;
|
||||
/* 0AF0 */
|
||||
|
||||
PSOBBFullSystemFile() = default;
|
||||
} __packed_ws__(PSOBBFullSystemFile, 0xAF0);
|
||||
|
||||
struct PSOBBCharacterFile {
|
||||
struct DefaultSymbolChatEntry {
|
||||
std::array<const char*, 8> language_to_name;
|
||||
uint32_t spec;
|
||||
std::array<uint16_t, 4> corner_objects;
|
||||
std::array<SymbolChatFacePart, 12> face_parts;
|
||||
|
||||
SaveFileSymbolChatEntryBB to_entry(uint8_t language) const;
|
||||
};
|
||||
|
||||
/* 0000 */ PlayerInventory inventory;
|
||||
/* 034C */ PlayerDispDataBB disp;
|
||||
/* 04DC */ le_uint32_t flags = 0;
|
||||
/* 04E0 */ le_uint32_t creation_timestamp = 0;
|
||||
/* 04E4 */ le_uint32_t signature = 0xC87ED5B1;
|
||||
/* 04E8 */ le_uint32_t play_time_seconds = 0;
|
||||
/* 04EC */ le_uint32_t option_flags = 0x00040058;
|
||||
/* 04F0 */ le_uint32_t save_count = 0;
|
||||
/* 04F4 */ QuestFlags quest_flags;
|
||||
/* 06F4 */ le_uint32_t death_count = 0;
|
||||
/* 06F8 */ PlayerBank bank;
|
||||
/* 19C0 */ GuildCardBB guild_card;
|
||||
/* 1AC8 */ le_uint32_t unknown_a3 = 0;
|
||||
/* 1ACC */ parray<SaveFileSymbolChatEntryBB, 0x0C> symbol_chats;
|
||||
/* 1FAC */ parray<SaveFileShortcutEntryBB, 0x10> shortcuts;
|
||||
/* 29EC */ pstring<TextEncoding::UTF16, 0x00AC> auto_reply;
|
||||
/* 2B44 */ pstring<TextEncoding::UTF16, 0x00AC> info_board;
|
||||
/* 2C9C */ PlayerRecordsBattle battle_records;
|
||||
/* 2CB4 */ parray<uint8_t, 4> unknown_a4;
|
||||
/* 2CB8 */ PlayerRecordsChallengeBB challenge_records;
|
||||
/* 2DF8 */ parray<le_uint16_t, 0x0014> tech_menu_shortcut_entries;
|
||||
/* 2E20 */ ChoiceSearchConfig choice_search_config;
|
||||
/* 2E38 */ parray<uint8_t, 0x0010> unknown_a6;
|
||||
/* 2E48 */ parray<le_uint32_t, 0x0010> quest_counters;
|
||||
/* 2E88 */ PlayerRecordsBattle offline_battle_records;
|
||||
/* 2EA0 */ parray<uint8_t, 4> unknown_a7;
|
||||
/* 2EA4 */
|
||||
|
||||
static const std::array<DefaultSymbolChatEntry, 6> DEFAULT_SYMBOL_CHATS;
|
||||
static const std::array<uint16_t, 20> DEFAULT_TECH_MENU_CONFIG;
|
||||
|
||||
PSOBBCharacterFile() = default;
|
||||
|
||||
PlayerDispDataBBPreview to_preview() const;
|
||||
|
||||
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,
|
||||
const PlayerDispDataBBPreview& preview,
|
||||
std::shared_ptr<const LevelTable> level_table);
|
||||
static std::shared_ptr<PSOBBCharacterFile> create_from_gc(const PSOGCCharacterFile::Character& gc_char);
|
||||
|
||||
void add_item(const ItemData& item, const ItemData::StackLimits& limits);
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits);
|
||||
void add_meseta(uint32_t amount);
|
||||
void remove_meseta(uint32_t amount, bool allow_overdraft);
|
||||
|
||||
uint8_t get_technique_level(uint8_t which) const; // Returns FF or 00-1D
|
||||
void set_technique_level(uint8_t which, uint8_t level);
|
||||
|
||||
enum class MaterialType : int8_t {
|
||||
HP = -2,
|
||||
TP = -1,
|
||||
POWER = 0,
|
||||
MIND = 1,
|
||||
EVADE = 2,
|
||||
DEF = 3,
|
||||
LUCK = 4,
|
||||
};
|
||||
|
||||
uint8_t get_material_usage(MaterialType which) const;
|
||||
void set_material_usage(MaterialType which, uint8_t usage);
|
||||
void clear_all_material_usage();
|
||||
|
||||
PSOGCCharacterFile::Character to_gc() const;
|
||||
} __packed_ws__(PSOBBCharacterFile, 0x2EA4);
|
||||
|
||||
struct PSOBBGuildCardFile {
|
||||
struct Entry {
|
||||
/* 0000 */ GuildCardBB data;
|
||||
/* 0108 */ pstring<TextEncoding::UTF16, 0x58> comment;
|
||||
/* 01B8 */ parray<uint8_t, 0x4> unknown_a1;
|
||||
/* 01BC */
|
||||
|
||||
void clear();
|
||||
} __packed_ws__(Entry, 0x1BC);
|
||||
|
||||
/* 0000 */ PSOBBMinimalSystemFile system_file;
|
||||
/* 0114 */ parray<GuildCardBB, 0x1C> blocked;
|
||||
/* 1DF4 */ parray<uint8_t, 0x180> unknown_a2;
|
||||
/* 1F74 */ parray<Entry, 0x69> entries;
|
||||
/* D590 */
|
||||
|
||||
PSOBBGuildCardFile() = default;
|
||||
|
||||
uint32_t checksum() const;
|
||||
} __packed_ws__(PSOBBGuildCardFile, 0xD590);
|
||||
|
||||
// This format is specific to newserv and is no longer used, but remains here
|
||||
// for backward compatibility.
|
||||
@@ -755,11 +800,11 @@ struct LegacySavedPlayerDataBB { // .nsc file format
|
||||
|
||||
/* 0000 */ be_uint64_t signature = SIGNATURE_V1;
|
||||
/* 0008 */ parray<uint8_t, 0x20> unused;
|
||||
/* 0028 */ PlayerRecords_Battle<false> battle_records;
|
||||
/* 0028 */ PlayerRecordsBattle battle_records;
|
||||
/* 0040 */ PlayerDispDataBBPreview preview;
|
||||
/* 00BC */ pstring<TextEncoding::UTF16, 0x00AC> auto_reply;
|
||||
/* 0214 */ PlayerBank bank;
|
||||
/* 14DC */ PlayerRecordsBB_Challenge challenge_records;
|
||||
/* 14DC */ PlayerRecordsChallengeBB challenge_records;
|
||||
/* 161C */ PlayerDispDataBB disp;
|
||||
/* 17AC */ pstring<TextEncoding::UTF16, 0x0058> guild_card_description;
|
||||
/* 185C */ pstring<TextEncoding::UTF16, 0x00AC> info_board;
|
||||
@@ -768,9 +813,9 @@ struct LegacySavedPlayerDataBB { // .nsc file format
|
||||
/* 1D04 */ QuestFlags quest_flags;
|
||||
/* 1F04 */ le_uint32_t death_count;
|
||||
/* 1F08 */ parray<le_uint32_t, 0x0016> quest_counters;
|
||||
/* 1F60 */ parray<le_uint16_t, 0x0014> tech_menu_config;
|
||||
/* 1F60 */ parray<le_uint16_t, 0x0014> tech_menu_shortcut_entries;
|
||||
/* 1F88 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(LegacySavedPlayerDataBB, 0x1F88);
|
||||
|
||||
// This format is specific to newserv and is no longer used, but remains here
|
||||
// for backward compatibility.
|
||||
@@ -783,8 +828,10 @@ struct LegacySavedAccountDataBB { // .nsa file format
|
||||
/* D648 */ PSOBBFullSystemFile system_file;
|
||||
/* E138 */ le_uint32_t unused;
|
||||
/* E13C */ le_uint32_t option_flags;
|
||||
/* E140 */ parray<uint8_t, 0x0A40> shortcuts;
|
||||
/* EB80 */ parray<PSOBBCharacterFile::SymbolChatEntry, 12> symbol_chats;
|
||||
/* E140 */ parray<SaveFileShortcutEntryBB, 0x10> shortcuts;
|
||||
/* EB80 */ parray<SaveFileSymbolChatEntryBB, 0x0C> symbol_chats;
|
||||
/* F060 */ pstring<TextEncoding::UTF16_ALWAYS_MARKED, 0x10> team_name;
|
||||
/* F080 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws__(LegacySavedAccountDataBB, 0xF080);
|
||||
|
||||
std::string encode_psobb_hangame_credentials(const std::string& user_id, const std::string& token, const std::string& unused = "");
|
||||
|
||||
+157
-79
@@ -155,11 +155,11 @@ static const char* dc_lobby_server_copyright = "DreamCast Lobby Server. Copyrigh
|
||||
static const char* bb_game_server_copyright = "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.";
|
||||
static const char* bb_pm_server_copyright = "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM.";
|
||||
|
||||
S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B<0xB4>
|
||||
S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4>
|
||||
prepare_server_init_contents_console(
|
||||
uint32_t server_key, uint32_t client_key, uint8_t flags) {
|
||||
bool initial_connection = (flags & SendServerInitFlag::IS_INITIAL_CONNECTION);
|
||||
S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B<0xB4> cmd;
|
||||
S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4> cmd;
|
||||
cmd.basic_cmd.copyright.encode(initial_connection ? dc_port_map_copyright : dc_lobby_server_copyright);
|
||||
cmd.basic_cmd.server_key = server_key;
|
||||
cmd.basic_cmd.client_key = client_key;
|
||||
@@ -205,13 +205,13 @@ void send_server_init_dc_pc_v3(shared_ptr<Client> c, uint8_t flags) {
|
||||
}
|
||||
}
|
||||
|
||||
S_ServerInitWithAfterMessage_BB_03_9B<0xB4>
|
||||
S_ServerInitWithAfterMessageT_BB_03_9B<0xB4>
|
||||
prepare_server_init_contents_bb(
|
||||
const parray<uint8_t, 0x30>& server_key,
|
||||
const parray<uint8_t, 0x30>& client_key,
|
||||
uint8_t flags) {
|
||||
bool use_secondary_message = (flags & SendServerInitFlag::USE_SECONDARY_MESSAGE);
|
||||
S_ServerInitWithAfterMessage_BB_03_9B<0xB4> cmd;
|
||||
S_ServerInitWithAfterMessageT_BB_03_9B<0xB4> cmd;
|
||||
cmd.basic_cmd.copyright.encode(use_secondary_message ? bb_pm_server_copyright : bb_game_server_copyright);
|
||||
cmd.basic_cmd.server_key = server_key;
|
||||
cmd.basic_cmd.client_key = client_key;
|
||||
@@ -277,7 +277,7 @@ void send_update_client_config(shared_ptr<Client> c, bool always_send) {
|
||||
c->config.set_flag(Client::Flag::HAS_GUILD_CARD_NUMBER);
|
||||
S_UpdateClientConfig_DC_PC_04 cmd;
|
||||
cmd.player_tag = 0x00010000;
|
||||
cmd.guild_card_number = c->license->serial_number;
|
||||
cmd.guild_card_number = c->login->account->account_id;
|
||||
send_command_t(c, 0x04, 0x00, cmd);
|
||||
}
|
||||
break;
|
||||
@@ -290,7 +290,7 @@ void send_update_client_config(shared_ptr<Client> c, bool always_send) {
|
||||
c->config.set_flag(Client::Flag::HAS_GUILD_CARD_NUMBER);
|
||||
S_UpdateClientConfig_V3_04 cmd;
|
||||
cmd.player_tag = 0x00010000;
|
||||
cmd.guild_card_number = c->license->serial_number;
|
||||
cmd.guild_card_number = c->login->account->account_id;
|
||||
c->config.serialize_into(cmd.client_config);
|
||||
send_command_t(c, 0x04, 0x00, cmd);
|
||||
break;
|
||||
@@ -338,11 +338,17 @@ void prepare_client_for_patches(shared_ptr<Client> c, function<void()> on_comple
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
bool is_gc = ::is_gc(c->version());
|
||||
bool is_xb = (c->version() == Version::XB_V3);
|
||||
if ((is_gc || is_xb) &&
|
||||
const char* version_detect_name = nullptr;
|
||||
if (c->version() == Version::DC_V2) {
|
||||
version_detect_name = "VersionDetectDC";
|
||||
} else if (is_gc(c->version())) {
|
||||
version_detect_name = "VersionDetectGC";
|
||||
} else if (c->version() == Version::XB_V3) {
|
||||
version_detect_name = "VersionDetectXB";
|
||||
}
|
||||
if (version_detect_name &&
|
||||
c->config.specific_version == default_specific_version_for_version(c->version(), -1)) {
|
||||
send_function_call(c, s->function_code_index->name_to_function.at(is_xb ? "VersionDetectXB" : "VersionDetectGC"));
|
||||
send_function_call(c, s->function_code_index->name_to_function.at(version_detect_name));
|
||||
c->function_call_response_queue.emplace_back([wc = weak_ptr<Client>(c), on_complete](uint32_t specific_version, uint32_t) -> void {
|
||||
auto c = wc.lock();
|
||||
if (!c) {
|
||||
@@ -534,7 +540,7 @@ bool send_protected_command(std::shared_ptr<Client> c, const void* data, size_t
|
||||
}
|
||||
|
||||
void send_reconnect(shared_ptr<Client> c, uint32_t address, uint16_t port) {
|
||||
S_Reconnect_19 cmd = {{address, port, 0}};
|
||||
S_Reconnect_19 cmd = {address, port, 0};
|
||||
send_command_t(c, is_patch(c->version()) ? 0x14 : 0x19, 0x00, cmd);
|
||||
}
|
||||
|
||||
@@ -556,7 +562,7 @@ void send_client_init_bb(shared_ptr<Client> c, uint32_t error_code) {
|
||||
S_ClientInit_BB_00E6 cmd;
|
||||
cmd.error_code = error_code;
|
||||
cmd.player_tag = 0x00010000;
|
||||
cmd.guild_card_number = c->license->serial_number;
|
||||
cmd.guild_card_number = c->login->account->account_id;
|
||||
cmd.security_token = team ? team->team_id : 0;
|
||||
c->config.serialize_into(cmd.client_config);
|
||||
cmd.can_create_team = 1;
|
||||
@@ -570,7 +576,7 @@ void send_system_file_bb(shared_ptr<Client> c) {
|
||||
PSOBBFullSystemFile cmd;
|
||||
cmd.base = *c->system_file();
|
||||
if (team) {
|
||||
cmd.team_membership = team->membership_for_member(c->license->serial_number);
|
||||
cmd.team_membership = team->membership_for_member(c->login->account->account_id);
|
||||
}
|
||||
send_command_t(c, 0x00E2, 0x00000000, cmd);
|
||||
}
|
||||
@@ -709,7 +715,7 @@ void send_complete_player_bb(shared_ptr<Client> c) {
|
||||
cmd.char_file = *p;
|
||||
cmd.system_file.base = *sys;
|
||||
if (team) {
|
||||
cmd.system_file.team_membership = team->membership_for_member(c->license->serial_number);
|
||||
cmd.system_file.team_membership = team->membership_for_member(c->login->account->account_id);
|
||||
}
|
||||
send_command_t(c, 0x00E7, 0x00000000, cmd);
|
||||
}
|
||||
@@ -864,7 +870,7 @@ void send_text_message(shared_ptr<Lobby> l, const string& text) {
|
||||
|
||||
void send_text_message(shared_ptr<ServerState> s, const string& text) {
|
||||
for (auto& it : s->channel_to_client) {
|
||||
if (it.second->license && !is_patch(it.second->version())) {
|
||||
if (it.second->login && !is_patch(it.second->version())) {
|
||||
send_text_message(it.second, text);
|
||||
}
|
||||
}
|
||||
@@ -884,6 +890,31 @@ __attribute__((format(printf, 2, 3))) void send_ep3_text_message_printf(shared_p
|
||||
}
|
||||
}
|
||||
|
||||
void send_scrolling_message_bb(shared_ptr<Client> c, const string& text) {
|
||||
if (c->version() != Version::BB_V4) {
|
||||
throw logic_error("cannot send scrolling message to non-BB player");
|
||||
}
|
||||
send_header_text(c->channel, 0x00EE, 0, text, ColorMode::ADD);
|
||||
}
|
||||
|
||||
void send_text_or_scrolling_message(shared_ptr<Client> c, const string& text, const string& scrolling) {
|
||||
if (is_v4(c->version())) {
|
||||
send_scrolling_message_bb(c, scrolling);
|
||||
} else {
|
||||
send_text_message(c, text);
|
||||
}
|
||||
}
|
||||
|
||||
void send_text_or_scrolling_message(
|
||||
std::shared_ptr<Lobby> l, std::shared_ptr<Client> exclude_c, const std::string& text, const std::string& scrolling) {
|
||||
for (const auto& lc : l->clients) {
|
||||
if (!lc || (lc == exclude_c)) {
|
||||
continue;
|
||||
}
|
||||
send_text_or_scrolling_message(lc, text, scrolling);
|
||||
}
|
||||
}
|
||||
|
||||
string prepare_chat_data(
|
||||
Version version,
|
||||
uint8_t language,
|
||||
@@ -977,7 +1008,7 @@ void send_simple_mail_t(shared_ptr<Client> c, uint32_t from_guild_card_number, c
|
||||
cmd.player_tag = from_guild_card_number ? 0x00010000 : 0;
|
||||
cmd.from_guild_card_number = from_guild_card_number;
|
||||
cmd.from_name.encode(from_name, c->language());
|
||||
cmd.to_guild_card_number = c->license->serial_number;
|
||||
cmd.to_guild_card_number = c->login->account->account_id;
|
||||
cmd.text.encode(text, c->language());
|
||||
send_command_t(c, 0x81, 0x00, cmd);
|
||||
}
|
||||
@@ -987,7 +1018,7 @@ void send_simple_mail_bb(shared_ptr<Client> c, uint32_t from_guild_card_number,
|
||||
cmd.player_tag = from_guild_card_number ? 0x00010000 : 0;
|
||||
cmd.from_guild_card_number = from_guild_card_number;
|
||||
cmd.from_name.encode(from_name, c->language());
|
||||
cmd.to_guild_card_number = c->license->serial_number;
|
||||
cmd.to_guild_card_number = c->login->account->account_id;
|
||||
cmd.received_date.encode(format_time(now()), c->language());
|
||||
cmd.text.encode(text, c->language());
|
||||
send_command_t(c, 0x81, 0x00, cmd);
|
||||
@@ -1020,7 +1051,7 @@ void send_simple_mail(shared_ptr<Client> c, uint32_t from_guild_card_number, con
|
||||
|
||||
void send_simple_mail(shared_ptr<ServerState> s, uint32_t from_guild_card_number, const string& from_name, const string& text) {
|
||||
for (const auto& it : s->channel_to_client) {
|
||||
if (it.second->license && !is_patch(it.second->version())) {
|
||||
if (it.second->login && !is_patch(it.second->version())) {
|
||||
send_simple_mail(it.second, from_guild_card_number, from_name, text);
|
||||
}
|
||||
}
|
||||
@@ -1031,7 +1062,7 @@ void send_simple_mail(shared_ptr<ServerState> s, uint32_t from_guild_card_number
|
||||
|
||||
template <TextEncoding NameEncoding, TextEncoding MessageEncoding>
|
||||
void send_info_board_t(shared_ptr<Client> c) {
|
||||
vector<S_InfoBoardEntry_D8<NameEncoding, MessageEncoding>> entries;
|
||||
vector<S_InfoBoardEntryT_D8<NameEncoding, MessageEncoding>> entries;
|
||||
auto l = c->require_lobby();
|
||||
for (const auto& other_c : l->clients) {
|
||||
if (!other_c.get()) {
|
||||
@@ -1100,10 +1131,10 @@ void send_card_search_result_t(
|
||||
auto s = c->require_server_state();
|
||||
string port_name = lobby_port_name_for_version(c->version());
|
||||
|
||||
S_GuildCardSearchResult<CommandHeaderT, Encoding> cmd;
|
||||
S_GuildCardSearchResultT<CommandHeaderT, Encoding> cmd;
|
||||
cmd.player_tag = 0x00010000;
|
||||
cmd.searcher_guild_card_number = c->license->serial_number;
|
||||
cmd.result_guild_card_number = result->license->serial_number;
|
||||
cmd.searcher_guild_card_number = c->login->account->account_id;
|
||||
cmd.result_guild_card_number = result->login->account->account_id;
|
||||
cmd.reconnect_command_header.size = sizeof(cmd.reconnect_command_header) + sizeof(cmd.reconnect_command);
|
||||
cmd.reconnect_command_header.command = 0x19;
|
||||
cmd.reconnect_command_header.flag = 0x00;
|
||||
@@ -1113,11 +1144,11 @@ void send_card_search_result_t(
|
||||
|
||||
string location_string;
|
||||
if (result_lobby->is_game()) {
|
||||
location_string = string_printf("%s,BLOCK01,%s", result_lobby->name.c_str(), s->name.c_str());
|
||||
location_string = string_printf("%s,,BLOCK01,%s", result_lobby->name.c_str(), s->name.c_str());
|
||||
} else if (result_lobby->is_ep3()) {
|
||||
location_string = string_printf("BLOCK01-C%02" PRIu32 ",BLOCK01,%s", result_lobby->lobby_id - 15, s->name.c_str());
|
||||
location_string = string_printf("BLOCK01-C%02" PRIu32 ",,BLOCK01,%s", result_lobby->lobby_id - 15, s->name.c_str());
|
||||
} else {
|
||||
location_string = string_printf("BLOCK01-%02" PRIu32 ",BLOCK01,%s", result_lobby->lobby_id, s->name.c_str());
|
||||
location_string = string_printf("BLOCK01-%02" PRIu32 ",,BLOCK01,%s", result_lobby->lobby_id, s->name.c_str());
|
||||
}
|
||||
cmd.location_string.encode(location_string, c->language());
|
||||
cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY;
|
||||
@@ -1276,20 +1307,20 @@ void send_guild_card(
|
||||
}
|
||||
|
||||
void send_guild_card(shared_ptr<Client> c, shared_ptr<Client> source) {
|
||||
if (!source->license) {
|
||||
throw runtime_error("source player does not have a license");
|
||||
if (!source->login) {
|
||||
throw runtime_error("source player does not have an account");
|
||||
}
|
||||
|
||||
auto source_p = source->character(true, false);
|
||||
auto source_team = source->team();
|
||||
|
||||
uint64_t xb_user_id = source->license->xb_user_id
|
||||
? source->license->xb_user_id
|
||||
: (0xAE00000000000000ULL | source->license->serial_number);
|
||||
uint64_t xb_user_id = (source->login->xb_license && source->login->xb_license->user_id)
|
||||
? source->login->xb_license->user_id
|
||||
: (0xAE00000000000000ULL | source->login->account->account_id);
|
||||
|
||||
send_guild_card(
|
||||
c->channel,
|
||||
source->license->serial_number,
|
||||
source->login->account->account_id,
|
||||
xb_user_id,
|
||||
source_p->disp.name.decode(source->language()),
|
||||
source_team ? source_team->name : "",
|
||||
@@ -1388,7 +1419,7 @@ void send_game_menu_t(
|
||||
bool show_tournaments_only) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
vector<S_GameMenuEntry<Encoding>> entries;
|
||||
vector<S_GameMenuEntryT<Encoding>> entries;
|
||||
{
|
||||
auto& e = entries.emplace_back();
|
||||
e.menu_id = MenuID::GAME;
|
||||
@@ -1436,7 +1467,7 @@ void send_game_menu_t(
|
||||
auto& e = entries.emplace_back();
|
||||
e.menu_id = MenuID::GAME;
|
||||
e.game_id = l->lobby_id;
|
||||
e.difficulty_tag = (l->is_ep3() ? 0x0A : (l->difficulty + 0x22));
|
||||
e.difficulty_tag = (is_ep3(c->version()) ? 0x0A : (l->difficulty + 0x22));
|
||||
e.num_players = l->count_clients();
|
||||
if (is_dc(c->version())) {
|
||||
e.episode = l->version_is_allowed(Version::DC_V1) ? 1 : 0;
|
||||
@@ -1465,6 +1496,10 @@ void send_game_menu_t(
|
||||
default:
|
||||
throw logic_error("invalid game mode");
|
||||
}
|
||||
// On v2, render name in orange if v1 is not allowed
|
||||
if (is_v2(c->version()) && !l->version_is_allowed(Version::DC_V1)) {
|
||||
e.flags |= 0x40;
|
||||
}
|
||||
// On BB, gray out games that can't be joined
|
||||
if ((c->version() == Version::BB_V4) && (l->join_error_for_client(c, nullptr) != Lobby::JoinError::ALLOWED)) {
|
||||
e.flags |= 0x04;
|
||||
@@ -1540,7 +1575,7 @@ void send_quest_categories_menu_t(
|
||||
QuestMenuType menu_type,
|
||||
Episode episode) {
|
||||
QuestIndex::IncludeCondition include_condition = nullptr;
|
||||
if (!c->license->check_flag(License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
if (!c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
auto l = c->lobby.lock();
|
||||
include_condition = l ? l->quest_include_condition() : nullptr;
|
||||
}
|
||||
@@ -1672,7 +1707,7 @@ void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr
|
||||
template <typename LobbyDataT>
|
||||
void populate_lobby_data_for_client(LobbyDataT& ret, shared_ptr<const Client> c, shared_ptr<const Client> viewer_c) {
|
||||
ret.player_tag = 0x00010000;
|
||||
ret.guild_card_number = c->license->serial_number;
|
||||
ret.guild_card_number = c->login->account->account_id;
|
||||
ret.client_id = c->lobby_client_id;
|
||||
string name = c->character()->disp.name.decode(c->language());
|
||||
ret.name.encode(name, viewer_c->language());
|
||||
@@ -1681,11 +1716,11 @@ void populate_lobby_data_for_client(LobbyDataT& ret, shared_ptr<const Client> c,
|
||||
template <>
|
||||
void populate_lobby_data_for_client(PlayerLobbyDataXB& ret, shared_ptr<const Client> c, shared_ptr<const Client> viewer_c) {
|
||||
ret.player_tag = 0x00010000;
|
||||
ret.guild_card_number = c->license->serial_number;
|
||||
ret.guild_card_number = c->login->account->account_id;
|
||||
if (c->xb_netloc) {
|
||||
ret.netloc = *c->xb_netloc;
|
||||
} else {
|
||||
ret.netloc.account_id = 0xAE00000000000000 | c->license->serial_number;
|
||||
ret.netloc.account_id = 0xAE00000000000000 | c->login->account->account_id;
|
||||
}
|
||||
ret.client_id = c->lobby_client_id;
|
||||
string name = c->character()->disp.name.decode(c->language());
|
||||
@@ -1695,11 +1730,11 @@ void populate_lobby_data_for_client(PlayerLobbyDataXB& ret, shared_ptr<const Cli
|
||||
template <>
|
||||
void populate_lobby_data_for_client<PlayerLobbyDataBB>(PlayerLobbyDataBB& ret, shared_ptr<const Client> c, shared_ptr<const Client> viewer_c) {
|
||||
ret.player_tag = 0x00010000;
|
||||
ret.guild_card_number = c->license->serial_number;
|
||||
ret.guild_card_number = c->login->account->account_id;
|
||||
ret.client_id = c->lobby_client_id;
|
||||
auto team = c->team();
|
||||
if (team) {
|
||||
ret.team_master_guild_card_number = team->master_serial_number;
|
||||
ret.team_master_guild_card_number = team->master_account_id;
|
||||
ret.team_id = team->team_id;
|
||||
} else {
|
||||
ret.team_master_guild_card_number = 0;
|
||||
@@ -1745,13 +1780,13 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
|
||||
auto& p = cmd.players[z];
|
||||
populate_lobby_data_for_client(p.lobby_data, wc, c);
|
||||
p.inventory = wc_p->inventory;
|
||||
p.inventory.encode_for_client(c);
|
||||
p.disp = wc_p->disp.to_dcpcv3(c->language(), p.inventory.language);
|
||||
p.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version()));
|
||||
p.disp = wc_p->disp.to_dcpcv3<false>(c->language(), p.inventory.language);
|
||||
p.disp.enforce_lobby_join_limits_for_version(c->version());
|
||||
|
||||
auto& e = cmd.entries[z];
|
||||
e.player_tag = 0x00010000;
|
||||
e.guild_card_number = wc->license->serial_number;
|
||||
e.guild_card_number = wc->login->account->account_id;
|
||||
e.name.encode(wc_p->disp.name.decode(wc_p->inventory.language), c->language());
|
||||
e.present = 1;
|
||||
e.level = wc->ep3_config
|
||||
@@ -1788,7 +1823,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
|
||||
auto& p = cmd.players[client_id];
|
||||
p.lobby_data = entry.lobby_data;
|
||||
p.inventory = entry.inventory;
|
||||
p.inventory.encode_for_client(c);
|
||||
p.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version()));
|
||||
p.disp = entry.disp;
|
||||
p.disp.enforce_lobby_join_limits_for_version(c->version());
|
||||
|
||||
@@ -1815,11 +1850,11 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
|
||||
auto& cmd_e = cmd.entries[z];
|
||||
populate_lobby_data_for_client(cmd_p.lobby_data, other_c, c);
|
||||
cmd_p.inventory = other_p->inventory;
|
||||
cmd_p.disp = other_p->disp.to_dcpcv3(c->language(), cmd_p.inventory.language);
|
||||
cmd_p.disp = other_p->disp.to_dcpcv3<false>(c->language(), cmd_p.inventory.language);
|
||||
cmd_p.disp.enforce_lobby_join_limits_for_version(c->version());
|
||||
|
||||
cmd_e.player_tag = 0x00010000;
|
||||
cmd_e.guild_card_number = other_c->license->serial_number;
|
||||
cmd_e.guild_card_number = other_c->login->account->account_id;
|
||||
cmd_e.name = cmd_p.lobby_data.name;
|
||||
cmd_e.present = 1;
|
||||
cmd_e.level = other_c->ep3_config
|
||||
@@ -1935,7 +1970,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
auto other_p = l->clients[x]->character();
|
||||
auto& cmd_p = cmd.players_ep3[x];
|
||||
cmd_p.inventory = other_p->inventory;
|
||||
cmd_p.inventory.encode_for_client(c);
|
||||
cmd_p.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version()));
|
||||
cmd_p.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(other_p->disp, c->language(), other_p->inventory.language);
|
||||
cmd_p.disp.enforce_lobby_join_limits_for_version(c->version());
|
||||
if (s->version_name_colors) {
|
||||
@@ -2023,7 +2058,7 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
|
||||
lobby_block = l->block;
|
||||
}
|
||||
|
||||
S_JoinLobby<LobbyFlags, LobbyDataT, DispDataT> cmd;
|
||||
S_JoinLobbyT<LobbyFlags, LobbyDataT, DispDataT> cmd;
|
||||
cmd.lobby_flags.client_id = c->lobby_client_id;
|
||||
cmd.lobby_flags.leader_id = l->leader_id;
|
||||
cmd.lobby_flags.disable_udp = 0x01;
|
||||
@@ -2051,7 +2086,7 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
|
||||
auto& e = cmd.entries[used_entries++];
|
||||
populate_lobby_data_for_client(e.lobby_data, lc, c);
|
||||
e.inventory = lp->inventory;
|
||||
e.inventory.encode_for_client(c);
|
||||
e.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version()));
|
||||
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 {
|
||||
@@ -2126,7 +2161,7 @@ void send_join_lobby_xb(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cl
|
||||
auto& e = cmd.entries[used_entries++];
|
||||
populate_lobby_data_for_client(e.lobby_data, lc, c);
|
||||
e.inventory = lp->inventory;
|
||||
e.inventory.encode_for_client(c);
|
||||
e.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version()));
|
||||
e.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(lp->disp, c->language(), lp->inventory.language);
|
||||
e.disp.enforce_lobby_join_limits_for_version(c->version());
|
||||
if (s->version_name_colors) {
|
||||
@@ -2174,7 +2209,7 @@ void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l,
|
||||
auto& e = cmd.entries[used_entries++];
|
||||
populate_lobby_data_for_client(e.lobby_data, lc, c);
|
||||
e.inventory = lp->inventory;
|
||||
e.inventory.encode_for_client(c);
|
||||
e.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version()));
|
||||
if ((lc == c) && is_v1_or_v2(c->version()) && lc->v1_v2_last_reported_disp) {
|
||||
e.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(*lc->v1_v2_last_reported_disp, c->language(), lp->inventory.language);
|
||||
} else {
|
||||
@@ -2294,8 +2329,39 @@ void send_self_leave_notification(shared_ptr<Client> c) {
|
||||
send_command_t(c, 0x69, c->lobby_client_id, cmd);
|
||||
}
|
||||
|
||||
void send_get_player_info(shared_ptr<Client> c) {
|
||||
send_command(c, (c->version() == Version::DC_NTE) ? 0x8D : 0x95, 0x00);
|
||||
static bool send_get_extended_player_info(shared_ptr<Client> c) {
|
||||
// TODO: Support extended player info on other versions.
|
||||
if (c->version() != Version::GC_V3) {
|
||||
return false;
|
||||
}
|
||||
auto s = c->require_server_state();
|
||||
if (c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL) ||
|
||||
c->config.check_flag(Client::Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
prepare_client_for_patches(c, [wc = weak_ptr<Client>(c)]() {
|
||||
auto c = wc.lock();
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
auto s = c->require_server_state();
|
||||
auto fn = s->function_code_index->get_patch("GetExtendedPlayerInfo", c->config.specific_version);
|
||||
send_function_call(c, fn);
|
||||
c->function_call_response_queue.emplace_back(empty_function_call_response_handler);
|
||||
} catch (const exception& e) {
|
||||
c->log.warning("Failed to send extended player info request: %s", e.what());
|
||||
send_get_player_info(c, false);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void send_get_player_info(shared_ptr<Client> c, bool request_extended) {
|
||||
if (!request_extended || !send_get_extended_player_info(c)) {
|
||||
send_command(c, (c->version() == Version::DC_NTE) ? 0x8D : 0x95, 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -2356,7 +2422,7 @@ void send_arrow_update(shared_ptr<Lobby> l) {
|
||||
}
|
||||
auto& e = entries.emplace_back();
|
||||
e.player_tag = 0x00010000;
|
||||
e.guild_card_number = lc->license->serial_number;
|
||||
e.guild_card_number = lc->login->account->account_id;
|
||||
e.arrow_color = lc->lobby_arrow_color;
|
||||
}
|
||||
|
||||
@@ -2417,7 +2483,9 @@ void send_remove_conditions(shared_ptr<Client> c) {
|
||||
cmd.unknown_a1 = z;
|
||||
cmd.unknown_a2 = 0;
|
||||
}
|
||||
send_protected_command(c, &cmds, sizeof(cmds), true);
|
||||
if (send_protected_command(c, &cmds, sizeof(cmds), true)) {
|
||||
send_command_excluding_client(c->require_lobby(), c, 0x60, 0x00, &cmds, sizeof(cmds));
|
||||
}
|
||||
}
|
||||
|
||||
void send_remove_conditions(Channel& ch, uint16_t client_id) {
|
||||
@@ -2709,10 +2777,14 @@ void send_game_flag_state_t(shared_ptr<Client> c) {
|
||||
void send_game_flag_state(shared_ptr<Client> c) {
|
||||
// DC NTE and 11/2000 don't have this command at all; v1 has it but it doesn't
|
||||
// include flags for Ultimate.
|
||||
if (!is_v1(c->version())) {
|
||||
send_game_flag_state_t<G_SetQuestFlagsV2V3V4_6x6F>(c);
|
||||
} else if (!is_pre_v1(c->version())) {
|
||||
send_game_flag_state_t<G_SetQuestFlagsV1_6x6F>(c);
|
||||
if (is_pre_v1(c->version())) {
|
||||
return;
|
||||
} else if (is_v1(c->version())) {
|
||||
send_game_flag_state_t<G_SetQuestFlags_DCv1_6x6F>(c);
|
||||
} else if (!is_v4(c->version())) {
|
||||
send_game_flag_state_t<G_SetQuestFlags_V2_V3_6x6F>(c);
|
||||
} else {
|
||||
send_game_flag_state_t<G_SetQuestFlags_BB_6x6F>(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2965,8 +3037,8 @@ void send_ep3_media_update(
|
||||
|
||||
void send_ep3_rank_update(shared_ptr<Client> c) {
|
||||
auto s = c->require_server_state();
|
||||
uint32_t current_meseta = s->ep3_infinite_meseta ? 1000000 : c->license->ep3_current_meseta;
|
||||
uint32_t total_meseta_earned = s->ep3_infinite_meseta ? 1000000 : c->license->ep3_total_meseta_earned;
|
||||
uint32_t current_meseta = s->ep3_infinite_meseta ? 1000000 : c->login->account->ep3_current_meseta;
|
||||
uint32_t total_meseta_earned = s->ep3_infinite_meseta ? 1000000 : c->login->account->ep3_total_meseta_earned;
|
||||
S_RankUpdate_Ep3_B7 cmd = {0, {}, current_meseta, total_meseta_earned, 0xFFFFFFFF};
|
||||
send_command_t(c, 0xB7, 0x00, cmd);
|
||||
}
|
||||
@@ -2990,7 +3062,7 @@ void send_ep3_card_battle_table_state(shared_ptr<Lobby> l, uint16_t table_number
|
||||
auto& e = is_nte ? cmd_nte.entries[c->card_battle_table_seat_number] : cmd_final.entries[c->card_battle_table_seat_number];
|
||||
if (e.state == 0) {
|
||||
e.state = c->card_battle_table_seat_state;
|
||||
e.guild_card_number = c->license->serial_number;
|
||||
e.guild_card_number = c->login->account->account_id;
|
||||
auto& clients = is_nte ? clients_nte : clients_final;
|
||||
clients.emplace(c);
|
||||
}
|
||||
@@ -3120,7 +3192,7 @@ template <typename RulesT>
|
||||
void send_ep3_tournament_details_t(
|
||||
shared_ptr<Client> c,
|
||||
shared_ptr<const Episode3::Tournament> tourn) {
|
||||
S_TournamentGameDetailsBase_Ep3_E3<RulesT> cmd;
|
||||
S_TournamentGameDetailsBaseT_Ep3_E3<RulesT> cmd;
|
||||
auto vm = tourn->get_map()->version(c->language());
|
||||
cmd.name.encode(tourn->get_name(), c->language());
|
||||
cmd.map_name.encode(vm->map->name.decode(vm->language), c->language());
|
||||
@@ -3172,7 +3244,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
auto tourn = tourn_match ? tourn_match->tournament.lock() : nullptr;
|
||||
|
||||
if (tourn) {
|
||||
S_TournamentGameDetailsBase_Ep3_E3<RulesT> cmd;
|
||||
S_TournamentGameDetailsBaseT_Ep3_E3<RulesT> cmd;
|
||||
cmd.name.encode(l->name, c->language());
|
||||
|
||||
auto vm = tourn->get_map()->version(c->language());
|
||||
@@ -3190,8 +3262,8 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
cmd.players_per_team = (tourn->get_flags() & Episode3::Tournament::Flag::IS_2V2) ? 2 : 1;
|
||||
|
||||
if (primary_lobby) {
|
||||
auto serial_number_to_client = primary_lobby->clients_by_serial_number();
|
||||
using TeamEntryT = typename S_TournamentGameDetailsBase_Ep3_E3<RulesT>::TeamEntry;
|
||||
auto account_id_to_client = primary_lobby->clients_by_account_id();
|
||||
using TeamEntryT = typename S_TournamentGameDetailsBaseT_Ep3_E3<RulesT>::TeamEntry;
|
||||
auto describe_team = [&](TeamEntryT& team_entry, shared_ptr<const Episode3::Tournament::Team> team) -> void {
|
||||
team_entry.team_name.encode(team->name, c->language());
|
||||
for (size_t z = 0; z < team->players.size(); z++) {
|
||||
@@ -3199,7 +3271,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
const auto& player = team->players[z];
|
||||
if (player.is_human()) {
|
||||
try {
|
||||
auto other_c = serial_number_to_client.at(player.serial_number);
|
||||
auto other_c = account_id_to_client.at(player.account_id);
|
||||
entry.name.encode(other_c->character()->disp.name.decode(other_c->language()), c->language());
|
||||
entry.description.encode(ep3_description_for_client(other_c), c->language());
|
||||
} catch (const out_of_range&) {
|
||||
@@ -3232,7 +3304,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
send_command_t(c, 0xE3, flag, cmd);
|
||||
|
||||
} else {
|
||||
S_GameInformationBase_Ep3_E1<RulesT> cmd;
|
||||
S_GameInformationBaseT_Ep3_E1<RulesT> cmd;
|
||||
cmd.game_name.encode(l->name, c->language());
|
||||
if (primary_lobby) {
|
||||
size_t num_players = 0;
|
||||
@@ -3261,7 +3333,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
// spectator count in the info window object. To account for this, we send
|
||||
// a mostly-blank E3 to set the spectator count, followed by an E1 with
|
||||
// the correct data.
|
||||
S_TournamentGameDetailsBase_Ep3_E3<RulesT> cmd_E3;
|
||||
S_TournamentGameDetailsBaseT_Ep3_E3<RulesT> cmd_E3;
|
||||
cmd_E3.num_spectators = num_spectators;
|
||||
send_command_t(c, 0xE3, 0x04, cmd_E3);
|
||||
|
||||
@@ -3321,7 +3393,7 @@ void send_ep3_set_tournament_player_decks_t(shared_ptr<Client> c) {
|
||||
if (player.is_human()) {
|
||||
entry.type = 1; // Human
|
||||
entry.player_name.encode(player.player_name, c->language());
|
||||
if (player.serial_number == c->license->serial_number) {
|
||||
if (player.account_id == c->login->account->account_id) {
|
||||
cmd.player_slot = base_index + z;
|
||||
}
|
||||
} else {
|
||||
@@ -3360,7 +3432,7 @@ void send_ep3_tournament_match_result(shared_ptr<Lobby> l, uint32_t meseta_rewar
|
||||
throw logic_error("cannot send tournament result without valid winner team");
|
||||
}
|
||||
|
||||
auto serial_number_to_client = l->clients_by_serial_number();
|
||||
auto account_id_to_client = l->clients_by_account_id();
|
||||
|
||||
for (const auto& lc : l->clients) {
|
||||
if (!lc) {
|
||||
@@ -3371,7 +3443,7 @@ void send_ep3_tournament_match_result(shared_ptr<Lobby> l, uint32_t meseta_rewar
|
||||
const auto& player = team->players[z];
|
||||
if (player.is_human()) {
|
||||
try {
|
||||
auto pc = serial_number_to_client.at(player.serial_number);
|
||||
auto pc = account_id_to_client.at(player.account_id);
|
||||
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());
|
||||
@@ -3829,9 +3901,10 @@ void send_team_membership_info(shared_ptr<Client> c) {
|
||||
auto team = c->team();
|
||||
S_TeamMembershipInformation_BB_12EA cmd;
|
||||
if (team) {
|
||||
cmd.guild_card_number = c->license->serial_number;
|
||||
cmd.guild_card_number = c->login->account->account_id;
|
||||
cmd.team_id = team->team_id;
|
||||
cmd.privilege_level = team->members.at(c->license->serial_number).privilege_level();
|
||||
cmd.privilege_level = team->members.at(c->login->account->account_id).privilege_level();
|
||||
cmd.team_member_count = min<size_t>(team->members.size(), 100);
|
||||
cmd.team_name.encode(team->name);
|
||||
}
|
||||
send_command_t(c, 0x12EA, 0x00000000, cmd);
|
||||
@@ -3841,12 +3914,13 @@ 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_number2 = c->license->serial_number;
|
||||
cmd.guild_card_number2 = c->login->account->account_id;
|
||||
cmd.player_name = c->character()->disp.name;
|
||||
if (team) {
|
||||
cmd.guild_card_number = c->license->serial_number;
|
||||
cmd.guild_card_number = c->login->account->account_id;
|
||||
cmd.team_id = team->team_id;
|
||||
cmd.privilege_level = team->members.at(c->license->serial_number).privilege_level();
|
||||
cmd.privilege_level = team->members.at(c->login->account->account_id).privilege_level();
|
||||
cmd.team_member_count = min<size_t>(team->members.size(), 100);
|
||||
cmd.team_name.encode(team->name);
|
||||
if (team->flag_data) {
|
||||
cmd.flag_data = *team->flag_data;
|
||||
@@ -3903,7 +3977,7 @@ void send_team_member_list(shared_ptr<Client> c) {
|
||||
auto& e = entries.emplace_back();
|
||||
e.rank = z + 1;
|
||||
e.privilege_level = m->privilege_level();
|
||||
e.guild_card_number = m->serial_number;
|
||||
e.guild_card_number = m->account_id;
|
||||
e.name.encode(m->name, c->language());
|
||||
}
|
||||
|
||||
@@ -3938,7 +4012,7 @@ void send_intra_team_ranking(shared_ptr<Client> c) {
|
||||
auto& e = entries.emplace_back();
|
||||
e.rank = z + 1;
|
||||
e.privilege_level = m->privilege_level();
|
||||
e.guild_card_number = m->serial_number;
|
||||
e.guild_card_number = m->account_id;
|
||||
e.player_name.encode(m->name);
|
||||
e.points = m->points;
|
||||
}
|
||||
@@ -3981,11 +4055,15 @@ void send_team_reward_list(shared_ptr<Client> c, bool show_purchased) {
|
||||
}
|
||||
auto s = c->require_server_state();
|
||||
|
||||
// Hide item rewards if the player's bank is full
|
||||
bool show_item_rewards = show_purchased || (c->current_bank().num_items < 200);
|
||||
|
||||
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) {
|
||||
// In the buy menu, hide rewards that can't be bought again (that is, unique
|
||||
// rewards that the team already has). In the bought menu, hide rewards that
|
||||
// the team does not have or that can be bought again.
|
||||
if (show_purchased != (team->has_reward(reward.key) && reward.is_unique)) {
|
||||
continue;
|
||||
}
|
||||
if (!show_item_rewards && !reward.reward_item.empty()) {
|
||||
|
||||
+7
-3
@@ -119,10 +119,10 @@ enum SendServerInitFlag {
|
||||
USE_SECONDARY_MESSAGE = 0x02,
|
||||
};
|
||||
|
||||
S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B<0xB4>
|
||||
S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4>
|
||||
prepare_server_init_contents_console(
|
||||
uint32_t server_key, uint32_t client_key, uint8_t flags);
|
||||
S_ServerInitWithAfterMessage_BB_03_9B<0xB4>
|
||||
S_ServerInitWithAfterMessageT_BB_03_9B<0xB4>
|
||||
prepare_server_init_contents_bb(
|
||||
const parray<uint8_t, 0x30>& server_key,
|
||||
const parray<uint8_t, 0x30>& client_key,
|
||||
@@ -191,6 +191,10 @@ void send_text_message(Channel& ch, const std::string& text);
|
||||
void send_text_message(std::shared_ptr<Client> c, const std::string& text);
|
||||
void send_text_message(std::shared_ptr<Lobby> l, const std::string& text);
|
||||
void send_text_message(std::shared_ptr<ServerState> s, const std::string& text);
|
||||
void send_scrolling_message_bb(std::shared_ptr<Client> c, const std::string& text);
|
||||
void send_text_or_scrolling_message(std::shared_ptr<Client> c, const std::string& text, const std::string& scrolling);
|
||||
void send_text_or_scrolling_message(
|
||||
std::shared_ptr<Lobby> l, std::shared_ptr<Client> exclude_c, const std::string& text, const std::string& scrolling);
|
||||
|
||||
std::string prepare_chat_data(
|
||||
Version version,
|
||||
@@ -283,7 +287,7 @@ void send_update_lobby_data_bb(std::shared_ptr<Client> c);
|
||||
void send_player_join_notification(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l, std::shared_ptr<Client> joining_client);
|
||||
void send_player_leave_notification(std::shared_ptr<Lobby> l, uint8_t leaving_client_id);
|
||||
void send_self_leave_notification(std::shared_ptr<Client> c);
|
||||
void send_get_player_info(std::shared_ptr<Client> c);
|
||||
void send_get_player_info(std::shared_ptr<Client> c, bool request_extended = false);
|
||||
|
||||
void send_execute_item_trade(std::shared_ptr<Client> c, const std::vector<ItemData>& items);
|
||||
void send_execute_card_trade(std::shared_ptr<Client> c, const std::vector<std::pair<uint32_t, uint32_t>>& card_to_count);
|
||||
|
||||
+51
-32
@@ -28,10 +28,10 @@ using namespace std;
|
||||
using namespace std::placeholders;
|
||||
|
||||
void Server::disconnect_client(shared_ptr<Client> c) {
|
||||
if (c->channel.is_virtual_connection) {
|
||||
server_log.info("Client disconnected: C-%" PRIX64 " on virtual connection %p", c->id, c->channel.bev.get());
|
||||
if (c->channel.virtual_network_id) {
|
||||
server_log.info("Client disconnected: C-%" PRIX64 " on N-%" PRIu64, c->id, c->channel.virtual_network_id);
|
||||
} else if (c->channel.bev) {
|
||||
server_log.info("Client disconnected: C-%" PRIX64 " on fd %d", c->id, bufferevent_getfd(c->channel.bev.get()));
|
||||
server_log.info("Client disconnected: C-%" PRIX64, c->id);
|
||||
} else {
|
||||
server_log.info("Client C-%" PRIX64 " removed from game server", c->id);
|
||||
}
|
||||
@@ -85,17 +85,20 @@ void Server::destroy_clients() {
|
||||
void Server::dispatch_on_listen_accept(
|
||||
struct evconnlistener* listener, evutil_socket_t fd,
|
||||
struct sockaddr* address, int socklen, void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_listen_accept(listener, fd, address,
|
||||
socklen);
|
||||
reinterpret_cast<Server*>(ctx)->on_listen_accept(listener, fd, address, socklen);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_listen_error(
|
||||
struct evconnlistener* listener, void* ctx) {
|
||||
void Server::dispatch_on_listen_error(struct evconnlistener* listener, void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_listen_error(listener);
|
||||
}
|
||||
|
||||
void Server::on_listen_accept(
|
||||
struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr*, int) {
|
||||
void Server::on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr*, int) {
|
||||
struct sockaddr_storage remote_addr;
|
||||
get_socket_addresses(fd, nullptr, &remote_addr);
|
||||
if (this->state->banned_ipv4_ranges->check(remote_addr)) {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
int listen_fd = evconnlistener_get_fd(listener);
|
||||
ListeningSocket* listening_socket;
|
||||
@@ -108,9 +111,8 @@ void Server::on_listen_accept(
|
||||
return;
|
||||
}
|
||||
|
||||
struct bufferevent* bev = bufferevent_socket_new(this->base.get(), fd,
|
||||
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
auto c = make_shared<Client>(this->shared_from_this(), bev, listening_socket->version, listening_socket->behavior);
|
||||
struct bufferevent* bev = bufferevent_socket_new(this->base.get(), fd, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
auto c = make_shared<Client>(this->shared_from_this(), bev, 0, 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;
|
||||
@@ -127,19 +129,24 @@ 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) {
|
||||
auto c = make_shared<Client>(this->shared_from_this(), bev, version, initial_state);
|
||||
void Server::connect_virtual_client(
|
||||
struct bufferevent* bev,
|
||||
uint64_t virtual_network_id,
|
||||
uint32_t address,
|
||||
uint16_t client_port,
|
||||
uint16_t server_port,
|
||||
Version version,
|
||||
ServerBehavior initial_state) {
|
||||
auto c = make_shared<Client>(this->shared_from_this(), bev, virtual_network_id, 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;
|
||||
this->state->channel_to_client.emplace(&c->channel, c);
|
||||
|
||||
server_log.info(
|
||||
"Client connected: C-%" PRIX64 " on virtual connection %p via T-%hu-%s-%s-VI",
|
||||
"Client connected: C-%" PRIX64 " on virtual network N-%" PRIu64 " via T-%hu-%s-%s-VI",
|
||||
c->id,
|
||||
bev,
|
||||
virtual_network_id,
|
||||
server_port,
|
||||
name_for_enum(version),
|
||||
name_for_enum(initial_state));
|
||||
@@ -159,7 +166,7 @@ void Server::connect_client(
|
||||
}
|
||||
}
|
||||
|
||||
void Server::connect_client(shared_ptr<Client> c, Channel&& ch) {
|
||||
void Server::connect_virtual_client(shared_ptr<Client> c, Channel&& ch) {
|
||||
c->channel.replace_with(std::move(ch), Server::on_client_input, Server::on_client_error, this, string_printf("C-%" PRIX64, c->id));
|
||||
this->state->channel_to_client.emplace(&c->channel, c);
|
||||
server_log.info("Client C-%" PRIX64 " added to game server", c->id);
|
||||
@@ -281,14 +288,14 @@ shared_ptr<Client> Server::get_client() const {
|
||||
}
|
||||
|
||||
vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident) const {
|
||||
int64_t serial_number_hex = -1;
|
||||
int64_t serial_number_dec = -1;
|
||||
int64_t account_id_hex = -1;
|
||||
int64_t account_id_dec = -1;
|
||||
try {
|
||||
serial_number_dec = stoul(ident, nullptr, 10);
|
||||
account_id_dec = stoul(ident, nullptr, 10);
|
||||
} catch (const invalid_argument&) {
|
||||
}
|
||||
try {
|
||||
serial_number_hex = stoul(ident, nullptr, 16);
|
||||
account_id_hex = stoul(ident, nullptr, 16);
|
||||
} catch (const invalid_argument&) {
|
||||
}
|
||||
|
||||
@@ -297,31 +304,35 @@ vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident
|
||||
vector<shared_ptr<Client>> results;
|
||||
for (const auto& it : this->state->channel_to_client) {
|
||||
auto c = it.second;
|
||||
if (c->license && c->license->serial_number == serial_number_dec) {
|
||||
results.emplace_back(std::move(c));
|
||||
if (c->login && c->login->account->account_id == account_id_hex) {
|
||||
results.emplace_back(c);
|
||||
continue;
|
||||
}
|
||||
if (c->license && c->license->serial_number == serial_number_hex) {
|
||||
results.emplace_back(std::move(c));
|
||||
if (c->login && c->login->account->account_id == account_id_dec) {
|
||||
results.emplace_back(c);
|
||||
continue;
|
||||
}
|
||||
if (c->license && c->license->bb_username == ident) {
|
||||
results.emplace_back(std::move(c));
|
||||
if (c->login && c->login->xb_license && c->login->xb_license->gamertag == ident) {
|
||||
results.emplace_back(c);
|
||||
continue;
|
||||
}
|
||||
if (c->login && c->login->bb_license && c->login->bb_license->username == ident) {
|
||||
results.emplace_back(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto p = c->character(false, false);
|
||||
if (p && p->disp.name.eq(ident, p->inventory.language)) {
|
||||
results.emplace_back(std::move(c));
|
||||
results.emplace_back(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c->channel.name == ident) {
|
||||
results.emplace_back(std::move(c));
|
||||
results.emplace_back(c);
|
||||
continue;
|
||||
}
|
||||
if (starts_with(c->channel.name, ident + " ")) {
|
||||
results.emplace_back(std::move(c));
|
||||
results.emplace_back(c);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -329,6 +340,14 @@ vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident
|
||||
return results;
|
||||
}
|
||||
|
||||
vector<shared_ptr<Client>> Server::all_clients() const {
|
||||
vector<shared_ptr<Client>> ret;
|
||||
for (const auto& it : this->state->channel_to_client) {
|
||||
ret.emplace_back(it.second);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
shared_ptr<struct event_base> Server::get_base() const {
|
||||
return this->base;
|
||||
}
|
||||
|
||||
+10
-4
@@ -23,14 +23,20 @@ public:
|
||||
void listen(const std::string& addr_str, int port, Version version, ServerBehavior initial_state);
|
||||
void add_socket(const std::string& addr_str, int fd, Version version, ServerBehavior initial_state);
|
||||
|
||||
void connect_client(struct bufferevent* bev, uint32_t address,
|
||||
uint16_t client_port, uint16_t server_port,
|
||||
Version version, ServerBehavior initial_state);
|
||||
void connect_client(std::shared_ptr<Client> c, Channel&& ch);
|
||||
void connect_virtual_client(
|
||||
struct bufferevent* bev,
|
||||
uint64_t virtual_network_id,
|
||||
uint32_t address,
|
||||
uint16_t client_port,
|
||||
uint16_t server_port,
|
||||
Version version,
|
||||
ServerBehavior initial_state);
|
||||
void connect_virtual_client(std::shared_ptr<Client> c, Channel&& ch);
|
||||
void disconnect_client(std::shared_ptr<Client> c);
|
||||
|
||||
std::shared_ptr<Client> get_client() const;
|
||||
std::vector<std::shared_ptr<Client>> get_clients_by_identifier(const std::string& ident) const;
|
||||
std::vector<std::shared_ptr<Client>> all_clients() const;
|
||||
std::shared_ptr<struct event_base> get_base() const;
|
||||
|
||||
inline std::shared_ptr<ServerState> get_state() const {
|
||||
|
||||
+335
-160
@@ -178,10 +178,10 @@ CommandDefinition c_on(
|
||||
Run a command on a specific game server client or proxy server session.\n\
|
||||
Without this prefix, commands that affect a single client or session will\n\
|
||||
work only if there's exactly one connected client or open session. SESSION\n\
|
||||
may be a client ID (e.g. C-3), a player name, a license serial number, or\n\
|
||||
a BB account username. For proxy commands, SESSION should be the session\n\
|
||||
ID, which generally is the same as the player\'s serial number and appears\n\
|
||||
after \"LinkedSession:\" in the log output.",
|
||||
may be a client ID (e.g. C-3), a player name, an account ID, an Xbox\n\
|
||||
gamertag, or a BB account username. For proxy commands, SESSION should be\n\
|
||||
the session ID, which generally is the same as the player\'s account ID\n\
|
||||
and appears after \"LinkedSession:\" in the log output.",
|
||||
false,
|
||||
+[](CommandArgs& args) {
|
||||
size_t session_name_end = skip_non_whitespace(args.args, 0);
|
||||
@@ -197,6 +197,7 @@ CommandDefinition c_on(
|
||||
CommandDefinition c_reload(
|
||||
"reload", "reload ITEM [ITEM...]\n\
|
||||
Reload various parts of the server configuration. The items are:\n\
|
||||
accounts - reindex user accounts\n\
|
||||
battle-params - reload the BB enemy stats files\n\
|
||||
bb-keys - reload BB private keys\n\
|
||||
config - reload most fields from config.json\n\
|
||||
@@ -209,7 +210,6 @@ CommandDefinition c_reload(
|
||||
item-definitions - reload item definitions files\n\
|
||||
item-name-index - regenerate item name list\n\
|
||||
level-table - reload the level-up tables\n\
|
||||
licenses - reindex user licenses\n\
|
||||
patch-files - reindex the PC and BB patch directories\n\
|
||||
quests - reindex all quests (including Episode3 download quests)\n\
|
||||
set-tables - reload set data tables\n\
|
||||
@@ -228,8 +228,8 @@ CommandDefinition c_reload(
|
||||
for (const auto& type : types) {
|
||||
if (type == "bb-keys") {
|
||||
args.s->load_bb_private_keys(true);
|
||||
} else if (type == "licenses") {
|
||||
args.s->load_licenses(true);
|
||||
} else if (type == "accounts") {
|
||||
args.s->load_accounts(true);
|
||||
} else if (type == "patch-files") {
|
||||
args.s->load_patch_indexes(true);
|
||||
} else if (type == "ep3-cards") {
|
||||
@@ -278,183 +278,358 @@ CommandDefinition c_reload(
|
||||
}
|
||||
});
|
||||
|
||||
CommandDefinition c_add_license(
|
||||
"add-license", "add-license PARAMETERS...\n\
|
||||
Add a license to the server. <parameters> is some subset of the following:\n\
|
||||
bb-username=<username> (BB username)\n\
|
||||
bb-password=<password> (BB password)\n\
|
||||
xb-gamertag=<gamertag> (Xbox gamertag)\n\
|
||||
xb-user-id=<user-id> (Xbox user ID)\n\
|
||||
xb-account-id=<account-id> (Xbox account ID)\n\
|
||||
gc-password=<password> (GC password)\n\
|
||||
dc-nte-serial-number=<serial-number> (DC NTE serial number)\n\
|
||||
dc-nte-access-key=<access-key> (DC NTE access key)\n\
|
||||
access-key=<access-key> (DC/GC/PC access key)\n\
|
||||
serial=<serial-number> (decimal serial number; required for all licenses)\n\
|
||||
flags=<privilege-mask> (see below)\n\
|
||||
If flags is specified in hex, the meanings of bits are:\n\
|
||||
00000001 = Can kick other users offline\n\
|
||||
00000002 = Can ban other users\n\
|
||||
00000004 = Can silence other users\n\
|
||||
00000010 = Can change lobby events\n\
|
||||
00000020 = Can make server-wide announcements\n\
|
||||
00000040 = Ignores game join restrictions (e.g. level/quest requirements)\n\
|
||||
01000000 = Can use debugging commands\n\
|
||||
02000000 = Can use cheat commands even if cheat mode is disabled\n\
|
||||
04000000 = Can play any quest without progression/flags restrictions\n\
|
||||
08000000 = Can use chat commands even if disabled in config.json\n\
|
||||
80000000 = License is a shared serial (disables Access Key and password\n\
|
||||
checks; players will get Guild Cards based on their player names)\n\
|
||||
There are also shorthands for some general privilege levels:\n\
|
||||
flags=moderator = 00000007\n\
|
||||
flags=admin = 000000FF\n\
|
||||
flags=root = 7FFFFFFF",
|
||||
CommandDefinition c_list_accounts(
|
||||
"list-accounts", "list-accounts\n\
|
||||
List all accounts registered on the server.",
|
||||
true,
|
||||
+[](CommandArgs& args) {
|
||||
auto l = args.s->license_index->create_license();
|
||||
|
||||
for (const string& token : split(args.args, ' ')) {
|
||||
if (starts_with(token, "bb-username=")) {
|
||||
l->bb_username = token.substr(12);
|
||||
} else if (starts_with(token, "bb-password=")) {
|
||||
l->bb_password = token.substr(12);
|
||||
} else if (starts_with(token, "xb-gamertag=")) {
|
||||
l->xb_gamertag = token.substr(12);
|
||||
} else if (starts_with(token, "xb-user-id=")) {
|
||||
l->xb_user_id = stoull(token.substr(11), nullptr, 16);
|
||||
} else if (starts_with(token, "xb-account-id=")) {
|
||||
l->xb_account_id = stoull(token.substr(14), nullptr, 16);
|
||||
} else if (starts_with(token, "gc-password=")) {
|
||||
l->gc_password = token.substr(12);
|
||||
} else if (starts_with(token, "dc-nte-serial-number=")) {
|
||||
l->dc_nte_serial_number = token.substr(21);
|
||||
} else if (starts_with(token, "dc-nte-access-key=")) {
|
||||
l->dc_nte_access_key = token.substr(18);
|
||||
} else if (starts_with(token, "access-key=")) {
|
||||
l->access_key = token.substr(11);
|
||||
} else if (starts_with(token, "serial=")) {
|
||||
l->serial_number = stoul(token.substr(7), nullptr, 0);
|
||||
|
||||
} else if (starts_with(token, "flags=")) {
|
||||
string mask = token.substr(6);
|
||||
if (mask == "normal") {
|
||||
l->flags = 0;
|
||||
} else if (mask == "mod") {
|
||||
l->replace_all_flags(License::Flag::MODERATOR);
|
||||
} else if (mask == "admin") {
|
||||
l->replace_all_flags(License::Flag::ADMINISTRATOR);
|
||||
} else if (mask == "root") {
|
||||
l->replace_all_flags(License::Flag::ROOT);
|
||||
} else {
|
||||
l->flags = stoul(mask, nullptr, 16);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw invalid_argument("incorrect field: " + token);
|
||||
auto accounts = args.s->account_index->all();
|
||||
if (accounts.empty()) {
|
||||
fprintf(stderr, "No accounts registered\n");
|
||||
} else {
|
||||
for (const auto& a : accounts) {
|
||||
a->print(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
if (!l->serial_number) {
|
||||
throw invalid_argument("license does not contain serial number");
|
||||
}
|
||||
|
||||
l->save();
|
||||
args.s->license_index->add(l);
|
||||
fprintf(stderr, "License added\n");
|
||||
});
|
||||
|
||||
CommandDefinition c_update_license(
|
||||
"update-license", "update-license SERIAL-NUMBER PARAMETERS...\n\
|
||||
Update an existing license. <serial-number> specifies which license to\n\
|
||||
update. The options in <parameters> are the same as for the add-license\n\
|
||||
command.",
|
||||
uint32_t parse_account_flags(const string& flags_str) {
|
||||
try {
|
||||
size_t end_pos = 0;
|
||||
uint32_t ret = stoul(flags_str, &end_pos, 16);
|
||||
if (end_pos == flags_str.size()) {
|
||||
return ret;
|
||||
}
|
||||
} catch (const exception&) {
|
||||
}
|
||||
|
||||
uint32_t ret = 0;
|
||||
auto tokens = split(flags_str, ',');
|
||||
for (const auto& token : tokens) {
|
||||
string token_upper = toupper(token);
|
||||
if (token_upper == "NONE") {
|
||||
// Nothing to do
|
||||
} else if (token_upper == "KICK_USER") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::KICK_USER);
|
||||
} else if (token_upper == "BAN_USER") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::BAN_USER);
|
||||
} else if (token_upper == "SILENCE_USER") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::SILENCE_USER);
|
||||
} else if (token_upper == "MODERATOR") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::MODERATOR);
|
||||
} else if (token_upper == "CHANGE_EVENT") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::CHANGE_EVENT);
|
||||
} else if (token_upper == "ANNOUNCE") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::ANNOUNCE);
|
||||
} else if (token_upper == "FREE_JOIN_GAMES") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::FREE_JOIN_GAMES);
|
||||
} else if (token_upper == "ADMINISTRATOR") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::ADMINISTRATOR);
|
||||
} else if (token_upper == "DEBUG") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::DEBUG);
|
||||
} else if (token_upper == "CHEAT_ANYWHERE") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::CHEAT_ANYWHERE);
|
||||
} else if (token_upper == "DISABLE_QUEST_REQUIREMENTS") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::DISABLE_QUEST_REQUIREMENTS);
|
||||
} else if (token_upper == "ALWAYS_ENABLE_CHAT_COMMANDS") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::ALWAYS_ENABLE_CHAT_COMMANDS);
|
||||
} else if (token_upper == "ROOT") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::ROOT);
|
||||
} else if (token_upper == "IS_SHARED_ACCOUNT") {
|
||||
ret |= static_cast<uint32_t>(Account::Flag::IS_SHARED_ACCOUNT);
|
||||
} else {
|
||||
throw runtime_error("invalid flag name: " + token_upper);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
CommandDefinition c_add_account(
|
||||
"add-account", "add-account [PARAMETERS...]\n\
|
||||
Add an account to the server. <parameters> is some subset of:\n\
|
||||
id=ACCOUNT-ID: preferred account ID in hex (optional)\n\
|
||||
flags=FLAGS: behaviors and permissions for the account (see below)\n\
|
||||
ep3-current-meseta=MESETA: Episode 3 Meseta value\n\
|
||||
ep3-total-meseta=MESETA: Episode 3 total Meseta ever earned\n\
|
||||
temporary: marks the account as temporary; it is not saved to disk and\n\
|
||||
therefore will be deleted when the server shuts down\n\
|
||||
If given, FLAGS is a comma-separated list of zero or more the following:\n\
|
||||
NONE: Placeholder if no other flags are specified\n\
|
||||
KICK_USER: Can kick other users offline\n\
|
||||
BAN_USER: Can ban other users\n\
|
||||
SILENCE_USER: Can silence other users\n\
|
||||
MODERATOR: Alias for all of the above flags\n\
|
||||
CHANGE_EVENT: Can change lobby events\n\
|
||||
ANNOUNCE: Can make server-wide announcements\n\
|
||||
FREE_JOIN_GAMES: Ignores game restrictions (level/quest requirements)\n\
|
||||
ADMINISTRATOR: Alias for all of the above flags (including MODERATOR)\n\
|
||||
DEBUG: Can use debugging commands\n\
|
||||
CHEAT_ANYWHERE: Can use cheat commands even if cheat mode is disabled\n\
|
||||
DISABLE_QUEST_REQUIREMENTS: Can play any quest without progression\n\
|
||||
restrictions\n\
|
||||
ALWAYS_ENABLE_CHAT_COMMANDS: Can use chat commands even if they are\n\
|
||||
disabled in config.json\n\
|
||||
ROOT: Alias for all of the above flags (including ADMINISTRATOR)\n\
|
||||
IS_SHARED_ACCOUNT: Account is a shared serial (disables Access Key and\n\
|
||||
password checks; players will get Guild Cards based on their player\n\
|
||||
names)",
|
||||
true,
|
||||
+[](CommandArgs& args) {
|
||||
auto account = make_shared<Account>();
|
||||
for (const string& token : split(args.args, ' ')) {
|
||||
if (starts_with(token, "id=")) {
|
||||
account->account_id = stoul(token.substr(3), nullptr, 16);
|
||||
} else if (starts_with(token, "ep3-current-meseta=")) {
|
||||
account->ep3_current_meseta = stoul(token.substr(19), nullptr, 0);
|
||||
} else if (starts_with(token, "ep3-total-meseta=")) {
|
||||
account->ep3_total_meseta_earned = stoul(token.substr(17), nullptr, 0);
|
||||
} else if (token == "temporary") {
|
||||
account->is_temporary = true;
|
||||
} else if (starts_with(token, "flags=")) {
|
||||
account->flags = parse_account_flags(token.substr(6));
|
||||
} else {
|
||||
throw invalid_argument("invalid account field: " + token);
|
||||
}
|
||||
}
|
||||
args.s->account_index->add(account);
|
||||
account->save();
|
||||
fprintf(stderr, "Account %08" PRIX32 " added\n", account->account_id);
|
||||
});
|
||||
CommandDefinition c_update_account(
|
||||
"update-account", "update-account ACCOUNT-ID PARAMETERS...\n\
|
||||
Update an existing license. ACCOUNT-ID (8 hex digits) specifies which\n\
|
||||
account to update. The options are similar to the add-account command:\n\
|
||||
flags=FLAGS: sets behaviors and permissions for the account (same as\n\
|
||||
with add-account)\n\
|
||||
ban-duration=DURATION: bans this account for the specified duration; the\n\
|
||||
duration should be of the form 3d, 2w, 1mo, or 1y\n\
|
||||
unban: clears any existing ban from this account\n\
|
||||
ep3-current-meseta=MESETA: sets Episode 3 Meseta value\n\
|
||||
ep3-total-meseta=MESETA: sets Episode 3 total Meseta ever earned\n\
|
||||
temporary: marks the account as temporary; it is not saved to disk and\n\
|
||||
therefore will be deleted when the server shuts down\n\
|
||||
permanent: if the account was temporary, makes it non-temporary",
|
||||
true,
|
||||
+[](CommandArgs& args) {
|
||||
auto tokens = split(args.args, ' ');
|
||||
if (tokens.size() < 2) {
|
||||
throw runtime_error("not enough arguments");
|
||||
}
|
||||
uint32_t serial_number = stoul(tokens[0]);
|
||||
auto account = args.s->account_index->from_account_id(stoul(tokens[0], nullptr, 16));
|
||||
tokens.erase(tokens.begin());
|
||||
auto orig_l = args.s->license_index->get(serial_number);
|
||||
auto l = args.s->license_index->create_license();
|
||||
*l = *orig_l;
|
||||
|
||||
args.s->license_index->remove(orig_l->serial_number);
|
||||
try {
|
||||
for (const string& token : tokens) {
|
||||
if (starts_with(token, "bb-username=")) {
|
||||
l->bb_username = token.substr(12);
|
||||
} else if (starts_with(token, "bb-password=")) {
|
||||
l->bb_password = token.substr(12);
|
||||
} else if (starts_with(token, "xb-gamertag=")) {
|
||||
l->xb_gamertag = token.substr(12);
|
||||
} else if (starts_with(token, "xb-user-id=")) {
|
||||
l->xb_user_id = stoull(token.substr(11), nullptr, 16);
|
||||
} else if (starts_with(token, "xb-account-id=")) {
|
||||
l->xb_account_id = stoull(token.substr(14), nullptr, 16);
|
||||
} else if (starts_with(token, "gc-password=")) {
|
||||
l->gc_password = token.substr(12);
|
||||
} else if (starts_with(token, "dc-nte-serial-number=")) {
|
||||
l->dc_nte_serial_number = token.substr(21);
|
||||
} else if (starts_with(token, "dc-nte-access-key=")) {
|
||||
l->dc_nte_access_key = token.substr(18);
|
||||
} else if (starts_with(token, "access-key=")) {
|
||||
l->access_key = token.substr(11);
|
||||
} else if (starts_with(token, "serial=")) {
|
||||
l->serial_number = stoul(token.substr(7), nullptr, 0);
|
||||
|
||||
} else if (starts_with(token, "flags=")) {
|
||||
string mask = token.substr(6);
|
||||
if (mask == "normal") {
|
||||
l->flags = 0;
|
||||
} else if (mask == "mod") {
|
||||
l->replace_all_flags(License::Flag::MODERATOR);
|
||||
} else if (mask == "admin") {
|
||||
l->replace_all_flags(License::Flag::ADMINISTRATOR);
|
||||
} else if (mask == "root") {
|
||||
l->replace_all_flags(License::Flag::ROOT);
|
||||
} else {
|
||||
l->flags = stoul(mask, nullptr, 16);
|
||||
}
|
||||
|
||||
// Do all the parsing first, then the updates afterward, so we won't
|
||||
// partially update the account if parsing a later option fails
|
||||
int64_t new_ep3_current_meseta = -1;
|
||||
int64_t new_ep3_total_meseta = -1;
|
||||
int64_t new_flags = -1;
|
||||
uint8_t new_is_temporary = 0xFF;
|
||||
int64_t new_ban_duration = -1;
|
||||
for (const string& token : tokens) {
|
||||
if (starts_with(token, "ep3-current-meseta=")) {
|
||||
new_ep3_current_meseta = stoul(token.substr(19), nullptr, 0);
|
||||
} else if (starts_with(token, "ep3-total-meseta=")) {
|
||||
new_ep3_total_meseta = stoul(token.substr(17), nullptr, 0);
|
||||
} else if (token == "temporary") {
|
||||
new_is_temporary = 1;
|
||||
} else if (token == "permanent") {
|
||||
new_is_temporary = 0;
|
||||
} else if (starts_with(token, "flags=")) {
|
||||
new_flags = parse_account_flags(token.substr(6));
|
||||
} else if (token == "unban") {
|
||||
new_ban_duration = 0;
|
||||
} else if (starts_with(token, "ban-duration=")) {
|
||||
auto duration_str = token.substr(13);
|
||||
if (ends_with(duration_str, "s")) {
|
||||
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 1000000LL;
|
||||
} else if (ends_with(duration_str, "m")) {
|
||||
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 60000000LL;
|
||||
} else if (ends_with(duration_str, "h")) {
|
||||
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 3600000000LL;
|
||||
} else if (ends_with(duration_str, "d")) {
|
||||
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 86400000000LL;
|
||||
} else if (ends_with(duration_str, "w")) {
|
||||
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 604800000000LL;
|
||||
} else if (ends_with(duration_str, "mo")) {
|
||||
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 2)) * 2952000000000LL;
|
||||
} else if (ends_with(duration_str, "y")) {
|
||||
new_ban_duration = stoull(duration_str.substr(0, duration_str.size() - 1)) * 31536000000000LL;
|
||||
} else {
|
||||
throw invalid_argument("incorrect field: " + token);
|
||||
throw runtime_error("invalid time unit");
|
||||
}
|
||||
} else {
|
||||
throw invalid_argument("invalid account field: " + token);
|
||||
}
|
||||
|
||||
if (!l->serial_number) {
|
||||
throw invalid_argument("license does not contain serial number");
|
||||
}
|
||||
} catch (const exception&) {
|
||||
args.s->license_index->add(orig_l);
|
||||
throw;
|
||||
}
|
||||
|
||||
l->save();
|
||||
args.s->license_index->add(l);
|
||||
fprintf(stderr, "License updated\n");
|
||||
if (new_ban_duration >= 0) {
|
||||
account->ban_end_time = now() + new_ban_duration;
|
||||
}
|
||||
if (new_ep3_current_meseta >= 0) {
|
||||
account->ep3_current_meseta = new_ep3_current_meseta;
|
||||
}
|
||||
if (new_ep3_total_meseta >= 0) {
|
||||
account->ep3_total_meseta_earned = new_ep3_total_meseta;
|
||||
}
|
||||
if (new_flags >= 0) {
|
||||
account->flags = new_flags;
|
||||
}
|
||||
if (new_is_temporary != 0xFF) {
|
||||
account->is_temporary = new_is_temporary;
|
||||
}
|
||||
|
||||
account->save();
|
||||
fprintf(stderr, "Account %08" PRIX32 " updated\n", account->account_id);
|
||||
|
||||
if (new_ban_duration > 0) {
|
||||
args.s->disconnect_all_banned_clients();
|
||||
}
|
||||
});
|
||||
CommandDefinition c_delete_account(
|
||||
"delete-account", "delete-account ACCOUNT-ID\n\
|
||||
Delete an account from the server. If a player is online with the deleted\n\
|
||||
account, they will not be automatically disconnected.",
|
||||
true,
|
||||
+[](CommandArgs& args) {
|
||||
auto account = args.s->account_index->from_account_id(stoul(args.args, nullptr, 16));
|
||||
args.s->account_index->remove(account->account_id);
|
||||
account->is_temporary = true;
|
||||
account->delete_file();
|
||||
fprintf(stderr, "Account deleted\n");
|
||||
});
|
||||
|
||||
CommandDefinition c_add_license(
|
||||
"add-license", "add-license ACCOUNT-ID TYPE CREDENTIALS...\n\
|
||||
Add a license to an account. Each account may have multiple licenses of\n\
|
||||
each type. The types are:\n\
|
||||
DC-NTE: CREDENTIALS is serial number and access key (16 characters each)\n\
|
||||
DC: CREDENTIALS is serial number and access key (8 characters each)\n\
|
||||
PC: CREDENTIALS is serial number and access key (8 characters each)\n\
|
||||
GC: CREDENTIALS is serial number (10 digits), access key (12 digits), and\n\
|
||||
password (up to 8 characters)\n\
|
||||
XB: CREDENTIALS is gamertag (up to 16 characters), user ID (16 hex\n\
|
||||
digits), and account ID (16 hex digits)\n\
|
||||
BB: CREDENTIALS is username and password (up to 16 characters each)\n\
|
||||
Examples (adding licenses to account 385A92C4):\n\
|
||||
add-license 385A92C4 DC 107862F9 d38XTu2p\n\
|
||||
add-license 385A92C4 GC 0418572923 282949185033 hunter2\n\
|
||||
add-license 385A92C4 BB user1 trustno1",
|
||||
true,
|
||||
+[](CommandArgs& args) {
|
||||
auto tokens = split(args.args, ' ');
|
||||
if (tokens.size() < 3) {
|
||||
throw runtime_error("not enough arguments");
|
||||
}
|
||||
|
||||
auto account = args.s->account_index->from_account_id(stoul(tokens[0], nullptr, 16));
|
||||
|
||||
string type_str = toupper(tokens[1]);
|
||||
if (type_str == "DC-NTE") {
|
||||
if (tokens.size() != 4) {
|
||||
throw runtime_error("incorrect number of parameters");
|
||||
}
|
||||
auto license = make_shared<DCNTELicense>();
|
||||
license->serial_number = std::move(tokens[2]);
|
||||
license->access_key = std::move(tokens[3]);
|
||||
args.s->account_index->add_dc_nte_license(account, license);
|
||||
|
||||
} else if (type_str == "DC") {
|
||||
if (tokens.size() != 4) {
|
||||
throw runtime_error("incorrect number of parameters");
|
||||
}
|
||||
auto license = make_shared<V1V2License>();
|
||||
license->serial_number = stoul(tokens[2], nullptr, 16);
|
||||
license->access_key = std::move(tokens[3]);
|
||||
args.s->account_index->add_dc_license(account, license);
|
||||
|
||||
} else if (type_str == "PC") {
|
||||
if (tokens.size() != 4) {
|
||||
throw runtime_error("incorrect number of parameters");
|
||||
}
|
||||
auto license = make_shared<V1V2License>();
|
||||
license->serial_number = stoul(tokens[2], nullptr, 16);
|
||||
license->access_key = std::move(tokens[3]);
|
||||
args.s->account_index->add_pc_license(account, license);
|
||||
|
||||
} else if (type_str == "GC") {
|
||||
if (tokens.size() != 5) {
|
||||
throw runtime_error("incorrect number of parameters");
|
||||
}
|
||||
auto license = make_shared<GCLicense>();
|
||||
license->serial_number = stoul(tokens[2], nullptr, 10);
|
||||
license->access_key = std::move(tokens[3]);
|
||||
license->password = std::move(tokens[4]);
|
||||
args.s->account_index->add_gc_license(account, license);
|
||||
|
||||
} else if (type_str == "XB") {
|
||||
if (tokens.size() != 5) {
|
||||
throw runtime_error("incorrect number of parameters");
|
||||
}
|
||||
auto license = make_shared<XBLicense>();
|
||||
license->gamertag = std::move(tokens[2]);
|
||||
license->user_id = stoull(tokens[3], nullptr, 16);
|
||||
license->account_id = stoull(tokens[4], nullptr, 16);
|
||||
args.s->account_index->add_xb_license(account, license);
|
||||
|
||||
} else if (type_str == "BB") {
|
||||
if (tokens.size() != 4) {
|
||||
throw runtime_error("incorrect number of parameters");
|
||||
}
|
||||
auto license = make_shared<BBLicense>();
|
||||
license->username = std::move(tokens[2]);
|
||||
license->password = std::move(tokens[3]);
|
||||
args.s->account_index->add_bb_license(account, license);
|
||||
|
||||
} else {
|
||||
throw runtime_error("invalid license type");
|
||||
}
|
||||
|
||||
account->save();
|
||||
fprintf(stderr, "Account %08" PRIX32 " updated\n", account->account_id);
|
||||
});
|
||||
CommandDefinition c_delete_license(
|
||||
"delete-license", "delete-license SERIAL-NUMBER\n\
|
||||
Delete a license from the server.",
|
||||
"delete-license", "delete-license ACCOUNT-ID TYPE PRIMARY-CREDENTIAL\n\
|
||||
Delete a license from an account. ACCOUNT-ID and TYPE have the same\n\
|
||||
meanings as for add-license. PRIMARY-CREDENTIAL is the first credential\n\
|
||||
for the license type; specifically:\n\
|
||||
DC-NTE: PRIMARY-CREDENTIAL is the serial number\n\
|
||||
DC: PRIMARY-CREDENTIAL is the serial number (hex)\n\
|
||||
PC: PRIMARY-CREDENTIAL is the serial number (hex)\n\
|
||||
GC: PRIMARY-CREDENTIAL is the serial number (decimal)\n\
|
||||
XB: PRIMARY-CREDENTIAL is the gamertag\n\
|
||||
BB: PRIMARY-CREDENTIAL is the username\n\
|
||||
Examples (deleting licenses from account 385A92C4):\n\
|
||||
delete-license 385A92C4 DC 107862F9\n\
|
||||
delete-license 385A92C4 GC 0418572923\n\
|
||||
delete-license 385A92C4 BB user1",
|
||||
true,
|
||||
+[](CommandArgs& args) {
|
||||
uint32_t serial_number = stoul(args.args);
|
||||
auto l = args.s->license_index->get(serial_number);
|
||||
l->delete_file();
|
||||
args.s->license_index->remove(l->serial_number);
|
||||
fprintf(stderr, "License deleted\n");
|
||||
});
|
||||
CommandDefinition c_list_licenses(
|
||||
"list-licenses", "list-licenses\n\
|
||||
List all licenses registered on the server.",
|
||||
true,
|
||||
+[](CommandArgs& args) {
|
||||
for (const auto& l : args.s->license_index->all()) {
|
||||
string s = l->str();
|
||||
fprintf(stderr, "%s\n", s.c_str());
|
||||
auto tokens = split(args.args, ' ');
|
||||
if (tokens.size() != 3) {
|
||||
throw runtime_error("incorrect argument count");
|
||||
}
|
||||
|
||||
auto account = args.s->account_index->from_account_id(stoul(tokens[0], nullptr, 16));
|
||||
|
||||
string type_str = toupper(tokens[1]);
|
||||
if (type_str == "DC-NTE") {
|
||||
args.s->account_index->remove_dc_nte_license(account, tokens[2]);
|
||||
} else if (type_str == "DC") {
|
||||
args.s->account_index->remove_dc_license(account, stoul(tokens[2], nullptr, 16));
|
||||
} else if (type_str == "PC") {
|
||||
args.s->account_index->remove_pc_license(account, stoul(tokens[2], nullptr, 16));
|
||||
} else if (type_str == "GC") {
|
||||
args.s->account_index->remove_gc_license(account, stoul(tokens[2], nullptr, 0));
|
||||
} else if (type_str == "XB") {
|
||||
args.s->account_index->remove_xb_license(account, tokens[2]);
|
||||
} else if (type_str == "BB") {
|
||||
args.s->account_index->remove_bb_license(account, tokens[2]);
|
||||
} else {
|
||||
throw runtime_error("invalid license type");
|
||||
}
|
||||
|
||||
account->save();
|
||||
fprintf(stderr, "Account %08" PRIX32 " updated\n", account->account_id);
|
||||
});
|
||||
|
||||
CommandDefinition c_announce(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user