├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ ├── resources │ ├── assets │ │ └── anglesnap │ │ │ ├── icon.png │ │ │ ├── textures │ │ │ └── gui │ │ │ │ ├── add.png │ │ │ │ ├── delete.png │ │ │ │ ├── edit.png │ │ │ │ ├── save.png │ │ │ │ ├── marker-0.png │ │ │ │ ├── marker-1.png │ │ │ │ ├── marker-2.png │ │ │ │ └── configure.png │ │ │ └── lang │ │ │ └── en_us.json │ ├── anglesnap.mixins.json │ └── fabric.mod.json │ └── java │ └── me │ └── contaria │ └── anglesnap │ ├── ModMenuApiImpl.java │ ├── gui │ ├── config │ │ ├── AngleSnapConfigScreen.java │ │ └── AngleSnapConfigListWidget.java │ ├── screen │ │ ├── IconButtonWidget.java │ │ ├── AngleSnapScreen.java │ │ └── AngleSnapListWidget.java │ ├── warning │ │ └── AngleSnapWarningScreen.java │ └── camerasnap │ │ ├── CameraSnapScreen.java │ │ └── CameraSnapListWidget.java │ ├── mixin │ ├── CameraMixin.java │ ├── MinecraftClientMixin.java │ └── MouseMixin.java │ ├── config │ ├── Option.java │ ├── BooleanOption.java │ ├── FloatOption.java │ └── AngleSnapConfig.java │ ├── CameraPosEntry.java │ ├── AngleEntry.java │ └── AngleSnap.java ├── settings.gradle ├── .gitattributes ├── .gitignore ├── gradle.properties ├── .github └── workflows │ └── build.yml ├── LICENSE ├── gradlew.bat └── gradlew /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contariaa/anglesnap/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/assets/anglesnap/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contariaa/anglesnap/HEAD/src/main/resources/assets/anglesnap/icon.png -------------------------------------------------------------------------------- /src/main/resources/assets/anglesnap/textures/gui/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contariaa/anglesnap/HEAD/src/main/resources/assets/anglesnap/textures/gui/add.png -------------------------------------------------------------------------------- /src/main/resources/assets/anglesnap/textures/gui/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contariaa/anglesnap/HEAD/src/main/resources/assets/anglesnap/textures/gui/delete.png -------------------------------------------------------------------------------- /src/main/resources/assets/anglesnap/textures/gui/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contariaa/anglesnap/HEAD/src/main/resources/assets/anglesnap/textures/gui/edit.png -------------------------------------------------------------------------------- /src/main/resources/assets/anglesnap/textures/gui/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contariaa/anglesnap/HEAD/src/main/resources/assets/anglesnap/textures/gui/save.png -------------------------------------------------------------------------------- /src/main/resources/assets/anglesnap/textures/gui/marker-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contariaa/anglesnap/HEAD/src/main/resources/assets/anglesnap/textures/gui/marker-0.png -------------------------------------------------------------------------------- /src/main/resources/assets/anglesnap/textures/gui/marker-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contariaa/anglesnap/HEAD/src/main/resources/assets/anglesnap/textures/gui/marker-1.png -------------------------------------------------------------------------------- /src/main/resources/assets/anglesnap/textures/gui/marker-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contariaa/anglesnap/HEAD/src/main/resources/assets/anglesnap/textures/gui/marker-2.png -------------------------------------------------------------------------------- /src/main/resources/assets/anglesnap/textures/gui/configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contariaa/anglesnap/HEAD/src/main/resources/assets/anglesnap/textures/gui/configure.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/resources/anglesnap.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "me.contaria.anglesnap.mixin", 5 | "compatibilityLevel": "JAVA_21", 6 | "client": [ 7 | "CameraMixin", 8 | "MinecraftClientMixin", 9 | "MouseMixin" 10 | ], 11 | "injectors": { 12 | "defaultRequire": 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # macos 28 | 29 | *.DS_Store 30 | 31 | # fabric 32 | 33 | run/ 34 | 35 | # java 36 | 37 | hs_err_*.log 38 | replay_*.log 39 | *.hprof 40 | *.jfr 41 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/ModMenuApiImpl.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap; 2 | 3 | import com.terraformersmc.modmenu.api.ConfigScreenFactory; 4 | import com.terraformersmc.modmenu.api.ModMenuApi; 5 | import me.contaria.anglesnap.gui.screen.AngleSnapScreen; 6 | 7 | public class ModMenuApiImpl implements ModMenuApi { 8 | 9 | @Override 10 | public ConfigScreenFactory getModConfigScreenFactory() { 11 | return AngleSnapScreen::create; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | org.gradle.parallel=true 4 | 5 | # Fabric Properties 6 | # check these on https://fabricmc.net/develop 7 | minecraft_version=1.21.8 8 | yarn_mappings=1.21.8+build.1 9 | loader_version=0.16.14 10 | 11 | # Mod Properties 12 | mod_version=0.8.0 13 | target_version=1.21.6-1.21.8 14 | maven_group=me.contaria 15 | archives_base_name=anglesnap 16 | 17 | # Dependencies 18 | fabric_version=0.130.0+1.21.8 19 | modmenu_version=15.0.0-beta.3 -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Automatically build the project and run any configured tests for every push 2 | # and submitted pull request. This can help catch issues that only occur on 3 | # certain platforms or Java versions, and provides a first line of defence 4 | # against bad commits. 5 | 6 | name: build 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-24.04 12 | steps: 13 | - name: checkout repository 14 | uses: actions/checkout@v4 15 | - name: validate gradle wrapper 16 | uses: gradle/actions/wrapper-validation@v4 17 | - name: setup jdk 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '21' 21 | distribution: 'microsoft' 22 | - name: make gradle wrapper executable 23 | run: chmod +x ./gradlew 24 | - name: build 25 | run: ./gradlew build 26 | - name: capture build artifacts 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: Artifacts 30 | path: build/libs/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 contaria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "anglesnap", 4 | "version": "${version}", 5 | "name": "AngleSnap", 6 | "description": "Save and snap to precise angles.", 7 | "authors": [ 8 | { 9 | "name": "contaria", 10 | "contact": { 11 | "homepage": "https://github.com/KingContaria/" 12 | } 13 | } 14 | ], 15 | "contact": { 16 | "homepage": "https://modrinth.com/mod/anglesnap", 17 | "sources": "https://github.com/KingContaria/anglesnap", 18 | "issues": "https://github.com/KingContaria/anglesnap/issues" 19 | }, 20 | "license": "MIT", 21 | "environment": "client", 22 | "entrypoints": { 23 | "client": [ 24 | "me.contaria.anglesnap.AngleSnap" 25 | ], 26 | "modmenu": [ 27 | "me.contaria.anglesnap.ModMenuApiImpl" 28 | ] 29 | }, 30 | "custom": { 31 | "modmenu": { 32 | "links": { 33 | "modmenu.discord": "https://discord.gg/B6ZV8SF672", 34 | "anglesnap.link.donations": "https://ko-fi.com/contaria" 35 | } 36 | } 37 | }, 38 | "mixins": [ 39 | "anglesnap.mixins.json" 40 | ], 41 | "depends": { 42 | "fabricloader": ">=0.16.10", 43 | "minecraft": ">=1.21.6 <=1.21.8", 44 | "java": ">=21", 45 | "fabric-api": ">=0.128.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/gui/config/AngleSnapConfigScreen.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.gui.config; 2 | 3 | import me.contaria.anglesnap.AngleSnap; 4 | import net.minecraft.client.MinecraftClient; 5 | import net.minecraft.client.gui.screen.Screen; 6 | import net.minecraft.client.gui.widget.ButtonWidget; 7 | import net.minecraft.client.gui.widget.TextWidget; 8 | import net.minecraft.screen.ScreenTexts; 9 | import net.minecraft.text.Text; 10 | 11 | public class AngleSnapConfigScreen extends Screen { 12 | private final Screen parent; 13 | 14 | public AngleSnapConfigScreen(Screen parent) { 15 | super(Text.translatable("anglesnap.gui.config.title")); 16 | this.parent = parent; 17 | } 18 | 19 | @Override 20 | protected void init() { 21 | this.addDrawableChild(new TextWidget(0, 10, this.width, 15, this.title, this.textRenderer).alignCenter()); 22 | this.addDrawableChild(new AngleSnapConfigListWidget(this.client, this.width, this.height - 70, 35)); 23 | this.addDrawableChild(ButtonWidget.builder(ScreenTexts.DONE, button -> this.close()).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build()); 24 | } 25 | 26 | @Override 27 | public void close() { 28 | MinecraftClient.getInstance().setScreen(this.parent); 29 | } 30 | 31 | @Override 32 | public void removed() { 33 | AngleSnap.CONFIG.save(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/mixin/CameraMixin.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.mixin; 2 | 3 | import me.contaria.anglesnap.AngleSnap; 4 | import me.contaria.anglesnap.CameraPosEntry; 5 | import net.minecraft.client.render.Camera; 6 | import net.minecraft.util.math.Vec3d; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.ModifyVariable; 10 | 11 | @Mixin(Camera.class) 12 | public abstract class CameraMixin { 13 | 14 | @ModifyVariable( 15 | method = "update", 16 | at = @At("HEAD"), 17 | ordinal = 0, 18 | argsOnly = true 19 | ) 20 | private boolean modifyThirdPerson(boolean thirdPerson) { 21 | return thirdPerson || AngleSnap.currentCameraPos != null; 22 | } 23 | 24 | @ModifyVariable( 25 | method = "update", 26 | at = @At("HEAD"), 27 | ordinal = 1, 28 | argsOnly = true 29 | ) 30 | private boolean modifyInverseView(boolean inverseView) { 31 | return inverseView && AngleSnap.currentCameraPos == null; 32 | } 33 | 34 | @ModifyVariable( 35 | method = "setPos(Lnet/minecraft/util/math/Vec3d;)V", 36 | at = @At("HEAD"), 37 | argsOnly = true 38 | ) 39 | private Vec3d modifyCameraPosition(Vec3d pos) { 40 | CameraPosEntry entry = AngleSnap.currentCameraPos; 41 | if (entry != null) { 42 | return new Vec3d(entry.x, entry.y, entry.z); 43 | } 44 | return pos; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/gui/screen/IconButtonWidget.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.gui.screen; 2 | 3 | import net.minecraft.client.gl.RenderPipelines; 4 | import net.minecraft.client.gui.DrawContext; 5 | import net.minecraft.client.gui.tooltip.Tooltip; 6 | import net.minecraft.client.gui.widget.ButtonWidget; 7 | import net.minecraft.text.Text; 8 | import net.minecraft.util.Identifier; 9 | 10 | public class IconButtonWidget extends ButtonWidget { 11 | private Identifier texture; 12 | 13 | public IconButtonWidget(Text message, PressAction onPress, Identifier texture) { 14 | this(0, 0, 16, 16, message, onPress, ButtonWidget.DEFAULT_NARRATION_SUPPLIER, texture); 15 | } 16 | 17 | public IconButtonWidget(int x, int y, Text message, PressAction onPress, Identifier texture) { 18 | this(x, y, 16, 16, message, onPress, ButtonWidget.DEFAULT_NARRATION_SUPPLIER, texture); 19 | } 20 | 21 | protected IconButtonWidget(int x, int y, int width, int height, Text message, PressAction onPress, NarrationSupplier narrationSupplier, Identifier texture) { 22 | super(x, y, width, height, message, onPress, narrationSupplier); 23 | this.texture = texture; 24 | this.setTooltip(Tooltip.of(message)); 25 | } 26 | 27 | @Override 28 | protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { 29 | context.drawTexture(RenderPipelines.GUI_TEXTURED, this.texture, this.getX(), this.getY(), 0, 0, this.getWidth(), this.getHeight(), 16, 16); 30 | } 31 | 32 | public void setTexture(Identifier texture) { 33 | this.texture = texture; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/config/Option.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.config; 2 | 3 | import com.google.gson.JsonElement; 4 | import net.minecraft.client.gui.widget.ClickableWidget; 5 | import net.minecraft.text.Text; 6 | import net.minecraft.util.Language; 7 | 8 | public abstract class Option { 9 | private final String id; 10 | 11 | protected Option(String id) { 12 | this.id = id; 13 | } 14 | 15 | public final String getId() { 16 | return this.id; 17 | } 18 | 19 | public final Text getName() { 20 | return Text.translatable("anglesnap.gui.config.option." + this.getId()); 21 | } 22 | 23 | public final Text getMessage() { 24 | Language language = Language.getInstance(); 25 | String valueSpecified = "anglesnap.gui.config.option." + this.getId() + ".value." + this.getValue(); 26 | if (language.hasTranslation(valueSpecified)) { 27 | return Text.translatable(valueSpecified); 28 | } 29 | String value = "anglesnap.gui.config.option." + this.getId() + ".value"; 30 | if (language.hasTranslation(value)) { 31 | return Text.translatable(value, this.getDefaultMessage()); 32 | } 33 | return this.getDefaultMessage(); 34 | } 35 | 36 | public abstract T getValue(); 37 | 38 | public abstract void setValue(T value); 39 | 40 | public abstract boolean hasWidget(); 41 | 42 | public abstract ClickableWidget createWidget(int x, int y, int width, int height); 43 | 44 | public abstract Text getDefaultMessage(); 45 | 46 | protected abstract void fromJson(JsonElement jsonElement); 47 | 48 | protected abstract JsonElement toJson(); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/config/BooleanOption.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.config; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonPrimitive; 5 | import net.minecraft.client.gui.widget.ButtonWidget; 6 | import net.minecraft.client.gui.widget.ClickableWidget; 7 | import net.minecraft.screen.ScreenTexts; 8 | import net.minecraft.text.Text; 9 | 10 | public class BooleanOption extends Option { 11 | private boolean value; 12 | 13 | protected BooleanOption(String id, boolean defaultValue) { 14 | super(id); 15 | this.setValue(defaultValue); 16 | } 17 | 18 | @Override 19 | public Boolean getValue() { 20 | return this.value; 21 | } 22 | 23 | @Override 24 | public void setValue(Boolean value) { 25 | this.value = value; 26 | } 27 | 28 | @Override 29 | public boolean hasWidget() { 30 | return true; 31 | } 32 | 33 | @Override 34 | public ClickableWidget createWidget(int x, int y, int width, int height) { 35 | return ButtonWidget.builder(this.getMessage(), button -> { 36 | this.setValue(!this.getValue()); 37 | button.setMessage(BooleanOption.this.getMessage()); 38 | }).dimensions(x, y, width, height).build(); 39 | } 40 | 41 | @Override 42 | public Text getDefaultMessage() { 43 | return ScreenTexts.onOrOff(this.getValue()); 44 | } 45 | 46 | @Override 47 | protected void fromJson(JsonElement jsonElement) { 48 | this.setValue(jsonElement.getAsBoolean()); 49 | } 50 | 51 | @Override 52 | protected JsonElement toJson() { 53 | return new JsonPrimitive(this.getValue()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/mixin/MinecraftClientMixin.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.mixin; 2 | 3 | import me.contaria.anglesnap.AngleSnap; 4 | import me.contaria.anglesnap.gui.camerasnap.CameraSnapScreen; 5 | import me.contaria.anglesnap.gui.screen.AngleSnapScreen; 6 | import net.minecraft.client.MinecraftClient; 7 | import net.minecraft.client.gui.screen.Screen; 8 | import org.jetbrains.annotations.Nullable; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.injection.At; 12 | import org.spongepowered.asm.mixin.injection.Inject; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | 15 | @Mixin(MinecraftClient.class) 16 | public abstract class MinecraftClientMixin { 17 | @Shadow 18 | @Nullable 19 | public Screen currentScreen; 20 | 21 | @Shadow 22 | public abstract void setScreen(@Nullable Screen screen); 23 | 24 | @Inject( 25 | method = "handleInputEvents", 26 | at = @At("TAIL") 27 | ) 28 | private void openMenu(CallbackInfo ci) { 29 | while (AngleSnap.openMenu.wasPressed()) { 30 | if (AngleSnap.CONFIG.hasAngles() && this.currentScreen == null) { 31 | this.setScreen(AngleSnapScreen.create(null)); 32 | } 33 | } 34 | while (AngleSnap.cameraPositions.wasPressed()) { 35 | if (AngleSnap.currentCameraPos == null) { 36 | if (AngleSnap.CONFIG.hasCameraPositions() && this.currentScreen == null) { 37 | this.setScreen(CameraSnapScreen.create(null)); 38 | } 39 | } else { 40 | AngleSnap.currentCameraPos = null; 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/gui/warning/AngleSnapWarningScreen.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.gui.warning; 2 | 3 | import net.minecraft.client.gui.screen.WarningScreen; 4 | import net.minecraft.client.gui.widget.ButtonWidget; 5 | import net.minecraft.client.gui.widget.DirectionalLayoutWidget; 6 | import net.minecraft.client.gui.widget.LayoutWidget; 7 | import net.minecraft.screen.ScreenTexts; 8 | import net.minecraft.text.Text; 9 | 10 | import java.util.function.Consumer; 11 | 12 | public class AngleSnapWarningScreen extends WarningScreen { 13 | private final Consumer onConfirm; 14 | private final Runnable onCancel; 15 | 16 | private AngleSnapWarningScreen(Text header, Text message, Text checkbox, Text narration, Consumer onConfirm, Runnable onCancel) { 17 | super(header, message, checkbox, narration); 18 | this.onConfirm = onConfirm; 19 | this.onCancel = onCancel; 20 | } 21 | 22 | @Override 23 | protected LayoutWidget getLayout() { 24 | DirectionalLayoutWidget layout = DirectionalLayoutWidget.horizontal().spacing(8); 25 | layout.add(ButtonWidget.builder(ScreenTexts.PROCEED, button -> this.onConfirm.accept(this.checkbox != null && this.checkbox.isChecked())).build()); 26 | layout.add(ButtonWidget.builder(ScreenTexts.BACK, button -> this.onCancel.run()).build()); 27 | return layout; 28 | } 29 | 30 | public static AngleSnapWarningScreen create(Consumer onConfirm, Runnable onCancel) { 31 | Text header = Text.translatable("anglesnap.gui.warning.header"); 32 | Text message = Text.translatable("anglesnap.gui.warning.message"); 33 | Text checkbox = Text.translatable("anglesnap.gui.warning.checkbox"); 34 | return new AngleSnapWarningScreen( 35 | header, 36 | message, 37 | checkbox, 38 | header.copy().append("\n").append(message), 39 | onConfirm, 40 | onCancel 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/config/FloatOption.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.config; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonPrimitive; 5 | import net.minecraft.client.gui.widget.ClickableWidget; 6 | import net.minecraft.client.gui.widget.SliderWidget; 7 | import net.minecraft.text.Text; 8 | 9 | public class FloatOption extends Option { 10 | private final float min; 11 | private final float max; 12 | 13 | private float value; 14 | 15 | protected FloatOption(String id, float min, float max, float defaultValue) { 16 | super(id); 17 | this.min = min; 18 | this.max = max; 19 | this.setValue(defaultValue); 20 | } 21 | 22 | @Override 23 | public Float getValue() { 24 | return this.value; 25 | } 26 | 27 | @Override 28 | public void setValue(Float value) { 29 | this.value = Math.min(Math.max(value, this.min), this.max); 30 | } 31 | 32 | @Override 33 | public boolean hasWidget() { 34 | return true; 35 | } 36 | 37 | @Override 38 | public ClickableWidget createWidget(int x, int y, int width, int height) { 39 | return new SliderWidget(x, y, width, height, FloatOption.this.getMessage(), ((double) this.getValue() - this.min) / (FloatOption.this.max - FloatOption.this.min)) { 40 | @Override 41 | protected void updateMessage() { 42 | this.setMessage(FloatOption.this.getMessage()); 43 | } 44 | 45 | @Override 46 | protected void applyValue() { 47 | FloatOption.this.setValue(FloatOption.this.min + (float) ((FloatOption.this.max - FloatOption.this.min) * this.value)); 48 | } 49 | }; 50 | } 51 | 52 | @Override 53 | public Text getDefaultMessage() { 54 | return Text.literal(String.valueOf(Math.round(FloatOption.this.getValue() * 100.0f) / 100.0f)); 55 | } 56 | 57 | @Override 58 | protected void fromJson(JsonElement jsonElement) { 59 | this.setValue(jsonElement.getAsFloat()); 60 | } 61 | 62 | @Override 63 | protected JsonElement toJson() { 64 | return new JsonPrimitive(this.getValue()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/CameraPosEntry.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonPrimitive; 7 | import net.minecraft.util.JsonHelper; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class CameraPosEntry { 13 | public String name; 14 | public double x; 15 | public double y; 16 | public double z; 17 | 18 | public CameraPosEntry(double x, double y, double z) { 19 | this("", x, y, z); 20 | } 21 | 22 | public CameraPosEntry(String name, double x, double y, double z) { 23 | this.name = name; 24 | this.x = x; 25 | this.y = y; 26 | this.z = z; 27 | } 28 | 29 | public static JsonObject toJson(CameraPosEntry pos) { 30 | JsonObject jsonObject = new JsonObject(); 31 | jsonObject.add("name", new JsonPrimitive(pos.name)); 32 | jsonObject.add("x", new JsonPrimitive(pos.x)); 33 | jsonObject.add("y", new JsonPrimitive(pos.y)); 34 | jsonObject.add("z", new JsonPrimitive(pos.z)); 35 | return jsonObject; 36 | } 37 | 38 | public static CameraPosEntry fromJson(JsonObject jsonObject) { 39 | return new CameraPosEntry( 40 | JsonHelper.getString(jsonObject, "name"), 41 | JsonHelper.getDouble(jsonObject, "x"), 42 | JsonHelper.getDouble(jsonObject, "y"), 43 | JsonHelper.getDouble(jsonObject, "z") 44 | ); 45 | } 46 | 47 | public static JsonObject listToJson(List positions) { 48 | JsonObject jsonObject = new JsonObject(); 49 | JsonArray jsonArray = new JsonArray(); 50 | for (CameraPosEntry pos : positions) { 51 | jsonArray.add(toJson(pos)); 52 | } 53 | jsonObject.add("positions", jsonArray); 54 | return jsonObject; 55 | } 56 | 57 | public static List listFromJson(JsonObject jsonObject) { 58 | List positions = new ArrayList<>(); 59 | for (JsonElement pos : jsonObject.getAsJsonArray("positions")) { 60 | positions.add(fromJson(pos.getAsJsonObject())); 61 | } 62 | return positions; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/resources/assets/anglesnap/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "anglesnap.gui.screen.title": "AngleSnap Screen", 3 | "anglesnap.gui.screen.title.camerasnap": "CameraSnap Screen", 4 | "anglesnap.gui.screen.configure": "Configure...", 5 | "anglesnap.gui.screen.name": "Name", 6 | "anglesnap.gui.screen.yaw": "Yaw", 7 | "anglesnap.gui.screen.pitch": "Pitch", 8 | "anglesnap.gui.screen.icon": "Icon", 9 | "anglesnap.gui.screen.color": "RBGA", 10 | "anglesnap.gui.screen.x": "X", 11 | "anglesnap.gui.screen.y": "Y", 12 | "anglesnap.gui.screen.z": "Z", 13 | "anglesnap.gui.screen.add": "Add", 14 | "anglesnap.gui.screen.delete": "Delete", 15 | "anglesnap.gui.screen.edit": "Edit", 16 | "anglesnap.gui.screen.save": "Save", 17 | 18 | "anglesnap.gui.warning.header": "AngleSnap Multiplayer Warning", 19 | "anglesnap.gui.warning.message": "Some servers may not allow some of AngleSnaps features!\nSnapping to angles through the UI in particular can trigger some anticheat plugins.\nPlease check your servers rules and ask someone if still in doubt.", 20 | "anglesnap.gui.warning.checkbox": "Do not show this screen again", 21 | 22 | "anglesnap.gui.config.title": "AngleSnap Config", 23 | "anglesnap.gui.config.option.angleHud": "Show Precise Angle", 24 | "anglesnap.gui.config.option.markerScale": "Marker Scale", 25 | "anglesnap.gui.config.option.markerScale.value": "%sx", 26 | "anglesnap.gui.config.option.markerScale.value.0.0": "OFF", 27 | "anglesnap.gui.config.option.textScale": "Text Scale", 28 | "anglesnap.gui.config.option.textScale.value": "%sx", 29 | "anglesnap.gui.config.option.textScale.value.0.0": "OFF", 30 | "anglesnap.gui.config.option.snapToAngle": "Snap To Markers", 31 | "anglesnap.gui.config.option.snapDelay": "Snap Delay", 32 | "anglesnap.gui.config.option.snapDelay.value": "%ss", 33 | "anglesnap.gui.config.option.snapDelay.value.0.0": "Instant", 34 | "anglesnap.gui.config.option.snapLock": "Snap Lock Delay", 35 | "anglesnap.gui.config.option.snapLock.value": "%ss", 36 | "anglesnap.gui.config.option.snapLock.value.0.0": "OFF", 37 | "anglesnap.gui.config.option.snapDistance": "Snap Distance", 38 | "anglesnap.gui.config.option.snapDistance.value": "%s°", 39 | "anglesnap.gui.config.option.disableMultiplayerWarning": "Disable Multiplayer Warning", 40 | 41 | "anglesnap.key": "AngleSnap", 42 | "anglesnap.key.openmenu": "Open AngleSnap Menu", 43 | "anglesnap.key.openoverlay": "Toggle AngleSnap Overlay", 44 | "anglesnap.key.camerapositions": "Select/Toggle Camera Position", 45 | 46 | "anglesnap.link.donations": "Support Development" 47 | } -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/mixin/MouseMixin.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.mixin; 2 | 3 | import com.llamalad7.mixinextras.injector.wrapoperation.Operation; 4 | import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; 5 | import me.contaria.anglesnap.AngleEntry; 6 | import me.contaria.anglesnap.AngleSnap; 7 | import net.minecraft.client.Mouse; 8 | import net.minecraft.client.network.ClientPlayerEntity; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.Unique; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | 14 | @Mixin(Mouse.class) 15 | public abstract class MouseMixin { 16 | @Shadow 17 | private double lastTickTime; 18 | 19 | @Unique 20 | private double lastCursorMoveTime; 21 | @Unique 22 | private double lastSnapAngleTime; 23 | 24 | @WrapOperation( 25 | method = "updateMouse", 26 | at = @At( 27 | value = "INVOKE", 28 | target = "Lnet/minecraft/client/network/ClientPlayerEntity;changeLookDirection(DD)V" 29 | ) 30 | ) 31 | private void snapToAngle(ClientPlayerEntity player, double cursorDeltaX, double cursorDeltaY, Operation original) { 32 | if (this.lastSnapAngleTime + AngleSnap.CONFIG.snapLock.getValue() > this.lastTickTime) { 33 | cursorDeltaX = 0.0; 34 | cursorDeltaY = 0.0; 35 | } 36 | 37 | original.call(player, cursorDeltaX, cursorDeltaY); 38 | 39 | if (cursorDeltaX != 0.0 || cursorDeltaY != 0.0) { 40 | this.lastCursorMoveTime = this.lastTickTime; 41 | } 42 | this.snapToAngle(player); 43 | } 44 | 45 | @Unique 46 | private void snapToAngle(ClientPlayerEntity player) { 47 | if (!(AngleSnap.shouldRenderOverlay() && AngleSnap.CONFIG.snapToAngle.getValue())) { 48 | return; 49 | } 50 | // don't snap to angle if mouse was in motion within snapDelay 51 | if (this.lastCursorMoveTime + AngleSnap.CONFIG.snapDelay.getValue() >= this.lastTickTime) { 52 | return; 53 | } 54 | 55 | AngleEntry closestAngle = null; 56 | float closestDistance = AngleSnap.CONFIG.snapDistance.getValue(); 57 | for (AngleEntry angle : AngleSnap.CONFIG.getAngles()) { 58 | float distance = angle.getDistance(player.getYaw(), player.getPitch()); 59 | if (distance < closestDistance) { 60 | closestAngle = angle; 61 | closestDistance = distance; 62 | } 63 | } 64 | if (closestAngle != null && closestDistance > 0.0f) { 65 | this.lastSnapAngleTime = this.lastTickTime; 66 | closestAngle.snap(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/gui/camerasnap/CameraSnapScreen.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.gui.camerasnap; 2 | 3 | import me.contaria.anglesnap.AngleSnap; 4 | import me.contaria.anglesnap.gui.config.AngleSnapConfigScreen; 5 | import me.contaria.anglesnap.gui.screen.IconButtonWidget; 6 | import me.contaria.anglesnap.gui.warning.AngleSnapWarningScreen; 7 | import net.minecraft.client.MinecraftClient; 8 | import net.minecraft.client.gui.screen.Screen; 9 | import net.minecraft.client.gui.widget.ButtonWidget; 10 | import net.minecraft.client.gui.widget.TextWidget; 11 | import net.minecraft.screen.ScreenTexts; 12 | import net.minecraft.text.Text; 13 | import net.minecraft.util.Identifier; 14 | 15 | public class CameraSnapScreen extends Screen { 16 | private static final Text CONFIGURE_TEXT = Text.translatable("anglesnap.gui.screen.configure"); 17 | private static final Identifier CONFIGURE_TEXTURE = Identifier.of("anglesnap", "textures/gui/configure.png"); 18 | 19 | private final Screen parent; 20 | 21 | protected CameraSnapScreen(Screen parent) { 22 | super(Text.translatable("anglesnap.gui.screen.title.camerasnap")); 23 | this.parent = parent; 24 | } 25 | 26 | @Override 27 | protected void init() { 28 | this.addDrawableChild(new TextWidget(0, 10, this.width, 15, this.title, this.textRenderer).alignCenter()); 29 | this.addDrawableChild(new IconButtonWidget(this.width - 26, 10, CONFIGURE_TEXT, button -> MinecraftClient.getInstance().setScreen(new AngleSnapConfigScreen(this)), CONFIGURE_TEXTURE)); 30 | this.addDrawableChild(new CameraSnapListWidget(this.client, this.width, this.height - 70, 35)); 31 | this.addDrawableChild(ButtonWidget.builder(ScreenTexts.DONE, button -> this.close()).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build()); 32 | } 33 | 34 | @Override 35 | public void close() { 36 | MinecraftClient.getInstance().setScreen(this.parent); 37 | } 38 | 39 | @Override 40 | public void removed() { 41 | AngleSnap.CONFIG.saveAngles(); 42 | } 43 | 44 | public static Screen create(Screen parent) { 45 | if (AngleSnap.CONFIG.hasCameraPositions()) { 46 | if (AngleSnap.isInMultiplayer() && !AngleSnap.CONFIG.disableMultiplayerWarning.getValue()) { 47 | return AngleSnapWarningScreen.create( 48 | disableMultiplayerWarning -> { 49 | if (disableMultiplayerWarning) { 50 | AngleSnap.CONFIG.disableMultiplayerWarning.setValue(true); 51 | AngleSnap.CONFIG.save(); 52 | } 53 | MinecraftClient.getInstance().setScreen(new CameraSnapScreen(parent)); 54 | }, 55 | () -> MinecraftClient.getInstance().setScreen(parent) 56 | ); 57 | } 58 | return new CameraSnapScreen(parent); 59 | } 60 | return new AngleSnapConfigScreen(parent); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/gui/screen/AngleSnapScreen.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.gui.screen; 2 | 3 | import me.contaria.anglesnap.AngleEntry; 4 | import me.contaria.anglesnap.AngleSnap; 5 | import me.contaria.anglesnap.gui.config.AngleSnapConfigScreen; 6 | import me.contaria.anglesnap.gui.warning.AngleSnapWarningScreen; 7 | import net.minecraft.client.MinecraftClient; 8 | import net.minecraft.client.gui.screen.Screen; 9 | import net.minecraft.client.gui.widget.ButtonWidget; 10 | import net.minecraft.client.gui.widget.TextWidget; 11 | import net.minecraft.screen.ScreenTexts; 12 | import net.minecraft.text.Text; 13 | import net.minecraft.util.Identifier; 14 | 15 | public class AngleSnapScreen extends Screen { 16 | private static final Text CONFIGURE_TEXT = Text.translatable("anglesnap.gui.screen.configure"); 17 | private static final Identifier CONFIGURE_TEXTURE = Identifier.of("anglesnap", "textures/gui/configure.png"); 18 | 19 | private final Screen parent; 20 | 21 | protected AngleSnapScreen(Screen parent) { 22 | super(Text.translatable("anglesnap.gui.screen.title")); 23 | this.parent = parent; 24 | } 25 | 26 | @Override 27 | protected void init() { 28 | this.addDrawableChild(new TextWidget(0, 10, this.width, 15, this.title, this.textRenderer).alignCenter()); 29 | this.addDrawableChild(new IconButtonWidget(this.width - 26, 10, CONFIGURE_TEXT, button -> MinecraftClient.getInstance().setScreen(new AngleSnapConfigScreen(this)), CONFIGURE_TEXTURE)); 30 | this.addDrawableChild(new AngleSnapListWidget(this.client, this.width, this.height - 70, 35, this)); 31 | this.addDrawableChild(ButtonWidget.builder(ScreenTexts.DONE, button -> this.close()).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build()); 32 | } 33 | 34 | public void snap(AngleEntry angle) { 35 | angle.snap(); 36 | this.close(); 37 | } 38 | 39 | @Override 40 | public void close() { 41 | MinecraftClient.getInstance().setScreen(this.parent); 42 | } 43 | 44 | @Override 45 | public void removed() { 46 | AngleSnap.CONFIG.saveAngles(); 47 | } 48 | 49 | public static Screen create(Screen parent) { 50 | if (AngleSnap.CONFIG.hasAngles()) { 51 | if (AngleSnap.isInMultiplayer() && !AngleSnap.CONFIG.disableMultiplayerWarning.getValue()) { 52 | return AngleSnapWarningScreen.create( 53 | disableMultiplayerWarning -> { 54 | if (disableMultiplayerWarning) { 55 | AngleSnap.CONFIG.disableMultiplayerWarning.setValue(true); 56 | AngleSnap.CONFIG.save(); 57 | } 58 | MinecraftClient.getInstance().setScreen(new AngleSnapScreen(parent)); 59 | }, 60 | () -> MinecraftClient.getInstance().setScreen(parent) 61 | ); 62 | } 63 | return new AngleSnapScreen(parent); 64 | } 65 | return new AngleSnapConfigScreen(parent); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/AngleEntry.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonPrimitive; 7 | import net.minecraft.client.MinecraftClient; 8 | import net.minecraft.client.network.ClientPlayerEntity; 9 | import net.minecraft.util.Colors; 10 | import net.minecraft.util.Identifier; 11 | import net.minecraft.util.JsonHelper; 12 | import net.minecraft.util.math.MathHelper; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | public class AngleEntry { 18 | public String name; 19 | public float yaw; 20 | public float pitch; 21 | public int icon; 22 | public int color; 23 | 24 | public AngleEntry(float yaw, float pitch) { 25 | this("", yaw, pitch); 26 | } 27 | 28 | public AngleEntry(String name, float yaw, float pitch) { 29 | this(name, yaw, pitch, 0, Colors.RED); 30 | } 31 | 32 | public AngleEntry(String name, float yaw, float pitch, int icon, int color) { 33 | this.name = name; 34 | this.yaw = yaw; 35 | this.pitch = pitch; 36 | this.icon = icon; 37 | this.color = color; 38 | } 39 | 40 | public Identifier nextIcon() { 41 | Identifier next = this.getIcon(++this.icon); 42 | if (MinecraftClient.getInstance().getResourceManager().getResource(next).isPresent()) { 43 | return next; 44 | } 45 | return this.getIcon(this.icon = 0); 46 | } 47 | 48 | public Identifier getIcon() { 49 | return this.getIcon(this.icon); 50 | } 51 | 52 | private Identifier getIcon(int icon) { 53 | return Identifier.of("anglesnap", "textures/gui/marker-" + icon + ".png"); 54 | } 55 | 56 | public float getDistance(float yaw, float pitch) { 57 | yaw = Math.abs(MathHelper.wrapDegrees(yaw) - MathHelper.wrapDegrees(this.yaw)); 58 | pitch = Math.abs(MathHelper.wrapDegrees(pitch) - MathHelper.wrapDegrees(this.pitch)); 59 | return (float) Math.sqrt(yaw * yaw + pitch * pitch); 60 | } 61 | 62 | public void snap() { 63 | ClientPlayerEntity player = MinecraftClient.getInstance().player; 64 | if (player != null) { 65 | player.setAngles(this.yaw, this.pitch); 66 | } 67 | } 68 | 69 | public static JsonObject toJson(AngleEntry angle) { 70 | JsonObject jsonObject = new JsonObject(); 71 | jsonObject.add("name", new JsonPrimitive(angle.name)); 72 | jsonObject.add("yaw", new JsonPrimitive(angle.yaw)); 73 | jsonObject.add("pitch", new JsonPrimitive(angle.pitch)); 74 | jsonObject.add("icon", new JsonPrimitive(angle.icon)); 75 | jsonObject.add("color", new JsonPrimitive(angle.color)); 76 | return jsonObject; 77 | } 78 | 79 | public static AngleEntry fromJson(JsonObject jsonObject) { 80 | return new AngleEntry( 81 | JsonHelper.getString(jsonObject, "name"), 82 | JsonHelper.getFloat(jsonObject, "yaw"), 83 | JsonHelper.getFloat(jsonObject, "pitch"), 84 | JsonHelper.getInt(jsonObject, "icon", 0), 85 | JsonHelper.getInt(jsonObject, "color", Colors.RED) 86 | ); 87 | } 88 | 89 | public static JsonObject listToJson(List angles) { 90 | JsonObject jsonObject = new JsonObject(); 91 | JsonArray jsonArray = new JsonArray(); 92 | for (AngleEntry angle : angles) { 93 | jsonArray.add(toJson(angle)); 94 | } 95 | jsonObject.add("angles", jsonArray); 96 | return jsonObject; 97 | } 98 | 99 | public static List listFromJson(JsonObject jsonObject) { 100 | List angles = new ArrayList<>(); 101 | for (JsonElement angle : jsonObject.getAsJsonArray("angles")) { 102 | angles.add(fromJson(angle.getAsJsonObject())); 103 | } 104 | return angles; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/gui/config/AngleSnapConfigListWidget.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.gui.config; 2 | 3 | import me.contaria.anglesnap.AngleSnap; 4 | import me.contaria.anglesnap.config.Option; 5 | import net.minecraft.client.MinecraftClient; 6 | import net.minecraft.client.font.TextRenderer; 7 | import net.minecraft.client.gui.DrawContext; 8 | import net.minecraft.client.gui.Element; 9 | import net.minecraft.client.gui.Selectable; 10 | import net.minecraft.client.gui.widget.ClickableWidget; 11 | import net.minecraft.client.gui.widget.ElementListWidget; 12 | import net.minecraft.util.Colors; 13 | import net.minecraft.util.math.ColorHelper; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | public class AngleSnapConfigListWidget extends ElementListWidget { 19 | private static final int HOVERED_COLOR = ColorHelper.getArgb(100, 200, 200, 200); 20 | 21 | public AngleSnapConfigListWidget(MinecraftClient minecraftClient, int width, int height, int y) { 22 | super(minecraftClient, width, height, y, 24, 0); 23 | 24 | for (Option option : AngleSnap.CONFIG.getOptions()) { 25 | if (option.hasWidget()) { 26 | this.addEntry(new Entry(option)); 27 | } 28 | } 29 | } 30 | 31 | @Override 32 | public int getRowLeft() { 33 | return 6; 34 | } 35 | 36 | @Override 37 | public int getRowWidth() { 38 | return this.width - 12; 39 | } 40 | 41 | @Override 42 | protected int getScrollbarX() { 43 | return this.width - 6; 44 | } 45 | 46 | public abstract static class AbstractEntry extends ElementListWidget.Entry { 47 | protected final MinecraftClient client; 48 | 49 | protected AbstractEntry(MinecraftClient client) { 50 | this.client = client; 51 | } 52 | 53 | protected void renderWidgetAt(DrawContext context, int mouseX, int mouseY, float tickDelta, ClickableWidget widget, int x, int y) { 54 | widget.setX(x); 55 | widget.setY(y); 56 | widget.render(context, mouseX, mouseY, tickDelta); 57 | } 58 | } 59 | 60 | public static class Entry extends AbstractEntry { 61 | private final Option option; 62 | 63 | private final List children; 64 | private final ClickableWidget widget; 65 | 66 | public Entry(Option option) { 67 | super(MinecraftClient.getInstance()); 68 | this.option = option; 69 | this.children = new ArrayList<>(); 70 | this.widget = this.addChild(option.createWidget(0, 0, 150, 20)); 71 | } 72 | 73 | private T addChild(T widget) { 74 | this.children.add(widget); 75 | return widget; 76 | } 77 | 78 | @Override 79 | public List selectableChildren() { 80 | return this.children; 81 | } 82 | 83 | @Override 84 | public List children() { 85 | return this.children; 86 | } 87 | 88 | @Override 89 | public void setFocused(boolean focused) { 90 | super.setFocused(focused); 91 | if (!focused) { 92 | this.setFocused(null); 93 | } 94 | } 95 | 96 | @Override 97 | public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { 98 | TextRenderer textRenderer = this.client.textRenderer; 99 | if (hovered) { 100 | context.fill(x, y, x + entryWidth, y + entryHeight, HOVERED_COLOR); 101 | } 102 | int textY = y + (entryHeight - textRenderer.fontHeight + 1) / 2; 103 | context.drawText(textRenderer, this.option.getName(), x + 5, textY, Colors.WHITE, true); 104 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, this.widget, x + 155, y); 105 | } 106 | 107 | @Override 108 | public boolean mouseClicked(double mouseX, double mouseY, int button) { 109 | return super.mouseClicked(mouseX, mouseY, button); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/AngleSnap.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap; 2 | 3 | import com.mojang.logging.LogUtils; 4 | import me.contaria.anglesnap.config.AngleSnapConfig; 5 | import net.fabricmc.api.ClientModInitializer; 6 | import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; 7 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; 8 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; 9 | import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; 10 | import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; 11 | import net.minecraft.client.MinecraftClient; 12 | import net.minecraft.client.font.TextRenderer; 13 | import net.minecraft.client.gui.DrawContext; 14 | import net.minecraft.client.option.KeyBinding; 15 | import net.minecraft.client.option.StickyKeyBinding; 16 | import net.minecraft.client.render.RenderLayer; 17 | import net.minecraft.client.render.RenderTickCounter; 18 | import net.minecraft.client.render.VertexConsumer; 19 | import net.minecraft.client.util.math.MatrixStack; 20 | import net.minecraft.util.Colors; 21 | import net.minecraft.util.Identifier; 22 | import net.minecraft.util.WorldSavePath; 23 | import net.minecraft.util.math.MathHelper; 24 | import net.minecraft.util.math.Vec3d; 25 | import org.jetbrains.annotations.Nullable; 26 | import org.joml.Matrix4f; 27 | import org.joml.Quaternionf; 28 | import org.joml.Vector3f; 29 | import org.lwjgl.glfw.GLFW; 30 | import org.slf4j.Logger; 31 | 32 | import java.util.Objects; 33 | 34 | public class AngleSnap implements ClientModInitializer { 35 | public static final Logger LOGGER = LogUtils.getLogger(); 36 | public static final AngleSnapConfig CONFIG = new AngleSnapConfig(); 37 | 38 | public static KeyBinding openMenu; 39 | public static KeyBinding openOverlay; 40 | public static KeyBinding cameraPositions; 41 | 42 | @Nullable 43 | public static CameraPosEntry currentCameraPos; 44 | 45 | @Override 46 | public void onInitializeClient() { 47 | openMenu = KeyBindingHelper.registerKeyBinding(new KeyBinding( 48 | "anglesnap.key.openmenu", 49 | GLFW.GLFW_KEY_F6, 50 | "anglesnap.key" 51 | )); 52 | openOverlay = KeyBindingHelper.registerKeyBinding(new StickyKeyBinding( 53 | "anglesnap.key.openoverlay", 54 | GLFW.GLFW_KEY_F7, 55 | "anglesnap.key", 56 | () -> true 57 | )); 58 | cameraPositions = KeyBindingHelper.registerKeyBinding(new KeyBinding( 59 | "anglesnap.key.camerapositions", 60 | GLFW.GLFW_KEY_F8, 61 | "anglesnap.key" 62 | )); 63 | 64 | WorldRenderEvents.LAST.register(AngleSnap::renderOverlay); 65 | HudElementRegistry.addFirst(Identifier.of("anglesnap", "overlay"), AngleSnap::renderHud); 66 | 67 | ClientPlayConnectionEvents.JOIN.register((networkHandler, packetSender, client) -> { 68 | if (client.isIntegratedServerRunning()) { 69 | AngleSnap.CONFIG.loadAnglesAndCameraPositions(Objects.requireNonNull(client.getServer()).getSavePath(WorldSavePath.ROOT).getParent().getFileName().toString(), AngleSnapConfig.AngleFolder.SINGLEPLAYER); 70 | } else if (Objects.requireNonNull(networkHandler.getServerInfo()).isRealm()) { 71 | AngleSnap.CONFIG.loadAnglesAndCameraPositions(networkHandler.getServerInfo().name, AngleSnapConfig.AngleFolder.REALMS); 72 | } else { 73 | AngleSnap.CONFIG.loadAnglesAndCameraPositions(networkHandler.getServerInfo().address, AngleSnapConfig.AngleFolder.MULTIPLAYER); 74 | } 75 | }); 76 | ClientPlayConnectionEvents.DISCONNECT.register((networkHandler, client) -> AngleSnap.CONFIG.unloadAnglesAndCameraPositions()); 77 | } 78 | 79 | public static boolean shouldRenderOverlay() { 80 | return openOverlay.isPressed(); 81 | } 82 | 83 | private static void renderHud(DrawContext context, RenderTickCounter tickCounter) { 84 | if (shouldRenderOverlay()) { 85 | if (AngleSnap.CONFIG.angleHud.getValue()) { 86 | renderAngleHud(context); 87 | } 88 | } 89 | } 90 | 91 | private static void renderOverlay(WorldRenderContext context) { 92 | if (shouldRenderOverlay()) { 93 | for (AngleEntry angle : AngleSnap.CONFIG.getAngles()) { 94 | renderMarker(context, angle, AngleSnap.CONFIG.markerScale.getValue(), AngleSnap.CONFIG.textScale.getValue()); 95 | } 96 | } 97 | } 98 | 99 | private static void renderMarker(WorldRenderContext context, AngleEntry angle, float markerScale, float textScale) { 100 | markerScale = markerScale / 10.0f; 101 | textScale = textScale / 50.0f; 102 | 103 | Vector3f pos = Vec3d.fromPolar( 104 | MathHelper.wrapDegrees(angle.pitch), 105 | MathHelper.wrapDegrees(angle.yaw + 180.0f) 106 | ).multiply(-1.0, 1.0, -1.0).toVector3f(); 107 | Quaternionf rotation = context.camera().getRotation(); 108 | 109 | drawIcon(context, pos, rotation, angle, markerScale); 110 | if (!angle.name.isEmpty()) { 111 | drawName(context, pos, rotation, angle, textScale); 112 | } 113 | } 114 | 115 | private static void drawIcon(WorldRenderContext context, Vector3f pos, Quaternionf rotation, AngleEntry angle, float scale) { 116 | if (scale == 0.0f) { 117 | return; 118 | } 119 | 120 | MatrixStack matrices = Objects.requireNonNull(context.matrixStack()); 121 | matrices.push(); 122 | matrices.translate(pos.x(), pos.y(), pos.z()); 123 | matrices.multiply(rotation); 124 | matrices.scale(scale, -scale, scale); 125 | 126 | Matrix4f matrix4f = matrices.peek().getPositionMatrix(); 127 | MinecraftClient client = MinecraftClient.getInstance(); 128 | RenderLayer layer = RenderLayer.getCelestial(angle.getIcon()); 129 | VertexConsumer consumer = client.getBufferBuilders().getEffectVertexConsumers().getBuffer(layer); 130 | consumer.vertex(matrix4f, -1.0f, -1.0f, 0.0f).color(angle.color).texture(0.0f, 0.0f); 131 | consumer.vertex(matrix4f, -1.0f, 1.0f, 0.0f).color(angle.color).texture(0.0f, 1.0f); 132 | consumer.vertex(matrix4f, 1.0f, 1.0f, 0.0f).color(angle.color).texture(1.0f, 1.0f); 133 | consumer.vertex(matrix4f, 1.0f, -1.0f, 0.0f).color(angle.color).texture(1.0f, 0.0f); 134 | 135 | matrices.scale(1.0f / scale, 1.0f / -scale, 1.0f / scale); 136 | matrices.pop(); 137 | } 138 | 139 | private static void drawName(WorldRenderContext context, Vector3f pos, Quaternionf rotation, AngleEntry angle, float scale) { 140 | if (scale == 0.0f || angle.name.isEmpty()) { 141 | return; 142 | } 143 | 144 | MatrixStack matrices = Objects.requireNonNull(context.matrixStack()); 145 | matrices.push(); 146 | matrices.translate(pos.x(), pos.y(), pos.z()); 147 | matrices.multiply(rotation); 148 | matrices.scale(scale, -scale, scale); 149 | 150 | Matrix4f matrix4f = matrices.peek().getPositionMatrix(); 151 | MinecraftClient client = MinecraftClient.getInstance(); 152 | TextRenderer textRenderer = client.textRenderer; 153 | float x = -textRenderer.getWidth(angle.name) / 2.0f; 154 | int backgroundColor = (int) (client.options.getTextBackgroundOpacity(0.25f) * 255.0f) << 24; 155 | textRenderer.draw( 156 | angle.name, x, -15.0f, Colors.WHITE, false, matrix4f, client.getBufferBuilders().getEffectVertexConsumers(), TextRenderer.TextLayerType.SEE_THROUGH, backgroundColor, 15 157 | ); 158 | 159 | matrices.scale(1.0f / scale, 1.0f / -scale, 1.0f / scale); 160 | matrices.pop(); 161 | } 162 | 163 | private static void renderAngleHud(DrawContext context) { 164 | MinecraftClient client = MinecraftClient.getInstance(); 165 | if (client.getDebugHud().shouldShowDebugHud() || client.player == null) { 166 | return; 167 | } 168 | 169 | TextRenderer textRenderer = client.textRenderer; 170 | String text = String.format("%.3f / %.3f", MathHelper.wrapDegrees(client.player.getYaw()), MathHelper.wrapDegrees(client.player.getPitch())); 171 | context.fill(5, 5, 5 + 2 + textRenderer.getWidth(text) + 2, 5 + 2 + textRenderer.fontHeight + 2, -1873784752); 172 | context.drawText(textRenderer, text, 5 + 2 + 1, 5 + 2 + 1, -2039584, false); 173 | } 174 | 175 | public static boolean isInMultiplayer() { 176 | return MinecraftClient.getInstance().world != null && !MinecraftClient.getInstance().isInSingleplayer(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/config/AngleSnapConfig.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.config; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.stream.JsonReader; 7 | import me.contaria.anglesnap.AngleEntry; 8 | import me.contaria.anglesnap.AngleSnap; 9 | import me.contaria.anglesnap.CameraPosEntry; 10 | import net.fabricmc.loader.api.FabricLoader; 11 | import net.minecraft.client.MinecraftClient; 12 | import net.minecraft.client.network.ClientPlayerEntity; 13 | import net.minecraft.util.math.MathHelper; 14 | import net.minecraft.util.math.Vec3d; 15 | import org.jetbrains.annotations.Nullable; 16 | 17 | import java.nio.charset.StandardCharsets; 18 | import java.nio.file.Files; 19 | import java.nio.file.InvalidPathException; 20 | import java.nio.file.Path; 21 | import java.util.*; 22 | 23 | public class AngleSnapConfig { 24 | private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().serializeNulls().create(); 25 | private static final Path CONFIG_DIR = FabricLoader.getInstance().getConfigDir().resolve("anglesnap"); 26 | private static final Path CONFIG_PATH = CONFIG_DIR.resolve("anglesnap.json"); 27 | private static final Path OLD_CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("anglesnap.json"); 28 | 29 | private final Map> options; 30 | 31 | public final BooleanOption angleHud; 32 | public final FloatOption markerScale; 33 | public final FloatOption textScale; 34 | public final BooleanOption snapToAngle; 35 | public final FloatOption snapDelay; 36 | public final FloatOption snapLock; 37 | public final FloatOption snapDistance; 38 | public final BooleanOption disableMultiplayerWarning; 39 | 40 | @Nullable 41 | private Path anglesPath; 42 | @Nullable 43 | private List angles; 44 | 45 | @Nullable 46 | private Path cameraPositionsPath; 47 | @Nullable 48 | private List cameraPositions; 49 | 50 | public AngleSnapConfig() { 51 | this.options = new LinkedHashMap<>(); 52 | this.angleHud = this.register("angleHud", true); 53 | this.markerScale = this.register("markerScale", 0.0f, 1.0f, 0.2f); 54 | this.textScale = this.register("textScale", 0.0f, 1.0f, 0.2f); 55 | this.snapToAngle = this.register("snapToAngle", false); 56 | this.snapDelay = this.register("snapDelay", 0.0f, 1.0f, 0.0f); 57 | this.snapLock = this.register("snapLock", 0.0f, 1.0f, 0.25f); 58 | this.snapDistance = this.register("snapDistance", 0.0f, 10.0f, 2.5f); 59 | this.disableMultiplayerWarning = this.register("disableMultiplayerWarning", false); 60 | this.load(); 61 | this.save(); 62 | } 63 | 64 | private BooleanOption register(String id, boolean defaultValue) { 65 | return this.register(new BooleanOption(id, defaultValue)); 66 | } 67 | 68 | private FloatOption register(String id, float min, float max, float defaultValue) { 69 | return this.register(new FloatOption(id, min, max, defaultValue)); 70 | } 71 | 72 | private > T register(T option) { 73 | if (this.options.put(option.getId(), option) != null) { 74 | throw new IllegalStateException("Tried to register option '" + option.getId() + "' twice!"); 75 | } 76 | return option; 77 | } 78 | 79 | public void loadAnglesAndCameraPositions(String name, AngleFolder folder) { 80 | Path directory = this.resolveDirectory(name, folder); 81 | this.loadAngles(directory); 82 | this.loadCameraPositions(directory); 83 | } 84 | 85 | public void unloadAnglesAndCameraPositions() { 86 | this.unloadAngles(); 87 | this.unloadCameraPositions(); 88 | } 89 | 90 | private Path resolveDirectory(String name, AngleFolder folder) { 91 | Path path = CONFIG_DIR.resolve(folder.getPath()); 92 | try { 93 | return path.resolve(name); 94 | } catch (InvalidPathException e) { 95 | return path.resolve(name.replaceAll("[^a-zA-Z0-9-_. ]", "_")); 96 | } 97 | } 98 | 99 | public void loadAngles(Path directory) { 100 | this.anglesPath = directory.resolve("angles.json"); 101 | this.loadAngles(); 102 | this.saveAngles(); 103 | } 104 | 105 | public void unloadAngles() { 106 | this.saveAngles(); 107 | this.anglesPath = null; 108 | this.angles = null; 109 | } 110 | 111 | private void loadAngles() { 112 | if (this.anglesPath == null) { 113 | return; 114 | } 115 | AngleSnap.LOGGER.info("[AngleSnap] Loading angles file..."); 116 | try { 117 | if (Files.exists(this.anglesPath)) { 118 | try (JsonReader reader = GSON.newJsonReader(Files.newBufferedReader(this.anglesPath, StandardCharsets.UTF_8))) { 119 | this.angles = AngleEntry.listFromJson(GSON.fromJson(reader, JsonObject.class)); 120 | } 121 | } else { 122 | this.angles = new ArrayList<>(); 123 | } 124 | } catch (Exception e) { 125 | AngleSnap.LOGGER.error("[AngleSnap] Failed to read angles file at '{}'!", this.anglesPath, e); 126 | this.angles = new ArrayList<>(); 127 | } 128 | } 129 | 130 | public void saveAngles() { 131 | if (this.anglesPath == null || this.angles == null) { 132 | return; 133 | } 134 | AngleSnap.LOGGER.info("[AngleSnap] Writing angles file..."); 135 | try { 136 | if (this.angles.isEmpty()) { 137 | Files.deleteIfExists(this.anglesPath); 138 | Files.deleteIfExists(this.anglesPath.getParent()); 139 | } else { 140 | Files.createDirectories(this.anglesPath.getParent()); 141 | Files.writeString(this.anglesPath, GSON.toJson(AngleEntry.listToJson(this.angles))); 142 | } 143 | } catch (Exception e) { 144 | AngleSnap.LOGGER.error("[AngleSnap] Failed to write angles file at '{}'!", this.anglesPath, e); 145 | } 146 | } 147 | 148 | public void loadCameraPositions(Path directory) { 149 | this.cameraPositionsPath = directory.resolve("camera_positions.json"); 150 | this.loadCameraPositions(); 151 | this.saveCameraPositions(); 152 | } 153 | 154 | public void unloadCameraPositions() { 155 | AngleSnap.currentCameraPos = null; 156 | this.saveCameraPositions(); 157 | this.cameraPositionsPath = null; 158 | this.cameraPositions = null; 159 | } 160 | 161 | private void loadCameraPositions() { 162 | if (this.cameraPositionsPath == null) { 163 | return; 164 | } 165 | AngleSnap.LOGGER.info("[AngleSnap] Loading camera positions file..."); 166 | try { 167 | if (Files.exists(this.cameraPositionsPath)) { 168 | try (JsonReader reader = GSON.newJsonReader(Files.newBufferedReader(this.cameraPositionsPath, StandardCharsets.UTF_8))) { 169 | this.cameraPositions = CameraPosEntry.listFromJson(GSON.fromJson(reader, JsonObject.class)); 170 | } 171 | } else { 172 | this.cameraPositions = new ArrayList<>(); 173 | } 174 | } catch (Exception e) { 175 | AngleSnap.LOGGER.error("[AngleSnap] Failed to read camera positions file at '{}'!", this.cameraPositionsPath, e); 176 | this.cameraPositions = new ArrayList<>(); 177 | } 178 | } 179 | 180 | public void saveCameraPositions() { 181 | if (this.cameraPositionsPath == null || this.cameraPositions == null) { 182 | return; 183 | } 184 | AngleSnap.LOGGER.info("[AngleSnap] Writing camera positions file..."); 185 | try { 186 | if (this.cameraPositions.isEmpty()) { 187 | Files.deleteIfExists(this.cameraPositionsPath); 188 | Files.deleteIfExists(this.cameraPositionsPath.getParent()); 189 | } else { 190 | Files.createDirectories(this.cameraPositionsPath.getParent()); 191 | Files.writeString(this.cameraPositionsPath, GSON.toJson(CameraPosEntry.listToJson(this.cameraPositions))); 192 | } 193 | } catch (Exception e) { 194 | AngleSnap.LOGGER.error("[AngleSnap] Failed to write camera positions file at '{}'!", this.cameraPositionsPath, e); 195 | } 196 | } 197 | 198 | public void load() { 199 | AngleSnap.LOGGER.info("[AngleSnap] Loading config file..."); 200 | try { 201 | if (Files.exists(OLD_CONFIG_PATH)) { 202 | if (!Files.exists(CONFIG_PATH)) { 203 | Files.createDirectories(CONFIG_DIR); 204 | Files.copy(OLD_CONFIG_PATH, CONFIG_PATH); 205 | } 206 | Files.delete(OLD_CONFIG_PATH); 207 | } 208 | if (Files.exists(CONFIG_PATH)) { 209 | try (JsonReader reader = GSON.newJsonReader(Files.newBufferedReader(CONFIG_PATH, StandardCharsets.UTF_8))) { 210 | this.fromJson(GSON.fromJson(reader, JsonObject.class)); 211 | } 212 | } 213 | } catch (Exception e) { 214 | AngleSnap.LOGGER.error("[AngleSnap] Failed to read config file!", e); 215 | } 216 | } 217 | 218 | private void fromJson(JsonObject jsonObject) { 219 | for (String key : jsonObject.keySet()) { 220 | if (this.options.containsKey(key)) { 221 | this.options.get(key).fromJson(jsonObject.get(key)); 222 | } 223 | } 224 | } 225 | 226 | public void save() { 227 | AngleSnap.LOGGER.info("[AngleSnap] Writing config file..."); 228 | try { 229 | Files.createDirectories(CONFIG_DIR); 230 | Files.writeString(CONFIG_PATH, GSON.toJson(this.toJson())); 231 | } catch (Exception e) { 232 | AngleSnap.LOGGER.error("[AngleSnap] Failed to write config file!", e); 233 | } 234 | } 235 | 236 | private JsonObject toJson() { 237 | JsonObject jsonObject = new JsonObject(); 238 | for (Option option : this.options.values()) { 239 | jsonObject.add(option.getId(), option.toJson()); 240 | } 241 | return jsonObject; 242 | } 243 | 244 | public boolean hasAngles() { 245 | return this.angles != null; 246 | } 247 | 248 | public List getAngles() { 249 | return this.angles != null ? Collections.unmodifiableList(this.angles) : Collections.emptyList(); 250 | } 251 | 252 | public void removeAngle(AngleEntry angle) { 253 | if (this.angles == null) { 254 | AngleSnap.LOGGER.warn("[AngleSnap] Tried to remove angle but no angles are currently loaded!"); 255 | return; 256 | } 257 | this.angles.remove(angle); 258 | } 259 | 260 | public AngleEntry createAngle() { 261 | if (this.angles == null) { 262 | AngleSnap.LOGGER.warn("[AngleSnap] Tried to create angle but no angles are currently loaded!"); 263 | return null; 264 | } 265 | ClientPlayerEntity player = MinecraftClient.getInstance().player; 266 | AngleEntry angle = new AngleEntry( 267 | player != null ? (int) (MathHelper.wrapDegrees(player.getYaw()) * 10.0f) / 10.0f : 0.0f, 268 | player != null ? (int) (MathHelper.wrapDegrees(player.getPitch()) * 10.0f) / 10.0f : 0.0f 269 | ); 270 | this.angles.add(angle); 271 | return angle; 272 | } 273 | 274 | public boolean hasCameraPositions() { 275 | return this.cameraPositions != null; 276 | } 277 | 278 | public List getCameraPositions() { 279 | return this.cameraPositions != null ? Collections.unmodifiableList(this.cameraPositions) : Collections.emptyList(); 280 | } 281 | 282 | public void removeCameraPosition(CameraPosEntry pos) { 283 | if (this.cameraPositions == null) { 284 | AngleSnap.LOGGER.warn("[AngleSnap] Tried to remove camera position but no positions are currently loaded!"); 285 | return; 286 | } 287 | this.cameraPositions.remove(pos); 288 | } 289 | 290 | public CameraPosEntry createCameraPosition() { 291 | if (this.cameraPositions == null) { 292 | AngleSnap.LOGGER.warn("[AngleSnap] Tried to create camera position but no positions are currently loaded!"); 293 | return null; 294 | } 295 | Vec3d cameraPos = MinecraftClient.getInstance().gameRenderer.getCamera().getPos(); 296 | CameraPosEntry pos = new CameraPosEntry( 297 | (int) (cameraPos.getX() * 100.0) / 100.0, 298 | (int) (cameraPos.getY() * 100.0) / 100.0, 299 | (int) (cameraPos.getZ() * 100.0) / 100.0 300 | ); 301 | this.cameraPositions.add(pos); 302 | return pos; 303 | } 304 | 305 | public Iterable> getOptions() { 306 | return this.options.values(); 307 | } 308 | 309 | public enum AngleFolder { 310 | SINGLEPLAYER("singleplayer"), 311 | REALMS("realms"), 312 | MULTIPLAYER("multiplayer"); 313 | 314 | private final String path; 315 | 316 | AngleFolder(String path) { 317 | this.path = path; 318 | } 319 | 320 | public String getPath() { 321 | return this.path; 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/gui/camerasnap/CameraSnapListWidget.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.gui.camerasnap; 2 | 3 | import me.contaria.anglesnap.AngleSnap; 4 | import me.contaria.anglesnap.CameraPosEntry; 5 | import me.contaria.anglesnap.gui.screen.IconButtonWidget; 6 | import net.minecraft.client.MinecraftClient; 7 | import net.minecraft.client.font.TextRenderer; 8 | import net.minecraft.client.gui.DrawContext; 9 | import net.minecraft.client.gui.Element; 10 | import net.minecraft.client.gui.Selectable; 11 | import net.minecraft.client.gui.widget.ButtonWidget; 12 | import net.minecraft.client.gui.widget.ClickableWidget; 13 | import net.minecraft.client.gui.widget.ElementListWidget; 14 | import net.minecraft.client.gui.widget.TextFieldWidget; 15 | import net.minecraft.text.Text; 16 | import net.minecraft.util.Colors; 17 | import net.minecraft.util.Identifier; 18 | import net.minecraft.util.math.ColorHelper; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | public class CameraSnapListWidget extends ElementListWidget { 24 | private static final Text NAME_TEXT = Text.translatable("anglesnap.gui.screen.name"); 25 | private static final Text X_TEXT = Text.translatable("anglesnap.gui.screen.x"); 26 | private static final Text Y_TEXT = Text.translatable("anglesnap.gui.screen.y"); 27 | private static final Text Z_TEXT = Text.translatable("anglesnap.gui.screen.z"); 28 | private static final Text ADD_TEXT = Text.translatable("anglesnap.gui.screen.add"); 29 | private static final Text DELETE_TEXT = Text.translatable("anglesnap.gui.screen.delete"); 30 | private static final Text EDIT_TEXT = Text.translatable("anglesnap.gui.screen.edit"); 31 | private static final Text SAVE_TEXT = Text.translatable("anglesnap.gui.screen.save"); 32 | 33 | private static final Identifier ADD_TEXTURE = Identifier.of("anglesnap", "textures/gui/add.png"); 34 | private static final Identifier DELETE_TEXTURE = Identifier.of("anglesnap", "textures/gui/delete.png"); 35 | private static final Identifier EDIT_TEXTURE = Identifier.of("anglesnap", "textures/gui/edit.png"); 36 | private static final Identifier SAVE_TEXTURE = Identifier.of("anglesnap", "textures/gui/save.png"); 37 | 38 | private static final int HOVERED_COLOR = ColorHelper.getArgb(100, 200, 200, 200); 39 | 40 | public CameraSnapListWidget(MinecraftClient minecraftClient, int width, int height, int y) { 41 | super(minecraftClient, width, height, y, 20, 25); 42 | 43 | for (CameraPosEntry pos : AngleSnap.CONFIG.getCameraPositions()) { 44 | this.addEntry(new Entry(pos)); 45 | } 46 | this.addEntry(new AddAngleEntry()); 47 | } 48 | 49 | @Override 50 | public int getRowLeft() { 51 | return 6; 52 | } 53 | 54 | @Override 55 | public int getRowWidth() { 56 | return this.width - 12; 57 | } 58 | 59 | @Override 60 | protected int getScrollbarX() { 61 | return this.width - 6; 62 | } 63 | 64 | @Override 65 | protected void renderHeader(DrawContext context, int x, int y) { 66 | TextRenderer textRenderer = this.client.textRenderer; 67 | context.drawText(textRenderer, NAME_TEXT, x + 5, y + (20 - textRenderer.fontHeight) / 2, Colors.WHITE, true); 68 | context.drawText(textRenderer, X_TEXT, x + 5 + 5 * this.getRowWidth() / 17, y + (20 - textRenderer.fontHeight) / 2, Colors.WHITE, true); 69 | context.drawText(textRenderer, Y_TEXT, x + 5 + 7 * this.getRowWidth() / 17, y + (20 - textRenderer.fontHeight) / 2, Colors.WHITE, true); 70 | context.drawText(textRenderer, Z_TEXT, x + 5 + 9 * this.getRowWidth() / 17, y + (20 - textRenderer.fontHeight) / 2, Colors.WHITE, true); 71 | } 72 | 73 | public abstract static class AbstractEntry extends ElementListWidget.Entry { 74 | protected final MinecraftClient client; 75 | protected final List children; 76 | 77 | protected AbstractEntry() { 78 | this.client = MinecraftClient.getInstance(); 79 | this.children = new ArrayList<>(); 80 | } 81 | 82 | protected T addChild(T widget) { 83 | this.children.add(widget); 84 | return widget; 85 | } 86 | 87 | @Override 88 | public List selectableChildren() { 89 | return this.children; 90 | } 91 | 92 | @Override 93 | public List children() { 94 | return this.children; 95 | } 96 | 97 | @Override 98 | public void setFocused(boolean focused) { 99 | super.setFocused(focused); 100 | if (!focused) { 101 | this.setFocused(null); 102 | } 103 | } 104 | 105 | protected void renderWidgetAt(DrawContext context, int mouseX, int mouseY, float tickDelta, ClickableWidget widget, int x, int y) { 106 | widget.setX(x); 107 | widget.setY(y); 108 | widget.render(context, mouseX, mouseY, tickDelta); 109 | } 110 | } 111 | 112 | public class Entry extends AbstractEntry { 113 | private final CameraPosEntry pos; 114 | 115 | private final TextFieldWidget name; 116 | private final TextFieldWidget x; 117 | private final TextFieldWidget y; 118 | private final TextFieldWidget z; 119 | private final IconButtonWidget edit; 120 | private final IconButtonWidget save; 121 | private final IconButtonWidget delete; 122 | 123 | private boolean editing; 124 | 125 | public Entry() { 126 | this(AngleSnap.CONFIG.createCameraPosition()); 127 | this.setEditing(true); 128 | this.setFocused(this.name); 129 | } 130 | 131 | public Entry(CameraPosEntry pos) { 132 | this.pos = pos; 133 | 134 | this.name = this.addChild(new TextFieldWidget( 135 | this.client.textRenderer, 136 | 5 * CameraSnapListWidget.this.getRowWidth() / 17 - 5, 137 | 20, 138 | NAME_TEXT 139 | )); 140 | this.name.setText(this.pos.name); 141 | this.name.setChangedListener(name -> this.pos.name = name); 142 | this.name.setDrawsBackground(false); 143 | this.name.setEditableColor(Colors.WHITE); 144 | this.name.setUneditableColor(Colors.WHITE); 145 | 146 | this.x = this.addChild(new TextFieldWidget( 147 | this.client.textRenderer, 148 | 2 * CameraSnapListWidget.this.getRowWidth() / 17 - 5, 149 | 20, 150 | X_TEXT 151 | )); 152 | this.x.setText(String.valueOf(this.pos.x)); 153 | this.x.setChangedListener(yaw -> { 154 | try { 155 | this.pos.x = Double.parseDouble(yaw); 156 | } catch (NumberFormatException e) { 157 | this.pos.x = 0.0; 158 | } 159 | }); 160 | this.x.setDrawsBackground(false); 161 | this.x.setEditableColor(Colors.WHITE); 162 | this.x.setUneditableColor(Colors.WHITE); 163 | 164 | this.y = this.addChild(new TextFieldWidget( 165 | this.client.textRenderer, 166 | 2 * CameraSnapListWidget.this.getRowWidth() / 17 - 5, 167 | 20, 168 | Y_TEXT 169 | )); 170 | this.y.setText(String.valueOf(this.pos.y)); 171 | this.y.setChangedListener(yaw -> { 172 | try { 173 | this.pos.y = Double.parseDouble(yaw); 174 | } catch (NumberFormatException e) { 175 | this.pos.y = 0.0; 176 | } 177 | }); 178 | this.y.setDrawsBackground(false); 179 | this.y.setEditableColor(Colors.WHITE); 180 | this.y.setUneditableColor(Colors.WHITE); 181 | 182 | this.z = this.addChild(new TextFieldWidget( 183 | this.client.textRenderer, 184 | 2 * CameraSnapListWidget.this.getRowWidth() / 17 - 5, 185 | 20, 186 | Z_TEXT 187 | )); 188 | this.z.setText(String.valueOf(this.pos.z)); 189 | this.z.setChangedListener(yaw -> { 190 | try { 191 | this.pos.z = Double.parseDouble(yaw); 192 | } catch (NumberFormatException e) { 193 | this.pos.z = 0.0; 194 | } 195 | }); 196 | this.z.setDrawsBackground(false); 197 | this.z.setEditableColor(Colors.WHITE); 198 | this.z.setUneditableColor(Colors.WHITE); 199 | 200 | this.edit = this.addChild(new IconButtonWidget(EDIT_TEXT, button -> this.toggleEditing(), EDIT_TEXTURE)); 201 | this.save = this.addChild(new IconButtonWidget(SAVE_TEXT, button -> this.toggleEditing(), SAVE_TEXTURE)); 202 | this.delete = this.addChild(new IconButtonWidget(DELETE_TEXT, button -> this.delete(), DELETE_TEXTURE)); 203 | 204 | this.setEditing(false); 205 | } 206 | 207 | private void toggleEditing() { 208 | this.setEditing(!this.editing); 209 | } 210 | 211 | private void setEditing(boolean editing) { 212 | this.editing = editing; 213 | 214 | this.setEditing(this.name, editing); 215 | this.setEditing(this.x, editing); 216 | this.setEditing(this.y, editing); 217 | this.setEditing(this.z, editing); 218 | 219 | if (!editing) { 220 | this.x.setText(String.valueOf(this.pos.x)); 221 | this.y.setText(String.valueOf(this.pos.y)); 222 | this.z.setText(String.valueOf(this.pos.z)); 223 | } 224 | 225 | this.edit.visible = !editing; 226 | this.save.visible = editing; 227 | } 228 | 229 | private void setEditing(TextFieldWidget widget, boolean editing) { 230 | widget.active = editing; 231 | widget.setEditable(editing); 232 | widget.setFocused(false); 233 | widget.setFocusUnlocked(editing); 234 | widget.setCursorToStart(false); 235 | } 236 | 237 | private void delete() { 238 | AngleSnap.CONFIG.removeCameraPosition(this.pos); 239 | CameraSnapListWidget.this.removeEntry(this); 240 | } 241 | 242 | @Override 243 | public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { 244 | if (hovered) { 245 | context.fill(x, y, x + entryWidth, y + entryHeight, HOVERED_COLOR); 246 | } 247 | int textY = y + (entryHeight - this.client.textRenderer.fontHeight + 1) / 2; 248 | this.renderTextWidgetAt(context, mouseX, mouseY, tickDelta, this.name, x + 5, textY); 249 | this.renderNumberWidgetAt(context, mouseX, mouseY, tickDelta, this.x, x + 5 + 5 * entryWidth / 17, textY); 250 | this.renderNumberWidgetAt(context, mouseX, mouseY, tickDelta, this.y, x + 5 + 7 * entryWidth / 17, textY); 251 | this.renderNumberWidgetAt(context, mouseX, mouseY, tickDelta, this.z, x + 5 + 9 * entryWidth / 17, textY); 252 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, this.edit, x + entryWidth - 5 - 40, y); 253 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, this.save, x + entryWidth - 5 - 40, y); 254 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, this.delete, x + entryWidth - 5 - 20, y); 255 | } 256 | 257 | private void renderTextWidgetAt(DrawContext context, int mouseX, int mouseY, float tickDelta, TextFieldWidget widget, int x, int y) { 258 | if (this.editing) { 259 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, widget, x, y); 260 | } else { 261 | context.drawText(this.client.textRenderer, widget.getText(), x, y, Colors.WHITE, true); 262 | } 263 | } 264 | 265 | private void renderNumberWidgetAt(DrawContext context, int mouseX, int mouseY, float tickDelta, TextFieldWidget widget, int x, int y) { 266 | if (this.isNumberOrEmpty(widget.getText())) { 267 | this.renderTextWidgetAt(context, mouseX, mouseY, tickDelta, widget, x, y); 268 | } else { 269 | widget.setEditableColor(Colors.LIGHT_RED); 270 | widget.setUneditableColor(Colors.LIGHT_RED); 271 | this.renderTextWidgetAt(context, mouseX, mouseY, tickDelta, widget, x, y); 272 | widget.setEditableColor(Colors.WHITE); 273 | widget.setUneditableColor(Colors.WHITE); 274 | } 275 | } 276 | 277 | private boolean isNumberOrEmpty(String text) { 278 | if (text.isEmpty()) { 279 | return true; 280 | } 281 | try { 282 | Float.parseFloat(text); 283 | return true; 284 | } catch (NumberFormatException e) { 285 | return false; 286 | } 287 | } 288 | 289 | @Override 290 | public boolean mouseClicked(double mouseX, double mouseY, int button) { 291 | if (super.mouseClicked(mouseX, mouseY, button)) { 292 | return true; 293 | } 294 | if (!this.editing && button == 0) { 295 | AngleSnap.currentCameraPos = this.pos; 296 | return true; 297 | } 298 | return false; 299 | } 300 | } 301 | 302 | public class AddAngleEntry extends AbstractEntry { 303 | private final ButtonWidget add; 304 | 305 | public AddAngleEntry() { 306 | this.add = this.addChild(new IconButtonWidget(ADD_TEXT, button -> this.add(), ADD_TEXTURE)); 307 | } 308 | 309 | @Override 310 | public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { 311 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, this.add, x + 5, y + (entryHeight - this.client.textRenderer.fontHeight) / 2); 312 | } 313 | 314 | private void add() { 315 | Entry entry = new Entry(); 316 | CameraSnapListWidget.this.removeEntry(this); 317 | CameraSnapListWidget.this.addEntry(entry); 318 | CameraSnapListWidget.this.addEntry(this); 319 | CameraSnapListWidget.this.setFocused(entry); 320 | } 321 | 322 | @Override 323 | public boolean mouseClicked(double mouseX, double mouseY, int button) { 324 | super.mouseClicked(mouseX, mouseY, button); 325 | return false; 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/main/java/me/contaria/anglesnap/gui/screen/AngleSnapListWidget.java: -------------------------------------------------------------------------------- 1 | package me.contaria.anglesnap.gui.screen; 2 | 3 | import me.contaria.anglesnap.AngleEntry; 4 | import me.contaria.anglesnap.AngleSnap; 5 | import net.minecraft.client.MinecraftClient; 6 | import net.minecraft.client.font.TextRenderer; 7 | import net.minecraft.client.gui.DrawContext; 8 | import net.minecraft.client.gui.Element; 9 | import net.minecraft.client.gui.Selectable; 10 | import net.minecraft.client.gui.widget.ButtonWidget; 11 | import net.minecraft.client.gui.widget.ClickableWidget; 12 | import net.minecraft.client.gui.widget.ElementListWidget; 13 | import net.minecraft.client.gui.widget.TextFieldWidget; 14 | import net.minecraft.text.Text; 15 | import net.minecraft.util.Colors; 16 | import net.minecraft.util.Identifier; 17 | import net.minecraft.util.math.ColorHelper; 18 | import org.apache.commons.lang3.StringUtils; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.Locale; 23 | 24 | public class AngleSnapListWidget extends ElementListWidget { 25 | private static final Text NAME_TEXT = Text.translatable("anglesnap.gui.screen.name"); 26 | private static final Text YAW_TEXT = Text.translatable("anglesnap.gui.screen.yaw"); 27 | private static final Text PITCH_TEXT = Text.translatable("anglesnap.gui.screen.pitch"); 28 | private static final Text ICON_TEXT = Text.translatable("anglesnap.gui.screen.icon"); 29 | private static final Text COLOR_TEXT = Text.translatable("anglesnap.gui.screen.color"); 30 | private static final Text ADD_TEXT = Text.translatable("anglesnap.gui.screen.add"); 31 | private static final Text DELETE_TEXT = Text.translatable("anglesnap.gui.screen.delete"); 32 | private static final Text EDIT_TEXT = Text.translatable("anglesnap.gui.screen.edit"); 33 | private static final Text SAVE_TEXT = Text.translatable("anglesnap.gui.screen.save"); 34 | 35 | private static final Identifier ADD_TEXTURE = Identifier.of("anglesnap", "textures/gui/add.png"); 36 | private static final Identifier DELETE_TEXTURE = Identifier.of("anglesnap", "textures/gui/delete.png"); 37 | private static final Identifier EDIT_TEXTURE = Identifier.of("anglesnap", "textures/gui/edit.png"); 38 | private static final Identifier SAVE_TEXTURE = Identifier.of("anglesnap", "textures/gui/save.png"); 39 | 40 | private static final int HOVERED_COLOR = ColorHelper.getArgb(100, 200, 200, 200); 41 | 42 | private final AngleSnapScreen parent; 43 | 44 | public AngleSnapListWidget(MinecraftClient minecraftClient, int width, int height, int y, AngleSnapScreen parent) { 45 | super(minecraftClient, width, height, y, 20, 25); 46 | this.parent = parent; 47 | 48 | for (AngleEntry angle : AngleSnap.CONFIG.getAngles()) { 49 | this.addEntry(new Entry(angle)); 50 | } 51 | this.addEntry(new AddAngleEntry()); 52 | } 53 | 54 | @Override 55 | public int getRowLeft() { 56 | return 6; 57 | } 58 | 59 | @Override 60 | public int getRowWidth() { 61 | return this.width - 12; 62 | } 63 | 64 | @Override 65 | protected int getScrollbarX() { 66 | return this.width - 6; 67 | } 68 | 69 | @Override 70 | protected void renderHeader(DrawContext context, int x, int y) { 71 | TextRenderer textRenderer = this.client.textRenderer; 72 | context.drawText(textRenderer, NAME_TEXT, x + 5, y + (20 - textRenderer.fontHeight) / 2, Colors.WHITE, true); 73 | context.drawText(textRenderer, YAW_TEXT, x + 5 + 5 * this.getRowWidth() / 17, y + (20 - textRenderer.fontHeight) / 2, Colors.WHITE, true); 74 | context.drawText(textRenderer, PITCH_TEXT, x + 5 + 7 * this.getRowWidth() / 17, y + (20 - textRenderer.fontHeight) / 2, Colors.WHITE, true); 75 | context.drawText(textRenderer, ICON_TEXT, x + 5 + 9 * this.getRowWidth() / 17, y + (20 - textRenderer.fontHeight) / 2, Colors.WHITE, true); 76 | context.drawText(textRenderer, COLOR_TEXT, x + 5 + 11 * this.getRowWidth() / 17, y + (20 - textRenderer.fontHeight) / 2, Colors.WHITE, true); 77 | } 78 | 79 | public abstract static class AbstractEntry extends ElementListWidget.Entry { 80 | protected final MinecraftClient client; 81 | protected final List children; 82 | 83 | protected AbstractEntry() { 84 | this.client = MinecraftClient.getInstance(); 85 | this.children = new ArrayList<>(); 86 | } 87 | 88 | protected T addChild(T widget) { 89 | this.children.add(widget); 90 | return widget; 91 | } 92 | 93 | @Override 94 | public List selectableChildren() { 95 | return this.children; 96 | } 97 | 98 | @Override 99 | public List children() { 100 | return this.children; 101 | } 102 | 103 | @Override 104 | public void setFocused(boolean focused) { 105 | super.setFocused(focused); 106 | if (!focused) { 107 | this.setFocused(null); 108 | } 109 | } 110 | 111 | protected void renderWidgetAt(DrawContext context, int mouseX, int mouseY, float tickDelta, ClickableWidget widget, int x, int y) { 112 | widget.setX(x); 113 | widget.setY(y); 114 | widget.render(context, mouseX, mouseY, tickDelta); 115 | } 116 | } 117 | 118 | public class Entry extends AbstractEntry { 119 | private final AngleEntry angle; 120 | 121 | private final TextFieldWidget name; 122 | private final TextFieldWidget yaw; 123 | private final TextFieldWidget pitch; 124 | private final IconButtonWidget icon; 125 | private final TextFieldWidget color; 126 | private final IconButtonWidget edit; 127 | private final IconButtonWidget save; 128 | private final IconButtonWidget delete; 129 | 130 | private boolean editing; 131 | 132 | public Entry() { 133 | this(AngleSnap.CONFIG.createAngle()); 134 | this.setEditing(true); 135 | this.setFocused(this.name); 136 | } 137 | 138 | public Entry(AngleEntry angle) { 139 | this.angle = angle; 140 | 141 | this.name = this.addChild(new TextFieldWidget( 142 | this.client.textRenderer, 143 | 5 * AngleSnapListWidget.this.getRowWidth() / 17 - 5, 144 | 20, 145 | NAME_TEXT 146 | )); 147 | this.name.setText(this.angle.name); 148 | this.name.setChangedListener(name -> this.angle.name = name); 149 | this.name.setDrawsBackground(false); 150 | this.name.setEditableColor(Colors.WHITE); 151 | this.name.setUneditableColor(Colors.WHITE); 152 | 153 | this.yaw = this.addChild(new TextFieldWidget( 154 | this.client.textRenderer, 155 | 2 * AngleSnapListWidget.this.getRowWidth() / 17 - 5, 156 | 20, 157 | YAW_TEXT 158 | )); 159 | this.yaw.setText(String.valueOf(this.angle.yaw)); 160 | this.yaw.setChangedListener(yaw -> { 161 | try { 162 | this.angle.yaw = Float.parseFloat(yaw); 163 | } catch (NumberFormatException e) { 164 | this.angle.yaw = 0.0f; 165 | } 166 | }); 167 | this.yaw.setDrawsBackground(false); 168 | this.yaw.setEditableColor(Colors.WHITE); 169 | this.yaw.setUneditableColor(Colors.WHITE); 170 | 171 | this.pitch = this.addChild(new TextFieldWidget( 172 | this.client.textRenderer, 173 | 2 * AngleSnapListWidget.this.getRowWidth() / 17 - 5, 174 | 20, 175 | PITCH_TEXT 176 | )); 177 | this.pitch.setText(String.valueOf(this.angle.pitch)); 178 | this.pitch.setChangedListener(pitch -> { 179 | try { 180 | this.angle.pitch = Float.parseFloat(pitch); 181 | } catch (NumberFormatException e) { 182 | this.angle.pitch = 0.0f; 183 | } 184 | }); 185 | this.pitch.setDrawsBackground(false); 186 | this.pitch.setEditableColor(Colors.WHITE); 187 | this.pitch.setUneditableColor(Colors.WHITE); 188 | 189 | this.icon = this.addChild(new IconButtonWidget(Text.empty(), button -> ((IconButtonWidget) button).setTexture(this.angle.nextIcon()), this.angle.getIcon())); 190 | 191 | this.color = this.addChild(new TextFieldWidget( 192 | this.client.textRenderer, 193 | 3 * AngleSnapListWidget.this.getRowWidth() / 17 - 5, 194 | 20, 195 | PITCH_TEXT 196 | )); 197 | this.color.setText(colorToString(this.angle.color)); 198 | this.color.setChangedListener(color -> this.angle.color = this.parseColor(color)); 199 | this.color.setTextPredicate(color -> color.length() <= (color.startsWith("#") ? 9 : 8)); 200 | this.color.setDrawsBackground(false); 201 | this.color.setEditableColor(Colors.WHITE); 202 | this.color.setUneditableColor(Colors.WHITE); 203 | 204 | this.edit = this.addChild(new IconButtonWidget(EDIT_TEXT, button -> this.toggleEditing(), EDIT_TEXTURE)); 205 | this.save = this.addChild(new IconButtonWidget(SAVE_TEXT, button -> this.toggleEditing(), SAVE_TEXTURE)); 206 | this.delete = this.addChild(new IconButtonWidget(DELETE_TEXT, button -> this.delete(), DELETE_TEXTURE)); 207 | 208 | this.setEditing(false); 209 | } 210 | 211 | private String colorToString(int color) { 212 | return String.format("#%08X", Integer.rotateLeft(color, 8)); 213 | } 214 | 215 | private int parseColor(String color) { 216 | if (color.startsWith("#")) { 217 | color = color.substring(1); 218 | } 219 | color = color.toLowerCase(Locale.ROOT); 220 | try { 221 | // pad RGB bits with 0's and alpha bit with f's 222 | // this means an RGB input as generated by some websites will also work 223 | String hex = StringUtils.rightPad(StringUtils.leftPad(color, 6, '0'), 8, 'f'); 224 | return Integer.rotateRight(Integer.parseUnsignedInt(hex, 16), 8); 225 | } catch (NumberFormatException e) { 226 | return Colors.WHITE; 227 | } 228 | } 229 | 230 | private void toggleEditing() { 231 | this.setEditing(!this.editing); 232 | } 233 | 234 | private void setEditing(boolean editing) { 235 | this.editing = editing; 236 | 237 | this.setEditing(this.name, editing); 238 | this.setEditing(this.yaw, editing); 239 | this.setEditing(this.pitch, editing); 240 | this.setEditing(this.icon, editing); 241 | this.setEditing(this.color, editing); 242 | 243 | if (!editing) { 244 | this.yaw.setText(String.valueOf(this.angle.yaw)); 245 | this.pitch.setText(String.valueOf(this.angle.pitch)); 246 | this.color.setText(this.colorToString(this.angle.color)); 247 | } 248 | 249 | this.edit.visible = !editing; 250 | this.save.visible = editing; 251 | } 252 | 253 | private void setEditing(ClickableWidget widget, boolean editing) { 254 | widget.active = editing; 255 | } 256 | 257 | private void setEditing(TextFieldWidget widget, boolean editing) { 258 | widget.active = editing; 259 | widget.setEditable(editing); 260 | widget.setFocused(false); 261 | widget.setFocusUnlocked(editing); 262 | widget.setCursorToStart(false); 263 | } 264 | 265 | private void delete() { 266 | AngleSnap.CONFIG.removeAngle(this.angle); 267 | AngleSnapListWidget.this.removeEntry(this); 268 | } 269 | 270 | @Override 271 | public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { 272 | if (hovered) { 273 | context.fill(x, y, x + entryWidth, y + entryHeight, HOVERED_COLOR); 274 | } 275 | int textY = y + (entryHeight - this.client.textRenderer.fontHeight + 1) / 2; 276 | this.renderTextWidgetAt(context, mouseX, mouseY, tickDelta, this.name, x + 5, textY); 277 | this.renderNumberWidgetAt(context, mouseX, mouseY, tickDelta, this.yaw, x + 5 + 5 * entryWidth / 17, textY); 278 | this.renderNumberWidgetAt(context, mouseX, mouseY, tickDelta, this.pitch, x + 5 + 7 * entryWidth / 17, textY); 279 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, this.icon, x + 5 + 9 * entryWidth / 17, y); 280 | this.renderHexadecimalWidgetAt(context, mouseX, mouseY, tickDelta, this.color, x + 5 + 11 * entryWidth / 17, textY); 281 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, this.edit, x + entryWidth - 5 - 40, y); 282 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, this.save, x + entryWidth - 5 - 40, y); 283 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, this.delete, x + entryWidth - 5 - 20, y); 284 | } 285 | 286 | private void renderTextWidgetAt(DrawContext context, int mouseX, int mouseY, float tickDelta, TextFieldWidget widget, int x, int y) { 287 | if (this.editing) { 288 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, widget, x, y); 289 | } else { 290 | context.drawText(this.client.textRenderer, widget.getText(), x, y, Colors.WHITE, true); 291 | } 292 | } 293 | 294 | private void renderNumberWidgetAt(DrawContext context, int mouseX, int mouseY, float tickDelta, TextFieldWidget widget, int x, int y) { 295 | if (this.isNumberOrEmpty(widget.getText())) { 296 | this.renderTextWidgetAt(context, mouseX, mouseY, tickDelta, widget, x, y); 297 | } else { 298 | widget.setEditableColor(Colors.LIGHT_RED); 299 | widget.setUneditableColor(Colors.LIGHT_RED); 300 | this.renderTextWidgetAt(context, mouseX, mouseY, tickDelta, widget, x, y); 301 | widget.setEditableColor(Colors.WHITE); 302 | widget.setUneditableColor(Colors.WHITE); 303 | } 304 | } 305 | 306 | private boolean isNumberOrEmpty(String text) { 307 | if (text.isEmpty()) { 308 | return true; 309 | } 310 | try { 311 | Float.parseFloat(text); 312 | return true; 313 | } catch (NumberFormatException e) { 314 | return false; 315 | } 316 | } 317 | 318 | private void renderHexadecimalWidgetAt(DrawContext context, int mouseX, int mouseY, float tickDelta, TextFieldWidget widget, int x, int y) { 319 | if (this.isHexadecimalOrEmpty(widget.getText())) { 320 | this.renderTextWidgetAt(context, mouseX, mouseY, tickDelta, widget, x, y); 321 | } else { 322 | widget.setEditableColor(Colors.LIGHT_RED); 323 | widget.setUneditableColor(Colors.LIGHT_RED); 324 | this.renderTextWidgetAt(context, mouseX, mouseY, tickDelta, widget, x, y); 325 | widget.setEditableColor(Colors.WHITE); 326 | widget.setUneditableColor(Colors.WHITE); 327 | } 328 | } 329 | 330 | private boolean isHexadecimalOrEmpty(String text) { 331 | if (text.startsWith("#")) { 332 | text = text.substring(1); 333 | } 334 | if (text.isEmpty()) { 335 | return true; 336 | } 337 | text = text.toLowerCase(Locale.ROOT); 338 | try { 339 | // noinspection ResultOfMethodCallIgnored 340 | Integer.parseUnsignedInt(text, 16); 341 | return true; 342 | } catch (NumberFormatException e) { 343 | return false; 344 | } 345 | } 346 | 347 | @Override 348 | public boolean mouseClicked(double mouseX, double mouseY, int button) { 349 | if (super.mouseClicked(mouseX, mouseY, button)) { 350 | return true; 351 | } 352 | if (!this.editing && button == 0) { 353 | AngleSnapListWidget.this.parent.snap(this.angle); 354 | return true; 355 | } 356 | return false; 357 | } 358 | } 359 | 360 | public class AddAngleEntry extends AbstractEntry { 361 | private final ButtonWidget add; 362 | 363 | public AddAngleEntry() { 364 | this.add = this.addChild(new IconButtonWidget(ADD_TEXT, button -> this.add(), ADD_TEXTURE)); 365 | } 366 | 367 | @Override 368 | public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { 369 | this.renderWidgetAt(context, mouseX, mouseY, tickDelta, this.add, x + 5, y + (entryHeight - this.client.textRenderer.fontHeight) / 2); 370 | } 371 | 372 | private void add() { 373 | Entry entry = new Entry(); 374 | AngleSnapListWidget.this.removeEntry(this); 375 | AngleSnapListWidget.this.addEntry(entry); 376 | AngleSnapListWidget.this.addEntry(this); 377 | AngleSnapListWidget.this.setFocused(entry); 378 | } 379 | 380 | @Override 381 | public boolean mouseClicked(double mouseX, double mouseY, int button) { 382 | super.mouseClicked(mouseX, mouseY, button); 383 | return false; 384 | } 385 | } 386 | } 387 | --------------------------------------------------------------------------------