Skip to content

Commit 5e8dd18

Browse files
committed
Rework of party member quest progresion PR #769
Definitely is not compatible with 1.8.0 servers. The community loves Members being able to advance quests (instead of only Party Leader), but it introduced a bunch of problems with duplicate quest triggers. This rework deals with it (hopefully). The most fundamental problem is that ScopedQuestOverride() doesn't work, because quest progress is executed on a different thread; the ScopedQuestOverride() is destructed before the quest OnEvent...() routines are invoked. So all the Members send their updates to the server even when told not to. The server just ignored them all before, but now with Member quest progression enabled, it ends up causing replicated quest updates. Some quests ignore it, many break. The engine is in control of the content of events passed to the OnEvent routines. I could not find a reliable way to pass any context to the OnEvent routines through that barrier (it runs async, so matching up was nigh-on impossible). So ended up implementing a dedup history server-side, where there is enough context to figure this out. The worst-case flow (when a Member triggers progress), is: 1) Member sends update. 2) Update is forwarded to Leader, only. 3) Leader OnEvent hooks send the Leader update to the server. 4) Leader SendsToParty 5) Party members reflect the updates to the Server, but are dropped by the dedup history because the Leader has already sent it. 6) Entries in the dedup history time out in seconds (needs some tuning), so quests can rewind and stages can be triggered multiple times to evaluate. Merged with old duplicate quest stage "fix"
1 parent 234465e commit 5e8dd18

File tree

9 files changed

+296
-75
lines changed

9 files changed

+296
-75
lines changed

Code/client/Games/Skyrim/Forms/TESQuest.cpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,14 @@ bool TESQuest::EnsureQuestStarted(bool& success, bool force)
9090

9191
bool TESQuest::SetStage(uint16_t newStage)
9292
{
93-
ScopedQuestOverride _;
94-
9593
TP_THIS_FUNCTION(TSetStage, bool, TESQuest, uint16_t);
9694
POINTER_SKYRIMSE(TSetStage, SetStage, 25004);
9795
return SetStage(this, newStage);
9896
}
9997

100-
void TESQuest::ScriptSetStage(uint16_t stageIndex)
98+
void TESQuest::ScriptSetStage(uint16_t stageIndex, bool bForce)
10199
{
102-
if (currentStage == stageIndex || IsStageDone(stageIndex))
100+
if ((currentStage == stageIndex || IsStageDone(stageIndex)) && !bForce) // Not clear this should be suppressed now that server dedups
103101
return;
104102

105103
using Quest = TESQuest;

Code/client/Games/Skyrim/Forms/TESQuest.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ struct TESQuest : BGSStoryManagerTreeForm
127127
bool EnsureQuestStarted(bool& succeded, bool force);
128128

129129
bool SetStage(uint16_t stage);
130-
void ScriptSetStage(uint16_t stage);
130+
void ScriptSetStage(uint16_t stage, bool bForce = false);
131131
void SetStopped();
132132
};
133133

Code/client/Services/Debug/Views/QuestDebugView.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ void DebugService::DrawQuestDebugView()
6161
sprintf_s(setStage, std::size(setStage), "Set stage (%d)", pStage->stageIndex);
6262

6363
if (ImGui::Button(setStage))
64-
pQuest->ScriptSetStage(pStage->stageIndex);
64+
pQuest->ScriptSetStage(pStage->stageIndex, true);
6565
}
6666
}
6767

Code/client/Services/Generic/QuestService.cpp

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons
5757
if (ScopedQuestOverride::IsOverriden() || !m_world.Get().GetPartyService().IsInParty())
5858
return BSTEventResult::kOk;
5959

