Skip to content

Commit e2aff51

Browse files
committed
Fix sliding NPCs by adding a replay cache for animations
Having a single `LatestAction` wasn't enough to bring Actors into a correct animation state. Now non-cell-owners (aka party members) will replay recent actions when an Actor is spawned in their world
1 parent 869d34f commit e2aff51

File tree

8 files changed

+325
-17
lines changed

8 files changed

+325
-17
lines changed

Code/client/Games/Animation.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ bool ActorMediator::ForceAction(TESActionData* apAction) noexcept
112112
uint8_t result = 0;
113113

114114
auto pActor = static_cast<Actor*>(apAction->actor);
115-
if (!pActor || pActor->animationGraphHolder.IsReady())
115+
if (pActor && pActor->animationGraphHolder.IsReady())
116116
{
117117
result = TiltedPhoques::ThisCall(PerformComplexAction, this, apAction);
118118

Code/client/Services/Generic/CharacterService.cpp

+18-8
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,11 @@ void CharacterService::OnAssignCharacter(const AssignCharacterResponse& acMessag
335335

336336
pActor->GetExtension()->SetRemote(true);
337337

338-
InterpolationSystem::Setup(m_world, cEntity);
339-
AnimationSystem::Setup(m_world, cEntity);
338+
// TODO: `AnimationSystem::Setup` erases my actions replay cache, gotta figure out why
339+
// these two lines were added in the first place
340+
341+
//InterpolationSystem::Setup(m_world, cEntity);
342+
//AnimationSystem::Setup(m_world, cEntity);
340343

341344
pActor->SetActorValues(acMessage.AllActorValues);
342345
pActor->SetActorInventory(acMessage.CurrentInventory);
@@ -400,11 +403,12 @@ void CharacterService::OnCharacterSpawn(const CharacterSpawnRequest& acMessage)
400403
auto waitingView = m_world.view<FormIdComponent, WaitingForAssignmentComponent>();
401404
const auto waitingItor = std::find_if(std::begin(waitingView), std::end(waitingView), [waitingView, cActorId](auto entity) { return waitingView.get<FormIdComponent>(entity).Id == cActorId; });
402405

403-
if (waitingItor != std::end(waitingView))
404-
{
405-
spdlog::info("Character with form id {:X} already has a spawn request in progress.", cActorId);
406-
return;
407-
}
406+
// TODO: Sometimes actors have "a spawn request in progress" when they shouldn't, debug this..
407+
//if (waitingItor != std::end(waitingView))
408+
//{
409+
// spdlog::info("Character with form id {:X} already has a spawn request in progress.", cActorId);
410+
// return;
411+
//}
408412

409413
auto* const pForm = TESForm::GetById(cActorId);
410414
pActor = Cast<Actor>(pForm);
@@ -472,7 +476,13 @@ void CharacterService::OnCharacterSpawn(const CharacterSpawnRequest& acMessage)
472476
m_world.emplace_or_replace<WaitingFor3D>(*entity, acMessage);
473477

474478
auto& remoteAnimationComponent = m_world.get<RemoteAnimationComponent>(*entity);
475-
remoteAnimationComponent.TimePoints.push_back(acMessage.LatestAction);
479+
480+
for (const ActionEvent& action : acMessage.ActionsReplayCache)
481+
{
482+
if (action.EventName.empty()) // TODO: skip empties for now. figure out why an empty event is present after deserialization
483+
continue;
484+
remoteAnimationComponent.TimePoints.push_back(action);
485+
}
476486
}
477487

478488
void CharacterService::OnRemoteSpawnDataReceived(const NotifySpawnData& acMessage) noexcept

Code/encoding/Messages/CharacterSpawnRequest.cpp

+17-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,17 @@ void CharacterSpawnRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter)
1212
Serialization::WriteString(aWriter, AppearanceBuffer);
1313
InventoryContent.Serialize(aWriter);
1414
FactionsContent.Serialize(aWriter);
15+
16+
// Actions
1517
LatestAction.GenerateDifferential(ActionEvent{}, aWriter);
18+
aWriter.WriteBits(ActionsReplayCache.size() & 0xFF, 8);
19+
ActionEvent lastSerialized{};
20+
for (int i = 0; i < ActionsReplayCache.size(); ++i)
21+
{
22+
ActionsReplayCache[i].GenerateDifferential(lastSerialized, aWriter);
23+
lastSerialized = ActionsReplayCache[i];
24+
}
25+
1626
FaceTints.Serialize(aWriter);
1727
InitialActorValues.Serialize(aWriter);
1828
Serialization::WriteVarInt(aWriter, PlayerId);
@@ -44,9 +54,15 @@ void CharacterSpawnRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReade
4454
FactionsContent = {};
4555
FactionsContent.Deserialize(aReader);
4656

57+
// Actions
4758
LatestAction = ActionEvent{};
4859
LatestAction.ApplyDifferential(aReader);
49-
60+
uint64_t replayActionsCount = 0;
61+
aReader.ReadBits(replayActionsCount, 8);
62+
ActionsReplayCache.resize(replayActionsCount);
63+
for (ActionEvent& replayAction : ActionsReplayCache)
64+
replayAction.ApplyDifferential(aReader);
65+
5066
FaceTints.Deserialize(aReader);
5167
InitialActorValues.Deserialize(aReader);
5268
PlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF;

