Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Code/client/Games/Animation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ static TPerformAction* RealPerformAction;
// TODO: make scoped override
thread_local bool g_forceAnimation = false;

// This is where the Actors AI is enabled/disabled: almost all of NPC AI/behavior is
// determined by Actions that are run on them.

uint8_t TP_MAKE_THISCALL(HookPerformAction, ActorMediator, TESActionData* apAction)
{
auto pActor = apAction->actor;
Expand Down
7 changes: 7 additions & 0 deletions Code/client/Games/Skyrim/AI/Movement/PlayerControls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ void PlayerControls::SetCamSwitch(bool aSet) noexcept
Data.remapMode = aSet;
}

bool PlayerControls::IsMovementControlsEnabled() noexcept
{
using TIsMovementControlsEnabled = bool();
POINTER_SKYRIMSE(TIsMovementControlsEnabled, s_isMovementControlsEnabled, 55485);
return s_isMovementControlsEnabled.Get()();
}

BSInputEnableManager* BSInputEnableManager::Get()
{
POINTER_SKYRIMSE(BSInputEnableManager*, s_instance, 400863);
Expand Down
2 changes: 2 additions & 0 deletions Code/client/Games/Skyrim/AI/Movement/PlayerControls.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ struct PlayerControls

void SetCamSwitch(bool aSet) noexcept;

static bool IsMovementControlsEnabled() noexcept;

public:
char pad0[0x20];
PlayerControlsData Data;
Expand Down
15 changes: 15 additions & 0 deletions Code/client/Games/Skyrim/Events/EventDispatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,31 @@ struct TESResolveNPCTemplatesEvent
{
};

// The RE'd fields in TESSceneEvent, TESSceneActionEvent and TESScenePhaseEvent may be incorrect

struct TESSceneEvent
{
void* ref;
uint32_t sceneFormId;
uint32_t sceneType; // BEGIN (0) or END (1)
};

struct TESSceneActionEvent
{
void* ref;
uint32_t sceneFormId;
uint32_t actionIndex;
uint32_t questFormId;
uint32_t actorAliasId;
};

struct TESScenePhaseEvent
{
uint32_t sceneFormId;
uint32_t phaseIndex;
uint32_t sceneType; // BEGIN (0) or END (1)
uint16_t questStageId;
void* callback;
};

struct TESSellEvent
Expand Down
85 changes: 63 additions & 22 deletions Code/client/Games/Skyrim/Forms/TESQuest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,9 @@ void TESQuest::SetActive(bool toggle)

bool TESQuest::IsStageDone(uint16_t stageIndex)
{
for (Stage* it : stages)
{
if (it->stageIndex == stageIndex)
return it->IsDone();
}

return false;
TP_THIS_FUNCTION(TIsStageDone, bool, TESQuest, uint16_t);
POINTER_SKYRIMSE(TIsStageDone, IsStageDone, 25011);
return IsStageDone(this, stageIndex);
}

bool TESQuest::Kill()
Expand Down Expand Up @@ -88,23 +84,51 @@ bool TESQuest::EnsureQuestStarted(bool& success, bool force)
return SetRunning(this, &success, force);
}

bool TESQuest::SetStage(uint16_t newStage)
bool TESQuest::SetStage(uint16_t stageIndex)
{
ScopedQuestOverride _;

// According to wiki, calling newStage == currentStage does nothing.
// Calling with newStage < currentStage, will rerun the stage actions
// IFF the target newStage is not marked IsCompleted(). Regardless,
// will not reduce currentStage (it stays the same).
// Actually reducing currentStage rquires reset() to be called first.
TP_THIS_FUNCTION(TSetStage, bool, TESQuest, uint16_t);
POINTER_SKYRIMSE(TSetStage, SetStage, 25004);
return SetStage(this, newStage);
bool bSuccess = SetStage(this, stageIndex);
if (!bSuccess)
{
spdlog::warn(__FUNCTION__ ": returned false quest formId {:X}, currentStage {}, newStage {}, name {}",
formID, currentStage, stageIndex, fullName.value.AsAscii());
}
return bSuccess;
}