60-
spdlog::info("Quest start/stop event: {:X}", apEvent->formId);
61-
6260
if (TESQuest* pQuest = Cast<TESQuest>(TESForm::GetById(apEvent->formId)))
6361
{
6462
if (IsNonSyncableQuest(pQuest))
@@ -72,15 +70,21 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons
7270
auto& modSys = m_world.GetModSystem();
7371
if (modSys.GetServerModId(pQuest->formID, Id))
7472
{
75-
spdlog::info(__FUNCTION__ ": queuing type none/misc quest gameId {:X} questStage {} questStatus {} questType {} formId {:X} name {}",
76-
Id.LogFormat(), pQuest->currentStage, pQuest->IsStopped() ? RequestQuestUpdate::Stopped : RequestQuestUpdate::Started,
77-
static_cast<std::underlying_type_t<TESQuest::Type>>(pQuest->type),
78-
pQuest->formID, pQuest->fullName.value.AsAscii());
73+
spdlog::info(__FUNCTION__ ": queuing type none/misc quest {} gameId {:X} questStage {} questType {} formId {:X} name {}",
74+
pQuest->IsStopped() ? "stop" : "start", Id.LogFormat(), pQuest->currentStage,
75+
static_cast<std::underlying_type_t<TESQuest::Type>>(pQuest->type), pQuest->formID, pQuest->fullName.value.AsAscii());
7976
}
8077
}
81-
82-
m_world.GetRunner().Queue(
83-
[&, formId = pQuest->formID, stageId = pQuest->currentStage, stopped = pQuest->IsStopped(), type = pQuest->type]()
78+
79+
spdlog::info(__FUNCTION__ ": quest {} formId: {:X}, questStage: {}, questType: {}, name: {}",
80+
pQuest->IsStopped() ? "stopped" : "started",
81+
pQuest->formID,
82+
pQuest->currentStage,
83+
static_cast<std::underlying_type_t<TESQuest::Type>>(pQuest->type),
84+
pQuest->fullName.value.AsAscii());
85+
86+
m_world.GetRunner().Queue([&, formId = pQuest->formID, stageId = pQuest->currentStage,
87+
stopped = pQuest->IsStopped(), type = pQuest->type]()
8488
{
8589
GameId Id;
8690
auto& modSys = m_world.GetModSystem();
@@ -91,7 +95,6 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons
9195
update.Stage = stageId;
9296
update.Status = stopped ? RequestQuestUpdate::Stopped : RequestQuestUpdate::Started;
9397
update.ClientQuestType = static_cast<std::underlying_type_t<TESQuest::Type>>(type);
94-
9598
m_world.GetTransport().Send(update);
9699
}
97100
});
@@ -105,9 +108,6 @@ BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const Ev
105108
if (ScopedQuestOverride::IsOverriden() || !m_world.Get().GetPartyService().IsInParty())
106109
return BSTEventResult::kOk;
107110

108-
spdlog::info("Quest stage event: {:X}, stage: {}", apEvent->formId, apEvent->stageId);
109-
110-
// there is no reason to even fetch the quest object, since the event provides everything already....
111111
if (TESQuest* pQuest = Cast<TESQuest>(TESForm::GetById(apEvent->formId)))
112112
{
113113
if (IsNonSyncableQuest(pQuest))
@@ -121,14 +121,15 @@ BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const Ev
121121
auto& modSys = m_world.GetModSystem();
122122
if (modSys.GetServerModId(pQuest->formID, Id))
123123
{
124-
spdlog::info(__FUNCTION__ ": queuing type none/misc quest gameId {:X} questStage {} questStatus {} questType {} formId {:X} name {}",
125-
Id.LogFormat(), pQuest->currentStage,
126-
RequestQuestUpdate::StageUpdate,
127-
static_cast<std::underlying_type_t<TESQuest::Type>>(pQuest->type),
124+
spdlog::info(__FUNCTION__ ": queuing type none/misc quest update gameId {:X} questStage {} questType {} formId {:X} name {}",
125+
Id.LogFormat(), pQuest->currentStage, static_cast<std::underlying_type_t<TESQuest::Type>>(pQuest->type),
128126
pQuest->formID, pQuest->fullName.value.AsAscii());
129127
}
130128
}
131129

130+
spdlog::info(__FUNCTION__ ": quest updated formId: {:X}, questStage: {}, questType: {}, name: {}",
131+
pQuest->formID, pQuest->currentStage, static_cast<std::underlying_type_t<TESQuest::Type>>(pQuest->type), pQuest->fullName.value.AsAscii());
132+
132133
m_world.GetRunner().Queue(
133134
[&, formId = apEvent->formId, stageId = apEvent->stageId, type = pQuest->type]()
134135
{
@@ -141,7 +142,6 @@ BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const Ev
141142
update.Stage = stageId;
142143
update.Status = RequestQuestUpdate::StageUpdate;
143144
update.ClientQuestType = static_cast<std::underlying_type_t<TESQuest::Type>>(type);
144-
145145
m_world.GetTransport().Send(update);
146146
}
147147
});
@@ -157,7 +157,7 @@ void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept
157157
TESQuest* pQuest = Cast<TESQuest>(TESForm::GetById(formId));
158158
if (!pQuest)
159159
{
160-
spdlog::error("Failed to find quest, base id: {:X}, mod id: {:X}", aUpdate.Id.BaseId, aUpdate.Id.ModId);
160+
spdlog::error(__FUNCTION__ ": failed to find quest, gameId: {:X}, stage: {}", aUpdate.Id.LogFormat(), aUpdate.Stage);
161161
return;
162162
}
163163

