Skip to content

Commit 941a6dc

Browse files
committed
Merge branch 'crime-interface' into 'master'
add OFFENSE_TYPE and commitCrime to lua Closes #8109 See merge request OpenMW/openmw!4319
2 parents 9325c80 + 9248e37 commit 941a6dc

File tree

11 files changed

+164
-2
lines changed

11 files changed

+164
-2
lines changed

apps/openmw/mwlua/types/player.cpp

+48-2
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
#include "types.hpp"
22

3+
#include <components/esm3/loadbsgn.hpp>
4+
#include <components/esm3/loadfact.hpp>
5+
36
#include "../birthsignbindings.hpp"
47
#include "../luamanagerimp.hpp"
58

69
#include "apps/openmw/mwbase/inputmanager.hpp"
710
#include "apps/openmw/mwbase/journal.hpp"
11+
#include "apps/openmw/mwbase/mechanicsmanager.hpp"
812
#include "apps/openmw/mwbase/world.hpp"
913
#include "apps/openmw/mwmechanics/npcstats.hpp"
1014
#include "apps/openmw/mwworld/class.hpp"
1115
#include "apps/openmw/mwworld/esmstore.hpp"
1216
#include "apps/openmw/mwworld/globals.hpp"
1317
#include "apps/openmw/mwworld/player.hpp"
1418

15-
#include <components/esm3/loadbsgn.hpp>
16-
1719
namespace MWLua
1820
{
1921
struct Quests
@@ -51,6 +53,14 @@ namespace
5153
throw std::runtime_error("Failed to find birth sign: " + std::string(textId));
5254
return id;
5355
}
56+
57+
ESM::RefId parseFactionId(std::string_view faction)
58+
{
59+
ESM::RefId id = ESM::RefId::deserializeText(faction);
60+
if (!MWBase::Environment::get().getESMStore()->get<ESM::Faction>().search(id))
61+
return ESM::RefId();
62+
return id;
63+
}
5464
}
5565

5666
namespace MWLua
@@ -61,6 +71,12 @@ namespace MWLua
6171
throw std::runtime_error("The argument must be a player!");
6272
}
6373

74+
static void verifyNpc(const MWWorld::Class& cls)
75+
{
76+
if (!cls.isNpc())
77+
throw std::runtime_error("The argument must be a NPC!");
78+
}
79+
6480
void addPlayerBindings(sol::table player, const Context& context)
6581
{
6682
MWBase::Journal* const journal = MWBase::Environment::get().getJournal();
@@ -201,6 +217,36 @@ namespace MWLua
201217
return MWBase::Environment::get().getWorld()->getGlobalFloat(MWWorld::Globals::sCharGenState) == -1;
202218
};
203219

