Initial commit. Integrates with client, saves refs and merch.

This commit is contained in:
2020-10-12 19:50:20 -04:00
commit 36abd362cd
24 changed files with 2006 additions and 0 deletions

25
src/BRClient.cpp Normal file
View File

@@ -0,0 +1,25 @@
#include "bindings.h"
void Init(RE::StaticFunctionTag*)
{
logger::info("Entered Init");
init();
logger::info("Init done");
}
std::string GenerateApiKey(RE::StaticFunctionTag*)
{
logger::info("Entered GenerateApiKey");
char *api_key = generate_api_key();
logger::info(FMT_STRING("GenerateApiKey api_key: {}"), api_key);
return api_key;
}
bool StatusCheck(RE::StaticFunctionTag*, RE::BSFixedString api_url)
{
logger::info("Entered StatusCheck");
logger::info(FMT_STRING("StatusCheck api_url: {}"), api_url);
bool result = status_check(api_url.c_str());
logger::info(FMT_STRING("StatusCheck result: {}"), result ? "true" : "false");
return result;
}

5
src/BRClient.h Normal file
View File

@@ -0,0 +1,5 @@
#pragma once
void Init(RE::StaticFunctionTag*);
std::string GenerateApiKey(RE::StaticFunctionTag*);
bool StatusCheck(RE::StaticFunctionTag*, RE::BSFixedString api_url);

278
src/BRInteriorRefList.cpp Normal file
View File

