diff --git a/build.gradle.kts b/build.gradle.kts index df957a7..a09fe27 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ plugins { } group = "dev.isxander" -version = "1.4.6+1.20" +version = "1.5.0+1.20" val isAlpha = "alpha" in version.toString() val isBeta = "beta" in version.toString() if (isAlpha) println("Alpha version detected.") diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 998388c..5ac80e0 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -45,10 +45,10 @@ import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.libsdl.SDL; import org.lwjgl.glfw.GLFW; import java.util.ArrayDeque; +import java.util.List; import java.util.Optional; import java.util.Queue; import java.util.concurrent.CompletableFuture; @@ -124,6 +124,8 @@ public class Controlify implements ControlifyApi { e.printStackTrace(); } }); + + notifyOfNewFeatures(); } private CompletableFuture askNatives() { @@ -583,6 +585,31 @@ public class Controlify implements ControlifyApi { } } + private void notifyOfNewFeatures() { + if (config().isFirstLaunch()) + return; + + var newFeatureVersions = List.of( + "1.5.0" + ).iterator(); + + String foundVersion = null; + while (foundVersion == null && newFeatureVersions.hasNext()) { + var version = newFeatureVersions.next(); + if (config().isLastSeenVersionLessThan(version)) { + foundVersion = version; + } + } + + if (foundVersion != null) { + ToastUtils.sendToast( + Component.translatable("controlify.new_features.title", foundVersion), + Component.translatable("controlify.new_features." + foundVersion), + true + ); + } + } + public static Controlify instance() { if (instance == null) instance = new Controlify(); return instance; diff --git a/src/main/java/dev/isxander/controlify/config/ClassTypeAdapter.java b/src/main/java/dev/isxander/controlify/config/ClassTypeAdapter.java deleted file mode 100644 index 1df5115..0000000 --- a/src/main/java/dev/isxander/controlify/config/ClassTypeAdapter.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.isxander.controlify.config; - -import com.google.gson.*; - -import java.lang.reflect.Type; - -public class ClassTypeAdapter implements JsonSerializer>, JsonDeserializer> { - - @Override - public Class deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - try { - return Class.forName(json.getAsString()); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - - @Override - public JsonElement serialize(Class src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.getName()); - } -} diff --git a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java index b4e1e3e..cb1c877 100644 --- a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java +++ b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java @@ -8,6 +8,8 @@ import dev.isxander.controlify.controller.joystick.CompoundJoystickInfo; import dev.isxander.controlify.utils.DebugLog; import dev.isxander.controlify.utils.Log; import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -25,7 +27,8 @@ public class ControlifyConfig { .serializeNulls() .setPrettyPrinting() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) - .registerTypeHierarchyAdapter(Class.class, new ClassTypeAdapter()) + .registerTypeHierarchyAdapter(Class.class, new TypeAdapters.ClassTypeAdapter()) + .registerTypeHierarchyAdapter(Version.class, new TypeAdapters.VersionTypeAdapter()) .create(); private final Controlify controlify; @@ -35,6 +38,7 @@ public class ControlifyConfig { private Map compoundJoysticks = Map.of(); private GlobalSettings globalSettings = new GlobalSettings(); private boolean firstLaunch; + private Version lastSeenVersion = null; private boolean dirty; @@ -59,6 +63,14 @@ public class ControlifyConfig { if (!Files.exists(CONFIG_PATH)) { firstLaunch = true; + if (lastSeenVersion == null) { + try { + lastSeenVersion = Version.parse("0.0.0"); + } catch (VersionParsingException e) { + throw new RuntimeException(e); + } + setDirty(); + } save(); return; } @@ -78,6 +90,8 @@ public class ControlifyConfig { private JsonObject generateConfig() { JsonObject config = new JsonObject(); + config.addProperty("last_seen_version", Log.VERSION.getFriendlyString()); + JsonObject newControllerData = controllerData.deepCopy(); // we use the old config, so we don't lose disconnected controller data for (var controller : ControllerManager.getConnectedControllers()) { @@ -103,7 +117,15 @@ public class ControlifyConfig { return object; } - private void applyConfig(JsonObject object) { + private void applyConfig(JsonObject object) throws VersionParsingException { + if (lastSeenVersion == null) { + boolean hasLastSeenVersion = object.has("last_seen_version"); + lastSeenVersion = hasLastSeenVersion ? Version.parse(object.get("last_seen_version").getAsString()) : Version.parse("0.0.0"); + if (!hasLastSeenVersion || lastSeenVersion.compareTo(Log.VERSION) < 0) { + setDirty(); + } + } + globalSettings = GSON.fromJson(object.getAsJsonObject("global"), GlobalSettings.class); if (globalSettings == null) { globalSettings = new GlobalSettings(); @@ -185,6 +207,18 @@ public class ControlifyConfig { return firstLaunch; } + public boolean isLastSeenVersionLessThan(Version version) { + return lastSeenVersion.compareTo(version) < 0; + } + + public boolean isLastSeenVersionLessThan(String version) { + try { + return isLastSeenVersionLessThan(Version.parse(version)); + } catch (VersionParsingException e) { + throw new RuntimeException(e); + } + } + @Nullable public String currentControllerUid() { return currentControllerUid; diff --git a/src/main/java/dev/isxander/controlify/config/TypeAdapters.java b/src/main/java/dev/isxander/controlify/config/TypeAdapters.java new file mode 100644 index 0000000..bf6604d --- /dev/null +++ b/src/main/java/dev/isxander/controlify/config/TypeAdapters.java @@ -0,0 +1,42 @@ +package dev.isxander.controlify.config; + +import com.google.gson.*; +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; + +import java.lang.reflect.Type; + +public final class TypeAdapters { + public static class ClassTypeAdapter implements JsonSerializer>, JsonDeserializer> { + + @Override + public Class deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return Class.forName(json.getAsString()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + @Override + public JsonElement serialize(Class src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getName()); + } + } + + public static class VersionTypeAdapter implements JsonSerializer, JsonDeserializer { + @Override + public JsonElement serialize(Version src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getFriendlyString()); + } + + @Override + public Version deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + return Version.parse(json.getAsString()); + } catch (VersionParsingException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java index bb4bf7f..f8269c2 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -36,37 +36,61 @@ import java.util.stream.IntStream; public class ControllerConfigScreenFactory { private static final Function percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100)); private static final Function percentOrOffFormatter = v -> v == 0 ? CommonComponents.OPTION_OFF : percentFormatter.apply(v); + private static final Component newOptionLabel = Component.translatable("controlify.gui.new_options.label").withStyle(ChatFormatting.GOLD); + + private final List> newOptions = new ArrayList<>(); public static Screen generateConfigScreen(Screen parent, Controller controller) { + return new ControllerConfigScreenFactory().generateConfigScreen0(parent, controller); + } + + private Screen generateConfigScreen0(Screen parent, Controller controller) { ControllerConfig def = controller.defaultConfig(); ControllerConfig config = controller.config(); + var advancedCategory = createAdvancedCategory(controller); + var bindsCategory = createBindsCategory(controller); + var basicCategory = createBasicCategory(controller, def, config); // must be last for new options + return YetAnotherConfigLib.createBuilder() .title(Component.literal("Controlify")) - .category(createBasicCategory(controller, def, config)) - .category(createAdvancedCategory(controller)) - .category(createBindsCategory(controller)) + .category(basicCategory) + .category(advancedCategory) + .category(bindsCategory) .save(() -> Controlify.instance().config().save()) .build().generateScreen(parent); } - private static ConfigCategory createBasicCategory(Controller controller, ControllerConfig def, ControllerConfig config) { - return ConfigCategory.createBuilder() + private ConfigCategory createBasicCategory(Controller controller, ControllerConfig def, ControllerConfig config) { + var sensitivityGroup = makeSensitivityGroup(controller, def, config); + var controlsGroup = makeControlsGroup(controller, def, config); + var accessibilityGroup = makeAccessibilityGroup(controller, def, config); + var deadzoneGroup = makeDeadzoneGroup(controller, def, config); + + ConfigCategory.Builder builder = ConfigCategory.createBuilder() .name(Component.translatable("controlify.gui.config.category.basic")) .option(Option.createBuilder() .name(Component.translatable("controlify.gui.custom_name")) .description(OptionDescription.of(Component.translatable("controlify.gui.custom_name.tooltip"))) .binding(def.customName == null ? "" : def.customName, () -> config.customName == null ? "" : config.customName, v -> config.customName = (v.equals("") ? null : v)) .controller(StringControllerBuilder::create) - .build()) - .group(makeSensitivityGroup(controller, def, config)) - .group(makeControlsGroup(controller, def, config)) - .group(makeAccessibilityGroup(controller, controller.defaultConfig(), controller.config())) - .group(makeDeadzoneGroup(controller, controller.defaultConfig(), controller.config())) - .build(); + .build()); + if (!newOptions.isEmpty()) { + builder.group(OptionGroup.createBuilder() + .name(Component.translatable("controlify.gui.new_options").withStyle(ChatFormatting.GOLD, ChatFormatting.BOLD)) + .description(OptionDescription.of(Component.translatable("controlify.gui.new_options.tooltip"))) + .options(newOptions) + .build()); + } + builder.group(sensitivityGroup) + .group(controlsGroup) + .group(accessibilityGroup) + .group(deadzoneGroup); + + return builder.build(); } - private static OptionGroup makeSensitivityGroup(Controller controller, ControllerConfig def, ControllerConfig config) { + private OptionGroup makeSensitivityGroup(Controller controller, ControllerConfig def, ControllerConfig config) { return OptionGroup.createBuilder() .name(Component.translatable("controlify.gui.config.group.sensitivity")) .option(Option.createBuilder() @@ -108,7 +132,7 @@ public class ControllerConfigScreenFactory { .build(); } - private static OptionGroup makeControlsGroup(Controller controller, ControllerConfig def, ControllerConfig config) { + private OptionGroup makeControlsGroup(Controller controller, ControllerConfig def, ControllerConfig config) { Function holdToggleFormatter = v -> Component.translatable("controlify.gui.format.hold_toggle." + (v ? "toggle" : "hold")); return OptionGroup.createBuilder() @@ -145,7 +169,7 @@ public class ControllerConfigScreenFactory { .build(); } - private static OptionGroup makeAccessibilityGroup(Controller controller, ControllerConfig def, ControllerConfig config) { + private OptionGroup makeAccessibilityGroup(Controller controller, ControllerConfig def, ControllerConfig config) { return OptionGroup.createBuilder() .name(Component.translatable("controlify.config.group.accessibility")) .option(Option.createBuilder() @@ -186,7 +210,7 @@ public class ControllerConfigScreenFactory { .build(); } - private static OptionGroup makeDeadzoneGroup(Controller controller, ControllerConfig def, ControllerConfig config) { + private OptionGroup makeDeadzoneGroup(Controller controller, ControllerConfig def, ControllerConfig config) { var deadzoneOpts = new ArrayList>(); var group = OptionGroup.createBuilder() @@ -251,9 +275,9 @@ public class ControllerConfigScreenFactory { var axis = axes[i]; Option deadzoneOpt = Option.createBuilder() - .name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name())) + .name(Component.translatable("controlify.gui.axis_deadzone", axis.name())) .description(OptionDescription.createBuilder() - .text(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name())) + .text(Component.translatable("controlify.gui.axis_deadzone.tooltip", axis.name())) .text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) .build()) .binding(jsCfgDef.getDeadzone(i), () -> jsCfg.getDeadzone(i), v -> jsCfg.setDeadzone(i, v)) @@ -291,7 +315,7 @@ public class ControllerConfigScreenFactory { return group.build(); } - private static ConfigCategory createAdvancedCategory(Controller controller) { + private ConfigCategory createAdvancedCategory(Controller controller) { return ConfigCategory.createBuilder() .name(Component.translatable("controlify.config.category.advanced")) .option(Option.createBuilder() @@ -305,7 +329,7 @@ public class ControllerConfigScreenFactory { .build(); } - private static ConfigCategory createBindsCategory(Controller controller) { + private ConfigCategory createBindsCategory(Controller controller) { var category = ConfigCategory.createBuilder() .name(Component.translatable("controlify.gui.group.controls")); @@ -330,7 +354,7 @@ public class ControllerConfigScreenFactory { return category.build(); } - private static void updateConflictingBinds(List all) { + private void updateConflictingBinds(List all) { all.forEach(pair -> ((AbstractBindController) pair.option().controller()).setConflicting(false)); for (OptionBindPair opt : all) { @@ -351,7 +375,7 @@ public class ControllerConfigScreenFactory { } } - private static OptionGroup makeVibrationGroup(Controller controller) { + private OptionGroup makeVibrationGroup(Controller controller) { boolean canRumble = controller.supportsRumble(); var config = controller.config(); var def = controller.defaultConfig(); @@ -417,7 +441,7 @@ public class ControllerConfigScreenFactory { return vibrationGroup.build(); } - private static OptionGroup makeGyroGroup(Controller controller) { + private OptionGroup makeGyroGroup(Controller controller) { GamepadController gamepad = (controller instanceof GamepadController) ? (GamepadController) controller : null; boolean hasGyro = gamepad != null && gamepad.hasGyro(); diff --git a/src/main/java/dev/isxander/controlify/utils/Log.java b/src/main/java/dev/isxander/controlify/utils/Log.java index 4a5423b..9034bd0 100644 --- a/src/main/java/dev/isxander/controlify/utils/Log.java +++ b/src/main/java/dev/isxander/controlify/utils/Log.java @@ -1,8 +1,12 @@ package dev.isxander.controlify.utils; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.Version; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Log { public static final Logger LOGGER = LoggerFactory.getLogger("Controlify"); + + public static final Version VERSION = FabricLoader.getInstance().getModContainer("controlify").orElseThrow().getMetadata().getVersion(); } diff --git a/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java b/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java index 84e9d00..baa295c 100644 --- a/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java +++ b/src/main/java/dev/isxander/controlify/virtualmouse/VirtualMouseHandler.java @@ -80,11 +80,12 @@ public class VirtualMouseHandler { } var sensitivity = controller.config().virtualMouseSensitivity; + var windowSizeModifier = Math.max(minecraft.getWindow().getWidth(), minecraft.getWindow().getHeight()) / 800f; // quadratic function to make small movements smaller // abs to keep sign - targetX += impulseX * Mth.abs(impulseX) * 20f * sensitivity; - targetY += impulseY * Mth.abs(impulseY) * 20f * sensitivity; + targetX += impulseX * Mth.abs(impulseX) * 20f * sensitivity * windowSizeModifier; + targetY += impulseY * Mth.abs(impulseY) * 20f * sensitivity * windowSizeModifier; targetX = Mth.clamp(targetX, 0, minecraft.getWindow().getWidth()); targetY = Mth.clamp(targetY, 0, minecraft.getWindow().getHeight()); @@ -128,8 +129,8 @@ public class VirtualMouseHandler { if (!virtualMouseEnabled) return; if (Math.round(targetX * 100) / 100.0 != Math.round(currentX * 100) / 100.0 || Math.round(targetY * 100) / 100.0 != Math.round(currentY * 100) / 100.0) { - currentX = Mth.lerp(minecraft.getDeltaFrameTime(), currentX, targetX); - currentY = Mth.lerp(minecraft.getDeltaFrameTime(), currentY, targetY); + currentX = Mth.lerp(minecraft.getFrameTime(), currentX, targetX); + currentY = Mth.lerp(minecraft.getFrameTime(), currentY, targetY); ((MouseHandlerAccessor) minecraft.mouseHandler).invokeOnMove(minecraft.getWindow().getWindow(), currentX, currentY); } else { diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index cb8ef37..ffd29e4 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -34,6 +34,9 @@ "controlify.gui.ingame_button_guide_scale.tooltip.warning": "This may cause scaling issues that will make it to look bad at anything but 100%.", "controlify.gui.open_issue_tracker": "Open Issue Tracker", + "controlify.gui.new_options": "New This Update", + "controlify.gui.new_options.tooltip": "A showcase of the new options in the latest updates.", + "controlify.gui.new_options.label": "This option is new!", "controlify.gui.battery_level": "Your controller battery is currently %s.", "controlify.gui.custom_name": "Display Name", "controlify.gui.custom_name.tooltip": "Name to display for this controller throughout Minecraft.", @@ -125,6 +128,9 @@ "controlify.gui.controller_unavailable": "Controller unavailable and cannot be edited.", + "controlify.new_features.title": "Controlify updated to %s!", + "controlify.new_features.1.5.0": "Added a radial menu that can be configured to any action you want. You can find it in your controller settings.", + "controlify.vibration_strength.controlify.master": "Master", "controlify.vibration_strength.controlify.master.tooltip": "The strength of all vibrations. Will also affect the strength of all other sources.", "controlify.vibration_strength.controlify.damage": "Take Damage",