Compare commits

...

98 Commits

Author SHA1 Message Date
Martin Michelsen c6f74e74c4 hide other players' EXP values in ServerEXPDisplay
Docker / Build (push) Has been cancelled
2025-11-18 17:29:22 -08:00
Martin Michelsen 328980628a fix $edit level 2025-11-18 17:28:48 -08:00
Martin Michelsen 886e9b9f4f fix 5% payment type in 6xDA 2025-11-18 10:27:31 -08:00
Martin Michelsen 26d2ae416e delete unused arguments 2025-11-16 22:37:13 -08:00
Martin Michelsen 62c4c82fcc rewrite HTTP interface 2025-11-16 15:09:28 -08:00
Martin Michelsen 11cc19fe3e update last player name when sending E7; closes #706 2025-11-16 11:01:06 -08:00
Martin Michelsen d1d045a70e fix rare enemy rate inheritance; closes #719 2025-11-16 10:56:39 -08:00
Martin Michelsen 54c790a63c fix notes on get_slot_meseta 2025-11-16 10:48:02 -08:00
Martin Michelsen f1f5c1036a fix invalid range check 2025-11-16 00:05:47 -08:00
Martin Michelsen 77d5436b15 implement quest item creation masks 2025-11-15 23:54:49 -08:00
Martin Michelsen 678c60dd14 update some notes; fix quest assembler bugs 2025-11-15 22:36:18 -08:00
Martin Michelsen d40d231584 add some new AR codes 2025-11-14 19:13:15 -08:00
Martin Michelsen 00ddff7e46 update notes on loading into games/lobbies 2025-11-14 19:13:15 -08:00
Blst34 5725af0f6b Add files via upload 2025-11-12 19:55:11 -08:00
Martin Michelsen 87248e7e67 fix enemy alias lookup logic 2025-11-11 00:04:55 -08:00
Martin Michelsen 712cfc9ac4 fix JSON common table parser 2025-11-10 22:56:23 -08:00
Martin Michelsen 1d8befde8e add fix for TJS rapid-switch crash on GC 2025-11-09 18:01:57 -08:00
Martin Michelsen fb036cda37 fix null pointer dereference in episode 4 free play; closes #717 2025-11-09 16:01:10 -08:00
Martin Michelsen 136e2730de rename Ep4 test door 2025-11-09 16:00:41 -08:00
Martin Michelsen ae47d92016 update notes on delayed_switch_episode 2025-11-08 10:30:39 -08:00
Martin Michelsen b80ed0021b add method to override enemy EXP in quests 2025-11-07 22:53:36 -08:00
Martin Michelsen 1d11879142 demote IPSS unhandled frames to debug logs; closes #713 2025-11-07 21:10:40 -08:00
Martin Michelsen a122b27b1f don't use client's floor for 6x0A and 6x0B 2025-11-07 21:02:08 -08:00
Martin Michelsen cbba724ba1 add pause menu UI code 2025-11-07 20:25:42 -08:00
Martin Michelsen 2c51571ea4 add some misc codes 2025-11-07 14:28:21 -08:00
Martin Michelsen e1d774ce49 fix quest name in HTTP API; closes #714 2025-11-07 11:01:43 -08:00
Martin Michelsen b9e3973c76 document specialized item box format 2025-11-06 22:47:07 -08:00
Martin Michelsen c878093c5f ignore map_designate, etc. if floor number isn't valid 2025-11-06 21:18:42 -08:00
Martin Michelsen 7210441878 allow 6x17 for enemies and objects 2025-11-05 23:06:17 -08:00
Martin Michelsen 36eeee5641 clean up character load function 2025-11-05 22:29:43 -08:00
Martin Michelsen 8d2ffba3e1 add unit specific modifiers 2025-11-05 22:29:18 -08:00
Martin Michelsen 766d4e0c7a fix many edge cases in item name parsing 2025-11-05 21:45:15 -08:00
Martin Michelsen a99f552e7c fix synchro description in mag creation 2025-11-05 19:14:37 -08:00
Martin Michelsen 540a41a583 add Ep3 battle replay test 2025-11-05 09:02:22 -08:00
Martin Michelsen 8cb7d2b2fe fix $playrec behavior 2025-11-04 09:12:48 -08:00
Martin Michelsen 293f25d579 add print-free-supermap 2025-11-04 09:12:40 -08:00
Martin Michelsen 64763e76af fix floor tracking on $exit 2025-11-04 09:12:27 -08:00
Martin Michelsen 69b7e7f998 more object notes 2025-11-02 22:38:02 -08:00
Martin Michelsen 5579bce5d9 delete proxy_session_id 2025-11-02 20:40:30 -08:00
Martin Michelsen 0dd5e2ac10 use bit_cast now that resource_dasm is required 2025-11-02 18:19:06 -08:00
Martin Michelsen 155ed6bcf9 add $makeobj; update some object notes 2025-11-02 17:14:38 -08:00
Martin Michelsen 4e2f62bc73 update notes on TObjDoor 2025-10-30 10:07:56 -07:00
Martin Michelsen bf36a185a2 document TContainerAncient01 2025-10-29 21:27:15 -07:00
Martin Michelsen 4c4c54c536 document TOSparkMachine01 2025-10-29 19:50:09 -07:00
Martin Michelsen e79e6944df update more object notes 2025-10-29 10:27:48 -07:00
Martin Michelsen f6079e3078 update notes for TOSensorAncient01 2025-10-29 10:11:01 -07:00
Martin Michelsen 31b49a71fb add fast tekker patch 2025-10-28 22:35:38 -07:00
Martin Michelsen 83260d5037 fix $sound in lobby 2025-10-28 22:24:38 -07:00
Martin Michelsen 648da83aa1 add new patch file 2025-10-28 10:00:43 -07:00
Martin Michelsen adf1db92c7 fix Ep3 quest download test 2025-10-28 09:50:07 -07:00
Martin Michelsen 662ee48a64 add patch to show EXP gains from the server 2025-10-28 09:50:07 -07:00
Martin Michelsen 446b521898 fix player levels on HTTP server 2025-10-27 22:20:22 -07:00
Martin Michelsen d6db731149 fix uninitialized memory in IPStackSimulator 2025-10-27 22:20:14 -07:00
Martin Michelsen 9106a11be8 add test for Ep3 download quests and map loader 2025-10-27 22:19:58 -07:00
Martin Michelsen 7bc58a757e reimplement Episode 3 map categories 2025-10-26 23:07:47 -07:00
Martin Michelsen 27b5556e4b fix EnemyDamageSync crash on Xbox at connect time 2025-10-26 21:13:20 -07:00
Martin Michelsen b39b4197ed add 59NJ version of CallProtectedHandler 2025-10-25 22:15:06 -07:00
Martin Michelsen a99647d4c7 fix two off-parity offsets in BankSize patch 2025-10-25 21:57:03 -07:00
Repflez 10a6bafb2f Add 59NJ version of BankSize function 2025-10-25 21:57:03 -07:00
Martin Michelsen b4f7688b82 add some new AR codes 2025-10-22 23:41:41 -07:00
Martin Michelsen 08e6b882f3 fix incorrect game metadata logic in proxy
update
2025-10-22 23:30:26 -07:00
Martin Michelsen 4adc174674 merge Ep3 tables in handler-tables 2025-10-22 23:30:26 -07:00
Martin Michelsen 01b1f42bac add some Ep3 command notes 2025-10-22 19:47:23 -07:00
Martin Michelsen be4c7f80cb add tests for quest indexes and function compiler 2025-10-21 22:54:48 -07:00
Martin Michelsen 790363adb5 clean up some patches 2025-10-20 23:11:18 -07:00
Martin Michelsen 09b96a4a86 add BB-DR in handler-tables 2025-10-18 01:03:00 -07:00
Martin Michelsen 6ffa656ad4 implement Hunters Report item behavior 2025-10-18 01:03:00 -07:00
Martin Michelsen 3f2df68ac5 fix flags check on Xbox EnemyDamageSync 2025-10-18 01:03:00 -07:00
Martin Michelsen a7f2ecefe5 don't use under-stack space in EnemyDamageSync 2025-10-18 01:03:00 -07:00
Martin Michelsen 46c2260d0f use enums for difficulty and language; fix enemy state aliases; closes #694 2025-10-18 01:03:00 -07:00
Martin Michelsen 052dcf8c6e update 6xB6 notes 2025-10-18 01:03:00 -07:00
Martin Michelsen cd5863fcde fix $edit for names with spaces 2025-10-18 01:03:00 -07:00
Martin Michelsen 90de571457 document contents of BugFixes patch 2025-10-18 01:03:00 -07:00
Martin Michelsen d9d33c2d65 add patch downloader 2025-10-18 01:03:00 -07:00
Repflez 09962696b7 Assemble the fleti instruction properly 2025-10-17 08:47:04 -07:00
Martin Michelsen d143cbb461 document GC RareItemNotifications patch 2025-10-12 09:48:09 -07:00
Martin Michelsen db7f7abfc4 update HTML drop table notes in command info 2025-10-12 09:48:09 -07:00
Martin Michelsen 6ba92d3a7a skip EXP computation for Level 200 characters 2025-10-12 09:48:09 -07:00
Martin Michelsen 36a1e0dfae fix common tables on GC NTE 2025-10-12 09:48:09 -07:00
Martin Michelsen 47f7e71ae9 display quest names in client's native language in game info window 2025-10-12 09:48:09 -07:00
Martin Michelsen c2008f1f9c handle Ep1&2 NTE protected commands properly 2025-10-12 09:48:09 -07:00
Martin Michelsen 3c32a66064 hide section ID for empty persistent games 2025-10-12 09:48:09 -07:00
Martin Michelsen 41026fbd93 add ep3 auction code 2025-10-12 09:48:09 -07:00
nolrinale d49750aa02 Added missing Coren map files 2025-10-10 21:26:57 -07:00
Martin Michelsen 54f309030e fix exact rate hint in drop tables 2025-10-08 21:37:30 -07:00
Martin Michelsen 093c25fce4 include DAR in generated drop tables 2025-10-08 21:29:55 -07:00
Martin Michelsen a777dc8236 make AsyncEvent resumption faster 2025-10-08 21:29:36 -07:00
Martin Michelsen 4044e4e5a6 fix battle table + $exit edge case 2025-10-05 20:38:44 -07:00
Martin Michelsen 036b4e9456 assign a specific_version for PC NTE 2025-10-05 11:27:59 -07:00
Martin Michelsen 4074530a71 disable EXP share during battle and challenge quests 2025-10-05 11:02:56 -07:00
Martin Michelsen 31eedd7e7e work around 6xD9 client bug 2025-10-05 10:49:07 -07:00
Martin Michelsen df2dfd21e3 fix 88 command during loading on proxy 2025-10-05 10:49:07 -07:00
Martin Michelsen 00b0f71bf4 update some notes 2025-10-05 10:47:20 -07:00
Martin Michelsen 1450a5acd3 allow 6x25 to overwrite slots on all versions 2025-10-04 09:55:00 -07:00
Martin Michelsen 2a138ea0b6 update some command notes 2025-10-04 09:54:37 -07:00
Martin Michelsen 2534ff37de fix potential race in socket closure 2025-10-04 09:54:21 -07:00
Martin Michelsen d61cb1106d allow $unset to remove assist cards too 2025-10-04 09:53:26 -07:00
Martin Michelsen d5f0c6aceb fix shared bank creation 2025-10-03 08:41:45 -07:00
1078 changed files with 13396 additions and 5550 deletions
+1
View File
@@ -18,6 +18,7 @@ build
# Files modified by the user and/or server that don't have defaults
system/config.json
system/ep3/battle-records/*.mzr
system/ep3/battle-records/*.mzrd
system/ep3/tournament-state.json
system/licenses.nsi
+1
View File
@@ -101,6 +101,7 @@ set(SOURCES
src/Map.cc
src/Menu.cc
src/NetworkAddresses.cc
src/PatchDownloadSession.cc
src/PatchFileIndex.cc
src/PlayerInventory.cc
src/PlayerSubordinates.cc
+21 -7
View File
@@ -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.*
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.
@@ -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.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.
* 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-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.
* 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.
* 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.
@@ -482,6 +480,7 @@ The specific versions are:
| PSO DC v2 JP | 2OJF | SH-4 |
| PSO DC v2 US | 2OEF | 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) 02/2003 | 2OJZ | Client functions not supported |
| 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.
* `$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.
* `$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
* `$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`.
* `$surrender`: Cause your team to immediately lose the current battle. If your story character is already defeated, you can't surrender - only your teammate can.
* `$saverec <name>`: Save the recording of the last battle.
* `$playrec <name>`: Play a battle recording. This command creates a spectator team immediately but the replay does not start automatically, to give other players a chance to join. To start the battle replay within the spectator team, run `$playrec` again (with no name). There is a bug in Dolphin that makes this command unstable in emulation (see the "Battle records" section above).
* `$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` (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.
* `$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.
* `$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.
* `$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 <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):
* `$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.
* Aesthetic commands
+129 -1
View File
@@ -17,6 +17,7 @@ Version codes (from README.md):
2OJF: PSO DC v2 JP
2OEF: PSO DC v2 US
2OPF: PSO DC v2 EU
2OJT: PSO PC Trial Edition
2OJW: PSO PC (v2) 04/2002
2OJZ: PSO PC (v2) 02/2003
3OJT: PSO GC Trial Edition
@@ -81,6 +82,21 @@ Disable item equip restrictions ("God of equip")
3OP0 => 041052D4 38000005
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
3OE0 => 040AC944 60000000 // Hildeblue
040C1B70 60000000 // Rappies
@@ -270,6 +286,23 @@ Disable lobby event music (but keep the visuals)
3SJ0 => 040B7078 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
3SE0 => 043101C0 38800004
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
3SJT => 042BE538 38600001
3SJ0 => 042C9C2C 38600001
3SP0 => 042CB50C 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
3SE0 => 042F5D18 4BD160E8
@@ -530,6 +569,68 @@ Heaven Punisher's special always works
3OE2 => 0412AD84 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
3OJ2 => 041FC784 38600007
041FC788 4E800020
@@ -793,3 +894,30 @@ Disable save file signature validation (for moving Xbox saves across consoles)
4OEU => 002F22DB 9090
4OPD => 002F215B 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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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]
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 */
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
BugFixes
View File
View File
View File
-20
View File
@@ -420,26 +420,6 @@ void Account::delete_file() const {
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 ret = std::format("Account:{:08X}", this->account->account_id);
if (this->account_was_created) {
-22
View File
@@ -17,10 +17,6 @@ struct DCNTELicense {
std::string serial_number;
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);
phosg::JSON json() const;
};
@@ -29,10 +25,6 @@ struct V1V2License {
uint32_t serial_number = 0;
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);
phosg::JSON json() const;
};
@@ -42,10 +34,6 @@ struct GCLicense {
std::string access_key;
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);
phosg::JSON json() const;
};
@@ -55,10 +43,6 @@ struct XBLicense {
uint64_t user_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);
phosg::JSON json() const;
};
@@ -67,10 +51,6 @@ struct BBLicense {
std::string username;
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);
phosg::JSON json() const;
};
@@ -178,8 +158,6 @@ struct Login {
std::shared_ptr<XBLicense> xb_license;
std::shared_ptr<BBLicense> bb_license;
uint64_t proxy_session_id() const;
std::string str() const;
};
+4 -4
View File
@@ -247,9 +247,9 @@ public:
static constexpr bool IsBE = BE;
U16T<BE> type;
U16T<BE> unknown_a1;
U16T<BE> unused;
U32T<BE> constructor_addr;
F32T<BE> unknown_a2;
F32T<BE> max_dist2; // Only applies for objects
U32T<BE> default_num_children;
} __attribute__((packed));
@@ -259,9 +259,9 @@ public:
pstring<TextEncoding::ASCII, 0x10> debug_name;
U16T<BE> type;
U16T<BE> unknown_a1;
U16T<BE> unused;
U32T<BE> constructor_addr;
F32T<BE> unknown_a2;
F32T<BE> max_dist2; // Only applies for objects
U32T<BE> default_num_children;
} __attribute__((packed));
+9 -10
View File
@@ -163,7 +163,7 @@ asio::awaitable<HTTPRequest> HTTPClient::recv_http_request(size_t max_line_size,
} else if (method_token == "TRACE") {
req.method = HTTPRequest::Method::TRACE;
} else {
throw HTTPError(400, "unknown request method");
throw HTTPError(400, "Unknown request method");
}
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 chunk_size = stoull(line, &parse_offset, 16);
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) {
break;
}
total_data_bytes += chunk_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));
auto after_chunk_data = co_await this->r.read_line("\r\n", 0x20);
if (!after_chunk_data.empty()) {
throw HTTPError(400, "incorrect trailing sequence after chunk data");
throw HTTPError(400, "Incorrect trailing sequence after chunk data");
}
}
} else {
auto content_length_header = req.get_header("content-length");
size_t content_length = content_length_header ? stoull(*content_length_header) : 0;
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) {
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()) {
WebSocketMessage msg;
// We need at most 10 bytes to determine if there's a valid frame, or as
// little as 2
// We need at most 10 bytes to determine if there's a valid frame, or as little as 2
co_await this->r.read_data_into(msg.header, 2);
// Get the payload size
@@ -335,7 +334,7 @@ asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_
} else if (opcode == 0x08) {
// Close message
co_await this->send_websocket_message(msg.data, msg.opcode);
this->r.get_socket().close();
this->r.close();
} else if (opcode == 0x09) {
// Ping message
@@ -343,7 +342,7 @@ asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_
} else {
// Unknown control message type
this->r.get_socket().close();
this->r.close();
}
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
// zero; if there's no pending message, it must not be zero
if (prev_msg_present == (opcode != 0)) {
this->r.get_socket().close();
this->r.close();
continue;
}
+124
View File
@@ -9,6 +9,7 @@
#include <functional>
#include <memory>
#include <optional>
#include <phosg/Encoding.hh>
#include <phosg/Hash.hh>
#include <phosg/Time.hh>
#include <string>
@@ -82,6 +83,106 @@ struct HTTPClient {
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 {
size_t max_http_request_line_size = 0x1000; // 4KB
size_t max_http_data_size = 0x200000; // 2MB
@@ -120,6 +221,29 @@ public:
protected:
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
// successfully (and the caller should then receive/send WebSocket messages),
// or false if this failed (and the caller should send an HTTP response).
+7 -4
View File
@@ -13,15 +13,18 @@ AsyncEvent::AsyncEvent(asio::any_io_executor ex)
: executor(ex), is_set(false) {}
void AsyncEvent::set() {
lock_guard g(this->lock);
this->is_set = true;
for (auto& waiter : this->waiters) {
std::vector<std::unique_ptr<asio::detail::awaitable_handler<asio::any_io_executor>>> waiters_to_resume;
{
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,
[handler = std::move(waiter)]() mutable {
(*handler)();
});
}
this->waiters.clear();
}
void AsyncEvent::clear() {
+12 -2
View File
@@ -189,8 +189,14 @@ public:
return this->sock;
}
inline bool is_open() const {
return this->sock.is_open();
}
inline void close() {
this->sock.close();
if (this->sock.is_open()) {
this->sock.close();
}
}
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)
auto promise = std::make_shared<AsyncPromise<ReturnT>>();
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();
}
+3 -3
View File
@@ -33,12 +33,12 @@ void BattleParamsIndex::Table::print(FILE* stream, Episode episode) const {
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",
abbreviation_for_difficulty(diff));
abbreviation_for_difficulty(difficulty));
for (size_t z = 0; z < 0x60; 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);
}
}
+17 -4
View File
@@ -70,12 +70,25 @@ public:
} __packed_ws__(MovementData, 0x30);
struct Table {
/* 0000 */ parray<parray<PlayerStats, 0x60>, 4> stats;
/* 3600 */ parray<parray<AttackData, 0x60>, 4> attack_data;
/* 7E00 */ parray<parray<ResistData, 0x60>, 4> resist_data;
/* AE00 */ parray<parray<MovementData, 0x60>, 4> movement_data;
/* 0000 */ parray<parray<PlayerStats, 0x60>, 4> stats; // [difficulty][bp_index]
/* 3600 */ parray<parray<AttackData, 0x60>, 4> attack_data; // [difficulty][bp_index]
/* 7E00 */ parray<parray<ResistData, 0x60>, 4> resist_data; // [difficulty][bp_index]
/* AE00 */ parray<parray<MovementData, 0x60>, 4> movement_data; // [difficulty][bp_index]
/* 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;
} __packed_ws__(Table, 0xF600);
+5 -4
View File
@@ -8,6 +8,7 @@
#include <phosg/Time.hh>
#include "Loggers.hh"
#include "StaticGameData.hh"
#include "Version.hh"
using namespace std;
@@ -16,7 +17,7 @@ extern bool use_terminal_colors;
Channel::Channel(
Version version,
uint8_t language,
Language language,
const string& name,
phosg::TerminalFormat terminal_send_color,
phosg::TerminalFormat terminal_recv_color)
@@ -249,7 +250,7 @@ shared_ptr<SocketChannel> SocketChannel::create(
std::shared_ptr<asio::io_context> io_context,
std::unique_ptr<asio::ip::tcp::socket>&& sock,
Version version,
uint8_t language,
Language language,
const string& name,
phosg::TerminalFormat terminal_send_color,
phosg::TerminalFormat terminal_recv_color) {
@@ -263,7 +264,7 @@ SocketChannel::SocketChannel(
std::shared_ptr<asio::io_context> io_context,
std::unique_ptr<asio::ip::tcp::socket>&& sock,
Version version,
uint8_t language,
Language language,
const string& name,
phosg::TerminalFormat terminal_send_color,
phosg::TerminalFormat terminal_recv_color)
@@ -331,7 +332,7 @@ asio::awaitable<void> SocketChannel::send_task() {
PeerChannel::PeerChannel(
std::shared_ptr<asio::io_context> io_context,
Version version,
uint8_t language,
Language language,
const std::string& name,
phosg::TerminalFormat terminal_send_color,
phosg::TerminalFormat terminal_recv_color)
+5 -5
View File
@@ -12,7 +12,7 @@
class Channel {
public:
Version version;
uint8_t language;
Language language;
std::shared_ptr<PSOEncryption> crypt_in;
std::shared_ptr<PSOEncryption> crypt_out;
@@ -87,7 +87,7 @@ public:
protected:
Channel(
Version version,
uint8_t language,
Language language,
const std::string& name,
phosg::TerminalFormat terminal_send_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,
std::unique_ptr<asio::ip::tcp::socket>&& sock,
Version version,
uint8_t language,
Language language,
const std::string& name = "",
phosg::TerminalFormat terminal_send_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::unique_ptr<asio::ip::tcp::socket>&& sock,
Version version,
uint8_t language,
Language language,
const std::string& name,
phosg::TerminalFormat terminal_send_color,
phosg::TerminalFormat terminal_recv_color);
@@ -158,7 +158,7 @@ public:
PeerChannel(
std::shared_ptr<asio::io_context> io_context,
Version version,
uint8_t language,
Language language,
const std::string& name = "",
phosg::TerminalFormat terminal_send_color = phosg::TerminalFormat::END,
phosg::TerminalFormat terminal_recv_color = phosg::TerminalFormat::END);
+125 -27
View File
@@ -1,5 +1,6 @@
#include "ChatCommands.hh"
#include <ctype.h>
#include <string.h>
#include <filesystem>
@@ -921,7 +922,7 @@ ChatCommandDefinition cc_edit(
p->disp.stats.experience = stoul(tokens.at(1));
} else if (tokens.at(0) == "level" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
p->disp.stats.level = stoul(tokens.at(1)) - 1;
p->recompute_stats(s->level_table(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)) {
if (tokens.at(1) == "reset") {
const auto& which = tokens.at(2);
@@ -959,14 +960,14 @@ ChatCommandDefinition cc_edit(
} else {
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") {
p->disp.visual.name_color = stoul(tokens.at(1), nullptr, 16);
} else if (tokens.at(0) == "language" || tokens.at(0) == "lang") {
if (tokens.at(1).size() != 1) {
throw runtime_error("invalid language");
}
uint8_t new_language = language_code_for_char(tokens.at(1).at(0));
Language new_language = language_for_char(tokens.at(1).at(0));
a.c->channel->language = new_language;
p->inventory.language = new_language;
p->guild_card.language = new_language;
@@ -985,7 +986,7 @@ ChatCommandDefinition cc_edit(
p->disp.visual.section_id = secid;
}
} 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);
} else if (tokens.at(0) == "npc") {
if (tokens.at(1) == "none") {
@@ -1103,14 +1104,12 @@ ChatCommandDefinition cc_exit(
is_in_quest = a.c->proxy_session->is_in_quest;
} else {
auto l = a.c->require_lobby();
is_in_quest = (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) ||
l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS));
is_in_quest = (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS));
}
if (is_in_quest) {
// Client is in a quest; command 6x73 triggers game exit
G_UnusedHeader cmd = {0x73, 0x01, 0x0000};
a.c->channel->send(0x60, 0x00, cmd);
a.c->floor = 0;
co_return;
}
@@ -1433,11 +1432,12 @@ ChatCommandDefinition cc_lobby_info(
if (l->max_level == 0xFFFFFFFF) {
lines.emplace_back(std::format("$C6{:08X}$C7 L$C6{}+$C7", l->lobby_id, l->min_level + 1));
} else {
lines.emplace_back(std::format(
"$C6{:08X}$C7 L$C6{}-{}$C7", l->lobby_id, l->min_level + 1, l->max_level + 1));
lines.emplace_back(std::format("$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) {
case ServerDropMode::DISABLED:
@@ -1621,6 +1621,97 @@ ChatCommandDefinition cc_loadchar(
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(
{"$matcount"},
+[](const Args& a) -> asio::awaitable<void> {
@@ -1791,13 +1882,13 @@ ChatCommandDefinition cc_ping(
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) {
if (ch <= 0x20 || ch > 0x7E || ch == '/') {
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(
@@ -1810,25 +1901,28 @@ ChatCommandDefinition cc_playrec(
if (l->is_game() && l->battle_player) {
l->battle_player->start();
} 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();
string filename = a.text;
bool start_battle_player_immediately = (filename[0] == '!');
if (start_battle_player_immediately) {
bool start_battle_player_immediately = (filename.at(0) != '!');
if (!start_battle_player_immediately) {
filename = filename.substr(1);
}
string data;
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&) {
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 battle_player = make_shared<Episode3::BattleRecordPlayer>(s->io_context, record);
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 (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));
}
} else {
uint8_t 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};
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}, static_cast<uint16_t>(difficulty), 0x0000};
a.c->channel->send(0x60, 0x00, &cmd, sizeof(cmd));
if (a.c->proxy_session) {
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) {
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();
phosg::save_file(file_path, data);
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);
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};
if (!echo_to_all) {
send_command_t(a.c, 0x60, 0x00, cmd);
@@ -2658,7 +2752,7 @@ ChatCommandDefinition cc_switchchar(
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_bank_character_index = index;
@@ -2697,8 +2791,12 @@ ChatCommandDefinition cc_unset(
throw precondition_failed("$C6Battle has not\nyet begun");
}
size_t index = stoull(a.text) - 1;
l->ep3_server->force_destroy_field_character(a.c->lobby_client_id, index);
size_t index = stoull(a.text);
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;
});
@@ -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;
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}",
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);
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}",
+105 -91
View File
@@ -371,7 +371,7 @@ bool Client::evaluate_quest_availability_expression(
shared_ptr<const IntegralExpression> expr,
shared_ptr<const Lobby> game,
uint8_t event,
uint8_t difficulty,
Difficulty difficulty,
size_t num_players,
bool v1_present) const {
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();
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,
.team = this->team(),
.num_players = num_players,
@@ -404,7 +404,7 @@ bool Client::can_see_quest(
shared_ptr<const Quest> q,
shared_ptr<const Lobby> game,
uint8_t event,
uint8_t difficulty,
Difficulty difficulty,
size_t num_players,
bool v1_present) const {
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 Lobby> game,
uint8_t event,
uint8_t difficulty,
Difficulty difficulty,
size_t num_players,
bool v1_present) const {
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");
}
this->load_all_files();
if (!this->character_data) {
throw std::runtime_error("none of the corresponding character files exist");
}
}
return this->character_data;
}
@@ -618,7 +621,7 @@ void Client::save_character_file() {
void Client::create_character_file(
uint32_t guild_card_number,
uint8_t language,
Language language,
const PlayerDispDataBBPreview& preview,
shared_ptr<const LevelTable> 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) {
this->bank_data = std::make_shared<PlayerBank>(this->character_file(true, false)->bank);
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) {
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;
this->bank_data = std::make_shared<PlayerBank>(character->bank);
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");
}
this->system_data.reset();
this->character_data.reset();
this->guild_card_data.reset();
this->bank_data.reset();
string sys_filename = this->system_filename();
if (std::filesystem::is_regular_file(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->system_data) {
string sys_filename = this->system_filename();
if (std::filesystem::is_regular_file(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();
if (std::filesystem::is_regular_file(char_filename)) {
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 (std::filesystem::is_regular_file(card_filename)) {
this->guild_card_data = make_shared<PSOBBGuildCardFile>(phosg::load_object_file<PSOBBGuildCardFile>(card_filename));
this->log.info_f("Loaded Guild Card data from {}", card_filename);
} else {
this->log.info_f("Guild Card file is missing: {}", card_filename);
if (!this->guild_card_data) {
string card_filename = this->guild_card_filename();
if (std::filesystem::is_regular_file(card_filename)) {
this->guild_card_data = make_shared<PSOBBGuildCardFile>(phosg::load_object_file<PSOBBGuildCardFile>(card_filename));
this->log.info_f("Loaded Guild Card data from {}", 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) {
string nsa_filename = this->legacy_account_filename();
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)) {
string nsc_filename = this->legacy_player_filename();
auto nsc_data = phosg::load_object_file<LegacySavedPlayerDataBB>(nsc_filename);
if (nsc_data.signature == LegacySavedPlayerDataBB::SIGNATURE_V0) {
nsc_data.signature = LegacySavedPlayerDataBB::SIGNATURE_V0;
nsc_data.unused.clear();
nsc_data.battle_records.place_counts.clear(0);
nsc_data.battle_records.disconnect_count = 0;
nsc_data.battle_records.unknown_a1.clear(0);
} else if (nsc_data.signature != LegacySavedPlayerDataBB::SIGNATURE_V1) {
throw runtime_error("legacy player data has incorrect signature");
}
if (std::filesystem::is_regular_file(nsc_filename)) {
auto nsc_data = phosg::load_object_file<LegacySavedPlayerDataBB>(nsc_filename);
if (nsc_data.signature == LegacySavedPlayerDataBB::SIGNATURE_V0) {
nsc_data.signature = LegacySavedPlayerDataBB::SIGNATURE_V0;
nsc_data.unused.clear();
nsc_data.battle_records.place_counts.clear(0);
nsc_data.battle_records.disconnect_count = 0;
nsc_data.battle_records.unknown_a1.clear(0);
} else if (nsc_data.signature != LegacySavedPlayerDataBB::SIGNATURE_V1) {
throw runtime_error("legacy player data has incorrect signature");
}
this->character_data = make_shared<PSOBBCharacterFile>();
this->character_data->inventory = nsc_data.inventory;
this->character_data->disp = nsc_data.disp;
this->character_data->play_time_seconds = 0;
this->character_data->quest_flags = nsc_data.quest_flags;
this->character_data->death_count = nsc_data.death_count;
this->character_data->bank = nsc_data.bank;
this->character_data->guild_card.guild_card_number = this->login->account->account_id;
this->character_data->guild_card.name = nsc_data.disp.name;
this->character_data->guild_card.description = nsc_data.guild_card_description;
this->character_data->guild_card.present = 1;
this->character_data->guild_card.language = nsc_data.inventory.language;
this->character_data->guild_card.section_id = nsc_data.disp.visual.section_id;
this->character_data->guild_card.char_class = nsc_data.disp.visual.char_class;
this->character_data->auto_reply = nsc_data.auto_reply;
this->character_data->info_board = nsc_data.info_board;
this->character_data->battle_records = nsc_data.battle_records;
this->character_data->challenge_records = nsc_data.challenge_records;
this->character_data->tech_menu_shortcut_entries = nsc_data.tech_menu_shortcut_entries;
this->character_data->quest_counters = nsc_data.quest_counters;
if (nsa_data) {
this->character_data->option_flags = nsa_data->option_flags;
this->character_data->symbol_chats = nsa_data->symbol_chats;
this->character_data->shortcuts = nsa_data->shortcuts;
this->log.info_f("Loaded legacy player data from {} and {}", nsa_filename, nsc_filename);
} else {
this->log.info_f("Loaded legacy player data from {}", nsc_filename);
this->character_data = make_shared<PSOBBCharacterFile>();
this->character_data->inventory = nsc_data.inventory;
this->character_data->disp = nsc_data.disp;
this->character_data->play_time_seconds = 0;
this->character_data->quest_flags = nsc_data.quest_flags;
this->character_data->death_count = nsc_data.death_count;
this->character_data->bank = nsc_data.bank;
this->character_data->guild_card.guild_card_number = this->login->account->account_id;
this->character_data->guild_card.name = nsc_data.disp.name;
this->character_data->guild_card.description = nsc_data.guild_card_description;
this->character_data->guild_card.present = 1;
this->character_data->guild_card.language = nsc_data.inventory.language;
this->character_data->guild_card.section_id = nsc_data.disp.visual.section_id;
this->character_data->guild_card.char_class = nsc_data.disp.visual.char_class;
this->character_data->auto_reply = nsc_data.auto_reply;
this->character_data->info_board = nsc_data.info_board;
this->character_data->battle_records = nsc_data.battle_records;
this->character_data->challenge_records = nsc_data.challenge_records;
this->character_data->tech_menu_shortcut_entries = nsc_data.tech_menu_shortcut_entries;
this->character_data->quest_counters = nsc_data.quest_counters;
if (nsa_data) {
this->character_data->option_flags = nsa_data->option_flags;
this->character_data->symbol_chats = nsa_data->symbol_chats;
this->character_data->shortcuts = nsa_data->shortcuts;
this->log.info_f("Loaded legacy player data from {} and {}", nsa_filename, nsc_filename);
} else {
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 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();
for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) {
if (this->guild_card_data->blocked[z].present) {
@@ -1011,20 +1018,23 @@ void Client::load_all_files() {
if (this->character_data) {
// Clear legacy play_time field
this->character_data->disp.name.clear_after_bytes(0x18);
this->character_data->inventory.enforce_stack_limits(stack_limits);
this->login->account->auto_reply_message = this->character_data->auto_reply.decode();
this->login->account->save();
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) {
charfile->import_tethealla_material_usage(this->require_server_state()->level_table(this->version()));
uint8_t lang = this->language();
this->log.info_f("Overriding language fields in save files with {:02X} ({})", lang, char_for_language_code(lang));
Language lang = this->language();
this->log.info_f("Overriding language fields in save files with {}", name_for_language(lang));
charfile->inventory.language = lang;
charfile->guild_card.language = lang;
}
@@ -1061,13 +1071,17 @@ shared_ptr<PSOGCEp3CharacterFile::Character> Client::load_ep3_backup_character(u
return ch;
}
void Client::save_and_unload_character() {
void Client::unload_character(bool save) {
if (this->character_data) {
this->save_character_file();
if (save) {
this->save_character_file();
}
this->character_data.reset();
this->log.info_f("Unloaded character");
if (this->bank_data) {
this->save_bank_file();
if (save) {
this->save_bank_file();
}
this->bank_data.reset();
this->log.info_f("Unloaded bank");
}
+7 -7
View File
@@ -149,7 +149,7 @@ public:
uint8_t override_lobby_number = 0x80; // 80 = no override
int64_t override_random_seed = -1;
std::unique_ptr<Variations> override_variations;
VectorXZF pos;
VectorXYZF pos;
uint32_t floor = 0x0F;
std::weak_ptr<Lobby> lobby;
uint8_t lobby_client_id = 0;
@@ -228,7 +228,7 @@ public:
inline Version version() const {
return this->channel->version;
}
inline uint8_t language() const {
inline Language language() const {
return this->channel->language;
}
@@ -267,21 +267,21 @@ public:
std::shared_ptr<const IntegralExpression> expr,
std::shared_ptr<const Lobby> game,
uint8_t event,
uint8_t difficulty,
Difficulty difficulty,
size_t num_players,
bool v1_present) const;
bool can_see_quest(
std::shared_ptr<const Quest> q,
std::shared_ptr<const Lobby> game,
uint8_t event,
uint8_t difficulty,
Difficulty difficulty,
size_t num_players,
bool v1_present) const;
bool can_play_quest(
std::shared_ptr<const Quest> q,
std::shared_ptr<const Lobby> game,
uint8_t event,
uint8_t difficulty,
Difficulty difficulty,
size_t num_players,
bool v1_present) const;
@@ -316,7 +316,7 @@ public:
void save_character_file();
void create_character_file(
uint32_t guild_card_number,
uint8_t language,
Language language,
const PlayerDispDataBBPreview& preview,
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);
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_bank() const;
+175 -112
View File
@@ -190,17 +190,17 @@ struct S_OpenFile_Patch_06 {
// 07 (S->C): Write file
// 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
// chunks to 0x4010 (including the header). Unlike the game server's 13 and A7
// commands, the chunks do not need to be the same size - the game opens the
// file with the "a+b" mode each time it is written, so the new data is always
// appended to the end.
// including the header, but the command may be shorter if the server chooses
// to use a shorter chunk size. Unlike the game server's 13 and A7 commands,
// the chunks do not need to be the same size - the game opens the file with
// the "a+b" mode each time it is written, so the new data is always appended
// to the end.
struct S_WriteFileHeader_Patch_07 {
le_uint32_t chunk_index = 0;
le_uint32_t chunk_checksum = 0; // CRC32 of the following chunk data
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);
// 08 (S->C): Close current file
@@ -240,8 +240,8 @@ struct S_FileChecksumRequest_Patch_0C {
struct C_FileInformation_Patch_0F {
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 size = 0;
le_uint32_t checksum = 0; // CRC32 of the file's data (0 if file not found)
le_uint32_t size = 0; // 0 if file not found
} __packed_ws__(C_FileInformation_Patch_0F, 0x0C);
// 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;
/* 08 */ le_uint32_t sub_version = 0;
/* 0C */ uint8_t is_extended = 0;
/* 0D */ uint8_t language = 0;
/* 0D */ Language language = Language::JAPANESE;
/* 0E */ le_uint16_t unused = 0;
// 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
@@ -404,7 +404,7 @@ struct C_LegacyLogin_PC_V3_04 {
/* 00 */ be_uint64_t hardware_id;
/* 08 */ le_uint32_t sub_version = 0;
/* 0C */ uint8_t is_extended = 0;
/* 0D */ uint8_t language = 0;
/* 0D */ Language language = Language::JAPANESE;
/* 0E */ le_uint16_t unused = 0;
/* 10 */ pstring<TextEncoding::ASCII, 0x10> serial_number;
/* 20 */ pstring<TextEncoding::ASCII, 0x10> access_key;
@@ -414,7 +414,7 @@ struct C_LegacyLogin_PC_V3_04 {
struct C_LegacyLogin_BB_04 {
/* 00 */ le_uint32_t sub_version = 0;
/* 04 */ uint8_t is_extended = 0;
/* 05 */ uint8_t language = 0;
/* 05 */ Language language = Language::JAPANESE;
/* 06 */ le_uint16_t unused = 0;
/* 08 */ pstring<TextEncoding::ASCII, 0x10> username;
/* 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
// 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
// RcvStartGame. The string RcvStartGame2 appears in the DC versions, but it
// 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;
/* 0105 */ uint8_t leader_id = 0;
/* 0106 */ uint8_t disable_udp = 1;
/* 0107 */ uint8_t difficulty = 0;
/* 0107 */ Difficulty difficulty = Difficulty::NORMAL;
/* 0108 */ uint8_t battle_mode = 0;
/* 0109 */ uint8_t event = 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 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 {
uint8_t client_id = 0;
uint8_t leader_id = 0;
@@ -1687,7 +1696,7 @@ struct C_Login_DCNTE_8B {
be_uint64_t hardware_id;
le_uint32_t sub_version = 0x20;
uint8_t is_extended = 0;
uint8_t language = 0;
Language language = Language::JAPANESE;
parray<uint8_t, 2> unused1;
pstring<TextEncoding::ASCII, 0x11> serial_number;
pstring<TextEncoding::ASCII, 0x11> access_key;
@@ -1748,7 +1757,7 @@ struct C_RegisterV1_DC_92 {
be_uint64_t hardware_id;
le_uint32_t sub_version;
uint8_t unused1 = 0;
uint8_t language = 0;
Language language = Language::JAPANESE;
parray<uint8_t, 2> unused2;
pstring<TextEncoding::ASCII, 0x30> serial_number2;
pstring<TextEncoding::ASCII, 0x30> access_key2;
@@ -1767,7 +1776,7 @@ struct C_LoginV1_DC_93 {
/* 08 */ be_uint64_t hardware_id;
/* 10 */ le_uint32_t sub_version = 0;
/* 14 */ uint8_t is_extended = 0;
/* 15 */ uint8_t language = 0;
/* 15 */ Language language = Language::JAPANESE;
/* 16 */ parray<uint8_t, 2> unused1;
/* 18 */ pstring<TextEncoding::ASCII, 0x11> serial_number;
/* 29 */ pstring<TextEncoding::ASCII, 0x11> access_key;
@@ -1788,7 +1797,7 @@ struct C_LoginBase_BB_93 {
/* 00 */ le_uint32_t player_tag = 0x00010000;
/* 04 */ le_uint32_t guild_card_number = 0;
/* 08 */ le_uint32_t sub_version = 0;
/* 0C */ uint8_t language = 0;
/* 0C */ Language language = Language::JAPANESE;
/* 0D */ int8_t character_slot = 0;
// Values for connection_phase:
// 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
// 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
// Internal name: SndPsoDirList
// No arguments
@@ -1952,7 +1965,7 @@ struct C_Register_DC_PC_V3_9C {
/* 00 */ be_uint64_t hardware_id;
/* 08 */ le_uint32_t sub_version = 0;
/* 0C */ uint8_t unused1 = 0;
/* 0D */ uint8_t language = 0;
/* 0D */ Language language = Language::JAPANESE;
/* 0E */ parray<uint8_t, 2> unused2;
/* 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
@@ -1963,7 +1976,7 @@ struct C_Register_DC_PC_V3_9C {
struct C_Register_BB_9C {
le_uint32_t sub_version = 0;
uint8_t unused1 = 0;
uint8_t language = 0;
Language language = Language::JAPANESE;
parray<uint8_t, 2> unused2;
pstring<TextEncoding::ASCII, 0x30> username;
pstring<TextEncoding::ASCII, 0x30> password;
@@ -2002,7 +2015,7 @@ struct C_Login_DC_PC_GC_9D {
/* 08 */ be_uint64_t hardware_id;
/* 10 */ le_uint32_t sub_version = 0;
/* 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
/* 18 */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
/* 28 */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
@@ -2062,7 +2075,7 @@ struct C_LoginExtended_BB_9E {
/* 0000 */ le_uint32_t player_tag = 0x00010000;
/* 0004 */ le_uint32_t guild_card_number = 0; // == account_id when on newserv
/* 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;
/* 0014 */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number; // Always blank?
/* 0024 */ pstring<TextEncoding::ASCII, 0x10> v1_access_key; // == "?"
@@ -2570,7 +2583,7 @@ check_struct_size(C_CreateGame_DCNTE, 0x28);
template <TextEncoding 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)
// Note: Episode 3 uses the challenge mode flag for view battle permissions.
// 0 = view battle allowed; 1 = not allowed
@@ -2915,7 +2928,7 @@ struct C_SetChallengeModeCharacterTemplate_BB_02DF {
struct C_SetChallengeModeDifficulty_BB_03DF {
// 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);
struct C_SetChallengeModeEXPMultiplier_BB_04DF {
@@ -3324,7 +3337,7 @@ struct S_JoinSpectatorTeam_Ep3_E8 {
/* 1170 */ uint8_t client_id = 0;
/* 1171 */ uint8_t leader_id = 0;
/* 1172 */ uint8_t disable_udp = 1;
/* 1173 */ uint8_t difficulty = 0;
/* 1173 */ Difficulty difficulty = Difficulty::NORMAL;
/* 1174 */ uint8_t battle_mode = 0;
/* 1175 */ uint8_t event = 0;
/* 1176 */ uint8_t section_id = 0;
@@ -4052,7 +4065,7 @@ struct G_SendGuildCard_BB_6x06 {
} __packed_ws__(G_SendGuildCard_BB_6x06, 0x10C);
// 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 {
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
// 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 cause undefined behavior on other clients. It seems that this broken
// logic predates even DC NTE.
// to cause undefined behavior on other clients. It seems that this logic
// 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 {
G_EntityIDHeader header;
@@ -4208,8 +4223,7 @@ struct G_VolOptBossActions_6x16 {
// 6x17: Set entity position and angle (not valid on Episode 3)
// 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
// affects a player other than the sender.
// validity checks, even on v3 and later.
struct G_SetEntityPositionAndAngle_6x17 {
G_EntityIDHeader header;
@@ -4236,13 +4250,14 @@ struct G_DarkFalzActions_6x19 {
// 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 {
G_ClientIDHeader header;
} __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 {
G_ClientIDHeader header;
@@ -4322,7 +4337,7 @@ struct G_TeleportPlayer_6x24 {
VectorXYZF pos;
} __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 {
G_ClientIDHeader header;
@@ -4331,7 +4346,7 @@ struct G_EquipItem_6x25 {
le_uint32_t equip_slot = 0;
} __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 {
G_ClientIDHeader header;
@@ -4339,14 +4354,14 @@ struct G_UnequipItem_6x26 {
le_uint32_t unused = 0;
} __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 {
G_ClientIDHeader header;
le_uint32_t item_id = 0;
} __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 {
G_ClientIDHeader header;
@@ -4355,7 +4370,7 @@ struct G_FeedMag_6x28 {
} __packed_ws__(G_FeedMag_6x28, 0x0C);
// 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
// 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;
} __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 {
G_ClientIDHeader header;
@@ -4375,7 +4390,7 @@ struct G_DropItem_6x2A {
VectorXYZF pos;
} __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.
// 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;
} __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
// 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
@@ -4405,13 +4420,13 @@ struct G_ImposeHold_6x2C {
le_float trigger_radius2 = 0.0f; // "2" here means "squared"
} __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 {
G_ClientIDHeader header;
} __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 {
G_ClientIDHeader header;
@@ -4419,7 +4434,7 @@ struct G_SetOrClearPlayerFlags_6x2E {
le_uint32_t or_mask = 0;
} __packed_ws__(G_SetOrClearPlayerFlags_6x2E, 0x0C);
// 6x2F: Change player HP
// 6x2F: Change player HP (protected on GC NTE only)
struct G_ChangePlayerHP_6x2F {
G_ClientIDHeader header;
@@ -4428,7 +4443,7 @@ struct G_ChangePlayerHP_6x2F {
le_uint16_t client_id = 0;
} __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
// single level at once. On other versions, this is not the case.
@@ -4454,11 +4469,11 @@ struct G_UseMedicalCenter_6x31 {
G_ClientIDHeader header;
} __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;
} __packed_ws__(G_MedicalCenterRevivePlayer_6x32, 4);
} __packed_ws__(G_MedicalCenterReviveAllPlayers_6x32, 4);
// 6x33: Revive player (with Moon Atomizer) (protected on V3/V4)
@@ -4561,7 +4576,7 @@ struct G_TargetBase_6x3D {
G_UnusedHeader header;
} __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 {
G_ClientIDHeader header;
@@ -4572,7 +4587,7 @@ struct G_StopAtPosition_6x3E {
VectorXYZF pos;
} __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 {
G_ClientIDHeader header;
@@ -4583,7 +4598,7 @@ struct G_SetPosition_6x3F {
VectorXYZF pos;
} __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.
struct G_WalkToPosition_6x40 {
@@ -4593,7 +4608,7 @@ struct G_WalkToPosition_6x40 {
} __packed_ws__(G_WalkToPosition_6x40, 0x10);
// 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.
// If UDP mode is enabled, this command is sent via UDP.
// TODO: Should newserv translate 6x41 to 6x42? Is there any difference in how
@@ -4604,9 +4619,9 @@ struct G_MoveToPosition_6x41_6x42 {
VectorXZF pos;
} __packed_ws__(G_MoveToPosition_6x41_6x42, 0x0C);
// 6x43: First attack (protected on V3/V4)
// 6x44: Second attack (protected on V3/V4)
// 6x45: Third attack (protected on V3/V4)
// 6x43: First attack (protected on GC NTE/V3/V4)
// 6x44: Second attack (protected on GC NTE/V3/V4)
// 6x45: Third attack (protected on GC NTE/V3/V4)
// If UDP mode is enabled, these commands are sent via UDP.
struct G_Attack_6x43_6x44_6x45 {
@@ -4615,7 +4630,8 @@ struct G_Attack_6x43_6x44_6x45 {
le_uint16_t unknown_a2 = 0;
} __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
// 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
@@ -4627,7 +4643,7 @@ struct G_AttackFinished_Header_6x46 {
// Up to 10 TargetEntries are sent here
} __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.
struct G_CastTechnique_Header_6x47 {
@@ -4644,7 +4660,7 @@ struct G_CastTechnique_Header_6x47 {
// Up to 10 TargetEntries are sent here
} __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 {
G_ClientIDHeader header;
@@ -4654,7 +4670,7 @@ struct G_CastTechniqueComplete_6x48 {
le_uint16_t level = 0;
} __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.
struct G_ExecutePhotonBlast_Header_6x49 {
@@ -4673,8 +4689,8 @@ struct G_ShieldAttack_6x4A {
G_ClientIDHeader header;
} __packed_ws__(G_ShieldAttack_6x4A, 4);
// 6x4B: Hit by enemy (protected on V3/V4)
// 6x4C: Hit by enemy (protected on V3/V4)
// 6x4B: Hit by enemy (protected on GC NTE/V3/V4)
// 6x4C: Hit by enemy (protected on GC NTE/V3/V4)
struct G_HitByEnemy_6x4B_6x4C {
G_ClientIDHeader header;
@@ -4683,14 +4699,14 @@ struct G_HitByEnemy_6x4B_6x4C {
VectorXZF velocity;
} __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 {
G_ClientIDHeader header;
le_uint32_t death_flags = 0; // Same as 6x70's death_flags field
} __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
// can target.
@@ -4705,7 +4721,9 @@ struct G_PlayerRevived_6x4F {
} __packed_ws__(G_PlayerRevived_6x4F, 4);
// 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 {
G_ClientIDHeader header;
@@ -4724,7 +4742,7 @@ struct G_SetPlayerAngle_6x51 {
parray<uint8_t, 2> unused;
} __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 {
G_ClientIDHeader header;
@@ -4733,7 +4751,7 @@ struct G_SetAnimationState_6x52 {
le_uint32_t angle = 0;
} __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 {
G_ClientIDHeader header;
@@ -4748,16 +4766,16 @@ struct G_Unknown_6x54 {
G_ClientIDHeader header;
} __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 {
G_ClientIDHeader header;
le_uint32_t angle_y = 0;
VectorXYZF from_pos;
VectorXYZF to_pos;
VectorXYZF pos;
} __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 {
G_ClientIDHeader header;
@@ -4771,7 +4789,7 @@ struct G_Unknown_6x57 {
G_ClientIDHeader header;
} __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.
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.
// 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
// the item's destructor.
// 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 {
G_ClientIDHeader header;
@@ -4821,7 +4838,7 @@ struct G_DropStackedItem_PC_V3_BB_6x5D : G_DropStackedItem_DC_6x5D {
le_uint32_t unused3 = 0;
} __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 {
G_ClientIDHeader header;
@@ -4896,10 +4913,10 @@ struct G_DestroyFloorItem_6x5C_6x63 {
} __packed_ws__(G_DestroyFloorItem_6x5C_6x63, 0x0C);
// 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)
// 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
@@ -5055,6 +5072,9 @@ struct G_SyncSetFlagState_6x6E_Decompressed {
} __packed_ws__(G_SyncSetFlagState_6x6E_Decompressed, 8);
// 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 {
G_UnusedHeader header;
@@ -5066,6 +5086,11 @@ struct G_SetQuestFlags_V2_V3_6x6F {
QuestFlags quest_flags;
} __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 {
G_UnusedHeader header;
QuestFlags quest_flags;
@@ -5143,7 +5168,7 @@ struct G_6x70_Base_V1 {
/* 0040 */ StatusEffectState attack_status_effect;
/* 004C */ StatusEffectState defense_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;
/* 006C */ le_uint32_t guild_card_number = 0;
/* 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);
// 6x75: Update quest flag
// This command does nothing on Episode 3.
struct G_UpdateQuestFlag_DC_PC_6x75 {
G_UnusedHeader header;
@@ -5254,7 +5280,7 @@ struct G_UpdateQuestFlag_DC_PC_6x75 {
} __packed_ws__(G_UpdateQuestFlag_DC_PC_6x75, 8);
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;
} __packed_ws__(G_UpdateQuestFlag_V3_BB_6x75, 0x0C);
@@ -5270,6 +5296,7 @@ struct G_SetEntitySetFlags_6x76 {
// 6x77: Sync quest register
// 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 {
G_UnusedHeader header;
@@ -5305,13 +5332,13 @@ struct G_GogoBall_6x79 {
parray<uint8_t, 3> unused;
} __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 {
G_ClientIDHeader header;
} __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 {
G_ClientIDHeader header;
@@ -5394,19 +5421,19 @@ struct G_TriggerTrap_6x80 {
le_uint16_t what = 0; // Must be 0, 1, or 2
} __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 {
G_ClientIDHeader header;
} __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 {
G_ClientIDHeader header;
} __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 {
G_ClientIDHeader header;
@@ -5603,7 +5630,7 @@ struct G_UpdateEntityStat_6x9A {
uint8_t amount = 0;
} __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
// upon character death.
@@ -5967,10 +5994,15 @@ struct G_MapData_Ep3_6xB6x41 {
} __packed_ws__(G_MapData_Ep3_6xB6x41, 0x14);
// 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 {
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;
le_uint16_t unused = 0;
// 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);
// 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 {
G_ClientIDHeader header;
le_uint32_t amount = 0;
} __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 {
G_UnusedHeader header;
@@ -6237,11 +6280,13 @@ struct G_AdjustPlayerMeseta_BB_6xC9 {
} __packed_ws__(G_AdjustPlayerMeseta_BB_6xC9, 8);
// 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;
ItemData item_data;
} __packed_ws__(G_ItemRewardRequest_BB_6xCA, 0x18);
} __packed_ws__(G_QuestCreateItem_BB_6xCA, 0x18);
// 6xCB: Transfer item via mail message (BB)
@@ -6287,7 +6332,14 @@ struct G_ChallengeModeGraveRecoveryItemRequest_BB_6xD1 {
le_uint16_t floor = 0;
le_uint16_t room_id = 0;
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);
// 6xD2: Set quest counter (BB)
@@ -6315,14 +6367,18 @@ struct G_Unknown_BB_6xD4 {
// 6xD5: Exchange item in quest (BB; handled by server)
// 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;
ItemData find_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 failure_label = 0;
} __packed_ws__(G_ExchangeItemInQuest_BB_6xD5, 0x30);
} __packed_ws__(G_QuestExchangeItem_BB_6xD5, 0x30);
// 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)
// 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 {
G_ClientIDHeader header;
@@ -6356,16 +6414,25 @@ struct G_AddSRankWeaponSpecial_BB_6xD8 {
} __packed_ws__(G_AddSRankWeaponSpecial_BB_6xD8, 0x24);
// 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 {
G_ClientIDHeader header;
ItemData find_item; // Only data1[0]-[2] are used
ItemData replace_item; // Only data1[0]-[2] are used
le_uint32_t token1 = 0; // valueC (from F95B opcode) ^ sender client ID
le_uint32_t token2 = 0; // valueD (from F95B opcode) ^ sender client ID
le_uint16_t success_label = 0;
le_uint16_t failure_label = 0;
/* 00 */ G_ClientIDHeader header;
// Only data1[0-2] are used in find_item and replace_item when this is sent
// by the F95B quest opcode.
/* 04 */ ItemData find_item;
/* 18 */ ItemData replace_item;
/* 2C */ le_uint32_t token1 = 0; // valueC (from F95B opcode) ^ sender client ID
/* 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);
// 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
// shows when an enemy is killed; actual EXP gains are controlled by the server
// 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 {
G_ParameterHeader header;
} __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)
// 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
// 2 instead of 3, so the last relevant field (failure_label) is not sent to
// the server.
// There is a bug in the client here: it sets the subcommand size to 2 instead
// of 3, so the last relevant field (failure_label) is not sent to the server.
// This is fixed in the MomokaItemExchangeFix patch.
struct G_ExchangeSecretLotteryTicket_BB_6xDE {
struct G_ExchangeSecretLotteryTicket_Incomplete_BB_6xDE {
G_ClientIDHeader header;
uint8_t index = 0;
uint8_t unknown_a1 = 0;
uint8_t index = 0; // 1-8
uint8_t start_reg_num = 0;
le_uint16_t success_label = 0;
// le_uint16_t failure_label = 0;
// parray<uint8_t, 2> unused;
} __packed_ws__(G_ExchangeSecretLotteryTicket_BB_6xDE, 8);
} __packed_ws__(G_ExchangeSecretLotteryTicket_Incomplete_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)
// 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 {
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 bid_value = 0; // 1-99
uint8_t bid_value = 0; // 0-99
parray<uint8_t, 2> unused;
} __packed_ws__(G_MakeCardAuctionBid_Ep3_6xB5x3E, 0x0C);
+47 -51
View File
@@ -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_item_classes_json = json.at("EnemyItemClasses").as_dict();
for (size_t z = 0; z < 0x64; z++) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
auto types = enemy_types_for_rare_table_index(episode, z);
vector<string> names;
if (types.empty()) {
names.emplace_back(std::format("{}:!{:02X}", abbreviation_for_episode(episode), z));
}
for (auto type : enemy_types_for_rare_table_index(episode, z)) {
auto types = enemy_types_for_rare_table_index(episode, z);
vector<string> names;
if (types.empty()) {
names.emplace_back(std::format("{}:!{:02X}", abbreviation_for_episode(episode), z));
} else {
for (auto type : types) {
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]);
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();
}
}
for (const auto& name : names) {
from_json_into(*enemy_meseta_ranges_json.at(name), this->enemy_meseta_ranges[z]);
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_item_classes_json = phosg::JSON::dict();
for (size_t z = 0; z < 0x64; z++) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
for (Episode episode : ALL_EPISODES_V4) {
auto types = enemy_types_for_rare_table_index(episode, z);
vector<string> names;
if (types.empty()) {
@@ -506,13 +503,11 @@ phosg::JSON CommonItemSet::Table::json() const {
phosg::JSON CommonItemSet::json() const {
auto modes_dict = phosg::JSON::dict();
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (const auto& mode : modes) {
for (const auto& mode : ALL_GAME_MODES_V4) {
auto episodes_dict = phosg::JSON::dict();
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (const auto& episode : episodes) {
for (const auto& episode : ALL_EPISODES_V4) {
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();
for (uint8_t section_id = 0; section_id < 10; section_id++) {
try {
@@ -532,11 +527,9 @@ phosg::JSON CommonItemSet::json() 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 : modes) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (const auto& episode : episodes) {
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
for (const auto& mode : ALL_GAME_MODES_V4) {
for (const auto& episode : ALL_EPISODES_V4) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
for (uint8_t section_id = 0; section_id < 10; section_id++) {
try {
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 {
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (const auto& mode : modes) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (const auto& episode : episodes) {
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
for (const auto& mode : ALL_GAME_MODES_V4) {
for (const auto& episode : ALL_EPISODES_V4) {
for (const auto& difficulty : ALL_DIFFICULTIES_V234) {
for (uint8_t section_id = 0; section_id < 10; section_id++) {
shared_ptr<const Table> this_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);
}
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
return (((static_cast<uint16_t>(episode) << 8) & 0x0700) |
((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(
Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) const {
Episode episode, GameMode mode, Difficulty difficulty, uint8_t secid) const {
try {
return this->tables.at(this->key_for_table(episode, mode, difficulty, secid));
} catch (const out_of_range&) {
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.
{
AFSArchive pt_afs(pt_afs_data);
size_t max_difficulty;
bool include_ultimate;
if (pt_afs.num_entries() >= 40) {
max_difficulty = 4;
include_ultimate = true;
} else if (pt_afs.num_entries() >= 30) {
max_difficulty = 3;
include_ultimate = false;
} else {
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++) {
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);
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);
@@ -702,16 +696,19 @@ AFSV2CommonItemSet::AFSV2CommonItemSet(
// 30th are used (section_id is ignored)
if (ct_afs_data) {
AFSArchive ct_afs(ct_afs_data);
size_t max_difficulty;
bool include_ultimate;
if (ct_afs.num_entries() >= 40) {
max_difficulty = 4;
include_ultimate = true;
} else if (ct_afs.num_entries() >= 30) {
max_difficulty = 3;
include_ultimate = false;
} else {
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++) {
auto r = ct_afs.get_reader(difficulty * 10);
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
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);
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);
@@ -723,7 +720,7 @@ AFSV2CommonItemSet::AFSV2CommonItemSet(
GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gsl_data, bool 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 = "";
switch (episode) {
case Episode::EP1:
@@ -746,9 +743,8 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gs
section_id);
};
vector<Episode> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
for (Episode episode : ALL_EPISODES_V4) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
for (size_t section_id = 0; section_id < 10; section_id++) {
phosg::StringReader r;
try {
@@ -773,7 +769,7 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gs
}
if (episode != Episode::EP4) {
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
try {
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
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);
for (const auto& difficulty_it : episode_it.second->as_dict()) {
static const unordered_map<string, uint8_t> difficulty_keys(
{{"Normal", 0}, {"Hard", 1}, {"VeryHard", 2}, {"Ultimate", 3}});
uint8_t difficulty = difficulty_keys.at(difficulty_it.first);
static const unordered_map<string, Difficulty> difficulty_keys(
{{"Normal", Difficulty::NORMAL}, {"Hard", Difficulty::HARD}, {"VeryHard", Difficulty::VERY_HARD}, {"Ultimate", Difficulty::ULTIMATE}});
Difficulty difficulty = difficulty_keys.at(difficulty_it.first);
for (const auto& section_id_it : difficulty_it.second->as_dict()) {
uint8_t section_id = section_id_for_name(section_id_it.first);
+2 -2
View File
@@ -271,7 +271,7 @@ public:
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;
void print(FILE* stream) const;
void print_diff(FILE* stream, const CommonItemSet& other) const;
@@ -279,7 +279,7 @@ public:
protected:
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;
};
+10 -10
View File
@@ -40,7 +40,7 @@ DownloadSession::DownloadSession(
uint16_t remote_port,
const std::string& output_dir,
Version version,
uint8_t language,
Language language,
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file,
uint32_t serial_number2,
uint32_t serial_number,
@@ -222,13 +222,13 @@ void DownloadSession::send_61_98(bool is_98) {
if (is_v1(this->version)) {
C_CharacterData_DCv1_61_98 ret;
ret.inventory = this->character->inventory;
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, Language::ENGLISH, Language::ENGLISH);
this->channel->send(command, 0x01, ret);
} else if (this->version == Version::DC_V2) {
C_CharacterData_DCv2_61_98 ret;
ret.inventory = this->character->inventory;
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, Language::ENGLISH, Language::ENGLISH);
ret.records.challenge = this->character->challenge_records;
ret.records.battle = this->character->battle_records;
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) {
C_CharacterData_PC_61_98 ret;
ret.inventory = this->character->inventory;
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, Language::ENGLISH, Language::ENGLISH);
ret.records.challenge = this->character->challenge_records;
ret.records.battle = this->character->battle_records;
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)) {
C_CharacterData_V3_61_98 ret;
ret.inventory = this->character->inventory;
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, 1, 1);
ret.disp = convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(this->character->disp, Language::ENGLISH, Language::ENGLISH);
ret.records.challenge = this->character->challenge_records;
ret.records.battle = this->character->battle_records;
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;
ret.name.encode(random_name());
ret.password.encode(random_name());
ret.difficulty = 0;
ret.difficulty = Difficulty::NORMAL;
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
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;
ret.name.encode(random_name());
ret.password.encode(random_name());
ret.difficulty = 0;
ret.difficulty = Difficulty::NORMAL;
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
if (is_v1(this->version)) {
@@ -582,7 +582,7 @@ asio::awaitable<void> DownloadSession::on_message(Channel::Message& msg) {
C_CreateGame_BB_C1 ret;
ret.name.encode(random_name());
ret.password.encode(random_name());
ret.difficulty = 0;
ret.difficulty = Difficulty::NORMAL;
ret.battle_mode = (game_config.mode == GameMode::BATTLE);
ret.challenge_mode = (game_config.mode == GameMode::CHALLENGE);
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 : '_');
}
string local_filename = std::format(
"{}/{:016X}_{}_{}_{}_{}",
"{}/{:016X}_{}_{}_{:c}_{}",
this->output_dir,
this->current_request,
phosg::now(),
phosg::name_for_enum(this->version),
char_for_language_code(this->language),
char_for_language(this->language),
filtered_name);
this->open_files.emplace(internal_name, OpenFile{.request = this->current_request, .filename = local_filename, .total_size = cmd.file_size, .data = ""});
};
+2 -2
View File
@@ -21,7 +21,7 @@ public:
uint16_t remote_port,
const std::string& output_dir,
Version version,
uint8_t language,
Language language,
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file,
uint32_t serial_number2,
uint32_t serial_number,
@@ -50,7 +50,7 @@ protected:
uint16_t remote_port;
std::string output_dir;
Version version;
uint8_t language;
Language language;
bool show_command_data;
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file;
uint32_t serial_number;
-25
View File
@@ -174,21 +174,6 @@ string BattleRecord::serialize() const {
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(
const PlayerLobbyDataDCGC& lobby_data,
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);
}
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 {
return this->random_stream;
}
+18 -4
View File
@@ -62,10 +62,25 @@ public:
explicit BattleRecord(const std::string& data);
std::string serialize() const;
bool writable() const;
bool battle_in_progress() const;
inline bool writable() 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(
const PlayerLobbyDataDCGC& lobby_data,
@@ -86,7 +101,6 @@ public:
void print(FILE* stream) const;
std::vector<std::string> get_all_server_data_commands() const;
const std::string& get_random_stream() const;
private:
+127 -47
View File
@@ -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();
for (size_t z = 0; z < this->card_ids.size(); z++) {
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();
for (size_t z = 0; z < this->params.size(); z++) {
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();
for (size_t z = 0; z < this->strings.size(); z++) {
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]);
}
string MapDefinition::str(const CardIndex* card_index, uint8_t language) const {
string MapDefinition::str(const CardIndex* card_index, Language language) const {
deque<string> lines;
auto add_map = [&](const parray<parray<uint8_t, 0x10>, 0x10>& tiles) {
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
// 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_normalized.emplace(this->normalize_card_name(name), entry);
@@ -2620,11 +2620,11 @@ string CardIndex::normalize_card_name(const string& name) {
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),
language(language) {}
MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t language)
MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, Language language)
: language(language),
compressed_data(make_shared<string>(std::move(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;
}
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),
visibility_flags(visibility_flags),
initial_version(initial_version) {
this->versions.resize(this->initial_version->language + 1);
this->versions[this->initial_version->language] = initial_version;
size_t lang_index = static_cast<size_t>(this->initial_version->language);
this->versions.resize(lang_index + 1);
this->versions[lang_index] = initial_version;
}
void MapIndex::Map::add_version(std::shared_ptr<const VersionedMap> vm) {
if (this->versions.size() <= vm->language) {
this->versions.resize(vm->language + 1);
size_t lang_index = static_cast<size_t>(vm->language);
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");
}
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 {
return (this->versions.size() > language) && !!this->versions[language];
bool MapIndex::Map::has_version(Language language) const {
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 ((language < this->versions.size()) && this->versions[language]) {
return this->versions[language];
if ((lang_index < this->versions.size()) && this->versions[lang_index]) {
return this->versions[lang_index];
}
// If English exists, return it
if ((1 < this->versions.size()) && this->versions[1]) {
return this->versions[1];
constexpr size_t english_lang_index = static_cast<size_t>(Language::ENGLISH);
if ((english_lang_index < this->versions.size()) && this->versions[english_lang_index]) {
return this->versions[english_lang_index];
}
// Return the first version that exists
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");
}
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;
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 {
string filename = phosg::basename(file_path);
string base_filename;
string compressed_data;
shared_ptr<MapDefinition> decompressed_data;
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);
} 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);
} 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);
} 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);
} 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);
} 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);
} 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);
} 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);
} else {
continue; // Silently skip file
return; // Silently skip file
}
if (base_filename.size() < 2) {
@@ -2754,7 +2768,7 @@ MapIndex::MapIndex(const string& directory) {
if (base_filename[base_filename.size() - 2] != '-') {
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;
if (decompressed_data) {
@@ -2765,36 +2779,90 @@ MapIndex::MapIndex(const string& directory) {
throw runtime_error("unknown map file format");
}
uint8_t visibility_flags = category ? category->visibility_flags : 0x00;
string name = vm->map->name.decode(vm->language);
auto map_it = mutable_maps.find(vm->map->map_number);
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);
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,
vm->map->map_number,
char_for_language_code(vm->language),
char_for_language(vm->language),
in_category_str,
vm->map->is_quest() ? "quest" : "free",
name);
} 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);
static_game_data_log.debug_f("({}) Added Episode 3 map version {:08X} {} ({}; {})",
filename,
vm->map->map_number,
char_for_language_code(vm->language),
char_for_language(vm->language),
vm->map->is_quest() ? "quest" : "free",
name);
}
this->maps_by_name.emplace(vm->map->name.decode(vm->language), map_it->second);
} catch (const exception& e) {
static_game_data_log.warning_f("Failed to index Episode 3 map {}: {}",
filename, e.what());
if (raise_on_any_failure) {
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) {
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");
}
if (language >= this->compressed_map_lists.size()) {
this->compressed_map_lists.resize(language + 1);
auto& compressed_lists = is_trial ? this->compressed_map_lists_trial : this->compressed_map_lists_final;
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()) {
phosg::StringWriter entries_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;
for (const auto& map_it : this->maps) {
if (!map_it.second->check_visibility_flag(vis_flag)) {
continue;
}
auto vm = map_it.second->version(language);
size_t map_num_players = 0;
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_map_list = std::move(compressed_w.str());
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();
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());
static_game_data_log.info_f("Generated Episode 3 compressed {} map list for {} player(s) ({} maps; 0x{:X} -> 0x{:X} bytes)",
is_trial ? "trial" : "final", num_players, num_maps, decompressed_size, compressed_map_list.size());
}
return compressed_map_list;
}
+80 -18
View File
@@ -1315,7 +1315,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
/* 00 */ pstring<TextEncoding::MARKED, 0x18> deck_name;
/* 18 */ parray<be_uint16_t, 0x20> card_ids; // Last one appears to always be FFFF
/* 58 */
phosg::JSON json(uint8_t language) const;
phosg::JSON json(Language language) const;
} __packed_ws__(NPCDeck, 0x58);
/* 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.
/* 0018 */ parray<be_uint16_t, 0x7E> params;
/* 0114 */
phosg::JSON json(uint8_t language) const;
phosg::JSON json(Language language) const;
} __packed_ws__(AIParams, 0x114);
/* 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 '^'.
/* 0004 */ parray<pstring<TextEncoding::MARKED, 0x40>, 4> strings;
/* 0104 */
phosg::JSON json(uint8_t language) const;
phosg::JSON json(Language language) const;
} __packed_ws__(DialogueSet, 0x104);
// 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.
void assert_semantically_equivalent(const MapDefinition& other) const;
std::string str(const CardIndex* card_index, uint8_t language) const;
phosg::JSON json(uint8_t language) const;
std::string str(const CardIndex* card_index, Language language) const;
phosg::JSON json(Language language) const;
} __packed_ws__(MapDefinition, 0x5A18);
struct MapDefinitionTrial {
@@ -1587,15 +1587,20 @@ private:
class MapIndex {
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 {
public:
std::shared_ptr<const MapDefinition> map;
uint8_t language;
Language language;
VersionedMap(std::shared_ptr<const MapDefinition> map, uint8_t language);
VersionedMap(std::string&& compressed_data, uint8_t language);
VersionedMap(std::shared_ptr<const MapDefinition> map, Language language);
VersionedMap(std::string&& compressed_data, Language language);
std::shared_ptr<const MapDefinitionTrial> trial() const;
std::shared_ptr<const std::string> compressed(bool trial) const;
@@ -1611,13 +1616,18 @@ public:
class Map {
public:
uint32_t map_number;
uint8_t visibility_flags;
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);
bool has_version(uint8_t language) const;
std::shared_ptr<const VersionedMap> version(uint8_t language) const;
bool has_version(Language language) const;
std::shared_ptr<const VersionedMap> version(Language language) const;
inline const std::vector<std::shared_ptr<const VersionedMap>>& all_versions() const {
return this->versions;
}
@@ -1626,24 +1636,76 @@ public:
std::vector<std::shared_ptr<const VersionedMap>> versions;
};
const std::string& get_compressed_list(size_t num_players, uint8_t language) const;
inline std::shared_ptr<const Map> get(uint32_t id) const {
class Category {
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);
}
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);
}
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;
}
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:
// The compressed map lists are generated on demand from the maps map below
mutable std::vector<std::array<std::string, 4>> compressed_map_lists;
// The compressed map lists are generated on demand from the maps map below.
// 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::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 {
public:
COMDeckIndex(const std::string& filename);
+1 -1
View File
@@ -320,7 +320,7 @@ void DeckState::print(FILE* stream, std::shared_ptr<const CardIndex> card_index)
}
}
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",
z, e.deck_index, this->card_refs[z], e.card_id, name, name_for_card_state(e.state));
} else {
+31 -19
View File
@@ -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 {
// 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) {
auto l = this->lobby.lock();
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
// debugging easier.
G_ServerVersionStrings_Ep3_6xB4x46 cmd;
cmd.version_signature.encode(this->options.is_nte() ? VERSION_SIGNATURE_NTE : VERSION_SIGNATURE, 1);
cmd.date_str1.encode(std::format("Card definitions: {:016X}", this->options.card_index->definitions_hash()), 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()), Language::ENGLISH);
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);
}
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);
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) {
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);
}
@@ -592,7 +597,14 @@ void Server::force_replace_assist_card(uint8_t client_id, uint16_t card_id) {
if (!ps) {
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) {
@@ -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
// smaller). So, use the values from the last chosen map if applicable, or
// 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;
auto& server_rules = this->map_and_rules->rules;
// 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;
uint8_t language = sender_c ? sender_c->language() : 1;
const auto& list_data = this->options.map_index->get_compressed_list(num_players, language);
Language language = sender_c ? sender_c->language() : Language::ENGLISH;
const auto& list_data = this->options.map_index->get_compressed_list(num_players, language, this->options.is_nte());
phosg::StringWriter w;
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) {
return;
}
if (map_commands_by_language.size() <= c->language()) {
map_commands_by_language.resize(c->language() + 1);
size_t lang_index = static_cast<size_t>(c->language());
if (map_commands_by_language.size() <= lang_index) {
map_commands_by_language.resize(lang_index + 1);
}
if (map_commands_by_language[c->language()].empty()) {
map_commands_by_language[c->language()] = this->prepare_6xB6x41_map_definition(
if (map_commands_by_language[lang_index].empty()) {
map_commands_by_language[lang_index] = this->prepare_6xB6x41_map_definition(
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);
send_command(c, 0x6C, 0x00, map_commands_by_language[c->language()]);
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[lang_index]);
};
for (const auto& c : l->clients) {
send_to_client(c);
@@ -2571,15 +2584,14 @@ void Server::send_6xB6x41_to_all_clients() const {
// in the playback lobby
for (string& data : map_commands_by_language) {
if (!data.empty()) {
this->battle_record->add_command(
BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
this->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
break;
}
}
}
} 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);
}
}
@@ -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);
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)) {
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();
}
}
+2 -1
View File
@@ -76,6 +76,7 @@ public:
std::shared_ptr<RandomGenerator> rand_crypt;
std::shared_ptr<const Tournament> tournament;
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 {
return (this->behavior_flags & BehaviorFlag::IS_TRIAL_EDITION);
@@ -265,7 +266,7 @@ public:
G_UpdateDecks_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
G_SetPlayerNames_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
static std::string prepare_6xB6x41_map_definition(std::shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_nte);
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;
G_SetTrapTileLocations_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
+2 -2
View File
@@ -357,7 +357,7 @@ void Tournament::init() {
bool is_registration_complete;
if (!this->source_json.is_null()) {
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->flags = this->source_json.get_int("flags", 0x02);
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) {
string map_name = en_vm->map->name.decode(en_vm->language);
ret += std::format(" Map: {:08X} ({})\n", this->map->map_number, map_name);
+117 -73
View File
@@ -108,12 +108,118 @@ bool CompiledFunctionCode::is_big_endian() const {
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(
CompiledFunctionCode::Architecture arch,
const string& function_directory,
const string& system_directory,
const string& name,
const string& text) {
const string& text,
bool raise_on_any_failure) {
unordered_set<string> get_include_stack;
function<string(const string&)> get_include = [&](const string& name) -> string {
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 + ")");
};
// Handle VERS tokens
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"));
}
}
auto version_texts = preprocess_function_code(text);
vector<shared_ptr<CompiledFunctionCode>> ret;
for (size_t vers_index = 0; vers_index < specific_versions.size(); vers_index++) {
uint32_t specific_version = specific_versions[vers_index];
const auto& version_text = version_texts.at(vers_index);
for (const auto& [specific_version, version_text] : version_texts) {
try {
ResourceDASM::EmulatorBase::AssembleResult assembled;
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
@@ -298,6 +336,9 @@ static vector<shared_ptr<CompiledFunctionCode>> compile_function_code(
} catch (const exception& e) {
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());
}
}
@@ -305,7 +346,7 @@ static vector<shared_ptr<CompiledFunctionCode>> compile_function_code(
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");
uint32_t next_menu_item_id = 1;
@@ -367,7 +408,7 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
string path = subdir_path + "/" + filename;
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) {
code->specific_version = specific_version;
}
@@ -388,6 +429,9 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
}
} 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());
}
};
+1 -1
View File
@@ -54,7 +54,7 @@ const char* name_for_architecture(CompiledFunctionCode::Architecture arch);
struct FunctionCodeIndex {
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<uint8_t, std::shared_ptr<CompiledFunctionCode>> index_to_function;
+4 -2
View File
@@ -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) {
uint32_t addr = ipv4_addr_for_asio_addr(client_sock.remote_endpoint().address());
if (this->state->banned_ipv4_ranges->check(addr)) {
client_sock.close();
if (client_sock.is_open()) {
client_sock.close();
}
return nullptr;
}
@@ -126,7 +128,7 @@ shared_ptr<Client> GameServer::create_client(shared_ptr<GameServerSocket> listen
this->io_context,
make_unique<asio::ip::tcp::socket>(std::move(client_sock)),
listen_sock->version,
1,
Language::ENGLISH,
"",
phosg::TerminalFormat::FG_YELLOW,
phosg::TerminalFormat::FG_GREEN);
+736 -782
View File
File diff suppressed because it is too large Load Diff
+6 -20
View File
@@ -4,6 +4,7 @@
#include <memory>
#include <string>
#include <variant>
#include "AsyncHTTPServer.hh"
#include "ServerState.hh"
@@ -20,28 +21,13 @@ public:
asio::awaitable<void> send_rare_drop_notification(std::shared_ptr<const phosg::JSON> message);
protected:
struct RawResponse {
std::string content_type;
std::string data;
};
std::shared_ptr<ServerState> state;
std::unordered_set<std::shared_ptr<HTTPClient>> rare_drop_subscribers;
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();
HTTPRouter<std::variant<RawResponse, std::shared_ptr<const phosg::JSON>>> router;
void require_GET(const HTTPRequest& req);
phosg::JSON require_POST(const HTTPRequest& req);
+32 -39
View File
@@ -129,10 +129,7 @@ void IPSSClient::TCPConnection::linearize_outbound_data(size_t size) {
}
IPSSClient::IPSSClient(
shared_ptr<IPStackSimulator> sim,
uint64_t network_id,
VirtualNetworkProtocol protocol,
asio::ip::tcp::socket&& sock)
shared_ptr<IPStackSimulator> sim, uint64_t network_id, VirtualNetworkProtocol protocol, asio::ip::tcp::socket&& sock)
: io_context(sim->get_io_context()),
sim(sim),
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) {
if (!ec) {
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::TCPConnection> tcp_conn,
Version version,
uint8_t language,
Language language,
const std::string& name,
phosg::TerminalFormat terminal_send_color,
phosg::TerminalFormat terminal_recv_color)
@@ -177,8 +176,7 @@ IPSSChannel::IPSSChannel(
std::string IPSSChannel::default_name() const {
auto ipc = this->ipss_client.lock();
if (ipc) {
string addr_str = str_for_endpoint(ipc->sock.remote_endpoint());
return std::format("ipss:N-{}:{}", ipc->network_id, addr_str);
return std::format("ipss:N-{}:{}", ipc->network_id, str_for_endpoint(ipc->sock.remote_endpoint()));
} else {
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;
size -= direct_size;
this->recv_buf_size -= direct_size;
this->recv_buf = this->recv_buf_size
? reinterpret_cast<uint8_t*>(this->recv_buf) + direct_size
: nullptr;
this->recv_buf = this->recv_buf_size ? (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
@@ -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) {
be_uint32_t be_addr = addr;
char addr_str[INET_ADDRSTRLEN];
memset(addr_str, 0, sizeof(addr_str));
if (!inet_ntop(AF_INET, &be_addr, addr_str, INET_ADDRSTRLEN)) {
return std::format("<UNKNOWN>:{}", port);
} else {
@@ -354,11 +351,12 @@ string IPStackSimulator::str_for_tcp_connection(
asio::awaitable<void> IPStackSimulator::send_ethernet_tapserver_frame(
shared_ptr<IPSSClient> c, FrameInfo::Protocol proto, const void* data, size_t size) const {
struct {
struct TapServerEthernetHeader {
phosg::le_uint16_t frame_size;
EthernetHeader ether;
} header;
static_assert(sizeof(header) == 0x10, "Ethernet tapserver header size is incorrect");
} __attribute__((packed));
static_assert(sizeof(TapServerEthernetHeader) == 0x10, "Ethernet tapserver header size is incorrect");
TapServerEthernetHeader header;
header.ether.dest_mac = c->mac_addr;
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);
array<asio::const_buffer, 2> bufs{
asio::buffer(static_cast<const void*>(&header), sizeof(header)),
asio::buffer(data, size)};
array<asio::const_buffer, 2> bufs{asio::buffer(static_cast<const void*>(&header), sizeof(header)), asio::buffer(data, size)};
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);
if (this->log.should_log(phosg::LogLevel::L_DEBUG)) {
string fi_header = fi.header_str();
this->log.debug_f("Frame header: {}", fi_header);
this->log.debug_f("Frame header: {}", fi.header_str());
}
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();
if (expected_checksum != stored_checksum) {
throw runtime_error(std::format(
"HDLC checksum is incorrect ({:04X} expected, {:04X} received)",
expected_checksum, stored_checksum));
"HDLC checksum is incorrect ({:04X} expected, {:04X} received)", expected_checksum, stored_checksum));
}
} else {
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();
if (fi.ipv4->checksum != expected_ipv4_checksum) {
throw runtime_error(std::format(
"IPv4 header checksum is incorrect ({:04X} expected, {:04X} received)",
expected_ipv4_checksum, fi.ipv4->checksum));
"IPv4 header checksum is incorrect ({:04X} expected, {:04X} received)", expected_ipv4_checksum, fi.ipv4->checksum));
}
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();
if (fi.udp->checksum != expected_udp_checksum) {
throw runtime_error(std::format(
"UDP checksum is incorrect ({:04X} expected, {:04X} received)",
expected_udp_checksum, fi.udp->checksum));
"UDP checksum is incorrect ({:04X} expected, {:04X} received)", expected_udp_checksum, fi.udp->checksum));
}
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();
if (fi.tcp->checksum != expected_tcp_checksum) {
throw runtime_error(std::format(
"TCP checksum is incorrect ({:04X} expected, {:04X} received)",
expected_tcp_checksum, fi.tcp->checksum));
"TCP checksum is incorrect ({:04X} expected, {:04X} received)", expected_tcp_checksum, fi.tcp->checksum));
}
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_udp.size = sizeof(UDPHeader) + r_data.size();
r_ipv4.checksum = FrameInfo::computed_ipv4_header_checksum(r_ipv4);
r_udp.checksum = FrameInfo::computed_udp4_checksum(
r_ipv4, r_udp, r_data.data(), r_data.size());
r_udp.checksum = FrameInfo::computed_udp4_checksum(r_ipv4, r_udp, r_data.data(), r_data.size());
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);
@@ -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);
} 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);
auto conn_it = c->tcp_connections.find(key);
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;
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
if (!conn->server_channel) {
this->log.warning_f("Client sent data on TCP connection {}, but server channel is missing",
conn_str);
this->log.warning_f("Client sent data on TCP connection {}, but server channel is missing", conn_str);
} else if (!conn->server_channel->connected()) {
this->log.warning_f("Client sent data on TCP connection {}, but server channel is disconnected",
conn_str);
this->log.warning_f("Client sent data on TCP connection {}, but server channel is disconnected", conn_str);
} else {
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->bytes_received += payload_size;
if (conn->next_client_seq < payload_size) {
this->log.warning_f("Client sequence number has wrapped (next={:08X}, bytes={:X})",
fi.tcp->seq_num, payload_size);
this->log.warning_f("Client sequence number has wrapped (next={:08X}, bytes={:X})", 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;
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()) {
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) {
uint32_t addr = ipv4_addr_for_asio_addr(client_sock.remote_endpoint().address());
if (this->state->banned_ipv4_ranges->check(addr)) {
client_sock.close();
if (client_sock.is_open()) {
client_sock.close();
}
return nullptr;
}
+1 -1
View File
@@ -109,7 +109,7 @@ public:
std::weak_ptr<IPSSClient> ipss_client,
std::weak_ptr<IPSSClient::TCPConnection> tcp_conn,
Version version,
uint8_t language,
Language language,
const std::string& name = "",
phosg::TerminalFormat terminal_send_color = phosg::TerminalFormat::END,
phosg::TerminalFormat terminal_recv_color = phosg::TerminalFormat::END);
+1 -1
View File
@@ -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});
GVMFileEntry file_entry;
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.format_flags = 0;
file_entry.dimensions = (dimensions_field << 4) | dimensions_field;
+6 -9
View File
@@ -32,7 +32,7 @@ ItemCreator::ItemCreator(
std::shared_ptr<const ItemData::StackLimits> stack_limits,
Episode episode,
GameMode mode,
uint8_t difficulty,
Difficulty difficulty,
uint8_t section_id,
std::shared_ptr<RandomGenerator> rand_crypt,
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[2] = pt.pop();
if ((this->difficulty == 3) && (player_level > 99)) {
if ((this->difficulty == Difficulty::ULTIMATE) && (player_level > 99)) {
if (player_level > 150) {
item.data1[2] += 3;
} 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[2] = pt.pop();
if ((this->difficulty == 3) && (player_level > 99)) {
if ((this->difficulty == Difficulty::ULTIMATE) && (player_level > 99)) {
if (player_level > 150) {
item.data1[2] += 3;
} else if (player_level >= 100) {
@@ -1360,7 +1360,7 @@ vector<ItemData> ItemCreator::generate_weapon_shop_contents(size_t player_level)
}
size_t table_index;
if (this->difficulty == 3) {
if (this->difficulty == Difficulty::ULTIMATE) {
if (player_level < 11) {
table_index = 0;
} else if (player_level < 26) {
@@ -1654,8 +1654,7 @@ void ItemCreator::generate_weapon_shop_item_bonus1(
} else {
const auto* range = this->weapon_random_set->get_bonus_range(0, table_index);
item.data1[7] = bonus_values.at(max<size_t>(
this->rand_int(range->max + 1), range->min));
item.data1[7] = bonus_values.at(max<size_t>(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 {
const auto* range = this->weapon_random_set->get_bonus_range(1, table_index);
item.data1[9] = bonus_values.at(max<size_t>(
this->rand_int(range->max + 1), range->min));
item.data1[9] = bonus_values.at(max<size_t>(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:
item.data2d = ((param5 >> 0x10) & 0xFFFF) * 10;
break;
default:
throw runtime_error("invalid item class");
}
+2 -2
View File
@@ -22,7 +22,7 @@ public:
std::shared_ptr<const ItemData::StackLimits> stack_limits,
Episode episode,
GameMode mode,
uint8_t difficulty,
Difficulty difficulty,
uint8_t section_id,
std::shared_ptr<RandomGenerator> rand_crypt,
std::shared_ptr<const BattleRules> restrictions = nullptr);
@@ -61,7 +61,7 @@ private:
std::shared_ptr<const ItemData::StackLimits> stack_limits;
Episode episode;
GameMode mode;
uint8_t difficulty;
Difficulty difficulty;
uint8_t section_id;
std::shared_ptr<const RareItemSet> rare_item_set;
std::shared_ptr<const ArmorRandomSet> armor_random_set;
+10 -7
View File
@@ -81,13 +81,14 @@ struct ItemData {
// QUICK ITEM FORMAT REFERENCE
// data1/0 data1/4 data1/8 data2
// Weapon: 00ZZZZGG SSNNAABB AABBAABB 00000000
// Armor: 0101ZZ00 FFTTDDDD EEEEXXXX 00000000
// Shield: 0102ZZ00 FFTTDDDD EEEEXXXX 00000000
// Unit: 0103ZZ00 FF00RRRR 0000XXXX 00000000
// Mag: 02ZZLLWW HHHHIIII JJJJKKKK YYQQPPVV
// Tool: 03ZZZZUU 00CC0000 0000XXXX 00000000
// Meseta: 04000000 00000000 00000000 MMMMMMMM
// Weapon: 00ZZZZGG SSNNAABB AABBAABB 00000000
// Armor: 0101ZZ00 FFTTDDDD EEEEXXXX 00000000
// Shield: 0102ZZ00 FFTTDDDD EEEEXXXX 00000000
// Unit: 0103ZZ00 FF00RRRR 0000XXXX 00000000
// Mag: 02ZZLLWW HHHHIIII JJJJKKKK YYQQPPVV
// Tool: 03ZZZZUU 00CC0000 0000XXXX 00000000
// 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)
// B = attribute amount (for S-ranks, custom name; last pair is kill count for some weapons)
// C = stack size (for tools)
@@ -113,6 +114,8 @@ struct ItemData {
// X = kill count (big-endian; high bit always set)
// Y = mag synchro
// Z = item ID
// & = technique level
// % = technique number
// 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
// versions). We manually byteswap data2 upon receipt and immediately before
+146 -23
View File
@@ -12,9 +12,16 @@ ItemNameIndex::ItemNameIndex(
limits(limits) {
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;
bool is_es_weapon = false;
try {
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));
} catch (const out_of_range&) {
}
@@ -25,11 +32,14 @@ ItemNameIndex::ItemNameIndex(
meta->name = *name;
this->primary_identifier_index.emplace(meta->primary_identifier, 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
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;
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) {
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(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]));
uint8_t flags = item.data2[2];
@@ -413,6 +423,112 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
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:")) {
auto tokens = phosg::split(desc, ' ');
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);
}
// TODO: It'd be nice to be able to parse S-rank weapon specials here too.
uint8_t weapon_special = 0;
if (!skip_special) {
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
ret.data1[4] = weapon_special | (is_wrapped ? 0x40 : 0x00) | (is_unidentified ? 0x80 : 0x00);
bool kill_count_set = false;
auto tokens = phosg::split(desc, ' ');
for (auto& token : tokens) {
if (token.empty()) {
@@ -517,26 +633,11 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
ret.data1[3] = stoul(token, nullptr, 10);
} else if (ret.is_s_rank_weapon()) {
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]);
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));
throw std::runtime_error("ES weapon must be prefixed with \"ES\"");
} else if (token.starts_with("k:")) {
ret.set_kill_count(stoul(token.substr(2), nullptr, 0));
kill_count_set = true;
} else {
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);
}
@@ -573,7 +674,29 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript
{"+", 0x0002},
{"++", 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
for (const auto& token : phosg::split(desc, ' ')) {
+1
View File
@@ -56,4 +56,5 @@ private:
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>> es_name_index;
};
+10
View File
@@ -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 {
// Use item combinations table from ItemPMT
bool combo_applied = false;
+8 -20
View File
@@ -146,26 +146,9 @@ Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id, bool is_game)
: server_state(s),
log(std::format("[{}:{:X}] ", is_game ? "Game" : "Lobby", id), lobby_log.min_level),
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>()),
rand_crypt(make_shared<DisabledRandomGenerator>()),
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) {
this->log.info_f("Created");
if (is_game) {
@@ -228,19 +211,23 @@ void Lobby::create_item_creator(Version logic_version) {
} else {
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>(
s->common_item_set(logic_version, this->quest),
s->rare_item_set(logic_version, this->quest),
s->armor_random_set,
s->tool_random_set,
s->weapon_random_sets.at(this->difficulty),
s->weapon_random_set(this->difficulty),
s->tekker_adjustment_set,
s->item_parameter_table(logic_version),
s->item_stack_limits(logic_version),
this->episode,
(this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode,
this->difficulty,
this->effective_section_id(),
effective_section_id,
rand_crypt,
this->quest ? this->quest->meta.battle_rules : nullptr);
}
@@ -256,7 +243,7 @@ uint8_t Lobby::effective_section_id() const {
if (leader) {
return leader->character_file()->disp.visual.section_id;
}
return 0;
return 0xFF;
}
uint16_t Lobby::quest_version_flags() const {
@@ -326,6 +313,7 @@ void Lobby::create_ep3_server() {
.rand_crypt = this->rand_crypt,
.tournament = tourn,
.trap_card_ids = s->ep3_trap_card_ids,
.output_queue = nullptr,
};
if (is_nte) {
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
+22 -22
View File
@@ -96,12 +96,12 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
uint32_t lobby_id;
uint32_t min_level;
uint32_t max_level;
uint32_t min_level = 0;
uint32_t max_level = 0xFFFFFFFF;
// Game state
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::shared_ptr<const MapState::RareEnemyRates> rare_enemy_rates;
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 are indexed as (1 << version), where version is a value from the
// Version enum.
uint16_t allowed_versions;
uint8_t creator_section_id;
uint8_t override_section_id;
Episode episode;
GameMode mode;
uint8_t difficulty; // 0-3
float base_exp_multiplier;
float exp_share_multiplier;
float challenge_exp_multiplier;
uint16_t allowed_versions = 0x0000;
uint8_t creator_section_id = 0xFF;
uint8_t override_section_id = 0xFF;
Episode episode = Episode::NONE;
GameMode mode = GameMode::NORMAL;
Difficulty difficulty = Difficulty::NORMAL;
float base_exp_multiplier = 1.0f;
float exp_share_multiplier = 0.5f;
float challenge_exp_multiplier = 1.0f;
std::string password;
std::string name;
// 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;
uint8_t allowed_drop_modes;
ServerDropMode drop_mode;
uint8_t allowed_drop_modes = 0x1F;
ServerDropMode drop_mode = ServerDropMode::CLIENT;
std::shared_ptr<ItemCreator> item_creator; // Always null for lobbies, never null for games
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;
// Lobby stuff
uint8_t event;
uint8_t block;
uint8_t leader_id;
uint8_t max_clients;
uint32_t enabled_flags;
uint8_t event = 0;
uint8_t block = 0;
uint8_t leader_id = 0;
uint8_t max_clients = 12;
uint32_t enabled_flags = 0;
std::shared_ptr<const Quest> quest;
std::array<std::shared_ptr<Client>, 12> clients;
// 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
// is not zero
uint64_t idle_timeout_usecs;
uint64_t idle_timeout_usecs = 0;
asio::steady_timer idle_timeout_timer;
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<ChallengeParameters> require_challenge_params() const;
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;
uint8_t client_extension_flags() const;
void load_maps();
+315 -92
View File
@@ -37,6 +37,7 @@
#include "PPKArchive.hh"
#include "PSOGCObjectGraph.hh"
#include "PSOProtocol.hh"
#include "PatchDownloadSession.hh"
#include "Quest.hh"
#include "QuestScript.hh"
#include "ReplaySession.hh"
@@ -1609,7 +1610,7 @@ Action a_disassemble_quest_script(
if (!args.get<bool>("decompressed")) {
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 use_qedit_names = args.get<bool>("qedit");
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(
"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\
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\
Edition). The output files are saved in system/client-functions.\n",
+[](phosg::Arguments&) {
auto fci = make_shared<FunctionCodeIndex>("system/client-functions");
+[](phosg::Arguments& args) {
auto fci = make_shared<FunctionCodeIndex>("system/client-functions", false);
auto process_code = +[](shared_ptr<const CompiledFunctionCode> code,
uint32_t checksum_addr,
uint32_t checksum_size,
uint32_t override_start_addr) -> void {
bool skip_encrypted = args.get<bool>("skip-encrypted");
auto process_code = [&](shared_ptr<const CompiledFunctionCode> code,
uint32_t checksum_addr,
uint32_t checksum_size,
uint32_t override_start_addr) -> void {
for (uint8_t encrypted = 0; encrypted < 2; encrypted++) {
if (encrypted && skip_encrypted) {
continue;
}
phosg::StringWriter w;
string data = prepare_send_function_call_data(
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"));
}
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>();
DownloadSession session(
io_context,
remote_host,
remote_port,
args.get<string>("output-dir", true),
version,
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"));
unique_ptr<DownloadSession> download_session;
unique_ptr<PatchDownloadSession> patch_download_session;
if (is_patch(version)) {
patch_download_session = std::make_unique<PatchDownloadSession>(
io_context,
remote_host,
remote_port,
args.get<string>("output-dir", true),
version,
args.get<string>("username", false),
args.get<string>("password", false),
args.get<string>("email", false),
args.get<bool>("show-command-data"));
asio::co_spawn(*io_context, patch_download_session->run(), asio::detached);
} else {
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);
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();
});
@@ -2089,7 +2114,8 @@ Action a_convert_rare_item_set(
.rel (Schtserv rare table; cannot be used in output 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\
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) {
double rate_factor = args.get<double>("multiply", 1.0);
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_item_definitions();
s->load_item_name_indexes();
s->load_drop_tables();
string input_filename = args.get<string>(1, false);
if (input_filename.empty() || (input_filename == "-")) {
@@ -2144,17 +2171,16 @@ Action a_convert_rare_item_set(
string data = rs->serialize_afs(is_v1);
write_output_data(args, data.data(), data.size(), nullptr);
} else if (output_filename_lower.ends_with(".html")) {
bool is_v1 = ::is_v1(get_cli_version(args, Version::BB_V4));
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (GameMode mode : modes) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
for (size_t difficulty = 0; difficulty < (is_v1 ? 3 : 4); difficulty++) {
if (!rs->has_entries_for_game_config(mode, episode, difficulty)) {
Version cli_version = get_cli_version(args, Version::BB_V4);
bool is_v1 = ::is_v1(cli_version);
for (GameMode mode : ALL_GAME_MODES_V4) {
for (Episode episode : ALL_EPISODES_V4) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
if ((is_v1 && (difficulty == Difficulty::ULTIMATE)) || (!rs->has_entries_for_game_config(mode, episode, difficulty))) {
continue;
}
auto item_name_index = s->item_name_index(get_cli_version(args, Version::BB_V4));
string data = rs->serialize_html(mode, episode, difficulty, item_name_index);
auto item_name_index = s->item_name_index(cli_version);
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);
phosg::save_file(out_filename, data);
phosg::log_info_f("... {}", out_filename);
@@ -2566,7 +2592,7 @@ Action a_generate_ep3_cards_html(
shared_ptr<const TextSet> text_english;
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&) {
}
@@ -2801,16 +2827,17 @@ Action a_show_ep3_maps(
s->load_ep3_cards();
s->load_ep3_maps();
const auto& map_ids = s->ep3_map_index->all();
phosg::log_info_f("{} maps", map_ids.size());
for (const auto& [map_number, map] : map_ids) {
const auto& all_maps = s->ep3_map_index->all_maps();
phosg::log_info_f("{} maps", all_maps.size());
for (const auto& [map_number, map] : all_maps) {
const auto& vms = map->all_versions();
for (size_t language = 0; language < vms.size(); language++) {
if (!vms[language]) {
for (size_t lang_index = 0; lang_index < vms.size(); lang_index++) {
if (!vms[lang_index]) {
continue;
}
string map_s = vms[language]->map->str(s->ep3_card_index.get(), language);
phosg::fwrite_fmt(stdout, "({}) {}\n", char_for_language_code(language), map_s);
Language language = static_cast<Language>(lang_index);
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) {
auto episode = static_cast<Episode>((it.first >> 28) & 7);
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 layout = (it.first >> 8) & 0xFF;
uint8_t entities = (it.first >> 0) & 0xFF;
@@ -2896,12 +2923,9 @@ Action a_check_supermaps(
// Generate MapStates for a few random variations
for (size_t z = 0; z < 0x20; z++) {
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
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;
Episode episode = ALL_EPISODES_V4[phosg::random_object<uint32_t>() % ALL_EPISODES_V4.size()];
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);
uint8_t event = phosg::random_object<uint32_t>() % 8;
uint32_t random_seed = phosg::random_object<uint32_t>();
phosg::fwrite_fmt(stderr, "FREE MAP STATE TEST: {} {} {}\n",
@@ -2986,7 +3010,7 @@ Action a_check_supermaps(
auto map_state = make_shared<MapState>(
0,
phosg::random_object<uint8_t>() & 3,
static_cast<Difficulty>(phosg::random_object<uint8_t>() & 3),
0,
phosg::random_object<uint32_t>(),
MapState::DEFAULT_RARE_ENEMIES,
@@ -3004,6 +3028,91 @@ Action a_check_supermaps(
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(
"check-quests", nullptr,
+[](phosg::Arguments& args) {
@@ -3014,7 +3123,25 @@ Action a_check_quests(
s->load_patch_indexes();
s->load_set_data_tables();
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(
@@ -3213,6 +3340,7 @@ Action a_replay_ep3_battle_commands(
.rand_crypt = make_shared<MT19937Generator>(seed),
.tournament = nullptr,
.trap_card_ids = {},
.output_queue = nullptr,
};
if (is_trial) {
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
@@ -3238,36 +3366,131 @@ Action a_replay_ep3_battle_commands(
Action a_replay_ep3_battle_record(
"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));
s->load_ep3_cards();
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 = {
.card_index = s->ep3_card_index,
.map_index = s->ep3_map_index,
.behavior_flags = (Episode3::BehaviorFlag::IGNORE_CARD_COUNTS |
Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES |
Episode3::BehaviorFlag::DISABLE_MASKING |
Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING),
.behavior_flags = rec->get_behavior_flags() & ~(Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING),
.opt_rand_stream = make_shared<phosg::StringReader>(rec->get_random_stream()),
.rand_crypt = make_shared<DisabledRandomGenerator>(),
.tournament = nullptr,
.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));
server->init();
for (const auto& command : rec->get_all_server_data_commands()) {
phosg::log_info_f("Server data command");
phosg::print_data(stderr, command, 0, nullptr, phosg::PrintDataFlags::PRINT_ASCII | phosg::PrintDataFlags::DISABLE_COLOR | phosg::PrintDataFlags::OFFSET_16_BITS);
server->on_server_data_input(nullptr, command);
// Ignore commands generated by the server when it's constructed (these
// are not included in the battle record)
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
View File
@@ -618,7 +618,8 @@ static const vector<DATEntityDefinition> dat_object_definitions({
// param4 = source type:
// 0 = use this set when advancing from a lower 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_EP3, 0x0000000000008001, "TObjPlayerSet"},
@@ -938,7 +939,7 @@ static const vector<DATEntityDefinition> dat_object_definitions({
// Radar collision. Params:
// param1 = radius
// param4 = TODO
// param4 = minimap segment ID
{0x0017, F_V0_V4, 0x00004FFFFFF8FFFE, "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
// is Challenge mode. Params:
// 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
// param6 = color (0 = blue, 1 = red)
{0x001B, F_V1_V4, 0x00005000000078FE, "TObjAreaWarpQuest"},
@@ -1005,10 +1006,10 @@ static const vector<DATEntityDefinition> dat_object_definitions({
// Params:
// param1 = TODO
// param2 = TODO
// param3 = TODO
// param4 = TODO
// param5 = TODO
// param6 = TODO
// param3 = TODO (1 byte only)
// param4 = TODO (1 byte only)
// param5 = TODO (1 byte only)
// param6 = TODO (1 byte only)
{0x001D, F_V2_V4, 0x0000400000000002, "TEffStarLight2D_Base"},
// 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:
// param1 = radius
// TODO: Test this.
{0x001F, F_V2_V4, 0x00004FFC3FFB07FE, "TObjRaderHideCol"},
// Item-detecting floor switch. Params:
@@ -1114,7 +1114,30 @@ static const vector<DATEntityDefinition> dat_object_definitions({
{0x0023, F_V2_V4, 0x00004FFC3FFB07FE, "TOAttackableCol"},
// 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
// param2 = damage value, scaled by difficulty:
// 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
// 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 this object just provides the timer functionality; the trigger
// switch flag must be activated by some other object. Params:
// that switch flag or activates up to three other switch flags. Note that
// this object just provides the timer functionality; the trigger switch
// flag must be activated by some other object. Params:
// angle.x = trigger mode:
// 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
// angle.y = if this is 1, play tick sound effect every second after
// activation (if any other value, no tick sound is played)
// angle.z = timer duration in frames
// 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)
// 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
// 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"},
// 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.
{0x0054, F_V0_V4, 0x0000600000000001, "TObjCityDoor_Lobby"},
// Version of the main warp for Challenge mode? This object seems to
// behave similarly to boss teleporters; it shows the player a Yes/No
// confirmation menu and sends 6x6A to synchronize state. There is a
// global named last_set_mainwarp_value which is set to param4 when this
// object is constructed, but may be changed by a set_mainwarp quest
// opcode after that. If that happens, this object replaces its
// dest_floor with the floor specified in the last set_mainwarp quest
// opcode. Params:
// Version of the main warp for Challenge mode. This object looks like the
// main Ragol warp on Pioneer 2, but behaves similarly to boss teleporters:
// it shows the player a Yes/No confirmation menu and sends 6x6A to
// synchronize state. There is a global named last_set_mainwarp_value which
// is set to param4 when this object is constructed, but may be changed by
// a set_mainwarp quest opcode after that. If that happens, this object
// replaces its dest_floor with the floor specified in the last
// set_mainwarp quest opcode. Params:
// param4 = destination floor
// 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"},
// Episode 2 Lab door. Params:
@@ -1378,15 +1399,20 @@ static const vector<DATEntityDefinition> dat_object_definitions({
{0x0057, F_V3_V4, 0x0000600000040001, "TObjTradeCollision"},
{0x0057, F_EP3, 0x0000000000000001, "TObjTradeCollision"},
// TODO: Describe this object. Presumably similar to TObjTradeCollision
// but enables the deck edit counter? Params:
// This object appears to be unused in both Ep3 NTE and the final release.
// 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
{0x0058, F_EP3, 0x0000000000000001, "TObjDeckCollision"},
// 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
// (second-lowest byte, modulo 0x0A)
// param6 = TODO (expected to be 0 or 1)
// (second-lowest byte, modulo 10)
// 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"},
// Forest switch. Params:
@@ -1395,9 +1421,13 @@ static const vector<DATEntityDefinition> dat_object_definitions({
{0x0081, F_V0_V4, 0x00004FF00078003E, "TObjDoorKey"},
// 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
// param6 = model (TODO)
// param6 = model (even value = short, odd value = long)
{0x0082, F_V0_V4, 0x00004FF0000300FE, "TObjLazerFenceNorm"},
{0x0083, F_V0_V4, 0x00004FF03FFB00FE, "TObjLazerFence4"},
@@ -1416,19 +1446,50 @@ static const vector<DATEntityDefinition> dat_object_definitions({
// param1-3 = TODO
{0x0086, F_V0_V4, 0x00004E0000000006, "TButterfly"},
// TODO: Describe this object. Params:
// param1 = model number
// Small vehicle. Params:
// param1 = model number (even value = crashed, odd value = intact)
{0x0087, F_V0_V4, 0x0000400000000006, "TMotorcycle"},
// Item box. Params:
// 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
// none at all (and param3-6 are all ignored)
// param3 = if zero, then bonuses, grinds, etc. are applied to the item
// after it's generated; if nonzero, the item is not randomized at
// all and drops exactly as specified in param4-6
// param4-6 = item definition (see base_item_for_specialized_box in
// ItemCreator.cc for how these values are decoded)
// param3 = if zero, then only data1[0-1] are used and the rest of the
// ItemData is cleared, then bonuses, grinds, etc. are applied to the
// item; if nonzero, the item is not randomized at all and drops
// exactly as specified in param4-6
// 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
// 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
@@ -1436,8 +1497,9 @@ static const vector<DATEntityDefinition> dat_object_definitions({
{0x0088, F_V0_V4, 0x00004FF0B00000FE, "TObjContainerBase2"},
{0x0088, F_EP3, 0x0000000000000002, "TObjContainerBase2"},
// Elevated cylindrical tank. Params:
// param1-3 = TODO
// Elevated cylindrical tank. There appear to be no parameters; param1-3
// are copied into the object instance as a Vector3F, but it appears that
// that vector is never used.
{0x0089, F_V0_V4, 0x0000400000000006, "TObjTank"},
// 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 + 1, param4 + 2, etc.)
// 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_V2_V4, 0x000040000000000E, "TObjContainerIdo"},
@@ -1470,8 +1532,9 @@ static const vector<DATEntityDefinition> dat_object_definitions({
// negative = enabled when player is within 70 units
// range [0x00, 0xFF] = enabled by corresponding switch flag
// 0x100 and above = never enabled
// param5 = message number (used with message quest opcode; TODO: has
// the same [100, 999] check as some other objects)
// param5 = message number ("character ID" in qedit; if this is outside
// 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
{0x008D, F_V0_V4, 0x00004000000027FE, "TOCapsuleAncient01"},
@@ -1489,7 +1552,7 @@ static const vector<DATEntityDefinition> dat_object_definitions({
{0x008F, F_V0_V4, 0x0000400000000006, "TObjHashi"},
// 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:
// param1 = activation mode:
// 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
// created.
// Params for TOHangceilingCave01Normal (param6 = 0):
// param1 = TODO (radius delta? value is param1 + 29)
// param2 = TODO (value is 1 - param2)
// param3 = TODO (value is param3 + 100)
// param1 = 1/4 time between crushes, in frames (value is param1 + 29, so
// total time between crushes is (param1 + 29) * 4 frames)
// 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):
// param1-3 = same as for TOHangceilingCave01Normal
// 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_V2_V4, 0x00004FF0007B00C6, "TOKeyMachine01"},
// Mines single-switch door, or Ep4 test door if in Episode 4. Params (for
// both object types):
// Mines single-switch door, or Subterranean Desert door if in Episode 4.
// Params (for both object types):
// param4 = switch flag number
{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.
{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).
{0x0106, F_V0_V4, 0x00004000000000C0, "TODragonflyMachine01"},
// Floating blue light. Params:
// param4 = TODO
// param5 = TODO
// param6 = TODO
// Floating rotating blue light (often appears next to doors). The light
// rotates about its vertical axis and floats up and down sinusoidally.
// Params:
// 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"},
// Self-destructing objects. Params:
@@ -1707,13 +1777,24 @@ static const vector<DATEntityDefinition> dat_object_definitions({
{0x0109, F_V0_V4, 0x00004000000000C0, "TOExplosiveMachine02"},
{0x010A, F_V0_V4, 0x00004000000000C0, "TOExplosiveMachine03"},
// Spark machine. Params:
// param1 = TODO (value is param1 - 0.98)
// param2 = TODO (seems it only matters if this is <= 0 or not)
// Spark machine. This looks like it's intended to appear in the bridge
// room in Mine 1, to create an effect of the columnar machines sparking.
// 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"},
// Large flashing box. Params:
// param2 = TODO (seems it only matters if this is < 0 or not)
// Open stall with a spinning red light on either side. Params:
// param2 = if > 0, a gray box is present in the left half of the stall
{0x010C, F_V0_V4, 0x00004000000000C0, "TOHangerMachine01"},
// 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
{0x0140, F_V0_V4, 0x0000400000000700, "TObjGoalWarpAncient"},
// Ruins intra-area warp. Same parameters as 0x0003 (TObjMapWarpForest),
// but also:
// param5 = type (negative = one-way, zero or positive = normal)
// Ruins intra-area warp. Params:
// param1-3 = destination (same as for TObjMapWarpForest)
// 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"},
// 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)
// param4 = switch flag number
// 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"},
// 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
// drops an item when destroyed. Unlike most other item boxes, it cannot
// be specialized (ignore_def is always true). Params:
// param1 = TODO (value is param1 + 299)
// param2 = TODO (value is param2 + 209)
// param3 = TODO (value is param3 + 399)
// param6 = TODO (value is param6 + 4)
// be specialized (ignore_def is always true). It cycles through the
// following 3 phases in order until it's destroyed:
// Phase 0: idle (duration depends on param1 and players' positions)
// Phase 1: preparing to spew poison (always lasts 60 frames)
// 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"},
// 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:
// param1 = trigger radius delta (value is (param1 / 2) + 30)
// 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"},
// Ruins crystal monument. Same parameters as TOCapsuleAncient01.
@@ -1885,7 +1980,7 @@ static const vector<DATEntityDefinition> dat_object_definitions({
// 2 = Gibarta
// 3 = Megid
// 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"},
// Flying white bird. Params:
@@ -2029,7 +2124,8 @@ static const vector<DATEntityDefinition> dat_object_definitions({
// Episode 3 lobby jukebox. No parameters.
{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"},
// 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"},
// 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)
// param5-6: same as 0x020F (TOTrapChainSawDamage)
// param5-6 = same as 0x020F (TOTrapChainSawDamage)
{0x0210, F_V3_V4, 0x0000400C3F800000, "TOTrapChainSawKey"},
// 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_EP3, 0x0000000000000002, "__SEAGULL__"},
// TODO: Describe this object. Params:
// param4 = model number (clamped to [0, 2])
// Jungle wooden objects. Params:
// param4 = model number:
// 0 or negative: long branch
// 1: shorter curved branch
// 2 or greater: small log
{0x0213, F_V3_V4, 0x00004E040F000000, "TOJungleDesign"},
// 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
{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"},
{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).
// TObjCardCityMapWarp takes no parameters.
{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)
// Morgue doors. None of these take any parameters. Unsurprisingly, the
// _Closed variants don't open.
{0x02D4, F_EP3, 0x0000000000000001, "TObjCardCityDoor(0)"}, // Yellow (to deck edit room)
{0x02D5, F_EP3, 0x0000000000000001, "TObjCardCityDoor(1)"}, // Blue (to battle entry counter)
{0x02D8, F_EP3, 0x0000000000000001, "TObjCardCityDoor(2)"}, // TODO
{0x02DF, F_EP3, 0x0000000000000001, "TObjCardCityDoor(3)"}, // Solid (always closed in normal Morgue)
// _Closed variants don't open. The _Closed variants also have a red light
// in the center instead of a blue light. Curiously, the (3) variant is
// opaque when _Closed, unlike the other _Closed doors.
{0x02D4, F_EP3, 0x0000000000000001, "TObjCardCityDoor(0)"}, // Yellow V-pattern (to deck edit room)
{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)
{0x02DC, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(0)"}, // TODO
{0x02DD, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(1)"}, // TODO
{0x02DE, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(2)"}, // TODO
{0x02E1, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(3)"}, // TODO
{0x02E2, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(4)"}, // TODO
{0x02DC, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(0)"}, // Yellow V-pattern (to deck edit room)
{0x02DD, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(1)"}, // Blue V-pattern (to battle entry counter)
{0x02DE, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(2)"}, // Green V-pattern (unused)
{0x02E1, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(3)"}, // Opaque gray X-pattern
{0x02E2, F_EP3, 0x0000000000000001, "TObjCardCityDoor_Closed(4)"}, // Gray (to chief)
// Mortis Fons geyser. Params:
// 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-
// significant bit; see Episode3LobbyBanners in config.example.json
// 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"},
// TODO: Describe this object. Params:
// param4 = model number (0 or 1)
// Morgue info screen. Params:
// param4 = model number:
// 0 = tall, yellow, vertical-scrolling
// 1 = short, blue, horizontal-scrolling
{0x02E5, F_EP3, 0x0000000000000001, "TObjCityMoji"},
// 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 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,
name_str,
this->floor,
@@ -3416,8 +3523,7 @@ string MapFile::ObjectSetEntry::str(Version version, uint8_t area) const {
this->param3,
this->param4,
this->param5,
this->param6,
this->unused);
this->param6);
}
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 {
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,
type_name,
this->num_children,
@@ -3459,8 +3565,7 @@ string MapFile::EnemySetEntry::str(Version version, uint8_t area) const {
this->param4,
this->param5,
this->param6,
this->param7,
this->unused);
this->param7);
}
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 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={}",
this->floor,
this->super_set_id,
this->super_id,
string ret = std::format("[Enemy {} type={} child_index={:X} alias_target_ene={} is_default_rare_v123={} is_default_rare_bb={}",
this->id_str(),
phosg::name_for_enum(this->type),
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_bb ? "true" : "false");
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(
Version version,
uint8_t floor,
const MapFile::EnemySetEntry* set_entry) {
Version version, uint8_t floor, const MapFile::EnemySetEntry* set_entry) {
shared_ptr<Enemy> head_ene = nullptr;
size_t next_child_index = 0;
auto add = [&](EnemyType type,
bool is_default_rare_v123 = 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);
// 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->is_default_rare_v123 = is_default_rare_v123;
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);
ene_ver.set_entry = set_entry;
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
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);
return ene;
};
// 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
add(EnemyType::VOL_OPT_2);
break;
case 0x00C8: // TBoss4DarkFalz
case 0x00C8: { // TBoss4DarkFalz
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);
}
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)
for (size_t x = 0; x < 0x1FD; x++) {
add(EnemyType::DARVANT);
}
add(EnemyType::DARK_FALZ_3, false, false, -0x1FE);
add(EnemyType::DARK_FALZ_2, false, false, -0x1FF);
add(EnemyType::DARK_FALZ_1, false, false, -0x200);
add(EnemyType::DARK_FALZ_3, false, false, root_ene);
add(EnemyType::DARK_FALZ_2, false, false, root_ene);
add(EnemyType::DARK_FALZ_1, false, false, root_ene);
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)
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;
}
case 0x00CB: // TBoss7DeRolLeC
add(EnemyType::BARBA_RAY);
child_type = EnemyType::PIG_RAY;
@@ -5383,25 +5491,16 @@ vector<shared_ptr<const SuperMap::Object>> SuperMap::doors_for_switch_flag(
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);
if (entities.enemies.empty()) {
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");
}
auto& enemy = entities.enemies[enemy_id];
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;
}
return entities.enemies[enemy_index];
}
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()) {
throw out_of_range("no enemies defined");
}
// TODO: Linear search is bad here. Do something better, like binary search
// for the floor start and just linear search through the floor enemies.
for (auto& ene : entities.enemies) {
if ((ene->floor == floor) && (ene->type == type)) {
size_t start_z = entities.enemy_floor_start_indexes.at(floor);
size_t end_z = (floor < entities.enemy_floor_start_indexes.size() - 1)
? entities.enemy_floor_start_indexes[floor + 1]
: 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;
}
}
@@ -5953,7 +6055,7 @@ size_t MapState::EventIterator::num_entities_on_current_floor() const {
MapState::MapState(
uint64_t lobby_or_session_id,
uint8_t difficulty,
Difficulty difficulty,
uint8_t event,
uint32_t random_seed,
std::shared_ptr<const RareEnemyRates> bb_rare_rates,
@@ -6003,7 +6105,7 @@ MapState::MapState(
MapState::MapState(
uint64_t lobby_or_session_id,
uint8_t difficulty,
Difficulty difficulty,
uint8_t event,
uint32_t random_seed,
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()) {
auto& ene_st = this->enemy_states.emplace_back(make_shared<EnemyState>());
if (ene->child_index == 0) {
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->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
EnemyType type;
switch (ene->type) {
case EnemyType::DARK_FALZ_3:
type = ((this->difficulty == 0) && (ene->alias_enemy_index_delta == 0))
? EnemyType::DARK_FALZ_2
: EnemyType::DARK_FALZ_3;
type = (this->difficulty == Difficulty::NORMAL) ? EnemyType::DARK_FALZ_2 : EnemyType::DARK_FALZ_3;
break;
case EnemyType::DARVANT:
type = (this->difficulty == 3) ? EnemyType::DARVANT_ULTIMATE : EnemyType::DARVANT;
type = (this->difficulty == Difficulty::ULTIMATE) ? EnemyType::DARVANT_ULTIMATE : EnemyType::DARVANT;
break;
default:
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);
}
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) {
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) {
@@ -6245,6 +6383,22 @@ vector<shared_ptr<MapState::ObjectState>> MapState::door_states_for_switch_flag(
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) {
const auto& fc = this->floor_config(floor);
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& 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);
if (ene_st->super_ene != ene) {
throw logic_error("super enemy link is incorrect");
}
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})",
enemy_index, ene_st->e_id, entry.flags, ene_st->game_flags);
ene_st->game_flags = entry.flags;
}
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 ({})",
enemy_index, ene_st->e_id, entry.total_damage, ene_st->total_damage);
ene_st->total_damage = entry.total_damage;
// Only set the state if it's not an alias
if (ene_st->super_ene == ene) {
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})",
enemy_index, ene_st->e_id, entry.flags, ene_st->game_flags);
ene_st->game_flags = entry.flags;
}
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 ({})",
enemy_index, ene_st->e_id, entry.total_damage, ene_st->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, "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");
for (size_t floor = 0; floor < this->floor_config_entries.size(); floor++) {
auto fc = this->floor_config_entries[floor];
+15 -8
View File
@@ -187,7 +187,7 @@ public:
/* 34 */ le_int32_t param4 = 0;
/* 38 */ le_int32_t param5 = 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 */
uint64_t semantic_hash(uint8_t floor) const;
@@ -215,7 +215,7 @@ public:
/* 3C */ le_float param5 = 0.0f;
/* 40 */ le_int16_t param6 = 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 */
uint64_t semantic_hash(uint8_t floor) const;
@@ -491,7 +491,7 @@ public:
size_t super_id = 0;
size_t super_set_id = 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;
bool is_default_rare_v123 = false;
bool is_default_rare_bb = false;
@@ -585,7 +585,7 @@ public:
std::vector<std::shared_ptr<const Object>> doors_for_switch_flag(
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::vector<std::shared_ptr<const Enemy>> enemies_for_floor_room_wave(
Version version, uint8_t floor, uint16_t room, uint16_t wave_number) const;
@@ -710,6 +710,7 @@ public:
};
struct EnemyState {
std::shared_ptr<EnemyState> alias_target_ene_st; // Null for most enemies
std::shared_ptr<const SuperMap::Enemy> super_ene;
enum Flag {
LAST_HIT_MASK = 0x0003,
@@ -745,7 +746,7 @@ public:
inline void set_mericarand_variant_flag(Version 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->is_rare(version)) {
return ((this->mericarand_variant_flags >> static_cast<size_t>(version)) & 1)
@@ -754,6 +755,10 @@ public:
} else {
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 {
return this->is_rare(version)
? type_definition_for_enemy(this->super_ene->type).rare_type(episode, event, this->super_ene->floor)
@@ -874,7 +879,7 @@ public:
phosg::PrefixedLogger log;
std::vector<FloorConfig> floor_config_entries;
uint8_t difficulty = 0;
Difficulty difficulty = Difficulty::NORMAL;
uint8_t event = 0;
uint32_t random_seed = 0;
std::shared_ptr<const RareEnemyRates> bb_rare_rates;
@@ -889,7 +894,7 @@ public:
// Constructor for free play
MapState(
uint64_t lobby_or_session_id,
uint8_t difficulty,
Difficulty difficulty,
uint8_t event,
uint32_t random_seed, // For client-matched rare enemies (non-BB)
std::shared_ptr<const RareEnemyRates> bb_rare_rates,
@@ -898,7 +903,7 @@ public:
// Constructor for quests
MapState(
uint64_t lobby_or_session_id,
uint8_t difficulty,
Difficulty difficulty,
uint8_t event,
uint32_t random_seed, // For client-matched rare enemies (non-BB)
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 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::vector<std::shared_ptr<ObjectState>> object_states_for_floor_room_group(
Version version, uint8_t floor, uint16_t room, uint16_t group);
std::vector<std::shared_ptr<ObjectState>> door_states_for_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_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);
+1 -1
View File
@@ -24,7 +24,7 @@ constexpr uint32_t QUEST_EP2 = 0x55020255;
constexpr uint32_t QUEST_EP3 = 0x55030355;
// See the decsription of the A2 command in CommandFormats.hh for why these
// 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 PROXY_DESTINATIONS = 0x77000077;
constexpr uint32_t PROGRAMS = 0x88000088;
+285
View File
@@ -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");
}
}
+61
View File
@@ -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);
};
+2 -2
View File
@@ -109,8 +109,8 @@ PatchFileIndex::PatchFileIndex(const string& root_dir)
if (!compute_crc32s_message.empty()) {
auto data = f->load_data(); // Sets f->size
f->crc32 = phosg::crc32(data->data(), f->size);
for (size_t x = 0; x < data->size(); x += 0x4000) {
size_t chunk_bytes = min<size_t>(f->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, this->CHUNK_SIZE);
f->chunk_crcs.emplace_back(phosg::crc32(data->data() + x, chunk_bytes));
}
+2
View File
@@ -9,6 +9,8 @@
#include <vector>
struct PatchFileIndex {
static constexpr size_t CHUNK_SIZE = 0x6000;
explicit PatchFileIndex(const std::string& root_dir);
struct File {
+10 -14
View File
@@ -116,7 +116,7 @@ struct PlayerInventoryT {
/* 0000 */ uint8_t num_items = 0;
/* 0001 */ uint8_t hp_from_materials = 0;
/* 0002 */ uint8_t tp_from_materials = 0;
/* 0003 */ uint8_t language = 0;
/* 0003 */ Language language = Language::JAPANESE;
/* 0004 */ parray<PlayerInventoryItemT<BE>, 30> items;
/* 034C */
@@ -179,11 +179,11 @@ struct PlayerInventoryT {
}
}
void equip_item_id(uint32_t item_id, EquipSlot slot, bool allow_overwrite) {
this->equip_item_index(this->find_item(item_id), slot, allow_overwrite);
void equip_item_id(uint32_t item_id, EquipSlot slot) {
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];
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 (allow_overwrite) {
this->unequip_item_slot(slot);
} else {
throw std::runtime_error("equip slot is already in use");
}
this->unequip_item_slot(slot);
}
item.flags |= 0x00000008;
@@ -269,14 +265,14 @@ struct PlayerInventoryT {
// issue - its inventory format matches the rest of the versions.
this->hp_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)) {
if (this->language > 4) {
this->language = 0;
if (static_cast<size_t>(this->language) > 4) {
this->language = Language::JAPANESE;
}
} else {
if (this->language > 7) {
this->language = 0;
if (static_cast<size_t>(this->language) > 7) {
this->language = Language::JAPANESE;
}
}
+8 -23
View File
@@ -50,7 +50,7 @@ void GuildCardBB::clear() {
this->team_name.clear();
this->description.clear();
this->present = 0;
this->language = 0;
this->language = Language::JAPANESE;
this->section_id = 0;
this->char_class = 0;
}
@@ -218,11 +218,11 @@ PlayerRecordsChallengeBB::PlayerRecordsChallengeBB(const PlayerRecordsChallengeD
grave_x(rec.grave_x),
grave_y(rec.grave_y),
grave_z(rec.grave_z),
grave_team(rec.grave_team.decode(), 1),
grave_message(rec.grave_message.decode(), 1),
grave_team(rec.grave_team.decode(), Language::ENGLISH),
grave_message(rec.grave_message.decode(), Language::ENGLISH),
unknown_m5(0),
unknown_t6(0),
rank_title(rec.rank_title.decode(), 1),
rank_title(rec.rank_title.decode(), Language::ENGLISH),
unknown_l7(0) {}
PlayerRecordsChallengeBB::PlayerRecordsChallengeBB(const PlayerRecordsChallengePC& rec)
@@ -242,11 +242,11 @@ PlayerRecordsChallengeBB::PlayerRecordsChallengeBB(const PlayerRecordsChallengeP
grave_x(rec.grave_x),
grave_y(rec.grave_y),
grave_z(rec.grave_z),
grave_team(rec.grave_team.decode(), 1),
grave_message(rec.grave_message.decode(), 1),
grave_team(rec.grave_team.decode(), Language::ENGLISH),
grave_message(rec.grave_message.decode(), Language::ENGLISH),
unknown_m5(0),
unknown_t6(0),
rank_title(rec.rank_title.decode(), 1),
rank_title(rec.rank_title.decode(), Language::ENGLISH),
unknown_l7(0) {}
PlayerRecordsChallengeBB::operator PlayerRecordsChallengeDC() const {
@@ -309,22 +309,7 @@ PlayerRecordsChallengeBB::operator PlayerRecordsChallengePC() const {
return ret;
}
QuestFlagsV1& QuestFlagsV1::operator=(const QuestFlags& other) {
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{{
const QuestFlagsForDifficulty BB_QUEST_FLAG_APPLY_MASK{{
// clang-format off
/* 0000 */ 0x00, 0x3F, 0xFF, 0xE3, 0xE0, 0xFF, 0xFF, 0x00,
/* 0040 */ 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00,
+91 -59
View File
@@ -328,7 +328,7 @@ struct PlayerDispDataDCPCV3T {
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));
using PlayerDispDataDCPCV3 = PlayerDispDataDCPCV3T<false>;
using PlayerDispDataDCPCV3BE = PlayerDispDataDCPCV3T<true>;
@@ -361,7 +361,7 @@ struct PlayerDispDataBB {
}
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;
ret.stats = this->stats;
ret.visual = this->visual;
@@ -377,7 +377,7 @@ struct PlayerDispDataBB {
} __packed_ws__(PlayerDispDataBB, 0x190);
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;
bb.stats = this->stats;
bb.visual = this->visual;
@@ -398,7 +398,7 @@ struct GuildCardDCNTE {
/* 20 */ pstring<TextEncoding::MARKED, 0x48> description;
/* 68 */ parray<uint8_t, 0x0F> unused2;
/* 77 */ uint8_t present = 0;
/* 78 */ uint8_t language = 0;
/* 78 */ Language language = Language::JAPANESE;
/* 79 */ uint8_t section_id = 0;
/* 7A */ uint8_t char_class = 0;
/* 7B */
@@ -413,7 +413,7 @@ struct GuildCardDC {
/* 20 */ pstring<TextEncoding::MARKED, 0x48> description;
/* 68 */ parray<uint8_t, 0x11> unused2;
/* 79 */ uint8_t present = 0;
/* 7A */ uint8_t language = 0;
/* 7A */ Language language = Language::JAPANESE;
/* 7B */ uint8_t section_id = 0;
/* 7C */ uint8_t char_class = 0;
/* 7D */
@@ -428,7 +428,7 @@ struct GuildCardPC {
/* 08 */ pstring<TextEncoding::UTF16, 0x18> name;
/* 38 */ pstring<TextEncoding::UTF16, 0x5A> description;
/* EC */ uint8_t present = 0;
/* ED */ uint8_t language = 0;
/* ED */ Language language = Language::JAPANESE;
/* EE */ uint8_t section_id = 0;
/* EF */ uint8_t char_class = 0;
/* F0 */
@@ -455,11 +455,11 @@ struct GuildCardGCT {
/* 04:04 */ U32T<BE> guild_card_number = 0;
/* 08:08 */ pstring<TextEncoding::ASCII, 0x18> name;
/* 20:20 */ pstring<TextEncoding::MARKED, DescriptionLength> description;
/* 8C:8C */ uint8_t present = 0;
/* 8D:8D */ uint8_t language = 0;
/* 8E:8E */ uint8_t section_id = 0;
/* 8F:8F */ uint8_t char_class = 0;
/* 90:90 */
/* A0:8C */ uint8_t present = 0;
/* A1:8D */ Language language = Language::JAPANESE;
/* A2:8E */ uint8_t section_id = 0;
/* A3:8F */ uint8_t char_class = 0;
/* A4:90 */
operator GuildCardBB() const;
} __attribute__((packed));
@@ -480,7 +480,7 @@ struct GuildCardXB {
/* 0010 */ pstring<TextEncoding::ASCII, 0x18> name;
/* 0028 */ pstring<TextEncoding::MARKED, 0x200> description;
/* 0228 */ uint8_t present = 0;
/* 0229 */ uint8_t language = 0;
/* 0229 */ Language language = Language::JAPANESE;
/* 022A */ uint8_t section_id = 0;
/* 022B */ uint8_t char_class = 0;
/* 022C */
@@ -494,7 +494,7 @@ struct GuildCardBB {
/* 0034 */ pstring<TextEncoding::UTF16_ALWAYS_MARKED, 0x10> team_name;
/* 0054 */ pstring<TextEncoding::UTF16, 0x58> description;
/* 0104 */ uint8_t present = 0;
/* 0105 */ uint8_t language = 0;
/* 0105 */ Language language = Language::JAPANESE;
/* 0106 */ uint8_t section_id = 0;
/* 0107 */ uint8_t char_class = 0;
/* 0108 */
@@ -776,13 +776,13 @@ struct PlayerRecordsChallengeBB {
grave_x(rec.grave_x),
grave_y(rec.grave_y),
grave_z(rec.grave_z),
grave_team(rec.grave_team.decode(), 1),
grave_message(rec.grave_message.decode(), 1),
grave_team(rec.grave_team.decode(), Language::ENGLISH),
grave_message(rec.grave_message.decode(), Language::ENGLISH),
unknown_m5(rec.unknown_m5),
ep1_online_award_state(rec.ep1_online_award_state),
ep2_online_award_state(rec.ep2_online_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) {
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];
@@ -810,8 +810,8 @@ struct PlayerRecordsChallengeBB {
ret.grave_x = this->grave_x;
ret.grave_y = this->grave_y;
ret.grave_z = this->grave_z;
ret.grave_team.encode(this->grave_team.decode(), 1);
ret.grave_message.encode(this->grave_message.decode(), 1);
ret.grave_team.encode(this->grave_team.decode(), Language::ENGLISH);
ret.grave_message.encode(this->grave_message.decode(), Language::ENGLISH);
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++) {
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.ep2_online_award_state = this->ep2_online_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;
return ret;
}
@@ -856,38 +856,53 @@ check_struct_size(PlayerRecordsBattle, 0x18);
check_struct_size(PlayerRecordsBattleBE, 0x18);
template <typename DestT, typename SrcT = DestT>
DestT convert_player_disp_data(const SrcT&, uint8_t, uint8_t) {
DestT convert_player_disp_data(const SrcT&, Language, Language) {
static_assert(phosg::always_false<DestT, SrcT>::v,
"unspecialized convert_player_disp_data should never be called");
}
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;
}
template <>
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);
}
template <>
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);
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
const PlayerDispDataBB& src, uint8_t, uint8_t) {
const PlayerDispDataBB& src, Language, Language) {
return src;
}
struct QuestFlagsForDifficulty {
static const QuestFlagsForDifficulty BB_QUEST_FLAG_APPLY_MASK;
template <size_t NumFlags>
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 {
size_t byte_index = flag_index >> 3;
@@ -904,6 +919,7 @@ struct QuestFlagsForDifficulty {
uint8_t mask = 0x80 >> (flag_index & 7);
this->data[byte_index] &= (~mask);
}
inline void update_all(bool set) {
if (set) {
this->data.clear(0xFF);
@@ -911,50 +927,66 @@ struct QuestFlagsForDifficulty {
this->data.clear(0x00);
}
}
} __packed_ws__(QuestFlagsForDifficulty, 0x80);
} __attribute__((packed));
struct QuestFlags {
parray<QuestFlagsForDifficulty, 4> data;
template <size_t NumFlagsPerTable, size_t NumTables, typename TableIndexT = size_t>
struct FlagsTable {
parray<FlagsArray<NumFlagsPerTable>, NumTables> data;
inline bool get(uint8_t difficulty, uint16_t flag_index) const {
return this->data[difficulty].get(flag_index);
FlagsTable() = default;
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) {
this->data[difficulty].clear(flag_index);
inline const FlagsArray<NumFlagsPerTable>& array(TableIndexT which) const {
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) {
for (size_t z = 0; z < 4; z++) {
for (size_t z = 0; z < this->data.size(); z++) {
this->update_all(z, set);
}
}
} __packed_ws__(QuestFlags, 0x200);
} __attribute__((packed));
struct QuestFlagsV1 {
parray<QuestFlagsForDifficulty, 3> data;
using QuestFlagsForDifficulty = FlagsArray<0x400>;
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);
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);
extern const QuestFlagsForDifficulty BB_QUEST_FLAG_APPLY_MASK;
struct BattleRules {
enum class TechDiskMode : uint8_t {
+34 -11
View File
@@ -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>;
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;
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;
@@ -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());
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;
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) {
shared_ptr<MapState::ObjectState> obj_st;
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) {
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) {
if (msg.command == 0x67) {
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_quest = false;
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_mode = GameMode::NORMAL;
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->floor = 0;
c->proxy_session->is_in_lobby = false;
c->proxy_session->is_in_game = true;
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_difficulty = cmd->difficulty;
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 {
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_mode = GameMode::NORMAL;
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>();
c->floor = 0;
c->proxy_session->is_in_lobby = false;
c->proxy_session->is_in_game = true;
c->proxy_session->is_in_quest = false;
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_mode = GameMode::NORMAL;
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) {
c->floor = 0x0F;
c->proxy_session->is_in_lobby = false;
c->proxy_session->is_in_game = false;
c->proxy_session->is_in_quest = false;
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_episode = Episode::EP1;
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 0x4C:
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);
}
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;
break;
case 0x40:
c->pos = msg.check_size_t<G_WalkToPosition_6x40>().pos;
case 0x40: {
const auto& cmd = msg.check_size_t<G_WalkToPosition_6x40>();
c->pos.x = cmd.pos.x;
c->pos.z = cmd.pos.z;
break;
}
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;
}
case 0x48:
if (!is_v1(c->version()) && c->check_flag(Client::Flag::INFINITE_TP_ENABLED)) {
+1 -1
View File
@@ -31,7 +31,7 @@ void ProxySession::set_drop_mode(
s->rare_item_set(version, nullptr),
s->armor_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->item_parameter_table(version),
s->item_stack_limits(version),
+3 -2
View File
@@ -28,16 +28,17 @@ struct ProxySession {
uint32_t guild_card_number = 0;
uint64_t xb_user_id = 0;
std::string name;
uint8_t language = 0;
Language language = Language::JAPANESE;
uint8_t section_id = 0;
uint8_t char_class = 0;
};
std::vector<LobbyPlayer> lobby_players;
bool is_in_lobby = false;
bool is_in_game = false;
bool is_in_quest = false;
uint8_t leader_client_id = 0;
uint8_t lobby_event = 0;
uint8_t lobby_difficulty = 0;
Difficulty lobby_difficulty = Difficulty::NORMAL;
uint8_t lobby_section_id = 0;
GameMode lobby_mode = GameMode::NORMAL;
Episode lobby_episode = Episode::EP1;
+62 -30
View File
@@ -203,7 +203,7 @@ void VersionedQuest::assert_valid() const {
if (this->version == Version::UNKNOWN) {
throw runtime_error("version is not set");
}
if (this->language == 0xFF) {
if (this->language == Language::UNKNOWN) {
throw runtime_error("language is not set");
}
switch (this->meta.episode) {
@@ -290,7 +290,7 @@ string VersionedQuest::pvr_filename() const {
string VersionedQuest::xb_filename() const {
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 {
@@ -301,7 +301,7 @@ string VersionedQuest::encode_qst() const {
files.emplace(std::format("quest{}.pvr", this->meta.quest_number), this->pvr_contents);
}
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);
}
@@ -315,7 +315,7 @@ phosg::JSON Quest::json() const {
for (const auto& [_, vq] : this->versions) {
versions_json.emplace_back(phosg::JSON::dict({
{"Version", phosg::name_for_enum(vq->version)},
{"Language", name_for_language_code(vq->language)},
{"Language", ::name_for_language(vq->language)},
{"Name", vq->meta.name},
{"ShortDescription", vq->meta.short_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) {
return (static_cast<uint32_t>(v) << 8) | language;
uint32_t Quest::versions_key(Version v, Language 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) {
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);
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 {
@@ -349,7 +375,7 @@ std::shared_ptr<const SuperMap> Quest::get_supermap(int64_t random_seed) const {
bool any_map_file_present = false;
array<shared_ptr<const MapFile>, NUM_VERSIONS> map_files;
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) {
auto map_file = vq->map_file;
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;
}
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));
}
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);
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
try {
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
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&) {
}
// Return the first language, if it exists
auto it = this->versions.lower_bound(this->versions_key(v, 0));
if ((it == this->versions.end()) || ((it->first & 0xFF00) != 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, Language::JAPANESE))) {
return nullptr;
}
return it->second;
@@ -413,7 +439,8 @@ QuestIndex::QuestIndex(
const string& directory,
shared_ptr<const QuestCategoryIndex> category_index,
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),
category_index(category_index) {
@@ -562,6 +589,9 @@ QuestIndex::QuestIndex(
}
} 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());
}
}
@@ -626,7 +656,7 @@ QuestIndex::QuestIndex(
if (language_token.size() != 1) {
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);
@@ -717,6 +747,10 @@ QuestIndex::QuestIndex(
vq->meta.lock_status_register = metadata_json.get_int("LockStatusRegister");
} 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 {
vq->meta.common_item_set_name = metadata_json.at("CommonItemSetName").as_string();
} catch (const out_of_range&) {
@@ -757,31 +791,32 @@ QuestIndex::QuestIndex(
auto q_it = this->quests_by_number.find(vq->meta.quest_number);
if (q_it != this->quests_by_number.end()) {
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,
phosg::name_for_enum(vq->version),
char_for_language_code(vq->language),
char_for_language(vq->language),
vq->meta.quest_number,
vq->meta.name,
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
vq->meta.name);
} else {
auto q = make_shared<Quest>(vq);
this->quests_by_number.emplace(vq->meta.quest_number, 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);
static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {}) with floors {}",
static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {})",
filenames_str,
phosg::name_for_enum(vq->version),
char_for_language_code(vq->language),
char_for_language(vq->language),
vq->meta.quest_number,
vq->meta.name,
name_for_episode(vq->meta.episode),
category_name,
vq->meta.category_id,
vq->meta.joinable ? "joinable" : "not joinable",
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
vq->meta.joinable ? "joinable" : "not joinable");
}
} 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());
}
}
@@ -810,9 +845,6 @@ phosg::JSON QuestIndex::json() const {
{"Categories", std::move(categories_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 {
@@ -915,7 +947,7 @@ string encode_download_quest_data(const string& compressed_data, size_t decompre
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
// 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
@@ -938,7 +970,7 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
if (decompressed_bin.size() < sizeof(PSOQuestHeaderDC)) {
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;
}
break;
@@ -947,7 +979,7 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
if (decompressed_bin.size() < sizeof(PSOQuestHeaderPC)) {
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;
}
break;
@@ -957,7 +989,7 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
if (decompressed_bin.size() < sizeof(PSOQuestHeaderGC)) {
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;
}
break;
+12 -8
View File
@@ -72,7 +72,7 @@ struct VersionedQuest {
// Most of these default values are intentionally invalid; we use these
// values to check if each field was parsed during quest indexing.
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> dat_contents;
std::shared_ptr<const MapFile> map_file;
@@ -86,7 +86,7 @@ struct VersionedQuest {
std::string pvr_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;
};
@@ -94,6 +94,7 @@ struct Quest {
QuestMetadata meta;
mutable std::shared_ptr<const SuperMap> supermap;
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
std::array<std::string, 8> names_by_language;
Quest() = delete;
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;
void add_version(std::shared_ptr<const VersionedQuest> vq);
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;
const std::string& name_for_language(Language 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 {
@@ -133,7 +136,8 @@ struct QuestIndex {
const std::string& directory,
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 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;
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
+137 -3
View File
@@ -44,6 +44,28 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
"quest version has a different lock status register (existing: {:04X}, new: {:04X})",
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) {
throw runtime_error(std::format(
"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) {
throw runtime_error(std::format(
"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++) {
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) {
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({
{"CategoryID", this->category_id},
{"Number", this->quest_number},
{"Episode", name_for_episode(this->episode)},
{"FloorAssignments", floors_json},
{"FloorAssignments", std::move(floors_json)},
{"Joinable", this->joinable},
{"MaxPlayers", this->max_players},
{"BattleRules", this->battle_rules ? this->battle_rules->json() : 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)},
{"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},
{"AvailableExpression", 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)},
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
{"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
View File
@@ -9,10 +9,12 @@
#include <vector>
#include "CommonItemSet.hh"
#include "EnemyType.hh"
#include "IntegralExpression.hh"
#include "Map.hh"
#include "PlayerSubordinates.hh"
#include "RareItemSet.hh"
#include "StaticGameData.hh"
struct QuestMetadata {
// This structure contains configuration that should be the same across all
@@ -28,7 +30,7 @@ struct QuestMetadata {
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index = -1;
float challenge_exp_multiplier = -1.0f;
int8_t challenge_difficulty = -1;
Difficulty challenge_difficulty = Difficulty::UNKNOWN;
uint8_t description_flag = 0x00;
std::shared_ptr<const IntegralExpression> available_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
bool allow_start_from_chat_command = false;
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 short_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 assert_compatible(const QuestMetadata& other) const;
phosg::JSON json() const;
+216 -82
View File
@@ -62,17 +62,6 @@ using AttackData = BattleParamsIndex::AttackData;
using ResistData = BattleParamsIndex::ResistData;
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 array<const char*, 3> names = {"Episode1", "Episode2", "Episode4"};
try {
@@ -82,8 +71,8 @@ static const char* name_for_header_episode_number(uint8_t episode) {
}
}
static TextEncoding encoding_for_language(uint8_t language) {
return (language ? TextEncoding::ISO8859 : TextEncoding::SJIS);
static TextEncoding encoding_for_language(Language language) {
return ((language == Language::JAPANESE) ? TextEncoding::SJIS : TextEncoding::ISO8859);
}
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)
// <fXX> => value of rXX as %f (floating-point) (v3 and later)
// <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
// <hero name> or <name hero> => character's name
// <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
// 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]
// 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},
@@ -1913,15 +1906,15 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// Specifies which enemy should be affected by subsequent get_*_data
// opcodes (the following 4 definitions). valueA is the battle parameter
// 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,
// ResistData, or MovementData) for the enemy previously specified with
// load_enemy_data.
{0xF892, "get_physical_data", nullptr, {{LABEL16, Arg::DataType::PLAYER_STATS, "stats"}}, F_V2_V4},
{0xF893, "get_attack_data", nullptr, {{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},
{0xF895, "get_movement_data", nullptr, {{LABEL16, Arg::DataType::MOVEMENT_DATA, "movement_data"}}, F_V2_V4},
{0xF892, "set_enemy_physical_data", "get_physical_data", {{LABEL16, Arg::DataType::PLAYER_STATS, "stats"}}, F_V2_V4},
{0xF893, "set_enemy_attack_data", "get_attack_data", {{LABEL16, Arg::DataType::ATTACK_DATA, "attack_data"}}, F_V2_V4},
{0xF894, "set_enemy_resist_data", "get_resist_data", {{LABEL16, Arg::DataType::RESIST_DATA, "resist_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.
// regA = event flag index
@@ -2257,7 +2250,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// valueC = current position along path
// valueD = loop flag (0 = no, 1 = yes)
// 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)
{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
// valueD = loop flag (0 = no, 1 = yes)
// 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)
{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
// bank.
// regA = returned Meseta amount in inventory
// regB = returned Meseta amount in bank
// regsA[0] = returned Meseta amount in inventory
// regsA[1] = returned Meseta amount in bank
{0xF91F, "get_slot_meseta", nullptr, {{W_REG_SET_FIXED, 2}}, F_V3_V4},
// Returns a player's level.
@@ -2583,10 +2576,12 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// strB = message
{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
// the same as for set_episode. Unlike set_episode, this opcode does not
// reset the floor configuration.
{0xF932, "set_episode2", nullptr, {R_REG}, F_V3_V4},
// Sets the episode to be loaded the next time an area is loaded (e.g. by
// the player changing floors). regA is the same as for set_episode. Like
// set_episode, it resets the floor configuration to the defaults, but this
// 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.
// 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},
// 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},
// Sets some values to be sent to the server with send_statistic.
@@ -2771,6 +2766,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// 5 = bank
// 6 = tekker
// 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
// the value 1 anywhere in memory.
{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.
{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
// valueD/valueE/valueF = item.data1[0-2] to replace it with
// labelG = label to call on success
@@ -2801,11 +2801,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// 2 = item not found)
{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
// labelD = label to call on success
// 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
// 6xD8.
@@ -2814,7 +2816,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// valueE = special type
// labelF = label to call on success
// 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
// 6xDA.
@@ -2824,12 +2826,12 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// valueF = payment count (number of PDs)
// labelG = label to call on success
// 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.
// Sends 6xDA. Same arguments as bb_exchange_pd_percent, except Photon
// 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
// minutes.
@@ -2840,24 +2842,28 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
// (even if the boss has already been defeated).
{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)
// valueB = replace_item.data1[0-2] (low 3 bytes; high byte unused)
// valueC = token1 (see 6xD9 in CommandFormats.hh)
// valueD = token2 (see 6xD9 in CommandFormats.hh)
// labelE = label to call on success
// 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.
// See SecretLotteryResultItems in config.json for the item pool used by
// this opcode.
// The pool of items that can be returned by this opcode is determined by
// 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
// valueB = unknown_a1
// labelC = label to call on success
// labelD = label to call on failure (unused because of a client bug; see
// 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
// 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(
const void* data,
size_t size,
Version version,
uint8_t override_language,
Language override_language,
bool reassembly_mode,
bool use_qedit_names) {
phosg::StringReader r(data, size);
@@ -2980,14 +3032,14 @@ std::string disassemble_quest_script(
bool use_wstrs = false;
size_t code_offset = 0;
size_t function_table_offset = 0;
uint8_t language;
Language language;
switch (version) {
case Version::DC_NTE: {
const auto& header = r.get<PSOQuestHeaderDCNTE>();
code_offset = header.code_offset;
function_table_offset = header.function_table_offset;
language = 0;
lines.emplace_back(".name " + escape_string(header.name.decode(0)));
language = Language::JAPANESE;
lines.emplace_back(".name " + escape_string(header.name.decode(Language::JAPANESE)));
break;
}
case Version::DC_11_2000:
@@ -2996,15 +3048,15 @@ std::string disassemble_quest_script(
const auto& header = r.get<PSOQuestHeaderDC>();
code_offset = header.code_offset;
function_table_offset = header.function_table_offset;
if (override_language != 0xFF) {
if (override_language != Language::UNKNOWN) {
language = override_language;
} else if (header.language < 5) {
} else if (static_cast<size_t>(header.language) < 5) {
language = header.language;
} else {
language = 1;
language = Language::ENGLISH;
}
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(".short_desc " + escape_string(header.short_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>();
code_offset = header.code_offset;
function_table_offset = header.function_table_offset;
if (override_language != 0xFF) {
if (override_language != Language::UNKNOWN) {
language = override_language;
} else if (header.language < 8) {
} else if (static_cast<size_t>(header.language) < 8) {
language = header.language;
} else {
language = 1;
language = Language::ENGLISH;
}
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(".short_desc " + escape_string(header.short_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>();
code_offset = header.code_offset;
function_table_offset = header.function_table_offset;
if (override_language != 0xFF) {
if (override_language != Language::UNKNOWN) {
language = override_language;
} else if (header.language < 5) {
} else if (static_cast<size_t>(header.language) < 5) {
language = header.language;
} else {
language = 1;
language = Language::ENGLISH;
}
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(".short_desc " + escape_string(header.short_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: {
use_wstrs = true;
const auto& header = r.get<PSOQuestHeaderBB>();
const auto& header = r.get<PSOQuestHeaderBBBase>();
code_offset = header.code_offset;
function_table_offset = header.function_table_offset;
if (override_language != 0xFF) {
if (override_language != Language::UNKNOWN) {
language = override_language;
} else {
language = 1;
language = Language::ENGLISH;
}
lines.emplace_back(std::format(".quest_num {}", header.quest_number));
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) {
lines.emplace_back(".joinable");
}
lines.emplace_back(".name " + escape_string(header.name.decode(language)));
lines.emplace_back(".short_desc " + escape_string(header.short_description.decode(language)));
lines.emplace_back(".long_desc " + escape_string(header.long_description.decode(language)));
lines.emplace_back(std::format(".name {}", escape_string(header.name.decode(language))));
lines.emplace_back(std::format(".short_desc {}", escape_string(header.short_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;
}
default:
@@ -3317,7 +3385,7 @@ std::string disassemble_quest_script(
case Type::FLOAT32: {
float v = cmd_r.get_f32l();
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);
break;
@@ -3335,7 +3403,7 @@ std::string disassemble_quest_script(
} else {
string s = cmd_r.get_cstr();
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));
}
@@ -3358,10 +3426,15 @@ std::string disassemble_quest_script(
} else {
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 */",
def->args.size(), arg_stack_values.size());
} 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;
for (size_t z = 0; z < def->args.size(); 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);
break;
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;
default:
dasm_arg = "/* invalid-type */";
@@ -3686,7 +3759,7 @@ std::string disassemble_quest_script(
phosg::StringReader r = cmd_r.sub(l->offset, size);
lines.emplace_back(" // As 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>();
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_long_desc;
int64_t quest_num = -1;
uint8_t quest_language = 1;
Language quest_language = Language::ENGLISH;
Episode quest_episode = Episode::EP1;
uint8_t quest_max_players = 4;
bool quest_joinable = false;
std::vector<QuestMetadata::CreateItemMask> create_item_mask_entries;
for (const auto& line : lines) {
if (line.text.empty()) {
continue;
@@ -4019,7 +4093,7 @@ AssembledQuestScript assemble_quest_script(
wrap_exceptions_with_line_ref(line, [&]() -> void {
if (line.text[0] == '.') {
if (line.text.starts_with(".include ")) {
// Nothing to do
// Nothing to do (see above)
} else if (line.text.starts_with(".version ")) {
string name = line.text.substr(9);
phosg::strip_leading_whitespace(name);
@@ -4030,10 +4104,25 @@ AssembledQuestScript assemble_quest_script(
quest_short_desc = phosg::parse_data_string(line.text.substr(12));
} else if (line.text.starts_with(".long_desc ")) {
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 ")) {
quest_num = stoul(line.text.substr(11), nullptr, 0);
} 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 ")) {
quest_episode = episode_for_token_name(line.text.substr(9));
} else if (line.text.starts_with(".max_players ")) {
@@ -4264,7 +4353,12 @@ AssembledQuestScript assemble_quest_script(
}
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);
if (!use_args) {
@@ -4322,7 +4416,7 @@ AssembledQuestScript assemble_quest_script(
case Version::GC_EP3_NTE:
case Version::GC_EP3:
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);
break;
case Version::PC_NTE:
@@ -4496,7 +4590,7 @@ AssembledQuestScript assemble_quest_script(
code_w.put_u32l(stoll(arg, nullptr, 0));
break;
case Type::FLOAT32:
code_w.put_u32l(stof(arg, nullptr));
code_w.put_f32l(stof(arg, nullptr));
break;
case Type::CSTRING:
if (arg.starts_with("bin:")) {
@@ -4571,7 +4665,7 @@ AssembledQuestScript assemble_quest_script(
header.code_offset = sizeof(header);
header.function_table_offset = sizeof(header) + code_w.size();
header.size = header.function_table_offset + function_table.size() * sizeof(function_table[0]);
header.name.encode(quest_name, 0);
header.name.encode(quest_name, Language::JAPANESE);
w.put(header);
break;
}
@@ -4639,6 +4733,10 @@ AssembledQuestScript assemble_quest_script(
header.name.encode(quest_name, quest_language);
header.short_description.encode(quest_short_desc, quest_language);
header.long_description.encode(quest_long_desc, quest_language);
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);
break;
}
@@ -4662,7 +4760,7 @@ AssembledQuestScript assemble_quest_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);
uint32_t code_offset = r.size();
uint32_t function_table_offset = r.size();
@@ -4746,7 +4844,7 @@ void populate_quest_metadata_from_script(
break;
}
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.joinable |= header.joinable;
meta.max_players = 4;
@@ -4756,6 +4854,21 @@ void populate_quest_metadata_from_script(
meta.name = header.name.decode(language);
meta.short_description = header.short_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;
function_table_offset = header.function_table_offset;
break;
@@ -5045,10 +5158,23 @@ void populate_quest_metadata_from_script(
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
uint8_t base_reg = r.get_u8();
meta.area_for_floor.at(regs.get(base_reg)) = regs.get(base_reg + 1);
// phosg::fwrite_fmt(stderr, ">>> Trace: map_designate_ex fa[{}]={}\n", regs.get(base_reg), regs.get(base_reg + 1));
uint32_t floor = regs.get(base_reg);
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;
}
@@ -5192,7 +5318,10 @@ void populate_quest_metadata_from_script(
break;
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);
break;
@@ -5288,9 +5417,14 @@ void populate_quest_metadata_from_script(
case 0xF951: { // bb_map_designate
uint8_t floor = r.get_u8();
meta.area_for_floor.at(floor) = r.get_u8();
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));
if (floor < meta.area_for_floor.size()) {
meta.area_for_floor.at(floor) = r.get_u8();
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;
}
+28 -8
View File
@@ -38,7 +38,7 @@ struct PSOQuestHeaderDC { // Same format for DC v1 and v2
/* 0008 */ le_uint32_t size = 0;
/* 000C */ le_uint16_t unknown_a1 = 0;
/* 000E */ le_uint16_t unknown_a2 = 0;
/* 0010 */ uint8_t language = 0;
/* 0010 */ Language language = Language::JAPANESE;
/* 0011 */ uint8_t unknown_a3 = 0;
/* 0012 */ le_uint16_t quest_number = 0; // 0xFFFF for challenge quests
/* 0014 */ pstring<TextEncoding::MARKED, 0x20> name;
@@ -53,7 +53,7 @@ struct PSOQuestHeaderPC {
/* 0008 */ le_uint32_t size = 0;
/* 000C */ le_uint16_t unknown_a1 = 0;
/* 000E */ le_uint16_t unknown_a2 = 0;
/* 0010 */ uint8_t language = 0;
/* 0010 */ Language language = Language::JAPANESE;
/* 0011 */ uint8_t unknown_a3 = 0;
/* 0012 */ le_uint16_t quest_number = 0; // 0xFFFF for challenge quests
/* 0014 */ pstring<TextEncoding::UTF16, 0x20> name;
@@ -70,7 +70,7 @@ struct PSOQuestHeaderGC {
/* 0008 */ le_uint32_t size = 0;
/* 000C */ le_uint16_t unknown_a1 = 0;
/* 000E */ le_uint16_t unknown_a2 = 0;
/* 0010 */ uint8_t language = 0;
/* 0010 */ Language language = Language::JAPANESE;
/* 0011 */ uint8_t unknown_a3 = 0;
// 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
@@ -83,7 +83,21 @@ struct PSOQuestHeaderGC {
/* 01D4 */
} __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;
/* 0004 */ le_uint32_t function_table_offset = 0;
/* 0008 */ le_uint32_t size = 0;
@@ -99,7 +113,13 @@ struct PSOQuestHeaderBB {
/* 0058 */ pstring<TextEncoding::UTF16, 0x80> short_description;
/* 0158 */ pstring<TextEncoding::UTF16, 0x120> long_description;
/* 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();
@@ -109,7 +129,7 @@ std::string disassemble_quest_script(
const void* data,
size_t size,
Version version,
uint8_t override_language = 0xFF,
Language override_language = Language::UNKNOWN,
bool reassembly_mode = false,
bool use_qedit_names = false);
@@ -117,7 +137,7 @@ struct AssembledQuestScript {
std::string data;
int64_t quest_number = -1;
Version version = Version::UNKNOWN;
uint8_t language = 0xFF;
Language language = Language::UNKNOWN;
Episode episode = Episode::NONE;
bool joinable = false;
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>& 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
View File
@@ -236,12 +236,11 @@ RareItemSet::SpecCollection RareItemSet::ParsedRELData::as_collection() const {
}
RareItemSet::RareItemSet(const AFSArchive& afs, bool is_v1) {
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (GameMode mode : modes) {
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
for (GameMode mode : ALL_GAME_MODES_V4) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
for (size_t section_id = 0; section_id < 10; section_id++) {
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);
this->collections.emplace(
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",
((mode == GameMode::CHALLENGE) ? "c" : ""),
((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) {
const array<Episode, 2> episodes = {Episode::EP1, Episode::EP2};
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (GameMode mode : modes) {
for (Episode episode : episodes) {
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
for (GameMode mode : ALL_GAME_MODES_V23) {
for (Episode episode : ALL_EPISODES_V3) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
for (size_t section_id = 0; section_id < 10; section_id++) {
try {
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) {
// Tables are 0x280 bytes in size in this format, laid out sequentially
phosg::StringReader r(rel_data);
array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (size_t ep_index = 0; ep_index < episodes.size(); ep_index++) {
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
for (Episode episode : ALL_EPISODES_V4) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
for (size_t section_id = 0; section_id < 10; section_id++) {
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);
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());
} 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);
for (const auto& difficulty_it : episode_it.second->as_dict()) {
static const unordered_map<string, uint8_t> difficulty_keys(
{{"Normal", 0}, {"Hard", 1}, {"VeryHard", 2}, {"Ultimate", 3}});
uint8_t difficulty = difficulty_keys.at(difficulty_it.first);
static const unordered_map<string, Difficulty> difficulty_keys(
{{"Normal", Difficulty::NORMAL}, {"Hard", Difficulty::HARD}, {"VeryHard", Difficulty::VERY_HARD}, {"Ultimate", Difficulty::ULTIMATE}});
Difficulty difficulty = difficulty_keys.at(difficulty_it.first);
for (const auto& section_id_it : difficulty_it.second->as_dict()) {
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 {
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++) {
ParsedRELData rel(this->get_collection(GameMode::NORMAL, Episode::EP1, difficulty, section_id));
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 {
unordered_map<string, string> files;
static const std::array<Episode, 2> episodes = {Episode::EP1, Episode::EP2};
for (Episode episode : episodes) {
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
for (Episode episode : ALL_EPISODES_V3) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
for (uint8_t section_id = 0; section_id < 10; section_id++) {
try {
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++) {
try {
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(
GameMode mode,
Episode episode,
uint8_t difficulty,
shared_ptr<const ItemNameIndex> name_index) const {
Difficulty difficulty,
shared_ptr<const ItemNameIndex> name_index,
shared_ptr<const CommonItemSet> common_item_set) const {
struct ZoneTypes {
const char* name;
@@ -683,7 +683,7 @@ string RareItemSet::serialize_html(
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;
for (const auto& specs_list : specs_lists) {
any_list_nonempty |= !specs_list.empty();
@@ -703,6 +703,16 @@ string RareItemSet::serialize_html(
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;
if (example_item.can_be_encoded_in_rel_rare_table()) {
// 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);
string denom_token = (floor(denom) == denom)
? std::format("1 / {:g}", denom)
: std::format("1 / %.02f", denom);
tokens.emplace_back(std::format(
"<span class=\"rate\" title=\"Exact rate: {} / {}\">{}</span>",
frac.first, frac.second, denom_token));
? std::format("1 / {:.0f}", denom)
: std::format("1 / {:.02f}", denom);
tokens.emplace_back(std::format("<span class=\"rate\" title=\"{}\">{}</span>", exact_token, denom_token));
}
if (!blocks.empty()) {
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);
}
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;
add_specs_row(name, false, specs_lists);
const char* name = (difficulty == Difficulty::ULTIMATE && type_def.ultimate_name) ? type_def.ultimate_name : type_def.in_game_name;
add_specs_row(&type_def, name, false, specs_lists);
}
for (uint8_t floor : zone_type.floors) {
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);
}
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>");
@@ -776,13 +784,11 @@ string RareItemSet::serialize_html(
phosg::JSON RareItemSet::json(shared_ptr<const ItemNameIndex> name_index) const {
auto modes_dict = phosg::JSON::dict();
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (const auto& mode : modes) {
for (const auto& mode : ALL_GAME_MODES_V4) {
auto episodes_dict = phosg::JSON::dict();
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (const auto& episode : episodes) {
for (const auto& episode : ALL_EPISODES_V4) {
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();
for (uint8_t section_id = 0; section_id < 10; section_id++) {
auto collection_dict = phosg::JSON::dict();
@@ -875,7 +881,7 @@ void RareItemSet::print_collection(
FILE* stream,
GameMode mode,
Episode episode,
uint8_t difficulty,
Difficulty difficulty,
uint8_t section_id,
shared_ptr<const ItemNameIndex> name_index) const {
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 {
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (GameMode mode : modes) {
for (Episode episode : episodes) {
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
for (GameMode mode : ALL_GAME_MODES_V4) {
for (Episode episode : ALL_EPISODES_V4) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
for (uint8_t section_id = 0; section_id < 10; section_id++) {
try {
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(
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 {
return this->get_collection(mode, episode, difficulty, secid).rt_index_to_specs.at(rt_index);
} 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(
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 {
return this->get_collection(mode, episode, difficulty, secid).box_area_norm_to_specs.at(area_norm);
} 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++) {
if (this->collections.count(this->key_for_params(mode, episode, difficulty, section_id))) {
return true;
@@ -965,19 +969,19 @@ bool RareItemSet::has_entries_for_game_config(GameMode mode, Episode episode, ui
}
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));
}
uint16_t RareItemSet::key_for_params(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid) {
if (difficulty > 3) {
uint16_t RareItemSet::key_for_params(GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid) {
if (static_cast<size_t>(difficulty) > 3) {
throw logic_error("incorrect difficulty");
}
if (secid > 10) {
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) {
case GameMode::NORMAL:
break;
+11 -9
View File
@@ -9,6 +9,7 @@
#include <string>
#include "AFSArchive.hh"
#include "CommonItemSet.hh"
#include "GSLArchive.hh"
#include "ItemNameIndex.hh"
#include "StaticGameData.hh"
@@ -33,19 +34,20 @@ public:
~RareItemSet() = default;
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(
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_gsl(bool big_endian) const;
std::string serialize_html(
GameMode mode,
Episode episode,
uint8_t difficulty,
std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
Difficulty difficulty,
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;
void multiply_all_rates(double factor);
@@ -54,7 +56,7 @@ public:
FILE* stream,
GameMode mode,
Episode episode,
uint8_t difficulty,
Difficulty difficulty,
uint8_t section_id,
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;
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 uint16_t key_for_params(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid);
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, Difficulty difficulty, uint8_t secid);
static uint32_t expand_rate(uint8_t pc);
static uint8_t compress_rate(uint32_t probability);
+90 -57
View File
@@ -347,7 +347,7 @@ static asio::awaitable<void> on_login_complete(shared_ptr<Client> c) {
if (!q) {
c->log.info_f("There is no quest to enable server function calls for specific version {:08X}", c->specific_version);
} else if (q) {
auto vq = q->version(c->version(), 1);
auto vq = q->version(c->version(), Language::ENGLISH);
if (vq) {
c->set_flag(Client::Flag::HAS_SEND_FUNCTION_CALL);
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;
send_command_t(c, 0x64, 0x01, cmd);
} 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 dat_filename = vq->dat_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()) {
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->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()) {
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);
for (size_t x = 0; x < req.file->chunk_crcs.size(); x++) {
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;
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(data->data() + (x * 0x4000), chunk_size);
blocks.emplace_back(data->data() + (x * PatchFileIndex::CHUNK_SIZE), chunk_size);
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->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) {
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) {
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) {
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();
switch (cmd.menu_id) {
case MenuID::QUEST_CATEGORIES_EP1:
case MenuID::QUEST_CATEGORIES_EP1_EP3_EP4:
case MenuID::QUEST_CATEGORIES_EP2:
// Don't send anything here. The quest filter menu already has short
// 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;
}
case MenuID::QUEST_EP3: {
auto map = s->ep3_download_map_index->get(cmd.item_id);
if (!map) {
auto vis_flag = (c->version() == Version::GC_EP3_NTE)
? 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);
} else {
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,
name_for_char_class(player->disp.visual.char_class),
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)
if (info.empty()) {
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) {
info += std::format("Req. level: {}-{}\n", game->min_level + 1, game->max_level + 1);
} 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) {
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";
} else if (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
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->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->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();
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 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;
case MainMenuItemID::DOWNLOAD_QUESTS: {
if (is_ep3(c->version())) {
send_ep3_download_quest_menu(c);
} else {
send_quest_categories_menu(c, QuestMenuType::DOWNLOAD, Episode::NONE);
}
send_quest_categories_menu(c, QuestMenuType::DOWNLOAD, Episode::NONE);
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) {
// Episode 3 doesn't have this menu
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();
if (!s->quest_index) {
send_lobby_message_box(c, "$C7Quests are not available.");
co_return;
}
} else {
auto s = c->require_server_state();
if (!s->quest_index) {
send_lobby_message_box(c, "$C7Quests are not available.");
co_return;
}
shared_ptr<Lobby> l = c->lobby.lock();
Episode episode = l ? l->episode : Episode::NONE;
uint16_t version_flags = (1 << static_cast<size_t>(c->version())) | (l ? l->quest_version_flags() : 0);
QuestIndex::IncludeCondition include_condition = nullptr;
if (l && !c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
include_condition = l->quest_include_condition();
}
shared_ptr<Lobby> l = c->lobby.lock();
Episode episode = l ? l->episode : Episode::NONE;
uint16_t version_flags = (1 << static_cast<size_t>(c->version())) | (l ? l->quest_version_flags() : 0);
QuestIndex::IncludeCondition include_condition = nullptr;
if (l && !c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
include_condition = l->quest_include_condition();
}
const auto& quests = s->quest_index->filter(episode, version_flags, item_id, include_condition);
send_quest_menu(c, quests, !l);
const auto& quests = s->quest_index->filter(episode, version_flags, item_id, include_condition);
send_quest_menu(c, quests, !l);
}
}
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()) {
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 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);
send_open_quest_file(c, name, filename, "", map->map_number, QuestFileType::EPISODE_3, data);
co_return;
@@ -3045,7 +3069,7 @@ static asio::awaitable<void> on_10(shared_ptr<Client> c, Channel::Message& msg)
case MenuID::GAME:
co_await on_10_game_menu(c, base_cmd.item_id, std::move(password));
break;
case MenuID::QUEST_CATEGORIES_EP1:
case MenuID::QUEST_CATEGORIES_EP1_EP3_EP4:
case MenuID::QUEST_CATEGORIES_EP2:
co_await on_10_quest_categories(c, base_cmd.item_id);
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);
if (c->bb_connection_phase != 0x00) {
c->save_and_unload_character();
c->unload_character(false);
c->bb_character_index = cmd.character_index;
c->bb_bank_character_index = cmd.character_index;
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 {
c->save_and_unload_character();
c->unload_character(false);
c->bb_character_index = index;
c->bb_bank_character_index = index;
try {
auto preview = c->character_file()->to_preview();
send_player_preview_bb(c, c->bb_character_index, &preview);
} catch (const exception& e) {
// 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);
}
c->unload_character(false);
};
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) {
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");
}
if (l->difficulty != cmd.difficulty) {
l->difficulty = cmd.difficulty;
if (l->difficulty != cmd_difficulty) {
l->difficulty = cmd_difficulty;
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;
}
@@ -4481,7 +4506,7 @@ shared_ptr<Lobby> create_game_generic(
const std::string& password,
Episode episode,
GameMode mode,
uint8_t difficulty,
Difficulty difficulty,
bool allow_v1,
shared_ptr<Lobby> watched_lobby,
shared_ptr<Episode3::BattleRecordPlayer> battle_player) {
@@ -4493,7 +4518,7 @@ shared_ptr<Lobby> create_game_generic(
throw invalid_argument("incorrect episode number");
}
if (difficulty > 3) {
if (static_cast<size_t>(difficulty) > 3) {
throw invalid_argument("incorrect difficulty level");
}
@@ -4517,7 +4542,7 @@ shared_ptr<Lobby> create_game_generic(
game->difficulty = difficulty;
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");
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_11_2000);
game->forbid_version(Version::DC_V1);
@@ -4691,7 +4716,7 @@ shared_ptr<Lobby> create_game_generic(
if (game->mode == GameMode::CHALLENGE) {
game->rare_enemy_rates = s->rare_enemy_rates_challenge;
} 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) {
@@ -4726,7 +4751,7 @@ shared_ptr<Lobby> create_game_generic(
if (quest_flag_rewrites && !quest_flag_rewrites->empty()) {
IntegralExpression::Env env = {
.flags = &p->quest_flags.data.at(difficulty),
.flags = &p->quest_flags.array(difficulty),
.challenge_records = &p->challenge_records,
.team = creator_c->team(),
.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;
if (is_pre_v1(c->version())) {
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 {
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&) {
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) {
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 dat_filename = vq->dat_filename();
string xb_filename = vq->xb_filename();
+1 -1
View File
@@ -13,7 +13,7 @@ std::shared_ptr<Lobby> create_game_generic(
const std::string& password = "",
Episode episode = Episode::EP1,
GameMode mode = GameMode::NORMAL,
uint8_t difficulty = 0,
Difficulty difficulty = Difficulty::NORMAL,
bool allow_v1 = false,
std::shared_ptr<Lobby> watched_lobby = nullptr,
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player = nullptr);
+301 -163
View File
File diff suppressed because it is too large Load Diff
+8 -4
View File
@@ -19,7 +19,10 @@ G_SpecializableItemDropRequest_6xA2 normalize_drop_request(const void* data, siz
struct DropReconcileResult {
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;
bool should_drop;
bool ignore_def;
@@ -29,6 +32,7 @@ DropReconcileResult reconcile_drop_request_with_map(
std::shared_ptr<Client> c,
G_SpecializableItemDropRequest_6xA2& cmd,
Episode episode,
Difficulty difficulty,
uint8_t event,
std::shared_ptr<MapState> map,
bool mark_drop);
@@ -50,7 +54,7 @@ public:
StatusEffectState attack_status_effect;
StatusEffectState defense_status_effect;
StatusEffectState unused_status_effect;
uint32_t language = 0;
Language language = Language::JAPANESE;
uint32_t player_tag = 0;
uint32_t guild_card_number = 0;
uint32_t unknown_a6 = 0;
@@ -79,7 +83,7 @@ public:
Parsed6x70Data(
const G_SyncPlayerDispAndInventory_DC112000_6x70& cmd,
uint32_t guild_card_number,
uint8_t language,
Language language,
Version from_version,
bool from_client_customization);
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_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_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;
void clear_v1_unused_item_fields();
+1 -1
View File
@@ -50,7 +50,7 @@ ReplaySession::Client::Client(shared_ptr<asio::io_context> io_context, uint64_t
: id(id),
port(port),
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 {
return std::format("Client[{}, T-{}, {}]", this->id, this->port, phosg::name_for_enum(this->version));
+32 -30
View File
@@ -15,10 +15,10 @@ struct DefaultSymbolChatEntry {
array<uint16_t, 4> corner_objects;
array<SymbolChatFacePart, 12> face_parts;
SaveFileSymbolChatEntryBB to_entry(uint8_t language) const {
SaveFileSymbolChatEntryBB to_entry(Language language) const {
SaveFileSymbolChatEntryBB ret;
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;
for (size_t z = 0; z < 4; 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(
uint32_t guild_card_number,
uint8_t language,
Language language,
const PlayerVisualConfig& visual,
const std::string& name,
shared_ptr<const LevelTable> level_table) {
@@ -540,7 +540,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_config(
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_preview(
uint32_t guild_card_number,
uint8_t language,
Language language,
const PlayerDispDataBBPreview& preview,
shared_ptr<const LevelTable> level_table) {
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) {
auto ret = PSOBBCharacterFile::create_from_config(
src.guild_card.guild_card_number,
0,
Language::JAPANESE,
src.disp.visual,
src.disp.visual.name.decode(0),
src.disp.visual.name.decode(Language::JAPANESE),
nullptr);
ret->inventory = src.inventory;
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->validation_flags = 0;
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.inventory.language,
src.disp.visual,
src.disp.visual.name.decode(0),
src.disp.visual.name.decode(Language::JAPANESE),
nullptr);
ret->inventory = src.inventory;
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->validation_flags = 0;
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.inventory.language,
src.disp.visual,
src.disp.visual.name.decode(0),
src.disp.visual.name.decode(Language::JAPANESE),
nullptr);
ret->inventory = src.inventory;
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->validation_flags = src.validation_flags;
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.inventory.language,
src.disp.visual,
src.disp.visual.name.decode(0),
src.disp.visual.name.decode(Language::JAPANESE),
nullptr);
ret->inventory = src.inventory;
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->validation_flags = src.validation_flags;
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.inventory.language,
src.disp.visual,
src.disp.visual.name.decode(0),
src.disp.visual.name.decode(Language::JAPANESE),
nullptr);
ret->inventory = src.inventory;
// 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
// 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.
uint8_t language = ret->inventory.language;
Language language = ret->inventory.language;
ret->disp = src.disp.to_bb(language, language);
ret->validation_flags = src.validation_flags;
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.inventory.language,
src.disp.visual,
src.disp.visual.name.decode(0),
src.disp.visual.name.decode(Language::JAPANESE),
nullptr);
ret->inventory = src.inventory;
// 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
// 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.
uint8_t language = ret->inventory.language;
Language language = ret->inventory.language;
ret->disp = src.disp.to_bb(language, language);
ret->validation_flags = src.validation_flags;
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.inventory.language,
src.disp.visual,
src.disp.visual.name.decode(0),
src.disp.visual.name.decode(Language::JAPANESE),
nullptr);
ret->inventory = src.inventory;
uint8_t language = ret->inventory.language;
Language language = ret->inventory.language;
ret->disp = src.disp.to_bb(language, language);
ret->validation_flags = src.validation_flags;
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.inventory.language,
src.disp.visual,
src.disp.visual.name.decode(0),
src.disp.visual.name.decode(Language::JAPANESE),
nullptr);
ret->inventory = src.inventory;
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->validation_flags = src.validation_flags;
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 {
uint8_t language = this->inventory.language;
Language language = this->inventory.language;
PSODCNTECharacterFile::Character ret;
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 {
uint8_t language = this->inventory.language;
Language language = this->inventory.language;
PSODC112000CharacterFile::Character ret;
ret.inventory = this->inventory;
@@ -949,7 +949,7 @@ PSODC112000CharacterFile::Character PSOBBCharacterFile::as_11_2000(uint64_t hard
}
PSOBBCharacterFile::operator PSODCV1CharacterFile::Character() const {
uint8_t language = this->inventory.language;
Language language = this->inventory.language;
PSODCV1CharacterFile::Character ret;
ret.inventory = this->inventory;
@@ -981,7 +981,7 @@ PSOBBCharacterFile::operator PSODCV1CharacterFile::Character() const {
}
PSOBBCharacterFile::operator PSODCV2CharacterFile::Character() const {
uint8_t language = this->inventory.language;
Language language = this->inventory.language;
PSODCV2CharacterFile::Character ret;
ret.inventory = this->inventory;
@@ -1022,7 +1022,7 @@ PSOBBCharacterFile::operator PSODCV2CharacterFile::Character() const {
}
PSOBBCharacterFile::operator PSOGCNTECharacterFileCharacter() const {
uint8_t language = this->inventory.language;
Language language = this->inventory.language;
PSOGCNTECharacterFileCharacter ret;
ret.inventory = this->inventory;
@@ -1060,7 +1060,7 @@ PSOBBCharacterFile::operator PSOGCNTECharacterFileCharacter() const {
}
PSOBBCharacterFile::operator PSOGCCharacterFile::Character() const {
uint8_t language = this->inventory.language;
Language language = this->inventory.language;
PSOGCCharacterFile::Character ret;
ret.inventory = this->inventory;
@@ -1108,7 +1108,7 @@ PSOBBCharacterFile::operator PSOGCCharacterFile::Character() const {
}
PSOBBCharacterFile::operator PSOXBCharacterFile::Character() const {
uint8_t language = this->inventory.language;
Language language = this->inventory.language;
PSOXBCharacterFile::Character ret;
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);
}
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 exp = this->disp.stats.experience;
level_table->reset_to_base(this->disp.stats, this->disp.visual.char_class);
level_table->advance_to_level(this->disp.stats, level, this->disp.visual.char_class);
this->disp.stats.experience = exp;
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.mst += (this->get_material_usage(MaterialType::MIND) * 2);
+11 -11
View File
@@ -193,7 +193,7 @@ struct SaveFileChatShortcutEntryT {
/* 40:54:A4 */
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;
ret.type = this->type;
switch (ret.type) {
@@ -268,7 +268,7 @@ struct PSOPCSystemFile { // PSO______COM
// assumption that Sega didn't change much between versions.
/* 0004 */ le_int16_t music_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;
/* 000C */ parray<le_uint16_t, 0x10> unknown_a4; // Last one is always 0x1234?
/* 002C */ parray<uint8_t, 0x100> event_flags;
@@ -281,7 +281,7 @@ struct PSOGCSystemFile {
/* 0000 */ be_uint32_t checksum = 0;
/* 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
/* 0007 */ uint8_t language = 1;
/* 0007 */ Language language = Language::ENGLISH;
// This field stores the effective time zone offset between the server and
// client, in frames. The default value is 1728000, which corresponds to 16
// hours. This is recomputed when the client receives a B1 command.
@@ -309,7 +309,7 @@ struct PSOXBSystemFile {
/* 0000 */ le_uint32_t checksum = 0;
/* 0004 */ le_int16_t music_volume = -50;
/* 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;
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
/* 000E */ be_uint16_t surround_sound_enabled = 0;
@@ -338,7 +338,7 @@ struct PSOBBMinimalSystemFile {
/* 0000 */ be_uint32_t checksum = 0;
/* 0004 */ be_int16_t music_volume = 0;
/* 0006 */ int8_t sound_volume = 0;
/* 0007 */ uint8_t language = 0;
/* 0007 */ Language language = Language::JAPANESE;
/* 0008 */ be_int32_t server_time_delta_frames = 1728000;
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
/* 000E */ be_uint16_t surround_sound_enabled = 0;
@@ -681,7 +681,7 @@ struct PSOGCEp3NTECharacter {
/* 0430:0014 */ be_uint32_t save_count = 1;
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
/* 0460:0044 */ parray<uint8_t, 0x400> seq_vars;
/* 0460:0044 */ Ep3SeqVars seq_vars;
/* 0860:0444 */ be_uint32_t death_count = 0;
/* 0864:0448 */ PlayerBank200BE bank;
/* 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.
// Logically, this structure maps to quest_flags in other versions, but is
// a different size.
/* 0460:0044 */ parray<uint8_t, 0x400> seq_vars;
/* 0460:0044 */ Ep3SeqVars seq_vars;
/* 0860:0444 */ be_uint32_t death_count = 0;
// Curiously, Episode 3 characters do have item banks, but there are only 4
// item slots. Presumably Sega didn't completely remove the bank in Ep3
@@ -871,13 +871,13 @@ struct PSOBBCharacterFile {
static std::shared_ptr<PSOBBCharacterFile> create_from_config(
uint32_t guild_card_number,
uint8_t language,
Language language,
const PlayerVisualConfig& visual,
const std::string& name,
std::shared_ptr<const LevelTable> level_table);
static std::shared_ptr<PSOBBCharacterFile> create_from_preview(
uint32_t guild_card_number,
uint8_t language,
Language language,
const PlayerDispDataBBPreview& preview,
std::shared_ptr<const LevelTable> level_table);
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 clear_all_material_usage();
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);
struct PSOCHARFile {
@@ -978,7 +978,7 @@ struct PSODCV1V2GuildCardFile {
/* 0004 */ parray<PSODCGuildCardFileEntry, 100> entries;
/* 3204 */ le_int16_t music_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
/* 320C */ le_uint32_t creation_timestamp = 0;
/* 3210 */ le_uint32_t round2_seed = 0;
+121 -68
View File
@@ -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) {
S_EnterDirectory_Patch_09 cmd = {{dir, 1}};
S_EnterDirectory_Patch_09 cmd = {{dir, Language::ENGLISH}};
c->channel->send(0x09, 0x00, cmd);
}
@@ -502,16 +502,14 @@ void send_function_call(
ch->send(0xB2, 0x00, data);
}
asio::awaitable<bool> send_protected_command(
std::shared_ptr<Client> c, const void* data, size_t size, bool echo_to_lobby) {
asio::awaitable<bool> send_protected_command(std::shared_ptr<Client> c, const void* data, size_t size, bool echo_to_lobby) {
switch (c->version()) {
case Version::DC_NTE:
case Version::DC_11_2000:
case Version::DC_V1:
case Version::DC_V2:
case Version::PC_NTE:
case Version::PC_V2:
case Version::GC_NTE: {
case Version::PC_V2: {
auto l = echo_to_lobby ? c->lobby.lock() : nullptr;
if (l) {
send_command(l, 0x60, 0x00, data, size);
@@ -521,6 +519,7 @@ asio::awaitable<bool> send_protected_command(
co_return true;
}
case Version::GC_NTE:
case Version::GC_V3:
case Version::XB_V3:
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) {
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 sys = c->system_file(true);
auto team = c->team();
if (c->check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) {
p->inventory.language = 1;
p->guild_card.language = 1;
sys->language = 1;
p->inventory.language = Language::ENGLISH;
p->guild_card.language = Language::ENGLISH;
sys->language = Language::ENGLISH;
}
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);
}
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(
Version version,
uint8_t language,
Language language,
uint8_t from_client_id,
const string& from_name,
const string& text,
@@ -1025,7 +1030,7 @@ string prepare_chat_data(
string data;
if (version == Version::BB_V4) {
data.append(language ? "\tE" : "\tJ");
data.append((language == Language::JAPANESE) ? "\tJ" : "\tE");
}
data.append(from_name);
if (version == Version::DC_NTE) {
@@ -1038,7 +1043,7 @@ string prepare_chat_data(
}
if (uses_utf16(version)) {
data.append(language ? "\tE" : "\tJ");
data.append((language == Language::JAPANESE) ? "\tJ" : "\tE");
data.append(text);
return tt_utf8_to_utf16(data);
} else if (version == Version::DC_NTE) {
@@ -1292,7 +1297,7 @@ void send_guild_card_dc_pc_gc_t(
uint32_t guild_card_number,
const string& name,
const string& description,
uint8_t language,
Language language,
uint8_t section_id,
uint8_t char_class) {
CmdT cmd;
@@ -1316,7 +1321,7 @@ void send_guild_card_xb(
uint64_t xb_user_id,
const string& name,
const string& description,
uint8_t language,
Language language,
uint8_t section_id,
uint8_t char_class) {
G_SendGuildCard_XB_6x06 cmd;
@@ -1342,7 +1347,7 @@ static void send_guild_card_bb(
const string& name,
const string& team_name,
const string& description,
uint8_t language,
Language language,
uint8_t section_id,
uint8_t char_class) {
G_SendGuildCard_BB_6x06 cmd;
@@ -1367,7 +1372,7 @@ void send_guild_card(
const string& name,
const string& team_name,
const string& description,
uint8_t language,
Language language,
uint8_t section_id,
uint8_t char_class) {
switch (ch->version) {
@@ -1574,7 +1579,7 @@ void send_game_menu_t(
auto& e = entries.emplace_back();
e.menu_id = MenuID::GAME;
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();
if (is_dc(c->version())) {
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);
}
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>
void send_quest_categories_menu_t(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
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();
for (const auto& cat : s->quest_index->categories(menu_type, episode, version_flags, include_condition)) {
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.name.encode(cat->name, 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);
}
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(
shared_ptr<Client> c,
const vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>>& quests,
@@ -1732,10 +1775,11 @@ void send_quest_menu(
case Version::DC_V2:
case Version::GC_NTE:
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);
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:
send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, quests, is_download_menu);
break;
@@ -1759,9 +1803,14 @@ void send_quest_categories_menu(shared_ptr<Client> c, QuestMenuType menu_type, E
case Version::DC_V2:
case Version::GC_NTE:
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:
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;
case Version::XB_V3:
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);
}
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;
if (ch->version == Version::DC_NTE) {
if (version == Version::DC_NTE) {
subcommand_number = 0x2B;
} else if (ch->version == Version::DC_11_2000) {
} else if (version == Version::DC_11_2000) {
subcommand_number = 0x2D;
} else {
subcommand_number = 0x2F;
}
G_ChangePlayerHP_6x2F cmd = {
return G_ChangePlayerHP_6x2F{
{subcommand_number, sizeof(G_ChangePlayerHP_6x2F) / 4, 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) {
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
phosg::StringWriter w;
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) {
continue;
}
const auto& diff_flags = l->quest_flag_values->data.at(difficulty);
const auto& diff_known_flags = l->quest_flags_known->data.at(difficulty);
const auto& diff_flags = l->quest_flag_values->array(difficulty);
const auto& diff_known_flags = l->quest_flags_known->array(difficulty);
for (uint8_t z = 0; z < diff_known_flags.data.size(); z++) {
uint8_t known_flags = diff_known_flags.data[z];
if (!known_flags) {
@@ -2955,7 +3019,7 @@ void send_game_flag_state_t(shared_ptr<Client> c) {
uint16_t flag_num = ((z << 3) | sh);
if (use_v3_cmd) {
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 {
w.put(G_UpdateQuestFlag_DC_PC_6x75{
{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);
}
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();
if (c->version() != Version::BB_V4) {
throw logic_error("6xBF can only be sent to BB clients");
}
uint16_t client_id = c->lobby_client_id;
G_GiveExperience_BB_6xBF cmd = {
{0xBF, sizeof(G_GiveExperience_BB_6xBF) / 4, client_id}, amount};
G_GiveExperience_Extension_BB_6xBF cmd = {
{0xBF, sizeof(G_GiveExperience_Extension_BB_6xBF) / 4, client_id}, amount, from_enemy_id, 0};
send_command_t(l, 0x60, 0x00, cmd);
}
@@ -3268,11 +3332,9 @@ void send_set_exp_multiplier(shared_ptr<Lobby> l) {
if (!l->is_game()) {
throw logic_error("6xDD can only be sent in games (not in lobbies)");
}
G_SetFractionalEXPMultiplier_Extension_BB_6xDD cmd = {
{0xDD, sizeof(G_SetFractionalEXPMultiplier_Extension_BB_6xDD) / 4, 1}, 1.0f};
G_SetEXPMultiplier_BB_6xDD cmd = {0xDD, sizeof(G_SetEXPMultiplier_BB_6xDD) / 4, 1};
if (l->mode != GameMode::CHALLENGE) {
cmd.header.param = l->base_exp_multiplier;
cmd.multiplier = l->base_exp_multiplier;
}
for (auto lc : l->clients) {
if (lc && (lc->version() == Version::BB_V4)) {
@@ -3527,7 +3589,7 @@ string ep3_description_for_client(shared_ptr<Client> c) {
"{} CLv{} {}",
name_for_char_class(p->disp.visual.char_class),
p->disp.stats.level + 1,
char_for_language_code(p->inventory.language));
char_for_language(p->inventory.language));
}
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.winner_team_id = (match->preceding_b->winner_team == match->winner_team);
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) &&
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
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;
for (auto c : l->clients) {
if (c) {
if ((c->version() == Version::GC_EP3) &&
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
if ((c->version() == Version::GC_EP3) && !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
G_SetGameMetadata_Ep3_6xB4x52 masked_cmd = cmd;
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
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) {
text = std::format("Viewing final match of tournament {}", tourn->get_name());
} else {
text = std::format(
"Viewing match in round {} of tournament {}",
l->tournament_match->round_num, tourn->get_name());
text = std::format("Viewing match in round {} of tournament {}", l->tournament_match->round_num, tourn->get_name());
}
} else {
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.text_size = text.size();
cmd.text.encode(text, 1);
cmd.text.encode(text, Language::ENGLISH);
for (auto c : watcher_l->clients) {
if (c) {
if ((c->version() == Version::GC_EP3) &&
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
if ((c->version() == Version::GC_EP3) && !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
G_SetGameMetadata_Ep3_6xB4x52 masked_cmd = cmd;
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
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(
shared_ptr<Client> c,
const string& filename,
size_t chunk_index,
const void* data,
size_t size,
bool is_download_quest) {
shared_ptr<Client> c, const string& filename, size_t chunk_index, const void* data, size_t size, bool is_download_quest) {
if (size > 0x400) {
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&,
uint32_t file_size,
uint32_t,
uint32_t, // quest_number (only used on Xbox)
QuestFileType type) {
CommandT cmd;
uint8_t command_num;
+7 -5
View File
@@ -231,7 +231,7 @@ void send_text_or_scrolling_message(std::shared_ptr<ServerState> s, const std::s
std::string prepare_chat_data(
Version version,
uint8_t language,
Language language,
uint8_t from_client_id,
const std::string& from_name,
const std::string& text,
@@ -298,7 +298,7 @@ void send_guild_card(
const std::string& name,
const std::string& team_name,
const std::string& description,
uint8_t language,
Language language,
uint8_t section_id,
uint8_t char_class);
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,
const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests,
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_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<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);
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_shop(std::shared_ptr<Client> c, uint8_t shop_type);
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_rare_enemy_index_list(std::shared_ptr<Client> c, const std::vector<size_t>& indexes);
+41 -58
View File
@@ -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;
}
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)
? this->min_levels_v4
: is_v3(version)
@@ -402,21 +402,21 @@ size_t ServerState::default_min_level_for_game(Version version, Episode episode,
: this->min_levels_v1_v2;
switch (episode) {
case Episode::EP1:
return min_levels[0].at(difficulty);
return min_levels[0].at(static_cast<size_t>(difficulty));
case Episode::EP2:
return min_levels[1].at(difficulty);
return min_levels[1].at(static_cast<size_t>(difficulty));
case Episode::EP3:
return 0;
case Episode::EP4:
return min_levels[2].at(difficulty);
return min_levels[2].at(static_cast<size_t>(difficulty));
default:
throw runtime_error("invalid episode");
}
}
shared_ptr<const SetDataTableBase> ServerState::set_data_table(
Version version, Episode episode, GameMode mode, uint8_t difficulty) const {
bool use_ult_tables = ((episode == Episode::EP1) && (difficulty == 3) && !is_v1(version) && (version != Version::PC_NTE));
Version version, Episode episode, GameMode mode, Difficulty difficulty) const {
bool use_ult_tables = ((episode == Episode::EP1) && (difficulty == Difficulty::ULTIMATE) && !is_v1(version) && (version != Version::PC_NTE));
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;
}
@@ -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 {
if (q && 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
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");
} else {
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;
} else if (is_v1(logic_version)) {
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");
} 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");
} else if (is_v4(logic_version)) {
return this->rare_item_sets.at("rare-table-v4");
@@ -1280,15 +1280,16 @@ void ServerState::load_config_early() {
} 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 {
string key = "RareEnemyRates-";
key += token_name_for_difficulty(z);
this->rare_enemy_rates_by_difficulty[z] = make_shared<MapState::RareEnemyRates>(this->config_json->at(key));
prev = this->rare_enemy_rates_by_difficulty[z];
key += token_name_for_difficulty(difficulty);
this->rare_enemy_rates_by_difficulty[diff_index] = make_shared<MapState::RareEnemyRates>(this->config_json->at(key));
prev = this->rare_enemy_rates_by_difficulty[diff_index];
} catch (const out_of_range&) {
this->rare_enemy_rates_by_difficulty[z] = prev;
this->rare_enemy_rates_by_difficulty[diff_index] = prev;
}
}
try {
@@ -1431,7 +1432,6 @@ void ServerState::load_config_late() {
this->quest_F95F_results.clear();
this->quest_F960_success_results.clear();
this->quest_F960_failure_results = QuestF960Result();
this->secret_lottery_results.clear();
if (this->item_name_index(Version::BB_V4)) {
try {
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&) {
}
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> {
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 enemies_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_e.dat");
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);
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;
@@ -1611,15 +1601,13 @@ void ServerState::load_maps() {
config_log.info_f("Loading free play map files");
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
for (Episode episode : episodes) {
for (Episode episode : ALL_EPISODES_V4) {
if ((episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) ||
(episode == Episode::EP4 && !is_v4(v))) {
continue;
}
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (GameMode mode : modes) {
for (GameMode mode : ALL_GAME_MODES_V4) {
if ((mode == GameMode::BATTLE) && is_pre_v1(v)) {
continue;
}
@@ -1629,8 +1617,8 @@ void ServerState::load_maps() {
if ((mode == GameMode::SOLO && !is_v4(v))) {
continue;
}
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
if ((difficulty == 3) && is_v1(v)) {
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
if ((difficulty == Difficulty::ULTIMATE) && is_v1(v)) {
continue;
}
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(
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);
try {
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(
Episode episode,
GameMode mode,
uint8_t difficulty,
const Variations& variations) {
Episode episode, GameMode mode, Difficulty difficulty, const Variations& variations) {
vector<shared_ptr<const SuperMap>> ret;
for (size_t floor = 0; floor < 0x12; floor++) {
Variations::Entry e;
@@ -1881,7 +1866,7 @@ void ServerState::load_word_select_table() {
unique_ptr<UnicodeTextSet> pc_unitxt_data;
if (this->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 {
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"));
@@ -1930,25 +1915,25 @@ shared_ptr<ItemNameIndex> ServerState::create_item_name_index_for_version(
shared_ptr<const TextIndex> text_index) const {
switch (limits->version) {
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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");
}
void ServerState::load_ep3_maps() {
void ServerState::load_ep3_maps(bool raise_on_any_failure) {
config_log.info_f("Collecting Episode 3 maps");
this->ep3_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps");
config_log.info_f("Collecting Episode 3 download maps");
this->ep3_download_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps-download");
this->ep3_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps", raise_on_any_failure);
}
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());
}
void ServerState::load_quest_index() {
void ServerState::load_quest_index(bool raise_on_any_failure) {
config_log.info_f("Collecting quests");
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");
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() {
+16 -11
View File
@@ -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_trial;
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 G_SetEXResultValues_Ep3_6xB4x4B> ep3_default_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::shared_ptr<const ArmorRandomSet> armor_random_set;
std::shared_ptr<const ToolRandomSet> tool_random_set;
std::array<std::shared_ptr<const WeaponRandomSet>, 4> weapon_random_sets;
std::array<std::shared_ptr<const WeaponRandomSet>, 4> weapon_random_sets; // Keyed oin difficulty
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set;
std::array<std::shared_ptr<const ItemParameterTable>, NUM_VERSIONS> item_parameter_tables;
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<QuestF960Result> quest_F960_success_results;
QuestF960Result quest_F960_failure_results;
std::vector<ItemData> secret_lottery_results;
float bb_global_exp_multiplier = 1.0f;
float exp_share_multiplier = 0.5f;
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;
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 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;
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);
@@ -391,7 +396,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::vector<PortConfiguration> parse_port_configuration(const phosg::JSON& json) const;
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) |
(static_cast<uint32_t>(mode) << 26) |
(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);
}
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(
Episode episode,
GameMode mode,
uint8_t difficulty,
Difficulty difficulty,
const Variations& variations);
void create_default_lobbies();
@@ -431,10 +436,10 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
void load_set_data_tables();
void load_word_select_table();
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_quest_index();
void compile_functions();
void load_quest_index(bool raise_on_any_failure = false);
void compile_functions(bool raise_on_any_failure = false);
void load_dol_files();
void load_all(bool enable_thread_pool);
+2 -2
View File
@@ -703,7 +703,7 @@ ShellCommand c_create_tournament(
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
string 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);
Episode3::Rules rules;
rules.set_defaults();
@@ -940,7 +940,7 @@ ShellCommand c_show_slots(
if (player.guild_card_number) {
ret.emplace_back(format(" {}: {} => {} ({}, {}, {})",
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_section_id(player.section_id)));
} else {
+30 -19
View File
@@ -476,36 +476,36 @@ bool char_class_is_force(uint8_t cls) {
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 = {
"Normal", "Hard", "Very Hard", "Ultimate"};
try {
return names.at(difficulty);
return names.at(static_cast<size_t>(difficulty));
} catch (const out_of_range&) {
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 = {
"Normal", "Hard", "VeryHard", "Ultimate"};
try {
return names.at(difficulty);
return names.at(static_cast<size_t>(difficulty));
} catch (const out_of_range&) {
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'};
try {
return names.at(difficulty);
return names.at(static_cast<size_t>(difficulty));
} catch (const out_of_range&) {
return '?';
}
}
const char* name_for_language_code(uint8_t language_code) {
const char* name_for_language(Language language) {
array<const char*, 8> names = {{"Japanese",
"English",
"German",
@@ -514,39 +514,41 @@ const char* name_for_language_code(uint8_t language_code) {
"Simplified Chinese",
"Traditional Chinese",
"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) {
return (language_code < 8) ? "JEGFSBTK"[language_code] : '?';
char char_for_language(Language language) {
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) {
case 'J':
case 'j':
return 0;
return Language::JAPANESE;
case 'E':
case 'e':
return 1;
return Language::ENGLISH;
case 'G':
case 'g':
return 2;
return Language::GERMAN;
case 'F':
case 'f':
return 3;
return Language::FRENCH;
case 'S':
case 's':
return 4;
return Language::SPANISH;
case 'B':
case 'b':
return 5;
return Language::SIMPLIFIED_CHINESE;
case 'T':
case 't':
return 6;
return Language::TRADITIONAL_CHINESE;
case 'K':
case 'k':
return 7;
return Language::KOREAN;
default:
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_EP2({0, 29, 49, 89});
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
View File
@@ -17,6 +17,26 @@ enum class Episode {
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);
const char* 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_force(uint8_t cls);
const char* name_for_difficulty(uint8_t difficulty);
const char* token_name_for_difficulty(uint8_t difficulty);
char abbreviation_for_difficulty(uint8_t difficulty);
const char* name_for_difficulty(Difficulty difficulty);
const char* token_name_for_difficulty(Difficulty difficulty);
char abbreviation_for_difficulty(Difficulty difficulty);
const char* name_for_language_code(uint8_t language_code);
char char_for_language_code(uint8_t language_code);
uint8_t language_code_for_char(char language_char);
const char* name_for_language(Language language);
char char_for_language(Language language);
Language language_for_char(char language_char);
extern const std::vector<const char*> name_for_mag_color;
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_EP2;
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
View File
@@ -20,6 +20,7 @@ TeamIndex::Team::Member::Member(const phosg::JSON& json)
try {
this->account_id = json.get_int("AccountID");
} catch (const out_of_range&) {
// Old format
this->account_id = json.get_int("SerialNumber");
}
}
@@ -78,7 +79,7 @@ void TeamIndex::Team::load_config() {
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();
for (const auto& it : this->members) {
members_json.emplace_back(it.second.json());
@@ -87,14 +88,17 @@ void TeamIndex::Team::save_config() const {
for (const auto& it : this->reward_keys) {
reward_keys_json.emplace_back(it);
}
phosg::JSON root = phosg::JSON::dict({
return phosg::JSON::dict({
{"Name", this->name},
{"SpentPoints", this->spent_points},
{"Members", std::move(members_json)},
{"RewardKeys", std::move(reward_keys_json)},
{"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() {
@@ -110,16 +114,21 @@ void TeamIndex::Team::load_flag() {
}
}
void TeamIndex::Team::save_flag() const {
if (!this->flag_data) {
return;
}
phosg::ImageRGBA8888N TeamIndex::Team::decode_flag_data() const {
phosg::ImageRGBA8888N img(32, 32);
for (size_t y = 0; y < 32; y++) {
for (size_t x = 0; x < 32; 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));
}

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