@@ -0,0 +1,278 @@
#include "bindings.h"
#include "NativeFunctions.h"
int CreateInteriorRefList(RE::StaticFunctionTag*, RE::BSFixedString api_url, RE::BSFixedString api_key, uint32_t shop_id, RE::TESObjectCELL * cell)
{
logger::info("Entered CreateInteriorRefList");
if (!cell)
return -1;
RE::TESDataHandler * data_handler = RE::TESDataHandler::GetSingleton();
std::vector<RefRecord> ref_records;
for (auto entry = cell->references.begin(); entry != cell->references.end(); ++entry)
{
// TODO: skip saving the ref if it is part of mod file
// OR: continue to save it, but with a special flag (useful in order to allow repositioning default statics)
// Then, during Load, skip PlaceAtMe, lookup the ref by id and then reposition it
RE::TESObjectREFR * ref = (*entry).get();
const char * name = ref->GetName();
logger::info(FMT_STRING("CreateInteriorRefList ref: {}"), name);
const RE::TESBoundObject * base = ref->GetBaseObject();
if (base) {
RE::FormID base_form_id = base->GetFormID();
if (base_form_id == 7) {
// skip saving player ref
continue;
}
const char * form_name = base->GetName();
const RE::FormType form_type = base->GetFormType();
logger::info(FMT_STRING("CreateInteriorRefList form: {} ({:x})"), form_name, (uint32_t)form_type);
float position_x = ref->GetPositionX();
float position_y = ref->GetPositionY();
float position_z = ref->GetPositionZ();
float angle_x = ref->GetAngleX();
float angle_y = ref->GetAngleY();
float angle_z = ref->GetAngleZ();
RE::NiNPShortPoint3 boundMin = base->boundData.boundMin;
RE::NiNPShortPoint3 boundMax = base->boundData.boundMax;
uint16_t bound_x = boundMax.x > boundMin.x ? boundMax.x - boundMin.x : boundMin.x - boundMax.x;
uint16_t bound_y = boundMax.y > boundMin.y ? boundMax.y - boundMin.y : boundMin.y - boundMax.y;
uint16_t bound_z = boundMax.z > boundMin.z ? boundMax.z - boundMin.z : boundMin.z - boundMax.z;
logger::info(FMT_STRING("CreateInteriorRefList bounds: width: {:d}, length: {:d}, height: {:d}"), bound_x, bound_y, bound_z);
uint16_t scale = ref->refScale;
logger::info(FMT_STRING("CreateInteriorRefList position: {:.2f}, {:.2f}, {:.2f} angle: {:.2f}, {:.2f}, {:.2f} scale: {:d}"), position_x, position_y, position_z, angle_x, angle_y, angle_z, scale);
logger::info(FMT_STRING("CreateInteriorRefList deleted: {:d}, wants delete: {:d}"), ref->IsMarkedForDeletion(), ref->inGameFormFlags.all(RE::TESObjectREFR::InGameFormFlag::kWantsDelete));
RE::TESFile * base_file = base->GetFile(0);
char * base_file_name = base_file->fileName;
bool is_light = base_file->recordFlags.all(RE::TESFile::RecordFlag::kSmallFile);
uint32_t base_local_form_id = is_light ? base_form_id & 0xfff : base_form_id & 0xFFFFFF;
//_MESSAGE("FILE: %s isLight: %d formID: 0x%x localFormId: 0x%x", file_name, is_light, form_id, local_form_id);
RE::FormID ref_form_id = ref->GetFormID();
uint16_t ref_mod_index = ref_form_id >> 24;
char * ref_file_name = nullptr;
uint32_t ref_local_form_id = ref_form_id & 0xFFFFFF;
if (ref_mod_index != 255) { // not a temp ref
if (ref_mod_index == 254) { // is a light mod
ref_local_form_id = ref_form_id & 0xfff;
ref_mod_index = (ref_form_id >> 12) & 0xfff;
const RE::TESFile * ref_file = data_handler->LookupLoadedLightModByIndex(ref_mod_index);
ref_file_name = _strdup(ref_file->fileName);
}
else {
const RE::TESFile * ref_file = data_handler->LookupLoadedModByIndex(ref_mod_index);
ref_file_name = _strdup(ref_file->fileName);
}
}
ref_records.push_back({
base_file_name,
base_local_form_id,
ref_file_name,
ref_local_form_id,
position_x,
position_y,
position_z,
angle_x,
angle_y,
angle_z,
scale,
});
}
}
int interior_ref_list_id = create_interior_ref_list(api_url.c_str(), api_key.c_str(), shop_id, &ref_records[0], ref_records.size());
logger::info(FMT_STRING("CreateInteriorRefList result: {}"), interior_ref_list_id);
return interior_ref_list_id;
}
bool ClearCell(RE::StaticFunctionTag*, RE::TESObjectCELL* cell_ignored)
{
logger::info("Entered ClearCell");
RE::TESDataHandler * data_handler = RE::TESDataHandler::GetSingleton();
using func_t = bool(RE::TESObjectREFR* a_thisObj, void* a_param1, void* a_param2, double& a_result);
RE::TESObjectCELL * cell = RE::TESObjectCELL::LookupByEditorID<RE::TESObjectCELL>("BREmpty");
logger::info(FMT_STRING("ClearCell lookup cell override name: {} id: {:x}"), cell->GetName(), (uint32_t)cell->GetFormID());
if (!cell) {
logger::error("ClearCell cell is null!");
return false;
}
// Destroy existing references
for (auto entry = cell->references.begin(); entry != cell->references.end(); ++entry)
{
RE::TESObjectREFR * ref = (*entry).get();
RE::FormID form_id = ref->GetFormID();
logger::info(FMT_STRING("ClearCell ref form_id: {:x}"), (uint32_t)form_id);
int mod_index = form_id >> 24;
if (mod_index != 255) {
RE::TESFile * file = ref->GetDescriptionOwnerFile();
if (file) {
bool is_light = file->recordFlags.all(RE::TESFile::RecordFlag::kSmallFile);
uint32_t local_form_id = is_light ? ref->GetFormID() & 0xfff : form_id & 0xFFFFFF;
if (!data_handler->LookupForm<RE::TESObjectREFR>(local_form_id, file->fileName)) {
logger::info(FMT_STRING("ClearCell ref was not in mod file! {:x} {}"), local_form_id, ref->GetName());
}
else {
logger::info(FMT_STRING("ClearCell ref in mod file {:x} {}"), local_form_id, ref->GetName());
}
} else {
logger::info(FMT_STRING("ClearCell ref not in ANY file! {:x} {}"), (uint32_t)form_id, ref->GetName());
}
}
else {
logger::info(FMT_STRING("ClearCell ref is a temp ref, deleting {:x} {}"), (uint32_t)form_id, ref->GetName());
ref->Disable(); // disabling first is required to prevent CTD on unloading cell
ref->SetDelete(true);
}
}
return true;
}
// TODO: store the FFIResult in a HashMap cache (with ids assigned from incrementing a global int) and return its key in this function
// * bool BRResult.IsOk(int resultId) ; if false, then only GetErr() shall be accessed
// * bool BRResult.IsErr(int resultId) ; maybe not needed
// * string BRResult.GetErr(int resultId) ; always a string
// * string BRResult.GetType(int resultId) ; returns type of Ok value encoded as string
// * BRResult.Drop(int resultId) ; once we are done using the data from the result or displaying an error, delete it from the cache and free the memory
// These will panic if a) the result is an error or b) the type of the result value different than what was requested
// That will cause CTDs but there's not much else to do, silently causing undefined behavior would be worse
// Every step of this API has an explicit check you must make before continuing. If you break that contract, that's on you, the scripter.
// * string BRResult.GetString(int resultId)
// * int BRResult.GetInt(int resultId)
// * bool BRResult.GetBool(int resultId)
// * int BRResult.GetResponseId(int resultId)
// For BRResponse (maybe just completely get rid of this):
// * int BRResponse.GetStatus(int responseId) ; if >= 300 then none of the other functions should be accessed
// * bool BRResponse.IsFromCache(int responseId)
// * string BRResponse.GetType(int responseId) ; may return things like "string" or "shop"
// * string BRResponse.GetString(int responseId) ; is this going to be needed?
// * int BRResponse.GetShopId(int responseId) ; for a get_shop request
// * int BRResponse.GetInteriorRefListId(int responseId) ; for a get_interior_ref_list request
// * BRResponse.Drop()
// The following will return values from a HashMap cache on the rust-side where entries are not evicted unless explicitly told to by papyrus.
// These methods will panic if there isn't an entry in the cache for the id. This can happen if a request was not made beforehand and verified to be non-err result with a non-error response.
// For BRShop
// * string BRShop.GetName(int shopId)
// * int BRShop.GetOwnerId(int shopId)
// * BRShop.Drop()
// For BROwner
// * string BROwner.GetName(int ownerId)
// * BROwner.Drop()
// Should split this up into GetInteriorRefList and LoadInteriorRefList.
// Get: makes the request to the api and stores the Result in a rust cache, returns id of Result
// Load: given id of interior ref list, gets the data from the rust cache, does the PlaceAtMe loop and returns an id of another Result
bool LoadInteriorRefList(RE::StaticFunctionTag*, RE::BSFixedString api_url, RE::BSFixedString api_key, uint32_t interior_ref_list_id, RE::TESObjectCELL* cell_ignored, RE::TESObjectREFR* target_ref)
{
logger::info("Entered LoadInteriorRefList");
RE::TESDataHandler * data_handler = RE::TESDataHandler::GetSingleton();
RE::BSScript::Internal::VirtualMachine * a_vm = RE::BSScript::Internal::VirtualMachine::GetSingleton();
using func_t = decltype(&PlaceAtMe);
REL::Relocation<func_t> PlaceAtMe_Native{ REL::ID(55672) };
using func_t2 = decltype(&MoveTo);
REL::Relocation<func_t2> MoveTo_Native(RE::Offset::TESObjectREFR::MoveTo);
// testing returning a script object
// RE::BSTSmartPointer<RE::BSScript::Object> ret = RE::BSTSmartPointer<RE::BSScript::Object>::BSTSmartPointer();
// a_vm->CreateObject(RE::BSFixedString("BRResult"), ret);
// RE::BSScript::Variable var = RE::BSScript::Variable::Variable(RE::BSScript::TypeInfo::TypeInfo(RE::BSScript::TypeInfo::RawType::kBool));
// var.SetBool(false);
// a_vm->SetPropertyValue(ret, "value", var);
// RE::BSScript::Variable ret_var = RE::BSScript::Variable::Variable(RE::BSScript::TypeInfo::TypeInfo(RE::BSScript::TypeInfo::RawType::kObject));
// ret_var.SetObject(ret);
// return ret_var;
RE::TESObjectCELL * cell = RE::TESObjectCELL::LookupByEditorID<RE::TESObjectCELL>("BREmpty");
logger::info(FMT_STRING("LoadInteriorRefList lookup cell override name: {} id: {:x}"), cell->GetName(), (uint32_t)cell->GetFormID());
if (!cell) {
logger::error("LoadInteriorRefList cell is null!");
}
//RE::TESObjectREFR * x_marker = data_handler->LookupForm<RE::TESObjectREFR>(6628, "BazaarRealm.esp");
if (target_ref) {
FFIResult<RefRecordVec> result = get_interior_ref_list(api_url.c_str(), api_key.c_str(), interior_ref_list_id);
if (result.IsOk()) {
logger::info("LoadInteriorRefList get_interior_ref_list result: OK");
RefRecordVec vec = result.AsOk();
logger::info(FMT_STRING("LoadInteriorRefList vec len: {:d}, cap: {:d}"), vec.len, vec.cap);
for (int i = 0; i < vec.len; i++) {
RefRecord ref = vec.ptr[i];
logger::info(FMT_STRING("LoadInteriorRefList ref base_mod_name: {}, base_local_form_id: {:x}"), ref.base_mod_name, ref.base_local_form_id);
logger::info(FMT_STRING("LoadInteriorRefList ref position {:.2f} {:.2f} {:.2f}, angle: {:.2f} {:.2f} {:.2f}, scale: {:d}"), ref.position_x, ref.position_y, ref.position_z, ref.angle_x, ref.angle_y, ref.angle_z, ref.scale);
if (ref.base_local_form_id == 7) {
logger::info("LoadInteriorRefList skipping player ref");
continue;
}
RE::NiPoint3 position = RE::NiPoint3::NiPoint3(ref.position_x, ref.position_y, ref.position_z);
RE::NiPoint3 angle = RE::NiPoint3::NiPoint3(ref.angle_x, ref.angle_y, ref.angle_z);
RE::TESObjectREFR * game_ref = nullptr;
if (ref.ref_mod_name) { // non-temp ref, try to lookup and reposition
game_ref = data_handler->LookupForm<RE::TESObjectREFR>(ref.ref_local_form_id, ref.ref_mod_name);
if (game_ref) {
logger::info(FMT_STRING("LoadInteriorRefList lookup ref name: {}, form_id: {:x}"), game_ref->GetName(), (uint32_t)game_ref->GetFormID());
// Failed experiment at trying to call the ObjectReference.Enable() papyrus native function
//RE::BSTSmartPointer<RE::BSScript::Object> object;
//RE::BSTSmartPointer<RE::BSScript::IStackCallbackFunctor> callback = RE::BSTSmartPointer<RE::BSScript::IStackCallbackFunctor>::BSTSmartPointer(RE::BSScript::IStackCallbackFunctor());
//a_vm->CreateObject(RE::BSFixedString("ObjectReference"), game_ref, object);
//_MESSAGE("Created Object isConstructed: %d, isInitialized: %d, isValid: %d, type: %s", object->IsConstructed(), object->IsInitialized(), object->IsValid(), object->GetTypeInfo()->GetName());
//RE::BSTHashMap<RE::VMStackID, RE::BSTSmartPointer<RE::BSScript::Stack>> runningStacks = a_vm->allRunningStacks;
//_MESSAGE("allRunningStacks size: %d", runningStacks.size());
//for (auto entry = runningStacks.begin(); entry != runningStacks.end(); ++entry) {
//_MESSAGE("allRunningStacks %d", entry->first);
//}
//a_vm->DispatchMethodCall(object, RE::BSFixedString("Enable"), RE::MakeFunctionArguments<>(), RE::BSScript::IStackCallbackFunctor::IStackCallbackFunctor())
}
else {
logger::info(FMT_STRING("LoadInteriorRefList lookup ref not found, ref_mod_name: {}, ref_local_form_id: {:x}"), ref.ref_mod_name, ref.ref_local_form_id);
}
}
if (!game_ref) { // temporary reference or non-temp ref not found, recreate from base form
RE::TESForm * form = data_handler->LookupForm(ref.base_local_form_id, ref.base_mod_name);
if (!form) { // form is not found, might be in an uninstalled mod
logger::warn(FMT_STRING("LoadInteriorRefList not spawning ref for form that could not be found in installed mods, base_mod_name: {}, base_local_form_id: {:d}"), ref.base_mod_name, ref.base_local_form_id);
continue;
}
logger::info(FMT_STRING("LoadInteriorRefList lookup form name: {}, form_id: {:x}"), form->GetName(), (uint32_t)form->GetFormID());
game_ref = PlaceAtMe_Native(a_vm, 0, target_ref, form, 1, false, false);
}
MoveTo_Native(game_ref, game_ref->CreateRefHandle(), cell, cell->worldSpace, position, angle);
game_ref->data.angle = angle; // set angle directly to fix bug with MoveTo in an unloaded target cell
}
}
else {
const char * error = result.AsErr();
logger::error(FMT_STRING("LoadInteriorRefList get_interior_ref_list error: {}"), error);
return false;
}
}
else {
logger::error("LoadInteriorRefList target_ref is null!");
return false;
}
//RE::TESForm * gold = RE::TESForm::LookupByID(15);
//_MESSAGE("Gold form name: %s", gold->GetName());
//_MESSAGE("Gold form id: %d", gold->GetFormID());
//using func_t = decltype(&PlaceAtMe);
//REL::Offset<func_t> func{ REL::ID(55672) };
//RE::TESObjectREFR * new_ref = func(RE::BSScript::Internal::VirtualMachine::GetSingleton(), 1, player, gold, 50, false, false);
//_MESSAGE("New ref initially disabled: %d", new_ref->IsDisabled());
//_MESSAGE("New ref persistent: %d", new_ref->loadedData->flags & RE::TESObjectREFR::RecordFlags::kPersistent);
return true;
}

