diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricEventForwarder.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricEventForwarder.java index cc3f74c0..230134f0 100644 --- a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricEventForwarder.java +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricEventForwarder.java @@ -35,10 +35,14 @@ import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.fabric.events.ChunkFinalizeCallback; +import de.bluecolored.bluemap.fabric.events.PlayerJoinCallback; +import de.bluecolored.bluemap.fabric.events.PlayerLeaveCallback; import de.bluecolored.bluemap.fabric.events.WorldSaveCallback; import net.fabricmc.fabric.api.event.player.AttackBlockCallback; import net.fabricmc.fabric.api.event.player.UseBlockCallback; import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; import net.minecraft.util.ActionResult; import net.minecraft.util.Hand; @@ -60,6 +64,9 @@ public FabricEventForwarder(FabricMod mod) { ChunkFinalizeCallback.EVENT.register(this::onChunkFinalize); AttackBlockCallback.EVENT.register(this::onBlockAttack); UseBlockCallback.EVENT.register(this::onBlockUse); + + PlayerJoinCallback.EVENT.register(this::onPlayerJoin); + PlayerLeaveCallback.EVENT.register(this::onPlayerLeave); } public void addEventListener(ServerEventListener listener) { @@ -116,4 +123,16 @@ public void onChunkFinalize(ServerWorld world, Vector2i chunkPos) { } } + public void onPlayerJoin(MinecraftServer server, ServerPlayerEntity player) { + if (this.mod.getServer() != server) return; + + this.eventListeners.forEach(l -> l.onPlayerJoin(player.getUuid())); + } + + public void onPlayerLeave(MinecraftServer server, ServerPlayerEntity player) { + if (this.mod.getServer() != server) return; + + this.eventListeners.forEach(l -> l.onPlayerLeave(player.getUuid())); + } + } diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricMod.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricMod.java index 6d14bd15..203f4693 100644 --- a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricMod.java +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricMod.java @@ -26,8 +26,10 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -47,25 +49,37 @@ import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; +import de.bluecolored.bluemap.fabric.events.PlayerJoinCallback; +import de.bluecolored.bluemap.fabric.events.PlayerLeaveCallback; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.server.ServerStartCallback; import net.fabricmc.fabric.api.event.server.ServerStopCallback; +import net.fabricmc.fabric.api.event.server.ServerTickCallback; import net.fabricmc.fabric.api.registry.CommandRegistry; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; public class FabricMod implements ModInitializer, ServerInterface { private Plugin pluginInstance = null; + private MinecraftServer serverInstance = null; private Map worldUuids; private FabricEventForwarder eventForwarder; private LoadingCache worldUuidCache; + + private int playerUpdateIndex = 0; + private Map onlinePlayerMap; + private List onlinePlayerList; public FabricMod() { Logger.global = new Log4jLogger(LogManager.getLogger(Plugin.PLUGIN_NAME)); + this.onlinePlayerMap = new ConcurrentHashMap<>(); + this.onlinePlayerList = Collections.synchronizedList(new ArrayList<>()); + pluginInstance = new Plugin("fabric", this); this.worldUuids = new ConcurrentHashMap<>(); @@ -90,6 +104,8 @@ public void onInitialize() { }); ServerStartCallback.EVENT.register((MinecraftServer server) -> { + this.serverInstance = server; + new Thread(()->{ Logger.global.logInfo("Loading BlueMap..."); @@ -106,6 +122,13 @@ public void onInitialize() { pluginInstance.unload(); Logger.global.logInfo("BlueMap unloaded!"); }); + + PlayerJoinCallback.EVENT.register(this::onPlayerJoin); + PlayerLeaveCallback.EVENT.register(this::onPlayerLeave); + + ServerTickCallback.EVENT.register((MinecraftServer server) -> { + if (server == this.serverInstance) this.updateSomePlayers(); + }); } @Override @@ -151,16 +174,57 @@ public File getConfigFolder() { return new File("config/bluemap"); } + public void onPlayerJoin(MinecraftServer server, ServerPlayerEntity playerInstance) { + if (this.serverInstance != server) return; + + FabricPlayer player = new FabricPlayer(this, playerInstance); + onlinePlayerMap.put(player.getUuid(), player); + onlinePlayerList.add(player); + } + + public void onPlayerLeave(MinecraftServer server, ServerPlayerEntity player) { + if (this.serverInstance != server) return; + + UUID playerUUID = player.getUuid(); + onlinePlayerMap.remove(playerUUID); + synchronized (onlinePlayerList) { + onlinePlayerList.removeIf(p -> p.getUuid().equals(playerUUID)); + } + } + + public MinecraftServer getServer() { + return this.serverInstance; + } + @Override public Collection getOnlinePlayers() { - // TODO Implement - return Collections.emptyList(); + return onlinePlayerMap.values(); } @Override public Optional getPlayer(UUID uuid) { - // TODO Implement - return Optional.empty(); + return Optional.ofNullable(onlinePlayerMap.get(uuid)); + } + + /** + * Only update some of the online players each tick to minimize performance impact on the server-thread. + * Only call this method on the server-thread. + */ + private void updateSomePlayers() { + int onlinePlayerCount = onlinePlayerList.size(); + if (onlinePlayerCount == 0) return; + + int playersToBeUpdated = onlinePlayerCount / 20; //with 20 tps, each player is updated once a second + if (playersToBeUpdated == 0) playersToBeUpdated = 1; + + for (int i = 0; i < playersToBeUpdated; i++) { + playerUpdateIndex++; + if (playerUpdateIndex >= 20 && playerUpdateIndex >= onlinePlayerCount) playerUpdateIndex = 0; + + if (playerUpdateIndex < onlinePlayerCount) { + onlinePlayerList.get(i).update(); + } + } } } diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricPlayer.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricPlayer.java new file mode 100644 index 00000000..13b7ca66 --- /dev/null +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/FabricPlayer.java @@ -0,0 +1,155 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.fabric; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.EnumMap; +import java.util.Map; +import java.util.UUID; + +import com.flowpowered.math.vector.Vector3d; + +import de.bluecolored.bluemap.common.plugin.serverinterface.Gamemode; +import de.bluecolored.bluemap.common.plugin.serverinterface.Player; +import de.bluecolored.bluemap.common.plugin.text.Text; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.GameMode; + +public class FabricPlayer implements Player { + + private static final UUID UNKNOWN_WORLD_UUID = UUID.randomUUID(); + + private static final Map GAMEMODE_MAP = new EnumMap<>(GameMode.class); + static { + GAMEMODE_MAP.put(GameMode.ADVENTURE, Gamemode.ADVENTURE); + GAMEMODE_MAP.put(GameMode.SURVIVAL, Gamemode.SURVIVAL); + GAMEMODE_MAP.put(GameMode.CREATIVE, Gamemode.CREATIVE); + GAMEMODE_MAP.put(GameMode.SPECTATOR, Gamemode.SPECTATOR); + } + + private UUID uuid; + private Text name; + private UUID world; + private Vector3d position; + private boolean online; + private boolean sneaking; + private boolean invisible; + private Gamemode gamemode; + + private FabricMod mod; + private WeakReference delegate; + + public FabricPlayer(FabricMod mod, ServerPlayerEntity delegate) { + this.uuid = delegate.getUuid(); + this.mod = mod; + this.delegate = new WeakReference<>(delegate); + + update(); + } + + @Override + public UUID getUuid() { + return this.uuid; + } + + @Override + public Text getName() { + return this.name; + } + + @Override + public UUID getWorld() { + return this.world; + } + + @Override + public Vector3d getPosition() { + return this.position; + } + + @Override + public boolean isOnline() { + return this.online; + } + + @Override + public boolean isSneaking() { + return this.sneaking; + } + + @Override + public boolean isInvisible() { + return this.invisible; + } + + @Override + public Gamemode getGamemode() { + return this.gamemode; + } + + /** + * API access, only call on server thread! + */ + public void update() { + ServerPlayerEntity player = delegate.get(); + if (player == null) { + MinecraftServer server = mod.getServer(); + if (server != null) { + player = server.getPlayerManager().getPlayer(uuid); + } + + if (player == null) { + this.online = false; + return; + } + + delegate = new WeakReference<>(player); + } + + this.gamemode = GAMEMODE_MAP.get(player.interactionManager.getGameMode()); + + StatusEffectInstance invis = player.getStatusEffect(StatusEffects.INVISIBILITY); + this.invisible = invis != null && invis.getDuration() > 0; + + this.name = Text.of(player.getName().getString()); + this.online = !player.removed; + + Vec3d pos = player.getPos(); + this.position = new Vector3d(pos.getX(), pos.getY(), pos.getZ()); + this.sneaking = player.isSneaking(); + + try { + this.world = mod.getUUIDForWorld(player.getServerWorld()); + } catch (IOException e) { + this.world = UNKNOWN_WORLD_UUID; + } + } + +} diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerJoinCallback.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerJoinCallback.java new file mode 100644 index 00000000..d564012c --- /dev/null +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerJoinCallback.java @@ -0,0 +1,42 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.fabric.events; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; + +public interface PlayerJoinCallback { + Event EVENT = EventFactory.createArrayBacked(PlayerJoinCallback.class, + (listeners) -> (server, player) -> { + for (PlayerJoinCallback event : listeners) { + event.onPlayerJoin(server, player); + } + } + ); + + void onPlayerJoin(MinecraftServer server, ServerPlayerEntity player); +} diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerLeaveCallback.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerLeaveCallback.java new file mode 100644 index 00000000..f1ce2922 --- /dev/null +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/events/PlayerLeaveCallback.java @@ -0,0 +1,42 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.fabric.events; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; + +public interface PlayerLeaveCallback { + Event EVENT = EventFactory.createArrayBacked(PlayerLeaveCallback.class, + (listeners) -> (server, player) -> { + for (PlayerLeaveCallback event : listeners) { + event.onPlayerLeave(server, player); + } + } + ); + + void onPlayerLeave(MinecraftServer server, ServerPlayerEntity player); +} diff --git a/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/mixin/MixinPlayerManager.java b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/mixin/MixinPlayerManager.java new file mode 100644 index 00000000..b7109357 --- /dev/null +++ b/BlueMapFabric/src/main/java/de/bluecolored/bluemap/fabric/mixin/MixinPlayerManager.java @@ -0,0 +1,56 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.fabric.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import de.bluecolored.bluemap.fabric.events.PlayerJoinCallback; +import de.bluecolored.bluemap.fabric.events.PlayerLeaveCallback; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; + +@Mixin(PlayerManager.class) +public abstract class MixinPlayerManager { + + @Shadow + public abstract MinecraftServer getServer(); + + @Inject(at = @At("RETURN"), method = "onPlayerConnect") + public void onPlayerConnect(ClientConnection connection, ServerPlayerEntity player, CallbackInfo ci) { + PlayerJoinCallback.EVENT.invoker().onPlayerJoin(this.getServer(), player); + } + + @Inject(at = @At("HEAD"), method = "remove") + public void remove(ServerPlayerEntity player, CallbackInfo ci) { + PlayerLeaveCallback.EVENT.invoker().onPlayerLeave(this.getServer(), player); + } + +} diff --git a/BlueMapFabric/src/main/resources/bluemap.mixins.json b/BlueMapFabric/src/main/resources/bluemap.mixins.json index e8c98a84..84d08a94 100644 --- a/BlueMapFabric/src/main/resources/bluemap.mixins.json +++ b/BlueMapFabric/src/main/resources/bluemap.mixins.json @@ -6,8 +6,9 @@ "mixins": [], "client": [], "server": [ - "MixinServerWorld", - "MixinChunkGenerator" + "MixinChunkGenerator", + "MixinPlayerManager", + "MixinServerWorld" ], "injectors": { "defaultRequire": 1