Solved ArmorStand Desync

Discussion in 'Spigot Plugin Development' started by iHells, May 8, 2022.

  1. Hey all,

    I seem to be having an ArmorStand location desync issue? Whenever I respawn these armorstands, it shows their locations inaccurately and fixes after relog - is there anyway to fix this or do I have to manually force a chunk update?
    Edit: I can also rectify this by simply re-rendering the entities (flying so they are out of my render distance, then returning)

    https://imgur.com/a/G066DJI

    Thanks for your help, folks
     
    #1 iHells, May 8, 2022
    Last edited: May 8, 2022
  2. Update: I've tried unloading and loading the chunk after the entities are spawned, but still the issue pertains.
     
  3. It looks like you are using two armor stands. One for the name, one as the flag? Is one the passenger of the other? As you can see, the name teleports to your block, but the other one doesn't. Do you pause the flag rotation task before teleporting? Please post the relevant code snippets (At least the rotation and the teleporting.)
     
  4. That's correct, there are two entities, one for the nametag and one as the actual flag. They are not passengers of one another, the reason for which is that I'm moving the flag constantly to keep the banner on the stand in a constant position, like the following gif shows:
    https://imgur.com/a/Hpnt51i
    If the tag were to be a passenger, it would also be moving around in a circle.

    I am not "teleporting" these entities to a new position, I remove them and then spawn new ones. Here is the Flag class which handles all the logic:
    Code (Kotlin):
    package dev.slohth.ctf.game.flag

    import dev.slohth.ctf.CaptureTheFlag
    import dev.slohth.ctf.game.team.GameTeam
    import dev.slohth.ctf.profile.Profile
    import dev.slohth.ctf.utils.ItemBuilder
    import org.bukkit.ChatColor
    import org.bukkit.Location
    import org.bukkit.block.Block
    import org.bukkit.entity.ArmorStand
    import org.bukkit.entity.EntityType
    import org.bukkit.scheduler.BukkitRunnable
    import org.bukkit.scheduler.BukkitTask
    import org.bukkit.util.BoundingBox
    import kotlin.math.abs
    import kotlin.math.cos
    import kotlin.math.sin

    class Flag(private val core: CaptureTheFlag, val team: GameTeam) {

        private lateinit var blockLocation: Location
        private lateinit var flagLocation: Location
        lateinit var region: BoundingBox

        private var stand: ArmorStand? = null
        private var tag: ArmorStand? = null
        private var task: BukkitTask? = null

        var state = FlagState.SAFE

        fun spawn() {
            spawn(team.flagSpawn)
        }

        fun spawn(block: Block) {
            remove()

            /* Calculate starting positions */
            blockLocation = block.location.clone()
            flagLocation = blockLocation.clone().add(0.5, -1.6, 0.5)
            region = BoundingBox.of(blockLocation.block.getRelative(-1, 2, 1), blockLocation.block.getRelative(1, -1, -1))

            val lower = flagLocation.y
            val higher = lower + 0.6

            /* Tag is in the center of the block, above the height of the flag */
            tag = flagLocation.world!!.spawnEntity(flagLocation.clone().add(0.0, 2.0, 0.0), EntityType.ARMOR_STAND) as ArmorStand
         
            /* Stand is offset in the block to account for the offset of the banner helmet */
            stand = flagLocation.world!!.spawnEntity(flagLocation.add(0.0,0.0, 0.3), EntityType.ARMOR_STAND) as ArmorStand

            stand!!.setBasePlate(false)
            stand!!.setGravity(false)
            stand!!.equipment?.helmet = ItemBuilder(team.type.banner).build()
            stand!!.isInvisible = true
            stand!!.isGlowing = true
            stand!!.isMarker = true
            team.team.addEntry(stand!!.uniqueId.toString())

            tag!!.setGravity(false)
            tag!!.isMarker = true
            tag!!.isInvisible = true
            tag!!.customName = "${team.type.color}${ChatColor.BOLD}${team.type.name[0]}${team.type.name.lowercase().substring(1)} Flag"
            tag!!.isCustomNameVisible = true

            val runnable = object: BukkitRunnable() {
                val radius = 0.3 /* Radius of the circle */
                var asc = true; /* Whether the stand is currently ascending */
                var change = 0.03 /* The change in height at which it is ascending */
                var y = lower; /* The lowest the stand should be */
                var yaw = 180; /* The yaw of the stand (facing direction of banner) */
                var degrees = 0 /* The current phase in the circle */

                override fun run() {
                    /* Calculate whether the stand should be moving upwards or downwards */
                    if (y >= higher) asc = false
                    if (y <= lower) asc = true
                    if (asc) { y += change; } else { y -= change; }

                    /* Calculate how much the stand should change height, the closer it is to the
                       bounds, the slower it should move for a smoother "dropped item" animation feel */

                    change = if ((abs(higher - y) < 0.03 || abs(y - lower) < 0.03)) { 0.005 }
                    else if (abs(higher - y) < 0.07 || abs(y - lower) < 0.07) { 0.01 }
                    else if (abs(higher - y) < 0.2 || abs(y - lower) < 0.2) { 0.015 } else { 0.03 }

                    /* Change the yaw of the stand to rotate the facing direction of the banner */
                    if (yaw <= -180) yaw = 180
                    yaw -= 2

                    if (degrees >= 360) degrees = 0

                    /* Calculate the position to offset the change in facing direction, so the stand
                       remains in a constant position */

                    val radians = degrees * (Math.PI / 180)
                    val x = sin(radians) * radius * -1
                    val z = cos(radians) * radius * -1

                    /* To perfectly sync the change of facing direction of the banner and the movement speed
                       in the circle, we change the yaw and the degrees at the same rate */

                    degrees += 2

                    /* Move the stand to its new calculated location */
                    stand?.teleport(Location(flagLocation.world,
                        flagLocation.x + x, y, flagLocation.z - 0.3 + z,
                        yaw.toFloat(), stand!!.location.pitch))

                    /* Simply change the height of the tag */
                    tag?.teleport(Location(flagLocation.world, tag!!.location.x, y + 3.5, tag!!.location.z))
                }
            }

            task = runnable.runTaskTimer(core.plugin, 0, 1)
        }

        fun remove() {
            task?.cancel()
            tag?.remove()
            team.team.removeEntry(stand?.uniqueId.toString())
            stand?.remove()
            stand = null; tag = null; task = null;
        }

        fun pickup(profile: Profile) {

        }

        fun drop() {

        }

        fun capture() {

        }

    }
    When the flag is initially spawned (using the same method), the intended behaviour works flawlessly. It is only when I try to "move" it (remove and respawn at a new block) that the location appears to desync on the client. When I relog or fly away out of my render distance and come back, it displays correctly on the client. So the locations are correct after the "move", but it just appears to display wrong on the client.
     
    #4 iHells, May 10, 2022
    Last edited: May 10, 2022
  5. Minecraft has two packets it uses to move entities, one does strictly relative movement, and the other handles absolute teleports.
    It usually prefers relative movements over shorter distances, as it's generally cheaper. I suspect that the desync is caused by a relative movement while the stand is constantly being updated through the teleport tag.

    You could try manually sending a teleport packet to clients within an x block radius, to ensure that those players get the full location instead of being moved relatively from the previous point.
     
  6. Thank you all who responded, I've resolved the issue, it was quite a simple fix.
    All I had to do was delay the animation by a few ticks, instead of starting it immediately on the same tick that the entities are spawned.

    Marked as resolved (y)