From a19a38c684ce25bc83bf784e48234122ced9fa4b Mon Sep 17 00:00:00 2001 From: Josiah Glosson Date: Mon, 15 Jul 2024 17:36:36 -0500 Subject: [PATCH] Allow plugins to register "friend adders" --- .../github/gaming32/worldhost/WorldHost.java | 9 + .../worldhost/gui/screen/AddFriendScreen.java | 188 ++++++++++-------- .../worldhost/gui/screen/FriendsScreen.java | 37 ++-- .../gui/screen/OnlineFriendsScreen.java | 2 +- .../gui/widget/FriendAdderSelectorButton.java | 24 +++ .../worldhost/plugin/FriendAdder.java | 14 ++ .../worldhost/plugin/FriendListFriend.java | 6 + .../worldhost/plugin/WorldHostPlugin.java | 5 + .../vanilla/GameProfileBasedProfilable.java | 3 + .../vanilla/GameProfileProfileInfo.java | 6 +- .../vanilla/VanillaWorldHostPlugin.java | 9 + .../plugin/vanilla/WorldHostFriendAdder.java | 53 +++++ .../vanilla/WorldHostFriendListFriend.java | 9 + .../protocol/WorldHostS2CMessage.java | 5 +- .../assets/world-host/lang/en_us.json | 1 + 15 files changed, 263 insertions(+), 108 deletions(-) create mode 100644 src/main/java/io/github/gaming32/worldhost/gui/widget/FriendAdderSelectorButton.java create mode 100644 src/main/java/io/github/gaming32/worldhost/plugin/FriendAdder.java create mode 100644 src/main/java/io/github/gaming32/worldhost/plugin/vanilla/WorldHostFriendAdder.java diff --git a/src/main/java/io/github/gaming32/worldhost/WorldHost.java b/src/main/java/io/github/gaming32/worldhost/WorldHost.java index 36a4162..2c0d4c6 100644 --- a/src/main/java/io/github/gaming32/worldhost/WorldHost.java +++ b/src/main/java/io/github/gaming32/worldhost/WorldHost.java @@ -11,6 +11,7 @@ import io.github.gaming32.worldhost.config.WorldHostConfig; import io.github.gaming32.worldhost.gui.OnlineStatusLocation; import io.github.gaming32.worldhost.gui.screen.JoiningWorldHostScreen; +import io.github.gaming32.worldhost.plugin.FriendAdder; import io.github.gaming32.worldhost.plugin.InfoTextsCategory; import io.github.gaming32.worldhost.plugin.OnlineFriend; import io.github.gaming32.worldhost.plugin.WorldHostPlugin; @@ -417,6 +418,14 @@ public static List getInfoTexts(InfoTextsCategory category) { return result; } + public static List getFriendAdders() { + return plugins.stream() + .map(LoadedWorldHostPlugin::plugin) + .map(WorldHostPlugin::friendAdder) + .flatMap(Optional::stream) + .toList(); + } + public static void tickHandler() { if (protoClient == null || protoClient.isClosed()) { protoClient = null; diff --git a/src/main/java/io/github/gaming32/worldhost/gui/screen/AddFriendScreen.java b/src/main/java/io/github/gaming32/worldhost/gui/screen/AddFriendScreen.java index 54e749d..4b1e717 100644 --- a/src/main/java/io/github/gaming32/worldhost/gui/screen/AddFriendScreen.java +++ b/src/main/java/io/github/gaming32/worldhost/gui/screen/AddFriendScreen.java @@ -1,8 +1,11 @@ package io.github.gaming32.worldhost.gui.screen; -import com.mojang.authlib.GameProfile; import com.mojang.blaze3d.systems.RenderSystem; import io.github.gaming32.worldhost.WorldHost; +import io.github.gaming32.worldhost.gui.widget.FriendAdderSelectorButton; +import io.github.gaming32.worldhost.plugin.FriendAdder; +import io.github.gaming32.worldhost.plugin.FriendListFriend; +import io.github.gaming32.worldhost.plugin.ProfileInfo; import io.github.gaming32.worldhost.versions.Components; import net.minecraft.Util; import net.minecraft.client.Minecraft; @@ -12,14 +15,12 @@ import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.server.players.GameProfileCache; -import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; -import java.nio.charset.StandardCharsets; -import java.util.UUID; +import java.util.List; import java.util.function.Consumer; -import java.util.regex.Pattern; //#if MC >= 1.20.0 import net.minecraft.client.gui.GuiGraphics; @@ -28,94 +29,135 @@ //#endif public class AddFriendScreen extends WorldHostScreen { - public static final Pattern VALID_USERNAME = Pattern.compile("^[a-zA-Z0-9_]{1,16}$"); - public static final Pattern VALID_UUID = Pattern.compile("^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$"); private static final Component FRIEND_USERNAME_TEXT = Components.translatable("world-host.add_friend.enter_username"); private final Screen parent; - private final Consumer addAction; + private final Consumer addAction; + + private final List friendAdders = WorldHost.getFriendAdders(); + private FriendAdder friendAdder = friendAdders.getFirst(); - private Consumer usernameResponder; private Button addFriendButton; - private EditBox usernameField; + private EditBox nameField; private long lastTyping; - private boolean usernameUpdate; - private GameProfile friendProfile; + private boolean delayedFriendUpdate; + + private FriendListFriend friend; + private int reloadCount; + private ProfileInfo friendProfile; - public AddFriendScreen(Screen parent, Component title, UUID prefilledUser, Consumer addAction) { + public AddFriendScreen( + Screen parent, + Component title, + @Nullable FriendListFriend prefilledFriend, + Consumer addAction + ) { super(title); this.parent = parent; this.addAction = addAction; - if (prefilledUser != null) { - friendProfile = WorldHost.fetchProfile(Minecraft.getInstance().getMinecraftSessionService(), prefilledUser); + if (prefilledFriend != null) { + setFriend(prefilledFriend); } } + private void setFriend(FriendListFriend friend) { + this.friend = friend; + addFriendButton.active = friend != null; + final int currentReloadCount = ++reloadCount; + if (friend == null) { + friendProfile = null; + return; + } + friendProfile = friend.fallbackProfileInfo(); + friend.profileInfo() + .thenAcceptAsync(ready -> { + if (reloadCount == currentReloadCount) { + friendProfile = ready; + } + }, Minecraft.getInstance()) + .exceptionally(t -> { + WorldHost.LOGGER.error("Failed to request profile info for {}", friend, t); + return null; + }); + } + + private void resolveFriend(String name) { + final int currentReloadCount = ++reloadCount; + friendAdder.resolveFriend(name) + .thenAcceptAsync(ready -> { + if (reloadCount == currentReloadCount) { + setFriend(ready.orElse(null)); + } + }, Minecraft.getInstance()) + .exceptionally(t -> { + WorldHost.LOGGER.error("Failed to request friend with name '{}'", name, t); + return null; + }); + } + @Override protected void init() { assert minecraft != null; sendRepeatEvents(true); GameProfileCache.setUsesAuthentication(true); // This makes non-existent users return an empty value instead of an offline mode fallback. + nameField = addRenderableWidget(new EditBox(font, width / 2 - 100, 66, 200, 20, nameField, FRIEND_USERNAME_TEXT)); + nameField.setMaxLength(36); + //#if MC >= 1.19.4 + setInitialFocus(nameField); + //#else + //$$ usernameField.setFocus(true); + //#endif + nameField.setResponder(name -> { + lastTyping = Util.getMillis(); + setFriend(null); + addFriendButton.active = false; + if (friendAdder.rateLimit(name)) { + delayedFriendUpdate = true; + } else { + delayedFriendUpdate = false; + resolveFriend(name); + } + }); + + int extraY = 0; + if (showFriendAddersButton()) { + addRenderableWidget(new FriendAdderSelectorButton( + width / 2 - 100, 90, 200, 20, + Components.translatable("world-host.add_friend.friend_adder"), + button -> { + friendAdder = button.getValue(); + setFriend(null); + addFriendButton.active = false; + resolveFriend(nameField.getValue()); + }, + friendAdders.toArray(FriendAdder[]::new) + )); + extraY += 24; + } + addFriendButton = addRenderableWidget( button(Components.translatable("world-host.add_friend"), button -> { - if (friendProfile != null) { // Just in case the user somehow clicks the button with this null - addAction.accept(friendProfile); + if (friend != null) { // Just in case the user somehow clicks the button with this null + addAction.accept(friend); } minecraft.setScreen(parent); - }).pos(width / 2 - 100, height / 4 + 108) + }).pos(width / 2 - 100, height / 4 + 108 + extraY) .width(200) .build() ); - addFriendButton.active = friendProfile != null; + addFriendButton.active = friend != null; addRenderableWidget( button(CommonComponents.GUI_CANCEL, button -> minecraft.setScreen(parent)) - .pos(width / 2 - 100, height / 4 + 132) + .pos(width / 2 - 100, height / 4 + 132 + extraY) .width(200) .build() ); - - usernameField = addWidget(new EditBox(font, width / 2 - 100, 66, 200, 20, FRIEND_USERNAME_TEXT)); - usernameField.setMaxLength(36); - //#if MC >= 1.19.4 - setInitialFocus(usernameField); - //#else - //$$ usernameField.setFocus(true); - //#endif - if (friendProfile != null) { - usernameField.setValue(StringUtils.getIfBlank(friendProfile.getName(), () -> friendProfile.getId().toString())); - } - if (usernameResponder == null) { - // Only set the responder here on first init - usernameField.setResponder(usernameResponder = username -> { - lastTyping = Util.getMillis(); - if (VALID_USERNAME.matcher(username).matches()) { - usernameUpdate = true; - friendProfile = null; - addFriendButton.active = false; - } else if (VALID_UUID.matcher(username).matches()) { - usernameUpdate = false; - friendProfile = new GameProfile(UUID.fromString(username), ""); - addFriendButton.active = true; - } else if (username.startsWith("o:")) { - usernameUpdate = false; - final String actualName = username.substring(2); - // TODO: Use createOfflinePlayerUUID when 1.19.2+ becomes the minimum, and createOfflineProfile in 1.20.4+ - friendProfile = new GameProfile(UUID.nameUUIDFromBytes(("OfflinePlayer:" + actualName).getBytes(StandardCharsets.UTF_8)), actualName); - addFriendButton.active = true; - } - }); - } } - @Override - public void resize(@NotNull Minecraft minecraft, int width, int height) { - final String oldUsername = usernameField.getValue(); - super.resize(minecraft, width, height); - usernameField.setValue(oldUsername); - // Make sure to set the responder *after* the value - usernameField.setResponder(usernameResponder); + private boolean showFriendAddersButton() { + return friendAdders.size() > 1; } @Override @@ -133,8 +175,8 @@ public void removed() { public boolean keyPressed(int keyCode, int scanCode, int modifiers) { if ( addFriendButton.active && - getFocused() == usernameField && - (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) + getFocused() == nameField && + (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) ) { addFriendButton.onPress(); return true; @@ -147,18 +189,9 @@ public void tick() { //#if MC < 1.20.2 //$$ usernameField.tick(); //#endif - if (Util.getMillis() - 300 > lastTyping && usernameUpdate) { - usernameUpdate = false; - final String username = usernameField.getValue(); - WorldHost.getMaybeAsync(WorldHost.getProfileCache(), username, p -> { - if (p.isPresent()) { - assert minecraft != null; - friendProfile = WorldHost.fetchProfile(minecraft.getMinecraftSessionService(), p.get()); - addFriendButton.active = true; - } else { - friendProfile = null; - } - }); + if (Util.getMillis() - 300 > lastTyping && delayedFriendUpdate) { + delayedFriendUpdate = false; + resolveFriend(nameField.getValue()); } } @@ -177,23 +210,20 @@ public void render( whRenderBackground(context, mouseX, mouseY, delta); drawCenteredString(context, font, title, width / 2, 20, 0xffffff); drawString(context, font, FRIEND_USERNAME_TEXT, width / 2 - 100, 50, labelColor); - usernameField.render(context, mouseX, mouseY, delta); super.render(context, mouseX, mouseY, delta); if (friendProfile != null) { assert minecraft != null; - final var skinTexture = WorldHost.getSkinLocationNow(friendProfile); RenderSystem.setShaderColor(1f, 1f, 1f, 1f); - RenderSystem.enableBlend(); + final int yShift = showFriendAddersButton() ? 24 : 0; //#if MC >= 1.19.4 final int addFriendY = addFriendButton.getY(); //#else //$$ final int addFriendY = addFriendButton.y; //#endif - final int size = addFriendY - 110; + final int size = addFriendY - 110 - yShift; final int x = width / 2 - size / 2; - blit(context, skinTexture, x, 98, size, size, 8, 8, 8, 8, 64, 64); - blit(context, skinTexture, x, 98, size, size, 40, 8, 8, 8, 64, 64); + friendProfile.iconRenderer().draw(context, x, 98 + yShift, size, size); RenderSystem.disableBlend(); } } 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 ecbc7cf..136cef9 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 @@ -1,13 +1,11 @@ package io.github.gaming32.worldhost.gui.screen; -import com.mojang.authlib.GameProfile; import com.mojang.blaze3d.systems.RenderSystem; import io.github.gaming32.worldhost.WorldHost; import io.github.gaming32.worldhost.WorldHostComponents; import io.github.gaming32.worldhost.plugin.FriendListFriend; import io.github.gaming32.worldhost.plugin.InfoTextsCategory; import io.github.gaming32.worldhost.plugin.ProfileInfo; -import io.github.gaming32.worldhost.plugin.vanilla.WorldHostFriendListFriend; import io.github.gaming32.worldhost.versions.Components; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; @@ -33,6 +31,11 @@ public class FriendsScreen extends ScreenWithInfoTexts { private Button removeButton; private FriendsList list; + private final Runnable refresher = () -> { + assert minecraft != null; + minecraft.execute(() -> list.reloadEntries()); + }; + public FriendsScreen(Screen parent) { super(WorldHostComponents.FRIENDS, InfoTextsCategory.FRIENDS_SCREEN); this.parent = parent; @@ -56,12 +59,10 @@ protected void init() { addRenderableWidget( button(ADD_FRIEND_TEXT, button -> { assert minecraft != null; - minecraft.setScreen(new AddFriendScreen(this, ADD_FRIEND_TEXT, null, profile -> { - addFriendAndUpdate(profile); - if (WorldHost.protoClient != null) { - WorldHost.protoClient.friendRequest(profile.getId()); - } - })); + minecraft.setScreen(new AddFriendScreen( + this, ADD_FRIEND_TEXT, null, + friend -> friend.addFriend(true, refresher) + )); }).width(152) .pos(width / 2 - 154, height - 54) .tooltip(Components.translatable("world-host.add_friend.tooltip")) @@ -71,7 +72,10 @@ protected void init() { addRenderableWidget( button(ADD_SILENTLY_TEXT, button -> { assert minecraft != null; - minecraft.setScreen(new AddFriendScreen(this, ADD_SILENTLY_TEXT, null, this::addFriendAndUpdate)); + minecraft.setScreen(new AddFriendScreen( + this, ADD_SILENTLY_TEXT, null, + friend -> friend.addFriend(false, refresher) + )); }).width(152) .pos(width / 2 - 154, height - 30) .tooltip(Components.translatable("world-host.friends.add_silently.tooltip")) @@ -107,15 +111,6 @@ public void onClose() { minecraft.setScreen(parent); } - private void addFriendAndUpdate(GameProfile profile) { - addFriend(profile); - list.addEntry(new FriendsEntry(new WorldHostFriendListFriend(profile))); - } - - public static void addFriend(GameProfile profile) { - WorldHost.addFriends(profile.getId()); - } - @Override public void render( @NotNull @@ -183,10 +178,10 @@ public FriendsEntry(FriendListFriend friend) { friend.profileInfo() .thenAccept(ready -> profile = ready) .exceptionally(t -> { - WorldHost.LOGGER.error("Failed to request profile skin for {}", friend, t); + WorldHost.LOGGER.error("Failed to request profile info for {}", friend, t); return null; }); - } + } @NotNull @Override @@ -224,7 +219,7 @@ public void maybeRemove() { minecraft.setScreen(new ConfirmScreen( yes -> { if (yes) { - friend.removeFriend(() -> minecraft.execute(() -> FriendsScreen.this.list.reloadEntries())); + friend.removeFriend(refresher); } minecraft.setScreen(FriendsScreen.this); }, diff --git a/src/main/java/io/github/gaming32/worldhost/gui/screen/OnlineFriendsScreen.java b/src/main/java/io/github/gaming32/worldhost/gui/screen/OnlineFriendsScreen.java index fd5a3cf..ec258ee 100644 --- a/src/main/java/io/github/gaming32/worldhost/gui/screen/OnlineFriendsScreen.java +++ b/src/main/java/io/github/gaming32/worldhost/gui/screen/OnlineFriendsScreen.java @@ -359,7 +359,7 @@ public OnlineFriendsListEntry(OnlineFriend friend) { friend.profileInfo() .thenAccept(ready -> profile = ready) .exceptionally(t -> { - WorldHost.LOGGER.error("Failed to request profile skin for {}", friend, t); + WorldHost.LOGGER.error("Failed to request profile info for {}", friend, t); return null; }); iconTextureId = ResourceLocations.worldHost("servers/" + friend.uuid() + "/icon"); diff --git a/src/main/java/io/github/gaming32/worldhost/gui/widget/FriendAdderSelectorButton.java b/src/main/java/io/github/gaming32/worldhost/gui/widget/FriendAdderSelectorButton.java new file mode 100644 index 0000000..41e183a --- /dev/null +++ b/src/main/java/io/github/gaming32/worldhost/gui/widget/FriendAdderSelectorButton.java @@ -0,0 +1,24 @@ +package io.github.gaming32.worldhost.gui.widget; + +import io.github.gaming32.worldhost.plugin.FriendAdder; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public class FriendAdderSelectorButton extends CustomCycleButton { + public FriendAdderSelectorButton( + int x, int y, int width, int height, + @Nullable Component title, + Consumer onUpdate, + FriendAdder[] values + ) { + super(x, y, width, height, title, onUpdate, values); + } + + @Override + public @NotNull Component getValueMessage() { + return getValue().label(); + } +} diff --git a/src/main/java/io/github/gaming32/worldhost/plugin/FriendAdder.java b/src/main/java/io/github/gaming32/worldhost/plugin/FriendAdder.java new file mode 100644 index 0000000..162e8b2 --- /dev/null +++ b/src/main/java/io/github/gaming32/worldhost/plugin/FriendAdder.java @@ -0,0 +1,14 @@ +package io.github.gaming32.worldhost.plugin; + +import net.minecraft.network.chat.Component; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public interface FriendAdder { + Component label(); + + CompletableFuture> resolveFriend(String name); + + boolean rateLimit(String name); +} 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 9671307..31cb557 100644 --- a/src/main/java/io/github/gaming32/worldhost/plugin/FriendListFriend.java +++ b/src/main/java/io/github/gaming32/worldhost/plugin/FriendListFriend.java @@ -5,6 +5,12 @@ import java.util.Optional; public interface FriendListFriend extends Profilable { + /** + * Handles adding a friend. This should only be overridden if you implement a {@link FriendAdder}. + */ + default void addFriend(boolean notify, Runnable refresher) { + } + void removeFriend(Runnable refresher); default Optional tag() { diff --git a/src/main/java/io/github/gaming32/worldhost/plugin/WorldHostPlugin.java b/src/main/java/io/github/gaming32/worldhost/plugin/WorldHostPlugin.java index 70fd413..0e7784d 100644 --- a/src/main/java/io/github/gaming32/worldhost/plugin/WorldHostPlugin.java +++ b/src/main/java/io/github/gaming32/worldhost/plugin/WorldHostPlugin.java @@ -8,6 +8,7 @@ import java.lang.annotation.Target; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; /** @@ -36,6 +37,10 @@ default void refreshOnlineFriends() { default void listFriends(Consumer friendConsumer) { } + default Optional friendAdder() { + return Optional.empty(); + } + /** * World Host plugin entrypoint class marker. This is used on Forge and NeoForge. On Fabric, use the * entrypoint system. diff --git a/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/GameProfileBasedProfilable.java b/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/GameProfileBasedProfilable.java index d7fd789..83713f2 100644 --- a/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/GameProfileBasedProfilable.java +++ b/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/GameProfileBasedProfilable.java @@ -19,6 +19,9 @@ default ProfileInfo fallbackProfileInfo() { @Override default CompletableFuture profileInfo() { + if (defaultProfile().getId().version() != 4) { + return CompletableFuture.completedFuture(fallbackProfileInfo()); + } return CompletableFuture.supplyAsync( () -> WorldHost.fetchProfile(Minecraft.getInstance().getMinecraftSessionService(), defaultProfile()), //#if MC >= 1.20.4 diff --git a/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/GameProfileProfileInfo.java b/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/GameProfileProfileInfo.java index 0d8f316..bfb814b 100644 --- a/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/GameProfileProfileInfo.java +++ b/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/GameProfileProfileInfo.java @@ -7,7 +7,6 @@ public final class GameProfileProfileInfo implements ProfileInfo { private final GameProfile profile; - private IconRenderer iconRenderer; public GameProfileProfileInfo(GameProfile profile) { this.profile = profile; @@ -20,9 +19,6 @@ public String name() { @Override public IconRenderer iconRenderer() { - if (iconRenderer == null) { - iconRenderer = IconRenderer.createSkinIconRenderer(WorldHost.getSkinLocationNow(profile)); - } - return iconRenderer; + return IconRenderer.createSkinIconRenderer(WorldHost.getSkinLocationNow(profile)); } } diff --git a/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/VanillaWorldHostPlugin.java b/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/VanillaWorldHostPlugin.java index 3cfc8f8..ec6d097 100644 --- a/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/VanillaWorldHostPlugin.java +++ b/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/VanillaWorldHostPlugin.java @@ -1,15 +1,19 @@ package io.github.gaming32.worldhost.plugin.vanilla; import io.github.gaming32.worldhost.WorldHost; +import io.github.gaming32.worldhost.plugin.FriendAdder; import io.github.gaming32.worldhost.plugin.FriendListFriend; import io.github.gaming32.worldhost.plugin.OnlineFriend; import io.github.gaming32.worldhost.plugin.WorldHostPlugin; import java.util.Collection; +import java.util.Optional; import java.util.function.Consumer; @WorldHostPlugin.Entrypoint public final class VanillaWorldHostPlugin implements WorldHostPlugin { + private final FriendAdder friendAdder = new WorldHostFriendAdder(); + @Override public int priority() { return 1_000_000_000; @@ -38,4 +42,9 @@ public void listFriends(Consumer friendConsumer) { .map(WorldHostFriendListFriend::new) .forEach(friendConsumer); } + + @Override + public Optional friendAdder() { + return Optional.of(friendAdder); + } } diff --git a/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/WorldHostFriendAdder.java b/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/WorldHostFriendAdder.java new file mode 100644 index 0000000..4799637 --- /dev/null +++ b/src/main/java/io/github/gaming32/worldhost/plugin/vanilla/WorldHostFriendAdder.java @@ -0,0 +1,53 @@ +package io.github.gaming32.worldhost.plugin.vanilla; + +import com.mojang.authlib.GameProfile; +import io.github.gaming32.worldhost.WorldHost; +import io.github.gaming32.worldhost.plugin.FriendAdder; +import io.github.gaming32.worldhost.plugin.FriendListFriend; +import net.minecraft.network.chat.Component; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +public class WorldHostFriendAdder implements FriendAdder { + public static final Pattern VALID_USERNAME = Pattern.compile("^[a-zA-Z0-9_]{1,16}$"); + public static final Pattern VALID_UUID = Pattern.compile("^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$"); + + @Override + public Component label() { + return Component.literal("World Host"); + } + + @Override + public CompletableFuture> resolveFriend(String name) { + if (VALID_USERNAME.matcher(name).matches()) { + final CompletableFuture> result = new CompletableFuture<>(); + WorldHost.getMaybeAsync( + WorldHost.getProfileCache(), name, + profile -> result.complete(profile.map(WorldHostFriendListFriend::new)) + ); + return result; + } + if (VALID_UUID.matcher(name).matches()) { + return CompletableFuture.completedFuture(Optional.of( + new WorldHostFriendListFriend(UUID.fromString(name)) + )); + } + if (name.startsWith("o:")) { + final String actualName = name.substring(2); + // TODO: Use createOfflinePlayerUUID when 1.19.2+ becomes the minimum, and createOfflineProfile in 1.20.4+ + return CompletableFuture.completedFuture(Optional.of(new WorldHostFriendListFriend(new GameProfile( + UUID.nameUUIDFromBytes(("OfflinePlayer:" + actualName).getBytes(StandardCharsets.UTF_8)), actualName + )))); + } + return CompletableFuture.completedFuture(Optional.empty()); + } + + @Override + public boolean rateLimit(String name) { + return !VALID_UUID.matcher(name).matches() && !name.startsWith("o:"); + } +} 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 3eb7766..3fa08cf 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 @@ -19,6 +19,15 @@ public WorldHostFriendListFriend(GameProfile profile) { this(profile.getId(), profile); } + @Override + public void addFriend(boolean notify, Runnable refresher) { + WorldHost.addFriends(uuid); + refresher.run(); + if (notify && WorldHost.protoClient != null) { + WorldHost.protoClient.friendRequest(uuid); + } + } + @Override public void removeFriend(Runnable refresher) { WorldHost.CONFIG.getFriends().remove(uuid); diff --git a/src/main/java/io/github/gaming32/worldhost/protocol/WorldHostS2CMessage.java b/src/main/java/io/github/gaming32/worldhost/protocol/WorldHostS2CMessage.java index 11ebe5d..cfbddd3 100644 --- a/src/main/java/io/github/gaming32/worldhost/protocol/WorldHostS2CMessage.java +++ b/src/main/java/io/github/gaming32/worldhost/protocol/WorldHostS2CMessage.java @@ -8,6 +8,7 @@ import io.github.gaming32.worldhost.gui.screen.FriendsScreen; import io.github.gaming32.worldhost.gui.screen.JoiningWorldHostScreen; import io.github.gaming32.worldhost.gui.screen.OnlineFriendsScreen; +import io.github.gaming32.worldhost.plugin.vanilla.WorldHostFriendListFriend; import io.github.gaming32.worldhost.plugin.vanilla.WorldHostOnlineFriend; import io.github.gaming32.worldhost.protocol.proxy.ProxyProtocolClient; import io.github.gaming32.worldhost.toast.WHToast; @@ -118,8 +119,8 @@ public void handle(ProtocolClient client) { minecraft.setScreen(new AddFriendScreen( minecraft.screen, FriendsScreen.ADD_FRIEND_TEXT, - fromUser, - FriendsScreen::addFriend + new WorldHostFriendListFriend(fromUser), + friend -> friend.addFriend(true, () -> {}) )); } ); 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 9961d22..0ada0d7 100644 --- a/src/main/resources/assets/world-host/lang/en_us.json +++ b/src/main/resources/assets/world-host/lang/en_us.json @@ -36,6 +36,7 @@ "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.", "world-host.add_friend.enter_username": "Friend Username", + "world-host.add_friend.friend_adder": "Friend Type", "world-host.wh_connect.close_failed": "Failed to close connection to World Host server.", "world-host.wh_connect.not_available": "Failed to get player UUID. Unable to use World Host.", "world-host.wh_connect.auth_failed": "Failed to authenticate user!",