Zay-es Bug in multiplayer? Entities are in the wrong sets

Hello dear community,

I really enjoy working with zay-es but now I need your help. This issue is a difficult one but I try to explain it.

I got two player models: A demon and a human player model. Those two have a different skeleton for animation. I created a SkeletonComponent for the entities so the animation systems know which entities to handle and which not. I got 2 animation app states: one for the human and one for the monster. Each of them uses a FieldFilter to get the right entities.

In singleplayer everything works fine but for some reason in multiplayer games something really weird happens: When a client connects to the server, a human player is created (exactly like in Singleplayer), again, a human player, not a demon! When another client connects to the game the first client crashes because of a really unexplainable exception. For some reason the DemonAnimationAppState wants to create a DemonAnimationControl for the human player. WTF? I had a look at the entity set and actually there was the human player contained in this set even though this entity had another SkeletonType so it mustn’t even be part of the EntitySet.

I hope I could explain this issue a little.

Here is the code of the DemonAnimationAppState with some System.out.println()

public class DemonAnimationAppState extends AbstractAppState {

    private EntitySet monsters;
    private ModelViewAppState modelViewAppState;
    private HashMap<EntityId, DemonAnimationControl> controls = new HashMap<>();

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        EntityData entityData = stateManager.getState(EntityDataState.class).getEntityData();
        this.monsters = entityData.getEntities(new FieldFilter<>(SkeletonComponent.class, "skeletonType", SkeletonType.Demon), SkeletonComponent.class, Model.class, CharacterMovementState.class);

        this.modelViewAppState = stateManager.getState(ModelViewAppState.class);

        for (Entity entity : monsters) {
            addControl(entity);
        }

        super.initialize(stateManager, app);
    }

    @Override
    public void update(float tpf) {

        if (monsters.applyChanges()) {

            for (Entity e : monsters) {
                System.out.println("SKEL: " + e.get(SkeletonComponent.class).getSkeletonType());
            }

            System.out.println(Thread.currentThread().getName());

            for (Entity entity : monsters.getAddedEntities()) {
                System.out.println("for: " + entity.getId() + entity.get(SkeletonComponent.class).getSkeletonType());
                addControl(entity);
            }

            for (Entity entity : monsters.getChangedEntities()) {
                updateControl(entity);
            }

            for (Entity entity : monsters.getRemovedEntities()) {
                removeControl(entity);
            }

        }

    }

    private void addControl(Entity entity) {
        DemonAnimationControl control = new DemonAnimationControl();
        Spatial demon = modelViewAppState.getSpatial(entity.getId());
        demon.addControl(control);
        control.setMovementState(entity.get(CharacterMovementState.class).getMovementState());
        controls.put(entity.getId(), control);
    }

    private void updateControl(Entity entity) {
        controls.get(entity.getId()).setMovementState(entity.get(CharacterMovementState.class).getMovementState());
    }

    private void removeControl(Entity entity) {
        DemonAnimationControl c = controls.remove(entity.getId());
        if (c.getSpatial() != null) {
            c.getSpatial().removeControl(c);
        }
    }

    @Override
    public void cleanup() {
        for (Entity entity : monsters) {
            removeControl(entity);
        }
        this.monsters.release();
        this.monsters.clear();
        this.monsters = null;
        super.cleanup();
    }
}

This the console output:

SKEL: Human
jME3 Main
for: EntityId[9]Human
DE:: jME3 Main
Jan 21, 2018 6:33:00 PM com.jme3.app.LegacyApplication handleError
SEVERE: Uncaught exception thrown in Thread[jME3 Main,5,main]
java.lang.IndexOutOfBoundsException: bitIndex < 0: -1
	at java.util.BitSet.set(BitSet.java:444)
	at com.jme3.animation.AnimChannel.addBone(AnimChannel.java:258)
	at com.jme3.animation.AnimChannel.addBone(AnimChannel.java:247)
	at de.gamedevbaden.crucified.controls.DemonAnimationControl.setSpatial(DemonAnimationControl.java:33)
	at com.jme3.scene.Spatial.addControl(Spatial.java:769)
	at de.gamedevbaden.crucified.appstates.view.DemonAnimationAppState.addControl(DemonAnimationAppState.java:72)
	at de.gamedevbaden.crucified.appstates.view.DemonAnimationAppState.update(DemonAnimationAppState.java:54)
	at com.jme3.app.state.AppStateManager.update(AppStateManager.java:287)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:236)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:151)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:197)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:232)
	at java.lang.Thread.run(Thread.java:748)


