Skip to content

Commit daa345a

Browse files
committed
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. Partial support for Remote actors to run AI packages when in-Scene
1 parent 2191f82 commit daa345a

28 files changed

+1142
-200
lines changed

Code/client/Games/Animation.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <Forms/BGSAction.h>
66
#include <Forms/TESIdleForm.h>
7+
#include <Forms/TESQuest.h>
78

89
#include <Structs/ActionEvent.h>
910

@@ -20,13 +21,21 @@ static TPerformAction* RealPerformAction;
2021
// TODO: make scoped override
2122
thread_local bool g_forceAnimation = false;
2223

24+
// This is where the Actors AI is enabled/disabled: almost all of NPC AI/behavior is
25+
// determined by Actions that are run on them.
26+
2327
uint8_t TP_MAKE_THISCALL(HookPerformAction, ActorMediator, TESActionData* apAction)
2428
{
2529
auto pActor = apAction->actor;
2630
const auto pExtension = pActor->GetExtension();
31+
const auto pScene = pActor->GetCurrentScene();
32+
const bool isPlaying = pScene && pScene->isPlaying;
2733

28-
if (!pExtension->IsRemote() || g_forceAnimation)
34+
if (pExtension->IsLocal() || isPlaying || g_forceAnimation)
2935
{
36+
if (pExtension->IsRemote())
37+
spdlog::warn(__FUNCTION__ ": performing actions for remote Actor formId {:X}, name {}", pActor->formID, pActor->baseForm->GetName());
38+
3039
ActionEvent action;
3140
action.State1 = pActor->actorState.flags1;
3241
action.State2 = pActor->actorState.flags2;

Code/client/Games/Skyrim/AI/Movement/PlayerControls.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ void PlayerControls::SetCamSwitch(bool aSet) noexcept
1919
Data.remapMode = aSet;
2020
}
2121

22+
bool PlayerControls::IsMovementControlsEnabled() noexcept
23+
{
24+
using TIsMovementControlsEnabled = bool();
25+
POINTER_SKYRIMSE(TIsMovementControlsEnabled, s_isMovementControlsEnabled, 55485);
26+
return s_isMovementControlsEnabled.Get()();
27+
}
28+
2229
BSInputEnableManager* BSInputEnableManager::Get()
2330
{
2431
POINTER_SKYRIMSE(BSInputEnableManager*, s_instance, 400863);

Code/client/Games/Skyrim/AI/Movement/PlayerControls.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ struct PlayerControls
4242

4343
void SetCamSwitch(bool aSet) noexcept;
4444

45+
static bool IsMovementControlsEnabled() noexcept;
46+
4547
public:
4648
char pad0[0x20];
4749
PlayerControlsData Data;

Code/client/Games/Skyrim/Actor.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,11 +1267,16 @@ void Actor::SpeakSound(const char* pFile)
12671267

12681268
char TP_MAKE_THISCALL(HookActorProcess, Actor, float a2)
12691269
{
1270-
// Don't process AI if we own the actor
1270+
// Don't process AI unless we own the actor, or they are playing Scene packages
1271+
const auto pScene = apThis->GetCurrentScene();
1272+
const auto isPlaying = pScene && pScene->isPlaying;
12711273

1272-
if (apThis->GetExtension()->IsRemote())
1274+
if (apThis->GetExtension()->IsRemote() && !isPlaying)
12731275
return 0;
12741276

1277+
if (apThis->GetExtension()->IsRemote() && isPlaying)
1278+
spdlog::warn(__FUNCTION__ ": enabling for remote Actor in Scene formId {:X}, name {}", apThis->formID, apThis->baseForm->GetName());
1279+
12751280
return TiltedPhoques::ThisCall(RealActorProcess, apThis, a2);
12761281
}
12771282

Code/client/Games/Skyrim/Events/EventDispatcher.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,31 @@ struct TESResolveNPCTemplatesEvent
188188
{
189189
};
190190

191+
// The RE'd fields in TESSceneEvent, TESSceneActionEvent and TESScenePhaseEvent may be incorrect
192+
191193
struct TESSceneEvent
192194
{
195+
void* ref;
196+
uint32_t sceneFormId;
197+
uint32_t sceneType; // BEGIN (0) or END (1)
193198
};
194199

195200
struct TESSceneActionEvent
196201
{
202+
void* ref;
203+
uint32_t sceneFormId;
204+
uint32_t actionIndex;
205+
uint32_t questFormId;
206+
uint32_t actorAliasId;
197207
};
198208

199209
struct TESScenePhaseEvent
200210
{
211+
void* callback;
212+
uint32_t sceneFormId;
213+
uint16_t phaseIndex;
214+
uint32_t sceneType; // BEGIN (0) or END (1)
215+
uint32_t questStageId;
201216
};
202217

203218
struct TESSellEvent

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

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,9 @@ void TESQuest::SetActive(bool toggle)
5454

5555
bool TESQuest::IsStageDone(uint16_t stageIndex)
5656
{
57-
for (Stage* it : stages)
58-
{
59-
if (it->stageIndex == stageIndex)
60-
return it->IsDone();
61-
}
62-
63-
return false;
57+
TP_THIS_FUNCTION(TIsStageDone, bool, TESQuest, uint16_t);
58+
POINTER_SKYRIMSE(TIsStageDone, IsStageDone, 25011);
59+
return IsStageDone(this, stageIndex);
6460
}
6561

6662
bool TESQuest::Kill()
@@ -88,34 +84,112 @@ bool TESQuest::EnsureQuestStarted(bool& success, bool force)
8884
return SetRunning(this, &success, force);
8985
}
9086

91-
bool TESQuest::SetStage(uint16_t newStage)
87+
bool TESQuest::SetStage(uint16_t stageIndex)
9288
{
93-
ScopedQuestOverride _;
94-
89+
// According to wiki, calling newStage == currentStage does nothing.
90+
// Calling with newStage < currentStage, will rerun the stage actions
91+
// IFF the target newStage is not marked IsCompleted(). Regardless,
92+
// will not reduce currentStage (it stays the same).
93+
// Actually reducing currentStage rquires reset() to be called first.
9594
TP_THIS_FUNCTION(TSetStage, bool, TESQuest, uint16_t);
9695
POINTER_SKYRIMSE(TSetStage, SetStage, 25004);
97-
return SetStage(this, newStage);
96+
bool bSuccess = SetStage(this, stageIndex);
97+
if (!bSuccess)
98+
{
99+
spdlog::warn(__FUNCTION__ ": returned false quest formId {:X}, currentStage {}, newStage {}, name {}",
100+
formID, currentStage, stageIndex, fullName.value.AsAscii());
101+
}
102+
return bSuccess;
98103
}
99104

100-
void TESQuest::ScriptSetStage(uint16_t stageIndex)
105+
bool TESQuest::ScriptSetStage(uint16_t stageIndex, bool bForce)
101106
{
102-
if (currentStage == stageIndex || IsStageDone(stageIndex))
103-
return;
107+
// According to wiki, calling with stageIndex == currentStage does nothing.
108+
// Calling with stageIndex < currentStage will rerun the stageIndex actions
109+
// IFF the target stageIndex is not marked IsCompleted(). Regardless,
110+
// will not reduce currentStage (it stays the same).
111+
// Actually reducing currentStage rquires reset() to be called first.
112+
// Since this is not well-known and hooks may be confused, filter rewind
113+
// according to TESQuest::SetStage rules.
114+
bool bSuccess = stageIndex > currentStage || stageIndex != currentStage && !IsStageDone(stageIndex) || bForce;
115+
116+
if (bSuccess)
117+
{
118+
using Quest = TESQuest;
119+
PAPYRUS_FUNCTION(bool, Quest, SetCurrentStageID, int);
120+
bSuccess = s_pSetCurrentStageID(this, stageIndex);
121+
}
122+
123+
if (!bSuccess)
124+
{
125+
spdlog::warn(__FUNCTION__ ": returned false quest formId {:X}, currentStage {}, newStage {}, name {}", formID, currentStage, stageIndex, fullName.value.AsAscii());
126+
}
104127

128+
return bSuccess;
129+
}
130+
131+
void TESQuest::ScriptReset()
132+
{
105133
using Quest = TESQuest;
106-
PAPYRUS_FUNCTION(void, Quest, SetCurrentStageID, int);
107-
s_pSetCurrentStageID(this, stageIndex);
134+
PAPYRUS_FUNCTION(void, Quest, Reset);
135+
s_pReset(this);
108136
}
109137

138+
void TESQuest::ScriptResetAndUpdate()
139+
{
140+
ScriptReset();
141+
142+
if (!IsEnabled())
143+
{
144+
if (!IsStopped())
145+
SetStopped();
146+
}
147+
148+
else
149+
{
150+
bool isStarted;
151+
if (flags & StartsEnabled && !EnsureQuestStarted(isStarted, false))
152+
spdlog::warn(__FUNCTION__ ": EnsureQuestStarted failed questId {:X}, is Started {}", formID, isStarted);
153+
}
154+
}
155+
156+
110157
void TESQuest::SetStopped()
111158
{
112159
flags &= 0xFFFE;
113160
MarkChanged(2);
114161
}
115162

163+
bool TESQuest::IsAnyCutscenePlaying()
164+
{
165+
for (const auto& scene : scenes)
166+
{
167+
if (scene->isPlaying)
168+
return true;
169+
}
170+
return false;
171+
}
172+
173+
void BGSScene::ScriptForceStart()
174+
{
175+
spdlog::info(__FUNCTION__ ": force starting scene questId {:X}, sceneId: {:X}, isPlaying? {}", owningQuest->formID, formID, isPlaying);
176+
using Scene = BGSScene;
177+
PAPYRUS_FUNCTION(void, Scene, ForceStart);
178+
s_pForceStart(this);
179+
}
180+
181+
void BGSScene::ScriptStop()
182+
{
183+
spdlog::info(__FUNCTION__ ": stopping scene questId {:X}, sceneId: {:X}, isPlaying? {}", owningQuest->formID, formID, isPlaying);
184+
using Scene = BGSScene;
185+
PAPYRUS_FUNCTION(void, Scene, Stop);
186+
s_pStop(this);
187+
}
188+
116189
static TiltedPhoques::Initializer s_questInitHooks(
117-
[]()
190+
[]()
118191
{
119-
// kill quest init in cold blood
120-
// TiltedPhoques::Write<uint8_t>(25003, 0xC3);
192+
// kill quest init in cold blood
193+
// TiltedPhoques::Write<uint8_t>(25003, 0xC3);
121194
});
195+

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

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@
55
#include <Components/TESFullName.h>
66
#include <Forms/BGSStoryManagerTree.h>
77

8+
struct BGSSceneAction
9+
{
10+
virtual ~BGSSceneAction();
11+
12+
uint32_t actorID;
13+
uint16_t startPhase;
14+
uint16_t endPhase;
15+
uint32_t flags;
16+
uint8_t status;
17+
18+
void Start()
19+
{
20+
this->status |= 1u;
21+
}
22+
};
23+
24+
static_assert(offsetof(BGSSceneAction, flags) == 0x10);
825
struct BGSScene : TESForm
926
{
1027
GameArray<void*> phases;
@@ -89,6 +106,10 @@ struct TESQuest : BGSStoryManagerTreeForm
89106
uint16_t stageIndex;
90107
uint8_t flags;
91108

109+
operator bool() const
110+
{
111+
return *reinterpret_cast<const std::uintptr_t*>(this) != 0;
112+
}
92113
inline bool IsDone() { return flags & 1; }
93114
};
94115

@@ -103,11 +124,8 @@ struct TESQuest : BGSStoryManagerTreeForm
103124
Type type; // 0x00DF
104125
int32_t scopedStatus; // 0x00E0 default init: -1, if not -1 outside of story manager scope
105126
uint32_t padE4;
106-
GameList<Stage> stages;
107-
/*
108-
GameList<Stage>* pExecutedStages; // 0x00E8
109-
GameList<Stage>* pWaitingStages; // 0x00F0
110-
*/
127+
GameValueList<Stage>* pExecutedStages; // 0x00E8
128+
GameValueList<Stage*>* pWaitingStages; // 0x00F0
111129
GameList<Objective> objectives; // 0x00F8
112130
char pad108[0x100]; // 0x0108
113131
GameArray<BGSScene*> scenes; // 0x0208
@@ -139,15 +157,18 @@ struct TESQuest : BGSStoryManagerTreeForm
139157

140158
bool EnsureQuestStarted(bool& succeded, bool force);
141159

142-
bool SetStage(uint16_t stage);
143-
void ScriptSetStage(uint16_t stage);
160+
bool SetStage(uint16_t stageIndex);
161+
bool ScriptSetStage(uint16_t stage, bool bForce = false);
162+
void ScriptReset();
163+
void ScriptResetAndUpdate();
144164
void SetStopped();
165+
bool IsAnyCutscenePlaying();
145166
};
146167

