diff --git a/README.md b/README.md index 2c2cfc8a..81861125 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,28 @@ All versions of PSO can see and interact with each other in the lobby. newserv a In V1/V2 cross-version play, when any of the server drop modes are used, the server uses the drop table corresponding to the version the game was created with. (For example, if a DC V1 player created the game, rare-table-v1.json will be used, even after V2 players join.) +## Server-side saves + +newserv has the ability to save character data on the server side. For PSO BB, this is required of course, but this feature can also be used on other PSO versions. + +Each account has 4 BB character slots and 16 non-BB character file slots. The non-BB slots are independent of the BB slots, and can be accessed with the `$savechar ` and `$loadchar ` commands (slots are numbered 1 through 16). `$savechar` copies the character you're currently playing as and saves the data on the server, and `$loadchar` does the reverse, overwriting your current character with the data saved on the server. Note that you can load a character that was saved from a different version of PSO, which allows you to easily transfer characters between games. On v1 and v2, changes done by `$loadchar` will be undone if you join a game; to permanently save your changes, disconnect from the lobby after using the command. + +There is a third command, `$bbchar `, which behaves similarly to `$savechar` but writes the character data to a BB character slot in a different account instead (slots are numbered 1 through 4). This can be used to "upgrade" a character to BB from an earlier version. + +Exactly which data is saved and loaded depends on the game version: + +| Game | Inventory | Character | Options/chats | Quest flags | Bank | Battle/challenge | +|----------------------|-----------|-----------|---------------|-------------|------|------------------| +| PSO DC v1 prototypes | Yes | Yes | No | No | No | N/A | +| PSO DC v1 | Yes | Yes | No | No | No | N/A | +| PSO DC v2 | Yes | Yes | Yes | Yes | Yes | Yes | +| PSO PC (v2) | Yes | Yes | No | No | No | Save only | +| PSO GC NTE | Yes | Yes | No | No | No | Save only | +| PSO GC (not Plus) | Yes | Yes | Yes | Yes | Yes | Yes | +| PSO GC Plus | Save only | Save only | No | No | No | Save only | +| PSO Xbox | Yes | Yes | Yes | Yes | Yes | Yes | +| PSO BB | Yes | Yes | Yes | Yes | Yes | Yes | + ## Episode 3 features newserv supports many features unique to Episode 3: @@ -510,9 +532,9 @@ Some commands only work on the game server and not on the proxy server. The chat * `$patch `: Run a patch on your client. `` must exactly match the name of a patch on the server. * Character data commands (game server only) - * `$savechar `: Saves your current character data on the server in the specified slot (each account has 16 slots, numbered 1-16). These slots are separate from BB character slots; using this command does not affect BB characters. On non-Plus GC versions, this command also saves your bank contents and chat shortcuts. - * `$loadchar ` (v1, v2, and GC non-Plus only): Loads your character data from the specified slot. The changes will be undone if you join a game - to save your changes, disconnect from the lobby. - * `$bbchar `: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot (1-4). Any character already in that slot is overwritten. (This command is similar to `$savechar`, except it overwrites a BB character slot, and can transfer characters across accounts.) Note that the character's chat data, quick menu config, and bank contents are not copied, since there is no way for the server to request those types of data. + * `$savechar `: Saves your current character data on the server in the specified slot. See the "Server-side saves" section for more details. + * `$loadchar `: Saves your current character data on the server in the specified slot. See the "Server-side saves" section for more details. + * `$bbchar `: Saves your current character data on the server in a different account's BB character slots. See the "Server-side saves" section for more details. * `$edit `: Modifies your character data. If you are on V3 (GameCube/Xbox), this command does nothing. If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby. If cheats are allowed on the server, `` can be any of `atp`, `mst`, `evp`, `hp`, `dfp`, `ata`, `lck`, `meseta`, `exp`, `level`, `namecolor`, `secid`, `name`, `language`, `npc`, or `tech`. If cheats are not allowed, only `namecolor`, `name`, `language`, and `npc` can be used. Changing your character's language is only useful on BB; to do so, use a single-character language code (e.g. to switch your character to English, use `$edit language E`; for Japanese, use `$edit language J`). * Blue Burst player commands (game server only) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 5ca9ec3c..7bd6e2f5 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -1469,7 +1469,10 @@ static void server_command_savechar(shared_ptr c, const std::string& arg } static void server_command_loadchar(shared_ptr c, const std::string& args) { - if (!is_v1_or_v2(c->version()) && (c->version() != Version::GC_V3) && (c->version() != Version::XB_V3)) { + if (!is_v1_or_v2(c->version()) && + (c->version() != Version::GC_V3) && + (c->version() != Version::XB_V3) && + (c->version() != Version::BB_V4)) { send_text_message(c, "$C7This command cannot\nbe used on your\ngame version"); return; } @@ -1487,12 +1490,19 @@ static void server_command_loadchar(shared_ptr c, const std::string& arg } c->load_backup_character(c->login->account->account_id, index); - if ((c->version() == Version::GC_V3) || (c->version() == Version::XB_V3)) { + if (c->version() == Version::BB_V4) { + // On BB, it suffices to simply send the character file again + auto s = c->require_server_state(); + send_complete_player_bb(c); + send_player_leave_notification(l, c->lobby_client_id); + s->send_lobby_join_notifications(l, c); + + } else if ((c->version() == Version::DC_V2) || (c->version() == Version::GC_V3) || (c->version() == Version::XB_V3)) { // TODO: Support extended player info on other versions auto s = c->require_server_state(); if (c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL) || c->config.check_flag(Client::Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY)) { - send_text_message_printf(c, "Can\'t load character\ndata on PSO Plus"); + send_text_message_printf(c, "Can\'t load character\ndata on this game\nversion"); return; } @@ -1525,7 +1535,10 @@ static void server_command_loadchar(shared_ptr c, const std::string& arg }); }; - if (c->version() == Version::GC_V3) { + if (c->version() == Version::DC_V2) { + auto dc_char = make_shared(c->character()->to_dc_v2()); + send_set_extended_player_info.operator()(c, dc_char); + } else if (c->version() == Version::GC_V3) { auto gc_char = make_shared(c->character()->to_gc()); send_set_extended_player_info.operator()(c, gc_char); } else if (c->version() == Version::XB_V3) { diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index b4d9302f..85c01975 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -600,6 +600,43 @@ shared_ptr PSOBBCharacterFile::create_from_xb(const PSOXBCha return ret; } +PSODCV2CharacterFile PSOBBCharacterFile::to_dc_v2() const { + uint8_t language = this->inventory.language; + + PSODCV2CharacterFile ret; + ret.inventory = this->inventory; + ret.inventory.encode_for_client(Version::DC_V2, nullptr); + ret.disp = this->disp.to_dcpcv3(language, language); + ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2); + ret.creation_timestamp = this->creation_timestamp.load(); + ret.play_time_seconds = this->play_time_seconds.load(); + ret.option_flags = this->option_flags.load(); + ret.save_count = this->save_count.load(); + ret.quest_flags = this->quest_flags; + ret.bank = this->bank; + ret.guild_card = this->guild_card; + for (size_t z = 0; z < std::min(ret.symbol_chats.size(), this->symbol_chats.size()); z++) { + auto& ret_sc = ret.symbol_chats[z]; + const auto& gc_sc = this->symbol_chats[z]; + ret_sc.present = gc_sc.present.load(); + ret_sc.name.encode(gc_sc.name.decode(language), language); + ret_sc.spec = gc_sc.spec; + } + for (size_t z = 0; z < std::min(ret.shortcuts.size(), this->shortcuts.size()); z++) { + ret.shortcuts[z] = this->shortcuts[z].convert(language); + } + ret.battle_records = this->battle_records; + ret.challenge_records = this->challenge_records; + for (size_t z = 0; z < std::min(ret.tech_menu_shortcut_entries.size(), this->tech_menu_shortcut_entries.size()); z++) { + ret.tech_menu_shortcut_entries[z] = this->tech_menu_shortcut_entries[z].load(); + } + for (size_t z = 0; z < 5; z++) { + ret.choice_search_config[z * 2] = this->choice_search_config.entries[z].parent_choice_id.load(); + ret.choice_search_config[z * 2 + 1] = this->choice_search_config.entries[z].choice_id.load(); + } + return ret; +} + PSOGCCharacterFile::Character PSOBBCharacterFile::to_gc() const { uint8_t language = this->inventory.language; @@ -607,6 +644,7 @@ PSOGCCharacterFile::Character PSOBBCharacterFile::to_gc() const { ret.inventory = this->inventory; ret.inventory.encode_for_client(Version::GC_V3, nullptr); ret.disp = this->disp.to_dcpcv3(language, language); + ret.disp.visual.enforce_lobby_join_limits_for_version(Version::GC_V3); ret.creation_timestamp = this->creation_timestamp.load(); ret.play_time_seconds = this->play_time_seconds.load(); ret.option_flags = this->option_flags.load(); @@ -650,6 +688,7 @@ PSOXBCharacterFileCharacter PSOBBCharacterFile::to_xb(uint64_t xb_user_id) const ret.inventory = this->inventory; ret.inventory.encode_for_client(Version::XB_V3, nullptr); ret.disp = this->disp.to_dcpcv3(language, language); + ret.disp.visual.enforce_lobby_join_limits_for_version(Version::XB_V3); ret.creation_timestamp = this->creation_timestamp.load(); ret.play_time_seconds = this->play_time_seconds.load(); ret.option_flags = this->option_flags.load(); diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 0a7f3de9..e45627bc 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -649,6 +649,10 @@ struct PSOBBCharacterFile { static std::shared_ptr create_from_gc(const PSOGCCharacterFile::Character& gc); static std::shared_ptr create_from_xb(const PSOXBCharacterFileCharacter& xb); + PSODCV2CharacterFile to_dc_v2() const; + PSOGCCharacterFile::Character to_gc() const; + PSOXBCharacterFileCharacter to_xb(uint64_t xb_user_id) 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); void add_meseta(uint32_t amount); @@ -670,9 +674,6 @@ struct PSOBBCharacterFile { uint8_t get_material_usage(MaterialType which) const; void set_material_usage(MaterialType which, uint8_t usage); void clear_all_material_usage(); - - PSOGCCharacterFile::Character to_gc() const; - PSOXBCharacterFileCharacter to_xb(uint64_t xb_user_id) const; } __packed_ws__(PSOBBCharacterFile, 0x2EA4); //////////////////////////////////////////////////////////////////////////////// diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OEF.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OEF.patch.s new file mode 100644 index 00000000..9e463cc1 --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OEF.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoDC +data: + .data 0x8C4EC4E0 # char_file_part1 + .data 0x8C4EC4E4 # char_file_part2 + # Server adds a PSODCV2CharacterFile here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJ5.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJ5.patch.s new file mode 100644 index 00000000..9e463cc1 --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJ5.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoDC +data: + .data 0x8C4EC4E0 # char_file_part1 + .data 0x8C4EC4E4 # char_file_part2 + # Server adds a PSODCV2CharacterFile here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJF.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJF.patch.s new file mode 100644 index 00000000..5ffdd0d3 --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OJF.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoDC +data: + .data 0x8C4E5F80 # char_file_part1 + .data 0x8C4E5F84 # char_file_part2 + # Server adds a PSODCV2CharacterFile here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OPF.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OPF.patch.s new file mode 100644 index 00000000..4b0bd131 --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.2OPF.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoDC +data: + .data 0x8C4DB9E0 # char_file_part1 + .data 0x8C4DB9E4 # char_file_part2 + # Server adds a PSODCV2CharacterFile here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OED.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OED.patch.s new file mode 100644 index 00000000..c1cd869d --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OED.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoXB +data: + .data 0x00632E04 # char_file_part1 + .data 0x00632EA8 # char_file_part2 + # Server adds a PSOXBCharacterFileCharacter here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OEU.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OEU.patch.s new file mode 100644 index 00000000..3d0eb8e6 --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OEU.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoXB +data: + .data 0x0063269C # char_file_part1 + .data 0x00632740 # char_file_part2 + # Server adds a PSOXBCharacterFileCharacter here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OJB.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OJB.patch.s new file mode 100644 index 00000000..73b772ff --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OJB.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoXB +data: + .data 0x0062D844 # char_file_part1 + .data 0x0062D8E8 # char_file_part2 + # Server adds a PSOXBCharacterFileCharacter here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OJD.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OJD.patch.s new file mode 100644 index 00000000..58d28716 --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OJD.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoXB +data: + .data 0x0062DDE4 # char_file_part1 + .data 0x0062DE88 # char_file_part2 + # Server adds a PSOXBCharacterFileCharacter here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OJU.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OJU.patch.s new file mode 100644 index 00000000..6592d5fc --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OJU.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoXB +data: + .data 0x0063591C # char_file_part1 + .data 0x006359C0 # char_file_part2 + # Server adds a PSOXBCharacterFileCharacter here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OPD.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OPD.patch.s new file mode 100644 index 00000000..c1cd869d --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OPD.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoXB +data: + .data 0x00632E04 # char_file_part1 + .data 0x00632EA8 # char_file_part2 + # Server adds a PSOXBCharacterFileCharacter here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OPU.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OPU.patch.s new file mode 100644 index 00000000..3c6b4b1a --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4OPU.patch.s @@ -0,0 +1,13 @@ +.meta hide_from_patches_menu +.meta name="SetExtendedPlayerInfo" +.meta description="" + +entry_ptr: +reloc0: + .offsetof start +start: + .include SetExtendedPlayerInfoXB +data: + .data 0x0063319C # char_file_part1 + .data 0x00633240 # char_file_part2 + # Server adds a PSOXBCharacterFileCharacter here diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfoDC.sh4.inc.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfoDC.sh4.inc.s new file mode 100644 index 00000000..e9313630 --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfoDC.sh4.inc.s @@ -0,0 +1,83 @@ +start: + sts.l -[r15], pr + + mova r0, [data] # r8 = data pointer + mov r7, r0 + + # memcpy(char_file_part1, inbound_part1, sizeof(char_file_part1)) + mov.l r4, [r7] + mov.l r4, [r4] + mov r5, r7 + add r5, 8 + calls memcpy + mov.l r6, [part1_size] + + # First, copy some important fields out of the saved character so that the + # game won't consider the character corrupt. Note that r5 already points to + # inbound_part2 here since it immediately follows inbound_part1. + mov.l r4, [r7 + 4] + mov.l r4, [r4] + mov r1, r4 # r1 = character_file_part2 + mov r2, r5 # r2 = inbound_part2 + + mov r4, r2 + mov r5, r1 + mov.l r0, [r5 + 0x04] # creation_timestamp + mov.l [r4 + 0x04], r0 + mov.l r0, [r5 + 0x08] # signature + mov.l [r4 + 0x08], r0 + add r4, 0x14 + add r5, 0x14 + calls memcpy # save_count, ppp_username, ppp_password (0x30 bytes total) + mov r6, 0x30 + + mov r4, r2 + mov r5, r1 + mov.l r0, [v1_creds_offset] + add r4, r0 + add r5, r0 + calls memcpy # v1_serial_number, v1_access_key + mov r6, 0x20 + + mov r4, r2 + mov r5, r1 + mov.l r0, [v2_creds_offset] + add r4, r0 + add r5, r0 + calls memcpy # v2_serial_number, v2_access_key + mov r6, 0x20 + + # memcpy(char_file_part2, inbound_part2, sizeof(char_file_part2)) + mov r4, r1 + mov r5, r2 + calls memcpy + mov.l r6, [part2_size] + + lds.l pr, [r15]+ + rets + mov r0, 0 + +memcpy: + test r6, r6 + bt memcpy_done + mov.l r0, [r5]+ + mov.l [r4], r0 + add r4, 4 + bs memcpy + add r6, -4 +memcpy_done: + rets + nop + + .align 4 +part1_size: + .data 0x0000041C +part2_size: + .data 0x000012D8 +v1_creds_offset: + .data 0x0000118C +v2_creds_offset: + .data 0x000012B8 + + .align 4 +data: diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfoXB.x86.inc.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfoXB.x86.inc.s new file mode 100644 index 00000000..7cda7f6f --- /dev/null +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfoXB.x86.inc.s @@ -0,0 +1,39 @@ + push ebx + + jmp get_data_ptr +get_data_ptr_ret: + pop ebx + + # Copy part1 data into place + mov eax, [ebx] + mov eax, [eax] + lea edx, [ebx + 8] + mov ecx, 0x041C + call memcpy + + # Copy part2 data into place, but retain the values of a few metadata fields + # so the game won't think the file is corrupt + mov eax, [ebx + 4] + mov eax, [eax] + push dword [eax + 0x04] # creation_timestamp + push dword [eax + 0x08] # signature + push dword [eax + 0x14] # save_count + lea edx, [ebx + 0x0424] + mov ecx, 0x23B8 # Intentionally skip the last 0xF0 bytes since they aren't populated by the server + call memcpy + mov eax, [ebx + 4] + mov eax, [eax] + pop dword [eax + 0x14] # save_count + pop dword [eax + 0x08] # signature + pop dword [eax + 0x04] # creation_timestamp + + mov eax, 1 + pop ebx + ret + +memcpy: + .include CopyData + ret + +get_data_ptr: + call get_data_ptr_ret