5
src/BRInteriorRefList.h Normal file
View File

@@ -0,0 +1,5 @@
#pragma once
int CreateInteriorRefList(RE::StaticFunctionTag*, RE::BSFixedString api_url, RE::BSFixedString api_key, uint32_t shop_id, RE::TESObjectCELL * cell);
bool ClearCell(RE::StaticFunctionTag*, RE::TESObjectCELL* cell_ignored);
bool LoadInteriorRefList(RE::StaticFunctionTag*, RE::BSFixedString api_url, RE::BSFixedString api_key, uint32_t interior_ref_list_id, RE::TESObjectCELL* cell_ignored, RE::TESObjectREFR* target_ref);

624
src/BRMerchandiseList.cpp Normal file
View File

@@ -0,0 +1,624 @@
#include "bindings.h"
#include "NativeFunctions.h"
// TODO: replace "placeholder" with "buy_activator" and "ref" with "item_ref"
bool ClearMerchandiseImpl(RE::TESObjectREFR* merchant_chest, RE::TESObjectREFR* merchant_shelf, RE::TESForm* placeholder_static, RE::BGSKeyword * shelf_keyword, RE::BGSKeyword * item_keyword)
{
logger::info("Entered ClearMerchandise");
if (merchant_chest && merchant_shelf) {
RE::TESObjectCELL * cell = merchant_shelf->GetParentCell();
RE::FormID placeholder_form_id = placeholder_static->GetFormID();
RE::FormID shelf_form_id = merchant_shelf->GetFormID();
for (auto entry = cell->references.begin(); entry != cell->references.end();)
{
RE::TESObjectREFR * ref = (*entry).get();
logger::info(FMT_STRING("ClearMerchandise ref form_id: {:x}, disabled: {:d}, marked for deletion: {:d}, deleted: {:d}"), (uint32_t)ref->GetFormID(), ref->IsDisabled(), ref->IsMarkedForDeletion(), ref->IsDeleted());
RE::TESBoundObject * base = ref->GetBaseObject();
if (base) {
RE::FormID form_id = base->GetFormID();
if (form_id == placeholder_form_id) {
logger::info("ClearMerchandise found placeholder ref");
RE::TESObjectREFR * shelf_linked_ref = ref->GetLinkedRef(shelf_keyword);
if (shelf_linked_ref && shelf_linked_ref->GetFormID() == shelf_form_id) {
logger::info("ClearMerchandise placeholder ref is linked with cleared shelf");
RE::TESObjectREFR * linked_ref = ref->GetLinkedRef(item_keyword);
if (linked_ref) {
logger::info(FMT_STRING("ClearMerchandise deleting ref linked to placeholder ref: {:x}"), (uint32_t)linked_ref->GetFormID());
// TODO: should I use the MemoryManager to free these references?
// linked_ref->Load3D(false);
// linked_ref->SetPosition(linked_ref->GetPosition() -= RE::NiPoint3(-10000, -10000, -10000));
// linked_ref->Update3DPosition(false);
// linked_ref->Set3D(ref->GetCurrent3D());
// linked_ref->Release3DRelatedData();
linked_ref->Disable(); // disabling first is required to prevent CTD on unloading cell
linked_ref->SetDelete(true);
// linked_ref->DeleteThis(); // does this do anything?
}
logger::info(FMT_STRING("ClearMerchandise deleting existing placeholder ref: {:x}"), (uint32_t)ref->GetFormID());
// ref->Load3D(false);
// ref->SetPosition(ref->GetPosition() -= RE::NiPoint3(-10000, -10000, -10000));
// ref->Update3DPosition(false);
// ref->Release3DRelatedData();
ref->Disable(); // disabling first is required to prevent CTD on unloading cell
ref->SetDelete(true);
// ref->DeleteThis(); // does this do anything?
cell->references.erase(*entry++); // prevents slowdowns after many runs of ClearMerchandise
}
else {
++entry;
}
}
else {
++entry;
}
}
else {
if (ref->IsDisabled() && ref->IsMarkedForDeletion() && ref->IsDeleted()) {
logger::info("ClearMerchandise ref is probably an item from old LoadMerchandise, clearing from cell now");
cell->references.erase(*entry++);
}
else {
logger::info("ClearMerchandise ref has no base, skipping");
++entry;
}
}
}
} else {
logger::error("ClearMerchandise merchant_chest or merchant_shelf is null!");
return false;
}
return true;
}
bool LoadMerchandiseImpl(
RE::BSFixedString api_url,
RE::BSFixedString api_key,
uint32_t merchandise_list_id,
RE::TESObjectREFR* merchant_shelf,
RE::TESForm* placeholder_static,
RE::BGSKeyword* shelf_keyword,
RE::BGSKeyword* chest_keyword,
RE::BGSKeyword* item_keyword,
RE::BGSKeyword* toggle_keyword,
RE::BGSKeyword* next_keyword,
RE::BGSKeyword* prev_keyword,
int page)
{
logger::info("Entered LoadMerchandise");
logger::info(FMT_STRING("LoadMerchandise page: {:d}"), page);
RE::TESDataHandler * data_handler = RE::TESDataHandler::GetSingleton();
RE::BSScript::Internal::VirtualMachine * a_vm = RE::BSScript::Internal::VirtualMachine::GetSingleton();
using func_t = decltype(&PlaceAtMe);
REL::Relocation<func_t> PlaceAtMe_Native{ REL::ID(55672) };
using func_t2 = decltype(&MoveTo);
REL::Relocation<func_t2> MoveTo_Native(RE::Offset::TESObjectREFR::MoveTo);
REL::ID extra_linked_ref_vtbl(static_cast<std::uint64_t>(229564));
if (!merchant_shelf) {
logger::error("LoadMerchandise merchant_shelf is null!");
return false;
}
RE::TESObjectREFR * merchant_chest = merchant_shelf->GetLinkedRef(chest_keyword);
if (!merchant_chest) {
logger::error("LoadMerchandise merchant_chest is null!");
return false;
}
FFIResult<MerchRecordVec> result = get_merchandise_list(api_url.c_str(), api_key.c_str(), merchandise_list_id);
if (result.IsOk()) {
logger::info("LoadMerchandise get_merchandise_list result OK");
MerchRecordVec vec = result.AsOk();
logger::info(FMT_STRING("LoadMerchandise vec len: {:d}, cap: {:d}"), vec.len, vec.cap);
int max_page = std::ceil((float)(vec.len - 1) / (float)9);
if (vec.len > 0 && page > max_page) {
logger::info(FMT_STRING("LoadMerchandise page {:d} is greater than max_page {:d}, doing nothing"), page, max_page);
return true;
}
ClearMerchandiseImpl(merchant_chest, merchant_shelf, placeholder_static, shelf_keyword, item_keyword);
logger::info(FMT_STRING("LoadMerchandise current shelf page is: {:d}"), merchant_shelf->extraList.GetCount());
for (int i = 0; i < vec.len; i++) {
MerchRecord rec = vec.ptr[i];
logger::info(FMT_STRING("LoadMerchandise item: {:d}"), i);
if (i < (page - 1) * 9 || i >= (page - 1) * 9 + 9) {
continue;
}
RE::TESForm * form = data_handler->LookupForm(rec.local_form_id, rec.mod_name);
if (!form) { // form is not found, might be in an uninstalled mod
logger::warn(FMT_STRING("LoadMerchandise not spawning ref for form that could not be found in installed mods: {} {:d}"), rec.mod_name, rec.local_form_id);
continue;
}
logger::info(FMT_STRING("LoadMerchandise lookup form name: {}, form_id: {:x}, form_type: {:x}"), form->GetName(), (uint32_t)form->GetFormID(), (uint32_t)form->GetFormType());
RE::TESObjectREFR * ref = PlaceAtMe_Native(a_vm, 0, merchant_shelf, form, 1, false, false);
RE::TESBoundObject * base = ref->GetBaseObject();
RE::NiNPShortPoint3 boundMin = base->boundData.boundMin;
RE::NiNPShortPoint3 boundMax = base->boundData.boundMax;
uint16_t bound_x = boundMax.x > boundMin.x ? boundMax.x - boundMin.x : boundMin.x - boundMax.x;
uint16_t bound_y = boundMax.y > boundMin.y ? boundMax.y - boundMin.y : boundMin.y - boundMax.y;
uint16_t bound_z = boundMax.z > boundMin.z ? boundMax.z - boundMin.z : boundMin.z - boundMax.z;
logger::info(FMT_STRING("LoadMerchandise ref bounds width: {:d}, length: {:d}, height: {:d}"), bound_x, bound_y, bound_z);
RE::TESObjectREFR * placeholder_ref = PlaceAtMe_Native(a_vm, 0, merchant_shelf, placeholder_static, 1, false, false);
RE::NiPoint3 bound_min = ref->GetBoundMin();
RE::NiPoint3 bound_max = ref->GetBoundMax();
logger::info(FMT_STRING("LoadMerchandise ref bounds min: {:.2f} {:.2f} {:.2f}, max: {:.2f} {:.2f} {:.2f}"), bound_min.x, bound_min.y, bound_min.z, bound_max.x, bound_max.y, bound_max.z);
RE::ExtraLinkedRef * extra_linked_ref = (RE::ExtraLinkedRef*)RE::BSExtraData::Create(sizeof(RE::ExtraLinkedRef), extra_linked_ref_vtbl.address());
// RE::BGSKeywordForm * place_keyword1 = data_handler->LookupForm<RE::BGSKeywordForm>(595228, "BazaarRealm.esm");
extra_linked_ref->linkedRefs.push_back({shelf_keyword, merchant_shelf});
placeholder_ref->extraList.Add(extra_linked_ref);
// _MESSAGE("PLACEHOLDER LINKED REF: %s", placeholder_ref->GetLinkedRef(nullptr)->GetName());
// This extra count stored on the placeholder_ref indicates the quanity of the merchandise item it is linked to
RE::ExtraCount * extra_page_num = (RE::ExtraCount*)RE::BSExtraData::Create(sizeof(RE::ExtraCount), RE::Offset::ExtraCount::Vtbl.address());
extra_page_num->count = rec.quantity;
placeholder_ref->extraList.Add(extra_page_num);
float scale = 1;
int max_over_bound = 0;
if (max_over_bound < bound_x - 34) {
max_over_bound = bound_x - 34;
}
if (max_over_bound < bound_y - 34) {
max_over_bound = bound_y - 34;
}
if (max_over_bound < bound_z - 34) {
max_over_bound = bound_z - 34;
}
if (max_over_bound > 0) {
scale = ((float)34 / (float)(max_over_bound + 34)) * (float)100;
logger::info(FMT_STRING("LoadMerchandise new scale: {:.2f} {:d} (max_over_bound: {:d}"), scale, static_cast<uint16_t>(scale), max_over_bound);
ref->refScale = static_cast<uint16_t>(scale);
placeholder_ref->refScale = static_cast<uint16_t>(scale);
}
RE::NiPoint3 shelf_position = merchant_shelf->data.location;
RE::NiPoint3 shelf_angle = merchant_shelf->data.angle;
RE::NiPoint3 ref_angle = RE::NiPoint3(shelf_angle.x, shelf_angle.y, shelf_angle.z - 3.14);
RE::NiPoint3 ref_position;
int x_imbalance = (((bound_min.x * -1) - bound_max.x) * (scale / 100)) / 2;
int y_imbalance = (((bound_min.y * -1) - bound_max.y) * (scale / 100)) / 2;
// adjusts z-height so item doesn't spawn underneath it's shelf
int z_imbalance = (bound_min.z * -1) - bound_max.z;
if (z_imbalance < 0) {
z_imbalance = 0;
}
// TODO: make page size and buy_activator positions configurable per "shelf" type (where is config stored?)
if (i % 9 == 0) {
ref_position = RE::NiPoint3(shelf_position.x + 40 + x_imbalance, shelf_position.y + y_imbalance, shelf_position.z + 110 + z_imbalance);
}
else if (i % 9 == 1) {
ref_position = RE::NiPoint3(shelf_position.x + x_imbalance, shelf_position.y + y_imbalance, shelf_position.z + 110 + z_imbalance);
}
else if (i % 9 == 2) {
ref_position = RE::NiPoint3(shelf_position.x - 40 + x_imbalance, shelf_position.y + y_imbalance, shelf_position.z + 110 + z_imbalance);
}
else if (i % 9 == 3) {
ref_position = RE::NiPoint3(shelf_position.x + 40 + x_imbalance, shelf_position.y + y_imbalance, shelf_position.z + 65 + z_imbalance);
}
else if (i % 9 == 4) {
ref_position = RE::NiPoint3(shelf_position.x + x_imbalance, shelf_position.y + y_imbalance, shelf_position.z + 65 + z_imbalance);
}
else if (i % 9 == 5) {
ref_position = RE::NiPoint3(shelf_position.x - 40 + x_imbalance, shelf_position.y + y_imbalance, shelf_position.z + 65 + z_imbalance);
}
else if (i % 9 == 6) {
ref_position = RE::NiPoint3(shelf_position.x + 40 + x_imbalance, shelf_position.y + y_imbalance, shelf_position.z + 20 + z_imbalance);
}
else if (i % 9 == 7) {
ref_position = RE::NiPoint3(shelf_position.x + x_imbalance, shelf_position.y + y_imbalance, shelf_position.z + 20 + z_imbalance);
}
else if (i % 9 == 8) {
ref_position = RE::NiPoint3(shelf_position.x - 40 + x_imbalance, shelf_position.y + y_imbalance, shelf_position.z + 20 + z_imbalance);
}
RE::TESObjectCELL * cell = merchant_shelf->GetParentCell();
MoveTo_Native(ref, ref->CreateRefHandle(), cell, cell->worldSpace, ref_position - RE::NiPoint3(10000, 10000, 10000), ref_angle);
MoveTo_Native(placeholder_ref, placeholder_ref->CreateRefHandle(), cell, cell->worldSpace, ref_position, ref_angle);
// ref->Load3D(false);
// Note: passing false to this method occasionally causes the game to crash due to access violation
// RE::NiAVObject * obj_3d = ref->Load3D(true);
// None of this works, havok is still applied, which isn't that bad really
// _MESSAGE("objectReference: %x, GetBaseObject: %x", *ref->data.objectReference, *ref->GetBaseObject());
// RE::NiAVObject * cloned_obj_3d = ref->data.objectReference->Clone3D(placeholder_ref, false);
// _MESSAGE("loaded 3d: %x, cloned 3d: %x", *obj_3d, *cloned_obj_3d);
// Need to Load3D() before calling this:
// ref->SetMotionType(RE::TESObjectREFR::MotionType::kKeyframed);
// obj_3d->SetMotionType(static_cast<uint32_t>(RE::TESObjectREFR::MotionType::kKeyframed));
// obj_3d->SetMotionType(5);
// Fails if loadedData is nullptr (if Load3D is not called first):
// ref->loadedData->flags = ref->loadedData->flags | RE::TESObjectREFR::RecordFlags::kDontHavokSettle | RE::TESObjectREFR::RecordFlags::kCollisionsDisabled | RE::TESObjectREFR::RecordFlags::kCollisionsDisabled;
// ref->InitHavok();
/// ref->DetachHavok(obj_3d);
// ref->SetCollision(false);
// ref->ClampToGround();
// placeholder_ref->Load3D(false);
// RE::NiPointer<RE::NiAVObject> placeholder_3d_data = placeholder_ref->loadedData->data3D;
// _MESSAGE("PLACEHOLDER 3D (pre-set-3d): %x", placeholder_3d_data.get());
// placeholder_ref->Set3D(obj_3d); // steal the 3D model from the item ref
// RE::ExtraLight * x_light = ref->extraList.GetByType<RE::ExtraLight>();
// if (x_light) {
// _MESSAGE("ExtraLight exists on ref: %x", x_light);
// ref->extraList.RemoveByType(RE::ExtraDataType::kLight);
// x_light = ref->extraList.GetByType<RE::ExtraLight>();
// if (!x_light) {
// _MESSAGE("ExtraLight removed");
// }
// else {
// _MESSAGE("After removing ExtraLight: %x", x_light);
// }
// }
// x_light = placeholder_ref->extraList.GetByType<RE::ExtraLight>();
// if (x_light) {
// _MESSAGE("ExtraLight exists on placeholder_ref: %x", x_light);
// placeholder_ref->extraList.RemoveByType(RE::ExtraDataType::kLight);
// x_light = placeholder_ref->extraList.GetByType<RE::ExtraLight>();
// if (!x_light) {
// _MESSAGE("ExtraLight removed");
// }
// else {
// _MESSAGE("After removing ExtraLight: %x", x_light);
// }
// }
RE::BSFixedString name = RE::BSFixedString::BSFixedString(ref->GetName());
placeholder_ref->SetDisplayName(name, true);
// placeholder_ref->SetObjectReference(base);
placeholder_ref->extraList.SetOwner(base); // I'm abusing "owner" to link the activator with the Form that should be bought once activated
// Do I still need to set this flag? I could maybe use the deleted flag instead
// uint32_t phantom_ref_flag = 1 << 9; // this is my own made up ExtraFlags::Flag that marks the reference we stole the 3D from as needing to be deleted at the start of the next LoadMerchandise
// RE::ExtraFlags * x_flags = ref->extraList.GetByType<RE::ExtraFlags>();
// RE::ExtraFlags::Flag new_flags;
// if (x_flags) {
// _MESSAGE("REF XFLAGS pre-set: %x", x_flags->flags);
// new_flags = (RE::ExtraFlags::Flag)((uint32_t)(x_flags->flags) | phantom_ref_flag);
// }
// else {
// new_flags = (RE::ExtraFlags::Flag)phantom_ref_flag;
// }
//ref->extraList.SetExtraFlags(new_flags, true);
// x_flags = ref->extraList.GetByType<RE::ExtraFlags>();
extra_linked_ref->linkedRefs.push_back({item_keyword, ref});
// _MESSAGE("REF XFLAGS post-set: %x", x_flags->flags);
// Test deleting ref that owns 3d
// ref->Disable(); // disabling first is required to prevent CTD on unloading cell
// ref->SetDelete(true);
// ref->Predestroy();
// ref->formFlags |= RE::TESObjectREFR::RecordFlags::kDeleted;
// ref->AddChange(RE::TESObjectREFR::ChangeFlags::kItemExtraData);
// ref->AddChange(RE::TESObjectREFR::ChangeFlags::kGameOnlyExtra);
// ref->AddChange(RE::TESObjectREFR::ChangeFlags::kCreatedOnlyExtra);
// placeholder_ref->inGameFormFlags |= RE::TESObjectREFR::InGameFormFlag::kWantsDelete;
// placeholder_ref->AddChange(RE::TESObjectREFR::ChangeFlags::kGameOnlyExtra);
}
// I'm abusing the ExtraCount ExtraData type for storing the current page number state of the shelf
RE::ExtraCount * extra_page_num = merchant_shelf->extraList.GetByType<RE::ExtraCount>();
if (!extra_page_num) {
extra_page_num = (RE::ExtraCount*)RE::BSExtraData::Create(sizeof(RE::ExtraCount), RE::Offset::ExtraCount::Vtbl.address());
merchant_shelf->extraList.Add(extra_page_num);
}
extra_page_num->count = page;
logger::info(FMT_STRING("LoadMerchandise set shelf page to: {:d}"), merchant_shelf->extraList.GetCount());
// I'm abusing the ExtraCannotWear ExtraData type as a boolean marker which stores whether the shelf is in a loaded or cleared state
// The presense of ExtraCannotWear == loaded, its absence == cleared
// Please don't try to wear the shelves :)
RE::ExtraCannotWear * extra_is_loaded = merchant_shelf->extraList.GetByType<RE::ExtraCannotWear>();
if (!extra_is_loaded) {
extra_is_loaded = (RE::ExtraCannotWear*)RE::BSExtraData::Create(sizeof(RE::ExtraCannotWear), RE::Offset::ExtraCannotWear::Vtbl.address());
merchant_shelf->extraList.Add(extra_is_loaded);
}
logger::info(FMT_STRING("LoadMerchandise set loaded: {:d}"), merchant_shelf->extraList.GetByType<RE::ExtraCannotWear>() != nullptr);
RE::TESObjectREFR * toggle_ref = merchant_shelf->GetLinkedRef(toggle_keyword);
if (!toggle_ref) {
logger::error("LoadMerchandise toggle_ref is null!");
return false;
}
RE::TESObjectREFR * next_ref = merchant_shelf->GetLinkedRef(next_keyword);
if (!next_ref) {
logger::error("LoadMerchandise next_ref is null!");
return false;
}
RE::TESObjectREFR * prev_ref = merchant_shelf->GetLinkedRef(prev_keyword);
if (!prev_ref) {
logger::error("LoadMerchandise prev_ref is null!");
return false;
}
toggle_ref->SetDisplayName("Clear merchandise", true);
if (page == max_page) {
next_ref->SetDisplayName("(No next page)", true);
}
else {
next_ref->SetDisplayName(fmt::format("Advance to page %d", page + 1).c_str(), true);
}
if (page == 1) {
prev_ref->SetDisplayName("(No previous page)", true);
}
else {
prev_ref->SetDisplayName(fmt::format("Back to page %d", page - 1).c_str(), true);
}
}
else {
const char * error = result.AsErr();
logger::error(FMT_STRING("LoadMerchandise get_merchandise_list error: {}"), error);
return false;
}
return true;
}
bool ToggleMerchandise(
RE::StaticFunctionTag*,
RE::BSFixedString api_url,
RE::BSFixedString api_key,
uint32_t merchandise_list_id,
RE::TESObjectREFR* merchant_shelf,
RE::TESForm* placeholder_static,
RE::BGSKeyword* shelf_keyword,
RE::BGSKeyword* chest_keyword,
RE::BGSKeyword* item_keyword,
RE::BGSKeyword* toggle_keyword,
RE::BGSKeyword* next_keyword,
RE::BGSKeyword* prev_keyword)
{
if (!merchant_shelf) {
logger::error("ToggleMerchandise merchant_shelf is null!");
return false;
}
// I'm abusing the ExtraCannotWear ExtraData type as a boolean marker which stores whether the shelf is in a loaded or cleared state
// The presense of ExtraCannotWear == loaded, its absence == cleared
// Please don't try to wear the shelves :)
RE::ExtraCannotWear * extra_is_loaded = merchant_shelf->extraList.GetByType<RE::ExtraCannotWear>();
if (extra_is_loaded) {
// Clear merchandise
RE::TESObjectREFR * merchant_chest = merchant_shelf->GetLinkedRef(chest_keyword);
if (!ClearMerchandiseImpl(merchant_chest, merchant_shelf, placeholder_static, shelf_keyword, item_keyword)) {
return false;
}
// Reset shelf page to 1 and set state to cleared
merchant_shelf->extraList.RemoveByType(RE::ExtraDataType::kCount);
merchant_shelf->extraList.RemoveByType(RE::ExtraDataType::kCannotWear);
RE::TESObjectREFR * toggle_ref = merchant_shelf->GetLinkedRef(toggle_keyword);
if (!toggle_ref) {
logger::error("ToggleMerchandise toggle_ref is null!");
return false;
}
RE::TESObjectREFR * next_ref = merchant_shelf->GetLinkedRef(next_keyword);
if (!next_ref) {
logger::error("ToggleMerchandise next_ref is null!");
return false;
}
RE::TESObjectREFR * prev_ref = merchant_shelf->GetLinkedRef(prev_keyword);
if (!prev_ref) {
logger::error("ToggleMerchandise prev_ref is null!");
return false;
}
toggle_ref->SetDisplayName("Load merchandise", true);
next_ref->SetDisplayName("Load merchandise", true);
prev_ref->SetDisplayName("Load merchandise", true);
return true;
}
else {
// Load merchandise
int page = merchant_shelf->extraList.GetCount();
return LoadMerchandiseImpl(api_url, api_key, merchandise_list_id, merchant_shelf, placeholder_static, shelf_keyword, chest_keyword, item_keyword, toggle_keyword, next_keyword, prev_keyword, page);
}
}
bool LoadNextMerchandise(
RE::StaticFunctionTag*,
RE::BSFixedString api_url,
RE::BSFixedString api_key,
uint32_t merchandise_list_id,
RE::TESObjectREFR* merchant_shelf,
RE::TESForm* placeholder_static,
RE::BGSKeyword* shelf_keyword,
RE::BGSKeyword* chest_keyword,
RE::BGSKeyword* item_keyword,
RE::BGSKeyword* toggle_keyword,
RE::BGSKeyword* next_keyword,
RE::BGSKeyword* prev_keyword)
{
if (!merchant_shelf) {
logger::error("LoadNextMerchandise merchant_shelf is null!");
return false;
}
int page = merchant_shelf->extraList.GetCount();
RE::ExtraCannotWear * extra_is_loaded = merchant_shelf->extraList.GetByType<RE::ExtraCannotWear>();
if (extra_is_loaded) {
// Only advance the page if shelf is in loaded state, else just load the (first) page
page = page + 1;
}
return LoadMerchandiseImpl(api_url, api_key, merchandise_list_id, merchant_shelf, placeholder_static, shelf_keyword, chest_keyword, item_keyword, toggle_keyword, next_keyword, prev_keyword, page);
}
bool LoadPrevMerchandise(
RE::StaticFunctionTag*,
RE::BSFixedString api_url,
RE::BSFixedString api_key,
uint32_t merchandise_list_id,
RE::TESObjectREFR* merchant_shelf,
RE::TESForm* placeholder_static,
RE::BGSKeyword* shelf_keyword,
RE::BGSKeyword* chest_keyword,
RE::BGSKeyword* item_keyword,
RE::BGSKeyword* toggle_keyword,
RE::BGSKeyword* next_keyword,
RE::BGSKeyword* prev_keyword)
{
if (!merchant_shelf) {
logger::error("LoadPrevMerchandise merchant_shelf is null!");
return false;
}
int page = merchant_shelf->extraList.GetCount();
if (page == 1) { // no-op on first page
return true;
}
RE::ExtraCannotWear * extra_is_loaded = merchant_shelf->extraList.GetByType<RE::ExtraCannotWear>();
if (extra_is_loaded) {
// Only advance the page if shelf is in loaded state, else just load the (first) page
page = page - 1;
}
return LoadMerchandiseImpl(api_url, api_key, merchandise_list_id, merchant_shelf, placeholder_static, shelf_keyword, chest_keyword, item_keyword, toggle_keyword, next_keyword, prev_keyword, page);
}
bool ReplaceMerch3D(RE::StaticFunctionTag*, RE::TESObjectREFR* merchant_shelf, RE::TESForm* placeholder_static, RE::BGSKeyword* shelf_keyword, RE::BGSKeyword* item_keyword) {
logger::info("Entered ReplaceMerch3D");
if (!merchant_shelf) {
logger::error("LoadMerchandise merchant_shelf is null!");
return false;
}
RE::TESObjectCELL * cell = merchant_shelf->GetParentCell();
RE::FormID placeholder_form_id = placeholder_static->GetFormID();
RE::FormID shelf_form_id = merchant_shelf->GetFormID();
for (auto entry = cell->references.begin(); entry != cell->references.end(); ++entry)
{
RE::TESObjectREFR * ref = (*entry).get();
RE::TESBoundObject * base = ref->GetBaseObject();
if (base) {
if (base->GetFormID() == placeholder_form_id) {
logger::info(FMT_STRING("ReplaceMerch3D REF is a placeholder ref: {:x}"), (uint32_t)ref->GetFormID());
RE::TESObjectREFR * shelf_linked_ref = ref->GetLinkedRef(shelf_keyword);
if (shelf_linked_ref && shelf_linked_ref->GetFormID() == shelf_form_id) {
logger::info("ReplaceMerch3D placeholder ref is linked with loaded shelf");
RE::TESObjectREFR * linked_ref = ref->GetLinkedRef(item_keyword);
if (linked_ref) {
logger::info(FMT_STRING("ReplaceMerch3D placeholder has linked item ref: {:x}"), (uint32_t)linked_ref->GetFormID());
if (linked_ref->Is3DLoaded()) {
logger::info("ReplaceMerch3D replaceing placeholder 3D with linked item 3D");
ref->Set3D(linked_ref->GetCurrent3D());
}
else {
logger::info("ReplaceMerch3D linked item ref 3D is not loaded yet, returning false");
return false;
}
}
}
}
} else {
logger::info("ReplaceMerch3D ref has no base, skipping");
}
}
return true;
}
RE::TESForm * BuyMerchandise(RE::StaticFunctionTag*, RE::TESObjectREFR * merchandise_placeholder) {
logger::info("Entered BuyMerchandise");
logger::info(FMT_STRING("BuyMerchandise activated ref: {}"), merchandise_placeholder->GetName());
RE::TESForm * owner = merchandise_placeholder->GetOwner();
logger::info(FMT_STRING("BuyMerchandise owner: {}"), owner->GetName());
logger::info(FMT_STRING("BuyMerchandise count: {:d}"), merchandise_placeholder->extraList.GetCount());
// TODO: do add item here
// player->AddObjectToContainer(owner, *RE::ExtraDataList::ExtraDataList(), 1, merchandise_placeholder)
return owner;
}
// Return code:
// -2: No changes to save, no create request was made
// -1: Error occured
// >= 0: ID of created MerchandiseList returned by API
int CreateMerchandiseList(RE::StaticFunctionTag*, RE::BSFixedString api_url, RE::BSFixedString api_key, uint32_t shop_id, RE::TESObjectREFR* merchant_chest)
{
logger::info("Entered CreateMerchandiseList");
RE::TESDataHandler * data_handler = RE::TESDataHandler::GetSingleton();
std::vector<MerchRecord> merch_records;
if (!merchant_chest) {
logger::error("CreateMerchandiseList merchant_chest is null!");
return -1;
}
RE::InventoryChanges * inventory_changes = merchant_chest->GetInventoryChanges();
if (inventory_changes == nullptr) {
logger::info("CreateMerchandiseList container empty, nothing to save");
return -2;
}
RE::BSSimpleList<RE::InventoryEntryData*>* entries = inventory_changes->entryList;
int count = 0;
for (auto entry = entries->begin(); entry != entries->end(); ++entry) {
logger::info(FMT_STRING("CreateMerchandiseList container iterator count: {:d}"), count);
RE::InventoryEntryData * entry_data = *entry;
RE::TESBoundObject * base = entry_data->GetObject();
if (base) {
RE::BSSimpleList<RE::ExtraDataList*> * x_lists = entry_data->extraLists;
if (x_lists) {
const char * entry_name = entry_data->extraLists->front()->GetDisplayName(base);
logger::info(FMT_STRING("CreateMerchandiseList container item display_name: {}"), entry_name);
int x_list_count = 0;
for (auto x_list = entry_data->extraLists->begin(); x_list != entry_data->extraLists->end(); ++x_list) {
logger::info(FMT_STRING("CreateMerchandiseList container item x_list: {:d}"), x_list_count);
int x_data_count = 0;
for (auto x_data = (*x_list)->begin(); x_data != (*x_list)->end(); ++x_data) {
logger::info(FMT_STRING("CreateMerchandiseList container item x_data: {:d}, type: {:x}"), x_data_count, (uint32_t)x_data->GetType());
x_data_count++;
}
RE::ExtraEnchantment * enchantment = (RE::ExtraEnchantment*)(*x_list)->GetByType(RE::ExtraDataType::kEnchantment);
if (enchantment) {
logger::info(FMT_STRING("CreateMerchandiseList container item enchantment charge: {:d}"), enchantment->charge);
}
RE::ExtraScale * scale = (RE::ExtraScale*)(*x_list)->GetByType(RE::ExtraDataType::kScale);
if (scale) {
logger::info(FMT_STRING("CreateMerchandiseList container item scale: {:.2f}"), scale->scale);
}
x_list_count++;
}
}
const char * name = base->GetName();
uint32_t form_type = (uint32_t)base->GetFormType();
uint32_t quantity = entry_data->countDelta;
RE::FormID form_id = base->GetFormID();
logger::info(FMT_STRING("CreateMerchandiseList quantity: {:d}"), quantity);
logger::info(FMT_STRING("CreateMerchandiseList base form_id: {:x}, name: {}, type: {:x}"), (uint32_t)form_id, name, (uint32_t)form_type);
RE::TESFile * file = base->GetFile(0);
const char * mod_name = file->fileName;
bool is_light = file->recordFlags.all(RE::TESFile::RecordFlag::kSmallFile);
uint32_t local_form_id = is_light ? form_id & 0xfff : form_id & 0xFFFFFF;
//_MESSAGE("FILE: %s isLight: %d formID: 0x%x localFormId: 0x%x", file_name, is_light, form_id, local_form_id);
logger::info(FMT_STRING("CreateMerchandiseList base form file_name: {}, local_form_id"), mod_name, local_form_id);
// TODO: implement is_food
uint8_t is_food = 0;
// TODO: implement price
uint32_t price = 0;
merch_records.push_back({
mod_name,
local_form_id,
name,
quantity,
form_type,
is_food,
price,
});
}
count++;
}
int merchandise_list_id = create_merchandise_list(api_url.c_str(), api_key.c_str(), shop_id, &merch_records[0], merch_records.size());
logger::info(FMT_STRING("CreateMerchandiseList create_merchandise_list result: {:d}"), merchandise_list_id);
return merchandise_list_id;
}

