Skip to content

Commit

Permalink
Fix sliding NPCs by adding a replay cache for animations
Browse files Browse the repository at this point in the history
Having a single `LatestAction` wasn't enough to bring Actors into a correct animation state. Now non-cell-owners (aka party members) will replay recently ran animations when an Actor is spawned in their world
  • Loading branch information
miredirex committed Jan 7, 2025
1 parent 869d34f commit 559fb4a
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 17 deletions.
2 changes: 1 addition & 1 deletion Code/client/Games/Animation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ bool ActorMediator::ForceAction(TESActionData* apAction) noexcept
uint8_t result = 0;

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

Expand Down
26 changes: 18 additions & 8 deletions Code/client/Services/Generic/CharacterService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,11 @@ void CharacterService::OnAssignCharacter(const AssignCharacterResponse& acMessag

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

InterpolationSystem::Setup(m_world, cEntity);
AnimationSystem::Setup(m_world, cEntity);
// TODO: `AnimationSystem::Setup` erases my actions replay cache, gotta figure out why
// these two lines were added in the first place

//InterpolationSystem::Setup(m_world, cEntity);
//AnimationSystem::Setup(m_world, cEntity);

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

if (waitingItor != std::end(waitingView))
{
spdlog::info("Character with form id {:X} already has a spawn request in progress.", cActorId);
return;
}
// TODO: Sometimes actors have "a spawn request in progress" when they shouldn't, debug this..
//if (waitingItor != std::end(waitingView))
//{
// spdlog::info("Character with form id {:X} already has a spawn request in progress.", cActorId);
// return;
//}

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

auto& remoteAnimationComponent = m_world.get<RemoteAnimationComponent>(*entity);
remoteAnimationComponent.TimePoints.push_back(acMessage.LatestAction);

for (const ActionEvent& action : acMessage.ActionsReplayCache)
{
if (action.EventName.empty()) // TODO: skip empties for now. figure out why an empty event is present after deserialization
continue;
remoteAnimationComponent.TimePoints.push_back(action);
}
}

void CharacterService::OnRemoteSpawnDataReceived(const NotifySpawnData& acMessage) noexcept
Expand Down
18 changes: 17 additions & 1 deletion Code/encoding/Messages/CharacterSpawnRequest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ void CharacterSpawnRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter)
Serialization::WriteString(aWriter, AppearanceBuffer);
InventoryContent.Serialize(aWriter);
FactionsContent.Serialize(aWriter);

// Actions
LatestAction.GenerateDifferential(ActionEvent{}, aWriter);
aWriter.WriteBits(ActionsReplayCache.size() & 0xFF, 8);
ActionEvent lastSerialized{};
for (int i = 0; i < ActionsReplayCache.size(); ++i)
{
ActionsReplayCache[i].GenerateDifferential(lastSerialized, aWriter);
lastSerialized = ActionsReplayCache[i];
}

FaceTints.Serialize(aWriter);
InitialActorValues.Serialize(aWriter);
Serialization::WriteVarInt(aWriter, PlayerId);
Expand Down Expand Up @@ -44,9 +54,15 @@ void CharacterSpawnRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReade
FactionsContent = {};
FactionsContent.Deserialize(aReader);

// Actions
LatestAction = ActionEvent{};
LatestAction.ApplyDifferential(aReader);

uint64_t replayActionsCount = 0;
aReader.ReadBits(replayActionsCount, 8);
ActionsReplayCache.resize(replayActionsCount);
for (ActionEvent& replayAction : ActionsReplayCache)
replayAction.ApplyDifferential(aReader);

FaceTints.Deserialize(aReader);
InitialActorValues.Deserialize(aReader);
PlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF;
Expand Down
1 change: 1 addition & 0 deletions Code/encoding/Messages/CharacterSpawnRequest.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ struct CharacterSpawnRequest final : ServerMessage
Inventory InventoryContent{};
Factions FactionsContent{};
ActionEvent LatestAction{};
Vector<ActionEvent> ActionsReplayCache{};
Tints FaceTints{};
ActorValues InitialActorValues{};
uint32_t PlayerId{};
Expand Down
1 change: 1 addition & 0 deletions Code/server/Components/AnimationComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
struct AnimationComponent
{
Vector<ActionEvent> Actions;
Vector<ActionEvent> ActionsReplayCache;
ActionEvent CurrentAction;
ActionEvent LastSerializedAction;
};
201 changes: 201 additions & 0 deletions Code/server/Game/AnimationEventLists.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#include <Game/AnimationEventLists.h>

