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.
+ *
+ *
+ * - If the existing selection is present in the new list, it is selected.
+ *
- If not, then the existing selection's nearest neighbor is selected.
+ *
- If no existing entries are present in the new list, then the first entry is selected.
+ *
- If the list is empty, then nothing is selected.
+ *
+ *
+ * @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 extends GuiEventListener> 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)
));
}