Code/encoding/Messages/CharacterSpawnRequest.h

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ struct CharacterSpawnRequest final : ServerMessage
4242
Inventory InventoryContent{};
4343
Factions FactionsContent{};
4444
ActionEvent LatestAction{};
45+
Vector<ActionEvent> ActionsReplayCache{};
4546
Tints FaceTints{};
4647
ActorValues InitialActorValues{};
4748
uint32_t PlayerId{};

Code/server/Components/AnimationComponent.h

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
struct AnimationComponent
1010
{
1111
Vector<ActionEvent> Actions;
12+
Vector<ActionEvent> ActionsReplayCache;
1213
ActionEvent CurrentAction;
1314
ActionEvent LastSerializedAction;
1415
};
+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#include <Game/AnimationEventLists.h>
2+
3+
/*
4+
/* The lists here may not contain all relevant animation events, so extend as necessary
5+
*/
6+
7+
const Set<String> AnimationEventLists::g_actionsStart = {
8+
{"moveStart"},
9+
{"bowAttackStart"},
10+
{"blockStart"},
11+
{"staggerStart"},
12+
{"staggerIdleStart"},
13+
{"shoutStart"},
14+
{"SwimStart"},
15+
{"SneakStart"},
16+
{"torchEquip"},
17+
{"blockHitStart"},
18+
{"bleedOutStart"},
19+
{"blockAnticipateStart"},
20+
{"HorseEnter"},
21+
{"HorseEnterInstant"},
22+
{"HorseEnterSwim"},
23+
{"MountedSwimStart"},
24+
{"ChairLookDownEnterInstant"},
25+
{"RagdollInstant"},
26+
// Idle animations
27+
{"IdleAlchemyEnter"},
28+
{"IdleBarDrinkingStart"},
29+
{"IdleBedEnterInstant"},
30+
{"IdleBedEnterStart"},
31+
{"IdleBedLeftEnterInstant"},
32+
{"IdleBedLeftEnterStart"},
33+
{"IdleBedRightEnterInstant"},
34+
{"IdleBedRightEnterStart"},
35+
{"IdleBedRollFrontEnterInstant"},
36+
{"IdleBedRollFrontEnterStart"},
37+
{"IdleBedRollLeftEnterInstant"},
38+
{"IdleBedRollLeftEnterStart"},
39+
{"IdleBedRollRightEnterInstant"},
40+
{"IdleBedRollRightEnterStart"},
41+
{"IdleBeggar"},
42+
{"IdleBlacksmithForgeEnter"},
43+
{"IdleBlackSmithingEnterInstant"},
44+
{"IdleBlackSmithingEnterStart"},
45+
{"IdleBoundKneesStart"},
46+
{"IdleCarryBucketFillEnter"},
47+
{"IdleCarryBucketPourEnter"},
48+
{"IdleCartBenchEnter"},
49+
{"IdleCartBenchEnterInstant"},
50+
{"IdleChairEnterInstant"},
51+
{"IdleChairEnterStart"},
52+
{"IdleChairEnterToSit"},
53+
{"IdleChairFrontEnter"},
54+
{"IdleChairLeftEnter"},
55+
{"IdleChairRightEnter"},
56+
{"IdleCombatShieldStart"},
57+
{"IdleCombatStart"},
58+
{"IdleCookingSpitEnter"},
59+
{"IdleCounterStart"},
60+
{"IdleDialogueAngryStart"},
61+
{"IdleDialogueExpressiveStart"},
62+
{"IdleDialogueHappyStart"},
63+
{"idleDrinkingStandingStart"},
64+
{"IdleDrumStart"},
65+
{"idleEatingStandingStart"},
66+
{"IdleEnchantingEnter"},
67+
{"IdleFluteStart"},
68+
{"IdleFurnitureStart"},
69+
{"IdleGetAttention"},
70+
{"IdleHammerTableEnter"},
71+
{"IdleHammerTableEnterInstant"},
72+
{"IdleHammerWallEnter"},
73+
{"IdleHammerWallEnterInstant"},
74+
{"IdleHideLEnter"},
75+
{"IdleHideREnter"},
76+
{"IdleJarlChairEnter"},
77+
{"IdleJarlChairEnterInstant"},
78+
{"IdleLadderEnter"},
79+
{"IdleLadderEnterInstant"},
80+
{"IdleLayDownEnter"},
81+
{"IdleLayDownEnterInstant"},
82+
{"IdleLeanTable"},
83+
{"IdleLeanTableEnter"},
84+
{"IdleLeanTableEnterInstant"},
85+
{"IdleLeftChairEnterStart"},
86+
{"IdleLeverPushStart"},
87+
{"idleLooseSweepingStart"},
88+
{"IdleLuteStart"},
89+
{"IdleMillLoadStart"},
90+
{"IdlePickaxeEnter"},
91+
{"IdlePickaxeEnterInstant"},
92+
{"IdlePickaxeFloorEnter"},
93+
{"IdlePickaxeTableEnter"},
94+
{"IdleReadElderScrollStart"},
95+
{"IdleRightChairEnterStart"},
96+
{"IdleSearchingChest"},
97+
{"IdleSearchingTable"},
98+
{"IdleSharpeningWheelStart"},
99+
{"IdleSitCrossLeggedEnter"},
100+
{"IdleSitCrossLeggedEnterInstant"},
101+
{"IdleSitLedge"},
102+
{"IdleSitLedge_Enter"},
103+
{"IdleSitLedgeEnter"},
104+
{"IdleSitLedgeEnterInstant"},
105+
{"IdleStoolEnter"},
106+
{"IdleStoolEnterInstant"},
107+
{"IdleTableEnter"},
108+
{"IdleTableEnterInstant"},
109+
{"IdleTanningEnter"},
110+
{"IdleWallLeanStart"},
111+
{"IdleWarmHands"},
112+
{"IdleWebEnterInstant"},
113+
{"IdleWoodChopStart"},
114+
{"IdleWoodPickUpEnter"},
115+
{"IdleWoodPickUpEnterInstant"},
116+
{"IdleChairCHILDEnterInstant"},
117+
{"IdleChairCHILDFrontEnter"},
118+
{"IdleChairCHILDLeftEnter"},
119+
{"IdleChairCHILDRightEnter"},
120+
};
121+
122+
const Set<String> AnimationEventLists::g_actionsExit = {
123+
{"IdleForceDefaultState"}, // Belongs here too
124+
{"BleedOutEarlyExit"},
125+
{"HorseExit"},
126+
{"IdleChairExitToStand"},
127+
{"IdleChairFrontExit"},
128+
{"idleChairLeftExit"},
129+
{"idleChairRightExit"},
130+
{"IdleBedExitToStand"},
131+
{"IdleCartPrisonerAExit"},
132+
{"IdleFurnitureExit"},
133+
{"IdleLaydown_Exit"},
134+
{"IdleLounge_Exit"},
135+
{"IdleRailLeanExit"},
136+
{"IdleSitLedge_Exit"},
137+
{"IdleStoolBackExit"},
138+
{"IdleTableBackExit"},
139+
{"IdleWebExit"},
140+
{"IdleChairExitStart"},
141+
{"IdleBedExitStart"},
142+
{"IdleBedLeftExitStart"},
143+
{"IdleBedRightExitStart"},
144+
{"IdleBedRollFrontExitStart"},
145+
{"IdleBedRollLeftExitStart"},
146+
{"IdleBedRollRightExitStart"},
147+
{"IdleChairCHILDFrontExit"},
148+
{"IdleChairCHILDLeftExit"},
149+
{"IdleChairCHILDRightExit"},
150+
};
151+
152+
// Skip these in the loop when searching for a valid animation chain start
153+
const Set<String> AnimationEventLists::g_actionsSkipIntermediate = {
154+
{"attackStart"},
155+
{"turnStop"},
156+
{"moveStop"},
157+
{"WeapEquip"},
158+
{"SprintStart"},
159+
{"SprintStop"},
160+
{"CyclicFreeze"},
161+
{"CyclicCrossBlend"},
162+
{"IdleStop"},
163+
{"IdleStopInstant"},
164+
{"IdleHDLeft"},
165+
{"IdleHDLeftAngry"},
166+
{"IdleHDRight"},
167+
{"IdleHDRightAngry"},
168+
{"MotionDrivenIdle"},
169+
{"torchUnequip"},
170+
};
171+
172+
const Set<String> AnimationEventLists::g_actionsIgnore = {
173+
// Animation events with empty names don't carry anything useful besides
174+
// various animvars updates and state changes to the running animations.
175+
// Ignored because there is too many of them on each frame
176+
{""},
177+
{"TurnLeft"},
178+
{"TurnRight"},
179+
{"NPC_TurnLeft180"},
180+
{"NPC_TurnLeft90"},
181+
{"NPC_TurnRight180"},
182+
{"NPC_TurnRight90"},
183+
{"NPC_TurnToWalkLeft180"},
184+
{"NPC_TurnToWalkLeft90"},
185+
{"NPC_TurnToWalkRight180"},
186+
{"NPC_TurnToWalkRight90"},
187+
// There's an elusive bug on the client where it would spam "Unequip"
188+
// and "combatStanceStop" a lot after changing cells
189+
{"Unequip"},
190+
{"combatStanceStop"},
191+
};
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#pragma once
2+
3+
#include <TiltedCore/Stl.hpp>
4+
5+
using TiltedPhoques::Set, TiltedPhoques::String;
6+
7+
namespace AnimationEventLists
8+
{
9+
extern const Set<String> g_actionsStart;
10+
11+
extern const Set<String> g_actionsExit;
12+
13+
extern const Set<String> g_actionsSkipIntermediate;
14+
15+
extern const Set<String> g_actionsIgnore;
16+
} // namespace

0 commit comments

Comments
 (0)