implement choice search
This commit is contained in:
@@ -47,6 +47,7 @@ set(SOURCES
|
||||
src/CatSession.cc
|
||||
src/Channel.cc
|
||||
src/ChatCommands.cc
|
||||
src/ChoiceSearch.cc
|
||||
src/Client.cc
|
||||
src/CommonItemSet.cc
|
||||
src/Compression.cc
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
- Find a way to silence audio in RunDOL.s
|
||||
- Encapsulate BB server-side random state and make replays deterministic
|
||||
- Implement choice search
|
||||
- Write a simple status API
|
||||
- Implement per-game logging
|
||||
- Build an exception-handling abstraction in ChatCommands that shows formatted error messages in all cases
|
||||
|
||||
+3
-5
@@ -112,7 +112,7 @@ static void server_command_lobby_info(shared_ptr<Client> c, const std::string&)
|
||||
} else {
|
||||
lines.emplace_back(string_printf("$C6%08X$C7 L$C6%d-%d$C7", l->lobby_id, l->min_level + 1, l->max_level + 1));
|
||||
}
|
||||
lines.emplace_back(string_printf("$C7Section ID: $C6%s$C7", name_for_section_id(l->section_id).c_str()));
|
||||
lines.emplace_back(string_printf("$C7Section ID: $C6%s$C7", name_for_section_id(l->section_id)));
|
||||
|
||||
if (l->check_flag(Lobby::Flag::DROPS_ENABLED)) {
|
||||
if (l->item_creator) {
|
||||
@@ -751,8 +751,7 @@ static void server_command_secid(shared_ptr<Client> c, const std::string& args)
|
||||
send_text_message(c, "$C6Invalid section ID");
|
||||
} else {
|
||||
c->config.override_section_id = new_secid;
|
||||
string name = name_for_section_id(new_secid);
|
||||
send_text_message_printf(c, "$C6Override section ID\nset to %s", name.c_str());
|
||||
send_text_message_printf(c, "$C6Override section ID\nset to %s", name_for_section_id(new_secid));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -768,8 +767,7 @@ static void proxy_command_secid(shared_ptr<ProxyServer::LinkedSession> ses, cons
|
||||
send_text_message(ses->client_channel, "$C6Invalid section ID");
|
||||
} else {
|
||||
ses->config.override_section_id = new_secid;
|
||||
string name = name_for_section_id(new_secid);
|
||||
send_text_message(ses->client_channel, "$C6Override section ID\nset to " + name);
|
||||
send_text_message_printf(ses->client_channel, "$C6Override section ID\nset to %s", name_for_section_id(new_secid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "ChoiceSearch.hh"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "Client.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
const vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
|
||||
ChoiceSearchCategory{
|
||||
.id = 0x0001,
|
||||
.name = "Level",
|
||||
.choices = {
|
||||
{0x0000, "Any"},
|
||||
{0x0001, "Own level +/- 5"},
|
||||
{0x0002, "Level 1-10"},
|
||||
{0x0003, "Level 11-20"},
|
||||
{0x0004, "Level 21-40"},
|
||||
{0x0005, "Level 41-60"},
|
||||
{0x0006, "Level 61-80"},
|
||||
{0x0007, "Level 81-100"},
|
||||
{0x0008, "Level 101-120"},
|
||||
{0x0009, "Level 121-160"},
|
||||
{0x000A, "Level 161-200"},
|
||||
},
|
||||
.client_matches = +[](shared_ptr<Client> searcher_c, shared_ptr<Client> target_c, uint16_t choice_id) -> bool {
|
||||
if (choice_id == 0x0000) {
|
||||
return true;
|
||||
}
|
||||
uint32_t target_level = target_c->game_data.character()->disp.stats.level + 1;
|
||||
switch (choice_id) {
|
||||
case 0x0001:
|
||||
return (labs(static_cast<int32_t>(target_level - searcher_c->game_data.character()->disp.stats.level)) <= 5);
|
||||
case 0x0002:
|
||||
return (target_level <= 10);
|
||||
case 0x0003:
|
||||
return (target_level > 10) && (target_level <= 20);
|
||||
case 0x0004:
|
||||
return (target_level > 20) && (target_level <= 40);
|
||||
case 0x0005:
|
||||
return (target_level > 40) && (target_level <= 60);
|
||||
case 0x0006:
|
||||
return (target_level > 60) && (target_level <= 80);
|
||||
case 0x0007:
|
||||
return (target_level > 80) && (target_level <= 100);
|
||||
case 0x0008:
|
||||
return (target_level > 100) && (target_level <= 120);
|
||||
case 0x0009:
|
||||
return (target_level > 120) && (target_level <= 160);
|
||||
case 0x000A:
|
||||
return (target_level > 160) && (target_level <= 200);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
ChoiceSearchCategory{
|
||||
.id = 0x0002,
|
||||
.name = "Class",
|
||||
.choices = {
|
||||
{0x0000, "Any"},
|
||||
{0x0010, "Hunter"},
|
||||
{0x0001, "HUmar"},
|
||||
{0x0002, "HUnewearl"},
|
||||
{0x0003, "HUcast"},
|
||||
{0x000A, "HUcaseal"},
|
||||
{0x0011, "Ranger"},
|
||||
{0x0004, "RAmar"},
|
||||
{0x000C, "RAmarl"},
|
||||
{0x0005, "RAcast"},
|
||||
{0x0006, "RAcaseal"},
|
||||
{0x0012, "Force"},
|
||||
{0x000B, "FOmar"},
|
||||
{0x0007, "FOmarl"},
|
||||
{0x0008, "FOnewm"},
|
||||
{0x0009, "FOnewearl"},
|
||||
},
|
||||
.client_matches = +[](shared_ptr<Client>, shared_ptr<Client> target_c, uint16_t choice_id) -> bool {
|
||||
switch (choice_id) {
|
||||
case 0x0000:
|
||||
return true;
|
||||
case 0x0010:
|
||||
return target_c->game_data.character()->disp.visual.class_flags & 0x20;
|
||||
case 0x0011:
|
||||
return target_c->game_data.character()->disp.visual.class_flags & 0x40;
|
||||
case 0x0012:
|
||||
return target_c->game_data.character()->disp.visual.class_flags & 0x80;
|
||||
default:
|
||||
return ((choice_id - 1) == target_c->game_data.character()->disp.visual.char_class);
|
||||
}
|
||||
},
|
||||
},
|
||||
ChoiceSearchCategory{
|
||||
.id = 0x0003,
|
||||
.name = "Platform",
|
||||
.choices = {
|
||||
{0x0000, "Any"},
|
||||
{0x0001, "DC betas"},
|
||||
{0x0002, "DC V1"},
|
||||
{0x0003, "DC V2 / PC"},
|
||||
{0x0004, "GC / Xbox Episodes 1&2"},
|
||||
{0x0005, "GC Episode 3"},
|
||||
{0x0006, "BB"},
|
||||
},
|
||||
.client_matches = +[](shared_ptr<Client>, shared_ptr<Client> target_c, uint16_t choice_id) -> bool {
|
||||
if (choice_id == 0x0000) {
|
||||
return true;
|
||||
}
|
||||
switch (target_c->version()) {
|
||||
case Version::DC_NTE:
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
return (choice_id == 0x0001);
|
||||
case Version::DC_V1:
|
||||
return (choice_id == 0x0002);
|
||||
case Version::DC_V2:
|
||||
case Version::PC_V2:
|
||||
return (choice_id == 0x0003);
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::XB_V3:
|
||||
return (choice_id == 0x0004);
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3:
|
||||
return (choice_id == 0x0005);
|
||||
case Version::BB_V4:
|
||||
return (choice_id == 0x0006);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
ChoiceSearchCategory{
|
||||
.id = 0x0204,
|
||||
.name = "Game mode",
|
||||
.choices = {
|
||||
{0x0000, "Any"},
|
||||
{0x0001, "Normal"},
|
||||
{0x0002, "Hard"},
|
||||
{0x0003, "Very Hard"},
|
||||
{0x0004, "Ultimate"},
|
||||
{0x0005, "Battle"},
|
||||
{0x0006, "Challenge"},
|
||||
},
|
||||
.client_matches = +[](shared_ptr<Client>, shared_ptr<Client> target_c, uint16_t choice_id) -> bool {
|
||||
uint16_t target_choice_id = target_c->game_data.character()->choice_search_config.get_setting(0x0204);
|
||||
return (choice_id == 0) || (target_choice_id == 0) || (choice_id == target_choice_id);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Text.hh"
|
||||
|
||||
struct Client;
|
||||
|
||||
struct ChoiceSearchConfig {
|
||||
le_uint32_t disabled = 1; // 0 = enabled, 1 = disabled. Unused in command C3
|
||||
struct Entry {
|
||||
le_uint16_t parent_choice_id = 0;
|
||||
le_uint16_t choice_id = 0;
|
||||
} __attribute__((packed));
|
||||
parray<Entry, 5> entries;
|
||||
|
||||
int32_t get_setting(uint16_t parent_choice_id) const {
|
||||
for (size_t z = 0; z < this->entries.size(); z++) {
|
||||
if (this->entries[z].parent_choice_id == parent_choice_id) {
|
||||
return this->entries[z].choice_id;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ChoiceSearchCategory {
|
||||
struct Choice {
|
||||
uint16_t id;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
uint16_t id;
|
||||
const char* name;
|
||||
std::vector<Choice> choices;
|
||||
std::function<bool(std::shared_ptr<Client> searcher_c, std::shared_ptr<Client> target_c, uint16_t choice_id)> client_matches;
|
||||
};
|
||||
|
||||
extern const std::vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES;
|
||||
+30
-33
@@ -605,9 +605,10 @@ struct SC_MeetUserExtension {
|
||||
le_uint32_t menu_id = 0;
|
||||
le_uint32_t item_id = 0;
|
||||
} __packed__;
|
||||
parray<LobbyReference, 8> lobby_refs;
|
||||
le_uint32_t unknown_a2 = 0;
|
||||
pstring<Encoding, 0x20> player_name;
|
||||
/* 00 */ parray<LobbyReference, 8> lobby_refs;
|
||||
/* 40 */ le_uint32_t unknown_a2 = 0;
|
||||
/* 44 */ pstring<Encoding, 0x20> player_name;
|
||||
/* 64 (or 84 on UTF16 versions) */
|
||||
} __packed__;
|
||||
|
||||
struct S_LegacyJoinGame_PC_0E {
|
||||
@@ -1103,7 +1104,7 @@ struct C_CharacterData_DCv2_61_98 {
|
||||
/* 0000 */ PlayerInventory inventory;
|
||||
/* 034C */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C */ PlayerRecordsEntry_DC records;
|
||||
/* 04D8 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
|
||||
/* 04D8 */ ChoiceSearchConfig choice_search_config;
|
||||
/* 04F0 */
|
||||
} __attribute__((packed));
|
||||
|
||||
@@ -1111,7 +1112,7 @@ struct C_CharacterData_PC_61_98 {
|
||||
/* 0000 */ PlayerInventory inventory;
|
||||
/* 034C */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C */ PlayerRecordsEntry_PC records;
|
||||
/* 0510 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
|
||||
/* 0510 */ ChoiceSearchConfig choice_search_config;
|
||||
/* 0528 */ parray<le_uint32_t, 0x1E> blocked_senders;
|
||||
/* 05A0 */ le_uint32_t auto_reply_enabled = 0;
|
||||
// The auto-reply message can be up to 0x200 characters. If it's shorter than
|
||||
@@ -1124,7 +1125,7 @@ struct C_CharacterData_V3_61_98 {
|
||||
/* 0000 */ PlayerInventory inventory;
|
||||
/* 034C */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C */ PlayerRecordsEntry_V3 records;
|
||||
/* 0538 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
|
||||
/* 0538 */ ChoiceSearchConfig choice_search_config;
|
||||
/* 0550 */ pstring<TextEncoding::MARKED, 0xAC> info_board;
|
||||
/* 05FC */ parray<le_uint32_t, 0x1E> blocked_senders;
|
||||
/* 0674 */ le_uint32_t auto_reply_enabled = 0;
|
||||
@@ -1138,7 +1139,7 @@ struct C_CharacterData_GC_Ep3_61_98 {
|
||||
/* 0000 */ PlayerInventory inventory;
|
||||
/* 034C */ PlayerDispDataDCPCV3 disp;
|
||||
/* 041C */ PlayerRecordsEntry_V3 records;
|
||||
/* 0538 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
|
||||
/* 0538 */ ChoiceSearchConfig choice_search_config;
|
||||
/* 0550 */ pstring<TextEncoding::MARKED, 0xAC> info_board;
|
||||
/* 05FC */ parray<le_uint32_t, 0x1E> blocked_senders;
|
||||
/* 0674 */ le_uint32_t auto_reply_enabled = 0;
|
||||
@@ -1151,7 +1152,7 @@ struct C_CharacterData_BB_61_98 {
|
||||
/* 0000 */ PlayerInventory inventory;
|
||||
/* 034C */ PlayerDispDataBB disp;
|
||||
/* 04DC */ PlayerRecordsEntry_BB records;
|
||||
/* 0638 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
|
||||
/* 0638 */ ChoiceSearchConfig choice_search_config;
|
||||
/* 0650 */ pstring<TextEncoding::UTF16, 0xAC> info_board;
|
||||
/* 07A8 */ parray<le_uint32_t, 0x1E> blocked_senders;
|
||||
/* 0820 */ le_uint32_t auto_reply_enabled = 0;
|
||||
@@ -2359,19 +2360,17 @@ struct S_TournamentMatchInformation_GC_Ep3_BB {
|
||||
// Internal name: RcvChoiceList
|
||||
|
||||
// Command is a list of these; header.flag is the entry count (incl. top-level).
|
||||
template <typename ItemIDT, TextEncoding Encoding>
|
||||
template <TextEncoding Encoding>
|
||||
struct S_ChoiceSearchEntry {
|
||||
// Category IDs are nonzero; if the high byte of the ID is nonzero then the
|
||||
// category can be set by the user at any time; otherwise it can't.
|
||||
ItemIDT parent_category_id = 0; // 0 for top-level categories
|
||||
ItemIDT category_id = 0;
|
||||
le_uint16_t parent_choice_id = 0; // 0 for top-level categories
|
||||
le_uint16_t choice_id = 0;
|
||||
pstring<Encoding, 0x1C> text;
|
||||
} __packed__;
|
||||
struct S_ChoiceSearchEntry_DC_C0 : S_ChoiceSearchEntry<le_uint32_t, TextEncoding::MARKED> {
|
||||
struct S_ChoiceSearchEntry_DC_V3_C0 : S_ChoiceSearchEntry<TextEncoding::MARKED> {
|
||||
} __packed__;
|
||||
struct S_ChoiceSearchEntry_V3_C0 : S_ChoiceSearchEntry<le_uint16_t, TextEncoding::MARKED> {
|
||||
} __packed__;
|
||||
struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry<le_uint16_t, TextEncoding::UTF16> {
|
||||
struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry<TextEncoding::UTF16> {
|
||||
} __packed__;
|
||||
|
||||
// Top-level categories are things like "Level", "Class", etc.
|
||||
@@ -2425,12 +2424,7 @@ struct C_CreateGame_BB_C1 : C_CreateGame<TextEncoding::UTF16> {
|
||||
// C2 (C->S): Set choice search parameters (DCv2 and later versions)
|
||||
// Internal name: PutChoiceList
|
||||
// Server does not respond.
|
||||
// The ChoiceSearchConfig structure is defined in PlayerSubordinates.hh.
|
||||
|
||||
struct C_ChoiceSearchSelections_DC_C2_C3 : ChoiceSearchConfig<le_uint32_t> {
|
||||
} __packed__;
|
||||
struct C_ChoiceSearchSelections_PC_V3_BB_C2_C3 : ChoiceSearchConfig<le_uint16_t> {
|
||||
} __packed__;
|
||||
// Contents is a ChoiceSearchConfig, which is defined in PlayerSubordinates.hh.
|
||||
|
||||
// C3 (C->S): Execute choice search (DCv2 and later versions)
|
||||
// Internal name: SndChoiceSeq
|
||||
@@ -2441,22 +2435,25 @@ struct C_ChoiceSearchSelections_PC_V3_BB_C2_C3 : ChoiceSearchConfig<le_uint16_t>
|
||||
// Internal name: RcvChoiceAns
|
||||
|
||||
// Command is a list of these; header.flag is the entry count
|
||||
struct S_ChoiceSearchResultEntry_V3_C4 {
|
||||
template <typename HeaderT, TextEncoding NameEncoding, TextEncoding DescEncoding, TextEncoding LocatorEncoding>
|
||||
struct S_ChoiceSearchResultEntry_C4 {
|
||||
le_uint32_t guild_card_number = 0;
|
||||
pstring<TextEncoding::ASCII, 0x10> name; // No language marker, as usual on V3
|
||||
pstring<TextEncoding::MARKED, 0x20> info_string; // Usually something like "<class> Lvl <level>"
|
||||
pstring<NameEncoding, 0x10> name;
|
||||
pstring<DescEncoding, 0x20> info_string; // Usually something like "<class> Lvl <level>"
|
||||
// Format is stricter here; this is "LOBBYNAME,BLOCKNUM,SHIPNAME"
|
||||
// If target is in game, for example, "Game Name,BLOCK01,Alexandria"
|
||||
// If target is in lobby, for example, "BLOCK01-1,BLOCK01,Alexandria"
|
||||
pstring<TextEncoding::MARKED, 0x34> locator_string;
|
||||
// Server IP and port for "meet user" option
|
||||
le_uint32_t server_ip = 0;
|
||||
le_uint16_t server_port = 0;
|
||||
le_uint16_t unused1 = 0;
|
||||
le_uint32_t menu_id = 0;
|
||||
le_uint32_t lobby_id = 0; // These two are guesses
|
||||
le_uint32_t game_id = 0; // Zero if target is in a lobby rather than a game
|
||||
parray<uint8_t, 0x58> unused2;
|
||||
pstring<LocatorEncoding, 0x30> location_string;
|
||||
HeaderT reconnect_command_header; // Ignored by the client
|
||||
S_Reconnect_19 reconnect_command;
|
||||
SC_MeetUserExtension<NameEncoding> meet_user;
|
||||
} __packed__;
|
||||
|
||||
struct S_ChoiceSearchResultEntry_DC_V3_C4 : S_ChoiceSearchResultEntry_C4<PSOCommandHeaderDCV3, TextEncoding::ASCII, TextEncoding::MARKED, TextEncoding::ASCII> {
|
||||
} __packed__;
|
||||
struct S_ChoiceSearchResultEntry_PC_C4 : S_ChoiceSearchResultEntry_C4<PSOCommandHeaderPC, TextEncoding::UTF16, TextEncoding::UTF16, TextEncoding::UTF16> {
|
||||
} __packed__;
|
||||
struct S_ChoiceSearchResultEntry_BB_C4 : S_ChoiceSearchResultEntry_C4<PSOCommandHeaderBB, TextEncoding::UTF16, TextEncoding::UTF16, TextEncoding::UTF16> {
|
||||
} __packed__;
|
||||
|
||||
// C5 (S->C): Player records update (DCv2 and later versions)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ChoiceSearch.hh"
|
||||
#include "FileContentsCache.hh"
|
||||
#include "ItemData.hh"
|
||||
#include "LevelTable.hh"
|
||||
@@ -453,17 +454,6 @@ struct PlayerRecords_Battle {
|
||||
/* 18 */
|
||||
} __attribute__((packed));
|
||||
|
||||
template <typename ItemIDT>
|
||||
struct ChoiceSearchConfig {
|
||||
// 0 = enabled, 1 = disabled. Unused for command C3
|
||||
le_uint32_t disabled = 1;
|
||||
struct Entry {
|
||||
ItemIDT parent_category_id = 0;
|
||||
ItemIDT category_id = 0;
|
||||
} __attribute__((packed));
|
||||
parray<Entry, 5> entries;
|
||||
} __attribute__((packed));
|
||||
|
||||
template <typename DestT, typename SrcT = DestT>
|
||||
DestT convert_player_disp_data(const SrcT&, uint8_t, uint8_t) {
|
||||
static_assert(always_false<DestT, SrcT>::v,
|
||||
|
||||
@@ -787,7 +787,9 @@ static HandlerResult S_C4(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t,
|
||||
return modified ? HandlerResult::Type::MODIFIED : HandlerResult::Type::FORWARD;
|
||||
}
|
||||
|
||||
constexpr on_command_t S_V3_C4 = &S_C4<S_ChoiceSearchResultEntry_V3_C4>;
|
||||
constexpr on_command_t S_DGX_C4 = &S_C4<S_ChoiceSearchResultEntry_DC_V3_C4>;
|
||||
constexpr on_command_t S_P_C4 = &S_C4<S_ChoiceSearchResultEntry_PC_C4>;
|
||||
constexpr on_command_t S_B_C4 = &S_C4<S_ChoiceSearchResultEntry_BB_C4>;
|
||||
|
||||
static HandlerResult S_G_E4(shared_ptr<ProxyServer::LinkedSession> ses, uint16_t, uint32_t, string& data) {
|
||||
auto& cmd = check_size_t<S_CardBattleTableState_GC_Ep3_E4>(data);
|
||||
@@ -1962,8 +1964,8 @@ static on_command_t handlers[0x100][13][2] = {
|
||||
/* C1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* C2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* C3 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* C4 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_V3_C4, nullptr}, {S_V3_C4, nullptr}, {S_V3_C4, nullptr}, {S_V3_C4, nullptr}, {nullptr, nullptr}},
|
||||
/* C5 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
/* C4 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_DGX_C4, nullptr}, {S_P_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_DGX_C4, nullptr}, {S_B_C4, nullptr}},
|
||||
/* C5 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}},
|
||||
/* C6 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* C7 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
/* C8 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, 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}},
|
||||
|
||||
+1
-2
@@ -1412,8 +1412,7 @@ std::string disassemble_quest_script(const void* data, size_t size, Version vers
|
||||
string unused = format_data_string(visual.unused.data(), visual.unused.bytes());
|
||||
lines.emplace_back(string_printf(" %04zX unused %s", l->offset + offsetof(PlayerVisualConfig, unused), unused.c_str()));
|
||||
lines.emplace_back(string_printf(" %04zX name_color_checksum %08" PRIX32, l->offset + offsetof(PlayerVisualConfig, name_color_checksum), visual.name_color_checksum.load()));
|
||||
string secid_name = name_for_section_id(visual.section_id);
|
||||
lines.emplace_back(string_printf(" %04zX section_id %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, section_id), visual.section_id, secid_name.c_str()));
|
||||
lines.emplace_back(string_printf(" %04zX section_id %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, section_id), visual.section_id, name_for_section_id(visual.section_id)));
|
||||
lines.emplace_back(string_printf(" %04zX char_class %02hhX (%s)", l->offset + offsetof(PlayerVisualConfig, char_class), visual.char_class, name_for_char_class(visual.char_class)));
|
||||
lines.emplace_back(string_printf(" %04zX validation_flags %02hhX", l->offset + offsetof(PlayerVisualConfig, validation_flags), visual.validation_flags));
|
||||
lines.emplace_back(string_printf(" %04zX version %02hhX", l->offset + offsetof(PlayerVisualConfig, version), visual.version));
|
||||
|
||||
+1
-2
@@ -518,12 +518,11 @@ void RareItemSet::print_collection(
|
||||
return;
|
||||
}
|
||||
|
||||
string secid_name = name_for_section_id(section_id);
|
||||
fprintf(stream, "%s %s %s %s\n",
|
||||
name_for_mode(mode),
|
||||
name_for_episode(episode),
|
||||
name_for_difficulty(difficulty),
|
||||
secid_name.c_str());
|
||||
name_for_section_id(section_id));
|
||||
|
||||
fprintf(stream, " Monster rares:\n");
|
||||
for (size_t z = 0; z < collection->rt_index_to_specs.size(); z++) {
|
||||
|
||||
+97
-7
@@ -1720,12 +1720,11 @@ static void on_09(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
}
|
||||
}
|
||||
|
||||
string secid_str = name_for_section_id(game->section_id);
|
||||
info += string_printf("%s %c %s %s\n",
|
||||
abbreviation_for_episode(game->episode),
|
||||
abbreviation_for_difficulty(game->difficulty),
|
||||
abbreviation_for_mode(game->mode),
|
||||
secid_str.c_str());
|
||||
name_for_section_id(game->section_id));
|
||||
|
||||
bool cheats_enabled = game->check_flag(Lobby::Flag::CHEATS_ENABLED);
|
||||
bool locked = !game->password.empty();
|
||||
@@ -3490,8 +3489,99 @@ static void on_40(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
}
|
||||
|
||||
static void on_C0(shared_ptr<Client> c, uint16_t, uint32_t, string&) {
|
||||
// TODO: Implement choice search.
|
||||
send_text_message(c, "$C6Choice Search is\nnot supported");
|
||||
send_choice_search_choices(c);
|
||||
}
|
||||
|
||||
static void on_C2(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
c->game_data.character()->choice_search_config = check_size_t<ChoiceSearchConfig>(data);
|
||||
}
|
||||
|
||||
template <typename ResultT>
|
||||
static void on_choice_search_t(shared_ptr<Client> c, const ChoiceSearchConfig& cmd) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
vector<ResultT> results;
|
||||
for (const auto& l : s->all_lobbies()) {
|
||||
for (const auto& lc : l->clients) {
|
||||
if (!lc || lc->game_data.character()->choice_search_config.disabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool is_match = true;
|
||||
for (const auto& cat : CHOICE_SEARCH_CATEGORIES) {
|
||||
int32_t setting = cmd.get_setting(cat.id);
|
||||
if (setting == -1) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (!cat.client_matches(c, lc, setting)) {
|
||||
is_match = false;
|
||||
break;
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
c->log.info("Error in Choice Search matching for category %s: %s", cat.name, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
if (is_match) {
|
||||
auto lp = lc->game_data.character();
|
||||
auto& result = results.emplace_back();
|
||||
result.guild_card_number = lc->license->serial_number;
|
||||
result.name.encode(lp->disp.name.decode(lc->language()), c->language());
|
||||
string info_string = string_printf("%s Lv%zu %s",
|
||||
name_for_char_class(lp->disp.visual.char_class),
|
||||
static_cast<size_t>(lp->disp.stats.level + 1),
|
||||
name_for_section_id(lp->disp.visual.section_id));
|
||||
result.info_string.encode(info_string, c->language());
|
||||
string lobby_name = l->is_game() ? l->name : string_printf("BLOCK01-%02" PRIu32, l->lobby_id);
|
||||
string location_string;
|
||||
if (l->is_game()) {
|
||||
location_string = string_printf("%s,BLOCK01,%s", l->name.c_str(), s->name.c_str());
|
||||
} else if (l->is_ep3()) {
|
||||
location_string = string_printf("BLOCK01-C%02" PRIu32 ",BLOCK01,%s", l->lobby_id - 15, s->name.c_str());
|
||||
} else {
|
||||
location_string = string_printf("BLOCK01-%02" PRIu32 ",BLOCK01,%s", l->lobby_id, s->name.c_str());
|
||||
}
|
||||
result.location_string.encode(location_string, c->language());
|
||||
result.reconnect_command_header.command = 0x19;
|
||||
result.reconnect_command_header.flag = 0x00;
|
||||
result.reconnect_command_header.size = sizeof(result.reconnect_command) + sizeof(result.reconnect_command_header);
|
||||
result.reconnect_command.address = s->connect_address_for_client(c);
|
||||
result.reconnect_command.port = s->name_to_port_config.at(lobby_port_name_for_version(c->version()))->port;
|
||||
result.meet_user.lobby_refs[0].menu_id = MenuID::LOBBY;
|
||||
result.meet_user.lobby_refs[0].item_id = l->lobby_id;
|
||||
result.meet_user.player_name.encode(lp->disp.name.decode(lc->language()), c->language());
|
||||
if (results.size() >= 0x20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send_command_vt(c, 0xC4, results.size(), results);
|
||||
}
|
||||
|
||||
static void on_C3(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
const auto& cmd = check_size_t<ChoiceSearchConfig>(data);
|
||||
switch (c->version()) {
|
||||
// DC V1 and the prototypes do not support this command
|
||||
case Version::DC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3:
|
||||
on_choice_search_t<S_ChoiceSearchResultEntry_DC_V3_C4>(c, cmd);
|
||||
break;
|
||||
case Version::PC_V2:
|
||||
on_choice_search_t<S_ChoiceSearchResultEntry_PC_C4>(c, cmd);
|
||||
break;
|
||||
case Version::BB_V4:
|
||||
on_choice_search_t<S_ChoiceSearchResultEntry_BB_C4>(c, cmd);
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("unimplemented versioned command");
|
||||
}
|
||||
}
|
||||
|
||||
static void on_81(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
||||
@@ -5095,10 +5185,10 @@ static on_command_t handlers[0x100][13] = {
|
||||
/* BE */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
/* BF */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
// PC_PATCH BB_PATCH DC_NTE DC_PROTO DCV1 DCV2 PC GCNTE GC EP3TE EP3 XB BB
|
||||
/* C0 */ {nullptr, nullptr, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, nullptr},
|
||||
/* C0 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0, on_C0},
|
||||
/* C1 */ {nullptr, nullptr, on_0C_C1_E7_EC,on_0C_C1_E7_EC,on_0C_C1_E7_EC,on_0C_C1_E7_EC, on_C1_PC, on_0C_C1_E7_EC, on_0C_C1_E7_EC, on_0C_C1_E7_EC, on_0C_C1_E7_EC, on_0C_C1_E7_EC, on_C1_BB},
|
||||
/* C2 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
/* C3 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
/* C2 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_C2, on_C2, on_C2, on_C2, on_C2, on_C2, on_C2, on_C2},
|
||||
/* C3 */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_C3, on_C3, on_C3, on_C3, on_C3, on_C3, on_C3, on_C3},
|
||||
/* C4 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
/* C5 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
/* C6 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, on_C6, on_C6, on_C6, on_C6, on_C6, on_C6, on_C6},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "Client.hh"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <phosg/Strings.hh>
|
||||
#include <string>
|
||||
|
||||
#include "ChoiceSearch.hh"
|
||||
#include "Episode3/DataIndexes.hh"
|
||||
#include "ItemNameIndex.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
@@ -207,7 +208,7 @@ struct PSOBBCharacterFile {
|
||||
/* 2CB4 */ parray<uint8_t, 4> unknown_a4;
|
||||
/* 2CB8 */ PlayerRecordsBB_Challenge challenge_records;
|
||||
/* 2DF8 */ parray<le_uint16_t, 0x0014> tech_menu_config;
|
||||
/* 2E20 */ ChoiceSearchConfig<le_uint16_t> choice_search_config;
|
||||
/* 2E20 */ ChoiceSearchConfig choice_search_config;
|
||||
/* 2E38 */ parray<uint8_t, 0x0010> unknown_a6;
|
||||
/* 2E48 */ parray<le_uint32_t, 0x0010> quest_global_flags;
|
||||
/* 2E88 */ parray<uint8_t, 0x1C> unknown_a7;
|
||||
|
||||
+40
-1
@@ -987,6 +987,44 @@ void send_info_board(shared_ptr<Client> c) {
|
||||
}
|
||||
}
|
||||
|
||||
template <typename CmdT>
|
||||
void send_choice_search_choices_t(shared_ptr<Client> c) {
|
||||
vector<CmdT> entries;
|
||||
for (const auto& cat : CHOICE_SEARCH_CATEGORIES) {
|
||||
auto& cat_e = entries.emplace_back();
|
||||
cat_e.parent_choice_id = 0;
|
||||
cat_e.choice_id = cat.id;
|
||||
cat_e.text.encode(cat.name, c->language());
|
||||
for (const auto& choice : cat.choices) {
|
||||
auto& e = entries.emplace_back();
|
||||
e.parent_choice_id = cat.id;
|
||||
e.choice_id = choice.id;
|
||||
e.text.encode(choice.name, c->language());
|
||||
}
|
||||
}
|
||||
send_command_vt(c, 0xC0, entries.size(), entries);
|
||||
}
|
||||
|
||||
void send_choice_search_choices(shared_ptr<Client> c) {
|
||||
switch (c->version()) {
|
||||
// DC V1 and the prototypes do not support this command
|
||||
case Version::DC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3:
|
||||
send_choice_search_choices_t<S_ChoiceSearchEntry_DC_V3_C0>(c);
|
||||
break;
|
||||
case Version::PC_V2:
|
||||
case Version::BB_V4:
|
||||
send_choice_search_choices_t<S_ChoiceSearchEntry_PC_BB_C0>(c);
|
||||
break;
|
||||
default:
|
||||
throw logic_error("unimplemented versioned command");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename CommandHeaderT, TextEncoding Encoding>
|
||||
void send_card_search_result_t(
|
||||
shared_ptr<Client> c,
|
||||
@@ -2981,7 +3019,8 @@ void send_quest_file_chunk(
|
||||
}
|
||||
cmd.data_size = size;
|
||||
|
||||
send_command_t(c, is_download_quest ? 0xA7 : 0x13, chunk_index, cmd);
|
||||
c->log.info("Sending quest file chunk %s:%zu", filename.c_str(), chunk_index);
|
||||
c->channel.send(is_download_quest ? 0xA7 : 0x13, chunk_index, &cmd, sizeof(cmd), true);
|
||||
}
|
||||
|
||||
template <typename CommandT>
|
||||
|
||||
@@ -232,6 +232,8 @@ __attribute__((format(printf, 2, 3))) void send_ep3_text_message_printf(
|
||||
|
||||
void send_info_board(std::shared_ptr<Client> c);
|
||||
|
||||
void send_choice_search_choices(std::shared_ptr<Client> c);
|
||||
|
||||
void send_card_search_result(
|
||||
std::shared_ptr<Client> c,
|
||||
std::shared_ptr<Client> result,
|
||||
|
||||
+2
-2
@@ -656,11 +656,11 @@ Proxy session commands:\n\
|
||||
for (size_t z = 0; z < ses->lobby_players.size(); z++) {
|
||||
const auto& player = ses->lobby_players[z];
|
||||
if (player.guild_card_number) {
|
||||
auto secid_name = name_for_section_id(player.section_id);
|
||||
fprintf(stderr, " %zu: %" PRIu32 " => %s (%c, %s, %s)\n",
|
||||
z, player.guild_card_number, player.name.c_str(),
|
||||
char_for_language_code(player.language),
|
||||
name_for_char_class(player.char_class), secid_name.c_str());
|
||||
name_for_char_class(player.char_class),
|
||||
name_for_section_id(player.section_id));
|
||||
} else {
|
||||
fprintf(stderr, " %zu: (no player)\n", z);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ const char* abbreviation_for_mode(GameMode mode) {
|
||||
}
|
||||
}
|
||||
|
||||
const vector<string> section_id_to_name = {
|
||||
static const array<const char*, 10> section_id_to_name = {
|
||||
"Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum",
|
||||
"Pinkal", "Redria", "Oran", "Yellowboze", "Whitill"};
|
||||
|
||||
@@ -205,12 +205,11 @@ const vector<string> npc_id_to_name({"ninja", "rico", "sonic", "knuckles", "tail
|
||||
const unordered_map<string, uint8_t> name_to_npc_id = {
|
||||
{"ninja", 0}, {"rico", 1}, {"sonic", 2}, {"knuckles", 3}, {"tails", 4}, {"flowen", 5}, {"elly", 6}};
|
||||
|
||||
const string& name_for_section_id(uint8_t section_id) {
|
||||
const char* name_for_section_id(uint8_t section_id) {
|
||||
if (section_id < section_id_to_name.size()) {
|
||||
return section_id_to_name[section_id];
|
||||
} else {
|
||||
static const string ret = "<Unknown section id>";
|
||||
return ret;
|
||||
return "<Unknown>";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ extern const std::unordered_map<std::string, uint8_t> name_to_tech_id;
|
||||
const std::string& name_for_technique(uint8_t tech);
|
||||
uint8_t technique_for_name(const std::string& name);
|
||||
|
||||
const std::string& name_for_section_id(uint8_t section_id);
|
||||
const char* name_for_section_id(uint8_t section_id);
|
||||
uint8_t section_id_for_name(const std::string& name);
|
||||
|
||||
const std::string& name_for_event(uint8_t event);
|
||||
|
||||
Reference in New Issue
Block a user