diff --git a/.gitignore b/.gitignore index 2f31456..0c1ff9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ run +runserver build /.idea .gradle diff --git a/build.gradle.kts b/build.gradle.kts index 8c7fc9d..9b60b00 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -74,9 +74,12 @@ dependencies { "fabric-key-binding-api-v1", "fabric-registry-sync-v0", "fabric-screen-api-v1", + "fabric-command-api-v2", + "fabric-networking-api-v1", ).forEach { modImplementation(fabricApi.module(it, libs.versions.fabric.api.get())) } + modRuntimeOnly(libs.fabric.api) listOf( // sodium requirements diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5062ca0..267438a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,13 +8,13 @@ machete = "2.+" grgit = "5.0.+" blossom = "1.3.+" -minecraft = "1.20-pre6" +minecraft = "1.20" quilt_mappings = "1" -fabric_loader = "0.14.19" -fabric_api = "0.81.2+1.20" +fabric_loader = "0.14.21" +fabric_api = "0.83.0+1.20" mixin_extras = "0.2.0-beta.8" -yet_another_config_lib = "3.0.0-beta.7+1.20" -mod_menu = "7.0.0-beta.2" +yet_another_config_lib = "3.0.1+1.20" +mod_menu = "7.0.0" hid4java = "0.7.0" quilt_json5 = "1.0.3" sodium = "f041f7ccba" diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index f1c4e3c..c58086f 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -1,7 +1,6 @@ package dev.isxander.controlify; import com.mojang.blaze3d.Blaze3D; -import com.mojang.logging.LogUtils; import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint; import dev.isxander.controlify.gui.controllers.ControllerBindHandler; @@ -12,6 +11,7 @@ import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen; import dev.isxander.controlify.gui.screen.SDLOnboardingScreen; +import dev.isxander.controlify.reacharound.ReachAroundHandler; import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.config.ControlifyConfig; import dev.isxander.controlify.controller.hid.ControllerHIDService; @@ -19,12 +19,19 @@ import dev.isxander.controlify.api.event.ControlifyEvents; import dev.isxander.controlify.gui.guide.InGameButtonGuide; import dev.isxander.controlify.ingame.InGameInputHandler; import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor; +import dev.isxander.controlify.server.EntityVibrationPacket; +import dev.isxander.controlify.server.OriginVibrationPacket; +import dev.isxander.controlify.server.ReachAroundPolicyPacket; +import dev.isxander.controlify.server.VibrationPacket; import dev.isxander.controlify.sound.ControlifySounds; import dev.isxander.controlify.utils.DebugLog; +import dev.isxander.controlify.utils.Log; import dev.isxander.controlify.utils.ToastUtils; import dev.isxander.controlify.virtualmouse.VirtualMouseHandler; import dev.isxander.controlify.wireless.LowBatteryNotifier; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; @@ -36,7 +43,6 @@ import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; -import org.slf4j.Logger; import java.util.ArrayDeque; import java.util.Optional; @@ -45,7 +51,6 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.IntStream; public class Controlify implements ControlifyApi { - public static final Logger LOGGER = LogUtils.getLogger(); private static Controlify instance = null; private final Minecraft minecraft = Minecraft.getInstance(); @@ -72,7 +77,7 @@ public class Controlify implements ControlifyApi { private ToastUtils.ControlifyToast askSwitchToast = null; public void initializeControlify() { - LOGGER.info("Initializing Controlify..."); + Log.LOGGER.info("Initializing Controlify..."); config().load(); @@ -150,7 +155,7 @@ public class Controlify implements ControlifyApi { if (controllerOpt.isEmpty()) continue; var controller = controllerOpt.get(); - LOGGER.info("Controller found: " + controller.name()); + Log.LOGGER.info("Controller found: " + controller.name()); config().loadOrCreateControllerData(controller); @@ -165,7 +170,7 @@ public class Controlify implements ControlifyApi { } if (ControllerManager.getConnectedControllers().isEmpty()) { - LOGGER.info("No controllers found."); + Log.LOGGER.info("No controllers found."); } if (getCurrentController().isEmpty()) { @@ -181,7 +186,7 @@ public class Controlify implements ControlifyApi { try { entrypoint.onControllersDiscovered(this); } catch (Exception e) { - LOGGER.error("Failed to run `onControllersDiscovered` on Controlify entrypoint: " + entrypoint.getClass().getName(), e); + Log.LOGGER.error("Failed to run `onControllersDiscovered` on Controlify entrypoint: " + entrypoint.getClass().getName(), e); } }); } @@ -189,7 +194,7 @@ public class Controlify implements ControlifyApi { public void preInitialiseControlify() { DebugProperties.printProperties(); - LOGGER.info("Pre-initializing Controlify..."); + Log.LOGGER.info("Pre-initializing Controlify..."); ControlifySounds.init(); @@ -201,11 +206,38 @@ public class Controlify implements ControlifyApi { ControllerBindHandler.setup(); + ClientPlayNetworking.registerGlobalReceiver(VibrationPacket.TYPE, (packet, player, sender) -> { + if (config().globalSettings().allowServerRumble) { + getCurrentController().ifPresent(controller -> + controller.rumbleManager().play(packet.source(), packet.createEffect())); + } + }); + ClientPlayNetworking.registerGlobalReceiver(OriginVibrationPacket.TYPE, (packet, player, sender) -> { + if (config().globalSettings().allowServerRumble) { + getCurrentController().ifPresent(controller -> + controller.rumbleManager().play(packet.source(), packet.createEffect())); + } + }); + ClientPlayNetworking.registerGlobalReceiver(EntityVibrationPacket.TYPE, (packet, player, sender) -> { + if (config().globalSettings().allowServerRumble) { + getCurrentController().ifPresent(controller -> + controller.rumbleManager().play(packet.source(), packet.createEffect())); + } + }); + ClientPlayNetworking.registerGlobalReceiver(ReachAroundPolicyPacket.TYPE, (packet, player, sender) -> { + Log.LOGGER.info("Connected server specified reach around policy is {}.", packet.allowed() ? "ALLOWED" : "DISALLOWED"); + ReachAroundHandler.reachAroundPolicy = packet.allowed(); + }); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> { + DebugLog.log("Disconnected from server, resetting reach around policy"); + ReachAroundHandler.reachAroundPolicy = true; + }); + FabricLoader.getInstance().getEntrypoints("controlify", ControlifyEntrypoint.class).forEach(entrypoint -> { try { entrypoint.onControlifyPreInit(this); } catch (Exception e) { - LOGGER.error("Failed to run `onControlifyPreInit` on Controlify entrypoint: " + entrypoint.getClass().getName(), e); + Log.LOGGER.error("Failed to run `onControlifyPreInit` on Controlify entrypoint: " + entrypoint.getClass().getName(), e); } }); } @@ -268,7 +300,7 @@ public class Controlify implements ControlifyApi { } if (consecutiveInputSwitches > 100) { - LOGGER.warn("Controlify detected current controller to be constantly giving input and has been disabled."); + Log.LOGGER.warn("Controlify detected current controller to be constantly giving input and has been disabled."); ToastUtils.sendToast( Component.translatable("controlify.toast.faulty_input.title"), Component.translatable("controlify.toast.faulty_input.description"), @@ -312,7 +344,7 @@ public class Controlify implements ControlifyApi { if (controllerOpt.isEmpty()) return; var controller = controllerOpt.get(); - LOGGER.info("Controller connected: " + controller.name()); + Log.LOGGER.info("Controller connected: " + controller.name()); config().loadOrCreateControllerData(controller); @@ -348,7 +380,7 @@ public class Controlify implements ControlifyApi { controller.hidInfo().ifPresent(controllerHIDService::unconsumeController); setCurrentController(ControllerManager.getConnectedControllers().stream().findFirst().orElse(null)); - LOGGER.info("Controller disconnected: " + controller.name()); + Log.LOGGER.info("Controller disconnected: " + controller.name()); this.setInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER); ToastUtils.sendToast( diff --git a/src/main/java/dev/isxander/controlify/ControllerManager.java b/src/main/java/dev/isxander/controlify/ControllerManager.java index 210459a..cc3eaab 100644 --- a/src/main/java/dev/isxander/controlify/ControllerManager.java +++ b/src/main/java/dev/isxander/controlify/ControllerManager.java @@ -1,7 +1,6 @@ package dev.isxander.controlify; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.controller.hid.ControllerHIDService; @@ -9,6 +8,7 @@ import dev.isxander.controlify.controller.joystick.CompoundJoystickController; import dev.isxander.controlify.controller.joystick.SingleJoystickController; import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.utils.DebugLog; +import dev.isxander.controlify.utils.Log; import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.ReportedException; @@ -89,12 +89,12 @@ public final class ControllerManager { Controlify.instance().config().getCompoundJoysticks().values().forEach(info -> { try { if (info.isLoaded() && !info.canBeUsed()) { - Controlify.LOGGER.warn("Unloading compound joystick " + info.friendlyName() + " due to missing controllers."); + Log.LOGGER.warn("Unloading compound joystick " + info.friendlyName() + " due to missing controllers."); disconnect(info.type().mappingId()); } if (!info.isLoaded() && info.canBeUsed()) { - Controlify.LOGGER.info("Loading compound joystick " + info.type().mappingId() + "."); + Log.LOGGER.info("Loading compound joystick " + info.type().mappingId() + "."); CompoundJoystickController controller = info.attemptCreate().orElseThrow(); CONTROLLERS.put(info.type().mappingId(), controller); Controlify.instance().config().loadOrCreateControllerData(controller); diff --git a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java index 1dbeb56..b98bc08 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControllerBindings.java @@ -13,6 +13,7 @@ import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.mixins.compat.fapi.KeyBindingRegistryImplAccessor; import dev.isxander.controlify.mixins.feature.bind.KeyMappingAccessor; import dev.isxander.controlify.mixins.feature.bind.ToggleKeyMappingAccessor; +import dev.isxander.controlify.utils.Log; import net.minecraft.ChatFormatting; import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; @@ -459,14 +460,14 @@ public class ControllerBindings { boolean clean = true; for (var binding : registry().values()) { if (!json.has(binding.id().toString())) { - Controlify.LOGGER.warn("Missing binding: " + binding.id() + " in config file. Skipping!"); + Log.LOGGER.warn("Missing binding: " + binding.id() + " in config file. Skipping!"); clean = false; continue; } var bind = json.get(binding.id().toString()).getAsJsonObject(); if (bind == null) { - Controlify.LOGGER.warn("Unknown binding: " + binding.id() + " in config file. Skipping!"); + Log.LOGGER.warn("Unknown binding: " + binding.id() + " in config file. Skipping!"); clean = false; continue; } @@ -510,7 +511,7 @@ public class ControllerBindings { register(binding); } catch (Exception e) { - Controlify.LOGGER.error("Failed to automatically register modded keybind: " + keyMapping.getName(), e); + Log.LOGGER.error("Failed to automatically register modded keybind: " + keyMapping.getName(), e); } } } diff --git a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java index 6df05da..b4e1e3e 100644 --- a/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java +++ b/src/main/java/dev/isxander/controlify/config/ControlifyConfig.java @@ -6,6 +6,7 @@ import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.controller.Controller; 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 org.jetbrains.annotations.Nullable; @@ -13,7 +14,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.HashMap; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -43,7 +43,7 @@ public class ControlifyConfig { } public void save() { - Controlify.LOGGER.info("Saving Controlify config..."); + Log.LOGGER.info("Saving Controlify config..."); try { Files.deleteIfExists(CONFIG_PATH); @@ -55,7 +55,7 @@ public class ControlifyConfig { } public void load() { - Controlify.LOGGER.info("Loading Controlify config..."); + Log.LOGGER.info("Loading Controlify config..."); if (!Files.exists(CONFIG_PATH)) { firstLaunch = true; @@ -66,7 +66,7 @@ public class ControlifyConfig { try { applyConfig(GSON.fromJson(Files.readString(CONFIG_PATH), JsonObject.class)); } catch (Exception e) { - Controlify.LOGGER.error("Failed to load Controlify config!", e); + Log.LOGGER.error("Failed to load Controlify config!", e); } if (dirty) { @@ -156,7 +156,7 @@ public class ControlifyConfig { controller.setConfig(GSON, object.getAsJsonObject("config")); dirty |= !controller.bindings().fromJson(object.getAsJsonObject("bindings")); } catch (Exception e) { - Controlify.LOGGER.error("Failed to load controller data for " + controller.uid() + ". Resetting to default!", e); + Log.LOGGER.error("Failed to load controller data for " + controller.uid() + ". Resetting to default!", e); controller.resetConfig(); save(); } diff --git a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java index 0d4c288..1c5f43b 100644 --- a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java +++ b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java @@ -18,6 +18,7 @@ public class GlobalSettings { public boolean loadVibrationNatives = false; public boolean vibrationOnboarded = false; public ReachAroundMode reachAround = ReachAroundMode.OFF; + public boolean allowServerRumble = true; public boolean uiSounds = false; public boolean notifyLowBattery = true; public boolean delegateSetup = false; diff --git a/src/main/java/dev/isxander/controlify/controller/AbstractController.java b/src/main/java/dev/isxander/controlify/controller/AbstractController.java index 5b47487..5be1e23 100644 --- a/src/main/java/dev/isxander/controlify/controller/AbstractController.java +++ b/src/main/java/dev/isxander/controlify/controller/AbstractController.java @@ -8,6 +8,7 @@ import dev.isxander.controlify.ControllerManager; import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.controller.hid.ControllerHIDService; import dev.isxander.controlify.rumble.RumbleCapable; +import dev.isxander.controlify.utils.Log; import org.lwjgl.glfw.GLFW; import java.util.Objects; @@ -106,7 +107,7 @@ public abstract class AbstractController(getClass()){}.getType()); } catch (Exception e) { - Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead. Printing json: " + json.toString(), e); + Log.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead. Printing json: " + json.toString(), e); Controlify.instance().config().setDirty(); return; } @@ -114,7 +115,7 @@ public abstract class AbstractController forceJoystick = reader.nextBoolean(); case "dont_load" -> dontLoad = reader.nextBoolean(); default -> { - Controlify.LOGGER.warn("Unknown key in HID DB: " + name + ". Skipping..."); + Log.LOGGER.warn("Unknown key in HID DB: " + name + ". Skipping..."); reader.skipValue(); } } @@ -98,13 +98,13 @@ public record ControllerType(String friendlyName, String mappingId, String theme reader.endObject(); if (legacyIdentifier != null) { - Controlify.LOGGER.warn("Legacy identifier found in HID DB. Please replace with `theme` and `mapping` (if needed)."); + Log.LOGGER.warn("Legacy identifier found in HID DB. Please replace with `theme` and `mapping` (if needed)."); themeId = legacyIdentifier; mappingId = legacyIdentifier; } if (friendlyName == null || themeId == null || hids.isEmpty()) { - Controlify.LOGGER.warn("Invalid entry in HID DB. Skipping..."); + Log.LOGGER.warn("Invalid entry in HID DB. Skipping..."); continue; } diff --git a/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java b/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java index abaeaaa..582a403 100644 --- a/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java +++ b/src/main/java/dev/isxander/controlify/controller/hid/ControllerHIDService.java @@ -4,6 +4,7 @@ import com.mojang.datafixers.util.Pair; import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.ControllerType; import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; +import dev.isxander.controlify.utils.Log; import dev.isxander.controlify.utils.ToastUtils; import net.minecraft.network.chat.Component; import org.hid4java.*; @@ -38,7 +39,7 @@ public class ControllerHIDService { services = HidManager.getHidServices(specification); services.start(); } catch (HidException e) { - Controlify.LOGGER.error("Failed to start controller HID service! If you are on Linux using flatpak or snap, this is likely because your launcher has not added libusb to their package.", e); + Log.LOGGER.error("Failed to start controller HID service! If you are on Linux using flatpak or snap, this is likely because your launcher has not added libusb to their package.", e); disabled = true; } } @@ -74,13 +75,13 @@ public class ControllerHIDService { Pair hid = unconsumedControllerHIDs.poll(); if (hid == null) { - Controlify.LOGGER.warn("No controller found via USB hardware scan! This prevents identifying controller type."); + Log.LOGGER.warn("No controller found via USB hardware scan! This prevents identifying controller type."); return new ControllerHIDInfo(ControllerType.UNKNOWN, Optional.empty()); } ControllerType type = ControllerType.getTypeForHID(hid.getSecond()); if (type == ControllerType.UNKNOWN) - Controlify.LOGGER.warn("Controller found via USB hardware scan, but it was not found in the controller identification database! (HID: {})", hid.getSecond()); + Log.LOGGER.warn("Controller found via USB hardware scan, but it was not found in the controller identification database! (HID: {})", hid.getSecond()); unconsumedControllerHIDs.removeIf(h -> hid.getFirst().getPath().equals(h.getFirst().getPath())); diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickController.java b/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickController.java index 8f22462..cf6173b 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickController.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/CompoundJoystickController.java @@ -3,7 +3,6 @@ package dev.isxander.controlify.controller.joystick; import com.google.common.collect.ImmutableList; import com.google.gson.Gson; import com.google.gson.JsonElement; -import dev.isxander.controlify.Controlify; import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.controller.ControllerType; import dev.isxander.controlify.controller.hid.ControllerHIDService; @@ -12,6 +11,7 @@ import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping; import dev.isxander.controlify.rumble.RumbleCapable; import dev.isxander.controlify.rumble.RumbleManager; import dev.isxander.controlify.rumble.RumbleSource; +import dev.isxander.controlify.utils.Log; import org.lwjgl.glfw.GLFW; import java.util.List; @@ -106,7 +106,7 @@ public class CompoundJoystickController implements JoystickController hats = Arrays.stream(mapping.hats()).map(hat -> hat.getHatState(data)).toList(); if (DebugProperties.PRINT_JOY_STATE) { - Controlify.LOGGER.info("Printing joystick state for controller {}", joystick); - Controlify.LOGGER.info(Arrays.stream(axes).map(axis -> axis.name().getString() + ": " + axis.getAxis(data)).toList().toString()); - Controlify.LOGGER.info(Arrays.stream(mapping.buttons()).map(button -> button.name().getString() + ": " + button.isPressed(data)).toList().toString()); - Controlify.LOGGER.info(Arrays.stream(mapping.hats()).map(hat -> hat.name().getString() + ": " + hat.getHatState(data)).toList().toString()); + Log.LOGGER.info("Printing joystick state for controller {}", joystick); + Log.LOGGER.info(Arrays.stream(axes).map(axis -> axis.name().getString() + ": " + axis.getAxis(data)).toList().toString()); + Log.LOGGER.info(Arrays.stream(mapping.buttons()).map(button -> button.name().getString() + ": " + button.isPressed(data)).toList().toString()); + Log.LOGGER.info(Arrays.stream(mapping.hats()).map(hat -> hat.name().getString() + ": " + hat.getHatState(data)).toList().toString()); } return new JoystickState(joystick.mapping(), deadzoneAxes, rawAxes, buttons, hats); diff --git a/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java b/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java index ae89820..679555e 100644 --- a/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java +++ b/src/main/java/dev/isxander/controlify/controller/joystick/SingleJoystickController.java @@ -2,9 +2,6 @@ package dev.isxander.controlify.controller.joystick; import com.google.gson.Gson; import com.google.gson.JsonElement; -import dev.isxander.controlify.Controlify; -import dev.isxander.controlify.InputMode; -import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.controller.AbstractController; import dev.isxander.controlify.controller.hid.ControllerHIDService; @@ -13,6 +10,7 @@ import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping; import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; import dev.isxander.controlify.rumble.RumbleManager; import dev.isxander.controlify.rumble.RumbleSource; +import dev.isxander.controlify.utils.Log; import org.libsdl.SDL; import java.util.Objects; @@ -106,7 +104,7 @@ public class SingleJoystickController extends AbstractController { - Controlify.LOGGER.warn("Unknown field in joystick mapping: " + name + ". Expected values: ['axes', 'buttons', 'hats']"); + Log.LOGGER.warn("Unknown field in joystick mapping: " + name + ". Expected values: ['axes', 'buttons', 'hats']"); reader.skipValue(); } } @@ -111,7 +111,7 @@ public class RPJoystickMapping implements JoystickMapping { } default -> { reader.skipValue(); - Controlify.LOGGER.info("Unknown axis range property: " + rangeName + ". Expected are ['in', 'out']"); + Log.LOGGER.info("Unknown axis range property: " + rangeName + ". Expected are ['in', 'out']"); } } } @@ -135,7 +135,7 @@ public class RPJoystickMapping implements JoystickMapping { } default -> { reader.skipValue(); - Controlify.LOGGER.info("Unknown axis property: " + name + ". Expected are ['identifier', 'axis_names', 'ids', 'range', 'rest', 'deadzone']"); + Log.LOGGER.info("Unknown axis property: " + name + ". Expected are ['identifier', 'axis_names', 'ids', 'range', 'rest', 'deadzone']"); } } } @@ -166,7 +166,7 @@ public class RPJoystickMapping implements JoystickMapping { case "name" -> btnName = reader.nextString(); default -> { reader.skipValue(); - Controlify.LOGGER.info("Unknown button property: " + name + ". Expected are ['button', 'name']"); + Log.LOGGER.info("Unknown button property: " + name + ". Expected are ['button', 'name']"); } } } @@ -214,11 +214,11 @@ public class RPJoystickMapping implements JoystickMapping { reader.endObject(); if (axisId == -1) { - Controlify.LOGGER.error("No axis id defined for emulated hat " + hatName + "! Skipping."); + Log.LOGGER.error("No axis id defined for emulated hat " + hatName + "! Skipping."); continue; } if (states.size() != JoystickState.HatState.values().length) { - Controlify.LOGGER.error("Not all hat states are defined for emulated hat " + hatName + "! Skipping."); + Log.LOGGER.error("Not all hat states are defined for emulated hat " + hatName + "! Skipping."); continue; } @@ -226,7 +226,7 @@ public class RPJoystickMapping implements JoystickMapping { } default -> { reader.skipValue(); - Controlify.LOGGER.info("Unknown hat property: " + name + ". Expected are ['hat', 'name']"); + Log.LOGGER.info("Unknown hat property: " + name + ". Expected are ['hat', 'name']"); } } } @@ -257,14 +257,14 @@ public class RPJoystickMapping implements JoystickMapping { public static JoystickMapping fromType(JoystickController joystick) { var resource = Minecraft.getInstance().getResourceManager().getResource(new ResourceLocation("controlify", "mappings/" + joystick.type().mappingId() + ".json")); if (resource.isEmpty()) { - Controlify.LOGGER.warn("No joystick mapping found for controller: '" + joystick.type().mappingId() + "'"); + Log.LOGGER.warn("No joystick mapping found for controller: '" + joystick.type().mappingId() + "'"); return new UnmappedJoystickMapping(joystick.joystickId()); } try (var reader = JsonReader.json5(resource.get().openAsReader())) { return new RPJoystickMapping(reader, joystick.type()); } catch (Exception e) { - Controlify.LOGGER.error("Failed to load joystick mapping for controller: '" + joystick.type().mappingId() + "'", e); + Log.LOGGER.error("Failed to load joystick mapping for controller: '" + joystick.type().mappingId() + "'", e); return new UnmappedJoystickMapping(joystick.joystickId()); } } diff --git a/src/main/java/dev/isxander/controlify/controller/sdl2/SDL2NativesManager.java b/src/main/java/dev/isxander/controlify/controller/sdl2/SDL2NativesManager.java index d6a0756..5b69a1d 100644 --- a/src/main/java/dev/isxander/controlify/controller/sdl2/SDL2NativesManager.java +++ b/src/main/java/dev/isxander/controlify/controller/sdl2/SDL2NativesManager.java @@ -1,7 +1,7 @@ package dev.isxander.controlify.controller.sdl2; -import dev.isxander.controlify.Controlify; import dev.isxander.controlify.utils.DebugLog; +import dev.isxander.controlify.utils.Log; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.Util; import org.libsdl.SDL; @@ -38,7 +38,7 @@ public class SDL2NativesManager { DebugLog.log("Initialising SDL2 native library"); if (!Target.CURRENT.hasNativeLibrary()) { - Controlify.LOGGER.warn("SDL2 native library not available for OS: " + Target.CURRENT); + Log.LOGGER.warn("SDL2 native library not available for OS: " + Target.CURRENT); return; } @@ -50,11 +50,11 @@ public class SDL2NativesManager { .map(Path::toFile) .forEachOrdered(File::delete); } catch (Exception e) { - Controlify.LOGGER.error("Failed to delete old SDL2 native library", e); + Log.LOGGER.error("Failed to delete old SDL2 native library", e); } } - Controlify.LOGGER.info("Downloading SDL2 native library: " + Target.CURRENT.getArtifactName()); + Log.LOGGER.info("Downloading SDL2 native library: " + Target.CURRENT.getArtifactName()); downloadLibrary(localLibraryPath); } @@ -65,7 +65,7 @@ public class SDL2NativesManager { loaded = true; } catch (Exception e) { - Controlify.LOGGER.error("Failed to load SDL2 native library", e); + Log.LOGGER.error("Failed to load SDL2 native library", e); } } @@ -88,7 +88,7 @@ public class SDL2NativesManager { int joystickSubsystem = 0x00000200; // implies event subsystem int gameControllerSubsystem = 0x00002000; // implies event subsystem if (SDL.SDL_Init(joystickSubsystem | gameControllerSubsystem) != 0) { - Controlify.LOGGER.error("Failed to initialise SDL2: " + SDL.SDL_GetError()); + Log.LOGGER.error("Failed to initialise SDL2: " + SDL.SDL_GetError()); throw new RuntimeException("Failed to initialise SDL2: " + SDL.SDL_GetError()); } @@ -111,7 +111,7 @@ public class SDL2NativesManager { ReadableByteChannel readableByteChannel = Channels.newChannel(downloadUrl.openStream()); FileChannel fileChannel = fileOutputStream.getChannel(); fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE); - Controlify.LOGGER.info("Downloaded SDL2 native library from " + downloadUrl); + Log.LOGGER.info("Downloaded SDL2 native library from " + downloadUrl); } catch (Exception e) { e.printStackTrace(); return false; diff --git a/src/main/java/dev/isxander/controlify/debug/DebugProperties.java b/src/main/java/dev/isxander/controlify/debug/DebugProperties.java index 065c1e8..ae9e249 100644 --- a/src/main/java/dev/isxander/controlify/debug/DebugProperties.java +++ b/src/main/java/dev/isxander/controlify/debug/DebugProperties.java @@ -1,6 +1,6 @@ package dev.isxander.controlify.debug; -import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.utils.Log; import net.fabricmc.loader.api.FabricLoader; import java.util.ArrayList; @@ -28,17 +28,17 @@ public class DebugProperties { return; String header = "*----------------- Controlify Debug Properties -----------------*"; - Controlify.LOGGER.error(header); + Log.LOGGER.error(header); int maxWidth = properties.stream().mapToInt(prop -> prop.name().length()).max().orElse(0); for (var prop : properties) { String line = "| %s%s = %s".formatted(prop.name(), " ".repeat(maxWidth - prop.name().length()), prop.enabled()); line += " ".repeat(header.length() - line.length() - 1) + "|"; - Controlify.LOGGER.error(line); + Log.LOGGER.error(line); } - Controlify.LOGGER.error("*---------------------------------------------------------------*"); + Log.LOGGER.error("*---------------------------------------------------------------*"); } private static boolean boolProp(String name, boolean defProd, boolean defDev) { diff --git a/src/main/java/dev/isxander/controlify/driver/GamepadDrivers.java b/src/main/java/dev/isxander/controlify/driver/GamepadDrivers.java index 8b7c910..4d608c8 100644 --- a/src/main/java/dev/isxander/controlify/driver/GamepadDrivers.java +++ b/src/main/java/dev/isxander/controlify/driver/GamepadDrivers.java @@ -1,9 +1,9 @@ package dev.isxander.controlify.driver; import com.google.common.collect.Sets; -import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; import dev.isxander.controlify.debug.DebugProperties; +import dev.isxander.controlify.utils.Log; import org.hid4java.HidDevice; import java.util.*; @@ -17,7 +17,7 @@ public record GamepadDrivers(BasicGamepadInputDriver basicGamepadInputDriver, Gy public void printDrivers() { if (DebugProperties.PRINT_DRIVER) { - Controlify.LOGGER.info("Drivers in use: Basic Input = '{}', Gyro = '{}', Rumble = '{}', Battery = '{}', Name = '{}', GUID = '{}'", + Log.LOGGER.info("Drivers in use: Basic Input = '{}', Gyro = '{}', Rumble = '{}', Battery = '{}', Name = '{}', GUID = '{}'", basicGamepadInputDriver.getBasicGamepadDetails(), gyroDriver.getGyroDetails(), rumbleDriver.getRumbleDetails(), diff --git a/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java b/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java index f22e3f7..3e99a2f 100644 --- a/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java @@ -1,9 +1,9 @@ package dev.isxander.controlify.driver; -import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.BatteryLevel; import dev.isxander.controlify.controller.gamepad.GamepadState; import dev.isxander.controlify.debug.DebugProperties; +import dev.isxander.controlify.utils.Log; import org.libsdl.SDL; public class SDL2GamepadDriver implements GyroDriver, RumbleDriver, BatteryDriver, GUIDProvider { @@ -29,9 +29,9 @@ public class SDL2GamepadDriver implements GyroDriver, RumbleDriver, BatteryDrive float[] gyro = new float[3]; if (SDL.SDL_GameControllerGetSensorData(ptrGamepad, SDL.SDL_SENSOR_GYRO, gyro, 3) == 0) { gyroDelta = new GamepadState.GyroState(gyro[0], gyro[1], gyro[2]); - if (DebugProperties.PRINT_GYRO) Controlify.LOGGER.info("Gyro delta: " + gyroDelta); + if (DebugProperties.PRINT_GYRO) Log.LOGGER.info("Gyro delta: " + gyroDelta); } else { - Controlify.LOGGER.error("Could not get gyro data: " + SDL.SDL_GetError()); + Log.LOGGER.error("Could not get gyro data: " + SDL.SDL_GetError()); } } @@ -42,7 +42,7 @@ public class SDL2GamepadDriver implements GyroDriver, RumbleDriver, BatteryDrive public boolean rumble(float strongMagnitude, float weakMagnitude) { // duration of 0 is infinite if (!SDL.SDL_GameControllerRumble(ptrGamepad, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 0)) { - Controlify.LOGGER.error("Could not rumble controller: " + SDL.SDL_GetError()); + Log.LOGGER.error("Could not rumble controller: " + SDL.SDL_GetError()); return false; } return true; diff --git a/src/main/java/dev/isxander/controlify/driver/SDL2JoystickDriver.java b/src/main/java/dev/isxander/controlify/driver/SDL2JoystickDriver.java index 56b9ce2..97a5571 100644 --- a/src/main/java/dev/isxander/controlify/driver/SDL2JoystickDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/SDL2JoystickDriver.java @@ -1,6 +1,6 @@ package dev.isxander.controlify.driver; -import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.utils.Log; import org.libsdl.SDL; public class SDL2JoystickDriver implements RumbleDriver { @@ -21,7 +21,7 @@ public class SDL2JoystickDriver implements RumbleDriver { public boolean rumble(float strongMagnitude, float weakMagnitude) { // duration of 0 is infinite if (!SDL.SDL_JoystickRumble(ptrJoystick, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 0)) { - Controlify.LOGGER.error("Could not rumble controller: " + SDL.SDL_GetError()); + Log.LOGGER.error("Could not rumble controller: " + SDL.SDL_GetError()); return false; } return true; diff --git a/src/main/java/dev/isxander/controlify/driver/SteamDeckDriver.java b/src/main/java/dev/isxander/controlify/driver/SteamDeckDriver.java index e7b7022..29a8c4c 100644 --- a/src/main/java/dev/isxander/controlify/driver/SteamDeckDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/SteamDeckDriver.java @@ -1,7 +1,7 @@ package dev.isxander.controlify.driver; -import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.gamepad.GamepadState; +import dev.isxander.controlify.utils.Log; import org.hid4java.HidDevice; import java.util.Arrays; @@ -33,10 +33,10 @@ public class SteamDeckDriver implements GyroDriver, BasicGamepadInputDriver { int readCnt = hidDevice.read(data); if (readCnt == 0) { - Controlify.LOGGER.warn("No data available."); + Log.LOGGER.warn("No data available."); } if (readCnt == -1) { - Controlify.LOGGER.warn("Error reading data."); + Log.LOGGER.warn("Error reading data."); } System.out.println(Arrays.toString(data)); diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerCarouselScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerCarouselScreen.java index b7d5b5d..28bd5df 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerCarouselScreen.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerCarouselScreen.java @@ -96,7 +96,7 @@ public class ControllerCarouselScreen extends Screen implements ScreenController public void refreshControllers() { Controller prevSelectedController; - if (carouselEntries != null) { + if (carouselEntries != null && !carouselEntries.isEmpty()) { carouselEntries.forEach(this::removeWidget); prevSelectedController = carouselEntries.get(carouselIndex).controller; } else { diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerDeadzoneCalibrationScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerDeadzoneCalibrationScreen.java index 272d2ee..22c5654 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerDeadzoneCalibrationScreen.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerDeadzoneCalibrationScreen.java @@ -2,6 +2,7 @@ package dev.isxander.controlify.gui.screen; import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.utils.Log; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -124,7 +125,7 @@ public class ControllerDeadzoneCalibrationScreen extends Screen { var deadzone = (float)Mth.clamp(0.05 * Math.ceil(minDeadzone / 0.05), 0, 0.95); if (Float.isNaN(deadzone)) { - Controlify.LOGGER.warn("Deadzone for axis {} is NaN, using default deadzone.", i); + Log.LOGGER.warn("Deadzone for axis {} is NaN, using default deadzone.", i); deadzone = controller.defaultConfig().getDeadzone(i); } diff --git a/src/main/java/dev/isxander/controlify/gui/screen/GlobalSettingsScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/GlobalSettingsScreenFactory.java index 200ec0d..b8c1bd3 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/GlobalSettingsScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/GlobalSettingsScreenFactory.java @@ -1,7 +1,9 @@ package dev.isxander.controlify.gui.screen; import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.config.GlobalSettings; +import dev.isxander.controlify.reacharound.ReachAroundHandler; import dev.isxander.controlify.reacharound.ReachAroundMode; import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.api.controller.BooleanControllerBuilder; @@ -37,9 +39,11 @@ public class GlobalSettingsScreenFactory { .text(Component.translatable("controlify.gui.reach_around.tooltip")) .text(Component.translatable("controlify.gui.reach_around.tooltip.parity").withStyle(ChatFormatting.GRAY)) .text(state == ReachAroundMode.EVERYWHERE ? Component.translatable("controlify.gui.reach_around.tooltip.warning").withStyle(ChatFormatting.RED) : Component.empty()) + .text(!ReachAroundHandler.reachAroundPolicy ? Component.translatable("controlify.gui.reach_around.tooltip.server_disabled").withStyle(ChatFormatting.GOLD) : Component.empty()) .build()) - .binding(GlobalSettings.DEFAULT.reachAround, () -> globalSettings.reachAround, v -> globalSettings.reachAround = v) + .binding(GlobalSettings.DEFAULT.reachAround, () -> ReachAroundHandler.reachAroundPolicy ? globalSettings.reachAround : ReachAroundMode.OFF, v -> globalSettings.reachAround = v) .controller(opt -> EnumControllerBuilder.create(opt).enumClass(ReachAroundMode.class)) + .available(ReachAroundHandler.reachAroundPolicy) .build()) .option(Option.createBuilder() .name(Component.translatable("controlify.gui.ui_sounds")) @@ -49,6 +53,17 @@ public class GlobalSettingsScreenFactory { .binding(GlobalSettings.DEFAULT.uiSounds, () -> globalSettings.uiSounds, v -> globalSettings.uiSounds = v) .controller(TickBoxControllerBuilder::create) .build()) + .option(Option.createBuilder() + .name(Component.translatable("controlify.gui.allow_server_rumble")) + .description(OptionDescription.createBuilder() + .text(Component.translatable("controlify.gui.allow_server_rumble.tooltip")) + .build()) + .binding(GlobalSettings.DEFAULT.allowServerRumble, () -> globalSettings.allowServerRumble, v -> globalSettings.allowServerRumble = v) + .controller(TickBoxControllerBuilder::create) + .listener((opt, val) -> { + if (!val) ControlifyApi.get().getCurrentController().ifPresent(c -> c.rumbleManager().clearEffects()); + }) + .build()) .option(Option.createBuilder() .name(Component.translatable("controlify.gui.notify_low_battery")) .description(OptionDescription.createBuilder() diff --git a/src/main/java/dev/isxander/controlify/reacharound/ReachAroundHandler.java b/src/main/java/dev/isxander/controlify/reacharound/ReachAroundHandler.java index 96ee9ee..9dd1228 100644 --- a/src/main/java/dev/isxander/controlify/reacharound/ReachAroundHandler.java +++ b/src/main/java/dev/isxander/controlify/reacharound/ReachAroundHandler.java @@ -8,8 +8,10 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; public class ReachAroundHandler { + public static boolean reachAroundPolicy = true; + public static HitResult getReachAroundHitResult(Entity entity, HitResult hitResult) { - // if there is already a valid hit, we don't want to override it + // if there is already a valid hit, we don't want to override it if (hitResult.getType() != HitResult.Type.MISS) return hitResult; @@ -29,8 +31,9 @@ public class ReachAroundHandler { } private static boolean canReachAround(Entity cameraEntity) { - return // don't want to place blocks while riding an entity - cameraEntity.getVehicle() == null + return reachAroundPolicy + // don't want to place blocks while riding an entity + && cameraEntity.getVehicle() == null // straight ahead = 0deg, up = -90deg, down = 90deg // 45deg and above is half between straight ahead and down, must be lower or equal to this threshold && cameraEntity.getXRot() >= 45 diff --git a/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java b/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java index 116218f..d0b04f4 100644 --- a/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java +++ b/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java @@ -5,7 +5,9 @@ import net.minecraft.util.Mth; import net.minecraft.world.phys.Vec3; import org.apache.commons.lang3.Validate; +import java.util.function.BooleanSupplier; import java.util.function.Function; +import java.util.function.Supplier; public class ContinuousRumbleEffect implements RumbleEffect { private final Function stateFunction; @@ -14,17 +16,21 @@ public class ContinuousRumbleEffect implements RumbleEffect { private final int minTime; private int tick; private boolean stopped; + private BooleanSupplier stopCondition; - public ContinuousRumbleEffect(Function stateFunction, int priority, int timeout, int minTime) { + public ContinuousRumbleEffect(Function stateFunction, int priority, int timeout, int minTime, BooleanSupplier stopCondition) { this.stateFunction = stateFunction; this.priority = priority; this.timeout = timeout; this.minTime = minTime; + this.stopCondition = stopCondition; } @Override public void tick() { tick++; + if (stopCondition.getAsBoolean()) + stop(); } @Override @@ -64,6 +70,7 @@ public class ContinuousRumbleEffect implements RumbleEffect { private int timeout = -1; private int minTime; private InWorldProperties inWorldProperties; + private BooleanSupplier stopCondition = () -> false; private Builder() { } @@ -101,8 +108,15 @@ public class ContinuousRumbleEffect implements RumbleEffect { return this; } - public Builder inWorld(Vec3 sourceLocation, float minMagnitude, float maxMagnitude, float minDistance, float maxDistance, Function fallofFunction) { - this.inWorldProperties = new InWorldProperties(sourceLocation, minMagnitude, maxMagnitude, minDistance, maxDistance, fallofFunction); + public Builder inWorld(Supplier sourceLocation, float min, float max, float effectRange, Function fallofFunction) { + this.inWorldProperties = new InWorldProperties(sourceLocation, min, max, effectRange, fallofFunction); + stopCondition(() -> Minecraft.getInstance().cameraEntity == null); + return this; + } + + public Builder stopCondition(BooleanSupplier stopCondition) { + BooleanSupplier oldStopCondition = this.stopCondition; + this.stopCondition = () -> stopCondition.getAsBoolean() || oldStopCondition.getAsBoolean(); return this; } @@ -114,15 +128,20 @@ public class ContinuousRumbleEffect implements RumbleEffect { if (inWorldProperties != null) stateFunction = inWorldProperties.modify(stateFunction); - return new ContinuousRumbleEffect(stateFunction, priority, timeout, minTime); + return new ContinuousRumbleEffect(stateFunction, priority, timeout, minTime, stopCondition); } - private record InWorldProperties(Vec3 sourceLocation, float minMagnitude, float maxMagnitude, float minDistance, float maxDistance, Function fallofFunction) { + private record InWorldProperties(Supplier sourceLocation, float minMagnitude, float maxMagnitude, float effectRange, Function fallofFunction) { private Function modify(Function stateFunction) { return tick -> { - float distanceSqr = (float) Mth.clamp(Minecraft.getInstance().player.distanceToSqr(sourceLocation), minDistance, maxDistance); - float magnitude = Mth.lerp(1f - fallofFunction.apply(distanceSqr / (maxDistance * maxDistance)), minMagnitude, maxMagnitude); - return stateFunction.apply(tick).mul(magnitude); + if (Minecraft.getInstance().cameraEntity == null) + return RumbleState.NONE; + + float distanceSqr = (float) Minecraft.getInstance().cameraEntity.distanceToSqr(sourceLocation.get()); + float normalizedDistance = Mth.clamp(distanceSqr / (effectRange * effectRange), 0, 1); + float multiplier = Mth.lerp(fallofFunction.apply(1f - normalizedDistance), minMagnitude, maxMagnitude); + + return stateFunction.apply(tick).mul(multiplier); }; } } diff --git a/src/main/java/dev/isxander/controlify/rumble/RumbleSource.java b/src/main/java/dev/isxander/controlify/rumble/RumbleSource.java index 318ba1e..f6ff8df 100644 --- a/src/main/java/dev/isxander/controlify/rumble/RumbleSource.java +++ b/src/main/java/dev/isxander/controlify/rumble/RumbleSource.java @@ -1,11 +1,12 @@ package dev.isxander.controlify.rumble; import com.google.gson.JsonObject; +import dev.isxander.controlify.utils.Log; import net.minecraft.resources.ResourceLocation; import java.util.*; -public class RumbleSource { +public record RumbleSource(ResourceLocation id) { private static final Map SOURCES = new LinkedHashMap<>(); public static final RumbleSource @@ -19,14 +20,13 @@ public class RumbleSource { MISC = register("misc"), GLOBAL_EVENT = register("global_event"); - private final ResourceLocation id; - - private RumbleSource(ResourceLocation id) { - this.id = id; - } - - public ResourceLocation id() { - return id; + public static RumbleSource get(ResourceLocation id) { + RumbleSource source = SOURCES.get(id); + if (source == null) { + Log.LOGGER.warn("Unknown rumble source: {}. Using master.", id); + return MASTER; + } + return source; } public static Collection values() { diff --git a/src/main/java/dev/isxander/controlify/rumble/RumbleState.java b/src/main/java/dev/isxander/controlify/rumble/RumbleState.java index c7b6329..924ebd4 100644 --- a/src/main/java/dev/isxander/controlify/rumble/RumbleState.java +++ b/src/main/java/dev/isxander/controlify/rumble/RumbleState.java @@ -1,6 +1,8 @@ package dev.isxander.controlify.rumble; public record RumbleState(float strong, float weak) { + public static final RumbleState NONE = new RumbleState(0.0F, 0.0F); + public RumbleState mul(float multiplier) { return new RumbleState(strong * multiplier, weak * multiplier); } diff --git a/src/main/java/dev/isxander/controlify/server/ControlifyServer.java b/src/main/java/dev/isxander/controlify/server/ControlifyServer.java new file mode 100644 index 0000000..e28bd21 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/server/ControlifyServer.java @@ -0,0 +1,27 @@ +package dev.isxander.controlify.server; + +import dev.isxander.controlify.utils.Log; +import net.fabricmc.api.DedicatedServerModInitializer; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; + +public class ControlifyServer implements ModInitializer, DedicatedServerModInitializer { + @Override + public void onInitialize() { + CommandRegistrationCallback.EVENT.register((dispatcher, registry, env) -> { + VibrateCommand.register(dispatcher); + }); + } + + @Override + public void onInitializeServer() { + ControlifyServerConfig.INSTANCE.load(); + Log.LOGGER.info("Reach-around policy: " + ControlifyServerConfig.INSTANCE.getConfig().reachAroundPolicy); + + ServerPlayConnectionEvents.INIT.register((handler, server) -> { + ServerPlayNetworking.send(handler.getPlayer(), new ReachAroundPolicyPacket(ControlifyServerConfig.INSTANCE.getConfig().reachAroundPolicy)); + }); + } +} diff --git a/src/main/java/dev/isxander/controlify/server/ControlifyServerConfig.java b/src/main/java/dev/isxander/controlify/server/ControlifyServerConfig.java new file mode 100644 index 0000000..71fbecc --- /dev/null +++ b/src/main/java/dev/isxander/controlify/server/ControlifyServerConfig.java @@ -0,0 +1,13 @@ +package dev.isxander.controlify.server; + +import dev.isxander.yacl3.config.ConfigInstance; +import dev.isxander.yacl3.config.GsonConfigInstance; +import net.fabricmc.loader.api.FabricLoader; + +public class ControlifyServerConfig { + public static final ConfigInstance INSTANCE = GsonConfigInstance.createBuilder(ControlifyServerConfig.class) + .setPath(FabricLoader.getInstance().getConfigDir().resolve("controlify.json")) + .build(); + + public boolean reachAroundPolicy = false; +} diff --git a/src/main/java/dev/isxander/controlify/server/EntityVibrationPacket.java b/src/main/java/dev/isxander/controlify/server/EntityVibrationPacket.java new file mode 100644 index 0000000..e593536 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/server/EntityVibrationPacket.java @@ -0,0 +1,48 @@ +package dev.isxander.controlify.server; + +import dev.isxander.controlify.rumble.ContinuousRumbleEffect; +import dev.isxander.controlify.rumble.RumbleEffect; +import dev.isxander.controlify.rumble.RumbleSource; +import dev.isxander.controlify.rumble.RumbleState; +import dev.isxander.controlify.utils.Easings; +import net.fabricmc.fabric.api.networking.v1.FabricPacket; +import net.fabricmc.fabric.api.networking.v1.PacketType; +import net.minecraft.client.Minecraft; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; + +public record EntityVibrationPacket(int entityId, float range, int duration, RumbleState state, RumbleSource source) implements FabricPacket { + public static final PacketType TYPE = PacketType.create(new ResourceLocation("controlify", "vibrate_from_entity"), EntityVibrationPacket::new); + + public EntityVibrationPacket(FriendlyByteBuf buf) { + this(buf.readInt(), buf.readFloat(), buf.readInt(), OriginVibrationPacket.readState(buf), RumbleSource.get(buf.readResourceLocation())); + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeInt(entityId); + buf.writeFloat(range); + buf.writeInt(duration); + + int high = (int)(state.strong() * 32767.0F); + int low = (int)(state.weak() * 32767.0F); + buf.writeInt((high << 16) | (low & 0xFFFF)); + + buf.writeResourceLocation(source.id()); + } + + public RumbleEffect createEffect() { + Entity entity = Minecraft.getInstance().level.getEntity(entityId); + return ContinuousRumbleEffect.builder() + .constant(state) + .inWorld(() -> entity.position(), 0, 1, range, Easings::easeInSine) + .timeout(duration) + .build(); + } + + @Override + public PacketType getType() { + return TYPE; + } +} diff --git a/src/main/java/dev/isxander/controlify/server/OriginVibrationPacket.java b/src/main/java/dev/isxander/controlify/server/OriginVibrationPacket.java new file mode 100644 index 0000000..d4f74ab --- /dev/null +++ b/src/main/java/dev/isxander/controlify/server/OriginVibrationPacket.java @@ -0,0 +1,53 @@ +package dev.isxander.controlify.server; + +import dev.isxander.controlify.rumble.*; +import dev.isxander.controlify.utils.Easings; +import dev.isxander.controlify.utils.Log; +import net.fabricmc.fabric.api.networking.v1.FabricPacket; +import net.fabricmc.fabric.api.networking.v1.PacketType; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; + +public record OriginVibrationPacket(Vector3f origin, float effectRange, int duration, RumbleState state, RumbleSource source) implements FabricPacket { + public static final PacketType TYPE = PacketType.create(new ResourceLocation("controlify", "vibrate_from_origin"), OriginVibrationPacket::new); + + public OriginVibrationPacket(FriendlyByteBuf buf) { + this(buf.readVector3f(), buf.readFloat(), buf.readVarInt(), readState(buf), RumbleSource.get(buf.readResourceLocation())); + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeVector3f(origin); + buf.writeFloat(effectRange); + buf.writeVarInt(duration); + + int high = (int)(state.strong() * 32767.0F); + int low = (int)(state.weak() * 32767.0F); + buf.writeInt((high << 16) | (low & 0xFFFF)); + + buf.writeResourceLocation(source.id()); + } + + @Override + public PacketType getType() { + return TYPE; + } + + public RumbleEffect createEffect() { + var originVec3 = new Vec3(origin); + return ContinuousRumbleEffect.builder() + .constant(state) + .inWorld(() -> originVec3, 0, 1, effectRange, Easings::easeInSine) + .timeout(duration) + .build(); + } + + public static RumbleState readState(FriendlyByteBuf buf) { + int packed = buf.readInt(); + float strong = (short)(packed >> 16) / 32767.0F; + float weak = (short)packed / 32767.0F; + return new RumbleState(strong, weak); + } +} diff --git a/src/main/java/dev/isxander/controlify/server/ReachAroundPolicyPacket.java b/src/main/java/dev/isxander/controlify/server/ReachAroundPolicyPacket.java new file mode 100644 index 0000000..4b0948d --- /dev/null +++ b/src/main/java/dev/isxander/controlify/server/ReachAroundPolicyPacket.java @@ -0,0 +1,24 @@ +package dev.isxander.controlify.server; + +import net.fabricmc.fabric.api.networking.v1.FabricPacket; +import net.fabricmc.fabric.api.networking.v1.PacketType; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +public record ReachAroundPolicyPacket(boolean allowed) implements FabricPacket { + public static final PacketType TYPE = PacketType.create(new ResourceLocation("controlify", "reach_around_policy"), ReachAroundPolicyPacket::new); + + public ReachAroundPolicyPacket(FriendlyByteBuf buf) { + this(buf.readBoolean()); + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeBoolean(allowed); + } + + @Override + public PacketType getType() { + return TYPE; + } +} diff --git a/src/main/java/dev/isxander/controlify/server/VibrateCommand.java b/src/main/java/dev/isxander/controlify/server/VibrateCommand.java new file mode 100644 index 0000000..7b18ae6 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/server/VibrateCommand.java @@ -0,0 +1,159 @@ +package dev.isxander.controlify.server; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.FloatArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import dev.isxander.controlify.rumble.RumbleSource; +import dev.isxander.controlify.rumble.RumbleState; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.ResourceLocationArgument; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; +import net.minecraft.commands.synchronization.SuggestionProviders; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; + +public class VibrateCommand { + private static final SuggestionProvider SOURCES_SUGGESTION = SuggestionProviders.register( + new ResourceLocation("controlify", "vibration_sources"), + (context, builder) -> SharedSuggestionProvider.suggestResource( + RumbleSource.values().stream() + .map(RumbleSource::id) + .toList(), + builder + ) + ); + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register( + Commands.literal("vibratecontroller") + .requires(source -> source.hasPermission(2)) + .then( + Commands.argument("receivers", EntityArgument.players()) + .then( + Commands.argument("low_freq_vibration", FloatArgumentType.floatArg(0, 1)) + .then( + Commands.argument("high_freq_vibration", FloatArgumentType.floatArg(0, 1)) + .then( + Commands.argument("duration", IntegerArgumentType.integer(1)) + .then( + Commands.literal("static") + .executes(context -> vibrateStatic( + context.getSource(), + EntityArgument.getPlayers(context, "receivers"), + FloatArgumentType.getFloat(context, "low_freq_vibration"), + FloatArgumentType.getFloat(context, "high_freq_vibration"), + IntegerArgumentType.getInteger(context, "duration"), + RumbleSource.MASTER + )) + ) + .then( + Commands.literal("positioned") + .then( + Commands.argument("range", FloatArgumentType.floatArg(0)) + .then( + Commands.argument("position", Vec3Argument.vec3(true)) + .executes(context -> vibrateFromOrigin( + context.getSource(), + EntityArgument.getPlayers(context, "receivers"), + Vec3Argument.getVec3(context, "position"), + FloatArgumentType.getFloat(context, "range"), + IntegerArgumentType.getInteger(context, "duration"), + FloatArgumentType.getFloat(context, "low_freq_vibration"), + FloatArgumentType.getFloat(context, "high_freq_vibration"), + RumbleSource.MASTER + )) + ) + .then( + Commands.argument("entity", EntityArgument.entity()) + .executes(context -> vibrateFromEntity( + context.getSource(), + EntityArgument.getPlayers(context, "receivers"), + EntityArgument.getEntity(context, "entity"), + FloatArgumentType.getFloat(context, "range"), + IntegerArgumentType.getInteger(context, "duration"), + FloatArgumentType.getFloat(context, "low_freq_vibration"), + FloatArgumentType.getFloat(context, "high_freq_vibration"), + RumbleSource.MASTER + )) + ) + ) + + ) + ) + ) + ) + ) + ); + } + + private static int vibrateStatic(CommandSourceStack source, Collection targets, float lowFreqMagnitude, float highFreqMagnitude, int durationTicks, RumbleSource rumbleSource) { + RumbleState[] frames = new RumbleState[durationTicks]; + Arrays.fill(frames, new RumbleState(lowFreqMagnitude, highFreqMagnitude)); + + VibrationPacket packet = new VibrationPacket(rumbleSource, frames); + for (var player : targets) { + ServerPlayNetworking.send(player, packet); + } + + source.sendSuccess( + () -> targets.size() == 1 + ? Component.translatable("controlify.command.vibratecontroller.static.single") + : Component.translatable("controlify.command.vibratecontroller.static.multiple", targets.size()), + true + ); + + return targets.size(); + } + + private static int vibrateFromOrigin(CommandSourceStack source, Collection targets, Vec3 origin, float effectRange, int duration, float lowFreqMagnitude, float highFreqMagnitude, RumbleSource rumbleSource) { + RumbleState state = new RumbleState(lowFreqMagnitude, highFreqMagnitude); + + OriginVibrationPacket packet = new OriginVibrationPacket(origin.toVector3f(), effectRange, duration, state, rumbleSource); + for (var player : targets) { + ServerPlayNetworking.send(player, packet); + } + + source.sendSuccess( + () -> targets.size() == 1 + ? Component.translatable("controlify.command.vibratecontroller.pos.single", formatDouble(origin.x), formatDouble(origin.y), formatDouble(origin.z)) + : Component.translatable("controlify.command.vibratecontroller.pos.multiple", targets.size(), formatDouble(origin.x), formatDouble(origin.y), formatDouble(origin.z)), + true + ); + + return targets.size(); + } + + private static int vibrateFromEntity(CommandSourceStack source, Collection targets, Entity origin, float effectRange, int duration, float lowFreqMagnitude, float highFreqMagnitude, RumbleSource rumbleSource) { + RumbleState state = new RumbleState(lowFreqMagnitude, highFreqMagnitude); + + EntityVibrationPacket packet = new EntityVibrationPacket(origin.getId(), effectRange, duration, state, rumbleSource); + for (var player : targets) { + ServerPlayNetworking.send(player, packet); + } + + source.sendSuccess( + () -> targets.size() == 1 + ? Component.translatable("controlify.command.vibratecontroller.entity.single", origin.getDisplayName()) + : Component.translatable("controlify.command.vibratecontroller.entity.multiple", targets.size(), origin.getDisplayName()), + true + ); + + return targets.size(); + } + + private static String formatDouble(double d) { + return String.format(Locale.ROOT, "%f", d); + } +} diff --git a/src/main/java/dev/isxander/controlify/server/VibrationPacket.java b/src/main/java/dev/isxander/controlify/server/VibrationPacket.java new file mode 100644 index 0000000..75d5d10 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/server/VibrationPacket.java @@ -0,0 +1,54 @@ +package dev.isxander.controlify.server; + +import dev.isxander.controlify.rumble.BasicRumbleEffect; +import dev.isxander.controlify.rumble.RumbleEffect; +import dev.isxander.controlify.rumble.RumbleSource; +import dev.isxander.controlify.rumble.RumbleState; +import net.fabricmc.fabric.api.networking.v1.FabricPacket; +import net.fabricmc.fabric.api.networking.v1.PacketType; +import net.minecraft.client.Minecraft; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +public record VibrationPacket(RumbleSource source, RumbleState[] frames) implements FabricPacket { + public static final PacketType TYPE = PacketType.create(new ResourceLocation("controlify", "vibration"), VibrationPacket::new); + + public VibrationPacket(FriendlyByteBuf buf) { + this(RumbleSource.get(buf.readResourceLocation()), readFrames(buf)); + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeResourceLocation(source.id()); + + int[] packedFrames = new int[frames.length]; + for (int i = 0; i < frames.length; i++) { + RumbleState frame = frames[i]; + int high = (int)(frame.strong() * 32767.0F); + int low = (int)(frame.weak() * 32767.0F); + packedFrames[i] = (high << 16) | (low & 0xFFFF); + } + buf.writeVarIntArray(packedFrames); + } + + @Override + public PacketType getType() { + return TYPE; + } + + public RumbleEffect createEffect() { + return new BasicRumbleEffect(frames).earlyFinish(() -> Minecraft.getInstance().level == null); + } + + private static RumbleState[] readFrames(FriendlyByteBuf buf) { + int[] packedFrames = buf.readVarIntArray(); + RumbleState[] frames = new RumbleState[packedFrames.length]; + for (int i = 0; i < packedFrames.length; i++) { + int packed = packedFrames[i]; + float strong = (short)(packed >> 16) / 32767.0F; + float weak = (short)packed / 32767.0F; + frames[i] = new RumbleState(strong, weak); + } + return frames; + } +} diff --git a/src/main/java/dev/isxander/controlify/utils/DebugLog.java b/src/main/java/dev/isxander/controlify/utils/DebugLog.java index 74d03e0..b2c6e47 100644 --- a/src/main/java/dev/isxander/controlify/utils/DebugLog.java +++ b/src/main/java/dev/isxander/controlify/utils/DebugLog.java @@ -1,12 +1,11 @@ package dev.isxander.controlify.utils; -import dev.isxander.controlify.Controlify; import dev.isxander.controlify.debug.DebugProperties; public class DebugLog { public static void log(String message, Object... args) { if (DebugProperties.DEBUG_LOGGING) { - Controlify.LOGGER.info(message, args); + Log.LOGGER.info(message, args); } } } diff --git a/src/main/java/dev/isxander/controlify/utils/Easings.java b/src/main/java/dev/isxander/controlify/utils/Easings.java index e502604..4f62c3f 100644 --- a/src/main/java/dev/isxander/controlify/utils/Easings.java +++ b/src/main/java/dev/isxander/controlify/utils/Easings.java @@ -1,6 +1,12 @@ package dev.isxander.controlify.utils; +import net.minecraft.util.Mth; + public class Easings { + public static float easeInSine(float t) { + return 1 - Mth.cos((float) ((t * Math.PI) / 2)); + } + public static float easeInQuad(float t) { return t * t; } diff --git a/src/main/java/dev/isxander/controlify/utils/Log.java b/src/main/java/dev/isxander/controlify/utils/Log.java new file mode 100644 index 0000000..4a5423b --- /dev/null +++ b/src/main/java/dev/isxander/controlify/utils/Log.java @@ -0,0 +1,8 @@ +package dev.isxander.controlify.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Log { + public static final Logger LOGGER = LoggerFactory.getLogger("Controlify"); +} diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 4f9669a..5ccefdd 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -13,12 +13,15 @@ "controlify.gui.reach_around.tooltip": "If enabled, you can interact with the block you are standing on in the direction you are looking.", "controlify.gui.reach_around.tooltip.parity": "This is parity with bedrock edition where you can also do this.", "controlify.gui.reach_around.tooltip.warning": "WARNING: This is an unfair advantage over other players without Controlify, and you will likely be flagged by many anti-cheats. This should only be used in situations where everyone playing recognises that you have this ability and are okay with it.", + "controlify.gui.reach_around.tooltip.server_disabled": "The server you are playing on has does not allow you to use this feature.", "controlify.reach_around.off": "Off", "controlify.reach_around.singleplayer_only": "Singleplayer Only", "controlify.reach_around.singleplayer_and_lan": "Singleplayer and LAN", "controlify.reach_around.everywhere": "Everywhere", "controlify.gui.ui_sounds": "UI Sounds", "controlify.gui.ui_sounds.tooltip": "If enabled, Controlify will play UI sounds when you interact with the UI, like in legacy console editions of Minecraft.", + "controlify.gui.allow_server_rumble": "Allow Server Vibration", + "controlify.gui.allow_server_rumble.tooltip": "Accepts vibration packets from servers and vibrates your controller. If a server is doing this maliciously you can turn it off here.", "controlify.gui.notify_low_battery": "Notify Low Battery", "controlify.gui.notify_low_battery.tooltip": "A toast will appear when your wireless controller's battery becomes low. (EXPERIMENTAL)", "controlify.gui.out_of_focus_input": "Out of Focus Input", @@ -270,6 +273,13 @@ "controlify.battery_level.high": "High", "controlify.battery_level.full": "Full", + "controlify.command.vibratecontroller.static.single": "Vibrated controller of 1 player.", + "controlify.command.vibratecontroller.static.multiple": "Vibrated controller of %s players.", + "controlify.command.vibratecontroller.pos.single": "Vibrated controller of 1 player at %s, %s, %s.", + "controlify.command.vibratecontroller.pos.multiple": "Vibrated controller of %s players at %s, %s, %s.", + "controlify.command.vibratecontroller.entity.single": "Vibrated controller of 1 player from %s's position.", + "controlify.command.vibratecontroller.entity.multiple": "Vibrated controller of %s players from %s's position.", + "controlify.hat_state.up": "Up", "controlify.hat_state.down": "Down", "controlify.hat_state.left": "Left", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 61118a9..8392579 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -13,7 +13,7 @@ "sources": "https://github.com/${github}" }, "license": "LGPL-3.0-or-later", - "environment": "client", + "environment": "*", "entrypoints": { "preLaunch": [ "com.llamalad7.mixinextras.MixinExtrasBootstrap::init" @@ -21,8 +21,14 @@ "modmenu": [ "dev.isxander.controlify.config.ModMenuIntegration" ], + "main": [ + "dev.isxander.controlify.server.ControlifyServer" + ], "client": [ "dev.isxander.controlify.ControlifyEntrypoint" + ], + "server": [ + "dev.isxander.controlify.server.ControlifyServer" ] }, "mixins": [ @@ -31,12 +37,14 @@ "icon": "icon.png", "accessWidener": "controlify.accesswidener", "depends": { - "fabricloader": ">=0.14.0", - "minecraft": ">1.20-", + "fabricloader": ">=0.14.21", + "minecraft": "1.20.x", "java": ">=17", - "yet_another_config_lib_v3": ">=3.0.0-" + "yet_another_config_lib_v3": ">=3.0.0-", + "fabric-api": "*" }, "breaks": { - "midnightcontrols": "*" + "midnightcontrols": "*", + "controllable": "*" } }