45
src/BRMerchandiseList.h Normal file
View File

@@ -0,0 +1,45 @@
#pragma once
bool ClearMerchandiseImpl(RE::TESObjectREFR* merchant_chest, RE::TESObjectREFR* merchant_shelf, RE::TESForm* placeholder_static, RE::BGSKeyword * shelf_keyword, RE::BGSKeyword * item_keyword);
bool ToggleMerchandise(
RE::StaticFunctionTag*,
RE::BSFixedString api_url,
RE::BSFixedString api_key,
uint32_t merchandise_list_id,
RE::TESObjectREFR* merchant_shelf,
RE::TESForm* placeholder_static,
RE::BGSKeyword* shelf_keyword,
RE::BGSKeyword* chest_keyword,
RE::BGSKeyword* item_keyword,
RE::BGSKeyword* toggle_keyword,
RE::BGSKeyword* next_keyword,
RE::BGSKeyword* prev_keyword);
bool LoadNextMerchandise(
RE::StaticFunctionTag*,
RE::BSFixedString api_url,
RE::BSFixedString api_key,
uint32_t merchandise_list_id,
RE::TESObjectREFR* merchant_shelf,
RE::TESForm* placeholder_static,
RE::BGSKeyword* shelf_keyword,
RE::BGSKeyword* chest_keyword,
RE::BGSKeyword* item_keyword,
RE::BGSKeyword* toggle_keyword,
RE::BGSKeyword* next_keyword,
RE::BGSKeyword* prev_keyword);
bool LoadPrevMerchandise(
RE::StaticFunctionTag*,
RE::BSFixedString api_url,
RE::BSFixedString api_key,
uint32_t merchandise_list_id,
RE::TESObjectREFR* merchant_shelf,
RE::TESForm* placeholder_static,
RE::BGSKeyword* shelf_keyword,
RE::BGSKeyword* chest_keyword,
RE::BGSKeyword* item_keyword,
RE::BGSKeyword* toggle_keyword,
RE::BGSKeyword* next_keyword,
RE::BGSKeyword* prev_keyword);
bool ReplaceMerch3D(RE::StaticFunctionTag*, RE::TESObjectREFR* merchant_shelf, RE::TESForm* placeholder_static, RE::BGSKeyword* shelf_keyword, RE::BGSKeyword* item_keyword);
RE::TESForm * BuyMerchandise(RE::StaticFunctionTag*, RE::TESObjectREFR * merchandise_placeholder);
int CreateMerchandiseList(RE::StaticFunctionTag*, RE::BSFixedString api_url, RE::BSFixedString api_key, uint32_t shop_id, RE::TESObjectREFR* merchant_chest);

