implement choice search

This commit is contained in:
Martin Michelsen
2023-11-28 18:37:24 -08:00
parent 4008d7f4ff
commit 556360c993
20 changed files with 387 additions and 73 deletions
+1
View File
@@ -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
-1
View File
@@ -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
View File
@@ -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));
}
}
}
+149
View File
@@ -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);
},
},
});
+43
View File
@@ -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
View File
@@ -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)
+2
View File
@@ -1,3 +1,5 @@
#pragma once
#include <netinet/in.h>
#include <stdint.h>
+1 -11
View File
@@ -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,
+5 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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},
+2
View File
@@ -1,3 +1,5 @@
#pragma once
#include <memory>
#include <string>
+2
View File
@@ -1,3 +1,5 @@
#pragma once
#include <stdint.h>
#include "Client.hh"
+2 -1
View File
@@ -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
View File
@@ -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>
+2
View File
@@ -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
View File
@@ -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);
}
+3 -4
View File
@@ -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>";
}
}
+1 -1
View File
@@ -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);