Browse Source

Added support for mods to add unique monsters without replacing any data (#8092)

pull/8101/head
Andrettin 7 months ago committed by GitHub
parent
commit
cfb52ee239
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      Source/CMakeLists.txt
  2. 84
      Source/loadsave.cpp
  3. 6
      Source/lua/lua_global.cpp
  4. 77
      Source/lua/modules/monsters.cpp
  5. 9
      Source/lua/modules/monsters.hpp
  6. 21
      Source/monstdat.cpp
  7. 6
      Source/monstdat.h
  8. 23
      Source/monsters/validation.cpp
  9. 3
      Source/monsters/validation.hpp
  10. 4
      assets/lua/devilutionx/events.lua

2
Source/CMakeLists.txt

@ -116,6 +116,7 @@ set(libdevilutionx_SRCS
lua/modules/i18n.cpp
lua/modules/items.cpp
lua/modules/log.cpp
lua/modules/monsters.cpp
lua/modules/player.cpp
lua/modules/render.cpp
lua/modules/towners.cpp
@ -525,6 +526,7 @@ add_devilutionx_object_library(libdevilutionx_monster
target_link_dependencies(libdevilutionx_monster
PUBLIC
DevilutionX::SDL
sol2::sol2
tl
libdevilutionx_game_mode
libdevilutionx_headless_mode

84
Source/loadsave.cpp

@ -32,6 +32,7 @@
#include "menu.h"
#include "missiles.h"
#include "monster.h"
#include "monsters/validation.hpp"
#include "mpq/mpq_common.hpp"
#include "pfile.h"
#include "playerdat.hpp"
@ -629,7 +630,7 @@ void LoadPlayer(LoadHelper &file, Player &player)
bool gbSkipSync = false;
void LoadMonster(LoadHelper *file, Monster &monster, MonsterConversionData *monsterConversionData = nullptr)
[[nodiscard]] bool LoadMonster(LoadHelper *file, Monster &monster, MonsterConversionData *monsterConversionData = nullptr)
{
monster.levelType = file->NextLE<int32_t>();
monster.mode = static_cast<MonsterMode>(file->NextLE<int32_t>());
@ -736,6 +737,54 @@ void LoadMonster(LoadHelper *file, Monster &monster, MonsterConversionData *mons
if (monster.mode == MonsterMode::Petrified)
monster.animInfo.isPetrified = true;
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);
if (!valid) {
LogWarn("Unique monster no longer valid, skipping it.");
return false;
}
}
return true;
}
void LoadMonsters(LoadHelper &file, ankerl::unordered_dense::set<unsigned> &removedMonsterIds, const bool applyLight, LevelConversionData *levelConversionData)
{
for (unsigned &monsterId : ActiveMonsters)
monsterId = file.NextBE<uint32_t>();
for (size_t i = 0; i < ActiveMonsterCount;) {
Monster &monster = Monsters[ActiveMonsters[i]];
MonsterConversionData *monsterConversionData = nullptr;
if (levelConversionData != nullptr)
monsterConversionData = &levelConversionData->monsterConversionData[ActiveMonsters[i]];
const bool valid = LoadMonster(&file, monster, monsterConversionData);
if (!valid) {
Monsters[ActiveMonsters[i]] = {};
removedMonsterIds.insert(ActiveMonsters[i]);
for (size_t j = i + 1; j < ActiveMonsterCount; j++) {
ActiveMonsters[j - 1] = ActiveMonsters[j];
}
--ActiveMonsterCount;
continue;
}
if (applyLight && monster.isUnique() && monster.lightId != NO_LIGHT)
Lights[monster.lightId].isInvalid = false;
i++;
}
for (const unsigned removedMonsterId : removedMonsterIds) {
for (size_t i = 0; i < ActiveMonsterCount; i++) {
Monster &activeMonster = Monsters[ActiveMonsters[i]];
if ((activeMonster.flags & MFLAG_TARGETS_MONSTER) != 0 && activeMonster.enemy == removedMonsterId) {
activeMonster.flags |= MFLAG_NO_ENEMY;
}
}
}
}
/**
@ -1944,18 +1993,11 @@ tl::expected<void, std::string> LoadLevel(LevelConversionData *levelConversionDa
auto savedItemCount = file.NextBE<uint32_t>();
ActiveObjectCount = file.NextBE<int32_t>();
ankerl::unordered_dense::set<unsigned> removedMonsterIds;
if (leveltype != DTYPE_TOWN) {
for (unsigned &monsterId : ActiveMonsters)
monsterId = file.NextBE<uint32_t>();
for (size_t i = 0; i < ActiveMonsterCount; i++) {
Monster &monster = Monsters[ActiveMonsters[i]];
MonsterConversionData *monsterConversionData = nullptr;
if (levelConversionData != nullptr)
monsterConversionData = &levelConversionData->monsterConversionData[ActiveMonsters[i]];
LoadMonster(&file, monster, monsterConversionData);
if (monster.isUnique() && monster.lightId != NO_LIGHT)
Lights[monster.lightId].isInvalid = false;
}
LoadMonsters(file, removedMonsterIds, true, levelConversionData);
if (!gbSkipSync) {
for (size_t i = 0; i < ActiveMonsterCount; i++)
RETURN_IF_ERROR(SyncMonsterAnim(Monsters[ActiveMonsters[i]]));
@ -1985,7 +2027,12 @@ tl::expected<void, std::string> LoadLevel(LevelConversionData *levelConversionDa
if (leveltype != DTYPE_TOWN) {
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
{
dMonster[i][j] = file.NextBE<int32_t>();
if (dMonster[i][j] > 0 && removedMonsterIds.contains(std::abs(dMonster[i][j]) - 1)) {
dMonster[i][j] = 0;
}
}
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
@ -2463,13 +2510,13 @@ tl::expected<void, std::string> LoadGame(bool firstflag)
for (int &monstkill : MonsterKillCounts)
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));
if (leveltype != DTYPE_TOWN) {
for (unsigned &monsterId : ActiveMonsters)
monsterId = file.NextBE<uint32_t>();
for (size_t i = 0; i < ActiveMonsterCount; i++)
LoadMonster(&file, Monsters[ActiveMonsters[i]]);
LoadMonsters(file, removedMonsterIds, false, nullptr);
for (size_t i = 0; i < ActiveMonsterCount; i++)
SyncPackSize(Monsters[ActiveMonsters[i]]);
// Skip ActiveMissiles
@ -2530,7 +2577,12 @@ tl::expected<void, std::string> LoadGame(bool firstflag)
if (leveltype != DTYPE_TOWN) {
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
{
dMonster[i][j] = file.NextBE<int32_t>();
if (dMonster[i][j] > 0 && removedMonsterIds.contains(std::abs(dMonster[i][j]) - 1)) {
dMonster[i][j] = 0;
}
}
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)

6
Source/lua/lua_global.cpp

@ -16,6 +16,7 @@
#include "lua/modules/i18n.hpp"
#include "lua/modules/items.hpp"
#include "lua/modules/log.hpp"
#include "lua/modules/monsters.hpp"
#include "lua/modules/player.hpp"
#include "lua/modules/render.hpp"
#include "lua/modules/towners.hpp"
@ -267,6 +268,7 @@ void LuaInitialize()
"devilutionx.items", LuaItemModule(lua),
"devilutionx.log", LuaLogModule(lua),
"devilutionx.audio", LuaAudioModule(lua),
"devilutionx.monsters", LuaMonstersModule(lua),
"devilutionx.player", LuaPlayerModule(lua),
"devilutionx.render", LuaRenderModule(lua),
"devilutionx.towners", LuaTownersModule(lua),
@ -295,6 +297,10 @@ void LuaShutdown()
void LuaEvent(std::string_view name)
{
if (!CurrentLuaState.has_value()) {
return;
}
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);

77
Source/lua/modules/monsters.cpp

@ -0,0 +1,77 @@
#include "lua/modules/monsters.hpp"
#include <string_view>
#include <fmt/format.h>
#include <sol/sol.hpp>
#include "lua/metadoc.hpp"
#include "monstdat.h"
#include "utils/language.h"
#include "utils/str_split.hpp"
namespace devilution {
namespace {
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;
const auto monsterTypeResult = ParseMonsterId(type);
if (!monsterTypeResult.has_value()) {
DisplayFatalErrorAndExit(_("Adding Unique Monster Failed"), fmt::format(fmt::runtime(_("Failed to parse monster type ID \"{}\": {}")), type, monsterTypeResult.error()));
}
monster.mtype = monsterTypeResult.value();
monster.mName = name;
monster.mTrnName = trn;
monster.mlevel = level;
monster.mmaxhp = maxHp;
const auto monsterAiResult = ParseAiId(ai);
if (!monsterAiResult.has_value()) {
DisplayFatalErrorAndExit(_("Adding Unique Monster Failed"), fmt::format(fmt::runtime(_("Failed to parse monster AI ID \"{}\": {}")), ai, monsterAiResult.error()));
}
monster.mAi = monsterAiResult.value();
monster.mint = intelligence;
monster.mMinDamage = minDamage;
monster.mMaxDamage = maxDamage;
monster.mMagicRes = {};
if (!resistance.empty()) {
for (const std::string_view resistancePart : SplitByChar(resistance, ',')) {
const auto monsterResistanceResult = ParseMonsterResistance(resistancePart);
if (!monsterResistanceResult.has_value()) {
DisplayFatalErrorAndExit(_("Adding Unique Monster Failed"), fmt::format(fmt::runtime(_("Failed to parse monster resistance \"{}\": {}")), resistance, monsterResistanceResult.error()));
}
monster.mMagicRes |= monsterResistanceResult.value();
}
}
const auto monsterPackResult = ParseUniqueMonsterPack(monsterPack);
if (!monsterPackResult.has_value()) {
DisplayFatalErrorAndExit(_("Adding Unique Monster Failed"), fmt::format(fmt::runtime(_("Failed to parse unique monster pack \"{}\": {}")), monsterPack, monsterPackResult.error()));
}
monster.monsterPack = monsterPackResult.value();
monster.customToHit = customToHit.value_or(0);
monster.customArmorClass = customArmorClass.value_or(0);
monster.mtalkmsg = TEXT_NONE;
UniqueMonstersData.push_back(std::move(monster));
}
} // namespace
sol::table LuaMonstersModule(sol::state_view &lua)
{
sol::table table = lua.create_table();
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;
}
} // namespace devilution

9
Source/lua/modules/monsters.hpp

@ -0,0 +1,9 @@
#pragma once
#include <sol/sol.hpp>
namespace devilution {
sol::table LuaMonstersModule(sol::state_view &lua);
} // namespace devilution

21
Source/monstdat.cpp

@ -8,12 +8,12 @@
#include <cstdint>
#include <ankerl/unordered_dense.h>
#include <expected.hpp>
#include "cursor.h"
#include "data/file.hpp"
#include "data/record_reader.hpp"
#include "items.h"
#include "lua/lua_global.hpp"
#include "monster.h"
#include "textdat.h"
#include "utils/language.h"
@ -206,8 +206,6 @@ const _monster_id MonstConvTbl[] = {
MT_LRDSAYTR,
};
namespace {
tl::expected<_monster_id, std::string> ParseMonsterId(std::string_view value)
{
if (value == "MT_NZOMBIE") return MT_NZOMBIE;
@ -370,6 +368,8 @@ tl::expected<_monster_id, std::string> ParseMonsterId(std::string_view value)
return tl::make_unexpected("Unknown enum value");
}
namespace {
tl::expected<MonsterAvailability, std::string> ParseMonsterAvailability(std::string_view value)
{
if (value == "Always") return MonsterAvailability::Always;
@ -378,6 +378,8 @@ tl::expected<MonsterAvailability, std::string> ParseMonsterAvailability(std::str
return tl::make_unexpected("Expected one of: Always, Never, or Retail");
}
} // namespace
tl::expected<MonsterAIID, std::string> ParseAiId(std::string_view value)
{
if (value == "Zombie") return MonsterAIID::Zombie;
@ -423,6 +425,8 @@ tl::expected<MonsterAIID, std::string> ParseAiId(std::string_view value)
return tl::make_unexpected("Unknown enum value");
}
namespace {
tl::expected<monster_flag, std::string> ParseMonsterFlag(std::string_view value)
{
if (value == "HIDDEN") return MFLAG_HIDDEN;
@ -448,6 +452,8 @@ tl::expected<MonsterClass, std::string> ParseMonsterClass(std::string_view value
return tl::make_unexpected("Unknown enum value");
}
} // namespace
tl::expected<monster_resistance, std::string> ParseMonsterResistance(std::string_view value)
{
if (value == "RESIST_MAGIC") return RESIST_MAGIC;
@ -460,6 +466,8 @@ tl::expected<monster_resistance, std::string> ParseMonsterResistance(std::string
return tl::make_unexpected("Unknown enum value");
}
namespace {
tl::expected<SelectionRegion, std::string> ParseSelectionRegion(std::string_view value)
{
if (value.empty()) return SelectionRegion::None;
@ -469,6 +477,8 @@ tl::expected<SelectionRegion, std::string> ParseSelectionRegion(std::string_view
return tl::make_unexpected("Unknown enum value");
}
} // namespace
tl::expected<UniqueMonsterPack, std::string> ParseUniqueMonsterPack(std::string_view value)
{
if (value == "None") return UniqueMonsterPack::None;
@ -477,6 +487,8 @@ tl::expected<UniqueMonsterPack, std::string> ParseUniqueMonsterPack(std::string_
return tl::make_unexpected("Unknown enum value");
}
namespace {
void LoadMonstDat()
{
const std::string_view filename = "txtdata\\monsters\\monstdat.tsv";
@ -583,7 +595,10 @@ void LoadUniqueMonstDat()
return tl::make_unexpected("Invalid value. NOTE: Parser is incomplete");
});
}
UniqueMonstersData.shrink_to_fit();
LuaEvent("UniqueMonsterDataLoaded");
}
} // namespace

6
Source/monstdat.h

@ -10,6 +10,8 @@
#include <string>
#include <vector>
#include <expected.hpp>
#include "cursor.h"
#include "textdat.h"
@ -334,6 +336,10 @@ extern std::vector<MonsterData> MonstersData;
extern const _monster_id MonstConvTbl[];
extern std::vector<UniqueMonsterData> UniqueMonstersData;
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 LoadMonsterData();
/**

23
Source/monsters/validation.cpp

@ -39,4 +39,27 @@ bool IsEnemyValid(size_t monsterId, size_t enemyId)
return IsEnemyValid(enemyId, true);
}
bool IsUniqueMonsterValid(const Monster &monster)
{
assert(monster.isUnique());
const size_t uniqueMonsterIndex = static_cast<size_t>(monster.uniqueType);
if (uniqueMonsterIndex >= UniqueMonstersData.size()) {
return false;
}
const CMonster &monsterType = LevelMonsterTypes[monster.levelType];
const _monster_id monsterId = monsterType.type;
const UniqueMonsterData &uniqueMonsterData = UniqueMonstersData.at(uniqueMonsterIndex);
if (monsterId != uniqueMonsterData.mtype) {
return false;
}
if (uniqueMonsterData.mlevel != 0 && uniqueMonsterData.mlevel != currlevel) {
return false;
}
return true;
}
} // namespace devilution

3
Source/monsters/validation.hpp

@ -9,7 +9,10 @@
namespace devilution {
struct Monster;
bool IsEnemyIdValid(size_t enemyId);
bool IsEnemyValid(size_t monsterId, size_t enemyId);
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 unique monster data TSV file has been loaded.
UniqueMonsterDataLoaded = CreateEvent(),
__doc_UniqueMonsterDataLoaded = "Called after the unique monster data TSV file has been loaded.",
---Called every time a new game is started.
GameStart = CreateEvent(),
__doc_GameStart = "Called every time a new game is started.",

Loading…
Cancel
Save