mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 15:57:13 +00:00
Components get the client's per-component functions: LoadDataFromTemplate, PrepareConfigData, LoadConfigData, LoadFromSaveData, Startup, Run, OnEnterScope, OnLeaveScope, Preshutdown and Shutdown, called by the entity in the client's order. Update is renamed Run, LoadFromXml LoadFromSaveData and UpdateXml WriteSaveData. Nothing sent changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1027 lines
34 KiB
C++
1027 lines
34 KiB
C++
/**
|
|
* Thanks to Simon for his early research on the racing system.
|
|
*/
|
|
|
|
#include "RacingControlComponent.h"
|
|
|
|
#include "CharacterComponent.h"
|
|
#include "DestroyableComponent.h"
|
|
#include "EntityManager.h"
|
|
#include "GameMessages.h"
|
|
#include "CombatMessages.h"
|
|
#include "ActivityMessages.h"
|
|
#include "RacingMessages.h"
|
|
#include "MovementMessages.h"
|
|
#include "InventoryComponent.h"
|
|
#include "Item.h"
|
|
#include "MissionComponent.h"
|
|
#include "ModuleAssemblyComponent.h"
|
|
#include "PossessableComponent.h"
|
|
#include "PossessorComponent.h"
|
|
#include "eRacingTaskParam.h"
|
|
#include "Spawner.h"
|
|
#include "HavokVehiclePhysicsComponent.h"
|
|
#include "dServer.h"
|
|
#include "dZoneManager.h"
|
|
#include "dConfig.h"
|
|
#include "Loot.h"
|
|
#include "eMissionTaskType.h"
|
|
#include "LeaderboardManager.h"
|
|
#include "dZoneManager.h"
|
|
#include "CDActivitiesTable.h"
|
|
#include "eStateChangeType.h"
|
|
#include <ranges>
|
|
#include <ctime>
|
|
|
|
#ifndef M_PI
|
|
#define M_PI 3.14159265358979323846264338327950288
|
|
#endif
|
|
|
|
RacingControlComponent::RacingControlComponent(Entity* parent, const int32_t componentID)
|
|
: ActivityComponent(parent, componentID) {
|
|
m_PathName = u"MainPath";
|
|
m_NumberOfLaps = 3;
|
|
m_RemainingLaps = m_NumberOfLaps;
|
|
m_LeadingPlayer = LWOOBJID_EMPTY;
|
|
m_RaceBestTime = 0;
|
|
m_RaceBestLap = 0;
|
|
m_Started = false;
|
|
m_StartTimer = 0;
|
|
m_Loaded = false;
|
|
m_LoadedPlayers = 0;
|
|
m_LoadTimer = 0;
|
|
m_Finished = 0;
|
|
m_EmptyTimer = 0;
|
|
m_SoloRacing = Game::config->GetValue("solo_racing") == "1";
|
|
|
|
m_MainWorld = 1200;
|
|
const auto worldID = Game::server->GetZoneID();
|
|
if (Game::zoneManager->CheckIfAccessibleZone((worldID / 10) * 10)) m_MainWorld = (worldID / 10) * 10;
|
|
|
|
m_ActivityID = 42;
|
|
CDActivitiesTable* activitiesTable = CDClientManager::GetTable<CDActivitiesTable>();
|
|
std::vector<CDActivities> activities = activitiesTable->Query([=](CDActivities entry) {return (entry.instanceMapID == worldID); });
|
|
for (CDActivities activity : activities) m_ActivityID = activity.ActivityID;
|
|
RegisterMsg(&RacingControlComponent::MsgConfigureRacingControl);
|
|
}
|
|
|
|
RacingControlComponent::~RacingControlComponent() {}
|
|
|
|
void RacingControlComponent::OnPlayerLoaded(Entity* player) {
|
|
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
|
|
if (!inventoryComponent) {
|
|
return;
|
|
}
|
|
|
|
auto* vehicle = inventoryComponent->FindItemByLot(8092);
|
|
|
|
// If the race has already started, send the player back to the main world.
|
|
if (m_Loaded || !vehicle || !TakeCost(player)) {
|
|
auto* characterComponent = player->GetComponent<CharacterComponent>();
|
|
if (characterComponent) characterComponent->SendToZone(m_MainWorld);
|
|
return;
|
|
}
|
|
|
|
m_LoadedPlayers++;
|
|
|
|
// not live accurate to stun the player but prevents them from using skills during the race that are not meant to be used.
|
|
GameMessages::SetStunned stun;
|
|
stun.target = player->GetObjectID();
|
|
stun.StateChangeType = eStateChangeType::PUSH;
|
|
stun.bCantAttack = true;
|
|
stun.bCantEquip = true;
|
|
stun.bCantInteract = true;
|
|
stun.bCantJump = true;
|
|
stun.bCantMove = true;
|
|
stun.bCantTurn = true;
|
|
stun.bCantUseItem = true;
|
|
stun.bDontTerminateInteract = true;
|
|
stun.Send(player->GetSystemAddress());
|
|
|
|
LOG("Loading player %i",
|
|
m_LoadedPlayers);
|
|
m_LobbyPlayers.push_back(player->GetObjectID());
|
|
}
|
|
|
|
void RacingControlComponent::LoadPlayerVehicle(Entity* player,
|
|
uint32_t positionNumber, bool initialLoad) {
|
|
// Load the player's vehicle.
|
|
|
|
if (player == nullptr) {
|
|
return;
|
|
}
|
|
|
|
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
|
|
|
|
if (inventoryComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// Find the player's vehicle.
|
|
|
|
auto* item = inventoryComponent->FindItemByLot(8092);
|
|
|
|
if (item == nullptr) {
|
|
LOG("Failed to find item");
|
|
auto* characterComponent = player->GetComponent<CharacterComponent>();
|
|
|
|
if (characterComponent) {
|
|
m_LoadedPlayers--;
|
|
characterComponent->SendToZone(m_MainWorld);
|
|
}
|
|
return;
|
|
|
|
}
|
|
|
|
// Calculate the vehicle's starting position.
|
|
|
|
auto* path = Game::zoneManager->GetZone()->GetPath(
|
|
GeneralUtils::UTF16ToWTF8(m_PathName));
|
|
|
|
auto spawnPointEntities = Game::entityManager->GetEntitiesByLOT(4843);
|
|
auto startPosition = NiPoint3Constant::ZERO;
|
|
auto startRotation = QuatUtils::IDENTITY;
|
|
const std::string placementAsString = std::to_string(positionNumber);
|
|
for (auto entity : spawnPointEntities) {
|
|
if (!entity) continue;
|
|
if (entity->GetVarAsString(u"placement") == placementAsString) {
|
|
startPosition = entity->GetPosition();
|
|
startRotation = entity->GetRotation();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Make sure the player is at the correct position.
|
|
|
|
GameMessages::Teleport(player->GetObjectID(), startPosition, startRotation, true).SendToClient(player->GetSystemAddress());
|
|
|
|
// Spawn the vehicle entity.
|
|
|
|
EntityInfo info{};
|
|
info.lot = 8092;
|
|
info.pos = startPosition;
|
|
info.rot = startRotation;
|
|
info.spawnerID = m_Parent->GetObjectID();
|
|
|
|
auto* carEntity =
|
|
Game::entityManager->CreateEntity(info, nullptr, m_Parent);
|
|
|
|
// Make the vehicle a child of the racing controller.
|
|
m_Parent->AddChild(carEntity);
|
|
|
|
auto* destroyableComponent =
|
|
carEntity->GetComponent<DestroyableComponent>();
|
|
|
|
// Setup the vehicle stats.
|
|
if (destroyableComponent != nullptr) {
|
|
destroyableComponent->SetMaxImagination(60);
|
|
destroyableComponent->SetImagination(0);
|
|
}
|
|
|
|
// Setup the vehicle as being possessed by the player.
|
|
auto* possessableComponent =
|
|
carEntity->GetComponent<PossessableComponent>();
|
|
|
|
if (possessableComponent != nullptr) {
|
|
possessableComponent->SetPossessor(player->GetObjectID());
|
|
}
|
|
|
|
// Load the vehicle's assemblyPartLOTs for display.
|
|
auto* moduleAssemblyComponent =
|
|
carEntity->GetComponent<ModuleAssemblyComponent>();
|
|
|
|
if (moduleAssemblyComponent) {
|
|
moduleAssemblyComponent->SetSubKey(item->GetSubKey());
|
|
moduleAssemblyComponent->SetUseOptionalParts(false);
|
|
|
|
const auto& lnv = item->GetConfig().values;
|
|
const auto itr = lnv.find(u"assemblyPartLOTs");
|
|
if (itr != lnv.end()) {
|
|
moduleAssemblyComponent->SetAssemblyPartsLOTs(GeneralUtils::ASCIIToUTF16(itr->second->GetValueAsString()));
|
|
}
|
|
}
|
|
|
|
// Setup the player as possessing the vehicle.
|
|
auto* possessorComponent = player->GetComponent<PossessorComponent>();
|
|
|
|
if (possessorComponent != nullptr) {
|
|
possessorComponent->SetPossessable(carEntity->GetObjectID());
|
|
possessorComponent->SetPossessableType(ePossessionType::ATTACHED_VISIBLE); // for racing it's always Attached_Visible
|
|
}
|
|
|
|
// Set the player's current activity as racing.
|
|
auto* characterComponent = player->GetComponent<CharacterComponent>();
|
|
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->SetIsRacing(true);
|
|
}
|
|
|
|
// Init the player's racing entry.
|
|
if (initialLoad) {
|
|
m_RacingPlayers.push_back(
|
|
{ player->GetObjectID(),
|
|
carEntity->GetObjectID(),
|
|
static_cast<uint32_t>(m_RacingPlayers.size()),
|
|
false,
|
|
{},
|
|
startPosition,
|
|
startRotation,
|
|
0,
|
|
0,
|
|
0,
|
|
0 });
|
|
m_AllPlayersReady = false;
|
|
}
|
|
|
|
// Construct and serialize everything when done.
|
|
|
|
Game::entityManager->ConstructEntity(carEntity);
|
|
Game::entityManager->SerializeEntity(player);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
GameMessages::RacingSetPlayerResetInfo resetInfo;
|
|
resetInfo.target = m_Parent->GetObjectID();
|
|
resetInfo.currentLap = 0;
|
|
resetInfo.furthestResetPlane = 0;
|
|
resetInfo.playerID = player->GetObjectID();
|
|
resetInfo.respawnPos = startPosition;
|
|
resetInfo.upcomingPlane = 1;
|
|
resetInfo.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
const auto playerID = player->GetObjectID();
|
|
|
|
// Reset the player to the start position during downtime, in case something
|
|
// went wrong.
|
|
m_Parent->AddCallbackTimer(1, [this, playerID]() {
|
|
auto* player = Game::entityManager->GetEntity(playerID);
|
|
|
|
if (player == nullptr) {
|
|
return;
|
|
}
|
|
|
|
GameMessages::RacingResetPlayerToLastReset resetPlayer;
|
|
resetPlayer.target = m_Parent->GetObjectID();
|
|
resetPlayer.playerID = playerID;
|
|
resetPlayer.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
});
|
|
|
|
GameMessages::SetJetPackMode jetPackMode;
|
|
jetPackMode.target = player->GetObjectID();
|
|
jetPackMode.bUse = false;
|
|
jetPackMode.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
// Set the vehicle's state.
|
|
GameMessages::NotifyVehicleOfRacingObject notifyVehicle;
|
|
notifyVehicle.target = carEntity->GetObjectID();
|
|
notifyVehicle.racingObjectID = m_Parent->GetObjectID();
|
|
notifyVehicle.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
GameMessages::VehicleSetWheelLockState wheelLockState;
|
|
wheelLockState.target = carEntity->GetObjectID();
|
|
wheelLockState.bExtraFriction = false;
|
|
wheelLockState.bLocked = initialLoad;
|
|
wheelLockState.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
// Make sure everything has the correct position.
|
|
GameMessages::Teleport(player->GetObjectID(), startPosition, startRotation, true).SendToClient(player->GetSystemAddress());
|
|
GameMessages::Teleport(carEntity->GetObjectID(), startPosition, startRotation, true).SendToClient(player->GetSystemAddress());
|
|
}
|
|
|
|
void RacingControlComponent::OnRacingClientReady(Entity* player) {
|
|
// Notify the other players that this player is ready.
|
|
|
|
for (auto& racingPlayer : m_RacingPlayers) {
|
|
GameMessages::RacingPlayerLoaded playerLoaded;
|
|
playerLoaded.target = m_Parent->GetObjectID();
|
|
playerLoaded.playerID = racingPlayer.playerID;
|
|
playerLoaded.vehicleID = racingPlayer.vehicleID;
|
|
|
|
if (racingPlayer.playerID != player->GetObjectID()) {
|
|
if (racingPlayer.playerLoaded) {
|
|
playerLoaded.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
racingPlayer.playerLoaded = true;
|
|
|
|
playerLoaded.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
void RacingControlComponent::OnRequestDie(Entity* player, const std::u16string& deathType) {
|
|
// Sent by the client when they collide with something which should smash
|
|
// them.
|
|
|
|
for (auto& racingPlayer : m_RacingPlayers) {
|
|
if (racingPlayer.playerID != player->GetObjectID()) {
|
|
continue;
|
|
}
|
|
|
|
auto* vehicle =
|
|
Game::entityManager->GetEntity(racingPlayer.vehicleID);
|
|
|
|
if (!vehicle) return;
|
|
|
|
if (!racingPlayer.noSmashOnReload) {
|
|
racingPlayer.smashedTimes++;
|
|
LOG("Death type %s", GeneralUtils::UTF16ToWTF8(deathType).c_str());
|
|
GameMessages::Die die;
|
|
die.target = vehicle->GetObjectID();
|
|
die.bClientDeath = false;
|
|
die.bSpawnLoot = true;
|
|
die.deathType = deathType;
|
|
die.directionRelative_Force = 90.0f;
|
|
die.killerID = vehicle->GetObjectID();
|
|
die.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
auto* destroyableComponent = vehicle->GetComponent<DestroyableComponent>();
|
|
int32_t respawnImagination = 0;
|
|
// Reset imagination to half its current value, rounded up to the nearest value divisible by 10, as it was done in live.
|
|
// Do not actually change the value yet. Do that on respawn.
|
|
if (destroyableComponent) {
|
|
respawnImagination = static_cast<int32_t>(ceil(destroyableComponent->GetImagination() / 2.0f / 10.0f)) * 10.0f;
|
|
GameMessages::SetResurrectRestoreValues restoreValues;
|
|
restoreValues.target = vehicle->GetObjectID();
|
|
restoreValues.iImaginationRestore = respawnImagination;
|
|
restoreValues.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
// Respawn the player in 2 seconds, as was done in live. Not sure if this value is in a setting somewhere else...
|
|
vehicle->AddCallbackTimer(2.0f, [=, this]() {
|
|
if (!vehicle || !this->m_Parent) return;
|
|
GameMessages::RacingResetPlayerToLastReset resetPlayer;
|
|
resetPlayer.target = m_Parent->GetObjectID();
|
|
resetPlayer.playerID = racingPlayer.playerID;
|
|
resetPlayer.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
GameMessages::VehicleStopBoost stopBoost;
|
|
stopBoost.target = vehicle->GetObjectID();
|
|
stopBoost.bAffectPassive = true;
|
|
stopBoost.Send(UNASSIGNED_SYSTEM_ADDRESS); // always a broadcast, as before
|
|
|
|
GameMessages::RacingSetPlayerResetInfo resetInfo;
|
|
resetInfo.target = m_Parent->GetObjectID();
|
|
resetInfo.currentLap = static_cast<int32_t>(racingPlayer.lap);
|
|
resetInfo.furthestResetPlane = racingPlayer.respawnIndex;
|
|
resetInfo.playerID = player->GetObjectID();
|
|
resetInfo.respawnPos = racingPlayer.respawnPosition;
|
|
resetInfo.upcomingPlane = racingPlayer.respawnIndex + 1;
|
|
resetInfo.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
for (auto& current : m_RacingPlayers) {
|
|
if (current.playerID != racingPlayer.playerID) continue;
|
|
current.lastPlane = resetInfo.furthestResetPlane;
|
|
current.upcomingPlane = resetInfo.upcomingPlane;
|
|
current.wrongWayCount = 0;
|
|
current.wrongWayTime = -1.0f;
|
|
}
|
|
|
|
DestroyableComponent::Resurrect(*vehicle);
|
|
auto* destroyableComponent = vehicle->GetComponent<DestroyableComponent>();
|
|
// Reset imagination to half its current value, rounded up to the nearest value divisible by 10, as it was done in live.
|
|
if (destroyableComponent) destroyableComponent->SetImagination(respawnImagination);
|
|
Game::entityManager->SerializeEntity(vehicle);
|
|
});
|
|
|
|
auto* characterComponent = player->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->UpdatePlayerStatistic(RacingTimesWrecked);
|
|
}
|
|
} else {
|
|
GameMessages::RacingSetPlayerResetInfo resetInfo;
|
|
resetInfo.target = m_Parent->GetObjectID();
|
|
resetInfo.currentLap = static_cast<int32_t>(racingPlayer.lap);
|
|
resetInfo.furthestResetPlane = racingPlayer.respawnIndex;
|
|
resetInfo.playerID = player->GetObjectID();
|
|
resetInfo.respawnPos = racingPlayer.respawnPosition;
|
|
resetInfo.upcomingPlane = racingPlayer.respawnIndex + 1;
|
|
resetInfo.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
racingPlayer.lastPlane = resetInfo.furthestResetPlane;
|
|
racingPlayer.upcomingPlane = resetInfo.upcomingPlane;
|
|
racingPlayer.wrongWayCount = 0;
|
|
racingPlayer.wrongWayTime = -1.0f;
|
|
|
|
GameMessages::RacingResetPlayerToLastReset resetPlayer;
|
|
resetPlayer.target = m_Parent->GetObjectID();
|
|
resetPlayer.playerID = racingPlayer.playerID;
|
|
resetPlayer.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RacingControlComponent::OnRacingPlayerInfoResetFinished(Entity* player) {
|
|
// When the player has respawned.
|
|
|
|
for (auto& racingPlayer : m_RacingPlayers) {
|
|
if (racingPlayer.playerID != player->GetObjectID()) {
|
|
continue;
|
|
}
|
|
|
|
auto* vehicle =
|
|
Game::entityManager->GetEntity(racingPlayer.vehicleID);
|
|
|
|
if (vehicle == nullptr) {
|
|
return;
|
|
}
|
|
|
|
racingPlayer.noSmashOnReload = false;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
void RacingControlComponent::HandleMessageBoxResponse(Entity* player, int32_t button, const std::string& id) {
|
|
auto* data = GetPlayerData(player->GetObjectID());
|
|
|
|
if (data == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (id == "rewardButton") {
|
|
if (data->collectedRewards) return;
|
|
|
|
data->collectedRewards = true;
|
|
|
|
// Calculate the score, different loot depending on player count
|
|
auto playersRating = m_LoadedPlayers;
|
|
if (m_LoadedPlayers == 1 && m_SoloRacing) {
|
|
playersRating *= 2;
|
|
}
|
|
|
|
const auto score = playersRating * 10 + data->finished;
|
|
Loot::GiveActivityLoot(player, m_Parent->GetObjectID(), m_ActivityID, score);
|
|
|
|
// Giving rewards
|
|
GameMessages::NotifyRacingClient notifyRacingClient;
|
|
notifyRacingClient.target = m_Parent->GetObjectID();
|
|
notifyRacingClient.eventType = eRacingClientNotificationType::REWARD_PLAYER;
|
|
notifyRacingClient.singleClient = player->GetObjectID();
|
|
notifyRacingClient.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
} else if ((id == "ACT_RACE_EXIT_THE_RACE?" || id == "Exit") && button == m_ActivityExitConfirm) {
|
|
auto* vehicle = Game::entityManager->GetEntity(data->vehicleID);
|
|
|
|
if (vehicle == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// Exiting race
|
|
GameMessages::NotifyRacingClient notifyRacingClient;
|
|
notifyRacingClient.target = m_Parent->GetObjectID();
|
|
notifyRacingClient.eventType = eRacingClientNotificationType::EXIT;
|
|
notifyRacingClient.singleClient = player->GetObjectID();
|
|
notifyRacingClient.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
auto* characterComponent = player->GetComponent<CharacterComponent>();
|
|
|
|
if (characterComponent) characterComponent->SendToZone(m_MainWorld);
|
|
|
|
vehicle->Kill();
|
|
}
|
|
}
|
|
|
|
void RacingControlComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
|
// BEGIN Scripted Activity
|
|
outBitStream.Write1();
|
|
|
|
outBitStream.Write<uint32_t>(m_RacingPlayers.size());
|
|
for (const auto& player : m_RacingPlayers) {
|
|
outBitStream.Write(player.playerID);
|
|
|
|
outBitStream.Write(player.data[0]);
|
|
if (player.finished != 0) outBitStream.Write<float>(player.raceTime.count() / 1000.0f);
|
|
else outBitStream.Write(player.data[1]);
|
|
if (player.finished != 0) outBitStream.Write<float>(player.bestLapTime.count() / 1000.0f);
|
|
else outBitStream.Write(player.data[2]);
|
|
if (player.finished == 1) outBitStream.Write<float>(1.0f);
|
|
else outBitStream.Write(player.data[3]);
|
|
outBitStream.Write(player.data[4]);
|
|
outBitStream.Write(player.data[5]);
|
|
outBitStream.Write(player.data[6]);
|
|
outBitStream.Write(player.data[7]);
|
|
outBitStream.Write(player.data[8]);
|
|
outBitStream.Write(player.data[9]);
|
|
}
|
|
|
|
// END Scripted Activity
|
|
|
|
outBitStream.Write1();
|
|
outBitStream.Write<uint16_t>(m_RacingPlayers.size());
|
|
|
|
outBitStream.Write(!m_AllPlayersReady);
|
|
if (!m_AllPlayersReady) {
|
|
int32_t numReady = 0;
|
|
for (const auto& player : m_RacingPlayers) {
|
|
outBitStream.Write1(); // Has more player data
|
|
outBitStream.Write(player.playerID);
|
|
outBitStream.Write(player.vehicleID);
|
|
outBitStream.Write(player.playerIndex);
|
|
outBitStream.Write(player.playerLoaded);
|
|
if (player.playerLoaded) numReady++;
|
|
}
|
|
|
|
outBitStream.Write0(); // No more data
|
|
if (numReady == m_RacingPlayers.size()) m_AllPlayersReady = true;
|
|
}
|
|
|
|
outBitStream.Write(!m_RacingPlayers.empty());
|
|
if (!m_RacingPlayers.empty()) {
|
|
for (const auto& player : m_RacingPlayers) {
|
|
if (player.finished == 0) continue;
|
|
outBitStream.Write1(); // Has more date
|
|
|
|
outBitStream.Write(player.playerID);
|
|
outBitStream.Write(player.finished);
|
|
}
|
|
|
|
outBitStream.Write0(); // No more data
|
|
}
|
|
|
|
outBitStream.Write(bIsInitialUpdate);
|
|
if (bIsInitialUpdate) {
|
|
outBitStream.Write(m_RemainingLaps);
|
|
outBitStream.Write<uint16_t>(m_PathName.size());
|
|
for (const auto character : m_PathName) {
|
|
outBitStream.Write(character);
|
|
}
|
|
}
|
|
|
|
outBitStream.Write(!m_RacingPlayers.empty());
|
|
if (!m_RacingPlayers.empty()) {
|
|
for (const auto& player : m_RacingPlayers) {
|
|
if (player.finished == 0) continue;
|
|
outBitStream.Write1(); // Has more data
|
|
outBitStream.Write(player.playerID);
|
|
outBitStream.Write<float>(player.bestLapTime.count() / 1000.0f);
|
|
outBitStream.Write<float>(player.raceTime.count() / 1000.0f);
|
|
}
|
|
|
|
outBitStream.Write0(); // No more data
|
|
}
|
|
}
|
|
|
|
RacingPlayerInfo* RacingControlComponent::GetPlayerData(LWOOBJID playerID) {
|
|
for (auto& player : m_RacingPlayers) {
|
|
if (player.playerID == playerID) {
|
|
return &player;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void RacingControlComponent::Run(float deltaTime) {
|
|
// This method is a mess.
|
|
|
|
// Pre-load routine
|
|
if (!m_Loaded) {
|
|
// Check if any players has disconnected before loading in
|
|
for (size_t i = 0; i < m_LobbyPlayers.size(); i++) {
|
|
auto* playerEntity =
|
|
Game::entityManager->GetEntity(m_LobbyPlayers[i]);
|
|
|
|
if (playerEntity == nullptr) {
|
|
--m_LoadedPlayers;
|
|
|
|
m_LobbyPlayers.erase(m_LobbyPlayers.begin() + i);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (m_LoadedPlayers >= 2 || (m_LoadedPlayers == 1 && m_SoloRacing)) {
|
|
m_LoadTimer += deltaTime;
|
|
} else {
|
|
m_EmptyTimer += deltaTime;
|
|
}
|
|
|
|
// If a player happens to be left alone for more then 30 seconds without
|
|
// anyone else loading in, send them back to the main world
|
|
if (m_EmptyTimer >= 30) {
|
|
for (const auto player : m_LobbyPlayers) {
|
|
auto* playerEntity =
|
|
Game::entityManager->GetEntity(player);
|
|
|
|
if (playerEntity == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
auto* characterComponent = playerEntity->GetComponent<CharacterComponent>();
|
|
|
|
if (characterComponent) characterComponent->SendToZone(m_MainWorld);
|
|
}
|
|
|
|
m_LobbyPlayers.clear();
|
|
}
|
|
|
|
// From the first 2 players loading in the rest have a max of 15 seconds
|
|
// to load in, can raise this if it's too low
|
|
if (m_LoadTimer >= 15) {
|
|
LOG("Loading all players...");
|
|
|
|
for (size_t positionNumber = 0; positionNumber < m_LobbyPlayers.size(); positionNumber++) {
|
|
LOG("Loading player now!");
|
|
|
|
auto* player =
|
|
Game::entityManager->GetEntity(m_LobbyPlayers[positionNumber]);
|
|
|
|
if (player == nullptr) {
|
|
return;
|
|
}
|
|
|
|
LOG("Loading player now NOW!");
|
|
|
|
LoadPlayerVehicle(player, positionNumber + 1, true);
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
m_Loaded = true;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// The players who will be participating have loaded
|
|
if (!m_Started) {
|
|
// Check if anyone has disconnected during this period
|
|
for (size_t i = 0; i < m_RacingPlayers.size(); i++) {
|
|
auto* playerEntity = Game::entityManager->GetEntity(
|
|
m_RacingPlayers[i].playerID);
|
|
|
|
if (playerEntity == nullptr) {
|
|
m_RacingPlayers.erase(m_RacingPlayers.begin() + i);
|
|
|
|
--m_LoadedPlayers;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If less then 2 players are left, send the rest back to the main world
|
|
if (m_LoadedPlayers < 2 && !(m_LoadedPlayers == 1 && m_SoloRacing)) {
|
|
for (const auto player : m_LobbyPlayers) {
|
|
auto* playerEntity =
|
|
Game::entityManager->GetEntity(player);
|
|
|
|
if (playerEntity == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
auto* characterComponent = playerEntity->GetComponent<CharacterComponent>();
|
|
|
|
if (characterComponent) characterComponent->SendToZone(m_MainWorld);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Check if all players have send a ready message
|
|
|
|
int32_t readyPlayers = 0;
|
|
|
|
for (const auto& player : m_RacingPlayers) {
|
|
if (player.playerLoaded) {
|
|
++readyPlayers;
|
|
}
|
|
}
|
|
|
|
if (readyPlayers >= m_LoadedPlayers) {
|
|
// Setup for racing
|
|
if (m_StartTimer == 0) {
|
|
GameMessages::NotifyRacingClient notifyRacingClient;
|
|
notifyRacingClient.target = m_Parent->GetObjectID();
|
|
notifyRacingClient.eventType = eRacingClientNotificationType::ACTIVITY_START;
|
|
notifyRacingClient.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
for (const auto& player : m_RacingPlayers) {
|
|
auto* vehicle =
|
|
Game::entityManager->GetEntity(player.vehicleID);
|
|
auto* playerEntity =
|
|
Game::entityManager->GetEntity(player.playerID);
|
|
|
|
if (vehicle != nullptr && playerEntity != nullptr) {
|
|
GameMessages::Teleport(player.playerID, player.respawnPosition, player.respawnRotation, true).SendToClient(playerEntity->GetSystemAddress());
|
|
|
|
vehicle->SetPosition(player.respawnPosition);
|
|
vehicle->SetRotation(player.respawnRotation);
|
|
|
|
auto* destroyableComponent =
|
|
vehicle->GetComponent<DestroyableComponent>();
|
|
|
|
if (destroyableComponent != nullptr) {
|
|
destroyableComponent->SetImagination(0);
|
|
}
|
|
|
|
Game::entityManager->SerializeEntity(vehicle);
|
|
Game::entityManager->SerializeEntity(
|
|
playerEntity);
|
|
}
|
|
}
|
|
|
|
GameMessages::ZoneLoadedInfo zoneLoadInfo{};
|
|
zoneLoadInfo.maxPlayers = m_LoadedPlayers;
|
|
m_Parent->GetScript()->OnZoneLoadedInfo(m_Parent, zoneLoadInfo);
|
|
|
|
// Reset players to their start location, without smashing them
|
|
for (auto& player : m_RacingPlayers) {
|
|
auto* vehicleEntity =
|
|
Game::entityManager->GetEntity(player.vehicleID);
|
|
auto* playerEntity =
|
|
Game::entityManager->GetEntity(player.playerID);
|
|
|
|
if (vehicleEntity == nullptr || playerEntity == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
player.noSmashOnReload = true;
|
|
|
|
OnRequestDie(playerEntity);
|
|
}
|
|
}
|
|
// This 6 seconds seems to be hardcoded in the client, start race
|
|
// after that amount of time
|
|
else if (m_StartTimer >= 6) {
|
|
// Activate the players movement
|
|
for (auto& player : m_RacingPlayers) {
|
|
auto* vehicleEntity =
|
|
Game::entityManager->GetEntity(player.vehicleID);
|
|
auto* playerEntity =
|
|
Game::entityManager->GetEntity(player.playerID);
|
|
|
|
if (vehicleEntity == nullptr || playerEntity == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
GameMessages::VehicleUnlockInput unlockInput;
|
|
unlockInput.target = player.vehicleID;
|
|
unlockInput.bLockWheels = false;
|
|
unlockInput.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
// Start the race
|
|
GameMessages::ActivityStart activityStart;
|
|
activityStart.target = m_Parent->GetObjectID();
|
|
activityStart.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
m_Started = true;
|
|
|
|
LOG("Starting race");
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
m_StartTime = std::chrono::high_resolution_clock::now();
|
|
}
|
|
|
|
m_StartTimer += deltaTime;
|
|
} else {
|
|
m_StartTimer = 0;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Race routines
|
|
auto* path = Game::zoneManager->GetZone()->GetPath(
|
|
GeneralUtils::UTF16ToWTF8(m_PathName));
|
|
|
|
for (auto& player : m_RacingPlayers) {
|
|
auto* vehicle = Game::entityManager->GetEntity(player.vehicleID);
|
|
auto* playerEntity =
|
|
Game::entityManager->GetEntity(player.playerID);
|
|
|
|
if (vehicle == nullptr || playerEntity == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
const auto vehiclePosition = vehicle->GetPosition();
|
|
|
|
// If the player is this far below the map, safe to assume they should
|
|
// be smashed by death plane
|
|
if (vehiclePosition.y < -500) {
|
|
GameMessages::Die die;
|
|
die.target = vehicle->GetObjectID();
|
|
die.bClientDeath = true;
|
|
die.bSpawnLoot = false;
|
|
die.killerID = m_Parent->GetObjectID();
|
|
die.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
OnRequestDie(playerEntity);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (m_Finished != 0) Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
if (path && player.lap < m_NumberOfLaps) UpdateWrongWay(player, *vehicle, *path, deltaTime);
|
|
|
|
// Loop through all the waypoints and see if the player has reached a
|
|
// new checkpoint
|
|
uint32_t respawnIndex = 0;
|
|
for (const auto& waypoint : path->pathWaypoints) {
|
|
if (player.lap == m_NumberOfLaps) {
|
|
break;
|
|
}
|
|
|
|
if (player.respawnIndex == respawnIndex) {
|
|
++respawnIndex;
|
|
|
|
continue;
|
|
}
|
|
|
|
const auto& position = waypoint.position;
|
|
|
|
if (std::abs(static_cast<int>(respawnIndex) - static_cast<int>(player.respawnIndex)) > 10 &&
|
|
player.respawnIndex != path->pathWaypoints.size() - 1) {
|
|
++respawnIndex;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (Vector3::DistanceSquared(position, vehiclePosition) > 50 * 50) {
|
|
++respawnIndex;
|
|
|
|
continue;
|
|
}
|
|
|
|
// Only go upwards, except if we've lapped
|
|
// Not sure how we are supposed to check if they've reach a
|
|
// checkpoint, within 50 units seems safe
|
|
if (!(respawnIndex > player.respawnIndex ||
|
|
player.respawnIndex == path->pathWaypoints.size() - 1)) {
|
|
++respawnIndex;
|
|
|
|
continue;
|
|
}
|
|
|
|
// Some offset up to make they don't fall through the terrain on a
|
|
// respawn, seems to fix itself to the track anyhow
|
|
if (waypoint.racing.isResetNode) {
|
|
player.respawnPosition = position + NiPoint3Constant::UNIT_Y * 5;
|
|
player.respawnRotation = vehicle->GetRotation();
|
|
}
|
|
player.respawnIndex = respawnIndex;
|
|
|
|
// Reached the start point, lapped
|
|
if (respawnIndex == 0) {
|
|
const auto now = std::chrono::high_resolution_clock::now();
|
|
const auto lapTime = std::chrono::duration_cast<std::chrono::milliseconds>(now - (player.lap == 0 ? m_StartTime : player.lapTime));
|
|
|
|
// Cheating check
|
|
if (lapTime.count() < 40000) {
|
|
continue;
|
|
}
|
|
|
|
player.lapTime = now;
|
|
|
|
if (player.bestLapTime > lapTime || player.lap == 0) {
|
|
player.bestLapTime = lapTime;
|
|
|
|
LOG("Best lap time (%llu)", lapTime);
|
|
}
|
|
|
|
player.lap++;
|
|
|
|
auto* missionComponent =
|
|
playerEntity->GetComponent<MissionComponent>();
|
|
|
|
if (missionComponent != nullptr) {
|
|
|
|
// Progress lap time tasks
|
|
missionComponent->Progress(eMissionTaskType::RACING, lapTime.count(), static_cast<LWOOBJID>(eRacingTaskParam::LAP_TIME));
|
|
|
|
if (player.lap == m_NumberOfLaps) {
|
|
m_Finished++;
|
|
player.finished = m_Finished;
|
|
|
|
const auto raceTime = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_StartTime);
|
|
|
|
player.raceTime = raceTime;
|
|
|
|
LOG("Completed time %llums %fs", raceTime.count(), raceTime.count() / 1000.0f);
|
|
|
|
LeaderboardManager::SaveScore(playerEntity->GetObjectID(), m_ActivityID, static_cast<float>(player.raceTime.count()) / 1000, static_cast<float>(player.bestLapTime.count()) / 1000, static_cast<float>(player.finished == 1));
|
|
// Entire race time
|
|
missionComponent->Progress(eMissionTaskType::RACING, player.raceTime.count(), static_cast<LWOOBJID>(eRacingTaskParam::TOTAL_TRACK_TIME));
|
|
|
|
missionComponent->Progress(eMissionTaskType::RACING, 0, static_cast<LWOOBJID>(eRacingTaskParam::COMPETED_IN_RACE)); // Progress task for competing in a race
|
|
missionComponent->Progress(eMissionTaskType::RACING, player.smashedTimes, static_cast<LWOOBJID>(eRacingTaskParam::SAFE_DRIVER)); // Finish a race without being smashed.
|
|
|
|
// If solo racing is enabled OR if there are 3 players in the race, progress placement tasks.
|
|
if (m_SoloRacing || m_RacingPlayers.size() > 2) {
|
|
missionComponent->Progress(eMissionTaskType::RACING, player.finished, static_cast<LWOOBJID>(eRacingTaskParam::FINISH_WITH_PLACEMENT)); // Finish in 1st place on a race
|
|
if (player.finished == 1) {
|
|
missionComponent->Progress(eMissionTaskType::RACING, Game::zoneManager->GetZone()->GetWorldID(), static_cast<LWOOBJID>(eRacingTaskParam::FIRST_PLACE_MULTIPLE_TRACKS)); // Finish in 1st place on multiple tracks.
|
|
missionComponent->Progress(eMissionTaskType::RACING, Game::zoneManager->GetZone()->GetWorldID(), static_cast<LWOOBJID>(eRacingTaskParam::WIN_RACE_IN_WORLD)); // Finished first place in specific world.
|
|
}
|
|
if (player.finished == m_RacingPlayers.size()) {
|
|
missionComponent->Progress(eMissionTaskType::RACING, Game::zoneManager->GetZone()->GetWorldID(), static_cast<LWOOBJID>(eRacingTaskParam::LAST_PLACE_FINISH)); // Finished first place in specific world.
|
|
}
|
|
}
|
|
|
|
auto* characterComponent = playerEntity->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->TrackRaceCompleted(m_Finished == 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
LOG("Lapped (%i) in (%llums %fs)", player.lap,
|
|
lapTime.count(), lapTime.count() / 1000.0f);
|
|
}
|
|
|
|
LOG("Reached point (%i)/(%i)", player.respawnIndex,
|
|
path->pathWaypoints.size());
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool RacingControlComponent::MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg) {
|
|
for (const auto& dataUnique : msg.racingSettings | std::views::values) {
|
|
if (!dataUnique) continue;
|
|
const auto* const data = dataUnique.get();
|
|
if (data->GetKey() == u"Race_PathName" && data->GetValueType() == LDF_TYPE_UTF_16) {
|
|
m_PathName = static_cast<const LDFData<std::u16string>*>(data)->GetValue();
|
|
} else if (data->GetKey() == u"activityID" && data->GetValueType() == LDF_TYPE_S32) {
|
|
m_ActivityID = static_cast<const LDFData<int32_t>*>(data)->GetValue();
|
|
} else if (data->GetKey() == u"Number_of_Laps" && data->GetValueType() == LDF_TYPE_S32) {
|
|
m_NumberOfLaps = static_cast<const LDFData<int32_t>*>(data)->GetValue();
|
|
m_RemainingLaps = m_NumberOfLaps;
|
|
} else if (data->GetKey() == u"Minimum_Players_for_Group_Achievements" && data->GetValueType() == LDF_TYPE_S32) {
|
|
m_MinimumPlayersForGroupAchievements = static_cast<const LDFData<int32_t>*>(data)->GetValue();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void RacingControlComponent::StepResetPlanes(RacingPlayerInfo& player, const std::vector<PathWaypoint>& waypoints, const NiPoint3& position) {
|
|
// How long the client counts down (its "UpdateRaceWrongWay" timer, 6 in 1.10.64) before a wrong way racer should
|
|
// be put back
|
|
constexpr float WRONG_WAY_SECONDS = 6.0f;
|
|
constexpr float EPSILON = 1e-5f;
|
|
|
|
const auto count = static_cast<uint32_t>(waypoints.size());
|
|
if (count == 0) return;
|
|
|
|
// Which side of a reset plane the car is on: the plane goes through the waypoint, facing along its rotation
|
|
// (ClassifyPointAgainstFacingPlane @ 010e2b80 in 1.10.64). 1 = in front, 2 = behind, 3 = on it.
|
|
const auto side = [&](const uint32_t index) {
|
|
const auto& waypoint = waypoints[index];
|
|
const float distance = QuatUtils::Forward(waypoint.rotation).DotProduct(position - waypoint.position);
|
|
return distance > EPSILON ? 1 : distance < -EPSILON ? 2 : 3;
|
|
};
|
|
|
|
// Behind the last plane: driven back through it (LWORacingControlComponent::CheckLastResetPlane @ 00c7f1a0, CrossResetPlaneBackward @ 00cba380). The second plane in a row makes the
|
|
// client show its wrong way countdown (UpdateWrongWayCount @ 00be5c10).
|
|
bool wentBack = false;
|
|
for (uint32_t guard = 0; guard < count && player.lastPlane < count && side(player.lastPlane) == 2; guard++) {
|
|
wentBack = true;
|
|
player.wrongWayCount++;
|
|
if (player.wrongWayCount == 2) player.wrongWayTime = WRONG_WAY_SECONDS;
|
|
player.upcomingPlane = player.lastPlane;
|
|
player.lastPlane = player.lastPlane == 0 ? count - 1 : player.lastPlane - 1;
|
|
}
|
|
if (wentBack) return;
|
|
|
|
// In front of the upcoming plane: driven forward through it, which ends going the wrong way (CheckUpcomingResetPlane @ 00c7edf0)
|
|
for (uint32_t guard = 0; guard < count && player.upcomingPlane < count && side(player.upcomingPlane) == 1; guard++) {
|
|
player.wrongWayCount = 0;
|
|
player.wrongWayTime = -1.0f;
|
|
player.lastPlane = player.upcomingPlane;
|
|
player.upcomingPlane = (player.upcomingPlane + 1) % count;
|
|
}
|
|
}
|
|
|
|
void RacingControlComponent::UpdateWrongWay(RacingPlayerInfo& player, const Entity& vehicle, const Path& path, const float deltaTime) {
|
|
StepResetPlanes(player, path.pathWaypoints, vehicle.GetPosition());
|
|
|
|
if (player.wrongWayTime < 0.0f) return;
|
|
player.wrongWayTime -= deltaTime;
|
|
if (player.wrongWayTime > 0.0f) return;
|
|
|
|
// Out of time: back to the last reset point, the way a reset without a smash does it (the client moves the car,
|
|
// clears its wrong way state and hides the countdown when it gets the reset info)
|
|
GameMessages::RacingSetPlayerResetInfo resetInfo;
|
|
resetInfo.target = m_Parent->GetObjectID();
|
|
resetInfo.currentLap = static_cast<int32_t>(player.lap);
|
|
resetInfo.furthestResetPlane = player.respawnIndex;
|
|
resetInfo.playerID = player.playerID;
|
|
resetInfo.respawnPos = player.respawnPosition;
|
|
resetInfo.upcomingPlane = player.respawnIndex + 1;
|
|
resetInfo.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
GameMessages::RacingResetPlayerToLastReset resetPlayer;
|
|
resetPlayer.target = m_Parent->GetObjectID();
|
|
resetPlayer.playerID = player.playerID;
|
|
resetPlayer.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
player.lastPlane = resetInfo.furthestResetPlane;
|
|
player.upcomingPlane = resetInfo.upcomingPlane;
|
|
player.wrongWayCount = 0;
|
|
player.wrongWayTime = -1.0f;
|
|
}
|