From f5709201dc49ce3afca6d159ca3f7e81b59658e9 Mon Sep 17 00:00:00 2001 From: Josiah Glosson Date: Mon, 22 Jul 2024 07:21:22 -0500 Subject: [PATCH] Player info screen --- .../worldhost/GameProfileRenderer.java | 268 ++++++++++++++++++ .../github/gaming32/worldhost/WorldHost.java | 10 +- .../worldhost/gui/screen/FriendsScreen.java | 10 +- .../gui/screen/PlayerInfoScreen.java | 73 +++++ .../mixin/MixinAbstractClientPlayer.java | 35 +++ .../worldhost/plugin/FriendListFriend.java | 3 + .../vanilla/WorldHostFriendListFriend.java | 7 + .../assets/world-host/lang/en_us.json | 1 + src/main/resources/world-host.mixins.json | 3 +- 9 files changed, 403 insertions(+), 7 deletions(-) create mode 100644 src/main/java/io/github/gaming32/worldhost/GameProfileRenderer.java create mode 100644 src/main/java/io/github/gaming32/worldhost/gui/screen/PlayerInfoScreen.java create mode 100644 src/main/java/io/github/gaming32/worldhost/mixin/MixinAbstractClientPlayer.java diff --git a/src/main/java/io/github/gaming32/worldhost/GameProfileRenderer.java b/src/main/java/io/github/gaming32/worldhost/GameProfileRenderer.java new file mode 100644 index 0000000..9a15cb1 --- /dev/null +++ b/src/main/java/io/github/gaming32/worldhost/GameProfileRenderer.java @@ -0,0 +1,268 @@ +package io.github.gaming32.worldhost; + +import com.google.common.base.Suppliers; +import com.mojang.authlib.GameProfile; +import net.minecraft.Util; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.player.RemotePlayer; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.commands.Commands; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.ServerPacksSource; +import net.minecraft.world.Difficulty; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +//#if MC >= 1.20.0 +import net.minecraft.client.gui.GuiGraphics; +//#else +//$$ import com.mojang.blaze3d.vertex.PoseStack; +//#endif + +//#if MC >= 1.19.2 +import net.minecraft.client.gui.screens.worldselection.WorldCreationContext; +import net.minecraft.server.WorldLoader; +import net.minecraft.world.level.dimension.BuiltinDimensionTypes; +import net.minecraft.world.level.levelgen.presets.WorldPresets; +//#else +//$$ import net.minecraft.world.level.dimension.DimensionType; +//#endif + +//#if MC >= 1.19.4 +import net.minecraft.client.telemetry.TelemetryEventSender; +import net.minecraft.client.telemetry.WorldSessionTelemetryManager; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.WorldDataConfiguration; +import net.minecraft.world.level.levelgen.WorldGenSettings; +import net.minecraft.world.level.levelgen.WorldOptions; +//#else +//$$ import com.mojang.datafixers.util.Pair; +//$$ import com.mojang.serialization.Lifecycle; +//$$ import net.minecraft.core.Registry; +//$$ import net.minecraft.core.RegistryAccess; +//$$ import net.minecraft.server.packs.PackType; +//$$ import net.minecraft.world.level.DataPackConfig; +//#endif + +//#if MC >= 1.20.4 +import net.minecraft.client.multiplayer.CommonListenerCookie; +import net.minecraft.world.flag.FeatureFlags; +//#endif + +//#if MC >= 1.20.6 +import java.util.Map; +//#endif + +//#if MC >= 1.21 +import java.util.List; +import net.minecraft.server.ServerLinks; +//#endif + +//#if NEOFORGE +//$$ import net.neoforged.neoforge.network.connection.ConnectionType; +//#endif + +public final class GameProfileRenderer { + private static final Supplier FAKE_LEVEL = Suppliers.memoize(GameProfileRenderer::createFakeLevel); + + private final Player player; + + private GameProfileRenderer(RemotePlayer player) { + this.player = player; + } + + public static GameProfileRenderer create(GameProfile profile) { + final RemotePlayer player = new RemotePlayer( + FAKE_LEVEL.get(), profile + //#if MC == 1.19.2 + //$$ , null + //#endif + ); + player.setPos(0, 80, 0); + return new GameProfileRenderer(player); + } + + public void renderFacingMouse( + //#if MC < 1.20.0 + //$$ PoseStack context, + //#else + GuiGraphics context, + //#endif + int x1, int y1, int x2, int y2, int scale, float mouseX, float mouseY + ) { + final EntityRenderDispatcher entityRenderDispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); + final Camera oldCamera = entityRenderDispatcher.camera; + entityRenderDispatcher.camera = new Camera(); + //#if MC >= 1.19.4 + context.pose().pushPose(); + context.pose().translate(0f, 0f, 1000f); + InventoryScreen.renderEntityInInventoryFollowsMouse( + context, x1, y1, x2, y2, scale, 0.0625f, mouseX, mouseY, player + ); + context.pose().popPose(); + //#else + //$$ final int centerX = (x1 + x2) / 2; + //$$ final int centerY = (y1 + y2) / 2; + //$$ InventoryScreen.renderEntityInInventory( + //$$ centerX, centerY, scale, centerX - mouseX, centerY - mouseY, player + //$$ ); + //#endif + entityRenderDispatcher.camera = oldCamera; + } + + private static ClientLevel createFakeLevel() { + final Minecraft minecraft = Minecraft.getInstance(); + final GameProfile nullProfile = new GameProfile(Util.NIL_UUID, ""); + //#if MC >= 1.19.2 + final WorldCreationContext context = loadDatapacks(minecraft); + //#endif + //#if MC >= 1.19.4 + final WorldSessionTelemetryManager worldTelemetry = new WorldSessionTelemetryManager( + TelemetryEventSender.DISABLED, false, null + //#if MC >= 1.20.1 + , null + //#endif + ); + //#endif + return new ClientLevel( + new ClientPacketListener( + minecraft, + //#if MC <= 1.20.1 + //$$ null, + //#endif + new Connection(PacketFlow.CLIENTBOUND), + //#if MC >= 1.20.4 + new CommonListenerCookie( + nullProfile, worldTelemetry, + context.worldgenLoadContext(), + FeatureFlags.REGISTRY.allFlags(), + null, null, null + //#if MC >= 1.20.6 + , Map.of(), null, false + //#endif + //#if MC >= 1.21 + , Map.of(), new ServerLinks(List.of()) + //#endif + //#if NEOFORGE + //$$ , ConnectionType.OTHER + //#endif + ) + //#else + //#if MC >= 1.19.4 + //$$ null, + //#endif + //$$ nullProfile, + //#if MC >= 1.19.4 + //$$ worldTelemetry + //#else + //$$ minecraft.createTelemetryManager() + //#endif + //#endif + ), + new ClientLevel.ClientLevelData(Difficulty.NORMAL, false, false), + Level.OVERWORLD, + //#if MC >= 1.19.4 + context.worldgenLoadContext() + .registryOrThrow(Registries.DIMENSION_TYPE) + .getHolderOrThrow(BuiltinDimensionTypes.OVERWORLD), + //#elseif MC >= 1.19.2 + //$$ context.registryAccess() + //$$ .registryOrThrow(Registry.DIMENSION_TYPE_REGISTRY) + //$$ .getHolderOrThrow(BuiltinDimensionTypes.OVERWORLD), + //#else + //$$ RegistryAccess.BUILTIN.get() + //$$ .registryOrThrow(Registry.DIMENSION_TYPE_REGISTRY) + //$$ .getOrCreateHolder(DimensionType.OVERWORLD_LOCATION), + //#endif + //#if MC >= 1.19.2 + minecraft.options.renderDistance().get(), + minecraft.options.simulationDistance().get(), + //#else + //$$ minecraft.options.renderDistance, + //$$ minecraft.options.simulationDistance, + //#endif + minecraft::getProfiler, minecraft.levelRenderer, false, 0L + ); + } + + //#if MC >= 1.19.2 + private static WorldCreationContext loadDatapacks(Minecraft minecraft) { + //#if MC >= 1.20.4 + final ServerPacksSource packsSource = new ServerPacksSource(minecraft.directoryValidator()); + //#else + //$$ final ServerPacksSource packsSource = new ServerPacksSource(); + //#endif + final PackRepository packRepository = new PackRepository( + //#if MC < 1.19.4 + //$$ PackType.SERVER_DATA, + //#endif + packsSource + ); + final WorldLoader.InitConfig initConfig = new WorldLoader.InitConfig( + new WorldLoader.PackConfig( + packRepository, + //#if MC >= 1.19.4 + WorldDataConfiguration.DEFAULT, + //#else + //$$ DataPackConfig.DEFAULT, + //#endif + false + //#if MC >= 1.19.4 + , false + //#endif + ), + Commands.CommandSelection.INTEGRATED, 2 + ); + //#if MC >= 1.19.4 + record DataPackReloadCookie(WorldGenSettings worldGenSettings, WorldDataConfiguration dataConfiguration) { + } + //#endif + try (ExecutorService gameExecutor = Executors.newSingleThreadExecutor()) { + final CompletableFuture completableFuture = WorldLoader.load( + initConfig, + //#if MC >= 1.19.4 + dataLoadContext -> new WorldLoader.DataLoadOutput<>( + new DataPackReloadCookie( + new WorldGenSettings( + WorldOptions.defaultWithRandomSeed(), + WorldPresets.createNormalWorldDimensions(dataLoadContext.datapackWorldgen()) + ), + dataLoadContext.dataConfiguration() + ), + dataLoadContext.datapackDimensions() + ), + //#else + //$$ (resourceManager, dataPackConfig) -> { + //$$ final var frozen = RegistryAccess.builtinCopy().freeze(); + //$$ final var worldGenSettings = WorldPresets.createNormalWorldFromPreset(frozen); + //$$ return Pair.of(worldGenSettings, frozen); + //$$ }, + //#endif + (closeableResourceManager, reloadableServerResources, layeredRegistryAccess, object) -> { + closeableResourceManager.close(); + return new WorldCreationContext( + //#if MC >= 1.19.4 + object.worldGenSettings, layeredRegistryAccess, reloadableServerResources, object.dataConfiguration + //#else + //$$ object, Lifecycle.stable(), layeredRegistryAccess, reloadableServerResources + //#endif + ); + }, + Util.backgroundExecutor(), gameExecutor + ); + return completableFuture.join(); + } + } + //#endif +} diff --git a/src/main/java/io/github/gaming32/worldhost/WorldHost.java b/src/main/java/io/github/gaming32/worldhost/WorldHost.java index 45f1eb6..a54b20f 100644 --- a/src/main/java/io/github/gaming32/worldhost/WorldHost.java +++ b/src/main/java/io/github/gaming32/worldhost/WorldHost.java @@ -636,9 +636,9 @@ public static GameProfile fetchProfile(MinecraftSessionService sessionService, G return fetchProfile(sessionService, profile.getId(), profile); } - public static CompletableFuture resolveProfileInfo(GameProfile profile) { + public static CompletableFuture resolveGameProfile(GameProfile profile) { if (profile.getId().version() != 4) { - return CompletableFuture.completedFuture(new GameProfileProfileInfo(profile)); + return CompletableFuture.completedFuture(profile); } return CompletableFuture.supplyAsync( () -> WorldHost.fetchProfile(Minecraft.getInstance().getMinecraftSessionService(), profile), @@ -647,7 +647,11 @@ public static CompletableFuture resolveProfileInfo(GameProfile prof //#else //$$ Util.ioPool() //#endif - ).thenApply(GameProfileProfileInfo::new); + ); + } + + public static CompletableFuture resolveProfileInfo(GameProfile profile) { + return resolveGameProfile(profile).thenApply(GameProfileProfileInfo::new); } public static boolean isFriend(UUID user) { diff --git a/src/main/java/io/github/gaming32/worldhost/gui/screen/FriendsScreen.java b/src/main/java/io/github/gaming32/worldhost/gui/screen/FriendsScreen.java index b3683ce..2f75f5b 100644 --- a/src/main/java/io/github/gaming32/worldhost/gui/screen/FriendsScreen.java +++ b/src/main/java/io/github/gaming32/worldhost/gui/screen/FriendsScreen.java @@ -28,6 +28,7 @@ public class FriendsScreen extends ScreenWithInfoTexts { public static final Component ADD_FRIEND_TEXT = Components.translatable("world-host.add_friend"); private final Screen parent; + private Button infoButton; private Button removeButton; private FriendsList list; @@ -68,12 +69,16 @@ protected void init() { .build() ); - addRenderableWidget( - button(Components.empty(), button -> { + infoButton = addRenderableWidget( + button(Components.translatable("world-host.friends.show_info"), button -> { + if (list.getSelected() != null) { + list.getSelected().friend.showFriendInfo(this); + } }).width(152) .pos(width / 2 - 154, height - 30) .build() ); + infoButton.active = false; removeButton = addRenderableWidget( button(Components.translatable("world-host.friends.remove"), button -> { @@ -138,6 +143,7 @@ public FriendsList() { @Override public void setSelected(@Nullable FriendsEntry entry) { super.setSelected(entry); + infoButton.active = entry != null; removeButton.active = entry != null; } diff --git a/src/main/java/io/github/gaming32/worldhost/gui/screen/PlayerInfoScreen.java b/src/main/java/io/github/gaming32/worldhost/gui/screen/PlayerInfoScreen.java new file mode 100644 index 0000000..dd1d7c7 --- /dev/null +++ b/src/main/java/io/github/gaming32/worldhost/gui/screen/PlayerInfoScreen.java @@ -0,0 +1,73 @@ +package io.github.gaming32.worldhost.gui.screen; + +import com.mojang.authlib.GameProfile; +import io.github.gaming32.worldhost.GameProfileRenderer; +import io.github.gaming32.worldhost.WorldHost; +import io.github.gaming32.worldhost.versions.Components; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; + +//#if MC >= 1.20.0 +import net.minecraft.client.gui.GuiGraphics; +//#else +//$$ import com.mojang.blaze3d.vertex.PoseStack; +//#endif + +public class PlayerInfoScreen extends WorldHostScreen { + private final Screen parentScreen; + private GameProfile profile; + private GameProfileRenderer renderer; + + public PlayerInfoScreen(Screen parentScreen, GameProfile profile) { + super(Components.empty()); + this.parentScreen = parentScreen; + setProfile(profile); + WorldHost.resolveGameProfile(profile) + .thenAccept(this::setProfile) + .exceptionally(t -> { + WorldHost.LOGGER.error("Failed to resolve profile {}", profile, t); + return null; + }); + } + + private void setProfile(GameProfile profile) { + if (profileEquals(this.profile, profile)) return; + this.profile = profile; + renderer = GameProfileRenderer.create(profile); + } + + private static boolean profileEquals(GameProfile a, GameProfile b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + return a.equals(b) && a.getProperties().equals(b.getProperties()); + } + + @Override + protected void init() { + assert minecraft != null; + addRenderableWidget( + button(CommonComponents.GUI_BACK, b -> minecraft.setScreen(parentScreen)) + .pos(width / 2 - 75, height / 2 + 100) + .build() + ); + } + + @Override + public void render( + //#if MC < 1.20.0 + //$$ PoseStack context, + //#else + GuiGraphics context, + //#endif + int mouseX, int mouseY, float partialTick + ) { + whRenderBackground(context, mouseX, mouseY, partialTick); + drawCenteredString(context, font, profile.getName(), width / 2, height / 2 + 85, 0xffffff); + renderer.renderFacingMouse(context, 0, -25, width, height - 25, 100, mouseX, mouseY); + super.render(context, mouseX, mouseY, partialTick); + } +} diff --git a/src/main/java/io/github/gaming32/worldhost/mixin/MixinAbstractClientPlayer.java b/src/main/java/io/github/gaming32/worldhost/mixin/MixinAbstractClientPlayer.java new file mode 100644 index 0000000..fde2255 --- /dev/null +++ b/src/main/java/io/github/gaming32/worldhost/mixin/MixinAbstractClientPlayer.java @@ -0,0 +1,35 @@ +package io.github.gaming32.worldhost.mixin; + +import com.mojang.authlib.GameProfile; +import io.github.gaming32.worldhost.gui.screen.PlayerInfoScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; +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.CallbackInfoReturnable; + +@Mixin(AbstractClientPlayer.class) +public abstract class MixinAbstractClientPlayer extends Player { + @Shadow @Nullable private PlayerInfo playerInfo; + + public MixinAbstractClientPlayer(Level level, BlockPos pos, float yRot, GameProfile gameProfile) { + super(level, pos, yRot, gameProfile); + } + + @Inject(method = "getPlayerInfo", at = @At("HEAD")) + private void fakeClientPlayerPlayerInfo(CallbackInfoReturnable cir) { + if (playerInfo == null && Minecraft.getInstance().getConnection() == null) { + final Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.getConnection() == null && minecraft.screen instanceof PlayerInfoScreen) { + playerInfo = new PlayerInfo(getGameProfile(), false); + } + } + } +} diff --git a/src/main/java/io/github/gaming32/worldhost/plugin/FriendListFriend.java b/src/main/java/io/github/gaming32/worldhost/plugin/FriendListFriend.java index 6dccae5..ab75040 100644 --- a/src/main/java/io/github/gaming32/worldhost/plugin/FriendListFriend.java +++ b/src/main/java/io/github/gaming32/worldhost/plugin/FriendListFriend.java @@ -1,5 +1,6 @@ package io.github.gaming32.worldhost.plugin; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import java.util.Optional; @@ -17,6 +18,8 @@ default boolean supportsNotifyAdd() { void removeFriend(Runnable refresher); + void showFriendInfo(Screen parentScreen); + default Optional tag() { return Optional.empty(); } diff --git a/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/WorldHostFriendListFriend.java b/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/WorldHostFriendListFriend.java index 9be4603..f393081 100644 --- a/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/WorldHostFriendListFriend.java +++ b/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/WorldHostFriendListFriend.java @@ -2,8 +2,10 @@ import com.mojang.authlib.GameProfile; import io.github.gaming32.worldhost.WorldHost; +import io.github.gaming32.worldhost.gui.screen.PlayerInfoScreen; import io.github.gaming32.worldhost.plugin.FriendListFriend; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; import java.util.Collections; import java.util.UUID; @@ -43,4 +45,9 @@ public void removeFriend(Runnable refresher) { WorldHost.protoClient.closedWorld(Collections.singleton(uuid)); } } + + @Override + public void showFriendInfo(Screen parentScreen) { + Minecraft.getInstance().setScreen(new PlayerInfoScreen(parentScreen, defaultProfile)); + } } diff --git a/src/main/resources/assets/world-host/lang/en_us.json b/src/main/resources/assets/world-host/lang/en_us.json index 0ada0d7..c621740 100644 --- a/src/main/resources/assets/world-host/lang/en_us.json +++ b/src/main/resources/assets/world-host/lang/en_us.json @@ -32,6 +32,7 @@ "world-host.friends.remove": "Remove Friend", "world-host.friends.remove.title": "Are you sure you want to remove this friend from your friends list?", "world-host.friends.remove.message": "You can always add them back later.", + "world-host.friends.show_info": "Show Info", "world-host.friends.tagged_friend": "%s (%s)", "world-host.add_friend": "Add Friend", "world-host.add_friend.tooltip": "This will add the person as a friend, and notify them that you did so, asking them to return the favor.\nNote that you must both friend each other in order to join each other's games.", diff --git a/src/main/resources/world-host.mixins.json b/src/main/resources/world-host.mixins.json index 6f5b4e3..70ddde8 100644 --- a/src/main/resources/world-host.mixins.json +++ b/src/main/resources/world-host.mixins.json @@ -16,6 +16,7 @@ "client": [ "DisconnectedScreenAccessor", "MinecraftAccessor", + "MixinAbstractClientPlayer", "MixinConnectScreen", "MixinConnectScreen_1", "MixinGameRenderer", @@ -31,9 +32,7 @@ "MixinWorldSelectionList_WorldListEntry", "PlainTextButtonAccessor", "ServerStatusPingerAccessor", - //#if FABRIC "modmenu.MixinModMenuEventHandler" - //#endif ], "injectors": { "defaultRequire": 1