/*
/* The lists here may not contain all relevant animation events, so extend as necessary
*/

const Set<String> AnimationEventLists::g_actionsStart = {
{"moveStart"},
{"bowAttackStart"},
{"blockStart"},
{"staggerStart"},
{"staggerIdleStart"},
{"shoutStart"},
{"SwimStart"},
{"SneakStart"},
{"torchEquip"},
{"blockHitStart"},
{"bleedOutStart"},
{"blockAnticipateStart"},
{"HorseEnter"},
{"HorseEnterInstant"},
{"HorseEnterSwim"},
{"MountedSwimStart"},
{"ChairLookDownEnterInstant"},
{"RagdollInstant"},
// Idle animations
{"IdleAlchemyEnter"},
{"IdleBarDrinkingStart"},
{"IdleBedEnterInstant"},
{"IdleBedEnterStart"},
{"IdleBedLeftEnterInstant"},
{"IdleBedLeftEnterStart"},
{"IdleBedRightEnterInstant"},
{"IdleBedRightEnterStart"},
{"IdleBedRollFrontEnterInstant"},
{"IdleBedRollFrontEnterStart"},
{"IdleBedRollLeftEnterInstant"},
{"IdleBedRollLeftEnterStart"},
{"IdleBedRollRightEnterInstant"},
{"IdleBedRollRightEnterStart"},
{"IdleBeggar"},
{"IdleBlacksmithForgeEnter"},
{"IdleBlackSmithingEnterInstant"},
{"IdleBlackSmithingEnterStart"},
{"IdleBoundKneesStart"},
{"IdleCarryBucketFillEnter"},
{"IdleCarryBucketPourEnter"},
{"IdleCartBenchEnter"},
{"IdleCartBenchEnterInstant"},
{"IdleChairCHILDEnterInstant"},
{"IdleChairCHILDFrontEnter"},
{"IdleChairCHILDLeftEnter"},
{"IdleChairCHILDRightEnter"},
{"IdleChairEnterInstant"},
{"IdleChairEnterStart"},
{"IdleChairEnterToSit"},
{"IdleChairFrontEnter"},
{"IdleChairLeftEnter"},
{"IdleChairRightEnter"},
{"IdleCombatShieldStart"},
{"IdleCombatStart"},
{"IdleCookingSpitEnter"},
{"IdleCounterStart"},
{"IdleDialogueAngryStart"},
{"IdleDialogueExpressiveStart"},
{"IdleDialogueHappyStart"},
{"idleDrinkingStandingStart"},
{"IdleDrumStart"},
{"idleEatingStandingStart"},
{"IdleEnchantingEnter"},
{"IdleFluteStart"},
{"IdleFurnitureStart"},
{"IdleGetAttention"},
{"IdleHammerTableEnter"},
{"IdleHammerTableEnterInstant"},
{"IdleHammerWallEnter"},
{"IdleHammerWallEnterInstant"},
{"IdleHideLEnter"},
{"IdleHideREnter"},
{"IdleJarlChairEnter"},
{"IdleJarlChairEnterInstant"},
{"IdleLadderEnter"},
{"IdleLadderEnterInstant"},
{"IdleLayDownEnter"},
{"IdleLayDownEnterInstant"},
{"IdleLeanTable"},
{"IdleLeanTableEnter"},
{"IdleLeanTableEnterInstant"},
{"IdleLeftChairEnterStart"},
{"IdleLeverPushStart"},
{"idleLooseSweepingStart"},
{"IdleLuteStart"},
{"IdleMillLoadStart"},
{"IdlePickaxeEnter"},
{"IdlePickaxeEnterInstant"},
{"IdlePickaxeFloorEnter"},
{"IdlePickaxeTableEnter"},
{"IdleReadElderScrollStart"},
{"IdleRightChairEnterStart"},
{"IdleSearchingChest"},
{"IdleSearchingTable"},
{"IdleSharpeningWheelStart"},
{"IdleSitCrossLeggedEnter"},
{"IdleSitCrossLeggedEnterInstant"},
{"IdleSitLedge"},
{"IdleSitLedge_Enter"},
{"IdleSitLedgeEnter"},
{"IdleSitLedgeEnterInstant"},
{"IdleStoolEnter"},
{"IdleStoolEnterInstant"},
{"IdleTableDrinkAndMugEnter"},
{"IdleTableDrinkEnter"},
{"IdleTableEnter"},
{"IdleTableEnterInstant"},
{"IdleTableEnterPlayer"},
{"IdleTableMugEnter"},
{"IdleTablePassOutEnter"},
{"IdleTanningEnter"},
{"IdleWallLeanStart"},
{"IdleWarmHands"},
{"IdleWebEnterInstant"},
{"IdleWoodChopStart"},
{"IdleWoodPickUpEnter"},
{"IdleWoodPickUpEnterInstant"},
};

