Browse Source

Parse Quest Data from TSV

pull/8139/head
Andrettin 7 months ago committed by GitHub
parent
commit
0c3fe1345c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CMake/Assets.cmake
  2. 1
      Source/diablo.cpp
  3. 10
      Source/levels/gendung.cpp
  4. 1
      Source/levels/gendung.h
  5. 1
      Source/lua/lua_global.cpp
  6. 14
      Source/monstdat.cpp
  7. 68
      Source/quests.cpp
  8. 6
      Source/quests.h
  9. 22
      Source/textdat.cpp
  10. 6
      Source/textdat.h
  11. 25
      assets/txtdata/quests/questdat.tsv
  12. 3
      test/drlg_l2_test.cpp
  13. 3
      test/drlg_test.hpp
  14. 9
      tools/extract_translation_data.py

1
CMake/Assets.cmake

@ -173,6 +173,7 @@ set(devilutionx_assets
txtdata/monsters/monstdat.tsv
txtdata/monsters/unique_monstdat.tsv
txtdata/objects/objdat.tsv
txtdata/quests/questdat.tsv
txtdata/sound/effects.tsv
txtdata/spells/spelldat.tsv
ui_art/diablo.pal

1
Source/diablo.cpp

@ -2675,6 +2675,7 @@ int DiabloMain(int argc, char **argv)
LoadMonsterData();
LoadItemData();
LoadObjectData();
LoadQuestData();
DiabloInit();
#ifdef __UWP__

10
Source/levels/gendung.cpp

@ -10,6 +10,7 @@
#include <ankerl/unordered_dense.h>
#include <expected.hpp>
#include <magic_enum/magic_enum.hpp>
#include "engine/clx_sprite.hpp"
#include "engine/load_file.hpp"
@ -844,4 +845,13 @@ tl::expected<dungeon_type, std::string> ParseDungeonType(std::string_view value)
return tl::make_unexpected("Unknown enum value");
}
tl::expected<_setlevels, std::string> ParseSetLevel(std::string_view value)
{
const std::optional<_setlevels> enumValueOpt = magic_enum::enum_cast<_setlevels>(value);
if (enumValueOpt.has_value()) {
return enumValueOpt.value();
}
return tl::make_unexpected("Unknown enum value");
}
} // namespace devilution

1
Source/levels/gendung.h

@ -59,6 +59,7 @@ inline bool IsArenaLevel(_setlevels setLevel)
}
tl::expected<dungeon_type, std::string> ParseDungeonType(std::string_view value);
tl::expected<_setlevels, std::string> ParseSetLevel(std::string_view value);
enum class DungeonFlag : uint8_t {
// clang-format off

1
Source/lua/lua_global.cpp

@ -236,6 +236,7 @@ void LuaReloadActiveMods()
LoadMonsterData();
LoadItemData();
LoadObjectData();
LoadQuestData();
LuaEvent("LoadModsComplete");
}

14
Source/monstdat.cpp

@ -448,19 +448,7 @@ void LoadUniqueMonstDatFromFile(DataFile &dataFile, std::string_view filename)
reader.read("monsterPack", monster.monsterPack, ParseUniqueMonsterPack);
reader.readInt("customToHit", monster.customToHit);
reader.readInt("customArmorClass", monster.customArmorClass);
// talkMessage
// TODO: Replace this hack with proper parsing once messages have been migrated to data files.
reader.read("talkMessage", monster.mtalkmsg, [](std::string_view value) -> tl::expected<_speech_id, std::string> {
if (value.empty()) return TEXT_NONE;
if (value == "TEXT_GARBUD1") return TEXT_GARBUD1;
if (value == "TEXT_ZHAR1") return TEXT_ZHAR1;
if (value == "TEXT_BANNER10") return TEXT_BANNER10;
if (value == "TEXT_VILE13") return TEXT_VILE13;
if (value == "TEXT_VEIL9") return TEXT_VEIL9;
if (value == "TEXT_WARLRD9") return TEXT_WARLRD9;
return tl::make_unexpected("Invalid value. NOTE: Parser is incomplete");
});
reader.read("talkMessage", monster.mtalkmsg, ParseSpeechId);
}
}

