PaperMC / Paper

The most widely used, high performance Minecraft server that aims to fix gameplay and mechanics inconsistencies
https://papermc.io/
Other
9.45k stars 2.21k forks source link

CreatureSpawnEvent.SpawnReason is null for new players #8828

Closed elmital closed 1 year ago

elmital commented 1 year ago

Expected behavior

CreatureSpawnEvent should always return a non null value, by default CreatureSpawnEvent.SpawnReason.DEFAULT value.

Observed/Actual behavior

The new players which login for the first time on a server have a null CreatureSpawnEvent.SpawnReason while they haven't disconnected one time.

Steps/models to reproduce

Create a new server with a plugin that listen PlayerJoinEvent and PlayerQuitEvent.

package io.papermc.testplugin;

import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;

public final class TestPlugin extends JavaPlugin implements Listener {

    @Override
    public void onEnable() {
        this.getServer().getPluginManager().registerEvents(this, this);
    }

    @EventHandler(priority = EventPriority.HIGH)
    public void onDeathEvent(EntityDeathEvent event) {
        if (!(event.getEntity() instanceof Player))
            return;
        System.out.println("Actual spawn reason on death : " + event.getEntity().getEntitySpawnReason());
    }

    @EventHandler(priority = EventPriority.HIGH)
    public void onConnect(PlayerJoinEvent event) {
        System.out.println("Actual spawn reason on join : " + event.getPlayer().getEntitySpawnReason());
    }

    @EventHandler(priority = EventPriority.HIGH)
    public void onDisConnect(PlayerQuitEvent event) {
        System.out.println("Actual spawn reason on quit : " + event.getPlayer().getEntitySpawnReason());
    }
}

Connect to the server the output is null then disconnect the output will be null too. Re-connect to the server the outputs will be DEFAULT value.

[17:01:45 INFO]: Time elapsed: 100 ms
[17:01:45 INFO]: Running delayed init tasks
[17:01:45 INFO]: Done (13.614s)! For help, type "help"
[17:01:45 INFO]: Timings Reset
[17:02:58] [Server thread/INFO]: [Paper-Test-Plugin] [STDOUT] Actual spawn reason on join : null
[17:02:58] [Server thread/WARN]: Nag author(s): '[PaperMC]' of 'Paper-Test-Plugin' about their usage of System.out/err.print. Please use your plugin's logger instead (JavaPlugin#getLogger).
[17:02:58] [Server thread/INFO]: elmito joined the game
[17:02:58] [Server thread/INFO]: elmito[/127.0.0.1:56654] logged in with entity id 198 at ([world]6.5, 68.0, 10.5)
[17:03:02 INFO]: elmito lost connection: Disconnected
[17:03:02 INFO]: [Paper-Test-Plugin] [STDOUT] Actual spawn reason on quit : null
[17:03:02 INFO]: elmito left the game
[17:03:14 INFO]: UUID of player elmito is c84e2a88-ef05-4c78-9631-5797c9da8e3e
[17:03:14 INFO]: [Paper-Test-Plugin] [STDOUT] Actual spawn reason on join : DEFAULT
[17:03:14 INFO]: elmito joined the game
[17:03:14 INFO]: elmito[/127.0.0.1:56596] logged in with entity id 448 at ([world]6.5, 68.0, 10.5)

Plugin and Datapack List

Test plugin

Paper version

This server is running Paper version null (MC: 1.19.3) (Implementing API version 1.19.3-R0.1-SNAPSHOT) (Git: 4d7269e)

Other

My first assemption about the issue was totally wrong so to avoid to open a new PR for nothing I let here a fix that I've tested. If and it's okay for you I can open a PR.

Output for step model after the fix :

[17:18:47 INFO]: Time elapsed: 96 ms
[17:18:47 INFO]: Running delayed init tasks
[17:18:47 INFO]: Done (11.986s)! For help, type "help"
[17:20:01 INFO]: [Paper-Test-Plugin] [STDOUT] Actual spawn reason on join : DEFAULT
[17:20:01 WARN]: Nag author(s): '[PaperMC]' of 'Paper-Test-Plugin' about their usage of System.out/err.print. Please use your plugin's logger instead (JavaPlugin#getLogger).
[17:20:01 INFO]: elmito joined the game
[17:20:01 INFO]: elmito[/127.0.0.1:56654] logged in with entity id 188 at ([world]-284.5, 69.0, 70.5)
[17:20:12 INFO]: elmito lost connection: Disconnected
[17:20:12 INFO]: [Paper-Test-Plugin] [STDOUT] Actual spawn reason on quit : DEFAULT
[17:20:12 INFO]: elmito left the game
[17:20:21 INFO]: UUID of player elmito is c84e2a88-ef05-4c78-9631-5797c9da8e3e
[17:20:21 INFO]: [Paper-Test-Plugin] [STDOUT] Actual spawn reason on join : DEFAULT
[17:20:21 INFO]: elmito joined the game
[17:20:21 INFO]: elmito[/127.0.0.1:56662] logged in with entity id 317 at ([world]-284.5, 69.0, 70.5)

