瀏覽代碼

Converted Reading Spellbook Database to YAML (#4440)

* Split database between pre-renewal and renewal.
Co-authored-by: Lemongrass3110 <lemongrass@kstp.at>
Aleos 5 年之前
父節點
當前提交
77d86c6da5

+ 0 - 9
db/import-tmpl/spellbook_db.txt

@@ -1,9 +0,0 @@
-// Reading Spellbook Preserve Points Database
-//
-// Structure of Database:
-// SkillID,PreservePoints,Required Book
-//
-// NOTE:
-// - To add more entries, increase MAX_SKILL_SPELLBOOK_DB in skill.h.
-// - To remove entry by importing, put 0 for 'PreservePoints'
-

+ 32 - 0
db/import-tmpl/spellbook_db.yml

@@ -0,0 +1,32 @@
+# This file is a part of rAthena.
+#   Copyright(C) 2019 rAthena Development Team
+#   https://rathena.org - https://github.com/rathena
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+###########################################################################
+# Reading Spellbook Database
+###########################################################################
+#
+# Reading Spellbook Settings
+#
+###########################################################################
+# - Skill             Skill that is usable through a Spellbook.
+#   Book              Book item required to cast skill.
+#   PreservePoints    Amount of points required to preserve the skill into the book.
+###########################################################################
+
+Header:
+  Type: READING_SPELLBOOK_DB
+  Version: 1

+ 85 - 0
db/re/spellbook_db.yml

@@ -0,0 +1,85 @@
+# This file is a part of rAthena.
+#   Copyright(C) 2019 rAthena Development Team
+#   https://rathena.org - https://github.com/rathena
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+###########################################################################
+# Reading Spellbook Database
+###########################################################################
+#
+# Reading Spellbook Settings
+#
+###########################################################################
+# - Skill             Skill that is usable through a Spellbook.
+#   Book              Book item required to cast skill.
+#   PreservePoints    Amount of points required to preserve the skill into the book.
+###########################################################################
+
+Header:
+  Type: READING_SPELLBOOK_DB
+  Version: 1
+
+Body:
+  - Skill: MG_COLDBOLT
+    Book: Magic_Book_CB
+    PreservePoints: 7
+  - Skill: MG_FIREBOLT
+    Book: Magic_Book_FB
+    PreservePoints: 7
+  - Skill: MG_LIGHTNINGBOLT
+    Book: Magic_Book_LB
+    PreservePoints: 7
+  - Skill: MG_THUNDERSTORM
+    Book: Magic_Book_TS
+    PreservePoints: 9
+  - Skill: WZ_METEOR
+    Book: Magic_Book_MS
+    PreservePoints: 10
+  - Skill: WZ_JUPITEL
+    Book: Magic_Book_JT
+    PreservePoints: 9
+  - Skill: WZ_VERMILION
+    Book: Magic_Book_LOV
+    PreservePoints: 10
+  - Skill: WZ_WATERBALL
+    Book: Magic_Book_WB
+    PreservePoints: 9
+  - Skill: WZ_STORMGUST
+    Book: Magic_Book_SG
+    PreservePoints: 10
+  - Skill: WZ_EARTHSPIKE
+    Book: Magic_Book_ES
+    PreservePoints: 8
+  - Skill: WZ_HEAVENDRIVE
+    Book: Magic_Book_HD
+    PreservePoints: 9
+  - Skill: WL_DRAINLIFE
+    Book: Magic_Book_DL
+    PreservePoints: 8
+  - Skill: WL_CRIMSONROCK
+    Book: Magic_Book_CR
+    PreservePoints: 12
+  - Skill: WL_COMET
+    Book: Magic_Book_CM
+    PreservePoints: 22
+  - Skill: WL_CHAINLIGHTNING
+    Book: Magic_Book_CL
+    PreservePoints: 12
+  - Skill: WL_EARTHSTRAIN
+    Book: Magic_Book_ES_
+    PreservePoints: 12
+  - Skill: WL_TETRAVORTEX
+    Book: Magic_Book_TV
+    PreservePoints: 22

+ 0 - 31
db/spellbook_db.txt

@@ -1,31 +0,0 @@
-// Reading Spellbook Preserve Points Database
-//
-// Structure of Database:
-// SkillID,PreservePoints,Required Book
-//
-// NOTE:
-// - To add more entries, increase MAX_SKILL_SPELLBOOK_DB in skill.hpp.
-// - To remove entry by importing, put 0 for 'PreservePoints'
-
-//Mage
-14,7,6190	//Cold Bolt
-19,7,6189	//Fire Bolt
-20,7,6191	//Lightning Bolt
-21,9,6197	//Thunder Storm
-
-//Wizard
-83,10,6194	//Meteor Storm
-84,9,6198	//Jupitel Thunder
-85,10,6193	//Lord of Vermillion
-86,9,6199	//Water Ball
-89,10,6192	//Storm Gust
-90,8,6201	//Earth Spike
-91,9,6200	//Heaven's Drive
-
-//Warlock
-2210,8,6205	//Drain Life
-2211,12,6204	//Crimson Rock
-2213,22,6195	//Comet
-2214,12,6203	//Chain Lightning
-2216,12,6202	//Earth Strain
-2217,22,6196	//Tetra Vortex

+ 38 - 0
db/spellbook_db.yml

@@ -0,0 +1,38 @@
+# This file is a part of rAthena.
+#   Copyright(C) 2019 rAthena Development Team
+#   https://rathena.org - https://github.com/rathena
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+###########################################################################
+# Reading Spellbook Database
+###########################################################################
+#
+# Reading Spellbook Settings
+#
+###########################################################################
+# - Skill             Skill that is usable through a Spellbook.
+#   Book              Book item required to cast skill.
+#   PreservePoints    Amount of points required to preserve the skill into the book.
+###########################################################################
+
+Header:
+  Type: READING_SPELLBOOK_DB
+  Version: 1
+
+Footer:
+  Imports:
+  - Path: db/re/spellbook_db.yml
+    Mode: Renewal
+  - Path: db/import/spellbook_db.yml

+ 11 - 0
doc/yaml/db/spellbook_db.yml

@@ -0,0 +1,11 @@
+###########################################################################
+# Reading Spellbook Database
+###########################################################################
+#
+# Reading Spellbook Settings
+#
+###########################################################################
+# - Skill             Skill that is usable through a Spellbook.
+#   Book              Book item required to cast skill.
+#   PreservePoints    Amount of points required to preserve the skill into the book.
+###########################################################################

+ 1 - 1
src/map/clif.cpp

@@ -18382,7 +18382,7 @@ int clif_spellbook_list(struct map_session_data *sd)
 
 	for( i = 0, c = 0; i < MAX_INVENTORY; i ++ )
 	{
-		if( itemdb_is_spellbook2(sd->inventory.u.items_inventory[i].nameid) )
+		if( reading_spellbook_db.findBook(sd->inventory.u.items_inventory[i].nameid) )
 		{
 			WFIFOW(fd, c * 2 + 4) = sd->inventory.u.items_inventory[i].nameid;
 			c++;

+ 0 - 13
src/map/itemdb.cpp

@@ -1634,19 +1634,6 @@ bool itemdb_isNoEquip(struct item_data *id, uint16 m) {
 	return false;
 }
 
-/**
-* Check if item is available in spellbook_db or not
-* @param nameid
-* @return True if item is spellbook; False if not
-*/
-bool itemdb_is_spellbook2(unsigned short nameid) {
-	unsigned char i;
-	if (!nameid || !itemdb_exists(nameid) || !skill_spellbook_count)
-		return false;
-	ARR_FIND(0, MAX_SKILL_SPELLBOOK_DB, i, skill_spellbook_db[i].nameid == nameid);
-	return i == MAX_SKILL_SPELLBOOK_DB ? false : true;
-}
-
 /**
 * Retrieves random option data
 */

+ 0 - 1
src/map/itemdb.hpp

@@ -916,7 +916,6 @@ struct item_data* itemdb_exists(unsigned short nameid);
 #define itemdb_dropeffect(n) (itemdb_search(n)->flag.dropEffect)
 const char* itemdb_typename(enum item_types type);
 const char *itemdb_typename_ammo (enum e_item_ammo ammo);
-bool itemdb_is_spellbook2(unsigned short nameid);
 
 struct s_item_group_entry *itemdb_get_randgroupitem(uint16 group_id, uint8 sub_group);
 unsigned short itemdb_searchrandomid(uint16 group_id, uint8 sub_group);

+ 1 - 1
src/map/map-server.vcxproj

@@ -369,7 +369,7 @@
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\skill_require_db.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\skill_require_db.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\skill_tree.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\skill_tree.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\skill_unit_db.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\skill_unit_db.txt')" />
-    <Copy SourceFiles="$(SolutionDir)db\import-tmpl\spellbook_db.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\spellbook_db.txt')" />
+    <Copy SourceFiles="$(SolutionDir)db\import-tmpl\spellbook_db.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\spellbook_db.yml')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\statpoint.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\statpoint.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\status_disabled.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\status_disabled.txt')" />
   </Target>

+ 118 - 65
src/map/skill.cpp

@@ -17,6 +17,7 @@
 #include "../common/showmsg.hpp"
 #include "../common/strlib.hpp"
 #include "../common/timer.hpp"
+#include "../common/utilities.hpp"
 #include "../common/utils.hpp"
 
 #include "achievement.hpp"
@@ -44,6 +45,8 @@
 #include "status.hpp"
 #include "unit.hpp"
 
+using namespace rathena;
+
 #define SKILLUNITTIMER_INTERVAL	100
 #define TIMERSKILL_INTERVAL	150
 
@@ -91,6 +94,8 @@ AbraDatabase abra_db;
 
 ImprovisedSongDatabase improvised_song_db;
 
+ReadingSpellbookDatabase reading_spellbook_db;
+
 #define MAX_SKILL_CHANGEMATERIAL_DB 75
 #define MAX_SKILL_CHANGEMATERIAL_SET 3
 struct s_skill_changematerial_db {
@@ -102,9 +107,6 @@ struct s_skill_changematerial_db {
 struct s_skill_changematerial_db skill_changematerial_db[MAX_SKILL_CHANGEMATERIAL_DB];
 static unsigned short skill_changematerial_count;
 
-//Warlock
-struct s_skill_spellbook_db skill_spellbook_db[MAX_SKILL_SPELLBOOK_DB];
-unsigned short skill_spellbook_count;
 
 MagicMushroomDatabase magic_mushroom_db;
 
@@ -19913,16 +19915,23 @@ int skill_magicdecoy(struct map_session_data *sd, unsigned short nameid) {
 
 // Warlock Spellbooks. [LimitLine/3CeAM]
 void skill_spellbook(struct map_session_data *sd, unsigned short nameid) {
-	int i, max_preserve, skill_id, point;
-	struct status_change *sc;
-
 	nullpo_retv(sd);
 
-	sc = status_get_sc(&sd->bl);
+	if (reading_spellbook_db.empty())
+		return;
+
+	int i;
+	struct status_change *sc = status_get_sc(&sd->bl);
+
 	status_change_end(&sd->bl, SC_STOP, INVALID_TIMER);
 
 	for (i = SC_SPELLBOOK1; i <= SC_MAXSPELLBOOK; i++) {
-		if( sc && !sc->data[i] )
+		// No further checks needed
+		if( !sc ){
+			break;
+		}
+
+		if( !sc->data[i] )
 			break;
 	}
 
@@ -19931,41 +19940,40 @@ void skill_spellbook(struct map_session_data *sd, unsigned short nameid) {
 		return;
 	}
 
-	if (!skill_spellbook_count)
-		return;
+	std::shared_ptr<s_skill_spellbook_db> spell = reading_spellbook_db.findBook(nameid);
 
-	ARR_FIND(0,MAX_SKILL_SPELLBOOK_DB,i,skill_spellbook_db[i].nameid == nameid); // Search for information of this item
-	if( i == MAX_SKILL_SPELLBOOK_DB )
+	if (spell == nullptr)
 		return;
 
-	if( !pc_checkskill(sd, (skill_id = skill_spellbook_db[i].skill_id)) ) { // User don't know the skill
-		sc_start(&sd->bl,&sd->bl, SC_SLEEP, 100, 1, skill_get_time(WL_READING_SB, pc_checkskill(sd,WL_READING_SB)));
+	uint16 skill_id = spell->skill_id, skill_lv = pc_checkskill(sd, skill_id);
+
+	if (!skill_lv) { // Caster hasn't learned the skill
+		sc_start(&sd->bl,&sd->bl, SC_SLEEP, 100, 1, skill_get_time(WL_READING_SB, pc_checkskill(sd, WL_READING_SB)));
 		clif_skill_fail(sd, WL_READING_SB, USESKILL_FAIL_SPELLBOOK_DIFFICULT_SLEEP, 0);
 		return;
 	}
 
-	max_preserve = 4 * pc_checkskill(sd, WL_FREEZE_SP) + status_get_int(&sd->bl) / 10 + sd->status.base_level / 10;
-	point = skill_spellbook_db[i].point;
+	int points = spell->points;
 
-	if( sc && sc->data[SC_FREEZE_SP] ) {
-		if( (sc->data[SC_FREEZE_SP]->val2 + point) > max_preserve ) {
+	if (sc && sc->data[SC_FREEZE_SP]) {
+		if ((sc->data[SC_FREEZE_SP]->val2 + points) > 4 * pc_checkskill(sd, WL_FREEZE_SP) + status_get_int(&sd->bl) / 10 + sd->status.base_level / 10) {
 			clif_skill_fail(sd, WL_READING_SB, USESKILL_FAIL_SPELLBOOK_PRESERVATION_POINT, 0);
 			return;
 		}
-		for(i = SC_MAXSPELLBOOK; i >= SC_SPELLBOOK1; i--){ // This is how official saves spellbook. [malufett]
-			if( !sc->data[i] ){
-				sc->data[SC_FREEZE_SP]->val2 += point; // increase points
-				sc_start4(&sd->bl,&sd->bl, (sc_type)i, 100, skill_id, pc_checkskill(sd,skill_id), point, 0, INFINITE_TICK);
+		for (i = SC_MAXSPELLBOOK; i >= SC_SPELLBOOK1; i--) { // This is how official saves spellbook. [malufett]
+			if (!sc->data[i]) {
+				sc->data[SC_FREEZE_SP]->val2 += points; // increase points
+				sc_start4(&sd->bl,&sd->bl, (sc_type)i, 100, skill_id, skill_lv, points, 0, INFINITE_TICK);
 				break;
 			}
 		}
 	} else {
-		sc_start2(&sd->bl,&sd->bl, SC_FREEZE_SP, 100, 0, point, INFINITE_TICK);
-		sc_start4(&sd->bl,&sd->bl, SC_MAXSPELLBOOK, 100, skill_id, pc_checkskill(sd,skill_id), point, 0, INFINITE_TICK);
+		sc_start2(&sd->bl, &sd->bl, SC_FREEZE_SP, 100, 0, points, INFINITE_TICK);
+		sc_start4(&sd->bl, &sd->bl, SC_MAXSPELLBOOK, 100, skill_id, skill_lv, points, 0, INFINITE_TICK);
 	}
 
 	// Reading Spell Book SP cost same as the sealed spell.
-	status_zap(&sd->bl, 0, skill_get_sp(skill_id, pc_checkskill(sd, skill_id)));
+	status_zap(&sd->bl, 0, skill_get_sp(skill_id, skill_lv));
 }
 
 int skill_select_menu(struct map_session_data *sd,uint16 skill_id) {
@@ -21418,42 +21426,89 @@ static bool skill_parse_row_createarrowdb(char* split[], int columns, int curren
 	return true;
 }
 
-/** Reads Spell book db
- * Structure: SkillID,PreservePoints,RequiredBook
+const std::string ReadingSpellbookDatabase::getDefaultLocation() {
+	return std::string(db_path) + "/spellbook_db.yml";
+}
+/**
+ * Reads and parses an entry from the spellbook_db.
+ * @param node: YAML node containing the entry.
+ * @return count of successfully parsed rows
  */
-static bool skill_parse_row_spellbookdb(char* split[], int columns, int current)
-{
-	unsigned short skill_id = atoi(split[0]), points = atoi(split[1]), nameid = atoi(split[2]);
+uint64 ReadingSpellbookDatabase::parseBodyNode(const YAML::Node &node) {
+	std::string skill_name;
 
-	if (!skill_get_index(skill_id) || !skill_get_max(skill_id))
-		ShowError("skill_parse_row_spellbookdb: Invalid skill ID %d\n", skill_id);
-	if (!skill_get_inf(skill_id))
-		ShowError("skill_parse_row_spellbookdb: Passive skills cannot be memorized (%d/%s)\n", skill_id, skill_get_name(skill_id));
-	else {
-		unsigned short i;
+	if (!this->asString(node, "Skill", skill_name))
+		return 0;
 
-		ARR_FIND(0, skill_spellbook_count, i, skill_spellbook_db[i].skill_id == skill_id);
-		if (i >= ARRAYLENGTH(skill_spellbook_db)) {
-			ShowError("skill_parse_row_spellbookdb: Maximum db entries reached.\n");
-			return false;
-		}
-		// Import just for clearing/disabling from original data
-		if (points == 0) {
-			memset(&skill_spellbook_db[i], 0, sizeof(skill_spellbook_db[i]));
-			//ShowInfo("skill_parse_row_spellbookdb: Skill %d removed from list.\n", skill_id);
-			return true;
+	uint16 skill_id = skill_name2id(skill_name.c_str());
+
+	if (skill_id == 0) {
+		this->invalidWarning(node["Skill"], "Invalid skill name \"%s\", skipping.\n", skill_name.c_str());
+		return 0;
+	}
+
+	if (!skill_get_inf(skill_id)) {
+		this->invalidWarning(node["Skill"], "Passive skill %s cannot be memorized in a Spell Book.\n", skill_name.c_str());
+		return 0;
+	}
+
+	std::shared_ptr<s_skill_spellbook_db> spell = this->find(skill_id);
+	bool exists = spell != nullptr;
+
+	if (!exists) {
+		if (!this->nodesExist(node, { "Book", "PreservePoints" }))
+			return 0;
+
+		spell = std::make_shared<s_skill_spellbook_db>();
+		spell->skill_id = skill_id;
+	}
+
+	if (this->nodeExists(node, "Book")) {
+		std::string book_name;
+
+		if (!this->asString(node, "Book", book_name))
+			return 0;
+
+		struct item_data *item = itemdb_search_aegisname(book_name.c_str());
+
+		if (item == nullptr) {
+			this->invalidWarning(node["Book"], "Book item %s does not exist.\n", book_name.c_str());
+			return 0;
 		}
 
-		skill_spellbook_db[i].skill_id = skill_id;
-		skill_spellbook_db[i].point = points;
-		skill_spellbook_db[i].nameid = nameid;
+		spell->nameid = item->nameid;
+	}
 
-		if (i == skill_spellbook_count)
-			skill_spellbook_count++;
-		return true;
+	if (this->nodeExists(node, "PreservePoints")) {
+		uint16 points;
+
+		if (!this->asUInt16(node, "PreservePoints", points))
+			return 0;
+
+		spell->points = points;
 	}
 
-	return false;
+	if (!exists)
+		this->put(skill_id, spell);
+
+	return 1;
+}
+
+/**
+ * Check if the specified item is available in the spellbook_db or not
+ * @param nameid: Book Item ID
+ * @return Spell data or nullptr otherwise
+ */
+std::shared_ptr<s_skill_spellbook_db> ReadingSpellbookDatabase::findBook(int32 nameid) {
+	if (nameid < 1 || !itemdb_exists(nameid) || reading_spellbook_db.size() == 0)
+		return nullptr;
+
+	for (const auto &spell : reading_spellbook_db) {
+		if (spell.second->nameid == nameid)
+			return spell.second;
+	}
+
+	return nullptr;
 }
 
 
@@ -21515,10 +21570,10 @@ const std::string MagicMushroomDatabase::getDefaultLocation() {
 }
 
 /**
-* Reads and parses an entry from the magicmushroom_db.
-* @param node: YAML node containing the entry.
-* @return count of successfully parsed rows
-*/
+ * Reads and parses an entry from the magicmushroom_db.
+ * @param node: YAML node containing the entry.
+ * @return count of successfully parsed rows
+ */
 uint64 MagicMushroomDatabase::parseBodyNode(const YAML::Node &node) {
 	std::string skill_name;
 
@@ -21613,10 +21668,10 @@ const std::string AbraDatabase::getDefaultLocation() {
 }
 
 /**
-* Reads and parses an entry from the abra_db.
-* @param node: YAML node containing the entry.
-* @return count of successfully parsed rows
-*/
+ * Reads and parses an entry from the abra_db.
+ * @param node: YAML node containing the entry.
+ * @return count of successfully parsed rows
+ */
 uint64 AbraDatabase::parseBodyNode(const YAML::Node &node) {
 	std::string skill_name;
 
@@ -21843,10 +21898,8 @@ static void skill_readdb(void)
 
 	memset(skill_produce_db,0,sizeof(skill_produce_db));
 	memset(skill_arrow_db,0,sizeof(skill_arrow_db));
-	memset(skill_spellbook_db,0,sizeof(skill_spellbook_db));
 	memset(skill_changematerial_db,0,sizeof(skill_changematerial_db));
-	skill_produce_count = skill_arrow_count = 
-		skill_changematerial_count = skill_spellbook_count = 0;
+	skill_produce_count = skill_arrow_count = skill_changematerial_count = 0;
 
 	for(i=0; i<ARRAYLENGTH(dbsubpath); i++){
 		size_t n1 = strlen(db_path)+strlen(dbsubpath[i])+1;
@@ -21871,7 +21924,6 @@ static void skill_readdb(void)
 
 		sv_readdb(dbsubpath2, "produce_db.txt"        , ',',   5,  5+2*MAX_PRODUCE_RESOURCE, MAX_SKILL_PRODUCE_DB, skill_parse_row_producedb, i > 0);
 		sv_readdb(dbsubpath1, "create_arrow_db.txt"   , ',', 1+2,  1+2*MAX_ARROW_RESULT, MAX_SKILL_ARROW_DB, skill_parse_row_createarrowdb, i > 0);
-		sv_readdb(dbsubpath1, "spellbook_db.txt"      , ',',   3,  3, MAX_SKILL_SPELLBOOK_DB, skill_parse_row_spellbookdb, i > 0);
 		sv_readdb(dbsubpath1, "skill_copyable_db.txt"       , ',',   2,  4, -1, skill_parse_row_copyabledb, i > 0);
 		sv_readdb(dbsubpath1, "skill_changematerial_db.txt" , ',',   5,  5+2*MAX_SKILL_CHANGEMATERIAL_SET, MAX_SKILL_CHANGEMATERIAL_DB, skill_parse_row_changematerialdb, i > 0);
 		sv_readdb(dbsubpath1, "skill_nonearnpc_db.txt"      , ',',   2,  3, -1, skill_parse_row_nonearnpcrangedb, i > 0);
@@ -21884,6 +21936,7 @@ static void skill_readdb(void)
 	abra_db.load();
 	improvised_song_db.load();
 	magic_mushroom_db.load();
+	reading_spellbook_db.load();
 	
 	skill_init_unit_layout();
 	skill_init_nounit_layout();

+ 16 - 6
src/map/skill.hpp

@@ -2181,20 +2181,30 @@ void skill_usave_trigger(struct map_session_data *sd);
 /**
  * Warlock
  **/
-#define MAX_SKILL_SPELLBOOK_DB	17
 enum wl_spheres {
 	WLS_FIRE = 0x44,
 	WLS_WIND,
 	WLS_WATER,
 	WLS_STONE,
 };
+
 struct s_skill_spellbook_db {
-	unsigned short nameid;
-	unsigned short skill_id;
-	unsigned short point;
+	uint16 skill_id, nameid, points;
 };
-extern struct s_skill_spellbook_db skill_spellbook_db[MAX_SKILL_SPELLBOOK_DB];
-extern unsigned short skill_spellbook_count;
+
+class ReadingSpellbookDatabase : public TypesafeYamlDatabase<uint16, s_skill_spellbook_db> {
+public:
+	ReadingSpellbookDatabase() : TypesafeYamlDatabase("READING_SPELLBOOK_DB", 1) {
+
+	}
+
+	const std::string getDefaultLocation();
+	uint64 parseBodyNode(const YAML::Node& node);
+	std::shared_ptr<s_skill_spellbook_db> findBook(int32 nameid);
+};
+
+extern ReadingSpellbookDatabase reading_spellbook_db;
+
 void skill_spellbook(struct map_session_data *sd, unsigned short nameid);
 int skill_block_check(struct block_list *bl, enum sc_type type, uint16 skill_id);
 

+ 37 - 0
src/tool/csv2yaml.cpp

@@ -72,6 +72,7 @@ static size_t pet_read_db( const char* file );
 static bool skill_parse_row_magicmushroomdb(char* split[], int column, int current);
 static bool skill_parse_row_abradb(char* split[], int columns, int current);
 static bool skill_parse_row_improvisedb(char* split[], int columns, int current);
+static bool skill_parse_row_spellbookdb(char* split[], int columns, int current);
 
 // Constants for conversion
 std::unordered_map<uint16, std::string> aegis_itemnames;
@@ -255,6 +256,12 @@ int do_init( int argc, char** argv ){
 		return 0;
 	}
 
+	if (!process("READING_SPELLBOOK_DB", 1, root_paths, "spellbook_db", [](const std::string& path, const std::string& name_ext) -> bool {
+		return sv_readdb(path.c_str(), name_ext.c_str(), ',', 3, 3, -1, &skill_parse_row_spellbookdb, false);
+	})) {
+		return 0;
+	}
+
 	// TODO: add implementations ;-)
 
 	return 0;
@@ -749,11 +756,13 @@ static bool skill_parse_row_abradb(char* split[], int columns, int current)
 
 		body << YAML::EndSeq;
 	}
+
 	body << YAML::EndMap;
 
 	return true;
 }
 
+// Copied and adjusted from skill.cpp
 static bool skill_parse_row_improvisedb(char* split[], int columns, int current)
 {
 	uint16 skill_id = atoi(split[0]);
@@ -771,3 +780,31 @@ static bool skill_parse_row_improvisedb(char* split[], int columns, int current)
 
 	return true;
 }
+
+// Copied and adjusted from skill.cpp
+static bool skill_parse_row_spellbookdb(char* split[], int columns, int current)
+{
+	uint16 skill_id = atoi(split[0]);
+	std::string *skill_name = util::umap_find(aegis_skillnames, skill_id);
+
+	if (skill_name == nullptr) {
+		ShowError("Skill name for Spell Book skill ID %hu is not known.\n", skill_id);
+		return false;
+	}
+
+	uint16 nameid = atoi(split[2]);
+	std::string *book_name = util::umap_find(aegis_itemnames, nameid);
+
+	if (book_name == nullptr) {
+		ShowError("Book name for item ID %hu is not known.\n", nameid);
+		return false;
+	}
+
+	body << YAML::BeginMap;
+	body << YAML::Key << "Skill" << YAML::Value << *skill_name;
+	body << YAML::Key << "Book" << YAML::Value << *book_name;
+	body << YAML::Key << "PreservePoints" << YAML::Value << atoi(split[1]);
+	body << YAML::EndMap;
+
+	return true;
+}