Bläddra i källkod

Updated the YAML parsing method (#3694)

Synchronized the process of loading and parsing YAML databases.
Provides more robust parse warnings/errors.
Added Header checks to validate database versions.
Adjusted root keys to a standard name of Body.
Adjusted inter-server storages, achievements, and attendance to follow the new format.

Fixes #3483

Thanks to @Lemongrass3110!
Aleos 6 år sedan
förälder
incheckning
78eed02273

+ 5 - 1
conf/import-tmpl/inter_server.yml

@@ -10,7 +10,11 @@
 #	  Table: "<storage_table>"  // (string) Name of table where storage is saved. The table stucture is the same as the default storage table.
 #	  Max: <max_amount>         // (int) *optional* Maximum number of items in storage. MAX_STORAGE will be used if no value is defined.
 ###############################################################################################################################################
-#Storages:
+Header:
+  Type: INTER_SERVER_DB
+  Version: 1
+
+#Body:
 #   - ID: 1
 #     Name: "VIP Storage"
 #     Table: "vip_storage"

+ 12 - 4
conf/inter_server.yml

@@ -28,7 +28,15 @@
 #	  Table: "<storage_table>"  // (string) Name of table where storage is saved. The table stucture is the same as the default storage table.
 #	  Max: <max_amount>         // (int) *optional* Maximum number of items in storage. MAX_STORAGE will be used if no value is defined.
 
-Storages:
-    - ID: 0
-      Name: "Storage"
-      Table: "storage"
+Header:
+  Type: INTER_SERVER_DB
+  Version: 1
+
+Body:
+  - ID: 0
+    Name: "Storage"
+    Table: "storage"
+
+Footer:
+  Imports:
+    - Path: conf/import/inter_server.yml

+ 90 - 0
db/achievement_db.yml

@@ -0,0 +1,90 @@
+# This file is a part of rAthena.
+#   Copyright(C) 2017 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/>.
+#
+###########################################################################
+# Renewal Achievement Database
+###########################################################################
+#
+# Achievement Settings
+#
+###########################################################################
+# ID - Unique achievement ID.
+###########################################################################
+# Group - Achievement group type. Each achievement type calls a specific
+# objective check.
+# Valid groups:
+#  AG_ADD_FRIEND
+#  AG_ADVENTURE
+#  AG_BABY
+#  AG_BATTLE
+#  AG_CHATTING
+#  AG_CHATTING_COUNT
+#  AG_CHATTING_CREATE
+#  AG_CHATTING_DYING
+#  AG_EAT
+#  AG_GET_ITEM
+#  AG_GET_ZENY
+#  AG_GOAL_ACHIEVE
+#  AG_GOAL_LEVEL
+#  AG_GOAL_STATUS
+#  AG_HEAR
+#  AG_JOB_CHANGE
+#  AG_MARRY
+#  AG_PARTY
+#  AG_ENCHANT_FAIL
+#  AG_ENCHANT_SUCCESS
+#  AG_SEE
+#  AG_SPEND_ZENY
+#  AG_TAMING
+###########################################################################
+# Name - Achievement name. Used when sending rewards through RODEX.
+###########################################################################
+# Target - A list of monster ID and count values that the achievement
+# requires. The target count can also be used for achievements that keep
+# a counter while not being related to monster kills.
+# Capped at MAX_ACHIEVEMENT_OBJECTIVES.
+###########################################################################
+# Condition - A conditional statement that must be met for the achievement
+# to be considered complete.
+###########################################################################
+# Map - A map name that is used for the AG_CHATTING type which increments
+# the counter based on the player's map.
+###########################################################################
+# Dependent: - A list of achievement IDs that need to be completed before
+# this achievement is considered complete.
+###########################################################################
+# Reward - A list of rewards that are given on completion. All fields are
+# optional.
+#   ItemID: Item ID
+#   Amount:  Amount of Item ID (default 1)
+#   Script: Bonus Script
+#   TitleID: Title ID
+###########################################################################
+# Score - Achievement points that are given on completion.
+###########################################################################
+
+Header:
+  Type: ACHIEVEMENT_DB
+  Version: 1
+
+Footer:
+  Imports:
+  - Path: db/pre-re/achievement_db.yml
+    Mode: Prerenewal
+  - Path: db/re/achievement_db.yml
+    Mode: Renewal
+  - Path: db/import/achievement_db.yml

+ 11 - 0
db/attendance.yml

@@ -0,0 +1,11 @@
+Header:
+  Type: ATTENDANCE_DB
+  Version: 1
+  
+Footer:
+  Imports:
+  - Path: db/pre-re/attendance.yml
+    Mode: Prerenewal
+  - Path: db/re/attendance.yml
+    Mode: Renewal
+  - Path: db/import/attendance.yml

+ 3 - 1
db/import-tmpl/achievement_db.yml

@@ -77,4 +77,6 @@
 # Score - Achievement points that are given on completion.
 ###########################################################################
 
-Achievements:
+Header:
+  Type: ACHIEVEMENT_DB
+  Version: 1

+ 1 - 1
db/import-tmpl/attendance.yml

@@ -1,3 +1,3 @@
 Header:
-  Type: ATTENDANCE_CONF
+  Type: ATTENDANCE_DB
   Version: 1

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 358 - 115
db/pre-re/achievement_db.yml


+ 1 - 1
db/pre-re/attendance.yml

@@ -1,3 +1,3 @@
 Header:
-  Type: ATTENDANCE_CONF
+  Type: ATTENDANCE_DB
   Version: 1

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 358 - 115
db/re/achievement_db.yml


+ 59 - 59
db/re/attendance.yml

@@ -1,61 +1,61 @@
 Header:
-  Type: ATTENDANCE_CONF
+  Type: ATTENDANCE_DB
   Version: 1
-  
-Attendance:
-  - Start: 20180502
-    End: 20180529
-    Rewards:
-      - Day: 1
-        ItemId: 22979
-      - Day: 2
-        ItemId: 6316
-      - Day: 3
-        ItemId: 12265
-        Amount: 5
-      - Day: 4
-        ItemId: 23047
-        Amount: 5
-      - Day: 5
-        ItemId: 23038
-      - Day: 6
-        ItemId: 23043
-      - Day: 7
-        ItemId: 23340
-        Amount: 3
-      - Day: 8
-        ItemId: 12516
-        Amount: 5
-      - Day: 9
-        ItemId: 23307
-        Amount: 5
-      - Day: 10
-        ItemId: 12610
-      - Day: 11
-        ItemId: 14533
-        Amount: 2
-      - Day: 12
-        ItemId: 23012
-        Amount: 3
-      - Day: 13
-        ItemId: 23048
-        Amount: 5
-      - Day: 14
-        ItemId: 12264
-        Amount: 5
-      - Day: 15
-        ItemId: 23046
-        Amount: 5
-      - Day: 16
-        ItemId: 12515
-        Amount: 5
-      - Day: 17
-        ItemId: 12522
-        Amount: 5
-      - Day: 18
-        ItemId: 12523
-        Amount: 5
-      - Day: 19
-        ItemId: 6234
-      - Day: 20
-        ItemId: 22845
+
+Body:
+#  - Start: 20180502
+#    End: 20180529
+#    Rewards:
+#      - Day: 1
+#        ItemId: 22979
+#      - Day: 2
+#        ItemId: 6316
+#      - Day: 3
+#        ItemId: 12265
+#        Amount: 5
+#      - Day: 4
+#        ItemId: 23047
+#        Amount: 5
+#      - Day: 5
+#        ItemId: 23038
+#      - Day: 6
+#        ItemId: 23043
+#      - Day: 7
+#        ItemId: 23340
+#        Amount: 3
+#      - Day: 8
+#        ItemId: 12516
+#        Amount: 5
+#      - Day: 9
+#        ItemId: 23307
+#        Amount: 5
+#      - Day: 10
+#        ItemId: 12610
+#      - Day: 11
+#        ItemId: 14533
+#        Amount: 2
+#      - Day: 12
+#        ItemId: 23012
+#        Amount: 3
+#      - Day: 13
+#        ItemId: 23048
+#        Amount: 5
+#      - Day: 14
+#        ItemId: 12264
+#        Amount: 5
+#      - Day: 15
+#        ItemId: 23046
+#        Amount: 5
+#      - Day: 16
+#        ItemId: 12515
+#        Amount: 5
+#      - Day: 17
+#        ItemId: 12522
+#        Amount: 5
+#      - Day: 18
+#        ItemId: 12523
+#        Amount: 5
+#      - Day: 19
+#        ItemId: 6234
+#      - Day: 20
+#        ItemId: 22845

+ 3 - 2
doc/achievements.txt

@@ -3,7 +3,7 @@
 //===== By: ==================================================
 //= rAthena Dev Team
 //===== Last Updated: ========================================
-//= 20170531
+//= 20190226
 //===== Description: =========================================
 //= Explanation of the achievements_db.yml file and structure.
 //============================================================
@@ -64,7 +64,8 @@ Example 2:
   // IE: In the achievement_list.lub file, UI_Type 0 is displayed as non-incremental while 1 shows a progress bar of completion for the achievement.
   Condition: " ARG0 >= 100 "
   Target:
-    - Count: 100
+    - Id: 0 // Array index value
+      Count: 100
 
 ---------------------------------------
 

+ 1 - 1
src/char/char.cpp

@@ -3208,7 +3208,7 @@ int do_init(int argc, char **argv)
 		ShowNotice("And then change the user/password to use in conf/char_athena.conf (or conf/import/char_conf.txt)\n");
 	}
 
-	inter_init_sql((argc > 2) ? argv[2] : inter_cfgName); // inter server configuration
+	inter_init_sql((argc > 2) ? argv[2] : SQL_CONF_NAME); // inter server configuration
 
 	auth_db = idb_alloc(DB_OPT_RELEASE_DATA);
 	online_char_db = idb_alloc(DB_OPT_RELEASE_DATA);

+ 25 - 20
src/char/int_storage.cpp

@@ -18,23 +18,18 @@
 #include "inter.hpp"
 #include "int_guild.hpp"
 
-/**
- * Check if storage ID is valid
- * @param id: Storage ID
- * @return True if success or false on failure
- */
-bool inter_premiumStorage_exists(uint8 id) {
-	return interserv_config.storages.find(id) != interserv_config.storages.end();
-}
-
 /**
  * Get max storage amount
  * @param id: Storage ID
  * @return Max amount
  */
 int inter_premiumStorage_getMax(uint8 id) {
-	if (inter_premiumStorage_exists(id))
-		return interserv_config.storages[id]->max_num;
+	std::shared_ptr<s_storage_table> storage = interServerDb.find( id );
+
+	if( storage != nullptr ){
+		return storage->max_num;
+	}
+
 	return MAX_STORAGE;
 }
 