Here is my proposal in PlayerList class

Before :

public void placeNewPlayer(Connection connection, ServerPlayer player) {
        player.isRealPlayer = true; // Paper
        player.loginTime = System.currentTimeMillis(); // Paper
        GameProfile gameprofile = player.getGameProfile();
        GameProfileCache usercache = this.server.getProfileCache();
        Optional<GameProfile> optional = usercache.get(gameprofile.getId());
        String s = (String) optional.map(GameProfile::getName).orElse(gameprofile.getName());

        usercache.add(gameprofile);
        CompoundTag nbttagcompound = this.load(player);
        ResourceKey resourcekey;
        // CraftBukkit start - Better rename detection
        if (nbttagcompound != null && nbttagcompound.contains("bukkit")) {
            CompoundTag bukkit = nbttagcompound.getCompound("bukkit");
            s = bukkit.contains("lastKnownName", 8) ? bukkit.getString("lastKnownName") : s;
        }
        // CraftBukkit end

        // Paper start - move logic in Entity to here, to use bukkit supplied world UUID.
        if (nbttagcompound != null && nbttagcompound.contains("WorldUUIDMost") && nbttagcompound.contains("WorldUUIDLeast")) {
            UUID uid = new UUID(nbttagcompound.getLong("WorldUUIDMost"), nbttagcompound.getLong("WorldUUIDLeast"));
            org.bukkit.World bWorld = org.bukkit.Bukkit.getServer().getWorld(uid);
            if (bWorld != null) {
                resourcekey = ((CraftWorld) bWorld).getHandle().dimension();
            } else {
                resourcekey = Level.OVERWORLD;
            }
        } else if (nbttagcompound != null) {
            // Vanilla migration support
            // Paper end
            DataResult<ResourceKey<Level>> dataresult = DimensionType.parseLegacy(new Dynamic(NbtOps.INSTANCE, nbttagcompound.get("Dimension"))); // CraftBukkit - decompile error
            Logger logger = PlayerList.LOGGER;

            Objects.requireNonNull(logger);
            resourcekey = (ResourceKey) dataresult.resultOrPartial(logger::error).orElse(Level.OVERWORLD);
        } else {
            resourcekey = Level.OVERWORLD;
        }

        ResourceKey<Level> resourcekey1 = resourcekey;
        ServerLevel worldserver = this.server.getLevel(resourcekey1);
        ServerLevel worldserver1;

        if (worldserver == null) {
            PlayerList.LOGGER.warn("Unknown respawn dimension {}, defaulting to overworld", resourcekey1);
            worldserver1 = this.server.overworld();
        } else {
            worldserver1 = worldserver;
        }

        if (nbttagcompound == null) player.fudgeSpawnLocation(worldserver1); // Paper - only move to spawn on first login, otherwise, stay where you are....

       ...
}

After :

