Browse Source

Allow NPC view data modifications (#4385)

* Fixes #4289.
* Updated script commands setunitdata and getunitdata to support the modification of NPC view data.
* Converted mob_avail database to YAML.
Thanks to @Lemongrass3110, @4144, @exneval, @Balferian, @cahya1992 and @teededung!
Co-authored-by: Lemongrass3110 <lemongrass@kstp.at>
Aleos 5 years ago
parent
commit
b0119631a4

+ 0 - 16
db/import-tmpl/mob_avail.txt

@@ -1,16 +0,0 @@
-// Mob Availability and Alias Database
-//
-// Structure of Database:
-// MobID,SpriteID{,Equipment}
-//
-// 01. MobID        Mob ID to change.
-// 02. SpriteID     Mob ID which will be sent to the client instead of MobID.
-//                  If 0, the mob becomes unavailable for use.
-// 03. Equipment    Item ID of pet equipment (must be available for pet counterpart, or this will cause problems).
-//
-// To disguise a mob as a player:
-// MobID,SpriteID,Sex,Hair_Style,Hair_Color,Weapon,Shield,Head_Top,Head_Middle,Head_Bottom,Option,Dye_Color
-//
-// SpriteID is a job class value.
-// Weapon and Shield uses Item ID, while Head uses View ID.
-

+ 123 - 0
db/import-tmpl/mob_avail.yml

@@ -0,0 +1,123 @@
+# 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/>.
+#
+###########################################################################
+# Mob Availability and Alias Database
+###########################################################################
+#
+# Mob Availability and Alias Settings
+#
+###########################################################################
+# - Mob                     Mob to adjust.
+#   Sprite                  Sprite which will be sent to the client instead of Mob.
+#   Sex                     Sex (if Sprite is a player). (Default: Female)
+#   HairStyle               Hair Style ID (if Sprite is a player). (Default: 0)
+#   HairColor               Hair Color ID (if Sprite is a player). (Default: 0)
+#   ClothColor              Cloth Color ID (if Sprite is a player). (Default: 0)
+#   Weapon                  Item name of weapon (if Sprite is a player). (Default: 0)
+#   Shield                  Item name of shield (if Sprite is a player). (Default: 0)
+#   HeadTop                 Item name of headgear (if Sprite is a player). (Default: 0)
+#   HeadMid                 Item name of headgear (if Sprite is a player). (Default: 0)
+#   HeadLow                 Item name of headgear (if Sprite is a player). (Default: 0)
+#   PetEquip                Item name of pet equipment (if Mob is a valid pet). (Default: 0)
+#   Options:                Set an option for an object. (Optional)
+#     <Option>: bool
+###########################################################################
+
+Header:
+  Type: MOB_AVAIL_DB
+  Version: 1
+
+#Body:
+  # Examples
+#  - Mob: PORING
+#    Sprite: BAPHOMET
+#  - Mob: E_OBEAUNE
+#    Sprite: PORING
+#    PetEquip: Backpack
+
+  # Easter Event Monsters
+#  - Mob: MOROCC_3
+#    Sprite: DOPPELGANGER
+#  - Mob: MOROCC_4
+#    Sprite: ECLIPSE
+
+  # rAthena Dev Team
+  # Valaris
+#  - Mob: BOW_GUARDIAN_
+#    Sprite: JOB_ASSASSIN_CROSS
+#    Sex: Male
+#    HairStyle: 1
+#    HairColor: 1
+#    ClothColor: 1
+#    Weapon: Jamadhar
+#    HeadTop: Sahkkat
+#    HeadMid: Sunglasses
+#    HeadLow: Cigar
+#    Options:
+#      Falcon: true
+  # Valaris Worshiper
+#  - Mob: E_CONDOR
+#    Sprite: JOB_THIEF
+#    Sex: Male
+#    HairStyle: 1
+#    HairColor: 1
+#    ClothColor: 1
+#    Weapon: Gladius
+#    Shield: Guard
+#    HeadTop: Sahkkat
+#    HeadMid: Sunglasses
+#    HeadLow: Cigar
+  # MC Cameri
+#  - Mob: E_TREASURE1
+#    Sprite: JOB_CRUSADER
+#    Sex: Male
+#    HairStyle: 6
+#    HairColor: 6
+#    ClothColor: 3
+#    Weapon: Sword
+#    Shield: Shield
+#    Options:
+#      Riding: true
+  # Poki#3
+#  - Mob: E_TREASURE2
+#    Sprite: JOB_SNIPER
+#    Sex: Male
+#    HairStyle: 21
+#    Weapon: Bow_Of_Rudra
+#    HeadTop: Boy's_Cap
+#    HeadMid: Takius_Blindfold
+#    HeadLow: Centimental_Leaf
+#    Options:
+#      Falcon: true
+  # Sentry
+#  - Mob: BOMBPORING
+#    Sprite: KNIGHT_GUARDIAN
+
+  # iRO Halloween Event 2009
+#  - Mob: EP14_MORS_BOSSB
+#    Sprite: ZOMBIE
+#  - Mob: EP14_MORS_MOB1
+#    Sprite: GHOUL
+#  - Mob: EP14_MORS_MOB2
+#    Sprite: ZOMBIE_MASTER
+
+  # iRO Halloween Event 2009
+#  - Mob: EP14_3_DEATH_B_MOB2
+#    Sprite: WHISPER
+#  - Mob: EP14_3_DEATH_B_MOB3
+#    Sprite: DARK_LORD

+ 0 - 44
db/mob_avail.txt

@@ -1,44 +0,0 @@
-// Mob Availability and Alias Database
-//
-// Structure of Database:
-// MobID,SpriteID{,Equipment}
-//
-// 01. MobID        Mob ID to change.
-// 02. SpriteID     Mob ID which will be sent to the client instead of MobID.
-//                  If 0, the mob becomes unavailable for use.
-// 03. Equipment    Item ID of pet equipment (must be available for pet counterpart, or this will cause problems).
-//
-// To disguise a mob as a player:
-// MobID,SpriteID,Sex,Hair_Style,Hair_Color,Weapon,Shield,Head_Top,Head_Middle,Head_Bottom,Option,Dye_Color
-//
-// SpriteID is a job class value.
-// Weapon and Shield uses Item ID, while Head uses View ID.
-// Option for carts only works if you compiled your server for a packet version before 2012-02-01
-
-//1002,1039		// Poring - Baphomet
-//1970,1002,10013	// Displays a Poring with a backpack
-
-// Easter Event Monsters
-//1920,1047,0
-//1921,1093,0
-
-// rAthena Dev Team
-// Valaris
-//1900,4013,1,1,1,1254,0,67,12,54,16,1
-// Valaris Worshiper
-//1901,6,1,1,1,1219,2101,67,12,54,0,1
-// MC Cameri
-//1902,14,1,6,6,1101,2105,0,0,0,32,3
-// Poki#3
-//1903,4012,1,21,0,1720,0,102,184,57,16,0
-// Sentry
-//1904,1286,0
-
-// iRO Halloween Event 2008
-//3000,1015,0
-//3001,1036,0
-//3002,1298,0
-
-// iRO Halloween Event 2009
-//3014,1179,0
-//3015,1272,0

+ 108 - 0
doc/mob_avail.txt

@@ -0,0 +1,108 @@
+//===== rAthena Documentation ================================
+//= rAthena Monster Availability Database Reference
+//===== By: ==================================================
+//= rAthena Dev Team
+//===== Last Updated: ========================================
+//= 20191213
+//===== Description: =========================================
+//= Explanation of the mob_avail.yml file and structure.
+//============================================================
+
+---------------------------------------
+
+Mob: The AEGIS name of the monster.
+
+---------------------------------------
+
+Sprite: The name of the sprite the monster will be changed to.
+
+This can be another mob, a player (prefixed with 'JOB_'), or an NPC. When using an NPC sprite,
+the prefix is not required in the mob_avail database as the script engine will strip it.
+
+Example:
+  - Mob: POPORING
+    Sprite: PORING # This will change the Poporing into a Poring.
+
+  - Mob: PORING
+    Sprite: JOB_STALKER # This will change the Poring into a Stalker.
+
+  - Mob: WOLF
+    Sprite: 4_M_BARBER # This will change the Wolf into the Barber NPC.
+
+These constants can be found in src/map/script_constants.hpp.
+
+---------------------------------------
+
+Sex: The sex to be displayed if the Sprite is a player.
+
+Valid types:
+	Female
+	Male
+
+---------------------------------------
+
+HairStyle: The hair style ID to be displayed if the Sprite is a player.
+
+---------------------------------------
+
+HairColor: The hair color ID to be displayed if the Sprite is a player.
+
+---------------------------------------
+
+ClothColor: The cloth color ID to be displayed if the Sprite is a player.
+
+---------------------------------------
+
+Weapon: The AEGIS name of the item to be displayed if the Sprite is a player.
+
+---------------------------------------
+
+Shield: The AEGIS name of the item to be displayed if the Sprite is a player.
+
+---------------------------------------
+
+HeadTop: The AEGIS name of the item to be displayed if the Sprite is a player.
+
+---------------------------------------
+
+HeadMid: The AEGIS name of the item to be displayed if the Sprite is a player.
+
+---------------------------------------
+
+HeadLow: The AEGIS name of the item to be displayed if the Sprite is a player.
+
+---------------------------------------
+
+PetEquip: The AEGIS name of the item to be displayed if the Mob is a valid pet.
+
+---------------------------------------
+
+Options: The view option to be applied to the Mob.
+
+Valid types:
+	Sight
+	Cart1
+	Falcon
+	Riding
+	Cart2
+	Cart3
+	Cart4
+	Cart5
+	Orcish
+	Wedding
+	Ruwach
+	Flying
+	Xmas
+	Transform
+	Summer
+	Dragon1
+	Wug
+	WugRider
+	MadoGear
+	Dragon2
+	Dragon3
+	Dragon4
+	Dragon5
+	Hanbok
+	Oktoberfest
+	Summer2

+ 18 - 2
doc/script_commands.txt

@@ -7979,6 +7979,8 @@ Parameters (indexes) for monsters are:
 	UMOB_ADELAY
 	UMOB_DMOTION
 	UMOB_TARGETID
+	UMOB_ROBE
+	UMOB_BODY2
 
 -----
 
@@ -8152,7 +8154,6 @@ Parameter (indexes) for elementals are:
 -----
 
 Parameter (indexes) for NPCs are:
-	UNPC_DISPLAY
 	UNPC_LEVEL
 	UNPC_HP
 	UNPC_MAXHP
@@ -8185,6 +8186,19 @@ Parameter (indexes) for NPCs are:
 	UNPC_AMOTION
 	UNPC_ADELAY
 	UNPC_DMOTION
+	UNPC_SEX
+	UNPC_CLASS
+	UNPC_HAIRSTYLE
+	UNPC_HAIRCOLOR
+	UNPC_HEADBOTTOM
+	UNPC_HEADMIDDLE
+	UNPC_HEADTOP
+	UNPC_CLOTHCOLOR
+	UNPC_SHIELD
+	UNPC_WEAPON
+	UNPC_ROBE
+	UNPC_BODY2
+	UNPC_DEADSIT
 
 *Notes:
 		- *_SIZE: small (0); medium (1); large (2)
@@ -8201,12 +8215,14 @@ Parameter (indexes) for NPCs are:
 		- *_AMOTION: see doc/mob_db.txt
 		- *_ADELAY: see doc/mob_db.txt
 		- *_DMOTION: see doc/mob_db.txt
+		- *_BODY2: enable (1) the alternate display, or disable (0)
 
 		- UMOB_AI: none (0); attack (1); marine sphere (2); flora (3); zanzou (4); legion (5); faw (6)
 		- UMOB_SCOPTION: see the 'Variables' section at the top of this document
 		- UMOB_SLAVECPYMSTRMD: make the slave copy the master's mode (1), or not (0)
 
-		- UNPC_PLUSALLSTAT: same as 'bAllStats'; increases/decreses all stats by given amount
+		- UNPC_PLUSALLSTAT: same as 'bAllStats'; increases/decreases all stats by given amount
+		- UNPC_DEADSIT: stand (0), dead (1), sit (2)
 
 Example:
 	// Spawn some Porings and save the Game ID.

+ 22 - 0
doc/yaml/db/mob_avail.yml

@@ -0,0 +1,22 @@
+###########################################################################
+# Mob Availability and Alias Database
+###########################################################################
+#
+# Mob Availability and Alias Settings
+#
+###########################################################################
+# - Mob                     Mob to adjust.
+#   Sprite                  Sprite which will be sent to the client instead of Mob.
+#   Sex                     Sex (if Sprite is a player). (Default: Female)
+#   HairStyle               Hair Style ID (if Sprite is a player). (Default: 0)
+#   HairColor               Hair Color ID (if Sprite is a player). (Default: 0)
+#   ClothColor              Cloth Color ID (if Sprite is a player). (Default: 0)
+#   Weapon                  Item name of weapon (if Sprite is a player). (Default: 0)
+#   Shield                  Item name of shield (if Sprite is a player). (Default: 0)
+#   HeadTop                 Item name of headgear (if Sprite is a player). (Default: 0)
+#   HeadMid                 Item name of headgear (if Sprite is a player). (Default: 0)
+#   HeadLow                 Item name of headgear (if Sprite is a player). (Default: 0)
+#   PetEquip                Item name of pet equipment (if Mob is a valid pet). (Default: 0)
+#   Options:                Set an option for an object. (Optional)
+#     <Option>: bool
+###########################################################################

+ 21 - 14
src/map/clif.cpp

@@ -283,7 +283,12 @@ static inline unsigned char clif_bl_type(struct block_list *bl) {
 	case BL_SKILL: return 0x3; //SKILL_TYPE
 	case BL_CHAT:  return 0x4; //UNKNOWN_TYPE
 	case BL_MOB:   return pcdb_checkid(status_get_viewdata(bl)->class_)?0x0:0x5; //NPC_MOB_TYPE
-	case BL_NPC:   return pcdb_checkid(status_get_viewdata(bl)->class_)?0x0:0x6; //NPC_EVT_TYPE
+	case BL_NPC:
+#if PACKETVER >= 20170726
+			return 0x6; //NPC_EVT_TYPE
+#else
+			return (pcdb_checkid(status_get_viewdata(bl)->class_) ? 0x0 : 0x6);
+#endif
 	case BL_PET:   return pcdb_checkid(status_get_viewdata(bl)->class_)?0x0:0x7; //NPC_PET_TYPE
 	case BL_HOM:   return 0x8; //NPC_HOM_TYPE
 	case BL_MER:   return 0x9; //NPC_MERSOL_TYPE
@@ -1144,7 +1149,7 @@ static int clif_set_unit_idle(struct block_list* bl, unsigned char* buffer, bool
 	WBUFW(buf,53) = (sd ? sd->status.font : 0);
 #endif
 #if PACKETVER >= 20120221
-	if ( battle_config.monster_hp_bars_info && !map_getmapflag(bl->m, MF_HIDEMOBHPBAR) && bl->type == BL_MOB && (status_get_hp(bl) < status_get_max_hp(bl)) ) {
+	if ( battle_config.monster_hp_bars_info && bl->type == BL_MOB && !map_getmapflag(bl->m, MF_HIDEMOBHPBAR) && (status_get_hp(bl) < status_get_max_hp(bl)) ) {
 		WBUFL(buf,55) = status_get_max_hp(bl);		// maxHP
 		WBUFL(buf,59) = status_get_hp(bl);		// HP
 	} else {
@@ -1447,7 +1452,7 @@ int clif_spawn(struct block_list *bl)
 	if(bl->type == BL_NPC && !((TBL_NPC*)bl)->chat_id && (((TBL_NPC*)bl)->sc.option&OPTION_INVISIBLE))
 		return 0;
 
-	len = clif_set_unit_idle(bl, buf,true);
+	len = clif_set_unit_idle(bl, buf, (bl->type == BL_NPC && vd->dead_sit ? false : true));
 	clif_send(buf, len, bl, AREA_WOS);
 	if (disguised(bl))
 		clif_setdisguise(bl, buf, len);
@@ -3606,7 +3611,7 @@ void clif_changelook(struct block_list *bl, int type, int val) {
 #if PACKETVER < 20150513
 				return;
 #else
-				if (val && sd->sc.option&OPTION_COSTUME)
+				if (val && sd && sd->sc.option&OPTION_COSTUME)
  					val = 0;
  				vd->body_style = val;
 #endif
@@ -3620,17 +3625,19 @@ void clif_changelook(struct block_list *bl, int type, int val) {
 #if PACKETVER < 4
 	clif_sprite_change(bl, bl->id, type, val, 0, target);
 #else
-	if(type == LOOK_WEAPON || type == LOOK_SHIELD) {
-		nullpo_retv(vd);
-		type = LOOK_WEAPON;
-		val = vd->weapon;
-		val2 = vd->shield;
-	}
-	if( disguised(bl) ) {
-		clif_sprite_change(bl, bl->id, type, val, val2, AREA_WOS);
-		clif_sprite_change(bl, -bl->id, type, val, val2, SELF);
+	if (bl->type != BL_NPC) {
+		if (type == LOOK_WEAPON || type == LOOK_SHIELD) {
+			type = LOOK_WEAPON;
+			val = (vd ? vd->weapon : 0);
+			val2 = (vd ? vd->shield : 0);
+		}
+		if (disguised(bl)) {
+			clif_sprite_change(bl, bl->id, type, val, val2, AREA_WOS);
+			clif_sprite_change(bl, -bl->id, type, val, val2, SELF);
+		} else
+			clif_sprite_change(bl, bl->id, type, val, val2, target);
 	} else
-		clif_sprite_change(bl, bl->id, type, val, val2, target);
+		unit_refresh(bl);
 #endif
 }
 

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

@@ -340,7 +340,7 @@
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\map_index.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\map_index.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\mercenary_db.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\mercenary_db.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\mercenary_skill_db.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\mercenary_skill_db.txt')" />
-    <Copy SourceFiles="$(SolutionDir)db\import-tmpl\mob_avail.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\mob_avail.txt')" />
+    <Copy SourceFiles="$(SolutionDir)db\import-tmpl\mob_avail.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\mob_avail.yml')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\mob_boss.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\mob_boss.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\mob_branch.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\mob_branch.txt')" />
     <Copy SourceFiles="$(SolutionDir)db\import-tmpl\mob_chat_db.txt" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\mob_chat_db.txt')" />

+ 296 - 45
src/map/mob.cpp

@@ -4348,55 +4348,308 @@ static int mob_read_sqldb(void)
 	return 0;
 }
 
-/*==========================================
- * MOB display graphic change data reading
- *------------------------------------------*/
-static bool mob_readdb_mobavail(char* str[], int columns, int current)
-{
-	int mob_id, sprite_id;
-	struct mob_db *db;
+const std::string MobAvailDatabase::getDefaultLocation() {
+	return std::string(db_path) + "/" + DBIMPORT + "/mob_avail.yml";
+}
 
-	mob_id = atoi(str[0]);
+/**
+ * Reads and parses an entry from the mob_avail.
+ * @param node: YAML node containing the entry.
+ * @return count of successfully parsed rows
+ */
+uint64 MobAvailDatabase::parseBodyNode(const YAML::Node &node) {
+	std::string mob_name;
 
-	if( (db=mob_db(mob_id)) == NULL)	// invalid class (probably undefined in db)
-	{
-		ShowWarning("mob_readdb_mobavail: Unknown mob id %d.\n", mob_id);
-		return false;
+	if (!this->asString(node, "Mob", mob_name))
+		return 0;
+
+	uint32 mob_id = mobdb_searchname(mob_name.c_str());
+
+	if (mob_id == 0) {
+		this->invalidWarning(node["Mob"], "Unknown mob %s.\n", mob_name.c_str());
+		return 0;
 	}
 
-	sprite_id = atoi(str[1]);
+	struct mob_db *mob = mob_db(mob_id);
 
-	memset(&db->vd, 0, sizeof(struct view_data));
-	db->vd.class_ = sprite_id;
+	if (this->nodeExists(node, "Sprite")) {
+		std::string sprite;
 
-	//Player sprites
-	if(pcdb_checkid(sprite_id) && columns==12) {
-		db->vd.sex=atoi(str[2]);
-		db->vd.hair_style=atoi(str[3]);
-		db->vd.hair_color=atoi(str[4]);
-		db->vd.weapon=atoi(str[5]);
-		db->vd.shield=atoi(str[6]);
-		db->vd.head_top=atoi(str[7]);
-		db->vd.head_mid=atoi(str[8]);
-		db->vd.head_bottom=atoi(str[9]);
-		db->option=atoi(str[10])&~(OPTION_HIDE|OPTION_CLOAK|OPTION_INVISIBLE);
-		db->vd.cloth_color=atoi(str[11]); // Monster player dye option - Valaris
+		if (!this->asString(node, "Sprite", sprite))
+			return 0;
 
-#ifdef NEW_CARTS
-		if( db->option & OPTION_CART ){
-			ShowWarning("mob_readdb_mobavail: You tried to use a cart for mob id %d. This does not work with setting an option anymore.\n", mob_id );
-			db->option &= ~OPTION_CART;
+		int constant;
+
+		if (script_get_constant(sprite.c_str(), &constant)) {
+			if (npcdb_checkid(constant) == 0 && pcdb_checkid(constant) == 0) {
+				this->invalidWarning(node["Sprite"], "Unknown sprite constant %s.\n", sprite.c_str());
+				return 0;
+			}
+		} else {
+			constant = mobdb_searchname(sprite.c_str());
+
+			if (constant == 0) {
+				this->invalidWarning(node["Sprite"], "Unknown mob sprite constant %s.\n", sprite.c_str());
+				return 0;
+			}
 		}
+
+		mob->vd.class_ = constant;
+	} else {
+		this->invalidWarning(node["Sprite"], "Sprite is missing.\n");
+		return 0;
+	}
+
+	if (this->nodeExists(node, "Sex")) {
+		if (pcdb_checkid(mob->vd.class_) == 0) {
+			this->invalidWarning(node["Sex"], "Sex is only applicable to Job sprites.\n");
+			return 0;
+		}
+
+		std::string sex;
+
+		if (!this->asString(node, "Sex", sex))
+			return 0;
+
+		std::string sex_constant = "SEX_" + sex;
+
+		int constant;
+
+		if (!script_get_constant(sex_constant.c_str(), &constant)) {
+			this->invalidWarning(node["Sex"], "Unknown sex constant %s.\n", sex.c_str());
+			return 0;
+		}
+
+		if (constant < SEX_FEMALE || constant > SEX_MALE) {
+			this->invalidWarning(node["Sex"], "Sex %s is not valid.\n", sex.c_str());
+			return 0;
+		}
+
+		mob->vd.sex = constant;
+	}
+
+	if (this->nodeExists(node, "HairStyle")) {
+		if (pcdb_checkid(mob->vd.class_) == 0) {
+			this->invalidWarning(node["HairStyle"], "HairStyle is only applicable to Job sprites.\n");
+			return 0;
+		}
+
+		uint16 hair_style;
+
+		if (!this->asUInt16(node, "HairStyle", hair_style))
+			return 0;
+
+		if (hair_style < MIN_HAIR_STYLE || hair_style > MAX_HAIR_STYLE) {
+			this->invalidWarning(node["HairStyle"], "HairStyle %d is out of range %d~%d. Setting to MIN_HAIR_STYLE.\n", hair_style, MIN_HAIR_STYLE, MAX_HAIR_STYLE);
+			hair_style = MIN_HAIR_STYLE;
+		}
+
+		mob->vd.hair_style = hair_style;
+	}
+
+	if (this->nodeExists(node, "HairColor")) {
+		if (pcdb_checkid(mob->vd.class_) == 0) {
+			this->invalidWarning(node["HairColor"], "HairColor is only applicable to Job sprites.\n");
+			return 0;
+		}
+
+		uint16 hair_color;
+
+		if (!this->asUInt16(node, "HairColor", hair_color))
+			return 0;
+
+		if (hair_color < MIN_HAIR_COLOR || hair_color > MAX_HAIR_COLOR) {
+			this->invalidWarning(node["HairColor"], "HairColor %d is out of range %d~%d. Setting to MIN_HAIR_COLOR.\n", hair_color, MIN_HAIR_COLOR, MAX_HAIR_COLOR);
+			hair_color = MIN_HAIR_COLOR;
+		}
+
+		mob->vd.hair_color = hair_color;
+	}
+
+	if (this->nodeExists(node, "ClothColor")) {
+		if (pcdb_checkid(mob->vd.class_) == 0) {
+			this->invalidWarning(node["ClothColor"], "ClothColor is only applicable to Job sprites.\n");
+			return 0;
+		}
+
+		uint32 cloth_color;
+
+		if (!this->asUInt32(node, "ClothColor", cloth_color))
+			return 0;
+
+		if (cloth_color < MIN_CLOTH_COLOR || cloth_color > MAX_CLOTH_COLOR) {
+			this->invalidWarning(node["ClothColor"], "ClothColor %d is out of range %d~%d. Setting to MIN_CLOTH_CLOR.\n", cloth_color, MIN_CLOTH_COLOR, MAX_CLOTH_COLOR);
+			cloth_color = MIN_CLOTH_COLOR;
+		}
+
+		mob->vd.cloth_color = cloth_color;
+	}
+
+	if (this->nodeExists(node, "Weapon")) {
+		if (pcdb_checkid(mob->vd.class_) == 0) {
+			this->invalidWarning(node["Weapon"], "Weapon is only applicable to Job sprites.\n");
+			return 0;
+		}
+
+		std::string weapon;
+
+		if (!this->asString(node, "Weapon", weapon))
+			return 0;
+
+		struct item_data *item = itemdb_searchname(weapon.c_str());
+
+		if (item == nullptr) {
+			this->invalidWarning(node["Weapon"], "Weapon %s is not a valid item.\n", weapon.c_str());
+			return 0;
+		}
+
+		mob->vd.weapon = item->nameid;
+	}
+
+	if (this->nodeExists(node, "Shield")) {
+		if (pcdb_checkid(mob->vd.class_) == 0) {
+			this->invalidWarning(node["Shield"], "Shield is only applicable to Job sprites.\n");
+			return 0;
+		}
+
+		std::string shield;
+
+		if (!this->asString(node, "Shield", shield))
+			return 0;
+
+		struct item_data *item = itemdb_searchname(shield.c_str());
+
+		if (item == nullptr) {
+			this->invalidWarning(node["Shield"], "Shield %s is not a valid item.\n", shield.c_str());
+			return 0;
+		}
+
+		mob->vd.shield = item->nameid;
+	}
+
+	if (this->nodeExists(node, "HeadTop")) {
+		if (pcdb_checkid(mob->vd.class_) == 0) {
+			this->invalidWarning(node["HeadTop"], "HeadTop is only applicable to Job sprites.\n");
+			return 0;
+		}
+
+		std::string head;
+
+		if (!this->asString(node, "HeadTop", head))
+			return 0;
+
+		struct item_data *item;
+
+		if ((item = itemdb_searchname(head.c_str())) == nullptr) {
+			this->invalidWarning(node["HeadTop"], "HeadTop %s is not a valid item.\n", head.c_str());
+			return 0;
+		}
+
+		mob->vd.head_top = item->look;
+	}
+
+	if (this->nodeExists(node, "HeadMid")) {
+		if (pcdb_checkid(mob->vd.class_) == 0) {
+			this->invalidWarning(node["HeadMid"], "HeadMid is only applicable to Job sprites.\n");
+			return 0;
+		}
+
+		std::string head;
+
+		if (!this->asString(node, "HeadMid", head))
+			return 0;
+
+		struct item_data *item = itemdb_searchname(head.c_str());
+
+		if (item == nullptr) {
+			this->invalidWarning(node["HeadMid"], "HeadMid %s is not a valid item.\n", head.c_str());
+			return 0;
+		}
+
+		mob->vd.head_mid = item->look;
+	}
+
+	if (this->nodeExists(node, "HeadLow")) {
+		if (pcdb_checkid(mob->vd.class_) == 0) {
+			this->invalidWarning(node["HeadLow"], "HeadLow is only applicable to Job sprites.\n");
+			return 0;
+		}
+
+		std::string head;
+
+		if (!this->asString(node, "HeadLow", head))
+			return 0;
+
+		struct item_data *item = itemdb_searchname(head.c_str());
+
+		if (item == nullptr) {
+			this->invalidWarning(node["HeadLow"], "HeadLow %s is not a valid item.\n", head.c_str());
+			return 0;
+		}
+
+		mob->vd.head_bottom = item->look;
+	}
+
+	if (this->nodeExists(node, "PetEquip")) {
+		std::shared_ptr<s_pet_db> pet_db_ptr = pet_db.find(mob->vd.class_);
+
+		if (pet_db_ptr == nullptr) {
+			this->invalidWarning(node["PetEquip"], "PetEquip is only applicable to defined pets.\n");
+			return 0;
+		}
+
+		std::string equipment;
+
+		if (!this->asString(node, "PetEquip", equipment))
+			return 0;
+
+		struct item_data *item = itemdb_searchname(equipment.c_str());
+
+		if (item == nullptr) {
+			this->invalidWarning(node["PetEquip"], "PetEquip %s is not a valid item.\n", equipment.c_str());
+			return 0;
+		}
+
+		mob->vd.head_bottom = item->nameid;
+	}
+
+	if (this->nodeExists(node, "Options")) {
+		for (const auto &optionNode : node["Options"]) {
+			std::string option = optionNode.first.as<std::string>();
+			std::string option_constant = "OPTION_" + option;
+			int constant;
+
+			if (!script_get_constant(option_constant.c_str(), &constant)) {
+				this->invalidWarning(optionNode, "Unknown option constant %s, skipping.\n", option.c_str());
+				continue;
+			}
+
+			bool active;
+
+			if (!this->asBool(node, option, active))
+				continue;
+
+#ifdef NEW_CARTS
+			if (constant & OPTION_CART) {
+				this->invalidWarning(optionNode, "OPTION_CART was replace by SC_PUSH_CART, skipping.\n");
+				continue;
+			}
 #endif
+
+			if (active)
+				mob->option |= constant;
+			else
+				mob->option &= ~constant;
+		}
+
+		mob->option &= ~(OPTION_HIDE | OPTION_CLOAK | OPTION_INVISIBLE | OPTION_CHASEWALK); // Remove hiding types
 	}
-	else if(columns==3)
-		db->vd.head_bottom=atoi(str[2]); // mob equipment [Valaris]
-	else if( columns != 2 )
-		return false;
 
-	return true;
+	return 1;
 }
 
+MobAvailDatabase mob_avail_db;
+
 /*==========================================
  * Reading of random monster data
  * MobGroup,MobID,DummyName,Rate
@@ -5282,7 +5535,6 @@ static void mob_load(void)
 			mob_readskilldb(dbsubpath2, silent);
 		}
 
-		sv_readdb(dbsubpath1, "mob_avail.txt", ',', 2, 12, -1, &mob_readdb_mobavail,silent);
 		sv_readdb(dbsubpath2, "mob_race2_db.txt", ',', 2, MAX_RACE2_MOBS, -1, &mob_readdb_race2, silent);
 		sv_readdb(dbsubpath1, "mob_item_ratio.txt", ',', 2, 2+MAX_ITEMRATIO_MOBS, -1, &mob_readdb_itemratio, silent);
 		sv_readdb(dbsubpath1, "mob_chat_db.txt", '#', 3, 3, -1, &mob_parse_row_chatdb, silent);
@@ -5299,6 +5551,8 @@ static void mob_load(void)
 		aFree(dbsubpath2);
 	}
 
+	mob_avail_db.load();
+
 	mob_drop_ratio_adjust();
 	mob_skill_db_set();
 }
@@ -5392,15 +5646,12 @@ static int mob_reload_sub( struct mob_data *md, va_list args ){
 static int mob_reload_sub_npc( struct npc_data *nd, va_list args ){
 	// If the view data points to a mob
 	if( mobdb_checkid(nd->class_) ){
-		// Get the new view data from the mob database
-		nd->vd = mob_get_viewdata(nd->class_);
+		struct view_data *vd = mob_get_viewdata(nd->class_);
 
-		// If they are spawned right now
-		if( nd->bl.prev != NULL ){
-			// Respawn all NPCs on client side so that they are displayed correctly(if their view id changed)
-			clif_clearunit_area(&nd->bl, CLR_OUTSIGHT);
-			clif_spawn(&nd->bl);
-		}
+		if (vd) // Get the new view data from the mob database
+			memcpy(&nd->vd, vd, sizeof(struct view_data));
+		if (nd->bl.prev) // If they are spawned right now
+			unit_refresh(&nd->bl); // Respawn all NPCs on client side so that they are displayed correctly(if their view id changed)
 	}
 
 	return 0;

+ 12 - 0
src/map/mob.hpp

@@ -6,6 +6,7 @@
 
 #include <vector>
 
+#include "../common/database.hpp"
 #include "../common/mmo.hpp" // struct item
 #include "../common/timer.hpp"
 
@@ -243,6 +244,17 @@ struct mob_data {
 	int tomb_nid;
 };
 
+class MobAvailDatabase : public YamlDatabase {
+public:
+	MobAvailDatabase() : YamlDatabase("MOB_AVAIL_DB", 1) {
+
+	}
+
+	void clear() { };
+	const std::string getDefaultLocation();
+	uint64 parseBodyNode(const YAML::Node& node);
+};
+
 enum e_mob_skill_target {
 	MST_TARGET	=	0,
 	MST_RANDOM,	//Random Target!

+ 12 - 12
src/map/npc.cpp

@@ -118,8 +118,12 @@ struct script_event_s{
 // Holds pointers to the commonly executed scripts for speedup. [Skotlex]
 std::map<enum npce_event, std::vector<struct script_event_s>> script_event;
 
-struct view_data* npc_get_viewdata(int class_)
-{	//Returns the viewdata for normal npc classes.
+/**
+ * Returns the viewdata for normal NPC classes.
+ * @param class_: NPC class ID
+ * @return viewdata or nullptr if the ID is invalid
+ */
+struct view_data* npc_get_viewdata(int class_) {
 	if( class_ == JT_INVISIBLE )
 		return &npc_viewdb[0];
 	if (npcdb_checkid(class_)){
@@ -129,7 +133,7 @@ struct view_data* npc_get_viewdata(int class_)
 			return &npc_viewdb[class_];
 		}
 	}
-	return NULL;
+	return nullptr;
 }
 
 int npc_isnear_sub(struct block_list* bl, va_list args) {
@@ -2556,17 +2560,18 @@ int npc_parseview(const char* w4, const char* start, const char* buffer, const c
  * @return npc_data
  */
 struct npc_data *npc_create_npc(int16 m, int16 x, int16 y){
-	struct npc_data *nd;
+	struct npc_data *nd = nullptr;
 
 	CREATE(nd, struct npc_data, 1);
 	nd->bl.id = npc_get_new_npc_id();
-	nd->bl.prev = nd->bl.next = NULL;
+	nd->bl.prev = nd->bl.next = nullptr;
 	nd->bl.m = m;
 	nd->bl.x = x;
 	nd->bl.y = y;
-	nd->sc_display = NULL;
+	nd->sc_display = nullptr;
 	nd->sc_display_count = 0;
 	nd->progressbar.timeout = 0;
+	nd->vd = npc_viewdb[0]; // Default to JT_INVISIBLE
 
 	return nd;
 }
@@ -3749,14 +3754,9 @@ void npc_setclass(struct npc_data* nd, short class_)
 	if( nd->class_ == class_ )
 		return;
 
-	struct map_data *mapdata = map_getmapdata(nd->bl.m);
-
-	if( mapdata->users )
-		clif_clearunit_area(&nd->bl, CLR_OUTSIGHT);// fade out
 	nd->class_ = class_;
 	status_set_viewdata(&nd->bl, class_);
-	if( mapdata->users )
-		clif_spawn(&nd->bl);// fade in
+	unit_refresh(&nd->bl);
 }
 
 // @commands (script based)

+ 2 - 2
src/map/npc.hpp

@@ -41,8 +41,8 @@ struct s_npc_buy_list {
 
 struct npc_data {
 	struct block_list bl;
-	struct unit_data  ud; //Because they need to be able to move....
-	struct view_data *vd;
+	struct unit_data ud; //Because they need to be able to move....
+	struct view_data vd;
 	struct status_change sc; //They can't have status changes, but.. they want the visual opt values.
 	struct npc_data *master_nd;
 	short class_,speed,instance_id;

+ 35 - 8
src/map/script.cpp

@@ -16542,10 +16542,7 @@ BUILDIN_FUNC(setnpcdisplay)
 	if( class_ != JT_FAKENPC && nd->class_ != class_ )
 		npc_setclass(nd, class_);
 	else if( size != -1 )
-	{ // Required to update the visual size
-		clif_clearunit_area(&nd->bl, CLR_OUTSIGHT);
-		clif_spawn(&nd->bl);
-	}
+		unit_refresh(&nd->bl); // Required to update the visual size
 
 	script_pushint(st,0);
 	return SCRIPT_CMD_SUCCESS;
@@ -17609,6 +17606,8 @@ BUILDIN_FUNC(getunitdata)
 			getunitdata_sub(UMOB_ADELAY, md->status.adelay);
 			getunitdata_sub(UMOB_DMOTION, md->status.dmotion);
 			getunitdata_sub(UMOB_TARGETID, md->target_id);
+			getunitdata_sub(UMOB_ROBE, md->vd->robe);
+			getunitdata_sub(UMOB_BODY2, md->vd->body_style);
 			break;
 
 		case BL_HOM:
@@ -17797,7 +17796,6 @@ BUILDIN_FUNC(getunitdata)
 				ShowWarning("buildin_getunitdata: Error in finding object BL_NPC!\n");
 				return SCRIPT_CMD_FAILURE;
 			}
-			getunitdata_sub(UNPC_DISPLAY, nd->class_);
 			getunitdata_sub(UNPC_LEVEL, nd->level);
 			getunitdata_sub(UNPC_HP, nd->status.hp);
 			getunitdata_sub(UNPC_MAXHP, nd->status.max_hp);
@@ -17830,6 +17828,19 @@ BUILDIN_FUNC(getunitdata)
 			getunitdata_sub(UNPC_AMOTION,  nd->status.amotion);
 			getunitdata_sub(UNPC_ADELAY, nd->status.adelay);
 			getunitdata_sub(UNPC_DMOTION, nd->status.dmotion);
+			getunitdata_sub(UNPC_SEX, nd->vd.sex);
+			getunitdata_sub(UNPC_CLASS, nd->vd.class_);
+			getunitdata_sub(UNPC_HAIRSTYLE, nd->vd.hair_style);
+			getunitdata_sub(UNPC_HAIRCOLOR, nd->vd.hair_color);
+			getunitdata_sub(UNPC_HEADBOTTOM, nd->vd.head_bottom);
+			getunitdata_sub(UNPC_HEADMIDDLE, nd->vd.head_mid);
+			getunitdata_sub(UNPC_HEADTOP, nd->vd.head_top);
+			getunitdata_sub(UNPC_CLOTHCOLOR, nd->vd.cloth_color);
+			getunitdata_sub(UNPC_SHIELD, nd->vd.shield);
+			getunitdata_sub(UNPC_WEAPON, nd->vd.weapon);
+			getunitdata_sub(UNPC_ROBE, nd->vd.robe);
+			getunitdata_sub(UNPC_BODY2, nd->vd.body_style);
+			getunitdata_sub(UNPC_DEADSIT, nd->vd.dead_sit);
 			break;
 
 		default:
@@ -17912,6 +17923,8 @@ BUILDIN_FUNC(setunitdata)
 			case UMOB_CLOTHCOLOR:
 			case UMOB_SHIELD:
 			case UMOB_WEAPON:
+			case UMOB_ROBE:
+			case UMOB_BODY2:
 				mob_set_dynamic_viewdata( md );
 				break;
 		}
@@ -17929,8 +17942,8 @@ BUILDIN_FUNC(setunitdata)
 			case UMOB_MODE: md->base_status->mode = (enum e_mode)value; calc_status = true; break;
 			case UMOB_AI: md->special_state.ai = (enum mob_ai)value; break;
 			case UMOB_SCOPTION: md->sc.option = (unsigned short)value; break;
-			case UMOB_SEX: md->vd->sex = (char)value; clif_clearunit_area(bl, CLR_OUTSIGHT); clif_spawn(bl); break;
-			case UMOB_CLASS: status_set_viewdata(bl, (unsigned short)value); clif_clearunit_area(bl, CLR_OUTSIGHT); clif_spawn(bl); break;
+			case UMOB_SEX: md->vd->sex = (char)value; unit_refresh(bl); break;
+			case UMOB_CLASS: status_set_viewdata(bl, (unsigned short)value); unit_refresh(bl); break;
 			case UMOB_HAIRSTYLE: clif_changelook(bl, LOOK_HAIR, (unsigned short)value); break;
 			case UMOB_HAIRCOLOR: clif_changelook(bl, LOOK_HAIR_COLOR, (unsigned short)value); break;
 			case UMOB_HEADBOTTOM: clif_changelook(bl, LOOK_HEAD_BOTTOM, (unsigned short)value); break;
@@ -17987,6 +18000,8 @@ BUILDIN_FUNC(setunitdata)
 				mob_target(md,target,0);
 				break;
 			}
+			case UMOB_ROBE: clif_changelook(bl, LOOK_ROBE, (unsigned short)value); break;
+			case UMOB_BODY2: clif_changelook(bl, LOOK_BODY2, (unsigned short)value); break;
 			default:
 				ShowError("buildin_setunitdata: Unknown data identifier %d for BL_MOB.\n", type);
 				return SCRIPT_CMD_FAILURE;
@@ -18233,7 +18248,6 @@ BUILDIN_FUNC(setunitdata)
 			return SCRIPT_CMD_FAILURE;
 		}
 		switch (type) {
-			case UNPC_DISPLAY: status_set_viewdata(bl, (unsigned short)value); break;
 			case UNPC_LEVEL: nd->level = (unsigned int)value; break;
 			case UNPC_HP: nd->status.hp = (unsigned int)value; status_set_hp(bl, (unsigned int)value, 0); break;
 			case UNPC_MAXHP: nd->status.hp = nd->status.max_hp = (unsigned int)value; status_set_maxhp(bl, (unsigned int)value, 0); break;
@@ -18265,6 +18279,19 @@ BUILDIN_FUNC(setunitdata)
 			case UNPC_AMOTION: nd->status.amotion = (short)value; break;
 			case UNPC_ADELAY: nd->status.adelay = (short)value; break;
 			case UNPC_DMOTION: nd->status.dmotion = (short)value; break;
+			case UNPC_SEX: nd->vd.sex = (char)value; unit_refresh(bl); break;
+			case UNPC_CLASS: npc_setclass(nd, (short)value); break;
+			case UNPC_HAIRSTYLE: clif_changelook(bl, LOOK_HAIR, (unsigned short)value); break;
+			case UNPC_HAIRCOLOR: clif_changelook(bl, LOOK_HAIR_COLOR, (unsigned short)value); break;
+			case UNPC_HEADBOTTOM: clif_changelook(bl, LOOK_HEAD_BOTTOM, (unsigned short)value); break;
+			case UNPC_HEADMIDDLE: clif_changelook(bl, LOOK_HEAD_MID, (unsigned short)value); break;
+			case UNPC_HEADTOP: clif_changelook(bl, LOOK_HEAD_TOP, (unsigned short)value); break;
+			case UNPC_CLOTHCOLOR: clif_changelook(bl, LOOK_CLOTHES_COLOR, (unsigned short)value); break;
+			case UNPC_SHIELD: clif_changelook(bl, LOOK_SHIELD, (unsigned short)value); break;
+			case UNPC_WEAPON: clif_changelook(bl, LOOK_WEAPON, (unsigned short)value); break;
+			case UNPC_ROBE: clif_changelook(bl, LOOK_ROBE, (unsigned short)value); break;
+			case UNPC_BODY2: clif_changelook(bl, LOOK_BODY2, (unsigned short)value); break;
+			case UNPC_DEADSIT: nd->vd.dead_sit = (char)value; unit_refresh(bl); break;
 			default:
 				ShowError("buildin_setunitdata: Unknown data identifier %d for BL_NPC.\n", type);
 				return SCRIPT_CMD_FAILURE;

+ 16 - 2
src/map/script.hpp

@@ -476,6 +476,8 @@ enum unitdata_mobtypes {
 	UMOB_ADELAY,
 	UMOB_DMOTION,
 	UMOB_TARGETID,
+	UMOB_ROBE,
+	UMOB_BODY2,
 };
 
 enum unitdata_homuntypes {
@@ -644,8 +646,7 @@ enum unitdata_elemtypes {
 };
 
 enum unitdata_npctypes {
-	UNPC_DISPLAY = 0,
-	UNPC_LEVEL,
+	UNPC_LEVEL = 0,
 	UNPC_HP,
 	UNPC_MAXHP,
 	UNPC_MAPID,
@@ -677,6 +678,19 @@ enum unitdata_npctypes {
 	UNPC_AMOTION,
 	UNPC_ADELAY,
 	UNPC_DMOTION,
+	UNPC_SEX,
+	UNPC_CLASS,
+	UNPC_HAIRSTYLE,
+	UNPC_HAIRCOLOR,
+	UNPC_HEADBOTTOM,
+	UNPC_HEADMIDDLE,
+	UNPC_HEADTOP,
+	UNPC_CLOTHCOLOR,
+	UNPC_SHIELD,
+	UNPC_WEAPON,
+	UNPC_ROBE,
+	UNPC_BODY2,
+	UNPC_DEADSIT,
 };
 
 enum navigation_service {

+ 25 - 2
src/map/script_constants.hpp

@@ -3964,9 +3964,14 @@
 	export_constant(OPTION_SIGHT);
 	export_constant(OPTION_HIDE);
 	export_constant(OPTION_CLOAK);
+	export_constant(OPTION_CART1);
 	export_constant(OPTION_FALCON);
 	export_constant(OPTION_RIDING);
 	export_constant(OPTION_INVISIBLE);
+	export_constant(OPTION_CART2);
+	export_constant(OPTION_CART3);
+	export_constant(OPTION_CART4);
+	export_constant(OPTION_CART5);
 	export_constant(OPTION_ORCISH);
 	export_constant(OPTION_WEDDING);
 	export_constant(OPTION_RUWACH);
@@ -3985,6 +3990,7 @@
 	export_constant(OPTION_DRAGON5);
 	export_constant(OPTION_HANBOK);
 	export_constant(OPTION_OKTOBERFEST);
+	export_constant(OPTION_SUMMER2);
 
 	/* status option compounds */
 	export_constant(OPTION_DRAGON);
@@ -4059,6 +4065,8 @@
 	export_constant(UMOB_ADELAY);
 	export_constant(UMOB_DMOTION);
 	export_constant(UMOB_TARGETID);
+	export_constant(UMOB_ROBE);
+	export_constant(UMOB_BODY2);
 
 	/* unit control - homunculus */
 	export_constant(UHOM_SIZE);
@@ -4222,7 +4230,7 @@
 	export_constant(UELE_TARGETID);
 
 	/* unit control - NPC */
-	export_constant(UNPC_DISPLAY);
+	export_deprecated_constant3("UNPC_DISPLAY", UNPC_CLASS, "UNPC_CLASS");
 	export_constant(UNPC_LEVEL);
 	export_constant(UNPC_HP);
 	export_constant(UNPC_MAXHP);
@@ -4255,6 +4263,19 @@
 	export_constant(UNPC_AMOTION);
 	export_constant(UNPC_ADELAY);
 	export_constant(UNPC_DMOTION);
+	export_constant(UNPC_SEX);
+	export_constant(UNPC_CLASS);
+	export_constant(UNPC_HAIRSTYLE);
+	export_constant(UNPC_HAIRCOLOR);
+	export_constant(UNPC_HEADBOTTOM);
+	export_constant(UNPC_HEADMIDDLE);
+	export_constant(UNPC_HEADTOP);
+	export_constant(UNPC_CLOTHCOLOR);
+	export_constant(UNPC_SHIELD);
+	export_constant(UNPC_WEAPON);
+	export_constant(UNPC_ROBE);
+	export_constant(UNPC_BODY2);
+	export_constant(UNPC_DEADSIT);
 
 	export_constant(NAV_NONE);
 	export_constant(NAV_AIRSHIP_ONLY);
@@ -4954,7 +4975,9 @@
 
 	/* NPC view ids */
 	// Special macro to strip the prefix 'JT_'
-	#define export_constant_npc(a) export_constant_offset(a,3)
+	#if !defined(export_constant_npc)
+		#define export_constant_npc(a) export_constant_offset(a,3)
+	#endif
 
 	export_constant_npc(JT_WARPNPC);
 	export_constant_npc(JT_1_ETC_01);

+ 14 - 9
src/map/status.cpp

@@ -7797,7 +7797,7 @@ struct view_data* status_get_viewdata(struct block_list *bl)
 		case BL_PC:  return &((TBL_PC*)bl)->vd;
 		case BL_MOB: return ((TBL_MOB*)bl)->vd;
 		case BL_PET: return &((TBL_PET*)bl)->vd;
-		case BL_NPC: return ((TBL_NPC*)bl)->vd;
+		case BL_NPC: return &((TBL_NPC*)bl)->vd;
 		case BL_HOM: return ((TBL_HOM*)bl)->vd;
 		case BL_MER: return ((TBL_MER*)bl)->vd;
 		case BL_ELEM: return ((TBL_ELEM*)bl)->vd;
@@ -7861,10 +7861,10 @@ void status_set_viewdata(struct block_list *bl, int class_)
 				sd->vd.head_top = sd->status.head_top;
 				sd->vd.head_mid = sd->status.head_mid;
 				sd->vd.head_bottom = sd->status.head_bottom;
-				sd->vd.hair_style = cap_value(sd->status.hair,0,battle_config.max_hair_style);
-				sd->vd.hair_color = cap_value(sd->status.hair_color,0,battle_config.max_hair_color);
-				sd->vd.cloth_color = cap_value(sd->status.clothes_color,0,battle_config.max_cloth_color);
-				sd->vd.body_style = cap_value(sd->status.body,0,battle_config.max_body_style);
+				sd->vd.hair_style = cap_value(sd->status.hair, MIN_HAIR_STYLE, MAX_HAIR_STYLE);
+				sd->vd.hair_color = cap_value(sd->status.hair_color, MIN_HAIR_COLOR, MAX_HAIR_COLOR);
+				sd->vd.cloth_color = cap_value(sd->status.clothes_color, MIN_CLOTH_COLOR, MAX_CLOTH_COLOR);
+				sd->vd.body_style = cap_value(sd->status.body, MIN_BODY_STYLE, MAX_BODY_STYLE);
 				sd->vd.sex = sd->status.sex;
 
 				if (sd->vd.cloth_color) {
@@ -7898,6 +7898,8 @@ void status_set_viewdata(struct block_list *bl, int class_)
 				mob_set_dynamic_viewdata( md );
 
 				md->vd->class_ = class_;
+				md->vd->hair_style = cap_value(md->vd->hair_style, MIN_HAIR_STYLE, MAX_HAIR_STYLE);
+				md->vd->hair_color = cap_value(md->vd->hair_color, MIN_HAIR_COLOR, MAX_HAIR_COLOR);
 			}else
 				ShowError("status_set_viewdata (MOB): No view data for class %d\n", class_);
 		}
@@ -7923,15 +7925,18 @@ void status_set_viewdata(struct block_list *bl, int class_)
 		{
 			TBL_NPC* nd = (TBL_NPC*)bl;
 			if (vd)
-				nd->vd = vd;
-			else {
-				ShowError("status_set_viewdata (NPC): No view data for class %d\n", class_);
+				memcpy(&nd->vd, vd, sizeof(struct view_data));
+			else if (pcdb_checkid(class_)) {
+				memset(&nd->vd, 0, sizeof(struct view_data));
+				nd->vd.class_ = class_;
+				nd->vd.hair_style = cap_value(nd->vd.hair_style, MIN_HAIR_STYLE, MAX_HAIR_STYLE);
+			} else {
 				if (bl->m >= 0)
 					ShowDebug("Source (NPC): %s at %s (%d,%d)\n", nd->name, map_mapid2mapname(bl->m), bl->x, bl->y);
 				else
 					ShowDebug("Source (NPC): %s (invisible/not on a map)\n", nd->name);
-				break;
 			}
+			break;
 		}
 	break;
 	case BL_HOM:

+ 20 - 0
src/map/unit.cpp

@@ -3134,6 +3134,26 @@ int unit_remove_map_(struct block_list *bl, clr_type clrtype, const char* file,
 	return 1;
 }
 
+/**
+ * Refresh the area with a change in display of a unit.
+ * @bl: Object to update
+ */
+void unit_refresh(struct block_list *bl) {
+	nullpo_retv(bl);
+
+	if (bl->m < 0)
+		return;
+
+	struct map_data *mapdata = map_getmapdata(bl->m);
+
+	// Using CLR_TRICKDEAD because other flags show effects
+	// Probably need to use another flag or other way to refresh it
+	if (mapdata->users) {
+		clif_clearunit_area(bl, CLR_TRICKDEAD); // Fade out
+		clif_spawn(bl); // Fade in
+	}
+}
+
 /**
  * Removes units of a master when the master is removed from map
  * @param sd: Player

+ 2 - 1
src/map/unit.hpp

@@ -79,7 +79,7 @@ unsigned short
 		cloth_color,
 		body_style;
 	char sex;
-	unsigned dead_sit : 2;
+	unsigned dead_sit : 2; // 0: Standing, 1: Dead, 2: Sitting
 };
 
 /// Enum for unit_blown_immune
@@ -161,6 +161,7 @@ void unit_dataset(struct block_list *bl);
 // Remove unit
 struct unit_data* unit_bl2ud(struct block_list *bl);
 void unit_remove_map_pc(struct map_session_data *sd, clr_type clrtype);
+void unit_refresh(struct block_list *bl);
 void unit_free_pc(struct map_session_data *sd);
 #define unit_remove_map(bl,clrtype) unit_remove_map_(bl,clrtype,__FILE__,__LINE__,__func__)
 int unit_remove_map_(struct block_list *bl, clr_type clrtype, const char* file, int line, const char* func);

+ 243 - 1
src/tool/csv2yaml.cpp

@@ -43,9 +43,9 @@
 #include "../map/npc.hpp"
 #include "../map/pc.hpp"
 #include "../map/pet.hpp"
+#include "../map/quest.hpp"
 #include "../map/script.hpp"
 #include "../map/storage.hpp"
-#include "../map/quest.hpp"
 
 using namespace rathena;
 
@@ -73,9 +73,11 @@ static bool skill_parse_row_magicmushroomdb(char* split[], int column, int curre
 static bool skill_parse_row_abradb(char* split[], int columns, int current);
 static bool skill_parse_row_improvisedb(char* split[], int columns, int current);
 static bool skill_parse_row_spellbookdb(char* split[], int columns, int current);
+static bool mob_readdb_mobavail(char *str[], int columns, int current);
 
 // Constants for conversion
 std::unordered_map<uint16, std::string> aegis_itemnames;
+std::unordered_map<uint16, uint16> aegis_itemviewid;
 std::unordered_map<uint16, std::string> aegis_mobnames;
 std::unordered_map<uint16, std::string> aegis_skillnames;
 std::unordered_map<const char*, int32> constants;
@@ -218,6 +220,7 @@ int do_init( int argc, char** argv ){
 	sv_readdb( path_db_import.c_str(), "skill_db.txt", ',', 18, 18, -1, parse_skill_constants, false );
 
 	// Load constants
+	#define export_constant_npc(a) export_constant(a)
 	#include "../map/script_constants.hpp"
 
 	std::vector<std::string> root_paths = {
@@ -262,6 +265,12 @@ int do_init( int argc, char** argv ){
 		return 0;
 	}
 
+	if (!process("MOB_AVAIL_DB", 1, root_paths, "mob_avail", [](const std::string& path, const std::string& name_ext) -> bool {
+		return sv_readdb(path.c_str(), name_ext.c_str(), ',', 2, 12, -1, &mob_readdb_mobavail, false);
+	})) {
+		return 0;
+	}
+
 	// TODO: add implementations ;-)
 
 	return 0;
@@ -419,6 +428,9 @@ static bool parse_item_constants( const char* path ){
 
 		aegis_itemnames[item_id] = std::string(name);
 
+		if (atoi(str[14]) & (EQP_HELM | EQP_COSTUME_HELM) && util::umap_find(aegis_itemviewid, (uint16)atoi(str[18])) == nullptr)
+			aegis_itemviewid[atoi(str[18])] = item_id;
+
 		count++;
 	}
 
@@ -808,3 +820,233 @@ static bool skill_parse_row_spellbookdb(char* split[], int columns, int current)
 
 	return true;
 }
+
+// Copied and adjusted from mob.cpp
+static bool mob_readdb_mobavail(char* str[], int columns, int current) {
+	uint16 mob_id = atoi(str[0]);
+	std::string *mob_name = util::umap_find(aegis_mobnames, mob_id);
+
+	if (mob_name == nullptr) {
+		ShowWarning("mob_avail reading: Invalid mob-class %hu, Mob not read.\n", mob_id);
+		return false;
+	}
+
+	body << YAML::Key << "Mob" << YAML::Value << *mob_name;
+
+	uint16 sprite_id = atoi(str[1]);
+	std::string *sprite_name = util::umap_find(aegis_mobnames, sprite_id);
+
+	if (sprite_name == nullptr) {
+		char *sprite = const_cast<char *>(constant_lookup(sprite_id, "JOB_"));
+
+		if (sprite == nullptr) {
+			sprite = const_cast<char *>(constant_lookup(sprite_id, "JT_"));
+
+			if (sprite == nullptr) {
+				ShowError("Sprite name %s is not known.\n", sprite);
+				return false;
+			}
+
+			sprite += 3; // Strip JT_ here because the script engine doesn't send this prefix for NPC.
+
+			body << YAML::Key << "Sprite" << YAML::Value << *sprite;
+		} else
+			body << YAML::Key << "Sprite" << YAML::Value << *sprite;
+	} else
+		body << YAML::Key << "Sprite" << YAML::Value << *sprite_name;
+
+	if (columns == 12) {
+		body << YAML::Key << "Sex" << YAML::Value << (atoi(str[2]) ? "Male" : "Female");
+		if (atoi(str[3]) != 0)
+			body << YAML::Key << "HairStyle" << YAML::Value << atoi(str[3]);
+		if (atoi(str[4]) != 0)
+			body << YAML::Key << "HairColor" << YAML::Value << atoi(str[4]);
+		if (atoi(str[11]) != 0)
+			body << YAML::Key << "ClothColor" << YAML::Value << atoi(str[11]);
+
+		if (atoi(str[5]) != 0) {
+			uint16 weapon_item_id = atoi(str[5]);
+			std::string *weapon_item_name = util::umap_find(aegis_itemnames, weapon_item_id);
+
+			if (weapon_item_name == nullptr) {
+				ShowError("Item name for item ID %hu (weapon) is not known.\n", weapon_item_id);
+				return false;
+			}
+
+			body << YAML::Key << "Weapon" << YAML::Value << *weapon_item_name;
+		}
+
+		if (atoi(str[6]) != 0) {
+			uint16 shield_item_id = atoi(str[6]);
+			std::string *shield_item_name = util::umap_find(aegis_itemnames, shield_item_id);
+
+			if (shield_item_name == nullptr) {
+				ShowError("Item name for item ID %hu (shield) is not known.\n", shield_item_id);
+				return false;
+			}
+
+			body << YAML::Key << "Shield" << YAML::Value << *shield_item_name;
+		}
+
+		if (atoi(str[7]) != 0) {
+			uint16 *headtop_item_id = util::umap_find(aegis_itemviewid, (uint16)atoi(str[7]));
+
+			if (headtop_item_id == nullptr) {
+				ShowError("Item ID for view ID %hu (head top) is not known.\n", atoi(str[7]));
+				return false;
+			}
+
+			std::string *headtop_item_name = util::umap_find(aegis_itemnames, *headtop_item_id);
+
+			if (headtop_item_name == nullptr) {
+				ShowError("Item name for item ID %hu (head top) is not known.\n", *headtop_item_id);
+				return false;
+			}
+
+			body << YAML::Key << "HeadTop" << YAML::Value << *headtop_item_name;
+		}
+
+		if (atoi(str[8]) != 0) {
+			uint16 *headmid_item_id = util::umap_find(aegis_itemviewid, (uint16)atoi(str[8]));
+
+			if (headmid_item_id == nullptr) {
+				ShowError("Item ID for view ID %hu (head mid) is not known.\n", atoi(str[8]));
+				return false;
+			}
+
+			std::string *headmid_item_name = util::umap_find(aegis_itemnames, *headmid_item_id);
+
+			if (headmid_item_name == nullptr) {
+				ShowError("Item name for item ID %hu (head mid) is not known.\n", *headmid_item_id);
+				return false;
+			}
+
+			body << YAML::Key << "HeadMid" << YAML::Value << *headmid_item_name;
+		}
+
+		if (atoi(str[9]) != 0) {
+			uint16 *headlow_item_id = util::umap_find(aegis_itemviewid, (uint16)atoi(str[9]));
+
+			if (headlow_item_id == nullptr) {
+				ShowError("Item ID for view ID %hu (head low) is not known.\n", atoi(str[9]));
+				return false;
+			}
+
+			std::string *headlow_item_name = util::umap_find(aegis_itemnames, *headlow_item_id);
+
+			if (headlow_item_name == nullptr) {
+				ShowError("Item name for item ID %hu (head low) is not known.\n", *headlow_item_id);
+				return false;
+			}
+
+			body << YAML::Key << "HeadLow" << YAML::Value << *headlow_item_name;
+		}
+
+		if (atoi(str[10]) != 0) {
+			uint32 options = atoi(str[10]);
+
+			body << YAML::Key << "Options";
+			body << YAML::BeginMap;
+
+			while (options > OPTION_NOTHING && options <= OPTION_SUMMER2) {
+				if (options & OPTION_SIGHT) {
+					body << YAML::Key << "Sight" << YAML::Value << "true";
+					options &= ~OPTION_SIGHT;
+				} else if (options & OPTION_CART1) {
+					body << YAML::Key << "Cart1" << YAML::Value << "true";
+					options &= ~OPTION_CART1;
+				} else if (options & OPTION_FALCON) {
+					body << YAML::Key << "Falcon" << YAML::Value << "true";
+					options &= ~OPTION_FALCON;
+				} else if (options & OPTION_RIDING) {
+					body << YAML::Key << "Riding" << YAML::Value << "true";
+					options &= ~OPTION_RIDING;
+				} else if (options & OPTION_CART2) {
+					body << YAML::Key << "Cart2" << YAML::Value << "true";
+					options &= ~OPTION_CART2;
+				} else if (options & OPTION_CART3) {
+					body << YAML::Key << "Cart2" << YAML::Value << "true";
+					options &= ~OPTION_CART3;
+				} else if (options & OPTION_CART4) {
+					body << YAML::Key << "Cart4" << YAML::Value << "true";
+					options &= ~OPTION_CART4;
+				} else if (options & OPTION_CART5) {
+					body << YAML::Key << "Cart5" << YAML::Value << "true";
+					options &= ~OPTION_CART5;
+				} else if (options & OPTION_ORCISH) {
+					body << YAML::Key << "Orcish" << YAML::Value << "true";
+					options &= ~OPTION_ORCISH;
+				} else if (options & OPTION_WEDDING) {
+					body << YAML::Key << "Wedding" << YAML::Value << "true";
+					options &= ~OPTION_WEDDING;
+				} else if (options & OPTION_RUWACH) {
+					body << YAML::Key << "Ruwach" << YAML::Value << "true";
+					options &= ~OPTION_RUWACH;
+				} else if (options & OPTION_FLYING) {
+					body << YAML::Key << "Flying" << YAML::Value << "true";
+					options &= ~OPTION_FLYING;
+				} else if (options & OPTION_XMAS) {
+					body << YAML::Key << "Xmas" << YAML::Value << "true";
+					options &= ~OPTION_XMAS;
+				} else if (options & OPTION_TRANSFORM) {
+					body << YAML::Key << "Transform" << YAML::Value << "true";
+					options &= ~OPTION_TRANSFORM;
+				} else if (options & OPTION_SUMMER) {
+					body << YAML::Key << "Summer" << YAML::Value << "true";
+					options &= ~OPTION_SUMMER;
+				} else if (options & OPTION_DRAGON1) {
+					body << YAML::Key << "Dragon1" << YAML::Value << "true";
+					options &= ~OPTION_DRAGON1;
+				} else if (options & OPTION_WUG) {
+					body << YAML::Key << "Wug" << YAML::Value << "true";
+					options &= ~OPTION_WUG;
+				} else if (options & OPTION_WUGRIDER) {
+					body << YAML::Key << "WugRider" << YAML::Value << "true";
+					options &= ~OPTION_WUGRIDER;
+				} else if (options & OPTION_MADOGEAR) {
+					body << YAML::Key << "MadoGear" << YAML::Value << "true";
+					options &= ~OPTION_MADOGEAR;
+				} else if (options & OPTION_DRAGON2) {
+					body << YAML::Key << "Dragon2" << YAML::Value << "true";
+					options &= ~OPTION_DRAGON2;
+				} else if (options & OPTION_DRAGON3) {
+					body << YAML::Key << "Dragon3" << YAML::Value << "true";
+					options &= ~OPTION_DRAGON3;
+				} else if (options & OPTION_DRAGON4) {
+					body << YAML::Key << "Dragon4" << YAML::Value << "true";
+					options &= ~OPTION_DRAGON4;
+				} else if (options & OPTION_DRAGON5) {
+					body << YAML::Key << "Dragon5" << YAML::Value << "true";
+					options &= ~OPTION_DRAGON5;
+				} else if (options & OPTION_HANBOK) {
+					body << YAML::Key << "Hanbok" << YAML::Value << "true";
+					options &= ~OPTION_HANBOK;
+				} else if (options & OPTION_OKTOBERFEST) {
+					body << YAML::Key << "Oktoberfest" << YAML::Value << "true";
+					options &= ~OPTION_OKTOBERFEST;
+				} else if (options & OPTION_SUMMER2) {
+					body << YAML::Key << "Summer2" << YAML::Value << "true";
+					options &= ~OPTION_SUMMER2;
+				}
+			}
+
+			body << YAML::EndMap;
+		}
+	} else if (columns == 3) {
+		if (atoi(str[5]) != 0) {
+			uint16 peteq_item_id = atoi(str[5]);
+			std::string *peteq_item_name = util::umap_find(aegis_itemnames, peteq_item_id);
+
+			if (peteq_item_name == nullptr) {
+				ShowError("Item name for item ID %hu (pet equip) is not known.\n", peteq_item_id);
+				return false;
+			}
+
+			body << YAML::Key << "PetEquip" << YAML::Value << *peteq_item_name;
+		}
+	}
+
+	body << YAML::EndMap;
+
+	return true;
+}