68
Source/quests.cpp

@ -12,6 +12,8 @@
#include "DiabloUI/ui_flags.hpp"
#include "control.h"
#include "cursor.h"
#include "data/file.hpp"
#include "data/record_reader.hpp"
#include "engine/load_file.hpp"
#include "engine/random.hpp"
#include "engine/render/clx_render.hpp"
@ -47,35 +49,7 @@ dungeon_type ReturnLevelType;
int ReturnLevel;
/** Contains the data related to each quest_id. */
QuestData QuestsData[] = {
// clang-format off
// _qdlvl, _qdmultlvl, _qlvlt, bookOrder, _qdrnd, _qslvl, isSinglePlayerOnly, _qdmsg, _qlstr
{ 5, -1, DTYPE_NONE, 5, 100, SL_NONE, true, TEXT_INFRA5, N_( /* TRANSLATORS: Quest Name Block */ "The Magic Rock") },
{ 9, -1, DTYPE_NONE, 10, 100, SL_NONE, true, TEXT_MUSH8, N_("Black Mushroom") },
{ 4, -1, DTYPE_NONE, 3, 100, SL_NONE, true, TEXT_GARBUD1, N_("Gharbad The Weak") },
{ 8, -1, DTYPE_NONE, 9, 100, SL_NONE, true, TEXT_ZHAR1, N_("Zhar the Mad") },
{ 14, -1, DTYPE_NONE, 21, 100, SL_NONE, true, TEXT_VEIL9, N_("Lachdanan") },
{ 15, -1, DTYPE_NONE, 23, 100, SL_NONE, false, TEXT_VILE3, N_("Diablo") },
{ 2, 2, DTYPE_NONE, 0, 100, SL_NONE, false, TEXT_BUTCH9, N_("The Butcher") },
{ 4, -1, DTYPE_NONE, 4, 100, SL_NONE, true, TEXT_BANNER2, N_("Ogden's Sign") },
{ 7, -1, DTYPE_NONE, 8, 100, SL_NONE, true, TEXT_BLINDING, N_("Halls of the Blind") },
{ 5, -1, DTYPE_NONE, 6, 100, SL_NONE, true, TEXT_BLOODY, N_("Valor") },
{ 10, -1, DTYPE_NONE, 11, 100, SL_NONE, true, TEXT_ANVIL5, N_("Anvil of Fury") },
{ 13, -1, DTYPE_NONE, 20, 100, SL_NONE, true, TEXT_BLOODWAR, N_("Warlord of Blood") },
{ 3, 3, DTYPE_CATHEDRAL, 2, 100, SL_SKELKING, false, TEXT_KING2, N_("The Curse of King Leoric") },
{ 2, -1, DTYPE_CAVES, 1, 100, SL_POISONWATER, true, TEXT_POISON3, N_("Poisoned Water Supply") },
{ 6, -1, DTYPE_CATACOMBS, 7, 100, SL_BONECHAMB, true, TEXT_BONER, N_("The Chamber of Bone") },
{ 15, 15, DTYPE_CATHEDRAL, 22, 100, SL_VILEBETRAYER, false, TEXT_VILE1, N_("Archbishop Lazarus") },
{ 17, 17, DTYPE_NONE, 17, 100, SL_NONE, false, TEXT_GRAVE7, N_("Grave Matters") },
{ 9, 9, DTYPE_NONE, 12, 100, SL_NONE, false, TEXT_FARMER1, N_("Farmer's Orchard") },
{ 17, -1, DTYPE_NONE, 14, 100, SL_NONE, true, TEXT_GIRL2, N_("Little Girl") },
{ 19, -1, DTYPE_NONE, 16, 100, SL_NONE, true, TEXT_TRADER, N_("Wandering Trader") },
{ 17, 17, DTYPE_NONE, 15, 100, SL_NONE, false, TEXT_DEFILER1, N_("The Defiler") },
{ 21, 21, DTYPE_NONE, 19, 100, SL_NONE, false, TEXT_NAKRUL1, N_("Na-Krul") },
{ 21, -1, DTYPE_NONE, 18, 100, SL_NONE, true, TEXT_CORNSTN, N_("Cornerstone of the World") },
{ 9, 9, DTYPE_NONE, 13, 100, SL_NONE, false, TEXT_JERSEY4, N_( /* TRANSLATORS: Quest Name Block end*/ "The Jersey's Jersey") },
// clang-format on
};
std::vector<QuestData> QuestsData;
namespace {
@ -948,4 +922,40 @@ bool Quest::IsAvailable() const
return true;
}
namespace {
void LoadQuestDatFromFile(DataFile &dataFile, std::string_view filename)
{
dataFile.skipHeaderOrDie(filename);
QuestsData.reserve(QuestsData.size() + dataFile.numRecords());
for (DataFileRecord record : dataFile) {
RecordReader reader { record, filename };
QuestData &quest = QuestsData.emplace_back();
reader.readInt("qdlvl", quest._qdlvl);
reader.readInt("qdmultlvl", quest._qdmultlvl);
reader.read("qlvlt", quest._qlvlt, ParseDungeonType);
reader.readInt("bookOrder", quest.questBookOrder);
reader.readInt("qdrnd", quest._qdrnd);
reader.read("qslvl", quest._qslvl, ParseSetLevel);
reader.readBool("isSinglePlayerOnly", quest.isSinglePlayerOnly);
reader.read("qdmsg", quest._qdmsg, ParseSpeechId);
reader.readString("qlstr", quest._qlstr);
}
}
} // namespace
void LoadQuestData()
{
const std::string_view filename = "txtdata\\quests\\questdat.tsv";
DataFile dataFile = DataFile::loadOrDie(filename);
QuestsData.clear();
LoadQuestDatFromFile(dataFile, filename);
QuestsData.shrink_to_fit();
}
} // namespace devilution

