1.8 like combat and Search and Destroy

Do you think Athios should implement these to closer replicate 1.8?

  • Yes

    Votes: 13 81.3%
  • No

    Votes: 1 6.3%
  • Unsure

    Votes: 2 12.5%

  • Total voters
    16

toomuchzelda

Active member
Joined
May 30, 2020
Messages
59
tl;dr: a bunch of adjustments to lib's RWF to make the combat feel better and closer to 1.8. Comparison video at the bottom.

This post is about replicating 1.8 combat in SnD. I'll go over some issues present in Red Warfare/Athios PvP system, and how I went about fixing them. This is in the hopes that, should Athios want their server to closer mimic the combat of pre-1.9, they will find this useful. I'll be referencing libraryaddict's Red Warfare source code, which you can find here: https://github.com/libraryaddict/RedWarfare

It all started 46 years ago. I came into this world weighing 3.5 kilograms, at 12.45pm on the 24th of November, 1975. My mother was

ha ha.

Knockback

Knockback has had a bad history on Red Warfare. libraryaddict tried to make '1.8 like' knockback on his server Red Warfare 2 (which was 1.10.2 at the time) but didn't succeed. General consensus was that it was too weak and just wasn't 1.8. Athios had this problem too a little more recently. There was lots of hit trading and little combo-ing. Martoph has improved it and most people seem to think it's good but the more competitive (sweaty) players think there is still room to improve. I looked into why libraryaddict's knockback was poor:

The main knockback calculating happens in me.libraryaddict.core.damage.CustomDamageEvent.recalculateKnockback().
libraryaddict's code:
Java:
 private Vector recalculateKnockback()
{
Vector toReturn = _knockback.clone();

if (getDamager() != null)
{
Vector offset = getDamager().getLocation().toVector().subtract(getDamagee().getLocation().toVector());

double xDist = offset.getX();
double zDist = offset.getZ();

while (!Double.isFinite(xDist * xDist + zDist * zDist) || xDist * xDist + zDist * zDist < 0.0001)
{
xDist = UtilMath.rr(-0.01, 0.01);
zDist = UtilMath.rr(-0.01, 0.01);
}

double dist = Math.sqrt(xDist * xDist + zDist * zDist);

if (_calculateKB)
{
Vector vec = getDamagee().getVelocity();

vec.setX(vec.getX() / 2);
vec.setY(vec.getY() / 2);
vec.setZ(vec.getZ() / 2);

vec.add(new Vector(-(xDist / dist * 0.4), 0.4, -(zDist / dist * 0.4)));

toReturn.add(vec);
}

double level = 0;

for (int value : _knockbackMult.values())
{
level += value;
}

if (level != 0)
{
level /= 2;

toReturn.add(new Vector(-(xDist / dist * level), 0.1, -(zDist / dist * level)));
}
}

return toReturn;
}
Generalised, the steps go like this (In the event that one entity has attacked another):
1. Get the difference in X and Z of damager/damagee (attacker/victim)
2. Ignoring height, find distance between damager/damagee
3. Get the current velocity of the victim and half it
4. Calculate a knockback vector with negative of normalised X and Z components multiplied by 0.4. Y is just 0.4. And add it to their halved movement vector. This finishes calculating a regular attack/punch/whatever
5. If the player has knockback on their sword, or is sprinting, or has some other knockback multipliers then add 0.1 to Y of the previous vector, and negative of normalised X Z multiplied by the number of knockback levels divided by 2. (Sprinting also counts as 1 knockback level).
6. Done
There are a few extra steps outside of this method but they aren't significant, at least for melee attacks.

I would post 1.8 vanilla code to compare but I believe that's copyrighted and not allowed to be distributed. Anyway, lib's method here is actually extremely similar to vanilla 1.8, there are just a couple of differences: After step 4, make sure the Y value isn't higher than 0.4 (0.4000000059604645 to be exact), and when applying knockback multipliers, use the attacker's horizontal looking direction (yaw) instead of the difference in attacker/victim locations.