13
src/BROwner.cpp Normal file
View File

@@ -0,0 +1,13 @@
#include "bindings.h"
int CreateOwner(RE::StaticFunctionTag*, RE::BSFixedString api_url, RE::BSFixedString api_key, RE::BSFixedString name, uint32_t mod_version)
{
logger::info("Entered CreateOwner");
logger::info(FMT_STRING("CreateOwner api_url: {}"), api_url);
logger::info(FMT_STRING("CreateOwner api_key: {}"), api_key);
logger::info(FMT_STRING("CreateOwner name: {}"), name);
logger::info(FMT_STRING("CreateOwner mod_version: {}"), mod_version);
int owner_id = create_owner(api_url.c_str(), api_key.c_str(), name.c_str(), mod_version);
logger::info(FMT_STRING("CreateOwner result: {}"), owner_id);
return owner_id;
}

3
src/BROwner.h Normal file
View File

@@ -0,0 +1,3 @@
#pragma once
int CreateOwner(RE::StaticFunctionTag*, RE::BSFixedString api_url, RE::BSFixedString api_key, RE::BSFixedString name, uint32_t mod_version);

13
src/BRShop.cpp Normal file
View File

@@ -0,0 +1,13 @@
#include "bindings.h"
int CreateShop(RE::StaticFunctionTag*, RE::BSFixedString api_url, RE::BSFixedString api_key, RE::BSFixedString name, RE::BSFixedString description)
{
logger::info("Entered CreateShop");
logger::info(FMT_STRING("CreateShop api_url: {}"), api_url);
logger::info(FMT_STRING("CreateShop api_key: {}"), api_key);
logger::info(FMT_STRING("CreateShop name: {}"), name);
logger::info(FMT_STRING("CreateShop description: {}"), description);
int shop_id = create_shop(api_url.c_str(), api_key.c_str(), name.c_str(), description.c_str());
logger::info(FMT_STRING("CreateShop result: {}"), shop_id);
return shop_id;
}

