diff --git a/.gitignore b/.gitignore index 3c37caf3..bff467bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # User-specific stuff .idea/ +.vscode/ *.iml *.ipr diff --git a/README.md b/README.md index a2c66b20..cf5675ec 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This mod works in multiplayer, but may be considered cheating on some servers, s |----------------|-------------------------------------------------------------------------------------------------------------------------|--------------| | Toggle Freecam | Enables/disables Freecam | `F4` | | Config GUI | Opens the settings screen. | `Unbound` | +| Goto GUI | Opens a "goto" screen, which allows jumping Freecam to any player within range. | `G` | | Control Player | Transfers control back to your player, but retains your current perspective (Can only be used while Freecam is active.) | `Unbound` | | Reset Tripod | Resets a tripod\* camera when pressed in combination with any of the hotbar keys | `Unbound` | @@ -53,10 +54,11 @@ This mod works in multiplayer, but may be considered cheating on some servers, s ## Notification Options -| Name | Description | Default Value | -|-----------------------|---------------------------------------------------------|---------------| -| Freecam Notifications | Notifies you when entering/exiting freecam. | `true` | -| Tripod Notifications | Notifies you when entering/exiting tripod cameras.
| `true` | +| Name | Description | Default Value | +|-----------------------|-----------------------------------------------------------|---------------| +| Freecam Notifications | Notifies you when entering/exiting freecam. | `true` | +| Tripod Notifications | Notifies you when entering/exiting tripod cameras. | `true` | +| Goto Notifications | Notifies you when using "Goto" to teleport the camera. | `true` | ## Requirements diff --git a/common/src/main/java/net/xolt/freecam/Freecam.java b/common/src/main/java/net/xolt/freecam/Freecam.java index a8779b3e..dbefb6e2 100644 --- a/common/src/main/java/net/xolt/freecam/Freecam.java +++ b/common/src/main/java/net/xolt/freecam/Freecam.java @@ -1,21 +1,17 @@ package net.xolt.freecam; -import me.shedaniel.autoconfig.AutoConfig; import net.minecraft.client.CameraType; import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; import net.minecraft.client.player.Input; import net.minecraft.client.player.KeyboardInput; import net.minecraft.network.chat.Component; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.level.ChunkPos; import net.xolt.freecam.config.ModConfig; +import net.xolt.freecam.gui.go.GotoScreen; import net.xolt.freecam.tripod.TripodRegistry; import net.xolt.freecam.tripod.TripodSlot; import net.xolt.freecam.util.FreeCamera; import net.xolt.freecam.util.FreecamPosition; -import net.xolt.freecam.variant.api.BuildVariant; -import org.jetbrains.annotations.Nullable; import static net.xolt.freecam.config.ModBindings.*; @@ -50,7 +46,7 @@ public static void preTick(Minecraft mc) { mc.player.input = input; } - mc.gameRenderer.setRenderHand(ModConfig.INSTANCE.visual.showHand); + mc.gameRenderer.setRenderHand(ModConfig.get().visual.showHand); } disableNextTick = false; } @@ -94,8 +90,12 @@ else if (KEY_TOGGLE.consumeClick() || toggleKeyHeldTicks > 0) { switchControls(); } + while (KEY_GOTO_GUI.consumeClick()) { + mc.setScreen(new GotoScreen()); + } + while (KEY_CONFIG_GUI.consumeClick()) { - mc.setScreen(AutoConfig.getConfigScreen(ModConfig.class, mc.screen).get()); + mc.setScreen(ModConfig.getScreen(mc.screen)); } } @@ -123,6 +123,29 @@ public static void toggle() { } } + public static void gotoPosition(FreecamPosition position) { + boolean wasEnabled = isEnabled(); + if (!wasEnabled) { + // FIXME this will move the camera to the default position and show a notification, + // even though we'll move the camera and show our own notification immediately after. + toggle(); + } + + freeCamera.applyPosition(position); + + if (ModConfig.get().notification.notifyGoto) { + Component notification; + if (!wasEnabled) { + notification = Component.translatable("msg.freecam.gotoPosition.enable", position.getName()); + } else if (tripodEnabled) { + notification = Component.translatable("msg.freecam.gotoPosition.moveTripod", activeTripod, position.getName()); + } else { + notification = Component.translatable("msg.freecam.gotoPosition.move", position.getName()); + } + MC.player.displayClientMessage(notification, true); + } + } + private static void toggleTripod(TripodSlot tripod) { if (tripod == TripodSlot.NONE) { return; @@ -165,40 +188,30 @@ public static void switchControls() { private static void onEnableTripod(TripodSlot tripod) { onEnable(); - FreecamPosition position = tripods.get(tripod); - boolean chunkLoaded = false; - if (position != null) { - ChunkPos chunkPos = position.getChunkPos(); - chunkLoaded = MC.level.getChunkSource().hasChunk(chunkPos.x, chunkPos.z); - } - - if (!chunkLoaded) { + FreecamPosition position = FreecamPosition.of(tripod); + while (!position.isInRange()) { resetCamera(tripod); - position = null; + position = FreecamPosition.of(tripod); } freeCamera = new FreeCamera(-420 - tripod.ordinal()); - if (position == null) { - moveToPlayer(); - } else { - moveToPosition(position); - } + freeCamera.applyPosition(position); freeCamera.spawn(); MC.setCameraEntity(freeCamera); activeTripod = tripod; - if (ModConfig.INSTANCE.notification.notifyTripod) { + if (ModConfig.get().notification.notifyTripod) { MC.player.displayClientMessage(Component.translatable("msg.freecam.openTripod", tripod), true); } } private static void onDisableTripod() { - tripods.put(activeTripod, new FreecamPosition(freeCamera)); + tripods.put(activeTripod, FreecamPosition.of(freeCamera)); onDisable(); if (MC.player != null) { - if (ModConfig.INSTANCE.notification.notifyTripod) { + if (ModConfig.get().notification.notifyTripod) { MC.player.displayClientMessage(Component.translatable("msg.freecam.closeTripod", activeTripod), true); } } @@ -208,11 +221,11 @@ private static void onDisableTripod() { private static void onEnableFreecam() { onEnable(); freeCamera = new FreeCamera(-420); - moveToPlayer(); + freeCamera.applyPosition(FreecamPosition.defaultPosition()); freeCamera.spawn(); MC.setCameraEntity(freeCamera); - if (ModConfig.INSTANCE.notification.notifyFreecam) { + if (ModConfig.get().notification.notifyFreecam) { MC.player.displayClientMessage(Component.translatable("msg.freecam.enable"), true); } } @@ -221,7 +234,7 @@ private static void onDisableFreecam() { onDisable(); if (MC.player != null) { - if (ModConfig.INSTANCE.notification.notifyFreecam) { + if (ModConfig.get().notification.notifyFreecam) { MC.player.displayClientMessage(Component.translatable("msg.freecam.disable"), true); } } @@ -229,7 +242,7 @@ private static void onDisableFreecam() { private static void onEnable() { MC.smartCull = false; - MC.gameRenderer.setRenderHand(ModConfig.INSTANCE.visual.showHand); + MC.gameRenderer.setRenderHand(ModConfig.get().visual.showHand); rememberedF5 = MC.options.getCameraType(); if (MC.gameRenderer.getMainCamera().isDetached()) { @@ -259,49 +272,16 @@ private static void onDisabled() { private static void resetCamera(TripodSlot tripod) { if (tripodEnabled && activeTripod != TripodSlot.NONE && activeTripod == tripod && freeCamera != null) { - moveToPlayer(); + freeCamera.applyPosition(FreecamPosition.defaultPosition()); } else { tripods.put(tripod, null); } - if (ModConfig.INSTANCE.notification.notifyTripod) { + if (ModConfig.get().notification.notifyTripod) { MC.player.displayClientMessage(Component.translatable("msg.freecam.tripodReset", tripod), true); } } - public static void moveToEntity(@Nullable Entity entity) { - if (freeCamera == null) { - return; - } - if (entity == null) { - moveToPlayer(); - return; - } - freeCamera.copyPosition(entity); - } - - public static void moveToPosition(@Nullable FreecamPosition position) { - if (freeCamera == null) { - return; - } - if (position == null) { - moveToPlayer(); - return; - } - freeCamera.applyPosition(position); - } - - public static void moveToPlayer() { - if (freeCamera == null) { - return; - } - freeCamera.copyPosition(MC.player); - freeCamera.applyPerspective( - ModConfig.INSTANCE.visual.perspective, - ModConfig.INSTANCE.collision.alwaysCheck || !(ModConfig.INSTANCE.collision.ignoreAll && BuildVariant.getInstance().cheatsPermitted()) - ); - } - public static FreeCamera getFreeCamera() { return freeCamera; } @@ -317,4 +297,8 @@ public static boolean isEnabled() { public static boolean isPlayerControlEnabled() { return playerControlEnabled; } + + public static FreecamPosition getTripod(TripodSlot tripod) { + return tripods.get(tripod); + } } diff --git a/common/src/main/java/net/xolt/freecam/config/ModBindings.java b/common/src/main/java/net/xolt/freecam/config/ModBindings.java index c7cafba4..af0ce908 100644 --- a/common/src/main/java/net/xolt/freecam/config/ModBindings.java +++ b/common/src/main/java/net/xolt/freecam/config/ModBindings.java @@ -12,14 +12,14 @@ import java.util.Spliterator; import java.util.function.Consumer; -import static org.lwjgl.glfw.GLFW.GLFW_KEY_F4; -import static org.lwjgl.glfw.GLFW.GLFW_KEY_UNKNOWN; +import static org.lwjgl.glfw.GLFW.*; public enum ModBindings { KEY_TOGGLE("toggle", GLFW_KEY_F4), KEY_PLAYER_CONTROL("playerControl"), KEY_TRIPOD_RESET("tripodReset"), + KEY_GOTO_GUI("goto", GLFW_KEY_G), KEY_CONFIG_GUI("configGui"); private final Supplier lazyMapping; @@ -57,6 +57,14 @@ public boolean consumeClick() { return get().consumeClick(); } + /** + * @return the result of calling {@link KeyMapping#matches(int, int)} on the represented {@link KeyMapping}. + * @see KeyMapping#matches(int, int) + */ + public boolean matches(int keyCode, int scanCode) { + return get().matches(keyCode, scanCode); + } + /** * Reset whether the key was pressed. *

