fix DC NTE lobby interactions

This commit is contained in:
Martin Michelsen
2023-11-16 23:22:26 -08:00
parent 9cbcd09be0
commit 132395a53a
6 changed files with 218 additions and 76 deletions
+169 -48
View File
@@ -23,6 +23,19 @@ using namespace std;
// The functions in this file are called when a client sends a game command
// (60, 62, 6C, 6D, C9, or CB).
// There are three different sets of subcommand numbers: the DC NTE set, the
// November 2000 prototype set, and the set used by all other versions of the
// game (starting from the December 2000 prototype, all the way through BB).
// Currently we do not support the November 2000 prototype, but we do support
// DC NTE. In general, DC NTE clients can only interact with non-NTE players in
// very limited ways, since most subcommand-based actions take place in games,
// and non-NTE players cannot join NTE games. Commands sent by DC NTE clients
// are not handled by the functions defined in subcommand_handlers, but are
// instead handled by handle_subcommand_dc_nte. This means we only have to
// consider sending to DC NTE clients in a small subset of the command handlers
// (those that can occur in the lobby), and we can skip sending most
// subcommands to DC NTE by default.
bool command_is_private(uint8_t command) {
return (command == 0x62) || (command == 0x6D);
}
@@ -39,7 +52,8 @@ static void forward_subcommand(
uint8_t command,
uint8_t flag,
const void* data,
size_t size) {
size_t size,
uint8_t dc_nte_subcommand = 0x00) {
// If the command is an Ep3-only command, make sure an Ep3 client sent it
bool command_is_ep3 = (command & 0xF0) == 0xC0;
@@ -56,7 +70,17 @@ static void forward_subcommand(
if (!target) {
return;
}
send_command(target, command, flag, data, size);
if (target->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION)) {
if (dc_nte_subcommand) {
string nte_data(reinterpret_cast<const char*>(data), size);
nte_data[0] = dc_nte_subcommand;
send_command(target, command, flag, nte_data);
} else {
c->log.warning("Attempted to send unsupported target command to DC NTE client; dropping command");
}
} else {
send_command(target, command, flag, data, size);
}
} else {
if (command_is_ep3) {
@@ -66,8 +90,25 @@ static void forward_subcommand(
}
send_command(target, command, flag, data, size);
}
} else {
send_command_excluding_client(l, c, command, flag, data, size);
string nte_data;
for (auto& lc : l->clients) {
if (!lc || (lc == c)) {
continue;
}
if (lc->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION)) {
if (dc_nte_subcommand) {
if (nte_data.empty()) {
nte_data.assign(reinterpret_cast<const char*>(data), size);
nte_data[0] = dc_nte_subcommand;
}
send_command(lc, command, flag, nte_data);
}
} else {
send_command(lc, command, flag, data, size);
}
}
}
// Before battle, forward only chat commands to watcher lobbies; during
@@ -590,13 +631,21 @@ static void on_word_select(shared_ptr<Client> c, uint8_t command, uint8_t flag,
}
}
static void on_set_player_visibility(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
static void on_set_player_invisible(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_SetPlayerVisibility_6x22_6x23>(data, size);
if (cmd.header.client_id != c->lobby_client_id) {
return;
}
forward_subcommand(c, command, flag, data, size, 0x1E);
}
static void on_set_player_visible(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_SetPlayerVisibility_6x22_6x23>(data, size);
if (cmd.header.client_id == c->lobby_client_id) {
auto l = c->require_lobby();
forward_subcommand(c, command, flag, data, size, 0x1F);
forward_subcommand(c, command, flag, data, size);
auto l = c->require_lobby();
if (!l->is_game() && !c->config.check_flag(Client::Flag::IS_DC_V1)) {
send_arrow_update(l);
}
@@ -610,13 +659,18 @@ static void on_set_player_visibility(shared_ptr<Client> c, uint8_t command, uint
////////////////////////////////////////////////////////////////////////////////
// Game commands used by cheat mechanisms
template <typename CmdT>
static void on_change_floor(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<CmdT>(data, size);
static void on_change_floor_6x1F(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_SetPlayerArea_6x1F>(data, size);
c->floor = cmd.floor;
forward_subcommand(c, command, flag, data, size);
}
static void on_change_floor_6x21(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_InterLevelWarp_6x21>(data, size);
c->floor = cmd.floor;
forward_subcommand(c, command, flag, data, size, 0x1D);
}
// When a player dies, decrease their mag's synchro
static void on_player_died(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_ClientIDHeader>(data, size, 0xFFFF);
@@ -722,7 +776,7 @@ static void on_switch_state_changed(shared_ptr<Client> c, uint8_t command, uint8
////////////////////////////////////////////////////////////////////////////////
template <typename CmdT>
template <typename CmdT, uint8_t DCNTESubcommand>
void on_movement(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<CmdT>(data, size);
if (cmd.header.client_id != c->lobby_client_id) {
@@ -732,10 +786,10 @@ void on_movement(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void
c->x = cmd.x;
c->z = cmd.z;
forward_subcommand(c, command, flag, data, size);
forward_subcommand(c, command, flag, data, size, DCNTESubcommand);
}
template <typename CmdT>
template <typename CmdT, uint8_t DCNTESubcommand>
void on_movement_with_floor(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<CmdT>(data, size);
if (cmd.header.client_id != c->lobby_client_id) {
@@ -746,7 +800,12 @@ void on_movement_with_floor(shared_ptr<Client> c, uint8_t command, uint8_t flag,
c->z = cmd.z;
c->floor = cmd.floor;
forward_subcommand(c, command, flag, data, size);
forward_subcommand(c, command, flag, data, size, DCNTESubcommand);
}
static void on_toggle_counter_interaction(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
check_size_t<G_ToggleCounterInteraction_6x52>(data, size, 0xFFFF);
forward_subcommand(c, command, flag, data, size, 0x46);
}
////////////////////////////////////////////////////////////////////////////////
@@ -2544,6 +2603,69 @@ static void on_write_quest_global_flag_bb(shared_ptr<Client> c, uint8_t, uint8_t
////////////////////////////////////////////////////////////////////////////////
static void handle_subcommand_dc_nte(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game()) {
// In a game, assume all other clients are DC NTE as well and forward the
// subcommand without any processing
forward_subcommand(c, command, flag, data, size);
} else {
// In a lobby, we have to deal with all other versions of the game having
// different subcommand numbers than DC NTE. We'll forward the command
// verbatim to other DC NTE clients, but will have to translate it for
// non-NTE clients. Some subcommands may not map cleanly; for those, we
// don't send anything at all to non-NTE clients.
auto& header = check_size_t<G_UnusedHeader>(data, size, 0xFFFF);
uint8_t nte_subcommand = header.subcommand;
uint8_t non_nte_subcommand = 0x00;
switch (nte_subcommand) {
case 0x1D:
non_nte_subcommand = 0x21;
break;
case 0x1E:
non_nte_subcommand = 0x22;
break;
case 0x1F:
non_nte_subcommand = 0x23;
break;
case 0x36:
non_nte_subcommand = 0x3F;
break;
case 0x37:
non_nte_subcommand = 0x40;
break;
case 0x39:
non_nte_subcommand = 0x42;
break;
case 0x46:
non_nte_subcommand = 0x52;
break;
default:
non_nte_subcommand = 0x00;
}
string non_nte_data;
for (auto lc : l->clients) {
if (!lc || (lc == c)) {
continue;
}
if (lc->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION)) {
send_command(lc, command, flag, data, size);
} else if (non_nte_subcommand != 0x00) {
if (non_nte_data.empty()) {
non_nte_data.assign(reinterpret_cast<const char*>(data), size);
non_nte_data[0] = non_nte_subcommand;
}
send_command(lc, command, flag, non_nte_data);
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
typedef void (*subcommand_handler_t)(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size);
subcommand_handler_t subcommand_handlers[0x100] = {
@@ -2578,11 +2700,11 @@ subcommand_handler_t subcommand_handlers[0x100] = {
/* 6x1C */ on_forward_check_size_game,
/* 6x1D */ nullptr,
/* 6x1E */ nullptr,
/* 6x1F */ on_change_floor<G_SetPlayerArea_6x1F>,
/* 6x20 */ on_movement_with_floor<G_SetPosition_6x20>,
/* 6x21 */ on_change_floor<G_InterLevelWarp_6x21>,
/* 6x22 */ on_forward_check_size_client,
/* 6x23 */ on_set_player_visibility,
/* 6x1F */ on_change_floor_6x1F,
/* 6x20 */ on_movement_with_floor<G_SetPosition_6x20, 0x00>,
/* 6x21 */ on_change_floor_6x21,
/* 6x22 */ on_set_player_invisible,
/* 6x23 */ on_set_player_visible,
/* 6x24 */ on_forward_check_size_game,
/* 6x25 */ on_equip_item,
/* 6x26 */ on_unequip_item,
@@ -2609,11 +2731,11 @@ subcommand_handler_t subcommand_handlers[0x100] = {
/* 6x3B */ on_forward_check_size,
/* 6x3C */ nullptr,
/* 6x3D */ nullptr,
/* 6x3E */ on_movement_with_floor<G_StopAtPosition_6x3E>,
/* 6x3F */ on_movement_with_floor<G_SetPosition_6x3F>,
/* 6x40 */ on_movement<G_WalkToPosition_6x40>,
/* 6x3E */ on_movement_with_floor<G_StopAtPosition_6x3E, 0x00>,
/* 6x3F */ on_movement_with_floor<G_SetPosition_6x3F, 0x36>,
/* 6x40 */ on_movement<G_WalkToPosition_6x40, 0x37>,
/* 6x41 */ nullptr,
/* 6x42 */ on_movement<G_RunToPosition_6x42>,
/* 6x42 */ on_movement<G_RunToPosition_6x42, 0x39>,
/* 6x43 */ on_forward_check_size_client,
/* 6x44 */ on_forward_check_size_client,
/* 6x45 */ on_forward_check_size_client,
@@ -2629,7 +2751,7 @@ subcommand_handler_t subcommand_handlers[0x100] = {
/* 6x4F */ on_forward_check_size_client,
/* 6x50 */ on_forward_check_size_client,
/* 6x51 */ nullptr,
/* 6x52 */ on_forward_check_size,
/* 6x52 */ on_toggle_counter_interaction,
/* 6x53 */ on_forward_check_size_game,
/* 6x54 */ nullptr,
/* 6x55 */ on_forward_check_size_client,
@@ -2809,32 +2931,31 @@ void on_subcommand_multi(shared_ptr<Client> c, uint8_t command, uint8_t flag, co
if (data.empty()) {
throw runtime_error("game command is empty");
}
if (c->version() == GameVersion::DC &&
(c->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION) || c->config.check_flag(Client::Flag::IS_DC_V1_PROTOTYPE))) {
// TODO: We should convert these to non-trial formats and vice versa
forward_subcommand(c, command, flag, data.data(), data.size());
} else {
StringReader r(data);
while (!r.eof()) {
size_t size;
const auto& header = r.get<G_UnusedHeader>(false);
if (header.size != 0) {
size = header.size << 2;
} else {
const auto& ext_header = r.get<G_ExtendedHeader<G_UnusedHeader>>(false);
size = ext_header.size;
if (size < 8) {
throw runtime_error("extended subcommand header has size < 8");
}
if (size & 3) {
throw runtime_error("extended subcommand size is not a multiple of 4");
}
}
if (size == 0) {
throw runtime_error("invalid subcommand size");
}
const void* data = r.getv(size);
StringReader r(data);
while (!r.eof()) {
size_t size;
const auto& header = r.get<G_UnusedHeader>(false);
if (header.size != 0) {
size = header.size << 2;
} else {
const auto& ext_header = r.get<G_ExtendedHeader<G_UnusedHeader>>(false);
size = ext_header.size;
if (size < 8) {
throw runtime_error("extended subcommand header has size < 8");
}
if (size & 3) {
throw runtime_error("extended subcommand size is not a multiple of 4");
}
}
if (size == 0) {
throw runtime_error("invalid subcommand size");
}
const void* data = r.getv(size);
if (c->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION)) {
handle_subcommand_dc_nte(c, command, flag, data, size);
} else {
auto fn = subcommand_handlers[header.subcommand];
if (fn) {
fn(c, command, flag, data, size);