diff --git a/apps/openmw/mwlua/types/player.cpp b/apps/openmw/mwlua/types/player.cpp index 97619dc3be..15dc719f2e 100644 --- a/apps/openmw/mwlua/types/player.cpp +++ b/apps/openmw/mwlua/types/player.cpp @@ -1,10 +1,14 @@ #include "types.hpp" +#include +#include + #include "../birthsignbindings.hpp" #include "../luamanagerimp.hpp" #include "apps/openmw/mwbase/inputmanager.hpp" #include "apps/openmw/mwbase/journal.hpp" +#include "apps/openmw/mwbase/mechanicsmanager.hpp" #include "apps/openmw/mwbase/world.hpp" #include "apps/openmw/mwmechanics/npcstats.hpp" #include "apps/openmw/mwworld/class.hpp" @@ -12,8 +16,6 @@ #include "apps/openmw/mwworld/globals.hpp" #include "apps/openmw/mwworld/player.hpp" -#include - namespace MWLua { struct Quests @@ -51,6 +53,14 @@ namespace throw std::runtime_error("Failed to find birth sign: " + std::string(textId)); return id; } + + ESM::RefId parseFactionId(std::string_view faction) + { + ESM::RefId id = ESM::RefId::deserializeText(faction); + if (!MWBase::Environment::get().getESMStore()->get().search(id)) + return ESM::RefId(); + return id; + } } namespace MWLua @@ -61,6 +71,12 @@ namespace MWLua throw std::runtime_error("The argument must be a player!"); } + static void verifyNpc(const MWWorld::Class& cls) + { + if (!cls.isNpc()) + throw std::runtime_error("The argument must be a NPC!"); + } + void addPlayerBindings(sol::table player, const Context& context) { MWBase::Journal* const journal = MWBase::Environment::get().getJournal(); @@ -201,6 +217,36 @@ namespace MWLua return MWBase::Environment::get().getWorld()->getGlobalFloat(MWWorld::Globals::sCharGenState) == -1; }; + player["OFFENSE_TYPE"] + = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(context.sol(), + { { "Theft", MWBase::MechanicsManager::OffenseType::OT_Theft }, + { "Assault", MWBase::MechanicsManager::OffenseType::OT_Assault }, + { "Murder", MWBase::MechanicsManager::OffenseType::OT_Murder }, + { "Trespassing", MWBase::MechanicsManager::OffenseType::OT_Trespassing }, + { "SleepingInOwnedBed", MWBase::MechanicsManager::OffenseType::OT_SleepingInOwnedBed }, + { "Pickpocket", MWBase::MechanicsManager::OffenseType::OT_Pickpocket } })); + player["_runStandardCommitCrime"] = [](const Object& o, const sol::optional victim, int type, + std::string_view faction, int arg = 0, bool victimAware = false) { + verifyPlayer(o); + if (victim.has_value() && !victim->ptrOrEmpty().isEmpty()) + verifyNpc(victim->ptrOrEmpty().getClass()); + if (!dynamic_cast(&o)) + throw std::runtime_error("Only global scripts can commit crime"); + if (type < 0 || type > MWBase::MechanicsManager::OffenseType::OT_Pickpocket) + throw std::runtime_error("Invalid offense type"); + + ESM::RefId factionId = parseFactionId(faction); + // If the faction is provided but not found, error out + if (faction != "" && factionId == ESM::RefId()) + throw std::runtime_error("Faction does not exist"); + + MWWorld::Ptr victimObj = nullptr; + if (victim.has_value()) + victimObj = victim->ptrOrEmpty(); + return MWBase::Environment::get().getMechanicsManager()->commitCrime(o.ptr(), victimObj, + static_cast(type), factionId, arg, victimAware); + }; + player["birthSigns"] = initBirthSignRecordBindings(context); player["getBirthSign"] = [](const Object& player) -> std::string { verifyPlayer(player); diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index bd8c96508c..997d5846af 100755 --- a/docs/source/luadoc_data_paths.sh +++ b/docs/source/luadoc_data_paths.sh @@ -11,5 +11,6 @@ paths=( scripts/omw/ui.lua scripts/omw/usehandlers.lua scripts/omw/skillhandlers.lua + scripts/omw/crimes.lua ) printf '%s\n' "${paths[@]}" diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index ef6637cf7d..82a860b355 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -45,6 +45,7 @@ Lua API reference interface_settings interface_skill_progression interface_ui + interface_crimes iterables diff --git a/docs/source/reference/lua-scripting/interface_crimes.rst b/docs/source/reference/lua-scripting/interface_crimes.rst new file mode 100644 index 0000000000..dc4e68477f --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_crimes.rst @@ -0,0 +1,5 @@ +Interface Crimes +========================== + +.. raw:: html + :file: generated_html/scripts_omw_crimes.html diff --git a/docs/source/reference/lua-scripting/tables/interfaces.rst b/docs/source/reference/lua-scripting/tables/interfaces.rst index 42a9cd70ba..3a074c1ad7 100644 --- a/docs/source/reference/lua-scripting/tables/interfaces.rst +++ b/docs/source/reference/lua-scripting/tables/interfaces.rst @@ -43,3 +43,6 @@ - by player scripts - | High-level UI modes interface. Allows to override parts | of the interface. + * - :ref:`Crimes ` + - by global scripts + - Commit crimes. diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index d03d7e634a..d9218b45b2 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -110,6 +110,7 @@ set(BUILTIN_DATA_FILES scripts/omw/mwui/space.lua scripts/omw/mwui/init.lua scripts/omw/skillhandlers.lua + scripts/omw/crimes.lua scripts/omw/ui.lua scripts/omw/usehandlers.lua scripts/omw/worldeventhandlers.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index c44b335861..37367783ab 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -11,6 +11,7 @@ GLOBAL: scripts/omw/activationhandlers.lua GLOBAL: scripts/omw/cellhandlers.lua GLOBAL: scripts/omw/usehandlers.lua GLOBAL: scripts/omw/worldeventhandlers.lua +GLOBAL: scripts/omw/crimes.lua CREATURE, NPC, PLAYER: scripts/omw/mechanics/animationcontroller.lua PLAYER: scripts/omw/skillhandlers.lua PLAYER: scripts/omw/mechanics/playercontroller.lua diff --git a/files/data/scripts/omw/crimes.lua b/files/data/scripts/omw/crimes.lua new file mode 100644 index 0000000000..58e043964b --- /dev/null +++ b/files/data/scripts/omw/crimes.lua @@ -0,0 +1,63 @@ +local types = require('openmw.types') +local I = require('openmw.interfaces') + +--- +-- Table with information needed to commit crimes. +-- @type CommitCrimeInputs +-- @field openmw.core#GameObject victim The victim of the crime (optional) +-- @field openmw.types#OFFENSE_TYPE type The type of the crime to commit. See @{openmw.types#OFFENSE_TYPE} (required) +-- @field #string faction ID of the faction the crime is committed against (optional) +-- @field #number arg The amount to increase the player bounty by, if the crime type is theft. Ignored otherwise (optional, defaults to 0) +-- @field #boolean victimAware Whether the victim is aware of the crime (optional, defaults to false) + +--- +-- Table containing information returned by the engine after committing a crime +-- @type CommitCrimeOutputs +-- @field #boolean wasCrimeSeen Whether the crime was seen + +return { + interfaceName = 'Crimes', + --- + -- Allows to utilize built-in crime mechanics. + -- @module Crimes + -- @usage require('openmw.interfaces').Crimes + interface = { + --- Interface version + -- @field [parent=#Crimes] #number version + version = 1, + + --- + -- Commits a crime as if done through an in-game action. Can only be used in global context. + -- @function [parent=#Crimes] commitCrime + -- @param openmw.core#GameObject player The player committing the crime + -- @param CommitCrimeInputs options A table of parameters describing the committed crime + -- @return CommitCrimeOutputs A table containing information about the committed crime + commitCrime = function(player, options) + assert(types.Player.objectIsInstance(player), "commitCrime requires a player game object") + + local returnTable = {} + local options = options or {} + + assert(type(options.faction) == "string" or options.faction == nil, + "faction id passed to commitCrime must be a string or nil") + assert(type(options.arg) == "number" or options.arg == nil, + "arg value passed to commitCrime must be a number or nil") + assert(type(options.victimAware) == "number" or options.victimAware == nil, + "victimAware value passed to commitCrime must be a boolean or nil") + + assert(options.type ~= nil, "crime type passed to commitCrime cannot be nil") + assert(type(options.type) == "number", "crime type passed to commitCrime must be a number") + + assert(options.victim == nil or types.NPC.objectIsInstance(options.victim), + "victim passed to commitCrime must be an NPC or nil") + + returnTable.wasCrimeSeen = types.Player._runStandardCommitCrime(player, options.victim, options.type, + options.faction or "", + options.arg or 0, options.victimAware or false) + return returnTable + end, + }, + eventHandlers = { + CommitCrime = function(data) I.Crimes.commitCrime(data.player, data) end, + } +} diff --git a/files/lua_api/openmw/interfaces.lua b/files/lua_api/openmw/interfaces.lua index 5ed98bd8be..6f6e86be1e 100644 --- a/files/lua_api/openmw/interfaces.lua +++ b/files/lua_api/openmw/interfaces.lua @@ -29,6 +29,9 @@ --- -- @field [parent=#interfaces] scripts.omw.skillhandlers#scripts.omw.skillhandlers SkillProgression +--- +-- @field [parent=#interfaces] scripts.omw.crimes#scripts.omw.crimes Crimes + --- -- @function [parent=#interfaces] __index -- @param #interfaces self diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 669227695b..e518723ea5 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -1169,6 +1169,19 @@ -- @param openmw.core#GameObject player -- @param #number crimeLevel The requested crime level +--- +-- @type OFFENSE_TYPE +-- @field #number Theft +-- @field #number Assault +-- @field #number Murder +-- @field #number Trespassing +-- @field #number SleepingInOwnedBed +-- @field #number Pickpocket + +--- +-- Available @{#OFFENSE_TYPE} values. Used in `I.Crimes.commitCrime`. +-- @field [parent=#Player] #OFFENSE_TYPE OFFENSE_TYPE + --- -- Whether the character generation for this player is finished. -- @function [parent=#Player] isCharGenFinished diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index ac45a2cc11..af2a17efc9 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -5,6 +5,7 @@ local util = require('openmw.util') local types = require('openmw.types') local vfs = require('openmw.vfs') local world = require('openmw.world') +local I = require('openmw.interfaces') local function testTimers() testing.expectAlmostEqual(core.getGameTimeScale(), 30, 'incorrect getGameTimeScale() result') @@ -261,6 +262,29 @@ local function testVFS() testing.expectEqual(vfs.type(handle), 'closed file', 'File should be closed') end +local function testCommitCrime() + initPlayer() + local player = world.players[1] + testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `testCommitCrime`') + testing.expectEqual(I.Crimes == nil, false, 'Crimes interface should be available in global contexts') + + -- Reset crime level to have a clean slate + types.Player.setCrimeLevel(player, 0) + testing.expectEqual(I.Crimes.commitCrime(player, { type = types.Player.OFFENSE_TYPE.Theft, victim = player, arg = 100}).wasCrimeSeen, false, "Running the crime with the player as the victim should not result in a seen crime") + testing.expectEqual(I.Crimes.commitCrime(player, { type = types.Player.OFFENSE_TYPE.Theft, arg = 50 }).wasCrimeSeen, false, "Running the crime with no victim and a type shouldn't raise errors") + testing.expectEqual(I.Crimes.commitCrime(player, { type = types.Player.OFFENSE_TYPE.Murder }).wasCrimeSeen, false, "Running a murder crime should work even without a victim") + + -- Create a mockup target for crimes + local victim = world.createObject(types.NPC.record(player).id) + victim:teleport(player.cell, player.position + util.vector3(0, 300, 0)) + coroutine.yield() + + -- Reset crime level for testing with a valid victim + types.Player.setCrimeLevel(player, 0) + testing.expectEqual(I.Crimes.commitCrime(player, { victim = victim, type = types.Player.OFFENSE_TYPE.Theft, arg = 50 }).wasCrimeSeen, true, "Running a crime with a valid victim should notify them when the player is not sneaking, even if it's not explicitly passed in") + testing.expectEqual(types.Player.getCrimeLevel(player), 0, "Crime level should not change if the victim's alarm value is low and there's no other witnesses") +end + tests = { {'timers', testTimers}, {'rotating player with controls.yawChange should change rotation', function() @@ -321,6 +345,7 @@ tests = { testing.runLocalTest(player, 'playerWeaponAttack') end}, {'vfs', testVFS}, + {'testCommitCrime', testCommitCrime} } return {