The exception is not really that important but the output of the System.out.println() is.

I don’t know why this set contains entities which should not be in there?

Does anybody know what is going on there and why this only happens in Multiplayer and always then when the second client connects?

What does the skeleton component class look like?

What you are experiencing shouldn’t happen. So it’s necessary to dig deeper.

Note: this approach should work and I’d like to figure out why it doesn’t… but it seems to me that your human and demon app states will end up being very similar, no?

Hey @pspeed very thanks for your answer!

It looks as follows:

@Serializable
public class SkeletonComponent implements EntityComponent {

    private SkeletonType skeletonType;

    public SkeletonComponent() {
    }

    public SkeletonComponent(SkeletonType skeletonType) {
        this.skeletonType = skeletonType;
    }

    public SkeletonType getSkeletonType() {
        return skeletonType;
    }
}

This is the SkeletonType enum class:

public enum SkeletonType {

    /**
     * A human skeleton (used by player characters).
     */
    Human,

    /**
     * Our demon model uses its own skeleton and animations.
     */
    Demon;

}

I try to for hours now…

Yes, they are similar but this shouldn’t have an effect on this. As I said in Singleplayer everything works as expected humans and demons will play their animations properly, just in networked games the human entity is put into the entity set of the demon app state.

No, I just meant that they could probably be the same app state and then you maybe wouldn’t even have found this issue.

Note: Zay-ES does not generally support enums as component fields for various reasons (for one thing the network serialization can get wonky for another DB storage if you use the SQL storage gets pretty huge as compared to an int)… anyway, maybe try switching it to an int and see if the same issue happens.

Unfortunately this doesn’t work. I replaced it with integers. But even here the values would represent the human player.

Can you try it by replacing

with

Filters.fieldEquals(SkeletonComponent.class, "skeletonType", SkeletonType.Demon), SkeletonComponent.class, Model.class, CharacterMovementState.class);

Thanks, but also this does not work. Hmmm… I have no clue really…

I am not sure if this is really a zay-es bug or if I messed up something but I would not know what should be wrong.

At this point, it’s either jumping into the debugger or turning logging levels up so you see what’s being sent/received or whatever. I don’t remember how much logging I left in zay-es-net, to be honest.

Hey @pspeed, I think there is a bug! I created a dummy component to test this behavior and saw that if you use just one (1) component with a filter it does work perfectly. But when I use several components (in my case 3) the same happens as I described in the topic.

What do you mean by using three components with a filter?

A given entity set can only have one component type filtered at a time… and that’s all your example code is doing above.

Edit: to the point:
This code:
this.monsters = entityData.getEntities(new FieldFilter<>(SkeletonComponent.class, “skeletonType”, SkeletonType.Demon), SkeletonComponent.class, Model.class, CharacterMovementState.class);

…should work fine presuming the filter is setup properly. I do this all the time, single player, multiplayer, etc… and it works fine.

Maybe you could put together a simple test case that illustrates the issue?

I will as soon as I come home from university.

If my EntitySet consists out of 3 components plus one FieldFilter. Sorry, I wrote that too quickly :wink:

That should work fine. I do it all the time.

I do so too. Could that be because of multithreading? But I read that Zay-Es is threadsafe.

Zay-es is multithreaded. That shouldn’t be an issue.

It would be easier to talk about a complete test case that illustrates the issue. Else I can spend (waste?) time finding examples where it works fine… but I don’t think that would be helpful.

As I said. I will upload one as soon as I come home again. Thanks in advance for your help :slight_smile:

Well, maybe it turns out it is helpful… because just in some basic searching I cannot find an example where I do this anymore. That doesn’t mean there isn’t one… but it may indicate that as my designs have evolved I’ve had less and less cases where I needed to filter this way. In other words: there could be a lurking bug that doesn’t hit any of my own code.

(And indeed, I’d argue wouldn’t hit yours after a phase of refactoring… but let’s see if we can find it anyway. :))

2 Likes

Hello @pspeed I created a testcase but this does not represent the issue… (so I doubt it will be helpful uploading it) :sob:

I could temporarily go around this by “merging” my two app states into one general AnimationAppState but still I really want to know what’s causing this ****** behavior.