Skip to content
Open
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
4 changes: 3 additions & 1 deletion Code/client/Events/DialogueEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
*/
struct DialogueEvent
{
DialogueEvent(uint32_t aActorID, String aVoiceFile, uint32_t aSceneID)
DialogueEvent(uint32_t aActorID, String aVoiceFile, uint32_t aSceneID, uint32_t aScenePhase)
: ActorID(aActorID)
, VoiceFile(aVoiceFile)
, SceneID(aSceneID)
, ScenePhase(aScenePhase)
{
}

uint32_t ActorID;
String VoiceFile;
uint32_t SceneID; // Scene the line is spoken in, 0 for player dialogue menu lines and lines outside scenes
uint32_t ScenePhase; // The scene's current phase when the line started
};
1 change: 1 addition & 0 deletions Code/client/Events/SubtitleEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ struct SubtitleEvent
// This is only relevant to fallout 4
uint32_t TopicFormID{};
uint32_t SceneID{}; // Scene the line is spoken in, 0 for player dialogue menu lines and lines outside scenes
uint32_t ScenePhase{}; // The scene's current phase when the line started
};
6 changes: 5 additions & 1 deletion Code/client/Games/Misc/SubtitleManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ void TP_MAKE_THISCALL(HookShowSubtitle, SubtitleManager, TESObjectREFR* apSpeake
BGSScene* pScene = isPlayerDialogue ? nullptr : pActor->GetCurrentScene();

SubtitleEvent event(apSpeaker->formID, apSubtitleText);
event.SceneID = pScene && pScene->isPlaying ? pScene->formID : 0;
if (pScene && pScene->isPlaying)
{
event.SceneID = pScene->formID;
event.ScenePhase = pScene->currentPhaseIndex;
}
World::Get().GetRunner().Trigger(event);
}

Expand Down
13 changes: 10 additions & 3 deletions Code/client/Games/Skyrim/Actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1221,12 +1221,19 @@ float TP_MAKE_THISCALL(HookSpeakSoundFunction, Actor, const char* apName, uint32
const bool cIsPlayerDialogue = MenuTopicManager::IsPlayerDialogueSpeaker(apThis);
const bool cShouldSync = apThis->GetExtension()->IsLocal() || cIsPlayerDialogue;

// Lines from a scene let other clients that play the same scene skip them. Menu dialogue always plays.
// Lines from a scene let other clients that play the same scene skip them, unless their copy is stuck in an earlier
// phase. Menu dialogue always plays.
BGSScene* pScene = cIsPlayerDialogue ? nullptr : apThis->GetCurrentScene();
const uint32_t cSceneId = pScene && pScene->isPlaying ? pScene->formID : 0;
uint32_t sceneId = 0;
uint32_t scenePhase = 0;
if (pScene && pScene->isPlaying)
{
sceneId = pScene->formID;
scenePhase = pScene->currentPhaseIndex;
}

if (cShouldSync)
World::Get().GetRunner().Trigger(DialogueEvent(apThis->formID, apName, cSceneId));
World::Get().GetRunner().Trigger(DialogueEvent(apThis->formID, apName, sceneId, scenePhase));

return TiltedPhoques::ThisCall(RealSpeakSoundFunction, apThis, apName, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14);
}
Expand Down
11 changes: 10 additions & 1 deletion Code/client/Games/Skyrim/Forms/TESQuest.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,22 @@ struct BGSSceneAction
virtual Type GetType() const;

[[nodiscard]] uint32_t GetIndex() const noexcept { return index & 0x3FFFFFFF; }
[[nodiscard]] bool IsRunning() const noexcept { return status & 1; }
// What the IsSceneActionComplete condition checks (BGSScene::IsActionComplete, 0x1403A18F0 in 1.6.1170)
[[nodiscard]] bool IsComplete() const noexcept { return status & 2; }
[[nodiscard]] bool IsInPhase(uint32_t aPhaseIndex) const noexcept { return aPhaseIndex >= startPhase && aPhaseIndex <= endPhase; }
// Whether the phase waits for this action to complete before it ends. Actions that loop or continue into the next
// phase don't hold it back.
[[nodiscard]] bool BlocksPhase(uint32_t aPhaseIndex) const noexcept
{
return IsInPhase(aPhaseIndex) && !IsInPhase(aPhaseIndex + 1) && IsRunning() && !IsComplete() && !Loops();
}

