Skip to content

Commit

Permalink
Goto GUI
Browse files Browse the repository at this point in the history
Adds a GUI which allows jumping freecam to a player within render distance.

Could be expanded in future with tripod locations and other "saved" positions.
  • Loading branch information
MattSturgeon committed Dec 25, 2023
1 parent b0c93ed commit d062a38
Show file tree
Hide file tree
Showing 14 changed files with 577 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

Expand Down
44 changes: 40 additions & 4 deletions common/src/main/java/net/xolt/freecam/Freecam.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.ChunkPos;
import net.xolt.freecam.config.ModConfig;
import net.xolt.freecam.gui.go.GotoScreen;
import net.xolt.freecam.util.FreeCamera;
import net.xolt.freecam.util.FreecamPosition;
import net.xolt.freecam.variant.api.BuildVariant;
import org.lwjgl.glfw.GLFW;

import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;

import static net.xolt.freecam.config.ModBindings.*;

Expand Down Expand Up @@ -79,6 +82,10 @@ public static void postTick(Minecraft mc) {
switchControls();
}

while (KEY_GOTO_GUI.wasPressed()) {
mc.setScreen(new GotoScreen());
}

while (KEY_CONFIG_GUI.wasPressed()) {
mc.setScreen(AutoConfig.getConfigScreen(ModConfig.class, mc.screen).get());
}
Expand Down Expand Up @@ -108,6 +115,34 @@ public static void toggle() {
}
}

public static void gotoPosition(FreecamPosition position, String name, boolean perspective) {
long notificationDelay = tripodEnabled || !freecamEnabled ? 1500 : 1;

if (tripodEnabled) {
toggleTripod(activeTripod);
}

if (!freecamEnabled) {
toggle();
}

freeCamera.applyPosition(position);
if (perspective) {
freeCamera.applyPerspective(ModConfig.INSTANCE.hidden.gotoPlayerPerspective, checkInitialCollision());
}

if (ModConfig.INSTANCE.notification.notifyGoto) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (freecamEnabled && MC.player != null) {
MC.player.displayClientMessage(Component.translatable("msg.freecam.gotoPosition", name), true);
}
}
}, notificationDelay);
}
}

