From 2cd4b643d052a7531113d242989bd7e5014368e6 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Fri, 1 Aug 2025 15:18:55 -0700 Subject: [PATCH 01/25] npc stuff --- apps/openmw/mwlua/objectbindings.cpp | 2 +- apps/openmw/mwlua/types/npc.cpp | 134 ++++++++++++++++++++++++++- apps/openmw/mwlua/worldbindings.cpp | 10 ++ 3 files changed, 144 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 3508fdcd44..708792fa51 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -388,7 +388,7 @@ namespace MWLua objectT["setScale"] = [context](const GObject& object, float scale) { context.mLuaManager->addAction( [object, scale] { MWBase::Environment::get().getWorld()->scaleObject(object.ptr(), scale); }); - }; + };--adjustScale objectT["addScript"] = [context](const GObject& object, std::string_view path, sol::object initData) { const LuaUtil::ScriptsConfiguration& cfg = context.mLua->getConfiguration(); std::optional scriptId = cfg.findId(VFS::Path::Normalized(path)); diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 380a2d1e9b..60f6325ea2 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -29,6 +29,138 @@ namespace sol }; } +namespace +{ +ESM::NPC tableToNPC(const sol::table& rec) +{ + ESM::NPC npc; + + // Start from template if provided + if (rec["template"] != sol::nil) + npc = LuaUtil::cast(rec["template"]); + else + npc.blank(); + + // Force dummy ID + npc.mId = ESM::RefId::deserializeText("blank"); + + // Basic fields + if (rec["name"] != sol::nil) + npc.mName = rec["name"]; + if (rec["model"] != sol::nil) + npc.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["mwscript"] != sol::nil) + npc.mScript = ESM::RefId::deserializeText(rec["mwscript"].get()); + if (rec["race"] != sol::nil) + npc.mRace = ESM::RefId::deserializeText(rec["race"].get()); + if (rec["class"] != sol::nil) + npc.mClass = ESM::RefId::deserializeText(rec["class"].get()); + if (rec["head"] != sol::nil) + npc.mHead = ESM::RefId::deserializeText(rec["head"].get()); + if (rec["hair"] != sol::nil) + npc.mHair = ESM::RefId::deserializeText(rec["hair"].get()); + + if (rec["isMale"] != sol::nil) + { + bool male = rec["isMale"]; + if (male) + npc.mFlags &= ~ESM::NPC::Female; + else + npc.mFlags |= ESM::NPC::Female; + } + + if (rec["isEssential"] != sol::nil) + { + bool essential = rec["isEssential"]; + if (essential) + npc.mFlags |= ESM::NPC::Essential; + else + npc.mFlags &= ~ESM::NPC::Essential; + } + + if (rec["isRespawning"] != sol::nil) + { + bool respawn = rec["isRespawning"]; + if (respawn) + npc.mFlags |= ESM::NPC::Respawn; + else + npc.mFlags &= ~ESM::NPC::Respawn; + } + + if (rec["baseDisposition"] != sol::nil) + npc.mNpdt.mDisposition = static_cast(rec["baseDisposition"]); + + if (rec["baseGold"] != sol::nil) + npc.mNpdt.mGold = static_cast(rec["baseGold"]); + + if (rec["bloodType"] != sol::nil) + npc.mBloodType = static_cast(rec["bloodType"]); + + // Services offered + if (rec["servicesOffered"] != sol::nil) + { + const sol::table services = rec["servicesOffered"]; + int flags = 0; + auto setFlag = [&](const char* key, int mask) { + if (services[key] != sol::nil && services[key]) + flags |= mask; + }; + + setFlag("Spells", ESM::NPC::Spells); + setFlag("Spellmaking", ESM::NPC::Spellmaking); + setFlag("Enchanting", ESM::NPC::Enchanting); + setFlag("Training", ESM::NPC::Training); + setFlag("Repair", ESM::NPC::Repair); + setFlag("Barter", ESM::NPC::AllItems); + setFlag("Weapon", ESM::NPC::Weapon); + setFlag("Armor", ESM::NPC::Armor); + setFlag("Clothing", ESM::NPC::Clothing); + setFlag("Books", ESM::NPC::Books); + setFlag("Ingredients", ESM::NPC::Ingredients); + setFlag("Picks", ESM::NPC::Picks); + setFlag("Probes", ESM::NPC::Probes); + setFlag("Lights", ESM::NPC::Lights); + setFlag("Apparatus", ESM::NPC::Apparatus); + setFlag("RepairItem", ESM::NPC::RepairItem); + setFlag("Misc", ESM::NPC::Misc); + setFlag("Potions", ESM::NPC::Potions); + setFlag("MagicItems", ESM::NPC::MagicItems); + + npc.mAiData.mServices = flags; + } + + // Travel destinations + if (rec["travelDestinations"] != sol::nil) + { + const sol::table travelDests = rec["travelDestinations"]; + npc.mTransport.clear(); + for (std::size_t i = 1; i <= travelDests.size(); ++i) + { + sol::table t = travelDests[i]; + ESM::Transport destination; + + // Position + destination.mPos.pos = t["position"]; + destination.mPos.rot = Misc::Convert::toRotation(LuaUtil::fromTransform(t["rotation"])); + + // Cell + std::string cellId = t["cellId"]; + destination.mCellName = cellId; // If empty, it will be handled as exterior + + npc.mTransport.push_back(destination); + } + } + + return npc; +} + // Blood type + if (rec["bloodType"] != sol::nil) + npc.mBloodType = static_cast(rec["bloodType"]); + + return npc; + } +} + namespace { size_t getValidRanksCount(const ESM::Faction* faction) @@ -151,7 +283,7 @@ namespace MWLua auto& stats = cls.getNpcStats(o.ptr()); stats.setBaseDisposition(stats.getBaseDisposition() + value); }; - + npc["createRecordDraft"] = tableToNPC; npc["getFactionRank"] = [](const Object& actor, std::string_view faction) -> size_t { const MWWorld::Ptr ptr = actor.ptr(); ESM::RefId factionId = parseFactionId(faction); diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index c02bad3bd3..de8eec3710 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -155,6 +156,11 @@ namespace MWLua MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, cell, count.value_or(1)); return GObject(newPtr); }; + api["advanceTime"] = [context](double hours, bool incremental) { + context.mLuaManager->addAction([ hours, incremental] { + MWBase::Environment::get().getWorld()->advanceTime(hours, incremental); + }); + }; api["getObjectByFormId"] = [](std::string_view formIdStr) -> GObject { ESM::RefId refId = ESM::RefId::deserializeText(formIdStr); if (!refId.is()) @@ -188,6 +194,10 @@ namespace MWLua checkGameInitialized(lua); return MWBase::Environment::get().getESMStore()->insert(potion); }, + [lua = context.mLua](const ESM::NPC& npc) -> const ESM::NPC* { + checkGameInitialized(lua); + return MWBase::Environment::get().getESMStore()->insert(npc); + }, [lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* { checkGameInitialized(lua); return MWBase::Environment::get().getESMStore()->insert(weapon); From abb381a163e177331c7758fe56bd3ff370de1cd1 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Fri, 1 Aug 2025 16:10:49 -0700 Subject: [PATCH 02/25] Fix issues --- apps/openmw/mwlua/objectbindings.cpp | 2 +- apps/openmw/mwlua/types/npc.cpp | 202 ++++++++++++--------------- 2 files changed, 88 insertions(+), 116 deletions(-) diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 708792fa51..3508fdcd44 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -388,7 +388,7 @@ namespace MWLua objectT["setScale"] = [context](const GObject& object, float scale) { context.mLuaManager->addAction( [object, scale] { MWBase::Environment::get().getWorld()->scaleObject(object.ptr(), scale); }); - };--adjustScale + }; objectT["addScript"] = [context](const GObject& object, std::string_view path, sol::object initData) { const LuaUtil::ScriptsConfiguration& cfg = context.mLua->getConfiguration(); std::optional scriptId = cfg.findId(VFS::Path::Normalized(path)); diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 60f6325ea2..705e0831fd 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -31,132 +31,104 @@ namespace sol namespace { -ESM::NPC tableToNPC(const sol::table& rec) -{ - ESM::NPC npc; - - // Start from template if provided - if (rec["template"] != sol::nil) - npc = LuaUtil::cast(rec["template"]); - else - npc.blank(); - - // Force dummy ID - npc.mId = ESM::RefId::deserializeText("blank"); - - // Basic fields - if (rec["name"] != sol::nil) - npc.mName = rec["name"]; - if (rec["model"] != sol::nil) - npc.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); - if (rec["mwscript"] != sol::nil) - npc.mScript = ESM::RefId::deserializeText(rec["mwscript"].get()); - if (rec["race"] != sol::nil) - npc.mRace = ESM::RefId::deserializeText(rec["race"].get()); - if (rec["class"] != sol::nil) - npc.mClass = ESM::RefId::deserializeText(rec["class"].get()); - if (rec["head"] != sol::nil) - npc.mHead = ESM::RefId::deserializeText(rec["head"].get()); - if (rec["hair"] != sol::nil) - npc.mHair = ESM::RefId::deserializeText(rec["hair"].get()); - - if (rec["isMale"] != sol::nil) + ESM::NPC tableToNPC(const sol::table& rec) { - bool male = rec["isMale"]; - if (male) - npc.mFlags &= ~ESM::NPC::Female; + ESM::NPC npc; + + // Start from template if provided + if (rec["template"] != sol::nil) + npc = LuaUtil::cast(rec["template"]); else - npc.mFlags |= ESM::NPC::Female; - } + npc.blank(); - if (rec["isEssential"] != sol::nil) - { - bool essential = rec["isEssential"]; - if (essential) - npc.mFlags |= ESM::NPC::Essential; - else - npc.mFlags &= ~ESM::NPC::Essential; - } + // Force dummy ID + npc.mId = ESM::RefId::deserializeText("blank"); - if (rec["isRespawning"] != sol::nil) - { - bool respawn = rec["isRespawning"]; - if (respawn) - npc.mFlags |= ESM::NPC::Respawn; - else - npc.mFlags &= ~ESM::NPC::Respawn; - } + // Basic fields + if (rec["name"] != sol::nil) + npc.mName = rec["name"]; + if (rec["model"] != sol::nil) + npc.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["mwscript"] != sol::nil) + npc.mScript = ESM::RefId::deserializeText(rec["mwscript"].get()); + if (rec["race"] != sol::nil) + npc.mRace = ESM::RefId::deserializeText(rec["race"].get()); + if (rec["class"] != sol::nil) + npc.mClass = ESM::RefId::deserializeText(rec["class"].get()); + if (rec["head"] != sol::nil) + npc.mHead = ESM::RefId::deserializeText(rec["head"].get()); + if (rec["hair"] != sol::nil) + npc.mHair = ESM::RefId::deserializeText(rec["hair"].get()); - if (rec["baseDisposition"] != sol::nil) - npc.mNpdt.mDisposition = static_cast(rec["baseDisposition"]); - - if (rec["baseGold"] != sol::nil) - npc.mNpdt.mGold = static_cast(rec["baseGold"]); - - if (rec["bloodType"] != sol::nil) - npc.mBloodType = static_cast(rec["bloodType"]); - - // Services offered - if (rec["servicesOffered"] != sol::nil) - { - const sol::table services = rec["servicesOffered"]; - int flags = 0; - auto setFlag = [&](const char* key, int mask) { - if (services[key] != sol::nil && services[key]) - flags |= mask; - }; - - setFlag("Spells", ESM::NPC::Spells); - setFlag("Spellmaking", ESM::NPC::Spellmaking); - setFlag("Enchanting", ESM::NPC::Enchanting); - setFlag("Training", ESM::NPC::Training); - setFlag("Repair", ESM::NPC::Repair); - setFlag("Barter", ESM::NPC::AllItems); - setFlag("Weapon", ESM::NPC::Weapon); - setFlag("Armor", ESM::NPC::Armor); - setFlag("Clothing", ESM::NPC::Clothing); - setFlag("Books", ESM::NPC::Books); - setFlag("Ingredients", ESM::NPC::Ingredients); - setFlag("Picks", ESM::NPC::Picks); - setFlag("Probes", ESM::NPC::Probes); - setFlag("Lights", ESM::NPC::Lights); - setFlag("Apparatus", ESM::NPC::Apparatus); - setFlag("RepairItem", ESM::NPC::RepairItem); - setFlag("Misc", ESM::NPC::Misc); - setFlag("Potions", ESM::NPC::Potions); - setFlag("MagicItems", ESM::NPC::MagicItems); - - npc.mAiData.mServices = flags; - } - - // Travel destinations - if (rec["travelDestinations"] != sol::nil) - { - const sol::table travelDests = rec["travelDestinations"]; - npc.mTransport.clear(); - for (std::size_t i = 1; i <= travelDests.size(); ++i) + if (rec["isMale"] != sol::nil) { - sol::table t = travelDests[i]; - ESM::Transport destination; - - // Position - destination.mPos.pos = t["position"]; - destination.mPos.rot = Misc::Convert::toRotation(LuaUtil::fromTransform(t["rotation"])); - - // Cell - std::string cellId = t["cellId"]; - destination.mCellName = cellId; // If empty, it will be handled as exterior - - npc.mTransport.push_back(destination); + bool male = rec["isMale"]; + if (male) + npc.mFlags &= ~ESM::NPC::Female; + else + npc.mFlags |= ESM::NPC::Female; } - } - return npc; -} - // Blood type + if (rec["isEssential"] != sol::nil) + { + bool essential = rec["isEssential"]; + if (essential) + npc.mFlags |= ESM::NPC::Essential; + else + npc.mFlags &= ~ESM::NPC::Essential; + } + + if (rec["isRespawning"] != sol::nil) + { + bool respawn = rec["isRespawning"]; + if (respawn) + npc.mFlags |= ESM::NPC::Respawn; + else + npc.mFlags &= ~ESM::NPC::Respawn; + } + + if (rec["baseDisposition"] != sol::nil) + npc.mNpdt.mDisposition = static_cast(rec["baseDisposition"]); + + if (rec["baseGold"] != sol::nil) + npc.mNpdt.mGold = static_cast(rec["baseGold"]); + if (rec["bloodType"] != sol::nil) npc.mBloodType = static_cast(rec["bloodType"]); + // Services offered + if (rec["servicesOffered"] != sol::nil) + { + const sol::table services = rec["servicesOffered"]; + int flags = 0; + auto setFlag = [&](const char* key, int mask) { + if (services[key] != sol::nil && services[key]) + flags |= mask; + }; + + setFlag("Spells", ESM::NPC::Spells); + setFlag("Spellmaking", ESM::NPC::Spellmaking); + setFlag("Enchanting", ESM::NPC::Enchanting); + setFlag("Training", ESM::NPC::Training); + setFlag("Repair", ESM::NPC::Repair); + setFlag("Barter", ESM::NPC::AllItems); + setFlag("Weapon", ESM::NPC::Weapon); + setFlag("Armor", ESM::NPC::Armor); + setFlag("Clothing", ESM::NPC::Clothing); + setFlag("Books", ESM::NPC::Books); + setFlag("Ingredients", ESM::NPC::Ingredients); + setFlag("Picks", ESM::NPC::Picks); + setFlag("Probes", ESM::NPC::Probes); + setFlag("Lights", ESM::NPC::Lights); + setFlag("Apparatus", ESM::NPC::Apparatus); + setFlag("RepairItem", ESM::NPC::RepairItem); + setFlag("Misc", ESM::NPC::Misc); + setFlag("Potions", ESM::NPC::Potions); + setFlag("MagicItems", ESM::NPC::MagicItems); + + npc.mAiData.mServices = flags; + } + return npc; } } From 64a45f8aeb5c9bbae51b8ffd5ccd71deb76b6bb2 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Fri, 1 Aug 2025 16:30:16 -0700 Subject: [PATCH 03/25] Change world store insert --- apps/openmw/mwworld/store.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 80bcdb056a..c088a4ef1f 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -269,15 +269,13 @@ namespace MWWorld list.push_back((*it)->mId); } } - template - T* TypedDynamicStore::insert(const T& item, bool overrideOnly) + template + T* TypedDynamicStore::insert(const T& item, bool /*overrideOnly*/) { - if (overrideOnly) - { - auto it = mStatic.find(item.mId); - if (it == mStatic.end()) - return nullptr; - } + // Check if the ID already exists in static or dynamic stores + auto itStatic = mStatic.find(item.mId); + auto itDynamic = mDynamic.find(item.mId); + std::pair result = mDynamic.insert_or_assign(item.mId, item); T* ptr = &result.first->second; if (result.second) From 5209685783946fac41c7970a7a091cdba7fd50da Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Fri, 1 Aug 2025 16:40:30 -0700 Subject: [PATCH 04/25] revert some changes --- apps/openmw/mwworld/esmstore.cpp | 2 +- apps/openmw/mwworld/store.cpp | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 7262805f81..1f41ff3d2c 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -738,9 +738,9 @@ namespace MWWorld case ESM::REC_LEVI: case ESM::REC_LEVC: case ESM::REC_LIGH: + case ESM::REC_NPC_: mStoreImp->mRecNameToStore[type]->read(reader); return true; - case ESM::REC_NPC_: case ESM::REC_CREA: case ESM::REC_CONT: mStoreImp->mRecNameToStore[type]->read(reader, true); diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index c088a4ef1f..80bcdb056a 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -269,13 +269,15 @@ namespace MWWorld list.push_back((*it)->mId); } } - template - T* TypedDynamicStore::insert(const T& item, bool /*overrideOnly*/) + template + T* TypedDynamicStore::insert(const T& item, bool overrideOnly) { - // Check if the ID already exists in static or dynamic stores - auto itStatic = mStatic.find(item.mId); - auto itDynamic = mDynamic.find(item.mId); - + if (overrideOnly) + { + auto it = mStatic.find(item.mId); + if (it == mStatic.end()) + return nullptr; + } std::pair result = mDynamic.insert_or_assign(item.mId, item); T* ptr = &result.first->second; if (result.second) From 1fbbaefb477d4f5ea78c9c5e13333859904ff6e2 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Fri, 1 Aug 2025 16:45:24 -0700 Subject: [PATCH 05/25] Remove unrelated change, doc --- apps/openmw/mwlua/worldbindings.cpp | 5 ----- files/lua_api/openmw/types.lua | 7 +++++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index de8eec3710..495761e46f 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -156,11 +156,6 @@ namespace MWLua MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, cell, count.value_or(1)); return GObject(newPtr); }; - api["advanceTime"] = [context](double hours, bool incremental) { - context.mLuaManager->addAction([ hours, incremental] { - MWBase::Environment::get().getWorld()->advanceTime(hours, incremental); - }); - }; api["getObjectByFormId"] = [](std::string_view formIdStr) -> GObject { ESM::RefId refId = ESM::RefId::deserializeText(formIdStr); if (!refId.is()) diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index b2711c895b..5c21de8f56 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -875,6 +875,13 @@ -- @field #Actor baseType @{#Actor} -- @field [parent=#NPC] #NpcStats stats +--- +-- Creates a @{#NpcRecord} without adding it to the world database. +-- Use @{openmw_world#(world).createRecord} to add the record to the world. +-- @function [parent=#NPC] createRecordDraft +-- @param #NpcRecord book A Lua table with the fields of a NpcRecord, with an optional field `template` that accepts a @{#NpcRecord} as a base. +-- @return #NpcRecord A strongly typed NPC record. + --- -- A read-only list of all @{#NpcRecord}s in the world database, may be indexed by recordId. -- Implements [iterables#List](iterables.html#List) of #NpcRecord. From f2cfaac4b882d161e5832dd07d10c0ce33b502eb Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Fri, 1 Aug 2025 16:46:25 -0700 Subject: [PATCH 06/25] Fix clang --- apps/openmw/mwlua/worldbindings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index 495761e46f..dca136cd63 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -7,9 +7,9 @@ #include #include #include +#include #include #include -#include #include #include From c139680ce3e79d760e474da220f7fb37510d4847 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Fri, 1 Aug 2025 16:49:19 -0700 Subject: [PATCH 07/25] Add line --- apps/openmw/mwlua/types/npc.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 705e0831fd..45d600becd 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -255,6 +255,7 @@ namespace MWLua auto& stats = cls.getNpcStats(o.ptr()); stats.setBaseDisposition(stats.getBaseDisposition() + value); }; + npc["createRecordDraft"] = tableToNPC; npc["getFactionRank"] = [](const Object& actor, std::string_view faction) -> size_t { const MWWorld::Ptr ptr = actor.ptr(); From 1986891e7988f2d723281b3b31f27bc19e71d066 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Fri, 1 Aug 2025 16:51:10 -0700 Subject: [PATCH 08/25] different empty line? --- apps/openmw/mwlua/types/npc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 45d600becd..9d5277eb72 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -255,7 +255,7 @@ namespace MWLua auto& stats = cls.getNpcStats(o.ptr()); stats.setBaseDisposition(stats.getBaseDisposition() + value); }; - + npc["createRecordDraft"] = tableToNPC; npc["getFactionRank"] = [](const Object& actor, std::string_view faction) -> size_t { const MWWorld::Ptr ptr = actor.ptr(); From 5203f300e2836c59cf95e05fc0c216fd99eca8eb Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Sat, 2 Aug 2025 05:33:27 -0700 Subject: [PATCH 09/25] Requested changes --- apps/openmw/mwlua/types/npc.cpp | 4 ++-- apps/openmw/mwworld/esmstore.cpp | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 9d5277eb72..3aa84257bd 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -88,7 +88,7 @@ namespace } if (rec["baseDisposition"] != sol::nil) - npc.mNpdt.mDisposition = static_cast(rec["baseDisposition"]); + npc.mNpdt.mDisposition = rec["baseDisposition"].get(); if (rec["baseGold"] != sol::nil) npc.mNpdt.mGold = static_cast(rec["baseGold"]); @@ -101,7 +101,7 @@ namespace { const sol::table services = rec["servicesOffered"]; int flags = 0; - auto setFlag = [&](const char* key, int mask) { + auto setFlag = [&](const std::string_view& key, int mask) { if (services[key] != sol::nil && services[key]) flags |= mask; }; diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 1f41ff3d2c..369731a019 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -726,8 +726,6 @@ namespace MWWorld switch (type) { case ESM::REC_ALCH: - case ESM::REC_MISC: - case ESM::REC_ACTI: case ESM::REC_ARMO: case ESM::REC_BOOK: case ESM::REC_CLAS: @@ -735,14 +733,16 @@ namespace MWWorld case ESM::REC_ENCH: case ESM::REC_SPEL: case ESM::REC_WEAP: + mStoreImp->mRecNameToStore[type]->read(reader); + return true; + case ESM::REC_NPC_: + case ESM::REC_CREA: + case ESM::REC_CONT: + case ESM::REC_MISC: + case ESM::REC_ACTI: case ESM::REC_LEVI: case ESM::REC_LEVC: case ESM::REC_LIGH: - case ESM::REC_NPC_: - mStoreImp->mRecNameToStore[type]->read(reader); - return true; - case ESM::REC_CREA: - case ESM::REC_CONT: mStoreImp->mRecNameToStore[type]->read(reader, true); return true; From 7c665643ad92e53baebb309729a1dd205185344f Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Sat, 2 Aug 2025 06:08:29 -0700 Subject: [PATCH 10/25] Use overload --- apps/openmw/mwlua/types/npc.cpp | 4 ++-- apps/openmw/mwworld/store.cpp | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 3aa84257bd..1905ac7c03 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -91,10 +91,10 @@ namespace npc.mNpdt.mDisposition = rec["baseDisposition"].get(); if (rec["baseGold"] != sol::nil) - npc.mNpdt.mGold = static_cast(rec["baseGold"]); + npc.mNpdt.mGold = rec["baseGold"].get(); if (rec["bloodType"] != sol::nil) - npc.mBloodType = static_cast(rec["bloodType"]); + npc.mBloodType = rec["bloodType"].get(); // Services offered if (rec["servicesOffered"] != sol::nil) diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 80bcdb056a..0e64e6e627 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -269,13 +269,29 @@ namespace MWWorld list.push_back((*it)->mId); } } + template + inline bool shouldInsert(const IdType& id, const StaticMap& map) + { + auto it = map.find(id); + return it != map.end(); + } + + template + inline bool shouldInsert(const ESM::RefId& id, const StaticMap& map) + { + if (!id.template is()) + { + auto it = map.find(id); + return it != map.end(); + } + return true; + } template T* TypedDynamicStore::insert(const T& item, bool overrideOnly) { if (overrideOnly) { - auto it = mStatic.find(item.mId); - if (it == mStatic.end()) + if (!shouldInsert(item.mId, mStatic)) return nullptr; } std::pair result = mDynamic.insert_or_assign(item.mId, item); From 31abd366ffde3e3cad2ef0d9807e86f87d5f1d24 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Sat, 2 Aug 2025 06:25:14 -0700 Subject: [PATCH 11/25] no const --- apps/openmw/mwlua/types/npc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 1905ac7c03..4cd942d831 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -101,7 +101,7 @@ namespace { const sol::table services = rec["servicesOffered"]; int flags = 0; - auto setFlag = [&](const std::string_view& key, int mask) { + auto setFlag = [&](std::string_view key, int mask) { if (services[key] != sol::nil && services[key]) flags |= mask; }; From d40a78e8ac39fcd62efe63a32047d225b41295d3 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Sat, 2 Aug 2025 06:32:46 -0700 Subject: [PATCH 12/25] Player shennanigans --- apps/openmw/mwlua/types/npc.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 4cd942d831..220b845882 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -41,8 +41,8 @@ namespace else npc.blank(); - // Force dummy ID - npc.mId = ESM::RefId::deserializeText("blank"); + if (npc.mId == ESM::RefId::deserializeText("Player")) + npc.mId = ESM::RefId::deserializeText("blank"); // Basic fields if (rec["name"] != sol::nil) From d7a411cc7214eda1ace3f2be9445e7e844709421 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Sun, 3 Aug 2025 11:44:45 -0700 Subject: [PATCH 13/25] Suggested fixes --- apps/openmw/mwlua/types/npc.cpp | 3 +-- apps/openmw/mwlua/worldbindings.cpp | 6 +++++- apps/openmw/mwworld/store.cpp | 15 ++++----------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 220b845882..2f6b0fa0ba 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -41,8 +41,7 @@ namespace else npc.blank(); - if (npc.mId == ESM::RefId::deserializeText("Player")) - npc.mId = ESM::RefId::deserializeText("blank"); + npc.mId = {}; // Basic fields if (rec["name"] != sol::nil) diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index dca136cd63..228e70b464 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -191,7 +191,11 @@ namespace MWLua }, [lua = context.mLua](const ESM::NPC& npc) -> const ESM::NPC* { checkGameInitialized(lua); - return MWBase::Environment::get().getESMStore()->insert(npc); + if (npc.mId.empty()) + return MWBase::Environment::get().getESMStore()->insert(npc); + ESM::NPC copy = npc; + copy.mId = {}; + return MWBase::Environment::get().getESMStore()->insert(copy); }, [lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* { checkGameInitialized(lua); diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 0e64e6e627..870c2a60c9 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -276,22 +276,15 @@ namespace MWWorld return it != map.end(); } - template - inline bool shouldInsert(const ESM::RefId& id, const StaticMap& map) - { - if (!id.template is()) - { - auto it = map.find(id); - return it != map.end(); - } - return true; - } template T* TypedDynamicStore::insert(const T& item, bool overrideOnly) { + if constexpr (std::is_same_v) + overrideOnly = overrideOnly && !item.mId.template is(); if (overrideOnly) { - if (!shouldInsert(item.mId, mStatic)) + auto it = mStatic.find(item.mId); + if (it == mStatic.end()) return nullptr; } std::pair result = mDynamic.insert_or_assign(item.mId, item); From a0585949a984105d592d83d59e3c536b0fbbd253 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Sun, 3 Aug 2025 12:50:57 -0700 Subject: [PATCH 14/25] Remove dead code, fix grammer --- apps/openmw/mwworld/store.cpp | 6 ------ files/lua_api/openmw/types.lua | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 870c2a60c9..12c45e947d 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -269,12 +269,6 @@ namespace MWWorld list.push_back((*it)->mId); } } - template - inline bool shouldInsert(const IdType& id, const StaticMap& map) - { - auto it = map.find(id); - return it != map.end(); - } template T* TypedDynamicStore::insert(const T& item, bool overrideOnly) diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index e85da3f0f6..6e1bee5ab4 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -876,10 +876,10 @@ -- @field [parent=#NPC] #NpcStats stats --- --- Creates a @{#NpcRecord} without adding it to the world database. +-- Creates an @{#NpcRecord} without adding it to the world database. -- Use @{openmw_world#(world).createRecord} to add the record to the world. -- @function [parent=#NPC] createRecordDraft --- @param #NpcRecord book A Lua table with the fields of a NpcRecord, with an optional field `template` that accepts a @{#NpcRecord} as a base. +-- @param #NpcRecord npc A Lua table with the fields of an NpcRecord, with an optional field `template` that accepts an @{#NpcRecord} as a base. -- @return #NpcRecord A strongly typed NPC record. --- From 99ae52e94ca1443bee0bb16d918de164cd5e620f Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Sun, 3 Aug 2025 13:42:04 -0700 Subject: [PATCH 15/25] Add fields to record --- apps/openmw/mwlua/types/actor.hpp | 13 ++--- apps/openmw/mwlua/types/npc.cpp | 56 ++++++++++++--------- apps/openmw/mwlua/types/servicesoffered.hpp | 26 ++++++++++ files/lua_api/openmw/types.lua | 3 ++ 4 files changed, 64 insertions(+), 34 deletions(-) create mode 100644 apps/openmw/mwlua/types/servicesoffered.hpp diff --git a/apps/openmw/mwlua/types/actor.hpp b/apps/openmw/mwlua/types/actor.hpp index 425e44451b..f55089c5a4 100644 --- a/apps/openmw/mwlua/types/actor.hpp +++ b/apps/openmw/mwlua/types/actor.hpp @@ -16,6 +16,7 @@ #include "../context.hpp" +#include "servicesoffered.hpp" namespace MWLua { @@ -25,15 +26,6 @@ namespace MWLua record["servicesOffered"] = sol::readonly_property([context](const T& rec) -> sol::table { sol::state_view lua = context.sol(); sol::table providedServices(lua, sol::create); - constexpr std::array, 19> serviceNames = { { { ESM::NPC::Spells, - "Spells" }, - { ESM::NPC::Spellmaking, "Spellmaking" }, { ESM::NPC::Enchanting, "Enchanting" }, - { ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" }, { ESM::NPC::AllItems, "Barter" }, - { ESM::NPC::Weapon, "Weapon" }, { ESM::NPC::Armor, "Armor" }, { ESM::NPC::Clothing, "Clothing" }, - { ESM::NPC::Books, "Books" }, { ESM::NPC::Ingredients, "Ingredients" }, { ESM::NPC::Picks, "Picks" }, - { ESM::NPC::Probes, "Probes" }, { ESM::NPC::Lights, "Lights" }, { ESM::NPC::Apparatus, "Apparatus" }, - { ESM::NPC::RepairItem, "RepairItem" }, { ESM::NPC::Misc, "Misc" }, { ESM::NPC::Potions, "Potions" }, - { ESM::NPC::MagicItems, "MagicItems" } } }; int services = rec.mAiData.mServices; if constexpr (std::is_same_v) @@ -42,10 +34,11 @@ namespace MWLua services = MWBase::Environment::get().getESMStore()->get().find(rec.mClass)->mData.mServices; } - for (const auto& [flag, name] : serviceNames) + for (const auto& [flag, name] : ServiceNames) { providedServices[name] = (services & flag) != 0; } + providedServices["Travel"] = !rec.getTransport().empty(); return LuaUtil::makeReadOnly(providedServices); }); diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 2f6b0fa0ba..c0ffce9402 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -2,6 +2,7 @@ #include "actor.hpp" #include "modelproperty.hpp" +#include "servicesoffered.hpp" #include #include @@ -58,6 +59,8 @@ namespace npc.mHead = ESM::RefId::deserializeText(rec["head"].get()); if (rec["hair"] != sol::nil) npc.mHair = ESM::RefId::deserializeText(rec["hair"].get()); + if (rec["primaryFaction"] != sol::nil) + npc.mFaction = ESM::RefId::deserializeText(rec["primaryFaction"].get()); if (rec["isMale"] != sol::nil) { @@ -77,6 +80,15 @@ namespace npc.mFlags &= ~ESM::NPC::Essential; } + if (rec["autocalc"] != sol::nil) + { + bool respawn = rec["autocalc"]; + if (respawn) + npc.mFlags |= ESM::NPC::Autocalc; + else + npc.mFlags &= ~ESM::NPC::Autocalc; + } + if (rec["isRespawning"] != sol::nil) { bool respawn = rec["isRespawning"]; @@ -95,35 +107,20 @@ namespace if (rec["bloodType"] != sol::nil) npc.mBloodType = rec["bloodType"].get(); - // Services offered + if (rec["primaryFactionRank"] != sol::nil) + npc.mNpdt.mRank = rec["primaryFactionRank"].get(); + if (rec["servicesOffered"] != sol::nil) { const sol::table services = rec["servicesOffered"]; int flags = 0; - auto setFlag = [&](std::string_view key, int mask) { - if (services[key] != sol::nil && services[key]) - flags |= mask; - }; - setFlag("Spells", ESM::NPC::Spells); - setFlag("Spellmaking", ESM::NPC::Spellmaking); - setFlag("Enchanting", ESM::NPC::Enchanting); - setFlag("Training", ESM::NPC::Training); - setFlag("Repair", ESM::NPC::Repair); - setFlag("Barter", ESM::NPC::AllItems); - setFlag("Weapon", ESM::NPC::Weapon); - setFlag("Armor", ESM::NPC::Armor); - setFlag("Clothing", ESM::NPC::Clothing); - setFlag("Books", ESM::NPC::Books); - setFlag("Ingredients", ESM::NPC::Ingredients); - setFlag("Picks", ESM::NPC::Picks); - setFlag("Probes", ESM::NPC::Probes); - setFlag("Lights", ESM::NPC::Lights); - setFlag("Apparatus", ESM::NPC::Apparatus); - setFlag("RepairItem", ESM::NPC::RepairItem); - setFlag("Misc", ESM::NPC::Misc); - setFlag("Potions", ESM::NPC::Potions); - setFlag("MagicItems", ESM::NPC::MagicItems); + for (const auto& [mask, key] : ServiceNames) + { + sol::object value = services[key]; + if (value != sol::nil && value.as()) + flags |= mask; + } npc.mAiData.mServices = flags; } @@ -198,9 +195,20 @@ namespace MWLua = sol::readonly_property([](const ESM::NPC& rec) -> int { return (int)rec.mNpdt.mDisposition; }); record["head"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHead.serializeText(); }); + record["primaryFaction"] = sol::readonly_property( + [](const ESM::NPC& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mFaction); }); + record["primaryFactionRank"] + = sol::readonly_property([](const ESM::NPC& rec, sol::this_state s) -> sol::object { + sol::state_view lua(s); + if (rec.mFaction.empty()) + return sol::make_object(lua, sol::nil); // return nil + return sol::make_object(lua, rec.mNpdt.mRank); // return the rank as a number + }); addModelProperty(record); record["isEssential"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Essential; }); + record["autocalc"] + = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Autocalc; }); record["isMale"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.isMale(); }); record["isRespawning"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; }); diff --git a/apps/openmw/mwlua/types/servicesoffered.hpp b/apps/openmw/mwlua/types/servicesoffered.hpp new file mode 100644 index 0000000000..c854baca4d --- /dev/null +++ b/apps/openmw/mwlua/types/servicesoffered.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include // for ESM::NPC constants + +inline constexpr std::array, 19> ServiceNames = {{ + { ESM::NPC::Spells, "Spells" }, + { ESM::NPC::Spellmaking, "Spellmaking" }, + { ESM::NPC::Enchanting, "Enchanting" }, + { ESM::NPC::Training, "Training" }, + { ESM::NPC::Repair, "Repair" }, + { ESM::NPC::AllItems, "Barter" }, + { ESM::NPC::Weapon, "Weapon" }, + { ESM::NPC::Armor, "Armor" }, + { ESM::NPC::Clothing, "Clothing" }, + { ESM::NPC::Books, "Books" }, + { ESM::NPC::Ingredients, "Ingredients" }, + { ESM::NPC::Picks, "Picks" }, + { ESM::NPC::Probes, "Probes" }, + { ESM::NPC::Lights, "Lights" }, + { ESM::NPC::Apparatus, "Apparatus" }, + { ESM::NPC::RepairItem, "RepairItem" }, + { ESM::NPC::Misc, "Misc" }, + { ESM::NPC::Potions, "Potions" }, + { ESM::NPC::MagicItems, "MagicItems" } +}}; \ No newline at end of file diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 6e1bee5ab4..5f78467bdc 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -861,6 +861,9 @@ -- @field #boolean canWalk whether the creature can walk -- @field #boolean canUseWeapons whether the creature can use weapons and shields -- @field #boolean isBiped whether the creature is a biped +-- @field #boolean autocalc If true, the actors stats will be automatically calculated based on level and class. +-- @field #string primaryFaction Faction ID of the NPCs default faction. Nil if no faction +-- @field #number primaryFactionRank Faction rank of the NPCs default faction. Nil if no faction -- @field #boolean isEssential whether the creature is essential -- @field #boolean isRespawning whether the creature respawns after death -- @field #number bloodType integer representing the blood type of the Creature. Used to generate the correct blood vfx. From afb7f1da54feab991f95c97656a0b27b9b0ddb62 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Sun, 3 Aug 2025 21:39:44 -0700 Subject: [PATCH 16/25] Fix formatting --- apps/openmw/mwlua/types/servicesoffered.hpp | 31 ++++++--------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/apps/openmw/mwlua/types/servicesoffered.hpp b/apps/openmw/mwlua/types/servicesoffered.hpp index c854baca4d..acf928dc7c 100644 --- a/apps/openmw/mwlua/types/servicesoffered.hpp +++ b/apps/openmw/mwlua/types/servicesoffered.hpp @@ -1,26 +1,13 @@ #pragma once #include +#include // for ESM::NPC constants #include -#include // for ESM::NPC constants -inline constexpr std::array, 19> ServiceNames = {{ - { ESM::NPC::Spells, "Spells" }, - { ESM::NPC::Spellmaking, "Spellmaking" }, - { ESM::NPC::Enchanting, "Enchanting" }, - { ESM::NPC::Training, "Training" }, - { ESM::NPC::Repair, "Repair" }, - { ESM::NPC::AllItems, "Barter" }, - { ESM::NPC::Weapon, "Weapon" }, - { ESM::NPC::Armor, "Armor" }, - { ESM::NPC::Clothing, "Clothing" }, - { ESM::NPC::Books, "Books" }, - { ESM::NPC::Ingredients, "Ingredients" }, - { ESM::NPC::Picks, "Picks" }, - { ESM::NPC::Probes, "Probes" }, - { ESM::NPC::Lights, "Lights" }, - { ESM::NPC::Apparatus, "Apparatus" }, - { ESM::NPC::RepairItem, "RepairItem" }, - { ESM::NPC::Misc, "Misc" }, - { ESM::NPC::Potions, "Potions" }, - { ESM::NPC::MagicItems, "MagicItems" } -}}; \ No newline at end of file +inline constexpr std::array, 19> ServiceNames + = { { { ESM::NPC::Spells, "Spells" }, { ESM::NPC::Spellmaking, "Spellmaking" }, + { ESM::NPC::Enchanting, "Enchanting" }, { ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" }, + { ESM::NPC::AllItems, "Barter" }, { ESM::NPC::Weapon, "Weapon" }, { ESM::NPC::Armor, "Armor" }, + { ESM::NPC::Clothing, "Clothing" }, { ESM::NPC::Books, "Books" }, { ESM::NPC::Ingredients, "Ingredients" }, + { ESM::NPC::Picks, "Picks" }, { ESM::NPC::Probes, "Probes" }, { ESM::NPC::Lights, "Lights" }, + { ESM::NPC::Apparatus, "Apparatus" }, { ESM::NPC::RepairItem, "RepairItem" }, { ESM::NPC::Misc, "Misc" }, + { ESM::NPC::Potions, "Potions" }, { ESM::NPC::MagicItems, "MagicItems" } } }; From b2746a7b4ba248b27173b2291025e060ea3b4887 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Mon, 4 Aug 2025 08:27:26 -0700 Subject: [PATCH 17/25] Fix autocalc, remove thing --- apps/openmw/mwlua/types/npc.cpp | 8 ++++---- apps/openmw/mwlua/types/servicesoffered.hpp | 3 +-- files/lua_api/openmw/types.lua | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index c0ffce9402..49acfb111b 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -80,10 +80,10 @@ namespace npc.mFlags &= ~ESM::NPC::Essential; } - if (rec["autocalc"] != sol::nil) + if (rec["isAutocalc"] != sol::nil) { - bool respawn = rec["autocalc"]; - if (respawn) + bool autoCalc = rec["isAutocalc"]; + if (autoCalc) npc.mFlags |= ESM::NPC::Autocalc; else npc.mFlags &= ~ESM::NPC::Autocalc; @@ -207,7 +207,7 @@ namespace MWLua addModelProperty(record); record["isEssential"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Essential; }); - record["autocalc"] + record["isAutocalc"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Autocalc; }); record["isMale"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.isMale(); }); record["isRespawning"] diff --git a/apps/openmw/mwlua/types/servicesoffered.hpp b/apps/openmw/mwlua/types/servicesoffered.hpp index acf928dc7c..bfc4f96fc9 100644 --- a/apps/openmw/mwlua/types/servicesoffered.hpp +++ b/apps/openmw/mwlua/types/servicesoffered.hpp @@ -1,6 +1,5 @@ -#pragma once #include -#include // for ESM::NPC constants +#include #include inline constexpr std::array, 19> ServiceNames diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 5f78467bdc..aa5b70c4e0 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -861,7 +861,7 @@ -- @field #boolean canWalk whether the creature can walk -- @field #boolean canUseWeapons whether the creature can use weapons and shields -- @field #boolean isBiped whether the creature is a biped --- @field #boolean autocalc If true, the actors stats will be automatically calculated based on level and class. +-- @field #boolean isAutocalc If true, the actors stats will be automatically calculated based on level and class. -- @field #string primaryFaction Faction ID of the NPCs default faction. Nil if no faction -- @field #number primaryFactionRank Faction rank of the NPCs default faction. Nil if no faction -- @field #boolean isEssential whether the creature is essential From 2ab87f2d2241fb98fffc1a07be4edd84026bbe9c Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Mon, 4 Aug 2025 08:35:13 -0700 Subject: [PATCH 18/25] Use namespace for servicesoffered --- apps/openmw/mwlua/types/actor.hpp | 2 +- apps/openmw/mwlua/types/npc.cpp | 2 +- apps/openmw/mwlua/types/servicesoffered.hpp | 26 ++++++++++++++------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwlua/types/actor.hpp b/apps/openmw/mwlua/types/actor.hpp index f55089c5a4..bd098d99d5 100644 --- a/apps/openmw/mwlua/types/actor.hpp +++ b/apps/openmw/mwlua/types/actor.hpp @@ -34,7 +34,7 @@ namespace MWLua services = MWBase::Environment::get().getESMStore()->get().find(rec.mClass)->mData.mServices; } - for (const auto& [flag, name] : ServiceNames) + for (const auto& [flag, name] : MWLua::ServiceNames) { providedServices[name] = (services & flag) != 0; } diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 49acfb111b..489b01e140 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -115,7 +115,7 @@ namespace const sol::table services = rec["servicesOffered"]; int flags = 0; - for (const auto& [mask, key] : ServiceNames) + for (const auto& [mask, key] : MWLua::ServiceNames) { sol::object value = services[key]; if (value != sol::nil && value.as()) diff --git a/apps/openmw/mwlua/types/servicesoffered.hpp b/apps/openmw/mwlua/types/servicesoffered.hpp index bfc4f96fc9..4a87975a7d 100644 --- a/apps/openmw/mwlua/types/servicesoffered.hpp +++ b/apps/openmw/mwlua/types/servicesoffered.hpp @@ -1,12 +1,22 @@ +#ifndef MWLUA_SERVICESOFFERED_HPP +#define MWLUA_SERVICESOFFERED_HPP + #include #include #include -inline constexpr std::array, 19> ServiceNames - = { { { ESM::NPC::Spells, "Spells" }, { ESM::NPC::Spellmaking, "Spellmaking" }, - { ESM::NPC::Enchanting, "Enchanting" }, { ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" }, - { ESM::NPC::AllItems, "Barter" }, { ESM::NPC::Weapon, "Weapon" }, { ESM::NPC::Armor, "Armor" }, - { ESM::NPC::Clothing, "Clothing" }, { ESM::NPC::Books, "Books" }, { ESM::NPC::Ingredients, "Ingredients" }, - { ESM::NPC::Picks, "Picks" }, { ESM::NPC::Probes, "Probes" }, { ESM::NPC::Lights, "Lights" }, - { ESM::NPC::Apparatus, "Apparatus" }, { ESM::NPC::RepairItem, "RepairItem" }, { ESM::NPC::Misc, "Misc" }, - { ESM::NPC::Potions, "Potions" }, { ESM::NPC::MagicItems, "MagicItems" } } }; +namespace MWLua +{ + + inline constexpr std::array, 19> ServiceNames + = { { { ESM::NPC::Spells, "Spells" }, { ESM::NPC::Spellmaking, "Spellmaking" }, + { ESM::NPC::Enchanting, "Enchanting" }, { ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" }, + { ESM::NPC::AllItems, "Barter" }, { ESM::NPC::Weapon, "Weapon" }, { ESM::NPC::Armor, "Armor" }, + { ESM::NPC::Clothing, "Clothing" }, { ESM::NPC::Books, "Books" }, { ESM::NPC::Ingredients, "Ingredients" }, + { ESM::NPC::Picks, "Picks" }, { ESM::NPC::Probes, "Probes" }, { ESM::NPC::Lights, "Lights" }, + { ESM::NPC::Apparatus, "Apparatus" }, { ESM::NPC::RepairItem, "RepairItem" }, { ESM::NPC::Misc, "Misc" }, + { ESM::NPC::Potions, "Potions" }, { ESM::NPC::MagicItems, "MagicItems" } } }; +} + + +#endif \ No newline at end of file From 4c5118a24bef8e388ddcb23d2303a4b7d91b7087 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Mon, 4 Aug 2025 08:43:37 -0700 Subject: [PATCH 19/25] Fix rank --- apps/openmw/mwlua/types/npc.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 489b01e140..71e2625063 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -108,7 +108,10 @@ namespace npc.mBloodType = rec["bloodType"].get(); if (rec["primaryFactionRank"] != sol::nil) - npc.mNpdt.mRank = rec["primaryFactionRank"].get(); + { + if (!npc.mFaction.empty()) + npc.mNpdt.mRank = LuaUtil::fromLuaIndex(rec["primaryFactionRank"]); + } if (rec["servicesOffered"] != sol::nil) { @@ -197,13 +200,12 @@ namespace MWLua = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHead.serializeText(); }); record["primaryFaction"] = sol::readonly_property( [](const ESM::NPC& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mFaction); }); - record["primaryFactionRank"] - = sol::readonly_property([](const ESM::NPC& rec, sol::this_state s) -> sol::object { - sol::state_view lua(s); - if (rec.mFaction.empty()) - return sol::make_object(lua, sol::nil); // return nil - return sol::make_object(lua, rec.mNpdt.mRank); // return the rank as a number - }); + record["primaryFactionRank"] = sol::readonly_property([](const ESM::NPC& rec, sol::this_state s) -> int { + sol::state_view lua(s); + if (rec.mFaction.empty()) + return 0; + return LuaUtil::toLuaIndex(rec.mNpdt.mRank); + }); addModelProperty(record); record["isEssential"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Essential; }); From aa545467fd5382005f54c3c78ec4259a90e4539c Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Mon, 4 Aug 2025 09:56:58 -0700 Subject: [PATCH 20/25] Remove unused --- apps/openmw/mwlua/types/npc.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 71e2625063..6d26b2409c 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -201,7 +201,6 @@ namespace MWLua record["primaryFaction"] = sol::readonly_property( [](const ESM::NPC& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mFaction); }); record["primaryFactionRank"] = sol::readonly_property([](const ESM::NPC& rec, sol::this_state s) -> int { - sol::state_view lua(s); if (rec.mFaction.empty()) return 0; return LuaUtil::toLuaIndex(rec.mNpdt.mRank); From 7a5fe778bcd2729bf608a38faf4d882ee1aff307 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Mon, 4 Aug 2025 10:04:34 -0700 Subject: [PATCH 21/25] Add validation --- apps/openmw/mwlua/types/npc.cpp | 53 +++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 6d26b2409c..92e147a4d2 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -32,6 +32,19 @@ namespace sol namespace { + size_t getValidRanksCount(const ESM::Faction* faction) + { + if (!faction) + return 0; + + for (size_t i = 0; i < faction->mRanks.size(); i++) + { + if (faction->mRanks[i].empty()) + return i; + } + + return faction->mRanks.size(); + } ESM::NPC tableToNPC(const sol::table& rec) { ESM::NPC npc; @@ -60,8 +73,16 @@ namespace if (rec["hair"] != sol::nil) npc.mHair = ESM::RefId::deserializeText(rec["hair"].get()); if (rec["primaryFaction"] != sol::nil) - npc.mFaction = ESM::RefId::deserializeText(rec["primaryFaction"].get()); + { + auto factionStr = rec["primaryFaction"].get(); + ESM::RefId factionId = ESM::RefId::deserializeText(factionStr); + const auto& factionStore = MWBase::Environment::get().getESMStore()->get(); + if (!factionStore.search(factionId)) + throw std::runtime_error("Invalid faction '" + std::string(factionStr) + "' in primaryFaction"); + + npc.mFaction = factionId; + } if (rec["isMale"] != sol::nil) { bool male = rec["isMale"]; @@ -110,7 +131,22 @@ namespace if (rec["primaryFactionRank"] != sol::nil) { if (!npc.mFaction.empty()) - npc.mNpdt.mRank = LuaUtil::fromLuaIndex(rec["primaryFactionRank"]); + { + const ESM::RefId factionId = npc.mFaction; + const ESM::Faction* faction + = MWBase::Environment::get().getESMStore()->get().find(factionId); + + int luaValue = rec["primaryFactionRank"]; + int rank = LuaUtil::fromLuaIndex(luaValue); + + int maxRank = static_cast(getValidRanksCount(faction)); + + if (rank < 0 || rank >= maxRank) + throw std::runtime_error("primaryFactionRank: Requested rank " + std::to_string(rank) + + " is out of bounds for faction " + factionId.toDebugString()); + + npc.mNpdt.mRank = rank; + } } if (rec["servicesOffered"] != sol::nil) @@ -134,19 +170,6 @@ namespace namespace { - size_t getValidRanksCount(const ESM::Faction* faction) - { - if (!faction) - return 0; - - for (size_t i = 0; i < faction->mRanks.size(); i++) - { - if (faction->mRanks[i].empty()) - return i; - } - - return faction->mRanks.size(); - } ESM::RefId parseFactionId(std::string_view faction) { From 1a886db951622bb1901ae56f7f26a5fea1827d68 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Mon, 4 Aug 2025 10:41:31 -0700 Subject: [PATCH 22/25] Merge namespace, remove redundant --- apps/openmw/mwlua/types/npc.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 92e147a4d2..f38f027d2a 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -74,14 +74,13 @@ namespace npc.mHair = ESM::RefId::deserializeText(rec["hair"].get()); if (rec["primaryFaction"] != sol::nil) { - auto factionStr = rec["primaryFaction"].get(); - ESM::RefId factionId = ESM::RefId::deserializeText(factionStr); - + const auto str = rec["primaryFaction"].get(); const auto& factionStore = MWBase::Environment::get().getESMStore()->get(); - if (!factionStore.search(factionId)) - throw std::runtime_error("Invalid faction '" + std::string(factionStr) + "' in primaryFaction"); - npc.mFaction = factionId; + if (!factionStore.search(ESM::RefId::deserializeText(str))) + throw std::runtime_error("Invalid faction '" + str + "' in primaryFaction"); + + npc.mFaction = ESM::RefId::deserializeText(str); } if (rec["isMale"] != sol::nil) { @@ -166,10 +165,6 @@ namespace return npc; } -} - -namespace -{ ESM::RefId parseFactionId(std::string_view faction) { From e74167c7b768a9e621eb332aa9c9d22cb1f149be Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Mon, 4 Aug 2025 11:09:26 -0700 Subject: [PATCH 23/25] Revert bad change, do actual fix --- apps/openmw/mwlua/types/npc.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index f38f027d2a..e847357bd9 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -74,13 +74,14 @@ namespace npc.mHair = ESM::RefId::deserializeText(rec["hair"].get()); if (rec["primaryFaction"] != sol::nil) { - const auto str = rec["primaryFaction"].get(); + auto factionStr = rec["primaryFaction"].get(); + ESM::RefId factionId = ESM::RefId::deserializeText(factionStr); + const auto& factionStore = MWBase::Environment::get().getESMStore()->get(); + if (!factionStore.search(factionId)) + throw std::runtime_error("Invalid faction '" + std::string(factionStr) + "' in primaryFaction"); - if (!factionStore.search(ESM::RefId::deserializeText(str))) - throw std::runtime_error("Invalid faction '" + str + "' in primaryFaction"); - - npc.mFaction = ESM::RefId::deserializeText(str); + npc.mFaction = factionId; } if (rec["isMale"] != sol::nil) { @@ -131,9 +132,8 @@ namespace { if (!npc.mFaction.empty()) { - const ESM::RefId factionId = npc.mFaction; const ESM::Faction* faction - = MWBase::Environment::get().getESMStore()->get().find(factionId); + = MWBase::Environment::get().getESMStore()->get().find(npc.mFaction); int luaValue = rec["primaryFactionRank"]; int rank = LuaUtil::fromLuaIndex(luaValue); @@ -142,7 +142,7 @@ namespace if (rank < 0 || rank >= maxRank) throw std::runtime_error("primaryFactionRank: Requested rank " + std::to_string(rank) - + " is out of bounds for faction " + factionId.toDebugString()); + + " is out of bounds for faction " + npc.mFaction.toDebugString()); npc.mNpdt.mRank = rank; } From 493f70a6614584dfd1d555a7e21fe9e7d8626719 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Mon, 4 Aug 2025 11:10:06 -0700 Subject: [PATCH 24/25] Formatting --- apps/openmw/mwlua/types/servicesoffered.hpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwlua/types/servicesoffered.hpp b/apps/openmw/mwlua/types/servicesoffered.hpp index 4a87975a7d..94695c44fb 100644 --- a/apps/openmw/mwlua/types/servicesoffered.hpp +++ b/apps/openmw/mwlua/types/servicesoffered.hpp @@ -7,7 +7,7 @@ namespace MWLua { - + inline constexpr std::array, 19> ServiceNames = { { { ESM::NPC::Spells, "Spells" }, { ESM::NPC::Spellmaking, "Spellmaking" }, { ESM::NPC::Enchanting, "Enchanting" }, { ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" }, @@ -18,5 +18,4 @@ namespace MWLua { ESM::NPC::Potions, "Potions" }, { ESM::NPC::MagicItems, "MagicItems" } } }; } - -#endif \ No newline at end of file +#endif From ef2332eb373a71e819662649bcdf43107f9f5860 Mon Sep 17 00:00:00 2001 From: SkyHasACat Date: Mon, 4 Aug 2025 11:30:47 -0700 Subject: [PATCH 25/25] Update API, feature --- CHANGELOG.md | 1 + CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2df2e025e..e3b281b909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ Feature #8580: Sort characters in the save loading menu Feature #8597: Lua: Add more built-in event handlers Feature #8629: Expose path grid data to Lua + Feature #8654: Allow lua world.createRecord to create NPC records 0.49.0 ------ diff --git a/CMakeLists.txt b/CMakeLists.txt index 57ebeefcfd..5991c57c4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 50) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 87) +set(OPENMW_LUA_API_REVISION 88) set(OPENMW_POSTPROCESSING_API_REVISION 3) set(OPENMW_VERSION_COMMITHASH "")