diff --git a/README.md b/README.md index 81861125..3152639d 100644 --- a/README.md +++ b/README.md @@ -343,7 +343,7 @@ Exactly which data is saved and loaded depends on the game version: | 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 NTE | Yes | Yes | Yes | Yes | Yes | Yes | | 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 | @@ -396,48 +396,48 @@ You can put assembly files in the system/client-functions directory with filenam The VERS token in client function filenames refers to the specific version of the game that the client function applies to. Some versions do not support receiving client functions at all. The specific versions are: -| Game | VERS | Supported | -|-------------------|------|-----------| -| PSO DC NTE | 1OJ1 | No | -| PSO DC 11/2000 | 1OJ2 | No | -| PSO DC 12/2000 | 1OJ3 | No | -| PSO DC 01/2001 | 1OJ4 | No | -| PSO DC v1 JP | 1OJF | No | -| PSO DC v1 US | 1OEF | No | -| PSO DC v1 EU | 1OPF | No | -| PSO DC 08/2001 | 2OJ5 | Yes | -| PSO DC v2 JP | 2OJF | Yes | -| PSO DC v2 US | 2OEF | Yes | -| PSO DC v2 EU | 2OPF | Yes | -| PSO PC (v2) | 2OJW | No | -| PSO GC NTE | 3OJT | Yes | -| PSO GC v1.2 JP | 3OJ2 | Yes | -| PSO GC v1.3 JP | 3OJ3 | Yes | -| PSO GC v1.4 JP | 3OJ4 | Yes | -| PSO GC v1.5 JP | 3OJ5 | No | -| PSO GC v1.0 US | 3OE0 | Yes | -| PSO GC v1.1 US | 3OE1 | Yes | -| PSO GC v1.2 US | 3OE2 | No | -| PSO GC v1.0 EU | 3OP0 | Yes | -| PSO GC Ep3 NTE | 3SJT | Yes | -| PSO GC Ep3 JP | 3SJ0 | Yes | -| PSO GC Ep3 US | 3SE0 | No | -| PSO GC Ep3 EU | 3SP0 | No | -| PSO Xbox Beta | 4OJB | Yes | -| PSO Xbox JP Disc | 4OJD | Yes | -| PSO Xbox JP TU | 4OJU | Yes | -| PSO Xbox US Disc | 4OED | Yes | -| PSO Xbox US TU | 4OEU | Yes | -| PSO Xbox EU Disc | 4OPD | Yes | -| PSO Xbox EU TU | 4OPU | Yes | -| PSO BB JP 1.25.13 | 51OC | Yes | -| PSO BB Tethealla | 51OC | Yes | +| Game | VERS | Architecture | +|-------------------|------|---------------| +| PSO DC NTE | 1OJ1 | Not supported | +| PSO DC 11/2000 | 1OJ2 | Not supported | +| PSO DC 12/2000 | 1OJ3 | Not supported | +| PSO DC 01/2001 | 1OJ4 | Not supported | +| PSO DC v1 JP | 1OJF | Not supported | +| PSO DC v1 US | 1OEF | Not supported | +| PSO DC v1 EU | 1OPF | Not supported | +| PSO DC 08/2001 | 2OJ5 | SH-4 | +| PSO DC v2 JP | 2OJF | SH-4 | +| PSO DC v2 US | 2OEF | SH-4 | +| PSO DC v2 EU | 2OPF | SH-4 | +| PSO PC (v2) | 2OJW | Not supported | +| PSO GC NTE | 3OJT | PowerPC | +| PSO GC v1.2 JP | 3OJ2 | PowerPC | +| PSO GC v1.3 JP | 3OJ3 | PowerPC | +| PSO GC v1.4 JP | 3OJ4 | PowerPC | +| PSO GC v1.5 JP | 3OJ5 | Not supported | +| PSO GC v1.0 US | 3OE0 | PowerPC | +| PSO GC v1.1 US | 3OE1 | PowerPC | +| PSO GC v1.2 US | 3OE2 | Not supported | +| PSO GC v1.0 EU | 3OP0 | PowerPC | +| PSO GC Ep3 NTE | 3SJT | PowerPC | +| PSO GC Ep3 JP | 3SJ0 | PowerPC | +| PSO GC Ep3 US | 3SE0 | Not supported | +| PSO GC Ep3 EU | 3SP0 | Not supported | +| PSO Xbox Beta | 4OJB | x86 | +| PSO Xbox JP Disc | 4OJD | x86 | +| PSO Xbox JP TU | 4OJU | x86 | +| PSO Xbox US Disc | 4OED | x86 | +| PSO Xbox US TU | 4OEU | x86 | +| PSO Xbox EU Disc | 4OPD | x86 | +| PSO Xbox EU TU | 4OPU | x86 | +| PSO BB JP 1.25.13 | 51OC | x86 | +| PSO BB Tethealla | 51OC | x86 | *Note: newserv uses the shorter GameCube versioning convention, where discs labeled DOL-XXXX-0-0Y are version 1.Y. The PSO community seems to use the convention 1.0Y in some places instead, but these are the same version. For example, the version that newserv calls v1.4 is the same as v1.04, and is labeled DOL-GPOJ-0-04 on the underside of the disc.* newserv comes with a set of patches for some of the above versions, based on AR codes originally made by Ralf at GC-Forever and Aleron Ives. Many of them were originally posted in [this thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050). -You can also put DOL files in the system/dol directory, and they will appear in the Programs menu for GC clients. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWord.ppc.s, WriteMemory.ppc.s, and RunDOL.ppc.s must be present in the system/client-functions directory. This has been tested on Dolphin but not on a real GameCube, so results may vary. +You can also put DOL files in the system/dol directory, and they will appear in the Programs menu for GC clients. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWord.ppc.s, WriteMemory.ppc.s, and RunDOL.ppc.s must be present in the system/client-functions/System directory. This has been tested on Dolphin but not on a real GameCube, so results may vary. Like other kinds of data, functions and DOL files are cached in memory. If you've changed any of these files, you can run `reload functions` or `reload dol-files` in the interactive shell to make the changes take effect without restarting the server. diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 878caf00..cefcc214 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -748,8 +748,7 @@ static void proxy_command_patch(shared_ptr ses, cons auto send_version_detect_or_send_call = [args, ses, send_call]() { bool is_gc = ::is_gc(ses->version()); bool is_xb = (ses->version() == Version::XB_V3); - if ((is_gc || is_xb) && - ses->config.specific_version == default_specific_version_for_version(ses->version(), -1)) { + if ((is_gc || is_xb) && specific_version_is_indeterminate(ses->config.specific_version)) { auto s = ses->require_server_state(); send_function_call( ses->client_channel, @@ -1472,6 +1471,7 @@ 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::GC_NTE) && (c->version() != Version::XB_V3) && (c->version() != Version::BB_V4)) { send_text_message(c, "$C7This command cannot\nbe used on your\ngame version"); @@ -1498,7 +1498,10 @@ static void server_command_loadchar(shared_ptr c, const std::string& arg 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)) { + } else if ((c->version() == Version::DC_V2) || + (c->version() == Version::GC_NTE) || + (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) || @@ -1539,6 +1542,9 @@ static void server_command_loadchar(shared_ptr c, const std::string& arg 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_NTE) { + auto gc_char = make_shared(c->character()->to_gc_nte()); + send_set_extended_player_info.operator()(c, gc_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); diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index ce7ed984..84db8a33 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -3911,6 +3911,11 @@ struct G_SendGuildCard_PC_6x06 { GuildCardPC guild_card; } __packed_ws__(G_SendGuildCard_PC_6x06, 0xF4); +struct G_SendGuildCard_GCNTE_6x06 { + G_UnusedHeader header; + GuildCardGCNTE guild_card; +} __packed_ws__(G_SendGuildCard_GCNTE_6x06, 0xA8); + struct G_SendGuildCard_GC_6x06 { G_UnusedHeader header; GuildCardGC guild_card; diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index 34ab6e93..4b4d5554 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -359,24 +359,41 @@ struct GuildCardPC { operator GuildCardBB() const; } __packed_ws__(GuildCardPC, 0xF0); -template +// 0000 | 62 00 AC 00 06 2A 00 00 00 00 01 00 90 96 66 8C | b * f +// 0010 | 31 31 31 31 00 00 00 00 00 00 00 00 00 00 00 00 | 1111 +// 0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +// 0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +// 0040 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +// 0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +// 0060 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +// 0070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +// 0080 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +// 0090 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +// 00A0 | 00 00 00 00 00 00 00 00 01 00 06 00 | + +template struct GuildCardGCT { using U32T = typename std::conditional::type; - /* 00 */ U32T player_tag = 0x00010000; - /* 04 */ U32T guild_card_number = 0; - /* 08 */ pstring name; - /* 20 */ pstring description; - /* 8C */ uint8_t present = 0; - /* 8D */ uint8_t language = 0; - /* 8E */ uint8_t section_id = 0; - /* 8F */ uint8_t char_class = 0; - /* 90 */ + /* NTE:Final */ + /* 00:00 */ U32T player_tag = 0x00010000; + /* 04:04 */ U32T guild_card_number = 0; + /* 08:08 */ pstring name; + /* 20:20 */ pstring description; + /* 8C:8C */ uint8_t present = 0; + /* 8D:8D */ uint8_t language = 0; + /* 8E:8E */ uint8_t section_id = 0; + /* 8F:8F */ uint8_t char_class = 0; + /* 90:90 */ operator GuildCardBB() const; } __packed__; -using GuildCardGC = GuildCardGCT; -using GuildCardGCBE = GuildCardGCT; +using GuildCardGCNTE = GuildCardGCT; +using GuildCardGCNTEBE = GuildCardGCT; +using GuildCardGC = GuildCardGCT; +using GuildCardGCBE = GuildCardGCT; +check_struct_size(GuildCardGCNTE, 0xA4); +check_struct_size(GuildCardGCNTEBE, 0xA4); check_struct_size(GuildCardGC, 0x90); check_struct_size(GuildCardGCBE, 0x90); @@ -412,9 +429,9 @@ struct GuildCardBB { operator GuildCardDCNTE() const; operator GuildCardDC() const; operator GuildCardPC() const; - template - operator GuildCardGCT() const { - GuildCardGCT ret; + template + operator GuildCardGCT() const { + GuildCardGCT ret; ret.player_tag = 0x00010000; ret.guild_card_number = this->guild_card_number.load(); ret.name.encode(this->name.decode(this->language), this->language); @@ -428,8 +445,8 @@ struct GuildCardBB { operator GuildCardXB() const; } __packed_ws__(GuildCardBB, 0x108); -template -GuildCardGCT::operator GuildCardBB() const { +template +GuildCardGCT::operator GuildCardBB() const { GuildCardBB ret; ret.guild_card_number = this->guild_card_number.load(); ret.name.encode(this->name.decode(this->language), this->language); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 99bced1a..310d3b07 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -3291,6 +3291,9 @@ static void on_30(shared_ptr c, uint16_t, uint32_t, string& data) { case Version::DC_V2: bb_char = PSOBBCharacterFile::create_from_dc_v2(check_size_t(data)); break; + case Version::GC_NTE: + bb_char = PSOBBCharacterFile::create_from_gc_nte(check_size_t(data)); + break; case Version::GC_V3: bb_char = PSOBBCharacterFile::create_from_gc(check_size_t(data)); break; @@ -3302,7 +3305,6 @@ static void on_30(shared_ptr c, uint16_t, uint32_t, string& data) { case Version::DC_V1: case Version::PC_NTE: case Version::PC_V2: - case Version::GC_NTE: case Version::GC_EP3_NTE: case Version::GC_EP3: case Version::BB_V4: diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index a06179e7..1a379ebe 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -517,6 +517,41 @@ shared_ptr PSOBBCharacterFile::create_from_dc_v2(const PSODC return ret; } +shared_ptr PSOBBCharacterFile::create_from_gc_nte(const PSOGCNTECharacterFileCharacter& gc_nte) { + auto ret = make_shared(); + ret->inventory = gc_nte.inventory; + // Note: We intentionally do not call ret->inventory.decode_from_client here. + // This is because the GC client byteswaps data2 in each item before sending + // it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does + // not do this, so the data2 fields are already in the correct order here. + uint8_t language = ret->inventory.language; + ret->disp = gc_nte.disp.to_bb(language, language); + ret->creation_timestamp = gc_nte.creation_timestamp.load(); + ret->play_time_seconds = gc_nte.play_time_seconds.load(); + ret->option_flags = gc_nte.option_flags.load(); + ret->save_count = gc_nte.save_count.load(); + ret->quest_flags = gc_nte.quest_flags; + ret->bank = gc_nte.bank; + ret->guild_card = gc_nte.guild_card; + for (size_t z = 0; z < std::min(ret->symbol_chats.size(), gc_nte.symbol_chats.size()); z++) { + auto& ret_sc = ret->symbol_chats[z]; + const auto& gc_sc = gc_nte.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(), gc_nte.shortcuts.size()); z++) { + ret->shortcuts[z] = gc_nte.shortcuts[z].convert(language); + } + ret->battle_records = gc_nte.battle_records; + ret->unknown_a4 = gc_nte.unknown_a4; + ret->challenge_records = gc_nte.challenge_records; + for (size_t z = 0; z < std::min(ret->tech_menu_shortcut_entries.size(), gc_nte.tech_menu_shortcut_entries.size()); z++) { + ret->tech_menu_shortcut_entries[z] = gc_nte.tech_menu_shortcut_entries[z].load(); + } + return ret; +} + shared_ptr PSOBBCharacterFile::create_from_gc(const PSOGCCharacterFile::Character& gc) { auto ret = make_shared(); ret->inventory = gc.inventory; @@ -643,6 +678,43 @@ PSODCV2CharacterFile PSOBBCharacterFile::to_dc_v2() const { return ret; } +PSOGCNTECharacterFileCharacter PSOBBCharacterFile::to_gc_nte() const { + uint8_t language = this->inventory.language; + + PSOGCNTECharacterFileCharacter ret; + ret.inventory = this->inventory; + // Note: We intentionally do not call ret.inventory.encode_for_client here. + // This is because the GC client byteswaps data2 in each item before sending + // it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does + // not do this, so the data2 fields are already in the correct order here. + 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(); + 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.unknown_a4 = this->unknown_a4; + 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(); + } + return ret; +} + PSOGCCharacterFile::Character PSOBBCharacterFile::to_gc() const { uint8_t language = this->inventory.language; diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 20f3c657..ed4befb0 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -412,6 +412,33 @@ struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD /* ECE40 */ } __packed_ws__(PSOPCCharacterFile, 0xECE40); +struct PSOGCNTECharacterFileCharacter { + /* 0000:---- */ PlayerInventoryBE inventory; + /* 034C:---- */ PlayerDispDataDCPCV3BE disp; + /* 041C:0000 */ be_uint32_t flags = 0; + /* 0420:0004 */ be_uint32_t creation_timestamp = 0; + /* 0424:0008 */ be_uint32_t signature = 0xA205B064; + /* 0428:000C */ be_uint32_t play_time_seconds = 0; + /* 042C:0010 */ be_uint32_t option_flags = 0x00040058; + /* 0430:0014 */ be_uint32_t save_count = 1; + /* 0434:0018 */ pstring ppp_username; + /* 0450:0034 */ pstring ppp_password; + /* 0460:0044 */ QuestFlags quest_flags; + /* 0660:0244 */ PlayerBank200BE bank; + /* 1928:150C */ GuildCardGCNTEBE guild_card; + /* 19CC:15B0 */ parray symbol_chats; + /* 1DEC:19D0 */ parray shortcuts; + /* 247C:2060 */ PlayerRecordsBattleBE battle_records; + /* 2494:2078 */ parray unknown_a4; + /* 2498:207C */ PlayerRecordsChallengeDC challenge_records; + /* 2538:211C */ parray tech_menu_shortcut_entries; + // TODO: choice_search_config and offline_battle_records may be in here + // somewhere. When they are found, don't forget to update the conversion + // functions in PSOBBCharacterFile. + /* 2560:2144 */ parray unknown_n2; + /* 2690:2274 */ +} __packed_ws__(PSOGCNTECharacterFileCharacter, 0x2690); + struct PSOGCCharacterFile { /* 00000 */ be_uint32_t checksum = 0; struct Character { @@ -646,10 +673,12 @@ struct PSOBBCharacterFile { const PlayerDispDataBBPreview& preview, std::shared_ptr level_table); static std::shared_ptr create_from_dc_v2(const PSODCV2CharacterFile& dc); + static std::shared_ptr create_from_gc_nte(const PSOGCNTECharacterFileCharacter& gc_nte); 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; + PSOGCNTECharacterFileCharacter to_gc_nte() const; PSOGCCharacterFile::Character to_gc() const; PSOXBCharacterFileCharacter to_xb(uint64_t xb_user_id) const; diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 3e8330bf..6f49af93 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -347,8 +347,7 @@ void prepare_client_for_patches(shared_ptr c, function on_comple } else if (c->version() == Version::XB_V3) { version_detect_name = "VersionDetectXB"; } - if (version_detect_name && - c->config.specific_version == default_specific_version_for_version(c->version(), -1)) { + if (version_detect_name && specific_version_is_indeterminate(c->config.specific_version)) { send_function_call(c, s->function_code_index->name_to_function.at(version_detect_name)); c->function_call_response_queue.emplace_back([wc = weak_ptr(c), on_complete](uint32_t specific_version, uint32_t) -> void { auto c = wc.lock(); @@ -2379,11 +2378,30 @@ void send_self_leave_notification(shared_ptr c) { } void send_get_player_info(shared_ptr c, bool request_extended) { - // TODO: Support extended player info on Ep3 JP and NTE + // TODO: Support extended player info on Ep3 (JP and NTE) + switch (c->version()) { + case Version::DC_NTE: + case Version::DC_V1_11_2000_PROTOTYPE: + case Version::DC_V1: + case Version::PC_NTE: + case Version::PC_V2: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + case Version::BB_V4: + request_extended = false; + break; + case Version::DC_V2: + case Version::GC_NTE: + case Version::GC_V3: + case Version::XB_V3: + break; + default: + throw logic_error("invalid version"); + } + if (request_extended && !c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL) && - !c->config.check_flag(Client::Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY) && - ((c->version() == Version::DC_V2) || (c->version() == Version::GC_V3) || (c->version() == Version::XB_V3))) { + !c->config.check_flag(Client::Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY)) { auto s = c->require_server_state(); prepare_client_for_patches(c, [wc = weak_ptr(c)]() { auto c = wc.lock();