commit 6ef0f3758d17b5fd7e2ddef62c220ec10e7269c6 Author: Shiewk Date: Sat Sep 7 18:54:20 2024 +0200 Initial Commit (1.0) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6dcdf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +.idea/ +*.iml +*.ipr +*.iws +out/ +.idea_modules/ +atlassian-ide-plugin.xml +*.class +*.log +*.ctxt +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +hs_err_pid* +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk +.gradle +build/ +gradle-app.setting +.gradletasknamecache +**/build/ +run/ +!gradle-wrapper.jar +gradle/ +gradlew +gradlew.bat \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..153d416 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3dfd9d5 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Widgets +**Adds customizable in-game widgets to your game.** + +![Screenshot of in-game hud](https://github.com/user-attachments/assets/362ffd66-75c2-487c-9d2a-21096ff2d042) + +This Minecraft mod adds customizable widgets that you may know from other Minecraft clients. + +This mod was specifically made for people who just use Fabric or Quilt (and no specific client). +It also has some benefits over using clients: +1. Performance: + - We've tried to optimize the mod as much as possible to make sure the game runs as smoothly as possible. + - Disabled widgets don't use any performance +2. No additional features + - The mod focuses on **widgets only**. It does not contain any other features such as performance improvements or mods. +3. Easy to use + - We've tried to make the mod as easy to use as possible, for example, there are three ways to open the config menu instead of only a keybind. + +## Widget settings +There are three ways to open the widget settings: +1. Press the key bind (bound to right shift by default) +2. Enter the command "/widgetsmod" +3. Open the config menu through [Mod Menu](https://modrinth.com/mod/modmenu) + +Once you've opened the settings, you will see something like this: + +![Widget Settings menu](https://github.com/user-attachments/assets/a8f8ac2d-6077-4de9-bf77-2968f8b7dbcf) + +You can now enable the widgets you want to enable, or click on them to view their additional settings, as you can see here: + +![CPS Widget Settings](https://github.com/user-attachments/assets/3703abb2-5f51-42dc-90e4-5847a9e0cc73) + +When you're done with configuring which widgets you want to enable and their settings, click the "Edit Layout" button in the bottom right corner. + +![Edit Layout button](https://github.com/user-attachments/assets/a788b082-ecb6-4a54-ba9c-cf513f128f2e) + +This will take you to a screen where you can freely move around your enabled widgets. + +![Edit Layout screen](https://github.com/user-attachments/assets/02f20215-8be1-4997-9a90-8ef7eb3662d9) + +When you're done, just press Escape until you return to the game. + +Now, everything is done. Your widgets automatically save for the next time you play. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..51dff3c --- /dev/null +++ b/build.gradle @@ -0,0 +1,106 @@ +plugins { + id 'fabric-loom' version '1.6-SNAPSHOT' + id 'maven-publish' +} + +version = project.mod_version +group = project.maven_group + +base { + archivesName = project.archives_base_name +} + +loom { + accessWidenerPath = file("src/main/resources/widgets.accesswidener") +} + + +repositories { + // Add repositories to retrieve artifacts from in here. + // You should only use this when depending on other mods because + // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. + // See https://docs.gradle.org/current/userguide/declaring_repositories.html + // for more information about repositories. + exclusiveContent { + forRepository { + maven { + name = "Modrinth" + url = "https://api.modrinth.com/maven" + } + } + filter { + includeGroup "maven.modrinth" + } + } +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + modImplementation "maven.modrinth:modmenu:11.0.2" +} + +processResources { + inputs.property "version", project.version + inputs.property "minecraft_version", project.minecraft_version + inputs.property "loader_version", project.loader_version + filteringCharset "UTF-8" + + filesMatching("fabric.mod.json") { + expand "version": project.version, + "minecraft_version": project.minecraft_version, + "loader_version": project.loader_version + } +} + +def targetJavaVersion = 21 +tasks.withType(JavaCompile).configureEach { + // ensure that the encoding is set to UTF-8, no matter what the system default is + // this fixes some edge cases with special characters not displaying correctly + // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html + // If Javadoc is generated, this must be specified in that task too. + it.options.encoding = "UTF-8" + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { + it.options.release.set(targetJavaVersion) + } +} + +java { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() +} + +jar { + from("LICENSE") { + rename { "${it}_${project.archivesBaseName}" } + } +} + +// configure the maven publication +publishing { + publications { + create("mavenJava", MavenPublication) { + artifactId = project.archives_base_name + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3417289 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,14 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +# Fabric Properties +# check these on https://modmuss50.me/fabric.html +minecraft_version=1.21 +yarn_mappings=1.21+build.9 +loader_version=0.16.2 +# Mod Properties +mod_version=1.0 +maven_group=de.shiewk +archives_base_name=Widgets +# Dependencies +# check this on https://modmuss50.me/fabric.html +fabric_version=0.101.2+1.21 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..f91a4fe --- /dev/null +++ b/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/src/main/java/de/shiewk/widgets/Dimensionable.java b/src/main/java/de/shiewk/widgets/Dimensionable.java new file mode 100644 index 0000000..12dc49f --- /dev/null +++ b/src/main/java/de/shiewk/widgets/Dimensionable.java @@ -0,0 +1,8 @@ +package de.shiewk.widgets; + +public interface Dimensionable { + int width(); + int height(); + double getX(int mx); + double getY(int my); +} diff --git a/src/main/java/de/shiewk/widgets/ModMenuConfig.java b/src/main/java/de/shiewk/widgets/ModMenuConfig.java new file mode 100644 index 0000000..f2d26df --- /dev/null +++ b/src/main/java/de/shiewk/widgets/ModMenuConfig.java @@ -0,0 +1,13 @@ +package de.shiewk.widgets; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import de.shiewk.widgets.client.screen.WidgetConfigScreen; + +public class ModMenuConfig implements ModMenuApi { + + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return WidgetConfigScreen::new; + } +} diff --git a/src/main/java/de/shiewk/widgets/ModWidget.java b/src/main/java/de/shiewk/widgets/ModWidget.java new file mode 100644 index 0000000..be7fa5c --- /dev/null +++ b/src/main/java/de/shiewk/widgets/ModWidget.java @@ -0,0 +1,45 @@ +package de.shiewk.widgets; + +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.List; +import java.util.Objects; + +public abstract class ModWidget implements Dimensionable { + + private final Identifier id; + private final WidgetSettings settings; + + protected ModWidget(Identifier id, List customSettings) { + Objects.requireNonNull(id, "id"); + this.id = id; + this.settings = WidgetSettings.ofId(id, customSettings); + } + + public final Identifier getId() { + return id; + } + + public final WidgetSettings getSettings() { + return settings; + } + public abstract void render(DrawContext context, long measuringTimeNano, TextRenderer textRenderer, int posX, int posY); + public abstract void tick(); + public abstract Text getName(); + public abstract Text getDescription(); + public abstract void onSettingsChanged(WidgetSettings settings); + + @Override + public double getX(int mx) { + return (int) WidgetUtils.translateToScreen(settings.posX, mx); + } + + @Override + public double getY(int my) { + return (int) WidgetUtils.translateToScreen(settings.posY, my); + } + +} diff --git a/src/main/java/de/shiewk/widgets/WidgetSettingOption.java b/src/main/java/de/shiewk/widgets/WidgetSettingOption.java new file mode 100644 index 0000000..d9067b3 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/WidgetSettingOption.java @@ -0,0 +1,86 @@ +package de.shiewk.widgets; + +import com.google.gson.JsonElement; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.Widget; +import net.minecraft.text.Text; + +import java.util.function.Consumer; + +public abstract class WidgetSettingOption implements Drawable, Widget { + private final String id; + private final Text name; + private int x = 0; + private int y = 0; + private boolean focused = false; + + protected WidgetSettingOption(String id, Text name) { + this.id = id; + this.name = name; + } + + public final String getId() { + return id; + } + + public final Text getName() { + return name; + } + + public abstract JsonElement saveState(); + public abstract void loadState(JsonElement state); + + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return false; + } + + public boolean mouseReleased(double mouseX, double mouseY, int button){ + return false; + } + + public boolean charTyped(char chr, int modifiers) { + return false; + } + + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + return false; + } + + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + return false; + } + + @Override + public void setX(int x) { + this.x = x; + } + + @Override + public void setY(int y) { + this.y = y; + } + + @Override + public int getX() { + return x; + } + + @Override + public int getY() { + return y; + } + + @Override + public final void forEachChild(Consumer consumer) { + throw new UnsupportedOperationException(); + } + + public boolean isFocused() { + return focused; + } + + public void setFocused(boolean focused) { + this.focused = focused; + } +} diff --git a/src/main/java/de/shiewk/widgets/WidgetSettings.java b/src/main/java/de/shiewk/widgets/WidgetSettings.java new file mode 100644 index 0000000..79ca9ef --- /dev/null +++ b/src/main/java/de/shiewk/widgets/WidgetSettings.java @@ -0,0 +1,104 @@ +package de.shiewk.widgets; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.shiewk.widgets.client.WidgetManager; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; + +import java.util.List; + +import static de.shiewk.widgets.WidgetUtils.translateToWidgetSettingsValue; +import static de.shiewk.widgets.WidgetsMod.LOGGER; + +public class WidgetSettings { + public double posX = 0; // posx * 100 = screen width + public double posY = 0; // posy * 100 = screen height + private boolean enabled = false; + private final ObjectArrayList customSettings; + + private WidgetSettings(JsonObject data, List settings){ + customSettings = new ObjectArrayList<>(settings); + if (data != null){ + final JsonElement enabled = data.get("enabled"); + this.enabled = enabled.isJsonPrimitive() && enabled.getAsJsonPrimitive().isBoolean() && enabled.getAsBoolean(); + final JsonElement x = data.get("x"); + this.posX = x.isJsonPrimitive() ? x.getAsJsonPrimitive().isNumber() ? x.getAsDouble() : 0 : 0; + final JsonElement y = data.get("y"); + this.posY = y.isJsonPrimitive() ? y.getAsJsonPrimitive().isNumber() ? y.getAsDouble() : 0 : 0; + final JsonElement s = data.get("settings"); + if (s != null && s.isJsonObject()){ + final JsonObject savedSettings = s.getAsJsonObject(); + for (WidgetSettingOption setting : this.customSettings) { + final String settingId = setting.getId(); + if (savedSettings.has(settingId)){ + try { + setting.loadState(savedSettings.get(settingId)); + } catch (Throwable e){ + LOGGER.error("Could not load setting '{}' from element {}:", settingId, savedSettings.get(settingId)); + LOGGER.error(e.toString()); + for (StackTraceElement element : e.getStackTrace()) { + LOGGER.error(element.toString()); + } + } + } + } + } + } + } + public static WidgetSettings ofId(Identifier id, List customSettings){ + final JsonObject data = WidgetManager.loadWidget(id); + return new WidgetSettings(data, customSettings); + } + + public void setPosX(double v, int widgetWidth, int maxWidth) { + posX = MathHelper.clamp(v, 0, 100 - translateToWidgetSettingsValue(widgetWidth, maxWidth)); + } + public void setPosY(double v, int widgetHeight, int maxHeight) { + posY = MathHelper.clamp(v, 0, 100 - translateToWidgetSettingsValue(widgetHeight, maxHeight)); + } + + public boolean isEnabled(){ + return enabled; + } + + public void setEnabled(ModWidget widget, boolean enabled){ + this.enabled = enabled; + if (enabled){ + WidgetManager.enable(widget); + } else { + WidgetManager.disable(widget); + } + } + + public void toggleEnabled(ModWidget widget){ + setEnabled(widget, !enabled); + } + + public final JsonObject saveState(){ + JsonObject object = new JsonObject(); + object.addProperty("x", posX); + object.addProperty("y", posY); + object.addProperty("enabled", enabled); + JsonObject customSettings = new JsonObject(); + for (WidgetSettingOption customSetting : this.customSettings) { + customSettings.add(customSetting.getId(), customSetting.saveState()); + } + object.add("settings", customSettings); + return object; + } + + public WidgetSettingOption optionById(String id){ + for (WidgetSettingOption customSetting : customSettings) { + if (customSetting.getId().equals(id)){ + return customSetting; + } + } + return null; + } + + public ObjectArrayList getCustomSettings() { + return customSettings.clone(); + } +} diff --git a/src/main/java/de/shiewk/widgets/WidgetUtils.java b/src/main/java/de/shiewk/widgets/WidgetUtils.java new file mode 100644 index 0000000..2c9f42a --- /dev/null +++ b/src/main/java/de/shiewk/widgets/WidgetUtils.java @@ -0,0 +1,18 @@ +package de.shiewk.widgets; + +public class WidgetUtils { + + public static double translateToWidgetSettingsValue(double value, int max){ + return (value / max) * 100; + } + + public static double translateToScreen(double value, int max){ + return value / 100d * max; + } + + public static double computeEasing(double x) { + return 1d - Math.pow(1d - x, 3.5d); + } + + +} diff --git a/src/main/java/de/shiewk/widgets/WidgetsMod.java b/src/main/java/de/shiewk/widgets/WidgetsMod.java new file mode 100644 index 0000000..90ef9d4 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/WidgetsMod.java @@ -0,0 +1,14 @@ +package de.shiewk.widgets; + +import net.fabricmc.api.ModInitializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WidgetsMod implements ModInitializer { + public static final Logger LOGGER = LoggerFactory.getLogger("Widgets"); + public static final String MOD_ID = "widgets"; + + @Override + public void onInitialize() { + } +} diff --git a/src/main/java/de/shiewk/widgets/client/WidgetManager.java b/src/main/java/de/shiewk/widgets/client/WidgetManager.java new file mode 100644 index 0000000..5439646 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/WidgetManager.java @@ -0,0 +1,98 @@ +package de.shiewk.widgets.client; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonWriter; +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetsMod; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.function.Function; + +public class WidgetManager { + + static final ObjectArrayList enabled = new ObjectArrayList<>(); // for performance + private static final ObjectArrayList allWidgets = new ObjectArrayList<>(); + + public static void register(ModWidget widget){ + allWidgets.add(widget); + if (widget.getSettings().isEnabled()){ + enable(widget); + } + } + + public static ObjectArrayList getEnabledWidgets() { + return new ObjectArrayList<>(enabled); + } + + public static ObjectArrayList getAllWidgets() { + return new ObjectArrayList<>(allWidgets); + } + + public static void enable(ModWidget widget) { + enabled.add(widget); + } + + public static void disable(ModWidget widget) { + enabled.remove(widget); + } + + static Function saveFileFactory; + private static final Gson gson = new Gson(); + + public static void saveWidgets(List widgets) { + for (ModWidget widget : widgets) { + WidgetsMod.LOGGER.info("Saving widget {}", widget.getId()); + final File saveFile = saveFileFactory.apply(widget.getId()); + try { + if (saveFile.getParentFile().isDirectory() || saveFile.getParentFile().mkdirs()){ + if (saveFile.isFile() || saveFile.createNewFile()){ + try (FileWriter fw = new FileWriter(saveFile)){ + try (JsonWriter jw = new JsonWriter(fw)){ + Streams.write(widget.getSettings().saveState(), jw); + } + } + } else { + WidgetsMod.LOGGER.error("Could not create file: {}", saveFile.getPath()); + } + } else { + WidgetsMod.LOGGER.error("Could not create directory: {}", saveFile.getParentFile().getPath()); + } + } catch (IOException e){ + WidgetsMod.LOGGER.error("An I/O operation failed while saving widget {} ({})", widget, widget.getId()); + WidgetsMod.LOGGER.error(e.toString()); + for (StackTraceElement element : e.getStackTrace()) { + WidgetsMod.LOGGER.error(String.valueOf(element)); + } + } + } + } + + public static @Nullable JsonObject loadWidget(Identifier id) { + WidgetsMod.LOGGER.info("Loading widget data of {}", id); + final File saveFile = saveFileFactory.apply(id); + if (!saveFile.isFile()){ + return null; + } + try { + try (FileReader fr = new FileReader(saveFile)){ + return gson.fromJson(fr, JsonObject.class); + } + } catch (IOException e){ + WidgetsMod.LOGGER.error("An I/O operation failed while loading widget {}", id); + WidgetsMod.LOGGER.error(e.toString()); + for (StackTraceElement element : e.getStackTrace()) { + WidgetsMod.LOGGER.error(String.valueOf(element)); + } + return null; + } + } +} diff --git a/src/main/java/de/shiewk/widgets/client/WidgetRenderer.java b/src/main/java/de/shiewk/widgets/client/WidgetRenderer.java new file mode 100644 index 0000000..596aede --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/WidgetRenderer.java @@ -0,0 +1,78 @@ +package de.shiewk.widgets.client; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.client.screen.EditWidgetPositionsScreen; +import de.shiewk.widgets.client.screen.WidgetConfigScreen; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.render.RenderTickCounter; +import net.minecraft.util.Util; +import net.minecraft.util.profiler.Profiler; + +import static de.shiewk.widgets.WidgetUtils.translateToScreen; + +public class WidgetRenderer implements HudRenderCallback, ClientTickEvents.StartTick, ClientLifecycleEvents.ClientStarted { + + private static MinecraftClient client; + + @Override + public void onHudRender(DrawContext drawContext, RenderTickCounter tickCounter) { + if (client.options.hudHidden) return; + if (client.currentScreen instanceof EditWidgetPositionsScreen) return; + final Profiler profiler = client.getProfiler(); + profiler.push("widgets"); + final TextRenderer textRenderer = client.textRenderer; + final long timeNano = Util.getMeasuringTimeNano(); + final int windowWidth = drawContext.getScaledWindowWidth(); + final int windowHeight = drawContext.getScaledWindowHeight(); + + final ObjectArrayList enabled = WidgetManager.enabled; + for (int i = 0, enabledSize = enabled.size(); i < enabledSize; i++) { + final ModWidget widget = enabled.get(i); + profiler.push(widget.getId().toString()); + final WidgetSettings settings = widget.getSettings(); + widget.render( + drawContext, + timeNano, + textRenderer, + (int) Math.round(Math.min(translateToScreen(settings.posX, windowWidth), windowWidth - widget.width())), + (int) Math.round(Math.min(translateToScreen(settings.posY, windowHeight), windowHeight - widget.height())) + ); + profiler.pop(); + } + profiler.pop(); + } + + @Override + public void onStartTick(MinecraftClient client) { + final Profiler profiler = (WidgetRenderer.client = client).getProfiler(); + profiler.push("widgets"); + + final ObjectArrayList enabled = WidgetManager.enabled; + for (int i = 0, enabledSize = enabled.size(); i < enabledSize; i++) { + final ModWidget widget = enabled.get(i); + profiler.push(widget.getId().toString()); + widget.tick(); + profiler.pop(); + } + + profiler.pop(); + + if (WidgetsModClient.configKeyBinding.wasPressed()){ + client.setScreen(new WidgetConfigScreen(client.currentScreen)); + } + } + + @Override + public void onClientStarted(MinecraftClient client) { + for (ModWidget widget : WidgetManager.getAllWidgets()) { + widget.onSettingsChanged(widget.getSettings()); + } + } +} diff --git a/src/main/java/de/shiewk/widgets/client/WidgetsModClient.java b/src/main/java/de/shiewk/widgets/client/WidgetsModClient.java new file mode 100644 index 0000000..68a0ce0 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/WidgetsModClient.java @@ -0,0 +1,62 @@ +package de.shiewk.widgets.client; + +import de.shiewk.widgets.WidgetsMod; +import de.shiewk.widgets.client.screen.WidgetConfigScreen; +import de.shiewk.widgets.widgets.*; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; +import net.minecraft.util.Identifier; +import org.lwjgl.glfw.GLFW; + +import java.io.File; + +public class WidgetsModClient implements ClientModInitializer { + + static KeyBinding configKeyBinding; + + + @Override + public void onInitializeClient() { + HudRenderCallback.EVENT.register(new WidgetRenderer()); + ClientTickEvents.START_CLIENT_TICK.register(new WidgetRenderer()); + ClientLifecycleEvents.CLIENT_STARTED.register(new WidgetRenderer()); + + // manage widgets keybind + configKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding( + "widgets.key.config", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_RIGHT_SHIFT, + "widgets.key.category" + )); + + // in-game /widgetsmod command + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> + dispatcher.register(ClientCommandManager.literal("widgetsmod").executes(ctx -> { + WidgetsMod.LOGGER.info("Ran in-game command"); + final MinecraftClient client = ctx.getSource().getClient(); + client.send(() -> client.setScreen(new WidgetConfigScreen(client.currentScreen))); + return 0; + }) + ) + ); + + WidgetManager.saveFileFactory = id -> new File(MinecraftClient.getInstance().runDirectory.getPath() + "/config/widgets/" + id.getNamespace() + "/" + id.getPath() + ".json"); + + WidgetManager.register(new FPSWidget(Identifier.of(WidgetsMod.MOD_ID, "fps"))); + WidgetManager.register(new ClockWidget(Identifier.of(WidgetsMod.MOD_ID, "clock"))); + WidgetManager.register(new CoordinatesWidget(Identifier.of(WidgetsMod.MOD_ID, "coordinates"))); + WidgetManager.register(new BandwidthWidget(Identifier.of(WidgetsMod.MOD_ID, "bandwidth"))); + WidgetManager.register(new PingWidget(Identifier.of(WidgetsMod.MOD_ID, "ping"))); + WidgetManager.register(new ServerIPWidget(Identifier.of(WidgetsMod.MOD_ID, "server_ip"))); + WidgetManager.register(new PlayerCountWidget(Identifier.of(WidgetsMod.MOD_ID, "player_count"))); + WidgetManager.register(new CPSWidget(Identifier.of(WidgetsMod.MOD_ID, "cps"))); + } +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/AnimatedScreen.java b/src/main/java/de/shiewk/widgets/client/screen/AnimatedScreen.java new file mode 100644 index 0000000..6040c64 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/AnimatedScreen.java @@ -0,0 +1,39 @@ +package de.shiewk.widgets.client.screen; + +import de.shiewk.widgets.WidgetUtils; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; +import net.minecraft.util.Util; + +public abstract class AnimatedScreen extends Screen { + protected final Screen parent; + private final int animationDurationMs; + private final long creationTime = Util.getMeasuringTimeNano(); + protected AnimatedScreen(Text title, Screen parent, int animationDurationMs) { + super(title); + this.parent = parent; + this.animationDurationMs = animationDurationMs; + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + double timeMs = (Util.getMeasuringTimeNano() - creationTime) / 1000000d; + final boolean shouldAnimate = timeMs < animationDurationMs; + if (shouldAnimate){ + double translation = WidgetUtils.computeEasing(timeMs / animationDurationMs) * this.width; + context.getMatrices().push(); + context.getMatrices().translate(-translation, 0, 0); + parent.render(context, (int) (mouseX + translation), mouseY, delta); + context.getMatrices().translate(this.width, 0, 0); + mouseX -= (int) translation; + } + super.render(context, mouseX, mouseY, delta); + this.renderScreenContents(context, mouseX, mouseY, delta); + if (shouldAnimate){ + context.getMatrices().pop(); + } + } + + public abstract void renderScreenContents(DrawContext context, int mouseX, int mouseY, float delta); +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/EditWidgetPositionsScreen.java b/src/main/java/de/shiewk/widgets/client/screen/EditWidgetPositionsScreen.java new file mode 100644 index 0000000..f047333 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/EditWidgetPositionsScreen.java @@ -0,0 +1,198 @@ +package de.shiewk.widgets.client.screen; + +import de.shiewk.widgets.Dimensionable; +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.client.WidgetManager; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; +import net.minecraft.util.Util; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.function.Consumer; + +import static de.shiewk.widgets.WidgetUtils.translateToScreen; +import static de.shiewk.widgets.WidgetUtils.translateToWidgetSettingsValue; + +public class EditWidgetPositionsScreen extends AnimatedScreen { + + private record Alignment(int x, int y, int width, int height) implements Dimensionable { + + @Override + public double getX(int mx) { + return x; + } + + @Override + public double getY(int my) { + return y; + } + } + + private final Screen parent; + private final Consumer onEdit; + private boolean alignX = true; + private boolean alignY = true; + public EditWidgetPositionsScreen(Screen parent, Consumer onEdit) { + super(Text.translatable("widgets.ui.editPositions"), parent, 500); + this.parent = parent; + this.onEdit = onEdit; + } + + @Override + public void close() { + assert client != null; + client.setScreen(parent); + } + + private record AlignResult(double result, boolean isEnd){} + + private @Nullable AlignResult alignX(double x, int width, ModWidget widget){ + double endX = x + width; + int factor = alignX ? 2 : 0; + for (Dimensionable rect : this.getAlignments(widget)) { + if (rect == widget) continue; + final double nwx = rect.getX(this.width); + final double nww = rect.width(); + if (endX < nwx + factor && endX > nwx - factor){ + return new AlignResult(nwx - width, true); + } else if (x < nwx + factor && x > nwx - factor){ + return new AlignResult(nwx, false); + } else if (endX < nwx+nww + factor && endX > nwx + nww - factor){ + return new AlignResult(nwx + nww - width, true); + } else if (x < nwx+nww + factor && x > nwx + nww - factor){ + return new AlignResult(nwx + nww, false); + } + } + return null; + } + + private Iterable getAlignments(Dimensionable rel) { + ObjectArrayList alignments = new ObjectArrayList<>(); + for (ModWidget widget : WidgetManager.getEnabledWidgets()) { + alignments.add(widget); + } + alignments.add(new Alignment(2, 2, this.width / 2 - 2, this.height / 2 - 2)); + alignments.add(new Alignment(this.width / 2, this.height / 2, this.width / 2 - 2, this.height / 2 - 2)); + alignments.add(new Alignment(2, this.height / 2, this.width / 2 - 2, this.height / 2 - 2)); + alignments.add(new Alignment(this.width / 2, 2, this.width / 2 - 2, this.height / 2 - 2)); + alignments.add(new Alignment(this.width / 2 - rel.width() / 2, this.height / 2 - rel.height() / 2, rel.width(), rel.height())); + return alignments; + } + + + private @Nullable AlignResult alignY(double y, int height, ModWidget widget){ + double endY = y + height; + int factor = alignY ? 2 : 0; + for (Dimensionable rect : this.getAlignments(widget)) { + if (rect == widget) continue; + final double nwy = rect.getY(this.height); + final double nwh = rect.height(); + if (endY < nwy + factor && endY > nwy - factor){ + return new AlignResult(nwy - height, true); + } else if (y < nwy + factor && y > nwy - factor){ + return new AlignResult(nwy, false); + } else if (endY < nwy+nwh + factor && endY > nwy + nwh - factor){ + return new AlignResult(nwy + nwh - height, true); + } else if (y < nwy+nwh + factor && y > nwy + nwh - factor){ + return new AlignResult(nwy + nwh, false); + } + } + return null; + } + + private static final int SELECT_COLOR = Color.GREEN.getRGB(), ALIGN_COLOR = Color.ORANGE.getRGB(), ALIGN_DISABLED_COLOR = Color.GRAY.getRGB(); + private ModWidget selectedWidget = null; + private ModWidget hoveredWidget = null; + + @Override + public void renderScreenContents(DrawContext context, int mouseX, int mouseY, float delta) { + assert client != null; + for (ModWidget widget : WidgetManager.getEnabledWidgets()) { + final WidgetSettings settings = widget.getSettings(); + final int ww = widget.width(); + double wx = Math.min(translateToScreen(settings.posX, this.width), this.width - ww); + final int wh = widget.height(); + double wy = Math.min(translateToScreen(settings.posY, this.height), this.height - wh); + if (selectedWidget == widget){ + final AlignResult alignedX = alignX(wx, ww, widget); + if (alignedX != null){ + context.drawVerticalLine((int) (!alignedX.isEnd() ? alignedX.result() : alignedX.result() + ww), 0, this.height, alignX ? ALIGN_COLOR : ALIGN_DISABLED_COLOR); + wx = alignedX.result(); + } + final AlignResult alignedY = alignY(wy, wh, widget); + if (alignedY != null){ + context.drawHorizontalLine(0, this.width, (int) (!alignedY.isEnd() ? alignedY.result() : alignedY.result() + wh), alignY ? ALIGN_COLOR : ALIGN_DISABLED_COLOR); + wy = alignedY.result(); + } + } + if (hoveredWidget == null || hoveredWidget == widget){ + if (mouseX <= wx + ww && mouseX >= wx && mouseY <= wy + wh && mouseY >= wy){ + if (hoveredWidget == null){ + hoveredWidget = widget; + } + } else { + hoveredWidget = null; + } + } + if (selectedWidget == null ? hoveredWidget == widget : selectedWidget == widget){ + context.drawBorder((int) Math.round(wx-1), (int) Math.round(wy-1), ww+2, wh+2, SELECT_COLOR); + context.drawBorder((int) Math.round(wx), (int) Math.round(wy), ww, wh, SELECT_COLOR); + } + widget.render(context, Util.getMeasuringTimeNano(), textRenderer, (int) Math.round(wx), (int) Math.round(wy)); + } + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button == 0 && selectedWidget != null){ + final AlignResult alignedX = alignX(translateToScreen(selectedWidget.getSettings().posX, this.width), selectedWidget.width(), selectedWidget); + if (alignedX != null){ + selectedWidget.getSettings().setPosX(translateToWidgetSettingsValue(alignedX.result(), this.width), selectedWidget.width(), this.width); + } + final AlignResult alignedY = alignY(translateToScreen(selectedWidget.getSettings().posY, this.height), selectedWidget.height(), selectedWidget); + if (alignedY != null){ + selectedWidget.getSettings().setPosY(translateToWidgetSettingsValue(alignedY.result(), this.height), selectedWidget.height(), this.height); + } + onEdit.accept(selectedWidget); + selectedWidget = null; + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && hoveredWidget != null){ + selectedWidget = hoveredWidget; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (button == 0){ + assert client != null; + final ModWidget widget = selectedWidget; + if (widget != null){ + final WidgetSettings settings = widget.getSettings(); + final int ww = widget.width(); + final int wx = (int) Math.min(translateToScreen(settings.posX, this.width), this.width - ww); + final int wh = widget.height(); + final int wy = (int) Math.min(translateToScreen(settings.posY, this.height), this.height - wh); + if (mouseX <= wx + ww + deltaX && mouseX >= wx + deltaX){ + if (mouseY <= wy + wh + deltaY && mouseY >= wy + deltaY){ + double newPosX = settings.posX + translateToWidgetSettingsValue(deltaX, this.width); + double newPosY = settings.posY + translateToWidgetSettingsValue(deltaY, this.height); + settings.setPosX(newPosX, ww, this.width); + settings.setPosY(newPosY, wh, this.height); + return true; + } + } + } + } + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/WidgetConfigScreen.java b/src/main/java/de/shiewk/widgets/client/screen/WidgetConfigScreen.java new file mode 100644 index 0000000..d78c234 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/WidgetConfigScreen.java @@ -0,0 +1,129 @@ +package de.shiewk.widgets.client.screen; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetUtils; +import de.shiewk.widgets.client.WidgetManager; +import de.shiewk.widgets.client.screen.components.WidgetListWidget; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.AxisGridWidget; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.text.Text; +import net.minecraft.util.Util; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.function.Consumer; + +public class WidgetConfigScreen extends Screen { + + private final Screen parent; + private final long creationTime = Util.getMeasuringTimeNano(); + private String search = ""; + private WidgetListWidget widgetList = null; + private final ArrayList widgetsEdited = new ArrayList<>(); + private final Consumer onWidgetEdit = this::changedSettings; + + private double getScreenTimeMs(){ + return (Util.getMeasuringTimeNano() - creationTime) / 1000000d; + } + + public WidgetConfigScreen(Screen parent) { + super(Text.translatable("widgets.ui.config")); + this.parent = parent; + } + + @Override + public void close() { + WidgetManager.saveWidgets(widgetsEdited); + for (ModWidget widget : widgetsEdited) { + widget.onSettingsChanged(widget.getSettings()); + } + assert client != null; + client.setScreen(parent); + } + + @Override + protected void init() { + super.init(); + AxisGridWidget agw = new AxisGridWidget(3, height - 22, width - 6, 20, AxisGridWidget.DisplayAxis.HORIZONTAL); + final TextFieldWidget searchField = new TextFieldWidget(textRenderer, this.width - 160, 20, Text.empty()); + searchField.setPlaceholder(Text.translatable("widgets.ui.search")); + searchField.setChangedListener(this::search); + if (this.widgetList != null){ + searchField.setText(this.getSearchQuery()); + } + agw.add(searchField); + agw.add(new ButtonWidget.Builder(Text.translatable("widgets.ui.editPositions"), this::switchToEditPositions).build()); + + agw.refreshPositions(); + agw.forEachChild(this::addDrawableChild); + + if (Objects.equals(search, "")){ + search(""); + } + } + + private String getSearchQuery() { + return search; + } + + public void setSearchQuery(String search) { + this.search = search; + } + + private void search(String query) { + this.setSearchQuery(query); + widgetList = new WidgetListWidget(0, 0, width-4, height-24, Text.translatable("widgets.ui.config"), client, WidgetManager.getAllWidgets().stream().filter(this::searchQueryMatches).toList(), textRenderer, this.onWidgetEdit); + } + + private void switchToEditPositions(ButtonWidget widget) { + widget.active = false; + assert client != null; + client.setScreen(new EditWidgetPositionsScreen(this, this.onWidgetEdit)); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (super.mouseClicked(mouseX, mouseY, button)){ + return true; + } else return widgetList.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + if (super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)){ + return true; + } else return widgetList.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + assert client != null; + final double time = getScreenTimeMs(); + if (time < 400){ + context.getMatrices().push(); + final float v = (float) WidgetUtils.computeEasing(time / 400d); + context.getMatrices().translate(width / 2d - (width * v / 2d), height / 2d - (height * v / 2d), 0); + context.getMatrices().scale(v, v, 1); + } + super.render(context, mouseX, mouseY, delta); + if (widgetList != null){ + widgetList.render(context, mouseX, mouseY, delta); + } + if (time < 400){ + context.getMatrices().pop(); + } + } + + private boolean searchQueryMatches(ModWidget widget) { + return widget.getName().getString().contains(search) || widget.getDescription().getString().contains(search) || widget.getId().toString().contains(search); + } + + public void changedSettings(ModWidget widget) { + if (!widgetsEdited.contains(widget)){ + widgetsEdited.add(widget); + } + } +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/WidgetSettingsScreen.java b/src/main/java/de/shiewk/widgets/client/screen/WidgetSettingsScreen.java new file mode 100644 index 0000000..0d0f592 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/WidgetSettingsScreen.java @@ -0,0 +1,50 @@ +package de.shiewk.widgets.client.screen; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.client.screen.components.WidgetSettingsEditWidget; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; +import net.minecraft.util.Util; + +public class WidgetSettingsScreen extends AnimatedScreen { + private static final Text previewText = Text.translatable("widgets.ui.preview"); + private final ModWidget widget; + private final Runnable onChange; + public WidgetSettingsScreen(Screen parent, ModWidget widget) { + super(Text.translatable("widgets.ui.widgetSettings", widget.getName()), parent, 500); + this.widget = widget; + onChange = () -> { + widget.onSettingsChanged(widget.getSettings()); + if (parent instanceof WidgetConfigScreen widgetConfigScreen){ + widgetConfigScreen.changedSettings(widget); + } + }; + } + + @Override + protected void init() { + super.init(); + addDrawableChild(new WidgetSettingsEditWidget(0, 0, this.width / 2 - 8, this.height, textRenderer, widget, this.onChange)); + } + + @Override + public void renderScreenContents(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawText(textRenderer, previewText, this.width * 3 / 4 - textRenderer.getWidth(previewText) / 2, this.height / 50, 0xffffffff, false); + widget.render(context, Util.getMeasuringTimeNano(), textRenderer, this.width * 3 / 4 - widget.width() / 2, this.height / 2 - widget.height() / 2); + } + + @Override + public void close() { + assert client != null; + client.setScreen(parent); + } + + @Override + public void tick() { + super.tick(); + if (!widget.getSettings().isEnabled()){ + widget.tick(); + } + } +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/components/WidgetListWidget.java b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetListWidget.java new file mode 100644 index 0000000..bc3ebc6 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetListWidget.java @@ -0,0 +1,92 @@ +package de.shiewk.widgets.client.screen.components; + +import de.shiewk.widgets.ModWidget; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.gui.widget.GridWidget; +import net.minecraft.client.gui.widget.ScrollableWidget; +import net.minecraft.client.gui.widget.SimplePositioningWidget; +import net.minecraft.text.Text; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class WidgetListWidget extends ScrollableWidget { + + private final MinecraftClient client; + private final List widgets; + private final List elements = new ArrayList<>(); + private final TextRenderer textRenderer; + private final Consumer onEdit; + + + public WidgetListWidget(int x, int y, int width, int height, Text message, MinecraftClient client, List widgets, TextRenderer textRenderer, Consumer onEdit) { + super(x, y, width, height, message); + this.client = client; + this.widgets = widgets; + this.textRenderer = textRenderer; + this.onEdit = onEdit; + init(); + } + + private void init(){ + GridWidget gw = new GridWidget(); + gw.getMainPositioner().margin(4, 4, 4, 4); + final GridWidget.Adder adder = gw.createAdder(this.width / 208); + for (ModWidget widget : widgets) { + adder.add(new WidgetWidget(0, 0, 200, 100, client, widget, textRenderer, onEdit)); + } + gw.refreshPositions(); + SimplePositioningWidget.setPos(gw, 0, 0, this.width, this.getContentsHeight(), 0.5F, 0.5F); + gw.forEachChild(w -> this.addWidget((WidgetWidget) w)); + } + + protected void addWidget(WidgetWidget drawableElement) { + this.elements.add(drawableElement); + } + + @Override + protected int getContentsHeight() { + final int rowSize = this.width / 208; + final int rows = widgets.size() % rowSize == 0 ? widgets.size() / rowSize : widgets.size() / rowSize + 1; + return 10 + (rows * 108); + } + + @Override + protected double getDeltaYPerScroll() { + return 35; + } + + @Override + protected void renderContents(DrawContext context, int mouseX, int mouseY, float delta) { + for (WidgetWidget element : elements) { + element.render(context, mouseX, (int) (mouseY + getScrollY()), delta); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + mouseY += getScrollY(); + for (Element element : elements) { + if (element.mouseClicked(mouseX, mouseY, 0)){ + return true; + } + } + return false; + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + for (ModWidget widget : widgets) { + builder.put(NarrationPart.HINT, widget.getName()); + } + } + + @Override + protected void drawBox(DrawContext context, int x, int y, int width, int height) {} +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/components/WidgetSettingsEditWidget.java b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetSettingsEditWidget.java new file mode 100644 index 0000000..7745fa7 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetSettingsEditWidget.java @@ -0,0 +1,135 @@ +package de.shiewk.widgets.client.screen.components; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetSettingOption; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ScrollableWidget; +import net.minecraft.text.Text; + +import java.awt.*; + +public class WidgetSettingsEditWidget extends ScrollableWidget { + private static final int COLOR_FG = Color.WHITE.getRGB(), COLOR_BG = new Color(0, 0, 0, 60).getRGB(); + private final TextRenderer textRenderer; + private final ModWidget widget; + private final Runnable onChange; + private WidgetSettingOption focus = null; + private int contentsHeight = 10; + public WidgetSettingsEditWidget(int x, int y, int width, int height, TextRenderer textRenderer, ModWidget widget, Runnable onChange) { + super(x, y, width, height, Text.empty()); + this.textRenderer = textRenderer; + this.widget = widget; + this.onChange = onChange; + for (WidgetSettingOption customSetting : widget.getSettings().getCustomSettings()) { + customSetting.setFocused(false); + } + } + + @Override + protected int getContentsHeight() { + return this.contentsHeight; + } + + @Override + protected double getDeltaYPerScroll() { + return 20; + } + + @Override + protected void renderContents(DrawContext context, int mouseX, int mouseY, float delta) { + context.getMatrices().push(); + context.getMatrices().scale(2, 2, 2); + context.drawText(textRenderer, widget.getName(), this.width / 4 - textRenderer.getWidth(widget.getName()) / 2, this.height / 100, COLOR_FG, true); + context.getMatrices().pop(); + int y = textRenderer.fontHeight * 2 + this.height / 50 + 5; + for (WidgetSettingOption setting : widget.getSettings().getCustomSettings()) { + if (this.width - setting.getWidth() > textRenderer.getWidth(setting.getName()) + 20){ + setting.setX(this.getX() + this.width - setting.getWidth() - 5); + setting.setY(y); + context.drawText(textRenderer, setting.getName(), getX() + 10, y + (setting.getHeight() / 2), COLOR_FG, true); + } else { + setting.setX(this.getX() + this.width / 2 - setting.getWidth() / 2); + setting.setY(y + 9 + 5); + context.drawText(textRenderer, setting.getName(), getX() + getWidth() / 2 - textRenderer.getWidth(setting.getName()) / 2, y, COLOR_FG, true); + y += 9 + 5; + } + setting.render(context, mouseX, (int) (mouseY + getScrollY()), delta); + y += setting.getHeight(); + y += 5; + } + this.contentsHeight = y; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + mouseY += getScrollY(); + for (WidgetSettingOption customSetting : widget.getSettings().getCustomSettings()) { + if (mouseX >= customSetting.getX() && mouseX <= customSetting.getX() + customSetting.getWidth() + && mouseY >= customSetting.getY() && mouseY <= customSetting.getY() + customSetting.getHeight()){ + focus = customSetting; + customSetting.setFocused(true); + if (customSetting.mouseClicked(mouseX, mouseY + getScrollY(), button)){ + onChange.run(); + return true; + } + } + } + return super.mouseClicked(mouseX, mouseY - getScrollY(), button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + for (WidgetSettingOption customSetting : widget.getSettings().getCustomSettings()) { + if (customSetting.mouseReleased(mouseX, mouseY + getScrollY(), button)){ + onChange.run(); + return true; + } + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (this.focus != null){ + if (this.focus.charTyped(chr, modifiers)){ + onChange.run(); + return true; + } + } + return super.charTyped(chr, modifiers); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (this.focus != null){ + if (this.focus.keyPressed(keyCode, scanCode, modifiers)){ + onChange.run(); + return true; + } + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + if (this.focus != null){ + if (this.focus.keyReleased(keyCode, scanCode, modifiers)){ + onChange.run(); + return true; + } + } + return super.keyReleased(keyCode, scanCode, modifiers); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + @Override + protected void drawBox(DrawContext context, int x, int y, int width, int height) { + context.fill(x, y, x+width, y+height, COLOR_BG); + } +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/components/WidgetWidget.java b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetWidget.java new file mode 100644 index 0000000..a696b35 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetWidget.java @@ -0,0 +1,130 @@ +package de.shiewk.widgets.client.screen.components; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetUtils; +import de.shiewk.widgets.client.screen.WidgetSettingsScreen; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import net.minecraft.util.Util; + +import java.awt.*; +import java.util.Iterator; +import java.util.function.Consumer; + +public class WidgetWidget extends ClickableWidget { + + + + protected static final int COLOR_BG = new Color(0, 0, 0, 80).getRGB(), + COLOR_BG_HOVER = new Color(40, 40, 40, 80).getRGB(), + COLOR_FG = Color.WHITE.getRGB(), + COLOR_DISABLED = new Color(200, 0, 0, 200).getRGB(), + COLOR_DISABLED_HOVER = new Color(255, 0, 0, 200).getRGB(), + COLOR_ENABLED = new Color(0, 200, 0, 200).getRGB(), + COLOR_ENABLED_HOVER = new Color(0, 255, 0, 200).getRGB(); + + private final MinecraftClient client; + private final ModWidget widget; + private final TextRenderer textRenderer; + private final Consumer onEdit; + + private long toggleTime = 0; + + public WidgetWidget(int x, int y, int width, int height, MinecraftClient client, ModWidget widget, TextRenderer textRenderer, Consumer onEdit) { + super(x, y, width, height, widget.getName()); + this.client = client; + this.widget = widget; + this.textRenderer = textRenderer; + this.onEdit = onEdit; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return this.active && this.visible && mouseX >= (double)this.getX() && mouseY >= (double)this.getY() && mouseX < (double)(this.getX() + this.width) && mouseY < (double)(this.getY() + this.height - 24); + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + boolean hover = this.isMouseOver(mouseX, mouseY); + boolean widgetEnabled = widget.getSettings().isEnabled(); + context.fill(this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight(), hover ? COLOR_BG_HOVER : COLOR_BG); + context.getMatrices().push(); + context.getMatrices().scale(2, 2, 1); + int titleSize = textRenderer.getWidth(widget.getName()); + context.drawText(textRenderer, widget.getName(), getX() / 2 + getWidth() / 4 - titleSize / 2, getY() / 2 + 4, COLOR_FG, false); + context.getMatrices().pop(); + int y = this.getY() + 12 + textRenderer.fontHeight * 2; + for (Iterator it = textRenderer.wrapLines(widget.getDescription(), this.getWidth() - 10).iterator(); it.hasNext(); y += 9) { + OrderedText t = it.next(); + context.drawText(textRenderer, t, getX() + 5 + ((getWidth() - 5) / 2) - (textRenderer.getWidth(t) / 2), y, COLOR_FG, false); + } + this.renderToggleButton(context, mouseX, mouseY, delta, widgetEnabled); + } + + @Override + protected boolean clicked(double mouseX, double mouseY) { + if (isMouseOver(mouseX, mouseY)){ + client.setScreen(new WidgetSettingsScreen(client.currentScreen, widget)); + return true; + } else if (isMouseOverToggle(mouseX, mouseY)){ + this.toggleWidget(); + return true; + } else { + return false; + } + } + + private void toggleWidget() { + widget.getSettings().toggleEnabled(widget); + toggleTime = Util.getMeasuringTimeNano(); + onEdit.accept(widget); + } + + private void renderToggleButton(DrawContext context, int mouseX, int mouseY, float delta, boolean widgetEnabled){ + boolean hoverToggle = this.isMouseOverToggle(mouseX, mouseY); + final int toggleColor; + final int toggleColorInvert; + if (hoverToggle){ + if (widgetEnabled){ + toggleColor = COLOR_ENABLED_HOVER; + toggleColorInvert = COLOR_DISABLED_HOVER; + } else { + toggleColor = COLOR_DISABLED_HOVER; + toggleColorInvert = COLOR_ENABLED_HOVER; + } + } else { + if (widgetEnabled){ + toggleColor = COLOR_ENABLED; + toggleColorInvert = COLOR_DISABLED; + } else { + toggleColor = COLOR_DISABLED; + toggleColorInvert = COLOR_ENABLED; + } + } + if (toggleTime > Util.getMeasuringTimeNano() - 250000000){ + context.fill(this.getX(), this.getY() + this.getHeight() - 24, this.getX() + this.getWidth(), this.getY() + this.getHeight(), toggleColorInvert); + context.fill(this.getX(), this.getY() + this.getHeight() - 24, (int) (WidgetUtils.computeEasing((Util.getMeasuringTimeNano() - toggleTime) / 250000000d) * this.getWidth() + this.getX()), this.getY() + this.getHeight(), toggleColor); + } else { + context.fill(this.getX(), this.getY() + this.getHeight() - 24, this.getX() + this.getWidth(), this.getY() + this.getHeight(), toggleColor); + } + context.drawCenteredTextWithShadow(textRenderer, Text.translatable(widgetEnabled ? "widgets.ui.enabled" : "widgets.ui.disabled"), this.getX() + (this.getWidth() / 2), this.getY() + this.getHeight() - 16, COLOR_FG); + } + + private boolean isMouseOverToggle(double mouseX, double mouseY) { + return this.active && this.visible && mouseX >= this.getX() && mouseY >= (this.getY() + this.height - 24) && mouseX < (this.getX() + this.width) && mouseY < (this.getY() + this.height); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + public ModWidget getWidget() { + return widget; + } +} diff --git a/src/main/java/de/shiewk/widgets/mixin/MixinMouse.java b/src/main/java/de/shiewk/widgets/mixin/MixinMouse.java new file mode 100644 index 0000000..d943086 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/mixin/MixinMouse.java @@ -0,0 +1,27 @@ +package de.shiewk.widgets.mixin; + +import de.shiewk.widgets.widgets.CPSWidget; +import net.minecraft.client.Mouse; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Mouse.class) +public class MixinMouse { + + @Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/client/Mouse;leftButtonClicked:Z"), method = "onMouseButton") + public void onLeftClick(long window, int button, int action, int mods, CallbackInfo ci){ + if (action == 1) CPSWidget.clickLeft(); + } + + @Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/client/Mouse;middleButtonClicked:Z"), method = "onMouseButton") + public void onMiddleClick(long window, int button, int action, int mods, CallbackInfo ci){ + if (action == 1) CPSWidget.clickMiddle(); + } + + @Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/client/Mouse;rightButtonClicked:Z"), method = "onMouseButton") + public void onRightClick(long window, int button, int action, int mods, CallbackInfo ci){ + if (action == 1) CPSWidget.clickRight(); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/BandwidthWidget.java b/src/main/java/de/shiewk/widgets/widgets/BandwidthWidget.java new file mode 100644 index 0000000..504ef85 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/BandwidthWidget.java @@ -0,0 +1,73 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.ToggleWidgetSetting; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.profiler.MultiValueDebugSampleLogImpl; + +import java.util.List; + +public class BandwidthWidget extends BasicTextWidget { + public BandwidthWidget(Identifier id) { + super(id, List.of( + new ToggleWidgetSetting("dynamic_color", Text.translatable("widgets.widgets.bandwidth.dynamicColor"), true) + )); + } + + private int t = 0; + private boolean dynamicColor = false; + + @Override + public void tick() { + t++; + if (t >= 20){ + t = 0; + final MultiValueDebugSampleLogImpl packetSizeLog = MinecraftClient.getInstance().getDebugHud().getPacketSizeLog(); + final int logLength = packetSizeLog.getLength(); + final int avgCompileLength = 60; + long size = 0; + for (int i = logLength-1; i > logLength-avgCompileLength; i--) { + if (i < 0) break; + size += packetSizeLog.get(i); + } + long avgBytesPerSecond = size / avgCompileLength * 20; + this.renderText = Text.of(formatByteSize(avgBytesPerSecond)); + if (this.dynamicColor){ + if (avgBytesPerSecond < 100000){ + this.textColor = 0x00ff00; + } else if (avgBytesPerSecond < 750000) { + this.textColor = 0xffff00; + } else { + this.textColor = 0xff3030; + } + } + } + } + + private String formatByteSize(long bytes) { + if (bytes > 1000) { + double mb = bytes / 100 / 10d; + return mb + " KB/s"; + } else { + return bytes + " B/s"; + } + } + + @Override + public void onSettingsChanged(WidgetSettings settings) { + super.onSettingsChanged(settings); + this.dynamicColor = ((ToggleWidgetSetting) settings.optionById("dynamic_color")).getValue(); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.bandwidth"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.bandwidth.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/BasicTextWidget.java b/src/main/java/de/shiewk/widgets/widgets/BasicTextWidget.java new file mode 100644 index 0000000..37182ad --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/BasicTextWidget.java @@ -0,0 +1,58 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetSettingOption; +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.RGBAColorWidgetSetting; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.List; + +public abstract class BasicTextWidget extends ModWidget { + protected Text renderText = Text.empty(); + + private static ObjectArrayList getCustomSettings(List otherCustomOptions) { + final ObjectArrayList list = new ObjectArrayList<>(otherCustomOptions); + list.add(new RGBAColorWidgetSetting("backgroundcolor", Text.translatable("widgets.widgets.basictext.background"), 0, 0, 0, 80)); + list.add(new RGBAColorWidgetSetting("textcolor", Text.translatable("widgets.widgets.basictext.textcolor"), 255, 255, 255, 255)); + return list; + } + protected BasicTextWidget(Identifier id, List otherCustomOptions) { + super(id, getCustomSettings(otherCustomOptions)); + } + + protected static final int + DEFAULT_WIDTH = 80, + DEFAULT_HEIGHT = 9 + 12, + DEFAULT_BACKGROUND_COLOR = new Color(0, 0, 0, 80).getRGB(), + DEFAULT_TEXT_COLOR = new Color(255, 255 ,255, 255).getRGB(); + + protected int backgroundColor = DEFAULT_BACKGROUND_COLOR, textColor = DEFAULT_TEXT_COLOR; + + @Override + public int width() { + return DEFAULT_WIDTH; + } + + @Override + public int height() { + return DEFAULT_HEIGHT; + } + + @Override + public void render(DrawContext context, long n, TextRenderer textRenderer, int posX, int posY) { + context.fill(posX, posY, posX + width(), posY + height(), this.backgroundColor); + context.drawCenteredTextWithShadow(textRenderer, renderText, posX + (width() / 2), posY + 6, this.textColor); + } + + @Override + public void onSettingsChanged(WidgetSettings settings) { + this.backgroundColor = ((RGBAColorWidgetSetting) settings.optionById("backgroundcolor")).getColor(); + this.textColor = ((RGBAColorWidgetSetting) settings.optionById("textcolor")).getColor(); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/CPSWidget.java b/src/main/java/de/shiewk/widgets/widgets/CPSWidget.java new file mode 100644 index 0000000..e07440b --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/CPSWidget.java @@ -0,0 +1,136 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.EnumWidgetSetting; +import de.shiewk.widgets.widgets.settings.ToggleWidgetSetting; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class CPSWidget extends BasicTextWidget { + + public static class Click { + private int ticks = 0; + + public int tick(){ + return ticks++; + } + } + + private static boolean countLeftClicks = true; + private static boolean countMiddleClicks = true; + private static boolean countRightClicks = true; + + private static final ObjectArrayList leftClicks = new ObjectArrayList<>(); + private static final ObjectArrayList middleClicks = new ObjectArrayList<>(); + private static final ObjectArrayList rightClicks = new ObjectArrayList<>(); + + private Appearance appearance = Appearance.UNIFIED; + + public enum Appearance { + SPLIT_PIPE("pipe"), + SPLIT_SLASH("slash"), + UNIFIED("unified"); + + public final String key; + + Appearance(String key) { + this.key = key; + } + } + + public CPSWidget(Identifier id) { + super(id, List.of( + new EnumWidgetSetting<>("appearance", Text.translatable("widgets.widgets.cps.appearance"), Appearance.class, Appearance.UNIFIED, appearance -> Text.translatable("widgets.widgets.cps.appearance."+appearance.key)), + new ToggleWidgetSetting("left", Text.translatable("widgets.widgets.cps.left"), true), + new ToggleWidgetSetting("middle", Text.translatable("widgets.widgets.cps.middle"), false), + new ToggleWidgetSetting("right", Text.translatable("widgets.widgets.cps.right"), true) + )); + } + + public static void clickLeft() { + if (countLeftClicks){ + leftClicks.add(new Click()); + } + } + + public static void clickMiddle() { + if (countMiddleClicks){ + middleClicks.add(new Click()); + } + } + + public static void clickRight() { + if (countRightClicks){ + rightClicks.add(new Click()); + } + } + + @Override + public void tick() { + int left = 0; + int right = 0; + int middle = 0; + if (countLeftClicks) { + leftClicks.removeIf(click -> click.tick() >= 20); + left = leftClicks.size(); + } + if (countRightClicks) { + rightClicks.removeIf(click -> click.tick() >= 20); + right = rightClicks.size(); + } + if (countMiddleClicks) { + middleClicks.removeIf(click -> click.tick() >= 20); + middle = middleClicks.size(); + } + switch (appearance){ + case UNIFIED -> renderText = Text.literal((left + right + middle) + " CPS"); + case SPLIT_PIPE, SPLIT_SLASH -> { + final StringBuilder sb = getClickText(left, middle, right); + renderText = Text.literal(sb + " CPS"); + } + } + } + + private @NotNull StringBuilder getClickText(int left, int middle, int right) { + StringBuilder sb = new StringBuilder(); + if (countLeftClicks){ + sb.append(left); + } + if (countMiddleClicks){ + if (!sb.isEmpty()){ + sb.append(appearance == Appearance.SPLIT_PIPE ? " | " : "/"); + } + sb.append(middle); + } + if (countRightClicks){ + if (!sb.isEmpty()){ + sb.append(appearance == Appearance.SPLIT_PIPE ? " | " : "/"); + } + sb.append(right); + } + return sb; + } + + @Override + public void onSettingsChanged(WidgetSettings settings) { + super.onSettingsChanged(settings); + countLeftClicks = ((ToggleWidgetSetting) settings.optionById("left")).getValue(); + countMiddleClicks = ((ToggleWidgetSetting) settings.optionById("middle")).getValue(); + countRightClicks = ((ToggleWidgetSetting) settings.optionById("right")).getValue(); + appearance = (Appearance) ((EnumWidgetSetting) settings.optionById("appearance")).getValue(); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.cps"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.cps.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/ClockWidget.java b/src/main/java/de/shiewk/widgets/widgets/ClockWidget.java new file mode 100644 index 0000000..218e9fe --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/ClockWidget.java @@ -0,0 +1,149 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.EnumWidgetSetting; +import de.shiewk.widgets.widgets.settings.ToggleWidgetSetting; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.Date; +import java.util.List; + +public class ClockWidget extends BasicTextWidget { + + private int width = DEFAULT_WIDTH; + + public enum TimeOption { + NO_TIME("none"), + HOUR_24("24hour"), + AM_PM("am_pm"); + + public final String key; + + TimeOption(String key) { + this.key = key; + } + } + + public enum DateOption { + NO_DATE(null), + FULL_MONTH_DAY("MMMM dd"), + DAY_FULL_MONTH("dd. MMMM"), + ABB_MONTH_DAY("MMM dd"), + DAY_ABB_MONTH("dd. MMM"), + MONTH_DAY("MM/dd"), + MONTH_DAY_2("dd.MM"), + FULL_MONTH_DAY_YEAR("MMMM dd, y"), + ABB_MONTH_DAY_YEAR("MMM dd, y"), + DAY_FULL_MONTH_YEAR("dd. MMMM y"), + DAY_ABB_MONTH_YEAR("dd. MMM y"), + MONTH_DAY_YEAR("MM/dd/y"), + MONTH_DAY_YEAR_2("dd.MM.y"); + + public final String format; + + DateOption(String format) { + this.format = format; + } + + public Text getName(){ + return this.format == null ? Text.translatable("widgets.widgets.clock.dateFormat.none") : Text.of(new SimpleDateFormat(format).format(Date.from(Instant.now()))); + } + } + + public enum WeekOption { + NO_DAY_OF_WEEK(null), + ABBREVIATED("E"), + FULL("EEEE"); + + public final String format; + + WeekOption(String format) { + this.format = format; + } + + public Text getName(){ + return this.format == null ? Text.translatable("widgets.widgets.clock.weekFormat.none") : Text.of(new SimpleDateFormat(format).format(Date.from(Instant.now()))); + } + } + public ClockWidget(Identifier id) { + super(id, List.of( + new EnumWidgetSetting<>("hour_format", + Text.translatable("widgets.widgets.clock.hourFormat"), + TimeOption.class, + TimeOption.HOUR_24, + timeOption -> Text.translatable("widgets.widgets.clock.hourFormat."+timeOption.key)), + new ToggleWidgetSetting("show_seconds", + Text.translatable("widgets.widgets.clock.showSeconds"), + true), + new EnumWidgetSetting<>("date_format", + Text.translatable("widgets.widgets.clock.dateFormat"), + DateOption.class, + DateOption.NO_DATE, + DateOption::getName), + new EnumWidgetSetting<>("week_format", + Text.translatable("widgets.widgets.clock.weekFormat"), + WeekOption.class, + WeekOption.NO_DAY_OF_WEEK, + WeekOption::getName) + )); + } + + private SimpleDateFormat dateFormat = new SimpleDateFormat("hh:mm:ss aa EEEE"); + + @Override + public int width() { + return width; + } + + @Override + public void tick() { + renderText = Text.literal(dateFormat.format(Date.from(Instant.now()))); + } + + @Override + public void onSettingsChanged(WidgetSettings settings) { + super.onSettingsChanged(settings); + String datePattern = "HH:mm:ss"; + TimeOption timeOption = (TimeOption) ((EnumWidgetSetting) settings.optionById("hour_format")).getValue(); + DateOption dateOption = (DateOption) ((EnumWidgetSetting) settings.optionById("date_format")).getValue(); + WeekOption weekOption = (WeekOption) ((EnumWidgetSetting) settings.optionById("week_format")).getValue(); + boolean showSeconds = ((ToggleWidgetSetting) settings.optionById("show_seconds")).getValue(); + if (timeOption == TimeOption.HOUR_24){ + datePattern = showSeconds ? "HH:mm:ss" : "HH:mm"; + } else if (timeOption == TimeOption.AM_PM){ + datePattern = showSeconds ? "hh:mm:ss aa" : "hh:mm aa"; + } else if (timeOption == TimeOption.NO_TIME){ + datePattern = ""; + } + if (dateOption.format != null){ + if (datePattern.isEmpty()) { + datePattern = dateOption.format + datePattern; + } else { + datePattern = dateOption.format + " " + datePattern; + } + } + if (weekOption.format != null){ + if (datePattern.isEmpty()){ + datePattern = weekOption.format + datePattern; + } else { + datePattern = weekOption.format + ", " + datePattern; + } + } + dateFormat = new SimpleDateFormat(datePattern); + width = Math.max(DEFAULT_WIDTH, MinecraftClient.getInstance().textRenderer.getWidth(dateFormat.format(Date.from(Instant.now()))) + 20); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.clock"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.clock.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/CoordinatesWidget.java b/src/main/java/de/shiewk/widgets/widgets/CoordinatesWidget.java new file mode 100644 index 0000000..1935952 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/CoordinatesWidget.java @@ -0,0 +1,115 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.RGBAColorWidgetSetting; +import de.shiewk.widgets.widgets.settings.ToggleWidgetSetting; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.List; + +public class CoordinatesWidget extends ModWidget { + public CoordinatesWidget(Identifier id) { + super(id, List.of( + new ToggleWidgetSetting("x", Text.translatable("widgets.widgets.coordinates.showX"), true), + new ToggleWidgetSetting("y", Text.translatable("widgets.widgets.coordinates.showY"), true), + new ToggleWidgetSetting("z", Text.translatable("widgets.widgets.coordinates.showZ"), true), + new RGBAColorWidgetSetting("backgroundcolor", Text.translatable("widgets.widgets.basictext.background"), 0, 0, 0, 80), + new RGBAColorWidgetSetting("textcolor", Text.translatable("widgets.widgets.basictext.textcolor"), 255, 255, 255, 255) + )); + } + + private String textX = "X", textY = "Y", textZ = "Z"; + private int txc = 0, tyc = 0, tzc = 0; + + @Override + public void render(DrawContext context, long measuringTimeNano, TextRenderer textRenderer, int posX, int posY) { + context.fill(posX, posY, posX + width(), posY + height(), this.backgroundColor); + final int PADDING = CoordinatesWidget.PADDING; + int y = PADDING; + if (showX){ + y++; + context.drawText(textRenderer, "X: ", posX + PADDING, posY + y, textColor, true); + context.drawText(textRenderer, textX, posX + txc, posY + y, textColor, true); + y += textRenderer.fontHeight + 1; + } + if (showY){ + y++; + context.drawText(textRenderer, "Y: ", posX + PADDING, posY + y, textColor, true); + context.drawText(textRenderer, textY, posX + tyc, posY + y, textColor, true); + y += textRenderer.fontHeight + 1; + } + if (showZ){ + y++; + context.drawText(textRenderer, "Z: ", posX + PADDING, posY + y, textColor, true); + context.drawText(textRenderer, textZ, posX + tzc, posY + y, textColor, true); + } + } + + @Override + public void tick() { + final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + txc = width() - textRenderer.getWidth(textX) - PADDING; + tyc = width() - textRenderer.getWidth(textY) - PADDING; + tzc = width() - textRenderer.getWidth(textZ) - PADDING; + + final ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (player == null){ + textX = "?"; + textY = "?"; + textZ = "?"; + } else { + textX = String.valueOf(player.getBlockX()); + textY = String.valueOf(player.getBlockY()); + textZ = String.valueOf(player.getBlockZ()); + } + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.coordinates"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.coordinates.description"); + } + + protected static final int + WIDTH = 80, + PADDING = 6, + DEFAULT_BACKGROUND_COLOR = new Color(0, 0, 0, 80).getRGB(), + DEFAULT_TEXT_COLOR = new Color(255, 255 ,255, 255).getRGB(); + + protected int backgroundColor = DEFAULT_BACKGROUND_COLOR, textColor = DEFAULT_TEXT_COLOR; + protected boolean showX = true, showY = true, showZ = true; + + @Override + public void onSettingsChanged(WidgetSettings settings) { + this.backgroundColor = ((RGBAColorWidgetSetting) settings.optionById("backgroundcolor")).getColor(); + this.textColor = ((RGBAColorWidgetSetting) settings.optionById("textcolor")).getColor(); + this.showX = ((ToggleWidgetSetting) settings.optionById("x")).getValue(); + this.showY = ((ToggleWidgetSetting) settings.optionById("y")).getValue(); + this.showZ = ((ToggleWidgetSetting) settings.optionById("z")).getValue(); + } + + @Override + public int width() { + return WIDTH; + } + + @Override + public int height() { + int height = 2 * PADDING; + if (showX) height += 11; + if (showY) height += 11; + if (showZ) height += 11; + return height; + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/FPSWidget.java b/src/main/java/de/shiewk/widgets/widgets/FPSWidget.java new file mode 100644 index 0000000..41eeebe --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/FPSWidget.java @@ -0,0 +1,28 @@ +package de.shiewk.widgets.widgets; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.List; + +public class FPSWidget extends BasicTextWidget { + public FPSWidget(Identifier id) { + super(id, List.of()); + } + + @Override + public void tick() { + this.renderText = Text.literal(MinecraftClient.getInstance().getCurrentFps() + " FPS"); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.fps"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.fps.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/PingWidget.java b/src/main/java/de/shiewk/widgets/widgets/PingWidget.java new file mode 100644 index 0000000..eec5d94 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/PingWidget.java @@ -0,0 +1,69 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.ToggleWidgetSetting; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import net.minecraft.util.profiler.MultiValueDebugSampleLogImpl; + +import java.util.List; + +public class PingWidget extends BasicTextWidget { + + long lastPingQuery = 0; + public PingWidget(Identifier id) { + super(id, List.of( + new ToggleWidgetSetting("dynamic_color", Text.translatable("widgets.widgets.ping.dynamicColor"), true) + )); + } + private boolean dynamicColor = false; + + @Override + public void tick() { + final ClientPlayNetworkHandler networkHandler = MinecraftClient.getInstance().getNetworkHandler(); + if (networkHandler != null){ + if (lastPingQuery < Util.getMeasuringTimeMs() - 5000){ + networkHandler.pingMeasurer.ping(); + lastPingQuery = Util.getMeasuringTimeMs(); + } + final MultiValueDebugSampleLogImpl pingLog = networkHandler.pingMeasurer.log; + final int logLength = pingLog.getLength(); + final int avgCompileLength = 3; + long ping = 0; + for (int i = logLength-1; i > logLength-avgCompileLength; i--) { + if (i < 0) break; + ping += pingLog.get(i); + } + long avgPing = ping / avgCompileLength; + this.renderText = Text.of(avgPing + " ms"); + if (this.dynamicColor){ + if (avgPing < 50){ + this.textColor = 0x00ff00; + } else if (avgPing < 120) { + this.textColor = 0xffff00; + } else { + this.textColor = 0xff3030; + } + } + } + } + + @Override + public void onSettingsChanged(WidgetSettings settings) { + super.onSettingsChanged(settings); + this.dynamicColor = ((ToggleWidgetSetting) settings.optionById("dynamic_color")).getValue(); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.ping"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.ping.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/PlayerCountWidget.java b/src/main/java/de/shiewk/widgets/widgets/PlayerCountWidget.java new file mode 100644 index 0000000..301d95f --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/PlayerCountWidget.java @@ -0,0 +1,31 @@ +package de.shiewk.widgets.widgets; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.List; + +public class PlayerCountWidget extends BasicTextWidget{ + public PlayerCountWidget(Identifier id) { + super(id, List.of()); + } + + @Override + public void tick() { + final ClientPlayNetworkHandler networkHandler = MinecraftClient.getInstance().getNetworkHandler(); + String online = networkHandler == null ? "?" : String.valueOf(networkHandler.getPlayerUuids().size()); + this.renderText = Text.literal(Text.translatable("widgets.widgets.playerCount.online", online).getString()); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.playerCount"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.playerCount.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/ServerIPWidget.java b/src/main/java/de/shiewk/widgets/widgets/ServerIPWidget.java new file mode 100644 index 0000000..9d153cd --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/ServerIPWidget.java @@ -0,0 +1,47 @@ +package de.shiewk.widgets.widgets; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.List; + +public class ServerIPWidget extends BasicTextWidget { + public ServerIPWidget(Identifier id) { + super(id, List.of()); + } + + private int width; + private int t = 0; + + @Override + public void tick() { + final ServerInfo serverEntry = MinecraftClient.getInstance().getCurrentServerEntry(); + if (serverEntry != null){ + this.renderText = Text.of(serverEntry.address); + } else { + this.renderText = Text.translatable("menu.singleplayer"); + } + t++; + if (t >= 20){ + t = 0; + this.width = MinecraftClient.getInstance().textRenderer.getWidth(this.renderText) + 20; + } + } + + @Override + public int width() { + return Math.max(super.width(), this.width); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.serverIP"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.serverIP.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/settings/EnumWidgetSetting.java b/src/main/java/de/shiewk/widgets/widgets/settings/EnumWidgetSetting.java new file mode 100644 index 0000000..04d51e4 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/settings/EnumWidgetSetting.java @@ -0,0 +1,106 @@ +package de.shiewk.widgets.widgets.settings; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import de.shiewk.widgets.WidgetSettingOption; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; + +import java.awt.*; +import java.util.function.Function; + +public class EnumWidgetSetting> extends WidgetSettingOption { + + private final Class enumClass; + private T value; + private final Function enumNameGetter; + private int height = 0; + private boolean mouseClick = false; + private boolean changed = false; + + public EnumWidgetSetting(String id, Text name, Class enumClass, T defaultValue, Function enumNameGetter) { + super(id, name); + this.enumClass = enumClass; + this.value = defaultValue; + this.enumNameGetter = enumNameGetter; + } + + public T getValue(){ + return value; + } + + @Override + public JsonElement saveState() { + return new JsonPrimitive(value.name()); + } + + @Override + public void loadState(JsonElement state) { + if (state.isJsonPrimitive() && state.getAsJsonPrimitive().isString()){ + final String name = state.getAsString(); + for (T constant : enumClass.getEnumConstants()) { + if (constant.name().equals(name)){ + this.value = constant; + } + } + } + } + + private static final int COLOR_SELECTED = new Color(0, 0x5f, 0x68, 255).getRGB(), + COLOR_UNSELECTED = new Color(0, 0, 0, 50).getRGB(), + COLOR_UNSELECTED_HOVER = new Color(80, 80, 80, 50).getRGB(), + COLOR_TEXT = new Color(255, 255, 255, 255).getRGB(); + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + int y = 0; + int nx = 5; + final int bx = getX() + getWidth(); + for (T constant : enumClass.getEnumConstants()) { + final Text name = enumNameGetter.apply(constant); + final int textRendererWidth = textRenderer.getWidth(name); + if (nx != 5 && nx + textRendererWidth + 20 > this.getWidth()){ + y += 24; + nx = 5; + } + final boolean hover = mouseX <= bx - nx && mouseX >= bx - nx - 10 - textRendererWidth && mouseY <= y + 19 + getY() && mouseY >= y + getY(); + context.fill(bx - 10 - textRendererWidth - nx, y + getY(), bx - nx, y + 19 + getY(), constant == value ? COLOR_SELECTED : hover ? COLOR_UNSELECTED_HOVER : COLOR_UNSELECTED); + context.drawText(textRenderer, name, bx - nx - 5 - textRendererWidth, y + 5 + getY(), COLOR_TEXT, true); + if (hover && mouseClick){ + this.value = constant; + this.changed = true; + } + nx += textRendererWidth + 20; + } + + mouseClick = false; + this.height = y + 34; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + mouseClick = true; + return false; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + mouseClick = false; + boolean changed = this.changed; + this.changed = false; + return changed; + } + + @Override + public int getWidth() { + return 200; + } + + @Override + public int getHeight() { + return height; + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/settings/RGBAColorWidgetSetting.java b/src/main/java/de/shiewk/widgets/widgets/settings/RGBAColorWidgetSetting.java new file mode 100644 index 0000000..e518474 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/settings/RGBAColorWidgetSetting.java @@ -0,0 +1,123 @@ +package de.shiewk.widgets.widgets.settings; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import de.shiewk.widgets.WidgetSettingOption; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.math.MathHelper; + +import java.awt.*; + +public class RGBAColorWidgetSetting extends WidgetSettingOption { + public RGBAColorWidgetSetting(String id, Text name, int defaultR, int defaultG, int defaultB, int defaultAlpha) { + super(id, name); + this.r = defaultR; + this.g = defaultG; + this.b = defaultB; + this.a = defaultAlpha; + } + + private int r; + private int g; + private int b; + private int a; + private boolean mouseClicked = false; + private int sv = 0; + + @Override + public JsonElement saveState() { + return new JsonPrimitive(getColor()); + } + + public int getColor(){ + return new Color(r, g, b, a).getRGB(); + } + + @Override + public void loadState(JsonElement state) { + if (state.isJsonPrimitive() && state.getAsJsonPrimitive().isNumber()){ + final Color color = new Color(state.getAsJsonPrimitive().getAsInt(), true); + this.r = color.getRed(); + this.g = color.getGreen(); + this.b = color.getBlue(); + this.a = color.getAlpha(); + } + } + + @Override + public int getWidth() { + return 28 + 127 + 7; + } + + @Override + public int getHeight() { + return 95; + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + int o = 0; + for (int i = 0; i <= 255; i+=2) { + context.drawVerticalLine(this.getX() + 5 + 7 + o, this.getY() + 5, this.getY() + 20, new Color(i, g, b, a).getRGB()); + context.drawVerticalLine(this.getX() + 5 + 7 + o, this.getY() + 25, this.getY() + 40, new Color(r, i, b, a).getRGB()); + context.drawVerticalLine(this.getX() + 5 + 7 + o, this.getY() + 45, this.getY() + 60, new Color(r, g, i, a).getRGB()); + context.drawVerticalLine(this.getX() + 5 + 7 + o, this.getY() + 65, this.getY() + 80, new Color(r, g, b, i).getRGB()); + o++; + } + + context.drawText(textRenderer, "R", this.getX() + 7 - textRenderer.getWidth("R"), this.getY() + 5 + 4, 0xffffff, true); + context.drawText(textRenderer, "G", this.getX() + 7 - textRenderer.getWidth("G"), this.getY() + 25 + 4, 0xffffff, true); + context.drawText(textRenderer, "B", this.getX() + 7 - textRenderer.getWidth("B"), this.getY() + 45 + 4, 0xffffff, true); + context.drawText(textRenderer, "A", this.getX() + 7 - textRenderer.getWidth("A"), this.getY() + 65 + 4, 0xffffff, true); + + context.drawText(textRenderer, String.valueOf(r), this.getX() + this.getWidth() - 19, this.getY() + 5 + 4, 0xffffff, true); + context.drawText(textRenderer, String.valueOf(g), this.getX() + this.getWidth() - 19, this.getY() + 25 + 4, 0xffffff, true); + context.drawText(textRenderer, String.valueOf(b), this.getX() + this.getWidth() - 19, this.getY() + 45 + 4, 0xffffff, true); + context.drawText(textRenderer, String.valueOf(a), this.getX() + this.getWidth() - 19, this.getY() + 65 + 4, 0xffffff, true); + + context.drawVerticalLine(this.getX() + 5 + 7 + r/2, this.getY() + 4, this.getY() + 21, 0xffffffff); + context.drawVerticalLine(this.getX() + 5 + 7 + g/2, this.getY() + 24, this.getY() + 41, 0xffffffff); + context.drawVerticalLine(this.getX() + 5 + 7 + b/2, this.getY() + 44, this.getY() + 61, 0xffffffff); + context.drawVerticalLine(this.getX() + 5 + 7 + a/2, this.getY() + 64, this.getY() + 81, 0xffffffff); + + if (mouseClicked){ + int col = MathHelper.clamp((mouseX - this.getX() - 5 - 7) * 2, 0, 255); + if (sv == 0){ + if (mouseY > this.getY() + 5 && mouseY < this.getY() + 20){ + sv = 1; + } else if (mouseY > this.getY() + 25 && mouseY < this.getY() + 40){ + sv = 2; + } else if (mouseY > this.getY() + 45 && mouseY < this.getY() + 60){ + sv = 3; + } else if (mouseY > this.getY() + 65 && mouseY < this.getY() + 80){ + sv = 4; + } + } + switch (sv){ + case 1 -> r = col; + case 2 -> g = col; + case 3 -> b = col; + case 4 -> a = col; + } + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + this.mouseClicked = true; + sv = 0; + return false; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + this.mouseClicked = false; + final boolean c = this.sv != 0; + this.sv = 0; + return c; + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/settings/ToggleWidgetSetting.java b/src/main/java/de/shiewk/widgets/widgets/settings/ToggleWidgetSetting.java new file mode 100644 index 0000000..39a5def --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/settings/ToggleWidgetSetting.java @@ -0,0 +1,78 @@ +package de.shiewk.widgets.widgets.settings; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import de.shiewk.widgets.WidgetSettingOption; +import de.shiewk.widgets.WidgetUtils; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Util; +import net.minecraft.util.math.MathHelper; + +import java.awt.*; + +public class ToggleWidgetSetting extends WidgetSettingOption { + + private boolean value; + private long toggleTime = 0; + + public ToggleWidgetSetting(String id, Text name, boolean defaultValue) { + super(id, name); + this.value = defaultValue; + } + + public boolean getValue(){ + return value; + } + + @Override + public JsonElement saveState() { + return new JsonPrimitive(value); + } + + @Override + public void loadState(JsonElement state) { + if (state.isJsonPrimitive() && state.getAsJsonPrimitive().isBoolean()){ + this.value = state.getAsBoolean(); + } + } + + protected static final int COLOR_ENABLED = new Color(0, 255, 0, 255).getRGB(), + COLOR_ENABLED_THUMB = new Color(0, 175, 0, 255).getRGB(), + COLOR_DISABLED = new Color(255, 0, 0, 255).getRGB(), + COLOR_DISABLED_THUMB = new Color(255, 200, 200, 255).getRGB(); + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + int col = value ? COLOR_ENABLED : COLOR_DISABLED; + int thumb = value ? COLOR_ENABLED_THUMB : COLOR_DISABLED_THUMB; + int thumbLoc = (Util.getMeasuringTimeNano() - toggleTime) < 300000000f ? + (int) (getX() + MathHelper.lerp(WidgetUtils.computeEasing((Util.getMeasuringTimeNano() - toggleTime) / 300000000f), + value ? 2 : getWidth() - 4 - 12, + value ? getWidth() - 4 - 12 : 2)) + : value ? getX() + getWidth() - 4 - 12 : getX() + 4; + context.fill(getX() + 2, getY() + 2, getX() + getWidth() - 2, getY() + getHeight() - 2, col); + context.fill(thumbLoc, getY() + 4, thumbLoc + 12, getY() + getHeight() - 4, thumb); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + toggle(); + return true; + } + + public void toggle(){ + this.value = !value; + this.toggleTime = Util.getMeasuringTimeNano(); + } + + @Override + public int getWidth() { + return 50; + } + + @Override + public int getHeight() { + return 20; + } +} diff --git a/src/main/resources/assets/widgets/icon.png b/src/main/resources/assets/widgets/icon.png new file mode 100644 index 0000000..65dbd58 Binary files /dev/null and b/src/main/resources/assets/widgets/icon.png differ diff --git a/src/main/resources/assets/widgets/lang/de_de.json b/src/main/resources/assets/widgets/lang/de_de.json new file mode 100644 index 0000000..c1f4fb3 --- /dev/null +++ b/src/main/resources/assets/widgets/lang/de_de.json @@ -0,0 +1,51 @@ +{ + "widgets.ui.config": "Widgets-Konfiguration", + "widgets.ui.editPositions": "Layout bearbeiten", + "widgets.widgets.fps": "FPS", + "widgets.widgets.fps.description": "Zeigt deine aktuellen FPS an.", + "widgets.ui.disabled": "Deaktiviert", + "widgets.ui.enabled": "Aktiviert", + "widgets.ui.search": "Suchen...", + "widgets.widgets.clock": "Uhr/Datum", + "widgets.widgets.clock.description": "Zeigt die aktuelle Uhrzeit und/oder das Datum an", + "widgets.ui.widgetSettings": "Bearbeite %s Einstellungen", + "widgets.widgets.basictext.background": "Hintergrundfarbe", + "widgets.widgets.basictext.textcolor": "Textfarbe", + "widgets.ui.preview": "Vorschau", + "widgets.widgets.clock.hourFormat": "Stundenformat", + "widgets.widgets.clock.hourFormat.none": "Keine Zeitangabe", + "widgets.widgets.clock.hourFormat.24hour": "24-Stunden-Zeit", + "widgets.widgets.clock.hourFormat.am_pm": "AM/PM", + "widgets.widgets.clock.dateFormat.none": "Kein Datum", + "widgets.widgets.clock.dateFormat": "Datumsformat", + "widgets.widgets.clock.weekFormat.none": "Nicht anzeigen", + "widgets.widgets.clock.weekFormat": "Wochentagformat", + "widgets.widgets.clock.showSeconds": "Sekunden anzeigen", + "widgets.widgets.coordinates": "Koordinaten", + "widgets.widgets.coordinates.description": "Zeigt deine aktuellen Koordinaten an", + "widgets.widgets.coordinates.showX": "X-Koordinate anzeigen:", + "widgets.widgets.coordinates.showY": "Y-Koordinate anzeigen:", + "widgets.widgets.coordinates.showZ": "Z-Koordinate anzeigen:", + "widgets.widgets.bandwidth": "Bandbreite", + "widgets.widgets.bandwidth.description": "Zeigt, wie viele Daten an den aktuell Server gesendet werden/vom Server an den Klient gesendet werden.", + "widgets.widgets.ping": "Ping", + "widgets.widgets.ping.description": "Zeigt deine Latenz zum Server an", + "widgets.widgets.bandwidth.dynamicColor": "Farbe dynamisch anzeigen", + "widgets.widgets.ping.dynamicColor": "Farbe dynamisch anzeigen", + "widgets.widgets.serverIP": "Server-IP", + "widgets.widgets.serverIP.description": "Zeigt die Serveradresse an", + "widgets.widgets.playerCount": "Spieleranzahl", + "widgets.widgets.playerCount.description": "Zeigt die Anzahl der online Spieler an. Dies könnte auf manchen Servern ungenau sein (besonders auf Servern, die gefälschte Spieler in der Tab-Liste anzeigen)", + "widgets.widgets.playerCount.online": "%s online", + "widgets.key.config": "Öffne Widget-Einstellungen", + "widgets.key.category": "Widgets", + "widgets.widgets.cps": "CPS", + "widgets.widgets.cps.description": "Zeigt deine Klicks pro Sekunde an", + "widgets.widgets.cps.left": "Zeigt/Zählt Links-Klicks", + "widgets.widgets.cps.middle": "Zeigt/Zählt Mittel-Klicks", + "widgets.widgets.cps.right": "Zeigt/Zählt Rechts-Klicks", + "widgets.widgets.cps.appearance": "Erscheinungsbild", + "widgets.widgets.cps.appearance.pipe": "Getrennt (Senkrechter Strich)", + "widgets.widgets.cps.appearance.slash": "Getrennt (Schrägstrich)", + "widgets.widgets.cps.appearance.unified": "Vereinheitlicht" +} diff --git a/src/main/resources/assets/widgets/lang/en_us.json b/src/main/resources/assets/widgets/lang/en_us.json new file mode 100644 index 0000000..109f26f --- /dev/null +++ b/src/main/resources/assets/widgets/lang/en_us.json @@ -0,0 +1,51 @@ +{ + "widgets.ui.config": "Widgets Config", + "widgets.ui.editPositions": "Edit Layout", + "widgets.widgets.fps": "FPS", + "widgets.widgets.fps.description": "Shows your current FPS.", + "widgets.ui.disabled": "Disabled", + "widgets.ui.enabled": "Enabled", + "widgets.ui.search": "Search...", + "widgets.widgets.clock": "Clock/Date", + "widgets.widgets.clock.description": "Shows the current time and/or date", + "widgets.ui.widgetSettings": "Edit %s settings", + "widgets.widgets.basictext.background": "Background color", + "widgets.widgets.basictext.textcolor": "Text color", + "widgets.ui.preview": "Preview", + "widgets.widgets.clock.hourFormat": "Hour Format", + "widgets.widgets.clock.hourFormat.none": "No time", + "widgets.widgets.clock.hourFormat.24hour": "Military Time", + "widgets.widgets.clock.hourFormat.am_pm": "AM/PM", + "widgets.widgets.clock.dateFormat.none": "No date", + "widgets.widgets.clock.dateFormat": "Date format", + "widgets.widgets.clock.weekFormat.none": "Don't show", + "widgets.widgets.clock.weekFormat": "Day of week format", + "widgets.widgets.clock.showSeconds": "Show seconds", + "widgets.widgets.coordinates": "Coordinates", + "widgets.widgets.coordinates.description": "Shows your current coordinates", + "widgets.widgets.coordinates.showX": "Show X coordinate:", + "widgets.widgets.coordinates.showY": "Show Y coordinate:", + "widgets.widgets.coordinates.showZ": "Show Z coordinate:", + "widgets.widgets.bandwidth": "Bandwidth", + "widgets.widgets.bandwidth.description": "Shows how much data is being read/sent from the server you're connected to.", + "widgets.widgets.ping": "Ping", + "widgets.widgets.ping.description": "Shows your latency to the server", + "widgets.widgets.bandwidth.dynamicColor": "Dynamic Color", + "widgets.widgets.ping.dynamicColor": "Dynamic Color", + "widgets.widgets.serverIP": "Server IP", + "widgets.widgets.serverIP.description": "Shows the server address", + "widgets.widgets.playerCount": "Player count", + "widgets.widgets.playerCount.description": "Shows the number of players online. May not be accurate on all servers (especially those that spawn fake players in the tab list)", + "widgets.widgets.playerCount.online": "%s online", + "widgets.key.config": "Open Widget Management", + "widgets.key.category": "Widgets", + "widgets.widgets.cps": "CPS", + "widgets.widgets.cps.description": "Shows your clicks per second", + "widgets.widgets.cps.left": "Display/Count left clicks", + "widgets.widgets.cps.middle": "Display/Count middle clicks", + "widgets.widgets.cps.right": "Display/Count right clicks", + "widgets.widgets.cps.appearance": "Appearance", + "widgets.widgets.cps.appearance.pipe": "Split (Pipe)", + "widgets.widgets.cps.appearance.slash": "Split (Slash)", + "widgets.widgets.cps.appearance.unified": "Unified" +} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..e4911cb --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "id": "widgets", + "version": "${version}", + "name": "Widgets", + "description": "Adds customizable in-game widgets to your game.", + "authors": [], + "contact": { + "sources": "https://github.com/Shiewk/widgets", + "issues": "https://github.com/Shiewk/widgets/issues" + }, + "license": "LGPL-3.0-only", + "icon": "assets/widgets/icon.png", + "accessWidener": "widgets.accesswidener", + "environment": "client", + "entrypoints": { + "client": [ + "de.shiewk.widgets.client.WidgetsModClient" + ], + "main": [ + "de.shiewk.widgets.WidgetsMod" + ], + "modmenu": [ + "de.shiewk.widgets.ModMenuConfig" + ] + }, + "mixins": [ + "widgets.mixins.json" + ], + "depends": { + "fabricloader": ">=${loader_version}", + "fabric": "*", + "minecraft": "~${minecraft_version}" + } +} diff --git a/src/main/resources/widgets.accesswidener b/src/main/resources/widgets.accesswidener new file mode 100644 index 0000000..4e35977 --- /dev/null +++ b/src/main/resources/widgets.accesswidener @@ -0,0 +1,4 @@ +accessWidener v2 named + +accessible field net/minecraft/client/network/ClientPlayNetworkHandler pingMeasurer Lnet/minecraft/client/network/PingMeasurer; +accessible field net/minecraft/client/network/PingMeasurer log Lnet/minecraft/util/profiler/MultiValueDebugSampleLogImpl; \ No newline at end of file diff --git a/src/main/resources/widgets.mixins.json b/src/main/resources/widgets.mixins.json new file mode 100644 index 0000000..f97a76f --- /dev/null +++ b/src/main/resources/widgets.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "de.shiewk.widgets.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + ], + "client": [ + "MixinMouse" + ], + "injectors": { + "defaultRequire": 1 + } +}