Skip to content

Commit aef55b7

Browse files
committed
WIP
1 parent 224516b commit aef55b7

File tree

4 files changed

+153
-119
lines changed

4 files changed

+153
-119
lines changed

Code/client/Games/Misc/SubtitleManager.cpp

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,8 @@ void* SubtitleManager::HideSubtitle(TESObjectREFR* apSpeaker) noexcept
3333
void TP_MAKE_THISCALL(HookShowSubtitle, SubtitleManager, TESObjectREFR* apSpeaker, const char* apSubtitleText, bool aIsInDialogue)
3434
{
3535
Actor* pActor = Cast<Actor>(apSpeaker);
36-
if (pActor)
37-
{
38-
auto isLocal = pActor->GetExtension()->IsLocal();
39-
auto isLocalPlayer = pActor->GetExtension()->IsLocalPlayer();
40-
auto isRemoteInScene = !isLocal && pActor->GetCurrentScene() && pActor->GetCurrentScene()->isPlaying;
41-
bool shouldForward = apSubtitleText && (isLocal && !isLocalPlayer || isRemoteInScene);
42-
const bool isLeader = World::Get().GetPartyService().IsLeader(); // Helps distinguish logs in 2-party
43-
const auto pname = pActor->baseForm->GetName() ? pActor->baseForm->GetName() : "";
44-
45-
if (shouldForward && isRemoteInScene)
46-
spdlog::debug(__FUNCTION__ ": forwarding subtitles because isRemoteInScene formId {:X} isLeader {} name {} text {}",
47-
pActor->formID, isLeader, pname, apSubtitleText);
48-
49-
if (shouldForward)
50-
World::Get().GetRunner().Trigger(SubtitleEvent(apSpeaker->formID, apSubtitleText));
51-
}
36+
if (apSubtitleText && std::strlen(apSubtitleText) && pActor && !pActor->GetExtension()->IsPlayer())
37+
World::Get().GetRunner().Trigger(SubtitleEvent(apSpeaker->formID, apSubtitleText));
5238

5339
TiltedPhoques::ThisCall(RealShowSubtitle, apThis, apSpeaker, apSubtitleText, aIsInDialogue);
5440
}

Code/client/Games/Skyrim/Actor.cpp

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,7 @@ static TSpeakSoundFunction* RealSpeakSoundFunction = nullptr;
12181218

12191219
float TP_MAKE_THISCALL(HookSpeakSoundFunction, Actor, const char* apName, uint32_t* a3, uint32_t a4, uint32_t a5, uint32_t a6, uint64_t a7, uint64_t a8, uint64_t a9, bool a10, uint64_t a11, bool a12, bool a13, bool a14)
12201220
{
1221+
#if 0
12211222
// Dedup back-to-back calls from Skyrim engine, not sure what it's doing.
12221223
// ONLY dedups DialogueEvents, original behavior preserved.
12231224
using std::chrono::steady_clock;
@@ -1235,32 +1236,12 @@ float TP_MAKE_THISCALL(HookSpeakSoundFunction, Actor, const char* apName, uint32
12351236
plastName = const_cast<char*>(apName);
12361237
lastTime = steady_clock::now();
12371238
}
1238-
1239-
auto isRemote = apThis->GetExtension()->IsRemote();
1240-
auto isInScene = apThis->GetCurrentScene() && apThis->GetCurrentScene()->isPlaying;
1241-
auto isRemoteInScene = isRemote && isInScene;
1242-
const bool isLeader = World::Get().GetPartyService().IsLeader(); // Helps distinguish logs in 2-party
1239+
#endif
12431240

12441241
spdlog::debug("a3: {:X}, a4: {}, a5: {}, a6: {}, a7: {}, a8: {:X}, a9: {:X}, a10: {}, a11: {:X}, a12: {}, a13: {}, a14: {}", (uint64_t)a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14);
12451242

1246-
if (isDuplicate)
1247-
spdlog::warn(__FUNCTION__ ": suppressing duplicate DialogueEvent isLocal {} isRemoteInScene {}, formId {:X} flags {:X}, flags2 {:X}, isLeader {} name {}, apName {}",
1248-
!isRemote, isRemoteInScene, apThis->formID, apThis->flags, apThis->flags2, isLeader, apThis->baseForm->GetName(), apName);
1249-
else
1250-
{
1251-
spdlog::warn(__FUNCTION__ ": DialogueEvent isLocal {} isRemoteInScene {}, formId {:X} flags {:X}, flags2 {:X}, isLeader {} name {}, apName {}",
1252-
!isRemote, isRemoteInScene, apThis->formID, apThis->flags, apThis->flags2, isLeader, apThis->baseForm->GetName(), apName);
1253-
1254-
if (isRemoteInScene)
1255-
{
1256-
spdlog::warn(__FUNCTION__ ": generating dialog event as remote because in-scene formId {:X} isLeader {} name {}, apName {}",
1257-
apThis->formID, isLeader, apThis->baseForm->GetName(), apName);
1258-
}
1259-
1260-
if (apThis->GetExtension()->IsLocal() || isRemoteInScene) // Send dialog of Actors under our control, or being generated by a Scene
1261-
World::Get().GetRunner().Trigger(DialogueEvent(apThis->formID, apName));
1262-
}
1263-
1243+
World::Get().GetRunner().Trigger(DialogueEvent(apThis->formID, apName));
1244+
12641245
return TiltedPhoques::ThisCall(RealSpeakSoundFunction, apThis, apName, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14);
12651246
}
12661247

