Skip to content

Commit

Permalink
Merge branch 'crime-interface' into 'master'
Browse files Browse the repository at this point in the history
add OFFENSE_TYPE and commitCrime to lua

Closes #8109

See merge request OpenMW/openmw!4319
  • Loading branch information
psi29a committed Oct 20, 2024
2 parents 9325c80 + 9248e37 commit 941a6dc
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 2 deletions.
50 changes: 48 additions & 2 deletions apps/openmw/mwlua/types/player.cpp
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
#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"
#include "apps/openmw/mwworld/esmstore.hpp"
#include "apps/openmw/mwworld/globals.hpp"
#include "apps/openmw/mwworld/player.hpp"

#include <components/esm3/loadbsgn.hpp>

namespace MWLua
{
struct Quests
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions docs/source/luadoc_data_paths.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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[@]}"
1 change: 1 addition & 0 deletions docs/source/reference/lua-scripting/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Lua API reference
interface_settings
interface_skill_progression
interface_ui
interface_crimes
iterables


Expand Down
5 changes: 5 additions & 0 deletions docs/source/reference/lua-scripting/interface_crimes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Interface Crimes
==========================

.. raw:: html
:file: generated_html/scripts_omw_crimes.html
3 changes: 3 additions & 0 deletions docs/source/reference/lua-scripting/tables/interfaces.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions files/data/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions files/data/builtin.omwscripts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 63 additions & 0 deletions files/data/scripts/omw/crimes.lua
Original file line number Diff line number Diff line change
@@ -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,
}
}
3 changes: 3 additions & 0 deletions files/lua_api/openmw/interfaces.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions files/lua_api/openmw/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions scripts/data/integration_tests/test_lua_api/test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -321,6 +345,7 @@ tests = {
testing.runLocalTest(player, 'playerWeaponAttack')
end},
{'vfs', testVFS},
{'testCommitCrime', testCommitCrime}
}

return {
Expand Down

0 comments on commit 941a6dc

Please sign in to comment.