Merge branch 'crime-interface' into 'master'

add OFFENSE_TYPE and commitCrime to lua

Closes #8109

See merge request OpenMW/openmw!4319
pull/3236/head
psi29a 2 months ago
commit 941a6dcf89

@ -1,10 +1,14 @@
#include "types.hpp"
#include <components/esm3/loadbsgn.hpp>
#include <components/esm3/loadfact.hpp>
#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 <components/esm3/loadbsgn.hpp>
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<ESM::Faction>().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<std::string_view, int>(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<Object> 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<const GObject*>(&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<MWBase::MechanicsManager::OffenseType>(type), factionId, arg, victimAware);
};
player["birthSigns"] = initBirthSignRecordBindings(context);
player["getBirthSign"] = [](const Object& player) -> std::string {
verifyPlayer(player);

@ -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[@]}"

@ -45,6 +45,7 @@ Lua API reference
interface_settings
interface_skill_progression
interface_ui
interface_crimes
iterables

@ -0,0 +1,5 @@
Interface Crimes
==========================
.. raw:: html
:file: generated_html/scripts_omw_crimes.html

@ -43,3 +43,6 @@
- by player scripts
- | High-level UI modes interface. Allows to override parts
| of the interface.
* - :ref:`Crimes <Interface Crimes>`
- by global scripts
- Commit crimes.

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

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

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

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

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

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

Loading…
Cancel
Save