Adjusted to match 1.8 it looks something like this:
Java:
 private Vector recalculateKnockback()
{
Vector toReturn = _knockback.clone();

if (getDamager() != null)
{
Vector offset = getDamager().getLocation().toVector().subtract(getDamagee().getLocation().toVector());

double xDist = offset.getX();
double zDist = offset.getZ();

while (!Double.isFinite(xDist * xDist + zDist * zDist) || xDist * xDist + zDist * zDist < 0.0001)
{
xDist = UtilMath.rr(-0.01, 0.01);
zDist = UtilMath.rr(-0.01, 0.01);
}

double dist = Math.sqrt(xDist * xDist + zDist * zDist);

if (_calculateKB)
{
Vector vec = getDamagee().getVelocity();

vec.setX(vec.getX() / 2);
vec.setY(vec.getY() / 2);
vec.setZ(vec.getZ() / 2);

vec.add(new Vector(-(xDist / dist * 0.4), 0.4, -(zDist / dist * 0.4)));

if(vec.getY() > 0.4) //cap Y value
vec.setY(0.4);

toReturn.add(vec);
}

double level = 0;

for (int value : _knockbackMult.values())
{
level += value;
}

if (level != 0)
{
level /= 2;

float yaw = getDamager().getLocation().getYaw();
Vector kbEnch;

double xKb = (double) -Math.sin(yaw * 3.1415927F / 180.0f) * (float) level;
double zKb = (double) Math.cos(yaw * 3.1415927F / 180.0f) * (float) level;

kbEnch = new Vector(xKb, 0.1d, zKb);
toReturn.add(kbEnch);
}
}

return toReturn;
}
Putting this into a 1.16.5 plugin, it's identical to 1.8. The distance pushed as well as the overall feel. But it's still different in snd for some reason?
It turns out that in another part of Red Warfare's code libraryaddict included a function to reduce or otherwise adjust velocity sent to the players by the server, including knockback velocity. It acts as a sort of filter for all velocity information sent to players.

Continued...
 
Last edited:

toomuchzelda

Active member
Joined
May 30, 2020
Messages
59
I think libraryaddict must have overlooked this when dealing with knockback or something, because it is the biggest reason why the knockback on Red Warfare 2 was not liked. The function is in me.libraryaddict.damage.DamageManager. In the constructor he registers a packet listener which catches any velocity packets (incl. knockback information) sent to the player and adjusts it with a method called reduceVelocity(). The packet listener itself isn't particularly interesting but this is the reduceVelocity method:
Java:
 private Vector reduceVelocity(LivingEntity entity, double motX, double motY, double motZ) {
float friction = 0.91f;
if (entity.isOnGround()) {
friction *= getFrictionFactor(entity.getLocation().getBlock().getRelative(BlockFace.DOWN).getType());
}

if (UtilEnt.isClimbing(entity)) {
motX = UtilMath.clamp(motX, -0.15, 0.15);
motZ = UtilMath.clamp(motZ, -0.15, 0.15);
entity.setFallDistance(0);

if (motY < -0.15) {
motY = -0.15;
}

boolean flag = entity instanceof Player && ((Player) entity).isSneaking();

if (flag && motY < 0.0) {
motY = 0;
}

if (((CraftEntity) entity).getHandle().positionChanged) {
motY = 0.2;
}
}

motY -= 0.08;

if (entity.hasPotionEffect(PotionEffectType.LEVITATION)) {
motY += (0.05 * (UtilEnt.getPotion(entity, PotionEffectType.LEVITATION).getAmplifier() + 1) - motY) * 0.2;
}

// System.out.println(MinecraftServer.currentTick + " Pre 3. " + motZ);

motY *= 0.98;
motX *= friction;
motZ *= friction;
// System.out.println(MinecraftServer.currentTick + " 3. " + motZ);

return new Vector(motX, motY, motZ);
}
Particularly for knockback, we're interested in the parts where he gets the 'Friction factor' of the block the player is standing on, and multiplies the knockback's values by it.
As far as I can tell, both in visible behaviour, and in the code, 1.8 doesn't consider the 'friction' of the block the person is standing on, so this is a big detriment (for knockback interests). If you're on the ground, you were taking more/less knockback based on the type of block you were standing on. You might think this makes sense for blocks like Ice and Soul sand, but I don't think that needs to be handled by the server. However, I did see a method that had similar checks (friction factor, if being on a ladder & sneaking: make Y not less than 0), but they are not used when sending knockback to players. In 1.8 NMS it's in EntityLiving.g(float, float), in 1.16.5 I saw it but forgot. These methods are called by movementTick() (m() in 1.8)