220+
player["OFFENSE_TYPE"]
221+
= LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs<std::string_view, int>(context.sol(),
222+
{ { "Theft", MWBase::MechanicsManager::OffenseType::OT_Theft },
223+
{ "Assault", MWBase::MechanicsManager::OffenseType::OT_Assault },
224+
{ "Murder", MWBase::MechanicsManager::OffenseType::OT_Murder },
225+
{ "Trespassing", MWBase::MechanicsManager::OffenseType::OT_Trespassing },
226+
{ "SleepingInOwnedBed", MWBase::MechanicsManager::OffenseType::OT_SleepingInOwnedBed },
227+
{ "Pickpocket", MWBase::MechanicsManager::OffenseType::OT_Pickpocket } }));
228+
player["_runStandardCommitCrime"] = [](const Object& o, const sol::optional<Object> victim, int type,
229+
std::string_view faction, int arg = 0, bool victimAware = false) {
230+
verifyPlayer(o);
231+
if (victim.has_value() && !victim->ptrOrEmpty().isEmpty())
232+
verifyNpc(victim->ptrOrEmpty().getClass());
233+
if (!dynamic_cast<const GObject*>(&o))
234+
throw std::runtime_error("Only global scripts can commit crime");
235+
if (type < 0 || type > MWBase::MechanicsManager::OffenseType::OT_Pickpocket)
236+
throw std::runtime_error("Invalid offense type");
237+
238+
ESM::RefId factionId = parseFactionId(faction);
239+
// If the faction is provided but not found, error out
240+
if (faction != "" && factionId == ESM::RefId())
241+
throw std::runtime_error("Faction does not exist");
242+
243+
MWWorld::Ptr victimObj = nullptr;
244+
if (victim.has_value())
245+
victimObj = victim->ptrOrEmpty();
246+
return MWBase::Environment::get().getMechanicsManager()->commitCrime(o.ptr(), victimObj,
247+
static_cast<MWBase::MechanicsManager::OffenseType>(type), factionId, arg, victimAware);
248+
};
249+
204250
player["birthSigns"] = initBirthSignRecordBindings(context);
205251
player["getBirthSign"] = [](const Object& player) -> std::string {
206252
verifyPlayer(player);

docs/source/luadoc_data_paths.sh

+1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ paths=(
1111
scripts/omw/ui.lua
1212
scripts/omw/usehandlers.lua
1313
scripts/omw/skillhandlers.lua
14+
scripts/omw/crimes.lua
1415
)
1516
printf '%s\n' "${paths[@]}"

docs/source/reference/lua-scripting/api.rst

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Lua API reference
4545
interface_settings
4646
interface_skill_progression
4747
interface_ui
48+
interface_crimes
4849
iterables
4950

5051

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Interface Crimes
2+
==========================
3+
4+
.. raw:: html
5+
:file: generated_html/scripts_omw_crimes.html

docs/source/reference/lua-scripting/tables/interfaces.rst

+3
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@
4343
- by player scripts
4444
- | High-level UI modes interface. Allows to override parts
4545
| of the interface.
46+
* - :ref:`Crimes <Interface Crimes>`
47+
- by global scripts
48+
- Commit crimes.

files/data/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ set(BUILTIN_DATA_FILES
110110
scripts/omw/mwui/space.lua
111111
scripts/omw/mwui/init.lua
112112
scripts/omw/skillhandlers.lua
113+
scripts/omw/crimes.lua
113114
scripts/omw/ui.lua
114115
scripts/omw/usehandlers.lua
115116
scripts/omw/worldeventhandlers.lua

files/data/builtin.omwscripts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ GLOBAL: scripts/omw/activationhandlers.lua
1111
GLOBAL: scripts/omw/cellhandlers.lua
1212
GLOBAL: scripts/omw/usehandlers.lua
1313
GLOBAL: scripts/omw/worldeventhandlers.lua
14+
GLOBAL: scripts/omw/crimes.lua
1415
CREATURE, NPC, PLAYER: scripts/omw/mechanics/animationcontroller.lua
1516
PLAYER: scripts/omw/skillhandlers.lua
1617
PLAYER: scripts/omw/mechanics/playercontroller.lua

files/data/scripts/omw/crimes.lua

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
local types = require('openmw.types')
2+
local I = require('openmw.interfaces')
3+
4+
---
5+
-- Table with information needed to commit crimes.
6+
-- @type CommitCrimeInputs
7+
-- @field openmw.core#GameObject victim The victim of the crime (optional)
8+
-- @field openmw.types#OFFENSE_TYPE type The type of the crime to commit. See @{openmw.types#OFFENSE_TYPE} (required)
9+
-- @field #string faction ID of the faction the crime is committed against (optional)
10+
-- @field #number arg The amount to increase the player bounty by, if the crime type is theft. Ignored otherwise (optional, defaults to 0)
11+
-- @field #boolean victimAware Whether the victim is aware of the crime (optional, defaults to false)
12+
13+
---
14+
-- Table containing information returned by the engine after committing a crime
15+
-- @type CommitCrimeOutputs
16+
-- @field #boolean wasCrimeSeen Whether the crime was seen
17+
18+
return {
19+
interfaceName = 'Crimes',
20+
---
21+
-- Allows to utilize built-in crime mechanics.
22+
-- @module Crimes
23+
-- @usage require('openmw.interfaces').Crimes
24+
interface = {
25+
--- Interface version
26+
-- @field [parent=#Crimes] #number version
27+
version = 1,
28+
29+
---
30+
-- Commits a crime as if done through an in-game action. Can only be used in global context.
31+
-- @function [parent=#Crimes] commitCrime
32+
-- @param openmw.core#GameObject player The player committing the crime
33+
-- @param CommitCrimeInputs options A table of parameters describing the committed crime
34+
-- @return CommitCrimeOutputs A table containing information about the committed crime
35+
commitCrime = function(player, options)
36+
assert(types.Player.objectIsInstance(player), "commitCrime requires a player game object")
37+
38+
local returnTable = {}
39+
local options = options or {}
40+
41+
assert(type(options.faction) == "string" or options.faction == nil,
42+
"faction id passed to commitCrime must be a string or nil")
43+
assert(type(options.arg) == "number" or options.arg == nil,
44+
"arg value passed to commitCrime must be a number or nil")
45+
assert(type(options.victimAware) == "number" or options.victimAware == nil,
46+
"victimAware value passed to commitCrime must be a boolean or nil")
47+
48+
assert(options.type ~= nil, "crime type passed to commitCrime cannot be nil")
49+
assert(type(options.type) == "number", "crime type passed to commitCrime must be a number")
50+
51+
assert(options.victim == nil or types.NPC.objectIsInstance(options.victim),
52+
"victim passed to commitCrime must be an NPC or nil")
53+
54+
returnTable.wasCrimeSeen = types.Player._runStandardCommitCrime(player, options.victim, options.type,
55+
options.faction or "",
56+
options.arg or 0, options.victimAware or false)
57+
return returnTable
58+
end,
59+
},
60+
eventHandlers = {
61+
CommitCrime = function(data) I.Crimes.commitCrime(data.player, data) end,
62+
}
63+
}

files/lua_api/openmw/interfaces.lua

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
---
3030
-- @field [parent=#interfaces] scripts.omw.skillhandlers#scripts.omw.skillhandlers SkillProgression
3131

32+
---
33+
-- @field [parent=#interfaces] scripts.omw.crimes#scripts.omw.crimes Crimes
34+
3235
---
3336
-- @function [parent=#interfaces] __index
3437
-- @param #interfaces self

files/lua_api/openmw/types.lua

+13
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,19 @@
11691169
-- @param openmw.core#GameObject player
11701170
-- @param #number crimeLevel The requested crime level
11711171

1172+
---
1173+
-- @type OFFENSE_TYPE
1174+
-- @field #number Theft
1175+
-- @field #number Assault
1176+
-- @field #number Murder
1177+
-- @field #number Trespassing
1178+
-- @field #number SleepingInOwnedBed
1179+
-- @field #number Pickpocket
1180+
1181+
---
1182+
-- Available @{#OFFENSE_TYPE} values. Used in `I.Crimes.commitCrime`.
1183+
-- @field [parent=#Player] #OFFENSE_TYPE OFFENSE_TYPE
1184+
11721185
---
11731186
-- Whether the character generation for this player is finished.
11741187
-- @function [parent=#Player] isCharGenFinished

scripts/data/integration_tests/test_lua_api/test.lua

+25
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ local util = require('openmw.util')
55
local types = require('openmw.types')
66
local vfs = require('openmw.vfs')
77
local world = require('openmw.world')
8+
local I = require('openmw.interfaces')
89

910
local function testTimers()
1011
testing.expectAlmostEqual(core.getGameTimeScale(), 30, 'incorrect getGameTimeScale() result')
@@ -261,6 +262,29 @@ local function testVFS()
261262
testing.expectEqual(vfs.type(handle), 'closed file', 'File should be closed')
262263
end
263264

265+
local function testCommitCrime()
266+
initPlayer()
267+
local player = world.players[1]
268+
testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `testCommitCrime`')
269+
testing.expectEqual(I.Crimes == nil, false, 'Crimes interface should be available in global contexts')
270+
271+
-- Reset crime level to have a clean slate
272+
types.Player.setCrimeLevel(player, 0)
273+
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")
274+
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")
275+
testing.expectEqual(I.Crimes.commitCrime(player, { type = types.Player.OFFENSE_TYPE.Murder }).wasCrimeSeen, false, "Running a murder crime should work even without a victim")
276+
277+
-- Create a mockup target for crimes
278+
local victim = world.createObject(types.NPC.record(player).id)
279+
victim:teleport(player.cell, player.position + util.vector3(0, 300, 0))
280+
coroutine.yield()
281+
282+
-- Reset crime level for testing with a valid victim
283+
types.Player.setCrimeLevel(player, 0)
284+
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")
285+
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")
286+
end
287+
264288
tests = {
265289
{'timers', testTimers},
266290
{'rotating player with controls.yawChange should change rotation', function()
@@ -321,6 +345,7 @@ tests = {
321345
testing.runLocalTest(player, 'playerWeaponAttack')
322346
end},
323347
{'vfs', testVFS},
348+
{'testCommitCrime', testCommitCrime}
324349
}
325350

326351
return {

0 commit comments

Comments
 (0)