uint32_t actorId; // 0x08 ALID, quest alias ID
uint16_t startPhase;
uint16_t endPhase;
uint32_t flags;
uint8_t status; // 0x14, bit 1 = complete
uint8_t status; // 0x14, bit 0 = running, bit 1 = complete
uint8_t unk15;
uint16_t unk16;
uint32_t index; // 0x18 INAM in the low 30 bits, the index scene action events carry
Expand All @@ -53,6 +60,8 @@ struct BGSScenePhase
// Once the phase has any, it ends as soon as they pass, even with actions still running
TESCondition completionConditions;
bool isRunning;

[[nodiscard]] bool HasCompletionConditions() const noexcept { return completionConditions.pHead != nullptr; }
};

static_assert(offsetof(BGSScenePhase, completionConditions) == 0x8);
Expand Down
14 changes: 11 additions & 3 deletions Code/client/Games/Skyrim/Projectiles/Projectile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,20 @@ BSPointerHandle<Projectile>* TP_MAKE_THISCALL(HookLaunch, BSPointerHandle<Projec

auto result = TiltedPhoques::ThisCall(RealLaunch, apThis, arData);

TP_ASSERT(result, "No projectile handle returned.");
if (!result)
{
spdlog::warn("No projectile handle returned, not syncing the launch");
return result;
}

// There is nothing to sync when the returned handle doesn't resolve to a projectile.
TESObjectREFR* pObject = TESObjectREFR::GetByHandle(result->handle.iBits);
Projectile* pProjectile = Cast<Projectile>(pObject);

TP_ASSERT(pProjectile, "No projectile found.");
if (!pProjectile)
{
spdlog::warn("No projectile found for handle {:X}, not syncing the launch", result->handle.iBits);
return result;
}

Event.Power = pProjectile->fPower;

Expand Down
60 changes: 49 additions & 11 deletions Code/client/Services/Generic/CharacterService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,45 @@ void CharacterService::OnNotifySyncExperience(const NotifySyncExperience& acMess
pPlayer->AddSkillExperience(PlayerCharacter::LastUsedCombatSkill, acMessage.Experience);
}

// Whether the phase waits on a package of an NPC remote here. Packages only complete on the client running the NPC's AI,
// so this copy of the scene can't get past the phase on its own.
static bool IsPhaseStuckOnRemotePackage(BGSScene* apScene, uint32_t aPhaseIndex) noexcept
{
// A phase with completion conditions ends once they pass, even mid-action, so this copy catches up within milliseconds.
const BGSScenePhase* pPhase = apScene->phases[aPhaseIndex];
if (!pPhase || pPhase->HasCompletionConditions())
return false;

for (BGSSceneAction* pAction : apScene->actions)
{
if (!pAction || pAction->GetType() != BGSSceneAction::Type::kPackage || !pAction->BlocksPhase(aPhaseIndex))
continue;

Actor* pActor = Cast<Actor>(apScene->owningQuest ? apScene->owningQuest->GetAliasedRef(pAction->actorId) : nullptr);
ActorExtension* pExtension = pActor ? pActor->GetExtension() : nullptr;
if (pExtension && pExtension->IsRemote())
return true;
}

return false;
}

// Whether the local copy of the line's scene voices it itself or already did, so the sender's would cut it off and end the
// phase early. A copy stuck in an earlier phase on a remote NPC's package never reaches the line, so it plays the sender's.
static bool IsLineVoicedByLocalScene(Actor* apActor, const GameId& acSceneId, uint32_t aScenePhase) noexcept
{
BGSScene* pScene = apActor->GetCurrentScene();
if (!acSceneId || !pScene || !pScene->isPlaying || pScene->formID != World::Get().GetModSystem().GetGameId(acSceneId))
return false;

// At or past the sender's phase. A phase index past the end means the scene is starting or ending here.
const uint32_t cPhase = pScene->currentPhaseIndex;
if (cPhase >= aScenePhase || cPhase >= pScene->phases.length)
return true;

return !IsPhaseStuckOnRemotePackage(pScene, cPhase);
}

void CharacterService::OnDialogueEvent(const DialogueEvent& acEvent) noexcept
{
if (!m_transport.IsConnected())
Expand All @@ -1132,8 +1171,8 @@ void CharacterService::OnDialogueEvent(const DialogueEvent& acEvent) noexcept
DialogueRequest request{};
request.ServerId = serverIdRes.value();
request.SoundFilename = acEvent.VoiceFile;
if (acEvent.SceneID)
m_world.GetModSystem().GetServerModId(acEvent.SceneID, request.SceneId);
if (acEvent.SceneID && m_world.GetModSystem().GetServerModId(acEvent.SceneID, request.SceneId))
request.ScenePhase = acEvent.ScenePhase;

m_transport.Send(request);
}
Expand All @@ -1145,12 +1184,12 @@ void CharacterService::OnNotifyDialogue(const NotifyDialogue& acMessage) noexcep
if (!pActor)
return;

// Every client plays scenes itself, so if the line's scene plays here too, the local copy already voices it. Playing
// the sender's line would cut that off and end the scene phase early. Menu and other-scene lines still play.
BGSScene* pScene = pActor->GetCurrentScene();
if (acMessage.SceneId && pScene && pScene->isPlaying && pScene->formID == m_world.GetModSystem().GetGameId(acMessage.SceneId))
// Every client plays scenes itself, the local copy usually voices the line. Menu and other-scene lines still play.
if (IsLineVoicedByLocalScene(pActor, acMessage.SceneId, acMessage.ScenePhase))
{
spdlog::info("Skipping remote dialogue for actor {:X}, it plays the same scene {:X} here, file: {}", pActor->formID, pScene->formID, acMessage.SoundFilename);
spdlog::info(
"Skipping remote dialogue for actor {:X}, sender's phase {}, it plays the same scene {:X} here, file: {}", pActor->formID, acMessage.ScenePhase,
pActor->GetCurrentScene()->formID, acMessage.SoundFilename);
return;
}

Expand Down Expand Up @@ -1180,8 +1219,8 @@ void CharacterService::OnSubtitleEvent(const SubtitleEvent& acEvent) noexcept
request.ServerId = serverIdRes.value();
request.Text = acEvent.Text;
request.TopicFormId = acEvent.TopicFormID;
if (acEvent.SceneID)
m_world.GetModSystem().GetServerModId(acEvent.SceneID, request.SceneId);
if (acEvent.SceneID && m_world.GetModSystem().GetServerModId(acEvent.SceneID, request.SceneId))
request.ScenePhase = acEvent.ScenePhase;

m_transport.Send(request);
}
Expand All @@ -1194,8 +1233,7 @@ void CharacterService::OnNotifySubtitle(const NotifySubtitle& acMessage) noexcep