I tried cutting the whole packet listener out and with lib's knockback algorithm you could see the difference (similarity to 1.8) immediately. I tested knockback on Ice, and while climbing ladders on both 1.16.5 and 1.8 and they were identical without this filter which lead me to believe that the adjustments were already being made by the server (in the NMS classes) and this filter was needlessy doing it twice and affecting the outcome badly.

(Before I noticed the existence of this packet listener, I tried to make a kb algorithm that matched 1.8 and got quite close. It's similar to 1.8, except boosted because of reduceVelocity() reducing it without me realising. You can see it here).
Java:
 private Vector recKnockback()
{
Vector vec = new Vector();

if(getDamager() != null)
{

double xOffset = getDamager().getLocation().getX() - getDamagee().getLocation().getX();
double y = 0.6d;
double zOffset = getDamager().getLocation().getZ() - getDamagee().getLocation().getZ();

double dist = xOffset * xOffset + zOffset * zOffset;

while (!Double.isFinite(dist) || dist < 0.0001)
{
xOffset = UtilMath.rr(-0.01, 0.01);
zOffset = UtilMath.rr(-0.01, 0.01);
dist = xOffset * xOffset + zOffset * zOffset;
}

dist = Math.sqrt(dist);

if(_calculateKB)
{
Vector punch = new Vector();
double currentY = getDamagee().getVelocity().getY();

punch.setX(-(xOffset / dist * y));
punch.setZ(-(zOffset / dist * y));

if(getDamagee().isOnGround())
{
punch.multiply(1.5);
punch.setY(0.425);
}
else
{
punch.setY((currentY / 2) + y);
if(punch.getY() > 0.4)
punch.setY(0.4);
}

vec.add(punch);
}

double level = 0;

for (int value : _knockbackMult.values())
{
level += value;
}

if(level > 0)
{
Entity damager = getDamager();
Vector kbEnch;

double xKb = (double) -Math.sin(damager.getLocation().getYaw() * 3.1415927F / 180.0f) * (float) level * 0.5f;
double yKb = 0.1d;
double zKb = (double) Math.cos(damager.getLocation().getYaw() * 3.1415927F / 180.0f) * (float) level * 0.5f;

kbEnch = new Vector(xKb, yKb, zKb);
vec.add(kbEnch);
}
}

return vec;
}
Here is a video showing 1.8 vanilla, lib's with reduceVelocity, and lib's adjusted to replicate 1.8 + without reduceVelocity():
 
Last edited:

toomuchzelda

Active member
Joined
May 30, 2020
Messages
59
Sending Knockback immediately:

Another difference between 1.8 and libraryaddict's combat system is how knockback is sent to the player. The difference is small but makes the combat feel slightly more 'responsive' and less sluggish. libraryaddict uses the Spigot API's Entity.setVelocity() method to change the player's velocity. The method looks as follows:

Java:
 public void setVelocity(Vector velocity) {
Preconditions.checkArgument((velocity != null), "velocity");
velocity.checkFinite();
this.entity.setMot(CraftVector.toNMS(velocity));
this.entity.velocityChanged = true;
}
First it checks the given velocity is valid, then sets the player's Mot (current velocity) field to the given vector, then marks them as 'velocity has changed'. I didn't check to see, but given how it doesn't send any packets to the player (or calls methods that does), it's safe to assume that the velocity is sent to the player on the next tick (the player is marked as "velocity changed" so when the server processes the next tick, it knows to tell the player to move). In the 1.8 server, and in 1.16.5, a velocity packet is sent to the player immediately after the knockback has finished calculating and the player is marked as velocity hasn't changed (so the server doesn't send the velocity twice). This also means it skips any other processing the server may do before sending it. You can find it in net.minecraft.server.EntityHuman.attack(). To achieve this in SnD it's simply just create an Entity Velocity packet for the player with the knockback vector and send it to them where lib uses setVelocity(). In this case I put it in DamageManager.applyKnockback() to affect only knockback:

