diff --git a/data/commands.xml b/data/commands.xml index f9e25b61683..36082c077c0 100644 --- a/data/commands.xml +++ b/data/commands.xml @@ -954,6 +954,24 @@ here later. For now, please refer to the code for the strings being used. --> state-scope="SS_LOBBY" /> + + + + + raceHasLaps()) { + LinearWorld *lin_world = dynamic_cast(World::getWorld()); + float track_length = Track::getCurrentTrack()->getTrackLength(); + float sender_distance = std::fmod(lin_world->getOverallDistance(m_owner->getWorldKartId()), track_length); + float recv_distance = std::fmod(lin_world->getOverallDistance(kart_hit->getWorldKartId()), track_length); + + int sender_lap = lin_world->getFinishedLapsOfKart(m_owner->getWorldKartId()); + int recv_lap = lin_world->getFinishedLapsOfKart(kart_hit->getWorldKartId()); + + // Blue flag settings could make the hit invalid, if it's between a lapper + // and a lapped or vice versa. Let ItemPolicy decide this + + ItemPolicy *policy = RaceManager::get()->getItemPolicy(); + return policy->isHitValid(sender_distance, sender_lap, m_owner->getPosition(), recv_distance, recv_lap, kart_hit->getPosition(), track_length); + } else { + return true; + } } // hit // ---------------------------------------------------------------------------- diff --git a/src/items/item.cpp b/src/items/item.cpp index b08d87ead20..600f4b63324 100644 --- a/src/items/item.cpp +++ b/src/items/item.cpp @@ -113,6 +113,30 @@ void ItemState::initItem(ItemType type, const Vec3& xyz, const Vec3& normal) setDisappearCounter(); } // initItem +static int getRespawnTicks(ItemState::ItemType type) { + auto& stk_config = STKConfig::get(); + switch (type) + { + case ItemState::ITEM_BONUS_BOX: + return stk_config->m_bonusbox_item_return_ticks; + break; + case ItemState::ITEM_NITRO_BIG: + case ItemState::ITEM_NITRO_SMALL: + return stk_config->m_nitro_item_return_ticks; + break; + case ItemState::ITEM_BANANA: + return stk_config->m_banana_item_return_ticks; + break; + case ItemState::ITEM_BUBBLEGUM: + case ItemState::ITEM_BUBBLEGUM_NOLOK: + return stk_config->m_bubblegum_item_return_ticks; + break; + default: + return stk_config->time2Ticks(2.0f); + break; + } +} + // ---------------------------------------------------------------------------- /** Update the state of the item, called once per physics frame. * \param ticks Number of ticks to simulate. While this value is 1 when @@ -127,6 +151,10 @@ void ItemState::update(int ticks) m_ticks_till_return -= ticks; } // if collected + ItemPolicy *policy = RaceManager::get()->getItemPolicy(); + m_ticks_till_return = policy->computeItemTicksTillReturn( + m_original_type, m_type, getRespawnTicks(m_type), m_ticks_till_return); + } // update // ---------------------------------------------------------------------------- @@ -154,26 +182,7 @@ void ItemState::collected(const AbstractKart *kart) } else { - switch (m_type) - { - case ITEM_BONUS_BOX: - m_ticks_till_return = stk_config->m_bonusbox_item_return_ticks; - break; - case ITEM_NITRO_BIG: - case ITEM_NITRO_SMALL: - m_ticks_till_return = stk_config->m_nitro_item_return_ticks; - break; - case ITEM_BANANA: - m_ticks_till_return = stk_config->m_banana_item_return_ticks; - break; - case ITEM_BUBBLEGUM: - case ITEM_BUBBLEGUM_NOLOK: - m_ticks_till_return = stk_config->m_bubblegum_item_return_ticks; - break; - default: - m_ticks_till_return = stk_config->time2Ticks(2.0f); - break; - } + m_ticks_till_return = getRespawnTicks(m_type); } if (RaceManager::get()->isBattleMode()) diff --git a/src/items/powerup_manager.cpp b/src/items/powerup_manager.cpp index 80774f662d0..79c1269e55d 100644 --- a/src/items/powerup_manager.cpp +++ b/src/items/powerup_manager.cpp @@ -95,28 +95,43 @@ void PowerupManager::unloadPowerups() } } // removeTextures +// Must match the order of PowerupType in powerup_manager.hpp!! +static const std::string powerup_names[] = { + "nothing", /* Nothing */ + "bubblegum", "cake", "bowling", "zipper", "plunger", "switch", + "swatter", "rubber-ball", "parachute", "anchor" +}; + //----------------------------------------------------------------------------- /** Determines the powerup type for a given name. * \param name Name of the powerup to look up. * \return The type, or POWERUP_NOTHING if the name is not found */ -PowerupManager::PowerupType - PowerupManager::getPowerupType(const std::string &name) const +PowerupManager::PowerupType PowerupManager::getPowerupType(const std::string &name) { - // Must match the order of PowerupType in powerup_manager.hpp!! - static const std::string powerup_names[] = { - "", /* Nothing */ - "bubblegum", "cake", "bowling", "zipper", "plunger", "switch", - "swatter", "rubber-ball", "parachute", "anchor" - }; - for(unsigned int i=POWERUP_FIRST; i<=POWERUP_LAST; i++) { - if(powerup_names[i]==name) return(PowerupType)i; + if (name == "") + return POWERUP_NOTHING; + if (powerup_names[i] == name) + return (PowerupType)i; } return POWERUP_NOTHING; } // getPowerupType +std::string PowerupManager::getPowerupAsString(PowerupManager::PowerupType type) +{ + int size = sizeof(powerup_names) / sizeof(*powerup_names); + + if (type == POWERUP_NOTHING) + return "nothing"; + + if (size < type - POWERUP_FIRST) + return "nothing"; + + return powerup_names[type - POWERUP_FIRST + 1]; +} // getPowerupAsString + //----------------------------------------------------------------------------- /** Loads powerups models and icons from the powerup.xml file. */ diff --git a/src/items/powerup_manager.hpp b/src/items/powerup_manager.hpp index b6070abff7b..491e8ec9588 100644 --- a/src/items/powerup_manager.hpp +++ b/src/items/powerup_manager.hpp @@ -152,8 +152,6 @@ class PowerupManager : public NoCopy /** The weight distribution to be used for the current race. */ WeightsData m_current_item_weights; - PowerupType getPowerupType(const std::string &name) const; - /** Seed for random powerup, for local game it will use a random number, * for network games it will use the start time from server. */ std::atomic m_random_seed; @@ -161,6 +159,8 @@ class PowerupManager : public NoCopy std::string m_config_file; public: + static PowerupType getPowerupType(const std::string &name); + static std::string getPowerupAsString(PowerupType type); static void unitTesting(); PowerupManager (); diff --git a/src/karts/abstract_kart.cpp b/src/karts/abstract_kart.cpp index 800f9d73ebe..37e31263f75 100644 --- a/src/karts/abstract_kart.cpp +++ b/src/karts/abstract_kart.cpp @@ -46,6 +46,8 @@ AbstractKart::AbstractKart(const std::string& ident, std::shared_ptr ri) : Moveable() { + m_item_amount_last_lap = 0; + m_item_type_last_lap = PowerupManager::POWERUP_NOTHING; m_world_kart_id = world_kart_id; if (RaceManager::get()->getKartGlobalPlayerId(m_world_kart_id) > -1) { diff --git a/src/karts/abstract_kart.hpp b/src/karts/abstract_kart.hpp index 8b512b0cd24..fc3c7cdfbdb 100644 --- a/src/karts/abstract_kart.hpp +++ b/src/karts/abstract_kart.hpp @@ -119,6 +119,11 @@ class AbstractKart : public Moveable std::shared_ptr ri); virtual ~AbstractKart(); // ------------------------------------------------------------------------ + + // amount in previous lap + int m_item_amount_last_lap; + PowerupManager::PowerupType m_item_type_last_lap; + /** Returns a name to be displayed for this kart. */ const core::stringw& getName() const { return m_name; } // ------------------------------------------------------------------------ @@ -358,7 +363,7 @@ class AbstractKart : public Moveable * \param max_speed_fraction Fraction of top speed to allow only. * \param fade_in_time How long till maximum speed is capped. */ virtual void setSlowdown(unsigned int category, float max_speed_fraction, - int fade_in_time) = 0; + int fade_in_time, int duration = -1) = 0; // ------------------------------------------------------------------------ /** Returns the remaining collected energy. */ virtual float getEnergy() const = 0; diff --git a/src/karts/kart.cpp b/src/karts/kart.cpp index 22cfe0c5f0e..48ab7df1921 100644 --- a/src/karts/kart.cpp +++ b/src/karts/kart.cpp @@ -497,9 +497,9 @@ void Kart::instantSpeedIncrease(unsigned int category, float add_max_speed, // ----------------------------------------------------------------------------- void Kart::setSlowdown(unsigned int category, float max_speed_fraction, - int fade_in_time) + int fade_in_time, int duration) { - m_max_speed->setSlowdown(category, max_speed_fraction, fade_in_time); + m_max_speed->setSlowdown(category, max_speed_fraction, fade_in_time, duration); } // setSlowdown // ----------------------------------------------------------------------------- @@ -2671,6 +2671,10 @@ void Kart::updatePhysics(int ticks) // Cap speed if necessary const Material *m = getMaterial(); + auto& stk_config = STKConfig::get(); + + ItemPolicy *item_policy = RaceManager::get()->getItemPolicy(); + item_policy->enforceVirtualPaceCarRulesForKart(this); float min_speed = m && m->isZipper() ? m->getZipperMinSpeed() : -1.0f; m_max_speed->setMinSpeed(min_speed); diff --git a/src/karts/kart.hpp b/src/karts/kart.hpp index 0fe118c1058..32641598ac2 100644 --- a/src/karts/kart.hpp +++ b/src/karts/kart.hpp @@ -109,8 +109,6 @@ class Kart : public AbstractKart uint8_t m_bounce_back_ticks; protected: - /** Handles speed increase and capping due to powerup, terrain, ... */ - MaxSpeed *m_max_speed; /** Stores information about the terrain the kart is on. */ TerrainInfo *m_terrain_info; @@ -296,6 +294,9 @@ class Kart : public AbstractKart void updateWeight(); void initSound(); public: + /** Handles speed increase and capping due to powerup, terrain, ... */ + MaxSpeed *m_max_speed; + Kart(const std::string& ident, unsigned int world_kart_id, int position, const btTransform& init_transform, HandicapLevel handicap, @@ -360,7 +361,7 @@ class Kart : public AbstractKart int duration, int fade_out_time) OVERRIDE; // ---------------------------------------------------------------------------------------- virtual void setSlowdown(unsigned int category, float max_speed_fraction, - int fade_in_time) OVERRIDE; + int fade_in_time, int duration = -1) OVERRIDE; // ---------------------------------------------------------------------------------------- virtual int getSpeedIncreaseTicksLeft(unsigned int category) const OVERRIDE; // ---------------------------------------------------------------------------------------- diff --git a/src/karts/max_speed.cpp b/src/karts/max_speed.cpp index 34a3fc10ed8..7f800ddc314 100644 --- a/src/karts/max_speed.cpp +++ b/src/karts/max_speed.cpp @@ -21,6 +21,7 @@ #include "config/stk_config.hpp" #include "karts/abstract_kart.hpp" #include "karts/kart_properties.hpp" +#include "karts/kart_properties_manager.hpp" #include "network/network_string.hpp" #include "physics/btKart.hpp" @@ -372,6 +373,15 @@ int MaxSpeed::getSpeedIncreaseTicksLeft(unsigned int category) return m_speed_increase[category].getTimeLeft(); } // getSpeedIncreaseTimeLeft +// ---------------------------------------------------------------------------- +/** Returns how much decreased speed time is left over in the given category. + * \param category Which category to report on. + */ +int MaxSpeed::getSpeedDecreaseTicksLeft(unsigned int category) +{ + return m_speed_decrease[category].getTimeLeft(); +} // getSpeedIncreaseTimeLeft + // ---------------------------------------------------------------------------- /** Returns if increased speed is active in the given category. * \param category Which category to report on. @@ -447,6 +457,14 @@ void MaxSpeed::update(int ticks) else m_kart->getVehicle()->setMinSpeed(0); // no additional acceleration + // TODO + // 1.X ITEM POLICY: ENABLE THIS OR NOT? It will cause slight lag for all non-tux karts, but it's very needed + // PIT LIMITED / MAX SPEED IN PITS / PIT SPEED LIMITER + // if(m_speed_decrease[MS_DECREASE_BUBBLE].m_duration != 0) { + // m_current_max_speed = kart_properties_manager->getKart(std::string("tux"))->getEngineMaxSpeed()*0.1f; + // m_add_engine_force = 0.0f; + // } + if (m_kart->isOnGround()) m_kart->getVehicle()->setMaxSpeed(m_current_max_speed); else diff --git a/src/karts/max_speed.hpp b/src/karts/max_speed.hpp index ac7fa46eb6c..ea16cdc8d38 100644 --- a/src/karts/max_speed.hpp +++ b/src/karts/max_speed.hpp @@ -195,6 +195,7 @@ friend class KartRewinder; void setSlowdown(unsigned int category, float max_speed_fraction, int fade_in_time, int duration=-1); int getSpeedIncreaseTicksLeft(unsigned int category); + int getSpeedDecreaseTicksLeft(unsigned int category); int isSpeedIncreaseActive(unsigned int category); int isSpeedDecreaseActive(unsigned int category); void update(int ticks); diff --git a/src/modes/linear_world.cpp b/src/modes/linear_world.cpp index 9b170a7c11d..deef25e1af8 100644 --- a/src/modes/linear_world.cpp +++ b/src/modes/linear_world.cpp @@ -23,6 +23,8 @@ #include "audio/sfx_base.hpp" #include "audio/sfx_manager.hpp" #include "config/user_config.hpp" +#include "items/powerup.hpp" +#include "karts/max_speed.hpp" #include "karts/abstract_kart.hpp" #include "karts/cannon_animation.hpp" #include "karts/controller/controller.hpp" @@ -40,6 +42,8 @@ #include "network/stk_host.hpp" #include "network/stk_peer.hpp" #include "race/history.hpp" +#include "race/race_manager.hpp" +#include "race/item_policy.hpp" #include "states_screens/race_gui_base.hpp" #include "tracks/check_manager.hpp" #include "tracks/check_structure.hpp" @@ -482,6 +486,15 @@ void LinearWorld::newLap(unsigned int kart_index) // duplicated race positions as well. updateRacePosition(); + ItemPolicy *item_policy = RaceManager::get()->getItemPolicy(); + + int sec = item_policy->applyRules(kart, kart_info.m_finished_laps, + World::getWorld()->getTime(), RaceManager::get()->getNumLaps()); + kart->m_item_type_last_lap = kart->getPowerup()->getType(); + kart->m_item_amount_last_lap = kart->getPowerup()->getNum(); + + item_policy->checkAndApplyVirtualPaceCarRules(kart, sec, kart_info.m_finished_laps); + // Race finished // We compute the exact moment the kart crossed the line // This way, even with poor framerate, we get a time significant to the ms diff --git a/src/network/protocols/command_manager.cpp b/src/network/protocols/command_manager.cpp index 66ff144be67..9908fbf128c 100644 --- a/src/network/protocols/command_manager.cpp +++ b/src/network/protocols/command_manager.cpp @@ -572,6 +572,8 @@ void CommandManager::initCommands() applyFunctionIfPossible("teamhit =", &CM::process_teamhit_assign); applyFunctionIfPossible("scoring", &CM::process_scoring); applyFunctionIfPossible("scoring =", &CM::process_scoring_assign); + applyFunctionIfPossible("itempolicy", &CM::process_itempolicy); + applyFunctionIfPossible("itempolicy =", &CM::process_itempolicy_assign); applyFunctionIfPossible("version", &CM::process_text); applyFunctionIfPossible("clear", &CM::process_text); applyFunctionIfPossible("register", &CM::process_register); @@ -3100,6 +3102,12 @@ void CommandManager::process_scoring(Context& context) } // process_scoring // ======================================================================== +void CommandManager::process_itempolicy(Context& context) +{ + context.say(RaceManager::get()->getItemPolicy()->toString()); +} // process_itempolicy +// ======================================================================== + void CommandManager::process_scoring_assign(Context& context) { std::string msg; @@ -3114,6 +3122,19 @@ void CommandManager::process_scoring_assign(Context& context) } // process_scoring_assign // ======================================================================== +void CommandManager::process_itempolicy_assign(Context& context) +{ + std::string msg; + auto& argv = context.m_argv; + + std::string cmd2; + CommandManager::restoreCmdByArgv(cmd2, argv, ' ', '"', '"', '\\', 1); + RaceManager::get()->setItemPolicy(cmd2); + + Comm::sendStringToAllPeers(StringUtils::insertValues( "Item policy set to \"%s\"", cmd2.c_str())); +} // process_itempolicy_assign +// ======================================================================== + void CommandManager::process_register(Context& context) { auto& argv = context.m_argv; @@ -4271,4 +4292,4 @@ void CommandManager::shift(std::string& cmd, std::vector& argv, CommandManager::restoreCmdByArgv(cmd, argv, ' ', '"', '"', '\\'); } // shift -//----------------------------------------------------------------------------- \ No newline at end of file +//----------------------------------------------------------------------------- diff --git a/src/network/protocols/command_manager.hpp b/src/network/protocols/command_manager.hpp index 7504d403278..1864dab50ae 100644 --- a/src/network/protocols/command_manager.hpp +++ b/src/network/protocols/command_manager.hpp @@ -342,6 +342,8 @@ class CommandManager: public LobbyContextComponent void process_teamhit_assign(Context& context); void process_scoring(Context& context); void process_scoring_assign(Context& context); + void process_itempolicy(Context& context); + void process_itempolicy_assign(Context& context); void process_register(Context& context); // soccer tournament commands void process_muteall(Context& context); diff --git a/src/network/server_config.hpp b/src/network/server_config.hpp index c135841ecf0..e0f45e51c81 100644 --- a/src/network/server_config.hpp +++ b/src/network/server_config.hpp @@ -708,6 +708,16 @@ namespace ServerConfig "are played in the order cyclically, " "except if something is in the regular karts queue.")); + SERVER_CFG_PREFIX StringServerConfigParam m_item_policy + SERVER_CFG_DEFAULT(StringServerConfigParam( + "normal", + "item-policy", + "A custom item policy to be used, " + "should have format 'num section section section ...', where " + "num is the number of sections, and each 'section' is of the form " + "'bitstring' linear_multiplier items_per_lap progressive_cap " + "progressive_penalty item_number [item1 weight1 item2 weight2...]")); + SERVER_CFG_PREFIX StringServerConfigParam m_gp_scoring SERVER_CFG_DEFAULT(StringServerConfigParam( "fixed 0 1 10 8 6 5 4 3 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", diff --git a/src/race/item_policy.cpp b/src/race/item_policy.cpp new file mode 100644 index 00000000000..81f297a09ad --- /dev/null +++ b/src/race/item_policy.cpp @@ -0,0 +1,581 @@ +// +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2025 Nomagno +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 3 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#include +#include +#include + +#include "race/item_policy.hpp" + +#include "race/race_manager.hpp" +#include "utils/types.hpp" +#include "utils/string_utils.hpp" +#include "modes/world.hpp" +#include "items/item.hpp" +#include "items/powerup.hpp" +#include "items/powerup_manager.hpp" +#include "karts/kart.hpp" +#include "karts/max_speed.hpp" +#include "config/stk_config.hpp" + +int ItemPolicy::selectItemFrom(const std::vector& types, + const std::vector& weights) +{ + if (types.size() != weights.size()) + { + Log::error("ItemPolicy", "Mismatch in item policy section weights and types lists size"); + return -1; + } + int sum_of_weight = 0; + + for (size_t i = 0; i < types.size(); ++i) + sum_of_weight += weights[i]; + + int rnd = rand() % sum_of_weight; + for (size_t i = 0; i < types.size(); ++i) + { + if (rnd < weights[i]) + return i; + rnd -= weights[i]; + } + + throw std::logic_error("No item selected from weighted list (this code path should be unreachable)"); +} // selectItemFrom +//----------------------------------------------------------------------------- + +void ItemPolicy::applySectionRules( + ItemPolicySection §ion, AbstractKart *kart, + int next_section_start_laps, int current_lap, + int current_time, int prev_lap_item_amount) +{ + if (section.m_section_type == IP_TIME_BASED) + { + Log::error("ItemPolicy", "Time-implemented item policy sections are not implemented yet"); + return; + } + + int section_start_lap = section.m_section_start; + + int curr_item_amount = kart->getPowerup()->getNum(); + PowerupManager::PowerupType curr_item_type = kart->getPowerup()->getType(); + + int16_t rules = section.m_rules; + bool overwrite = rules & ItemPolicyRules::IPT_OVERWRITE_ITEMS; + bool linear_add = rules & ItemPolicyRules::IPT_LINEAR; + bool linear_clear = rules & ItemPolicyRules::IPT_CLEAR; + bool gradual_add = rules & ItemPolicyRules::IPT_GRADUAL; + bool gradual_replenish = rules & ItemPolicyRules::IPT_REPLENISH; + bool progressive_cap = rules & ItemPolicyRules::IPT_PROGRESSIVE_CAP; + bool section_start = current_lap == section_start_lap; + + bool active_role = gradual_add || gradual_replenish; + + int amount_to_add = section_start + ? section.m_items_per_lap + : (prev_lap_item_amount - curr_item_amount); + if (amount_to_add > section.m_items_per_lap) + amount_to_add = section.m_items_per_lap; + if (gradual_add && !gradual_replenish) + amount_to_add = section.m_items_per_lap; + if (!gradual_add) amount_to_add = 0; + + int remaining_laps = next_section_start_laps - current_lap; + int amount_to_add_linear; + if (section_start) + amount_to_add_linear = linear_add ? section.m_linear_mult * remaining_laps : 0; + else + amount_to_add_linear = 0; + + + PowerupManager::PowerupType new_type = curr_item_type; + + bool item_is_valid = false; + + // If the list of weights is empty, then we take this to mean that any item type is correct. + bool empty_weights = section.m_weight_distribution.size() == 0; + if (!empty_weights) + { + auto found_item = std::find(section.m_possible_types.begin(), + section.m_possible_types.end(), + curr_item_type); + item_is_valid = found_item != section.m_possible_types.end(); + } else { + item_is_valid = true; + } + + int new_amount = curr_item_amount; + + if (!item_is_valid) + new_amount = 0; + + if (section_start && linear_clear) + new_amount = 0; + + new_amount += amount_to_add; + new_amount += amount_to_add_linear; + if (progressive_cap && (new_amount > section.m_progressive_cap*remaining_laps)) + new_amount = section.m_progressive_cap*remaining_laps; + + if (!empty_weights) + { + bool selecting_item = overwrite || new_amount == 0; + selecting_item |= (section_start && (linear_clear || new_amount != 0)); + selecting_item |= (!section_start && !item_is_valid && active_role); + + if (selecting_item) + { + int index = selectItemFrom(section.m_possible_types, section.m_weight_distribution); + if (index == -1) + return; + new_type = section.m_possible_types[index]; + } + } + + // If the powerup type is NOTHING, the amount must be 0. + // If the amount is 0, the powerup type must be NOTHING. + if (new_amount == 0) + new_type = PowerupManager::PowerupType::POWERUP_NOTHING; + if (new_type == PowerupManager::PowerupType::POWERUP_NOTHING) + new_amount = 0; + + if (new_type == curr_item_type) + { + // STK by default will add instead of overwriting items of the same type, + // so we set it to 0 like this manually if that will happen. + // Yes, this is stupid, but it's the only way without touching the whole codebase. + kart->setPowerup(new_type, -curr_item_amount); + } + kart->setPowerup(new_type, new_amount); +} // applySectionRules +//----------------------------------------------------------------------------- + +// Returns the section that was applied. Returns -1 if it tried to appply an unsupported section or if there are no sections +int ItemPolicy::applyRules(AbstractKart *kart, int current_lap, int current_time, int total_laps_of_race) +{ + if (m_policy_sections.size() == 0) return -1; + for (unsigned i = 0; i < m_policy_sections.size(); i++) + { + int next_section_start_laps = total_laps_of_race; + int prev_lap_item_amount = kart->m_item_amount_last_lap; + if (i + 1 == m_policy_sections.size()) + { + next_section_start_laps = RaceManager::get()->getNumLaps(); + applySectionRules(m_policy_sections[i], kart, next_section_start_laps, current_lap, current_time, prev_lap_item_amount); + return i; + break; + } + else if (m_policy_sections[i].m_section_type == IP_TIME_BASED) + { + Log::error("ItemPolicy", "Time-implemented item policy sections are not implemented yet"); + return i; + break; + } + else if (m_policy_sections[i].m_section_type == IP_LAPS_BASED) + { + if (m_policy_sections[i + 1].m_section_type == IP_TIME_BASED) + { + Log::error("ItemPolicy", "Time-implemented item policy sections are not implemented yet"); + return i; + break; + } + else if (m_policy_sections[i + 1].m_section_type == IP_LAPS_BASED) + { + if (current_lap >= m_policy_sections[i].m_section_start && + current_lap < m_policy_sections[i + 1].m_section_start) + { + next_section_start_laps = m_policy_sections[i + 1].m_section_start; + applySectionRules(m_policy_sections[i], kart, next_section_start_laps, current_lap, current_time, prev_lap_item_amount); + return i; + break; + } + else + continue; + } + } + } + return -1; +} // applyRules +//----------------------------------------------------------------------------- + +static std::string fetch(std::vector& strings, unsigned idx) +{ + if (idx >= strings.size()) + throw std::logic_error("Out of bounds in item policy parsing"); + + return strings[idx]; +} // fetch +//----------------------------------------------------------------------------- + +void ItemPolicy::fromString(std::string& input) +{ + std::string normal_race_preset = "1 0 0000000000 0 0 0 0 0 0 0"; + std::string tt_preset = "1 0 0010000001 1 0 0 0 0 0 1 zipper 1"; + if (input.empty()) + { + fromString(normal_race_preset); + return; + } + if (input == "normal" || input == "") + { + fromString(normal_race_preset); + return; + } + if (input == "tt" || input == "timetrial" || input == "time-trial") + { + fromString(tt_preset); + return; + } + std::vector params = StringUtils::split(input, ' '); + // Format can not form a valid policy with less than 10 space-separated parameters: + // 1 0 0000000000 0 0 0 0 0 0 0 + // 1 section starting on lap 1 with no rules, all data to 0, no overridden stop time, and a length-0 item vector + if (params.empty() || params.size() < 8) + { + fromString(normal_race_preset); + return; + } + int idx = 0; + + auto retrieve_float = [&idx, ¶ms](float &x) + { + StringUtils::fromString(fetch(params, idx), x); + idx += 1; + }; + auto retrieve_int = [&idx, ¶ms](int &x) + { + StringUtils::fromString(fetch(params, idx), x); + idx += 1; + }; + auto retrieve_uint = [&idx, ¶ms](unsigned &x) + { + StringUtils::fromString(fetch(params, idx), x); + idx += 1; + }; + + + unsigned section_number = 0; + retrieve_uint(section_number); + if (section_number == 0) return; + + m_policy_sections.clear(); + for (unsigned i = 0; i < section_number; i++) + { + ItemPolicySection tmp; + + tmp.m_section_type = IP_LAPS_BASED; + retrieve_int(tmp.m_section_start); + + std::string bitstring = fetch(params, idx); + idx++; + + tmp.m_rules = 0; + for (unsigned j = 0; j < bitstring.size(); j++) + { + tmp.m_rules |= (bitstring[j] != '0') << (bitstring.size() - j - 1); + } + + retrieve_float(tmp.m_linear_mult); + retrieve_float(tmp.m_items_per_lap); + retrieve_float(tmp.m_progressive_cap); + retrieve_float(tmp.m_virtual_pace_gaps); + + unsigned item_vector_length = 0; + retrieve_uint(item_vector_length); + + for (unsigned j = 0; j < item_vector_length; j++) + { + tmp.m_possible_types.push_back(PowerupManager::getPowerupType(fetch(params, idx))); + idx++; + + tmp.m_weight_distribution.push_back(std::stoi(fetch(params, idx))); + idx++; + } + if (item_vector_length != tmp.m_possible_types.size() || + item_vector_length != tmp.m_weight_distribution.size() || + tmp.m_possible_types.size() != tmp.m_weight_distribution.size()) + { + throw std::logic_error("Mismatched length of item and weights lists during item policy parsing"); + } + m_policy_sections.push_back(tmp); + } +} // fromString +//----------------------------------------------------------------------------- + +std::string ItemPolicy::toString() +{ + std::stringstream ss; + ss << std::setprecision(4); + ss << m_policy_sections.size() << " "; + for (unsigned i = 0; i < m_policy_sections.size(); i++) + { + if (m_policy_sections[i].m_section_type == IP_TIME_BASED) + { + Log::error("ItemPolicy", "Can't print time based section data because time based sections are not supported yet"); + return "Time based sections not supported yet"; + } + ss << m_policy_sections[i].m_section_start << " "; + std::string bs = std::bitset<16>(m_policy_sections[i].m_rules).to_string(); + ss << bs << " "; + ss << m_policy_sections[i].m_linear_mult << " "; + ss << m_policy_sections[i].m_items_per_lap << " "; + ss << m_policy_sections[i].m_progressive_cap << " "; + ss << m_policy_sections[i].m_virtual_pace_gaps << " "; + ss << m_policy_sections[i].m_possible_types.size() << " "; + for (unsigned j = 0; j < m_policy_sections[i].m_possible_types.size(); j++) + { + ss << PowerupManager::getPowerupAsString(m_policy_sections[i].m_possible_types[j]) + << " "; + ss << m_policy_sections[i].m_weight_distribution[j] << " "; + } + } + return ss.str(); +} // toString +//----------------------------------------------------------------------------- + +static bool isKartUnderVirtualPaceCarSlowdown(ItemPolicy *self, int position) { + bool start_of_race_vpc = self->m_leader_section <= -1 && (self->m_policy_sections[0].m_rules & ItemPolicyRules::IPT_VIRTUAL_PACE); + // Not in a virtual pace car yet, but since it is on the start of the race, this is done to prevent overtaking + if (start_of_race_vpc) + return true; + + bool is_restart = self->m_virtual_pace_code <= -3; + bool did_restart = false; + if (is_restart) { + int restart_time = -(self->m_virtual_pace_code + 3); + float gap = self->m_policy_sections[self->m_leader_section].m_virtual_pace_gaps; + gap *= position; + restart_time += gap; + int current_time = World::getWorld()->getTime(); + if (current_time > restart_time) { + // Set slowdown time to 0 (disable it) if its time to restart + did_restart = true; + } + } + if (is_restart && !did_restart) + return true; + else + return false; +} + + +bool ItemPolicy::isHitValid(float sender_distance, float sender_lap, int sender_position, float recv_distance, int recv_position, float recv_lap, float track_length) { + int leader_section_idx = m_leader_section; + // If leader is not in a valid section, allow the hit + if (leader_section_idx <= -1) + return true; + // If blue flags are not enabled, ALSO allow the hit + if (!(m_policy_sections[leader_section_idx].m_rules & ItemPolicyRules::IPT_BLUE_FLAGS)) + return true; + + // If one of the karts is under a virtual pace car restart, forbid the hit + if (isKartUnderVirtualPaceCarSlowdown(this, sender_position) || isKartUnderVirtualPaceCarSlowdown(this, recv_position)) + return false; + + //float minimum_distance_empirical = 200.0f; + + // for too short tracks we instead take 1/5th of the track + //if (track_length < 750.0f) + // minimum_distance_empirical = track_length / 5.0f; + + float distance_normal = std::fabs(sender_distance - recv_distance); + float distance_complimentary = track_length - distance_normal; + + bool across_finish_line; + bool forwards_throw; + if (distance_complimentary < distance_normal) + { + across_finish_line = true; + if (sender_distance > recv_distance) + forwards_throw = true; + else + forwards_throw = false; + } + else + across_finish_line = false; + + // if the distance is less than 5% from half the track length, + // it is nonsense to try to predict if the hit is across the finish line + if (distance_normal / track_length > 0.45 && distance_normal / track_length < 0.55) + across_finish_line = false; + + bool hit_is_valid; + // sender with a 1 lap difference whose distance is less than an empirical number are almost certainly hitting each other across the start/finish line + if (across_finish_line && forwards_throw) + hit_is_valid = (recv_lap - sender_lap) == 1; + else if (across_finish_line && !forwards_throw) + hit_is_valid = (sender_lap - recv_lap) == 1; + else + hit_is_valid = sender_lap == recv_lap; + + return hit_is_valid; +} // isHitValid +//----------------------------------------------------------------------------- + +// Returns the amount of ticks till return to set for the current situation +int ItemPolicy::computeItemTicksTillReturn( + ItemState::ItemType orig_type, + ItemState::ItemType curr_type, + int curr_type_respawn_ticks, int curr_ticks_till_return) +{ + int current_section = m_leader_section; + + if (current_section <= -1) + current_section = 0; + + uint16_t rules_curr = m_policy_sections[current_section].m_rules; + uint16_t rules_prev; + if (current_section > 0) + rules_prev = m_policy_sections[current_section-1].m_rules; + else + rules_prev = rules_curr; + + // Note during PR: This is ridiculously ugly. + // Please come up with a better way as I don't want to. + bool was_gum = (orig_type==ItemState::ItemType::ITEM_BUBBLEGUM) || (curr_type==ItemState::ItemType::ITEM_BUBBLEGUM_NOLOK); + + bool is_nitro = (curr_type==ItemState::ItemType::ITEM_NITRO_SMALL) || (curr_type==ItemState::ItemType::ITEM_NITRO_BIG); + bool was_nitro = (orig_type==ItemState::ItemType::ITEM_NITRO_SMALL) || (orig_type==ItemState::ItemType::ITEM_NITRO_BIG); + + bool forbid_prev = ((rules_prev & ItemPolicyRules::IPT_FORBID_BONUSBOX) && curr_type==ItemState::ItemType::ITEM_BONUS_BOX) || + ((rules_prev & ItemPolicyRules::IPT_FORBID_BANANA) && curr_type==ItemState::ItemType::ITEM_BANANA) || + ((rules_prev & ItemPolicyRules::IPT_FORBID_NITRO) && (is_nitro || was_nitro)); + + bool forbid_curr = ((rules_curr & ItemPolicyRules::IPT_FORBID_BONUSBOX) && curr_type==ItemState::ItemType::ITEM_BONUS_BOX) || + ((rules_curr & ItemPolicyRules::IPT_FORBID_BANANA) && curr_type==ItemState::ItemType::ITEM_BANANA) || + ((rules_curr & ItemPolicyRules::IPT_FORBID_NITRO) && (is_nitro || was_nitro)); + + + auto& stk_config = STKConfig::get(); + int new_ticks_till_return = curr_ticks_till_return; + // There's redundant cases here, but it is like this for maintainability + if (forbid_prev && forbid_curr) + new_ticks_till_return = stk_config->time2Ticks(99999); + else if (!forbid_prev && forbid_curr) + new_ticks_till_return = stk_config->time2Ticks(99999); + else if (forbid_prev && !forbid_curr) + { + int respawn_ticks = curr_type_respawn_ticks; + // If the ticks till return are abnormally high, set them back to normal. + // If we don't do it like this, it will set the ticks till return perpetually + // when transitioning from a section without to a section with this item type allowed. + if (curr_ticks_till_return > 10 * respawn_ticks) + new_ticks_till_return = respawn_ticks; + } + else if (!forbid_prev && !forbid_curr) + { + // Nothing to do + // This wouldn't be needed normally, but we do it in case of switched items + int respawn_ticks = curr_type_respawn_ticks; + if (curr_ticks_till_return > 10 * respawn_ticks && curr_type != ItemState::ItemType::ITEM_EASTER_EGG) + new_ticks_till_return = respawn_ticks; + } // Gums that were switched into nitro are NEVER forbidden + + bool instant = (was_gum && is_nitro); + if (instant) + new_ticks_till_return = 0; + + return new_ticks_till_return; +} // computeItemTicksTillReturn +//----------------------------------------------------------------------------- + +void ItemPolicy::enforceVirtualPaceCarRulesForKart(Kart *kart) { + auto& stk_config = STKConfig::get(); + + bool start_of_race_vpc = m_leader_section <= -1 && (m_policy_sections[0].m_rules & ItemPolicyRules::IPT_VIRTUAL_PACE); + // Not in a virtual pace car yet, but since it is on the start of the race, this is done to prevent overtaking + if (start_of_race_vpc) { + kart->setSlowdown(MaxSpeed::MS_DECREASE_BUBBLE, 0.1f, stk_config->time2Ticks(0.1f), -1); + return; + } + + bool is_restart = m_virtual_pace_code <= -3; + bool did_restart = false; + if (is_restart) { + // Reaffirm the penalty in case someone tried to be funny and hit a gum in the middle of a safety car restart to shorten their penalty + kart->setSlowdown(MaxSpeed::MS_DECREASE_BUBBLE, 0.1f, stk_config->time2Ticks(0.1f), -1); + int restart_time = -(m_virtual_pace_code + 3); + float gap = m_policy_sections[m_leader_section].m_virtual_pace_gaps; + gap *= kart->getPosition(); + restart_time += gap; + int current_time = World::getWorld()->getTime(); + if (current_time > restart_time) + { + // Set slowdown time to 0 (disable it) if its time to restart + kart->setSlowdown(MaxSpeed::MS_DECREASE_BUBBLE, 0.1f, stk_config->time2Ticks(0.1f), stk_config->time2Ticks(0)); + did_restart = true; + } + } + + bool is_last = (unsigned)kart->getPosition() == RaceManager::get()->getNumberOfKarts(); + + if (is_last && did_restart) + { + m_virtual_pace_code = -1; + m_restart_count = -1; + } + + // the only reason such a ridiculous infinite gum penalty (-1) can be given is if it's a virtual pace car restart + // plainly, the only reason this exists is because first place won't get its penalty overturned if for some reason + if (m_virtual_pace_code == -1 && kart->m_max_speed->getSpeedDecreaseTicksLeft(MaxSpeed::MS_DECREASE_BUBBLE) == -1) + kart->setSlowdown(MaxSpeed::MS_DECREASE_BUBBLE, 0.1f, stk_config->time2Ticks(0.1f), stk_config->time2Ticks(0)); + +} // enforceVirtualPaceCarRulesForKart +//----------------------------------------------------------------------------- + +void ItemPolicy::checkAndApplyVirtualPaceCarRules( + AbstractKart *kart, int kart_section, int finished_laps) +{ + if (kart->getPosition() == 1) + { + m_leader_section = kart_section; + int start_lap = m_policy_sections[kart_section].m_section_start; + int16_t rules = m_policy_sections[kart_section].m_rules; + bool do_virtual_pace = rules & ItemPolicyRules::IPT_VIRTUAL_PACE; + bool do_unlapping = rules & ItemPolicyRules::IPT_UNLAPPING; + if (do_virtual_pace && start_lap == finished_laps) + { + m_restart_count = 0; + m_virtual_pace_code = do_unlapping + ? start_lap // Lappings must slow down when they reach the lead lap + : -2; // Lappings must slow down as soon as possible + } + } + + bool slowed_down = false; + if (m_virtual_pace_code == finished_laps || m_virtual_pace_code == -2) + { + auto& stk_config = STKConfig::get(); + m_restart_count += 1; + kart->setSlowdown(MaxSpeed::MS_DECREASE_BUBBLE, 0.1f, + stk_config->time2Ticks(0.1f), + stk_config->time2Ticks(99999)); + slowed_down = true; + } + + bool is_last = m_restart_count == (int)RaceManager::get()->getNumberOfKarts(); + // Technically there could be ghosts but you can't get into that situation. Probably. + // If the kart is last, also fire the virtual pace car restart procedure + if (slowed_down && is_last) + { + int time = World::getWorld()->getTime(); + time = (-time) - 3; + // code till be added to 3 and inverted to get time + m_virtual_pace_code = time; + } +} // checkAndApplyVirtualPaceCarRules +//----------------------------------------------------------------------------- diff --git a/src/race/item_policy.hpp b/src/race/item_policy.hpp new file mode 100644 index 00000000000..d8714078748 --- /dev/null +++ b/src/race/item_policy.hpp @@ -0,0 +1,170 @@ +// +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2025 Nomagno +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 3 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#ifndef HEADER_ITEM_POLICY_HPP +#define HEADER_ITEM_POLICY_HPP + +#include +#include +#include +#include "items/powerup_manager.hpp" +#include "items/item.hpp" + +class AbstractKart; +class Kart; +class PowerupManager; + + +// Note during PR: Please make it prettier. + +enum ItemPolicyRules { + // Give items at section start: m_linear_mult X section_length + IPT_LINEAR = 1 << 0, + + // Clear items at section start + IPT_CLEAR = 1 << 1, + + // Give items at every lap of the section: m_items_per_lap + IPT_GRADUAL = 1 << 2, + + // If previously given gradual items at start of new + // lap were not fully spent, refill them instead of + // blindly adding + IPT_REPLENISH = 1 << 3, + + // For every lap in the section, if the amount + // of items is above m_progressive_cap X remaining_section_length, + // then cap them at m_progressive_penalty X remaining_section_length + // (those two multipliers will usually be the same) + IPT_PROGRESSIVE_CAP = 1 << 4, + + // When there are multiple items, at section start, and whenever items reach 0, + // there will normally be an invokation of the random number generator with the + // specified weights to decide the first item, and all items after will be of the same type. + // If this bit is set to true, instead every lap where something is to be done, an invokation of the RNG will be made. + IPT_OVERWRITE_ITEMS = 1 << 5, + + // Prevent cake & bowl hits between lappings and lappers from causing damage. + // It helps with traffic, like blue flags in real motorsports. + // The leader's section will be applied for everyone + IPT_BLUE_FLAGS = 1 << 6, + + // Spawns/despawns bonus boxes. + // The leader's section will be applied for everyone, or section 0 if it fails. + IPT_FORBID_BONUSBOX = 1 << 7, + + // Spawns/despawns bananas. + // The leader's section will be applied for everyone, or section 0 if it fails. + IPT_FORBID_BANANA = 1 << 8, + + // Spawns/despawns nitro. + // The leader's section will be applied for everyone, or section 0 if it fails. + IPT_FORBID_NITRO = 1 << 9, + + // A "virtual pace car" procedure will be initiated at section start + IPT_VIRTUAL_PACE = 1 << 10, + + // The "virtual pace car" procedure will let all karts fully unlap. + IPT_UNLAPPING = 1 << 11, + + // Bonus boxes will instead have their powerup pool draw from the item policy section's current one, rather than the one in powerup.xml. + IPT_BONUS_BOX_OVERRIDE = 1 << 12, + + // Provided weights for the powerup pool will be ignored, instead using automatic ones to try to attempt reasonable balance. + IPT_AUTOMATIC_WEIGHTS = 1 << 13, + +}; + +enum ItemPolicySectionBase +{ + IP_LAPS_BASED = 0, + IP_TIME_BASED = 1 +}; + +// An event can have several sections which each run by different rules. +// Item policies are only applied at the start of the race (lap 0), and at the start of every new lap. +struct ItemPolicySection { + int m_section_type; + + // The key used to decide the current section, + // can be a time in seconds or a lap + int m_section_start; + + // Bitstring of IPT_XX rule bits + uint16_t m_rules; + + float m_linear_mult; + float m_items_per_lap; + float m_progressive_cap; + float m_virtual_pace_gaps; + + // Which items can be handed out + std::vector m_possible_types; + std::vector m_weight_distribution; +}; + +// Sections only have their start specified, so for instance if there is a section +// that starts in lap 1, and another that starts in lap 4, both would technically be applicable. +// The way this ambiguity is solved is, the section with the highest index that is still applicable is always used. +struct ItemPolicy { + std::vector m_policy_sections; + + // Holds the section the leader is in. + // -1 if this gamemode doesn't support leaders for some reason + int m_leader_section; + + // Holds the status of the "virtual pace car" procedure: + // code <= -3 : All karts must wait until the global clock hits second -([code] + 3) to start + // the restart procedure. Afterwards, they must wait until second + // -([code] + 3) + position_in_race*m_virtual_pace_gaps[m_leader_section] + // to go back to normal running. This type of code is triggered by the last + // kart when it starts slowing down. + // code = -2 : Slow down IMMEDIATELY and indefinitely (on next pass by finish line) + // code = -1 : Normal racing, remove all penalties. + // code >= 0 : Slow down indefinitely when the kart finishes lap [code] + int m_virtual_pace_code; + + // Number of karts that are ready to restart the race. Used to trigger the restart procedure. + int m_restart_count; + + int selectItemFrom(const std::vector& types, + const std::vector& weights); + + void applySectionRules (ItemPolicySection §ion, AbstractKart *kart, + int next_section_start_laps, int current_lap, + int current_time, int prev_lap_item_amount); + + int applyRules(AbstractKart *kart, int current_lap, int current_time, + int total_laps_of_race); + + bool isHitValid(float sender_distance, float sender_lap, int sender_position, float recv_distance, int recv_position, float recv_lap, float track_length); + + int computeItemTicksTillReturn(ItemState::ItemType orig_type, + ItemState::ItemType curr_type, + int curr_type_respawn_ticks, + int curr_ticks_till_return); + + void enforceVirtualPaceCarRulesForKart(Kart *k); + + void checkAndApplyVirtualPaceCarRules(AbstractKart *kart, int kart_section, int finished_laps); + + void fromString(std::string& str); + std::string toString(); +}; + +#endif // HEADER_ITEM_POLICY_HPP diff --git a/src/race/race_manager.cpp b/src/race/race_manager.cpp index e2a6e0d4748..f4b2e8f47d4 100644 --- a/src/race/race_manager.cpp +++ b/src/race/race_manager.cpp @@ -152,6 +152,7 @@ RaceManager::RaceManager() m_default_ai_list.clear(); setNumPlayers(0); setSpareTireKartNum(0); + setItemPolicy("normal"); } // RaceManager //--------------------------------------------------------------------------------------------- @@ -172,6 +173,14 @@ void RaceManager::reset() m_num_finished_players = 0; } // reset +void RaceManager::setItemPolicy(std::string str) +{ + m_item_policy.fromString(str); + m_item_policy.m_leader_section = -1; + m_item_policy.m_virtual_pace_code = -1; + m_item_policy.m_restart_count = 0; +} + // ---------------------------------------------------------------------------- /** Sets the default list of AI karts to use. * \param ai_kart_list List of the identifier of the karts to use. @@ -1381,4 +1390,4 @@ void RaceManager::setBenchmarking(bool benchmark) void RaceManager::scheduleBenchmark() { m_scheduled_benchmark = true; -} // scheduleBenchmark \ No newline at end of file +} // scheduleBenchmark diff --git a/src/race/race_manager.hpp b/src/race/race_manager.hpp index 35838c03ea2..f7ff0d9aaf7 100644 --- a/src/race/race_manager.hpp +++ b/src/race/race_manager.hpp @@ -34,6 +34,7 @@ #include "network/remote_kart_info.hpp" #include "race/grand_prix_data.hpp" +#include "race/item_policy.hpp" #include "utils/vec3.hpp" #include "utils/types.hpp" @@ -249,6 +250,9 @@ class RaceManager bool m_started_from_overworld; public: + ItemPolicy m_item_policy; + void setItemPolicy(std::string str); + ItemPolicy *getItemPolicy() { return &m_item_policy; }; /** This data structure accumulates kart data and race result data from * each race. */ diff --git a/src/utils/lobby_settings.cpp b/src/utils/lobby_settings.cpp index b7a81791ef8..60c35b1ead3 100644 --- a/src/utils/lobby_settings.cpp +++ b/src/utils/lobby_settings.cpp @@ -78,6 +78,8 @@ void LobbySettings::setupContextUser() loadWhiteList(); loadPreservedSettings(); + RaceManager::get()->setItemPolicy(ServerConfig::m_item_policy); + m_live_players = ServerConfig::m_live_players; m_ai_anywhere = ServerConfig::m_ai_anywhere;