@@ -1286,13 +1267,48 @@ bool Actor::IsTalking() noexcept
12861267
return TiltedPhoques::ThisCall(s_IsTalking, this);
12871268
}
12881269

1270+
bool Actor::IsInScene() noexcept
1271+
{
1272+
//PAPYRUS_FUNCTION(bool, Actor, IsInScene);
1273+
//bool papyrusValue = s_pIsInScene(this);
1274+
1275+
//TP_THIS_FUNCTION(TIsInScene, bool, Actor);
1276+
//POINTER_SKYRIMSE(TIsInScene, s_IsInScene, 21682);
1277+
//bool nativeAnswer = TiltedPhoques::ThisCall(s_IsInScene, this);
1278+
bool flagsValue= (flags1 & ActorBoolBits::kHasSceneExtra) != 0;
1279+
//if (nativeAnswer != flagsAnswer)
1280+
// spdlog::critical(__FUNCTION__ ": papyrus says {}, flags say {}", nativeAnswer, flagsAnswer);
1281+
1282+
//if (flagsValue != papyrusValue)
1283+
// spdlog::critical(__FUNCTION__ ": papyrusValue {}, flagsValue {}", papyrusValue, flagsValue);
1284+
1285+
return flagsValue;
1286+
}
1287+
1288+
bool Actor::IsInDialogueWithPlayer() noexcept
1289+
{
1290+
using ObjectReference = TESObjectREFR;
1291+
PAPYRUS_FUNCTION(bool, ObjectReference, IsInDialogueWithPlayer);
1292+
bool papyrusValue = s_pIsInDialogueWithPlayer(this);
1293+
1294+
//TP_THIS_FUNCTION(TIsInDialogueWithPlayer, bool, Actor);
1295+
//POINTER_SKYRIMSE(TIsInDialogueWithPlayer, s_TIsInDialogueWithPlayer, 21692);
1296+
//bool nativeValue = TiltedPhoques::ThisCall(s_TIsInDialogueWithPlayer, this);
1297+
1298+
//if (nativeValue != papyrusValue)
1299+
// spdlog::critical(__FUNCTION__ ": papyrusValue {}, nativeValue {}", papyrusValue, nativeValue);
1300+
return papyrusValue;
1301+
}
1302+
12891303
float Actor::GetVoiceRecoveryTime() noexcept
12901304
{
1305+
PAPYRUS_FUNCTION(float, Actor, GetVoiceRecoveryTime);
1306+
float fVRT = s_pGetVoiceRecoveryTime(this);
12911307
//Always returns zero, something is wrong in the RE::
1292-
TP_THIS_FUNCTION(TGetVoiceRecoveryTime, float, Actor);
1293-
POINTER_SKYRIMSE(TGetVoiceRecoveryTime, s_getVoiceRecoveryTime, 38808);
1308+
//TP_THIS_FUNCTION(TGetVoiceRecoveryTime, float, Actor);
1309+
//POINTER_SKYRIMSE(TGetVoiceRecoveryTime, s_getVoiceRecoveryTime, 38808);
12941310
//return TiltedPhoques::ThisCall(s_getVoiceRecoveryTime, this);
1295-
float fVRT = TiltedPhoques::ThisCall(s_getVoiceRecoveryTime, this);
1311+
//float fVRT = TiltedPhoques::ThisCall(s_getVoiceRecoveryTime, this);
12961312

12971313
if (fVRT != fVoiceTimer)
12981314
spdlog::warn(__FUNCTION__ ": fVRT {}, fVoiceTimer {}", fVRT, fVoiceTimer);

Code/client/Games/Skyrim/Actor.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ struct Actor : TESObjectREFR
211211
[[nodiscard]] bool ShouldWearBodyPiece() const noexcept;
212212
[[nodiscard]] bool IsVampireLord() const noexcept;
213213
[[nodiscard]] bool IsTalking() noexcept;
214+
[[nodiscard]] bool IsInScene() noexcept;
215+
[[nodiscard]] bool IsInDialogueWithPlayer() noexcept;
214216
[[nodiscard]] float GetVoiceRecoveryTime() noexcept;
215217
[[nodiscard]] bool IsSpeakingInScene();
216218

@@ -256,6 +258,12 @@ struct Actor : TESObjectREFR
256258
void FixVampireLordModel() noexcept;
257259
bool RemoveSpell(MagicItem* apSpell) noexcept;
258260

261+
enum ActorBoolBits
262+
{
263+
kNone = 0,
264+
kHasSceneExtra = 1 << 3,
265+
};
266+
259267
enum ActorFlags
260268
{
261269
IS_A_MOUNT = 1 << 1,

Code/client/Services/Generic/CharacterService.cpp

Lines changed: 101 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -920,39 +920,76 @@ void CharacterService::OnDialogueEvent(const DialogueEvent& acEvent) noexcept
920920
if (!m_transport.IsConnected())
921921
return;
922922

923+
const bool isLeader = World::Get().GetPartyService().IsLeader(); // Helps distinguish in 2-party logs
923924
auto view = m_world.view<FormIdComponent>(entt::exclude<ObjectComponent>);
924-
auto entityIt = std::find_if(view.begin(), view.end(), [view, formId = acEvent.ActorID](auto entity) { return view.get<FormIdComponent>(entity).Id == formId; });
925+
auto entityIt = std::find_if(view.begin(), view.end(), [view, formId = acEvent.ActorID](auto entity) {
926+
return view.get<FormIdComponent>(entity).Id == formId;
927+
});
925928

926929
if (entityIt == view.end())
927930
{
928-
spdlog::error( __FUNCTION__ ": failed to find speaking Actor's FormIdComponent, formId {:X}",acEvent.ActorID);
931+
spdlog::error(__FUNCTION__ ": failed to find speaking Actor's FormIdComponent, formId {:X}, isLeader {}", acEvent.ActorID, isLeader);
929932
return;
930933
}
931934

932935
auto serverIdRes = Utils::GetServerId(*entityIt);
933936
if (!serverIdRes)
934937
{
935-
spdlog::warn(__FUNCTION__ ": server id not found for formId {:X}", acEvent.ActorID);
938+
spdlog::warn(__FUNCTION__ ": server id not found for formId {:X}, isLeader {}", acEvent.ActorID, isLeader);
936939
return;
937940
}
938-
941+
939942
Actor* pActor = Cast<Actor>(TESForm::GetById(acEvent.ActorID));
940-
auto isRemote = pActor->GetExtension()->IsRemote();
941-
auto isRemoteInScene = isRemote && pActor->GetCurrentScene() && pActor->GetCurrentScene()->isPlaying;
942-
const bool isLeader = m_world.Get().GetPartyService().IsLeader(); // Helps distinguish in 2-party
943-
if (isRemoteInScene) // If we're forwarding Remote dialog, it should be in-Scene Dialogue Actions, or Task Dialogue.
944-
spdlog::warn(__FUNCTION__ ": remote actor is speaking in-scene formId {:X} serverId {:X} isRemote {} isLeader {}, name {}", acEvent.ActorID, serverIdRes.value(), isRemote, isLeader, pActor->baseForm->GetName());
943+
if (!pActor)
944+
return;
945945

946-
DialogueRequest request{};
947-
request.ServerId = serverIdRes.value();
948-
request.SoundFilename = acEvent.VoiceFile;
946+
bool isLocal = pActor->GetExtension()->IsLocal();
947+
bool isInScene = pActor->IsInScene();
948+
auto sceneId = isInScene ? pActor->GetCurrentScene()->formID : 0;
949+
bool isTaskDialogue = pActor->IsTalking() && pActor->IsInDialogueWithPlayer();
949950

950-
m_transport.Send(request);
951+
// There's no good way to distinguish the goodbye line
952+
// from the first line of the scene, so disable this.
953+
bool repairEdgeCase = false;
954+
#if 0
955+
// Fix the edge case, "goodbye line" of TaskDialogue
956+
static Actor* plastDialogueActor = nullptr;
957+
static bool lastIsInDialogueWithPlayer = false;
958+
bool repairEdgeCase = (pActor == plastDialogueActor) && lastIsInDialogueWithPlayer;
959+
960+
if (repairEdgeCase)
961+
{
962+
plastDialogueActor = nullptr;
963+
lastIsInDialogueWithPlayer = false;
964+
}
965+
966+
else if (isTaskDialogue)
967+
{
968+
plastDialogueActor = pActor;
969+
lastIsInDialogueWithPlayer = true;
970+
}
971+
#endif
972+
973+
const bool willSync = isTaskDialogue || repairEdgeCase || isLocal && !isInScene;
974+
975+
spdlog::warn(__FUNCTION__ ": isLocal {}, isInScene {}, isTaskDialogue {} repaireEdgeCase {}, willSync {}, scene {:X}, Actor "
976+
"{:X}, serverId {:X}, isLeader {}, name {}, soundFile {}",
977+
isLocal, isInScene, isTaskDialogue, repairEdgeCase, willSync, sceneId, pActor->formID, serverIdRes.value(),
978+
isLeader, pActor->baseForm->GetName(), acEvent.VoiceFile);
979+
980+
if (willSync)
981+
{
982+
DialogueRequest request{};
983+
request.ServerId = serverIdRes.value();
984+
request.SoundFilename = acEvent.VoiceFile;
985+
986+
m_transport.Send(request);
987+
}
951988
}
952989

953990
void CharacterService::OnNotifyDialogue(const NotifyDialogue& acMessage) noexcept
954991
{
955-
const bool isLeader = m_world.Get().GetPartyService().IsLeader();
992+
const bool isLeader = World::Get().GetPartyService().IsLeader(); // Helps distinguish in 2-party logs
956993
auto view = m_world.view<FormIdComponent>(entt::exclude<ObjectComponent>);
957994
auto viewIt = std::find_if(
958995
view.begin(), view.end(),
@@ -972,60 +1009,22 @@ void CharacterService::OnNotifyDialogue(const NotifyDialogue& acMessage) noexcep
9721009
if (!pActor)
9731010
return;
9741011

975-
// If we receive remote dialog for a Local actor, it was sent because
976-
// the remote version is in a cutscene and we should play it. Similarly if
977-
// receive dialog for a Remote actor, it's most often from the Leader and we
978-
// also should play it.
979-
//
980-
// Unless the actor is speaking! If actively talking, the remote dialog interrupts
981-
// our lines. That's a problem for Scene Dialog Actions, they often control Scene
982-
// timing, aborting them breaks the scene.
983-
//
984-
// FIXME: there is no known perfect test for "is talking," Papyrus IsTalking() only works
985-
// on TaskDialog() (Dialog speak / response trees). It doesn't work on Scene dialog (Dialog
986-
// Actions). The closest test is if a Scene is playing.
987-
//
988-
// The compromise is: dialog is rejected if the NPC is playing a scene.
989-
//
990-
// Most quests trigger scenes with Quest Stage changes. That means everyone is playing
991-
// the scene, everyone gets their own dialog, and the scene timing stays correct. TaskDialog
992-
// dialog won't play to others that don't participate in the dialog say&respond. Can't fix it.
993-
//
994-
// There are evil scenes that report "IsPlaying" when they are waiting to be triggered, and don't
995-
// fire quest stages to make everyone play the scene. Companions C02 (e.g., Brotherhood /
996-
// Induction scene). Whoever triggers gets dialog. Others would also have to trigger to hear
997-
// the dialog. It's the best we can do.
998-
//
999-
const auto pScene = pActor->GetCurrentScene();
1000-
// const bool isPlaying = pScene && pScene->isPlaying;
1001-
const bool isTalking = pActor->IsTalking(); // TaskDialogue?
1002-
const bool isSpeakingInScene = pActor->IsSpeakingInScene();
1003-
const auto pName = (pActor->baseForm && pActor->baseForm->GetName()) ? pActor->baseForm->GetName() : "";
1004-
1005-
if (!isTalking && isSpeakingInScene)
1006-
spdlog::warn(__FUNCTION__ ": aborting dialog sync while speaking in scene {:X}, Actor {:X}, serverId {:X}, "
1007-
"isLeader {}, isTalking {}, isSpeakingInScene {}, name {}",
1008-
pScene->formID, pActor->formID, acMessage.ServerId, isLeader, isTalking, isSpeakingInScene, pName);
1009-
else
1010-
{
1011-
spdlog::warn(__FUNCTION__ ": syncing dialog should NOT be speaking in scene Actor {:X}, serverId {:X}, "
1012-
"isLeader {}, isTalking {}, isSpeakingInScene {}, name {}",
1013-
pActor->formID, acMessage.ServerId, isLeader, isTalking, isSpeakingInScene, pName);
10141012

1015-
//if (pActor->IsTalking()) // To break out of dialogues.
1016-
pActor->StopCurrentDialogue(true);
1017-
pActor->SpeakSound(acMessage.SoundFilename.c_str());
1018-
}
1013+
spdlog::warn(__FUNCTION__ ": playing dialogue Actor {:X}, serverId {:X}, isLeader {}, name {}, soundFile {}",
1014+
pActor->formID, acMessage.ServerId, isLeader, pActor->baseForm->GetName(), acMessage.SoundFilename);
1015+
1016+
pActor->StopCurrentDialogue(true);
1017+
pActor->SpeakSound(acMessage.SoundFilename.c_str());
10191018
}
10201019

10211020
void CharacterService::OnSubtitleEvent(const SubtitleEvent& acEvent) noexcept
10221021
{
10231022
if (!m_transport.IsConnected())
10241023
return;
10251024

1025+
const bool isLeader = World::Get().GetPartyService().IsLeader(); // Helps distinguish in 2-party logs
10261026
auto view = m_world.view<FormIdComponent>(entt::exclude<ObjectComponent>);
10271027
auto entityIt = std::find_if(view.begin(), view.end(), [view, formId = acEvent.SpeakerID](auto entity) { return view.get<FormIdComponent>(entity).Id == formId; });
1028-
const bool isLeader = m_world.Get().GetPartyService().IsLeader();
10291028

10301029
if (entityIt == view.end())
10311030
{
@@ -1036,16 +1035,52 @@ void CharacterService::OnSubtitleEvent(const SubtitleEvent& acEvent) noexcept
10361035
auto serverIdRes = Utils::GetServerId(*entityIt);
10371036
if (!serverIdRes)
10381037
{
1039-
spdlog::warn(__FUNCTION__ ": server id not found for formId {:X}", acEvent.SpeakerID);
1038+
spdlog::warn(__FUNCTION__ ": server id not found for formId {:X}, isLeader {}", acEvent.SpeakerID, isLeader);
10401039
return;
10411040
}
10421041

1043-
SubtitleRequest request{};
1044-
request.ServerId = serverIdRes.value();
1045-
request.Text = acEvent.Text;
1046-
request.TopicFormId = acEvent.TopicFormID;
1042+
Actor* pActor = Cast<Actor>(TESForm::GetById(acEvent.SpeakerID));
1043+
if (!pActor)
1044+
return;
1045+
1046+
bool isLocal = pActor->GetExtension()->IsLocal();
1047+
bool isInScene = pActor->IsInScene();
1048+
auto sceneId = isInScene ? pActor->GetCurrentScene()->formID : 0;
1049+
bool isTaskDialogue = pActor->IsTalking() && pActor->IsInDialogueWithPlayer();
10471050

1048-
m_transport.Send(request);
1051+
// Fix the edge case, "goodbye line" of TaskDialogue
1052+
static Actor* plastDialogueActor = nullptr;
1053+
static bool lastIsInDialogueWithPlayer = false;
1054+
bool repairEdgeCase = (pActor == plastDialogueActor) && lastIsInDialogueWithPlayer;
1055+
1056+
if (repairEdgeCase)
1057+
{
1058+
plastDialogueActor = nullptr;
1059+
lastIsInDialogueWithPlayer = false;
1060+
}
1061+
1062+
else if (isTaskDialogue)
1063+
{
1064+
plastDialogueActor = pActor;
1065+
lastIsInDialogueWithPlayer = true;
1066+
}
1067+
1068+
const bool willSync = isTaskDialogue || repairEdgeCase || isLocal && !isInScene;
1069+
1070+
spdlog::warn(
1071+
__FUNCTION__ ": isLocal {}, isInScene {}, isTaskDialogue {} repaireEdgeCase {}, willSync {}, scene {:X}, Actor "
1072+
"{:X}, serverId {:X}, isLeader {}, name {}, subtitle {}",
1073+
isLocal, isInScene, isTaskDialogue, repairEdgeCase, willSync, sceneId, pActor->formID, serverIdRes.value(),
1074+
isLeader, pActor->baseForm->GetName(), acEvent.Text);
1075+
1076+
if (willSync)
1077+
{
1078+
SubtitleRequest request{};
1079+
request.ServerId = serverIdRes.value();
1080+
request.Text = acEvent.Text;
1081+
request.TopicFormId = acEvent.TopicFormID;
1082+
m_transport.Send(request);
1083+
}
10491084
}
10501085

10511086
void CharacterService::OnNotifySubtitle(const NotifySubtitle& acMessage) noexcept
@@ -1074,19 +1109,8 @@ void CharacterService::OnNotifySubtitle(const NotifySubtitle& acMessage) noexcep
10741109
TESTopicInfo* pInfo = nullptr;
10751110
pInfo = Cast<TESTopicInfo>(TESForm::GetById(acMessage.TopicFormId));
10761111

1077-
// If we receive remote subtitles for a Local actor, it was sent because
1078-
// the remote version is in a cutscene and we should play it.
1079-
const auto pScene = pActor->GetCurrentScene();
1080-
const bool isSpeakingInScene = pActor->IsSpeakingInScene();
1081-
// const bool isPlaying = pScene && pScene->isPlaying;
1082-
const auto pName = (pActor->baseForm && pActor->baseForm->GetName()) ? pActor->baseForm->GetName() : "";
1083-
if (isSpeakingInScene)
1084-
spdlog::warn(__FUNCTION__ ": subtitle sync while speaking in (likely same) scene {:X}, Actor {:X}, serverId {:X}, "
1085-
"isLeader {}, name {}, message: {}",
1086-
pScene->formID, pActor->formID, acMessage.ServerId, isLeader, pName, acMessage.Text.c_str());
1087-
else
1088-
spdlog::warn(__FUNCTION__ ": showing subtitle Actor {:X}, serverId {:X}, isLeader {}, name {}, message: {}",
1089-
pActor->formID, acMessage.ServerId, isLeader, pName, acMessage.Text.c_str());
1112+
spdlog::warn(__FUNCTION__ ": showing subtitle Actor {:X}, serverId {:X}, isLeader {}, name {}, message: {}",
1113+
pActor->formID, acMessage.ServerId, isLeader, pActor->baseForm->GetName(), acMessage.Text);
10901114

10911115
SubtitleManager::Get()->ShowSubtitle(pActor, acMessage.Text.c_str(), pInfo);
10921116
}

0 commit comments

Comments
 (0)