mirror of
https://github.com/Jous99/F4MP.git
synced 2026-01-13 00:20:54 +01:00
432 lines
14 KiB
C++
432 lines
14 KiB
C++
|
|
#include <Services/ObjectService.h>
|
||
|
|
|
||
|
|
#include <World.h>
|
||
|
|
#include <Events/DisconnectedEvent.h>
|
||
|
|
#include <Events/UpdateEvent.h>
|
||
|
|
#include <Events/CellChangeEvent.h>
|
||
|
|
#include <Events/ActivateEvent.h>
|
||
|
|
#include <Events/LockChangeEvent.h>
|
||
|
|
#include <Events/ScriptAnimationEvent.h>
|
||
|
|
#include <Messages/ServerTimeSettings.h>
|
||
|
|
#include <Messages/AssignObjectsRequest.h>
|
||
|
|
#include <Messages/AssignObjectsResponse.h>
|
||
|
|
#include <Messages/ActivateRequest.h>
|
||
|
|
#include <Messages/NotifyActivate.h>
|
||
|
|
#include <Messages/LockChangeRequest.h>
|
||
|
|
#include <Messages/NotifyLockChange.h>
|
||
|
|
#include <Messages/ScriptAnimationRequest.h>
|
||
|
|
#include <Messages/NotifyScriptAnimation.h>
|
||
|
|
|
||
|
|
#include <PlayerCharacter.h>
|
||
|
|
#include <Forms/TESObjectCELL.h>
|
||
|
|
#include <Forms/TESWorldSpace.h>
|
||
|
|
#include <Forms/BGSEncounterZone.h>
|
||
|
|
|
||
|
|
#include <inttypes.h>
|
||
|
|
|
||
|
|
ObjectService::ObjectService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport)
|
||
|
|
: m_world(aWorld)
|
||
|
|
, m_transport(aTransport)
|
||
|
|
{
|
||
|
|
m_disconnectedConnection = aDispatcher.sink<DisconnectedEvent>().connect<&ObjectService::OnDisconnected>(this);
|
||
|
|
m_cellChangeConnection = aDispatcher.sink<CellChangeEvent>().connect<&ObjectService::OnCellChange>(this);
|
||
|
|
m_onActivateConnection = aDispatcher.sink<ActivateEvent>().connect<&ObjectService::OnActivate>(this);
|
||
|
|
m_activateConnection = aDispatcher.sink<NotifyActivate>().connect<&ObjectService::OnActivateNotify>(this);
|
||
|
|
m_lockChangeConnection = aDispatcher.sink<LockChangeEvent>().connect<&ObjectService::OnLockChange>(this);
|
||
|
|
m_lockChangeNotifyConnection = aDispatcher.sink<NotifyLockChange>().connect<&ObjectService::OnLockChangeNotify>(this);
|
||
|
|
m_assignObjectConnection = aDispatcher.sink<AssignObjectsResponse>().connect<&ObjectService::OnAssignObjectsResponse>(this);
|
||
|
|
m_scriptAnimationConnection = aDispatcher.sink<ScriptAnimationEvent>().connect<&ObjectService::OnScriptAnimationEvent>(this);
|
||
|
|
m_scriptAnimationNotifyConnection = aDispatcher.sink<NotifyScriptAnimation>().connect<&ObjectService::OnNotifyScriptAnimation>(this);
|
||
|
|
|
||
|
|
EventDispatcherManager::Get()->activateEvent.RegisterSink(this);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool IsPlayerHome(const TESObjectCELL* pCell) noexcept
|
||
|
|
{
|
||
|
|
if (pCell && pCell->loadedCellData && pCell->loadedCellData->encounterZone)
|
||
|
|
{
|
||
|
|
// Only return true if cell has the NoResetZone encounter zone
|
||
|
|
if (pCell->loadedCellData->encounterZone->formID == 0xf90b1)
|
||
|
|
{
|
||
|
|
switch (pCell->formID)
|
||
|
|
{
|
||
|
|
case 0xeec55: // one known exception: Sinderion's Field Lab
|
||
|
|
return false;
|
||
|
|
default: return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ShouldSyncObject(const TESObjectREFR* apObject) noexcept
|
||
|
|
{
|
||
|
|
if (!apObject)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
switch (apObject->formID)
|
||
|
|
{
|
||
|
|
case 0x39CF1: // Don't sync the chest in the "Diplomatic Immunity" quest
|
||
|
|
return false;
|
||
|
|
case 0x3EF03: // ...as well as in the "No One Escapes Cidhna Mine" quest
|
||
|
|
return false;
|
||
|
|
default:
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void ObjectService::OnDisconnected(const DisconnectedEvent&) noexcept
|
||
|
|
{
|
||
|
|
// TODO(cosideci): clear object components
|
||
|
|
}
|
||
|
|
|
||
|
|
void ObjectService::OnCellChange(const CellChangeEvent& acEvent) noexcept
|
||
|
|
{
|
||
|
|
if (!m_transport.IsConnected())
|
||
|
|
return;
|
||
|
|
|
||
|
|
PlayerCharacter* pPlayer = PlayerCharacter::Get();
|
||
|
|
TESObjectCELL* pCell = pPlayer->parentCell;
|
||
|
|
|
||
|
|
// Player homes should not be synced, so that chest contents,
|
||
|
|
// which are often used as storage, are never accidentally wiped.
|
||
|
|
if (!World::Get().GetServerSettings().SyncPlayerHomes && IsPlayerHome(pCell))
|
||
|
|
return;
|
||
|
|
|
||
|
|
GameId cellId{};
|
||
|
|
if (!m_world.GetModSystem().GetServerModId(pCell->formID, cellId))
|
||
|
|
{
|
||
|
|
spdlog::error("Server cell id not found for cell form id {:X}", pCell->formID);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
GameId worldSpaceId{};
|
||
|
|
if (TESWorldSpace* pWorldSpace = pPlayer->GetWorldSpace())
|
||
|
|
{
|
||
|
|
if (!m_world.GetModSystem().GetServerModId(pWorldSpace->formID, worldSpaceId))
|
||
|
|
{
|
||
|
|
spdlog::error("Server world space id not found for world space form id {:X}", pWorldSpace->formID);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Vector<FormType> formTypes = {FormType::Container, FormType::Door};
|
||
|
|
// Door seemed to be at the wrong form id (29, now 32), verify this.
|
||
|
|
Vector<TESObjectREFR*> objects = pCell->GetRefsByFormTypes(formTypes);
|
||
|
|
|
||
|
|
AssignObjectsRequest request{};
|
||
|
|
|
||
|
|
for (TESObjectREFR* pObject : objects)
|
||
|
|
{
|
||
|
|
if (!ShouldSyncObject(pObject))
|
||
|
|
{
|
||
|
|
spdlog::warn("Excluding sync for {:X}", pObject->formID);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
ObjectData objectData{};
|
||
|
|
objectData.CellId = cellId;
|
||
|
|
objectData.WorldSpaceId = worldSpaceId;
|
||
|
|
objectData.CurrentCoords = GridCellCoords::CalculateGridCellCoords(pObject->position.x, pObject->position.y);
|
||
|
|
|
||
|
|
if (!m_world.GetModSystem().GetServerModId(pObject->formID, objectData.Id))
|
||
|
|
{
|
||
|
|
spdlog::error("Server form id not found for object with form id {:X}", pObject->formID);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Lock* pLock = pObject->GetLock())
|
||
|
|
{
|
||
|
|
objectData.CurrentLockData.IsLocked = pLock->IsLocked();
|
||
|
|
objectData.CurrentLockData.LockLevel = pLock->lockLevel;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (pObject->baseForm->formType == FormType::Container)
|
||
|
|
objectData.CurrentInventory = pObject->GetInventory();
|
||
|
|
|
||
|
|
request.Objects.push_back(objectData);
|
||
|
|
}
|
||
|
|
|
||
|
|
m_transport.Send(request);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ObjectService::OnAssignObjectsResponse(const AssignObjectsResponse& acMessage) noexcept
|
||
|
|
{
|
||
|
|
for (const ObjectData& objectData : acMessage.Objects)
|
||
|
|
{
|
||
|
|
const uint32_t cObjectId = World::Get().GetModSystem().GetGameId(objectData.Id);
|
||
|
|
TESObjectREFR* pObject = Cast<TESObjectREFR>(TESForm::GetById(cObjectId));
|
||
|
|
if (!pObject)
|
||
|
|
{
|
||
|
|
spdlog::error("Object not found for form id {:X}", objectData.Id);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
CreateObjectEntity(pObject->formID, objectData.ServerId);
|
||
|
|
|
||
|
|
if (objectData.IsSenderFirst)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
if (objectData.CurrentLockData != LockData{})
|
||
|
|
{
|
||
|
|
Lock* pLock = pObject->GetLock();
|
||
|
|
|
||
|
|
if (!pLock)
|
||
|
|
{
|
||
|
|
pLock = pObject->CreateLock();
|
||
|
|
if (!pLock)
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
pLock->lockLevel = objectData.CurrentLockData.LockLevel;
|
||
|
|
pLock->SetLock(objectData.CurrentLockData.IsLocked);
|
||
|
|
pObject->LockChange();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (pObject->baseForm->formType == FormType::Container)
|
||
|
|
{
|
||
|
|
Inventory currentInventory = pObject->GetInventory();
|
||
|
|
|
||
|
|
if (currentInventory.ContainsQuestItems())
|
||
|
|
pObject->SetInventoryRetainingQuestItems(currentInventory, objectData.CurrentInventory);
|
||
|
|
else
|
||
|
|
pObject->SetInventory(objectData.CurrentInventory);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
entt::entity ObjectService::CreateObjectEntity(const uint32_t acFormId, const uint32_t acServerId) noexcept
|
||
|
|
{
|
||
|
|
const auto view = m_world.view<FormIdComponent, ObjectComponent>();
|
||
|
|
|
||
|
|
auto it = std::find_if(view.begin(), view.end(), [acServerId, view](entt::entity entity) { return view.get<ObjectComponent>(entity).Id == acServerId; });
|
||
|
|
|
||
|
|
if (it != view.end())
|
||
|
|
return *it;
|
||
|
|
|
||
|
|
entt::entity entity = m_world.create();
|
||
|
|
spdlog::info("Created object entity, server id: {:X}, form id {:X}", acServerId, acFormId);
|
||
|
|
|
||
|
|
m_world.emplace<FormIdComponent>(entity, acFormId);
|
||
|
|
m_world.emplace<ObjectComponent>(entity, acServerId);
|
||
|
|
|
||
|
|
return entity;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ObjectService::OnActivate(const ActivateEvent& acEvent) noexcept
|
||
|
|
{
|
||
|
|
if (acEvent.ActivateFlag)
|
||
|
|
{
|
||
|
|
acEvent.pObject->Activate(acEvent.pActivator, acEvent.Unk1, acEvent.pObjectToGet, acEvent.Count, acEvent.DefaultProcessing);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!m_transport.IsConnected())
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (Lock* pLock = acEvent.pObject->GetLock())
|
||
|
|
{
|
||
|
|
if (pLock->flags & 0xFF)
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
ActivateRequest request;
|
||
|
|
|
||
|
|
if (!m_world.GetModSystem().GetServerModId(acEvent.pObject->formID, request.Id))
|
||
|
|
{
|
||
|
|
spdlog::error("Server form id not found for object form id {:X}", acEvent.pObject->formID);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
TESObjectCELL* pCell = acEvent.pObject->GetParentCellEx();
|
||
|
|
if (!pCell)
|
||
|
|
{
|
||
|
|
spdlog::error("Activated object has no parent cell: {:X}", acEvent.pObject->formID);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!m_world.GetModSystem().GetServerModId(pCell->formID, request.CellId))
|
||
|
|
{
|
||
|
|
spdlog::error("Server cell id not found for cell form id {:X}", acEvent.pObject->parentCell->formID);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
auto view = m_world.view<FormIdComponent>();
|
||
|
|
const auto pEntity = std::find_if(std::begin(view), std::end(view), [id = acEvent.pActivator->formID, view](entt::entity entity) { return view.get<FormIdComponent>(entity).Id == id; });
|
||
|
|
|
||
|
|
if (pEntity == std::end(view))
|
||
|
|
{
|
||
|
|
// spdlog::error("Activator entity not found for form id {:X}", acEvent.pActivator->formID);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<uint32_t> serverIdRes = Utils::GetServerId(*pEntity);
|
||
|
|
if (!serverIdRes.has_value())
|
||
|
|
return;
|
||
|
|
|
||
|
|
request.ActivatorId = serverIdRes.value();
|
||
|
|
request.PreActivationOpenState = acEvent.PreActivationOpenState;
|
||
|
|
|
||
|
|
m_transport.Send(request);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ObjectService::OnActivateNotify(const NotifyActivate& acMessage) noexcept
|
||
|
|
{
|
||
|
|
Actor* pActor = Utils::GetByServerId<Actor>(acMessage.ActivatorId);
|
||
|
|
if (!pActor)
|
||
|
|
{
|
||
|
|
spdlog::error("{}: could not find actor server id {:X}", __FUNCTION__, acMessage.ActivatorId);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const uint32_t cObjectId = World::Get().GetModSystem().GetGameId(acMessage.Id);
|
||
|
|
TESObjectREFR* pObject = Cast<TESObjectREFR>(TESForm::GetById(cObjectId));
|
||
|
|
if (!pObject)
|
||
|
|
{
|
||
|
|
spdlog::error("Failed to retrieve object to activate.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (pObject->baseForm->formType == FormType::Door)
|
||
|
|
{
|
||
|
|
auto remotePreActivationState = static_cast<TESObjectREFR::OpenState>(acMessage.PreActivationOpenState);
|
||
|
|
TESObjectREFR::OpenState localState = pObject->GetOpenState();
|
||
|
|
|
||
|
|
if (remotePreActivationState != localState)
|
||
|
|
{
|
||
|
|
// The doors are unsynced at this point. If we'll Activate the one on our side
|
||
|
|
// it'll just continue to be unsynced (open remotely, closed locally and vice versa)
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// unsure if these flags are the best, but these are passed with the papyrus Activate fn
|
||
|
|
// might be an idea to have the client send the flags through NotifyActivate
|
||
|
|
pObject->Activate(pActor, 0, nullptr, 1, 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ObjectService::OnLockChange(const LockChangeEvent& acEvent) noexcept
|
||
|
|
{
|
||
|
|
if (!m_transport.IsConnected())
|
||
|
|
return;
|
||
|
|
|
||
|
|
LockChangeRequest request;
|
||
|
|
|
||
|
|
if (!m_world.GetModSystem().GetServerModId(acEvent.FormId, request.Id))
|
||
|
|
{
|
||
|
|
spdlog::error("Server form id for lock object not found, form id: {:X}", acEvent.FormId);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const auto* const pObject = Cast<TESObjectREFR>(TESForm::GetById(acEvent.FormId));
|
||
|
|
|
||
|
|
TESObjectCELL* pCell = pObject->GetParentCellEx();
|
||
|
|
if (!pCell)
|
||
|
|
{
|
||
|
|
spdlog::error("Activated object has no parent cell: {:X}", pObject->formID);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!m_world.GetModSystem().GetServerModId(pCell->formID, request.CellId))
|
||
|
|
{
|
||
|
|
spdlog::error("Server cell id for cell not found, cell form id: {:X}", pObject->parentCell->formID);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
request.IsLocked = acEvent.IsLocked;
|
||
|
|
request.LockLevel = acEvent.LockLevel;
|
||
|
|
|
||
|
|
m_transport.Send(request);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ObjectService::OnLockChangeNotify(const NotifyLockChange& acMessage) noexcept
|
||
|
|
{
|
||
|
|
const auto cObjectId = World::Get().GetModSystem().GetGameId(acMessage.Id);
|
||
|
|
if (cObjectId == 0)
|
||
|
|
{
|
||
|
|
spdlog::error("Failed to retrieve object id to (un)lock.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
auto* pObject = Cast<TESObjectREFR>(TESForm::GetById(cObjectId));
|
||
|
|
if (!pObject)
|
||
|
|
{
|
||
|
|
spdlog::error("Failed to retrieve object to (un)lock.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
auto* pLock = pObject->GetLock();
|
||
|
|
|
||
|
|
if(!acMessage.IsLocked)
|
||
|
|
{
|
||
|
|
if (!pLock || !pLock->IsLocked())
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!pLock && acMessage.IsLocked)
|
||
|
|
{
|
||
|
|
pLock = pObject->CreateLock();
|
||
|
|
if (!pLock)
|
||
|
|
{
|
||
|
|
spdlog::error("Failed to create lock for object form id {:X}", pObject->formID);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pLock->lockLevel = acMessage.LockLevel;
|
||
|
|
pLock->SetLock(acMessage.IsLocked);
|
||
|
|
pObject->LockChange();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ObjectService::OnScriptAnimationEvent(const ScriptAnimationEvent& acEvent) noexcept
|
||
|
|
{
|
||
|
|
ScriptAnimationRequest request{};
|
||
|
|
request.FormID = acEvent.FormID;
|
||
|
|
request.Animation = acEvent.Animation;
|
||
|
|
request.EventName = acEvent.EventName;
|
||
|
|
|
||
|
|
m_transport.Send(request);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ObjectService::OnNotifyScriptAnimation(const NotifyScriptAnimation& acMessage) noexcept
|
||
|
|
{
|
||
|
|
if (acMessage.FormID == 0)
|
||
|
|
return;
|
||
|
|
|
||
|
|
auto* pForm = TESForm::GetById(acMessage.FormID);
|
||
|
|
auto* pObject = Cast<TESObjectREFR>(pForm);
|
||
|
|
|
||
|
|
if (!pObject)
|
||
|
|
{
|
||
|
|
spdlog::error("Failed to fetch notify script animation object, form id: {:X}", acMessage.FormID);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
BSFixedString eventName(acMessage.EventName.c_str());
|
||
|
|
if (acMessage.Animation == String{})
|
||
|
|
{
|
||
|
|
pObject->PlayAnimation(&eventName);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
BSFixedString animation(acMessage.Animation.c_str());
|
||
|
|
pObject->PlayAnimationAndWait(&animation, &eventName);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
BSTEventResult ObjectService::OnEvent(const TESActivateEvent* acEvent, const EventDispatcher<TESActivateEvent>* aDispatcher)
|
||
|
|
{
|
||
|
|
#if ENVIRONMENT_DEBUG
|
||
|
|
auto view = m_world.view<ObjectComponent>();
|
||
|
|
|
||
|
|
const auto itor = std::find_if(std::begin(view), std::end(view), [id = acEvent->object->formID, view](entt::entity entity) { return view.get<ObjectComponent>(entity).Id == id; });
|
||
|
|
|
||
|
|
if (itor == std::end(view))
|
||
|
|
{
|
||
|
|
AddObjectComponent(acEvent->object);
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
|
||
|
|
return BSTEventResult::kOk;
|
||
|
|
}
|