diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index 0ecceb78..64b56f87 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -1978,6 +1978,13 @@ void Server::handle_6xB3x1D_start_battle(const string& data) { if (l->battle_record) { l->battle_record->set_battle_start_timestamp(); } + + // 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 (l->ep3_ex_result_values) { + this->send(*l->ep3_ex_result_values); + } + this->setup_and_start_battle(); this->battle_in_progress = true; } diff --git a/src/Lobby.hh b/src/Lobby.hh index 98742a86..35b43610 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -11,6 +11,7 @@ #include #include "Client.hh" +#include "CommandFormats.hh" #include "Episode3/BattleRecord.hh" #include "Episode3/Server.hh" #include "ItemCreator.hh" @@ -96,6 +97,7 @@ struct Lobby : public std::enable_shared_from_this { std::shared_ptr prev_battle_record; // Only used in primary games std::shared_ptr battle_player; // Only used in replay games std::shared_ptr tournament_match; + std::shared_ptr ep3_ex_result_values; // Lobby stuff uint8_t event; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 511272c1..84f3093f 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -893,8 +893,7 @@ static void on_BA_Ep3(shared_ptr s, send_command(c, command, 0x03, &out_cmd, sizeof(out_cmd)); } -static bool add_next_game_client( - shared_ptr s, shared_ptr l) { +static bool add_next_game_client(shared_ptr s, shared_ptr l) { auto it = l->clients_to_add.begin(); if (it == l->clients_to_add.end()) { return false; @@ -937,33 +936,6 @@ static bool add_next_game_client( send_command_t(c, 0xC9, 0x00, state_cmd); } - // For the final match, use higher EX values. - // TODO: What was the behavior on Sega's servers? We only have logs - // of two different threshold sets (which are implemented here). - static const std::pair non_final_win_entries[10] = { - {60, 70}, {40, 50}, {25, 45}, {20, 40}, {13, 35}, {8, 30}, {5, 25}, {2, 20}, {-1, 15}, {0, 10}}; - static const std::pair non_final_lose_entries[10] = { - {1, 0}, {-1, 0}, {-3, 0}, {-5, 0}, {-7, 0}, {-10, 0}, {-12, 0}, {-15, 0}, {-18, 0}, {0, 0}}; - static const std::pair final_win_entries[10] = { - {40, 100}, {25, 95}, {20, 85}, {15, 75}, {10, 65}, {8, 60}, {5, 50}, {2, 40}, {-1, 30}, {0, 20}}; - static const std::pair final_lose_entries[10] = { - {1, -5}, {-1, -10}, {-3, -15}, {-7, -20}, {-15, -20}, {-20, -25}, {-30, -30}, {-40, -30}, {-50, -34}, {0, -40}}; - G_SetEXResultValues_GC_Ep3_6xB4x4B ex_cmd; - bool is_final_match = (tourn && (l->tournament_match == tourn->get_final_match())); - const auto& win_entries = is_final_match ? final_win_entries : non_final_win_entries; - const auto& lose_entries = is_final_match ? final_lose_entries : non_final_lose_entries; - for (size_t z = 0; z < 10; z++) { - ex_cmd.win_entries[z].threshold = win_entries[z].first; - ex_cmd.win_entries[z].value = win_entries[z].second; - ex_cmd.lose_entries[z].threshold = lose_entries[z].first; - ex_cmd.lose_entries[z].value = lose_entries[z].second; - } - if (!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) { - uint8_t mask_key = (random_object() % 0xFF) + 1; - set_mask_for_ep3_game_command(&ex_cmd, sizeof(ex_cmd), mask_key); - } - send_command_t(c, 0xC9, 0x00, ex_cmd); - s->change_client_lobby(c, l, true, target_client_id); c->flags |= Client::Flag::LOADING; c->disconnect_hooks.emplace(ADD_NEXT_CLIENT_DISCONNECT_HOOK_NAME, [s, l]() -> void { @@ -1114,6 +1086,9 @@ static bool start_ep3_battle_table_game_if_ready( return false; } game->tournament_match = tourn_match; + game->ep3_ex_result_values = (tourn_match && tourn && tourn->get_final_match() == tourn_match) + ? s->ep3_tournament_final_round_ex_values + : s->ep3_tournament_ex_values; game->clients_to_add.clear(); for (const auto& it : game_clients) { game->clients_to_add.emplace(it.first, it.second); @@ -3435,6 +3410,9 @@ static void on_0C_C1_E7_EC(shared_ptr s, shared_ptr c, game = create_game_generic( s, c, name.c_str(), password.c_str(), episode, mode, cmd.difficulty, flags, watched_lobby); + if (game->episode == Episode::EP3) { + game->ep3_ex_result_values = s->ep3_default_ex_values; + } } if (game) { diff --git a/src/ServerState.cc b/src/ServerState.cc index eb531778..cd639e84 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -650,6 +650,35 @@ void ServerState::parse_config(const JSON& json) { .card_name = it.first}); } + { + auto parse_ep3_ex_result_cmd = [&](const JSON& src) -> shared_ptr { + shared_ptr ret(new G_SetEXResultValues_GC_Ep3_6xB4x4B()); + const auto& win_json = src.at("Win"); + for (size_t z = 0; z < min(win_json.size(), 10); z++) { + ret->win_entries[z].threshold = win_json.at(z).at(0).as_int(); + ret->win_entries[z].value = win_json.at(z).at(1).as_int(); + } + const auto& lose_json = src.at("Lose"); + for (size_t z = 0; z < min(lose_json.size(), 10); z++) { + ret->lose_entries[z].threshold = lose_json.at(z).at(0).as_int(); + ret->lose_entries[z].value = lose_json.at(z).at(1).as_int(); + } + return ret; + }; + const auto& categories_json = json.at("Episode3EXResultValues"); + this->ep3_default_ex_values = parse_ep3_ex_result_cmd(categories_json.at("Default")); + try { + this->ep3_tournament_ex_values = parse_ep3_ex_result_cmd(categories_json.at("Tournament")); + } catch (const out_of_range&) { + this->ep3_tournament_ex_values = this->ep3_default_ex_values; + } + try { + this->ep3_tournament_ex_values = parse_ep3_ex_result_cmd(categories_json.at("TournamentFinalMatch")); + } catch (const out_of_range&) { + this->ep3_tournament_final_round_ex_values = this->ep3_tournament_ex_values; + } + } + set_log_levels_from_json(json.get("LogLevels", JSON::dict())); for (const string& filename : list_directory("system/blueburst/keys")) { diff --git a/src/ServerState.hh b/src/ServerState.hh index 3c2e7fc5..35009bb6 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -74,6 +74,9 @@ struct ServerState { std::shared_ptr ep3_card_index_trial; std::shared_ptr ep3_map_index; std::shared_ptr ep3_com_deck_index; + std::shared_ptr ep3_default_ex_values; + std::shared_ptr ep3_tournament_ex_values; + std::shared_ptr ep3_tournament_final_round_ex_values; std::shared_ptr quest_category_index; std::shared_ptr quest_index; std::shared_ptr level_table; diff --git a/system/config.example.json b/system/config.example.json index 66e24bf8..3d433060 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -308,6 +308,36 @@ // rescue) "Episode3BehaviorFlags": 0x0002, + // Episode 3 EX result values. This allows you to set the amount of EX players + // will get for winning or losing online matches. Each set of numbers is a + // list of thresholds; the first number in each pair is the level difference + // threshold and the second is the amount of EX. The game scans each list for + // the entry whose threshold value is less than or equal to the level + // difference. For example, if player A's CLv is 70 and player B's CLv is 55, + // and the lists are set up like this: + // "Win": [[30, 40], [20, 30], [10, 20], ...], + // "Lose": [[0, 0], [-10, -5], [-20, -10], ...], + // ...then player A would get 20 EX for defeating player B (since the + // difference between their CLvs is 15, so the first two Win entries don't + // apply), and player B would lose 10 EX for losing to player A (again, since + // the first two Lose entries don't apply). If none of the thresholds apply, + // then the last entry's value is used regardless of its threshold. All lists + // here must contain 10 [threshold, value] pairs. + "Episode3EXResultValues": { + "Default": { + "Win": [[50, 100], [30, 80], [15, 70], [10, 55], [7, 45], [4, 35], [1, 25], [-1, 20], [-9, 15], [0, 10]], + "Lose": [[1, 0], [-2, 0], [-3, 0], [-4, 0], [-5, 0], [-6, 0], [-7, 0], [-10, -10], [-30, -10], [0, -15]], + }, + "Tournament": { + "Win": [[60, 70], [40, 50], [25, 45], [20, 40], [13, 35], [8, 30], [5, 25], [2, 20], [-1, 15], [0, 10]], + "Lose": [[1, 0], [-1, 0], [-3, 0], [-5, 0], [-7, 0], [-10, 0], [-12, 0], [-15, 0], [-18, 0], [0, 0]], + }, + "TournamentFinalMatch": { + "Win": [[40, 100], [25, 95], [20, 85], [15, 75], [10, 65], [8, 60], [5, 50], [2, 40], [-1, 30], [0, 20]], + "Lose": [[1, -5], [-1, -10], [-3, -15], [-7, -20], [-15, -20], [-20, -25], [-30, -30], [-40, -30], [-50, -34], [0, -40]], + }, + }, + // Episode 3 card auction configuration. CardAuctionPoints specifies how many // points each player gets when they join an auction (this may be anywhere // from 0 to 65535, but a somewhat-low number is generally good). diff --git a/tests/GC-Episode3Battle.test.txt b/tests/GC-Episode3Battle.test.txt index 42f21647..015ee1a3 100644 --- a/tests/GC-Episode3Battle.test.txt +++ b/tests/GC-Episode3Battle.test.txt @@ -5242,6 +5242,13 @@ I 30034 2023-08-20 20:27:24 - [Commands] Sending to C-2 (Tali) (version=GC comma 0010 | 2F 43 41 78 31 44 20 53 54 41 52 54 20 42 41 54 | /CAx1D START BAT 0020 | 54 4C 45 00 00 00 00 00 | TLE I 30034 2023-08-20 20:27:24 - [Commands] Sending to C-2 (Tali) (version=GC command=C9 flag=00) +0000 | C9 00 5C 00 B4 16 00 00 4B 00 00 00 32 00 64 00 | \ K 2 d +0010 | 1E 00 50 00 0F 00 46 00 0A 00 37 00 07 00 2D 00 | P F 7 - +0020 | 04 00 23 00 01 00 19 00 FF FF 14 00 F7 FF 0F 00 | # +0030 | 00 00 0A 00 01 00 00 00 FE FF 00 00 FD FF 00 00 | +0040 | FC FF 00 00 FB FF 00 00 FA FF 00 00 F9 FF 00 00 | +0050 | F6 FF F6 FF E2 FF F6 FF 00 00 F1 FF | +I 30034 2023-08-20 20:27:24 - [Commands] Sending to C-2 (Tali) (version=GC command=C9 flag=00) 0000 | C9 00 64 00 B4 18 00 00 02 00 00 00 00 00 00 00 | d 0010 | 00 00 00 00 00 00 00 00 00 00 00 00 1D 00 16 00 | 0020 | 01 00 1E 00 0A 00 FF FF FF FF FF FF FF FF FF FF | diff --git a/tests/config.json b/tests/config.json index b4d11a32..74cf0e52 100644 --- a/tests/config.json +++ b/tests/config.json @@ -97,6 +97,21 @@ "PCPatchServerMessage": "newserv patch server\r\n\r\nThis server is not affiliated with, sponsored by, or in any other way connected to SEGA or Sonic Team, and is owned and operated completely independently.", "BBPatchServerMessage": "$C7newserv patch server\n\nThis server is not affiliated with, sponsored by, or in any\nother way connected to SEGA or Sonic Team, and is owned\nand operated completely independently.", + "Episode3EXResultValues": { + "Default": { + "Win": [[50, 100], [30, 80], [15, 70], [10, 55], [7, 45], [4, 35], [1, 25], [-1, 20], [-9, 15], [0, 10]], + "Lose": [[1, 0], [-2, 0], [-3, 0], [-4, 0], [-5, 0], [-6, 0], [-7, 0], [-10, -10], [-30, -10], [0, -15]], + }, + "Tournament": { + "Win": [[60, 70], [40, 50], [25, 45], [20, 40], [13, 35], [8, 30], [5, 25], [2, 20], [-1, 15], [0, 10]], + "Lose": [[1, 0], [-1, 0], [-3, 0], [-5, 0], [-7, 0], [-10, 0], [-12, 0], [-15, 0], [-18, 0], [0, 0]], + }, + "TournamentFinalMatch": { + "Win": [[40, 100], [25, 95], [20, 85], [15, 75], [10, 65], [8, 60], [5, 50], [2, 40], [-1, 30], [0, 20]], + "Lose": [[1, -5], [-1, -10], [-3, -15], [-7, -20], [-15, -20], [-20, -25], [-30, -30], [-40, -30], [-50, -34], [0, -40]], + }, + }, + "QuestCategories": [ // Each entry here is [type, token, flags, category_name, description]. The // order these are defined matches the order they'll appear in the quest