diff --git a/common/src/main/java/net/xolt/freecam/config/ModConfig.java b/common/src/main/java/net/xolt/freecam/config/ModConfig.java index 8b00c2d8..2b84cca8 100644 --- a/common/src/main/java/net/xolt/freecam/config/ModConfig.java +++ b/common/src/main/java/net/xolt/freecam/config/ModConfig.java @@ -2,23 +2,37 @@ import me.shedaniel.autoconfig.AutoConfig; import me.shedaniel.autoconfig.ConfigData; +import me.shedaniel.autoconfig.ConfigHolder; import me.shedaniel.autoconfig.annotation.Config; import me.shedaniel.autoconfig.annotation.ConfigEntry; import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.EnumHandler.EnumDisplayOption; import me.shedaniel.autoconfig.serializer.JanksonConfigSerializer; import me.shedaniel.clothconfig2.gui.entries.SelectionListEntry; +import net.minecraft.client.gui.screens.Screen; +import net.xolt.freecam.gui.go.tabs.GotoScreenTab; import net.xolt.freecam.variant.api.BuildVariant; +import org.jetbrains.annotations.NotNull; @Config(name = "freecam") public class ModConfig implements ConfigData { - @ConfigEntry.Gui.Excluded - public static ModConfig INSTANCE; + private static ConfigHolder CONFIG_HOLDER; public static void init() { - AutoConfig.register(ModConfig.class, JanksonConfigSerializer::new); + CONFIG_HOLDER = AutoConfig.register(ModConfig.class, JanksonConfigSerializer::new); ConfigExtensions.init(AutoConfig.getGuiRegistry(ModConfig.class)); - INSTANCE = AutoConfig.getConfigHolder(ModConfig.class).getConfig(); + } + + public static ModConfig get() { + return CONFIG_HOLDER.get(); + } + + public static void save() { + CONFIG_HOLDER.save(); + } + + public static Screen getScreen(Screen parent) { + return AutoConfig.getConfigScreen(ModConfig.class, parent).get(); } @ConfigEntry.Gui.Tooltip @@ -104,52 +118,65 @@ public static class NotificationConfig { @ConfigEntry.Gui.Tooltip public boolean notifyTripod = true; + + @ConfigEntry.Gui.Tooltip + public boolean notifyGoto = true; + } + + @ConfigEntry.Gui.Excluded + public Hidden hidden = new Hidden(); + public static class Hidden { + public GotoScreenTab currentTab = GotoScreenTab.PLAYER; + public Perspective gotoPlayerPerspective = Perspective.THIRD_PERSON; } public enum FlightMode implements SelectionListEntry.Translatable { - CREATIVE("text.autoconfig.freecam.option.movement.flightMode.creative"), - DEFAULT("text.autoconfig.freecam.option.movement.flightMode.default"); + CREATIVE("creative"), + DEFAULT("default"); - private final String name; + private final String key; FlightMode(String name) { - this.name = name; + this.key = "text.autoconfig.freecam.option.movement.flightMode." + name; } - public String getKey() { - return name; + @Override + public @NotNull String getKey() { + return key; } } public enum InteractionMode implements SelectionListEntry.Translatable { - CAMERA("text.autoconfig.freecam.option.utility.interactionMode.camera"), - PLAYER("text.autoconfig.freecam.option.utility.interactionMode.player"); + CAMERA("camera"), + PLAYER("player"); - private final String name; + private final String key; InteractionMode(String name) { - this.name = name; + this.key = "text.autoconfig.freecam.option.utility.interactionMode." + name; } - public String getKey() { - return name; + @Override + public @NotNull String getKey() { + return key; } } public enum Perspective implements SelectionListEntry.Translatable { - FIRST_PERSON("text.autoconfig.freecam.option.visual.perspective.firstPerson"), - THIRD_PERSON("text.autoconfig.freecam.option.visual.perspective.thirdPerson"), - THIRD_PERSON_MIRROR("text.autoconfig.freecam.option.visual.perspective.thirdPersonMirror"), - INSIDE("text.autoconfig.freecam.option.visual.perspective.inside"); + FIRST_PERSON("firstPerson"), + THIRD_PERSON("thirdPerson"), + THIRD_PERSON_MIRROR("thirdPersonMirror"), + INSIDE("inside"); - private final String name; + private final String key; Perspective(String name) { - this.name = name; + this.key = "text.autoconfig.freecam.option.visual.perspective." + name; } - public String getKey() { - return name; + @Override + public @NotNull String getKey() { + return key; } } } diff --git a/common/src/main/java/net/xolt/freecam/gui/go/GotoScreen.java b/common/src/main/java/net/xolt/freecam/gui/go/GotoScreen.java new file mode 100644 index 00000000..ea044cdd --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/GotoScreen.java @@ -0,0 +1,146 @@ +package net.xolt.freecam.gui.go; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.navigation.CommonInputs; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FastColor; +import net.xolt.freecam.Freecam; +import net.xolt.freecam.config.ModConfig; +import net.xolt.freecam.gui.textures.ScaledTexture; +import net.xolt.freecam.gui.go.tabs.GotoScreenTab; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +import static net.xolt.freecam.config.ModBindings.KEY_GOTO_GUI; + +public class GotoScreen extends Screen { + + private static final ScaledTexture BACKGROUND = ScaledTexture.get(new ResourceLocation(Freecam.MOD_ID, "textures/gui/goto_background.png")); + public static final int GRAY_COLOR = FastColor.ARGB32.color(255, 74, 74, 74); + public static final int WHITE_COLOR = FastColor.ARGB32.color(255, 255, 255, 255); + public static final int MIN_GUI_HEIGHT = 80; + private static final int GUI_TOP = 50; + private static final int GUI_WIDTH = 236; + private static final int LIST_TOP = GUI_TOP + 8; + + private TargetsPane targets; + private Button buttonBack; + private Button buttonJump; + private boolean initialized; + + public GotoScreen() { + super(Component.translatable("gui.freecam.goto.title")); + } + + @Override + protected void init() { + super.init(); + + if (!initialized) { + targets = new TargetsPane(LIST_TOP, width, height, ModConfig.get().hidden.currentTab); + + buttonJump = Button.builder(Component.translatable("gui.freecam.goto.button.go"), button -> targets.gotoTarget()) + .tooltip(Tooltip.create(Component.translatable("gui.freecam.goto.button.go.@Tooltip"))) + .width(48) + .build(); + + buttonBack = Button.builder(CommonComponents.GUI_BACK, button -> onClose()).width(48).build(); + } + + targets.setTab(ModConfig.get().hidden.currentTab); + targets.setSize(width, getListHeight()); + + int innerWidth = GUI_WIDTH - 10; + int innerX = (width - innerWidth) / 2; + FrameLayout positioner = new FrameLayout(innerX, LIST_TOP + getListHeight() + 3, innerWidth, 0); + positioner.defaultChildLayoutSetting() + .alignVerticallyBottom() + .alignHorizontallyRight(); + LinearLayout layout = positioner.addChild(LinearLayout.horizontal()); + layout.defaultCellSetting() + .alignVerticallyBottom() + .paddingHorizontal(2); + + layout.addChild(buttonBack); + ModConfig.get().hidden.currentTab.extraButtons().forEach(layout::addChild); + layout.addChild(buttonJump); + + positioner.arrangeElements(); + positioner.visitWidgets(this::addRenderableWidget); + + addRenderableWidget(targets); + setInitialFocus(targets); + + initialized = true; + } + + @Override + public void renderBackground(GuiGraphics gfx, int mouseX, int mouseY, float delta) { + super.renderBackground(gfx, mouseX, mouseY, delta); + int left = (width - GUI_WIDTH) / 2; + BACKGROUND.draw(gfx, left, GUI_TOP, GUI_WIDTH, getGuiHeight()); + } + + @Override + public void tick() { + super.tick(); + if (initialized) { + targets.tick(); + buttonJump.active = targets.hasTarget(); + } + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (targets.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + if (CommonInputs.selected(keyCode)) { + targets.gotoTarget(); + return true; + } + if (KEY_GOTO_GUI.matches(keyCode, scanCode) && !targets.isTyping()) { + onClose(); + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public void setFocused(@Nullable GuiEventListener focused) { + if (Objects.equals(getFocused(), focused)) { + return; + } + super.setFocused(focused); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + public void setTab(GotoScreenTab tab) { + ModConfig.get().hidden.currentTab = tab; + ModConfig.save(); + init(); + } + + // GUI height + private int getGuiHeight() { + return Math.max(MIN_GUI_HEIGHT, height - (GUI_TOP * 2)); + } + + // List window height + private int getListHeight() { + return getGuiHeight() - 29 - 8; + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/go/PlayerListEntry.java b/common/src/main/java/net/xolt/freecam/gui/go/PlayerListEntry.java new file mode 100644 index 00000000..40056e86 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/PlayerListEntry.java @@ -0,0 +1,96 @@ +package net.xolt.freecam.gui.go; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.PlayerFaceRenderer; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.client.resources.PlayerSkin; +import net.minecraft.network.chat.Component; +import net.xolt.freecam.Freecam; +import net.xolt.freecam.config.ModConfig; +import net.xolt.freecam.util.FreecamPosition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.UUID; +import java.util.function.Supplier; + +import static net.xolt.freecam.Freecam.MC; +import static net.xolt.freecam.gui.go.GotoScreen.WHITE_COLOR; + +public class PlayerListEntry extends TargetListEntry { + + private final Component displayText; + private final AbstractClientPlayer player; + private final @Nullable Supplier skinSupplier; + + public PlayerListEntry(TargetListWidget widget, AbstractClientPlayer player) { + super(MC, widget); + this.player = player; + + var text = player.getName().plainCopy(); + if (Objects.equals(player, MC.player)) { + text = Component.translatable("gui.freecam.goto.entry.player.you", text); + } + this.displayText = text; + + var playerInfo = MC.player.connection.getPlayerInfo(player.getUUID()); + this.skinSupplier = playerInfo == null ? null : playerInfo::getSkin; + } + + @Override + public void render(GuiGraphics gfx, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + super.render(gfx, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta); + + int padding = 4; + int skinSize = 24; + boolean hasSkin = skinSupplier != null; + + if (hasSkin) { + int skinX = x + padding; + int skinY = y + (entryHeight - skinSize) / 2; + PlayerFaceRenderer.draw(gfx, skinSupplier.get(), skinX, skinY, skinSize); + } + + int textX = x + padding + (hasSkin ? skinSize + padding : 0); + int textY = y + (entryHeight - mc.font.lineHeight) / 2; + gfx.drawString(mc.font, displayText, textX, textY, WHITE_COLOR, false); + } + + @Override + public int compareTo(@NotNull TargetListEntry entry) { + // Sort before non-player entries + if (!(entry instanceof PlayerListEntry playerEntry)) { + return -1; + } + + // Sort mc.player before other players + if (Objects.equals(getUUID(), playerEntry.getUUID())) { + return 0; + } + if (Objects.equals(getUUID(), mc.player.getUUID())) { + return -1; + } + if (Objects.equals(playerEntry.getUUID(), mc.player.getUUID())) { + return 1; + } + + // Fallback to default comparison + return super.compareTo(entry); + } + + @Override + public String getName() { + return player.getScoreboardName(); + } + + @Override + public void go() { + mc.setScreen(null); + Freecam.gotoPosition(FreecamPosition.of(player, ModConfig.get().hidden.gotoPlayerPerspective)); + } + + public UUID getUUID() { + return player.getUUID(); + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/go/TargetListEntry.java b/common/src/main/java/net/xolt/freecam/gui/go/TargetListEntry.java new file mode 100644 index 00000000..54ea572f --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/TargetListEntry.java @@ -0,0 +1,68 @@ +package net.xolt.freecam.gui.go; + +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.ObjectSelectionList; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.xolt.freecam.util.FreeCamera; +import org.jetbrains.annotations.NotNull; + +import java.util.Locale; + +import static net.xolt.freecam.gui.go.GotoScreen.GRAY_COLOR; + +public abstract class TargetListEntry extends ObjectSelectionList.Entry implements Comparable { + + private static final long DOUBLE_CLICK_MILLIS = 250; + + protected final Minecraft mc; + private final TargetListWidget parent; + private long lastClicked; + + protected TargetListEntry(Minecraft mc, TargetListWidget parent) { + this.mc = mc; + this.parent = parent; + } + + @Override + public void render(GuiGraphics gfx, int index, int y, int x, int fullEntryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + // We are passed a reduced entryHeight but the full entryWidth... + int entryWidth = fullEntryWidth - 4; + gfx.fill(x, y, x + entryWidth, y + entryHeight, GRAY_COLOR); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + long time = Util.getMillis(); + parent.setSelected(this); + parent.setFocused(this); + if (time - lastClicked < DOUBLE_CLICK_MILLIS) { + go(); + } + lastClicked = time; + return true; + } + + @Override + public @NotNull Component getNarration() { + return Component.literal(getName()); + } + + @Override + public int compareTo(@NotNull TargetListEntry entry) { + return getName().compareTo(entry.getName()); + } + + public boolean matchesSearch(String string) { + return getName().toLowerCase(Locale.ROOT).contains(string); + } + + public abstract String getName(); + + /** + * Close the current {@link Screen} and move {@link FreeCamera} to this target. + */ + public abstract void go(); +} diff --git a/common/src/main/java/net/xolt/freecam/gui/go/TargetListWidget.java b/common/src/main/java/net/xolt/freecam/gui/go/TargetListWidget.java new file mode 100644 index 00000000..9c70b248 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/TargetListWidget.java @@ -0,0 +1,128 @@ +package net.xolt.freecam.gui.go; + +import net.minecraft.client.gui.components.ObjectSelectionList; +import net.minecraft.client.gui.components.events.GuiEventListener; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.IntPredicate; + +import static net.xolt.freecam.Freecam.MC; + +/** + * A GUI widget displaying a list of {@link TargetListEntry}s. + */ +public class TargetListWidget extends ObjectSelectionList { + + public TargetListWidget(int top, int width, int height, int itemHeight) { + super(MC, width, height, top, itemHeight); + this.setRenderBackground(false); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + TargetListEntry entry = getSelected(); + return entry != null && entry.keyPressed(keyCode, scanCode, modifiers) || super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public void setFocused(@Nullable GuiEventListener focused) { + if (Objects.equals(getFocused(), focused)) { + return; + } + super.setFocused(focused); + } + + @Override + public void setFocused(boolean focused) { + if (focused) { + // When the list gains focus, try to focus an entry + if (getFocused() == null) { + setFocused(getSelected()); + } + } else { + // When the list looses focus, remove focus from entry + setFocused(null); + } + super.setFocused(focused); + } + + /** + * Replace the list's content with new {@link TargetListEntry} entries. + * + *

+ * + * @param targets the new list of {@link TargetListEntry} entries. + */ + @Override + public void replaceEntries(Collection targets) { + if (Objects.equals(children(), targets)) { + // Update only if the list has changed + return; + } + TargetListEntry selection = migrateSelection(getSelected(), targets, children()); + super.replaceEntries(targets); + setSelected(selection); + } + + private static @Nullable TargetListEntry migrateSelection(TargetListEntry selection, Collection newEntries, Collection oldEntries) { + // New list is empty, can't select anything + if (newEntries.isEmpty()) { + return null; + } + + // No previous selection existed, nothing to check + if (selection == null) { + return newEntries.stream().findFirst().orElse(null); + } + + // Migrate to the nearest neighbor of the previous selection + return nearestNeighbor(selection, newEntries, oldEntries); + } + + // Search for the "nearest neighbor" still present in the new list. + // This minimises GUI focus jumps when the selection is lost, improving UX. + private static @Nullable TargetListEntry nearestNeighbor(TargetListEntry selection, Collection newEntries, Collection oldEntries) { + // Need a list to access by index, avoid creating a new one if we already have one + List newList = newEntries instanceof List list ? list : newEntries.stream().toList(); + List oldList = oldEntries instanceof List list ? list : oldEntries.stream().toList(); + + int len = oldList.size(); + int index = oldList.indexOf(selection); + IntPredicate newContains = i -> newList.contains(oldList.get(i)); + + // Check if the previous selection is still present + if (index >= 0 && index < len && newContains.test(index)) { + return selection; + } + + // Search in both directions with a single pass + // Iterate until both dec & inc are out of bounds + int dec = index - 1; + int inc = index + 1; + while (dec >= 0 || inc < len) { + // Check for a lesser neighbor + if (dec >= 0 && newContains.test(dec)) { + return oldList.get(dec); + } + + // Check for a greater neighbor + if (inc < len && newContains.test(inc)) { + return oldList.get(inc); + } + + dec--; + inc++; + } + + // No existing entry is still present + return newList.stream().findFirst().orElse(null); + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/go/TargetsPane.java b/common/src/main/java/net/xolt/freecam/gui/go/TargetsPane.java new file mode 100644 index 00000000..ce1a62a7 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/TargetsPane.java @@ -0,0 +1,199 @@ +package net.xolt.freecam.gui.go; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractContainerWidget; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.renderer.texture.Tickable; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.xolt.freecam.Freecam; +import net.xolt.freecam.gui.textures.ScaledTexture; +import net.xolt.freecam.gui.go.tabs.GotoScreenTab; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; + +import static net.xolt.freecam.Freecam.MC; + +public class TargetsPane extends AbstractContainerWidget implements Tickable { + + private static final ScaledTexture BACKGROUND = ScaledTexture.get(new ResourceLocation(Freecam.MOD_ID, "textures/gui/goto_list_background.png")); + private static final ResourceLocation SEARCH_ICON = new ResourceLocation("icon/search"); + private static final Component SEARCH_TEXT = Component.translatable("gui.recipebook.search_hint").withStyle(ChatFormatting.ITALIC).withStyle(ChatFormatting.GRAY); + private static final int SEARCH_ICON_SIZE = 12; + private static final int SEARCH_Y_OFFSET = 2; + private static final int SEARCH_X_OFFSET = SEARCH_ICON_SIZE + 6; + private static final int SEARCH_HEIGHT = 15; + private static final int LIST_Y_OFFSET = SEARCH_Y_OFFSET + SEARCH_HEIGHT; + private static final int LIST_ITEM_HEIGHT = 36; + + private final EditBox searchBox; + private final TargetListWidget list; + private final List children; + + private GotoScreenTab currentTab; + private String currentSearch; + + public TargetsPane(int top, int width, int height, GotoScreenTab tab) { + super(0, top, width, height, Component.empty()); + + this.currentTab = tab; + this.list = new TargetListWidget(top + LIST_Y_OFFSET, width, height - LIST_Y_OFFSET - 1, LIST_ITEM_HEIGHT); + this.searchBox = new EditBox(MC.font, list.getRowWidth() - SEARCH_X_OFFSET - 1, SEARCH_HEIGHT, SEARCH_TEXT); + this.searchBox.setPosition(renderX() + SEARCH_X_OFFSET, getY() + SEARCH_Y_OFFSET); + this.searchBox.setHint(SEARCH_TEXT); + this.searchBox.setMaxLength(16); + this.searchBox.setVisible(true); + this.searchBox.setTextColor(0xFFFFFF); + this.searchBox.setResponder(search -> currentSearch = search.trim().toLowerCase(Locale.ROOT)); + + this.children = List.of(searchBox, list); + + this.setFocused(list); + + // Prevent slight delay in showing player list + this.tick(); + } + + @Override + protected void renderWidget(GuiGraphics gfx, int mouseX, int mouseY, float partialTick) { + BACKGROUND.draw(gfx, renderX(), getY(), renderWidth(), getHeight()); + gfx.blitSprite(SEARCH_ICON, renderX() + 3, getY() + 4, SEARCH_ICON_SIZE, SEARCH_ICON_SIZE); + children.forEach(widget -> widget.render(gfx, mouseX, mouseY, partialTick)); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + children.forEach(widget -> widget.updateNarration(narrationElementOutput)); + } + + @Override + public @NotNull List children() { + return children; + } + + @Override + public void tick() { + list.replaceEntries(currentTab.provideEntriesFor(list).stream() + .filter(entry -> currentSearch == null + || currentSearch.isEmpty() + || entry.matchesSearch(currentSearch)) + .sorted() + .toList()); + } + + @Override + public void setWidth(int width) { + super.setWidth(width); + searchBox.setX(renderX() + SEARCH_X_OFFSET); + list.setWidth(width); + } + + @Override + public void setHeight(int height) { + super.setHeight(height); + list.setHeight(height - LIST_Y_OFFSET - 1); + } + + @Override + public void setX(int x) { + throw new UnsupportedOperationException(); + } + + @Override + public void setY(int y) { + super.setY(y); + searchBox.setY(y + SEARCH_Y_OFFSET); + list.setY(y + LIST_Y_OFFSET); + } + + @Override + public void setSize(int width, int height) { + setWidth(width); + setHeight(height); + } + + @Override + public void setFocused(@Nullable GuiEventListener focused) { + if (Objects.equals(getFocused(), focused)) { + return; + } + super.setFocused(focused); + } + + @Override + public void setFocused(boolean focused) { + if (focused) { + // Ensure something is focused + if (getFocused() == null) { + setFocused(list); + } + } else { + // Remove focus from child + setFocused(null); + } + super.setFocused(focused); + } + + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!isFocused()) { + return false; + } + + if (searchBox.isFocused()) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + setFocused(list); + return true; + } + return searchBox.keyPressed(keyCode, scanCode, modifiers); + } + + if (list.isFocused()) { + return list.keyPressed(keyCode, scanCode, modifiers); + } + + return false; + } + + private int renderWidth() { + return list.getRowWidth() + 2; + } + + private int renderX() { + return (width - renderWidth()) / 2; + } + + public void setTab(GotoScreenTab tab) { + this.currentTab = tab; + } + + /** + * @return whether a text entry widget is in focus. + */ + public boolean isTyping() { + return searchBox.isFocused(); + } + + /** + * @return whether a goto target is currently selected. + */ + public boolean hasTarget() { + return list.getSelected() != null; + } + + /** + * Calls {@link TargetListEntry#go()} on the current target, if there is one. + */ + public void gotoTarget() { + Optional.ofNullable(list.getSelected()).ifPresent(TargetListEntry::go); + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/go/tabs/GotoScreenTab.java b/common/src/main/java/net/xolt/freecam/gui/go/tabs/GotoScreenTab.java new file mode 100644 index 00000000..77dd0583 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/tabs/GotoScreenTab.java @@ -0,0 +1,27 @@ +package net.xolt.freecam.gui.go.tabs; + +import net.minecraft.client.gui.components.AbstractButton; +import net.xolt.freecam.gui.go.TargetListEntry; +import net.xolt.freecam.gui.go.TargetListWidget; + +import java.util.List; + +public enum GotoScreenTab implements Tab { + PLAYER(new PlayerTab()); + + private final Tab implementation; + + GotoScreenTab(Tab tab) { + this.implementation = tab; + } + + @Override + public List provideEntriesFor(TargetListWidget widget) { + return implementation.provideEntriesFor(widget); + } + + @Override + public List extraButtons() { + return implementation.extraButtons(); + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/go/tabs/PlayerTab.java b/common/src/main/java/net/xolt/freecam/gui/go/tabs/PlayerTab.java new file mode 100644 index 00000000..b1115a0b --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/tabs/PlayerTab.java @@ -0,0 +1,67 @@ +package net.xolt.freecam.gui.go.tabs; + +import com.google.common.base.Suppliers; +import net.minecraft.client.gui.components.AbstractButton; +import net.minecraft.client.gui.components.CycleButton; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.network.chat.Component; +import net.xolt.freecam.config.ModConfig; +import net.xolt.freecam.gui.go.PlayerListEntry; +import net.xolt.freecam.gui.go.TargetListEntry; +import net.xolt.freecam.gui.go.TargetListWidget; +import net.xolt.freecam.util.FreeCamera; +import net.xolt.freecam.variant.api.BuildVariant; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static net.xolt.freecam.Freecam.MC; + +class PlayerTab implements Tab { + + private final Supplier> perspectiveButton = Suppliers.memoize(() -> CycleButton + .builder((ModConfig.Perspective value) -> Component.translatable(value.getKey())) + .withValues(ModConfig.Perspective.values()) + .withInitialValue(ModConfig.get().hidden.gotoPlayerPerspective) + .withTooltip(value -> Tooltip.create(Component.translatable("gui.freecam.goto.button.perspective.@Tooltip"))) + .displayOnlyValue() + .create(0, 0, 80, 20, Component.empty(), (button, value) -> { + ModConfig.get().hidden.gotoPlayerPerspective = value; + ModConfig.save(); + })); + + @Override + public List provideEntriesFor(TargetListWidget widget) { + // Store the existing entries in a UUID map for easy lookup + Map currentEntries = widget.children() + .parallelStream() + .filter(PlayerListEntry.class::isInstance) + .map(PlayerListEntry.class::cast) + .collect(Collectors.toUnmodifiableMap(PlayerListEntry::getUUID, Function.identity())); + + // Map the in-range players into PlayerListEntries + // Use existing entries if possible + return MC.level.players() + .parallelStream() + .filter(player -> !(player instanceof FreeCamera)) + .filter(this::permitted) + .map(player -> Objects.requireNonNullElseGet( + currentEntries.get(player.getUUID()), + () -> new PlayerListEntry(widget, player))) + .map(TargetListEntry.class::cast) + .toList(); + } + + @Override + public List extraButtons() { + return Collections.singletonList(perspectiveButton.get()); + } + + private boolean permitted(AbstractClientPlayer player) { + // TODO check if player is visible + return BuildVariant.getInstance().cheatsPermitted() || Objects.equals(MC.player, player); + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/go/tabs/Tab.java b/common/src/main/java/net/xolt/freecam/gui/go/tabs/Tab.java new file mode 100644 index 00000000..1fe1bb07 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/tabs/Tab.java @@ -0,0 +1,14 @@ +package net.xolt.freecam.gui.go.tabs; + +import net.minecraft.client.gui.components.AbstractButton; +import net.xolt.freecam.gui.go.TargetListEntry; +import net.xolt.freecam.gui.go.TargetListWidget; + +import java.util.List; + +interface Tab { + + List provideEntriesFor(TargetListWidget widget); + + List extraButtons(); +} diff --git a/common/src/main/java/net/xolt/freecam/gui/textures/NineSliceTexture.java b/common/src/main/java/net/xolt/freecam/gui/textures/NineSliceTexture.java new file mode 100644 index 00000000..11de6b10 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/textures/NineSliceTexture.java @@ -0,0 +1,83 @@ +package net.xolt.freecam.gui.textures; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling.NineSlice; +import net.minecraft.resources.ResourceLocation; + +public class NineSliceTexture extends ScaledTexture { + private final ResourceLocation identifier; + private final int left; + private final int right; + private final int top; + private final int bottom; + + NineSliceTexture(ResourceLocation identifier, NineSlice scaling) { + super(identifier, scaling.width(), scaling.height()); + this.identifier = identifier; + this.left = scaling.border().left(); + this.right = scaling.border().right(); + this.top = scaling.border().top(); + this.bottom = scaling.border().bottom(); + } + + /** + * Draw the texture to screen, using {@link NineSlice} scaling to fill the specified {@code width}/{@code height}. + * + * @param gfx {@link GuiGraphics graphics} context to use + * @param x x position on screen + * @param y y position on screen + * @param width width to draw on screen + * @param height height to draw on screen + * @see GuiGraphics#blitNineSlicedSprite(TextureAtlasSprite, NineSlice, int, int, int, int, int) vanilla implementation. + */ + @Override + public void draw(GuiGraphics gfx, int x, int y, int width, int height) { + // Fast path: no scaling + if (width == textureWidth && height == textureHeight) { + blitRegionStretch(gfx, x, y, width, height, 0, 0, textureWidth, textureHeight); + return; + } + + // Clamp borders so they never exceed half of the target size + int l = Math.min(this.left, width / 2); + int r = Math.min(this.right, width / 2); + int t = Math.min(this.top, height / 2); + int b = Math.min(this.bottom, height / 2); + + int midW = Math.max(0, width - l - r); + int midH = Math.max(0, height - t - b); + + int texMidW = Math.max(0, textureWidth - this.left - this.right); + int texMidH = Math.max(0, textureHeight - this.top - this.bottom); + + // Corners + blitRegionStretch(gfx, x, y, l, t, 0, 0, this.left, this.top); // top-left + blitRegionStretch(gfx, x + width - r, y, r, t, textureWidth - this.right, 0, this.right, this.top); // top-right + blitRegionStretch(gfx, x, y + height - b, l, b, 0, textureHeight - this.bottom, this.left, this.bottom); // bottom-left + blitRegionStretch(gfx, x + width - r, y + height - b, r, b, textureWidth - this.right, textureHeight - this.bottom, this.right, this.bottom); // bottom-right + + // Edges + if (midW > 0) { + blitRegionStretch(gfx, x + l, y, midW, t, this.left, 0, texMidW, this.top); // top edge + blitRegionStretch(gfx, x + l, y + height - b, midW, b, this.left, textureHeight - this.bottom, texMidW, this.bottom); // bottom edge + } + + if (midH > 0) { + blitRegionStretch(gfx, x, y + t, l, midH, 0, this.top, this.left, texMidH); // left edge + blitRegionStretch(gfx, x + width - r, y + t, r, midH, textureWidth - this.right, this.top, this.right, texMidH); // right edge + } + + // Center + if (midW > 0 && midH > 0) { + blitRegionStretch(gfx, x + l, y + t, midW, midH, this.left, this.top, texMidW, texMidH); + } + } + + /** + * Stretch-draw a sub-rectangle (u,v,uW,uH) of the texture to (x,y,drawW,drawH). + */ + private void blitRegionStretch(GuiGraphics gfx, int x, int y, int drawW, int drawH, int u, int v, int uW, int uH) { + gfx.blit(this.identifier, x, y, drawW, drawH, u, v, uW, uH, this.textureWidth, this.textureHeight); + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/textures/ScaledTexture.java b/common/src/main/java/net/xolt/freecam/gui/textures/ScaledTexture.java new file mode 100644 index 00000000..a87d2e79 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/textures/ScaledTexture.java @@ -0,0 +1,146 @@ +package net.xolt.freecam.gui.textures; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.GuiSpriteManager; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.metadata.gui.GuiMetadataSection; +import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling; +import net.minecraft.resources.ResourceLocation; +import org.slf4j.Logger; + +import java.io.IOException; + +/** + * Reimplement a subset of vanilla Minecraft's {@link TextureAtlasSprite atlas/sprite} rendering so that we can draw + * scaled textures without needing to use {@link GuiSpriteManager}. + * + * @see GuiSpriteScaling + * @see TextureAtlasSprite + */ +public abstract class ScaledTexture { + protected static final Logger LOGGER = LogUtils.getLogger(); + protected final ResourceLocation location; + protected final int textureWidth; + protected final int textureHeight; + + protected ScaledTexture(ResourceLocation location, int width, int height) { + this.location = location; + this.textureWidth = width; + this.textureHeight = height; + } + + /** + * Get the {@code ScaledTexture} representing the specified texture. The texture must have a {@code mcmeta} file + * containing a {@link GuiMetadataSection} specifying a supported {@link GuiSpriteScaling}. + *

+ * Currently, {@link GuiSpriteScaling.Tile tile} and {@link GuiSpriteScaling.NineSlice nine_slice} are supported. + * Notably, {@link GuiSpriteScaling.Stretch stretch} is not. + * + * @param location the location of the texture asset + * @return a {@code ScaledTexture} representing the asset + * @throws UnsupportedOperationException if a supported {@link GuiSpriteScaling scaling type} is not specified + */ + public static ScaledTexture get(ResourceLocation location) { + GuiSpriteScaling scaling = Minecraft.getInstance() + .getResourceManager() + .getResource(location) + .map(resource -> { + try { + return resource.metadata(); + } catch (IOException e) { + LOGGER.error("Unable to parse metadata from {}", location, e); + return null; + } + }) + .flatMap(metadata -> metadata.getSection(GuiMetadataSection.TYPE)) + .orElse(GuiMetadataSection.DEFAULT) + .scaling(); + + return switch (scaling.type()) { + case TILE -> new TileTexture(location, (GuiSpriteScaling.Tile) scaling); + case NINE_SLICE -> new NineSliceTexture(location, (GuiSpriteScaling.NineSlice) scaling); + default -> throw new UnsupportedOperationException("Unsupported scaling type: " + scaling.type()); + }; + } + + /** + * Draw the texture to screen, scaling to the specified {@code width}/{@code height} using the specific implementation. + * + * @param gfx {@link GuiGraphics graphics} context to use + * @param x x position on screen + * @param y y position on screen + * @param width width to draw on screen + * @param height height to draw on screen + */ + public abstract void draw(GuiGraphics gfx, int x, int y, int width, int height); + + /** + * Draw a region of the texture to screen, without scaling. + * + * @param gfx {@link GuiGraphics graphics} context to use + * @param x x position on screen + * @param y y position on screen + * @param width width of the texture region + * @param height height of the texture region + * @param u left position in texture + * @param v top position in texture + */ + protected void drawRegion(GuiGraphics gfx, int x, int y, int width, int height, int u, int v) { + this.drawRegion(gfx, x, y, width, height, u, v, width, height); + } + + /** + * Draw a region of the texture to screen, scaling to the {@code width}/{@code height} drawn to screen. + * + * @param gfx {@link GuiGraphics graphics} context to use + * @param x x position on screen + * @param y y position on screen + * @param width width to draw on screen + * @param height height to draw on screen + * @param u left position in texture + * @param v top position in texture + * @param regionWidth width of the texture region + * @param regionHeight height of the texture region + * @see GuiGraphics#blit(net.minecraft.resources.ResourceLocation, int, int, int, int, float, float, int, int, int, int) vanilla implementation + */ + protected void drawRegion(GuiGraphics gfx, int x, int y, int width, int height, int u, int v, int regionWidth, int regionHeight) { + gfx.blit(location, x, y, width, height, u, v, regionWidth, regionHeight, textureWidth, textureHeight); + } + + /** + * Draw a region of the texture to screen, tiling as necessary to fill the {@code width}/{@code height} drawn to screen. + * + * @param gfx {@link GuiGraphics graphics} context to use + * @param x x position on screen + * @param y y position on screen + * @param width width to draw on screen + * @param height height to draw on screen + * @param u left position in texture + * @param v top position in texture + * @param regionWidth width of the texture region + * @param regionHeight height of the texture region + * @see GuiGraphics#blitTiledSprite(TextureAtlasSprite, int, int, int, int, int, int, int, int, int, int, int) vanilla implementation + */ + protected void drawRegionTiled(GuiGraphics gfx, int x, int y, int width, int height, int u, int v, int regionWidth, int regionHeight) { + if (width <= 0 || height <= 0) { + // Nothing to do + return; + } + + if (regionWidth <= 0 || regionHeight <= 0) { + throw new IllegalArgumentException("Tiled sprite texture size must be positive, got " + regionWidth + "x" + regionHeight); + } + + // `progress` tracks how many pixels have been rendered on each axis so far. + // `next` is the number of pixels to draw during this iteration, clamped to avoid overflowing width or height. + for (int xProgress = 0; xProgress < width; xProgress += regionWidth) { + int xNext = Math.min(regionWidth, width - xProgress); + for (int yProgress = 0; yProgress < height; yProgress += regionHeight) { + int yNext = Math.min(regionHeight, height - yProgress); + drawRegion(gfx, x + xProgress, y + yProgress, xNext, yNext, u, v, regionWidth, regionHeight); + } + } + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/textures/TileTexture.java b/common/src/main/java/net/xolt/freecam/gui/textures/TileTexture.java new file mode 100644 index 00000000..19ccf5f6 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/textures/TileTexture.java @@ -0,0 +1,34 @@ +package net.xolt.freecam.gui.textures; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling; +import net.minecraft.resources.ResourceLocation; + +public class TileTexture extends ScaledTexture { + + TileTexture(ResourceLocation identifier, GuiSpriteScaling.Tile scaling) { + super(identifier, scaling.width(), scaling.height()); + } + + /** + * Draw the texture to screen, tiling as necessary to fill the specified {@code width}/{@code height}. + * + * @param gfx {@link GuiGraphics graphics} context to use + * @param x x position on screen + * @param y y position on screen + * @param width width to draw on screen + * @param height height to draw on screen + * @see GuiGraphics#blitTiledSprite(TextureAtlasSprite, int, int, int, int, int, int, int, int, int, int, int) vanilla implementation + */ + @Override + public void draw(GuiGraphics gfx, int x, int y, int width, int height) { + if (width == textureWidth && height == textureHeight) { + // Draw without scaling + this.drawRegion(gfx, x, y, width, height, 0, 0, textureWidth, textureHeight); + return; + } + + this.drawRegionTiled(gfx, x, y, width, height, 0, 0, textureWidth, textureHeight); + } +} diff --git a/common/src/main/java/net/xolt/freecam/mixins/BlockStateBaseMixin.java b/common/src/main/java/net/xolt/freecam/mixins/BlockStateBaseMixin.java index 682cc289..be0c5136 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/BlockStateBaseMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/BlockStateBaseMixin.java @@ -28,19 +28,19 @@ public abstract class BlockStateBaseMixin { private void onGetCollisionShape(BlockGetter world, BlockPos pos, CollisionContext context, CallbackInfoReturnable cir) { if (context instanceof EntityCollisionContext entityShapeContext && entityShapeContext.getEntity() instanceof FreeCamera) { // Return early if "Always Check Initial Collision" is on and Freecam isn't enabled yet - if (ModConfig.INSTANCE.collision.alwaysCheck && !Freecam.isEnabled()) { + if (ModConfig.get().collision.alwaysCheck && !Freecam.isEnabled()) { return; } // Ignore all collisions - if (ModConfig.INSTANCE.collision.ignoreAll && BuildVariant.getInstance().cheatsPermitted()) { + if (ModConfig.get().collision.ignoreAll && BuildVariant.getInstance().cheatsPermitted()) { cir.setReturnValue(Shapes.empty()); } // Ignore transparent block collisions - if (ModConfig.INSTANCE.collision.ignoreTransparent && CollisionWhitelist.isTransparent(getBlock())) { + if (ModConfig.get().collision.ignoreTransparent && CollisionWhitelist.isTransparent(getBlock())) { cir.setReturnValue(Shapes.empty()); } // Ignore openable block collisions - if (ModConfig.INSTANCE.collision.ignoreOpenable && CollisionWhitelist.isOpenable(getBlock())) { + if (ModConfig.get().collision.ignoreOpenable && CollisionWhitelist.isOpenable(getBlock())) { cir.setReturnValue(Shapes.empty()); } } diff --git a/common/src/main/java/net/xolt/freecam/mixins/CameraMixin.java b/common/src/main/java/net/xolt/freecam/mixins/CameraMixin.java index 5fc7c25c..c1757078 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/CameraMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/CameraMixin.java @@ -36,7 +36,7 @@ public void onUpdate(BlockGetter area, Entity newFocusedEntity, boolean thirdPer // Removes the submersion overlay when underwater, in lava, or powdered snow. @Inject(method = "getFluidInCamera", at = @At("HEAD"), cancellable = true) public void onGetSubmersionType(CallbackInfoReturnable cir) { - if (Freecam.isEnabled() && !ModConfig.INSTANCE.visual.showSubmersion) { + if (Freecam.isEnabled() && !ModConfig.get().visual.showSubmersion) { cir.setReturnValue(FogType.NONE); } } diff --git a/common/src/main/java/net/xolt/freecam/mixins/EntityMixin.java b/common/src/main/java/net/xolt/freecam/mixins/EntityMixin.java index 3864c04e..9cd4d752 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/EntityMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/EntityMixin.java @@ -71,6 +71,6 @@ private void onSetPos(CallbackInfo ci) { @Unique private boolean freecam$allowFreeze() { - return ModConfig.INSTANCE.utility.freezePlayer && BuildVariant.getInstance().cheatsPermitted() && !Freecam.isPlayerControlEnabled(); + return ModConfig.get().utility.freezePlayer && BuildVariant.getInstance().cheatsPermitted() && !Freecam.isPlayerControlEnabled(); } } diff --git a/common/src/main/java/net/xolt/freecam/mixins/GameRendererMixin.java b/common/src/main/java/net/xolt/freecam/mixins/GameRendererMixin.java index 0bdd800b..94ee20e7 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/GameRendererMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/GameRendererMixin.java @@ -29,7 +29,7 @@ private void onShouldRenderBlockOutline(CallbackInfoReturnable cir) { // Makes mouse clicks come from the player rather than the freecam entity when player control is enabled or if interaction mode is set to player. @ModifyVariable(method = "pick", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/client/Minecraft;getCameraEntity()Lnet/minecraft/world/entity/Entity;")) private Entity onUpdateTargetedEntity(Entity entity) { - if (Freecam.isEnabled() && (Freecam.isPlayerControlEnabled() || ModConfig.INSTANCE.utility.interactionMode.equals(ModConfig.InteractionMode.PLAYER))) { + if (Freecam.isEnabled() && (Freecam.isPlayerControlEnabled() || ModConfig.get().utility.interactionMode.equals(ModConfig.InteractionMode.PLAYER))) { return MC.player; } return entity; @@ -37,6 +37,6 @@ private Entity onUpdateTargetedEntity(Entity entity) { @Unique private static boolean freecam$allowInteract() { - return ModConfig.INSTANCE.utility.allowInteract && (BuildVariant.getInstance().cheatsPermitted() || ModConfig.INSTANCE.utility.interactionMode.equals(PLAYER)); + return ModConfig.get().utility.allowInteract && (BuildVariant.getInstance().cheatsPermitted() || ModConfig.get().utility.interactionMode.equals(PLAYER)); } } diff --git a/common/src/main/java/net/xolt/freecam/mixins/LevelRendererMixin.java b/common/src/main/java/net/xolt/freecam/mixins/LevelRendererMixin.java index b156f2a7..55a80e8a 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/LevelRendererMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/LevelRendererMixin.java @@ -27,7 +27,7 @@ public class LevelRendererMixin { // Makes the player render if showPlayer is enabled. @Inject(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/LevelRenderer;checkPoseStack(Lcom/mojang/blaze3d/vertex/PoseStack;)V", ordinal = 0)) private void onRender(PoseStack matrices, float tickDelta, long limitTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightTexture lightmapTextureManager, Matrix4f positionMatrix, CallbackInfo ci) { - if (Freecam.isEnabled() && ModConfig.INSTANCE.visual.showPlayer) { + if (Freecam.isEnabled() && ModConfig.get().visual.showPlayer) { Vec3 cameraPos = camera.getPosition(); renderEntity(MC.player, cameraPos.x, cameraPos.y, cameraPos.z, tickDelta, matrices, renderBuffers.bufferSource()); } diff --git a/common/src/main/java/net/xolt/freecam/mixins/LightTextureMixin.java b/common/src/main/java/net/xolt/freecam/mixins/LightTextureMixin.java index ee6d1f25..35b8f17f 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/LightTextureMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/LightTextureMixin.java @@ -12,7 +12,7 @@ public class LightTextureMixin { @ModifyArg(method = "updateLightTexture", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/NativeImage;setPixelRGBA(III)V"), index = 2) private int onSetColor(int color) { - if (Freecam.isEnabled() && ModConfig.INSTANCE.visual.fullBright) { + if (Freecam.isEnabled() && ModConfig.get().visual.fullBright) { return 0xFFFFFFFF; } return color; diff --git a/common/src/main/java/net/xolt/freecam/mixins/LivingEntityMixin.java b/common/src/main/java/net/xolt/freecam/mixins/LivingEntityMixin.java index 3cd4187c..1cc6af56 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/LivingEntityMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/LivingEntityMixin.java @@ -1,5 +1,6 @@ package net.xolt.freecam.mixins; +import net.minecraft.world.entity.LivingEntity; import net.xolt.freecam.Freecam; import net.xolt.freecam.config.ModConfig; import org.spongepowered.asm.mixin.Mixin; @@ -12,8 +13,6 @@ import static net.xolt.freecam.Freecam.MC; import static net.xolt.freecam.config.ModConfig.FlightMode.CREATIVE; -import net.minecraft.world.entity.LivingEntity; - @Mixin(LivingEntity.class) public abstract class LivingEntityMixin { @@ -22,15 +21,15 @@ public abstract class LivingEntityMixin { // Allows for the horizontal speed of creative flight to be configured separately from vertical speed. @Inject(method = "getFrictionInfluencedSpeed", at = @At("HEAD"), cancellable = true) private void onGetMovementSpeed(CallbackInfoReturnable cir) { - if (Freecam.isEnabled() && ModConfig.INSTANCE.movement.flightMode.equals(CREATIVE) && this.equals(Freecam.getFreeCamera())) { - cir.setReturnValue((float) (ModConfig.INSTANCE.movement.horizontalSpeed / 10) * (Freecam.getFreeCamera().isSprinting() ? 2 : 1)); + if (Freecam.isEnabled() && ModConfig.get().movement.flightMode.equals(CREATIVE) && this.equals(Freecam.getFreeCamera())) { + cir.setReturnValue((float) (ModConfig.get().movement.horizontalSpeed / 10) * (Freecam.getFreeCamera().isSprinting() ? 2 : 1)); } } // Disables freecam upon receiving damage if disableOnDamage is enabled. @Inject(method = "setHealth", at = @At("HEAD")) private void onSetHealth(float health, CallbackInfo ci) { - if (Freecam.isEnabled() && ModConfig.INSTANCE.utility.disableOnDamage && this.equals(MC.player)) { + if (Freecam.isEnabled() && ModConfig.get().utility.disableOnDamage && this.equals(MC.player)) { if (!MC.player.isCreative() && getHealth() > health) { Freecam.disableNextTick(); } diff --git a/common/src/main/java/net/xolt/freecam/mixins/MinecraftMixin.java b/common/src/main/java/net/xolt/freecam/mixins/MinecraftMixin.java index 24934309..3d1b3839 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/MinecraftMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/MinecraftMixin.java @@ -57,6 +57,6 @@ private void onHandleInputEvents(CallbackInfo ci) { @Unique private static boolean freecam$allowInteract() { - return ModConfig.INSTANCE.utility.allowInteract && (BuildVariant.getInstance().cheatsPermitted() || ModConfig.INSTANCE.utility.interactionMode.equals(PLAYER)); + return ModConfig.get().utility.allowInteract && (BuildVariant.getInstance().cheatsPermitted() || ModConfig.get().utility.interactionMode.equals(PLAYER)); } } diff --git a/common/src/main/java/net/xolt/freecam/mixins/MultiPlayerGameModeMixin.java b/common/src/main/java/net/xolt/freecam/mixins/MultiPlayerGameModeMixin.java index 4224bb7b..ac4291d9 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/MultiPlayerGameModeMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/MultiPlayerGameModeMixin.java @@ -63,6 +63,6 @@ private void onAttackEntity(Player player, Entity target, CallbackInfo ci) { @Unique private static boolean freecam$allowInteract() { - return ModConfig.INSTANCE.utility.allowInteract && (BuildVariant.getInstance().cheatsPermitted() || ModConfig.INSTANCE.utility.interactionMode.equals(PLAYER)); + return ModConfig.get().utility.allowInteract && (BuildVariant.getInstance().cheatsPermitted() || ModConfig.get().utility.interactionMode.equals(PLAYER)); } } diff --git a/common/src/main/java/net/xolt/freecam/util/FreeCamera.java b/common/src/main/java/net/xolt/freecam/util/FreeCamera.java index 01af8e47..c687210e 100644 --- a/common/src/main/java/net/xolt/freecam/util/FreeCamera.java +++ b/common/src/main/java/net/xolt/freecam/util/FreeCamera.java @@ -17,6 +17,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.PushReaction; import net.xolt.freecam.config.ModConfig; +import net.xolt.freecam.variant.api.BuildVariant; import java.util.UUID; @@ -51,7 +52,7 @@ public FreeCamera(int id) { @Override public void copyPosition(Entity entity) { - applyPosition(new FreecamPosition(entity)); + applyPosition(FreecamPosition.of(entity)); } public void applyPosition(FreecamPosition position) { @@ -60,12 +61,13 @@ public void applyPosition(FreecamPosition position) { yBob = getYRot(); xBobO = xBob; // Prevents camera from rotating upon entering freecam. yBobO = yBob; + applyPerspective(position.perspective, ModConfig.get().collision.alwaysCheck || !(ModConfig.get().collision.ignoreAll && BuildVariant.getInstance().cheatsPermitted())); } // Mutate the position and rotation based on perspective // If checkCollision is true, move as far as possible without colliding - public void applyPerspective(ModConfig.Perspective perspective, boolean checkCollision) { - FreecamPosition position = new FreecamPosition(this); + private void applyPerspective(ModConfig.Perspective perspective, boolean checkCollision) { + FreecamPosition position = FreecamPosition.of(this); switch (perspective) { case INSIDE: @@ -105,7 +107,7 @@ private boolean moveForwardUntilCollision(FreecamPosition position, double maxDi // Move forward by increment until we reach maxDistance or hit a collision for (double distance = 0.0; distance < maxDistance; distance += increment) { - FreecamPosition oldPosition = new FreecamPosition(this); + FreecamPosition oldPosition = FreecamPosition.of(this); position.moveForward(negative ? -1 * increment : increment); applyPosition(position); @@ -176,7 +178,7 @@ public MobEffectInstance getEffect(MobEffect effect) { // Prevents pistons from moving FreeCamera when collision.ignoreAll is enabled. @Override public PushReaction getPistonPushReaction() { - return ModConfig.INSTANCE.collision.ignoreAll ? PushReaction.IGNORE : PushReaction.NORMAL; + return ModConfig.get().collision.ignoreAll ? PushReaction.IGNORE : PushReaction.NORMAL; } // Prevents collision with solid entities (shulkers, boats) @@ -210,11 +212,11 @@ protected void doWaterSplashEffect() {} @Override public void aiStep() { - if (ModConfig.INSTANCE.movement.flightMode.equals(ModConfig.FlightMode.DEFAULT)) { + if (ModConfig.get().movement.flightMode.equals(ModConfig.FlightMode.DEFAULT)) { getAbilities().setFlyingSpeed(0); - Motion.doMotion(this, ModConfig.INSTANCE.movement.horizontalSpeed, ModConfig.INSTANCE.movement.verticalSpeed); + Motion.doMotion(this, ModConfig.get().movement.horizontalSpeed, ModConfig.get().movement.verticalSpeed); } else { - getAbilities().setFlyingSpeed((float) ModConfig.INSTANCE.movement.verticalSpeed / 10); + getAbilities().setFlyingSpeed((float) ModConfig.get().movement.verticalSpeed / 10); } super.aiStep(); getAbilities().flying = true; diff --git a/common/src/main/java/net/xolt/freecam/util/FreecamPosition.java b/common/src/main/java/net/xolt/freecam/util/FreecamPosition.java index f306ca6a..af7e6c30 100644 --- a/common/src/main/java/net/xolt/freecam/util/FreecamPosition.java +++ b/common/src/main/java/net/xolt/freecam/util/FreecamPosition.java @@ -1,28 +1,68 @@ package net.xolt.freecam.util; +import net.minecraft.network.chat.Component; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Pose; import net.minecraft.world.level.ChunkPos; +import net.xolt.freecam.Freecam; +import net.xolt.freecam.config.ModConfig; +import net.xolt.freecam.tripod.TripodSlot; import org.joml.Quaternionf; import org.joml.Vector3f; +import java.util.Optional; +import java.util.function.Supplier; + +import static net.xolt.freecam.Freecam.MC; + public class FreecamPosition { public double x; public double y; public double z; + public ModConfig.Perspective perspective; public float pitch; public float yaw; + private Supplier nameSupplier = null; private final Quaternionf rotation = new Quaternionf(0.0F, 0.0F, 0.0F, 1.0F); private final Vector3f verticalPlane = new Vector3f(0.0F, 1.0F, 0.0F); private final Vector3f diagonalPlane = new Vector3f(1.0F, 0.0F, 0.0F); private final Vector3f horizontalPlane = new Vector3f(0.0F, 0.0F, 1.0F); - public FreecamPosition(Entity entity) { - x = entity.getX(); - y = getSwimmingY(entity); - z = entity.getZ(); - setRotation(entity.getYRot(), entity.getXRot()); + private FreecamPosition(double x, double y, double z, float yaw, float pitch, ModConfig.Perspective perspective) { + this.x = x; + this.y = y; + this.z = z; + this.perspective = perspective; + setRotation(yaw, pitch); + } + + public static FreecamPosition defaultPosition() { + return initialPerspectiveOf(MC.player); + } + + public static FreecamPosition initialPerspectiveOf(Entity entity) { + return of(entity, ModConfig.get().visual.perspective); + } + + public static FreecamPosition of(Entity entity) { + return of(entity, ModConfig.Perspective.INSIDE); + } + + public static FreecamPosition of(Entity entity, ModConfig.Perspective perspective) { + FreecamPosition position = new FreecamPosition(entity.getX(), getSwimmingY(entity), entity.getZ(), entity.getYRot(), entity.getXRot(), perspective); + position.setNameSupplier(() -> entity.getName().plainCopy()); + return position; + } + + public static FreecamPosition of(TripodSlot tripod) { + FreecamPosition position = Optional.ofNullable(Freecam.getTripod(tripod)).orElseGet(FreecamPosition::defaultPosition); + position.setNameSupplier(() -> Component.literal(tripod.toString())); + return position; + } + + public static FreecamPosition copyOf(FreecamPosition position) { + return new FreecamPosition(position.x, position.y, position.z, position.yaw, position.pitch, position.perspective); } // From net.minecraft.client.render.Camera.setRotation @@ -66,6 +106,25 @@ public ChunkPos getChunkPos() { return new ChunkPos((int) (x / 16), (int) (z / 16)); } + public boolean isInRange() { + ChunkPos chunk = getChunkPos(); + return MC.level.getChunkSource().hasChunk(chunk.x, chunk.z); + } + + public Component getName() { + return Optional.ofNullable(nameSupplier) + .map(Supplier::get) + .orElseGet(() -> Component.translatable("msg.freecamPosition.coords", x, y, z)); + } + + public void setNameSupplier(Supplier supplier) { + this.nameSupplier = supplier; + } + + public void setName(Component name) { + this.nameSupplier = () -> name; + } + private static double getSwimmingY(Entity entity) { if (entity.getPose() == Pose.SWIMMING) { return entity.getY(); diff --git a/common/src/main/resources/assets/freecam/lang/en_us.json b/common/src/main/resources/assets/freecam/lang/en_us.json index 2c8460c0..a622d3b4 100644 --- a/common/src/main/resources/assets/freecam/lang/en_us.json +++ b/common/src/main/resources/assets/freecam/lang/en_us.json @@ -3,13 +3,24 @@ "key.freecam.toggle": "Toggle Freecam", "key.freecam.playerControl": "Control Player", "key.freecam.tripodReset": "Reset Tripod", + "key.freecam.goto": "Goto GUI", "key.freecam.configGui": "Config GUI", "msg.freecam.enable": "Freecam has been enabled.", "msg.freecam.disable": "Freecam has been disabled.", "msg.freecam.openTripod": "Opening camera %s", "msg.freecam.closeTripod": "Closing camera %s", "msg.freecam.tripodReset": "Reset camera %s", + "msg.freecam.gotoPosition.enable": "Freecam enabled at %s.", + "msg.freecam.gotoPosition.move": "Freecam moved to %s.", + "msg.freecam.gotoPosition.moveTripod": "Tripod %s moved to %s.", + "msg.freecamPosition.coords": "x: %1.2f, y: %1.2f, z: %1.2f", "text.autoconfig.freecam.title": "Freecam Options", + "msg.freecam.gotoPosition": "Freecam jumped to %s.", + "gui.freecam.goto.title": "Freecam Goto...", + "gui.freecam.goto.button.go": "Go", + "gui.freecam.goto.button.go.@Tooltip": "Go to the selected target.", + "gui.freecam.goto.button.perspective.@Tooltip": "Initial perspective on target player.", + "gui.freecam.goto.entry.player.you": "§l%s§r§o (you)", "text.autoconfig.freecam.option.movement": "Movement Options", "text.autoconfig.freecam.option.movement.@Tooltip": "How the camera moves.", "text.autoconfig.freecam.option.movement.flightMode": "Flight Mode", @@ -71,5 +82,7 @@ "text.autoconfig.freecam.option.notification.notifyFreecam": "Freecam Notifications", "text.autoconfig.freecam.option.notification.notifyFreecam.@Tooltip": "Notifies you when entering/exiting freecam.", "text.autoconfig.freecam.option.notification.notifyTripod": "Tripod Notifications", - "text.autoconfig.freecam.option.notification.notifyTripod.@Tooltip": "Notifies you when entering/exiting tripod cameras." + "text.autoconfig.freecam.option.notification.notifyTripod.@Tooltip": "Notifies you when entering/exiting tripod cameras.", + "text.autoconfig.freecam.option.notification.notifyGoto": "Goto Notifications", + "text.autoconfig.freecam.option.notification.notifyGoto.@Tooltip": "Notifies you when using \"Goto\" to teleport the camera." } diff --git a/common/src/main/resources/assets/freecam/textures/gui/goto_background.png b/common/src/main/resources/assets/freecam/textures/gui/goto_background.png new file mode 100644 index 00000000..dff39ff9 Binary files /dev/null and b/common/src/main/resources/assets/freecam/textures/gui/goto_background.png differ diff --git a/common/src/main/resources/assets/freecam/textures/gui/goto_background.png.mcmeta b/common/src/main/resources/assets/freecam/textures/gui/goto_background.png.mcmeta new file mode 100644 index 00000000..234f4568 --- /dev/null +++ b/common/src/main/resources/assets/freecam/textures/gui/goto_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 9, + "height": 9, + "border": 4 + } + } +} diff --git a/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png b/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png new file mode 100644 index 00000000..0b4153a3 Binary files /dev/null and b/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png differ diff --git a/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png.mcmeta b/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png.mcmeta new file mode 100644 index 00000000..15c4cd9e --- /dev/null +++ b/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 3, + "height": 3, + "border": 1 + } + } +} diff --git a/common/src/main/resources/freecam.accesswidener b/common/src/main/resources/freecam.accesswidener index a8472364..dc37fa50 100644 --- a/common/src/main/resources/freecam.accesswidener +++ b/common/src/main/resources/freecam.accesswidener @@ -1,4 +1,4 @@ accessWidener v2 named #Used by EntityRendererMixin -accessible field net/minecraft/client/renderer/entity/EntityRenderDispatcher shouldRenderShadow Z \ No newline at end of file +accessible field net/minecraft/client/renderer/entity/EntityRenderDispatcher shouldRenderShadow Z diff --git a/fabric/src/main/java/net/xolt/freecam/fabric/ModMenuIntegration.java b/fabric/src/main/java/net/xolt/freecam/fabric/ModMenuIntegration.java index 01d88324..5edce27b 100644 --- a/fabric/src/main/java/net/xolt/freecam/fabric/ModMenuIntegration.java +++ b/fabric/src/main/java/net/xolt/freecam/fabric/ModMenuIntegration.java @@ -2,7 +2,6 @@ import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; -import me.shedaniel.autoconfig.AutoConfig; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.xolt.freecam.config.ModConfig; @@ -12,6 +11,6 @@ public class ModMenuIntegration implements ModMenuApi { @Override public ConfigScreenFactory getModConfigScreenFactory() { - return parent -> AutoConfig.getConfigScreen(ModConfig.class, parent).get(); + return ModConfig::getScreen; } } diff --git a/neoforge/src/main/java/net/xolt/freecam/forge/FreecamForge.java b/neoforge/src/main/java/net/xolt/freecam/forge/FreecamForge.java index 67d5b774..7bc5e483 100644 --- a/neoforge/src/main/java/net/xolt/freecam/forge/FreecamForge.java +++ b/neoforge/src/main/java/net/xolt/freecam/forge/FreecamForge.java @@ -1,6 +1,5 @@ package net.xolt.freecam.forge; -import me.shedaniel.autoconfig.AutoConfig; import net.minecraft.client.Minecraft; import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.EventPriority; @@ -9,7 +8,7 @@ import net.neoforged.fml.common.Mod; import net.neoforged.fml.common.Mod.EventBusSubscriber.Bus; import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; -import net.neoforged.neoforge.client.ConfigScreenHandler; +import net.neoforged.neoforge.client.ConfigScreenHandler.ConfigScreenFactory; import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; import net.neoforged.neoforge.event.TickEvent; import net.xolt.freecam.Freecam; @@ -26,8 +25,8 @@ public static void clientSetup(FMLClientSetupEvent event) { ModConfig.init(); // Register our config screen with Forge - ModLoadingContext.get().registerExtensionPoint(ConfigScreenHandler.ConfigScreenFactory.class, () -> new ConfigScreenHandler.ConfigScreenFactory((client, parent) -> - AutoConfig.getConfigScreen(ModConfig.class, parent).get() + ModLoadingContext.get().registerExtensionPoint(ConfigScreenFactory.class, () -> new ConfigScreenFactory((client, parent) -> + ModConfig.getScreen(parent) )); }