// The local copy of the scene shows its own subtitle, see OnNotifyDialogue. Only the actor's own voice line ending
// hides a subtitle, so a remote one arriving after the local line has finished would stay on screen.
BGSScene* pScene = pActor->GetCurrentScene();
if (acMessage.SceneId && pScene && pScene->isPlaying && pScene->formID == m_world.GetModSystem().GetGameId(acMessage.SceneId))
if (IsLineVoicedByLocalScene(pActor, acMessage.SceneId, acMessage.ScenePhase))
return;

// This is only for fallout 4
Expand Down
2 changes: 2 additions & 0 deletions Code/encoding/Messages/DialogueRequest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ void DialogueRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const
Serialization::WriteVarInt(aWriter, ServerId);
Serialization::WriteString(aWriter, SoundFilename);
SceneId.Serialize(aWriter);
Serialization::WriteVarInt(aWriter, ScenePhase);
}

void DialogueRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept
Expand All @@ -14,4 +15,5 @@ void DialogueRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noe
ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF;
SoundFilename = Serialization::ReadString(aReader);
SceneId.Deserialize(aReader);
ScenePhase = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF;
}
4 changes: 3 additions & 1 deletion Code/encoding/Messages/DialogueRequest.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ struct DialogueRequest final : ClientMessage
void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override;
void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override;

bool operator==(const DialogueRequest& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && SoundFilename == acRhs.SoundFilename && SceneId == acRhs.SceneId; }
bool operator==(const DialogueRequest& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && SoundFilename == acRhs.SoundFilename && SceneId == acRhs.SceneId && ScenePhase == acRhs.ScenePhase; }

uint32_t ServerId{};
TiltedPhoques::String SoundFilename{};
// The scene the line was spoken in on the sender, if not in the player dialogue menu
GameId SceneId{};
// The sender's current phase of that scene when the line started
uint32_t ScenePhase{};
};
2 changes: 2 additions & 0 deletions Code/encoding/Messages/NotifyDialogue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ void NotifyDialogue::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const
Serialization::WriteVarInt(aWriter, ServerId);
Serialization::WriteString(aWriter, SoundFilename);
SceneId.Serialize(aWriter);
Serialization::WriteVarInt(aWriter, ScenePhase);
}

void NotifyDialogue::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept
Expand All @@ -14,4 +15,5 @@ void NotifyDialogue::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noex
ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF;
SoundFilename = Serialization::ReadString(aReader);
SceneId.Deserialize(aReader);
ScenePhase = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF;
}
4 changes: 3 additions & 1 deletion Code/encoding/Messages/NotifyDialogue.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ struct NotifyDialogue final : ServerMessage
void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override;
void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override;

