浏览代码

Converted penalty db to YAML (#5562)

Fixes #5463

Added penalty for MVP exp and MVP drops
Removed penalty support for monster classes

Thanks to @attackjom and @aleos89
Lemongrass3110 4 年之前
父节点
当前提交
81e7d8ef89

+ 0 - 14
db/import-tmpl/level_penalty.txt

@@ -1,14 +0,0 @@
-// Experience & Drop Rate Modifier Database
-//
-// Structure of Database:
-// Type,Class,Level difference,Rate
-//
-// TYPE:
-//   1=experience, 2=item drop
-// CLASS:
-//   0=Normal monsters, 1=Boss monsters, 2=Guardians
-//
-// Note: RENEWAL_DROP and/or RENEWAL_EXP must be enabled.
-
-// EXP modifiers due to level difference
-

+ 33 - 0
db/import-tmpl/level_penalty.yml

@@ -0,0 +1,33 @@
+# 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/>.
+#
+###########################################################################
+# Level Penalty Database
+###########################################################################
+#
+# Level Penalty Settings
+#
+###########################################################################
+# - Type:             Type of Penalty (Exp, Drop, Mvp_Exp, Mvp_Drop)
+#   LevelDifferences: List of level difference between player and monster
+#     - Difference:   Level difference between player and monster
+#       Rate:         Rate applied to original exp or drop rate (0-10000)
+###########################################################################
+
+Header:
+  Type: PENALTY_DB
+  Version: 1

+ 39 - 0
db/level_penalty.yml

@@ -0,0 +1,39 @@
+# 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/>.
+#
+###########################################################################
+# Level Penalty Database
+###########################################################################
+#
+# Level Penalty Settings
+#
+###########################################################################
+# - Type:             Type of Penalty (Exp, Drop, Mvp_Exp, Mvp_Drop)
+#   LevelDifferences: List of level difference between player and monster
+#     - Difference:   Level difference between player and monster
+#       Rate:         Rate applied to original exp or drop rate (0-10000)
+###########################################################################
+
+Header:
+  Type: PENALTY_DB
+  Version: 1
+
+Footer:
+  Imports:
+  - Path: db/re/level_penalty.yml
+    Mode: Renewal
+  - Path: db/import/level_penalty.yml

+ 0 - 60
db/re/level_penalty.txt

@@ -1,60 +0,0 @@
-// Experience & Drop Rate Modifier Database
-//
-// Structure of Database:
-// Type,Class,Level difference,Rate
-//
-// TYPE:
-//   1=experience, 2=item drop
-// CLASS:
-//   0=Normal monsters, 1=Boss monsters, 2=Guardians
-//
-// Note: RENEWAL_DROP and/or RENEWAL_EXP must be enabled.
-
-// EXP modifiers due to level difference
-1,CLASS_NORMAL,16,40
-1,CLASS_NORMAL,15,115
-1,CLASS_NORMAL,14,120
-1,CLASS_NORMAL,13,125
-1,CLASS_NORMAL,12,130
-1,CLASS_NORMAL,11,135
-1,CLASS_NORMAL,10,140
-1,CLASS_NORMAL,9,135
-1,CLASS_NORMAL,8,130
-1,CLASS_NORMAL,7,125
-1,CLASS_NORMAL,6,120
-1,CLASS_NORMAL,5,115
-1,CLASS_NORMAL,4,110
-1,CLASS_NORMAL,3,105
-1,CLASS_NORMAL,0,100
-1,CLASS_NORMAL,-1,100
-1,CLASS_NORMAL,-6,95
-1,CLASS_NORMAL,-11,90
-1,CLASS_NORMAL,-16,85
-1,CLASS_NORMAL,-21,60
-1,CLASS_NORMAL,-26,35
-1,CLASS_NORMAL,-31,10
-
-// Boss Type
-1,CLASS_BOSS,0,100
-
-// Guardian Type
-1,CLASS_GUARDIAN,0,100
-
-// Drop rate modifiers due to level difference
-2,CLASS_NORMAL,16,50
-2,CLASS_NORMAL,13,60
-2,CLASS_NORMAL,10,70
-2,CLASS_NORMAL,7,80
-2,CLASS_NORMAL,4,90
-2,CLASS_NORMAL,0,100
-2,CLASS_NORMAL,-4,90
-2,CLASS_NORMAL,-7,80
-2,CLASS_NORMAL,-10,70
-2,CLASS_NORMAL,-13,60
-2,CLASS_NORMAL,-16,50
-
-// Boss Type
-2,CLASS_BOSS,0,100
-
-// Guardian Type
-2,CLASS_GUARDIAN,0,100

+ 99 - 0
db/re/level_penalty.yml

@@ -0,0 +1,99 @@
+# 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/>.
+#
+###########################################################################
+# Level Penalty Database
+###########################################################################
+#
+# Level Penalty Settings
+#
+###########################################################################
+# - Type:             Type of Penalty (Exp, Drop, Mvp_Exp, Mvp_Drop)
+#   LevelDifferences: List of level difference between player and monster
+#     - Difference:   Level difference between player and monster
+#       Rate:         Rate applied to original exp or drop rate (0-10000)
+###########################################################################
+
+Header:
+  Type: PENALTY_DB
+  Version: 1
+
+Body:
+  - Type: Exp
+    LevelDifferences:
+      - Difference: 16
+        Rate: 40
+      - Difference: 15
+        Rate: 115
+      - Difference: 14
+        Rate: 120
+      - Difference: 13
+        Rate: 125
+      - Difference: 12
+        Rate: 130
+      - Difference: 11
+        Rate: 135
+      - Difference: 10
+        Rate: 140
+      - Difference: 9
+        Rate: 135
+      - Difference: 8
+        Rate: 130
+      - Difference: 7
+        Rate: 125
+      - Difference: 6
+        Rate: 120
+      - Difference: 5
+        Rate: 115
+      - Difference: 4
+        Rate: 110
+      - Difference: 3
+        Rate: 105
+      - Difference: -6
+        Rate: 95
+      - Difference: -11
+        Rate: 90
+      - Difference: -16
+        Rate: 85
+      - Difference: -21
+        Rate: 60
+      - Difference: -26
+        Rate: 35
+      - Difference: -31
+        Rate: 10
+  - Type: Drop
+    LevelDifferences:
+      - Difference: 16
+        Rate: 50
+      - Difference: 13
+        Rate: 60
+      - Difference: 10
+        Rate: 70
+      - Difference: 7
+        Rate: 80
+      - Difference: 4
+        Rate: 90
+      - Difference: -4
+        Rate: 90
+      - Difference: -7
+        Rate: 80
+      - Difference: -10
+        Rate: 70
+      - Difference: -13
+        Rate: 60
+      - Difference: -16
+        Rate: 50

+ 12 - 0
doc/yaml/db/level_penalty.yml

@@ -0,0 +1,12 @@
+###########################################################################
+# Level Penalty Database
+###########################################################################
+#
+# Level Penalty Settings
+#
+###########################################################################
+# - Type:             Type of Penalty (Exp, Drop, Mvp_Exp, Mvp_Drop)
+#   LevelDifferences: List of level difference between player and monster
+#     - Difference:   Level difference between player and monster
+#       Rate:         Rate applied to original exp or drop rate (0-10000)
+###########################################################################

+ 10 - 4
src/map/atcommand.cpp

@@ -7389,8 +7389,10 @@ ACMD_FUNC(mobinfo)
 		}
 #ifdef RENEWAL_EXP
 		if( battle_config.atcommand_mobinfo_type ) {
-			base_exp = base_exp * pc_level_penalty_mod(mob->lv - sd->status.base_level, mob->status.class_, mob->status.mode, 1) / 100;
-			job_exp = job_exp * pc_level_penalty_mod(mob->lv - sd->status.base_level, mob->status.class_, mob->status.mode, 1) / 100;
+			int penalty = pc_level_penalty_mod( sd, PENALTY_EXP, mob );
+
+			base_exp = base_exp * penalty / 100;
+			job_exp = job_exp * penalty / 100;
 		}
 #endif
 		// stats
