Files
DarkflameServer/dGame/dGameMessages/MissionMessages.cpp
Aaron Kimbrell fe4a1e5e98 fix: check the mission state before completing it from a dialogue
MissionDialogueOK accepted or completed the mission for whichever
object the message named as the responder. The responder now has to be
the player that sent the message, the object the dialogue belongs to
has to give or take the mission (on its own or through a random
mission's pool), a mission is only accepted when the player can accept
it and only completed when it is ready to complete. The object's
script runs after these checks.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:04:01 -06:00

336 lines
12 KiB
C++

#include "MissionMessages.h"
#include "BitStreamUtils.h"
#include "Character.h"
#include "CppScripts.h"
#include "dConfig.h"
#include "eMissionLockState.h"
#include "eMissionState.h"
#include "eMissionTaskType.h"
#include "ePlayerFlag.h"
#include "eReplicaComponentType.h"
#include "Entity.h"
#include "EntityManager.h"
#include "Game.h"
#include "Logger.h"
#include "Mission.h"
#include "CollectibleComponent.h"
#include "MissionComponent.h"
#include "MissionOfferComponent.h"
#include "EffectsMessages.h"
namespace GameMessages {
void OfferMission::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(missionID);
bitStream.Write(offerer);
}
bool OfferMission::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(missionID));
VALIDATE_READ(bitStream.Read(offerer));
return true;
}
void RespondToMission::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(missionID);
bitStream.Write(playerID);
bitStream.Write(receiver);
BitStreamUtils::WriteOptional(bitStream, rewardItem, LOT_NULL);
}
bool RespondToMission::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(missionID));
VALIDATE_READ(bitStream.Read(playerID));
VALIDATE_READ(bitStream.Read(receiver));
VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, rewardItem, LOT_NULL));
return true;
}
void RespondToMission::Handle(Entity& entity, const SystemAddress& sysAddr) {
const auto reward = rewardItem;
MissionComponent* missionComponent = static_cast<MissionComponent*>(entity.GetComponent(eReplicaComponentType::MISSION));
if (!missionComponent) {
LOG("Unable to get mission component for entity %llu to handle RespondToMission", playerID);
return;
}
Mission* mission = missionComponent->GetMission(missionID);
if (mission) {
mission->SetReward(reward);
} else {
LOG("Unable to get mission %i for entity %llu to update reward in RespondToMission", missionID, playerID);
}
Entity* offerer = Game::entityManager->GetEntity(receiver);
if (offerer == nullptr) {
LOG("Unable to get receiver entity %llu for RespondToMission", receiver);
return;
}
offerer->GetScript()->OnRespondToMission(offerer, missionID, Game::entityManager->GetEntity(playerID), reward);
}
void NotifyMission::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(missionID);
bitStream.Write(missionState);
bitStream.Write(sendingRewards);
}
bool NotifyMission::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(missionID));
VALIDATE_READ(bitStream.Read(missionState));
VALIDATE_READ(bitStream.Read(sendingRewards));
return true;
}
void NotifyMissionTask::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(missionID);
bitStream.Write(taskMask);
bitStream.Write<uint8_t>(updates.size());
for (const auto update : updates) bitStream.Write(update);
}
bool NotifyMissionTask::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(missionID));
VALIDATE_READ(bitStream.Read(taskMask));
uint8_t count{};
VALIDATE_READ(bitStream.Read(count));
updates.resize(count);
for (auto& update : updates) VALIDATE_READ(bitStream.Read(update));
return true;
}
void ResetMissions::Serialize(RakNet::BitStream& bitStream) const {
BitStreamUtils::WriteOptional(bitStream, missionID, -1);
}
bool ResetMissions::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, missionID, -1));
return true;
}
void MissionDialogueOK::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bIsComplete);
bitStream.Write(iMissionState);
bitStream.Write(missionID);
bitStream.Write(responder);
}
bool MissionDialogueOK::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(bIsComplete));
VALIDATE_READ(bitStream.Read(iMissionState));
VALIDATE_READ(bitStream.Read(missionID));
VALIDATE_READ(bitStream.Read(responder));
return true;
}
void MissionDialogueCancelled::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bIsComplete);
bitStream.Write(iMissionState);
bitStream.Write(missionID);
bitStream.Write(responder);
}
bool MissionDialogueCancelled::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(bIsComplete));
VALIDATE_READ(bitStream.Read(iMissionState));
VALIDATE_READ(bitStream.Read(missionID));
VALIDATE_READ(bitStream.Read(responder));
return true;
}
void MissionDialogueOK::Handle(Entity& entityRef, const SystemAddress& sysAddr) {
Entity* entity = &entityRef;
Entity* player = Game::entityManager->GetEntity(responder);
// A player answers a mission dialogue for themselves
if (!player || !player->IsPlayer() || player->GetSystemAddress() != sysAddr) {
LOG("Responder %llu of MissionDialogueOK for mission %i is not the player that sent it", responder, missionID);
return;
}
// Get the player's mission component
MissionComponent* missionComponent = static_cast<MissionComponent*>(player->GetComponent(eReplicaComponentType::MISSION));
if (!missionComponent) {
LOG("Unable to get mission component for entity %llu to handle MissionDialogueOK", player->GetObjectID());
return;
}
// The mission has to be one the object the dialogue belongs to gives or takes
const auto* const missionOfferComponent = entity->GetComponent<MissionOfferComponent>();
if (!missionOfferComponent || !missionOfferComponent->HasMission(missionID)) {
LOG("Object %llu (LOT %i) has no mission %i for player %llu to answer", entity->GetObjectID(), entity->GetLOT(), missionID, player->GetObjectID());
return;
}
const bool isAccepting = iMissionState == eMissionState::AVAILABLE || iMissionState == eMissionState::COMPLETE_AVAILABLE;
const bool isCompleting = iMissionState == eMissionState::READY_TO_COMPLETE || iMissionState == eMissionState::COMPLETE_READY_TO_COMPLETE;
if (isAccepting && !missionComponent->CanAccept(missionID)) {
LOG("Player %llu tried to accept mission %i, which they can not accept", player->GetObjectID(), missionID);
return;
}
if (isCompleting) {
// Only a mission with all of its tasks done can be handed in
const auto* const readyMission = missionComponent->GetMission(missionID);
if (!readyMission || !readyMission->IsReadyToComplete()) {
LOG("Player %llu tried to hand in mission %i, which is not ready to complete", player->GetObjectID(), missionID);
return;
}
}
entity->GetScript()->OnMissionDialogueOK(entity, player, missionID, iMissionState);
if (isAccepting) {
missionComponent->AcceptMission(missionID);
} else if (isCompleting) {
// LWOMissionComponent::MissionWindowOK (0x00d501f0) only turns a mission in when every reward fits
// (CanReceiveAllRewards, with the reward the player picked)
const auto* const mission = missionComponent->GetMission(missionID);
const auto chosen = mission && mission->GetReward() > 0 ? mission->GetReward() : -1;
if (!mission || !mission->CanReceiveAllRewards(chosen)) {
LOG("Player %llu cannot receive all the rewards of mission %i, not turning it in", player->GetObjectID(), missionID);
return;
}
missionComponent->CompleteMission(missionID);
}
if (Game::config->GetValue("allow_players_to_skip_cinematics") != "1"
|| !player->GetCharacter()
|| !player->GetCharacter()->GetPlayerFlag(ePlayerFlag::DLU_SKIP_CINEMATICS)) return;
player->AddCallbackTimer(0.5f, [player]() {
if (!player) return;
GameMessages::EndCinematic endCinematic;
endCinematic.target = player->GetObjectID();
endCinematic.Send(player->GetSystemAddress());
});
}
void RequestLinkedMission::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(playerID);
bitStream.Write(missionID);
bitStream.Write(bMissionOffered);
}
bool RequestLinkedMission::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(playerID));
VALIDATE_READ(bitStream.Read(missionID));
VALIDATE_READ(bitStream.Read(bMissionOffered));
return true;
}
void RequestLinkedMission::Handle(Entity& entity, const SystemAddress& sysAddr) {
auto* player = Game::entityManager->GetEntity(playerID);
auto* missionOfferComponent = static_cast<MissionOfferComponent*>(entity.GetComponent(eReplicaComponentType::MISSION_OFFER));
if (missionOfferComponent != nullptr) {
missionOfferComponent->OfferLinkedMission(player, missionID);
}
}
void SetFlag::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bFlag);
bitStream.Write(iFlagID);
}
bool SetFlag::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(bFlag));
VALIDATE_READ(bitStream.Read(iFlagID));
return true;
}
void SetFlag::Handle(Entity& entity, const SystemAddress& sysAddr) {
auto character = entity.GetCharacter();
if (character) character->SetPlayerFlag(iFlagID, bFlag);
// This is always set the first time a player loads into a world from character select
// and is used to know when to refresh the players inventory items so they show up.
if (iFlagID == ePlayerFlag::IS_NEWS_SCREEN_VISIBLE && bFlag) {
entity.SetVar<bool>(u"dlu_first_time_load", true);
}
}
void NotifyClientFlagChange::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bFlag);
bitStream.Write(iFlagID);
}
bool NotifyClientFlagChange::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(bFlag));
VALIDATE_READ(bitStream.Read(iFlagID));
return true;
}
void HasBeenCollected::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(playerID);
}
bool HasBeenCollected::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(playerID));
return true;
}
void HasBeenCollected::Handle(Entity& entity, const SystemAddress& sysAddr) {
Entity* player = Game::entityManager->GetEntity(playerID);
if (!player || entity.GetCollectibleID() == 0) return;
// Collectibles of a mission to accept only count while the player has it
const auto* const collectible = entity.GetComponent<CollectibleComponent>();
if (collectible && !collectible->CountsFor(*player)) return;
MissionComponent* missionComponent = static_cast<MissionComponent*>(player->GetComponent(eReplicaComponentType::MISSION));
if (missionComponent) {
missionComponent->Progress(eMissionTaskType::COLLECTION, entity.GetLOT(), entity.GetObjectID());
}
}
SetMissionTypeState::SetMissionTypeState() : NetGameMsg(MessageType::Game::SET_MISSION_TYPE_STATE), state{ eMissionLockState::NEW } {}
void SetMissionTypeState::Serialize(RakNet::BitStream& bitStream) const {
BitStreamUtils::WriteOptional(bitStream, state, eMissionLockState::NEW);
BitStreamUtils::WriteLengthPrefixed(bitStream, subtype);
BitStreamUtils::WriteLengthPrefixed(bitStream, type);
}
bool SetMissionTypeState::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, state, eMissionLockState::NEW));
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, subtype));
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, type));
return true;
}
void SetMissionTypeState::Handle(Entity& entity, const SystemAddress& sysAddr) {
auto* const missionComponent = entity.GetComponent<MissionComponent>();
if (missionComponent) missionComponent->SetMissionTypeState(type, subtype, state);
}
void SetMissionViewed::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(missionID);
bitStream.Write(viewed);
}
bool SetMissionViewed::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(missionID));
VALIDATE_READ(bitStream.Read(viewed));
return true;
}
void SetMissionViewed::Handle(Entity& entity, const SystemAddress& sysAddr) {
auto* const missionComponent = entity.GetComponent<MissionComponent>();
if (missionComponent) missionComponent->SetMissionViewed(missionID, viewed);
}
void NotifyLevelRewards::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(level);
bitStream.Write(sendingRewards);
}
bool NotifyLevelRewards::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(level));
VALIDATE_READ(bitStream.Read(sendingRewards));
return true;
}
}