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
24 changes: 24 additions & 0 deletions Code/client/Services/CharacterService.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
#include "Structs/Inventory.h"
#include "Structs/ActorData.h"

#if TP_SKYRIMVR
#include "Services/GearRedraw.h"
#endif

struct ActorAddedEvent;
struct ActorRemovedEvent;
struct UpdateEvent;
Expand Down Expand Up @@ -87,6 +91,14 @@ struct CharacterService

void ProcessNewEntity(entt::entity aEntity) const noexcept;

#if TP_SKYRIMVR
/**
* @brief Tells a pending armor redraw that the owner changed this item, so the redraw does not put it back
* or take it off again. Called by InventoryService for every remote equipment change.
*/
void OnRemoteEquipmentChange(uint32_t aFormId, uint32_t aItemId) noexcept;
#endif

private:
void MoveActor(const Actor* apActor, const GameId& acWorldSpaceId, const GameId& acCellId, const Vector3_NetQuantize& acPosition) const noexcept;

Expand All @@ -106,6 +118,10 @@ struct CharacterService
void RunExperienceUpdates() noexcept;
void ApplyCachedWeaponDraws(const UpdateEvent& acUpdateEvent) noexcept;
void RunOffHandWeaponUpdates() noexcept;
#if TP_SKYRIMVR
void QueueGearRedraw(entt::entity aEntity, Actor& aActor, const Inventory& acReceived) const noexcept;
void RunGearRedraws(const UpdateEvent& acUpdateEvent) noexcept;
#endif

/**
* @brief Asks for a body's weapon state to be applied and, on VR, its hand items to be reseated, without
Expand Down Expand Up @@ -175,6 +191,14 @@ struct CharacterService
};

Map<uint32_t, OffHandWeapon> m_offHandWeapons{};

/**
* @brief Remote player bodies whose worn armor is due to be taken off and put back, keyed by form id.
*
* See GearRedraw. Mutable because the redraw is queued from ProcessNewEntity, which is const, when a body
* kept through a 3D rebuild gets its equipment back.
*/
mutable Map<uint32_t, GearRedraw> m_gearRedraws{};
#endif

/**
Expand Down
112 changes: 112 additions & 0 deletions Code/client/Services/GearRedraw.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#pragma once

#include <algorithm>
#include <cstdint>
#include <vector>

/**
* @brief The schedule for redrawing a remote player body's worn armor after its inventory is set.
*
* A remote body built while its owner wears no torso armor shows none of its worn armor: the hood, boots and
* gauntlets are in its worn list, and the owner sees them equipped, but they are not drawn. Feet and hands
* vanish with the boots and gauntlets. A body with a torso piece draws everything. Any armor change by the
* owner redraws the whole body at once, so the owner taking one piece off is a complete repair, and that is
* the shape this follows: take the worn armor off, wait, put it back.
*
* The two halves run in separate updates, at least kRestoreGap apart, because an unequip and an equip queued
* into the same drain cancel out (see ReattachHandItems). What goes back on is what was worn when the armor
* came off, less anything the owner changed in between: their change has already been applied by the time the
* restore runs, and putting a removed piece back, or one whose slots the owner has since filled, would undo it.
*
* Kept free of game types so the schedule can be tested on its own.
*/
struct GearRedraw
{
// After the first weapon state pass, which is also at half a second.
static constexpr double kStripAt = 0.5;
static constexpr double kRestoreGap = 0.25;

enum class Step : uint8_t
{
Wait,
Strip,
Restore,
};

// An armor piece taken off, and the biped slot mask it occupies (TESObjectARMO::slotType).
struct Piece
{
uint32_t Id = 0;
uint32_t Slots = 0;
};

GearRedraw() = default;
GearRedraw(const uint32_t aEntity, const uint32_t aOwnershipEpoch) noexcept
: Entity(aEntity)
, OwnershipEpoch(aOwnershipEpoch)
{
}

/**
* @brief Adds the frame time and says what is due. One step per call, so a long frame cannot run both halves
* in one update.
*/
Step Advance(const double acDelta) noexcept
{
Timer += acDelta;

if (!Stripped)
return Timer >= kStripAt ? Step::Strip : Step::Wait;

return Timer - StrippedAt >= kRestoreGap ? Step::Restore : Step::Wait;
}

void MarkStripped(std::vector<Piece> aArmor) noexcept
{
Stripped = true;
StrippedAt = Timer;
Armor = std::move(aArmor);
}

/**
* @brief Drops an item the owner equipped or unequipped while the armor was off.
*
* Before the strip there is nothing to drop: the strip takes whatever is worn at that moment, which already
* includes the change.
*/
void Forget(const uint32_t acItemId) noexcept
{
if (Stripped)
Armor.erase(std::remove_if(Armor.begin(), Armor.end(), [acItemId](const Piece& acPiece) { return acPiece.Id == acItemId; }), Armor.end());
}

/**
* @brief The pieces to put back, given the slots the body wears now.
*
* A piece whose slots are now taken was replaced by the owner while it was off (boots A came off, the owner
* put on boots B), so it goes into aSkipped instead: equipping it would take B off again.
*/
std::vector<uint32_t> Restorable(const uint32_t acWornSlots, std::vector<uint32_t>& aSkipped) const noexcept
{
std::vector<uint32_t> restore;
for (const Piece& cPiece : Armor)
{
if (cPiece.Slots & acWornSlots)
aSkipped.push_back(cPiece.Id);
else
restore.push_back(cPiece.Id);
}

return restore;
}

// The hand item passes strip and restore the same armor, so they wait while it is off.
bool HoldsHandPasses() const noexcept { return Stripped; }

uint32_t Entity = 0;
uint32_t OwnershipEpoch = 0;
double Timer = 0.0;
double StrippedAt = 0.0;
bool Stripped = false;
std::vector<Piece> Armor{};
};
Loading
Loading