6
Source/quests.h

@ -103,7 +103,7 @@ struct QuestData {
_setlevels _qslvl;
bool isSinglePlayerOnly;
_speech_id _qdmsg;
const char *_qlstr;
std::string _qlstr;
};
extern bool QuestLogIsOpen;
@ -141,6 +141,8 @@ void SetMultiQuest(int q, quest_state s, bool log, int v1, int v2, int16_t qmsg)
bool UseMultiplayerQuests();
/* rdata */
extern QuestData QuestsData[];
extern std::vector<QuestData> QuestsData;
void LoadQuestData();
} // namespace devilution

22
Source/textdat.cpp

@ -4,8 +4,17 @@
* Implementation of all dialog texts.
*/
#include "textdat.h"
#include <magic_enum/magic_enum.hpp>
#include "utils/language.h"
template <>
struct magic_enum::customize::enum_range<devilution::_speech_id> {
static constexpr int min = devilution::TEXT_NONE;
static constexpr int max = devilution::NUM_TEXT_IDS;
};
namespace devilution {
/* todo: move text out of struct */
@ -919,4 +928,17 @@ const Speech Speeches[] = {
const size_t SpeechCount = sizeof(Speeches) / sizeof(Speech);
tl::expected<_speech_id, std::string> ParseSpeechId(std::string_view value)
{
if (value.empty()) {
return TEXT_NONE;
}
const std::optional<_speech_id> enumValueOpt = magic_enum::enum_cast<_speech_id>(value);
if (enumValueOpt.has_value()) {
return enumValueOpt.value();
}
return tl::make_unexpected("Invalid value.");
}
} // namespace devilution

6
Source/textdat.h

@ -7,6 +7,9 @@
#include <cstddef>
#include <cstdint>
#include <string>
#include <expected.hpp>
#include "sound_effect_enums.h"
@ -422,6 +425,7 @@ enum _speech_id : int16_t {
TEXT_GRISWOLD36,
TEXT_GRISWOLD37,
*/
NUM_TEXT_IDS,
TEXT_NONE = -1,
};
@ -434,4 +438,6 @@ struct Speech {
extern const size_t SpeechCount;
extern const Speech Speeches[];
tl::expected<_speech_id, std::string> ParseSpeechId(std::string_view value);
} // namespace devilution

25
assets/txtdata/quests/questdat.tsv

@ -0,0 +1,25 @@
qdlvl qdmultlvl qlvlt bookOrder qdrnd qslvl isSinglePlayerOnly qdmsg qlstr
5 -1 5 100 SL_NONE true TEXT_INFRA5 The Magic Rock
9 -1 10 100 SL_NONE true TEXT_MUSH8 Black Mushroom
4 -1 3 100 SL_NONE true TEXT_GARBUD1 Gharbad The Weak
8 -1 9 100 SL_NONE true TEXT_ZHAR1 Zhar the Mad
14 -1 21 100 SL_NONE true TEXT_VEIL9 Lachdanan
15 -1 23 100 SL_NONE false TEXT_VILE3 Diablo
2 2 0 100 SL_NONE false TEXT_BUTCH9 The Butcher
4 -1 4 100 SL_NONE true TEXT_BANNER2 Ogden's Sign
7 -1 8 100 SL_NONE true TEXT_BLINDING Halls of the Blind
5 -1 6 100 SL_NONE true TEXT_BLOODY Valor
10 -1 11 100 SL_NONE true TEXT_ANVIL5 Anvil of Fury
13 -1 20 100 SL_NONE true TEXT_BLOODWAR Warlord of Blood
3 3 DTYPE_CATHEDRAL 2 100 SL_SKELKING false TEXT_KING2 The Curse of King Leoric
2 -1 DTYPE_CAVES 1 100 SL_POISONWATER true TEXT_POISON3 Poisoned Water Supply
6 -1 DTYPE_CATACOMBS 7 100 SL_BONECHAMB true TEXT_BONER The Chamber of Bone
15 15 DTYPE_CATHEDRAL 22 100 SL_VILEBETRAYER false TEXT_VILE1 Archbishop Lazarus
17 17 17 100 SL_NONE false TEXT_GRAVE7 Grave Matters
9 9 12 100 SL_NONE false TEXT_FARMER1 Farmer's Orchard
17 -1 14 100 SL_NONE true TEXT_GIRL2 Little Girl
19 -1 16 100 SL_NONE true TEXT_TRADER Wandering Trader
17 17 15 100 SL_NONE false TEXT_DEFILER1 The Defiler
21 21 19 100 SL_NONE false TEXT_NAKRUL1 Na-Krul
21 -1 18 100 SL_NONE true TEXT_CORNSTN Cornerstone of the World
9 9 13 100 SL_NONE false TEXT_JERSEY4 The Jersey's Jersey
1 qdlvl qdmultlvl qlvlt bookOrder qdrnd qslvl isSinglePlayerOnly qdmsg qlstr
2 5 -1 5 100 SL_NONE true TEXT_INFRA5 The Magic Rock
3 9 -1 10 100 SL_NONE true TEXT_MUSH8 Black Mushroom
4 4 -1 3 100 SL_NONE true TEXT_GARBUD1 Gharbad The Weak
5 8 -1 9 100 SL_NONE true TEXT_ZHAR1 Zhar the Mad
6 14 -1 21 100 SL_NONE true TEXT_VEIL9 Lachdanan
7 15 -1 23 100 SL_NONE false TEXT_VILE3 Diablo
8 2 2 0 100 SL_NONE false TEXT_BUTCH9 The Butcher
9 4 -1 4 100 SL_NONE true TEXT_BANNER2 Ogden's Sign
10 7 -1 8 100 SL_NONE true TEXT_BLINDING Halls of the Blind
11 5 -1 6 100 SL_NONE true TEXT_BLOODY Valor
12 10 -1 11 100 SL_NONE true TEXT_ANVIL5 Anvil of Fury
13 13 -1 20 100 SL_NONE true TEXT_BLOODWAR Warlord of Blood
14 3 3 DTYPE_CATHEDRAL 2 100 SL_SKELKING false TEXT_KING2 The Curse of King Leoric
15 2 -1 DTYPE_CAVES 1 100 SL_POISONWATER true TEXT_POISON3 Poisoned Water Supply
16 6 -1 DTYPE_CATACOMBS 7 100 SL_BONECHAMB true TEXT_BONER The Chamber of Bone
17 15 15 DTYPE_CATHEDRAL 22 100 SL_VILEBETRAYER false TEXT_VILE1 Archbishop Lazarus
18 17 17 17 100 SL_NONE false TEXT_GRAVE7 Grave Matters
19 9 9 12 100 SL_NONE false TEXT_FARMER1 Farmer's Orchard
20 17 -1 14 100 SL_NONE true TEXT_GIRL2 Little Girl
21 19 -1 16 100 SL_NONE true TEXT_TRADER Wandering Trader
22 17 17 15 100 SL_NONE false TEXT_DEFILER1 The Defiler
23 21 21 19 100 SL_NONE false TEXT_NAKRUL1 Na-Krul
24 21 -1 18 100 SL_NONE true TEXT_CORNSTN Cornerstone of the World
25 9 9 13 100 SL_NONE false TEXT_JERSEY4 The Jersey's Jersey

3
test/drlg_l2_test.cpp

@ -12,6 +12,9 @@ TEST(Drlg_l2, CreateL2Dungeon_diablo_5_1677631846)
{
LoadExpectedLevelData("diablo/5-1677631846.dun");
LoadCoreArchives();
LoadQuestData();
InitQuests();
Quests[Q_BLOOD]._qactive = QUEST_NOTAVAIL;

3
test/drlg_test.hpp

@ -60,6 +60,9 @@ void TestInitGame(bool fullQuests = true, bool originalCathedral = true, bool he
sgGameInitInfo.fullQuests = fullQuests ? 1 : 0;
gbIsMultiplayer = !fullQuests;
LoadCoreArchives();
LoadQuestData();
UnloadModArchives();
if (hellfire) {
LoadModArchives({ { "Hellfire" } });

9
tools/extract_translation_data.py

@ -12,6 +12,7 @@ base_paths = {
"unique_itemdat": root.joinpath("assets/txtdata/items/unique_itemdat.tsv"),
"item_prefixes": root.joinpath("assets/txtdata/items/item_prefixes.tsv"),
"item_suffixes": root.joinpath("assets/txtdata/items/item_suffixes.tsv"),
"questdat": root.joinpath("assets/txtdata/quests/questdat.tsv"),
"spelldat": root.joinpath("assets/txtdata/spells/spelldat.tsv"),
}
@ -81,6 +82,14 @@ def process_files(paths, temp_source):
for i, row in enumerate(reader):
write_entry(temp_source, f'ITEM_SUFFIX_{i}_NAME', "default", row['name'], False)
# Quests
if "questdat" in paths:
with open(paths["questdat"], 'r') as tsv:
reader = csv.DictReader(tsv, delimiter='\t')
for i, row in enumerate(reader):
var_name = 'QUEST_' + row['qlstr'].upper().replace(' ', '_').replace('-', '_')
write_entry(temp_source, f'{var_name}_NAME', "default", row['qlstr'], False)
# Spells
with open(paths["spelldat"], 'r') as tsv:
reader = csv.DictReader(tsv, delimiter='\t')

Loading…
Cancel
Save