void TESQuest::ScriptSetStage(uint16_t stageIndex)
bool TESQuest::ScriptSetStage(uint16_t stageIndex, bool bForce)
{
if (currentStage == stageIndex || IsStageDone(stageIndex))
return;
// According to wiki, calling with stageIndex == currentStage does nothing.
// Calling with stageIndex < currentStage will rerun the stageIndex actions
// IFF the target stageIndex is not marked IsCompleted(). Regardless,
// will not reduce currentStage (it stays the same).
// Actually reducing currentStage rquires reset() to be called first.
// Since this is not well-known and hooks may be confused, filter rewind
// according to TESQuest::SetStage rules.
bool bSuccess = stageIndex > currentStage
|| stageIndex != currentStage && !IsStageDone(stageIndex)
|| bForce;

if (bSuccess)
{
using Quest = TESQuest;
PAPYRUS_FUNCTION(bool, Quest, SetCurrentStageID, int);
bSuccess = s_pSetCurrentStageID(this, stageIndex);
}

using Quest = TESQuest;
PAPYRUS_FUNCTION(void, Quest, SetCurrentStageID, int);
s_pSetCurrentStageID(this, stageIndex);
if (!bSuccess)
{
spdlog::warn(__FUNCTION__ ": returned false quest formId {:X}, currentStage {}, newStage {}, name {}",
formID, currentStage, stageIndex, fullName.value.AsAscii());
}

return bSuccess;
}

void TESQuest::SetStopped()
Expand All @@ -113,9 +137,26 @@ void TESQuest::SetStopped()
MarkChanged(2);
}

static TiltedPhoques::Initializer s_questInitHooks(
[]()
bool TESQuest::IsAnyCutscenePlaying()
{
for (const auto& scene : scenes)
{
// kill quest init in cold blood
// TiltedPhoques::Write<uint8_t>(25003, 0xC3);
});
if (scene->isPlaying)
return true;
}
return false;
}

void BGSScene::ScriptForceStart()
{
using Scene = BGSScene;
PAPYRUS_FUNCTION(void, Scene, ForceStart);
s_pForceStart(this);
spdlog::info(__FUNCTION__ ": force started scene questId {:X}, sceneId: {:X}, isPlaying? {}", owningQuest->formID, formID, isPlaying);
}

static TiltedPhoques::Initializer s_questInitHooks([]() {
// kill quest init in cold blood
// TiltedPhoques::Write<uint8_t>(25003, 0xC3);
});

47 changes: 39 additions & 8 deletions Code/client/Games/Skyrim/Forms/TESQuest.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,40 @@
#include <Components/TESFullName.h>
#include <Forms/BGSStoryManagerTree.h>

struct BGSSceneAction
{
virtual ~BGSSceneAction();

uint32_t actorID;
uint16_t startPhase;
uint16_t endPhase;
uint32_t flags;
uint8_t status;

void Start()
{
this->status |= 1u;
}
};

static_assert(offsetof(BGSSceneAction, flags) == 0x10);
struct BGSScene : TESForm
{
GameArray<void*> phases;
GameArray<uint32_t> actorIds;
GameArray<uint32_t> actorFlags;
GameArray<uint32_t> actorProgressionFlags;
GameArray<BGSSceneAction*> actions;
TESQuest* owningQuest;
uint32_t flags;
uint32_t padA4;
TESCondition conditions;
bool isPlaying;

void ScriptForceStart();
};
static_assert(offsetof(BGSScene, owningQuest) == 0x98);
static_assert(offsetof(BGSScene, isPlaying) == 0xB0);

struct TESQuest : BGSStoryManagerTreeForm
{
Expand Down Expand Up @@ -76,6 +105,10 @@ struct TESQuest : BGSStoryManagerTreeForm
uint16_t stageIndex;
uint8_t flags;

operator bool() const
{
return *reinterpret_cast<const std::uintptr_t*>(this) != 0;
}
inline bool IsDone() { return flags & 1; }
};

Expand All @@ -90,11 +123,8 @@ struct TESQuest : BGSStoryManagerTreeForm
Type type; // 0x00DF
int32_t scopedStatus; // 0x00E0 default init: -1, if not -1 outside of story manager scope
uint32_t padE4;
GameList<Stage> stages;
/*
GameList<Stage>* pExecutedStages; // 0x00E8
GameList<Stage>* pWaitingStages; // 0x00F0
*/
GameValueList<Stage>* pExecutedStages; // 0x00E8
GameValueList<Stage*>* pWaitingStages; // 0x00F0
GameList<Objective> objectives; // 0x00F8
char pad108[0x100]; // 0x0108
GameArray<BGSScene*> scenes; // 0x0208
Expand Down Expand Up @@ -126,15 +156,16 @@ struct TESQuest : BGSStoryManagerTreeForm

