Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ set(libdevilutionx_SRCS
lua/modules/dev/search.cpp
lua/modules/dev/towners.cpp
lua/modules/floatingnumbers.cpp
lua/modules/game.cpp
lua/modules/i18n.cpp
lua/modules/items.cpp
lua/modules/log.cpp
Expand Down
5 changes: 5 additions & 0 deletions Source/inv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
#include <SDL3/SDL_keyboard.h>
#include <SDL3/SDL_rect.h>
#else
#include <SDL.h>

Check warning on line 16 in Source/inv.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/inv.cpp:16:1 [misc-include-cleaner]

included header SDL.h is not used directly
#endif

#include <fmt/format.h>

Check warning on line 19 in Source/inv.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/inv.cpp:19:1 [misc-include-cleaner]

included header format.h is not used directly

#include "DiabloUI/ui_flags.hpp"
#include "controls/control_mode.hpp"
Expand All @@ -29,25 +29,26 @@
#include "engine/render/clx_render.hpp"
#include "engine/render/text_render.hpp"
#include "engine/size.hpp"
#include "hwcursor.hpp"

Check warning on line 32 in Source/inv.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/inv.cpp:32:1 [misc-include-cleaner]

included header hwcursor.hpp is not used directly
#include "inv_iterators.hpp"

Check warning on line 33 in Source/inv.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/inv.cpp:33:1 [misc-include-cleaner]

included header inv_iterators.hpp is not used directly
#include "levels/tile_properties.hpp"
#include "levels/town.h"
#include "lua/lua_global.hpp"
#include "minitext.h"
#include "options.h"
#include "panels/ui_panels.hpp"
#include "player.h"
#include "plrmsg.h"

Check warning on line 41 in Source/inv.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/inv.cpp:41:1 [misc-include-cleaner]

included header plrmsg.h is not used directly
#include "qol/stash.h"
#include "stores.h"
#include "towners.h"

Check warning on line 44 in Source/inv.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/inv.cpp:44:1 [misc-include-cleaner]

included header towners.h is not used directly
#include "utils/display.h"
#include "utils/format_int.hpp"
#include "utils/is_of.hpp"
#include "utils/language.h"
#include "utils/sdl_geometry.h"
#include "utils/str_cat.hpp"
#include "utils/utf8.hpp"

Check warning on line 51 in Source/inv.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/inv.cpp:51:1 [misc-include-cleaner]

included header utf8.hpp is not used directly

namespace devilution {

Expand Down Expand Up @@ -75,7 +76,7 @@
* 47 48 49 50 51 52 53 54
* @endcode
*/
const Rectangle InvRect[] = {

Check warning on line 79 in Source/inv.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/inv.cpp:79:7 [misc-include-cleaner]

no header providing "devilution::Rectangle" is directly included
// clang-format off
//{ X, Y }, { W, H }
{ { 132, 2 }, { 58, 59 } }, // helmet
Expand Down Expand Up @@ -154,9 +155,9 @@
const int rowGridIndex = invGridIndex + pitch * y;
for (int x = 0; x < itemSize.width; x++) {
if (x == 0 && y == itemSize.height - 1)
player.InvGrid[rowGridIndex + x] = invListIndex;

Check warning on line 158 in Source/inv.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/inv.cpp:158:40 [bugprone-narrowing-conversions]

narrowing conversion from 'int' to signed type 'int8_t' (aka 'signed char') is implementation-defined
else
player.InvGrid[rowGridIndex + x] = -invListIndex; // use negative index to denote it's occupied but it's not the top-left cell.

Check warning on line 160 in Source/inv.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/inv.cpp:160:40 [bugprone-narrowing-conversions]

narrowing conversion from 'int' to signed type 'int8_t' (aka 'signed char') is implementation-defined
}
}

Expand Down Expand Up @@ -2178,6 +2179,10 @@
return true;
}

if (LuaEventCancellable("OnItemUse", &player, item)) {
return true;
}

const int idata = ItemCAnimTbl[item->_iCurs];
if (item->_iMiscId == IMISC_BOOK)
PlaySFX(SfxID::ReadBook);
Expand Down
32 changes: 32 additions & 0 deletions Source/lua/lua_global.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
#include "appfat.h"
#include "effects.h"
#include "engine/assets.hpp"
#include "items.h"
#include "lua/modules/audio.hpp"
#include "lua/modules/floatingnumbers.hpp"
#include "lua/modules/game.hpp"
#include "lua/modules/hellfire.hpp"
#include "lua/modules/i18n.hpp"
#include "lua/modules/items.hpp"
Expand Down Expand Up @@ -281,6 +283,7 @@ void LuaInitialize()
"devilutionx.dev", LuaDevModule(lua),
#endif
"devilutionx.version", PROJECT_VERSION,
"devilutionx.game", LuaGameModule(lua),
"devilutionx.i18n", LuaI18nModule(lua),
"devilutionx.items", LuaItemModule(lua),
"devilutionx.log", LuaLogModule(lua),
Expand Down Expand Up @@ -350,11 +353,40 @@ void LuaEvent(std::string_view name, const Monster *monster, int arg1, int arg2)
CallLuaEvent(name, monster, arg1, arg2);
}

