1

Moved most synchronous operations to DLL; decoded Formlist.Revert and FormList.RemoveAddedForm

This commit is contained in:
Eddoursul 2022-06-27 13:38:49 +02:00
parent bb427cc419
commit a7229e0bea
25 changed files with 710 additions and 684 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -23,8 +23,9 @@ configure_file(
set(sources
src/Main.cpp
src/Papyrus.cpp
src/ArtifactTracker.cpp
src/EventListener.cpp
src/BookCheck.cpp
src/MiscCheck.cpp
${CMAKE_CURRENT_BINARY_DIR}/version.rc)

View File

@ -0,0 +1,321 @@
#include "ArtifactTracker.h"
#include "BookCheck.h"
#include "EventListener.h"
#include "Util.h"
namespace ArtifactTracker
{
RE::TESGlobal* notifyNewArtifact;
RE::TESBoundObject* cellContainer;
RE::BGSListForm* listNew;
RE::BGSListForm* listStored;
RE::BGSListForm* listFound;
RE::BGSListForm* persistentStorage;
RE::BGSKeyword* homeKeyword;
bool bAtHome;
std::unordered_map<RE::FormID, RE::TESObjectMISC*> validMisc;
std::unordered_map<RE::FormID, RE::TESObjectBOOK*> validBooks;
void Init()
{
EventListener::Install();
const auto dataHandler = RE::TESDataHandler::GetSingleton();
notifyNewArtifact = dataHandler->LookupForm<RE::TESGlobal>(0x809, "Artifact Tracker.esp");
cellContainer = dataHandler->LookupForm(0x800, "Artifact Tracker.esp")->As<RE::TESBoundObject>();
listNew = dataHandler->LookupForm<RE::BGSListForm>(0x803, "Artifact Tracker.esp");
listStored = dataHandler->LookupForm<RE::BGSListForm>(0x805, "Artifact Tracker.esp");
listFound = dataHandler->LookupForm<RE::BGSListForm>(0x806, "Artifact Tracker.esp");
persistentStorage = dataHandler->LookupForm<RE::BGSListForm>(0x807, "Artifact Tracker.esp");
homeKeyword = dataHandler->LookupForm<RE::BGSKeyword>(0xFC1A3, "Skyrim.esm");
if (!cellContainer) {
SKSE::log::warn("cellContainer is empty");
}
if (!persistentStorage) {
SKSE::log::warn("persistentStorage is empty");
}
// Preloading item lists
const RE::BGSKeyword* recipeKeyword = dataHandler->LookupForm<RE::BGSKeyword>(0xF5CB0, "Skyrim.esm"); // VendorItemRecipe
for (const auto& form : dataHandler->GetFormArray<RE::TESObjectBOOK>()) {
if (form && !form->TeachesSpell() && (!form->HasKeyword(recipeKeyword) || BookCheck::IsBook(form))) {
validBooks[form->formID] = form;
}
}
RE::BGSListForm* excludeKeywords = dataHandler->LookupForm<RE::BGSListForm>(0x801, "Artifact Tracker.esp"); // ETR_ExcludeMiscKeywords
for (const auto& form : dataHandler->GetFormArray<RE::TESObjectMISC>()) {
if (form->GetPlayable() && !form->IsGold() && !form->IsLockpick() && !form->HasKeywordInList(excludeKeywords, false)) {
validMisc[form->formID] = form;
}
}
}
bool IsArtifact(RE::TESForm* a_form)
{
switch (a_form->GetFormType()) {
case RE::FormType::Armor:
return a_form->GetPlayable();
case RE::FormType::Weapon:
return a_form->GetPlayable() && a_form->formID != 0x000001F4;
case RE::FormType::Book:
return validBooks.contains(a_form->formID);
case RE::FormType::Misc:
return validMisc.contains(a_form->formID);
default:
return false;
}
}
bool SetHomeLocation(RE::BGSLocation* a_location = NULL)
{
bAtHome = a_location && a_location->HasKeyword(homeKeyword);
return bAtHome;
}
RE::TESObjectREFR* GetCellStorage(
RE::TESObjectREFR* a_ref,
RE::BGSListForm* a_refList,
RE::TESBoundObject* a_objectToCreate,
bool a_autoCreate)
{
RE::TESObjectREFR* result = NULL;
if (!a_ref || !a_refList || !a_objectToCreate) {
SKSE::log::warn("Invalid arguments in GetCellStorage");
return result;
}
RE::TESObjectCELL* cell = a_ref->GetParentCell();
a_refList->ForEachForm([&result, &cell, &a_objectToCreate](RE::TESForm& a_exform) {
const auto ref = a_exform.As<RE::TESObjectREFR>();
if (ref && ref->GetParentCell() == cell && ref->GetBaseObject()->formID == a_objectToCreate->formID) {
result = ref;
return false;
}
return true;
});
if (!result && a_autoCreate) {
result = a_ref->PlaceObjectAtMe(a_objectToCreate, true).get();
result->Disable();
a_refList->AddForm(result);
}
if (!result) {
SKSE::log::warn("Failed to find or create cell storage in GetCellStorage");
}
return result;
}
void SyncCellStorage(RE::TESObjectREFR* a_cellStorage, RE::BGSListForm* a_excludeContainers)
{
if (!a_cellStorage || !a_excludeContainers) {
SKSE::log::warn("Invalid arguments in SyncCellStorage");
return;
}
std::unordered_map<RE::FormID, bool> cellItems;
const auto cell = a_cellStorage->GetParentCell();
const auto inv = a_cellStorage->GetInventory();
for (const RE::NiPointer<RE::TESObjectREFR> a_ref : cell->references) {
const auto baseObj = a_ref->GetBaseObject();
if (baseObj->formType == RE::FormType::Container || (baseObj->formType == RE::FormType::NPC && !a_ref->IsDisabled() && baseObj->As<RE::TESNPC>()->GetRace()->formID == 0x0010760A)) {
if (a_excludeContainers->HasForm(a_ref->formID) || baseObj->formID == 0xDC9E7) { // skip persistent and PlayerBookShelfContainer
continue;
}
const auto contInv = a_ref->GetInventory([&](RE::TESBoundObject& a_object) -> bool {
return !cellItems.contains(a_object.formID);
});
for (const auto& [item, data] : contInv) {
if (data.first > 0) {
cellItems[item->formID] = true;
if (inv.find(item) == inv.end()) {
if (IsArtifact(item)) {
a_cellStorage->AddObjectToContainer(item, nullptr, 1, nullptr);
}
}
}
}
continue;
}
if (a_ref->IsMarkedForDeletion()) {
SKSE::log::info("found marked for deletion");
}
if (a_ref->IsDisabled() || a_ref->IsMarkedForDeletion()) {
continue;
}
if (cellItems.contains(baseObj->formID)) {
continue;
}
cellItems[baseObj->formID] = true;
if (!IsArtifact(baseObj)) {
continue;
}
if (inv.find(baseObj) == inv.end()) {
a_cellStorage->AddObjectToContainer(baseObj, nullptr, 1, nullptr);
}
}
for (const auto& [item, data] : inv) {
const auto& [count, entry] = data;
if (count > 0 && !cellItems.contains(item->formID)) {
a_cellStorage->RemoveItem(item, count, RE::ITEM_REMOVE_REASON::kRemove, nullptr, nullptr);
}
}
cellItems.clear();
}
void OnItemPickup(RE::TESBoundObject* form)
{
RE::TESObjectREFR* cellStorage = GetCellStorage(RE::PlayerCharacter::GetSingleton(), persistentStorage, cellContainer, true);
SyncCellStorage(cellStorage, persistentStorage);
if (!RefHasItem(cellStorage, form) && !GetItemCountInList(persistentStorage, form)) {
RemoveListItem(listStored, form);
listFound->AddForm(form);
}
}
void OnContainerChanged(const RE::TESContainerChangedEvent* a_event)
{
if (a_event->newContainer == 0x14) {
return;
RE::TESBoundObject* form = RE::TESForm::LookupByID<RE::TESBoundObject>(a_event->baseObj);
if (!form || !IsArtifact(form)) {
return;
}
if (listFound->HasForm(form) || listStored->HasForm(form)) {
return;
}
if (listNew->HasForm(form)) {
RemoveListItem(listNew, form);
listFound->AddForm(form);
if (notifyNewArtifact->value) {
const auto itemName = form->GetName();
char* notificationText = new char[strlen("New artifact acquired: ") + strlen(itemName) + 1];
strcpy(notificationText, "New artifact acquired: ");
strcat(notificationText, itemName);
RE::DebugNotification(notificationText);
delete[] notificationText;
}
}
} else if (a_event->oldContainer == 0x14) {
RE::TESBoundObject* form = RE::TESForm::LookupByID<RE::TESBoundObject>(a_event->baseObj);
SKSE::log::info("{}", form->GetName());
if (!form || !IsArtifact(form)) {
return;
}
if (!a_event->newContainer) {
if (bAtHome && a_event->reference) { // dropped at home
RE::TESObjectREFR* cellStorage = GetCellStorage(RE::PlayerCharacter::GetSingleton(), persistentStorage, cellContainer, true);
if (!RefHasItem(cellStorage, form)) {
cellStorage->AddObjectToContainer(form, nullptr, 1, nullptr);
}
if (listFound->HasForm(form)) {
RemoveListItem(listFound, form);
}
listStored->AddForm(form);
RE::DebugNotification("added to stored");
return;
}
if (listStored->HasForm(form)) {
return;
}
if (!RefHasItem(RE::PlayerCharacter::GetSingleton(), form) && !FollowersHaveItem(form)) {
if (listFound->HasForm(form)) {
RemoveListItem(listFound, form);
}
listNew->AddForm(form);
RE::DebugNotification("added to new");
}
return;
}
bool bPersistent = false;
for (const auto& ref : persistentStorage->forms) {
if (ref && ref->formID == a_event->newContainer) {
bPersistent = true;
break;
}
}
if (!bPersistent && !bAtHome) {
if (!RefHasItem(RE::PlayerCharacter::GetSingleton(), form) && !FollowersHaveItem(form->As<RE::TESBoundObject>())) {
if (listFound->HasForm(form)) {
RemoveListItem(listFound, form);
}
listNew->AddForm(form);
RE::DebugNotification("added to new");
}
return;
}
if (listStored->HasForm(form)) {
return;
}
if (!bPersistent && bAtHome) {
RE::TESObjectREFR* targetContainer = RE::TESForm::LookupByID<RE::TESObjectREFR>(a_event->newContainer);
if (!targetContainer || targetContainer->GetParentCell() != RE::PlayerCharacter::GetSingleton()->GetParentCell()) {
return;
}
if (targetContainer->GetBaseObject()->Is(RE::FormType::NPC) && targetContainer->GetBaseObject()->As<RE::TESNPC>()->GetRace()->formID != 0x0010760A) {
return;
}
RE::TESObjectREFR* cellStorage = GetCellStorage(RE::PlayerCharacter::GetSingleton(), persistentStorage, cellContainer, true);
cellStorage->AddObjectToContainer(form->As<RE::TESBoundObject>(), nullptr, 1, nullptr);
RE::DebugNotification("updated cellStorage");
}
if (listFound->HasForm(form)) {
RemoveListItem(listFound, form);
}
listStored->AddForm(form);
RE::DebugNotification("added to stored");
}
}
}

View File

@ -0,0 +1,29 @@
#pragma once
namespace ArtifactTracker
{
extern RE::TESGlobal* notifyNewArtifact;
extern RE::TESBoundObject* cellContainer;
extern RE::BGSListForm* listNew;
extern RE::BGSListForm* listStored;
extern RE::BGSListForm* listFound;
extern RE::BGSListForm* persistentStorage;
extern RE::BGSKeyword* homeKeyword;
extern bool bAtHome;
extern std::unordered_map<RE::FormID, RE::TESObjectMISC*> validMisc;
extern std::unordered_map<RE::FormID, RE::TESObjectBOOK*> validBooks;
void Init();
bool IsArtifact(RE::TESForm* a_item);
bool SetHomeLocation(RE::BGSLocation* a_location);
RE::TESObjectREFR* GetCellStorage(RE::TESObjectREFR* a_ref, RE::BGSListForm* a_refList, RE::TESBoundObject* a_objectToCreate, bool a_autoCreate = true);
void SyncCellStorage(RE::TESObjectREFR* a_cellStorage, RE::BGSListForm* a_excludeContainers);
void OnItemPickup(RE::TESBoundObject* form);
void OnContainerChanged(const RE::TESContainerChangedEvent* a_event);
}

View File

@ -3,33 +3,6 @@
// Mostly borrowed from Fix Note icon for SkyUI by 0xC0000005
namespace BookCheck
{
std::unordered_map<RE::FormID, RE::TESObjectBOOK*> validBooks;
void PreloadBookList()
{
const auto dataHandler = RE::TESDataHandler::GetSingleton();
if (!dataHandler) {
return;
}
const RE::BGSKeyword* recipeKeyword = dataHandler->LookupForm<RE::BGSKeyword>(0xF5CB0, "Skyrim.esm"); // VendorItemRecipe
for (const auto& form : dataHandler->GetFormArray<RE::TESObjectBOOK>()) {
if (!form || form->TeachesSpell()) {
continue;
}
if (form->HasKeyword(recipeKeyword) || IsBook(form)) {
validBooks[form->formID] = form;
}
}
}
std::unordered_map<RE::FormID, RE::TESObjectBOOK*> GetBookList()
{
return validBooks;
}
const char* ExtractFileName(const char* const path, size_t& fileNameLength)
{
if (!path) {

View File

@ -3,10 +3,6 @@
// Mostly borrowed from Fix Note icon for SkyUI by 0xC0000005
namespace BookCheck
{
void PreloadBookList();
std::unordered_map<RE::FormID, RE::TESObjectBOOK*> GetBookList();
const char* ExtractFileName(const char* const path, size_t& fileNameLength);
// Performs a case-sensitive search for 'needle' in the specified string,

View File

@ -0,0 +1,38 @@
#include "EventListener.h"
#include "ArtifactTracker.h"
auto EventListener::GetSingleton() -> EventListener*
{
static EventListener singleton{};
return std::addressof(singleton);
}
void EventListener::Install()
{
const auto eventSource = RE::ScriptEventSourceHolder::GetSingleton();
eventSource->AddEventSink<RE::TESContainerChangedEvent>(EventListener::GetSingleton());
eventSource->AddEventSink<RE::TESActorLocationChangeEvent>(EventListener::GetSingleton());
}
auto EventListener::ProcessEvent(
const RE::TESActorLocationChangeEvent* a_event,
RE::BSTEventSource<RE::TESActorLocationChangeEvent>* a_eventSource)
-> RE::BSEventNotifyControl
{
if (a_event->actor && a_event->actor->IsPlayerRef()) {
ArtifactTracker::SetHomeLocation(a_event->newLoc);
}
return RE::BSEventNotifyControl::kContinue;
}
auto EventListener::ProcessEvent(
const RE::TESContainerChangedEvent* a_event,
RE::BSTEventSource<RE::TESContainerChangedEvent>* a_eventSource)
-> RE::BSEventNotifyControl
{
ArtifactTracker::OnContainerChanged(a_event);
return RE::BSEventNotifyControl::kContinue;
}

View File

@ -0,0 +1,29 @@
#pragma once
class EventListener :
public RE::BSTEventSink<RE::TESActorLocationChangeEvent>,
public RE::BSTEventSink<RE::TESContainerChangedEvent>
{
public:
~EventListener() = default;
EventListener(const EventListener&) = delete;
EventListener& operator=(const EventListener&) = delete;
EventListener& operator=(EventListener&&) = delete;
static auto GetSingleton() -> EventListener*;
static void Install();
auto ProcessEvent(
const RE::TESActorLocationChangeEvent* a_event,
RE::BSTEventSource<RE::TESActorLocationChangeEvent>* a_eventSource)
-> RE::BSEventNotifyControl override;
auto ProcessEvent(
const RE::TESContainerChangedEvent* a_event,
RE::BSTEventSource<RE::TESContainerChangedEvent>* a_eventSource)
-> RE::BSEventNotifyControl override;
private:
EventListener() = default;
};

View File

@ -1,288 +0,0 @@
#pragma once
namespace Papyrus::ArtifactTracker
{
inline bool is_artifact(RE::TESForm* a_form)
{
switch (a_form->GetFormType()) {
case RE::FormType::Armor:
return a_form->GetPlayable();
case RE::FormType::Weapon:
return a_form->GetPlayable() && a_form->formID != 0x000001F4;
case RE::FormType::Book:
return BookCheck::GetBookList().contains(a_form->formID);
case RE::FormType::Misc:
return MiscCheck::GetMiscList().contains(a_form->formID);
default:
return false;
}
}
inline std::int32_t AddAllFormsToList(RE::StaticFunctionTag*,
RE::BGSListForm* a_targetList,
short a_formType,
RE::BGSListForm* a_storedList,
RE::BGSListForm* a_foundList)
{
const auto formType = static_cast<RE::FormType>(a_formType);
switch (formType) {
case RE::FormType::Book:
for (auto const& item : BookCheck::GetBookList()) {
if (!a_storedList->HasForm(item.second) && !a_foundList->HasForm(item.second)) {
a_targetList->AddForm(item.second);
}
}
break;
case RE::FormType::Misc:
for (auto const& item : MiscCheck::GetMiscList()) {
if (!a_storedList->HasForm(item.second) && !a_foundList->HasForm(item.second)) {
a_targetList->AddForm(item.second);
}
}
break;
case RE::FormType::None:
break;
default:
const auto dataHandler = RE::TESDataHandler::GetSingleton();
if (!dataHandler) {
return a_targetList->forms.size();
}
for (const auto& form : dataHandler->GetFormArray(formType)) {
if (!form || !form->GetPlayable()) {
continue;
}
if (a_storedList->HasForm(form) || a_foundList->HasForm(form)) {
continue;
}
a_targetList->AddForm(form);
}
}
return a_targetList->forms.size();
}
inline std::int32_t AddArtifactsToList(VM* a_vm, StackID a_stackID, RE::StaticFunctionTag*,
RE::TESForm* a_refOrList,
RE::BGSListForm* a_targetList,
RE::BGSListForm* a_excludeList = NULL)
{
if (!a_refOrList) {
a_vm->TraceStack("a_refOrList in AddArtifactsToList is None", a_stackID);
return 0;
}
if (!a_targetList) {
a_vm->TraceStack("a_targetList in AddArtifactsToList is None", a_stackID);
return 0;
}
if (a_refOrList->Is(RE::FormType::FormList)) {
a_refOrList->As<RE::BGSListForm>()->ForEachForm([&](RE::TESForm& a_exform) {
const auto refrItem = a_exform.As<RE::TESObjectREFR>();
if (refrItem) {
AddArtifactsToList(a_vm, a_stackID, nullptr, refrItem, a_targetList, a_excludeList);
}
return true;
});
return a_targetList->forms.size();
}
const auto containerRef = a_refOrList->As<RE::TESObjectREFR>();
if (!containerRef) {
a_vm->TraceStack("containerRef in AddArtifactsToList is not a reference", a_stackID);
return 0;
}
const auto inv = containerRef->GetInventory([&](RE::TESBoundObject& a_exform) {
return is_artifact(&a_exform) && (!a_excludeList || !a_excludeList->HasForm(&a_exform));
});
for (const auto& item : inv) {
if (item.second.first > 0) {
a_targetList->AddForm(item.first);
}
}
return a_targetList->forms.size();
}
inline RE::TESObjectREFR* GetCellStorage(RE::StaticFunctionTag*,
RE::TESObjectREFR* a_ref,
RE::BGSListForm* a_refList,
RE::TESBoundObject* a_objectToCreate,
bool a_autoCreate = true)
{
RE::TESObjectREFR* result = NULL;
if (!a_ref || !a_refList || !a_objectToCreate) {
return result;
}
RE::TESObjectCELL* cell = a_ref->GetParentCell();
a_refList->ForEachForm([&result, &cell, &a_objectToCreate](RE::TESForm& a_exform) {
const auto ref = a_exform.As<RE::TESObjectREFR>();
if (ref && ref->GetParentCell() == cell && ref->GetBaseObject()->formID == a_objectToCreate->formID) {
result = ref;
return false;
}
return true;
});
if (!result && a_autoCreate) {
result = a_ref->PlaceObjectAtMe(a_objectToCreate, true).get();
result->Disable();
a_refList->AddForm(result);
}
return result;
}
inline void SyncCellStorage(VM* a_vm, StackID a_stackID, RE::StaticFunctionTag*,
RE::TESObjectREFR* a_cellStorage,
RE::BGSListForm* a_excludeContainers)
{
if (!a_cellStorage) {
return;
}
std::unordered_map<RE::FormID, bool> cellItems;
const auto cell = a_cellStorage->GetParentCell();
const auto inv = a_cellStorage->GetInventory();
for (const RE::NiPointer<RE::TESObjectREFR> a_ref : cell->references) {
const auto baseObj = a_ref->GetBaseObject();
if (baseObj->formType == RE::FormType::Container || (baseObj->formType == RE::FormType::NPC && !a_ref->IsDisabled() && baseObj->As<RE::TESNPC>()->GetRace()->formID == 0x0010760A)) {
if (a_excludeContainers->HasForm(a_ref->formID) || baseObj->formID == 0xDC9E7) { // skip persistent and PlayerBookShelfContainer
continue;
}
const auto contInv = a_ref->GetInventory([&](RE::TESBoundObject& a_object) -> bool {
return !cellItems.contains(a_object.formID);
});
for (const auto& [item, data] : contInv) {
if (data.first > 0) {
cellItems[item->formID] = true;
if (inv.find(item) == inv.end()) {
if (is_artifact(item)) {
a_cellStorage->AddObjectToContainer(item, nullptr, 1, nullptr);
}
}
}
}
continue;
}
if (a_ref->IsDisabled() || a_ref->IsMarkedForDeletion()) {
continue;
}
if (cellItems.contains(baseObj->formID)) {
continue;
}
cellItems[baseObj->formID] = true;
if (!is_artifact(baseObj)) {
continue;
}
if (inv.find(baseObj) == inv.end()) {
a_cellStorage->AddObjectToContainer(baseObj, nullptr, 1, nullptr);
}
}
for (const auto& [item, data] : inv) {
const auto& [count, entry] = data;
if (count > 0 && !cellItems.contains(item->formID)) {
a_cellStorage->RemoveItem(item, count, RE::ITEM_REMOVE_REASON::kRemove, nullptr, nullptr);
}
}
cellItems.clear();
}
// From po3's Papyrus Extender
inline std::vector<RE::Actor*> GetPlayerFollowers(RE::StaticFunctionTag*)
{
std::vector<RE::Actor*> result;
if (const auto processLists = RE::ProcessLists::GetSingleton(); processLists) {
for (auto& actorHandle : processLists->highActorHandles) {
if (auto actor = actorHandle.get(); actor && actor->IsPlayerTeammate()) {
result.push_back(actor.get());
}
}
}
return result;
}
inline std::int32_t AddArtifactsFromFollowersToList(RE::StaticFunctionTag*,
RE::BGSListForm* a_targetList,
RE::BGSListForm* a_excludeList = NULL)
{
for (const auto& actor : GetPlayerFollowers(nullptr)) {
const auto inv = actor->GetInventory([&](RE::TESBoundObject& a_exform) {
return is_artifact(&a_exform) && (!a_excludeList || !a_excludeList->HasForm(&a_exform));
});
for (const auto& item : inv) {
if (item.second.first > 0) {
a_targetList->AddForm(item.first);
}
}
}
return a_targetList->forms.size();
}
inline bool FollowersHaveItem(RE::StaticFunctionTag*,
RE::TESBoundObject* a_form)
{
for (const auto& actor : GetPlayerFollowers(nullptr)) {
const auto inv = actor->GetInventory([&](RE::TESBoundObject& a_object) -> bool {
return a_form->formID == a_object.formID;
});
const auto it = inv.find(a_form);
const auto iCount = it != inv.end() ? it->second.first : 0;
if (iCount > 0) {
return true;
}
}
return false;
}
inline void Bind(VM& a_vm)
{
BIND(AddAllFormsToList);
logger::info("Registered AddAllFormsToList"sv);
BIND(AddArtifactsToList);
logger::info("Registered AddArtifactsToList"sv);
BIND(AddArtifactsFromFollowersToList);
logger::info("Registered AddArtifactsFromFollowersToList"sv);
BIND(GetCellStorage);
logger::info("Registered GetCellStorage"sv);
BIND(SyncCellStorage);
logger::info("Registered SyncCellStorage"sv);
BIND(FollowersHaveItem);
logger::info("Registered FollowersHaveItem"sv);
BIND(GetPlayerFollowers);
logger::info("Registered GetPlayerFollowers"sv);
}
}

View File

@ -1,35 +0,0 @@
#pragma once
namespace Papyrus::ObjectReference
{
inline std::uint32_t GetItemCountInList(RE::StaticFunctionTag*,
RE::BGSListForm* a_containerList,
RE::TESBoundObject* a_form)
{
if (!a_containerList || !a_form) {
return 0;
}
std::uint32_t iResult = 0;
a_containerList->ForEachForm([&](RE::TESForm& a_container) {
const auto refrItem = a_container.As<RE::TESObjectREFR>();
if (refrItem) {
const auto inv = refrItem->GetInventory([&](RE::TESBoundObject& a_object) -> bool {
return a_form->formID == a_object.formID;
});
const auto it = inv.find(a_form);
iResult += it != inv.end() ? it->second.first : 0;
}
return true;
});
return iResult;
}
inline void Bind(VM& a_vm)
{
BIND(GetItemCountInList);
logger::info("Registered GetItemCountInList"sv);
}
}

View File

@ -1,7 +1,7 @@
#include "Papyrus.h"
#include "BookCheck.h"
#include "MiscCheck.h"
#include "ArtifactTracker.h"
using namespace ArtifactTracker;
using namespace RE::BSScript;
using namespace SKSE;
using namespace SKSE::log;
@ -9,7 +9,7 @@ using namespace SKSE::stl;
namespace {
void InitializeLogging() {
void InitializeLogging() {
auto path = logger::log_directory();
if (!path) {
stl::report_and_fail("Failed to find standard logging directory"sv);
@ -27,14 +27,14 @@ namespace {
spdlog::set_pattern("[%l] %v"s);
}
void InitializeMessaging() {
void InitializeMessaging()
{
GetMessagingInterface()->RegisterListener([](MessagingInterface::Message* message) {
if (message->type == MessagingInterface::kDataLoaded) {
BookCheck::PreloadBookList();
MiscCheck::PreloadMiscList();
ArtifactTracker::Init();
}
});
}
}
}
SKSEPluginLoad(const LoadInterface* skse) {
@ -45,7 +45,8 @@ SKSEPluginLoad(const LoadInterface* skse) {
log::info("{} {} is loading...", plugin->GetName(), version);
Init(skse);
InitializeMessaging();
InitializeMessaging();
SKSE::GetPapyrusInterface()->Register(Papyrus::Bind);
log::info("{} has finished loading.", plugin->GetName());

View File

@ -1,28 +0,0 @@
#include "MiscCheck.h"
namespace MiscCheck
{
std::unordered_map<RE::FormID, RE::TESObjectMISC*> validMisc;
void PreloadMiscList()
{
const auto dataHandler = RE::TESDataHandler::GetSingleton();
if (!dataHandler) {
return;
}
RE::BGSListForm* excludeKeywords = dataHandler->LookupForm<RE::BGSListForm>(0x801, "Artifact Tracker.esp"); // ETR_ExcludeMiscKeywords
for (const auto& form : dataHandler->GetFormArray<RE::TESObjectMISC>()) {
if (form->GetPlayable() && !form->IsGold() && !form->IsLockpick() && !form->HasKeywordInList(excludeKeywords, false)) {
validMisc[form->formID] = form;
}
}
}
std::unordered_map<RE::FormID, RE::TESObjectMISC*> GetMiscList()
{
return validMisc;
}
}

View File

@ -1,8 +0,0 @@
#pragma once
namespace MiscCheck
{
void PreloadMiscList();
std::unordered_map<RE::FormID, RE::TESObjectMISC*> GetMiscList();
}

View File

@ -1,9 +1,5 @@
#include "Papyrus.h"
#include "BookCheck.h"
#include "MiscCheck.h"
#include "Functions/ObjectReference.h"
#include "Functions/ArtifactTracker.h"
#include "PapyrusFunctions.h"
namespace Papyrus
{
@ -16,8 +12,7 @@ namespace Papyrus
logger::info("{:*^30}", "FUNCTIONS"sv);
ObjectReference::Bind(*a_vm);
ArtifactTracker::Bind(*a_vm);
PapyrusFunctions::Bind(*a_vm);
return true;
}

View File

@ -0,0 +1,164 @@
#pragma once
#include "ArtifactTracker.h"
namespace Papyrus::PapyrusFunctions
{
inline std::int32_t AddAllFormsToList(RE::StaticFunctionTag*,
RE::BGSListForm* a_targetList,
short a_formType,
RE::BGSListForm* a_storedList,
RE::BGSListForm* a_foundList)
{
const auto formType = static_cast<RE::FormType>(a_formType);
switch (formType) {
case RE::FormType::Book:
for (auto const& item : ArtifactTracker::validBooks) {
if (!a_storedList->HasForm(item.second) && !a_foundList->HasForm(item.second)) {
a_targetList->AddForm(item.second);
}
}
break;
case RE::FormType::Misc:
for (auto const& item : ArtifactTracker::validMisc) {
if (!a_storedList->HasForm(item.second) && !a_foundList->HasForm(item.second)) {
a_targetList->AddForm(item.second);
}
}
break;
case RE::FormType::None:
break;
default:
const auto dataHandler = RE::TESDataHandler::GetSingleton();
if (!dataHandler) {
return a_targetList->forms.size();
}
for (const auto& form : dataHandler->GetFormArray(formType)) {
if (!form || !form->GetPlayable()) {
continue;
}
if (a_storedList->HasForm(form) || a_foundList->HasForm(form)) {
continue;
}
a_targetList->AddForm(form);
}
}
return a_targetList->forms.size();
}
inline std::int32_t AddArtifactsToList(VM* a_vm, StackID a_stackID, RE::StaticFunctionTag*,
RE::TESForm* a_refOrList,
RE::BGSListForm* a_targetList,
RE::BGSListForm* a_excludeList = NULL)
{
if (!a_refOrList) {
a_vm->TraceStack("a_refOrList in AddArtifactsToList is None", a_stackID);
return 0;
}
if (!a_targetList) {
a_vm->TraceStack("a_targetList in AddArtifactsToList is None", a_stackID);
return 0;
}
if (a_refOrList->Is(RE::FormType::FormList)) {
a_refOrList->As<RE::BGSListForm>()->ForEachForm([&](RE::TESForm& a_exform) {
const auto refrItem = a_exform.As<RE::TESObjectREFR>();
if (refrItem) {
AddArtifactsToList(a_vm, a_stackID, nullptr, refrItem, a_targetList, a_excludeList);
}
return true;
});
return a_targetList->forms.size();
}
const auto containerRef = a_refOrList->As<RE::TESObjectREFR>();
if (!containerRef) {
a_vm->TraceStack("containerRef in AddArtifactsToList is not a reference", a_stackID);
return 0;
}
const auto inv = containerRef->GetInventory([&](RE::TESBoundObject& a_exform) {
return ArtifactTracker::IsArtifact(&a_exform) && (!a_excludeList || !a_excludeList->HasForm(&a_exform));
});
for (const auto& item : inv) {
if (item.second.first > 0) {
a_targetList->AddForm(item.first);
}
}
return a_targetList->forms.size();
}
inline RE::TESObjectREFR* GetCellStorage(RE::StaticFunctionTag*,
RE::TESObjectREFR* a_ref,
RE::BGSListForm* a_refList,
RE::TESBoundObject* a_objectToCreate,
bool a_autoCreate = true)
{
return ArtifactTracker::GetCellStorage(a_ref, a_refList, a_objectToCreate, a_autoCreate);
}
inline void SyncCellStorage(RE::StaticFunctionTag*,
RE::TESObjectREFR* a_cellStorage,
RE::BGSListForm* a_excludeContainers)
{
ArtifactTracker::SyncCellStorage(a_cellStorage, a_excludeContainers);
}
inline std::int32_t AddArtifactsFromFollowersToList(RE::StaticFunctionTag*,
RE::BGSListForm* a_targetList,
RE::BGSListForm* a_excludeList = NULL)
{
if (const auto processLists = RE::ProcessLists::GetSingleton(); processLists) {
for (auto& actorHandle : processLists->highActorHandles) {
if (auto actor = actorHandle.get(); actor && actor->IsPlayerTeammate()) {
const auto inv = actor->GetInventory([&](RE::TESBoundObject& a_exform) {
return ArtifactTracker::IsArtifact(&a_exform) && (!a_excludeList || !a_excludeList->HasForm(&a_exform));
});
for (const auto& item : inv) {
if (item.second.first > 0) {
a_targetList->AddForm(item.first);
}
}
}
}
}
return a_targetList->forms.size();
}
inline void OnItemPickup(RE::StaticFunctionTag*,
RE::TESBoundObject* a_item)
{
ArtifactTracker::OnItemPickup(a_item);
}
inline void Bind(VM& a_vm)
{
BIND(AddAllFormsToList);
logger::info("Registered AddAllFormsToList"sv);
BIND(AddArtifactsToList);
logger::info("Registered AddArtifactsToList"sv);
BIND(AddArtifactsFromFollowersToList);
logger::info("Registered AddArtifactsFromFollowersToList"sv);
BIND(GetCellStorage);
logger::info("Registered GetCellStorage"sv);
BIND(SyncCellStorage);
logger::info("Registered SyncCellStorage"sv);
BIND(OnItemPickup);
logger::info("Registered OnItemPickup"sv);
}
}

View File

@ -0,0 +1,78 @@
#pragma once
void RemoveListItem(RE::BGSListForm* a_List, RE::TESForm* a_form)
{
using func_t = decltype(&RemoveListItem);
REL::Relocation<func_t> func{ REL::RelocationID(20471, 20914) };
return func(a_List, a_form);
}
void RevertList(RE::TESForm* a_form)
{
using func_t = decltype(&RevertList);
REL::Relocation<func_t> func{ REL::RelocationID(20469, 20912) };
return func(a_form);
}
inline bool RefHasItem(RE::TESObjectREFR* a_ref, RE::TESForm* a_item)
{
if (!a_ref || !a_item) {
SKSE::log::warn("Invalid arguments in RefHasItem");
return false;
}
auto invChanges = a_ref->GetInventoryChanges();
if (invChanges && invChanges->entryList) {
for (auto& entry : *invChanges->entryList) {
if (entry && entry->object && entry->object->formID == a_item->formID) {
return entry->countDelta > 0;
}
}
}
return false;
}
inline std::uint32_t GetItemCountInList(RE::BGSListForm* a_containerList, RE::TESBoundObject* a_form)
{
if (!a_containerList || !a_form) {
SKSE::log::warn("Invalid arguments in GetItemCountInList");
return 0;
}
std::uint32_t iResult = 0;
a_containerList->ForEachForm([&](RE::TESForm& a_container) {
const auto refrItem = a_container.As<RE::TESObjectREFR>();
if (refrItem) {
const auto inv = refrItem->GetInventory([&](RE::TESBoundObject& a_object) -> bool {
return a_form->formID == a_object.formID;
});
const auto it = inv.find(a_form);
iResult += it != inv.end() ? it->second.first : 0;
}
return true;
});
return iResult;
}
inline bool FollowersHaveItem(RE::TESBoundObject* a_form)
{
if (const auto processLists = RE::ProcessLists::GetSingleton(); processLists) {
for (auto& actorHandle : processLists->highActorHandles) {
if (auto actor = actorHandle.get(); actor && actor->IsPlayerTeammate()) {
const auto inv = actor->GetInventory([&](RE::TESBoundObject& a_object) -> bool {
return a_form->formID == a_object.formID;
});
const auto it = inv.find(a_form);
const auto iCount = it != inv.end() ? it->second.first : 0;
if (iCount > 0) {
return true;
}
}
}
}
return false;
}

View File

@ -4,14 +4,10 @@ int function AddAllFormsToList(FormList targetList, int formType, FormList store
int function AddArtifactsToList(Form refOrList, FormList targetList, FormList excludeList = None) native global
int function GetItemCountInList(FormList refList, Form baseForm) native global
ObjectReference function GetCellStorage(ObjectReference ref, FormList refList, Form refToCreate, bool autoCreate = true) native global
function SyncCellStorage(ObjectReference cellStorage, FormList excludeContainers) native global
Actor[] function GetPlayerFollowers() native global
bool function FollowersHaveItem(Form baseForm) native global
int function AddArtifactsFromFollowersToList(FormList targetList, FormList excludeList = None) native global
function OnItemPickup(Form item) native global

View File

@ -1,168 +0,0 @@
Scriptname ETR_TrackFoundItems extends ReferenceAlias
Actor Property PlayerRef Auto
FormList Property ETR_ItemsNew Auto
FormList Property ETR_ItemsFound Auto
FormList Property ETR_ItemsStored Auto
FormList Property ETR_PersistentStorageList Auto
Container Property ETR_CellStorageContainer Auto
Keyword Property LocTypePlayerHouse Auto
bool bBusy = false
int iFollowerIndex = 0
bool bAtHome = false
bool bRescanHome = false
bool bRescanPersistent = false
bool bRescanFollowers = false
ObjectReference lastDestContainer = None
bool lastDestIsPersistent = false
int iUpdateCount
event OnInit()
OnPlayerLoadGame()
endevent
Event OnPlayerLoadGame()
AddInventoryEventFilter(ETR_ItemsFound)
Location currentLocation = PlayerRef.GetCurrentLocation()
bAtHome = currentLocation && currentLocation.HasKeyword(LocTypePlayerHouse)
lastDestContainer = None
bBusy = false
EndEvent
Event OnLocationChange(Location akOldLoc, Location akNewLoc)
bAtHome = akNewLoc && akNewLoc.HasKeyword(LocTypePlayerHouse)
lastDestContainer = None
bRescanFollowers = true
RegisterForSingleUpdate(3.0)
endEvent
Event OnMenuClose(String MenuName)
UnregisterForUpdate()
OnUpdate()
EndEvent
Event OnUpdate()
if bRescanFollowers
bRescanFollowers = false
int iCurrentFollowers = 0;
Actor[] aFollowers = ETR_Functions.GetPlayerFollowers()
int i = aFollowers.length
while i > 0
i -= 1
iCurrentFollowers += aFollowers[i].GetFormID()
endwhile
if iCurrentFollowers == iFollowerIndex
return
endif
iFollowerIndex = iCurrentFollowers
Debug.Notification("Team changed, rescanning")
endif
if lastDestContainer && lastDestContainer as Actor && (lastDestContainer as Actor).IsPlayerTeammate()
lastDestContainer = None
bRescanHome = false
return
endif
if UI.IsMenuOpen("ContainerMenu")
RegisterForMenu("ContainerMenu")
return
endif
while bBusy
Debug.Notification("Found OnUpdate is busy")
Utility.wait(0.5)
endwhile
bBusy = true
iUpdateCount += 1
Debug.Notification("Running Found OnUpdate " + iUpdateCount)
if bRescanHome
bRescanHome = false
if lastDestContainer && lastDestContainer as PlayerBookShelfContainerScript
int iLimit = 10
while iLimit >= 0 && (lastDestContainer as PlayerBookShelfContainerScript).GetState() == "PlacingBooks"
Debug.Notification("Waiting for shelf update")
iLimit -= 1
Utility.wait(0.5)
endwhile
endif
ObjectReference cellStorage = ETR_Functions.GetCellStorage(PlayerRef, ETR_PersistentStorageList, ETR_CellStorageContainer)
ETR_Functions.SyncCellStorage(cellStorage, ETR_PersistentStorageList)
if ! bRescanPersistent
ETR_Functions.AddArtifactsToList(cellStorage, ETR_ItemsStored)
endif
endif
if bRescanPersistent
bRescanPersistent = false
Form[] aContainers = ETR_PersistentStorageList.ToArray()
int n = aContainers.length
while n > 0
n -= 1
ETR_Functions.AddArtifactsToList(aContainers[n], ETR_ItemsStored)
endwhile
endif
ETR_ItemsFound.Revert()
ETR_Functions.AddArtifactsToList(PlayerRef, ETR_ItemsFound, ETR_ItemsStored)
ETR_Functions.AddArtifactsFromFollowersToList(ETR_ItemsFound, ETR_ItemsStored)
bBusy = false
EndEvent
event OnItemRemoved(Form akBaseItem, int aiItemCount, ObjectReference akItemReference, ObjectReference akDestContainer)
if akDestContainer
if lastDestContainer != akDestContainer
lastDestContainer = akDestContainer
lastDestIsPersistent = ETR_PersistentStorageList.HasForm(akDestContainer)
endif
; Moving items without latent functions should help with avoiding stack dumps
if lastDestIsPersistent
bRescanHome = false
bRescanPersistent = true
RegisterForSingleUpdate(0.5)
elseif bAtHome
bRescanHome = true
bRescanPersistent = false
RegisterForSingleUpdate(0.5)
elseif PlayerRef.GetItemCount(akBaseItem) == 0 && ! ETR_Functions.FollowersHaveItem(akBaseItem)
ETR_ItemsFound.RemoveAddedForm(akBaseItem)
ETR_ItemsNew.AddForm(akBaseItem)
endif
elseif bAtHome && akItemReference
ETR_ItemsFound.RemoveAddedForm(akBaseItem)
ETR_ItemsStored.AddForm(akBaseItem)
ETR_Functions.GetCellStorage(PlayerRef, ETR_PersistentStorageList, ETR_CellStorageContainer).AddItem(akBaseItem, 1, true)
elseif PlayerRef.GetItemCount(akBaseItem) == 0 && ! ETR_Functions.FollowersHaveItem(akBaseItem)
ETR_ItemsFound.RemoveAddedForm(akBaseItem)
ETR_ItemsNew.AddForm(akBaseItem)
endif
endevent

View File

@ -1,68 +0,0 @@
Scriptname ETR_TrackNewItems extends ReferenceAlias
Actor Property PlayerRef Auto
FormList Property ETR_ItemsNew Auto
FormList Property ETR_ItemsFound Auto
FormList Property ETR_ItemsStored Auto
FormList Property ETR_PersistentStorageList Auto
GlobalVariable Property ETR_NotifyNewArtifact Auto
event OnInit()
OnPlayerLoadGame()
endevent
event OnPlayerLoadGame()
AddInventoryEventFilter(ETR_ItemsNew)
if skse.GetPluginVersion("Ahzaab's moreHUD Plugin") >= 30800
ahzmorehud.RegisterIconFormList("dbmNew", ETR_ItemsNew)
ahzmorehud.RegisterIconFormList("dbmFound", ETR_ItemsFound)
ahzmorehud.RegisterIconFormList("dbmDisp", ETR_ItemsStored)
endif
if skse.GetPluginVersion("Ahzaab's moreHUD Inventory Plugin") >= 10017
ahzmorehudie.RegisterIconFormList("dbmNew", ETR_ItemsNew)
ahzmorehudie.RegisterIconFormList("dbmFound", ETR_ItemsFound)
ahzmorehudie.RegisterIconFormList("dbmDisp", ETR_ItemsStored)
endif
if SKSE.GetPluginVersion("QuickLootRE") >= 292
QuickLootRE.RegisterNewItemsList(ETR_ItemsNew)
QuickLootRE.RegisterDisplayedItemsList(ETR_ItemsStored)
QuickLootRE.RegisterFoundItemsList(ETR_ItemsFound)
endif
; Rebuild all lists to avoid discrepancies, stale data, and broken records
ETR_ItemsStored.Revert()
ETR_Functions.AddArtifactsToList(ETR_PersistentStorageList, ETR_ItemsStored)
ETR_ItemsFound.Revert()
ETR_Functions.AddArtifactsToList(PlayerRef, ETR_ItemsFound, ETR_ItemsStored)
ETR_Functions.AddArtifactsFromFollowersToList(ETR_ItemsFound, ETR_ItemsStored)
ETR_ItemsNew.Revert()
ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 41, ETR_ItemsStored, ETR_ItemsFound)
ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 32, ETR_ItemsStored, ETR_ItemsFound)
ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 27, ETR_ItemsStored, ETR_ItemsFound)
ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 26, ETR_ItemsStored, ETR_ItemsFound)
endevent
event OnItemAdded(Form akBaseItem, int aiItemCount, ObjectReference akItemReference, ObjectReference akSourceContainer)
ETR_ItemsNew.RemoveAddedForm(akBaseItem)
ETR_ItemsFound.AddForm(akBaseItem)
ETR_ItemsStored.RemoveAddedForm(akBaseItem)
if ETR_NotifyNewArtifact.Value
Debug.Notification("New artifact acquired: " + akBaseItem.GetName())
endif
endevent

View File

@ -29,6 +29,40 @@ endevent
Event OnPlayerLoadGame()
AddInventoryEventFilter(ETR_ItemsStored)
if skse.GetPluginVersion("Ahzaab's moreHUD Plugin") >= 30800
ahzmorehud.RegisterIconFormList("dbmNew", ETR_ItemsNew)
ahzmorehud.RegisterIconFormList("dbmFound", ETR_ItemsFound)
ahzmorehud.RegisterIconFormList("dbmDisp", ETR_ItemsStored)
endif
if skse.GetPluginVersion("Ahzaab's moreHUD Inventory Plugin") >= 10017
ahzmorehudie.RegisterIconFormList("dbmNew", ETR_ItemsNew)
ahzmorehudie.RegisterIconFormList("dbmFound", ETR_ItemsFound)
ahzmorehudie.RegisterIconFormList("dbmDisp", ETR_ItemsStored)
endif
if SKSE.GetPluginVersion("QuickLootRE") >= 292
QuickLootRE.RegisterNewItemsList(ETR_ItemsNew)
QuickLootRE.RegisterDisplayedItemsList(ETR_ItemsStored)
QuickLootRE.RegisterFoundItemsList(ETR_ItemsFound)
endif
; Rebuild all lists to avoid discrepancies, stale data, and broken records
ETR_ItemsStored.Revert()
ETR_Functions.AddArtifactsToList(ETR_PersistentStorageList, ETR_ItemsStored)
ETR_ItemsFound.Revert()
ETR_Functions.AddArtifactsToList(PlayerRef, ETR_ItemsFound, ETR_ItemsStored)
ETR_Functions.AddArtifactsFromFollowersToList(ETR_ItemsFound, ETR_ItemsStored)
ETR_ItemsNew.Revert()
ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 41, ETR_ItemsStored, ETR_ItemsFound)
ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 32, ETR_ItemsStored, ETR_ItemsFound)
ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 27, ETR_ItemsStored, ETR_ItemsFound)
ETR_Functions.AddAllFormsToList(ETR_ItemsNew, 26, ETR_ItemsStored, ETR_ItemsFound)
Location currentLocation = PlayerRef.GetCurrentLocation()
bAtHome = currentLocation && currentLocation.HasKeyword(LocTypePlayerHouse)
if bAtHome
@ -108,6 +142,7 @@ Event OnUpdate()
ETR_ItemsStored.Revert()
Form[] aContainers = ETR_PersistentStorageList.ToArray()
int n = aContainers.length
while n > 0
n -= 1
@ -117,7 +152,6 @@ Event OnUpdate()
ETR_ItemsFound.Revert()
ETR_Functions.AddArtifactsToList(PlayerRef, ETR_ItemsFound, ETR_ItemsStored)
ETR_Functions.AddArtifactsFromFollowersToList(ETR_ItemsFound, ETR_ItemsStored)
endif
bBusy = false
@ -139,41 +173,7 @@ event OnItemAdded(Form akBaseItem, int aiItemCount, ObjectReference akItemRefere
RegisterForSingleUpdate(0.5)
endif
elseif bAtHome
ObjectReference cellStorage = ETR_Functions.GetCellStorage(PlayerRef, ETR_PersistentStorageList, ETR_CellStorageContainer)
if cellStorage.GetItemCount(akBaseItem)
ETR_Functions.SyncCellStorage(cellStorage, ETR_PersistentStorageList)
if ETR_Functions.GetItemCountInList(ETR_PersistentStorageList, akBaseItem) == 0
ETR_ItemsStored.RemoveAddedForm(akBaseItem)
ETR_ItemsFound.AddForm(akBaseItem)
endif
endif
ETR_Functions.OnItemPickup(akBaseItem)
endif
endevent
state AtHome
; The item is already registered as stored, and we just stored more
event OnItemRemoved(Form akBaseItem, int aiItemCount, ObjectReference akItemReference, ObjectReference akDestContainer)
if akDestContainer
if lastDestContainer != akDestContainer
lastDestContainer = akDestContainer
lastDestIsPersistent = ETR_PersistentStorageList.HasForm(akDestContainer)
endif
if ! lastDestIsPersistent
bRescanHome = true
bRescanPersistent = false
RegisterForSingleUpdate(0.5)
endif
elseif akItemReference
ObjectReference cellStorage = ETR_Functions.GetCellStorage(PlayerRef, ETR_PersistentStorageList, ETR_CellStorageContainer)
if cellStorage.GetItemCount(akBaseItem) == 0
cellStorage.AddItem(akBaseItem, 1, true)
endif
endif
endevent
endstate