@@ -44,8 +39,12 @@ int inter_premiumStorage_getMax(uint8 id) {
  * @return Table name
  */
 const char *inter_premiumStorage_getTableName(uint8 id) {
-	if (inter_premiumStorage_exists(id))
-		return interserv_config.storages[id]->table;
+	std::shared_ptr<s_storage_table> storage = interServerDb.find( id );
+
+	if( storage != nullptr ){
+		return storage->table;
+	}
+
 	return schema_config.storage_db;
 }
 
@@ -55,8 +54,12 @@ const char *inter_premiumStorage_getTableName(uint8 id) {
  * @return printable name
  */
 const char *inter_premiumStorage_getPrintableName(uint8 id) {
-	if (inter_premiumStorage_exists(id))
-		return interserv_config.storages[id]->name;
+	std::shared_ptr<s_storage_table> storage = interServerDb.find( id );
+
+	if( storage != nullptr ){
+		return storage->name;
+	}
+
 	return "Storage";
 }
 
@@ -152,7 +155,7 @@ bool guild_storage_fromsql(int guild_id, struct s_storage* p)
 
 void inter_storage_checkDB(void) {
 	// Checking storage tables
-	for (auto storage_table : interserv_config.storages) {
+	for( auto storage_table : interServerDb ){
 		if (SQL_ERROR == Sql_Query(sql_handle, "SELECT  `id`,`account_id`,`nameid`,`amount`,`equip`,`identify`,`refine`,"
 			"`attribute`,`card0`,`card1`,`card2`,`card3`,`option_id0`,`option_val0`,`option_parm0`,`option_id1`,`option_val1`,`option_parm1`,"
 			"`option_id2`,`option_val2`,`option_parm2`,`option_id3`,`option_val3`,`option_parm3`,`option_id4`,`option_val4`,`option_parm4`,"
@@ -522,10 +525,11 @@ bool mapif_parse_StorageLoad(int fd) {
 	switch (type) {
 		case TABLE_INVENTORY: res = inventory_fromsql(cid, &stor); break;
 		case TABLE_STORAGE:
-			if (!inter_premiumStorage_exists(stor_id)) {
-				ShowError("Invalid storage with id %d\n", stor_id);
+			if( !interServerDb.exists( stor_id ) ){
+				ShowError( "Invalid storage with id %d\n", stor_id );
 				return false;
 			}
+
 			res = storage_fromsql(aid, &stor);
 			break;
 		case TABLE_CART:      res = cart_fromsql(cid, &stor);      break;
@@ -561,10 +565,11 @@ bool mapif_parse_StorageSave(int fd) {
 	switch(type){
 		case TABLE_INVENTORY:	inventory_tosql(cid, &stor); break;
 		case TABLE_STORAGE:
-			if (!inter_premiumStorage_exists(stor.stor_id)) {
-				ShowError("Invalid storage with id %d\n", stor.stor_id);
+			if( !interServerDb.exists( stor.stor_id ) ){
+				ShowError( "Invalid storage with id %d\n", stor.stor_id );
 				return false;
 			}
+
 			storage_tosql(aid, &stor);
 			break;
 		case TABLE_CART:	cart_tosql(cid, &stor); break;

+ 0 - 1
src/char/int_storage.hpp

@@ -11,7 +11,6 @@ struct s_storage;
 void inter_storage_sql_init(void);
 void inter_storage_sql_final(void);
 
-bool inter_premiumStorage_exists(uint8 id);
 int inter_premiumStorage_getMax(uint8 id);
 const char *inter_premiumStorage_getTableName(uint8 id);
 const char *inter_premiumStorage_getPrintableName(uint8 id);

+ 63 - 79
src/char/inter.cpp

@@ -11,6 +11,7 @@
 #include <yaml-cpp/yaml.h>
 
 #include "../common/cbasetypes.hpp"
+#include "../common/database.hpp"
 #include "../common/malloc.hpp"
 #include "../common/showmsg.hpp"
 #include "../common/socket.hpp"
@@ -34,6 +35,9 @@
 #include "int_quest.hpp"
 #include "int_storage.hpp"
 
+std::string cfgFile = "inter_athena.yml"; ///< Inter-Config file
+InterServerDatabase interServerDb;
+
 #define WISDATA_TTL (60*1000)	//Wis data Time To Live (60 seconds)
 #define WISDELLIST_MAX 256		// Number of elements in the list Delete data Wis
 
@@ -46,7 +50,6 @@ char char_server_id[32] = "ragnarok";
 char char_server_pw[32] = ""; // Allow user to send empty password (bugreport:7787)
 char char_server_db[32] = "ragnarok";
 char default_codepage[32] = ""; //Feature by irmin.
-struct Inter_Config interserv_config;
 unsigned int party_share_level = 10;
 
 /// Received packet Lengths from map-server
@@ -819,7 +822,7 @@ int inter_config_read(const char* cfgName)
 		else if(!strcmpi(w1,"log_inter"))
 			charserv_config.log_inter = atoi(w2);
 		else if(!strcmpi(w1,"inter_server_conf"))
-			interserv_config.cfgFile = w2;
+			cfgFile = w2;
 		else if(!strcmpi(w1,"import"))
 			inter_config_read(w2);
 	}
@@ -848,103 +851,85 @@ int inter_log(const char* fmt, ...)
 	return 0;
 }
 
-void yaml_invalid_warning(const char* fmt, YAML::Node &node, std::string &file) {
-	YAML::Emitter out;
-	out << node;
-	ShowWarning(fmt, file.c_str());
-	ShowMessage("%s\n", out.c_str());
+const std::string InterServerDatabase::getDefaultLocation(){
+	return std::string(conf_path) + "/" + cfgFile;
 }
 
 /**
- * Read inter config file
- **/
-void inter_config_readConf(void) {
-	std::vector<std::string> directories = { "conf/", "conf/import/" };
-	static const std::string file_name(interserv_config.cfgFile);
+ * Reads and parses an entry from the inter_server.
+ * @param node: YAML node containing the entry.
+ * @return count of successfully parsed rows
+ */
+uint64 InterServerDatabase::parseBodyNode( const YAML::Node& node ){
+	uint32 id;
 
-	for (auto directory : directories) {
-		std::string current_file = directory + file_name;
-		YAML::Node config;
-		int count = 0;
+	if( !this->asUInt32( node, "ID", id ) ){
+		return 0;
+	}
 
-		try {
-			config = YAML::LoadFile(current_file);
-		}
-		catch (std::exception &e) {
-			ShowError("Cannot read storage definition file '" CL_WHITE "%s" CL_RESET "' (Caused by : " CL_WHITE "%s" CL_RESET ").\n", current_file.c_str(), e.what());
-			return;
+	auto storage_table = this->find( id );
+	bool existing = storage_table != nullptr;
+
+	if( !existing ){
+		if( !this->nodeExists( node, "Name" ) ){
+			this->invalidWarning( node, "Node \"Name\" is missing.\n" );
+			return 0;
 		}
 
-		if (config["Storages"]) {
-			for (auto node : config["Storages"]) {
-				unsigned int id;
+		if( !this->nodeExists( node, "Table" ) ){
+			this->invalidWarning( node, "Node \"Table\" is missing.\n" );
+			return 0;
+		}
 
-				if (!node["ID"]) {
-					yaml_invalid_warning("inter_config_readConf: Storage definition with no ID field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, current_file);
-					continue;
-				}
+		storage_table = std::make_shared<s_storage_table>();
 
-				try {
-					id = node["ID"].as<unsigned int>();
-				}
-				catch (const std::exception&) {
-					yaml_invalid_warning("inter_config_readConf: Storage definition with invalid ID field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, current_file);
-					continue;
-				}
+		storage_table->id = (uint8)id;
+	}
 
-				if( id > UINT8_MAX ){
-					yaml_invalid_warning("inter_config_readConf: Storage definition with invalid ID field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, current_file);
-					continue;
-				}
+	if( this->nodeExists( node, "Name" ) ){
+		std::string name;
 
-				bool existing = inter_premiumStorage_exists(id);
-				auto storage_table = existing ? interserv_config.storages[id] : std::make_shared<s_storage_table>();
+		if( !this->asString( node, "Name", name ) ){
+			return 0;
+		}
 
-				if (!existing && (!node["Name"] || !node["Table"])) {
-					yaml_invalid_warning("inter_config_readConf: Invalid storage definition in '" CL_WHITE "%s" CL_RESET "'.\n", node, current_file);
-					continue;
-				}
-				
-				if (node["Name"])
-					safestrncpy(storage_table->name, node["Name"].as<std::string>().c_str(), NAME_LENGTH);
-				if(node["Table"])
-					safestrncpy(storage_table->table, node["Table"].as<std::string>().c_str(), DB_NAME_LEN);
-				if (node["Max"]) {
-					try {
-						storage_table->max_num = node["Max"].as<uint16>();
-					}
-					catch (const std::exception&) {
-						yaml_invalid_warning("inter_config_readConf: Storage definition with invalid Max field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, current_file);
-						continue;
-					}
-				}
-				else if (!existing)
-					storage_table->max_num = MAX_STORAGE;
+		safestrncpy( storage_table->name, name.c_str(), NAME_LENGTH );
+	}
 
-				if (!existing) {
-					storage_table->id = (uint8)id;
-					interserv_config.storages[id] = storage_table;
-				}
+	if( this->nodeExists( node, "Table" ) ){
+		std::string table;
 
-				count++;
-			}
+		if( !this->asString( node, "Table", table ) ){
+			return 0;
 		}
-		ShowStatus("Done reading '" CL_WHITE "%d" CL_RESET "' storage information in '" CL_WHITE "%s" CL_RESET "'\n", count, current_file.c_str());
+
+		safestrncpy( storage_table->table, table.c_str(), DB_NAME_LEN );
 	}
-}
 
-void inter_config_finalConf(void) {
+	if( this->nodeExists( node, "Max" ) ){
+		uint16 max;
 
-}
+		if( !this->asUInt16( node, "Max", max ) ){
+			return 0;
+		}
+
+		storage_table->max_num = max;
+	}else{
+		if( !existing ){
+			storage_table->max_num = MAX_STORAGE;
+		}
+	}
+
+	if( !existing ){
+		this->put( storage_table->id, storage_table );
+	}
 
-void inter_config_defaults(void) {
-	interserv_config.cfgFile = "inter_server.yml";
+	return 1;
 }
 
 // initialize
 int inter_init_sql(const char *file)
 {
-	inter_config_defaults();
 	inter_config_read(file);
 
 	//DB connection initialized
@@ -965,7 +950,7 @@ int inter_init_sql(const char *file)
 	}
 
 	wis_db = idb_alloc(DB_OPT_RELEASE_DATA);
-	inter_config_readConf();
+	interServerDb.load();
 	inter_guild_sql_init();
 	inter_storage_sql_init();
 	inter_party_sql_init();
@@ -986,7 +971,6 @@ void inter_final(void)
 {
 	wis_db->destroy(wis_db, NULL);
 
-	inter_config_finalConf();
 	inter_guild_sql_final();
 	inter_storage_sql_final();
 	inter_party_sql_final();
@@ -1009,13 +993,13 @@ void inter_final(void)
  * @param fd
  **/
 void inter_Storage_sendInfo(int fd) {
-	int size = sizeof(struct s_storage_table), len = 4 + interserv_config.storages.size() * size, offset;
+	int size = sizeof(struct s_storage_table), len = 4 + interServerDb.size() * size, offset;
 	// Send storage table information
 	WFIFOHEAD(fd, len);
 	WFIFOW(fd, 0) = 0x388c;
 	WFIFOW(fd, 2) = len;
 	offset = 4;
-	for (auto storage : interserv_config.storages) {
+	for( auto storage : interServerDb ){
 		memcpy(WFIFOP(fd, offset), storage.second.get(), size);
 		offset += size;
 	}

+ 11 - 6
src/char/inter.hpp

@@ -9,15 +9,22 @@
 #include <unordered_map>
 
 #include "../common/cbasetypes.hpp"
+#include "../common/database.hpp"
 #include "../common/sql.hpp"
 
 struct s_storage_table;
-struct Inter_Config {
-	std::string cfgFile; ///< Inter-Config file
-	std::unordered_map< uint8, std::shared_ptr<s_storage_table> > storages; ///< Storage name & table information
+
+class InterServerDatabase : public TypesafeYamlDatabase<uint32, s_storage_table>{
+public:
+	InterServerDatabase() : TypesafeYamlDatabase( "INTER_SERVER_DB", 1 ){
+
+	}
+
+	const std::string getDefaultLocation();
+	uint64 parseBodyNode( const YAML::Node& node );
 };
 
-extern struct Inter_Config interserv_config;
+extern InterServerDatabase interServerDb;
 
 int inter_init_sql(const char *file);
 void inter_final(void);
@@ -30,8 +37,6 @@ void mapif_accinfo_ack(bool success, int map_fd, int u_fd, int u_aid, int accoun
 
 int inter_log(const char *fmt,...);
 
-#define inter_cfgName "conf/inter_athena.conf"
-
 extern unsigned int party_share_level;
 
 extern Sql* sql_handle;

+ 2 - 0
src/common/CMakeLists.txt

@@ -86,6 +86,7 @@ set( COMMON_BASE_HEADERS
 	${COMMON_ALL_HEADERS}
 	"${COMMON_SOURCE_DIR}/conf.hpp"
 	"${COMMON_SOURCE_DIR}/core.hpp"
+	"${COMMON_SOURCE_DIR}/database.hpp"
 	"${COMMON_SOURCE_DIR}/db.hpp"
 	"${COMMON_SOURCE_DIR}/des.hpp"
 	"${COMMON_SOURCE_DIR}/ers.hpp"
@@ -109,6 +110,7 @@ set( COMMON_BASE_HEADERS
 set( COMMON_BASE_SOURCES
 	"${COMMON_SOURCE_DIR}/conf.cpp"
 	"${COMMON_SOURCE_DIR}/core.cpp"
+	"${COMMON_SOURCE_DIR}/database.cpp"
 	"${COMMON_SOURCE_DIR}/db.cpp"
 	"${COMMON_SOURCE_DIR}/des.cpp"
 	"${COMMON_SOURCE_DIR}/ers.cpp"

+ 1 - 1
src/common/Makefile.in

@@ -1,7 +1,7 @@
 
 COMMON_OBJ = core.o socket.o timer.o db.o nullpo.o malloc.o showmsg.o strlib.o utils.o utilities.o \
 	grfio.o mapindex.o ers.o md5calc.o minicore.o minisocket.o minimalloc.o random.o des.o \
-	conf.o msg_conf.o cli.o sql.o
+	conf.o msg_conf.o cli.o sql.o database.o
 COMMON_DIR_OBJ = $(COMMON_OBJ:%=obj/%)
 COMMON_H = $(shell ls ../common/*.hpp)
 COMMON_AR = obj/common.a

+ 2 - 0
src/common/common.vcxproj

@@ -23,6 +23,7 @@
     <ClInclude Include="cli.hpp" />
     <ClInclude Include="conf.hpp" />
     <ClInclude Include="core.hpp" />
+    <ClInclude Include="database.hpp" />
     <ClInclude Include="db.hpp" />
     <ClInclude Include="des.hpp" />
     <ClInclude Include="ers.hpp" />
@@ -47,6 +48,7 @@
     <ClCompile Include="cli.cpp" />
     <ClCompile Include="conf.cpp" />
     <ClCompile Include="core.cpp" />
+    <ClCompile Include="database.cpp" />
     <ClCompile Include="db.cpp" />
     <ClCompile Include="des.cpp" />
     <ClCompile Include="ers.cpp" />

+ 6 - 0
src/common/common.vcxproj.filters

@@ -23,6 +23,9 @@
     <ClInclude Include="core.hpp">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="database.hpp">
+      <Filter>Header Files</Filter>
+    </ClInclude>
     <ClInclude Include="db.hpp">
       <Filter>Header Files</Filter>
     </ClInclude>
@@ -91,6 +94,9 @@
     <ClCompile Include="core.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="database.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="db.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>

+ 1 - 0
src/common/core.cpp

@@ -33,6 +33,7 @@ void (*shutdown_callback)(void) = NULL;
 
 int runflag = CORE_ST_RUN;
 char db_path[12] = "db"; /// relative path for db from server
+char conf_path[12] = "conf"; /// relative path for conf from server
 
 char *SERVER_NAME = NULL;
 char SERVER_TYPE = ATHENA_SERVER_NONE;

+ 1 - 0
src/common/core.hpp

@@ -22,6 +22,7 @@ extern char **arg_v;
 extern int runflag;
 extern char *SERVER_NAME;
 extern char db_path[12]; /// relative path for db from servers
+extern char conf_path[12]; /// relative path for conf from servers
 
 enum {
 	ATHENA_SERVER_NONE = 0,	// not defined

+ 238 - 0
src/common/database.cpp

@@ -0,0 +1,238 @@
+// Copyright (c) rAthena Dev Teams - Licensed under GNU GPL
+// For more information, see LICENCE in the main folder
+
+#include "database.hpp"
+
+#include "showmsg.hpp"
+
+bool YamlDatabase::nodeExists( const YAML::Node& node, const std::string& name ){
+	try{
+		if( node[name] ){
+			return true;
+		}else{
+			return false;
+		}
+	// TODO: catch( ... ) instead?
+	}catch( const YAML::InvalidNode& ){
+		return false;
+	}catch( const YAML::BadSubscript& ){
+		return false;
+	}
+}
+
+bool YamlDatabase::verifyCompatibility( const YAML::Node& rootNode ){
+	if( !this->nodeExists( rootNode, "Header" ) ){
+		ShowError( "No database \"Header\" was found.\n" );
+		return false;
+	}
+
+	const YAML::Node& headerNode = rootNode["Header"];
+
+	if( !this->nodeExists( headerNode, "Type" ) ){
+		ShowError( "No database \"Type\" was found.\n" );
+		return false;
+	}
+
+	const YAML::Node& typeNode = headerNode["Type"];
+	const std::string& tmpType = typeNode.as<std::string>();
+
+	if( tmpType != this->type ){
+		ShowError( "Database type mismatch: %s != %s.\n", this->type.c_str(), tmpType.c_str() );
+		return false;
+	}
+
+	uint16 tmpVersion;
+
+	if( !this->asUInt16( headerNode, "Version", tmpVersion ) ){
+		ShowError("Invalid header \"Version\" type for %s database.\n", this->type.c_str());
+		return false;
+	}
+
+	if( tmpVersion != this->version ){
+		if( tmpVersion > this->version ){
+			ShowError( "Database version %hu is not supported. Maximum version is: %hu\n", tmpVersion, this->version );
+			return false;
+		}else if( tmpVersion >= this->minimumVersion ){
+			ShowWarning( "Database version %hu is outdated and should be updated. Current version is: %hu\n", tmpVersion, this->minimumVersion );
+		}else{
+			ShowError( "Database version %hu is not supported anymore. Minimum version is: %hu\n", tmpVersion, this->minimumVersion );
+			return false;
+		}
+	}
+
+	return true;
+}
+
+bool YamlDatabase::load(){
+	return this->load( this->getDefaultLocation() );
+}
+
+bool YamlDatabase::load(const std::string& path) {
+	YAML::Node rootNode;
+
+	try {
+		rootNode = YAML::LoadFile(path);
+	}
+	catch(YAML::Exception &e) {
+		ShowError("Failed to read %s database file from '" CL_WHITE "%s" CL_RESET "'.\n", this->type.c_str(), path.c_str());
+		ShowError("%s (Line %d: Column %d)\n", e.msg.c_str(), e.mark.line, e.mark.column);
+		return false;
+	}
+
+	// Required here already for header error reporting
+	this->currentFile = path;
+
+	if (!this->verifyCompatibility(rootNode)){
+		ShowError("Failed to verify compatibility with %s database file from '" CL_WHITE "%s" CL_RESET "'.\n", this->type.c_str(), this->currentFile.c_str());
+		return false;
+	}
+
+	const YAML::Node& header = rootNode["Header"];
+
+	if( this->nodeExists( header, "Clear" ) ){
+		bool clear;
+
+		if( this->asBool( header, "Clear", clear ) && clear ){
+			this->clear();
+		}
+	}
+
+	this->parse( rootNode );
+
+	this->parseImports( rootNode );
+
+	return true;
+}
+
+void YamlDatabase::parse( const YAML::Node& rootNode ){
+	uint64 count = 0;
+
+	if( this->nodeExists( rootNode, "Body" ) ){
+		for( const YAML::Node &node : rootNode["Body"] ){
+			count += this->parseBodyNode( node );
+		}
+
+		ShowStatus("Done reading '" CL_WHITE "%" PRIu64 CL_RESET "' entries in '" CL_WHITE "%s" CL_RESET "'\n", count, this->currentFile.c_str());
+	}
+}
+
+void YamlDatabase::parseImports( const YAML::Node& rootNode ){
+	if( this->nodeExists( rootNode, "Footer" ) ){
+		const YAML::Node& footerNode = rootNode["Footer"];
+
+		if( this->nodeExists( footerNode, "Imports") ){
+			for( const YAML::Node& node : footerNode["Imports"] ){
+				std::string importFile;
+
+				if( !this->asString( node, "Path", importFile ) ){
+					continue;
+				}
+
+				if( this->nodeExists( node, "Mode" ) ){
+					std::string mode;
+
+					if( !this->asString( node, "Mode", mode ) ){
+						continue;
+					}
+
+#ifdef RENEWAL
+					std::string compiledMode = "Renewal";
+#else
+					std::string compiledMode = "Prerenewal";
+#endif
+
+					if( compiledMode != mode ){
+						// Skip this import
+						continue;
+					}
+				}				
+
+				this->load( importFile );
+			}
+		}
+	}
+}
+
+template <typename R> bool YamlDatabase::asType( const YAML::Node& node, const std::string& name, R& out ){
+	if( this->nodeExists( node, name ) ){
+		const YAML::Node& dataNode = node[name];
+
+		try {
+			R value = dataNode.as<R>();
+
+			out = value;
+
+			return true;
+		}catch( const YAML::BadConversion& ){
+			this->invalidWarning( dataNode, "Unable to parse \"%s\".\n", name.c_str() );
+			return false;
+		}
+	}else{
+		this->invalidWarning( node, "Missing node \"%s\".\n", name.c_str() );
+		return false;
+	}
+}
+
+bool YamlDatabase::asBool(const YAML::Node &node, const std::string &name, bool &out) {
+	return asType<bool>(node, name, out);
+}
+
+bool YamlDatabase::asInt16( const YAML::Node& node, const std::string& name, int16& out ){
+	return asType<int16>( node, name, out);
+}
+
+bool YamlDatabase::asUInt16(const YAML::Node& node, const std::string& name, uint16& out) {
+	return asType<uint16>(node, name, out);
+}
+
+bool YamlDatabase::asInt32(const YAML::Node &node, const std::string &name, int32 &out) {
+	return asType<int32>(node, name, out);
+}
+
+bool YamlDatabase::asUInt32(const YAML::Node &node, const std::string &name, uint32 &out) {
+	return asType<uint32>(node, name, out);
+}
+
+bool YamlDatabase::asInt64(const YAML::Node &node, const std::string &name, int64 &out) {
+	return asType<int64>(node, name, out);
+}
+
+bool YamlDatabase::asUInt64(const YAML::Node &node, const std::string &name, uint64 &out) {
+	return asType<uint64>(node, name, out);
+}
+
+bool YamlDatabase::asFloat(const YAML::Node &node, const std::string &name, float &out) {
+	return asType<float>(node, name, out);
+}
+
+bool YamlDatabase::asDouble(const YAML::Node &node, const std::string &name, double &out) {
+	return asType<double>(node, name, out);
+}
+
+bool YamlDatabase::asString(const YAML::Node &node, const std::string &name, std::string &out) {
+	return asType<std::string>(node, name, out);
+}
+
+void YamlDatabase::invalidWarning( const YAML::Node &node, const char* fmt, ... ){
+	va_list ap;
+
+	va_start(ap, fmt);
+
+	_vShowMessage( MSG_ERROR, fmt, ap );
+
+	va_end(ap);
+
+	ShowError( "Occurred in file '" CL_WHITE "%s" CL_RESET "' on line %d and column %d.\n", this->currentFile.c_str(), node.Mark().line + 1, node.Mark().column );
+
+#ifdef DEBUG
+	YAML::Emitter out;
+
+	out << node;
+
+	ShowMessage( "%s\n", out.c_str() );
+#endif
+}
+
+std::string YamlDatabase::getCurrentFile(){
+	return this->currentFile;
+}

+ 114 - 0
src/common/database.hpp

@@ -0,0 +1,114 @@
+// Copyright (c) rAthena Dev Teams - Licensed under GNU GPL
+// For more information, see LICENCE in the main folder
+
+#ifndef DATABASE_HPP
+#define DATABASE_HPP
+
+#include <unordered_map>
+
+#include <yaml-cpp/yaml.h>
+
+#include "../config/core.hpp"
+
+#include "cbasetypes.hpp"
+#include "core.hpp"
+
+class YamlDatabase{
+// Internal stuff
+private:
+	std::string type;
+	uint16 version;
+	uint16 minimumVersion;
+	std::string currentFile;
+
+	bool verifyCompatibility( const YAML::Node& rootNode );
+	bool load( const std::string& path );
+	void parse( const YAML::Node& rootNode );
+	void parseImports( const YAML::Node& rootNode );
+	template <typename R> bool asType( const YAML::Node& node, const std::string& name, R& out );
+
+// These should be visible/usable by the implementation provider
+protected:
+	// Helper functions
+	bool nodeExists( const YAML::Node& node, const std::string& name );
+	void invalidWarning( const YAML::Node &node, const char* fmt, ... );
+	std::string getCurrentFile();
+
+	// Conversion functions
+	bool asBool(const YAML::Node &node, const std::string &name, bool &out);
+	bool asInt16(const YAML::Node& node, const std::string& name, int16& out );
+	bool asUInt16(const YAML::Node& node, const std::string& name, uint16& out);
+	bool asInt32(const YAML::Node &node, const std::string &name, int32 &out);
+	bool asUInt32(const YAML::Node &node, const std::string &name, uint32 &out);
+	bool asInt64(const YAML::Node &node, const std::string &name, int64 &out);
+	bool asUInt64(const YAML::Node &node, const std::string &name, uint64 &out);
+	bool asFloat(const YAML::Node &node, const std::string &name, float &out);
+	bool asDouble(const YAML::Node &node, const std::string &name, double &out);
+	bool asString(const YAML::Node &node, const std::string &name, std::string &out);
+
+public:
+	YamlDatabase( const std::string type_, uint16 version_, uint16 minimumVersion_ ){
+		this->type = type_;
+		this->version = version_;
+		this->minimumVersion = minimumVersion_;
+	}
+
+	YamlDatabase( const std::string& type_, uint16 version_ ) : YamlDatabase( type_, version_, version_ ){
+		// Empty since everything is handled by the real constructor
+	}
+
+	bool load();
+
+	// Functions that need to be implemented for each type
+	virtual void clear() = 0;
+	virtual const std::string getDefaultLocation() = 0;
+	virtual uint64 parseBodyNode( const YAML::Node& node ) = 0;
+};
+
+template <typename keytype, typename datatype> class TypesafeYamlDatabase : public YamlDatabase{
+protected:
+	std::unordered_map<keytype, std::shared_ptr<datatype>> data;
+
+public:
+	TypesafeYamlDatabase( const std::string type_, uint16 version_, uint16 minimumVersion_ ) : YamlDatabase( type_, version_, minimumVersion_ ){
+	}
+
+	TypesafeYamlDatabase( const std::string& type_, uint16 version_ ) : YamlDatabase( type_, version_, version_ ){
+	}
+
+	void clear(){
+		this->data.clear();
+	}
+
+	bool exists( keytype key ){
+		return this->find( key ) != nullptr;
+	}
+
+	std::shared_ptr<datatype> find( keytype key ){
+		auto it = this->data.find( key );
+
+		if( it != this->data.end() ){
+			return it->second;
+		}else{
+			return nullptr;
+		}
+	}
+
+	void put( keytype key, std::shared_ptr<datatype> ptr ){
+		this->data[key] = ptr;
+	}
+
+	typename std::unordered_map<keytype, std::shared_ptr<datatype>>::iterator begin(){
+		return this->data.begin();
+	}
+
+	typename std::unordered_map<keytype, std::shared_ptr<datatype>>::iterator end(){
+		return this->data.end();
+	}
+
+	size_t size(){
+		return this->data.size();
+	}
+};
+
+#endif /* DATABASE_HPP */

+ 1 - 0
src/common/showmsg.hpp

@@ -86,6 +86,7 @@ enum msg_type {
 };
 
 extern void ClearScreen(void);
+extern int _vShowMessage(enum msg_type flag, const char *string, va_list ap);
 extern void ShowMessage(const char *, ...);
 extern void ShowStatus(const char *, ...);
 extern void ShowSQL(const char *, ...);

+ 31 - 0
src/common/utilities.hpp

@@ -43,6 +43,22 @@ namespace rathena {
 			}
 		}
 
+		/**
+		 * Find a key-value pair and return the key value as a reference
+		 * @param map: Map to search through
+		 * @param key: Key wanted
+		 * @return Key value on success or nullptr on failure
+		 */
+		template <typename K, typename V> std::shared_ptr<V> map_find( std::map<K,std::shared_ptr<V>>& map, K key ){
+			auto it = map.find( key );
+
+			if( it != map.end() ){
+				return it->second;
+			}else{
+				return nullptr;
+			}
+		}
+
 		/**
 		 * Get a key-value pair and return the key value
 		 * @param map: Map to search through
@@ -74,6 +90,21 @@ namespace rathena {
 				return nullptr;
 		}
 
+		/**
+		 * Find a key-value pair and return the key value as a reference
+		 * @param map: Unordered Map to search through
+		 * @param key: Key wanted
+		 * @return Key value on success or nullptr on failure
+		 */
+		template <typename K, typename V> std::shared_ptr<V> umap_find(std::unordered_map<K, std::shared_ptr<V>>& map, K key) {
+			auto it = map.find(key);
+
+			if (it != map.end())
+				return it->second;
+			else
+				return nullptr;
+		}
+
 		/**
 		 * Get a key-value pair and return the key value
 		 * @param map: Unordered Map to search through

+ 317 - 282
src/map/achievement.cpp

@@ -11,10 +11,12 @@
 #include <yaml-cpp/yaml.h>
 
 #include "../common/cbasetypes.hpp"
+#include "../common/database.hpp"
 #include "../common/malloc.hpp"
 #include "../common/nullpo.hpp"
 #include "../common/showmsg.hpp"
 #include "../common/strlib.hpp"
+#include "../common/utilities.hpp"
 #include "../common/utils.hpp"
 
 #include "battle.hpp"
@@ -28,42 +30,290 @@
 #include "script.hpp"
 #include "status.hpp"
 
-std::unordered_map<int, std::shared_ptr<s_achievement_db>> achievements;
-std::vector<int> achievement_mobs; // Avoids checking achievements on every mob killed
+using namespace rathena;
 
-/**
- * Searches an achievement by ID
- * @param achievement_id: ID to lookup
- * @return True if achievement exists or false if it doesn't
- */
-bool achievement_exists(int achievement_id)
-{
-	return achievements.find(achievement_id) != achievements.end();
+void AchievementDatabase::clear(){
+	TypesafeYamlDatabase::clear();
+	this->achievement_mobs.clear();
+}
+
+const std::string AchievementDatabase::getDefaultLocation(){
+	return std::string(db_path) + "/achievement_db.yml";
 }
 
 /**
- * Return an achievement by ID
- * @param achievement_id: ID to lookup
- * @return shared_ptr of achievement
+ * Reads and parses an entry from the achievement_db.
+ * @param node: YAML node containing the entry.
+ * @return count of successfully parsed rows
  */
-std::shared_ptr<s_achievement_db> &achievement_get(int achievement_id) 
-{
-	return achievements[achievement_id];
+uint64 AchievementDatabase::parseBodyNode(const YAML::Node &node){
+	uint32 achievement_id;
+
+	// TODO: doesnt match camel case
+	if( !this->asUInt32( node, "ID", achievement_id ) ){
+		return 0;
+	}
+
+	std::shared_ptr<s_achievement_db> achievement = this->find( achievement_id );
+	bool exists = achievement != nullptr;
+
+	if( !exists ){
+		if( !this->nodeExists( node, "Group" ) ){
+			return 0;
+		}
+
+		if( !this->nodeExists( node, "Name" ) ){
+			return 0;
+		}
+
+		achievement = std::make_shared<s_achievement_db>();
+		achievement->achievement_id = achievement_id;
+	}
+
+	if( this->nodeExists( node, "Group" ) ){
+		std::string group_name;
+
+		if( !this->asString( node, "Group", group_name ) ){
+			return 0;
+		}
+
+		int constant;
+
+		if( !script_get_constant( group_name.c_str(), &constant ) ){
+			this->invalidWarning( node, "achievement_read_db_sub: Invalid group %s for achievement %d, skipping.\n", group_name.c_str(), achievement_id );
+			return 0;
+		}
+
+		achievement->group = (e_achievement_group)constant;
+	}
+
+	if( this->nodeExists( node, "Name" ) ){
+		std::string name;
+
+		if( !this->asString( node, "Name", name ) ){
+			return 0;
+		}
+
+		achievement->name = name;
+	}
+
+	if( this->nodeExists( node, "Target" ) ){
+		const YAML::Node& targets = node["Target"];
+
+		for( const YAML::Node& targetNode : targets ){
+			if( achievement->targets.size() >= MAX_ACHIEVEMENT_OBJECTIVES ){
+				this->invalidWarning( targetNode, "Node \"Target\" list exceeds the maximum of %d, skipping.\n", MAX_ACHIEVEMENT_OBJECTIVES );
+				return 0;
+			}
+
+			uint16 targetId;
+
+			if( !this->asUInt16( targetNode, "Id", targetId ) ){
+				continue;
+			}
+
+			if( targetId >= MAX_ACHIEVEMENT_OBJECTIVES ){
+				this->invalidWarning( targetNode["Id"], "Node \"Id\" is out of valid range [0,%d], skipping.\n", MAX_ACHIEVEMENT_OBJECTIVES );
+				return 0;
+			}
+
+			std::shared_ptr<achievement_target> target = rathena::util::umap_find( achievement->targets, targetId );
+			bool targetExists = target != nullptr;
+
+			if( !targetExists ){
+				if( !this->nodeExists( targetNode, "Count" ) && !this->nodeExists( targetNode, "MobID" ) ){
+					this->invalidWarning( targetNode, "Node \"Target\" has no data specified, skipping.\n" );
+					return 0;
+				}
+
+				target = std::make_shared<achievement_target>();
+			}
+			
+			if( this->nodeExists( targetNode, "Count" ) ){
+				uint32 count;
+
+				if( !this->asUInt32( targetNode, "Count", count ) ){
+					return 0;
+				}
+
+				target->count = count;
+			}else{
+				if( !targetExists ){
+					target->count = 0;
+				}
+			}
+
+			if( this->nodeExists( targetNode, "MobID" ) ){
+				if( achievement->group != AG_BATTLE && achievement->group != AG_TAMING ){
+					this->invalidWarning( targets, "Node \"MobID\" is only supported for targets in group AG_BATTLE or AG_TAMING, skipping.\n" );
+					return 0;
+				}
+
+				uint32 mob_id;
+
+				// TODO: not camel case
+				if( !this->asUInt32( targetNode, "MobID", mob_id ) ){
+					return 0;
+				}
+
+				if( mob_db( mob_id ) == nullptr ){
+					this->invalidWarning( targetNode["MobID"], "Unknown monster ID %d, skipping.\n", mob_id );
+					return 0;
+				}
+
+				if( !this->mobexists( mob_id ) ){
+					this->achievement_mobs.push_back( mob_id );
+				}
+			}else{
+				if( !targetExists ){
+					target->mob = 0;
+				}
+			}
+
+			achievement->targets[targetId] = target;
+		}
+	}
+
+	if( this->nodeExists( node, "Condition" ) ){
+		std::string condition;
+
+		if( !this->asString( node, "Condition", condition ) ){
+			return 0;
+		}
+
+		if( condition.find( "achievement_condition" ) == std::string::npos ){
+			condition = "achievement_condition( " + condition + " );";
+		}
+
+		achievement->condition = parse_script( condition.c_str(), this->getCurrentFile().c_str(), node["Condition"].Mark().line + 1, SCRIPT_IGNORE_EXTERNAL_BRACKETS );
+	}
+
+	if( this->nodeExists( node, "Map" ) ){
+		if( achievement->group != AG_CHAT ){
+			this->invalidWarning( node, "Node \"Map\" can only be used with the group AG_CHATTING, skipping.\n" );
+			return 0;
+		}
+
+		std::string mapname;
+
+		if( !this->asString( node, "Map", mapname ) ){
+			return 0;
+		}
+
+		achievement->mapindex = map_mapname2mapid( mapname.c_str() );
+
+		if( achievement->mapindex == -1 ){
+			this->invalidWarning( node["Map"], "Unknown map name '%s'.\n", mapname.c_str() );
+			return 0;
+		}
+	}else{
+		if( !exists ){
+			achievement->mapindex = -1;
+		}
+	}
+
+	if( this->nodeExists( node, "Dependent" ) ){
+		for( const YAML::Node& subNode : node["Dependent"] ){
+			uint32 dependent_achievement_id;
+
+			if( !this->asUInt32( subNode, "Id", dependent_achievement_id ) ){
+				return 0;
+			}
+
+			// TODO: import logic for clearing => continue
+			// TODO: change to set to prevent multiple entries with the same id?
+			achievement->dependent_ids.push_back( dependent_achievement_id );
+		}
+	}
+
+	// TODO: not plural
+	if( this->nodeExists( node, "Reward" ) ){
+		const YAML::Node& rewardNode = node["Reward"];
+
+		// TODO: not camel case
+		if( this->nodeExists( rewardNode, "ItemID" ) ){
+			uint16 itemId;
+
+			if( !this->asUInt16( rewardNode, "ItemID", itemId ) ){
+				return 0;
+			}
+
+			if( !itemdb_exists( itemId ) ){
+				this->invalidWarning( rewardNode["ItemID"], "Unknown item with ID %hu.\n", itemId );
+				return 0;
+			}
+
+			achievement->rewards.nameid = itemId;
+
+			if( achievement->rewards.amount == 0 ){
+				// Default the amount to 1
+				achievement->rewards.amount = 1;
+			}
+		}
+
+		if( this->nodeExists( rewardNode, "Amount" ) ){
+			uint16 amount;
+
+			if( !this->asUInt16( rewardNode, "Amount", amount ) ){
+				return 0;
+			}
+
+			achievement->rewards.nameid = amount;
+		}
+
+		if( this->nodeExists( rewardNode, "Script" ) ){
+			std::string script;
+
+			if( !this->asString( rewardNode, "Script", script ) ){
+				return 0;
+			}
+
+			achievement->rewards.script = parse_script( script.c_str(), this->getCurrentFile().c_str(), achievement_id, SCRIPT_IGNORE_EXTERNAL_BRACKETS );
+		}
+
+		// TODO: not camel case
+		if( this->nodeExists( rewardNode, "TitleID" ) ){
+			uint32 title;
+
+			if( !this->asUInt32( rewardNode, "TitleID", title ) ){
+				return 0;
+			}
+
+			achievement->rewards.title_id = title;
+		}
+	}
+
+	if( this->nodeExists( node, "Score" ) ){
+		uint32 score;
+
+		if( !this->asUInt32( node, "Score", score ) ){
+			return 0;
+		}
+
+		achievement->score = score;
+	}
+
+	if( !exists ){
+		this->put( achievement_id, achievement );
+	}
+
+	return 1;
 }
 
+AchievementDatabase achievement_db;
+
 /**
  * Searches for an achievement by monster ID
  * @param mob_id: Monster ID to lookup
  * @return True on success, false on failure
  */
-bool achievement_mobexists(int mob_id)
-{
+bool AchievementDatabase::mobexists( uint32 mob_id ){
 	if (!battle_config.feature_achievement)
 		return false;
 
-	auto it = std::find(achievement_mobs.begin(), achievement_mobs.end(), mob_id);
+	auto it = std::find(this->achievement_mobs.begin(), this->achievement_mobs.end(), mob_id);
 
-	return (it != achievement_mobs.end()) ? true : false;
+	return (it != this->achievement_mobs.end()) ? true : false;
 }
 
 /**
@@ -78,12 +328,12 @@ struct achievement *achievement_add(struct map_session_data *sd, int achievement
 
 	nullpo_retr(NULL, sd);
 
-	if (!achievement_exists(achievement_id)) {
-		ShowError("achievement_add: Achievement %d not found in DB.\n", achievement_id);
-		return NULL;
-	}
+	std::shared_ptr<s_achievement_db> adb = achievement_db.find( achievement_id );
 
-	auto &adb = achievements[achievement_id];
+	if( adb == nullptr ){
+		ShowError( "achievement_add: Achievement %d not found in DB.\n", achievement_id );
+		return nullptr;
+	}
 
 	ARR_FIND(0, sd->achievement_data.count, i, sd->achievement_data.achievements[i].achievement_id == achievement_id);
 	if (i < sd->achievement_data.count) {
@@ -125,7 +375,7 @@ bool achievement_remove(struct map_session_data *sd, int achievement_id)
 
 	nullpo_retr(false, sd);
 
-	if (!achievement_exists(achievement_id)) {
+	if (!achievement_db.exists(achievement_id)) {
 		ShowError("achievement_delete: Achievement %d not found in DB.\n", achievement_id);
 		return false;
 	}
@@ -184,10 +434,11 @@ bool achievement_check_dependent(struct map_session_data *sd, int achievement_id
 {
 	nullpo_retr(false, sd);
 
-	if (!achievement_exists(achievement_id))
-		return false;
+	std::shared_ptr<s_achievement_db> adb = achievement_db.find( achievement_id );
 
-	auto &adb = achievements[achievement_id];
+	if( adb == nullptr ){
+		return false;
+	}
 
 	// Check if the achievement has a dependent
 	// If so, then do a check on all dependents to see if they're complete
@@ -242,10 +493,11 @@ bool achievement_update_achievement(struct map_session_data *sd, int achievement
 
 	nullpo_retr(false, sd);
 
-	if (!achievement_exists(achievement_id))
-		return false;
+	std::shared_ptr<s_achievement_db> adb = achievement_db.find( achievement_id );
 
-	auto &adb = achievements[achievement_id];
+	if( adb == nullptr ){
+		return false;
+	}
 
 	ARR_FIND(0, sd->achievement_data.incompleteCount, i, sd->achievement_data.achievements[i].achievement_id == achievement_id);
 	if (i == sd->achievement_data.incompleteCount)
@@ -260,7 +512,7 @@ bool achievement_update_achievement(struct map_session_data *sd, int achievement
 			int k;
 
 			for (k = 0; k < adb->targets.size(); k++)
-				sd->achievement_data.achievements[i].count[k] = adb->targets[k].count;
+				sd->achievement_data.achievements[i].count[k] = adb->targets[k]->count;
 
 			for (k = 1; k < adb->dependent_ids.size(); k++) {
 				sd->achievement_data.achievements[i].count[k] = max(1, sd->achievement_data.achievements[i].count[k]);
@@ -279,7 +531,7 @@ bool achievement_update_achievement(struct map_session_data *sd, int achievement
 
 		achievement_level(sd, true); // Re-calculate achievement level
 		// Check dependents
-		for (auto &ach : achievements)
+		for (auto &ach : achievement_db)
 			achievement_check_groups(sd, ach.second.get());
 		ARR_FIND(sd->achievement_data.incompleteCount, sd->achievement_data.count, i, sd->achievement_data.achievements[i].achievement_id == achievement_id); // Look for the index again, the position most likely changed
 	}
@@ -301,13 +553,13 @@ void achievement_get_reward(struct map_session_data *sd, int achievement_id, tim
 
 	nullpo_retv(sd);
 
-	if (!achievement_exists(achievement_id)) {
-		ShowError("achievement_reward: Inter server sent a reward claim for achievement %d not found in DB.\n", achievement_id);
+	std::shared_ptr<s_achievement_db> adb = achievement_db.find( achievement_id );
+
+	if( adb == nullptr ){
+		ShowError( "achievement_reward: Inter server sent a reward claim for achievement %d not found in DB.\n", achievement_id );
 		return;
 	}
 
-	auto &adb = achievements[achievement_id];
-
 	if (rewarded == 0) {
 		clif_achievement_reward_ack(sd->fd, 0, achievement_id);
 		return;
@@ -342,14 +594,14 @@ void achievement_check_reward(struct map_session_data *sd, int achievement_id)
 
 	nullpo_retv(sd);
 
-	if (!achievement_exists(achievement_id)) {
-		ShowError("achievement_reward: Trying to reward achievement %d not found in DB.\n", achievement_id);
-		clif_achievement_reward_ack(sd->fd, 0, achievement_id);
+	std::shared_ptr<s_achievement_db> adb = achievement_db.find( achievement_id );
+
+	if( adb == nullptr ){
+		ShowError( "achievement_reward: Trying to reward achievement %d not found in DB.\n", achievement_id );
+		clif_achievement_reward_ack( sd->fd, 0, achievement_id );
 		return;
 	}
 
-	auto &adb = achievements[achievement_id];
-
 	ARR_FIND(0, sd->achievement_data.count, i, sd->achievement_data.achievements[i].achievement_id == achievement_id);
 	if (i == sd->achievement_data.count) {
 		clif_achievement_reward_ack(sd->fd, 0, achievement_id);
@@ -379,13 +631,12 @@ void achievement_get_titles(uint32 char_id)
 
 		if (sd->achievement_data.count) {
 			for (int i = 0; i < sd->achievement_data.count; i++) {
-				if (!achievement_exists(sd->achievement_data.achievements[i].achievement_id))
-					continue;
-
-				auto &adb = achievements[sd->achievement_data.achievements[i].achievement_id];
+				std::shared_ptr<s_achievement_db> adb = achievement_db.find( sd->achievement_data.achievements[i].achievement_id );
 
-				if (adb && adb->rewards.title_id && sd->achievement_data.achievements[i].completed > 0) // If the achievement has a title and is complete, give it to the player
-					sd->titles.push_back(adb->rewards.title_id);
+				// If the achievement has a title and is complete, give it to the player
+				if( adb != nullptr && adb->rewards.title_id && sd->achievement_data.achievements[i].completed > 0 ){
+					sd->titles.push_back( adb->rewards.title_id );
+				}
 			}
 		}
 	}
@@ -616,13 +867,13 @@ static int achievement_update_objectives(struct map_session_data *sd, std::share
 				break;
 
 			for (i = 0; i < ad->targets.size(); i++) {
-				if (objective_count[i] < ad->targets[i].count)
+				if (objective_count[i] < ad->targets[i]->count)
 					objective_count[i] += update_count[0];
 			}
 
 			changed = true;
 
-			ARR_FIND(0, ad->targets.size(), k, objective_count[k] < ad->targets[k].count);
+			ARR_FIND(0, ad->targets.size(), k, objective_count[k] < ad->targets[k]->count);
 			if (k == ad->targets.size())
 				complete = true;
 
@@ -634,20 +885,20 @@ static int achievement_update_objectives(struct map_session_data *sd, std::share
 		case AG_BATTLE:
 		case AG_TAMING:
 			auto it = std::find_if(ad->targets.begin(), ad->targets.end(), [&update_count]
-			(const achievement_target &curTarget) {
-				return curTarget.mob == update_count[0];
+			(std::pair<const uint16, std::shared_ptr<achievement_target>> &curTarget) {
+				return curTarget.second->mob == update_count[0];
 			});
 			if (it == ad->targets.end())
 				break; // Mob wasn't found
 
 			for (k = 0; k < ad->targets.size(); k++) {
-				if (ad->targets[k].mob == update_count[0] && objective_count[k] < ad->targets[k].count) {
+				if (ad->targets[k]->mob == update_count[0] && objective_count[k] < ad->targets[k]->count) {
 					objective_count[k]++;
 					changed = true;
 				}
 			}
 
-			ARR_FIND(0, ad->targets.size(), k, objective_count[k] < ad->targets[k].count);
+			ARR_FIND(0, ad->targets.size(), k, objective_count[k] < ad->targets[k]->count);
 			if (k == ad->targets.size())
 				complete = true;
 
@@ -701,7 +952,7 @@ void achievement_update_objective(struct map_session_data *sd, enum e_achievemen
 				// These have no objective use right now.
 				break;
 			default:
-				for (auto &ach : achievements)
+				for (auto &ach : achievement_db)
 					achievement_update_objectives(sd, ach.second, group, count);
 				break;
 		}
@@ -715,237 +966,23 @@ void achievement_update_objective(struct map_session_data *sd, enum e_achievemen
 	}
 }
 
-static void yaml_invalid_warning(const char* fmt, const YAML::Node &node, const std::string &file) {
-	YAML::Emitter out;
-	out << node;
-	ShowWarning(fmt, file.c_str());
-	ShowMessage("%s\n", out.c_str());
-}
-
-/**
- * Reads and parses an entry from the achievement_db.
- * @param node: YAML node containing the entry.
- * @param n: The sequential index of the current entry.
- * @param source: The source YAML file.
- * @return True on successful parse or false otherwise
- */
-bool achievement_read_db_sub(const YAML::Node &node, int n, const std::string &source)
-{
-	enum e_achievement_group group = AG_NONE;
-	int achievement_id = 0;
-	std::string group_char, name, condition, mapname;
-	bool existing = false;
-
-	if (!node["ID"]) {
-		yaml_invalid_warning("achievement_read_db_sub: Missing ID field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-		return false;
-	}
-	try {
-		achievement_id = node["ID"].as<unsigned int>();
-	} catch (...) {
-		yaml_invalid_warning("achievement_read_db_sub: Achievement definition with invalid ID field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-		return false;
-	}
-	if (achievement_id < 1 || achievement_id > INT_MAX) {
-		ShowWarning("achievement_read_db_sub: Invalid achievement ID %d in \"%s\", entry #%d (min: 1, max: %d), skipping.\n", achievement_id, source.c_str(), n, INT_MAX);
-		return false;
-	}
-
-	if (achievement_exists(achievement_id)) {
-		if (source.find("import") != std::string::npos) // Import file read-in, free previous value and store new value
-			existing = true;
-		else { // Normal file read-in
-			ShowWarning("achievement_read_db: Duplicate achievement %d.\n", achievement_id);
-			return false;
-		}
-	}
-
-	if(!existing)
-		achievements[achievement_id] = std::make_shared<s_achievement_db>();
-	auto &entry = achievements[achievement_id];
-	entry->achievement_id = achievement_id;
-
-	if (!node["Group"]) {
-		yaml_invalid_warning("achievement_read_db_sub: Missing group field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-		return false;
-	}
-	try {
-		group_char = node["Group"].as<std::string>();
-	} catch (...) {
-		yaml_invalid_warning("achievement_read_db_sub: Achievement definition with invalid group field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-		return false;
-	}
-	if (!script_get_constant(group_char.c_str(), (int *)&group)) {
-		ShowWarning("achievement_read_db_sub: Invalid group %s for achievement %d in \"%s\", skipping.\n", group_char.c_str(), achievement_id, source.c_str());
-		return false;
-	}
-
-	if (!node["Name"]) {
-		ShowWarning("achievement_read_db_sub: Missing achievement name for achievement %d in \"%s\", skipping.\n", achievement_id, source.c_str());
-		return false;
-	}
-	try {
-		name = node["Name"].as<std::string>();
-	} catch (...) {
-		yaml_invalid_warning("achievement_read_db_sub: Achievement definition with invalid name field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-		return false;
-	}
-	
-	entry->group = group;
-	entry->name = name;
-	entry->mapindex = -1;
-
-	if (node["Target"]) {
-		try {
-			const YAML::Node &target_list = node["Target"];
-
-			for (auto targetit = target_list.begin(); targetit != target_list.end() && target_list.size() < MAX_ACHIEVEMENT_OBJECTIVES; ++targetit) {
-				const YAML::Node &target = *targetit;
-				int mobid = 0, count = 0;
-
-				if (target["MobID"] && (mobid = target["MobID"].as<int>()) && mob_db(mobid) == NULL) { // The mob ID field is not required
-					ShowError("achievement_read_db_sub: Invalid mob ID %d for achievement %d in \"%s\", skipping.\n", mobid, achievement_id, source.c_str());
-					continue;
-				}
-				if (target["Count"] && (!(count = target["Count"].as<int>()) || count <= 0)) {
-					ShowError("achievement_read_db_sub: Invalid count %d for achievement %d in \"%s\", skipping.\n", count, achievement_id, source.c_str());
-					continue;
-				}
-				if (mobid && group == AG_BATTLE && !achievement_mobexists(mobid))
-					achievement_mobs.push_back(mobid);
-
-				entry->targets.push_back({ mobid, count });
-			}
-		} catch (...) {
-			yaml_invalid_warning("achievement_read_db_sub: Achievement definition with invalid target field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-			return false;
-		}
-	}
-
-	if (node["Condition"]) {
-		try {
-			condition = node["Condition"].as<std::string>();
-		} catch (...) {
-			yaml_invalid_warning("achievement_read_db_sub: Achievement definition with invalid condition field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-			return false;
-		}
-
-		if( condition.find( "achievement_condition" ) == std::string::npos ){
-			condition = "achievement_condition( " + condition + " );";
-		}
-
-		entry->condition = parse_script( condition.c_str(), source.c_str(), node["Condition"].Mark().line, SCRIPT_IGNORE_EXTERNAL_BRACKETS );
-	}
-	
-	if (node["Map"]) {
-		try {
-			mapname = node["Map"].as<std::string>();
-		} catch (...) {
-			yaml_invalid_warning("achievement_read_db_sub: Achievement definition with invalid map field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-			return false;
-		}
-		if (group != AG_CHAT)
-			ShowWarning("achievement_read_db_sub: The map argument can only be used with the group AG_CHATTING (achievement %d in \"%s\"), skipping.\n", achievement_id, source.c_str());
-		else {
-			entry->mapindex = map_mapname2mapid(mapname.c_str());
-
-			if (entry->mapindex == -1)
-				ShowWarning("achievement_read_db_sub: Invalid map name %s for achievement %d in \"%s\".\n", mapname.c_str(), achievement_id, source.c_str());
-		}
-	}
-
-	if (node["Dependent"]) {
-		try {
-			const YAML::Node dependent_list = node["Dependent"];
-
-			if (dependent_list.IsSequence()) {
-				for (uint8 i = 0; i < dependent_list.size() && dependent_list.size() < MAX_ACHIEVEMENT_DEPENDENTS; i++)
-					entry->dependent_ids.push_back(dependent_list[i].as<int>());
-			} else
-				ShowWarning("achievement_read_db_sub: Invalid dependent format for achievement %d in \"%s\".\n", achievement_id, source.c_str());
-		} catch (...) {
-			yaml_invalid_warning("achievement_read_db_sub: Achievement definition with invalid dependent field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-			return false;
-		}
-	}
-
-	if (node["Reward"]) {
-		try {
-			const YAML::Node reward_list = node["Reward"];
-			int nameid = 0, amount = 0, titleid = 0;
-
-			if (reward_list["ItemID"] && (nameid = reward_list["ItemID"].as<unsigned short>())) {
-				if (itemdb_exists(nameid)) {
-					entry->rewards.nameid = nameid;
-					entry->rewards.amount = 1; // Default the amount to 1
-				} else if (nameid && !itemdb_exists(nameid)) {
-					ShowWarning("achievement_read_db_sub: Invalid reward item ID %hu for achievement %d in \"%s\". Setting to 0.\n", nameid, achievement_id, source.c_str());
-					entry->rewards.nameid = nameid = 0;
-				}
-
-				if (reward_list["Amount"] && (amount = reward_list["Amount"].as<unsigned short>()) && amount > 0 && nameid > 0)
-					entry->rewards.amount = amount;
-			}
-			if (reward_list["Script"])
-				entry->rewards.script = parse_script(reward_list["Script"].as<std::string>().c_str(), source.c_str(), achievement_id, SCRIPT_IGNORE_EXTERNAL_BRACKETS);
-			if (reward_list["TitleID"] && (titleid = reward_list["TitleID"].as<int>()) && titleid > 0)
-				entry->rewards.title_id = titleid;
-		} catch (...) {
-			yaml_invalid_warning("achievement_read_db_sub: Achievement definition with invalid target field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-			return false;
-		}
-	}
-
-	if (node["Score"]) {
-		try {
-			entry->score = node["Score"].as<int>();
-		} catch (...) {
-			yaml_invalid_warning("achievement_read_db_sub: Achievement definition with invalid score field in '" CL_WHITE "%s" CL_RESET "', skipping.\n", node, source);
-			return false;
-		}
-	}
-
-	return true;
-}
-
 /**
  * Loads achievements from the achievement db.
  */
 void achievement_read_db(void)
-{
-	std::vector<std::string> directories = { std::string(db_path) + "/" + std::string(DBPATH),  std::string(db_path) + "/" + std::string(DBIMPORT) + "/" };
-	static const std::string file_name("achievement_db.yml");
-
-	for (auto &directory : directories) {
-		std::string current_file = directory + file_name;
-		YAML::Node config;
-		int count = 0;
-
-		try {
-			config = YAML::LoadFile(current_file);
-		} catch (...) {
-			ShowError("Cannot read '" CL_WHITE "%s" CL_RESET "'.\n", current_file.c_str());
-			return;
-		}
+{	
+	achievement_db.load();
 
-		for (const auto &node : config["Achievements"]) {
-			if (node.IsDefined() && achievement_read_db_sub(node, count, current_file))
-				count++;
-		}
-		for (auto &achit : achievements) {
-			const auto ach = achit.second;
+	for (auto &achit : achievement_db) {
+		const auto ach = achit.second;
 
-			for (int i = 0; i < ach->dependent_ids.size(); i++) {
-				if (!achievement_exists(ach->dependent_ids[i])) {
-					ShowWarning("achievement_read_db: An invalid Dependent ID %d was given for Achievement %d. Removing from list.\n", ach->dependent_ids[i], ach->achievement_id);
-					ach->dependent_ids.erase(ach->dependent_ids.begin() + i);
-				}
+		for (int i = 0; i < ach->dependent_ids.size(); i++) {
+			if (!achievement_db.exists(ach->dependent_ids[i])) {
+				ShowWarning("achievement_read_db: An invalid Dependent ID %d was given for Achievement %d. Removing from list.\n", ach->dependent_ids[i], ach->achievement_id);
+				ach->dependent_ids.erase(ach->dependent_ids.begin() + i);
 			}
 		}
-		ShowStatus("Done reading '" CL_WHITE "%d" CL_RESET "' entries in '" CL_WHITE "%s" CL_RESET "'\n", count, current_file.c_str());
 	}
-
-	return;
 }
 
 /**
@@ -972,10 +1009,8 @@ void do_init_achievement(void)
 /**
  * Finalizes the achievement database
  */
-void do_final_achievement(void)
-{
-	achievement_mobs.clear();
-	achievements.clear();
+void do_final_achievement(void){
+	achievement_db.clear();
 }
 
 /**

+ 25 - 7
src/map/achievement.hpp

@@ -11,6 +11,7 @@
 #include <vector>
 
 #include "../common/mmo.hpp"
+#include "../common/database.hpp"
 #include "../common/db.hpp"
 
 struct map_session_data;
@@ -69,17 +70,17 @@ struct achievement_target {
 };
 
 struct s_achievement_db {
-	int achievement_id;
+	uint32 achievement_id;
 	std::string name;
 	enum e_achievement_group group;
-	std::vector <achievement_target> targets;
-	std::vector <int> dependent_ids;
+	std::unordered_map<uint16, std::shared_ptr<achievement_target>> targets;
+	std::vector<uint32> dependent_ids;
 	struct script_code* condition;
 	int16 mapindex;
 	struct ach_reward {
 		unsigned short nameid, amount;
 		struct script_code *script;
-		int title_id;
+		uint32 title_id;
 		ach_reward();
 		~ach_reward();
 	} rewards;
@@ -90,9 +91,26 @@ struct s_achievement_db {
 	~s_achievement_db();
 };
 
-bool achievement_exists(int achievement_id);
-std::shared_ptr<s_achievement_db>& achievement_get(int achievement_id);
-bool achievement_mobexists(int mob_id);
+class AchievementDatabase : public TypesafeYamlDatabase<uint32, s_achievement_db>{
+private:
+	// Avoids checking achievements on every mob killed
+	std::vector<uint32> achievement_mobs;
+
+public:
+	AchievementDatabase() : TypesafeYamlDatabase( "ACHIEVEMENT_DB", 1 ){
+
+	}
+
+	void clear();
+	const std::string getDefaultLocation();
+	uint64 parseBodyNode( const YAML::Node& node );
+
+	// Additional
+	bool mobexists(uint32 mob_id);
+};
+
+extern AchievementDatabase achievement_db;
+
 void achievement_get_reward(struct map_session_data *sd, int achievement_id, time_t rewarded);
 struct achievement *achievement_add(struct map_session_data *sd, int achievement_id);
 bool achievement_remove(struct map_session_data *sd, int achievement_id);

+ 3 - 3
src/map/intif.cpp

@@ -2150,14 +2150,14 @@ void intif_parse_achievements(int fd)
 			CREATE(sd->achievement_data.achievements, struct achievement, num_received);
 
 		for (i = 0; i < num_received; i++) {
+			std::shared_ptr<s_achievement_db> adb = achievement_db.find( received[i].achievement_id );
 
-			if (!achievement_exists(received[i].achievement_id)) {
+
+			if( adb == nullptr ){
 				ShowError("intif_parse_achievementlog: Achievement %d not found in DB.\n", received[i].achievement_id);
 				continue;
 			}
 
-			auto &adb = achievement_get(received[i].achievement_id);
-
 			received[i].score = adb->score;
 
 			if (received[i].completed == 0) // Insert at the beginning

+ 1 - 1
src/map/mob.cpp

@@ -2976,7 +2976,7 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type)
 			else if (sd->avail_quests)
 				quest_update_objective(sd, md->mob_id);
 
-			if (achievement_mobexists(md->mob_id))
+			if (achievement_db.mobexists(md->mob_id))
 				achievement_update_objective(sd, AG_BATTLE, 1, md->mob_id);
 
 			if (sd->md && src && src->type == BL_MER && mob_db(md->mob_id)->lv > sd->status.base_level / 2)

+ 203 - 159
src/map/pc.cpp

@@ -12,6 +12,7 @@
 
 #include "../common/cbasetypes.hpp"
 #include "../common/core.hpp" // get_svn_revision()
+#include "../common/database.hpp"
 #include "../common/ers.hpp"  // ers_destroy
 #include "../common/malloc.hpp"
 #include "../common/mmo.hpp" //NAME_LENGTH
@@ -97,10 +98,197 @@ struct s_attendance_reward{
 struct s_attendance_period{
 	uint32 start;
 	uint32 end;
-	std::map<int,struct s_attendance_reward> rewards;
+	std::map<uint32,std::shared_ptr<struct s_attendance_reward>> rewards;
 };
 
-std::vector<struct s_attendance_period> attendance_periods;
+class AttendanceDatabase : public TypesafeYamlDatabase<uint32,s_attendance_period>{
+public:
+	AttendanceDatabase() : TypesafeYamlDatabase( "ATTENDANCE_DB", 1 ){
+
+	}
+
+	const std::string getDefaultLocation();
+	uint64 parseBodyNode( const YAML::Node& node );
+};
+
+const std::string AttendanceDatabase::getDefaultLocation(){
+	return std::string(db_path) + "/attendance.yml";
+}
+
+/**
+ * Reads and parses an entry from the attendance_db.
+ * @param node: YAML node containing the entry.
+ * @return count of successfully parsed rows
+ */
+uint64 AttendanceDatabase::parseBodyNode(const YAML::Node &node){
+	uint32 start;
+
+	if( !this->asUInt32( node, "Start", start ) ){
+		return 0;
+	}
+
+	std::shared_ptr<s_attendance_period> attendance_period = this->find( start );
+	bool exists = attendance_period != nullptr;
+
+	if( !exists ){
+		if( !this->nodeExists( node, "End" ) ){
+			this->invalidWarning( node, "Node \"End\" is missing.\n" );
+			return 0;
+		}
+
+		if( !this->nodeExists( node, "Rewards" ) ){
+			this->invalidWarning( node, "Node \"Rewards\" is missing.\n" );
+			return 0;
+		}
+
+		attendance_period = std::make_shared<s_attendance_period>();
+
+		attendance_period->start = start;
+	}
+
+	// If it does not exist yet, we need to check it for sure
+	bool requiresCollisionDetection = !exists;
+
+	if( this->nodeExists( node, "End" ) ){
+		uint32 end;
+
+		if( !this->asUInt32( node, "End", end ) ){
+			return 0;
+		}
+
+		// If the period is outdated already, we do not even bother parsing
+		if( end < date_get( DT_YYYYMMDD ) ){
+			this->invalidWarning( node, "Node \"End\" date %u has already passed, skipping.\n", end );
+			return 0;
+		}
+
+		if( !exists || attendance_period->end != end ){
+			requiresCollisionDetection = true;
+			attendance_period->end = end;
+		}
+	}
+
+	// Collision detection
+	if( requiresCollisionDetection ){
+		bool collision = false;
+
+		for( std::pair<const uint32,std::shared_ptr<s_attendance_period>>& pair : *this ){
+			std::shared_ptr<s_attendance_period> period = pair.second;
+
+			if( exists && period->start == attendance_period->start ){
+				// Dont compare to yourself
+				continue;
+			}
+
+			// Check if start is inside another period
+			if( period->start <= attendance_period->start && start <= period->end ){
+				this->invalidWarning( node, "Node \"Start\" period %u intersects with period %u-%u, skipping.\n", attendance_period->start, period->start, period->end );
+				collision = true;
+				break;
+			}
+
+			// Check if end is inside another period
+			if( period->start <= attendance_period->end && attendance_period->end <= period->end ){
+				this->invalidWarning( node, "Node \"End\" period %u intersects with period %u-%u.\n", attendance_period->start, period->start, period->end );
+				collision = true;
+				break;
+			}
+		}
+
+		if( collision ){
+			return 0;
+		}
+	}
+
+	if( this->nodeExists( node, "Rewards" ) ){
+		const YAML::Node& rewardsNode = node["Rewards"];
+
+		for( const YAML::Node& rewardNode : rewardsNode ){
+			uint32 day;
+
+			if( !this->asUInt32( rewardNode, "Day", day ) ){
+				continue;
+			}
+
+			day -= 1;
+
+			std::shared_ptr<s_attendance_reward> reward = util::map_find( attendance_period->rewards, day );
+			bool reward_exists = reward != nullptr;
+
+			if( !reward_exists ){
+				if( !this->nodeExists( rewardNode, "ItemId" ) ){
+					this->invalidWarning( rewardNode, "Node \"ItemId\" is missing.\n" );
+					return 0;
+				}
+
+				reward = std::make_shared<s_attendance_reward>();
+			}
+
+			if( this->nodeExists( rewardNode, "ItemId" ) ){
+				uint16 item_id;
+
+				if( !this->asUInt16( rewardNode, "ItemId", item_id ) ){
+					continue;
+				}
+
+				if( item_id == 0 || !itemdb_exists( item_id ) ){
+					ShowError( "pc_attendance_load: Unknown item ID %hu for day %d.\n", item_id, day + 1 );
+					continue;
+				}
+
+				reward->item_id = item_id;
+			}
+
+			if( this->nodeExists( rewardNode, "Amount" ) ){
+				uint16 amount;
+
+				if( !this->asUInt16( rewardNode, "Amount", amount ) ){
+					continue;
+				}
+
+				if( amount == 0 ){
+					ShowWarning( "pc_attendance_load: Invalid reward count %hu for day %d. Defaulting to 1...\n", amount, day + 1 );
+					amount = 1;
+				}else if( amount > MAX_AMOUNT ){
+					ShowError( "pc_attendance_load: Reward count %hu above maximum %hu for day %d. Defaulting to %hu...\n", amount, MAX_AMOUNT, day + 1, MAX_AMOUNT );
+					amount = MAX_AMOUNT;
+				}
+
+				reward->amount = amount;
+			}else{
+				if( !reward_exists ){
+					reward->amount = 1;
+				}
+			}
+
+			if( !reward_exists ){
+				attendance_period->rewards[day] = reward;
+			}
+		}
+
+		bool missing_day = false;
+
+		for( int day = 0; day < attendance_period->rewards.size(); day++ ){
+			if( attendance_period->rewards.find( day ) == attendance_period->rewards.end() ){
+				ShowError( "pc_attendance_load: Reward for day %d is missing.\n", day + 1 );
+				missing_day = true;
+				break;
+			}
+		}
+
+		if( missing_day ){
+			return 0;
+		}
+	}
+
+	if( !exists ){
+		this->put( start, attendance_period );
+	}
+
+	return 1;
+}
+
+AttendanceDatabase attendance_db;
 
 #define MOTD_LINE_SIZE 128
 static char motd_text[MOTD_LINE_SIZE][CHAT_SIZE_MAX]; // Message of the day buffer [Valaris]
@@ -12766,12 +12954,14 @@ void pc_set_costume_view(struct map_session_data *sd) {
 		clif_changelook(&sd->bl, LOOK_ROBE, sd->status.robe);
 }
 
-struct s_attendance_period* pc_attendance_period(){
+std::shared_ptr<s_attendance_period> pc_attendance_period(){
 	uint32 date = date_get(DT_YYYYMMDD);
 
-	for( struct s_attendance_period& period : attendance_periods ){
-		if( period.start <= date && period.end >= date ){
-			return &period;
+	for( std::pair<const uint32,std::shared_ptr<s_attendance_period>>& pair : attendance_db ){
+		std::shared_ptr<s_attendance_period> period = pair.second;
+
+		if( period->start <= date && period->end >= date ){
+			return period;
 		}
 	}
 
@@ -12793,7 +12983,7 @@ static inline bool pc_attendance_rewarded_today( struct map_session_data* sd ){
 }
 
 int32 pc_attendance_counter( struct map_session_data* sd ){
-	struct s_attendance_period* period = pc_attendance_period();
+	std::shared_ptr<s_attendance_period> period = pc_attendance_period();
 
 	// No running attendance period
 	if( period == nullptr ){
@@ -12834,7 +13024,7 @@ void pc_attendance_claim_reward( struct map_session_data* sd ){
 
 	attendance_counter += 1;
 
-	struct s_attendance_period* period = pc_attendance_period();
+	std::shared_ptr<s_attendance_period> period = pc_attendance_period();
 
 	if( period == nullptr ){
 		return;
@@ -12850,7 +13040,7 @@ void pc_attendance_claim_reward( struct map_session_data* sd ){
 	if( save_settings&CHARSAVE_ATTENDANCE )
 		chrif_save(sd, CSAVE_NORMAL);
 
-	struct s_attendance_reward& reward = period->rewards.at( attendance_counter - 1 );
+	std::shared_ptr<s_attendance_reward> reward = period->rewards[attendance_counter - 1];
 
 	struct mail_message msg;
 
@@ -12861,8 +13051,8 @@ void pc_attendance_claim_reward( struct map_session_data* sd ){
 	safesnprintf( msg.title, MAIL_TITLE_LENGTH, msg_txt( sd, 789 ), attendance_counter );
 	safesnprintf( msg.body, MAIL_BODY_LENGTH, msg_txt( sd, 790 ), attendance_counter );
 
-	msg.item[0].nameid = reward.item_id;
-	msg.item[0].amount = reward.amount;
+	msg.item[0].nameid = reward->item_id;
+	msg.item[0].amount = reward->amount;
 	msg.item[0].identify = 1;
 
 	msg.status = MAIL_NEW;
@@ -12874,152 +13064,6 @@ void pc_attendance_claim_reward( struct map_session_data* sd ){
 	clif_attendence_response( sd, attendance_counter );
 }
 
-void pc_attendance_load( std::string path ){
-	YAML::Node root;
-
-	try{
-		root = YAML::LoadFile( path );
-	}catch( ... ){
-		ShowError( "pc_attendance_load: Failed to read attendance configuration file \"%s\".\n", path.c_str() );
-		return;
-	}
-
-	if( root["Attendance"] ){
-		YAML::Node attendance = root["Attendance"];
-
-		for( const auto &periodNode : attendance ){
-			if( !periodNode["Start"].IsDefined() ){
-				ShowError( "pc_attendance_load: Missing \"Start\" for period in line %d.\n", periodNode.Mark().line );
-				continue;
-			}
-
-			YAML::Node startNode = periodNode["Start"];
-
-			if( !periodNode["End"].IsDefined() ){
-				ShowError( "pc_attendance_load: Missing \"End\" for period in line %d.\n", periodNode.Mark().line );
-				continue;
-			}
-
-			YAML::Node endNode = periodNode["End"];
-
-			if( !periodNode["Rewards"].IsDefined() ){
-				ShowError( "pc_attendance_load: Missing \"Rewards\" for period in line %d.\n", periodNode.Mark().line );
-				continue;
-			}
-
-			YAML::Node rewardsNode = periodNode["Rewards"];
-
-			uint32 start = startNode.as<uint32>();
-			uint32 end = endNode.as<uint32>();
-
-			// If the period is outdated already, we do not even bother parsing
-			if( end < date_get( DT_YYYYMMDD ) ){
-				continue;
-			}
-
-			// Collision detection
-			bool collision = false;
-
-			for( struct s_attendance_period& period : attendance_periods ){
-				// Check if start is inside another period
-				if( period.start <= start && start <= period.end ){
-					ShowError( "pc_attendance_load: period start %u intersects with period %u-%u.\n", start, period.start, period.end );
-					collision = true;
-					break;
-				}
-
-				// Check if end is inside another period
-				if( period.start <= end && end <= period.end ){
-					ShowError( "pc_attendance_load: period end %u intersects with period %u-%u.\n", start, period.start, period.end );
-					collision = true;
-					break;
-				}
-			}
-
-			if( collision ){
-				continue;
-			}
-
-			struct s_attendance_period period;
-
-			period.start = start;
-			period.end = end;
-
-			for( const auto& rewardNode : rewardsNode ){
-				if( !rewardNode["Day"].IsDefined() ){
-					ShowError( "pc_attendance_load: No day defined for node in line %d.\n", rewardNode.Mark().line );
-					continue;
-				}
-
-				uint32 day = rewardNode["Day"].as<uint32>();
-
-				if( !rewardNode["ItemId"].IsDefined() ){
-					ShowError( "pc_attendance_load: No reward defined for day %d.\n", day );
-					continue;
-				}
-
-				YAML::Node itemNode = rewardNode["ItemId"];
-
-				uint16 item_id = itemNode.as<uint16>();
-
-				if( item_id == 0 || !itemdb_exists( item_id ) ){
-					ShowError( "pc_attendance_load: Unknown item ID %hu for day %d.\n", item_id, day );
-					continue;
-				}
-
-				uint16 amount;
-
-				if( rewardNode["Amount"] ){
-					amount = rewardNode["Amount"].as<uint16>();
-
-					if( amount == 0 ){
-						ShowError( "pc_attendance_load: Invalid reward count %hu for day %d. Defaulting to 1...\n", amount, day );
-						amount = 1;
-					}else if( amount > MAX_AMOUNT ){
-						ShowError( "pc_attendance_load: Reward count %hu above maximum %hu for day %d. Defaulting to %hu...\n", amount, MAX_AMOUNT, day, MAX_AMOUNT );
-						amount = MAX_AMOUNT;
-					}
-				}else{
-					amount = 1;
-				}
-
-				struct s_attendance_reward* reward = &period.rewards[day - 1];
-
-				reward->item_id = item_id;
-				reward->amount = amount;
-			}
-
-			bool missing_day = false;
-
-			for( int day = 0; day < period.rewards.size(); day++ ){
-				if( !util::map_exists( period.rewards, day ) ){
-					ShowError( "pc_attendance_load: Reward for day %d is missing.\n", day + 1 );
-					missing_day = true;
-					break;
-				}
-			}
-
-			if( missing_day ){
-				continue;
-			}
-
-			attendance_periods.push_back( period );
-		}
-	}
-}
-
-void pc_read_attendance(){
-	char path[1024];
-
-	sprintf( path, "%s/%sattendance.yml", db_path, DBPATH );
-
-	pc_attendance_load( path );
-
-	sprintf( path, "%s/%s/attendance.yml", db_path, DBIMPORT );
-
-	pc_attendance_load( path );
-}
-
 /*==========================================
  * pc Init/Terminate
  *------------------------------------------*/
@@ -13031,7 +13075,7 @@ void do_final_pc(void) {
 	ers_destroy(num_reg_ers);
 	ers_destroy(str_reg_ers);
 
-	attendance_periods.clear();
+	attendance_db.clear();
 }
 
 void do_init_pc(void) {
@@ -13040,7 +13084,7 @@ void do_init_pc(void) {
 
 	pc_readdb();
 	pc_read_motd(); // Read MOTD [Valaris]
-	pc_read_attendance();
+	attendance_db.load();
 
 	add_timer_func_list(pc_invincible_timer, "pc_invincible_timer");
 	add_timer_func_list(pc_eventtimer, "pc_eventtimer");

+ 6 - 6
src/map/script.cpp

@@ -23358,7 +23358,7 @@ BUILDIN_FUNC(achievementadd) {
 		return SCRIPT_CMD_FAILURE;
 	}
 
-	if (achievement_exists(achievement_id) == false) {
+	if (achievement_db.exists(achievement_id) == false) {
 		ShowWarning("buildin_achievementadd: Achievement '%d' doesn't exist.\n", achievement_id);
 		script_pushint(st, false);
 		return SCRIPT_CMD_FAILURE;
@@ -23395,7 +23395,7 @@ BUILDIN_FUNC(achievementremove) {
 		return SCRIPT_CMD_FAILURE;
 	}
 
-	if (achievement_exists(achievement_id) == false) {
+	if (achievement_db.exists(achievement_id) == false) {
 		ShowWarning("buildin_achievementremove: Achievement '%d' doesn't exist.\n", achievement_id);
 		script_pushint(st, false);
 		return SCRIPT_CMD_SUCCESS;
@@ -23431,7 +23431,7 @@ BUILDIN_FUNC(achievementinfo) {
 		return SCRIPT_CMD_FAILURE;
 	}
 
-	if (achievement_exists(achievement_id) == false) {
+	if (achievement_db.exists(achievement_id) == false) {
 		ShowWarning("buildin_achievementinfo: Achievement '%d' doesn't exist.\n", achievement_id);
 		script_pushint(st, false);
 		return SCRIPT_CMD_FAILURE;
@@ -23465,7 +23465,7 @@ BUILDIN_FUNC(achievementcomplete) {
 		return SCRIPT_CMD_FAILURE;
 	}
 
-	if (achievement_exists(achievement_id) == false) {
+	if (achievement_db.exists(achievement_id) == false) {
 		ShowWarning("buildin_achievementcomplete: Achievement '%d' doesn't exist.\n", achievement_id);
 		script_pushint(st, false);
 		return SCRIPT_CMD_FAILURE;
@@ -23502,7 +23502,7 @@ BUILDIN_FUNC(achievementexists) {
 		return SCRIPT_CMD_FAILURE;
 	}
 
-	if (achievement_exists(achievement_id) == false) {
+	if (achievement_db.exists(achievement_id) == false) {
 		ShowWarning("buildin_achievementexists: Achievement '%d' doesn't exist.\n", achievement_id);
 		script_pushint(st, false);
 		return SCRIPT_CMD_SUCCESS;
@@ -23541,7 +23541,7 @@ BUILDIN_FUNC(achievementupdate) {
 		return SCRIPT_CMD_FAILURE;
 	}
 
-	if (achievement_exists(achievement_id) == false) {
+	if (achievement_db.exists(achievement_id) == false) {
 		ShowWarning("buildin_achievementupdate: Achievement '%d' doesn't exist.\n", achievement_id);
 		script_pushint(st, false);
 		return SCRIPT_CMD_FAILURE;

Vissa filer visades inte eftersom för många filer har ändrats