Files
psopeeps-newserv/src/Episode3/Server.cc
T
2026-06-13 20:07:23 -07:00

3179 lines
111 KiB
C++

#include "Server.hh"
#include <phosg/Random.hh>
#include <phosg/Time.hh>
#include "../Loggers.hh"
#include "../Revision.hh"
#include "../SendCommands.hh"
namespace Episode3 {
// This is (obviously) not the original std::string. The original std::string is:
// NTE: "03/05/29 18:00 by K.Toya"
// Final: "[V1][FINAL2.0] 03/09/13 15:30 by K.Toya"
static const char* VERSION_SIGNATURE = "newserv Ep3 based on [V1][FINAL2.0] 03/09/13 15:30 by K.Toya";
static const char* VERSION_SIGNATURE_NTE = "newserv Ep3 NTE based on 03/05/29 18:00 by K.Toya";
Server::PresenceEntry::PresenceEntry() {
this->clear();
}
void Server::PresenceEntry::clear() {
this->player_present = 0;
this->deck_valid = 0;
this->is_cpu_player = 0;
}
Server::Server(std::shared_ptr<Lobby> lobby, Options&& options)
: lobby(lobby),
battle_record(lobby ? lobby->battle_record : nullptr),
has_lobby(lobby != nullptr),
options(std::move(options)),
last_chosen_map(this->options.tournament ? this->options.tournament->get_map() : nullptr),
tournament_match_result_sent(false),
override_environment_number(0xFF),
def_dice_value_range_override(0xFF),
atk_dice_value_range_2v1_override(0xFF),
def_dice_value_range_2v1_override(0xFF),
battle_finished(false),
battle_in_progress(false),
round_num(1),
battle_phase(BattlePhase::INVALID_00),
first_team_turn(0xFF),
current_team_turn1(0xFF),
setup_phase(SetupPhase::REGISTRATION),
registration_phase(RegistrationPhase::AWAITING_NUM_PLAYERS),
action_subphase(ActionSubphase::ATTACK),
current_team_turn2(0xFF),
num_pending_attacks(0),
client_done_enqueuing_attacks(false),
player_ready_to_end_phase(false),
unknown_a10(0),
overall_time_expired(false),
battle_start_usecs(0),
should_copy_prev_states_to_current_states(0),
card_special(nullptr),
clients_done_in_redraw_initial_hand_phase(false),
num_pending_attacks_with_cards(0),
unknown_a14(0),
unknown_a15(0),
defense_list_ended_for_client(false),
next_assist_card_set_number(1),
team_exp(0),
team_dice_bonus(0),
team_client_count(0),
team_num_ally_fcs_destroyed(0),
team_num_cards_destroyed(0),
num_trap_tiles_of_type(0),
chosen_trap_tile_index_of_type(0),
has_done_pb(0),
num_6xB4x06_commands_sent(0),
prev_num_6xB4x06_commands_sent(0) {
if (this->has_lobby) {
new StackLogger(this, lobby->log.prefix + "[Ep3::Server] ", lobby->log.min_level);
} else {
new StackLogger(this, "[Ep3::Server] ", lobby_log.min_level);
}
}
Server::~Server() noexcept(false) {
if (this->logger_stack.size() != 1) {
throw std::logic_error(std::format("incorrect logger stack size: expected 1, received {}", this->logger_stack.size()));
}
delete this->logger_stack.back();
}
void Server::init() {
this->log().info_f("Creating server with random seed {:08X}", this->options.rand_crypt->seed());
this->map_and_rules = std::make_shared<MapAndRulesState>();
this->num_clients_present = 0;
this->overlay_state.clear();
for (size_t z = 0; z < 4; z++) {
this->presence_entries[z].clear();
this->deck_entries[z] = std::make_shared<DeckEntry>();
this->name_entries[z].clear();
this->name_entries_valid[z] = false;
}
this->card_special = std::make_shared<CardSpecial>(this->shared_from_this());
// Note: The original implementation calls the default PSOV2Encryption constructor for random_crypt, which just uses
// 0 as the seed. It then re-seeds the generator later. We instead expect the caller to provide a seeded generator,
// and we don't re-seed it at all.
// this->random_crypt = std::make_shared<PSOV2Encryption>(0);
this->state_flags = std::make_shared<StateFlags>();
this->clear_player_flags_after_dice_phase();
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
this->assist_server = std::make_shared<AssistServer>(this->shared_from_this());
this->ruler_server = std::make_shared<RulerServer>(this->shared_from_this());
this->ruler_server->link_objects(this->map_and_rules, this->state_flags, this->assist_server);
this->send_6xB4x46();
}
Server::StackLogger::StackLogger(const Server* s, const std::string& prefix)
: PrefixedLogger(s->logger_stack.back()->prefix + prefix, s->logger_stack.back()->min_level),
server(s) {
s->logger_stack.push_back(this);
}
Server::StackLogger::StackLogger(const Server* s, const std::string& prefix, phosg::LogLevel min_level)
: PrefixedLogger(prefix, min_level),
server(s) {
s->logger_stack.push_back(this);
}
Server::StackLogger::StackLogger(StackLogger&& other)
: PrefixedLogger(std::move(other)),
server(other.server) {
if (this->server->logger_stack.back() != &other) {
throw std::logic_error("cannot move StackLogger unless it is the last one");
}
this->server->logger_stack.back() = this;
}
Server::StackLogger& Server::StackLogger::operator=(StackLogger&& other) {
this->PrefixedLogger::operator=(std::move(other));
this->server = other.server;
if (this->server->logger_stack.back() != &other) {
throw std::logic_error("cannot move StackLogger unless it is the last one");
}
this->server->logger_stack.back() = this;
return *this;
}
Server::StackLogger::~StackLogger() noexcept(false) {
if (this->server->logger_stack.back() != this) {
throw std::logic_error("incorrect logger stack unwind order");
}
this->server->logger_stack.pop_back();
}
Server::StackLogger Server::log_stack(const std::string& prefix) const {
return StackLogger(this, prefix);
}
const Server::StackLogger& Server::log() const {
return *this->logger_stack.back();
}
std::string Server::debug_str_for_card_ref(uint16_t card_ref) const {
if (card_ref == 0xFFFF) {
return "@FFFF";
}
auto ce = this->definition_for_card_ref(card_ref);
if (ce) {
return std::format("@{:04X} (#{:04X} {})", card_ref, ce->def.card_id, ce->def.en_name.decode());
} else {
return std::format("@{:04X} (missing)", card_ref);
}
}
std::string Server::debug_str_for_card_id(uint16_t card_id) const {
if (card_id == 0xFFFF) {
return "#FFFF";
}
auto ce = this->definition_for_card_id(card_id);
if (ce) {
return std::format("#{:04X} ({})", card_id, ce->def.en_name.decode());
} else {
return std::format("#{:04X} (missing)", card_id);
}
}
int8_t Server::get_winner_team_id() const {
// Note: This function is not part of the original implementation.
parray<size_t, 2> team_player_counts(0);
parray<size_t, 2> team_win_flag_counts(0);
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (!ps) {
continue;
}
uint8_t team_id = ps->get_team_id();
team_player_counts[team_id]++;
if (ps->assist_flags & AssistFlag::HAS_WON_BATTLE) {
team_win_flag_counts[team_id]++;
}
}
if (!team_player_counts[0] || !team_player_counts[1]) {
throw std::logic_error("at least one team has no players");
}
if (team_win_flag_counts[0] && team_win_flag_counts[1]) {
throw std::logic_error("both teams have winning players");
}
for (int8_t z = 0; z < 2; z++) {
if (!team_win_flag_counts[z]) {
continue;
}
if (team_win_flag_counts[z] != team_player_counts[z]) {
throw std::logic_error("only some players on team have won");
}
return z;
}
return -1; // No team has won (yet)
}
void Server::send(const void* data, size_t size, uint8_t command, bool enable_masking) const {
// Note: This function is (obviously) not part of the original implementation.
if (this->options.output_queue) {
this->options.output_queue->emplace_back(reinterpret_cast<const char*>(data), size);
}
if (this->has_lobby) {
auto l = this->lobby.lock();
if (!l) {
throw std::runtime_error("lobby is deleted");
}
std::string masked_data;
if (enable_masking &&
!this->options.is_nte() &&
!(this->options.behavior_flags & BehaviorFlag::DISABLE_MASKING) &&
(size >= 8)) {
masked_data.assign(reinterpret_cast<const char*>(data), size);
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
set_mask_for_ep3_game_command(masked_data.data(), masked_data.size(), mask_key);
data = masked_data.data();
size = masked_data.size();
}
send_command(l, command, 0x00, data, size);
for (auto watcher_l : l->watcher_lobbies) {
send_command_if_not_loading(watcher_l, command, 0x00, data, size);
}
if (this->battle_record && this->battle_record->writable()) {
this->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, data, size);
}
} else if ((this->options.behavior_flags & BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING) &&
this->log().info_f("Generated command")) {
phosg::print_data(stderr, data, size, 0, phosg::FormatDataFlags::PRINT_ASCII | phosg::FormatDataFlags::OFFSET_16_BITS);
}
}
void Server::send_6xB4x46() const {
// Note: This function is not part of the original implementation; it was factored out from its callsites in this
// file and the std::strings were changed.
// NTE doesn't have the date_str2 field, but we send it anyway to make debugging easier.
G_ServerVersionStrings_Ep3_6xB4x46 cmd;
cmd.version_signature.encode(this->options.is_nte() ? VERSION_SIGNATURE_NTE : VERSION_SIGNATURE, Language::ENGLISH);
cmd.date_str1.encode(
std::format("Card definitions: {:016X}", this->options.card_index->definitions_hash()),
Language::ENGLISH);
std::string build_date = phosg::format_time(BUILD_TIMESTAMP);
cmd.date_str2.encode(std::format("newserv {} compiled at {}", GIT_REVISION_HASH, build_date), Language::ENGLISH);
this->send(cmd);
}
std::string Server::prepare_6xB6x41_map_definition(
std::shared_ptr<const MapIndex::Map> map, Language language, bool is_nte) {
auto vm = map->version(language);
const auto& compressed = vm->compressed(is_nte);
phosg::StringWriter w;
uint32_t subcommand_size = (compressed->size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3);
w.put<G_MapData_Ep3_6xB6x41>(
{{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed->size(), 0});
w.write(*compressed);
return std::move(w.str());
}
void Server::send_commands_for_joining_spectator(std::shared_ptr<Channel> ch) const {
if (this->last_chosen_map) {
std::string data = this->prepare_6xB6x41_map_definition(
this->last_chosen_map, ch->language, this->options.is_nte());
this->log().info_f(
"Sending {} version of map {:08X}", name_for_language(ch->language), this->last_chosen_map->map_number);
ch->send(0x6C, 0x00, data);
}
// If registration is still in progress, we don't need to send the battle state
if ((this->setup_phase != SetupPhase::REGISTRATION) ||
(this->registration_phase == RegistrationPhase::REGISTERED) ||
(this->registration_phase == RegistrationPhase::BATTLE_STARTED)) {
ch->send(0xC9, 0x00, this->prepare_6xB4x03());
for (uint8_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
ch->send(0xC9, 0x00, ps->prepare_6xB4x02());
ch->send(0xC9, 0x00, ps->prepare_6xB4x04());
}
}
if (ch->version == Version::GC_EP3_NTE) {
G_UpdateMap_Ep3NTE_6xB4x05 cmd;
cmd.state = *this->map_and_rules;
ch->send(0xC9, 0x00, cmd);
} else {
G_UpdateMap_Ep3_6xB4x05 cmd;
cmd.state = *this->map_and_rules;
ch->send(0xC9, 0x00, cmd);
}
// TODO: Sega does something like this; do we have to do this too?
// for (uint8_t client_id = 0; client_id < 4; client_id++) {
// (send 6xB4x4E, 6xB4x4C, 6xB4x4D for each set card)
// (send 6xB4x4F for client_id)
// }
ch->send(0xC9, 0x00, this->prepare_6xB4x07_decks_update());
// TODO: Sega sends 6xB4x05 here again; why? Is that necessary? They also send 6xB4x02 again for each player after
// that (but not 6xB4x04)
ch->send(0xC9, 0x00, this->prepare_6xB4x1C_names_update());
ch->send(0xC9, 0x00, this->prepare_6xB4x50_trap_tile_locations());
{
G_LoadCurrentEnvironment_Ep3_6xB4x3B cmd_3B;
ch->send(0xC9, 0x00, &cmd_3B, sizeof(cmd_3B));
}
}
}
void Server::send_debug_command_received_message(
uint8_t client_id, uint8_t subsubcommand, const char* description) const {
this->log().debug_f("{}/CAx{:02X} {}", client_id, subsubcommand, description);
this->send_debug_message("$C5{}/CAx{:02X} {}", client_id, subsubcommand, description);
}
void Server::send_debug_command_received_message(uint8_t subsubcommand, const char* description) const {
this->log().debug_f("*/CAx{:02X} {}", subsubcommand, description);
this->send_debug_message("$C5*/CAx{:02X} {}", subsubcommand, description);
}
void Server::send_debug_message_if_error_code_nonzero(uint8_t client_id, int32_t error_code) const {
if (error_code < 0) {
this->send_debug_message("$C4{}/ERROR -0x{:X}", client_id, static_cast<ssize_t>(-error_code));
} else if (error_code > 0) {
this->send_debug_message("$C4{}/ERROR 0x{:X}", client_id, static_cast<ssize_t>(error_code));
}
}
void Server::add_team_exp(uint8_t team_id, int32_t exp) {
size_t num_assists = this->assist_server->compute_num_assist_effects_for_team(team_id);
for (size_t z = 0; z < num_assists; z++) {
if (this->assist_server->get_active_assist_by_index(z) == AssistEffect::GOLD_RUSH) {
exp += (exp / 2);
}
}
this->team_exp[team_id] = std::clamp<int16_t>(
this->team_exp[team_id] + exp, 0, this->team_client_count[team_id] * 96);
uint8_t dice_boost = this->team_exp[team_id] / (this->team_client_count[team_id] * 12);
this->card_special->adjust_dice_boost_if_team_has_condition_52(team_id, &dice_boost, 0);
this->team_dice_bonus[team_id] = std::min<uint8_t>(dice_boost, 8);
}
bool Server::advance_battle_phase() {
switch (this->battle_phase) {
case BattlePhase::DICE:
this->dice_phase_after();
this->set_phase_before();
break;
case BattlePhase::SET:
this->set_phase_after();
this->move_phase_before();
break;
case BattlePhase::MOVE:
this->move_phase_after();
this->action_phase_before();
break;
case BattlePhase::ACTION:
this->action_phase_after();
this->draw_phase_before();
break;
case BattlePhase::DRAW:
this->draw_phase_after();
this->dice_phase_before();
break;
default:
throw std::logic_error("invalid battle phase");
}
return this->check_for_battle_end();
}
void Server::action_phase_after() {
this->send_6xB4x02_for_all_players_if_needed();
this->battle_phase = BattlePhase::DRAW;
}
void Server::draw_phase_before() {
for (size_t z = 0; z < 4; z++) {
if (this->player_states[z]) {
this->player_states[z]->draw_phase_before();
}
}
}
std::shared_ptr<const CardIndex::CardEntry> Server::definition_for_card_ref(uint16_t card_ref) const {
try {
return this->options.card_index->definition_for_id(this->card_id_for_card_ref(card_ref));
} catch (const std::out_of_range&) {
return nullptr;
}
}
std::shared_ptr<Card> Server::card_for_set_card_ref(uint16_t card_ref) {
if (card_ref == 0xFFFF) {
return nullptr;
}
uint8_t client_id = client_id_for_card_ref(card_ref);
if (client_id == 0xFF) {
return nullptr;
}
auto ps = this->player_states.at(client_id);
if (!ps) {
return nullptr;
}
auto card = ps->get_sc_card();
if (card && (card->get_card_ref() == card_ref)) {
return card;
}
for (size_t set_index = 0; set_index < 8; set_index++) {
card = ps->get_set_card(set_index);
if (card && (card->get_card_ref() == card_ref)) {
return card;
}
}
return nullptr;
}
std::shared_ptr<const Card> Server::card_for_set_card_ref(uint16_t card_ref) const {
return const_cast<Server*>(this)->card_for_set_card_ref(card_ref);
}
uint16_t Server::card_id_for_card_ref(uint16_t card_ref) const {
uint8_t client_id = client_id_for_card_ref(card_ref);
if (client_id != 0xFF) {
auto ps = this->player_states.at(client_id);
if (!ps) {
return 0xFFFF;
}
auto deck = ps->get_deck();
if (deck) {
return deck->card_id_for_card_ref(card_ref);
}
}
return 0xFFFF;
}
bool Server::card_ref_is_empty_or_has_valid_card_id(uint16_t card_ref) const {
if (card_ref == 0xFFFF) {
return true;
} else {
return this->card_id_for_card_ref(card_ref) != 0xFFFF;
}
}
bool Server::check_for_battle_end() {
bool ret = false;
if (this->map_and_rules->rules.hp_type == HPType::DEFEAT_TEAM) {
bool teams_defeated[2] = {true, true};
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (!ps) {
continue;
}
auto sc_card = ps->get_sc_card();
if (sc_card && !sc_card->check_card_flag(2)) {
teams_defeated[ps->get_team_id()] = false;
}
}
if (this->options.is_nte()) {
if (teams_defeated[0] || teams_defeated[1]) {
ret = true;
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps && teams_defeated[ps->get_team_id()] == 0) {
ps->assist_flags |= AssistFlag::HAS_WON_BATTLE;
}
}
}
} else if (!teams_defeated[0] || !teams_defeated[1]) {
if (teams_defeated[0] || teams_defeated[1]) {
ret = true;
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
ps->assist_flags &= ~(AssistFlag::HAS_WON_BATTLE |
AssistFlag::WINNER_DECIDED_BY_DEFEAT |
AssistFlag::BATTLE_DID_NOT_END_DUE_TO_TIME_LIMIT);
if (teams_defeated[ps->get_team_id()] == 0) {
ps->assist_flags |= AssistFlag::HAS_WON_BATTLE;
}
}
}
}
} else { // Both teams defeated?? I guess this is technically possible
ret = true;
this->compute_losing_team_id_and_add_winner_flags(AssistFlag::BATTLE_DID_NOT_END_DUE_TO_TIME_LIMIT);
}
} else { // Not DEFEAT_TEAM
if (this->options.is_nte()) {
uint8_t loser_team_id = 0;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps && ps->get_sc_card() && ps->get_sc_card()->check_card_flag(2)) {
ret = true;
loser_team_id = ps->get_team_id();
break;
}
}
if (ret) {
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps && (ps->get_team_id() != loser_team_id)) {
ps->assist_flags |= AssistFlag::HAS_WON_BATTLE;
}
}
}
} else {
bool teams_alive[2] = {false, false};
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (!ps) {
continue;
}
auto sc_card = ps->get_sc_card();
if (sc_card && sc_card->check_card_flag(2)) {
teams_alive[ps->get_team_id()] = true;
}
}
if (!teams_alive[0] || !teams_alive[1]) {
if (teams_alive[0] || teams_alive[1]) {
ret = true;
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
ps->assist_flags &= ~(AssistFlag::HAS_WON_BATTLE |
AssistFlag::WINNER_DECIDED_BY_DEFEAT |
AssistFlag::BATTLE_DID_NOT_END_DUE_TO_TIME_LIMIT);
if (!teams_alive[ps->get_team_id()]) {
ps->assist_flags |= AssistFlag::HAS_WON_BATTLE;
}
}
}
}
} else {
ret = true;
this->compute_losing_team_id_and_add_winner_flags(AssistFlag::BATTLE_DID_NOT_END_DUE_TO_TIME_LIMIT);
}
}
}
if (ret) {
this->set_battle_ended();
}
return ret;
}
void Server::force_replace_assist_card(uint8_t client_id, uint16_t card_id) {
auto ps = this->player_states.at(client_id);
if (!ps) {
throw std::runtime_error("player does not exist");
}
if (card_id == 0xFFFF) {
ps->discard_set_assist_card();
this->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02();
this->check_for_battle_end();
} else {
ps->replace_assist_card_by_id(card_id);
}
}
void Server::force_destroy_field_character(uint8_t client_id, size_t visible_index) {
auto ps = this->player_states.at(client_id);
if (!ps) {
throw std::runtime_error("player does not exist");
}
// TODO: Is it possible for there to be gaps in the set cards array? If not, we could just do a direct array lookup
// here instead of this loop
std::shared_ptr<Card> set_card = nullptr;
for (size_t set_index = 0; set_index < 8; set_index++) {
if (!ps->set_cards[set_index]) {
continue;
}
if (visible_index == 0) {
set_card = ps->set_cards[set_index];
break;
} else {
visible_index--;
}
}
if (set_card) {
set_card->card_flags |= 2;
this->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02();
this->check_for_battle_end();
}
}
void Server::force_battle_result(uint8_t specified_client_id, bool set_winner) {
auto specified_ps = this->player_states.at(specified_client_id);
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->assist_flags &= ~(AssistFlag::HAS_WON_BATTLE |
AssistFlag::WINNER_DECIDED_BY_DEFEAT |
AssistFlag::BATTLE_DID_NOT_END_DUE_TO_TIME_LIMIT);
if ((ps->get_team_id() == specified_ps->get_team_id()) == set_winner) {
ps->assist_flags |= AssistFlag::HAS_WON_BATTLE;
}
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(true);
}
}
this->set_battle_ended();
}
void Server::check_for_destroyed_cards_and_send_6xB4x05_6xB4x02() {
for (size_t z = 0; z < 4; z++) {
if (this->player_states[z]) {
this->player_states[z]->on_cards_destroyed();
}
}
this->send_6xB4x05();
this->send_6xB4x02_for_all_players_if_needed();
}
bool Server::check_presence_entry(uint8_t client_id) const {
return (client_id < 4) ? this->presence_entries[client_id].player_present : false;
}
void Server::clear_player_flags_after_dice_phase() {
this->player_ready_to_end_phase.clear(0);
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->assist_flags &= ~(AssistFlag::READY_TO_END_PHASE | AssistFlag::READY_TO_END_ACTION_PHASE);
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
}
}
void Server::compute_all_map_occupied_bits() {
for (size_t y = 0; y < 0x10; y++) {
for (size_t x = 0; x < 0x10; x++) {
this->map_and_rules->clear_occupied_bit_for_tile(x, y);
}
}
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->set_map_occupied_bits_for_sc_and_creatures();
}
}
}
void Server::compute_team_dice_bonus(uint8_t team_id) {
this->team_dice_bonus[team_id] = std::clamp<int16_t>(
this->team_exp[team_id] / (this->team_client_count[team_id] * 12), 0, 8);
}
void Server::copy_player_states_to_prev_states() {
if (this->should_copy_prev_states_to_current_states != 1) {
this->should_copy_prev_states_to_current_states = 1;
this->num_6xB4x06_commands_sent = 0;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->prev_set_card_action_chains = *ps->set_card_action_chains;
ps->prev_set_card_action_metadatas = *ps->set_card_action_metadatas;
ps->prev_card_short_statuses = *ps->card_short_statuses;
}
}
}
}
std::shared_ptr<const CardIndex::CardEntry> Server::definition_for_card_id(uint16_t card_id) const {
try {
return this->options.card_index->definition_for_id(card_id);
} catch (const std::out_of_range&) {
return nullptr;
}
}
void Server::destroy_cards_with_zero_hp() {
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
bool any_card_destroyed = false;
for (ssize_t set_index = -1; set_index < 8; set_index++) {
auto card = (set_index < 0) ? ps->get_sc_card() : ps->get_set_card(set_index);
if (card && !(card->card_flags & 2) && (card->get_current_hp() < 1)) {
card->destroy_set_card(this->options.is_nte() ? nullptr : card->w_destroyer_sc_card.lock());
any_card_destroyed = true;
}
}
if (any_card_destroyed) {
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
}
}
}
void Server::determine_first_team_turn() {
this->team_client_count[0] = this->map_and_rules->num_team0_players;
this->team_client_count[1] = this->map_and_rules->num_players - this->team_client_count[0];
if (this->team_client_count[0] == 0 || this->team_client_count[1] == 0) {
throw std::runtime_error("one or both teams have no players");
}
this->first_team_turn = 0xFF;
while (this->first_team_turn == 0xFF) {
uint8_t results[2] = {0, 0};
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
results[ps->get_team_id()] += ps->roll_dice(1);
}
}
// Handle unbalanced team sizes by weighting the results by the other team's
// player count
results[0] *= this->team_client_count[1];
results[1] *= this->team_client_count[0];
if (results[1] < results[0]) {
this->first_team_turn = 0;
} else if (results[0] < results[1]) {
this->first_team_turn = 1;
}
}
this->current_team_turn1 = this->first_team_turn;
this->current_team_turn2 = this->current_team_turn1;
}
void Server::dice_phase_after() {
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (!ps) {
continue;
}
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id);
for (size_t z = 0; z < num_assists; z++) {
auto eff = this->assist_server->get_active_assist_by_index(z);
if ((eff == AssistEffect::CHARITY) || (!this->options.is_nte() && (eff == AssistEffect::CHARITY_PLUS))) {
int16_t exp_delta = (eff == AssistEffect::CHARITY_PLUS) ? -1 : 1;
for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) {
auto other_ps = this->player_states[other_client_id];
if (other_ps && this->current_team_turn2 == other_ps->get_team_id()) {
for (size_t die_index = 0; die_index < 2; die_index++) {
if (other_ps->get_dice_result(die_index) >= 5) {
this->add_team_exp(ps->get_team_id(), exp_delta);
}
}
}
}
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
}
}
}
this->battle_phase = BattlePhase::SET;
}
void Server::set_phase_before() {
for (size_t z = 0; z < 4; z++) {
if (this->player_states[z]) {
this->player_states[z]->handle_before_turn_assist_effects();
}
}
this->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02();
}
void Server::draw_phase_after() {
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
if (ps->draw_cards_allowed()) {
ps->draw_hand(0);
}
if (ps->is_team_turn()) {
ps->compute_team_dice_bonus_after_draw_phase();
}
}
}
this->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02();
this->battle_phase = BattlePhase::DICE;
this->current_team_turn1 ^= 1;
this->round_num++;
if (this->current_team_turn1 == this->first_team_turn) {
if (this->map_and_rules->rules.overall_time_limit > 0) {
// Battle time limits are specified in increments of 5 minutes. This part is not based on the original code
// because the timing facilities used are different.
uint64_t limit_5mins = this->map_and_rules->rules.overall_time_limit;
uint64_t end_usecs = this->battle_start_usecs + (limit_5mins * 300 * 1000 * 1000);
if (phosg::now() >= end_usecs) {
this->overall_time_expired = true;
}
}
// Apparently the hard limit of 1000 was added after NTE was released
if (this->overall_time_expired || (!this->options.is_nte() && (this->round_num >= 1000))) {
bool no_winner_specified = true;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps && (ps->assist_flags & AssistFlag::HAS_WON_BATTLE)) {
no_winner_specified = false;
break;
}
}
if (no_winner_specified) {
this->compute_losing_team_id_and_add_winner_flags(0);
}
this->round_num--;
this->set_battle_ended();
}
}
}
void Server::dice_phase_before() {
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->dice_phase_before();
}
this->client_done_enqueuing_attacks[z] = 0;
}
this->destroy_cards_with_zero_hp();
this->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02();
this->check_for_battle_end();
this->send_6xB4x02_for_all_players_if_needed();
this->action_subphase = ActionSubphase::ATTACK;
this->current_team_turn2 = this->current_team_turn1;
this->num_pending_attacks = 0;
this->num_pending_attacks_with_cards = 0;
this->unknown_a14 = 0;
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
}
void Server::end_attack_list_for_client(uint8_t client_id) {
if (client_id >= 4) {
return;
}
auto ps = this->player_states.at(client_id);
if (!ps) {
return;
}
if (this->current_team_turn2 == ps->get_team_id()) {
this->client_done_enqueuing_attacks[client_id] = true;
}
bool all_clients_done_enqueuing_attacks = true;
for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) {
auto other_ps = this->player_states[other_client_id];
if (!other_ps) {
continue;
}
auto card = other_ps->get_sc_card();
if (card &&
!card->check_card_flag(2) &&
(other_ps->get_team_id() == this->current_team_turn2) &&
(this->client_done_enqueuing_attacks[other_client_id] == 0)) {
all_clients_done_enqueuing_attacks = false;
}
}
if (all_clients_done_enqueuing_attacks) {
for (size_t z = 0; z < 4; z++) {
auto other_ps = this->player_states[z];
if (other_ps) {
other_ps->assist_flags &= (~AssistFlag::READY_TO_END_ACTION_PHASE);
other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
}
this->end_action_phase();
this->client_done_enqueuing_attacks.clear(false);
} else {
ps->assist_flags |= AssistFlag::READY_TO_END_ACTION_PHASE;
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
}
void Server::end_action_phase() {
this->num_pending_attacks = 0;
this->unknown_a15 = 1;
// Annoyingly, this is the original logic. We use an enum because it appears that this can only ever be 0 or 2, but
// we may have to delete the enum if that turns out to be false.
this->action_subphase = static_cast<ActionSubphase>(static_cast<uint8_t>(this->action_subphase) + 2);
if (this->options.is_nte()) {
this->unknown_8023EEF4();
this->update_battle_state_flags_and_send_6xB4x03_if_needed(0);
this->send_6xB4x02_for_all_players_if_needed();
} else {
this->copy_player_states_to_prev_states();
this->unknown_8023EEF4();
this->send_set_card_updates_and_6xB4x04_if_needed();
}
}
bool Server::enqueue_attack_or_defense(uint8_t client_id, ActionState* pa) {
auto log = this->log_stack("enqueue_attack_or_defense: ");
if (log.should_log(phosg::LogLevel::L_DEBUG)) {
std::string s = pa->str(this->shared_from_this());
log.debug_f("input: {}", s);
}
if (client_id >= 4) {
this->ruler_server->error_code3 = -0x78;
log.debug_f("failed: invalid client ID");
return false;
}
auto ps = this->player_states[client_id];
if (!ps) {
this->ruler_server->error_code3 = -0x72;
log.debug_f("failed: player not present");
return false;
}
if (pa->action_card_refs[0] == 0xFFFF) {
if (pa->defense_card_ref != 0xFFFF) {
pa->action_card_refs[0] = pa->defense_card_ref;
log.debug_f("moved defense card ref to action card ref 0");
}
} else {
pa->defense_card_ref = pa->action_card_refs[0];
log.debug_f("moved action card ref 0 to defense card ref");
}
if (!this->ruler_server->is_attack_or_defense_valid(*pa)) {
log.debug_f("failed: attack or defense not valid");
return false;
}
int16_t ally_atk_result = this->send_6xB4x33_remove_ally_atk_if_needed(*pa);
if (ally_atk_result == 1) {
log.debug_f("pending: need ally approval");
return true;
} else if (ally_atk_result == -1) {
log.debug_f("failed: ally declined");
return false;
}
if (this->num_pending_attacks >= 0x20) {
this->ruler_server->error_code3 = -0x71;
log.debug_f("failed: too many pending attacks");
return false;
}
size_t attack_index = this->num_pending_attacks++;
this->pending_attacks[attack_index] = *pa;
if (log.should_log(phosg::LogLevel::L_DEBUG)) {
log.debug_f("set pending attack {}: {}",
attack_index, this->pending_attacks[attack_index].str(this->shared_from_this()));
}
ps->set_action_cards_for_action_state(*pa);
log.debug_f("set action cards");
auto card = this->card_for_set_card_ref(this->send_6xB4x06_if_card_ref_invalid(pa->attacker_card_ref, 1));
if (card) {
card->card_flags |= 0x400;
auto card_ps = card->player_state();
if (card_ps) {
card_ps->send_6xB4x04_if_needed();
}
}
card = this->card_for_set_card_ref(this->send_6xB4x06_if_card_ref_invalid(pa->original_attacker_card_ref, 2));
if (card) {
card = this->card_for_set_card_ref(pa->target_card_refs[0]);
if (card) {
card->card_flags |= 0x800;
card->player_state()->send_6xB4x04_if_needed();
}
}
return true;
}
BattlePhase Server::get_battle_phase() const {
return this->battle_phase;
}
ActionSubphase Server::get_current_action_subphase() const {
return this->action_subphase;
}
uint8_t Server::get_current_team_turn() const {
return this->current_team_turn1;
}
std::shared_ptr<PlayerState> Server::get_player_state(uint8_t client_id) {
if (client_id >= 4) {
return nullptr;
}
return this->player_states[client_id];
}
std::shared_ptr<const PlayerState> Server::get_player_state(uint8_t client_id) const {
if (client_id >= 4) {
return nullptr;
}
return this->player_states[client_id];
}
uint32_t Server::get_random_raw() {
le_uint32_t ret;
if (this->options.opt_rand_stream) {
this->options.opt_rand_stream->readx(&ret, sizeof(ret));
} else {
ret = this->options.rand_crypt->next();
}
if (this->battle_record && this->battle_record->writable()) {
this->battle_record->add_random_data(&ret, sizeof(ret));
}
return ret;
}
uint32_t Server::get_random(uint32_t max) {
// The original implementation was essentially:
// return (static_cast<double>(this->random_source->next() >> 16) / 65536.0) * max
// This is unnecessarily complicated and imprecise, so we instead just do:
return this->get_random_raw() % max;
}
float Server::get_random_float_0_1() {
// This lacks some precision, but matches the original implementation.
return (static_cast<double>(this->get_random_raw() >> 16) / 65536.0);
}
uint32_t Server::get_round_num() const {
return this->round_num;
}
SetupPhase Server::get_setup_phase() const {
return this->setup_phase;
}
uint32_t Server::get_should_copy_prev_states_to_current_states() const {
return this->should_copy_prev_states_to_current_states;
}
bool Server::is_registration_complete() const {
return this->setup_phase != SetupPhase::REGISTRATION;
}
void Server::move_phase_after() {
if (!this->options.is_nte()) {
for (size_t trap_type = 0; trap_type < 5; trap_type++) {
uint8_t trap_tile_index = this->chosen_trap_tile_index_of_type[trap_type];
if (trap_tile_index == 0xFF) {
continue;
}
bool should_trigger = false;
int16_t trap_x = this->trap_tile_locs[trap_type][trap_tile_index][0];
int16_t trap_y = this->trap_tile_locs[trap_type][trap_tile_index][1];
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
auto sc_card = ps->get_sc_card();
if (sc_card && (sc_card->card_flags & 0x80) && (sc_card->loc.x == trap_x) && (sc_card->loc.y == trap_y)) {
should_trigger = true;
break;
}
}
}
if (!should_trigger) {
continue;
}
static const std::array<std::vector<uint16_t>, 5> DEFAULT_TRAP_CARD_IDS = {
// Red: Dice Fever, Heavy Fog, Muscular, Immortality, Snail Pace
std::vector<uint16_t>{0x00F7, 0x010F, 0x012E, 0x013B, 0x013C},
// Blue: Gold Rush, Charity, Requiem
std::vector<uint16_t>{0x0131, 0x012B, 0x0133},
// Purple: Powerless Rain, Trash 1, Empty Hand, Skip Draw
std::vector<uint16_t>{0x00FA, 0x0125, 0x0126, 0x0137},
// Green: Brave Wind, Homesick, Fly
std::vector<uint16_t>{0x00FB, 0x014E, 0x0107},
// Yellow: Dice+1, Battle Royale, Reverse Card, Giant Garden, Fix
std::vector<uint16_t>{0x00F6, 0x0242, 0x014B, 0x0145, 0x012D}};
const std::vector<uint16_t>* trap_card_ids = &this->options.trap_card_ids.at(trap_type);
if (trap_card_ids->empty()) {
trap_card_ids = &DEFAULT_TRAP_CARD_IDS.at(trap_type);
}
// This is the original implementation. We do something smarter instead.
// uint16_t trap_card_id = 0;
// while (trap_card_id == 0) {
// trap_card_id = TRAP_CARD_IDS[trap_type][this->get_random(5)];
// }
uint16_t trap_card_id = 0xFFFF;
if (trap_card_ids->size() == 1) {
trap_card_id = trap_card_ids->at(0);
} else if (trap_card_ids->size() > 1) {
trap_card_id = trap_card_ids->at(this->get_random(trap_card_ids->size()));
}
if (trap_card_id != 0xFFFF) {
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
auto sc_card = ps->get_sc_card();
if (sc_card && (abs(sc_card->loc.x - trap_x) < 2) && (abs(sc_card->loc.y - trap_y) < 2) &&
ps->replace_assist_card_by_id(trap_card_id)) {
G_EnqueueAnimation_Ep3_6xB4x2C cmd;
cmd.change_type = 0x01;
cmd.client_id = client_id;
cmd.card_refs.clear(0xFFFF);
cmd.loc.x = trap_x;
cmd.loc.y = trap_y;
cmd.loc.direction = static_cast<Direction>(trap_type);
cmd.trap_card_id = trap_card_id;
cmd.unknown_a3 = 0xFFFFFFFF;
this->send(cmd);
}
}
}
}
// Note: This is the original implementation:
// if (this->num_trap_tiles_of_type[trap_type] > 1) {
// uint8_t new_index = this->chosen_trap_tile_index_of_type[trap_type];
// while (new_index == this->chosen_trap_tile_index_of_type[trap_type]) {
// new_index = this->get_random(this->num_trap_tiles_of_type[trap_type]);
// }
// this->chosen_trap_tile_index_of_type[trap_type] = new_index;
// this->send_6xB4x50();
// }
// We instead use an implementation that consumes a constant amount of randomness per pass.
if (this->num_trap_tiles_of_type[trap_type] == 2) {
this->chosen_trap_tile_index_of_type[trap_type] ^= 1;
this->send_6xB4x50_trap_tile_locations();
} else if (this->num_trap_tiles_of_type[trap_type] > 2) {
// Generate a new random index, but forbid it from matching the existing index
uint8_t new_index = this->get_random(this->num_trap_tiles_of_type[trap_type] - 1);
if (new_index >= this->chosen_trap_tile_index_of_type[trap_type]) {
new_index++;
}
this->chosen_trap_tile_index_of_type[trap_type] = new_index;
this->send_6xB4x50_trap_tile_locations();
}
}
}
this->battle_phase = BattlePhase::ACTION;
}
void Server::action_phase_before() {
this->unknown_a10 = 0;
this->current_team_turn2 = this->current_team_turn1;
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
ps->action_phase_before();
}
this->has_done_pb[client_id] = false;
}
}
G_SetPlayerNames_Ep3_6xB4x1C Server::prepare_6xB4x1C_names_update() const {
G_SetPlayerNames_Ep3_6xB4x1C cmd;
cmd.entries = this->name_entries;
return cmd;
}
void Server::send_6xB4x1C_names_update() {
this->send(this->prepare_6xB4x1C_names_update());
}
int8_t Server::send_6xB4x33_remove_ally_atk_if_needed(const ActionState& pa) {
G_SubtractAllyATKPoints_Ep3_6xB4x33 cmd;
bool has_ally_cost = false;
uint8_t ally_cost = 0;
uint8_t setter_client_id = 0xFF;
std::shared_ptr<PlayerState> setter_ps = nullptr;
cmd.card_ref = 0xFFFF;
for (size_t z = 0; (z < 8) && (pa.action_card_refs[z] != 0xFFFF); z++) {
auto ce = this->definition_for_card_ref(pa.action_card_refs[z]);
if (ce && (ce->def.ally_cost > 0)) {
ally_cost = ce->def.ally_cost;
has_ally_cost = true;
setter_client_id = client_id_for_card_ref(pa.action_card_refs[z]);
setter_ps = this->get_player_state(setter_client_id);
cmd.card_ref = pa.action_card_refs[z];
break;
}
}
if (!has_ally_cost) {
return 0;
}
if (!setter_ps) {
this->ruler_server->error_code3 = -0x67;
return 0;
}
bool ally_has_sufficient_atk = false;
for (size_t z = 0; z < 4; z++) {
auto ally_ps = this->get_player_state(z);
if ((z != setter_client_id) && ally_ps) {
if ((ally_ps->get_team_id() == setter_ps->get_team_id()) && (ally_ps->get_atk_points() >= ally_cost)) {
ally_has_sufficient_atk = true;
}
}
}
if (!ally_has_sufficient_atk) {
this->ruler_server->error_code3 = -0x66;
return -1;
}
this->pb_action_states[setter_client_id] = pa;
this->has_done_pb[setter_client_id] = true;
for (size_t z = 0; z < 4; z++) {
this->has_done_pb_with_client[setter_client_id][z] = false;
}
cmd.client_id = setter_client_id;
cmd.ally_cost = ally_cost;
this->send(cmd);
return 1;
}
G_UpdateDecks_Ep3_6xB4x07 Server::prepare_6xB4x07_decks_update() const {
G_UpdateDecks_Ep3_6xB4x07 cmd07;
for (size_t z = 0; z < 4; z++) {
if (!this->check_presence_entry(z)) {
cmd07.entries_present[z] = 0;
cmd07.entries[z].clear();
cmd07.entries[z].team_id = 0xFFFFFFFF;
} else {
cmd07.entries_present[z] = 1;
cmd07.entries[z] = *this->deck_entries[z];
}
}
return cmd07;
}
void Server::send_all_state_updates() {
this->send(this->prepare_6xB4x07_decks_update());
if (this->options.is_nte()) {
G_UpdateMap_Ep3NTE_6xB4x05 cmd;
cmd.state = *this->map_and_rules;
this->send(cmd);
} else {
G_UpdateMap_Ep3_6xB4x05 cmd;
cmd.state = *this->map_and_rules;
this->send(cmd);
}
this->send_6xB4x02_for_all_players_if_needed();
}
void Server::send_set_card_updates_and_6xB4x04_if_needed() {
if (this->should_copy_prev_states_to_current_states == 0) {
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->send_set_card_updates();
ps->send_6xB4x04_if_needed();
}
}
} else {
this->should_copy_prev_states_to_current_states = 0;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
*ps->set_card_action_chains = ps->prev_set_card_action_chains;
*ps->set_card_action_metadatas = ps->prev_set_card_action_metadatas;
ps->send_set_card_updates();
*ps->card_short_statuses = ps->prev_card_short_statuses;
ps->send_6xB4x04_if_needed();
}
}
this->num_6xB4x06_commands_sent = 0;
}
}
void Server::set_battle_ended() {
this->setup_phase = SetupPhase::BATTLE_ENDED;
this->send_6xB4x39();
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
}
void Server::set_battle_started() {
this->setup_phase = SetupPhase::MAIN_BATTLE;
this->round_num = 1;
this->battle_phase = BattlePhase::DICE;
this->dice_phase_before();
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
this->send_6xB4x02_for_all_players_if_needed();
this->send_6xB4x05();
}
bool Server::player_can_receive_dice_boost(uint8_t client_id) const {
auto ps = this->player_states[client_id];
bool is_1p_2v1 = (this->team_client_count.at(ps->get_team_id()) < this->team_client_count[ps->get_team_id() ^ 1]);
const auto& rules = this->map_and_rules->rules;
return (rules.atk_dice_range(is_1p_2v1).second >= 3) || (rules.def_dice_range(is_1p_2v1).second >= 3);
}
void Server::set_client_id_ready_to_advance_phase(uint8_t client_id, BattlePhase battle_phase) {
if (client_id >= 4) {
return;
}
bool is_nte = this->options.is_nte();
auto ps = this->player_states[client_id];
if (ps &&
(this->current_team_turn1 == ps->get_team_id()) &&
(!is_nte || (this->battle_phase == battle_phase)) &&
(this->setup_phase == SetupPhase::MAIN_BATTLE)) {
ps->assist_flags |= AssistFlag::READY_TO_END_PHASE;
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
if (this->battle_phase == BattlePhase::DICE) {
if (is_nte ||
!(ps->assist_flags & AssistFlag::ELIGIBLE_FOR_DICE_BOOST) ||
this->map_and_rules->rules.disable_dice_boost ||
!this->player_can_receive_dice_boost(client_id)) {
ps->assist_flags &= (~AssistFlag::ELIGIBLE_FOR_DICE_BOOST);
ps->roll_main_dice_or_apply_after_effects();
if (!is_nte && (ps->get_atk_points() < 3) && (ps->get_def_points() < 3)) {
ps->assist_flags |= AssistFlag::ELIGIBLE_FOR_DICE_BOOST;
}
} else {
// TODO: It'd be nice to do this in a constant-randomness way, but I'm lazy, and this matches Sega's original
// implementation. The less-lazy way to do it would be to roll three dice: one in the range [1, 2] to decide
// which of ATK or DEF will be boosted, then roll the ATK die in range [1, N] (or [3, N] if it's boosted), and
// do the same for the DEF die.
for (size_t z = 0; z < 200; z++) {
ps->roll_main_dice_or_apply_after_effects();
if ((ps->get_atk_points() >= 3) || (ps->get_def_points() >= 3)) {
break;
}
}
ps->assist_flags &= (~AssistFlag::ELIGIBLE_FOR_DICE_BOOST);
}
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
this->player_ready_to_end_phase[client_id] = true;
bool should_advance_phase = true;
for (size_t z = 0; z < 4; z++) {
auto other_ps = this->player_states[z];
if (!other_ps) {
continue;
}
auto sc_card = other_ps->get_sc_card();
if (sc_card && !sc_card->check_card_flag(2) &&
(this->current_team_turn1 == other_ps->get_team_id()) &&
!this->player_ready_to_end_phase[z]) {
should_advance_phase = false;
break;
}
}
if (should_advance_phase) {
if (!this->options.is_nte()) {
this->copy_player_states_to_prev_states();
}
this->advance_battle_phase();
if (!this->options.is_nte()) {
this->send_set_card_updates_and_6xB4x04_if_needed();
}
this->clear_player_flags_after_dice_phase();
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
this->send_6xB4x39();
}
}
}
void Server::set_phase_after() {
bool is_nte = this->options.is_nte();
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
auto card = ps->get_sc_card();
if (card) {
this->card_special->apply_action_conditions(
EffectWhen::AFTER_SET_PHASE, nullptr, card, is_nte ? 0x1F : 0x04, nullptr);
}
for (size_t set_index = 0; set_index < 8; set_index++) {
auto card = ps->get_set_card(set_index);
if (card) {
this->card_special->apply_action_conditions(
EffectWhen::AFTER_SET_PHASE, nullptr, card, is_nte ? 0x1F : 0x04, nullptr);
}
}
}
}
this->send_6xB4x02_for_all_players_if_needed();
bool clients_with_assist_vanish[4] = {false, false, false, false};
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (!ps) {
continue;
}
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id);
for (size_t z = 0; z < num_assists; z++) {
switch (this->assist_server->get_active_assist_by_index(z)) {
case AssistEffect::SHUFFLE_ALL:
case AssistEffect::SHUFFLE_GROUP:
if (is_nte ||
(!this->map_and_rules->rules.disable_deck_shuffle && !this->map_and_rules->rules.disable_deck_loop)) {
ps->discard_and_redraw_hand();
}
break;
case AssistEffect::TRASH_1:
ps->discard_random_hand_card();
break;
case AssistEffect::EMPTY_HAND:
ps->discard_all_attack_action_cards_from_hand();
break;
case AssistEffect::HITMAN:
ps->discard_all_item_and_creature_cards_from_hand();
break;
case AssistEffect::ASSIST_TRASH:
ps->discard_all_assist_cards_from_hand();
break;
case AssistEffect::ASSIST_VANISH:
if (!is_nte) {
clients_with_assist_vanish[client_id] = true;
} else if (ps->get_assist_turns_remaining() != 90) {
ps->discard_set_assist_card();
}
break;
default:
break;
}
}
}
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps && clients_with_assist_vanish[client_id]) {
ps->discard_set_assist_card();
}
}
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps && (ps->get_assist_turns_remaining() == 90) && (ps->assist_delay_turns < 1)) {
ps->discard_set_assist_card();
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
}
this->battle_phase = BattlePhase::MOVE;
}
void Server::move_phase_before() {
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps) {
ps->move_phase_before();
}
}
}
void Server::set_player_deck_valid(uint8_t client_id) {
this->presence_entries[client_id].deck_valid = true;
}
void Server::setup_and_start_battle() {
bool is_nte = this->options.is_nte();
this->setup_phase = SetupPhase::STARTER_ROLLS;
// Note: This is where original implementation re-seeds its random generator
// (it uses time() as the seed value).
for (size_t z = 0; z < 4; z++) {
if (!this->check_presence_entry(z)) {
if (!is_nte) {
this->name_entries[z].clear();
}
} else {
this->player_states[z] = std::make_shared<PlayerState>(z, this->shared_from_this());
this->player_states[z]->init();
}
}
if (!is_nte && (this->map_and_rules->rules.hp_type == HPType::COMMON_HP)) {
int16_t team_hp[2] = {99, 99};
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (!ps) {
continue;
}
auto card = ps->get_sc_card();
if (card) {
team_hp[ps->get_team_id()] = std::min<int16_t>(team_hp[ps->get_team_id()], card->get_current_hp());
}
}
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (!ps) {
continue;
}
auto card = ps->get_sc_card();
if (card) {
int16_t this_team_hp = team_hp[ps->get_team_id()];
if (this_team_hp < 99) {
card->set_current_and_max_hp(this_team_hp);
}
}
}
}
this->map_and_rules->start_facing_directions = 0;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->set_initial_location();
}
}
this->determine_first_team_turn();
this->compute_all_map_occupied_bits();
for (size_t y = 0; y < 0x10; y++) {
for (size_t x = 0; x < 0x10; x++) {
if (this->map_and_rules->map.tiles[y][x] > 1) {
this->map_and_rules->map.tiles[y][x] = 1;
}
}
}
// Non-NTE:
// this->__unused6__ = 0;
// NTE:
// this->unknown_a1 = 0;
// this->unknown_a2 = 0;
for (size_t warp_type = 0; warp_type < 5; warp_type++) {
this->warp_positions[warp_type][0].clear(0xFF);
this->warp_positions[warp_type][1].clear(0xFF);
}
this->num_trap_tiles_nte = 0;
for (size_t z = 0; z < 0x10; z++) {
this->trap_tile_locs_nte[z].clear(0xFF);
}
for (size_t y = 0; y < 0x10; y++) {
for (size_t x = 0; x < 0x10; x++) {
uint8_t tile_spec = this->overlay_state.tiles[y][x];
uint8_t tile_type = tile_spec & 0xF0;
uint8_t tile_subtype = tile_spec & 0x0F;
if (tile_type == 0x30) {
if (this->warp_positions[tile_subtype][0][0] == 0xFF) {
this->warp_positions[tile_subtype][0][0] = x;
this->warp_positions[tile_subtype][0][1] = y;
} else if (this->warp_positions[tile_subtype][1][0] == 0xFF) {
this->warp_positions[tile_subtype][1][0] = x;
this->warp_positions[tile_subtype][1][1] = y;
}
} else if ((tile_type == 0x10) || (tile_type == 0x20) || (tile_type == 0x50)) {
this->map_and_rules->map.tiles[y][x] = 0;
} else if (is_nte && (tile_type == 0x40) && (this->num_trap_tiles_nte < 0x10)) {
this->trap_tile_locs_nte[this->num_trap_tiles_nte][0] = x;
this->trap_tile_locs_nte[this->num_trap_tiles_nte][1] = y;
this->num_trap_tiles_nte++;
}
}
}
if (!is_nte) {
for (size_t trap_type = 0; trap_type < 5; trap_type++) {
this->chosen_trap_tile_index_of_type[trap_type] = 0xFF;
size_t num_trap_tiles = 0;
for (size_t y = 0; y < 0x10; y++) {
for (size_t x = 0; x < 0x10; x++) {
if ((this->overlay_state.tiles[y][x] == (trap_type | 0x40)) && (num_trap_tiles < 8)) {
this->trap_tile_locs[trap_type][num_trap_tiles][0] = x;
this->trap_tile_locs[trap_type][num_trap_tiles][1] = y;
num_trap_tiles++;
}
}
}
this->num_trap_tiles_of_type[trap_type] = num_trap_tiles;
if (num_trap_tiles > 0) {
this->chosen_trap_tile_index_of_type[trap_type] = this->get_random(num_trap_tiles);
}
}
}
if (is_nte) {
this->send_all_state_updates();
this->send_6xB4x02_for_all_players_if_needed();
G_UpdateMap_Ep3NTE_6xB4x05 cmd;
cmd.state = *this->map_and_rules;
cmd.start_battle = 1;
this->send(cmd);
} else {
this->send_6xB4x02_for_all_players_if_needed(true);
this->send_6xB4x05();
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->send_set_card_updates(true);
}
}
this->send_all_state_updates();
this->send_6xB4x1C_names_update();
}
this->registration_phase = RegistrationPhase::BATTLE_STARTED;
this->update_battle_state_flags_and_send_6xB4x03_if_needed(true);
if (!is_nte) {
this->send_6xB4x50_trap_tile_locations();
G_UpdateMap_Ep3_6xB4x05 cmd;
cmd.state = *this->map_and_rules;
cmd.start_battle = 1;
this->send(cmd);
}
this->battle_start_usecs = phosg::now();
this->send_6xB4x46();
// Re-send game metadata to spectator teams, since loading the battle scene seems to delete it
auto l = this->lobby.lock();
if (l) {
send_ep3_update_game_metadata(l);
}
}
G_SetStateFlags_Ep3_6xB4x03 Server::prepare_6xB4x03() const {
G_SetStateFlags_Ep3_6xB4x03 cmd;
cmd.state.turn_num = this->round_num;
cmd.state.battle_phase = this->battle_phase;
cmd.state.current_team_turn1 = this->current_team_turn1;
cmd.state.current_team_turn2 = this->current_team_turn2;
cmd.state.action_subphase = this->action_subphase;
cmd.state.setup_phase = this->setup_phase;
cmd.state.registration_phase = this->registration_phase;
cmd.state.team_exp[0] = this->team_exp[0];
cmd.state.team_exp[1] = this->team_exp[1];
cmd.state.team_dice_bonus[0] = this->team_dice_bonus[0];
cmd.state.team_dice_bonus[1] = this->team_dice_bonus[1];
cmd.state.first_team_turn = this->first_team_turn;
cmd.state.tournament_flag = this->options.tournament ? 1 : 0;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
cmd.state.client_sc_card_types[z] = ps ? ps->get_sc_card_type() : CardType::INVALID_FF;
}
return cmd;
}
void Server::update_battle_state_flags_and_send_6xB4x03_if_needed(bool always_send) {
G_SetStateFlags_Ep3_6xB4x03 cmd = this->prepare_6xB4x03();
if (always_send || (*this->state_flags != cmd.state)) {
*this->state_flags = cmd.state;
this->send(cmd);
}
}
// Returns true if the battle can begin
bool Server::update_registration_phase() {
auto log = this->log_stack("update_registration_phase: ");
if (this->setup_phase != SetupPhase::REGISTRATION) {
log.debug_f("setup_phase is not REGISTRATION");
return false;
}
if (this->map_and_rules->num_players == 0) {
this->registration_phase = RegistrationPhase::AWAITING_NUM_PLAYERS;
log.debug_f("registration_phase set to AWAITING_NUM_PLAYERS");
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
return false;
}
if (this->map_and_rules->num_players != this->num_clients_present) {
this->registration_phase = RegistrationPhase::AWAITING_PLAYERS;
log.debug_f("registration_phase set to AWAITING_PLAYERS");
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
return false;
}
size_t num_team0_registered_players = 0;
for (size_t z = 0; z < 4; z++) {
if (this->deck_entries[z]->team_id == 0) {
num_team0_registered_players++;
}
}
if (num_team0_registered_players != this->map_and_rules->num_team0_players) {
this->registration_phase = RegistrationPhase::AWAITING_DECKS;
log.debug_f("registration_phase set to AWAITING_DECKS");
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
return false;
}
this->registration_phase = RegistrationPhase::REGISTERED;
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
log.debug_f("battle can begin");
return true;
}
const std::unordered_map<uint8_t, Server::handler_t> Server::subcommand_handlers{
{0x0B, &Server::handle_CAx0B_redraw_initial_hand},
{0x0C, &Server::handle_CAx0C_end_redraw_initial_hand_phase},
{0x0D, &Server::handle_CAx0D_end_non_action_phase},
{0x0E, &Server::handle_CAx0E_discard_card_from_hand},
{0x0F, &Server::handle_CAx0F_set_card_from_hand},
{0x10, &Server::handle_CAx10_move_fc_to_location},
{0x11, &Server::handle_CAx11_enqueue_attack_or_defense},
{0x12, &Server::handle_CAx12_end_attack_list},
{0x13, &Server::handle_CAx13_update_map_during_setup},
{0x14, &Server::handle_CAx14_update_deck_during_setup},
{0x15, &Server::handle_CAx15_unused_hard_reset_server_state},
{0x1B, &Server::handle_CAx1B_update_player_name},
{0x1D, &Server::handle_CAx1D_start_battle},
{0x21, &Server::handle_CAx21_end_battle},
{0x28, &Server::handle_CAx28_end_defense_list},
{0x2B, &Server::handle_CAx2B_legacy_set_card},
{0x34, &Server::handle_CAx34_subtract_ally_atk_points},
{0x37, &Server::handle_CAx37_client_ready_to_advance_from_starter_roll_phase},
{0x3A, &Server::handle_CAx3A_time_limit_expired},
{0x40, &Server::handle_CAx40_map_list_request},
{0x41, &Server::handle_CAx41_map_request},
{0x48, &Server::handle_CAx48_end_turn},
{0x49, &Server::handle_CAx49_card_counts},
};
void Server::on_server_data_input(std::shared_ptr<Client> sender_c, const std::string& data) {
auto header = check_size_t<G_CardBattleCommandHeader>(data, 0xFFFF);
size_t expected_size = header.size * 4;
if (expected_size < data.size()) {
phosg::print_data(stderr, data);
throw std::runtime_error(std::format(
"command is incomplete: expected {:X} bytes, received {:X} bytes", expected_size, data.size()));
}
if (header.subcommand != 0xB3) {
throw std::runtime_error("server data command is not 6xB3");
}
handler_t handler = nullptr;
try {
handler = this->subcommand_handlers.at(header.subsubcommand);
} catch (const std::out_of_range&) {
throw std::runtime_error("unknown CAx subsubcommand");
}
if (this->battle_record && this->battle_record->writable()) {
this->battle_record->add_command(BattleRecord::Event::Type::SERVER_DATA_COMMAND, data.data(), data.size());
}
if ((sender_c && (sender_c->version() == Version::GC_EP3_NTE)) || !header.mask_key) {
(this->*handler)(sender_c, data);
} else {
std::string unmasked_data = data;
set_mask_for_ep3_game_command(unmasked_data.data(), unmasked_data.size(), 0);
(this->*handler)(sender_c, unmasked_data);
}
}
void Server::handle_CAx0B_redraw_initial_hand(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_RedrawInitialHand_Ep3_CAx0B>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "REDRAW");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
int32_t error_code = 0;
if (this->setup_phase != SetupPhase::HAND_REDRAW_OPTION) {
error_code = -0x5D;
}
if (in_cmd.client_id >= 4) {
error_code = -0x78;
}
if (error_code == 0) {
auto ps = this->player_states.at(in_cmd.client_id);
if (!ps) {
error_code = -0x72;
} else {
ps->redraw_initial_hand();
}
}
if (!this->options.is_nte() || (error_code == 0)) {
G_ActionResult_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.header.sequence_num;
out_cmd.error_code = error_code;
this->send(out_cmd);
}
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, error_code);
}
void Server::handle_CAx0C_end_redraw_initial_hand_phase(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_EndInitialRedrawPhase_Ep3_CAx0C>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "HAND READY");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
int32_t error_code = 0;
if ((this->setup_phase != SetupPhase::HAND_REDRAW_OPTION) && (this->setup_phase != SetupPhase::STARTER_ROLLS)) {
error_code = -0x5D;
}
if (in_cmd.client_id > 4) {
error_code = -0x78;
}
if (this->options.is_nte() && error_code) {
return;
}
G_ActionResult_Ep3_6xB4x1E out_cmd_ack;
out_cmd_ack.sequence_num = in_cmd.header.sequence_num;
out_cmd_ack.response_phase = 1;
this->send(out_cmd_ack);
if (error_code == 0) {
auto ps = this->player_states.at(in_cmd.client_id);
if (!ps) {
error_code = -0x72;
} else {
this->clients_done_in_redraw_initial_hand_phase[in_cmd.client_id] = true;
ps->assist_flags |= AssistFlag::READY_TO_END_PHASE;
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
bool all_clients_ready = true;
for (size_t z = 0; z < 4; z++) {
if (this->player_states[z] && !this->clients_done_in_redraw_initial_hand_phase[z]) {
all_clients_ready = false;
break;
}
}
if (all_clients_ready) {
this->set_battle_started();
}
}
}
if (!this->options.is_nte() || !error_code) {
G_ActionResult_Ep3_6xB4x1E out_cmd_fin;
out_cmd_fin.sequence_num = in_cmd.header.sequence_num;
out_cmd_fin.response_phase = 2;
out_cmd_fin.error_code = error_code;
this->send(out_cmd_fin);
}
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, error_code);
}
void Server::handle_CAx0D_end_non_action_phase(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_EndNonAttackPhase_Ep3_CAx0D>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "END PHASE");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
if (!this->options.is_nte()) {
G_ActionResult_Ep3_6xB4x1E out_cmd_ack;
out_cmd_ack.sequence_num = in_cmd.header.sequence_num;
out_cmd_ack.response_phase = 1;
this->send(out_cmd_ack);
}
this->set_client_id_ready_to_advance_phase(in_cmd.client_id, static_cast<BattlePhase>(in_cmd.battle_phase.load()));
G_ActionResult_Ep3_6xB4x1E out_cmd_fin;
out_cmd_fin.sequence_num = in_cmd.header.sequence_num;
out_cmd_fin.response_phase = this->options.is_nte() ? 0 : 2;
this->send(out_cmd_fin);
}
void Server::handle_CAx0E_discard_card_from_hand(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_DiscardCardFromHand_Ep3_CAx0E>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "DISCARD");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
int32_t error_code = 0;
if (this->setup_phase != SetupPhase::MAIN_BATTLE) {
error_code = -0x5D;
}
if (this->battle_phase != BattlePhase::DRAW) {
error_code = -0x5D;
}
if (in_cmd.client_id >= 4) {
error_code = -0x78;
}
if (error_code == 0) {
auto ps = this->player_states.at(in_cmd.client_id);
if (!ps) {
error_code = -0x72;
} else if (!(ps->assist_flags & AssistFlag::IS_SKIPPING_TURN)) {
error_code = ps->discard_ref_from_hand(in_cmd.card_ref) ? 0 : 1;
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
} else {
error_code = -0x70;
}
}
if (!this->options.is_nte() || (error_code == 0)) {
G_ActionResult_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.header.sequence_num;
out_cmd.error_code = error_code;
this->send(out_cmd);
}
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, error_code);
}
void Server::handle_CAx0F_set_card_from_hand(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_SetCardFromHand_Ep3_CAx0F>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "SET FC");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
int32_t error_code = 0;
if (this->setup_phase != SetupPhase::MAIN_BATTLE) {
error_code = -0x5D;
}
if (this->battle_phase != BattlePhase::SET) {
error_code = -0x5D;
}
if (in_cmd.card_ref == 0xFFFF) {
error_code = -0x78;
}
bool was_set = false;
if (in_cmd.client_id >= 4) {
error_code = -0x78;
}
bool always_send_response = (error_code == 0);
if (always_send_response) {
this->ruler_server->error_code1 = 0;
auto ps = this->player_states.at(in_cmd.client_id);
if (!ps) {
this->ruler_server->error_code1 = -0x72;
} else {
was_set = ps->set_card_from_hand(in_cmd.card_ref, in_cmd.set_index, &in_cmd.loc, in_cmd.assist_target_player, 0);
}
} else {
this->ruler_server->error_code1 = error_code;
}
if (!this->options.is_nte() || always_send_response) {
G_ActionResult_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.header.sequence_num;
out_cmd.error_code = this->options.is_nte() ? (was_set ? 0 : 1) : this->ruler_server->error_code1;
this->send(out_cmd);
}
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, this->ruler_server->error_code1);
}
void Server::handle_CAx10_move_fc_to_location(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_MoveFieldCharacter_Ep3_CAx10>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "MOVE");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
int32_t error_code = 0;
if (this->setup_phase != SetupPhase::MAIN_BATTLE) {
error_code = -0x5D;
}
if (this->battle_phase != BattlePhase::MOVE) {
error_code = -0x5D;
}
if (in_cmd.client_id >= 4) {
error_code = -0x78;
}
if (error_code == 0) {
auto ps = this->player_states.at(in_cmd.client_id);
if (!ps) {
this->ruler_server->error_code2 = -0x72;
} else {
this->ruler_server->error_code2 = 0;
ps->move_card_to_location_by_card_index(in_cmd.set_index, in_cmd.loc);
}
} else {
this->ruler_server->error_code2 = error_code;
}
if (!this->options.is_nte() || (this->ruler_server->error_code2 == 0)) {
G_ActionResult_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.header.sequence_num;
out_cmd.error_code = this->options.is_nte() ? 0 : this->ruler_server->error_code2;
this->send(out_cmd);
}
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, this->ruler_server->error_code2);
}
void Server::handle_CAx11_enqueue_attack_or_defense(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_EnqueueAttackOrDefense_Ep3_CAx11>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "ENQUEUE ACT");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
int32_t error_code = 0;
if (this->setup_phase != SetupPhase::MAIN_BATTLE) {
error_code = -0x5D;
}
if (this->battle_phase != BattlePhase::ACTION) {
error_code = -0x5D;
}
if (error_code == 0) {
this->ruler_server->error_code3 = 0;
ActionState pa = in_cmd.entry;
if (this->enqueue_attack_or_defense(in_cmd.client_id, &pa)) {
G_SetActionState_Ep3_6xB4x09 out_cmd;
out_cmd.client_id = in_cmd.client_id;
out_cmd.state = in_cmd.entry;
this->send(out_cmd);
}
} else {
this->ruler_server->error_code3 = error_code;
}
bool is_nte = this->options.is_nte();
if (!is_nte || (error_code == 0)) {
G_ActionResult_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.header.sequence_num;
out_cmd.error_code = is_nte ? !!this->ruler_server->error_code3 : this->ruler_server->error_code3;
this->send(out_cmd);
}
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, this->ruler_server->error_code3);
}
void Server::handle_CAx12_end_attack_list(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_EndAttackList_Ep3_CAx12>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "END ATK LIST");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
int32_t error_code = 0;
if (this->setup_phase != SetupPhase::MAIN_BATTLE) {
error_code = -0x5D;
}
if (error_code == 0) {
this->end_attack_list_for_client(in_cmd.client_id);
}
if (!this->options.is_nte() || (error_code == 0)) {
G_ActionResult_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.header.sequence_num;
this->send(out_cmd);
}
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, error_code);
}
template <typename CmdT>
void Server::handle_CAx13_update_map_during_setup_t(std::shared_ptr<Client> c, const std::string& data) {
const auto& in_cmd = check_size_t<CmdT>(data);
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "UPDATE MAP");
if (!this->battle_in_progress &&
(this->setup_phase == SetupPhase::REGISTRATION) &&
(this->map_and_rules->num_players == 0) &&
(this->registration_phase != RegistrationPhase::REGISTERED) &&
(this->registration_phase != RegistrationPhase::BATTLE_STARTED)) {
*this->map_and_rules = in_cmd.map_and_rules_state;
// The client will likely send incorrect values for the extended rules (or in the case of NTE, no values at all,
// since the Rules structure is smaller). So, use the values from the last chosen map if applicable, or the values
// from the $dicerange command if available.
Language language = c ? c->language() : Language::ENGLISH;
const Rules* map_rules = this->last_chosen_map ? &this->last_chosen_map->version(language)->map->default_rules : nullptr;
auto& server_rules = this->map_and_rules->rules;
// NTE can specify the DEF dice value range in its Rules struct, so we use that unless the map or $dicerange
// overrides it.
server_rules.def_dice_value_range = (map_rules && (map_rules->def_dice_value_range != 0xFF))
? map_rules->def_dice_value_range
: (this->def_dice_value_range_override != 0xFF)
? this->def_dice_value_range_override
: this->options.is_nte()
? server_rules.def_dice_value_range
: 0;
server_rules.atk_dice_value_range_2v1 = (map_rules && (map_rules->atk_dice_value_range_2v1 != 0xFF))
? map_rules->atk_dice_value_range_2v1
: (this->atk_dice_value_range_2v1_override != 0xFF)
? this->atk_dice_value_range_2v1_override
: 0;
server_rules.def_dice_value_range_2v1 = (map_rules && (map_rules->def_dice_value_range_2v1 != 0xFF))
? map_rules->def_dice_value_range_2v1
: (this->def_dice_value_range_2v1_override != 0xFF)
? this->def_dice_value_range_2v1_override
: 0;
// If this match is part of a tournament, ignore the rules sent by the client and use the tournament rules instead.
if (this->options.tournament) {
this->map_and_rules->rules = this->options.tournament->get_rules();
}
if (this->override_environment_number != 0xFF) {
this->map_and_rules->environment_number = this->override_environment_number;
this->override_environment_number = 0xFF;
}
this->overlay_state = in_cmd.overlay_state;
if (this->options.behavior_flags & BehaviorFlag::DISABLE_TIME_LIMITS) {
this->map_and_rules->rules.overall_time_limit = 0;
this->map_and_rules->rules.phase_time_limit = 0;
}
if (this->map_and_rules->rules.check_invalid_fields()) {
this->map_and_rules->rules.check_and_reset_invalid_fields();
}
if (this->map_and_rules->num_players_per_team == 0) {
this->map_and_rules->num_players_per_team = this->map_and_rules->num_players >> 1;
}
this->update_registration_phase();
}
}
void Server::handle_CAx13_update_map_during_setup(std::shared_ptr<Client> c, const std::string& data) {
if (this->options.is_nte()) {
this->handle_CAx13_update_map_during_setup_t<G_SetMapState_Ep3NTE_CAx13>(c, data);
} else {
this->handle_CAx13_update_map_during_setup_t<G_SetMapState_Ep3_CAx13>(c, data);
}
}
void Server::handle_CAx14_update_deck_during_setup(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_SetPlayerDeck_Ep3_CAx14>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "UPDATE DECK");
if (!this->battle_in_progress) {
if ((this->setup_phase == SetupPhase::REGISTRATION) &&
(this->registration_phase != RegistrationPhase::REGISTERED) &&
(this->registration_phase != RegistrationPhase::BATTLE_STARTED)) {
if (in_cmd.client_id >= 4) {
return;
}
DeckEntry entry = in_cmd.entry;
int32_t verify_error = 0;
if (!(this->options.behavior_flags & BehaviorFlag::SKIP_DECK_VERIFY)) {
// Note: Sega's original implementation doesn't use the card counts here
if (this->options.behavior_flags & BehaviorFlag::IGNORE_CARD_COUNTS) {
verify_error = this->ruler_server->verify_deck(entry.card_ids);
} else {
verify_error = this->ruler_server->verify_deck(entry.card_ids,
&this->client_card_counts[in_cmd.client_id]);
}
}
if (verify_error) {
throw std::runtime_error(std::format("invalid deck: -0x{:X}", verify_error));
}
if (!this->options.is_nte() && !(this->options.behavior_flags & BehaviorFlag::SKIP_D1_D2_REPLACE)) {
this->ruler_server->replace_D1_D2_rank_cards_with_Attack(entry.card_ids);
}
*this->deck_entries[in_cmd.client_id] = in_cmd.entry;
this->presence_entries[in_cmd.client_id].player_present = true;
this->presence_entries[in_cmd.client_id].is_cpu_player = in_cmd.is_cpu_player;
this->set_player_deck_valid(in_cmd.client_id);
}
this->num_clients_present = 0;
for (size_t z = 0; z < 4; z++) {
if (this->check_presence_entry(z)) {
this->num_clients_present++;
}
}
this->send_all_state_updates();
this->update_registration_phase();
}
}
void Server::handle_CAx15_unused_hard_reset_server_state(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_HardResetServerState_Ep3_CAx15>(data);
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "HARD RESET");
// In the original implementation, this command recreates the server object. This is possible because the dispatch
// function is not part of the server object in the original implementation; however, in our implementation, it is,
// so we don't support this. The original implementation did this:
// this->base()->recreate_server(); // Destroys *this, which we can't do
// root_card_server = this->server;
// *this->map_and_rules = *this->initial_map_and_rules;
// this->send_all_state_updates();
// this->update_registration_phase();
// this->setup_and_start_battle();
throw std::runtime_error("hard reset command received");
}
void Server::handle_CAx1B_update_player_name(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_SetPlayerName_Ep3_CAx1B>(data);
this->send_debug_command_received_message(in_cmd.entry.client_id, in_cmd.header.subsubcommand, "UPDATE NAME");
if (in_cmd.entry.client_id < 4) {
if (!this->is_registration_complete()) {
this->name_entries[in_cmd.entry.client_id] = in_cmd.entry;
this->name_entries_valid[in_cmd.entry.client_id] = false;
}
// Note: This check is not part of the original code. This replaces a disconnecting player with a CPU if the battle
// is in progress.
auto l = this->lobby.lock();
if (l && !l->clients[in_cmd.entry.client_id]) {
this->name_entries[in_cmd.entry.client_id].is_cpu_player = 1;
this->presence_entries[in_cmd.entry.client_id].is_cpu_player = 1;
auto ps = this->player_states[in_cmd.entry.client_id];
if (ps && ps->hand_and_equip && !ps->hand_and_equip->is_cpu_player) {
ps->hand_and_equip->is_cpu_player = 1;
this->send_6xB4x02_for_all_players_if_needed();
}
}
}
G_SetPlayerNames_Ep3_6xB4x1C out_cmd;
for (size_t z = 0; z < 4; z++) {
out_cmd.entries[z] = this->name_entries[z];
}
this->send(out_cmd);
}
void Server::handle_CAx1D_start_battle(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_StartBattle_Ep3_CAx1D>(data);
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "START BATTLE");
if (!this->battle_in_progress) {
bool is_nte = this->options.is_nte();
bool should_start = false;
if (is_nte) {
should_start = (this->registration_phase == RegistrationPhase::REGISTERED);
} else if (this->update_registration_phase()) {
should_start = true;
} else {
G_RejectBattleStartRequest_Ep3_6xB4x53 out_cmd;
out_cmd.setup_phase = this->setup_phase;
out_cmd.registration_phase = this->registration_phase;
out_cmd.state = *this->map_and_rules;
this->send(out_cmd);
for (size_t z = 0; z < 4; z++) {
this->deck_entries[z]->clear();
this->presence_entries[z].clear();
}
this->battle_in_progress = false;
}
if (should_start) {
if (this->battle_record && this->battle_record->writable()) {
this->battle_record->set_battle_start_timestamp();
}
auto l = this->lobby.lock();
if (l) {
// Note: Sega's implementation doesn't set EX results values here; they did it at game join time instead. We do
// it here for code simplicity.
if (!this->options.is_nte() && l->ep3_ex_result_values) {
this->send(*l->ep3_ex_result_values);
}
}
this->setup_and_start_battle();
this->battle_in_progress = true;
}
}
}
void Server::handle_CAx21_end_battle(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_EndBattle_Ep3_CAx21>(data);
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "END BATTLE");
if (this->setup_phase == SetupPhase::BATTLE_ENDED) {
this->battle_finished = true;
// This logic isn't part of the original implementation.
auto l = this->lobby.lock();
if (l) {
send_ep3_disband_watcher_lobbies(l);
}
}
}
void Server::handle_CAx28_end_defense_list(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_EndDefenseList_Ep3_CAx28>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "END DEF LIST");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
G_ActionResult_Ep3_6xB4x1E out_cmd_ack;
out_cmd_ack.sequence_num = in_cmd.header.sequence_num;
out_cmd_ack.response_phase = 1;
this->send(out_cmd_ack);
this->defense_list_ended_for_client[in_cmd.client_id] = 1;
bool all_defense_lists_ended = true;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps && (this->current_team_turn1 != ps->get_team_id())) {
if (!ps->get_sc_card()->check_card_flag(2) && (this->defense_list_ended_for_client[z] == 0)) {
all_defense_lists_ended = false;
break;
}
}
}
if (all_defense_lists_ended && (this->unknown_a10 == 0)) {
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->assist_flags &= (~AssistFlag::READY_TO_END_ACTION_PHASE);
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
}
this->unknown_8023EE48();
this->unknown_a10 = 1;
} else {
auto ps = this->player_states[in_cmd.client_id];
ps->assist_flags |= AssistFlag::READY_TO_END_ACTION_PHASE;
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
if (this->unknown_a10 != 0) {
this->unknown_8023EE80();
this->unknown_a10 = 0;
}
G_ActionResult_Ep3_6xB4x1E out_cmd_fin;
out_cmd_fin.sequence_num = in_cmd.header.sequence_num;
out_cmd_fin.response_phase = 2;
this->send(out_cmd_fin);
}
void Server::handle_CAx2B_legacy_set_card(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_ExecLegacyCard_Ep3_CAx2B>(data);
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "EXEC LEGACY");
// Sega's original implementation does nothing here, so we do nothing as well.
}
void Server::handle_CAx34_subtract_ally_atk_points(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_PhotonBlastRequest_Ep3_CAx34>(data);
uint8_t card_ref_client_id = client_id_for_card_ref(in_cmd.card_ref);
this->send_debug_command_received_message(card_ref_client_id, in_cmd.header.subsubcommand, "SUB ALLY ATK");
if (card_ref_client_id >= 4) {
return;
}
auto this_ps = this->player_states[card_ref_client_id];
if (this_ps && (in_cmd.ally_client_id < 4)) {
auto ally_ps = this->player_states[in_cmd.ally_client_id];
if (ally_ps && (this->has_done_pb[card_ref_client_id])) {
if (in_cmd.reason == 0) {
this->has_done_pb_with_client[card_ref_client_id][in_cmd.ally_client_id] = true;
bool accepted = true;
for (size_t z = 0; z < 4; z++) {
auto ally_ps = this->get_player_state(z);
if ((z != card_ref_client_id) && ally_ps) {
if (this_ps->get_team_id() == ally_ps->get_team_id()) {
if (this->has_done_pb_with_client[card_ref_client_id][z] == 0) {
accepted = false;
}
break;
}
}
}
if (accepted) {
G_PhotonBlastStatus_Ep3_6xB4x35 out_cmd;
out_cmd.accepted = 0;
out_cmd.card_ref = in_cmd.card_ref;
out_cmd.client_id = card_ref_client_id;
this->send(out_cmd);
}
} else {
auto ce = this->definition_for_card_ref(in_cmd.card_ref);
if (ce->def.ally_cost <= ally_ps->get_atk_points()) {
auto& pa = this->pb_action_states[card_ref_client_id];
if (this->num_pending_attacks < 0x20) {
this->num_pending_attacks++;
this->pending_attacks[this->num_pending_attacks] = pa;
this_ps->set_action_cards_for_action_state(pa);
ally_ps->subtract_atk_points(ce->def.ally_cost);
if (ce->def.ally_cost > 0) {
ally_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
auto attacker_card = this->card_for_set_card_ref(pa.attacker_card_ref);
if (attacker_card) {
attacker_card->card_flags |= 0x400;
attacker_card->player_state()->send_6xB4x04_if_needed();
}
uint16_t card_ref = this->send_6xB4x06_if_card_ref_invalid(pa.original_attacker_card_ref, 9);
auto orig_attacker_card = this->card_for_set_card_ref(card_ref);
auto target_card = this->card_for_set_card_ref(pa.target_card_refs[0]);
if (orig_attacker_card && target_card) {
target_card->card_flags |= 0x800;
target_card->player_state()->send_6xB4x04_if_needed();
}
this->has_done_pb[card_ref_client_id] = false;
G_PhotonBlastStatus_Ep3_6xB4x35 out_cmd;
out_cmd.client_id = card_ref_client_id;
out_cmd.accepted = 1;
out_cmd.card_ref = in_cmd.card_ref;
this->send(out_cmd);
}
}
}
}
}
}
void Server::handle_CAx37_client_ready_to_advance_from_starter_roll_phase(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_AdvanceFromStartingRollsPhase_Ep3_CAx37>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "CHOOSE ORDER");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
auto ps = this->player_states[in_cmd.client_id];
if (ps) {
ps->assist_flags |= AssistFlag::READY_TO_END_STARTER_ROLL_PHASE;
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
if (this->setup_phase == SetupPhase::STARTER_ROLLS) {
bool all_clients_ready = true;
for (size_t z = 0; z < 4; z++) {
auto other_ps = this->player_states[z];
if (other_ps && !(other_ps->assist_flags & AssistFlag::READY_TO_END_STARTER_ROLL_PHASE)) {
all_clients_ready = false;
break;
}
}
if (all_clients_ready) {
this->setup_phase = SetupPhase::HAND_REDRAW_OPTION;
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
}
}
}
void Server::handle_CAx3A_time_limit_expired(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_OverallTimeLimitExpired_Ep3_CAx3A>(data);
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "TIME EXPIRED");
// We don't need to do anything here because the overall time limit is tracked server-side instead.
}
void Server::handle_CAx40_map_list_request(std::shared_ptr<Client> sender_c, const std::string& data) {
const auto& in_cmd = check_size_t<G_MapListRequest_Ep3_CAx40>(data);
this->send_debug_command_received_message(in_cmd.header.subsubcommand, "MAP LIST");
auto l = this->lobby.lock();
if (!l) {
throw std::runtime_error("lobby is deleted");
}
size_t num_players = l ? l->count_clients() : 1;
Language language = sender_c ? sender_c->language() : Language::ENGLISH;
const auto& list_data = this->options.map_index->get_compressed_list(num_players, language, this->options.is_nte());
phosg::StringWriter w;
uint32_t subcommand_size = (list_data.size() + sizeof(G_MapList_Ep3_6xB6x40) + 3) & (~3);
w.put<G_MapList_Ep3_6xB6x40>(G_MapList_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, list_data.size(), 0});
w.write(list_data);
while (w.size() & 3) {
w.put_u8(0);
}
const auto& out_data = w.str();
this->send(out_data.data(), out_data.size(), 0x6C, false);
}
void Server::send_6xB6x41_to_all_clients() const {
if (!this->last_chosen_map) {
throw std::logic_error("cannot send 6xB4x41 without a map chosen");
}
auto l = this->lobby.lock();
if (l) {
std::vector<std::string> map_commands_by_language;
auto send_to_client = [&](std::shared_ptr<Client> c) -> void {
if (!c) {
return;
}
size_t lang_index = static_cast<size_t>(c->language());
if (map_commands_by_language.size() <= lang_index) {
map_commands_by_language.resize(lang_index + 1);
}
if (map_commands_by_language[lang_index].empty()) {
map_commands_by_language[lang_index] = this->prepare_6xB6x41_map_definition(
this->last_chosen_map, c->language(), this->options.is_nte());
}
this->log().info_f("Sending {} version of map {:08X}", name_for_language(c->language()), this->last_chosen_map->map_number);
send_command(c, 0x6C, 0x00, map_commands_by_language[lang_index]);
};
for (const auto& c : l->clients) {
send_to_client(c);
}
for (auto watcher_l : l->watcher_lobbies) {
for (const auto& c : watcher_l->clients) {
send_to_client(c);
}
}
if (this->battle_record && this->battle_record->writable()) {
// TODO: It's not great that we just pick the first one; ideally we'd put all of them in the recording and send
// the appropriate one to the client in the playback lobby
for (std::string& data : map_commands_by_language) {
if (!data.empty()) {
this->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data));
break;
}
}
}
} else {
auto out_data = this->prepare_6xB6x41_map_definition(this->last_chosen_map, Language::ENGLISH, false);
this->send(out_data.data(), out_data.size(), 0x6C, false);
}
}
void Server::handle_CAx41_map_request(std::shared_ptr<Client>, const std::string& data) {
const auto& cmd = check_size_t<G_MapDataRequest_Ep3_CAx41>(data);
this->send_debug_command_received_message(cmd.header.subsubcommand, "MAP DATA");
if (!this->options.tournament || (this->options.tournament->get_map()->map_number == cmd.map_number)) {
this->last_chosen_map = this->options.map_index->map_for_id(cmd.map_number);
this->send_6xB6x41_to_all_clients();
}
}
void Server::handle_CAx48_end_turn(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_EndTurn_Ep3_CAx48>(data);
this->send_debug_command_received_message(in_cmd.client_id, in_cmd.header.subsubcommand, "END TURN");
if (in_cmd.client_id >= 4) {
throw std::runtime_error("invalid client ID");
}
auto ps = this->get_player_state(in_cmd.client_id);
if (ps && ps->draw_cards_allowed()) {
ps->draw_hand(0);
}
G_ActionResult_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.header.sequence_num;
this->send(out_cmd);
}
void Server::handle_CAx49_card_counts(std::shared_ptr<Client>, const std::string& data) {
const auto& in_cmd = check_size_t<G_CardCounts_Ep3_CAx49>(data);
this->send_debug_command_received_message(in_cmd.header.sender_client_id, in_cmd.header.subsubcommand, "CARD COUNTS");
// Note: Sega's implmentation completely ignores this command. This implementation is not based on the original code.
auto& dest_counts = this->client_card_counts[in_cmd.header.sender_client_id];
dest_counts = in_cmd.card_id_to_count;
decrypt_trivial_gci_data(dest_counts.data(), dest_counts.bytes(), in_cmd.basis);
}
void Server::compute_losing_team_id_and_add_winner_flags(uint32_t flags) {
bool is_nte = this->options.is_nte();
if (!is_nte) {
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->assist_flags &= ~(AssistFlag::HAS_WON_BATTLE |
AssistFlag::WINNER_DECIDED_BY_DEFEAT |
AssistFlag::BATTLE_DID_NOT_END_DUE_TO_TIME_LIMIT);
}
}
}
uint32_t winner_flags = flags | AssistFlag::HAS_WON_BATTLE | AssistFlag::WINNER_DECIDED_BY_DEFEAT;
int8_t losing_team_id = -1;
std::array<uint32_t, 2> team_counts{0, 0};
if (!is_nte) {
// First, check which team has more dead SCs
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
auto sc_card = ps->get_sc_card();
if (sc_card && (sc_card->card_flags & 2)) {
team_counts.at(ps->get_team_id())++;
}
}
}
if (team_counts[1] < team_counts[0]) {
losing_team_id = 0;
} else if (team_counts[0] < team_counts[1]) {
losing_team_id = 1;
}
// If the SC counts match, break ties by remaining SC HP
if (losing_team_id == -1) {
team_counts[0] = 0;
team_counts[1] = 0;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
auto sc_card = ps->get_sc_card();
if (sc_card) {
team_counts.at(ps->get_team_id()) += sc_card->get_current_hp();
}
}
}
if (team_counts[0] < team_counts[1]) {
losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) {
losing_team_id = 1;
}
}
}
// If still tied, break ties by number of opponent cards destroyed
if (losing_team_id == -1) {
team_counts[0] = 0;
team_counts[1] = 0;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
team_counts.at(ps->get_team_id()) += ps->stats.num_opponent_cards_destroyed;
}
}
if (team_counts[0] < team_counts[1]) {
losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) {
losing_team_id = 1;
}
}
// If still tied, break ties by amount of damage given
if (losing_team_id == -1) {
team_counts[0] = 0;
team_counts[1] = 0;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
team_counts.at(ps->get_team_id()) += ps->stats.damage_given;
}
}
if (team_counts[0] < team_counts[1]) {
losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) {
losing_team_id = 1;
}
}
// If STILL tied, roll dice and arbitrarily make one team the winner
if (losing_team_id == -1) {
while (losing_team_id == -1) {
team_counts[1] = 0;
team_counts[0] = 0;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
team_counts.at(ps->get_team_id()) += ps->roll_dice(1);
}
}
team_counts[0] *= this->team_client_count[1];
team_counts[1] *= this->team_client_count[0];
if (team_counts[0] < team_counts[1]) {
losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) {
losing_team_id = 1;
}
}
winner_flags = flags | AssistFlag::HAS_WON_BATTLE | AssistFlag::WINNER_DECIDED_BY_RANDOM;
}
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
if (losing_team_id != ps->get_team_id()) {
ps->assist_flags |= winner_flags;
}
if (!is_nte || (losing_team_id != ps->get_team_id())) {
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
}
}
}
}
uint32_t Server::get_team_exp(uint8_t team_id) const {
return this->team_exp[team_id];
}
uint32_t Server::send_6xB4x06_if_card_ref_invalid(uint16_t card_ref, int16_t negative_value) {
if (this->card_special) {
return this->card_special->send_6xB4x06_if_card_ref_invalid(card_ref, -negative_value);
}
return card_ref;
}
void Server::unknown_8023EEF4() {
auto log = this->log_stack("unknown_8023EEF4: ");
if (this->unknown_a14 >= 0x20) {
log.debug_f("unknown_a14 too large (0x{:X})", this->unknown_a14);
return;
}
bool is_nte = this->options.is_nte();
while (this->unknown_a14 < this->num_pending_attacks_with_cards) {
auto card = this->attack_cards[this->unknown_a14];
if (this->get_current_team_turn() == card->get_team_id()) {
ActionState as = this->pending_attacks_with_cards[this->unknown_a14];
if (log.should_log(phosg::LogLevel::L_DEBUG)) {
log.debug_f("card @{:04X} #{:04X} can attack", card->get_card_ref(), card->get_card_id());
log.debug_f("as: {}", as.str(this->shared_from_this()));
}
if (is_nte) {
this->replace_targets_due_to_destruction_nte(&as);
} else {
this->replace_targets_due_to_destruction_or_conditions(&as);
}
if (log.should_log(phosg::LogLevel::L_DEBUG)) {
log.debug_f("as after target replacement: {}", as.str(this->shared_from_this()));
}
if (this->any_target_exists_for_attack(as)) {
log.debug_f("as is valid");
break;
} else {
log.debug_f("as is not valid");
}
} else {
log.debug_f("card @{:04X} #{:04X} cannot attack (wrong turn)", card->get_card_ref(), card->get_card_id());
}
this->unknown_a14++;
}
if (this->unknown_a14 < this->num_pending_attacks_with_cards) {
log.debug_f("a14 ({}) < num_pending_attacks_with_cards ({})", this->unknown_a14, this->num_pending_attacks_with_cards);
this->defense_list_ended_for_client.clear(false);
G_UpdateAttackTargets_Ep3_6xB4x29 cmd;
cmd.attack_number = this->unknown_a14;
cmd.state = this->pending_attacks_with_cards[this->unknown_a14];
if (is_nte) {
this->replace_targets_due_to_destruction_nte(&cmd.state);
} else {
this->replace_targets_due_to_destruction_or_conditions(&cmd.state);
}
ActionState as = cmd.state;
this->send(cmd);
this->card_special->apply_effects_after_attack_target_resolution(as);
if (!is_nte) {
this->attack_cards[this->unknown_a14]->compute_action_chain_results(1, 0);
this->attack_cards[this->unknown_a14]->unknown_80236374(this->attack_cards[this->unknown_a14], &as);
if (!this->attack_cards[this->unknown_a14]->action_chain.check_flag(0x40)) {
this->card_special->unknown_8024945C(this->attack_cards[this->unknown_a14], &as);
}
this->attack_cards[this->unknown_a14]->compute_action_chain_results(1, 0);
this->attack_cards[this->unknown_a14]->unknown_80236374(this->attack_cards[this->unknown_a14], &as);
if (!this->attack_cards[this->unknown_a14]->action_chain.check_flag(0x40)) {
this->card_special->unknown_8024966C(this->attack_cards[this->unknown_a14], &as);
}
this->attack_cards[this->unknown_a14]->compute_action_chain_results(1, 0);
this->attack_cards[this->unknown_a14]->unknown_80236374(this->attack_cards[this->unknown_a14], &as);
this->attack_cards[this->unknown_a14]->send_6xB4x4E_4C_4D_if_needed();
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->send_set_card_updates();
}
}
}
} else {
this->unknown_a15 = 0;
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps && (this->current_team_turn1 == ps->get_team_id())) {
this->set_client_id_ready_to_advance_phase(z, this->battle_phase);
}
}
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
this->send_6xB4x02_for_all_players_if_needed();
}
if (!is_nte) {
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
this->send_6xB4x02_for_all_players_if_needed();
}
}
void Server::execute_bomb_assist_effect() {
int16_t max_hp = -999;
int16_t min_hp = 999;
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps && !this->assist_server->should_block_assist_effects_for_client(client_id)) {
for (size_t set_index = 0; set_index < 8; set_index++) {
auto card = ps->get_set_card(set_index);
if (card && !(card->card_flags & 2)) {
max_hp = std::max<int16_t>(max_hp, card->get_current_hp());
min_hp = std::min<int16_t>(min_hp, card->get_current_hp());
}
}
}
}
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
// Possible bug: shouldn't we check should_block_assist_effects_for_client here too? If the player has a card with
// the same HP as another one that would be destroyed, it looks like the card can be destroyed even if the client
// should be immune to assist effects here.
if (ps) {
for (size_t set_index = 0; set_index < 8; set_index++) {
auto card = ps->get_set_card(set_index);
if (card && !(card->card_flags & 2) &&
((card->get_current_hp() == max_hp) || (card->get_current_hp() == min_hp))) {
card->player_state()->handle_homesick_assist_effect_from_bomb(card);
}
}
}
}
}
void Server::replace_targets_due_to_destruction_nte(ActionState* as) {
auto attacker_card = this->card_for_set_card_ref(this->send_6xB4x06_if_card_ref_invalid(as->attacker_card_ref, 3));
for (size_t z = 0; z < 0x24; z++) {
auto target_card = this->card_for_set_card_ref(as->target_card_refs[z]);
if (!target_card) {
break;
}
if (!(target_card->card_flags & 2) ||
(target_card->get_definition()->def.type != CardType::ITEM) ||
attacker_card->action_chain.check_flag(0x02)) {
continue;
}
auto ps = target_card->player_state();
std::shared_ptr<Card> found_guard_item;
for (size_t z = 0; z < 8; z++) {
auto set_card = ps->get_set_card(z);
if (set_card && (set_card != target_card) && !(set_card->card_flags & 2) && set_card->is_guard_item()) {
found_guard_item = set_card;
break;
}
}
auto replaced_target = found_guard_item;
if (!found_guard_item) {
for (size_t z = 0; z < 8; z++) {
auto set_card = ps->get_set_card(z);
if (set_card && (set_card != target_card) && !(set_card->card_flags & 2)) {
replaced_target = set_card;
break;
}
}
}
as->target_card_refs[z] = replaced_target ? replaced_target->get_card_ref() : ps->get_sc_card()->get_card_ref();
}
size_t write_offset = 0;
for (size_t z = 0; z < as->target_card_refs.size(); z++) {
if (as->target_card_refs[z] != 0xFFFF) {
if (z != write_offset) {
as->target_card_refs[write_offset] = as->target_card_refs[z];
}
write_offset++;
}
}
as->target_card_refs.clear_after(write_offset, 0xFFFF);
}
void Server::replace_targets_due_to_destruction_or_conditions(ActionState* as) {
auto attacker_card = this->card_for_set_card_ref(this->send_6xB4x06_if_card_ref_invalid(as->attacker_card_ref, 3));
if (!attacker_card) {
as->target_card_refs[0] = 0xFFFF;
return;
}
std::vector<uint16_t> phase1_replaced_card_refs;
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->get_player_state(client_id);
if (!attacker_card->action_chain.check_flag(0x200 << client_id)) {
for (size_t target_index = 0; target_index < 4 * 9; target_index++) {
uint32_t target_card_ref = as->target_card_refs[target_index];
if (target_card_ref == 0xFFFF) {
break;
}
if (client_id == client_id_for_card_ref(target_card_ref)) {
auto target_card = this->card_for_set_card_ref(this->send_6xB4x06_if_card_ref_invalid(target_card_ref, 5));
auto ce = this->definition_for_card_ref(target_card_ref);
if (ce && ps) {
if (!target_card || (target_card->card_flags & 2)) {
if (ce->def.type == CardType::ITEM) {
for (size_t set_index = 0; set_index < 8; set_index++) {
target_card = ps->get_set_card(set_index);
if (target_card &&
(target_card->get_card_ref() != target_card_ref) &&
!(target_card->card_flags & 2) &&
target_card->is_guard_item()) {
break;
}
target_card = nullptr;
}
auto replaced_target_card = target_card;
if (!target_card) {
for (size_t set_index = 0; set_index < 8; set_index++) {
replaced_target_card = ps->get_set_card(set_index);
if (replaced_target_card &&
(replaced_target_card->get_card_ref() != target_card_ref) &&
!(replaced_target_card->card_flags & 2)) {
break;
}
replaced_target_card = target_card;
}
}
if (!replaced_target_card) {
target_card = ps->get_sc_card();
if (target_card) {
phase1_replaced_card_refs.emplace_back(target_card->get_card_ref());
}
} else {
phase1_replaced_card_refs.emplace_back(replaced_target_card->get_card_ref());
}
}
} else {
phase1_replaced_card_refs.emplace_back(target_card_ref);
}
}
}
}
} else {
if (ps) {
size_t present_target_count = 0;
size_t missing_target_count = 0;
size_t set_card_count = ps->count_set_cards();
for (size_t target_index = 0; (target_index < 4 * 9) && (as->target_card_refs[target_index] != 0xFFFF); target_index++) {
if (client_id == client_id_for_card_ref(as->target_card_refs[target_index])) {
auto target_card = this->card_for_set_card_ref(as->target_card_refs[target_index]);
if (!target_card || (target_card->card_flags & 2)) {
missing_target_count++;
} else {
present_target_count++;
phase1_replaced_card_refs.emplace_back(target_card->get_card_ref());
}
}
}
auto sc_card = ps->get_sc_card();
if ((present_target_count == 0) &&
(missing_target_count > 0) &&
(set_card_count == 0) &&
sc_card &&
sc_card->get_definition() &&
(sc_card->get_definition()->def.type == CardType::HUNTERS_SC)) {
phase1_replaced_card_refs.emplace_back(sc_card->get_card_ref());
}
}
}
}
// Note: The original code only writes a single FFFF after the last card ref in this array; we instead clear the
// entire array.
as->target_card_refs.clear(0xFFFF);
for (size_t z = 0; z < phase1_replaced_card_refs.size(); z++) {
as->target_card_refs[z] = this->send_6xB4x06_if_card_ref_invalid(phase1_replaced_card_refs[z], 4);
}
// as->target_card_refs[phase1_replaced_card_refs.size()] = 0xFFFF;
std::vector<uint16_t> phase2_replaced_card_refs;
for (size_t z = 0; (z < 4 * 9) && (as->target_card_refs[z] != 0xFFFF); z++) {
uint16_t target_card_ref = this->send_6xB4x06_if_card_ref_invalid(as->target_card_refs[z], 7);
auto target_card = this->card_for_set_card_ref(target_card_ref);
if (target_card) {
auto replaced_target = this->card_special->compute_replaced_target_based_on_conditions(
target_card->get_card_ref(), 1, 0, as->attacker_card_ref, 0xFFFF, 1,
0, 0xFF, 0, 0xFFFF);
if (!replaced_target) {
replaced_target = target_card;
}
phase2_replaced_card_refs.emplace_back(this->send_6xB4x06_if_card_ref_invalid(replaced_target->get_card_ref(), 8));
}
}
// Note: This is different from the original code in the same way as above: we clear the entire array first.
as->target_card_refs.clear(0xFFFF);
for (size_t z = 0; z < phase2_replaced_card_refs.size(); z++) {
as->target_card_refs[z] = this->send_6xB4x06_if_card_ref_invalid(phase2_replaced_card_refs[z], 4);
}
// as->target_card_refs[phase2_replaced_card_refs.size()] = 0xFFFF;
}
bool Server::any_target_exists_for_attack(const ActionState& as) {
auto card = this->card_for_set_card_ref(as.attacker_card_ref);
if (!card || (card->card_flags & 2)) {
return false;
}
for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) {
card = this->card_for_set_card_ref(as.target_card_refs[z]);
if (!card) {
break;
}
if (!(card->card_flags & 2)) {
return true;
}
}
return false;
}
uint8_t Server::get_current_team_turn2() const {
return this->current_team_turn2;
}
void Server::unknown_8023EE48() {
this->unknown_802402F4();
this->send_6xB4x02_for_all_players_if_needed();
}
void Server::unknown_8023EE80() {
bool is_nte = this->options.is_nte();
if (this->unknown_a14 < this->num_pending_attacks_with_cards) {
this->attack_cards[this->unknown_a14]->apply_attack_result();
if (is_nte) {
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->unknown_8023C110();
}
}
}
this->unknown_a14++;
}
this->check_for_battle_end();
if (is_nte) {
this->unknown_8023EEF4();
this->update_battle_state_flags_and_send_6xB4x03_if_needed();
this->send_6xB4x02_for_all_players_if_needed();
} else {
this->copy_player_states_to_prev_states();
this->unknown_8023EEF4();
this->send_set_card_updates_and_6xB4x04_if_needed();
}
}
void Server::unknown_802402F4() {
auto log = this->log_stack("unknown_802402F4: ");
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (ps && (this->current_team_turn2 == ps->get_team_id())) {
auto card = ps->get_sc_card();
if (card) {
log.debug_f("SC card has action chain");
card->compute_action_chain_results(true, false);
}
for (size_t set_index = 0; set_index < 8; set_index++) {
card = ps->get_set_card(set_index);
if (card) {
log.debug_f("set card {} has action chain", set_index);
card->compute_action_chain_results(true, false);
}
}
}
}
}
std::vector<std::shared_ptr<Card>> Server::const_cast_set_cards_v(
const std::vector<std::shared_ptr<const Card>>& cards) {
// TODO: This is dumb. Figure out a not-dumb way to do this.
std::vector<std::shared_ptr<Card>> ret;
for (auto const_card : cards) {
auto mutable_card = this->card_for_set_card_ref(const_card->get_card_ref());
if (mutable_card.get() != const_card.get()) {
throw std::logic_error("inconsistent set cards index");
}
ret.emplace_back(mutable_card);
}
return ret;
}
void Server::send_6xB4x39() const {
if (this->options.is_nte()) {
G_UpdateAllPlayerStatistics_Ep3NTE_6xB4x39 cmd;
for (size_t z = 0; z < 4; z++) {
if (this->player_states[z]) {
cmd.stats[z] = this->player_states[z]->stats;
}
}
this->send(cmd);
} else {
G_UpdateAllPlayerStatistics_Ep3_6xB4x39 cmd;
for (size_t z = 0; z < 4; z++) {
if (this->player_states[z]) {
cmd.stats[z] = this->player_states[z]->stats;
}
}
this->send(cmd);
}
}
void Server::send_6xB4x05() {
this->compute_all_map_occupied_bits();
if (this->options.is_nte()) {
G_UpdateMap_Ep3NTE_6xB4x05 cmd;
cmd.state = *this->map_and_rules;
this->send(cmd);
} else {
G_UpdateMap_Ep3_6xB4x05 cmd;
cmd.state = *this->map_and_rules;
this->send(cmd);
}
}
void Server::send_6xB4x02_for_all_players_if_needed(bool always_send) {
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(always_send);
}
}
}
G_SetTrapTileLocations_Ep3_6xB4x50 Server::prepare_6xB4x50_trap_tile_locations() const {
G_SetTrapTileLocations_Ep3_6xB4x50 cmd;
for (size_t trap_type = 0; trap_type < 5; trap_type++) {
uint8_t trap_index = this->chosen_trap_tile_index_of_type[trap_type];
if (trap_index != 0xFF) {
cmd.locations[trap_type] = this->trap_tile_locs[trap_type][trap_index];
} else {
cmd.locations[trap_type].clear(0xFF);
}
}
return cmd;
}
void Server::send_6xB4x50_trap_tile_locations() const {
this->send(this->prepare_6xB4x50_trap_tile_locations());
}
} // namespace Episode3