bool EnsureQuestStarted(bool& succeded, bool force);

bool SetStage(uint16_t stage);
void ScriptSetStage(uint16_t stage);
bool SetStage(uint16_t stageIndex);
bool ScriptSetStage(uint16_t stage, bool bForce = false);
void SetStopped();
bool IsAnyCutscenePlaying();
};

static_assert(sizeof(TESQuest) == 0x268);
static_assert(offsetof(TESQuest, fullName) == 0x28);
static_assert(offsetof(TESQuest, flags) == 0xDC);
static_assert(offsetof(TESQuest, stages) == 0xE8);
static_assert(offsetof(TESQuest, pExecutedStages) == 0xE8);
static_assert(offsetof(TESQuest, objectives) == 0xF8);
static_assert(offsetof(TESQuest, currentStage) == 0x228);
static_assert(offsetof(TESQuest, unkFlags) == 0x248);
2 changes: 1 addition & 1 deletion Code/client/Games/Skyrim/Magic/MagicTarget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ bool MagicTarget::AddTargetData::IsForbiddenEffect(Actor* apTarget)
if (apTarget != PlayerCharacter::Get())
return false;

return pEffectItem->IsNightVisionEffect();
return pEffectItem->IsNightVisionEffect() || pEffectItem->IsSlowEffect();
}

Actor* MagicTarget::GetTargetAsActor()
Expand Down
41 changes: 38 additions & 3 deletions Code/client/Services/Debug/Views/QuestDebugView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ void DebugService::DrawQuestDebugView()

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

char setStage[64];
sprintf_s(setStage, std::size(setStage), "Set stage (%d)", pStage->stageIndex);
Expand All @@ -64,6 +66,38 @@ void DebugService::DrawQuestDebugView()
pQuest->ScriptSetStage(pStage->stageIndex);
}
}
if (ImGui::CollapsingHeader("Waiting Stages"))
{
for (auto& pStage : *pQuest->pWaitingStages)
{
ImGui::TextColored({0.f, 255.f, 255.f, 255.f}, "Stage: %d, is done? %s", pStage->stageIndex,
pStage->IsDone() ? "true" : "false");

char setStage[64];
sprintf_s(setStage, std::size(setStage), "Set stage (%d)", pStage->stageIndex);

if (ImGui::Button(setStage))
pQuest->ScriptSetStage(pStage->stageIndex);
}
}

if (ImGui::CollapsingHeader("Scenes"))
{
for (auto& pScene : pQuest->scenes)
{
ImGui::TextColored({0.f, 255.f, 255.f, 255.f}, "Scene Form ID: %x, is playing? %s", pScene->formID,
pScene->isPlaying ? "true" : "false");

ImGui::Text("Scene actions:");
for (int i = 0; i < pScene->actions.length; ++i)
{
char startAction[64];
sprintf_s(startAction, std::size(startAction), "Start action %d", i);
if (ImGui::Button(startAction))
pScene->actions[i]->Start();
}
}
}

if (ImGui::CollapsingHeader("Actors"))
{
Expand All @@ -90,7 +124,8 @@ void DebugService::DrawQuestDebugView()
if (ImGui::Button("Teleport to me"))
{
auto* pPlayer = PlayerCharacter::Get();
m_world.GetRunner().Trigger(MoveActorEvent(pActor->formID, pPlayer->parentCell->formID, pPlayer->position));
m_world.GetRunner().Trigger(
MoveActorEvent(pActor->formID, pPlayer->parentCell->formID, pPlayer->position));
}
ImGui::PopID();
}
Expand Down
7 changes: 6 additions & 1 deletion Code/client/Services/Generic/MagicService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,12 @@ void MagicService::OnNotifyAddTarget(const NotifyAddTarget& acMessage) noexcept

// This hack is here because slow time seems to be twice as slow when cast by an npc
if (pEffect->IsSlowEffect())
pActor = PlayerCharacter::Get();
{
spdlog::warn(__FUNCTION__ ": hacking IsSlowEffect() targetId {:X}, casterId {:X}, magnitued {}, IsDualCasting {}",
acMessage.TargetId, acMessage.CasterId, acMessage.Magnitude, acMessage.IsDualCasting);
acMessage.CasterId && (pCaster = PlayerCharacter::Get());
}


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