@@ -169,39 +169,64 @@ void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept
169169
}
170170

171171
bool bResult = false;
172+
bool bRunning = pQuest->getState() == TESQuest::State::Running;
172173
switch (aUpdate.Status)
173174
{
174175
case NotifyQuestUpdate::Started:
175-
{
176-
pQuest->ScriptSetStage(aUpdate.Stage);
177-
pQuest->SetActive(true);
176+
if (bRunning)
177+
{
178+
spdlog::info(__FUNCTION__ ": suppressing duplicate quest start gameId: {:X}, questStage: {}, questStatus: {}, questType: {}, formId: {:X}, name: {}",
179+
aUpdate.Id.LogFormat(), aUpdate.Stage, aUpdate.Status, aUpdate.ClientQuestType, formId, pQuest->fullName.value.AsAscii());
180+
}
181+
else
182+
{
183+
spdlog::info(__FUNCTION__ ": quest started remotely gameId: {:X}, questStage: {}, questStatus: {}, questType: {}, formId: {:X}, name: {}",
184+
aUpdate.Id.LogFormat(), aUpdate.Stage, aUpdate.Status, aUpdate.ClientQuestType, formId, pQuest->fullName.value.AsAscii());
185+
pQuest->ScriptSetStage(aUpdate.Stage);
186+
pQuest->SetActive(true);
187+
}
178188
bResult = true;
179189
spdlog::info("Remote quest started: {:X}, stage: {}", formId, aUpdate.Stage);
180190
break;
181-
}
191+
182192
case NotifyQuestUpdate::StageUpdate:
193+
spdlog::info(__FUNCTION__ ": quest updated remotely gameId: {:X}, questStage: {}, questStatus: {}, questType: {}, formId: {:X}, name: {}",
194+
aUpdate.Id.LogFormat(), aUpdate.Stage, aUpdate.Status, aUpdate.ClientQuestType, formId, pQuest->fullName.value.AsAscii());
195+
183196
pQuest->ScriptSetStage(aUpdate.Stage);
184197
bResult = true;
185-
spdlog::info("Remote quest updated: {:X}, stage: {}", formId, aUpdate.Stage);
186198
break;
199+
187200
case NotifyQuestUpdate::Stopped:
201+
spdlog::info(__FUNCTION__ ": quest stopped remotely gameId: {:X}, questStage: {}, questStatus: {}, questType: {}, formId: {:X}, name: {}",
202+
aUpdate.Id.LogFormat(), aUpdate.Stage, aUpdate.Status, aUpdate.ClientQuestType, formId, pQuest->fullName.value.AsAscii());
188203
bResult = StopQuest(formId);
189-
spdlog::info("Remote quest stopped: {:X}, stage: {}", formId, aUpdate.Stage);
190204
break;
205+
191206
default: break;
192207
}
193208

