Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bab3f2f8f | |||
| fdd0bfea08 | |||
| 48c225366f | |||
| 0d88253334 | |||
| d7b17aa383 | |||
| ba131ab94a | |||
| 648d9c5164 | |||
| 60487daf6f | |||
| e0c43836b3 | |||
| 719a403b1d | |||
| 6f88c3d31a | |||
| 7114798e69 | |||
| 65384435a3 | |||
| 4236ff62b1 | |||
| 277be9bcd6 | |||
| 9493e2d3e7 | |||
| 16b15162d5 | |||
| 9854b93d02 | |||
| d02ab1e7a5 | |||
| e0c8ca677f | |||
| 2cea44f790 | |||
| fb783034bc | |||
| 40a6f49b29 | |||
| dea0ac99c3 | |||
| 24cf8e73c6 | |||
| c301a921e6 | |||
| 22d7825ba3 | |||
| 526bfb64e5 | |||
| 55cbf6e20b | |||
| 0b86ffb227 | |||
| e28596c825 | |||
| 716676b87d | |||
| 5ca0265c37 | |||
| c7a0873ca8 | |||
| b1d51cdbbe | |||
| 5a7151bc63 | |||
| 49d861919f | |||
| 3f20c4239f | |||
| 038f306661 | |||
| 0575f3c9cf | |||
| e37307acb3 | |||
| 4b32b41183 | |||
| c8f8a6f65b | |||
| 0c93275e88 | |||
| c44ab27c7e | |||
| 3f09a7b57b | |||
| 0b4d5b2f89 | |||
| 45824b46fe | |||
| e78f3142e3 | |||
| 4166149841 | |||
| 45131dabc0 | |||
| b235644575 | |||
| 377d8beac3 | |||
| 16bff52575 | |||
| 49fb7eba60 | |||
| 00b46d7161 | |||
| 5bea9d3a2b |
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Install libraries (macOS)
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
run: |
|
||||
brew install cmake asio libiconv
|
||||
brew install asio libiconv
|
||||
|
||||
cat << EOF > nproc
|
||||
#!/bin/sh
|
||||
|
||||
+3
-1
@@ -102,7 +102,7 @@ set(SOURCES
|
||||
src/Menu.cc
|
||||
src/NetworkAddresses.cc
|
||||
src/PatchFileIndex.cc
|
||||
src/PlayerFilesManager.cc
|
||||
src/PlayerInventory.cc
|
||||
src/PlayerSubordinates.cc
|
||||
src/PPKArchive.cc
|
||||
src/ProxyCommands.cc
|
||||
@@ -111,6 +111,7 @@ set(SOURCES
|
||||
src/PSOGCObjectGraph.cc
|
||||
src/PSOProtocol.cc
|
||||
src/Quest.cc
|
||||
src/QuestMetadata.cc
|
||||
src/QuestScript.cc
|
||||
src/RareItemSet.cc
|
||||
src/ReceiveCommands.cc
|
||||
@@ -136,6 +137,7 @@ target_link_libraries(newserv phosg::phosg ${Iconv_LIBRARIES} pthread resource_f
|
||||
if (WIN32)
|
||||
target_compile_definitions(newserv PUBLIC -DWINVER=0x0A00 -D_WIN32_WINNT=0x0A00)
|
||||
target_link_libraries(newserv ws2_32 mswsock bcrypt iphlpapi -static -static-libgcc -static-libstdc++)
|
||||
target_compile_options(newserv PRIVATE -Wa,-mbig-obj)
|
||||
endif()
|
||||
add_dependencies(newserv newserv-Revision-cc)
|
||||
|
||||
|
||||
@@ -118,14 +118,15 @@ newserv supports all known versions of PSO, including various development protot
|
||||
| GC Ep1&2 Plus | Yes | Yes | Yes |
|
||||
| GC Ep3 NTE | Yes | Yes (2) | Yes |
|
||||
| GC Ep3 | Yes | Yes | Yes |
|
||||
| Xbox Ep1&2 Beta | Yes | Yes | Yes |
|
||||
| Xbox Ep1&2 | Yes | Yes | Yes |
|
||||
| Xbox Ep1&2 Beta | Yes (3) | Yes (3) | Yes (3) |
|
||||
| Xbox Ep1&2 | Yes (3) | Yes (3) | Yes (3) |
|
||||
| BB (vanilla) | Yes | Yes | Yes |
|
||||
| BB (Tethealla) | Yes | Yes | Yes |
|
||||
|
||||
*Notes:*
|
||||
1. *This is the only version of PSO that doesn't have any way to identify the player's account - there is no serial number or username. For this reason, AllowUnregisteredUsers must be enabled in config.json to support PC NTE, and PC NTE players receive a random Guild Card number every time they connect. To prevent abuse, PC NTE support can be disabled in config.json.*
|
||||
2. *Episode 3 NTE battles are not well-tested; some things may not work. See notes/ep3-nte-differences.txt for a list of known differences between NTE and the final version. NTE and non-NTE players cannot battle each other.*
|
||||
3. *PSO Xbox connects through Xbox Live, so you can't easily host a private server for this version of the game. See the [How to connect](#pso-xbox) section.*
|
||||
|
||||
# Setup
|
||||
|
||||
@@ -167,6 +168,12 @@ To use newserv in other ways (e.g. for translating data), see the end of this do
|
||||
|
||||
The current version of newserv is cross-compiled using mingw-w64 on a macOS build machine, with the necessary libraries manually installed. Setting up such a build environment is tedious and not recommended; it's recommended to just use a release version instead.
|
||||
|
||||
Here is a rough outline of the Windows build process. You should only attempt this yourself if you're familiar with setting up build environments and can deal with issues you may encounter along the way.
|
||||
1. Install recent versions of MinGW and CMake.
|
||||
2. Build and install zlib, libiconv, asio, phosg, and resource_dasm into your MinGW environment.
|
||||
3. Clone the newserv repository with symlinks enabled: `git clone -c core.symlinks=true https://github.com/fuzziqersoftware/newserv.git`
|
||||
4. Build newserv via CMake.
|
||||
|
||||
## Client patch directories
|
||||
|
||||
newserv implements a patch server for PSO PC and PSO BB game data. Any file or directory you put in the system/patch-bb or system/patch-pc directories will be synced to clients when they connect to the patch server.
|
||||
@@ -256,6 +263,10 @@ If you're using the tapserver BBA or modem type, you can make it connect to a ne
|
||||
3. In PSO's network settings, enable DHCP ("Automatically obtain an IP address"), set DNS server address to "Automatic", and leave DHCP Hostname as "Not set". Leave the proxy server settings blank.
|
||||
4. Start an online game.
|
||||
|
||||
### PSO Xbox
|
||||
|
||||
Unfortunately, you can't easily host a private server for PSO Xbox because the Xbox version of the game tunnels its connections through Xbox Live. There is a modern replacement for Xbox Live named [Insignia](https://insignia.live/), which supports the three main PSO Xbox servers, but as of now does not support other private PSO servers.
|
||||
|
||||
### PSO BB
|
||||
|
||||
The PSO BB client has been modified and distributed in many different forms. newserv supports most, but not all, of the common distributions. Unlike other versions, it's common for various BB clients to have different map files. It's important that the client and server have the same map files, so make sure to set up the patch directory based on the client you'll be using with newserv. (See the [client patch directories](#client-patch-directories) section for instructions on setting this up.)
|
||||
@@ -307,7 +318,10 @@ For .dat files, the `LANGUAGE` token may be omitted. If it's present, then that
|
||||
|
||||
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-gc-e.bin` and `q058-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the files are within the retrieval/ directory within system/quests/.
|
||||
|
||||
Some quests (mostly battle and challenge mode quests) have additional JSON metadata files that describe how the server should handle them. These files include flags that can be used to hide the quest unless a preceding quest has been cleared, or to hide the quest unless purchased as a BB team reward. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See system/quests/battle/b88001.json for documentation on the exact format of the JSON file.
|
||||
Some quests have additional JSON metadata files that describe how the server should handle them. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See the comments in system/quests/retrieval/q058.json for all of the available options and how to use them. Some of the options are:
|
||||
- Disable or hide the quest if certain preceding quests aren't cleared or other conditions aren't met
|
||||
- Enable the quest to be joined while in progress
|
||||
- Override the common and/or rare item tables and set the allowed drop modes
|
||||
|
||||
Some quests may also include a .pvr file, which contains an image used in the quest. These files are named similarly to their .bin and .dat counterparts.
|
||||
|
||||
@@ -366,7 +380,7 @@ In the server drop modes, the item tables used to generate common items are in t
|
||||
|
||||
## Cross-version play
|
||||
|
||||
All versions of PSO can see and interact with each other in the lobby. By default, newserv allows V1 and V2 players to play together, and allows GC and Xbox players to play together. You can change these rules to allow all versions to play together, or to prevent versions from playing together, with the CompatibilityGroups setting in config.json.
|
||||
All versions of PSO can see and interact with each other in the lobby. By default, newserv allows V1 and V2 players to play in games together, and allows GC and Xbox players to play in games together. You can change these rules to allow all versions to play in games together, or to prevent versions from playing in games together, with the CompatibilityGroups setting in config.json.
|
||||
|
||||
There are several cross-version restrictions that always apply regardless of the compatibility groups setting:
|
||||
* DC V1 players cannot join DC V2 games if the game creator didn't choose to allow them.
|
||||
@@ -604,6 +618,7 @@ Some commands only work for clients not in proxy sessions. The chat commands are
|
||||
* `$patch <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
|
||||
|
||||
* Character data commands (non-proxy only)
|
||||
* `$switchchar <slot>` (BB only): Switch to a different character from your account without logging out.
|
||||
* `$savechar <slot>`: Save your current character data on the server in the specified slot. See the [server-side saves section](#server-side-saves) for more details.
|
||||
* `$loadchar <slot>`: Load character data from the specified slot on the server, and replace your current character with it. See the [server-side saves section](#server-side-saves) for more details.
|
||||
* `$bbchar <username> <password> <slot>`: Save your current character data on the server in a different account's BB character slots. See the [server-side saves section](#server-side-saves) for more details.
|
||||
|
||||
@@ -70,14 +70,50 @@ Disable serial number validation (untested)
|
||||
8C2670B6 01E0
|
||||
|
||||
Disable item equip restrictions ("God of equip")
|
||||
3OE0 => 0410521C 38000005
|
||||
3OE1 => 0410521C 38000005
|
||||
3OE2 => 041050E4 38000005
|
||||
3OJ2 => 04104F78 38000005
|
||||
3OJ3 => 04105154 38000005
|
||||
3OJ4 => 04105240 38000005
|
||||
3OJ5 => 041050D4 38000005
|
||||
3OJT => 0415BF50 38000005
|
||||
3OP0 => 041052D4 38000005
|
||||
59NL => 005C9F31 E9A7000000
|
||||
|
||||
All rareable enemies are rare
|
||||
3OE0 => 040AC944 60000000 // Hildeblue
|
||||
040C1B70 60000000 // Rappies
|
||||
040C3FC8 60000000 // Nar Lily
|
||||
040EB050 48000010 // Pouilly Slime
|
||||
3OE1 => 040AC944 60000000 // Hildeblue
|
||||
040C1B70 60000000 // Rappies
|
||||
040C3FC8 60000000 // Nar Lily
|
||||
040EB050 48000010 // Pouilly Slime
|
||||
3OE2 => 040ACAFC 60000000 // Hildeblue
|
||||
040C1D08 60000000 // Rappies
|
||||
040C4160 60000000 // Nar Lily
|
||||
040EB1E8 48000010 // Pouilly Slime
|
||||
3OJ2 => 040AC6B8 60000000 // Hildeblue
|
||||
040C18CC 60000000 // Rappies
|
||||
040C3D24 60000000 // Nar Lily
|
||||
040EADAC 48000010 // Pouilly Slime
|
||||
3OJ3 => 040AC9C4 60000000 // Hildeblue
|
||||
040C1BD0 60000000 // Rappies
|
||||
040C4028 60000000 // Nar Lily
|
||||
040EB0B0 48000010 // Pouilly Slime
|
||||
3OJ4 => 040ACB3C 60000000 // Hildeblue
|
||||
040C1E04 60000000 // Rappies
|
||||
040C41A0 60000000 // Nar Lily
|
||||
040EB374 48000010 // Pouilly Slime
|
||||
3OJ5 => 040ACAEC 60000000 // Hildeblue
|
||||
040C1CF8 60000000 // Rappies
|
||||
040C4150 60000000 // Nar Lily
|
||||
040EB1D8 48000010 // Pouilly Slime
|
||||
3OP0 => 040ACAC4 60000000 // Hildeblue
|
||||
040C1CD0 60000000 // Rappies
|
||||
040C4128 60000000 // Nar Lily
|
||||
040EB1B0 48000010 // Pouilly Slime
|
||||
|
||||
Unlock all songs in BGM test
|
||||
Note: sadly, there are no secret/unused ones
|
||||
@@ -199,6 +235,16 @@ Unlock all COM decks
|
||||
3SP0 => 042CB414 38600001
|
||||
3SE0 => 042CA908 38600001
|
||||
|
||||
Enable marker color menu in all lobbies
|
||||
3OJ2 => 04138200 3800000E
|
||||
3OJ3 => 04138508 3800000E
|
||||
3OJ4 => 041390AC 3800000E
|
||||
3OJ5 => 041385B0 3800000E
|
||||
3OE0 => 041384BC 3800000E
|
||||
3OE1 => 041384BC 3800000E
|
||||
3OE2 => 041385C0 3800000E
|
||||
3OP0 => 04138840 3800000E
|
||||
|
||||
Enable all lobby counter options in non-CARD lobbies
|
||||
3SE0 => 04096A8C 480000C0
|
||||
04096B4C 38800007
|
||||
@@ -219,7 +265,10 @@ Change HUD color mask
|
||||
0438CA90 6000BBAA
|
||||
|
||||
Disable lobby event music (but keep the visuals)
|
||||
3OJT => 040B2394 38000000
|
||||
3SE0 => 040B705C 38000000
|
||||
3SJ0 => 040B7078 38000000
|
||||
3SP0 => 040B74A0 38000000
|
||||
|
||||
Enable Pinz's Shop Super Card Capsule Machine as a fourth option
|
||||
3SE0 => 043101C0 38800004
|
||||
@@ -434,6 +483,11 @@ Note: Without a TextEnglish.pr2/pr3 patch, the menu items for these sounds will
|
||||
0442B6E0 802C0000
|
||||
|
||||
Use English language files
|
||||
3OJT => 04189FE8 38000001
|
||||
0418A010 38000001
|
||||
0418A0A0 38000001
|
||||
0418A0C8 38000001
|
||||
04189EC4 3BC00001
|
||||
3SJT => 0408E414 38600001
|
||||
0408E448 38000001
|
||||
0408E44C 900DA62C
|
||||
@@ -722,7 +776,14 @@ Show extended item info when targeting a dropped item
|
||||
04005190 4E800020
|
||||
|
||||
All weapons can do 3-hit combos
|
||||
3OE0 => 041D3248 38000001
|
||||
3OE1 => 041D3248 38000001
|
||||
3OE2 => 041D3448 38000001
|
||||
3OJ2 => 041D2DEC 38000001
|
||||
3OJ3 => 041D3318 38000001
|
||||
3OJ4 => 041D3144 38000001
|
||||
3OJ5 => 041D33E4 38000001
|
||||
3OP0 => 041D3904 38000001
|
||||
|
||||
Disable save file signature validation (for moving Xbox saves across consoles)
|
||||
4OJB => 002F01CB 9090
|
||||
|
||||
+919
-919
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
0019 = P2 Scientist after defeating dragon
|
||||
001E = Entered Caves 1 (Gov 2-1)
|
||||
001F = Entered De Rol Le in 2-4
|
||||
0020 = De Ro lee defeated
|
||||
0020 = De Rol Le defeated
|
||||
0021 = Mines unlocked (P2 Tyrell after defeating De Rol Le)
|
||||
0028 = Entered Mines 1
|
||||
0029 = Entered Vol Opt Area
|
||||
|
||||
@@ -23,6 +23,10 @@ public:
|
||||
return this->entries;
|
||||
}
|
||||
|
||||
inline size_t num_entries() const {
|
||||
return this->entries.size();
|
||||
}
|
||||
|
||||
std::pair<const void*, size_t> get(size_t index) const;
|
||||
std::string get_copy(size_t index) const;
|
||||
phosg::StringReader get_reader(size_t index) const;
|
||||
|
||||
+22
-17
@@ -34,7 +34,7 @@ public:
|
||||
}
|
||||
|
||||
void set_value(T&& result) {
|
||||
if (this->exc || this->val.has_value()) {
|
||||
if (this->done()) {
|
||||
throw std::logic_error("attempted to set value on completed promise");
|
||||
}
|
||||
this->val = result;
|
||||
@@ -42,7 +42,7 @@ public:
|
||||
}
|
||||
|
||||
void set_exception(std::exception_ptr ex) {
|
||||
if (this->exc || this->val.has_value()) {
|
||||
if (this->done()) {
|
||||
throw std::logic_error("attempted to set value on completed promise");
|
||||
}
|
||||
this->exc = ex;
|
||||
@@ -67,12 +67,13 @@ private:
|
||||
std::optional<ResolverRef> resolver_ref;
|
||||
|
||||
void resolve() {
|
||||
if (this->resolver_ref.has_value()) {
|
||||
if (this->resolver_ref) {
|
||||
auto* executor = this->resolver_ref->executor;
|
||||
asio::post(*executor, [ref = std::move(this->resolver_ref)]() mutable -> void {
|
||||
ref->resolve(std::error_code{});
|
||||
});
|
||||
ResolverRef ref = std::move(*this->resolver_ref);
|
||||
this->resolver_ref.reset();
|
||||
asio::post(*executor, [ref = std::move(ref)]() mutable -> void {
|
||||
ref.resolve(std::error_code{});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -102,7 +103,7 @@ public:
|
||||
}
|
||||
|
||||
void set_value() {
|
||||
if (this->exc || this->returned) {
|
||||
if (this->done()) {
|
||||
throw std::logic_error("attempted to set value on completed promise");
|
||||
}
|
||||
this->returned = true;
|
||||
@@ -110,7 +111,7 @@ public:
|
||||
}
|
||||
|
||||
void set_exception(std::exception_ptr ex) {
|
||||
if (this->exc || this->returned) {
|
||||
if (this->done()) {
|
||||
throw std::logic_error("attempted to set value on completed promise");
|
||||
}
|
||||
this->exc = ex;
|
||||
@@ -130,17 +131,18 @@ private:
|
||||
asio::detail::awaitable_handler<asio::any_io_executor, std::error_code> resolve;
|
||||
asio::any_io_executor* executor;
|
||||
};
|
||||
bool returned;
|
||||
bool returned = false;
|
||||
std::exception_ptr exc;
|
||||
std::optional<ResolverRef> resolver_ref;
|
||||
|
||||
void resolve() {
|
||||
if (this->resolver_ref.has_value()) {
|
||||
if (this->resolver_ref) {
|
||||
auto* executor = this->resolver_ref->executor;
|
||||
asio::post(*executor, [ref = std::move(this->resolver_ref)]() mutable -> void {
|
||||
ref->resolve(std::error_code{});
|
||||
});
|
||||
ResolverRef ref = std::move(*this->resolver_ref);
|
||||
this->resolver_ref.reset();
|
||||
asio::post(*executor, [ref = std::move(ref)]() mutable -> void {
|
||||
ref.resolve(std::error_code{});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -251,10 +253,13 @@ template <typename FnT, typename... ArgTs>
|
||||
asio::awaitable<std::invoke_result_t<FnT, ArgTs...>> call_on_thread_pool(asio::thread_pool& pool, FnT&& f, ArgTs&&... args) {
|
||||
using ReturnT = std::invoke_result_t<FnT, ArgTs...>;
|
||||
auto bound = std::bind(std::forward<FnT>(f), std::forward<ArgTs>(args)...);
|
||||
AsyncPromise<ReturnT> promise;
|
||||
|
||||
asio::post(pool, [&promise, &bound]() -> void {
|
||||
promise.set_value(bound());
|
||||
// We have to use a shared_ptr here in case call_on_thread_pool is canceled
|
||||
// (in that case, the posted callback will try to use promise after the
|
||||
// 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());
|
||||
});
|
||||
co_return co_await promise.get();
|
||||
co_return co_await promise->get();
|
||||
}
|
||||
|
||||
+117
-80
@@ -225,7 +225,7 @@ static asio::awaitable<void> server_command_announce_inner(const Args& a, bool m
|
||||
send_text_or_scrolling_message(s, a.text, a.text);
|
||||
}
|
||||
} else {
|
||||
auto from_name = a.c->character()->disp.name.decode(a.c->language());
|
||||
auto from_name = a.c->character_file()->disp.name.decode(a.c->language());
|
||||
if (mail) {
|
||||
send_simple_mail(s, 0, from_name, a.text);
|
||||
} else {
|
||||
@@ -332,7 +332,7 @@ ChatCommandDefinition cc_auction(
|
||||
});
|
||||
|
||||
static string name_for_client(shared_ptr<Client> c) {
|
||||
auto player = c->character(false);
|
||||
auto player = c->character_file(false);
|
||||
if (player.get()) {
|
||||
return escape_player_name(player->disp.name.decode(player->inventory.language));
|
||||
}
|
||||
@@ -417,28 +417,24 @@ ChatCommandDefinition cc_bank(
|
||||
|
||||
ssize_t new_char_index = a.text.empty() ? (a.c->bb_character_index + 1) : stol(a.text, nullptr, 0);
|
||||
|
||||
if (new_char_index == 0) {
|
||||
if (a.c->use_shared_bank()) {
|
||||
send_text_message(a.c, "$C6Using shared bank (0)");
|
||||
} else {
|
||||
send_text_message(a.c, "$C6Created shared bank (0)");
|
||||
}
|
||||
} else if (new_char_index <= 4) {
|
||||
a.c->use_character_bank(new_char_index - 1);
|
||||
auto bp = a.c->current_bank_character();
|
||||
if (new_char_index <= 0) {
|
||||
a.c->change_bank(-1);
|
||||
send_text_message(a.c, "$C6Using shared bank");
|
||||
|
||||
} else if (new_char_index <= 127) {
|
||||
a.c->change_bank(new_char_index - 1);
|
||||
send_text_message_fmt(a.c, "$C6Using character {}'s bank", new_char_index);
|
||||
|
||||
auto name = escape_player_name(bp->disp.name.decode(a.c->language()));
|
||||
send_text_message_fmt(a.c, "$C6Using {}\'s bank ({})", name, new_char_index);
|
||||
} else {
|
||||
throw precondition_failed("$C6Invalid bank number");
|
||||
}
|
||||
|
||||
auto& bank = a.c->current_bank();
|
||||
bank.assign_ids(0x99000000 + (a.c->lobby_client_id << 20));
|
||||
auto bank = a.c->bank_file();
|
||||
bank->assign_ids(0x99000000 + (a.c->lobby_client_id << 20));
|
||||
a.c->log.info_f("Assigned bank item IDs");
|
||||
a.c->print_bank();
|
||||
|
||||
send_text_message_fmt(a.c, "{} items\n{} Meseta", bank.num_items, bank.meseta);
|
||||
send_text_message_fmt(a.c, "{} items\n{} Meseta", bank->items.size(), bank->meseta);
|
||||
co_return;
|
||||
});
|
||||
|
||||
@@ -489,7 +485,7 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
|
||||
// server already has it)
|
||||
GetPlayerInfoResult ch;
|
||||
if (a.c->version() == Version::BB_V4) {
|
||||
ch.character = a.c->character();
|
||||
ch.character = a.c->character_file();
|
||||
ch.is_full_info = true;
|
||||
} else {
|
||||
ch = co_await send_get_player_info(a.c, true);
|
||||
@@ -499,11 +495,6 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
|
||||
? Client::character_filename(dest_bb_license->username, dest_character_index)
|
||||
: Client::backup_character_filename(dest_account->account_id, dest_character_index, is_ep3(a.c->version()));
|
||||
|
||||
if (s->player_files_manager->get_character(filename)) {
|
||||
send_text_message(a.c, "$C6The target player\nis currently loaded.\nSign off in Blue\nBurst and try again.");
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (ch.is_full_info) {
|
||||
// Client sent 30; ch contains the verbatim save file from the client
|
||||
if (ch.ep3_character) {
|
||||
@@ -783,41 +774,40 @@ ChatCommandDefinition cc_dropmode(
|
||||
|
||||
if (a.c->proxy_session) {
|
||||
|
||||
using DropMode = ProxySession::DropMode;
|
||||
if (a.text.empty()) {
|
||||
switch (a.c->proxy_session->drop_mode) {
|
||||
case DropMode::DISABLED:
|
||||
case ProxyDropMode::DISABLED:
|
||||
send_text_message(a.c, "Drop mode: disabled");
|
||||
break;
|
||||
case DropMode::PASSTHROUGH:
|
||||
case ProxyDropMode::PASSTHROUGH:
|
||||
send_text_message(a.c, "Drop mode: default");
|
||||
break;
|
||||
case DropMode::INTERCEPT:
|
||||
case ProxyDropMode::INTERCEPT:
|
||||
send_text_message(a.c, "Drop mode: proxy");
|
||||
break;
|
||||
}
|
||||
|
||||
} else {
|
||||
DropMode new_mode;
|
||||
ProxyDropMode new_mode;
|
||||
if ((a.text == "none") || (a.text == "disabled")) {
|
||||
new_mode = DropMode::DISABLED;
|
||||
new_mode = ProxyDropMode::DISABLED;
|
||||
} else if ((a.text == "default") || (a.text == "passthrough")) {
|
||||
new_mode = DropMode::PASSTHROUGH;
|
||||
new_mode = ProxyDropMode::PASSTHROUGH;
|
||||
} else if ((a.text == "proxy") || (a.text == "intercept")) {
|
||||
new_mode = DropMode::INTERCEPT;
|
||||
new_mode = ProxyDropMode::INTERCEPT;
|
||||
} else {
|
||||
throw precondition_failed("Invalid drop mode");
|
||||
}
|
||||
|
||||
a.c->proxy_session->set_drop_mode(s, a.c->version(), a.c->override_random_seed, new_mode);
|
||||
switch (a.c->proxy_session->drop_mode) {
|
||||
case DropMode::DISABLED:
|
||||
case ProxyDropMode::DISABLED:
|
||||
send_text_message(a.c->channel, "Item drops disabled");
|
||||
break;
|
||||
case DropMode::PASSTHROUGH:
|
||||
case ProxyDropMode::PASSTHROUGH:
|
||||
send_text_message(a.c->channel, "Item drops changed\nto default mode");
|
||||
break;
|
||||
case DropMode::INTERCEPT:
|
||||
case ProxyDropMode::INTERCEPT:
|
||||
send_text_message(a.c->channel, "Item drops changed\nto proxy mode");
|
||||
break;
|
||||
}
|
||||
@@ -827,36 +817,36 @@ ChatCommandDefinition cc_dropmode(
|
||||
auto l = a.c->require_lobby();
|
||||
if (a.text.empty()) {
|
||||
switch (l->drop_mode) {
|
||||
case Lobby::DropMode::DISABLED:
|
||||
case ServerDropMode::DISABLED:
|
||||
send_text_message(a.c, "Drop mode: disabled");
|
||||
break;
|
||||
case Lobby::DropMode::CLIENT:
|
||||
case ServerDropMode::CLIENT:
|
||||
send_text_message(a.c, "Drop mode: client");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_SHARED:
|
||||
case ServerDropMode::SERVER_SHARED:
|
||||
send_text_message(a.c, "Drop mode: server\nshared");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_PRIVATE:
|
||||
case ServerDropMode::SERVER_PRIVATE:
|
||||
send_text_message(a.c, "Drop mode: server\nprivate");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_DUPLICATE:
|
||||
case ServerDropMode::SERVER_DUPLICATE:
|
||||
send_text_message(a.c, "Drop mode: server\nduplicate");
|
||||
break;
|
||||
}
|
||||
|
||||
} else {
|
||||
a.check_is_leader();
|
||||
Lobby::DropMode new_mode;
|
||||
ServerDropMode new_mode;
|
||||
if ((a.text == "none") || (a.text == "disabled")) {
|
||||
new_mode = Lobby::DropMode::DISABLED;
|
||||
new_mode = ServerDropMode::DISABLED;
|
||||
} else if (a.text == "client") {
|
||||
new_mode = Lobby::DropMode::CLIENT;
|
||||
new_mode = ServerDropMode::CLIENT;
|
||||
} else if ((a.text == "shared") || (a.text == "server")) {
|
||||
new_mode = Lobby::DropMode::SERVER_SHARED;
|
||||
new_mode = ServerDropMode::SERVER_SHARED;
|
||||
} else if ((a.text == "private") || (a.text == "priv")) {
|
||||
new_mode = Lobby::DropMode::SERVER_PRIVATE;
|
||||
new_mode = ServerDropMode::SERVER_PRIVATE;
|
||||
} else if ((a.text == "duplicate") || (a.text == "dup")) {
|
||||
new_mode = Lobby::DropMode::SERVER_DUPLICATE;
|
||||
new_mode = ServerDropMode::SERVER_DUPLICATE;
|
||||
} else {
|
||||
throw precondition_failed("Invalid drop mode");
|
||||
}
|
||||
@@ -867,19 +857,19 @@ ChatCommandDefinition cc_dropmode(
|
||||
|
||||
l->drop_mode = new_mode;
|
||||
switch (l->drop_mode) {
|
||||
case Lobby::DropMode::DISABLED:
|
||||
case ServerDropMode::DISABLED:
|
||||
send_text_message(l, "Item drops disabled");
|
||||
break;
|
||||
case Lobby::DropMode::CLIENT:
|
||||
case ServerDropMode::CLIENT:
|
||||
send_text_message(l, "Item drops changed\nto client mode");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_SHARED:
|
||||
case ServerDropMode::SERVER_SHARED:
|
||||
send_text_message(l, "Item drops changed\nto server shared\nmode");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_PRIVATE:
|
||||
case ServerDropMode::SERVER_PRIVATE:
|
||||
send_text_message(l, "Item drops changed\nto server private\nmode");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_DUPLICATE:
|
||||
case ServerDropMode::SERVER_DUPLICATE:
|
||||
send_text_message(l, "Item drops changed\nto server duplicate\nmode");
|
||||
break;
|
||||
}
|
||||
@@ -910,7 +900,7 @@ ChatCommandDefinition cc_edit(
|
||||
using MatType = PSOBBCharacterFile::MaterialType;
|
||||
|
||||
try {
|
||||
auto p = a.c->character();
|
||||
auto p = a.c->character_file();
|
||||
if (tokens.at(0) == "atp" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
p->disp.stats.char_stats.atp = stoul(tokens.at(1));
|
||||
} else if (tokens.at(0) == "mst" && (cheats_allowed || !s->cheat_flags.edit_stats)) {
|
||||
@@ -1260,7 +1250,7 @@ ChatCommandDefinition cc_item(
|
||||
item = s->parse_item_description(a.c->version(), a.text);
|
||||
item.id = l->generate_item_id(a.c->lobby_client_id);
|
||||
|
||||
if ((l->drop_mode == Lobby::DropMode::SERVER_PRIVATE) || (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE)) {
|
||||
if ((l->drop_mode == ServerDropMode::SERVER_PRIVATE) || (l->drop_mode == ServerDropMode::SERVER_DUPLICATE)) {
|
||||
l->add_item(a.c->floor, item, a.c->pos, nullptr, nullptr, (1 << a.c->lobby_client_id));
|
||||
send_drop_stacked_item_to_channel(s, a.c->channel, item, a.c->floor, a.c->pos);
|
||||
} else {
|
||||
@@ -1330,7 +1320,7 @@ ChatCommandDefinition cc_killcount(
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
a.check_is_proxy(false);
|
||||
|
||||
auto p = a.c->character();
|
||||
auto p = a.c->character_file();
|
||||
vector<size_t> item_indexes;
|
||||
for (size_t z = 0; z < p->inventory.num_items; z++) {
|
||||
const auto& item = p->inventory.items[z];
|
||||
@@ -1450,19 +1440,19 @@ ChatCommandDefinition cc_lobby_info(
|
||||
"$C7Section ID: $C6{}$C7", name_for_section_id(l->effective_section_id())));
|
||||
|
||||
switch (l->drop_mode) {
|
||||
case Lobby::DropMode::DISABLED:
|
||||
case ServerDropMode::DISABLED:
|
||||
lines.emplace_back("Drops disabled");
|
||||
break;
|
||||
case Lobby::DropMode::CLIENT:
|
||||
case ServerDropMode::CLIENT:
|
||||
lines.emplace_back("Client item table");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_SHARED:
|
||||
case ServerDropMode::SERVER_SHARED:
|
||||
lines.emplace_back("Server item table");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_PRIVATE:
|
||||
case ServerDropMode::SERVER_PRIVATE:
|
||||
lines.emplace_back("Server indiv items");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_DUPLICATE:
|
||||
case ServerDropMode::SERVER_DUPLICATE:
|
||||
lines.emplace_back("Server dup items");
|
||||
break;
|
||||
default:
|
||||
@@ -1596,13 +1586,13 @@ ChatCommandDefinition cc_loadchar(
|
||||
};
|
||||
|
||||
if (a.c->version() == Version::DC_V2) {
|
||||
PSODCV2CharacterFile::Character dc_char = *a.c->character();
|
||||
PSODCV2CharacterFile::Character dc_char = *a.c->character_file();
|
||||
co_await send_set_extended_player_info(dc_char);
|
||||
} else if (a.c->version() == Version::GC_NTE) {
|
||||
PSOGCNTECharacterFileCharacter gc_char = *a.c->character();
|
||||
PSOGCNTECharacterFileCharacter gc_char = *a.c->character_file();
|
||||
co_await send_set_extended_player_info(gc_char);
|
||||
} else if (a.c->version() == Version::GC_V3) {
|
||||
PSOGCCharacterFile::Character gc_char = *a.c->character();
|
||||
PSOGCCharacterFile::Character gc_char = *a.c->character_file();
|
||||
co_await send_set_extended_player_info(gc_char);
|
||||
} else if (a.c->version() == Version::GC_EP3_NTE) {
|
||||
PSOGCEp3NTECharacter nte_char = *ep3_char;
|
||||
@@ -1613,7 +1603,7 @@ ChatCommandDefinition cc_loadchar(
|
||||
if (!a.c->login || !a.c->login->xb_license) {
|
||||
throw runtime_error("XB client is not logged in");
|
||||
}
|
||||
PSOXBCharacterFile::Character xb_char = *a.c->character();
|
||||
PSOXBCharacterFile::Character xb_char = *a.c->character_file();
|
||||
xb_char.guild_card.xb_user_id_high = (a.c->login->xb_license->user_id >> 32) & 0xFFFFFFFF;
|
||||
xb_char.guild_card.xb_user_id_low = a.c->login->xb_license->user_id & 0xFFFFFFFF;
|
||||
co_await send_set_extended_player_info(xb_char);
|
||||
@@ -1636,7 +1626,7 @@ ChatCommandDefinition cc_matcount(
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
a.check_is_proxy(false);
|
||||
|
||||
auto p = a.c->character();
|
||||
auto p = a.c->character_file();
|
||||
if (is_v1_or_v2(a.c->version())) {
|
||||
send_text_message_fmt(a.c, "{} HP, {} TP",
|
||||
p->get_material_usage(PSOBBCharacterFile::MaterialType::HP),
|
||||
@@ -1878,7 +1868,7 @@ ChatCommandDefinition cc_qcheck(
|
||||
if (!l->quest_flags_known || l->quest_flags_known->get(l->difficulty, flag_num)) {
|
||||
send_text_message_fmt(a.c, "$C7Game: flag 0x{:X} ({})\nis {} on {}",
|
||||
flag_num, flag_num,
|
||||
a.c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
|
||||
a.c->character_file()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
|
||||
name_for_difficulty(l->difficulty));
|
||||
} else {
|
||||
send_text_message_fmt(a.c, "$C7Game: flag 0x{:X} ({})\nis unknown on {}",
|
||||
@@ -1887,7 +1877,7 @@ ChatCommandDefinition cc_qcheck(
|
||||
} else if (a.c->version() == Version::BB_V4) {
|
||||
send_text_message_fmt(a.c, "$C7Player: flag 0x{:X} ({})\nis {} on {}",
|
||||
flag_num, flag_num,
|
||||
a.c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
|
||||
a.c->character_file()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set",
|
||||
name_for_difficulty(l->difficulty));
|
||||
}
|
||||
co_return;
|
||||
@@ -1912,7 +1902,7 @@ static void command_qset_qclear(const Args& a, bool should_set) {
|
||||
}
|
||||
}
|
||||
|
||||
auto p = a.c->character(false);
|
||||
auto p = a.c->character_file(false);
|
||||
if (p) {
|
||||
if (should_set) {
|
||||
p->quest_flags.set(l->difficulty, flag_num);
|
||||
@@ -1965,7 +1955,7 @@ ChatCommandDefinition cc_qfread(
|
||||
throw runtime_error("invalid quest counter definition");
|
||||
}
|
||||
|
||||
uint32_t counter_value = a.c->character()->quest_counters.at(counter_index) & mask;
|
||||
uint32_t counter_value = a.c->character_file()->quest_counters.at(counter_index) & mask;
|
||||
|
||||
while (!(mask & 1)) {
|
||||
mask >>= 1;
|
||||
@@ -1985,7 +1975,7 @@ ChatCommandDefinition cc_qgread(
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
a.check_is_proxy(false);
|
||||
uint8_t counter_num = stoul(a.text, nullptr, 0);
|
||||
const auto& counters = a.c->character()->quest_counters;
|
||||
const auto& counters = a.c->character_file()->quest_counters;
|
||||
if (counter_num >= counters.size()) {
|
||||
throw precondition_failed("$C7Counter ID must be\nless than {}", counters.size());
|
||||
} else {
|
||||
@@ -2014,11 +2004,11 @@ ChatCommandDefinition cc_qgwrite(
|
||||
|
||||
uint8_t counter_num = stoul(tokens[0], nullptr, 0);
|
||||
uint32_t value = stoul(tokens[1], nullptr, 0);
|
||||
auto& counters = a.c->character()->quest_counters;
|
||||
auto& counters = a.c->character_file()->quest_counters;
|
||||
if (counter_num >= counters.size()) {
|
||||
throw precondition_failed("$C7Counter ID must be\nless than {}", counters.size());
|
||||
} else {
|
||||
a.c->character()->quest_counters[counter_num] = value;
|
||||
a.c->character_file()->quest_counters[counter_num] = value;
|
||||
G_SetQuestCounter_BB_6xD2 cmd = {{0xD2, sizeof(G_SetQuestCounter_BB_6xD2) / 4, a.c->lobby_client_id}, counter_num, value};
|
||||
send_command_t(a.c, 0x60, 0x00, cmd);
|
||||
send_text_message_fmt(a.c, "$C7Quest counter {}\nset to {}", counter_num, value);
|
||||
@@ -2084,8 +2074,7 @@ ChatCommandDefinition cc_quest(
|
||||
a.check_is_game(true);
|
||||
|
||||
auto s = a.c->require_server_state();
|
||||
Version effective_version = is_ep3(a.c->version()) ? Version::GC_V3 : a.c->version();
|
||||
auto q = s->quest_index(effective_version)->get(stoul(a.text));
|
||||
auto q = s->quest_index->get(stoul(a.text));
|
||||
if (!q) {
|
||||
throw precondition_failed("$C6Quest not found");
|
||||
}
|
||||
@@ -2095,11 +2084,20 @@ ChatCommandDefinition cc_quest(
|
||||
if (l->count_clients() > 1) {
|
||||
throw precondition_failed("$C6This command can only\nbe used with no\nother players present");
|
||||
}
|
||||
if (!q->allow_start_from_chat_command) {
|
||||
if (!q->meta.allow_start_from_chat_command) {
|
||||
throw precondition_failed("$C6This quest cannot\nbe started with the\n%squest command");
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t client_id = 0; client_id < l->max_clients; client_id++) {
|
||||
auto lc = l->clients[client_id];
|
||||
if (lc) {
|
||||
if (!q->version(lc->version(), lc->language())) {
|
||||
throw precondition_failed("$C6Quest does not exist\nfor all players\' game\nversions");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set_lobby_quest(a.c->require_lobby(), q, true);
|
||||
co_return;
|
||||
});
|
||||
@@ -2375,8 +2373,9 @@ ChatCommandDefinition cc_sound(
|
||||
bool echo_to_all = (!a.text.empty() && a.text[0] == '!');
|
||||
uint32_t sound_id = stoul(echo_to_all ? a.text.substr(1) : a.text, nullptr, 16);
|
||||
|
||||
// TODO: Using floor is technically incorrect here; it should be area
|
||||
G_PlaySoundFromPlayer_6xB2 cmd = {{0xB2, 0x03, 0x0000}, static_cast<uint8_t>(a.c->floor), 0x00, a.c->lobby_client_id, sound_id};
|
||||
auto l = a.c->require_lobby();
|
||||
uint8_t area = l->area_for_floor(a.c->version(), a.c->floor);
|
||||
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);
|
||||
} else if (a.c->proxy_session) {
|
||||
@@ -2533,7 +2532,7 @@ ChatCommandDefinition cc_surrender(
|
||||
if (!ps || !ps->is_alive()) {
|
||||
throw precondition_failed("$C6Defeated players\ncannot surrender");
|
||||
}
|
||||
string name = remove_color(a.c->character()->disp.name.decode(a.c->language()));
|
||||
string name = remove_color(a.c->character_file()->disp.name.decode(a.c->language()));
|
||||
send_text_message_fmt(l, "$C6{} has\nsurrendered", name);
|
||||
for (const auto& watcher_l : l->watcher_lobbies) {
|
||||
send_text_message_fmt(watcher_l, "$C6{} has\nsurrendered", name);
|
||||
@@ -2638,6 +2637,47 @@ ChatCommandDefinition cc_swsetall(
|
||||
co_return;
|
||||
});
|
||||
|
||||
ChatCommandDefinition cc_switchchar(
|
||||
{"$switchchar"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
auto l = a.c->require_lobby();
|
||||
auto s = a.c->require_server_state();
|
||||
|
||||
a.check_is_proxy(false);
|
||||
a.check_is_game(false);
|
||||
if (a.c->version() != Version::BB_V4) {
|
||||
throw precondition_failed("This command can only\nbe used on BB");
|
||||
}
|
||||
|
||||
int32_t index = stol(a.text, nullptr, 0) - 1;
|
||||
if (index < 0) {
|
||||
throw precondition_failed("Invalid slot number");
|
||||
}
|
||||
auto filename = Client::character_filename(a.c->login->bb_license->username, index);
|
||||
if (!std::filesystem::is_regular_file(filename)) {
|
||||
throw precondition_failed("No character exists\nin that slot");
|
||||
}
|
||||
|
||||
a.c->save_and_unload_character();
|
||||
a.c->bb_character_index = index;
|
||||
a.c->bb_bank_character_index = index;
|
||||
|
||||
// TODO: This can trigger a client bug where the previous character's
|
||||
// name label object isn't deleted if the leave and join notifications
|
||||
// are received on the same frame. This results in the receiving player
|
||||
// seeing both labels over the new character, with the latest one
|
||||
// appearing on top. We could fix this by requiring each recipient to
|
||||
// reply to a ping between the two commands, similar to how the 64 and
|
||||
// 6x6D commands are split during game joining, but implementing that
|
||||
// here seems not worth the effort given the low likelihood and impact of
|
||||
// this bug.
|
||||
send_complete_player_bb(a.c);
|
||||
send_player_leave_notification(l, a.c->lobby_client_id);
|
||||
s->send_lobby_join_notifications(l, a.c);
|
||||
|
||||
co_return;
|
||||
});
|
||||
|
||||
ChatCommandDefinition cc_unset(
|
||||
{"$unset"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
@@ -2781,13 +2821,10 @@ static void whatobj_whatene_fn(const Args& a, bool include_objs, bool include_en
|
||||
throw precondition_failed("$C4No map loaded");
|
||||
}
|
||||
|
||||
// TODO: We should use the actual area if a loaded quest has reassigned
|
||||
// them; it's likely that the variations will be wrong if we don't
|
||||
uint8_t area, layout_var;
|
||||
auto s = a.c->require_server_state();
|
||||
if (l->episode != Episode::EP3) {
|
||||
auto sdt = s->set_data_table(a.c->version(), l->episode, l->mode, l->difficulty);
|
||||
area = sdt->default_area_for_floor(l->episode, a.c->floor);
|
||||
area = l->area_for_floor(a.c->version(), a.c->floor);
|
||||
layout_var = (a.c->floor < 0x10) ? l->variations.entries[a.c->floor].layout.load() : 0x00;
|
||||
} else {
|
||||
area = a.c->floor;
|
||||
@@ -2911,7 +2948,7 @@ ChatCommandDefinition cc_where(
|
||||
if (!a.c->proxy_session && l && l->is_game()) {
|
||||
for (auto lc : l->clients) {
|
||||
if (lc && (lc != a.c)) {
|
||||
string name = lc->character()->disp.name.decode(lc->language());
|
||||
string name = lc->character_file()->disp.name.decode(lc->language());
|
||||
send_text_message_fmt(a.c, "$C6{}$C7 {:X}:{}",
|
||||
name, lc->floor, FloorDefinition::get(l->episode, lc->floor).short_name);
|
||||
}
|
||||
|
||||
+7
-7
@@ -28,10 +28,10 @@ const vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
|
||||
if (choice_id == 0x0000) {
|
||||
return true;
|
||||
}
|
||||
uint32_t target_level = target_c->character()->disp.stats.level + 1;
|
||||
uint32_t target_level = target_c->character_file()->disp.stats.level + 1;
|
||||
switch (choice_id) {
|
||||
case 0x0001:
|
||||
return (labs(static_cast<int32_t>(target_level - searcher_c->character()->disp.stats.level)) <= 5);
|
||||
return (labs(static_cast<int32_t>(target_level - searcher_c->character_file()->disp.stats.level)) <= 5);
|
||||
case 0x0002:
|
||||
return (target_level <= 10);
|
||||
case 0x0003:
|
||||
@@ -80,13 +80,13 @@ const vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
|
||||
case 0x0000:
|
||||
return true;
|
||||
case 0x0010:
|
||||
return target_c->character()->disp.visual.class_flags & 0x20;
|
||||
return target_c->character_file()->disp.visual.class_flags & 0x20;
|
||||
case 0x0011:
|
||||
return target_c->character()->disp.visual.class_flags & 0x40;
|
||||
return target_c->character_file()->disp.visual.class_flags & 0x40;
|
||||
case 0x0012:
|
||||
return target_c->character()->disp.visual.class_flags & 0x80;
|
||||
return target_c->character_file()->disp.visual.class_flags & 0x80;
|
||||
default:
|
||||
return ((choice_id - 1) == target_c->character()->disp.visual.char_class);
|
||||
return ((choice_id - 1) == target_c->character_file()->disp.visual.char_class);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -143,7 +143,7 @@ const vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
|
||||
{0x0006, "Challenge"},
|
||||
},
|
||||
.client_matches = +[](shared_ptr<Client>, shared_ptr<Client> target_c, uint16_t choice_id) -> bool {
|
||||
uint16_t target_choice_id = target_c->character()->choice_search_config.get_setting(0x0204);
|
||||
uint16_t target_choice_id = target_c->character_file()->choice_search_config.get_setting(0x0204);
|
||||
return (choice_id == 0) || (target_choice_id == 0) || (choice_id == target_choice_id);
|
||||
},
|
||||
},
|
||||
|
||||
+322
-303
@@ -244,12 +244,11 @@ Client::~Client() {
|
||||
void Client::update_channel_name() {
|
||||
string default_name = this->channel->default_name();
|
||||
|
||||
auto player = this->character(false, false);
|
||||
auto player = this->character_file(false, false);
|
||||
if (player) {
|
||||
string name_str = player->disp.name.decode(this->language());
|
||||
size_t level = player->disp.stats.level + 1;
|
||||
this->channel->name = std::format("C-{:X} ({} Lv.{}) @ {}",
|
||||
this->id, name_str, level, default_name);
|
||||
this->channel->name = std::format("C-{:X} ({} Lv.{}) @ {}", this->id, name_str, level, default_name);
|
||||
} else {
|
||||
this->channel->name = std::format("C-{:X} @ {}", this->id, default_name);
|
||||
}
|
||||
@@ -263,7 +262,7 @@ void Client::reschedule_save_game_data_timer() {
|
||||
this->save_game_data_timer.expires_after(std::chrono::seconds(60));
|
||||
this->save_game_data_timer.async_wait([this](std::error_code ec) {
|
||||
if (!ec) {
|
||||
if (this->character(false)) {
|
||||
if (this->character_file(false)) {
|
||||
this->save_all();
|
||||
}
|
||||
this->reschedule_save_game_data_timer();
|
||||
@@ -336,7 +335,7 @@ shared_ptr<const TeamIndex::Team> Client::team() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto p = this->character(false);
|
||||
auto p = this->character_file(false);
|
||||
auto s = this->require_server_state();
|
||||
auto team = s->team_index->get_by_id(this->login->account->bb_team_id);
|
||||
if (!team) {
|
||||
@@ -384,7 +383,7 @@ bool Client::evaluate_quest_availability_expression(
|
||||
if (game && !game->quest_flag_values) {
|
||||
throw logic_error("quest flags are missing from game");
|
||||
}
|
||||
auto p = this->character();
|
||||
auto p = this->character_file();
|
||||
IntegralExpression::Env env = {
|
||||
.flags = &p->quest_flags.data.at(difficulty),
|
||||
.challenge_records = &p->challenge_records,
|
||||
@@ -411,7 +410,8 @@ bool Client::can_see_quest(
|
||||
if (!q->has_version_any_language(this->version())) {
|
||||
return false;
|
||||
}
|
||||
return this->evaluate_quest_availability_expression(q->available_expression, game, event, difficulty, num_players, v1_present);
|
||||
return this->evaluate_quest_availability_expression(
|
||||
q->meta.available_expression, game, event, difficulty, num_players, v1_present);
|
||||
}
|
||||
|
||||
bool Client::can_play_quest(
|
||||
@@ -424,10 +424,11 @@ bool Client::can_play_quest(
|
||||
if (!q->has_version_any_language(this->version())) {
|
||||
return false;
|
||||
}
|
||||
if (num_players > q->max_players) {
|
||||
if (num_players > q->meta.max_players) {
|
||||
return false;
|
||||
}
|
||||
return this->evaluate_quest_availability_expression(q->enabled_expression, game, event, difficulty, num_players, v1_present);
|
||||
return this->evaluate_quest_availability_expression(
|
||||
q->meta.enabled_expression, game, event, difficulty, num_players, v1_present);
|
||||
}
|
||||
|
||||
bool Client::can_use_chat_commands() const {
|
||||
@@ -448,8 +449,184 @@ void Client::set_login(shared_ptr<Login> login) {
|
||||
}
|
||||
}
|
||||
|
||||
// System file
|
||||
|
||||
string Client::system_filename(const string& bb_username) {
|
||||
return std::format("system/players/system_{}.psosys", bb_username);
|
||||
}
|
||||
|
||||
string Client::system_filename() const {
|
||||
if (this->version() != Version::BB_V4) {
|
||||
throw logic_error("non-BB players do not have system data");
|
||||
}
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return this->system_filename(this->login->bb_license->username);
|
||||
}
|
||||
|
||||
shared_ptr<PSOBBBaseSystemFile> Client::system_file(bool allow_load) {
|
||||
if (!this->system_data && allow_load) {
|
||||
this->load_all_files();
|
||||
}
|
||||
return this->system_data;
|
||||
}
|
||||
|
||||
shared_ptr<const PSOBBBaseSystemFile> Client::system_file(bool throw_if_missing) const {
|
||||
if (!this->system_data.get() && throw_if_missing) {
|
||||
throw runtime_error("system file is not loaded");
|
||||
}
|
||||
return this->system_data;
|
||||
}
|
||||
|
||||
void Client::save_system_file() const {
|
||||
if (!this->system_data) {
|
||||
throw logic_error("no system file loaded");
|
||||
}
|
||||
string filename = this->system_filename();
|
||||
phosg::save_object_file(filename, *this->system_data);
|
||||
this->log.info_f("Saved system file {}", filename);
|
||||
}
|
||||
|
||||
// Guild Card file
|
||||
|
||||
string Client::guild_card_filename(const string& bb_username) {
|
||||
return std::format("system/players/guild_cards_{}.psocard", bb_username);
|
||||
}
|
||||
|
||||
string Client::guild_card_filename() const {
|
||||
if (this->version() != Version::BB_V4) {
|
||||
throw logic_error("non-BB players do not have saved Guild Card files");
|
||||
}
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return this->guild_card_filename(this->login->bb_license->username);
|
||||
}
|
||||
|
||||
shared_ptr<PSOBBGuildCardFile> Client::guild_card_file(bool allow_load) {
|
||||
if (!this->guild_card_data && allow_load) {
|
||||
this->load_all_files();
|
||||
}
|
||||
return this->guild_card_data;
|
||||
}
|
||||
|
||||
shared_ptr<const PSOBBGuildCardFile> Client::guild_card_file(bool allow_load) const {
|
||||
if (!this->guild_card_data && allow_load) {
|
||||
throw runtime_error("account data is not loaded");
|
||||
}
|
||||
return this->guild_card_data;
|
||||
}
|
||||
|
||||
void Client::save_guild_card_file() const {
|
||||
if (!this->guild_card_data.get()) {
|
||||
throw logic_error("no Guild Card file loaded");
|
||||
}
|
||||
string filename = this->guild_card_filename();
|
||||
phosg::save_object_file(filename, *this->guild_card_data);
|
||||
this->log.info_f("Saved Guild Card file {}", filename);
|
||||
}
|
||||
|
||||
// Character file
|
||||
|
||||
string Client::character_filename(const std::string& bb_username, ssize_t index) {
|
||||
if (bb_username.empty()) {
|
||||
throw logic_error("non-BB players do not have saved character filenames");
|
||||
}
|
||||
if (index < 0) {
|
||||
throw logic_error("character index is not set");
|
||||
}
|
||||
return std::format("system/players/player_{}_{}.psochar", bb_username, index);
|
||||
}
|
||||
|
||||
string Client::backup_character_filename(uint32_t account_id, size_t index, bool is_ep3) {
|
||||
return std::format("system/players/backup_player_{}_{}.{}",
|
||||
account_id, index, is_ep3 ? "pso3char" : "psochar");
|
||||
}
|
||||
|
||||
string Client::character_filename() const {
|
||||
if (this->version() != Version::BB_V4) {
|
||||
throw logic_error("non-BB players do not have saved character filenames");
|
||||
}
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return this->character_filename(this->login->bb_license->username, this->bb_character_index);
|
||||
}
|
||||
|
||||
shared_ptr<PSOBBCharacterFile> Client::character_file(bool allow_load, bool allow_overlay) {
|
||||
if (this->overlay_character_data && allow_overlay) {
|
||||
return this->overlay_character_data;
|
||||
}
|
||||
if (!this->character_data && allow_load) {
|
||||
if ((this->version() == Version::BB_V4) && (this->bb_character_index < 0)) {
|
||||
throw runtime_error("character index not specified");
|
||||
}
|
||||
this->load_all_files();
|
||||
}
|
||||
return this->character_data;
|
||||
}
|
||||
|
||||
shared_ptr<const PSOBBCharacterFile> Client::character_file(bool throw_if_missing, bool allow_overlay) const {
|
||||
if (allow_overlay && this->overlay_character_data) {
|
||||
return this->overlay_character_data;
|
||||
}
|
||||
if (!this->character_data && throw_if_missing) {
|
||||
throw runtime_error("character data is not loaded");
|
||||
}
|
||||
return this->character_data;
|
||||
}
|
||||
|
||||
void Client::save_character_file(
|
||||
const string& filename,
|
||||
shared_ptr<const PSOBBBaseSystemFile> system,
|
||||
shared_ptr<const PSOBBCharacterFile> character) {
|
||||
PSOCHARFile::save(filename, system, character);
|
||||
}
|
||||
|
||||
void Client::save_ep3_character_file(
|
||||
const string& filename,
|
||||
const PSOGCEp3CharacterFile::Character& character) {
|
||||
phosg::save_file(filename, &character, sizeof(character));
|
||||
}
|
||||
|
||||
void Client::save_character_file() {
|
||||
if (!this->system_data.get()) {
|
||||
throw logic_error("no system file loaded");
|
||||
}
|
||||
if (!this->character_data.get()) {
|
||||
throw logic_error("no character file loaded");
|
||||
}
|
||||
if (this->should_update_play_time) {
|
||||
// This is slightly inaccurate, since fractions of a second are truncated
|
||||
// off each time we save. I'm lazy, so insert shrug emoji here.
|
||||
uint64_t t = phosg::now();
|
||||
uint64_t seconds = (t - this->last_play_time_update) / 1000000;
|
||||
this->character_data->play_time_seconds += seconds;
|
||||
this->log.info_f("Added {} seconds to play time", seconds);
|
||||
this->last_play_time_update = t;
|
||||
if (this->bank_data && (this->bb_bank_character_index == this->bb_character_index)) {
|
||||
this->character_data->bank = *this->bank_data;
|
||||
this->log.info_f("Committed bank data back to character file");
|
||||
}
|
||||
}
|
||||
|
||||
auto filename = this->character_filename();
|
||||
this->save_character_file(filename, this->system_data, this->character_data);
|
||||
this->log.info_f("Saved character file {}", filename);
|
||||
}
|
||||
|
||||
void Client::create_character_file(
|
||||
uint32_t guild_card_number,
|
||||
uint8_t language,
|
||||
const PlayerDispDataBBPreview& preview,
|
||||
shared_ptr<const LevelTable> level_table) {
|
||||
this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table);
|
||||
this->save_character_file();
|
||||
}
|
||||
|
||||
void Client::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_ptr<const LevelTable> level_table) {
|
||||
this->overlay_character_data = make_shared<PSOBBCharacterFile>(*this->character(true, false));
|
||||
this->overlay_character_data = make_shared<PSOBBCharacterFile>(*this->character_file(true, false));
|
||||
|
||||
if (rules->weapon_and_armor_mode != BattleRules::WeaponAndArmorMode::ALLOW) {
|
||||
this->overlay_character_data->inventory.remove_all_items_of_type(0);
|
||||
@@ -499,7 +676,7 @@ void Client::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_p
|
||||
}
|
||||
|
||||
void Client::create_challenge_overlay(Version version, size_t template_index, shared_ptr<const LevelTable> level_table) {
|
||||
auto p = this->character(true, false);
|
||||
auto p = this->character_file(true, false);
|
||||
const auto& tpl = get_challenge_template_definition(version, p->disp.visual.class_flags, template_index);
|
||||
|
||||
this->overlay_character_data = make_shared<PSOBBCharacterFile>(*p);
|
||||
@@ -543,124 +720,109 @@ void Client::create_challenge_overlay(Version version, size_t template_index, sh
|
||||
}
|
||||
}
|
||||
|
||||
void Client::import_blocked_senders(const parray<le_uint32_t, 30>& blocked_senders) {
|
||||
this->blocked_senders.clear();
|
||||
for (size_t z = 0; z < blocked_senders.size(); z++) {
|
||||
if (blocked_senders[z]) {
|
||||
this->blocked_senders.emplace(blocked_senders[z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bank file
|
||||
|
||||
shared_ptr<PSOBBBaseSystemFile> Client::system_file(bool allow_load) {
|
||||
if (!this->system_data && allow_load) {
|
||||
this->load_all_files();
|
||||
}
|
||||
return this->system_data;
|
||||
}
|
||||
|
||||
shared_ptr<const PSOBBBaseSystemFile> Client::system_file(bool allow_load) const {
|
||||
if (!this->system_data.get() && allow_load) {
|
||||
throw runtime_error("system data is not loaded");
|
||||
}
|
||||
return this->system_data;
|
||||
}
|
||||
|
||||
shared_ptr<PSOBBCharacterFile> Client::character(bool allow_load, bool allow_overlay) {
|
||||
if (this->overlay_character_data && allow_overlay) {
|
||||
return this->overlay_character_data;
|
||||
}
|
||||
if (!this->character_data && allow_load) {
|
||||
if ((this->version() == Version::BB_V4) && (this->bb_character_index < 0)) {
|
||||
throw runtime_error("character index not specified");
|
||||
}
|
||||
this->load_all_files();
|
||||
}
|
||||
return this->character_data;
|
||||
}
|
||||
|
||||
shared_ptr<const PSOBBCharacterFile> Client::character(bool allow_load, bool allow_overlay) const {
|
||||
if (allow_overlay && this->overlay_character_data) {
|
||||
return this->overlay_character_data;
|
||||
}
|
||||
if (!this->character_data && allow_load) {
|
||||
throw runtime_error("character data is not loaded");
|
||||
}
|
||||
return this->character_data;
|
||||
}
|
||||
|
||||
shared_ptr<PSOBBGuildCardFile> Client::guild_card_file(bool allow_load) {
|
||||
if (!this->guild_card_data && allow_load) {
|
||||
this->load_all_files();
|
||||
}
|
||||
return this->guild_card_data;
|
||||
}
|
||||
|
||||
shared_ptr<const PSOBBGuildCardFile> Client::guild_card_file(bool allow_load) const {
|
||||
if (!this->guild_card_data && allow_load) {
|
||||
throw runtime_error("account data is not loaded");
|
||||
}
|
||||
return this->guild_card_data;
|
||||
}
|
||||
|
||||
string Client::system_filename() const {
|
||||
if (this->version() != Version::BB_V4) {
|
||||
throw logic_error("non-BB players do not have system data");
|
||||
}
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return std::format("system/players/system_{}.psosys", this->login->bb_license->username);
|
||||
}
|
||||
|
||||
string Client::character_filename(const std::string& bb_username, ssize_t index) {
|
||||
string Client::bank_filename(const std::string& bb_username, ssize_t index) {
|
||||
if (bb_username.empty()) {
|
||||
throw logic_error("non-BB players do not have character data");
|
||||
throw logic_error("non-BB players do not have saved bank files");
|
||||
}
|
||||
if (index < 0) {
|
||||
throw logic_error("character index is not set");
|
||||
return std::format("system/players/shared_bank_{}.psobank", bb_username);
|
||||
} else {
|
||||
return std::format("system/players/player_{}_{}.psobank", bb_username, index);
|
||||
}
|
||||
return std::format("system/players/player_{}_{}.psochar", bb_username, index);
|
||||
}
|
||||
|
||||
string Client::backup_character_filename(uint32_t account_id, size_t index, bool is_ep3) {
|
||||
return std::format("system/players/backup_player_{}_{}.{}",
|
||||
account_id, index, is_ep3 ? "pso3char" : "psochar");
|
||||
}
|
||||
|
||||
string Client::character_filename(ssize_t index) const {
|
||||
string Client::bank_filename() const {
|
||||
if (this->version() != Version::BB_V4) {
|
||||
throw logic_error("non-BB players do not have character data");
|
||||
throw logic_error("non-BB players do not have saved bank filenames");
|
||||
}
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return this->character_filename(this->login->bb_license->username, (index < 0) ? this->bb_character_index : index);
|
||||
return this->bank_filename(this->login->bb_license->username, this->bb_bank_character_index);
|
||||
}
|
||||
|
||||
string Client::guild_card_filename() const {
|
||||
std::shared_ptr<PlayerBank> Client::bank_file(bool allow_load) {
|
||||
if (this->version() != Version::BB_V4) {
|
||||
throw logic_error("non-BB players do not have character data");
|
||||
throw logic_error("non-BB players do not have saved bank files");
|
||||
}
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
if (this->has_overlay()) {
|
||||
throw std::runtime_error("bank is inaccessible when overlay is present");
|
||||
}
|
||||
return std::format("system/players/guild_cards_{}.psocard", this->login->bb_license->username);
|
||||
if (!this->bank_data && allow_load) {
|
||||
try {
|
||||
// If there's a psobank file, load it and ignore the character file bank
|
||||
auto filename = this->bank_filename();
|
||||
auto f = phosg::fopen_unique(filename, "rb");
|
||||
this->bank_data = make_shared<PlayerBank>();
|
||||
this->bank_data->load(f.get());
|
||||
this->log.info_f("Loaded bank data from {}", filename);
|
||||
} catch (const phosg::cannot_open_file&) {
|
||||
// If there isn't a psobank file, use the loaded character data if the
|
||||
// bank character index matches the current character index (that is, we
|
||||
// should use the current character's bank); otherwise, load the
|
||||
// corresponding character and parse the bank from that character file
|
||||
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 {
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
string filename = this->character_filename(this->login->bb_license->username, this->bb_bank_character_index);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
auto s = this->require_server_state();
|
||||
this->bank_data->max_items = s->bb_max_bank_items;
|
||||
this->bank_data->max_meseta = s->bb_max_bank_meseta;
|
||||
}
|
||||
return this->bank_data;
|
||||
}
|
||||
|
||||
string Client::shared_bank_filename() const {
|
||||
if (this->version() != Version::BB_V4) {
|
||||
throw logic_error("non-BB players do not have character data");
|
||||
std::shared_ptr<const PlayerBank> Client::bank_file(bool throw_if_missing) const {
|
||||
if (!this->bank_data && throw_if_missing) {
|
||||
throw std::runtime_error("bank is not loaded");
|
||||
}
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
}
|
||||
return std::format("system/players/shared_bank_{}.psobank", this->login->bb_license->username);
|
||||
return this->bank_data;
|
||||
}
|
||||
|
||||
void Client::save_bank_file(const string& filename, const PlayerBank& bank) {
|
||||
auto f = phosg::fopen_unique(filename, "wb");
|
||||
bank.save(f.get());
|
||||
}
|
||||
|
||||
void Client::save_bank_file() const {
|
||||
if (!this->bank_data) {
|
||||
throw logic_error("no bank file loaded");
|
||||
}
|
||||
auto filename = this->bank_filename();
|
||||
this->save_bank_file(filename, *this->bank_data);
|
||||
this->log.info_f("Saved bank file {}", filename);
|
||||
}
|
||||
|
||||
void Client::change_bank(ssize_t index) {
|
||||
if (this->bank_data) {
|
||||
this->save_bank_file();
|
||||
this->bank_data.reset();
|
||||
if (this->bb_bank_character_index < 0) {
|
||||
this->log.info_f("Unloaded shared bank");
|
||||
} else {
|
||||
this->log.info_f("Unloaded bank from character {}", this->bb_bank_character_index);
|
||||
}
|
||||
}
|
||||
this->bb_bank_character_index = index;
|
||||
}
|
||||
|
||||
// Legacy files
|
||||
|
||||
string Client::legacy_account_filename() const {
|
||||
if (this->version() != Version::BB_V4) {
|
||||
throw logic_error("non-BB players do not have character data");
|
||||
throw logic_error("non-BB players do not have saved account data");
|
||||
}
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
@@ -670,7 +832,7 @@ string Client::legacy_account_filename() const {
|
||||
|
||||
string Client::legacy_player_filename() const {
|
||||
if (this->version() != Version::BB_V4) {
|
||||
throw logic_error("non-BB players do not have character data");
|
||||
throw logic_error("non-BB players do not have saved player files");
|
||||
}
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
throw logic_error("client is not logged in");
|
||||
@@ -684,13 +846,13 @@ string Client::legacy_player_filename() const {
|
||||
static_cast<ssize_t>(this->bb_character_index + 1));
|
||||
}
|
||||
|
||||
void Client::create_character_file(
|
||||
uint32_t guild_card_number,
|
||||
uint8_t language,
|
||||
const PlayerDispDataBBPreview& preview,
|
||||
shared_ptr<const LevelTable> level_table) {
|
||||
this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table);
|
||||
this->save_character_file();
|
||||
void Client::import_blocked_senders(const parray<le_uint32_t, 30>& blocked_senders) {
|
||||
this->blocked_senders.clear();
|
||||
for (size_t z = 0; z < blocked_senders.size(); z++) {
|
||||
if (blocked_senders[z]) {
|
||||
this->blocked_senders.emplace(blocked_senders[z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Client::load_all_files() {
|
||||
@@ -698,6 +860,7 @@ void Client::load_all_files() {
|
||||
this->system_data = make_shared<PSOBBBaseSystemFile>();
|
||||
this->character_data = make_shared<PSOBBCharacterFile>();
|
||||
this->guild_card_data = make_shared<PSOBBGuildCardFile>();
|
||||
this->bank_data = make_shared<PlayerBank>();
|
||||
return;
|
||||
}
|
||||
if (!this->login || !this->login->bb_license) {
|
||||
@@ -707,31 +870,22 @@ void Client::load_all_files() {
|
||||
this->system_data.reset();
|
||||
this->character_data.reset();
|
||||
this->guild_card_data.reset();
|
||||
|
||||
auto files_manager = this->require_server_state()->player_files_manager;
|
||||
this->bank_data.reset();
|
||||
|
||||
string sys_filename = this->system_filename();
|
||||
this->system_data = files_manager->get_system(sys_filename);
|
||||
if (this->system_data) {
|
||||
player_data_log.info_f("Using loaded system file {}", sys_filename);
|
||||
} else if (std::filesystem::is_regular_file(sys_filename)) {
|
||||
if (std::filesystem::is_regular_file(sys_filename)) {
|
||||
this->system_data = make_shared<PSOBBBaseSystemFile>(phosg::load_object_file<PSOBBBaseSystemFile>(sys_filename, true));
|
||||
files_manager->set_system(sys_filename, this->system_data);
|
||||
player_data_log.info_f("Loaded system data from {}", sys_filename);
|
||||
this->log.info_f("Loaded system data from {}", sys_filename);
|
||||
} else {
|
||||
player_data_log.info_f("System file is missing: {}", sys_filename);
|
||||
this->log.info_f("System file is missing: {}", sys_filename);
|
||||
}
|
||||
|
||||
if (this->bb_character_index >= 0) {
|
||||
string char_filename = this->character_filename();
|
||||
this->character_data = files_manager->get_character(char_filename);
|
||||
if (this->character_data) {
|
||||
player_data_log.info_f("Using loaded character file {}", char_filename);
|
||||
} else if (std::filesystem::is_regular_file(char_filename)) {
|
||||
if (std::filesystem::is_regular_file(char_filename)) {
|
||||
auto psochar = PSOCHARFile::load_shared(char_filename, !this->system_data);
|
||||
this->character_data = psochar.character_file;
|
||||
files_manager->set_character(char_filename, this->character_data);
|
||||
player_data_log.info_f("Loaded character data from {}", char_filename);
|
||||
this->log.info_f("Loaded character data from {}", char_filename);
|
||||
|
||||
// If there was no .psosys file, use the system file from the .psochar
|
||||
// file instead
|
||||
@@ -740,28 +894,23 @@ void Client::load_all_files() {
|
||||
throw logic_error("account system data not present, and also not loaded from psochar file");
|
||||
}
|
||||
this->system_data = psochar.system_file;
|
||||
files_manager->set_system(sys_filename, this->system_data);
|
||||
player_data_log.info_f("Loaded system data from {}", char_filename);
|
||||
this->log.info_f("Loaded system data from {}", char_filename);
|
||||
}
|
||||
|
||||
this->update_character_data_after_load(this->character_data);
|
||||
this->system_data->language = this->language();
|
||||
|
||||
} else {
|
||||
player_data_log.info_f("Character file is missing: {}", char_filename);
|
||||
this->log.info_f("Character file is missing: {}", char_filename);
|
||||
}
|
||||
}
|
||||
|
||||
string card_filename = this->guild_card_filename();
|
||||
this->guild_card_data = files_manager->get_guild_card(card_filename);
|
||||
if (this->guild_card_data) {
|
||||
player_data_log.info_f("Using loaded Guild Card file {}", card_filename);
|
||||
} else if (std::filesystem::is_regular_file(card_filename)) {
|
||||
if (std::filesystem::is_regular_file(card_filename)) {
|
||||
this->guild_card_data = make_shared<PSOBBGuildCardFile>(phosg::load_object_file<PSOBBGuildCardFile>(card_filename));
|
||||
files_manager->set_guild_card(card_filename, this->guild_card_data);
|
||||
player_data_log.info_f("Loaded Guild Card data from {}", card_filename);
|
||||
this->log.info_f("Loaded Guild Card data from {}", card_filename);
|
||||
} else {
|
||||
player_data_log.info_f("Guild Card file is missing: {}", card_filename);
|
||||
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
|
||||
@@ -775,13 +924,11 @@ void Client::load_all_files() {
|
||||
}
|
||||
if (!this->system_data) {
|
||||
this->system_data = make_shared<PSOBBBaseSystemFile>(nsa_data->system_file);
|
||||
files_manager->set_system(sys_filename, this->system_data);
|
||||
player_data_log.info_f("Loaded legacy system data from {}", nsa_filename);
|
||||
this->log.info_f("Loaded legacy system data from {}", nsa_filename);
|
||||
}
|
||||
if (!this->guild_card_data) {
|
||||
this->guild_card_data = make_shared<PSOBBGuildCardFile>(nsa_data->guild_card_file);
|
||||
files_manager->set_guild_card(card_filename, this->guild_card_data);
|
||||
player_data_log.info_f("Loaded legacy Guild Card data from {}", nsa_filename);
|
||||
this->log.info_f("Loaded legacy Guild Card data from {}", nsa_filename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,13 +941,11 @@ void Client::load_all_files() {
|
||||
if (s->bb_default_joystick_config) {
|
||||
this->system_data->joystick_config = *s->bb_default_joystick_config;
|
||||
}
|
||||
files_manager->set_system(sys_filename, this->system_data);
|
||||
player_data_log.info_f("Created new system data");
|
||||
this->log.info_f("Created new system data");
|
||||
}
|
||||
if (!this->guild_card_data) {
|
||||
this->guild_card_data = make_shared<PSOBBGuildCardFile>();
|
||||
files_manager->set_guild_card(card_filename, this->guild_card_data);
|
||||
player_data_log.info_f("Created new Guild Card data");
|
||||
this->log.info_f("Created new Guild Card data");
|
||||
}
|
||||
|
||||
if (!this->character_data && (this->bb_character_index >= 0)) {
|
||||
@@ -817,7 +962,6 @@ void Client::load_all_files() {
|
||||
}
|
||||
|
||||
this->character_data = make_shared<PSOBBCharacterFile>();
|
||||
files_manager->set_character(this->character_filename(), this->character_data);
|
||||
this->character_data->inventory = nsc_data.inventory;
|
||||
this->character_data->disp = nsc_data.disp;
|
||||
this->character_data->play_time_seconds = 0;
|
||||
@@ -841,14 +985,22 @@ void Client::load_all_files() {
|
||||
this->character_data->option_flags = nsa_data->option_flags;
|
||||
this->character_data->symbol_chats = nsa_data->symbol_chats;
|
||||
this->character_data->shortcuts = nsa_data->shortcuts;
|
||||
player_data_log.info_f("Loaded legacy player data from {} and {}", nsa_filename, nsc_filename);
|
||||
this->log.info_f("Loaded legacy player data from {} and {}", nsa_filename, nsc_filename);
|
||||
} else {
|
||||
player_data_log.info_f("Loaded legacy player data from {}", nsc_filename);
|
||||
this->log.info_f("Loaded legacy player data from {}", nsc_filename);
|
||||
}
|
||||
this->update_character_data_after_load(this->character_data);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -860,11 +1012,7 @@ void Client::load_all_files() {
|
||||
// Clear legacy play_time field
|
||||
this->character_data->disp.name.clear_after_bytes(0x18);
|
||||
|
||||
// Enforce item stack limits, in case they've changed
|
||||
auto s = this->require_server_state();
|
||||
auto stack_limits = s->item_stack_limits(this->version());
|
||||
this->character_data->inventory.enforce_stack_limits(stack_limits);
|
||||
this->character_data->bank.enforce_stack_limits(stack_limits);
|
||||
|
||||
this->login->account->auto_reply_message = this->character_data->auto_reply.decode();
|
||||
this->login->account->save();
|
||||
@@ -876,7 +1024,7 @@ void Client::update_character_data_after_load(shared_ptr<PSOBBCharacterFile> cha
|
||||
charfile->import_tethealla_material_usage(this->require_server_state()->level_table(this->version()));
|
||||
|
||||
uint8_t lang = this->language();
|
||||
player_data_log.info_f("Overriding language fields in save files with {:02X} ({})", lang, char_for_language_code(lang));
|
||||
this->log.info_f("Overriding language fields in save files with {:02X} ({})", lang, char_for_language_code(lang));
|
||||
charfile->inventory.language = lang;
|
||||
charfile->guild_card.language = lang;
|
||||
}
|
||||
@@ -891,70 +1039,9 @@ void Client::save_all() {
|
||||
if (this->guild_card_data) {
|
||||
this->save_guild_card_file();
|
||||
}
|
||||
if (this->external_bank) {
|
||||
string filename = this->shared_bank_filename();
|
||||
phosg::save_object_file<PlayerBank200>(filename, *this->external_bank);
|
||||
player_data_log.info_f("Saved shared bank file {}", filename);
|
||||
if (this->bank_data) {
|
||||
this->save_bank_file();
|
||||
}
|
||||
if (this->external_bank_character) {
|
||||
this->save_character_file(
|
||||
this->character_filename(this->external_bank_character_index),
|
||||
this->system_data,
|
||||
this->external_bank_character);
|
||||
}
|
||||
}
|
||||
|
||||
void Client::save_system_file() const {
|
||||
if (!this->system_data) {
|
||||
throw logic_error("no system file loaded");
|
||||
}
|
||||
string filename = this->system_filename();
|
||||
phosg::save_object_file(filename, *this->system_data);
|
||||
player_data_log.info_f("Saved system file {}", filename);
|
||||
}
|
||||
|
||||
void Client::save_character_file(
|
||||
const string& filename,
|
||||
shared_ptr<const PSOBBBaseSystemFile> system,
|
||||
shared_ptr<const PSOBBCharacterFile> character) {
|
||||
PSOCHARFile::save(filename, system, character);
|
||||
player_data_log.info_f("Saved character file {}", filename);
|
||||
}
|
||||
|
||||
void Client::save_ep3_character_file(
|
||||
const string& filename,
|
||||
const PSOGCEp3CharacterFile::Character& character) {
|
||||
phosg::save_file(filename, &character, sizeof(character));
|
||||
player_data_log.info_f("Saved Episode 3 character file {}", filename);
|
||||
}
|
||||
|
||||
void Client::save_character_file() {
|
||||
if (!this->system_data.get()) {
|
||||
throw logic_error("no system file loaded");
|
||||
}
|
||||
if (!this->character_data.get()) {
|
||||
throw logic_error("no character file loaded");
|
||||
}
|
||||
if (this->should_update_play_time) {
|
||||
// This is slightly inaccurate, since fractions of a second are truncated
|
||||
// off each time we save. I'm lazy, so insert shrug emoji here.
|
||||
uint64_t t = phosg::now();
|
||||
uint64_t seconds = (t - this->last_play_time_update) / 1000000;
|
||||
this->character_data->play_time_seconds += seconds;
|
||||
player_data_log.info_f("Added {} seconds to play time", seconds);
|
||||
this->last_play_time_update = t;
|
||||
}
|
||||
|
||||
this->save_character_file(this->character_filename(), this->system_data, this->character_data);
|
||||
}
|
||||
|
||||
void Client::save_guild_card_file() const {
|
||||
if (!this->guild_card_data.get()) {
|
||||
throw logic_error("no Guild Card file loaded");
|
||||
}
|
||||
string filename = this->guild_card_filename();
|
||||
phosg::save_object_file(filename, *this->guild_card_data);
|
||||
player_data_log.info_f("Saved Guild Card file {}", filename);
|
||||
}
|
||||
|
||||
void Client::load_backup_character(uint32_t account_id, size_t index) {
|
||||
@@ -979,88 +1066,17 @@ void Client::save_and_unload_character() {
|
||||
this->save_character_file();
|
||||
this->character_data.reset();
|
||||
this->log.info_f("Unloaded character");
|
||||
}
|
||||
}
|
||||
|
||||
PlayerBank200& Client::current_bank() {
|
||||
if (this->external_bank) {
|
||||
return *this->external_bank;
|
||||
} else if (this->external_bank_character) {
|
||||
return this->external_bank_character->bank;
|
||||
}
|
||||
return this->character()->bank;
|
||||
}
|
||||
|
||||
const PlayerBank200& Client::current_bank() const {
|
||||
return const_cast<Client*>(this)->current_bank();
|
||||
}
|
||||
|
||||
std::shared_ptr<PSOBBCharacterFile> Client::current_bank_character() {
|
||||
return this->external_bank_character ? this->external_bank_character : this->character();
|
||||
}
|
||||
|
||||
void Client::use_default_bank() {
|
||||
if (this->external_bank) {
|
||||
string filename = this->shared_bank_filename();
|
||||
phosg::save_object_file<PlayerBank200>(filename, *this->external_bank);
|
||||
this->external_bank.reset();
|
||||
player_data_log.info_f("Detached shared bank {}", filename);
|
||||
}
|
||||
if (this->external_bank_character) {
|
||||
string filename = this->character_filename(this->external_bank_character_index);
|
||||
this->save_character_file(filename, this->system_data, this->external_bank_character);
|
||||
this->external_bank_character.reset();
|
||||
player_data_log.info_f("Detached character {} from bank", filename);
|
||||
}
|
||||
}
|
||||
|
||||
bool Client::use_shared_bank() {
|
||||
this->use_default_bank();
|
||||
|
||||
string filename = this->shared_bank_filename();
|
||||
auto files_manager = this->require_server_state()->player_files_manager;
|
||||
this->external_bank = files_manager->get_bank(filename);
|
||||
if (this->external_bank) {
|
||||
player_data_log.info_f("Using loaded shared bank {}", filename);
|
||||
return true;
|
||||
} else if (std::filesystem::is_regular_file(filename)) {
|
||||
this->external_bank = make_shared<PlayerBank200>(phosg::load_object_file<PlayerBank200>(filename));
|
||||
files_manager->set_bank(filename, this->external_bank);
|
||||
player_data_log.info_f("Loaded shared bank {}", filename);
|
||||
return true;
|
||||
} else {
|
||||
this->external_bank = make_shared<PlayerBank200>();
|
||||
files_manager->set_bank(filename, this->external_bank);
|
||||
player_data_log.info_f("Created shared bank for {}", filename);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Client::use_character_bank(ssize_t index) {
|
||||
this->use_default_bank();
|
||||
if (index != this->bb_character_index) {
|
||||
auto files_manager = this->require_server_state()->player_files_manager;
|
||||
|
||||
string filename = this->character_filename(index);
|
||||
this->external_bank_character = files_manager->get_character(filename);
|
||||
if (this->external_bank_character) {
|
||||
this->external_bank_character_index = index;
|
||||
player_data_log.info_f("Using loaded character file {} for external bank", filename);
|
||||
} else if (std::filesystem::is_regular_file(filename)) {
|
||||
this->external_bank_character = PSOCHARFile::load_shared(filename, false).character_file;
|
||||
this->update_character_data_after_load(this->external_bank_character);
|
||||
this->external_bank_character_index = index;
|
||||
files_manager->set_character(filename, this->external_bank_character);
|
||||
player_data_log.info_f("Loaded character data from {} for external bank", filename);
|
||||
} else {
|
||||
throw runtime_error("character does not exist");
|
||||
if (this->bank_data) {
|
||||
this->save_bank_file();
|
||||
this->bank_data.reset();
|
||||
this->log.info_f("Unloaded bank");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Client::print_inventory() const {
|
||||
auto s = this->require_server_state();
|
||||
auto p = this->character();
|
||||
auto p = this->character_file();
|
||||
this->log.info_f("[PlayerInventory] Meseta: {}", p->disp.stats.meseta);
|
||||
this->log.info_f("[PlayerInventory] {} items", p->inventory.num_items);
|
||||
for (size_t x = 0; x < p->inventory.num_items; x++) {
|
||||
@@ -1072,16 +1088,19 @@ void Client::print_inventory() const {
|
||||
}
|
||||
|
||||
void Client::print_bank() const {
|
||||
auto s = this->require_server_state();
|
||||
auto bank = this->current_bank();
|
||||
this->log.info_f("[PlayerBank] Meseta: {}", bank.meseta);
|
||||
this->log.info_f("[PlayerBank] {} items", bank.num_items);
|
||||
for (size_t x = 0; x < bank.num_items; x++) {
|
||||
const auto& item = bank.items[x];
|
||||
const char* present_token = item.present ? "" : " (missing present flag)";
|
||||
auto hex = item.data.hex();
|
||||
auto name = s->describe_item(this->version(), item.data);
|
||||
this->log.info_f("[PlayerBank] {:3}: {} ({}) (x{}){}", x, hex, name, item.amount, present_token);
|
||||
if (this->bank_data) {
|
||||
auto s = this->require_server_state();
|
||||
this->log.info_f("[PlayerBank] Meseta: {}", this->bank_data->meseta);
|
||||
this->log.info_f("[PlayerBank] {} items", this->bank_data->items.size());
|
||||
for (size_t x = 0; x < this->bank_data->items.size(); x++) {
|
||||
const auto& item = this->bank_data->items[x];
|
||||
const char* present_token = item.present ? "" : " (missing present flag)";
|
||||
auto hex = item.data.hex();
|
||||
auto name = s->describe_item(this->version(), item.data);
|
||||
this->log.info_f("[PlayerBank] {:3}: {} ({}) (x{}){}", x, hex, name, item.amount, present_token);
|
||||
}
|
||||
} else {
|
||||
this->log.info_f("[PlayerBank] Bank data not loaded");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-43
@@ -114,6 +114,7 @@ public:
|
||||
uint8_t bb_client_code = 0;
|
||||
uint8_t bb_connection_phase = 0xFF;
|
||||
ssize_t bb_character_index = -1; // -1 = not set
|
||||
ssize_t bb_bank_character_index = -1; // -1 = shared bank
|
||||
uint32_t bb_security_token = 0;
|
||||
parray<uint8_t, 0x28> bb_client_config;
|
||||
std::string login_character_name;
|
||||
@@ -288,6 +289,36 @@ public:
|
||||
|
||||
void set_login(std::shared_ptr<Login> login);
|
||||
|
||||
void import_blocked_senders(const parray<le_uint32_t, 30>& blocked_senders);
|
||||
|
||||
static std::string system_filename(const std::string& bb_username);
|
||||
std::string system_filename() const;
|
||||
std::shared_ptr<PSOBBBaseSystemFile> system_file(bool allow_load = true);
|
||||
std::shared_ptr<const PSOBBBaseSystemFile> system_file(bool throw_if_missing = true) const;
|
||||
void save_system_file() const;
|
||||
|
||||
static std::string guild_card_filename(const std::string& bb_username);
|
||||
std::string guild_card_filename() const;
|
||||
std::shared_ptr<PSOBBGuildCardFile> guild_card_file(bool allow_load = true);
|
||||
std::shared_ptr<const PSOBBGuildCardFile> guild_card_file(bool allow_load = true) const;
|
||||
void save_guild_card_file() const;
|
||||
|
||||
static std::string character_filename(const std::string& bb_username, ssize_t index);
|
||||
static std::string backup_character_filename(uint32_t account_id, size_t index, bool is_ep3);
|
||||
std::string character_filename() const;
|
||||
std::shared_ptr<PSOBBCharacterFile> character_file(bool allow_load = true, bool allow_overlay = true);
|
||||
std::shared_ptr<const PSOBBCharacterFile> character_file(bool throw_if_missing = true, bool allow_overlay = true) const;
|
||||
static void save_character_file(
|
||||
const std::string& filename,
|
||||
std::shared_ptr<const PSOBBBaseSystemFile> sys,
|
||||
std::shared_ptr<const PSOBBCharacterFile> character);
|
||||
static void save_ep3_character_file(const std::string& filename, const PSOGCEp3CharacterFile::Character& character);
|
||||
void save_character_file();
|
||||
void create_character_file(
|
||||
uint32_t guild_card_number,
|
||||
uint8_t 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);
|
||||
void create_challenge_overlay(Version version, size_t template_index, std::shared_ptr<const LevelTable> level_table);
|
||||
inline void delete_overlay() {
|
||||
@@ -297,55 +328,23 @@ public:
|
||||
return this->overlay_character_data.get() != nullptr;
|
||||
}
|
||||
|
||||
void import_blocked_senders(const parray<le_uint32_t, 30>& blocked_senders);
|
||||
static std::string bank_filename(const std::string& bb_username, ssize_t index);
|
||||
std::string bank_filename() const;
|
||||
std::shared_ptr<PlayerBank> bank_file(bool allow_load = true);
|
||||
std::shared_ptr<const PlayerBank> bank_file(bool throw_if_missing = true) const;
|
||||
static void save_bank_file(const std::string& filename, const PlayerBank& bank);
|
||||
void save_bank_file() const;
|
||||
void change_bank(ssize_t bb_character_index); // -1 = use shared bank
|
||||
|
||||
std::shared_ptr<PSOBBBaseSystemFile> system_file(bool allow_load = true);
|
||||
std::shared_ptr<PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true);
|
||||
std::shared_ptr<PSOBBGuildCardFile> guild_card_file(bool allow_load = true);
|
||||
std::shared_ptr<const PSOBBBaseSystemFile> system_file(bool allow_load = true) const;
|
||||
std::shared_ptr<const PSOBBCharacterFile> character(bool allow_load = true, bool allow_overlay = true) const;
|
||||
std::shared_ptr<const PSOBBGuildCardFile> guild_card_file(bool allow_load = true) const;
|
||||
|
||||
void create_character_file(
|
||||
uint32_t guild_card_number,
|
||||
uint8_t language,
|
||||
const PlayerDispDataBBPreview& preview,
|
||||
std::shared_ptr<const LevelTable> level_table);
|
||||
|
||||
std::string system_filename() const;
|
||||
static std::string character_filename(const std::string& bb_username, ssize_t index);
|
||||
static std::string backup_character_filename(uint32_t account_id, size_t index, bool is_ep3);
|
||||
std::string character_filename(ssize_t index = -1) const;
|
||||
std::string guild_card_filename() const;
|
||||
std::string shared_bank_filename() const;
|
||||
|
||||
std::string legacy_player_filename() const;
|
||||
std::string legacy_account_filename() const;
|
||||
std::string legacy_player_filename() const;
|
||||
|
||||
void save_all();
|
||||
void save_system_file() const;
|
||||
static void save_character_file(
|
||||
const std::string& filename,
|
||||
std::shared_ptr<const PSOBBBaseSystemFile> sys,
|
||||
std::shared_ptr<const PSOBBCharacterFile> character);
|
||||
static void save_ep3_character_file(
|
||||
const std::string& filename,
|
||||
const PSOGCEp3CharacterFile::Character& character);
|
||||
// Note: This function is not const because it updates the player's play time.
|
||||
void save_character_file();
|
||||
void save_guild_card_file() const;
|
||||
|
||||
void load_backup_character(uint32_t 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();
|
||||
|
||||
PlayerBank200& current_bank();
|
||||
const PlayerBank200& current_bank() const;
|
||||
std::shared_ptr<PSOBBCharacterFile> current_bank_character();
|
||||
bool use_shared_bank(); // Returns true if the bank exists; false if it was created
|
||||
void use_character_bank(ssize_t bb_character_index);
|
||||
void use_default_bank();
|
||||
|
||||
void print_inventory() const;
|
||||
void print_bank() const;
|
||||
|
||||
@@ -359,9 +358,7 @@ private:
|
||||
std::shared_ptr<PSOBBCharacterFile> overlay_character_data;
|
||||
std::shared_ptr<PSOBBCharacterFile> character_data;
|
||||
std::shared_ptr<PSOBBGuildCardFile> guild_card_data;
|
||||
std::shared_ptr<PlayerBank200> external_bank;
|
||||
std::shared_ptr<PSOBBCharacterFile> external_bank_character;
|
||||
ssize_t external_bank_character_index = -1;
|
||||
std::shared_ptr<PlayerBank> bank_data;
|
||||
uint64_t last_play_time_update = 0;
|
||||
|
||||
void load_all_files();
|
||||
|
||||
+256
-149
@@ -344,7 +344,6 @@ struct S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B {
|
||||
|
||||
// 03 (C->S): Legacy register (non-BB)
|
||||
// Internal name: SndRegist
|
||||
// TODO: Are the DCv1 and DCv2 formats the same as this structure?
|
||||
|
||||
struct C_LegacyLogin_PC_V3_03 {
|
||||
/* 00 */ be_uint64_t hardware_id;
|
||||
@@ -400,7 +399,6 @@ struct S_ServerInitWithAfterMessageT_BB_03_9B {
|
||||
// now-unused sequence. Like 03, this command isn't used by any known PSO
|
||||
// version.
|
||||
// header.flag is 1 if the client has UDP disabled.
|
||||
// TODO: Are the DCv1 and DCv2 formats the same as this structure?
|
||||
|
||||
struct C_LegacyLogin_PC_V3_04 {
|
||||
/* 00 */ be_uint64_t hardware_id;
|
||||
@@ -435,9 +433,9 @@ struct C_LegacyLogin_BB_04 {
|
||||
// 05 = Server down for maintenance (108)
|
||||
// 06 = Incorrect password (127)
|
||||
// Any other nonzero value = Generic failure (101)
|
||||
// The client config field in this command is ignored by pre-V3 clients as well
|
||||
// as Episodes 1&2 Trial Edition. All other V3 clients save it as opaque data to
|
||||
// be returned in a 9E or 9F command later.
|
||||
// The client config field in this command is ignored by all clients that never
|
||||
// send 9E. Clients that do send 9E will save thie client config as opaque data
|
||||
// to be returned in a 9E or 9F command later.
|
||||
// The client will respond with a 96 command, but only the first time it
|
||||
// receives this command - for later 04 commands, the client will still update
|
||||
// its client config but will not respond. Changing the security data at any
|
||||
@@ -676,49 +674,59 @@ struct S_LegacyJoinGame_XB_0E {
|
||||
|
||||
// 10 (C->S): Menu selection
|
||||
// Internal name: SndAction
|
||||
// header.flag contains two flags: 02 specifies if a password is present, and 01
|
||||
// specifies... something else. These two bits directly correspond to the two
|
||||
// lowest bits in the flags field of the game menu: 02 specifies that the game
|
||||
// is locked, but the function of 01 is unknown.
|
||||
// Annoyingly, the no-arguments form of the command can have any flag value, so
|
||||
// it doesn't suffice to check the flag value to know which format is being
|
||||
// used!
|
||||
// header.flag has different meanings depending on which menu is selected.
|
||||
// For most menus, header.flag is a bit field containing two flags: 02
|
||||
// specifies if a password is present, and 01 specifies if a name is present.
|
||||
// (If both are set, the name comes first, as described below). These two bits
|
||||
// directly correspond to the two lowest bits in the flags field of the game
|
||||
// menu: 02 specifies that the game is locked, but the function of 01 is
|
||||
// unknown. The ability to send a name along with a menu choice is unused in
|
||||
// all client versions except Episode 3, where it's used in the tournament
|
||||
// entries menu. It's not clear why all other versions have the ability send a
|
||||
// name here - it may be a relic from very early development.
|
||||
// For the quest categories menu, header.flag specifies the player's
|
||||
// progression through the story. The values are:
|
||||
// 0 = has not yet defeated Dragon
|
||||
// 1 = has defeated Dragon but not De Rol Le
|
||||
// 2 = has defeated De Rol Le but not Vol Opt
|
||||
// 3 = has defeated Vol Opt but not Dark Falz
|
||||
// 4 = has defeated Dark Falz
|
||||
// For the challenge categories menu, header.flag specifies something related
|
||||
// to challenge stage completion (TODO: reverse-engineer function at
|
||||
// 59NL:004DA300 to see what this is)
|
||||
|
||||
struct C_MenuSelection_10_Flag00 {
|
||||
struct C_MenuSelectionBase_10 {
|
||||
le_uint32_t menu_id = 0;
|
||||
le_uint32_t item_id = 0;
|
||||
} __packed_ws__(C_MenuSelection_10_Flag00, 8);
|
||||
} __packed_ws__(C_MenuSelectionBase_10, 8);
|
||||
|
||||
template <TextEncoding Encoding>
|
||||
struct C_MenuSelectionT_10_Flag01 {
|
||||
C_MenuSelection_10_Flag00 basic_cmd;
|
||||
struct C_MenuSelectionWithNameT_10 : C_MenuSelectionBase_10 {
|
||||
pstring<Encoding, 0x10> name;
|
||||
} __attribute__((packed));
|
||||
using C_MenuSelection_DC_V3_10_Flag01 = C_MenuSelectionT_10_Flag01<TextEncoding::MARKED>;
|
||||
using C_MenuSelection_PC_BB_10_Flag01 = C_MenuSelectionT_10_Flag01<TextEncoding::UTF16>;
|
||||
check_struct_size(C_MenuSelection_DC_V3_10_Flag01, 0x18);
|
||||
check_struct_size(C_MenuSelection_PC_BB_10_Flag01, 0x28);
|
||||
using C_MenuSelectionWithName_DC_V3_10 = C_MenuSelectionWithNameT_10<TextEncoding::MARKED>;
|
||||
using C_MenuSelectionWithName_PC_BB_10 = C_MenuSelectionWithNameT_10<TextEncoding::UTF16>;
|
||||
check_struct_size(C_MenuSelectionWithName_DC_V3_10, 0x18);
|
||||
check_struct_size(C_MenuSelectionWithName_PC_BB_10, 0x28);
|
||||
|
||||
template <TextEncoding Encoding>
|
||||
struct C_MenuSelectionT_10_Flag02 {
|
||||
C_MenuSelection_10_Flag00 basic_cmd;
|
||||
struct C_MenuSelectionWithPasswordT_10 : C_MenuSelectionBase_10 {
|
||||
pstring<Encoding, 0x10> password;
|
||||
} __attribute__((packed));
|
||||
using C_MenuSelection_DC_V3_10_Flag02 = C_MenuSelectionT_10_Flag02<TextEncoding::MARKED>;
|
||||
using C_MenuSelection_PC_BB_10_Flag02 = C_MenuSelectionT_10_Flag02<TextEncoding::UTF16>;
|
||||
check_struct_size(C_MenuSelection_DC_V3_10_Flag02, 0x18);
|
||||
check_struct_size(C_MenuSelection_PC_BB_10_Flag02, 0x28);
|
||||
using C_MenuSelectionWithPassword_DC_V3_10 = C_MenuSelectionWithPasswordT_10<TextEncoding::MARKED>;
|
||||
using C_MenuSelectionWithPassword_PC_BB_10 = C_MenuSelectionWithPasswordT_10<TextEncoding::UTF16>;
|
||||
check_struct_size(C_MenuSelectionWithPassword_DC_V3_10, 0x18);
|
||||
check_struct_size(C_MenuSelectionWithPassword_PC_BB_10, 0x28);
|
||||
|
||||
template <TextEncoding Encoding>
|
||||
struct C_MenuSelectionT_10_Flag03 {
|
||||
C_MenuSelection_10_Flag00 basic_cmd;
|
||||
struct C_MenuSelectionWithNameAndPasswordT_10 : C_MenuSelectionBase_10 {
|
||||
pstring<Encoding, 0x10> name;
|
||||
pstring<Encoding, 0x10> password;
|
||||
} __attribute__((packed));
|
||||
using C_MenuSelection_DC_V3_10_Flag03 = C_MenuSelectionT_10_Flag03<TextEncoding::MARKED>;
|
||||
using C_MenuSelection_PC_BB_10_Flag03 = C_MenuSelectionT_10_Flag03<TextEncoding::UTF16>;
|
||||
check_struct_size(C_MenuSelection_DC_V3_10_Flag03, 0x28);
|
||||
check_struct_size(C_MenuSelection_PC_BB_10_Flag03, 0x48);
|
||||
using C_MenuSelectionWithNameAndPassword_DC_V3_10 = C_MenuSelectionWithNameAndPasswordT_10<TextEncoding::MARKED>;
|
||||
using C_MenuSelectionWithNameAndPassword_PC_BB_10 = C_MenuSelectionWithNameAndPasswordT_10<TextEncoding::UTF16>;
|
||||
check_struct_size(C_MenuSelectionWithNameAndPassword_DC_V3_10, 0x28);
|
||||
check_struct_size(C_MenuSelectionWithNameAndPassword_PC_BB_10, 0x48);
|
||||
|
||||
// 11 (S->C): Ship info
|
||||
// Internal name: RcvMessage
|
||||
@@ -848,7 +856,8 @@ struct S_ReconnectSplit_19 {
|
||||
// the chat log window contents will appear in the message box, prepended to
|
||||
// the message text from the command.
|
||||
// The maximum length of the message is 0x400 bytes. This is the only
|
||||
// difference between this command and the D5 command.
|
||||
// difference between this command and the D5 command (except on BB - see the
|
||||
// notes on D5 for more information).
|
||||
|
||||
// 1B (S->C): Valid but ignored (all versions)
|
||||
// Internal name: RcvBattleData
|
||||
@@ -1367,9 +1376,6 @@ struct LobbyFlags {
|
||||
// DCv2 and later, the game mode option is always present.
|
||||
uint8_t enable_battle_mode_v1 = 1;
|
||||
uint8_t event = 0;
|
||||
// TODO: This is a partially-informed, but untested, guess based on
|
||||
// disassembly of the Xbox client. It'd be nice if someone let me know if
|
||||
// this is correct or not
|
||||
uint8_t xb_enable_voice_chat = 1;
|
||||
le_uint32_t random_seed = 0; // Unused for lobbies
|
||||
} __packed_ws__(LobbyFlags, 0x0C);
|
||||
@@ -1742,7 +1748,7 @@ struct C_RegisterV1_DC_92 {
|
||||
be_uint64_t hardware_id;
|
||||
le_uint32_t sub_version;
|
||||
uint8_t unused1 = 0;
|
||||
uint8_t language = 0; // TODO: This is a guess; verify it
|
||||
uint8_t language = 0;
|
||||
parray<uint8_t, 2> unused2;
|
||||
pstring<TextEncoding::ASCII, 0x30> serial_number2;
|
||||
pstring<TextEncoding::ASCII, 0x30> access_key2;
|
||||
@@ -2376,8 +2382,9 @@ struct S_RankUpdate_Ep3_B7 {
|
||||
// No arguments
|
||||
// The client sends this after it receives a B8 from the server.
|
||||
|
||||
// B8 (C->S): Unknown (BB)
|
||||
// The client accepts this command, but ignores it.
|
||||
// B8 (C->S): Valid but ignored (BB)
|
||||
// The client accepts this command, but ignores it. It may have had some
|
||||
// later-removed purpose during BB's development.
|
||||
|
||||
// B9 (S->C): Update CARD lobby media (Episode 3)
|
||||
// This command is not valid on Episode 3 Trial Edition.
|
||||
@@ -2704,26 +2711,27 @@ struct S_ConfirmTournamentEntry_Ep3_CC {
|
||||
// CF: Invalid command
|
||||
|
||||
// D0 (C->S): Start trade sequence (V3/BB)
|
||||
// The trade window sequence is a bit complicated. The normal flow is:
|
||||
// The trade window sequence is a bit complicated. On pre-BB versions, the
|
||||
// normal flow is:
|
||||
// - Clients sync trade state with 6xA6 commands
|
||||
// - When both have confirmed, one client (the initiator) sends a D0
|
||||
// - Server sends a D1 to the non-initiator
|
||||
// - Non-initiator sends a D0
|
||||
// - Server sends a D1 to both clients
|
||||
// - Both clients delete the sent items from their inventories (and send the
|
||||
// appropriate subcommand)
|
||||
// - Both clients send a D2 (similarly to how AC works, the server should not
|
||||
// proceed until both D2s are received)
|
||||
// - Server sends a D3 to both clients with each other's data from their D0s,
|
||||
// followed immediately by a D4 01 to both clients, which completes the trade
|
||||
// - When both have confirmed, one client (the initiator) sends D0
|
||||
// - The server sends D1 to the other client (the responder)
|
||||
// - The responder sends D0
|
||||
// - The server sends D1 to both clients
|
||||
// - Both clients delete the sent items from their inventories and send the
|
||||
// appropriate subcommand (6x29)
|
||||
// - Both clients send D2; similarly to how AC works, the server doesn't
|
||||
// proceed until both D2 commands are received
|
||||
// - The server sends D3 to both clients with each other's data from their D0
|
||||
// commands, followed immediately by D4 01 to both clients, which completes
|
||||
// the trade
|
||||
// - Both clients send the appropriate subcommand to create inventory items
|
||||
// TODO: On BB, is the server responsible for sending the appropriate item
|
||||
// delete/create subcommands?
|
||||
// On BB, the flow is similar, except after both D2 commands are received, the
|
||||
// server instead handles the rest of the process - it sends 6x29 commands to
|
||||
// delete the inventory items and 6xBE to create the traded items.
|
||||
// At any point if an error occurs, either client may send a D4 00, which
|
||||
// cancels the entire sequence. The server should then send D4 00 to both
|
||||
// clients.
|
||||
// TODO: The server should presumably also send a D4 00 if either client
|
||||
// disconnects during the sequence.
|
||||
|
||||
struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server
|
||||
le_uint16_t target_client_id = 0;
|
||||
@@ -2758,7 +2766,10 @@ struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server
|
||||
// See D0 description for usage information.
|
||||
|
||||
// D5: Large message box (V3/BB)
|
||||
// Same as 1A command, except the maximum length of the message is 0x1000 bytes.
|
||||
// Same as 1A command, except the maximum length of the message is 0x1000
|
||||
// bytes. On BB, this command is not valid during the data server phase
|
||||
// (whereas 1A is valid there). The BB client ignores all D5 commands after the
|
||||
// first one sent in each connection; this logic does not apply to 1A.
|
||||
|
||||
// D6 (C->S): Large message box closed (V3)
|
||||
// No arguments
|
||||
@@ -2949,7 +2960,11 @@ struct S_TournamentList_Ep3NTE_E0 {
|
||||
le_uint16_t max_teams = 0;
|
||||
} __packed_ws__(Entry, 0x34);
|
||||
parray<Entry, 0x20> entries;
|
||||
} __packed_ws__(S_TournamentList_Ep3NTE_E0, 0x680);
|
||||
uint8_t unknown_a1 = 0;
|
||||
uint8_t unknown_a2 = 0;
|
||||
uint8_t unknown_a3 = 0;
|
||||
uint8_t unknown_a4 = 0;
|
||||
} __packed_ws__(S_TournamentList_Ep3NTE_E0, 0x684);
|
||||
|
||||
struct S_TournamentList_Ep3_E0 {
|
||||
struct Entry {
|
||||
@@ -2983,7 +2998,13 @@ struct S_TournamentList_Ep3_E0 {
|
||||
le_uint16_t unknown_a4 = 0xFFFF;
|
||||
} __packed_ws__(Entry, 0x38);
|
||||
parray<Entry, 0x20> entries;
|
||||
} __packed_ws__(S_TournamentList_Ep3_E0, 0x700);
|
||||
// These fields exist in the command (the copy constructor copies them over)
|
||||
// but it seems they aren't used by the client at all.
|
||||
uint8_t unknown_a1 = 0;
|
||||
uint8_t unknown_a2 = 0;
|
||||
uint8_t unknown_a3 = 0;
|
||||
uint8_t unknown_a4 = 0;
|
||||
} __packed_ws__(S_TournamentList_Ep3_E0, 0x704);
|
||||
|
||||
// E0 (C->S): Request system file (BB)
|
||||
// No arguments. The server should respond with an E1 or E2 command.
|
||||
@@ -3047,10 +3068,9 @@ struct S_SystemFileCreated_00E1_BB {
|
||||
// E2 (S->C): Tournament entry list (Episode 3)
|
||||
// Client may send 09 commands if the player presses X. It's not clear what the
|
||||
// server should respond with in this case.
|
||||
// If the player selects an entry slot, client will respond with a long-form 10
|
||||
// command (the Flag03 variant); in this case, unknown_a1 is the team name, and
|
||||
// password is the team password. The server should respond to that with a CC
|
||||
// command.
|
||||
// If the player selects an entry slot, client will respond with a 10 command
|
||||
// containing both a team name and password (with flag = 3). The server should
|
||||
// respond to that with a CC command.
|
||||
|
||||
struct S_TournamentEntryList_Ep3_E2 {
|
||||
le_uint16_t players_per_team = 0;
|
||||
@@ -3487,7 +3507,9 @@ struct S_TeamMemberList_BB_09EA {
|
||||
} __packed_ws__(S_TeamMemberList_BB_09EA, 4);
|
||||
|
||||
// 0CEA (S->C): Unknown
|
||||
// The client ignores this command.
|
||||
// The client ignores this command. It calls filter_curse_words on the
|
||||
// presumably variable-sized text that follows the first 0x20 bytes of the
|
||||
// command, but then does nothing with the result.
|
||||
|
||||
struct S_Unknown_BB_0CEA {
|
||||
parray<uint8_t, 0x20> unknown_a1;
|
||||
@@ -3558,7 +3580,7 @@ struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry {
|
||||
// the same as for the 13EA command.
|
||||
|
||||
// 16EA (S->C): Transfer item via Simple Mail result
|
||||
// No arguments except header.flag, which is 0 if the transfer failed and
|
||||
// No arguments except header.flag, which is zero if the transfer failed or
|
||||
// nonzero if it succeeded.
|
||||
|
||||
// 18EA: Intra-team ranking information
|
||||
@@ -3936,7 +3958,8 @@ struct G_ExtendedHeaderT {
|
||||
// 6x03: Unknown
|
||||
// These subcommands are completely ignored on V3 and later.
|
||||
// On all known DC versions (NTE through V2), the contents of these commands
|
||||
// are written to a global array, but nothing reads from this array.
|
||||
// are written to a global array, but nothing reads from this array. This
|
||||
// command is likely a relic from pre-NTE development.
|
||||
|
||||
struct G_Unknown_6x02_6x03 {
|
||||
G_ClientIDHeader header;
|
||||
@@ -3948,16 +3971,16 @@ struct G_Unknown_6x02_6x03 {
|
||||
} __packed_ws__(G_Unknown_6x02_6x03, 0x14);
|
||||
|
||||
// 6x04: Activate switch by token (deprecated)
|
||||
// This appears to be an early version of 6x0B; it only affects TObjDoorKey
|
||||
// objects and can only activate them, not deactivate them. 6x0B is much more
|
||||
// versatile and is used instead in all known versions of the game. This
|
||||
// command is likely a relic from pre-NTE development.
|
||||
// This appears to be an early version of 6x05 or 6x0B; it only affects
|
||||
// TObjDoorKey objects and can only activate them, not deactivate them. 6x05
|
||||
// and 6x0B are much more versatile and are used instead in all known versions
|
||||
// of the game. This command is likely a relic from pre-NTE development.
|
||||
|
||||
struct G_Unknown_6x04 {
|
||||
struct G_LegacyActivateSwitchByToken_6x04 {
|
||||
G_ParameterHeader header; // param = door token (NOT entity ID or index)
|
||||
le_uint16_t unused1 = 0;
|
||||
le_uint16_t unused2 = 0;
|
||||
} __packed_ws__(G_Unknown_6x04, 8);
|
||||
le_uint16_t switch_token = 0;
|
||||
le_uint16_t unused = 0;
|
||||
} __packed_ws__(G_LegacyActivateSwitchByToken_6x04, 8);
|
||||
|
||||
// 6x05: Write switch flag
|
||||
// Some things that don't look like switches are implemented as switches using
|
||||
@@ -3968,11 +3991,14 @@ struct G_Unknown_6x04 {
|
||||
|
||||
struct G_WriteSwitchFlag_6x05 {
|
||||
// header.entity_id may be 0xFFFF if no object is responsible for the switch
|
||||
// flag state change - this can happen when a wave event script sets a switch
|
||||
// flag state change - this happens when a wave event script sets a switch
|
||||
// flag, for example.
|
||||
G_EntityIDHeader header;
|
||||
// TODO: Some of these might be big-endian on GC; it only byteswaps
|
||||
// switch_flag_num. Are the others actually uint16, or are they uint8[2]?
|
||||
// It seems client_id isn't used anywhere. Some switch-like objects set it
|
||||
// when sending this command, but it seems it's never read when 6x05 is
|
||||
// received (including by virtual functions called on the affected object).
|
||||
// PSO GC doesn't even bother to byteswap it, and we don't either since it's
|
||||
// unused.
|
||||
le_uint16_t client_id = 0;
|
||||
le_uint16_t unused = 0;
|
||||
le_uint16_t switch_flag_num = 0;
|
||||
@@ -3984,6 +4010,9 @@ struct G_WriteSwitchFlag_6x05 {
|
||||
} __packed_ws__(G_WriteSwitchFlag_6x05, 0x0C);
|
||||
|
||||
// 6x06: Send guild card
|
||||
// On BB, the server is responsible for generating and sending the Guild Card
|
||||
// data. newserv applies this logic for all versions of the game, to prevent
|
||||
// players from sending Guild Cards other than their own.
|
||||
|
||||
struct G_SendGuildCard_DCNTE_6x06 {
|
||||
G_UnusedHeader header;
|
||||
@@ -4173,19 +4202,20 @@ struct G_VolOptBossActions_6x15 {
|
||||
|
||||
struct G_VolOptBossActions_6x16 {
|
||||
G_EntityIDHeader header;
|
||||
parray<uint8_t, 6> unknown_a2;
|
||||
le_uint16_t unknown_a3 = 0;
|
||||
parray<uint8_t, 6> entity_index_table;
|
||||
le_uint16_t entity_index_count = 0;
|
||||
} __packed_ws__(G_VolOptBossActions_6x16, 0x0C);
|
||||
|
||||
// 6x17: Vol Opt phase 2 boss actions (not valid on Episode 3)
|
||||
// 6x17: Set entity position and angle (not valid on Episode 3)
|
||||
// This command sets an entity's position and angle without performing any
|
||||
// validity checks, even on v3 and later. We unconditionally block this if it
|
||||
// affects a player other than the sender.
|
||||
|
||||
struct G_VolOpt2BossActions_6x17 {
|
||||
struct G_SetEntityPositionAndAngle_6x17 {
|
||||
G_EntityIDHeader header;
|
||||
le_float unknown_a2 = 0.0f;
|
||||
le_float unknown_a3 = 0.0f;
|
||||
le_float unknown_a4 = 0.0f;
|
||||
le_uint32_t unknown_a5 = 0;
|
||||
} __packed_ws__(G_VolOpt2BossActions_6x17, 0x14);
|
||||
VectorXYZF pos;
|
||||
le_uint32_t angle = 0;
|
||||
} __packed_ws__(G_SetEntityPositionAndAngle_6x17, 0x14);
|
||||
|
||||
// 6x18: Vol Opt phase 2 boss actions (not valid on Episode 3)
|
||||
|
||||
@@ -4218,8 +4248,34 @@ struct G_DisablePKModeForPlayer_6x1C {
|
||||
G_ClientIDHeader header;
|
||||
} __packed_ws__(G_DisablePKModeForPlayer_6x1C, 4);
|
||||
|
||||
// 6x1D: Invalid subcommand
|
||||
// 6x1E: Invalid subcommand
|
||||
// 6x1D: Request partial player data (pre-v1 only)
|
||||
// The subcommand number 6x1D is not used in any final version of PSO; this
|
||||
// number is assigned based on what the command number would be if it were. On
|
||||
// DC NTE, this is subcommand 6x19; on 11/2000, it's 6x1B.
|
||||
// This command does not appear to ever be sent by the client; however, it will
|
||||
// respond with 6x1E if it receives this command.
|
||||
|
||||
struct G_RequestPartialPlayerData_DCProtos_6x1D {
|
||||
G_UnusedHeader header;
|
||||
} __packed_ws__(G_RequestPartialPlayerData_DCProtos_6x1D, 4);
|
||||
|
||||
// 6x1E: Partial player data (pre-v1 only)
|
||||
// The subcommand number 6x1E is not used in any final version of PSO; this
|
||||
// number is assigned based on what the command number would be if it were. On
|
||||
// DC NTE, this is subcommand 6x1A; on 11/2000, it's 6x1C.
|
||||
// The command is truncated after the last valid item in the inventory (that
|
||||
// is, there will be less than 0x360 bytes if the player has fewer than 30
|
||||
// items on hand).
|
||||
|
||||
struct G_PartialPlayerData_DCProtos_6x1E {
|
||||
/* 0000 */ G_ClientIDHeader header;
|
||||
/* 0004 */ le_uint16_t floor;
|
||||
/* 0006 */ le_uint16_t num_items;
|
||||
/* 0008 */ VectorXYZF pos;
|
||||
/* 0014 */ le_uint32_t angle_y;
|
||||
/* 0018 */ parray<PlayerInventoryItem, 30> items;
|
||||
/* 0360 */
|
||||
} __packed_ws__(G_PartialPlayerData_DCProtos_6x1E, 0x360);
|
||||
|
||||
// 6x1F: Set player floor and request positions
|
||||
|
||||
@@ -4298,7 +4354,8 @@ struct G_FeedMag_6x28 {
|
||||
le_uint32_t fed_item_id = 0;
|
||||
} __packed_ws__(G_FeedMag_6x28, 0x0C);
|
||||
|
||||
// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG) (protected on V3 but not V4)
|
||||
// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG)
|
||||
// (protected on 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.
|
||||
|
||||
@@ -4414,7 +4471,18 @@ struct G_RevivePlayer_6x33 {
|
||||
// 6x34: Unknown
|
||||
// This subcommand is ignored by all versions of PSO.
|
||||
|
||||
// 6x35: Invalid subcommand
|
||||
// 6x35: Unknown (pre-v1 only)
|
||||
// This command seems to have a unique history. In DC NTE, it is 6x30, and has
|
||||
// a handler that does something related to what 6x36 does (6x31 in DC NTE). In
|
||||
// 11/2000, however, it seems to have been entirely deleted, and has no command
|
||||
// number at all. But then in DC v1 and later, there is a gap in the subcommand
|
||||
// number table, implying that this command was assigned a number, but there is
|
||||
// no function to send or handle it.
|
||||
|
||||
struct G_Unknown_DCNTE_6x35 {
|
||||
G_ClientIDHeader header;
|
||||
parray<uint8_t, 4> unused;
|
||||
} __packed_ws__(G_Unknown_DCNTE_6x35, 8);
|
||||
|
||||
// 6x36: Unknown (supported; game only)
|
||||
// This subcommand is completely ignored on V3.
|
||||
@@ -4438,10 +4506,20 @@ struct G_PhotonBlast_6x37 {
|
||||
|
||||
struct G_DonateToPhotonBlast_6x38 {
|
||||
G_ClientIDHeader header;
|
||||
le_uint16_t unknown_a1 = 0;
|
||||
le_uint16_t target_client_id = 0;
|
||||
le_uint16_t unused = 0;
|
||||
} __packed_ws__(G_DonateToPhotonBlast_6x38, 8);
|
||||
|
||||
// 6x38.5 (nominally) / 6x33 (DC NTE) / 6x35 (11/2000): Unknown (related to
|
||||
// level up sequence)
|
||||
// This command was deleted after 11/2000 and has no assigned number in any
|
||||
// final version of PSO, hence the odd numbering. It is sent during the level
|
||||
// up sequence in certain situations (TODO).
|
||||
|
||||
struct G_UnknownLevelUpSequence_DCNTE_6x33_112000_6x35 {
|
||||
G_ClientIDHeader header;
|
||||
} __packed_ws__(G_UnknownLevelUpSequence_DCNTE_6x33_112000_6x35, 4);
|
||||
|
||||
// 6x39: Photon blast ready (protected on V3/V4)
|
||||
// This is sent when a player's PB meter reaches 100.
|
||||
|
||||
@@ -4467,7 +4545,21 @@ struct G_ClearTemporaryPhotonBlastStateFlags_6x3B {
|
||||
// 6x3C: Unknown (DCv1 and earlier)
|
||||
// This command has a handler, but it does nothing, even on DC NTE.
|
||||
|
||||
// 6x3D: Invalid subcommand
|
||||
// 6x3D: Target list base
|
||||
// This appears to be a base class for 6x46, 6x47, and 6x49 (and possibly other
|
||||
// subcommands), but it is never sent on the wire. Its likely purpose is to
|
||||
// provide the TargetEntry structure and related functions to derived classes,
|
||||
// but it does still have a subcommand number and a structure of its own, as
|
||||
// described here.
|
||||
|
||||
struct TargetEntry {
|
||||
le_uint16_t entity_id = 0;
|
||||
le_uint16_t unknown_a2 = 0;
|
||||
} __packed_ws__(TargetEntry, 4);
|
||||
|
||||
struct G_TargetBase_6x3D {
|
||||
G_UnusedHeader header;
|
||||
} __packed_ws__(G_TargetBase_6x3D, 4);
|
||||
|
||||
// 6x3E: Stop moving (protected on V3/V4)
|
||||
|
||||
@@ -4519,7 +4611,7 @@ struct G_MoveToPosition_6x41_6x42 {
|
||||
|
||||
struct G_Attack_6x43_6x44_6x45 {
|
||||
G_ClientIDHeader header;
|
||||
le_uint16_t unknown_a1 = 0;
|
||||
le_uint16_t angle_y = 0;
|
||||
le_uint16_t unknown_a2 = 0;
|
||||
} __packed_ws__(G_Attack_6x43_6x44_6x45, 8);
|
||||
|
||||
@@ -4529,22 +4621,16 @@ struct G_Attack_6x43_6x44_6x45 {
|
||||
// targets is too large, the client will byteswap the function's return address
|
||||
// on the stack, and it will crash.
|
||||
|
||||
struct TargetEntry {
|
||||
le_uint16_t entity_id = 0;
|
||||
le_uint16_t unknown_a2 = 0;
|
||||
} __packed_ws__(TargetEntry, 4);
|
||||
|
||||
struct G_AttackFinished_6x46 {
|
||||
struct G_AttackFinished_Header_6x46 {
|
||||
G_ClientIDHeader header;
|
||||
le_uint32_t target_count = 0;
|
||||
// The client may send a shorter command if not all of these are used.
|
||||
parray<TargetEntry, 10> targets;
|
||||
} __packed_ws__(G_AttackFinished_6x46, 0x30);
|
||||
// Up to 10 TargetEntries are sent here
|
||||
} __packed_ws__(G_AttackFinished_Header_6x46, 8);
|
||||
|
||||
// 6x47: Cast technique (protected on V3/V4)
|
||||
// On GC, this command has the same bounds-check bug as 6x46.
|
||||
|
||||
struct G_CastTechnique_6x47 {
|
||||
struct G_CastTechnique_Header_6x47 {
|
||||
G_ClientIDHeader header;
|
||||
uint8_t technique_number = 0;
|
||||
uint8_t unused = 0; // Must not be negative
|
||||
@@ -4555,9 +4641,8 @@ struct G_CastTechnique_6x47 {
|
||||
// cleaned it up.
|
||||
uint8_t level = 0;
|
||||
uint8_t target_count = 0; // Must be in [0, 10]
|
||||
// The client may send a shorter command if not all of these are used.
|
||||
parray<TargetEntry, 10> targets;
|
||||
} __packed_ws__(G_CastTechnique_6x47, 0x30);
|
||||
// Up to 10 TargetEntries are sent here
|
||||
} __packed_ws__(G_CastTechnique_Header_6x47, 8);
|
||||
|
||||
// 6x48: Cast technique complete (protected on V3/V4)
|
||||
|
||||
@@ -4572,16 +4657,15 @@ struct G_CastTechniqueComplete_6x48 {
|
||||
// 6x49: Execute Photon Blast (protected on V3/V4)
|
||||
// On GC, this command has the same bounds-check bug as 6x46.
|
||||
|
||||
struct G_ExecutePhotonBlast_6x49 {
|
||||
struct G_ExecutePhotonBlast_Header_6x49 {
|
||||
G_ClientIDHeader header;
|
||||
uint8_t unknown_a1 = 0;
|
||||
uint8_t unknown_a2 = 0;
|
||||
int8_t unknown_a1 = 0;
|
||||
uint8_t unused1 = 0;
|
||||
le_uint16_t target_count = 0;
|
||||
le_uint16_t unknown_a3 = 0;
|
||||
le_uint16_t unknown_a4 = 0;
|
||||
// The client may send a shorter command if not all of these are used.
|
||||
parray<TargetEntry, 10> targets;
|
||||
} __packed_ws__(G_ExecutePhotonBlast_6x49, 0x34);
|
||||
le_uint16_t pb_meter_value = 0;
|
||||
le_uint16_t unused2 = 0;
|
||||
// Up to 10 TargetEntries are sent here
|
||||
} __packed_ws__(G_ExecutePhotonBlast_Header_6x49, 0x0C);
|
||||
|
||||
// 6x4A: Fully shield attack (protected on V3/V4)
|
||||
|
||||
@@ -4631,8 +4715,8 @@ struct G_SwitchInteraction_6x50 {
|
||||
// 6x51: Set player angle
|
||||
// If UDP mode is enabled, this command is sent via UDP.
|
||||
// This command appears to be vestigial - no version of the game has a handler
|
||||
// for it (it is always ignored), but PSO GC has a function that sends it. It's
|
||||
// not known if this function is ever called, or how to trigger it.
|
||||
// for it (it is always ignored), but most versions have a function that sends
|
||||
// it. It's not known if this function is ever called, or how to trigger it.
|
||||
|
||||
struct G_SetPlayerAngle_6x51 {
|
||||
G_ClientIDHeader header;
|
||||
@@ -5198,6 +5282,10 @@ struct G_SyncQuestRegister_6x77 {
|
||||
} __packed_ws__(G_SyncQuestRegister_6x77, 0x0C);
|
||||
|
||||
// 6x78: Unknown
|
||||
// This command appears to set a per-player timer of some sort. It was
|
||||
// introduced in v2, and there is an object that uses this timer, but it seems
|
||||
// that that object is never constructed. In v3 and later, that object has been
|
||||
// entirely deleted, so the command does nothing.
|
||||
|
||||
struct G_Unknown_6x78 {
|
||||
G_UnusedHeader header;
|
||||
@@ -5210,7 +5298,7 @@ struct G_Unknown_6x78 {
|
||||
|
||||
struct G_GogoBall_6x79 {
|
||||
G_UnusedHeader header;
|
||||
le_uint32_t unknown_a1 = 0;
|
||||
le_uint32_t unknown_a1 = 0; // Appears to be time-related; it's based on server_time_delta_frames
|
||||
le_uint32_t angle = 0;
|
||||
VectorXZF ball_pos;
|
||||
uint8_t use_missile_sound = 0;
|
||||
@@ -5329,10 +5417,7 @@ struct G_PlaceTrap_6x83 {
|
||||
// 6x84: Vol Opt boss actions (not valid on Episode 3)
|
||||
// Same format and usage as 6x16, except unknown_a2 is ignored in 6x84.
|
||||
|
||||
struct G_VolOptBossActions_6x84 {
|
||||
G_UnusedHeader header;
|
||||
parray<uint8_t, 6> unknown_a1;
|
||||
le_uint16_t unknown_a2 = 0;
|
||||
struct G_VolOptBossActions_6x84 : G_VolOptBossActions_6x16 {
|
||||
le_uint16_t unknown_a3 = 0;
|
||||
le_uint16_t unused = 0;
|
||||
} __packed_ws__(G_VolOptBossActions_6x84, 0x10);
|
||||
@@ -5691,11 +5776,13 @@ struct G_Unknown_GCNTE_6xAB {
|
||||
struct G_CreateLobbyChair_6xAB {
|
||||
G_ClientIDHeader header;
|
||||
le_uint16_t unused = 0;
|
||||
le_uint16_t flags = 0; // Only the low two bits are used
|
||||
// Only two bits in this field have meanings:
|
||||
// 01 = unknown
|
||||
// 02 = which pose/animation (on GC, 0 = X+A, 1 = X+B)
|
||||
le_uint16_t flags = 0;
|
||||
} __packed_ws__(G_CreateLobbyChair_6xAB, 8);
|
||||
|
||||
// 6xAC: Unknown (not valid on pre-V3)
|
||||
// This command appears to be different on GC NTE than on any other version.
|
||||
// 6xAC: De Rol Le / Barba Ray boss actions (GC NTE)
|
||||
|
||||
struct G_Unknown_GCNTE_6xAC {
|
||||
G_EntityIDHeader header;
|
||||
@@ -5725,28 +5812,30 @@ struct G_OlgaFlowSubordinateBossActions_6xAD {
|
||||
parray<uint8_t, 0x40> unknown_a1;
|
||||
} __packed_ws__(G_OlgaFlowSubordinateBossActions_6xAD, 0x44);
|
||||
|
||||
// 6xAE: Set lobby chair state (sent by existing clients at join time)
|
||||
// 6xAE: Set animation state (sent by existing clients at join time)
|
||||
// This subcommand is not valid on DC, PC, or GC Trial Edition.
|
||||
|
||||
struct G_SetLobbyChairState_6xAE {
|
||||
struct G_SetAnimationState_6xAE {
|
||||
G_ClientIDHeader header;
|
||||
le_uint16_t unknown_a1 = 0;
|
||||
le_uint16_t animation_number = 0;
|
||||
le_uint16_t unused = 0;
|
||||
// This field contains the flags field on the sender's TObjPlayer object.
|
||||
// If the bit 04000000 is set in this field, then (flags & 1C000000) is or'ed
|
||||
// into the TObjPlayer's flags field. All other bits are ignored.
|
||||
le_uint32_t flags = 0;
|
||||
le_float unknown_a4 = 0;
|
||||
} __packed_ws__(G_SetLobbyChairState_6xAE, 0x10);
|
||||
le_float animation_timer = 0;
|
||||
} __packed_ws__(G_SetAnimationState_6xAE, 0x10);
|
||||
|
||||
// 6xAF: Turn lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4)
|
||||
// 6xAF: Turn lobby chair (not valid on pre-V3 or GC Trial Edition) (protected
|
||||
// on V3/V4)
|
||||
|
||||
struct G_TurnLobbyChair_6xAF {
|
||||
G_ClientIDHeader header;
|
||||
le_uint32_t angle = 0; // In range [0x0000, 0xFFFF]
|
||||
} __packed_ws__(G_TurnLobbyChair_6xAF, 8);
|
||||
|
||||
// 6xB0: Move lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4)
|
||||
// 6xB0: Move lobby chair (not valid on pre-V3 or GC Trial Edition) (protected
|
||||
// on V3/V4)
|
||||
|
||||
struct G_MoveLobbyChair_6xB0 {
|
||||
G_ClientIDHeader header;
|
||||
@@ -6299,7 +6388,7 @@ struct G_ExchangeItemInQuest_BB_6xDB {
|
||||
G_ClientIDHeader header;
|
||||
// If this is 0, the command is identical to 6x29. If this is 1, a function
|
||||
// similar to find_item_by_id is called instead of find_item_by_id, but I
|
||||
// don't yet know what exactly the logic differences are.
|
||||
// don't yet know what exactly the logic differences are (TODO).
|
||||
le_uint32_t unknown_a1 = 0;
|
||||
le_uint32_t item_id = 0;
|
||||
le_uint32_t amount = 0;
|
||||
@@ -6315,12 +6404,21 @@ struct G_Episode4BossActions_BB_6xDC {
|
||||
|
||||
// 6xDD: Set EXP multiplier (BB)
|
||||
// header.param specifies the EXP multiplier. It is 1-based, so the value 2
|
||||
// means all EXP is doubled, for example.
|
||||
// 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
|
||||
@@ -6356,7 +6454,8 @@ struct G_RequestItemDropFromQuest_BB_6xE0 {
|
||||
} __packed_ws__(G_RequestItemDropFromQuest_BB_6xE0, 0x10);
|
||||
|
||||
// 6xE1: Exchange Photon Tickets (BB; handled by server)
|
||||
// The client sends this when it executes an F95F quest opcode.
|
||||
// The client sends this when it executes an F95F quest opcode. The comments
|
||||
// denote where in the command each argument to that opcode is sent.
|
||||
|
||||
struct G_ExchangePhotonTickets_BB_6xE1 {
|
||||
G_ClientIDHeader header;
|
||||
@@ -6684,9 +6783,10 @@ struct G_ForceDisconnect_Ep3_6xB5x1A {
|
||||
// No arguments
|
||||
} __packed_ws__(G_ForceDisconnect_Ep3_6xB5x1A, 8);
|
||||
|
||||
// 6xB3x1B / CAx1B: Set player name during setup
|
||||
// Curiously, this command can be used during a non-setup phase; the server
|
||||
// should ignore the command's contents but still send a 6xB4x1C in response.
|
||||
// 6xB3x1B / CAx1B: Set player name
|
||||
// This command is normally sent during battle setup to populate player slots,
|
||||
// but is also sent if a player disconnects during battle to switch their
|
||||
// player type from human to CPU.
|
||||
|
||||
struct G_SetPlayerName_Ep3_CAx1B {
|
||||
G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetPlayerName_Ep3_CAx1B) / 4, 0, 0x1B, 0, 0, 0, 0, 0};
|
||||
@@ -6840,7 +6940,7 @@ struct G_EnqueueAnimation_Ep3_6xB4x2C {
|
||||
/* 0A */ parray<le_uint16_t, 3> card_refs;
|
||||
/* 10 */ Episode3::Location loc;
|
||||
/* 14 */ le_uint32_t trap_card_id = 0xFFFFFFFF;
|
||||
/* 14 */ le_uint32_t unknown_a3 = 0xFFFFFFFF;
|
||||
/* 18 */ le_uint32_t unknown_a3 = 0xFFFFFFFF;
|
||||
/* 1C */
|
||||
} __packed_ws__(G_EnqueueAnimation_Ep3_6xB4x2C, 0x1C);
|
||||
|
||||
@@ -7108,7 +7208,11 @@ struct G_OpenBlockingMenu_Ep3_6xB5x3F {
|
||||
int8_t menu_type = 0; // Must be in the range [-1, 0x14]
|
||||
uint8_t client_id = 0;
|
||||
parray<uint8_t, 2> unused1;
|
||||
le_uint32_t unknown_a3 = 0;
|
||||
// The random_seed field is only used for the battle prep menu (01/02); for
|
||||
// other menu types, it contains uninitialized data. This is used as the
|
||||
// seed for a PSOV2Encryption instance, but it's not clear what that instance
|
||||
// is used for (if anything).
|
||||
le_uint32_t random_seed = 0;
|
||||
parray<uint8_t, 4> unused2;
|
||||
} __packed_ws__(G_OpenBlockingMenu_Ep3_6xB5x3F, 0x14);
|
||||
|
||||
@@ -7145,9 +7249,9 @@ struct G_InitiateCardAuction_Ep3_6xB5x42 {
|
||||
// 6xB5x43: Unused legacy card auction
|
||||
// This command stores the card IDs and counts in a global array on the client,
|
||||
// but this array is never read from. The function that handles this command is
|
||||
// remarkably similar to the function that handles the EF command, so It's
|
||||
// likely that this command is a now-unused early implementation of the card
|
||||
// auction sequence.
|
||||
// very similar to the function that handles the EF command, so it's likely
|
||||
// that this command is a now-unused early implementation of the card auction
|
||||
// initiation sequence.
|
||||
|
||||
struct G_UnusedLegacyCardAuction_Ep3_6xB5x43 {
|
||||
G_CardBattleCommandHeader header = {0xB5, sizeof(G_UnusedLegacyCardAuction_Ep3_6xB5x43) / 4, 0, 0x43, 0, 0, 0};
|
||||
@@ -7155,7 +7259,7 @@ struct G_UnusedLegacyCardAuction_Ep3_6xB5x43 {
|
||||
// Both fields here are masked. To get the actual values used by the game,
|
||||
// XOR the values here with 0x39AB.
|
||||
le_uint16_t masked_card_id = 0xFFFF; // Must be < 0x2F1 (when unmasked)
|
||||
le_uint16_t masked_count = 0; // Must be in [1, 99] (when unmasked)
|
||||
le_uint16_t masked_cost = 0; // Must be in [1, 99] (when unmasked)
|
||||
} __packed_ws__(Entry, 4);
|
||||
parray<Entry, 0x14> entries;
|
||||
} __packed_ws__(G_UnusedLegacyCardAuction_Ep3_6xB5x43, 0x58);
|
||||
@@ -7456,14 +7560,17 @@ struct G_RejectBattleStartRequest_Ep3_6xB4x53 {
|
||||
// GC v3: PSOGCCharacterFile::Character
|
||||
// XB v3: PSOXBCharacterFile::Character
|
||||
|
||||
// 6xE4: Increment enemy damage threshold
|
||||
// 6xE4: Increment enemy damage
|
||||
// This command increments or decrements the amount of damage an enemy has
|
||||
// sustained. This replaces the use of total_damage in 6x0A to update enemy HP.
|
||||
// sustained. This replaces the use of total_damage in 6x0A to update enemy HP
|
||||
// when used with the EnemyDamageSync patch.
|
||||
|
||||
struct G_IncrementEnemyDamage_Extension_6xE4 {
|
||||
G_EntityIDHeader header = {0xE4, sizeof(G_IncrementEnemyDamage_Extension_6xE4) / 4, 0x0000};
|
||||
le_int16_t hit_amount = 0;
|
||||
le_uint16_t total_damage_before_hit = 0;
|
||||
le_uint16_t current_hp_before_hit = 0;
|
||||
le_uint16_t max_hp = 0;
|
||||
} __packed_ws__(G_IncrementEnemyDamage_Extension_6xE4, 0x0C);
|
||||
/* 00 */ G_EntityIDHeader header = {0xE4, sizeof(G_IncrementEnemyDamage_Extension_6xE4) / 4, 0x0000};
|
||||
/* 04 */ le_int16_t hit_amount = 0;
|
||||
/* 06 */ le_uint16_t total_damage_before_hit = 0;
|
||||
/* 08 */ le_uint16_t current_hp_before_hit = 0;
|
||||
/* 0A */ le_uint16_t max_hp = 0;
|
||||
/* 0C */ le_float factor = -1.0;
|
||||
/* 10 */
|
||||
} __packed_ws__(G_IncrementEnemyDamage_Extension_6xE4, 0x10);
|
||||
|
||||
+61
-54
@@ -48,11 +48,7 @@ void from_json_into(const phosg::JSON& json, parray<CommonItemSet::Table::Range<
|
||||
|
||||
template <typename IntT>
|
||||
phosg::JSON to_json(const CommonItemSet::Table::Range<IntT>& v) {
|
||||
if (v.min == v.max) {
|
||||
return phosg::JSON(v.min);
|
||||
} else {
|
||||
return phosg::JSON::list({v.min, v.max});
|
||||
}
|
||||
return (v.min == v.max) ? phosg::JSON(v.min) : phosg::JSON::list({v.min, v.max});
|
||||
}
|
||||
|
||||
template <typename IntT>
|
||||
@@ -677,29 +673,49 @@ shared_ptr<const CommonItemSet::Table> CommonItemSet::get_table(
|
||||
}
|
||||
|
||||
AFSV2CommonItemSet::AFSV2CommonItemSet(
|
||||
std::shared_ptr<const std::string> pt_afs_data,
|
||||
std::shared_ptr<const std::string> ct_afs_data) {
|
||||
// ItemPT.afs has 40 entries; the first 10 are for Normal, then Hard, etc.
|
||||
AFSArchive pt_afs(pt_afs_data);
|
||||
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
|
||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||
auto entry = pt_afs.get(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);
|
||||
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::BATTLE, difficulty, section_id), table);
|
||||
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::SOLO, difficulty, section_id), table);
|
||||
std::shared_ptr<const std::string> pt_afs_data, std::shared_ptr<const std::string> ct_afs_data) {
|
||||
// Each AFS file has 40 entries (30 on v1); the first 10 are for Normal, then
|
||||
// Hard, etc.
|
||||
{
|
||||
AFSArchive pt_afs(pt_afs_data);
|
||||
size_t max_difficulty;
|
||||
if (pt_afs.num_entries() >= 40) {
|
||||
max_difficulty = 4;
|
||||
} else if (pt_afs.num_entries() >= 30) {
|
||||
max_difficulty = 3;
|
||||
} 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 (size_t section_id = 0; section_id < 10; section_id++) {
|
||||
auto entry = pt_afs.get(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);
|
||||
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::BATTLE, difficulty, section_id), table);
|
||||
this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::SOLO, difficulty, section_id), table);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ItemCT.afs also has 40 entries, but only the 0th, 10th, 20th, and 30th are
|
||||
// used (section_id is ignored)
|
||||
AFSArchive ct_afs(ct_afs_data);
|
||||
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
|
||||
auto r = ct_afs.get_reader(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);
|
||||
// ItemCT AFS files also have 40 entries, but only the 0th, 10th, 20th, and
|
||||
// 30th are used (section_id is ignored)
|
||||
if (ct_afs_data) {
|
||||
AFSArchive ct_afs(ct_afs_data);
|
||||
size_t max_difficulty;
|
||||
if (ct_afs.num_entries() >= 40) {
|
||||
max_difficulty = 4;
|
||||
} else if (ct_afs.num_entries() >= 30) {
|
||||
max_difficulty = 3;
|
||||
} 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -758,10 +774,14 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gs
|
||||
|
||||
if (episode != Episode::EP4) {
|
||||
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
|
||||
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
|
||||
auto table = make_shared<Table>(r, is_big_endian, true, episode);
|
||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||
this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table);
|
||||
try {
|
||||
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
|
||||
auto table = make_shared<Table>(r, is_big_endian, true, episode);
|
||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||
this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table);
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
// GC NTE doesn't have Ep2 challenge; just skip adding the table
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -796,8 +816,7 @@ JSONCommonItemSet::JSONCommonItemSet(const phosg::JSON& json) {
|
||||
}
|
||||
|
||||
RELFileSet::RELFileSet(std::shared_ptr<const std::string> data)
|
||||
: data(data),
|
||||
r(*this->data) {}
|
||||
: data(data), r(*this->data) {}
|
||||
|
||||
ArmorRandomSet::ArmorRandomSet(std::shared_ptr<const std::string> data)
|
||||
: RELFileSet(data) {
|
||||
@@ -825,18 +844,13 @@ ArmorRandomSet::get_unit_table(size_t index) const {
|
||||
ToolRandomSet::ToolRandomSet(std::shared_ptr<const std::string> data)
|
||||
: RELFileSet(data) {
|
||||
uint32_t specs_offset = r.pget_u32b(data->size() - 0x10);
|
||||
this->common_recovery_table_spec = &r.pget<TableSpec>(r.pget_u32b(
|
||||
specs_offset));
|
||||
this->rare_recovery_table_spec = &r.pget<TableSpec>(
|
||||
r.pget_u32b(specs_offset + sizeof(uint32_t)),
|
||||
2 * sizeof(TableSpec));
|
||||
this->common_recovery_table_spec = &r.pget<TableSpec>(r.pget_u32b(specs_offset));
|
||||
this->rare_recovery_table_spec = &r.pget<TableSpec>(r.pget_u32b(specs_offset + sizeof(uint32_t)), 2 * sizeof(TableSpec));
|
||||
this->tech_disk_table_spec = this->rare_recovery_table_spec + 1;
|
||||
this->tech_disk_level_table_spec = &r.pget<TableSpec>(r.pget_u32b(
|
||||
specs_offset + 2 * sizeof(uint32_t)));
|
||||
this->tech_disk_level_table_spec = &r.pget<TableSpec>(r.pget_u32b(specs_offset + 2 * sizeof(uint32_t)));
|
||||
}
|
||||
|
||||
pair<const uint8_t*, size_t> ToolRandomSet::get_common_recovery_table(
|
||||
size_t index) const {
|
||||
pair<const uint8_t*, size_t> ToolRandomSet::get_common_recovery_table(size_t index) const {
|
||||
return this->get_table<uint8_t>(*this->common_recovery_table_spec, index);
|
||||
}
|
||||
|
||||
@@ -863,25 +877,21 @@ WeaponRandomSet::WeaponRandomSet(std::shared_ptr<const std::string> data)
|
||||
|
||||
std::pair<const WeaponRandomSet::WeightTableEntry8*, size_t>
|
||||
WeaponRandomSet::get_weapon_type_table(size_t index) const {
|
||||
const auto& spec = this->r.pget<TableSpec>(
|
||||
this->offsets->weapon_type_table + index * sizeof(TableSpec));
|
||||
const auto* data = &this->r.pget<WeightTableEntry8>(
|
||||
spec.offset, spec.entries_per_table * sizeof(WeightTableEntry8));
|
||||
const auto& spec = this->r.pget<TableSpec>(this->offsets->weapon_type_table + index * sizeof(TableSpec));
|
||||
const auto* data = &this->r.pget<WeightTableEntry8>(spec.offset, spec.entries_per_table * sizeof(WeightTableEntry8));
|
||||
return make_pair(data, spec.entries_per_table);
|
||||
}
|
||||
|
||||
const parray<WeaponRandomSet::WeightTableEntry32, 6>*
|
||||
WeaponRandomSet::get_bonus_type_table(size_t which, size_t index) const {
|
||||
uint32_t base_offset = which ? this->offsets->bonus_type_table2 : this->offsets->bonus_type_table1;
|
||||
return &this->r.pget<parray<WeightTableEntry32, 6>>(
|
||||
base_offset + sizeof(parray<WeightTableEntry32, 6>) * index);
|
||||
return &this->r.pget<parray<WeightTableEntry32, 6>>(base_offset + sizeof(parray<WeightTableEntry32, 6>) * index);
|
||||
}
|
||||
|
||||
const WeaponRandomSet::RangeTableEntry*
|
||||
WeaponRandomSet::get_bonus_range(size_t which, size_t index) const {
|
||||
uint32_t base_offset = which ? this->offsets->bonus_range_table2 : this->offsets->bonus_range_table1;
|
||||
return &this->r.pget<RangeTableEntry>(
|
||||
base_offset + sizeof(RangeTableEntry) * index);
|
||||
return &this->r.pget<RangeTableEntry>(base_offset + sizeof(RangeTableEntry) * index);
|
||||
}
|
||||
|
||||
const parray<WeaponRandomSet::WeightTableEntry32, 3>*
|
||||
@@ -892,19 +902,16 @@ WeaponRandomSet::get_special_mode_table(size_t index) const {
|
||||
|
||||
const WeaponRandomSet::RangeTableEntry*
|
||||
WeaponRandomSet::get_standard_grind_range(size_t index) const {
|
||||
return &this->r.pget<RangeTableEntry>(
|
||||
this->offsets->standard_grind_range_table + sizeof(RangeTableEntry) * index);
|
||||
return &this->r.pget<RangeTableEntry>(this->offsets->standard_grind_range_table + sizeof(RangeTableEntry) * index);
|
||||
}
|
||||
|
||||
const WeaponRandomSet::RangeTableEntry*
|
||||
WeaponRandomSet::get_favored_grind_range(size_t index) const {
|
||||
return &this->r.pget<RangeTableEntry>(
|
||||
this->offsets->favored_grind_range_table + sizeof(RangeTableEntry) * index);
|
||||
return &this->r.pget<RangeTableEntry>(this->offsets->favored_grind_range_table + sizeof(RangeTableEntry) * index);
|
||||
}
|
||||
|
||||
TekkerAdjustmentSet::TekkerAdjustmentSet(std::shared_ptr<const std::string> data)
|
||||
: data(data),
|
||||
r(*data) {
|
||||
: data(data), r(*data) {
|
||||
this->offsets = &this->r.pget<Offsets>(this->r.pget_u32b(this->r.size() - 0x10));
|
||||
}
|
||||
|
||||
|
||||
@@ -457,7 +457,7 @@ asio::awaitable<void> DownloadSession::on_message(Channel::Message& msg) {
|
||||
case 0x1F:
|
||||
case 0xA0:
|
||||
case 0xA1: {
|
||||
C_MenuSelection_10_Flag00 ret;
|
||||
C_MenuSelectionBase_10 ret;
|
||||
|
||||
auto handle_command = [&]<typename CmdT>() {
|
||||
const auto* items = check_size_vec_t<CmdT>(msg.data, msg.flag + 1);
|
||||
@@ -764,7 +764,7 @@ void DownloadSession::send_next_request() {
|
||||
this->log.info_f("Sending request {:016X}", this->current_request);
|
||||
}
|
||||
|
||||
C_MenuSelection_10_Flag00 cmd;
|
||||
C_MenuSelectionBase_10 cmd;
|
||||
cmd.menu_id = (this->current_request >> 32) & 0xFFFFFFFF;
|
||||
cmd.item_id = this->current_request & 0xFFFFFFFF;
|
||||
this->channel->send(0x10, 0x00, cmd);
|
||||
|
||||
+23
-28
@@ -2626,8 +2626,8 @@ MapIndex::VersionedMap::VersionedMap(shared_ptr<const MapDefinition> map, uint8_
|
||||
|
||||
MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t language)
|
||||
: language(language),
|
||||
compressed_data(std::move(compressed_data)) {
|
||||
string decompressed = prs_decompress(this->compressed_data);
|
||||
compressed_data(make_shared<string>(std::move(compressed_data))) {
|
||||
string decompressed = prs_decompress(*this->compressed_data);
|
||||
if (decompressed.size() == sizeof(MapDefinitionTrial)) {
|
||||
this->map = make_shared<MapDefinition>(*reinterpret_cast<const MapDefinitionTrial*>(decompressed.data()));
|
||||
} else if (decompressed.size() == sizeof(MapDefinition)) {
|
||||
@@ -2646,21 +2646,30 @@ shared_ptr<const MapDefinitionTrial> MapIndex::VersionedMap::trial() const {
|
||||
return this->trial_map;
|
||||
}
|
||||
|
||||
const std::string& MapIndex::VersionedMap::compressed(bool is_nte) const {
|
||||
if (is_nte) {
|
||||
if (this->compressed_trial_data.empty()) {
|
||||
std::shared_ptr<const std::string> MapIndex::VersionedMap::compressed(bool trial) const {
|
||||
if (trial) {
|
||||
if (!this->compressed_data_trial) {
|
||||
auto md = this->trial();
|
||||
this->compressed_trial_data = prs_compress(md.get(), sizeof(*md));
|
||||
this->compressed_data_trial = make_shared<string>(prs_compress(md.get(), sizeof(*md)));
|
||||
}
|
||||
return this->compressed_trial_data;
|
||||
return this->compressed_data_trial;
|
||||
} else {
|
||||
if (this->compressed_data.empty()) {
|
||||
this->compressed_data = prs_compress(this->map.get(), sizeof(*this->map));
|
||||
if (!this->compressed_data) {
|
||||
this->compressed_data = make_shared<string>(prs_compress(this->map.get(), sizeof(*this->map)));
|
||||
}
|
||||
return this->compressed_data;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<const std::string> MapIndex::VersionedMap::trial_download() const {
|
||||
if (!this->download_data_trial) {
|
||||
MapDefinitionTrial trial_map = *this->map;
|
||||
trial_map.tag = 0x96;
|
||||
this->download_data_trial = make_shared<string>(prs_compress(&trial_map, sizeof(trial_map)));
|
||||
}
|
||||
return this->download_data_trial;
|
||||
}
|
||||
|
||||
MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version)
|
||||
: map_number(initial_version->map->map_number),
|
||||
initial_version(initial_version) {
|
||||
@@ -2704,6 +2713,7 @@ shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(uint8_t language
|
||||
}
|
||||
|
||||
MapIndex::MapIndex(const string& directory) {
|
||||
map<uint32_t, shared_ptr<Map>> mutable_maps;
|
||||
for (const auto& item : std::filesystem::directory_iterator(directory)) {
|
||||
string filename = item.path().filename().string();
|
||||
try {
|
||||
@@ -2756,9 +2766,10 @@ MapIndex::MapIndex(const string& directory) {
|
||||
}
|
||||
|
||||
string name = vm->map->name.decode(vm->language);
|
||||
auto map_it = this->maps.find(vm->map->map_number);
|
||||
if (map_it == this->maps.end()) {
|
||||
map_it = this->maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first;
|
||||
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;
|
||||
this->maps.emplace(vm->map->map_number, map_it->second);
|
||||
static_game_data_log.debug_f("({}) Created Episode 3 map {:08X} {} ({}; {})",
|
||||
filename,
|
||||
vm->map->map_number,
|
||||
@@ -2866,22 +2877,6 @@ const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language
|
||||
return compressed_map_list;
|
||||
}
|
||||
|
||||
shared_ptr<const MapIndex::Map> MapIndex::for_number(uint32_t id) const {
|
||||
return this->maps.at(id);
|
||||
}
|
||||
|
||||
shared_ptr<const MapIndex::Map> MapIndex::for_name(const string& name) const {
|
||||
return this->maps_by_name.at(name);
|
||||
}
|
||||
|
||||
set<uint32_t> MapIndex::all_numbers() const {
|
||||
set<uint32_t> ret;
|
||||
for (const auto& it : this->maps) {
|
||||
ret.emplace(it.first);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
COMDeckIndex::COMDeckIndex(const string& filename) {
|
||||
try {
|
||||
auto json = phosg::JSON::parse(phosg::load_file(filename));
|
||||
|
||||
@@ -1179,7 +1179,8 @@ struct OverlayState {
|
||||
struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
// If tag is not 0x00000100, the game considers the map to be corrupt in
|
||||
// offline mode and will delete it (if it's a download quest). The tag field
|
||||
// doesn't seem to have any other use.
|
||||
// doesn't seem to have any other use. In Trial Edition, download quests are
|
||||
// expected to have 0x96 here instead.
|
||||
/* 0000 */ be_uint32_t tag;
|
||||
|
||||
/* 0004 */ be_uint32_t map_number; // Must be unique across all maps
|
||||
@@ -1597,12 +1598,14 @@ public:
|
||||
VersionedMap(std::string&& compressed_data, uint8_t language);
|
||||
|
||||
std::shared_ptr<const MapDefinitionTrial> trial() const;
|
||||
const std::string& compressed(bool is_nte) const;
|
||||
std::shared_ptr<const std::string> compressed(bool trial) const;
|
||||
std::shared_ptr<const std::string> trial_download() const;
|
||||
|
||||
private:
|
||||
mutable std::shared_ptr<const MapDefinitionTrial> trial_map;
|
||||
mutable std::string compressed_data;
|
||||
mutable std::string compressed_trial_data;
|
||||
mutable std::shared_ptr<std::string> compressed_data;
|
||||
mutable std::shared_ptr<std::string> compressed_data_trial;
|
||||
mutable std::shared_ptr<std::string> download_data_trial;
|
||||
};
|
||||
|
||||
class Map {
|
||||
@@ -1624,14 +1627,20 @@ public:
|
||||
};
|
||||
|
||||
const std::string& get_compressed_list(size_t num_players, uint8_t language) const;
|
||||
std::shared_ptr<const Map> for_number(uint32_t id) const;
|
||||
std::shared_ptr<const Map> for_name(const std::string& name) const;
|
||||
std::set<uint32_t> all_numbers() const;
|
||||
inline std::shared_ptr<const Map> get(uint32_t id) const {
|
||||
return this->maps.at(id);
|
||||
}
|
||||
inline std::shared_ptr<const Map> get(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 {
|
||||
return this->maps;
|
||||
}
|
||||
|
||||
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;
|
||||
std::map<uint32_t, std::shared_ptr<Map>> maps;
|
||||
std::map<uint32_t, std::shared_ptr<const Map>> maps;
|
||||
std::unordered_map<std::string, std::shared_ptr<Map>> maps_by_name;
|
||||
};
|
||||
|
||||
|
||||
@@ -287,9 +287,9 @@ string Server::prepare_6xB6x41_map_definition(shared_ptr<const MapIndex::Map> ma
|
||||
const auto& compressed = vm->compressed(is_nte);
|
||||
|
||||
phosg::StringWriter w;
|
||||
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3);
|
||||
w.put<G_MapData_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed.size(), 0});
|
||||
w.write(compressed);
|
||||
uint32_t subcommand_size = (compressed->size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3);
|
||||
w.put<G_MapData_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed->size(), 0});
|
||||
w.write(*compressed);
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
@@ -2588,7 +2588,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->for_number(cmd.map_number);
|
||||
this->last_chosen_map = this->options.map_index->get(cmd.map_number);
|
||||
this->send_6xB6x41_to_all_clients();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ Tournament::PlayerEntry::PlayerEntry(uint32_t account_id, const string& player_n
|
||||
Tournament::PlayerEntry::PlayerEntry(shared_ptr<Client> c)
|
||||
: account_id(c->login->account->account_id),
|
||||
client(c),
|
||||
player_name(c->character()->disp.name.decode(c->language())) {}
|
||||
player_name(c->character_file()->disp.name.decode(c->language())) {}
|
||||
|
||||
Tournament::PlayerEntry::PlayerEntry(
|
||||
shared_ptr<const COMDeckDefinition> com_deck)
|
||||
@@ -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->for_number(this->source_json.get_int("map_number"));
|
||||
this->map = this->map_index->get(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)) {
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ vector<shared_ptr<Client>> GameServer::get_clients_by_identifier(const string& i
|
||||
continue;
|
||||
}
|
||||
|
||||
auto p = c->character(false, false);
|
||||
auto p = c->character_file(false, false);
|
||||
if (p && p->disp.name.eq(ident, p->inventory.language)) {
|
||||
results.emplace_back(c);
|
||||
continue;
|
||||
|
||||
+34
-21
@@ -169,7 +169,7 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
|
||||
if (c->version() == Version::BB_V4) {
|
||||
ret->emplace("BBCharacterIndex", c->bb_character_index);
|
||||
}
|
||||
auto p = c->character(false, false);
|
||||
auto p = c->character_file(false, false);
|
||||
if (p) {
|
||||
if (!is_ep3(c->version())) {
|
||||
if (c->version() != Version::DC_NTE) {
|
||||
@@ -328,13 +328,13 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
|
||||
{"LobbyPlayers", std::move(lobby_players_json)},
|
||||
});
|
||||
switch (ses->drop_mode) {
|
||||
case ProxySession::DropMode::DISABLED:
|
||||
case ProxyDropMode::DISABLED:
|
||||
ses_json.emplace("DropMode", "none");
|
||||
break;
|
||||
case ProxySession::DropMode::PASSTHROUGH:
|
||||
case ProxyDropMode::PASSTHROUGH:
|
||||
ses_json.emplace("DropMode", "default");
|
||||
break;
|
||||
case ProxySession::DropMode::INTERCEPT:
|
||||
case ProxyDropMode::INTERCEPT:
|
||||
ses_json.emplace("DropMode", "proxy");
|
||||
break;
|
||||
}
|
||||
@@ -386,19 +386,19 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_lobby_json(
|
||||
ret->emplace("EXPShareMultiplier", l->exp_share_multiplier);
|
||||
ret->emplace("AllowedDropModes", l->allowed_drop_modes);
|
||||
switch (l->drop_mode) {
|
||||
case Lobby::DropMode::DISABLED:
|
||||
case ServerDropMode::DISABLED:
|
||||
ret->emplace("DropMode", "none");
|
||||
break;
|
||||
case Lobby::DropMode::CLIENT:
|
||||
case ServerDropMode::CLIENT:
|
||||
ret->emplace("DropMode", "client");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_SHARED:
|
||||
case ServerDropMode::SERVER_SHARED:
|
||||
ret->emplace("DropMode", "shared");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_PRIVATE:
|
||||
case ServerDropMode::SERVER_PRIVATE:
|
||||
ret->emplace("DropMode", "private");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_DUPLICATE:
|
||||
case ServerDropMode::SERVER_DUPLICATE:
|
||||
ret->emplace("DropMode", "duplicate");
|
||||
break;
|
||||
}
|
||||
@@ -596,7 +596,7 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_lobbies_json() const {
|
||||
std::shared_ptr<phosg::JSON> HTTPServer::generate_summary_json() const {
|
||||
auto clients_json = phosg::JSON::list();
|
||||
for (const auto& c : this->state->game_server->all_clients()) {
|
||||
auto p = c->character(false, false);
|
||||
auto p = c->character_file(false, false);
|
||||
auto l = c->lobby.lock();
|
||||
clients_json.emplace_back(phosg::JSON::dict({
|
||||
{"ID", c->id},
|
||||
@@ -669,12 +669,23 @@ asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_ep3_cards_jso
|
||||
});
|
||||
}
|
||||
|
||||
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_common_tables_json() const {
|
||||
auto v2_table = this->state->common_item_set_v2;
|
||||
auto v3_v4_table = this->state->common_item_set_v3_v4;
|
||||
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> {
|
||||
return make_shared<phosg::JSON>(phosg::JSON::dict({{"v1_v2", v2_table->json()}, {"v3_v4", v3_v4_table->json()}}));
|
||||
});
|
||||
std::shared_ptr<phosg::JSON> HTTPServer::generate_common_table_list_json() const {
|
||||
auto ret = make_shared<phosg::JSON>(phosg::JSON::list());
|
||||
for (const auto& it : this->state->common_item_sets) {
|
||||
ret->emplace_back(it.first);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_common_table_json(const std::string& table_name) const {
|
||||
try {
|
||||
const auto& table = this->state->common_item_sets.at(table_name);
|
||||
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> {
|
||||
return make_shared<phosg::JSON>(table->json());
|
||||
});
|
||||
} catch (const out_of_range&) {
|
||||
throw HTTPError(404, "Table does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<phosg::JSON> HTTPServer::generate_rare_table_list_json() const {
|
||||
@@ -706,10 +717,9 @@ asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_rare_table_js
|
||||
}
|
||||
}
|
||||
|
||||
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_quest_list_json(
|
||||
std::shared_ptr<const QuestIndex> quest_index) {
|
||||
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_quest_list_json() {
|
||||
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> {
|
||||
return make_shared<phosg::JSON>(quest_index->json());
|
||||
return make_shared<phosg::JSON>(this->state->quest_index->json());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -794,7 +804,10 @@ asio::awaitable<std::unique_ptr<HTTPResponse>> HTTPServer::handle_request(shared
|
||||
ret = co_await this->generate_ep3_cards_json(true);
|
||||
} else if (req.path == "/y/data/common-tables") {
|
||||
this->require_GET(req);
|
||||
ret = co_await this->generate_common_tables_json();
|
||||
ret = this->generate_common_table_list_json();
|
||||
} else if (req.path.starts_with("/y/data/common-tables/")) {
|
||||
this->require_GET(req);
|
||||
ret = co_await this->generate_common_table_json(req.path.substr(22));
|
||||
} else if (req.path == "/y/data/rare-tables") {
|
||||
this->require_GET(req);
|
||||
ret = this->generate_rare_table_list_json();
|
||||
@@ -803,7 +816,7 @@ asio::awaitable<std::unique_ptr<HTTPResponse>> HTTPServer::handle_request(shared
|
||||
ret = co_await this->generate_rare_table_json(req.path.substr(20));
|
||||
} else if (req.path == "/y/data/quests") {
|
||||
this->require_GET(req);
|
||||
ret = co_await this->generate_quest_list_json(this->state->quest_index(Version::GC_V3));
|
||||
ret = co_await this->generate_quest_list_json();
|
||||
} else if (req.path == "/y/data/config") {
|
||||
this->require_GET(req);
|
||||
ret = this->state->config_json;
|
||||
|
||||
+3
-2
@@ -37,10 +37,11 @@ protected:
|
||||
std::shared_ptr<phosg::JSON> generate_all_json() const;
|
||||
|
||||
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_ep3_cards_json(bool trial) const;
|
||||
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_common_tables_json() 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(std::shared_ptr<const QuestIndex> q);
|
||||
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_quest_list_json();
|
||||
|
||||
void require_GET(const HTTPRequest& req);
|
||||
phosg::JSON require_POST(const HTTPRequest& req);
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "PlayerSubordinates.hh"
|
||||
#include "QuestScript.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "TeamIndex.hh"
|
||||
|
||||
|
||||
+1
-1
@@ -342,7 +342,7 @@ bool ItemCreator::should_allow_meseta_drops() const {
|
||||
|
||||
ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_type, uint8_t area_norm) {
|
||||
ItemData item;
|
||||
if (this->are_rare_drops_allowed() && (enemy_type > 0) && (enemy_type < 0x58)) {
|
||||
if (this->are_rare_drops_allowed() && (enemy_type > 0) && (enemy_type < 0x64)) {
|
||||
// Note: In the original implementation, enemies can only have one possible
|
||||
// rare drop. In our implementation, they can have multiple rare drops if
|
||||
// JSONRareItemSet is used (the other RareItemSet implementations never
|
||||
|
||||
+7
-6
@@ -82,14 +82,14 @@ struct ItemData {
|
||||
// QUICK ITEM FORMAT REFERENCE
|
||||
// data1/0 data1/4 data1/8 data2
|
||||
// Weapon: 00ZZZZGG SSNNAABB AABBAABB 00000000
|
||||
// Armor: 0101ZZ00 FFTTDDDD EEEE0000 00000000
|
||||
// Shield: 0102ZZ00 FFTTDDDD EEEE0000 00000000
|
||||
// Unit: 0103ZZ00 FF00RRRR 00000000 00000000
|
||||
// Armor: 0101ZZ00 FFTTDDDD EEEEXXXX 00000000
|
||||
// Shield: 0102ZZ00 FFTTDDDD EEEEXXXX 00000000
|
||||
// Unit: 0103ZZ00 FF00RRRR 0000XXXX 00000000
|
||||
// Mag: 02ZZLLWW HHHHIIII JJJJKKKK YYQQPPVV
|
||||
// Tool: 03ZZZZUU 00CC0000 00000000 00000000
|
||||
// Tool: 03ZZZZUU 00CC0000 0000XXXX 00000000
|
||||
// Meseta: 04000000 00000000 00000000 MMMMMMMM
|
||||
// A = attribute type (for S-ranks, custom name)
|
||||
// B = attribute amount (for S-ranks, custom name)
|
||||
// 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)
|
||||
// D = DEF bonus
|
||||
// E = EVP bonus
|
||||
@@ -110,6 +110,7 @@ struct ItemData {
|
||||
// U = tool flags (40=present; unused if item is stackable)
|
||||
// V = mag color
|
||||
// W = photon blasts
|
||||
// X = kill count (big-endian; high bit always set)
|
||||
// Y = mag synchro
|
||||
// Z = item ID
|
||||
// Note: PSO GC erroneously byteswaps data2 even when the item is a mag. This
|
||||
|
||||
@@ -679,7 +679,7 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
auto pmt = this->item_parameter_table;
|
||||
|
||||
phosg::fwrite_fmt(stream, "WEAPONS\n");
|
||||
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS FLAG ATPLO ATPHI ATPRQ MSTRQ ATARQ -MST- GND PH SP ATA SB(S1:AMT1,S2:AMT2) PJ 1X 1Y 2X 2Y CL A1 A2 A3 A4 A5 TB BF V1 ST* USL ---DIVISOR--- NAME\n");
|
||||
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS FLAG ATPLO ATPHI ATPRQ MSTRQ ATARQ -MST- GND PH SP ATA SB(S1:AMT1,S2:AMT2) PJ 1X 1Y 2X 2Y CL --A1-- A4 A5 TB BF V1 ST* USL ---DIVISOR--- NAME\n");
|
||||
for (size_t data1_1 = 0; data1_1 < pmt->num_weapon_classes; data1_1++) {
|
||||
uint8_t v1_replacement = pmt->get_weapon_v1_replacement(data1_1);
|
||||
float sale_divisor = pmt->get_sale_divisor(0x00, data1_1);
|
||||
@@ -699,7 +699,7 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
string name = this->describe_item(item);
|
||||
|
||||
auto& stat_boost = pmt->get_stat_boost(w.stat_boost_entry_index);
|
||||
phosg::fwrite_fmt(stream, " 00{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:04X} {:5} {:5} {:5} {:5} {:5} {:5} {:3} {:02X} {:02X} {:3} {:02X}({:02X}:{:04X},{:02X}:{:04X}) {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:2}* {} {} {}\n",
|
||||
phosg::fwrite_fmt(stream, " 00{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:04X} {:5} {:5} {:5} {:5} {:5} {:5} {:3} {:02X} {:02X} {:3} {:02X}({:02X}:{:04X},{:02X}:{:04X}) {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}{:02X}{:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:2}* {} {} {}\n",
|
||||
data1_1,
|
||||
data1_2,
|
||||
w.base.id,
|
||||
@@ -728,9 +728,9 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
w.trail2_x,
|
||||
w.trail2_y,
|
||||
w.color,
|
||||
w.unknown_a1,
|
||||
w.unknown_a2,
|
||||
w.unknown_a3,
|
||||
w.unknown_a1[0],
|
||||
w.unknown_a1[1],
|
||||
w.unknown_a1[2],
|
||||
w.unknown_a4,
|
||||
w.unknown_a5,
|
||||
w.tech_boost,
|
||||
|
||||
@@ -4,6 +4,41 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
template <>
|
||||
ServerDropMode phosg::enum_for_name<ServerDropMode>(const char* name) {
|
||||
if (!strcmp(name, "DISABLED")) {
|
||||
return ServerDropMode::DISABLED;
|
||||
} else if (!strcmp(name, "CLIENT")) {
|
||||
return ServerDropMode::CLIENT;
|
||||
} else if (!strcmp(name, "SERVER_SHARED")) {
|
||||
return ServerDropMode::SERVER_SHARED;
|
||||
} else if (!strcmp(name, "SERVER_PRIVATE")) {
|
||||
return ServerDropMode::SERVER_PRIVATE;
|
||||
} else if (!strcmp(name, "SERVER_DUPLICATE")) {
|
||||
return ServerDropMode::SERVER_DUPLICATE;
|
||||
} else {
|
||||
throw runtime_error("invalid drop mode");
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
const char* phosg::name_for_enum<ServerDropMode>(ServerDropMode value) {
|
||||
switch (value) {
|
||||
case ServerDropMode::DISABLED:
|
||||
return "DISABLED";
|
||||
case ServerDropMode::CLIENT:
|
||||
return "CLIENT";
|
||||
case ServerDropMode::SERVER_SHARED:
|
||||
return "SERVER_SHARED";
|
||||
case ServerDropMode::SERVER_PRIVATE:
|
||||
return "SERVER_PRIVATE";
|
||||
case ServerDropMode::SERVER_DUPLICATE:
|
||||
return "SERVER_DUPLICATE";
|
||||
default:
|
||||
throw runtime_error("invalid drop mode");
|
||||
}
|
||||
}
|
||||
|
||||
ItemParameterTable::ItemParameterTable(shared_ptr<const string> data, Version version)
|
||||
: version(version),
|
||||
data(data),
|
||||
@@ -203,8 +238,6 @@ ItemParameterTable::WeaponV4 ItemParameterTable::WeaponGCNTE::to_v4() const {
|
||||
ret.trail2_y = this->trail2_y;
|
||||
ret.color = this->color;
|
||||
ret.unknown_a1 = this->unknown_a1;
|
||||
ret.unknown_a2 = this->unknown_a2;
|
||||
ret.unknown_a3 = this->unknown_a3;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -233,8 +266,6 @@ ItemParameterTable::WeaponV4 ItemParameterTable::WeaponV3T<BE>::to_v4() const {
|
||||
ret.trail2_y = this->trail2_y;
|
||||
ret.color = this->color;
|
||||
ret.unknown_a1 = this->unknown_a1;
|
||||
ret.unknown_a2 = this->unknown_a2;
|
||||
ret.unknown_a3 = this->unknown_a3;
|
||||
ret.unknown_a4 = this->unknown_a4;
|
||||
ret.unknown_a5 = this->unknown_a5;
|
||||
ret.tech_boost = this->tech_boost;
|
||||
|
||||
+26
-10
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "WindowsPlatform.hh"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <array>
|
||||
@@ -16,6 +18,26 @@
|
||||
#include "Types.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
// TODO: These don't really belong here, but putting them anywhere else creates
|
||||
// annoying dependency cycles. Find or make a better place for these.
|
||||
enum class ServerDropMode {
|
||||
DISABLED = 0,
|
||||
CLIENT = 1, // Not allowed for BB games
|
||||
SERVER_SHARED = 2,
|
||||
SERVER_PRIVATE = 3,
|
||||
SERVER_DUPLICATE = 4,
|
||||
};
|
||||
enum class ProxyDropMode {
|
||||
DISABLED = 0,
|
||||
PASSTHROUGH,
|
||||
INTERCEPT,
|
||||
};
|
||||
|
||||
template <>
|
||||
ServerDropMode phosg::enum_for_name<ServerDropMode>(const char* name);
|
||||
template <>
|
||||
const char* phosg::name_for_enum<ServerDropMode>(ServerDropMode value);
|
||||
|
||||
class ItemParameterTable {
|
||||
public:
|
||||
// TODO: This implementation is ugly. We should use real classes and virtual
|
||||
@@ -98,9 +120,7 @@ public:
|
||||
/* 1E */ int8_t trail2_x = 0;
|
||||
/* 1F */ int8_t trail2_y = 0;
|
||||
/* 20 */ int8_t color = 0;
|
||||
/* 21 */ uint8_t unknown_a1 = 0;
|
||||
/* 22 */ uint8_t unknown_a2 = 0;
|
||||
/* 23 */ uint8_t unknown_a3 = 0;
|
||||
/* 21 */ parray<uint8_t, 3> unknown_a1 = 0;
|
||||
/* 24 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
@@ -127,9 +147,7 @@ public:
|
||||
/* 1E */ int8_t trail2_x = 0;
|
||||
/* 1F */ int8_t trail2_y = 0;
|
||||
/* 20 */ int8_t color = 0;
|
||||
/* 21 */ uint8_t unknown_a1 = 0;
|
||||
/* 22 */ uint8_t unknown_a2 = 0;
|
||||
/* 23 */ uint8_t unknown_a3 = 0;
|
||||
/* 21 */ parray<uint8_t, 3> unknown_a1 = 0;
|
||||
/* 24 */ uint8_t unknown_a4 = 0;
|
||||
/* 25 */ uint8_t unknown_a5 = 0;
|
||||
/* 26 */ uint8_t tech_boost = 0;
|
||||
@@ -169,9 +187,7 @@ public:
|
||||
/* 22 */ int8_t trail2_x = 0;
|
||||
/* 23 */ int8_t trail2_y = 0;
|
||||
/* 24 */ int8_t color = 0;
|
||||
/* 25 */ uint8_t unknown_a1 = 0;
|
||||
/* 26 */ uint8_t unknown_a2 = 0;
|
||||
/* 27 */ uint8_t unknown_a3 = 0;
|
||||
/* 25 */ parray<uint8_t, 3> unknown_a1 = 0;
|
||||
/* 28 */ uint8_t unknown_a4 = 0;
|
||||
/* 29 */ uint8_t unknown_a5 = 0;
|
||||
/* 2A */ uint8_t tech_boost = 0;
|
||||
@@ -669,7 +685,7 @@ public:
|
||||
// This specifies which entry in ItemMagMotion.dat is used. The file is
|
||||
// just a list of 0x64-byte structures. 0xFF = no TItemMagSub is created
|
||||
uint8_t motion_table_entry = 0xFF;
|
||||
parray<uint8_t, 5> unknown_a1;
|
||||
parray<uint8_t, 5> unknown_a1 = 0;
|
||||
} __packed_ws__(Side, 0x06);
|
||||
parray<Side, 2> sides; // [0] = right side, [1] = left side
|
||||
} __packed_ws__(MotionReference, 0x0C);
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<RandomG
|
||||
bool is_v3_or_later = is_v3(c->version()) || is_v4;
|
||||
bool should_delete_item = is_v3_or_later;
|
||||
|
||||
auto player = c->character();
|
||||
auto player = c->character_file();
|
||||
auto& item = player->inventory.items[item_index];
|
||||
uint32_t primary_identifier = item.data.primary_identifier();
|
||||
|
||||
@@ -47,7 +47,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<RandomG
|
||||
weapon.data.data1[3] = min<uint8_t>(weapon.data.data1[3] + item.data.data1[2] + 1, weapon_def.max_grind);
|
||||
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x030B0000) { // Material
|
||||
auto p = c->character();
|
||||
auto p = c->character_file();
|
||||
|
||||
using Type = PSOBBCharacterFile::MaterialType;
|
||||
Type type;
|
||||
@@ -499,7 +499,7 @@ void apply_mag_feed_result(
|
||||
|
||||
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index) {
|
||||
auto s = c->require_server_state();
|
||||
auto player = c->character();
|
||||
auto player = c->character_file();
|
||||
apply_mag_feed_result(
|
||||
player->inventory.items[mag_item_index].data,
|
||||
player->inventory.items[fed_item_index].data,
|
||||
|
||||
+34
-102
@@ -154,12 +154,12 @@ Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id, bool is_game)
|
||||
episode(Episode::NONE),
|
||||
mode(GameMode::NORMAL),
|
||||
difficulty(0),
|
||||
base_exp_multiplier(1),
|
||||
exp_share_multiplier(0.5),
|
||||
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(DropMode::CLIENT),
|
||||
drop_mode(ServerDropMode::CLIENT),
|
||||
event(0),
|
||||
block(0),
|
||||
leader_id(0),
|
||||
@@ -186,6 +186,14 @@ void Lobby::reset_next_item_ids() {
|
||||
this->next_game_item_id = 0xCC000000;
|
||||
}
|
||||
|
||||
uint8_t Lobby::area_for_floor(Version version, uint8_t floor) const {
|
||||
if (this->quest) {
|
||||
return this->quest->meta.area_for_floor.at(floor);
|
||||
}
|
||||
auto sdt = this->require_server_state()->set_data_table(version, this->episode, this->mode, this->difficulty);
|
||||
return sdt->default_area_for_floor(this->episode, floor);
|
||||
}
|
||||
|
||||
shared_ptr<ServerState> Lobby::require_server_state() const {
|
||||
auto s = this->server_state.lock();
|
||||
if (!s) {
|
||||
@@ -214,41 +222,6 @@ void Lobby::create_item_creator(Version logic_version) {
|
||||
logic_version = leader_c ? leader_c->version() : Version::BB_V4;
|
||||
}
|
||||
|
||||
shared_ptr<const RareItemSet> rare_item_set;
|
||||
shared_ptr<const CommonItemSet> common_item_set;
|
||||
switch (logic_version) {
|
||||
case Version::PC_PATCH:
|
||||
case Version::BB_PATCH:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
throw runtime_error("cannot create item creator for this base version");
|
||||
case Version::DC_NTE:
|
||||
case Version::DC_11_2000:
|
||||
case Version::DC_V1:
|
||||
// TODO: We should probably have a v1 common item set at some point too
|
||||
common_item_set = s->common_item_set_v2;
|
||||
rare_item_set = s->rare_item_sets.at("rare-table-v1");
|
||||
break;
|
||||
case Version::DC_V2:
|
||||
case Version::PC_NTE:
|
||||
case Version::PC_V2:
|
||||
common_item_set = s->common_item_set_v2;
|
||||
rare_item_set = s->rare_item_sets.at("rare-table-v2");
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::XB_V3:
|
||||
common_item_set = s->common_item_set_v3_v4;
|
||||
rare_item_set = s->rare_item_sets.at("rare-table-v3");
|
||||
break;
|
||||
case Version::BB_V4:
|
||||
common_item_set = s->common_item_set_v3_v4;
|
||||
rare_item_set = s->rare_item_sets.at("rare-table-v4");
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid lobby base version");
|
||||
}
|
||||
|
||||
shared_ptr<RandomGenerator> rand_crypt;
|
||||
if (s->use_psov2_rand_crypt) {
|
||||
rand_crypt = make_shared<PSOV2Encryption>(this->rand_crypt->seed());
|
||||
@@ -256,8 +229,8 @@ void Lobby::create_item_creator(Version logic_version) {
|
||||
rand_crypt = make_shared<MT19937Generator>(this->rand_crypt->seed());
|
||||
}
|
||||
this->item_creator = make_shared<ItemCreator>(
|
||||
common_item_set,
|
||||
rare_item_set,
|
||||
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),
|
||||
@@ -269,7 +242,7 @@ void Lobby::create_item_creator(Version logic_version) {
|
||||
this->difficulty,
|
||||
this->effective_section_id(),
|
||||
rand_crypt,
|
||||
this->quest ? this->quest->battle_rules : nullptr);
|
||||
this->quest ? this->quest->meta.battle_rules : nullptr);
|
||||
}
|
||||
|
||||
uint8_t Lobby::effective_section_id() const {
|
||||
@@ -281,7 +254,7 @@ uint8_t Lobby::effective_section_id() const {
|
||||
}
|
||||
auto leader = this->clients.at(this->leader_id);
|
||||
if (leader) {
|
||||
return leader->character()->disp.visual.section_id;
|
||||
return leader->character_file()->disp.visual.section_id;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -309,24 +282,16 @@ void Lobby::load_maps() {
|
||||
auto rare_rates = this->rare_enemy_rates ? this->rare_enemy_rates : MapState::DEFAULT_RARE_ENEMIES;
|
||||
|
||||
if (this->quest) {
|
||||
this->log.info_f("Loading quest supermap");
|
||||
auto supermap = this->quest->get_supermap(this->random_seed);
|
||||
this->map_state = make_shared<MapState>(
|
||||
this->lobby_id,
|
||||
this->difficulty,
|
||||
this->event,
|
||||
this->random_seed,
|
||||
this->rare_enemy_rates,
|
||||
this->rand_crypt,
|
||||
this->quest->get_supermap(this->random_seed));
|
||||
this->lobby_id, this->difficulty, this->event, this->random_seed, this->rare_enemy_rates, this->rand_crypt, supermap);
|
||||
} else {
|
||||
this->log.info_f("Loading free play supermaps");
|
||||
auto s = this->require_server_state();
|
||||
auto supermaps = s->supermaps_for_variations(this->episode, this->mode, this->difficulty, this->variations);
|
||||
this->map_state = make_shared<MapState>(
|
||||
this->lobby_id,
|
||||
this->difficulty,
|
||||
this->event,
|
||||
this->random_seed,
|
||||
this->rare_enemy_rates,
|
||||
this->rand_crypt,
|
||||
s->supermaps_for_variations(this->episode, this->mode, this->difficulty, this->variations));
|
||||
this->lobby_id, this->difficulty, this->event, this->random_seed, this->rare_enemy_rates, this->rand_crypt, supermaps);
|
||||
}
|
||||
|
||||
if (this->check_flag(Lobby::Flag::DEBUG)) {
|
||||
@@ -500,7 +465,7 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
|
||||
|
||||
// If the lobby is recording a battle record, add the player join event
|
||||
if (this->battle_record) {
|
||||
auto p = c->character();
|
||||
auto p = c->character_file();
|
||||
PlayerLobbyDataDCGC lobby_data;
|
||||
lobby_data.player_tag = 0x00010000;
|
||||
lobby_data.guild_card_number = c->login->account->account_id;
|
||||
@@ -642,7 +607,7 @@ shared_ptr<Client> Lobby::find_client(const string* identifier, uint64_t account
|
||||
if (account_id && lc->login && (lc->login->account->account_id == account_id)) {
|
||||
return lc;
|
||||
}
|
||||
if (identifier && (lc->character()->disp.name.eq(*identifier, lc->language()))) {
|
||||
if (identifier && (lc->character_file()->disp.name.eq(*identifier, lc->language()))) {
|
||||
return lc;
|
||||
}
|
||||
}
|
||||
@@ -679,7 +644,7 @@ Lobby::JoinError Lobby::join_error_for_client(std::shared_ptr<Client> c, const s
|
||||
if (password && !this->password.empty() && (*password != this->password)) {
|
||||
return JoinError::INCORRECT_PASSWORD;
|
||||
}
|
||||
auto p = c->character();
|
||||
auto p = c->character_file();
|
||||
if (p->disp.stats.level < this->min_level) {
|
||||
return JoinError::LEVEL_TOO_LOW;
|
||||
}
|
||||
@@ -773,7 +738,7 @@ void Lobby::on_item_id_generated_externally(uint32_t item_id) {
|
||||
}
|
||||
|
||||
void Lobby::assign_inventory_and_bank_item_ids(shared_ptr<Client> c, bool consume_ids) {
|
||||
auto p = c->character();
|
||||
auto p = c->character_file();
|
||||
uint32_t orig_next_item_id = this->next_item_id_for_client.at(c->lobby_client_id);
|
||||
for (size_t z = 0; z < p->inventory.num_items; z++) {
|
||||
p->inventory.items[z].data.id = this->generate_item_id(c->lobby_client_id);
|
||||
@@ -784,13 +749,15 @@ void Lobby::assign_inventory_and_bank_item_ids(shared_ptr<Client> c, bool consum
|
||||
|
||||
if (c->log.info_f("Assigned inventory item IDs{}", consume_ids ? "" : " but did not mark IDs as used")) {
|
||||
c->print_inventory();
|
||||
auto& bank = c->current_bank();
|
||||
if (p->bank.num_items) {
|
||||
bank.assign_ids(0x99000000 + (c->lobby_client_id << 20));
|
||||
c->log.info_f("Assigned bank item IDs");
|
||||
c->print_bank();
|
||||
} else {
|
||||
c->log.info_f("Bank is empty");
|
||||
if ((c->version() == Version::BB_V4) && !c->has_overlay()) {
|
||||
auto bank = c->bank_file();
|
||||
if (!bank->items.empty()) {
|
||||
bank->assign_ids(0x99000000 + (c->lobby_client_id << 20));
|
||||
c->log.info_f("Assigned bank item IDs");
|
||||
c->print_bank();
|
||||
} else {
|
||||
c->log.info_f("Bank is empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -884,38 +851,3 @@ bool Lobby::compare_shared(const shared_ptr<const Lobby>& a, const shared_ptr<co
|
||||
|
||||
return a->name < b->name;
|
||||
}
|
||||
|
||||
template <>
|
||||
Lobby::DropMode phosg::enum_for_name<Lobby::DropMode>(const char* name) {
|
||||
if (!strcmp(name, "DISABLED")) {
|
||||
return Lobby::DropMode::DISABLED;
|
||||
} else if (!strcmp(name, "CLIENT")) {
|
||||
return Lobby::DropMode::CLIENT;
|
||||
} else if (!strcmp(name, "SERVER_SHARED")) {
|
||||
return Lobby::DropMode::SERVER_SHARED;
|
||||
} else if (!strcmp(name, "SERVER_PRIVATE")) {
|
||||
return Lobby::DropMode::SERVER_PRIVATE;
|
||||
} else if (!strcmp(name, "SERVER_DUPLICATE")) {
|
||||
return Lobby::DropMode::SERVER_DUPLICATE;
|
||||
} else {
|
||||
throw runtime_error("invalid drop mode");
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
const char* phosg::name_for_enum<Lobby::DropMode>(Lobby::DropMode value) {
|
||||
switch (value) {
|
||||
case Lobby::DropMode::DISABLED:
|
||||
return "DISABLED";
|
||||
case Lobby::DropMode::CLIENT:
|
||||
return "CLIENT";
|
||||
case Lobby::DropMode::SERVER_SHARED:
|
||||
return "SERVER_SHARED";
|
||||
case Lobby::DropMode::SERVER_PRIVATE:
|
||||
return "SERVER_PRIVATE";
|
||||
case Lobby::DropMode::SERVER_DUPLICATE:
|
||||
return "SERVER_DUPLICATE";
|
||||
default:
|
||||
throw runtime_error("invalid drop mode");
|
||||
}
|
||||
}
|
||||
|
||||
+6
-11
@@ -90,13 +90,6 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
IS_OVERFLOW = 0x08000000,
|
||||
// clang-format on
|
||||
};
|
||||
enum class DropMode {
|
||||
DISABLED = 0,
|
||||
CLIENT = 1, // Not allowed for BB games
|
||||
SERVER_SHARED = 2,
|
||||
SERVER_PRIVATE = 3,
|
||||
SERVER_DUPLICATE = 4,
|
||||
};
|
||||
|
||||
std::weak_ptr<ServerState> server_state;
|
||||
phosg::PrefixedLogger log;
|
||||
@@ -127,7 +120,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
Episode episode;
|
||||
GameMode mode;
|
||||
uint8_t difficulty; // 0-3
|
||||
uint16_t base_exp_multiplier;
|
||||
float base_exp_multiplier;
|
||||
float exp_share_multiplier;
|
||||
float challenge_exp_multiplier;
|
||||
std::string password;
|
||||
@@ -136,7 +129,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
uint32_t random_seed;
|
||||
std::shared_ptr<RandomGenerator> rand_crypt;
|
||||
uint8_t allowed_drop_modes;
|
||||
DropMode drop_mode;
|
||||
ServerDropMode drop_mode;
|
||||
std::shared_ptr<ItemCreator> item_creator; // Always null for lobbies, never null for games
|
||||
|
||||
struct ChallengeParameters {
|
||||
@@ -208,6 +201,8 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
this->enabled_flags ^= static_cast<uint32_t>(flag);
|
||||
}
|
||||
|
||||
uint8_t area_for_floor(Version version, uint8_t floor) const;
|
||||
|
||||
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);
|
||||
@@ -291,6 +286,6 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
};
|
||||
|
||||
template <>
|
||||
Lobby::DropMode phosg::enum_for_name<Lobby::DropMode>(const char* name);
|
||||
ServerDropMode phosg::enum_for_name<ServerDropMode>(const char* name);
|
||||
template <>
|
||||
const char* phosg::name_for_enum<Lobby::DropMode>(Lobby::DropMode value);
|
||||
const char* phosg::name_for_enum<ServerDropMode>(ServerDropMode value);
|
||||
|
||||
+45
-15
@@ -1195,11 +1195,24 @@ Action a_decode_bitmap_font(
|
||||
decode-bitmap-font --width=WIDTH [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
||||
Decode a 2-bit bitmap font file (.fon) into a BMP image. The --width\n\
|
||||
option is required; if the output looks wrong, try increasing or\n\
|
||||
decreasing this number. For S18all04.fon, the width should be 20.\n",
|
||||
decreasing this number. For S18all04.fon, the width should be 20. If\n\
|
||||
--show-unused is given, highlights the unused ares of ISO8859 characters\n\
|
||||
in red.\n",
|
||||
+[](phosg::Arguments& args) {
|
||||
std::string data = read_input_data(args);
|
||||
size_t width = args.get<size_t>("width");
|
||||
std::string bmp_data = decode_fon(data, width).serialize(phosg::ImageFormat::WINDOWS_BITMAP);
|
||||
phosg::Image res = decode_fon(data, width);
|
||||
if (width == 20 && args.get<bool>("show-unused")) {
|
||||
static const array<uint8_t, 0xBF> iso8859_widths{7, 9, 13, 11, 15, 14, 7, 8, 8, 11, 11, 7, 11, 7, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 7, 7, 9, 11, 9, 10, 15, 13, 12, 13, 12, 11, 11, 13, 12, 8, 11, 12, 11, 15, 12, 13, 11, 13, 12, 11, 13, 12, 13, 15, 12, 13, 11, 8, 11, 8, 8, 9, 8, 12, 11, 12, 11, 12, 10, 12, 11, 6, 9, 11, 6, 14, 11, 12, 11, 11, 9, 11, 10, 11, 12, 15, 11, 11, 11, 9, 8, 9, 9, 9, 12, 7, 10, 13, 10, 10, 7, 10, 8, 17, 9, 12, 11, 9, 17, 9, 7, 11, 8, 8, 8, 11, 11, 8, 7, 6, 9, 12, 13, 13, 13, 10, 13, 13, 13, 13, 13, 13, 17, 13, 11, 11, 11, 11, 8, 8, 8, 8, 12, 12, 13, 13, 13, 13, 13, 11, 13, 12, 12, 12, 12, 15, 11, 10, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 6, 6, 6, 6, 11, 11, 12, 12, 12, 12, 12, 11, 12, 11, 11, 11, 11, 11, 11, 11};
|
||||
for (size_t z = 0; z < iso8859_widths.size(); z++) {
|
||||
for (size_t y = (z + 1) * 0x12; y < (z + 2) * 0x12; y++) {
|
||||
for (size_t x = iso8859_widths.at(z); x < width; x++) {
|
||||
res.write(x, y, 0xFF0000FF);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
string bmp_data = res.serialize(phosg::ImageFormat::WINDOWS_BITMAP);
|
||||
write_output_data(args, bmp_data.data(), bmp_data.size(), "bmp");
|
||||
});
|
||||
Action a_encode_bitmap_font(
|
||||
@@ -1892,7 +1905,7 @@ Action a_decode_text_archive(
|
||||
ts = make_unique<BinaryTextAndKeyboardsSet>(data, args.get<bool>("big-endian"), is_sjis);
|
||||
}
|
||||
phosg::JSON j = ts->json();
|
||||
string out_data = j.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY);
|
||||
string out_data = j.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY | phosg::JSON::SerializeOption::EXPAND_LEAF_CONTAINERS);
|
||||
write_output_data(args, out_data.data(), out_data.size(), "json");
|
||||
});
|
||||
Action a_encode_text_archive(
|
||||
@@ -1932,7 +1945,7 @@ Action a_decode_unicode_text_set(
|
||||
"decode-unicode-text-set", nullptr, +[](phosg::Arguments& args) {
|
||||
UnicodeTextSet uts(read_input_data(args));
|
||||
phosg::JSON j = uts.json();
|
||||
string out_data = j.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY);
|
||||
string out_data = j.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY | phosg::JSON::SerializeOption::EXPAND_LEAF_CONTAINERS);
|
||||
write_output_data(args, out_data.data(), out_data.size(), "json");
|
||||
});
|
||||
Action a_encode_unicode_text_set(
|
||||
@@ -2153,16 +2166,20 @@ Action a_convert_rare_item_set(
|
||||
}
|
||||
});
|
||||
|
||||
static shared_ptr<CommonItemSet> load_common_item_set(const std::string& filename, bool big_endian) {
|
||||
static shared_ptr<CommonItemSet> load_common_item_set(
|
||||
const std::string& filename, const std::string& ct_filename, bool big_endian) {
|
||||
auto data = make_shared<string>(phosg::load_file(filename));
|
||||
if (filename.ends_with(".json")) {
|
||||
return make_shared<JSONCommonItemSet>(phosg::JSON::parse(*data));
|
||||
} else if (filename.ends_with(".afs")) {
|
||||
auto ct_data = make_shared<string>(phosg::load_file(ct_filename));
|
||||
return make_shared<AFSV2CommonItemSet>(data, ct_data);
|
||||
} else if (filename.ends_with(".gsl")) {
|
||||
return make_shared<GSLV3V4CommonItemSet>(data, big_endian);
|
||||
} else if (filename.ends_with(".gslb")) {
|
||||
return make_shared<GSLV3V4CommonItemSet>(data, true);
|
||||
} else {
|
||||
throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, or .gslb");
|
||||
throw runtime_error("cannot determine input format; use a filename ending with .json, .afs, .gsl, or .gslb");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2173,6 +2190,7 @@ Action a_convert_common_item_set(
|
||||
OUTPUT-FILENAME or stdout. The input filename must end in one of the\n\
|
||||
following extensions:\n\
|
||||
.json (newserv JSON common item table)\n\
|
||||
.afs (PSO v2 AFS archive; --ct-filename is required in this case)\n\
|
||||
.gsl (PSO BB little-endian GSL archive)\n\
|
||||
.gslb (PSO GC big-endian GSL archive)\n",
|
||||
+[](phosg::Arguments& args) {
|
||||
@@ -2181,7 +2199,7 @@ Action a_convert_common_item_set(
|
||||
throw runtime_error("input filename must be given");
|
||||
}
|
||||
|
||||
auto cs = load_common_item_set(input_filename, args.get<bool>("big-endian"));
|
||||
auto cs = load_common_item_set(input_filename, args.get<string>("ct-filename", false), args.get<bool>("big-endian"));
|
||||
const string& output_filename = args.get<string>(2, false);
|
||||
if (output_filename.empty()) {
|
||||
cs->print(stdout);
|
||||
@@ -2203,8 +2221,8 @@ Action a_compare_common_item_set(
|
||||
throw runtime_error("two input filenames must be given");
|
||||
}
|
||||
|
||||
auto cs1 = load_common_item_set(input_filename1, args.get<bool>("big-endian1"));
|
||||
auto cs2 = load_common_item_set(input_filename2, args.get<bool>("big-endian2"));
|
||||
auto cs1 = load_common_item_set(input_filename1, args.get<string>("ct-filename1", false), args.get<bool>("big-endian1"));
|
||||
auto cs2 = load_common_item_set(input_filename2, args.get<string>("ct-filename2", false), args.get<bool>("big-endian2"));
|
||||
cs1->print_diff(stdout, *cs2);
|
||||
});
|
||||
|
||||
@@ -2783,10 +2801,9 @@ Action a_show_ep3_maps(
|
||||
s->load_ep3_cards();
|
||||
s->load_ep3_maps();
|
||||
|
||||
auto map_ids = s->ep3_map_index->all_numbers();
|
||||
const auto& map_ids = s->ep3_map_index->all();
|
||||
phosg::log_info_f("{} maps", map_ids.size());
|
||||
for (uint32_t map_id : map_ids) {
|
||||
auto map = s->ep3_map_index->for_number(map_id);
|
||||
for (const auto& [map_number, map] : map_ids) {
|
||||
const auto& vms = map->all_versions();
|
||||
for (size_t language = 0; language < vms.size(); language++) {
|
||||
if (!vms[language]) {
|
||||
@@ -2910,7 +2927,7 @@ Action a_check_supermaps(
|
||||
|
||||
SuperMap::EfficiencyStats all_quests_eff;
|
||||
uint32_t random_seed = args.get<uint32_t>("random-seed", 0, phosg::Arguments::IntFormat::HEX);
|
||||
for (const auto& it : s->default_quest_index->quests_by_number) {
|
||||
for (const auto& it : s->quest_index->quests_by_number) {
|
||||
auto supermap = it.second->get_supermap(random_seed);
|
||||
if (!supermap) {
|
||||
throw logic_error("quest does not have a supermap, even with a specified random seed");
|
||||
@@ -2920,7 +2937,7 @@ Action a_check_supermaps(
|
||||
if (save_disassembly) {
|
||||
string filename = std::format("supermap_quest_{}_{:08X}.txt", it.first, random_seed);
|
||||
auto f = phosg::fopen_unique(filename, "wt");
|
||||
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->name);
|
||||
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->meta.name);
|
||||
supermap->print(f.get());
|
||||
filename_token = " => " + filename;
|
||||
}
|
||||
@@ -2931,7 +2948,7 @@ Action a_check_supermaps(
|
||||
}
|
||||
string filename = std::format("supermap_quest_{}_{:08X}_enemy_counts.txt", it.first, random_seed);
|
||||
auto f = phosg::fopen_unique(filename, "wt");
|
||||
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->name);
|
||||
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->meta.name);
|
||||
phosg::fwrite_fmt(f.get(), "ENEMY--------------- DCNTE 11/2K DC-V1 DC-V2 PCNTE PC-V2 GCNTE GC-V3 XB-V3 BB-V4\n");
|
||||
for (size_t type_ss = 0; type_ss < static_cast<size_t>(EnemyType::MAX_ENEMY_TYPE); type_ss++) {
|
||||
EnemyType type = static_cast<EnemyType>(type_ss);
|
||||
@@ -2987,6 +3004,19 @@ Action a_check_supermaps(
|
||||
phosg::fwrite_fmt(stderr, "ALL QUEST MAPS: {}\n", all_quests_eff_str);
|
||||
});
|
||||
|
||||
Action a_check_quests(
|
||||
"check-quests", nullptr,
|
||||
+[](phosg::Arguments& args) {
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->is_debug = true;
|
||||
s->load_config_early();
|
||||
s->clear_file_caches();
|
||||
s->load_patch_indexes();
|
||||
s->load_set_data_tables();
|
||||
s->load_maps();
|
||||
s->load_quest_index();
|
||||
});
|
||||
|
||||
Action a_parse_object_graph(
|
||||
"parse-object-graph", nullptr, +[](phosg::Arguments& args) {
|
||||
uint32_t root_object_address = args.get<uint32_t>("root", phosg::Arguments::IntFormat::HEX);
|
||||
|
||||
+24
-10
@@ -67,7 +67,7 @@ vector<string> SetDataTableBase::map_filenames_for_variations(
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const {
|
||||
uint8_t SetDataTableBase::default_area_for_floor(Version version, Episode episode, uint8_t floor) {
|
||||
// For some inscrutable reason, Pioneer 2's area number in Episode 4 is
|
||||
// discontiguous with all the rest. Why, Sega??
|
||||
static const array<uint8_t, 0x12> areas_ep1 = {
|
||||
@@ -82,7 +82,7 @@ uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor)
|
||||
case Episode::EP1:
|
||||
return areas_ep1.at(floor);
|
||||
case Episode::EP2: {
|
||||
const auto& areas = ((this->version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2);
|
||||
const auto& areas = ((version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2);
|
||||
return areas.at(floor);
|
||||
}
|
||||
case Episode::EP4:
|
||||
@@ -92,6 +92,10 @@ uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor)
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const {
|
||||
return this->default_area_for_floor(this->version, episode, floor);
|
||||
}
|
||||
|
||||
SetDataTable::SetDataTable(Version version, const string& data) : SetDataTableBase(version) {
|
||||
if (is_big_endian(this->version)) {
|
||||
this->load_table_t<true>(data);
|
||||
@@ -2007,8 +2011,18 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
||||
{0x018C, F_V3_V4, 0x0000400000008000, "TObjParticleLobby"},
|
||||
{0x018C, F_EP3, 0x0000000000008000, "TObjParticleLobby"},
|
||||
|
||||
// Episode 3 lobby battle table. Params:
|
||||
// param4 = player count (1-4); only 2 and 4 are used in-game
|
||||
// Episode 3 lobby battle table. This object is responsible for the red
|
||||
// panels on the floor next to the battle table that turn green when you
|
||||
// step on them; it also shows the confirmation window and sends the
|
||||
// necessary commands to the server. The actual table model and the non-lit
|
||||
// parts of the floor panels are part of the lobby geometry, not this
|
||||
// object. Params:
|
||||
// param4 = player count
|
||||
// 1 = 1 player (doesn't work properly - there's no way to confirm)
|
||||
// 2 = 2 players
|
||||
// 3 = 3 players (unused, but works)
|
||||
// 4 = 4 players
|
||||
// anything else = object doesn't load
|
||||
// param5 = table number (used in E4 and E5 commands)
|
||||
{0x018D, F_EP3, 0x0000000000008000, "TObjLobbyTable"},
|
||||
|
||||
@@ -4038,12 +4052,12 @@ string MapFile::disassemble_action_stream(const void* data, size_t size) {
|
||||
}
|
||||
case 0x0A: {
|
||||
uint16_t id = r.get_u16l();
|
||||
ret.emplace_back(std::format(" 0A {:04X} enable_switch_flag id={:04X}", id, id));
|
||||
ret.emplace_back(std::format(" 0A {:04X} set_switch_flag id={:04X}", id, id));
|
||||
break;
|
||||
}
|
||||
case 0x0B: {
|
||||
uint16_t id = r.get_u16l();
|
||||
ret.emplace_back(std::format(" 0B {:04X} disable_switch_flag id={:04X}", id, id));
|
||||
ret.emplace_back(std::format(" 0B {:04X} clear_switch_flag id={:04X}", id, id));
|
||||
break;
|
||||
}
|
||||
case 0x0C: {
|
||||
@@ -4885,8 +4899,8 @@ static size_t get_action_stream_size(const void* data, size_t size) {
|
||||
r.skip(4);
|
||||
done = (cmd == 0x0D);
|
||||
break;
|
||||
case 0x0A: // enable_switch_flag(uint16_t flag_num)
|
||||
case 0x0B: // disable_switch_flag(uint16_t flag_num)
|
||||
case 0x0A: // set_switch_flag(uint16_t flag_num)
|
||||
case 0x0B: // clear_switch_flag(uint16_t flag_num)
|
||||
r.skip(2);
|
||||
break;
|
||||
default:
|
||||
@@ -5823,7 +5837,7 @@ phosg::JSON MapState::RareEnemyRates::json() const {
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t MapState::RareEnemyRates::for_enemy_type(EnemyType type) const {
|
||||
uint32_t MapState::RareEnemyRates::get(EnemyType type) const {
|
||||
switch (type) {
|
||||
case EnemyType::HILDEBEAR:
|
||||
return this->hildeblue;
|
||||
@@ -6061,7 +6075,7 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptr<RandomGenerator
|
||||
auto rare_type = type_definition_for_enemy(type).rare_type(fc.super_map->get_episode(), this->event, ene->floor);
|
||||
if ((type == EnemyType::MERICARAND) || (rare_type != type)) {
|
||||
unordered_map<uint32_t, float> det_cache;
|
||||
uint32_t bb_rare_rate = this->bb_rare_rates->for_enemy_type(type);
|
||||
uint32_t bb_rare_rate = this->bb_rare_rates->get(type);
|
||||
for (Version v : ALL_NON_PATCH_VERSIONS) {
|
||||
// Skip this version if the enemy doesn't exist there
|
||||
uint16_t relative_enemy_index = ene->version(v).relative_enemy_index;
|
||||
|
||||
+2
-1
@@ -67,6 +67,7 @@ public:
|
||||
std::vector<std::string> map_filenames_for_variations(
|
||||
Episode episode, GameMode mode, const Variations& variations, FilenameType type) const;
|
||||
|
||||
static uint8_t default_area_for_floor(Version version, Episode episode, uint8_t floor);
|
||||
uint8_t default_area_for_floor(Episode episode, uint8_t floor) const;
|
||||
|
||||
protected:
|
||||
@@ -672,7 +673,7 @@ public:
|
||||
RareEnemyRates(uint32_t enemy_rate, uint32_t mericarand_rate, uint32_t boss_rate);
|
||||
explicit RareEnemyRates(const phosg::JSON& json);
|
||||
|
||||
uint32_t for_enemy_type(EnemyType type) const;
|
||||
uint32_t get(EnemyType type) const;
|
||||
|
||||
std::string str() const;
|
||||
phosg::JSON json() const;
|
||||
|
||||
@@ -21,6 +21,7 @@ constexpr uint32_t LOBBY = 0x33000033;
|
||||
constexpr uint32_t GAME = 0x44000044;
|
||||
constexpr uint32_t QUEST_EP1 = 0x55010155;
|
||||
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;
|
||||
|
||||
@@ -356,6 +356,36 @@ inline std::string decrypt_v2_registry_value(const std::string& s) {
|
||||
return decrypt_v2_registry_value(s.data(), s.size());
|
||||
}
|
||||
|
||||
template <bool BE>
|
||||
std::string decrypt_pr1_data(const void* data, size_t size) {
|
||||
if (size < 4) {
|
||||
throw std::runtime_error("not enough data for PR1 footer");
|
||||
}
|
||||
phosg::StringReader r(data, size);
|
||||
std::string ret = r.read(size - 4);
|
||||
PSOV2Encryption crypt(r.get<U32T<BE>>());
|
||||
if constexpr (BE) {
|
||||
crypt.encrypt_big_endian(ret.data(), ret.size());
|
||||
} else {
|
||||
crypt.decrypt(ret.data(), ret.size());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <bool BE>
|
||||
std::string encrypt_pr1_data(const void* data, size_t size, uint32_t seed) {
|
||||
phosg::StringWriter w;
|
||||
w.write(data, size);
|
||||
w.put<U32T<BE>>(seed);
|
||||
PSOV2Encryption crypt(seed);
|
||||
if constexpr (BE) {
|
||||
crypt.encrypt_big_endian(w.str().data(), size);
|
||||
} else {
|
||||
crypt.encrypt(w.str().data(), size);
|
||||
}
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
struct DecryptedPR2 {
|
||||
std::string compressed_data;
|
||||
size_t decompressed_size;
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
#include "PlayerFilesManager.hh"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <wchar.h>
|
||||
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Hash.hh>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "FileContentsCache.hh"
|
||||
#include "ItemData.hh"
|
||||
#include "Loggers.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
PlayerFilesManager::PlayerFilesManager(std::shared_ptr<asio::io_context> io_context)
|
||||
: io_context(io_context),
|
||||
clear_expired_files_timer(*this->io_context) {
|
||||
this->schedule_callback();
|
||||
}
|
||||
|
||||
std::shared_ptr<PSOBBBaseSystemFile> PlayerFilesManager::get_system(const std::string& filename) {
|
||||
try {
|
||||
return this->loaded_system_files.at(filename);
|
||||
} catch (const out_of_range&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<PSOBBCharacterFile> PlayerFilesManager::get_character(const std::string& filename) {
|
||||
try {
|
||||
return this->loaded_character_files.at(filename);
|
||||
} catch (const out_of_range&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<PSOBBGuildCardFile> PlayerFilesManager::get_guild_card(const std::string& filename) {
|
||||
try {
|
||||
return this->loaded_guild_card_files.at(filename);
|
||||
} catch (const out_of_range&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<PlayerBank200> PlayerFilesManager::get_bank(const std::string& filename) {
|
||||
try {
|
||||
return this->loaded_bank_files.at(filename);
|
||||
} catch (const out_of_range&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerFilesManager::set_system(const std::string& filename, std::shared_ptr<PSOBBBaseSystemFile> file) {
|
||||
if (!this->loaded_system_files.emplace(filename, file).second) {
|
||||
throw runtime_error("Guild Card file already loaded: " + filename);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerFilesManager::set_character(const std::string& filename, std::shared_ptr<PSOBBCharacterFile> file) {
|
||||
if (!this->loaded_character_files.emplace(filename, file).second) {
|
||||
throw runtime_error("character file already loaded: " + filename);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerFilesManager::set_guild_card(const std::string& filename, std::shared_ptr<PSOBBGuildCardFile> file) {
|
||||
if (!this->loaded_guild_card_files.emplace(filename, file).second) {
|
||||
throw runtime_error("Guild Card file already loaded: " + filename);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerFilesManager::set_bank(const std::string& filename, std::shared_ptr<PlayerBank200> file) {
|
||||
if (!this->loaded_bank_files.emplace(filename, file).second) {
|
||||
throw runtime_error("bank file already loaded: " + filename);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerFilesManager::schedule_callback() {
|
||||
this->clear_expired_files_timer.expires_after(std::chrono::seconds(30));
|
||||
this->clear_expired_files_timer.async_wait(bind(&PlayerFilesManager::clear_expired_files, this));
|
||||
}
|
||||
|
||||
template <typename KeyT, typename ValueT>
|
||||
size_t erase_unused(std::unordered_map<KeyT, std::shared_ptr<ValueT>>& m) {
|
||||
size_t ret = 0;
|
||||
for (auto it = m.begin(); it != m.end();) {
|
||||
if (it->second.use_count() <= 1) {
|
||||
it = m.erase(it);
|
||||
ret++;
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PlayerFilesManager::clear_expired_files() {
|
||||
size_t num_deleted = erase_unused(this->loaded_system_files);
|
||||
if (num_deleted) {
|
||||
player_data_log.info_f("Cleared {} expired system file(s)", num_deleted);
|
||||
}
|
||||
num_deleted = erase_unused(this->loaded_character_files);
|
||||
if (num_deleted) {
|
||||
player_data_log.info_f("Cleared {} expired character file(s)", num_deleted);
|
||||
}
|
||||
num_deleted = erase_unused(this->loaded_guild_card_files);
|
||||
if (num_deleted) {
|
||||
player_data_log.info_f("Cleared {} expired Guild Card file(s)", num_deleted);
|
||||
}
|
||||
num_deleted = erase_unused(this->loaded_bank_files);
|
||||
if (num_deleted) {
|
||||
player_data_log.info_f("Cleared {} expired bank file(s)", num_deleted);
|
||||
}
|
||||
|
||||
this->schedule_callback();
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include <array>
|
||||
#include <asio.hpp>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Episode3/DataIndexes.hh"
|
||||
#include "ItemCreator.hh"
|
||||
#include "ItemNameIndex.hh"
|
||||
#include "LevelTable.hh"
|
||||
#include "PlayerSubordinates.hh"
|
||||
#include "SaveFileFormats.hh"
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
class PlayerFilesManager {
|
||||
public:
|
||||
explicit PlayerFilesManager(std::shared_ptr<asio::io_context> io_context);
|
||||
~PlayerFilesManager() = default;
|
||||
|
||||
std::shared_ptr<PSOBBBaseSystemFile> get_system(const std::string& filename);
|
||||
std::shared_ptr<PSOBBCharacterFile> get_character(const std::string& filename);
|
||||
std::shared_ptr<PSOBBGuildCardFile> get_guild_card(const std::string& filename);
|
||||
std::shared_ptr<PlayerBank200> get_bank(const std::string& filename);
|
||||
|
||||
void set_system(const std::string& filename, std::shared_ptr<PSOBBBaseSystemFile> file);
|
||||
void set_character(const std::string& filename, std::shared_ptr<PSOBBCharacterFile> file);
|
||||
void set_guild_card(const std::string& filename, std::shared_ptr<PSOBBGuildCardFile> file);
|
||||
void set_bank(const std::string& filename, std::shared_ptr<PlayerBank200> file);
|
||||
|
||||
private:
|
||||
std::shared_ptr<asio::io_context> io_context;
|
||||
asio::steady_timer clear_expired_files_timer;
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<PSOBBBaseSystemFile>> loaded_system_files;
|
||||
std::unordered_map<std::string, std::shared_ptr<PSOBBCharacterFile>> loaded_character_files;
|
||||
std::unordered_map<std::string, std::shared_ptr<PSOBBGuildCardFile>> loaded_guild_card_files;
|
||||
std::unordered_map<std::string, std::shared_ptr<PlayerBank200>> loaded_bank_files;
|
||||
|
||||
void schedule_callback();
|
||||
void clear_expired_files();
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "PlayerInventory.hh"
|
||||
|
||||
void PlayerBank::load(FILE* f) {
|
||||
le_uint32_t num_items;
|
||||
le_uint32_t meseta;
|
||||
phosg::freadx(f, &num_items, sizeof(num_items));
|
||||
phosg::freadx(f, &meseta, sizeof(meseta));
|
||||
this->meseta = meseta;
|
||||
this->items.reserve(num_items);
|
||||
while (this->items.size() < num_items) {
|
||||
auto& item = this->items.emplace_back();
|
||||
phosg::freadx(f, &item, sizeof(item));
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerBank::save(FILE* f) const {
|
||||
le_uint32_t num_items = this->items.size();
|
||||
le_uint32_t meseta = this->meseta;
|
||||
phosg::fwritex(f, &num_items, sizeof(num_items));
|
||||
phosg::fwritex(f, &meseta, sizeof(meseta));
|
||||
for (const auto& item : this->items) {
|
||||
phosg::fwritex(f, &item, sizeof(item));
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t PlayerBank::bb_checksum() const {
|
||||
le_uint32_t num_items = this->items.size();
|
||||
le_uint32_t meseta = this->meseta;
|
||||
uint32_t ret = phosg::crc32(&num_items, sizeof(num_items));
|
||||
ret = phosg::crc32(&meseta, sizeof(meseta), ret);
|
||||
for (const auto& item : this->items) {
|
||||
ret = phosg::crc32(&item, sizeof(item), ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PlayerBank::add_item(const ItemData& item, const ItemData::StackLimits& limits) {
|
||||
uint32_t primary_identifier = item.primary_identifier();
|
||||
|
||||
if (primary_identifier == 0x04000000) {
|
||||
this->meseta += item.data2d;
|
||||
if (this->meseta > this->max_meseta) {
|
||||
this->meseta = this->max_meseta;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
size_t combine_max = item.max_stack_size(limits);
|
||||
if (combine_max > 1) {
|
||||
size_t y;
|
||||
for (y = 0; y < this->items.size(); y++) {
|
||||
if (this->items[y].data.primary_identifier() == primary_identifier) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (y < this->items.size()) {
|
||||
uint8_t new_count = this->items[y].data.data1[5] + item.data1[5];
|
||||
if (new_count > combine_max) {
|
||||
throw std::runtime_error("stack size would exceed limit");
|
||||
}
|
||||
this->items[y].data.data1[5] = new_count;
|
||||
this->items[y].amount = new_count;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->items.size() >= this->max_items) {
|
||||
throw std::runtime_error("no free space in bank");
|
||||
}
|
||||
|
||||
auto& new_item = this->items.emplace_back();
|
||||
new_item.data = item;
|
||||
new_item.amount = (item.max_stack_size(limits) > 1) ? item.data1[5] : 1;
|
||||
new_item.present = 1;
|
||||
}
|
||||
|
||||
ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) {
|
||||
size_t index = this->find_item(item_id);
|
||||
auto& bank_item = this->items[index];
|
||||
|
||||
ItemData ret = bank_item.data;
|
||||
if (amount && (bank_item.data.stack_size(limits) > 1) && (amount < bank_item.data.data1[5])) {
|
||||
ret.data1[5] = amount;
|
||||
bank_item.data.data1[5] -= amount;
|
||||
bank_item.amount -= amount;
|
||||
} else {
|
||||
this->items.erase(this->items.begin() + index);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t PlayerBank::find_item(uint32_t item_id) {
|
||||
for (size_t x = 0; x < this->items.size(); x++) {
|
||||
if (this->items[x].data.id == item_id) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw std::out_of_range("item not present");
|
||||
}
|
||||
|
||||
void PlayerBank::sort() {
|
||||
std::sort(this->items.begin(), this->items.end());
|
||||
}
|
||||
|
||||
void PlayerBank::assign_ids(uint32_t base_id) {
|
||||
for (size_t z = 0; z < this->items.size(); z++) {
|
||||
this->items[z].data.id = base_id + z;
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerBank::enforce_stack_limits(std::shared_ptr<const ItemData::StackLimits> stack_limits) {
|
||||
for (auto& item : this->items) {
|
||||
item.data.enforce_stack_size_limits(*stack_limits);
|
||||
}
|
||||
}
|
||||
+43
-95
@@ -315,95 +315,6 @@ struct PlayerBankT {
|
||||
/* 0008 */ parray<PlayerBankItemT<BE>, SlotCount> items;
|
||||
/* 05A8 for 60 items (v1/v2), 12C8 for 200 items (v3/v4) */
|
||||
|
||||
uint32_t checksum() const {
|
||||
return phosg::crc32(this, 2 * sizeof(U32T<BE>) + sizeof(PlayerBankItemT<BE>) * std::min<size_t>(SlotCount, this->num_items));
|
||||
}
|
||||
|
||||
void add_item(const ItemData& item, const ItemData::StackLimits& limits) {
|
||||
uint32_t primary_identifier = item.primary_identifier();
|
||||
|
||||
if (primary_identifier == 0x04000000) {
|
||||
this->meseta += item.data2d;
|
||||
if (this->meseta > 999999) {
|
||||
this->meseta = 999999;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
size_t combine_max = item.max_stack_size(limits);
|
||||
if (combine_max > 1) {
|
||||
size_t y;
|
||||
for (y = 0; y < this->num_items; y++) {
|
||||
if (this->items[y].data.primary_identifier() == primary_identifier) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (y < this->num_items) {
|
||||
uint8_t new_count = this->items[y].data.data1[5] + item.data1[5];
|
||||
if (new_count > combine_max) {
|
||||
throw std::runtime_error("stack size would exceed limit");
|
||||
}
|
||||
this->items[y].data.data1[5] = new_count;
|
||||
this->items[y].amount = new_count;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->num_items >= SlotCount) {
|
||||
throw std::runtime_error("no free space in bank");
|
||||
}
|
||||
auto& last_item = this->items[this->num_items];
|
||||
last_item.data = item;
|
||||
last_item.amount = (item.max_stack_size(limits) > 1) ? item.data1[5] : 1;
|
||||
last_item.present = 1;
|
||||
this->num_items++;
|
||||
}
|
||||
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) {
|
||||
size_t index = this->find_item(item_id);
|
||||
auto& bank_item = this->items[index];
|
||||
|
||||
ItemData ret;
|
||||
if (amount && (bank_item.data.stack_size(limits) > 1) && (amount < bank_item.data.data1[5])) {
|
||||
ret = bank_item.data;
|
||||
ret.data1[5] = amount;
|
||||
bank_item.data.data1[5] -= amount;
|
||||
bank_item.amount -= amount;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = bank_item.data;
|
||||
this->num_items--;
|
||||
for (size_t x = index; x < this->num_items; x++) {
|
||||
this->items[x] = this->items[x + 1];
|
||||
}
|
||||
auto& last_item = this->items[this->num_items];
|
||||
last_item.amount = 0;
|
||||
last_item.present = 0;
|
||||
last_item.data.clear();
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t find_item(uint32_t item_id) {
|
||||
for (size_t x = 0; x < this->num_items; x++) {
|
||||
if (this->items[x].data.id == item_id) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
throw std::out_of_range("item not present");
|
||||
}
|
||||
|
||||
void sort() {
|
||||
std::sort(this->items.data(), this->items.data() + this->num_items);
|
||||
}
|
||||
|
||||
void assign_ids(uint32_t base_id) {
|
||||
for (size_t z = 0; z < this->num_items; z++) {
|
||||
this->items[z].data.id = base_id + z;
|
||||
}
|
||||
}
|
||||
|
||||
void decode_from_client(Version v) {
|
||||
for (size_t z = 0; z < this->items.size(); z++) {
|
||||
this->items[z].data.decode_for_version(v);
|
||||
@@ -416,12 +327,6 @@ struct PlayerBankT {
|
||||
}
|
||||
}
|
||||
|
||||
void enforce_stack_limits(std::shared_ptr<const ItemData::StackLimits> stack_limits) {
|
||||
for (size_t z = 0; z < std::min<uint8_t>(this->num_items, this->items.size()); z++) {
|
||||
this->items[z].data.enforce_stack_size_limits(*stack_limits);
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t DestSlotCount, bool DestBE>
|
||||
operator PlayerBankT<DestSlotCount, DestBE>() const {
|
||||
PlayerBankT<DestSlotCount, DestBE> ret;
|
||||
@@ -439,3 +344,46 @@ using PlayerBank200BE = PlayerBankT<200, true>;
|
||||
check_struct_size(PlayerBank60, 0x05A8);
|
||||
check_struct_size(PlayerBank200, 0x12C8);
|
||||
check_struct_size(PlayerBank200BE, 0x12C8);
|
||||
|
||||
struct PlayerBank {
|
||||
uint32_t max_meseta = 999999;
|
||||
uint32_t max_items = 200;
|
||||
uint32_t meseta = 0;
|
||||
std::vector<PlayerBankItem> items;
|
||||
|
||||
PlayerBank() = default;
|
||||
|
||||
template <size_t SrcSlotCount, bool SrcBE>
|
||||
PlayerBank(const PlayerBankT<SrcSlotCount, SrcBE>& src)
|
||||
: max_meseta(999999), max_items(SrcSlotCount), meseta(src.meseta) {
|
||||
this->items.reserve(src.num_items);
|
||||
for (size_t z = 0; z < src.num_items; z++) {
|
||||
this->items.emplace_back(src.items[z]);
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t DestSlotCount, bool DestBE>
|
||||
operator PlayerBankT<DestSlotCount, DestBE>() const {
|
||||
PlayerBankT<DestSlotCount, DestBE> ret;
|
||||
ret.num_items = std::min<size_t>(ret.items.size(), this->items.size());
|
||||
ret.meseta = this->meseta;
|
||||
for (size_t z = 0; z < ret.num_items; z++) {
|
||||
ret.items[z] = this->items[z];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void load(FILE* f);
|
||||
void save(FILE* f) const;
|
||||
|
||||
uint32_t bb_checksum() const;
|
||||
|
||||
void add_item(const ItemData& item, const ItemData::StackLimits& limits);
|
||||
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits);
|
||||
size_t find_item(uint32_t item_id);
|
||||
void sort();
|
||||
|
||||
void assign_ids(uint32_t base_id);
|
||||
|
||||
void enforce_stack_limits(std::shared_ptr<const ItemData::StackLimits> stack_limits);
|
||||
};
|
||||
|
||||
@@ -563,6 +563,8 @@ struct XBNetworkLocation {
|
||||
/* 04 */ le_uint32_t external_ipv4_address = 0x23232323;
|
||||
/* 08 */ le_uint16_t port = 9500;
|
||||
/* 0A */ parray<uint8_t, 6> mac_address = 0x77;
|
||||
// The remainder of this struct appears to be private/opaque in the XDK (and
|
||||
// newserv doesn't use it either)
|
||||
/* 10 */ le_uint32_t sg_ip_address = 0x0B0B0B0B;
|
||||
/* 14 */ le_uint32_t spi = 0xCCCCCCCC;
|
||||
/* 18 */ le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
|
||||
|
||||
+89
-15
@@ -844,13 +844,12 @@ static asio::awaitable<HandlerResult> SC_6x60_6xA2(shared_ptr<Client> c, Channel
|
||||
co_return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
using DropMode = ProxySession::DropMode;
|
||||
switch (c->proxy_session->drop_mode) {
|
||||
case DropMode::DISABLED:
|
||||
case ProxyDropMode::DISABLED:
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
case DropMode::PASSTHROUGH:
|
||||
case ProxyDropMode::PASSTHROUGH:
|
||||
co_return HandlerResult::FORWARD;
|
||||
case DropMode::INTERCEPT:
|
||||
case ProxyDropMode::INTERCEPT:
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid drop mode");
|
||||
@@ -916,10 +915,43 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
|
||||
c->log.warning_f("Blocking invalid subcommand from server");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
|
||||
case 0x16:
|
||||
case 0x84: {
|
||||
const auto& cmd = msg.check_size_t<G_VolOptBossActions_6x16>(0xFFFF);
|
||||
if (cmd.entity_index_count > 6) {
|
||||
c->log.warning_f("Blocking subcommand 6x16/6x84 with invalid entity index count");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
for (size_t z = 0; z < cmd.entity_index_table.size(); z++) {
|
||||
if (cmd.entity_index_table[z] >= 6) {
|
||||
c->log.warning_f("Blocking subcommand 6x16/6x84 with invalid entity index");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x17: {
|
||||
const auto& cmd = msg.check_size_t<G_SetEntityPositionAndAngle_6x17>();
|
||||
if (cmd.header.entity_id == c->lobby_client_id) {
|
||||
c->log.warning_f("Blocking subcommand 6x17 targeting local client");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x2F: {
|
||||
const auto& cmd = msg.check_size_t<G_ChangePlayerHP_6x2F>();
|
||||
if (cmd.client_id == c->lobby_client_id) {
|
||||
c->log.warning_f("Blocking subcommand 6x2F targeting local player");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x46: {
|
||||
const auto& cmd = msg.check_size_t<G_AttackFinished_6x46>(
|
||||
offsetof(G_AttackFinished_6x46, targets), sizeof(G_AttackFinished_6x46));
|
||||
if (cmd.target_count > min<size_t>(cmd.header.size - 2, cmd.targets.size())) {
|
||||
const auto& header = msg.check_size_t<G_AttackFinished_Header_6x46>(0xFFFF);
|
||||
if (header.target_count > min<size_t>(header.header.size - sizeof(G_AttackFinished_Header_6x46) / 4, 10)) {
|
||||
c->log.warning_f("Blocking subcommand 6x46 with invalid count");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
@@ -927,9 +959,8 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
|
||||
}
|
||||
|
||||
case 0x47: {
|
||||
const auto& cmd = msg.check_size_t<G_CastTechnique_6x47>(
|
||||
offsetof(G_CastTechnique_6x47, targets), sizeof(G_CastTechnique_6x47));
|
||||
if (cmd.target_count > min<size_t>(cmd.header.size - 2, cmd.targets.size())) {
|
||||
const auto& header = msg.check_size_t<G_CastTechnique_Header_6x47>(0xFFFF);
|
||||
if (header.target_count > min<size_t>(header.header.size - sizeof(G_CastTechnique_Header_6x47) / 4, 10)) {
|
||||
c->log.warning_f("Blocking subcommand 6x47 with invalid count");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
@@ -937,9 +968,8 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
|
||||
}
|
||||
|
||||
case 0x49: {
|
||||
const auto& cmd = msg.check_size_t<G_ExecutePhotonBlast_6x49>(
|
||||
offsetof(G_ExecutePhotonBlast_6x49, targets), sizeof(G_ExecutePhotonBlast_6x49));
|
||||
if (cmd.target_count > min<size_t>(cmd.header.size - 3, cmd.targets.size())) {
|
||||
const auto& header = msg.check_size_t<G_ExecutePhotonBlast_Header_6x49>(0xFFFF);
|
||||
if (header.target_count > min<size_t>(header.header.size - sizeof(G_ExecutePhotonBlast_Header_6x49) / 4, 10)) {
|
||||
c->log.warning_f("Blocking subcommand 6x49 with invalid count");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
@@ -958,6 +988,42 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
|
||||
case 0xA2:
|
||||
co_return co_await SC_6x60_6xA2(c, msg);
|
||||
|
||||
case 0x6A: {
|
||||
auto& cmd = msg.check_size_t<G_SetBossWarpFlags_6x6A>();
|
||||
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);
|
||||
} catch (const exception& e) {
|
||||
c->log.warning_f("Invalid object reference ({})", e.what());
|
||||
}
|
||||
|
||||
if (!obj_st || !obj_st->super_obj) {
|
||||
c->log.warning_f("Blocking subcommand 6x6A with missing object");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
auto set_entry = obj_st->super_obj->version(c->version()).set_entry;
|
||||
if (!set_entry) {
|
||||
c->log.warning_f("Blocking subcommand 6x6A with missing set entry");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
if (set_entry->base_type != 0x0019 && set_entry->base_type != 0x0055) {
|
||||
c->log.warning_f("Blocking subcommand 6x6A with incorrect object type");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x7D: {
|
||||
const auto& cmd = msg.check_size_t<G_SetBattleModeData_6x7D>();
|
||||
if ((cmd.what == 3 || cmd.what == 4) && cmd.params[0] >= 4) {
|
||||
c->log.warning_f("Blocking subcommand 6x7D with invalid client ID");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xB3:
|
||||
case 0xB4:
|
||||
case 0xB5: {
|
||||
@@ -1378,6 +1444,14 @@ static asio::awaitable<HandlerResult> S_G_B9(shared_ptr<Client> c, Channel::Mess
|
||||
co_return (c->version() == Version::GC_EP3) ? HandlerResult::FORWARD : HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
static asio::awaitable<HandlerResult> C_G_B9(shared_ptr<Client> c, Channel::Message&) {
|
||||
if (c->proxy_session->suppress_next_ep3_media_update_confirmation) {
|
||||
c->proxy_session->suppress_next_ep3_media_update_confirmation = false;
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
co_return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static asio::awaitable<HandlerResult> S_G_EF(shared_ptr<Client> c, Channel::Message& msg) {
|
||||
if (is_ep3(c->version())) {
|
||||
if (c->check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED)) {
|
||||
@@ -1601,7 +1675,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_section_id = c->character()->disp.visual.section_id;
|
||||
c->proxy_session->lobby_section_id = c->character_file()->disp.visual.section_id;
|
||||
c->proxy_session->lobby_mode = GameMode::NORMAL;
|
||||
c->proxy_session->lobby_random_seed = phosg::random_object<uint32_t>();
|
||||
}
|
||||
@@ -2217,7 +2291,7 @@ static on_message_t handlers[0x100][NUM_VERSIONS][2] = {
|
||||
/* B6 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
|
||||
/* B7 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_B7, nullptr}, {S_G_B7, nullptr}, {S_G_B7, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
|
||||
/* B8 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_B8, nullptr}, {S_G_B8, nullptr}, {S_G_B8, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
|
||||
/* B9 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_B9, nullptr}, {S_G_B9, nullptr}, {S_G_B9, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
|
||||
/* B9 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_B9, C_G_B9}, {S_G_B9, C_G_B9}, {S_G_B9, C_G_B9}, {S_invalid, nullptr}, {S_invalid, nullptr}},
|
||||
/* BA */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_G_BA, nullptr}, {S_G_BA, nullptr}, {S_G_BA, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
|
||||
/* BB */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
|
||||
/* BC */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
|
||||
|
||||
+5
-38
@@ -21,47 +21,14 @@ ProxySession::~ProxySession() {
|
||||
this->num_proxy_sessions--;
|
||||
}
|
||||
|
||||
void ProxySession::set_drop_mode(shared_ptr<ServerState> s, Version version, int64_t override_random_seed, DropMode new_mode) {
|
||||
void ProxySession::set_drop_mode(
|
||||
shared_ptr<ServerState> s, Version version, int64_t override_random_seed, ProxyDropMode new_mode) {
|
||||
this->drop_mode = new_mode;
|
||||
if (this->drop_mode == DropMode::INTERCEPT) {
|
||||
shared_ptr<const RareItemSet> rare_item_set;
|
||||
shared_ptr<const CommonItemSet> common_item_set;
|
||||
switch (version) {
|
||||
case Version::PC_PATCH:
|
||||
case Version::BB_PATCH:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
throw runtime_error("cannot create item creator for this base version");
|
||||
case Version::DC_NTE:
|
||||
case Version::DC_11_2000:
|
||||
case Version::DC_V1:
|
||||
// TODO: We should probably have a v1 common item set at some point too
|
||||
common_item_set = s->common_item_set_v2;
|
||||
rare_item_set = s->rare_item_sets.at("rare-table-v1");
|
||||
break;
|
||||
case Version::DC_V2:
|
||||
case Version::PC_NTE:
|
||||
case Version::PC_V2:
|
||||
common_item_set = s->common_item_set_v2;
|
||||
rare_item_set = s->rare_item_sets.at("rare-table-v2");
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::XB_V3:
|
||||
common_item_set = s->common_item_set_v3_v4;
|
||||
rare_item_set = s->rare_item_sets.at("rare-table-v3");
|
||||
break;
|
||||
case Version::BB_V4:
|
||||
common_item_set = s->common_item_set_v3_v4;
|
||||
rare_item_set = s->rare_item_sets.at("rare-table-v4");
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid lobby base version");
|
||||
}
|
||||
if (this->drop_mode == ProxyDropMode::INTERCEPT) {
|
||||
auto rand_crypt = make_shared<MT19937Generator>((override_random_seed >= 0) ? override_random_seed : this->lobby_random_seed);
|
||||
this->item_creator = make_shared<ItemCreator>(
|
||||
common_item_set,
|
||||
rare_item_set,
|
||||
s->common_item_set(version, nullptr),
|
||||
s->rare_item_set(version, nullptr),
|
||||
s->armor_random_set,
|
||||
s->tool_random_set,
|
||||
s->weapon_random_sets.at(this->lobby_difficulty),
|
||||
|
||||
+3
-7
@@ -43,16 +43,12 @@ struct ProxySession {
|
||||
Episode lobby_episode = Episode::EP1;
|
||||
uint32_t lobby_random_seed = 0;
|
||||
uint64_t server_ping_start_time = 0;
|
||||
bool suppress_next_ep3_media_update_confirmation = false;
|
||||
|
||||
int64_t remote_guild_card_number = -1;
|
||||
parray<uint8_t, 0x28> remote_client_config_data;
|
||||
|
||||
enum class DropMode {
|
||||
DISABLED = 0,
|
||||
PASSTHROUGH,
|
||||
INTERCEPT,
|
||||
};
|
||||
DropMode drop_mode = DropMode::PASSTHROUGH;
|
||||
ProxyDropMode drop_mode = ProxyDropMode::PASSTHROUGH;
|
||||
std::shared_ptr<std::string> quest_dat_data;
|
||||
std::shared_ptr<ItemCreator> item_creator;
|
||||
std::shared_ptr<MapState> map_state;
|
||||
@@ -80,7 +76,7 @@ struct ProxySession {
|
||||
};
|
||||
std::unordered_map<std::string, SavingFile> saving_files;
|
||||
|
||||
void set_drop_mode(std::shared_ptr<ServerState> s, Version version, int64_t override_random_seed, DropMode new_mode);
|
||||
void set_drop_mode(std::shared_ptr<ServerState> s, Version version, int64_t override_random_seed, ProxyDropMode new_mode);
|
||||
|
||||
void clear_lobby_players(size_t num_slots);
|
||||
};
|
||||
|
||||
+164
-344
@@ -194,10 +194,10 @@ struct PSODownloadQuestHeader {
|
||||
} __packed_ws__(PSODownloadQuestHeader, 8);
|
||||
|
||||
void VersionedQuest::assert_valid() const {
|
||||
if (this->category_id == 0xFFFFFFFF) {
|
||||
if (this->meta.category_id == 0xFFFFFFFF) {
|
||||
throw runtime_error("category ID is not set");
|
||||
}
|
||||
if (this->quest_number == 0xFFFFFFFF) {
|
||||
if (this->meta.quest_number == 0xFFFFFFFF) {
|
||||
throw runtime_error("quest number is not set");
|
||||
}
|
||||
if (this->version == Version::UNKNOWN) {
|
||||
@@ -206,81 +206,107 @@ void VersionedQuest::assert_valid() const {
|
||||
if (this->language == 0xFF) {
|
||||
throw runtime_error("language is not set");
|
||||
}
|
||||
if (this->episode == Episode::NONE) {
|
||||
throw runtime_error("episode is not set");
|
||||
switch (this->meta.episode) {
|
||||
case Episode::EP1:
|
||||
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
|
||||
uint8_t area = this->meta.area_for_floor[floor];
|
||||
if (area >= 0x12) {
|
||||
throw runtime_error("Episode 1 quest specifies invalid area");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Episode::EP2:
|
||||
if (is_v1_or_v2(this->version)) {
|
||||
throw runtime_error("v1 or v2 quest specifies Episode 2");
|
||||
}
|
||||
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
|
||||
uint8_t area = this->meta.area_for_floor[floor];
|
||||
if ((area < 0x12) || (area >= 0x24)) {
|
||||
throw runtime_error("Episode 2 quest specifies invalid area");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Episode::EP3:
|
||||
if (!is_ep3(this->version)) {
|
||||
throw runtime_error("non-Ep3 quest specifies Episode 3");
|
||||
}
|
||||
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
|
||||
if (this->meta.area_for_floor[floor] != 0xFF) {
|
||||
throw runtime_error("Episode 3 quest specifies floor overrides");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Episode::EP4:
|
||||
if (!is_v4(this->version)) {
|
||||
throw runtime_error("non-v4 quest specifies Episode 4");
|
||||
}
|
||||
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
|
||||
uint8_t area = this->meta.area_for_floor[floor];
|
||||
if (area != 0xFF && (area < 0x24 || area >= 0x2F)) {
|
||||
throw runtime_error("Episode 4 quest specifies invalid floor");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Episode::NONE:
|
||||
throw runtime_error("episode is not set");
|
||||
default:
|
||||
throw runtime_error("episode is not valid");
|
||||
}
|
||||
if (this->max_players == 0) {
|
||||
if (this->meta.max_players == 0) {
|
||||
throw runtime_error("max players is not set");
|
||||
}
|
||||
if (!this->bin_contents) {
|
||||
throw runtime_error("bin file is missing");
|
||||
}
|
||||
if (!is_ep3(this->version) && !this->dat_contents) {
|
||||
if (!this->dat_contents) {
|
||||
throw runtime_error("dat file is missing");
|
||||
}
|
||||
if (!is_ep3(this->version) && !this->map_file) {
|
||||
if (!this->map_file) {
|
||||
throw runtime_error("parsed map file is missing");
|
||||
}
|
||||
if (this->meta.common_item_set_name.empty() != !this->meta.common_item_set) {
|
||||
throw runtime_error("common item set name/pointer mismatch");
|
||||
}
|
||||
if (this->meta.rare_item_set_name.empty() != !this->meta.rare_item_set) {
|
||||
throw runtime_error("rare item set name/pointer mismatch");
|
||||
}
|
||||
if (this->meta.allowed_drop_modes &&
|
||||
!(this->meta.allowed_drop_modes & (1 << static_cast<size_t>(this->meta.default_drop_mode)))) {
|
||||
throw runtime_error("default drop mode is not allowed");
|
||||
}
|
||||
}
|
||||
|
||||
string VersionedQuest::bin_filename() const {
|
||||
if (this->episode == Episode::EP3) {
|
||||
return std::format("m{:06}p_e.bin", this->quest_number);
|
||||
} else {
|
||||
return std::format("quest{}.bin", this->quest_number);
|
||||
}
|
||||
return std::format("quest{}.bin", this->meta.quest_number);
|
||||
}
|
||||
|
||||
string VersionedQuest::dat_filename() const {
|
||||
if (this->episode == Episode::EP3) {
|
||||
throw logic_error("Episode 3 quests do not have .dat files");
|
||||
} else {
|
||||
return std::format("quest{}.dat", this->quest_number);
|
||||
}
|
||||
return std::format("quest{}.dat", this->meta.quest_number);
|
||||
}
|
||||
|
||||
string VersionedQuest::pvr_filename() const {
|
||||
if (this->episode == Episode::EP3) {
|
||||
throw logic_error("Episode 3 quests do not have .pvr files");
|
||||
} else {
|
||||
return std::format("quest{}.pvr", this->quest_number);
|
||||
}
|
||||
return std::format("quest{}.pvr", this->meta.quest_number);
|
||||
}
|
||||
|
||||
string VersionedQuest::xb_filename() const {
|
||||
if (this->episode == Episode::EP3) {
|
||||
throw logic_error("Episode 3 quests do not have Xbox filenames");
|
||||
} else {
|
||||
return std::format("quest{}_{}.dat", this->quest_number, static_cast<char>(tolower(char_for_language_code(this->language))));
|
||||
}
|
||||
return std::format("quest{}_{}.dat",
|
||||
this->meta.quest_number, static_cast<char>(tolower(char_for_language_code(this->language))));
|
||||
}
|
||||
|
||||
string VersionedQuest::encode_qst() const {
|
||||
unordered_map<string, shared_ptr<const string>> files;
|
||||
files.emplace(std::format("quest{}.bin", this->quest_number), this->bin_contents);
|
||||
files.emplace(std::format("quest{}.dat", this->quest_number), this->dat_contents);
|
||||
files.emplace(std::format("quest{}.bin", this->meta.quest_number), this->bin_contents);
|
||||
files.emplace(std::format("quest{}.dat", this->meta.quest_number), this->dat_contents);
|
||||
if (this->pvr_contents) {
|
||||
files.emplace(std::format("quest{}.pvr", this->quest_number), this->pvr_contents);
|
||||
files.emplace(std::format("quest{}.pvr", this->meta.quest_number), this->pvr_contents);
|
||||
}
|
||||
string xb_filename = std::format("quest{}_{}.dat", quest_number, static_cast<char>(tolower(char_for_language_code(language))));
|
||||
return encode_qst_file(files, this->name, this->quest_number, xb_filename, this->version, this->is_dlq_encoded);
|
||||
string xb_filename = std::format("quest{}_{}.dat",
|
||||
this->meta.quest_number, static_cast<char>(tolower(char_for_language_code(language))));
|
||||
return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->version, this->is_dlq_encoded);
|
||||
}
|
||||
|
||||
Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
|
||||
: quest_number(initial_version->quest_number),
|
||||
category_id(initial_version->category_id),
|
||||
episode(initial_version->episode),
|
||||
allow_start_from_chat_command(initial_version->allow_start_from_chat_command),
|
||||
joinable(initial_version->joinable),
|
||||
max_players(initial_version->max_players),
|
||||
lock_status_register(initial_version->lock_status_register),
|
||||
name(initial_version->name),
|
||||
supermap(nullptr),
|
||||
battle_rules(initial_version->battle_rules),
|
||||
challenge_template_index(initial_version->challenge_template_index),
|
||||
description_flag(initial_version->description_flag),
|
||||
available_expression(initial_version->available_expression),
|
||||
enabled_expression(initial_version->enabled_expression) {
|
||||
: meta(initial_version->meta), supermap(nullptr) {
|
||||
this->add_version(initial_version);
|
||||
}
|
||||
|
||||
@@ -290,32 +316,17 @@ phosg::JSON Quest::json() const {
|
||||
versions_json.emplace_back(phosg::JSON::dict({
|
||||
{"Version", phosg::name_for_enum(vq->version)},
|
||||
{"Language", name_for_language_code(vq->language)},
|
||||
{"ShortDescription", vq->short_description},
|
||||
{"LongDescription", vq->long_description},
|
||||
{"Name", vq->meta.name},
|
||||
{"ShortDescription", vq->meta.short_description},
|
||||
{"LongDescription", vq->meta.long_description},
|
||||
{"BINFileSize", vq->bin_contents ? vq->bin_contents->size() : phosg::JSON(nullptr)},
|
||||
{"DATFileSize", vq->dat_contents ? vq->dat_contents->size() : phosg::JSON(nullptr)},
|
||||
{"PVRFileSize", vq->pvr_contents ? vq->pvr_contents->size() : phosg::JSON(nullptr)},
|
||||
}));
|
||||
}
|
||||
|
||||
auto battle_rules_json = this->battle_rules ? this->battle_rules->json() : nullptr;
|
||||
auto challenge_template_index_json = (this->challenge_template_index >= 0)
|
||||
? this->challenge_template_index
|
||||
: phosg::JSON(nullptr);
|
||||
return phosg::JSON::dict({
|
||||
{"Number", this->quest_number},
|
||||
{"CategoryID", this->category_id},
|
||||
{"Episode", name_for_episode(this->episode)},
|
||||
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
|
||||
{"Joinable", this->joinable},
|
||||
{"MaxPlayers", this->max_players},
|
||||
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
|
||||
{"Name", this->name},
|
||||
{"BattleRules", std::move(battle_rules_json)},
|
||||
{"ChallengeTemplateIndex", std::move(challenge_template_index_json)},
|
||||
{"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)},
|
||||
{"Metadata", this->meta.json()},
|
||||
{"Versions", std::move(versions_json)},
|
||||
});
|
||||
}
|
||||
@@ -325,88 +336,7 @@ uint32_t Quest::versions_key(Version v, uint8_t language) {
|
||||
}
|
||||
|
||||
void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
|
||||
if (this->quest_number != vq->quest_number) {
|
||||
throw logic_error(std::format(
|
||||
"incorrect versioned quest number (existing: {:08X}, new: {:08X})",
|
||||
this->quest_number, vq->quest_number));
|
||||
}
|
||||
if (this->category_id != vq->category_id) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version is in a different category (existing: {:08X}, new: {:08X})",
|
||||
this->category_id, vq->category_id));
|
||||
}
|
||||
if (this->episode != vq->episode) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version is in a different episode (existing: {}, new: {})",
|
||||
name_for_episode(this->episode), name_for_episode(vq->episode)));
|
||||
}
|
||||
if (this->allow_start_from_chat_command != vq->allow_start_from_chat_command) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different allow_start_from_chat_command state (existing: {}, new: {})",
|
||||
this->allow_start_from_chat_command ? "true" : "false", vq->allow_start_from_chat_command ? "true" : "false"));
|
||||
}
|
||||
if (this->joinable != vq->joinable) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different joinability state (existing: {}, new: {})",
|
||||
this->joinable ? "true" : "false", vq->joinable ? "true" : "false"));
|
||||
}
|
||||
if (this->max_players != vq->max_players) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different maximum player count (existing: {}, new: {})",
|
||||
this->max_players, vq->max_players));
|
||||
}
|
||||
if (this->lock_status_register != vq->lock_status_register) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different lock status register (existing: {:04X}, new: {:04X})",
|
||||
this->lock_status_register, vq->lock_status_register));
|
||||
}
|
||||
if (!this->battle_rules != !vq->battle_rules) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different battle rules presence state (existing: {}, new: {})",
|
||||
this->battle_rules ? "present" : "absent", vq->battle_rules ? "present" : "absent"));
|
||||
}
|
||||
if (this->battle_rules && (*this->battle_rules != *vq->battle_rules)) {
|
||||
string existing_str = this->battle_rules->json().serialize();
|
||||
string new_str = vq->battle_rules->json().serialize();
|
||||
throw runtime_error(std::format(
|
||||
"quest version has different battle rules (existing: {}, new: {})",
|
||||
existing_str, new_str));
|
||||
}
|
||||
if (this->challenge_template_index != vq->challenge_template_index) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has different challenge template index (existing: {}, new: {})",
|
||||
this->challenge_template_index, vq->challenge_template_index));
|
||||
}
|
||||
if (this->description_flag != vq->description_flag) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has different description flag (existing: {:02X}, new: {:02X})",
|
||||
this->description_flag, vq->description_flag));
|
||||
}
|
||||
if (!this->available_expression != !vq->available_expression) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has available expression but root quest does not, or vice versa (existing: {}, new: {})",
|
||||
this->available_expression ? "present" : "absent", vq->available_expression ? "present" : "absent"));
|
||||
}
|
||||
if (this->available_expression && *this->available_expression != *vq->available_expression) {
|
||||
string existing_str = this->available_expression->str();
|
||||
string new_str = vq->available_expression->str();
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different available expression (existing: {}, new: {})",
|
||||
existing_str, new_str));
|
||||
}
|
||||
if (!this->enabled_expression != !vq->enabled_expression) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has enabled expression but root quest does not, or vice versa (existing: {}, new: {})",
|
||||
this->enabled_expression ? "present" : "absent", vq->enabled_expression ? "present" : "absent"));
|
||||
}
|
||||
if (this->enabled_expression && *this->enabled_expression != *vq->enabled_expression) {
|
||||
string existing_str = this->enabled_expression->str();
|
||||
string new_str = vq->enabled_expression->str();
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different enabled expression (existing: {}, new: {})",
|
||||
existing_str, new_str));
|
||||
}
|
||||
|
||||
this->meta.assert_compatible(vq->meta);
|
||||
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
|
||||
}
|
||||
|
||||
@@ -438,12 +368,12 @@ std::shared_ptr<const SuperMap> Quest::get_supermap(int64_t random_seed) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto supermap = make_shared<SuperMap>(this->episode, map_files);
|
||||
auto supermap = make_shared<SuperMap>(this->meta.episode, map_files);
|
||||
if (save_to_cache) {
|
||||
this->supermap = supermap;
|
||||
}
|
||||
static_game_data_log.info_f("Constructed {} supermap for quest {} ({})",
|
||||
save_to_cache ? "cacheable" : "temporary", this->quest_number, this->name);
|
||||
save_to_cache ? "cacheable" : "temporary", this->meta.quest_number, this->meta.name);
|
||||
|
||||
return supermap;
|
||||
}
|
||||
@@ -482,7 +412,8 @@ shared_ptr<const VersionedQuest> Quest::version(Version v, uint8_t language) con
|
||||
QuestIndex::QuestIndex(
|
||||
const string& directory,
|
||||
shared_ptr<const QuestCategoryIndex> category_index,
|
||||
bool is_ep3)
|
||||
const unordered_map<string, shared_ptr<const CommonItemSet>>& common_item_sets,
|
||||
const unordered_map<string, shared_ptr<const RareItemSet>>& rare_item_sets)
|
||||
: directory(directory),
|
||||
category_index(category_index) {
|
||||
|
||||
@@ -492,7 +423,7 @@ QuestIndex::QuestIndex(
|
||||
};
|
||||
struct BINFileData {
|
||||
string filename;
|
||||
unique_ptr<QuestMetadata> metadata;
|
||||
shared_ptr<const AssembledQuestScript> assembled;
|
||||
shared_ptr<const string> data;
|
||||
};
|
||||
struct DATFileData {
|
||||
@@ -506,12 +437,6 @@ QuestIndex::QuestIndex(
|
||||
map<string, FileData> json_files;
|
||||
map<string, uint32_t> categories;
|
||||
for (const auto& cat : this->category_index->categories) {
|
||||
// Don't index Ep3 download categories for non-Ep3 quest indexing, and vice
|
||||
// versa
|
||||
if (is_ep3 != cat->check_flag(QuestMenuType::EP3_DOWNLOAD)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto add_file = [&](map<string, FileData>& files, const string& basename, const string& filename, string&& value, bool check_chunk_size) {
|
||||
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
|
||||
throw runtime_error("file " + basename + " exists in multiple categories");
|
||||
@@ -528,7 +453,7 @@ QuestIndex::QuestIndex(
|
||||
}
|
||||
};
|
||||
|
||||
auto add_bin_file = [&](const string& basename, const string& filename, string&& data, const QuestMetadata* metadata) {
|
||||
auto add_bin_file = [&](const string& basename, const string& filename, string&& data, shared_ptr<AssembledQuestScript> assembled) {
|
||||
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
|
||||
throw runtime_error("bin file " + basename + " exists in multiple categories");
|
||||
}
|
||||
@@ -540,9 +465,7 @@ QuestIndex::QuestIndex(
|
||||
auto& entry = emplace_ret.first->second;
|
||||
entry.filename = filename;
|
||||
entry.data = data_ptr;
|
||||
if (metadata) {
|
||||
entry.metadata = make_unique<QuestMetadata>(*metadata);
|
||||
}
|
||||
entry.assembled = assembled;
|
||||
if (!(data_ptr->size() & 0x3FF)) {
|
||||
data_ptr->push_back(0x00);
|
||||
}
|
||||
@@ -573,7 +496,7 @@ QuestIndex::QuestIndex(
|
||||
}
|
||||
|
||||
string file_path = cat_path + "/" + filename;
|
||||
unique_ptr<AssembledQuestScript> assembled;
|
||||
shared_ptr<AssembledQuestScript> assembled;
|
||||
try {
|
||||
string orig_filename = filename;
|
||||
string file_data;
|
||||
@@ -588,7 +511,7 @@ QuestIndex::QuestIndex(
|
||||
filename.resize(filename.size() - 4);
|
||||
} else if (filename.ends_with(".bin.txt")) {
|
||||
string include_dir = phosg::dirname(file_path);
|
||||
assembled = make_unique<AssembledQuestScript>(assemble_quest_script(
|
||||
assembled = make_shared<AssembledQuestScript>(assemble_quest_script(
|
||||
phosg::load_file(file_path),
|
||||
{include_dir, "system/quests/includes"},
|
||||
{include_dir, "system/quests/includes", "system/client-functions/System"}));
|
||||
@@ -614,9 +537,9 @@ QuestIndex::QuestIndex(
|
||||
if (extension == "json") {
|
||||
add_file(json_files, file_basename, orig_filename, std::move(file_data), false);
|
||||
} else if (extension == "bin" || extension == "mnm") {
|
||||
add_bin_file(file_basename, orig_filename, std::move(file_data), assembled ? &assembled->metadata : nullptr);
|
||||
add_bin_file(file_basename, orig_filename, std::move(file_data), assembled);
|
||||
} else if (extension == "bind" || extension == "mnmd") {
|
||||
add_bin_file(file_basename, orig_filename, prs_compress_optimal(file_data), assembled ? &assembled->metadata : nullptr);
|
||||
add_bin_file(file_basename, orig_filename, prs_compress_optimal(file_data), assembled);
|
||||
} else if (extension == "dat") {
|
||||
add_dat_file(file_basename, orig_filename, std::move(file_data));
|
||||
} else if (extension == "datd") {
|
||||
@@ -669,28 +592,18 @@ QuestIndex::QuestIndex(
|
||||
version_token = std::move(filename_tokens[1]);
|
||||
language_token = std::move(filename_tokens[2]);
|
||||
}
|
||||
vq->category_id = categories.at(basename);
|
||||
|
||||
// Find the quest's metadata. If the quest was assembled (that is, if it
|
||||
// came from a .bin.txt file), use the metadata from the source file;
|
||||
// otherwise, figure it out from the already-assembled code
|
||||
if (entry.metadata) {
|
||||
vq->quest_number = entry.metadata->quest_number;
|
||||
vq->version = ::is_ep3(entry.metadata->version) ? Version::GC_V3 : entry.metadata->version;
|
||||
vq->language = entry.metadata->language;
|
||||
vq->episode = entry.metadata->episode;
|
||||
vq->joinable = entry.metadata->joinable;
|
||||
vq->max_players = entry.metadata->max_players;
|
||||
vq->name = entry.metadata->name;
|
||||
vq->short_description = entry.metadata->short_description;
|
||||
vq->long_description = entry.metadata->long_description;
|
||||
vq->meta.category_id = categories.at(basename);
|
||||
|
||||
if (entry.assembled) {
|
||||
vq->meta.quest_number = entry.assembled->quest_number;
|
||||
vq->version = entry.assembled->version;
|
||||
vq->language = entry.assembled->language;
|
||||
} else {
|
||||
// Get the number from the first token
|
||||
if (quest_number_token.empty()) {
|
||||
throw runtime_error("quest number token is missing");
|
||||
}
|
||||
vq->quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10);
|
||||
vq->meta.quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10);
|
||||
|
||||
// Get the version from the second token
|
||||
static const unordered_map<string, Version> name_to_version({
|
||||
@@ -714,147 +627,45 @@ QuestIndex::QuestIndex(
|
||||
throw runtime_error("language token is not a single character");
|
||||
}
|
||||
vq->language = language_code_for_char(language_token[0]);
|
||||
|
||||
auto bin_decompressed = prs_decompress(*entry.data);
|
||||
switch (vq->version) {
|
||||
case Version::DC_NTE: {
|
||||
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDCNTE)) {
|
||||
throw invalid_argument("file is too small for header");
|
||||
}
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderDCNTE*>(bin_decompressed.data());
|
||||
vq->episode = Episode::EP1;
|
||||
vq->max_players = 4;
|
||||
vq->name = header->name.decode(vq->language);
|
||||
if (vq->quest_number == 0xFFFFFFFF) {
|
||||
vq->quest_number = phosg::fnv1a64(vq->name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Version::DC_11_2000:
|
||||
case Version::DC_V1:
|
||||
case Version::DC_V2: {
|
||||
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) {
|
||||
throw invalid_argument("file is too small for header");
|
||||
}
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderDC*>(bin_decompressed.data());
|
||||
vq->episode = Episode::EP1;
|
||||
vq->max_players = 4;
|
||||
if (vq->quest_number == 0xFFFFFFFF) {
|
||||
vq->quest_number = header->quest_number;
|
||||
}
|
||||
vq->name = header->name.decode(vq->language);
|
||||
vq->short_description = header->short_description.decode(vq->language);
|
||||
vq->long_description = header->long_description.decode(vq->language);
|
||||
break;
|
||||
}
|
||||
|
||||
case Version::PC_NTE:
|
||||
case Version::PC_V2: {
|
||||
if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) {
|
||||
throw invalid_argument("file is too small for header");
|
||||
}
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderPC*>(bin_decompressed.data());
|
||||
vq->episode = Episode::EP1;
|
||||
vq->max_players = 4;
|
||||
if (vq->quest_number == 0xFFFFFFFF) {
|
||||
vq->quest_number = header->quest_number;
|
||||
}
|
||||
vq->name = header->name.decode(vq->language);
|
||||
vq->short_description = header->short_description.decode(vq->language);
|
||||
vq->long_description = header->long_description.decode(vq->language);
|
||||
break;
|
||||
}
|
||||
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3: {
|
||||
// Note: This codepath handles Episode 3 download quests, which are not
|
||||
// the same as Episode 3 quest scripts. The latter are only used offline
|
||||
// in story mode, but can be disassembled with disassemble_quest_script.
|
||||
// It's unfortunate that Version::GC_EP3 is used here for Episode 3
|
||||
// download quests (maps) and there for offline story mode scripts, but
|
||||
// it's probably not worth refactoring this logic, at least right now.
|
||||
if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) {
|
||||
throw invalid_argument("file is incorrect size");
|
||||
}
|
||||
auto* map = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
|
||||
vq->episode = Episode::EP3;
|
||||
vq->max_players = 4;
|
||||
if (vq->quest_number == 0xFFFFFFFF) {
|
||||
vq->quest_number = map->map_number;
|
||||
}
|
||||
vq->name = map->name.decode(vq->language);
|
||||
vq->short_description = map->quest_name.decode(vq->language);
|
||||
vq->long_description = map->description.decode(vq->language);
|
||||
break;
|
||||
}
|
||||
|
||||
case Version::XB_V3:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3: {
|
||||
if (bin_decompressed.size() < sizeof(PSOQuestHeaderGC)) {
|
||||
throw invalid_argument("file is too small for header");
|
||||
}
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderGC*>(bin_decompressed.data());
|
||||
vq->episode = find_quest_episode_from_script(
|
||||
bin_decompressed.data(), bin_decompressed.size(), vq->version);
|
||||
vq->max_players = 4;
|
||||
if (vq->quest_number == 0xFFFFFFFF) {
|
||||
vq->quest_number = header->quest_number;
|
||||
}
|
||||
vq->name = header->name.decode(vq->language);
|
||||
vq->short_description = header->short_description.decode(vq->language);
|
||||
vq->long_description = header->long_description.decode(vq->language);
|
||||
break;
|
||||
}
|
||||
|
||||
case Version::BB_V4: {
|
||||
if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) {
|
||||
throw invalid_argument("file is too small for header");
|
||||
}
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderBB*>(bin_decompressed.data());
|
||||
vq->episode = find_quest_episode_from_script(
|
||||
bin_decompressed.data(), bin_decompressed.size(), vq->version);
|
||||
vq->joinable |= header->joinable;
|
||||
vq->max_players = 4;
|
||||
if (vq->quest_number == 0xFFFFFFFF) {
|
||||
vq->quest_number = header->quest_number;
|
||||
}
|
||||
vq->name = header->name.decode(vq->language);
|
||||
vq->short_description = header->short_description.decode(vq->language);
|
||||
vq->long_description = header->long_description.decode(vq->language);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw logic_error("invalid quest game version");
|
||||
}
|
||||
}
|
||||
|
||||
// Find the corresponding dat and pvr files
|
||||
auto bin_decompressed = prs_decompress(*entry.data);
|
||||
populate_quest_metadata_from_script(vq->meta, bin_decompressed.data(), bin_decompressed.size(), vq->version, vq->language);
|
||||
|
||||
// If the quest was assembled (that is, if it came from a .bin.txt file),
|
||||
// the metadata from the source file overrides any automatically-detected
|
||||
// values from above
|
||||
if (entry.assembled) {
|
||||
vq->meta.quest_number = entry.assembled->quest_number;
|
||||
vq->meta.episode = entry.assembled->episode;
|
||||
vq->meta.joinable = entry.assembled->joinable;
|
||||
vq->meta.max_players = entry.assembled->max_players;
|
||||
vq->meta.name = entry.assembled->name;
|
||||
vq->meta.short_description = entry.assembled->short_description;
|
||||
vq->meta.long_description = entry.assembled->long_description;
|
||||
}
|
||||
|
||||
// Find the corresponding dat and pvr files with the same basename as the
|
||||
// bin file; if not found, look for them without the language suffix
|
||||
const DATFileData* dat_filedata = nullptr;
|
||||
const FileData* pvr_filedata = nullptr;
|
||||
if (!::is_ep3(vq->version)) {
|
||||
// Look for dat and pvr files with the same basename as the bin file; if
|
||||
// not found, look for them without the language suffix
|
||||
try {
|
||||
dat_filedata = &dat_files.at(basename);
|
||||
} catch (const out_of_range&) {
|
||||
try {
|
||||
dat_filedata = &dat_files.at(basename);
|
||||
dat_filedata = &dat_files.at(quest_number_token + "-" + version_token);
|
||||
} catch (const out_of_range&) {
|
||||
try {
|
||||
dat_filedata = &dat_files.at(quest_number_token + "-" + version_token);
|
||||
} catch (const out_of_range&) {
|
||||
throw runtime_error("no dat file found for bin file " + basename);
|
||||
}
|
||||
throw runtime_error("no dat file found for bin file " + basename);
|
||||
}
|
||||
}
|
||||
try {
|
||||
pvr_filedata = &pvr_files.at(basename);
|
||||
} catch (const out_of_range&) {
|
||||
try {
|
||||
pvr_filedata = &pvr_files.at(basename);
|
||||
pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token);
|
||||
} catch (const out_of_range&) {
|
||||
try {
|
||||
pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token);
|
||||
} catch (const out_of_range&) {
|
||||
// pvr files aren't required (and most quests do not have them), so
|
||||
// don't fail if it's missing
|
||||
}
|
||||
// pvr files aren't required (and most quests do not have them), so
|
||||
// don't fail if it's missing
|
||||
}
|
||||
}
|
||||
vq->bin_contents = entry.data;
|
||||
@@ -883,42 +694,56 @@ QuestIndex::QuestIndex(
|
||||
if (json_filedata) {
|
||||
auto metadata_json = phosg::JSON::parse(*json_filedata->data);
|
||||
try {
|
||||
vq->battle_rules = make_shared<BattleRules>(metadata_json.at("BattleRules"));
|
||||
vq->meta.description_flag = metadata_json.at("DescriptionFlag").as_int();
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
vq->challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int();
|
||||
vq->meta.available_expression = make_shared<IntegralExpression>(metadata_json.get_string("AvailableIf"));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
vq->description_flag = metadata_json.at("DescriptionFlag").as_int();
|
||||
vq->meta.enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
vq->available_expression = make_shared<IntegralExpression>(metadata_json.get_string("AvailableIf"));
|
||||
vq->meta.allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand");
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
vq->enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
|
||||
vq->meta.joinable = metadata_json.get_bool("Joinable");
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
vq->allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand");
|
||||
vq->meta.lock_status_register = metadata_json.get_int("LockStatusRegister");
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
vq->joinable = metadata_json.get_bool("Joinable");
|
||||
vq->meta.common_item_set_name = metadata_json.at("CommonItemSetName").as_string();
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
if (!vq->meta.common_item_set_name.empty()) {
|
||||
vq->meta.common_item_set = common_item_sets.at(vq->meta.common_item_set_name);
|
||||
}
|
||||
try {
|
||||
vq->meta.rare_item_set_name = metadata_json.at("RareItemSetName").as_string();
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
if (!vq->meta.rare_item_set_name.empty()) {
|
||||
vq->meta.rare_item_set = rare_item_sets.at(vq->meta.rare_item_set_name);
|
||||
}
|
||||
try {
|
||||
vq->meta.allowed_drop_modes = metadata_json.at("AllowedDropModes").as_int();
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
vq->lock_status_register = metadata_json.get_int("LockStatusRegister");
|
||||
vq->meta.default_drop_mode = phosg::enum_for_name<ServerDropMode>(metadata_json.at("DefaultDropMode").as_string());
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
|
||||
vq->assert_valid();
|
||||
|
||||
auto category_name = this->category_index->at(vq->category_id)->name;
|
||||
auto category_name = this->category_index->at(vq->meta.category_id)->name;
|
||||
string filenames_str = entry.filename;
|
||||
if (dat_filedata) {
|
||||
filenames_str += std::format("/{}", dat_filedata->filename);
|
||||
@@ -929,30 +754,32 @@ QuestIndex::QuestIndex(
|
||||
if (json_filedata) {
|
||||
filenames_str += std::format("/{}", json_filedata->filename);
|
||||
}
|
||||
auto q_it = this->quests_by_number.find(vq->quest_number);
|
||||
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 {} ({})",
|
||||
static_game_data_log.debug_f("({}) Added {} {} version of quest {} ({}) with floors {}",
|
||||
filenames_str,
|
||||
phosg::name_for_enum(vq->version),
|
||||
char_for_language_code(vq->language),
|
||||
vq->quest_number,
|
||||
vq->name);
|
||||
vq->meta.quest_number,
|
||||
vq->meta.name,
|
||||
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
|
||||
} else {
|
||||
auto q = make_shared<Quest>(vq);
|
||||
this->quests_by_number.emplace(vq->quest_number, q);
|
||||
this->quests_by_name.emplace(vq->name, q);
|
||||
this->quests_by_category_id_and_number[q->category_id].emplace(vq->quest_number, q);
|
||||
static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {})",
|
||||
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 {}",
|
||||
filenames_str,
|
||||
phosg::name_for_enum(vq->version),
|
||||
char_for_language_code(vq->language),
|
||||
vq->quest_number,
|
||||
vq->name,
|
||||
name_for_episode(vq->episode),
|
||||
vq->meta.quest_number,
|
||||
vq->meta.name,
|
||||
name_for_episode(vq->meta.episode),
|
||||
category_name,
|
||||
vq->category_id,
|
||||
vq->joinable ? "joinable" : "not joinable");
|
||||
vq->meta.category_id,
|
||||
vq->meta.joinable ? "joinable" : "not joinable",
|
||||
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning_f("({}) Failed to index quest file: {}", basename, e.what());
|
||||
@@ -1033,7 +860,7 @@ vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>> QuestIndex::filt
|
||||
return ret;
|
||||
}
|
||||
for (auto it : category_it->second) {
|
||||
if ((effective_episode != Episode::NONE) && (it.second->episode != effective_episode)) {
|
||||
if ((effective_episode != Episode::NONE) && (it.second->meta.episode != effective_episode)) {
|
||||
continue;
|
||||
}
|
||||
bool all_required_versions_present = true;
|
||||
@@ -1082,8 +909,7 @@ string encode_download_quest_data(const string& compressed_data, size_t decompre
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
|
||||
PSOV2Encryption encr(encryption_seed);
|
||||
encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader),
|
||||
data.size() - sizeof(PSODownloadQuestHeader));
|
||||
encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader), data.size() - sizeof(PSODownloadQuestHeader));
|
||||
data.resize(original_size);
|
||||
|
||||
return data;
|
||||
@@ -1095,12 +921,6 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
|
||||
// this flag, we need to decompress the quest's .bin file, set the flag, then
|
||||
// recompress it again.
|
||||
|
||||
// This function should not be used for Episode 3 quests (they should be sent
|
||||
// to the client as-is, without any encryption or other preprocessing)
|
||||
if (this->episode == Episode::EP3 || is_ep3(this->version)) {
|
||||
throw logic_error("Episode 3 quests cannot be converted to download quests");
|
||||
}
|
||||
|
||||
string decompressed_bin = prs_decompress(*this->bin_contents);
|
||||
|
||||
void* data_ptr = decompressed_bin.data();
|
||||
|
||||
+12
-30
@@ -8,10 +8,14 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "CommonItemSet.hh"
|
||||
#include "IntegralExpression.hh"
|
||||
#include "ItemParameterTable.hh"
|
||||
#include "Map.hh"
|
||||
#include "PlayerSubordinates.hh"
|
||||
#include "QuestMetadata.hh"
|
||||
#include "QuestScript.hh"
|
||||
#include "RareItemSet.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "TeamIndex.hh"
|
||||
|
||||
@@ -31,7 +35,6 @@ enum class QuestMenuType {
|
||||
SOLO = 3,
|
||||
GOVERNMENT = 4,
|
||||
DOWNLOAD = 5,
|
||||
EP3_DOWNLOAD = 6,
|
||||
// 7 can't be used as a menu type (it enables the per-episode filter)
|
||||
};
|
||||
|
||||
@@ -64,29 +67,16 @@ struct QuestCategoryIndex {
|
||||
};
|
||||
|
||||
struct VersionedQuest {
|
||||
QuestMetadata meta;
|
||||
|
||||
// Most of these default values are intentionally invalid; we use these
|
||||
// values to check if each field was parsed during quest indexing.
|
||||
uint32_t category_id = 0xFFFFFFFF;
|
||||
uint32_t quest_number = 0xFFFFFFFF;
|
||||
Version version = Version::UNKNOWN;
|
||||
uint8_t language = 0xFF;
|
||||
Episode episode = Episode::NONE;
|
||||
bool joinable = false;
|
||||
uint8_t max_players = 0x00;
|
||||
std::string name;
|
||||
std::string short_description;
|
||||
std::string long_description;
|
||||
std::shared_ptr<const std::string> bin_contents;
|
||||
std::shared_ptr<const std::string> dat_contents;
|
||||
std::shared_ptr<const MapFile> map_file;
|
||||
std::shared_ptr<const std::string> pvr_contents;
|
||||
std::shared_ptr<const BattleRules> battle_rules;
|
||||
ssize_t challenge_template_index = -1;
|
||||
uint8_t description_flag = 0x00;
|
||||
std::shared_ptr<const IntegralExpression> available_expression;
|
||||
std::shared_ptr<const IntegralExpression> enabled_expression;
|
||||
bool allow_start_from_chat_command = false;
|
||||
int16_t lock_status_register = -1;
|
||||
bool is_dlq_encoded = false;
|
||||
|
||||
void assert_valid() const;
|
||||
@@ -101,20 +91,8 @@ struct VersionedQuest {
|
||||
};
|
||||
|
||||
struct Quest {
|
||||
uint32_t quest_number;
|
||||
uint32_t category_id;
|
||||
Episode episode;
|
||||
bool allow_start_from_chat_command;
|
||||
bool joinable;
|
||||
uint8_t max_players;
|
||||
int16_t lock_status_register;
|
||||
std::string name;
|
||||
QuestMetadata meta;
|
||||
mutable std::shared_ptr<const SuperMap> supermap;
|
||||
std::shared_ptr<const BattleRules> battle_rules;
|
||||
ssize_t challenge_template_index;
|
||||
uint8_t description_flag;
|
||||
std::shared_ptr<const IntegralExpression> available_expression;
|
||||
std::shared_ptr<const IntegralExpression> enabled_expression;
|
||||
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
|
||||
|
||||
Quest() = delete;
|
||||
@@ -151,7 +129,11 @@ struct QuestIndex {
|
||||
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;
|
||||
|
||||
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index, bool is_ep3);
|
||||
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);
|
||||
phosg::JSON json() const;
|
||||
|
||||
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "QuestMetadata.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
void QuestMetadata::assign_default_areas(Version version, Episode episode) {
|
||||
for (size_t z = 0; z < 0x12; z++) {
|
||||
this->area_for_floor[z] = SetDataTableBase::default_area_for_floor(version, episode, z);
|
||||
}
|
||||
}
|
||||
|
||||
void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
|
||||
if (this->quest_number != other.quest_number) {
|
||||
throw logic_error(std::format(
|
||||
"incorrect versioned quest number (existing: {:08X}, new: {:08X})",
|
||||
this->quest_number, other.quest_number));
|
||||
}
|
||||
if (this->category_id != other.category_id) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version is in a different category (existing: {:08X}, new: {:08X})",
|
||||
this->category_id, other.category_id));
|
||||
}
|
||||
if (this->episode != other.episode) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version is in a different episode (existing: {}, new: {})",
|
||||
name_for_episode(this->episode), name_for_episode(other.episode)));
|
||||
}
|
||||
if (this->allow_start_from_chat_command != other.allow_start_from_chat_command) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different allow_start_from_chat_command state (existing: {}, new: {})",
|
||||
this->allow_start_from_chat_command ? "true" : "false", other.allow_start_from_chat_command ? "true" : "false"));
|
||||
}
|
||||
if (this->joinable != other.joinable) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different joinability state (existing: {}, new: {})",
|
||||
this->joinable ? "true" : "false", other.joinable ? "true" : "false"));
|
||||
}
|
||||
if (this->max_players != other.max_players) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different maximum player count (existing: {}, new: {})",
|
||||
this->max_players, other.max_players));
|
||||
}
|
||||
if (this->lock_status_register != other.lock_status_register) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different lock status register (existing: {:04X}, new: {:04X})",
|
||||
this->lock_status_register, other.lock_status_register));
|
||||
}
|
||||
if (!this->battle_rules != !other.battle_rules) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different battle rules presence state (existing: {}, new: {})",
|
||||
this->battle_rules ? "present" : "absent", other.battle_rules ? "present" : "absent"));
|
||||
}
|
||||
if (this->battle_rules && (*this->battle_rules != *other.battle_rules)) {
|
||||
string existing_str = this->battle_rules->json().serialize();
|
||||
string new_str = other.battle_rules->json().serialize();
|
||||
throw runtime_error(std::format(
|
||||
"quest version has different battle rules (existing: {}, new: {})",
|
||||
existing_str, new_str));
|
||||
}
|
||||
if (this->challenge_template_index != other.challenge_template_index) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has different challenge template index (existing: {}, new: {})",
|
||||
this->challenge_template_index, other.challenge_template_index));
|
||||
}
|
||||
if (this->challenge_exp_multiplier != other.challenge_exp_multiplier) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has different challenge EXP multiplier (existing: {}, new: {})",
|
||||
this->challenge_exp_multiplier, other.challenge_exp_multiplier));
|
||||
}
|
||||
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));
|
||||
}
|
||||
for (size_t z = 0; z < this->area_for_floor.size(); z++) {
|
||||
const auto& this_fa = this->area_for_floor[z];
|
||||
const auto& other_fa = other.area_for_floor[z];
|
||||
if (this_fa != other_fa) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has different area on floor 0x{:02X} (existing: {}, new: {})",
|
||||
z, phosg::format_data_string(this->area_for_floor.data(), 0x12), phosg::format_data_string(other.area_for_floor.data(), 0x12)));
|
||||
}
|
||||
}
|
||||
if (this->description_flag != other.description_flag) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has different description flag (existing: {:02X}, new: {:02X})",
|
||||
this->description_flag, other.description_flag));
|
||||
}
|
||||
if (!this->available_expression != !other.available_expression) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has available expression but root quest does not, or vice versa (existing: {}, new: {})",
|
||||
this->available_expression ? "present" : "absent", other.available_expression ? "present" : "absent"));
|
||||
}
|
||||
if (this->available_expression && *this->available_expression != *other.available_expression) {
|
||||
string existing_str = this->available_expression->str();
|
||||
string new_str = other.available_expression->str();
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different available expression (existing: {}, new: {})",
|
||||
existing_str, new_str));
|
||||
}
|
||||
if (!this->enabled_expression != !other.enabled_expression) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has enabled expression but root quest does not, or vice versa (existing: {}, new: {})",
|
||||
this->enabled_expression ? "present" : "absent", other.enabled_expression ? "present" : "absent"));
|
||||
}
|
||||
if (this->enabled_expression && *this->enabled_expression != *other.enabled_expression) {
|
||||
string existing_str = this->enabled_expression->str();
|
||||
string new_str = other.enabled_expression->str();
|
||||
throw runtime_error(std::format(
|
||||
"quest version has a different enabled expression (existing: {}, new: {})",
|
||||
existing_str, new_str));
|
||||
}
|
||||
if (this->common_item_set_name != other.common_item_set_name) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has different common table name (existing: {}, new: {})",
|
||||
this->common_item_set_name, other.common_item_set_name));
|
||||
}
|
||||
if (this->common_item_set != other.common_item_set) {
|
||||
throw runtime_error("quest version has different common table");
|
||||
}
|
||||
if (this->rare_item_set_name != other.rare_item_set_name) {
|
||||
throw runtime_error(std::format(
|
||||
"quest version has different rare table name (existing: {}, new: {})",
|
||||
this->rare_item_set_name, other.rare_item_set_name));
|
||||
}
|
||||
if (this->rare_item_set != other.rare_item_set) {
|
||||
throw runtime_error("quest version has different rare table");
|
||||
}
|
||||
if (this->allowed_drop_modes != other.allowed_drop_modes) {
|
||||
throw runtime_error(format("quest version has different allowed drop modes (existing: {:02X}, new: {:02X})",
|
||||
this->allowed_drop_modes, other.allowed_drop_modes));
|
||||
}
|
||||
if (this->default_drop_mode != other.default_drop_mode) {
|
||||
throw runtime_error(format("quest version has different default drop mode (existing: {}, new: {})",
|
||||
phosg::name_for_enum(this->default_drop_mode), phosg::name_for_enum(other.default_drop_mode)));
|
||||
}
|
||||
}
|
||||
|
||||
phosg::JSON QuestMetadata::json() const {
|
||||
auto floors_json = phosg::JSON::list();
|
||||
for (const auto& fa : this->area_for_floor) {
|
||||
floors_json.emplace_back(fa);
|
||||
}
|
||||
return phosg::JSON::dict({
|
||||
{"CategoryID", this->category_id},
|
||||
{"Number", this->quest_number},
|
||||
{"Episode", name_for_episode(this->episode)},
|
||||
{"FloorAssignments", 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)},
|
||||
{"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)},
|
||||
{"CommonItemSetName", this->common_item_set_name.empty() ? phosg::JSON(nullptr) : this->common_item_set_name},
|
||||
{"RareItemSetName", this->rare_item_set_name.empty() ? phosg::JSON(nullptr) : this->rare_item_set_name},
|
||||
{"AllowedDropModes", this->allowed_drop_modes},
|
||||
{"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)},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "CommonItemSet.hh"
|
||||
#include "IntegralExpression.hh"
|
||||
#include "Map.hh"
|
||||
#include "PlayerSubordinates.hh"
|
||||
#include "RareItemSet.hh"
|
||||
|
||||
struct QuestMetadata {
|
||||
// This structure contains configuration that should be the same across all
|
||||
// versions of the quest, except for the name and description strings. This
|
||||
// is used in both the Quest and VersionedQuest structures; in Quest, the
|
||||
// name and description are used only internally.
|
||||
uint32_t category_id = 0xFFFFFFFF;
|
||||
uint32_t quest_number = 0xFFFFFFFF;
|
||||
Episode episode = Episode::NONE;
|
||||
std::array<uint8_t, 0x12> area_for_floor;
|
||||
bool joinable = false;
|
||||
uint8_t max_players = 0x00;
|
||||
std::shared_ptr<const BattleRules> battle_rules;
|
||||
ssize_t challenge_template_index = -1;
|
||||
float challenge_exp_multiplier = -1.0f;
|
||||
int8_t challenge_difficulty = -1;
|
||||
uint8_t description_flag = 0x00;
|
||||
std::shared_ptr<const IntegralExpression> available_expression;
|
||||
std::shared_ptr<const IntegralExpression> enabled_expression;
|
||||
std::string common_item_set_name; // blank = use default
|
||||
std::string rare_item_set_name; // blank = use default
|
||||
std::shared_ptr<const CommonItemSet> common_item_set;
|
||||
std::shared_ptr<const RareItemSet> rare_item_set;
|
||||
uint8_t allowed_drop_modes = 0x00; // 0 = use server default
|
||||
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::string name;
|
||||
std::string short_description;
|
||||
std::string long_description;
|
||||
|
||||
void assign_default_areas(Version version, Episode episode);
|
||||
void assert_compatible(const QuestMetadata& other) const;
|
||||
phosg::JSON json() const;
|
||||
std::string areas_str() const;
|
||||
};
|
||||
+1329
-726
File diff suppressed because it is too large
Load Diff
+16
-6
@@ -5,6 +5,7 @@
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Tools.hh>
|
||||
|
||||
#include "QuestMetadata.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
@@ -19,6 +20,18 @@ struct PSOQuestHeaderDCNTE {
|
||||
/* 0020 */
|
||||
} __packed_ws__(PSOQuestHeaderDCNTE, 0x20);
|
||||
|
||||
struct PSOQuestHeaderDC112000 {
|
||||
/* 0000 */ le_uint32_t code_offset = 0;
|
||||
/* 0004 */ le_uint32_t function_table_offset = 0;
|
||||
/* 0008 */ le_uint32_t size = 0;
|
||||
/* 000C */ le_uint16_t unknown_a1 = 0;
|
||||
/* 000E */ le_uint16_t unknown_a2 = 0;
|
||||
/* 0010 */ pstring<TextEncoding::MARKED, 0x20> name;
|
||||
/* 0030 */ pstring<TextEncoding::MARKED, 0x80> short_description;
|
||||
/* 00B0 */ pstring<TextEncoding::MARKED, 0x120> long_description;
|
||||
/* 01D0 */
|
||||
} __packed_ws__(PSOQuestHeaderDC112000, 0x1D0);
|
||||
|
||||
struct PSOQuestHeaderDC { // Same format for DC v1 and v2
|
||||
/* 0000 */ le_uint32_t code_offset = 0;
|
||||
/* 0004 */ le_uint32_t function_table_offset = 0;
|
||||
@@ -100,7 +113,8 @@ std::string disassemble_quest_script(
|
||||
bool reassembly_mode = false,
|
||||
bool use_qedit_names = false);
|
||||
|
||||
struct QuestMetadata {
|
||||
struct AssembledQuestScript {
|
||||
std::string data;
|
||||
int64_t quest_number = -1;
|
||||
Version version = Version::UNKNOWN;
|
||||
uint8_t language = 0xFF;
|
||||
@@ -111,13 +125,9 @@ struct QuestMetadata {
|
||||
std::string short_description;
|
||||
std::string long_description;
|
||||
};
|
||||
struct AssembledQuestScript {
|
||||
std::string data;
|
||||
QuestMetadata metadata;
|
||||
};
|
||||
AssembledQuestScript assemble_quest_script(
|
||||
const std::string& text,
|
||||
const std::vector<std::string>& script_include_directories,
|
||||
const std::vector<std::string>& native_include_directories);
|
||||
|
||||
Episode find_quest_episode_from_script(const void* data, size_t size, Version version);
|
||||
void populate_quest_metadata_from_script(QuestMetadata& meta, const void* data, size_t size, Version version, uint8_t language);
|
||||
|
||||
+3
-4
@@ -28,11 +28,10 @@ string RareItemSet::ExpandedDrop::str(shared_ptr<const ItemNameIndex> name_index
|
||||
}
|
||||
|
||||
uint32_t RareItemSet::expand_rate(uint8_t pc) {
|
||||
// To compute the actual drop rare drop rate from pc, first decode pc into
|
||||
// shift and value:
|
||||
// To compute the actual rare drop rate from pc, first decode pc:
|
||||
// pc = bits SSSSSVVV
|
||||
// shift = S - 4 (so shift is 0-27)
|
||||
// value = V + 7 (so value is 7-14)
|
||||
// shift = S - 4 (so shift is 0-27)
|
||||
// value = V + 7 (so value is 7-14)
|
||||
// Then, take the value 0x00000002, shift it left by shift (0-27), and
|
||||
// multiply the result by value (7-14) to get the actual drop rate. The result
|
||||
// is a probability out of 0xFFFFFFFF (so 0x40000000 means the item will drop
|
||||
|
||||
+253
-188
@@ -341,14 +341,13 @@ static asio::awaitable<void> on_login_complete(shared_ptr<Client> c) {
|
||||
!c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE))) {
|
||||
shared_ptr<const Quest> q;
|
||||
try {
|
||||
int64_t quest_num = s->enable_send_function_call_quest_numbers.at(c->specific_version);
|
||||
q = s->default_quest_index->get(quest_num);
|
||||
q = s->quest_index->get(s->enable_send_function_call_quest_numbers.at(c->specific_version));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
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(is_ep3(c->version()) ? Version::GC_V3 : c->version(), 1);
|
||||
auto vq = q->version(c->version(), 1);
|
||||
if (vq) {
|
||||
c->set_flag(Client::Flag::HAS_SEND_FUNCTION_CALL);
|
||||
c->set_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE);
|
||||
@@ -364,12 +363,14 @@ 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->name);
|
||||
c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name);
|
||||
string bin_filename = vq->bin_filename();
|
||||
string dat_filename = vq->dat_filename();
|
||||
string xb_filename = vq->xb_filename();
|
||||
send_open_quest_file(c, bin_filename, bin_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
|
||||
send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
|
||||
send_open_quest_file(
|
||||
c, bin_filename, bin_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents);
|
||||
send_open_quest_file(
|
||||
c, dat_filename, dat_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
|
||||
|
||||
if (!is_v1_or_v2(c->version())) {
|
||||
send_command(c, 0xAC, 0x00);
|
||||
@@ -433,10 +434,6 @@ asio::awaitable<void> start_proxy_session(shared_ptr<Client> c, const string& ho
|
||||
if (!s->proxy_allow_save_files) {
|
||||
c->clear_flag(Client::Flag::PROXY_SAVE_FILES);
|
||||
}
|
||||
if (c->version() == Version::GC_EP3) {
|
||||
send_ep3_media_update(c, 4, 0, "");
|
||||
c->clear_flag(Client::Flag::HAS_EP3_MEDIA_UPDATES);
|
||||
}
|
||||
|
||||
string netloc_str = std::format("{}:{}", host, port);
|
||||
c->log.info_f("Connecting to {}", netloc_str);
|
||||
@@ -484,6 +481,13 @@ asio::awaitable<void> start_proxy_session(shared_ptr<Client> c, const string& ho
|
||||
phosg::TerminalFormat::FG_RED);
|
||||
c->proxy_session = make_shared<ProxySession>(channel, pc);
|
||||
|
||||
if (c->version() == Version::GC_EP3) {
|
||||
send_ep3_media_update(c, 4, 0, "");
|
||||
c->clear_flag(Client::Flag::HAS_EP3_MEDIA_UPDATES);
|
||||
c->proxy_session->suppress_next_ep3_media_update_confirmation = true;
|
||||
}
|
||||
send_change_event(c, 0x00);
|
||||
|
||||
c->log.info_f("Server channel connected");
|
||||
asio::co_spawn(*s->io_context, handle_proxy_server_commands(c, c->proxy_session, channel), asio::detached);
|
||||
}
|
||||
@@ -1397,6 +1401,7 @@ static asio::awaitable<void> on_93_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
c->sub_version = base_cmd.sub_version;
|
||||
// c->channel->language set after version check
|
||||
c->bb_character_index = base_cmd.character_slot;
|
||||
c->bb_bank_character_index = base_cmd.character_slot;
|
||||
c->bb_connection_phase = base_cmd.connection_phase;
|
||||
c->bb_client_code = base_cmd.client_code;
|
||||
c->bb_security_token = base_cmd.security_token;
|
||||
@@ -1968,7 +1973,7 @@ static asio::awaitable<void> on_CA_Ep3(shared_ptr<Client> c, Channel::Message& m
|
||||
l->battle_record = make_shared<Episode3::BattleRecord>(s->ep3_behavior_flags);
|
||||
for (auto existing_c : l->clients) {
|
||||
if (existing_c) {
|
||||
auto existing_p = existing_c->character();
|
||||
auto existing_p = existing_c->character_file();
|
||||
PlayerLobbyDataDCGC lobby_data;
|
||||
lobby_data.name.encode(existing_p->disp.name.decode(existing_c->language()), c->language());
|
||||
lobby_data.player_tag = 0x00010000;
|
||||
@@ -2149,11 +2154,10 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
|
||||
case MenuID::QUEST_EP1:
|
||||
case MenuID::QUEST_EP2: {
|
||||
bool is_download_quest = !c->lobby.lock();
|
||||
auto quest_index = s->quest_index(c->version());
|
||||
if (!quest_index) {
|
||||
if (!s->quest_index) {
|
||||
send_quest_info(c, "$C7Quests are not available.", 0x00, is_download_quest);
|
||||
} else {
|
||||
auto q = quest_index->get(cmd.item_id);
|
||||
auto q = s->quest_index->get(cmd.item_id);
|
||||
if (!q) {
|
||||
send_quest_info(c, "$C4Quest does not\nexist.", 0x00, is_download_quest);
|
||||
} else {
|
||||
@@ -2161,12 +2165,22 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
|
||||
if (!vq) {
|
||||
send_quest_info(c, "$C4Quest does not\nexist for this game\nversion.", 0x00, is_download_quest);
|
||||
} else {
|
||||
send_quest_info(c, vq->long_description, vq->description_flag, is_download_quest);
|
||||
send_quest_info(c, vq->meta.long_description, vq->meta.description_flag, is_download_quest);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MenuID::QUEST_EP3: {
|
||||
auto map = s->ep3_download_map_index->get(cmd.item_id);
|
||||
if (!map) {
|
||||
send_quest_info(c, "$C4Map does not exist.", 0x00, true);
|
||||
} else {
|
||||
auto vm = map->version(c->language());
|
||||
send_quest_info(c, vm->map->description.decode(vm->language), 0x00, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MenuID::GAME: {
|
||||
auto game = s->find_lobby(cmd.item_id);
|
||||
@@ -2209,7 +2223,7 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
|
||||
const char* version_token = (game_c->version() != c->version())
|
||||
? version_tokens.at(static_cast<size_t>(game_c->version()))
|
||||
: "";
|
||||
auto player = game_c->character();
|
||||
auto player = game_c->character_file();
|
||||
string name = escape_player_name(player->disp.name.decode(game_c->language()));
|
||||
info += std::format("{}{}\n {} Lv{} {}\n",
|
||||
name,
|
||||
@@ -2241,7 +2255,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->name);
|
||||
info += remove_color(game->quest->meta.name);
|
||||
info += "\n";
|
||||
} else if (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
|
||||
info += "$C6Quest in progress\n";
|
||||
@@ -2252,19 +2266,19 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
|
||||
}
|
||||
|
||||
switch (game->drop_mode) {
|
||||
case Lobby::DropMode::DISABLED:
|
||||
case ServerDropMode::DISABLED:
|
||||
info += "$C6Drops disabled$C7\n";
|
||||
break;
|
||||
case Lobby::DropMode::CLIENT:
|
||||
case ServerDropMode::CLIENT:
|
||||
info += "$C6Client drops$C7\n";
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_SHARED:
|
||||
case ServerDropMode::SERVER_SHARED:
|
||||
info += "$C6Server drops$C7\n";
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_PRIVATE:
|
||||
case ServerDropMode::SERVER_PRIVATE:
|
||||
info += "$C6Private drops$C7\n";
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_DUPLICATE:
|
||||
case ServerDropMode::SERVER_DUPLICATE:
|
||||
info += "$C6Duplicate drops$C7\n";
|
||||
break;
|
||||
}
|
||||
@@ -2366,6 +2380,10 @@ static void on_quest_loaded(shared_ptr<Lobby> l) {
|
||||
if (!l->quest) {
|
||||
throw logic_error("on_quest_loaded called without a quest loaded");
|
||||
}
|
||||
auto leader_c = l->clients.at(l->leader_id);
|
||||
if (!leader_c) {
|
||||
throw std::logic_error("lobby has no leader");
|
||||
}
|
||||
|
||||
// Replace the free-play map with the quest's map
|
||||
l->load_maps();
|
||||
@@ -2387,18 +2405,24 @@ static void on_quest_loaded(shared_ptr<Lobby> l) {
|
||||
}
|
||||
|
||||
lc->delete_overlay();
|
||||
if (l->quest->battle_rules) {
|
||||
lc->use_default_bank();
|
||||
lc->create_battle_overlay(l->quest->battle_rules, s->level_table(lc->version()));
|
||||
lc->log.info_f("Created battle overlay");
|
||||
} else if (l->quest->challenge_template_index >= 0 && !is_v4(lc->version())) {
|
||||
// On BB, the client will send a sequence of DF commands that creates the
|
||||
// overlay; on non-BB, we do it at quest start time instead (hence the
|
||||
// version check above).
|
||||
lc->use_default_bank();
|
||||
lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(lc->version()));
|
||||
|
||||
if ((l->quest->meta.challenge_template_index >= 0) && !is_v4(leader_c->version())) {
|
||||
// If the leader is BB, they will send an 02DF command that will create
|
||||
// the overlays later; on other versions, we do it at quest start time
|
||||
// (now) instead, hence the version check above.
|
||||
if (is_v4(lc->version())) {
|
||||
lc->change_bank(lc->bb_character_index);
|
||||
}
|
||||
lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(lc->version()));
|
||||
lc->log.info_f("Created challenge overlay");
|
||||
l->assign_inventory_and_bank_item_ids(lc, true);
|
||||
|
||||
} else if (l->quest->meta.battle_rules) {
|
||||
if (is_v4(lc->version())) {
|
||||
lc->change_bank(lc->bb_character_index);
|
||||
}
|
||||
lc->create_battle_overlay(l->quest->meta.battle_rules, s->level_table(lc->version()));
|
||||
lc->log.info_f("Created battle overlay");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2412,16 +2436,16 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q, bool substi
|
||||
}
|
||||
|
||||
// Only allow loading battle/challenge quests if the game mode is correct
|
||||
if ((q->challenge_template_index >= 0) != (l->mode == GameMode::CHALLENGE)) {
|
||||
if ((q->meta.challenge_template_index >= 0) != (l->mode == GameMode::CHALLENGE)) {
|
||||
throw runtime_error("incorrect game mode");
|
||||
}
|
||||
if ((q->battle_rules != nullptr) != (l->mode == GameMode::BATTLE)) {
|
||||
if ((q->meta.battle_rules != nullptr) != (l->mode == GameMode::BATTLE)) {
|
||||
throw runtime_error("incorrect game mode");
|
||||
}
|
||||
|
||||
auto s = l->require_server_state();
|
||||
|
||||
if (q->joinable) {
|
||||
if (q->meta.joinable) {
|
||||
l->set_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS);
|
||||
} else {
|
||||
l->set_flag(Lobby::Flag::QUEST_IN_PROGRESS);
|
||||
@@ -2431,7 +2455,14 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q, bool substi
|
||||
|
||||
l->quest = q;
|
||||
if (l->episode != Episode::EP3) {
|
||||
l->episode = q->episode;
|
||||
l->episode = q->meta.episode;
|
||||
}
|
||||
if (l->quest->meta.allowed_drop_modes) {
|
||||
l->allowed_drop_modes = l->quest->meta.allowed_drop_modes;
|
||||
l->drop_mode = l->quest->meta.default_drop_mode;
|
||||
}
|
||||
if (l->quest->meta.challenge_difficulty >= 0) {
|
||||
l->difficulty = l->quest->meta.challenge_difficulty;
|
||||
}
|
||||
l->create_item_creator();
|
||||
|
||||
@@ -2450,13 +2481,15 @@ 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->name);
|
||||
lc->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name);
|
||||
|
||||
string bin_filename = vq->bin_filename();
|
||||
string dat_filename = vq->dat_filename();
|
||||
string xb_filename = vq->xb_filename();
|
||||
send_open_quest_file(lc, bin_filename, bin_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
|
||||
send_open_quest_file(lc, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
|
||||
send_open_quest_file(
|
||||
lc, bin_filename, bin_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents);
|
||||
send_open_quest_file(
|
||||
lc, dat_filename, dat_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
|
||||
|
||||
// There is no such thing as command AC (quest barrier) on PSO V1 and V2;
|
||||
// quests just start immediately when they're done downloading. (This is
|
||||
@@ -2514,24 +2547,11 @@ static asio::awaitable<void> on_10_main_menu(shared_ptr<Client> c, uint32_t item
|
||||
break;
|
||||
|
||||
case MainMenuItemID::DOWNLOAD_QUESTS: {
|
||||
QuestMenuType menu_type = QuestMenuType::DOWNLOAD;
|
||||
if (is_ep3(c->version())) {
|
||||
menu_type = QuestMenuType::EP3_DOWNLOAD;
|
||||
// Episode 3 has only download quests, not online quests, so this is
|
||||
// always the download quest menu. (Episode 3 does actually have
|
||||
// online quests, but they're served via a server data request
|
||||
// instead of the file download paradigm that other versions use.)
|
||||
auto quest_index = s->quest_index(c->version());
|
||||
uint16_t version_flags = (1 << static_cast<size_t>(c->version()));
|
||||
const auto& categories = quest_index->categories(menu_type, Episode::EP3, version_flags);
|
||||
if (categories.size() == 1) {
|
||||
auto quests = quest_index->filter(Episode::EP3, version_flags, categories[0]->category_id);
|
||||
send_quest_menu(c, quests, true);
|
||||
break;
|
||||
}
|
||||
send_ep3_download_quest_menu(c);
|
||||
} else {
|
||||
send_quest_categories_menu(c, QuestMenuType::DOWNLOAD, Episode::NONE);
|
||||
}
|
||||
|
||||
send_quest_categories_menu(c, s->quest_index(c->version()), menu_type, Episode::NONE);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2781,9 +2801,13 @@ 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();
|
||||
auto quest_index = s->quest_index(c->version());
|
||||
if (!quest_index) {
|
||||
if (!s->quest_index) {
|
||||
send_lobby_message_box(c, "$C7Quests are not available.");
|
||||
co_return;
|
||||
}
|
||||
@@ -2796,18 +2820,21 @@ static asio::awaitable<void> on_10_quest_categories(shared_ptr<Client> c, uint32
|
||||
include_condition = l->quest_include_condition();
|
||||
}
|
||||
|
||||
const auto& quests = quest_index->filter(episode, version_flags, item_id, include_condition);
|
||||
const auto& quests = s->quest_index->filter(episode, version_flags, item_id, include_condition);
|
||||
send_quest_menu(c, quests, !l);
|
||||
}
|
||||
|
||||
static asio::awaitable<void> on_10_quest_menu(shared_ptr<Client> c, uint32_t item_id) {
|
||||
if (is_ep3(c->version())) {
|
||||
throw runtime_error("Episode 1/2/4 quests cannot be downloaded by Ep3 clients");
|
||||
}
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto quest_index = s->quest_index(c->version());
|
||||
if (!quest_index) {
|
||||
if (!s->quest_index) {
|
||||
send_lobby_message_box(c, "$C7Quests are not\navailable.");
|
||||
co_return;
|
||||
}
|
||||
auto q = quest_index->get(item_id);
|
||||
auto q = s->quest_index->get(item_id);
|
||||
if (!q) {
|
||||
send_lobby_message_box(c, "$C7Quest does not exist.");
|
||||
co_return;
|
||||
@@ -2822,7 +2849,7 @@ static asio::awaitable<void> on_10_quest_menu(shared_ptr<Client> c, uint32_t ite
|
||||
}
|
||||
|
||||
if (l) {
|
||||
if (q->episode == Episode::EP3) {
|
||||
if (q->meta.episode == Episode::EP3) {
|
||||
send_lobby_message_box(c, "$C7Episode 3 quests\ncannot be loaded\nvia this interface.");
|
||||
co_return;
|
||||
}
|
||||
@@ -2842,26 +2869,34 @@ static asio::awaitable<void> on_10_quest_menu(shared_ptr<Client> c, uint32_t ite
|
||||
send_lobby_message_box(c, "$C7Quest does not exist\nfor this game version.");
|
||||
co_return;
|
||||
}
|
||||
// Episode 3 uses the download quest commands (A6/A7) but does not
|
||||
// expect the server to have already encrypted the quest files, unlike
|
||||
// other versions.
|
||||
// TODO: This is not true for Episode 3 Trial Edition. We also would
|
||||
// have to convert the map to a MapDefinitionTrial, though.
|
||||
if (is_ep3(vq->version)) {
|
||||
send_open_quest_file(c, q->name, vq->bin_filename(), "", vq->quest_number, QuestFileType::EPISODE_3, vq->bin_contents);
|
||||
} else {
|
||||
vq = vq->create_download_quest(c->language());
|
||||
string xb_filename = vq->xb_filename();
|
||||
QuestFileType type = vq->pvr_contents ? QuestFileType::DOWNLOAD_WITH_PVR : QuestFileType::DOWNLOAD_WITHOUT_PVR;
|
||||
send_open_quest_file(c, q->name, vq->bin_filename(), xb_filename, vq->quest_number, type, vq->bin_contents);
|
||||
send_open_quest_file(c, q->name, vq->dat_filename(), xb_filename, vq->quest_number, type, vq->dat_contents);
|
||||
if (vq->pvr_contents) {
|
||||
send_open_quest_file(c, q->name, vq->pvr_filename(), xb_filename, vq->quest_number, type, vq->pvr_contents);
|
||||
}
|
||||
vq = vq->create_download_quest(c->language());
|
||||
string xb_filename = vq->xb_filename();
|
||||
QuestFileType type = vq->pvr_contents ? QuestFileType::DOWNLOAD_WITH_PVR : QuestFileType::DOWNLOAD_WITHOUT_PVR;
|
||||
send_open_quest_file(c, q->meta.name, vq->bin_filename(), xb_filename, vq->meta.quest_number, type, vq->bin_contents);
|
||||
send_open_quest_file(c, q->meta.name, vq->dat_filename(), xb_filename, vq->meta.quest_number, type, vq->dat_contents);
|
||||
if (vq->pvr_contents) {
|
||||
send_open_quest_file(c, q->meta.name, vq->pvr_filename(), xb_filename, vq->meta.quest_number, type, vq->pvr_contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static asio::awaitable<void> on_10_ep3_download_quest_menu(shared_ptr<Client> c, uint32_t item_id) {
|
||||
auto s = c->require_server_state();
|
||||
if (!is_ep3(c->version())) {
|
||||
throw runtime_error("Episode 3 quests can only be downloaded by Ep3 clients");
|
||||
}
|
||||
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 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)));
|
||||
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;
|
||||
}
|
||||
|
||||
static asio::awaitable<void> on_10_patch_switches(shared_ptr<Client> c, uint32_t item_id) {
|
||||
if (item_id == PatchesMenuItemID::GO_BACK) {
|
||||
send_main_menu(c);
|
||||
@@ -2920,7 +2955,7 @@ static asio::awaitable<void> on_10_tournament_entries(
|
||||
co_return;
|
||||
}
|
||||
if (team_name.empty()) {
|
||||
team_name = c->character()->disp.name.decode(c->language());
|
||||
team_name = c->character_file()->disp.name.decode(c->language());
|
||||
team_name += std::format("/{:X}", c->login->account->account_id);
|
||||
}
|
||||
uint16_t tourn_num = item_id >> 16;
|
||||
@@ -2956,80 +2991,83 @@ partner (if any) and opponent(s).",
|
||||
}
|
||||
}
|
||||
|
||||
template <bool UsesUTF16>
|
||||
static asio::awaitable<void> on_10(shared_ptr<Client> c, Channel::Message& msg) {
|
||||
bool uses_utf16 = ::uses_utf16(c->version());
|
||||
constexpr TextEncoding Encoding = UsesUTF16 ? TextEncoding::UTF16 : TextEncoding::MARKED;
|
||||
|
||||
uint32_t menu_id;
|
||||
uint32_t item_id;
|
||||
string team_name;
|
||||
const auto& base_cmd = check_size_t<C_MenuSelectionBase_10>(msg.data, 0xFFFF);
|
||||
|
||||
string name;
|
||||
string password;
|
||||
|
||||
if (msg.data.size() > sizeof(C_MenuSelection_10_Flag00)) {
|
||||
if (uses_utf16) {
|
||||
// TODO: We can support the Flag03 variant here, but PC/BB probably never
|
||||
// actually use it.
|
||||
const auto& cmd = check_size_t<C_MenuSelection_PC_BB_10_Flag02>(msg.data);
|
||||
password = cmd.password.decode(c->language());
|
||||
menu_id = cmd.basic_cmd.menu_id;
|
||||
item_id = cmd.basic_cmd.item_id;
|
||||
} else if (msg.data.size() > sizeof(C_MenuSelection_DC_V3_10_Flag02)) {
|
||||
const auto& cmd = check_size_t<C_MenuSelection_DC_V3_10_Flag03>(msg.data);
|
||||
team_name = cmd.name.decode(c->language());
|
||||
password = cmd.password.decode(c->language());
|
||||
menu_id = cmd.basic_cmd.menu_id;
|
||||
item_id = cmd.basic_cmd.item_id;
|
||||
} else {
|
||||
const auto& cmd = check_size_t<C_MenuSelection_DC_V3_10_Flag02>(msg.data);
|
||||
password = cmd.password.decode(c->language());
|
||||
menu_id = cmd.basic_cmd.menu_id;
|
||||
item_id = cmd.basic_cmd.item_id;
|
||||
switch (msg.data.size()) {
|
||||
case sizeof(C_MenuSelectionBase_10):
|
||||
break;
|
||||
case sizeof(C_MenuSelectionWithNameT_10<Encoding>): {
|
||||
static_assert(
|
||||
sizeof(C_MenuSelectionWithNameT_10<Encoding>) == sizeof(C_MenuSelectionWithPasswordT_10<Encoding>),
|
||||
"Single-flag 10 commands should be the same size");
|
||||
if (msg.flag & 1) {
|
||||
const auto& cmd = check_size_t<C_MenuSelectionWithNameT_10<Encoding>>(msg.data);
|
||||
name = cmd.name.decode(c->language());
|
||||
} else if (msg.flag & 2) {
|
||||
const auto& cmd = check_size_t<C_MenuSelectionWithPasswordT_10<Encoding>>(msg.data);
|
||||
password = cmd.password.decode(c->language());
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const auto& cmd = check_size_t<C_MenuSelection_10_Flag00>(msg.data);
|
||||
menu_id = cmd.menu_id;
|
||||
item_id = cmd.item_id;
|
||||
case sizeof(C_MenuSelectionWithNameAndPasswordT_10<Encoding>): {
|
||||
const auto& cmd = check_size_t<C_MenuSelectionWithNameAndPasswordT_10<Encoding>>(msg.data);
|
||||
name = cmd.name.decode(c->language());
|
||||
password = cmd.password.decode(c->language());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw runtime_error("unknown menu selection format");
|
||||
}
|
||||
|
||||
auto s = c->require_server_state();
|
||||
switch (menu_id) {
|
||||
switch (base_cmd.menu_id) {
|
||||
case MenuID::MAIN:
|
||||
co_await on_10_main_menu(c, item_id);
|
||||
co_await on_10_main_menu(c, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::CLEAR_LICENSE_CONFIRMATION:
|
||||
co_await on_10_clear_license_confirmation(c, item_id);
|
||||
co_await on_10_clear_license_confirmation(c, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::INFORMATION:
|
||||
co_await on_10_information(c, item_id);
|
||||
co_await on_10_information(c, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::PROXY_OPTIONS:
|
||||
co_await on_10_proxy_options(c, item_id);
|
||||
co_await on_10_proxy_options(c, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::PROXY_DESTINATIONS:
|
||||
co_await on_10_proxy_destinations(c, item_id);
|
||||
co_await on_10_proxy_destinations(c, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::GAME:
|
||||
co_await on_10_game_menu(c, item_id, std::move(password));
|
||||
co_await on_10_game_menu(c, base_cmd.item_id, std::move(password));
|
||||
break;
|
||||
case MenuID::QUEST_CATEGORIES_EP1:
|
||||
case MenuID::QUEST_CATEGORIES_EP2:
|
||||
co_await on_10_quest_categories(c, item_id);
|
||||
co_await on_10_quest_categories(c, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::QUEST_EP1:
|
||||
case MenuID::QUEST_EP2:
|
||||
co_await on_10_quest_menu(c, item_id);
|
||||
co_await on_10_quest_menu(c, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::QUEST_EP3:
|
||||
co_await on_10_ep3_download_quest_menu(c, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::PATCH_SWITCHES:
|
||||
co_await on_10_patch_switches(c, item_id);
|
||||
co_await on_10_patch_switches(c, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::PROGRAMS:
|
||||
co_await on_10_programs(c, item_id);
|
||||
co_await on_10_programs(c, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::TOURNAMENTS_FOR_SPEC:
|
||||
case MenuID::TOURNAMENTS:
|
||||
co_await on_10_tournaments(c, menu_id, item_id);
|
||||
co_await on_10_tournaments(c, base_cmd.menu_id, base_cmd.item_id);
|
||||
break;
|
||||
case MenuID::TOURNAMENT_ENTRIES:
|
||||
co_await on_10_tournament_entries(c, item_id, std::move(team_name), std::move(password));
|
||||
co_await on_10_tournament_entries(c, base_cmd.item_id, std::move(name), std::move(password));
|
||||
break;
|
||||
default:
|
||||
send_message_box(c, "Incorrect menu ID");
|
||||
@@ -3189,7 +3227,7 @@ static asio::awaitable<void> on_A2(shared_ptr<Client> c, Channel::Message& msg)
|
||||
}
|
||||
}
|
||||
|
||||
send_quest_categories_menu(c, s->quest_index(c->version()), menu_type, l->episode);
|
||||
send_quest_categories_menu(c, menu_type, l->episode);
|
||||
l->set_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS);
|
||||
}
|
||||
|
||||
@@ -3333,7 +3371,7 @@ static asio::awaitable<void> on_61_98(shared_ptr<Client> c, Channel::Message& ms
|
||||
c->clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_PLAYER_STATES);
|
||||
}
|
||||
|
||||
auto player = c->character();
|
||||
auto player = c->character_file();
|
||||
|
||||
switch (c->version()) {
|
||||
case Version::DC_NTE:
|
||||
@@ -3638,7 +3676,7 @@ static asio::awaitable<void> on_06(shared_ptr<Client> c, Channel::Message& msg)
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto p = c->character();
|
||||
auto p = c->character_file();
|
||||
string from_name = p->disp.name.decode(c->language());
|
||||
static const string whisper_text = "(whisper)";
|
||||
for (size_t x = 0; x < l->max_clients; x++) {
|
||||
@@ -3692,6 +3730,7 @@ static asio::awaitable<void> on_E3_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
if (c->bb_connection_phase != 0x00) {
|
||||
c->save_and_unload_character();
|
||||
c->bb_character_index = cmd.character_index;
|
||||
c->bb_bank_character_index = cmd.character_index;
|
||||
send_approve_player_choice_bb(c);
|
||||
|
||||
} else {
|
||||
@@ -3703,8 +3742,9 @@ 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->bb_character_index = index;
|
||||
c->bb_bank_character_index = index;
|
||||
try {
|
||||
auto preview = c->character()->to_preview();
|
||||
auto preview = c->character_file()->to_preview();
|
||||
send_player_preview_bb(c, c->bb_character_index, &preview);
|
||||
|
||||
} catch (const exception& e) {
|
||||
@@ -3789,7 +3829,7 @@ static asio::awaitable<void> on_E8_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
}
|
||||
}
|
||||
if (c->login && new_gc.guild_card_number == c->login->account->account_id) {
|
||||
c->character(true, false)->guild_card.description = new_gc.description;
|
||||
c->character_file(true, false)->guild_card.description = new_gc.description;
|
||||
c->log.info_f("Updated character's guild card");
|
||||
}
|
||||
break;
|
||||
@@ -3915,18 +3955,20 @@ static asio::awaitable<void> on_E5_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (c->character(false).get()) {
|
||||
if (c->character_file(false).get()) {
|
||||
throw runtime_error("player already exists");
|
||||
}
|
||||
|
||||
c->bb_character_index = -1;
|
||||
c->bb_bank_character_index = -1;
|
||||
c->system_file(); // Ensure system file is loaded
|
||||
c->bb_character_index = cmd.character_index;
|
||||
c->bb_bank_character_index = cmd.character_index;
|
||||
|
||||
bool should_send_approve = true;
|
||||
if (c->bb_connection_phase == 0x03) { // Dressing room
|
||||
try {
|
||||
c->character()->disp.apply_dressing_room(cmd.preview);
|
||||
c->character_file()->disp.apply_dressing_room(cmd.preview);
|
||||
} catch (const exception& e) {
|
||||
send_message_box(c, std::format("$C6Character could not be modified:\n{}", e.what()));
|
||||
should_send_approve = false;
|
||||
@@ -3951,17 +3993,17 @@ static asio::awaitable<void> on_ED_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
switch (msg.command) {
|
||||
case 0x01ED: {
|
||||
const auto& cmd = check_size_t<C_UpdateOptionFlags_BB_01ED>(msg.data);
|
||||
c->character(true, false)->option_flags = cmd.option_flags;
|
||||
c->character_file(true, false)->option_flags = cmd.option_flags;
|
||||
break;
|
||||
}
|
||||
case 0x02ED: {
|
||||
const auto& cmd = check_size_t<C_UpdateSymbolChats_BB_02ED>(msg.data);
|
||||
c->character(true, false)->symbol_chats = cmd.symbol_chats;
|
||||
c->character_file(true, false)->symbol_chats = cmd.symbol_chats;
|
||||
break;
|
||||
}
|
||||
case 0x03ED: {
|
||||
const auto& cmd = check_size_t<C_UpdateChatShortcuts_BB_03ED>(msg.data);
|
||||
c->character(true, false)->shortcuts = cmd.chat_shortcuts;
|
||||
c->character_file(true, false)->shortcuts = cmd.chat_shortcuts;
|
||||
break;
|
||||
}
|
||||
case 0x04ED: {
|
||||
@@ -3978,17 +4020,17 @@ static asio::awaitable<void> on_ED_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
}
|
||||
case 0x06ED: {
|
||||
const auto& cmd = check_size_t<C_UpdateTechMenu_BB_06ED>(msg.data);
|
||||
c->character(true, false)->tech_menu_shortcut_entries = cmd.tech_menu;
|
||||
c->character_file(true, false)->tech_menu_shortcut_entries = cmd.tech_menu;
|
||||
break;
|
||||
}
|
||||
case 0x07ED: {
|
||||
const auto& cmd = check_size_t<C_UpdateCustomizeMenu_BB_07ED>(msg.data);
|
||||
c->character()->disp.config = cmd.customize;
|
||||
c->character_file()->disp.config = cmd.customize;
|
||||
break;
|
||||
}
|
||||
case 0x08ED: {
|
||||
const auto& cmd = check_size_t<C_UpdateChallengeRecords_BB_08ED>(msg.data);
|
||||
c->character(true, false)->challenge_records = cmd.records;
|
||||
c->character_file(true, false)->challenge_records = cmd.records;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -4003,7 +4045,7 @@ static asio::awaitable<void> on_E7_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
// TODO: In the future, we shouldn't need to trust any of the client's data
|
||||
// here. We should instead verify our copy of the player against what the
|
||||
// client sent, and alert on anything that's out of sync.
|
||||
auto p = c->character();
|
||||
auto p = c->character_file();
|
||||
p->challenge_records = cmd.char_file.challenge_records;
|
||||
p->battle_records = cmd.char_file.battle_records;
|
||||
p->death_count = cmd.char_file.death_count;
|
||||
@@ -4045,8 +4087,15 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
if (!l->quest) {
|
||||
throw runtime_error("challenge mode character template config command sent in non-challenge game");
|
||||
}
|
||||
auto leader_c = l->clients.at(l->leader_id);
|
||||
if (!leader_c) {
|
||||
throw logic_error("lobby has no leader");
|
||||
}
|
||||
if (leader_c != c) {
|
||||
throw runtime_error("non-leader sent 02DF command");
|
||||
}
|
||||
auto vq = l->quest->version(Version::BB_V4, c->language());
|
||||
if (vq->challenge_template_index != static_cast<ssize_t>(cmd.template_index)) {
|
||||
if (vq->meta.challenge_template_index != static_cast<ssize_t>(cmd.template_index)) {
|
||||
throw runtime_error("challenge template index in quest metadata does not match index sent by client");
|
||||
}
|
||||
|
||||
@@ -4055,11 +4104,14 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
}
|
||||
|
||||
for (auto lc : l->clients) {
|
||||
// On non-BB, there is no DF command, and overlays are created at quest
|
||||
// start time instead, hence the version check here.
|
||||
if (lc && is_v4(lc->version())) {
|
||||
lc->use_default_bank();
|
||||
lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(lc->version()));
|
||||
// See comment in on_quest_loaded about when the leader is responsible
|
||||
// for creating challenge overlays vs. when the server should do it at
|
||||
// quest load time
|
||||
if (lc) {
|
||||
if (is_v4(lc->version())) {
|
||||
lc->change_bank(lc->bb_character_index);
|
||||
}
|
||||
lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(lc->version()));
|
||||
lc->log.info_f("Created challenge overlay");
|
||||
l->assign_inventory_and_bank_item_ids(lc, true);
|
||||
}
|
||||
@@ -4071,6 +4123,12 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
|
||||
case 0x03DF: {
|
||||
const auto& cmd = check_size_t<C_SetChallengeModeDifficulty_BB_03DF>(msg.data);
|
||||
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) {
|
||||
throw runtime_error("incorrect difficulty level");
|
||||
}
|
||||
if (l->difficulty != cmd.difficulty) {
|
||||
l->difficulty = cmd.difficulty;
|
||||
l->create_item_creator();
|
||||
@@ -4080,8 +4138,13 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
}
|
||||
|
||||
case 0x04DF: {
|
||||
const auto& cmd = check_size_t<C_SetChallengeModeEXPMultiplier_BB_04DF>(msg.data);
|
||||
l->challenge_exp_multiplier = cmd.exp_multiplier;
|
||||
check_size_t<C_SetChallengeModeEXPMultiplier_BB_04DF>(msg.data);
|
||||
if (!l->quest) {
|
||||
throw runtime_error("challenge mode difficulty config command sent in non-challenge game");
|
||||
}
|
||||
l->challenge_exp_multiplier = (l->quest->meta.challenge_exp_multiplier < 0)
|
||||
? 1.0
|
||||
: l->quest->meta.challenge_exp_multiplier;
|
||||
l->log.info_f("(Challenge mode) EXP multiplier set to {:g}", l->challenge_exp_multiplier);
|
||||
break;
|
||||
}
|
||||
@@ -4107,7 +4170,7 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
|
||||
case 0x07DF: {
|
||||
const auto& cmd = check_size_t<C_CreateChallengeModeAwardItem_BB_07DF>(msg.data);
|
||||
auto p = c->character(true, false);
|
||||
auto p = c->character_file(true, false);
|
||||
auto& award_state = (l->episode == Episode::EP2)
|
||||
? p->challenge_records.ep2_online_award_state
|
||||
: p->challenge_records.ep1_online_award_state;
|
||||
@@ -4155,7 +4218,7 @@ static asio::awaitable<void> on_C0(shared_ptr<Client> c, Channel::Message&) {
|
||||
}
|
||||
|
||||
static asio::awaitable<void> on_C2(shared_ptr<Client> c, Channel::Message& msg) {
|
||||
c->character()->choice_search_config = check_size_t<ChoiceSearchConfig>(msg.data);
|
||||
c->character_file()->choice_search_config = check_size_t<ChoiceSearchConfig>(msg.data);
|
||||
co_return;
|
||||
}
|
||||
|
||||
@@ -4166,7 +4229,7 @@ static void on_choice_search_t(shared_ptr<Client> c, const ChoiceSearchConfig& c
|
||||
vector<ResultT> results;
|
||||
for (const auto& l : s->all_lobbies()) {
|
||||
for (const auto& lc : l->clients) {
|
||||
if (!lc || !lc->login || lc->character()->choice_search_config.disabled) {
|
||||
if (!lc || !lc->login || lc->character_file()->choice_search_config.disabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4187,7 +4250,7 @@ static void on_choice_search_t(shared_ptr<Client> c, const ChoiceSearchConfig& c
|
||||
}
|
||||
|
||||
if (is_match) {
|
||||
auto lp = lc->character();
|
||||
auto lp = lc->character_file();
|
||||
auto& result = results.emplace_back();
|
||||
result.guild_card_number = lc->login->account->account_id;
|
||||
result.name.encode(lp->disp.name.decode(lc->language()), c->language());
|
||||
@@ -4332,7 +4395,7 @@ static asio::awaitable<void> on_81(shared_ptr<Client> c, Channel::Message& msg)
|
||||
// If the target has auto-reply enabled, send the autoreply. Note that we also
|
||||
// forward the message in this case.
|
||||
if (!c->blocked_senders.count(target->login->account->account_id)) {
|
||||
auto target_p = target->character();
|
||||
auto target_p = target->character_file();
|
||||
if (!target_p->auto_reply.empty()) {
|
||||
send_simple_mail(
|
||||
c,
|
||||
@@ -4346,7 +4409,7 @@ static asio::awaitable<void> on_81(shared_ptr<Client> c, Channel::Message& msg)
|
||||
send_simple_mail(
|
||||
target,
|
||||
c->login->account->account_id,
|
||||
c->character()->disp.name.decode(c->language()),
|
||||
c->character_file()->disp.name.decode(c->language()),
|
||||
message);
|
||||
}
|
||||
}
|
||||
@@ -4364,7 +4427,7 @@ static asio::awaitable<void> on_D9(shared_ptr<Client> c, Channel::Message& msg)
|
||||
msg.data.push_back(0);
|
||||
}
|
||||
try {
|
||||
c->character(true, false)->info_board.encode(tt_decode_marked(msg.data, c->language(), is_w), c->language());
|
||||
c->character_file(true, false)->info_board.encode(tt_decode_marked(msg.data, c->language(), is_w), c->language());
|
||||
} catch (const runtime_error& e) {
|
||||
c->log.warning_f("Failed to decode info board message: {}", e.what());
|
||||
}
|
||||
@@ -4379,7 +4442,7 @@ static asio::awaitable<void> on_C7(shared_ptr<Client> c, Channel::Message& msg)
|
||||
}
|
||||
|
||||
string message = tt_decode_marked(msg.data, c->language(), is_w);
|
||||
c->character(true, false)->auto_reply.encode(message, c->language());
|
||||
c->character_file(true, false)->auto_reply.encode(message, c->language());
|
||||
c->login->account->auto_reply_message = message;
|
||||
c->login->account->save();
|
||||
co_return;
|
||||
@@ -4387,7 +4450,7 @@ static asio::awaitable<void> on_C7(shared_ptr<Client> c, Channel::Message& msg)
|
||||
|
||||
static asio::awaitable<void> on_C8(shared_ptr<Client> c, Channel::Message& msg) {
|
||||
check_size_v(msg.data.size(), 0);
|
||||
c->character(true, false)->auto_reply.clear();
|
||||
c->character_file(true, false)->auto_reply.clear();
|
||||
c->login->account->auto_reply_message.clear();
|
||||
c->login->account->save();
|
||||
co_return;
|
||||
@@ -4438,7 +4501,7 @@ shared_ptr<Lobby> create_game_generic(
|
||||
|
||||
size_t min_level = s->default_min_level_for_game(creator_c->version(), episode, difficulty);
|
||||
|
||||
auto p = creator_c->character();
|
||||
auto p = creator_c->character_file();
|
||||
if (!creator_c->login->account->check_flag(Account::Flag::FREE_JOIN_GAMES) && (min_level > p->disp.stats.level)) {
|
||||
// Note: We don't throw here because this is a situation players might
|
||||
// actually encounter while playing the game normally
|
||||
@@ -4585,7 +4648,7 @@ shared_ptr<Lobby> create_game_generic(
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
quest_flag_rewrites = nullptr;
|
||||
game->drop_mode = Lobby::DropMode::DISABLED;
|
||||
game->drop_mode = ServerDropMode::DISABLED;
|
||||
game->allowed_drop_modes = (1 << static_cast<size_t>(game->drop_mode));
|
||||
break;
|
||||
case Version::BB_V4:
|
||||
@@ -4601,10 +4664,10 @@ shared_ptr<Lobby> create_game_generic(
|
||||
game->allowed_drop_modes = s->allowed_drop_modes_v4_normal;
|
||||
}
|
||||
// Disallow CLIENT mode on BB
|
||||
if (game->drop_mode == Lobby::DropMode::CLIENT) {
|
||||
if (game->drop_mode == ServerDropMode::CLIENT) {
|
||||
throw logic_error("CLIENT mode not allowed on BB");
|
||||
}
|
||||
if (game->allowed_drop_modes & (1 << static_cast<size_t>(Lobby::DropMode::CLIENT))) {
|
||||
if (game->allowed_drop_modes & (1 << static_cast<size_t>(ServerDropMode::CLIENT))) {
|
||||
throw logic_error("CLIENT mode not allowed on BB");
|
||||
}
|
||||
break;
|
||||
@@ -4875,7 +4938,7 @@ static asio::awaitable<void> on_6F(shared_ptr<Client> c, Channel::Message& msg)
|
||||
shared_ptr<const Quest> q;
|
||||
try {
|
||||
int64_t quest_num = s->enable_send_function_call_quest_numbers.at(c->specific_version);
|
||||
q = s->default_quest_index->get(quest_num);
|
||||
q = s->quest_index->get(quest_num);
|
||||
} catch (const out_of_range&) {
|
||||
throw std::logic_error("cannot find patch enable quest after it was previously found during login");
|
||||
}
|
||||
@@ -4883,12 +4946,12 @@ static asio::awaitable<void> on_6F(shared_ptr<Client> c, Channel::Message& msg)
|
||||
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->name);
|
||||
c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name);
|
||||
string bin_filename = vq->bin_filename();
|
||||
string dat_filename = vq->dat_filename();
|
||||
string xb_filename = vq->xb_filename();
|
||||
send_open_quest_file(c, bin_filename, bin_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
|
||||
send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
|
||||
send_open_quest_file(c, bin_filename, bin_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents);
|
||||
send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
|
||||
co_return;
|
||||
}
|
||||
// Now l is not null
|
||||
@@ -4958,8 +5021,8 @@ static asio::awaitable<void> on_6F(shared_ptr<Client> c, Channel::Message& msg)
|
||||
string bin_filename = vq->bin_filename();
|
||||
string dat_filename = vq->dat_filename();
|
||||
|
||||
send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
|
||||
send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
|
||||
send_open_quest_file(c, bin_filename, bin_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents);
|
||||
send_open_quest_file(c, dat_filename, dat_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
|
||||
c->set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
|
||||
c->log.info_f("LOADING_RUNNING_JOINABLE_QUEST flag set");
|
||||
should_resume_game = false;
|
||||
@@ -5026,8 +5089,8 @@ static asio::awaitable<void> on_99(shared_ptr<Client> c, Channel::Message& msg)
|
||||
string bin_filename = vq->bin_filename();
|
||||
string dat_filename = vq->dat_filename();
|
||||
|
||||
send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
|
||||
send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
|
||||
send_open_quest_file(c, bin_filename, bin_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents);
|
||||
send_open_quest_file(c, dat_filename, dat_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
|
||||
c->set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
|
||||
c->log.info_f("LOADING_RUNNING_JOINABLE_QUEST flag set");
|
||||
|
||||
@@ -5110,42 +5173,44 @@ static asio::awaitable<void> on_D2_V3_BB(shared_ptr<Client> c, Channel::Message&
|
||||
}
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto complete_trade_for_side = +[](shared_ptr<Client> to_c, shared_ptr<Client> from_c) {
|
||||
auto l = to_c->require_lobby();
|
||||
auto s = to_c->require_server_state();
|
||||
auto complete_trade_for_side = [s, l](shared_ptr<Client> c, shared_ptr<Client> other_c) -> void {
|
||||
if (c->version() == Version::BB_V4) {
|
||||
// On BB, the server generates the delete/create item commands
|
||||
auto p = c->character_file();
|
||||
auto other_p = other_c->character_file();
|
||||
|
||||
if (to_c->version() == Version::BB_V4) {
|
||||
// On BB, the server is expected to generate the delete item and create
|
||||
// item commands
|
||||
auto to_p = to_c->character();
|
||||
auto from_p = from_c->character();
|
||||
for (const auto& trade_item : from_c->pending_item_trade->items) {
|
||||
size_t amount = trade_item.stack_size(*s->item_stack_limits(from_c->version()));
|
||||
// Delete items that are being given away
|
||||
for (const auto& item : c->pending_item_trade->items) {
|
||||
size_t amount = item.stack_size(*s->item_stack_limits(c->version()));
|
||||
p->remove_item(item.id, amount, *s->item_stack_limits(c->version()));
|
||||
|
||||
auto item = from_p->remove_item(trade_item.id, amount, *s->item_stack_limits(from_c->version()));
|
||||
// This is a special case: when the trade is executed, the client
|
||||
// deletes the traded items from its own inventory automatically, so we
|
||||
// should NOT send the 6x29 to that client; we should only send it to
|
||||
// the other clients in the game.
|
||||
G_DeleteInventoryItem_6x29 cmd = {{0x29, 0x03, from_c->lobby_client_id}, item.id, amount};
|
||||
G_DeleteInventoryItem_6x29 cmd = {{0x29, 0x03, c->lobby_client_id}, item.id, amount};
|
||||
for (auto lc : l->clients) {
|
||||
if (lc && (lc != from_c)) {
|
||||
if (lc && (lc != c)) {
|
||||
send_command_t(l, 0x60, 0x00, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
to_p->add_item(trade_item, *s->item_stack_limits(to_c->version()));
|
||||
send_create_inventory_item_to_lobby(to_c, to_c->lobby_client_id, item);
|
||||
}
|
||||
send_command(to_c, 0xD3, 0x00);
|
||||
|
||||
for (const auto& trade_item : other_c->pending_item_trade->items) {
|
||||
ItemData added_item = trade_item;
|
||||
added_item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(added_item, *s->item_stack_limits(c->version()));
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, added_item);
|
||||
}
|
||||
send_command(c, 0xD3, 0x00);
|
||||
|
||||
} else {
|
||||
// On V3, the clients will handle it; we just send their final trade lists
|
||||
// to each other
|
||||
send_execute_item_trade(to_c, from_c->pending_item_trade->items);
|
||||
// On V3, the client will handle it; we just have to forward the other
|
||||
// client's trade list
|
||||
send_execute_item_trade(c, other_c->pending_item_trade->items);
|
||||
}
|
||||
|
||||
send_command(to_c, 0xD4, 0x01);
|
||||
send_command(c, 0xD4, 0x01);
|
||||
};
|
||||
|
||||
c->pending_item_trade->confirmed = true;
|
||||
@@ -5312,7 +5377,7 @@ static asio::awaitable<void> on_EA_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
// TODO: What's the right error code to use here?
|
||||
send_command(c, 0x02EA, 0x00000001);
|
||||
} else {
|
||||
string player_name = c->character()->disp.name.decode(c->language());
|
||||
string player_name = c->character_file()->disp.name.decode(c->language());
|
||||
auto team = s->team_index->create(team_name, c->login->account->account_id, player_name);
|
||||
c->login->account->bb_team_id = team->team_id;
|
||||
c->login->account->save();
|
||||
@@ -5351,7 +5416,7 @@ static asio::awaitable<void> on_EA_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
s->team_index->add_member(
|
||||
team->team_id,
|
||||
added_c->login->account->account_id,
|
||||
added_c->character()->disp.name.decode(added_c->language()));
|
||||
added_c->character_file()->disp.name.decode(added_c->language()));
|
||||
send_command(c, 0x04EA, 0x00000000);
|
||||
send_command(added_c, 0x04EA, 0x00000000);
|
||||
send_team_metadata_change_notifications(
|
||||
@@ -5512,7 +5577,7 @@ static asio::awaitable<void> on_EA_BB(shared_ptr<Client> c, Channel::Message& ms
|
||||
send_team_metadata_change_notifications(s, team, 0, TeamMetadataChange::REWARD_FLAGS);
|
||||
}
|
||||
if (!reward.reward_item.empty()) {
|
||||
c->current_bank().add_item(reward.reward_item, *s->item_stack_limits(c->version()));
|
||||
c->bank_file()->add_item(reward.reward_item, *s->item_stack_limits(c->version()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -5572,7 +5637,7 @@ static on_command_t handlers[0x100][NUM_VERSIONS] = {
|
||||
/* 0E */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
/* 0F */ {on_0F_U, on_0F_U, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
// PC_PATCH BB_PATCH DC_NTE DC_PROTO DCV1 DCV2 PC-NTE PC GCNTE GC EP3TE EP3 XB BB
|
||||
/* 10 */ {on_10_U, on_10_U, on_10, on_10, on_10, on_10, on_10, on_10, on_10, on_10, on_10, on_10, on_10, on_10},
|
||||
/* 10 */ {on_10_U, on_10_U, on_10<false>, on_10<false>, on_10<false>, on_10<false>, on_10<true>, on_10<true>, on_10<false>, on_10<false>, on_10<false>, on_10<false>, on_10<false>, on_10<true>},
|
||||
/* 11 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
/* 12 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
/* 13 */ {nullptr, nullptr, on_ignored, on_ignored, on_ignored, on_ignored, on_ignored, on_ignored, on_13_A7_V3_V4, on_13_A7_V3_V4, on_13_A7_V3_V4, on_13_A7_V3_V4, on_13_A7_V3_V4, on_13_A7_V3_V4},
|
||||
|
||||
+319
-224
File diff suppressed because it is too large
Load Diff
@@ -1268,8 +1268,7 @@ ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, cons
|
||||
// then create a new item and reduce the amount of the existing stack. Note
|
||||
// that passing amount == 0 means to remove the entire stack, so this only
|
||||
// applies if amount is nonzero.
|
||||
if (amount && (inventory_item.data.stack_size(limits) > 1) &&
|
||||
(amount < inventory_item.data.data1[5])) {
|
||||
if (amount && (inventory_item.data.stack_size(limits) > 1) && (amount < inventory_item.data.data1[5])) {
|
||||
if (is_equipped) {
|
||||
throw runtime_error("character has a combine item equipped");
|
||||
}
|
||||
|
||||
+120
-94
@@ -790,7 +790,7 @@ void send_approve_player_choice_bb(shared_ptr<Client> c) {
|
||||
}
|
||||
|
||||
void send_complete_player_bb(shared_ptr<Client> c) {
|
||||
auto p = c->character(true, false);
|
||||
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)) {
|
||||
@@ -870,32 +870,49 @@ static void send_header_text(std::shared_ptr<Channel> ch, uint16_t command, uint
|
||||
}
|
||||
|
||||
void send_message_box(shared_ptr<Client> c, const string& text) {
|
||||
uint16_t command;
|
||||
switch (c->version()) {
|
||||
case Version::PC_PATCH:
|
||||
case Version::BB_PATCH:
|
||||
command = 0x13;
|
||||
break;
|
||||
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:
|
||||
command = 0x1A;
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3:
|
||||
case Version::BB_V4:
|
||||
command = 0xD5;
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid game version");
|
||||
if (is_v4(c->version())) {
|
||||
phosg::StringWriter w;
|
||||
try {
|
||||
w.write(tt_encode_marked_optional(add_color(text), c->language(), true));
|
||||
} catch (const runtime_error& e) {
|
||||
phosg::log_warning_f("Failed to encode text for message box command: {}", e.what());
|
||||
return;
|
||||
}
|
||||
w.put_u16(0);
|
||||
while (w.str().size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
send_command(c, (w.size() <= 0x400) ? 0x1A : 0xD5, 0x00, w.str());
|
||||
|
||||
} else {
|
||||
uint16_t command;
|
||||
switch (c->version()) {
|
||||
case Version::PC_PATCH:
|
||||
case Version::BB_PATCH:
|
||||
command = 0x13;
|
||||
break;
|
||||
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:
|
||||
command = 0x1A;
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3:
|
||||
command = 0xD5;
|
||||
break;
|
||||
case Version::BB_V4:
|
||||
throw std::logic_error("BB not handled before version switch");
|
||||
default:
|
||||
throw logic_error("invalid game version");
|
||||
}
|
||||
send_text(c->channel, command, 0x00, text, ColorMode::ADD);
|
||||
}
|
||||
send_text(c->channel, command, 0x00, text, ColorMode::ADD);
|
||||
}
|
||||
|
||||
void send_ep3_timed_message_box(std::shared_ptr<Channel> ch, uint32_t frames, const string& message) {
|
||||
@@ -1151,7 +1168,7 @@ void send_info_board_t(shared_ptr<Client> c) {
|
||||
if (!other_c.get()) {
|
||||
continue;
|
||||
}
|
||||
auto other_p = other_c->character(true, false);
|
||||
auto other_p = other_c->character_file(true, false);
|
||||
auto& e = entries.emplace_back();
|
||||
e.name.encode(other_p->disp.name.decode(other_p->inventory.language), c->language());
|
||||
e.message.encode(add_color(other_p->info_board.decode(other_p->inventory.language)), c->language());
|
||||
@@ -1235,7 +1252,7 @@ void send_card_search_result_t(
|
||||
cmd.location_string.encode(location_string, c->language());
|
||||
cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY;
|
||||
cmd.extension.lobby_refs[0].item_id = result_lobby->lobby_id;
|
||||
auto rp = result->character(true, false);
|
||||
auto rp = result->character_file(true, false);
|
||||
cmd.extension.player_name.encode(rp->disp.name.decode(rp->inventory.language), c->language());
|
||||
|
||||
send_command_t(c, 0x41, 0x00, cmd);
|
||||
@@ -1393,7 +1410,7 @@ void send_guild_card(shared_ptr<Client> c, shared_ptr<Client> source) {
|
||||
throw runtime_error("source player does not have an account");
|
||||
}
|
||||
|
||||
auto source_p = source->character(true, false);
|
||||
auto source_p = source->character_file(true, false);
|
||||
auto source_team = source->team();
|
||||
|
||||
uint64_t xb_user_id = (source->login->xb_license && source->login->xb_license->user_id)
|
||||
@@ -1628,10 +1645,10 @@ void send_quest_menu_t(
|
||||
}
|
||||
|
||||
auto& e = entries.emplace_back();
|
||||
e.menu_id = ((it.second->episode == Episode::EP1) || (it.second->episode == Episode::EP3)) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2;
|
||||
e.item_id = it.second->quest_number;
|
||||
e.name.encode(vq->name, c->language());
|
||||
e.short_description.encode(add_color(vq->short_description), c->language());
|
||||
e.menu_id = (it.second->meta.episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1;
|
||||
e.item_id = it.second->meta.quest_number;
|
||||
e.name.encode(vq->meta.name, c->language());
|
||||
e.short_description.encode(add_color(vq->meta.short_description), c->language());
|
||||
}
|
||||
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
|
||||
}
|
||||
@@ -1649,21 +1666,31 @@ void send_quest_menu_bb(
|
||||
}
|
||||
|
||||
auto& e = entries.emplace_back();
|
||||
e.menu_id = (it.second->episode == Episode::EP1) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2;
|
||||
e.item_id = it.second->quest_number;
|
||||
e.name.encode(vq->name, c->language());
|
||||
e.short_description.encode(add_color(vq->short_description), c->language());
|
||||
e.menu_id = (it.second->meta.episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1;
|
||||
e.item_id = it.second->meta.quest_number;
|
||||
e.name.encode(vq->meta.name, c->language());
|
||||
e.short_description.encode(add_color(vq->meta.short_description), c->language());
|
||||
e.disabled = (it.first == QuestIndex::IncludeState::DISABLED) ? 1 : 0;
|
||||
}
|
||||
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
|
||||
}
|
||||
|
||||
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,
|
||||
shared_ptr<const QuestIndex> quest_index,
|
||||
QuestMenuType menu_type,
|
||||
Episode episode) {
|
||||
void send_quest_categories_menu_t(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
|
||||
QuestIndex::IncludeCondition include_condition = nullptr;
|
||||
if (!c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
auto l = c->lobby.lock();
|
||||
@@ -1677,7 +1704,8 @@ void send_quest_categories_menu_t(
|
||||
}
|
||||
|
||||
vector<EntryT> entries;
|
||||
for (const auto& cat : quest_index->categories(menu_type, episode, version_flags, include_condition)) {
|
||||
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.item_id = cat->category_id;
|
||||
@@ -1685,7 +1713,7 @@ void send_quest_categories_menu_t(
|
||||
e.short_description.encode(add_color(cat->description), c->language());
|
||||
}
|
||||
|
||||
bool is_download_menu = (menu_type == QuestMenuType::DOWNLOAD) || (menu_type == QuestMenuType::EP3_DOWNLOAD);
|
||||
bool is_download_menu = (menu_type == QuestMenuType::DOWNLOAD);
|
||||
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
|
||||
}
|
||||
|
||||
@@ -1719,15 +1747,11 @@ void send_quest_menu(
|
||||
}
|
||||
}
|
||||
|
||||
void send_quest_categories_menu(
|
||||
shared_ptr<Client> c,
|
||||
shared_ptr<const QuestIndex> quest_index,
|
||||
QuestMenuType menu_type,
|
||||
Episode episode) {
|
||||
void send_quest_categories_menu(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
|
||||
switch (c->version()) {
|
||||
case Version::PC_NTE:
|
||||
case Version::PC_V2:
|
||||
send_quest_categories_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, quest_index, menu_type, episode);
|
||||
send_quest_categories_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_type, episode);
|
||||
break;
|
||||
case Version::DC_NTE:
|
||||
case Version::DC_11_2000:
|
||||
@@ -1737,13 +1761,13 @@ void send_quest_categories_menu(
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
send_quest_categories_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, quest_index, menu_type, episode);
|
||||
send_quest_categories_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_type, episode);
|
||||
break;
|
||||
case Version::XB_V3:
|
||||
send_quest_categories_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, quest_index, menu_type, episode);
|
||||
send_quest_categories_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_type, episode);
|
||||
break;
|
||||
case Version::BB_V4:
|
||||
send_quest_categories_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, quest_index, menu_type, episode);
|
||||
send_quest_categories_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, menu_type, episode);
|
||||
break;
|
||||
default:
|
||||
throw logic_error("unimplemented versioned command");
|
||||
@@ -1751,9 +1775,12 @@ void send_quest_categories_menu(
|
||||
}
|
||||
|
||||
void send_lobby_list(shared_ptr<Client> c) {
|
||||
// This command appears to be deprecated, as PSO expects it to be exactly how
|
||||
// this server sends it, and does not react if it's different, except by
|
||||
// changing the lobby IDs.
|
||||
// DC v1 expects 10 lobbies in this list; DC v2 and later accept a variable
|
||||
// number, but other parts of the code expect there to always be 15 lobbies.
|
||||
// Furthermore, there are only 16 entries in the array in TProtocol and the
|
||||
// writes aren't bounds-checked, so the 83 command could overwrite later
|
||||
// parts of TProtocol if more than 16 entries are sent. (On Episode 3, there
|
||||
// are 21 entries instead.)
|
||||
|
||||
auto s = c->require_server_state();
|
||||
vector<S_LobbyListEntry_83> entries;
|
||||
@@ -1780,7 +1807,7 @@ template <typename EntryT>
|
||||
void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client) {
|
||||
vector<EntryT> entries;
|
||||
auto add_client = [&](shared_ptr<Client> lc) -> void {
|
||||
auto lp = lc->character(true, false);
|
||||
auto lp = lc->character_file(true, false);
|
||||
auto& e = entries.emplace_back();
|
||||
e.client_id = lc->lobby_client_id;
|
||||
e.challenge = lp->challenge_records;
|
||||
@@ -1805,7 +1832,7 @@ void populate_lobby_data_for_client(LobbyDataT& ret, shared_ptr<const Client> c,
|
||||
ret.player_tag = 0x00010000;
|
||||
ret.guild_card_number = c->login->account->account_id;
|
||||
ret.client_id = c->lobby_client_id;
|
||||
string name = c->character()->disp.name.decode(c->language());
|
||||
string name = c->character_file()->disp.name.decode(c->language());
|
||||
ret.name.encode(name, viewer_c->language());
|
||||
}
|
||||
|
||||
@@ -1819,7 +1846,7 @@ void populate_lobby_data_for_client(PlayerLobbyDataXB& ret, shared_ptr<const Cli
|
||||
ret.netloc.account_id = 0xAE00000000000000 | c->login->account->account_id;
|
||||
}
|
||||
ret.client_id = c->lobby_client_id;
|
||||
string name = c->character()->disp.name.decode(c->language());
|
||||
string name = c->character_file()->disp.name.decode(c->language());
|
||||
ret.name.encode(name, viewer_c->language());
|
||||
}
|
||||
|
||||
@@ -1836,7 +1863,7 @@ void populate_lobby_data_for_client<PlayerLobbyDataBB>(PlayerLobbyDataBB& ret, s
|
||||
ret.team_master_guild_card_number = 0;
|
||||
ret.team_id = 0;
|
||||
}
|
||||
string name = c->character()->disp.name.decode(c->language());
|
||||
string name = c->character_file()->disp.name.decode(c->language());
|
||||
ret.name.encode(name, viewer_c->language());
|
||||
}
|
||||
|
||||
@@ -1872,7 +1899,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
|
||||
if (!wc) {
|
||||
continue;
|
||||
}
|
||||
auto wc_p = wc->character();
|
||||
auto wc_p = wc->character_file();
|
||||
auto& p = cmd.players[z];
|
||||
populate_lobby_data_for_client(p.lobby_data, wc, c);
|
||||
p.inventory = wc_p->inventory;
|
||||
@@ -1942,7 +1969,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
|
||||
for (size_t z = 4; z < 12; z++) {
|
||||
if (l->clients[z]) {
|
||||
auto other_c = l->clients[z];
|
||||
auto other_p = other_c->character();
|
||||
auto other_p = other_c->character_file();
|
||||
auto& cmd_p = cmd.spectator_players[z - 4];
|
||||
auto& cmd_e = cmd.entries[z];
|
||||
populate_lobby_data_for_client(cmd_p.lobby_data, other_c, c);
|
||||
@@ -2066,7 +2093,7 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
for (size_t x = 0; x < 4; x++) {
|
||||
auto lc = l->clients[x];
|
||||
if (lc) {
|
||||
auto other_p = lc->character();
|
||||
auto other_p = lc->character_file();
|
||||
auto& cmd_p = cmd.players_ep3[x];
|
||||
cmd_p.inventory = other_p->inventory;
|
||||
cmd_p.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version()));
|
||||
@@ -2178,7 +2205,7 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
|
||||
|
||||
size_t used_entries = 0;
|
||||
for (const auto& lc : lobby_clients) {
|
||||
auto lp = lc->character();
|
||||
auto lp = lc->character_file();
|
||||
auto& e = cmd.entries[used_entries++];
|
||||
populate_lobby_data_for_client(e.lobby_data, lc, c);
|
||||
e.inventory = lp->inventory;
|
||||
@@ -2251,7 +2278,7 @@ void send_join_lobby_xb(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cl
|
||||
|
||||
size_t used_entries = 0;
|
||||
for (const auto& lc : lobby_clients) {
|
||||
auto lp = lc->character();
|
||||
auto lp = lc->character_file();
|
||||
auto& e = cmd.entries[used_entries++];
|
||||
populate_lobby_data_for_client(e.lobby_data, lc, c);
|
||||
e.inventory = lp->inventory;
|
||||
@@ -2300,7 +2327,7 @@ void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l,
|
||||
|
||||
size_t used_entries = 0;
|
||||
for (const auto& lc : lobby_clients) {
|
||||
auto lp = lc->character();
|
||||
auto lp = lc->character_file();
|
||||
auto& e = cmd.entries[used_entries++];
|
||||
populate_lobby_data_for_client(e.lobby_data, lc, c);
|
||||
e.inventory = lp->inventory;
|
||||
@@ -2955,7 +2982,7 @@ void send_game_flag_state_t(shared_ptr<Client> c) {
|
||||
cmd.header.subcommand = 0x6F;
|
||||
cmd.header.size = sizeof(CmdT) >> 2;
|
||||
cmd.header.unused = 0x0000;
|
||||
cmd.quest_flags = (l && !l->quest_flags_known) ? *l->quest_flag_values : c->character()->quest_flags;
|
||||
cmd.quest_flags = (l && !l->quest_flags_known) ? *l->quest_flag_values : c->character_file()->quest_flags;
|
||||
|
||||
if (c->game_join_command_queue) {
|
||||
c->log.info_f("Client not ready to receive join commands; adding to queue");
|
||||
@@ -3004,7 +3031,7 @@ void send_game_player_state(shared_ptr<Client> to_c, shared_ptr<Client> from_c,
|
||||
}
|
||||
|
||||
if (apply_overrides) {
|
||||
auto from_p = from_c->character();
|
||||
auto from_p = from_c->character_file();
|
||||
to_send.base.pos.x = from_c->pos.x;
|
||||
to_send.base.pos.y = 0.0;
|
||||
to_send.base.pos.z = from_c->pos.z;
|
||||
@@ -3169,17 +3196,15 @@ void send_bank(shared_ptr<Client> c) {
|
||||
throw logic_error("6xBC can only be sent to BB clients");
|
||||
}
|
||||
|
||||
auto p = c->character();
|
||||
auto& bank = c->current_bank();
|
||||
bank.sort();
|
||||
const auto* items_it = bank.items.data();
|
||||
vector<PlayerBankItem> items(items_it, items_it + bank.num_items);
|
||||
auto p = c->character_file();
|
||||
auto bank = c->bank_file();
|
||||
bank->sort();
|
||||
|
||||
G_BankContentsHeader_BB_6xBC cmd = {
|
||||
{{0xBC, 0, 0}, sizeof(G_BankContentsHeader_BB_6xBC) + items.size() * sizeof(PlayerBankItem)},
|
||||
bank.checksum(), bank.num_items, bank.meseta};
|
||||
{{0xBC, 0, 0}, sizeof(G_BankContentsHeader_BB_6xBC) + bank->items.size() * sizeof(PlayerBankItem)},
|
||||
bank->bb_checksum(), bank->items.size(), bank->meseta};
|
||||
|
||||
send_command_t_vt(c, 0x6C, 0x00, cmd, items);
|
||||
send_command_t_vt(c, 0x6C, 0x00, cmd, bank->items);
|
||||
}
|
||||
|
||||
void send_shop(shared_ptr<Client> c, uint8_t shop_type) {
|
||||
@@ -3205,7 +3230,7 @@ void send_shop(shared_ptr<Client> c, uint8_t shop_type) {
|
||||
|
||||
void send_level_up(shared_ptr<Client> c) {
|
||||
auto l = c->require_lobby();
|
||||
auto p = c->character();
|
||||
auto p = c->character_file();
|
||||
CharacterStats stats = p->disp.stats.char_stats;
|
||||
|
||||
const ItemData* mag = nullptr;
|
||||
@@ -3243,7 +3268,12 @@ 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_SetEXPMultiplier_BB_6xDD cmd = {{0xDD, sizeof(G_SetEXPMultiplier_BB_6xDD) / 4, (l->mode == GameMode::CHALLENGE) ? 1 : l->base_exp_multiplier}};
|
||||
G_SetFractionalEXPMultiplier_Extension_BB_6xDD cmd = {
|
||||
{0xDD, sizeof(G_SetFractionalEXPMultiplier_Extension_BB_6xDD) / 4, 1}, 1.0f};
|
||||
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)) {
|
||||
send_command_t(lc, 0x60, 0x00, cmd);
|
||||
@@ -3291,11 +3321,7 @@ void send_ep3_card_list_update(shared_ptr<Client> c) {
|
||||
}
|
||||
}
|
||||
|
||||
void send_ep3_media_update(
|
||||
shared_ptr<Client> c,
|
||||
uint32_t type,
|
||||
uint32_t which,
|
||||
const string& compressed_data) {
|
||||
void send_ep3_media_update(shared_ptr<Client> c, uint32_t type, uint32_t which, const string& compressed_data) {
|
||||
phosg::StringWriter w;
|
||||
w.put<S_UpdateMediaHeader_Ep3_B9>({type, which, compressed_data.size(), 0});
|
||||
w.write(compressed_data);
|
||||
@@ -3496,7 +3522,7 @@ string ep3_description_for_client(shared_ptr<Client> c) {
|
||||
if (!is_ep3(c->version())) {
|
||||
throw runtime_error("client is not Episode 3");
|
||||
}
|
||||
auto p = c->character();
|
||||
auto p = c->character_file();
|
||||
return std::format(
|
||||
"{} CLv{} {}",
|
||||
name_for_char_class(p->disp.visual.char_class),
|
||||
@@ -3546,7 +3572,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
if (player.is_human()) {
|
||||
try {
|
||||
auto other_c = account_id_to_client.at(player.account_id);
|
||||
entry.name.encode(other_c->character()->disp.name.decode(other_c->language()), c->language());
|
||||
entry.name.encode(other_c->character_file()->disp.name.decode(other_c->language()), c->language());
|
||||
entry.description.encode(ep3_description_for_client(other_c), c->language());
|
||||
} catch (const out_of_range&) {
|
||||
entry.name.encode(player.player_name, c->language());
|
||||
@@ -3567,7 +3593,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
for (auto spec_c : l->clients) {
|
||||
if (spec_c) {
|
||||
auto& entry = cmd.spectator_entries[cmd.num_spectators++];
|
||||
entry.name.encode(spec_c->character()->disp.name.decode(spec_c->language()), c->language());
|
||||
entry.name.encode(spec_c->character_file()->disp.name.decode(spec_c->language()), c->language());
|
||||
entry.description.encode(ep3_description_for_client(spec_c), c->language());
|
||||
}
|
||||
}
|
||||
@@ -3584,7 +3610,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
size_t num_players = 0;
|
||||
for (const auto& opp_c : primary_lobby->clients) {
|
||||
if (opp_c) {
|
||||
cmd.player_entries[num_players].name.encode(opp_c->character()->disp.name.decode(opp_c->language()), c->language());
|
||||
cmd.player_entries[num_players].name.encode(opp_c->character_file()->disp.name.decode(opp_c->language()), c->language());
|
||||
cmd.player_entries[num_players].description.encode(ep3_description_for_client(opp_c), c->language());
|
||||
num_players++;
|
||||
}
|
||||
@@ -3597,7 +3623,7 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
|
||||
for (auto spec_c : l->clients) {
|
||||
if (spec_c) {
|
||||
auto& entry = cmd.spectator_entries[num_spectators++];
|
||||
entry.name.encode(spec_c->character()->disp.name.decode(spec_c->language()), c->language());
|
||||
entry.name.encode(spec_c->character_file()->disp.name.decode(spec_c->language()), c->language());
|
||||
entry.description.encode(ep3_description_for_client(spec_c), c->language());
|
||||
}
|
||||
}
|
||||
@@ -3718,7 +3744,7 @@ void send_ep3_tournament_match_result(shared_ptr<Lobby> l, uint32_t meseta_rewar
|
||||
if (player.is_human()) {
|
||||
try {
|
||||
auto pc = account_id_to_client.at(player.account_id);
|
||||
entry.player_names[z].encode(pc->character()->disp.name.decode(pc->language()), lc->language());
|
||||
entry.player_names[z].encode(pc->character_file()->disp.name.decode(pc->language()), lc->language());
|
||||
} catch (const out_of_range&) {
|
||||
entry.player_names[z].encode(player.player_name, lc->language());
|
||||
}
|
||||
@@ -4004,12 +4030,11 @@ void send_open_quest_file(
|
||||
if (chunk_bytes > 0x400) {
|
||||
chunk_bytes = 0x400;
|
||||
}
|
||||
send_quest_file_chunk(c, filename, offset / 0x400,
|
||||
contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
|
||||
send_quest_file_chunk(c, filename, offset / 0x400, contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
|
||||
}
|
||||
|
||||
// If there are still chunks to send, track the file so the chunk
|
||||
// acknowledgement handler (13 or A7) cna know what to send next
|
||||
// acknowledgement handler (13 or A7) can know what to send next
|
||||
if (chunks_to_send < total_chunks) {
|
||||
c->sending_files.emplace(filename, contents);
|
||||
c->log.info_f("Opened file {}", filename);
|
||||
@@ -4208,7 +4233,7 @@ static S_TeamInfoForPlayer_BB_13EA_15EA_Entry team_metadata_for_client(shared_pt
|
||||
S_TeamInfoForPlayer_BB_13EA_15EA_Entry cmd;
|
||||
cmd.lobby_client_id = c->lobby_client_id;
|
||||
cmd.guild_card_number = c->login->account->account_id;
|
||||
cmd.player_name = c->character()->disp.name;
|
||||
cmd.player_name = c->character_file()->disp.name;
|
||||
if (team) {
|
||||
cmd.membership = team->base_membership_for_member(c->login->account->account_id);
|
||||
if (team->flag_data) {
|
||||
@@ -4350,7 +4375,8 @@ void send_team_reward_list(shared_ptr<Client> c, bool show_purchased) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
// Hide item rewards if the player's bank is full
|
||||
bool show_item_rewards = show_purchased || (c->current_bank().num_items < 200);
|
||||
auto bank = c->bank_file();
|
||||
bool show_item_rewards = show_purchased || (bank->items.size() < bank->max_items);
|
||||
|
||||
vector<S_TeamRewardList_BB_19EA_1AEA::Entry> entries;
|
||||
for (const auto& reward : s->team_index->reward_definitions()) {
|
||||
|
||||
+8
-7
@@ -118,8 +118,12 @@ void send_command_vt(std::shared_ptr<Channel> ch, uint16_t command, uint32_t fla
|
||||
}
|
||||
|
||||
template <typename TargetT, typename StructT, typename EntryT>
|
||||
void send_command_t_vt(std::shared_ptr<TargetT> c, uint16_t command,
|
||||
uint32_t flag, const StructT& data, const std::vector<EntryT>& array_data) {
|
||||
void send_command_t_vt(
|
||||
std::shared_ptr<TargetT> c,
|
||||
uint16_t command,
|
||||
uint32_t flag,
|
||||
const StructT& data,
|
||||
const std::vector<EntryT>& array_data) {
|
||||
std::string all_data(reinterpret_cast<const char*>(&data), sizeof(StructT));
|
||||
all_data.append(reinterpret_cast<const char*>(array_data.data()),
|
||||
array_data.size() * sizeof(EntryT));
|
||||
@@ -308,11 +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_quest_categories_menu(
|
||||
std::shared_ptr<Client> c,
|
||||
std::shared_ptr<const QuestIndex> quest_index,
|
||||
QuestMenuType menu_type,
|
||||
Episode episode);
|
||||
void send_ep3_download_quest_menu(std::shared_ptr<Client> c);
|
||||
void send_quest_categories_menu(std::shared_ptr<Client> c, QuestMenuType menu_type, Episode episode);
|
||||
void send_lobby_list(std::shared_ptr<Client> c);
|
||||
|
||||
void send_player_records(
|
||||
|
||||
+141
-82
@@ -72,8 +72,7 @@ ServerState::ServerState(const string& config_filename)
|
||||
thread_pool(make_unique<asio::thread_pool>()),
|
||||
bb_stream_files_cache(new FileContentsCache(3600000000ULL)),
|
||||
bb_system_cache(new FileContentsCache(3600000000ULL)),
|
||||
gba_files_cache(new FileContentsCache(3600000000ULL)),
|
||||
player_files_manager(make_shared<PlayerFilesManager>(this->io_context)) {}
|
||||
gba_files_cache(new FileContentsCache(3600000000ULL)) {}
|
||||
|
||||
void ServerState::add_client_to_available_lobby(shared_ptr<Client> c) {
|
||||
shared_ptr<Lobby> added_to_lobby;
|
||||
@@ -395,10 +394,6 @@ 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;
|
||||
}
|
||||
|
||||
shared_ptr<const QuestIndex> ServerState::quest_index(Version version) const {
|
||||
return is_ep3(version) ? this->ep3_download_quest_index : this->default_quest_index;
|
||||
}
|
||||
|
||||
size_t ServerState::default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const {
|
||||
const auto& min_levels = is_v4(version)
|
||||
? this->min_levels_v4
|
||||
@@ -512,6 +507,35 @@ ItemData ServerState::parse_item_description(Version version, const string& desc
|
||||
return this->item_name_index(version)->parse_item_description(description);
|
||||
}
|
||||
|
||||
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)) {
|
||||
// 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)) {
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const RareItemSet> ServerState::rare_item_set(Version logic_version, shared_ptr<const Quest> q) const {
|
||||
if (q && q->meta.rare_item_set) {
|
||||
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)) {
|
||||
return this->rare_item_sets.at("rare-table-v2");
|
||||
} else if (is_v3(logic_version)) {
|
||||
return this->rare_item_sets.at("rare-table-v3");
|
||||
} else if (is_v4(logic_version)) {
|
||||
return this->rare_item_sets.at("rare-table-v4");
|
||||
} else {
|
||||
throw runtime_error(std::format("no default rare item set is available for {}", phosg::name_for_enum(logic_version)));
|
||||
}
|
||||
}
|
||||
|
||||
void ServerState::set_port_configuration(const vector<PortConfiguration>& port_configs) {
|
||||
this->name_to_port_config.clear();
|
||||
this->number_to_port_config.clear();
|
||||
@@ -835,22 +859,22 @@ void ServerState::load_config_early() {
|
||||
this->allowed_drop_modes_v4_normal = this->config_json->get_int("AllowedDropModesV4Normal", 0x1D);
|
||||
this->allowed_drop_modes_v4_battle = this->config_json->get_int("AllowedDropModesV4Battle", 0x05);
|
||||
this->allowed_drop_modes_v4_challenge = this->config_json->get_int("AllowedDropModesV4Challenge", 0x05);
|
||||
this->default_drop_mode_v1_v2_normal = this->config_json->get_enum("DefaultDropModeV1V2Normal", Lobby::DropMode::CLIENT);
|
||||
this->default_drop_mode_v1_v2_battle = this->config_json->get_enum("DefaultDropModeV1V2Battle", Lobby::DropMode::CLIENT);
|
||||
this->default_drop_mode_v1_v2_challenge = this->config_json->get_enum("DefaultDropModeV1V2Challenge", Lobby::DropMode::CLIENT);
|
||||
this->default_drop_mode_v3_normal = this->config_json->get_enum("DefaultDropModeV3Normal", Lobby::DropMode::CLIENT);
|
||||
this->default_drop_mode_v3_battle = this->config_json->get_enum("DefaultDropModeV3Battle", Lobby::DropMode::CLIENT);
|
||||
this->default_drop_mode_v3_challenge = this->config_json->get_enum("DefaultDropModeV3Challenge", Lobby::DropMode::CLIENT);
|
||||
this->default_drop_mode_v4_normal = this->config_json->get_enum("DefaultDropModeV4Normal", Lobby::DropMode::SERVER_SHARED);
|
||||
this->default_drop_mode_v4_battle = this->config_json->get_enum("DefaultDropModeV4Battle", Lobby::DropMode::SERVER_SHARED);
|
||||
this->default_drop_mode_v4_challenge = this->config_json->get_enum("DefaultDropModeV4Challenge", Lobby::DropMode::SERVER_SHARED);
|
||||
if ((this->default_drop_mode_v4_normal == Lobby::DropMode::CLIENT) ||
|
||||
(this->default_drop_mode_v4_battle == Lobby::DropMode::CLIENT) ||
|
||||
(this->default_drop_mode_v4_challenge == Lobby::DropMode::CLIENT)) {
|
||||
this->default_drop_mode_v1_v2_normal = this->config_json->get_enum("DefaultDropModeV1V2Normal", ServerDropMode::CLIENT);
|
||||
this->default_drop_mode_v1_v2_battle = this->config_json->get_enum("DefaultDropModeV1V2Battle", ServerDropMode::CLIENT);
|
||||
this->default_drop_mode_v1_v2_challenge = this->config_json->get_enum("DefaultDropModeV1V2Challenge", ServerDropMode::CLIENT);
|
||||
this->default_drop_mode_v3_normal = this->config_json->get_enum("DefaultDropModeV3Normal", ServerDropMode::CLIENT);
|
||||
this->default_drop_mode_v3_battle = this->config_json->get_enum("DefaultDropModeV3Battle", ServerDropMode::CLIENT);
|
||||
this->default_drop_mode_v3_challenge = this->config_json->get_enum("DefaultDropModeV3Challenge", ServerDropMode::CLIENT);
|
||||
this->default_drop_mode_v4_normal = this->config_json->get_enum("DefaultDropModeV4Normal", ServerDropMode::SERVER_SHARED);
|
||||
this->default_drop_mode_v4_battle = this->config_json->get_enum("DefaultDropModeV4Battle", ServerDropMode::SERVER_SHARED);
|
||||
this->default_drop_mode_v4_challenge = this->config_json->get_enum("DefaultDropModeV4Challenge", ServerDropMode::SERVER_SHARED);
|
||||
if ((this->default_drop_mode_v4_normal == ServerDropMode::CLIENT) ||
|
||||
(this->default_drop_mode_v4_battle == ServerDropMode::CLIENT) ||
|
||||
(this->default_drop_mode_v4_challenge == ServerDropMode::CLIENT)) {
|
||||
throw runtime_error("default V4 drop mode cannot be CLIENT");
|
||||
}
|
||||
if ((this->allowed_drop_modes_v4_normal & (1 << static_cast<size_t>(Lobby::DropMode::CLIENT))) ||
|
||||
(this->allowed_drop_modes_v4_battle & (1 << static_cast<size_t>(Lobby::DropMode::CLIENT))) || (this->allowed_drop_modes_v4_challenge & (1 << static_cast<size_t>(Lobby::DropMode::CLIENT)))) {
|
||||
if ((this->allowed_drop_modes_v4_normal & (1 << static_cast<size_t>(ServerDropMode::CLIENT))) ||
|
||||
(this->allowed_drop_modes_v4_battle & (1 << static_cast<size_t>(ServerDropMode::CLIENT))) || (this->allowed_drop_modes_v4_challenge & (1 << static_cast<size_t>(ServerDropMode::CLIENT)))) {
|
||||
throw runtime_error("CLIENT drop mode cannot be allowed in V4");
|
||||
}
|
||||
|
||||
@@ -1036,6 +1060,9 @@ void ServerState::load_config_early() {
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
this->bb_max_bank_items = this->config_json->get_int("BBMaxBankItems", 200);
|
||||
this->bb_max_bank_meseta = this->config_json->get_int("BBMaxBankMeseta", 999999);
|
||||
|
||||
for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) {
|
||||
if (!this->item_stack_limits_tables[v_s]) {
|
||||
Version v = static_cast<Version>(v_s);
|
||||
@@ -1052,9 +1079,9 @@ void ServerState::load_config_early() {
|
||||
}
|
||||
}
|
||||
|
||||
this->bb_global_exp_multiplier = this->config_json->get_int("BBGlobalEXPMultiplier", 1);
|
||||
this->exp_share_multiplier = this->config_json->get_float("BBEXPShareMultiplier", 0.5);
|
||||
this->server_global_drop_rate_multiplier = this->config_json->get_float("ServerGlobalDropRateMultiplier", 1);
|
||||
this->bb_global_exp_multiplier = this->config_json->get_float("BBGlobalEXPMultiplier", 1.0f);
|
||||
this->exp_share_multiplier = this->config_json->get_float("BBEXPShareMultiplier", 0.5f);
|
||||
this->server_global_drop_rate_multiplier = this->config_json->get_float("ServerGlobalDropRateMultiplier", 1.0f);
|
||||
|
||||
if (this->is_debug) {
|
||||
set_all_log_levels(phosg::LogLevel::L_DEBUG);
|
||||
@@ -1940,61 +1967,103 @@ void ServerState::load_item_name_indexes() {
|
||||
}
|
||||
|
||||
void ServerState::load_drop_tables() {
|
||||
config_log.info_f("Loading rare item sets");
|
||||
config_log.info_f("Loading item sets");
|
||||
|
||||
unordered_map<string, shared_ptr<RareItemSet>> new_rare_item_sets;
|
||||
unordered_map<string, shared_ptr<const RareItemSet>> new_rare_item_sets;
|
||||
unordered_map<string, shared_ptr<const CommonItemSet>> new_common_item_sets;
|
||||
for (const auto& item : std::filesystem::directory_iterator("system/item-tables")) {
|
||||
string filename = item.path().filename().string();
|
||||
if (!filename.starts_with("rare-table-")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
string path = "system/item-tables/" + filename;
|
||||
size_t ext_offset = filename.rfind('.');
|
||||
string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset);
|
||||
if (filename.starts_with("common-table-") || filename.starts_with("ItemPT-")) {
|
||||
string path = "system/item-tables/" + filename;
|
||||
size_t ext_offset = filename.rfind('.');
|
||||
string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset);
|
||||
|
||||
if (filename.ends_with("-v1.json")) {
|
||||
config_log.info_f("Loading v1 JSON rare item table {}", filename);
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::DC_V1)));
|
||||
} else if (filename.ends_with("-v2.json")) {
|
||||
config_log.info_f("Loading v2 JSON rare item table {}", filename);
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::PC_V2)));
|
||||
} else if (filename.ends_with("-v3.json")) {
|
||||
config_log.info_f("Loading v3 JSON rare item table {}", filename);
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::GC_V3)));
|
||||
} else if (filename.ends_with("-v4.json")) {
|
||||
config_log.info_f("Loading v4 JSON rare item table {}", filename);
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::BB_V4)));
|
||||
// AFSV2CommonItemSet(std::shared_ptr<const std::string> pt_afs_data, std::shared_ptr<const std::string> ct_afs_data);
|
||||
|
||||
} else if (filename.ends_with(".afs")) {
|
||||
config_log.info_f("Loading AFS rare item table {}", filename);
|
||||
auto data = make_shared<string>(phosg::load_file(path));
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(AFSArchive(data), false));
|
||||
if (filename.ends_with(".json")) {
|
||||
config_log.info_f("Loading JSON common item table {}", filename);
|
||||
new_common_item_sets.emplace(basename, make_shared<JSONCommonItemSet>(phosg::JSON::parse(phosg::load_file(path))));
|
||||
} else if (filename.ends_with(".afs")) {
|
||||
string ct_filename;
|
||||
if (filename.starts_with("ItemPT-")) {
|
||||
ct_filename = "ItemCT-" + filename.substr(7);
|
||||
} else if (filename.starts_with("common-table-")) {
|
||||
ct_filename = "challenge-common-table-" + filename.substr(13);
|
||||
} else {
|
||||
throw std::runtime_error(std::format("cannot determine challenge table filename for common table file: {}", filename));
|
||||
}
|
||||
auto data = make_shared<string>(phosg::load_file(path));
|
||||
shared_ptr<string> ct_data;
|
||||
try {
|
||||
string ct_path = "system/item-tables/" + ct_filename;
|
||||
ct_data = make_shared<string>(phosg::load_file(ct_path));
|
||||
config_log.info_f("Loading AFS common item table {} with challenge table {}", filename, ct_filename);
|
||||
} catch (const phosg::cannot_open_file&) {
|
||||
config_log.info_f("Loading AFS common item table {} without challenge table", filename);
|
||||
}
|
||||
new_common_item_sets.emplace(basename, make_shared<AFSV2CommonItemSet>(data, ct_data));
|
||||
} else if (filename.ends_with(".gsl")) {
|
||||
config_log.info_f("Loading little-endian GSL common item table {}", filename);
|
||||
auto data = make_shared<string>(phosg::load_file(path));
|
||||
new_common_item_sets.emplace(basename, make_shared<GSLV3V4CommonItemSet>(data, false));
|
||||
} else if (filename.ends_with(".gslb")) {
|
||||
config_log.info_f("Loading big-endian GSL common item table {}", filename);
|
||||
auto data = make_shared<string>(phosg::load_file(path));
|
||||
new_common_item_sets.emplace(basename, make_shared<GSLV3V4CommonItemSet>(data, true));
|
||||
} else {
|
||||
throw std::runtime_error(std::format("unknown format for common table file: {}", filename));
|
||||
}
|
||||
|
||||
} else if (filename.ends_with(".gsl")) {
|
||||
config_log.info_f("Loading GSL rare item table {}", filename);
|
||||
auto data = make_shared<string>(phosg::load_file(path));
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(GSLArchive(data, false), false));
|
||||
} else if (filename.starts_with("rare-table-") || filename.starts_with("ItemRT-")) {
|
||||
string path = "system/item-tables/" + filename;
|
||||
size_t ext_offset = filename.rfind('.');
|
||||
string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset);
|
||||
|
||||
} else if (filename.ends_with(".gslb")) {
|
||||
config_log.info_f("Loading GSL rare item table {}", filename);
|
||||
auto data = make_shared<string>(phosg::load_file(path));
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(GSLArchive(data, true), true));
|
||||
shared_ptr<RareItemSet> rare_set;
|
||||
if (filename.ends_with("-v1.json")) {
|
||||
config_log.info_f("Loading v1 JSON rare item table {}", filename);
|
||||
rare_set = make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::DC_V1));
|
||||
} else if (filename.ends_with("-v2.json")) {
|
||||
config_log.info_f("Loading v2 JSON rare item table {}", filename);
|
||||
rare_set = make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::PC_V2));
|
||||
} else if (filename.ends_with("-v3.json")) {
|
||||
config_log.info_f("Loading v3 JSON rare item table {}", filename);
|
||||
rare_set = make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::GC_V3));
|
||||
} else if (filename.ends_with("-v4.json")) {
|
||||
config_log.info_f("Loading v4 JSON rare item table {}", filename);
|
||||
rare_set = make_shared<RareItemSet>(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::BB_V4));
|
||||
|
||||
} else if (filename.ends_with(".rel")) {
|
||||
config_log.info_f("Loading REL rare item table {}", filename);
|
||||
new_rare_item_sets.emplace(basename, make_shared<RareItemSet>(phosg::load_file(path), true));
|
||||
} else if (filename.ends_with(".afs")) {
|
||||
config_log.info_f("Loading AFS rare item table {}", filename);
|
||||
auto data = make_shared<string>(phosg::load_file(path));
|
||||
rare_set = make_shared<RareItemSet>(AFSArchive(data), false);
|
||||
|
||||
} else if (filename.ends_with(".gsl")) {
|
||||
config_log.info_f("Loading GSL rare item table {}", filename);
|
||||
auto data = make_shared<string>(phosg::load_file(path));
|
||||
rare_set = make_shared<RareItemSet>(GSLArchive(data, false), false);
|
||||
|
||||
} else if (filename.ends_with(".gslb")) {
|
||||
config_log.info_f("Loading GSL rare item table {}", filename);
|
||||
auto data = make_shared<string>(phosg::load_file(path));
|
||||
rare_set = make_shared<RareItemSet>(GSLArchive(data, true), true);
|
||||
|
||||
} else if (filename.ends_with(".rel")) {
|
||||
config_log.info_f("Loading REL rare item table {}", filename);
|
||||
rare_set = make_shared<RareItemSet>(phosg::load_file(path), true);
|
||||
|
||||
} else {
|
||||
throw std::runtime_error(std::format("unknown format for rare table file: {}", filename));
|
||||
}
|
||||
|
||||
if (this->server_global_drop_rate_multiplier != 1.0) {
|
||||
rare_set->multiply_all_rates(this->server_global_drop_rate_multiplier);
|
||||
}
|
||||
new_rare_item_sets.emplace(basename, std::move(rare_set));
|
||||
}
|
||||
}
|
||||
|
||||
config_log.info_f("Loading v2 common item table");
|
||||
auto ct_data_v2 = make_shared<string>(phosg::load_file("system/item-tables/ItemCT-pc-v2.afs"));
|
||||
auto pt_data_v2 = make_shared<string>(phosg::load_file("system/item-tables/ItemPT-pc-v2.afs"));
|
||||
auto new_common_item_set_v2 = make_shared<AFSV2CommonItemSet>(pt_data_v2, ct_data_v2);
|
||||
config_log.info_f("Loading v3+v4 common item table");
|
||||
auto pt_data_v3_v4 = make_shared<string>(phosg::load_file("system/item-tables/ItemPT-gc-v3.gsl"));
|
||||
auto new_common_item_set_v3_v4 = make_shared<GSLV3V4CommonItemSet>(pt_data_v3_v4, true);
|
||||
|
||||
config_log.info_f("Loading armor table");
|
||||
auto armor_data = make_shared<string>(phosg::load_file("system/item-tables/ArmorRandom-gc-v3.rel"));
|
||||
auto new_armor_random_set = make_shared<ArmorRandomSet>(armor_data);
|
||||
@@ -2020,19 +2089,8 @@ void ServerState::load_drop_tables() {
|
||||
auto tekker_data = make_shared<string>(phosg::load_file("system/item-tables/JudgeItem-gc-v3.rel"));
|
||||
auto new_tekker_adjustment_set = make_shared<TekkerAdjustmentSet>(tekker_data);
|
||||
|
||||
if (this->server_global_drop_rate_multiplier != 1.0) {
|
||||
for (auto& it : new_rare_item_sets) {
|
||||
it.second->multiply_all_rates(this->server_global_drop_rate_multiplier);
|
||||
}
|
||||
}
|
||||
// We can't just std::move() new_rare_item_sets into place because its values are
|
||||
// not const :(
|
||||
this->rare_item_sets.clear();
|
||||
for (auto& it : new_rare_item_sets) {
|
||||
this->rare_item_sets.emplace(it.first, std::move(it.second));
|
||||
}
|
||||
this->common_item_set_v2 = std::move(new_common_item_set_v2);
|
||||
this->common_item_set_v3_v4 = std::move(new_common_item_set_v3_v4);
|
||||
this->rare_item_sets = std::move(new_rare_item_sets);
|
||||
this->common_item_sets = std::move(new_common_item_sets);
|
||||
this->armor_random_set = std::move(new_armor_random_set);
|
||||
this->tool_random_set = std::move(new_tool_random_set);
|
||||
this->weapon_random_sets = std::move(new_weapon_random_sets);
|
||||
@@ -2095,6 +2153,8 @@ void ServerState::load_ep3_cards() {
|
||||
void ServerState::load_ep3_maps() {
|
||||
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");
|
||||
}
|
||||
|
||||
void ServerState::load_ep3_tournament_state() {
|
||||
@@ -2107,9 +2167,8 @@ void ServerState::load_ep3_tournament_state() {
|
||||
|
||||
void ServerState::load_quest_index() {
|
||||
config_log.info_f("Collecting quests");
|
||||
this->default_quest_index = make_shared<QuestIndex>("system/quests", this->quest_category_index, false);
|
||||
config_log.info_f("Collecting Episode 3 download quests");
|
||||
this->ep3_download_quest_index = make_shared<QuestIndex>("system/ep3/maps-download", this->quest_category_index, true);
|
||||
this->quest_index = make_shared<QuestIndex>(
|
||||
"system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets);
|
||||
}
|
||||
|
||||
void ServerState::compile_functions() {
|
||||
|
||||
+20
-19
@@ -24,7 +24,6 @@
|
||||
#include "LevelTable.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "Menu.hh"
|
||||
#include "PlayerFilesManager.hh"
|
||||
#include "Quest.hh"
|
||||
#include "TeamIndex.hh"
|
||||
#include "WordSelectTable.hh"
|
||||
@@ -128,15 +127,15 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
uint8_t allowed_drop_modes_v4_normal = 0x1D; // CLIENT not allowed
|
||||
uint8_t allowed_drop_modes_v4_battle = 0x05;
|
||||
uint8_t allowed_drop_modes_v4_challenge = 0x05;
|
||||
Lobby::DropMode default_drop_mode_v1_v2_normal = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v1_v2_battle = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v1_v2_challenge = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v3_normal = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v3_battle = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v3_challenge = Lobby::DropMode::CLIENT;
|
||||
Lobby::DropMode default_drop_mode_v4_normal = Lobby::DropMode::SERVER_SHARED;
|
||||
Lobby::DropMode default_drop_mode_v4_battle = Lobby::DropMode::SERVER_SHARED;
|
||||
Lobby::DropMode default_drop_mode_v4_challenge = Lobby::DropMode::SERVER_SHARED;
|
||||
ServerDropMode default_drop_mode_v1_v2_normal = ServerDropMode::CLIENT;
|
||||
ServerDropMode default_drop_mode_v1_v2_battle = ServerDropMode::CLIENT;
|
||||
ServerDropMode default_drop_mode_v1_v2_challenge = ServerDropMode::CLIENT;
|
||||
ServerDropMode default_drop_mode_v3_normal = ServerDropMode::CLIENT;
|
||||
ServerDropMode default_drop_mode_v3_battle = ServerDropMode::CLIENT;
|
||||
ServerDropMode default_drop_mode_v3_challenge = ServerDropMode::CLIENT;
|
||||
ServerDropMode default_drop_mode_v4_normal = ServerDropMode::SERVER_SHARED;
|
||||
ServerDropMode default_drop_mode_v4_battle = ServerDropMode::SERVER_SHARED;
|
||||
ServerDropMode default_drop_mode_v4_challenge = ServerDropMode::SERVER_SHARED;
|
||||
std::unordered_map<uint16_t, IntegralExpression> quest_flag_rewrites_v1_v2;
|
||||
std::unordered_map<uint16_t, IntegralExpression> quest_flag_rewrites_v3;
|
||||
std::unordered_map<uint16_t, IntegralExpression> quest_flag_rewrites_v4;
|
||||
@@ -184,21 +183,20 @@ 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;
|
||||
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_tournament_final_round_ex_values;
|
||||
std::shared_ptr<const QuestCategoryIndex> quest_category_index;
|
||||
std::shared_ptr<const QuestIndex> default_quest_index;
|
||||
std::shared_ptr<const QuestIndex> ep3_download_quest_index;
|
||||
std::shared_ptr<const QuestIndex> quest_index;
|
||||
std::shared_ptr<const LevelTableV2> level_table_v1_v2;
|
||||
std::shared_ptr<const LevelTable> level_table_v3;
|
||||
std::shared_ptr<const LevelTable> level_table_v4;
|
||||
std::shared_ptr<const BattleParamsIndex> battle_params;
|
||||
std::shared_ptr<const GSLArchive> bb_data_gsl;
|
||||
std::unordered_map<std::string, std::shared_ptr<const CommonItemSet>> common_item_sets;
|
||||
std::unordered_map<std::string, std::shared_ptr<const RareItemSet>> rare_item_sets;
|
||||
std::shared_ptr<const CommonItemSet> common_item_set_v2;
|
||||
std::shared_ptr<const CommonItemSet> common_item_set_v3_v4;
|
||||
std::shared_ptr<const ArmorRandomSet> armor_random_set;
|
||||
std::shared_ptr<const ToolRandomSet> tool_random_set;
|
||||
std::array<std::shared_ptr<const WeaponRandomSet>, 4> weapon_random_sets;
|
||||
@@ -206,6 +204,8 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::array<std::shared_ptr<const ItemParameterTable>, NUM_VERSIONS> item_parameter_tables;
|
||||
std::shared_ptr<const ItemTranslationTable> item_translation_table;
|
||||
std::array<std::shared_ptr<const ItemData::StackLimits>, NUM_VERSIONS> item_stack_limits_tables;
|
||||
size_t bb_max_bank_items = 200;
|
||||
size_t bb_max_bank_meseta = 999999;
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v1_v2;
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v3;
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v4;
|
||||
@@ -241,9 +241,9 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::vector<QuestF960Result> quest_F960_success_results;
|
||||
QuestF960Result quest_F960_failure_results;
|
||||
std::vector<ItemData> secret_lottery_results;
|
||||
uint16_t bb_global_exp_multiplier = 1;
|
||||
float exp_share_multiplier = 0.5;
|
||||
double server_global_drop_rate_multiplier = 1.0;
|
||||
float bb_global_exp_multiplier = 1.0f;
|
||||
float exp_share_multiplier = 0.5f;
|
||||
float server_global_drop_rate_multiplier = 1.0f;
|
||||
|
||||
std::shared_ptr<Episode3::TournamentIndex> ep3_tournament_index;
|
||||
|
||||
@@ -288,7 +288,6 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::string pc_patch_server_message;
|
||||
std::string bb_patch_server_message;
|
||||
|
||||
std::shared_ptr<PlayerFilesManager> player_files_manager;
|
||||
std::map<int64_t, std::shared_ptr<Lobby>> id_to_lobby;
|
||||
std::array<std::vector<uint32_t>, NUM_VERSIONS> public_lobby_search_orders;
|
||||
std::vector<uint32_t> client_customization_public_lobby_search_order;
|
||||
@@ -357,6 +356,9 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::string describe_item(Version version, const ItemData& item, uint8_t flags = 0) const;
|
||||
ItemData parse_item_description(Version version, const std::string& description) const;
|
||||
|
||||
std::shared_ptr<const CommonItemSet> common_item_set(Version logic_version, std::shared_ptr<const Quest> q) const;
|
||||
std::shared_ptr<const RareItemSet> rare_item_set(Version logic_version, std::shared_ptr<const Quest> q) const;
|
||||
|
||||
const std::vector<uint32_t>& public_lobby_search_order(Version version, bool is_client_customization) const;
|
||||
inline const std::vector<uint32_t>& public_lobby_search_order(std::shared_ptr<const Client> c) const {
|
||||
return this->public_lobby_search_order(c->version(), c->check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION));
|
||||
@@ -373,7 +375,6 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
}
|
||||
|
||||
std::shared_ptr<const std::vector<std::string>> information_contents_for_client(std::shared_ptr<const Client> c) const;
|
||||
std::shared_ptr<const QuestIndex> quest_index(Version version) const;
|
||||
|
||||
size_t default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const;
|
||||
|
||||
|
||||
@@ -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->for_name(map_name);
|
||||
auto map = args.s->ep3_map_index->get(map_name);
|
||||
uint32_t num_teams = stoul(get_quoted_string(args.args), nullptr, 0);
|
||||
Episode3::Rules rules;
|
||||
rules.set_defaults();
|
||||
@@ -740,17 +740,17 @@ ShellCommand c_create_tournament(
|
||||
if (subtokens.size() < 1) {
|
||||
throw runtime_error("no dice ranges specified in dice= option");
|
||||
}
|
||||
auto atk_range = parse_range_p(tokens[0]);
|
||||
auto atk_range = parse_range_p(subtokens[0]);
|
||||
rules.min_dice_value = atk_range.first;
|
||||
rules.max_dice_value = atk_range.second;
|
||||
if (subtokens.size() >= 2) {
|
||||
rules.def_dice_value_range = parse_range_c(tokens[1]);
|
||||
rules.def_dice_value_range = parse_range_c(subtokens[1]);
|
||||
if (subtokens.size() >= 3) {
|
||||
rules.atk_dice_value_range_2v1 = parse_range_c(tokens[2]);
|
||||
rules.atk_dice_value_range_2v1 = parse_range_c(subtokens[2]);
|
||||
if (subtokens.size() == 3) {
|
||||
rules.def_dice_value_range_2v1 = rules.atk_dice_value_range_2v1;
|
||||
} else if (subtokens.size() == 4) {
|
||||
rules.def_dice_value_range_2v1 = parse_range_c(tokens[3]);
|
||||
rules.def_dice_value_range_2v1 = parse_range_c(subtokens[3]);
|
||||
} else {
|
||||
throw runtime_error("too many range specs given");
|
||||
}
|
||||
@@ -975,7 +975,7 @@ asio::awaitable<deque<string>> fn_chat(ShellCommand::Args& args) {
|
||||
auto l = c->require_lobby();
|
||||
for (auto& lc : l->clients) {
|
||||
if (lc) {
|
||||
send_chat_message(lc, c->login->account->account_id, c->character()->disp.name.decode(c->language()), text, 0);
|
||||
send_chat_message(lc, c->login->account->account_id, c->character_file()->disp.name.decode(c->language()), text, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1008,7 +1008,8 @@ asio::awaitable<deque<string>> fn_wchat(ShellCommand::Args& args) {
|
||||
auto l = c->require_lobby();
|
||||
for (auto& lc : l->clients) {
|
||||
if (lc) {
|
||||
send_chat_message(lc, c->login->account->account_id, c->character()->disp.name.decode(c->language()), args.args, 0x40);
|
||||
send_chat_message(
|
||||
lc, c->login->account->account_id, c->character_file()->disp.name.decode(c->language()), args.args, 0x40);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
#ifdef PHOSG_WINDOWS
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#ifdef DELETE
|
||||
#undef DELETE
|
||||
#endif
|
||||
#ifdef ERROR
|
||||
#undef ERROR
|
||||
#endif
|
||||
#ifdef PASSTHROUGH
|
||||
#undef PASSTHROUGH
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.meta name="Kill count fix"
|
||||
.meta description="Fixes client-side\nkill counts when\nmultiple enemies are\nkilled on the same\nframe"
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
.versions 3OJ2 3OJ3 3OJ4 3OJ5 3OE0 3OE1 3OE2 3OP0
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.meta name="Kill count fix"
|
||||
.meta description="Fixes client-side\nkill counts when\nmultiple enemies are\nkilled on the same\nframe"
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
.versions 4OJB 4OJD 4OJU 4OED 4OEU 4OPD 4OPU
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.meta name="Kill count fix"
|
||||
.meta description="Fixes client-side\nkill counts when\nmultiple enemies are\nkilled on the same\nframe"
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# This patch changes the amount of items and Meseta that can be stored in the
|
||||
# bank. If the bank item limit is increased beyond 200, this patch requires
|
||||
# server support for extended bank data stored outside of the player's data.
|
||||
# newserv has support for this, but you must set the BBBankItemLimit and
|
||||
# BBBankMesetaLimit values in config.json to match the values used here.
|
||||
|
||||
# As written, this changes the meseta limit to 2000000000 and the item limit to
|
||||
# 1000. The meseta limit can be any value up to 2147483647, and the item limit
|
||||
# can be any value up to 1321. To use different values than the defaults, first
|
||||
# compute the data size as ((slot count * 0x18) + 8), then replace each value
|
||||
# below appropriately.
|
||||
|
||||
.meta name="More bank slots"
|
||||
.meta description=""
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
start:
|
||||
.include WriteCodeBlocksBB
|
||||
|
||||
.data 0x006C8C0F
|
||||
.data 4
|
||||
.data 1000 # slot count
|
||||
.data 0x006C8C4D
|
||||
.data 4
|
||||
.data 1000 # slot count
|
||||
.data 0x006C8B54
|
||||
.data 4
|
||||
.data 999 # slot count - 1
|
||||
.data 0x006C8B94
|
||||
.data 4
|
||||
.data 0x5DC0 # data size - 8
|
||||
.data 0x006C8D16
|
||||
.data 4
|
||||
.data 999 # slot count - 1
|
||||
.data 0x006C8E5E
|
||||
.data 4
|
||||
.data 999 # slot count - 1
|
||||
.data 0x006C8F2C
|
||||
.data 4
|
||||
.data 999 # slot count - 1
|
||||
.data 0x006C9016
|
||||
.data 4
|
||||
.data 0x5DB0 # data size - 0x18
|
||||
.data 0x006C9034
|
||||
.data 4
|
||||
.data 0x5DC0 # data size - 8
|
||||
.data 0x006C910D
|
||||
.data 4
|
||||
.data 0x5DB0 # data size - 0x18
|
||||
.data 0x006C9129
|
||||
.data 4
|
||||
.data 0x5DC8 # data size
|
||||
.data 0x006C9236
|
||||
.data 4
|
||||
.data 1000 # slot count
|
||||
.data 0x006C924C
|
||||
.data 4
|
||||
.data 999 # slot count - 1
|
||||
.data 0x006C9286
|
||||
.data 4
|
||||
.data 999 # slot count - 1
|
||||
.data 0x006C92FA
|
||||
.data 4
|
||||
.data 1000 # slot count
|
||||
.data 0x006C9883
|
||||
.data 4
|
||||
.data 1000 # slot count
|
||||
.data 0x006C9A22
|
||||
.data 4
|
||||
.data 2000000000 # max meseta
|
||||
.data 0x006CA2DB
|
||||
.data 4
|
||||
.data 0x5DC8 # data size
|
||||
.data 0x006CA303
|
||||
.data 4
|
||||
.data 1000 # slot count
|
||||
.data 0x006CA37F
|
||||
.data 4
|
||||
.data 0x5DC8 # data size
|
||||
.data 0x006D7DAC
|
||||
.data 4
|
||||
.data 1000 # slot count
|
||||
.data 0x006D7DBD
|
||||
.data 4
|
||||
.data 1000 # slot count
|
||||
.data 0x006D7E14
|
||||
.data 4
|
||||
.data 1000 # slot count
|
||||
.data 0x006D7BF5
|
||||
.data 4
|
||||
.data 1000 # slot count
|
||||
|
||||
.data 0x006C8DBF
|
||||
.data 2
|
||||
jmp +0x27
|
||||
|
||||
.data 0
|
||||
.data 0
|
||||
@@ -0,0 +1,63 @@
|
||||
# This patch changes the 6xDD command to support fractional multipliers.
|
||||
|
||||
.meta name="Fractional EXP multiplier"
|
||||
.meta description=""
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
call install_hook
|
||||
call apply_static_patches
|
||||
fild st0, dword [0x009F9EE0]
|
||||
fstp dword [0x009F9EE0], st0
|
||||
ret
|
||||
|
||||
|
||||
|
||||
install_hook:
|
||||
pop ecx
|
||||
push 7
|
||||
push 0x0078747E
|
||||
call get_code_size
|
||||
.deltaof hook_start, hook_end
|
||||
get_code_size:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call hook_end
|
||||
hook_start: # [eax, ebx]() -> void
|
||||
push edx
|
||||
fild st0, dword [esp]
|
||||
fld st0, dword [0x009F9EE0]
|
||||
fmulp st1, st0
|
||||
fistp dword [esp], st0
|
||||
pop edx
|
||||
ret
|
||||
hook_end:
|
||||
push ecx
|
||||
.include WriteCallToCode-59NL
|
||||
|
||||
|
||||
|
||||
apply_static_patches:
|
||||
.include WriteCodeBlocksBB
|
||||
.data 0x00787998
|
||||
.deltaof handle_6xDD_start, handle_6xDD_end
|
||||
handle_6xDD_start: # [std](G_6xDD* cmd @ [esp + 4]) -> void
|
||||
mov eax, [esp + 4]
|
||||
test eax, eax
|
||||
je handle_6xDD_ret
|
||||
cmp byte [eax + 1], 1
|
||||
jg handle_6xDD_use_float
|
||||
fild st0, word [eax + 2]
|
||||
jmp handle_6xDD_write_float
|
||||
handle_6xDD_use_float:
|
||||
fld st0, dword [eax + 4]
|
||||
handle_6xDD_write_float:
|
||||
fstp dword [0x009F9EE0], st0
|
||||
handle_6xDD_ret:
|
||||
ret
|
||||
handle_6xDD_end:
|
||||
.data 0x00000000
|
||||
.data 0x00000000
|
||||
@@ -82,7 +82,7 @@ patch_func_3:
|
||||
mov dword [0x00518803], 0x90909090
|
||||
ret
|
||||
|
||||
# TODO: Which objects this affects?
|
||||
# TOComputerMachine01
|
||||
patch_func_4:
|
||||
pop ecx
|
||||
push 7
|
||||
@@ -103,7 +103,7 @@ patch_code_end4:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# TODO: This one too?
|
||||
# TObjCamera
|
||||
patch_func_5:
|
||||
pop ecx
|
||||
push 6
|
||||
|
||||
+97
-24
@@ -1,4 +1,3 @@
|
||||
.meta hide_from_patches_menu
|
||||
.meta name="DMC"
|
||||
.meta description="Mitigates effects\nof enemy health\ndesync"
|
||||
.meta client_flag="0x2000000000000000"
|
||||
@@ -92,10 +91,20 @@ start:
|
||||
.data 4
|
||||
.address <VERS 0x80012C58 0x80012C88 0x80012F50 0x80012C38 0x80012C70 0x80012C70 0x80012C38 0x80012CB0>
|
||||
bl on_TObjectV8047c128_subtract_hp_with_sync
|
||||
.data <VERS 0x80012FE0 0x80013010 0x800132D8 0x80012FC0 0x80012FF8 0x80012FF8 0x80012FC0 0x80013038>
|
||||
.data 8
|
||||
fmuls f3, f0, f2
|
||||
fmuls f31, f30, f3
|
||||
.data <VERS 0x80012FFC 0x8001302C 0x800132F4 0x80012FDC 0x80013014 0x80013014 0x80012FDC 0x80013054>
|
||||
.data 4
|
||||
fctiwz f3, f31
|
||||
.data <VERS 0x80013004 0x80013034 0x800132FC 0x80012FE4 0x8001301C 0x8001301C 0x80012FE4 0x8001305C>
|
||||
.data 4
|
||||
stfd [r1 + 0x40], f3
|
||||
.data <VERS 0x8001300C 0x8001303C 0x80013304 0x80012FEC 0x80013024 0x80013024 0x80012FEC 0x80013064>
|
||||
.data 4
|
||||
.address <VERS 0x8001300C 0x8001303C 0x80013304 0x80012FEC 0x80013024 0x80013024 0x80012FEC 0x80013064>
|
||||
bl on_TObjectV8047c128_subtract_hp_with_sync
|
||||
bl on_TObjectV8047c128_subtract_hp_with_sync_demons_devils
|
||||
# subtract_hp callsites in TObjectV8047c128_v17_accept_hit
|
||||
.data <VERS 0x80013454 0x80013484 0x800137F4 0x80013434 0x8001346C 0x8001346C 0x80013434 0x800134AC>
|
||||
.data 4
|
||||
@@ -155,8 +164,49 @@ handle_6xE4: # [std] (G_IncrementEnemyDamage_Extension_6xE4* cmd @ r3) -> void
|
||||
blt handle_6xE4_return
|
||||
cmplwi r3, 0x1B50
|
||||
bge handle_6xE4_return
|
||||
bl state_for_enemy # auto* st = state_for_enemy(cmd->header.entity_id);
|
||||
|
||||
bl get_enemy_entity
|
||||
stw [r1 + 0x18], r3 # TObjEnemy* ene @ var18 = get_enemy_entity(cmd->header.entity_id);
|
||||
|
||||
li r3, 2
|
||||
lhbrx r3, [r31 + r3]
|
||||
bl state_for_enemy # EnemyState* st = state_for_enemy(cmd->header.entity_id);
|
||||
|
||||
li r4, 0x0C
|
||||
lwbrx r5, [r31 + r4] # cmd->factor
|
||||
andis. r0, r5, 0x8000
|
||||
bne handle_6xE4_not_proportional
|
||||
stwx [r31 + r4], r5
|
||||
|
||||
li r8, 0x0A
|
||||
lhbrx r8, [r31 + r8]
|
||||
lhz r4, [r3 + 6]
|
||||
sub r8, r8, r4 # current_hp = cmd->max_hp - st->total_damage
|
||||
cmpwi r8, 0
|
||||
blt handle_6xE4_not_proportional
|
||||
|
||||
lis r4, 0x4B00
|
||||
or r5, r4, r8
|
||||
stw [r1 - 4], r5
|
||||
lfs f1, [r1 - 4]
|
||||
stw [r1 - 4], r4
|
||||
lfs f2, [r1 - 4]
|
||||
fsubs f1, f1, f2 # f1 = static_cast<float>(current_hp)
|
||||
lfs f2, [r31 + 0x0C]
|
||||
fmuls f1, f1, f2
|
||||
fctiwz f1, f1
|
||||
stfd [r1 - 8], f1
|
||||
lwz r8, [r1 - 4]
|
||||
li r4, 1
|
||||
cmp r8, r4
|
||||
bge handle_6xE4_proportional_positive
|
||||
mr r8, r4
|
||||
handle_6xE4_proportional_positive:
|
||||
li r5, 0x04
|
||||
sthbrx [r31 + r5], r8
|
||||
handle_6xE4_not_proportional:
|
||||
|
||||
# r3 still has the return value of state_for_enemy
|
||||
lhz r4, [r3 + 6] # st->total_damage
|
||||
li r5, 0x04
|
||||
lhbrx r5, [r31 + r5] # cmd->hit_amount
|
||||
@@ -175,7 +225,10 @@ handle_6xE4: # [std] (G_IncrementEnemyDamage_Extension_6xE4* cmd @ r3) -> void
|
||||
ori r4, r4, 0x800
|
||||
stw [r3], r4 # st->game_flags |= 0x800;
|
||||
|
||||
# Send 6x0A with dead flag
|
||||
# Send 6x0A with dead flag, but only if the entity is constructed
|
||||
lwz r6, [r1 + 0x18]
|
||||
cmplwi r6, 0
|
||||
beq handle_6xE4_return
|
||||
stw [r1 + 0x14], r4
|
||||
li r6, 0x12
|
||||
sthbrx [r1 + r6], r5
|
||||
@@ -198,9 +251,7 @@ handle_6xE4_damage_nonnegative:
|
||||
mr r30, r3
|
||||
bl send_debug_info
|
||||
|
||||
li r3, 2
|
||||
lhbrx r3, [r31 + r3]
|
||||
bl get_enemy_entity
|
||||
lwz r3, [r1 + 0x18] # if (ene) ene->v50_on_state_updated(&st);
|
||||
cmplwi r3, 0
|
||||
beq handle_6xE4_return
|
||||
mr r4, r30
|
||||
@@ -229,13 +280,23 @@ state_for_enemy: # [/r4] (uint16_t entity_id @ r3) -> EnemyState* @ r3
|
||||
|
||||
|
||||
|
||||
on_TObjectV8047c128_add_hp_with_sync: # [std] (TObjectV8047c128* ene @ r3, int16_t amount @ r4) -> void
|
||||
# AdjustmentType:
|
||||
# 0 = SUBTRACT
|
||||
# 1 = SUBTRACT_PROPORTION
|
||||
# 2 = ADD
|
||||
|
||||
on_TObjectV8047c128_subtract_hp_with_sync_demons_devils:
|
||||
li r5, 1
|
||||
b on_add_or_subtract_hp
|
||||
|
||||
on_TObjectV8047c128_add_hp_with_sync: # [std] (TObjectV8047c128* ene @ r3, int16_t amount @ r4) -> void
|
||||
li r5, 2
|
||||
b on_add_or_subtract_hp
|
||||
|
||||
on_TObjectV8047c128_subtract_hp_with_sync: # [std] (TObjectV8047c128* ene @ r3, int16_t amount @ r4) -> void
|
||||
li r5, 0
|
||||
on_add_or_subtract_hp: # [std] (TObjectV8047c128* ene @ r3, int16_t amount @ r4, bool is_add @ r5) -> void
|
||||
|
||||
on_add_or_subtract_hp: # [std] (TObjectV8047c128* ene @ r3, int16_t amount @ r4, AdjustmentType type @ r5) -> void
|
||||
lwz r12, [r13 - <VERS 0x50A0 0x5098 0x5078 0x5078 0x5088 0x5088 0x5068 0x5028>]
|
||||
andi. r12, r12, 0x0080
|
||||
beq on_add_or_subtract_hp_skip_send
|
||||
@@ -252,10 +313,10 @@ on_add_or_subtract_hp: # [std] (TObjectV8047c128* ene @ r3, int16_t amount @ r4
|
||||
|
||||
mflr r0
|
||||
stw [r1 + 4], r0
|
||||
stwu [r1 - 0x20], r1
|
||||
stw [r1 + 0x14], r29
|
||||
stw [r1 + 0x18], r30
|
||||
stw [r1 + 0x1C], r31
|
||||
stwu [r1 - 0x40], r1
|
||||
stw [r1 + 0x34], r29
|
||||
stw [r1 + 0x38], r30
|
||||
stw [r1 + 0x3C], r31
|
||||
mr r29, r3
|
||||
mr r30, r4
|
||||
mr r31, r5
|
||||
@@ -264,14 +325,14 @@ on_add_or_subtract_hp: # [std] (TObjectV8047c128* ene @ r3, int16_t amount @ r4
|
||||
bl state_for_enemy # EnemyState* st = state_for_enemy(ene->entity_id);
|
||||
|
||||
mr r5, r30
|
||||
cmplwi r31, 0
|
||||
beq on_add_or_subtract_hp_skip_negate_amount
|
||||
cmplwi r31, 2
|
||||
bne on_add_or_subtract_hp_skip_negate_amount
|
||||
neg r5, r5
|
||||
on_add_or_subtract_hp_skip_negate_amount:
|
||||
|
||||
li r4, 0x1C
|
||||
lhbrx r4, [r29 + r4]
|
||||
oris r4, r4, 0xE403
|
||||
oris r4, r4, 0xE404
|
||||
stw [r1 + 0x08], r4
|
||||
li r4, 0x0C
|
||||
sthbrx [r1 + r4], r5
|
||||
@@ -284,25 +345,37 @@ on_add_or_subtract_hp_skip_negate_amount:
|
||||
li r4, 0x2B8
|
||||
lhbrx r4, [r29 + r4]
|
||||
sth [r1 + 0x12], r4
|
||||
|
||||
li r5, 0x14
|
||||
cmplwi r31, 1
|
||||
bne on_add_or_subtract_hp_not_proportional
|
||||
fmuls f0, f30, f0
|
||||
stfsx [r1 + r5], f0 # current_hp_factor (== (1.0 - special_amount * 0.01)) * weapon_reduction_factor
|
||||
lwzx r4, [r1 + r5]
|
||||
b on_add_or_subtract_hp_proportional_check_end
|
||||
on_add_or_subtract_hp_not_proportional:
|
||||
lis r4, 0xBF80
|
||||
on_add_or_subtract_hp_proportional_check_end:
|
||||
stwbrx [r1 + r5], r4
|
||||
|
||||
mr r3, r11
|
||||
addi r4, r1, 0x08
|
||||
li r5, 0x0C
|
||||
li r5, 0x10
|
||||
bl send_60
|
||||
|
||||
on_add_or_subtract_hp_tail_call:
|
||||
mr r3, r29
|
||||
mr r4, r30
|
||||
mr r5, r31
|
||||
lwz r31, [r1 + 0x1C]
|
||||
lwz r30, [r1 + 0x18]
|
||||
lwz r29, [r1 + 0x14]
|
||||
addi r1, r1, 0x20
|
||||
lwz r31, [r1 + 0x3C]
|
||||
lwz r30, [r1 + 0x38]
|
||||
lwz r29, [r1 + 0x34]
|
||||
addi r1, r1, 0x40
|
||||
lwz r0, [r1 + 4]
|
||||
mtlr r0
|
||||
|
||||
on_add_or_subtract_hp_skip_send:
|
||||
cmplwi r5, 0
|
||||
beq on_add_or_subtract_hp_tail_call_subtract_hp
|
||||
cmplwi r5, 2
|
||||
bne on_add_or_subtract_hp_tail_call_subtract_hp
|
||||
b TObjectV8047c128_add_hp
|
||||
on_add_or_subtract_hp_tail_call_subtract_hp:
|
||||
b TObjectV8047c128_subtract_hp
|
||||
+55
-11
@@ -1,4 +1,3 @@
|
||||
.meta hide_from_patches_menu
|
||||
.meta name="DMC"
|
||||
.meta description="Mitigates effects\nof enemy health\ndesync"
|
||||
.meta client_flag="0x2000000000000000"
|
||||
@@ -103,11 +102,11 @@ on_add_or_subtract_hp_start: # (TObjectV004434c8* this @ eax, int16_t amount @
|
||||
imul edx, edx, 0x0C
|
||||
add edx, [<VERS 0x00633068 0x006336C8 0x0063B210 0x006386F8 0x00637F90 0x006386F8 0x00638A90>] # eax = state_for_enemy(cmd->header.entity_id)
|
||||
|
||||
sub esp, 0x0C
|
||||
mov word [esp], 0x03E4
|
||||
sub esp, 0x10
|
||||
mov word [esp], 0x04E4
|
||||
mov bx, [eax + 0x1C]
|
||||
mov [esp + 0x02], bx # cmd.entity_id
|
||||
cmp dword [esp + 0x18], <VERS 0x002A6900 0x002A73E0 0x002A88B0 0x002A8340 0x002A8520 0x002A8360 0x002A85E0> # Check if callsite is add_hp
|
||||
cmp dword [esp + 0x1C], <VERS 0x002A6900 0x002A73E0 0x002A88B0 0x002A8340 0x002A8520 0x002A8360 0x002A85E0> # Check if callsite is add_hp
|
||||
jne on_add_or_subtract_hp_skip_negate_amount
|
||||
neg cx
|
||||
on_add_or_subtract_hp_skip_negate_amount:
|
||||
@@ -118,16 +117,31 @@ on_add_or_subtract_hp_skip_negate_amount:
|
||||
mov [esp + 0x08], bx # cmd.current_hp
|
||||
mov bx, [eax + 0x02BC]
|
||||
mov [esp + 0x0A], bx # cmd.max_hp
|
||||
mov dword [esp + 0x0C], 0xBF800000 # cmd.factor
|
||||
|
||||
cmp dword [esp + 0x1C], <VERS 0x002A7CE0 0x002A87C4 0x002A9C94 0x002A9724 0x002A9904 0x002A9744 0x002A99C4> # Check if callsite is Devil's/Demon's
|
||||
jne on_add_or_subtract_hp_not_proportional
|
||||
# esp is 0x20 down from where it is in caller's context
|
||||
mov cx, 100
|
||||
sub cx, [esp + 0x34] # cx = (100 - special_amount)
|
||||
movsx ecx, cx
|
||||
mov [esp - 4], ecx
|
||||
fild st0, dword [esp - 4] # current_hp_factor = static_cast<float>(100 - special_amount)
|
||||
fmul st0, dword [esp + 0x38] # *= weapon_reduction_factor
|
||||
mov dword [esp - 4], 0x42C80000 # 100.0f
|
||||
fdiv st0, dword [esp - 4]
|
||||
fstp dword [esp + 0x0C], st0 # cmd.factor = ((100 - special_amount) * weapon_reduction_factor) / 100
|
||||
on_add_or_subtract_hp_not_proportional:
|
||||
|
||||
mov ecx, esp
|
||||
mov ebx, [<VERS 0x0071EEFC 0x0071F55C 0x007270A0 0x0072459C 0x00723E20 0x0072459C 0x00724920>] # root_protocol
|
||||
test ebx, ebx
|
||||
jz on_add_or_subtract_hp_skip_send
|
||||
mov eax, 0x0C
|
||||
mov eax, 0x10
|
||||
# Can't just `call <addr>` here because this code is relocated at apply time
|
||||
mov edx, <VERS 0x002DA120 0x002DACF0 0x002DC5B0 0x002DC080 0x002DC580 0x002DC0B0 0x002DC600>
|
||||
call edx # send_60(root_protocol, &out_cmd, sizeof(out_cmd))
|
||||
add esp, 0x0C
|
||||
add esp, 0x10
|
||||
|
||||
on_add_or_subtract_hp_skip_send:
|
||||
mov edx, <VERS 0x002A80C0 0x002A8BA0 0x002AA070 0x002A9B00 0x002A9CE0 0x002A9B20 0x002A9DA0> # subtract_hp
|
||||
@@ -179,10 +193,36 @@ handle_6xE4: # [std] (G_6xE4* cmd @ [esp + 4]) -> void
|
||||
cmp eax, 0x1B50
|
||||
jge handle_6xE4_return
|
||||
|
||||
mov edi, eax
|
||||
call <VERS 0x002B36B0 0x002B4180 0x002B5710 0x002B5220 0x002B5400 0x002B5240 0x002B5510> # TObjEnemy* ene = get_enemy_entity(cmd->header.entity_id);
|
||||
push eax
|
||||
|
||||
movzx eax, word [ebx + 2]
|
||||
and eax, 0x0FFF
|
||||
imul eax, eax, 0x0C
|
||||
add eax, [<VERS 0x00633068 0x006336C8 0x0063B210 0x006386F8 0x00637F90 0x006386F8 0x00638A90>] # eax = state_for_enemy(cmd->header.entity_id)
|
||||
|
||||
cmp dword [ebx + 0x0C], 0
|
||||
jl handle_6xE4_not_proportional
|
||||
mov cx, [ebx + 0x0A] # cmd->max_hp
|
||||
sub cx, [eax + 0x06] # st.total_damage
|
||||
movzx ecx, cx
|
||||
xor edx, edx
|
||||
cmp ecx, edx
|
||||
cmovl ecx, edx
|
||||
mov [esp - 4], ecx
|
||||
fild st0, dword [esp - 4] # current_hp = static_cast<float>(max<int32_t>(cmd->max_hp - st.total_damage, 0))
|
||||
fld st0, dword [ebx + 0x0C]
|
||||
fmulp st1, st0
|
||||
fistp dword [esp - 4], st0
|
||||
mov ecx, dword [esp - 4] # adjusted_hit_amount = static_cast<int16_t>(current_hp * cmd->factor)
|
||||
xor edx, edx
|
||||
inc edx
|
||||
cmp ecx, edx
|
||||
cmovl ecx, edx
|
||||
mov [ebx + 0x04], cx # cmd->hit_amount = min<int32_t>(1, adjusted_hit_amount)
|
||||
handle_6xE4_not_proportional:
|
||||
|
||||
movzx edx, word [eax + 0x06] # st.total_damage
|
||||
movsx esi, word [ebx + 0x04] # cmd->hit_amount
|
||||
movzx edi, word [ebx + 0x0A] # cmd->max_hp
|
||||
@@ -192,9 +232,12 @@ handle_6xE4: # [std] (G_6xE4* cmd @ [esp + 4]) -> void
|
||||
mov [eax + 0x06], di # st.total_damage = cmd->max_hp;
|
||||
mov edx, [eax]
|
||||
test edx, 0x800
|
||||
jnz handle_6xE4_return
|
||||
jnz handle_6xE4_return_pop_ene
|
||||
or edx, 0x800
|
||||
mov [eax], edx
|
||||
|
||||
cmp dword [esp], 0
|
||||
je handle_6xE4_return_pop_ene
|
||||
push edx # out_cmd.flags
|
||||
sub esp, 8
|
||||
mov word [esp], 0x030A # out_cmd.header.{subcommand,size}
|
||||
@@ -216,7 +259,7 @@ handle_6xE4_root_protocol_missing:
|
||||
call <VERS 0x002DBC30 0x002DC7B0 0x002DE070 0x002DDB90 0x002DE090 0x002DDBC0 0x002DE0C0> # handle_60(&out_cmd)
|
||||
mov dword [<VERS 0x0071E8C8 0x0071EF28 0x00726A68 0x00723F68 0x007237E8 0x00723F68 0x007242E8>], 0
|
||||
|
||||
add esp, 0x10
|
||||
add esp, 0x14
|
||||
jmp handle_6xE4_return
|
||||
|
||||
handle_6xE4_damage_less_than_max_hp:
|
||||
@@ -226,15 +269,16 @@ handle_6xE4_damage_less_than_max_hp:
|
||||
mov [eax + 0x06], dx # st.total_damage = std::max<int16_t>(st.total_damage + cmd->hit_amount, 0);
|
||||
|
||||
mov esi, eax # esi = ene_st
|
||||
movzx di, word [ebx + 2]
|
||||
call <VERS 0x002B36B0 0x002B4180 0x002B5710 0x002B5220 0x002B5400 0x002B5240 0x002B5510> # auto* ene = get_enemy_entity(cmd->header.entity_id);
|
||||
mov eax, [esp] # eax = ene
|
||||
test eax, eax
|
||||
jz handle_6xE4_return
|
||||
jz handle_6xE4_return_pop_ene
|
||||
mov ecx, eax
|
||||
push esi
|
||||
mov edx, [ecx]
|
||||
call [edx + 0x138] # ene->vtable[0x4E](ene, &st);
|
||||
|
||||
handle_6xE4_return_pop_ene:
|
||||
add esp, 4
|
||||
handle_6xE4_return:
|
||||
pop edi
|
||||
pop esi
|
||||
+56
-13
@@ -1,4 +1,3 @@
|
||||
.meta hide_from_patches_menu
|
||||
.meta name="DMC"
|
||||
.meta description="Mitigates effects\nof enemy health\ndesync"
|
||||
.meta client_flag="0x2000000000000000"
|
||||
@@ -37,10 +36,36 @@ handle_6xE4_start: # (G_6xE4* cmd @ [esp + 4]) -> void
|
||||
cmp eax, 0x1B50
|
||||
jge handle_6xE4_return
|
||||
|
||||
movzx eax, word [ebx + 2]
|
||||
.include GetEnemyEntity-59NL # auto* ene = get_enemy_entity(cmd->header.entity_id);
|
||||
push eax
|
||||
|
||||
movzx eax, word [ebx + 2]
|
||||
and eax, 0x0FFF
|
||||
imul eax, eax, 0x0C
|
||||
add eax, [0x00AB02B8] # eax = state_for_enemy(cmd->header.entity_id)
|
||||
|
||||
cmp dword [ebx + 0x0C], 0
|
||||
jl handle_6xE4_not_proportional
|
||||
mov cx, [ebx + 0x0A] # cmd->max_hp
|
||||
sub cx, [eax + 0x06] # st.total_damage
|
||||
movzx ecx, cx
|
||||
xor edx, edx
|
||||
cmp ecx, edx
|
||||
cmovl ecx, edx
|
||||
mov [esp - 4], ecx
|
||||
fild st0, dword [esp - 4] # current_hp = static_cast<float>(max<int32_t>(cmd->max_hp - st.total_damage, 0))
|
||||
fld st0, dword [ebx + 0x0C]
|
||||
fmulp st1, st0
|
||||
fistp dword [esp - 4], st0
|
||||
mov ecx, dword [esp - 4] # adjusted_hit_amount = static_cast<int16_t>(current_hp * cmd->factor)
|
||||
xor edx, edx
|
||||
inc edx
|
||||
cmp ecx, edx
|
||||
cmovl ecx, edx
|
||||
mov [ebx + 0x04], cx # cmd->hit_amount = min<int32_t>(1, adjusted_hit_amount)
|
||||
handle_6xE4_not_proportional:
|
||||
|
||||
movzx edx, word [eax + 0x06] # st.total_damage
|
||||
movsx esi, word [ebx + 0x04] # cmd->hit_amount
|
||||
movzx edi, word [ebx + 0x0A] # cmd->max_hp
|
||||
@@ -50,9 +75,11 @@ handle_6xE4_start: # (G_6xE4* cmd @ [esp + 4]) -> void
|
||||
mov [eax + 0x06], di # st.total_damage = cmd->max_hp;
|
||||
mov edx, [eax]
|
||||
test edx, 0x800
|
||||
jnz handle_6xE4_return
|
||||
jnz handle_6xE4_return_pop_ene
|
||||
or edx, 0x800
|
||||
mov [eax], edx
|
||||
cmp dword [esp], 0
|
||||
je handle_6xE4_return_pop_ene
|
||||
push edx # out_cmd.flags
|
||||
sub esp, 8
|
||||
mov word [esp], 0x030A # out_cmd.header.{subcommand,size}
|
||||
@@ -64,7 +91,7 @@ handle_6xE4_start: # (G_6xE4* cmd @ [esp + 4]) -> void
|
||||
mov ecx, esp
|
||||
mov edx, 0x008003E0
|
||||
call edx # send_and_handle_60(&out_cmd);
|
||||
add esp, 0x0C
|
||||
add esp, 0x10
|
||||
jmp handle_6xE4_return
|
||||
|
||||
handle_6xE4_damage_less_than_max_hp:
|
||||
@@ -74,15 +101,16 @@ handle_6xE4_damage_less_than_max_hp:
|
||||
mov [eax + 0x06], dx # st.total_damage = std::max<int16_t>(st.total_damage + cmd->hit_amount, 0);
|
||||
|
||||
mov edx, eax # edx = ene_st
|
||||
movzx eax, word [ebx + 2]
|
||||
.include GetEnemyEntity-59NL # auto* ene = get_enemy_entity(cmd->header.entity_id);
|
||||
mov eax, [esp] # eax = ene
|
||||
test eax, eax
|
||||
jz handle_6xE4_return
|
||||
jz handle_6xE4_return_pop_ene
|
||||
mov ecx, eax
|
||||
push edx
|
||||
mov edx, [ecx]
|
||||
call [edx + 0x148] # ene->vtable[0x52](ene, &st);
|
||||
|
||||
handle_6xE4_return_pop_ene:
|
||||
add esp, 4
|
||||
handle_6xE4_return:
|
||||
pop edi
|
||||
pop esi
|
||||
@@ -114,7 +142,7 @@ handle_6xE4_end:
|
||||
push 5
|
||||
push 0x00775A60 # TObjectV00b441c0::v19_handle_hit_special_effects
|
||||
push 5
|
||||
push 0x00775726 # TObjectV00b441c0::v19_handle_hit_special_effects
|
||||
push 0x00775726 # TObjectV00b441c0::v19_handle_hit_special_effects (Devil's/Demon's)
|
||||
push 5
|
||||
push 0x00774D7B # TObjectV00b441c0::v18_accept_hit
|
||||
push 5
|
||||
@@ -151,12 +179,12 @@ on_add_or_subtract_hp_start: # (TObjectV00b441c0* this @ ecx, int16_t amount @
|
||||
imul eax, eax, 0x0C
|
||||
add eax, [0x00AB02B8] # eax = state_for_enemy(cmd->header.entity_id)
|
||||
|
||||
sub esp, 0x0C
|
||||
mov word [esp], 0x03E4
|
||||
sub esp, 0x10
|
||||
mov word [esp], 0x04E4
|
||||
mov dx, [ecx + 0x1C]
|
||||
mov [esp + 0x02], dx # cmd.entity_id
|
||||
mov dx, [esp + 0x10]
|
||||
cmp dword [esp + 0x0C], 0x0077444D # Check if callsite is add_hp
|
||||
mov dx, [esp + 0x14]
|
||||
cmp dword [esp + 0x10], 0x0077444D # Check if callsite is add_hp
|
||||
jne on_add_or_subtract_hp_skip_negate_amount
|
||||
neg dx
|
||||
on_add_or_subtract_hp_skip_negate_amount:
|
||||
@@ -167,15 +195,30 @@ on_add_or_subtract_hp_skip_negate_amount:
|
||||
mov [esp + 0x08], dx # cmd.current_hp
|
||||
mov dx, [ecx + 0x02BC]
|
||||
mov [esp + 0x0A], dx # cmd.max_hp
|
||||
mov dword [esp + 0x0C], 0xBF800000 # cmd.factor
|
||||
|
||||
cmp dword [esp + 0x10], 0x0077572B # Check if callsite is Devil's/Demon's
|
||||
jne on_add_or_subtract_hp_not_proportional
|
||||
# esp is 0x18 down from where it is in caller's context
|
||||
mov edx, 100
|
||||
sub edx, [esp + 0x24] # edx = (100 - special_amount)
|
||||
mov [esp - 4], edx
|
||||
fild st0, dword [esp - 4] # current_hp_factor = static_cast<float>(100 - special_amount)
|
||||
fmul st0, dword [esp + 0x50] # *= weapon_reduction_factor
|
||||
mov dword [esp - 4], 0x42C80000 # 100.0f
|
||||
fdiv st0, dword [esp - 4]
|
||||
fstp dword [esp + 0x0C], st0 # cmd.factor = ((100 - special_amount) * weapon_reduction_factor) / 100
|
||||
on_add_or_subtract_hp_not_proportional:
|
||||
|
||||
mov edx, esp
|
||||
push ecx
|
||||
push 0x0C
|
||||
push 0x10
|
||||
push edx
|
||||
mov ecx, [0x00AAB284]
|
||||
mov edx, 0x007D3F38
|
||||
call edx # send_60(root_protocol, &cmd, sizeof(cmd));
|
||||
pop ecx
|
||||
add esp, 0x0C
|
||||
add esp, 0x10
|
||||
|
||||
on_add_or_subtract_hp_skip_send:
|
||||
mov eax, 0x00777414 # subtract_hp
|
||||
@@ -1,5 +1,6 @@
|
||||
.meta name="VIP card"
|
||||
.meta description="Gives you a VIP card"
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
.versions 3SJT 3SJ0 3SE0 3SP0
|
||||
|
||||
|
||||
@@ -17,7 +17,12 @@ start:
|
||||
.data 0x8000BF30
|
||||
.deltaof code_start, code_end
|
||||
.address 0x8000BF30
|
||||
code_start:
|
||||
code_start: # [std] (TItemMag* this @ r3) -> void
|
||||
lwz r4, [r3 + 0xF0]
|
||||
lhz r4, [r4 + 0x1C] # r4 = this->owner_player->entity_id
|
||||
lwz r5, [r13 - <VERS 0x5298 0x5290 0x5270 0x5270 0x5280 0x5280 0x5260 0x5220>] # local_client_id
|
||||
cmpl r4, r5
|
||||
bnelr
|
||||
lis r3, 0x0002
|
||||
ori r3, r3, 0x2825
|
||||
li r4, 0
|
||||
|
||||
@@ -14,16 +14,70 @@ start:
|
||||
.include WriteCodeBlocksXB
|
||||
|
||||
.data <VERS 0x00180EF5 0x00181075 0x00181125 0x00181065 0x00181095 0x00181085 0x00181055>
|
||||
.data 0x0000000A
|
||||
.binary E998010000CCCC83C410
|
||||
.data 0x0A
|
||||
.address <VERS 0x00180EF5 0x00181075 0x00181125 0x00181065 0x00181095 0x00181085 0x00181055>
|
||||
hook_call:
|
||||
jmp hook1
|
||||
int 3
|
||||
int 3
|
||||
hook_ret_sound:
|
||||
add esp, 0x10
|
||||
hook_ret_no_sound:
|
||||
|
||||
.data <VERS 0x00181024 0x001811A4 0x00181254 0x00181194 0x001811C4 0x001811B4 0x00181184>
|
||||
.deltaof hook1, hook1_end
|
||||
.address <VERS 0x00181024 0x001811A4 0x00181254 0x00181194 0x001811C4 0x001811B4 0x00181184>
|
||||
hook1:
|
||||
xor eax, eax
|
||||
mov dword [esi + 0x00000194], eax
|
||||
jmp hook2
|
||||
hook1_end:
|
||||
|
||||
.data <VERS 0x00181092 0x00181212 0x001812C2 0x00181202 0x00181232 0x00181222 0x001811F2>
|
||||
.data 0x0000000D
|
||||
.binary 31C0898694010000505050EB52
|
||||
.deltaof hook2, hook2_end
|
||||
.address <VERS 0x00181092 0x00181212 0x001812C2 0x00181202 0x00181232 0x00181222 0x001811F2>
|
||||
hook2:
|
||||
mov edx, dword [esi + 0x000000F0]
|
||||
movzx edx, word [edx + 0x0000001C] # edx = this->owner_player->entity_id
|
||||
jmp hook3
|
||||
hook2_end:
|
||||
|
||||
.data <VERS 0x001810F1 0x00181271 0x00181321 0x00181261 0x00181291 0x00181281 0x00181251>
|
||||
.data 0x0000000F
|
||||
.binary <VERS 048D50BA30922E00FFD2E9FCFDFFFF 048D50BAB09D2E00FFD2E9FCFDFFFF 048D50BA90B32E00FFD2E9FCFDFFFF 048D50BA90B12E00FFD2E9FCFDFFFF 048D50BAB0B32E00FFD2E9FCFDFFFF 048D50BAC0B12E00FFD2E9FCFDFFFF 048D50BAE0B32E00FFD2E9FCFDFFFF>
|
||||
.deltaof hook3, hook3_end
|
||||
.address <VERS 0x001810F1 0x00181271 0x00181321 0x00181261 0x00181291 0x00181281 0x00181251>
|
||||
hook3:
|
||||
cmp [<VERS 0x0071EFC0 0x0071F620 0x00727164 0x00724660 0x00723EE4 0x00724660 0x007249E4>], edx # local_client_id
|
||||
jmp hook4
|
||||
hook3_end:
|
||||
|
||||
.data <VERS 0x001811D7 0x00181357 0x00181407 0x00181347 0x00181377 0x00181367 0x00181337>
|
||||
.deltaof hook4, hook4_end
|
||||
.address <VERS 0x001811D7 0x00181357 0x00181407 0x00181347 0x00181377 0x00181367 0x00181337>
|
||||
hook4:
|
||||
jne hook_ret_no_sound
|
||||
jmp hook5
|
||||
hook4_end:
|
||||
|
||||
.data <VERS 0x001811F3 0x00181373 0x00181423 0x00181363 0x00181393 0x00181383 0x00181353>
|
||||
.deltaof hook5, hook5_end
|
||||
.address <VERS 0x001811F3 0x00181373 0x00181423 0x00181363 0x00181393 0x00181383 0x00181353>
|
||||
hook5:
|
||||
push eax
|
||||
push eax
|
||||
push eax
|
||||
add al, 0x8D
|
||||
push eax
|
||||
mov edx, <VERS 0x002E9230 0x002E9DB0 0x002EB390 0x002EB190 0x002EB3B0 0x002EB1C0 0x002EB3E0> # play_sound
|
||||
jmp hook6
|
||||
hook5_end:
|
||||
|
||||
.data <VERS 0x00181211 0x00181391 0x00181441 0x00181381 0x001813B1 0x001813A1 0x00181371>
|
||||
.deltaof hook6, hook6_end
|
||||
.address <VERS 0x00181211 0x00181391 0x00181441 0x00181381 0x001813B1 0x001813A1 0x00181371>
|
||||
hook6:
|
||||
call edx
|
||||
jmp hook_ret_sound
|
||||
hook6_end:
|
||||
|
||||
.data 0x00000000
|
||||
.data 0x00000000
|
||||
|
||||
@@ -14,8 +14,12 @@ get_code_size:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end
|
||||
patch_code:
|
||||
patch_code: # [eax] (TItemMag* this @ ecx) -> void
|
||||
mov dword [ecx + 0x01B8], eax
|
||||
mov eax, [ecx + 0x00F8]
|
||||
movzx eax, word [eax + 0x001C] # eax = this->owner_player->entity_id
|
||||
cmp [0x00A9C4F4], eax
|
||||
jne patch_code_skip_sound
|
||||
push 0
|
||||
push 0
|
||||
push 0
|
||||
@@ -23,6 +27,7 @@ patch_code:
|
||||
mov eax, 0x00814298
|
||||
call eax
|
||||
add esp, 0x10
|
||||
patch_code_skip_sound:
|
||||
ret
|
||||
patch_code_end:
|
||||
push ecx
|
||||
|
||||
+24
-13
@@ -639,7 +639,6 @@
|
||||
// 0x008 - appears in solo mode (BB)
|
||||
// 0x010 - appears at government counter (BB)
|
||||
// 0x020 - appears in download quest menu
|
||||
// 0x040 - appears in Episode 3 download quest menu
|
||||
// 0x080 - hide quests that don't match the game's episode
|
||||
// 0x100 - is Episode 2 Challenge category
|
||||
// directory_name: the directory inside system/quests that contains quests
|
||||
@@ -668,19 +667,31 @@
|
||||
[0x010, "government-ep2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"],
|
||||
[0x010, "government-ep4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"],
|
||||
[0x020, "download", "Download", "$E$C6Quests to download\nto your Memory Card"],
|
||||
[0x040, "download-ep3-trial", "Trial Download", "$E$C6Quests to download\nto your Memory Card\nfrom Episode 3\nTrial Edition"],
|
||||
[0x040, "download-ep3", "Download", "$E$C6Quests to download\nto your Memory Card"],
|
||||
],
|
||||
|
||||
// BB bank size. If you change either of these values, you must also add
|
||||
// "BankSize" to BBRequiredPatches, and change the patch contents in
|
||||
// system/client-functions/BlueBurstExclusive/BankSize.59NL.patch.s to
|
||||
// reflect the counts you set here.
|
||||
"BBMaxBankItems": 200,
|
||||
"BBMaxBankMeseta": 999999,
|
||||
|
||||
// Item stack limits. Note that changing these does not affect the client's
|
||||
// behavior automatically - this only exists to allow the server to understand
|
||||
// the behavior of clients that are already patched with different stack
|
||||
// limits. If you want to use an unpatched BB client but still have custom
|
||||
// stack limits, you can use the StackLimits runtime patch by editing
|
||||
// behavior automatically - this only exists to allow the server to
|
||||
// understand the behavior of clients that are already patched with different
|
||||
// stack limits.
|
||||
// If you want to use an unpatched BB client but still have custom stack
|
||||
// limits, you can use the StackLimits runtime patch by editing
|
||||
// system/client-functions/BlueBurstExclusive/StackLimits.59NL.patch.s to
|
||||
// match the BB stack limits and adding "StackLimits" to the BBRequiredPatches
|
||||
// list. The ToolLimits list is indexed by data1[1] (that is, the second byte
|
||||
// of the item data); for items beyond the end of the list, the last entry's
|
||||
// match the BB stack limits and adding "StackLimits" to the
|
||||
// BBRequiredPatches list.
|
||||
// It's important that all players in the same game have the same stack
|
||||
// limits, both on the client and server! So, if you change the BB stack
|
||||
// limits, you must either prevent BB from playing with other versions (see
|
||||
// CompatibilityGroups) or write an analogous patch for all other versions
|
||||
// and add it to AutoPatches.
|
||||
// The ToolLimits list is indexed by data1[1] (that is, the second byte of
|
||||
// the item data); for items beyond the end of the list, the last entry's
|
||||
// value is used.
|
||||
"ItemStackLimits": [
|
||||
{"MesetaLimit": 999999, "ToolLimits": [10]}, // DC NTE
|
||||
@@ -1217,9 +1228,9 @@
|
||||
// true and false here, since the server doesn't have direct access to the
|
||||
// client's quest flags from their save file.
|
||||
// If you use an expression, the format is the same as the AvailableIf and
|
||||
// EnabledIf fields in quest JSON files (see system/quests/battle/b88001.json
|
||||
// for details). Note that the expression is only evaluated at the time the
|
||||
// game is created, and the player-specific tokens like C_EpX_YY refer to the
|
||||
// EnabledIf fields in quest JSONs (see system/quests/retrieval/q058.json for
|
||||
// details). Note that the expression is only evaluated at the time the game
|
||||
// is created, and the player-specific tokens like C_EpX_YY refer to the
|
||||
// player who created the game.
|
||||
// The UnlockAllAreas option is now gone; if you want the same behavior as if
|
||||
// it were enabled, uncomment all the "area unlocks" lines below. Note that
|
||||
|
||||
@@ -1 +1 @@
|
||||
../maps-download/download-ep3-trial/e765-gc3-e.mnm
|
||||
../maps-download/e765-gc3-e.mnm
|
||||
@@ -1 +1 @@
|
||||
../maps-download/download-ep3-trial/e765-gc3-j.mnm
|
||||
../maps-download/e765-gc3-j.mnm
|
||||
@@ -1 +1 @@
|
||||
../maps-download/download-ep3/e901-gc3-e.mnm
|
||||
../maps-download/e901-gc3-e.mnm
|
||||
@@ -1 +1 @@
|
||||
../maps-download/download-ep3/e901-gc3-j.mnm
|
||||
../maps-download/e901-gc3-j.mnm
|
||||
@@ -1 +1 @@
|
||||
../maps-download/download-ep3/e903-gc3-e.mnm
|
||||
../maps-download/e903-gc3-e.mnm
|
||||
@@ -1 +1 @@
|
||||
../maps-download/download-ep3/e903-gc3-j.mnm
|
||||
../maps-download/e903-gc3-j.mnm
|
||||
@@ -1 +1 @@
|
||||
../maps-download/download-ep3/e904-gc3-e.mnm
|
||||
../maps-download/e904-gc3-e.mnm
|
||||
@@ -1 +1 @@
|
||||
../maps-download/download-ep3/e904-gc3-j.mnm
|
||||
../maps-download/e904-gc3-j.mnm
|
||||
@@ -1 +1 @@
|
||||
../maps-download/download-ep3/e905-gc3-e.mnm
|
||||
../maps-download/e905-gc3-e.mnm
|
||||
@@ -1 +1 @@
|
||||
../maps-download/download-ep3/e905-gc3-j.mnm
|
||||
../maps-download/e905-gc3-j.mnm
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user