package com.mojang.realmsclient; import com.google.common.collect.Lists; import com.google.common.util.concurrent.RateLimiter; import com.mojang.authlib.yggdrasil.ProfileResult; import com.mojang.logging.LogUtils; import com.mojang.math.Axis; import com.mojang.realmsclient.client.Ping; import com.mojang.realmsclient.client.RealmsClient; import com.mojang.realmsclient.dto.PingResult; import com.mojang.realmsclient.dto.RealmsNotification; import com.mojang.realmsclient.dto.RealmsServer; import com.mojang.realmsclient.dto.RealmsServerPlayerLists; import com.mojang.realmsclient.dto.RegionPingResult; import com.mojang.realmsclient.dto.RealmsNotification.InfoPopup; import com.mojang.realmsclient.dto.RealmsNotification.VisitUrl; import com.mojang.realmsclient.dto.RealmsServer.State; import com.mojang.realmsclient.exception.RealmsServiceException; import com.mojang.realmsclient.gui.RealmsDataFetcher; import com.mojang.realmsclient.gui.RealmsServerList; import com.mojang.realmsclient.gui.screens.AddRealmPopupScreen; import com.mojang.realmsclient.gui.screens.RealmsConfigureWorldScreen; import com.mojang.realmsclient.gui.screens.RealmsCreateRealmScreen; import com.mojang.realmsclient.gui.screens.RealmsGenericErrorScreen; import com.mojang.realmsclient.gui.screens.RealmsLongRunningMcoTaskScreen; import com.mojang.realmsclient.gui.screens.RealmsPendingInvitesScreen; import com.mojang.realmsclient.gui.screens.RealmsPopups; import com.mojang.realmsclient.gui.task.DataFetcher.Subscription; import com.mojang.realmsclient.gui.task.DataFetcher.Task; import com.mojang.realmsclient.util.RealmsPersistence; import com.mojang.realmsclient.util.RealmsUtil; import com.mojang.realmsclient.util.RealmsPersistence.RealmsPersistenceData; import com.mojang.realmsclient.util.task.GetServerDetailsTask; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Supplier; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.ChatFormatting; import net.minecraft.SharedConstants; import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.CycleButton; import net.minecraft.client.gui.components.FocusableTextWidget; import net.minecraft.client.gui.components.ImageButton; import net.minecraft.client.gui.components.ImageWidget; import net.minecraft.client.gui.components.LoadingDotsWidget; import net.minecraft.client.gui.components.MultiLineTextWidget; import net.minecraft.client.gui.components.ObjectSelectionList; import net.minecraft.client.gui.components.PlayerFaceRenderer; import net.minecraft.client.gui.components.PopupScreen; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.components.WidgetSprites; import net.minecraft.client.gui.components.WidgetTooltipHolder; import net.minecraft.client.gui.components.Button.OnPress; import net.minecraft.client.gui.components.SpriteIconButton.CenteredIcon; import net.minecraft.client.gui.layouts.FrameLayout; import net.minecraft.client.gui.layouts.GridLayout; import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; import net.minecraft.client.gui.layouts.Layout; import net.minecraft.client.gui.layouts.LayoutSettings; import net.minecraft.client.gui.layouts.LinearLayout; import net.minecraft.client.gui.layouts.SpacerElement; import net.minecraft.client.gui.layouts.GridLayout.RowHelper; import net.minecraft.client.gui.navigation.CommonInputs; import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.client.gui.screens.ConfirmLinkScreen; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.tooltip.ClientActivePlayersTooltip.ActivePlayersTooltip; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.realms.RealmsScreen; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvents; import net.minecraft.util.CommonLinks; import net.minecraft.util.Mth; import net.minecraft.world.level.GameType; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class RealmsMainScreen extends RealmsScreen { static final ResourceLocation INFO_SPRITE = ResourceLocation.withDefaultNamespace("icon/info"); static final ResourceLocation NEW_REALM_SPRITE = ResourceLocation.withDefaultNamespace("icon/new_realm"); static final ResourceLocation EXPIRED_SPRITE = ResourceLocation.withDefaultNamespace("realm_status/expired"); static final ResourceLocation EXPIRES_SOON_SPRITE = ResourceLocation.withDefaultNamespace("realm_status/expires_soon"); static final ResourceLocation OPEN_SPRITE = ResourceLocation.withDefaultNamespace("realm_status/open"); static final ResourceLocation CLOSED_SPRITE = ResourceLocation.withDefaultNamespace("realm_status/closed"); private static final ResourceLocation INVITE_SPRITE = ResourceLocation.withDefaultNamespace("icon/invite"); private static final ResourceLocation NEWS_SPRITE = ResourceLocation.withDefaultNamespace("icon/news"); public static final ResourceLocation HARDCORE_MODE_SPRITE = ResourceLocation.withDefaultNamespace("hud/heart/hardcore_full"); static final Logger LOGGER = LogUtils.getLogger(); private static final ResourceLocation LOGO_LOCATION = ResourceLocation.withDefaultNamespace("textures/gui/title/realms.png"); private static final ResourceLocation NO_REALMS_LOCATION = ResourceLocation.withDefaultNamespace("textures/gui/realms/no_realms.png"); private static final Component TITLE = Component.translatable("menu.online"); private static final Component LOADING_TEXT = Component.translatable("mco.selectServer.loading"); static final Component SERVER_UNITIALIZED_TEXT = Component.translatable("mco.selectServer.uninitialized"); static final Component SUBSCRIPTION_EXPIRED_TEXT = Component.translatable("mco.selectServer.expiredList"); private static final Component SUBSCRIPTION_RENEW_TEXT = Component.translatable("mco.selectServer.expiredRenew"); static final Component TRIAL_EXPIRED_TEXT = Component.translatable("mco.selectServer.expiredTrial"); private static final Component PLAY_TEXT = Component.translatable("mco.selectServer.play"); private static final Component LEAVE_SERVER_TEXT = Component.translatable("mco.selectServer.leave"); private static final Component CONFIGURE_SERVER_TEXT = Component.translatable("mco.selectServer.configure"); static final Component SERVER_EXPIRED_TOOLTIP = Component.translatable("mco.selectServer.expired"); static final Component SERVER_EXPIRES_SOON_TOOLTIP = Component.translatable("mco.selectServer.expires.soon"); static final Component SERVER_EXPIRES_IN_DAY_TOOLTIP = Component.translatable("mco.selectServer.expires.day"); static final Component SERVER_OPEN_TOOLTIP = Component.translatable("mco.selectServer.open"); static final Component SERVER_CLOSED_TOOLTIP = Component.translatable("mco.selectServer.closed"); static final Component UNITIALIZED_WORLD_NARRATION = Component.translatable("gui.narrate.button", SERVER_UNITIALIZED_TEXT); private static final Component NO_REALMS_TEXT = Component.translatable("mco.selectServer.noRealms"); private static final Component NO_PENDING_INVITES = Component.translatable("mco.invites.nopending"); private static final Component PENDING_INVITES = Component.translatable("mco.invites.pending"); private static final Component INCOMPATIBLE_POPUP_TITLE = Component.translatable("mco.compatibility.incompatible.popup.title"); private static final Component INCOMPATIBLE_RELEASE_TYPE_POPUP_MESSAGE = Component.translatable("mco.compatibility.incompatible.releaseType.popup.message"); private static final int BUTTON_WIDTH = 100; private static final int BUTTON_COLUMNS = 3; private static final int BUTTON_SPACING = 4; private static final int CONTENT_WIDTH = 308; private static final int LOGO_WIDTH = 128; private static final int LOGO_HEIGHT = 34; private static final int LOGO_TEXTURE_WIDTH = 128; private static final int LOGO_TEXTURE_HEIGHT = 64; private static final int LOGO_PADDING = 5; private static final int HEADER_HEIGHT = 44; private static final int FOOTER_PADDING = 11; private static final int NEW_REALM_SPRITE_WIDTH = 40; private static final int NEW_REALM_SPRITE_HEIGHT = 20; private static final int ENTRY_WIDTH = 216; private static final int ITEM_HEIGHT = 36; private static final boolean SNAPSHOT = !SharedConstants.getCurrentVersion().isStable(); private static boolean snapshotToggle = SNAPSHOT; private final CompletableFuture availability = RealmsAvailability.get(); @Nullable private Subscription dataSubscription; private final Set handledSeenNotifications = new HashSet(); private static boolean regionsPinged; private final RateLimiter inviteNarrationLimiter; private final Screen lastScreen; private Button playButton; private Button backButton; private Button renewButton; private Button configureButton; private Button leaveButton; RealmsMainScreen.RealmSelectionList realmSelectionList; RealmsServerList serverList; List availableSnapshotServers = List.of(); RealmsServerPlayerLists onlinePlayersPerRealm = new RealmsServerPlayerLists(); private volatile boolean trialsAvailable; @Nullable private volatile String newsLink; long lastClickTime; final List notifications = new ArrayList(); private Button addRealmButton; private RealmsMainScreen.NotificationButton pendingInvitesButton; private RealmsMainScreen.NotificationButton newsButton; private RealmsMainScreen.LayoutState activeLayoutState; @Nullable private HeaderAndFooterLayout layout; public RealmsMainScreen(Screen lastScreen) { super(TITLE); this.lastScreen = lastScreen; this.inviteNarrationLimiter = RateLimiter.create(0.016666668F); } @Override public void init() { this.serverList = new RealmsServerList(this.minecraft); this.realmSelectionList = new RealmsMainScreen.RealmSelectionList(); Component component = Component.translatable("mco.invites.title"); this.pendingInvitesButton = new RealmsMainScreen.NotificationButton( component, INVITE_SPRITE, button -> this.minecraft.setScreen(new RealmsPendingInvitesScreen(this, component)) ); Component component2 = Component.translatable("mco.news"); this.newsButton = new RealmsMainScreen.NotificationButton(component2, NEWS_SPRITE, button -> { String string = this.newsLink; if (string != null) { ConfirmLinkScreen.confirmLinkNow(this, string); if (this.newsButton.notificationCount() != 0) { RealmsPersistenceData realmsPersistenceData = RealmsPersistence.readFile(); realmsPersistenceData.hasUnreadNews = false; RealmsPersistence.writeFile(realmsPersistenceData); this.newsButton.setNotificationCount(0); } } }); this.newsButton.setTooltip(Tooltip.create(component2)); this.playButton = Button.builder(PLAY_TEXT, button -> play(this.getSelectedServer(), this)).width(100).build(); this.configureButton = Button.builder(CONFIGURE_SERVER_TEXT, button -> this.configureClicked(this.getSelectedServer())).width(100).build(); this.renewButton = Button.builder(SUBSCRIPTION_RENEW_TEXT, button -> this.onRenew(this.getSelectedServer())).width(100).build(); this.leaveButton = Button.builder(LEAVE_SERVER_TEXT, button -> this.leaveClicked(this.getSelectedServer())).width(100).build(); this.addRealmButton = Button.builder(Component.translatable("mco.selectServer.purchase"), button -> this.openTrialAvailablePopup()).size(100, 20).build(); this.backButton = Button.builder(CommonComponents.GUI_BACK, button -> this.onClose()).width(100).build(); if (RealmsClient.ENVIRONMENT == RealmsClient.Environment.STAGE) { this.addRenderableWidget( CycleButton.booleanBuilder(Component.literal("Snapshot"), Component.literal("Release")) .create(5, 5, 100, 20, Component.literal("Realm"), (cycleButton, boolean_) -> { snapshotToggle = boolean_; this.availableSnapshotServers = List.of(); this.debugRefreshDataFetchers(); }) ); } this.updateLayout(RealmsMainScreen.LayoutState.LOADING); this.updateButtonStates(); this.availability.thenAcceptAsync(result -> { Screen screen = result.createErrorScreen(this.lastScreen); if (screen == null) { this.dataSubscription = this.initDataFetcher(this.minecraft.realmsDataFetcher()); } else { this.minecraft.setScreen(screen); } }, this.screenExecutor); } public static boolean isSnapshot() { return SNAPSHOT && snapshotToggle; } @Override protected void repositionElements() { if (this.layout != null) { this.realmSelectionList.updateSize(this.width, this.layout); this.layout.arrangeElements(); } } @Override public void onClose() { this.minecraft.setScreen(this.lastScreen); } private void updateLayout() { if (this.serverList.isEmpty() && this.availableSnapshotServers.isEmpty() && this.notifications.isEmpty()) { this.updateLayout(RealmsMainScreen.LayoutState.NO_REALMS); } else { this.updateLayout(RealmsMainScreen.LayoutState.LIST); } } private void updateLayout(RealmsMainScreen.LayoutState layoutState) { if (this.activeLayoutState != layoutState) { if (this.layout != null) { this.layout.visitWidgets(guiEventListener -> this.removeWidget(guiEventListener)); } this.layout = this.createLayout(layoutState); this.activeLayoutState = layoutState; this.layout.visitWidgets(guiEventListener -> { AbstractWidget var10000 = this.addRenderableWidget(guiEventListener); }); this.repositionElements(); } } private HeaderAndFooterLayout createLayout(RealmsMainScreen.LayoutState layoutState) { HeaderAndFooterLayout headerAndFooterLayout = new HeaderAndFooterLayout(this); headerAndFooterLayout.setHeaderHeight(44); headerAndFooterLayout.addToHeader(this.createHeader()); Layout layout = this.createFooter(layoutState); layout.arrangeElements(); headerAndFooterLayout.setFooterHeight(layout.getHeight() + 22); headerAndFooterLayout.addToFooter(layout); switch (layoutState) { case LOADING: headerAndFooterLayout.addToContents(new LoadingDotsWidget(this.font, LOADING_TEXT)); break; case NO_REALMS: headerAndFooterLayout.addToContents(this.createNoRealmsContent()); break; case LIST: headerAndFooterLayout.addToContents(this.realmSelectionList); } return headerAndFooterLayout; } private Layout createHeader() { int i = 90; LinearLayout linearLayout = LinearLayout.horizontal().spacing(4); linearLayout.defaultCellSetting().alignVerticallyMiddle(); linearLayout.addChild(this.pendingInvitesButton); linearLayout.addChild(this.newsButton); LinearLayout linearLayout2 = LinearLayout.horizontal(); linearLayout2.defaultCellSetting().alignVerticallyMiddle(); linearLayout2.addChild(SpacerElement.width(90)); linearLayout2.addChild(ImageWidget.texture(128, 34, LOGO_LOCATION, 128, 64), LayoutSettings::alignHorizontallyCenter); linearLayout2.addChild(new FrameLayout(90, 44)).addChild(linearLayout, LayoutSettings::alignHorizontallyRight); return linearLayout2; } private Layout createFooter(RealmsMainScreen.LayoutState layoutState) { GridLayout gridLayout = new GridLayout().spacing(4); RowHelper rowHelper = gridLayout.createRowHelper(3); if (layoutState == RealmsMainScreen.LayoutState.LIST) { rowHelper.addChild(this.playButton); rowHelper.addChild(this.configureButton); rowHelper.addChild(this.renewButton); rowHelper.addChild(this.leaveButton); } rowHelper.addChild(this.addRealmButton); rowHelper.addChild(this.backButton); return gridLayout; } private LinearLayout createNoRealmsContent() { LinearLayout linearLayout = LinearLayout.vertical().spacing(8); linearLayout.defaultCellSetting().alignHorizontallyCenter(); linearLayout.addChild(ImageWidget.texture(130, 64, NO_REALMS_LOCATION, 130, 64)); FocusableTextWidget focusableTextWidget = new FocusableTextWidget(308, NO_REALMS_TEXT, this.font, false, 4); linearLayout.addChild(focusableTextWidget); return linearLayout; } void updateButtonStates() { RealmsServer realmsServer = this.getSelectedServer(); this.addRealmButton.active = this.activeLayoutState != RealmsMainScreen.LayoutState.LOADING; this.playButton.active = realmsServer != null && this.shouldPlayButtonBeActive(realmsServer); this.renewButton.active = realmsServer != null && this.shouldRenewButtonBeActive(realmsServer); this.leaveButton.active = realmsServer != null && this.shouldLeaveButtonBeActive(realmsServer); this.configureButton.active = realmsServer != null && this.shouldConfigureButtonBeActive(realmsServer); } boolean shouldPlayButtonBeActive(RealmsServer realmsServer) { boolean bl = !realmsServer.expired && realmsServer.state == State.OPEN; return bl && (realmsServer.isCompatible() || realmsServer.needsUpgrade() || isSelfOwnedServer(realmsServer)); } private boolean shouldRenewButtonBeActive(RealmsServer realmsServer) { return realmsServer.expired && isSelfOwnedServer(realmsServer); } private boolean shouldConfigureButtonBeActive(RealmsServer realmsServer) { return isSelfOwnedServer(realmsServer) && realmsServer.state != State.UNINITIALIZED; } private boolean shouldLeaveButtonBeActive(RealmsServer realmsServer) { return !isSelfOwnedServer(realmsServer); } @Override public void tick() { super.tick(); if (this.dataSubscription != null) { this.dataSubscription.tick(); } } public static void refreshPendingInvites() { Minecraft.getInstance().realmsDataFetcher().pendingInvitesTask.reset(); } public static void refreshServerList() { Minecraft.getInstance().realmsDataFetcher().serverListUpdateTask.reset(); } private void debugRefreshDataFetchers() { for (Task task : this.minecraft.realmsDataFetcher().getTasks()) { task.reset(); } } private Subscription initDataFetcher(RealmsDataFetcher dataFetcher) { Subscription subscription = dataFetcher.dataFetcher.createSubscription(); subscription.subscribe(dataFetcher.serverListUpdateTask, serverListData -> { this.serverList.updateServersList(serverListData.serverList()); this.availableSnapshotServers = serverListData.availableSnapshotServers(); this.refreshListAndLayout(); boolean bl = false; for (RealmsServer realmsServer : this.serverList) { if (this.isSelfOwnedNonExpiredServer(realmsServer)) { bl = true; } } if (!regionsPinged && bl) { regionsPinged = true; this.pingRegions(); } }); callRealmsClient(RealmsClient::getNotifications, list -> { this.notifications.clear(); this.notifications.addAll(list); for (RealmsNotification realmsNotification : list) { if (realmsNotification instanceof InfoPopup infoPopup) { PopupScreen popupScreen = infoPopup.buildScreen(this, this::dismissNotification); if (popupScreen != null) { this.minecraft.setScreen(popupScreen); this.markNotificationsAsSeen(List.of(realmsNotification)); break; } } } if (!this.notifications.isEmpty() && this.activeLayoutState != RealmsMainScreen.LayoutState.LOADING) { this.refreshListAndLayout(); } }); subscription.subscribe(dataFetcher.pendingInvitesTask, integer -> { this.pendingInvitesButton.setNotificationCount(integer); this.pendingInvitesButton.setTooltip(integer == 0 ? Tooltip.create(NO_PENDING_INVITES) : Tooltip.create(PENDING_INVITES)); if (integer > 0 && this.inviteNarrationLimiter.tryAcquire(1)) { this.minecraft.getNarrator().sayNow(Component.translatable("mco.configure.world.invite.narration", integer)); } }); subscription.subscribe(dataFetcher.trialAvailabilityTask, boolean_ -> this.trialsAvailable = boolean_); subscription.subscribe(dataFetcher.onlinePlayersTask, realmsServerPlayerLists -> this.onlinePlayersPerRealm = realmsServerPlayerLists); subscription.subscribe(dataFetcher.newsTask, realmsNews -> { dataFetcher.newsManager.updateUnreadNews(realmsNews); this.newsLink = dataFetcher.newsManager.newsLink(); this.newsButton.setNotificationCount(dataFetcher.newsManager.hasUnreadNews() ? Integer.MAX_VALUE : 0); }); return subscription; } void markNotificationsAsSeen(Collection notifications) { List list = new ArrayList(notifications.size()); for (RealmsNotification realmsNotification : notifications) { if (!realmsNotification.seen() && !this.handledSeenNotifications.contains(realmsNotification.uuid())) { list.add(realmsNotification.uuid()); } } if (!list.isEmpty()) { callRealmsClient(realmsClient -> { realmsClient.notificationsSeen(list); return null; }, object -> this.handledSeenNotifications.addAll(list)); } } private static void callRealmsClient(RealmsMainScreen.RealmsCall call, Consumer onFinish) { Minecraft minecraft = Minecraft.getInstance(); CompletableFuture.supplyAsync(() -> { try { return call.request(RealmsClient.getOrCreate(minecraft)); } catch (RealmsServiceException var3) { throw new RuntimeException(var3); } }).thenAcceptAsync(onFinish, minecraft).exceptionally(throwable -> { LOGGER.error("Failed to execute call to Realms Service", throwable); return null; }); } private void refreshListAndLayout() { this.realmSelectionList.refreshEntries(this, this.getSelectedServer()); this.updateLayout(); this.updateButtonStates(); } private void pingRegions() { new Thread(() -> { List list = Ping.pingAllRegions(); RealmsClient realmsClient = RealmsClient.getOrCreate(); PingResult pingResult = new PingResult(); pingResult.pingResults = list; pingResult.realmIds = this.getOwnedNonExpiredRealmIds(); try { realmsClient.sendPingResults(pingResult); } catch (Throwable var5) { LOGGER.warn("Could not send ping result to Realms: ", var5); } }).start(); } private List getOwnedNonExpiredRealmIds() { List list = Lists.newArrayList(); for (RealmsServer realmsServer : this.serverList) { if (this.isSelfOwnedNonExpiredServer(realmsServer)) { list.add(realmsServer.id); } } return list; } private void onRenew(@Nullable RealmsServer realmsServer) { if (realmsServer != null) { String string = CommonLinks.extendRealms(realmsServer.remoteSubscriptionId, this.minecraft.getUser().getProfileId(), realmsServer.expiredTrial); this.minecraft.keyboardHandler.setClipboard(string); Util.getPlatform().openUri(string); } } private void configureClicked(@Nullable RealmsServer realmsServer) { if (realmsServer != null && this.minecraft.isLocalPlayer(realmsServer.ownerUUID)) { this.minecraft.setScreen(new RealmsConfigureWorldScreen(this, realmsServer.id)); } } private void leaveClicked(@Nullable RealmsServer realmsServer) { if (realmsServer != null && !this.minecraft.isLocalPlayer(realmsServer.ownerUUID)) { Component component = Component.translatable("mco.configure.world.leave.question.line1"); this.minecraft.setScreen(RealmsPopups.infoPopupScreen(this, component, popupScreen -> this.leaveServer(realmsServer))); } } @Nullable private RealmsServer getSelectedServer() { return this.realmSelectionList.getSelected() instanceof RealmsMainScreen.ServerEntry serverEntry ? serverEntry.getServer() : null; } private void leaveServer(RealmsServer server) { (new Thread("Realms-leave-server") { public void run() { try { RealmsClient realmsClient = RealmsClient.getOrCreate(); realmsClient.uninviteMyselfFrom(server.id); RealmsMainScreen.this.minecraft.execute(RealmsMainScreen::refreshServerList); } catch (RealmsServiceException var2) { RealmsMainScreen.LOGGER.error("Couldn't configure world", (Throwable)var2); RealmsMainScreen.this.minecraft.execute(() -> RealmsMainScreen.this.minecraft.setScreen(new RealmsGenericErrorScreen(var2, RealmsMainScreen.this))); } } }).start(); this.minecraft.setScreen(this); } void dismissNotification(UUID uuid) { callRealmsClient(realmsClient -> { realmsClient.notificationsDismiss(List.of(uuid)); return null; }, object -> { this.notifications.removeIf(realmsNotification -> realmsNotification.dismissable() && uuid.equals(realmsNotification.uuid())); this.refreshListAndLayout(); }); } public void resetScreen() { this.realmSelectionList.setSelected(null); refreshServerList(); } @Override public Component getNarrationMessage() { return (Component)(switch (this.activeLayoutState) { case LOADING -> CommonComponents.joinForNarration(super.getNarrationMessage(), LOADING_TEXT); case NO_REALMS -> CommonComponents.joinForNarration(super.getNarrationMessage(), NO_REALMS_TEXT); case LIST -> super.getNarrationMessage(); }); } @Override public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { super.render(guiGraphics, mouseX, mouseY, partialTick); if (isSnapshot()) { guiGraphics.drawString(this.font, "Minecraft " + SharedConstants.getCurrentVersion().getName(), 2, this.height - 10, -1); } if (this.trialsAvailable && this.addRealmButton.active) { AddRealmPopupScreen.renderDiamond(guiGraphics, this.addRealmButton); } switch (RealmsClient.ENVIRONMENT) { case STAGE: this.renderEnvironment(guiGraphics, "STAGE!", -256); break; case LOCAL: this.renderEnvironment(guiGraphics, "LOCAL!", 8388479); } } private void openTrialAvailablePopup() { this.minecraft.setScreen(new AddRealmPopupScreen(this, this.trialsAvailable)); } public static void play(@Nullable RealmsServer realmsServer, Screen lastScreen) { play(realmsServer, lastScreen, false); } public static void play(@Nullable RealmsServer realmsServer, Screen lastScreen, boolean allowSnapshots) { if (realmsServer != null) { if (!isSnapshot() || allowSnapshots || realmsServer.isMinigameActive()) { Minecraft.getInstance().setScreen(new RealmsLongRunningMcoTaskScreen(lastScreen, new GetServerDetailsTask(lastScreen, realmsServer))); return; } switch (realmsServer.compatibility) { case COMPATIBLE: Minecraft.getInstance().setScreen(new RealmsLongRunningMcoTaskScreen(lastScreen, new GetServerDetailsTask(lastScreen, realmsServer))); break; case UNVERIFIABLE: confirmToPlay( realmsServer, lastScreen, Component.translatable("mco.compatibility.unverifiable.title").withColor(-171), Component.translatable("mco.compatibility.unverifiable.message"), CommonComponents.GUI_CONTINUE ); break; case NEEDS_DOWNGRADE: confirmToPlay( realmsServer, lastScreen, Component.translatable("selectWorld.backupQuestion.downgrade").withColor(-2142128), Component.translatable( "mco.compatibility.downgrade.description", Component.literal(realmsServer.activeVersion).withColor(-171), Component.literal(SharedConstants.getCurrentVersion().getName()).withColor(-171) ), Component.translatable("mco.compatibility.downgrade") ); break; case NEEDS_UPGRADE: upgradeRealmAndPlay(realmsServer, lastScreen); break; case INCOMPATIBLE: Minecraft.getInstance() .setScreen( new PopupScreen.Builder(lastScreen, INCOMPATIBLE_POPUP_TITLE) .setMessage( Component.translatable( "mco.compatibility.incompatible.series.popup.message", Component.literal(realmsServer.activeVersion).withColor(-171), Component.literal(SharedConstants.getCurrentVersion().getName()).withColor(-171) ) ) .addButton(CommonComponents.GUI_BACK, PopupScreen::onClose) .build() ); break; case RELEASE_TYPE_INCOMPATIBLE: Minecraft.getInstance() .setScreen( new PopupScreen.Builder(lastScreen, INCOMPATIBLE_POPUP_TITLE) .setMessage(INCOMPATIBLE_RELEASE_TYPE_POPUP_MESSAGE) .addButton(CommonComponents.GUI_BACK, PopupScreen::onClose) .build() ); } } } private static void confirmToPlay(RealmsServer realmsServer, Screen lastScreen, Component title, Component message, Component confirmButton) { Minecraft.getInstance().setScreen(new PopupScreen.Builder(lastScreen, title).setMessage(message).addButton(confirmButton, popupScreen -> { Minecraft.getInstance().setScreen(new RealmsLongRunningMcoTaskScreen(lastScreen, new GetServerDetailsTask(lastScreen, realmsServer))); refreshServerList(); }).addButton(CommonComponents.GUI_CANCEL, PopupScreen::onClose).build()); } private static void upgradeRealmAndPlay(RealmsServer server, Screen lastScreen) { Component component = Component.translatable("mco.compatibility.upgrade.title").withColor(-171); Component component2 = Component.translatable("mco.compatibility.upgrade"); Component component3 = Component.literal(server.activeVersion).withColor(-171); Component component4 = Component.literal(SharedConstants.getCurrentVersion().getName()).withColor(-171); Component component5 = isSelfOwnedServer(server) ? Component.translatable("mco.compatibility.upgrade.description", component3, component4) : Component.translatable("mco.compatibility.upgrade.friend.description", component3, component4); confirmToPlay(server, lastScreen, component, component5, component2); } public static Component getVersionComponent(String version, boolean compatible) { return getVersionComponent(version, compatible ? -8355712 : -2142128); } public static Component getVersionComponent(String version, int color) { return (Component)(StringUtils.isBlank(version) ? CommonComponents.EMPTY : Component.literal(version).withColor(color)); } public static Component getGameModeComponent(int gamemode, boolean hardcore) { return (Component)(hardcore ? Component.translatable("gameMode.hardcore").withColor(-65536) : GameType.byId(gamemode).getLongDisplayName()); } static boolean isSelfOwnedServer(RealmsServer server) { return Minecraft.getInstance().isLocalPlayer(server.ownerUUID); } private boolean isSelfOwnedNonExpiredServer(RealmsServer server) { return isSelfOwnedServer(server) && !server.expired; } private void renderEnvironment(GuiGraphics guiGraphics, String text, int color) { guiGraphics.pose().pushPose(); guiGraphics.pose().translate((float)(this.width / 2 - 25), 20.0F, 0.0F); guiGraphics.pose().mulPose(Axis.ZP.rotationDegrees(-20.0F)); guiGraphics.pose().scale(1.5F, 1.5F, 1.5F); guiGraphics.drawString(this.font, text, 0, 0, color); guiGraphics.pose().popPose(); } @Environment(EnvType.CLIENT) class AvailableSnapshotEntry extends RealmsMainScreen.Entry { private static final Component START_SNAPSHOT_REALM = Component.translatable("mco.snapshot.start"); private static final int TEXT_PADDING = 5; private final WidgetTooltipHolder tooltip = new WidgetTooltipHolder(); private final RealmsServer parent; public AvailableSnapshotEntry(final RealmsServer parent) { this.parent = parent; this.tooltip.set(Tooltip.create(Component.translatable("mco.snapshot.tooltip"))); } @Override public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { guiGraphics.blitSprite(RenderType::guiTextured, RealmsMainScreen.NEW_REALM_SPRITE, left - 5, top + height / 2 - 10, 40, 20); int i = top + height / 2 - 9 / 2; guiGraphics.drawString(RealmsMainScreen.this.font, START_SNAPSHOT_REALM, left + 40 - 2, i - 5, 8388479); guiGraphics.drawString( RealmsMainScreen.this.font, Component.translatable("mco.snapshot.description", Objects.requireNonNullElse(this.parent.name, "unknown server")), left + 40 - 2, i + 5, -8355712 ); this.tooltip.refreshTooltipForNextRenderPass(hovering, this.isFocused(), new ScreenRectangle(left, top, width, height)); } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { this.addSnapshotRealm(); return true; } @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { if (CommonInputs.selected(keyCode)) { this.addSnapshotRealm(); return false; } else { return super.keyPressed(keyCode, scanCode, modifiers); } } private void addSnapshotRealm() { RealmsMainScreen.this.minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); RealmsMainScreen.this.minecraft .setScreen( new PopupScreen.Builder(RealmsMainScreen.this, Component.translatable("mco.snapshot.createSnapshotPopup.title")) .setMessage(Component.translatable("mco.snapshot.createSnapshotPopup.text")) .addButton( Component.translatable("mco.selectServer.create"), popupScreen -> RealmsMainScreen.this.minecraft.setScreen(new RealmsCreateRealmScreen(RealmsMainScreen.this, this.parent, true)) ) .addButton(CommonComponents.GUI_CANCEL, PopupScreen::onClose) .build() ); } @Override public Component getNarration() { return Component.translatable( "gui.narrate.button", CommonComponents.joinForNarration( START_SNAPSHOT_REALM, Component.translatable("mco.snapshot.description", Objects.requireNonNullElse(this.parent.name, "unknown server")) ) ); } } @Environment(EnvType.CLIENT) class ButtonEntry extends RealmsMainScreen.Entry { private final Button button; public ButtonEntry(final Button button) { this.button = button; } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { this.button.mouseClicked(mouseX, mouseY, button); return super.mouseClicked(mouseX, mouseY, button); } @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { return this.button.keyPressed(keyCode, scanCode, modifiers) ? true : super.keyPressed(keyCode, scanCode, modifiers); } @Override public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { this.button.setPosition(RealmsMainScreen.this.width / 2 - 75, top + 4); this.button.render(guiGraphics, mouseX, mouseY, partialTick); } @Override public void setFocused(boolean focused) { super.setFocused(focused); this.button.setFocused(focused); } @Override public Component getNarration() { return this.button.getMessage(); } } @Environment(EnvType.CLIENT) static class CrossButton extends ImageButton { private static final WidgetSprites SPRITES = new WidgetSprites( ResourceLocation.withDefaultNamespace("widget/cross_button"), ResourceLocation.withDefaultNamespace("widget/cross_button_highlighted") ); protected CrossButton(OnPress onPress, Component message) { super(0, 0, 14, 14, SPRITES, onPress); this.setTooltip(Tooltip.create(message)); } } @Environment(EnvType.CLIENT) class EmptyEntry extends RealmsMainScreen.Entry { @Override public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { } @Override public Component getNarration() { return Component.empty(); } } @Environment(EnvType.CLIENT) abstract class Entry extends ObjectSelectionList.Entry { protected static final int STATUS_LIGHT_WIDTH = 10; private static final int STATUS_LIGHT_HEIGHT = 28; protected static final int PADDING_X = 7; protected static final int PADDING_Y = 2; protected void renderStatusLights(RealmsServer realmsServer, GuiGraphics guiGraphics, int x, int y, int mouseX, int mouseY) { int i = x - 10 - 7; int j = y + 2; if (realmsServer.expired) { this.drawRealmStatus(guiGraphics, i, j, mouseX, mouseY, RealmsMainScreen.EXPIRED_SPRITE, () -> RealmsMainScreen.SERVER_EXPIRED_TOOLTIP); } else if (realmsServer.state == State.CLOSED) { this.drawRealmStatus(guiGraphics, i, j, mouseX, mouseY, RealmsMainScreen.CLOSED_SPRITE, () -> RealmsMainScreen.SERVER_CLOSED_TOOLTIP); } else if (RealmsMainScreen.isSelfOwnedServer(realmsServer) && realmsServer.daysLeft < 7) { this.drawRealmStatus( guiGraphics, i, j, mouseX, mouseY, RealmsMainScreen.EXPIRES_SOON_SPRITE, () -> { if (realmsServer.daysLeft <= 0) { return RealmsMainScreen.SERVER_EXPIRES_SOON_TOOLTIP; } else { return (Component)(realmsServer.daysLeft == 1 ? RealmsMainScreen.SERVER_EXPIRES_IN_DAY_TOOLTIP : Component.translatable("mco.selectServer.expires.days", realmsServer.daysLeft)); } } ); } else if (realmsServer.state == State.OPEN) { this.drawRealmStatus(guiGraphics, i, j, mouseX, mouseY, RealmsMainScreen.OPEN_SPRITE, () -> RealmsMainScreen.SERVER_OPEN_TOOLTIP); } } private void drawRealmStatus( GuiGraphics guiGraphics, int x, int y, int mouseX, int mouseY, ResourceLocation spriteLocation, Supplier tooltipSupplier ) { guiGraphics.blitSprite(RenderType::guiTextured, spriteLocation, x, y, 10, 28); if (RealmsMainScreen.this.realmSelectionList.isMouseOver(mouseX, mouseY) && mouseX >= x && mouseX <= x + 10 && mouseY >= y && mouseY <= y + 28) { RealmsMainScreen.this.setTooltipForNextRenderPass((Component)tooltipSupplier.get()); } } protected void renderThirdLine(GuiGraphics guiGraphics, int top, int left, RealmsServer server) { int i = this.textX(left); int j = this.firstLineY(top); int k = this.thirdLineY(j); if (!RealmsMainScreen.isSelfOwnedServer(server)) { guiGraphics.drawString(RealmsMainScreen.this.font, server.owner, i, this.thirdLineY(j), -8355712); } else if (server.expired) { Component component = server.expiredTrial ? RealmsMainScreen.TRIAL_EXPIRED_TEXT : RealmsMainScreen.SUBSCRIPTION_EXPIRED_TEXT; guiGraphics.drawString(RealmsMainScreen.this.font, component, i, k, -2142128); } } protected void renderClampedString(GuiGraphics guiGraphics, @Nullable String text, int minX, int y, int maxX, int color) { if (text != null) { int i = maxX - minX; if (RealmsMainScreen.this.font.width(text) > i) { String string = RealmsMainScreen.this.font.plainSubstrByWidth(text, i - RealmsMainScreen.this.font.width("... ")); guiGraphics.drawString(RealmsMainScreen.this.font, string + "...", minX, y, color); } else { guiGraphics.drawString(RealmsMainScreen.this.font, text, minX, y, color); } } } protected int versionTextX(int left, int width, Component versionComponent) { return left + width - RealmsMainScreen.this.font.width(versionComponent) - 20; } protected int gameModeTextX(int left, int width, Component component) { return left + width - RealmsMainScreen.this.font.width(component) - 20; } protected int renderGameMode(RealmsServer server, GuiGraphics guiGraphics, int left, int width, int firstLineY) { boolean bl = server.isHardcore; int i = server.gameMode; int j = left; if (GameType.isValidId(i)) { Component component = RealmsMainScreen.getGameModeComponent(i, bl); j = this.gameModeTextX(left, width, component); guiGraphics.drawString(RealmsMainScreen.this.font, component, j, this.secondLineY(firstLineY), -8355712); } if (bl) { j -= 10; guiGraphics.blitSprite(RenderType::guiTextured, RealmsMainScreen.HARDCORE_MODE_SPRITE, j, this.secondLineY(firstLineY), 8, 8); } return j; } protected int firstLineY(int top) { return top + 1; } protected int lineHeight() { return 2 + 9; } protected int textX(int left) { return left + 36 + 2; } protected int secondLineY(int firstLineY) { return firstLineY + this.lineHeight(); } protected int thirdLineY(int firstLineY) { return firstLineY + this.lineHeight() * 2; } } @Environment(EnvType.CLIENT) static enum LayoutState { LOADING, NO_REALMS, LIST; } @Environment(EnvType.CLIENT) static class NotificationButton extends CenteredIcon { private static final ResourceLocation[] NOTIFICATION_ICONS = new ResourceLocation[]{ ResourceLocation.withDefaultNamespace("notification/1"), ResourceLocation.withDefaultNamespace("notification/2"), ResourceLocation.withDefaultNamespace("notification/3"), ResourceLocation.withDefaultNamespace("notification/4"), ResourceLocation.withDefaultNamespace("notification/5"), ResourceLocation.withDefaultNamespace("notification/more") }; private static final int UNKNOWN_COUNT = Integer.MAX_VALUE; private static final int SIZE = 20; private static final int SPRITE_SIZE = 14; private int notificationCount; public NotificationButton(Component message, ResourceLocation sprite, OnPress onPress) { super(20, 20, message, 14, 14, sprite, onPress, null); } int notificationCount() { return this.notificationCount; } public void setNotificationCount(int notificationCount) { this.notificationCount = notificationCount; } @Override public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { super.renderWidget(guiGraphics, mouseX, mouseY, partialTick); if (this.active && this.notificationCount != 0) { this.drawNotificationCounter(guiGraphics); } } private void drawNotificationCounter(GuiGraphics guiGraphics) { guiGraphics.blitSprite( RenderType::guiTextured, NOTIFICATION_ICONS[Math.min(this.notificationCount, 6) - 1], this.getX() + this.getWidth() - 5, this.getY() - 3, 8, 8 ); } } @Environment(EnvType.CLIENT) class NotificationMessageEntry extends RealmsMainScreen.Entry { private static final int SIDE_MARGINS = 40; private static final int OUTLINE_COLOR = -12303292; private final Component text; private final int frameItemHeight; private final List children = new ArrayList(); @Nullable private final RealmsMainScreen.CrossButton dismissButton; private final MultiLineTextWidget textWidget; private final GridLayout gridLayout; private final FrameLayout textFrame; private int lastEntryWidth = -1; public NotificationMessageEntry(final Component text, final int frameItemHeight, final RealmsNotification notification) { this.text = text; this.frameItemHeight = frameItemHeight; this.gridLayout = new GridLayout(); int i = 7; this.gridLayout.addChild(ImageWidget.sprite(20, 20, RealmsMainScreen.INFO_SPRITE), 0, 0, this.gridLayout.newCellSettings().padding(7, 7, 0, 0)); this.gridLayout.addChild(SpacerElement.width(40), 0, 0); this.textFrame = this.gridLayout.addChild(new FrameLayout(0, 9 * 3 * (frameItemHeight - 1)), 0, 1, this.gridLayout.newCellSettings().paddingTop(7)); this.textWidget = this.textFrame .addChild( new MultiLineTextWidget(text, RealmsMainScreen.this.font).setCentered(true), this.textFrame.newChildLayoutSettings().alignHorizontallyCenter().alignVerticallyTop() ); this.gridLayout.addChild(SpacerElement.width(40), 0, 2); if (notification.dismissable()) { this.dismissButton = this.gridLayout .addChild( new RealmsMainScreen.CrossButton( button -> RealmsMainScreen.this.dismissNotification(notification.uuid()), Component.translatable("mco.notification.dismiss") ), 0, 2, this.gridLayout.newCellSettings().alignHorizontallyRight().padding(0, 7, 7, 0) ); } else { this.dismissButton = null; } this.gridLayout.visitWidgets(this.children::add); } @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { return this.dismissButton != null && this.dismissButton.keyPressed(keyCode, scanCode, modifiers) ? true : super.keyPressed(keyCode, scanCode, modifiers); } private void updateEntryWidth(int entryWidth) { if (this.lastEntryWidth != entryWidth) { this.refreshLayout(entryWidth); this.lastEntryWidth = entryWidth; } } private void refreshLayout(int width) { int i = width - 80; this.textFrame.setMinWidth(i); this.textWidget.setMaxWidth(i); this.gridLayout.arrangeElements(); } @Override public void renderBack( GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean isMouseOver, float partialTick ) { super.renderBack(guiGraphics, index, top, left, width, height, mouseX, mouseY, isMouseOver, partialTick); guiGraphics.renderOutline(left - 2, top - 2, width, 36 * this.frameItemHeight - 2, -12303292); } @Override public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { this.gridLayout.setPosition(left, top); this.updateEntryWidth(width - 4); this.children.forEach(abstractWidget -> abstractWidget.render(guiGraphics, mouseX, mouseY, partialTick)); } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { if (this.dismissButton != null) { this.dismissButton.mouseClicked(mouseX, mouseY, button); } return super.mouseClicked(mouseX, mouseY, button); } @Override public Component getNarration() { return this.text; } } @Environment(EnvType.CLIENT) class ParentEntry extends RealmsMainScreen.Entry { private final RealmsServer server; private final WidgetTooltipHolder tooltip = new WidgetTooltipHolder(); public ParentEntry(final RealmsServer server) { this.server = server; if (!server.expired) { this.tooltip.set(Tooltip.create(Component.translatable("mco.snapshot.parent.tooltip"))); } } @Override public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { int i = this.textX(left); int j = this.firstLineY(top); RealmsUtil.renderPlayerFace(guiGraphics, left, top, 32, this.server.ownerUUID); Component component = RealmsMainScreen.getVersionComponent(this.server.activeVersion, -8355712); int k = this.versionTextX(left, width, component); this.renderClampedString(guiGraphics, this.server.getName(), i, j, k, -8355712); if (component != CommonComponents.EMPTY) { guiGraphics.drawString(RealmsMainScreen.this.font, component, k, j, -8355712); } int l = left; if (!this.server.isMinigameActive()) { l = this.renderGameMode(this.server, guiGraphics, left, width, j); } this.renderClampedString(guiGraphics, this.server.getDescription(), i, this.secondLineY(j), l, -8355712); this.renderThirdLine(guiGraphics, top, left, this.server); this.renderStatusLights(this.server, guiGraphics, left + width, top, mouseX, mouseY); this.tooltip.refreshTooltipForNextRenderPass(hovering, this.isFocused(), new ScreenRectangle(left, top, width, height)); } @Override public Component getNarration() { return Component.literal((String)Objects.requireNonNullElse(this.server.name, "unknown server")); } } @Environment(EnvType.CLIENT) class RealmSelectionList extends ObjectSelectionList { public RealmSelectionList() { super(Minecraft.getInstance(), RealmsMainScreen.this.width, RealmsMainScreen.this.height, 0, 36); } public void setSelected(@Nullable RealmsMainScreen.Entry entry) { super.setSelected(entry); RealmsMainScreen.this.updateButtonStates(); } @Override public int getRowWidth() { return 300; } void refreshEntries(RealmsMainScreen screen, @Nullable RealmsServer server) { this.clearEntries(); for (RealmsNotification realmsNotification : RealmsMainScreen.this.notifications) { if (realmsNotification instanceof VisitUrl visitUrl) { this.addEntriesForNotification(visitUrl, screen); RealmsMainScreen.this.markNotificationsAsSeen(List.of(realmsNotification)); break; } } this.refreshServerEntries(server); } private void refreshServerEntries(@Nullable RealmsServer server) { for (RealmsServer realmsServer : RealmsMainScreen.this.availableSnapshotServers) { this.addEntry(RealmsMainScreen.this.new AvailableSnapshotEntry(realmsServer)); } for (RealmsServer realmsServer : RealmsMainScreen.this.serverList) { RealmsMainScreen.Entry entry; if (RealmsMainScreen.isSnapshot() && !realmsServer.isSnapshotRealm()) { if (realmsServer.state == State.UNINITIALIZED) { continue; } entry = RealmsMainScreen.this.new ParentEntry(realmsServer); } else { entry = RealmsMainScreen.this.new ServerEntry(realmsServer); } this.addEntry(entry); if (server != null && server.id == realmsServer.id) { this.setSelected(entry); } } } private void addEntriesForNotification(VisitUrl url, RealmsMainScreen mainScreen) { Component component = url.getMessage(); int i = RealmsMainScreen.this.font.wordWrapHeight(component, 216); int j = Mth.positiveCeilDiv(i + 7, 36) - 1; this.addEntry(RealmsMainScreen.this.new NotificationMessageEntry(component, j + 2, url)); for (int k = 0; k < j; k++) { this.addEntry(RealmsMainScreen.this.new EmptyEntry()); } this.addEntry(RealmsMainScreen.this.new ButtonEntry(url.buildOpenLinkButton(mainScreen))); } } @Environment(EnvType.CLIENT) interface RealmsCall { T request(RealmsClient realmsClient) throws RealmsServiceException; } @Environment(EnvType.CLIENT) class ServerEntry extends RealmsMainScreen.Entry { private static final Component ONLINE_PLAYERS_TOOLTIP_HEADER = Component.translatable("mco.onlinePlayers"); private static final int PLAYERS_ONLINE_SPRITE_SIZE = 9; private static final int SKIN_HEAD_LARGE_WIDTH = 36; private final RealmsServer serverData; private final WidgetTooltipHolder tooltip = new WidgetTooltipHolder(); public ServerEntry(final RealmsServer serverData) { this.serverData = serverData; boolean bl = RealmsMainScreen.isSelfOwnedServer(serverData); if (RealmsMainScreen.isSnapshot() && bl && serverData.isSnapshotRealm()) { this.tooltip.set(Tooltip.create(Component.translatable("mco.snapshot.paired", serverData.parentWorldName))); } else if (!bl && serverData.needsDowngrade()) { this.tooltip.set(Tooltip.create(Component.translatable("mco.snapshot.friendsRealm.downgrade", serverData.activeVersion))); } } @Override public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { if (this.serverData.state == State.UNINITIALIZED) { guiGraphics.blitSprite(RenderType::guiTextured, RealmsMainScreen.NEW_REALM_SPRITE, left - 5, top + height / 2 - 10, 40, 20); int i = top + height / 2 - 9 / 2; guiGraphics.drawString(RealmsMainScreen.this.font, RealmsMainScreen.SERVER_UNITIALIZED_TEXT, left + 40 - 2, i, 8388479); } else { this.renderStatusLights(this.serverData, guiGraphics, left + 36, top, mouseX, mouseY); RealmsUtil.renderPlayerFace(guiGraphics, left, top, 32, this.serverData.ownerUUID); this.renderFirstLine(guiGraphics, top, left, width); this.renderSecondLine(guiGraphics, top, left, width); this.renderThirdLine(guiGraphics, top, left, this.serverData); boolean bl = this.renderOnlinePlayers(guiGraphics, top, left, width, height, mouseX, mouseY); this.renderStatusLights(this.serverData, guiGraphics, left + width, top, mouseX, mouseY); if (!bl) { this.tooltip.refreshTooltipForNextRenderPass(hovering, this.isFocused(), new ScreenRectangle(left, top, width, height)); } } } private void renderFirstLine(GuiGraphics guiGraphics, int top, int left, int width) { int i = this.textX(left); int j = this.firstLineY(top); Component component = RealmsMainScreen.getVersionComponent(this.serverData.activeVersion, this.serverData.isCompatible()); int k = this.versionTextX(left, width, component); this.renderClampedString(guiGraphics, this.serverData.getName(), i, j, k, -1); if (component != CommonComponents.EMPTY && !this.serverData.isMinigameActive()) { guiGraphics.drawString(RealmsMainScreen.this.font, component, k, j, -8355712); } } private void renderSecondLine(GuiGraphics guiGraphics, int top, int left, int width) { int i = this.textX(left); int j = this.firstLineY(top); int k = this.secondLineY(j); String string = this.serverData.getMinigameName(); boolean bl = this.serverData.isMinigameActive(); if (bl && string != null) { Component component = Component.literal(string).withStyle(ChatFormatting.GRAY); guiGraphics.drawString(RealmsMainScreen.this.font, Component.translatable("mco.selectServer.minigameName", component).withColor(-171), i, k, -1); } else { int l = this.renderGameMode(this.serverData, guiGraphics, left, width, j); this.renderClampedString(guiGraphics, this.serverData.getDescription(), i, this.secondLineY(j), l, -8355712); } } private boolean renderOnlinePlayers(GuiGraphics guiGraphics, int top, int left, int width, int height, int mouseX, int mouseY) { List list = RealmsMainScreen.this.onlinePlayersPerRealm.getProfileResultsFor(this.serverData.id); if (!list.isEmpty()) { int i = left + width - 21; int j = top + height - 9 - 2; int k = i; for (int l = 0; l < list.size(); l++) { k -= 9 + (l == 0 ? 0 : 3); PlayerFaceRenderer.draw(guiGraphics, Minecraft.getInstance().getSkinManager().getInsecureSkin(((ProfileResult)list.get(l)).profile()), k, j, 9); } if (mouseX >= k && mouseX <= i && mouseY >= j && mouseY <= j + 9) { guiGraphics.renderTooltip(RealmsMainScreen.this.font, List.of(ONLINE_PLAYERS_TOOLTIP_HEADER), Optional.of(new ActivePlayersTooltip(list)), mouseX, mouseY); return true; } } return false; } private void playRealm() { RealmsMainScreen.this.minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); RealmsMainScreen.play(this.serverData, RealmsMainScreen.this); } private void createUnitializedRealm() { RealmsMainScreen.this.minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); RealmsCreateRealmScreen realmsCreateRealmScreen = new RealmsCreateRealmScreen(RealmsMainScreen.this, this.serverData, this.serverData.isSnapshotRealm()); RealmsMainScreen.this.minecraft.setScreen(realmsCreateRealmScreen); } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { if (this.serverData.state == State.UNINITIALIZED) { this.createUnitializedRealm(); } else if (RealmsMainScreen.this.shouldPlayButtonBeActive(this.serverData)) { if (Util.getMillis() - RealmsMainScreen.this.lastClickTime < 250L && this.isFocused()) { this.playRealm(); } RealmsMainScreen.this.lastClickTime = Util.getMillis(); } return true; } @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { if (CommonInputs.selected(keyCode)) { if (this.serverData.state == State.UNINITIALIZED) { this.createUnitializedRealm(); return true; } if (RealmsMainScreen.this.shouldPlayButtonBeActive(this.serverData)) { this.playRealm(); return true; } } return super.keyPressed(keyCode, scanCode, modifiers); } @Override public Component getNarration() { return (Component)(this.serverData.state == State.UNINITIALIZED ? RealmsMainScreen.UNITIALIZED_WORLD_NARRATION : Component.translatable("narrator.select", Objects.requireNonNullElse(this.serverData.name, "unknown server"))); } public RealmsServer getServer() { return this.serverData; } } }