@@ -7415,6 +7417,10 @@ ACMD_FUNC(mobinfo)
 		clif_displaymessage(fd, msg_txt(sd,1245)); //  Drops:
 		strcpy(atcmd_output, " ");
 		j = 0;
+#ifdef RENEWAL_DROP
+		int penalty = pc_level_penalty_mod( sd, PENALTY_DROP, mob );
+#endif
+
 		for (i = 0; i < MAX_MOB_DROP_TOTAL; i++) {
 			int droprate;
 			if (mob->dropitem[i].nameid == 0 || mob->dropitem[i].rate < 1 || (item_data = itemdb_exists(mob->dropitem[i].nameid)) == NULL)
@@ -7423,7 +7429,7 @@ ACMD_FUNC(mobinfo)
 
 #ifdef RENEWAL_DROP
 			if( battle_config.atcommand_mobinfo_type ) {
-				droprate = droprate * pc_level_penalty_mod(mob->lv - sd->status.base_level, mob->status.class_, mob->status.mode, 2) / 100;
+				droprate = droprate * penalty / 100;
 				if (droprate <= 0 && !battle_config.drop_rate0item)
 					droprate = 1;
 			}
@@ -7962,7 +7968,7 @@ ACMD_FUNC(whodrops)
 
 #ifdef RENEWAL_DROP
 				if( battle_config.atcommand_mobinfo_type )
-					dropchance = dropchance * pc_level_penalty_mod(mob_db(item_data->mob[j].id)->lv - sd->status.base_level, mob_db(item_data->mob[j].id)->status.class_, mob_db(item_data->mob[j].id)->status.mode, 2) / 100;
+					dropchance = dropchance * pc_level_penalty_mod( sd, PENALTY_DROP, mob_db( item_data->mob[j].id ) ) / 100;
 #endif
 				if (pc_isvip(sd)) // Display item rate increase for VIP
 					dropchance += (dropchance * battle_config.vip_drop_increase) / 100;

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

@@ -330,7 +330,7 @@
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\job_exp.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\job_exp.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\job_noenter_map.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\job_noenter_map.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\job_param_db.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\job_param_db.txt')" />
-    <Copy SourceFiles="$(SolutionDir)db\import-tmpl\level_penalty.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\level_penalty.txt')" />
+    <Copy SourceFiles="$(SolutionDir)db\import-tmpl\level_penalty.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\level_penalty.yml')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\magicmushroom_db.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\magicmushroom_db.yml')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\map_cache.dat" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\map_cache.dat')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\map_index.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\map_index.txt')" />