void LuaEvent(std::string_view name, const Monster *monster, int arg1)
{
CallLuaEvent(name, monster, arg1);
}

void LuaEvent(std::string_view name, const Player *player, uint32_t arg1)
{
CallLuaEvent(name, player, arg1);
}

bool LuaEventCancellable(std::string_view name, const Player *player, const Item *item)
{
if (!CurrentLuaState.has_value()) {
return false;
}

const auto trigger = CurrentLuaState->events.traverse_get<std::optional<sol::object>>(name, "trigger");
if (!trigger.has_value() || !trigger->is<sol::protected_function>()) {
LogError("events.{}.trigger is not a function", name);
return false;
}
const sol::protected_function fn = trigger->as<sol::protected_function>();
const sol::protected_function_result result = fn(player, item);
if (!result.valid()) {
const std::string error = result.get_type() == sol::type::string
? StrCat("Lua error: ", result.get<std::string>())
: "Unknown Lua error";
LogError(error);
return false;
}
const sol::object retVal = result;
return retVal.is<bool>() && retVal.as<bool>();
}

sol::state &GetLuaState()
{
return CurrentLuaState->sol;
Expand Down
8 changes: 8 additions & 0 deletions Source/lua/lua_global.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace devilution {

struct Item;
struct Player;
struct Monster;

Expand All @@ -18,7 +19,14 @@ void LuaEvent(std::string_view name);
void LuaEvent(std::string_view name, std::string_view arg);
void LuaEvent(std::string_view name, const Player *player, int arg1, int arg2);
void LuaEvent(std::string_view name, const Monster *monster, int arg1, int arg2);
void LuaEvent(std::string_view name, const Monster *monster, int arg1);
void LuaEvent(std::string_view name, const Player *player, uint32_t arg1);

/**
* @brief Fires a cancellable Lua event.
* @return true if any handler returned true (i.e. default behaviour should be cancelled).
*/
bool LuaEventCancellable(std::string_view name, const Player *player, const Item *item);
sol::state &GetLuaState();
sol::environment CreateLuaSandbox();
sol::object SafeCallResult(sol::protected_function_result result, bool optional);
Expand Down
34 changes: 34 additions & 0 deletions Source/lua/modules/game.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include "lua/modules/game.hpp"

#include <sol/sol.hpp>

#include "lua/metadoc.hpp"
#include "monster.h"
#include "multi.h"
#include "quests.h"
#include "tables/objdat.h"

namespace devilution {

sol::table LuaGameModule(sol::state_view &lua)
{
sol::table table = lua.create_table();

LuaSetDocFn(table, "prepDoEnding", "()",
"Triggers the game-ending sequence (win condition). Safe to call in multiplayer.",
PrepDoEnding);

LuaSetDocFn(table, "isQuestDone", "(questId: integer) -> boolean",
"Returns true if the quest with the given ID has been completed.",
[](int questId) {
return Quests[questId]._qactive == QUEST_DONE;
});

LuaSetDocFn(table, "isMultiplayer", "() -> boolean",
"Returns true when running in a multiplayer session.",
[]() { return gbIsMultiplayer; });

return table;
}

} // namespace devilution
9 changes: 9 additions & 0 deletions Source/lua/modules/game.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include <sol/sol.hpp>

namespace devilution {

sol::table LuaGameModule(sol::state_view &lua);

} // namespace devilution
11 changes: 11 additions & 0 deletions Source/lua/modules/items.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
#include <fmt/format.h>
#include <sol/sol.hpp>

#include "cursor.h"
#include "data/file.hpp"
#include "engine/point.hpp"
#include "items.h"
#include "lua/metadoc.hpp"
#include "player.h"
Expand Down Expand Up @@ -465,6 +467,11 @@ void AddUniqueItemDataFromTsv(const std::string_view path, const int32_t baseMap
LoadUniqueItemDatFromFile(dataFile, path, baseMappingId);
}

void LuaSpawnQuestItem(int itemIdx, int x, int y, bool sendmsg)
{
SpawnQuestItem(static_cast<_item_indexes>(itemIdx), Point { x, y }, 0, SelectionRegion::Bottom, sendmsg);
}

} // namespace

sol::table LuaItemModule(sol::state_view &lua)
Expand All @@ -484,6 +491,10 @@ sol::table LuaItemModule(sol::state_view &lua)

LuaSetDocFn(table, "addItemDataFromTsv", "(path: string, baseMappingId: number)", AddItemDataFromTsv);
LuaSetDocFn(table, "addUniqueItemDataFromTsv", "(path: string, baseMappingId: number)", AddUniqueItemDataFromTsv);
LuaSetDocFn(table, "spawnQuestItem",
"(itemIdx: ItemIndex, x: integer, y: integer, sendmsg: boolean = true)",
"Spawns a quest item at the given world coordinates. Pass sendmsg=true to sync with other clients.",
LuaSpawnQuestItem);

// Expose enums through the module table
table["ItemIndex"] = lua["ItemIndex"];
Expand Down
10 changes: 10 additions & 0 deletions Source/lua/modules/monsters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ void InitMonsterUserType(sol::state_view &lua)
[](const Monster &monster) {
return static_cast<int>(reinterpret_cast<uintptr_t>(&monster));
});
LuaSetDocReadonlyProperty(monsterType, "typeId", "integer",
"Monster type ID matching monsters.MonsterID constants (readonly)",
[](const Monster &monster) {
return static_cast<int>(monster.type().type);
});
LuaSetDocReadonlyProperty(monsterType, "name", "string",
"Monster's display name (readonly)",
[](const Monster &monster) -> std::string_view {
return monster.name();
});
}

} // namespace
Expand Down
40 changes: 40 additions & 0 deletions Source/lua/modules/player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "items.h"
#include "lua/metadoc.hpp"
#include "player.h"
#include "sound_effect_enums.h"

