SyncListStruct sync only when Add

I am working on simple 2D multiplayer shooter. I want to have a list of equipped weapons that should work like a stacked - last picked up weapon is equipped until its ammo is out. I used SyncListStruct for that. I have the code that works almost fine but the state of the list is synced to clients only when I add but not when I modify an item or remove from the list.

Here is simplified code of the component (I skipped the parts that does not interact with the list).

public class PlayerShooting : NetworkBehaviour
{
    public class EquippedWeaponList : SyncListStruct<EquippedWeapon> { }
    public EquippedWeaponList weapons = new EquippedWeaponList();
    EquippedWeapon currentWeapon;

    public override void OnStartClient()
    {
        weapons.Callback = OnChangeWeapons;
    }

    // this works fine
    public override void OnStartServer()
    {
        weapons.Add(EquippedWeapon.Create("Blaster", -1, Weapon.Blaster));
    }

    // this is called from outside when the player enters the trigger
    // also works fine
    public void AddEquippedWeapon(EquippedWeapon weapon)
    {
        weapons.Add(weapon);
    }

    [Command]
    void CmdShoot()
    {
        var weapon = weapons[weapons.Count - 1];

        // shoot with the weapon (skipped for brevity)

        // this doesn't sync properly
        weapon.ammo--;
        if (weapon.ammo == 0) {
            weapons.RemoveAt(weapons.Count - 1);
        } else {
            weapons[weapons.Count - 1] = weapon;
        }
    }

    void OnChangeWeapons(EquippedWeaponList.Operation op, int itemIndex)
    {
        if (weapons.Count > 0) {
            currentWeapon = weapons.Last();
            aura.color = WeaponSettings.instance.GetColorFor(currentWeapon.weapon);

            if (isLocalPlayer) {
                weaponName.text = currentWeapon.name;
                weaponAmmo.text = currentWeapon.ammo < 0 ? "∞" : currentWeapon.ammo + "";
            }
        }
    }
}

This is the weapon struct:

[Serializable]
public struct EquippedWeapon
{
    public string name;
    public int ammo;
    public Weapon weapon;

    public static EquippedWeapon Create(string name, int ammo, Weapon weapon) {
        var instance = new EquippedWeapon();
        instance.name = name;
        instance.ammo = ammo;
        instance.weapon = weapon;
        return instance;
    }
}

And Weapon is just an enum (I also tried to change it to int but it didn’t help):

public enum Weapon
{
    Blaster,
    HyperBlaster
}

What am I doing wrong?

You actually have to name the SyncList in a specific way (or did). So you have

    public class EquippedWeaponList : SyncListStruct<EquippedWeapon> { }
    public EquippedWeaponList weapons = new EquippedWeaponList();

but i think it needs to be

    public class SyncListEquippedWeaponList : SyncListStruct<EquippedWeapon> { }
    public SyncListEquippedWeaponList weapons = new SyncListEquippedWeaponList();

Crazy, but it was true, and may still be.

You should never change internal field values of a SyncStruct struct by itself. For safety they should be immutable (or you must remember to set dirty bits manually every time a field is set which opens you up for errors) . Setting the internal field of a sync struct does not flag dirtyBits and therefore does not sync to client. Only assigning the entire struct sets automatically sets dirt bits.

Option1: DOES NOT WORK
syncStructList[3].field1 = 123; //this will only change value on server, will not set dirtybits

Option2: WORKS
syncStructList[3] = new CustomStruct(123); //setting the ENTIRE struct flags dirtyBits to be flagged.

Option3: WORKS (but can cause syncing to fail if you forget to set dirty bits everytime you update a field)
syncStructList[3].field1 = 123;
…then also set dirty bits manually or re-assign to flag dirty bits

Thanks for help guys but still no luck. I updated the type name:

public class SyncListEquippedWeapon : SyncListStruct<EquippedWeapon> { }
public SyncListEquippedWeapon weapons = new SyncListEquippedWeapon();

I also created a method that creates new struct with reduced ammo instead of changig the struct:

    public static EquippedWeapon ReduceAmmo(EquippedWeapon previous)
    {
        var instance = new EquippedWeapon();
        instance.name = previous.name;
        instance.ammo = previous.ammo - 1;
        instance.weapon = previous.weapon;
        return instance;
    }

And change the code that reduces the ammo to use the copy instead:

        if (weapon.ammo > -1 && weapon.ammo <= 1) {
            weapons.RemoveAt(weapons.Count - 1);
        } else {
            weapons[weapons.Count - 1] = EquippedWeapon.ReduceAmmo(weapon);
        }

However, the list is still not synced. It is interesting that the callback OnChangeWeapons is called on the client when the list should be changed but it is not synced.

Ok I finally found the problem and it has completely nothing to do with SyncList. The original code works just fine. The problem was that when the player enters the trigger AddEquippedWeapon method was called on both, client and server. Then the server was working with the last item BUT it was last but one on the client and client was displaying info about the last item which was something that the server was unaware of and doesn’t change it.