+ 18 - 5
src/map/mob.cpp

@@ -2713,7 +2713,7 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type)
 				if(base_exp || job_exp) {
 					if( md->dmglog[i].flag != MDLF_PET || battle_config.pet_attack_exp_to_master ) {
 #ifdef RENEWAL_EXP
-						int rate = pc_level_penalty_mod(md->level - tmpsd[i]->status.base_level, md->status.class_, md->status.mode, 1);
+						int rate = pc_level_penalty_mod( tmpsd[i], PENALTY_EXP, nullptr, md );
 						if (rate != 100) {
 							if (base_exp)
 								base_exp = (unsigned int)cap_value(apply_rate(base_exp, rate), 1, UINT_MAX);
@@ -2748,10 +2748,7 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type)
 		struct item_data* it = NULL;
 		int drop_rate;
 #ifdef RENEWAL_DROP
-		int drop_modifier = mvp_sd    ? pc_level_penalty_mod(md->level - mvp_sd->status.base_level, md->status.class_, md->status.mode, 2)   :
-							second_sd ? pc_level_penalty_mod(md->level - second_sd->status.base_level, md->status.class_, md->status.mode, 2):
-							third_sd  ? pc_level_penalty_mod(md->level - third_sd->status.base_level, md->status.class_, md->status.mode, 2) :
-							100; // No player was attached, we don't use any modifier (100 = rates are not touched)
+		int drop_modifier = pc_level_penalty_mod( mvp_sd != nullptr ? mvp_sd : second_sd != nullptr ? second_sd : third_sd, PENALTY_DROP, nullptr, md );
 #endif
 		dlist->m = md->bl.m;
 		dlist->x = md->bl.x;
@@ -2931,6 +2928,13 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type)
 			exp =1;
 		else {
 			exp = md->db->mexp;
+
+#if defined(RENEWAL_EXP)
+			int penalty = pc_level_penalty_mod( mvp_sd, PENALTY_MVP_EXP, nullptr, md );
+
+			exp = cap_value( apply_rate( exp, penalty ), 0, MAX_EXP );
+#endif
+
 			if (count > 1)
 				exp += exp*(battle_config.exp_bonus_attacker*(count-1))/100.; //[Gengar]
 		}
@@ -2968,6 +2972,10 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type)
 				}
 			}
 
