implement savechar/loadchar on DCv2 and Xbox

This commit is contained in:
Martin Michelsen
2024-05-12 21:07:56 -07:00
parent 625e8e0624
commit 2ff75fe132
17 changed files with 350 additions and 10 deletions
+25 -3
View File
@@ -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 <slot>` and `$loadchar <slot>` 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 <username> <password> <slot>`, 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 <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
* Character data commands (game server only)
* `$savechar <slot>`: 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 <slot>` (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 <username> <password> <slot>`: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot (1-4). Any character already in that slot is overwritten. (This command is similar to `$savechar`, except it overwrites a BB character slot, and can transfer characters across accounts.) Note that the character's chat data, quick menu config, and bank contents are not copied, since there is no way for the server to request those types of data.
* `$savechar <slot>`: Saves your current character data on the server in the specified slot. See the "Server-side saves" section for more details.
* `$loadchar <slot>`: Saves your current character data on the server in the specified slot. See the "Server-side saves" section for more details.
* `$bbchar <username> <password> <slot>`: 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 <stat> <value>`: Modifies your character data. If you are on V3 (GameCube/Xbox), this command does nothing. If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby. If cheats are allowed on the server, `<stat>` can be any of `atp`, `mst`, `evp`, `hp`, `dfp`, `ata`, `lck`, `meseta`, `exp`, `level`, `namecolor`, `secid`, `name`, `language`, `npc`, or `tech`. If cheats are not allowed, only `namecolor`, `name`, `language`, and `npc` can be used. Changing your character's language is only useful on BB; to do so, use a single-character language code (e.g. to switch your character to English, use `$edit language E`; for Japanese, use `$edit language J`).
* Blue Burst player commands (game server only)
+17 -4
View File
@@ -1469,7 +1469,10 @@ static void server_command_savechar(shared_ptr<Client> c, const std::string& arg
}
static void server_command_loadchar(shared_ptr<Client> 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<Client> 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<Client> c, const std::string& arg
});
};
if (c->version() == Version::GC_V3) {
if (c->version() == Version::DC_V2) {
auto dc_char = make_shared<PSODCV2CharacterFile>(c->character()->to_dc_v2());
send_set_extended_player_info.operator()<PSODCV2CharacterFile>(c, dc_char);
} else if (c->version() == Version::GC_V3) {
auto gc_char = make_shared<PSOGCCharacterFile::Character>(c->character()->to_gc());
send_set_extended_player_info.operator()<PSOGCCharacterFile::Character>(c, gc_char);
} else if (c->version() == Version::XB_V3) {
+39
View File
@@ -600,6 +600,43 @@ shared_ptr<PSOBBCharacterFile> 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<false>(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<size_t>(ret.symbol_chats.size(), this->symbol_chats.size()); z++) {
auto& ret_sc = ret.symbol_chats[z];
const auto& gc_sc = this->symbol_chats[z];
ret_sc.present = gc_sc.present.load();
ret_sc.name.encode(gc_sc.name.decode(language), language);
ret_sc.spec = gc_sc.spec;
}
for (size_t z = 0; z < std::min<size_t>(ret.shortcuts.size(), this->shortcuts.size()); z++) {
ret.shortcuts[z] = this->shortcuts[z].convert<false, TextEncoding::MARKED, 0x3C>(language);
}
ret.battle_records = this->battle_records;
ret.challenge_records = this->challenge_records;
for (size_t z = 0; z < std::min<size_t>(ret.tech_menu_shortcut_entries.size(), this->tech_menu_shortcut_entries.size()); z++) {
ret.tech_menu_shortcut_entries[z] = this->tech_menu_shortcut_entries[z].load();
}
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<true>(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<false>(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();
+4 -3
View File
@@ -649,6 +649,10 @@ struct PSOBBCharacterFile {
static std::shared_ptr<PSOBBCharacterFile> create_from_gc(const PSOGCCharacterFile::Character& gc);
static std::shared_ptr<PSOBBCharacterFile> 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);
////////////////////////////////////////////////////////////////////////////////
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
@@ -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