bool operator==(const NotifyDialogue& achRhs) const noexcept { return GetOpcode() == achRhs.GetOpcode() && ServerId == achRhs.ServerId && SoundFilename == achRhs.SoundFilename && SceneId == achRhs.SceneId; }
bool operator==(const NotifyDialogue& achRhs) const noexcept { return GetOpcode() == achRhs.GetOpcode() && ServerId == achRhs.ServerId && SoundFilename == achRhs.SoundFilename && SceneId == achRhs.SceneId && ScenePhase == achRhs.ScenePhase; }

uint32_t ServerId{};
TiltedPhoques::String SoundFilename{};
// The scene the line was spoken in on the sender, if not in the player dialogue menu
GameId SceneId{};
// The sender's current phase of that scene when the line started
uint32_t ScenePhase{};
};
2 changes: 2 additions & 0 deletions Code/encoding/Messages/NotifySubtitle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ void NotifySubtitle::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const
Serialization::WriteString(aWriter, Text);
Serialization::WriteVarInt(aWriter, TopicFormId);
SceneId.Serialize(aWriter);
Serialization::WriteVarInt(aWriter, ScenePhase);
}

void NotifySubtitle::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept
Expand All @@ -16,4 +17,5 @@ void NotifySubtitle::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noex
Text = Serialization::ReadString(aReader);
TopicFormId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF;
SceneId.Deserialize(aReader);
ScenePhase = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF;
}
4 changes: 3 additions & 1 deletion Code/encoding/Messages/NotifySubtitle.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ struct NotifySubtitle final : ServerMessage
void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override;
void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override;

bool operator==(const NotifySubtitle& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && Text == acRhs.Text && TopicFormId == acRhs.TopicFormId && SceneId == acRhs.SceneId; }
bool operator==(const NotifySubtitle& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && Text == acRhs.Text && TopicFormId == acRhs.TopicFormId && SceneId == acRhs.SceneId && ScenePhase == acRhs.ScenePhase; }

uint32_t ServerId{};
TiltedPhoques::String Text{};
uint32_t TopicFormId{};
// The scene the line was spoken in on the sender, if not in the player dialogue menu
GameId SceneId{};
// The sender's current phase of that scene when the line started
uint32_t ScenePhase{};
};
2 changes: 2 additions & 0 deletions Code/encoding/Messages/SubtitleRequest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ void SubtitleRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const
Serialization::WriteString(aWriter, Text);
Serialization::WriteVarInt(aWriter, TopicFormId);
SceneId.Serialize(aWriter);
Serialization::WriteVarInt(aWriter, ScenePhase);
}

void SubtitleRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept
Expand All @@ -16,4 +17,5 @@ void SubtitleRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noe
Text = Serialization::ReadString(aReader);
TopicFormId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF;
SceneId.Deserialize(aReader);
ScenePhase = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF;
}
4 changes: 3 additions & 1 deletion Code/encoding/Messages/SubtitleRequest.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ struct SubtitleRequest final : ClientMessage
void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override;
void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override;

bool operator==(const SubtitleRequest& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && Text == acRhs.Text && TopicFormId == acRhs.TopicFormId && SceneId == acRhs.SceneId; }
bool operator==(const SubtitleRequest& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && Text == acRhs.Text && TopicFormId == acRhs.TopicFormId && SceneId == acRhs.SceneId && ScenePhase == acRhs.ScenePhase; }

uint32_t ServerId{};
TiltedPhoques::String Text{};
uint32_t TopicFormId{};
// The scene the line was spoken in on the sender, if not in the player dialogue menu
GameId SceneId{};
// The sender's current phase of that scene when the line started
uint32_t ScenePhase{};
};
2 changes: 2 additions & 0 deletions Code/server/Services/CharacterService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@ void CharacterService::OnDialogueRequest(const PacketEvent<DialogueRequest>& acM
notify.ServerId = message.ServerId;
notify.SoundFilename = message.SoundFilename;
notify.SceneId = message.SceneId;
notify.ScenePhase = message.ScenePhase;

const entt::entity cEntity = static_cast<entt::entity>(message.ServerId);
if (!GameServer::Get()->SendToPlayersInRange(notify, cEntity, acMessage.GetSender()))
Expand All @@ -609,6 +610,7 @@ void CharacterService::OnSubtitleRequest(const PacketEvent<SubtitleRequest>& acM
notify.ServerId = message.ServerId;
notify.Text = message.Text;
notify.SceneId = message.SceneId;
notify.ScenePhase = message.ScenePhase;

const entt::entity cEntity = static_cast<entt::entity>(message.ServerId);
if (!GameServer::Get()->SendToPlayersInRange(notify, cEntity, acMessage.GetSender()))
Expand Down