namespace devilution {
namespace {
Expand Down Expand Up @@ -111,13 +112,52 @@ void InitPlayerUserType(sol::state_view &lua)
LuaSetDocReadonlyProperty(playerType, "maxMana", "number",
"Maximum mana (readonly)",
[](Player &player) { return player._pMaxMana >> 6; });
LuaSetDocReadonlyProperty(playerType, "isMyPlayer", "boolean",
"Whether this is the locally controlled player (readonly)",
[](const Player &player) { return &player == MyPlayer; });
LuaSetDocReadonlyProperty(playerType, "level", "integer",
"Current dungeon level the player is on (readonly)",
[](const Player &player) { return static_cast<int>(player.plrlevel); });
LuaSetDocFn(playerType, "isOnLevel", "(level: integer) -> boolean",
"Returns true if the player is on the given dungeon level",
[](const Player &player, int level) {
return player.isOnLevel(static_cast<uint8_t>(level));
});
LuaSetDocFn(playerType, "say", "(speechId: HeroSpeech) -> void",
"Makes the player character say the given speech line",
[](Player &player, HeroSpeech speechId) {
player.Say(speechId);
});
}

void RegisterHeroSpeechEnum(sol::state_view &lua)
{
lua.new_enum<HeroSpeech>("HeroSpeech",
{
{ "ICantUseThisYet", HeroSpeech::ICantUseThisYet },
{ "ThatWontWorkHere", HeroSpeech::ThatWontWorkHere },
{ "ThatWontWork", HeroSpeech::ThatWontWork },
{ "VengeanceIsMine", HeroSpeech::VengeanceIsMine },
{ "JustWhatIWasLookingFor", HeroSpeech::JustWhatIWasLookingFor },
{ "ICantCarryAnymore", HeroSpeech::ICantCarryAnymore },
{ "IHaveNoRoom", HeroSpeech::IHaveNoRoom },
{ "ItsTooBig", HeroSpeech::ItsTooBig },
{ "ItsTooHeavy", HeroSpeech::ItsTooHeavy },
{ "No", HeroSpeech::No },
{ "Yes", HeroSpeech::Yes },
{ "Die", HeroSpeech::Die },
{ "TimeToDie", HeroSpeech::TimeToDie },
{ "OhTooEasy", HeroSpeech::OhTooEasy },
});
}
} // namespace

