diff --git a/README.md b/README.md index 9f4868bf..715497a8 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,7 @@ Some commands only work on the game server and not on the proxy server. The chat * Episode 3 commands (game server only) * `$spec`: Toggles the allow spectators flag for Episode 3 games. If any players are spectating when this flag is disabled, they will be sent back to the lobby. * `$inftime`: Toggles infinite-time mode. Must be used before starting a battle. If infinite-time mode is enabled, the overall and per-phase time limits will be disabled regardless of the values chosen during battle setup. After completing a battle, infinite-time mode is reset to the server's default value (which can be set in Episode3BehaviorFlags in config.json). + * `$defrange -`: Sets the DEF dice range for the next battle. If this is used, the dice range set during battle rules setup will apply only to the ATK dice; the DEF dice will use this range instead. Assist cards and other dice effects will still apply. Dice exchange also still applies if it is enabled. * `$stat `: Shows a statistic about your player or team in the current battle. `` can be `duration`, `fcs-destroyed`, `cards-destroyed`, `damage-given`, `damage-taken`, `opp-cards-destroyed`, `own-cards-destroyed`, `move-distance`, `cards-set`, `fcs-set`, `attack-actions-set`, `techs-set`, `assists-set`, `defenses-self`, `defenses-ally`, `cards-drawn`, `max-attack-damage`, `max-combo`, `attacks-given`, `attacks-taken`, `sc-damage`, `damage-defended`, or `rank`. * `$surrender`: Causes your team to immediately lose the current battle. * `$saverec `: Saves the recording of the last battle. diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index de9ccfa4..e941a739 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -1280,6 +1280,54 @@ static void server_command_ep3_infinite_time(shared_ptr c, const std::u1 send_text_message(l, infinite_time_enabled ? u"$C6Infinite time enabled" : u"$C6Infinite time disabled"); } +static void server_command_ep3_set_def_dice_range(shared_ptr c, const std::u16string& args) { + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_ep3(c, true); + check_is_leader(l, c); + + if (l->episode != Episode::EP3) { + throw logic_error("non-Ep3 client in Ep3 game"); + } + if (!l->ep3_server) { + send_text_message(c, u"$C6Episode 3 server\nis not initialized"); + return; + } + if (l->ep3_server->setup_phase != Episode3::SetupPhase::REGISTRATION) { + send_text_message(c, u"$C6Battle is already\nin progress"); + return; + } + + if (args.empty()) { + l->ep3_server->map_and_rules->rules.def_dice_range = 0; + send_text_message_printf(l, "$C6DEF dice range\nset to default"); + } else { + uint8_t min_dice, max_dice; + auto tokens = split(encode_sjis(args), '-'); + if (tokens.size() == 1) { + min_dice = stoul(tokens[0]); + max_dice = min_dice; + } else if (tokens.size() == 2) { + min_dice = stoul(tokens[0]); + max_dice = stoul(tokens[1]); + } else { + send_text_message(c, u"$C6Specify DEF dice\nrange as MIN-MAX"); + return; + } + if (min_dice == 0 || min_dice > 9 || max_dice == 0 || max_dice > 9) { + send_text_message(c, u"$C6DEF dice must be\nin range 1-9"); + return; + } + if (min_dice > max_dice) { + uint8_t t = min_dice; + min_dice = max_dice; + max_dice = t; + } + l->ep3_server->map_and_rules->rules.def_dice_range = ((min_dice << 4) & 0xF0) | (max_dice & 0x0F); + send_text_message_printf(l, "$C6DEF dice range\nset to %hhu-%hhu", min_dice, max_dice); + } +} + static void server_command_ep3_unset_field_character(shared_ptr c, const std::u16string& args) { auto s = c->require_server_state(); auto l = c->require_lobby(); @@ -1429,6 +1477,7 @@ static const unordered_map chat_commands({ {u"$bbchar", {server_command_convert_char_to_bb, nullptr}}, {u"$cheat", {server_command_cheat, nullptr}}, {u"$debug", {server_command_debug, nullptr}}, + {u"$defrange", {server_command_ep3_set_def_dice_range, nullptr}}, {u"$drop", {server_command_drop, nullptr}}, {u"$edit", {server_command_edit, nullptr}}, {u"$event", {server_command_lobby_event, proxy_command_lobby_event}}, diff --git a/src/Episode3/DataIndexes.cc b/src/Episode3/DataIndexes.cc index 2a91009d..9fde2274 100644 --- a/src/Episode3/DataIndexes.cc +++ b/src/Episode3/DataIndexes.cc @@ -1172,10 +1172,6 @@ void PlayerConfig::encrypt(uint8_t basis) { this->basis = basis; } -Rules::Rules() { - this->clear(); -} - Rules::Rules(const JSON& json) { this->clear(); this->overall_time_limit = json.get_int("overall_time_limit", this->overall_time_limit); @@ -1191,6 +1187,9 @@ Rules::Rules(const JSON& json) { this->disable_dialogue = json.get_bool("disable_dialogue", this->disable_dialogue); this->dice_exchange_mode = json.get_enum("dice_exchange_mode", this->dice_exchange_mode); this->disable_dice_boost = json.get_bool("disable_dice_boost", this->disable_dice_boost); + uint8_t min_def_dice = json.get_int("min_def_dice", (this->def_dice_range >> 4) & 0x0F); + uint8_t max_def_dice = json.get_int("max_def_dice", this->def_dice_range & 0x0F); + this->def_dice_range = ((min_def_dice << 4) & 0xF0) | (max_def_dice & 0x0F); } JSON Rules::json() const { @@ -1208,6 +1207,8 @@ JSON Rules::json() const { {"disable_dialogue", static_cast(this->disable_dialogue)}, {"dice_exchange_mode", name_for_enum(this->dice_exchange_mode)}, {"disable_dice_boost", static_cast(this->disable_dice_boost)}, + {"min_def_dice", ((this->def_dice_range >> 4) & 0x0F)}, + {"max_def_dice", (this->def_dice_range & 0x0F)}, }); } @@ -1234,7 +1235,9 @@ void Rules::clear() { this->disable_dialogue = 0; this->dice_exchange_mode = DiceExchangeMode::HIGH_ATK; this->disable_dice_boost = 0; - this->unused.clear(0); + this->def_dice_range = 0; + this->unused1 = 0; + this->unused2 = 0; } string Rules::str() const { @@ -1281,6 +1284,17 @@ string Rules::str() const { tokens.emplace_back(string_printf("max_dice=%hhu", this->max_dice)); } + if (this->min_def_dice() == 0) { + tokens.emplace_back("min_def_dice=(default)"); + } else { + tokens.emplace_back(string_printf("min_def_dice=%hhu", this->min_def_dice())); + } + if (this->max_def_dice() == 0) { + tokens.emplace_back("max_def_dice=(default)"); + } else { + tokens.emplace_back(string_printf("max_def_dice=%hhu", this->max_def_dice())); + } + switch (this->dice_exchange_mode) { case DiceExchangeMode::HIGH_ATK: tokens.emplace_back("dice_exchange=high-atk"); @@ -1743,6 +1757,23 @@ bool Rules::check_and_reset_invalid_fields() { this->max_dice = t; ret = true; } + uint8_t min_def_dice = this->min_def_dice(); + uint8_t max_def_dice = this->max_def_dice(); + if (min_def_dice > 9) { + min_def_dice = 0; + ret = true; + } + if (max_def_dice > 9) { + max_def_dice = 0; + ret = true; + } + if ((min_def_dice != 0) && (max_def_dice != 0) && (max_def_dice < min_def_dice)) { + uint8_t t = min_def_dice; + min_def_dice = max_def_dice; + max_def_dice = t; + ret = true; + } + this->def_dice_range = ((min_def_dice << 4) & 0xF0) | (max_def_dice & 0x0F); if (this->disable_deck_shuffle > 1) { this->disable_deck_shuffle = 0; ret = true; @@ -1778,6 +1809,14 @@ bool Rules::check_and_reset_invalid_fields() { return ret; } +uint8_t Rules::min_def_dice() const { + return (this->def_dice_range >> 4) & 0x0F; +} + +uint8_t Rules::max_def_dice() const { + return this->def_dice_range & 0x0F; +} + CardIndex::CardIndex(const string& filename, const string& decompressed_filename, const string& text_filename) { unordered_map> card_tags; unordered_map card_text; diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index 24da9dec..e41d0cd6 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -864,20 +864,24 @@ struct Rules { // When this structure is used in a map/quest definition, FF in any of these // fields means the user is allowed to override it. Any non-FF fields are // fixed for the map/quest and cannot be overridden. - /* 00 */ uint8_t overall_time_limit; // In increments of 5 mins; 0 = unlimited - /* 01 */ uint8_t phase_time_limit; // In seconds; 0 = unlimited - /* 02 */ AllowedCards allowed_cards; - /* 03 */ uint8_t min_dice; // 0 = default (1) - /* 04 */ uint8_t max_dice; // 0 = default (6) - /* 05 */ uint8_t disable_deck_shuffle; // 0 = shuffle on, 1 = off - /* 06 */ uint8_t disable_deck_loop; // 0 = loop on, 1 = off - /* 07 */ uint8_t char_hp; - /* 08 */ HPType hp_type; - /* 09 */ uint8_t no_assist_cards; // 1 = assist cards disallowed - /* 0A */ uint8_t disable_dialogue; // 0 = dialogue on, 1 = dialogue off - /* 0B */ DiceExchangeMode dice_exchange_mode; - /* 0C */ uint8_t disable_dice_boost; // 0 = dice boost on, 1 = off - /* 0D */ parray unused; + /* 00 */ uint8_t overall_time_limit = 0; // In increments of 5 mins; 0 = unlimited + /* 01 */ uint8_t phase_time_limit = 0; // In seconds; 0 = unlimited + /* 02 */ AllowedCards allowed_cards = AllowedCards::ALL; + /* 03 */ uint8_t min_dice = 1; // 0 = default (1) + /* 04 */ uint8_t max_dice = 6; // 0 = default (6) + /* 05 */ uint8_t disable_deck_shuffle = 0; // 0 = shuffle on, 1 = off + /* 06 */ uint8_t disable_deck_loop = 0; // 0 = loop on, 1 = off + /* 07 */ uint8_t char_hp = 15; + /* 08 */ HPType hp_type = HPType::DEFEAT_PLAYER; + /* 09 */ uint8_t no_assist_cards = 0; // 1 = assist cards disallowed + /* 0A */ uint8_t disable_dialogue = 0; // 0 = dialogue on, 1 = dialogue off + /* 0B */ DiceExchangeMode dice_exchange_mode = DiceExchangeMode::HIGH_ATK; + /* 0C */ uint8_t disable_dice_boost = 0; // 0 = dice boost on, 1 = off + // NOTE: The following fields are unused in PSO's implementation, but newserv + // uses them to implement extended rules. + /* 0D */ uint8_t def_dice_range = 0; // High 4 bits = min, low 4 = max + /* 0E */ uint8_t unused1 = 0; + /* 0F */ uint8_t unused2 = 0; /* 10 */ // Annoyingly, this structure is a different size in Episode 3 Trial Edition. @@ -886,7 +890,7 @@ struct Rules { // clients. It'd be nice to support Trial Edition battles, but that would // likely be more work than it's worth. - Rules(); + Rules() = default; explicit Rules(const JSON& json); JSON json() const; bool operator==(const Rules& other) const; @@ -897,6 +901,9 @@ struct Rules { bool check_invalid_fields() const; bool check_and_reset_invalid_fields(); + uint8_t min_def_dice() const; + uint8_t max_def_dice() const; + std::string str() const; } __attribute__((packed)); diff --git a/src/Episode3/PlayerState.cc b/src/Episode3/PlayerState.cc index c83acd1d..18d8c019 100644 --- a/src/Episode3/PlayerState.cc +++ b/src/Episode3/PlayerState.cc @@ -1826,32 +1826,44 @@ void PlayerState::roll_main_dice() { auto s = this->server(); const auto& rules = s->map_and_rules->rules; - uint8_t min_dice = rules.min_dice; - uint8_t max_dice = rules.max_dice; - if (min_dice == 0) { - min_dice = 1; + uint8_t min_atk_dice = rules.min_dice; + uint8_t max_atk_dice = rules.max_dice; + if (min_atk_dice == 0) { + min_atk_dice = 1; } - if (max_dice == 0) { - max_dice = 6; + if (max_atk_dice == 0) { + max_atk_dice = 6; } - - if (max_dice < min_dice) { - uint8_t t = max_dice; - max_dice = min_dice; - min_dice = t; + if (max_atk_dice < min_atk_dice) { + uint8_t t = max_atk_dice; + max_atk_dice = min_atk_dice; + min_atk_dice = t; } - - uint8_t dice_range_width = (max_dice - min_dice) + 1; - if (dice_range_width < 2) { - this->dice_results[0] = min_dice; - this->dice_results[1] = min_dice; - this->atk_points = min_dice; - this->def_points = min_dice; + uint8_t atk_dice_range_width = (max_atk_dice - min_atk_dice) + 1; + if (atk_dice_range_width < 2) { + this->dice_results[0] = min_atk_dice; } else { - this->dice_results[0] = min_dice + s->get_random(dice_range_width); - this->dice_results[1] = min_dice + s->get_random(dice_range_width); - this->atk_points = this->dice_results[0]; - this->def_points = this->dice_results[1]; + this->dice_results[0] = min_atk_dice + s->get_random(atk_dice_range_width); + } + + uint8_t min_def_dice = rules.min_def_dice() ? rules.min_def_dice() : rules.min_dice; + uint8_t max_def_dice = rules.max_def_dice() ? rules.max_def_dice() : rules.max_dice; + if (min_def_dice == 0) { + min_def_dice = 1; + } + if (max_def_dice == 0) { + max_def_dice = 6; + } + if (max_def_dice < min_def_dice) { + uint8_t t = max_def_dice; + max_def_dice = min_def_dice; + min_def_dice = t; + } + uint8_t def_dice_range_width = (max_def_dice - min_def_dice) + 1; + if (def_dice_range_width < 2) { + this->dice_results[1] = min_def_dice; + } else { + this->dice_results[1] = min_def_dice + s->get_random(def_dice_range_width); } bool should_exchange = false; @@ -1862,12 +1874,12 @@ void PlayerState::roll_main_dice() { } if (!should_exchange) { - this->atk_points = (short)(char)this->dice_results[0]; - this->def_points = (short)(char)this->dice_results[1]; + this->atk_points = this->dice_results[0]; + this->def_points = this->dice_results[1]; this->assist_flags &= (~AssistFlag::DICE_WERE_EXCHANGED); } else { - this->atk_points = (short)(char)this->dice_results[1]; - this->def_points = (short)(char)this->dice_results[0]; + this->atk_points = this->dice_results[1]; + this->def_points = this->dice_results[0]; this->assist_flags |= AssistFlag::DICE_WERE_EXCHANGED; } diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index 55871d4a..2c8bc1c5 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -1932,7 +1932,13 @@ void Server::handle_CAx13_update_map_during_setup(const string& data) { (this->map_and_rules->num_players == 0) && (this->registration_phase != RegistrationPhase::REGISTERED) && (this->registration_phase != RegistrationPhase::BATTLE_STARTED)) { + // newserv's extended rules are stored in unused parts of the Rules struct, + // and clients will probably overwrite them with zeroes if we allow them to. + // So, we preserve the extended rules manually here. + uint8_t def_dice_range = this->map_and_rules->rules.def_dice_range; *this->map_and_rules = in_cmd.map_and_rules_state; + this->map_and_rules->rules.def_dice_range = def_dice_range; + if (this->override_environment_number != 0xFF) { this->map_and_rules->environment_number = this->override_environment_number; this->override_environment_number = 0xFF; diff --git a/src/ServerShell.cc b/src/ServerShell.cc index a4f7d759..9368734e 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -166,6 +166,8 @@ Server commands:\n\ resize: If the tournament is less than half full when it starts, reduce\n\ the number of rounds to fit the existing entries\n\ dice=MIN-MAX: Set minimum and maximum dice rolls\n\ + dice=MIN-MAX:MIN-MAX: Set minimum and maximum dice rolls for ATK and DEF\n\ + dice separately\n\ overall-time-limit=N: Set battle time limit (in multiples of 5 minutes)\n\ phase-time-limit=N: Set phase time limit (in seconds)\n\ allowed-cards=ALL/N/NR/NRS: Set rarities of allowed cards\n\ @@ -479,12 +481,24 @@ Proxy session commands:\n\ } else if (token == "resize") { flags |= Episode3::Tournament::Flag::RESIZE_ON_START; } else if (starts_with(token, "dice=")) { - auto subtokens = split(token.substr(5), '-'); - if (subtokens.size() != 2) { - throw runtime_error("dice option must be of the form dice=X-Y"); + auto subtokens = split(token.substr(5), ':'); + if (subtokens.size() == 1) { + rules.def_dice_range = 0x00; + } else if (subtokens.size() == 2) { + auto subsubtokens = split(subtokens[1], '-'); + if (subsubtokens.size() != 2) { + throw runtime_error("dice option must be of the form dice=A-B or dice=A-B:C-D"); + } + rules.def_dice_range = ((stoul(subsubtokens[0]) << 4) & 0xF0) | (stoul(subsubtokens[1]) & 0x0F); + } else { + throw runtime_error("dice option must be of the form dice=A-B or dice=A-B:C-D"); } - rules.min_dice = stoul(subtokens[0]); - rules.max_dice = stoul(subtokens[1]); + auto subsubtokens = split(subtokens[0], '-'); + if (subsubtokens.size() != 2) { + throw runtime_error("dice option must be of the form dice=A-B or dice=A-B:C-D"); + } + rules.min_dice = stoul(subsubtokens[0]); + rules.max_dice = stoul(subsubtokens[1]); } else if (starts_with(token, "overall-time-limit=")) { uint32_t limit = stoul(token.substr(19)); if (limit > 600) {