Java:
 private void applyKnockback(CustomDamageEvent event) {
Vector knockback = event.getFinalKnockback();

if (knockback.length() <= 0) {
return;
}

net.minecraft.server.v1_16_R3.Entity nmsEntity = ((CraftEntity) (event.getDamagee())).getHandle();
nmsEntity.impulse = true;

if(event.isPlayerDamagee())
{
Player damagee = event.getPlayerDamagee();
EntityPlayer nmsPlayer = ((CraftPlayer) (damagee)).getHandle();

PacketPlayOutEntityVelocity kbPacket =
new PacketPlayOutEntityVelocity(damagee.getEntityId(), CraftVector.toNMS(knockback));;
nmsPlayer.playerConnection.sendPacket(kbPacket);
nmsPlayer.velocityChanged = false;
}
else
UtilEnt.velocity(event.getDamagee(), knockback, false);

ConditionManager.addFall(event.getDamagee(), event.getFinalDamager());

}
I used NMS instead of ProtocolLib in this example as the constructor for the packet has various checks on each of the fields, and the packet layout is easy to understand so I thought this was better than copying over all the checks and constructing the packet manually.
(Using ProtocolLib will bypass the packet listener, say if you wanted to keep reduceVelocity() but have it not affect knockback, but I think its better to just cut that whole thing out.)

Here's a video displaying the latency between taking damage and receiving knockback, with and without this patch. In particular, watch the difference in amount of time from taking damage (the screen tilts in pain), and when they move from the knockback:


The difference may seem negligible, but it's more noticeable in actual combat.



Arrow Knockback

What appears to be an oversight in lib's algorithm is how knockback from arrows is calculated. Since it uses the difference in attacker (arrow) and victim's positions, arrows can push entities sideways if they hit just right on the edge of the hitbox. This doesn't occur in 1.8.

I didn't pain myself with digging through 1.8 server code for this one, as the solution should be pretty simple; just use the arrow's direction to calculate the knockback. The z has to be flipped because the arrow's looking direction on the X axis is opposite to the direction it's actually moving in.

Java:
 ...

Vector offset;
if(getDamager() instanceof Projectile && getAttackType() == AttackType.PROJECTILE)
{
offset = getDamager().getLocation().getDirection();
offset.setZ(-offset.getZ());
}
else
{
offset = getDamager().getLocation().toVector().subtract(getDamagee().getLocation().toVector());
}

double xDist = offset.getX();
double zDist = offset.getZ();

...
Demo video:


Also, the knockback from projectiles doesn't match 1.8 unless you add the previous sending knockback packet immediately solution. I imagine this is because of the built-in reduceVelocity() lib tried to copy being skipped, but I didn't look into it.



Sprinting

In newer versions there seems to be an issue where players stop sprinting during combat despite holding the sprint key. This never occured in 1.8 and overall it's an inconvenience. After some testing I found that: The sprinting stops on attacks where the server recognizes you are sprinting, or basically the first hit of a sprint. Sometimes the stopping is immediate, other times it's a few seconds after the attack, and sometimes it doesn't happen at all. It's strange because the client is still 'sprinting' in that there's particles at their feet but they are moving at the speed of walking. Demo video:


It's not immediately clear, but the behaviour is partially caused by the server. libraryaddict uses the Spigot Player.setSprinting() method which just calls the NMS method on that player. I won't share vanilla Minecraft code but what it does is it marks the player as sprinting (or not) and then modifies their Attributes, particularly movement_speed. These are the same attributes that you can change with /attribute in game. It adds/removes a modifier for sprinting speed. When these attributes are modified, a packet is sent to the player (UPDATE_ATTRIBUTES in ProtocolLib). Can see this with a packet listener like this:

Code:
 ProtocolLibrary.getProtocolManager()

