diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 91f5f962..58ca397d 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -205,6 +205,22 @@ static void server_command_dbgid(shared_ptr, shared_ptr, c->prefer_high_lobby_client_id ? "high" : "low"); } +static void server_command_auction(shared_ptr, shared_ptr l, + shared_ptr c, const std::u16string&) { + check_privileges(c, Privilege::DEBUG); + if (l->is_game() && (l->flags & Lobby::Flag::EPISODE_3_ONLY)) { + G_InitiateCardAuction_GC_Ep3_6xB5x42 cmd; + send_command_t(l, 0xC9, 0x00, cmd); + } +} + +static void proxy_command_auction(shared_ptr, + ProxyServer::LinkedSession& session, const std::u16string&) { + G_InitiateCardAuction_GC_Ep3_6xB5x42 cmd; + session.client_channel.send(0xC9, 0x00, &cmd, sizeof(cmd)); + session.server_channel.send(0xC9, 0x00, &cmd, sizeof(cmd)); +} + static void server_command_patch(shared_ptr s, shared_ptr, shared_ptr c, const std::u16string& args) { std::shared_ptr fn; @@ -976,6 +992,7 @@ static const unordered_map chat_commands({ {u"$allevent", {server_command_lobby_event_all, nullptr, u"Usage:\nallevent "}}, {u"$ann", {server_command_announce, nullptr, u"Usage:\nann "}}, {u"$arrow", {server_command_arrow, proxy_command_arrow, u"Usage:\narrow "}}, + {u"$auction", {server_command_auction, proxy_command_auction, u"Usage:\nauction"}}, {u"$ax", {server_command_ax, nullptr, u"Usage:\nax "}}, {u"$ban", {server_command_ban, nullptr, u"Usage:\nban "}}, // TODO: implement this on proxy server diff --git a/src/Client.hh b/src/Client.hh index 0535c1c7..acd38641 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -33,43 +33,45 @@ struct Client { // Note that this flag is NOT set for Episode 3 Trial Edition clients, since // that version is similar enough to the release version of Episode 3 that // newserv does not have to change its behavior at all. - IS_TRIAL_EDITION = 0x2000, + IS_TRIAL_EDITION = 0x00002000, // Client is DC v1 - IS_DC_V1 = 0x0010, + IS_DC_V1 = 0x00000010, // For patch server clients, client is Blue Burst rather than PC - IS_BB_PATCH = 0x0001, + IS_BB_PATCH = 0x00000001, // After joining a lobby, client will no longer send D6 commands when they // close message boxes - NO_D6_AFTER_LOBBY = 0x0002, + NO_D6_AFTER_LOBBY = 0x00000002, // Client has the above flag and has already joined a lobby, or is not GC - NO_D6 = 0x0004, + NO_D6 = 0x00000004, // Client is Episode 3, should be able to see CARD lobbies, and should only // be able to see/join games with the EPISODE_3_ONLY flag - IS_EPISODE_3 = 0x0008, + IS_EPISODE_3 = 0x00000008, // Client disconnects if it receives B2 (send_function_call) - NO_SEND_FUNCTION_CALL = 0x0200, + NO_SEND_FUNCTION_CALL = 0x00000200, // Client requires doubly-encrypted code section in send_function_call - ENCRYPTED_SEND_FUNCTION_CALL = 0x0800, + ENCRYPTED_SEND_FUNCTION_CALL = 0x00000800, // Client supports send_function_call but does not actually run the code - SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x1000, + SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x00001000, // Client is vulnerable to a buffer overflow that we can use to enable // send_function_call - USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x8000, + USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x00008000, // Client is loading into a game - LOADING = 0x0020, + LOADING = 0x00000020, // Client is loading a quest - LOADING_QUEST = 0x0040, + LOADING_QUEST = 0x00000040, + // Client is waiting to join an Episode 3 card auction + AWAITING_CARD_AUCTION = 0x00010000, // Client is in the information menu (login server only) - IN_INFORMATION_MENU = 0x0080, + IN_INFORMATION_MENU = 0x00000080, // Client is at the welcome message (login server only) - AT_WELCOME_MESSAGE = 0x0100, + AT_WELCOME_MESSAGE = 0x00000100, // Client has already received a 97 (enable saves) command, so don't show // the programs menu anymore - SAVE_ENABLED = 0x0400, + SAVE_ENABLED = 0x00000400, // Client has received newserv's Episode 3 card definitions, so don't send // them again - HAS_EP3_CARD_DEFS = 0x4000, + HAS_EP3_CARD_DEFS = 0x00004000, }; uint64_t id; @@ -82,7 +84,7 @@ struct Client { // config can be up to 0x20 bytes; on BB it can be 0x28 bytes. We don't use // all of that space. uint8_t bb_game_state; - uint16_t flags; + uint32_t flags; // Network Channel channel; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index ea7b9951..006a2ac5 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -71,10 +71,10 @@ enum ClientStateBB : uint8_t { struct ClientConfig { uint64_t magic; - uint16_t flags; + uint32_t flags; uint32_t proxy_destination_address; uint16_t proxy_destination_port; - parray unused; + parray unused; } __packed__; struct ClientConfigBB { diff --git a/src/Episode3/DataIndex.cc b/src/Episode3/DataIndex.cc index 2c42f287..04eab1e7 100644 --- a/src/Episode3/DataIndex.cc +++ b/src/Episode3/DataIndex.cc @@ -1241,6 +1241,11 @@ DataIndex::DataIndex(const string& directory, bool debug) "duplicate card id: %08" PRIX32, entry->def.card_id.load())); } + // Some cards intentionally have the same name, so we just leave them + // unindexed (they can still be looked up by ID, of course) + string name = entry->def.en_name; + this->card_definitions_by_name.emplace(name, entry); + entry->def.hp.decode_code(); entry->def.ap.decode_code(); entry->def.tp.decode_code(); @@ -1320,6 +1325,11 @@ shared_ptr DataIndex::definition_for_card_id( return this->card_definitions.at(id); } +shared_ptr DataIndex::definition_for_card_name( + const string& name) const { + return this->card_definitions_by_name.at(name); +} + set DataIndex::all_card_ids() const { set ret; for (const auto& it : this->card_definitions) { diff --git a/src/Episode3/DataIndex.hh b/src/Episode3/DataIndex.hh index 92167337..5a52537c 100644 --- a/src/Episode3/DataIndex.hh +++ b/src/Episode3/DataIndex.hh @@ -790,6 +790,8 @@ public: const std::string& get_compressed_card_definitions() const; std::shared_ptr definition_for_card_id(uint32_t id) const; + std::shared_ptr definition_for_card_name( + const std::string& name) const; std::set all_card_ids() const; const std::string& get_compressed_map_list() const; @@ -801,6 +803,7 @@ private: std::string compressed_card_definitions; std::unordered_map> card_definitions; + std::unordered_map> card_definitions_by_name; // The compressed map list is generated on demand from the maps map below. // It's marked mutable because the logical consistency of the DataIndex object diff --git a/src/Main.cc b/src/Main.cc index df187769..99cfb362 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -160,6 +160,34 @@ void populate_state_from_config(shared_ptr s, s->ep3_behavior_flags = 0; } + try { + s->ep3_card_auction_points = d.at("CardAuctionPoints")->as_int(); + } catch (const out_of_range&) { + s->ep3_card_auction_points = 0; + } + try { + auto i = d.at("CardAuctionSize"); + if (i->is_int()) { + s->ep3_card_auction_min_size = i->as_int(); + s->ep3_card_auction_max_size = s->ep3_card_auction_min_size; + } else { + s->ep3_card_auction_min_size = i->as_list().at(0)->as_int(); + s->ep3_card_auction_max_size = i->as_list().at(1)->as_int(); + } + } catch (const out_of_range&) { + s->ep3_card_auction_min_size = 0; + s->ep3_card_auction_max_size = 0; + } + + try { + for (const auto& it : d.at("CardAuctionPool")->as_dict()) { + const auto& card_name = it.first; + const auto& card_cfg_json = it.second->as_list(); + s->ep3_card_auction_pool.emplace(card_name, make_pair( + card_cfg_json.at(0)->as_int(), card_cfg_json.at(1)->as_int())); + } + } catch (const out_of_range&) { } + shared_ptr log_levels_json; try { log_levels_json = d.at("LogLevels"); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 843718ad..8cff5561 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -923,6 +923,15 @@ static void on_ep3_server_data_request(shared_ptr s, shared_ptrep3_server_base = make_shared( l, s->ep3_data_index, s->ep3_behavior_flags, l->random_seed); l->ep3_server_base->init(); + + if (s->ep3_behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES) { + for (size_t z = 0; z < l->max_clients; z++) { + if (l->clients[z]) { + send_text_message_printf(l->clients[z], "Your client ID: $C6%zu", z); + } + } + send_text_message_printf(l, "State seed: $C6%08" PRIX32, l->random_seed); + } } l->ep3_server_base->server->on_server_data_input(data); } @@ -2831,6 +2840,85 @@ static void on_card_trade(shared_ptr s, shared_ptr c, } } +static void on_card_auction_join(shared_ptr s, shared_ptr c, + uint16_t, uint32_t, const string& data) { // EF + check_size_v(data.size(), 0); + + if (!(c->flags & Client::Flag::IS_EPISODE_3)) { + throw runtime_error("non-Ep3 client sent card auction join command"); + } + auto l = s->find_lobby(c->lobby_id); + if (!(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { + throw runtime_error("client sent card auction join command outside of Ep3 lobby"); + } + if (!l->is_game()) { + throw runtime_error("client sent card auction join command in non-game lobby"); + } + + if (c->flags & Client::Flag::AWAITING_CARD_AUCTION) { + return; + } + c->flags |= Client::Flag::AWAITING_CARD_AUCTION; + + // Check if any client is still loading + // TODO: We need to handle clients disconnecting during this procedure. + // Probably on_client_disconnect needs to check for this case... + size_t x; + for (x = 0; x < l->max_clients; x++) { + if (!l->clients[x]) { + continue; + } + if (!(l->clients[x]->flags & Client::Flag::AWAITING_CARD_AUCTION)) { + break; + } + } + if (x != l->max_clients) { + return; + } + + for (x = 0; x < l->max_clients; x++) { + if (l->clients[x]) { + l->clients[x]->flags &= ~Client::Flag::AWAITING_CARD_AUCTION; + } + } + + if ((s->ep3_card_auction_points == 0) || + (s->ep3_card_auction_min_size == 0) || + (s->ep3_card_auction_max_size == 0)) { + throw runtime_error("card auctions are not configured on this server"); + } + + uint16_t num_cards; + if (s->ep3_card_auction_min_size == s->ep3_card_auction_max_size) { + num_cards = s->ep3_card_auction_min_size; + } else { + num_cards = s->ep3_card_auction_min_size + + (random_object() % (s->ep3_card_auction_max_size - s->ep3_card_auction_min_size + 1)); + } + num_cards = min(num_cards, 0x14); + + uint64_t distribution_size = 0; + for (const auto& it : s->ep3_card_auction_pool) { + distribution_size += it.second.first; + } + + S_StartCardAuction_GC_Ep3_EF cmd; + cmd.points_available = s->ep3_card_auction_points; + for (size_t z = 0; z < num_cards; z++) { + uint64_t v = random_object() % distribution_size; + for (const auto& it : s->ep3_card_auction_pool) { + if (v >= it.second.first) { + v -= it.second.first; + } else { + cmd.entries[z].card_id = s->ep3_data_index->definition_for_card_name(it.first)->def.card_id.load(); + cmd.entries[z].min_price = it.second.second; + break; + } + } + } + send_command_t(l, 0xEF, num_cards, cmd); +} + static void on_team_command_bb(shared_ptr, shared_ptr c, @@ -3263,7 +3351,7 @@ static on_command_t handlers[0x100][6] = { /* EC */ {nullptr, nullptr, nullptr, on_create_game_dc_v3, nullptr, on_leave_char_select_bb, }, /* EC */ /* ED */ {nullptr, nullptr, nullptr, nullptr, nullptr, on_change_account_data_bb, }, /* ED */ /* EE */ {nullptr, nullptr, nullptr, on_card_trade, nullptr, nullptr, }, /* EE */ - /* EF */ {nullptr, nullptr, nullptr, on_ignored_command, nullptr, nullptr, }, /* EF */ + /* EF */ {nullptr, nullptr, nullptr, on_card_auction_join, nullptr, nullptr, }, /* EF */ /* F0 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F0 */ /* F1 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F1 */ /* F2 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* F2 */ diff --git a/src/ServerState.cc b/src/ServerState.cc index 03853eef..434ac3ac 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -25,7 +25,11 @@ ServerState::ServerState() episode_3_send_function_call_enabled(false), catch_handler_exceptions(true), ep3_behavior_flags(0), - run_shell_behavior(RunShellBehavior::DEFAULT), next_lobby_id(1), + run_shell_behavior(RunShellBehavior::DEFAULT), + ep3_card_auction_points(0), + ep3_card_auction_min_size(0), + ep3_card_auction_max_size(0), + next_lobby_id(1), pre_lobby_event(0), ep3_menu_song(-1) { vector> non_v1_only_lobbies; diff --git a/src/ServerState.hh b/src/ServerState.hh index bae36be8..4e257032 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -66,6 +66,11 @@ struct ServerState { std::shared_ptr bb_data_gsl; std::shared_ptr rare_item_set; + uint16_t ep3_card_auction_points; + uint16_t ep3_card_auction_min_size; + uint16_t ep3_card_auction_max_size; + std::unordered_map> ep3_card_auction_pool; + std::shared_ptr license_manager; std::vector main_menu; diff --git a/system/config.example.json b/system/config.example.json index 19d42168..73f45e5b 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -249,9 +249,37 @@ // "Episode3MenuSong": 0, // Episode 3 battle behavior flags. When set to zero, battles behave as they - // did on the original Sega servers. - // TODO: Document what nonzero values do here. - "Episode3BehaviorFlags": 0, + // did on the original Sega servers. Combinations of behaviors can be enabled + // by bitwise-OR'ing together the following values: + // 0x00000001 => Disable deck verification entirely + // 0x00000002 => Disable owned card count check during deck verification (this + // enables the use of the non-saveable Have All Cards code, but + // retains all the other validity checks) + // 0x00000004 => Allow card with the D1 and D2 ranks to be used in battle + // 0x00000008 => Disable overall and per-phase battle time limits, regardless + // of the options chosen during battle setup + // 0x00000010 => Enable debug messages in Episode 3 games and battles + "Episode3BehaviorFlags": 0x00000002, + + // Episode 3 card auction configuration. CardAuctionPoints specifies how many + // points each player gets when they join an auction (may be anywhere from 0 + // to 65535, but a somewhat-low number is generally good). CardAuctionSize + // specifies how many cards will be present in each auction; if this is a + // list, then the number of cards is random in the specified range. Finally, + // CardAuctionContents is a dictionary specifying the relative frequencies and + // costs of each card in the auction pool. Relative frequencies are 64-bit + // integers, but should generally be less than 0x0100000000000000 to avoid + // excessive bias. There is no fixed summation bound for relative frequencies. + // Cards are always drawn (with replacement) from the same distribution. + "CardAuctionPoints": 30, + "CardAuctionSize": [2, 4], + "CardAuctionPool": { + // "CardName": [RelativeFrequency, MinPrice] + "Red Sword": [500, 8], + "Hildeblue": [400, 10], + "Grants": [300, 15], + "Megid": [700, 6], + }, // Whether to enable patches on Episode 3 USA. This functionality depends on // exploiting a bug in Episode 3, and while it seems to work reliably on