public void placeNewPlayer(Connection connection, ServerPlayer player) {
        player.isRealPlayer = true; // Paper
        player.loginTime = System.currentTimeMillis(); // Paper
        GameProfile gameprofile = player.getGameProfile();
        GameProfileCache usercache = this.server.getProfileCache();
        Optional<GameProfile> optional = usercache.get(gameprofile.getId());
        String s = (String) optional.map(GameProfile::getName).orElse(gameprofile.getName());

        usercache.add(gameprofile);
        CompoundTag nbttagcompound = this.load(player);
        ResourceKey resourcekey;
        // CraftBukkit start - Better rename detection
        if (nbttagcompound != null && nbttagcompound.contains("bukkit")) {
            CompoundTag bukkit = nbttagcompound.getCompound("bukkit");
            s = bukkit.contains("lastKnownName", 8) ? bukkit.getString("lastKnownName") : s;
        }
        // CraftBukkit end

        // Paper start - move logic in Entity to here, to use bukkit supplied world UUID.
        if (nbttagcompound != null && nbttagcompound.contains("WorldUUIDMost") && nbttagcompound.contains("WorldUUIDLeast")) {
            UUID uid = new UUID(nbttagcompound.getLong("WorldUUIDMost"), nbttagcompound.getLong("WorldUUIDLeast"));
            org.bukkit.World bWorld = org.bukkit.Bukkit.getServer().getWorld(uid);
            if (bWorld != null) {
                resourcekey = ((CraftWorld) bWorld).getHandle().dimension();
            } else {
                resourcekey = Level.OVERWORLD;
            }
        } else if (nbttagcompound != null) {
            // Vanilla migration support
            // Paper end
            DataResult<ResourceKey<Level>> dataresult = DimensionType.parseLegacy(new Dynamic(NbtOps.INSTANCE, nbttagcompound.get("Dimension"))); // CraftBukkit - decompile error
            Logger logger = PlayerList.LOGGER;

            Objects.requireNonNull(logger);
            resourcekey = (ResourceKey) dataresult.resultOrPartial(logger::error).orElse(Level.OVERWORLD);
        } else {
            resourcekey = Level.OVERWORLD;
        }

        ResourceKey<Level> resourcekey1 = resourcekey;
        ServerLevel worldserver = this.server.getLevel(resourcekey1);
        ServerLevel worldserver1;

        if (worldserver == null) {
            PlayerList.LOGGER.warn("Unknown respawn dimension {}, defaulting to overworld", resourcekey1);
            worldserver1 = this.server.overworld();
        } else {
            worldserver1 = worldserver;
        }

        // Paper start - set Player SpawnReason to DEFAULT on first login
        if (nbttagcompound == null) {
            player.fudgeSpawnLocation(worldserver1); // Paper - only move to spawn on first login, otherwise, stay where you are....
            player.spawnReason = CreatureSpawnEvent.SpawnReason.DEFAULT;
        }
        // Paper end

       ...
}       
electronicboy commented 1 year ago

I'll have to look when I get back to my desk, but, the spawn reason of the entity being transported should be retained, last I looked the logic for persisting the spawn reason was especially fragile around stuff like entity teleports and spawners, I had ideas for a better fix around this stuff but have completely forgot what all I said

On Sun, 5 Feb 2023, 15:50 elmital, @.***> wrote:

Expected behavior

Some getters in Entity, LivingEntity, CreatureSpawnEvent,... for the SpawnReason of an entity are annoted as @NotNull and should return not null results, by default it seems that if a SpawnReason wasn't set it should return CreatureSpawnEvent.SpawnReason.DEFAULT value. Observed/Actual behavior

Actually sometimes these methods return null results like in the logs here:

java.lang.NullPointerException: Cannot invoke "org.bukkit.event.entity.CreatureSpawnEvent$SpawnReason.equals(Object)" because the return value of "org.bukkit.entity.LivingEntity.getEntitySpawnReason()" is null at me.elmital.customplugin.Listeners.onEntityKilled(Listeners.java:405) ~[customplugin-1.0.0.jar:?] at com.destroystokyo.paper.event.executor.asm.generated.GeneratedEventExecutor202.execute(Unknown Source) ~[?:?] at org.bukkit.plugin.EventExecutor$2.execute(EventExecutor.java:77) ~[paper-api-1.19.3-R0.1-SNAPSHOT.jar:?] at co.aikar.timings.TimedEventExecutor.execute(TimedEventExecutor.java:80) ~[paper-api-1.19.3-R0.1-SNAPSHOT.jar:git-Paper-380] at org.bukkit.plugin.RegisteredListener.callEvent(RegisteredListener.java:70) ~[paper-api-1.19.3-R0.1-SNAPSHOT.jar:?] at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:672) ~[paper-api-1.19.3-R0.1-SNAPSHOT.jar:?] at org.bukkit.craftbukkit.v1_19_R2.event.CraftEventFactory.callPlayerDeathEvent(CraftEventFactory.java:907) ~[paper-1.19.3.jar:git-Paper-380] at net.minecraft.server.level.ServerPlayer.die(ServerPlayer.java:908) ~[?:?] at net.minecraft.world.entity.LivingEntity.hurt(LivingEntity.java:1491) ~[?:?] at net.minecraft.world.entity.player.Player.hurt(Player.java:961) ~[?:?] at net.minecraft.server.level.ServerPlayer.hurt(ServerPlayer.java:1071) ~[?:?] at net.minecraft.world.entity.projectile.AbstractArrow.onHitEntity(AbstractArrow.java:402) ~[?:?] at net.minecraft.world.entity.projectile.Projectile.onHit(Projectile.java:182) ~[?:?] at net.minecraft.world.entity.projectile.Projectile.preOnHit(Projectile.java:173) ~[?:?] at net.minecraft.world.entity.projectile.AbstractArrow.preOnHit(AbstractArrow.java:296) ~[?:?] at net.minecraft.world.entity.projectile.AbstractArrow.tick(AbstractArrow.java:232) ~[?:?] at net.minecraft.world.entity.projectile.Arrow.tick(Arrow.java:112) ~[?:?] at net.minecraft.server.level.ServerLevel.tickNonPassenger(ServerLevel.java:1211) ~[?:?] at net.minecraft.world.level.Level.guardEntityTick(Level.java:918) ~[?:?] at net.minecraft.server.level.ServerLevel.lambda$tick$6(ServerLevel.java:731) ~[?:?] at net.minecraft.world.level.entity.EntityTickList.forEach(EntityTickList.java:42) ~[paper-1.19.3.jar:git-Paper-380] at net.minecraft.server.level.ServerLevel.tick(ServerLevel.java:711) ~[?:?] at net.minecraft.server.MinecraftServer.tickChildren(MinecraftServer.java:1535) ~[paper-1.19.3.jar:git-Paper-380] at net.minecraft.server.dedicated.DedicatedServer.tickChildren(DedicatedServer.java:440) ~[paper-1.19.3.jar:git-Paper-380] at net.minecraft.server.MinecraftServer.tickServer(MinecraftServer.java:1397) ~[paper-1.19.3.jar:git-Paper-380] at net.minecraft.server.MinecraftServer.runServer(MinecraftServer.java:1173) ~[paper-1.19.3.jar:git-Paper-380] at net.minecraft.server.MinecraftServer.lambda$spin$0(MinecraftServer.java:316) ~[paper-1.19.3.jar:git-Paper-380] at java.lang.Thread.run(Thread.java:833) ~[?:?]



