Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6f74e74c4 | |||
| 328980628a | |||
| 886e9b9f4f | |||
| 26d2ae416e | |||
| 62c4c82fcc | |||
| 11cc19fe3e | |||
| d1d045a70e | |||
| 54c790a63c | |||
| f1f5c1036a | |||
| 77d5436b15 | |||
| 678c60dd14 | |||
| d40d231584 | |||
| 00ddff7e46 | |||
| 5725af0f6b | |||
| 87248e7e67 | |||
| 712cfc9ac4 | |||
| 1d8befde8e | |||
| fb036cda37 | |||
| 136e2730de | |||
| ae47d92016 | |||
| b80ed0021b | |||
| 1d11879142 | |||
| a122b27b1f | |||
| cbba724ba1 | |||
| 2c51571ea4 | |||
| e1d774ce49 | |||
| b9e3973c76 | |||
| c878093c5f | |||
| 7210441878 | |||
| 36eeee5641 | |||
| 8d2ffba3e1 | |||
| 766d4e0c7a | |||
| a99f552e7c | |||
| 540a41a583 | |||
| 8cb7d2b2fe | |||
| 293f25d579 | |||
| 64763e76af | |||
| 69b7e7f998 | |||
| 5579bce5d9 | |||
| 0dd5e2ac10 | |||
| 155ed6bcf9 | |||
| 4e2f62bc73 | |||
| bf36a185a2 | |||
| 4c4c54c536 | |||
| e79e6944df | |||
| f6079e3078 | |||
| 31b49a71fb | |||
| 83260d5037 | |||
| 648da83aa1 | |||
| adf1db92c7 | |||
| 662ee48a64 | |||
| 446b521898 | |||
| d6db731149 | |||
| 9106a11be8 | |||
| 7bc58a757e | |||
| 27b5556e4b | |||
| b39b4197ed | |||
| a99647d4c7 | |||
| 10a6bafb2f | |||
| b4f7688b82 | |||
| 08e6b882f3 | |||
| 4adc174674 | |||
| 01b1f42bac | |||
| be4c7f80cb | |||
| 790363adb5 | |||
| 09b96a4a86 | |||
| 6ffa656ad4 | |||
| 3f2df68ac5 | |||
| a7f2ecefe5 | |||
| 46c2260d0f | |||
| 052dcf8c6e | |||
| cd5863fcde | |||
| 90de571457 | |||
| d9d33c2d65 | |||
| 09962696b7 | |||
| d143cbb461 | |||
| db7f7abfc4 | |||
| 6ba92d3a7a | |||
| 36a1e0dfae | |||
| 47f7e71ae9 | |||
| c2008f1f9c | |||
| 3c32a66064 | |||
| 41026fbd93 | |||
| d49750aa02 | |||
| 54f309030e | |||
| 093c25fce4 | |||
| a777dc8236 | |||
| 4044e4e5a6 | |||
| 036b4e9456 | |||
| 4074530a71 | |||
| 31eedd7e7e | |||
| df2dfd21e3 | |||
| 00b0f71bf4 | |||
| 1450a5acd3 | |||
| 2a138ea0b6 | |||
| 2534ff37de | |||
| d61cb1106d | |||
| d5f0c6aceb |
@@ -18,6 +18,7 @@ build
|
|||||||
|
|
||||||
# Files modified by the user and/or server that don't have defaults
|
# Files modified by the user and/or server that don't have defaults
|
||||||
system/config.json
|
system/config.json
|
||||||
|
system/ep3/battle-records/*.mzr
|
||||||
system/ep3/battle-records/*.mzrd
|
system/ep3/battle-records/*.mzrd
|
||||||
system/ep3/tournament-state.json
|
system/ep3/tournament-state.json
|
||||||
system/licenses.nsi
|
system/licenses.nsi
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ set(SOURCES
|
|||||||
src/Map.cc
|
src/Map.cc
|
||||||
src/Menu.cc
|
src/Menu.cc
|
||||||
src/NetworkAddresses.cc
|
src/NetworkAddresses.cc
|
||||||
|
src/PatchDownloadSession.cc
|
||||||
src/PatchFileIndex.cc
|
src/PatchFileIndex.cc
|
||||||
src/PlayerInventory.cc
|
src/PlayerInventory.cc
|
||||||
src/PlayerSubordinates.cc
|
src/PlayerSubordinates.cc
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ There are multiple PSO quest formats out there; newserv supports all of them. It
|
|||||||
4. *Episode 3 quests don't go in the system/quests directory. See the [Episode 3 section](#episode-3-features) section below.*
|
4. *Episode 3 quests don't go in the system/quests directory. See the [Episode 3 section](#episode-3-features) section below.*
|
||||||
5. *Quest source can be assembled into a .bin or .bind file with `newserv assemble-quest-script FILENAME.txt`. See system/quests/retrieval/q058-gc-e.bin.txt for an annotated example; this is the English GameCube version of Lost HEAT SWORD.*
|
5. *Quest source can be assembled into a .bin or .bind file with `newserv assemble-quest-script FILENAME.txt`. See system/quests/retrieval/q058-gc-e.bin.txt for an annotated example; this is the English GameCube version of Lost HEAT SWORD.*
|
||||||
|
|
||||||
Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/). These files can be encoded in any of the formats described above, except .qst.
|
Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/maps/). These files can be encoded in any of the formats described above, except .qst.
|
||||||
|
|
||||||
When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats.
|
When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats.
|
||||||
|
|
||||||
@@ -452,9 +452,7 @@ Episode 3 state and game data is stored in the system/ep3 directory. The files i
|
|||||||
* card-text.mnr: Compressed card text archive. Generally only used for debugging.
|
* card-text.mnr: Compressed card text archive. Generally only used for debugging.
|
||||||
* card-text.mnrd: Decompressed card text archive; same format as TextCardE.bin. Generally only used for debugging.
|
* card-text.mnrd: Decompressed card text archive; same format as TextCardE.bin. Generally only used for debugging.
|
||||||
* com-decks.json: COM decks used in tournaments. The default decks in this file come from logs from Sega's servers, so the file doesn't include every COM deck Sega ever made - the rest are probably lost to time.
|
* com-decks.json: COM decks used in tournaments. The default decks in this file come from logs from Sega's servers, so the file doesn't include every COM deck Sega ever made - the rest are probably lost to time.
|
||||||
* maps/: Online free battle and quest maps (.mnm/.bin/.mnmd/.bind files). newserv comes with the default online maps, as well as some fan-made variations and quests to help new players get up to speed.
|
* maps/: Online free battle and quest maps (.mnm/.bin/.mnmd/.bind files). newserv comes with the default online maps, as well as some fan-made variations and quests to help new players get up to speed. Within the maps/ directory, each subdirectory is treated as a separate category and may be optionally downloadable or available at the battle setup counter. The category.json file in each subdirectory specifies the category's behavior; see system/ep3/maps/online/category.json for a documented example.
|
||||||
* maps-download/: Download maps and quests (.mnm/.bin/.mnmd/.bind files). There are two subcategories by default (download maps and Trial Edition download maps), but you can add more by editing QuestCategories in config.json. Categories that have flag 0x40 (Ep3 download) set are indexed from this directory; all others are indexed from system/quests/. Files in maps-download/ subdirectories have the same format as those in the maps/ directory, but should be named like `e###-gc3-LANGUAGE.EXT` (similar to how non-Episode 3 quests are named in the system/quests/ directory). If you want a map to be available for online play and for downloading, the file must exist in both maps/ and in a maps-download/ subdirectory (a symbolic link is acceptable).
|
|
||||||
* maps-offline/: Offline map files. These are all the offline quests and free battle maps from the client, including some debugging/test maps that were inaccessible during normal play. To make them playable online, put the files in the maps/ directory.
|
|
||||||
* tournament-state.json: State of all active tournaments. This file is automatically written when any tournament changes state for any reason (e.g. a tournament is created/started/deleted or a match is resolved).
|
* tournament-state.json: State of all active tournaments. This file is automatically written when any tournament changes state for any reason (e.g. a tournament is created/started/deleted or a match is resolved).
|
||||||
|
|
||||||
There is no public editor for Episode 3 maps and quests, but the format is described fairly thoroughly in src/Episode3/DataIndexes.hh (see the MapDefinition structure). You'll need to use `newserv decompress-prs ...` to decompress a .bin or .mnm file before editing it, but you don't need to compress it again to use it - just put the .bind or .mnmd file in the maps directory and newserv will make it available.
|
There is no public editor for Episode 3 maps and quests, but the format is described fairly thoroughly in src/Episode3/DataIndexes.hh (see the MapDefinition structure). You'll need to use `newserv decompress-prs ...` to decompress a .bin or .mnm file before editing it, but you don't need to compress it again to use it - just put the .bind or .mnmd file in the maps directory and newserv will make it available.
|
||||||
@@ -482,6 +480,7 @@ The specific versions are:
|
|||||||
| PSO DC v2 JP | 2OJF | SH-4 |
|
| PSO DC v2 JP | 2OJF | SH-4 |
|
||||||
| PSO DC v2 US | 2OEF | SH-4 |
|
| PSO DC v2 US | 2OEF | SH-4 |
|
||||||
| PSO DC v2 EU | 2OPF | SH-4 |
|
| PSO DC v2 EU | 2OPF | SH-4 |
|
||||||
|
| PSO PC (v2) Trial Edition | 2OJT | Client functions not supported |
|
||||||
| PSO PC (v2) 04/2002 | 2OJW | Client functions not supported |
|
| PSO PC (v2) 04/2002 | 2OJW | Client functions not supported |
|
||||||
| PSO PC (v2) 02/2003 | 2OJZ | Client functions not supported |
|
| PSO PC (v2) 02/2003 | 2OJZ | Client functions not supported |
|
||||||
| PSO GC Trial Edition | 3OJT | PowerPC |
|
| PSO GC Trial Edition | 3OJT | PowerPC |
|
||||||
@@ -607,6 +606,7 @@ Some commands only work for clients not in proxy sessions. The chat commands are
|
|||||||
* `$ss <data>`: Send a command to the remote server (if in a proxy session) or to the game 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.
|
* `$sb <data>`: Send a command to yourself, and to the remote server or game server.
|
||||||
* `$auction` (Episode 3 only): Bring up the CARD Auction menu, even if there are fewer than 4 players are in the game or you don't have a VIP card.
|
* `$auction` (Episode 3 only): Bring up the CARD Auction menu, even if there are fewer than 4 players are in the game or you don't have a VIP card.
|
||||||
|
* `$makeobj <type> [coords...] [angles...] [params...]`: Create a map object. This is only implemented for a few specific client versions. The type is an integer like `273` or `0x0107`. Coordinates are specified as e.g. `x:30 y:0 z:-25.5`; if coordinates are not specified, the object is created at the player's coordinates. Angles are specified as e.g. `r:0 p:0x1000 w:-0x400` (for roll, pitch, and yaw, respectively). Parameters are specified as e.g. `1:2.0 2:0.0 5:0x4000`; any unspecified parameters are set to zero. The object is only created for the calling player and is not added to the server's map state; if the object ever sends update commands (e.g. 6x0B), it will likely result in a disconnection.
|
||||||
|
|
||||||
* Personal state commands
|
* Personal state commands
|
||||||
* `$arrow <color-id>`: Change your lobby arrow color. The color may be specified by number (0-12) or by name (red, blue, green, yellow, purple, cyan, orange, pink, white, white2, white3, or black).
|
* `$arrow <color-id>`: Change your lobby arrow color. The color may be specified by number (0-12) or by name (red, blue, green, yellow, purple, cyan, orange, pink, white, white2, white3, or black).
|
||||||
@@ -644,7 +644,7 @@ Some commands only work for clients not in proxy sessions. The chat commands are
|
|||||||
* `$stat <what>`: Show a statistic about your player or team in the current battle. `<what>` can be `duration`, `fcs-destroyed`, `cards-destroyed`, `damage-given`, `damage-taken`, `opp-cards-destroyed`, `own-cards-destroyed`, `move-distance`, `cards-set`, `fcs-set`, `attack-actions-set`, `techs-set`, `assists-set`, `defenses-self`, `defenses-ally`, `cards-drawn`, `max-attack-damage`, `max-combo`, `attacks-given`, `attacks-taken`, `sc-damage`, `damage-defended`, or `rank`.
|
* `$stat <what>`: Show a statistic about your player or team in the current battle. `<what>` can be `duration`, `fcs-destroyed`, `cards-destroyed`, `damage-given`, `damage-taken`, `opp-cards-destroyed`, `own-cards-destroyed`, `move-distance`, `cards-set`, `fcs-set`, `attack-actions-set`, `techs-set`, `assists-set`, `defenses-self`, `defenses-ally`, `cards-drawn`, `max-attack-damage`, `max-combo`, `attacks-given`, `attacks-taken`, `sc-damage`, `damage-defended`, or `rank`.
|
||||||
* `$surrender`: Cause your team to immediately lose the current battle. If your story character is already defeated, you can't surrender - only your teammate can.
|
* `$surrender`: Cause your team to immediately lose the current battle. If your story character is already defeated, you can't surrender - only your teammate can.
|
||||||
* `$saverec <name>`: Save the recording of the last battle.
|
* `$saverec <name>`: Save the recording of the last battle.
|
||||||
* `$playrec <name>`: Play a battle recording. This command creates a spectator team immediately but the replay does not start automatically, to give other players a chance to join. To start the battle replay within the spectator team, run `$playrec` again (with no name). There is a bug in Dolphin that makes this command unstable in emulation (see the "Battle records" section above).
|
* `$playrec <name>`: Play a battle recording. This command creates a spectator team and plays the specified recording as if it were happening in real time. By default, playback will start immediately when the spectator team is ready; you can delay this to allow others to join by prepending a `!` to the recording name. In that case, using `$playrec` again (with no argument) within the spectator team will start playback.
|
||||||
|
|
||||||
* Cheat mode commands
|
* Cheat mode commands
|
||||||
* `$cheat` (non-proxy only): Enable or disable cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. By default, cheat mode is off in new games but can be enabled; there is an option in config.json that allows you to disable cheat mode entirely, or set it to on by default in new games. Cheat mode is always enabled on the proxy, unless cheat mode is disabled on the entire server.
|
* `$cheat` (non-proxy only): Enable or disable cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. By default, cheat mode is off in new games but can be enabled; there is an option in config.json that allows you to disable cheat mode entirely, or set it to on by default in new games. Cheat mode is always enabled on the proxy, unless cheat mode is disabled on the entire server.
|
||||||
@@ -653,8 +653,22 @@ Some commands only work for clients not in proxy sessions. The chat commands are
|
|||||||
* `$warpme <floor-id>` (or `$warp <floor-id>`): Warp yourself to the given floor.
|
* `$warpme <floor-id>` (or `$warp <floor-id>`): Warp yourself to the given floor.
|
||||||
* `$warpall <floor-id>`: Warp everyone in the game to the given floor. You must be the leader to use this command, unless you're on the proxy.
|
* `$warpall <floor-id>`: Warp everyone in the game to the given floor. You must be the leader to use this command, unless you're on the proxy.
|
||||||
* `$next`: Warp yourself to the next floor.
|
* `$next`: Warp yourself to the next floor.
|
||||||
* `$item <desc>` (or `$i <desc>`): Create an item. `desc` may be a description of the item (e.g. "Hell Saber +5 0/10/25/0/10") or a string of hex data specifying the item code. Item codes are 16 hex bytes; at least 2 bytes must be specified, and all unspecified bytes are zeroes. If you are on the proxy, you must not be using Blue Burst for this command to work. On the game server, this command works for all versions.
|
* `$item <desc>` (or `$i <desc>`): Create an item. `desc` may be a description of the item or a string of hex data specifying the item code. Item codes are 16 hex bytes; at least 2 bytes must be specified, and all unspecified bytes are zeroes. If you are on the proxy, you must not be using Blue Burst for this command to work. On the game server, this command works for all versions. Here are some examples to illustrate the syntax (nothing is case-sensitive, and everything except the item name itself is optional):
|
||||||
* `$unset <index>` (non-proxy only): In an Episode 3 battle, removes one of your set cards from the field. `<index>` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed.
|
* `$item Saber +5 0/10/25/0/10` (weapon with special, grind and attributes)
|
||||||
|
* `$item ???? Draw Autogun` (untekked weapon with special; can have grind/attributes too, as above)
|
||||||
|
* `$item SEALED J-SWORD K:2000` (weapon with kill count)
|
||||||
|
* `$item ES APHEX ZALURE TWIN +200` (ES weapon must be prefixed with "ES"; name comes before special)
|
||||||
|
* `$item DF FIELD +10DEF +20EVP +4` (armor with DFP bonus, EVP bonus, and slot count)
|
||||||
|
* `$item RED MERGE +10DFP +20EVP` (shield; same as armor except without slot count)
|
||||||
|
* `$item Knight/Power +9` (unit with specific modifier)
|
||||||
|
* `$item Knight/Power++` (unit with normal modifier; ++/-- are +4/-4 and +/- are +2/-2)
|
||||||
|
* `$item LIMITER K:1000` (sealed unit with kill count)
|
||||||
|
* `$item Tapas PB:F,G,M&Y 120% 200IQ 5/195/0/0 green` (mag with PBs, synchro, IO, stats, and color)
|
||||||
|
* `$item Trimate x10` (tool with stack size)
|
||||||
|
* `$item Disk:Reverser` (technique disk without level)
|
||||||
|
* `$item Disk:Razonde Lv.30` (technique disk with level)
|
||||||
|
* `$item 1000 Meseta`
|
||||||
|
* `$unset <index>` (non-proxy only): In an Episode 3 battle, removes one of your set cards from the field. `<index>` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed. You can also destroy the assist card set on yourself with `$unset 0`.
|
||||||
* `$dropmode [mode]` (proxy only): Change the way item drops behave in the current game, if you are not on BB. Unlike the game server version of this command, using this on the proxy requires cheats to be enabled. This works by intercepting the drop requests sent to and from the leader. (So, if you are the leader and not using server drop mode on the remote server, it affects the entire game; otherwise, it affects only items generated by your actions.) `mode` can be `none` (no drops), `default` (normal drops), or `proxy` (use newserv's drop tables instead of the remote server's). If `mode` is not given, tells you the current drop mode without changing it.
|
* `$dropmode [mode]` (proxy only): Change the way item drops behave in the current game, if you are not on BB. Unlike the game server version of this command, using this on the proxy requires cheats to be enabled. This works by intercepting the drop requests sent to and from the leader. (So, if you are the leader and not using server drop mode on the remote server, it affects the entire game; otherwise, it affects only items generated by your actions.) `mode` can be `none` (no drops), `default` (normal drops), or `proxy` (use newserv's drop tables instead of the remote server's). If `mode` is not given, tells you the current drop mode without changing it.
|
||||||
|
|
||||||
* Aesthetic commands
|
* Aesthetic commands
|
||||||
|
|||||||
+129
-1
@@ -17,6 +17,7 @@ Version codes (from README.md):
|
|||||||
2OJF: PSO DC v2 JP
|
2OJF: PSO DC v2 JP
|
||||||
2OEF: PSO DC v2 US
|
2OEF: PSO DC v2 US
|
||||||
2OPF: PSO DC v2 EU
|
2OPF: PSO DC v2 EU
|
||||||
|
2OJT: PSO PC Trial Edition
|
||||||
2OJW: PSO PC (v2) 04/2002
|
2OJW: PSO PC (v2) 04/2002
|
||||||
2OJZ: PSO PC (v2) 02/2003
|
2OJZ: PSO PC (v2) 02/2003
|
||||||
3OJT: PSO GC Trial Edition
|
3OJT: PSO GC Trial Edition
|
||||||
@@ -81,6 +82,21 @@ Disable item equip restrictions ("God of equip")
|
|||||||
3OP0 => 041052D4 38000005
|
3OP0 => 041052D4 38000005
|
||||||
59NL => 005C9F31 E9A7000000
|
59NL => 005C9F31 E9A7000000
|
||||||
|
|
||||||
|
All items visible in Pioneer 2
|
||||||
|
3OE1 => 04102D88 38600000
|
||||||
|
|
||||||
|
Mags visible in Pioneer 2
|
||||||
|
59NL => 005D8F4B EB04
|
||||||
|
|
||||||
|
Disable pause menu background + offset
|
||||||
|
3OE1 => 0424BD5C 48000370
|
||||||
|
0428735C 4800000C
|
||||||
|
3OE2 => 0424CED8 48000370
|
||||||
|
042887D8 4800000C
|
||||||
|
59NL => 00719B54 9090
|
||||||
|
00733BA7 9090
|
||||||
|
00733A0E 90E9
|
||||||
|
|
||||||
All rareable enemies are rare
|
All rareable enemies are rare
|
||||||
3OE0 => 040AC944 60000000 // Hildeblue
|
3OE0 => 040AC944 60000000 // Hildeblue
|
||||||
040C1B70 60000000 // Rappies
|
040C1B70 60000000 // Rappies
|
||||||
@@ -270,6 +286,23 @@ Disable lobby event music (but keep the visuals)
|
|||||||
3SJ0 => 040B7078 38000000
|
3SJ0 => 040B7078 38000000
|
||||||
3SP0 => 040B74A0 38000000
|
3SP0 => 040B74A0 38000000
|
||||||
|
|
||||||
|
Disable rate limit for lobby chair movement
|
||||||
|
3OJ2 => 041C73B0 60000000
|
||||||
|
3OJ3 => 041C786C 60000000
|
||||||
|
3OJ4 => 041C7DA8 60000000
|
||||||
|
3OJ5 => 041C7938 60000000
|
||||||
|
3OE0 => 041C77CC 60000000
|
||||||
|
3OE1 => 041C77CC 60000000
|
||||||
|
3OE2 => 041C799C 60000000
|
||||||
|
3OP0 => 041C7E58 60000000
|
||||||
|
3SJT => 040E290C 60000000
|
||||||
|
3SJ0 => 040DE6C4 60000000
|
||||||
|
3SE0 => 040DE6A8 60000000
|
||||||
|
3SP0 => 040DEAEC 60000000
|
||||||
|
|
||||||
|
Make lobby chairs fast (client-side only)
|
||||||
|
3SE0 => 0457E618 40000000
|
||||||
|
|
||||||
Enable Pinz's Shop Super Card Capsule Machine as a fourth option
|
Enable Pinz's Shop Super Card Capsule Machine as a fourth option
|
||||||
3SE0 => 043101C0 38800004
|
3SE0 => 043101C0 38800004
|
||||||
04310238 2C1D0004
|
04310238 2C1D0004
|
||||||
@@ -316,8 +349,14 @@ Unlock all offline free battle maps
|
|||||||
This unlocks ALL maps, including a bunch of maps with garbage names that crash if you try to play them
|
This unlocks ALL maps, including a bunch of maps with garbage names that crash if you try to play them
|
||||||
3SJT => 042BE538 38600001
|
3SJT => 042BE538 38600001
|
||||||
3SJ0 => 042C9C2C 38600001
|
3SJ0 => 042C9C2C 38600001
|
||||||
3SP0 => 042CB50C 38600001
|
|
||||||
3SE0 => 042CAA00 38600001
|
3SE0 => 042CAA00 38600001
|
||||||
|
3SP0 => 042CB50C 38600001
|
||||||
|
|
||||||
|
Card auctions accessible with fewer than 4 players
|
||||||
|
3SJT => 042DD618 38600004
|
||||||
|
3SJ0 => 042F4F20 38600004
|
||||||
|
3SE0 => 042F5D88 38600004
|
||||||
|
3SP0 => 042F698C 38600004
|
||||||
|
|
||||||
Talk to auction counter offline to get all cards
|
Talk to auction counter offline to get all cards
|
||||||
3SE0 => 042F5D18 4BD160E8
|
3SE0 => 042F5D18 4BD160E8
|
||||||
@@ -530,6 +569,68 @@ Heaven Punisher's special always works
|
|||||||
3OE2 => 0412AD84 38800001
|
3OE2 => 0412AD84 38800001
|
||||||
3OP0 => 0412AF5C 38800001
|
3OP0 => 0412AF5C 38800001
|
||||||
|
|
||||||
|
Fast tekker (skips wind-up jingle)
|
||||||
|
1OJ1 => 8C15B0CA mov r1, 1
|
||||||
|
8C15B0E6 nop
|
||||||
|
1OJ2 => 8C162302 mov r1, 1
|
||||||
|
8C16231E nop
|
||||||
|
1OJ3 => 8C175E66 mov r1, 1
|
||||||
|
8C175E82 nop
|
||||||
|
1OJ4 => 8C1780AE mov r1, 1
|
||||||
|
8C1780CA nop
|
||||||
|
1OJF => 8C17600E mov r1, 1
|
||||||
|
8C17602A nop
|
||||||
|
1OEF => 8C17863E mov r1, 1
|
||||||
|
8C17865A nop
|
||||||
|
1OPF => 8C1783FA mov r1, 1
|
||||||
|
8C178416 nop
|
||||||
|
2OJ5 => 8C19BD4A mov r1, 1
|
||||||
|
8C19BD66 nop
|
||||||
|
2OJF => 8C19ADB6 mov r1, 1
|
||||||
|
8C19ADD2 nop
|
||||||
|
2OEF => 8C19BD4A mov r1, 1
|
||||||
|
8C19BD66 nop
|
||||||
|
2OPF => 8C19B7E2 mov r1, 1
|
||||||
|
8C19B7FE nop
|
||||||
|
2OJW => 005B14A3 mov dword [ebx + 0x150], 1
|
||||||
|
005B14BF jmp +0x0D
|
||||||
|
2OJZ => 005B0193 mov dword [ebx + 0x150], 1
|
||||||
|
005B01AF jmp +0x0D
|
||||||
|
3OJT => 0426FAE8 38000001
|
||||||
|
0426FB10 60000000
|
||||||
|
3OJ2 => 0421F8CC 38000001
|
||||||
|
0421F8F4 60000000
|
||||||
|
3OJ3 => 04220250 38000001
|
||||||
|
04220278 60000000
|
||||||
|
3OJ4 => 04221154 38000001
|
||||||
|
0422117C 60000000
|
||||||
|
3OJ5 => 04220EF0 38000001
|
||||||
|
04220F18 60000000
|
||||||
|
3OE0 => 04220170 38000001
|
||||||
|
04220198 60000000
|
||||||
|
3OE1 => 04220170 38000001
|
||||||
|
04220198 60000000
|
||||||
|
3OE2 => 04221224 38000001
|
||||||
|
0422124C 60000000
|
||||||
|
3OP0 => 04220ABC 38000001
|
||||||
|
04220AE4 60000000
|
||||||
|
4OED => 0023EF3C mov dword [ebp + 0x14C], 1
|
||||||
|
0023EF57 jmp +0x0A
|
||||||
|
4OEU => 0023F0BC mov dword [ebp + 0x14C], 1
|
||||||
|
0023F0D7 jmp +0x0A
|
||||||
|
4OJB => 0023EC5C mov dword [ebp + 0x14C], 1
|
||||||
|
0023EC77 jmp +0x0A
|
||||||
|
4OJD => 0023EEAC mov dword [ebp + 0x14C], 1
|
||||||
|
0023EEC7 jmp +0x0A
|
||||||
|
4OJU => 0023F21C mov dword [ebp + 0x14C], 1
|
||||||
|
0023F237 jmp +0x0A
|
||||||
|
4OPD => 0023EF5C mov dword [ebp + 0x14C], 1
|
||||||
|
0023EF77 jmp +0x0A
|
||||||
|
4OPU => 0023F14C mov dword [ebp + 0x14C], 1
|
||||||
|
0023F167 jmp +0x0A
|
||||||
|
59NL => 006DA113 mov dword [edi + 0x14C], 1
|
||||||
|
006DA130 jmp +0x0B
|
||||||
|
|
||||||
Allow loading corrupted save files
|
Allow loading corrupted save files
|
||||||
3OJ2 => 041FC784 38600007
|
3OJ2 => 041FC784 38600007
|
||||||
041FC788 4E800020
|
041FC788 4E800020
|
||||||
@@ -793,3 +894,30 @@ Disable save file signature validation (for moving Xbox saves across consoles)
|
|||||||
4OEU => 002F22DB 9090
|
4OEU => 002F22DB 9090
|
||||||
4OPD => 002F215B 9090
|
4OPD => 002F215B 9090
|
||||||
4OPU => 002F234B 9090
|
4OPU => 002F234B 9090
|
||||||
|
|
||||||
|
Enable UDP test mode online
|
||||||
|
3OE1 => 041A3D60 38600001
|
||||||
|
|
||||||
|
Main warp door opens in Challenge mode
|
||||||
|
3OE1 => 041820A4 38600001
|
||||||
|
041820A8 4E800020
|
||||||
|
|
||||||
|
Allow arbitrary tech disk levels
|
||||||
|
3OE1 => 0410EBE8 60000000
|
||||||
|
04100D18 60000000
|
||||||
|
041D6C0C 60000000
|
||||||
|
041D6C5C 60000000
|
||||||
|
0422CB50 60000000
|
||||||
|
042CD74C 4E800020
|
||||||
|
|
||||||
|
Change particle colors in quest loading screen
|
||||||
|
3OE1 => 04472C20 AARRGGBB // Default color
|
||||||
|
04472C24 AARRGGBB // Color after 1 A press
|
||||||
|
04472C28 AARRGGBB // Color after 2 A presses
|
||||||
|
04472C2C AARRGGBB // Color after 3 A presses
|
||||||
|
04472C30 AARRGGBB // Color after 4 A presses
|
||||||
|
04472C34 AARRGGBB // Color after 5 A presses
|
||||||
|
|
||||||
|
Floor warp loading screen speed modifier
|
||||||
|
// XXXX = speed; default is 01B4; 0800 = very fast/wobbly; 0020 = very slow
|
||||||
|
3OE1 => 0434A350 3863XXXX
|
||||||
|
|||||||
+972
-975
File diff suppressed because it is too large
Load Diff
@@ -557,7 +557,7 @@ BugFixes
|
|||||||
8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 lwz r3, [r31 + 0x0024]
|
8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 8000C6E8 807F0024 lwz r3, [r31 + 0x0024]
|
||||||
8000C6EC 48165AA0 8000C6EC 482147D4 8000C6EC 482156C0 8000C6EC 48215474 8000C6EC 482146F4 8000C6EC 482146F4 8000C6EC 482157A8 8000C6EC 48215040 b +0x002146F4 /* 80220DE0 */
|
8000C6EC 48165AA0 8000C6EC 482147D4 8000C6EC 482156C0 8000C6EC 48215474 8000C6EC 482146F4 8000C6EC 482146F4 8000C6EC 482157A8 8000C6EC 48215040 b +0x002146F4 /* 80220DE0 */
|
||||||
8021D098 4BDEF638 8021D9FC 4BDEECD4 8021E8E8 4BDEDDE8 8021E69C 4BDEE034 8021D91C 4BDEEDB4 8021D91C 4BDEEDB4 8021E9D0 4BDEDD00 8021E268 4BDEE468 b -0x0021124C /* 8000C6D0 */
|
8021D098 4BDEF638 8021D9FC 4BDEECD4 8021E8E8 4BDEDDE8 8021E69C 4BDEE034 8021D91C 4BDEEDB4 8021D91C 4BDEEDB4 8021E9D0 4BDEDD00 8021E268 4BDEE468 b -0x0021124C /* 8000C6D0 */
|
||||||
80172188 4BE9A558 80220EBC 4BDEB824 80221DA8 4BDEA938 80221B5C 4BDEAB84 80220DDC 4BDEB904 80220DDC 4BDEB904 80221E90 4BDEA850 80221728 4BDEAFB8 b -0x002146FC /* 8000C6E0 */
|
80220528 4BE9A558 80220EBC 4BDEB824 80221DA8 4BDEA938 80221B5C 4BDEAB84 80220DDC 4BDEB904 80220DDC 4BDEB904 80221E90 4BDEA850 80221728 4BDEAFB8 b -0x002146FC /* 8000C6E0 */
|
||||||
|
|
||||||
Dropped Mag Colour Bug Fix
|
Dropped Mag Colour Bug Fix
|
||||||
BugFixes
|
BugFixes
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Binary file not shown.
Binary file not shown.
Executable → Regular
Binary file not shown.
Binary file not shown.
@@ -420,26 +420,6 @@ void Account::delete_file() const {
|
|||||||
remove(filename.c_str());
|
remove(filename.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t Login::proxy_session_id() const {
|
|
||||||
uint64_t low_part = 0;
|
|
||||||
if (this->dc_nte_license) {
|
|
||||||
low_part = this->dc_nte_license->proxy_session_id_part();
|
|
||||||
} else if (this->dc_license) {
|
|
||||||
low_part = this->dc_license->proxy_session_id_part();
|
|
||||||
} else if (this->pc_license) {
|
|
||||||
low_part = this->pc_license->proxy_session_id_part();
|
|
||||||
} else if (this->gc_license) {
|
|
||||||
low_part = this->gc_license->proxy_session_id_part();
|
|
||||||
} else if (this->xb_license) {
|
|
||||||
low_part = this->xb_license->proxy_session_id_part();
|
|
||||||
} else if (this->bb_license) {
|
|
||||||
low_part = this->bb_license->proxy_session_id_part();
|
|
||||||
} else {
|
|
||||||
throw logic_error("none of the licenses in a Login were present");
|
|
||||||
}
|
|
||||||
return (static_cast<uint64_t>(this->account->account_id) << 32) | low_part;
|
|
||||||
}
|
|
||||||
|
|
||||||
string Login::str() const {
|
string Login::str() const {
|
||||||
string ret = std::format("Account:{:08X}", this->account->account_id);
|
string ret = std::format("Account:{:08X}", this->account->account_id);
|
||||||
if (this->account_was_created) {
|
if (this->account_was_created) {
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ struct DCNTELicense {
|
|||||||
std::string serial_number;
|
std::string serial_number;
|
||||||
std::string access_key;
|
std::string access_key;
|
||||||
|
|
||||||
inline uint64_t proxy_session_id_part() const {
|
|
||||||
return phosg::fnv1a32(this->serial_number);
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::shared_ptr<DCNTELicense> from_json(const phosg::JSON& json);
|
static std::shared_ptr<DCNTELicense> from_json(const phosg::JSON& json);
|
||||||
phosg::JSON json() const;
|
phosg::JSON json() const;
|
||||||
};
|
};
|
||||||
@@ -29,10 +25,6 @@ struct V1V2License {
|
|||||||
uint32_t serial_number = 0;
|
uint32_t serial_number = 0;
|
||||||
std::string access_key;
|
std::string access_key;
|
||||||
|
|
||||||
inline uint64_t proxy_session_id_part() const {
|
|
||||||
return this->serial_number;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::shared_ptr<V1V2License> from_json(const phosg::JSON& json);
|
static std::shared_ptr<V1V2License> from_json(const phosg::JSON& json);
|
||||||
phosg::JSON json() const;
|
phosg::JSON json() const;
|
||||||
};
|
};
|
||||||
@@ -42,10 +34,6 @@ struct GCLicense {
|
|||||||
std::string access_key;
|
std::string access_key;
|
||||||
std::string password;
|
std::string password;
|
||||||
|
|
||||||
inline uint64_t proxy_session_id_part() const {
|
|
||||||
return this->serial_number;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::shared_ptr<GCLicense> from_json(const phosg::JSON& json);
|
static std::shared_ptr<GCLicense> from_json(const phosg::JSON& json);
|
||||||
phosg::JSON json() const;
|
phosg::JSON json() const;
|
||||||
};
|
};
|
||||||
@@ -55,10 +43,6 @@ struct XBLicense {
|
|||||||
uint64_t user_id = 0;
|
uint64_t user_id = 0;
|
||||||
uint64_t account_id = 0;
|
uint64_t account_id = 0;
|
||||||
|
|
||||||
inline uint64_t proxy_session_id_part() const {
|
|
||||||
return phosg::fnv1a32(this->gamertag);
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::shared_ptr<XBLicense> from_json(const phosg::JSON& json);
|
static std::shared_ptr<XBLicense> from_json(const phosg::JSON& json);
|
||||||
phosg::JSON json() const;
|
phosg::JSON json() const;
|
||||||
};
|
};
|
||||||
@@ -67,10 +51,6 @@ struct BBLicense {
|
|||||||
std::string username;
|
std::string username;
|
||||||
std::string password;
|
std::string password;
|
||||||
|
|
||||||
inline uint64_t proxy_session_id_part() const {
|
|
||||||
return phosg::fnv1a32(this->username);
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::shared_ptr<BBLicense> from_json(const phosg::JSON& json);
|
static std::shared_ptr<BBLicense> from_json(const phosg::JSON& json);
|
||||||
phosg::JSON json() const;
|
phosg::JSON json() const;
|
||||||
};
|
};
|
||||||
@@ -178,8 +158,6 @@ struct Login {
|
|||||||
std::shared_ptr<XBLicense> xb_license;
|
std::shared_ptr<XBLicense> xb_license;
|
||||||
std::shared_ptr<BBLicense> bb_license;
|
std::shared_ptr<BBLicense> bb_license;
|
||||||
|
|
||||||
uint64_t proxy_session_id() const;
|
|
||||||
|
|
||||||
std::string str() const;
|
std::string str() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -247,9 +247,9 @@ public:
|
|||||||
static constexpr bool IsBE = BE;
|
static constexpr bool IsBE = BE;
|
||||||
|
|
||||||
U16T<BE> type;
|
U16T<BE> type;
|
||||||
U16T<BE> unknown_a1;
|
U16T<BE> unused;
|
||||||
U32T<BE> constructor_addr;
|
U32T<BE> constructor_addr;
|
||||||
F32T<BE> unknown_a2;
|
F32T<BE> max_dist2; // Only applies for objects
|
||||||
U32T<BE> default_num_children;
|
U32T<BE> default_num_children;
|
||||||
} __attribute__((packed));
|
} __attribute__((packed));
|
||||||
|
|
||||||
@@ -259,9 +259,9 @@ public:
|
|||||||
|
|
||||||
pstring<TextEncoding::ASCII, 0x10> debug_name;
|
pstring<TextEncoding::ASCII, 0x10> debug_name;
|
||||||
U16T<BE> type;
|
U16T<BE> type;
|
||||||
U16T<BE> unknown_a1;
|
U16T<BE> unused;
|
||||||
U32T<BE> constructor_addr;
|
U32T<BE> constructor_addr;
|
||||||
F32T<BE> unknown_a2;
|
F32T<BE> max_dist2; // Only applies for objects
|
||||||
U32T<BE> default_num_children;
|
U32T<BE> default_num_children;
|
||||||
} __attribute__((packed));
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
|||||||
+9
-10
@@ -163,7 +163,7 @@ asio::awaitable<HTTPRequest> HTTPClient::recv_http_request(size_t max_line_size,
|
|||||||
} else if (method_token == "TRACE") {
|
} else if (method_token == "TRACE") {
|
||||||
req.method = HTTPRequest::Method::TRACE;
|
req.method = HTTPRequest::Method::TRACE;
|
||||||
} else {
|
} else {
|
||||||
throw HTTPError(400, "unknown request method");
|
throw HTTPError(400, "Unknown request method");
|
||||||
}
|
}
|
||||||
|
|
||||||
req.http_version = std::move(line_tokens[2]);
|
req.http_version = std::move(line_tokens[2]);
|
||||||
@@ -237,26 +237,26 @@ asio::awaitable<HTTPRequest> HTTPClient::recv_http_request(size_t max_line_size,
|
|||||||
size_t parse_offset = 0;
|
size_t parse_offset = 0;
|
||||||
size_t chunk_size = stoull(line, &parse_offset, 16);
|
size_t chunk_size = stoull(line, &parse_offset, 16);
|
||||||
if (parse_offset != line.size()) {
|
if (parse_offset != line.size()) {
|
||||||
throw HTTPError(400, "invalid chunk header during chunked encoding");
|
throw HTTPError(400, "Invalid chunk header during chunked encoding");
|
||||||
}
|
}
|
||||||
if (chunk_size == 0) {
|
if (chunk_size == 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
total_data_bytes += chunk_size;
|
total_data_bytes += chunk_size;
|
||||||
if (total_data_bytes > max_body_size) {
|
if (total_data_bytes > max_body_size) {
|
||||||
throw HTTPError(400, "request data size too large");
|
throw HTTPError(400, "Request data size too large");
|
||||||
}
|
}
|
||||||
chunks.emplace_back(co_await this->r.read_data(chunk_size));
|
chunks.emplace_back(co_await this->r.read_data(chunk_size));
|
||||||
auto after_chunk_data = co_await this->r.read_line("\r\n", 0x20);
|
auto after_chunk_data = co_await this->r.read_line("\r\n", 0x20);
|
||||||
if (!after_chunk_data.empty()) {
|
if (!after_chunk_data.empty()) {
|
||||||
throw HTTPError(400, "incorrect trailing sequence after chunk data");
|
throw HTTPError(400, "Incorrect trailing sequence after chunk data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
auto content_length_header = req.get_header("content-length");
|
auto content_length_header = req.get_header("content-length");
|
||||||
size_t content_length = content_length_header ? stoull(*content_length_header) : 0;
|
size_t content_length = content_length_header ? stoull(*content_length_header) : 0;
|
||||||
if (content_length > max_body_size) {
|
if (content_length > max_body_size) {
|
||||||
throw HTTPError(400, "request data size too large");
|
throw HTTPError(400, "Request data size too large");
|
||||||
} else if (content_length > 0) {
|
} else if (content_length > 0) {
|
||||||
req.data = co_await this->r.read_data(content_length);
|
req.data = co_await this->r.read_data(content_length);
|
||||||
}
|
}
|
||||||
@@ -289,8 +289,7 @@ asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_
|
|||||||
while (this->r.get_socket().is_open()) {
|
while (this->r.get_socket().is_open()) {
|
||||||
WebSocketMessage msg;
|
WebSocketMessage msg;
|
||||||
|
|
||||||
// We need at most 10 bytes to determine if there's a valid frame, or as
|
// We need at most 10 bytes to determine if there's a valid frame, or as little as 2
|
||||||
// little as 2
|
|
||||||
co_await this->r.read_data_into(msg.header, 2);
|
co_await this->r.read_data_into(msg.header, 2);
|
||||||
|
|
||||||
// Get the payload size
|
// Get the payload size
|
||||||
@@ -335,7 +334,7 @@ asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_
|
|||||||
} else if (opcode == 0x08) {
|
} else if (opcode == 0x08) {
|
||||||
// Close message
|
// Close message
|
||||||
co_await this->send_websocket_message(msg.data, msg.opcode);
|
co_await this->send_websocket_message(msg.data, msg.opcode);
|
||||||
this->r.get_socket().close();
|
this->r.close();
|
||||||
|
|
||||||
} else if (opcode == 0x09) {
|
} else if (opcode == 0x09) {
|
||||||
// Ping message
|
// Ping message
|
||||||
@@ -343,7 +342,7 @@ asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Unknown control message type
|
// Unknown control message type
|
||||||
this->r.get_socket().close();
|
this->r.close();
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -351,7 +350,7 @@ asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_
|
|||||||
// If there's an existing fragment, the current message's opcode should be
|
// If there's an existing fragment, the current message's opcode should be
|
||||||
// zero; if there's no pending message, it must not be zero
|
// zero; if there's no pending message, it must not be zero
|
||||||
if (prev_msg_present == (opcode != 0)) {
|
if (prev_msg_present == (opcode != 0)) {
|
||||||
this->r.get_socket().close();
|
this->r.close();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <phosg/Encoding.hh>
|
||||||
#include <phosg/Hash.hh>
|
#include <phosg/Hash.hh>
|
||||||
#include <phosg/Time.hh>
|
#include <phosg/Time.hh>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -82,6 +83,106 @@ struct HTTPClient {
|
|||||||
asio::awaitable<void> send_websocket_message(const std::string& data, uint8_t opcode = 0x01);
|
asio::awaitable<void> send_websocket_message(const std::string& data, uint8_t opcode = 0x01);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template <typename RetT>
|
||||||
|
class HTTPRouter {
|
||||||
|
public:
|
||||||
|
struct Args {
|
||||||
|
std::shared_ptr<HTTPClient> client;
|
||||||
|
const HTTPRequest& req;
|
||||||
|
std::unordered_map<std::string, std::string> params;
|
||||||
|
phosg::JSON post_data;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
requires(std::is_integral_v<T>)
|
||||||
|
T get_param(const char* name, bool hex = false) const {
|
||||||
|
const auto& value_str = this->params.at(name);
|
||||||
|
size_t conversion_end;
|
||||||
|
int64_t v = std::stoull(value_str, &conversion_end, hex ? 16 : 0);
|
||||||
|
if (conversion_end != value_str.size()) {
|
||||||
|
throw HTTPError(400, "Invalid integer value");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t uv = static_cast<uint64_t>(v);
|
||||||
|
if constexpr (std::is_unsigned_v<T>) {
|
||||||
|
if (uv & (~phosg::mask_for_type<T>)) {
|
||||||
|
throw HTTPError(400, "Unsigned value out of range");
|
||||||
|
}
|
||||||
|
return uv;
|
||||||
|
} else {
|
||||||
|
if (((uv & (~(phosg::mask_for_type<T> >> 1))) != 0) && ((uv & (~(phosg::mask_for_type<T> >> 1))) != (~(phosg::mask_for_type<T> >> 1)))) {
|
||||||
|
throw HTTPError(400, "Signed value out of range");
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using Handler = std::function<asio::awaitable<RetT>(Args&&)>;
|
||||||
|
|
||||||
|
static std::vector<std::string> split_and_normalize_path(const std::string& path) {
|
||||||
|
auto path_tokens = phosg::split(path, '/');
|
||||||
|
while (!path_tokens.empty() && path_tokens.back().empty()) {
|
||||||
|
path_tokens.pop_back();
|
||||||
|
}
|
||||||
|
return path_tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(HTTPRequest::Method method, const std::string& path_pattern, Handler handler) {
|
||||||
|
this->routes.emplace_back(Route{
|
||||||
|
.method = method, .path_tokens = this->split_and_normalize_path(path_pattern), .handler = handler});
|
||||||
|
}
|
||||||
|
|
||||||
|
asio::awaitable<RetT> call_handler(std::shared_ptr<HTTPClient> c, const HTTPRequest& req) {
|
||||||
|
Args args = {.client = c, .req = req, .params = {}, .post_data = phosg::JSON()};
|
||||||
|
|
||||||
|
auto tokens = this->split_and_normalize_path(req.path);
|
||||||
|
for (const auto& route : this->routes) {
|
||||||
|
if (route.path_tokens.size() != tokens.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool matched = true;
|
||||||
|
args.params.clear();
|
||||||
|
for (size_t z = 0; z < tokens.size(); z++) {
|
||||||
|
if (route.path_tokens[z].starts_with(':')) {
|
||||||
|
args.params.emplace(route.path_tokens[z].substr(1), tokens[z]);
|
||||||
|
} else if (route.path_tokens[z] != tokens[z]) {
|
||||||
|
matched = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
if (req.method != route.method) {
|
||||||
|
throw HTTPError(405, "Incorrect HTTP method");
|
||||||
|
}
|
||||||
|
if (req.method == HTTPRequest::Method::POST) {
|
||||||
|
auto* content_type = req.get_header("content-type");
|
||||||
|
if (!content_type || (*content_type != "application/json")) {
|
||||||
|
throw HTTPError(400, "POST requests must use the application/json content type");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
args.post_data = phosg::JSON::parse(req.data);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
throw HTTPError(400, std::format("Invalid JSON: {}", e.what()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
co_return co_await route.handler(std::move(args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw HTTPError(404, "Request path did not match any route");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Route {
|
||||||
|
HTTPRequest::Method method;
|
||||||
|
std::vector<std::string> path_tokens;
|
||||||
|
Handler handler;
|
||||||
|
};
|
||||||
|
std::vector<Route> routes;
|
||||||
|
};
|
||||||
|
|
||||||
struct HTTPServerLimits {
|
struct HTTPServerLimits {
|
||||||
size_t max_http_request_line_size = 0x1000; // 4KB
|
size_t max_http_request_line_size = 0x1000; // 4KB
|
||||||
size_t max_http_data_size = 0x200000; // 2MB
|
size_t max_http_data_size = 0x200000; // 2MB
|
||||||
@@ -120,6 +221,29 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
HTTPServerLimits limits;
|
HTTPServerLimits limits;
|
||||||
|
|
||||||
|
void require_GET(const HTTPRequest& req) {
|
||||||
|
if (req.method != HTTPRequest::Method::GET) {
|
||||||
|
throw HTTPError(405, "GET method required for this endpoint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
phosg::JSON require_JSON_POST(const HTTPRequest& req) {
|
||||||
|
if (req.method != HTTPRequest::Method::POST) {
|
||||||
|
throw HTTPError(405, "POST method required for this endpoint");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* content_type = req.get_header("content-type");
|
||||||
|
if (!content_type || (*content_type != "application/json")) {
|
||||||
|
throw HTTPError(400, "POST requests must use the application/json content type");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return phosg::JSON::parse(req.data);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
throw HTTPError(400, std::format("Invalid JSON: {}", e.what()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Attempts to switch the client to WebSockets. Returns true if this is done
|
// Attempts to switch the client to WebSockets. Returns true if this is done
|
||||||
// successfully (and the caller should then receive/send WebSocket messages),
|
// successfully (and the caller should then receive/send WebSocket messages),
|
||||||
// or false if this failed (and the caller should send an HTTP response).
|
// or false if this failed (and the caller should send an HTTP response).
|
||||||
|
|||||||
+7
-4
@@ -13,15 +13,18 @@ AsyncEvent::AsyncEvent(asio::any_io_executor ex)
|
|||||||
: executor(ex), is_set(false) {}
|
: executor(ex), is_set(false) {}
|
||||||
|
|
||||||
void AsyncEvent::set() {
|
void AsyncEvent::set() {
|
||||||
lock_guard g(this->lock);
|
std::vector<std::unique_ptr<asio::detail::awaitable_handler<asio::any_io_executor>>> waiters_to_resume;
|
||||||
this->is_set = true;
|
{
|
||||||
for (auto& waiter : this->waiters) {
|
lock_guard g(this->lock);
|
||||||
|
this->is_set = true;
|
||||||
|
this->waiters.swap(waiters_to_resume);
|
||||||
|
}
|
||||||
|
for (auto& waiter : waiters_to_resume) {
|
||||||
asio::post(this->executor,
|
asio::post(this->executor,
|
||||||
[handler = std::move(waiter)]() mutable {
|
[handler = std::move(waiter)]() mutable {
|
||||||
(*handler)();
|
(*handler)();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this->waiters.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AsyncEvent::clear() {
|
void AsyncEvent::clear() {
|
||||||
|
|||||||
+12
-2
@@ -189,8 +189,14 @@ public:
|
|||||||
return this->sock;
|
return this->sock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool is_open() const {
|
||||||
|
return this->sock.is_open();
|
||||||
|
}
|
||||||
|
|
||||||
inline void close() {
|
inline void close() {
|
||||||
this->sock.close();
|
if (this->sock.is_open()) {
|
||||||
|
this->sock.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -259,7 +265,11 @@ asio::awaitable<std::invoke_result_t<FnT, ArgTs...>> call_on_thread_pool(asio::t
|
|||||||
// call_on_thread_pool coroutine has been destroyed)
|
// call_on_thread_pool coroutine has been destroyed)
|
||||||
auto promise = std::make_shared<AsyncPromise<ReturnT>>();
|
auto promise = std::make_shared<AsyncPromise<ReturnT>>();
|
||||||
asio::post(pool, [bound = std::move(bound), promise]() mutable {
|
asio::post(pool, [bound = std::move(bound), promise]() mutable {
|
||||||
promise->set_value(bound());
|
try {
|
||||||
|
promise->set_value(bound());
|
||||||
|
} catch (...) {
|
||||||
|
promise->set_exception(std::current_exception());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
co_return co_await promise->get();
|
co_return co_await promise->get();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ void BattleParamsIndex::Table::print(FILE* stream, Episode episode) const {
|
|||||||
names_str);
|
names_str);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (size_t diff = 0; diff < 4; diff++) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
phosg::fwrite_fmt(stream, "{} ZZ ATP PSV EVP HP DFP ATA LCK ESP EXP DIFF NAMES\n",
|
phosg::fwrite_fmt(stream, "{} ZZ ATP PSV EVP HP DFP ATA LCK ESP EXP DIFF NAMES\n",
|
||||||
abbreviation_for_difficulty(diff));
|
abbreviation_for_difficulty(difficulty));
|
||||||
for (size_t z = 0; z < 0x60; z++) {
|
for (size_t z = 0; z < 0x60; z++) {
|
||||||
phosg::fwrite_fmt(stream, " {:02X} ", z);
|
phosg::fwrite_fmt(stream, " {:02X} ", z);
|
||||||
print_entry(this->stats[diff][z], z);
|
print_entry(this->stats[static_cast<size_t>(difficulty)][z], z);
|
||||||
fputc('\n', stream);
|
fputc('\n', stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,12 +70,25 @@ public:
|
|||||||
} __packed_ws__(MovementData, 0x30);
|
} __packed_ws__(MovementData, 0x30);
|
||||||
|
|
||||||
struct Table {
|
struct Table {
|
||||||
/* 0000 */ parray<parray<PlayerStats, 0x60>, 4> stats;
|
/* 0000 */ parray<parray<PlayerStats, 0x60>, 4> stats; // [difficulty][bp_index]
|
||||||
/* 3600 */ parray<parray<AttackData, 0x60>, 4> attack_data;
|
/* 3600 */ parray<parray<AttackData, 0x60>, 4> attack_data; // [difficulty][bp_index]
|
||||||
/* 7E00 */ parray<parray<ResistData, 0x60>, 4> resist_data;
|
/* 7E00 */ parray<parray<ResistData, 0x60>, 4> resist_data; // [difficulty][bp_index]
|
||||||
/* AE00 */ parray<parray<MovementData, 0x60>, 4> movement_data;
|
/* AE00 */ parray<parray<MovementData, 0x60>, 4> movement_data; // [difficulty][bp_index]
|
||||||
/* F600 */
|
/* F600 */
|
||||||
|
|
||||||
|
const PlayerStats& stats_for_index(Difficulty difficulty, uint8_t index) const {
|
||||||
|
return this->stats.at(static_cast<size_t>(difficulty)).at(index);
|
||||||
|
}
|
||||||
|
const AttackData& attack_data_for_index(Difficulty difficulty, uint8_t index) const {
|
||||||
|
return this->attack_data.at(static_cast<size_t>(difficulty)).at(index);
|
||||||
|
}
|
||||||
|
const ResistData& resist_data_for_index(Difficulty difficulty, uint8_t index) const {
|
||||||
|
return this->resist_data.at(static_cast<size_t>(difficulty)).at(index);
|
||||||
|
}
|
||||||
|
const MovementData& movement_data_for_index(Difficulty difficulty, uint8_t index) const {
|
||||||
|
return this->movement_data.at(static_cast<size_t>(difficulty)).at(index);
|
||||||
|
}
|
||||||
|
|
||||||
void print(FILE* stream, Episode episode) const;
|
void print(FILE* stream, Episode episode) const;
|
||||||
} __packed_ws__(Table, 0xF600);
|
} __packed_ws__(Table, 0xF600);
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -8,6 +8,7 @@
|
|||||||
#include <phosg/Time.hh>
|
#include <phosg/Time.hh>
|
||||||
|
|
||||||
#include "Loggers.hh"
|
#include "Loggers.hh"
|
||||||
|
#include "StaticGameData.hh"
|
||||||
#include "Version.hh"
|
#include "Version.hh"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
@@ -16,7 +17,7 @@ extern bool use_terminal_colors;
|
|||||||
|
|
||||||
Channel::Channel(
|
Channel::Channel(
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const string& name,
|
const string& name,
|
||||||
phosg::TerminalFormat terminal_send_color,
|
phosg::TerminalFormat terminal_send_color,
|
||||||
phosg::TerminalFormat terminal_recv_color)
|
phosg::TerminalFormat terminal_recv_color)
|
||||||
@@ -249,7 +250,7 @@ shared_ptr<SocketChannel> SocketChannel::create(
|
|||||||
std::shared_ptr<asio::io_context> io_context,
|
std::shared_ptr<asio::io_context> io_context,
|
||||||
std::unique_ptr<asio::ip::tcp::socket>&& sock,
|
std::unique_ptr<asio::ip::tcp::socket>&& sock,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const string& name,
|
const string& name,
|
||||||
phosg::TerminalFormat terminal_send_color,
|
phosg::TerminalFormat terminal_send_color,
|
||||||
phosg::TerminalFormat terminal_recv_color) {
|
phosg::TerminalFormat terminal_recv_color) {
|
||||||
@@ -263,7 +264,7 @@ SocketChannel::SocketChannel(
|
|||||||
std::shared_ptr<asio::io_context> io_context,
|
std::shared_ptr<asio::io_context> io_context,
|
||||||
std::unique_ptr<asio::ip::tcp::socket>&& sock,
|
std::unique_ptr<asio::ip::tcp::socket>&& sock,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const string& name,
|
const string& name,
|
||||||
phosg::TerminalFormat terminal_send_color,
|
phosg::TerminalFormat terminal_send_color,
|
||||||
phosg::TerminalFormat terminal_recv_color)
|
phosg::TerminalFormat terminal_recv_color)
|
||||||
@@ -331,7 +332,7 @@ asio::awaitable<void> SocketChannel::send_task() {
|
|||||||
PeerChannel::PeerChannel(
|
PeerChannel::PeerChannel(
|
||||||
std::shared_ptr<asio::io_context> io_context,
|
std::shared_ptr<asio::io_context> io_context,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const std::string& name,
|
const std::string& name,
|
||||||
phosg::TerminalFormat terminal_send_color,
|
phosg::TerminalFormat terminal_send_color,
|
||||||
phosg::TerminalFormat terminal_recv_color)
|
phosg::TerminalFormat terminal_recv_color)
|
||||||
|
|||||||
+5
-5
@@ -12,7 +12,7 @@
|
|||||||
class Channel {
|
class Channel {
|
||||||
public:
|
public:
|
||||||
Version version;
|
Version version;
|
||||||
uint8_t language;
|
Language language;
|
||||||
std::shared_ptr<PSOEncryption> crypt_in;
|
std::shared_ptr<PSOEncryption> crypt_in;
|
||||||
std::shared_ptr<PSOEncryption> crypt_out;
|
std::shared_ptr<PSOEncryption> crypt_out;
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
Channel(
|
Channel(
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const std::string& name,
|
const std::string& name,
|
||||||
phosg::TerminalFormat terminal_send_color = phosg::TerminalFormat::END,
|
phosg::TerminalFormat terminal_send_color = phosg::TerminalFormat::END,
|
||||||
phosg::TerminalFormat terminal_recv_color = phosg::TerminalFormat::END);
|
phosg::TerminalFormat terminal_recv_color = phosg::TerminalFormat::END);
|
||||||
@@ -120,7 +120,7 @@ public:
|
|||||||
static std::shared_ptr<SocketChannel> create(std::shared_ptr<asio::io_context> io_context,
|
static std::shared_ptr<SocketChannel> create(std::shared_ptr<asio::io_context> io_context,
|
||||||
std::unique_ptr<asio::ip::tcp::socket>&& sock,
|
std::unique_ptr<asio::ip::tcp::socket>&& sock,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const std::string& name = "",
|
const std::string& name = "",
|
||||||
phosg::TerminalFormat terminal_send_color = phosg::TerminalFormat::END,
|
phosg::TerminalFormat terminal_send_color = phosg::TerminalFormat::END,
|
||||||
phosg::TerminalFormat terminal_recv_color = phosg::TerminalFormat::END);
|
phosg::TerminalFormat terminal_recv_color = phosg::TerminalFormat::END);
|
||||||
@@ -138,7 +138,7 @@ private:
|
|||||||
std::shared_ptr<asio::io_context> io_context,
|
std::shared_ptr<asio::io_context> io_context,
|
||||||
std::unique_ptr<asio::ip::tcp::socket>&& sock,
|
std::unique_ptr<asio::ip::tcp::socket>&& sock,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const std::string& name,
|
const std::string& name,
|
||||||
phosg::TerminalFormat terminal_send_color,
|
phosg::TerminalFormat terminal_send_color,
|
||||||
phosg::TerminalFormat terminal_recv_color);
|
phosg::TerminalFormat terminal_recv_color);
|
||||||
@@ -158,7 +158,7 @@ public:
|
|||||||
PeerChannel(
|
PeerChannel(
|
||||||
std::shared_ptr<asio::io_context> io_context,
|
std::shared_ptr<asio::io_context> io_context,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const std::string& name = "",
|
const std::string& name = "",
|
||||||
phosg::TerminalFormat terminal_send_color = phosg::TerminalFormat::END,
|
phosg::TerminalFormat terminal_send_color = phosg::TerminalFormat::END,
|
||||||
phosg::TerminalFormat terminal_recv_color = phosg::TerminalFormat::END);
|
phosg::TerminalFormat terminal_recv_color = phosg::TerminalFormat::END);
|
||||||
|
|||||||
+125
-27
@@ -1,5 +1,6 @@
|
|||||||
#include "ChatCommands.hh"
|
#include "ChatCommands.hh"
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
@@ -921,7 +922,7 @@ ChatCommandDefinition cc_edit(
|
|||||||
p->disp.stats.experience = stoul(tokens.at(1));
|
p->disp.stats.experience = stoul(tokens.at(1));
|
||||||
} else if (tokens.at(0) == "level" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
} else if (tokens.at(0) == "level" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||||
p->disp.stats.level = stoul(tokens.at(1)) - 1;
|
p->disp.stats.level = stoul(tokens.at(1)) - 1;
|
||||||
p->recompute_stats(s->level_table(a.c->version()));
|
p->recompute_stats(s->level_table(a.c->version()), true);
|
||||||
} else if (((tokens.at(0) == "material") || (tokens.at(0) == "mat")) && !is_v1_or_v2(a.c->version()) && (cheats_allowed || !s->cheat_flags.reset_materials)) {
|
} else if (((tokens.at(0) == "material") || (tokens.at(0) == "mat")) && !is_v1_or_v2(a.c->version()) && (cheats_allowed || !s->cheat_flags.reset_materials)) {
|
||||||
if (tokens.at(1) == "reset") {
|
if (tokens.at(1) == "reset") {
|
||||||
const auto& which = tokens.at(2);
|
const auto& which = tokens.at(2);
|
||||||
@@ -959,14 +960,14 @@ ChatCommandDefinition cc_edit(
|
|||||||
} else {
|
} else {
|
||||||
throw precondition_failed("$C6Invalid subcommand");
|
throw precondition_failed("$C6Invalid subcommand");
|
||||||
}
|
}
|
||||||
p->recompute_stats(s->level_table(a.c->version()));
|
p->recompute_stats(s->level_table(a.c->version()), false);
|
||||||
} else if (tokens.at(0) == "namecolor") {
|
} else if (tokens.at(0) == "namecolor") {
|
||||||
p->disp.visual.name_color = stoul(tokens.at(1), nullptr, 16);
|
p->disp.visual.name_color = stoul(tokens.at(1), nullptr, 16);
|
||||||
} else if (tokens.at(0) == "language" || tokens.at(0) == "lang") {
|
} else if (tokens.at(0) == "language" || tokens.at(0) == "lang") {
|
||||||
if (tokens.at(1).size() != 1) {
|
if (tokens.at(1).size() != 1) {
|
||||||
throw runtime_error("invalid language");
|
throw runtime_error("invalid language");
|
||||||
}
|
}
|
||||||
uint8_t new_language = language_code_for_char(tokens.at(1).at(0));
|
Language new_language = language_for_char(tokens.at(1).at(0));
|
||||||
a.c->channel->language = new_language;
|
a.c->channel->language = new_language;
|
||||||
p->inventory.language = new_language;
|
p->inventory.language = new_language;
|
||||||
p->guild_card.language = new_language;
|
p->guild_card.language = new_language;
|
||||||
@@ -985,7 +986,7 @@ ChatCommandDefinition cc_edit(
|
|||||||
p->disp.visual.section_id = secid;
|
p->disp.visual.section_id = secid;
|
||||||
}
|
}
|
||||||
} else if (tokens.at(0) == "name") {
|
} else if (tokens.at(0) == "name") {
|
||||||
vector<string> orig_tokens = phosg::split(a.text, ' ');
|
vector<string> orig_tokens = phosg::split(a.text, ' ', 1);
|
||||||
p->disp.name.encode(orig_tokens.at(1), p->inventory.language);
|
p->disp.name.encode(orig_tokens.at(1), p->inventory.language);
|
||||||
} else if (tokens.at(0) == "npc") {
|
} else if (tokens.at(0) == "npc") {
|
||||||
if (tokens.at(1) == "none") {
|
if (tokens.at(1) == "none") {
|
||||||
@@ -1103,14 +1104,12 @@ ChatCommandDefinition cc_exit(
|
|||||||
is_in_quest = a.c->proxy_session->is_in_quest;
|
is_in_quest = a.c->proxy_session->is_in_quest;
|
||||||
} else {
|
} else {
|
||||||
auto l = a.c->require_lobby();
|
auto l = a.c->require_lobby();
|
||||||
is_in_quest = (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) ||
|
is_in_quest = (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS));
|
||||||
l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS));
|
|
||||||
}
|
}
|
||||||
if (is_in_quest) {
|
if (is_in_quest) {
|
||||||
// Client is in a quest; command 6x73 triggers game exit
|
// Client is in a quest; command 6x73 triggers game exit
|
||||||
G_UnusedHeader cmd = {0x73, 0x01, 0x0000};
|
G_UnusedHeader cmd = {0x73, 0x01, 0x0000};
|
||||||
a.c->channel->send(0x60, 0x00, cmd);
|
a.c->channel->send(0x60, 0x00, cmd);
|
||||||
a.c->floor = 0;
|
|
||||||
co_return;
|
co_return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1433,11 +1432,12 @@ ChatCommandDefinition cc_lobby_info(
|
|||||||
if (l->max_level == 0xFFFFFFFF) {
|
if (l->max_level == 0xFFFFFFFF) {
|
||||||
lines.emplace_back(std::format("$C6{:08X}$C7 L$C6{}+$C7", l->lobby_id, l->min_level + 1));
|
lines.emplace_back(std::format("$C6{:08X}$C7 L$C6{}+$C7", l->lobby_id, l->min_level + 1));
|
||||||
} else {
|
} else {
|
||||||
lines.emplace_back(std::format(
|
lines.emplace_back(std::format("$C6{:08X}$C7 L$C6{}-{}$C7", l->lobby_id, l->min_level + 1, l->max_level + 1));
|
||||||
"$C6{:08X}$C7 L$C6{}-{}$C7", l->lobby_id, l->min_level + 1, l->max_level + 1));
|
}
|
||||||
|
uint8_t effective_section_id = l->effective_section_id();
|
||||||
|
if (effective_section_id < 10) {
|
||||||
|
lines.emplace_back(std::format("$C7Section ID: $C6{}$C7", name_for_section_id(effective_section_id)));
|
||||||
}
|
}
|
||||||
lines.emplace_back(std::format(
|
|
||||||
"$C7Section ID: $C6{}$C7", name_for_section_id(l->effective_section_id())));
|
|
||||||
|
|
||||||
switch (l->drop_mode) {
|
switch (l->drop_mode) {
|
||||||
case ServerDropMode::DISABLED:
|
case ServerDropMode::DISABLED:
|
||||||
@@ -1621,6 +1621,97 @@ ChatCommandDefinition cc_loadchar(
|
|||||||
co_return;
|
co_return;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ChatCommandDefinition cc_makeobj(
|
||||||
|
{"$makeobj"},
|
||||||
|
+[](const Args& a) -> asio::awaitable<void> {
|
||||||
|
a.check_debug_enabled();
|
||||||
|
|
||||||
|
auto tokens = phosg::split(a.text, ' ');
|
||||||
|
if (tokens.size() < 1) {
|
||||||
|
throw runtime_error("not enough arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t base_type_high = stoul(tokens[0], nullptr, 0) << 16;
|
||||||
|
VectorXYZF pos = a.c->pos;
|
||||||
|
VectorXYZI angle{0, 0, 0};
|
||||||
|
VectorXYZF param123{0, 0, 0};
|
||||||
|
VectorXYZI param456{0, 0, 0};
|
||||||
|
for (size_t z = 1; z < tokens.size(); z++) {
|
||||||
|
auto subtokens = phosg::split(tokens[z], ':');
|
||||||
|
if (subtokens.size() != 2 || subtokens[0].size() != 1) {
|
||||||
|
throw runtime_error("invalid argument: " + tokens[z]);
|
||||||
|
}
|
||||||
|
switch (tolower(subtokens[0].front())) {
|
||||||
|
case 'X':
|
||||||
|
case 'x':
|
||||||
|
pos.x = stof(subtokens[1]);
|
||||||
|
break;
|
||||||
|
case 'Y':
|
||||||
|
case 'y':
|
||||||
|
pos.y = stof(subtokens[1]);
|
||||||
|
break;
|
||||||
|
case 'Z':
|
||||||
|
case 'z':
|
||||||
|
pos.z = stof(subtokens[1]);
|
||||||
|
break;
|
||||||
|
case 'R':
|
||||||
|
case 'r':
|
||||||
|
angle.x = stol(subtokens[1], nullptr, 0);
|
||||||
|
break;
|
||||||
|
case 'P':
|
||||||
|
case 'p':
|
||||||
|
angle.y = stol(subtokens[1], nullptr, 0);
|
||||||
|
break;
|
||||||
|
case 'W':
|
||||||
|
case 'w':
|
||||||
|
angle.z = stol(subtokens[1], nullptr, 0);
|
||||||
|
break;
|
||||||
|
case '1':
|
||||||
|
param123.x = stof(subtokens[1]);
|
||||||
|
break;
|
||||||
|
case '2':
|
||||||
|
param123.y = stof(subtokens[1]);
|
||||||
|
break;
|
||||||
|
case '3':
|
||||||
|
param123.z = stof(subtokens[1]);
|
||||||
|
break;
|
||||||
|
case '4':
|
||||||
|
param456.x = stol(subtokens[1], nullptr, 0);
|
||||||
|
break;
|
||||||
|
case '5':
|
||||||
|
param456.y = stol(subtokens[1], nullptr, 0);
|
||||||
|
break;
|
||||||
|
case '6':
|
||||||
|
param456.z = stol(subtokens[1], nullptr, 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw runtime_error("invalid argument: " + tokens[z]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unordered_map<string, uint32_t> label_writes{
|
||||||
|
{"base_type_high", base_type_high},
|
||||||
|
{"floor_low", a.c->floor},
|
||||||
|
{"pos_x", std::bit_cast<uint32_t>(pos.x.load())},
|
||||||
|
{"pos_y", std::bit_cast<uint32_t>(pos.y.load())},
|
||||||
|
{"pos_z", std::bit_cast<uint32_t>(pos.z.load())},
|
||||||
|
{"angle_x", angle.x},
|
||||||
|
{"angle_y", angle.y},
|
||||||
|
{"angle_z", angle.z},
|
||||||
|
{"param1", std::bit_cast<uint32_t>(param123.x.load())},
|
||||||
|
{"param2", std::bit_cast<uint32_t>(param123.y.load())},
|
||||||
|
{"param3", std::bit_cast<uint32_t>(param123.z.load())},
|
||||||
|
{"param4", param456.x},
|
||||||
|
{"param5", param456.y},
|
||||||
|
{"param6", param456.z},
|
||||||
|
};
|
||||||
|
|
||||||
|
co_await prepare_client_for_patches(a.c);
|
||||||
|
auto s = a.c->require_server_state();
|
||||||
|
auto fn = s->function_code_index->get_patch("CreateObject", a.c->specific_version);
|
||||||
|
co_await send_function_call(a.c, fn, label_writes);
|
||||||
|
});
|
||||||
|
|
||||||
ChatCommandDefinition cc_matcount(
|
ChatCommandDefinition cc_matcount(
|
||||||
{"$matcount"},
|
{"$matcount"},
|
||||||
+[](const Args& a) -> asio::awaitable<void> {
|
+[](const Args& a) -> asio::awaitable<void> {
|
||||||
@@ -1791,13 +1882,13 @@ ChatCommandDefinition cc_ping(
|
|||||||
co_return;
|
co_return;
|
||||||
});
|
});
|
||||||
|
|
||||||
static string file_path_for_recording(const std::string& args, uint32_t account_id) {
|
static string file_path_for_recording(const std::string& args, uint32_t account_id, bool compressed) {
|
||||||
for (char ch : args) {
|
for (char ch : args) {
|
||||||
if (ch <= 0x20 || ch > 0x7E || ch == '/') {
|
if (ch <= 0x20 || ch > 0x7E || ch == '/') {
|
||||||
throw runtime_error("invalid recording name");
|
throw runtime_error("invalid recording name");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return std::format("system/ep3/battle-records/{:010}_{}.mzrd", account_id, args);
|
return std::format("system/ep3/battle-records/{:010}_{}.mzr{}", account_id, args, compressed ? "" : "d");
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatCommandDefinition cc_playrec(
|
ChatCommandDefinition cc_playrec(
|
||||||
@@ -1810,25 +1901,28 @@ ChatCommandDefinition cc_playrec(
|
|||||||
if (l->is_game() && l->battle_player) {
|
if (l->is_game() && l->battle_player) {
|
||||||
l->battle_player->start();
|
l->battle_player->start();
|
||||||
} else if (!l->is_game()) {
|
} else if (!l->is_game()) {
|
||||||
string file_path = file_path_for_recording(a.text, a.c->login->account->account_id);
|
|
||||||
|
|
||||||
auto s = a.c->require_server_state();
|
auto s = a.c->require_server_state();
|
||||||
string filename = a.text;
|
string filename = a.text;
|
||||||
bool start_battle_player_immediately = (filename[0] == '!');
|
bool start_battle_player_immediately = (filename.at(0) != '!');
|
||||||
if (start_battle_player_immediately) {
|
if (!start_battle_player_immediately) {
|
||||||
filename = filename.substr(1);
|
filename = filename.substr(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
string data;
|
string data;
|
||||||
try {
|
try {
|
||||||
data = phosg::load_file(file_path);
|
data = phosg::load_file(file_path_for_recording(filename, a.c->login->account->account_id, false));
|
||||||
} catch (const phosg::cannot_open_file&) {
|
} catch (const phosg::cannot_open_file&) {
|
||||||
throw precondition_failed("$C4The recording does\nnot exist");
|
try {
|
||||||
|
data = prs_decompress(phosg::load_file(file_path_for_recording(filename, a.c->login->account->account_id, true)));
|
||||||
|
} catch (const phosg::cannot_open_file&) {
|
||||||
|
throw precondition_failed("$C4The recording does\nnot exist");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
auto record = make_shared<Episode3::BattleRecord>(data);
|
auto record = make_shared<Episode3::BattleRecord>(data);
|
||||||
auto battle_player = make_shared<Episode3::BattleRecordPlayer>(s->io_context, record);
|
auto battle_player = make_shared<Episode3::BattleRecordPlayer>(s->io_context, record);
|
||||||
auto game = create_game_generic(
|
auto game = create_game_generic(
|
||||||
s, a.c, a.text, "", Episode::EP3, GameMode::NORMAL, 0, false, nullptr, battle_player);
|
s, a.c, filename, "", Episode::EP3, GameMode::NORMAL, Difficulty::NORMAL, false, nullptr, battle_player);
|
||||||
if (game) {
|
if (game) {
|
||||||
if (start_battle_player_immediately) {
|
if (start_battle_player_immediately) {
|
||||||
game->set_flag(Lobby::Flag::START_BATTLE_PLAYER_IMMEDIATELY);
|
game->set_flag(Lobby::Flag::START_BATTLE_PLAYER_IMMEDIATELY);
|
||||||
@@ -1919,8 +2013,8 @@ static void command_qset_qclear(const Args& a, bool should_set) {
|
|||||||
a.c->proxy_session->server_channel->send(0x60, 0x00, &cmd, sizeof(cmd));
|
a.c->proxy_session->server_channel->send(0x60, 0x00, &cmd, sizeof(cmd));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
uint8_t difficulty = a.c->proxy_session ? a.c->proxy_session->lobby_difficulty : a.c->require_lobby()->difficulty;
|
Difficulty difficulty = a.c->proxy_session ? a.c->proxy_session->lobby_difficulty : a.c->require_lobby()->difficulty;
|
||||||
G_UpdateQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x03, 0x0000}, flag_num, should_set ? 0 : 1}, difficulty, 0x0000};
|
G_UpdateQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x03, 0x0000}, flag_num, should_set ? 0 : 1}, static_cast<uint16_t>(difficulty), 0x0000};
|
||||||
a.c->channel->send(0x60, 0x00, &cmd, sizeof(cmd));
|
a.c->channel->send(0x60, 0x00, &cmd, sizeof(cmd));
|
||||||
if (a.c->proxy_session) {
|
if (a.c->proxy_session) {
|
||||||
a.c->proxy_session->server_channel->send(0x60, 0x00, &cmd, sizeof(cmd));
|
a.c->proxy_session->server_channel->send(0x60, 0x00, &cmd, sizeof(cmd));
|
||||||
@@ -2197,7 +2291,7 @@ ChatCommandDefinition cc_saverec(
|
|||||||
if (!a.c->ep3_prev_battle_record) {
|
if (!a.c->ep3_prev_battle_record) {
|
||||||
throw precondition_failed("$C4No finished\nrecording is\npresent");
|
throw precondition_failed("$C4No finished\nrecording is\npresent");
|
||||||
}
|
}
|
||||||
string file_path = file_path_for_recording(a.text, a.c->login->account->account_id);
|
string file_path = file_path_for_recording(a.text, a.c->login->account->account_id, false);
|
||||||
string data = a.c->ep3_prev_battle_record->serialize();
|
string data = a.c->ep3_prev_battle_record->serialize();
|
||||||
phosg::save_file(file_path, data);
|
phosg::save_file(file_path, data);
|
||||||
send_text_message(a.c, "$C7Recording saved");
|
send_text_message(a.c, "$C7Recording saved");
|
||||||
@@ -2374,7 +2468,7 @@ ChatCommandDefinition cc_sound(
|
|||||||
uint32_t sound_id = stoul(echo_to_all ? a.text.substr(1) : a.text, nullptr, 16);
|
uint32_t sound_id = stoul(echo_to_all ? a.text.substr(1) : a.text, nullptr, 16);
|
||||||
|
|
||||||
auto l = a.c->require_lobby();
|
auto l = a.c->require_lobby();
|
||||||
uint8_t area = l->area_for_floor(a.c->version(), a.c->floor);
|
uint8_t area = l->is_game() ? l->area_for_floor(a.c->version(), a.c->floor) : 0x0F;
|
||||||
G_PlaySoundFromPlayer_6xB2 cmd = {{0xB2, 0x03, 0x0000}, area, 0x00, a.c->lobby_client_id, sound_id};
|
G_PlaySoundFromPlayer_6xB2 cmd = {{0xB2, 0x03, 0x0000}, area, 0x00, a.c->lobby_client_id, sound_id};
|
||||||
if (!echo_to_all) {
|
if (!echo_to_all) {
|
||||||
send_command_t(a.c, 0x60, 0x00, cmd);
|
send_command_t(a.c, 0x60, 0x00, cmd);
|
||||||
@@ -2658,7 +2752,7 @@ ChatCommandDefinition cc_switchchar(
|
|||||||
throw precondition_failed("No character exists\nin that slot");
|
throw precondition_failed("No character exists\nin that slot");
|
||||||
}
|
}
|
||||||
|
|
||||||
a.c->save_and_unload_character();
|
a.c->unload_character(true);
|
||||||
a.c->bb_character_index = index;
|
a.c->bb_character_index = index;
|
||||||
a.c->bb_bank_character_index = index;
|
a.c->bb_bank_character_index = index;
|
||||||
|
|
||||||
@@ -2697,8 +2791,12 @@ ChatCommandDefinition cc_unset(
|
|||||||
throw precondition_failed("$C6Battle has not\nyet begun");
|
throw precondition_failed("$C6Battle has not\nyet begun");
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t index = stoull(a.text) - 1;
|
size_t index = stoull(a.text);
|
||||||
l->ep3_server->force_destroy_field_character(a.c->lobby_client_id, index);
|
if (index == 0) {
|
||||||
|
l->ep3_server->force_replace_assist_card(a.c->lobby_client_id, 0xFFFF);
|
||||||
|
} else {
|
||||||
|
l->ep3_server->force_destroy_field_character(a.c->lobby_client_id, index - 1);
|
||||||
|
}
|
||||||
co_return;
|
co_return;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2887,7 +2985,7 @@ static void whatobj_whatene_fn(const Args& a, bool include_objs, bool include_en
|
|||||||
const auto* set_entry = nearest_ene->super_ene->version(a.c->version()).set_entry;
|
const auto* set_entry = nearest_ene->super_ene->version(a.c->version()).set_entry;
|
||||||
string type_name = MapFile::name_for_enemy_type(set_entry->base_type, a.c->version(), area);
|
string type_name = MapFile::name_for_enemy_type(set_entry->base_type, a.c->version(), area);
|
||||||
send_text_message_fmt(a.c, "$C5E-{:03X}\n$C6{}\n$C2{}\n$C7X:{:.2f} Z:{:.2f}",
|
send_text_message_fmt(a.c, "$C5E-{:03X}\n$C6{}\n$C2{}\n$C7X:{:.2f} Z:{:.2f}",
|
||||||
nearest_ene->e_id, phosg::name_for_enum(nearest_ene->type(a.c->version(), l->episode, l->event)),
|
nearest_ene->e_id, phosg::name_for_enum(nearest_ene->type(a.c->version(), l->episode, l->difficulty, l->event)),
|
||||||
type_name, nearest_worldspace_pos.x, nearest_worldspace_pos.z);
|
type_name, nearest_worldspace_pos.x, nearest_worldspace_pos.z);
|
||||||
auto set_str = set_entry->str(a.c->version(), area);
|
auto set_str = set_entry->str(a.c->version(), area);
|
||||||
a.c->log.info_f("Enemy found via $whatobj: E-{:03X} {} at x={:g} y={:g} z={:g}",
|
a.c->log.info_f("Enemy found via $whatobj: E-{:03X} {} at x={:g} y={:g} z={:g}",
|
||||||
|
|||||||
+105
-91
@@ -371,7 +371,7 @@ bool Client::evaluate_quest_availability_expression(
|
|||||||
shared_ptr<const IntegralExpression> expr,
|
shared_ptr<const IntegralExpression> expr,
|
||||||
shared_ptr<const Lobby> game,
|
shared_ptr<const Lobby> game,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
size_t num_players,
|
size_t num_players,
|
||||||
bool v1_present) const {
|
bool v1_present) const {
|
||||||
if (this->login && this->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
if (this->login && this->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||||
@@ -385,7 +385,7 @@ bool Client::evaluate_quest_availability_expression(
|
|||||||
}
|
}
|
||||||
auto p = this->character_file();
|
auto p = this->character_file();
|
||||||
IntegralExpression::Env env = {
|
IntegralExpression::Env env = {
|
||||||
.flags = &p->quest_flags.data.at(difficulty),
|
.flags = &p->quest_flags.data.at(static_cast<size_t>(difficulty)),
|
||||||
.challenge_records = &p->challenge_records,
|
.challenge_records = &p->challenge_records,
|
||||||
.team = this->team(),
|
.team = this->team(),
|
||||||
.num_players = num_players,
|
.num_players = num_players,
|
||||||
@@ -404,7 +404,7 @@ bool Client::can_see_quest(
|
|||||||
shared_ptr<const Quest> q,
|
shared_ptr<const Quest> q,
|
||||||
shared_ptr<const Lobby> game,
|
shared_ptr<const Lobby> game,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
size_t num_players,
|
size_t num_players,
|
||||||
bool v1_present) const {
|
bool v1_present) const {
|
||||||
if (!q->has_version_any_language(this->version())) {
|
if (!q->has_version_any_language(this->version())) {
|
||||||
@@ -418,7 +418,7 @@ bool Client::can_play_quest(
|
|||||||
shared_ptr<const Quest> q,
|
shared_ptr<const Quest> q,
|
||||||
shared_ptr<const Lobby> game,
|
shared_ptr<const Lobby> game,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
size_t num_players,
|
size_t num_players,
|
||||||
bool v1_present) const {
|
bool v1_present) const {
|
||||||
if (!q->has_version_any_language(this->version())) {
|
if (!q->has_version_any_language(this->version())) {
|
||||||
@@ -563,6 +563,9 @@ shared_ptr<PSOBBCharacterFile> Client::character_file(bool allow_load, bool allo
|
|||||||
throw runtime_error("character index not specified");
|
throw runtime_error("character index not specified");
|
||||||
}
|
}
|
||||||
this->load_all_files();
|
this->load_all_files();
|
||||||
|
if (!this->character_data) {
|
||||||
|
throw std::runtime_error("none of the corresponding character files exist");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this->character_data;
|
return this->character_data;
|
||||||
}
|
}
|
||||||
@@ -618,7 +621,7 @@ void Client::save_character_file() {
|
|||||||
|
|
||||||
void Client::create_character_file(
|
void Client::create_character_file(
|
||||||
uint32_t guild_card_number,
|
uint32_t guild_card_number,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const PlayerDispDataBBPreview& preview,
|
const PlayerDispDataBBPreview& preview,
|
||||||
shared_ptr<const LevelTable> level_table) {
|
shared_ptr<const LevelTable> level_table) {
|
||||||
this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table);
|
this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table);
|
||||||
@@ -766,7 +769,7 @@ std::shared_ptr<PlayerBank> Client::bank_file(bool allow_load) {
|
|||||||
if (this->bb_bank_character_index == this->bb_character_index) {
|
if (this->bb_bank_character_index == this->bb_character_index) {
|
||||||
this->bank_data = std::make_shared<PlayerBank>(this->character_file(true, false)->bank);
|
this->bank_data = std::make_shared<PlayerBank>(this->character_file(true, false)->bank);
|
||||||
this->log.info_f("Using bank data from loaded character");
|
this->log.info_f("Using bank data from loaded character");
|
||||||
} else {
|
} else if (this->bb_bank_character_index >= 0) {
|
||||||
if (!this->login || !this->login->bb_license) {
|
if (!this->login || !this->login->bb_license) {
|
||||||
throw logic_error("client is not logged in");
|
throw logic_error("client is not logged in");
|
||||||
}
|
}
|
||||||
@@ -774,6 +777,10 @@ std::shared_ptr<PlayerBank> Client::bank_file(bool allow_load) {
|
|||||||
auto character = PSOCHARFile::load_shared(filename, false).character_file;
|
auto character = PSOCHARFile::load_shared(filename, false).character_file;
|
||||||
this->bank_data = std::make_shared<PlayerBank>(character->bank);
|
this->bank_data = std::make_shared<PlayerBank>(character->bank);
|
||||||
this->log.info_f("Using bank data from {}", filename);
|
this->log.info_f("Using bank data from {}", filename);
|
||||||
|
} else {
|
||||||
|
// The shared bank doesn't exist; make a new one
|
||||||
|
this->bank_data = make_shared<PlayerBank>();
|
||||||
|
this->log.info_f("Created new shared bank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,20 +874,17 @@ void Client::load_all_files() {
|
|||||||
throw logic_error("cannot load BB player data until client is logged in");
|
throw logic_error("cannot load BB player data until client is logged in");
|
||||||
}
|
}
|
||||||
|
|
||||||
this->system_data.reset();
|
if (!this->system_data) {
|
||||||
this->character_data.reset();
|
string sys_filename = this->system_filename();
|
||||||
this->guild_card_data.reset();
|
if (std::filesystem::is_regular_file(sys_filename)) {
|
||||||
this->bank_data.reset();
|
this->system_data = make_shared<PSOBBBaseSystemFile>(phosg::load_object_file<PSOBBBaseSystemFile>(sys_filename, true));
|
||||||
|
this->log.info_f("Loaded system data from {}", sys_filename);
|
||||||
string sys_filename = this->system_filename();
|
} else {
|
||||||
if (std::filesystem::is_regular_file(sys_filename)) {
|
this->log.info_f("System file is missing: {}", sys_filename);
|
||||||
this->system_data = make_shared<PSOBBBaseSystemFile>(phosg::load_object_file<PSOBBBaseSystemFile>(sys_filename, true));
|
}
|
||||||
this->log.info_f("Loaded system data from {}", sys_filename);
|
|
||||||
} else {
|
|
||||||
this->log.info_f("System file is missing: {}", sys_filename);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->bb_character_index >= 0) {
|
if (!this->character_data && (this->bb_character_index >= 0)) {
|
||||||
string char_filename = this->character_filename();
|
string char_filename = this->character_filename();
|
||||||
if (std::filesystem::is_regular_file(char_filename)) {
|
if (std::filesystem::is_regular_file(char_filename)) {
|
||||||
auto psochar = PSOCHARFile::load_shared(char_filename, !this->system_data);
|
auto psochar = PSOCHARFile::load_shared(char_filename, !this->system_data);
|
||||||
@@ -905,15 +909,18 @@ void Client::load_all_files() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string card_filename = this->guild_card_filename();
|
if (!this->guild_card_data) {
|
||||||
if (std::filesystem::is_regular_file(card_filename)) {
|
string card_filename = this->guild_card_filename();
|
||||||
this->guild_card_data = make_shared<PSOBBGuildCardFile>(phosg::load_object_file<PSOBBGuildCardFile>(card_filename));
|
if (std::filesystem::is_regular_file(card_filename)) {
|
||||||
this->log.info_f("Loaded Guild Card data from {}", card_filename);
|
this->guild_card_data = make_shared<PSOBBGuildCardFile>(phosg::load_object_file<PSOBBGuildCardFile>(card_filename));
|
||||||
} else {
|
this->log.info_f("Loaded Guild Card data from {}", card_filename);
|
||||||
this->log.info_f("Guild Card file is missing: {}", card_filename);
|
} else {
|
||||||
|
this->log.info_f("Guild Card file is missing: {}", card_filename);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If any of the above files were missing, try to load from .nsa/.nsc files instead
|
// If any of the above files are still missing, try to load from .nsa/.nsc
|
||||||
|
// files instead
|
||||||
if (!this->system_data || (!this->character_data && (this->bb_character_index >= 0)) || !this->guild_card_data) {
|
if (!this->system_data || (!this->character_data && (this->bb_character_index >= 0)) || !this->guild_card_data) {
|
||||||
string nsa_filename = this->legacy_account_filename();
|
string nsa_filename = this->legacy_account_filename();
|
||||||
shared_ptr<LegacySavedAccountDataBB> nsa_data;
|
shared_ptr<LegacySavedAccountDataBB> nsa_data;
|
||||||
@@ -932,75 +939,75 @@ void Client::load_all_files() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this->system_data) {
|
|
||||||
this->system_data = make_shared<PSOBBBaseSystemFile>();
|
|
||||||
auto s = this->require_server_state();
|
|
||||||
if (s->bb_default_keyboard_config) {
|
|
||||||
this->system_data->key_config = *s->bb_default_keyboard_config;
|
|
||||||
}
|
|
||||||
if (s->bb_default_joystick_config) {
|
|
||||||
this->system_data->joystick_config = *s->bb_default_joystick_config;
|
|
||||||
}
|
|
||||||
this->log.info_f("Created new system data");
|
|
||||||
}
|
|
||||||
if (!this->guild_card_data) {
|
|
||||||
this->guild_card_data = make_shared<PSOBBGuildCardFile>();
|
|
||||||
this->log.info_f("Created new Guild Card data");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this->character_data && (this->bb_character_index >= 0)) {
|
if (!this->character_data && (this->bb_character_index >= 0)) {
|
||||||
string nsc_filename = this->legacy_player_filename();
|
string nsc_filename = this->legacy_player_filename();
|
||||||
auto nsc_data = phosg::load_object_file<LegacySavedPlayerDataBB>(nsc_filename);
|
if (std::filesystem::is_regular_file(nsc_filename)) {
|
||||||
if (nsc_data.signature == LegacySavedPlayerDataBB::SIGNATURE_V0) {
|
auto nsc_data = phosg::load_object_file<LegacySavedPlayerDataBB>(nsc_filename);
|
||||||
nsc_data.signature = LegacySavedPlayerDataBB::SIGNATURE_V0;
|
if (nsc_data.signature == LegacySavedPlayerDataBB::SIGNATURE_V0) {
|
||||||
nsc_data.unused.clear();
|
nsc_data.signature = LegacySavedPlayerDataBB::SIGNATURE_V0;
|
||||||
nsc_data.battle_records.place_counts.clear(0);
|
nsc_data.unused.clear();
|
||||||
nsc_data.battle_records.disconnect_count = 0;
|
nsc_data.battle_records.place_counts.clear(0);
|
||||||
nsc_data.battle_records.unknown_a1.clear(0);
|
nsc_data.battle_records.disconnect_count = 0;
|
||||||
} else if (nsc_data.signature != LegacySavedPlayerDataBB::SIGNATURE_V1) {
|
nsc_data.battle_records.unknown_a1.clear(0);
|
||||||
throw runtime_error("legacy player data has incorrect signature");
|
} else if (nsc_data.signature != LegacySavedPlayerDataBB::SIGNATURE_V1) {
|
||||||
}
|
throw runtime_error("legacy player data has incorrect signature");
|
||||||
|
}
|
||||||
|
|
||||||
this->character_data = make_shared<PSOBBCharacterFile>();
|
this->character_data = make_shared<PSOBBCharacterFile>();
|
||||||
this->character_data->inventory = nsc_data.inventory;
|
this->character_data->inventory = nsc_data.inventory;
|
||||||
this->character_data->disp = nsc_data.disp;
|
this->character_data->disp = nsc_data.disp;
|
||||||
this->character_data->play_time_seconds = 0;
|
this->character_data->play_time_seconds = 0;
|
||||||
this->character_data->quest_flags = nsc_data.quest_flags;
|
this->character_data->quest_flags = nsc_data.quest_flags;
|
||||||
this->character_data->death_count = nsc_data.death_count;
|
this->character_data->death_count = nsc_data.death_count;
|
||||||
this->character_data->bank = nsc_data.bank;
|
this->character_data->bank = nsc_data.bank;
|
||||||
this->character_data->guild_card.guild_card_number = this->login->account->account_id;
|
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.name = nsc_data.disp.name;
|
||||||
this->character_data->guild_card.description = nsc_data.guild_card_description;
|
this->character_data->guild_card.description = nsc_data.guild_card_description;
|
||||||
this->character_data->guild_card.present = 1;
|
this->character_data->guild_card.present = 1;
|
||||||
this->character_data->guild_card.language = nsc_data.inventory.language;
|
this->character_data->guild_card.language = nsc_data.inventory.language;
|
||||||
this->character_data->guild_card.section_id = nsc_data.disp.visual.section_id;
|
this->character_data->guild_card.section_id = nsc_data.disp.visual.section_id;
|
||||||
this->character_data->guild_card.char_class = nsc_data.disp.visual.char_class;
|
this->character_data->guild_card.char_class = nsc_data.disp.visual.char_class;
|
||||||
this->character_data->auto_reply = nsc_data.auto_reply;
|
this->character_data->auto_reply = nsc_data.auto_reply;
|
||||||
this->character_data->info_board = nsc_data.info_board;
|
this->character_data->info_board = nsc_data.info_board;
|
||||||
this->character_data->battle_records = nsc_data.battle_records;
|
this->character_data->battle_records = nsc_data.battle_records;
|
||||||
this->character_data->challenge_records = nsc_data.challenge_records;
|
this->character_data->challenge_records = nsc_data.challenge_records;
|
||||||
this->character_data->tech_menu_shortcut_entries = nsc_data.tech_menu_shortcut_entries;
|
this->character_data->tech_menu_shortcut_entries = nsc_data.tech_menu_shortcut_entries;
|
||||||
this->character_data->quest_counters = nsc_data.quest_counters;
|
this->character_data->quest_counters = nsc_data.quest_counters;
|
||||||
if (nsa_data) {
|
if (nsa_data) {
|
||||||
this->character_data->option_flags = nsa_data->option_flags;
|
this->character_data->option_flags = nsa_data->option_flags;
|
||||||
this->character_data->symbol_chats = nsa_data->symbol_chats;
|
this->character_data->symbol_chats = nsa_data->symbol_chats;
|
||||||
this->character_data->shortcuts = nsa_data->shortcuts;
|
this->character_data->shortcuts = nsa_data->shortcuts;
|
||||||
this->log.info_f("Loaded legacy player data from {} and {}", nsa_filename, nsc_filename);
|
this->log.info_f("Loaded legacy player data from {} and {}", nsa_filename, nsc_filename);
|
||||||
} else {
|
} else {
|
||||||
this->log.info_f("Loaded legacy player data from {}", nsc_filename);
|
this->log.info_f("Loaded legacy player data from {}", nsc_filename);
|
||||||
|
}
|
||||||
|
this->update_character_data_after_load(this->character_data);
|
||||||
}
|
}
|
||||||
this->update_character_data_after_load(this->character_data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The system and Guild Card files can be auto-created if they can't be
|
||||||
|
// loaded. After this, system_data and guild_card_data are always non-null,
|
||||||
|
// but character_data may still be null
|
||||||
|
if (!this->system_data) {
|
||||||
|
this->system_data = make_shared<PSOBBBaseSystemFile>();
|
||||||
|
auto s = this->require_server_state();
|
||||||
|
if (s->bb_default_keyboard_config) {
|
||||||
|
this->system_data->key_config = *s->bb_default_keyboard_config;
|
||||||
|
}
|
||||||
|
if (s->bb_default_joystick_config) {
|
||||||
|
this->system_data->joystick_config = *s->bb_default_joystick_config;
|
||||||
|
}
|
||||||
|
this->log.info_f("Created new system data");
|
||||||
|
}
|
||||||
|
if (!this->guild_card_data) {
|
||||||
|
this->guild_card_data = make_shared<PSOBBGuildCardFile>();
|
||||||
|
this->log.info_f("Created new Guild Card data");
|
||||||
|
}
|
||||||
|
|
||||||
auto s = this->require_server_state();
|
auto s = this->require_server_state();
|
||||||
auto stack_limits = s->item_stack_limits(this->version());
|
auto stack_limits = s->item_stack_limits(this->version());
|
||||||
|
|
||||||
if (this->bb_character_index >= 0) {
|
|
||||||
// bank_file() loads the bank data
|
|
||||||
this->bank_file()->enforce_stack_limits(stack_limits);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->blocked_senders.clear();
|
this->blocked_senders.clear();
|
||||||
for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) {
|
for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) {
|
||||||
if (this->guild_card_data->blocked[z].present) {
|
if (this->guild_card_data->blocked[z].present) {
|
||||||
@@ -1011,20 +1018,23 @@ void Client::load_all_files() {
|
|||||||
if (this->character_data) {
|
if (this->character_data) {
|
||||||
// Clear legacy play_time field
|
// Clear legacy play_time field
|
||||||
this->character_data->disp.name.clear_after_bytes(0x18);
|
this->character_data->disp.name.clear_after_bytes(0x18);
|
||||||
|
|
||||||
this->character_data->inventory.enforce_stack_limits(stack_limits);
|
this->character_data->inventory.enforce_stack_limits(stack_limits);
|
||||||
|
|
||||||
this->login->account->auto_reply_message = this->character_data->auto_reply.decode();
|
this->login->account->auto_reply_message = this->character_data->auto_reply.decode();
|
||||||
this->login->account->save();
|
this->login->account->save();
|
||||||
this->last_play_time_update = phosg::now();
|
this->last_play_time_update = phosg::now();
|
||||||
|
if (this->bb_character_index >= 0) {
|
||||||
|
// Note that bank_file() can't recur infinitely here because
|
||||||
|
// character_file is already set; it will not call load_all_files() again
|
||||||
|
this->bank_file()->enforce_stack_limits(stack_limits);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Client::update_character_data_after_load(shared_ptr<PSOBBCharacterFile> charfile) {
|
void Client::update_character_data_after_load(shared_ptr<PSOBBCharacterFile> charfile) {
|
||||||
charfile->import_tethealla_material_usage(this->require_server_state()->level_table(this->version()));
|
charfile->import_tethealla_material_usage(this->require_server_state()->level_table(this->version()));
|
||||||
|
|
||||||
uint8_t lang = this->language();
|
Language lang = this->language();
|
||||||
this->log.info_f("Overriding language fields in save files with {:02X} ({})", lang, char_for_language_code(lang));
|
this->log.info_f("Overriding language fields in save files with {}", name_for_language(lang));
|
||||||
charfile->inventory.language = lang;
|
charfile->inventory.language = lang;
|
||||||
charfile->guild_card.language = lang;
|
charfile->guild_card.language = lang;
|
||||||
}
|
}
|
||||||
@@ -1061,13 +1071,17 @@ shared_ptr<PSOGCEp3CharacterFile::Character> Client::load_ep3_backup_character(u
|
|||||||
return ch;
|
return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Client::save_and_unload_character() {
|
void Client::unload_character(bool save) {
|
||||||
if (this->character_data) {
|
if (this->character_data) {
|
||||||
this->save_character_file();
|
if (save) {
|
||||||
|
this->save_character_file();
|
||||||
|
}
|
||||||
this->character_data.reset();
|
this->character_data.reset();
|
||||||
this->log.info_f("Unloaded character");
|
this->log.info_f("Unloaded character");
|
||||||
if (this->bank_data) {
|
if (this->bank_data) {
|
||||||
this->save_bank_file();
|
if (save) {
|
||||||
|
this->save_bank_file();
|
||||||
|
}
|
||||||
this->bank_data.reset();
|
this->bank_data.reset();
|
||||||
this->log.info_f("Unloaded bank");
|
this->log.info_f("Unloaded bank");
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -149,7 +149,7 @@ public:
|
|||||||
uint8_t override_lobby_number = 0x80; // 80 = no override
|
uint8_t override_lobby_number = 0x80; // 80 = no override
|
||||||
int64_t override_random_seed = -1;
|
int64_t override_random_seed = -1;
|
||||||
std::unique_ptr<Variations> override_variations;
|
std::unique_ptr<Variations> override_variations;
|
||||||
VectorXZF pos;
|
VectorXYZF pos;
|
||||||
uint32_t floor = 0x0F;
|
uint32_t floor = 0x0F;
|
||||||
std::weak_ptr<Lobby> lobby;
|
std::weak_ptr<Lobby> lobby;
|
||||||
uint8_t lobby_client_id = 0;
|
uint8_t lobby_client_id = 0;
|
||||||
@@ -228,7 +228,7 @@ public:
|
|||||||
inline Version version() const {
|
inline Version version() const {
|
||||||
return this->channel->version;
|
return this->channel->version;
|
||||||
}
|
}
|
||||||
inline uint8_t language() const {
|
inline Language language() const {
|
||||||
return this->channel->language;
|
return this->channel->language;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,21 +267,21 @@ public:
|
|||||||
std::shared_ptr<const IntegralExpression> expr,
|
std::shared_ptr<const IntegralExpression> expr,
|
||||||
std::shared_ptr<const Lobby> game,
|
std::shared_ptr<const Lobby> game,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
size_t num_players,
|
size_t num_players,
|
||||||
bool v1_present) const;
|
bool v1_present) const;
|
||||||
bool can_see_quest(
|
bool can_see_quest(
|
||||||
std::shared_ptr<const Quest> q,
|
std::shared_ptr<const Quest> q,
|
||||||
std::shared_ptr<const Lobby> game,
|
std::shared_ptr<const Lobby> game,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
size_t num_players,
|
size_t num_players,
|
||||||
bool v1_present) const;
|
bool v1_present) const;
|
||||||
bool can_play_quest(
|
bool can_play_quest(
|
||||||
std::shared_ptr<const Quest> q,
|
std::shared_ptr<const Quest> q,
|
||||||
std::shared_ptr<const Lobby> game,
|
std::shared_ptr<const Lobby> game,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
size_t num_players,
|
size_t num_players,
|
||||||
bool v1_present) const;
|
bool v1_present) const;
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ public:
|
|||||||
void save_character_file();
|
void save_character_file();
|
||||||
void create_character_file(
|
void create_character_file(
|
||||||
uint32_t guild_card_number,
|
uint32_t guild_card_number,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const PlayerDispDataBBPreview& preview,
|
const PlayerDispDataBBPreview& preview,
|
||||||
std::shared_ptr<const LevelTable> level_table);
|
std::shared_ptr<const LevelTable> level_table);
|
||||||
void create_battle_overlay(std::shared_ptr<const BattleRules> rules, std::shared_ptr<const LevelTable> level_table);
|
void create_battle_overlay(std::shared_ptr<const BattleRules> rules, std::shared_ptr<const LevelTable> level_table);
|
||||||
@@ -343,7 +343,7 @@ public:
|
|||||||
|
|
||||||
void load_backup_character(uint32_t account_id, size_t index);
|
void load_backup_character(uint32_t account_id, size_t index);
|
||||||
std::shared_ptr<PSOGCEp3CharacterFile::Character> load_ep3_backup_character(uint32_t account_id, size_t index);
|
std::shared_ptr<PSOGCEp3CharacterFile::Character> load_ep3_backup_character(uint32_t account_id, size_t index);
|
||||||
void save_and_unload_character();
|
void unload_character(bool save);
|
||||||
|
|
||||||
void print_inventory() const;
|
void print_inventory() const;
|
||||||
void print_bank() const;
|
void print_bank() const;
|
||||||
|
|||||||
+175
-112
@@ -190,17 +190,17 @@ struct S_OpenFile_Patch_06 {
|
|||||||
|
|
||||||
// 07 (S->C): Write file
|
// 07 (S->C): Write file
|
||||||
// The client's handler table says this command's maximum size is 0x6010
|
// The client's handler table says this command's maximum size is 0x6010
|
||||||
// including the header, but the only servers I've seen use this command limit
|
// including the header, but the command may be shorter if the server chooses
|
||||||
// chunks to 0x4010 (including the header). Unlike the game server's 13 and A7
|
// to use a shorter chunk size. Unlike the game server's 13 and A7 commands,
|
||||||
// commands, the chunks do not need to be the same size - the game opens the
|
// the chunks do not need to be the same size - the game opens the file with
|
||||||
// file with the "a+b" mode each time it is written, so the new data is always
|
// the "a+b" mode each time it is written, so the new data is always appended
|
||||||
// appended to the end.
|
// to the end.
|
||||||
|
|
||||||
struct S_WriteFileHeader_Patch_07 {
|
struct S_WriteFileHeader_Patch_07 {
|
||||||
le_uint32_t chunk_index = 0;
|
le_uint32_t chunk_index = 0;
|
||||||
le_uint32_t chunk_checksum = 0; // CRC32 of the following chunk data
|
le_uint32_t chunk_checksum = 0; // CRC32 of the following chunk data
|
||||||
le_uint32_t chunk_size = 0;
|
le_uint32_t chunk_size = 0;
|
||||||
// The chunk data immediately follows here
|
// The chunk's data immediately follows here
|
||||||
} __packed_ws__(S_WriteFileHeader_Patch_07, 0x0C);
|
} __packed_ws__(S_WriteFileHeader_Patch_07, 0x0C);
|
||||||
|
|
||||||
// 08 (S->C): Close current file
|
// 08 (S->C): Close current file
|
||||||
@@ -240,8 +240,8 @@ struct S_FileChecksumRequest_Patch_0C {
|
|||||||
|
|
||||||
struct C_FileInformation_Patch_0F {
|
struct C_FileInformation_Patch_0F {
|
||||||
le_uint32_t request_id = 0; // Matches request_id from an earlier 0C command
|
le_uint32_t request_id = 0; // Matches request_id from an earlier 0C command
|
||||||
le_uint32_t checksum = 0; // CRC32 of the file's data
|
le_uint32_t checksum = 0; // CRC32 of the file's data (0 if file not found)
|
||||||
le_uint32_t size = 0;
|
le_uint32_t size = 0; // 0 if file not found
|
||||||
} __packed_ws__(C_FileInformation_Patch_0F, 0x0C);
|
} __packed_ws__(C_FileInformation_Patch_0F, 0x0C);
|
||||||
|
|
||||||
// 10 (C->S): End of file information command list
|
// 10 (C->S): End of file information command list
|
||||||
@@ -349,7 +349,7 @@ struct C_LegacyLogin_PC_V3_03 {
|
|||||||
/* 00 */ be_uint64_t hardware_id;
|
/* 00 */ be_uint64_t hardware_id;
|
||||||
/* 08 */ le_uint32_t sub_version = 0;
|
/* 08 */ le_uint32_t sub_version = 0;
|
||||||
/* 0C */ uint8_t is_extended = 0;
|
/* 0C */ uint8_t is_extended = 0;
|
||||||
/* 0D */ uint8_t language = 0;
|
/* 0D */ Language language = Language::JAPANESE;
|
||||||
/* 0E */ le_uint16_t unused = 0;
|
/* 0E */ le_uint16_t unused = 0;
|
||||||
// Note: These are suffixed with 2 since they come from the same source data
|
// Note: These are suffixed with 2 since they come from the same source data
|
||||||
// as the corresponding fields in 9D/9E. (Even though serial_number and
|
// as the corresponding fields in 9D/9E. (Even though serial_number and
|
||||||
@@ -404,7 +404,7 @@ struct C_LegacyLogin_PC_V3_04 {
|
|||||||
/* 00 */ be_uint64_t hardware_id;
|
/* 00 */ be_uint64_t hardware_id;
|
||||||
/* 08 */ le_uint32_t sub_version = 0;
|
/* 08 */ le_uint32_t sub_version = 0;
|
||||||
/* 0C */ uint8_t is_extended = 0;
|
/* 0C */ uint8_t is_extended = 0;
|
||||||
/* 0D */ uint8_t language = 0;
|
/* 0D */ Language language = Language::JAPANESE;
|
||||||
/* 0E */ le_uint16_t unused = 0;
|
/* 0E */ le_uint16_t unused = 0;
|
||||||
/* 10 */ pstring<TextEncoding::ASCII, 0x10> serial_number;
|
/* 10 */ pstring<TextEncoding::ASCII, 0x10> serial_number;
|
||||||
/* 20 */ pstring<TextEncoding::ASCII, 0x10> access_key;
|
/* 20 */ pstring<TextEncoding::ASCII, 0x10> access_key;
|
||||||
@@ -414,7 +414,7 @@ struct C_LegacyLogin_PC_V3_04 {
|
|||||||
struct C_LegacyLogin_BB_04 {
|
struct C_LegacyLogin_BB_04 {
|
||||||
/* 00 */ le_uint32_t sub_version = 0;
|
/* 00 */ le_uint32_t sub_version = 0;
|
||||||
/* 04 */ uint8_t is_extended = 0;
|
/* 04 */ uint8_t is_extended = 0;
|
||||||
/* 05 */ uint8_t language = 0;
|
/* 05 */ Language language = Language::JAPANESE;
|
||||||
/* 06 */ le_uint16_t unused = 0;
|
/* 06 */ le_uint16_t unused = 0;
|
||||||
/* 08 */ pstring<TextEncoding::ASCII, 0x10> username;
|
/* 08 */ pstring<TextEncoding::ASCII, 0x10> username;
|
||||||
/* 18 */ pstring<TextEncoding::ASCII, 0x10> password;
|
/* 18 */ pstring<TextEncoding::ASCII, 0x10> password;
|
||||||
@@ -1265,6 +1265,12 @@ struct C_CharacterData_BB_61_98 {
|
|||||||
// disp or inventory data. The clients in the game are responsible for sending
|
// disp or inventory data. The clients in the game are responsible for sending
|
||||||
// that data to each other during the join process with 60/62/6C/6D commands.
|
// that data to each other during the join process with 60/62/6C/6D commands.
|
||||||
|
|
||||||
|
// After receiving a 64 command, the client starts the game loading procedure,
|
||||||
|
// during which it will completely ignore other 64 or 65 commands, and will
|
||||||
|
// delay processing of all other commands except 1D until the loading procedure
|
||||||
|
// is done. If more than 0x10000 bytes of commands are sent during loading, any
|
||||||
|
// commands that don't fit in the buffer are lost.
|
||||||
|
|
||||||
// Curiously, this command is named RcvStartGame3 internally, while 0E is named
|
// Curiously, this command is named RcvStartGame3 internally, while 0E is named
|
||||||
// RcvStartGame. The string RcvStartGame2 appears in the DC versions, but it
|
// RcvStartGame. The string RcvStartGame2 appears in the DC versions, but it
|
||||||
// seems the relevant code was deleted - there are no references to the string.
|
// seems the relevant code was deleted - there are no references to the string.
|
||||||
@@ -1286,7 +1292,7 @@ struct S_JoinGameT_DC_PC {
|
|||||||
/* 0104 */ uint8_t client_id = 0;
|
/* 0104 */ uint8_t client_id = 0;
|
||||||
/* 0105 */ uint8_t leader_id = 0;
|
/* 0105 */ uint8_t leader_id = 0;
|
||||||
/* 0106 */ uint8_t disable_udp = 1;
|
/* 0106 */ uint8_t disable_udp = 1;
|
||||||
/* 0107 */ uint8_t difficulty = 0;
|
/* 0107 */ Difficulty difficulty = Difficulty::NORMAL;
|
||||||
/* 0108 */ uint8_t battle_mode = 0;
|
/* 0108 */ uint8_t battle_mode = 0;
|
||||||
/* 0109 */ uint8_t event = 0;
|
/* 0109 */ uint8_t event = 0;
|
||||||
/* 010A */ uint8_t section_id = 0;
|
/* 010A */ uint8_t section_id = 0;
|
||||||
@@ -1357,6 +1363,9 @@ struct S_JoinGame_BB_64 : S_JoinGameT_DC_PC<PlayerLobbyDataBB> {
|
|||||||
// command (described above), and the players already in the game receive a 65
|
// command (described above), and the players already in the game receive a 65
|
||||||
// command containing only the joining player's data.
|
// command containing only the joining player's data.
|
||||||
|
|
||||||
|
// Similarly to 64, the client will ignore 64 and 65 commands while loading,
|
||||||
|
// and will buffer all other commands except 1D until loading is done.
|
||||||
|
|
||||||
struct LobbyFlags_DCNTE {
|
struct LobbyFlags_DCNTE {
|
||||||
uint8_t client_id = 0;
|
uint8_t client_id = 0;
|
||||||
uint8_t leader_id = 0;
|
uint8_t leader_id = 0;
|
||||||
@@ -1687,7 +1696,7 @@ struct C_Login_DCNTE_8B {
|
|||||||
be_uint64_t hardware_id;
|
be_uint64_t hardware_id;
|
||||||
le_uint32_t sub_version = 0x20;
|
le_uint32_t sub_version = 0x20;
|
||||||
uint8_t is_extended = 0;
|
uint8_t is_extended = 0;
|
||||||
uint8_t language = 0;
|
Language language = Language::JAPANESE;
|
||||||
parray<uint8_t, 2> unused1;
|
parray<uint8_t, 2> unused1;
|
||||||
pstring<TextEncoding::ASCII, 0x11> serial_number;
|
pstring<TextEncoding::ASCII, 0x11> serial_number;
|
||||||
pstring<TextEncoding::ASCII, 0x11> access_key;
|
pstring<TextEncoding::ASCII, 0x11> access_key;
|
||||||
@@ -1748,7 +1757,7 @@ struct C_RegisterV1_DC_92 {
|
|||||||
be_uint64_t hardware_id;
|
be_uint64_t hardware_id;
|
||||||
le_uint32_t sub_version;
|
le_uint32_t sub_version;
|
||||||
uint8_t unused1 = 0;
|
uint8_t unused1 = 0;
|
||||||
uint8_t language = 0;
|
Language language = Language::JAPANESE;
|
||||||
parray<uint8_t, 2> unused2;
|
parray<uint8_t, 2> unused2;
|
||||||
pstring<TextEncoding::ASCII, 0x30> serial_number2;
|
pstring<TextEncoding::ASCII, 0x30> serial_number2;
|
||||||
pstring<TextEncoding::ASCII, 0x30> access_key2;
|
pstring<TextEncoding::ASCII, 0x30> access_key2;
|
||||||
@@ -1767,7 +1776,7 @@ struct C_LoginV1_DC_93 {
|
|||||||
/* 08 */ be_uint64_t hardware_id;
|
/* 08 */ be_uint64_t hardware_id;
|
||||||
/* 10 */ le_uint32_t sub_version = 0;
|
/* 10 */ le_uint32_t sub_version = 0;
|
||||||
/* 14 */ uint8_t is_extended = 0;
|
/* 14 */ uint8_t is_extended = 0;
|
||||||
/* 15 */ uint8_t language = 0;
|
/* 15 */ Language language = Language::JAPANESE;
|
||||||
/* 16 */ parray<uint8_t, 2> unused1;
|
/* 16 */ parray<uint8_t, 2> unused1;
|
||||||
/* 18 */ pstring<TextEncoding::ASCII, 0x11> serial_number;
|
/* 18 */ pstring<TextEncoding::ASCII, 0x11> serial_number;
|
||||||
/* 29 */ pstring<TextEncoding::ASCII, 0x11> access_key;
|
/* 29 */ pstring<TextEncoding::ASCII, 0x11> access_key;
|
||||||
@@ -1788,7 +1797,7 @@ struct C_LoginBase_BB_93 {
|
|||||||
/* 00 */ le_uint32_t player_tag = 0x00010000;
|
/* 00 */ le_uint32_t player_tag = 0x00010000;
|
||||||
/* 04 */ le_uint32_t guild_card_number = 0;
|
/* 04 */ le_uint32_t guild_card_number = 0;
|
||||||
/* 08 */ le_uint32_t sub_version = 0;
|
/* 08 */ le_uint32_t sub_version = 0;
|
||||||
/* 0C */ uint8_t language = 0;
|
/* 0C */ Language language = Language::JAPANESE;
|
||||||
/* 0D */ int8_t character_slot = 0;
|
/* 0D */ int8_t character_slot = 0;
|
||||||
// Values for connection_phase:
|
// Values for connection_phase:
|
||||||
// 00 - initial connection (client will request system file, characters, etc.)
|
// 00 - initial connection (client will request system file, characters, etc.)
|
||||||
@@ -1882,6 +1891,10 @@ struct C_CharSaveInfo_DCv2_PC_V3_BB_96 {
|
|||||||
// NOT assign it to an available lobby. The client will send an 84 when it's
|
// NOT assign it to an available lobby. The client will send an 84 when it's
|
||||||
// ready to join a lobby.
|
// ready to join a lobby.
|
||||||
|
|
||||||
|
// Similarly to 64 and 65, the client will ignore 64 and 65 commands while
|
||||||
|
// loading the lobby after sending 98, and will buffer all other commands
|
||||||
|
// except 1D until loading is done.
|
||||||
|
|
||||||
// 99 (C->S): Server time accepted
|
// 99 (C->S): Server time accepted
|
||||||
// Internal name: SndPsoDirList
|
// Internal name: SndPsoDirList
|
||||||
// No arguments
|
// No arguments
|
||||||
@@ -1952,7 +1965,7 @@ struct C_Register_DC_PC_V3_9C {
|
|||||||
/* 00 */ be_uint64_t hardware_id;
|
/* 00 */ be_uint64_t hardware_id;
|
||||||
/* 08 */ le_uint32_t sub_version = 0;
|
/* 08 */ le_uint32_t sub_version = 0;
|
||||||
/* 0C */ uint8_t unused1 = 0;
|
/* 0C */ uint8_t unused1 = 0;
|
||||||
/* 0D */ uint8_t language = 0;
|
/* 0D */ Language language = Language::JAPANESE;
|
||||||
/* 0E */ parray<uint8_t, 2> unused2;
|
/* 0E */ parray<uint8_t, 2> unused2;
|
||||||
/* 10 */ pstring<TextEncoding::ASCII, 0x30> serial_number; // On XB, this is the XBL gamertag
|
/* 10 */ pstring<TextEncoding::ASCII, 0x30> serial_number; // On XB, this is the XBL gamertag
|
||||||
/* 40 */ pstring<TextEncoding::ASCII, 0x30> access_key; // On XB, this is the XBL user ID
|
/* 40 */ pstring<TextEncoding::ASCII, 0x30> access_key; // On XB, this is the XBL user ID
|
||||||
@@ -1963,7 +1976,7 @@ struct C_Register_DC_PC_V3_9C {
|
|||||||
struct C_Register_BB_9C {
|
struct C_Register_BB_9C {
|
||||||
le_uint32_t sub_version = 0;
|
le_uint32_t sub_version = 0;
|
||||||
uint8_t unused1 = 0;
|
uint8_t unused1 = 0;
|
||||||
uint8_t language = 0;
|
Language language = Language::JAPANESE;
|
||||||
parray<uint8_t, 2> unused2;
|
parray<uint8_t, 2> unused2;
|
||||||
pstring<TextEncoding::ASCII, 0x30> username;
|
pstring<TextEncoding::ASCII, 0x30> username;
|
||||||
pstring<TextEncoding::ASCII, 0x30> password;
|
pstring<TextEncoding::ASCII, 0x30> password;
|
||||||
@@ -2002,7 +2015,7 @@ struct C_Login_DC_PC_GC_9D {
|
|||||||
/* 08 */ be_uint64_t hardware_id;
|
/* 08 */ be_uint64_t hardware_id;
|
||||||
/* 10 */ le_uint32_t sub_version = 0;
|
/* 10 */ le_uint32_t sub_version = 0;
|
||||||
/* 14 */ uint8_t is_extended = 0; // If 1, structure has extended format
|
/* 14 */ uint8_t is_extended = 0; // If 1, structure has extended format
|
||||||
/* 15 */ uint8_t language = 0; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES
|
/* 15 */ Language language = Language::JAPANESE; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES
|
||||||
/* 16 */ parray<uint8_t, 0x2> unused3; // Always zeroes
|
/* 16 */ parray<uint8_t, 0x2> unused3; // Always zeroes
|
||||||
/* 18 */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
|
/* 18 */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
|
||||||
/* 28 */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
|
/* 28 */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
|
||||||
@@ -2062,7 +2075,7 @@ struct C_LoginExtended_BB_9E {
|
|||||||
/* 0000 */ le_uint32_t player_tag = 0x00010000;
|
/* 0000 */ le_uint32_t player_tag = 0x00010000;
|
||||||
/* 0004 */ le_uint32_t guild_card_number = 0; // == account_id when on newserv
|
/* 0004 */ le_uint32_t guild_card_number = 0; // == account_id when on newserv
|
||||||
/* 0008 */ le_uint32_t sub_version = 0;
|
/* 0008 */ le_uint32_t sub_version = 0;
|
||||||
/* 000C */ le_uint32_t language = 0;
|
/* 000C */ le_uint32_t language32 = 0;
|
||||||
/* 0010 */ le_uint32_t unknown_a2 = 0;
|
/* 0010 */ le_uint32_t unknown_a2 = 0;
|
||||||
/* 0014 */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number; // Always blank?
|
/* 0014 */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number; // Always blank?
|
||||||
/* 0024 */ pstring<TextEncoding::ASCII, 0x10> v1_access_key; // == "?"
|
/* 0024 */ pstring<TextEncoding::ASCII, 0x10> v1_access_key; // == "?"
|
||||||
@@ -2570,7 +2583,7 @@ check_struct_size(C_CreateGame_DCNTE, 0x28);
|
|||||||
|
|
||||||
template <TextEncoding Encoding>
|
template <TextEncoding Encoding>
|
||||||
struct C_CreateGameT : C_CreateGameBaseT<Encoding> {
|
struct C_CreateGameT : C_CreateGameBaseT<Encoding> {
|
||||||
uint8_t difficulty = 0; // 0-3 (always 0 on Episode 3)
|
Difficulty difficulty = Difficulty::NORMAL; // Always NORMAL on Episode 3
|
||||||
uint8_t battle_mode = 0; // 0 or 1 (always 0 on Episode 3)
|
uint8_t battle_mode = 0; // 0 or 1 (always 0 on Episode 3)
|
||||||
// Note: Episode 3 uses the challenge mode flag for view battle permissions.
|
// Note: Episode 3 uses the challenge mode flag for view battle permissions.
|
||||||
// 0 = view battle allowed; 1 = not allowed
|
// 0 = view battle allowed; 1 = not allowed
|
||||||
@@ -2915,7 +2928,7 @@ struct C_SetChallengeModeCharacterTemplate_BB_02DF {
|
|||||||
|
|
||||||
struct C_SetChallengeModeDifficulty_BB_03DF {
|
struct C_SetChallengeModeDifficulty_BB_03DF {
|
||||||
// No existing challenge mode quest sets this to a value other than zero.
|
// No existing challenge mode quest sets this to a value other than zero.
|
||||||
le_uint32_t difficulty = 0;
|
le_uint32_t difficulty32 = 0;
|
||||||
} __packed_ws__(C_SetChallengeModeDifficulty_BB_03DF, 4);
|
} __packed_ws__(C_SetChallengeModeDifficulty_BB_03DF, 4);
|
||||||
|
|
||||||
struct C_SetChallengeModeEXPMultiplier_BB_04DF {
|
struct C_SetChallengeModeEXPMultiplier_BB_04DF {
|
||||||
@@ -3324,7 +3337,7 @@ struct S_JoinSpectatorTeam_Ep3_E8 {
|
|||||||
/* 1170 */ uint8_t client_id = 0;
|
/* 1170 */ uint8_t client_id = 0;
|
||||||
/* 1171 */ uint8_t leader_id = 0;
|
/* 1171 */ uint8_t leader_id = 0;
|
||||||
/* 1172 */ uint8_t disable_udp = 1;
|
/* 1172 */ uint8_t disable_udp = 1;
|
||||||
/* 1173 */ uint8_t difficulty = 0;
|
/* 1173 */ Difficulty difficulty = Difficulty::NORMAL;
|
||||||
/* 1174 */ uint8_t battle_mode = 0;
|
/* 1174 */ uint8_t battle_mode = 0;
|
||||||
/* 1175 */ uint8_t event = 0;
|
/* 1175 */ uint8_t event = 0;
|
||||||
/* 1176 */ uint8_t section_id = 0;
|
/* 1176 */ uint8_t section_id = 0;
|
||||||
@@ -4052,7 +4065,7 @@ struct G_SendGuildCard_BB_6x06 {
|
|||||||
} __packed_ws__(G_SendGuildCard_BB_6x06, 0x10C);
|
} __packed_ws__(G_SendGuildCard_BB_6x06, 0x10C);
|
||||||
|
|
||||||
// 6x07: Symbol chat
|
// 6x07: Symbol chat
|
||||||
// If UDP mode is enabled, this command is sent via UDP.
|
// If UDP is enabled, this command is sent via UDP.
|
||||||
|
|
||||||
struct G_SymbolChat_6x07 {
|
struct G_SymbolChat_6x07 {
|
||||||
G_UnusedHeader header;
|
G_UnusedHeader header;
|
||||||
@@ -4069,8 +4082,10 @@ struct G_SymbolChat_6x07 {
|
|||||||
// equal to 0x1000, so any valid enemy ID would be far outside the array's
|
// equal to 0x1000, so any valid enemy ID would be far outside the array's
|
||||||
// range. newserv unconditionally blocks this command because it appears never
|
// range. newserv unconditionally blocks this command because it appears never
|
||||||
// to be used, and the array write is not bounds-checked, so it could be used
|
// to be used, and the array write is not bounds-checked, so it could be used
|
||||||
// to cause undefined behavior on other clients. It seems that this broken
|
// to cause undefined behavior on other clients. It seems that this logic
|
||||||
// logic predates even DC NTE.
|
// predates even DC NTE; it's likely that this was part of the implementation
|
||||||
|
// of enemy states before entity IDs and the standard 0xB50-entry array of
|
||||||
|
// states were introduced.
|
||||||
|
|
||||||
struct G_LegacyKillEnemy_6x09 {
|
struct G_LegacyKillEnemy_6x09 {
|
||||||
G_EntityIDHeader header;
|
G_EntityIDHeader header;
|
||||||
@@ -4208,8 +4223,7 @@ struct G_VolOptBossActions_6x16 {
|
|||||||
|
|
||||||
// 6x17: Set entity position and angle (not valid on Episode 3)
|
// 6x17: Set entity position and angle (not valid on Episode 3)
|
||||||
// This command sets an entity's position and angle without performing any
|
// This command sets an entity's position and angle without performing any
|
||||||
// validity checks, even on v3 and later. We unconditionally block this if it
|
// validity checks, even on v3 and later.
|
||||||
// affects a player other than the sender.
|
|
||||||
|
|
||||||
struct G_SetEntityPositionAndAngle_6x17 {
|
struct G_SetEntityPositionAndAngle_6x17 {
|
||||||
G_EntityIDHeader header;
|
G_EntityIDHeader header;
|
||||||
@@ -4236,13 +4250,14 @@ struct G_DarkFalzActions_6x19 {
|
|||||||
|
|
||||||
// 6x1A: Invalid subcommand
|
// 6x1A: Invalid subcommand
|
||||||
|
|
||||||
// 6x1B: Enable PK mode for player (not valid on Episode 3) (protected on V3/V4)
|
// 6x1B: Enable PK mode for player (not valid on Episode 3) (protected on GC
|
||||||
|
// NTE/V3/V4)
|
||||||
|
|
||||||
struct G_EnablePKModeForPlayer_6x1B {
|
struct G_EnablePKModeForPlayer_6x1B {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
} __packed_ws__(G_EnablePKModeForPlayer_6x1B, 4);
|
} __packed_ws__(G_EnablePKModeForPlayer_6x1B, 4);
|
||||||
|
|
||||||
// 6x1C: Disable PK mode for player (protected on V3/V4)
|
// 6x1C: Disable PK mode for player (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_DisablePKModeForPlayer_6x1C {
|
struct G_DisablePKModeForPlayer_6x1C {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4322,7 +4337,7 @@ struct G_TeleportPlayer_6x24 {
|
|||||||
VectorXYZF pos;
|
VectorXYZF pos;
|
||||||
} __packed_ws__(G_TeleportPlayer_6x24, 0x14);
|
} __packed_ws__(G_TeleportPlayer_6x24, 0x14);
|
||||||
|
|
||||||
// 6x25: Equip item (protected on V3/V4)
|
// 6x25: Equip item (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_EquipItem_6x25 {
|
struct G_EquipItem_6x25 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4331,7 +4346,7 @@ struct G_EquipItem_6x25 {
|
|||||||
le_uint32_t equip_slot = 0;
|
le_uint32_t equip_slot = 0;
|
||||||
} __packed_ws__(G_EquipItem_6x25, 0x0C);
|
} __packed_ws__(G_EquipItem_6x25, 0x0C);
|
||||||
|
|
||||||
// 6x26: Unequip item (protected on V3/V4)
|
// 6x26: Unequip item (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_UnequipItem_6x26 {
|
struct G_UnequipItem_6x26 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4339,14 +4354,14 @@ struct G_UnequipItem_6x26 {
|
|||||||
le_uint32_t unused = 0;
|
le_uint32_t unused = 0;
|
||||||
} __packed_ws__(G_UnequipItem_6x26, 0x0C);
|
} __packed_ws__(G_UnequipItem_6x26, 0x0C);
|
||||||
|
|
||||||
// 6x27: Use item (protected on V3/V4)
|
// 6x27: Use item (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_UseItem_6x27 {
|
struct G_UseItem_6x27 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
le_uint32_t item_id = 0;
|
le_uint32_t item_id = 0;
|
||||||
} __packed_ws__(G_UseItem_6x27, 8);
|
} __packed_ws__(G_UseItem_6x27, 8);
|
||||||
|
|
||||||
// 6x28: Feed MAG (protected on V3/V4)
|
// 6x28: Feed MAG (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_FeedMag_6x28 {
|
struct G_FeedMag_6x28 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4355,7 +4370,7 @@ struct G_FeedMag_6x28 {
|
|||||||
} __packed_ws__(G_FeedMag_6x28, 0x0C);
|
} __packed_ws__(G_FeedMag_6x28, 0x0C);
|
||||||
|
|
||||||
// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG)
|
// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG)
|
||||||
// (protected on V3 but not on V4)
|
// (protected on GC NTE/V3 but not on V4)
|
||||||
// This subcommand is also used for reducing the size of stacks - if amount is
|
// This subcommand is also used for reducing the size of stacks - if amount is
|
||||||
// less than the stack count, the item is not deleted and its ID remains valid.
|
// less than the stack count, the item is not deleted and its ID remains valid.
|
||||||
|
|
||||||
@@ -4365,7 +4380,7 @@ struct G_DeleteInventoryItem_6x29 {
|
|||||||
le_uint32_t amount = 0;
|
le_uint32_t amount = 0;
|
||||||
} __packed_ws__(G_DeleteInventoryItem_6x29, 0x0C);
|
} __packed_ws__(G_DeleteInventoryItem_6x29, 0x0C);
|
||||||
|
|
||||||
// 6x2A: Drop item (protected on V3/V4)
|
// 6x2A: Drop item (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_DropItem_6x2A {
|
struct G_DropItem_6x2A {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4375,7 +4390,7 @@ struct G_DropItem_6x2A {
|
|||||||
VectorXYZF pos;
|
VectorXYZF pos;
|
||||||
} __packed_ws__(G_DropItem_6x2A, 0x18);
|
} __packed_ws__(G_DropItem_6x2A, 0x18);
|
||||||
|
|
||||||
// 6x2B: Create item in inventory (tekker/bank) (protected on V3/V4)
|
// 6x2B: Create item in inventory (tekker/bank) (protected on GC NTE/V3/V4)
|
||||||
// On BB, the 6xBE command is used instead of 6x2B to create inventory items.
|
// On BB, the 6xBE command is used instead of 6x2B to create inventory items.
|
||||||
// If equip_item is nonzero, the item is equipped immediately.
|
// If equip_item is nonzero, the item is equipped immediately.
|
||||||
|
|
||||||
@@ -4390,7 +4405,7 @@ struct G_CreateInventoryItem_PC_V3_BB_6x2B : G_CreateInventoryItem_DC_6x2B {
|
|||||||
parray<uint8_t, 2> unused2 = 0;
|
parray<uint8_t, 2> unused2 = 0;
|
||||||
} __packed_ws__(G_CreateInventoryItem_PC_V3_BB_6x2B, 0x1C);
|
} __packed_ws__(G_CreateInventoryItem_PC_V3_BB_6x2B, 0x1C);
|
||||||
|
|
||||||
// 6x2C: Impose hold (protected on V3/V4)
|
// 6x2C: Impose hold (protected on GC NTE/V3/V4)
|
||||||
// This updates PlayerHoldState in the TObjPlayer struct, but the format is not
|
// This updates PlayerHoldState in the TObjPlayer struct, but the format is not
|
||||||
// the same. The names here match the fields in PlayerHoldState. A player hold
|
// the same. The names here match the fields in PlayerHoldState. A player hold
|
||||||
// prevents the player from moving further than the trigger radius from the
|
// prevents the player from moving further than the trigger radius from the
|
||||||
@@ -4405,13 +4420,13 @@ struct G_ImposeHold_6x2C {
|
|||||||
le_float trigger_radius2 = 0.0f; // "2" here means "squared"
|
le_float trigger_radius2 = 0.0f; // "2" here means "squared"
|
||||||
} __packed_ws__(G_ImposeHold_6x2C, 0x14);
|
} __packed_ws__(G_ImposeHold_6x2C, 0x14);
|
||||||
|
|
||||||
// 6x2D: Release hold (protected on V3/V4)
|
// 6x2D: Release hold (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_ReleaseHold_6x2D {
|
struct G_ReleaseHold_6x2D {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
} __packed_ws__(G_ReleaseHold_6x2D, 4);
|
} __packed_ws__(G_ReleaseHold_6x2D, 4);
|
||||||
|
|
||||||
// 6x2E: Set and/or clear player flags (protected on V3/V4)
|
// 6x2E: Set and/or clear player flags (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_SetOrClearPlayerFlags_6x2E {
|
struct G_SetOrClearPlayerFlags_6x2E {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4419,7 +4434,7 @@ struct G_SetOrClearPlayerFlags_6x2E {
|
|||||||
le_uint32_t or_mask = 0;
|
le_uint32_t or_mask = 0;
|
||||||
} __packed_ws__(G_SetOrClearPlayerFlags_6x2E, 0x0C);
|
} __packed_ws__(G_SetOrClearPlayerFlags_6x2E, 0x0C);
|
||||||
|
|
||||||
// 6x2F: Change player HP
|
// 6x2F: Change player HP (protected on GC NTE only)
|
||||||
|
|
||||||
struct G_ChangePlayerHP_6x2F {
|
struct G_ChangePlayerHP_6x2F {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4428,7 +4443,7 @@ struct G_ChangePlayerHP_6x2F {
|
|||||||
le_uint16_t client_id = 0;
|
le_uint16_t client_id = 0;
|
||||||
} __packed_ws__(G_ChangePlayerHP_6x2F, 0x0C);
|
} __packed_ws__(G_ChangePlayerHP_6x2F, 0x0C);
|
||||||
|
|
||||||
// 6x30: Change player level
|
// 6x30: Change player level (protected on GC NTE/V3 but not V4)
|
||||||
// On DC NTE, the updated stats aren't sent, and the client may only gain a
|
// On DC NTE, the updated stats aren't sent, and the client may only gain a
|
||||||
// single level at once. On other versions, this is not the case.
|
// single level at once. On other versions, this is not the case.
|
||||||
|
|
||||||
@@ -4454,11 +4469,11 @@ struct G_UseMedicalCenter_6x31 {
|
|||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
} __packed_ws__(G_UseMedicalCenter_6x31, 4);
|
} __packed_ws__(G_UseMedicalCenter_6x31, 4);
|
||||||
|
|
||||||
// 6x32: Revive player (Medical Center)
|
// 6x32: Revive all players (Medical Center)
|
||||||
|
|
||||||
struct G_MedicalCenterRevivePlayer_6x32 {
|
struct G_MedicalCenterReviveAllPlayers_6x32 {
|
||||||
G_UnusedHeader header;
|
G_UnusedHeader header;
|
||||||
} __packed_ws__(G_MedicalCenterRevivePlayer_6x32, 4);
|
} __packed_ws__(G_MedicalCenterReviveAllPlayers_6x32, 4);
|
||||||
|
|
||||||
// 6x33: Revive player (with Moon Atomizer) (protected on V3/V4)
|
// 6x33: Revive player (with Moon Atomizer) (protected on V3/V4)
|
||||||
|
|
||||||
@@ -4561,7 +4576,7 @@ struct G_TargetBase_6x3D {
|
|||||||
G_UnusedHeader header;
|
G_UnusedHeader header;
|
||||||
} __packed_ws__(G_TargetBase_6x3D, 4);
|
} __packed_ws__(G_TargetBase_6x3D, 4);
|
||||||
|
|
||||||
// 6x3E: Stop moving (protected on V3/V4)
|
// 6x3E: Stop moving (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_StopAtPosition_6x3E {
|
struct G_StopAtPosition_6x3E {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4572,7 +4587,7 @@ struct G_StopAtPosition_6x3E {
|
|||||||
VectorXYZF pos;
|
VectorXYZF pos;
|
||||||
} __packed_ws__(G_StopAtPosition_6x3E, 0x18);
|
} __packed_ws__(G_StopAtPosition_6x3E, 0x18);
|
||||||
|
|
||||||
// 6x3F: Set position (protected on V3/V4)
|
// 6x3F: Set position (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_SetPosition_6x3F {
|
struct G_SetPosition_6x3F {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4583,7 +4598,7 @@ struct G_SetPosition_6x3F {
|
|||||||
VectorXYZF pos;
|
VectorXYZF pos;
|
||||||
} __packed_ws__(G_SetPosition_6x3F, 0x18);
|
} __packed_ws__(G_SetPosition_6x3F, 0x18);
|
||||||
|
|
||||||
// 6x40: Walk (protected on V3/V4)
|
// 6x40: Walk (protected on GC NTE/V3/V4)
|
||||||
// If UDP mode is enabled, this command is sent via UDP.
|
// If UDP mode is enabled, this command is sent via UDP.
|
||||||
|
|
||||||
struct G_WalkToPosition_6x40 {
|
struct G_WalkToPosition_6x40 {
|
||||||
@@ -4593,7 +4608,7 @@ struct G_WalkToPosition_6x40 {
|
|||||||
} __packed_ws__(G_WalkToPosition_6x40, 0x10);
|
} __packed_ws__(G_WalkToPosition_6x40, 0x10);
|
||||||
|
|
||||||
// 6x41: Move to position (v1)
|
// 6x41: Move to position (v1)
|
||||||
// 6x42: Run (protected on V3/V4)
|
// 6x42: Run (protected on GC NTE/V3/V4)
|
||||||
// Command 6x41 is completely ignored by v2 and later.
|
// Command 6x41 is completely ignored by v2 and later.
|
||||||
// If UDP mode is enabled, this command is sent via UDP.
|
// If UDP mode is enabled, this command is sent via UDP.
|
||||||
// TODO: Should newserv translate 6x41 to 6x42? Is there any difference in how
|
// TODO: Should newserv translate 6x41 to 6x42? Is there any difference in how
|
||||||
@@ -4604,9 +4619,9 @@ struct G_MoveToPosition_6x41_6x42 {
|
|||||||
VectorXZF pos;
|
VectorXZF pos;
|
||||||
} __packed_ws__(G_MoveToPosition_6x41_6x42, 0x0C);
|
} __packed_ws__(G_MoveToPosition_6x41_6x42, 0x0C);
|
||||||
|
|
||||||
// 6x43: First attack (protected on V3/V4)
|
// 6x43: First attack (protected on GC NTE/V3/V4)
|
||||||
// 6x44: Second attack (protected on V3/V4)
|
// 6x44: Second attack (protected on GC NTE/V3/V4)
|
||||||
// 6x45: Third attack (protected on V3/V4)
|
// 6x45: Third attack (protected on GC NTE/V3/V4)
|
||||||
// If UDP mode is enabled, these commands are sent via UDP.
|
// If UDP mode is enabled, these commands are sent via UDP.
|
||||||
|
|
||||||
struct G_Attack_6x43_6x44_6x45 {
|
struct G_Attack_6x43_6x44_6x45 {
|
||||||
@@ -4615,7 +4630,8 @@ struct G_Attack_6x43_6x44_6x45 {
|
|||||||
le_uint16_t unknown_a2 = 0;
|
le_uint16_t unknown_a2 = 0;
|
||||||
} __packed_ws__(G_Attack_6x43_6x44_6x45, 8);
|
} __packed_ws__(G_Attack_6x43_6x44_6x45, 8);
|
||||||
|
|
||||||
// 6x46: Attack finished (sent after each of 43, 44, and 45) (protected on V3/V4)
|
// 6x46: Attack finished (sent after each of 43, 44, and 45) (protected on GC
|
||||||
|
// NTE/V3/V4)
|
||||||
// The number of targets is not bounds-checked during byteswapping on GC
|
// The number of targets is not bounds-checked during byteswapping on GC
|
||||||
// clients. The client only expects up to 10 entries here, so if the number of
|
// clients. The client only expects up to 10 entries here, so if the number of
|
||||||
// targets is too large, the client will byteswap the function's return address
|
// targets is too large, the client will byteswap the function's return address
|
||||||
@@ -4627,7 +4643,7 @@ struct G_AttackFinished_Header_6x46 {
|
|||||||
// Up to 10 TargetEntries are sent here
|
// Up to 10 TargetEntries are sent here
|
||||||
} __packed_ws__(G_AttackFinished_Header_6x46, 8);
|
} __packed_ws__(G_AttackFinished_Header_6x46, 8);
|
||||||
|
|
||||||
// 6x47: Cast technique (protected on V3/V4)
|
// 6x47: Cast technique (protected on GC NTE/V3/V4)
|
||||||
// On GC, this command has the same bounds-check bug as 6x46.
|
// On GC, this command has the same bounds-check bug as 6x46.
|
||||||
|
|
||||||
struct G_CastTechnique_Header_6x47 {
|
struct G_CastTechnique_Header_6x47 {
|
||||||
@@ -4644,7 +4660,7 @@ struct G_CastTechnique_Header_6x47 {
|
|||||||
// Up to 10 TargetEntries are sent here
|
// Up to 10 TargetEntries are sent here
|
||||||
} __packed_ws__(G_CastTechnique_Header_6x47, 8);
|
} __packed_ws__(G_CastTechnique_Header_6x47, 8);
|
||||||
|
|
||||||
// 6x48: Cast technique complete (protected on V3/V4)
|
// 6x48: Cast technique complete (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_CastTechniqueComplete_6x48 {
|
struct G_CastTechniqueComplete_6x48 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4654,7 +4670,7 @@ struct G_CastTechniqueComplete_6x48 {
|
|||||||
le_uint16_t level = 0;
|
le_uint16_t level = 0;
|
||||||
} __packed_ws__(G_CastTechniqueComplete_6x48, 8);
|
} __packed_ws__(G_CastTechniqueComplete_6x48, 8);
|
||||||
|
|
||||||
// 6x49: Execute Photon Blast (protected on V3/V4)
|
// 6x49: Execute Photon Blast (protected on GC NTE/V3/V4)
|
||||||
// On GC, this command has the same bounds-check bug as 6x46.
|
// On GC, this command has the same bounds-check bug as 6x46.
|
||||||
|
|
||||||
struct G_ExecutePhotonBlast_Header_6x49 {
|
struct G_ExecutePhotonBlast_Header_6x49 {
|
||||||
@@ -4673,8 +4689,8 @@ struct G_ShieldAttack_6x4A {
|
|||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
} __packed_ws__(G_ShieldAttack_6x4A, 4);
|
} __packed_ws__(G_ShieldAttack_6x4A, 4);
|
||||||
|
|
||||||
// 6x4B: Hit by enemy (protected on V3/V4)
|
// 6x4B: Hit by enemy (protected on GC NTE/V3/V4)
|
||||||
// 6x4C: Hit by enemy (protected on V3/V4)
|
// 6x4C: Hit by enemy (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_HitByEnemy_6x4B_6x4C {
|
struct G_HitByEnemy_6x4B_6x4C {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4683,14 +4699,14 @@ struct G_HitByEnemy_6x4B_6x4C {
|
|||||||
VectorXZF velocity;
|
VectorXZF velocity;
|
||||||
} __packed_ws__(G_HitByEnemy_6x4B_6x4C, 0x10);
|
} __packed_ws__(G_HitByEnemy_6x4B_6x4C, 0x10);
|
||||||
|
|
||||||
// 6x4D: Player died (protected on V3/V4)
|
// 6x4D: Player died (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_PlayerDied_6x4D {
|
struct G_PlayerDied_6x4D {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
le_uint32_t death_flags = 0; // Same as 6x70's death_flags field
|
le_uint32_t death_flags = 0; // Same as 6x70's death_flags field
|
||||||
} __packed_ws__(G_PlayerDied_6x4D, 8);
|
} __packed_ws__(G_PlayerDied_6x4D, 8);
|
||||||
|
|
||||||
// 6x4E: Player is dead can be revived (protected on V3/V4)
|
// 6x4E: Player can be revived (protected on GC NTE/V3/V4)
|
||||||
// This command creates the particle effect that Reverser and Moon Atomizers
|
// This command creates the particle effect that Reverser and Moon Atomizers
|
||||||
// can target.
|
// can target.
|
||||||
|
|
||||||
@@ -4705,7 +4721,9 @@ struct G_PlayerRevived_6x4F {
|
|||||||
} __packed_ws__(G_PlayerRevived_6x4F, 4);
|
} __packed_ws__(G_PlayerRevived_6x4F, 4);
|
||||||
|
|
||||||
// 6x50: Switch interaction (protected on V3/V4)
|
// 6x50: Switch interaction (protected on V3/V4)
|
||||||
// If UDP mode is enabled, this command is sent via UDP.
|
// If UDP mode is enabled, this command is sent via UDP. This command doesn't
|
||||||
|
// actually do anything with the switch; it just sets the player's animation
|
||||||
|
// state. 6x05 is used to set the switch flag if needed.
|
||||||
|
|
||||||
struct G_SwitchInteraction_6x50 {
|
struct G_SwitchInteraction_6x50 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4724,7 +4742,7 @@ struct G_SetPlayerAngle_6x51 {
|
|||||||
parray<uint8_t, 2> unused;
|
parray<uint8_t, 2> unused;
|
||||||
} __packed_ws__(G_SetPlayerAngle_6x51, 8);
|
} __packed_ws__(G_SetPlayerAngle_6x51, 8);
|
||||||
|
|
||||||
// 6x52: Set animation state (protected on V3/V4)
|
// 6x52: Set animation state (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_SetAnimationState_6x52 {
|
struct G_SetAnimationState_6x52 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4733,7 +4751,7 @@ struct G_SetAnimationState_6x52 {
|
|||||||
le_uint32_t angle = 0;
|
le_uint32_t angle = 0;
|
||||||
} __packed_ws__(G_SetAnimationState_6x52, 0x0C);
|
} __packed_ws__(G_SetAnimationState_6x52, 0x0C);
|
||||||
|
|
||||||
// 6x53: Unknown (supported; game only) (protected on V3/V4)
|
// 6x53: Unknown (supported; game only) (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_Unknown_6x53 {
|
struct G_Unknown_6x53 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4748,16 +4766,16 @@ struct G_Unknown_6x54 {
|
|||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
} __packed_ws__(G_Unknown_6x54, 4);
|
} __packed_ws__(G_Unknown_6x54, 4);
|
||||||
|
|
||||||
// 6x55: Intra-map warp (protected on V3/V4)
|
// 6x55: Intra-map warp (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_IntraMapWarp_6x55 {
|
struct G_IntraMapWarp_6x55 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
le_uint32_t angle_y = 0;
|
le_uint32_t angle_y = 0;
|
||||||
VectorXYZF from_pos;
|
VectorXYZF from_pos;
|
||||||
VectorXYZF to_pos;
|
VectorXYZF pos;
|
||||||
} __packed_ws__(G_IntraMapWarp_6x55, 0x20);
|
} __packed_ws__(G_IntraMapWarp_6x55, 0x20);
|
||||||
|
|
||||||
// 6x56: Set player position and angle (protected on V3/V4)
|
// 6x56: Set player position and angle (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_SetPlayerPositionAndAngle_6x56 {
|
struct G_SetPlayerPositionAndAngle_6x56 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4771,7 +4789,7 @@ struct G_Unknown_6x57 {
|
|||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
} __packed_ws__(G_Unknown_6x57, 4);
|
} __packed_ws__(G_Unknown_6x57, 4);
|
||||||
|
|
||||||
// 6x58: Lobby animation (protected on V3/V4)
|
// 6x58: Lobby animation (protected on GC NTE/V3/V4)
|
||||||
// If UDP mode is enabled, this command is sent via UDP.
|
// If UDP mode is enabled, this command is sent via UDP.
|
||||||
|
|
||||||
struct G_LobbyAnimation_6x58 {
|
struct G_LobbyAnimation_6x58 {
|
||||||
@@ -4802,12 +4820,11 @@ struct G_PickUpItemRequest_6x5A {
|
|||||||
// This command has a handler, but it does nothing, even on DC NTE.
|
// This command has a handler, but it does nothing, even on DC NTE.
|
||||||
|
|
||||||
// 6x5C: Destroy floor item
|
// 6x5C: Destroy floor item
|
||||||
// Same format as 6x63. It appears this version should not be used because it
|
// Same format as 6x63. It appears this command should not be used because it
|
||||||
// removes the item from the floor just like 6x63 does, but 6x5C doesn't call
|
// removes the item from the floor just like 6x63 does, but 6x5C doesn't call
|
||||||
// the item's destructor.
|
// the item's destructor.
|
||||||
|
|
||||||
// 6x5D: Drop meseta or stacked item
|
// 6x5D: Drop meseta or stacked item
|
||||||
// On DC NTE, this command has the same format, but is subcommand 6x4F instead.
|
|
||||||
|
|
||||||
struct G_DropStackedItem_DC_6x5D {
|
struct G_DropStackedItem_DC_6x5D {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4821,7 +4838,7 @@ struct G_DropStackedItem_PC_V3_BB_6x5D : G_DropStackedItem_DC_6x5D {
|
|||||||
le_uint32_t unused3 = 0;
|
le_uint32_t unused3 = 0;
|
||||||
} __packed_ws__(G_DropStackedItem_PC_V3_BB_6x5D, 0x28);
|
} __packed_ws__(G_DropStackedItem_PC_V3_BB_6x5D, 0x28);
|
||||||
|
|
||||||
// 6x5E: Buy item at shop
|
// 6x5E: Buy item at shop (protected on GC NTE/V3)
|
||||||
|
|
||||||
struct G_BuyShopItem_6x5E {
|
struct G_BuyShopItem_6x5E {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -4896,10 +4913,10 @@ struct G_DestroyFloorItem_6x5C_6x63 {
|
|||||||
} __packed_ws__(G_DestroyFloorItem_6x5C_6x63, 0x0C);
|
} __packed_ws__(G_DestroyFloorItem_6x5C_6x63, 0x0C);
|
||||||
|
|
||||||
// 6x64: Unused (not valid on Episode 3)
|
// 6x64: Unused (not valid on Episode 3)
|
||||||
// This command has a handler, but it does nothing even on DC NTE.
|
// This command has a handler, but it does nothing, even on DC NTE.
|
||||||
|
|
||||||
// 6x65: Unused (not valid on Episode 3)
|
// 6x65: Unused (not valid on Episode 3)
|
||||||
// This command has a handler, but it does nothing even on DC NTE.
|
// This command has a handler, but it does nothing, even on DC NTE.
|
||||||
|
|
||||||
// 6x66: Use star atomizer
|
// 6x66: Use star atomizer
|
||||||
|
|
||||||
@@ -5055,6 +5072,9 @@ struct G_SyncSetFlagState_6x6E_Decompressed {
|
|||||||
} __packed_ws__(G_SyncSetFlagState_6x6E_Decompressed, 8);
|
} __packed_ws__(G_SyncSetFlagState_6x6E_Decompressed, 8);
|
||||||
|
|
||||||
// 6x6F: Set quest flags (used while loading into game)
|
// 6x6F: Set quest flags (used while loading into game)
|
||||||
|
// On Episode 3, this command sets the seq vars instead. However, the client
|
||||||
|
// never sends this, since seq vars don't need to be synced to the entire game
|
||||||
|
// in online play.
|
||||||
|
|
||||||
struct G_SetQuestFlags_DCv1_6x6F {
|
struct G_SetQuestFlags_DCv1_6x6F {
|
||||||
G_UnusedHeader header;
|
G_UnusedHeader header;
|
||||||
@@ -5066,6 +5086,11 @@ struct G_SetQuestFlags_V2_V3_6x6F {
|
|||||||
QuestFlags quest_flags;
|
QuestFlags quest_flags;
|
||||||
} __packed_ws__(G_SetQuestFlags_V2_V3_6x6F, 0x204);
|
} __packed_ws__(G_SetQuestFlags_V2_V3_6x6F, 0x204);
|
||||||
|
|
||||||
|
struct G_SetSeqVars_Ep3_6x6F {
|
||||||
|
G_UnusedHeader header;
|
||||||
|
Ep3SeqVars seq_vars;
|
||||||
|
} __packed_ws__(G_SetSeqVars_Ep3_6x6F, 0x404);
|
||||||
|
|
||||||
struct G_SetQuestFlags_BB_6x6F {
|
struct G_SetQuestFlags_BB_6x6F {
|
||||||
G_UnusedHeader header;
|
G_UnusedHeader header;
|
||||||
QuestFlags quest_flags;
|
QuestFlags quest_flags;
|
||||||
@@ -5143,7 +5168,7 @@ struct G_6x70_Base_V1 {
|
|||||||
/* 0040 */ StatusEffectState attack_status_effect;
|
/* 0040 */ StatusEffectState attack_status_effect;
|
||||||
/* 004C */ StatusEffectState defense_status_effect;
|
/* 004C */ StatusEffectState defense_status_effect;
|
||||||
/* 0058 */ StatusEffectState unused_status_effect;
|
/* 0058 */ StatusEffectState unused_status_effect;
|
||||||
/* 0064 */ le_uint32_t language = 0;
|
/* 0064 */ le_uint32_t language32 = 0;
|
||||||
/* 0068 */ le_uint32_t player_tag = 0;
|
/* 0068 */ le_uint32_t player_tag = 0;
|
||||||
/* 006C */ le_uint32_t guild_card_number = 0;
|
/* 006C */ le_uint32_t guild_card_number = 0;
|
||||||
/* 0070 */ le_uint32_t unknown_a6 = 0; // Probably battle-related (assigned together with battle_team_number)
|
/* 0070 */ le_uint32_t unknown_a6 = 0; // Probably battle-related (assigned together with battle_team_number)
|
||||||
@@ -5246,6 +5271,7 @@ check_struct_size(G_WordSelect_6x74, 0x20);
|
|||||||
check_struct_size(G_WordSelectBE_6x74, 0x20);
|
check_struct_size(G_WordSelectBE_6x74, 0x20);
|
||||||
|
|
||||||
// 6x75: Update quest flag
|
// 6x75: Update quest flag
|
||||||
|
// This command does nothing on Episode 3.
|
||||||
|
|
||||||
struct G_UpdateQuestFlag_DC_PC_6x75 {
|
struct G_UpdateQuestFlag_DC_PC_6x75 {
|
||||||
G_UnusedHeader header;
|
G_UnusedHeader header;
|
||||||
@@ -5254,7 +5280,7 @@ struct G_UpdateQuestFlag_DC_PC_6x75 {
|
|||||||
} __packed_ws__(G_UpdateQuestFlag_DC_PC_6x75, 8);
|
} __packed_ws__(G_UpdateQuestFlag_DC_PC_6x75, 8);
|
||||||
|
|
||||||
struct G_UpdateQuestFlag_V3_BB_6x75 : G_UpdateQuestFlag_DC_PC_6x75 {
|
struct G_UpdateQuestFlag_V3_BB_6x75 : G_UpdateQuestFlag_DC_PC_6x75 {
|
||||||
le_uint16_t difficulty = 0;
|
le_uint16_t difficulty16 = 0;
|
||||||
le_uint16_t unused = 0;
|
le_uint16_t unused = 0;
|
||||||
} __packed_ws__(G_UpdateQuestFlag_V3_BB_6x75, 0x0C);
|
} __packed_ws__(G_UpdateQuestFlag_V3_BB_6x75, 0x0C);
|
||||||
|
|
||||||
@@ -5270,6 +5296,7 @@ struct G_SetEntitySetFlags_6x76 {
|
|||||||
|
|
||||||
// 6x77: Sync quest register
|
// 6x77: Sync quest register
|
||||||
// This is sent by the client when an opcode D9 is executed within a quest.
|
// This is sent by the client when an opcode D9 is executed within a quest.
|
||||||
|
// This command does nothing on Episode 3.
|
||||||
|
|
||||||
struct G_SyncQuestRegister_6x77 {
|
struct G_SyncQuestRegister_6x77 {
|
||||||
G_UnusedHeader header;
|
G_UnusedHeader header;
|
||||||
@@ -5305,13 +5332,13 @@ struct G_GogoBall_6x79 {
|
|||||||
parray<uint8_t, 3> unused;
|
parray<uint8_t, 3> unused;
|
||||||
} __packed_ws__(G_GogoBall_6x79, 0x18);
|
} __packed_ws__(G_GogoBall_6x79, 0x18);
|
||||||
|
|
||||||
// 6x7A: Enable Stealth Suit effect (protected on V3/V4)
|
// 6x7A: Enable Stealth Suit effect (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_EnableStealthSuitEffect_6x7A {
|
struct G_EnableStealthSuitEffect_6x7A {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
} __packed_ws__(G_EnableStealthSuitEffect_6x7A, 4);
|
} __packed_ws__(G_EnableStealthSuitEffect_6x7A, 4);
|
||||||
|
|
||||||
// 6x7B: Disable Stealth Suit effect (protected on V3/V4)
|
// 6x7B: Disable Stealth Suit effect (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_DisableStealthSuitEffect_6x7B {
|
struct G_DisableStealthSuitEffect_6x7B {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -5394,19 +5421,19 @@ struct G_TriggerTrap_6x80 {
|
|||||||
le_uint16_t what = 0; // Must be 0, 1, or 2
|
le_uint16_t what = 0; // Must be 0, 1, or 2
|
||||||
} __packed_ws__(G_TriggerTrap_6x80, 8);
|
} __packed_ws__(G_TriggerTrap_6x80, 8);
|
||||||
|
|
||||||
// 6x81: Disable drop weapon on death (protected on V3/V4)
|
// 6x81: Disable drop weapon on death (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_DisableDropWeaponOnDeath_6x81 {
|
struct G_DisableDropWeaponOnDeath_6x81 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
} __packed_ws__(G_DisableDropWeaponOnDeath_6x81, 4);
|
} __packed_ws__(G_DisableDropWeaponOnDeath_6x81, 4);
|
||||||
|
|
||||||
// 6x82: Enable drop weapon on death (protected on V3/V4)
|
// 6x82: Enable drop weapon on death (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_EnableDropWeaponOnDeath_6x82 {
|
struct G_EnableDropWeaponOnDeath_6x82 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
} __packed_ws__(G_EnableDropWeaponOnDeath_6x82, 4);
|
} __packed_ws__(G_EnableDropWeaponOnDeath_6x82, 4);
|
||||||
|
|
||||||
// 6x83: Place trap (protected on V3/V4)
|
// 6x83: Place trap (protected on GC NTE/V3/V4)
|
||||||
|
|
||||||
struct G_PlaceTrap_6x83 {
|
struct G_PlaceTrap_6x83 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -5603,7 +5630,7 @@ struct G_UpdateEntityStat_6x9A {
|
|||||||
uint8_t amount = 0;
|
uint8_t amount = 0;
|
||||||
} __packed_ws__(G_UpdateEntityStat_6x9A, 8);
|
} __packed_ws__(G_UpdateEntityStat_6x9A, 8);
|
||||||
|
|
||||||
// 6x9B: Level up all techniques (protected on V3/V4)
|
// 6x9B: Level up all techniques (protected on GC NTE/V3/V4)
|
||||||
// Used in battle mode if the rules specify that techniques should level up
|
// Used in battle mode if the rules specify that techniques should level up
|
||||||
// upon character death.
|
// upon character death.
|
||||||
|
|
||||||
@@ -5967,10 +5994,15 @@ struct G_MapData_Ep3_6xB6x41 {
|
|||||||
} __packed_ws__(G_MapData_Ep3_6xB6x41, 0x14);
|
} __packed_ws__(G_MapData_Ep3_6xB6x41, 0x14);
|
||||||
|
|
||||||
// 6xB6: BB shop contents (server->client only)
|
// 6xB6: BB shop contents (server->client only)
|
||||||
|
// The client will ignore this command (leaving the player softlocked) if there
|
||||||
|
// are too many items. The limits are:
|
||||||
|
// - Tool shop: up to 18 items allowed
|
||||||
|
// - Weapon shop: up to 16 items allowed
|
||||||
|
// - Armor shop: up to 21 items allowed
|
||||||
|
|
||||||
struct G_ShopContents_BB_6xB6 {
|
struct G_ShopContents_BB_6xB6 {
|
||||||
G_UnusedHeader header;
|
G_UnusedHeader header;
|
||||||
uint8_t shop_type = 0;
|
uint8_t shop_type = 0; // 0 = tool shop, 1 = weapon shop, 2 = armor shop
|
||||||
uint8_t num_items = 0;
|
uint8_t num_items = 0;
|
||||||
le_uint16_t unused = 0;
|
le_uint16_t unused = 0;
|
||||||
// Note: data2d of these entries should be the price
|
// Note: data2d of these entries should be the price
|
||||||
@@ -6152,13 +6184,24 @@ struct G_ChangeLobbyMusic_Ep3_6xBF {
|
|||||||
} __packed_ws__(G_ChangeLobbyMusic_Ep3_6xBF, 8);
|
} __packed_ws__(G_ChangeLobbyMusic_Ep3_6xBF, 8);
|
||||||
|
|
||||||
// 6xBF: Give EXP (BB) (server->client only)
|
// 6xBF: Give EXP (BB) (server->client only)
|
||||||
|
// newserv implements an extension that causes this command to show the purple
|
||||||
|
// EXP numbers which are normally generated by the client instead. This
|
||||||
|
// requires the server to also send the enemy ID that generated the EXP, hence
|
||||||
|
// the extension struct here. See ServerEXPDisplay.59NL.patch.s for details.
|
||||||
|
|
||||||
struct G_GiveExperience_BB_6xBF {
|
struct G_GiveExperience_BB_6xBF {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
le_uint32_t amount = 0;
|
le_uint32_t amount = 0;
|
||||||
} __packed_ws__(G_GiveExperience_BB_6xBF, 8);
|
} __packed_ws__(G_GiveExperience_BB_6xBF, 8);
|
||||||
|
|
||||||
// 6xC0: Sell item at shop (BB) (protected on V3/V4)
|
struct G_GiveExperience_Extension_BB_6xBF {
|
||||||
|
G_ClientIDHeader header;
|
||||||
|
le_uint32_t amount = 0;
|
||||||
|
le_uint16_t from_enemy_id = 0;
|
||||||
|
le_uint16_t unused = 0;
|
||||||
|
} __packed_ws__(G_GiveExperience_Extension_BB_6xBF, 0x0C);
|
||||||
|
|
||||||
|
// 6xC0: Sell item at shop (BB) (protected)
|
||||||
|
|
||||||
struct G_SellItemAtShop_BB_6xC0 {
|
struct G_SellItemAtShop_BB_6xC0 {
|
||||||
G_UnusedHeader header;
|
G_UnusedHeader header;
|
||||||
@@ -6237,11 +6280,13 @@ struct G_AdjustPlayerMeseta_BB_6xC9 {
|
|||||||
} __packed_ws__(G_AdjustPlayerMeseta_BB_6xC9, 8);
|
} __packed_ws__(G_AdjustPlayerMeseta_BB_6xC9, 8);
|
||||||
|
|
||||||
// 6xCA: Request item reward from quest (BB; handled by server)
|
// 6xCA: Request item reward from quest (BB; handled by server)
|
||||||
|
// The server should create the item in the player's inventory using 6xBE if it
|
||||||
|
// matches at least one of the item creation masks in the quest's header.
|
||||||
|
|
||||||
struct G_ItemRewardRequest_BB_6xCA {
|
struct G_QuestCreateItem_BB_6xCA {
|
||||||
G_UnusedHeader header;
|
G_UnusedHeader header;
|
||||||
ItemData item_data;
|
ItemData item_data;
|
||||||
} __packed_ws__(G_ItemRewardRequest_BB_6xCA, 0x18);
|
} __packed_ws__(G_QuestCreateItem_BB_6xCA, 0x18);
|
||||||
|
|
||||||
// 6xCB: Transfer item via mail message (BB)
|
// 6xCB: Transfer item via mail message (BB)
|
||||||
|
|
||||||
@@ -6287,7 +6332,14 @@ struct G_ChallengeModeGraveRecoveryItemRequest_BB_6xD1 {
|
|||||||
le_uint16_t floor = 0;
|
le_uint16_t floor = 0;
|
||||||
le_uint16_t room_id = 0;
|
le_uint16_t room_id = 0;
|
||||||
VectorXZF pos;
|
VectorXZF pos;
|
||||||
le_uint32_t item_type = 0; // Should be < 6
|
// Values for item_type:
|
||||||
|
// 0 = Monomate x1
|
||||||
|
// 1 = Dimate x1
|
||||||
|
// 2 = Trimate x1
|
||||||
|
// 3 = Monofluid x1
|
||||||
|
// 4 = Difluid x1
|
||||||
|
// 5 = Trifluid x1
|
||||||
|
le_uint32_t item_type = 0;
|
||||||
} __packed_ws__(G_ChallengeModeGraveRecoveryItemRequest_BB_6xD1, 0x14);
|
} __packed_ws__(G_ChallengeModeGraveRecoveryItemRequest_BB_6xD1, 0x14);
|
||||||
|
|
||||||
// 6xD2: Set quest counter (BB)
|
// 6xD2: Set quest counter (BB)
|
||||||
@@ -6315,14 +6367,18 @@ struct G_Unknown_BB_6xD4 {
|
|||||||
|
|
||||||
// 6xD5: Exchange item in quest (BB; handled by server)
|
// 6xD5: Exchange item in quest (BB; handled by server)
|
||||||
// The client sends this when it executes an F953 quest opcode.
|
// The client sends this when it executes an F953 quest opcode.
|
||||||
|
// If any item matching find_item.data1[0-2] is present in the player's
|
||||||
|
// inventory, the server should destroy that item using 6x29, then create
|
||||||
|
// replace_item in the player's inventory using 6xBE if it matches at least one
|
||||||
|
// of the item creation masks in the quest's header.
|
||||||
|
|
||||||
struct G_ExchangeItemInQuest_BB_6xD5 {
|
struct G_QuestExchangeItem_BB_6xD5 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
ItemData find_item; // Only data1[0]-[2] are used
|
ItemData find_item; // Only data1[0]-[2] are used
|
||||||
ItemData replace_item; // Only data1[0]-[2] are used
|
ItemData replace_item; // Only data1[0]-[2] are used
|
||||||
le_uint16_t success_label = 0;
|
le_uint16_t success_label = 0;
|
||||||
le_uint16_t failure_label = 0;
|
le_uint16_t failure_label = 0;
|
||||||
} __packed_ws__(G_ExchangeItemInQuest_BB_6xD5, 0x30);
|
} __packed_ws__(G_QuestExchangeItem_BB_6xD5, 0x30);
|
||||||
|
|
||||||
// 6xD6: Wrap item (BB; handled by server)
|
// 6xD6: Wrap item (BB; handled by server)
|
||||||
|
|
||||||
@@ -6335,6 +6391,8 @@ struct G_WrapItem_BB_6xD6 {
|
|||||||
|
|
||||||
// 6xD7: Paganini Photon Drop exchange (BB; handled by server)
|
// 6xD7: Paganini Photon Drop exchange (BB; handled by server)
|
||||||
// The client sends this when it executes an F955 quest opcode.
|
// The client sends this when it executes an F955 quest opcode.
|
||||||
|
// The server should create the item in the player's inventory using 6xBE if it
|
||||||
|
// matches at least one of the item creation masks in the quest's header.
|
||||||
|
|
||||||
struct G_PaganiniPhotonDropExchange_BB_6xD7 {
|
struct G_PaganiniPhotonDropExchange_BB_6xD7 {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
@@ -6356,16 +6414,25 @@ struct G_AddSRankWeaponSpecial_BB_6xD8 {
|
|||||||
} __packed_ws__(G_AddSRankWeaponSpecial_BB_6xD8, 0x24);
|
} __packed_ws__(G_AddSRankWeaponSpecial_BB_6xD8, 0x24);
|
||||||
|
|
||||||
// 6xD9: Momoka item exchange (BB; handled by server)
|
// 6xD9: Momoka item exchange (BB; handled by server)
|
||||||
// The client sends this when it executes an F95B quest opcode.
|
// The client sends this when it executes an F95B quest opcode. The client has
|
||||||
|
// an unfortunate bug where it doesn't set the size field when generating this
|
||||||
|
// command, so the size ends up as an uninitialized value and the client sends
|
||||||
|
// more (or less!) data than necessary. The MomokaItemExchangeFix patch fixes
|
||||||
|
// this bug.
|
||||||
|
// The server should create the item in the player's inventory using 6xBE if it
|
||||||
|
// matches at least one of the item creation masks in the quest's header.
|
||||||
|
|
||||||
struct G_MomokaItemExchange_BB_6xD9 {
|
struct G_MomokaItemExchange_BB_6xD9 {
|
||||||
G_ClientIDHeader header;
|
/* 00 */ G_ClientIDHeader header;
|
||||||
ItemData find_item; // Only data1[0]-[2] are used
|
// Only data1[0-2] are used in find_item and replace_item when this is sent
|
||||||
ItemData replace_item; // Only data1[0]-[2] are used
|
// by the F95B quest opcode.
|
||||||
le_uint32_t token1 = 0; // valueC (from F95B opcode) ^ sender client ID
|
/* 04 */ ItemData find_item;
|
||||||
le_uint32_t token2 = 0; // valueD (from F95B opcode) ^ sender client ID
|
/* 18 */ ItemData replace_item;
|
||||||
le_uint16_t success_label = 0;
|
/* 2C */ le_uint32_t token1 = 0; // valueC (from F95B opcode) ^ sender client ID
|
||||||
le_uint16_t failure_label = 0;
|
/* 30 */ le_uint32_t token2 = 0; // valueD (from F95B opcode) ^ sender client ID
|
||||||
|
/* 34 */ le_uint16_t success_label = 0;
|
||||||
|
/* 36 */ le_uint16_t failure_label = 0;
|
||||||
|
/* 38 */
|
||||||
} __packed_ws__(G_MomokaItemExchange_BB_6xD9, 0x38);
|
} __packed_ws__(G_MomokaItemExchange_BB_6xD9, 0x38);
|
||||||
|
|
||||||
// 6xDA: Upgrade weapon attribute (BB; handled by server)
|
// 6xDA: Upgrade weapon attribute (BB; handled by server)
|
||||||
@@ -6407,32 +6474,28 @@ struct G_Episode4BossActions_BB_6xDC {
|
|||||||
// means all EXP is doubled, for example. This only affects what the client
|
// means all EXP is doubled, for example. This only affects what the client
|
||||||
// shows when an enemy is killed; actual EXP gains are controlled by the server
|
// shows when an enemy is killed; actual EXP gains are controlled by the server
|
||||||
// in response to the 6xC8 command.
|
// in response to the 6xC8 command.
|
||||||
// newserv supports an extension to this command that supports fractional
|
|
||||||
// multipliers. This is implemented in FractionalEXPMultiplier.59NL.patch.s.
|
|
||||||
|
|
||||||
struct G_SetEXPMultiplier_BB_6xDD {
|
struct G_SetEXPMultiplier_BB_6xDD {
|
||||||
G_ParameterHeader header;
|
G_ParameterHeader header;
|
||||||
} __packed_ws__(G_SetEXPMultiplier_BB_6xDD, 4);
|
} __packed_ws__(G_SetEXPMultiplier_BB_6xDD, 4);
|
||||||
|
|
||||||
struct G_SetFractionalEXPMultiplier_Extension_BB_6xDD {
|
|
||||||
G_ParameterHeader header;
|
|
||||||
le_float multiplier;
|
|
||||||
} __packed_ws__(G_SetFractionalEXPMultiplier_Extension_BB_6xDD, 8);
|
|
||||||
|
|
||||||
// 6xDE: Exchange Secret Lottery Ticket (BB; handled by server)
|
// 6xDE: Exchange Secret Lottery Ticket (BB; handled by server)
|
||||||
// The client sends this when it executes an F95C quest opcode.
|
// The client sends this when it executes an F95C quest opcode.
|
||||||
// There appears to be a bug in the client here: it sets the subcommand size to
|
// There is a bug in the client here: it sets the subcommand size to 2 instead
|
||||||
// 2 instead of 3, so the last relevant field (failure_label) is not sent to
|
// of 3, so the last relevant field (failure_label) is not sent to the server.
|
||||||
// the server.
|
// This is fixed in the MomokaItemExchangeFix patch.
|
||||||
|
|
||||||
struct G_ExchangeSecretLotteryTicket_BB_6xDE {
|
struct G_ExchangeSecretLotteryTicket_Incomplete_BB_6xDE {
|
||||||
G_ClientIDHeader header;
|
G_ClientIDHeader header;
|
||||||
uint8_t index = 0;
|
uint8_t index = 0; // 1-8
|
||||||
uint8_t unknown_a1 = 0;
|
uint8_t start_reg_num = 0;
|
||||||
le_uint16_t success_label = 0;
|
le_uint16_t success_label = 0;
|
||||||
// le_uint16_t failure_label = 0;
|
} __packed_ws__(G_ExchangeSecretLotteryTicket_Incomplete_BB_6xDE, 8);
|
||||||
// parray<uint8_t, 2> unused;
|
|
||||||
} __packed_ws__(G_ExchangeSecretLotteryTicket_BB_6xDE, 8);
|
struct G_ExchangeSecretLotteryTicket_BB_6xDE : G_ExchangeSecretLotteryTicket_Incomplete_BB_6xDE {
|
||||||
|
le_uint16_t failure_label = 0;
|
||||||
|
parray<uint8_t, 2> unused;
|
||||||
|
} __packed_ws__(G_ExchangeSecretLotteryTicket_BB_6xDE, 0x0C);
|
||||||
|
|
||||||
// 6xDF: Exchange Photon Crystals (BB; handled by server)
|
// 6xDF: Exchange Photon Crystals (BB; handled by server)
|
||||||
// The client sends this when it executes an F95D quest opcode.
|
// The client sends this when it executes an F95D quest opcode.
|
||||||
@@ -7189,7 +7252,7 @@ check_struct_size(G_SetTournamentPlayerDecks_Ep3_6xB4x3D, 0x1CC);
|
|||||||
struct G_MakeCardAuctionBid_Ep3_6xB5x3E {
|
struct G_MakeCardAuctionBid_Ep3_6xB5x3E {
|
||||||
G_CardBattleCommandHeader header = {0xB5, sizeof(G_MakeCardAuctionBid_Ep3_6xB5x3E) / 4, 0, 0x3E, 0, 0, 0};
|
G_CardBattleCommandHeader header = {0xB5, sizeof(G_MakeCardAuctionBid_Ep3_6xB5x3E) / 4, 0, 0x3E, 0, 0, 0};
|
||||||
uint8_t card_index = 0; // Index of card in EF command
|
uint8_t card_index = 0; // Index of card in EF command
|
||||||
uint8_t bid_value = 0; // 1-99
|
uint8_t bid_value = 0; // 0-99
|
||||||
parray<uint8_t, 2> unused;
|
parray<uint8_t, 2> unused;
|
||||||
} __packed_ws__(G_MakeCardAuctionBid_Ep3_6xB5x3E, 0x0C);
|
} __packed_ws__(G_MakeCardAuctionBid_Ep3_6xB5x3E, 0x0C);
|
||||||
|
|
||||||
|
|||||||
+47
-51
@@ -122,21 +122,19 @@ CommonItemSet::Table::Table(const phosg::JSON& json, Episode episode)
|
|||||||
const auto& enemy_type_drop_probs_json = json.at("EnemyTypeDropProbs").as_dict();
|
const auto& enemy_type_drop_probs_json = json.at("EnemyTypeDropProbs").as_dict();
|
||||||
const auto& enemy_item_classes_json = json.at("EnemyItemClasses").as_dict();
|
const auto& enemy_item_classes_json = json.at("EnemyItemClasses").as_dict();
|
||||||
for (size_t z = 0; z < 0x64; z++) {
|
for (size_t z = 0; z < 0x64; z++) {
|
||||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
auto types = enemy_types_for_rare_table_index(episode, z);
|
||||||
for (Episode episode : episodes) {
|
vector<string> names;
|
||||||
auto types = enemy_types_for_rare_table_index(episode, z);
|
if (types.empty()) {
|
||||||
vector<string> names;
|
names.emplace_back(std::format("{}:!{:02X}", abbreviation_for_episode(episode), z));
|
||||||
if (types.empty()) {
|
} else {
|
||||||
names.emplace_back(std::format("{}:!{:02X}", abbreviation_for_episode(episode), z));
|
for (auto type : types) {
|
||||||
}
|
|
||||||
for (auto type : enemy_types_for_rare_table_index(episode, z)) {
|
|
||||||
names.emplace_back(std::format("{}:{}", abbreviation_for_episode(episode), phosg::name_for_enum(type)));
|
names.emplace_back(std::format("{}:{}", abbreviation_for_episode(episode), phosg::name_for_enum(type)));
|
||||||
}
|
}
|
||||||
for (const auto& name : names) {
|
}
|
||||||
from_json_into(*enemy_meseta_ranges_json.at(name), this->enemy_meseta_ranges[z]);
|
for (const auto& name : names) {
|
||||||
this->enemy_type_drop_probs[z] = enemy_type_drop_probs_json.at(name)->as_int();
|
from_json_into(*enemy_meseta_ranges_json.at(name), this->enemy_meseta_ranges[z]);
|
||||||
this->enemy_item_classes[z] = enemy_item_classes_json.at(name)->as_int();
|
this->enemy_type_drop_probs[z] = enemy_type_drop_probs_json.at(name)->as_int();
|
||||||
}
|
this->enemy_item_classes[z] = enemy_item_classes_json.at(name)->as_int();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,8 +458,7 @@ phosg::JSON CommonItemSet::Table::json() const {
|
|||||||
phosg::JSON enemy_type_drop_probs_json = phosg::JSON::dict();
|
phosg::JSON enemy_type_drop_probs_json = phosg::JSON::dict();
|
||||||
phosg::JSON enemy_item_classes_json = phosg::JSON::dict();
|
phosg::JSON enemy_item_classes_json = phosg::JSON::dict();
|
||||||
for (size_t z = 0; z < 0x64; z++) {
|
for (size_t z = 0; z < 0x64; z++) {
|
||||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
for (Episode episode : ALL_EPISODES_V4) {
|
||||||
for (Episode episode : episodes) {
|
|
||||||
auto types = enemy_types_for_rare_table_index(episode, z);
|
auto types = enemy_types_for_rare_table_index(episode, z);
|
||||||
vector<string> names;
|
vector<string> names;
|
||||||
if (types.empty()) {
|
if (types.empty()) {
|
||||||
@@ -506,13 +503,11 @@ phosg::JSON CommonItemSet::Table::json() const {
|
|||||||
|
|
||||||
phosg::JSON CommonItemSet::json() const {
|
phosg::JSON CommonItemSet::json() const {
|
||||||
auto modes_dict = phosg::JSON::dict();
|
auto modes_dict = phosg::JSON::dict();
|
||||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
for (const auto& mode : ALL_GAME_MODES_V4) {
|
||||||
for (const auto& mode : modes) {
|
|
||||||
auto episodes_dict = phosg::JSON::dict();
|
auto episodes_dict = phosg::JSON::dict();
|
||||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
for (const auto& episode : ALL_EPISODES_V4) {
|
||||||
for (const auto& episode : episodes) {
|
|
||||||
auto difficulty_dict = phosg::JSON::dict();
|
auto difficulty_dict = phosg::JSON::dict();
|
||||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
for (const auto& difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
auto section_id_dict = phosg::JSON::dict();
|
auto section_id_dict = phosg::JSON::dict();
|
||||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||||
try {
|
try {
|
||||||
@@ -532,11 +527,9 @@ phosg::JSON CommonItemSet::json() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CommonItemSet::print(FILE* stream) const {
|
void CommonItemSet::print(FILE* stream) const {
|
||||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
for (const auto& mode : ALL_GAME_MODES_V4) {
|
||||||
for (const auto& mode : modes) {
|
for (const auto& episode : ALL_EPISODES_V4) {
|
||||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
for (const auto& episode : episodes) {
|
|
||||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
|
||||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||||
try {
|
try {
|
||||||
auto table = this->get_table(episode, mode, difficulty, section_id);
|
auto table = this->get_table(episode, mode, difficulty, section_id);
|
||||||
@@ -552,11 +545,9 @@ void CommonItemSet::print(FILE* stream) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CommonItemSet::print_diff(FILE* stream, const CommonItemSet& other) const {
|
void CommonItemSet::print_diff(FILE* stream, const CommonItemSet& other) const {
|
||||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
for (const auto& mode : ALL_GAME_MODES_V4) {
|
||||||
for (const auto& mode : modes) {
|
for (const auto& episode : ALL_EPISODES_V4) {
|
||||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
for (const auto& difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
for (const auto& episode : episodes) {
|
|
||||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
|
||||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||||
shared_ptr<const Table> this_table;
|
shared_ptr<const Table> this_table;
|
||||||
shared_ptr<const Table> other_table;
|
shared_ptr<const Table> other_table;
|
||||||
@@ -654,7 +645,7 @@ void CommonItemSet::Table::parse_itempt_t(const phosg::StringReader& r, bool is_
|
|||||||
this->box_item_class_prob_table = r.pget<parray<parray<uint8_t, 10>, 7>>(offsets.box_item_class_prob_table_offset);
|
this->box_item_class_prob_table = r.pget<parray<parray<uint8_t, 10>, 7>>(offsets.box_item_class_prob_table_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t CommonItemSet::key_for_table(Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) {
|
uint16_t CommonItemSet::key_for_table(Episode episode, GameMode mode, Difficulty difficulty, uint8_t secid) {
|
||||||
// Bits: -----EEEMMDDSSSS
|
// Bits: -----EEEMMDDSSSS
|
||||||
return (((static_cast<uint16_t>(episode) << 8) & 0x0700) |
|
return (((static_cast<uint16_t>(episode) << 8) & 0x0700) |
|
||||||
((static_cast<uint16_t>(mode) << 6) & 0x00C0) |
|
((static_cast<uint16_t>(mode) << 6) & 0x00C0) |
|
||||||
@@ -663,12 +654,12 @@ uint16_t CommonItemSet::key_for_table(Episode episode, GameMode mode, uint8_t di
|
|||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<const CommonItemSet::Table> CommonItemSet::get_table(
|
shared_ptr<const CommonItemSet::Table> CommonItemSet::get_table(
|
||||||
Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) const {
|
Episode episode, GameMode mode, Difficulty difficulty, uint8_t secid) const {
|
||||||
try {
|
try {
|
||||||
return this->tables.at(this->key_for_table(episode, mode, difficulty, secid));
|
return this->tables.at(this->key_for_table(episode, mode, difficulty, secid));
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
throw runtime_error(std::format("common item table not available for episode={}, mode={}, difficulty={}, secid={}",
|
throw runtime_error(std::format("common item table not available for episode={}, mode={}, difficulty={}, secid={}",
|
||||||
name_for_episode(episode), name_for_mode(mode), difficulty, secid));
|
name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), secid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,17 +669,20 @@ AFSV2CommonItemSet::AFSV2CommonItemSet(
|
|||||||
// Hard, etc.
|
// Hard, etc.
|
||||||
{
|
{
|
||||||
AFSArchive pt_afs(pt_afs_data);
|
AFSArchive pt_afs(pt_afs_data);
|
||||||
size_t max_difficulty;
|
bool include_ultimate;
|
||||||
if (pt_afs.num_entries() >= 40) {
|
if (pt_afs.num_entries() >= 40) {
|
||||||
max_difficulty = 4;
|
include_ultimate = true;
|
||||||
} else if (pt_afs.num_entries() >= 30) {
|
} else if (pt_afs.num_entries() >= 30) {
|
||||||
max_difficulty = 3;
|
include_ultimate = false;
|
||||||
} else {
|
} else {
|
||||||
throw std::runtime_error(std::format("PT AFS file has unexpected entry count ({})", pt_afs.num_entries()));
|
throw std::runtime_error(std::format("PT AFS file has unexpected entry count ({})", pt_afs.num_entries()));
|
||||||
}
|
}
|
||||||
for (size_t difficulty = 0; difficulty < max_difficulty; difficulty++) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
|
if ((difficulty == Difficulty::ULTIMATE) && !include_ultimate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||||
auto entry = pt_afs.get(difficulty * 10 + section_id);
|
auto entry = pt_afs.get(static_cast<size_t>(difficulty) * 10 + section_id);
|
||||||
phosg::StringReader r(entry.first, entry.second);
|
phosg::StringReader r(entry.first, entry.second);
|
||||||
auto table = make_shared<Table>(r, false, false, Episode::EP1);
|
auto table = make_shared<Table>(r, false, false, Episode::EP1);
|
||||||
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::NORMAL, difficulty, section_id), table);
|
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::NORMAL, difficulty, section_id), table);
|
||||||
@@ -702,16 +696,19 @@ AFSV2CommonItemSet::AFSV2CommonItemSet(
|
|||||||
// 30th are used (section_id is ignored)
|
// 30th are used (section_id is ignored)
|
||||||
if (ct_afs_data) {
|
if (ct_afs_data) {
|
||||||
AFSArchive ct_afs(ct_afs_data);
|
AFSArchive ct_afs(ct_afs_data);
|
||||||
size_t max_difficulty;
|
bool include_ultimate;
|
||||||
if (ct_afs.num_entries() >= 40) {
|
if (ct_afs.num_entries() >= 40) {
|
||||||
max_difficulty = 4;
|
include_ultimate = true;
|
||||||
} else if (ct_afs.num_entries() >= 30) {
|
} else if (ct_afs.num_entries() >= 30) {
|
||||||
max_difficulty = 3;
|
include_ultimate = false;
|
||||||
} else {
|
} else {
|
||||||
throw std::runtime_error(std::format("CT AFS file has unexpected entry count ({})", ct_afs.num_entries()));
|
throw std::runtime_error(std::format("CT AFS file has unexpected entry count ({})", ct_afs.num_entries()));
|
||||||
}
|
}
|
||||||
for (size_t difficulty = 0; difficulty < max_difficulty; difficulty++) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
auto r = ct_afs.get_reader(difficulty * 10);
|
if ((difficulty == Difficulty::ULTIMATE) && !include_ultimate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto r = ct_afs.get_reader(static_cast<size_t>(difficulty) * 10);
|
||||||
auto table = make_shared<Table>(r, false, false, Episode::EP1);
|
auto table = make_shared<Table>(r, false, false, Episode::EP1);
|
||||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||||
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::CHALLENGE, difficulty, section_id), table);
|
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::CHALLENGE, difficulty, section_id), table);
|
||||||
@@ -723,7 +720,7 @@ AFSV2CommonItemSet::AFSV2CommonItemSet(
|
|||||||
GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gsl_data, bool is_big_endian) {
|
GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gsl_data, bool is_big_endian) {
|
||||||
GSLArchive gsl(gsl_data, is_big_endian);
|
GSLArchive gsl(gsl_data, is_big_endian);
|
||||||
|
|
||||||
auto filename_for_table = +[](Episode episode, uint8_t difficulty, uint8_t section_id, bool is_challenge) -> string {
|
auto filename_for_table = +[](Episode episode, Difficulty difficulty, uint8_t section_id, bool is_challenge) -> string {
|
||||||
const char* episode_token = "";
|
const char* episode_token = "";
|
||||||
switch (episode) {
|
switch (episode) {
|
||||||
case Episode::EP1:
|
case Episode::EP1:
|
||||||
@@ -746,9 +743,8 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gs
|
|||||||
section_id);
|
section_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
vector<Episode> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
for (Episode episode : ALL_EPISODES_V4) {
|
||||||
for (Episode episode : episodes) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
|
|
||||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||||
phosg::StringReader r;
|
phosg::StringReader r;
|
||||||
try {
|
try {
|
||||||
@@ -773,7 +769,7 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gs
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (episode != Episode::EP4) {
|
if (episode != Episode::EP4) {
|
||||||
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
try {
|
try {
|
||||||
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
|
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
|
||||||
auto table = make_shared<Table>(r, is_big_endian, true, episode);
|
auto table = make_shared<Table>(r, is_big_endian, true, episode);
|
||||||
@@ -800,9 +796,9 @@ JSONCommonItemSet::JSONCommonItemSet(const phosg::JSON& json) {
|
|||||||
Episode episode = episode_keys.at(episode_it.first);
|
Episode episode = episode_keys.at(episode_it.first);
|
||||||
|
|
||||||
for (const auto& difficulty_it : episode_it.second->as_dict()) {
|
for (const auto& difficulty_it : episode_it.second->as_dict()) {
|
||||||
static const unordered_map<string, uint8_t> difficulty_keys(
|
static const unordered_map<string, Difficulty> difficulty_keys(
|
||||||
{{"Normal", 0}, {"Hard", 1}, {"VeryHard", 2}, {"Ultimate", 3}});
|
{{"Normal", Difficulty::NORMAL}, {"Hard", Difficulty::HARD}, {"VeryHard", Difficulty::VERY_HARD}, {"Ultimate", Difficulty::ULTIMATE}});
|
||||||
uint8_t difficulty = difficulty_keys.at(difficulty_it.first);
|
Difficulty difficulty = difficulty_keys.at(difficulty_it.first);
|
||||||
|
|
||||||
for (const auto& section_id_it : difficulty_it.second->as_dict()) {
|
for (const auto& section_id_it : difficulty_it.second->as_dict()) {
|
||||||
uint8_t section_id = section_id_for_name(section_id_it.first);
|
uint8_t section_id = section_id_for_name(section_id_it.first);
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ public:
|
|||||||
bool operator==(const CommonItemSet& other) const = default;
|
bool operator==(const CommonItemSet& other) const = default;
|
||||||
bool operator!=(const CommonItemSet& other) const = default;
|
bool operator!=(const CommonItemSet& other) const = default;
|
||||||
|
|
||||||
std::shared_ptr<const Table> get_table(Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) const;
|
std::shared_ptr<const Table> get_table(Episode episode, GameMode mode, Difficulty difficulty, uint8_t secid) const;
|
||||||
phosg::JSON json() const;
|
phosg::JSON json() const;
|
||||||
void print(FILE* stream) const;
|
void print(FILE* stream) const;
|
||||||
void print_diff(FILE* stream, const CommonItemSet& other) const;
|
void print_diff(FILE* stream, const CommonItemSet& other) const;
|
||||||
@@ -279,7 +279,7 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
CommonItemSet() = default;
|
CommonItemSet() = default;
|
||||||
|
|
||||||
static uint16_t key_for_table(Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid);
|
static uint16_t key_for_table(Episode episode, GameMode mode, Difficulty difficulty, uint8_t secid);
|
||||||
|
|
||||||
std::unordered_map<uint16_t, std::shared_ptr<Table>> tables;
|
std::unordered_map<uint16_t, std::shared_ptr<Table>> tables;
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-10
@@ -40,7 +40,7 @@ DownloadSession::DownloadSession(
|
|||||||
uint16_t remote_port,
|
uint16_t remote_port,
|
||||||
const std::string& output_dir,
|
const std::string& output_dir,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file,
|
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file,
|
||||||
uint32_t serial_number2,
|
uint32_t serial_number2,
|
||||||
uint32_t serial_number,
|
uint32_t serial_number,
|
||||||
@@ -222,13 +222,13 @@ void DownloadSession::send_61_98(bool is_98) {
|
|||||||
if (is_v1(this->version)) {
|
if (is_v1(this->version)) {
|
||||||
C_CharacterData_DCv1_61_98 ret;
|
C_CharacterData_DCv1_61_98 ret;
|
||||||
ret.inventory = this->character->inventory;
|
ret.inventory = this->character->inventory;
|
||||||
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
|
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, Language::ENGLISH, Language::ENGLISH);
|
||||||
this->channel->send(command, 0x01, ret);
|
this->channel->send(command, 0x01, ret);
|
||||||
|
|
||||||
} else if (this->version == Version::DC_V2) {
|
} else if (this->version == Version::DC_V2) {
|
||||||
C_CharacterData_DCv2_61_98 ret;
|
C_CharacterData_DCv2_61_98 ret;
|
||||||
ret.inventory = this->character->inventory;
|
ret.inventory = this->character->inventory;
|
||||||
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
|
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, Language::ENGLISH, Language::ENGLISH);
|
||||||
ret.records.challenge = this->character->challenge_records;
|
ret.records.challenge = this->character->challenge_records;
|
||||||
ret.records.battle = this->character->battle_records;
|
ret.records.battle = this->character->battle_records;
|
||||||
ret.choice_search_config = this->character->choice_search_config;
|
ret.choice_search_config = this->character->choice_search_config;
|
||||||
@@ -237,7 +237,7 @@ void DownloadSession::send_61_98(bool is_98) {
|
|||||||
} else if (this->version == Version::PC_V2) {
|
} else if (this->version == Version::PC_V2) {
|
||||||
C_CharacterData_PC_61_98 ret;
|
C_CharacterData_PC_61_98 ret;
|
||||||
ret.inventory = this->character->inventory;
|
ret.inventory = this->character->inventory;
|
||||||
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
|
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, Language::ENGLISH, Language::ENGLISH);
|
||||||
ret.records.challenge = this->character->challenge_records;
|
ret.records.challenge = this->character->challenge_records;
|
||||||
ret.records.battle = this->character->battle_records;
|
ret.records.battle = this->character->battle_records;
|
||||||
ret.choice_search_config = this->character->choice_search_config;
|
ret.choice_search_config = this->character->choice_search_config;
|
||||||
@@ -246,7 +246,7 @@ void DownloadSession::send_61_98(bool is_98) {
|
|||||||
} else if (is_v3(this->version)) {
|
} else if (is_v3(this->version)) {
|
||||||
C_CharacterData_V3_61_98 ret;
|
C_CharacterData_V3_61_98 ret;
|
||||||
ret.inventory = this->character->inventory;
|
ret.inventory = this->character->inventory;
|
||||||
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
|
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, Language::ENGLISH, Language::ENGLISH);
|
||||||
ret.records.challenge = this->character->challenge_records;
|
ret.records.challenge = this->character->challenge_records;
|
||||||
ret.records.battle = this->character->battle_records;
|
ret.records.battle = this->character->battle_records;
|
||||||
ret.choice_search_config = this->character->choice_search_config;
|
ret.choice_search_config = this->character->choice_search_config;
|
||||||
@@ -552,7 +552,7 @@ asio::awaitable<void> DownloadSession::on_message(Channel::Message& msg) {
|
|||||||
C_CreateGame_PC_C1 ret;
|
C_CreateGame_PC_C1 ret;
|
||||||
ret.name.encode(random_name());
|
ret.name.encode(random_name());
|
||||||
ret.password.encode(random_name());
|
ret.password.encode(random_name());
|
||||||
ret.difficulty = 0;
|
ret.difficulty = Difficulty::NORMAL;
|
||||||
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
|
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
|
||||||
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
|
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
|
||||||
ret.episode = 1;
|
ret.episode = 1;
|
||||||
@@ -562,7 +562,7 @@ asio::awaitable<void> DownloadSession::on_message(Channel::Message& msg) {
|
|||||||
C_CreateGame_DC_V3_0C_C1_Ep3_EC ret;
|
C_CreateGame_DC_V3_0C_C1_Ep3_EC ret;
|
||||||
ret.name.encode(random_name());
|
ret.name.encode(random_name());
|
||||||
ret.password.encode(random_name());
|
ret.password.encode(random_name());
|
||||||
ret.difficulty = 0;
|
ret.difficulty = Difficulty::NORMAL;
|
||||||
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
|
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
|
||||||
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
|
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
|
||||||
if (is_v1(this->version)) {
|
if (is_v1(this->version)) {
|
||||||
@@ -582,7 +582,7 @@ asio::awaitable<void> DownloadSession::on_message(Channel::Message& msg) {
|
|||||||
C_CreateGame_BB_C1 ret;
|
C_CreateGame_BB_C1 ret;
|
||||||
ret.name.encode(random_name());
|
ret.name.encode(random_name());
|
||||||
ret.password.encode(random_name());
|
ret.password.encode(random_name());
|
||||||
ret.difficulty = 0;
|
ret.difficulty = Difficulty::NORMAL;
|
||||||
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
|
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
|
||||||
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
|
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
|
||||||
if (game_config.episode == Episode::EP1) {
|
if (game_config.episode == Episode::EP1) {
|
||||||
@@ -651,12 +651,12 @@ asio::awaitable<void> DownloadSession::on_message(Channel::Message& msg) {
|
|||||||
filtered_name.push_back((isalnum(ch) || (ch == '-') || (ch == '.') || (ch == '_')) ? ch : '_');
|
filtered_name.push_back((isalnum(ch) || (ch == '-') || (ch == '.') || (ch == '_')) ? ch : '_');
|
||||||
}
|
}
|
||||||
string local_filename = std::format(
|
string local_filename = std::format(
|
||||||
"{}/{:016X}_{}_{}_{}_{}",
|
"{}/{:016X}_{}_{}_{:c}_{}",
|
||||||
this->output_dir,
|
this->output_dir,
|
||||||
this->current_request,
|
this->current_request,
|
||||||
phosg::now(),
|
phosg::now(),
|
||||||
phosg::name_for_enum(this->version),
|
phosg::name_for_enum(this->version),
|
||||||
char_for_language_code(this->language),
|
char_for_language(this->language),
|
||||||
filtered_name);
|
filtered_name);
|
||||||
this->open_files.emplace(internal_name, OpenFile{.request = this->current_request, .filename = local_filename, .total_size = cmd.file_size, .data = ""});
|
this->open_files.emplace(internal_name, OpenFile{.request = this->current_request, .filename = local_filename, .total_size = cmd.file_size, .data = ""});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public:
|
|||||||
uint16_t remote_port,
|
uint16_t remote_port,
|
||||||
const std::string& output_dir,
|
const std::string& output_dir,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file,
|
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file,
|
||||||
uint32_t serial_number2,
|
uint32_t serial_number2,
|
||||||
uint32_t serial_number,
|
uint32_t serial_number,
|
||||||
@@ -50,7 +50,7 @@ protected:
|
|||||||
uint16_t remote_port;
|
uint16_t remote_port;
|
||||||
std::string output_dir;
|
std::string output_dir;
|
||||||
Version version;
|
Version version;
|
||||||
uint8_t language;
|
Language language;
|
||||||
bool show_command_data;
|
bool show_command_data;
|
||||||
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file;
|
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file;
|
||||||
uint32_t serial_number;
|
uint32_t serial_number;
|
||||||
|
|||||||
@@ -174,21 +174,6 @@ string BattleRecord::serialize() const {
|
|||||||
return std::move(w.str());
|
return std::move(w.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BattleRecord::writable() const {
|
|
||||||
return this->is_writable;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BattleRecord::battle_in_progress() const {
|
|
||||||
return (this->battle_start_timestamp != 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const BattleRecord::Event* BattleRecord::get_first_event() const {
|
|
||||||
if (this->events.empty()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return &this->events.front();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BattleRecord::add_player(
|
void BattleRecord::add_player(
|
||||||
const PlayerLobbyDataDCGC& lobby_data,
|
const PlayerLobbyDataDCGC& lobby_data,
|
||||||
const PlayerInventory& inventory,
|
const PlayerInventory& inventory,
|
||||||
@@ -256,16 +241,6 @@ void BattleRecord::add_random_data(const void* data, size_t size) {
|
|||||||
this->random_stream.append(reinterpret_cast<const char*>(data), size);
|
this->random_stream.append(reinterpret_cast<const char*>(data), size);
|
||||||
}
|
}
|
||||||
|
|
||||||
vector<string> BattleRecord::get_all_server_data_commands() const {
|
|
||||||
vector<string> ret;
|
|
||||||
for (const auto& event : this->events) {
|
|
||||||
if (event.type == Event::Type::SERVER_DATA_COMMAND) {
|
|
||||||
ret.emplace_back(event.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const string& BattleRecord::get_random_stream() const {
|
const string& BattleRecord::get_random_stream() const {
|
||||||
return this->random_stream;
|
return this->random_stream;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,10 +62,25 @@ public:
|
|||||||
explicit BattleRecord(const std::string& data);
|
explicit BattleRecord(const std::string& data);
|
||||||
std::string serialize() const;
|
std::string serialize() const;
|
||||||
|
|
||||||
bool writable() const;
|
inline bool writable() const {
|
||||||
bool battle_in_progress() const;
|
return this->is_writable;
|
||||||
|
}
|
||||||
|
|
||||||
const Event* get_first_event() const;
|
inline uint32_t get_behavior_flags() const {
|
||||||
|
return this->behavior_flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool battle_in_progress() const {
|
||||||
|
return (this->battle_start_timestamp != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const Event* get_first_event() const {
|
||||||
|
return this->events.empty() ? nullptr : &this->events.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::deque<Event> get_all_events() const {
|
||||||
|
return this->events;
|
||||||
|
}
|
||||||
|
|
||||||
void add_player(
|
void add_player(
|
||||||
const PlayerLobbyDataDCGC& lobby_data,
|
const PlayerLobbyDataDCGC& lobby_data,
|
||||||
@@ -86,7 +101,6 @@ public:
|
|||||||
|
|
||||||
void print(FILE* stream) const;
|
void print(FILE* stream) const;
|
||||||
|
|
||||||
std::vector<std::string> get_all_server_data_commands() const;
|
|
||||||
const std::string& get_random_stream() const;
|
const std::string& get_random_stream() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
+127
-47
@@ -1720,7 +1720,7 @@ phosg::JSON MapDefinition::CameraSpec::json() const {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
phosg::JSON MapDefinition::NPCDeck::json(uint8_t language) const {
|
phosg::JSON MapDefinition::NPCDeck::json(Language language) const {
|
||||||
phosg::JSON card_ids_json = phosg::JSON::list();
|
phosg::JSON card_ids_json = phosg::JSON::list();
|
||||||
for (size_t z = 0; z < this->card_ids.size(); z++) {
|
for (size_t z = 0; z < this->card_ids.size(); z++) {
|
||||||
if (this->card_ids[z] != 0xFFFF) {
|
if (this->card_ids[z] != 0xFFFF) {
|
||||||
@@ -1733,7 +1733,7 @@ phosg::JSON MapDefinition::NPCDeck::json(uint8_t language) const {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
phosg::JSON MapDefinition::AIParams::json(uint8_t language) const {
|
phosg::JSON MapDefinition::AIParams::json(Language language) const {
|
||||||
phosg::JSON params_json = phosg::JSON::list();
|
phosg::JSON params_json = phosg::JSON::list();
|
||||||
for (size_t z = 0; z < this->params.size(); z++) {
|
for (size_t z = 0; z < this->params.size(); z++) {
|
||||||
params_json.emplace_back(this->params[z].load());
|
params_json.emplace_back(this->params[z].load());
|
||||||
@@ -1745,7 +1745,7 @@ phosg::JSON MapDefinition::AIParams::json(uint8_t language) const {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
phosg::JSON MapDefinition::DialogueSet::json(uint8_t language) const {
|
phosg::JSON MapDefinition::DialogueSet::json(Language language) const {
|
||||||
phosg::JSON strings_json = phosg::JSON::list();
|
phosg::JSON strings_json = phosg::JSON::list();
|
||||||
for (size_t z = 0; z < this->strings.size(); z++) {
|
for (size_t z = 0; z < this->strings.size(); z++) {
|
||||||
strings_json.emplace_back(this->strings[z].decode(language));
|
strings_json.emplace_back(this->strings[z].decode(language));
|
||||||
@@ -1818,7 +1818,7 @@ string MapDefinition::CameraSpec::str() const {
|
|||||||
this->unknown_a2[1], this->unknown_a2[2]);
|
this->unknown_a2[1], this->unknown_a2[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
string MapDefinition::str(const CardIndex* card_index, uint8_t language) const {
|
string MapDefinition::str(const CardIndex* card_index, Language language) const {
|
||||||
deque<string> lines;
|
deque<string> lines;
|
||||||
auto add_map = [&](const parray<parray<uint8_t, 0x10>, 0x10>& tiles) {
|
auto add_map = [&](const parray<parray<uint8_t, 0x10>, 0x10>& tiles) {
|
||||||
for (size_t y = 0; y < this->height; y++) {
|
for (size_t y = 0; y < this->height; y++) {
|
||||||
@@ -2503,7 +2503,7 @@ CardIndex::CardIndex(
|
|||||||
|
|
||||||
// Some cards intentionally have the same name, so we just leave them
|
// Some cards intentionally have the same name, so we just leave them
|
||||||
// unindexed (they can still be looked up by ID, of course)
|
// unindexed (they can still be looked up by ID, of course)
|
||||||
string name = entry->def.en_name.decode(1);
|
string name = entry->def.en_name.decode(Language::ENGLISH);
|
||||||
this->card_definitions_by_name.emplace(name, entry);
|
this->card_definitions_by_name.emplace(name, entry);
|
||||||
this->card_definitions_by_name_normalized.emplace(this->normalize_card_name(name), entry);
|
this->card_definitions_by_name_normalized.emplace(this->normalize_card_name(name), entry);
|
||||||
|
|
||||||
@@ -2620,11 +2620,11 @@ string CardIndex::normalize_card_name(const string& name) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
MapIndex::VersionedMap::VersionedMap(shared_ptr<const MapDefinition> map, uint8_t language)
|
MapIndex::VersionedMap::VersionedMap(shared_ptr<const MapDefinition> map, Language language)
|
||||||
: map(map),
|
: map(map),
|
||||||
language(language) {}
|
language(language) {}
|
||||||
|
|
||||||
MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t language)
|
MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, Language language)
|
||||||
: language(language),
|
: language(language),
|
||||||
compressed_data(make_shared<string>(std::move(compressed_data))) {
|
compressed_data(make_shared<string>(std::move(compressed_data))) {
|
||||||
string decompressed = prs_decompress(*this->compressed_data);
|
string decompressed = prs_decompress(*this->compressed_data);
|
||||||
@@ -2670,36 +2670,43 @@ std::shared_ptr<const std::string> MapIndex::VersionedMap::trial_download() cons
|
|||||||
return this->download_data_trial;
|
return this->download_data_trial;
|
||||||
}
|
}
|
||||||
|
|
||||||
MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version)
|
MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version, uint8_t visibility_flags)
|
||||||
: map_number(initial_version->map->map_number),
|
: map_number(initial_version->map->map_number),
|
||||||
|
visibility_flags(visibility_flags),
|
||||||
initial_version(initial_version) {
|
initial_version(initial_version) {
|
||||||
this->versions.resize(this->initial_version->language + 1);
|
size_t lang_index = static_cast<size_t>(this->initial_version->language);
|
||||||
this->versions[this->initial_version->language] = initial_version;
|
this->versions.resize(lang_index + 1);
|
||||||
|
this->versions[lang_index] = initial_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MapIndex::Map::add_version(std::shared_ptr<const VersionedMap> vm) {
|
void MapIndex::Map::add_version(std::shared_ptr<const VersionedMap> vm) {
|
||||||
if (this->versions.size() <= vm->language) {
|
size_t lang_index = static_cast<size_t>(vm->language);
|
||||||
this->versions.resize(vm->language + 1);
|
if (this->versions.size() <= lang_index) {
|
||||||
|
this->versions.resize(lang_index + 1);
|
||||||
}
|
}
|
||||||
if (this->versions[vm->language]) {
|
if (this->versions[lang_index]) {
|
||||||
throw runtime_error("map version already exists");
|
throw runtime_error("map version already exists");
|
||||||
}
|
}
|
||||||
this->initial_version->map->assert_semantically_equivalent(*vm->map);
|
this->initial_version->map->assert_semantically_equivalent(*vm->map);
|
||||||
this->versions[vm->language] = vm;
|
this->versions[lang_index] = vm;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MapIndex::Map::has_version(uint8_t language) const {
|
bool MapIndex::Map::has_version(Language language) const {
|
||||||
return (this->versions.size() > language) && !!this->versions[language];
|
size_t lang_index = static_cast<size_t>(language);
|
||||||
|
return (this->versions.size() > lang_index) && !!this->versions[lang_index];
|
||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(uint8_t language) const {
|
shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(Language language) const {
|
||||||
|
size_t lang_index = static_cast<size_t>(language);
|
||||||
|
|
||||||
// If the requested language exists, return it
|
// If the requested language exists, return it
|
||||||
if ((language < this->versions.size()) && this->versions[language]) {
|
if ((lang_index < this->versions.size()) && this->versions[lang_index]) {
|
||||||
return this->versions[language];
|
return this->versions[lang_index];
|
||||||
}
|
}
|
||||||
// If English exists, return it
|
// If English exists, return it
|
||||||
if ((1 < this->versions.size()) && this->versions[1]) {
|
constexpr size_t english_lang_index = static_cast<size_t>(Language::ENGLISH);
|
||||||
return this->versions[1];
|
if ((english_lang_index < this->versions.size()) && this->versions[english_lang_index]) {
|
||||||
|
return this->versions[english_lang_index];
|
||||||
}
|
}
|
||||||
// Return the first version that exists
|
// Return the first version that exists
|
||||||
for (const auto& vm : this->versions) {
|
for (const auto& vm : this->versions) {
|
||||||
@@ -2712,40 +2719,47 @@ shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(uint8_t language
|
|||||||
throw logic_error("no map versions exist");
|
throw logic_error("no map versions exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
MapIndex::MapIndex(const string& directory) {
|
MapIndex::Category::Category(uint32_t category_id, const phosg::JSON& json)
|
||||||
|
: category_id(category_id),
|
||||||
|
visibility_flags(json.get_int("VisibilityFlags")),
|
||||||
|
name(json.get_string("Name", "")),
|
||||||
|
description(json.get_string("Description", "")) {}
|
||||||
|
|
||||||
|
MapIndex::MapIndex(const string& directory, bool raise_on_any_failure) {
|
||||||
map<uint32_t, shared_ptr<Map>> mutable_maps;
|
map<uint32_t, shared_ptr<Map>> mutable_maps;
|
||||||
for (const auto& item : std::filesystem::directory_iterator(directory)) {
|
|
||||||
string filename = item.path().filename().string();
|
auto try_add_map_file = [&](std::shared_ptr<Category> category, const std::string& file_path) -> void {
|
||||||
try {
|
try {
|
||||||
|
string filename = phosg::basename(file_path);
|
||||||
string base_filename;
|
string base_filename;
|
||||||
string compressed_data;
|
string compressed_data;
|
||||||
shared_ptr<MapDefinition> decompressed_data;
|
shared_ptr<MapDefinition> decompressed_data;
|
||||||
if (filename.ends_with(".mnmd") || filename.ends_with(".bind")) {
|
if (filename.ends_with(".mnmd") || filename.ends_with(".bind")) {
|
||||||
decompressed_data = make_shared<MapDefinition>(phosg::load_object_file<MapDefinition>(directory + "/" + filename));
|
decompressed_data = make_shared<MapDefinition>(phosg::load_object_file<MapDefinition>(file_path));
|
||||||
base_filename = filename.substr(0, filename.size() - 5);
|
base_filename = filename.substr(0, filename.size() - 5);
|
||||||
} else if (filename.ends_with(".mnm") || filename.ends_with(".bin")) {
|
} else if (filename.ends_with(".mnm") || filename.ends_with(".bin")) {
|
||||||
compressed_data = phosg::load_file(directory + "/" + filename);
|
compressed_data = phosg::load_file(file_path);
|
||||||
base_filename = filename.substr(0, filename.size() - 4);
|
base_filename = filename.substr(0, filename.size() - 4);
|
||||||
} else if (filename.ends_with(".bin.gci") || filename.ends_with(".mnm.gci")) {
|
} else if (filename.ends_with(".bin.gci") || filename.ends_with(".mnm.gci")) {
|
||||||
compressed_data = decode_gci_data(phosg::load_file(directory + "/" + filename));
|
compressed_data = decode_gci_data(phosg::load_file(file_path));
|
||||||
base_filename = filename.substr(0, filename.size() - 8);
|
base_filename = filename.substr(0, filename.size() - 8);
|
||||||
} else if (filename.ends_with(".gci")) {
|
} else if (filename.ends_with(".gci")) {
|
||||||
compressed_data = decode_gci_data(phosg::load_file(directory + "/" + filename));
|
compressed_data = decode_gci_data(phosg::load_file(file_path));
|
||||||
base_filename = filename.substr(0, filename.size() - 4);
|
base_filename = filename.substr(0, filename.size() - 4);
|
||||||
} else if (filename.ends_with(".bin.vms") || filename.ends_with(".mnm.vms")) {
|
} else if (filename.ends_with(".bin.vms") || filename.ends_with(".mnm.vms")) {
|
||||||
compressed_data = decode_vms_data(phosg::load_file(directory + "/" + filename));
|
compressed_data = decode_vms_data(phosg::load_file(file_path));
|
||||||
base_filename = filename.substr(0, filename.size() - 8);
|
base_filename = filename.substr(0, filename.size() - 8);
|
||||||
} else if (filename.ends_with(".vms")) {
|
} else if (filename.ends_with(".vms")) {
|
||||||
compressed_data = decode_vms_data(phosg::load_file(directory + "/" + filename));
|
compressed_data = decode_vms_data(phosg::load_file(file_path));
|
||||||
base_filename = filename.substr(0, filename.size() - 4);
|
base_filename = filename.substr(0, filename.size() - 4);
|
||||||
} else if (filename.ends_with(".bin.dlq") || filename.ends_with(".mnm.dlq")) {
|
} else if (filename.ends_with(".bin.dlq") || filename.ends_with(".mnm.dlq")) {
|
||||||
compressed_data = decode_dlq_data(phosg::load_file(directory + "/" + filename));
|
compressed_data = decode_dlq_data(phosg::load_file(file_path));
|
||||||
base_filename = filename.substr(0, filename.size() - 8);
|
base_filename = filename.substr(0, filename.size() - 8);
|
||||||
} else if (filename.ends_with(".dlq")) {
|
} else if (filename.ends_with(".dlq")) {
|
||||||
compressed_data = decode_dlq_data(phosg::load_file(directory + "/" + filename));
|
compressed_data = decode_dlq_data(phosg::load_file(file_path));
|
||||||
base_filename = filename.substr(0, filename.size() - 4);
|
base_filename = filename.substr(0, filename.size() - 4);
|
||||||
} else {
|
} else {
|
||||||
continue; // Silently skip file
|
return; // Silently skip file
|
||||||
}
|
}
|
||||||
|
|
||||||
if (base_filename.size() < 2) {
|
if (base_filename.size() < 2) {
|
||||||
@@ -2754,7 +2768,7 @@ MapIndex::MapIndex(const string& directory) {
|
|||||||
if (base_filename[base_filename.size() - 2] != '-') {
|
if (base_filename[base_filename.size() - 2] != '-') {
|
||||||
throw runtime_error("language code not present");
|
throw runtime_error("language code not present");
|
||||||
}
|
}
|
||||||
uint8_t language = language_code_for_char(base_filename[base_filename.size() - 1]);
|
Language language = language_for_char(base_filename[base_filename.size() - 1]);
|
||||||
|
|
||||||
shared_ptr<VersionedMap> vm;
|
shared_ptr<VersionedMap> vm;
|
||||||
if (decompressed_data) {
|
if (decompressed_data) {
|
||||||
@@ -2765,36 +2779,90 @@ MapIndex::MapIndex(const string& directory) {
|
|||||||
throw runtime_error("unknown map file format");
|
throw runtime_error("unknown map file format");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t visibility_flags = category ? category->visibility_flags : 0x00;
|
||||||
|
|
||||||
string name = vm->map->name.decode(vm->language);
|
string name = vm->map->name.decode(vm->language);
|
||||||
auto map_it = mutable_maps.find(vm->map->map_number);
|
auto map_it = mutable_maps.find(vm->map->map_number);
|
||||||
if (map_it == mutable_maps.end()) {
|
if (map_it == mutable_maps.end()) {
|
||||||
map_it = mutable_maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first;
|
map_it = mutable_maps.emplace(vm->map->map_number, make_shared<Map>(vm, visibility_flags)).first;
|
||||||
this->maps.emplace(vm->map->map_number, map_it->second);
|
this->maps.emplace(vm->map->map_number, map_it->second);
|
||||||
static_game_data_log.debug_f("({}) Created Episode 3 map {:08X} {} ({}; {})",
|
|
||||||
|
string in_category_str;
|
||||||
|
if (category) {
|
||||||
|
in_category_str = std::format(" in category {}", category->name);
|
||||||
|
category->add_map(map_it->second);
|
||||||
|
}
|
||||||
|
static_game_data_log.debug_f("({}) Created Episode 3 map {:08X} {}{} ({}; {})",
|
||||||
filename,
|
filename,
|
||||||
vm->map->map_number,
|
vm->map->map_number,
|
||||||
char_for_language_code(vm->language),
|
char_for_language(vm->language),
|
||||||
|
in_category_str,
|
||||||
vm->map->is_quest() ? "quest" : "free",
|
vm->map->is_quest() ? "quest" : "free",
|
||||||
name);
|
name);
|
||||||
} else {
|
} else {
|
||||||
|
if (map_it->second->visibility_flags != visibility_flags) {
|
||||||
|
throw std::runtime_error(std::format("visibility flags {:02X} for added map {} do not match existing flags {}",
|
||||||
|
map_it->second->visibility_flags, file_path, visibility_flags));
|
||||||
|
}
|
||||||
map_it->second->add_version(vm);
|
map_it->second->add_version(vm);
|
||||||
static_game_data_log.debug_f("({}) Added Episode 3 map version {:08X} {} ({}; {})",
|
static_game_data_log.debug_f("({}) Added Episode 3 map version {:08X} {} ({}; {})",
|
||||||
filename,
|
filename,
|
||||||
vm->map->map_number,
|
vm->map->map_number,
|
||||||
char_for_language_code(vm->language),
|
char_for_language(vm->language),
|
||||||
vm->map->is_quest() ? "quest" : "free",
|
vm->map->is_quest() ? "quest" : "free",
|
||||||
name);
|
name);
|
||||||
}
|
}
|
||||||
this->maps_by_name.emplace(vm->map->name.decode(vm->language), map_it->second);
|
this->maps_by_name.emplace(vm->map->name.decode(vm->language), map_it->second);
|
||||||
|
|
||||||
} catch (const exception& e) {
|
} catch (const exception& e) {
|
||||||
static_game_data_log.warning_f("Failed to index Episode 3 map {}: {}",
|
if (raise_on_any_failure) {
|
||||||
filename, e.what());
|
throw;
|
||||||
|
}
|
||||||
|
static_game_data_log.warning_f("Failed to index Episode 3 map {}: {}", file_path, e.what());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::filesystem::directory_entry> cat_items;
|
||||||
|
for (const auto& cat_item : std::filesystem::directory_iterator(directory)) {
|
||||||
|
cat_items.emplace_back(cat_item);
|
||||||
|
}
|
||||||
|
auto sort_fn = +[](const std::filesystem::directory_entry& a, const std::filesystem::directory_entry& b) -> bool {
|
||||||
|
return a.path().filename().string() < b.path().filename().string();
|
||||||
|
};
|
||||||
|
sort(cat_items.begin(), cat_items.end(), sort_fn);
|
||||||
|
|
||||||
|
for (const auto& cat_item : cat_items) {
|
||||||
|
string cat_dir_path = cat_item.path().string();
|
||||||
|
|
||||||
|
if (cat_item.is_directory()) {
|
||||||
|
shared_ptr<Category> category;
|
||||||
|
try {
|
||||||
|
string json_filename = std::format("{}/{}", cat_item.path().string(), "category.json");
|
||||||
|
auto category_json = phosg::JSON::parse(phosg::load_file(json_filename));
|
||||||
|
uint32_t category_id = this->categories.size() + 1;
|
||||||
|
auto category = make_shared<Category>(category_id, category_json);
|
||||||
|
this->categories.emplace(category_id, category);
|
||||||
|
static_game_data_log.debug_f("({}) Created Episode 3 map category {:08X} ({})",
|
||||||
|
cat_item.path().filename().string(), category_id, category->name);
|
||||||
|
|
||||||
|
for (const auto& map_item : std::filesystem::directory_iterator(cat_item)) {
|
||||||
|
try_add_map_file(category, map_item.path().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (const exception& e) {
|
||||||
|
if (raise_on_any_failure) {
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
static_game_data_log.warning_f("Failed to index Episode 3 map category {}: {}", cat_item.path().string(), e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
try_add_map_file(nullptr, cat_dir_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language) const {
|
const string& MapIndex::get_compressed_list(size_t num_players, Language language, bool is_trial) const {
|
||||||
if (num_players == 0) {
|
if (num_players == 0) {
|
||||||
throw runtime_error("cannot generate map list for no players");
|
throw runtime_error("cannot generate map list for no players");
|
||||||
}
|
}
|
||||||
@@ -2802,16 +2870,27 @@ const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language
|
|||||||
throw logic_error("player count is too high in map list generation");
|
throw logic_error("player count is too high in map list generation");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (language >= this->compressed_map_lists.size()) {
|
auto& compressed_lists = is_trial ? this->compressed_map_lists_trial : this->compressed_map_lists_final;
|
||||||
this->compressed_map_lists.resize(language + 1);
|
|
||||||
|
size_t lang_index = static_cast<size_t>(language);
|
||||||
|
if (lang_index >= compressed_lists.size()) {
|
||||||
|
compressed_lists.resize(lang_index + 1);
|
||||||
}
|
}
|
||||||
string& compressed_map_list = this->compressed_map_lists[language].at(num_players - 1);
|
string& compressed_map_list = compressed_lists[lang_index].at(num_players - 1);
|
||||||
if (compressed_map_list.empty()) {
|
if (compressed_map_list.empty()) {
|
||||||
phosg::StringWriter entries_w;
|
phosg::StringWriter entries_w;
|
||||||
phosg::StringWriter strings_w;
|
phosg::StringWriter strings_w;
|
||||||
|
|
||||||
|
auto vis_flag = is_trial
|
||||||
|
? Episode3::MapIndex::VisibilityFlag::ONLINE_TRIAL
|
||||||
|
: Episode3::MapIndex::VisibilityFlag::ONLINE_FINAL;
|
||||||
|
|
||||||
size_t num_maps = 0;
|
size_t num_maps = 0;
|
||||||
for (const auto& map_it : this->maps) {
|
for (const auto& map_it : this->maps) {
|
||||||
|
if (!map_it.second->check_visibility_flag(vis_flag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
auto vm = map_it.second->version(language);
|
auto vm = map_it.second->version(language);
|
||||||
size_t map_num_players = 0;
|
size_t map_num_players = 0;
|
||||||
for (size_t z = 0; z < 4; z++) {
|
for (size_t z = 0; z < 4; z++) {
|
||||||
@@ -2868,11 +2947,12 @@ const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language
|
|||||||
compressed_w.write(prs.close());
|
compressed_w.write(prs.close());
|
||||||
compressed_map_list = std::move(compressed_w.str());
|
compressed_map_list = std::move(compressed_w.str());
|
||||||
if (compressed_map_list.size() > 0x7BEC) {
|
if (compressed_map_list.size() > 0x7BEC) {
|
||||||
throw runtime_error(std::format("compressed map list for {} players is too large (0x{:X} bytes)", num_players, compressed_map_list.size()));
|
throw runtime_error(std::format("compressed {} map list for {} players is too large (0x{:X} bytes)",
|
||||||
|
is_trial ? "trial" : "final", num_players, compressed_map_list.size()));
|
||||||
}
|
}
|
||||||
size_t decompressed_size = sizeof(header) + entries_w.size() + strings_w.size();
|
size_t decompressed_size = sizeof(header) + entries_w.size() + strings_w.size();
|
||||||
static_game_data_log.info_f("Generated Episode 3 compressed map list for {} player(s) ({} maps; 0x{:X} -> 0x{:X} bytes)",
|
static_game_data_log.info_f("Generated Episode 3 compressed {} map list for {} player(s) ({} maps; 0x{:X} -> 0x{:X} bytes)",
|
||||||
num_players, num_maps, decompressed_size, compressed_map_list.size());
|
is_trial ? "trial" : "final", num_players, num_maps, decompressed_size, compressed_map_list.size());
|
||||||
}
|
}
|
||||||
return compressed_map_list;
|
return compressed_map_list;
|
||||||
}
|
}
|
||||||
|
|||||||
+80
-18
@@ -1315,7 +1315,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
|||||||
/* 00 */ pstring<TextEncoding::MARKED, 0x18> deck_name;
|
/* 00 */ pstring<TextEncoding::MARKED, 0x18> deck_name;
|
||||||
/* 18 */ parray<be_uint16_t, 0x20> card_ids; // Last one appears to always be FFFF
|
/* 18 */ parray<be_uint16_t, 0x20> card_ids; // Last one appears to always be FFFF
|
||||||
/* 58 */
|
/* 58 */
|
||||||
phosg::JSON json(uint8_t language) const;
|
phosg::JSON json(Language language) const;
|
||||||
} __packed_ws__(NPCDeck, 0x58);
|
} __packed_ws__(NPCDeck, 0x58);
|
||||||
/* 1FE8 */ parray<NPCDeck, 3> npc_decks; // Unused if name[0] == 0
|
/* 1FE8 */ parray<NPCDeck, 3> npc_decks; // Unused if name[0] == 0
|
||||||
|
|
||||||
@@ -1331,7 +1331,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
|||||||
// TODO: Figure out exactly how these are used and document here.
|
// TODO: Figure out exactly how these are used and document here.
|
||||||
/* 0018 */ parray<be_uint16_t, 0x7E> params;
|
/* 0018 */ parray<be_uint16_t, 0x7E> params;
|
||||||
/* 0114 */
|
/* 0114 */
|
||||||
phosg::JSON json(uint8_t language) const;
|
phosg::JSON json(Language language) const;
|
||||||
} __packed_ws__(AIParams, 0x114);
|
} __packed_ws__(AIParams, 0x114);
|
||||||
/* 20F0 */ parray<AIParams, 3> npc_ai_params; // Unused if name[0] == 0
|
/* 20F0 */ parray<AIParams, 3> npc_ai_params; // Unused if name[0] == 0
|
||||||
|
|
||||||
@@ -1384,7 +1384,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
|||||||
// strings, excluding any that are empty or begin with the character '^'.
|
// strings, excluding any that are empty or begin with the character '^'.
|
||||||
/* 0004 */ parray<pstring<TextEncoding::MARKED, 0x40>, 4> strings;
|
/* 0004 */ parray<pstring<TextEncoding::MARKED, 0x40>, 4> strings;
|
||||||
/* 0104 */
|
/* 0104 */
|
||||||
phosg::JSON json(uint8_t language) const;
|
phosg::JSON json(Language language) const;
|
||||||
} __packed_ws__(DialogueSet, 0x104);
|
} __packed_ws__(DialogueSet, 0x104);
|
||||||
|
|
||||||
// There are up to 0x10 of these per valid NPC, but only the first 13 of them
|
// There are up to 0x10 of these per valid NPC, but only the first 13 of them
|
||||||
@@ -1490,8 +1490,8 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
|||||||
// text may differ.
|
// text may differ.
|
||||||
void assert_semantically_equivalent(const MapDefinition& other) const;
|
void assert_semantically_equivalent(const MapDefinition& other) const;
|
||||||
|
|
||||||
std::string str(const CardIndex* card_index, uint8_t language) const;
|
std::string str(const CardIndex* card_index, Language language) const;
|
||||||
phosg::JSON json(uint8_t language) const;
|
phosg::JSON json(Language language) const;
|
||||||
} __packed_ws__(MapDefinition, 0x5A18);
|
} __packed_ws__(MapDefinition, 0x5A18);
|
||||||
|
|
||||||
struct MapDefinitionTrial {
|
struct MapDefinitionTrial {
|
||||||
@@ -1587,15 +1587,20 @@ private:
|
|||||||
|
|
||||||
class MapIndex {
|
class MapIndex {
|
||||||
public:
|
public:
|
||||||
MapIndex(const std::string& directory);
|
enum class VisibilityFlag : uint8_t {
|
||||||
|
ONLINE_TRIAL = 0x01,
|
||||||
|
ONLINE_FINAL = 0x02,
|
||||||
|
DOWNLOAD_TRIAL = 0x04,
|
||||||
|
DOWNLOAD_FINAL = 0x08,
|
||||||
|
};
|
||||||
|
|
||||||
class VersionedMap {
|
class VersionedMap {
|
||||||
public:
|
public:
|
||||||
std::shared_ptr<const MapDefinition> map;
|
std::shared_ptr<const MapDefinition> map;
|
||||||
uint8_t language;
|
Language language;
|
||||||
|
|
||||||
VersionedMap(std::shared_ptr<const MapDefinition> map, uint8_t language);
|
VersionedMap(std::shared_ptr<const MapDefinition> map, Language language);
|
||||||
VersionedMap(std::string&& compressed_data, uint8_t language);
|
VersionedMap(std::string&& compressed_data, Language language);
|
||||||
|
|
||||||
std::shared_ptr<const MapDefinitionTrial> trial() const;
|
std::shared_ptr<const MapDefinitionTrial> trial() const;
|
||||||
std::shared_ptr<const std::string> compressed(bool trial) const;
|
std::shared_ptr<const std::string> compressed(bool trial) const;
|
||||||
@@ -1611,13 +1616,18 @@ public:
|
|||||||
class Map {
|
class Map {
|
||||||
public:
|
public:
|
||||||
uint32_t map_number;
|
uint32_t map_number;
|
||||||
|
uint8_t visibility_flags;
|
||||||
std::shared_ptr<const VersionedMap> initial_version;
|
std::shared_ptr<const VersionedMap> initial_version;
|
||||||
|
|
||||||
explicit Map(std::shared_ptr<const VersionedMap> initial_version);
|
Map(std::shared_ptr<const VersionedMap> initial_version, uint8_t visibility_flags);
|
||||||
|
|
||||||
|
inline bool check_visibility_flag(VisibilityFlag flag) const {
|
||||||
|
return (this->visibility_flags & static_cast<uint8_t>(flag));
|
||||||
|
}
|
||||||
|
|
||||||
void add_version(std::shared_ptr<const VersionedMap> vm);
|
void add_version(std::shared_ptr<const VersionedMap> vm);
|
||||||
bool has_version(uint8_t language) const;
|
bool has_version(Language language) const;
|
||||||
std::shared_ptr<const VersionedMap> version(uint8_t language) const;
|
std::shared_ptr<const VersionedMap> version(Language language) const;
|
||||||
inline const std::vector<std::shared_ptr<const VersionedMap>>& all_versions() const {
|
inline const std::vector<std::shared_ptr<const VersionedMap>>& all_versions() const {
|
||||||
return this->versions;
|
return this->versions;
|
||||||
}
|
}
|
||||||
@@ -1626,24 +1636,76 @@ public:
|
|||||||
std::vector<std::shared_ptr<const VersionedMap>> versions;
|
std::vector<std::shared_ptr<const VersionedMap>> versions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::string& get_compressed_list(size_t num_players, uint8_t language) const;
|
class Category {
|
||||||
inline std::shared_ptr<const Map> get(uint32_t id) const {
|
public:
|
||||||
|
uint32_t category_id;
|
||||||
|
uint8_t visibility_flags;
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
|
||||||
|
Category(uint32_t category_id, const phosg::JSON& json);
|
||||||
|
|
||||||
|
inline bool check_visibility_flag(VisibilityFlag flag) const {
|
||||||
|
return (this->visibility_flags & static_cast<uint8_t>(flag));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void add_map(std::shared_ptr<const Map> map) {
|
||||||
|
this->maps.emplace(map->map_number, map);
|
||||||
|
}
|
||||||
|
inline const std::map<uint32_t, std::shared_ptr<const Map>>& all_maps() const {
|
||||||
|
return this->maps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<uint32_t, std::shared_ptr<const Map>> maps;
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit MapIndex(const std::string& directory, bool raise_on_any_failure = false);
|
||||||
|
|
||||||
|
const std::string& get_compressed_list(size_t num_players, Language language, bool is_trial) const;
|
||||||
|
inline std::shared_ptr<const Map> map_for_id(uint32_t id) const {
|
||||||
return this->maps.at(id);
|
return this->maps.at(id);
|
||||||
}
|
}
|
||||||
inline std::shared_ptr<const Map> get(const std::string& name) const {
|
inline std::shared_ptr<const Map> map_for_name(const std::string& name) const {
|
||||||
return this->maps_by_name.at(name);
|
return this->maps_by_name.at(name);
|
||||||
}
|
}
|
||||||
inline const std::map<uint32_t, std::shared_ptr<const Map>>& all() const {
|
inline const std::map<uint32_t, std::shared_ptr<const Map>>& all_maps() const {
|
||||||
return this->maps;
|
return this->maps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::shared_ptr<const Category> category_for_id(uint32_t id) const {
|
||||||
|
return this->categories.at(id);
|
||||||
|
}
|
||||||
|
inline const std::map<uint32_t, std::shared_ptr<const Category>>& all_categories() const {
|
||||||
|
return this->categories;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// The compressed map lists are generated on demand from the maps map below
|
// The compressed map lists are generated on demand from the maps map below.
|
||||||
mutable std::vector<std::array<std::string, 4>> compressed_map_lists;
|
// THey are indexed as [language][num_players]
|
||||||
|
mutable std::vector<std::array<std::string, 4>> compressed_map_lists_trial;
|
||||||
|
mutable std::vector<std::array<std::string, 4>> compressed_map_lists_final;
|
||||||
|
|
||||||
|
std::map<uint32_t, std::shared_ptr<const Category>> categories;
|
||||||
std::map<uint32_t, std::shared_ptr<const Map>> maps;
|
std::map<uint32_t, std::shared_ptr<const Map>> maps;
|
||||||
std::unordered_map<std::string, std::shared_ptr<Map>> maps_by_name;
|
std::unordered_map<std::string, std::shared_ptr<Map>> maps_by_name;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class MapCategoryIndex {
|
||||||
|
public:
|
||||||
|
explicit MapCategoryIndex(const std::string& directory);
|
||||||
|
|
||||||
|
inline std::shared_ptr<const MapIndex> get(uint32_t id) const {
|
||||||
|
return this->indexes.at(id);
|
||||||
|
}
|
||||||
|
inline const std::map<uint32_t, std::shared_ptr<const MapIndex>>& all() const {
|
||||||
|
return this->indexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<uint32_t, std::shared_ptr<const MapIndex>> indexes;
|
||||||
|
};
|
||||||
|
|
||||||
class COMDeckIndex {
|
class COMDeckIndex {
|
||||||
public:
|
public:
|
||||||
COMDeckIndex(const std::string& filename);
|
COMDeckIndex(const std::string& filename);
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ void DeckState::print(FILE* stream, std::shared_ptr<const CardIndex> card_index)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ce) {
|
if (ce) {
|
||||||
string name = ce->def.en_name.decode(1);
|
string name = ce->def.en_name.decode(Language::ENGLISH);
|
||||||
phosg::fwrite_fmt(stream, " ({:02}) index={:02X} ref=@{:04X} card_id=#{:04X} \"{}\" {}\n",
|
phosg::fwrite_fmt(stream, " ({:02}) index={:02X} ref=@{:04X} card_id=#{:04X} \"{}\" {}\n",
|
||||||
z, e.deck_index, this->card_refs[z], e.card_id, name, name_for_card_state(e.state));
|
z, e.deck_index, this->card_refs[z], e.card_id, name, name_for_card_state(e.state));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+31
-19
@@ -231,6 +231,11 @@ int8_t Server::get_winner_team_id() const {
|
|||||||
|
|
||||||
void Server::send(const void* data, size_t size, uint8_t command, bool enable_masking) const {
|
void Server::send(const void* data, size_t size, uint8_t command, bool enable_masking) const {
|
||||||
// Note: This function is (obviously) not part of the original implementation.
|
// Note: This function is (obviously) not part of the original implementation.
|
||||||
|
|
||||||
|
if (this->options.output_queue) {
|
||||||
|
this->options.output_queue->emplace_back(reinterpret_cast<const char*>(data), size);
|
||||||
|
}
|
||||||
|
|
||||||
if (this->has_lobby) {
|
if (this->has_lobby) {
|
||||||
auto l = this->lobby.lock();
|
auto l = this->lobby.lock();
|
||||||
if (!l) {
|
if (!l) {
|
||||||
@@ -274,14 +279,14 @@ void Server::send_6xB4x46() const {
|
|||||||
// NTE doesn't have the date_str2 field, but we send it anyway to make
|
// NTE doesn't have the date_str2 field, but we send it anyway to make
|
||||||
// debugging easier.
|
// debugging easier.
|
||||||
G_ServerVersionStrings_Ep3_6xB4x46 cmd;
|
G_ServerVersionStrings_Ep3_6xB4x46 cmd;
|
||||||
cmd.version_signature.encode(this->options.is_nte() ? VERSION_SIGNATURE_NTE : VERSION_SIGNATURE, 1);
|
cmd.version_signature.encode(this->options.is_nte() ? VERSION_SIGNATURE_NTE : VERSION_SIGNATURE, Language::ENGLISH);
|
||||||
cmd.date_str1.encode(std::format("Card definitions: {:016X}", this->options.card_index->definitions_hash()), 1);
|
cmd.date_str1.encode(std::format("Card definitions: {:016X}", this->options.card_index->definitions_hash()), Language::ENGLISH);
|
||||||
string build_date = phosg::format_time(BUILD_TIMESTAMP);
|
string build_date = phosg::format_time(BUILD_TIMESTAMP);
|
||||||
cmd.date_str2.encode(std::format("newserv {} compiled at {}", GIT_REVISION_HASH, build_date), 1);
|
cmd.date_str2.encode(std::format("newserv {} compiled at {}", GIT_REVISION_HASH, build_date), Language::ENGLISH);
|
||||||
this->send(cmd);
|
this->send(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
string Server::prepare_6xB6x41_map_definition(shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_nte) {
|
string Server::prepare_6xB6x41_map_definition(shared_ptr<const MapIndex::Map> map, Language language, bool is_nte) {
|
||||||
auto vm = map->version(language);
|
auto vm = map->version(language);
|
||||||
|
|
||||||
const auto& compressed = vm->compressed(is_nte);
|
const auto& compressed = vm->compressed(is_nte);
|
||||||
@@ -306,7 +311,7 @@ void Server::send_commands_for_joining_spectator(std::shared_ptr<Channel> ch) co
|
|||||||
|
|
||||||
if (this->last_chosen_map) {
|
if (this->last_chosen_map) {
|
||||||
string data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, ch->language, this->options.is_nte());
|
string data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, ch->language, this->options.is_nte());
|
||||||
this->log().info_f("Sending {} version of map {:08X}", char_for_language_code(ch->language), this->last_chosen_map->map_number);
|
this->log().info_f("Sending {} version of map {:08X}", name_for_language(ch->language), this->last_chosen_map->map_number);
|
||||||
ch->send(0x6C, 0x00, data);
|
ch->send(0x6C, 0x00, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,7 +597,14 @@ void Server::force_replace_assist_card(uint8_t client_id, uint16_t card_id) {
|
|||||||
if (!ps) {
|
if (!ps) {
|
||||||
throw runtime_error("player does not exist");
|
throw runtime_error("player does not exist");
|
||||||
}
|
}
|
||||||
ps->replace_assist_card_by_id(card_id);
|
if (card_id == 0xFFFF) {
|
||||||
|
ps->discard_set_assist_card();
|
||||||
|
this->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02();
|
||||||
|
this->check_for_battle_end();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ps->replace_assist_card_by_id(card_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::force_destroy_field_character(uint8_t client_id, size_t visible_index) {
|
void Server::force_destroy_field_character(uint8_t client_id, size_t visible_index) {
|
||||||
@@ -2130,7 +2142,7 @@ 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
|
// 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
|
// smaller). So, use the values from the last chosen map if applicable, or
|
||||||
// the values from the $dicerange command if available.
|
// the values from the $dicerange command if available.
|
||||||
uint8_t language = c ? c->language() : 1;
|
Language language = c ? c->language() : Language::ENGLISH;
|
||||||
const Rules* map_rules = this->last_chosen_map ? &this->last_chosen_map->version(language)->map->default_rules : nullptr;
|
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;
|
auto& server_rules = this->map_and_rules->rules;
|
||||||
// NTE can specify the DEF dice value range in its Rules struct, so we use
|
// NTE can specify the DEF dice value range in its Rules struct, so we use
|
||||||
@@ -2519,8 +2531,8 @@ void Server::handle_CAx40_map_list_request(shared_ptr<Client> sender_c, const st
|
|||||||
}
|
}
|
||||||
|
|
||||||
size_t num_players = l ? l->count_clients() : 1;
|
size_t num_players = l ? l->count_clients() : 1;
|
||||||
uint8_t language = sender_c ? sender_c->language() : 1;
|
Language language = sender_c ? sender_c->language() : Language::ENGLISH;
|
||||||
const auto& list_data = this->options.map_index->get_compressed_list(num_players, language);
|
const auto& list_data = this->options.map_index->get_compressed_list(num_players, language, this->options.is_nte());
|
||||||
|
|
||||||
phosg::StringWriter w;
|
phosg::StringWriter w;
|
||||||
uint32_t subcommand_size = (list_data.size() + sizeof(G_MapList_Ep3_6xB6x40) + 3) & (~3);
|
uint32_t subcommand_size = (list_data.size() + sizeof(G_MapList_Ep3_6xB6x40) + 3) & (~3);
|
||||||
@@ -2546,15 +2558,16 @@ void Server::send_6xB6x41_to_all_clients() const {
|
|||||||
if (!c) {
|
if (!c) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (map_commands_by_language.size() <= c->language()) {
|
size_t lang_index = static_cast<size_t>(c->language());
|
||||||
map_commands_by_language.resize(c->language() + 1);
|
if (map_commands_by_language.size() <= lang_index) {
|
||||||
|
map_commands_by_language.resize(lang_index + 1);
|
||||||
}
|
}
|
||||||
if (map_commands_by_language[c->language()].empty()) {
|
if (map_commands_by_language[lang_index].empty()) {
|
||||||
map_commands_by_language[c->language()] = this->prepare_6xB6x41_map_definition(
|
map_commands_by_language[lang_index] = this->prepare_6xB6x41_map_definition(
|
||||||
this->last_chosen_map, c->language(), this->options.is_nte());
|
this->last_chosen_map, c->language(), this->options.is_nte());
|
||||||
}
|
}
|
||||||
this->log().info_f("Sending {} version of map {:08X}", char_for_language_code(c->language()), this->last_chosen_map->map_number);
|
this->log().info_f("Sending {} version of map {:08X}", name_for_language(c->language()), this->last_chosen_map->map_number);
|
||||||
send_command(c, 0x6C, 0x00, map_commands_by_language[c->language()]);
|
send_command(c, 0x6C, 0x00, map_commands_by_language[lang_index]);
|
||||||
};
|
};
|
||||||
for (const auto& c : l->clients) {
|
for (const auto& c : l->clients) {
|
||||||
send_to_client(c);
|
send_to_client(c);
|
||||||
@@ -2571,15 +2584,14 @@ void Server::send_6xB6x41_to_all_clients() const {
|
|||||||
// in the playback lobby
|
// in the playback lobby
|
||||||
for (string& data : map_commands_by_language) {
|
for (string& data : map_commands_by_language) {
|
||||||
if (!data.empty()) {
|
if (!data.empty()) {
|
||||||
this->battle_record->add_command(
|
this->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
|
||||||
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
auto out_data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, 1, false);
|
auto out_data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, Language::ENGLISH, false);
|
||||||
this->send(out_data.data(), out_data.size(), 0x6C, false);
|
this->send(out_data.data(), out_data.size(), 0x6C, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2588,7 +2600,7 @@ void Server::handle_CAx41_map_request(shared_ptr<Client>, const string& data) {
|
|||||||
const auto& cmd = check_size_t<G_MapDataRequest_Ep3_CAx41>(data);
|
const auto& cmd = check_size_t<G_MapDataRequest_Ep3_CAx41>(data);
|
||||||
this->send_debug_command_received_message(cmd.header.subsubcommand, "MAP DATA");
|
this->send_debug_command_received_message(cmd.header.subsubcommand, "MAP DATA");
|
||||||
if (!this->options.tournament || (this->options.tournament->get_map()->map_number == cmd.map_number)) {
|
if (!this->options.tournament || (this->options.tournament->get_map()->map_number == cmd.map_number)) {
|
||||||
this->last_chosen_map = this->options.map_index->get(cmd.map_number);
|
this->last_chosen_map = this->options.map_index->map_for_id(cmd.map_number);
|
||||||
this->send_6xB6x41_to_all_clients();
|
this->send_6xB6x41_to_all_clients();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public:
|
|||||||
std::shared_ptr<RandomGenerator> rand_crypt;
|
std::shared_ptr<RandomGenerator> rand_crypt;
|
||||||
std::shared_ptr<const Tournament> tournament;
|
std::shared_ptr<const Tournament> tournament;
|
||||||
std::array<std::vector<uint16_t>, 5> trap_card_ids;
|
std::array<std::vector<uint16_t>, 5> trap_card_ids;
|
||||||
|
std::shared_ptr<std::deque<std::string>> output_queue; // For replay testing
|
||||||
|
|
||||||
inline bool is_nte() const {
|
inline bool is_nte() const {
|
||||||
return (this->behavior_flags & BehaviorFlag::IS_TRIAL_EDITION);
|
return (this->behavior_flags & BehaviorFlag::IS_TRIAL_EDITION);
|
||||||
@@ -265,7 +266,7 @@ public:
|
|||||||
|
|
||||||
G_UpdateDecks_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
|
G_UpdateDecks_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
|
||||||
G_SetPlayerNames_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
|
G_SetPlayerNames_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
|
||||||
static std::string prepare_6xB6x41_map_definition(std::shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_nte);
|
static std::string prepare_6xB6x41_map_definition(std::shared_ptr<const MapIndex::Map> map, Language language, bool is_nte);
|
||||||
void send_6xB6x41_to_all_clients() const;
|
void send_6xB6x41_to_all_clients() const;
|
||||||
G_SetTrapTileLocations_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
|
G_SetTrapTileLocations_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
|
||||||
|
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ void Tournament::init() {
|
|||||||
bool is_registration_complete;
|
bool is_registration_complete;
|
||||||
if (!this->source_json.is_null()) {
|
if (!this->source_json.is_null()) {
|
||||||
this->name = this->source_json.get_string("name");
|
this->name = this->source_json.get_string("name");
|
||||||
this->map = this->map_index->get(this->source_json.get_int("map_number"));
|
this->map = this->map_index->map_for_id(this->source_json.get_int("map_number"));
|
||||||
this->rules = Rules(this->source_json.at("rules"));
|
this->rules = Rules(this->source_json.at("rules"));
|
||||||
this->flags = this->source_json.get_int("flags", 0x02);
|
this->flags = this->source_json.get_int("flags", 0x02);
|
||||||
if (this->source_json.get_bool("is_2v2", false)) {
|
if (this->source_json.get_bool("is_2v2", false)) {
|
||||||
@@ -737,7 +737,7 @@ string Tournament::bracket_str() const {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto en_vm = this->map->version(1);
|
auto en_vm = this->map->version(Language::ENGLISH);
|
||||||
if (en_vm) {
|
if (en_vm) {
|
||||||
string map_name = en_vm->map->name.decode(en_vm->language);
|
string map_name = en_vm->map->name.decode(en_vm->language);
|
||||||
ret += std::format(" Map: {:08X} ({})\n", this->map->map_number, map_name);
|
ret += std::format(" Map: {:08X} ({})\n", this->map->map_number, map_name);
|
||||||
|
|||||||
+117
-73
@@ -108,12 +108,118 @@ bool CompiledFunctionCode::is_big_endian() const {
|
|||||||
return this->arch == Architecture::POWERPC;
|
return this->arch == Architecture::POWERPC;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static unordered_map<uint32_t, std::string> preprocess_function_code(const std::string& text) {
|
||||||
|
auto parse_specific_version_list = +[](std::string&& text) -> vector<uint32_t> {
|
||||||
|
phosg::strip_whitespace(text);
|
||||||
|
vector<uint32_t> ret;
|
||||||
|
for (auto& vers_token : phosg::split(text, ' ')) {
|
||||||
|
phosg::strip_whitespace(vers_token);
|
||||||
|
if (vers_token.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (vers_token.size() != 4) {
|
||||||
|
throw std::runtime_error("invalid specific_version: " + vers_token);
|
||||||
|
}
|
||||||
|
ret.emplace_back(*reinterpret_cast<const be_uint32_t*>(vers_token.data()));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find a .versions directive and populate specific_versions
|
||||||
|
vector<uint32_t> specific_versions;
|
||||||
|
auto lines = phosg::split(text, '\n');
|
||||||
|
for (auto& line : lines) {
|
||||||
|
if (line.starts_with(".versions ")) {
|
||||||
|
if (!specific_versions.empty()) {
|
||||||
|
throw std::runtime_error("multiple .versions directives in file");
|
||||||
|
}
|
||||||
|
specific_versions = parse_specific_version_list(line.substr(10));
|
||||||
|
if (specific_versions.empty()) {
|
||||||
|
throw std::runtime_error(".versions directive does not specify any versions");
|
||||||
|
}
|
||||||
|
line.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's no .versions directive, just return the text as-is
|
||||||
|
if (specific_versions.empty()) {
|
||||||
|
return {{0, std::move(text)}};
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<deque<string>> version_lines;
|
||||||
|
version_lines.resize(specific_versions.size());
|
||||||
|
|
||||||
|
size_t line_num = 1;
|
||||||
|
vector<uint32_t> current_only_versions;
|
||||||
|
unordered_set<uint32_t> current_only_versions_set;
|
||||||
|
for (auto& line : lines) {
|
||||||
|
phosg::strip_whitespace(line);
|
||||||
|
if (line.starts_with(".only_versions ")) {
|
||||||
|
current_only_versions = parse_specific_version_list(line.substr(15));
|
||||||
|
current_only_versions_set.clear();
|
||||||
|
for (uint32_t specific_version : current_only_versions) {
|
||||||
|
current_only_versions_set.emplace(specific_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (line == ".all_versions") {
|
||||||
|
current_only_versions.clear();
|
||||||
|
current_only_versions_set.clear();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
size_t vers_offset = line.find("<VERS ");
|
||||||
|
if (vers_offset == string::npos) {
|
||||||
|
for (size_t vers_index = 0; vers_index < specific_versions.size(); vers_index++) {
|
||||||
|
if (current_only_versions.empty() || current_only_versions_set.count(specific_versions[vers_index])) {
|
||||||
|
version_lines[vers_index].emplace_back(line);
|
||||||
|
} else {
|
||||||
|
version_lines[vers_index].emplace_back("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
size_t token_index = 0;
|
||||||
|
for (size_t vers_index = 0; vers_index < specific_versions.size(); vers_index++) {
|
||||||
|
if (current_only_versions.empty() || current_only_versions_set.count(specific_versions[vers_index])) {
|
||||||
|
string version_line = line;
|
||||||
|
size_t vers_offset = line.find("<VERS ");
|
||||||
|
while (vers_offset != string::npos) {
|
||||||
|
size_t end_offset = version_line.find('>', vers_offset + 6);
|
||||||
|
if (end_offset == string::npos) {
|
||||||
|
throw runtime_error(std::format("(line {}) unterminated <VERS> replacement", line_num));
|
||||||
|
}
|
||||||
|
auto tokens = phosg::split(version_line.substr(vers_offset + 6, end_offset - vers_offset - 6), ' ');
|
||||||
|
if (tokens.size() <= token_index) {
|
||||||
|
throw runtime_error(std::format("(line {}) invalid <VERS> replacement", line_num));
|
||||||
|
}
|
||||||
|
version_line = version_line.substr(0, vers_offset) + tokens.at(token_index) + version_line.substr(end_offset + 1);
|
||||||
|
vers_offset = version_line.find("<VERS ");
|
||||||
|
}
|
||||||
|
version_lines[vers_index].emplace_back(version_line);
|
||||||
|
token_index++;
|
||||||
|
} else {
|
||||||
|
version_lines[vers_index].emplace_back("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line_num++;
|
||||||
|
}
|
||||||
|
|
||||||
|
unordered_map<uint32_t, string> ret;
|
||||||
|
for (size_t z = 0; z < specific_versions.size(); z++) {
|
||||||
|
ret.emplace(specific_versions[z], phosg::join(version_lines.at(z), "\n"));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
static vector<shared_ptr<CompiledFunctionCode>> compile_function_code(
|
static vector<shared_ptr<CompiledFunctionCode>> compile_function_code(
|
||||||
CompiledFunctionCode::Architecture arch,
|
CompiledFunctionCode::Architecture arch,
|
||||||
const string& function_directory,
|
const string& function_directory,
|
||||||
const string& system_directory,
|
const string& system_directory,
|
||||||
const string& name,
|
const string& name,
|
||||||
const string& text) {
|
const string& text,
|
||||||
|
bool raise_on_any_failure) {
|
||||||
unordered_set<string> get_include_stack;
|
unordered_set<string> get_include_stack;
|
||||||
function<string(const string&)> get_include = [&](const string& name) -> string {
|
function<string(const string&)> get_include = [&](const string& name) -> string {
|
||||||
const char* arch_name_token;
|
const char* arch_name_token;
|
||||||
@@ -169,78 +275,10 @@ static vector<shared_ptr<CompiledFunctionCode>> compile_function_code(
|
|||||||
throw runtime_error("data not found for include: " + name + " (from " + asm_filename + " or " + bin_filename + ")");
|
throw runtime_error("data not found for include: " + name + " (from " + asm_filename + " or " + bin_filename + ")");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle VERS tokens
|
auto version_texts = preprocess_function_code(text);
|
||||||
vector<uint32_t> specific_versions;
|
|
||||||
auto lines = phosg::split(text, '\n');
|
|
||||||
for (auto& line : lines) {
|
|
||||||
if (line.starts_with(".versions ")) {
|
|
||||||
if (!specific_versions.empty()) {
|
|
||||||
throw std::runtime_error("multiple .versions directives in file");
|
|
||||||
}
|
|
||||||
for (auto& vers_token : phosg::split(line.substr(10), ' ')) {
|
|
||||||
phosg::strip_whitespace(vers_token);
|
|
||||||
if (vers_token.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (vers_token.size() != 4) {
|
|
||||||
throw std::runtime_error("invalid token in .version directive: " + vers_token);
|
|
||||||
}
|
|
||||||
specific_versions.emplace_back(*reinterpret_cast<const be_uint32_t*>(vers_token.data()));
|
|
||||||
}
|
|
||||||
line.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preprocess <VERS> tokens in the text if a .versions directive was given
|
|
||||||
vector<string> version_texts;
|
|
||||||
if (specific_versions.empty()) {
|
|
||||||
specific_versions.emplace_back(0);
|
|
||||||
version_texts.emplace_back(text);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
vector<deque<string>> version_lines;
|
|
||||||
version_lines.resize(specific_versions.size());
|
|
||||||
|
|
||||||
size_t line_num = 1;
|
|
||||||
for (const auto& line : lines) {
|
|
||||||
size_t vers_offset = line.find("<VERS ");
|
|
||||||
if (vers_offset == string::npos) {
|
|
||||||
for (auto& lines : version_lines) {
|
|
||||||
lines.emplace_back(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
for (size_t vers_index = 0; vers_index < specific_versions.size(); vers_index++) {
|
|
||||||
string version_line = line;
|
|
||||||
size_t vers_offset = line.find("<VERS ");
|
|
||||||
while (vers_offset != string::npos) {
|
|
||||||
size_t end_offset = version_line.find('>', vers_offset + 6);
|
|
||||||
if (end_offset == string::npos) {
|
|
||||||
throw runtime_error(std::format("(line {}) unterminated <VERS> replacement", line_num));
|
|
||||||
}
|
|
||||||
auto tokens = phosg::split(version_line.substr(vers_offset + 6, end_offset - vers_offset - 6), ' ');
|
|
||||||
if (tokens.size() != specific_versions.size()) {
|
|
||||||
throw runtime_error(std::format("(line {}) invalid <VERS> replacement", line_num));
|
|
||||||
}
|
|
||||||
version_line = version_line.substr(0, vers_offset) + tokens.at(vers_index) + version_line.substr(end_offset + 1);
|
|
||||||
vers_offset = version_line.find("<VERS ");
|
|
||||||
}
|
|
||||||
version_lines[vers_index].emplace_back(version_line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line_num++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& lines : version_lines) {
|
|
||||||
version_texts.emplace_back(phosg::join(lines, "\n"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vector<shared_ptr<CompiledFunctionCode>> ret;
|
vector<shared_ptr<CompiledFunctionCode>> ret;
|
||||||
for (size_t vers_index = 0; vers_index < specific_versions.size(); vers_index++) {
|
for (const auto& [specific_version, version_text] : version_texts) {
|
||||||
uint32_t specific_version = specific_versions[vers_index];
|
|
||||||
const auto& version_text = version_texts.at(vers_index);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ResourceDASM::EmulatorBase::AssembleResult assembled;
|
ResourceDASM::EmulatorBase::AssembleResult assembled;
|
||||||
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
|
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
|
||||||
@@ -298,6 +336,9 @@ static vector<shared_ptr<CompiledFunctionCode>> compile_function_code(
|
|||||||
|
|
||||||
} catch (const exception& e) {
|
} catch (const exception& e) {
|
||||||
string version_str = specific_version ? (" (" + str_for_specific_version(specific_version) + ")") : "";
|
string version_str = specific_version ? (" (" + str_for_specific_version(specific_version) + ")") : "";
|
||||||
|
if (raise_on_any_failure) {
|
||||||
|
throw;
|
||||||
|
}
|
||||||
function_compiler_log.warning_f("Failed to compile function {}{}: {}", name, version_str, e.what());
|
function_compiler_log.warning_f("Failed to compile function {}{}: {}", name, version_str, e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,7 +346,7 @@ static vector<shared_ptr<CompiledFunctionCode>> compile_function_code(
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
|
FunctionCodeIndex::FunctionCodeIndex(const string& directory, bool raise_on_any_failure) {
|
||||||
string system_dir_path = directory.ends_with("/") ? (directory + "System") : (directory + "/System");
|
string system_dir_path = directory.ends_with("/") ? (directory + "System") : (directory + "/System");
|
||||||
|
|
||||||
uint32_t next_menu_item_id = 1;
|
uint32_t next_menu_item_id = 1;
|
||||||
@@ -367,7 +408,7 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
|
|||||||
|
|
||||||
string path = subdir_path + "/" + filename;
|
string path = subdir_path + "/" + filename;
|
||||||
string text = phosg::load_file(path);
|
string text = phosg::load_file(path);
|
||||||
for (auto code : compile_function_code(arch, subdir_path, system_dir_path, name, text)) {
|
for (auto code : compile_function_code(arch, subdir_path, system_dir_path, name, text, raise_on_any_failure)) {
|
||||||
if (code->specific_version == 0) {
|
if (code->specific_version == 0) {
|
||||||
code->specific_version = specific_version;
|
code->specific_version = specific_version;
|
||||||
}
|
}
|
||||||
@@ -388,6 +429,9 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (const exception& e) {
|
} catch (const exception& e) {
|
||||||
|
if (raise_on_any_failure) {
|
||||||
|
throw runtime_error(format("({}) {}", filename, e.what()));
|
||||||
|
}
|
||||||
function_compiler_log.warning_f("Failed to compile function {}: {}", filename, e.what());
|
function_compiler_log.warning_f("Failed to compile function {}: {}", filename, e.what());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const char* name_for_architecture(CompiledFunctionCode::Architecture arch);
|
|||||||
|
|
||||||
struct FunctionCodeIndex {
|
struct FunctionCodeIndex {
|
||||||
FunctionCodeIndex() = default;
|
FunctionCodeIndex() = default;
|
||||||
explicit FunctionCodeIndex(const std::string& directory);
|
FunctionCodeIndex(const std::string& directory, bool raise_on_any_failure);
|
||||||
|
|
||||||
std::unordered_map<std::string, std::shared_ptr<CompiledFunctionCode>> name_to_function;
|
std::unordered_map<std::string, std::shared_ptr<CompiledFunctionCode>> name_to_function;
|
||||||
std::unordered_map<uint8_t, std::shared_ptr<CompiledFunctionCode>> index_to_function;
|
std::unordered_map<uint8_t, std::shared_ptr<CompiledFunctionCode>> index_to_function;
|
||||||
|
|||||||
+4
-2
@@ -118,7 +118,9 @@ vector<shared_ptr<Client>> GameServer::get_clients_by_identifier(const string& i
|
|||||||
shared_ptr<Client> GameServer::create_client(shared_ptr<GameServerSocket> listen_sock, asio::ip::tcp::socket&& client_sock) {
|
shared_ptr<Client> GameServer::create_client(shared_ptr<GameServerSocket> listen_sock, asio::ip::tcp::socket&& client_sock) {
|
||||||
uint32_t addr = ipv4_addr_for_asio_addr(client_sock.remote_endpoint().address());
|
uint32_t addr = ipv4_addr_for_asio_addr(client_sock.remote_endpoint().address());
|
||||||
if (this->state->banned_ipv4_ranges->check(addr)) {
|
if (this->state->banned_ipv4_ranges->check(addr)) {
|
||||||
client_sock.close();
|
if (client_sock.is_open()) {
|
||||||
|
client_sock.close();
|
||||||
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ shared_ptr<Client> GameServer::create_client(shared_ptr<GameServerSocket> listen
|
|||||||
this->io_context,
|
this->io_context,
|
||||||
make_unique<asio::ip::tcp::socket>(std::move(client_sock)),
|
make_unique<asio::ip::tcp::socket>(std::move(client_sock)),
|
||||||
listen_sock->version,
|
listen_sock->version,
|
||||||
1,
|
Language::ENGLISH,
|
||||||
"",
|
"",
|
||||||
phosg::TerminalFormat::FG_YELLOW,
|
phosg::TerminalFormat::FG_YELLOW,
|
||||||
phosg::TerminalFormat::FG_GREEN);
|
phosg::TerminalFormat::FG_GREEN);
|
||||||
|
|||||||
+736
-782
File diff suppressed because it is too large
Load Diff
+6
-20
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
#include "AsyncHTTPServer.hh"
|
#include "AsyncHTTPServer.hh"
|
||||||
#include "ServerState.hh"
|
#include "ServerState.hh"
|
||||||
@@ -20,28 +21,13 @@ public:
|
|||||||
asio::awaitable<void> send_rare_drop_notification(std::shared_ptr<const phosg::JSON> message);
|
asio::awaitable<void> send_rare_drop_notification(std::shared_ptr<const phosg::JSON> message);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
struct RawResponse {
|
||||||
|
std::string content_type;
|
||||||
|
std::string data;
|
||||||
|
};
|
||||||
std::shared_ptr<ServerState> state;
|
std::shared_ptr<ServerState> state;
|
||||||
std::unordered_set<std::shared_ptr<HTTPClient>> rare_drop_subscribers;
|
std::unordered_set<std::shared_ptr<HTTPClient>> rare_drop_subscribers;
|
||||||
|
HTTPRouter<std::variant<RawResponse, std::shared_ptr<const phosg::JSON>>> router;
|
||||||
std::shared_ptr<phosg::JSON> generate_server_version() const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_account_json(std::shared_ptr<const Account> a) const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_client_json(
|
|
||||||
std::shared_ptr<const Client> c, std::shared_ptr<const ItemNameIndex> item_name_index) const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_lobby_json(
|
|
||||||
std::shared_ptr<const Lobby> l, std::shared_ptr<const ItemNameIndex> item_name_index) const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_accounts_json() const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_clients_json() const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_server_info_json() const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_lobbies_json() const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_summary_json() const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_all_json() const;
|
|
||||||
|
|
||||||
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_ep3_cards_json(bool trial) const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_common_table_list_json() const;
|
|
||||||
std::shared_ptr<phosg::JSON> generate_rare_table_list_json() const;
|
|
||||||
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_common_table_json(const std::string& table_name) const;
|
|
||||||
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_rare_table_json(const std::string& table_name) const;
|
|
||||||
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_quest_list_json();
|
|
||||||
|
|
||||||
void require_GET(const HTTPRequest& req);
|
void require_GET(const HTTPRequest& req);
|
||||||
phosg::JSON require_POST(const HTTPRequest& req);
|
phosg::JSON require_POST(const HTTPRequest& req);
|
||||||
|
|||||||
+32
-39
@@ -129,10 +129,7 @@ void IPSSClient::TCPConnection::linearize_outbound_data(size_t size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IPSSClient::IPSSClient(
|
IPSSClient::IPSSClient(
|
||||||
shared_ptr<IPStackSimulator> sim,
|
shared_ptr<IPStackSimulator> sim, uint64_t network_id, VirtualNetworkProtocol protocol, asio::ip::tcp::socket&& sock)
|
||||||
uint64_t network_id,
|
|
||||||
VirtualNetworkProtocol protocol,
|
|
||||||
asio::ip::tcp::socket&& sock)
|
|
||||||
: io_context(sim->get_io_context()),
|
: io_context(sim->get_io_context()),
|
||||||
sim(sim),
|
sim(sim),
|
||||||
network_id(network_id),
|
network_id(network_id),
|
||||||
@@ -154,7 +151,9 @@ void IPSSClient::reschedule_idle_timeout() {
|
|||||||
this->idle_timeout_timer.async_wait([this, sim](std::error_code ec) {
|
this->idle_timeout_timer.async_wait([this, sim](std::error_code ec) {
|
||||||
if (!ec) {
|
if (!ec) {
|
||||||
sim->log.info_f("Idle timeout expired on N-{:X}", this->network_id);
|
sim->log.info_f("Idle timeout expired on N-{:X}", this->network_id);
|
||||||
this->sock.close();
|
if (this->sock.is_open()) {
|
||||||
|
this->sock.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -164,7 +163,7 @@ IPSSChannel::IPSSChannel(
|
|||||||
std::weak_ptr<IPSSClient> ipss_client,
|
std::weak_ptr<IPSSClient> ipss_client,
|
||||||
std::weak_ptr<IPSSClient::TCPConnection> tcp_conn,
|
std::weak_ptr<IPSSClient::TCPConnection> tcp_conn,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const std::string& name,
|
const std::string& name,
|
||||||
phosg::TerminalFormat terminal_send_color,
|
phosg::TerminalFormat terminal_send_color,
|
||||||
phosg::TerminalFormat terminal_recv_color)
|
phosg::TerminalFormat terminal_recv_color)
|
||||||
@@ -177,8 +176,7 @@ IPSSChannel::IPSSChannel(
|
|||||||
std::string IPSSChannel::default_name() const {
|
std::string IPSSChannel::default_name() const {
|
||||||
auto ipc = this->ipss_client.lock();
|
auto ipc = this->ipss_client.lock();
|
||||||
if (ipc) {
|
if (ipc) {
|
||||||
string addr_str = str_for_endpoint(ipc->sock.remote_endpoint());
|
return std::format("ipss:N-{}:{}", ipc->network_id, str_for_endpoint(ipc->sock.remote_endpoint()));
|
||||||
return std::format("ipss:N-{}:{}", ipc->network_id, addr_str);
|
|
||||||
} else {
|
} else {
|
||||||
return std::format("ipss:N-{}:__unknown_address__", ipc->network_id);
|
return std::format("ipss:N-{}:__unknown_address__", ipc->network_id);
|
||||||
}
|
}
|
||||||
@@ -211,9 +209,7 @@ void IPSSChannel::add_inbound_data(const void* data, size_t size) {
|
|||||||
data = reinterpret_cast<const uint8_t*>(data) + direct_size;
|
data = reinterpret_cast<const uint8_t*>(data) + direct_size;
|
||||||
size -= direct_size;
|
size -= direct_size;
|
||||||
this->recv_buf_size -= direct_size;
|
this->recv_buf_size -= direct_size;
|
||||||
this->recv_buf = this->recv_buf_size
|
this->recv_buf = this->recv_buf_size ? (reinterpret_cast<uint8_t*>(this->recv_buf) + direct_size) : nullptr;
|
||||||
? reinterpret_cast<uint8_t*>(this->recv_buf) + direct_size
|
|
||||||
: nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is still data left after the above, add it to the pending inbound
|
// If there is still data left after the above, add it to the pending inbound
|
||||||
@@ -336,6 +332,7 @@ uint64_t IPStackSimulator::tcp_conn_key_for_client_frame(const FrameInfo& fi) {
|
|||||||
string IPStackSimulator::str_for_ipv4_netloc(uint32_t addr, uint16_t port) {
|
string IPStackSimulator::str_for_ipv4_netloc(uint32_t addr, uint16_t port) {
|
||||||
be_uint32_t be_addr = addr;
|
be_uint32_t be_addr = addr;
|
||||||
char addr_str[INET_ADDRSTRLEN];
|
char addr_str[INET_ADDRSTRLEN];
|
||||||
|
memset(addr_str, 0, sizeof(addr_str));
|
||||||
if (!inet_ntop(AF_INET, &be_addr, addr_str, INET_ADDRSTRLEN)) {
|
if (!inet_ntop(AF_INET, &be_addr, addr_str, INET_ADDRSTRLEN)) {
|
||||||
return std::format("<UNKNOWN>:{}", port);
|
return std::format("<UNKNOWN>:{}", port);
|
||||||
} else {
|
} else {
|
||||||
@@ -354,11 +351,12 @@ string IPStackSimulator::str_for_tcp_connection(
|
|||||||
asio::awaitable<void> IPStackSimulator::send_ethernet_tapserver_frame(
|
asio::awaitable<void> IPStackSimulator::send_ethernet_tapserver_frame(
|
||||||
shared_ptr<IPSSClient> c, FrameInfo::Protocol proto, const void* data, size_t size) const {
|
shared_ptr<IPSSClient> c, FrameInfo::Protocol proto, const void* data, size_t size) const {
|
||||||
|
|
||||||
struct {
|
struct TapServerEthernetHeader {
|
||||||
phosg::le_uint16_t frame_size;
|
phosg::le_uint16_t frame_size;
|
||||||
EthernetHeader ether;
|
EthernetHeader ether;
|
||||||
} header;
|
} __attribute__((packed));
|
||||||
static_assert(sizeof(header) == 0x10, "Ethernet tapserver header size is incorrect");
|
static_assert(sizeof(TapServerEthernetHeader) == 0x10, "Ethernet tapserver header size is incorrect");
|
||||||
|
TapServerEthernetHeader header;
|
||||||
|
|
||||||
header.ether.dest_mac = c->mac_addr;
|
header.ether.dest_mac = c->mac_addr;
|
||||||
header.ether.src_mac = this->host_mac_address_bytes;
|
header.ether.src_mac = this->host_mac_address_bytes;
|
||||||
@@ -378,9 +376,7 @@ asio::awaitable<void> IPStackSimulator::send_ethernet_tapserver_frame(
|
|||||||
}
|
}
|
||||||
header.frame_size = size + sizeof(EthernetHeader);
|
header.frame_size = size + sizeof(EthernetHeader);
|
||||||
|
|
||||||
array<asio::const_buffer, 2> bufs{
|
array<asio::const_buffer, 2> bufs{asio::buffer(static_cast<const void*>(&header), sizeof(header)), asio::buffer(data, size)};
|
||||||
asio::buffer(static_cast<const void*>(&header), sizeof(header)),
|
|
||||||
asio::buffer(data, size)};
|
|
||||||
co_await asio::async_write(c->sock, bufs, asio::use_awaitable);
|
co_await asio::async_write(c->sock, bufs, asio::use_awaitable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,8 +458,7 @@ asio::awaitable<void> IPStackSimulator::on_client_frame(shared_ptr<IPSSClient> c
|
|||||||
|
|
||||||
FrameInfo fi(link_type, data, size);
|
FrameInfo fi(link_type, data, size);
|
||||||
if (this->log.should_log(phosg::LogLevel::L_DEBUG)) {
|
if (this->log.should_log(phosg::LogLevel::L_DEBUG)) {
|
||||||
string fi_header = fi.header_str();
|
this->log.debug_f("Frame header: {}", fi.header_str());
|
||||||
this->log.debug_f("Frame header: {}", fi_header);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fi.ether) {
|
if (fi.ether) {
|
||||||
@@ -477,8 +472,7 @@ asio::awaitable<void> IPStackSimulator::on_client_frame(shared_ptr<IPSSClient> c
|
|||||||
uint16_t stored_checksum = fi.stored_hdlc_checksum();
|
uint16_t stored_checksum = fi.stored_hdlc_checksum();
|
||||||
if (expected_checksum != stored_checksum) {
|
if (expected_checksum != stored_checksum) {
|
||||||
throw runtime_error(std::format(
|
throw runtime_error(std::format(
|
||||||
"HDLC checksum is incorrect ({:04X} expected, {:04X} received)",
|
"HDLC checksum is incorrect ({:04X} expected, {:04X} received)", expected_checksum, stored_checksum));
|
||||||
expected_checksum, stored_checksum));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw runtime_error("frame is not Ethernet or HDLC");
|
throw runtime_error("frame is not Ethernet or HDLC");
|
||||||
@@ -500,8 +494,7 @@ asio::awaitable<void> IPStackSimulator::on_client_frame(shared_ptr<IPSSClient> c
|
|||||||
uint16_t expected_ipv4_checksum = fi.computed_ipv4_header_checksum();
|
uint16_t expected_ipv4_checksum = fi.computed_ipv4_header_checksum();
|
||||||
if (fi.ipv4->checksum != expected_ipv4_checksum) {
|
if (fi.ipv4->checksum != expected_ipv4_checksum) {
|
||||||
throw runtime_error(std::format(
|
throw runtime_error(std::format(
|
||||||
"IPv4 header checksum is incorrect ({:04X} expected, {:04X} received)",
|
"IPv4 header checksum is incorrect ({:04X} expected, {:04X} received)", expected_ipv4_checksum, fi.ipv4->checksum));
|
||||||
expected_ipv4_checksum, fi.ipv4->checksum));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((fi.ipv4->src_addr != c->ipv4_addr) && (fi.ipv4->src_addr != 0)) {
|
if ((fi.ipv4->src_addr != c->ipv4_addr) && (fi.ipv4->src_addr != 0)) {
|
||||||
@@ -512,8 +505,7 @@ asio::awaitable<void> IPStackSimulator::on_client_frame(shared_ptr<IPSSClient> c
|
|||||||
uint16_t expected_udp_checksum = fi.computed_udp4_checksum();
|
uint16_t expected_udp_checksum = fi.computed_udp4_checksum();
|
||||||
if (fi.udp->checksum != expected_udp_checksum) {
|
if (fi.udp->checksum != expected_udp_checksum) {
|
||||||
throw runtime_error(std::format(
|
throw runtime_error(std::format(
|
||||||
"UDP checksum is incorrect ({:04X} expected, {:04X} received)",
|
"UDP checksum is incorrect ({:04X} expected, {:04X} received)", expected_udp_checksum, fi.udp->checksum));
|
||||||
expected_udp_checksum, fi.udp->checksum));
|
|
||||||
}
|
}
|
||||||
co_await this->on_client_udp_frame(c, fi);
|
co_await this->on_client_udp_frame(c, fi);
|
||||||
|
|
||||||
@@ -521,8 +513,7 @@ asio::awaitable<void> IPStackSimulator::on_client_frame(shared_ptr<IPSSClient> c
|
|||||||
uint16_t expected_tcp_checksum = fi.computed_tcp4_checksum();
|
uint16_t expected_tcp_checksum = fi.computed_tcp4_checksum();
|
||||||
if (fi.tcp->checksum != expected_tcp_checksum) {
|
if (fi.tcp->checksum != expected_tcp_checksum) {
|
||||||
throw runtime_error(std::format(
|
throw runtime_error(std::format(
|
||||||
"TCP checksum is incorrect ({:04X} expected, {:04X} received)",
|
"TCP checksum is incorrect ({:04X} expected, {:04X} received)", expected_tcp_checksum, fi.tcp->checksum));
|
||||||
expected_tcp_checksum, fi.tcp->checksum));
|
|
||||||
}
|
}
|
||||||
co_await this->on_client_tcp_frame(c, fi);
|
co_await this->on_client_tcp_frame(c, fi);
|
||||||
|
|
||||||
@@ -988,8 +979,7 @@ asio::awaitable<void> IPStackSimulator::on_client_udp_frame(shared_ptr<IPSSClien
|
|||||||
r_ipv4.size = sizeof(IPv4Header) + sizeof(UDPHeader) + r_data.size();
|
r_ipv4.size = sizeof(IPv4Header) + sizeof(UDPHeader) + r_data.size();
|
||||||
r_udp.size = sizeof(UDPHeader) + r_data.size();
|
r_udp.size = sizeof(UDPHeader) + r_data.size();
|
||||||
r_ipv4.checksum = FrameInfo::computed_ipv4_header_checksum(r_ipv4);
|
r_ipv4.checksum = FrameInfo::computed_ipv4_header_checksum(r_ipv4);
|
||||||
r_udp.checksum = FrameInfo::computed_udp4_checksum(
|
r_udp.checksum = FrameInfo::computed_udp4_checksum(r_ipv4, r_udp, r_data.data(), r_data.size());
|
||||||
r_ipv4, r_udp, r_data.data(), r_data.size());
|
|
||||||
|
|
||||||
if (this->log.should_log(phosg::LogLevel::L_DEBUG)) {
|
if (this->log.should_log(phosg::LogLevel::L_DEBUG)) {
|
||||||
string remote_str = this->str_for_ipv4_netloc(fi.ipv4->src_addr, fi.udp->src_port);
|
string remote_str = this->str_for_ipv4_netloc(fi.ipv4->src_addr, fi.udp->src_port);
|
||||||
@@ -1103,11 +1093,15 @@ asio::awaitable<void> IPStackSimulator::on_client_tcp_frame(shared_ptr<IPSSClien
|
|||||||
conn_str, conn->acked_server_seq, conn->next_client_seq);
|
conn_str, conn->acked_server_seq, conn->next_client_seq);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// This frame isn't a SYN, so a connection object should already exist
|
// This frame isn't a SYN, so a connection object should already exist;
|
||||||
|
// ignore the frame if there's no connection
|
||||||
uint64_t key = this->tcp_conn_key_for_client_frame(fi);
|
uint64_t key = this->tcp_conn_key_for_client_frame(fi);
|
||||||
auto conn_it = c->tcp_connections.find(key);
|
auto conn_it = c->tcp_connections.find(key);
|
||||||
if (conn_it == c->tcp_connections.end()) {
|
if (conn_it == c->tcp_connections.end()) {
|
||||||
throw runtime_error("non-SYN frame does not correspond to any open TCP connection");
|
if (this->log.debug_f("Ignoring non-SYN TCP frame with no active connection")) {
|
||||||
|
phosg::print_data(stderr, fi.payload, fi.payload_size);
|
||||||
|
}
|
||||||
|
co_return;
|
||||||
}
|
}
|
||||||
auto& conn = conn_it->second;
|
auto& conn = conn_it->second;
|
||||||
bool conn_valid = true;
|
bool conn_valid = true;
|
||||||
@@ -1233,11 +1227,9 @@ asio::awaitable<void> IPStackSimulator::on_client_tcp_frame(shared_ptr<IPSSClien
|
|||||||
|
|
||||||
// Send the new data to the server
|
// Send the new data to the server
|
||||||
if (!conn->server_channel) {
|
if (!conn->server_channel) {
|
||||||
this->log.warning_f("Client sent data on TCP connection {}, but server channel is missing",
|
this->log.warning_f("Client sent data on TCP connection {}, but server channel is missing", conn_str);
|
||||||
conn_str);
|
|
||||||
} else if (!conn->server_channel->connected()) {
|
} else if (!conn->server_channel->connected()) {
|
||||||
this->log.warning_f("Client sent data on TCP connection {}, but server channel is disconnected",
|
this->log.warning_f("Client sent data on TCP connection {}, but server channel is disconnected", conn_str);
|
||||||
conn_str);
|
|
||||||
} else {
|
} else {
|
||||||
conn->server_channel->add_inbound_data(payload, payload_size);
|
conn->server_channel->add_inbound_data(payload, payload_size);
|
||||||
}
|
}
|
||||||
@@ -1246,8 +1238,7 @@ asio::awaitable<void> IPStackSimulator::on_client_tcp_frame(shared_ptr<IPSSClien
|
|||||||
conn->next_client_seq += payload_size;
|
conn->next_client_seq += payload_size;
|
||||||
conn->bytes_received += payload_size;
|
conn->bytes_received += payload_size;
|
||||||
if (conn->next_client_seq < payload_size) {
|
if (conn->next_client_seq < payload_size) {
|
||||||
this->log.warning_f("Client sequence number has wrapped (next={:08X}, bytes={:X})",
|
this->log.warning_f("Client sequence number has wrapped (next={:08X}, bytes={:X})", fi.tcp->seq_num, payload_size);
|
||||||
fi.tcp->seq_num, payload_size);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1396,7 +1387,7 @@ asio::awaitable<void> IPStackSimulator::open_server_connection(
|
|||||||
}
|
}
|
||||||
const auto& port_config = port_config_it->second;
|
const auto& port_config = port_config_it->second;
|
||||||
|
|
||||||
conn->server_channel = make_shared<IPSSChannel>(this->shared_from_this(), c, conn, port_config->version, 1);
|
conn->server_channel = make_shared<IPSSChannel>(this->shared_from_this(), c, conn, port_config->version, Language::ENGLISH);
|
||||||
|
|
||||||
if (!this->state->game_server.get()) {
|
if (!this->state->game_server.get()) {
|
||||||
this->log.error_f("No server available for TCP connection {}", conn_str);
|
this->log.error_f("No server available for TCP connection {}", conn_str);
|
||||||
@@ -1425,7 +1416,9 @@ std::shared_ptr<IPSSClient> IPStackSimulator::create_client(
|
|||||||
std::shared_ptr<IPSSSocket> listen_sock, asio::ip::tcp::socket&& client_sock) {
|
std::shared_ptr<IPSSSocket> listen_sock, asio::ip::tcp::socket&& client_sock) {
|
||||||
uint32_t addr = ipv4_addr_for_asio_addr(client_sock.remote_endpoint().address());
|
uint32_t addr = ipv4_addr_for_asio_addr(client_sock.remote_endpoint().address());
|
||||||
if (this->state->banned_ipv4_ranges->check(addr)) {
|
if (this->state->banned_ipv4_ranges->check(addr)) {
|
||||||
client_sock.close();
|
if (client_sock.is_open()) {
|
||||||
|
client_sock.close();
|
||||||
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public:
|
|||||||
std::weak_ptr<IPSSClient> ipss_client,
|
std::weak_ptr<IPSSClient> ipss_client,
|
||||||
std::weak_ptr<IPSSClient::TCPConnection> tcp_conn,
|
std::weak_ptr<IPSSClient::TCPConnection> tcp_conn,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const std::string& name = "",
|
const std::string& name = "",
|
||||||
phosg::TerminalFormat terminal_send_color = phosg::TerminalFormat::END,
|
phosg::TerminalFormat terminal_send_color = phosg::TerminalFormat::END,
|
||||||
phosg::TerminalFormat terminal_recv_color = phosg::TerminalFormat::END);
|
phosg::TerminalFormat terminal_recv_color = phosg::TerminalFormat::END);
|
||||||
|
|||||||
+1
-1
@@ -71,7 +71,7 @@ string encode_gvm(const phosg::ImageRGBA8888N& img, GVRDataFormat data_format, c
|
|||||||
w.put<GVMFileHeader>({.signature = 0x47564D48, .header_size = 0x48, .flags = 0x000F, .num_files = 1});
|
w.put<GVMFileHeader>({.signature = 0x47564D48, .header_size = 0x48, .flags = 0x000F, .num_files = 1});
|
||||||
GVMFileEntry file_entry;
|
GVMFileEntry file_entry;
|
||||||
file_entry.file_num = 0;
|
file_entry.file_num = 0;
|
||||||
file_entry.name.encode(internal_name, 1);
|
file_entry.name.encode(internal_name, Language::ENGLISH);
|
||||||
file_entry.data_format = data_format;
|
file_entry.data_format = data_format;
|
||||||
file_entry.format_flags = 0;
|
file_entry.format_flags = 0;
|
||||||
file_entry.dimensions = (dimensions_field << 4) | dimensions_field;
|
file_entry.dimensions = (dimensions_field << 4) | dimensions_field;
|
||||||
|
|||||||
+6
-9
@@ -32,7 +32,7 @@ ItemCreator::ItemCreator(
|
|||||||
std::shared_ptr<const ItemData::StackLimits> stack_limits,
|
std::shared_ptr<const ItemData::StackLimits> stack_limits,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
GameMode mode,
|
GameMode mode,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
uint8_t section_id,
|
uint8_t section_id,
|
||||||
std::shared_ptr<RandomGenerator> rand_crypt,
|
std::shared_ptr<RandomGenerator> rand_crypt,
|
||||||
shared_ptr<const BattleRules> restrictions)
|
shared_ptr<const BattleRules> restrictions)
|
||||||
@@ -1072,7 +1072,7 @@ void ItemCreator::generate_armor_shop_armors(vector<ItemData>& shop, size_t play
|
|||||||
item.data1[1] = 1;
|
item.data1[1] = 1;
|
||||||
item.data1[2] = pt.pop();
|
item.data1[2] = pt.pop();
|
||||||
|
|
||||||
if ((this->difficulty == 3) && (player_level > 99)) {
|
if ((this->difficulty == Difficulty::ULTIMATE) && (player_level > 99)) {
|
||||||
if (player_level > 150) {
|
if (player_level > 150) {
|
||||||
item.data1[2] += 3;
|
item.data1[2] += 3;
|
||||||
} else if (player_level >= 100) {
|
} else if (player_level >= 100) {
|
||||||
@@ -1116,7 +1116,7 @@ void ItemCreator::generate_armor_shop_shields(vector<ItemData>& shop, size_t pla
|
|||||||
item.data1[1] = 2;
|
item.data1[1] = 2;
|
||||||
item.data1[2] = pt.pop();
|
item.data1[2] = pt.pop();
|
||||||
|
|
||||||
if ((this->difficulty == 3) && (player_level > 99)) {
|
if ((this->difficulty == Difficulty::ULTIMATE) && (player_level > 99)) {
|
||||||
if (player_level > 150) {
|
if (player_level > 150) {
|
||||||
item.data1[2] += 3;
|
item.data1[2] += 3;
|
||||||
} else if (player_level >= 100) {
|
} else if (player_level >= 100) {
|
||||||
@@ -1360,7 +1360,7 @@ vector<ItemData> ItemCreator::generate_weapon_shop_contents(size_t player_level)
|
|||||||
}
|
}
|
||||||
|
|
||||||
size_t table_index;
|
size_t table_index;
|
||||||
if (this->difficulty == 3) {
|
if (this->difficulty == Difficulty::ULTIMATE) {
|
||||||
if (player_level < 11) {
|
if (player_level < 11) {
|
||||||
table_index = 0;
|
table_index = 0;
|
||||||
} else if (player_level < 26) {
|
} else if (player_level < 26) {
|
||||||
@@ -1654,8 +1654,7 @@ void ItemCreator::generate_weapon_shop_item_bonus1(
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
const auto* range = this->weapon_random_set->get_bonus_range(0, table_index);
|
const auto* range = this->weapon_random_set->get_bonus_range(0, table_index);
|
||||||
item.data1[7] = bonus_values.at(max<size_t>(
|
item.data1[7] = bonus_values.at(max<size_t>(this->rand_int(range->max + 1), range->min));
|
||||||
this->rand_int(range->max + 1), range->min));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1700,8 +1699,7 @@ void ItemCreator::generate_weapon_shop_item_bonus2(ItemData& item, size_t player
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
const auto* range = this->weapon_random_set->get_bonus_range(1, table_index);
|
const auto* range = this->weapon_random_set->get_bonus_range(1, table_index);
|
||||||
item.data1[9] = bonus_values.at(max<size_t>(
|
item.data1[9] = bonus_values.at(max<size_t>(this->rand_int(range->max + 1), range->min));
|
||||||
this->rand_int(range->max + 1), range->min));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1752,7 +1750,6 @@ ItemData ItemCreator::base_item_for_specialized_box(uint32_t param4, uint32_t pa
|
|||||||
case 0x04:
|
case 0x04:
|
||||||
item.data2d = ((param5 >> 0x10) & 0xFFFF) * 10;
|
item.data2d = ((param5 >> 0x10) & 0xFFFF) * 10;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw runtime_error("invalid item class");
|
throw runtime_error("invalid item class");
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ public:
|
|||||||
std::shared_ptr<const ItemData::StackLimits> stack_limits,
|
std::shared_ptr<const ItemData::StackLimits> stack_limits,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
GameMode mode,
|
GameMode mode,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
uint8_t section_id,
|
uint8_t section_id,
|
||||||
std::shared_ptr<RandomGenerator> rand_crypt,
|
std::shared_ptr<RandomGenerator> rand_crypt,
|
||||||
std::shared_ptr<const BattleRules> restrictions = nullptr);
|
std::shared_ptr<const BattleRules> restrictions = nullptr);
|
||||||
@@ -61,7 +61,7 @@ private:
|
|||||||
std::shared_ptr<const ItemData::StackLimits> stack_limits;
|
std::shared_ptr<const ItemData::StackLimits> stack_limits;
|
||||||
Episode episode;
|
Episode episode;
|
||||||
GameMode mode;
|
GameMode mode;
|
||||||
uint8_t difficulty;
|
Difficulty difficulty;
|
||||||
uint8_t section_id;
|
uint8_t section_id;
|
||||||
std::shared_ptr<const RareItemSet> rare_item_set;
|
std::shared_ptr<const RareItemSet> rare_item_set;
|
||||||
std::shared_ptr<const ArmorRandomSet> armor_random_set;
|
std::shared_ptr<const ArmorRandomSet> armor_random_set;
|
||||||
|
|||||||
+10
-7
@@ -81,13 +81,14 @@ struct ItemData {
|
|||||||
|
|
||||||
// QUICK ITEM FORMAT REFERENCE
|
// QUICK ITEM FORMAT REFERENCE
|
||||||
// data1/0 data1/4 data1/8 data2
|
// data1/0 data1/4 data1/8 data2
|
||||||
// Weapon: 00ZZZZGG SSNNAABB AABBAABB 00000000
|
// Weapon: 00ZZZZGG SSNNAABB AABBAABB 00000000
|
||||||
// Armor: 0101ZZ00 FFTTDDDD EEEEXXXX 00000000
|
// Armor: 0101ZZ00 FFTTDDDD EEEEXXXX 00000000
|
||||||
// Shield: 0102ZZ00 FFTTDDDD EEEEXXXX 00000000
|
// Shield: 0102ZZ00 FFTTDDDD EEEEXXXX 00000000
|
||||||
// Unit: 0103ZZ00 FF00RRRR 0000XXXX 00000000
|
// Unit: 0103ZZ00 FF00RRRR 0000XXXX 00000000
|
||||||
// Mag: 02ZZLLWW HHHHIIII JJJJKKKK YYQQPPVV
|
// Mag: 02ZZLLWW HHHHIIII JJJJKKKK YYQQPPVV
|
||||||
// Tool: 03ZZZZUU 00CC0000 0000XXXX 00000000
|
// Tool: 03ZZZZUU 00CC0000 0000XXXX 00000000
|
||||||
// Meseta: 04000000 00000000 00000000 MMMMMMMM
|
// Tech disk: 0302&&UU %%CC0000 0000XXXX 00000000
|
||||||
|
// Meseta: 04000000 00000000 00000000 MMMMMMMM
|
||||||
// A = attribute type (for S-ranks, custom name; last pair is kill count for some weapons)
|
// A = attribute type (for S-ranks, custom name; last pair is kill count for some weapons)
|
||||||
// B = attribute amount (for S-ranks, custom name; last pair is kill count for some weapons)
|
// B = attribute amount (for S-ranks, custom name; last pair is kill count for some weapons)
|
||||||
// C = stack size (for tools)
|
// C = stack size (for tools)
|
||||||
@@ -113,6 +114,8 @@ struct ItemData {
|
|||||||
// X = kill count (big-endian; high bit always set)
|
// X = kill count (big-endian; high bit always set)
|
||||||
// Y = mag synchro
|
// Y = mag synchro
|
||||||
// Z = item ID
|
// Z = item ID
|
||||||
|
// & = technique level
|
||||||
|
// % = technique number
|
||||||
// Note: PSO GC erroneously byteswaps data2 even when the item is a mag. This
|
// Note: PSO GC erroneously byteswaps data2 even when the item is a mag. This
|
||||||
// makes it incompatible with little-endian versions of PSO (i.e. all other
|
// makes it incompatible with little-endian versions of PSO (i.e. all other
|
||||||
// versions). We manually byteswap data2 upon receipt and immediately before
|
// versions). We manually byteswap data2 upon receipt and immediately before
|
||||||
|
|||||||
+146
-23
@@ -12,9 +12,16 @@ ItemNameIndex::ItemNameIndex(
|
|||||||
limits(limits) {
|
limits(limits) {
|
||||||
|
|
||||||
for (uint32_t primary_identifier : item_parameter_table->compute_all_valid_primary_identifiers()) {
|
for (uint32_t primary_identifier : item_parameter_table->compute_all_valid_primary_identifiers()) {
|
||||||
|
// 00000000 is a valid primary identifier but not a valid weapon; skip it
|
||||||
|
if (primary_identifier == 0x00000000) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const string* name = nullptr;
|
const string* name = nullptr;
|
||||||
|
bool is_es_weapon = false;
|
||||||
try {
|
try {
|
||||||
ItemData item = ItemData::from_primary_identifier(*this->limits, primary_identifier);
|
ItemData item = ItemData::from_primary_identifier(*this->limits, primary_identifier);
|
||||||
|
is_es_weapon = item.is_s_rank_weapon();
|
||||||
name = &name_coll.at(item_parameter_table->get_item_id(item));
|
name = &name_coll.at(item_parameter_table->get_item_id(item));
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
}
|
}
|
||||||
@@ -25,11 +32,14 @@ ItemNameIndex::ItemNameIndex(
|
|||||||
meta->name = *name;
|
meta->name = *name;
|
||||||
this->primary_identifier_index.emplace(meta->primary_identifier, meta);
|
this->primary_identifier_index.emplace(meta->primary_identifier, meta);
|
||||||
this->name_index.emplace(phosg::tolower(meta->name), meta);
|
this->name_index.emplace(phosg::tolower(meta->name), meta);
|
||||||
|
if (is_es_weapon && ((primary_identifier & 0x0000FFFF) == 0x00000000)) {
|
||||||
|
this->es_name_index.emplace(phosg::tolower(meta->name), meta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char* s_rank_name_characters = "\0ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_";
|
static std::string s_rank_name_characters("\0ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_", 0x20);
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static const array<const char*, 0x29> name_for_weapon_special = {
|
static const array<const char*, 0x29> name_for_weapon_special = {
|
||||||
@@ -201,7 +211,7 @@ std::string ItemNameIndex::describe_item(const ItemData& item, uint8_t flags) co
|
|||||||
|
|
||||||
string name;
|
string name;
|
||||||
for (size_t x = 0; x < 8; x++) {
|
for (size_t x = 0; x < 8; x++) {
|
||||||
char ch = s_rank_name_characters[char_indexes[x]];
|
char ch = s_rank_name_characters.at(char_indexes[x]);
|
||||||
if (ch == 0) {
|
if (ch == 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -309,7 +319,7 @@ std::string ItemNameIndex::describe_item(const ItemData& item, uint8_t flags) co
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
ret_tokens.emplace_back(format_stat(def) + "/" + format_stat(pow) + "/" + format_stat(dex) + "/" + format_stat(mind));
|
ret_tokens.emplace_back(format_stat(def) + "/" + format_stat(pow) + "/" + format_stat(dex) + "/" + format_stat(mind));
|
||||||
ret_tokens.emplace_back(std::format("{}%", item.data2[0]));
|
ret_tokens.emplace_back(std::format("{}{}", item.data2[0], include_color_escapes ? "%%" : "%"));
|
||||||
ret_tokens.emplace_back(std::format("{}IQ", item.data2[1]));
|
ret_tokens.emplace_back(std::format("{}IQ", item.data2[1]));
|
||||||
|
|
||||||
uint8_t flags = item.data2[2];
|
uint8_t flags = item.data2[2];
|
||||||
@@ -413,6 +423,112 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (desc.starts_with("es ")) {
|
||||||
|
auto parse_name = [&](const std::string& token) -> void {
|
||||||
|
if (token.size() > 8) {
|
||||||
|
throw runtime_error("s-rank name too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t char_indexes[8] = {0, 0, 0, 0, 0, 0, 0, 0};
|
||||||
|
for (size_t z = 0; z < token.size(); z++) {
|
||||||
|
char ch = toupper(token[z]);
|
||||||
|
size_t pos = s_rank_name_characters.find(ch);
|
||||||
|
if (pos == std::string::npos) {
|
||||||
|
throw runtime_error(std::format("s-rank name contains invalid character {:02X} ({})", ch, ch));
|
||||||
|
}
|
||||||
|
char_indexes[z] = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.data1w[3] = phosg::bswap16(0x8000 | (char_indexes[1] & 0x1F) | ((char_indexes[0] & 0x1F) << 5));
|
||||||
|
ret.data1w[4] = phosg::bswap16(0x8000 | (char_indexes[4] & 0x1F) | ((char_indexes[3] & 0x1F) << 5) | ((char_indexes[2] & 0x1F) << 10));
|
||||||
|
ret.data1w[5] = phosg::bswap16(0x8000 | (char_indexes[7] & 0x1F) | ((char_indexes[6] & 0x1F) << 5) | ((char_indexes[5] & 0x1F) << 10));
|
||||||
|
};
|
||||||
|
auto parse_special = [&](const std::string& token) -> bool {
|
||||||
|
for (size_t z = 0; z < name_for_s_rank_special.size(); z++) {
|
||||||
|
if (name_for_s_rank_special[z] && (token == phosg::tolower(name_for_s_rank_special[z]))) {
|
||||||
|
ret.data1[2] = z;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
auto parse_grind = [&](const std::string& token) -> bool {
|
||||||
|
if (token.starts_with('+')) {
|
||||||
|
ret.data1[3] = stoul(token.substr(1), nullptr, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
auto parse_type = [&](const std::string& token) -> void {
|
||||||
|
const auto& meta = this->es_name_index.at(token);
|
||||||
|
if (meta->primary_identifier & 0xFF00FFFF) {
|
||||||
|
throw std::runtime_error("ES weapon has invalid bits in primary identifier");
|
||||||
|
}
|
||||||
|
ret.data1[1] = meta->primary_identifier >> 16;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Syntax: ES [NAME] [SPECIAL] TYPE [+GRIND]
|
||||||
|
auto tokens = phosg::split(desc, ' ');
|
||||||
|
switch (tokens.size()) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
throw std::runtime_error("ES weapon type is missing");
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// Must be ES TYPE
|
||||||
|
parse_name(tokens[1]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
// Any of the following:
|
||||||
|
// ES TYPE +N
|
||||||
|
// ES SPECIAL TYPE
|
||||||
|
// ES NAME TYPE
|
||||||
|
if (parse_grind(tokens[2])) {
|
||||||
|
parse_type(tokens[1]);
|
||||||
|
} else if (parse_special(tokens[1])) {
|
||||||
|
parse_type(tokens[2]);
|
||||||
|
} else {
|
||||||
|
parse_name(tokens[1]);
|
||||||
|
parse_type(tokens[2]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// Any of the following:
|
||||||
|
// ES SPECIAL TYPE +N
|
||||||
|
// ES NAME TYPE +N
|
||||||
|
// ES NAME SPECIAL TYPE
|
||||||
|
if (parse_grind(tokens[3])) {
|
||||||
|
if (!parse_special(tokens[1])) {
|
||||||
|
parse_name(tokens[1]);
|
||||||
|
}
|
||||||
|
parse_type(tokens[2]);
|
||||||
|
} else {
|
||||||
|
parse_name(tokens[1]);
|
||||||
|
if (!parse_special(tokens[2])) {
|
||||||
|
throw std::runtime_error("invalid ES special");
|
||||||
|
}
|
||||||
|
parse_type(tokens[3]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
// Must be ES NAME SPECIAL TYPE +N
|
||||||
|
parse_name(tokens[1]);
|
||||||
|
if (!parse_special(tokens[2])) {
|
||||||
|
throw std::runtime_error("invalid ES special");
|
||||||
|
}
|
||||||
|
parse_type(tokens[3]);
|
||||||
|
if (!parse_grind(tokens[4])) {
|
||||||
|
throw std::runtime_error("invalid grind");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw std::runtime_error("too many ES weapon tokens");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
if (desc.starts_with("disk:")) {
|
if (desc.starts_with("disk:")) {
|
||||||
auto tokens = phosg::split(desc, ' ');
|
auto tokens = phosg::split(desc, ' ');
|
||||||
tokens[0] = tokens[0].substr(5); // Trim off "disk:"
|
tokens[0] = tokens[0].substr(5); // Trim off "disk:"
|
||||||
@@ -454,7 +570,6 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
|
|||||||
desc = desc.substr(z);
|
desc = desc.substr(z);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: It'd be nice to be able to parse S-rank weapon specials here too.
|
|
||||||
uint8_t weapon_special = 0;
|
uint8_t weapon_special = 0;
|
||||||
if (!skip_special) {
|
if (!skip_special) {
|
||||||
for (size_t z = 0; z < name_for_weapon_special.size(); z++) {
|
for (size_t z = 0; z < name_for_weapon_special.size(); z++) {
|
||||||
@@ -507,6 +622,7 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
|
|||||||
// kill count if unsealable
|
// kill count if unsealable
|
||||||
ret.data1[4] = weapon_special | (is_wrapped ? 0x40 : 0x00) | (is_unidentified ? 0x80 : 0x00);
|
ret.data1[4] = weapon_special | (is_wrapped ? 0x40 : 0x00) | (is_unidentified ? 0x80 : 0x00);
|
||||||
|
|
||||||
|
bool kill_count_set = false;
|
||||||
auto tokens = phosg::split(desc, ' ');
|
auto tokens = phosg::split(desc, ' ');
|
||||||
for (auto& token : tokens) {
|
for (auto& token : tokens) {
|
||||||
if (token.empty()) {
|
if (token.empty()) {
|
||||||
@@ -517,26 +633,11 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
|
|||||||
ret.data1[3] = stoul(token, nullptr, 10);
|
ret.data1[3] = stoul(token, nullptr, 10);
|
||||||
|
|
||||||
} else if (ret.is_s_rank_weapon()) {
|
} else if (ret.is_s_rank_weapon()) {
|
||||||
if (token.size() > 8) {
|
throw std::runtime_error("ES weapon must be prefixed with \"ES\"");
|
||||||
throw runtime_error("s-rank name too long");
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t char_indexes[8] = {0, 0, 0, 0, 0, 0, 0, 0};
|
|
||||||
for (size_t z = 0; z < token.size(); z++) {
|
|
||||||
char ch = toupper(token[z]);
|
|
||||||
const char* pos = strchr(s_rank_name_characters, ch);
|
|
||||||
if (!pos) {
|
|
||||||
throw runtime_error(std::format("s-rank name contains invalid character {:02X} ({})", ch, ch));
|
|
||||||
}
|
|
||||||
char_indexes[z] = (pos - s_rank_name_characters);
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.data1w[3] = phosg::bswap16(0x8000 | (char_indexes[1] & 0x1F) | ((char_indexes[0] & 0x1F) << 5));
|
|
||||||
ret.data1w[4] = phosg::bswap16(0x8000 | (char_indexes[4] & 0x1F) | ((char_indexes[3] & 0x1F) << 5) | ((char_indexes[2] & 0x1F) << 10));
|
|
||||||
ret.data1w[5] = phosg::bswap16(0x8000 | (char_indexes[7] & 0x1F) | ((char_indexes[6] & 0x1F) << 5) | ((char_indexes[5] & 0x1F) << 10));
|
|
||||||
|
|
||||||
} else if (token.starts_with("k:")) {
|
} else if (token.starts_with("k:")) {
|
||||||
ret.set_kill_count(stoul(token.substr(2), nullptr, 0));
|
ret.set_kill_count(stoul(token.substr(2), nullptr, 0));
|
||||||
|
kill_count_set = true;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
auto p_tokens = phosg::split(token, '/');
|
auto p_tokens = phosg::split(token, '/');
|
||||||
@@ -560,7 +661,7 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->item_parameter_table->is_unsealable_item(ret)) {
|
if (this->item_parameter_table->is_unsealable_item(ret) && !kill_count_set) {
|
||||||
ret.set_kill_count(0);
|
ret.set_kill_count(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,7 +674,29 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
|
|||||||
{"+", 0x0002},
|
{"+", 0x0002},
|
||||||
{"++", 0x0004},
|
{"++", 0x0004},
|
||||||
});
|
});
|
||||||
ret.data1w[3] = modifiers.at(desc);
|
bool kill_count_set = false;
|
||||||
|
for (const auto& token : phosg::split(desc, ' ')) {
|
||||||
|
if (token == "--") {
|
||||||
|
ret.data1w[3] = 0xFFFC;
|
||||||
|
} else if (token == "-") {
|
||||||
|
ret.data1w[3] = 0xFFFE;
|
||||||
|
} else if (token == "+") {
|
||||||
|
ret.data1w[3] = 0x0002;
|
||||||
|
} else if (token == "++") {
|
||||||
|
ret.data1w[3] = 0x0004;
|
||||||
|
} else if (token.starts_with('+')) {
|
||||||
|
ret.data1w[3] = stoul(token.substr(1), nullptr, 0);
|
||||||
|
} else if (token.starts_with('-')) {
|
||||||
|
ret.data1w[3] = static_cast<uint16_t>(stol(token, nullptr, 0));
|
||||||
|
} else if (token.starts_with("k:")) {
|
||||||
|
ret.set_kill_count(stoul(token.substr(2), nullptr, 0));
|
||||||
|
kill_count_set = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->item_parameter_table->is_unsealable_item(ret) && !kill_count_set) {
|
||||||
|
ret.set_kill_count(0);
|
||||||
|
}
|
||||||
|
|
||||||
} else { // Armor/shield
|
} else { // Armor/shield
|
||||||
for (const auto& token : phosg::split(desc, ' ')) {
|
for (const auto& token : phosg::split(desc, ' ')) {
|
||||||
|
|||||||
@@ -56,4 +56,5 @@ private:
|
|||||||
|
|
||||||
std::unordered_map<uint32_t, std::shared_ptr<const ItemMetadata>> primary_identifier_index;
|
std::unordered_map<uint32_t, std::shared_ptr<const ItemMetadata>> primary_identifier_index;
|
||||||
std::map<std::string, std::shared_ptr<const ItemMetadata>> name_index;
|
std::map<std::string, std::shared_ptr<const ItemMetadata>> name_index;
|
||||||
|
std::map<std::string, std::shared_ptr<const ItemMetadata>> es_name_index;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -200,6 +200,16 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<RandomG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (primary_identifier == 0x03170000) { // Unopened Hunters Report
|
||||||
|
// The unopened Hunters Report's rank is stored in the kill count field; using the unopened report copies the rank
|
||||||
|
// to data1[2] and replaces the inventory item with a new item with the same ID. The game also moves the item to
|
||||||
|
// the end of the inventory, so we do the same.
|
||||||
|
const auto& stack_limits = *s->item_stack_limits(c->version());
|
||||||
|
auto report_item = player->remove_item(item.data.id, 1, stack_limits);
|
||||||
|
report_item.data1[2] = report_item.get_kill_count();
|
||||||
|
player->add_item(report_item, stack_limits);
|
||||||
|
should_delete_item = false;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Use item combinations table from ItemPMT
|
// Use item combinations table from ItemPMT
|
||||||
bool combo_applied = false;
|
bool combo_applied = false;
|
||||||
|
|||||||
+8
-20
@@ -146,26 +146,9 @@ Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id, bool is_game)
|
|||||||
: server_state(s),
|
: server_state(s),
|
||||||
log(std::format("[{}:{:X}] ", is_game ? "Game" : "Lobby", id), lobby_log.min_level),
|
log(std::format("[{}:{:X}] ", is_game ? "Game" : "Lobby", id), lobby_log.min_level),
|
||||||
lobby_id(id),
|
lobby_id(id),
|
||||||
min_level(0),
|
|
||||||
max_level(0xFFFFFFFF),
|
|
||||||
next_game_item_id(0xCC000000),
|
|
||||||
allowed_versions(0x0000),
|
|
||||||
override_section_id(0xFF),
|
|
||||||
episode(Episode::NONE),
|
|
||||||
mode(GameMode::NORMAL),
|
|
||||||
difficulty(0),
|
|
||||||
base_exp_multiplier(1.0f),
|
|
||||||
exp_share_multiplier(0.5f),
|
|
||||||
challenge_exp_multiplier(1.0f),
|
|
||||||
random_seed(phosg::random_object<uint32_t>()),
|
random_seed(phosg::random_object<uint32_t>()),
|
||||||
rand_crypt(make_shared<DisabledRandomGenerator>()),
|
rand_crypt(make_shared<DisabledRandomGenerator>()),
|
||||||
drop_mode(ServerDropMode::CLIENT),
|
drop_mode(ServerDropMode::CLIENT),
|
||||||
event(0),
|
|
||||||
block(0),
|
|
||||||
leader_id(0),
|
|
||||||
max_clients(12),
|
|
||||||
enabled_flags(0),
|
|
||||||
idle_timeout_usecs(0),
|
|
||||||
idle_timeout_timer(*s->io_context) {
|
idle_timeout_timer(*s->io_context) {
|
||||||
this->log.info_f("Created");
|
this->log.info_f("Created");
|
||||||
if (is_game) {
|
if (is_game) {
|
||||||
@@ -228,19 +211,23 @@ void Lobby::create_item_creator(Version logic_version) {
|
|||||||
} else {
|
} else {
|
||||||
rand_crypt = make_shared<MT19937Generator>(this->rand_crypt->seed());
|
rand_crypt = make_shared<MT19937Generator>(this->rand_crypt->seed());
|
||||||
}
|
}
|
||||||
|
uint8_t effective_section_id = this->effective_section_id();
|
||||||
|
if (effective_section_id >= 10) {
|
||||||
|
effective_section_id = 0x00;
|
||||||
|
}
|
||||||
this->item_creator = make_shared<ItemCreator>(
|
this->item_creator = make_shared<ItemCreator>(
|
||||||
s->common_item_set(logic_version, this->quest),
|
s->common_item_set(logic_version, this->quest),
|
||||||
s->rare_item_set(logic_version, this->quest),
|
s->rare_item_set(logic_version, this->quest),
|
||||||
s->armor_random_set,
|
s->armor_random_set,
|
||||||
s->tool_random_set,
|
s->tool_random_set,
|
||||||
s->weapon_random_sets.at(this->difficulty),
|
s->weapon_random_set(this->difficulty),
|
||||||
s->tekker_adjustment_set,
|
s->tekker_adjustment_set,
|
||||||
s->item_parameter_table(logic_version),
|
s->item_parameter_table(logic_version),
|
||||||
s->item_stack_limits(logic_version),
|
s->item_stack_limits(logic_version),
|
||||||
this->episode,
|
this->episode,
|
||||||
(this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode,
|
(this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode,
|
||||||
this->difficulty,
|
this->difficulty,
|
||||||
this->effective_section_id(),
|
effective_section_id,
|
||||||
rand_crypt,
|
rand_crypt,
|
||||||
this->quest ? this->quest->meta.battle_rules : nullptr);
|
this->quest ? this->quest->meta.battle_rules : nullptr);
|
||||||
}
|
}
|
||||||
@@ -256,7 +243,7 @@ uint8_t Lobby::effective_section_id() const {
|
|||||||
if (leader) {
|
if (leader) {
|
||||||
return leader->character_file()->disp.visual.section_id;
|
return leader->character_file()->disp.visual.section_id;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t Lobby::quest_version_flags() const {
|
uint16_t Lobby::quest_version_flags() const {
|
||||||
@@ -326,6 +313,7 @@ void Lobby::create_ep3_server() {
|
|||||||
.rand_crypt = this->rand_crypt,
|
.rand_crypt = this->rand_crypt,
|
||||||
.tournament = tourn,
|
.tournament = tourn,
|
||||||
.trap_card_ids = s->ep3_trap_card_ids,
|
.trap_card_ids = s->ep3_trap_card_ids,
|
||||||
|
.output_queue = nullptr,
|
||||||
};
|
};
|
||||||
if (is_nte) {
|
if (is_nte) {
|
||||||
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
||||||
|
|||||||
+22
-22
@@ -96,12 +96,12 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
|||||||
|
|
||||||
uint32_t lobby_id;
|
uint32_t lobby_id;
|
||||||
|
|
||||||
uint32_t min_level;
|
uint32_t min_level = 0;
|
||||||
uint32_t max_level;
|
uint32_t max_level = 0xFFFFFFFF;
|
||||||
|
|
||||||
// Game state
|
// Game state
|
||||||
std::array<uint32_t, 12> next_item_id_for_client;
|
std::array<uint32_t, 12> next_item_id_for_client;
|
||||||
uint32_t next_game_item_id;
|
uint32_t next_game_item_id = 0xCC000000;
|
||||||
std::vector<FloorItemManager> floor_item_managers;
|
std::vector<FloorItemManager> floor_item_managers;
|
||||||
std::shared_ptr<const MapState::RareEnemyRates> rare_enemy_rates;
|
std::shared_ptr<const MapState::RareEnemyRates> rare_enemy_rates;
|
||||||
std::shared_ptr<MapState> map_state; // Always null for lobbies, never null for games
|
std::shared_ptr<MapState> map_state; // Always null for lobbies, never null for games
|
||||||
@@ -114,22 +114,22 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
|||||||
// Bits in allowed_versions specify who is allowed to join this game. The
|
// Bits in allowed_versions specify who is allowed to join this game. The
|
||||||
// bits are indexed as (1 << version), where version is a value from the
|
// bits are indexed as (1 << version), where version is a value from the
|
||||||
// Version enum.
|
// Version enum.
|
||||||
uint16_t allowed_versions;
|
uint16_t allowed_versions = 0x0000;
|
||||||
uint8_t creator_section_id;
|
uint8_t creator_section_id = 0xFF;
|
||||||
uint8_t override_section_id;
|
uint8_t override_section_id = 0xFF;
|
||||||
Episode episode;
|
Episode episode = Episode::NONE;
|
||||||
GameMode mode;
|
GameMode mode = GameMode::NORMAL;
|
||||||
uint8_t difficulty; // 0-3
|
Difficulty difficulty = Difficulty::NORMAL;
|
||||||
float base_exp_multiplier;
|
float base_exp_multiplier = 1.0f;
|
||||||
float exp_share_multiplier;
|
float exp_share_multiplier = 0.5f;
|
||||||
float challenge_exp_multiplier;
|
float challenge_exp_multiplier = 1.0f;
|
||||||
std::string password;
|
std::string password;
|
||||||
std::string name;
|
std::string name;
|
||||||
// This seed is also sent to the client for rare enemy generation
|
// This seed is also sent to the client for rare enemy generation
|
||||||
uint32_t random_seed;
|
uint32_t random_seed = 0;
|
||||||
std::shared_ptr<RandomGenerator> rand_crypt;
|
std::shared_ptr<RandomGenerator> rand_crypt;
|
||||||
uint8_t allowed_drop_modes;
|
uint8_t allowed_drop_modes = 0x1F;
|
||||||
ServerDropMode drop_mode;
|
ServerDropMode drop_mode = ServerDropMode::CLIENT;
|
||||||
std::shared_ptr<ItemCreator> item_creator; // Always null for lobbies, never null for games
|
std::shared_ptr<ItemCreator> item_creator; // Always null for lobbies, never null for games
|
||||||
|
|
||||||
struct ChallengeParameters {
|
struct ChallengeParameters {
|
||||||
@@ -164,11 +164,11 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
|||||||
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_ex_result_values;
|
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_ex_result_values;
|
||||||
|
|
||||||
// Lobby stuff
|
// Lobby stuff
|
||||||
uint8_t event;
|
uint8_t event = 0;
|
||||||
uint8_t block;
|
uint8_t block = 0;
|
||||||
uint8_t leader_id;
|
uint8_t leader_id = 0;
|
||||||
uint8_t max_clients;
|
uint8_t max_clients = 12;
|
||||||
uint32_t enabled_flags;
|
uint32_t enabled_flags = 0;
|
||||||
std::shared_ptr<const Quest> quest;
|
std::shared_ptr<const Quest> quest;
|
||||||
std::array<std::shared_ptr<Client>, 12> clients;
|
std::array<std::shared_ptr<Client>, 12> clients;
|
||||||
// Keys in this map are client_id
|
// Keys in this map are client_id
|
||||||
@@ -176,7 +176,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
|||||||
|
|
||||||
// This is only used when the PERSISTENT flag is set and idle_timeout_usecs
|
// This is only used when the PERSISTENT flag is set and idle_timeout_usecs
|
||||||
// is not zero
|
// is not zero
|
||||||
uint64_t idle_timeout_usecs;
|
uint64_t idle_timeout_usecs = 0;
|
||||||
asio::steady_timer idle_timeout_timer;
|
asio::steady_timer idle_timeout_timer;
|
||||||
|
|
||||||
Lobby(std::shared_ptr<ServerState> s, uint32_t id, bool is_game);
|
Lobby(std::shared_ptr<ServerState> s, uint32_t id, bool is_game);
|
||||||
@@ -206,7 +206,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
|||||||
std::shared_ptr<ServerState> require_server_state() const;
|
std::shared_ptr<ServerState> require_server_state() const;
|
||||||
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
|
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
|
||||||
void create_item_creator(Version logic_version = Version::UNKNOWN);
|
void create_item_creator(Version logic_version = Version::UNKNOWN);
|
||||||
uint8_t effective_section_id() const;
|
uint8_t effective_section_id() const; // Returns 0xFF if not assigned (e.g. empty persistent game)
|
||||||
uint16_t quest_version_flags() const;
|
uint16_t quest_version_flags() const;
|
||||||
uint8_t client_extension_flags() const;
|
uint8_t client_extension_flags() const;
|
||||||
void load_maps();
|
void load_maps();
|
||||||
|
|||||||
+315
-92
@@ -37,6 +37,7 @@
|
|||||||
#include "PPKArchive.hh"
|
#include "PPKArchive.hh"
|
||||||
#include "PSOGCObjectGraph.hh"
|
#include "PSOGCObjectGraph.hh"
|
||||||
#include "PSOProtocol.hh"
|
#include "PSOProtocol.hh"
|
||||||
|
#include "PatchDownloadSession.hh"
|
||||||
#include "Quest.hh"
|
#include "Quest.hh"
|
||||||
#include "QuestScript.hh"
|
#include "QuestScript.hh"
|
||||||
#include "ReplaySession.hh"
|
#include "ReplaySession.hh"
|
||||||
@@ -1609,7 +1610,7 @@ Action a_disassemble_quest_script(
|
|||||||
if (!args.get<bool>("decompressed")) {
|
if (!args.get<bool>("decompressed")) {
|
||||||
data = prs_decompress(data);
|
data = prs_decompress(data);
|
||||||
}
|
}
|
||||||
uint8_t override_language = args.get<uint8_t>("language", 0xFF);
|
Language override_language = static_cast<Language>(args.get<uint8_t>("language", 0xFF));
|
||||||
bool reassembly_mode = args.get<bool>("reassembly");
|
bool reassembly_mode = args.get<bool>("reassembly");
|
||||||
bool use_qedit_names = args.get<bool>("qedit");
|
bool use_qedit_names = args.get<bool>("qedit");
|
||||||
string result = disassemble_quest_script(data.data(), data.size(), version, override_language, reassembly_mode, use_qedit_names);
|
string result = disassemble_quest_script(data.data(), data.size(), version, override_language, reassembly_mode, use_qedit_names);
|
||||||
@@ -1713,19 +1714,23 @@ Action a_assemble_quest_script(
|
|||||||
|
|
||||||
Action a_assemble_all_patches(
|
Action a_assemble_all_patches(
|
||||||
"assemble-all-patches", "\
|
"assemble-all-patches", "\
|
||||||
assemble-all-patches\n\
|
assemble-all-patches [--skip-encrypted]\n\
|
||||||
Assemble all patches in the system/client-functions directory, and produce\n\
|
Assemble all patches in the system/client-functions directory, and produce\n\
|
||||||
two compiled .bin files for each patch (one unencrypted, for most PSO\n\
|
two compiled .bin files for each patch (one unencrypted, for most PSO\n\
|
||||||
versions, and one encrypted, for PSO GC JP v1.4, JP Ep3, and Ep3 Trial\n\
|
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",
|
Edition). The output files are saved in system/client-functions.\n",
|
||||||
+[](phosg::Arguments&) {
|
+[](phosg::Arguments& args) {
|
||||||
auto fci = make_shared<FunctionCodeIndex>("system/client-functions");
|
auto fci = make_shared<FunctionCodeIndex>("system/client-functions", false);
|
||||||
|
|
||||||
auto process_code = +[](shared_ptr<const CompiledFunctionCode> code,
|
bool skip_encrypted = args.get<bool>("skip-encrypted");
|
||||||
uint32_t checksum_addr,
|
auto process_code = [&](shared_ptr<const CompiledFunctionCode> code,
|
||||||
uint32_t checksum_size,
|
uint32_t checksum_addr,
|
||||||
uint32_t override_start_addr) -> void {
|
uint32_t checksum_size,
|
||||||
|
uint32_t override_start_addr) -> void {
|
||||||
for (uint8_t encrypted = 0; encrypted < 2; encrypted++) {
|
for (uint8_t encrypted = 0; encrypted < 2; encrypted++) {
|
||||||
|
if (encrypted && skip_encrypted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
phosg::StringWriter w;
|
phosg::StringWriter w;
|
||||||
string data = prepare_send_function_call_data(
|
string data = prepare_send_function_call_data(
|
||||||
code, {}, nullptr, 0, checksum_addr, checksum_size, override_start_addr, encrypted);
|
code, {}, nullptr, 0, checksum_addr, checksum_size, override_start_addr, encrypted);
|
||||||
@@ -2028,50 +2033,70 @@ Action a_download_files(
|
|||||||
phosg::load_object_file<PSOBBEncryption::KeyFile>("system/blueburst/keys/" + key_file_name + ".nsk"));
|
phosg::load_object_file<PSOBBEncryption::KeyFile>("system/blueburst/keys/" + key_file_name + ".nsk"));
|
||||||
}
|
}
|
||||||
auto [remote_host, remote_port] = phosg::parse_netloc(args.get<string>(1));
|
auto [remote_host, remote_port] = phosg::parse_netloc(args.get<string>(1));
|
||||||
auto character = PSOCHARFile::load_shared(args.get<string>("character", true), false).character_file;
|
|
||||||
auto ship_menu_selections_str = args.get<string>("ship-menu-selections", false);
|
|
||||||
|
|
||||||
unordered_set<string> ship_menu_selections;
|
|
||||||
if (!ship_menu_selections_str.empty()) {
|
|
||||||
for (const string& s : phosg::split(ship_menu_selections_str, ',')) {
|
|
||||||
ship_menu_selections.emplace(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vector<string> on_request_complete_commands;
|
|
||||||
string on_request_complete_arg = args.get<string>("on-request-complete-command", false);
|
|
||||||
if (!on_request_complete_arg.empty()) {
|
|
||||||
for (const string& command : phosg::split(on_request_complete_arg, ',')) {
|
|
||||||
on_request_complete_commands.emplace_back(phosg::parse_data_string(command));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t serial_number = args.get<uint32_t>(
|
|
||||||
"serial-number",
|
|
||||||
0,
|
|
||||||
is_v1_or_v2(version) ? phosg::Arguments::IntFormat::HEX : phosg::Arguments::IntFormat::DEFAULT);
|
|
||||||
auto io_context = make_shared<asio::io_context>();
|
auto io_context = make_shared<asio::io_context>();
|
||||||
DownloadSession session(
|
unique_ptr<DownloadSession> download_session;
|
||||||
io_context,
|
unique_ptr<PatchDownloadSession> patch_download_session;
|
||||||
remote_host,
|
if (is_patch(version)) {
|
||||||
remote_port,
|
patch_download_session = std::make_unique<PatchDownloadSession>(
|
||||||
args.get<string>("output-dir", true),
|
io_context,
|
||||||
version,
|
remote_host,
|
||||||
args.get<uint8_t>("language"),
|
remote_port,
|
||||||
key,
|
args.get<string>("output-dir", true),
|
||||||
phosg::random_object<uint32_t>(),
|
version,
|
||||||
serial_number,
|
args.get<string>("username", false),
|
||||||
args.get<string>("access-key", false),
|
args.get<string>("password", false),
|
||||||
args.get<string>("username", false),
|
args.get<string>("email", false),
|
||||||
args.get<string>("password", false),
|
args.get<bool>("show-command-data"));
|
||||||
args.get<string>("xb-gamertag", false),
|
asio::co_spawn(*io_context, patch_download_session->run(), asio::detached);
|
||||||
args.get<uint64_t>("xb-user-id", 0, phosg::Arguments::IntFormat::HEX),
|
|
||||||
args.get<uint64_t>("xb-account-id", 0, phosg::Arguments::IntFormat::HEX),
|
} else {
|
||||||
character,
|
auto character = PSOCHARFile::load_shared(args.get<string>("character", true), false).character_file;
|
||||||
ship_menu_selections,
|
auto ship_menu_selections_str = args.get<string>("ship-menu-selections", false);
|
||||||
on_request_complete_commands,
|
|
||||||
args.get<bool>("interactive"),
|
unordered_set<string> ship_menu_selections;
|
||||||
args.get<bool>("show-command-data"));
|
if (!ship_menu_selections_str.empty()) {
|
||||||
|
for (const string& s : phosg::split(ship_menu_selections_str, ',')) {
|
||||||
|
ship_menu_selections.emplace(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<string> on_request_complete_commands;
|
||||||
|
string on_request_complete_arg = args.get<string>("on-request-complete-command", false);
|
||||||
|
if (!on_request_complete_arg.empty()) {
|
||||||
|
for (const string& command : phosg::split(on_request_complete_arg, ',')) {
|
||||||
|
on_request_complete_commands.emplace_back(phosg::parse_data_string(command));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t serial_number = args.get<uint32_t>(
|
||||||
|
"serial-number",
|
||||||
|
0,
|
||||||
|
is_v1_or_v2(version) ? phosg::Arguments::IntFormat::HEX : phosg::Arguments::IntFormat::DEFAULT);
|
||||||
|
|
||||||
|
download_session = std::make_unique<DownloadSession>(
|
||||||
|
io_context,
|
||||||
|
remote_host,
|
||||||
|
remote_port,
|
||||||
|
args.get<string>("output-dir", true),
|
||||||
|
version,
|
||||||
|
static_cast<Language>(args.get<uint8_t>("language")),
|
||||||
|
key,
|
||||||
|
phosg::random_object<uint32_t>(),
|
||||||
|
serial_number,
|
||||||
|
args.get<string>("access-key", false),
|
||||||
|
args.get<string>("username", false),
|
||||||
|
args.get<string>("password", false),
|
||||||
|
args.get<string>("xb-gamertag", false),
|
||||||
|
args.get<uint64_t>("xb-user-id", 0, phosg::Arguments::IntFormat::HEX),
|
||||||
|
args.get<uint64_t>("xb-account-id", 0, phosg::Arguments::IntFormat::HEX),
|
||||||
|
character,
|
||||||
|
ship_menu_selections,
|
||||||
|
on_request_complete_commands,
|
||||||
|
args.get<bool>("interactive"),
|
||||||
|
args.get<bool>("show-command-data"));
|
||||||
|
asio::co_spawn(*io_context, download_session->run(), asio::detached);
|
||||||
|
}
|
||||||
io_context->run();
|
io_context->run();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2089,7 +2114,8 @@ Action a_convert_rare_item_set(
|
|||||||
.rel (Schtserv rare table; cannot be used in output filename)\n\
|
.rel (Schtserv rare table; cannot be used in output filename)\n\
|
||||||
.html (HTML rare table; cannot be used in input filename)\n\
|
.html (HTML rare table; cannot be used in input filename)\n\
|
||||||
If the --multiply=X option is given, multiplies all drop rates by X (given\n\
|
If the --multiply=X option is given, multiplies all drop rates by X (given\n\
|
||||||
as a decimal value).\n",
|
as a decimal value). The HTML drop tables will account for each enemy\'s\n\
|
||||||
|
drop-anything rate; the true drop rates are shown in tooltips.\n",
|
||||||
+[](phosg::Arguments& args) {
|
+[](phosg::Arguments& args) {
|
||||||
double rate_factor = args.get<double>("multiply", 1.0);
|
double rate_factor = args.get<double>("multiply", 1.0);
|
||||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||||
@@ -2098,6 +2124,7 @@ Action a_convert_rare_item_set(
|
|||||||
s->load_text_index();
|
s->load_text_index();
|
||||||
s->load_item_definitions();
|
s->load_item_definitions();
|
||||||
s->load_item_name_indexes();
|
s->load_item_name_indexes();
|
||||||
|
s->load_drop_tables();
|
||||||
|
|
||||||
string input_filename = args.get<string>(1, false);
|
string input_filename = args.get<string>(1, false);
|
||||||
if (input_filename.empty() || (input_filename == "-")) {
|
if (input_filename.empty() || (input_filename == "-")) {
|
||||||
@@ -2144,17 +2171,16 @@ Action a_convert_rare_item_set(
|
|||||||
string data = rs->serialize_afs(is_v1);
|
string data = rs->serialize_afs(is_v1);
|
||||||
write_output_data(args, data.data(), data.size(), nullptr);
|
write_output_data(args, data.data(), data.size(), nullptr);
|
||||||
} else if (output_filename_lower.ends_with(".html")) {
|
} else if (output_filename_lower.ends_with(".html")) {
|
||||||
bool is_v1 = ::is_v1(get_cli_version(args, Version::BB_V4));
|
Version cli_version = get_cli_version(args, Version::BB_V4);
|
||||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
bool is_v1 = ::is_v1(cli_version);
|
||||||
for (GameMode mode : modes) {
|
for (GameMode mode : ALL_GAME_MODES_V4) {
|
||||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
for (Episode episode : ALL_EPISODES_V4) {
|
||||||
for (Episode episode : episodes) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
for (size_t difficulty = 0; difficulty < (is_v1 ? 3 : 4); difficulty++) {
|
if ((is_v1 && (difficulty == Difficulty::ULTIMATE)) || (!rs->has_entries_for_game_config(mode, episode, difficulty))) {
|
||||||
if (!rs->has_entries_for_game_config(mode, episode, difficulty)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
auto item_name_index = s->item_name_index(get_cli_version(args, Version::BB_V4));
|
auto item_name_index = s->item_name_index(cli_version);
|
||||||
string data = rs->serialize_html(mode, episode, difficulty, item_name_index);
|
string data = rs->serialize_html(mode, episode, difficulty, item_name_index, s->common_item_set(cli_version, nullptr));
|
||||||
string out_filename = output_filename.substr(0, output_filename.size() - 5) + "." + name_for_mode(mode) + "." + abbreviation_for_episode(episode) + "." + abbreviation_for_difficulty(difficulty) + output_filename.substr(output_filename.size() - 5);
|
string out_filename = output_filename.substr(0, output_filename.size() - 5) + "." + name_for_mode(mode) + "." + abbreviation_for_episode(episode) + "." + abbreviation_for_difficulty(difficulty) + output_filename.substr(output_filename.size() - 5);
|
||||||
phosg::save_file(out_filename, data);
|
phosg::save_file(out_filename, data);
|
||||||
phosg::log_info_f("... {}", out_filename);
|
phosg::log_info_f("... {}", out_filename);
|
||||||
@@ -2566,7 +2592,7 @@ Action a_generate_ep3_cards_html(
|
|||||||
|
|
||||||
shared_ptr<const TextSet> text_english;
|
shared_ptr<const TextSet> text_english;
|
||||||
try {
|
try {
|
||||||
text_english = s->text_index->get(Version::GC_EP3, 1);
|
text_english = s->text_index->get(Version::GC_EP3, Language::ENGLISH);
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2801,16 +2827,17 @@ Action a_show_ep3_maps(
|
|||||||
s->load_ep3_cards();
|
s->load_ep3_cards();
|
||||||
s->load_ep3_maps();
|
s->load_ep3_maps();
|
||||||
|
|
||||||
const auto& map_ids = s->ep3_map_index->all();
|
const auto& all_maps = s->ep3_map_index->all_maps();
|
||||||
phosg::log_info_f("{} maps", map_ids.size());
|
phosg::log_info_f("{} maps", all_maps.size());
|
||||||
for (const auto& [map_number, map] : map_ids) {
|
for (const auto& [map_number, map] : all_maps) {
|
||||||
const auto& vms = map->all_versions();
|
const auto& vms = map->all_versions();
|
||||||
for (size_t language = 0; language < vms.size(); language++) {
|
for (size_t lang_index = 0; lang_index < vms.size(); lang_index++) {
|
||||||
if (!vms[language]) {
|
if (!vms[lang_index]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
string map_s = vms[language]->map->str(s->ep3_card_index.get(), language);
|
Language language = static_cast<Language>(lang_index);
|
||||||
phosg::fwrite_fmt(stdout, "({}) {}\n", char_for_language_code(language), map_s);
|
string map_s = vms[lang_index]->map->str(s->ep3_card_index.get(), language);
|
||||||
|
phosg::fwrite_fmt(stdout, "({}) {}\n", char_for_language(language), map_s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2863,7 +2890,7 @@ Action a_check_supermaps(
|
|||||||
for (const auto& it : s->supermap_for_free_play_key) {
|
for (const auto& it : s->supermap_for_free_play_key) {
|
||||||
auto episode = static_cast<Episode>((it.first >> 28) & 7);
|
auto episode = static_cast<Episode>((it.first >> 28) & 7);
|
||||||
auto mode = static_cast<GameMode>((it.first >> 26) & 3);
|
auto mode = static_cast<GameMode>((it.first >> 26) & 3);
|
||||||
uint8_t difficulty = (it.first >> 24) & 3;
|
Difficulty difficulty = static_cast<Difficulty>((it.first >> 24) & 3);
|
||||||
uint8_t floor = (it.first >> 16) & 0xFF;
|
uint8_t floor = (it.first >> 16) & 0xFF;
|
||||||
uint8_t layout = (it.first >> 8) & 0xFF;
|
uint8_t layout = (it.first >> 8) & 0xFF;
|
||||||
uint8_t entities = (it.first >> 0) & 0xFF;
|
uint8_t entities = (it.first >> 0) & 0xFF;
|
||||||
@@ -2896,12 +2923,9 @@ Action a_check_supermaps(
|
|||||||
|
|
||||||
// Generate MapStates for a few random variations
|
// Generate MapStates for a few random variations
|
||||||
for (size_t z = 0; z < 0x20; z++) {
|
for (size_t z = 0; z < 0x20; z++) {
|
||||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
Episode episode = ALL_EPISODES_V4[phosg::random_object<uint32_t>() % ALL_EPISODES_V4.size()];
|
||||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
GameMode mode = ALL_GAME_MODES_V4[phosg::random_object<uint32_t>() % ALL_GAME_MODES_V4.size()];
|
||||||
|
Difficulty difficulty = static_cast<Difficulty>(phosg::random_object<uint32_t>() % 4);
|
||||||
Episode episode = episodes[phosg::random_object<uint32_t>() % episodes.size()];
|
|
||||||
GameMode mode = modes[phosg::random_object<uint32_t>() % modes.size()];
|
|
||||||
uint8_t difficulty = phosg::random_object<uint32_t>() % 4;
|
|
||||||
uint8_t event = phosg::random_object<uint32_t>() % 8;
|
uint8_t event = phosg::random_object<uint32_t>() % 8;
|
||||||
uint32_t random_seed = phosg::random_object<uint32_t>();
|
uint32_t random_seed = phosg::random_object<uint32_t>();
|
||||||
phosg::fwrite_fmt(stderr, "FREE MAP STATE TEST: {} {} {}\n",
|
phosg::fwrite_fmt(stderr, "FREE MAP STATE TEST: {} {} {}\n",
|
||||||
@@ -2986,7 +3010,7 @@ Action a_check_supermaps(
|
|||||||
|
|
||||||
auto map_state = make_shared<MapState>(
|
auto map_state = make_shared<MapState>(
|
||||||
0,
|
0,
|
||||||
phosg::random_object<uint8_t>() & 3,
|
static_cast<Difficulty>(phosg::random_object<uint8_t>() & 3),
|
||||||
0,
|
0,
|
||||||
phosg::random_object<uint32_t>(),
|
phosg::random_object<uint32_t>(),
|
||||||
MapState::DEFAULT_RARE_ENEMIES,
|
MapState::DEFAULT_RARE_ENEMIES,
|
||||||
@@ -3004,6 +3028,91 @@ Action a_check_supermaps(
|
|||||||
phosg::fwrite_fmt(stderr, "ALL QUEST MAPS: {}\n", all_quests_eff_str);
|
phosg::fwrite_fmt(stderr, "ALL QUEST MAPS: {}\n", all_quests_eff_str);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Action a_print_free_supermap(
|
||||||
|
"print-free-supermap", "\
|
||||||
|
print-free-supermap [--psov2] [--seed=SEED] [--episode=1|2|4]\n\
|
||||||
|
[--mode=N|B|C|S] [--difficulty=N|H|V|U] [--event=EVENT] VARIATIONS\n\
|
||||||
|
Generates and prints the specified free play supermap.\n",
|
||||||
|
+[](phosg::Arguments& args) {
|
||||||
|
Episode episode;
|
||||||
|
{
|
||||||
|
const string& episode_str = args.get<string>("episode", false);
|
||||||
|
if (episode_str == "1" || episode_str == "") {
|
||||||
|
episode = Episode::EP1;
|
||||||
|
} else if (episode_str == "2") {
|
||||||
|
episode = Episode::EP2;
|
||||||
|
} else if (episode_str == "4") {
|
||||||
|
episode = Episode::EP4;
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("invalid episode number");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GameMode mode;
|
||||||
|
{
|
||||||
|
string mode_str = phosg::tolower(args.get<string>("mode", false));
|
||||||
|
if (mode_str == "n" || mode_str == "") {
|
||||||
|
mode = GameMode::NORMAL;
|
||||||
|
} else if (mode_str == "b") {
|
||||||
|
mode = GameMode::BATTLE;
|
||||||
|
} else if (mode_str == "c") {
|
||||||
|
mode = GameMode::CHALLENGE;
|
||||||
|
} else if (mode_str == "s") {
|
||||||
|
mode = GameMode::SOLO;
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("invalid game mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Difficulty difficulty;
|
||||||
|
{
|
||||||
|
string mode_str = phosg::tolower(args.get<string>("difficulty", false));
|
||||||
|
if (mode_str == "n" || mode_str == "") {
|
||||||
|
difficulty = Difficulty::NORMAL;
|
||||||
|
} else if (mode_str == "h") {
|
||||||
|
difficulty = Difficulty::HARD;
|
||||||
|
} else if (mode_str == "v") {
|
||||||
|
difficulty = Difficulty::VERY_HARD;
|
||||||
|
} else if (mode_str == "u") {
|
||||||
|
difficulty = Difficulty::ULTIMATE;
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("invalid difficulty level");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t event = args.get<uint8_t>("event", 0, phosg::Arguments::IntFormat::HEX);
|
||||||
|
uint32_t random_seed = args.get<uint32_t>("seed", phosg::random_object<uint32_t>(), phosg::Arguments::IntFormat::HEX);
|
||||||
|
|
||||||
|
string variations_str = args.get<string>(1);
|
||||||
|
Variations variations;
|
||||||
|
for (size_t z = 0; z < variations_str.size(); z++) {
|
||||||
|
if (z & 1) {
|
||||||
|
variations.entries[z >> 1].entities = phosg::value_for_hex_char(variations_str[z]);
|
||||||
|
} else {
|
||||||
|
variations.entries[z >> 1].layout = phosg::value_for_hex_char(variations_str[z]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||||
|
s->load_config_early();
|
||||||
|
s->clear_file_caches();
|
||||||
|
s->load_patch_indexes();
|
||||||
|
s->load_set_data_tables();
|
||||||
|
s->load_maps();
|
||||||
|
|
||||||
|
shared_ptr<RandomGenerator> rand_crypt;
|
||||||
|
if (args.get<bool>("--psov2")) {
|
||||||
|
rand_crypt = std::make_shared<MT19937Generator>(random_seed);
|
||||||
|
} else {
|
||||||
|
rand_crypt = std::make_shared<PSOV2Encryption>(random_seed);
|
||||||
|
}
|
||||||
|
auto sdt = s->set_data_table(get_cli_version(args, Version::BB_V4), episode, mode, difficulty);
|
||||||
|
auto supermaps = s->supermaps_for_variations(episode, mode, difficulty, variations);
|
||||||
|
MapState map_state(0, difficulty, event, random_seed, MapState::DEFAULT_RARE_ENEMIES, rand_crypt, supermaps);
|
||||||
|
map_state.verify();
|
||||||
|
map_state.print(stdout);
|
||||||
|
});
|
||||||
|
|
||||||
Action a_check_quests(
|
Action a_check_quests(
|
||||||
"check-quests", nullptr,
|
"check-quests", nullptr,
|
||||||
+[](phosg::Arguments& args) {
|
+[](phosg::Arguments& args) {
|
||||||
@@ -3014,7 +3123,25 @@ Action a_check_quests(
|
|||||||
s->load_patch_indexes();
|
s->load_patch_indexes();
|
||||||
s->load_set_data_tables();
|
s->load_set_data_tables();
|
||||||
s->load_maps();
|
s->load_maps();
|
||||||
s->load_quest_index();
|
s->load_quest_index(true);
|
||||||
|
phosg::fwrite_fmt(stdout, "All quests indexed\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
Action a_check_ep3_maps(
|
||||||
|
"check-ep3-maps", nullptr,
|
||||||
|
+[](phosg::Arguments& args) {
|
||||||
|
config_log.info_f("Collecting Episode 3 data");
|
||||||
|
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||||
|
s->is_debug = true;
|
||||||
|
s->load_ep3_maps(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Action a_check_client_functions(
|
||||||
|
"check-client-functions", nullptr,
|
||||||
|
+[](phosg::Arguments&) {
|
||||||
|
set_all_log_levels(phosg::LogLevel::L_DEBUG);
|
||||||
|
FunctionCodeIndex fci("system/client-functions", true);
|
||||||
|
phosg::fwrite_fmt(stdout, "All client functions compiled\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
Action a_parse_object_graph(
|
Action a_parse_object_graph(
|
||||||
@@ -3213,6 +3340,7 @@ Action a_replay_ep3_battle_commands(
|
|||||||
.rand_crypt = make_shared<MT19937Generator>(seed),
|
.rand_crypt = make_shared<MT19937Generator>(seed),
|
||||||
.tournament = nullptr,
|
.tournament = nullptr,
|
||||||
.trap_card_ids = {},
|
.trap_card_ids = {},
|
||||||
|
.output_queue = nullptr,
|
||||||
};
|
};
|
||||||
if (is_trial) {
|
if (is_trial) {
|
||||||
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
||||||
@@ -3238,36 +3366,131 @@ Action a_replay_ep3_battle_commands(
|
|||||||
|
|
||||||
Action a_replay_ep3_battle_record(
|
Action a_replay_ep3_battle_record(
|
||||||
"replay-ep3-battle-record", nullptr, +[](phosg::Arguments& args) {
|
"replay-ep3-battle-record", nullptr, +[](phosg::Arguments& args) {
|
||||||
auto rec = make_shared<Episode3::BattleRecord>(read_input_data(args));
|
auto record_data = read_input_data(args);
|
||||||
|
if (args.get<bool>("compressed")) {
|
||||||
|
record_data = prs_decompress(record_data);
|
||||||
|
}
|
||||||
|
auto rec = make_shared<Episode3::BattleRecord>(record_data);
|
||||||
|
|
||||||
|
bool use_color = isatty(fileno(stdout));
|
||||||
|
|
||||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||||
s->load_ep3_cards();
|
s->load_ep3_cards();
|
||||||
s->load_ep3_maps();
|
s->load_ep3_maps();
|
||||||
|
|
||||||
bool is_trial = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE);
|
bool is_nte = rec->get_behavior_flags() & Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
||||||
|
auto output_queue = std::make_shared<std::deque<std::string>>();
|
||||||
Episode3::Server::Options options = {
|
Episode3::Server::Options options = {
|
||||||
.card_index = s->ep3_card_index,
|
.card_index = s->ep3_card_index,
|
||||||
.map_index = s->ep3_map_index,
|
.map_index = s->ep3_map_index,
|
||||||
.behavior_flags = (Episode3::BehaviorFlag::IGNORE_CARD_COUNTS |
|
.behavior_flags = rec->get_behavior_flags() & ~(Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING),
|
||||||
Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES |
|
|
||||||
Episode3::BehaviorFlag::DISABLE_MASKING |
|
|
||||||
Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING),
|
|
||||||
.opt_rand_stream = make_shared<phosg::StringReader>(rec->get_random_stream()),
|
.opt_rand_stream = make_shared<phosg::StringReader>(rec->get_random_stream()),
|
||||||
.rand_crypt = make_shared<DisabledRandomGenerator>(),
|
.rand_crypt = make_shared<DisabledRandomGenerator>(),
|
||||||
.tournament = nullptr,
|
.tournament = nullptr,
|
||||||
.trap_card_ids = {},
|
.trap_card_ids = {},
|
||||||
|
.output_queue = output_queue,
|
||||||
};
|
};
|
||||||
if (is_trial) {
|
|
||||||
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
|
||||||
}
|
|
||||||
options.behavior_flags |= Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING;
|
|
||||||
auto server = make_shared<Episode3::Server>(nullptr, std::move(options));
|
auto server = make_shared<Episode3::Server>(nullptr, std::move(options));
|
||||||
server->init();
|
server->init();
|
||||||
for (const auto& command : rec->get_all_server_data_commands()) {
|
|
||||||
phosg::log_info_f("Server data command");
|
// Ignore commands generated by the server when it's constructed (these
|
||||||
phosg::print_data(stderr, command, 0, nullptr, phosg::PrintDataFlags::PRINT_ASCII | phosg::PrintDataFlags::DISABLE_COLOR | phosg::PrintDataFlags::OFFSET_16_BITS);
|
// are not included in the battle record)
|
||||||
server->on_server_data_input(nullptr, command);
|
output_queue->clear();
|
||||||
|
|
||||||
|
std::array<bool, 4> players_present = {false, false, false, false};
|
||||||
|
for (const auto& ev : rec->get_all_events()) {
|
||||||
|
switch (ev.type) {
|
||||||
|
case Episode3::BattleRecord::Event::Type::SET_INITIAL_PLAYERS:
|
||||||
|
ev.print(stdout);
|
||||||
|
for (const auto& player : ev.players) {
|
||||||
|
players_present.at(player.lobby_data.client_id) = true;
|
||||||
|
phosg::fwrite_fmt(stderr, "Player {} is present\n", player.lobby_data.client_id.load());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Episode3::BattleRecord::Event::Type::PLAYER_JOIN:
|
||||||
|
case Episode3::BattleRecord::Event::Type::PLAYER_LEAVE:
|
||||||
|
case Episode3::BattleRecord::Event::Type::CHAT_MESSAGE:
|
||||||
|
case Episode3::BattleRecord::Event::Type::GAME_COMMAND:
|
||||||
|
case Episode3::BattleRecord::Event::Type::EP3_GAME_COMMAND:
|
||||||
|
ev.print(stdout);
|
||||||
|
break;
|
||||||
|
case Episode3::BattleRecord::Event::Type::BATTLE_COMMAND:
|
||||||
|
// Ignore the map command (this is handled separately) and 6xB4x4B
|
||||||
|
// (which is only generated when a lobby is present)
|
||||||
|
if (ev.data.empty() || (static_cast<uint8_t>(ev.data[0]) == 0xB6) || (ev.data.at(4) == 0x4B)) {
|
||||||
|
ev.print(stdout);
|
||||||
|
} else {
|
||||||
|
if (use_color) {
|
||||||
|
phosg::print_color_escape(stdout, phosg::TerminalFormat::FG_RED, phosg::TerminalFormat::BOLD, phosg::TerminalFormat::END);
|
||||||
|
}
|
||||||
|
ev.print(stdout);
|
||||||
|
if (use_color) {
|
||||||
|
phosg::print_color_escape(stdout, phosg::TerminalFormat::NORMAL, phosg::TerminalFormat::END);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
if (output_queue->empty()) {
|
||||||
|
phosg::fwrite_fmt(stderr, "Output queue is empty, but expected battle command:\n");
|
||||||
|
phosg::print_data(stderr, ev.data, 0, nullptr, phosg::PrintDataFlags::OFFSET_16_BITS | phosg::PrintDataFlags::PRINT_ASCII);
|
||||||
|
throw std::runtime_error("Output did not match expectations");
|
||||||
|
}
|
||||||
|
// Hack: don't check the last field in 6xB4x46 since it contains
|
||||||
|
// a timestamp on non-NTE
|
||||||
|
bool matched = false;
|
||||||
|
if ((ev.data.at(4) == 0x46) && !is_nte) {
|
||||||
|
auto received_cmd = check_size_t<G_ServerVersionStrings_Ep3_6xB4x46>(output_queue->front());
|
||||||
|
auto expected_cmd = check_size_t<G_ServerVersionStrings_Ep3_6xB4x46>(ev.data);
|
||||||
|
received_cmd.date_str2.clear(0);
|
||||||
|
expected_cmd.date_str2.clear(0);
|
||||||
|
matched = !memcmp(&received_cmd, &expected_cmd, sizeof(received_cmd));
|
||||||
|
} else {
|
||||||
|
matched = (output_queue->front() == ev.data);
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
const void* prev = (ev.data.size() == output_queue->front().size()) ? ev.data.data() : nullptr;
|
||||||
|
phosg::fwrite_fmt(stderr, "Output queue front did not match expected command; expected:\n");
|
||||||
|
phosg::print_data(stderr, ev.data, 0, nullptr, phosg::PrintDataFlags::OFFSET_16_BITS | phosg::PrintDataFlags::PRINT_ASCII);
|
||||||
|
phosg::fwrite_fmt(stderr, "Received:\n");
|
||||||
|
phosg::print_data(stderr, output_queue->front(), 0, prev, phosg::PrintDataFlags::OFFSET_16_BITS | phosg::PrintDataFlags::PRINT_ASCII);
|
||||||
|
throw std::runtime_error("Output did not match expectations");
|
||||||
|
}
|
||||||
|
output_queue->pop_front();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Episode3::BattleRecord::Event::Type::SERVER_DATA_COMMAND:
|
||||||
|
if (use_color) {
|
||||||
|
phosg::print_color_escape(stdout, phosg::TerminalFormat::FG_GREEN, phosg::TerminalFormat::BOLD, phosg::TerminalFormat::END);
|
||||||
|
}
|
||||||
|
ev.print(stdout);
|
||||||
|
if (use_color) {
|
||||||
|
phosg::print_color_escape(stdout, phosg::TerminalFormat::NORMAL, phosg::TerminalFormat::END);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
if (!output_queue->empty()) {
|
||||||
|
phosg::fwrite_fmt(stderr, "Received extra output after preceding SERVER_DATA event:\n");
|
||||||
|
phosg::print_data(stderr, output_queue->front());
|
||||||
|
throw std::runtime_error("Output did not match expectations");
|
||||||
|
}
|
||||||
|
// Hack: Set the CPU player flag if the player isn't present in the
|
||||||
|
// recording (normally this is done by checking the Lobby, but
|
||||||
|
// there's no Lobby during a replay)
|
||||||
|
if (ev.data.at(4) == 0x1B) {
|
||||||
|
string mutable_data = ev.data;
|
||||||
|
auto& cmd = check_size_t<G_SetPlayerName_Ep3_CAx1B>(mutable_data);
|
||||||
|
cmd.entry.is_cpu_player = !players_present.at(cmd.entry.client_id);
|
||||||
|
phosg::fwrite_fmt(stderr, "Overriding is_cpu_player with {}\n", cmd.entry.is_cpu_player ? "true" : "false");
|
||||||
|
server->on_server_data_input(nullptr, mutable_data);
|
||||||
|
} else {
|
||||||
|
server->on_server_data_input(nullptr, ev.data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw std::runtime_error("unknown event type: {}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!output_queue->empty()) {
|
||||||
|
phosg::fwrite_fmt(stderr, "Received extra output after recording completed:\n");
|
||||||
|
phosg::print_data(stderr, output_queue->front());
|
||||||
|
throw std::runtime_error("Output did not match expectations");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+304
-150
@@ -618,7 +618,8 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// param4 = source type:
|
// param4 = source type:
|
||||||
// 0 = use this set when advancing from a lower floor
|
// 0 = use this set when advancing from a lower floor
|
||||||
// 1 = use this set when returning from a higher floor
|
// 1 = use this set when returning from a higher floor
|
||||||
// anything else = set is unused
|
// anything else = set is unused (TODO: but maybe used by
|
||||||
|
// TObjAreaWarpQuest?)
|
||||||
{0x0000, F_V0_V4, 0x00007FFFFFFFFFFF, "TObjPlayerSet"},
|
{0x0000, F_V0_V4, 0x00007FFFFFFFFFFF, "TObjPlayerSet"},
|
||||||
{0x0000, F_EP3, 0x0000000000008001, "TObjPlayerSet"},
|
{0x0000, F_EP3, 0x0000000000008001, "TObjPlayerSet"},
|
||||||
|
|
||||||
@@ -938,7 +939,7 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
|
|
||||||
// Radar collision. Params:
|
// Radar collision. Params:
|
||||||
// param1 = radius
|
// param1 = radius
|
||||||
// param4 = TODO
|
// param4 = minimap segment ID
|
||||||
{0x0017, F_V0_V4, 0x00004FFFFFF8FFFE, "TObjRaderCol"},
|
{0x0017, F_V0_V4, 0x00004FFFFFF8FFFE, "TObjRaderCol"},
|
||||||
{0x0017, F_EP3, 0x0000000000008000, "TObjRaderCol"},
|
{0x0017, F_EP3, 0x0000000000008000, "TObjRaderCol"},
|
||||||
|
|
||||||
@@ -990,7 +991,7 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// that the object is not destroyed immediately if it's blue and the game
|
// that the object is not destroyed immediately if it's blue and the game
|
||||||
// is Challenge mode. Params:
|
// is Challenge mode. Params:
|
||||||
// param1 = player set ID (TODO: what exactly does this do? Looks like it
|
// param1 = player set ID (TODO: what exactly does this do? Looks like it
|
||||||
// does nothing unless it's >= 2)
|
// does nothing unless it's >= 2; see TObjPlayerSet)
|
||||||
// param4 = destination floor
|
// param4 = destination floor
|
||||||
// param6 = color (0 = blue, 1 = red)
|
// param6 = color (0 = blue, 1 = red)
|
||||||
{0x001B, F_V1_V4, 0x00005000000078FE, "TObjAreaWarpQuest"},
|
{0x001B, F_V1_V4, 0x00005000000078FE, "TObjAreaWarpQuest"},
|
||||||
@@ -1005,10 +1006,10 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// Params:
|
// Params:
|
||||||
// param1 = TODO
|
// param1 = TODO
|
||||||
// param2 = TODO
|
// param2 = TODO
|
||||||
// param3 = TODO
|
// param3 = TODO (1 byte only)
|
||||||
// param4 = TODO
|
// param4 = TODO (1 byte only)
|
||||||
// param5 = TODO
|
// param5 = TODO (1 byte only)
|
||||||
// param6 = TODO
|
// param6 = TODO (1 byte only)
|
||||||
{0x001D, F_V2_V4, 0x0000400000000002, "TEffStarLight2D_Base"},
|
{0x001D, F_V2_V4, 0x0000400000000002, "TEffStarLight2D_Base"},
|
||||||
|
|
||||||
// VR Temple Beta / Barba Ray lens flare effect.
|
// VR Temple Beta / Barba Ray lens flare effect.
|
||||||
@@ -1017,7 +1018,6 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
|
|
||||||
// Hides the area map when the player is near this object. Params:
|
// Hides the area map when the player is near this object. Params:
|
||||||
// param1 = radius
|
// param1 = radius
|
||||||
// TODO: Test this.
|
|
||||||
{0x001F, F_V2_V4, 0x00004FFC3FFB07FE, "TObjRaderHideCol"},
|
{0x001F, F_V2_V4, 0x00004FFC3FFB07FE, "TObjRaderHideCol"},
|
||||||
|
|
||||||
// Item-detecting floor switch. Params:
|
// Item-detecting floor switch. Params:
|
||||||
@@ -1114,7 +1114,30 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
{0x0023, F_V2_V4, 0x00004FFC3FFB07FE, "TOAttackableCol"},
|
{0x0023, F_V2_V4, 0x00004FFC3FFB07FE, "TOAttackableCol"},
|
||||||
|
|
||||||
// Damage effect. Params:
|
// Damage effect. Params:
|
||||||
// angle.x = effect type (in range [0x00, 0x14]; TODO: list these)
|
// angle.x = effect type (in range [0x00, 0x14]; the following were
|
||||||
|
// tested in Forest 1 and may have different effects in different
|
||||||
|
// areas):
|
||||||
|
// 00 = fiery explosion with vertical camera shake
|
||||||
|
// 01 = vertical camera shake only
|
||||||
|
// 02 = bluish energy ball
|
||||||
|
// 03 = expanding white flash
|
||||||
|
// 04 = contracting white flash
|
||||||
|
// 05 = nothing
|
||||||
|
// 06 = rising fireball
|
||||||
|
// 07 = blue flash
|
||||||
|
// 08 = purple flash
|
||||||
|
// 09 = teal flash
|
||||||
|
// 0A = nothing
|
||||||
|
// 0B = big orange flash
|
||||||
|
// 0C = nothing
|
||||||
|
// 0D = big white flash
|
||||||
|
// 0E = black smoky flash
|
||||||
|
// 0F = nothing
|
||||||
|
// 10 = nothing
|
||||||
|
// 11 = smaller black smoke
|
||||||
|
// 12 = bluish-purple flash
|
||||||
|
// 13 = quick blue flash
|
||||||
|
// 14 = lavender-gray flash
|
||||||
// param1 = damage radius
|
// param1 = damage radius
|
||||||
// param2 = damage value, scaled by difficulty:
|
// param2 = damage value, scaled by difficulty:
|
||||||
// Normal: param2 * 2
|
// Normal: param2 * 2
|
||||||
@@ -1132,23 +1155,23 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
|
|
||||||
// Switch flag timer. This object watches for a switch flag to be
|
// Switch flag timer. This object watches for a switch flag to be
|
||||||
// activated, then once it is, waits for a specified time, then disables
|
// activated, then once it is, waits for a specified time, then disables
|
||||||
// that switch flag and activates up to three other switch flags. Note
|
// that switch flag or activates up to three other switch flags. Note that
|
||||||
// that this object just provides the timer functionality; the trigger
|
// this object just provides the timer functionality; the trigger switch
|
||||||
// switch flag must be activated by some other object. Params:
|
// flag must be activated by some other object. Params:
|
||||||
// angle.x = trigger mode:
|
// angle.x = trigger mode:
|
||||||
// 0 = disable watched switch flag when timer expires
|
// 0 = disable watched switch flag when timer expires
|
||||||
// any other value = enable up to 3 other switch flags when timer
|
// any positive number = enable up to 3 other switch flags when timer
|
||||||
// expires and don't disable watched switch flag
|
// expires and don't disable watched switch flag
|
||||||
// angle.y = if this is 1, play tick sound effect every second after
|
// angle.y = if this is 1, play tick sound effect every second after
|
||||||
// activation (if any other value, no tick sound is played)
|
// activation (if any other value, no tick sound is played)
|
||||||
// angle.z = timer duration in frames
|
// angle.z = timer duration in frames
|
||||||
// param4 = switch flag to watch for activation in low 16 bits, switch
|
// param4 = switch flag to watch for activation in low 16 bits, switch
|
||||||
// flag 1 to activate when timer expires (if angle.x = 0) in high 16
|
// flag 1 to activate when timer expires (if angle.x != 0) in high 16
|
||||||
// bits (>= 0x100 if not needed)
|
// bits (>= 0x100 if not needed)
|
||||||
// param5 = switch flag 2 to activate when timer expires (if
|
// param5 = switch flag 2 to activate when timer expires (if
|
||||||
// angle.x = 0) in high 16 bits (>= 0x100 if not needed)
|
// angle.x != 0) in high 16 bits (>= 0x100 if not needed)
|
||||||
// param6 = switch flag 3 to activate when timer expires (if
|
// param6 = switch flag 3 to activate when timer expires (if
|
||||||
// angle.x = 0) in high 16 bits (>= 0x100 if not needed)
|
// angle.x != 0) in high 16 bits (>= 0x100 if not needed)
|
||||||
{0x0025, F_V2_V4, 0x00006FFC3FFF07FF, "TOSwitchTimer"},
|
{0x0025, F_V2_V4, 0x00006FFC3FFF07FF, "TOSwitchTimer"},
|
||||||
|
|
||||||
// Chat sensor. This object watches for chat messages said by players
|
// Chat sensor. This object watches for chat messages said by players
|
||||||
@@ -1341,18 +1364,16 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// be no parameters.
|
// be no parameters.
|
||||||
{0x0054, F_V0_V4, 0x0000600000000001, "TObjCityDoor_Lobby"},
|
{0x0054, F_V0_V4, 0x0000600000000001, "TObjCityDoor_Lobby"},
|
||||||
|
|
||||||
// Version of the main warp for Challenge mode? This object seems to
|
// Version of the main warp for Challenge mode. This object looks like the
|
||||||
// behave similarly to boss teleporters; it shows the player a Yes/No
|
// main Ragol warp on Pioneer 2, but behaves similarly to boss teleporters:
|
||||||
// confirmation menu and sends 6x6A to synchronize state. There is a
|
// it shows the player a Yes/No confirmation menu and sends 6x6A to
|
||||||
// global named last_set_mainwarp_value which is set to param4 when this
|
// synchronize state. There is a global named last_set_mainwarp_value which
|
||||||
// object is constructed, but may be changed by a set_mainwarp quest
|
// is set to param4 when this object is constructed, but may be changed by
|
||||||
// opcode after that. If that happens, this object replaces its
|
// a set_mainwarp quest opcode after that. If that happens, this object
|
||||||
// dest_floor with the floor specified in the last set_mainwarp quest
|
// replaces its dest_floor with the floor specified in the last
|
||||||
// opcode. Params:
|
// set_mainwarp quest opcode. Params:
|
||||||
// param4 = destination floor
|
// param4 = destination floor
|
||||||
// param5 = switch flag number
|
// param5 = switch flag number
|
||||||
// TODO: This thing has a lot of code; figure out if there are any other
|
|
||||||
// parameters
|
|
||||||
{0x0055, F_V2_V4, 0x0000600000040001, "TObjCityMainWarpChallenge"},
|
{0x0055, F_V2_V4, 0x0000600000040001, "TObjCityMainWarpChallenge"},
|
||||||
|
|
||||||
// Episode 2 Lab door. Params:
|
// Episode 2 Lab door. Params:
|
||||||
@@ -1378,15 +1399,20 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
{0x0057, F_V3_V4, 0x0000600000040001, "TObjTradeCollision"},
|
{0x0057, F_V3_V4, 0x0000600000040001, "TObjTradeCollision"},
|
||||||
{0x0057, F_EP3, 0x0000000000000001, "TObjTradeCollision"},
|
{0x0057, F_EP3, 0x0000000000000001, "TObjTradeCollision"},
|
||||||
|
|
||||||
// TODO: Describe this object. Presumably similar to TObjTradeCollision
|
// This object appears to be unused in both Ep3 NTE and the final release.
|
||||||
// but enables the deck edit counter? Params:
|
// It may have been an early version of Deck Edit or Entry Counter
|
||||||
|
// sequence, perhaps responsible for setting the players' statuses when
|
||||||
|
// they're near either of those counters. Params:
|
||||||
// param1 = radius
|
// param1 = radius
|
||||||
{0x0058, F_EP3, 0x0000000000000001, "TObjDeckCollision"},
|
{0x0058, F_EP3, 0x0000000000000001, "TObjDeckCollision"},
|
||||||
|
|
||||||
// Forest door. Params:
|
// Forest door. Params:
|
||||||
|
// param1-3 = x, y, z coordinates for unlock "cutscene" (see param6)
|
||||||
// param4 = switch flag number (low byte) and number to appear on door
|
// param4 = switch flag number (low byte) and number to appear on door
|
||||||
// (second-lowest byte, modulo 0x0A)
|
// (second-lowest byte, modulo 10)
|
||||||
// param6 = TODO (expected to be 0 or 1)
|
// param6 = if set to 1, enables unlock "cutscene" - when the door is
|
||||||
|
// unlocked, the camera will snap to the coordinates in param1-3,
|
||||||
|
// pointing toward the door, for 2 seconds
|
||||||
{0x0080, F_V0_V4, 0x0000400000000006, "TObjDoor"},
|
{0x0080, F_V0_V4, 0x0000400000000006, "TObjDoor"},
|
||||||
|
|
||||||
// Forest switch. Params:
|
// Forest switch. Params:
|
||||||
@@ -1395,9 +1421,13 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
{0x0081, F_V0_V4, 0x00004FF00078003E, "TObjDoorKey"},
|
{0x0081, F_V0_V4, 0x00004FF00078003E, "TObjDoorKey"},
|
||||||
|
|
||||||
// Laser fence and square laser fence. Params:
|
// Laser fence and square laser fence. Params:
|
||||||
// param1 = color (range [0, 3])
|
// param1 = color:
|
||||||
|
// <=0 = red
|
||||||
|
// 1 = cyan
|
||||||
|
// 2 = green
|
||||||
|
// >=3 = magenta
|
||||||
// param4 = switch flag number
|
// param4 = switch flag number
|
||||||
// param6 = model (TODO)
|
// param6 = model (even value = short, odd value = long)
|
||||||
{0x0082, F_V0_V4, 0x00004FF0000300FE, "TObjLazerFenceNorm"},
|
{0x0082, F_V0_V4, 0x00004FF0000300FE, "TObjLazerFenceNorm"},
|
||||||
{0x0083, F_V0_V4, 0x00004FF03FFB00FE, "TObjLazerFence4"},
|
{0x0083, F_V0_V4, 0x00004FF03FFB00FE, "TObjLazerFence4"},
|
||||||
|
|
||||||
@@ -1416,19 +1446,50 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// param1-3 = TODO
|
// param1-3 = TODO
|
||||||
{0x0086, F_V0_V4, 0x00004E0000000006, "TButterfly"},
|
{0x0086, F_V0_V4, 0x00004E0000000006, "TButterfly"},
|
||||||
|
|
||||||
// TODO: Describe this object. Params:
|
// Small vehicle. Params:
|
||||||
// param1 = model number
|
// param1 = model number (even value = crashed, odd value = intact)
|
||||||
{0x0087, F_V0_V4, 0x0000400000000006, "TMotorcycle"},
|
{0x0087, F_V0_V4, 0x0000400000000006, "TMotorcycle"},
|
||||||
|
|
||||||
// Item box. Params:
|
// Item box. Params:
|
||||||
// param1 = if positive, box is specialized to drop a specific item or
|
// param1 = if positive, box is specialized to drop a specific item or
|
||||||
// type of item; if zero or negative, box drops any common item or
|
// type of item; if zero or negative, box drops any common item or
|
||||||
// none at all (and param3-6 are all ignored)
|
// none at all (and param3-6 are all ignored)
|
||||||
// param3 = if zero, then bonuses, grinds, etc. are applied to the item
|
// param3 = if zero, then only data1[0-1] are used and the rest of the
|
||||||
// after it's generated; if nonzero, the item is not randomized at
|
// ItemData is cleared, then bonuses, grinds, etc. are applied to the
|
||||||
// all and drops exactly as specified in param4-6
|
// item; if nonzero, the item is not randomized at all and drops
|
||||||
// param4-6 = item definition (see base_item_for_specialized_box in
|
// exactly as specified in param4-6
|
||||||
// ItemCreator.cc for how these values are decoded)
|
// param4-6 = item definition (see below)
|
||||||
|
// Not all fields in ItemData can be specified in the item definition here.
|
||||||
|
// The field order here does not match the field order in ItemData! The
|
||||||
|
// item definition is encoded here as follows:
|
||||||
|
// -param4- -param5- -param6-
|
||||||
|
// Weapon: 00wwZZSS GG--PPQQ PPQQPPQQ
|
||||||
|
// Armor: 0100ZZTT 00VV---- --------
|
||||||
|
// Shield: 0101ZZTT 00VV---- --------
|
||||||
|
// Unit: 0102ZZ00 00VV---- --------
|
||||||
|
// Mag: 02zz---- -------- --------
|
||||||
|
// Tool: 03zzZZ-- -------- --------
|
||||||
|
// Tech disk: 0302&&%% -------- --------
|
||||||
|
// Meseta: 040000-- $$$$---- --------
|
||||||
|
// - = ignored
|
||||||
|
// G = weapon grind
|
||||||
|
// P = attribute type (for S-ranks, custom name; last pair is kill count
|
||||||
|
// for some weapons)
|
||||||
|
// Q = attribute amount (for S-ranks, custom name; last pair is kill
|
||||||
|
// count for some weapons)
|
||||||
|
// S = weapon flags (80=untekked, 40=present) and special (low 6 bits)
|
||||||
|
// T = slot count
|
||||||
|
// U = tool flags (40=present; unused if item is stackable)
|
||||||
|
// V = armor/shield/unit flags (40=present; low 4 bits are present color)
|
||||||
|
// w = weapon class - 1 (second byte of item code, offset by 1, so e.g.
|
||||||
|
// Mechgun is 07 here, not 08)
|
||||||
|
// z = item class (second byte of item code)
|
||||||
|
// Z = item subclass (third byte of item code)
|
||||||
|
// & = technique level
|
||||||
|
// % = technique number
|
||||||
|
// $ = meseta amount, divided by 10 (so max possible amount is 655350)
|
||||||
|
// See base_item_for_specialized_box in ItemCreator.cc for newserv's
|
||||||
|
// implementation of decoding this format.
|
||||||
// In the non-specialized case (param1 <= 0), param3-6 are still sent via
|
// In the non-specialized case (param1 <= 0), param3-6 are still sent via
|
||||||
// the 6xA2 command when the box is opened on v3 and later, and the
|
// the 6xA2 command when the box is opened on v3 and later, and the
|
||||||
// server may choose to use those parameters for some purpose. The client
|
// server may choose to use those parameters for some purpose. The client
|
||||||
@@ -1436,8 +1497,9 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
{0x0088, F_V0_V4, 0x00004FF0B00000FE, "TObjContainerBase2"},
|
{0x0088, F_V0_V4, 0x00004FF0B00000FE, "TObjContainerBase2"},
|
||||||
{0x0088, F_EP3, 0x0000000000000002, "TObjContainerBase2"},
|
{0x0088, F_EP3, 0x0000000000000002, "TObjContainerBase2"},
|
||||||
|
|
||||||
// Elevated cylindrical tank. Params:
|
// Elevated cylindrical tank. There appear to be no parameters; param1-3
|
||||||
// param1-3 = TODO
|
// are copied into the object instance as a Vector3F, but it appears that
|
||||||
|
// that vector is never used.
|
||||||
{0x0089, F_V0_V4, 0x0000400000000006, "TObjTank"},
|
{0x0089, F_V0_V4, 0x0000400000000006, "TObjTank"},
|
||||||
|
|
||||||
// TODO: Describe this object. Params:
|
// TODO: Describe this object. Params:
|
||||||
@@ -1460,7 +1522,7 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// param4 = base switch flag (the actual switch flags used are param4,
|
// param4 = base switch flag (the actual switch flags used are param4,
|
||||||
// param4 + 1, param4 + 2, etc.)
|
// param4 + 1, param4 + 2, etc.)
|
||||||
// param5 = number of switch flags (clamped to [1, 4])
|
// param5 = number of switch flags (clamped to [1, 4])
|
||||||
// param6 = TODO (only matters if this is zero or nonzero)
|
// param6 = movement effect (0 = sparks + thud, 1 = grinding sound)
|
||||||
{0x008C, F_V0_V1, 0x0000000000000006, "TObjContainerIdo"},
|
{0x008C, F_V0_V1, 0x0000000000000006, "TObjContainerIdo"},
|
||||||
{0x008C, F_V2_V4, 0x000040000000000E, "TObjContainerIdo"},
|
{0x008C, F_V2_V4, 0x000040000000000E, "TObjContainerIdo"},
|
||||||
|
|
||||||
@@ -1470,8 +1532,9 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// negative = enabled when player is within 70 units
|
// negative = enabled when player is within 70 units
|
||||||
// range [0x00, 0xFF] = enabled by corresponding switch flag
|
// range [0x00, 0xFF] = enabled by corresponding switch flag
|
||||||
// 0x100 and above = never enabled
|
// 0x100 and above = never enabled
|
||||||
// param5 = message number (used with message quest opcode; TODO: has
|
// param5 = message number ("character ID" in qedit; if this is outside
|
||||||
// the same [100, 999] check as some other objects)
|
// the range [100, 999], the quest label in param6 is called in the
|
||||||
|
// free play script instead of the quest script)
|
||||||
// param6 = quest label to call when activated
|
// param6 = quest label to call when activated
|
||||||
{0x008D, F_V0_V4, 0x00004000000027FE, "TOCapsuleAncient01"},
|
{0x008D, F_V0_V4, 0x00004000000027FE, "TOCapsuleAncient01"},
|
||||||
|
|
||||||
@@ -1489,7 +1552,7 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
{0x008F, F_V0_V4, 0x0000400000000006, "TObjHashi"},
|
{0x008F, F_V0_V4, 0x0000400000000006, "TObjHashi"},
|
||||||
|
|
||||||
// Generic switch. Visually, this is the type usually used for objects
|
// Generic switch. Visually, this is the type usually used for objects
|
||||||
// other than doors, such as lights, poison rooms, and the Forest 1
|
// other than doors, such as lights, poison rooms, and the Forest 2
|
||||||
// bridge. Params:
|
// bridge. Params:
|
||||||
// param1 = activation mode:
|
// param1 = activation mode:
|
||||||
// negative = temporary (TODO: test this)
|
// negative = temporary (TODO: test this)
|
||||||
@@ -1553,9 +1616,13 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// can be chosen via param6. If param6 is not 0, 1, or 2, no object is
|
// can be chosen via param6. If param6 is not 0, 1, or 2, no object is
|
||||||
// created.
|
// created.
|
||||||
// Params for TOHangceilingCave01Normal (param6 = 0):
|
// Params for TOHangceilingCave01Normal (param6 = 0):
|
||||||
// param1 = TODO (radius delta? value is param1 + 29)
|
// param1 = 1/4 time between crushes, in frames (value is param1 + 29, so
|
||||||
// param2 = TODO (value is 1 - param2)
|
// total time between crushes is (param1 + 29) * 4 frames)
|
||||||
// param3 = TODO (value is param3 + 100)
|
// param2 = wait time in frames after construction until first crush
|
||||||
|
// (value is 1 - param2, so a more negative value means a longer wait
|
||||||
|
// time)
|
||||||
|
// param3 = base damage (value is param3 + 100; scaled by difficulty:
|
||||||
|
// Normal = 1x, Hard = 2x, Very Hard = 3x, Ultimate = 6x)
|
||||||
// Params for TOHangceilingCave01Key (param6 = 1):
|
// Params for TOHangceilingCave01Key (param6 = 1):
|
||||||
// param1-3 = same as for TOHangceilingCave01Normal
|
// param1-3 = same as for TOHangceilingCave01Normal
|
||||||
// param4 = switch flag number (drops when switch flag is activated;
|
// param4 = switch flag number (drops when switch flag is activated;
|
||||||
@@ -1674,11 +1741,11 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
{0x0101, F_V0_V1, 0x00000000000000C0, "TOKeyMachine01"},
|
{0x0101, F_V0_V1, 0x00000000000000C0, "TOKeyMachine01"},
|
||||||
{0x0101, F_V2_V4, 0x00004FF0007B00C6, "TOKeyMachine01"},
|
{0x0101, F_V2_V4, 0x00004FF0007B00C6, "TOKeyMachine01"},
|
||||||
|
|
||||||
// Mines single-switch door, or Ep4 test door if in Episode 4. Params (for
|
// Mines single-switch door, or Subterranean Desert door if in Episode 4.
|
||||||
// both object types):
|
// Params (for both object types):
|
||||||
// param4 = switch flag number
|
// param4 = switch flag number
|
||||||
{0x0102, F_V0_V4, 0x00000000000000C0, "TODoorMachine02"},
|
{0x0102, F_V0_V4, 0x00000000000000C0, "TODoorMachine02"},
|
||||||
{0x0102, F_V4, 0x00004E0000000000, "__EP4_TEST_DOOR__"},
|
{0x0102, F_V4, 0x00004E0000000000, "__EP4_DOOR__"},
|
||||||
|
|
||||||
// Large cryotube. There appear to be no parameters.
|
// Large cryotube. There appear to be no parameters.
|
||||||
{0x0103, F_V0_V4, 0x00004008000000C0, "TOCapsuleMachine01"},
|
{0x0103, F_V0_V4, 0x00004008000000C0, "TOCapsuleMachine01"},
|
||||||
@@ -1695,10 +1762,13 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// appears that some may have different scale factors or offsets (TODO).
|
// appears that some may have different scale factors or offsets (TODO).
|
||||||
{0x0106, F_V0_V4, 0x00004000000000C0, "TODragonflyMachine01"},
|
{0x0106, F_V0_V4, 0x00004000000000C0, "TODragonflyMachine01"},
|
||||||
|
|
||||||
// Floating blue light. Params:
|
// Floating rotating blue light (often appears next to doors). The light
|
||||||
// param4 = TODO
|
// rotates about its vertical axis and floats up and down sinusoidally.
|
||||||
// param5 = TODO
|
// Params:
|
||||||
// param6 = TODO
|
// param4 = float cycles per second (value is param4 * 0.1 + 1.0)
|
||||||
|
// param5 = max float distance (value is param5 * 0.1 + 0.5)
|
||||||
|
// param6 = rotation speed in angle units per frame (0x10000 = 1 complete
|
||||||
|
// rotation)
|
||||||
{0x0107, F_V0_V4, 0x00004000000000C0, "TOLightMachine01"},
|
{0x0107, F_V0_V4, 0x00004000000000C0, "TOLightMachine01"},
|
||||||
|
|
||||||
// Self-destructing objects. Params:
|
// Self-destructing objects. Params:
|
||||||
@@ -1707,13 +1777,24 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
{0x0109, F_V0_V4, 0x00004000000000C0, "TOExplosiveMachine02"},
|
{0x0109, F_V0_V4, 0x00004000000000C0, "TOExplosiveMachine02"},
|
||||||
{0x010A, F_V0_V4, 0x00004000000000C0, "TOExplosiveMachine03"},
|
{0x010A, F_V0_V4, 0x00004000000000C0, "TOExplosiveMachine03"},
|
||||||
|
|
||||||
// Spark machine. Params:
|
// Spark machine. This looks like it's intended to appear in the bridge
|
||||||
// param1 = TODO (value is param1 - 0.98)
|
// room in Mine 1, to create an effect of the columnar machines sparking.
|
||||||
// param2 = TODO (seems it only matters if this is <= 0 or not)
|
// This is implemented as four columns that randomly change their
|
||||||
|
// visibility, intended to be in the same position as the map geometry.
|
||||||
|
// Each column has an accumulator value, which is initially zero. Every
|
||||||
|
// frame, the value (param1 - 0.98) is added to each column's accumulator
|
||||||
|
// and a random number between 0 and 1 is chosen; if the random number is
|
||||||
|
// less than the column's accumulator, its visibility state is changed and
|
||||||
|
// its accumulator is reset to zero. In this manner, param1 can be thought
|
||||||
|
// of as the frequency of state changes - 0.98 would mean they never change
|
||||||
|
// state, 1.98 would mean they change every frame. Params:
|
||||||
|
// param1 = state change accumulation per frame (value is param1 - 0.98)
|
||||||
|
// param2 = if <= 0, only one column flickers and the others are always
|
||||||
|
// visible; if > 0, all columns flicker
|
||||||
{0x010B, F_V0_V4, 0x00004000000000C0, "TOSparkMachine01"},
|
{0x010B, F_V0_V4, 0x00004000000000C0, "TOSparkMachine01"},
|
||||||
|
|
||||||
// Large flashing box. Params:
|
// Open stall with a spinning red light on either side. Params:
|
||||||
// param2 = TODO (seems it only matters if this is < 0 or not)
|
// param2 = if > 0, a gray box is present in the left half of the stall
|
||||||
{0x010C, F_V0_V4, 0x00004000000000C0, "TOHangerMachine01"},
|
{0x010C, F_V0_V4, 0x00004000000000C0, "TOHangerMachine01"},
|
||||||
|
|
||||||
// Ruins entrance door (after Vol Opt). This object reads quest flags
|
// Ruins entrance door (after Vol Opt). This object reads quest flags
|
||||||
@@ -1731,9 +1812,12 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// >= 0 in Challenge mode, the warp is destroyed immediately
|
// >= 0 in Challenge mode, the warp is destroyed immediately
|
||||||
{0x0140, F_V0_V4, 0x0000400000000700, "TObjGoalWarpAncient"},
|
{0x0140, F_V0_V4, 0x0000400000000700, "TObjGoalWarpAncient"},
|
||||||
|
|
||||||
// Ruins intra-area warp. Same parameters as 0x0003 (TObjMapWarpForest),
|
// Ruins intra-area warp. Params:
|
||||||
// but also:
|
// param1-3 = destination (same as for TObjMapWarpForest)
|
||||||
// param5 = type (negative = one-way, zero or positive = normal)
|
// param4 = destination angle (same as for TObjMapWarpForest)
|
||||||
|
// param5 = if negative, no warp lines render (only the floor pad
|
||||||
|
// appears) and the player cannot use the warp; if zero or positive,
|
||||||
|
// the warp functions normally
|
||||||
{0x0141, F_V0_V4, 0x0000400000000700, "TObjMapWarpAncient"},
|
{0x0141, F_V0_V4, 0x0000400000000700, "TObjMapWarpAncient"},
|
||||||
|
|
||||||
// Ruins switch. Same parameters as 0x00C0 (TOKeyCave01).
|
// Ruins switch. Same parameters as 0x00C0 (TOKeyCave01).
|
||||||
@@ -1767,7 +1851,9 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// param1 = activation radius delta (actual radius is param1 + 50)
|
// param1 = activation radius delta (actual radius is param1 + 50)
|
||||||
// param4 = switch flag number
|
// param4 = switch flag number
|
||||||
// param5 = if negative, sensor is always on
|
// param5 = if negative, sensor is always on
|
||||||
// param6 = TODO (clamped to [0, 1] - model index?)
|
// param6 = texture index; uses fs_obj_o_sensor01r if <= 0, uses
|
||||||
|
// fs_obj_o_sensor02r if positive; the two texture files are identical
|
||||||
|
// (at least on GC) so this has no user-visible effects
|
||||||
{0x014C, F_V0_V4, 0x0000400000000700, "TOSensorAncient01"},
|
{0x014C, F_V0_V4, 0x0000400000000700, "TOSensorAncient01"},
|
||||||
|
|
||||||
// Ruins laser fence switch. Params:
|
// Ruins laser fence switch. Params:
|
||||||
@@ -1787,11 +1873,19 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
|
|
||||||
// Ruins poison-spewing blob. This object is technically an item box, and
|
// Ruins poison-spewing blob. This object is technically an item box, and
|
||||||
// drops an item when destroyed. Unlike most other item boxes, it cannot
|
// drops an item when destroyed. Unlike most other item boxes, it cannot
|
||||||
// be specialized (ignore_def is always true). Params:
|
// be specialized (ignore_def is always true). It cycles through the
|
||||||
// param1 = TODO (value is param1 + 299)
|
// following 3 phases in order until it's destroyed:
|
||||||
// param2 = TODO (value is param2 + 209)
|
// Phase 0: idle (duration depends on param1 and players' positions)
|
||||||
// param3 = TODO (value is param3 + 399)
|
// Phase 1: preparing to spew poison (always lasts 60 frames)
|
||||||
// param6 = TODO (value is param6 + 4)
|
// Phase 2: spewing poison (duration specified by param2)
|
||||||
|
// Params:
|
||||||
|
// param1 = maximum phase 0 duration in frames (value is param1 + 299;
|
||||||
|
// it advances to phase 1 early if a player is within 80 units of it)
|
||||||
|
// param2 = duration of phase 2 in frames (value is param2 + 209)
|
||||||
|
// param3 = poison radius squared (value is param3 + 399, so if param3 =
|
||||||
|
// 1 for example, the poison radius is 20 units)
|
||||||
|
// param6 = how often to create more particles during spewing phase (in
|
||||||
|
// frames; value is param6 + 4)
|
||||||
{0x0152, F_V0_V4, 0x00004E000F800700, "TContainerAncient01"},
|
{0x0152, F_V0_V4, 0x00004E000F800700, "TContainerAncient01"},
|
||||||
|
|
||||||
// Ruins falling trap. Trap power seems to be scaled by difficulty
|
// Ruins falling trap. Trap power seems to be scaled by difficulty
|
||||||
@@ -1809,7 +1903,8 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// Ruins pop-up trap. Params:
|
// Ruins pop-up trap. Params:
|
||||||
// param1 = trigger radius delta (value is (param1 / 2) + 30)
|
// param1 = trigger radius delta (value is (param1 / 2) + 30)
|
||||||
// param4 = delay (value is param4 + 30; clamped below to 0)
|
// param4 = delay (value is param4 + 30; clamped below to 0)
|
||||||
// angle.z = TODO (seems it only matters if angle.z is > 0 or not)
|
// angle.z = hide body (positive = only the blue highlights appear; zero
|
||||||
|
// or negative = normal visibility)
|
||||||
{0x0154, F_V0_V4, 0x0000400000000700, "TOTrapAncient02"},
|
{0x0154, F_V0_V4, 0x0000400000000700, "TOTrapAncient02"},
|
||||||
|
|
||||||
// Ruins crystal monument. Same parameters as TOCapsuleAncient01.
|
// Ruins crystal monument. Same parameters as TOCapsuleAncient01.
|
||||||
@@ -1885,7 +1980,7 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// 2 = Gibarta
|
// 2 = Gibarta
|
||||||
// 3 = Megid
|
// 3 = Megid
|
||||||
// anything else = Gifoie
|
// anything else = Gifoie
|
||||||
// angle.z = TODO (seems it only matters if angle.z is > 0 or not)
|
// angle.z = hide body (same as for TOTrapAncient02)
|
||||||
{0x0167, F_V2_V4, 0x0000400C3FF807C0, "TOTrapAncient02R"},
|
{0x0167, F_V2_V4, 0x0000400C3FF807C0, "TOTrapAncient02R"},
|
||||||
|
|
||||||
// Flying white bird. Params:
|
// Flying white bird. Params:
|
||||||
@@ -2029,7 +2124,8 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// Episode 3 lobby jukebox. No parameters.
|
// Episode 3 lobby jukebox. No parameters.
|
||||||
{0x018E, F_EP3, 0x0000000000008000, "TObjJukeBox"},
|
{0x018E, F_EP3, 0x0000000000008000, "TObjJukeBox"},
|
||||||
|
|
||||||
// TODO: Describe this object. There appear to be no parameters.
|
// Spaceship overhead camera with red light and annoying gear noises. There
|
||||||
|
// appear to be no parameters.
|
||||||
{0x0190, F_V2_V4, 0x0000400000610000, "TObjCamera"},
|
{0x0190, F_V2_V4, 0x0000400000610000, "TObjCamera"},
|
||||||
|
|
||||||
// Short Spaceship wall. There appear to be no parameters.
|
// Short Spaceship wall. There appear to be no parameters.
|
||||||
@@ -2178,9 +2274,9 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
{0x020F, F_V3_V4, 0x0000400C3F800000, "TOTrapChainSawDamage"},
|
{0x020F, F_V3_V4, 0x0000400C3F800000, "TOTrapChainSawDamage"},
|
||||||
|
|
||||||
// Laser detector trap. Params:
|
// Laser detector trap. Params:
|
||||||
// param3 = model number (<= for small laser, > 0 for large laser)
|
// param3 = model number (<= 0 for small laser, > 0 for large laser)
|
||||||
// param4 = switch flag number (enables this flag when triggered)
|
// param4 = switch flag number (enables this flag when triggered)
|
||||||
// param5-6: same as 0x020F (TOTrapChainSawDamage)
|
// param5-6 = same as 0x020F (TOTrapChainSawDamage)
|
||||||
{0x0210, F_V3_V4, 0x0000400C3F800000, "TOTrapChainSawKey"},
|
{0x0210, F_V3_V4, 0x0000400C3F800000, "TOTrapChainSawKey"},
|
||||||
|
|
||||||
// TODO: Describe this object. It's a subclass of TODragonfly and has the
|
// TODO: Describe this object. It's a subclass of TODragonfly and has the
|
||||||
@@ -2194,8 +2290,11 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
{0x0212, F_V3_V4, 0x000040080F800000, "__SEAGULL__"},
|
{0x0212, F_V3_V4, 0x000040080F800000, "__SEAGULL__"},
|
||||||
{0x0212, F_EP3, 0x0000000000000002, "__SEAGULL__"},
|
{0x0212, F_EP3, 0x0000000000000002, "__SEAGULL__"},
|
||||||
|
|
||||||
// TODO: Describe this object. Params:
|
// Jungle wooden objects. Params:
|
||||||
// param4 = model number (clamped to [0, 2])
|
// param4 = model number:
|
||||||
|
// 0 or negative: long branch
|
||||||
|
// 1: shorter curved branch
|
||||||
|
// 2 or greater: small log
|
||||||
{0x0213, F_V3_V4, 0x00004E040F000000, "TOJungleDesign"},
|
{0x0213, F_V3_V4, 0x00004E040F000000, "TOJungleDesign"},
|
||||||
|
|
||||||
// Fish. This object is not constructed in split-screen mode. Params:
|
// Fish. This object is not constructed in split-screen mode. Params:
|
||||||
@@ -2363,7 +2462,8 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// param6 low byte = TODO
|
// param6 low byte = TODO
|
||||||
{0x02D0, F_EP3, 0x0000000000000002, "TObjKazariCard"},
|
{0x02D0, F_EP3, 0x0000000000000002, "TObjKazariCard"},
|
||||||
|
|
||||||
// TODO: Describe these objects. There appear to be no parameters.
|
// Effects visible in the central column in the Morgue when cards
|
||||||
|
// transform. There appear to be no parameters.
|
||||||
{0x02D1, F_EP3, 0x0000000000000001, "TObj_FloatingCardMaterial_Dark"},
|
{0x02D1, F_EP3, 0x0000000000000001, "TObj_FloatingCardMaterial_Dark"},
|
||||||
{0x02D2, F_EP3, 0x0000000000000001, "TObj_FloatingCardMaterial_Hero"},
|
{0x02D2, F_EP3, 0x0000000000000001, "TObj_FloatingCardMaterial_Hero"},
|
||||||
|
|
||||||
@@ -2372,21 +2472,23 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// for the lobby teleporter, or TShopGenerator for the battle counter).
|
// for the lobby teleporter, or TShopGenerator for the battle counter).
|
||||||
// TObjCardCityMapWarp takes no parameters.
|
// TObjCardCityMapWarp takes no parameters.
|
||||||
{0x02D3, F_EP3, 0x0000000000000001, "TObjCardCityMapWarp(0)"}, // Battle counter warp (blue lines)
|
{0x02D3, F_EP3, 0x0000000000000001, "TObjCardCityMapWarp(0)"}, // Battle counter warp (blue lines)
|
||||||
{0x02D9, F_EP3, 0x0000000000000001, "TObjCardCityMapWarp(1)"}, // TODO
|
{0x02D9, F_EP3, 0x0000000000000001, "TObjCardCityMapWarp(1)"}, // Battle counter warp (green lines; unused)
|
||||||
{0x02E3, F_EP3, 0x0000000000000001, "TObjCardCityMapWarp(2)"}, // Lobby warp (yellow lines)
|
{0x02E3, F_EP3, 0x0000000000000001, "TObjCardCityMapWarp(2)"}, // Lobby warp (yellow lines)
|
||||||
|
|
||||||
// Morgue doors. None of these take any parameters. Unsurprisingly, the
|
// Morgue doors. None of these take any parameters. Unsurprisingly, the
|
||||||
// _Closed variants don't open.
|
// _Closed variants don't open. The _Closed variants also have a red light
|
||||||
{0x02D4, F_EP3, 0x0000000000000001, "TObjCardCityDoor(0)"}, // Yellow (to deck edit room)
|
// in the center instead of a blue light. Curiously, the (3) variant is
|
||||||
{0x02D5, F_EP3, 0x0000000000000001, "TObjCardCityDoor(1)"}, // Blue (to battle entry counter)
|
// opaque when _Closed, unlike the other _Closed doors.
|
||||||
{0x02D8, F_EP3, 0x0000000000000001, "TObjCardCityDoor(2)"}, // TODO
|
{0x02D4, F_EP3, 0x0000000000000001, "TObjCardCityDoor(0)"}, // Yellow V-pattern (to deck edit room)
|
||||||
{0x02DF, F_EP3, 0x0000000000000001, "TObjCardCityDoor(3)"}, // Solid (always closed in normal Morgue)
|
{0x02D5, F_EP3, 0x0000000000000001, "TObjCardCityDoor(1)"}, // Blue V-pattern (to battle entry counter)
|
||||||
|
{0x02D8, F_EP3, 0x0000000000000001, "TObjCardCityDoor(2)"}, // Green V-pattern (unused)
|
||||||
|
{0x02DF, F_EP3, 0x0000000000000001, "TObjCardCityDoor(3)"}, // Blue X-pattern (to lobby teleporter)
|
||||||
{0x02E0, F_EP3, 0x0000000000000001, "TObjCardCityDoor(4)"}, // Gray (to chief)
|
{0x02E0, F_EP3, 0x0000000000000001, "TObjCardCityDoor(4)"}, // Gray (to chief)
|
||||||
{0x02DC, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(0)"}, // TODO
|
{0x02DC, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(0)"}, // Yellow V-pattern (to deck edit room)
|
||||||
{0x02DD, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(1)"}, // TODO
|
{0x02DD, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(1)"}, // Blue V-pattern (to battle entry counter)
|
||||||
{0x02DE, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(2)"}, // TODO
|
{0x02DE, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(2)"}, // Green V-pattern (unused)
|
||||||
{0x02E1, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(3)"}, // TODO
|
{0x02E1, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(3)"}, // Opaque gray X-pattern
|
||||||
{0x02E2, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(4)"}, // TODO
|
{0x02E2, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(4)"}, // Gray (to chief)
|
||||||
|
|
||||||
// Mortis Fons geyser. Params:
|
// Mortis Fons geyser. Params:
|
||||||
// param1-3 = TODO
|
// param1-3 = TODO
|
||||||
@@ -2425,11 +2527,16 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
|||||||
// param4 = index into location bit field (0-31 where 0 is the least-
|
// param4 = index into location bit field (0-31 where 0 is the least-
|
||||||
// significant bit; see Episode3LobbyBanners in config.example.json
|
// significant bit; see Episode3LobbyBanners in config.example.json
|
||||||
// for the bits' meanings)
|
// for the bits' meanings)
|
||||||
// param5 = TODO
|
// param5 = per-axis mirror flags (the low 3 nybbles of this value
|
||||||
|
// specify whether to invert each axis of the model; if any nybble is 1
|
||||||
|
// then the model is inverted along the corresponding axis; the lowest
|
||||||
|
// nybble corresponds to the x axis)
|
||||||
{0x02E4, F_EP3, 0x0000000000008001, "TObjSinBoardCard"},
|
{0x02E4, F_EP3, 0x0000000000008001, "TObjSinBoardCard"},
|
||||||
|
|
||||||
// TODO: Describe this object. Params:
|
// Morgue info screen. Params:
|
||||||
// param4 = model number (0 or 1)
|
// param4 = model number:
|
||||||
|
// 0 = tall, yellow, vertical-scrolling
|
||||||
|
// 1 = short, blue, horizontal-scrolling
|
||||||
{0x02E5, F_EP3, 0x0000000000000001, "TObjCityMoji"},
|
{0x02E5, F_EP3, 0x0000000000000001, "TObjCityMoji"},
|
||||||
|
|
||||||
// Like TObjCardCityMapWarp(2) (the warp to the lobby from the Morgue)
|
// Like TObjCardCityMapWarp(2) (the warp to the lobby from the Morgue)
|
||||||
@@ -3398,7 +3505,7 @@ string MapFile::name_for_enemy_type(uint16_t type, Version version, uint8_t area
|
|||||||
|
|
||||||
string MapFile::ObjectSetEntry::str(Version version, uint8_t area) const {
|
string MapFile::ObjectSetEntry::str(Version version, uint8_t area) const {
|
||||||
string name_str = MapFile::name_for_object_type(this->base_type, version, area);
|
string name_str = MapFile::name_for_object_type(this->base_type, version, area);
|
||||||
return std::format("[ObjectSetEntry type={:04X} \"{}\" floor={:04X} group={:04X} room={:04X} a3={:04X} x={:g} y={:g} z={:g} x_angle={:08X} y_angle={:08X} z_angle={:08X} params=[{:g} {:g} {:g} {:08X} {:08X} {:08X}] unused={:08X}]",
|
return std::format("[ObjectSetEntry type={:04X} \"{}\" floor={:04X} group={:04X} room={:04X} a3={:04X} x={:g} y={:g} z={:g} x_angle={:08X} y_angle={:08X} z_angle={:08X} params=[{:g} {:g} {:g} {:08X} {:08X} {:08X}]]",
|
||||||
this->base_type,
|
this->base_type,
|
||||||
name_str,
|
name_str,
|
||||||
this->floor,
|
this->floor,
|
||||||
@@ -3416,8 +3523,7 @@ string MapFile::ObjectSetEntry::str(Version version, uint8_t area) const {
|
|||||||
this->param3,
|
this->param3,
|
||||||
this->param4,
|
this->param4,
|
||||||
this->param5,
|
this->param5,
|
||||||
this->param6,
|
this->param6);
|
||||||
this->unused);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t MapFile::ObjectSetEntry::semantic_hash(uint8_t floor) const {
|
uint64_t MapFile::ObjectSetEntry::semantic_hash(uint8_t floor) const {
|
||||||
@@ -3438,7 +3544,7 @@ uint64_t MapFile::ObjectSetEntry::semantic_hash(uint8_t floor) const {
|
|||||||
|
|
||||||
string MapFile::EnemySetEntry::str(Version version, uint8_t area) const {
|
string MapFile::EnemySetEntry::str(Version version, uint8_t area) const {
|
||||||
auto type_name = MapFile::name_for_enemy_type(this->base_type, version, area);
|
auto type_name = MapFile::name_for_enemy_type(this->base_type, version, area);
|
||||||
return std::format("[EnemySetEntry type={:04X} \"{}\" num_children={:04X} floor={:04X} room={:04X} wave_number={:04X} wave_number2={:04X} a1={:04X} x={:g} y={:g} z={:g} x_angle={:08X} y_angle={:08X} z_angle={:08X} params=[{:g} {:g} {:g} {:g} {:g} {:04X} {:04X}] unused={:08X}]",
|
return std::format("[EnemySetEntry type={:04X} \"{}\" num_children={:04X} floor={:04X} room={:04X} wave_number={:04X} wave_number2={:04X} a1={:04X} x={:g} y={:g} z={:g} x_angle={:08X} y_angle={:08X} z_angle={:08X} params=[{:g} {:g} {:g} {:g} {:g} {:04X} {:04X}]]",
|
||||||
this->base_type,
|
this->base_type,
|
||||||
type_name,
|
type_name,
|
||||||
this->num_children,
|
this->num_children,
|
||||||
@@ -3459,8 +3565,7 @@ string MapFile::EnemySetEntry::str(Version version, uint8_t area) const {
|
|||||||
this->param4,
|
this->param4,
|
||||||
this->param5,
|
this->param5,
|
||||||
this->param6,
|
this->param6,
|
||||||
this->param7,
|
this->param7);
|
||||||
this->unused);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t MapFile::EnemySetEntry::semantic_hash(uint8_t floor) const {
|
uint64_t MapFile::EnemySetEntry::semantic_hash(uint8_t floor) const {
|
||||||
@@ -4215,13 +4320,11 @@ string SuperMap::Enemy::id_str() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
string SuperMap::Enemy::str() const {
|
string SuperMap::Enemy::str() const {
|
||||||
string ret = std::format("[Enemy ES-{:02X}-{:03X}-{:03X} type={} child_index={:X} alias_enemy_index_delta={:X} is_default_rare_v123={} is_default_rare_bb={}",
|
string ret = std::format("[Enemy {} type={} child_index={:X} alias_target_ene={} is_default_rare_v123={} is_default_rare_bb={}",
|
||||||
this->floor,
|
this->id_str(),
|
||||||
this->super_set_id,
|
|
||||||
this->super_id,
|
|
||||||
phosg::name_for_enum(this->type),
|
phosg::name_for_enum(this->type),
|
||||||
this->child_index,
|
this->child_index,
|
||||||
this->alias_enemy_index_delta,
|
(this->alias_target_ene ? this->alias_target_ene->id_str() : "(none)"),
|
||||||
this->is_default_rare_v123 ? "true" : "false",
|
this->is_default_rare_v123 ? "true" : "false",
|
||||||
this->is_default_rare_bb ? "true" : "false");
|
this->is_default_rare_bb ? "true" : "false");
|
||||||
for (Version v : ALL_NON_PATCH_VERSIONS) {
|
for (Version v : ALL_NON_PATCH_VERSIONS) {
|
||||||
@@ -4361,16 +4464,18 @@ void SuperMap::link_object_version(std::shared_ptr<Object> obj, Version version,
|
|||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<SuperMap::Enemy> SuperMap::add_enemy_and_children(
|
shared_ptr<SuperMap::Enemy> SuperMap::add_enemy_and_children(
|
||||||
Version version,
|
Version version, uint8_t floor, const MapFile::EnemySetEntry* set_entry) {
|
||||||
uint8_t floor,
|
|
||||||
const MapFile::EnemySetEntry* set_entry) {
|
|
||||||
|
|
||||||
shared_ptr<Enemy> head_ene = nullptr;
|
shared_ptr<Enemy> head_ene = nullptr;
|
||||||
size_t next_child_index = 0;
|
size_t next_child_index = 0;
|
||||||
auto add = [&](EnemyType type,
|
auto add = [&](EnemyType type,
|
||||||
bool is_default_rare_v123 = false,
|
bool is_default_rare_v123 = false,
|
||||||
bool is_default_rare_bb = false,
|
bool is_default_rare_bb = false,
|
||||||
int16_t alias_enemy_index_delta = 0) -> void {
|
std::shared_ptr<Enemy> alias_target_ene = nullptr) -> std::shared_ptr<Enemy> {
|
||||||
|
if (alias_target_ene && alias_target_ene->alias_target_ene) {
|
||||||
|
throw std::logic_error("alias may not point to an enemy that also is an alias");
|
||||||
|
}
|
||||||
|
|
||||||
auto& entities = this->version(version);
|
auto& entities = this->version(version);
|
||||||
|
|
||||||
// TODO: It'd be nice to share some code between this function and
|
// TODO: It'd be nice to share some code between this function and
|
||||||
@@ -4385,7 +4490,7 @@ shared_ptr<SuperMap::Enemy> SuperMap::add_enemy_and_children(
|
|||||||
ene->type = type;
|
ene->type = type;
|
||||||
ene->is_default_rare_v123 = is_default_rare_v123;
|
ene->is_default_rare_v123 = is_default_rare_v123;
|
||||||
ene->is_default_rare_bb = is_default_rare_bb;
|
ene->is_default_rare_bb = is_default_rare_bb;
|
||||||
ene->alias_enemy_index_delta = alias_enemy_index_delta;
|
ene->alias_target_ene = alias_target_ene;
|
||||||
auto& ene_ver = ene->version(version);
|
auto& ene_ver = ene->version(version);
|
||||||
ene_ver.set_entry = set_entry;
|
ene_ver.set_entry = set_entry;
|
||||||
ene_ver.relative_enemy_index = entities.enemies.size();
|
ene_ver.relative_enemy_index = entities.enemies.size();
|
||||||
@@ -4407,6 +4512,7 @@ shared_ptr<SuperMap::Enemy> SuperMap::add_enemy_and_children(
|
|||||||
// Add to room/group index
|
// Add to room/group index
|
||||||
uint64_t k = room_index_key(ene->floor, set_entry->room, set_entry->wave_number);
|
uint64_t k = room_index_key(ene->floor, set_entry->room, set_entry->wave_number);
|
||||||
entities.enemy_for_floor_room_and_wave_number.emplace(k, ene);
|
entities.enemy_for_floor_room_and_wave_number.emplace(k, ene);
|
||||||
|
return ene;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The following logic was originally based on the public version of
|
// The following logic was originally based on the public version of
|
||||||
@@ -4650,26 +4756,28 @@ shared_ptr<SuperMap::Enemy> SuperMap::add_enemy_and_children(
|
|||||||
case 0x00C5: // Unnamed subclass of TObjEnemyCustom
|
case 0x00C5: // Unnamed subclass of TObjEnemyCustom
|
||||||
add(EnemyType::VOL_OPT_2);
|
add(EnemyType::VOL_OPT_2);
|
||||||
break;
|
break;
|
||||||
case 0x00C8: // TBoss4DarkFalz
|
case 0x00C8: { // TBoss4DarkFalz
|
||||||
if ((set_entry->num_children != 0) && (set_entry->num_children != 0x200)) {
|
if ((set_entry->num_children != 0) && (set_entry->num_children != 0x200)) {
|
||||||
this->log.warning_f("DARK_FALZ has an unusual num_children (0x{:X})", set_entry->num_children);
|
this->log.warning_f("DARK_FALZ has an unusual num_children (0x{:X})", set_entry->num_children);
|
||||||
}
|
}
|
||||||
add(EnemyType::DARK_FALZ_3);
|
auto root_ene = add(EnemyType::DARK_FALZ_3);
|
||||||
default_num_children = -1; // Skip adding children (because we do it here)
|
default_num_children = -1; // Skip adding children (because we do it here)
|
||||||
for (size_t x = 0; x < 0x1FD; x++) {
|
for (size_t x = 0; x < 0x1FD; x++) {
|
||||||
add(EnemyType::DARVANT);
|
add(EnemyType::DARVANT);
|
||||||
}
|
}
|
||||||
add(EnemyType::DARK_FALZ_3, false, false, -0x1FE);
|
add(EnemyType::DARK_FALZ_3, false, false, root_ene);
|
||||||
add(EnemyType::DARK_FALZ_2, false, false, -0x1FF);
|
add(EnemyType::DARK_FALZ_2, false, false, root_ene);
|
||||||
add(EnemyType::DARK_FALZ_1, false, false, -0x200);
|
add(EnemyType::DARK_FALZ_1, false, false, root_ene);
|
||||||
break;
|
break;
|
||||||
case 0x00CA: // TBoss6PlotFalz
|
}
|
||||||
add(EnemyType::OLGA_FLOW_2);
|
case 0x00CA: { // TBoss6PlotFalz
|
||||||
|
auto root_ene = add(EnemyType::OLGA_FLOW_2);
|
||||||
default_num_children = -1; // Skip adding children (because we do it here)
|
default_num_children = -1; // Skip adding children (because we do it here)
|
||||||
for (size_t x = 0; x < 0x200; x++) {
|
for (size_t x = 0; x < 0x200; x++) {
|
||||||
add(EnemyType::OLGA_FLOW_2, false, false, -(x + 1));
|
add(EnemyType::OLGA_FLOW_2, false, false, root_ene);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 0x00CB: // TBoss7DeRolLeC
|
case 0x00CB: // TBoss7DeRolLeC
|
||||||
add(EnemyType::BARBA_RAY);
|
add(EnemyType::BARBA_RAY);
|
||||||
child_type = EnemyType::PIG_RAY;
|
child_type = EnemyType::PIG_RAY;
|
||||||
@@ -5383,25 +5491,16 @@ vector<shared_ptr<const SuperMap::Object>> SuperMap::doors_for_switch_flag(
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<const SuperMap::Enemy> SuperMap::enemy_for_index(Version version, uint16_t enemy_id, bool follow_alias) const {
|
shared_ptr<const SuperMap::Enemy> SuperMap::enemy_for_index(Version version, uint16_t enemy_index) const {
|
||||||
const auto& entities = this->version(version);
|
const auto& entities = this->version(version);
|
||||||
|
|
||||||
if (entities.enemies.empty()) {
|
if (entities.enemies.empty()) {
|
||||||
throw out_of_range("no enemies defined");
|
throw out_of_range("no enemies defined");
|
||||||
}
|
}
|
||||||
if (enemy_id >= entities.enemies.size()) {
|
if (enemy_index >= entities.enemies.size()) {
|
||||||
throw out_of_range("enemy ID out of range");
|
throw out_of_range("enemy ID out of range");
|
||||||
}
|
}
|
||||||
auto& enemy = entities.enemies[enemy_id];
|
return entities.enemies[enemy_index];
|
||||||
if (follow_alias && (enemy->alias_enemy_index_delta != 0)) {
|
|
||||||
uint16_t target_id = enemy_id + enemy->alias_enemy_index_delta;
|
|
||||||
if (target_id >= entities.enemies.size()) {
|
|
||||||
throw out_of_range("aliased enemy ID out of range");
|
|
||||||
}
|
|
||||||
return entities.enemies[target_id];
|
|
||||||
} else {
|
|
||||||
return enemy;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<const SuperMap::Enemy> SuperMap::enemy_for_floor_type(Version version, uint8_t floor, EnemyType type) const {
|
shared_ptr<const SuperMap::Enemy> SuperMap::enemy_for_floor_type(Version version, uint8_t floor, EnemyType type) const {
|
||||||
@@ -5410,10 +5509,13 @@ shared_ptr<const SuperMap::Enemy> SuperMap::enemy_for_floor_type(Version version
|
|||||||
if (entities.enemies.empty()) {
|
if (entities.enemies.empty()) {
|
||||||
throw out_of_range("no enemies defined");
|
throw out_of_range("no enemies defined");
|
||||||
}
|
}
|
||||||
// TODO: Linear search is bad here. Do something better, like binary search
|
size_t start_z = entities.enemy_floor_start_indexes.at(floor);
|
||||||
// for the floor start and just linear search through the floor enemies.
|
size_t end_z = (floor < entities.enemy_floor_start_indexes.size() - 1)
|
||||||
for (auto& ene : entities.enemies) {
|
? entities.enemy_floor_start_indexes[floor + 1]
|
||||||
if ((ene->floor == floor) && (ene->type == type)) {
|
: entities.enemy_floor_start_indexes.size();
|
||||||
|
for (size_t z = start_z; z < end_z; z++) {
|
||||||
|
auto& ene = entities.enemies[z];
|
||||||
|
if ((ene->floor == floor) || (ene->type == type)) {
|
||||||
return ene;
|
return ene;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5953,7 +6055,7 @@ size_t MapState::EventIterator::num_entities_on_current_floor() const {
|
|||||||
|
|
||||||
MapState::MapState(
|
MapState::MapState(
|
||||||
uint64_t lobby_or_session_id,
|
uint64_t lobby_or_session_id,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
uint32_t random_seed,
|
uint32_t random_seed,
|
||||||
std::shared_ptr<const RareEnemyRates> bb_rare_rates,
|
std::shared_ptr<const RareEnemyRates> bb_rare_rates,
|
||||||
@@ -6003,7 +6105,7 @@ MapState::MapState(
|
|||||||
|
|
||||||
MapState::MapState(
|
MapState::MapState(
|
||||||
uint64_t lobby_or_session_id,
|
uint64_t lobby_or_session_id,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
uint32_t random_seed,
|
uint32_t random_seed,
|
||||||
std::shared_ptr<const RareEnemyRates> bb_rare_rates,
|
std::shared_ptr<const RareEnemyRates> bb_rare_rates,
|
||||||
@@ -6050,6 +6152,7 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptr<RandomGenerator
|
|||||||
|
|
||||||
for (const auto& ene : fc.super_map->all_enemies()) {
|
for (const auto& ene : fc.super_map->all_enemies()) {
|
||||||
auto& ene_st = this->enemy_states.emplace_back(make_shared<EnemyState>());
|
auto& ene_st = this->enemy_states.emplace_back(make_shared<EnemyState>());
|
||||||
|
|
||||||
if (ene->child_index == 0) {
|
if (ene->child_index == 0) {
|
||||||
this->enemy_set_states.emplace_back(ene_st);
|
this->enemy_set_states.emplace_back(ene_st);
|
||||||
}
|
}
|
||||||
@@ -6057,16 +6160,25 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptr<RandomGenerator
|
|||||||
ene_st->set_id = this->enemy_set_states.size() - 1;
|
ene_st->set_id = this->enemy_set_states.size() - 1;
|
||||||
ene_st->super_ene = ene;
|
ene_st->super_ene = ene;
|
||||||
|
|
||||||
|
if (ene->alias_target_ene) {
|
||||||
|
ssize_t delta = ene->alias_target_ene->super_id - ene->super_id;
|
||||||
|
ene_st->alias_target_ene_st = this->enemy_states.at(ene_st->e_id + delta);
|
||||||
|
if (ene_st->alias_target_ene_st->super_ene != ene->alias_target_ene) {
|
||||||
|
throw std::logic_error("found incorrect alias target state for enemy");
|
||||||
|
}
|
||||||
|
if (ene_st->alias_target_ene_st->super_ene->alias_target_ene) {
|
||||||
|
throw std::runtime_error("target for enemy state alias is itself an alias");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle random rare enemies and difficulty-based effects
|
// Handle random rare enemies and difficulty-based effects
|
||||||
EnemyType type;
|
EnemyType type;
|
||||||
switch (ene->type) {
|
switch (ene->type) {
|
||||||
case EnemyType::DARK_FALZ_3:
|
case EnemyType::DARK_FALZ_3:
|
||||||
type = ((this->difficulty == 0) && (ene->alias_enemy_index_delta == 0))
|
type = (this->difficulty == Difficulty::NORMAL) ? EnemyType::DARK_FALZ_2 : EnemyType::DARK_FALZ_3;
|
||||||
? EnemyType::DARK_FALZ_2
|
|
||||||
: EnemyType::DARK_FALZ_3;
|
|
||||||
break;
|
break;
|
||||||
case EnemyType::DARVANT:
|
case EnemyType::DARVANT:
|
||||||
type = (this->difficulty == 3) ? EnemyType::DARVANT_ULTIMATE : EnemyType::DARVANT;
|
type = (this->difficulty == Difficulty::ULTIMATE) ? EnemyType::DARVANT_ULTIMATE : EnemyType::DARVANT;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
type = ene->type;
|
type = ene->type;
|
||||||
@@ -6198,6 +6310,32 @@ uint16_t MapState::index_for_event_state(Version version, shared_ptr<const Event
|
|||||||
: (relative_index + this->floor_config(ev_st->super_ev->floor).base_indexes_for_version(version).base_event_index);
|
: (relative_index + this->floor_config(ev_st->super_ev->floor).base_indexes_for_version(version).base_event_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shared_ptr<MapState::ObjectState> MapState::object_state_for_index(Version version, uint16_t object_index) {
|
||||||
|
size_t dynamic_obj_base_index = this->dynamic_obj_base_index_for_version.at(static_cast<size_t>(version));
|
||||||
|
if (object_index < dynamic_obj_base_index) {
|
||||||
|
int8_t floor;
|
||||||
|
for (floor = this->floor_config_entries.size() - 1; floor >= 0; floor--) {
|
||||||
|
const auto& fc = this->floor_config_entries[floor];
|
||||||
|
size_t base_object_index = fc.base_indexes_for_version(version).base_object_index;
|
||||||
|
if (object_index >= base_object_index) {
|
||||||
|
if (!fc.super_map) {
|
||||||
|
throw out_of_range("there are no objects on the specified floor");
|
||||||
|
}
|
||||||
|
const auto& obj = fc.super_map->version(version).objects.at(object_index - base_object_index);
|
||||||
|
return this->object_states.at(fc.base_super_ids.base_object_index + obj->super_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw out_of_range("the specified enemy does not exist");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
size_t k_id_delta = object_index - dynamic_obj_base_index;
|
||||||
|
auto obj_st = make_shared<ObjectState>();
|
||||||
|
obj_st->k_id = this->dynamic_obj_base_k_id + k_id_delta;
|
||||||
|
obj_st->super_obj = nullptr;
|
||||||
|
return obj_st;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
shared_ptr<MapState::ObjectState> MapState::object_state_for_index(Version version, uint8_t floor, uint16_t object_index) {
|
shared_ptr<MapState::ObjectState> MapState::object_state_for_index(Version version, uint8_t floor, uint16_t object_index) {
|
||||||
size_t dynamic_obj_base_index = this->dynamic_obj_base_index_for_version.at(static_cast<size_t>(version));
|
size_t dynamic_obj_base_index = this->dynamic_obj_base_index_for_version.at(static_cast<size_t>(version));
|
||||||
if (object_index < dynamic_obj_base_index) {
|
if (object_index < dynamic_obj_base_index) {
|
||||||
@@ -6245,6 +6383,22 @@ vector<shared_ptr<MapState::ObjectState>> MapState::door_states_for_switch_flag(
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shared_ptr<MapState::EnemyState> MapState::enemy_state_for_index(Version version, uint16_t enemy_index) {
|
||||||
|
int8_t floor;
|
||||||
|
for (floor = this->floor_config_entries.size() - 1; floor >= 0; floor--) {
|
||||||
|
const auto& fc = this->floor_config_entries[floor];
|
||||||
|
size_t base_enemy_index = fc.base_indexes_for_version(version).base_enemy_index;
|
||||||
|
if (enemy_index >= base_enemy_index) {
|
||||||
|
if (!fc.super_map) {
|
||||||
|
throw out_of_range("there are no enemies on the specified floor");
|
||||||
|
}
|
||||||
|
const auto& ene = fc.super_map->version(version).enemies.at(enemy_index - base_enemy_index);
|
||||||
|
return this->enemy_states.at(fc.base_super_ids.base_enemy_index + ene->super_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw out_of_range("the specified enemy does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
shared_ptr<MapState::EnemyState> MapState::enemy_state_for_index(Version version, uint8_t floor, uint16_t enemy_index) {
|
shared_ptr<MapState::EnemyState> MapState::enemy_state_for_index(Version version, uint8_t floor, uint16_t enemy_index) {
|
||||||
const auto& fc = this->floor_config(floor);
|
const auto& fc = this->floor_config(floor);
|
||||||
size_t base_enemy_index = fc.base_indexes_for_version(version).base_enemy_index;
|
size_t base_enemy_index = fc.base_indexes_for_version(version).base_enemy_index;
|
||||||
@@ -6405,18 +6559,18 @@ void MapState::import_enemy_states_from_sync(Version from_version, const SyncEne
|
|||||||
const auto& entry = entries[enemy_index];
|
const auto& entry = entries[enemy_index];
|
||||||
const auto& ene = entities.enemies.at(enemy_index - base_indexes.base_enemy_index);
|
const auto& ene = entities.enemies.at(enemy_index - base_indexes.base_enemy_index);
|
||||||
auto& ene_st = this->enemy_states.at(fc.base_super_ids.base_enemy_index + ene->super_id);
|
auto& ene_st = this->enemy_states.at(fc.base_super_ids.base_enemy_index + ene->super_id);
|
||||||
if (ene_st->super_ene != ene) {
|
// Only set the state if it's not an alias
|
||||||
throw logic_error("super enemy link is incorrect");
|
if (ene_st->super_ene == ene) {
|
||||||
}
|
if (ene_st->game_flags != entry.flags) {
|
||||||
if (ene_st->game_flags != entry.flags) {
|
this->log.warning_f("({:04X} => E-{:03X}) Flags from client ({:08X}) do not match game flags from map ({:08X})",
|
||||||
this->log.warning_f("({:04X} => E-{:03X}) Flags from client ({:08X}) do not match game flags from map ({:08X})",
|
enemy_index, ene_st->e_id, entry.flags, ene_st->game_flags);
|
||||||
enemy_index, ene_st->e_id, entry.flags, ene_st->game_flags);
|
ene_st->game_flags = entry.flags;
|
||||||
ene_st->game_flags = entry.flags;
|
}
|
||||||
}
|
if (ene_st->total_damage != entry.total_damage) {
|
||||||
if (ene_st->total_damage != entry.total_damage) {
|
this->log.warning_f("({:04X} => E-{:03X}) Total damage from client ({}) does not match total damage from map ({})",
|
||||||
this->log.warning_f("({:04X} => E-{:03X}) Total damage from client ({}) does not match total damage from map ({})",
|
enemy_index, ene_st->e_id, entry.total_damage, ene_st->total_damage);
|
||||||
enemy_index, ene_st->e_id, entry.total_damage, ene_st->total_damage);
|
ene_st->total_damage = entry.total_damage;
|
||||||
ene_st->total_damage = entry.total_damage;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6689,7 +6843,7 @@ void MapState::print(FILE* stream) const {
|
|||||||
phosg::fwrite_fmt(stream, "BB rare rates: {}\n", rare_rates_str);
|
phosg::fwrite_fmt(stream, "BB rare rates: {}\n", rare_rates_str);
|
||||||
|
|
||||||
phosg::fwrite_fmt(stream, "Base indexes:\n");
|
phosg::fwrite_fmt(stream, "Base indexes:\n");
|
||||||
phosg::fwrite_fmt(stream, " FL DCTE----------- DCPR----------- DCV1----------- DCV2----------- PCTE----------- PCV2----------- GCTE----------- GCV3----------- GCEP3TE-------- GCEP3---------- XBV3----------- BBV4-----------\n");
|
phosg::fwrite_fmt(stream, " FL DC-NTE--------- DC-11-2000----- DC-V1---------- DC-V2---------- PC-NTE--------- PC-V2---------- GC-NTE--------- GC-V3---------- GC-EP3-NTE----- GC-EP3--------- XB-V3---------- BB-V4----------\n");
|
||||||
phosg::fwrite_fmt(stream, " FL KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT\n");
|
phosg::fwrite_fmt(stream, " FL KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT KST EST ESS EVT\n");
|
||||||
for (size_t floor = 0; floor < this->floor_config_entries.size(); floor++) {
|
for (size_t floor = 0; floor < this->floor_config_entries.size(); floor++) {
|
||||||
auto fc = this->floor_config_entries[floor];
|
auto fc = this->floor_config_entries[floor];
|
||||||
|
|||||||
+15
-8
@@ -187,7 +187,7 @@ public:
|
|||||||
/* 34 */ le_int32_t param4 = 0;
|
/* 34 */ le_int32_t param4 = 0;
|
||||||
/* 38 */ le_int32_t param5 = 0;
|
/* 38 */ le_int32_t param5 = 0;
|
||||||
/* 3C */ le_int32_t param6 = 0;
|
/* 3C */ le_int32_t param6 = 0;
|
||||||
/* 40 */ le_uint32_t unused = 0; // Reserved for pointer in client's memory; unused by server
|
/* 40 */ le_uint32_t unused_obj_ptr = 0; // Reserved for pointer in client's memory; unused by server
|
||||||
/* 44 */
|
/* 44 */
|
||||||
|
|
||||||
uint64_t semantic_hash(uint8_t floor) const;
|
uint64_t semantic_hash(uint8_t floor) const;
|
||||||
@@ -215,7 +215,7 @@ public:
|
|||||||
/* 3C */ le_float param5 = 0.0f;
|
/* 3C */ le_float param5 = 0.0f;
|
||||||
/* 40 */ le_int16_t param6 = 0;
|
/* 40 */ le_int16_t param6 = 0;
|
||||||
/* 42 */ le_int16_t param7 = 0;
|
/* 42 */ le_int16_t param7 = 0;
|
||||||
/* 44 */ le_uint32_t unused = 0; // Reserved for pointer in client's memory; unused by server
|
/* 44 */ le_uint32_t unused_obj_ptr = 0; // Reserved for pointer in client's memory; unused by server
|
||||||
/* 48 */
|
/* 48 */
|
||||||
|
|
||||||
uint64_t semantic_hash(uint8_t floor) const;
|
uint64_t semantic_hash(uint8_t floor) const;
|
||||||
@@ -491,7 +491,7 @@ public:
|
|||||||
size_t super_id = 0;
|
size_t super_id = 0;
|
||||||
size_t super_set_id = 0;
|
size_t super_set_id = 0;
|
||||||
uint16_t child_index = 0;
|
uint16_t child_index = 0;
|
||||||
int16_t alias_enemy_index_delta = 0; // 0 = no alias
|
std::shared_ptr<Enemy> alias_target_ene; // May be null
|
||||||
EnemyType type = EnemyType::UNKNOWN;
|
EnemyType type = EnemyType::UNKNOWN;
|
||||||
bool is_default_rare_v123 = false;
|
bool is_default_rare_v123 = false;
|
||||||
bool is_default_rare_bb = false;
|
bool is_default_rare_bb = false;
|
||||||
@@ -585,7 +585,7 @@ public:
|
|||||||
std::vector<std::shared_ptr<const Object>> doors_for_switch_flag(
|
std::vector<std::shared_ptr<const Object>> doors_for_switch_flag(
|
||||||
Version version, uint8_t floor, uint8_t switch_flag) const;
|
Version version, uint8_t floor, uint8_t switch_flag) const;
|
||||||
|
|
||||||
std::shared_ptr<const Enemy> enemy_for_index(Version version, uint16_t enemy_index, bool follow_alias) const;
|
std::shared_ptr<const Enemy> enemy_for_index(Version version, uint16_t enemy_index) const;
|
||||||
std::shared_ptr<const Enemy> enemy_for_floor_type(Version version, uint8_t floor, EnemyType type) const;
|
std::shared_ptr<const Enemy> enemy_for_floor_type(Version version, uint8_t floor, EnemyType type) const;
|
||||||
std::vector<std::shared_ptr<const Enemy>> enemies_for_floor_room_wave(
|
std::vector<std::shared_ptr<const Enemy>> enemies_for_floor_room_wave(
|
||||||
Version version, uint8_t floor, uint16_t room, uint16_t wave_number) const;
|
Version version, uint8_t floor, uint16_t room, uint16_t wave_number) const;
|
||||||
@@ -710,6 +710,7 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct EnemyState {
|
struct EnemyState {
|
||||||
|
std::shared_ptr<EnemyState> alias_target_ene_st; // Null for most enemies
|
||||||
std::shared_ptr<const SuperMap::Enemy> super_ene;
|
std::shared_ptr<const SuperMap::Enemy> super_ene;
|
||||||
enum Flag {
|
enum Flag {
|
||||||
LAST_HIT_MASK = 0x0003,
|
LAST_HIT_MASK = 0x0003,
|
||||||
@@ -745,7 +746,7 @@ public:
|
|||||||
inline void set_mericarand_variant_flag(Version version) {
|
inline void set_mericarand_variant_flag(Version version) {
|
||||||
this->mericarand_variant_flags |= (1 << static_cast<size_t>(version));
|
this->mericarand_variant_flags |= (1 << static_cast<size_t>(version));
|
||||||
}
|
}
|
||||||
inline EnemyType type(Version version, Episode episode, uint8_t event) const {
|
inline EnemyType type(Version version, Episode episode, Difficulty difficulty, uint8_t event) const {
|
||||||
if (this->super_ene->type == EnemyType::MERICARAND) {
|
if (this->super_ene->type == EnemyType::MERICARAND) {
|
||||||
if (this->is_rare(version)) {
|
if (this->is_rare(version)) {
|
||||||
return ((this->mericarand_variant_flags >> static_cast<size_t>(version)) & 1)
|
return ((this->mericarand_variant_flags >> static_cast<size_t>(version)) & 1)
|
||||||
@@ -754,6 +755,10 @@ public:
|
|||||||
} else {
|
} else {
|
||||||
return EnemyType::MERICAROL;
|
return EnemyType::MERICAROL;
|
||||||
}
|
}
|
||||||
|
} else if (this->super_ene->type == EnemyType::DARK_FALZ_3) {
|
||||||
|
return ((difficulty == Difficulty::NORMAL) && !this->super_ene->alias_target_ene)
|
||||||
|
? EnemyType::DARK_FALZ_2
|
||||||
|
: EnemyType::DARK_FALZ_3;
|
||||||
} else {
|
} else {
|
||||||
return this->is_rare(version)
|
return this->is_rare(version)
|
||||||
? type_definition_for_enemy(this->super_ene->type).rare_type(episode, event, this->super_ene->floor)
|
? type_definition_for_enemy(this->super_ene->type).rare_type(episode, event, this->super_ene->floor)
|
||||||
@@ -874,7 +879,7 @@ public:
|
|||||||
|
|
||||||
phosg::PrefixedLogger log;
|
phosg::PrefixedLogger log;
|
||||||
std::vector<FloorConfig> floor_config_entries;
|
std::vector<FloorConfig> floor_config_entries;
|
||||||
uint8_t difficulty = 0;
|
Difficulty difficulty = Difficulty::NORMAL;
|
||||||
uint8_t event = 0;
|
uint8_t event = 0;
|
||||||
uint32_t random_seed = 0;
|
uint32_t random_seed = 0;
|
||||||
std::shared_ptr<const RareEnemyRates> bb_rare_rates;
|
std::shared_ptr<const RareEnemyRates> bb_rare_rates;
|
||||||
@@ -889,7 +894,7 @@ public:
|
|||||||
// Constructor for free play
|
// Constructor for free play
|
||||||
MapState(
|
MapState(
|
||||||
uint64_t lobby_or_session_id,
|
uint64_t lobby_or_session_id,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
uint32_t random_seed, // For client-matched rare enemies (non-BB)
|
uint32_t random_seed, // For client-matched rare enemies (non-BB)
|
||||||
std::shared_ptr<const RareEnemyRates> bb_rare_rates,
|
std::shared_ptr<const RareEnemyRates> bb_rare_rates,
|
||||||
@@ -898,7 +903,7 @@ public:
|
|||||||
// Constructor for quests
|
// Constructor for quests
|
||||||
MapState(
|
MapState(
|
||||||
uint64_t lobby_or_session_id,
|
uint64_t lobby_or_session_id,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
uint32_t random_seed, // For client-matched rare enemies (non-BB)
|
uint32_t random_seed, // For client-matched rare enemies (non-BB)
|
||||||
std::shared_ptr<const RareEnemyRates> bb_rare_rates,
|
std::shared_ptr<const RareEnemyRates> bb_rare_rates,
|
||||||
@@ -940,12 +945,14 @@ public:
|
|||||||
uint16_t set_index_for_enemy_state(Version version, std::shared_ptr<const EnemyState> ene_st) const;
|
uint16_t set_index_for_enemy_state(Version version, std::shared_ptr<const EnemyState> ene_st) const;
|
||||||
uint16_t index_for_event_state(Version version, std::shared_ptr<const EventState> evt_st) const;
|
uint16_t index_for_event_state(Version version, std::shared_ptr<const EventState> evt_st) const;
|
||||||
|
|
||||||
|
std::shared_ptr<ObjectState> object_state_for_index(Version version, uint16_t object_index);
|
||||||
std::shared_ptr<ObjectState> object_state_for_index(Version version, uint8_t floor, uint16_t object_index);
|
std::shared_ptr<ObjectState> object_state_for_index(Version version, uint8_t floor, uint16_t object_index);
|
||||||
std::vector<std::shared_ptr<ObjectState>> object_states_for_floor_room_group(
|
std::vector<std::shared_ptr<ObjectState>> object_states_for_floor_room_group(
|
||||||
Version version, uint8_t floor, uint16_t room, uint16_t group);
|
Version version, uint8_t floor, uint16_t room, uint16_t group);
|
||||||
std::vector<std::shared_ptr<ObjectState>> door_states_for_switch_flag(
|
std::vector<std::shared_ptr<ObjectState>> door_states_for_switch_flag(
|
||||||
Version version, uint8_t floor, uint8_t switch_flag);
|
Version version, uint8_t floor, uint8_t switch_flag);
|
||||||
|
|
||||||
|
std::shared_ptr<EnemyState> enemy_state_for_index(Version version, uint16_t enemy_index);
|
||||||
std::shared_ptr<EnemyState> enemy_state_for_index(Version version, uint8_t floor, uint16_t enemy_index);
|
std::shared_ptr<EnemyState> enemy_state_for_index(Version version, uint8_t floor, uint16_t enemy_index);
|
||||||
std::shared_ptr<EnemyState> enemy_state_for_set_index(Version version, uint8_t floor, uint16_t enemy_set_index);
|
std::shared_ptr<EnemyState> enemy_state_for_set_index(Version version, uint8_t floor, uint16_t enemy_set_index);
|
||||||
std::shared_ptr<EnemyState> enemy_state_for_floor_type(Version version, uint8_t floor, EnemyType type);
|
std::shared_ptr<EnemyState> enemy_state_for_floor_type(Version version, uint8_t floor, EnemyType type);
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ constexpr uint32_t QUEST_EP2 = 0x55020255;
|
|||||||
constexpr uint32_t QUEST_EP3 = 0x55030355;
|
constexpr uint32_t QUEST_EP3 = 0x55030355;
|
||||||
// See the decsription of the A2 command in CommandFormats.hh for why these
|
// See the decsription of the A2 command in CommandFormats.hh for why these
|
||||||
// menu IDs don't fit the rest of the pattern.
|
// menu IDs don't fit the rest of the pattern.
|
||||||
constexpr uint32_t QUEST_CATEGORIES_EP1 = 0x01000001;
|
constexpr uint32_t QUEST_CATEGORIES_EP1_EP3_EP4 = 0x01000001;
|
||||||
constexpr uint32_t QUEST_CATEGORIES_EP2 = 0x02000002;
|
constexpr uint32_t QUEST_CATEGORIES_EP2 = 0x02000002;
|
||||||
constexpr uint32_t PROXY_DESTINATIONS = 0x77000077;
|
constexpr uint32_t PROXY_DESTINATIONS = 0x77000077;
|
||||||
constexpr uint32_t PROGRAMS = 0x88000088;
|
constexpr uint32_t PROGRAMS = 0x88000088;
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
#include "PatchDownloadSession.hh"
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <iostream>
|
||||||
|
#include <phosg/Encoding.hh>
|
||||||
|
#include <phosg/Filesystem.hh>
|
||||||
|
#include <phosg/Network.hh>
|
||||||
|
#include <phosg/Random.hh>
|
||||||
|
#include <phosg/Strings.hh>
|
||||||
|
#include <phosg/Time.hh>
|
||||||
|
|
||||||
|
#include "Loggers.hh"
|
||||||
|
#include "PSOProtocol.hh"
|
||||||
|
#include "ReceiveCommands.hh"
|
||||||
|
#include "ReceiveSubcommands.hh"
|
||||||
|
#include "SendCommands.hh"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
PatchDownloadSession::PatchDownloadSession(
|
||||||
|
std::shared_ptr<asio::io_context> io_context,
|
||||||
|
const std::string& remote_host,
|
||||||
|
uint16_t remote_port,
|
||||||
|
const std::string& output_dir,
|
||||||
|
Version version,
|
||||||
|
const std::string& username,
|
||||||
|
const std::string& password,
|
||||||
|
const std::string& email,
|
||||||
|
bool show_command_data)
|
||||||
|
: remote_host(remote_host),
|
||||||
|
remote_port(remote_port),
|
||||||
|
output_dir(output_dir),
|
||||||
|
version(version),
|
||||||
|
username(username),
|
||||||
|
password(password),
|
||||||
|
email(email),
|
||||||
|
show_command_data(show_command_data),
|
||||||
|
log(std::format("[PatchDownloadSession:{}] ", phosg::name_for_enum(version)), proxy_server_log.min_level),
|
||||||
|
io_context(io_context),
|
||||||
|
current_file(nullptr, +[](FILE* f) -> void { fclose(f); }) {
|
||||||
|
if (this->output_dir.empty()) {
|
||||||
|
this->output_dir = ".";
|
||||||
|
}
|
||||||
|
if (!is_patch(this->version)) {
|
||||||
|
throw std::logic_error("invalid version in PatchDownloadSession");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
asio::awaitable<void> PatchDownloadSession::run() {
|
||||||
|
string netloc_str = std::format("{}:{}", this->remote_host, this->remote_port);
|
||||||
|
this->log.info_f("Connecting to {}", netloc_str);
|
||||||
|
auto sock = make_unique<asio::ip::tcp::socket>(co_await async_connect_tcp(this->remote_host, this->remote_port));
|
||||||
|
this->channel = SocketChannel::create(
|
||||||
|
this->io_context,
|
||||||
|
std::move(sock),
|
||||||
|
this->version,
|
||||||
|
Language::ENGLISH,
|
||||||
|
netloc_str,
|
||||||
|
this->show_command_data ? phosg::TerminalFormat::FG_GREEN : phosg::TerminalFormat::END,
|
||||||
|
this->show_command_data ? phosg::TerminalFormat::FG_YELLOW : phosg::TerminalFormat::END);
|
||||||
|
this->log.info_f("Server channel connected");
|
||||||
|
|
||||||
|
while (this->channel->connected()) {
|
||||||
|
auto msg = co_await this->channel->recv();
|
||||||
|
co_await this->on_message(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PatchDownloadSession::check_path_token(const std::string& token) {
|
||||||
|
if (token == "..") {
|
||||||
|
throw std::runtime_error("parent directory token is not allowed");
|
||||||
|
}
|
||||||
|
if ((token.find('/') != string::npos) || (token.find('\\') != string::npos)) {
|
||||||
|
throw std::runtime_error("directory token contains path separator");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PatchDownloadSession::resolve_filename(const std::string& filename) const {
|
||||||
|
check_path_token(filename);
|
||||||
|
string path = this->output_dir;
|
||||||
|
for (const auto& dir_name : this->dir_path) {
|
||||||
|
path.push_back('/');
|
||||||
|
path += dir_name;
|
||||||
|
}
|
||||||
|
if (!filename.empty()) {
|
||||||
|
path.push_back('/');
|
||||||
|
path += filename;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
asio::awaitable<void> PatchDownloadSession::on_message(Channel::Message& msg) {
|
||||||
|
switch (msg.command) {
|
||||||
|
case 0x02: {
|
||||||
|
const auto& cmd = msg.check_size_t<S_ServerInit_Patch_02>();
|
||||||
|
if (cmd.copyright.decode() != "Patch Server. Copyright SonicTeam, LTD. 2001") {
|
||||||
|
throw std::runtime_error("incorrect copyright message");
|
||||||
|
}
|
||||||
|
this->channel->crypt_in = make_shared<PSOV2Encryption>(cmd.server_key);
|
||||||
|
this->channel->crypt_out = make_shared<PSOV2Encryption>(cmd.client_key);
|
||||||
|
this->channel->send(0x02);
|
||||||
|
this->log.info_f("Enabled encryption");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x04: {
|
||||||
|
if (!msg.data.empty()) {
|
||||||
|
throw std::runtime_error("invalid login request command");
|
||||||
|
}
|
||||||
|
C_Login_Patch_04 cmd;
|
||||||
|
cmd.username.encode(this->username);
|
||||||
|
cmd.password.encode(this->password);
|
||||||
|
cmd.email_address.encode(this->email);
|
||||||
|
this->channel->send(0x04, 0x00, &cmd, sizeof(cmd));
|
||||||
|
this->log.info_f("Sent login credentials");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x05: {
|
||||||
|
this->log.info_f("Server sent disconnect command");
|
||||||
|
this->channel->disconnect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x06: {
|
||||||
|
if (this->current_file) {
|
||||||
|
throw std::runtime_error("protocol violation: previous file was not closed before open file command");
|
||||||
|
}
|
||||||
|
const auto& cmd = msg.check_size_t<S_OpenFile_Patch_06>();
|
||||||
|
this->current_file_bytes_remaining = cmd.size;
|
||||||
|
auto filename = this->resolve_filename(cmd.filename.decode());
|
||||||
|
this->current_file = phosg::fopen_unique(filename, "wb");
|
||||||
|
this->log.info_f("Opened file {}", filename);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x07: {
|
||||||
|
if (!this->current_file) {
|
||||||
|
throw std::runtime_error("protocol violation: no file is open; cannot write data");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& cmd = msg.check_size_t<S_WriteFileHeader_Patch_07>(0xFFFF);
|
||||||
|
const void* data = msg.data.data() + sizeof(cmd);
|
||||||
|
if (cmd.chunk_size > msg.data.size() - sizeof(cmd)) {
|
||||||
|
throw std::runtime_error("protocol violation: write command size is invalid");
|
||||||
|
}
|
||||||
|
if (cmd.chunk_size > this->current_file_bytes_remaining) {
|
||||||
|
throw std::runtime_error("protocol violation: chunk would exceed file size specified in open command");
|
||||||
|
}
|
||||||
|
if (phosg::crc32(data, cmd.chunk_size) != cmd.chunk_checksum) {
|
||||||
|
throw std::runtime_error("protocol violation: write command checksum is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
phosg::fwritex(this->current_file.get(), data, cmd.chunk_size);
|
||||||
|
this->current_file_bytes_remaining -= cmd.chunk_size;
|
||||||
|
this->log.info_f("Wrote {} to file", phosg::format_size(cmd.chunk_size));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x08: {
|
||||||
|
if (!this->current_file) {
|
||||||
|
throw std::runtime_error("protocol violation: no file is open; cannot close it");
|
||||||
|
}
|
||||||
|
this->current_file.reset();
|
||||||
|
this->log.info_f("Closed file");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x09: {
|
||||||
|
if (this->current_file) {
|
||||||
|
throw std::runtime_error("protocol violation: cannot enter directory with a file open");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& cmd = msg.check_size_t<S_EnterDirectory_Patch_09>();
|
||||||
|
string dirname = cmd.name.decode();
|
||||||
|
check_path_token(dirname);
|
||||||
|
this->dir_path.emplace_back(std::move(dirname));
|
||||||
|
std::filesystem::create_directories(this->resolve_filename(""));
|
||||||
|
this->log.info_f("Entered directory {}", dirname);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x0A: {
|
||||||
|
if (this->current_file) {
|
||||||
|
throw std::runtime_error("protocol violation: cannot exit directory with a file open");
|
||||||
|
}
|
||||||
|
if (this->dir_path.empty()) {
|
||||||
|
throw std::runtime_error("protocol violation: cannot exit directory with empty directory stack");
|
||||||
|
}
|
||||||
|
this->dir_path.pop_back();
|
||||||
|
this->log.info_f("Left directory");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x0B:
|
||||||
|
if (this->current_file) {
|
||||||
|
throw std::runtime_error("protocol violation: cannot start patch session when file is already open");
|
||||||
|
}
|
||||||
|
this->dir_path.clear();
|
||||||
|
this->log.info_f("Started patch session");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x0C: {
|
||||||
|
const auto& cmd = msg.check_size_t<S_FileChecksumRequest_Patch_0C>();
|
||||||
|
auto filename = this->resolve_filename(cmd.filename.decode());
|
||||||
|
uint32_t checksum = 0, size = 0;
|
||||||
|
try {
|
||||||
|
auto data = phosg::load_file(filename);
|
||||||
|
checksum = phosg::crc32(data.data(), data.size());
|
||||||
|
size = data.size();
|
||||||
|
} catch (const phosg::cannot_open_file&) {
|
||||||
|
}
|
||||||
|
this->pending_checksum_results.emplace_back(C_FileInformation_Patch_0F{cmd.request_id, checksum, size});
|
||||||
|
this->log.info_f("Checked file {}", filename);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x0D:
|
||||||
|
for (const auto& it : this->pending_checksum_results) {
|
||||||
|
this->channel->send(0x0F, 0x00, &it, sizeof(it));
|
||||||
|
}
|
||||||
|
this->pending_checksum_results.clear();
|
||||||
|
this->channel->send(0x10);
|
||||||
|
this->log.info_f("Sent all checksum results");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x11: {
|
||||||
|
const auto& cmd = msg.check_size_t<S_StartFileDownloads_Patch_11>();
|
||||||
|
this->log.info_f("{} files ({}) to download", cmd.num_files.load(), phosg::format_size(cmd.total_bytes));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x12:
|
||||||
|
this->log.info_f("Patch session succeeded");
|
||||||
|
this->channel->disconnect();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x13: {
|
||||||
|
phosg::strip_trailing_zeroes(msg.data);
|
||||||
|
if (msg.data.size() & 1) {
|
||||||
|
msg.data.push_back(0);
|
||||||
|
}
|
||||||
|
this->log.info_f("Message from server:\n{}", strip_color(tt_utf16_to_utf8(msg.data)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x14: {
|
||||||
|
const auto& cmd = msg.check_size_t<S_Reconnect_Patch_14>();
|
||||||
|
|
||||||
|
auto new_ep = make_endpoint_ipv4(cmd.address, cmd.port);
|
||||||
|
string netloc_str = str_for_endpoint(new_ep);
|
||||||
|
this->log.info_f("Connecting to {}", netloc_str);
|
||||||
|
auto sock = make_unique<asio::ip::tcp::socket>(co_await async_connect_tcp(new_ep));
|
||||||
|
|
||||||
|
auto old_channel = this->channel;
|
||||||
|
auto new_channel = SocketChannel::create(
|
||||||
|
this->io_context,
|
||||||
|
std::move(sock),
|
||||||
|
this->channel->version,
|
||||||
|
this->channel->language,
|
||||||
|
netloc_str,
|
||||||
|
this->channel->terminal_send_color,
|
||||||
|
this->channel->terminal_recv_color);
|
||||||
|
this->channel = new_channel;
|
||||||
|
old_channel->disconnect();
|
||||||
|
this->log.info_f("Server channel connected");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x15:
|
||||||
|
this->log.error_f("Server rejected login credentials");
|
||||||
|
this->channel->disconnect();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw std::runtime_error("invalid command");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <asio.hpp>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <phosg/Filesystem.hh>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Channel.hh"
|
||||||
|
#include "CommandFormats.hh"
|
||||||
|
#include "PSOEncryption.hh"
|
||||||
|
#include "PSOProtocol.hh"
|
||||||
|
|
||||||
|
class PatchDownloadSession {
|
||||||
|
public:
|
||||||
|
PatchDownloadSession(
|
||||||
|
std::shared_ptr<asio::io_context> io_context,
|
||||||
|
const std::string& remote_host,
|
||||||
|
uint16_t remote_port,
|
||||||
|
const std::string& output_dir,
|
||||||
|
Version version,
|
||||||
|
const std::string& username,
|
||||||
|
const std::string& password,
|
||||||
|
const std::string& email,
|
||||||
|
bool show_command_data);
|
||||||
|
PatchDownloadSession(const PatchDownloadSession&) = delete;
|
||||||
|
PatchDownloadSession(PatchDownloadSession&&) = delete;
|
||||||
|
PatchDownloadSession& operator=(const PatchDownloadSession&) = delete;
|
||||||
|
PatchDownloadSession& operator=(PatchDownloadSession&&) = delete;
|
||||||
|
virtual ~PatchDownloadSession() = default;
|
||||||
|
|
||||||
|
asio::awaitable<void> run();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Config (must be set by caller)
|
||||||
|
std::string remote_host;
|
||||||
|
uint16_t remote_port;
|
||||||
|
std::string output_dir;
|
||||||
|
Version version;
|
||||||
|
std::string username;
|
||||||
|
std::string password;
|
||||||
|
std::string email;
|
||||||
|
bool show_command_data;
|
||||||
|
|
||||||
|
// State (set during session)
|
||||||
|
phosg::PrefixedLogger log;
|
||||||
|
std::shared_ptr<asio::io_context> io_context;
|
||||||
|
std::shared_ptr<Channel> channel;
|
||||||
|
std::vector<std::string> dir_path;
|
||||||
|
std::unique_ptr<FILE, void (*)(FILE*)> current_file;
|
||||||
|
size_t current_file_bytes_remaining = 0;
|
||||||
|
std::vector<C_FileInformation_Patch_0F> pending_checksum_results;
|
||||||
|
|
||||||
|
static void check_path_token(const std::string& token);
|
||||||
|
std::string resolve_filename(const std::string& filename) const;
|
||||||
|
|
||||||
|
asio::awaitable<void> on_message(Channel::Message& msg);
|
||||||
|
};
|
||||||
@@ -109,8 +109,8 @@ PatchFileIndex::PatchFileIndex(const string& root_dir)
|
|||||||
if (!compute_crc32s_message.empty()) {
|
if (!compute_crc32s_message.empty()) {
|
||||||
auto data = f->load_data(); // Sets f->size
|
auto data = f->load_data(); // Sets f->size
|
||||||
f->crc32 = phosg::crc32(data->data(), f->size);
|
f->crc32 = phosg::crc32(data->data(), f->size);
|
||||||
for (size_t x = 0; x < data->size(); x += 0x4000) {
|
for (size_t x = 0; x < data->size(); x += this->CHUNK_SIZE) {
|
||||||
size_t chunk_bytes = min<size_t>(f->size - x, 0x4000);
|
size_t chunk_bytes = min<size_t>(f->size - x, this->CHUNK_SIZE);
|
||||||
f->chunk_crcs.emplace_back(phosg::crc32(data->data() + x, chunk_bytes));
|
f->chunk_crcs.emplace_back(phosg::crc32(data->data() + x, chunk_bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
struct PatchFileIndex {
|
struct PatchFileIndex {
|
||||||
|
static constexpr size_t CHUNK_SIZE = 0x6000;
|
||||||
|
|
||||||
explicit PatchFileIndex(const std::string& root_dir);
|
explicit PatchFileIndex(const std::string& root_dir);
|
||||||
|
|
||||||
struct File {
|
struct File {
|
||||||
|
|||||||
+10
-14
@@ -116,7 +116,7 @@ struct PlayerInventoryT {
|
|||||||
/* 0000 */ uint8_t num_items = 0;
|
/* 0000 */ uint8_t num_items = 0;
|
||||||
/* 0001 */ uint8_t hp_from_materials = 0;
|
/* 0001 */ uint8_t hp_from_materials = 0;
|
||||||
/* 0002 */ uint8_t tp_from_materials = 0;
|
/* 0002 */ uint8_t tp_from_materials = 0;
|
||||||
/* 0003 */ uint8_t language = 0;
|
/* 0003 */ Language language = Language::JAPANESE;
|
||||||
/* 0004 */ parray<PlayerInventoryItemT<BE>, 30> items;
|
/* 0004 */ parray<PlayerInventoryItemT<BE>, 30> items;
|
||||||
/* 034C */
|
/* 034C */
|
||||||
|
|
||||||
@@ -179,11 +179,11 @@ struct PlayerInventoryT {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void equip_item_id(uint32_t item_id, EquipSlot slot, bool allow_overwrite) {
|
void equip_item_id(uint32_t item_id, EquipSlot slot) {
|
||||||
this->equip_item_index(this->find_item(item_id), slot, allow_overwrite);
|
this->equip_item_index(this->find_item(item_id), slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
void equip_item_index(size_t index, EquipSlot slot, bool allow_overwrite) {
|
void equip_item_index(size_t index, EquipSlot slot) {
|
||||||
auto& item = this->items[index];
|
auto& item = this->items[index];
|
||||||
|
|
||||||
if ((slot == EquipSlot::UNKNOWN) || !item.data.can_be_equipped_in_slot(slot)) {
|
if ((slot == EquipSlot::UNKNOWN) || !item.data.can_be_equipped_in_slot(slot)) {
|
||||||
@@ -191,11 +191,7 @@ struct PlayerInventoryT {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this->has_equipped_item(slot)) {
|
if (this->has_equipped_item(slot)) {
|
||||||
if (allow_overwrite) {
|
this->unequip_item_slot(slot);
|
||||||
this->unequip_item_slot(slot);
|
|
||||||
} else {
|
|
||||||
throw std::runtime_error("equip slot is already in use");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item.flags |= 0x00000008;
|
item.flags |= 0x00000008;
|
||||||
@@ -269,14 +265,14 @@ struct PlayerInventoryT {
|
|||||||
// issue - its inventory format matches the rest of the versions.
|
// issue - its inventory format matches the rest of the versions.
|
||||||
this->hp_from_materials = 0;
|
this->hp_from_materials = 0;
|
||||||
this->tp_from_materials = 0;
|
this->tp_from_materials = 0;
|
||||||
this->language = 0;
|
this->language = Language::JAPANESE;
|
||||||
} else if ((v != Version::PC_NTE) && (v != Version::PC_V2)) {
|
} else if ((v != Version::PC_NTE) && (v != Version::PC_V2)) {
|
||||||
if (this->language > 4) {
|
if (static_cast<size_t>(this->language) > 4) {
|
||||||
this->language = 0;
|
this->language = Language::JAPANESE;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this->language > 7) {
|
if (static_cast<size_t>(this->language) > 7) {
|
||||||
this->language = 0;
|
this->language = Language::JAPANESE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ void GuildCardBB::clear() {
|
|||||||
this->team_name.clear();
|
this->team_name.clear();
|
||||||
this->description.clear();
|
this->description.clear();
|
||||||
this->present = 0;
|
this->present = 0;
|
||||||
this->language = 0;
|
this->language = Language::JAPANESE;
|
||||||
this->section_id = 0;
|
this->section_id = 0;
|
||||||
this->char_class = 0;
|
this->char_class = 0;
|
||||||
}
|
}
|
||||||
@@ -218,11 +218,11 @@ PlayerRecordsChallengeBB::PlayerRecordsChallengeBB(const PlayerRecordsChallengeD
|
|||||||
grave_x(rec.grave_x),
|
grave_x(rec.grave_x),
|
||||||
grave_y(rec.grave_y),
|
grave_y(rec.grave_y),
|
||||||
grave_z(rec.grave_z),
|
grave_z(rec.grave_z),
|
||||||
grave_team(rec.grave_team.decode(), 1),
|
grave_team(rec.grave_team.decode(), Language::ENGLISH),
|
||||||
grave_message(rec.grave_message.decode(), 1),
|
grave_message(rec.grave_message.decode(), Language::ENGLISH),
|
||||||
unknown_m5(0),
|
unknown_m5(0),
|
||||||
unknown_t6(0),
|
unknown_t6(0),
|
||||||
rank_title(rec.rank_title.decode(), 1),
|
rank_title(rec.rank_title.decode(), Language::ENGLISH),
|
||||||
unknown_l7(0) {}
|
unknown_l7(0) {}
|
||||||
|
|
||||||
PlayerRecordsChallengeBB::PlayerRecordsChallengeBB(const PlayerRecordsChallengePC& rec)
|
PlayerRecordsChallengeBB::PlayerRecordsChallengeBB(const PlayerRecordsChallengePC& rec)
|
||||||
@@ -242,11 +242,11 @@ PlayerRecordsChallengeBB::PlayerRecordsChallengeBB(const PlayerRecordsChallengeP
|
|||||||
grave_x(rec.grave_x),
|
grave_x(rec.grave_x),
|
||||||
grave_y(rec.grave_y),
|
grave_y(rec.grave_y),
|
||||||
grave_z(rec.grave_z),
|
grave_z(rec.grave_z),
|
||||||
grave_team(rec.grave_team.decode(), 1),
|
grave_team(rec.grave_team.decode(), Language::ENGLISH),
|
||||||
grave_message(rec.grave_message.decode(), 1),
|
grave_message(rec.grave_message.decode(), Language::ENGLISH),
|
||||||
unknown_m5(0),
|
unknown_m5(0),
|
||||||
unknown_t6(0),
|
unknown_t6(0),
|
||||||
rank_title(rec.rank_title.decode(), 1),
|
rank_title(rec.rank_title.decode(), Language::ENGLISH),
|
||||||
unknown_l7(0) {}
|
unknown_l7(0) {}
|
||||||
|
|
||||||
PlayerRecordsChallengeBB::operator PlayerRecordsChallengeDC() const {
|
PlayerRecordsChallengeBB::operator PlayerRecordsChallengeDC() const {
|
||||||
@@ -309,22 +309,7 @@ PlayerRecordsChallengeBB::operator PlayerRecordsChallengePC() const {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
QuestFlagsV1& QuestFlagsV1::operator=(const QuestFlags& other) {
|
const QuestFlagsForDifficulty BB_QUEST_FLAG_APPLY_MASK{{
|
||||||
this->data[0] = other.data[0];
|
|
||||||
this->data[1] = other.data[1];
|
|
||||||
this->data[2] = other.data[2];
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
QuestFlagsV1::operator QuestFlags() const {
|
|
||||||
QuestFlags ret;
|
|
||||||
ret.data[0] = this->data[0];
|
|
||||||
ret.data[1] = this->data[1];
|
|
||||||
ret.data[2] = this->data[2];
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QuestFlagsForDifficulty QuestFlagsForDifficulty::BB_QUEST_FLAG_APPLY_MASK{{
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
/* 0000 */ 0x00, 0x3F, 0xFF, 0xE3, 0xE0, 0xFF, 0xFF, 0x00,
|
/* 0000 */ 0x00, 0x3F, 0xFF, 0xE3, 0xE0, 0xFF, 0xFF, 0x00,
|
||||||
/* 0040 */ 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00,
|
/* 0040 */ 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00,
|
||||||
|
|||||||
+91
-59
@@ -328,7 +328,7 @@ struct PlayerDispDataDCPCV3T {
|
|||||||
this->visual.enforce_lobby_join_limits_for_version(v);
|
this->visual.enforce_lobby_join_limits_for_version(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerDispDataBB to_bb(uint8_t to_language, uint8_t from_language) const;
|
PlayerDispDataBB to_bb(Language to_language, Language from_language) const;
|
||||||
} __attribute__((packed));
|
} __attribute__((packed));
|
||||||
using PlayerDispDataDCPCV3 = PlayerDispDataDCPCV3T<false>;
|
using PlayerDispDataDCPCV3 = PlayerDispDataDCPCV3T<false>;
|
||||||
using PlayerDispDataDCPCV3BE = PlayerDispDataDCPCV3T<true>;
|
using PlayerDispDataDCPCV3BE = PlayerDispDataDCPCV3T<true>;
|
||||||
@@ -361,7 +361,7 @@ struct PlayerDispDataBB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <bool BE>
|
template <bool BE>
|
||||||
PlayerDispDataDCPCV3T<BE> to_dcpcv3(uint8_t to_language, uint8_t from_language) const {
|
PlayerDispDataDCPCV3T<BE> to_dcpcv3(Language to_language, Language from_language) const {
|
||||||
PlayerDispDataDCPCV3T<BE> ret;
|
PlayerDispDataDCPCV3T<BE> ret;
|
||||||
ret.stats = this->stats;
|
ret.stats = this->stats;
|
||||||
ret.visual = this->visual;
|
ret.visual = this->visual;
|
||||||
@@ -377,7 +377,7 @@ struct PlayerDispDataBB {
|
|||||||
} __packed_ws__(PlayerDispDataBB, 0x190);
|
} __packed_ws__(PlayerDispDataBB, 0x190);
|
||||||
|
|
||||||
template <bool BE>
|
template <bool BE>
|
||||||
PlayerDispDataBB PlayerDispDataDCPCV3T<BE>::to_bb(uint8_t to_language, uint8_t from_language) const {
|
PlayerDispDataBB PlayerDispDataDCPCV3T<BE>::to_bb(Language to_language, Language from_language) const {
|
||||||
PlayerDispDataBB bb;
|
PlayerDispDataBB bb;
|
||||||
bb.stats = this->stats;
|
bb.stats = this->stats;
|
||||||
bb.visual = this->visual;
|
bb.visual = this->visual;
|
||||||
@@ -398,7 +398,7 @@ struct GuildCardDCNTE {
|
|||||||
/* 20 */ pstring<TextEncoding::MARKED, 0x48> description;
|
/* 20 */ pstring<TextEncoding::MARKED, 0x48> description;
|
||||||
/* 68 */ parray<uint8_t, 0x0F> unused2;
|
/* 68 */ parray<uint8_t, 0x0F> unused2;
|
||||||
/* 77 */ uint8_t present = 0;
|
/* 77 */ uint8_t present = 0;
|
||||||
/* 78 */ uint8_t language = 0;
|
/* 78 */ Language language = Language::JAPANESE;
|
||||||
/* 79 */ uint8_t section_id = 0;
|
/* 79 */ uint8_t section_id = 0;
|
||||||
/* 7A */ uint8_t char_class = 0;
|
/* 7A */ uint8_t char_class = 0;
|
||||||
/* 7B */
|
/* 7B */
|
||||||
@@ -413,7 +413,7 @@ struct GuildCardDC {
|
|||||||
/* 20 */ pstring<TextEncoding::MARKED, 0x48> description;
|
/* 20 */ pstring<TextEncoding::MARKED, 0x48> description;
|
||||||
/* 68 */ parray<uint8_t, 0x11> unused2;
|
/* 68 */ parray<uint8_t, 0x11> unused2;
|
||||||
/* 79 */ uint8_t present = 0;
|
/* 79 */ uint8_t present = 0;
|
||||||
/* 7A */ uint8_t language = 0;
|
/* 7A */ Language language = Language::JAPANESE;
|
||||||
/* 7B */ uint8_t section_id = 0;
|
/* 7B */ uint8_t section_id = 0;
|
||||||
/* 7C */ uint8_t char_class = 0;
|
/* 7C */ uint8_t char_class = 0;
|
||||||
/* 7D */
|
/* 7D */
|
||||||
@@ -428,7 +428,7 @@ struct GuildCardPC {
|
|||||||
/* 08 */ pstring<TextEncoding::UTF16, 0x18> name;
|
/* 08 */ pstring<TextEncoding::UTF16, 0x18> name;
|
||||||
/* 38 */ pstring<TextEncoding::UTF16, 0x5A> description;
|
/* 38 */ pstring<TextEncoding::UTF16, 0x5A> description;
|
||||||
/* EC */ uint8_t present = 0;
|
/* EC */ uint8_t present = 0;
|
||||||
/* ED */ uint8_t language = 0;
|
/* ED */ Language language = Language::JAPANESE;
|
||||||
/* EE */ uint8_t section_id = 0;
|
/* EE */ uint8_t section_id = 0;
|
||||||
/* EF */ uint8_t char_class = 0;
|
/* EF */ uint8_t char_class = 0;
|
||||||
/* F0 */
|
/* F0 */
|
||||||
@@ -455,11 +455,11 @@ struct GuildCardGCT {
|
|||||||
/* 04:04 */ U32T<BE> guild_card_number = 0;
|
/* 04:04 */ U32T<BE> guild_card_number = 0;
|
||||||
/* 08:08 */ pstring<TextEncoding::ASCII, 0x18> name;
|
/* 08:08 */ pstring<TextEncoding::ASCII, 0x18> name;
|
||||||
/* 20:20 */ pstring<TextEncoding::MARKED, DescriptionLength> description;
|
/* 20:20 */ pstring<TextEncoding::MARKED, DescriptionLength> description;
|
||||||
/* 8C:8C */ uint8_t present = 0;
|
/* A0:8C */ uint8_t present = 0;
|
||||||
/* 8D:8D */ uint8_t language = 0;
|
/* A1:8D */ Language language = Language::JAPANESE;
|
||||||
/* 8E:8E */ uint8_t section_id = 0;
|
/* A2:8E */ uint8_t section_id = 0;
|
||||||
/* 8F:8F */ uint8_t char_class = 0;
|
/* A3:8F */ uint8_t char_class = 0;
|
||||||
/* 90:90 */
|
/* A4:90 */
|
||||||
|
|
||||||
operator GuildCardBB() const;
|
operator GuildCardBB() const;
|
||||||
} __attribute__((packed));
|
} __attribute__((packed));
|
||||||
@@ -480,7 +480,7 @@ struct GuildCardXB {
|
|||||||
/* 0010 */ pstring<TextEncoding::ASCII, 0x18> name;
|
/* 0010 */ pstring<TextEncoding::ASCII, 0x18> name;
|
||||||
/* 0028 */ pstring<TextEncoding::MARKED, 0x200> description;
|
/* 0028 */ pstring<TextEncoding::MARKED, 0x200> description;
|
||||||
/* 0228 */ uint8_t present = 0;
|
/* 0228 */ uint8_t present = 0;
|
||||||
/* 0229 */ uint8_t language = 0;
|
/* 0229 */ Language language = Language::JAPANESE;
|
||||||
/* 022A */ uint8_t section_id = 0;
|
/* 022A */ uint8_t section_id = 0;
|
||||||
/* 022B */ uint8_t char_class = 0;
|
/* 022B */ uint8_t char_class = 0;
|
||||||
/* 022C */
|
/* 022C */
|
||||||
@@ -494,7 +494,7 @@ struct GuildCardBB {
|
|||||||
/* 0034 */ pstring<TextEncoding::UTF16_ALWAYS_MARKED, 0x10> team_name;
|
/* 0034 */ pstring<TextEncoding::UTF16_ALWAYS_MARKED, 0x10> team_name;
|
||||||
/* 0054 */ pstring<TextEncoding::UTF16, 0x58> description;
|
/* 0054 */ pstring<TextEncoding::UTF16, 0x58> description;
|
||||||
/* 0104 */ uint8_t present = 0;
|
/* 0104 */ uint8_t present = 0;
|
||||||
/* 0105 */ uint8_t language = 0;
|
/* 0105 */ Language language = Language::JAPANESE;
|
||||||
/* 0106 */ uint8_t section_id = 0;
|
/* 0106 */ uint8_t section_id = 0;
|
||||||
/* 0107 */ uint8_t char_class = 0;
|
/* 0107 */ uint8_t char_class = 0;
|
||||||
/* 0108 */
|
/* 0108 */
|
||||||
@@ -776,13 +776,13 @@ struct PlayerRecordsChallengeBB {
|
|||||||
grave_x(rec.grave_x),
|
grave_x(rec.grave_x),
|
||||||
grave_y(rec.grave_y),
|
grave_y(rec.grave_y),
|
||||||
grave_z(rec.grave_z),
|
grave_z(rec.grave_z),
|
||||||
grave_team(rec.grave_team.decode(), 1),
|
grave_team(rec.grave_team.decode(), Language::ENGLISH),
|
||||||
grave_message(rec.grave_message.decode(), 1),
|
grave_message(rec.grave_message.decode(), Language::ENGLISH),
|
||||||
unknown_m5(rec.unknown_m5),
|
unknown_m5(rec.unknown_m5),
|
||||||
ep1_online_award_state(rec.ep1_online_award_state),
|
ep1_online_award_state(rec.ep1_online_award_state),
|
||||||
ep2_online_award_state(rec.ep2_online_award_state),
|
ep2_online_award_state(rec.ep2_online_award_state),
|
||||||
ep1_offline_award_state(rec.ep1_offline_award_state),
|
ep1_offline_award_state(rec.ep1_offline_award_state),
|
||||||
rank_title(rec.rank_title.decode(), 1),
|
rank_title(rec.rank_title.decode(), Language::ENGLISH),
|
||||||
unknown_l7(rec.unknown_l7) {
|
unknown_l7(rec.unknown_l7) {
|
||||||
for (size_t z = 0; z < std::min<size_t>(this->unknown_t6.size(), rec.unknown_t6.size()); z++) {
|
for (size_t z = 0; z < std::min<size_t>(this->unknown_t6.size(), rec.unknown_t6.size()); z++) {
|
||||||
this->unknown_t6[z] = rec.unknown_t6[z];
|
this->unknown_t6[z] = rec.unknown_t6[z];
|
||||||
@@ -810,8 +810,8 @@ struct PlayerRecordsChallengeBB {
|
|||||||
ret.grave_x = this->grave_x;
|
ret.grave_x = this->grave_x;
|
||||||
ret.grave_y = this->grave_y;
|
ret.grave_y = this->grave_y;
|
||||||
ret.grave_z = this->grave_z;
|
ret.grave_z = this->grave_z;
|
||||||
ret.grave_team.encode(this->grave_team.decode(), 1);
|
ret.grave_team.encode(this->grave_team.decode(), Language::ENGLISH);
|
||||||
ret.grave_message.encode(this->grave_message.decode(), 1);
|
ret.grave_message.encode(this->grave_message.decode(), Language::ENGLISH);
|
||||||
ret.unknown_m5 = this->unknown_m5;
|
ret.unknown_m5 = this->unknown_m5;
|
||||||
for (size_t z = 0; z < std::min<size_t>(ret.unknown_t6.size(), this->unknown_t6.size()); z++) {
|
for (size_t z = 0; z < std::min<size_t>(ret.unknown_t6.size(), this->unknown_t6.size()); z++) {
|
||||||
ret.unknown_t6[z] = this->unknown_t6[z];
|
ret.unknown_t6[z] = this->unknown_t6[z];
|
||||||
@@ -819,7 +819,7 @@ struct PlayerRecordsChallengeBB {
|
|||||||
ret.ep1_online_award_state = this->ep1_online_award_state;
|
ret.ep1_online_award_state = this->ep1_online_award_state;
|
||||||
ret.ep2_online_award_state = this->ep2_online_award_state;
|
ret.ep2_online_award_state = this->ep2_online_award_state;
|
||||||
ret.ep1_offline_award_state = this->ep1_offline_award_state;
|
ret.ep1_offline_award_state = this->ep1_offline_award_state;
|
||||||
ret.rank_title.encode(this->rank_title.decode(), 1);
|
ret.rank_title.encode(this->rank_title.decode(), Language::ENGLISH);
|
||||||
ret.unknown_l7 = this->unknown_l7;
|
ret.unknown_l7 = this->unknown_l7;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -856,38 +856,53 @@ check_struct_size(PlayerRecordsBattle, 0x18);
|
|||||||
check_struct_size(PlayerRecordsBattleBE, 0x18);
|
check_struct_size(PlayerRecordsBattleBE, 0x18);
|
||||||
|
|
||||||
template <typename DestT, typename SrcT = DestT>
|
template <typename DestT, typename SrcT = DestT>
|
||||||
DestT convert_player_disp_data(const SrcT&, uint8_t, uint8_t) {
|
DestT convert_player_disp_data(const SrcT&, Language, Language) {
|
||||||
static_assert(phosg::always_false<DestT, SrcT>::v,
|
static_assert(phosg::always_false<DestT, SrcT>::v,
|
||||||
"unspecialized convert_player_disp_data should never be called");
|
"unspecialized convert_player_disp_data should never be called");
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3>(const PlayerDispDataDCPCV3& src, uint8_t, uint8_t) {
|
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3>(const PlayerDispDataDCPCV3& src, Language, Language) {
|
||||||
return src;
|
return src;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(
|
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(
|
||||||
const PlayerDispDataBB& src, uint8_t to_language, uint8_t from_language) {
|
const PlayerDispDataBB& src, Language to_language, Language from_language) {
|
||||||
return src.to_dcpcv3<false>(to_language, from_language);
|
return src.to_dcpcv3<false>(to_language, from_language);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB, PlayerDispDataDCPCV3>(
|
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB, PlayerDispDataDCPCV3>(
|
||||||
const PlayerDispDataDCPCV3& src, uint8_t to_language, uint8_t from_language) {
|
const PlayerDispDataDCPCV3& src, Language to_language, Language from_language) {
|
||||||
return src.to_bb(to_language, from_language);
|
return src.to_bb(to_language, from_language);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
|
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
|
||||||
const PlayerDispDataBB& src, uint8_t, uint8_t) {
|
const PlayerDispDataBB& src, Language, Language) {
|
||||||
return src;
|
return src;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct QuestFlagsForDifficulty {
|
template <size_t NumFlags>
|
||||||
static const QuestFlagsForDifficulty BB_QUEST_FLAG_APPLY_MASK;
|
struct FlagsArray {
|
||||||
|
parray<uint8_t, (NumFlags >> 3)> data = 0;
|
||||||
|
|
||||||
parray<uint8_t, 0x80> data;
|
FlagsArray() = default;
|
||||||
|
FlagsArray(const FlagsArray& other) = default;
|
||||||
|
FlagsArray(FlagsArray&& other) = default;
|
||||||
|
FlagsArray& operator=(const FlagsArray& other) = default;
|
||||||
|
FlagsArray& operator=(FlagsArray&& other) = default;
|
||||||
|
|
||||||
|
FlagsArray(std::initializer_list<uint8_t> init_items) : data(init_items) {}
|
||||||
|
|
||||||
|
template <size_t OtherNumFlags>
|
||||||
|
explicit FlagsArray(const FlagsArray<OtherNumFlags>& other) : data(other.data) {}
|
||||||
|
template <size_t OtherNumFlags>
|
||||||
|
FlagsArray& operator=(const FlagsArray<OtherNumFlags>& other) {
|
||||||
|
this->data = other.data;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool get(uint16_t flag_index) const {
|
inline bool get(uint16_t flag_index) const {
|
||||||
size_t byte_index = flag_index >> 3;
|
size_t byte_index = flag_index >> 3;
|
||||||
@@ -904,6 +919,7 @@ struct QuestFlagsForDifficulty {
|
|||||||
uint8_t mask = 0x80 >> (flag_index & 7);
|
uint8_t mask = 0x80 >> (flag_index & 7);
|
||||||
this->data[byte_index] &= (~mask);
|
this->data[byte_index] &= (~mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void update_all(bool set) {
|
inline void update_all(bool set) {
|
||||||
if (set) {
|
if (set) {
|
||||||
this->data.clear(0xFF);
|
this->data.clear(0xFF);
|
||||||
@@ -911,50 +927,66 @@ struct QuestFlagsForDifficulty {
|
|||||||
this->data.clear(0x00);
|
this->data.clear(0x00);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} __packed_ws__(QuestFlagsForDifficulty, 0x80);
|
} __attribute__((packed));
|
||||||
|
|
||||||
struct QuestFlags {
|
template <size_t NumFlagsPerTable, size_t NumTables, typename TableIndexT = size_t>
|
||||||
parray<QuestFlagsForDifficulty, 4> data;
|
struct FlagsTable {
|
||||||
|
parray<FlagsArray<NumFlagsPerTable>, NumTables> data;
|
||||||
|
|
||||||
inline bool get(uint8_t difficulty, uint16_t flag_index) const {
|
FlagsTable() = default;
|
||||||
return this->data[difficulty].get(flag_index);
|
FlagsTable(const FlagsTable& other) = default;
|
||||||
|
FlagsTable(FlagsTable&& other) = default;
|
||||||
|
FlagsTable& operator=(const FlagsTable& other) = default;
|
||||||
|
FlagsTable& operator=(FlagsTable&& other) = default;
|
||||||
|
|
||||||
|
template <size_t OtherNumFlagsPerTable, size_t OtherNumTables>
|
||||||
|
explicit FlagsTable(const FlagsTable<OtherNumFlagsPerTable, OtherNumTables, TableIndexT>& other) : data(other.data) {}
|
||||||
|
template <size_t OtherNumFlagsPerTable, size_t OtherNumTables>
|
||||||
|
FlagsTable& operator=(const FlagsTable<OtherNumFlagsPerTable, OtherNumTables, TableIndexT>& other) {
|
||||||
|
this->data = other.data;
|
||||||
|
return *this;
|
||||||
}
|
}
|
||||||
inline void set(uint8_t difficulty, uint16_t flag_index) {
|
|
||||||
this->data[difficulty].set(flag_index);
|
inline FlagsArray<NumFlagsPerTable>& array(TableIndexT which) {
|
||||||
|
return this->data[static_cast<size_t>(which)];
|
||||||
}
|
}
|
||||||
inline void clear(uint8_t difficulty, uint16_t flag_index) {
|
inline const FlagsArray<NumFlagsPerTable>& array(TableIndexT which) const {
|
||||||
this->data[difficulty].clear(flag_index);
|
return this->data[static_cast<size_t>(which)];
|
||||||
}
|
}
|
||||||
inline void update_all(uint8_t difficulty, bool set) {
|
|
||||||
this->data[difficulty].update_all(set);
|
inline bool get(TableIndexT array_index, size_t flag_index) const {
|
||||||
|
return this->array(array_index).get(flag_index);
|
||||||
|
}
|
||||||
|
inline void set(TableIndexT array_index, size_t flag_index) {
|
||||||
|
this->array(array_index).set(flag_index);
|
||||||
|
}
|
||||||
|
inline void clear(TableIndexT array_index, size_t flag_index) {
|
||||||
|
this->array(array_index).clear(flag_index);
|
||||||
|
}
|
||||||
|
inline void update_all(TableIndexT array_index, bool set) {
|
||||||
|
this->array(array_index).update_all(set);
|
||||||
}
|
}
|
||||||
inline void update_all(bool set) {
|
inline void update_all(bool set) {
|
||||||
for (size_t z = 0; z < 4; z++) {
|
for (size_t z = 0; z < this->data.size(); z++) {
|
||||||
this->update_all(z, set);
|
this->update_all(z, set);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} __packed_ws__(QuestFlags, 0x200);
|
} __attribute__((packed));
|
||||||
|
|
||||||
struct QuestFlagsV1 {
|
using QuestFlagsForDifficulty = FlagsArray<0x400>;
|
||||||
parray<QuestFlagsForDifficulty, 3> data;
|
using QuestFlagsV1 = FlagsTable<0x400, 3, Difficulty>;
|
||||||
|
using QuestFlags = FlagsTable<0x400, 4, Difficulty>;
|
||||||
|
using Ep3SeqVars = FlagsArray<0x2000>;
|
||||||
|
using SwitchFlagsV1 = FlagsTable<0x100, 0x10>;
|
||||||
|
using SwitchFlags = FlagsTable<0x100, 0x12>;
|
||||||
|
static_assert(sizeof(QuestFlagsForDifficulty) == 0x80);
|
||||||
|
static_assert(sizeof(QuestFlagsV1) == 0x180);
|
||||||
|
static_assert(sizeof(QuestFlags) == 0x200);
|
||||||
|
static_assert(sizeof(Ep3SeqVars) == 0x400);
|
||||||
|
static_assert(sizeof(SwitchFlagsV1) == 0x200);
|
||||||
|
static_assert(sizeof(SwitchFlags) == 0x240);
|
||||||
|
|
||||||
QuestFlagsV1& operator=(const QuestFlags& other);
|
extern const QuestFlagsForDifficulty BB_QUEST_FLAG_APPLY_MASK;
|
||||||
operator QuestFlags() const;
|
|
||||||
} __packed_ws__(QuestFlagsV1, 0x180);
|
|
||||||
|
|
||||||
struct SwitchFlags {
|
|
||||||
parray<parray<uint8_t, 0x20>, 0x12> data;
|
|
||||||
|
|
||||||
inline bool get(uint8_t floor, uint16_t flag_num) const {
|
|
||||||
return this->data[floor][flag_num >> 3] & (0x80 >> (flag_num & 7));
|
|
||||||
}
|
|
||||||
inline void set(uint8_t floor, uint16_t flag_num) {
|
|
||||||
this->data[floor][flag_num >> 3] |= (0x80 >> (flag_num & 7));
|
|
||||||
}
|
|
||||||
inline void clear(uint8_t floor, uint16_t flag_num) {
|
|
||||||
this->data[floor][flag_num >> 3] &= ~(0x80 >> (flag_num & 7));
|
|
||||||
}
|
|
||||||
} __packed_ws__(SwitchFlags, 0x240);
|
|
||||||
|
|
||||||
struct BattleRules {
|
struct BattleRules {
|
||||||
enum class TechDiskMode : uint8_t {
|
enum class TechDiskMode : uint8_t {
|
||||||
|
|||||||
+34
-11
@@ -523,6 +523,12 @@ constexpr on_message_t S_P_81 = &S_81<SC_SimpleMail_PC_81>;
|
|||||||
constexpr on_message_t S_B_81 = &S_81<SC_SimpleMail_BB_81>;
|
constexpr on_message_t S_B_81 = &S_81<SC_SimpleMail_BB_81>;
|
||||||
|
|
||||||
static asio::awaitable<HandlerResult> S_88(shared_ptr<Client> c, Channel::Message& msg) {
|
static asio::awaitable<HandlerResult> S_88(shared_ptr<Client> c, Channel::Message& msg) {
|
||||||
|
// If the client isn't in the lobby, suppress the command (Ep3 can crash if
|
||||||
|
// it receives this while loading; other versions probably also will crash)
|
||||||
|
if (!c->proxy_session->is_in_lobby) {
|
||||||
|
co_return HandlerResult::SUPPRESS;
|
||||||
|
}
|
||||||
|
|
||||||
bool modified = false;
|
bool modified = false;
|
||||||
if (c->login && c->login->account->account_id != c->proxy_session->remote_guild_card_number) {
|
if (c->login && c->login->account->account_id != c->proxy_session->remote_guild_card_number) {
|
||||||
size_t expected_size = sizeof(S_ArrowUpdateEntry_88) * msg.flag;
|
size_t expected_size = sizeof(S_ArrowUpdateEntry_88) * msg.flag;
|
||||||
@@ -866,7 +872,13 @@ static asio::awaitable<HandlerResult> SC_6x60_6xA2(shared_ptr<Client> c, Channel
|
|||||||
|
|
||||||
G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(msg.data.data(), msg.data.size());
|
G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(msg.data.data(), msg.data.size());
|
||||||
auto rec = reconcile_drop_request_with_map(
|
auto rec = reconcile_drop_request_with_map(
|
||||||
c, cmd, c->proxy_session->lobby_episode, c->proxy_session->lobby_event, c->proxy_session->map_state, false);
|
c,
|
||||||
|
cmd,
|
||||||
|
c->proxy_session->lobby_episode,
|
||||||
|
c->proxy_session->lobby_difficulty,
|
||||||
|
c->proxy_session->lobby_event,
|
||||||
|
c->proxy_session->map_state,
|
||||||
|
false);
|
||||||
|
|
||||||
ItemCreator::DropResult res;
|
ItemCreator::DropResult res;
|
||||||
if (rec.obj_st) {
|
if (rec.obj_st) {
|
||||||
@@ -993,7 +1005,7 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
|
|||||||
if (c->proxy_session->map_state) {
|
if (c->proxy_session->map_state) {
|
||||||
shared_ptr<MapState::ObjectState> obj_st;
|
shared_ptr<MapState::ObjectState> obj_st;
|
||||||
try {
|
try {
|
||||||
obj_st = c->proxy_session->map_state->object_state_for_index(c->version(), c->floor, cmd.header.entity_id - 0x4000);
|
obj_st = c->proxy_session->map_state->object_state_for_index(c->version(), cmd.header.entity_id - 0x4000);
|
||||||
} catch (const exception& e) {
|
} catch (const exception& e) {
|
||||||
c->log.warning_f("Invalid object reference ({})", e.what());
|
c->log.warning_f("Invalid object reference ({})", e.what());
|
||||||
}
|
}
|
||||||
@@ -1499,10 +1511,11 @@ template <typename CmdT>
|
|||||||
static asio::awaitable<HandlerResult> S_65_67_68_EB(shared_ptr<Client> c, Channel::Message& msg) {
|
static asio::awaitable<HandlerResult> S_65_67_68_EB(shared_ptr<Client> c, Channel::Message& msg) {
|
||||||
if (msg.command == 0x67) {
|
if (msg.command == 0x67) {
|
||||||
c->proxy_session->clear_lobby_players(12);
|
c->proxy_session->clear_lobby_players(12);
|
||||||
|
c->proxy_session->is_in_lobby = true;
|
||||||
c->proxy_session->is_in_game = false;
|
c->proxy_session->is_in_game = false;
|
||||||
c->proxy_session->is_in_quest = false;
|
c->proxy_session->is_in_quest = false;
|
||||||
c->floor = 0x0F;
|
c->floor = 0x0F;
|
||||||
c->proxy_session->lobby_difficulty = 0;
|
c->proxy_session->lobby_difficulty = Difficulty::NORMAL;
|
||||||
c->proxy_session->lobby_section_id = 0;
|
c->proxy_session->lobby_section_id = 0;
|
||||||
c->proxy_session->lobby_mode = GameMode::NORMAL;
|
c->proxy_session->lobby_mode = GameMode::NORMAL;
|
||||||
c->proxy_session->lobby_episode = Episode::EP1;
|
c->proxy_session->lobby_episode = Episode::EP1;
|
||||||
@@ -1642,9 +1655,10 @@ static asio::awaitable<HandlerResult> S_64(shared_ptr<Client> c, Channel::Messag
|
|||||||
|
|
||||||
c->proxy_session->clear_lobby_players(4);
|
c->proxy_session->clear_lobby_players(4);
|
||||||
c->floor = 0;
|
c->floor = 0;
|
||||||
|
c->proxy_session->is_in_lobby = false;
|
||||||
c->proxy_session->is_in_game = true;
|
c->proxy_session->is_in_game = true;
|
||||||
c->proxy_session->is_in_quest = false;
|
c->proxy_session->is_in_quest = false;
|
||||||
if constexpr (sizeof(cmd) > sizeof(S_JoinGame_DCNTE_64)) {
|
if constexpr (sizeof(*cmd) > sizeof(S_JoinGame_DCNTE_64)) {
|
||||||
c->proxy_session->lobby_event = cmd->event;
|
c->proxy_session->lobby_event = cmd->event;
|
||||||
c->proxy_session->lobby_difficulty = cmd->difficulty;
|
c->proxy_session->lobby_difficulty = cmd->difficulty;
|
||||||
c->proxy_session->lobby_section_id = cmd->section_id;
|
c->proxy_session->lobby_section_id = cmd->section_id;
|
||||||
@@ -1674,7 +1688,7 @@ static asio::awaitable<HandlerResult> S_64(shared_ptr<Client> c, Channel::Messag
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
c->proxy_session->lobby_event = 0;
|
c->proxy_session->lobby_event = 0;
|
||||||
c->proxy_session->lobby_difficulty = 0;
|
c->proxy_session->lobby_difficulty = Difficulty::NORMAL;
|
||||||
c->proxy_session->lobby_section_id = c->character_file()->disp.visual.section_id;
|
c->proxy_session->lobby_section_id = c->character_file()->disp.visual.section_id;
|
||||||
c->proxy_session->lobby_mode = GameMode::NORMAL;
|
c->proxy_session->lobby_mode = GameMode::NORMAL;
|
||||||
c->proxy_session->lobby_random_seed = phosg::random_object<uint32_t>();
|
c->proxy_session->lobby_random_seed = phosg::random_object<uint32_t>();
|
||||||
@@ -1746,10 +1760,11 @@ static asio::awaitable<HandlerResult> S_E8(shared_ptr<Client> c, Channel::Messag
|
|||||||
auto& cmd = msg.check_size_t<S_JoinSpectatorTeam_Ep3_E8>();
|
auto& cmd = msg.check_size_t<S_JoinSpectatorTeam_Ep3_E8>();
|
||||||
|
|
||||||
c->floor = 0;
|
c->floor = 0;
|
||||||
|
c->proxy_session->is_in_lobby = false;
|
||||||
c->proxy_session->is_in_game = true;
|
c->proxy_session->is_in_game = true;
|
||||||
c->proxy_session->is_in_quest = false;
|
c->proxy_session->is_in_quest = false;
|
||||||
c->proxy_session->lobby_event = cmd.event;
|
c->proxy_session->lobby_event = cmd.event;
|
||||||
c->proxy_session->lobby_difficulty = 0;
|
c->proxy_session->lobby_difficulty = Difficulty::NORMAL;
|
||||||
c->proxy_session->lobby_section_id = cmd.section_id;
|
c->proxy_session->lobby_section_id = cmd.section_id;
|
||||||
c->proxy_session->lobby_mode = GameMode::NORMAL;
|
c->proxy_session->lobby_mode = GameMode::NORMAL;
|
||||||
c->proxy_session->lobby_random_seed = 0;
|
c->proxy_session->lobby_random_seed = 0;
|
||||||
@@ -1835,10 +1850,11 @@ static asio::awaitable<HandlerResult> S_66_69_E9(shared_ptr<Client> c, Channel::
|
|||||||
|
|
||||||
static asio::awaitable<HandlerResult> C_98(shared_ptr<Client> c, Channel::Message& msg) {
|
static asio::awaitable<HandlerResult> C_98(shared_ptr<Client> c, Channel::Message& msg) {
|
||||||
c->floor = 0x0F;
|
c->floor = 0x0F;
|
||||||
|
c->proxy_session->is_in_lobby = false;
|
||||||
c->proxy_session->is_in_game = false;
|
c->proxy_session->is_in_game = false;
|
||||||
c->proxy_session->is_in_quest = false;
|
c->proxy_session->is_in_quest = false;
|
||||||
c->proxy_session->lobby_event = 0;
|
c->proxy_session->lobby_event = 0;
|
||||||
c->proxy_session->lobby_difficulty = 0;
|
c->proxy_session->lobby_difficulty = Difficulty::NORMAL;
|
||||||
c->proxy_session->lobby_section_id = 0;
|
c->proxy_session->lobby_section_id = 0;
|
||||||
c->proxy_session->lobby_episode = Episode::EP1;
|
c->proxy_session->lobby_episode = Episode::EP1;
|
||||||
c->proxy_session->lobby_mode = GameMode::NORMAL;
|
c->proxy_session->lobby_mode = GameMode::NORMAL;
|
||||||
@@ -2007,7 +2023,7 @@ asio::awaitable<HandlerResult> C_6x(shared_ptr<Client> c, Channel::Message& msg)
|
|||||||
case 0x4B:
|
case 0x4B:
|
||||||
case 0x4C:
|
case 0x4C:
|
||||||
if (c->check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
if (c->check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
||||||
send_change_player_hp(c->channel, c->lobby_client_id, PlayerHPChange::MAXIMIZE_HP, 0);
|
co_await send_change_player_hp(c, c->lobby_client_id, PlayerHPChange::MAXIMIZE_HP, 0);
|
||||||
send_change_player_hp(c->proxy_session->server_channel, c->lobby_client_id, PlayerHPChange::MAXIMIZE_HP, 0);
|
send_change_player_hp(c->proxy_session->server_channel, c->lobby_client_id, PlayerHPChange::MAXIMIZE_HP, 0);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2020,13 +2036,20 @@ asio::awaitable<HandlerResult> C_6x(shared_ptr<Client> c, Channel::Message& msg)
|
|||||||
c->pos = msg.check_size_t<G_SetPosition_6x3F>().pos;
|
c->pos = msg.check_size_t<G_SetPosition_6x3F>().pos;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 0x40:
|
case 0x40: {
|
||||||
c->pos = msg.check_size_t<G_WalkToPosition_6x40>().pos;
|
const auto& cmd = msg.check_size_t<G_WalkToPosition_6x40>();
|
||||||
|
c->pos.x = cmd.pos.x;
|
||||||
|
c->pos.z = cmd.pos.z;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 0x41:
|
case 0x41:
|
||||||
c->pos = msg.check_size_t<G_MoveToPosition_6x41_6x42>().pos;
|
case 0x42: {
|
||||||
|
const auto& cmd = msg.check_size_t<G_MoveToPosition_6x41_6x42>();
|
||||||
|
c->pos.x = cmd.pos.x;
|
||||||
|
c->pos.z = cmd.pos.z;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 0x48:
|
case 0x48:
|
||||||
if (!is_v1(c->version()) && c->check_flag(Client::Flag::INFINITE_TP_ENABLED)) {
|
if (!is_v1(c->version()) && c->check_flag(Client::Flag::INFINITE_TP_ENABLED)) {
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ void ProxySession::set_drop_mode(
|
|||||||
s->rare_item_set(version, nullptr),
|
s->rare_item_set(version, nullptr),
|
||||||
s->armor_random_set,
|
s->armor_random_set,
|
||||||
s->tool_random_set,
|
s->tool_random_set,
|
||||||
s->weapon_random_sets.at(this->lobby_difficulty),
|
s->weapon_random_set(this->lobby_difficulty),
|
||||||
s->tekker_adjustment_set,
|
s->tekker_adjustment_set,
|
||||||
s->item_parameter_table(version),
|
s->item_parameter_table(version),
|
||||||
s->item_stack_limits(version),
|
s->item_stack_limits(version),
|
||||||
|
|||||||
+3
-2
@@ -28,16 +28,17 @@ struct ProxySession {
|
|||||||
uint32_t guild_card_number = 0;
|
uint32_t guild_card_number = 0;
|
||||||
uint64_t xb_user_id = 0;
|
uint64_t xb_user_id = 0;
|
||||||
std::string name;
|
std::string name;
|
||||||
uint8_t language = 0;
|
Language language = Language::JAPANESE;
|
||||||
uint8_t section_id = 0;
|
uint8_t section_id = 0;
|
||||||
uint8_t char_class = 0;
|
uint8_t char_class = 0;
|
||||||
};
|
};
|
||||||
std::vector<LobbyPlayer> lobby_players;
|
std::vector<LobbyPlayer> lobby_players;
|
||||||
|
bool is_in_lobby = false;
|
||||||
bool is_in_game = false;
|
bool is_in_game = false;
|
||||||
bool is_in_quest = false;
|
bool is_in_quest = false;
|
||||||
uint8_t leader_client_id = 0;
|
uint8_t leader_client_id = 0;
|
||||||
uint8_t lobby_event = 0;
|
uint8_t lobby_event = 0;
|
||||||
uint8_t lobby_difficulty = 0;
|
Difficulty lobby_difficulty = Difficulty::NORMAL;
|
||||||
uint8_t lobby_section_id = 0;
|
uint8_t lobby_section_id = 0;
|
||||||
GameMode lobby_mode = GameMode::NORMAL;
|
GameMode lobby_mode = GameMode::NORMAL;
|
||||||
Episode lobby_episode = Episode::EP1;
|
Episode lobby_episode = Episode::EP1;
|
||||||
|
|||||||
+62
-30
@@ -203,7 +203,7 @@ void VersionedQuest::assert_valid() const {
|
|||||||
if (this->version == Version::UNKNOWN) {
|
if (this->version == Version::UNKNOWN) {
|
||||||
throw runtime_error("version is not set");
|
throw runtime_error("version is not set");
|
||||||
}
|
}
|
||||||
if (this->language == 0xFF) {
|
if (this->language == Language::UNKNOWN) {
|
||||||
throw runtime_error("language is not set");
|
throw runtime_error("language is not set");
|
||||||
}
|
}
|
||||||
switch (this->meta.episode) {
|
switch (this->meta.episode) {
|
||||||
@@ -290,7 +290,7 @@ string VersionedQuest::pvr_filename() const {
|
|||||||
|
|
||||||
string VersionedQuest::xb_filename() const {
|
string VersionedQuest::xb_filename() const {
|
||||||
return std::format("quest{}_{}.dat",
|
return std::format("quest{}_{}.dat",
|
||||||
this->meta.quest_number, static_cast<char>(tolower(char_for_language_code(this->language))));
|
this->meta.quest_number, static_cast<char>(tolower(char_for_language(this->language))));
|
||||||
}
|
}
|
||||||
|
|
||||||
string VersionedQuest::encode_qst() const {
|
string VersionedQuest::encode_qst() const {
|
||||||
@@ -301,7 +301,7 @@ string VersionedQuest::encode_qst() const {
|
|||||||
files.emplace(std::format("quest{}.pvr", this->meta.quest_number), this->pvr_contents);
|
files.emplace(std::format("quest{}.pvr", this->meta.quest_number), this->pvr_contents);
|
||||||
}
|
}
|
||||||
string xb_filename = std::format("quest{}_{}.dat",
|
string xb_filename = std::format("quest{}_{}.dat",
|
||||||
this->meta.quest_number, static_cast<char>(tolower(char_for_language_code(language))));
|
this->meta.quest_number, static_cast<char>(tolower(char_for_language(language))));
|
||||||
return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->version, this->is_dlq_encoded);
|
return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->version, this->is_dlq_encoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +315,7 @@ phosg::JSON Quest::json() const {
|
|||||||
for (const auto& [_, vq] : this->versions) {
|
for (const auto& [_, vq] : this->versions) {
|
||||||
versions_json.emplace_back(phosg::JSON::dict({
|
versions_json.emplace_back(phosg::JSON::dict({
|
||||||
{"Version", phosg::name_for_enum(vq->version)},
|
{"Version", phosg::name_for_enum(vq->version)},
|
||||||
{"Language", name_for_language_code(vq->language)},
|
{"Language", ::name_for_language(vq->language)},
|
||||||
{"Name", vq->meta.name},
|
{"Name", vq->meta.name},
|
||||||
{"ShortDescription", vq->meta.short_description},
|
{"ShortDescription", vq->meta.short_description},
|
||||||
{"LongDescription", vq->meta.long_description},
|
{"LongDescription", vq->meta.long_description},
|
||||||
@@ -331,13 +331,39 @@ phosg::JSON Quest::json() const {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Quest::versions_key(Version v, uint8_t language) {
|
uint32_t Quest::versions_key(Version v, Language language) {
|
||||||
return (static_cast<uint32_t>(v) << 8) | language;
|
return (static_cast<uint32_t>(v) << 8) | static_cast<uint8_t>(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
const string& Quest::name_for_language(Language language) const {
|
||||||
|
size_t lang_index = static_cast<size_t>(language);
|
||||||
|
if (!this->names_by_language.at(lang_index).empty()) {
|
||||||
|
return this->names_by_language[lang_index];
|
||||||
|
}
|
||||||
|
constexpr size_t english_lang_index = static_cast<size_t>(Language::ENGLISH);
|
||||||
|
if (!this->names_by_language[english_lang_index].empty()) {
|
||||||
|
return this->names_by_language[english_lang_index];
|
||||||
|
}
|
||||||
|
for (const string& name : this->names_by_language) {
|
||||||
|
if (!name.empty()) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this->meta.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
|
void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
|
||||||
this->meta.assert_compatible(vq->meta);
|
this->meta.assert_compatible(vq->meta);
|
||||||
|
if (this->meta.create_item_mask_entries.empty()) {
|
||||||
|
this->meta.create_item_mask_entries = vq->meta.create_item_mask_entries;
|
||||||
|
}
|
||||||
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
|
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
|
||||||
|
|
||||||
|
size_t lang_index = static_cast<size_t>(vq->language);
|
||||||
|
auto& name_by_language = this->names_by_language.at(lang_index);
|
||||||
|
if (name_by_language.empty()) {
|
||||||
|
name_by_language = vq->meta.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<const SuperMap> Quest::get_supermap(int64_t random_seed) const {
|
std::shared_ptr<const SuperMap> Quest::get_supermap(int64_t random_seed) const {
|
||||||
@@ -349,7 +375,7 @@ std::shared_ptr<const SuperMap> Quest::get_supermap(int64_t random_seed) const {
|
|||||||
bool any_map_file_present = false;
|
bool any_map_file_present = false;
|
||||||
array<shared_ptr<const MapFile>, NUM_VERSIONS> map_files;
|
array<shared_ptr<const MapFile>, NUM_VERSIONS> map_files;
|
||||||
for (Version v : ALL_NON_PATCH_VERSIONS) {
|
for (Version v : ALL_NON_PATCH_VERSIONS) {
|
||||||
auto vq = this->version(v, 1);
|
auto vq = this->version(v, Language::ENGLISH);
|
||||||
if (vq && vq->map_file) {
|
if (vq && vq->map_file) {
|
||||||
auto map_file = vq->map_file;
|
auto map_file = vq->map_file;
|
||||||
if (map_file->has_random_sections()) {
|
if (map_file->has_random_sections()) {
|
||||||
@@ -378,17 +404,17 @@ std::shared_ptr<const SuperMap> Quest::get_supermap(int64_t random_seed) const {
|
|||||||
return supermap;
|
return supermap;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Quest::has_version(Version v, uint8_t language) const {
|
bool Quest::has_version(Version v, Language language) const {
|
||||||
return this->versions.count(this->versions_key(v, language));
|
return this->versions.count(this->versions_key(v, language));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Quest::has_version_any_language(Version v) const {
|
bool Quest::has_version_any_language(Version v) const {
|
||||||
uint32_t k = this->versions_key(v, 0);
|
uint32_t k = this->versions_key(v, Language::JAPANESE);
|
||||||
auto it = this->versions.lower_bound(k);
|
auto it = this->versions.lower_bound(k);
|
||||||
return ((it != this->versions.end()) && ((it->first & 0xFF00) == k));
|
return ((it != this->versions.end()) && ((it->first & 0xFF00) == k));
|
||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<const VersionedQuest> Quest::version(Version v, uint8_t language) const {
|
shared_ptr<const VersionedQuest> Quest::version(Version v, Language language) const {
|
||||||
// Return the requested version, if it exists
|
// Return the requested version, if it exists
|
||||||
try {
|
try {
|
||||||
return this->versions.at(this->versions_key(v, language));
|
return this->versions.at(this->versions_key(v, language));
|
||||||
@@ -397,13 +423,13 @@ shared_ptr<const VersionedQuest> Quest::version(Version v, uint8_t language) con
|
|||||||
|
|
||||||
// Return the English version, if it exists
|
// Return the English version, if it exists
|
||||||
try {
|
try {
|
||||||
return this->versions.at(this->versions_key(v, 1));
|
return this->versions.at(this->versions_key(v, Language::ENGLISH));
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the first language, if it exists
|
// Return the first language, if it exists
|
||||||
auto it = this->versions.lower_bound(this->versions_key(v, 0));
|
auto it = this->versions.lower_bound(this->versions_key(v, Language::JAPANESE));
|
||||||
if ((it == this->versions.end()) || ((it->first & 0xFF00) != this->versions_key(v, 0))) {
|
if ((it == this->versions.end()) || ((it->first & 0xFF00) != this->versions_key(v, Language::JAPANESE))) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return it->second;
|
return it->second;
|
||||||
@@ -413,7 +439,8 @@ QuestIndex::QuestIndex(
|
|||||||
const string& directory,
|
const string& directory,
|
||||||
shared_ptr<const QuestCategoryIndex> category_index,
|
shared_ptr<const QuestCategoryIndex> category_index,
|
||||||
const unordered_map<string, shared_ptr<const CommonItemSet>>& common_item_sets,
|
const unordered_map<string, shared_ptr<const CommonItemSet>>& common_item_sets,
|
||||||
const unordered_map<string, shared_ptr<const RareItemSet>>& rare_item_sets)
|
const unordered_map<string, shared_ptr<const RareItemSet>>& rare_item_sets,
|
||||||
|
bool raise_on_any_failure)
|
||||||
: directory(directory),
|
: directory(directory),
|
||||||
category_index(category_index) {
|
category_index(category_index) {
|
||||||
|
|
||||||
@@ -562,6 +589,9 @@ QuestIndex::QuestIndex(
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (const exception& e) {
|
} catch (const exception& e) {
|
||||||
|
if (raise_on_any_failure) {
|
||||||
|
throw runtime_error(format("({}) {}", filename, e.what()));
|
||||||
|
}
|
||||||
static_game_data_log.warning_f("({}) Failed to load quest file: ({})", filename, e.what());
|
static_game_data_log.warning_f("({}) Failed to load quest file: ({})", filename, e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -626,7 +656,7 @@ QuestIndex::QuestIndex(
|
|||||||
if (language_token.size() != 1) {
|
if (language_token.size() != 1) {
|
||||||
throw runtime_error("language token is not a single character");
|
throw runtime_error("language token is not a single character");
|
||||||
}
|
}
|
||||||
vq->language = language_code_for_char(language_token[0]);
|
vq->language = language_for_char(language_token[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto bin_decompressed = prs_decompress(*entry.data);
|
auto bin_decompressed = prs_decompress(*entry.data);
|
||||||
@@ -717,6 +747,10 @@ QuestIndex::QuestIndex(
|
|||||||
vq->meta.lock_status_register = metadata_json.get_int("LockStatusRegister");
|
vq->meta.lock_status_register = metadata_json.get_int("LockStatusRegister");
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
vq->meta.enemy_exp_overrides = QuestMetadata::parse_enemy_exp_overrides(metadata_json.at("EnemyEXPOverrides"));
|
||||||
|
} catch (const out_of_range&) {
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
vq->meta.common_item_set_name = metadata_json.at("CommonItemSetName").as_string();
|
vq->meta.common_item_set_name = metadata_json.at("CommonItemSetName").as_string();
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
@@ -757,31 +791,32 @@ QuestIndex::QuestIndex(
|
|||||||
auto q_it = this->quests_by_number.find(vq->meta.quest_number);
|
auto q_it = this->quests_by_number.find(vq->meta.quest_number);
|
||||||
if (q_it != this->quests_by_number.end()) {
|
if (q_it != this->quests_by_number.end()) {
|
||||||
q_it->second->add_version(vq);
|
q_it->second->add_version(vq);
|
||||||
static_game_data_log.debug_f("({}) Added {} {} version of quest {} ({}) with floors {}",
|
static_game_data_log.debug_f("({}) Added {} {} version of quest {} ({})",
|
||||||
filenames_str,
|
filenames_str,
|
||||||
phosg::name_for_enum(vq->version),
|
phosg::name_for_enum(vq->version),
|
||||||
char_for_language_code(vq->language),
|
char_for_language(vq->language),
|
||||||
vq->meta.quest_number,
|
vq->meta.quest_number,
|
||||||
vq->meta.name,
|
vq->meta.name);
|
||||||
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
|
|
||||||
} else {
|
} else {
|
||||||
auto q = make_shared<Quest>(vq);
|
auto q = make_shared<Quest>(vq);
|
||||||
this->quests_by_number.emplace(vq->meta.quest_number, q);
|
this->quests_by_number.emplace(vq->meta.quest_number, q);
|
||||||
this->quests_by_name.emplace(vq->meta.name, q);
|
this->quests_by_name.emplace(vq->meta.name, q);
|
||||||
this->quests_by_category_id_and_number[q->meta.category_id].emplace(vq->meta.quest_number, q);
|
this->quests_by_category_id_and_number[q->meta.category_id].emplace(vq->meta.quest_number, q);
|
||||||
static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {}) with floors {}",
|
static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {})",
|
||||||
filenames_str,
|
filenames_str,
|
||||||
phosg::name_for_enum(vq->version),
|
phosg::name_for_enum(vq->version),
|
||||||
char_for_language_code(vq->language),
|
char_for_language(vq->language),
|
||||||
vq->meta.quest_number,
|
vq->meta.quest_number,
|
||||||
vq->meta.name,
|
vq->meta.name,
|
||||||
name_for_episode(vq->meta.episode),
|
name_for_episode(vq->meta.episode),
|
||||||
category_name,
|
category_name,
|
||||||
vq->meta.category_id,
|
vq->meta.category_id,
|
||||||
vq->meta.joinable ? "joinable" : "not joinable",
|
vq->meta.joinable ? "joinable" : "not joinable");
|
||||||
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
|
|
||||||
}
|
}
|
||||||
} catch (const exception& e) {
|
} catch (const exception& e) {
|
||||||
|
if (raise_on_any_failure) {
|
||||||
|
throw runtime_error(format("({}) {}", basename, e.what()));
|
||||||
|
}
|
||||||
static_game_data_log.warning_f("({}) Failed to index quest file: {}", basename, e.what());
|
static_game_data_log.warning_f("({}) Failed to index quest file: {}", basename, e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -810,9 +845,6 @@ phosg::JSON QuestIndex::json() const {
|
|||||||
{"Categories", std::move(categories_json)},
|
{"Categories", std::move(categories_json)},
|
||||||
{"Quests", std::move(quests_json)},
|
{"Quests", std::move(quests_json)},
|
||||||
});
|
});
|
||||||
// std::map<uint32_t, std::shared_ptr<Quest>> quests_by_number;
|
|
||||||
// std::map<std::string, std::shared_ptr<Quest>> quests_by_name;
|
|
||||||
// std::map<uint32_t, std::map<uint32_t, std::shared_ptr<Quest>>> quests_by_category_id_and_number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
|
shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
|
||||||
@@ -915,7 +947,7 @@ string encode_download_quest_data(const string& compressed_data, size_t decompre
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t override_language) const {
|
shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(Language override_language) const {
|
||||||
// The download flag needs to be set in the bin header, or else the client
|
// The download flag needs to be set in the bin header, or else the client
|
||||||
// will ignore it when scanning for download quests in an offline game. To set
|
// will ignore it when scanning for download quests in an offline game. To set
|
||||||
// this flag, we need to decompress the quest's .bin file, set the flag, then
|
// this flag, we need to decompress the quest's .bin file, set the flag, then
|
||||||
@@ -938,7 +970,7 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
|
|||||||
if (decompressed_bin.size() < sizeof(PSOQuestHeaderDC)) {
|
if (decompressed_bin.size() < sizeof(PSOQuestHeaderDC)) {
|
||||||
throw runtime_error("bin file is too small for header");
|
throw runtime_error("bin file is too small for header");
|
||||||
}
|
}
|
||||||
if (override_language != 0xFF) {
|
if (override_language != Language::UNKNOWN) {
|
||||||
reinterpret_cast<PSOQuestHeaderDC*>(data_ptr)->language = override_language;
|
reinterpret_cast<PSOQuestHeaderDC*>(data_ptr)->language = override_language;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -947,7 +979,7 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
|
|||||||
if (decompressed_bin.size() < sizeof(PSOQuestHeaderPC)) {
|
if (decompressed_bin.size() < sizeof(PSOQuestHeaderPC)) {
|
||||||
throw runtime_error("bin file is too small for header");
|
throw runtime_error("bin file is too small for header");
|
||||||
}
|
}
|
||||||
if (override_language != 0xFF) {
|
if (override_language != Language::UNKNOWN) {
|
||||||
reinterpret_cast<PSOQuestHeaderPC*>(data_ptr)->language = override_language;
|
reinterpret_cast<PSOQuestHeaderPC*>(data_ptr)->language = override_language;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -957,7 +989,7 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
|
|||||||
if (decompressed_bin.size() < sizeof(PSOQuestHeaderGC)) {
|
if (decompressed_bin.size() < sizeof(PSOQuestHeaderGC)) {
|
||||||
throw runtime_error("bin file is too small for header");
|
throw runtime_error("bin file is too small for header");
|
||||||
}
|
}
|
||||||
if (override_language != 0xFF) {
|
if (override_language != Language::UNKNOWN) {
|
||||||
reinterpret_cast<PSOQuestHeaderGC*>(data_ptr)->language = override_language;
|
reinterpret_cast<PSOQuestHeaderGC*>(data_ptr)->language = override_language;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
+12
-8
@@ -72,7 +72,7 @@ struct VersionedQuest {
|
|||||||
// Most of these default values are intentionally invalid; we use these
|
// Most of these default values are intentionally invalid; we use these
|
||||||
// values to check if each field was parsed during quest indexing.
|
// values to check if each field was parsed during quest indexing.
|
||||||
Version version = Version::UNKNOWN;
|
Version version = Version::UNKNOWN;
|
||||||
uint8_t language = 0xFF;
|
Language language = Language::UNKNOWN;
|
||||||
std::shared_ptr<const std::string> bin_contents;
|
std::shared_ptr<const std::string> bin_contents;
|
||||||
std::shared_ptr<const std::string> dat_contents;
|
std::shared_ptr<const std::string> dat_contents;
|
||||||
std::shared_ptr<const MapFile> map_file;
|
std::shared_ptr<const MapFile> map_file;
|
||||||
@@ -86,7 +86,7 @@ struct VersionedQuest {
|
|||||||
std::string pvr_filename() const;
|
std::string pvr_filename() const;
|
||||||
std::string xb_filename() const;
|
std::string xb_filename() const;
|
||||||
|
|
||||||
std::shared_ptr<VersionedQuest> create_download_quest(uint8_t override_language = 0xFF) const;
|
std::shared_ptr<VersionedQuest> create_download_quest(Language override_language = Language::UNKNOWN) const;
|
||||||
std::string encode_qst() const;
|
std::string encode_qst() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,6 +94,7 @@ struct Quest {
|
|||||||
QuestMetadata meta;
|
QuestMetadata meta;
|
||||||
mutable std::shared_ptr<const SuperMap> supermap;
|
mutable std::shared_ptr<const SuperMap> supermap;
|
||||||
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
|
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
|
||||||
|
std::array<std::string, 8> names_by_language;
|
||||||
|
|
||||||
Quest() = delete;
|
Quest() = delete;
|
||||||
explicit Quest(std::shared_ptr<const VersionedQuest> initial_version);
|
explicit Quest(std::shared_ptr<const VersionedQuest> initial_version);
|
||||||
@@ -106,12 +107,14 @@ struct Quest {
|
|||||||
|
|
||||||
std::shared_ptr<const SuperMap> get_supermap(int64_t random_seed) const;
|
std::shared_ptr<const SuperMap> get_supermap(int64_t random_seed) const;
|
||||||
|
|
||||||
void add_version(std::shared_ptr<const VersionedQuest> vq);
|
const std::string& name_for_language(Language language) const;
|
||||||
bool has_version(Version v, uint8_t language) const;
|
|
||||||
bool has_version_any_language(Version v) const;
|
|
||||||
std::shared_ptr<const VersionedQuest> version(Version v, uint8_t language) const;
|
|
||||||
|
|
||||||
static uint32_t versions_key(Version v, uint8_t language);
|
void add_version(std::shared_ptr<const VersionedQuest> vq);
|
||||||
|
bool has_version(Version v, Language language) const;
|
||||||
|
bool has_version_any_language(Version v) const;
|
||||||
|
std::shared_ptr<const VersionedQuest> version(Version v, Language language) const;
|
||||||
|
|
||||||
|
static uint32_t versions_key(Version v, Language language);
|
||||||
};
|
};
|
||||||
|
|
||||||
struct QuestIndex {
|
struct QuestIndex {
|
||||||
@@ -133,7 +136,8 @@ struct QuestIndex {
|
|||||||
const std::string& directory,
|
const std::string& directory,
|
||||||
std::shared_ptr<const QuestCategoryIndex> category_index,
|
std::shared_ptr<const QuestCategoryIndex> category_index,
|
||||||
const std::unordered_map<std::string, std::shared_ptr<const CommonItemSet>>& common_item_sets,
|
const std::unordered_map<std::string, std::shared_ptr<const CommonItemSet>>& common_item_sets,
|
||||||
const std::unordered_map<std::string, std::shared_ptr<const RareItemSet>>& rare_item_sets);
|
const std::unordered_map<std::string, std::shared_ptr<const RareItemSet>>& rare_item_sets,
|
||||||
|
bool raise_on_any_failure);
|
||||||
phosg::JSON json() const;
|
phosg::JSON json() const;
|
||||||
|
|
||||||
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
|
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
|
||||||
|
|||||||
+137
-3
@@ -44,6 +44,28 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
|
|||||||
"quest version has a different lock status register (existing: {:04X}, new: {:04X})",
|
"quest version has a different lock status register (existing: {:04X}, new: {:04X})",
|
||||||
this->lock_status_register, other.lock_status_register));
|
this->lock_status_register, other.lock_status_register));
|
||||||
}
|
}
|
||||||
|
if (this->enemy_exp_overrides != other.enemy_exp_overrides) {
|
||||||
|
throw runtime_error("quest version has different enemy EXP overrides");
|
||||||
|
}
|
||||||
|
if (!this->create_item_mask_entries.empty() &&
|
||||||
|
!other.create_item_mask_entries.empty() &&
|
||||||
|
this->create_item_mask_entries != other.create_item_mask_entries) {
|
||||||
|
string this_str, other_str;
|
||||||
|
for (const auto& item : this->create_item_mask_entries) {
|
||||||
|
if (!this_str.empty()) {
|
||||||
|
this_str += ", ";
|
||||||
|
}
|
||||||
|
this_str += item.str();
|
||||||
|
}
|
||||||
|
for (const auto& item : other.create_item_mask_entries) {
|
||||||
|
if (!other_str.empty()) {
|
||||||
|
other_str += ", ";
|
||||||
|
}
|
||||||
|
other_str += item.str();
|
||||||
|
}
|
||||||
|
throw runtime_error(std::format(
|
||||||
|
"quest version has a different set of create item masks (existing: {}, new: {})", this_str, other_str));
|
||||||
|
}
|
||||||
if (!this->battle_rules != !other.battle_rules) {
|
if (!this->battle_rules != !other.battle_rules) {
|
||||||
throw runtime_error(std::format(
|
throw runtime_error(std::format(
|
||||||
"quest version has a different battle rules presence state (existing: {}, new: {})",
|
"quest version has a different battle rules presence state (existing: {}, new: {})",
|
||||||
@@ -69,7 +91,7 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
|
|||||||
if (this->challenge_difficulty != other.challenge_difficulty) {
|
if (this->challenge_difficulty != other.challenge_difficulty) {
|
||||||
throw runtime_error(std::format(
|
throw runtime_error(std::format(
|
||||||
"quest version has different challenge difficulty (existing: {}, new: {})",
|
"quest version has different challenge difficulty (existing: {}, new: {})",
|
||||||
this->challenge_difficulty, other.challenge_difficulty));
|
name_for_difficulty(this->challenge_difficulty), name_for_difficulty(other.challenge_difficulty)));
|
||||||
}
|
}
|
||||||
for (size_t z = 0; z < this->area_for_floor.size(); z++) {
|
for (size_t z = 0; z < this->area_for_floor.size(); z++) {
|
||||||
const auto& this_fa = this->area_for_floor[z];
|
const auto& this_fa = this->area_for_floor[z];
|
||||||
@@ -140,17 +162,31 @@ phosg::JSON QuestMetadata::json() const {
|
|||||||
for (const auto& fa : this->area_for_floor) {
|
for (const auto& fa : this->area_for_floor) {
|
||||||
floors_json.emplace_back(fa);
|
floors_json.emplace_back(fa);
|
||||||
}
|
}
|
||||||
|
auto enemy_exp_overrides_json = phosg::JSON::dict();
|
||||||
|
for (const auto& [key, exp_override] : this->enemy_exp_overrides) {
|
||||||
|
auto difficulty = static_cast<Difficulty>((key >> 24) & 3);
|
||||||
|
auto floor = static_cast<uint8_t>((key >> 16) & 0xFF);
|
||||||
|
auto enemy_type = static_cast<EnemyType>(key & 0xFFFF);
|
||||||
|
auto key_str = std::format("{}:0x{:02X}:{}", name_for_difficulty(difficulty), floor, phosg::name_for_enum(enemy_type));
|
||||||
|
enemy_exp_overrides_json.emplace(key_str, exp_override);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto create_item_mask_entries_json = phosg::JSON::list();
|
||||||
|
for (const auto& item : this->create_item_mask_entries) {
|
||||||
|
create_item_mask_entries_json.emplace_back(item.str());
|
||||||
|
}
|
||||||
|
|
||||||
return phosg::JSON::dict({
|
return phosg::JSON::dict({
|
||||||
{"CategoryID", this->category_id},
|
{"CategoryID", this->category_id},
|
||||||
{"Number", this->quest_number},
|
{"Number", this->quest_number},
|
||||||
{"Episode", name_for_episode(this->episode)},
|
{"Episode", name_for_episode(this->episode)},
|
||||||
{"FloorAssignments", floors_json},
|
{"FloorAssignments", std::move(floors_json)},
|
||||||
{"Joinable", this->joinable},
|
{"Joinable", this->joinable},
|
||||||
{"MaxPlayers", this->max_players},
|
{"MaxPlayers", this->max_players},
|
||||||
{"BattleRules", this->battle_rules ? this->battle_rules->json() : phosg::JSON(nullptr)},
|
{"BattleRules", this->battle_rules ? this->battle_rules->json() : phosg::JSON(nullptr)},
|
||||||
{"ChallengeTemplateIndex", (this->challenge_template_index >= 0) ? this->challenge_template_index : phosg::JSON(nullptr)},
|
{"ChallengeTemplateIndex", (this->challenge_template_index >= 0) ? this->challenge_template_index : phosg::JSON(nullptr)},
|
||||||
{"ChallengeEXPMultiplier", (this->challenge_exp_multiplier >= 0) ? this->challenge_exp_multiplier : phosg::JSON(nullptr)},
|
{"ChallengeEXPMultiplier", (this->challenge_exp_multiplier >= 0) ? this->challenge_exp_multiplier : phosg::JSON(nullptr)},
|
||||||
{"ChallengeDifficulty", (this->challenge_difficulty >= 0) ? this->challenge_difficulty : phosg::JSON(nullptr)},
|
{"ChallengeDifficulty", (this->challenge_difficulty != Difficulty::UNKNOWN) ? name_for_difficulty(this->challenge_difficulty) : phosg::JSON(nullptr)},
|
||||||
{"DescriptionFlag", this->description_flag},
|
{"DescriptionFlag", this->description_flag},
|
||||||
{"AvailableExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
|
{"AvailableExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
|
||||||
{"EnabledExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
|
{"EnabledExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
|
||||||
@@ -160,5 +196,103 @@ phosg::JSON QuestMetadata::json() const {
|
|||||||
{"DefaultDropMode", phosg::name_for_enum(this->default_drop_mode)},
|
{"DefaultDropMode", phosg::name_for_enum(this->default_drop_mode)},
|
||||||
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
|
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
|
||||||
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
|
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
|
||||||
|
{"EnemyEXPOverrides", std::move(enemy_exp_overrides_json)},
|
||||||
|
{"CreateItemMasks", std::move(create_item_mask_entries_json)},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QuestMetadata::CreateItemMask::CreateItemMask(const std::string& s) {
|
||||||
|
phosg::StringReader r(s);
|
||||||
|
for (size_t z = 0; z < 12 && !r.eof(); z++) {
|
||||||
|
auto& range = this->data1_ranges[z];
|
||||||
|
char c = r.get_s8();
|
||||||
|
if (c == '[') {
|
||||||
|
c = r.get_s8();
|
||||||
|
range.min = (phosg::value_for_hex_char(c) << 4) | phosg::value_for_hex_char(r.get_s8());
|
||||||
|
if (r.get_s8() != '-') {
|
||||||
|
throw std::runtime_error("invalid range spec");
|
||||||
|
}
|
||||||
|
c = r.get_s8();
|
||||||
|
range.max = (phosg::value_for_hex_char(c) << 4) | phosg::value_for_hex_char(r.get_s8());
|
||||||
|
if (r.get_s8() != ']') {
|
||||||
|
throw std::runtime_error("invalid range spec");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
range.min = (phosg::value_for_hex_char(c) << 4) | phosg::value_for_hex_char(r.get_s8());
|
||||||
|
range.max = range.min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string QuestMetadata::CreateItemMask::str() const {
|
||||||
|
std::string ret;
|
||||||
|
for (size_t z = 0; z < 12; z++) {
|
||||||
|
const auto& r = this->data1_ranges[z];
|
||||||
|
if (r.min == r.max) {
|
||||||
|
ret += std::format("{:02X}", r.min);
|
||||||
|
} else {
|
||||||
|
ret += std::format("[{:02X}-{:02X}]", r.min, r.max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QuestMetadata::CreateItemMask::match(const ItemData& item) const {
|
||||||
|
for (size_t z = 0; z < 12; z++) {
|
||||||
|
const auto& r = this->data1_ranges[z];
|
||||||
|
uint8_t v = item.data1[z];
|
||||||
|
if (v < r.min || v > r.max) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t QuestMetadata::CreateItemMask::primary_identifier() const {
|
||||||
|
uint32_t ret = 0;
|
||||||
|
for (size_t z = 0; z < 3; z++) {
|
||||||
|
const auto& r = this->data1_ranges[z];
|
||||||
|
if (r.min != r.max) {
|
||||||
|
throw std::runtime_error("create item mask is ambiguous; cannot compute primary identifier");
|
||||||
|
}
|
||||||
|
ret = (ret << 8) | r.min;
|
||||||
|
}
|
||||||
|
return ret << 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_map<uint32_t, uint32_t> QuestMetadata::parse_enemy_exp_overrides(const phosg::JSON& json) {
|
||||||
|
try {
|
||||||
|
std::unordered_map<uint32_t, uint32_t> ret;
|
||||||
|
for (const auto& [key, exp_value_json] : json.as_dict()) {
|
||||||
|
// Key is like "Difficulty:Floor:EnemyType" or "Difficulty:EnemyType"
|
||||||
|
auto key_tokens = phosg::split(key, ':');
|
||||||
|
|
||||||
|
static const unordered_map<string, Difficulty> difficulty_keys(
|
||||||
|
{{"Normal", Difficulty::NORMAL}, {"Hard", Difficulty::HARD}, {"VeryHard", Difficulty::VERY_HARD}, {"Ultimate", Difficulty::ULTIMATE}});
|
||||||
|
|
||||||
|
Difficulty difficulty = Difficulty::NORMAL;
|
||||||
|
EnemyType enemy_type = EnemyType::UNKNOWN;
|
||||||
|
uint8_t floor = 0xFF;
|
||||||
|
if (key_tokens.size() == 2) {
|
||||||
|
enemy_type = phosg::enum_for_name<EnemyType>(key_tokens[1]);
|
||||||
|
} else if (key_tokens.size() == 3) {
|
||||||
|
floor = stoul(key_tokens[1], nullptr, 0);
|
||||||
|
enemy_type = phosg::enum_for_name<EnemyType>(key_tokens[2]);
|
||||||
|
} else {
|
||||||
|
throw runtime_error("malformatted key: " + key);
|
||||||
|
}
|
||||||
|
difficulty = difficulty_keys.at(key_tokens[0]);
|
||||||
|
if (floor == 0xFF) {
|
||||||
|
for (size_t floor = 0; floor < 0x12; floor++) {
|
||||||
|
ret.emplace(QuestMetadata::exp_override_key(difficulty, floor, enemy_type), exp_value_json->as_int());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ret.emplace(QuestMetadata::exp_override_key(difficulty, floor, enemy_type), exp_value_json->as_int());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
} catch (const exception& e) {
|
||||||
|
throw std::runtime_error(std::format("invalid enemy EXP overrides: ", e.what()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+36
-1
@@ -9,10 +9,12 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "CommonItemSet.hh"
|
#include "CommonItemSet.hh"
|
||||||
|
#include "EnemyType.hh"
|
||||||
#include "IntegralExpression.hh"
|
#include "IntegralExpression.hh"
|
||||||
#include "Map.hh"
|
#include "Map.hh"
|
||||||
#include "PlayerSubordinates.hh"
|
#include "PlayerSubordinates.hh"
|
||||||
#include "RareItemSet.hh"
|
#include "RareItemSet.hh"
|
||||||
|
#include "StaticGameData.hh"
|
||||||
|
|
||||||
struct QuestMetadata {
|
struct QuestMetadata {
|
||||||
// This structure contains configuration that should be the same across all
|
// This structure contains configuration that should be the same across all
|
||||||
@@ -28,7 +30,7 @@ struct QuestMetadata {
|
|||||||
std::shared_ptr<const BattleRules> battle_rules;
|
std::shared_ptr<const BattleRules> battle_rules;
|
||||||
ssize_t challenge_template_index = -1;
|
ssize_t challenge_template_index = -1;
|
||||||
float challenge_exp_multiplier = -1.0f;
|
float challenge_exp_multiplier = -1.0f;
|
||||||
int8_t challenge_difficulty = -1;
|
Difficulty challenge_difficulty = Difficulty::UNKNOWN;
|
||||||
uint8_t description_flag = 0x00;
|
uint8_t description_flag = 0x00;
|
||||||
std::shared_ptr<const IntegralExpression> available_expression;
|
std::shared_ptr<const IntegralExpression> available_expression;
|
||||||
std::shared_ptr<const IntegralExpression> enabled_expression;
|
std::shared_ptr<const IntegralExpression> enabled_expression;
|
||||||
@@ -40,11 +42,44 @@ struct QuestMetadata {
|
|||||||
ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0
|
ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0
|
||||||
bool allow_start_from_chat_command = false;
|
bool allow_start_from_chat_command = false;
|
||||||
int16_t lock_status_register = -1;
|
int16_t lock_status_register = -1;
|
||||||
|
std::unordered_map<uint32_t, uint32_t> enemy_exp_overrides;
|
||||||
|
|
||||||
|
// Item create allowances (only used on BB)
|
||||||
|
struct CreateItemMask {
|
||||||
|
struct Range {
|
||||||
|
uint8_t min = 0x00;
|
||||||
|
uint8_t max = 0x00;
|
||||||
|
|
||||||
|
bool operator==(const Range& other) const = default;
|
||||||
|
bool operator!=(const Range& other) const = default;
|
||||||
|
};
|
||||||
|
std::array<Range, 12> data1_ranges;
|
||||||
|
|
||||||
|
CreateItemMask() = default;
|
||||||
|
CreateItemMask(const CreateItemMask& other) = default;
|
||||||
|
CreateItemMask(CreateItemMask&& other) = default;
|
||||||
|
CreateItemMask& operator=(const CreateItemMask& other) = default;
|
||||||
|
CreateItemMask& operator=(CreateItemMask&& other) = default;
|
||||||
|
bool operator==(const CreateItemMask& other) const = default;
|
||||||
|
bool operator!=(const CreateItemMask& other) const = default;
|
||||||
|
|
||||||
|
explicit CreateItemMask(const std::string& s); // Inverse of str()
|
||||||
|
std::string str() const;
|
||||||
|
|
||||||
|
bool match(const ItemData& item) const;
|
||||||
|
uint32_t primary_identifier() const; // Raises if any of data1[0-2] are ambiguous
|
||||||
|
};
|
||||||
|
std::vector<CreateItemMask> create_item_mask_entries;
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string short_description;
|
std::string short_description;
|
||||||
std::string long_description;
|
std::string long_description;
|
||||||
|
|
||||||
|
static std::unordered_map<uint32_t, uint32_t> parse_enemy_exp_overrides(const phosg::JSON& json);
|
||||||
|
static inline uint32_t exp_override_key(Difficulty difficulty, uint8_t floor, EnemyType enemy_type) {
|
||||||
|
return (static_cast<uint32_t>(difficulty) << 24) | (static_cast<uint32_t>(floor) << 16) | static_cast<uint32_t>(enemy_type);
|
||||||
|
}
|
||||||
|
|
||||||
void assign_default_areas(Version version, Episode episode);
|
void assign_default_areas(Version version, Episode episode);
|
||||||
void assert_compatible(const QuestMetadata& other) const;
|
void assert_compatible(const QuestMetadata& other) const;
|
||||||
phosg::JSON json() const;
|
phosg::JSON json() const;
|
||||||
|
|||||||
+216
-82
@@ -62,17 +62,6 @@ using AttackData = BattleParamsIndex::AttackData;
|
|||||||
using ResistData = BattleParamsIndex::ResistData;
|
using ResistData = BattleParamsIndex::ResistData;
|
||||||
using MovementData = BattleParamsIndex::MovementData;
|
using MovementData = BattleParamsIndex::MovementData;
|
||||||
|
|
||||||
// bit_cast isn't in the standard place on macOS (it is apparently implicitly
|
|
||||||
// included by resource_dasm, but newserv can be built without resource_dasm)
|
|
||||||
// and I'm too lazy to go find the right header to include
|
|
||||||
template <typename ToT, typename FromT>
|
|
||||||
ToT as_type(const FromT& v) {
|
|
||||||
static_assert(sizeof(FromT) == sizeof(ToT), "types are not the same size");
|
|
||||||
ToT ret;
|
|
||||||
memcpy(&ret, &v, sizeof(ToT));
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const char* name_for_header_episode_number(uint8_t episode) {
|
static const char* name_for_header_episode_number(uint8_t episode) {
|
||||||
static const array<const char*, 3> names = {"Episode1", "Episode2", "Episode4"};
|
static const array<const char*, 3> names = {"Episode1", "Episode2", "Episode4"};
|
||||||
try {
|
try {
|
||||||
@@ -82,8 +71,8 @@ static const char* name_for_header_episode_number(uint8_t episode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static TextEncoding encoding_for_language(uint8_t language) {
|
static TextEncoding encoding_for_language(Language language) {
|
||||||
return (language ? TextEncoding::ISO8859 : TextEncoding::SJIS);
|
return ((language == Language::JAPANESE) ? TextEncoding::SJIS : TextEncoding::ISO8859);
|
||||||
}
|
}
|
||||||
|
|
||||||
static string escape_string(const string& data, TextEncoding encoding = TextEncoding::UTF8) {
|
static string escape_string(const string& data, TextEncoding encoding = TextEncoding::UTF8) {
|
||||||
@@ -630,7 +619,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// <rXX> => value of rXX as %d (signed integer)
|
// <rXX> => value of rXX as %d (signed integer)
|
||||||
// <fXX> => value of rXX as %f (floating-point) (v3 and later)
|
// <fXX> => value of rXX as %f (floating-point) (v3 and later)
|
||||||
// <color X> => changes text color like $CX would (supported on 11/2000 and
|
// <color X> => changes text color like $CX would (supported on 11/2000 and
|
||||||
// later); X must be numeric, so <color G> does not work
|
// later); X must be numeric and in the range 0-7, so <color 8>, <color
|
||||||
|
// 9>, and <color G> do not work (though \tC8, \tC9, and \tCG can be used
|
||||||
|
// directly in the text, and do work)
|
||||||
// <cr> => newline
|
// <cr> => newline
|
||||||
// <hero name> or <name hero> => character's name
|
// <hero name> or <name hero> => character's name
|
||||||
// <hero job> or <name job> => character's class
|
// <hero job> or <name job> => character's class
|
||||||
@@ -978,7 +969,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
|
|
||||||
// Creates an item in the player's inventory. If the item is successfully
|
// Creates an item in the player's inventory. If the item is successfully
|
||||||
// created, this opcode sends 6x2B on all versions except BB. On BB, this
|
// created, this opcode sends 6x2B on all versions except BB. On BB, this
|
||||||
// opcode sends 6xCA, and the server sends 6xBE to create the item.
|
// opcode sends 6xCA, and the server sends 6xBE to create the item. The
|
||||||
|
// requested item must match one of the item creation masks in the quest
|
||||||
|
// script's header.
|
||||||
// regsA[0-2] = item.data1[0-2]
|
// regsA[0-2] = item.data1[0-2]
|
||||||
// regB = returned item ID, or FFFFFFFF if item can't be created
|
// regB = returned item ID, or FFFFFFFF if item can't be created
|
||||||
{0xB3, "item_create", nullptr, {{R_REG_SET_FIXED, 3}, W_REG}, F_V0_V4},
|
{0xB3, "item_create", nullptr, {{R_REG_SET_FIXED, 3}, W_REG}, F_V0_V4},
|
||||||
@@ -1913,15 +1906,15 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// Specifies which enemy should be affected by subsequent get_*_data
|
// Specifies which enemy should be affected by subsequent get_*_data
|
||||||
// opcodes (the following 4 definitions). valueA is the battle parameter
|
// opcodes (the following 4 definitions). valueA is the battle parameter
|
||||||
// index for the desired enemy.
|
// index for the desired enemy.
|
||||||
{0xF891, "load_enemy_data", nullptr, {I32}, F_V2_V4 | F_ARGS},
|
{0xF891, "set_override_enemy_bp_index", "load_enemy_data", {I32}, F_V2_V4 | F_ARGS},
|
||||||
|
|
||||||
// Replaces enemy stats with the given structures (PlayerStats, AttackData,
|
// Replaces enemy stats with the given structures (PlayerStats, AttackData,
|
||||||
// ResistData, or MovementData) for the enemy previously specified with
|
// ResistData, or MovementData) for the enemy previously specified with
|
||||||
// load_enemy_data.
|
// load_enemy_data.
|
||||||
{0xF892, "get_physical_data", nullptr, {{LABEL16, Arg::DataType::PLAYER_STATS, "stats"}}, F_V2_V4},
|
{0xF892, "set_enemy_physical_data", "get_physical_data", {{LABEL16, Arg::DataType::PLAYER_STATS, "stats"}}, F_V2_V4},
|
||||||
{0xF893, "get_attack_data", nullptr, {{LABEL16, Arg::DataType::ATTACK_DATA, "attack_data"}}, F_V2_V4},
|
{0xF893, "set_enemy_attack_data", "get_attack_data", {{LABEL16, Arg::DataType::ATTACK_DATA, "attack_data"}}, F_V2_V4},
|
||||||
{0xF894, "get_resist_data", nullptr, {{LABEL16, Arg::DataType::RESIST_DATA, "resist_data"}}, F_V2_V4},
|
{0xF894, "set_enemy_resist_data", "get_resist_data", {{LABEL16, Arg::DataType::RESIST_DATA, "resist_data"}}, F_V2_V4},
|
||||||
{0xF895, "get_movement_data", nullptr, {{LABEL16, Arg::DataType::MOVEMENT_DATA, "movement_data"}}, F_V2_V4},
|
{0xF895, "set_enemy_movement_data", "get_movement_data", {{LABEL16, Arg::DataType::MOVEMENT_DATA, "movement_data"}}, F_V2_V4},
|
||||||
|
|
||||||
// Reads 2 bytes or 4 bytes from the event flags in the system file.
|
// Reads 2 bytes or 4 bytes from the event flags in the system file.
|
||||||
// regA = event flag index
|
// regA = event flag index
|
||||||
@@ -2257,7 +2250,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// valueC = current position along path
|
// valueC = current position along path
|
||||||
// valueD = loop flag (0 = no, 1 = yes)
|
// valueD = loop flag (0 = no, 1 = yes)
|
||||||
// regsE[0-2] = result point (x, y, z as floats)
|
// regsE[0-2] = result point (x, y, z as floats)
|
||||||
// regsE[3] = the result code (0 = failed, 1 = success)
|
// regsE[3] = result code (0 = failed, 1 = success)
|
||||||
// labelF = control point entries (array of valueA VectorXYZTF structures)
|
// labelF = control point entries (array of valueA VectorXYZTF structures)
|
||||||
{0xF8DB, "get_vector_from_path", "unknownF8DB", {I32, FLOAT32, FLOAT32, I32, {W_REG_SET_FIXED, 4}, SCRIPT16}, F_V3_V4 | F_ARGS},
|
{0xF8DB, "get_vector_from_path", "unknownF8DB", {I32, FLOAT32, FLOAT32, I32, {W_REG_SET_FIXED, 4}, SCRIPT16}, F_V3_V4 | F_ARGS},
|
||||||
|
|
||||||
@@ -2353,7 +2346,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// valueC = current position along path
|
// valueC = current position along path
|
||||||
// valueD = loop flag (0 = no, 1 = yes)
|
// valueD = loop flag (0 = no, 1 = yes)
|
||||||
// regsE[0-2] = result point (x, y, z as floats)
|
// regsE[0-2] = result point (x, y, z as floats)
|
||||||
// regsE[3] = the result code (0 = failed, 1 = success)
|
// regsE[3] = result code (0 = failed, 1 = success)
|
||||||
// labelF = control point entries (array of valueA VectorXYZTF structures)
|
// labelF = control point entries (array of valueA VectorXYZTF structures)
|
||||||
{0xF8F2, "compute_bezier_curve_point", "load_unk_data", {I32, FLOAT32, FLOAT32, I32, {W_REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::BEZIER_CONTROL_POINT_DATA}}, F_V3_V4 | F_ARGS},
|
{0xF8F2, "compute_bezier_curve_point", "load_unk_data", {I32, FLOAT32, FLOAT32, I32, {W_REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::BEZIER_CONTROL_POINT_DATA}}, F_V3_V4 | F_ARGS},
|
||||||
|
|
||||||
@@ -2462,8 +2455,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
|
|
||||||
// Returns the amount of Meseta the player has in both their inventory and
|
// Returns the amount of Meseta the player has in both their inventory and
|
||||||
// bank.
|
// bank.
|
||||||
// regA = returned Meseta amount in inventory
|
// regsA[0] = returned Meseta amount in inventory
|
||||||
// regB = returned Meseta amount in bank
|
// regsA[1] = returned Meseta amount in bank
|
||||||
{0xF91F, "get_slot_meseta", nullptr, {{W_REG_SET_FIXED, 2}}, F_V3_V4},
|
{0xF91F, "get_slot_meseta", nullptr, {{W_REG_SET_FIXED, 2}}, F_V3_V4},
|
||||||
|
|
||||||
// Returns a player's level.
|
// Returns a player's level.
|
||||||
@@ -2583,10 +2576,12 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// strB = message
|
// strB = message
|
||||||
{0xF931, "chat_bubble", nullptr, {I32, CSTRING}, F_V3_V4 | F_ARGS},
|
{0xF931, "chat_bubble", nullptr, {I32, CSTRING}, F_V3_V4 | F_ARGS},
|
||||||
|
|
||||||
// Sets the episode to be loaded the next time an area is loaded. regA is
|
// Sets the episode to be loaded the next time an area is loaded (e.g. by
|
||||||
// the same as for set_episode. Unlike set_episode, this opcode does not
|
// the player changing floors). regA is the same as for set_episode. Like
|
||||||
// reset the floor configuration.
|
// set_episode, it resets the floor configuration to the defaults, but this
|
||||||
{0xF932, "set_episode2", nullptr, {R_REG}, F_V3_V4},
|
// happens at the time the player changes floors, not when the opcode is
|
||||||
|
// executed.
|
||||||
|
{0xF932, "delayed_switch_episode", "set_episode2", {R_REG}, F_V3_V4},
|
||||||
|
|
||||||
// Sets the rank prizes in offline challenge mode.
|
// Sets the rank prizes in offline challenge mode.
|
||||||
// regsA[0] = rank (unusual value order: 0 = S, 1 = B, 2 = A)
|
// regsA[0] = rank (unusual value order: 0 = S, 1 = B, 2 = A)
|
||||||
@@ -2649,7 +2644,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
{0xF93C, "wrap_item_with_color", "item_packing2", {ITEM_ID, I32}, F_V3_V4 | F_ARGS},
|
{0xF93C, "wrap_item_with_color", "item_packing2", {ITEM_ID, I32}, F_V3_V4 | F_ARGS},
|
||||||
|
|
||||||
// Returns the local player's language setting. For values, see
|
// Returns the local player's language setting. For values, see
|
||||||
// name_for_language_code in StaticGameData.cc.
|
// name_for_language in StaticGameData.cc.
|
||||||
{0xF93D, "get_lang_setting", "get_lang_setting?", {W_REG}, F_V3_V4 | F_ARGS},
|
{0xF93D, "get_lang_setting", "get_lang_setting?", {W_REG}, F_V3_V4 | F_ARGS},
|
||||||
|
|
||||||
// Sets some values to be sent to the server with send_statistic.
|
// Sets some values to be sent to the server with send_statistic.
|
||||||
@@ -2771,6 +2766,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// 5 = bank
|
// 5 = bank
|
||||||
// 6 = tekker
|
// 6 = tekker
|
||||||
// 7 = government quest counter
|
// 7 = government quest counter
|
||||||
|
// 8 = Momoka item exchange (this opens a menu displaying the items
|
||||||
|
// specified in the quest header, which sends 6xD9 when the player
|
||||||
|
// chooses an item)
|
||||||
// valueA is not bounds-checked, so it could be used to write a byte with
|
// valueA is not bounds-checked, so it could be used to write a byte with
|
||||||
// the value 1 anywhere in memory.
|
// the value 1 anywhere in memory.
|
||||||
{0xF950, "bb_p2_menu", "BB_p2_menu", {I32}, F_V4 | F_ARGS},
|
{0xF950, "bb_p2_menu", "BB_p2_menu", {I32}, F_V4 | F_ARGS},
|
||||||
@@ -2788,7 +2786,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// Returns the number of items in the player's inventory.
|
// Returns the number of items in the player's inventory.
|
||||||
{0xF952, "bb_get_number_in_pack", "BB_get_number_in_pack", {W_REG}, F_V4},
|
{0xF952, "bb_get_number_in_pack", "BB_get_number_in_pack", {W_REG}, F_V4},
|
||||||
|
|
||||||
// Requests an item exchange in the player's inventory. Sends 6xD5.
|
// Requests an item exchange in the player's inventory. Sends 6xD5. The
|
||||||
|
// requested item must match one of the item creation masks in the quest
|
||||||
|
// script's header.
|
||||||
// valueA/valueB/valueC = item.data1[0-2] to search for
|
// valueA/valueB/valueC = item.data1[0-2] to search for
|
||||||
// valueD/valueE/valueF = item.data1[0-2] to replace it with
|
// valueD/valueE/valueF = item.data1[0-2] to replace it with
|
||||||
// labelG = label to call on success
|
// labelG = label to call on success
|
||||||
@@ -2801,11 +2801,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// 2 = item not found)
|
// 2 = item not found)
|
||||||
{0xF954, "bb_check_wrap", "BB_check_wrap", {I32, W_REG}, F_V4 | F_ARGS},
|
{0xF954, "bb_check_wrap", "BB_check_wrap", {I32, W_REG}, F_V4 | F_ARGS},
|
||||||
|
|
||||||
// Requests an item exchange for Photon Drops. Sends 6xD7.
|
// Requests an item exchange for Photon Drops. Sends 6xD7. The requested
|
||||||
|
// item must match one of the item creation masks in the quest script's
|
||||||
|
// header.
|
||||||
// valueA/valueB/valueC = item.data1[0-2] for requested item
|
// valueA/valueB/valueC = item.data1[0-2] for requested item
|
||||||
// labelD = label to call on success
|
// labelD = label to call on success
|
||||||
// labelE = label to call on failure
|
// labelE = label to call on failure
|
||||||
{0xF955, "bb_exchange_pd_item", "BB_exchange_PD_item", {I32, I32, I32, LABEL16, LABEL16}, F_V4 | F_ARGS},
|
{0xF955, "bb_exchange_pd_item", "BB_exchange_PD_item", {I32, I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||||
|
|
||||||
// Requests an S-rank special upgrade in exchange for Photon Drops. Sends
|
// Requests an S-rank special upgrade in exchange for Photon Drops. Sends
|
||||||
// 6xD8.
|
// 6xD8.
|
||||||
@@ -2814,7 +2816,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// valueE = special type
|
// valueE = special type
|
||||||
// labelF = label to call on success
|
// labelF = label to call on success
|
||||||
// labelG = label to call on failure
|
// labelG = label to call on failure
|
||||||
{0xF956, "bb_exchange_pd_srank", "BB_exchange_PD_srank", {I32, I32, I32, I32, I32, LABEL16, LABEL16}, F_V4 | F_ARGS},
|
{0xF956, "bb_exchange_pd_srank", "BB_exchange_PD_srank", {I32, I32, I32, I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||||
|
|
||||||
// Requests a weapon attribute upgrade in exchange for Photon Drops. Sends
|
// Requests a weapon attribute upgrade in exchange for Photon Drops. Sends
|
||||||
// 6xDA.
|
// 6xDA.
|
||||||
@@ -2824,12 +2826,12 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// valueF = payment count (number of PDs)
|
// valueF = payment count (number of PDs)
|
||||||
// labelG = label to call on success
|
// labelG = label to call on success
|
||||||
// labelH = label to call on failure
|
// labelH = label to call on failure
|
||||||
{0xF957, "bb_exchange_pd_percent", "BB_exchange_PD_special", {I32, I32, I32, I32, I32, I32, LABEL16, LABEL16}, F_V4 | F_ARGS},
|
{0xF957, "bb_exchange_pd_percent", "BB_exchange_PD_special", {I32, I32, I32, I32, I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||||
|
|
||||||
// Requests a weapon attribute upgrade in exchange for Photon Spheres.
|
// Requests a weapon attribute upgrade in exchange for Photon Spheres.
|
||||||
// Sends 6xDA. Same arguments as bb_exchange_pd_percent, except Photon
|
// Sends 6xDA. Same arguments as bb_exchange_pd_percent, except Photon
|
||||||
// Spheres are used instead.
|
// Spheres are used instead.
|
||||||
{0xF958, "bb_exchange_ps_percent", "BB_exchange_PS_percent", {I32, I32, I32, I32, I32, I32, LABEL16, LABEL16}, F_V4 | F_ARGS},
|
{0xF958, "bb_exchange_ps_percent", "BB_exchange_PS_percent", {I32, I32, I32, I32, I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||||
|
|
||||||
// Determines whether the Episode 4 boss can escape if undefeated after 20
|
// Determines whether the Episode 4 boss can escape if undefeated after 20
|
||||||
// minutes.
|
// minutes.
|
||||||
@@ -2840,24 +2842,28 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
|||||||
// (even if the boss has already been defeated).
|
// (even if the boss has already been defeated).
|
||||||
{0xF95A, "bb_is_ep4_boss_dying", nullptr, {W_REG}, F_V4},
|
{0xF95A, "bb_is_ep4_boss_dying", nullptr, {W_REG}, F_V4},
|
||||||
|
|
||||||
// Requests an item exchange. Sends 6xD9.
|
// Requests an item exchange. Sends 6xD9. The requested item must match one
|
||||||
|
// of the item creation masks in the quest script's header.
|
||||||
// valueA = find_item.data1[0-2] (low 3 bytes; high byte unused)
|
// valueA = find_item.data1[0-2] (low 3 bytes; high byte unused)
|
||||||
// valueB = replace_item.data1[0-2] (low 3 bytes; high byte unused)
|
// valueB = replace_item.data1[0-2] (low 3 bytes; high byte unused)
|
||||||
// valueC = token1 (see 6xD9 in CommandFormats.hh)
|
// valueC = token1 (see 6xD9 in CommandFormats.hh)
|
||||||
// valueD = token2 (see 6xD9 in CommandFormats.hh)
|
// valueD = token2 (see 6xD9 in CommandFormats.hh)
|
||||||
// labelE = label to call on success
|
// labelE = label to call on success
|
||||||
// labelF = label to call on failure
|
// labelF = label to call on failure
|
||||||
{0xF95B, "bb_send_6xD9", nullptr, {I32, I32, I32, I32, LABEL16, LABEL16}, F_V4 | F_ARGS},
|
{0xF95B, "bb_replace_item", "bb_send_6xD9", {I32, I32, I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||||
|
|
||||||
// Requests an exchange of Secret Lottery Tickets for items. Sends 6xDE.
|
// Requests an exchange of Secret Lottery Tickets for items. Sends 6xDE.
|
||||||
// See SecretLotteryResultItems in config.json for the item pool used by
|
// The pool of items that can be returned by this opcode is determined by
|
||||||
// this opcode.
|
// the quest's header; newserv assembles/disassembles this field as
|
||||||
|
// .exchange_item directives. The first entry in the header is the currency
|
||||||
|
// item (which is to be deleted from the inventory); the others are the
|
||||||
|
// items the server randomly chooses from.
|
||||||
// valueA = index
|
// valueA = index
|
||||||
// valueB = unknown_a1
|
// valueB = unknown_a1
|
||||||
// labelC = label to call on success
|
// labelC = label to call on success
|
||||||
// labelD = label to call on failure (unused because of a client bug; see
|
// labelD = label to call on failure (unused because of a client bug; see
|
||||||
// 6xDE description in CommandFormats.hh for details)
|
// 6xDE description in CommandFormats.hh for details)
|
||||||
{0xF95C, "bb_exchange_slt", "BB_exchange_SLT", {I32, I32, LABEL32, LABEL32}, F_V4 | F_ARGS},
|
{0xF95C, "bb_exchange_slt", "BB_exchange_SLT", {I32, I32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS},
|
||||||
|
|
||||||
// Removes a single Photon Crystal from the player's inventory, and
|
// Removes a single Photon Crystal from the player's inventory, and
|
||||||
// disables drops for the rest of the quest. Sends 6xDF.
|
// disables drops for the rest of the quest. Sends 6xDF.
|
||||||
@@ -2966,11 +2972,57 @@ void check_opcode_definitions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CreateItemMaskEntry::CreateItemMaskEntry(const QuestMetadata::CreateItemMask& mask) {
|
||||||
|
for (size_t z = 0; z < 12; z++) {
|
||||||
|
auto& r = mask.data1_ranges[z];
|
||||||
|
if (r.min == r.max) {
|
||||||
|
this->data1_fields[z] = r.min;
|
||||||
|
} else if (r.min == 0x00 && r.max == 0xFF) {
|
||||||
|
this->data1_fields[z] = -1;
|
||||||
|
} else {
|
||||||
|
this->data1_fields[z] = (r.min * 1000000) + (r.max * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateItemMaskEntry::operator QuestMetadata::CreateItemMask() const {
|
||||||
|
using Range = QuestMetadata::CreateItemMask::Range;
|
||||||
|
|
||||||
|
QuestMetadata::CreateItemMask ret;
|
||||||
|
for (size_t z = 0; z < 12; z++) {
|
||||||
|
int32_t v = this->data1_fields[z];
|
||||||
|
|
||||||
|
if (v < 0) {
|
||||||
|
// If v is negative, any value is allowed in this field
|
||||||
|
ret.data1_ranges[z] = Range{.min = 0x00, .max = 0xFF};
|
||||||
|
|
||||||
|
} else if (v < 0x100) {
|
||||||
|
// If v fits in an unsigned byte, this field must match exactly
|
||||||
|
ret.data1_ranges[z] = Range{.min = static_cast<uint8_t>(v), .max = static_cast<uint8_t>(v)};
|
||||||
|
|
||||||
|
} else if (v >= 1000000 && v <= 2000000) {
|
||||||
|
// Otherwise, the allowed range of values is encoded in decimal as
|
||||||
|
// 1MMMmmm (m = min, M = max)
|
||||||
|
uint32_t min = v % 1000;
|
||||||
|
uint32_t max = (v / 1000) % 1000;
|
||||||
|
if ((min > 0xFF) || (max > 0xFF) || (min > max)) {
|
||||||
|
throw std::runtime_error(std::format("invalid range spec {} (0x{:X})", v, v));
|
||||||
|
}
|
||||||
|
ret.data1_ranges[z] = Range{.min = static_cast<uint8_t>(min), .max = static_cast<uint8_t>(max)};
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error(std::format("invalid range spec {} (0x{:X})", v, v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
std::string disassemble_quest_script(
|
std::string disassemble_quest_script(
|
||||||
const void* data,
|
const void* data,
|
||||||
size_t size,
|
size_t size,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t override_language,
|
Language override_language,
|
||||||
bool reassembly_mode,
|
bool reassembly_mode,
|
||||||
bool use_qedit_names) {
|
bool use_qedit_names) {
|
||||||
phosg::StringReader r(data, size);
|
phosg::StringReader r(data, size);
|
||||||
@@ -2980,14 +3032,14 @@ std::string disassemble_quest_script(
|
|||||||
bool use_wstrs = false;
|
bool use_wstrs = false;
|
||||||
size_t code_offset = 0;
|
size_t code_offset = 0;
|
||||||
size_t function_table_offset = 0;
|
size_t function_table_offset = 0;
|
||||||
uint8_t language;
|
Language language;
|
||||||
switch (version) {
|
switch (version) {
|
||||||
case Version::DC_NTE: {
|
case Version::DC_NTE: {
|
||||||
const auto& header = r.get<PSOQuestHeaderDCNTE>();
|
const auto& header = r.get<PSOQuestHeaderDCNTE>();
|
||||||
code_offset = header.code_offset;
|
code_offset = header.code_offset;
|
||||||
function_table_offset = header.function_table_offset;
|
function_table_offset = header.function_table_offset;
|
||||||
language = 0;
|
language = Language::JAPANESE;
|
||||||
lines.emplace_back(".name " + escape_string(header.name.decode(0)));
|
lines.emplace_back(".name " + escape_string(header.name.decode(Language::JAPANESE)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Version::DC_11_2000:
|
case Version::DC_11_2000:
|
||||||
@@ -2996,15 +3048,15 @@ std::string disassemble_quest_script(
|
|||||||
const auto& header = r.get<PSOQuestHeaderDC>();
|
const auto& header = r.get<PSOQuestHeaderDC>();
|
||||||
code_offset = header.code_offset;
|
code_offset = header.code_offset;
|
||||||
function_table_offset = header.function_table_offset;
|
function_table_offset = header.function_table_offset;
|
||||||
if (override_language != 0xFF) {
|
if (override_language != Language::UNKNOWN) {
|
||||||
language = override_language;
|
language = override_language;
|
||||||
} else if (header.language < 5) {
|
} else if (static_cast<size_t>(header.language) < 5) {
|
||||||
language = header.language;
|
language = header.language;
|
||||||
} else {
|
} else {
|
||||||
language = 1;
|
language = Language::ENGLISH;
|
||||||
}
|
}
|
||||||
lines.emplace_back(std::format(".quest_num {}", header.quest_number));
|
lines.emplace_back(std::format(".quest_num {}", header.quest_number));
|
||||||
lines.emplace_back(std::format(".language {}", header.language));
|
lines.emplace_back(std::format(".language {}", char_for_language(header.language)));
|
||||||
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
|
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
|
||||||
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
|
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
|
||||||
lines.emplace_back(".long_desc " + escape_string(header.long_description.decode(language)));
|
lines.emplace_back(".long_desc " + escape_string(header.long_description.decode(language)));
|
||||||
@@ -3016,15 +3068,15 @@ std::string disassemble_quest_script(
|
|||||||
const auto& header = r.get<PSOQuestHeaderPC>();
|
const auto& header = r.get<PSOQuestHeaderPC>();
|
||||||
code_offset = header.code_offset;
|
code_offset = header.code_offset;
|
||||||
function_table_offset = header.function_table_offset;
|
function_table_offset = header.function_table_offset;
|
||||||
if (override_language != 0xFF) {
|
if (override_language != Language::UNKNOWN) {
|
||||||
language = override_language;
|
language = override_language;
|
||||||
} else if (header.language < 8) {
|
} else if (static_cast<size_t>(header.language) < 8) {
|
||||||
language = header.language;
|
language = header.language;
|
||||||
} else {
|
} else {
|
||||||
language = 1;
|
language = Language::ENGLISH;
|
||||||
}
|
}
|
||||||
lines.emplace_back(std::format(".quest_num {}", header.quest_number));
|
lines.emplace_back(std::format(".quest_num {}", header.quest_number));
|
||||||
lines.emplace_back(std::format(".language {}", header.language));
|
lines.emplace_back(std::format(".language {}", char_for_language(header.language)));
|
||||||
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
|
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
|
||||||
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
|
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
|
||||||
lines.emplace_back(".long_desc " + escape_string(header.long_description.decode(language)));
|
lines.emplace_back(".long_desc " + escape_string(header.long_description.decode(language)));
|
||||||
@@ -3038,15 +3090,15 @@ std::string disassemble_quest_script(
|
|||||||
const auto& header = r.get<PSOQuestHeaderGC>();
|
const auto& header = r.get<PSOQuestHeaderGC>();
|
||||||
code_offset = header.code_offset;
|
code_offset = header.code_offset;
|
||||||
function_table_offset = header.function_table_offset;
|
function_table_offset = header.function_table_offset;
|
||||||
if (override_language != 0xFF) {
|
if (override_language != Language::UNKNOWN) {
|
||||||
language = override_language;
|
language = override_language;
|
||||||
} else if (header.language < 5) {
|
} else if (static_cast<size_t>(header.language) < 5) {
|
||||||
language = header.language;
|
language = header.language;
|
||||||
} else {
|
} else {
|
||||||
language = 1;
|
language = Language::ENGLISH;
|
||||||
}
|
}
|
||||||
lines.emplace_back(std::format(".quest_num {}", header.quest_number));
|
lines.emplace_back(std::format(".quest_num {}", header.quest_number));
|
||||||
lines.emplace_back(std::format(".language {}", header.language));
|
lines.emplace_back(std::format(".language {}", char_for_language(header.language)));
|
||||||
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
|
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
|
||||||
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
|
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
|
||||||
lines.emplace_back(".long_desc " + escape_string(header.long_description.decode(language)));
|
lines.emplace_back(".long_desc " + escape_string(header.long_description.decode(language)));
|
||||||
@@ -3054,13 +3106,13 @@ std::string disassemble_quest_script(
|
|||||||
}
|
}
|
||||||
case Version::BB_V4: {
|
case Version::BB_V4: {
|
||||||
use_wstrs = true;
|
use_wstrs = true;
|
||||||
const auto& header = r.get<PSOQuestHeaderBB>();
|
const auto& header = r.get<PSOQuestHeaderBBBase>();
|
||||||
code_offset = header.code_offset;
|
code_offset = header.code_offset;
|
||||||
function_table_offset = header.function_table_offset;
|
function_table_offset = header.function_table_offset;
|
||||||
if (override_language != 0xFF) {
|
if (override_language != Language::UNKNOWN) {
|
||||||
language = override_language;
|
language = override_language;
|
||||||
} else {
|
} else {
|
||||||
language = 1;
|
language = Language::ENGLISH;
|
||||||
}
|
}
|
||||||
lines.emplace_back(std::format(".quest_num {}", header.quest_number));
|
lines.emplace_back(std::format(".quest_num {}", header.quest_number));
|
||||||
lines.emplace_back(std::format(".episode {}", name_for_header_episode_number(header.episode)));
|
lines.emplace_back(std::format(".episode {}", name_for_header_episode_number(header.episode)));
|
||||||
@@ -3068,9 +3120,25 @@ std::string disassemble_quest_script(
|
|||||||
if (header.joinable) {
|
if (header.joinable) {
|
||||||
lines.emplace_back(".joinable");
|
lines.emplace_back(".joinable");
|
||||||
}
|
}
|
||||||
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
|
lines.emplace_back(std::format(".name {}", escape_string(header.name.decode(language))));
|
||||||
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
|
lines.emplace_back(std::format(".short_desc {}", escape_string(header.short_description.decode(language))));
|
||||||
lines.emplace_back(".long_desc " + escape_string(header.long_description.decode(language)));
|
lines.emplace_back(std::format(".long_desc {}", escape_string(header.long_description.decode(language))));
|
||||||
|
// Quests saved with Qedit may not have the full header, so only parse
|
||||||
|
// the full header if the code and function table offsets don't point to
|
||||||
|
// space within it
|
||||||
|
if ((header.code_offset >= sizeof(PSOQuestHeaderBB)) &&
|
||||||
|
(header.function_table_offset >= sizeof(PSOQuestHeaderBB))) {
|
||||||
|
r.go(0);
|
||||||
|
const auto& header = r.get<PSOQuestHeaderBB>();
|
||||||
|
for (size_t z = 0; z < header.create_item_mask_entries.size(); z++) {
|
||||||
|
const auto& qh_mask = header.create_item_mask_entries[z];
|
||||||
|
if (!qh_mask.is_valid()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
QuestMetadata::CreateItemMask qm_mask = qh_mask;
|
||||||
|
lines.emplace_back(std::format(".allow_create_item {}", qm_mask.str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -3317,7 +3385,7 @@ std::string disassemble_quest_script(
|
|||||||
case Type::FLOAT32: {
|
case Type::FLOAT32: {
|
||||||
float v = cmd_r.get_f32l();
|
float v = cmd_r.get_f32l();
|
||||||
if (def->flags & F_PUSH_ARG) {
|
if (def->flags & F_PUSH_ARG) {
|
||||||
arg_stack_values.emplace_back(ArgStackValue::Type::INT, as_type<uint32_t>(v));
|
arg_stack_values.emplace_back(ArgStackValue::Type::INT, std::bit_cast<uint32_t>(v));
|
||||||
}
|
}
|
||||||
dasm_arg = std::format("{:g}", v);
|
dasm_arg = std::format("{:g}", v);
|
||||||
break;
|
break;
|
||||||
@@ -3335,7 +3403,7 @@ std::string disassemble_quest_script(
|
|||||||
} else {
|
} else {
|
||||||
string s = cmd_r.get_cstr();
|
string s = cmd_r.get_cstr();
|
||||||
if (def->flags & F_PUSH_ARG) {
|
if (def->flags & F_PUSH_ARG) {
|
||||||
arg_stack_values.emplace_back(language ? tt_8859_to_utf8(s) : tt_sega_sjis_to_utf8(s));
|
arg_stack_values.emplace_back((language == Language::JAPANESE) ? tt_sega_sjis_to_utf8(s) : tt_8859_to_utf8(s));
|
||||||
}
|
}
|
||||||
dasm_arg = escape_string(s, encoding_for_language(language));
|
dasm_arg = escape_string(s, encoding_for_language(language));
|
||||||
}
|
}
|
||||||
@@ -3358,10 +3426,15 @@ std::string disassemble_quest_script(
|
|||||||
} else {
|
} else {
|
||||||
dasm_line += "... ";
|
dasm_line += "... ";
|
||||||
|
|
||||||
if (def->args.size() != arg_stack_values.size()) {
|
if (def->args.size() > arg_stack_values.size()) {
|
||||||
dasm_line += std::format("/* matching error: expected {} arguments, received {} arguments */",
|
dasm_line += std::format("/* matching error: expected {} arguments, received {} arguments */",
|
||||||
def->args.size(), arg_stack_values.size());
|
def->args.size(), arg_stack_values.size());
|
||||||
} else {
|
} else {
|
||||||
|
if (def->args.size() < arg_stack_values.size()) {
|
||||||
|
dasm_line += std::format("/* warning: expected {} arguments, received {} arguments */",
|
||||||
|
def->args.size(), arg_stack_values.size());
|
||||||
|
}
|
||||||
|
|
||||||
bool is_first_arg = true;
|
bool is_first_arg = true;
|
||||||
for (size_t z = 0; z < def->args.size(); z++) {
|
for (size_t z = 0; z < def->args.size(); z++) {
|
||||||
const auto& arg_def = def->args[z];
|
const auto& arg_def = def->args[z];
|
||||||
@@ -3442,7 +3515,7 @@ std::string disassemble_quest_script(
|
|||||||
dasm_arg = std::format("f{}", arg_value.as_int);
|
dasm_arg = std::format("f{}", arg_value.as_int);
|
||||||
break;
|
break;
|
||||||
case ArgStackValue::Type::INT:
|
case ArgStackValue::Type::INT:
|
||||||
dasm_arg = std::format("{:g}", as_type<float>(arg_value.as_int));
|
dasm_arg = std::format("{:g}", std::bit_cast<float>(arg_value.as_int));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
dasm_arg = "/* invalid-type */";
|
dasm_arg = "/* invalid-type */";
|
||||||
@@ -3686,7 +3759,7 @@ std::string disassemble_quest_script(
|
|||||||
phosg::StringReader r = cmd_r.sub(l->offset, size);
|
phosg::StringReader r = cmd_r.sub(l->offset, size);
|
||||||
lines.emplace_back(" // As VectorXYZTF");
|
lines.emplace_back(" // As VectorXYZTF");
|
||||||
while (r.remaining() >= sizeof(VectorXYZTF)) {
|
while (r.remaining() >= sizeof(VectorXYZTF)) {
|
||||||
size_t offset = l->offset + cmd_r.where();
|
size_t offset = l->offset + r.where();
|
||||||
const auto& e = r.get<VectorXYZTF>();
|
const auto& e = r.get<VectorXYZTF>();
|
||||||
lines.emplace_back(std::format(" {:04X} vector x={:g}, y={:g}, z={:g}, t={:g}", offset, e.x, e.y, e.z, e.t));
|
lines.emplace_back(std::format(" {:04X} vector x={:g}, y={:g}, z={:g}, t={:g}", offset, e.x, e.y, e.z, e.t));
|
||||||
}
|
}
|
||||||
@@ -4008,10 +4081,11 @@ AssembledQuestScript assemble_quest_script(
|
|||||||
string quest_short_desc;
|
string quest_short_desc;
|
||||||
string quest_long_desc;
|
string quest_long_desc;
|
||||||
int64_t quest_num = -1;
|
int64_t quest_num = -1;
|
||||||
uint8_t quest_language = 1;
|
Language quest_language = Language::ENGLISH;
|
||||||
Episode quest_episode = Episode::EP1;
|
Episode quest_episode = Episode::EP1;
|
||||||
uint8_t quest_max_players = 4;
|
uint8_t quest_max_players = 4;
|
||||||
bool quest_joinable = false;
|
bool quest_joinable = false;
|
||||||
|
std::vector<QuestMetadata::CreateItemMask> create_item_mask_entries;
|
||||||
for (const auto& line : lines) {
|
for (const auto& line : lines) {
|
||||||
if (line.text.empty()) {
|
if (line.text.empty()) {
|
||||||
continue;
|
continue;
|
||||||
@@ -4019,7 +4093,7 @@ AssembledQuestScript assemble_quest_script(
|
|||||||
wrap_exceptions_with_line_ref(line, [&]() -> void {
|
wrap_exceptions_with_line_ref(line, [&]() -> void {
|
||||||
if (line.text[0] == '.') {
|
if (line.text[0] == '.') {
|
||||||
if (line.text.starts_with(".include ")) {
|
if (line.text.starts_with(".include ")) {
|
||||||
// Nothing to do
|
// Nothing to do (see above)
|
||||||
} else if (line.text.starts_with(".version ")) {
|
} else if (line.text.starts_with(".version ")) {
|
||||||
string name = line.text.substr(9);
|
string name = line.text.substr(9);
|
||||||
phosg::strip_leading_whitespace(name);
|
phosg::strip_leading_whitespace(name);
|
||||||
@@ -4030,10 +4104,25 @@ AssembledQuestScript assemble_quest_script(
|
|||||||
quest_short_desc = phosg::parse_data_string(line.text.substr(12));
|
quest_short_desc = phosg::parse_data_string(line.text.substr(12));
|
||||||
} else if (line.text.starts_with(".long_desc ")) {
|
} else if (line.text.starts_with(".long_desc ")) {
|
||||||
quest_long_desc = phosg::parse_data_string(line.text.substr(11));
|
quest_long_desc = phosg::parse_data_string(line.text.substr(11));
|
||||||
|
} else if (line.text.starts_with(".allow_create_item ")) {
|
||||||
|
if (create_item_mask_entries.size() >= 0x40) {
|
||||||
|
throw std::runtime_error("too many .allow_create_item directives; at most 64 are allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
string args_str = line.text.substr(19);
|
||||||
|
phosg::strip_whitespace(args_str);
|
||||||
|
|
||||||
|
QuestMetadata::CreateItemMask mask(line.text.substr(19));
|
||||||
|
create_item_mask_entries.emplace_back(mask);
|
||||||
|
|
||||||
} else if (line.text.starts_with(".quest_num ")) {
|
} else if (line.text.starts_with(".quest_num ")) {
|
||||||
quest_num = stoul(line.text.substr(11), nullptr, 0);
|
quest_num = stoul(line.text.substr(11), nullptr, 0);
|
||||||
} else if (line.text.starts_with(".language ")) {
|
} else if (line.text.starts_with(".language ")) {
|
||||||
quest_language = stoul(line.text.substr(10), nullptr, 0);
|
auto code = line.text.substr(10);
|
||||||
|
if (code.size() != 1) {
|
||||||
|
throw runtime_error(".language directive argument is invalid");
|
||||||
|
}
|
||||||
|
quest_language = language_for_char(code[0]);
|
||||||
} else if (line.text.starts_with(".episode ")) {
|
} else if (line.text.starts_with(".episode ")) {
|
||||||
quest_episode = episode_for_token_name(line.text.substr(9));
|
quest_episode = episode_for_token_name(line.text.substr(9));
|
||||||
} else if (line.text.starts_with(".max_players ")) {
|
} else if (line.text.starts_with(".max_players ")) {
|
||||||
@@ -4264,7 +4353,12 @@ AssembledQuestScript assemble_quest_script(
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto line_tokens = phosg::split(line.text, ' ', 1);
|
auto line_tokens = phosg::split(line.text, ' ', 1);
|
||||||
const auto& opcode_def = opcodes.at(phosg::tolower(line_tokens.at(0)));
|
const QuestScriptOpcodeDefinition* opcode_def;
|
||||||
|
try {
|
||||||
|
opcode_def = opcodes.at(phosg::tolower(line_tokens.at(0)));
|
||||||
|
} catch (const out_of_range&) {
|
||||||
|
throw std::runtime_error(std::format("invalid opcode name: {}", line_tokens.at(0)));
|
||||||
|
}
|
||||||
|
|
||||||
bool use_args = version_has_args && (opcode_def->flags & F_ARGS);
|
bool use_args = version_has_args && (opcode_def->flags & F_ARGS);
|
||||||
if (!use_args) {
|
if (!use_args) {
|
||||||
@@ -4322,7 +4416,7 @@ AssembledQuestScript assemble_quest_script(
|
|||||||
case Version::GC_EP3_NTE:
|
case Version::GC_EP3_NTE:
|
||||||
case Version::GC_EP3:
|
case Version::GC_EP3:
|
||||||
case Version::XB_V3:
|
case Version::XB_V3:
|
||||||
code_w.write(bin ? text : (quest_language ? tt_utf8_to_8859(text) : tt_utf8_to_sega_sjis(text)));
|
code_w.write(bin ? text : ((quest_language == Language::JAPANESE) ? tt_utf8_to_sega_sjis(text) : tt_utf8_to_8859(text)));
|
||||||
code_w.put_u8(0);
|
code_w.put_u8(0);
|
||||||
break;
|
break;
|
||||||
case Version::PC_NTE:
|
case Version::PC_NTE:
|
||||||
@@ -4496,7 +4590,7 @@ AssembledQuestScript assemble_quest_script(
|
|||||||
code_w.put_u32l(stoll(arg, nullptr, 0));
|
code_w.put_u32l(stoll(arg, nullptr, 0));
|
||||||
break;
|
break;
|
||||||
case Type::FLOAT32:
|
case Type::FLOAT32:
|
||||||
code_w.put_u32l(stof(arg, nullptr));
|
code_w.put_f32l(stof(arg, nullptr));
|
||||||
break;
|
break;
|
||||||
case Type::CSTRING:
|
case Type::CSTRING:
|
||||||
if (arg.starts_with("bin:")) {
|
if (arg.starts_with("bin:")) {
|
||||||
@@ -4571,7 +4665,7 @@ AssembledQuestScript assemble_quest_script(
|
|||||||
header.code_offset = sizeof(header);
|
header.code_offset = sizeof(header);
|
||||||
header.function_table_offset = sizeof(header) + code_w.size();
|
header.function_table_offset = sizeof(header) + code_w.size();
|
||||||
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
|
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
|
||||||
header.name.encode(quest_name, 0);
|
header.name.encode(quest_name, Language::JAPANESE);
|
||||||
w.put(header);
|
w.put(header);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -4639,6 +4733,10 @@ AssembledQuestScript assemble_quest_script(
|
|||||||
header.name.encode(quest_name, quest_language);
|
header.name.encode(quest_name, quest_language);
|
||||||
header.short_description.encode(quest_short_desc, quest_language);
|
header.short_description.encode(quest_short_desc, quest_language);
|
||||||
header.long_description.encode(quest_long_desc, quest_language);
|
header.long_description.encode(quest_long_desc, quest_language);
|
||||||
|
header.unknown_a5.clear(0xFF);
|
||||||
|
for (size_t z = 0; z < create_item_mask_entries.size(); z++) {
|
||||||
|
header.create_item_mask_entries[z] = create_item_mask_entries[z];
|
||||||
|
}
|
||||||
w.put(header);
|
w.put(header);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -4662,7 +4760,7 @@ AssembledQuestScript assemble_quest_script(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void populate_quest_metadata_from_script(
|
void populate_quest_metadata_from_script(
|
||||||
QuestMetadata& meta, const void* data, size_t size, Version version, uint8_t language) {
|
QuestMetadata& meta, const void* data, size_t size, Version version, Language language) {
|
||||||
phosg::StringReader r(data, size);
|
phosg::StringReader r(data, size);
|
||||||
uint32_t code_offset = r.size();
|
uint32_t code_offset = r.size();
|
||||||
uint32_t function_table_offset = r.size();
|
uint32_t function_table_offset = r.size();
|
||||||
@@ -4746,7 +4844,7 @@ void populate_quest_metadata_from_script(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Version::BB_V4: {
|
case Version::BB_V4: {
|
||||||
const auto& header = r.get<PSOQuestHeaderBB>();
|
const auto& header = r.get<PSOQuestHeaderBBBase>();
|
||||||
meta.episode = episode_for_quest_episode_number(header.episode);
|
meta.episode = episode_for_quest_episode_number(header.episode);
|
||||||
meta.joinable |= header.joinable;
|
meta.joinable |= header.joinable;
|
||||||
meta.max_players = 4;
|
meta.max_players = 4;
|
||||||
@@ -4756,6 +4854,21 @@ void populate_quest_metadata_from_script(
|
|||||||
meta.name = header.name.decode(language);
|
meta.name = header.name.decode(language);
|
||||||
meta.short_description = header.short_description.decode(language);
|
meta.short_description = header.short_description.decode(language);
|
||||||
meta.long_description = header.long_description.decode(language);
|
meta.long_description = header.long_description.decode(language);
|
||||||
|
// Quests saved with Qedit may not have the full header, so only parse
|
||||||
|
// the full header if the code and function table offsets don't point to
|
||||||
|
// space within it
|
||||||
|
if ((header.code_offset >= sizeof(PSOQuestHeaderBB)) &&
|
||||||
|
(header.function_table_offset >= sizeof(PSOQuestHeaderBB))) {
|
||||||
|
r.go(0);
|
||||||
|
const auto& header = r.get<PSOQuestHeaderBB>();
|
||||||
|
for (size_t z = 0; z < header.create_item_mask_entries.size(); z++) {
|
||||||
|
const auto& item = header.create_item_mask_entries[z];
|
||||||
|
if (!item.is_valid()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
meta.create_item_mask_entries.emplace_back(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
code_offset = header.code_offset;
|
code_offset = header.code_offset;
|
||||||
function_table_offset = header.function_table_offset;
|
function_table_offset = header.function_table_offset;
|
||||||
break;
|
break;
|
||||||
@@ -5045,10 +5158,23 @@ void populate_quest_metadata_from_script(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 0x00C4: { // map_designate
|
||||||
|
uint32_t floor = regs.get(r.get_u8());
|
||||||
|
if (floor < meta.area_for_floor.size()) {
|
||||||
|
meta.area_for_floor[floor] = floor;
|
||||||
|
}
|
||||||
|
// phosg::fwrite_fmt(stderr, ">>> Trace: map_designate fa[{}]={}\n", floor, floor);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 0xF80D: { // map_designate_ex
|
case 0xF80D: { // map_designate_ex
|
||||||
uint8_t base_reg = r.get_u8();
|
uint8_t base_reg = r.get_u8();
|
||||||
meta.area_for_floor.at(regs.get(base_reg)) = regs.get(base_reg + 1);
|
uint32_t floor = regs.get(base_reg);
|
||||||
// phosg::fwrite_fmt(stderr, ">>> Trace: map_designate_ex fa[{}]={}\n", regs.get(base_reg), regs.get(base_reg + 1));
|
uint32_t area = regs.get(base_reg + 1);
|
||||||
|
if (floor < meta.area_for_floor.size()) {
|
||||||
|
meta.area_for_floor[floor] = area;
|
||||||
|
}
|
||||||
|
// phosg::fwrite_fmt(stderr, ">>> Trace: map_designate_ex fa[{}]={}\n", floor, area);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5192,7 +5318,10 @@ void populate_quest_metadata_from_script(
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 0xF824: // set_cmode_difficulty
|
case 0xF824: // set_cmode_difficulty
|
||||||
meta.challenge_difficulty = get_single_int32_arg();
|
meta.challenge_difficulty = static_cast<Difficulty>(get_single_int32_arg());
|
||||||
|
if (static_cast<size_t>(meta.challenge_difficulty) > 3) {
|
||||||
|
throw std::runtime_error("invalid challenge mode difficulty");
|
||||||
|
}
|
||||||
// phosg::fwrite_fmt(stderr, ">>> Trace: meta.challenge_difficulty = {}\n", meta.challenge_difficulty);
|
// phosg::fwrite_fmt(stderr, ">>> Trace: meta.challenge_difficulty = {}\n", meta.challenge_difficulty);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -5288,9 +5417,14 @@ void populate_quest_metadata_from_script(
|
|||||||
|
|
||||||
case 0xF951: { // bb_map_designate
|
case 0xF951: { // bb_map_designate
|
||||||
uint8_t floor = r.get_u8();
|
uint8_t floor = r.get_u8();
|
||||||
meta.area_for_floor.at(floor) = r.get_u8();
|
if (floor < meta.area_for_floor.size()) {
|
||||||
r.skip(3); // entities_list_type, vars.layout, vars.entities
|
meta.area_for_floor.at(floor) = r.get_u8();
|
||||||
// phosg::fwrite_fmt(stderr, ">>> Trace: bb_map_designate fa[{}]={}\n", floor, meta.area_for_floor.at(floor));
|
r.skip(3); // entities_list_type, vars.layout, vars.entities
|
||||||
|
// phosg::fwrite_fmt(stderr, ">>> Trace: bb_map_designate fa[{}]={}\n", floor, meta.area_for_floor.at(floor));
|
||||||
|
} else {
|
||||||
|
r.skip(4); // area, entities_list_type, vars.layout, vars.entities
|
||||||
|
// phosg::fwrite_fmt(stderr, ">>> Trace: bb_map_designate fa[{}]=(ignored)\n", floor);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-8
@@ -38,7 +38,7 @@ struct PSOQuestHeaderDC { // Same format for DC v1 and v2
|
|||||||
/* 0008 */ le_uint32_t size = 0;
|
/* 0008 */ le_uint32_t size = 0;
|
||||||
/* 000C */ le_uint16_t unknown_a1 = 0;
|
/* 000C */ le_uint16_t unknown_a1 = 0;
|
||||||
/* 000E */ le_uint16_t unknown_a2 = 0;
|
/* 000E */ le_uint16_t unknown_a2 = 0;
|
||||||
/* 0010 */ uint8_t language = 0;
|
/* 0010 */ Language language = Language::JAPANESE;
|
||||||
/* 0011 */ uint8_t unknown_a3 = 0;
|
/* 0011 */ uint8_t unknown_a3 = 0;
|
||||||
/* 0012 */ le_uint16_t quest_number = 0; // 0xFFFF for challenge quests
|
/* 0012 */ le_uint16_t quest_number = 0; // 0xFFFF for challenge quests
|
||||||
/* 0014 */ pstring<TextEncoding::MARKED, 0x20> name;
|
/* 0014 */ pstring<TextEncoding::MARKED, 0x20> name;
|
||||||
@@ -53,7 +53,7 @@ struct PSOQuestHeaderPC {
|
|||||||
/* 0008 */ le_uint32_t size = 0;
|
/* 0008 */ le_uint32_t size = 0;
|
||||||
/* 000C */ le_uint16_t unknown_a1 = 0;
|
/* 000C */ le_uint16_t unknown_a1 = 0;
|
||||||
/* 000E */ le_uint16_t unknown_a2 = 0;
|
/* 000E */ le_uint16_t unknown_a2 = 0;
|
||||||
/* 0010 */ uint8_t language = 0;
|
/* 0010 */ Language language = Language::JAPANESE;
|
||||||
/* 0011 */ uint8_t unknown_a3 = 0;
|
/* 0011 */ uint8_t unknown_a3 = 0;
|
||||||
/* 0012 */ le_uint16_t quest_number = 0; // 0xFFFF for challenge quests
|
/* 0012 */ le_uint16_t quest_number = 0; // 0xFFFF for challenge quests
|
||||||
/* 0014 */ pstring<TextEncoding::UTF16, 0x20> name;
|
/* 0014 */ pstring<TextEncoding::UTF16, 0x20> name;
|
||||||
@@ -70,7 +70,7 @@ struct PSOQuestHeaderGC {
|
|||||||
/* 0008 */ le_uint32_t size = 0;
|
/* 0008 */ le_uint32_t size = 0;
|
||||||
/* 000C */ le_uint16_t unknown_a1 = 0;
|
/* 000C */ le_uint16_t unknown_a1 = 0;
|
||||||
/* 000E */ le_uint16_t unknown_a2 = 0;
|
/* 000E */ le_uint16_t unknown_a2 = 0;
|
||||||
/* 0010 */ uint8_t language = 0;
|
/* 0010 */ Language language = Language::JAPANESE;
|
||||||
/* 0011 */ uint8_t unknown_a3 = 0;
|
/* 0011 */ uint8_t unknown_a3 = 0;
|
||||||
// Note: The GC client byteswaps this field, then loads it as a byte, so
|
// Note: The GC client byteswaps this field, then loads it as a byte, so
|
||||||
// technically the high byte of this is what the client uses as the quest
|
// technically the high byte of this is what the client uses as the quest
|
||||||
@@ -83,7 +83,21 @@ struct PSOQuestHeaderGC {
|
|||||||
/* 01D4 */
|
/* 01D4 */
|
||||||
} __packed_ws__(PSOQuestHeaderGC, 0x1D4);
|
} __packed_ws__(PSOQuestHeaderGC, 0x1D4);
|
||||||
|
|
||||||
struct PSOQuestHeaderBB {
|
struct CreateItemMaskEntry {
|
||||||
|
parray<le_int32_t, 12> data1_fields;
|
||||||
|
le_uint32_t present = 0;
|
||||||
|
le_uint32_t unknown_a3 = 0;
|
||||||
|
|
||||||
|
bool is_valid() const {
|
||||||
|
return (this->data1_fields[0] || this->data1_fields[1] || this->data1_fields[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateItemMaskEntry() = default;
|
||||||
|
CreateItemMaskEntry(const QuestMetadata::CreateItemMask& mask);
|
||||||
|
operator QuestMetadata::CreateItemMask() const;
|
||||||
|
} __packed_ws__(CreateItemMaskEntry, 0x38);
|
||||||
|
|
||||||
|
struct PSOQuestHeaderBBBase {
|
||||||
/* 0000 */ le_uint32_t code_offset = 0;
|
/* 0000 */ le_uint32_t code_offset = 0;
|
||||||
/* 0004 */ le_uint32_t function_table_offset = 0;
|
/* 0004 */ le_uint32_t function_table_offset = 0;
|
||||||
/* 0008 */ le_uint32_t size = 0;
|
/* 0008 */ le_uint32_t size = 0;
|
||||||
@@ -99,7 +113,13 @@ struct PSOQuestHeaderBB {
|
|||||||
/* 0058 */ pstring<TextEncoding::UTF16, 0x80> short_description;
|
/* 0058 */ pstring<TextEncoding::UTF16, 0x80> short_description;
|
||||||
/* 0158 */ pstring<TextEncoding::UTF16, 0x120> long_description;
|
/* 0158 */ pstring<TextEncoding::UTF16, 0x120> long_description;
|
||||||
/* 0398 */
|
/* 0398 */
|
||||||
} __packed_ws__(PSOQuestHeaderBB, 0x398);
|
} __packed_ws__(PSOQuestHeaderBBBase, 0x0398);
|
||||||
|
|
||||||
|
struct PSOQuestHeaderBB : PSOQuestHeaderBBBase {
|
||||||
|
/* 0398 */ parray<uint8_t, 0x94> unknown_a5;
|
||||||
|
/* 042C */ parray<CreateItemMaskEntry, 0x40> create_item_mask_entries;
|
||||||
|
/* 122C */
|
||||||
|
} __packed_ws__(PSOQuestHeaderBB, 0x122C);
|
||||||
|
|
||||||
void check_opcode_definitions();
|
void check_opcode_definitions();
|
||||||
|
|
||||||
@@ -109,7 +129,7 @@ std::string disassemble_quest_script(
|
|||||||
const void* data,
|
const void* data,
|
||||||
size_t size,
|
size_t size,
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t override_language = 0xFF,
|
Language override_language = Language::UNKNOWN,
|
||||||
bool reassembly_mode = false,
|
bool reassembly_mode = false,
|
||||||
bool use_qedit_names = false);
|
bool use_qedit_names = false);
|
||||||
|
|
||||||
@@ -117,7 +137,7 @@ struct AssembledQuestScript {
|
|||||||
std::string data;
|
std::string data;
|
||||||
int64_t quest_number = -1;
|
int64_t quest_number = -1;
|
||||||
Version version = Version::UNKNOWN;
|
Version version = Version::UNKNOWN;
|
||||||
uint8_t language = 0xFF;
|
Language language = Language::UNKNOWN;
|
||||||
Episode episode = Episode::NONE;
|
Episode episode = Episode::NONE;
|
||||||
bool joinable = false;
|
bool joinable = false;
|
||||||
uint8_t max_players = 0x00;
|
uint8_t max_players = 0x00;
|
||||||
@@ -130,4 +150,4 @@ AssembledQuestScript assemble_quest_script(
|
|||||||
const std::vector<std::string>& script_include_directories,
|
const std::vector<std::string>& script_include_directories,
|
||||||
const std::vector<std::string>& native_include_directories);
|
const std::vector<std::string>& native_include_directories);
|
||||||
|
|
||||||
void populate_quest_metadata_from_script(QuestMetadata& meta, const void* data, size_t size, Version version, uint8_t language);
|
void populate_quest_metadata_from_script(QuestMetadata& meta, const void* data, size_t size, Version version, Language language);
|
||||||
|
|||||||
+56
-52
@@ -236,12 +236,11 @@ RareItemSet::SpecCollection RareItemSet::ParsedRELData::as_collection() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RareItemSet::RareItemSet(const AFSArchive& afs, bool is_v1) {
|
RareItemSet::RareItemSet(const AFSArchive& afs, bool is_v1) {
|
||||||
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
for (GameMode mode : ALL_GAME_MODES_V4) {
|
||||||
for (GameMode mode : modes) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
|
|
||||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||||
try {
|
try {
|
||||||
size_t index = difficulty * 10 + section_id;
|
size_t index = static_cast<size_t>(difficulty) * 10 + section_id;
|
||||||
ParsedRELData rel(afs.get_reader(index), false, is_v1);
|
ParsedRELData rel(afs.get_reader(index), false, is_v1);
|
||||||
this->collections.emplace(
|
this->collections.emplace(
|
||||||
this->key_for_params(mode, Episode::EP1, difficulty, section_id),
|
this->key_for_params(mode, Episode::EP1, difficulty, section_id),
|
||||||
@@ -253,7 +252,7 @@ RareItemSet::RareItemSet(const AFSArchive& afs, bool is_v1) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string RareItemSet::gsl_entry_name_for_table(GameMode mode, Episode episode, uint8_t difficulty, uint8_t section_id) {
|
string RareItemSet::gsl_entry_name_for_table(GameMode mode, Episode episode, Difficulty difficulty, uint8_t section_id) {
|
||||||
return std::format("ItemRT{}{}{}{}.rel",
|
return std::format("ItemRT{}{}{}{}.rel",
|
||||||
((mode == GameMode::CHALLENGE) ? "c" : ""),
|
((mode == GameMode::CHALLENGE) ? "c" : ""),
|
||||||
((episode == Episode::EP2) ? "l" : ""),
|
((episode == Episode::EP2) ? "l" : ""),
|
||||||
@@ -262,11 +261,9 @@ string RareItemSet::gsl_entry_name_for_table(GameMode mode, Episode episode, uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
RareItemSet::RareItemSet(const GSLArchive& gsl, bool is_big_endian) {
|
RareItemSet::RareItemSet(const GSLArchive& gsl, bool is_big_endian) {
|
||||||
const array<Episode, 2> episodes = {Episode::EP1, Episode::EP2};
|
for (GameMode mode : ALL_GAME_MODES_V23) {
|
||||||
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
for (Episode episode : ALL_EPISODES_V3) {
|
||||||
for (GameMode mode : modes) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
for (Episode episode : episodes) {
|
|
||||||
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
|
|
||||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||||
try {
|
try {
|
||||||
string filename = this->gsl_entry_name_for_table(mode, episode, difficulty, section_id);
|
string filename = this->gsl_entry_name_for_table(mode, episode, difficulty, section_id);
|
||||||
@@ -285,15 +282,15 @@ RareItemSet::RareItemSet(const GSLArchive& gsl, bool is_big_endian) {
|
|||||||
RareItemSet::RareItemSet(const string& rel_data, bool is_big_endian) {
|
RareItemSet::RareItemSet(const string& rel_data, bool is_big_endian) {
|
||||||
// Tables are 0x280 bytes in size in this format, laid out sequentially
|
// Tables are 0x280 bytes in size in this format, laid out sequentially
|
||||||
phosg::StringReader r(rel_data);
|
phosg::StringReader r(rel_data);
|
||||||
array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
for (Episode episode : ALL_EPISODES_V4) {
|
||||||
for (size_t ep_index = 0; ep_index < episodes.size(); ep_index++) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
|
|
||||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||||
try {
|
try {
|
||||||
size_t index = (ep_index * 40) + difficulty * 10 + section_id;
|
size_t ep_index = (episode == Episode::EP1) ? 0 : ((episode == Episode::EP2) ? 1 : 2);
|
||||||
|
size_t index = (ep_index * 40) + static_cast<size_t>(difficulty) * 10 + section_id;
|
||||||
ParsedRELData rel(r.sub(0x280 * index, 0x280), is_big_endian, false);
|
ParsedRELData rel(r.sub(0x280 * index, 0x280), is_big_endian, false);
|
||||||
this->collections.emplace(
|
this->collections.emplace(
|
||||||
this->key_for_params(GameMode::NORMAL, episodes[ep_index], difficulty, section_id),
|
this->key_for_params(GameMode::NORMAL, episode, difficulty, section_id),
|
||||||
rel.as_collection());
|
rel.as_collection());
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
}
|
}
|
||||||
@@ -314,9 +311,9 @@ RareItemSet::RareItemSet(const phosg::JSON& json, shared_ptr<const ItemNameIndex
|
|||||||
Episode episode = episode_keys.at(episode_it.first);
|
Episode episode = episode_keys.at(episode_it.first);
|
||||||
|
|
||||||
for (const auto& difficulty_it : episode_it.second->as_dict()) {
|
for (const auto& difficulty_it : episode_it.second->as_dict()) {
|
||||||
static const unordered_map<string, uint8_t> difficulty_keys(
|
static const unordered_map<string, Difficulty> difficulty_keys(
|
||||||
{{"Normal", 0}, {"Hard", 1}, {"VeryHard", 2}, {"Ultimate", 3}});
|
{{"Normal", Difficulty::NORMAL}, {"Hard", Difficulty::HARD}, {"VeryHard", Difficulty::VERY_HARD}, {"Ultimate", Difficulty::ULTIMATE}});
|
||||||
uint8_t difficulty = difficulty_keys.at(difficulty_it.first);
|
Difficulty difficulty = difficulty_keys.at(difficulty_it.first);
|
||||||
|
|
||||||
for (const auto& section_id_it : difficulty_it.second->as_dict()) {
|
for (const auto& section_id_it : difficulty_it.second->as_dict()) {
|
||||||
uint8_t section_id = section_id_for_name(section_id_it.first);
|
uint8_t section_id = section_id_for_name(section_id_it.first);
|
||||||
@@ -385,7 +382,10 @@ RareItemSet::RareItemSet(const phosg::JSON& json, shared_ptr<const ItemNameIndex
|
|||||||
|
|
||||||
std::string RareItemSet::serialize_afs(bool is_v1) const {
|
std::string RareItemSet::serialize_afs(bool is_v1) const {
|
||||||
vector<string> files;
|
vector<string> files;
|
||||||
for (uint8_t difficulty = 0; difficulty < (is_v1 ? 3 : 4); difficulty++) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
|
if (is_v1 && (difficulty == Difficulty::ULTIMATE)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||||
ParsedRELData rel(this->get_collection(GameMode::NORMAL, Episode::EP1, difficulty, section_id));
|
ParsedRELData rel(this->get_collection(GameMode::NORMAL, Episode::EP1, difficulty, section_id));
|
||||||
files.emplace_back(rel.serialize(false, is_v1));
|
files.emplace_back(rel.serialize(false, is_v1));
|
||||||
@@ -397,9 +397,8 @@ std::string RareItemSet::serialize_afs(bool is_v1) const {
|
|||||||
std::string RareItemSet::serialize_gsl(bool big_endian) const {
|
std::string RareItemSet::serialize_gsl(bool big_endian) const {
|
||||||
unordered_map<string, string> files;
|
unordered_map<string, string> files;
|
||||||
|
|
||||||
static const std::array<Episode, 2> episodes = {Episode::EP1, Episode::EP2};
|
for (Episode episode : ALL_EPISODES_V3) {
|
||||||
for (Episode episode : episodes) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
|
||||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||||
try {
|
try {
|
||||||
string filename = this->gsl_entry_name_for_table(GameMode::NORMAL, episode, difficulty, section_id);
|
string filename = this->gsl_entry_name_for_table(GameMode::NORMAL, episode, difficulty, section_id);
|
||||||
@@ -412,7 +411,7 @@ std::string RareItemSet::serialize_gsl(bool big_endian) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||||
try {
|
try {
|
||||||
string filename = this->gsl_entry_name_for_table(GameMode::CHALLENGE, Episode::EP1, difficulty, section_id);
|
string filename = this->gsl_entry_name_for_table(GameMode::CHALLENGE, Episode::EP1, difficulty, section_id);
|
||||||
@@ -429,8 +428,9 @@ std::string RareItemSet::serialize_gsl(bool big_endian) const {
|
|||||||
string RareItemSet::serialize_html(
|
string RareItemSet::serialize_html(
|
||||||
GameMode mode,
|
GameMode mode,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
shared_ptr<const ItemNameIndex> name_index) const {
|
shared_ptr<const ItemNameIndex> name_index,
|
||||||
|
shared_ptr<const CommonItemSet> common_item_set) const {
|
||||||
|
|
||||||
struct ZoneTypes {
|
struct ZoneTypes {
|
||||||
const char* name;
|
const char* name;
|
||||||
@@ -683,7 +683,7 @@ string RareItemSet::serialize_html(
|
|||||||
blocks.emplace_back("</tr>");
|
blocks.emplace_back("</tr>");
|
||||||
};
|
};
|
||||||
|
|
||||||
auto add_specs_row = [&](const char* loc_name, bool is_box, const array<vector<ExpandedDrop>, 10>& specs_lists) -> void {
|
auto add_specs_row = [&](const EnemyTypeDefinition* type_def, const char* loc_name, bool is_box, const array<vector<ExpandedDrop>, 10>& specs_lists) -> void {
|
||||||
bool any_list_nonempty = false;
|
bool any_list_nonempty = false;
|
||||||
for (const auto& specs_list : specs_lists) {
|
for (const auto& specs_list : specs_lists) {
|
||||||
any_list_nonempty |= !specs_list.empty();
|
any_list_nonempty |= !specs_list.empty();
|
||||||
@@ -703,6 +703,16 @@ string RareItemSet::serialize_html(
|
|||||||
|
|
||||||
auto frac = phosg::reduce_fraction<uint64_t>(spec.probability, 0x100000000);
|
auto frac = phosg::reduce_fraction<uint64_t>(spec.probability, 0x100000000);
|
||||||
|
|
||||||
|
std::string exact_token = std::format("Exact rate: {} / {}", frac.first, frac.second);
|
||||||
|
if (common_item_set && type_def && type_def->rt_index != 0xFF) {
|
||||||
|
auto table = common_item_set->get_table(episode, mode, difficulty, section_id);
|
||||||
|
uint8_t dar = table->enemy_type_drop_probs.at(type_def->rt_index);
|
||||||
|
exact_token += std::format(" (DAR: {}%)", dar);
|
||||||
|
frac.first *= dar;
|
||||||
|
frac.second *= 100;
|
||||||
|
frac = phosg::reduce_fraction<uint64_t>(frac.first, frac.second);
|
||||||
|
}
|
||||||
|
|
||||||
ItemData example_item = spec.data;
|
ItemData example_item = spec.data;
|
||||||
if (example_item.can_be_encoded_in_rel_rare_table()) {
|
if (example_item.can_be_encoded_in_rel_rare_table()) {
|
||||||
// Apparently Return to Ragol has a patch that allows it to use the
|
// Apparently Return to Ragol has a patch that allows it to use the
|
||||||
@@ -726,11 +736,9 @@ string RareItemSet::serialize_html(
|
|||||||
|
|
||||||
float denom = static_cast<float>(frac.second) / static_cast<double>(frac.first);
|
float denom = static_cast<float>(frac.second) / static_cast<double>(frac.first);
|
||||||
string denom_token = (floor(denom) == denom)
|
string denom_token = (floor(denom) == denom)
|
||||||
? std::format("1 / {:g}", denom)
|
? std::format("1 / {:.0f}", denom)
|
||||||
: std::format("1 / %.02f", denom);
|
: std::format("1 / {:.02f}", denom);
|
||||||
tokens.emplace_back(std::format(
|
tokens.emplace_back(std::format("<span class=\"rate\" title=\"{}\">{}</span>", exact_token, denom_token));
|
||||||
"<span class=\"rate\" title=\"Exact rate: {} / {}\">{}</span>",
|
|
||||||
frac.first, frac.second, denom_token));
|
|
||||||
}
|
}
|
||||||
if (!blocks.empty()) {
|
if (!blocks.empty()) {
|
||||||
blocks.emplace_back(phosg::join(tokens, "<br />"));
|
blocks.emplace_back(phosg::join(tokens, "<br />"));
|
||||||
@@ -753,8 +761,8 @@ string RareItemSet::serialize_html(
|
|||||||
specs_lists[section_id] = this->get_enemy_specs(mode, episode, difficulty, section_id, rt_index);
|
specs_lists[section_id] = this->get_enemy_specs(mode, episode, difficulty, section_id, rt_index);
|
||||||
}
|
}
|
||||||
const auto& type_def = type_definition_for_enemy(type);
|
const auto& type_def = type_definition_for_enemy(type);
|
||||||
const char* name = (difficulty == 3 && type_def.ultimate_name) ? type_def.ultimate_name : type_def.in_game_name;
|
const char* name = (difficulty == Difficulty::ULTIMATE && type_def.ultimate_name) ? type_def.ultimate_name : type_def.in_game_name;
|
||||||
add_specs_row(name, false, specs_lists);
|
add_specs_row(&type_def, name, false, specs_lists);
|
||||||
}
|
}
|
||||||
for (uint8_t floor : zone_type.floors) {
|
for (uint8_t floor : zone_type.floors) {
|
||||||
const auto& floor_def = FloorDefinition::get(episode, floor);
|
const auto& floor_def = FloorDefinition::get(episode, floor);
|
||||||
@@ -766,7 +774,7 @@ string RareItemSet::serialize_html(
|
|||||||
specs_lists[section_id] = this->get_box_specs(mode, episode, difficulty, section_id, floor_def.drop_area_norm);
|
specs_lists[section_id] = this->get_box_specs(mode, episode, difficulty, section_id, floor_def.drop_area_norm);
|
||||||
}
|
}
|
||||||
auto loc_name = std::format("{} (box)", floor_def.in_game_name);
|
auto loc_name = std::format("{} (box)", floor_def.in_game_name);
|
||||||
add_specs_row(loc_name.c_str(), true, specs_lists);
|
add_specs_row(nullptr, loc_name.c_str(), true, specs_lists);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
blocks.emplace_back("</table></div></body></html>");
|
blocks.emplace_back("</table></div></body></html>");
|
||||||
@@ -776,13 +784,11 @@ string RareItemSet::serialize_html(
|
|||||||
|
|
||||||
phosg::JSON RareItemSet::json(shared_ptr<const ItemNameIndex> name_index) const {
|
phosg::JSON RareItemSet::json(shared_ptr<const ItemNameIndex> name_index) const {
|
||||||
auto modes_dict = phosg::JSON::dict();
|
auto modes_dict = phosg::JSON::dict();
|
||||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
for (const auto& mode : ALL_GAME_MODES_V4) {
|
||||||
for (const auto& mode : modes) {
|
|
||||||
auto episodes_dict = phosg::JSON::dict();
|
auto episodes_dict = phosg::JSON::dict();
|
||||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
for (const auto& episode : ALL_EPISODES_V4) {
|
||||||
for (const auto& episode : episodes) {
|
|
||||||
auto difficulty_dict = phosg::JSON::dict();
|
auto difficulty_dict = phosg::JSON::dict();
|
||||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
for (const auto& difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
auto section_id_dict = phosg::JSON::dict();
|
auto section_id_dict = phosg::JSON::dict();
|
||||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||||
auto collection_dict = phosg::JSON::dict();
|
auto collection_dict = phosg::JSON::dict();
|
||||||
@@ -875,7 +881,7 @@ void RareItemSet::print_collection(
|
|||||||
FILE* stream,
|
FILE* stream,
|
||||||
GameMode mode,
|
GameMode mode,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
uint8_t section_id,
|
uint8_t section_id,
|
||||||
shared_ptr<const ItemNameIndex> name_index) const {
|
shared_ptr<const ItemNameIndex> name_index) const {
|
||||||
const SpecCollection* collection;
|
const SpecCollection* collection;
|
||||||
@@ -919,11 +925,9 @@ void RareItemSet::print_collection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RareItemSet::print_all_collections(FILE* stream, std::shared_ptr<const ItemNameIndex> name_index) const {
|
void RareItemSet::print_all_collections(FILE* stream, std::shared_ptr<const ItemNameIndex> name_index) const {
|
||||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
for (GameMode mode : ALL_GAME_MODES_V4) {
|
||||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
for (Episode episode : ALL_EPISODES_V4) {
|
||||||
for (GameMode mode : modes) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
for (Episode episode : episodes) {
|
|
||||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
|
||||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||||
try {
|
try {
|
||||||
this->print_collection(stream, mode, episode, difficulty, section_id, name_index);
|
this->print_collection(stream, mode, episode, difficulty, section_id, name_index);
|
||||||
@@ -936,7 +940,7 @@ void RareItemSet::print_all_collections(FILE* stream, std::shared_ptr<const Item
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_enemy_specs(
|
std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_enemy_specs(
|
||||||
GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t rt_index) const {
|
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t rt_index) const {
|
||||||
try {
|
try {
|
||||||
return this->get_collection(mode, episode, difficulty, secid).rt_index_to_specs.at(rt_index);
|
return this->get_collection(mode, episode, difficulty, secid).rt_index_to_specs.at(rt_index);
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
@@ -946,7 +950,7 @@ std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_enemy_specs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_box_specs(
|
std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_box_specs(
|
||||||
GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t area_norm) const {
|
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t area_norm) const {
|
||||||
try {
|
try {
|
||||||
return this->get_collection(mode, episode, difficulty, secid).box_area_norm_to_specs.at(area_norm);
|
return this->get_collection(mode, episode, difficulty, secid).box_area_norm_to_specs.at(area_norm);
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
@@ -955,7 +959,7 @@ std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_box_specs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RareItemSet::has_entries_for_game_config(GameMode mode, Episode episode, uint8_t difficulty) const {
|
bool RareItemSet::has_entries_for_game_config(GameMode mode, Episode episode, Difficulty difficulty) const {
|
||||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||||
if (this->collections.count(this->key_for_params(mode, episode, difficulty, section_id))) {
|
if (this->collections.count(this->key_for_params(mode, episode, difficulty, section_id))) {
|
||||||
return true;
|
return true;
|
||||||
@@ -965,19 +969,19 @@ bool RareItemSet::has_entries_for_game_config(GameMode mode, Episode episode, ui
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RareItemSet::SpecCollection& RareItemSet::get_collection(
|
const RareItemSet::SpecCollection& RareItemSet::get_collection(
|
||||||
GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid) const {
|
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid) const {
|
||||||
return this->collections.at(this->key_for_params(mode, episode, difficulty, secid));
|
return this->collections.at(this->key_for_params(mode, episode, difficulty, secid));
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t RareItemSet::key_for_params(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid) {
|
uint16_t RareItemSet::key_for_params(GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid) {
|
||||||
if (difficulty > 3) {
|
if (static_cast<size_t>(difficulty) > 3) {
|
||||||
throw logic_error("incorrect difficulty");
|
throw logic_error("incorrect difficulty");
|
||||||
}
|
}
|
||||||
if (secid > 10) {
|
if (secid > 10) {
|
||||||
throw logic_error("incorrect section id");
|
throw logic_error("incorrect section id");
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t key = ((difficulty & 3) << 4) | (secid & 0x0F);
|
uint16_t key = ((static_cast<size_t>(difficulty) & 3) << 4) | (secid & 0x0F);
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case GameMode::NORMAL:
|
case GameMode::NORMAL:
|
||||||
break;
|
break;
|
||||||
|
|||||||
+11
-9
@@ -9,6 +9,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "AFSArchive.hh"
|
#include "AFSArchive.hh"
|
||||||
|
#include "CommonItemSet.hh"
|
||||||
#include "GSLArchive.hh"
|
#include "GSLArchive.hh"
|
||||||
#include "ItemNameIndex.hh"
|
#include "ItemNameIndex.hh"
|
||||||
#include "StaticGameData.hh"
|
#include "StaticGameData.hh"
|
||||||
@@ -33,19 +34,20 @@ public:
|
|||||||
~RareItemSet() = default;
|
~RareItemSet() = default;
|
||||||
|
|
||||||
std::vector<ExpandedDrop> get_enemy_specs(
|
std::vector<ExpandedDrop> get_enemy_specs(
|
||||||
GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t rt_index) const;
|
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t rt_index) const;
|
||||||
std::vector<ExpandedDrop> get_box_specs(
|
std::vector<ExpandedDrop> get_box_specs(
|
||||||
GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t area_norm) const;
|
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t area_norm) const;
|
||||||
|
|
||||||
bool has_entries_for_game_config(GameMode mode, Episode episode, uint8_t difficulty) const;
|
bool has_entries_for_game_config(GameMode mode, Episode episode, Difficulty difficulty) const;
|
||||||
|
|
||||||
std::string serialize_afs(bool is_v1) const;
|
std::string serialize_afs(bool is_v1) const;
|
||||||
std::string serialize_gsl(bool big_endian) const;
|
std::string serialize_gsl(bool big_endian) const;
|
||||||
std::string serialize_html(
|
std::string serialize_html(
|
||||||
GameMode mode,
|
GameMode mode,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
std::shared_ptr<const ItemNameIndex> name_index = nullptr,
|
||||||
|
std::shared_ptr<const CommonItemSet> common_item_set = nullptr) const;
|
||||||
phosg::JSON json(std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
phosg::JSON json(std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||||
|
|
||||||
void multiply_all_rates(double factor);
|
void multiply_all_rates(double factor);
|
||||||
@@ -54,7 +56,7 @@ public:
|
|||||||
FILE* stream,
|
FILE* stream,
|
||||||
GameMode mode,
|
GameMode mode,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
uint8_t section_id,
|
uint8_t section_id,
|
||||||
std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||||
void print_all_collections(FILE* stream, std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
void print_all_collections(FILE* stream, std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||||
@@ -111,10 +113,10 @@ protected:
|
|||||||
|
|
||||||
std::unordered_map<uint16_t, SpecCollection> collections;
|
std::unordered_map<uint16_t, SpecCollection> collections;
|
||||||
|
|
||||||
const SpecCollection& get_collection(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid) const;
|
const SpecCollection& get_collection(GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid) const;
|
||||||
|
|
||||||
static std::string gsl_entry_name_for_table(GameMode mode, Episode episode, uint8_t difficulty, uint8_t section_id);
|
static std::string gsl_entry_name_for_table(GameMode mode, Episode episode, Difficulty difficulty, uint8_t section_id);
|
||||||
static uint16_t key_for_params(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid);
|
static uint16_t key_for_params(GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid);
|
||||||
|
|
||||||
static uint32_t expand_rate(uint8_t pc);
|
static uint32_t expand_rate(uint8_t pc);
|
||||||
static uint8_t compress_rate(uint32_t probability);
|
static uint8_t compress_rate(uint32_t probability);
|
||||||
|
|||||||
+90
-57
@@ -347,7 +347,7 @@ static asio::awaitable<void> on_login_complete(shared_ptr<Client> c) {
|
|||||||
if (!q) {
|
if (!q) {
|
||||||
c->log.info_f("There is no quest to enable server function calls for specific version {:08X}", c->specific_version);
|
c->log.info_f("There is no quest to enable server function calls for specific version {:08X}", c->specific_version);
|
||||||
} else if (q) {
|
} else if (q) {
|
||||||
auto vq = q->version(c->version(), 1);
|
auto vq = q->version(c->version(), Language::ENGLISH);
|
||||||
if (vq) {
|
if (vq) {
|
||||||
c->set_flag(Client::Flag::HAS_SEND_FUNCTION_CALL);
|
c->set_flag(Client::Flag::HAS_SEND_FUNCTION_CALL);
|
||||||
c->set_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE);
|
c->set_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE);
|
||||||
@@ -363,7 +363,7 @@ static asio::awaitable<void> on_login_complete(shared_ptr<Client> c) {
|
|||||||
lobby_data.guild_card_number = c->login->account->account_id;
|
lobby_data.guild_card_number = c->login->account->account_id;
|
||||||
send_command_t(c, 0x64, 0x01, cmd);
|
send_command_t(c, 0x64, 0x01, cmd);
|
||||||
} else {
|
} else {
|
||||||
c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name);
|
c->log.info_f("Sending {} version of quest \"{}\"", name_for_language(vq->language), vq->meta.name);
|
||||||
string bin_filename = vq->bin_filename();
|
string bin_filename = vq->bin_filename();
|
||||||
string dat_filename = vq->dat_filename();
|
string dat_filename = vq->dat_filename();
|
||||||
string xb_filename = vq->xb_filename();
|
string xb_filename = vq->xb_filename();
|
||||||
@@ -613,7 +613,7 @@ static asio::awaitable<void> on_04_U(shared_ptr<Client> c, Channel::Message& msg
|
|||||||
for (const auto& file : index->all_files()) {
|
for (const auto& file : index->all_files()) {
|
||||||
send_patch_change_to_directory(c, path_directories, file->path_directories);
|
send_patch_change_to_directory(c, path_directories, file->path_directories);
|
||||||
|
|
||||||
S_FileChecksumRequest_Patch_0C req = {c->patch_file_checksum_requests.size(), {file->name, 1}};
|
S_FileChecksumRequest_Patch_0C req = {c->patch_file_checksum_requests.size(), {file->name, Language::ENGLISH}};
|
||||||
c->channel->send(0x0C, 0x00, req);
|
c->channel->send(0x0C, 0x00, req);
|
||||||
c->patch_file_checksum_requests.emplace_back(file);
|
c->patch_file_checksum_requests.emplace_back(file);
|
||||||
}
|
}
|
||||||
@@ -667,17 +667,17 @@ asio::awaitable<void> on_10_U(shared_ptr<Client> c, Channel::Message&) {
|
|||||||
if (req.needs_update()) {
|
if (req.needs_update()) {
|
||||||
send_patch_change_to_directory(c, path_directories, req.file->path_directories);
|
send_patch_change_to_directory(c, path_directories, req.file->path_directories);
|
||||||
|
|
||||||
S_OpenFile_Patch_06 open_cmd = {0, req.file->size, {req.file->name, 1}};
|
S_OpenFile_Patch_06 open_cmd = {0, req.file->size, {req.file->name, Language::ENGLISH}};
|
||||||
c->channel->send(0x06, 0x00, open_cmd);
|
c->channel->send(0x06, 0x00, open_cmd);
|
||||||
|
|
||||||
for (size_t x = 0; x < req.file->chunk_crcs.size(); x++) {
|
for (size_t x = 0; x < req.file->chunk_crcs.size(); x++) {
|
||||||
auto data = req.file->load_data();
|
auto data = req.file->load_data();
|
||||||
size_t chunk_size = min<uint32_t>(req.file->size - (x * 0x4000), 0x4000);
|
size_t chunk_size = min<uint32_t>(req.file->size - (x * PatchFileIndex::CHUNK_SIZE), PatchFileIndex::CHUNK_SIZE);
|
||||||
|
|
||||||
vector<pair<const void*, size_t>> blocks;
|
vector<pair<const void*, size_t>> blocks;
|
||||||
S_WriteFileHeader_Patch_07 cmd_header = {x, req.file->chunk_crcs[x], chunk_size};
|
S_WriteFileHeader_Patch_07 cmd_header = {x, req.file->chunk_crcs[x], chunk_size};
|
||||||
blocks.emplace_back(&cmd_header, sizeof(cmd_header));
|
blocks.emplace_back(&cmd_header, sizeof(cmd_header));
|
||||||
blocks.emplace_back(data->data() + (x * 0x4000), chunk_size);
|
blocks.emplace_back(data->data() + (x * PatchFileIndex::CHUNK_SIZE), chunk_size);
|
||||||
c->channel->send(0x07, 0x00, blocks);
|
c->channel->send(0x07, 0x00, blocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1490,7 +1490,7 @@ static asio::awaitable<void> on_93_BB(shared_ptr<Client> c, Channel::Message& ms
|
|||||||
c->set_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB);
|
c->set_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c->channel->language = c->check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB) ? 1 : base_cmd.language;
|
c->channel->language = c->check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB) ? Language::ENGLISH : base_cmd.language;
|
||||||
|
|
||||||
if (base_cmd.menu_id == MenuID::LOBBY) {
|
if (base_cmd.menu_id == MenuID::LOBBY) {
|
||||||
c->preferred_lobby_id = base_cmd.preferred_lobby_id;
|
c->preferred_lobby_id = base_cmd.preferred_lobby_id;
|
||||||
@@ -1849,7 +1849,14 @@ static void on_ep3_battle_table_state_updated(shared_ptr<Lobby> l, int16_t table
|
|||||||
|
|
||||||
static asio::awaitable<void> on_E4_Ep3(shared_ptr<Client> c, Channel::Message& msg) {
|
static asio::awaitable<void> on_E4_Ep3(shared_ptr<Client> c, Channel::Message& msg) {
|
||||||
const auto& cmd = check_size_t<C_CardBattleTableState_Ep3_E4>(msg.data);
|
const auto& cmd = check_size_t<C_CardBattleTableState_Ep3_E4>(msg.data);
|
||||||
auto l = c->require_lobby();
|
|
||||||
|
// This command can be received shortly after a proxy session closes if the
|
||||||
|
// player uses $exit while standing on a battle table pad. In that situation,
|
||||||
|
// the player will not be in any lobby, so we just ignore the command.
|
||||||
|
auto l = c->lobby.lock();
|
||||||
|
if (!l) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
if (cmd.seat_number >= 4) {
|
if (cmd.seat_number >= 4) {
|
||||||
throw runtime_error("invalid seat number");
|
throw runtime_error("invalid seat number");
|
||||||
@@ -2145,7 +2152,7 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
|
|||||||
auto s = c->require_server_state();
|
auto s = c->require_server_state();
|
||||||
|
|
||||||
switch (cmd.menu_id) {
|
switch (cmd.menu_id) {
|
||||||
case MenuID::QUEST_CATEGORIES_EP1:
|
case MenuID::QUEST_CATEGORIES_EP1_EP3_EP4:
|
||||||
case MenuID::QUEST_CATEGORIES_EP2:
|
case MenuID::QUEST_CATEGORIES_EP2:
|
||||||
// Don't send anything here. The quest filter menu already has short
|
// Don't send anything here. The quest filter menu already has short
|
||||||
// descriptions included with the entries, which the client shows in the
|
// descriptions included with the entries, which the client shows in the
|
||||||
@@ -2172,8 +2179,12 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MenuID::QUEST_EP3: {
|
case MenuID::QUEST_EP3: {
|
||||||
auto map = s->ep3_download_map_index->get(cmd.item_id);
|
auto vis_flag = (c->version() == Version::GC_EP3_NTE)
|
||||||
if (!map) {
|
? Episode3::MapIndex::VisibilityFlag::ONLINE_TRIAL
|
||||||
|
: Episode3::MapIndex::VisibilityFlag::ONLINE_FINAL;
|
||||||
|
|
||||||
|
auto map = s->ep3_map_index->map_for_id(cmd.item_id);
|
||||||
|
if (!map || !map->check_visibility_flag(vis_flag)) {
|
||||||
send_quest_info(c, "$C4Map does not exist.", 0x00, true);
|
send_quest_info(c, "$C4Map does not exist.", 0x00, true);
|
||||||
} else {
|
} else {
|
||||||
auto vm = map->version(c->language());
|
auto vm = map->version(c->language());
|
||||||
@@ -2230,7 +2241,7 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
|
|||||||
version_token,
|
version_token,
|
||||||
name_for_char_class(player->disp.visual.char_class),
|
name_for_char_class(player->disp.visual.char_class),
|
||||||
player->disp.stats.level + 1,
|
player->disp.stats.level + 1,
|
||||||
char_for_language_code(game_c->language()));
|
char_for_language(game_c->language()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2239,7 +2250,10 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
|
|||||||
// time, send page 2 (extended info)
|
// time, send page 2 (extended info)
|
||||||
if (info.empty()) {
|
if (info.empty()) {
|
||||||
c->last_game_info_requested = 0;
|
c->last_game_info_requested = 0;
|
||||||
info += std::format("Section ID: {}\n", name_for_section_id(game->effective_section_id()));
|
uint8_t effective_section_id = game->effective_section_id();
|
||||||
|
if (effective_section_id < 10) {
|
||||||
|
info += std::format("Section ID: {}\n", name_for_section_id(effective_section_id));
|
||||||
|
}
|
||||||
if (game->max_level != 0xFFFFFFFF) {
|
if (game->max_level != 0xFFFFFFFF) {
|
||||||
info += std::format("Req. level: {}-{}\n", game->min_level + 1, game->max_level + 1);
|
info += std::format("Req. level: {}-{}\n", game->min_level + 1, game->max_level + 1);
|
||||||
} else if (game->min_level != 0) {
|
} else if (game->min_level != 0) {
|
||||||
@@ -2255,7 +2269,7 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
|
|||||||
|
|
||||||
if (game->quest) {
|
if (game->quest) {
|
||||||
info += (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) ? "$C6Quest: " : "$C4Quest: ";
|
info += (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) ? "$C6Quest: " : "$C4Quest: ";
|
||||||
info += remove_color(game->quest->meta.name);
|
info += remove_color(game->quest->name_for_language(c->language()));
|
||||||
info += "\n";
|
info += "\n";
|
||||||
} else if (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
|
} else if (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
|
||||||
info += "$C6Quest in progress\n";
|
info += "$C6Quest in progress\n";
|
||||||
@@ -2461,7 +2475,7 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q, bool substi
|
|||||||
l->allowed_drop_modes = l->quest->meta.allowed_drop_modes;
|
l->allowed_drop_modes = l->quest->meta.allowed_drop_modes;
|
||||||
l->drop_mode = l->quest->meta.default_drop_mode;
|
l->drop_mode = l->quest->meta.default_drop_mode;
|
||||||
}
|
}
|
||||||
if (l->quest->meta.challenge_difficulty >= 0) {
|
if (l->quest->meta.challenge_difficulty != Difficulty::UNKNOWN) {
|
||||||
l->difficulty = l->quest->meta.challenge_difficulty;
|
l->difficulty = l->quest->meta.challenge_difficulty;
|
||||||
}
|
}
|
||||||
l->create_item_creator();
|
l->create_item_creator();
|
||||||
@@ -2481,7 +2495,7 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q, bool substi
|
|||||||
lc->channel->disconnect();
|
lc->channel->disconnect();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
lc->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name);
|
lc->log.info_f("Sending {} version of quest \"{}\"", name_for_language(vq->language), vq->meta.name);
|
||||||
|
|
||||||
string bin_filename = vq->bin_filename();
|
string bin_filename = vq->bin_filename();
|
||||||
string dat_filename = vq->dat_filename();
|
string dat_filename = vq->dat_filename();
|
||||||
@@ -2547,11 +2561,7 @@ static asio::awaitable<void> on_10_main_menu(shared_ptr<Client> c, uint32_t item
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MainMenuItemID::DOWNLOAD_QUESTS: {
|
case MainMenuItemID::DOWNLOAD_QUESTS: {
|
||||||
if (is_ep3(c->version())) {
|
send_quest_categories_menu(c, QuestMenuType::DOWNLOAD, Episode::NONE);
|
||||||
send_ep3_download_quest_menu(c);
|
|
||||||
} else {
|
|
||||||
send_quest_categories_menu(c, QuestMenuType::DOWNLOAD, Episode::NONE);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2801,27 +2811,32 @@ static asio::awaitable<void> on_10_game_menu(shared_ptr<Client> c, uint32_t item
|
|||||||
}
|
}
|
||||||
|
|
||||||
static asio::awaitable<void> on_10_quest_categories(shared_ptr<Client> c, uint32_t item_id) {
|
static asio::awaitable<void> on_10_quest_categories(shared_ptr<Client> c, uint32_t item_id) {
|
||||||
// Episode 3 doesn't have this menu
|
|
||||||
if (is_ep3(c->version())) {
|
if (is_ep3(c->version())) {
|
||||||
throw runtime_error("Episode 3 client made selection on quest categories menu");
|
auto s = c->require_server_state();
|
||||||
}
|
if (!s->ep3_map_index) {
|
||||||
|
send_lobby_message_box(c, "$C7Quests are not available.");
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
send_ep3_download_quest_menu(c, item_id);
|
||||||
|
|
||||||
auto s = c->require_server_state();
|
} else {
|
||||||
if (!s->quest_index) {
|
auto s = c->require_server_state();
|
||||||
send_lobby_message_box(c, "$C7Quests are not available.");
|
if (!s->quest_index) {
|
||||||
co_return;
|
send_lobby_message_box(c, "$C7Quests are not available.");
|
||||||
}
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
shared_ptr<Lobby> l = c->lobby.lock();
|
shared_ptr<Lobby> l = c->lobby.lock();
|
||||||
Episode episode = l ? l->episode : Episode::NONE;
|
Episode episode = l ? l->episode : Episode::NONE;
|
||||||
uint16_t version_flags = (1 << static_cast<size_t>(c->version())) | (l ? l->quest_version_flags() : 0);
|
uint16_t version_flags = (1 << static_cast<size_t>(c->version())) | (l ? l->quest_version_flags() : 0);
|
||||||
QuestIndex::IncludeCondition include_condition = nullptr;
|
QuestIndex::IncludeCondition include_condition = nullptr;
|
||||||
if (l && !c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
if (l && !c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||||
include_condition = l->quest_include_condition();
|
include_condition = l->quest_include_condition();
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& quests = s->quest_index->filter(episode, version_flags, item_id, include_condition);
|
const auto& quests = s->quest_index->filter(episode, version_flags, item_id, include_condition);
|
||||||
send_quest_menu(c, quests, !l);
|
send_quest_menu(c, quests, !l);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static asio::awaitable<void> on_10_quest_menu(shared_ptr<Client> c, uint32_t item_id) {
|
static asio::awaitable<void> on_10_quest_menu(shared_ptr<Client> c, uint32_t item_id) {
|
||||||
@@ -2888,10 +2903,19 @@ static asio::awaitable<void> on_10_ep3_download_quest_menu(shared_ptr<Client> c,
|
|||||||
if (c->lobby.lock()) {
|
if (c->lobby.lock()) {
|
||||||
throw runtime_error("Episode 3 quests can only be downloaded when client is not in a lobby");
|
throw runtime_error("Episode 3 quests can only be downloaded when client is not in a lobby");
|
||||||
}
|
}
|
||||||
auto map = s->ep3_download_map_index->get(item_id);
|
|
||||||
|
auto map = s->ep3_map_index->map_for_id(item_id);
|
||||||
|
|
||||||
|
auto vis_flag = (c->version() == Version::GC_EP3_NTE)
|
||||||
|
? Episode3::MapIndex::VisibilityFlag::ONLINE_TRIAL
|
||||||
|
: Episode3::MapIndex::VisibilityFlag::ONLINE_FINAL;
|
||||||
|
if (!map->check_visibility_flag(vis_flag)) {
|
||||||
|
throw runtime_error("map is not visible to this client");
|
||||||
|
}
|
||||||
|
|
||||||
auto vm = map->version(c->language());
|
auto vm = map->version(c->language());
|
||||||
auto name = vm->map->name.decode(vm->language);
|
auto name = vm->map->name.decode(vm->language);
|
||||||
string filename = std::format("m{:06}p_{:c}.bin", map->map_number, tolower(char_for_language_code(vm->language)));
|
string filename = std::format("m{:06}p_{:c}.bin", map->map_number, tolower(char_for_language(vm->language)));
|
||||||
auto data = (c->version() == Version::GC_EP3_NTE) ? vm->trial_download() : vm->compressed(false);
|
auto data = (c->version() == Version::GC_EP3_NTE) ? vm->trial_download() : vm->compressed(false);
|
||||||
send_open_quest_file(c, name, filename, "", map->map_number, QuestFileType::EPISODE_3, data);
|
send_open_quest_file(c, name, filename, "", map->map_number, QuestFileType::EPISODE_3, data);
|
||||||
co_return;
|
co_return;
|
||||||
@@ -3045,7 +3069,7 @@ static asio::awaitable<void> on_10(shared_ptr<Client> c, Channel::Message& msg)
|
|||||||
case MenuID::GAME:
|
case MenuID::GAME:
|
||||||
co_await on_10_game_menu(c, base_cmd.item_id, std::move(password));
|
co_await on_10_game_menu(c, base_cmd.item_id, std::move(password));
|
||||||
break;
|
break;
|
||||||
case MenuID::QUEST_CATEGORIES_EP1:
|
case MenuID::QUEST_CATEGORIES_EP1_EP3_EP4:
|
||||||
case MenuID::QUEST_CATEGORIES_EP2:
|
case MenuID::QUEST_CATEGORIES_EP2:
|
||||||
co_await on_10_quest_categories(c, base_cmd.item_id);
|
co_await on_10_quest_categories(c, base_cmd.item_id);
|
||||||
break;
|
break;
|
||||||
@@ -3728,7 +3752,7 @@ static asio::awaitable<void> on_E3_BB(shared_ptr<Client> c, Channel::Message& ms
|
|||||||
const auto& cmd = check_size_t<C_PlayerPreviewRequest_BB_E3>(msg.data);
|
const auto& cmd = check_size_t<C_PlayerPreviewRequest_BB_E3>(msg.data);
|
||||||
|
|
||||||
if (c->bb_connection_phase != 0x00) {
|
if (c->bb_connection_phase != 0x00) {
|
||||||
c->save_and_unload_character();
|
c->unload_character(false);
|
||||||
c->bb_character_index = cmd.character_index;
|
c->bb_character_index = cmd.character_index;
|
||||||
c->bb_bank_character_index = cmd.character_index;
|
c->bb_bank_character_index = cmd.character_index;
|
||||||
send_approve_player_choice_bb(c);
|
send_approve_player_choice_bb(c);
|
||||||
@@ -3740,18 +3764,18 @@ static asio::awaitable<void> on_E3_BB(shared_ptr<Client> c, Channel::Message& ms
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto send_preview = [&c](size_t index) -> void {
|
auto send_preview = [&c](size_t index) -> void {
|
||||||
c->save_and_unload_character();
|
c->unload_character(false);
|
||||||
c->bb_character_index = index;
|
c->bb_character_index = index;
|
||||||
c->bb_bank_character_index = index;
|
c->bb_bank_character_index = index;
|
||||||
try {
|
try {
|
||||||
auto preview = c->character_file()->to_preview();
|
auto preview = c->character_file()->to_preview();
|
||||||
send_player_preview_bb(c, c->bb_character_index, &preview);
|
send_player_preview_bb(c, c->bb_character_index, &preview);
|
||||||
|
|
||||||
} catch (const exception& e) {
|
} catch (const exception& e) {
|
||||||
// Player doesn't exist
|
// Player doesn't exist
|
||||||
c->log.warning_f("Can\'t load character data: {}", e.what());
|
c->log.info_f("Can\'t load character data: {}", e.what());
|
||||||
send_player_preview_bb(c, c->bb_character_index, nullptr);
|
send_player_preview_bb(c, c->bb_character_index, nullptr);
|
||||||
}
|
}
|
||||||
|
c->unload_character(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (msg.flag == 0) {
|
if (msg.flag == 0) {
|
||||||
@@ -4126,14 +4150,15 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
|
|||||||
if (!l->quest) {
|
if (!l->quest) {
|
||||||
throw runtime_error("challenge mode difficulty config command sent in non-challenge game");
|
throw runtime_error("challenge mode difficulty config command sent in non-challenge game");
|
||||||
}
|
}
|
||||||
if (static_cast<uint32_t>(l->quest->meta.challenge_difficulty) != cmd.difficulty) {
|
Difficulty cmd_difficulty = static_cast<Difficulty>(cmd.difficulty32.load());
|
||||||
|
if (l->quest->meta.challenge_difficulty != cmd_difficulty) {
|
||||||
throw runtime_error("incorrect difficulty level");
|
throw runtime_error("incorrect difficulty level");
|
||||||
}
|
}
|
||||||
if (l->difficulty != cmd.difficulty) {
|
if (l->difficulty != cmd_difficulty) {
|
||||||
l->difficulty = cmd.difficulty;
|
l->difficulty = cmd_difficulty;
|
||||||
l->create_item_creator();
|
l->create_item_creator();
|
||||||
}
|
}
|
||||||
l->log.info_f("(Challenge mode) Difficulty set to {:02X}", l->difficulty);
|
l->log.info_f("(Challenge mode) Difficulty set to {}", name_for_difficulty(l->difficulty));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4481,7 +4506,7 @@ shared_ptr<Lobby> create_game_generic(
|
|||||||
const std::string& password,
|
const std::string& password,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
GameMode mode,
|
GameMode mode,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
bool allow_v1,
|
bool allow_v1,
|
||||||
shared_ptr<Lobby> watched_lobby,
|
shared_ptr<Lobby> watched_lobby,
|
||||||
shared_ptr<Episode3::BattleRecordPlayer> battle_player) {
|
shared_ptr<Episode3::BattleRecordPlayer> battle_player) {
|
||||||
@@ -4493,7 +4518,7 @@ shared_ptr<Lobby> create_game_generic(
|
|||||||
throw invalid_argument("incorrect episode number");
|
throw invalid_argument("incorrect episode number");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (difficulty > 3) {
|
if (static_cast<size_t>(difficulty) > 3) {
|
||||||
throw invalid_argument("incorrect difficulty level");
|
throw invalid_argument("incorrect difficulty level");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4517,7 +4542,7 @@ shared_ptr<Lobby> create_game_generic(
|
|||||||
game->difficulty = difficulty;
|
game->difficulty = difficulty;
|
||||||
game->allowed_versions = s->compatibility_groups.at(static_cast<size_t>(creator_c->version()));
|
game->allowed_versions = s->compatibility_groups.at(static_cast<size_t>(creator_c->version()));
|
||||||
static_assert(NUM_VERSIONS == 14, "Don't forget to update the group compatibility restrictions");
|
static_assert(NUM_VERSIONS == 14, "Don't forget to update the group compatibility restrictions");
|
||||||
if (!allow_v1 || (difficulty > 2) || (mode == GameMode::CHALLENGE) || (mode == GameMode::SOLO)) {
|
if (!allow_v1 || (difficulty == Difficulty::ULTIMATE) || (mode == GameMode::CHALLENGE) || (mode == GameMode::SOLO)) {
|
||||||
game->forbid_version(Version::DC_NTE);
|
game->forbid_version(Version::DC_NTE);
|
||||||
game->forbid_version(Version::DC_11_2000);
|
game->forbid_version(Version::DC_11_2000);
|
||||||
game->forbid_version(Version::DC_V1);
|
game->forbid_version(Version::DC_V1);
|
||||||
@@ -4691,7 +4716,7 @@ shared_ptr<Lobby> create_game_generic(
|
|||||||
if (game->mode == GameMode::CHALLENGE) {
|
if (game->mode == GameMode::CHALLENGE) {
|
||||||
game->rare_enemy_rates = s->rare_enemy_rates_challenge;
|
game->rare_enemy_rates = s->rare_enemy_rates_challenge;
|
||||||
} else {
|
} else {
|
||||||
game->rare_enemy_rates = s->rare_enemy_rates_by_difficulty.at(game->difficulty);
|
game->rare_enemy_rates = s->rare_enemy_rates(game->difficulty);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game->episode != Episode::EP3) {
|
if (game->episode != Episode::EP3) {
|
||||||
@@ -4726,7 +4751,7 @@ shared_ptr<Lobby> create_game_generic(
|
|||||||
|
|
||||||
if (quest_flag_rewrites && !quest_flag_rewrites->empty()) {
|
if (quest_flag_rewrites && !quest_flag_rewrites->empty()) {
|
||||||
IntegralExpression::Env env = {
|
IntegralExpression::Env env = {
|
||||||
.flags = &p->quest_flags.data.at(difficulty),
|
.flags = &p->quest_flags.array(difficulty),
|
||||||
.challenge_records = &p->challenge_records,
|
.challenge_records = &p->challenge_records,
|
||||||
.team = creator_c->team(),
|
.team = creator_c->team(),
|
||||||
.num_players = 1,
|
.num_players = 1,
|
||||||
@@ -4778,7 +4803,15 @@ static asio::awaitable<void> on_0C_C1_E7_EC(shared_ptr<Client> c, Channel::Messa
|
|||||||
shared_ptr<Lobby> game;
|
shared_ptr<Lobby> game;
|
||||||
if (is_pre_v1(c->version())) {
|
if (is_pre_v1(c->version())) {
|
||||||
const auto& cmd = check_size_t<C_CreateGame_DCNTE>(msg.data);
|
const auto& cmd = check_size_t<C_CreateGame_DCNTE>(msg.data);
|
||||||
game = create_game_generic(s, c, cmd.name.decode(c->language()), cmd.password.decode(c->language()), Episode::EP1, GameMode::NORMAL, 0, true);
|
game = create_game_generic(
|
||||||
|
s,
|
||||||
|
c,
|
||||||
|
cmd.name.decode(c->language()),
|
||||||
|
cmd.password.decode(c->language()),
|
||||||
|
Episode::EP1,
|
||||||
|
GameMode::NORMAL,
|
||||||
|
Difficulty::NORMAL,
|
||||||
|
true);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const auto& cmd = check_size_t<C_CreateGame_DC_V3_0C_C1_Ep3_EC>(msg.data);
|
const auto& cmd = check_size_t<C_CreateGame_DC_V3_0C_C1_Ep3_EC>(msg.data);
|
||||||
@@ -4942,11 +4975,11 @@ static asio::awaitable<void> on_6F(shared_ptr<Client> c, Channel::Message& msg)
|
|||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
throw std::logic_error("cannot find patch enable quest after it was previously found during login");
|
throw std::logic_error("cannot find patch enable quest after it was previously found during login");
|
||||||
}
|
}
|
||||||
auto vq = q->version(is_ep3(c->version()) ? Version::GC_V3 : c->version(), 1);
|
auto vq = q->version(is_ep3(c->version()) ? Version::GC_V3 : c->version(), Language::ENGLISH);
|
||||||
if (!vq) {
|
if (!vq) {
|
||||||
throw std::logic_error("cannot find patch enable quest version after it was previously found during login");
|
throw std::logic_error("cannot find patch enable quest version after it was previously found during login");
|
||||||
}
|
}
|
||||||
c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name);
|
c->log.info_f("Sending {} version of quest \"{}\"", name_for_language(vq->language), vq->meta.name);
|
||||||
string bin_filename = vq->bin_filename();
|
string bin_filename = vq->bin_filename();
|
||||||
string dat_filename = vq->dat_filename();
|
string dat_filename = vq->dat_filename();
|
||||||
string xb_filename = vq->xb_filename();
|
string xb_filename = vq->xb_filename();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ std::shared_ptr<Lobby> create_game_generic(
|
|||||||
const std::string& password = "",
|
const std::string& password = "",
|
||||||
Episode episode = Episode::EP1,
|
Episode episode = Episode::EP1,
|
||||||
GameMode mode = GameMode::NORMAL,
|
GameMode mode = GameMode::NORMAL,
|
||||||
uint8_t difficulty = 0,
|
Difficulty difficulty = Difficulty::NORMAL,
|
||||||
bool allow_v1 = false,
|
bool allow_v1 = false,
|
||||||
std::shared_ptr<Lobby> watched_lobby = nullptr,
|
std::shared_ptr<Lobby> watched_lobby = nullptr,
|
||||||
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player = nullptr);
|
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player = nullptr);
|
||||||
|
|||||||
+301
-163
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,10 @@ G_SpecializableItemDropRequest_6xA2 normalize_drop_request(const void* data, siz
|
|||||||
|
|
||||||
struct DropReconcileResult {
|
struct DropReconcileResult {
|
||||||
std::shared_ptr<MapState::ObjectState> obj_st;
|
std::shared_ptr<MapState::ObjectState> obj_st;
|
||||||
std::shared_ptr<MapState::EnemyState> ene_st;
|
// The ref ene_st is the one the client referenced in the drop request; the target ene_st is the one actually used
|
||||||
|
// for drop computation (which may be the result of following an alias from the ref ene_st)
|
||||||
|
std::shared_ptr<MapState::EnemyState> ref_ene_st;
|
||||||
|
std::shared_ptr<MapState::EnemyState> target_ene_st;
|
||||||
uint8_t effective_rt_index;
|
uint8_t effective_rt_index;
|
||||||
bool should_drop;
|
bool should_drop;
|
||||||
bool ignore_def;
|
bool ignore_def;
|
||||||
@@ -29,6 +32,7 @@ DropReconcileResult reconcile_drop_request_with_map(
|
|||||||
std::shared_ptr<Client> c,
|
std::shared_ptr<Client> c,
|
||||||
G_SpecializableItemDropRequest_6xA2& cmd,
|
G_SpecializableItemDropRequest_6xA2& cmd,
|
||||||
Episode episode,
|
Episode episode,
|
||||||
|
Difficulty difficulty,
|
||||||
uint8_t event,
|
uint8_t event,
|
||||||
std::shared_ptr<MapState> map,
|
std::shared_ptr<MapState> map,
|
||||||
bool mark_drop);
|
bool mark_drop);
|
||||||
@@ -50,7 +54,7 @@ public:
|
|||||||
StatusEffectState attack_status_effect;
|
StatusEffectState attack_status_effect;
|
||||||
StatusEffectState defense_status_effect;
|
StatusEffectState defense_status_effect;
|
||||||
StatusEffectState unused_status_effect;
|
StatusEffectState unused_status_effect;
|
||||||
uint32_t language = 0;
|
Language language = Language::JAPANESE;
|
||||||
uint32_t player_tag = 0;
|
uint32_t player_tag = 0;
|
||||||
uint32_t guild_card_number = 0;
|
uint32_t guild_card_number = 0;
|
||||||
uint32_t unknown_a6 = 0;
|
uint32_t unknown_a6 = 0;
|
||||||
@@ -79,7 +83,7 @@ public:
|
|||||||
Parsed6x70Data(
|
Parsed6x70Data(
|
||||||
const G_SyncPlayerDispAndInventory_DC112000_6x70& cmd,
|
const G_SyncPlayerDispAndInventory_DC112000_6x70& cmd,
|
||||||
uint32_t guild_card_number,
|
uint32_t guild_card_number,
|
||||||
uint8_t language,
|
Language language,
|
||||||
Version from_version,
|
Version from_version,
|
||||||
bool from_client_customization);
|
bool from_client_customization);
|
||||||
Parsed6x70Data(
|
Parsed6x70Data(
|
||||||
@@ -108,7 +112,7 @@ public:
|
|||||||
G_SyncPlayerDispAndInventory_DC_PC_6x70 as_dc_pc(std::shared_ptr<ServerState> s, Version to_version) const;
|
G_SyncPlayerDispAndInventory_DC_PC_6x70 as_dc_pc(std::shared_ptr<ServerState> s, Version to_version) const;
|
||||||
G_SyncPlayerDispAndInventory_GC_6x70 as_gc_gcnte(std::shared_ptr<ServerState> s, Version to_version) const;
|
G_SyncPlayerDispAndInventory_GC_6x70 as_gc_gcnte(std::shared_ptr<ServerState> s, Version to_version) const;
|
||||||
G_SyncPlayerDispAndInventory_XB_6x70 as_xb(std::shared_ptr<ServerState> s) const;
|
G_SyncPlayerDispAndInventory_XB_6x70 as_xb(std::shared_ptr<ServerState> s) const;
|
||||||
G_SyncPlayerDispAndInventory_BB_6x70 as_bb(std::shared_ptr<ServerState> s, uint8_t language) const;
|
G_SyncPlayerDispAndInventory_BB_6x70 as_bb(std::shared_ptr<ServerState> s, Language language) const;
|
||||||
|
|
||||||
uint64_t default_xb_user_id() const;
|
uint64_t default_xb_user_id() const;
|
||||||
void clear_v1_unused_item_fields();
|
void clear_v1_unused_item_fields();
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ ReplaySession::Client::Client(shared_ptr<asio::io_context> io_context, uint64_t
|
|||||||
: id(id),
|
: id(id),
|
||||||
port(port),
|
port(port),
|
||||||
version(version),
|
version(version),
|
||||||
channel(make_shared<PeerChannel>(io_context, this->version, 1, std::format("R-{:X}", this->id))) {}
|
channel(make_shared<PeerChannel>(io_context, this->version, Language::ENGLISH, std::format("R-{:X}", this->id))) {}
|
||||||
|
|
||||||
string ReplaySession::Client::str() const {
|
string ReplaySession::Client::str() const {
|
||||||
return std::format("Client[{}, T-{}, {}]", this->id, this->port, phosg::name_for_enum(this->version));
|
return std::format("Client[{}, T-{}, {}]", this->id, this->port, phosg::name_for_enum(this->version));
|
||||||
|
|||||||
+32
-30
@@ -15,10 +15,10 @@ struct DefaultSymbolChatEntry {
|
|||||||
array<uint16_t, 4> corner_objects;
|
array<uint16_t, 4> corner_objects;
|
||||||
array<SymbolChatFacePart, 12> face_parts;
|
array<SymbolChatFacePart, 12> face_parts;
|
||||||
|
|
||||||
SaveFileSymbolChatEntryBB to_entry(uint8_t language) const {
|
SaveFileSymbolChatEntryBB to_entry(Language language) const {
|
||||||
SaveFileSymbolChatEntryBB ret;
|
SaveFileSymbolChatEntryBB ret;
|
||||||
ret.present = 1;
|
ret.present = 1;
|
||||||
ret.name.encode(this->language_to_name.at(language), language);
|
ret.name.encode(this->language_to_name.at(static_cast<size_t>(language)), language);
|
||||||
ret.spec.spec = this->spec;
|
ret.spec.spec = this->spec;
|
||||||
for (size_t z = 0; z < 4; z++) {
|
for (size_t z = 0; z < 4; z++) {
|
||||||
ret.spec.corner_objects[z] = this->corner_objects[z];
|
ret.spec.corner_objects[z] = this->corner_objects[z];
|
||||||
@@ -350,7 +350,7 @@ PlayerDispDataBBPreview PSOBBCharacterFile::to_preview() const {
|
|||||||
|
|
||||||
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_config(
|
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_config(
|
||||||
uint32_t guild_card_number,
|
uint32_t guild_card_number,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const PlayerVisualConfig& visual,
|
const PlayerVisualConfig& visual,
|
||||||
const std::string& name,
|
const std::string& name,
|
||||||
shared_ptr<const LevelTable> level_table) {
|
shared_ptr<const LevelTable> level_table) {
|
||||||
@@ -540,7 +540,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_config(
|
|||||||
|
|
||||||
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
|
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
|
||||||
uint32_t guild_card_number,
|
uint32_t guild_card_number,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const PlayerDispDataBBPreview& preview,
|
const PlayerDispDataBBPreview& preview,
|
||||||
shared_ptr<const LevelTable> level_table) {
|
shared_ptr<const LevelTable> level_table) {
|
||||||
return PSOBBCharacterFile::create_from_config(
|
return PSOBBCharacterFile::create_from_config(
|
||||||
@@ -550,13 +550,13 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
|
|||||||
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSODCNTECharacterFile::Character& src) {
|
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSODCNTECharacterFile::Character& src) {
|
||||||
auto ret = PSOBBCharacterFile::create_from_config(
|
auto ret = PSOBBCharacterFile::create_from_config(
|
||||||
src.guild_card.guild_card_number,
|
src.guild_card.guild_card_number,
|
||||||
0,
|
Language::JAPANESE,
|
||||||
src.disp.visual,
|
src.disp.visual,
|
||||||
src.disp.visual.name.decode(0),
|
src.disp.visual.name.decode(Language::JAPANESE),
|
||||||
nullptr);
|
nullptr);
|
||||||
ret->inventory = src.inventory;
|
ret->inventory = src.inventory;
|
||||||
ret->inventory.decode_from_client(Version::DC_V1);
|
ret->inventory.decode_from_client(Version::DC_V1);
|
||||||
uint8_t language = ret->inventory.language;
|
Language language = ret->inventory.language;
|
||||||
ret->disp = src.disp.to_bb(language, language);
|
ret->disp = src.disp.to_bb(language, language);
|
||||||
ret->validation_flags = 0;
|
ret->validation_flags = 0;
|
||||||
ret->creation_timestamp = src.creation_timestamp;
|
ret->creation_timestamp = src.creation_timestamp;
|
||||||
@@ -585,11 +585,11 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSODC1
|
|||||||
src.guild_card.guild_card_number,
|
src.guild_card.guild_card_number,
|
||||||
src.inventory.language,
|
src.inventory.language,
|
||||||
src.disp.visual,
|
src.disp.visual,
|
||||||
src.disp.visual.name.decode(0),
|
src.disp.visual.name.decode(Language::JAPANESE),
|
||||||
nullptr);
|
nullptr);
|
||||||
ret->inventory = src.inventory;
|
ret->inventory = src.inventory;
|
||||||
ret->inventory.decode_from_client(Version::DC_V1);
|
ret->inventory.decode_from_client(Version::DC_V1);
|
||||||
uint8_t language = ret->inventory.language;
|
Language language = ret->inventory.language;
|
||||||
ret->disp = src.disp.to_bb(language, language);
|
ret->disp = src.disp.to_bb(language, language);
|
||||||
ret->validation_flags = 0;
|
ret->validation_flags = 0;
|
||||||
ret->creation_timestamp = src.creation_timestamp;
|
ret->creation_timestamp = src.creation_timestamp;
|
||||||
@@ -628,11 +628,11 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSODCV
|
|||||||
src.guild_card.guild_card_number,
|
src.guild_card.guild_card_number,
|
||||||
src.inventory.language,
|
src.inventory.language,
|
||||||
src.disp.visual,
|
src.disp.visual,
|
||||||
src.disp.visual.name.decode(0),
|
src.disp.visual.name.decode(Language::JAPANESE),
|
||||||
nullptr);
|
nullptr);
|
||||||
ret->inventory = src.inventory;
|
ret->inventory = src.inventory;
|
||||||
ret->inventory.decode_from_client(Version::DC_V1);
|
ret->inventory.decode_from_client(Version::DC_V1);
|
||||||
uint8_t language = ret->inventory.language;
|
Language language = ret->inventory.language;
|
||||||
ret->disp = src.disp.to_bb(language, language);
|
ret->disp = src.disp.to_bb(language, language);
|
||||||
ret->validation_flags = src.validation_flags;
|
ret->validation_flags = src.validation_flags;
|
||||||
ret->creation_timestamp = src.creation_timestamp;
|
ret->creation_timestamp = src.creation_timestamp;
|
||||||
@@ -661,11 +661,11 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSODCV
|
|||||||
src.guild_card.guild_card_number,
|
src.guild_card.guild_card_number,
|
||||||
src.inventory.language,
|
src.inventory.language,
|
||||||
src.disp.visual,
|
src.disp.visual,
|
||||||
src.disp.visual.name.decode(0),
|
src.disp.visual.name.decode(Language::JAPANESE),
|
||||||
nullptr);
|
nullptr);
|
||||||
ret->inventory = src.inventory;
|
ret->inventory = src.inventory;
|
||||||
ret->inventory.decode_from_client(Version::DC_V2);
|
ret->inventory.decode_from_client(Version::DC_V2);
|
||||||
uint8_t language = ret->inventory.language;
|
Language language = ret->inventory.language;
|
||||||
ret->disp = src.disp.to_bb(language, language);
|
ret->disp = src.disp.to_bb(language, language);
|
||||||
ret->validation_flags = src.validation_flags;
|
ret->validation_flags = src.validation_flags;
|
||||||
ret->creation_timestamp = src.creation_timestamp;
|
ret->creation_timestamp = src.creation_timestamp;
|
||||||
@@ -701,14 +701,14 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOGCN
|
|||||||
src.guild_card.guild_card_number,
|
src.guild_card.guild_card_number,
|
||||||
src.inventory.language,
|
src.inventory.language,
|
||||||
src.disp.visual,
|
src.disp.visual,
|
||||||
src.disp.visual.name.decode(0),
|
src.disp.visual.name.decode(Language::JAPANESE),
|
||||||
nullptr);
|
nullptr);
|
||||||
ret->inventory = src.inventory;
|
ret->inventory = src.inventory;
|
||||||
// Note: We intentionally do not call ret->inventory.decode_from_client here.
|
// Note: We intentionally do not call ret->inventory.decode_from_client here.
|
||||||
// This is because the GC client byteswaps data2 in each item before sending
|
// This is because the GC client byteswaps data2 in each item before sending
|
||||||
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does
|
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does
|
||||||
// not do this, so the data2 fields are already in the correct order here.
|
// not do this, so the data2 fields are already in the correct order here.
|
||||||
uint8_t language = ret->inventory.language;
|
Language language = ret->inventory.language;
|
||||||
ret->disp = src.disp.to_bb(language, language);
|
ret->disp = src.disp.to_bb(language, language);
|
||||||
ret->validation_flags = src.validation_flags;
|
ret->validation_flags = src.validation_flags;
|
||||||
ret->creation_timestamp = src.creation_timestamp;
|
ret->creation_timestamp = src.creation_timestamp;
|
||||||
@@ -742,14 +742,14 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOGCC
|
|||||||
src.guild_card.guild_card_number,
|
src.guild_card.guild_card_number,
|
||||||
src.inventory.language,
|
src.inventory.language,
|
||||||
src.disp.visual,
|
src.disp.visual,
|
||||||
src.disp.visual.name.decode(0),
|
src.disp.visual.name.decode(Language::JAPANESE),
|
||||||
nullptr);
|
nullptr);
|
||||||
ret->inventory = src.inventory;
|
ret->inventory = src.inventory;
|
||||||
// Note: We intentionally do not call ret->inventory.decode_from_client here.
|
// Note: We intentionally do not call ret->inventory.decode_from_client here.
|
||||||
// This is because the GC client byteswaps data2 in each item before sending
|
// This is because the GC client byteswaps data2 in each item before sending
|
||||||
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does
|
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does
|
||||||
// not do this, so the data2 fields are already in the correct order here.
|
// not do this, so the data2 fields are already in the correct order here.
|
||||||
uint8_t language = ret->inventory.language;
|
Language language = ret->inventory.language;
|
||||||
ret->disp = src.disp.to_bb(language, language);
|
ret->disp = src.disp.to_bb(language, language);
|
||||||
ret->validation_flags = src.validation_flags;
|
ret->validation_flags = src.validation_flags;
|
||||||
ret->creation_timestamp = src.creation_timestamp;
|
ret->creation_timestamp = src.creation_timestamp;
|
||||||
@@ -793,10 +793,10 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOGCE
|
|||||||
src.guild_card.guild_card_number,
|
src.guild_card.guild_card_number,
|
||||||
src.inventory.language,
|
src.inventory.language,
|
||||||
src.disp.visual,
|
src.disp.visual,
|
||||||
src.disp.visual.name.decode(0),
|
src.disp.visual.name.decode(Language::JAPANESE),
|
||||||
nullptr);
|
nullptr);
|
||||||
ret->inventory = src.inventory;
|
ret->inventory = src.inventory;
|
||||||
uint8_t language = ret->inventory.language;
|
Language language = ret->inventory.language;
|
||||||
ret->disp = src.disp.to_bb(language, language);
|
ret->disp = src.disp.to_bb(language, language);
|
||||||
ret->validation_flags = src.validation_flags;
|
ret->validation_flags = src.validation_flags;
|
||||||
ret->creation_timestamp = src.creation_timestamp;
|
ret->creation_timestamp = src.creation_timestamp;
|
||||||
@@ -841,11 +841,11 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOXBC
|
|||||||
src.guild_card.guild_card_number,
|
src.guild_card.guild_card_number,
|
||||||
src.inventory.language,
|
src.inventory.language,
|
||||||
src.disp.visual,
|
src.disp.visual,
|
||||||
src.disp.visual.name.decode(0),
|
src.disp.visual.name.decode(Language::JAPANESE),
|
||||||
nullptr);
|
nullptr);
|
||||||
ret->inventory = src.inventory;
|
ret->inventory = src.inventory;
|
||||||
ret->inventory.decode_from_client(Version::XB_V3);
|
ret->inventory.decode_from_client(Version::XB_V3);
|
||||||
uint8_t language = ret->inventory.language;
|
Language language = ret->inventory.language;
|
||||||
ret->disp = src.disp.to_bb(language, language);
|
ret->disp = src.disp.to_bb(language, language);
|
||||||
ret->validation_flags = src.validation_flags;
|
ret->validation_flags = src.validation_flags;
|
||||||
ret->creation_timestamp = src.creation_timestamp;
|
ret->creation_timestamp = src.creation_timestamp;
|
||||||
@@ -886,7 +886,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOXBC
|
|||||||
}
|
}
|
||||||
|
|
||||||
PSODCNTECharacterFile::Character PSOBBCharacterFile::as_dc_nte(uint64_t hardware_id) const {
|
PSODCNTECharacterFile::Character PSOBBCharacterFile::as_dc_nte(uint64_t hardware_id) const {
|
||||||
uint8_t language = this->inventory.language;
|
Language language = this->inventory.language;
|
||||||
|
|
||||||
PSODCNTECharacterFile::Character ret;
|
PSODCNTECharacterFile::Character ret;
|
||||||
ret.inventory = this->inventory;
|
ret.inventory = this->inventory;
|
||||||
@@ -912,7 +912,7 @@ PSODCNTECharacterFile::Character PSOBBCharacterFile::as_dc_nte(uint64_t hardware
|
|||||||
}
|
}
|
||||||
|
|
||||||
PSODC112000CharacterFile::Character PSOBBCharacterFile::as_11_2000(uint64_t hardware_id) const {
|
PSODC112000CharacterFile::Character PSOBBCharacterFile::as_11_2000(uint64_t hardware_id) const {
|
||||||
uint8_t language = this->inventory.language;
|
Language language = this->inventory.language;
|
||||||
|
|
||||||
PSODC112000CharacterFile::Character ret;
|
PSODC112000CharacterFile::Character ret;
|
||||||
ret.inventory = this->inventory;
|
ret.inventory = this->inventory;
|
||||||
@@ -949,7 +949,7 @@ PSODC112000CharacterFile::Character PSOBBCharacterFile::as_11_2000(uint64_t hard
|
|||||||
}
|
}
|
||||||
|
|
||||||
PSOBBCharacterFile::operator PSODCV1CharacterFile::Character() const {
|
PSOBBCharacterFile::operator PSODCV1CharacterFile::Character() const {
|
||||||
uint8_t language = this->inventory.language;
|
Language language = this->inventory.language;
|
||||||
|
|
||||||
PSODCV1CharacterFile::Character ret;
|
PSODCV1CharacterFile::Character ret;
|
||||||
ret.inventory = this->inventory;
|
ret.inventory = this->inventory;
|
||||||
@@ -981,7 +981,7 @@ PSOBBCharacterFile::operator PSODCV1CharacterFile::Character() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PSOBBCharacterFile::operator PSODCV2CharacterFile::Character() const {
|
PSOBBCharacterFile::operator PSODCV2CharacterFile::Character() const {
|
||||||
uint8_t language = this->inventory.language;
|
Language language = this->inventory.language;
|
||||||
|
|
||||||
PSODCV2CharacterFile::Character ret;
|
PSODCV2CharacterFile::Character ret;
|
||||||
ret.inventory = this->inventory;
|
ret.inventory = this->inventory;
|
||||||
@@ -1022,7 +1022,7 @@ PSOBBCharacterFile::operator PSODCV2CharacterFile::Character() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PSOBBCharacterFile::operator PSOGCNTECharacterFileCharacter() const {
|
PSOBBCharacterFile::operator PSOGCNTECharacterFileCharacter() const {
|
||||||
uint8_t language = this->inventory.language;
|
Language language = this->inventory.language;
|
||||||
|
|
||||||
PSOGCNTECharacterFileCharacter ret;
|
PSOGCNTECharacterFileCharacter ret;
|
||||||
ret.inventory = this->inventory;
|
ret.inventory = this->inventory;
|
||||||
@@ -1060,7 +1060,7 @@ PSOBBCharacterFile::operator PSOGCNTECharacterFileCharacter() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PSOBBCharacterFile::operator PSOGCCharacterFile::Character() const {
|
PSOBBCharacterFile::operator PSOGCCharacterFile::Character() const {
|
||||||
uint8_t language = this->inventory.language;
|
Language language = this->inventory.language;
|
||||||
|
|
||||||
PSOGCCharacterFile::Character ret;
|
PSOGCCharacterFile::Character ret;
|
||||||
ret.inventory = this->inventory;
|
ret.inventory = this->inventory;
|
||||||
@@ -1108,7 +1108,7 @@ PSOBBCharacterFile::operator PSOGCCharacterFile::Character() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PSOBBCharacterFile::operator PSOXBCharacterFile::Character() const {
|
PSOBBCharacterFile::operator PSOXBCharacterFile::Character() const {
|
||||||
uint8_t language = this->inventory.language;
|
Language language = this->inventory.language;
|
||||||
|
|
||||||
PSOXBCharacterFile::Character ret;
|
PSOXBCharacterFile::Character ret;
|
||||||
ret.inventory = this->inventory;
|
ret.inventory = this->inventory;
|
||||||
@@ -1415,12 +1415,14 @@ void PSOBBCharacterFile::import_tethealla_material_usage(std::shared_ptr<const L
|
|||||||
this->set_material_usage(MaterialType::LUCK, luck);
|
this->set_material_usage(MaterialType::LUCK, luck);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PSOBBCharacterFile::recompute_stats(std::shared_ptr<const LevelTable> level_table) {
|
void PSOBBCharacterFile::recompute_stats(std::shared_ptr<const LevelTable> level_table, bool reset_exp) {
|
||||||
uint32_t level = this->disp.stats.level;
|
uint32_t level = this->disp.stats.level;
|
||||||
uint32_t exp = this->disp.stats.experience;
|
uint32_t exp = this->disp.stats.experience;
|
||||||
level_table->reset_to_base(this->disp.stats, this->disp.visual.char_class);
|
level_table->reset_to_base(this->disp.stats, this->disp.visual.char_class);
|
||||||
level_table->advance_to_level(this->disp.stats, level, this->disp.visual.char_class);
|
level_table->advance_to_level(this->disp.stats, level, this->disp.visual.char_class);
|
||||||
this->disp.stats.experience = exp;
|
if (!reset_exp) {
|
||||||
|
this->disp.stats.experience = exp;
|
||||||
|
}
|
||||||
|
|
||||||
this->disp.stats.char_stats.atp += (this->get_material_usage(MaterialType::POWER) * 2);
|
this->disp.stats.char_stats.atp += (this->get_material_usage(MaterialType::POWER) * 2);
|
||||||
this->disp.stats.char_stats.mst += (this->get_material_usage(MaterialType::MIND) * 2);
|
this->disp.stats.char_stats.mst += (this->get_material_usage(MaterialType::MIND) * 2);
|
||||||
|
|||||||
+11
-11
@@ -193,7 +193,7 @@ struct SaveFileChatShortcutEntryT {
|
|||||||
/* 40:54:A4 */
|
/* 40:54:A4 */
|
||||||
|
|
||||||
template <bool RetBE, TextEncoding RetEncoding, size_t RetMaxSize>
|
template <bool RetBE, TextEncoding RetEncoding, size_t RetMaxSize>
|
||||||
SaveFileChatShortcutEntryT<RetBE, RetEncoding, RetMaxSize> convert(uint8_t language) const {
|
SaveFileChatShortcutEntryT<RetBE, RetEncoding, RetMaxSize> convert(Language language) const {
|
||||||
SaveFileChatShortcutEntryT<RetBE, RetEncoding, RetMaxSize> ret;
|
SaveFileChatShortcutEntryT<RetBE, RetEncoding, RetMaxSize> ret;
|
||||||
ret.type = this->type;
|
ret.type = this->type;
|
||||||
switch (ret.type) {
|
switch (ret.type) {
|
||||||
@@ -268,7 +268,7 @@ struct PSOPCSystemFile { // PSO______COM
|
|||||||
// assumption that Sega didn't change much between versions.
|
// assumption that Sega didn't change much between versions.
|
||||||
/* 0004 */ le_int16_t music_volume = 0;
|
/* 0004 */ le_int16_t music_volume = 0;
|
||||||
/* 0006 */ int8_t sound_volume = 0;
|
/* 0006 */ int8_t sound_volume = 0;
|
||||||
/* 0007 */ uint8_t language = 1;
|
/* 0007 */ Language language = Language::ENGLISH;
|
||||||
/* 0008 */ le_int32_t server_time_delta_frames = 1728000;
|
/* 0008 */ le_int32_t server_time_delta_frames = 1728000;
|
||||||
/* 000C */ parray<le_uint16_t, 0x10> unknown_a4; // Last one is always 0x1234?
|
/* 000C */ parray<le_uint16_t, 0x10> unknown_a4; // Last one is always 0x1234?
|
||||||
/* 002C */ parray<uint8_t, 0x100> event_flags;
|
/* 002C */ parray<uint8_t, 0x100> event_flags;
|
||||||
@@ -281,7 +281,7 @@ struct PSOGCSystemFile {
|
|||||||
/* 0000 */ be_uint32_t checksum = 0;
|
/* 0000 */ be_uint32_t checksum = 0;
|
||||||
/* 0004 */ be_int16_t music_volume = 0; // 0 = full volume; -250 = min volume
|
/* 0004 */ be_int16_t music_volume = 0; // 0 = full volume; -250 = min volume
|
||||||
/* 0006 */ int8_t sound_volume = 0; // 0 = full volume; -100 = min volume
|
/* 0006 */ int8_t sound_volume = 0; // 0 = full volume; -100 = min volume
|
||||||
/* 0007 */ uint8_t language = 1;
|
/* 0007 */ Language language = Language::ENGLISH;
|
||||||
// This field stores the effective time zone offset between the server and
|
// This field stores the effective time zone offset between the server and
|
||||||
// client, in frames. The default value is 1728000, which corresponds to 16
|
// client, in frames. The default value is 1728000, which corresponds to 16
|
||||||
// hours. This is recomputed when the client receives a B1 command.
|
// hours. This is recomputed when the client receives a B1 command.
|
||||||
@@ -309,7 +309,7 @@ struct PSOXBSystemFile {
|
|||||||
/* 0000 */ le_uint32_t checksum = 0;
|
/* 0000 */ le_uint32_t checksum = 0;
|
||||||
/* 0004 */ le_int16_t music_volume = -50;
|
/* 0004 */ le_int16_t music_volume = -50;
|
||||||
/* 0006 */ int8_t sound_volume = 0;
|
/* 0006 */ int8_t sound_volume = 0;
|
||||||
/* 0007 */ uint8_t language = 0;
|
/* 0007 */ Language language = Language::JAPANESE;
|
||||||
/* 0008 */ be_int32_t server_time_delta_frames = 200;
|
/* 0008 */ be_int32_t server_time_delta_frames = 200;
|
||||||
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
|
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
|
||||||
/* 000E */ be_uint16_t surround_sound_enabled = 0;
|
/* 000E */ be_uint16_t surround_sound_enabled = 0;
|
||||||
@@ -338,7 +338,7 @@ struct PSOBBMinimalSystemFile {
|
|||||||
/* 0000 */ be_uint32_t checksum = 0;
|
/* 0000 */ be_uint32_t checksum = 0;
|
||||||
/* 0004 */ be_int16_t music_volume = 0;
|
/* 0004 */ be_int16_t music_volume = 0;
|
||||||
/* 0006 */ int8_t sound_volume = 0;
|
/* 0006 */ int8_t sound_volume = 0;
|
||||||
/* 0007 */ uint8_t language = 0;
|
/* 0007 */ Language language = Language::JAPANESE;
|
||||||
/* 0008 */ be_int32_t server_time_delta_frames = 1728000;
|
/* 0008 */ be_int32_t server_time_delta_frames = 1728000;
|
||||||
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
|
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
|
||||||
/* 000E */ be_uint16_t surround_sound_enabled = 0;
|
/* 000E */ be_uint16_t surround_sound_enabled = 0;
|
||||||
@@ -681,7 +681,7 @@ struct PSOGCEp3NTECharacter {
|
|||||||
/* 0430:0014 */ be_uint32_t save_count = 1;
|
/* 0430:0014 */ be_uint32_t save_count = 1;
|
||||||
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
|
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
|
||||||
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
|
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
|
||||||
/* 0460:0044 */ parray<uint8_t, 0x400> seq_vars;
|
/* 0460:0044 */ Ep3SeqVars seq_vars;
|
||||||
/* 0860:0444 */ be_uint32_t death_count = 0;
|
/* 0860:0444 */ be_uint32_t death_count = 0;
|
||||||
/* 0864:0448 */ PlayerBank200BE bank;
|
/* 0864:0448 */ PlayerBank200BE bank;
|
||||||
/* 1B2C:1710 */ GuildCardGCBE guild_card;
|
/* 1B2C:1710 */ GuildCardGCBE guild_card;
|
||||||
@@ -722,7 +722,7 @@ struct PSOGCEp3CharacterFile {
|
|||||||
// NPC decks are unlocked, and whether the player has a VIP card or not.
|
// NPC decks are unlocked, and whether the player has a VIP card or not.
|
||||||
// Logically, this structure maps to quest_flags in other versions, but is
|
// Logically, this structure maps to quest_flags in other versions, but is
|
||||||
// a different size.
|
// a different size.
|
||||||
/* 0460:0044 */ parray<uint8_t, 0x400> seq_vars;
|
/* 0460:0044 */ Ep3SeqVars seq_vars;
|
||||||
/* 0860:0444 */ be_uint32_t death_count = 0;
|
/* 0860:0444 */ be_uint32_t death_count = 0;
|
||||||
// Curiously, Episode 3 characters do have item banks, but there are only 4
|
// Curiously, Episode 3 characters do have item banks, but there are only 4
|
||||||
// item slots. Presumably Sega didn't completely remove the bank in Ep3
|
// item slots. Presumably Sega didn't completely remove the bank in Ep3
|
||||||
@@ -871,13 +871,13 @@ struct PSOBBCharacterFile {
|
|||||||
|
|
||||||
static std::shared_ptr<PSOBBCharacterFile> create_from_config(
|
static std::shared_ptr<PSOBBCharacterFile> create_from_config(
|
||||||
uint32_t guild_card_number,
|
uint32_t guild_card_number,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const PlayerVisualConfig& visual,
|
const PlayerVisualConfig& visual,
|
||||||
const std::string& name,
|
const std::string& name,
|
||||||
std::shared_ptr<const LevelTable> level_table);
|
std::shared_ptr<const LevelTable> level_table);
|
||||||
static std::shared_ptr<PSOBBCharacterFile> create_from_preview(
|
static std::shared_ptr<PSOBBCharacterFile> create_from_preview(
|
||||||
uint32_t guild_card_number,
|
uint32_t guild_card_number,
|
||||||
uint8_t language,
|
Language language,
|
||||||
const PlayerDispDataBBPreview& preview,
|
const PlayerDispDataBBPreview& preview,
|
||||||
std::shared_ptr<const LevelTable> level_table);
|
std::shared_ptr<const LevelTable> level_table);
|
||||||
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSODCNTECharacterFile::Character& src);
|
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSODCNTECharacterFile::Character& src);
|
||||||
@@ -920,7 +920,7 @@ struct PSOBBCharacterFile {
|
|||||||
void set_material_usage(MaterialType which, uint8_t usage);
|
void set_material_usage(MaterialType which, uint8_t usage);
|
||||||
void clear_all_material_usage();
|
void clear_all_material_usage();
|
||||||
void import_tethealla_material_usage(std::shared_ptr<const LevelTable> level_table);
|
void import_tethealla_material_usage(std::shared_ptr<const LevelTable> level_table);
|
||||||
void recompute_stats(std::shared_ptr<const LevelTable> level_table);
|
void recompute_stats(std::shared_ptr<const LevelTable> level_table, bool reset_exp = false);
|
||||||
} __packed_ws__(PSOBBCharacterFile, 0x2EA4);
|
} __packed_ws__(PSOBBCharacterFile, 0x2EA4);
|
||||||
|
|
||||||
struct PSOCHARFile {
|
struct PSOCHARFile {
|
||||||
@@ -978,7 +978,7 @@ struct PSODCV1V2GuildCardFile {
|
|||||||
/* 0004 */ parray<PSODCGuildCardFileEntry, 100> entries;
|
/* 0004 */ parray<PSODCGuildCardFileEntry, 100> entries;
|
||||||
/* 3204 */ le_int16_t music_volume = 0;
|
/* 3204 */ le_int16_t music_volume = 0;
|
||||||
/* 3206 */ int8_t sound_volume = 0;
|
/* 3206 */ int8_t sound_volume = 0;
|
||||||
/* 3207 */ uint8_t language = 1;
|
/* 3207 */ Language language = Language::ENGLISH;
|
||||||
/* 3208 */ le_int32_t server_time_delta_frames = 540000; // 648000 on DCv1
|
/* 3208 */ le_int32_t server_time_delta_frames = 540000; // 648000 on DCv1
|
||||||
/* 320C */ le_uint32_t creation_timestamp = 0;
|
/* 320C */ le_uint32_t creation_timestamp = 0;
|
||||||
/* 3210 */ le_uint32_t round2_seed = 0;
|
/* 3210 */ le_uint32_t round2_seed = 0;
|
||||||
|
|||||||
+121
-68
@@ -306,7 +306,7 @@ void send_set_guild_card_number(shared_ptr<Client> c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void send_patch_enter_directory(shared_ptr<Client> c, const string& dir) {
|
void send_patch_enter_directory(shared_ptr<Client> c, const string& dir) {
|
||||||
S_EnterDirectory_Patch_09 cmd = {{dir, 1}};
|
S_EnterDirectory_Patch_09 cmd = {{dir, Language::ENGLISH}};
|
||||||
c->channel->send(0x09, 0x00, cmd);
|
c->channel->send(0x09, 0x00, cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,16 +502,14 @@ void send_function_call(
|
|||||||
ch->send(0xB2, 0x00, data);
|
ch->send(0xB2, 0x00, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
asio::awaitable<bool> send_protected_command(
|
asio::awaitable<bool> send_protected_command(std::shared_ptr<Client> c, const void* data, size_t size, bool echo_to_lobby) {
|
||||||
std::shared_ptr<Client> c, const void* data, size_t size, bool echo_to_lobby) {
|
|
||||||
switch (c->version()) {
|
switch (c->version()) {
|
||||||
case Version::DC_NTE:
|
case Version::DC_NTE:
|
||||||
case Version::DC_11_2000:
|
case Version::DC_11_2000:
|
||||||
case Version::DC_V1:
|
case Version::DC_V1:
|
||||||
case Version::DC_V2:
|
case Version::DC_V2:
|
||||||
case Version::PC_NTE:
|
case Version::PC_NTE:
|
||||||
case Version::PC_V2:
|
case Version::PC_V2: {
|
||||||
case Version::GC_NTE: {
|
|
||||||
auto l = echo_to_lobby ? c->lobby.lock() : nullptr;
|
auto l = echo_to_lobby ? c->lobby.lock() : nullptr;
|
||||||
if (l) {
|
if (l) {
|
||||||
send_command(l, 0x60, 0x00, data, size);
|
send_command(l, 0x60, 0x00, data, size);
|
||||||
@@ -521,6 +519,7 @@ asio::awaitable<bool> send_protected_command(
|
|||||||
co_return true;
|
co_return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case Version::GC_NTE:
|
||||||
case Version::GC_V3:
|
case Version::GC_V3:
|
||||||
case Version::XB_V3:
|
case Version::XB_V3:
|
||||||
case Version::GC_EP3_NTE:
|
case Version::GC_EP3_NTE:
|
||||||
@@ -790,13 +789,17 @@ void send_approve_player_choice_bb(shared_ptr<Client> c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void send_complete_player_bb(shared_ptr<Client> c) {
|
void send_complete_player_bb(shared_ptr<Client> c) {
|
||||||
|
if (!c->login) {
|
||||||
|
throw std::logic_error("cannot send player for client who is not logged in");
|
||||||
|
}
|
||||||
|
|
||||||
auto p = c->character_file(true, false);
|
auto p = c->character_file(true, false);
|
||||||
auto sys = c->system_file(true);
|
auto sys = c->system_file(true);
|
||||||
auto team = c->team();
|
auto team = c->team();
|
||||||
if (c->check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) {
|
if (c->check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) {
|
||||||
p->inventory.language = 1;
|
p->inventory.language = Language::ENGLISH;
|
||||||
p->guild_card.language = 1;
|
p->guild_card.language = Language::ENGLISH;
|
||||||
sys->language = 1;
|
sys->language = Language::ENGLISH;
|
||||||
}
|
}
|
||||||
|
|
||||||
SC_SyncSaveFiles_BB_E7 cmd;
|
SC_SyncSaveFiles_BB_E7 cmd;
|
||||||
@@ -806,6 +809,8 @@ void send_complete_player_bb(shared_ptr<Client> c) {
|
|||||||
cmd.team_membership = team->full_membership_for_member(c->login->account->account_id);
|
cmd.team_membership = team->full_membership_for_member(c->login->account->account_id);
|
||||||
}
|
}
|
||||||
send_command_t(c, 0x00E7, 0x00000000, cmd);
|
send_command_t(c, 0x00E7, 0x00000000, cmd);
|
||||||
|
|
||||||
|
c->login->account->last_player_name = p->disp.name.decode(p->inventory.language);
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -1017,7 +1022,7 @@ void send_text_or_scrolling_message(shared_ptr<ServerState> s, const std::string
|
|||||||
|
|
||||||
string prepare_chat_data(
|
string prepare_chat_data(
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
uint8_t from_client_id,
|
uint8_t from_client_id,
|
||||||
const string& from_name,
|
const string& from_name,
|
||||||
const string& text,
|
const string& text,
|
||||||
@@ -1025,7 +1030,7 @@ string prepare_chat_data(
|
|||||||
string data;
|
string data;
|
||||||
|
|
||||||
if (version == Version::BB_V4) {
|
if (version == Version::BB_V4) {
|
||||||
data.append(language ? "\tE" : "\tJ");
|
data.append((language == Language::JAPANESE) ? "\tJ" : "\tE");
|
||||||
}
|
}
|
||||||
data.append(from_name);
|
data.append(from_name);
|
||||||
if (version == Version::DC_NTE) {
|
if (version == Version::DC_NTE) {
|
||||||
@@ -1038,7 +1043,7 @@ string prepare_chat_data(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uses_utf16(version)) {
|
if (uses_utf16(version)) {
|
||||||
data.append(language ? "\tE" : "\tJ");
|
data.append((language == Language::JAPANESE) ? "\tJ" : "\tE");
|
||||||
data.append(text);
|
data.append(text);
|
||||||
return tt_utf8_to_utf16(data);
|
return tt_utf8_to_utf16(data);
|
||||||
} else if (version == Version::DC_NTE) {
|
} else if (version == Version::DC_NTE) {
|
||||||
@@ -1292,7 +1297,7 @@ void send_guild_card_dc_pc_gc_t(
|
|||||||
uint32_t guild_card_number,
|
uint32_t guild_card_number,
|
||||||
const string& name,
|
const string& name,
|
||||||
const string& description,
|
const string& description,
|
||||||
uint8_t language,
|
Language language,
|
||||||
uint8_t section_id,
|
uint8_t section_id,
|
||||||
uint8_t char_class) {
|
uint8_t char_class) {
|
||||||
CmdT cmd;
|
CmdT cmd;
|
||||||
@@ -1316,7 +1321,7 @@ void send_guild_card_xb(
|
|||||||
uint64_t xb_user_id,
|
uint64_t xb_user_id,
|
||||||
const string& name,
|
const string& name,
|
||||||
const string& description,
|
const string& description,
|
||||||
uint8_t language,
|
Language language,
|
||||||
uint8_t section_id,
|
uint8_t section_id,
|
||||||
uint8_t char_class) {
|
uint8_t char_class) {
|
||||||
G_SendGuildCard_XB_6x06 cmd;
|
G_SendGuildCard_XB_6x06 cmd;
|
||||||
@@ -1342,7 +1347,7 @@ static void send_guild_card_bb(
|
|||||||
const string& name,
|
const string& name,
|
||||||
const string& team_name,
|
const string& team_name,
|
||||||
const string& description,
|
const string& description,
|
||||||
uint8_t language,
|
Language language,
|
||||||
uint8_t section_id,
|
uint8_t section_id,
|
||||||
uint8_t char_class) {
|
uint8_t char_class) {
|
||||||
G_SendGuildCard_BB_6x06 cmd;
|
G_SendGuildCard_BB_6x06 cmd;
|
||||||
@@ -1367,7 +1372,7 @@ void send_guild_card(
|
|||||||
const string& name,
|
const string& name,
|
||||||
const string& team_name,
|
const string& team_name,
|
||||||
const string& description,
|
const string& description,
|
||||||
uint8_t language,
|
Language language,
|
||||||
uint8_t section_id,
|
uint8_t section_id,
|
||||||
uint8_t char_class) {
|
uint8_t char_class) {
|
||||||
switch (ch->version) {
|
switch (ch->version) {
|
||||||
@@ -1574,7 +1579,7 @@ void send_game_menu_t(
|
|||||||
auto& e = entries.emplace_back();
|
auto& e = entries.emplace_back();
|
||||||
e.menu_id = MenuID::GAME;
|
e.menu_id = MenuID::GAME;
|
||||||
e.item_id = l->lobby_id;
|
e.item_id = l->lobby_id;
|
||||||
e.difficulty_tag = (is_ep3(c->version()) ? 0x0A : (l->difficulty + 0x22));
|
e.difficulty_tag = (is_ep3(c->version()) ? 0x0A : (static_cast<size_t>(l->difficulty) + 0x22));
|
||||||
e.num_players = l->count_clients();
|
e.num_players = l->count_clients();
|
||||||
if (is_dc(c->version())) {
|
if (is_dc(c->version())) {
|
||||||
e.episode = l->version_is_allowed(Version::DC_V1) ? 1 : 0;
|
e.episode = l->version_is_allowed(Version::DC_V1) ? 1 : 0;
|
||||||
@@ -1675,20 +1680,6 @@ void send_quest_menu_bb(
|
|||||||
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
|
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_ep3_download_quest_menu(shared_ptr<Client> c) {
|
|
||||||
auto s = c->require_server_state();
|
|
||||||
vector<S_QuestMenuEntry_DC_GC_A2_A4> entries;
|
|
||||||
for (const auto& it : s->ep3_download_map_index->all()) {
|
|
||||||
auto vm = it.second->version(c->language());
|
|
||||||
auto& e = entries.emplace_back();
|
|
||||||
e.menu_id = MenuID::QUEST_EP3;
|
|
||||||
e.item_id = it.first; // map_number
|
|
||||||
e.name.encode(vm->map->name.decode(vm->language), c->language());
|
|
||||||
e.short_description.encode(add_color(vm->map->location_name.decode(vm->language)), c->language());
|
|
||||||
}
|
|
||||||
send_command_vt(c, 0xA4, entries.size(), entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename EntryT>
|
template <typename EntryT>
|
||||||
void send_quest_categories_menu_t(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
|
void send_quest_categories_menu_t(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
|
||||||
QuestIndex::IncludeCondition include_condition = nullptr;
|
QuestIndex::IncludeCondition include_condition = nullptr;
|
||||||
@@ -1707,7 +1698,7 @@ void send_quest_categories_menu_t(shared_ptr<Client> c, QuestMenuType menu_type,
|
|||||||
auto s = c->require_server_state();
|
auto s = c->require_server_state();
|
||||||
for (const auto& cat : s->quest_index->categories(menu_type, episode, version_flags, include_condition)) {
|
for (const auto& cat : s->quest_index->categories(menu_type, episode, version_flags, include_condition)) {
|
||||||
auto& e = entries.emplace_back();
|
auto& e = entries.emplace_back();
|
||||||
e.menu_id = cat->use_ep2_icon() ? MenuID::QUEST_CATEGORIES_EP2 : MenuID::QUEST_CATEGORIES_EP1;
|
e.menu_id = cat->use_ep2_icon() ? MenuID::QUEST_CATEGORIES_EP2 : MenuID::QUEST_CATEGORIES_EP1_EP3_EP4;
|
||||||
e.item_id = cat->category_id;
|
e.item_id = cat->category_id;
|
||||||
e.name.encode(cat->name, c->language());
|
e.name.encode(cat->name, c->language());
|
||||||
e.short_description.encode(add_color(cat->description), c->language());
|
e.short_description.encode(add_color(cat->description), c->language());
|
||||||
@@ -1717,6 +1708,58 @@ void send_quest_categories_menu_t(shared_ptr<Client> c, QuestMenuType menu_type,
|
|||||||
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
|
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void send_ep3_download_quest_categories_menu(shared_ptr<Client> c) {
|
||||||
|
if (c->lobby.lock()) {
|
||||||
|
throw std::runtime_error("cannot send Ep3 download quest menu to client in a lobby");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto vis_flag = (c->version() == Version::GC_EP3_NTE)
|
||||||
|
? Episode3::MapIndex::VisibilityFlag::DOWNLOAD_TRIAL
|
||||||
|
: Episode3::MapIndex::VisibilityFlag::DOWNLOAD_FINAL;
|
||||||
|
|
||||||
|
vector<S_QuestMenuEntry_DC_GC_A2_A4> entries;
|
||||||
|
auto s = c->require_server_state();
|
||||||
|
for (const auto& [_, cat] : s->ep3_map_index->all_categories()) {
|
||||||
|
if (cat->check_visibility_flag(vis_flag)) {
|
||||||
|
auto& e = entries.emplace_back();
|
||||||
|
e.menu_id = MenuID::QUEST_CATEGORIES_EP1_EP3_EP4;
|
||||||
|
e.item_id = cat->category_id;
|
||||||
|
e.name.encode(cat->name, c->language());
|
||||||
|
e.short_description.encode(add_color(cat->description), c->language());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send_command_vt(c, 0xA4, entries.size(), entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_ep3_download_quest_menu(shared_ptr<Client> c, uint32_t category_id) {
|
||||||
|
if (c->lobby.lock()) {
|
||||||
|
throw std::runtime_error("cannot send Ep3 download quest menu to client in a lobby");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto vis_flag = (c->version() == Version::GC_EP3_NTE)
|
||||||
|
? Episode3::MapIndex::VisibilityFlag::ONLINE_TRIAL
|
||||||
|
: Episode3::MapIndex::VisibilityFlag::ONLINE_FINAL;
|
||||||
|
|
||||||
|
auto s = c->require_server_state();
|
||||||
|
auto category = s->ep3_map_index->category_for_id(category_id);
|
||||||
|
if (!category->check_visibility_flag(vis_flag)) {
|
||||||
|
throw std::runtime_error("category is not visible to this client");
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<S_QuestMenuEntry_DC_GC_A2_A4> entries;
|
||||||
|
for (const auto& [map_number, map] : category->all_maps()) {
|
||||||
|
auto vm = map->version(c->language());
|
||||||
|
auto& e = entries.emplace_back();
|
||||||
|
e.menu_id = MenuID::QUEST_EP3;
|
||||||
|
e.item_id = map_number;
|
||||||
|
e.name.encode(vm->map->name.decode(vm->language), c->language());
|
||||||
|
e.short_description.encode(add_color(vm->map->location_name.decode(vm->language)), c->language());
|
||||||
|
}
|
||||||
|
|
||||||
|
send_command_vt(c, 0xA4, entries.size(), entries);
|
||||||
|
}
|
||||||
|
|
||||||
void send_quest_menu(
|
void send_quest_menu(
|
||||||
shared_ptr<Client> c,
|
shared_ptr<Client> c,
|
||||||
const vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>>& quests,
|
const vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>>& quests,
|
||||||
@@ -1732,10 +1775,11 @@ void send_quest_menu(
|
|||||||
case Version::DC_V2:
|
case Version::DC_V2:
|
||||||
case Version::GC_NTE:
|
case Version::GC_NTE:
|
||||||
case Version::GC_V3:
|
case Version::GC_V3:
|
||||||
case Version::GC_EP3_NTE:
|
|
||||||
case Version::GC_EP3:
|
|
||||||
send_quest_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, quests, is_download_menu);
|
send_quest_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, quests, is_download_menu);
|
||||||
break;
|
break;
|
||||||
|
case Version::GC_EP3_NTE:
|
||||||
|
case Version::GC_EP3:
|
||||||
|
throw std::logic_error("Episode 3 clients cannot receive a non-download quest menu");
|
||||||
case Version::XB_V3:
|
case Version::XB_V3:
|
||||||
send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, quests, is_download_menu);
|
send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, quests, is_download_menu);
|
||||||
break;
|
break;
|
||||||
@@ -1759,9 +1803,14 @@ void send_quest_categories_menu(shared_ptr<Client> c, QuestMenuType menu_type, E
|
|||||||
case Version::DC_V2:
|
case Version::DC_V2:
|
||||||
case Version::GC_NTE:
|
case Version::GC_NTE:
|
||||||
case Version::GC_V3:
|
case Version::GC_V3:
|
||||||
|
send_quest_categories_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_type, episode);
|
||||||
|
break;
|
||||||
case Version::GC_EP3_NTE:
|
case Version::GC_EP3_NTE:
|
||||||
case Version::GC_EP3:
|
case Version::GC_EP3:
|
||||||
send_quest_categories_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_type, episode);
|
if (menu_type != QuestMenuType::DOWNLOAD) {
|
||||||
|
throw std::runtime_error("Episode 3 clients cannot receive a non-download quest menu");
|
||||||
|
}
|
||||||
|
send_ep3_download_quest_categories_menu(c);
|
||||||
break;
|
break;
|
||||||
case Version::XB_V3:
|
case Version::XB_V3:
|
||||||
send_quest_categories_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_type, episode);
|
send_quest_categories_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_type, episode);
|
||||||
@@ -2624,25 +2673,40 @@ void send_player_stats_change(std::shared_ptr<Channel> ch, uint16_t client_id, P
|
|||||||
send_command_vt(ch, (subs.size() > 0x400 / sizeof(G_UpdateEntityStat_6x9A)) ? 0x6C : 0x60, 0x00, subs);
|
send_command_vt(ch, (subs.size() > 0x400 / sizeof(G_UpdateEntityStat_6x9A)) ? 0x6C : 0x60, 0x00, subs);
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_change_player_hp(std::shared_ptr<Channel> ch, uint16_t client_id, PlayerHPChange what, int16_t amount) {
|
static G_ChangePlayerHP_6x2F generate_hp_restore_command(
|
||||||
|
Version version, uint8_t client_id, PlayerHPChange what, int16_t amount) {
|
||||||
uint8_t subcommand_number;
|
uint8_t subcommand_number;
|
||||||
if (ch->version == Version::DC_NTE) {
|
if (version == Version::DC_NTE) {
|
||||||
subcommand_number = 0x2B;
|
subcommand_number = 0x2B;
|
||||||
} else if (ch->version == Version::DC_11_2000) {
|
} else if (version == Version::DC_11_2000) {
|
||||||
subcommand_number = 0x2D;
|
subcommand_number = 0x2D;
|
||||||
} else {
|
} else {
|
||||||
subcommand_number = 0x2F;
|
subcommand_number = 0x2F;
|
||||||
}
|
}
|
||||||
G_ChangePlayerHP_6x2F cmd = {
|
return G_ChangePlayerHP_6x2F{
|
||||||
{subcommand_number, sizeof(G_ChangePlayerHP_6x2F) / 4, client_id},
|
{subcommand_number, sizeof(G_ChangePlayerHP_6x2F) / 4, client_id},
|
||||||
static_cast<uint32_t>(what), amount, client_id};
|
static_cast<uint32_t>(what), amount, client_id};
|
||||||
send_command_t(ch, 0x60, 0x00, cmd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_change_player_hp(std::shared_ptr<Lobby> l, uint16_t client_id, PlayerHPChange what, int16_t amount) {
|
void send_change_player_hp(
|
||||||
|
std::shared_ptr<Channel> ch, uint16_t client_id, PlayerHPChange what, int16_t amount) {
|
||||||
|
send_command_t(ch, 0x60, 0x00, generate_hp_restore_command(ch->version, client_id, what, amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
asio::awaitable<void> send_change_player_hp(
|
||||||
|
std::shared_ptr<Client> c, uint16_t client_id, PlayerHPChange what, int16_t amount) {
|
||||||
|
if ((c->version() == Version::GC_NTE) && (client_id == c->lobby_client_id)) {
|
||||||
|
auto cmd = generate_hp_restore_command(c->version(), client_id, what, amount);
|
||||||
|
co_await send_protected_command(c, &cmd, sizeof(cmd), false);
|
||||||
|
} else {
|
||||||
|
send_change_player_hp(c->channel, client_id, what, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
asio::awaitable<void> send_change_player_hp(std::shared_ptr<Lobby> l, uint16_t client_id, PlayerHPChange what, int16_t amount) {
|
||||||
for (const auto& lc : l->clients) {
|
for (const auto& lc : l->clients) {
|
||||||
if (lc) {
|
if (lc) {
|
||||||
send_change_player_hp(lc->channel, client_id, what, amount);
|
co_await send_change_player_hp(lc, client_id, what, amount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2938,12 +3002,12 @@ void send_game_flag_state_t(shared_ptr<Client> c) {
|
|||||||
if (l->quest_flags_known) { // Not all flags known; send multiple 6x75s
|
if (l->quest_flags_known) { // Not all flags known; send multiple 6x75s
|
||||||
phosg::StringWriter w;
|
phosg::StringWriter w;
|
||||||
bool use_v3_cmd = !is_v1_or_v2(c->version()) || (c->version() == Version::GC_NTE);
|
bool use_v3_cmd = !is_v1_or_v2(c->version()) || (c->version() == Version::GC_NTE);
|
||||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
if ((difficulty != l->difficulty) && !use_v3_cmd) {
|
if ((difficulty != l->difficulty) && !use_v3_cmd) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto& diff_flags = l->quest_flag_values->data.at(difficulty);
|
const auto& diff_flags = l->quest_flag_values->array(difficulty);
|
||||||
const auto& diff_known_flags = l->quest_flags_known->data.at(difficulty);
|
const auto& diff_known_flags = l->quest_flags_known->array(difficulty);
|
||||||
for (uint8_t z = 0; z < diff_known_flags.data.size(); z++) {
|
for (uint8_t z = 0; z < diff_known_flags.data.size(); z++) {
|
||||||
uint8_t known_flags = diff_known_flags.data[z];
|
uint8_t known_flags = diff_known_flags.data[z];
|
||||||
if (!known_flags) {
|
if (!known_flags) {
|
||||||
@@ -2955,7 +3019,7 @@ void send_game_flag_state_t(shared_ptr<Client> c) {
|
|||||||
uint16_t flag_num = ((z << 3) | sh);
|
uint16_t flag_num = ((z << 3) | sh);
|
||||||
if (use_v3_cmd) {
|
if (use_v3_cmd) {
|
||||||
w.put(G_UpdateQuestFlag_V3_BB_6x75{
|
w.put(G_UpdateQuestFlag_V3_BB_6x75{
|
||||||
{{0x75, 0x03, 0x0000}, flag_num, (((flag_values << sh) & 0x80) ? 0 : 1)}, difficulty, 0});
|
{{0x75, 0x03, 0x0000}, flag_num, (((flag_values << sh) & 0x80) ? 0 : 1)}, static_cast<uint16_t>(difficulty), 0});
|
||||||
} else {
|
} else {
|
||||||
w.put(G_UpdateQuestFlag_DC_PC_6x75{
|
w.put(G_UpdateQuestFlag_DC_PC_6x75{
|
||||||
{0x75, 0x02, 0x0000}, flag_num, (((flag_values << sh) & 0x80) ? 0 : 1)});
|
{0x75, 0x02, 0x0000}, flag_num, (((flag_values << sh) & 0x80) ? 0 : 1)});
|
||||||
@@ -3253,14 +3317,14 @@ void send_level_up(shared_ptr<Client> c) {
|
|||||||
send_command_t(l, 0x60, 0x00, cmd);
|
send_command_t(l, 0x60, 0x00, cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_give_experience(shared_ptr<Client> c, uint32_t amount) {
|
void send_give_experience(shared_ptr<Client> c, uint32_t amount, uint16_t from_enemy_id) {
|
||||||
auto l = c->require_lobby();
|
auto l = c->require_lobby();
|
||||||
if (c->version() != Version::BB_V4) {
|
if (c->version() != Version::BB_V4) {
|
||||||
throw logic_error("6xBF can only be sent to BB clients");
|
throw logic_error("6xBF can only be sent to BB clients");
|
||||||
}
|
}
|
||||||
uint16_t client_id = c->lobby_client_id;
|
uint16_t client_id = c->lobby_client_id;
|
||||||
G_GiveExperience_BB_6xBF cmd = {
|
G_GiveExperience_Extension_BB_6xBF cmd = {
|
||||||
{0xBF, sizeof(G_GiveExperience_BB_6xBF) / 4, client_id}, amount};
|
{0xBF, sizeof(G_GiveExperience_Extension_BB_6xBF) / 4, client_id}, amount, from_enemy_id, 0};
|
||||||
send_command_t(l, 0x60, 0x00, cmd);
|
send_command_t(l, 0x60, 0x00, cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3268,11 +3332,9 @@ void send_set_exp_multiplier(shared_ptr<Lobby> l) {
|
|||||||
if (!l->is_game()) {
|
if (!l->is_game()) {
|
||||||
throw logic_error("6xDD can only be sent in games (not in lobbies)");
|
throw logic_error("6xDD can only be sent in games (not in lobbies)");
|
||||||
}
|
}
|
||||||
G_SetFractionalEXPMultiplier_Extension_BB_6xDD cmd = {
|
G_SetEXPMultiplier_BB_6xDD cmd = {0xDD, sizeof(G_SetEXPMultiplier_BB_6xDD) / 4, 1};
|
||||||
{0xDD, sizeof(G_SetFractionalEXPMultiplier_Extension_BB_6xDD) / 4, 1}, 1.0f};
|
|
||||||
if (l->mode != GameMode::CHALLENGE) {
|
if (l->mode != GameMode::CHALLENGE) {
|
||||||
cmd.header.param = l->base_exp_multiplier;
|
cmd.header.param = l->base_exp_multiplier;
|
||||||
cmd.multiplier = l->base_exp_multiplier;
|
|
||||||
}
|
}
|
||||||
for (auto lc : l->clients) {
|
for (auto lc : l->clients) {
|
||||||
if (lc && (lc->version() == Version::BB_V4)) {
|
if (lc && (lc->version() == Version::BB_V4)) {
|
||||||
@@ -3527,7 +3589,7 @@ string ep3_description_for_client(shared_ptr<Client> c) {
|
|||||||
"{} CLv{} {}",
|
"{} CLv{} {}",
|
||||||
name_for_char_class(p->disp.visual.char_class),
|
name_for_char_class(p->disp.visual.char_class),
|
||||||
p->disp.stats.level + 1,
|
p->disp.stats.level + 1,
|
||||||
char_for_language_code(p->inventory.language));
|
char_for_language(p->inventory.language));
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename RulesT>
|
template <typename RulesT>
|
||||||
@@ -3769,7 +3831,7 @@ void send_ep3_tournament_match_result(shared_ptr<Lobby> l, uint32_t meseta_rewar
|
|||||||
cmd.num_players_per_team = match->preceding_a->winner_team->max_players;
|
cmd.num_players_per_team = match->preceding_a->winner_team->max_players;
|
||||||
cmd.winner_team_id = (match->preceding_b->winner_team == match->winner_team);
|
cmd.winner_team_id = (match->preceding_b->winner_team == match->winner_team);
|
||||||
cmd.meseta_amount = meseta_reward;
|
cmd.meseta_amount = meseta_reward;
|
||||||
cmd.meseta_reward_text.encode("You got %s meseta!", 1);
|
cmd.meseta_reward_text.encode("You got %s meseta!", Language::ENGLISH);
|
||||||
if ((lc->version() != Version::GC_EP3_NTE) &&
|
if ((lc->version() != Version::GC_EP3_NTE) &&
|
||||||
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
|
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
|
||||||
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
|
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
|
||||||
@@ -3808,8 +3870,7 @@ void send_ep3_update_game_metadata(shared_ptr<Lobby> l) {
|
|||||||
cmd.total_spectators = total_spectators;
|
cmd.total_spectators = total_spectators;
|
||||||
for (auto c : l->clients) {
|
for (auto c : l->clients) {
|
||||||
if (c) {
|
if (c) {
|
||||||
if ((c->version() == Version::GC_EP3) &&
|
if ((c->version() == Version::GC_EP3) && !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
|
||||||
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
|
|
||||||
G_SetGameMetadata_Ep3_6xB4x52 masked_cmd = cmd;
|
G_SetGameMetadata_Ep3_6xB4x52 masked_cmd = cmd;
|
||||||
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
|
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
|
||||||
set_mask_for_ep3_game_command(&masked_cmd, sizeof(masked_cmd), mask_key);
|
set_mask_for_ep3_game_command(&masked_cmd, sizeof(masked_cmd), mask_key);
|
||||||
@@ -3827,9 +3888,7 @@ void send_ep3_update_game_metadata(shared_ptr<Lobby> l) {
|
|||||||
if (tourn->get_final_match() == l->tournament_match) {
|
if (tourn->get_final_match() == l->tournament_match) {
|
||||||
text = std::format("Viewing final match of tournament {}", tourn->get_name());
|
text = std::format("Viewing final match of tournament {}", tourn->get_name());
|
||||||
} else {
|
} else {
|
||||||
text = std::format(
|
text = std::format("Viewing match in round {} of tournament {}", l->tournament_match->round_num, tourn->get_name());
|
||||||
"Viewing match in round {} of tournament {}",
|
|
||||||
l->tournament_match->round_num, tourn->get_name());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
text = "Viewing battle in game " + l->name;
|
text = "Viewing battle in game " + l->name;
|
||||||
@@ -3843,11 +3902,10 @@ void send_ep3_update_game_metadata(shared_ptr<Lobby> l) {
|
|||||||
}
|
}
|
||||||
cmd.total_spectators = total_spectators;
|
cmd.total_spectators = total_spectators;
|
||||||
cmd.text_size = text.size();
|
cmd.text_size = text.size();
|
||||||
cmd.text.encode(text, 1);
|
cmd.text.encode(text, Language::ENGLISH);
|
||||||
for (auto c : watcher_l->clients) {
|
for (auto c : watcher_l->clients) {
|
||||||
if (c) {
|
if (c) {
|
||||||
if ((c->version() == Version::GC_EP3) &&
|
if ((c->version() == Version::GC_EP3) && !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
|
||||||
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
|
|
||||||
G_SetGameMetadata_Ep3_6xB4x52 masked_cmd = cmd;
|
G_SetGameMetadata_Ep3_6xB4x52 masked_cmd = cmd;
|
||||||
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
|
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
|
||||||
set_mask_for_ep3_game_command(&masked_cmd, sizeof(masked_cmd), mask_key);
|
set_mask_for_ep3_game_command(&masked_cmd, sizeof(masked_cmd), mask_key);
|
||||||
@@ -3900,12 +3958,7 @@ void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void send_quest_file_chunk(
|
void send_quest_file_chunk(
|
||||||
shared_ptr<Client> c,
|
shared_ptr<Client> c, const string& filename, size_t chunk_index, const void* data, size_t size, bool is_download_quest) {
|
||||||
const string& filename,
|
|
||||||
size_t chunk_index,
|
|
||||||
const void* data,
|
|
||||||
size_t size,
|
|
||||||
bool is_download_quest) {
|
|
||||||
if (size > 0x400) {
|
if (size > 0x400) {
|
||||||
throw logic_error("quest file chunks must be 1KB or smaller");
|
throw logic_error("quest file chunks must be 1KB or smaller");
|
||||||
}
|
}
|
||||||
@@ -3930,7 +3983,7 @@ void send_open_quest_file_t(
|
|||||||
const string& filename,
|
const string& filename,
|
||||||
const string&,
|
const string&,
|
||||||
uint32_t file_size,
|
uint32_t file_size,
|
||||||
uint32_t,
|
uint32_t, // quest_number (only used on Xbox)
|
||||||
QuestFileType type) {
|
QuestFileType type) {
|
||||||
CommandT cmd;
|
CommandT cmd;
|
||||||
uint8_t command_num;
|
uint8_t command_num;
|
||||||
|
|||||||
+7
-5
@@ -231,7 +231,7 @@ void send_text_or_scrolling_message(std::shared_ptr<ServerState> s, const std::s
|
|||||||
|
|
||||||
std::string prepare_chat_data(
|
std::string prepare_chat_data(
|
||||||
Version version,
|
Version version,
|
||||||
uint8_t language,
|
Language language,
|
||||||
uint8_t from_client_id,
|
uint8_t from_client_id,
|
||||||
const std::string& from_name,
|
const std::string& from_name,
|
||||||
const std::string& text,
|
const std::string& text,
|
||||||
@@ -298,7 +298,7 @@ void send_guild_card(
|
|||||||
const std::string& name,
|
const std::string& name,
|
||||||
const std::string& team_name,
|
const std::string& team_name,
|
||||||
const std::string& description,
|
const std::string& description,
|
||||||
uint8_t language,
|
Language language,
|
||||||
uint8_t section_id,
|
uint8_t section_id,
|
||||||
uint8_t char_class);
|
uint8_t char_class);
|
||||||
void send_guild_card(std::shared_ptr<Client> c, std::shared_ptr<Client> source);
|
void send_guild_card(std::shared_ptr<Client> c, std::shared_ptr<Client> source);
|
||||||
@@ -312,7 +312,8 @@ void send_quest_menu(
|
|||||||
std::shared_ptr<Client> c,
|
std::shared_ptr<Client> c,
|
||||||
const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests,
|
const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests,
|
||||||
bool is_download_menu);
|
bool is_download_menu);
|
||||||
void send_ep3_download_quest_menu(std::shared_ptr<Client> c);
|
void send_ep3_download_quest_categories_menu(std::shared_ptr<Client> c);
|
||||||
|
void send_ep3_download_quest_menu(std::shared_ptr<Client> c, uint32_t category_id);
|
||||||
void send_quest_categories_menu(std::shared_ptr<Client> c, QuestMenuType menu_type, Episode episode);
|
void send_quest_categories_menu(std::shared_ptr<Client> c, QuestMenuType menu_type, Episode episode);
|
||||||
void send_lobby_list(std::shared_ptr<Client> c);
|
void send_lobby_list(std::shared_ptr<Client> c);
|
||||||
|
|
||||||
@@ -353,7 +354,8 @@ enum class PlayerHPChange {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void send_change_player_hp(std::shared_ptr<Channel> ch, uint16_t client_id, PlayerHPChange what, int16_t amount);
|
void send_change_player_hp(std::shared_ptr<Channel> ch, uint16_t client_id, PlayerHPChange what, int16_t amount);
|
||||||
void send_change_player_hp(std::shared_ptr<Lobby> l, uint16_t client_id, PlayerHPChange what, int16_t amount);
|
asio::awaitable<void> send_change_player_hp(std::shared_ptr<Client> c, uint16_t client_id, PlayerHPChange what, int16_t amount);
|
||||||
|
asio::awaitable<void> send_change_player_hp(std::shared_ptr<Lobby> l, uint16_t client_id, PlayerHPChange what, int16_t amount);
|
||||||
|
|
||||||
asio::awaitable<void> send_remove_negative_conditions(std::shared_ptr<Client> c);
|
asio::awaitable<void> send_remove_negative_conditions(std::shared_ptr<Client> c);
|
||||||
void send_remove_negative_conditions(std::shared_ptr<Channel> ch, uint16_t client_id);
|
void send_remove_negative_conditions(std::shared_ptr<Channel> ch, uint16_t client_id);
|
||||||
@@ -397,7 +399,7 @@ void send_item_identify_result(std::shared_ptr<Client> c);
|
|||||||
void send_bank(std::shared_ptr<Client> c);
|
void send_bank(std::shared_ptr<Client> c);
|
||||||
void send_shop(std::shared_ptr<Client> c, uint8_t shop_type);
|
void send_shop(std::shared_ptr<Client> c, uint8_t shop_type);
|
||||||
void send_level_up(std::shared_ptr<Client> c);
|
void send_level_up(std::shared_ptr<Client> c);
|
||||||
void send_give_experience(std::shared_ptr<Client> c, uint32_t amount);
|
void send_give_experience(std::shared_ptr<Client> c, uint32_t amount, uint16_t entity_id);
|
||||||
void send_set_exp_multiplier(std::shared_ptr<Lobby> l);
|
void send_set_exp_multiplier(std::shared_ptr<Lobby> l);
|
||||||
void send_rare_enemy_index_list(std::shared_ptr<Client> c, const std::vector<size_t>& indexes);
|
void send_rare_enemy_index_list(std::shared_ptr<Client> c, const std::vector<size_t>& indexes);
|
||||||
|
|
||||||
|
|||||||
+41
-58
@@ -394,7 +394,7 @@ shared_ptr<const vector<string>> ServerState::information_contents_for_client(sh
|
|||||||
return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3;
|
return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t ServerState::default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const {
|
size_t ServerState::default_min_level_for_game(Version version, Episode episode, Difficulty difficulty) const {
|
||||||
const auto& min_levels = is_v4(version)
|
const auto& min_levels = is_v4(version)
|
||||||
? this->min_levels_v4
|
? this->min_levels_v4
|
||||||
: is_v3(version)
|
: is_v3(version)
|
||||||
@@ -402,21 +402,21 @@ size_t ServerState::default_min_level_for_game(Version version, Episode episode,
|
|||||||
: this->min_levels_v1_v2;
|
: this->min_levels_v1_v2;
|
||||||
switch (episode) {
|
switch (episode) {
|
||||||
case Episode::EP1:
|
case Episode::EP1:
|
||||||
return min_levels[0].at(difficulty);
|
return min_levels[0].at(static_cast<size_t>(difficulty));
|
||||||
case Episode::EP2:
|
case Episode::EP2:
|
||||||
return min_levels[1].at(difficulty);
|
return min_levels[1].at(static_cast<size_t>(difficulty));
|
||||||
case Episode::EP3:
|
case Episode::EP3:
|
||||||
return 0;
|
return 0;
|
||||||
case Episode::EP4:
|
case Episode::EP4:
|
||||||
return min_levels[2].at(difficulty);
|
return min_levels[2].at(static_cast<size_t>(difficulty));
|
||||||
default:
|
default:
|
||||||
throw runtime_error("invalid episode");
|
throw runtime_error("invalid episode");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<const SetDataTableBase> ServerState::set_data_table(
|
shared_ptr<const SetDataTableBase> ServerState::set_data_table(
|
||||||
Version version, Episode episode, GameMode mode, uint8_t difficulty) const {
|
Version version, Episode episode, GameMode mode, Difficulty difficulty) const {
|
||||||
bool use_ult_tables = ((episode == Episode::EP1) && (difficulty == 3) && !is_v1(version) && (version != Version::PC_NTE));
|
bool use_ult_tables = ((episode == Episode::EP1) && (difficulty == Difficulty::ULTIMATE) && !is_v1(version) && (version != Version::PC_NTE));
|
||||||
if (mode == GameMode::SOLO && is_v4(version)) {
|
if (mode == GameMode::SOLO && is_v4(version)) {
|
||||||
return use_ult_tables ? this->bb_solo_set_data_table_ep1_ult : this->bb_solo_set_data_table;
|
return use_ult_tables ? this->bb_solo_set_data_table_ep1_ult : this->bb_solo_set_data_table;
|
||||||
}
|
}
|
||||||
@@ -510,10 +510,10 @@ ItemData ServerState::parse_item_description(Version version, const string& desc
|
|||||||
shared_ptr<const CommonItemSet> ServerState::common_item_set(Version logic_version, shared_ptr<const Quest> q) const {
|
shared_ptr<const CommonItemSet> ServerState::common_item_set(Version logic_version, shared_ptr<const Quest> q) const {
|
||||||
if (q && q->meta.common_item_set) {
|
if (q && q->meta.common_item_set) {
|
||||||
return q->meta.common_item_set;
|
return q->meta.common_item_set;
|
||||||
} else if (is_v1_or_v2(logic_version)) {
|
} else if (is_v1_or_v2(logic_version) && (logic_version != Version::GC_NTE)) {
|
||||||
// TODO: We should probably have a v1 common item set at some point too
|
// TODO: We should probably have a v1 common item set at some point too
|
||||||
return this->common_item_sets.at("common-table-v1-v2");
|
return this->common_item_sets.at("common-table-v1-v2");
|
||||||
} else if (is_v3(logic_version) || is_v4(logic_version)) {
|
} else if ((logic_version == Version::GC_NTE) || is_v3(logic_version) || is_v4(logic_version)) {
|
||||||
return this->common_item_sets.at("common-table-v3-v4");
|
return this->common_item_sets.at("common-table-v3-v4");
|
||||||
} else {
|
} else {
|
||||||
throw runtime_error(std::format("no default common item set is available for {}", phosg::name_for_enum(logic_version)));
|
throw runtime_error(std::format("no default common item set is available for {}", phosg::name_for_enum(logic_version)));
|
||||||
@@ -525,9 +525,9 @@ shared_ptr<const RareItemSet> ServerState::rare_item_set(Version logic_version,
|
|||||||
return q->meta.rare_item_set;
|
return q->meta.rare_item_set;
|
||||||
} else if (is_v1(logic_version)) {
|
} else if (is_v1(logic_version)) {
|
||||||
return this->rare_item_sets.at("rare-table-v1");
|
return this->rare_item_sets.at("rare-table-v1");
|
||||||
} else if (is_v2(logic_version)) {
|
} else if (is_v2(logic_version) && (logic_version != Version::GC_NTE)) {
|
||||||
return this->rare_item_sets.at("rare-table-v2");
|
return this->rare_item_sets.at("rare-table-v2");
|
||||||
} else if (is_v3(logic_version)) {
|
} else if (is_v3(logic_version) || (logic_version == Version::GC_NTE)) {
|
||||||
return this->rare_item_sets.at("rare-table-v3");
|
return this->rare_item_sets.at("rare-table-v3");
|
||||||
} else if (is_v4(logic_version)) {
|
} else if (is_v4(logic_version)) {
|
||||||
return this->rare_item_sets.at("rare-table-v4");
|
return this->rare_item_sets.at("rare-table-v4");
|
||||||
@@ -1280,15 +1280,16 @@ void ServerState::load_config_early() {
|
|||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t z = 0; z < 4; z++) {
|
shared_ptr<const MapState::RareEnemyRates> prev = MapState::DEFAULT_RARE_ENEMIES;
|
||||||
shared_ptr<const MapState::RareEnemyRates> prev = MapState::DEFAULT_RARE_ENEMIES;
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
|
size_t diff_index = static_cast<size_t>(difficulty);
|
||||||
try {
|
try {
|
||||||
string key = "RareEnemyRates-";
|
string key = "RareEnemyRates-";
|
||||||
key += token_name_for_difficulty(z);
|
key += token_name_for_difficulty(difficulty);
|
||||||
this->rare_enemy_rates_by_difficulty[z] = make_shared<MapState::RareEnemyRates>(this->config_json->at(key));
|
this->rare_enemy_rates_by_difficulty[diff_index] = make_shared<MapState::RareEnemyRates>(this->config_json->at(key));
|
||||||
prev = this->rare_enemy_rates_by_difficulty[z];
|
prev = this->rare_enemy_rates_by_difficulty[diff_index];
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
this->rare_enemy_rates_by_difficulty[z] = prev;
|
this->rare_enemy_rates_by_difficulty[diff_index] = prev;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -1431,7 +1432,6 @@ void ServerState::load_config_late() {
|
|||||||
this->quest_F95F_results.clear();
|
this->quest_F95F_results.clear();
|
||||||
this->quest_F960_success_results.clear();
|
this->quest_F960_success_results.clear();
|
||||||
this->quest_F960_failure_results = QuestF960Result();
|
this->quest_F960_failure_results = QuestF960Result();
|
||||||
this->secret_lottery_results.clear();
|
|
||||||
if (this->item_name_index(Version::BB_V4)) {
|
if (this->item_name_index(Version::BB_V4)) {
|
||||||
try {
|
try {
|
||||||
for (const auto& type_it : this->config_json->get_list("QuestF95EResultItems")) {
|
for (const auto& type_it : this->config_json->get_list("QuestF95EResultItems")) {
|
||||||
@@ -1468,16 +1468,6 @@ void ServerState::load_config_late() {
|
|||||||
}
|
}
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
for (const auto& it : this->config_json->get_list("SecretLotteryResultItems")) {
|
|
||||||
try {
|
|
||||||
this->secret_lottery_results.emplace_back(this->parse_item_description(Version::BB_V4, it->as_string()));
|
|
||||||
} catch (const exception& e) {
|
|
||||||
config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", it->as_string(), e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (const out_of_range&) {
|
|
||||||
}
|
|
||||||
|
|
||||||
auto parse_primary_identifier_list = [&](const char* key, Version v) -> unordered_set<uint32_t> {
|
auto parse_primary_identifier_list = [&](const char* key, Version v) -> unordered_set<uint32_t> {
|
||||||
unordered_set<uint32_t> ret;
|
unordered_set<uint32_t> ret;
|
||||||
@@ -1599,7 +1589,7 @@ void ServerState::load_maps() {
|
|||||||
auto objects_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_o.dat");
|
auto objects_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_o.dat");
|
||||||
auto enemies_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_e.dat");
|
auto enemies_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_e.dat");
|
||||||
if (objects_data || enemies_data) {
|
if (objects_data || enemies_data) {
|
||||||
uint32_t free_play_key = this->free_play_key(Episode::EP3, GameMode::NORMAL, 0, 0, 0, 0);
|
uint32_t free_play_key = this->free_play_key(Episode::EP3, GameMode::NORMAL, Difficulty::NORMAL, 0, 0, 0);
|
||||||
auto map_file = make_shared<MapFile>(0, objects_data, enemies_data, nullptr);
|
auto map_file = make_shared<MapFile>(0, objects_data, enemies_data, nullptr);
|
||||||
new_map_file_for_source_hash.emplace(map_file->source_hash(), map_file);
|
new_map_file_for_source_hash.emplace(map_file->source_hash(), map_file);
|
||||||
new_map_files_for_free_play_key[free_play_key].at(static_cast<size_t>(Version::GC_EP3)) = map_file;
|
new_map_files_for_free_play_key[free_play_key].at(static_cast<size_t>(Version::GC_EP3)) = map_file;
|
||||||
@@ -1611,15 +1601,13 @@ void ServerState::load_maps() {
|
|||||||
|
|
||||||
config_log.info_f("Loading free play map files");
|
config_log.info_f("Loading free play map files");
|
||||||
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
|
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
|
||||||
const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
for (Episode episode : ALL_EPISODES_V4) {
|
||||||
for (Episode episode : episodes) {
|
|
||||||
if ((episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) ||
|
if ((episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) ||
|
||||||
(episode == Episode::EP4 && !is_v4(v))) {
|
(episode == Episode::EP4 && !is_v4(v))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
for (GameMode mode : ALL_GAME_MODES_V4) {
|
||||||
for (GameMode mode : modes) {
|
|
||||||
if ((mode == GameMode::BATTLE) && is_pre_v1(v)) {
|
if ((mode == GameMode::BATTLE) && is_pre_v1(v)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1629,8 +1617,8 @@ void ServerState::load_maps() {
|
|||||||
if ((mode == GameMode::SOLO && !is_v4(v))) {
|
if ((mode == GameMode::SOLO && !is_v4(v))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||||
if ((difficulty == 3) && is_v1(v)) {
|
if ((difficulty == Difficulty::ULTIMATE) && is_v1(v)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
auto sdt = this->set_data_table(v, episode, mode, difficulty);
|
auto sdt = this->set_data_table(v, episode, mode, difficulty);
|
||||||
@@ -1705,7 +1693,7 @@ void ServerState::load_maps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<const SuperMap> ServerState::get_free_play_supermap(
|
shared_ptr<const SuperMap> ServerState::get_free_play_supermap(
|
||||||
Episode episode, GameMode mode, uint8_t difficulty, uint8_t floor, uint32_t layout, uint32_t entities) {
|
Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities) {
|
||||||
uint32_t free_play_key = this->free_play_key(episode, mode, difficulty, floor, layout, entities);
|
uint32_t free_play_key = this->free_play_key(episode, mode, difficulty, floor, layout, entities);
|
||||||
try {
|
try {
|
||||||
return this->supermap_for_free_play_key.at(free_play_key);
|
return this->supermap_for_free_play_key.at(free_play_key);
|
||||||
@@ -1759,10 +1747,7 @@ shared_ptr<const SuperMap> ServerState::get_free_play_supermap(
|
|||||||
}
|
}
|
||||||
|
|
||||||
vector<shared_ptr<const SuperMap>> ServerState::supermaps_for_variations(
|
vector<shared_ptr<const SuperMap>> ServerState::supermaps_for_variations(
|
||||||
Episode episode,
|
Episode episode, GameMode mode, Difficulty difficulty, const Variations& variations) {
|
||||||
GameMode mode,
|
|
||||||
uint8_t difficulty,
|
|
||||||
const Variations& variations) {
|
|
||||||
vector<shared_ptr<const SuperMap>> ret;
|
vector<shared_ptr<const SuperMap>> ret;
|
||||||
for (size_t floor = 0; floor < 0x12; floor++) {
|
for (size_t floor = 0; floor < 0x12; floor++) {
|
||||||
Variations::Entry e;
|
Variations::Entry e;
|
||||||
@@ -1881,7 +1866,7 @@ void ServerState::load_word_select_table() {
|
|||||||
unique_ptr<UnicodeTextSet> pc_unitxt_data;
|
unique_ptr<UnicodeTextSet> pc_unitxt_data;
|
||||||
if (this->text_index) {
|
if (this->text_index) {
|
||||||
config_log.debug_f("(Word select) Using PC_V2 unitxt_e.prs from text index");
|
config_log.debug_f("(Word select) Using PC_V2 unitxt_e.prs from text index");
|
||||||
pc_unitxt_collection = &this->text_index->get(Version::PC_V2, 1, 35);
|
pc_unitxt_collection = &this->text_index->get(Version::PC_V2, Language::ENGLISH, 35);
|
||||||
} else {
|
} else {
|
||||||
config_log.debug_f("(Word select) Loading PC_V2 unitxt_e.prs");
|
config_log.debug_f("(Word select) Loading PC_V2 unitxt_e.prs");
|
||||||
pc_unitxt_data = make_unique<UnicodeTextSet>(phosg::load_file("system/text-sets/pc-v2/unitxt_e.prs"));
|
pc_unitxt_data = make_unique<UnicodeTextSet>(phosg::load_file("system/text-sets/pc-v2/unitxt_e.prs"));
|
||||||
@@ -1930,25 +1915,25 @@ shared_ptr<ItemNameIndex> ServerState::create_item_name_index_for_version(
|
|||||||
shared_ptr<const TextIndex> text_index) const {
|
shared_ptr<const TextIndex> text_index) const {
|
||||||
switch (limits->version) {
|
switch (limits->version) {
|
||||||
case Version::DC_NTE:
|
case Version::DC_NTE:
|
||||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_NTE, 0, 2));
|
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_NTE, Language::JAPANESE, 2));
|
||||||
case Version::DC_11_2000:
|
case Version::DC_11_2000:
|
||||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_11_2000, 1, 2));
|
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_11_2000, Language::ENGLISH, 2));
|
||||||
case Version::DC_V1:
|
case Version::DC_V1:
|
||||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_V1, 1, 2));
|
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_V1, Language::ENGLISH, 2));
|
||||||
case Version::DC_V2:
|
case Version::DC_V2:
|
||||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_V2, 1, 3));
|
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_V2, Language::ENGLISH, 3));
|
||||||
case Version::PC_NTE:
|
case Version::PC_NTE:
|
||||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::PC_NTE, 1, 3));
|
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::PC_NTE, Language::ENGLISH, 3));
|
||||||
case Version::PC_V2:
|
case Version::PC_V2:
|
||||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::PC_V2, 1, 3));
|
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::PC_V2, Language::ENGLISH, 3));
|
||||||
case Version::GC_NTE:
|
case Version::GC_NTE:
|
||||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::GC_NTE, 1, 0));
|
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::GC_NTE, Language::ENGLISH, 0));
|
||||||
case Version::GC_V3:
|
case Version::GC_V3:
|
||||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::GC_V3, 1, 0));
|
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::GC_V3, Language::ENGLISH, 0));
|
||||||
case Version::XB_V3:
|
case Version::XB_V3:
|
||||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::XB_V3, 1, 0));
|
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::XB_V3, Language::ENGLISH, 0));
|
||||||
case Version::BB_V4:
|
case Version::BB_V4:
|
||||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::BB_V4, 1, 1));
|
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::BB_V4, Language::ENGLISH, 1));
|
||||||
default:
|
default:
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -2150,11 +2135,9 @@ void ServerState::load_ep3_cards() {
|
|||||||
this->ep3_com_deck_index = make_shared<Episode3::COMDeckIndex>("system/ep3/com-decks.json");
|
this->ep3_com_deck_index = make_shared<Episode3::COMDeckIndex>("system/ep3/com-decks.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServerState::load_ep3_maps() {
|
void ServerState::load_ep3_maps(bool raise_on_any_failure) {
|
||||||
config_log.info_f("Collecting Episode 3 maps");
|
config_log.info_f("Collecting Episode 3 maps");
|
||||||
this->ep3_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps");
|
this->ep3_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps", raise_on_any_failure);
|
||||||
config_log.info_f("Collecting Episode 3 download maps");
|
|
||||||
this->ep3_download_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps-download");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServerState::load_ep3_tournament_state() {
|
void ServerState::load_ep3_tournament_state() {
|
||||||
@@ -2165,15 +2148,15 @@ void ServerState::load_ep3_tournament_state() {
|
|||||||
this->ep3_tournament_index->link_all_clients(this->shared_from_this());
|
this->ep3_tournament_index->link_all_clients(this->shared_from_this());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServerState::load_quest_index() {
|
void ServerState::load_quest_index(bool raise_on_any_failure) {
|
||||||
config_log.info_f("Collecting quests");
|
config_log.info_f("Collecting quests");
|
||||||
this->quest_index = make_shared<QuestIndex>(
|
this->quest_index = make_shared<QuestIndex>(
|
||||||
"system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets);
|
"system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets, raise_on_any_failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServerState::compile_functions() {
|
void ServerState::compile_functions(bool raise_on_any_failure) {
|
||||||
config_log.info_f("Compiling client functions");
|
config_log.info_f("Compiling client functions");
|
||||||
this->function_code_index = make_shared<FunctionCodeIndex>("system/client-functions");
|
this->function_code_index = make_shared<FunctionCodeIndex>("system/client-functions", raise_on_any_failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServerState::load_dol_files() {
|
void ServerState::load_dol_files() {
|
||||||
|
|||||||
+16
-11
@@ -183,7 +183,6 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
std::shared_ptr<const Episode3::CardIndex> ep3_card_index;
|
std::shared_ptr<const Episode3::CardIndex> ep3_card_index;
|
||||||
std::shared_ptr<const Episode3::CardIndex> ep3_card_index_trial;
|
std::shared_ptr<const Episode3::CardIndex> ep3_card_index_trial;
|
||||||
std::shared_ptr<const Episode3::MapIndex> ep3_map_index;
|
std::shared_ptr<const Episode3::MapIndex> ep3_map_index;
|
||||||
std::shared_ptr<const Episode3::MapIndex> ep3_download_map_index;
|
|
||||||
std::shared_ptr<const Episode3::COMDeckIndex> ep3_com_deck_index;
|
std::shared_ptr<const Episode3::COMDeckIndex> ep3_com_deck_index;
|
||||||
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_default_ex_values;
|
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_default_ex_values;
|
||||||
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_tournament_ex_values;
|
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_tournament_ex_values;
|
||||||
@@ -199,7 +198,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
std::unordered_map<std::string, std::shared_ptr<const RareItemSet>> rare_item_sets;
|
std::unordered_map<std::string, std::shared_ptr<const RareItemSet>> rare_item_sets;
|
||||||
std::shared_ptr<const ArmorRandomSet> armor_random_set;
|
std::shared_ptr<const ArmorRandomSet> armor_random_set;
|
||||||
std::shared_ptr<const ToolRandomSet> tool_random_set;
|
std::shared_ptr<const ToolRandomSet> tool_random_set;
|
||||||
std::array<std::shared_ptr<const WeaponRandomSet>, 4> weapon_random_sets;
|
std::array<std::shared_ptr<const WeaponRandomSet>, 4> weapon_random_sets; // Keyed oin difficulty
|
||||||
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set;
|
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set;
|
||||||
std::array<std::shared_ptr<const ItemParameterTable>, NUM_VERSIONS> item_parameter_tables;
|
std::array<std::shared_ptr<const ItemParameterTable>, NUM_VERSIONS> item_parameter_tables;
|
||||||
std::shared_ptr<const ItemTranslationTable> item_translation_table;
|
std::shared_ptr<const ItemTranslationTable> item_translation_table;
|
||||||
@@ -240,7 +239,6 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
std::vector<std::pair<size_t, ItemData>> quest_F95F_results; // [(num_photon_tickets, item)]
|
std::vector<std::pair<size_t, ItemData>> quest_F95F_results; // [(num_photon_tickets, item)]
|
||||||
std::vector<QuestF960Result> quest_F960_success_results;
|
std::vector<QuestF960Result> quest_F960_success_results;
|
||||||
QuestF960Result quest_F960_failure_results;
|
QuestF960Result quest_F960_failure_results;
|
||||||
std::vector<ItemData> secret_lottery_results;
|
|
||||||
float bb_global_exp_multiplier = 1.0f;
|
float bb_global_exp_multiplier = 1.0f;
|
||||||
float exp_share_multiplier = 0.5f;
|
float exp_share_multiplier = 0.5f;
|
||||||
float server_global_drop_rate_multiplier = 1.0f;
|
float server_global_drop_rate_multiplier = 1.0f;
|
||||||
@@ -344,7 +342,14 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
std::shared_ptr<const Menu> proxy_destinations_menu(Version version) const;
|
std::shared_ptr<const Menu> proxy_destinations_menu(Version version) const;
|
||||||
const std::vector<std::pair<std::string, uint16_t>>& proxy_destinations(Version version) const;
|
const std::vector<std::pair<std::string, uint16_t>>& proxy_destinations(Version version) const;
|
||||||
|
|
||||||
std::shared_ptr<const SetDataTableBase> set_data_table(Version version, Episode episode, GameMode mode, uint8_t difficulty) const;
|
std::shared_ptr<const SetDataTableBase> set_data_table(Version version, Episode episode, GameMode mode, Difficulty difficulty) const;
|
||||||
|
|
||||||
|
inline std::shared_ptr<const WeaponRandomSet> weapon_random_set(Difficulty difficulty) const {
|
||||||
|
return this->weapon_random_sets.at(static_cast<size_t>(difficulty));
|
||||||
|
}
|
||||||
|
inline std::shared_ptr<const MapState::RareEnemyRates> rare_enemy_rates(Difficulty difficulty) const {
|
||||||
|
return this->rare_enemy_rates_by_difficulty.at(static_cast<size_t>(difficulty));
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<const LevelTable> level_table(Version version) const;
|
std::shared_ptr<const LevelTable> level_table(Version version) const;
|
||||||
std::shared_ptr<const ItemParameterTable> item_parameter_table(Version version) const;
|
std::shared_ptr<const ItemParameterTable> item_parameter_table(Version version) const;
|
||||||
@@ -376,7 +381,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
|
|
||||||
std::shared_ptr<const std::vector<std::string>> information_contents_for_client(std::shared_ptr<const Client> c) const;
|
std::shared_ptr<const std::vector<std::string>> information_contents_for_client(std::shared_ptr<const Client> c) const;
|
||||||
|
|
||||||
size_t default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const;
|
size_t default_min_level_for_game(Version version, Episode episode, Difficulty difficulty) const;
|
||||||
|
|
||||||
void set_port_configuration(const std::vector<PortConfiguration>& port_configs);
|
void set_port_configuration(const std::vector<PortConfiguration>& port_configs);
|
||||||
|
|
||||||
@@ -391,7 +396,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
std::vector<PortConfiguration> parse_port_configuration(const phosg::JSON& json) const;
|
std::vector<PortConfiguration> parse_port_configuration(const phosg::JSON& json) const;
|
||||||
|
|
||||||
static constexpr uint32_t free_play_key(
|
static constexpr uint32_t free_play_key(
|
||||||
Episode episode, GameMode mode, uint8_t difficulty, uint8_t floor, uint32_t layout, uint32_t entities) {
|
Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities) {
|
||||||
return (static_cast<uint32_t>(episode) << 28) |
|
return (static_cast<uint32_t>(episode) << 28) |
|
||||||
(static_cast<uint32_t>(mode) << 26) |
|
(static_cast<uint32_t>(mode) << 26) |
|
||||||
(static_cast<uint32_t>(difficulty) << 24) |
|
(static_cast<uint32_t>(difficulty) << 24) |
|
||||||
@@ -400,11 +405,11 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
(static_cast<uint32_t>(entities) << 0);
|
(static_cast<uint32_t>(entities) << 0);
|
||||||
}
|
}
|
||||||
std::shared_ptr<const SuperMap> get_free_play_supermap(
|
std::shared_ptr<const SuperMap> get_free_play_supermap(
|
||||||
Episode episode, GameMode mode, uint8_t difficulty, uint8_t floor, uint32_t layout, uint32_t entities);
|
Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities);
|
||||||
std::vector<std::shared_ptr<const SuperMap>> supermaps_for_variations(
|
std::vector<std::shared_ptr<const SuperMap>> supermaps_for_variations(
|
||||||
Episode episode,
|
Episode episode,
|
||||||
GameMode mode,
|
GameMode mode,
|
||||||
uint8_t difficulty,
|
Difficulty difficulty,
|
||||||
const Variations& variations);
|
const Variations& variations);
|
||||||
|
|
||||||
void create_default_lobbies();
|
void create_default_lobbies();
|
||||||
@@ -431,10 +436,10 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
|||||||
void load_set_data_tables();
|
void load_set_data_tables();
|
||||||
void load_word_select_table();
|
void load_word_select_table();
|
||||||
void load_ep3_cards();
|
void load_ep3_cards();
|
||||||
void load_ep3_maps();
|
void load_ep3_maps(bool raise_on_any_failure = false);
|
||||||
void load_ep3_tournament_state();
|
void load_ep3_tournament_state();
|
||||||
void load_quest_index();
|
void load_quest_index(bool raise_on_any_failure = false);
|
||||||
void compile_functions();
|
void compile_functions(bool raise_on_any_failure = false);
|
||||||
void load_dol_files();
|
void load_dol_files();
|
||||||
|
|
||||||
void load_all(bool enable_thread_pool);
|
void load_all(bool enable_thread_pool);
|
||||||
|
|||||||
@@ -703,7 +703,7 @@ ShellCommand c_create_tournament(
|
|||||||
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
|
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
|
||||||
string name = get_quoted_string(args.args);
|
string name = get_quoted_string(args.args);
|
||||||
string map_name = get_quoted_string(args.args);
|
string map_name = get_quoted_string(args.args);
|
||||||
auto map = args.s->ep3_map_index->get(map_name);
|
auto map = args.s->ep3_map_index->map_for_name(map_name);
|
||||||
uint32_t num_teams = stoul(get_quoted_string(args.args), nullptr, 0);
|
uint32_t num_teams = stoul(get_quoted_string(args.args), nullptr, 0);
|
||||||
Episode3::Rules rules;
|
Episode3::Rules rules;
|
||||||
rules.set_defaults();
|
rules.set_defaults();
|
||||||
@@ -940,7 +940,7 @@ ShellCommand c_show_slots(
|
|||||||
if (player.guild_card_number) {
|
if (player.guild_card_number) {
|
||||||
ret.emplace_back(format(" {}: {} => {} ({}, {}, {})",
|
ret.emplace_back(format(" {}: {} => {} ({}, {}, {})",
|
||||||
z, player.guild_card_number, player.name,
|
z, player.guild_card_number, player.name,
|
||||||
char_for_language_code(player.language),
|
char_for_language(player.language),
|
||||||
name_for_char_class(player.char_class),
|
name_for_char_class(player.char_class),
|
||||||
name_for_section_id(player.section_id)));
|
name_for_section_id(player.section_id)));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+30
-19
@@ -476,36 +476,36 @@ bool char_class_is_force(uint8_t cls) {
|
|||||||
return class_flags.at(cls) & ClassFlag::FORCE;
|
return class_flags.at(cls) & ClassFlag::FORCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* name_for_difficulty(uint8_t difficulty) {
|
const char* name_for_difficulty(Difficulty difficulty) {
|
||||||
static const array<const char*, 4> names = {
|
static const array<const char*, 4> names = {
|
||||||
"Normal", "Hard", "Very Hard", "Ultimate"};
|
"Normal", "Hard", "Very Hard", "Ultimate"};
|
||||||
try {
|
try {
|
||||||
return names.at(difficulty);
|
return names.at(static_cast<size_t>(difficulty));
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* token_name_for_difficulty(uint8_t difficulty) {
|
const char* token_name_for_difficulty(Difficulty difficulty) {
|
||||||
static const array<const char*, 4> names = {
|
static const array<const char*, 4> names = {
|
||||||
"Normal", "Hard", "VeryHard", "Ultimate"};
|
"Normal", "Hard", "VeryHard", "Ultimate"};
|
||||||
try {
|
try {
|
||||||
return names.at(difficulty);
|
return names.at(static_cast<size_t>(difficulty));
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
char abbreviation_for_difficulty(uint8_t difficulty) {
|
char abbreviation_for_difficulty(Difficulty difficulty) {
|
||||||
static const array<char, 4> names = {'N', 'H', 'V', 'U'};
|
static const array<char, 4> names = {'N', 'H', 'V', 'U'};
|
||||||
try {
|
try {
|
||||||
return names.at(difficulty);
|
return names.at(static_cast<size_t>(difficulty));
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
return '?';
|
return '?';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* name_for_language_code(uint8_t language_code) {
|
const char* name_for_language(Language language) {
|
||||||
array<const char*, 8> names = {{"Japanese",
|
array<const char*, 8> names = {{"Japanese",
|
||||||
"English",
|
"English",
|
||||||
"German",
|
"German",
|
||||||
@@ -514,39 +514,41 @@ const char* name_for_language_code(uint8_t language_code) {
|
|||||||
"Simplified Chinese",
|
"Simplified Chinese",
|
||||||
"Traditional Chinese",
|
"Traditional Chinese",
|
||||||
"Korean"}};
|
"Korean"}};
|
||||||
return (language_code < 8) ? names[language_code] : "Unknown";
|
size_t lang_index = static_cast<size_t>(language);
|
||||||
|
return (lang_index < 8) ? names[lang_index] : "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
char char_for_language_code(uint8_t language_code) {
|
char char_for_language(Language language) {
|
||||||
return (language_code < 8) ? "JEGFSBTK"[language_code] : '?';
|
size_t lang_index = static_cast<size_t>(language);
|
||||||
|
return (lang_index < 8) ? "JEGFSBTK"[lang_index] : '?';
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t language_code_for_char(char language_char) {
|
Language language_for_char(char language_char) {
|
||||||
switch (language_char) {
|
switch (language_char) {
|
||||||
case 'J':
|
case 'J':
|
||||||
case 'j':
|
case 'j':
|
||||||
return 0;
|
return Language::JAPANESE;
|
||||||
case 'E':
|
case 'E':
|
||||||
case 'e':
|
case 'e':
|
||||||
return 1;
|
return Language::ENGLISH;
|
||||||
case 'G':
|
case 'G':
|
||||||
case 'g':
|
case 'g':
|
||||||
return 2;
|
return Language::GERMAN;
|
||||||
case 'F':
|
case 'F':
|
||||||
case 'f':
|
case 'f':
|
||||||
return 3;
|
return Language::FRENCH;
|
||||||
case 'S':
|
case 'S':
|
||||||
case 's':
|
case 's':
|
||||||
return 4;
|
return Language::SPANISH;
|
||||||
case 'B':
|
case 'B':
|
||||||
case 'b':
|
case 'b':
|
||||||
return 5;
|
return Language::SIMPLIFIED_CHINESE;
|
||||||
case 'T':
|
case 'T':
|
||||||
case 't':
|
case 't':
|
||||||
return 6;
|
return Language::TRADITIONAL_CHINESE;
|
||||||
case 'K':
|
case 'K':
|
||||||
case 'k':
|
case 'k':
|
||||||
return 7;
|
return Language::KOREAN;
|
||||||
default:
|
default:
|
||||||
throw runtime_error("unknown language");
|
throw runtime_error("unknown language");
|
||||||
}
|
}
|
||||||
@@ -809,3 +811,12 @@ const array<size_t, 4> DEFAULT_MIN_LEVELS_V123({0, 19, 39, 79});
|
|||||||
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1({0, 19, 39, 79});
|
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1({0, 19, 39, 79});
|
||||||
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2({0, 29, 49, 89});
|
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2({0, 29, 49, 89});
|
||||||
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4({0, 39, 79, 109});
|
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4({0, 39, 79, 109});
|
||||||
|
|
||||||
|
const array<GameMode, 2> ALL_GAME_MODES_V1 = {GameMode::NORMAL, GameMode::BATTLE};
|
||||||
|
const array<GameMode, 3> ALL_GAME_MODES_V23 = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE};
|
||||||
|
const array<GameMode, 4> ALL_GAME_MODES_V4 = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
||||||
|
const array<Episode, 1> ALL_EPISODES_V12 = {Episode::EP1};
|
||||||
|
const array<Episode, 2> ALL_EPISODES_V3 = {Episode::EP1, Episode::EP2};
|
||||||
|
const array<Episode, 3> ALL_EPISODES_V4 = {Episode::EP1, Episode::EP2, Episode::EP4};
|
||||||
|
const array<Difficulty, 3> ALL_DIFFICULTIES_V1 = {Difficulty::NORMAL, Difficulty::HARD, Difficulty::VERY_HARD};
|
||||||
|
const array<Difficulty, 4> ALL_DIFFICULTIES_V234 = {Difficulty::NORMAL, Difficulty::HARD, Difficulty::VERY_HARD, Difficulty::ULTIMATE};
|
||||||
|
|||||||
+35
-6
@@ -17,6 +17,26 @@ enum class Episode {
|
|||||||
EP4 = 4,
|
EP4 = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class Difficulty : uint8_t {
|
||||||
|
NORMAL = 0,
|
||||||
|
HARD = 1,
|
||||||
|
VERY_HARD = 2,
|
||||||
|
ULTIMATE = 3,
|
||||||
|
UNKNOWN = 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Language : uint8_t {
|
||||||
|
JAPANESE = 0,
|
||||||
|
ENGLISH = 1,
|
||||||
|
GERMAN = 2,
|
||||||
|
FRENCH = 3,
|
||||||
|
SPANISH = 4,
|
||||||
|
SIMPLIFIED_CHINESE = 5,
|
||||||
|
TRADITIONAL_CHINESE = 6,
|
||||||
|
KOREAN = 7,
|
||||||
|
UNKNOWN = 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
bool episode_has_arpg_semantics(Episode ep);
|
bool episode_has_arpg_semantics(Episode ep);
|
||||||
const char* name_for_episode(Episode ep);
|
const char* name_for_episode(Episode ep);
|
||||||
const char* token_name_for_episode(Episode ep);
|
const char* token_name_for_episode(Episode ep);
|
||||||
@@ -63,13 +83,13 @@ bool char_class_is_hunter(uint8_t cls);
|
|||||||
bool char_class_is_ranger(uint8_t cls);
|
bool char_class_is_ranger(uint8_t cls);
|
||||||
bool char_class_is_force(uint8_t cls);
|
bool char_class_is_force(uint8_t cls);
|
||||||
|
|
||||||
const char* name_for_difficulty(uint8_t difficulty);
|
const char* name_for_difficulty(Difficulty difficulty);
|
||||||
const char* token_name_for_difficulty(uint8_t difficulty);
|
const char* token_name_for_difficulty(Difficulty difficulty);
|
||||||
char abbreviation_for_difficulty(uint8_t difficulty);
|
char abbreviation_for_difficulty(Difficulty difficulty);
|
||||||
|
|
||||||
const char* name_for_language_code(uint8_t language_code);
|
const char* name_for_language(Language language);
|
||||||
char char_for_language_code(uint8_t language_code);
|
char char_for_language(Language language);
|
||||||
uint8_t language_code_for_char(char language_char);
|
Language language_for_char(char language_char);
|
||||||
|
|
||||||
extern const std::vector<const char*> name_for_mag_color;
|
extern const std::vector<const char*> name_for_mag_color;
|
||||||
extern const std::unordered_map<std::string, uint8_t> mag_color_for_name;
|
extern const std::unordered_map<std::string, uint8_t> mag_color_for_name;
|
||||||
@@ -110,3 +130,12 @@ extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V123;
|
|||||||
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1;
|
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1;
|
||||||
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2;
|
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2;
|
||||||
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4;
|
extern const std::array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4;
|
||||||
|
|
||||||
|
extern const std::array<GameMode, 2> ALL_GAME_MODES_V1;
|
||||||
|
extern const std::array<GameMode, 3> ALL_GAME_MODES_V23;
|
||||||
|
extern const std::array<GameMode, 4> ALL_GAME_MODES_V4;
|
||||||
|
extern const std::array<Episode, 1> ALL_EPISODES_V12;
|
||||||
|
extern const std::array<Episode, 2> ALL_EPISODES_V3;
|
||||||
|
extern const std::array<Episode, 3> ALL_EPISODES_V4;
|
||||||
|
extern const std::array<Difficulty, 3> ALL_DIFFICULTIES_V1;
|
||||||
|
extern const std::array<Difficulty, 4> ALL_DIFFICULTIES_V234;
|
||||||
|
|||||||
+16
-7
@@ -20,6 +20,7 @@ TeamIndex::Team::Member::Member(const phosg::JSON& json)
|
|||||||
try {
|
try {
|
||||||
this->account_id = json.get_int("AccountID");
|
this->account_id = json.get_int("AccountID");
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
|
// Old format
|
||||||
this->account_id = json.get_int("SerialNumber");
|
this->account_id = json.get_int("SerialNumber");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,7 @@ void TeamIndex::Team::load_config() {
|
|||||||
this->reward_flags = json.get_int("RewardFlags");
|
this->reward_flags = json.get_int("RewardFlags");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TeamIndex::Team::save_config() const {
|
phosg::JSON TeamIndex::Team::json() const {
|
||||||
phosg::JSON members_json = phosg::JSON::list();
|
phosg::JSON members_json = phosg::JSON::list();
|
||||||
for (const auto& it : this->members) {
|
for (const auto& it : this->members) {
|
||||||
members_json.emplace_back(it.second.json());
|
members_json.emplace_back(it.second.json());
|
||||||
@@ -87,14 +88,17 @@ void TeamIndex::Team::save_config() const {
|
|||||||
for (const auto& it : this->reward_keys) {
|
for (const auto& it : this->reward_keys) {
|
||||||
reward_keys_json.emplace_back(it);
|
reward_keys_json.emplace_back(it);
|
||||||
}
|
}
|
||||||
phosg::JSON root = phosg::JSON::dict({
|
return phosg::JSON::dict({
|
||||||
{"Name", this->name},
|
{"Name", this->name},
|
||||||
{"SpentPoints", this->spent_points},
|
{"SpentPoints", this->spent_points},
|
||||||
{"Members", std::move(members_json)},
|
{"Members", std::move(members_json)},
|
||||||
{"RewardKeys", std::move(reward_keys_json)},
|
{"RewardKeys", std::move(reward_keys_json)},
|
||||||
{"RewardFlags", this->reward_flags},
|
{"RewardFlags", this->reward_flags},
|
||||||
});
|
});
|
||||||
phosg::save_file(this->json_filename(), root.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY));
|
}
|
||||||
|
|
||||||
|
void TeamIndex::Team::save_config() const {
|
||||||
|
phosg::save_file(this->json_filename(), this->json().serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TeamIndex::Team::load_flag() {
|
void TeamIndex::Team::load_flag() {
|
||||||
@@ -110,16 +114,21 @@ void TeamIndex::Team::load_flag() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TeamIndex::Team::save_flag() const {
|
phosg::ImageRGBA8888N TeamIndex::Team::decode_flag_data() const {
|
||||||
if (!this->flag_data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
phosg::ImageRGBA8888N img(32, 32);
|
phosg::ImageRGBA8888N img(32, 32);
|
||||||
for (size_t y = 0; y < 32; y++) {
|
for (size_t y = 0; y < 32; y++) {
|
||||||
for (size_t x = 0; x < 32; x++) {
|
for (size_t x = 0; x < 32; x++) {
|
||||||
img.write(x, y, phosg::rgba8888_for_argb1555(this->flag_data->at(y * 0x20 + x)));
|
img.write(x, y, phosg::rgba8888_for_argb1555(this->flag_data->at(y * 0x20 + x)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TeamIndex::Team::save_flag() const {
|
||||||
|
if (!this->flag_data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto img = this->decode_flag_data();
|
||||||
phosg::save_file(this->flag_filename(), img.serialize(phosg::ImageFormat::WINDOWS_BITMAP));
|
phosg::save_file(this->flag_filename(), img.serialize(phosg::ImageFormat::WINDOWS_BITMAP));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user