.addPacketListener(new PacketAdapter(plugin, ListenerPriority.LOW, PacketType.Play.Server.UPDATE_ATTRIBUTES) {

@Override
public void onPacketSending(PacketEvent event)
{
PacketContainer packet = event.getPacket();
int id = packet.getIntegers().read(0);
Player player = event.getPlayer();

if(player.getEntityId() != id)
{
//Bukkit.broadcastMessage("returned early for " + player.getName() + ". id:" + id + ","
// + " player ID:" + player.getEntityId());
return;
}

List<PacketPlayOutUpdateAttributes.AttributeSnapshot> list = (List<AttributeSnapshot>) packet.getModifier().read(1);

for(AttributeSnapshot attribute : list)
{
//Bukkit.broadcastMessage("AttrSnapshot double:" + attribute.b());
// .a() == get AttributeBase field of AttributeSnapshot

if(attribute.a().getName().equals("attribute.name.generic.movement_speed"))
{
//Bukkit.broadcastMessage(" " + attribute.a().getName());
AttributeBase base = attribute.a();
//Bukkit.broadcastMessage(" AttrBase: default= " + base.getDefault()
// + " bool b=" + base.b());

Collection<AttributeModifier> attrModColl = attribute.c();

//when sprint hitting and empty of this is sent to the player
// assuming it means reset to default
if(attrModColl.size() == 0)
{
//if(player.isSprinting())
//{
if(shouldCancelSprint(event.getPlayer()))
{
event.setCancelled(true);

if(sprintStatus)
Bukkit.broadcastMessage(ChatColor.YELLOW + "Cancelled movement_speed packet for " + player.getName());
shouldCancelPacket.put(event.getPlayer(), false);

}
//}
}
else
{
for(AttributeModifier attrMod : attrModColl)
{

//Bukkit.broadcastMessage(" AttributeModifier:" + attrMod.toString());

/*
Bukkit.broadcastMessage(" amount=" + attrMod.getAmount());
Bukkit.broadcastMessage(" attr name:" + attrMod.getName() + " value:"
+ attrMod.getAmount() + " operation " + attrMod.getOperation().toString() +

" uuid:" + attrMod.getUniqueId());

//UUID is for sprint movement boost. Taken from 1.16.5 NMS EntityLiving
if(attrMod.getUniqueId().equals(UUID.fromString("662A6B8D-DA3E-4C1C-8813-96EA6097278D")))
{
//Bukkit.broadcastMessage("sprint packet detected");
//event.setCancelled(true);

}
}
}
}
}
}

});
(Code is a bit messy, was just for testing, need to uncomment stuff to see effects)

When the player stops sprinting, their current Attributes are sent to them again. They might have changed how the client handled this in newer versions or what, but when this happens the player sometimes stops sprinting. Since the client doesn't need these attribute updates to start or stop sprinting, the easiest solution is to not modify the attributes when marking the player as not sprinting. I tried to cancel the packet event which you can see here, but it interferes with legitimate Attribute changes so it's not preferable. Just use the setFlags part from setSprinting() without removing/adding attributes

In DamageManager.createEvent()

Java:
 if (cause instanceof Player && ((Player) cause).isSprinting()) {
event.addKnockMult("Sprint", 1);

event.addRunnable(new DamageRunnable("Sprinting") {

@Override
public void run(CustomDamageEvent event2) {
EntityPlayer player = ((CraftPlayer) cause).getHandle();
//method and args taken from NMS setSprinting()
player.setFlag(3, false);

Vec3D currentMot = player.getMot();
player.setMot(currentMot.getX() * 0.6, currentMot.getY(), currentMot.getZ() * 0.6);

}

});

}
 
Last edited:

toomuchzelda

Active member
Joined
May 30, 2020
Messages
59
Attack rate

The attack rate in libraryaddict's SnD is actually slightly slower than regular Minecraft. Ordinarily, when you take damage you get 20 Invulnerability ticks (a.k.a No Damage Ticks). For some reason though, you're able to be attacked only after 10 ticks. When libraryaddict checks if the player can be attacked, he does this:

In DamageManager.canHit() and canAttemptHit()

Java:
if (damageTicks <= damagee.getMaximumNoDamageTicks() / 2F)

return false;
Basically it says: if the amount of invuln ticks the victim has had so far is less than or equal to 10, they can't be hit. The problem here is the "or equal to", because it means even after waiting 10 invuln ticks they're still invuln for 1 more tick when that shouldn't be the case. So easy change is just remove the equal to.

WIth a little message saying the victim's NDT (invuln ticks) everytime they're attacked, it looks like this:
(Ignore the critical hits, that's another can of beans)


The difference is small, but noticeable in combat.



Hitboxes

The hitboxes in 1.8 are wider than they appear when you press F3+B. libraryaddict managed to implement this on his server by spawning four invisible players around each player, each positioned slightly outwards from the player in each direction. This artifically 'extended' everyone's hitbox as there was actually four hitboxes combining to make one big one. I thought this was worth mentioning since as far as I can tell Athios has disabled this.

I was lazy to record all the different versions so just imagine 1.8 being the same as this and then having it turned off being only punchable if you aim inside the white box:




Other (unclear)

There are a couple of variables that are changed in 1.8 and 1.16 when a mob attacks another.

In the methods that inflict base knockback (and another velocity changing methods) to an entity (EntityLiving.a(float, double, double) in 1.16.5, EntitLiving.a(Entity, float, double, double) in 1.8.8), a boolean is set to true (EntityLiving.impulse in 1.16.5, EntityLiving.ai in 1.8.8). libraryaddict never sets this variable to true in his system. I don't know the exact use for it, but it is related to movement. You can see this in EntityTrackerEntry.a() in 1.16.5 and EntityTrackerEntry.track(List) in 1.8.8. Since vanilla uses it when attacks occur I think it is worth setting here too. I put it in DamageManager.applyKnockback(), example can be seen under the "Sending knockback immediately" heading.

There is also this float that's set to 1.5 when an entity receives damage, in EntityLiving.damageEntity(DamageSource, float). In 1.8.8 it's called EntityLiving.aB, in 1.16.5 EntityLiving.av (animationSpeed according to Mojang Mappings). This is also related to movement in some way, seen in 1.16.5 EntityLiving.a(EntityLiving, boolean). libraryaddict already sets this value when damaging entities in DamageManager.callDamage(), but I managed to miss it when I was updating to 1.16.5 so I thought it was worth mentioning here.

Edit: Looking at Mojang Mapped code the impulse field is used by net.minecraft.server.level.ServerEntity.sendChanges(), which is called by net.minecraft.server.level.ChunkMap.tick(). The impulse field seems to play an important role in there so I would include setting it to true.
Also animationSpeed is used to calculate a field animationPosition (Mojang Mapping), and the only use for it I could find was for calculating a Strider's height.

I think they're worth including, since they're part of the vanilla attack process on old and new versions and particularly impulse as that likely has an effect in combat.

Demo video:

A (poorly made) video of a PvP battle on Athios, and on a version of lib's SnD with these adjustments added for comparison. I couldn't get 1.8 reference footage in time, so I've taken something from YouTube (apology for poor gameplay).

2:32 for epic pov.



I think with all of these changes the combat is identical to 1.8 bar block-hitting which is impossible to replicate (to a decent standard).
If you have any questions or comments say so (or die)

Thanks to Toed, woaxa, maymay, onnet, Tyrue, joshuanp, Pandu, DrabJuice for helping me.

apologies for bad indents in code, blame forums or something that's not how i write code

(edit small note about balancing)
Knockback is much stronger in 1.8/with these changes so if these are added then it may be worth considering how some kits are balanced rn particularly longbow and knockback swords and void maps.
 
Last edited:

Woaxa

Staff Member
Administrator
Joined
May 25, 2020
Messages
115
Athios has had some pvp issues for a while which has removed almost any skill gap there once was. I'm extremely grateful to zelda for putting in all the effort into making something of this scale. There's not too much for me too explain on why or how the current Athios system is lackluster since zelda has already done that. Just want to voice my praise and how on board with this I am. This will also help the growth of the server by appealing to a whole new playerbase we've skipped until now.
 

Tyrue

Member
Joined
Jun 8, 2020
Messages
27
I was able to do some testing with a few of the guys Zelda listed, and I can tell you that this version of PvP feels very smooth.

One thing that I don’t think Zelda mentioned but that he also fixed was kb when getting hit by multiple people. On Athios that kb is basically multiplied and you can be sent flying when getting hit by 2 or more players at the same time. Zelda has fixed that.
Thank you Zelda, this system that you’ve created is awesome.
 

Log in

Join our Discord!

Members online

No members online now.

Forum statistics

Threads
484
Messages
2,521
Members
259
Latest member
Froggo