### Steps/models to reproduce

I experienced these bugs when players are killed and a PlayerDeathEvent listener try to get the org.bukkit.entity.LivingEntity.getEntitySpawnReason(), sometimes a NPE is thrown.

``` Java
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    @SuppressWarnings("unused")
    public void onEntityKilled(EntityDeathEvent event) {
        if (!event.getEntity().getEntitySpawnReason().equals(CreatureSpawnEvent.SpawnReason.CUSTOM))
            return;
        var id = event.getEntity().getPersistentDataContainer().get(idKey(),PersistentDataType.INTEGER);
        if (id != null) {
            if (event.getEntity() instanceof Mob mob) {
                ...
            }
        }
    }

Plugin and Datapack List

I don't think it's related to the bug but I've a Custom Core Plugin the
one where logs above came from, and other publics plugins :
Plugins (8): Chunky, FreedomChat, LuckPerms, PL-Hide, MyCustomPlugin,
spark, WorldEdit, WorldGuard
Paper version

This server is running Paper version git-Paper-380 (MC: 1.19.3)
(Implementing API version 1.19.3-R0.1-SNAPSHOT) (Git: 0ed4b91
<https://github.com/PaperMC/Paper/commit/0ed4b9148b72a7fb702333cf1221be6aeecff7ca>
)
Code bellow use the last paper version
Other

It seems that since the Spigot fix
https://hub.spigotmc.org/jira/browse/SPIGOT-6415, in the ServerLevel
class, for entities which are teleported. The SpawnReason is passed as null
while before it was with the CreatureSpawnEvent.SpawnReason.DEFAULT value.

public void addDuringTeleport(Entity entity) {
        // CraftBukkit start
        // SPIGOT-6415: Don't call spawn event for entities which travel trough worlds,
        // since it is only an implementation detail, that a new entity is created when
        // they are traveling between worlds.
        this.addDuringTeleport(entity, null); <-- Here is the Spigot change
    }

    public void addDuringTeleport(Entity entity, CreatureSpawnEvent.SpawnReason reason) {
        this.addEntity(entity, reason);
        // CraftBukkit end
    }

When we look at the ServerLevel#addEntity(Entity entity,
CreatureSpawnEvent.SpawnReason spawnReason) a check is done for the
Nullability of the SpawnReason:
if (entity.spawnReason == null) entity.spawnReason = spawnReason; // Paper
But the spawnReason could be null since the fix.

// CraftBukkit start
    private boolean addEntity(Entity entity, CreatureSpawnEvent.SpawnReason spawnReason) {
        org.spigotmc.AsyncCatcher.catchOp("entity add"); // Spigot
        // Paper start
        if (entity.valid) {
            MinecraftServer.LOGGER.error("Attempted Double World add on " + entity, new Throwable());

            if (DEBUG_ENTITIES) {
                Throwable thr = entity.addedToWorldStack;
                if (thr == null) {
                    MinecraftServer.LOGGER.error("Double add entity has no add stacktrace");
                } else {
                    MinecraftServer.LOGGER.error("Double add stacktrace: ", thr);
                }
            }
            return true;
        }
        // Paper end
        if (entity.spawnReason == null) entity.spawnReason = spawnReason; // Paper
        if (entity.isRemoved()) {
            // Paper start
            if (DEBUG_ENTITIES) {
                io.papermc.paper.util.TraceUtil.dumpTraceForThread("Tried to add entity " + entity + " but it was marked as removed already"); // CraftBukkit
                getAddToWorldStackTrace(entity).printStackTrace();
            }
            // Paper end
            // WorldServer.LOGGER.warn("Tried to add entity {} but it was marked as removed already", EntityTypes.getKey(entity.getType())); // CraftBukkit
            return false;
        } else {
            if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added
            // Paper start - capture all item additions to the world
            if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) {
                captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity);
                return true;
            }
            // Paper end
            // SPIGOT-6415: Don't call spawn event when reason is null. For example when an entity teleports to a new world.
            if (spawnReason != null && !CraftEventFactory.doEntityAddEventCalling(this, entity, spawnReason)) {
                return false;
            }
            // CraftBukkit end

            return this.entityLookup.addNewEntity(entity); // Paper - rewrite chunk system
        }
    }

From what I've found it look to be the only place where a null value is
passed, and that's a breaking change for plugin developper. I think two
solutions can be applied :

   - All the concerned methods annoted as @NotNull should be annoted as
   @Nullable
   - Or adapt the fix with something like that maybe ?

// CraftBukkit start
    private boolean addEntity(Entity entity, CreatureSpawnEvent.SpawnReason spawnReason) {
        org.spigotmc.AsyncCatcher.catchOp("entity add"); // Spigot
        // Paper start
        if (entity.valid) {
            MinecraftServer.LOGGER.error("Attempted Double World add on " + entity, new Throwable());

            if (DEBUG_ENTITIES) {
                Throwable thr = entity.addedToWorldStack;
                if (thr == null) {
                    MinecraftServer.LOGGER.error("Double add entity has no add stacktrace");
                } else {
                    MinecraftServer.LOGGER.error("Double add stacktrace: ", thr);
                }
            }
            return true;
        }
        // Paper end
        // Paper start - Since SPIGOT-6415 fix spawnReason may be null
        boolean callSpawnEvent = spawnReason != null;
        if (entity.spawnReason == null) {
            if (spawnReason == null) entity.spawnReason = CreatureSpawnEvent.SpawnReason.DEFAULT;
            else entity.spawnReason = spawnReason;
        }
        // Paper end
        if (entity.isRemoved()) {
            // Paper start
            if (DEBUG_ENTITIES) {
                io.papermc.paper.util.TraceUtil.dumpTraceForThread("Tried to add entity " + entity + " but it was marked as removed already"); // CraftBukkit
                getAddToWorldStackTrace(entity).printStackTrace();
            }
            // Paper end
            // WorldServer.LOGGER.warn("Tried to add entity {} but it was marked as removed already", EntityTypes.getKey(entity.getType())); // CraftBukkit
            return false;
        } else {
            if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added
            // Paper start - capture all item additions to the world
            if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) {
                captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity);
                return true;
            }
            // Paper end
            // SPIGOT-6415: Don't call spawn event when reason is null. For example when an entity teleports to a new world.
            if (callSpawnEvent && !CraftEventFactory.doEntityAddEventCalling(this, entity, spawnReason)) { // Paper
                return false;
            }
            // CraftBukkit end

            return this.entityLookup.addNewEntity(entity); // Paper - rewrite chunk system
        }
    }

—
Reply to this email directly, view it on GitHub
<https://github.com/PaperMC/Paper/issues/8828>, or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAJMAZH7OWHNFX52W7RQO5TWV7D57ANCNFSM6AAAAAAUR25GWI>
.
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
elmital commented 1 year ago

I've update the issue after more checks in my logs and tests. The issue of null value for SpawnReason apears only with new players until they disconnect a first time the SpawnReason is always set to null. To avoid to open a new useless PR I've made a proposal here to fix the issue waiting for your approval before doing it.

Machine-Maker commented 1 year ago

I was able to replicate the issue using your steps, and your solution looks fine. Feel free to open another PR.