147168
static_assert(sizeof(TESQuest) == 0x268);
148169
static_assert(offsetof(TESQuest, fullName) == 0x28);
149170
static_assert(offsetof(TESQuest, flags) == 0xDC);
150-
static_assert(offsetof(TESQuest, stages) == 0xE8);
171+
static_assert(offsetof(TESQuest, pExecutedStages) == 0xE8);
151172
static_assert(offsetof(TESQuest, objectives) == 0xF8);
152173
static_assert(offsetof(TESQuest, currentStage) == 0x228);
153174
static_assert(offsetof(TESQuest, unkFlags) == 0x248);

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ void DebugService::DrawQuestDebugView()
5353

5454
if (ImGui::CollapsingHeader("Stages"))
5555
{
56-
for (auto* pStage : pQuest->stages)
56+
for (auto& stage : *pQuest->pExecutedStages)
5757
{
58-
ImGui::TextColored({0.f, 255.f, 255.f, 255.f}, "Stage: %d, is done? %s", pStage->stageIndex, pStage->IsDone() ? "true" : "false");
58+
auto pStage = &stage;
59+
ImGui::TextColored({0.f, 255.f, 255.f, 255.f}, "Stage: %d, is done? %s", pStage->stageIndex,
60+
pStage->IsDone() ? "true" : "false");
5961

6062
char setStage[64];
6163
sprintf_s(setStage, std::size(setStage), "Set stage (%d)", pStage->stageIndex);
@@ -64,6 +66,38 @@ void DebugService::DrawQuestDebugView()
6466
pQuest->ScriptSetStage(pStage->stageIndex);
6567
}
6668
}
69+
if (ImGui::CollapsingHeader("Waiting Stages"))
70+
{
71+
for (auto& pStage : *pQuest->pWaitingStages)
72+
{
73+
ImGui::TextColored({0.f, 255.f, 255.f, 255.f}, "Stage: %d, is done? %s", pStage->stageIndex,
74+
pStage->IsDone() ? "true" : "false");
75+
76+
char setStage[64];
77+
sprintf_s(setStage, std::size(setStage), "Set stage (%d)", pStage->stageIndex);
78+
79+
if (ImGui::Button(setStage))
80+
pQuest->ScriptSetStage(pStage->stageIndex);
81+
}
82+
}
83+
84+
if (ImGui::CollapsingHeader("Scenes"))
85+
{
86+
for (auto& pScene : pQuest->scenes)
87+
{
88+
ImGui::TextColored({0.f, 255.f, 255.f, 255.f}, "Scene Form ID: %x, is playing? %s", pScene->formID,
89+
pScene->isPlaying ? "true" : "false");
90+
91+
ImGui::Text("Scene actions:");
92+
for (int i = 0; i < pScene->actions.length; ++i)
93+
{
94+
char startAction[64];
95+
sprintf_s(startAction, std::size(startAction), "Start action %d", i);
96+
if (ImGui::Button(startAction))
97+
pScene->actions[i]->Start();
98+
}
99+
}
100+
}
67101

68102
if (ImGui::CollapsingHeader("Actors"))
69103
{

Code/client/Services/Generic/MagicService.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,13 @@ void MagicService::OnNotifyAddTarget(const NotifyAddTarget& acMessage) noexcept
425425

426426
// This hack is here because slow time seems to be twice as slow when cast by an npc
427427
if (pEffect->IsSlowEffect())
428-
pActor = PlayerCharacter::Get();
428+
{
429+
spdlog::debug(
430+
__FUNCTION__ ": hacking IsSlowEffect() targetId {:X}, casterId {:X}, magnitued {}, IsDualCasting {}",
431+
acMessage.TargetId, acMessage.CasterId, acMessage.Magnitude, acMessage.IsDualCasting);
432+
acMessage.CasterId && (pCaster = PlayerCharacter::Get());
433+
}
434+
429435

430436
pActor->magicTarget.AddTarget(data, acMessage.ApplyHealPerkBonus, acMessage.ApplyStaminaPerkBonus);
431437
spdlog::debug("Applied remote magic effect");

0 commit comments

Comments
 (0)