3
src/BRShop.h Normal file
View File

@@ -0,0 +1,3 @@
#pragma once
int CreateShop(RE::StaticFunctionTag*, RE::BSFixedString api_url, RE::BSFixedString api_key, RE::BSFixedString name, RE::BSFixedString description);

4
src/NativeFunctions.h Normal file
View File

@@ -0,0 +1,4 @@
#pragma once
RE::TESObjectREFR* PlaceAtMe(RE::BSScript::IVirtualMachine* a_vm, int something, RE::TESObjectREFR* ref, RE::TESForm* form, int count, bool forcePersist, bool initiallyDisabled);
void MoveTo(RE::TESObjectREFR * ref, const RE::ObjectRefHandle& a_targetHandle, RE::TESObjectCELL* a_targetCell, RE::TESWorldSpace* a_selfWorldSpace, const RE::NiPoint3& a_position, const RE::NiPoint3& a_rotation);

1
src/PCH.cpp Normal file
View File

@@ -0,0 +1 @@
#include "PCH.h"

16
src/PCH.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include "RE/Skyrim.h"
#include "SKSE/SKSE.h"
#ifdef NDEBUG
#include <spdlog/sinks/basic_file_sink.h>
#else
#include <spdlog/sinks/msvc_sink.h>
#endif
using namespace std::literals;
namespace logger = SKSE::log;
#define DLLEXPORT __declspec(dllexport)

