Browse Source

Added Lua Bindings for Adding New Monster Types

pull/8122/head
Andrettin 7 months ago committed by GitHub
parent
commit
be956f0608
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      Source/CMakeLists.txt
  2. 16
      Source/loadsave.cpp
  3. 4
      Source/lua/modules/dev/monsters.cpp
  4. 8
      Source/lua/modules/monsters.cpp
  5. 103
      Source/monstdat.cpp
  6. 68
      Source/monstdat.h
  7. 6
      Source/monster.cpp
  8. 2
      Source/monster.h
  9. 17
      Source/monsters/validation.cpp
  10. 1
      Source/monsters/validation.hpp
  11. 4
      assets/lua/devilutionx/events.lua

2
Source/CMakeLists.txt

@ -455,6 +455,7 @@ target_link_dependencies(libdevilutionx_lighting PUBLIC
DevilutionX::SDL
fmt::fmt
tl
unordered_dense::unordered_dense
libdevilutionx_vision
)
@ -529,6 +530,7 @@ target_link_dependencies(libdevilutionx_monster
magic_enum::magic_enum
sol2::sol2
tl
unordered_dense::unordered_dense
libdevilutionx_game_mode
libdevilutionx_headless_mode
libdevilutionx_sound

16
Source/loadsave.cpp

@ -740,9 +740,9 @@ bool gbSkipSync = false;
if (monster.isUnique()) {
// check if the unique monster is still valid (it could no longer be valid e.g. because the loaded mods changed and the unique monsters changed as a consequence)
const bool valid = IsUniqueMonsterValid(monster);
const bool valid = IsMonsterValid(monster);
if (!valid) {
LogWarn("Unique monster no longer valid, skipping it.");
LogWarn("Monster no longer valid, skipping it.");
return false;
}
}
@ -2507,13 +2507,15 @@ tl::expected<void, std::string> LoadGame(bool firstflag)
ActiveMonsterCount = tmpNummonsters;
ActiveObjectCount = tmpNobjects;
for (int &monstkill : MonsterKillCounts)
for (size_t i = 0; i < MonstersData.size(); ++i) {
int &monstkill = MonsterKillCounts[i];
monstkill = file.NextBE<int32_t>();
}
ankerl::unordered_dense::set<unsigned> removedMonsterIds;
// skip ahead for vanilla save compatibility (Related to bugfix where MonsterKillCounts[MaxMonsters] was changed to MonsterKillCounts[NUM_MTYPES]
file.Skip(4 * (MaxMonsters - NUM_MTYPES));
file.Skip(4 * (MaxMonsters - MonstersData.size()));
if (leveltype != DTYPE_TOWN) {
LoadMonsters(file, removedMonsterIds, false, nullptr);
@ -2776,10 +2778,12 @@ void SaveGameData(SaveWriter &saveWriter)
SaveQuest(&file, i);
for (int i = 0; i < MAXPORTAL; i++)
SavePortal(&file, i);
for (const int monstkill : MonsterKillCounts)
for (size_t i = 0; i < MonstersData.size(); ++i) {
const int monstkill = MonsterKillCounts[i];
file.WriteBE<int32_t>(monstkill);
}
// add padding for vanilla save compatibility (Related to bugfix where MonsterKillCounts[MaxMonsters] was changed to MonsterKillCounts[NUM_MTYPES]
file.Skip(4 * (MaxMonsters - NUM_MTYPES));
file.Skip(4 * (MaxMonsters - MonstersData.size()));
if (leveltype != DTYPE_TOWN) {
for (const unsigned monsterId : ActiveMonsters)

4
Source/lua/modules/dev/monsters.cpp

@ -107,12 +107,12 @@ std::string DebugCmdSpawnMonster(std::string name, std::optional<unsigned> count
int mtype = -1;
for (int i = 0; i < NUM_MTYPES; i++) {
for (size_t i = 0; i < MonstersData.size(); i++) {
const auto &mondata = MonstersData[i];
const std::string monsterName = AsciiStrToLower(std::string_view(mondata.name));
if (monsterName.find(name) == std::string::npos)
continue;
mtype = i;
mtype = static_cast<int>(i);
if (monsterName == name) // to support partial name matching but always choose the correct monster if full name is given
break;
}

8
Source/lua/modules/monsters.cpp

@ -5,6 +5,7 @@
#include <fmt/format.h>
#include <sol/sol.hpp>
#include "data/file.hpp"
#include "lua/metadoc.hpp"
#include "monstdat.h"
#include "utils/language.h"
@ -14,6 +15,12 @@ namespace devilution {
namespace {
void AddMonsterDataFromTsv(const std::string_view path)
{
DataFile dataFile = DataFile::loadOrDie(path);
LoadMonstDatFromFile(dataFile, path);
}
void AddUniqueMonsterData(const std::string_view type, const std::string_view name, const std::string_view trn, const uint8_t level, const uint16_t maxHp, const std::string_view ai, const uint8_t intelligence, const uint8_t minDamage, const uint8_t maxDamage, const std::string_view resistance, const std::string_view monsterPack, const std::optional<uint8_t> customToHit, const std::optional<uint8_t> customArmorClass)
{
UniqueMonsterData monster;
@ -70,6 +77,7 @@ void AddUniqueMonsterData(const std::string_view type, const std::string_view na
sol::table LuaMonstersModule(sol::state_view &lua)
{
sol::table table = lua.create_table();
LuaSetDocFn(table, "addMonsterDataFromTsv", "(path: string)", AddMonsterDataFromTsv);
LuaSetDocFn(table, "addUniqueMonsterData", "(type: string, name: string, trn: string, level: number, maxHp: number, ai: string, intelligence: number, minDamage: number, maxDamage: number, resistance: string, monsterPack: string, customToHit: number = nil, customArmorClass: number = nil)", AddUniqueMonsterData);
return table;
}

103
Source/monstdat.cpp

@ -8,6 +8,7 @@
#include <cstdint>
#include <ankerl/unordered_dense.h>
#include <fmt/format.h>
#include <magic_enum/magic_enum.hpp>
#include "cursor.h"
@ -22,7 +23,7 @@
template <>
struct magic_enum::customize::enum_range<devilution::_monster_id> {
static constexpr int min = devilution::MT_INVALID;
static constexpr int max = devilution::NUM_MTYPES;
static constexpr int max = devilution::NUM_DEFAULT_MTYPES;
};
namespace devilution {
@ -213,17 +214,37 @@ const _monster_id MonstConvTbl[] = {
MT_LRDSAYTR,
};
namespace {
/** Contains the mapping between monster ID strings and indices, used for parsing additional monster data. */
ankerl::unordered_dense::map<std::string, int16_t> AdditionalMonsterIdStringsToIndices;
} // namespace
tl::expected<_monster_id, std::string> ParseMonsterId(std::string_view value)
{
const std::optional<_monster_id> enumValueOpt = magic_enum::enum_cast<_monster_id>(value);
if (enumValueOpt.has_value()) {
return enumValueOpt.value();
}
const auto findIt = AdditionalMonsterIdStringsToIndices.find(std::string(value));
if (findIt != AdditionalMonsterIdStringsToIndices.end()) {
return static_cast<_monster_id>(findIt->second);
}
return tl::make_unexpected("Unknown enum value");
}
namespace {
tl::expected<_monster_id, std::string> ParseMonsterIdIfNotEmpty(std::string_view value)
{
if (value.empty()) {
return MT_INVALID;
}
return ParseMonsterId(value);
}
tl::expected<MonsterAvailability, std::string> ParseMonsterAvailability(std::string_view value)
{
if (value == "Always") return MonsterAvailability::Always;
@ -331,6 +352,17 @@ tl::expected<SelectionRegion, std::string> ParseSelectionRegion(std::string_view
return tl::make_unexpected("Unknown enum value");
}
tl::expected<uint16_t, std::string> ParseMonsterTreasure(std::string_view value)
{
// TODO: Replace this hack with proper parsing.
if (value.empty()) return 0;
if (value == "None") return T_NODROP;
if (value == "Uniq(SKCROWN)") return Uniq(UITEM_SKCROWN);
if (value == "Uniq(CLEAVER)") return Uniq(UITEM_CLEAVER);
return tl::make_unexpected("Invalid value. NOTE: Parser is incomplete");
}
} // namespace
tl::expected<UniqueMonsterPack, std::string> ParseUniqueMonsterPack(std::string_view value)
@ -341,29 +373,45 @@ tl::expected<UniqueMonsterPack, std::string> ParseUniqueMonsterPack(std::string_
return tl::make_unexpected("Unknown enum value");
}
namespace {
void LoadMonstDat()
void LoadMonstDatFromFile(DataFile &dataFile, const std::string_view filename)
{
const std::string_view filename = "txtdata\\monsters\\monstdat.tsv";
DataFile dataFile = DataFile::loadOrDie(filename);
dataFile.skipHeaderOrDie(filename);
MonstersData.clear();
MonstersData.reserve(dataFile.numRecords());
ankerl::unordered_dense::map<std::string, size_t> spritePathToId;
MonstersData.reserve(MonstersData.size() + dataFile.numRecords());
for (DataFileRecord record : dataFile) {
if (MonstersData.size() >= static_cast<size_t>(NUM_MAX_MTYPES)) {
DisplayFatalErrorAndExit(_("Loading Monster Data Failed"), fmt::format(fmt::runtime(_("Could not add a monster, since the maximum monster type number of {} has already been reached.")), static_cast<size_t>(NUM_MAX_MTYPES)));
}
RecordReader reader { record, filename };
MonsterData &monster = MonstersData.emplace_back();
reader.advance(); // Skip the first column (monster ID).
std::string monsterId;
reader.readString("_monster_id", monsterId);
const std::optional<_monster_id> monsterIdEnumValueOpt = magic_enum::enum_cast<_monster_id>(monsterId);
if (!monsterIdEnumValueOpt.has_value()) {
const size_t monsterIndex = MonstersData.size();
const auto [it, inserted] = AdditionalMonsterIdStringsToIndices.emplace(monsterId, static_cast<int16_t>(monsterIndex));
if (!inserted) {
DisplayFatalErrorAndExit(_("Loading Monster Data Failed"), fmt::format(fmt::runtime(_("A monster type already exists for ID \"{}\".")), monsterId));
}
}
// for hardcoded monsters, use their predetermined slot; for non-hardcoded ones, use the slots after that
MonsterData &monster = monsterIdEnumValueOpt.has_value() ? MonstersData[monsterIdEnumValueOpt.value()] : MonstersData.emplace_back();
reader.readString("name", monster.name);
{
std::string assetsSuffix;
reader.readString("assetsSuffix", assetsSuffix);
const auto [it, inserted] = spritePathToId.emplace(assetsSuffix, spritePathToId.size());
if (inserted)
MonsterSpritePaths.push_back(it->first);
monster.spriteId = static_cast<uint16_t>(it->second);
const auto findIt = std::find(MonsterSpritePaths.begin(), MonsterSpritePaths.end(), assetsSuffix);
if (findIt != MonsterSpritePaths.end()) {
monster.spriteId = static_cast<uint16_t>(findIt - MonsterSpritePaths.begin());
} else {
monster.spriteId = static_cast<uint16_t>(MonsterSpritePaths.size());
MonsterSpritePaths.push_back(std::string(assetsSuffix));
}
}
reader.readString("soundSuffix", monster.soundSuffix);
reader.readString("trnFile", monster.trnFile);
@ -395,22 +443,27 @@ void LoadMonstDat()
reader.readEnumList("resistance", monster.resistance, ParseMonsterResistance);
reader.readEnumList("resistanceHell", monster.resistanceHell, ParseMonsterResistance);
reader.readEnumList("selectionRegion", monster.selectionRegion, ParseSelectionRegion);
// treasure
// TODO: Replace this hack with proper parsing once items have been migrated to data files.
reader.read("treasure", monster.treasure, [](std::string_view value) -> tl::expected<uint16_t, std::string> {
if (value.empty()) return 0;
if (value == "None") return T_NODROP;
if (value == "Uniq(SKCROWN)") return Uniq(UITEM_SKCROWN);
if (value == "Uniq(CLEAVER)") return Uniq(UITEM_CLEAVER);
return tl::make_unexpected("Invalid value. NOTE: Parser is incomplete");
});
reader.read("treasure", monster.treasure, ParseMonsterTreasure);
reader.readInt("exp", monster.exp);
}
MonstersData.shrink_to_fit();
}
namespace {
void LoadMonstDat()
{
const std::string_view filename = "txtdata\\monsters\\monstdat.tsv";
DataFile dataFile = DataFile::loadOrDie(filename);
MonstersData.clear();
AdditionalMonsterIdStringsToIndices.clear();
MonstersData.resize(NUM_DEFAULT_MTYPES); // ensure the hardcoded monster type slots are filled
LoadMonstDatFromFile(dataFile, filename);
LuaEvent("MonsterDataLoaded");
}
void LoadUniqueMonstDat()
{
const std::string_view filename = "txtdata\\monsters\\unique_monstdat.tsv";

68
Source/monstdat.h

@ -17,6 +17,8 @@
namespace devilution {
class DataFile;
enum class MonsterAIID : int8_t {
Zombie,
Fat,
@ -97,44 +99,44 @@ struct MonsterData {
std::string name;
std::string soundSuffix;
std::string trnFile;
uint16_t spriteId;
MonsterAvailability availability;
uint16_t width;
uint16_t image;
bool hasSpecial;
bool hasSpecialSound;
int8_t frames[6];
int8_t rate[6];
int8_t minDunLvl;
int8_t maxDunLvl;
int8_t level;
uint16_t hitPointsMinimum;
uint16_t hitPointsMaximum;
MonsterAIID ai;
uint16_t spriteId = 0;
MonsterAvailability availability = MonsterAvailability::Never;
uint16_t width = 0;
uint16_t image = 0;
bool hasSpecial = false;
bool hasSpecialSound = false;
int8_t frames[6] {};
int8_t rate[6] {};
int8_t minDunLvl = 0;
int8_t maxDunLvl = 0;
int8_t level = 0;
uint16_t hitPointsMinimum = 0;
uint16_t hitPointsMaximum = 0;
MonsterAIID ai = MonsterAIID::Invalid;
/**
* @brief Denotes monster's abilities defined in @p monster_flag as bitflags
* For usage, see @p MonstersData in monstdat.cpp
*/
uint16_t abilityFlags;
uint8_t intelligence;
uint8_t toHit;
int8_t animFrameNum;
uint8_t minDamage;
uint8_t maxDamage;
uint8_t toHitSpecial;
int8_t animFrameNumSpecial;
uint8_t minDamageSpecial;
uint8_t maxDamageSpecial;
uint8_t armorClass;
MonsterClass monsterClass;
uint16_t abilityFlags = 0;
uint8_t intelligence = 0;
uint8_t toHit = 0;
int8_t animFrameNum = 0;
uint8_t minDamage = 0;
uint8_t maxDamage = 0;
uint8_t toHitSpecial = 0;
int8_t animFrameNumSpecial = 0;
uint8_t minDamageSpecial = 0;
uint8_t maxDamageSpecial = 0;
uint8_t armorClass = 0;
MonsterClass monsterClass {};
/** Using monster_resistance as bitflags */
uint8_t resistance;
uint8_t resistance = 0;
/** Using monster_resistance as bitflags */
uint8_t resistanceHell;
SelectionRegion selectionRegion;
uint8_t resistanceHell = 0;
SelectionRegion selectionRegion = SelectionRegion::None;
/** Using monster_treasure */
uint16_t treasure;
uint16_t exp;
uint16_t treasure = 0;
uint16_t exp = 0;
[[nodiscard]] const char *spritePath() const;
@ -288,7 +290,8 @@ enum _monster_id : int16_t {
MT_FLESTHNG,
MT_REAPER,
MT_NAKRUL,
NUM_MTYPES,
NUM_DEFAULT_MTYPES,
NUM_MAX_MTYPES = 200, // same as MaxMonsters, for the sake of save game compability
MT_INVALID = -1,
};
@ -340,6 +343,7 @@ tl::expected<_monster_id, std::string> ParseMonsterId(std::string_view value);
tl::expected<MonsterAIID, std::string> ParseAiId(std::string_view value);
tl::expected<monster_resistance, std::string> ParseMonsterResistance(std::string_view value);
tl::expected<UniqueMonsterPack, std::string> ParseUniqueMonsterPack(std::string_view value);
void LoadMonstDatFromFile(DataFile &dataFile, std::string_view filename);
void LoadMonsterData();
/**

6
Source/monster.cpp

@ -69,7 +69,7 @@ Monster Monsters[MaxMonsters];
unsigned ActiveMonsters[MaxMonsters];
size_t ActiveMonsterCount;
/** Tracks the total number of monsters killed per monster_id. */
int MonsterKillCounts[NUM_MTYPES];
int MonsterKillCounts[NUM_MAX_MTYPES];
bool sgbSaveSoundOn;
namespace {
@ -3336,7 +3336,7 @@ tl::expected<void, std::string> GetLevelMTypes()
RETURN_IF_ERROR(AddMonsterType(MT_SKING, PLACE_UNIQUE));
int skeletonTypeCount = 0;
_monster_id skeltypes[NUM_MTYPES];
_monster_id skeltypes[NUM_MAX_MTYPES];
for (const _monster_id skeletonType : SkeletonTypes) {
if (!IsMonsterAvailable(MonstersData[skeletonType]))
continue;
@ -3581,7 +3581,7 @@ tl::expected<void, std::string> InitMonsters()
numplacemonsters = MaxMonsters - 10 - ActiveMonsterCount;
totalmonsters = ActiveMonsterCount + numplacemonsters;
int numscattypes = 0;
size_t scattertypes[NUM_MTYPES];
size_t scattertypes[NUM_MAX_MTYPES];
for (size_t i = 0; i < LevelMonsterTypeCount; i++) {
if ((LevelMonsterTypes[i].placeFlags & PLACE_SCATTER) != 0) {
scattertypes[numscattypes] = i;

2
Source/monster.h

@ -482,7 +482,7 @@ extern size_t LevelMonsterTypeCount;
extern Monster Monsters[MaxMonsters];
extern unsigned ActiveMonsters[MaxMonsters];
extern size_t ActiveMonsterCount;
extern int MonsterKillCounts[NUM_MTYPES];
extern int MonsterKillCounts[NUM_MAX_MTYPES];
extern bool sgbSaveSoundOn;
tl::expected<void, std::string> PrepareUniqueMonst(Monster &monster, UniqueMonsterType monsterType, size_t miniontype, int bosspacksize, const UniqueMonsterData &uniqueMonsterData);

17
Source/monsters/validation.cpp

@ -39,6 +39,23 @@ bool IsEnemyValid(size_t monsterId, size_t enemyId)
return IsEnemyValid(enemyId, true);
}
bool IsMonsterValid(const Monster &monster)
{
const CMonster &monsterType = LevelMonsterTypes[monster.levelType];
const _monster_id monsterId = monsterType.type;
const size_t monsterIndex = static_cast<size_t>(monsterId);
if (monsterIndex >= MonstersData.size()) {
return false;
}
if (monster.isUnique() && !IsUniqueMonsterValid(monster)) {
return false;
}
return true;
}
bool IsUniqueMonsterValid(const Monster &monster)
{
assert(monster.isUnique());

1
Source/monsters/validation.hpp

@ -13,6 +13,7 @@ struct Monster;
bool IsEnemyIdValid(size_t enemyId);
bool IsEnemyValid(size_t monsterId, size_t enemyId);
bool IsMonsterValid(const Monster &monster);
bool IsUniqueMonsterValid(const Monster &monster);
} // namespace devilution

4
assets/lua/devilutionx/events.lua

@ -44,6 +44,10 @@ local events = {
LoadModsComplete = CreateEvent(),
__doc_LoadModsComplete = "Called after all mods have been loaded.",
---Called after the monster data TSV file has been loaded.
MonsterDataLoaded = CreateEvent(),
__doc_MonsterDataLoaded = "Called after the monster data TSV file has been loaded.",
---Called after the unique monster data TSV file has been loaded.
UniqueMonsterDataLoaded = CreateEvent(),
__doc_UniqueMonsterDataLoaded = "Called after the unique monster data TSV file has been loaded.",

Loading…
Cancel
Save