Browse Source

Synchronize Damage Feature (#8305)

- Added a new monster stat "ClientAttackMotion" to mob_db.yml which is the time from when a monster attacks until which the damage shows on the client at 1x speed
- Added a new config synchronize_damage; when set to "yes", the client will display the damage of normal monster attacks at the exact time it is applied on the server, removing position lag (fixes #259)

Special thanks to all people who worked together to make this possible.

Co-authored-by: aleos, Lemongrass3110, Atemo
Playtester 11 months ago
parent
commit
b4ae40d401

+ 11 - 2
conf/battle/battle.conf

@@ -130,10 +130,19 @@ equip_self_break_rate: 100
 // This affects the behaviour of skills like acid terror and meltdown
 equip_skill_break_rate: 100
 
-// Do weapon attacks have a attack speed delay before actual damage is applied? (Note 1)
-// NOTE: The official setting is yes, even thought it degrades performance a bit.
+// Should damage have a delay before it is applied? (Note 1)
+// Some skills might not have a delay by default regardless of this setting.
+// The official setting is yes, even thought it degrades performance a bit.
 delay_battle_damage: yes
 
+// Should the damage timing be synchronized between the client and server? (Note 1)
+// This is not official behavior, but it should remove the position lag after being hit by a monster.
+// This setting only affects normal monster attacks and takes priority over "delay_battle_damage".
+// Many skills show their damage immediately, so setting "delay_battle_damage" to "no" at the same
+// time might improve the experience further, but will not work for all skills.
+// Tired of Dark Illusion hitting you 5 seconds too late? Then turn this on.
+synchronize_damage: no
+
 // Are arrows/ammo consumed when used on a bow/gun?
 // 0 = No
 // 1 = Yes

+ 2 - 1
db/import-tmpl/mob_db.yml

@@ -56,6 +56,7 @@
 #   WalkSpeed               Walk speed. (Default: DEFAULT_WALK_SPEED)
 #   AttackDelay             Attack speed. (Default: 0)
 #   AttackMotion            Attack animation speed. (Default: 0)
+#   ClientAttackMotion      Client attack speed. (Default: AttackMotion)
 #   DamageMotion            Damage animation speed. (Default: 0)
 #   DamageTaken             Rate at which the monster will receive incoming damage. (Default: 100)
 #   Ai                      Aegis monster type AI behavior. (Default: 06)
@@ -77,7 +78,7 @@
 
 Header:
   Type: MOB_DB
-  Version: 3
+  Version: 4
 
 #Body:
 # eAthena Dev Team

+ 2 - 1
db/mob_db.yml

@@ -56,6 +56,7 @@
 #   WalkSpeed               Walk speed. (Default: DEFAULT_WALK_SPEED)
 #   AttackDelay             Attack speed. (Default: 0)
 #   AttackMotion            Attack animation speed. (Default: 0)
+#   ClientAttackMotion      Client attack speed. (Default: AttackMotion)
 #   DamageMotion            Damage animation speed. (Default: 0)
 #   DamageTaken             Rate at which the monster will receive incoming damage. (Default: 100)
 #   Ai                      Aegis monster type AI behavior. (Default: 06)
@@ -77,7 +78,7 @@
 
 Header:
   Type: MOB_DB
-  Version: 3
+  Version: 4
 
 Footer:
   Imports:

File diff suppressed because it is too large
+ 125 - 1
db/pre-re/mob_db.yml


File diff suppressed because it is too large
+ 126 - 2
db/re/mob_db.yml


+ 12 - 0
doc/mob_db.txt

@@ -204,6 +204,18 @@ AttackMotion: Attack animation motion. Low value means monster's attack will be
 
 ---------------------------------------
 
+ClientAttackMotion: The time from the start of a normal attack until the damage frame shows on client. At the same time you also get stopped on the client.
+This value is only needed if you use the "synchronize_damage" feature (battle/battle.conf).
+
+If you created a custom sprite, you want to set this value to the timing of the damage frame in your *.act file.
+In Act Editor you can set the damage frame by setting the frame sound to "atk". If you don't define a damage frame, it will default to the second to last
+frame. Also keep in mind that the Act Editor displays slightly inaccurate speed. Every 25ms in Act Editor is 24ms in reality.
+
+Example: Drops has a animation speed of 24ms per frame and the 13th frame (frame #12) is the damage frame. This means the damage shows after 12 frames.
+That's why Drops has a ClientAttackMotion of 24*12 = 288.
+
+---------------------------------------
+
 DamageMotion: Damage animation motion, same as aMotion but used to display the "I am hit" animation. Coincidentally, this same value is used to determine how long it is before the monster/player can move again. Endure is dMotion = 0, obviously.
 
 ---------------------------------------

+ 1 - 0
doc/yaml/db/mob_db.yml

@@ -39,6 +39,7 @@
 #   WalkSpeed               Walk speed. (Default: DEFAULT_WALK_SPEED)
 #   AttackDelay             Attack speed. (Default: 0)
 #   AttackMotion            Attack animation speed. (Default: 0)
+#   ClientAttackMotion      Client attack speed. (Default: AttackMotion)
 #   DamageMotion            Damage animation speed. (Default: 0)
 #   DamageTaken             Rate at which the monster will receive incoming damage. (Default: 100)
 #   Ai                      Aegis monster type AI behavior. (Default: 06)

+ 15 - 3
src/map/battle.cpp

@@ -393,7 +393,20 @@ int battle_delay_damage(t_tick tick, int amotion, struct block_list *src, struct
 		damage = 0;
 	}
 
-	if ( !battle_config.delay_battle_damage || amotion <= 1 ) {
+	// The client refuses to display animations slower than 1x speed
+	// So we need to shorten AttackMotion to be in-sync with the client in this case
+	if (battle_config.synchronize_damage && skill_id == 0 && src->type == BL_MOB && amotion > status_get_clientamotion(src))
+		amotion = status_get_clientamotion(src);
+	// Check for delay battle damage config
+	else if (!battle_config.delay_battle_damage)
+		amotion = 1;
+	// Aegis places a damage-delay cap of 1 sec to non player attacks
+	// We only want to apply this cap if damage was not synchronized
+	else if (src->type != BL_PC && amotion > 1000)
+		amotion = 1000;
+
+	// Skip creation of timer
+	if (amotion <= 1) {
 		//Deal damage
 		battle_damage(src, target, damage, ddelay, skill_lv, skill_id, dmg_lv, attack_type, additional_effects, gettick(), isspdamage);
 		return 0;
@@ -411,8 +424,6 @@ int battle_delay_damage(t_tick tick, int amotion, struct block_list *src, struct
 	dat->additional_effects = additional_effects;
 	dat->src_type = src->type;
 	dat->isspdamage = isspdamage;
-	if (src->type != BL_PC && amotion > 1000)
-		amotion = 1000; //Aegis places a damage-delay cap of 1 sec to non player attacks. [Skotlex]
 
 	if( src->type == BL_PC )
 		((TBL_PC*)src)->delayed_damage++;
@@ -11505,6 +11516,7 @@ static const struct _battle_data {
 #else
 	{ "feature.instance_allow_reconnect",   &battle_config.instance_allow_reconnect,        0,      0,      1,              },
 #endif
+	{ "synchronize_damage",                 &battle_config.synchronize_damage,              0,      0,      1,              },
 
 #include <custom/battle_config_init.inc>
 };

+ 1 - 0
src/map/battle.hpp

@@ -755,6 +755,7 @@ struct Battle_Config
 	int feature_stylist;
 	int feature_banking_state_enforce;
 	int instance_allow_reconnect;
+	int synchronize_damage;
 
 #include <custom/battle_config_struct.inc>
 };

+ 25 - 0
src/map/clif.cpp

@@ -5177,6 +5177,8 @@ static int clif_hallucination_damage()
 	return (rnd() % 32767);
 }
 
+#define DEFAULT_ANIMATION_SPEED 432
+
 /// Sends a 'damage' packet (src performs action on dst)
 /// 008a <src ID>.L <dst ID>.L <server tick>.L <src speed>.L <dst speed>.L <damage>.W <div>.W <type>.B <damage2>.W (ZC_NOTIFY_ACT)
 /// 02e1 <src ID>.L <dst ID>.L <server tick>.L <src speed>.L <dst speed>.L <damage>.L <div>.W <type>.B <damage2>.L (ZC_NOTIFY_ACT2)
@@ -5226,6 +5228,29 @@ int clif_damage(struct block_list* src, struct block_list* dst, t_tick tick, int
 		}
 	}
 
+	// Calculate what sdelay to send to the client so it applies damage at the same time as the server
+	if (battle_config.synchronize_damage && src->type == BL_MOB) {
+		// When a clif_damage packet is sent to the client it will also send "sdelay" (amotion) as value.
+		// The client however does not interpret this value as AttackMotion but incorrectly as an inverted
+		// animation speed modifier, with 432 standing for 1x animation speed.
+		// The client will ignore all values above 432, but lower values will speed up the animation.
+		// 216 for example means play the animation at double the speed. 108 is quadruple speed.
+		// Each monster has an attack animation and may define the frame in the attack animation on which
+		// it displays the damage and makes the target flinch / stop. If the damage frame is undefined,
+		// it instead displays the damage / flinch / stop at the beginning of the second to last frame.
+		// We define the time after which the damage frame shows at 1x speed as clientamotion.
+		uint16 clientamotion = std::max((uint16)1, status_get_clientamotion(src));
+
+		// Knowing when the damage frame happens in the animation allows us to synchronize the timing
+		// between client and server using the formula below.
+		sdelay = sdelay * DEFAULT_ANIMATION_SPEED / clientamotion;
+
+		// Hint: If amotion is larger than clientamotion this results in a value above 432 which makes the
+		// client display the attack at 1x speed. In this case we need to shorten the delay damage timer
+		// on the server to clientamotion ms instead (see battle_delay_damage).
+		sdelay = std::min(sdelay, DEFAULT_ANIMATION_SPEED);
+	}
+
 	WBUFW(buf,0) = cmd;
 	WBUFL(buf,2) = src->id;
 	WBUFL(buf,6) = dst->id;

+ 13 - 0
src/map/mob.cpp

@@ -4461,6 +4461,7 @@ s_mob_db::s_mob_db()
 	status.speed = DEFAULT_WALK_SPEED;
 	status.adelay = cap_value(0, battle_config.monster_max_aspd * 2, 4000);
 	status.amotion = cap_value(0, battle_config.monster_max_aspd, 2000);
+	status.clientamotion = cap_value(status.amotion, 1, USHRT_MAX);
 	status.mode = static_cast<e_mode>(MONSTER_TYPE_06);
 
 	vd.class_ = id;
@@ -4884,6 +4885,18 @@ uint64 MobDatabase::parseBodyNode(const ryml::NodeRef& node) {
 		mob->status.amotion = cap_value(speed, battle_config.monster_max_aspd, 2000);
 	}
 
+	if (this->nodeExists(node, "ClientAttackMotion")) {
+		uint16 speed;
+
+		if (!this->asUInt16(node, "ClientAttackMotion", speed))
+			return 0;
+
+		mob->status.clientamotion = cap_value(speed, 1, USHRT_MAX);
+	} else {
+		if (!exists)
+			mob->status.clientamotion = cap_value(mob->status.amotion, 1, USHRT_MAX);
+	}
+
 	if (this->nodeExists(node, "DamageMotion")) {
 		uint16 speed;
 

+ 1 - 1
src/map/mob.hpp

@@ -280,7 +280,7 @@ private:
 	bool parseDropNode(std::string nodeName, const ryml::NodeRef& node, uint8 max, s_mob_drop *drops);
 
 public:
-	MobDatabase() : TypesafeCachedYamlDatabase("MOB_DB", 3, 1) {
+	MobDatabase() : TypesafeCachedYamlDatabase("MOB_DB", 4, 1) {
 
 	}
 

+ 2 - 1
src/map/status.hpp

@@ -3172,7 +3172,7 @@ struct status_data {
 #endif
 		matk_min, matk_max,
 		speed,
-		amotion, adelay, dmotion;
+		amotion, clientamotion, adelay, dmotion;
 	int mode;
 	short
 		hit, flee, cri, flee2,
@@ -3392,6 +3392,7 @@ defType status_get_def(struct block_list *bl);
 unsigned short status_get_speed(struct block_list *bl);
 #define status_get_adelay(bl) status_get_status_data(bl)->adelay
 #define status_get_amotion(bl) status_get_status_data(bl)->amotion
+#define status_get_clientamotion(bl) status_get_status_data(bl)->clientamotion
 #define status_get_dmotion(bl) status_get_status_data(bl)->dmotion
 #define status_get_patk(bl) status_get_status_data(bl)->patk
 #define status_get_smatk(bl) status_get_status_data(bl)->smatk

Some files were not shown because too many files changed in this diff