diff --git a/CMakeLists.txt b/CMakeLists.txt index 28019d1a..3cce4a48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,7 @@ set(SOURCES src/FunctionCompiler.cc src/GSLArchive.cc src/GVMEncoder.cc + src/HTTPServer.cc src/IPFrameInfo.cc src/IPStackSimulator.cc src/ItemCreator.cc diff --git a/src/Episode3/Card.cc b/src/Episode3/Card.cc index 88d28b9c..0bdb6899 100644 --- a/src/Episode3/Card.cc +++ b/src/Episode3/Card.cc @@ -914,7 +914,7 @@ void Card::compute_action_chain_results(bool apply_action_conditions, bool ignor this->action_chain.chain.tp_effect_bonus = 0; log.debug("(initial) medium=%s, strike_count=%hhu, ap_effect_bonus=%hhd, tp_effect_bonus=%hhd", - name_for_attack_medium(this->action_chain.chain.attack_medium), + name_for_enum(this->action_chain.chain.attack_medium), this->action_chain.chain.strike_count, this->action_chain.chain.ap_effect_bonus, this->action_chain.chain.tp_effect_bonus); diff --git a/src/Episode3/CardSpecial.cc b/src/Episode3/CardSpecial.cc index 9088249e..e9d47748 100644 --- a/src/Episode3/CardSpecial.cc +++ b/src/Episode3/CardSpecial.cc @@ -589,7 +589,7 @@ bool CardSpecial::apply_stat_deltas_to_card_from_condition_and_clear_cond(Condit break; trial_unimplemented: default: - log.debug("%s: no adjustments for condition type", name_for_condition_type(cond_type)); + log.debug("%s: no adjustments for condition type", name_for_enum(cond_type)); break; } @@ -1777,7 +1777,7 @@ bool CardSpecial::execute_effect( auto log = s->log_stack(string_printf("execute_effect(@%04hX #%04hX): ", card->get_card_ref(), card->get_card_id())); { string cond_str = cond.str(s); - log.debug("cond=%s, card=@%04hX, expr_value=%hd, unknown_p5=%hd, cond_type=%s, unknown_p7=%" PRIu32 ", attacker_card_ref=@%04hX", cond_str.c_str(), ref_for_card(card), expr_value, unknown_p5, name_for_condition_type(cond_type), unknown_p7, attacker_card_ref); + log.debug("cond=%s, card=@%04hX, expr_value=%hd, unknown_p5=%hd, cond_type=%s, unknown_p7=%" PRIu32 ", attacker_card_ref=@%04hX", cond_str.c_str(), ref_for_card(card), expr_value, unknown_p5, name_for_enum(cond_type), unknown_p7, attacker_card_ref); } bool is_nte = s->options.is_nte(); @@ -2841,7 +2841,7 @@ vector> CardSpecial::get_targeted_cards_for_condition( AttackMedium attack_medium = card2 ? card2->action_chain.chain.attack_medium : AttackMedium::UNKNOWN; - log.debug("attack_medium=%s", name_for_attack_medium(attack_medium)); + log.debug("attack_medium=%s", name_for_enum(attack_medium)); auto add_card_refs = [&](const vector& result_card_refs) -> void { for (uint16_t result_card_ref : result_card_refs) { diff --git a/src/Episode3/DataIndexes.cc b/src/Episode3/DataIndexes.cc index fb975353..b2e344c8 100644 --- a/src/Episode3/DataIndexes.cc +++ b/src/Episode3/DataIndexes.cc @@ -80,168 +80,6 @@ const char* name_for_link_color(uint8_t color) { } } -const char* name_for_card_type(CardType type) { - switch (type) { - case CardType::HUNTERS_SC: - return "HUNTERS_SC"; - case CardType::ARKZ_SC: - return "ARKZ_SC"; - case CardType::ITEM: - return "ITEM"; - case CardType::CREATURE: - return "CREATURE"; - case CardType::ACTION: - return "ACTION"; - case CardType::ASSIST: - return "ASSIST"; - case CardType::INVALID_FF: - return "INVALID_FF"; - default: - throw invalid_argument("invalid card type"); - } -} - -const char* name_for_card_class(CardClass cc) { - switch (cc) { - case CardClass::HU_SC: - return "HU_SC"; - case CardClass::RA_SC: - return "RA_SC"; - case CardClass::FO_SC: - return "FO_SC"; - case CardClass::NATIVE_CREATURE: - return "NATIVE_CREATURE"; - case CardClass::A_BEAST_CREATURE: - return "A_BEAST_CREATURE"; - case CardClass::MACHINE_CREATURE: - return "MACHINE_CREATURE"; - case CardClass::DARK_CREATURE: - return "DARK_CREATURE"; - case CardClass::GUARD_ITEM: - return "GUARD_ITEM"; - case CardClass::MAG_ITEM: - return "MAG_ITEM"; - case CardClass::SWORD_ITEM: - return "SWORD_ITEM"; - case CardClass::GUN_ITEM: - return "GUN_ITEM"; - case CardClass::CANE_ITEM: - return "CANE_ITEM"; - case CardClass::ATTACK_ACTION: - return "ATTACK_ACTION"; - case CardClass::DEFENSE_ACTION: - return "DEFENSE_ACTION"; - case CardClass::TECH: - return "TECH"; - case CardClass::PHOTON_BLAST: - return "PHOTON_BLAST"; - case CardClass::CONNECT_ONLY_ATTACK_ACTION: - return "CONNECT_ONLY_ATTACK_ACTION"; - case CardClass::BOSS_ATTACK_ACTION: - return "BOSS_ATTACK_ACTION"; - case CardClass::BOSS_TECH: - return "BOSS_TECH"; - case CardClass::ASSIST: - return "ASSIST"; - default: - throw invalid_argument("invalid card class"); - } -} - -const char* name_for_attack_medium(AttackMedium medium) { - switch (medium) { - case AttackMedium::UNKNOWN: - return "UNKNOWN"; - case AttackMedium::PHYSICAL: - return "PHYSICAL"; - case AttackMedium::TECH: - return "TECH"; - case AttackMedium::UNKNOWN_03: - return "UNKNOWN_03"; - case AttackMedium::INVALID_FF: - return "INVALID_FF"; - default: - return "__INVALID__"; - } -} - -const char* name_for_criterion_code(CriterionCode code) { - switch (code) { - case CriterionCode::NONE: - return "NONE"; - case CriterionCode::HU_CLASS_SC: - return "HU_CLASS_SC"; - case CriterionCode::RA_CLASS_SC: - return "RA_CLASS_SC"; - case CriterionCode::FO_CLASS_SC: - return "FO_CLASS_SC"; - case CriterionCode::SAME_TEAM: - return "SAME_TEAM"; - case CriterionCode::SAME_PLAYER: - return "SAME_PLAYER"; - case CriterionCode::SAME_TEAM_NOT_SAME_PLAYER: - return "SAME_TEAM_NOT_SAME_PLAYER"; - case CriterionCode::FC: - return "FC"; - case CriterionCode::NOT_SC: - return "NOT_SC"; - case CriterionCode::SC: - return "SC"; - case CriterionCode::HU_OR_RA_CLASS_SC: - return "HU_OR_RA_CLASS_SC"; - case CriterionCode::HUNTER_NON_ANDROID_SC: - return "HUNTER_NON_ANDROID_SC"; - case CriterionCode::HUNTER_HU_CLASS_MALE_SC: - return "HUNTER_HU_CLASS_MALE_SC"; - case CriterionCode::HUNTER_FEMALE_SC: - return "HUNTER_FEMALE_SC"; - case CriterionCode::HUNTER_NON_RA_CLASS_HUMAN_SC: - return "HUNTER_NON_RA_CLASS_HUMAN_SC"; - case CriterionCode::HUNTER_HU_CLASS_ANDROID_SC: - return "HUNTER_HU_CLASS_ANDROID_SC"; - case CriterionCode::HUNTER_NON_RA_CLASS_NON_NEWMAN_SC: - return "HUNTER_NON_RA_CLASS_NON_NEWMAN_SC"; - case CriterionCode::HUNTER_NON_NEWMAN_NON_FORCE_MALE_SC: - return "HUNTER_NON_NEWMAN_NON_FORCE_MALE_SC"; - case CriterionCode::HUNTER_HUNEWEARL_CLASS_SC: - return "HUNTER_HUNEWEARL_CLASS_SC"; - case CriterionCode::HUNTER_RA_CLASS_MALE_SC: - return "HUNTER_RA_CLASS_MALE_SC"; - case CriterionCode::HUNTER_RA_CLASS_FEMALE_SC: - return "HUNTER_RA_CLASS_FEMALE_SC"; - case CriterionCode::HUNTER_RA_OR_FO_CLASS_FEMALE_SC: - return "HUNTER_RA_OR_FO_CLASS_FEMALE_SC"; - case CriterionCode::HUNTER_HU_OR_RA_CLASS_HUMAN_SC: - return "HUNTER_HU_OR_RA_CLASS_HUMAN_SC"; - case CriterionCode::HUNTER_RA_CLASS_ANDROID_SC: - return "HUNTER_RA_CLASS_ANDROID_SC"; - case CriterionCode::HUNTER_FO_CLASS_FEMALE_SC: - return "HUNTER_FO_CLASS_FEMALE_SC"; - case CriterionCode::HUNTER_HUMAN_FEMALE_SC: - return "HUNTER_HUMAN_FEMALE_SC"; - case CriterionCode::HUNTER_ANDROID_SC: - return "HUNTER_ANDROID_SC"; - case CriterionCode::HU_OR_FO_CLASS_SC: - return "HU_OR_FO_CLASS_SC"; - case CriterionCode::RA_OR_FO_CLASS_SC: - return "RA_OR_FO_CLASS_SC"; - case CriterionCode::PHYSICAL_OR_UNKNOWN_ATTACK_MEDIUM: - return "PHYSICAL_OR_UNKNOWN_ATTACK_MEDIUM"; - case CriterionCode::TECH_OR_UNKNOWN_ATTACK_MEDIUM: - return "TECH_OR_UNKNOWN_ATTACK_MEDIUM"; - case CriterionCode::PHYSICAL_OR_TECH_OR_UNKNOWN_ATTACK_MEDIUM: - return "PHYSICAL_OR_TECH_OR_UNKNOWN_ATTACK_MEDIUM"; - case CriterionCode::NON_PHYSICAL_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC: - return "NON_PHYSICAL_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC"; - case CriterionCode::NON_PHYSICAL_NON_TECH_ATTACK_MEDIUM_NON_SC: - return "NON_PHYSICAL_NON_TECH_ATTACK_MEDIUM_NON_SC"; - case CriterionCode::NON_PHYSICAL_NON_TECH_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC: - return "NON_PHYSICAL_NON_TECH_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC"; - default: - throw invalid_argument("invalid criterion code"); - } -} - Location::Location() : Location(0, 0) {} Location::Location(uint8_t x, uint8_t y) : Location(x, y, Direction::RIGHT) {} Location::Location(uint8_t x, uint8_t y, Direction direction) @@ -262,7 +100,7 @@ bool Location::operator!=(const Location& other) const { std::string Location::str() const { return string_printf("Location[x=%hhu, y=%hhu, dir=%s, u=%hhu]", - this->x, this->y, name_for_direction(this->direction), this->unused); + this->x, this->y, name_for_enum(this->direction), this->unused); } void Location::clear() { @@ -324,23 +162,6 @@ Direction turn_around(Direction d) { } } -const char* name_for_direction(Direction d) { - switch (d) { - case Direction::RIGHT: - return "LEFT"; - case Direction::UP: - return "DOWN"; - case Direction::LEFT: - return "RIGHT"; - case Direction::DOWN: - return "UP"; - case Direction::INVALID_FF: - return "INVALID_FF"; - default: - return "__INVALID__"; - } -} - bool card_class_is_tech_like(CardClass cc, bool is_nte) { // NTE does not consider BOSS_TECH to be a tech-like card class, but that's // probably because that card class just doesn't exist on NTE. @@ -632,27 +453,6 @@ static const vector description_for_condition_type({ /* 0x7D */ {false, "UNKNOWN_7D", nullptr}, }); -const char* name_for_condition_type(ConditionType cond_type) { - try { - return description_for_condition_type.at(static_cast(cond_type)).name; - } catch (const out_of_range&) { - return "__INVALID__"; - } -} - -const char* name_for_action_subphase(ActionSubphase subphase) { - switch (subphase) { - case ActionSubphase::ATTACK: - return "ATTACK"; - case ActionSubphase::DEFENSE: - return "DEFENSE"; - case ActionSubphase::INVALID_FF: - return "INVALID_FF"; - default: - return "__INVALID__"; - } -} - void CardDefinition::Stat::decode_code() { this->type = static_cast(this->code / 1000); int16_t value = this->code - (this->type * 1000); @@ -801,7 +601,7 @@ string CardDefinition::Effect::str(const char* separator, const TextSet* text_ar uint8_t type = static_cast(this->apply_criterion); string cond_str = string_printf("cond=%02hhX", type); try { - const char* name = name_for_criterion_code(this->apply_criterion); + const char* name = name_for_enum(this->apply_criterion); cond_str += ':'; cond_str += name; } catch (const invalid_argument&) { @@ -1072,24 +872,9 @@ static const char* name_for_assist_ai_param_target(uint8_t target) { } string CardDefinition::str(bool single_line, const TextSet* text_archive) const { - string type_str; - try { - type_str = name_for_card_type(this->type); - } catch (const invalid_argument&) { - type_str = string_printf("%02hhX", static_cast(this->type)); - } - string criterion_str; - try { - criterion_str = name_for_criterion_code(this->usable_criterion); - } catch (const invalid_argument&) { - criterion_str = string_printf("%02hhX", static_cast(this->usable_criterion)); - } - string card_class_str; - try { - card_class_str = name_for_card_class(this->card_class()); - } catch (const invalid_argument&) { - card_class_str = string_printf("%04hX", this->be_card_class.load()); - } + string type_str = name_for_enum(this->type); + string criterion_str = name_for_enum(this->usable_criterion); + string card_class_str = name_for_enum(this->card_class()); string rank_str = name_for_rank(this->rank); string target_mode_str = name_for_target_mode(this->target_mode); string assist_turns_str = string_for_assist_turns(this->assist_turns); @@ -2863,6 +2648,275 @@ const char* name_for_enum(Episode3::AllowedCards allowed case Episode3::AllowedCards::N_R_S_ONLY: return "N_R_S_ONLY"; default: - throw out_of_range("invalid allowed cards"); + return "__INVALID__"; + } +} + +template <> +const char* name_for_enum(Episode3::BattlePhase phase) { + switch (phase) { + case Episode3::BattlePhase::INVALID_00: + return "INVALID_00"; + case Episode3::BattlePhase::DICE: + return "DICE"; + case Episode3::BattlePhase::SET: + return "SET"; + case Episode3::BattlePhase::MOVE: + return "MOVE"; + case Episode3::BattlePhase::ACTION: + return "ACTION"; + case Episode3::BattlePhase::DRAW: + return "DRAW"; + case Episode3::BattlePhase::INVALID_FF: + return "INVALID_FF"; + default: + return "__INVALID__"; + } +} + +template <> +const char* name_for_enum(Episode3::SetupPhase phase) { + switch (phase) { + case Episode3::SetupPhase::REGISTRATION: + return "REGISTRATION"; + case Episode3::SetupPhase::STARTER_ROLLS: + return "STARTER_ROLLS"; + case Episode3::SetupPhase::HAND_REDRAW_OPTION: + return "HAND_REDRAW_OPTION"; + case Episode3::SetupPhase::MAIN_BATTLE: + return "MAIN_BATTLE"; + case Episode3::SetupPhase::BATTLE_ENDED: + return "BATTLE_ENDED"; + case Episode3::SetupPhase::INVALID_FF: + return "INVALID_FF"; + default: + return "__INVALID__"; + } +} + +template <> +const char* name_for_enum(Episode3::RegistrationPhase phase) { + switch (phase) { + case Episode3::RegistrationPhase::AWAITING_NUM_PLAYERS: + return "AWAITING_NUM_PLAYERS"; + case Episode3::RegistrationPhase::AWAITING_PLAYERS: + return "AWAITING_PLAYERS"; + case Episode3::RegistrationPhase::AWAITING_DECKS: + return "AWAITING_DECKS"; + case Episode3::RegistrationPhase::REGISTERED: + return "REGISTERED"; + case Episode3::RegistrationPhase::BATTLE_STARTED: + return "BATTLE_STARTED"; + case Episode3::RegistrationPhase::INVALID_FF: + return "INVALID_FF"; + default: + return "__INVALID__"; + } +} + +template <> +const char* name_for_enum(Episode3::ActionSubphase phase) { + switch (phase) { + case Episode3::ActionSubphase::ATTACK: + return "ATTACK"; + case Episode3::ActionSubphase::DEFENSE: + return "DEFENSE"; + case Episode3::ActionSubphase::INVALID_FF: + return "INVALID_FF"; + default: + return "__INVALID__"; + } +} + +template <> +const char* name_for_enum(Episode3::AttackMedium medium) { + switch (medium) { + case Episode3::AttackMedium::UNKNOWN: + return "UNKNOWN"; + case Episode3::AttackMedium::PHYSICAL: + return "PHYSICAL"; + case Episode3::AttackMedium::TECH: + return "TECH"; + case Episode3::AttackMedium::UNKNOWN_03: + return "UNKNOWN_03"; + case Episode3::AttackMedium::INVALID_FF: + return "INVALID_FF"; + default: + return "__INVALID__"; + } +} + +template <> +const char* name_for_enum(Episode3::CriterionCode code) { + switch (code) { + case Episode3::CriterionCode::NONE: + return "NONE"; + case Episode3::CriterionCode::HU_CLASS_SC: + return "HU_CLASS_SC"; + case Episode3::CriterionCode::RA_CLASS_SC: + return "RA_CLASS_SC"; + case Episode3::CriterionCode::FO_CLASS_SC: + return "FO_CLASS_SC"; + case Episode3::CriterionCode::SAME_TEAM: + return "SAME_TEAM"; + case Episode3::CriterionCode::SAME_PLAYER: + return "SAME_PLAYER"; + case Episode3::CriterionCode::SAME_TEAM_NOT_SAME_PLAYER: + return "SAME_TEAM_NOT_SAME_PLAYER"; + case Episode3::CriterionCode::FC: + return "FC"; + case Episode3::CriterionCode::NOT_SC: + return "NOT_SC"; + case Episode3::CriterionCode::SC: + return "SC"; + case Episode3::CriterionCode::HU_OR_RA_CLASS_SC: + return "HU_OR_RA_CLASS_SC"; + case Episode3::CriterionCode::HUNTER_NON_ANDROID_SC: + return "HUNTER_NON_ANDROID_SC"; + case Episode3::CriterionCode::HUNTER_HU_CLASS_MALE_SC: + return "HUNTER_HU_CLASS_MALE_SC"; + case Episode3::CriterionCode::HUNTER_FEMALE_SC: + return "HUNTER_FEMALE_SC"; + case Episode3::CriterionCode::HUNTER_NON_RA_CLASS_HUMAN_SC: + return "HUNTER_NON_RA_CLASS_HUMAN_SC"; + case Episode3::CriterionCode::HUNTER_HU_CLASS_ANDROID_SC: + return "HUNTER_HU_CLASS_ANDROID_SC"; + case Episode3::CriterionCode::HUNTER_NON_RA_CLASS_NON_NEWMAN_SC: + return "HUNTER_NON_RA_CLASS_NON_NEWMAN_SC"; + case Episode3::CriterionCode::HUNTER_NON_NEWMAN_NON_FORCE_MALE_SC: + return "HUNTER_NON_NEWMAN_NON_FORCE_MALE_SC"; + case Episode3::CriterionCode::HUNTER_HUNEWEARL_CLASS_SC: + return "HUNTER_HUNEWEARL_CLASS_SC"; + case Episode3::CriterionCode::HUNTER_RA_CLASS_MALE_SC: + return "HUNTER_RA_CLASS_MALE_SC"; + case Episode3::CriterionCode::HUNTER_RA_CLASS_FEMALE_SC: + return "HUNTER_RA_CLASS_FEMALE_SC"; + case Episode3::CriterionCode::HUNTER_RA_OR_FO_CLASS_FEMALE_SC: + return "HUNTER_RA_OR_FO_CLASS_FEMALE_SC"; + case Episode3::CriterionCode::HUNTER_HU_OR_RA_CLASS_HUMAN_SC: + return "HUNTER_HU_OR_RA_CLASS_HUMAN_SC"; + case Episode3::CriterionCode::HUNTER_RA_CLASS_ANDROID_SC: + return "HUNTER_RA_CLASS_ANDROID_SC"; + case Episode3::CriterionCode::HUNTER_FO_CLASS_FEMALE_SC: + return "HUNTER_FO_CLASS_FEMALE_SC"; + case Episode3::CriterionCode::HUNTER_HUMAN_FEMALE_SC: + return "HUNTER_HUMAN_FEMALE_SC"; + case Episode3::CriterionCode::HUNTER_ANDROID_SC: + return "HUNTER_ANDROID_SC"; + case Episode3::CriterionCode::HU_OR_FO_CLASS_SC: + return "HU_OR_FO_CLASS_SC"; + case Episode3::CriterionCode::RA_OR_FO_CLASS_SC: + return "RA_OR_FO_CLASS_SC"; + case Episode3::CriterionCode::PHYSICAL_OR_UNKNOWN_ATTACK_MEDIUM: + return "PHYSICAL_OR_UNKNOWN_ATTACK_MEDIUM"; + case Episode3::CriterionCode::TECH_OR_UNKNOWN_ATTACK_MEDIUM: + return "TECH_OR_UNKNOWN_ATTACK_MEDIUM"; + case Episode3::CriterionCode::PHYSICAL_OR_TECH_OR_UNKNOWN_ATTACK_MEDIUM: + return "PHYSICAL_OR_TECH_OR_UNKNOWN_ATTACK_MEDIUM"; + case Episode3::CriterionCode::NON_PHYSICAL_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC: + return "NON_PHYSICAL_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC"; + case Episode3::CriterionCode::NON_PHYSICAL_NON_TECH_ATTACK_MEDIUM_NON_SC: + return "NON_PHYSICAL_NON_TECH_ATTACK_MEDIUM_NON_SC"; + case Episode3::CriterionCode::NON_PHYSICAL_NON_TECH_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC: + return "NON_PHYSICAL_NON_TECH_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC"; + default: + return "__UNKNOWN__"; + } +} + +template <> +const char* name_for_enum(Episode3::CardType type) { + switch (type) { + case Episode3::CardType::HUNTERS_SC: + return "HUNTERS_SC"; + case Episode3::CardType::ARKZ_SC: + return "ARKZ_SC"; + case Episode3::CardType::ITEM: + return "ITEM"; + case Episode3::CardType::CREATURE: + return "CREATURE"; + case Episode3::CardType::ACTION: + return "ACTION"; + case Episode3::CardType::ASSIST: + return "ASSIST"; + case Episode3::CardType::INVALID_FF: + return "INVALID_FF"; + default: + return "__UNKNOWN__"; + } +} + +template <> +const char* name_for_enum(Episode3::CardClass cc) { + switch (cc) { + case Episode3::CardClass::HU_SC: + return "HU_SC"; + case Episode3::CardClass::RA_SC: + return "RA_SC"; + case Episode3::CardClass::FO_SC: + return "FO_SC"; + case Episode3::CardClass::NATIVE_CREATURE: + return "NATIVE_CREATURE"; + case Episode3::CardClass::A_BEAST_CREATURE: + return "A_BEAST_CREATURE"; + case Episode3::CardClass::MACHINE_CREATURE: + return "MACHINE_CREATURE"; + case Episode3::CardClass::DARK_CREATURE: + return "DARK_CREATURE"; + case Episode3::CardClass::GUARD_ITEM: + return "GUARD_ITEM"; + case Episode3::CardClass::MAG_ITEM: + return "MAG_ITEM"; + case Episode3::CardClass::SWORD_ITEM: + return "SWORD_ITEM"; + case Episode3::CardClass::GUN_ITEM: + return "GUN_ITEM"; + case Episode3::CardClass::CANE_ITEM: + return "CANE_ITEM"; + case Episode3::CardClass::ATTACK_ACTION: + return "ATTACK_ACTION"; + case Episode3::CardClass::DEFENSE_ACTION: + return "DEFENSE_ACTION"; + case Episode3::CardClass::TECH: + return "TECH"; + case Episode3::CardClass::PHOTON_BLAST: + return "PHOTON_BLAST"; + case Episode3::CardClass::CONNECT_ONLY_ATTACK_ACTION: + return "CONNECT_ONLY_ATTACK_ACTION"; + case Episode3::CardClass::BOSS_ATTACK_ACTION: + return "BOSS_ATTACK_ACTION"; + case Episode3::CardClass::BOSS_TECH: + return "BOSS_TECH"; + case Episode3::CardClass::ASSIST: + return "ASSIST"; + default: + return "__INVALID__"; + } +} + +template <> +const char* name_for_enum(Episode3::ConditionType cond_type) { + try { + return Episode3::description_for_condition_type.at(static_cast(cond_type)).name; + } catch (const out_of_range&) { + return "__INVALID__"; + } +} + +template <> +const char* name_for_enum(Episode3::Direction d) { + switch (d) { + case Episode3::Direction::RIGHT: + return "LEFT"; + case Episode3::Direction::UP: + return "DOWN"; + case Episode3::Direction::LEFT: + return "RIGHT"; + case Episode3::Direction::DOWN: + return "UP"; + case Episode3::Direction::INVALID_FF: + return "INVALID_FF"; + default: + return "__INVALID__"; } } diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index 3616740e..f90ba736 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -59,8 +59,6 @@ enum class AttackMedium : uint8_t { INVALID_FF = 0xFF, }; -const char* name_for_attack_medium(AttackMedium medium); - enum class CriterionCode : uint8_t { NONE = 0x00, HU_CLASS_SC = 0x01, @@ -99,8 +97,6 @@ enum class CriterionCode : uint8_t { NON_PHYSICAL_NON_TECH_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC = 0x22, }; -const char* name_for_criterion_code(CriterionCode code); - enum class CardRank : uint8_t { N1 = 0x01, R1 = 0x02, @@ -138,8 +134,6 @@ enum class CardType : uint8_t { END_CARD_LIST = 0xFF, }; -const char* name_for_card_type(CardType type); - enum class CardClass : uint16_t { HU_SC = 0x0000, RA_SC = 0x0001, @@ -163,7 +157,6 @@ enum class CardClass : uint16_t { ASSIST = 0x0028, }; -const char* name_for_card_class(CardClass cc); bool card_class_is_tech_like(CardClass cc, bool is_nte); enum class TargetMode : uint8_t { @@ -310,8 +303,6 @@ enum class ConditionType : uint8_t { ANY_FF = 0xFF, // Used as a wildcard in some search functions }; -const char* name_for_condition_type(ConditionType cond_type); - enum class AssistEffect : uint16_t { NONE = 0x0000, DICE_HALF = 0x0001, @@ -408,8 +399,6 @@ enum class ActionSubphase : uint8_t { INVALID_FF = 0xFF, }; -const char* name_for_action_subphase(ActionSubphase subphase); - enum class SetupPhase : uint8_t { REGISTRATION = 0, STARTER_ROLLS = 1, @@ -439,7 +428,6 @@ enum class Direction : uint8_t { Direction turn_left(Direction d); Direction turn_right(Direction d); Direction turn_around(Direction d); -const char* name_for_direction(Direction d); struct Location { /* 00 */ uint8_t x; @@ -519,7 +507,7 @@ struct CardDefinition { } __attribute__((packed)); /* 0000 */ be_uint32_t card_id; - /* 0004 */ parray jp_name; + /* 0004 */ pstring jp_name; // The list of card definitions ends with a "sentinel" definition that isn't a // real card, but instead has a negative number in the type field here. @@ -1556,3 +1544,24 @@ template <> Episode3::AllowedCards enum_for_name(const char* name); template <> const char* name_for_enum(Episode3::AllowedCards allowed_cards); + +template <> +const char* name_for_enum(Episode3::BattlePhase phase); +template <> +const char* name_for_enum(Episode3::SetupPhase phase); +template <> +const char* name_for_enum(Episode3::RegistrationPhase phase); +template <> +const char* name_for_enum(Episode3::ActionSubphase phase); +template <> +const char* name_for_enum(Episode3::AttackMedium medium); +template <> +const char* name_for_enum(Episode3::CriterionCode code); +template <> +const char* name_for_enum(Episode3::CardType type); +template <> +const char* name_for_enum(Episode3::CardClass cc); +template <> +const char* name_for_enum(Episode3::ConditionType cond_type); +template <> +const char* name_for_enum(Episode3::Direction d); diff --git a/src/Episode3/DeckState.hh b/src/Episode3/DeckState.hh index 5f1b780f..aeedfd6d 100644 --- a/src/Episode3/DeckState.hh +++ b/src/Episode3/DeckState.hh @@ -11,7 +11,7 @@ namespace Episode3 { struct NameEntry { - /* 00 */ parray name; + /* 00 */ pstring name; /* 10 */ uint8_t client_id; /* 11 */ uint8_t present; /* 12 */ uint8_t is_cpu_player; @@ -23,7 +23,7 @@ struct NameEntry { } __attribute__((packed)); struct DeckEntry { - /* 00 */ pstring name; + /* 00 */ pstring name; /* 10 */ le_uint32_t team_id; /* 14 */ parray card_ids; // If the following flag is not set to 3, then the God Whim assist effect can diff --git a/src/Episode3/PlayerState.cc b/src/Episode3/PlayerState.cc index 315f8c96..f69c84b3 100644 --- a/src/Episode3/PlayerState.cc +++ b/src/Episode3/PlayerState.cc @@ -1781,7 +1781,7 @@ bool PlayerState::set_action_cards_for_action_state(const ActionState& pa) { auto card = s->card_for_set_card_ref(pa.attacker_card_ref); if (card) { card->loc.direction = pa.facing_direction; - log.debug("set facing direction to %s", name_for_direction(card->loc.direction)); + log.debug("set facing direction to %s", name_for_enum(card->loc.direction)); G_Unknown_Ep3_6xB4x4A cmd; cmd.card_refs.clear(0xFFFF); diff --git a/src/Episode3/PlayerStateSubordinates.cc b/src/Episode3/PlayerStateSubordinates.cc index 360a741b..3c8c0f9c 100644 --- a/src/Episode3/PlayerStateSubordinates.cc +++ b/src/Episode3/PlayerStateSubordinates.cc @@ -68,7 +68,7 @@ std::string Condition::str(shared_ptr s) const { "Condition[type=%s, turns=%hhu, a_arg=%hhd, dice=%hhu, flags=%02hhX, " "def_eff_index=%hhu, ref=%s, value=%hd, giver_ref=%s " "percent=%hhu value8=%hd order=%hu a8=%hu]", - name_for_condition_type(this->type), + name_for_enum(this->type), this->remaining_turns, this->a_arg_value, this->dice_roll_value, @@ -199,7 +199,7 @@ std::string ActionState::str(shared_ptr s) const { "orig_attacker_ref=%s]", this->client_id.load(), this->unused, - name_for_direction(this->facing_direction), + name_for_enum(this->facing_direction), attacker_ref_s.c_str(), defense_ref_s.c_str(), target_refs_s.c_str(), @@ -258,9 +258,9 @@ std::string ActionChain::str(shared_ptr s) const { unknown_card_ref_a3_s.c_str(), attack_action_card_refs_s.c_str(), this->attack_action_card_ref_count, - name_for_attack_medium(this->attack_medium), + name_for_enum(this->attack_medium), this->target_card_ref_count, - name_for_action_subphase(this->action_subphase), + name_for_enum(this->action_subphase), this->strike_count, this->damage_multiplier, this->attack_number, @@ -588,7 +588,7 @@ std::string ActionMetadata::str(shared_ptr s) const { card_ref_s.c_str(), this->target_card_ref_count, this->defense_card_ref_count, - name_for_action_subphase(this->action_subphase), + name_for_enum(this->action_subphase), this->defense_power, this->defense_bonus, this->attack_bonus, diff --git a/src/Episode3/RulerServer.cc b/src/Episode3/RulerServer.cc index d69dcb2a..91ae8fc1 100644 --- a/src/Episode3/RulerServer.cc +++ b/src/Episode3/RulerServer.cc @@ -939,7 +939,7 @@ bool RulerServer::check_usability_or_condition_apply( bool is_item_usability_check, AttackMedium attack_medium) const { auto s = this->server(); - auto log = s->log_stack(string_printf("check_usability_or_condition_apply(%02hhX, #%04hX, %02hhX, #%04hX, #%04hX, %02hhX, %s, %s): ", client_id1, card_id1, client_id2, card_id2, card_id3, def_effect_index, is_item_usability_check ? "true" : "false", name_for_attack_medium(attack_medium))); + auto log = s->log_stack(string_printf("check_usability_or_condition_apply(%02hhX, #%04hX, %02hhX, #%04hX, #%04hX, %02hhX, %s, %s): ", client_id1, card_id1, client_id2, card_id2, card_id3, def_effect_index, is_item_usability_check ? "true" : "false", name_for_enum(attack_medium))); if (static_cast(attack_medium) & 0x80) { attack_medium = AttackMedium::UNKNOWN; @@ -967,7 +967,7 @@ bool RulerServer::check_usability_or_condition_apply( } criterion_code = ce1->def.effects[def_effect_index].apply_criterion; } - log.debug("criterion_code=%s", name_for_criterion_code(criterion_code)); + log.debug("criterion_code=%s", name_for_enum(criterion_code)); // For item usability checks, prevent criteria that depend on player // positioning/team setup diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc new file mode 100644 index 00000000..59e01c05 --- /dev/null +++ b/src/HTTPServer.cc @@ -0,0 +1,851 @@ +#include "HTTPServer.hh" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Loggers.hh" +#include "ProxyServer.hh" +#include "Server.hh" + +using namespace std; + +const unordered_map HTTPServer::explanation_for_response_code({ + {100, "Continue"}, + {101, "Switching Protocols"}, + {102, "Processing"}, + {200, "OK"}, + {201, "Created"}, + {202, "Accepted"}, + {203, "Non-Authoritative Information"}, + {204, "No Content"}, + {205, "Reset Content"}, + {206, "Partial Content"}, + {207, "Multi-Status"}, + {208, "Already Reported"}, + {226, "IM Used"}, + {300, "Multiple Choices"}, + {301, "Moved Permanently"}, + {302, "Found"}, + {303, "See Other"}, + {304, "Not Modified"}, + {305, "Use Proxy"}, + {307, "Temporary Redirect"}, + {308, "Permanent Redirect"}, + {400, "Bad Request"}, + {401, "Unathorized"}, + {402, "Payment Required"}, + {403, "Forbidden"}, + {404, "Not Found"}, + {405, "Method Not Allowed"}, + {406, "Not Acceptable"}, + {407, "Proxy Authentication Required"}, + {408, "Request Timeout"}, + {409, "Conflict"}, + {410, "Gone"}, + {411, "Length Required"}, + {412, "Precondition Failed"}, + {413, "Request Entity Too Large"}, + {414, "Request-URI Too Long"}, + {415, "Unsupported Media Type"}, + {416, "Requested Range Not Satisfiable"}, + {417, "Expectation Failed"}, + {418, "I\'m a Teapot"}, + {420, "Enhance Your Calm"}, + {422, "Unprocessable Entity"}, + {423, "Locked"}, + {424, "Failed Dependency"}, + {426, "Upgrade Required"}, + {428, "Precondition Required"}, + {429, "Too Many Requests"}, + {431, "Request Header Fields Too Large"}, + {444, "No Response"}, + {449, "Retry With"}, + {451, "Unavailable For Legal Reasons"}, + {500, "Internal Server Error"}, + {501, "Not Implemented"}, + {502, "Bad Gateway"}, + {503, "Service Unavailable"}, + {504, "Gateway Timeout"}, + {505, "HTTP Version Not Supported"}, + {506, "Variant Also Negotiates"}, + {507, "Insufficient Storage"}, + {508, "Loop Detected"}, + {509, "Bandwidth Limit Exceeded"}, + {510, "Not Extended"}, + {511, "Network Authentication Required"}, + {598, "Network Read Timeout Error"}, + {599, "Network Connect Timeout Error"}, +}); + +HTTPServer::http_error::http_error(int code, const string& what) + : runtime_error(what), + code(code) {} + +void HTTPServer::send_response(struct evhttp_request* req, int code, const char* content_type, struct evbuffer* b) { + struct evkeyvalq* headers = evhttp_request_get_output_headers(req); + evhttp_add_header(headers, "Content-Type", content_type); + evhttp_add_header(headers, "Server", "newserv"); + evhttp_send_reply(req, code, explanation_for_response_code.at(code), b); +} + +void HTTPServer::send_response(struct evhttp_request* req, int code, const char* content_type, const char* fmt, ...) { + unique_ptr out_buffer(evbuffer_new(), evbuffer_free); + va_list va; + va_start(va, fmt); + evbuffer_add_vprintf(out_buffer.get(), fmt, va); + va_end(va); + HTTPServer::send_response(req, code, content_type, out_buffer.get()); +} + +unordered_multimap HTTPServer::parse_url_params(const string& query) { + unordered_multimap params; + if (query.empty()) { + return params; + } + for (auto it : split(query, '&')) { + size_t first_equals = it.find('='); + if (first_equals != string::npos) { + string value(it, first_equals + 1); + + size_t write_offset = 0, read_offset = 0; + for (; read_offset < value.size(); write_offset++) { + if ((value[read_offset] == '%') && (read_offset < value.size() - 2)) { + value[write_offset] = + static_cast(value_for_hex_char(value[read_offset + 1]) << 4) | + static_cast(value_for_hex_char(value[read_offset + 2])); + read_offset += 3; + } else if (value[write_offset] == '+') { + value[write_offset] = ' '; + read_offset++; + } else { + value[write_offset] = value[read_offset]; + read_offset++; + } + } + value.resize(write_offset); + + params.emplace(piecewise_construct, forward_as_tuple(it, 0, first_equals), + forward_as_tuple(value)); + } else { + params.emplace(it, ""); + } + } + return params; +} + +unordered_map HTTPServer::parse_url_params_unique(const string& query) { + unordered_map ret; + for (const auto& it : HTTPServer::parse_url_params(query)) { + ret.emplace(it.first, std::move(it.second)); + } + return ret; +} + +const string& HTTPServer::get_url_param( + const unordered_multimap& params, const string& key, const string* _default) { + + auto range = params.equal_range(key); + if (range.first == range.second) { + if (!_default) { + throw out_of_range("URL parameter " + key + " not present"); + } + return *_default; + } + + return range.first->second; +} + +HTTPServer::HTTPServer(shared_ptr state) + : state(state), + http(evhttp_new(this->state->base.get()), evhttp_free) { + evhttp_set_gencb(this->http.get(), this->dispatch_handle_request, this); +} + +void HTTPServer::listen(const string& socket_path) { + int fd = ::listen(socket_path, 0, SOMAXCONN); + server_log.info("Listening on Unix socket %s on fd %d (HTTP)", socket_path.c_str(), fd); + this->add_socket(fd); +} + +void HTTPServer::listen(const string& addr, int port) { + if (port == 0) { + this->listen(addr); + } else { + int fd = ::listen(addr, port, SOMAXCONN); + string netloc_str = render_netloc(addr, port); + server_log.info("Listening on TCP interface %s on fd %d (HTTP)", netloc_str.c_str(), fd); + this->add_socket(fd); + } +} + +void HTTPServer::listen(int port) { + this->listen("", port); +} + +void HTTPServer::add_socket(int fd) { + evhttp_accept_socket(this->http.get(), fd); +} + +void HTTPServer::dispatch_handle_request(struct evhttp_request* req, void* ctx) { + reinterpret_cast(ctx)->handle_request(req); +} + +JSON HTTPServer::generate_client_config_json(const Client::Config& config) const { + const char* drop_notifications_mode = "unknown"; + switch (config.get_drop_notification_mode()) { + case Client::ItemDropNotificationMode::NOTHING: + drop_notifications_mode = "off"; + break; + case Client::ItemDropNotificationMode::RARES_ONLY: + drop_notifications_mode = "rare"; + break; + case Client::ItemDropNotificationMode::ALL_ITEMS: + drop_notifications_mode = "on"; + break; + case Client::ItemDropNotificationMode::ALL_ITEMS_INCLUDING_MESETA: + drop_notifications_mode = "every"; + break; + } + + auto ret = JSON::dict({ + {"SpecificVersion", config.specific_version}, + {"SwitchAssistEnabled", (config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? true : false)}, + {"InfiniteHPEnabled", (config.check_flag(Client::Flag::INFINITE_HP_ENABLED) ? true : false)}, + {"InfiniteTPEnabled", (config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? true : false)}, + {"DropNotificationMode", drop_notifications_mode}, + {"DebugEnabled", (config.check_flag(Client::Flag::DEBUG_ENABLED) ? true : false)}, + {"ProxySaveFilesEnabled", (config.check_flag(Client::Flag::PROXY_SAVE_FILES) ? true : false)}, + {"ProxyChatCommandsEnabled", (config.check_flag(Client::Flag::PROXY_CHAT_COMMANDS_ENABLED) ? true : false)}, + {"ProxyPlayerNotificationsEnabled", (config.check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) ? true : false)}, + {"ProxySuppressClientPings", (config.check_flag(Client::Flag::PROXY_SUPPRESS_CLIENT_PINGS) ? true : false)}, + {"ProxyEp3InfiniteMesetaEnabled", (config.check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED) ? true : false)}, + {"ProxyEp3InfiniteTimeEnabled", (config.check_flag(Client::Flag::PROXY_EP3_INFINITE_TIME_ENABLED) ? true : false)}, + {"ProxyBlockFunctionCalls", (config.check_flag(Client::Flag::PROXY_BLOCK_FUNCTION_CALLS) ? true : false)}, + {"ProxyEp3UnmaskWhispers", (config.check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) ? true : false)}, + }); + ret.emplace("OverrideRandomSeed", config.check_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED) ? config.override_random_seed : JSON(nullptr)); + ret.emplace("OverrideSectionID", (config.override_section_id != 0xFF) ? config.override_section_id : JSON(nullptr)); + ret.emplace("OverrideLobbyEvent", (config.override_lobby_event != 0xFF) ? config.override_lobby_event : JSON(nullptr)); + ret.emplace("OverrideLobbyNumber", (config.override_lobby_number != 0x80) ? config.override_lobby_number : JSON(nullptr)); + return ret; +} + +JSON HTTPServer::generate_license_json(shared_ptr l) const { + auto ret = JSON::dict({ + {"SerialNumber", l->serial_number}, + {"Flags", l->flags}, + {"Ep3CurrentMeseta", l->ep3_current_meseta}, + {"Ep3TotalMesetaEarned", l->ep3_total_meseta_earned}, + {"BBTeamID", l->bb_team_id}, + }); + ret.emplace("BanEndTime", l->ban_end_time ? l->ban_end_time : JSON(nullptr)); + ret.emplace("XBGamertag", !l->xb_gamertag.empty() ? l->xb_gamertag : JSON(nullptr)); + ret.emplace("XBUserID", l->xb_user_id ? l->xb_user_id : JSON(nullptr)); + ret.emplace("BBUsername", !l->bb_username.empty() ? l->bb_username : JSON(nullptr)); + return ret; +}; + +JSON HTTPServer::generate_game_client_json(shared_ptr c) const { + auto ret = JSON::dict({ + {"ID", c->id}, + {"RemoteAddress", render_sockaddr_storage(c->channel.remote_addr)}, + {"Version", name_for_enum(c->version())}, + {"SubVersion", c->sub_version}, + {"Config", this->generate_client_config_json(c->config)}, + {"Language", name_for_language_code(c->language())}, + {"LocationX", c->x}, + {"LocationZ", c->z}, + {"LocationFloor", c->floor}, + {"CanChat", c->can_chat}, + }); + ret.emplace("license", c->license ? this->generate_license_json(c->license) : JSON(nullptr)); + auto l = c->lobby.lock(); + if (l) { + ret.emplace("LobbyID", l->lobby_id); + ret.emplace("LobbyClientID", c->lobby_client_id); + } + if (c->version() == Version::BB_V4) { + ret.emplace("BBCharacterIndex", c->bb_character_index); + } + auto p = c->character(false, false); + if (p) { + if (!is_ep3(c->version())) { + ret.emplace("InventoryItems", p->inventory.num_items); + if (c->version() != Version::DC_NTE) { + ret.emplace("InventoryLanguage", p->inventory.language); + ret.emplace("NumHPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::HP)); + ret.emplace("NumTPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::TP)); + if (!is_v1_or_v2(c->version())) { + ret.emplace("NumPowerMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER)); + ret.emplace("NumDefMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF)); + ret.emplace("NumMindMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND)); + ret.emplace("NumEvadeMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE)); + ret.emplace("NumLuckMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK)); + } + } + JSON items_json = JSON::list(); + for (size_t z = 0; z < p->inventory.num_items; z++) { + const auto& item = p->inventory.items[z]; + string description = this->state->describe_item(c->version(), item.data, false); + string data_str = item.data.hex(); + auto item_dict = JSON::dict({ + {"Flags", item.flags.load()}, + {"Data", std::move(data_str)}, + {"Description", std::move(description)}, + {"ItemID", item.data.id.load()}, + }); + items_json.emplace_back(std::move(item_dict)); + } + ret.emplace("ATP", p->disp.stats.char_stats.atp.load()); + ret.emplace("MST", p->disp.stats.char_stats.mst.load()); + ret.emplace("EVP", p->disp.stats.char_stats.evp.load()); + ret.emplace("HP", p->disp.stats.char_stats.hp.load()); + ret.emplace("DFP", p->disp.stats.char_stats.dfp.load()); + ret.emplace("ATA", p->disp.stats.char_stats.ata.load()); + ret.emplace("LCK", p->disp.stats.char_stats.lck.load()); + ret.emplace("EXP", p->disp.stats.experience.load()); + ret.emplace("Meseta", p->disp.stats.meseta.load()); + auto tech_levels_json = JSON::dict(); + for (size_t z = 0; z < 0x13; z++) { + auto level = p->get_technique_level(z); + tech_levels_json.emplace(name_for_technique(z), (level != 0xFF) ? level : JSON(nullptr)); + } + ret.emplace("TechniqueLevels", std::move(tech_levels_json)); + } + ret.emplace("Height", p->disp.stats.height.load()); + ret.emplace("Level", p->disp.stats.level.load()); + ret.emplace("NameColor", p->disp.visual.name_color.load()); + ret.emplace("ExtraModel", (p->disp.visual.validation_flags & 2) ? p->disp.visual.extra_model : JSON(nullptr)); + ret.emplace("SectionID", name_for_section_id(p->disp.visual.section_id)); + ret.emplace("CharClass", name_for_char_class(p->disp.visual.section_id)); + ret.emplace("Costume", p->disp.visual.costume.load()); + ret.emplace("Skin", p->disp.visual.skin.load()); + ret.emplace("Face", p->disp.visual.face.load()); + ret.emplace("Head", p->disp.visual.head.load()); + ret.emplace("Hair", p->disp.visual.hair.load()); + ret.emplace("HairR", p->disp.visual.hair_r.load()); + ret.emplace("HairG", p->disp.visual.hair_g.load()); + ret.emplace("HairB", p->disp.visual.hair_b.load()); + ret.emplace("ProportionX", p->disp.visual.proportion_x.load()); + ret.emplace("ProportionY", p->disp.visual.proportion_y.load()); + + ret.emplace("Name", p->disp.name.decode(c->language())); + ret.emplace("PlayTimeSeconds", p->disp.play_time.load()); + + ret.emplace("AutoReply", p->auto_reply.decode(c->language())); + ret.emplace("InfoBoard", p->info_board.decode(c->language())); + auto battle_place_counts = JSON::list({ + p->battle_records.place_counts[0].load(), + p->battle_records.place_counts[1].load(), + p->battle_records.place_counts[2].load(), + p->battle_records.place_counts[3].load(), + }); + ret.emplace("BattlePlaceCounts", std::move(battle_place_counts)); + ret.emplace("BattleDisconnectCount", p->battle_records.disconnect_count.load()); + + if (!is_ep3(c->version())) { + auto json_for_challenge_times = [](const parray, Count>& times) -> JSON { + auto times_json = JSON::list(); + for (size_t z = 0; z < times.size(); z++) { + times_json.emplace_back(times[z].load()); + } + return times_json; + }; + ret.emplace("ChallengeTitleColorXRGB1555", p->challenge_records.title_color.load()); + ret.emplace("ChallengeTimesEp1Online", json_for_challenge_times(p->challenge_records.times_ep1_online)); + ret.emplace("ChallengeTimesEp2Online", json_for_challenge_times(p->challenge_records.times_ep2_online)); + ret.emplace("ChallengeTimesEp1Offline", json_for_challenge_times(p->challenge_records.times_ep1_offline)); + ret.emplace("ChallengeGraveIsEp2", p->challenge_records.grave_is_ep2 ? true : false); + ret.emplace("ChallengeGraveStageNum", p->challenge_records.grave_stage_num); + ret.emplace("ChallengeGraveFloor", p->challenge_records.grave_floor); + ret.emplace("ChallengeGraveDeaths", p->challenge_records.grave_deaths.load()); + { + uint16_t year = 2000 + ((p->challenge_records.grave_time >> 28) & 0x0F); + uint8_t month = (p->challenge_records.grave_time >> 24) & 0x0F; + uint8_t day = (p->challenge_records.grave_time >> 16) & 0xFF; + uint8_t hour = (p->challenge_records.grave_time >> 8) & 0xFF; + uint8_t minute = p->challenge_records.grave_time & 0xFF; + ret.emplace("ChallengeGraveTime", string_printf("%04hu-%02hhu-%02hhu %02hhu:%02hhu:00", year, month, day, hour, minute)); + } + string grave_enemy_types; + if (p->challenge_records.grave_defeated_by_enemy_rt_index) { + for (EnemyType type : enemy_types_for_rare_table_index(p->challenge_records.grave_is_ep2 ? Episode::EP2 : Episode::EP1, p->challenge_records.grave_defeated_by_enemy_rt_index)) { + if (!grave_enemy_types.empty()) { + grave_enemy_types += "/"; + } + grave_enemy_types += name_for_enum(type); + } + } + ret.emplace("ChallengeGraveDefeatedByEnemy", std::move(grave_enemy_types)); + ret.emplace("ChallengeGraveX", p->challenge_records.grave_x.load()); + ret.emplace("ChallengeGraveY", p->challenge_records.grave_y.load()); + ret.emplace("ChallengeGraveZ", p->challenge_records.grave_z.load()); + ret.emplace("ChallengeGraveTeam", p->challenge_records.grave_team.decode()); + ret.emplace("ChallengeGraveMessage", p->challenge_records.grave_message.decode()); + ret.emplace("ChallengeAwardStateEp1OnlineFlags", p->challenge_records.ep1_online_award_state.rank_award_flags.load()); + ret.emplace("ChallengeAwardStateEp1OnlineMaxRank", p->challenge_records.ep1_online_award_state.maximum_rank.load()); + ret.emplace("ChallengeAwardStateEp2OnlineFlags", p->challenge_records.ep2_online_award_state.rank_award_flags.load()); + ret.emplace("ChallengeAwardStateEp2OnlineMaxRank", p->challenge_records.ep2_online_award_state.maximum_rank.load()); + ret.emplace("ChallengeAwardStateEp1OfflineFlags", p->challenge_records.ep1_offline_award_state.rank_award_flags.load()); + ret.emplace("ChallengeAwardStateEp1OfflineMaxRank", p->challenge_records.ep1_offline_award_state.maximum_rank.load()); + ret.emplace("ChallengeRankTitle", p->challenge_records.rank_title.decode()); + } + } + return ret; +} + +JSON HTTPServer::generate_proxy_client_json(shared_ptr ses) const { + struct LobbyPlayer { + uint32_t guild_card_number = 0; + uint64_t xb_user_id = 0; + std::string name; + uint8_t language = 0; + uint8_t section_id = 0; + uint8_t char_class = 0; + }; + std::vector lobby_players; + + auto lobby_players_json = JSON::list(); + for (size_t z = 0; z < ses->lobby_players.size(); z++) { + const auto& p = ses->lobby_players[z]; + if (p.guild_card_number) { + lobby_players_json.emplace_back(JSON::dict({ + {"GuildCardNumber", p.guild_card_number}, + {"Name", p.name}, + {"Language", name_for_language_code(p.language)}, + {"SectionID", name_for_section_id(p.section_id)}, + {"CharClass", name_for_char_class(p.char_class)}, + })); + lobby_players_json.back().emplace("XBUserID", p.xb_user_id ? p.xb_user_id : JSON(nullptr)); + } else { + lobby_players_json.emplace_back(nullptr); + } + } + + auto ret = JSON::dict({ + {"ID", ses->id}, + {"RemoteClientAddress", render_sockaddr_storage(ses->client_channel.remote_addr)}, + {"RemoteServerAddress", render_sockaddr_storage(ses->server_channel.remote_addr)}, + {"LocalPort", ses->local_port}, + {"NextDestination", render_sockaddr_storage(ses->next_destination)}, + {"Version", name_for_enum(ses->version())}, + {"SubVersion", ses->sub_version}, + {"Name", ses->character_name}, + {"DCHardwareID", ses->hardware_id}, + {"RemoteGuildCardNumber", ses->remote_guild_card_number}, + {"RemoteClientConfigData", format_data_string(&ses->remote_client_config_data[0], ses->remote_client_config_data.size())}, + {"Config", this->generate_client_config_json(ses->config)}, + {"Language", name_for_language_code(ses->language())}, + {"LobbyClientID", ses->lobby_client_id}, + {"LeaderClientID", ses->leader_client_id}, + {"LocationX", ses->x}, + {"LocationZ", ses->z}, + {"LocationFloor", ses->floor}, + {"IsInGame", ses->is_in_game}, + {"IsInQuest", ses->is_in_quest}, + {"LobbyEvent", ses->lobby_event}, + {"LobbyDifficulty", name_for_difficulty(ses->lobby_difficulty)}, + {"LobbySectionID", name_for_section_id(ses->lobby_section_id)}, + {"LobbyMode", name_for_mode(ses->lobby_mode)}, + {"LobbyEpisode", name_for_episode(ses->lobby_episode)}, + {"LobbyRandomSeed", ses->lobby_random_seed}, + {"LobbyPlayers", std::move(lobby_players_json)}, + }); + switch (ses->drop_mode) { + case ProxyServer::LinkedSession::DropMode::DISABLED: + ret.emplace("DropMode", "none"); + break; + case ProxyServer::LinkedSession::DropMode::PASSTHROUGH: + ret.emplace("DropMode", "default"); + break; + case ProxyServer::LinkedSession::DropMode::INTERCEPT: + ret.emplace("DropMode", "proxy"); + break; + } + ret.emplace("License", ses->license ? this->generate_license_json(ses->license) : JSON(nullptr)); + return ret; +} + +JSON HTTPServer::generate_lobby_json(shared_ptr l) const { + std::array, 12> clients; + + auto client_ids_json = JSON::list(); + for (size_t z = 0; z < l->max_clients; z++) { + client_ids_json.emplace_back(l->clients[z] ? l->clients[z]->id : JSON(nullptr)); + } + + auto ret = JSON::dict({ + {"ID", l->lobby_id}, + {"AllowedVersions", l->allowed_versions}, + {"Event", l->event}, + {"LeaderClientID", l->leader_id}, + {"MaxClients", l->max_clients}, + {"IdleTimeoutUsecs", l->idle_timeout_usecs}, + {"ClientIDs", std::move(client_ids_json)}, + {"IsGame", l->is_game()}, + {"IsPersistent", l->check_flag(Lobby::Flag::PERSISTENT)}, + }); + + if (l->is_game()) { + ret.emplace("CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)); + ret.emplace("MinLevel", l->min_level + 1); + ret.emplace("MaxLevel", l->max_level + 1); + ret.emplace("BaseVersion", l->base_version); + ret.emplace("Episode", name_for_episode(l->episode)); + ret.emplace("HasPassword", !l->password.empty()); + ret.emplace("Name", l->name); + ret.emplace("RandomSeed", l->random_seed); + if (l->episode != Episode::EP3) { + ret.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)); + ret.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)); + auto variations_json = JSON::list(); + for (size_t z = 0; z < l->variations.size(); z++) { + variations_json.emplace_back(l->variations[z].load()); + } + ret.emplace("Variations", std::move(variations_json)); + ret.emplace("SectionID", name_for_section_id(l->section_id)); + ret.emplace("Mode", name_for_mode(l->mode)); + ret.emplace("Difficulty", name_for_difficulty(l->difficulty)); + ret.emplace("BaseEXPMultiplier", l->base_exp_multiplier); + ret.emplace("AllowedDropModes", l->allowed_drop_modes); + switch (l->drop_mode) { + case Lobby::DropMode::DISABLED: + ret.emplace("DropMode", "none"); + break; + case Lobby::DropMode::CLIENT: + ret.emplace("DropMode", "client"); + break; + case Lobby::DropMode::SERVER_SHARED: + ret.emplace("DropMode", "shared"); + break; + case Lobby::DropMode::SERVER_PRIVATE: + ret.emplace("DropMode", "private"); + break; + case Lobby::DropMode::SERVER_DUPLICATE: + ret.emplace("DropMode", "duplicate"); + break; + } + if (l->mode == GameMode::CHALLENGE) { + ret.emplace("ChallengeEXPMultiplier", l->challenge_exp_multiplier); + if (l->challenge_params) { + ret.emplace("ChallengeStageNumber", l->challenge_params->stage_number); + ret.emplace("ChallengeRankColor", l->challenge_params->rank_color); + ret.emplace("ChallengeRankText", l->challenge_params->rank_text); + ret.emplace("ChallengeRank0ThresholdBitmask", l->challenge_params->rank_thresholds[0].bitmask); + ret.emplace("ChallengeRank0ThresholdSeconds", l->challenge_params->rank_thresholds[0].seconds); + ret.emplace("ChallengeRank1ThresholdBitmask", l->challenge_params->rank_thresholds[1].bitmask); + ret.emplace("ChallengeRank1ThresholdSeconds", l->challenge_params->rank_thresholds[1].seconds); + ret.emplace("ChallengeRank2ThresholdBitmask", l->challenge_params->rank_thresholds[2].bitmask); + ret.emplace("ChallengeRank2ThresholdSeconds", l->challenge_params->rank_thresholds[2].seconds); + } + } + + auto floor_items_json = JSON::list(); + for (size_t floor = 0; floor < l->floor_item_managers.size(); floor++) { + for (const auto& it : l->floor_item_managers[floor].items) { + const auto& item = it.second; + string description = this->state->describe_item(l->base_version, item->data, false); + string data_str = item->data.hex(); + auto item_dict = JSON::dict({ + {"LocationFloor", floor}, + {"LocationX", item->x}, + {"LocationZ", item->z}, + {"DropNumber", item->drop_number}, + {"VisibilityFlags", item->visibility_flags}, + {"Data", std::move(data_str)}, + {"Description", std::move(description)}, + {"ItemID", item->data.id.load()}, + }); + floor_items_json.emplace_back(std::move(item_dict)); + } + } + ret.emplace("FloorItems", std::move(floor_items_json)); + if (l->quest) { + auto battle_rules_json = l->quest->battle_rules ? l->quest->battle_rules->json() : nullptr; + auto challenge_template_index_json = (l->quest->challenge_template_index >= 0) + ? l->quest->challenge_template_index + : JSON(nullptr); + auto quest_json = JSON::dict({ + {"Number", l->quest->quest_number}, + {"Episode", name_for_episode(l->quest->episode)}, + {"Joinable", l->quest->joinable}, + {"Name", l->quest->name}, + {"BattleRules", std::move(battle_rules_json)}, + {"ChallengeTemplateIndex", std::move(challenge_template_index_json)}, + }); + ret.emplace("Quest", std::move(quest_json)); + } else { + ret.emplace("Quest", nullptr); + } + + } else { + ret.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); + ret.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)); + ret.emplace("SpectatorsForbidden", l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN)); + + auto ep3s = l->ep3_server; + if (ep3s) { + auto players_json = JSON::list(); + for (size_t z = 0; z < 4; z++) { + if (!ep3s->name_entries[z].present) { + players_json.emplace_back(nullptr); + } else { + auto lc = l->clients[z]; + + auto deck_entry = ep3s->deck_entries[z]; + JSON deck_json = nullptr; + if (deck_entry) { + auto cards_json = JSON::list(); + for (size_t w = 0; w < deck_entry->card_ids.size(); w++) { + try { + const auto& ce = ep3s->options.card_index->definition_for_id(deck_entry->card_ids[w]); + auto name = ce->def.en_name.decode(); + if (name.empty()) { + name = ce->def.en_short_name.decode(); + } + if (name.empty()) { + name = ce->def.jp_name.decode(); + } + if (name.empty()) { + name = ce->def.jp_short_name.decode(); + } + cards_json.emplace_back(name); + } catch (const out_of_range&) { + cards_json.emplace_back(deck_entry->card_ids[w].load()); + } + } + deck_json = JSON::dict({ + {"Name", deck_entry->name.decode(lc ? lc->language() : 1)}, + {"TeamID", deck_entry->team_id.load()}, + {"Cards", std::move(cards_json)}, + {"GodWhimFlag", deck_entry->god_whim_flag}, + {"PlayerLevel", deck_entry->player_level.load()}, + }); + } + + auto player_json = JSON::dict({ + {"PlayerName", ep3s->name_entries[z].name.decode(lc ? lc->language() : 1)}, + {"ClientID", ep3s->name_entries[z].client_id}, + {"IsCOM", !!ep3s->name_entries[z].is_cpu_player}, + {"Deck", std::move(deck_json)}, + }); + players_json.emplace_back(std::move(player_json)); + } + } + auto battle_state_json = JSON::dict({ + {"BehaviorFlags", ep3s->options.behavior_flags}, + {"RandomSeed", ep3s->options.random_crypt ? ep3s->options.random_crypt->seed() : JSON(nullptr)}, + {"RandomOffset", ep3s->options.random_crypt ? ep3s->options.random_crypt->absolute_offset() : JSON(nullptr)}, + {"Tournament", ep3s->options.tournament ? ep3s->options.tournament->json() : nullptr}, + {"MapNumber", ep3s->last_chosen_map ? ep3s->last_chosen_map->map_number : JSON(nullptr)}, + {"EnvironmentNumber", ep3s->map_and_rules ? ep3s->map_and_rules->environment_number : JSON(nullptr)}, + {"Rules", ep3s->map_and_rules ? ep3s->map_and_rules->rules.json() : nullptr}, + {"Players", std::move(players_json)}, + {"IsBattleFinished", ep3s->battle_finished}, + {"IsBattleInprogress", ep3s->battle_in_progress}, + {"RoundNumber", ep3s->round_num}, + {"FirstTeamTurn", ep3s->first_team_turn}, + {"CurrentTeamTurn", ep3s->current_team_turn1}, + {"BattlePhase", name_for_enum(ep3s->battle_phase)}, + {"SetupPhase", ep3s->setup_phase}, + {"RegistrationPhase", ep3s->registration_phase}, + {"ActionSubphase", ep3s->action_subphase}, + {"BattleStartTimeUsecs", ep3s->battle_start_usecs}, + {"TeamEXP", JSON::list({ep3s->team_exp[0], ep3s->team_exp[1]})}, + {"TeamDiceBonus", JSON::list({ep3s->team_dice_bonus[0], ep3s->team_dice_bonus[1]})}, + }); + // std::shared_ptr state_flags; + // std::array, 4> player_states; + ret.emplace("Episode3BattleState", std::move(battle_state_json)); + } else { + ret.emplace("Episode3BattleState", nullptr); + } + auto watched_lobby = l->watched_lobby.lock(); + if (watched_lobby) { + ret.emplace("WatchedLobbyID", watched_lobby->lobby_id); + } + auto watcher_lobby_ids_json = JSON::list(); + for (const auto& watcher_lobby : l->watcher_lobbies) { + watcher_lobby_ids_json.emplace_back(watcher_lobby->lobby_id); + } + ret.emplace("WatcherLobbyIDs", std::move(watcher_lobby_ids_json)); + ret.emplace("IsReplayLobby", !!l->battle_player); + } + + } else { // Not game + ret.emplace("IsPublic", l->check_flag(Lobby::Flag::PUBLIC)); + ret.emplace("IsDefault", l->check_flag(Lobby::Flag::DEFAULT)); + ret.emplace("IsOverflow", l->check_flag(Lobby::Flag::IS_OVERFLOW)); + ret.emplace("Block", l->block); + } + return ret; +} + +JSON HTTPServer::generate_game_server_clients_json() const { + JSON res = JSON::list(); + for (const auto& it : this->state->channel_to_client) { + res.emplace_back(this->generate_game_client_json(it.second)); + } + return res; +} + +JSON HTTPServer::generate_proxy_server_clients_json() const { + JSON res = JSON::list(); + for (const auto& it : this->state->proxy_server->all_sessions()) { + res.emplace_back(this->generate_proxy_client_json(it.second)); + } + return res; +} + +JSON HTTPServer::generate_lobbies_json() const { + JSON res = JSON::list(); + for (const auto& it : this->state->id_to_lobby) { + res.emplace_back(this->generate_lobby_json(it.second)); + } + return res; +} + +JSON HTTPServer::generate_summary_json() const { + auto clients_json = JSON::list(); + for (const auto& it : this->state->channel_to_client) { + auto c = it.second; + auto p = c->character(false, false); + auto l = c->lobby.lock(); + clients_json.emplace_back(JSON::dict({ + {"ID", c->id}, + {"SerialNumber", c->license ? c->license->serial_number : JSON(nullptr)}, + {"Name", p ? p->disp.name.decode(it.second->language()) : JSON(nullptr)}, + {"Version", name_for_enum(it.second->version())}, + {"Language", name_for_language_code(it.second->language())}, + {"Level", p ? p->disp.stats.level + 1 : JSON(nullptr)}, + {"Class", p ? name_for_char_class(p->disp.visual.char_class) : JSON(nullptr)}, + {"SectionID", p ? name_for_section_id(p->disp.visual.section_id) : JSON(nullptr)}, + {"LobbyID", l ? l->lobby_id : JSON(nullptr)}, + })); + } + + auto proxy_clients_json = JSON::list(); + for (const auto& it : this->state->proxy_server->all_sessions()) { + proxy_clients_json.emplace_back(JSON::dict({ + {"SerialNumber", it.second->license ? it.second->license->serial_number : JSON(nullptr)}, + {"Name", it.second->character_name}, + {"Version", name_for_enum(it.second->version())}, + {"Language", name_for_language_code(it.second->language())}, + })); + } + + auto games_json = JSON::list(); + for (const auto& it : this->state->id_to_lobby) { + auto l = it.second; + if (l->is_game()) { + games_json.emplace_back(JSON::dict({ + {"ID", l->lobby_id}, + {"Name", l->name}, + {"BaseVersion", name_for_enum(l->base_version)}, + {"Players", l->count_clients()}, + {"CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)}, + {"QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)}, + {"JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)}, + {"BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)}, + {"IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)}, + {"SectionID", name_for_section_id(l->section_id)}, + {"Episode", name_for_episode(l->episode)}, + {"Mode", name_for_mode(l->mode)}, + {"Difficulty", name_for_difficulty(l->difficulty)}, + {"HasPassword", !l->password.empty()}, + })); + auto ep3s = l->ep3_server; + if (l->episode == Episode::EP3 && ep3s) { + games_json.back().emplace("Ep3MapNumber", ep3s->last_chosen_map ? ep3s->last_chosen_map->map_number : JSON(nullptr)); + games_json.back().emplace("Ep3Rules", ep3s->map_and_rules ? ep3s->map_and_rules->rules.json() : nullptr); + } + } + } + + return JSON::dict({ + {"Clients", std::move(clients_json)}, + {"ProxyClients", std::move(proxy_clients_json)}, + {"Games", std::move(games_json)}, + }); +} + +JSON HTTPServer::generate_all_json() const { + return JSON::dict({ + {"Clients", this->generate_game_server_clients_json()}, + {"ProxyClients", this->generate_proxy_server_clients_json()}, + {"Lobbies", this->generate_lobbies_json()}, + }); +} + +void HTTPServer::handle_request(struct evhttp_request* req) { + JSON ret; + uint32_t serialize_options = 0; + try { + string uri = evhttp_request_get_uri(req); + + std::unordered_multimap query; + size_t query_pos = uri.find('?'); + if (query_pos != string::npos) { + query = this->parse_url_params(uri.substr(query_pos + 1)); + uri.resize(query_pos); + } + + static const string default_format_option = "false"; + if (this->get_url_param(query, "format", &default_format_option) == "true") { + serialize_options = JSON::SerializeOption::FORMAT | JSON::SerializeOption::SORT_DICT_KEYS; + } + + if (uri == "/") { + auto endpoints_json = JSON::list({ + "/y/clients", + "/y/proxy-clients", + "/y/lobbies", + "/y/summary", + "/y/all", + }); + ret = JSON::dict({{"endpoints", std::move(endpoints_json)}}); + + } else if (uri == "/y/clients") { + ret = this->generate_game_server_clients_json(); + } else if (uri == "/y/proxy-clients") { + ret = this->generate_proxy_server_clients_json(); + } else if (uri == "/y/lobbies") { + ret = this->generate_lobbies_json(); + } else if (uri == "/y/summary") { + ret = this->generate_summary_json(); + } else if (uri == "/y/all") { + ret = this->generate_all_json(); + + } else { + throw http_error(404, "unknown action"); + } + + } catch (const http_error& e) { + unique_ptr out_buffer(evbuffer_new(), evbuffer_free); + evbuffer_add_printf(out_buffer.get(), "%s", e.what()); + this->send_response(req, e.code, "text/plain", out_buffer.get()); + return; + + } catch (const exception& e) { + unique_ptr out_buffer(evbuffer_new(), evbuffer_free); + evbuffer_add_printf(out_buffer.get(), "Error during request: %s", e.what()); + this->send_response(req, 500, "text/plain", out_buffer.get()); + server_log.warning("internal server error during http request: %s", e.what()); + return; + } + + unique_ptr out_buffer(evbuffer_new(), evbuffer_free); + string* serialized = new string(ret.serialize(JSON::SerializeOption::ESCAPE_CONTROLS_ONLY | serialize_options)); + auto cleanup = +[](const void*, size_t, void* s) -> void { + delete reinterpret_cast(s); + }; + evbuffer_add_reference(out_buffer.get(), serialized->data(), serialized->size(), cleanup, serialized); + this->send_response(req, 200, "application/json", out_buffer.get()); +} diff --git a/src/HTTPServer.hh b/src/HTTPServer.hh new file mode 100644 index 00000000..762f81cb --- /dev/null +++ b/src/HTTPServer.hh @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "ProxyServer.hh" +#include "ServerState.hh" + +class HTTPServer { +public: + HTTPServer(std::shared_ptr state); + HTTPServer(const HTTPServer&) = delete; + HTTPServer(HTTPServer&&) = delete; + HTTPServer& operator=(const HTTPServer&) = delete; + HTTPServer& operator=(HTTPServer&&) = delete; + virtual ~HTTPServer() = default; + + void listen(const std::string& socket_path); + void listen(const std::string& addr, int port); + void listen(int port); + void add_socket(int fd); + +protected: + class http_error : public std::runtime_error { + public: + http_error(int code, const std::string& what); + int code; + }; + + std::shared_ptr state; + std::shared_ptr http; + + static void dispatch_handle_request(struct evhttp_request* req, void* ctx); + void handle_request(struct evhttp_request* req); + + static const std::unordered_map explanation_for_response_code; + static void send_response(struct evhttp_request* req, int code, const char* content_type, struct evbuffer* b); + static void send_response(struct evhttp_request* req, int code, const char* content_type, const char* fmt, ...); + + static std::unordered_multimap parse_url_params(const std::string& query); + static std::unordered_map parse_url_params_unique(const std::string& query); + static const std::string& get_url_param( + const std::unordered_multimap& params, + const std::string& key, + const std::string* _default = nullptr); + + JSON generate_client_config_json(const Client::Config& config) const; + JSON generate_license_json(std::shared_ptr l) const; + JSON generate_game_client_json(std::shared_ptr c) const; + JSON generate_proxy_client_json(std::shared_ptr ses) const; + JSON generate_lobby_json(std::shared_ptr l) const; + JSON generate_game_server_clients_json() const; + JSON generate_proxy_server_clients_json() const; + JSON generate_lobbies_json() const; + JSON generate_summary_json() const; + JSON generate_all_json() const; +}; diff --git a/src/Main.cc b/src/Main.cc index a2c4377e..2125334a 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -28,6 +28,7 @@ #include "DNSServer.hh" #include "GSLArchive.hh" #include "GVMEncoder.hh" +#include "HTTPServer.hh" #include "IPStackSimulator.hh" #include "Loggers.hh" #include "NetworkAddresses.hh" @@ -2368,6 +2369,7 @@ Action a_run_server_replay_log( shared_ptr shell; shared_ptr replay_session; shared_ptr ip_stack_simulator; + shared_ptr http_server; if (is_replay) { config_log.info("Starting proxy server"); state->proxy_server = make_shared(base, state); @@ -2453,6 +2455,15 @@ Action a_run_server_replay_log( ip_stack_simulator->listen(spec, netloc.first, netloc.second, FrameInfo::LinkType::HDLC); } } + + if (!state->http_addresses.empty() || !state->http_addresses.empty()) { + config_log.info("Starting HTTP server"); + http_server = make_shared(state); + for (const auto& it : state->http_addresses) { + auto netloc = parse_netloc(it); + http_server->listen(netloc.first, netloc.second); + } + } } if (!state->username.empty()) { diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index da68dd42..88402602 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -843,7 +843,7 @@ void ProxyServer::LinkedSession::on_input(Channel& ch, uint16_t command, uint32_ } } -shared_ptr ProxyServer::get_session() { +shared_ptr ProxyServer::get_session() const { if (this->id_to_session.empty()) { throw runtime_error("no sessions exist"); } @@ -853,8 +853,7 @@ shared_ptr ProxyServer::get_session() { return this->id_to_session.begin()->second; } -shared_ptr ProxyServer::get_session_by_name( - const std::string& name) { +shared_ptr ProxyServer::get_session_by_name(const std::string& name) const { try { uint64_t session_id = stoull(name, nullptr, 16); return this->id_to_session.at(session_id); @@ -865,6 +864,10 @@ shared_ptr ProxyServer::get_session_by_name( } } +const unordered_map>& ProxyServer::all_sessions() const { + return this->id_to_session; +} + shared_ptr ProxyServer::create_licensed_session( shared_ptr l, uint16_t local_port, diff --git a/src/ProxyServer.hh b/src/ProxyServer.hh index b30cba38..31499800 100644 --- a/src/ProxyServer.hh +++ b/src/ProxyServer.hh @@ -173,13 +173,17 @@ public: void clear_lobby_players(size_t num_slots); + void set_drop_mode(DropMode new_mode); + void send_to_game_server(const char* error_message = nullptr); void disconnect(); bool is_connected() const; }; - std::shared_ptr get_session(); - std::shared_ptr get_session_by_name(const std::string& name); + std::shared_ptr get_session() const; + std::shared_ptr get_session_by_name(const std::string& name) const; + const std::unordered_map>& all_sessions() const; + std::shared_ptr create_licensed_session( std::shared_ptr l, uint16_t local_port, diff --git a/src/Server.cc b/src/Server.cc index 1ee70048..8f59385d 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -216,11 +216,7 @@ Server::Server( destroy_clients_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &Server::dispatch_destroy_clients, this), event_free), state(state) {} -void Server::listen( - const std::string& addr_str, - const string& socket_path, - Version version, - ServerBehavior behavior) { +void Server::listen(const std::string& addr_str, const string& socket_path, Version version, ServerBehavior behavior) { int fd = ::listen(socket_path, 0, SOMAXCONN); server_log.info("Listening on Unix socket %s on fd %d as %s", socket_path.c_str(), fd, addr_str.c_str()); diff --git a/src/ServerState.cc b/src/ServerState.cc index a3a4e1e8..9ed6ab50 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -615,6 +615,16 @@ void ServerState::load_config() { } } catch (const out_of_range&) { } + try { + for (const auto& item : json.at("HTTPListen").as_list()) { + if (item->is_int()) { + this->http_addresses.emplace_back(string_printf("0.0.0.0:%" PRId64, item->as_int())); + } else { + this->http_addresses.emplace_back(item->as_string()); + } + } + } catch (const out_of_range&) { + } } auto local_address_str = json.at("LocalAddress").as_string(); diff --git a/src/ServerState.hh b/src/ServerState.hh index b98fac9b..ba327704 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -78,6 +78,7 @@ struct ServerState : public std::enable_shared_from_this { uint16_t dns_server_port = 0; std::vector ip_stack_addresses; std::vector ppp_stack_addresses; + std::vector http_addresses; uint64_t client_ping_interval_usecs = 30000000; uint64_t client_idle_timeout_usecs = 60000000; uint64_t patch_client_idle_timeout_usecs = 300000000; diff --git a/src/StaticGameData.cc b/src/StaticGameData.cc index 67acd0af..150b2f7a 100644 --- a/src/StaticGameData.cc +++ b/src/StaticGameData.cc @@ -229,7 +229,7 @@ const char* abbreviation_for_section_id(uint8_t section_id) { if (section_id < section_id_to_abbreviation.size()) { return section_id_to_abbreviation[section_id]; } else { - return ""; + return "Unknown"; } } @@ -237,7 +237,7 @@ const char* name_for_section_id(uint8_t section_id) { if (section_id < section_id_to_name.size()) { return section_id_to_name[section_id]; } else { - return ""; + return "Unknown"; } } @@ -262,7 +262,7 @@ const string& name_for_event(uint8_t event) { if (event < lobby_event_to_name.size()) { return lobby_event_to_name[event]; } else { - static const string ret = ""; + static const string ret = "Unknown lobby event"; return ret; } } @@ -287,7 +287,7 @@ const string& name_for_lobby_type(uint8_t type) { try { return lobby_type_to_name.at(type); } catch (const out_of_range&) { - static const string ret = ""; + static const string ret = "Unknown lobby type"; return ret; } } @@ -312,7 +312,7 @@ const string& name_for_npc(uint8_t npc) { try { return npc_id_to_name.at(npc); } catch (const out_of_range&) { - static const string ret = ""; + static const string ret = "Unknown NPC"; return ret; } } @@ -459,6 +459,18 @@ char abbreviation_for_difficulty(uint8_t difficulty) { } } +const char* name_for_language_code(uint8_t language_code) { + array names = {{"Japanese", + "English", + "German", + "French", + "Spanish", + "Simplified Chinese", + "Traditional Chinese", + "Korean"}}; + return (language_code < 8) ? names[language_code] : "Unknown"; +} + char char_for_language_code(uint8_t language_code) { return (language_code < 8) ? "JEGFSBTK"[language_code] : '?'; } @@ -544,7 +556,7 @@ const string& name_for_technique(uint8_t tech) { try { return tech_id_to_name.at(tech); } catch (const out_of_range&) { - static const string ret = ""; + static const string ret = "Unknown technique"; return ret; } } diff --git a/src/StaticGameData.hh b/src/StaticGameData.hh index dfcfbdad..b1e534d7 100644 --- a/src/StaticGameData.hh +++ b/src/StaticGameData.hh @@ -68,6 +68,7 @@ const char* name_for_difficulty(uint8_t difficulty); const char* token_name_for_difficulty(uint8_t difficulty); char abbreviation_for_difficulty(uint8_t difficulty); +const char* name_for_language_code(uint8_t language_code); char char_for_language_code(uint8_t language_code); uint8_t language_code_for_char(char language_char); diff --git a/system/config.example.json b/system/config.example.json index 196246f9..10ddaf0b 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -150,11 +150,19 @@ // On Windows, Unix sockets are not available, so you can only use TCP sockets // here. You can get Dolphin to connect locally by adding a port to this list // and configuring Dolphin to connect to the same port. For example, you could - // set this to ["127.0.0.1:5059"], and configure Dolphin's tapserver BBA to - // connect to 127.0.0.1:5059. + // set this to ["127.0.0.1:5059"] (which listens on port 5059 but only accepts + // connections from the local machine), and configure Dolphin's tapserver BBA + // to connect to 127.0.0.1:5059. "IPStackListen": [], "PPPStackListen": [], + // Where to listen for HTTP connections. The HTTP server is intended as a + // private interface to interact with newserv from e.g. an in-house Web portal + // or Discord bot. It would be unwise to expose any of these ports to the + // public Internet (hence why the default here is blank). The format of + // entries in this list is the same as for IPStackListen and PPPStackListen. + "HTTPListen": [], + // Other servers to support proxying to. If this is empty for any game // version, the proxy server is disabled for that version. Entries in these // dictionaries should be of the form "name": "address:port"; the names are diff --git a/tests/config.json b/tests/config.json index 4ed758d0..257e0025 100644 --- a/tests/config.json +++ b/tests/config.json @@ -38,6 +38,7 @@ "DNSServerPort": 0, "IPStackListen": [], "PPPStackListen": [], + "HTTPListen": [], "Episode3BehaviorFlags": 0xFA, "Episode3InfiniteMeseta": false,