194209
if (!bResult)
195-
spdlog::error("Failed to update the client quest state, quest: {:X}, stage: {}, status: {}", formId, aUpdate.Stage, aUpdate.Status);
210+
spdlog::error(__FUNCTION__ ": failed to update the client quest state gameId: {:X}, questStage: {}, questStatus: {}, questType: {}, formId: {:X}, name: {}",
211+
aUpdate.Id.LogFormat(), aUpdate.Stage, aUpdate.Status, aUpdate.ClientQuestType, formId, pQuest->fullName.value.AsAscii());
196212
}
197213

198214
bool QuestService::StopQuest(uint32_t aformId)
199215
{
200216
TESQuest* pQuest = Cast<TESQuest>(TESForm::GetById(aformId));
201217
if (pQuest)
202218
{
203-
pQuest->SetActive(false);
204-
pQuest->SetStopped();
219+
if (pQuest->getState() == TESQuest::State::Stopped) // Supress duplicate or loopback quest stop
220+
{
221+
spdlog::info(__FUNCTION__ ": suppressing duplicate quest stop formId: {:X}, questStage: {}, questFlags: {:X}, questType: {}, formId: {:X}, name: {}",
222+
aformId, pQuest->currentStage, static_cast<uint8_t>(pQuest->flags), static_cast<uint16_t>(pQuest->type), aformId, pQuest->fullName.value.AsAscii());
223+
}
224+
else
225+
{
226+
pQuest->SetActive(false);
227+
pQuest->SetStopped();
228+
}
229+
205230
return true;
206231
}
207232

Code/server/Game/Player.h

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,64 @@
11
#pragma once
22

3+
/* With the introduction of party member (not just leader) quest progression, there are a few challenges:
4+
SendToParty() will cause reflections of the same quest updates from all party members.
5+
This is because ScopedQuestOverride() doesn't work as intended because QuestServic::OnEvent calls
6+
happen later on a different thread. We need to prevent these reflections from sending out dups.
7+
8+
We want the quest progress to be sent out by the Leader. It has already happened for one Member when
9+
members advance the quest and it is theoretically possible more than one sends progress, so SendToParty
10+
needs to not send to those who already have it.
11+
12+
Since we have to track this anyway, if a Member advances quest, we forward it to the Leader for broadcast.
13+
This means Leader gets the update in QuestService::OnUpdate(), and when the Leader update reflects back, the
14+
Leader does SendToParty() (with the original Member and the Leadre in the cache, so SendToParty doesn't send
15+
to them). This enables the Leader to make a centralized decision; it can reject sending out a member update
16+
that shouldn't be forwarded; it looks like there are a couple of exceptions like that.
17+
*/
18+
struct QuestStageDedupHistory
19+
{
20+
static constexpr std::chrono::seconds timeout{30s};
21+
22+
using QuestId = GameId;
23+
using QuestStage = uint16_t;
24+
using PlayerId = uint32_t;
25+
using TimeStamp = std::chrono::time_point<std::chrono::steady_clock>;
26+
27+
struct Entry
28+
{
29+
QuestId questId{};
30+
QuestStage questStage{};
31+
PlayerId playerId{};
32+
TimeStamp timestamp{};
33+
};
34+
35+
using Container = std::deque<Entry>;
36+
using iterator = Container::iterator;
37+
using const_iterator = Container::const_iterator;
38+
39+
void Add(QuestId aQuestId, QuestStage aQuestStage, PlayerId aPlayerId,
40+
TimeStamp aTimeStamp = std::chrono::steady_clock::now());
41+
42+
const_iterator FindStage(const QuestId& aQuestId, const QuestStage& aQuestStage);
43+
const_iterator FindStageWPlayerId(const QuestId& aQuestId, const QuestStage& aQuestStage, const PlayerId& aPlayerId);
44+
45+
void Reset() noexcept { m_Cache.clear(); }
46+
bool FoundStage(const QuestId& aQuestId, const QuestStage& aQuestStage)
47+
{
48+
return FindStage(aQuestId, aQuestStage) != m_Cache.end();
49+
}
50+
bool FoundStageWPlayerId(const QuestId& aQuestId, const QuestStage& aQuestStage, const PlayerId& aPlayerId)
51+
{
52+
return FindStageWPlayerId(aQuestId, aQuestStage, aPlayerId) != m_Cache.end();
53+
}
54+
55+
private:
56+
void inline Expire();
57+
Container m_Cache; // Short, time-ordered, duplicates valid (do they ever happen?)
58+
};
59+
60+
61+
362
struct ServerMessage;
463
struct Player
564
{
@@ -40,6 +99,9 @@ struct Player
4099
void SetCellComponent(const CellIdComponent& aCellComponent) noexcept;
41100

42101
void Send(const ServerMessage& acServerMessage) const;
102+
QuestStageDedupHistory m_questStageDedupHistory;
103+
QuestStageDedupHistory& GetQuestStageDedupHistory() { return m_questStageDedupHistory; }
104+
43105

44106
private:
45107
uint32_t m_id{0};

Code/server/GameServer.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#include <Components.h>
1+
#include <Components.h>
22
#include <GameServer.h>
33
#include <Packet.hpp>
44

@@ -601,6 +601,7 @@ void GameServer::OnDisconnection(const ConnectionId_t aConnectionId, EDisconnect
601601
m_adminSessions.erase(aConnectionId);
602602

603603
auto* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(aConnectionId);
604+
pPlayer->GetQuestStageDedupHistory().Reset();
604605

605606
spdlog::info("Connection ended {:x} - '{}' disconnected", aConnectionId, (pPlayer != NULL ? pPlayer->GetUsername().c_str() : "NULL"));
606607

@@ -732,6 +733,23 @@ bool GameServer::SendToPlayersInRange(const ServerMessage& acServerMessage, cons
732733
return true;
733734
}
734735

736+
void GameServer::SendToLeader(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const Player* apLeader) const
737+
{
738+
if (!acPartyComponent.JoinedPartyId.has_value())
739+
{
740+
spdlog::warn("Party does not exist, canceling broadcast.");
741+
return;
742+
}
743+
744+
if (const_cast<Player*>(apLeader)->GetParty().JoinedPartyId != acPartyComponent.JoinedPartyId)
745+
{
746+
spdlog::warn("Specified party leader belongs to different party? Canceling send");
747+
return;
748+
}
749+
750+
apLeader->Send(acServerMessage);
751+
}
752+
735753
void GameServer::SendToParty(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const Player* apExcludeSender) const
736754
{
737755
if (!acPartyComponent.JoinedPartyId.has_value())

Code/server/GameServer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ struct GameServer final : Server
5959
void SendToLoaded(const ServerMessage& acServerMessage) const;
6060
void SendToPlayers(const ServerMessage& acServerMessage, const Player* apExcludeSender = nullptr) const;
6161
bool SendToPlayersInRange(const ServerMessage& acServerMessage, const entt::entity acOrigin, const Player* apExcludeSender = nullptr) const;
62+
void SendToLeader(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const Player* apLeader) const;
6263
void SendToParty(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const Player* apExcludeSender = nullptr) const;
6364
void SendToPartyInRange(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const entt::entity acOrigin, const Player* apExcludeSender = nullptr) const;
6465

Code/server/Services/PartyService.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ void PartyService::OnPartyCreate(const PacketEvent<PartyCreateRequest>& acPacket
130130
{
131131
party.Members.push_back(otherPlayer);
132132
otherPlayer->GetParty().JoinedPartyId = partyId;
133+
otherPlayer->GetQuestStageDedupHistory().Reset();
133134

134135
SendPartyJoinedEvent(party, otherPlayer);
135136
}

0 commit comments

Comments
 (0)