diff --git a/CMakeLists.txt b/CMakeLists.txt index 90b261e1..3e254a74 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,7 @@ add_executable(alicia-server src/server/chat/AllChatDirector.cpp src/server/chat/PrivateChatDirector.cpp src/server/messenger/MessengerDirector.cpp + src/server/race/MagicSystem.cpp src/server/race/RaceInstance.cpp src/server/race/RaceNetworkHandler.cpp src/server/ranch/RanchDirector.cpp diff --git a/include/libserver/network/command/proto/RaceMessageDefinitions.hpp b/include/libserver/network/command/proto/RaceMessageDefinitions.hpp index 59c5228c..58ed8ca6 100644 --- a/include/libserver/network/command/proto/RaceMessageDefinitions.hpp +++ b/include/libserver/network/command/proto/RaceMessageDefinitions.hpp @@ -1542,7 +1542,7 @@ struct AcCmdUserRaceUpdatePos //! 1 = In the air uint16_t member5{}; //! Race track progress - float member6{}; + float progress{}; //! Ticks since connected to race director? uint32_t member7{}; diff --git a/include/libserver/registry/MagicRegistry.hpp b/include/libserver/registry/MagicRegistry.hpp index 59c56c00..7b25f491 100644 --- a/include/libserver/registry/MagicRegistry.hpp +++ b/include/libserver/registry/MagicRegistry.hpp @@ -20,6 +20,7 @@ #ifndef MAGICREGISTRY_HPP #define MAGICREGISTRY_HPP +#include #include #include #include @@ -30,6 +31,8 @@ namespace server::registry struct Magic { + using SlotWeight = uint32_t; + //! Per-magic-slot definition (MagicSlotInfo). struct SlotInfo { @@ -67,6 +70,8 @@ struct Magic uint32_t affectByCriticalAura{}; uint32_t criticalByDarkFire{}; + //! Weights for each position on the scoreboard. + std::array positionalWeights{}; }; //! Magic gauge (SP) regen settings for Magic mode. @@ -122,6 +127,10 @@ class MagicRegistry //! Returns the stat scaling for a basicType, or nullptr if none applies. [[nodiscard]] const Magic::StatScaling* GetStatScaling(uint32_t basicType) const; + //! Returns the position weights for use in random magic selection. + [[nodiscard]] const std::vector>& GetSoloPositionWeights(uint32_t position) const; + [[nodiscard]] const std::vector>& GetTeamPositionWeights(uint32_t position) const; + private: std::unordered_map _slotInfo{}; std::vector _soloPool{}; @@ -130,6 +139,9 @@ class MagicRegistry uint32_t _baseCritChanceBp{500}; //! Keyed by basicType. std::unordered_map _statScalings{}; + //! Position weights for use in random magic selection. + std::array>, 8> _soloPositionWeights; + std::array>, 8> _teamPositionWeights; }; } // namespace server::registry diff --git a/include/server/race/MagicSystem.hpp b/include/server/race/MagicSystem.hpp new file mode 100644 index 00000000..939cce60 --- /dev/null +++ b/include/server/race/MagicSystem.hpp @@ -0,0 +1,54 @@ +/** + * Alicia Server - dedicated server software + * Copyright (C) 2026 Story Of Alicia + * + * 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 2 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + **/ + +#ifndef MAGICSYSTEM_HPP +#define MAGICSYSTEM_HPP + +#include "server/tracker/RaceTracker.hpp" + +#include +#include + +#include + +namespace server::race +{ + +class MagicSystem +{ +public: + static uint32_t GetMountStatValue( + const tracker::RaceTracker::Racer::MountStatsSnapshot& stats, + registry::Magic::MountStat which); + + //! Function to select a random item based on position weights + static const registry::Magic::SlotInfo& SelectMagicTypeByPosition( + const registry::MagicRegistry& magicRegistry, + uint32_t position, + bool isTeam); + + static const registry::Magic::SlotInfo& RandomMagicItem( + const registry::MagicRegistry& magicRegistry, + tracker::RaceTracker& tracker, + data::Uid racerUid); +}; + +} // namespace server::race + +#endif // MAGICSYSTEM_HPP diff --git a/include/server/tracker/RaceTracker.hpp b/include/server/tracker/RaceTracker.hpp index ca277ab0..3a0105e2 100644 --- a/include/server/tracker/RaceTracker.hpp +++ b/include/server/tracker/RaceTracker.hpp @@ -72,11 +72,15 @@ class RaceTracker Oid oid{InvalidEntityOid}; State state{State::Disconnected}; Team team{Team::Solo}; - protocol::Vector3 position{}; + //! The racer's position in the world, as a vector. + protocol::Vector3 worldPosition{}; uint32_t starPointValue{}; uint32_t jumpComboValue{}; uint32_t courseTime{InvalidCourseTime}; std::optional magicItem{}; + //! The racer's progress on the race track. + //! Normalised by the client to: 0.0f <= x <= 1.0f + float raceProgress{}; //! A set of tracked items in racer's proximity. std::unordered_set trackedDecks; diff --git a/resources/config/game/magic.yaml b/resources/config/game/magic.yaml index f4d138ce..9802f78f 100644 --- a/resources/config/game/magic.yaml +++ b/resources/config/game/magic.yaml @@ -31,6 +31,7 @@ magic: affectByCriticalAura: 1 criticalByDarkFire: 1 attackRank: 2 + positionalWeights: [1, 45, 45, 45, 30, 20, 3, 3] - type: 3 # FireBall (Critical) basicType: 2 @@ -90,6 +91,7 @@ magic: massEffect: 0 affectByCriticalAura: 1 criticalByDarkFire: 0 + positionalWeights: [103, 10, 10, 5, 4, 3, 3, 3] - type: 5 # WaterShield (Critical) basicType: 4 @@ -148,6 +150,7 @@ magic: massEffect: 0 affectByCriticalAura: 1 criticalByDarkFire: 0 + positionalWeights: [0, 10, 15, 18, 45, 55, 105, 105] - type: 7 # Booster (Critical) basicType: 6 @@ -206,6 +209,7 @@ magic: massEffect: 0 affectByCriticalAura: 1 criticalByDarkFire: 0 + positionalWeights: [0, 0, 0, 0, 5, 20, 50, 50] - type: 9 # HotRodding (Critical) basicType: 8 @@ -265,6 +269,7 @@ magic: affectByCriticalAura: 1 criticalByDarkFire: 0 attackRank: 1 + positionalWeights: [89, 35, 20, 20, 10, 12, 3, 3] - type: 11 # IceWall (Critical) basicType: 10 @@ -324,6 +329,7 @@ magic: massEffect: 0 affectByCriticalAura: 1 criticalByDarkFire: 0 + positionalWeights: [0, 5, 20, 20, 30, 20, 3, 3] - type: 13 # JumpStun (Critical) basicType: 12 @@ -382,6 +388,7 @@ magic: massEffect: 0 affectByCriticalAura: 1 criticalByDarkFire: 0 + positionalWeights: [5, 31, 20, 20, 17, 22, 8, 8] - type: 15 # DarkFire (Critical) basicType: 14 @@ -441,6 +448,7 @@ magic: affectByCriticalAura: 1 criticalByDarkFire: 0 attackRank: 2 + positionalWeights: [1, 45, 45, 50, 35, 20, 3, 3] - type: 17 # Summon (Critical) basicType: 16 @@ -501,6 +509,7 @@ magic: affectByCriticalAura: 1 criticalByDarkFire: 1 attackRank: 3 + positionalWeights: [0, 3, 3, 3, 4, 4, 4, 4] - type: 19 # Lightning (Critical) basicType: 18 @@ -560,6 +569,7 @@ magic: massEffect: 0 affectByCriticalAura: 1 criticalByDarkFire: 0 + positionalWeights: [1, 6, 10, 7, 8, 8, 4, 4] - type: 21 # BufPower (Critical) basicType: 20 @@ -618,6 +628,7 @@ magic: massEffect: 0 affectByCriticalAura: 0 criticalByDarkFire: 0 + positionalWeights: [0, 0, 0, 0, 0, 0, 0, 0] - type: 23 # BufGauge (Critical) basicType: 22 @@ -676,6 +687,7 @@ magic: massEffect: 0 affectByCriticalAura: 1 criticalByDarkFire: 0 + positionalWeights: [0, 10, 12, 12, 12, 16, 14, 14] - type: 25 # BufSpeed (Critical) basicType: 24 diff --git a/src/libserver/network/Server.cpp b/src/libserver/network/Server.cpp index 102e06c3..55d4c2a0 100644 --- a/src/libserver/network/Server.cpp +++ b/src/libserver/network/Server.cpp @@ -31,7 +31,7 @@ namespace server::network namespace { -constexpr std::size_t MaxConnectionsPerAddress = 3; +constexpr std::size_t MaxConnectionsPerAddress = 10; constexpr std::size_t MaxTotalConnections = 1024; constexpr std::size_t MaxConnectRatePerAddress = 10; constexpr auto RateWindow = std::chrono::seconds(30); diff --git a/src/libserver/network/command/proto/RaceMessageDefinitions.cpp b/src/libserver/network/command/proto/RaceMessageDefinitions.cpp index 4ae581e2..c34729e3 100644 --- a/src/libserver/network/command/proto/RaceMessageDefinitions.cpp +++ b/src/libserver/network/command/proto/RaceMessageDefinitions.cpp @@ -1105,7 +1105,7 @@ void AcCmdUserRaceUpdatePos::Read( stream.Read(command.member4) .Read(command.member5) - .Read(command.member6) + .Read(command.progress) .Read(command.member7); } diff --git a/src/libserver/registry/MagicRegistry.cpp b/src/libserver/registry/MagicRegistry.cpp index d959827a..5869956c 100644 --- a/src/libserver/registry/MagicRegistry.cpp +++ b/src/libserver/registry/MagicRegistry.cpp @@ -67,6 +67,10 @@ uint32_t ReadSlotInfo(const YAML::Node& section, Magic::SlotInfo& slot) slot.criticalByDarkFire = section["criticalByDarkFire"].as(); slot.attackRank = section["attackRank"].as(0); + // Crit items do not need the positional weights defined, base items carry that info + if (slot.type == slot.basicType) + slot.positionalWeights = section["positionalWeights"].as>(); + return slot.type; } @@ -98,7 +102,7 @@ void MagicRegistry::ReadConfig(const std::filesystem::path& configPath) } } - // Pre-build the pick pools so RandomMagicItem never has to filter at runtime. + // Pre-build the pick pools and positional weights so RandomMagicItem never has to filter at runtime. for (const auto& [type, slot] : _slotInfo) { if (slot.basicType != type) @@ -106,6 +110,26 @@ void MagicRegistry::ReadConfig(const std::filesystem::path& configPath) _teamPool.push_back(type); if (slot.teamMode == 0) _soloPool.push_back(type); + + // Only compile weights from base type + if (slot.type != slot.basicType) + continue; + + for (size_t i = 0; i < slot.positionalWeights.size(); ++i) + { + const auto& pair = std::make_pair( + slot.positionalWeights[i], + slot); + + // Add to team magic item weights since team is a superset of solo + _teamPositionWeights[i].emplace_back(pair); + + // Do not add to solo position weights if team item + if (slot.teamMode != 0) + continue; + + _soloPositionWeights[i].emplace_back(pair); + } } if (const auto regenSection = magicSection["regen"]) @@ -202,4 +226,14 @@ const Magic::StatScaling* MagicRegistry::GetStatScaling(uint32_t basicType) cons return it == _statScalings.cend() ? nullptr : &it->second; } +const std::vector>& MagicRegistry::GetSoloPositionWeights(uint32_t position) const +{ + return _soloPositionWeights.at(position); +} + +const std::vector>& MagicRegistry::GetTeamPositionWeights(uint32_t position) const +{ + return _teamPositionWeights.at(position); +} + } // namespace server::registry diff --git a/src/server/race/MagicSystem.cpp b/src/server/race/MagicSystem.cpp new file mode 100644 index 00000000..1e825cc3 --- /dev/null +++ b/src/server/race/MagicSystem.cpp @@ -0,0 +1,138 @@ +/** + * Alicia Server - dedicated server software + * Copyright (C) 2026 Story Of Alicia + * + * 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 2 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + **/ + +#include "server/race/MagicSystem.hpp" +#include "server/tracker/RaceTracker.hpp" + +#include +#include +#include + +namespace +{ + +std::random_device _randomDevice; +std::mt19937 mt(_randomDevice()); + +} // namespace + +namespace server::race +{ + +uint32_t MagicSystem::GetMountStatValue( + const tracker::RaceTracker::Racer::MountStatsSnapshot& stats, + registry::Magic::MountStat which) +{ + switch (which) + { + case registry::Magic::MountStat::Agility: return stats.agility; + case registry::Magic::MountStat::Ambition: return stats.ambition; + case registry::Magic::MountStat::Rush: return stats.rush; + case registry::Magic::MountStat::Endurance: return stats.endurance; + case registry::Magic::MountStat::Courage: return stats.courage; + } + return 0; +} + +// Function to select a random item based on position weights +const registry::Magic::SlotInfo& MagicSystem::SelectMagicTypeByPosition( + const registry::MagicRegistry& magicRegistry, + uint32_t position, + bool isTeam) +{ + // Validate position + if (position > 7) + throw std::out_of_range("Position must be between 0 and 7"); + + // Get the weights for the specified position + const auto& positionSlotInfoWeights = isTeam ? magicRegistry.GetTeamPositionWeights(position) : magicRegistry.GetSoloPositionWeights(position); + + // Keep reference to weights only for the discrete distribution + const auto& positionWeights = positionSlotInfoWeights | std::views::keys; + + // Create a discrete distribution based on the weights + std::discrete_distribution dist( + positionWeights.cbegin(), + positionWeights.cend()); + + // Select a random index based on the distribution + // and map the discrete index to the corresponding magic item + const uint32_t selectedIndex = dist(mt); + return positionSlotInfoWeights[selectedIndex].second; +} + +const registry::Magic::SlotInfo& MagicSystem::RandomMagicItem( + const registry::MagicRegistry& magicRegistry, + tracker::RaceTracker& tracker, + data::Uid racerUid) +{ + const auto& racer = tracker.GetRacer(racerUid); + + // Determine the racer's position (0 = 1st place) + uint32_t racerPosition = 0; + const auto& allRacers = tracker.GetRacers(); + size_t totalRacers = allRacers.size(); + + for (const auto& [uid, instanceRacer] : allRacers) + { + if (uid == racerUid) + continue; + + // TODO: do we ignore disconnected racers too? + + // Check if instance racer is ahead of the racer requesting item + if (instanceRacer.raceProgress > racer.raceProgress) + racerPosition++; + } + + // Map the actual position to one of the 8 weight slots [0, 7] via linear interpolation. + // This ensures last place in a small race gets "last-place" item weights. + uint32_t effectivePosition = racerPosition; + if (totalRacers > 1) + { + effectivePosition = static_cast( + static_cast(racerPosition) * 7.0f / + static_cast(totalRacers - 1)); + } + + // Clamp effective position to [0, 7] for safety + effectivePosition = std::clamp(effectivePosition, 0u, 7u); + + const registry::Magic::SlotInfo& magicSlotInfo = SelectMagicTypeByPosition( + magicRegistry, + effectivePosition, + racer.team == protocol::TeamColor::Red or racer.team == protocol::TeamColor::Blue); + + uint32_t critChanceBp = magicRegistry.GetBaseCritChanceBp(); + if (magicSlotInfo.criticalType != 0) + { + if (const auto* scaling = magicRegistry.GetStatScaling(magicSlotInfo.basicType)) + { + const uint32_t statValue = GetMountStatValue(racer.mountStats, scaling->stat); + critChanceBp += scaling->critStepBp * (statValue / 10u); + } + } + + if ((rand() % 10000) < static_cast(critChanceBp)) + return magicRegistry.GetSlotInfo(magicSlotInfo.criticalType); + + return magicSlotInfo; +} + +} // namespace server::race diff --git a/src/server/race/RaceInstance.cpp b/src/server/race/RaceInstance.cpp index 9ceddea6..aef871a4 100644 --- a/src/server/race/RaceInstance.cpp +++ b/src/server/race/RaceInstance.cpp @@ -529,7 +529,7 @@ void RaceInstance::TickItemSpawners() const protocol::Vector3& position, const uint32_t spawnStyle = 0) { - const auto distance = (racer.position - position).Length(); + const auto distance = (racer.worldPosition - position).Length(); const bool isInProximity = distance < ItemSpawnDistanceThreshold; const bool isAlreadyTracked = racer.trackedDecks.contains(oid); diff --git a/src/server/race/RaceNetworkHandler.cpp b/src/server/race/RaceNetworkHandler.cpp index 1c69bebb..c1b0ce83 100644 --- a/src/server/race/RaceNetworkHandler.cpp +++ b/src/server/race/RaceNetworkHandler.cpp @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. **/ +#include "server/race/MagicSystem.hpp" #include "server/race/RaceNetworkHandler.hpp" #include "server/ServerInstance.hpp" @@ -38,66 +39,8 @@ namespace std::random_device _randomDevice; -uint32_t GetMountStatValue( - const tracker::RaceTracker::Racer::MountStatsSnapshot& stats, - registry::Magic::MountStat which) -{ - switch (which) - { - case registry::Magic::MountStat::Agility: return stats.agility; - case registry::Magic::MountStat::Ambition: return stats.ambition; - case registry::Magic::MountStat::Rush: return stats.rush; - case registry::Magic::MountStat::Endurance: return stats.endurance; - case registry::Magic::MountStat::Courage: return stats.courage; - } - return 0; } -registry::Magic::SlotInfo RandomMagicItem( - ServerInstance& serverInstance, - const tracker::RaceTracker::Racer& racer) -{ - const auto& itemPool = (racer.team == tracker::RaceTracker::Racer::Team::Solo - ? serverInstance.GetMagicRegistry().GetSoloPool() - : serverInstance.GetMagicRegistry().GetTeamPool()); - - // Build weights: Lightning (type 18) gets a reduced roll chance. - // Booster, HotRodding, and team-only items get a slightly reduced chance as well. - // TODO: Replace with a proper per-spell weight system. - const auto& magicRegistry = serverInstance.GetMagicRegistry(); - std::vector weights; - weights.reserve(itemPool.size()); - for (const uint32_t type : itemPool) - { - const auto& slotInfo = magicRegistry.GetSlotInfo(type); - const uint32_t w = (type == 18) ? 1u - : (slotInfo.basicType == 6 || slotInfo.basicType == 8 || slotInfo.basicType == 12) ? 2u - : (slotInfo.teamMode != 0) ? 2u - : 4u; - weights.push_back(w); - } - - std::discrete_distribution distribution(weights.begin(), weights.end()); - auto magicSlotInfo = magicRegistry.GetSlotInfo(itemPool[distribution(_randomDevice)]); - uint32_t critChanceBp = magicRegistry.GetBaseCritChanceBp(); - if (magicSlotInfo.criticalType != 0) - { - if (const auto* scaling = magicRegistry.GetStatScaling(magicSlotInfo.basicType)) - { - const uint32_t statValue = GetMountStatValue(racer.mountStats, scaling->stat); - critChanceBp += scaling->critStepBp * (statValue / 10u); - } - } - - if ((rand() % 10000) < static_cast(critChanceBp)) - { - magicSlotInfo = serverInstance.GetMagicRegistry().GetSlotInfo(magicSlotInfo.criticalType); - } - return magicSlotInfo; -} - -} // anon namespace - RaceNetworkHandler::RaceNetworkHandler(ServerInstance& serverInstance) : _serverInstance(serverInstance) , _commandServer(*this) @@ -2137,7 +2080,8 @@ void RaceNetworkHandler::HandleRaceUserPos( // TODO: player position anticheat - racer.position = command.position; + racer.worldPosition = command.position; + racer.raceProgress = command.progress; } void RaceNetworkHandler::HandleChat( @@ -2439,7 +2383,8 @@ void RaceNetworkHandler::HandleRequestMagicItem( std::scoped_lock lock(_raceInstancesMutex); auto& raceInstance = GetRaceInstance(clientContext); const auto& parameters = raceInstance.GetParameters(); - auto& racer = raceInstance.GetTracker().GetRacer(clientContext.characterUid); + auto& tracker = raceInstance.GetTracker(); + auto& racer = tracker.GetRacer(clientContext.characterUid); // TODO: Revise this on NPC races if (command.characterOid != racer.oid) @@ -2459,7 +2404,11 @@ void RaceNetworkHandler::HandleRequestMagicItem( return; } - racer.magicItem.emplace(RandomMagicItem(_serverInstance, racer).type); + const auto& magicItemSlotInfo = race::MagicSystem::RandomMagicItem( + _serverInstance.GetMagicRegistry(), + tracker, + clientContext.characterUid); + racer.magicItem.emplace(magicItemSlotInfo.type); racer.starPointValue = 0; protocol::AcCmdCRStarPointGetOK starPointResponse{ @@ -3171,7 +3120,7 @@ uint32_t RaceNetworkHandler::ComputeEffectDurationMs( if (attackerRacerIter != racers.cend()) { - const uint32_t statValue = GetMountStatValue( + const uint32_t statValue = race::MagicSystem::GetMountStatValue( attackerRacerIter->second.mountStats, scaling->stat); @@ -3187,7 +3136,7 @@ uint32_t RaceNetworkHandler::ComputeEffectDurationMs( // Target-side reduction (e.g. IceWall shock mitigation), clamped to 100%. if (scaling->targetDurationReductionBp > 0) { - const uint32_t statValue = GetMountStatValue( + const uint32_t statValue = race::MagicSystem::GetMountStatValue( targetRacer.mountStats, scaling->stat);