sol::table LuaPlayerModule(sol::state_view &lua)
{
InitPlayerUserType(lua);
RegisterHeroSpeechEnum(lua);
sol::table table = lua.create_table();
table["HeroSpeech"] = lua["HeroSpeech"];
LuaSetDocFn(table, "self", "()",
"The current player",
[]() {
Expand Down
1 change: 1 addition & 0 deletions Source/monster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1499,6 +1499,7 @@ void ShrinkLeaderPacksize(const Monster &monster)
void MonsterDeath(Monster &monster)
{
monster.var1++;
LuaEvent("OnMonsterDeath", &monster, monster.var1);
if (monster.type().type == MT_DIABLO) {
if (monster.position.tile.x < ViewPosition.x) {
ViewPosition.x--;
Expand Down
45 changes: 45 additions & 0 deletions assets/lua/devilutionx/events.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,42 @@ local function CreateEvent()
}
end

---Creates a cancellable event. If any handler returns true the trigger returns true.
local function CreateCancellableEvent()
local functions = {}
return {
---@param func function
add = function(func)
table.insert(functions, func)
end,

---@param func function
remove = function(func)
for i, f in ipairs(functions) do
if f == func then
table.remove(functions, i)
break
end
end
end,

---Triggers the event. Returns true if any handler cancelled the default behaviour.
---@param ... any
---@return boolean
trigger = function(...)
local cancelled = false
local args = {...}
for _, func in ipairs(functions) do
if func(table.unpack(args)) == true then
cancelled = true
end
end
return cancelled
end,
__sig_trigger = "(...) -> boolean",
}
end

local events = {
---Called after all mods have been loaded.
LoadModsComplete = CreateEvent(),
Expand Down Expand Up @@ -84,6 +120,15 @@ local events = {
---Called when Player gains experience.
OnPlayerGainExperience = CreateEvent(),
__doc_OnPlayerGainExperience = "Called when Player gains experience.",

---Called each frame of a monster's death animation. Arguments: monster, deathFrame (integer).
OnMonsterDeath = CreateEvent(),
__doc_OnMonsterDeath = "Called each frame of a monster's death animation. Arguments: monster, deathFrame.",

---Called when a player is about to use an item. Arguments: player, item.
---If any handler returns true the default item-use behaviour is cancelled.
OnItemUse = CreateCancellableEvent(),
__doc_OnItemUse = "Called when a player uses an item. Return true to cancel default behaviour.",
}

---Registers a custom event type with the given name.
Expand Down
Loading