+#if defined(RENEWAL_DROP)
+			int penalty = pc_level_penalty_mod( mvp_sd, PENALTY_MVP_DROP, nullptr, md );
+#endif
+
 			for(i = 0; i < MAX_MVP_DROP_TOTAL; i++) {
 				struct item_data *i_data;
 
@@ -2975,6 +2983,11 @@ int mob_dead(struct mob_data *md, struct block_list *src, int type)
 					continue;
 
 				temp = mdrop[i].rate;
+
+#if defined(RENEWAL_DROP)
+				temp = cap_value( apply_rate( temp, penalty ), 0, 10000 );
+#endif
+
 				if (temp != 10000) {
 					if(temp <= 0 && !battle_config.drop_rate0item)
 						temp = 1;

+ 1 - 1
src/map/party.cpp

@@ -1115,7 +1115,7 @@ void party_exp_share(struct party_data* p, struct block_list* src, unsigned int
 #ifdef RENEWAL_EXP
 		uint32 base_gained = base_exp, job_gained = job_exp;
 		if (base_exp || job_exp) {
-			int rate = pc_level_penalty_mod(md->level - sd[i]->status.base_level, md->db->status.class_, md->db->status.mode, 1);
+			int rate = pc_level_penalty_mod( sd[i], PENALTY_EXP, nullptr, md );
 			if (rate != 100) {
 				if (base_exp)
 					base_gained = (unsigned int)cap_value(apply_rate(base_exp, rate), 1, UINT_MAX);

+ 127 - 58
src/map/pc.cpp

@@ -67,9 +67,6 @@ static inline bool pc_attendance_rewarded_today( struct map_session_data* sd );
 #define PVP_CALCRANK_INTERVAL 1000	// PVP calculation interval
 
 static unsigned int statp[MAX_LEVEL+1];
-#if defined(RENEWAL_DROP) || defined(RENEWAL_EXP)
-static unsigned int level_penalty[3][CLASS_MAX][MAX_LEVEL*2+1];
-#endif
 
 // h-files are for declarations, not for implementations... [Shinomori]
 struct skill_tree_entry skill_tree[CLASS_COUNT][MAX_SKILL_TREE];
@@ -267,6 +264,105 @@ uint64 AttendanceDatabase::parseBodyNode(const YAML::Node &node){
 
 AttendanceDatabase attendance_db;
 
+const std::string PenaltyDatabase::getDefaultLocation(){
+	return std::string( db_path ) + "/level_penalty.yml";
+}
+
+uint64 PenaltyDatabase::parseBodyNode( const YAML::Node& node ){
+	std::string type_constant;
+
+	if( !this->asString( node, "Type", type_constant ) ){
+		return 0;
+	}
+
+	int64 constant_value;
+
+	if( !script_get_constant( ( "PENALTY_" + type_constant ).c_str(), &constant_value ) ){
+		this->invalidWarning( node["Type"], "Unknown penalty type \"%s\".\n", type_constant.c_str() );
+		return 0;
+	}
+
+	if( constant_value < PENALTY_NONE || constant_value > PENALTY_MAX ){
+		this->invalidWarning( node["Type"], "Invalid penalty type \"%s\".\n", type_constant.c_str() );
+		return 0;
+	}
+
+	e_penalty_type type = static_cast<e_penalty_type>( constant_value );
+
+	std::shared_ptr<s_penalty> penalty = this->find( type );
+	bool exists = penalty != nullptr;
+
+	if( !exists ){
+		penalty = std::make_shared<s_penalty>();
+		penalty->type = type;
+
+		for( int i = 0, max = ARRAYLENGTH( penalty->rate ); i < max; i++ ){
+			penalty->rate[i] = UINT16_MAX;
+		}
+	}
+
+	if( this->nodeExists( node, "LevelDifferences" ) ){
+		for( const YAML::Node& levelNode : node["LevelDifferences"] ){
+			if( !this->nodesExist( levelNode, { "Difference", "Rate" } ) ){
+				return 0;
+			}
+
+			int32 difference;
+
+			if( !this->asInt32( levelNode, "Difference", difference ) ){
+				return 0;
+			}
+
+			if( std::abs( difference ) > MAX_LEVEL ){
+				this->invalidWarning( levelNode["Difference"], "Level difference %d is bigger than maximum level %d.\n", difference, MAX_LEVEL );
+				return 0;
+			}
+
+			uint16 rate;
+
+			if( !this->asUInt16Rate( levelNode, "Rate", rate ) ){
+				return 0;
+			}
+
+			penalty->rate[difference + MAX_LEVEL - 1] = rate;
+		}
+	}
+
+	if( !exists ){
+		this->put( type, penalty );
+	}
+
+	return 1;
+}
+
+void PenaltyDatabase::loadingFinished(){
+	for( const auto& pair : *this ){
+		for( int i = MAX_LEVEL - 1, max = ARRAYLENGTH( pair.second->rate ), last_rate = 100; i < max; i++ ){
+			uint16 rate = pair.second->rate[i];
+
+			// Check if it has been defined
+			if( rate == UINT16_MAX ){
+				pair.second->rate[i] = last_rate;
+			}else{
+				last_rate = rate;
+			}
+		}
+
+		for( int i = MAX_LEVEL - 1, last_rate = 100; i >= 0; i-- ){
+			uint16 rate = pair.second->rate[i];
+
+			// Check if it has been defined
+			if( rate == UINT16_MAX ){
+				pair.second->rate[i] = last_rate;
+			}else{
+				last_rate = rate;
+			}
+		}
+	}
+}
+
+PenaltyDatabase penalty_db;
+
 #define MOTD_LINE_SIZE 128
 static char motd_text[MOTD_LINE_SIZE][CHAT_SIZE_MAX]; // Message of the day buffer [Valaris]
 
@@ -11727,20 +11823,36 @@ void pc_delspiritcharm(struct map_session_data *sd, int count, int type)
  * @param type: 1 - EXP, 2 - Item Drop
  * @return Penalty rate
  */
-int pc_level_penalty_mod(int level_diff, uint32 mob_class, enum e_mode mode, int type)
-{
-	int rate = 100;
+uint16 pc_level_penalty_mod( struct map_session_data* sd, e_penalty_type type, struct mob_db* mob, mob_data* md ){
+	// No player was attached, we don't use any modifier (100 = rates are not touched)
+	if( sd == nullptr ){
+		return 100;
+	}
 
-	if (type == 2 && (mode&MD_FIXED_ITEMDROP))
-		return rate;
+	int monster_level;
 
-	if (level_diff < 0)
-		level_diff = MAX_LEVEL + (~level_diff + 1);
+	if( md != nullptr ){
+		monster_level = md->level;
+		mob = md->db;
+	}else if( mob != nullptr ){
+		monster_level = mob->lv;
+	}else{
+		return 100;
+	}
+
+	if( ( type == PENALTY_DROP || type == PENALTY_MVP_DROP ) && status_has_mode( &mob->status, MD_FIXED_ITEMDROP )  ){
+		return 100;
+	}
 
-	if ((rate = level_penalty[type][mob_class][level_diff]) > 0) // Monster class found, return rate
-		return rate;
+	int level_difference = monster_level - sd->status.base_level;
 
-	return 100; // Penalty not found, return default
+	std::shared_ptr<s_penalty> penalty = penalty_db.find( type );
+
+	if( penalty != nullptr ){
+		return penalty->rate[ level_difference + MAX_LEVEL - 1 ];
+	}else{
+		return 100;
+	}
 }
 #endif
 
@@ -11897,35 +12009,6 @@ static bool pc_readdb_skilltree(char* fields[], int columns, int current)
 	}
 	return true;
 }
-#if defined(RENEWAL_DROP) || defined(RENEWAL_EXP)
-static bool pc_readdb_levelpenalty(char* fields[], int columns, int current)
-{
-	int type, class_, diff;
-
-	type = atoi(fields[0]); //1=experience, 2=item drop
-	class_ = atoi(fields[1]);
-	diff = atoi(fields[2]);
-
-	if( type != 1 && type != 2 ){
-		ShowWarning("pc_readdb_levelpenalty: Invalid type %d specified.\n", type);
-		return false;
-	}
-
-	if( !CHK_CLASS(class_) ){
-		ShowWarning("pc_readdb_levelpenalty: Invalid class %d specified.\n", class_);
-		return false;
-	}
-
-	diff = min(diff, MAX_LEVEL);
-
-	if( diff < 0 )
-		diff = min(MAX_LEVEL + ( ~(diff) + 1 ), MAX_LEVEL*2);
-
-	level_penalty[type][class_][diff] = atoi(fields[3]);
-
-	return true;
-}
-#endif
 
 /** [Cydh]
 * Calculates base hp of player. Reference: http://irowiki.org/wiki/Max_HP
@@ -12303,22 +12386,7 @@ void pc_readdb(void) {
 	memset(job_info,0,sizeof(job_info)); // job_info table
 
 #if defined(RENEWAL_DROP) || defined(RENEWAL_EXP)
-	sv_readdb(db_path, DBPATH "level_penalty.txt", ',', 4, 4, -1, &pc_readdb_levelpenalty, 0);
-	sv_readdb(db_path, DBIMPORT"/level_penalty.txt", ',', 4, 4, -1, &pc_readdb_levelpenalty, 1);
-	for( k=1; k < 3; k++ ){ // fill in the blanks
-		int j;
-		for( j = 0; j < CLASS_ALL; j++ ){
-			int tmp = 0;
-			for( i = 0; i < MAX_LEVEL*2; i++ ){
-				if( i == MAX_LEVEL+1 )
-					tmp = level_penalty[k][j][0];// reset
-				if( level_penalty[k][j][i] > 0 )
-					tmp = level_penalty[k][j][i];
-				else
-					level_penalty[k][j][i] = tmp;
-			}
-		}
-	}
+	penalty_db.load();
 #endif
 
 	 // reset then read statspoint
@@ -13576,6 +13644,7 @@ void do_final_pc(void) {
 	ers_destroy(str_reg_ers);
 
 	attendance_db.clear();
+	penalty_db.clear();
 }
 
 void do_init_pc(void) {

+ 28 - 1
src/map/pc.hpp

@@ -7,6 +7,8 @@
 #include <memory>
 #include <vector>
 
+#include "../common/cbasetypes.hpp"
+#include "../common/database.hpp"
 #include "../common/mmo.hpp" // JOB_*, MAX_FAME_LIST, struct fame_list, struct mmo_charstatus
 #include "../common/strlib.hpp"// StringBuf
 #include "../common/timer.hpp"
@@ -895,6 +897,31 @@ enum item_check {
 	ITMCHK_ALL       = ITMCHK_INVENTORY|ITMCHK_CART|ITMCHK_STORAGE,
 };
 
+enum e_penalty_type : uint16{
+	PENALTY_NONE,
+	PENALTY_EXP,
+	PENALTY_DROP,
+	PENALTY_MVP_EXP,
+	PENALTY_MVP_DROP,
+	PENALTY_MAX
+};
+
+struct s_penalty{
+	e_penalty_type type;
+	uint16 rate[MAX_LEVEL * 2 - 1];
+};
+
+class PenaltyDatabase : public TypesafeYamlDatabase<uint16, s_penalty> {
+public:
+	PenaltyDatabase() : TypesafeYamlDatabase( "PENALTY_DB", 1 ){
+
+	}
+
+	const std::string getDefaultLocation();
+	uint64 parseBodyNode(const YAML::Node& node);
+	void loadingFinished();
+};
+
 struct s_job_info {
 	unsigned int base_hp[MAX_LEVEL], base_sp[MAX_LEVEL]; //Storage for the first calculation with hp/sp factor and multiplicator
 	int hp_factor, hp_multiplicator, sp_factor;
@@ -1420,7 +1447,7 @@ void pc_show_questinfo_reinit(struct map_session_data *sd);
 bool pc_job_can_entermap(enum e_job jobid, int m, int group_lv);
 
 #if defined(RENEWAL_DROP) || defined(RENEWAL_EXP)
-int pc_level_penalty_mod(int level_diff, uint32 mob_class, enum e_mode mode, int type);
+uint16 pc_level_penalty_mod( struct map_session_data* sd, e_penalty_type type, struct mob_db* mob, mob_data* md = nullptr );
 #endif
 
 bool pc_attendance_enabled();

+ 6 - 0
src/map/script_constants.hpp

@@ -7981,6 +7981,12 @@
 	export_constant(DROPEFFECT_ORANGE_PILLAR);
 	export_constant(DROPEFFECT_MAX);
 
+	/* penalty types */
+	export_constant(PENALTY_EXP);
+	export_constant(PENALTY_DROP);
+	export_constant(PENALTY_MVP_EXP);
+	export_constant(PENALTY_MVP_DROP);
+
 	#undef export_constant
 	#undef export_constant2
 	#undef export_parameter

+ 81 - 0
src/tool/csv2yaml.cpp

@@ -87,6 +87,8 @@ std::unordered_map<uint16, s_skill_unit_csv> skill_unit;
 std::unordered_map<uint16, s_skill_copyable> skill_copyable;
 std::unordered_map<uint16, s_skill_db> skill_nearnpc;
 
+static unsigned int level_penalty[3][CLASS_MAX][MAX_LEVEL * 2 + 1];
+
 struct s_item_flag_csv2yaml {
 	bool buyingstore, dead_branch, group, guid, broadcast, bindOnEquip, delay_consume;
 	e_item_drop_effect dropEffect;
@@ -215,6 +217,8 @@ static bool itemdb_read_db(const char *file);
 static bool itemdb_read_randomopt(const char* file);
 static bool itemdb_read_randomopt_group(char *str[], int columns, int current);
 static bool itemdb_randomopt_group_yaml(void);
+static bool pc_readdb_levelpenalty(char* fields[], int columns, int current);
+static bool pc_levelpenalty_yaml();
 
 // Constants for conversion
 std::unordered_map<t_itemid, std::string> aegis_itemnames;
@@ -537,6 +541,22 @@ int do_init( int argc, char** argv ){
 		return 0;
 	}
 
+#ifdef RENEWAL
+	memset( level_penalty, 0, sizeof( level_penalty ) );
+	if (!process("PENALTY_DB", 1, { path_db_mode }, "level_penalty", [](const std::string& path, const std::string& name_ext) -> bool {
+		return sv_readdb(path.c_str(), name_ext.c_str(), ',', 4, 4, -1, &pc_readdb_levelpenalty, false) && pc_levelpenalty_yaml();
+	})) {
+		return 0;
+	}
+
+	memset( level_penalty, 0, sizeof( level_penalty ) );
+	if (!process("PENALTY_DB", 1, { path_db_import }, "level_penalty", [](const std::string& path, const std::string& name_ext) -> bool {
+		return sv_readdb(path.c_str(), name_ext.c_str(), ',', 4, 4, -1, &pc_readdb_levelpenalty, false) && pc_levelpenalty_yaml();
+	})) {
+		return 0;
+	}
+#endif
+
 	// TODO: add implementations ;-)
 
 	return 0;
@@ -3538,6 +3558,67 @@ static bool itemdb_randomopt_group_yaml(void) {
 	return true;
 }
 
+static bool pc_readdb_levelpenalty( char* fields[], int columns, int current ){
+	// 1=experience, 2=item drop
+	int type = atoi( fields[0] );
+
+	if( type != 1 && type != 2 ){
+		ShowWarning( "pc_readdb_levelpenalty: Invalid type %d specified.\n", type );
+		return false;
+	}
+
+	int64 val = constant_lookup_int( fields[1] );
+
+	if( val == -100 ){
+		ShowWarning("pc_readdb_levelpenalty: Unknown class constant %s specified.\n", fields[1] );
+		return false;
+	}
+
+	int class_ = atoi( fields[1] );
+
+	if( !CHK_CLASS( class_ ) ){
+		ShowWarning( "pc_readdb_levelpenalty: Invalid class %d specified.\n", class_ );
+		return false;
+	}
+
+	int diff = atoi( fields[2] );
+
+	if( std::abs( diff ) > MAX_LEVEL ){
+		ShowWarning( "pc_readdb_levelpenalty: Level difference %d is too high.\n", diff );
+		return false;
+	}
+
+	diff += MAX_LEVEL - 1;
+
+	level_penalty[type][class_][diff] = atoi(fields[3]);
+
+	return true;
+}
+
+void pc_levelpenalty_yaml_sub( int type, const std::string& name ){
+	body << YAML::BeginMap;
+	body << YAML::Key << "Type" << YAML::Value << name;
+	body << YAML::Key << "LevelDifferences";
+	body << YAML::BeginSeq;
+	for( int i = ARRAYLENGTH( level_penalty[type][CLASS_NORMAL] ); i >= 0; i-- ){
+		if( level_penalty[type][CLASS_NORMAL][i] > 0 && level_penalty[type][CLASS_NORMAL][i] != 100 ){
+			body << YAML::BeginMap;
+			body << YAML::Key << "Difference" << YAML::Value << ( i - MAX_LEVEL + 1 );
+			body << YAML::Key << "Rate" << YAML::Value << level_penalty[type][CLASS_NORMAL][i];
+			body << YAML::EndMap;
+		}
+	}
+	body << YAML::EndSeq;
+	body << YAML::EndMap;
+}
+
+bool pc_levelpenalty_yaml(){
+	pc_levelpenalty_yaml_sub( 1, "Exp" );
+	pc_levelpenalty_yaml_sub( 2, "Drop" );
+
+	return true;
+}
+
 // Initialize Random Option constants
 void init_random_option_constants() {
 	#define export_constant2(a, b) script_set_constant_(a, b, a, false, false)