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
This commit is contained in:
Playtester 2024-05-25 18:59:22 +02:00 committed by GitHub
parent 023263df7d
commit b4ae40d401
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 3419 additions and 15 deletions

View File

@ -130,10 +130,19 @@ equip_self_break_rate: 100
// This affects the behaviour of skills like acid terror and meltdown // This affects the behaviour of skills like acid terror and meltdown
equip_skill_break_rate: 100 equip_skill_break_rate: 100
// Do weapon attacks have a attack speed delay before actual damage is applied? (Note 1) // Should damage have a delay before it is applied? (Note 1)
// NOTE: The official setting is yes, even thought it degrades performance a bit. // 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 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? // Are arrows/ammo consumed when used on a bow/gun?
// 0 = No // 0 = No
// 1 = Yes // 1 = Yes

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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. 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.
--------------------------------------- ---------------------------------------

View File

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

View File

@ -393,7 +393,20 @@ int battle_delay_damage(t_tick tick, int amotion, struct block_list *src, struct
damage = 0; 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 //Deal damage
battle_damage(src, target, damage, ddelay, skill_lv, skill_id, dmg_lv, attack_type, additional_effects, gettick(), isspdamage); battle_damage(src, target, damage, ddelay, skill_lv, skill_id, dmg_lv, attack_type, additional_effects, gettick(), isspdamage);
return 0; 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->additional_effects = additional_effects;
dat->src_type = src->type; dat->src_type = src->type;
dat->isspdamage = isspdamage; 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 ) if( src->type == BL_PC )
((TBL_PC*)src)->delayed_damage++; ((TBL_PC*)src)->delayed_damage++;
@ -11505,6 +11516,7 @@ static const struct _battle_data {
#else #else
{ "feature.instance_allow_reconnect", &battle_config.instance_allow_reconnect, 0, 0, 1, }, { "feature.instance_allow_reconnect", &battle_config.instance_allow_reconnect, 0, 0, 1, },
#endif #endif
{ "synchronize_damage", &battle_config.synchronize_damage, 0, 0, 1, },
#include <custom/battle_config_init.inc> #include <custom/battle_config_init.inc>
}; };

View File

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

View File

@ -5177,6 +5177,8 @@ static int clif_hallucination_damage()
return (rnd() % 32767); return (rnd() % 32767);
} }
#define DEFAULT_ANIMATION_SPEED 432
/// Sends a 'damage' packet (src performs action on dst) /// 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) /// 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) /// 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; WBUFW(buf,0) = cmd;
WBUFL(buf,2) = src->id; WBUFL(buf,2) = src->id;
WBUFL(buf,6) = dst->id; WBUFL(buf,6) = dst->id;

View File

@ -4461,6 +4461,7 @@ s_mob_db::s_mob_db()
status.speed = DEFAULT_WALK_SPEED; status.speed = DEFAULT_WALK_SPEED;
status.adelay = cap_value(0, battle_config.monster_max_aspd * 2, 4000); status.adelay = cap_value(0, battle_config.monster_max_aspd * 2, 4000);
status.amotion = cap_value(0, battle_config.monster_max_aspd, 2000); 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); status.mode = static_cast<e_mode>(MONSTER_TYPE_06);
vd.class_ = id; 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); 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")) { if (this->nodeExists(node, "DamageMotion")) {
uint16 speed; uint16 speed;

View File

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

View File

@ -3172,7 +3172,7 @@ struct status_data {
#endif #endif
matk_min, matk_max, matk_min, matk_max,
speed, speed,
amotion, adelay, dmotion; amotion, clientamotion, adelay, dmotion;
int mode; int mode;
short short
hit, flee, cri, flee2, 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); unsigned short status_get_speed(struct block_list *bl);
#define status_get_adelay(bl) status_get_status_data(bl)->adelay #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_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_dmotion(bl) status_get_status_data(bl)->dmotion
#define status_get_patk(bl) status_get_status_data(bl)->patk #define status_get_patk(bl) status_get_status_data(bl)->patk
#define status_get_smatk(bl) status_get_status_data(bl)->smatk #define status_get_smatk(bl) status_get_status_data(bl)->smatk