1+ #include < TiltedOnlinePCH.h>
2+
13#include < Games/References.h>
24#include < Games/Skyrim/EquipManager.h>
35#include < AI/AIProcess.h>
3133#include < Games/Skyrim/Misc/InventoryEntry.h>
3234#include < Games/Skyrim/ExtraData/ExtraCount.h>
3335#include < Games/Misc/ActorKnowledge.h>
36+ #include < Games/Skyrim/TESObjectREFR.h>
37+
38+ #include < optional>
3439
3540#include < ExtraData/ExtraDataList.h>
3641#include < ExtraData/ExtraCharge.h>
5560
5661#include < ModCompat/BehaviorVar.h>
5762
63+ namespace
64+ {
65+ constexpr float kDropSearchRadiusSquared = 200 .0f * 200 .0f ;
66+ using DropHandleMap = TiltedPhoques::Map<uint32_t , TiltedPhoques::Map<uint32_t , uint32_t >>;
67+ DropHandleMap s_actorDropHandles;
68+ TiltedPhoques::Map<uint32_t , uint32_t > s_actorNextDropId;
69+
70+ TESObjectREFR* FindDroppedReferenceNear (const TESBoundObject* apObject, const NiPoint3& acCenter)
71+ {
72+ if (!apObject)
73+ return nullptr ;
74+
75+ TES* pTes = TES::Get ();
76+ if (!pTes || !pTes->cells || !pTes->cells ->arr )
77+ return nullptr ;
78+
79+ const int dimension = pTes->cells ->dimension ;
80+ if (dimension <= 0 )
81+ return nullptr ;
82+
83+ auto evaluateCell = [&](TESObjectCELL* apCell, TESObjectREFR*& apBestMatch, float & aBestDistanceSq) {
84+ if (!apCell || !apCell->IsValid ())
85+ return ;
86+
87+ auto * pReferences = apCell->refData .refArray ;
88+ if (!pReferences)
89+ return ;
90+
91+ const uint32_t referenceCount = apCell->refData .Count ();
92+ for (uint32_t j = 0 ; j < referenceCount; ++j)
93+ {
94+ TESObjectREFR* pCandidate = pReferences[j].Get ();
95+ if (!pCandidate || pCandidate->baseForm != apObject || pCandidate->formType == Actor::Type)
96+ continue ;
97+
98+ const float diffX = pCandidate->position .x - acCenter.x ;
99+ const float diffY = pCandidate->position .y - acCenter.y ;
100+ const float diffZ = pCandidate->position .z - acCenter.z ;
101+ const float distanceSq = diffX * diffX + diffY * diffY + diffZ * diffZ;
102+
103+ if (distanceSq < aBestDistanceSq)
104+ {
105+ aBestDistanceSq = distanceSq;
106+ apBestMatch = pCandidate;
107+ }
108+ }
109+ };
110+
111+ TESObjectREFR* pClosest = nullptr ;
112+ float closestDistanceSq = kDropSearchRadiusSquared ;
113+
114+ const int cellCount = dimension * dimension;
115+ for (int i = 0 ; i < cellCount; ++i)
116+ {
117+ TESObjectCELL* pCell = pTes->cells ->arr [i];
118+ evaluateCell (pCell, pClosest, closestDistanceSq);
119+ }
120+
121+ if (!pClosest)
122+ evaluateCell (pTes->interiorCell , pClosest, closestDistanceSq);
123+
124+ return pClosest;
125+ }
126+ } // namespace
127+
128+ namespace DropSync
129+ {
130+ thread_local std::optional<uint32_t > PendingDropId{};
131+ thread_local uint32_t PendingDropActorFormId = 0 ;
132+ } // namespace DropSync
133+
134+ uint32_t Actor::RegisterLocalDrop (uint32_t aActorFormId, uint32_t aHandleBits) noexcept
135+ {
136+ if (aHandleBits == 0 )
137+ return 0 ;
138+
139+ auto & nextId = s_actorNextDropId[aActorFormId];
140+ uint32_t dropId = ++nextId;
141+ s_actorDropHandles[aActorFormId][dropId] = aHandleBits;
142+ return dropId;
143+ }
144+
145+ void Actor::TrackRemoteDrop (uint32_t aActorFormId, uint32_t aDropId, uint32_t aHandleBits) noexcept
146+ {
147+ if (aDropId == 0 || aHandleBits == 0 )
148+ return ;
149+
150+ s_actorDropHandles[aActorFormId][aDropId] = aHandleBits;
151+ auto & nextId = s_actorNextDropId[aActorFormId];
152+ if (aDropId > nextId)
153+ nextId = aDropId;
154+ }
155+
156+ std::optional<uint32_t > Actor::ConsumeTrackedDrop (uint32_t aActorFormId, uint32_t aDropId) noexcept
157+ {
158+ if (aDropId == 0 )
159+ return std::nullopt ;
160+
161+ if (s_actorDropHandles.find (aActorFormId) == s_actorDropHandles.end ())
162+ return std::nullopt ;
163+
164+ auto & bucket = s_actorDropHandles.at (aActorFormId);
165+ auto handleIt = bucket.find (aDropId);
166+ if (handleIt == bucket.end ())
167+ return std::nullopt ;
168+
169+ uint32_t handleBits = handleIt->second ;
170+ bucket.erase (handleIt);
171+ if (bucket.empty ())
172+ s_actorDropHandles.erase (aActorFormId);
173+
174+ return handleBits;
175+ }
176+
177+ std::optional<uint32_t > Actor::ConsumeTrackedDropByHandle (uint32_t aActorFormId, uint32_t aHandleBits) noexcept
178+ {
179+ if (aHandleBits == 0 )
180+ return std::nullopt ;
181+
182+ if (s_actorDropHandles.find (aActorFormId) == s_actorDropHandles.end ())
183+ return std::nullopt ;
184+
185+ auto & bucket = s_actorDropHandles.at (aActorFormId);
186+ for (auto it = bucket.begin (); it != bucket.end (); ++it)
187+ {
188+ if (it->second == aHandleBits)
189+ {
190+ uint32_t dropId = it->first ;
191+ bucket.erase (dropId);
192+ if (bucket.empty ())
193+ s_actorDropHandles.erase (aActorFormId);
194+ return dropId;
195+ }
196+ }
197+
198+ return std::nullopt ;
199+ }
200+
58201#ifdef SAVE_STUFF
59202
60203#include < Games/Skyrim/SaveLoad.h>
@@ -1098,7 +1241,19 @@ void* TP_MAKE_THISCALL(HookPickUpObject, Actor, TESObjectREFR* apObject, int32_t
10981241 // The inventory change event should always be sent to the server, otherwise the server inventory won't be updated.
10991242 bool shouldUpdateClients = apObject->IsTemporary () && !ScopedActivateOverride::IsOverriden ();
11001243
1101- World::Get ().GetRunner ().Trigger (InventoryChangeEvent (apThis->formID , std::move (item), false , shouldUpdateClients));
1244+ std::optional<NiPoint3> pickupLocation{};
1245+ std::optional<NiPoint3> pickupRotation{};
1246+ std::optional<uint32_t > dropInstanceId{};
1247+ if (apObject)
1248+ {
1249+ pickupLocation.emplace (apObject->position );
1250+ pickupRotation.emplace (apObject->rotation );
1251+ auto handle = apObject->GetHandle ();
1252+ if (handle && handle.handle .iBits )
1253+ dropInstanceId = Actor::ConsumeTrackedDropByHandle (apThis->formID , handle.handle .iBits );
1254+ }
1255+
1256+ World::Get ().GetRunner ().Trigger (InventoryChangeEvent (apThis->formID , std::move (item), false , shouldUpdateClients, std::move (pickupLocation), std::move (pickupRotation), dropInstanceId));
11021257 }
11031258
11041259 return TiltedPhoques::ThisCall (RealPickUpObject, apThis, apObject, aCount, aUnk1, aUnk2);
@@ -1120,11 +1275,66 @@ void* TP_MAKE_THISCALL(HookDropObject, Actor, void* apResult, TESBoundObject* ap
11201275 if (apExtraData)
11211276 apThis->GetItemFromExtraData (item, apExtraData);
11221277
1123- World::Get ().GetRunner ().Trigger (InventoryChangeEvent (apThis->formID , std::move (item), true ));
1278+ const bool shouldSend = !ScopedInventoryOverride::IsOverriden ();
1279+
1280+ void * pReturn = nullptr ;
1281+ {
1282+ ScopedInventoryOverride _;
1283+ pReturn = TiltedPhoques::ThisCall (RealDropObject, apThis, apResult, apObject, apExtraData, aCount, apLocation, apRotation);
1284+ }
1285+
1286+ uint32_t handleBits = 0 ;
1287+ TESObjectREFR* pDroppedRef = nullptr ;
1288+ if (auto * pHandle = static_cast <BSPointerHandle<TESObjectREFR>*>(apResult); pHandle && *pHandle)
1289+ {
1290+ handleBits = pHandle->handle .iBits ;
1291+ if (handleBits)
1292+ pDroppedRef = TESObjectREFR::GetByHandle (handleBits);
1293+ }
1294+
1295+ if (DropSync::PendingDropId)
1296+ {
1297+ if (handleBits)
1298+ Actor::TrackRemoteDrop (DropSync::PendingDropActorFormId, *DropSync::PendingDropId, handleBits);
1299+ DropSync::PendingDropId.reset ();
1300+ DropSync::PendingDropActorFormId = 0 ;
1301+ }
1302+
1303+ std::optional<NiPoint3> dropLocation{};
1304+ std::optional<NiPoint3> dropRotation{};
1305+
1306+ if (pDroppedRef)
1307+ {
1308+ dropLocation.emplace (pDroppedRef->position );
1309+ dropRotation.emplace (pDroppedRef->rotation );
1310+ }
1311+
1312+ if (!dropLocation)
1313+ {
1314+ if (apLocation)
1315+ dropLocation.emplace (*apLocation);
1316+ else
1317+ dropLocation.emplace (apThis->position );
1318+ }
1319+
1320+ if (!dropRotation)
1321+ {
1322+ if (apRotation)
1323+ dropRotation.emplace (*apRotation);
1324+ else
1325+ dropRotation.emplace (apThis->rotation );
1326+ }
1327+
1328+ std::optional<uint32_t > dropInstanceId{};
1329+ if (shouldSend && handleBits)
1330+ dropInstanceId = Actor::RegisterLocalDrop (apThis->formID , handleBits);
11241331
1125- ScopedInventoryOverride _;
1332+ if (shouldSend)
1333+ {
1334+ World::Get ().GetRunner ().Trigger (InventoryChangeEvent (apThis->formID , std::move (item), true , true , std::move (dropLocation), std::move (dropRotation), dropInstanceId));
1335+ }
11261336
1127- return TiltedPhoques::ThisCall (RealDropObject, apThis, apResult, apObject, apExtraData, aCount, apLocation, apRotation) ;
1337+ return pReturn ;
11281338}
11291339
11301340void Actor::DropOrPickUpObject (const Inventory::Entry& arEntry, NiPoint3* apLocation, NiPoint3* apRotation) noexcept
@@ -1143,7 +1353,23 @@ void Actor::DropOrPickUpObject(const Inventory::Entry& arEntry, NiPoint3* apLoca
11431353
11441354 if (arEntry.Count < 0 )
11451355 DropObject (pObject, pExtraData, -arEntry.Count , apLocation, apRotation);
1146- // TODO: pick up
1356+ else if (arEntry.Count > 0 )
1357+ {
1358+ NiPoint3 searchLocation = apLocation ? *apLocation : position;
1359+ TESObjectREFR* pDroppedRef = FindDroppedReferenceNear (pObject, searchLocation);
1360+
1361+ if (!pDroppedRef && apLocation)
1362+ pDroppedRef = FindDroppedReferenceNear (pObject, position);
1363+
1364+ if (!pDroppedRef)
1365+ {
1366+ spdlog::warn (" Object to pick up not found near target location, {:X}:{:X}." , arEntry.BaseId .ModId , arEntry.BaseId .BaseId );
1367+ return ;
1368+ }
1369+
1370+ spdlog::debug (" Picking up object, form id: {:X}, count: {}, actor: {:X}" , pObject->formID , arEntry.Count , formID);
1371+ PickUpObject (pDroppedRef, arEntry.Count , false , 0 .0f );
1372+ }
11471373}
11481374
11491375void Actor::DropObject (TESBoundObject* apObject, ExtraDataList* apExtraData, int32_t aCount, NiPoint3* apLocation, NiPoint3* apRotation) noexcept
0 commit comments