const Set<String> AnimationEventLists::g_actionsExit = {
{"IdleForceDefaultState"}, // Belongs here too
{"BleedOutEarlyExit"},
{"HorseExit"},
{"IdleChairExitToStand"},
{"IdleChairFrontExit"},
{"idleChairLeftExit"},
{"idleChairRightExit"},
{"IdleBedExitToStand"},
{"IdleCartPrisonerAExit"},
{"IdleFurnitureExit"},
{"IdleLaydown_Exit"},
{"IdleLounge_Exit"},
{"IdleRailLeanExit"},
{"IdleSitLedge_Exit"},
{"IdleStoolBackExit"},
{"IdleTableBackExit"},
{"IdleWebExit"},
{"IdleChairExitStart"},
{"IdleBedExitStart"},
{"IdleBedLeftExitStart"},
{"IdleBedRightExitStart"},
{"IdleBedRollFrontExitStart"},
{"IdleBedRollLeftExitStart"},
{"IdleBedRollRightExitStart"},
{"IdleChairCHILDFrontExit"},
{"IdleChairCHILDLeftExit"},
{"IdleChairCHILDRightExit"},
};

// Skip these in the loop when searching for a valid animation chain start
const Set<String> AnimationEventLists::g_actionsSkipIntermediate = {
{"attackStart"},
{"turnStop"},
{"moveStop"},
{"WeapEquip"},
{"SprintStart"},
{"SprintStop"},
{"CyclicFreeze"},
{"CyclicCrossBlend"},
{"IdleStop"},
{"IdleStopInstant"},
{"IdleHDLeft"},
{"IdleHDLeftAngry"},
{"IdleHDRight"},
{"IdleHDRightAngry"},
{"MotionDrivenIdle"},
{"torchUnequip"},
};

const Set<String> AnimationEventLists::g_actionsIgnore = {
// Animation events with empty names don't carry anything useful besides
// various animvars updates and state changes to the running animations.
// Ignored because there is too many of them on each frame
{""},
{"TurnLeft"},
{"TurnRight"},
{"NPC_BumpedFromBack"},
{"NPC_BumpedFromLeft"},
{"NPC_BumpedFromRight"},
{"NPC_BumpEnd"},
{"NPC_BumpFromFront"},
{"NPC_TurnLeft180"},
{"NPC_TurnLeft90"},
{"NPC_TurnRight180"},
{"NPC_TurnRight90"},
{"NPC_TurnToWalkLeft180"},
{"NPC_TurnToWalkLeft90"},
{"NPC_TurnToWalkRight180"},
{"NPC_TurnToWalkRight90"},
// There's an elusive bug on the client where it would spam "Unequip"
// and "combatStanceStop" a lot after changing cells
{"Unequip"},
{"combatStanceStop"},
};
16 changes: 16 additions & 0 deletions Code/server/Game/AnimationEventLists.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include <TiltedCore/Stl.hpp>

using TiltedPhoques::Set, TiltedPhoques::String;

namespace AnimationEventLists
{
extern const Set<String> g_actionsStart;

extern const Set<String> g_actionsExit;

extern const Set<String> g_actionsSkipIntermediate;

extern const Set<String> g_actionsIgnore;
} // namespace
Loading

0 comments on commit 559fb4a

Please sign in to comment.