private static void toggleTripod(Integer keyCode) {
if (keyCode == null) {
return;
Expand Down Expand Up @@ -192,10 +227,7 @@ private static void onDisableTripod() {
private static void onEnableFreecam() {
onEnable();
freeCamera = new FreeCamera(-420);
freeCamera.applyPerspective(
ModConfig.INSTANCE.visual.perspective,
ModConfig.INSTANCE.collision.alwaysCheck || !(ModConfig.INSTANCE.collision.ignoreAll && BuildVariant.getInstance().cheatsPermitted())
);
freeCamera.applyPerspective(ModConfig.INSTANCE.visual.perspective, checkInitialCollision());
freeCamera.spawn();
MC.setCameraEntity(freeCamera);

Expand Down Expand Up @@ -282,6 +314,10 @@ public static HashMap<Integer, FreecamPosition> getTripodsForDimension() {
return result;
}

private static boolean checkInitialCollision() {
return ModConfig.INSTANCE.collision.alwaysCheck || !(ModConfig.INSTANCE.collision.ignoreAll && BuildVariant.getInstance().cheatsPermitted());
}

public static boolean disableNextTick() {
return disableNextTick;
}
Expand Down
12 changes: 10 additions & 2 deletions common/src/main/java/net/xolt/freecam/config/ModBindings.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
import java.util.function.Consumer;
import net.minecraft.client.KeyMapping;

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<KeyMapping> lazyBinding;
Expand Down Expand Up @@ -57,6 +57,14 @@ public boolean wasPressed() {
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 matchesKey(int keyCode, int scanCode) {
return get().matches(keyCode, scanCode);
}

/**
* Lazily get the actual {@link KeyMapping} represented by this enum value.
* <p>
Expand Down
11 changes: 11 additions & 0 deletions common/src/main/java/net/xolt/freecam/config/ModConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.EnumHandler.EnumDisplayOption;
import me.shedaniel.autoconfig.serializer.JanksonConfigSerializer;
import me.shedaniel.clothconfig2.gui.entries.SelectionListEntry;
import org.jetbrains.annotations.NotNull;

import net.xolt.freecam.variant.api.BuildVariant;

@Config(name = "freecam")
Expand Down Expand Up @@ -104,6 +106,15 @@ 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 Perspective gotoPlayerPerspective = Perspective.THIRD_PERSON;
}

public enum FlightMode implements SelectionListEntry.Translatable {
Expand Down
234 changes: 234 additions & 0 deletions common/src/main/java/net/xolt/freecam/gui/go/GotoScreen.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package net.xolt.freecam.gui.go;

import me.shedaniel.autoconfig.AutoConfig;
import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.CycleButton;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.components.Tooltip;
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.Texture;
import net.xolt.freecam.util.FreeCamera;
import org.lwjgl.glfw.GLFW;

import java.util.*;
import java.util.stream.Collectors;

import static net.xolt.freecam.config.ModBindings.KEY_GOTO_GUI;

public class GotoScreen extends Screen {
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);
private static final int GUI_WIDTH = 236;
private static final int GUI_TOP = 50;
private static final int LIST_TOP = GUI_TOP + 8;
private static final int LIST_ITEM_HEIGHT = 36;
private static final ResourceLocation SEARCH_ICON_TEXTURE = 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 Texture JUMP_BACKGROUND = new Texture(new ResourceLocation(Freecam.MOD_ID, "textures/gui/goto_background.png"));
private static final Texture JUMP_LIST_BACKGROUND = new Texture(new ResourceLocation(Freecam.MOD_ID, "textures/gui/goto_list_background.png"));

private ListWidget list;
private boolean initialized;
private Button buttonBack;
private Button buttonJump;
private CycleButton<ModConfig.Perspective> buttonPerspective;
private EditBox searchBox;
private String currentSearch;

public GotoScreen() {
super(Component.translatable("gui.freecam.goto.title"));
}

@Override
protected void init() {
super.init();

if (!this.initialized) {
this.list = new ListWidget(this, this.minecraft, 0, this.height, LIST_ITEM_HEIGHT);
this.searchBox = new EditBox(this.font, 0, 15, SEARCH_TEXT);
this.searchBox.setHint(SEARCH_TEXT);
this.searchBox.setMaxLength(16);
this.searchBox.setVisible(true);
this.searchBox.setTextColor(0xFFFFFF);
this.searchBox.setResponder(this::onSearchChange);

this.buttonJump = Button.builder(Component.translatable("gui.freecam.goto.button.go"), button -> this.go())
.tooltip(Tooltip.create(Component.translatable("gui.freecam.goto.button.go.@Tooltip")))
.width(48)
.build();

this.buttonPerspective = CycleButton
.builder((ModConfig.Perspective value) -> Component.translatable(value.getKey()))
.withValues(ModConfig.Perspective.values())
.withInitialValue(ModConfig.INSTANCE.hidden.gotoPlayerPerspective)
.withTooltip(value -> Tooltip.create(Component.translatable("gui.freecam.goto.button.perspective.@Tooltip")))
.displayOnlyValue()
.create(0, 0, 80, 20, null, (button, value) -> {
ModConfig.INSTANCE.hidden.gotoPlayerPerspective = value;
AutoConfig.getConfigHolder(ModConfig.class).save();
});

this.buttonBack = Button.builder(CommonComponents.GUI_BACK, button -> this.onClose()).width(48).build();
}

int listTop = LIST_TOP + 16;
int listBottom = this.getListBottom();
int innerWidth = GUI_WIDTH - 10;
int innerX = (this.width - innerWidth) / 2;

this.list.setY(listTop);
this.list.setSize(this.width, listBottom - listTop);
this.searchBox.setPosition(innerX + 20, LIST_TOP + 1);
this.searchBox.setWidth(this.list.getRowWidth() - 19);

FrameLayout positioner = new FrameLayout(innerX, listBottom + 3, innerWidth, 0);
positioner.defaultChildLayoutSetting()
.alignVerticallyBottom()
.alignHorizontallyRight();
LinearLayout layout = positioner.addChild(LinearLayout.horizontal());
layout.defaultCellSetting()
.alignVerticallyBottom()
.paddingHorizontal(2);

layout.addChild(this.buttonBack);
layout.addChild(this.buttonPerspective);
layout.addChild(this.buttonJump);

positioner.arrangeElements();
positioner.visitWidgets(this::addRenderableWidget);

List.of(this.searchBox, this.list).forEach(this::addRenderableWidget);
this.setInitialFocus(this.list);

this.initialized = true;
}

@Override
public void renderBackground(GuiGraphics gfx, int mouseX, int mouseY, float delta) {
super.renderBackground(gfx, mouseX, mouseY, delta);
int left = (this.width - GUI_WIDTH) / 2;
JUMP_BACKGROUND.draw(gfx, left, GUI_TOP, 0, GUI_WIDTH, this.getGuiHeight());
JUMP_LIST_BACKGROUND.draw(gfx, left + 7, LIST_TOP - 1, 0, this.list.getRowWidth() + 2, this.getListHeight() + 2);
gfx.blitSprite(SEARCH_ICON_TEXTURE, left + 10, LIST_TOP + 3, 12, 12);
}

@Override
public void tick() {
super.tick();
if (this.initialized) {
this.updateEntries();
}
}

@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (this.searchBox.isFocused()) {
if (keyCode == GLFW.GLFW_KEY_ESCAPE) {
this.magicalSpecialHackyFocus(null);
return true;
}
} else {
if (KEY_GOTO_GUI.matchesKey(keyCode, scanCode)) {
this.onClose();
return true;
}
}
if (this.list.getSelected() != null) {
if (CommonInputs.selected(keyCode)) {
this.go();
return true;
}
if (this.list.keyPressed(keyCode, scanCode, modifiers)) {
return true;
}
}
return super.keyPressed(keyCode, scanCode, modifiers);
}

@Override
public boolean isPauseScreen() {
return false;
}

public void updateEntries() {
List<ListEntry> entries = calculatePlayerEntries().stream()
.filter(entry -> this.currentSearch == null
|| this.currentSearch.isEmpty()
|| entry.matchesSearch(this.currentSearch))
.sorted()
.toList();

// Update only if the list has changed
if (!Objects.equals(this.list.children(), entries)) {
this.list.updateEntries(entries);
}
}

private List<ListEntry> calculatePlayerEntries() {
// Store the existing entries in a UUID map for easy lookup
Map<UUID, PlayerListEntry> currentEntries = this.list.children()
.parallelStream()
.filter(PlayerListEntry.class::isInstance)
.map(PlayerListEntry.class::cast)
.collect(Collectors.toUnmodifiableMap(PlayerListEntry::getUUID, entry -> entry));

// Map the in-range players into PlayerListEntries
// Use existing entries if possible
return this.minecraft.level.players()
.parallelStream()
.filter(player -> !(player instanceof FreeCamera))
.map(player -> Objects.requireNonNullElseGet(
currentEntries.get(player.getUUID()),
() -> new PlayerListEntry(this.minecraft, this, player)))
.map(ListEntry.class::cast)
.toList();
}

public void go() {
Optional.ofNullable(this.list.getSelected())
.ifPresent(listEntry -> {
boolean perspective = this.buttonPerspective != null && this.buttonPerspective.active;
this.onClose();
Freecam.gotoPosition(listEntry.getPosition(), listEntry.getName(), perspective);
});
}

public void select(ListEntry entry) {
this.list.setSelected(entry);
this.updateButtonState();
}

public void updateButtonState() {
ListEntry selected = this.list.getSelected();
this.buttonJump.active = selected != null;
}

private void onSearchChange(String search) {
this.currentSearch = search.toLowerCase(Locale.ROOT);
}

// GUI height
private int getGuiHeight() {
return Math.max(52, this.height - (GUI_TOP * 2));
}

// List height including search bar
private int getListHeight() {
return this.getGuiHeight() - 29 - 8;
}

private int getListBottom() {
return LIST_TOP + this.getListHeight();
}
}
Loading

0 comments on commit d062a38

Please sign in to comment.