88
src/main.cpp Normal file
View File

@@ -0,0 +1,88 @@
#include "version.h"
#include "bindings.h"
#include "BRClient.h"
#include "BROwner.h"
#include "BRShop.h"
#include "BRInteriorRefList.h"
#include "BRMerchandiseList.h"
bool RegisterFuncs(RE::BSScript::IVirtualMachine* a_vm)
{
a_vm->RegisterFunction("Init", "BRClient", Init);
a_vm->RegisterFunction("StatusCheck", "BRClient", StatusCheck);
a_vm->RegisterFunction("GenerateApiKey", "BRClient", GenerateApiKey);
a_vm->RegisterFunction("Create", "BROwner", CreateOwner);
a_vm->RegisterFunction("Create", "BRShop", CreateShop);
a_vm->RegisterFunction("Create", "BRInteriorRefList", CreateInteriorRefList);
a_vm->RegisterFunction("ClearCell", "BRInteriorRefList", ClearCell);
a_vm->RegisterFunction("Load", "BRInteriorRefList", LoadInteriorRefList);
a_vm->RegisterFunction("Toggle", "BRMerchandiseList", ToggleMerchandise);
a_vm->RegisterFunction("NextPage", "BRMerchandiseList", LoadNextMerchandise);
a_vm->RegisterFunction("PrevPage", "BRMerchandiseList", LoadPrevMerchandise);
a_vm->RegisterFunction("Buy", "BRMerchandiseList", BuyMerchandise);
a_vm->RegisterFunction("Replace3D", "BRMerchandiseList", ReplaceMerch3D);
a_vm->RegisterFunction("Create", "BRMerchandiseList", CreateMerchandiseList);
return true;
}
extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Query(const SKSE::QueryInterface* a_skse, SKSE::PluginInfo* a_info)
{
#ifndef NDEBUG
auto sink = std::make_shared<spdlog::sinks::msvc_sink_mt>();
#else
auto path = logger::log_directory();
if (!path) {
return false;
}
*path /= "BazaarRealmPlugin.log"sv;
auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path->string(), true);
#endif
auto log = std::make_shared<spdlog::logger>("global log"s, std::move(sink));
#ifndef NDEBUG
log->set_level(spdlog::level::trace);
#else
log->set_level(spdlog::level::info);
log->flush_on(spdlog::level::warn);
#endif
spdlog::set_default_logger(std::move(log));
spdlog::set_pattern("%g(%#): [%^%l%$] %v"s);
logger::info(FMT_STRING("BazaarRealmPlugin v{}"), MYFP_VERSION_VERSTRING);
a_info->infoVersion = SKSE::PluginInfo::kVersion;
a_info->name = "BazaarRealmPlugin";
a_info->version = MYFP_VERSION_MAJOR;
if (a_skse->IsEditor()) {
logger::critical("Loaded in editor, marking as incompatible"sv);
return false;
}
const auto ver = a_skse->RuntimeVersion();
if (ver < SKSE::RUNTIME_1_5_39) {
logger::critical(FMT_STRING("Unsupported runtime version {}"), ver.string());
return false;
}
return true;
}
extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse)
{
logger::info("BazaarRealmPlugin loaded");
SKSE::Init(a_skse);
auto papyrus = SKSE::GetPapyrusInterface();
if (!papyrus->Register(RegisterFuncs)) {
return false;
}
return true;
}

15
src/version.h Normal file
View File

@@ -0,0 +1,15 @@
#ifndef MYFP_VERSION_INCLUDED
#define MYFP_VERSION_INCLUDED
#define MAKE_STR_HELPER(a_str) #a_str
#define MAKE_STR(a_str) MAKE_STR_HELPER(a_str)
#define MYFP_VERSION_MAJOR 1
#define MYFP_VERSION_MINOR 0
#define MYFP_VERSION_PATCH 0
#define MYFP_VERSION_BETA 0
#define MYFP_VERSION_VERSTRING \
MAKE_STR(MYFP_VERSION_MAJOR) \
"." MAKE_STR(MYFP_VERSION_MINOR) "." MAKE_STR(MYFP_VERSION_PATCH) "." MAKE_STR(MYFP_VERSION_BETA)
#endif