1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-10-17 22:16:38 +00:00
This commit is contained in:
Andrew Lanzone 2025-08-05 17:32:50 -07:00
commit e4cc3adc98
9 changed files with 186 additions and 15 deletions

View file

@ -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
------

View file

@ -645,6 +645,9 @@ namespace MWLua
}
inventoryT["isResolved"] = [](const InventoryT& inventory) -> bool {
const MWWorld::Ptr& ptr = inventory.mObj.ptr();
// Avoid initializing custom data
if (!ptr.getRefData().getCustomData())
return false;
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
return store.isResolved();
};

View file

@ -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<std::pair<int, std::string_view>, 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<T, ESM::NPC>)
@ -42,10 +34,11 @@ namespace MWLua
services
= MWBase::Environment::get().getESMStore()->get<ESM::Class>().find(rec.mClass)->mData.mServices;
}
for (const auto& [flag, name] : serviceNames)
for (const auto& [flag, name] : MWLua::ServiceNames)
{
providedServices[name] = (services & flag) != 0;
}
providedServices["Travel"] = !rec.getTransport().empty();
return LuaUtil::makeReadOnly(providedServices);
});

View file

@ -2,6 +2,7 @@
#include "actor.hpp"
#include "modelproperty.hpp"
#include "servicesoffered.hpp"
#include <components/esm3/loadfact.hpp>
#include <components/esm3/loadnpc.hpp>
@ -44,6 +45,126 @@ namespace
return faction->mRanks.size();
}
ESM::NPC tableToNPC(const sol::table& rec)
{
ESM::NPC npc;
// Start from template if provided
if (rec["template"] != sol::nil)
npc = LuaUtil::cast<ESM::NPC>(rec["template"]);
else
npc.blank();
npc.mId = {};
// Basic fields
if (rec["name"] != sol::nil)
npc.mName = rec["name"];
if (rec["model"] != sol::nil)
npc.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get<std::string_view>());
if (rec["mwscript"] != sol::nil)
npc.mScript = ESM::RefId::deserializeText(rec["mwscript"].get<std::string_view>());
if (rec["race"] != sol::nil)
npc.mRace = ESM::RefId::deserializeText(rec["race"].get<std::string_view>());
if (rec["class"] != sol::nil)
npc.mClass = ESM::RefId::deserializeText(rec["class"].get<std::string_view>());
if (rec["head"] != sol::nil)
npc.mHead = ESM::RefId::deserializeText(rec["head"].get<std::string_view>());
if (rec["hair"] != sol::nil)
npc.mHair = ESM::RefId::deserializeText(rec["hair"].get<std::string_view>());
if (rec["primaryFaction"] != sol::nil)
{
auto factionStr = rec["primaryFaction"].get<std::string_view>();
ESM::RefId factionId = ESM::RefId::deserializeText(factionStr);
const auto& factionStore = MWBase::Environment::get().getESMStore()->get<ESM::Faction>();
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"];
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["isAutocalc"] != sol::nil)
{
bool autoCalc = rec["isAutocalc"];
if (autoCalc)
npc.mFlags |= ESM::NPC::Autocalc;
else
npc.mFlags &= ~ESM::NPC::Autocalc;
}
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 = rec["baseDisposition"].get<int>();
if (rec["baseGold"] != sol::nil)
npc.mNpdt.mGold = rec["baseGold"].get<int>();
if (rec["bloodType"] != sol::nil)
npc.mBloodType = rec["bloodType"].get<int>();
if (rec["primaryFactionRank"] != sol::nil)
{
if (!npc.mFaction.empty())
{
const ESM::Faction* faction
= MWBase::Environment::get().getESMStore()->get<ESM::Faction>().find(npc.mFaction);
int luaValue = rec["primaryFactionRank"];
int rank = LuaUtil::fromLuaIndex(luaValue);
int maxRank = static_cast<int>(getValidRanksCount(faction));
if (rank < 0 || rank >= maxRank)
throw std::runtime_error("primaryFactionRank: Requested rank " + std::to_string(rank)
+ " is out of bounds for faction " + npc.mFaction.toDebugString());
npc.mNpdt.mRank = rank;
}
}
if (rec["servicesOffered"] != sol::nil)
{
const sol::table services = rec["servicesOffered"];
int flags = 0;
for (const auto& [mask, key] : MWLua::ServiceNames)
{
sol::object value = services[key];
if (value != sol::nil && value.as<bool>())
flags |= mask;
}
npc.mAiData.mServices = flags;
}
return npc;
}
ESM::RefId parseFactionId(std::string_view faction)
{
@ -95,9 +216,18 @@ 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<std::string> { return LuaUtil::serializeRefId(rec.mFaction); });
record["primaryFactionRank"] = sol::readonly_property([](const ESM::NPC& rec, sol::this_state s) -> int {
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; });
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"]
= sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; });
@ -152,6 +282,7 @@ namespace MWLua
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);

View file

@ -0,0 +1,21 @@
#ifndef MWLUA_SERVICESOFFERED_HPP
#define MWLUA_SERVICESOFFERED_HPP
#include <array>
#include <components/esm3/loadnpc.hpp>
#include <string_view>
namespace MWLua
{
inline constexpr std::array<std::pair<int, std::string_view>, 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

View file

@ -7,6 +7,7 @@
#include <components/esm3/loadclot.hpp>
#include <components/esm3/loadligh.hpp>
#include <components/esm3/loadmisc.hpp>
#include <components/esm3/loadnpc.hpp>
#include <components/esm3/loadweap.hpp>
#include <components/lua/luastate.hpp>
#include <components/misc/finitenumbers.hpp>
@ -188,6 +189,14 @@ namespace MWLua
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(potion);
},
[lua = context.mLua](const ESM::NPC& npc) -> const ESM::NPC* {
checkGameInitialized(lua);
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);
return MWBase::Environment::get().getESMStore()->insert(weapon);

View file

@ -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:
case ESM::REC_LEVI:
case ESM::REC_LEVC:
case ESM::REC_LIGH:
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:
mStoreImp->mRecNameToStore[type]->read(reader, true);
return true;

View file

@ -269,9 +269,12 @@ namespace MWWorld
list.push_back((*it)->mId);
}
}
template <class T, class Id>
T* TypedDynamicStore<T, Id>::insert(const T& item, bool overrideOnly)
{
if constexpr (std::is_same_v<decltype(item.mId), ESM::RefId>)
overrideOnly = overrideOnly && !item.mId.template is<ESM::GeneratedRefId>();
if (overrideOnly)
{
auto it = mStatic.find(item.mId);

View file

@ -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 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
-- @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.
@ -875,6 +878,13 @@
-- @field #Actor baseType @{#Actor}
-- @field [parent=#NPC] #NpcStats stats
---
-- 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 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.
---
-- 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.