1
0
forked from Clones/Controlify

server support

This commit is contained in:
isXander
2023-06-11 20:25:55 +01:00
parent d81e0dabf8
commit 5d4cc1232d
40 changed files with 599 additions and 112 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
run
runserver
build
/.idea
.gradle

View File

@ -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

View File

@ -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"

View File

@ -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(

View File

@ -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);

View File

@ -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<T extends ControllerState> {
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<T extends ControllerState> {
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);
}
}
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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<S extends ControllerState, C extends Co
try {
newConfig = gson.fromJson(json, new TypeToken<C>(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<S extends ControllerState, C extends Co
if (newConfig != null) {
this.config = newConfig;
} else {
Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead.");
Log.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead.");
this.config = defaultConfig();
Controlify.instance().config().setDirty();
}

View File

@ -1,8 +1,8 @@
package dev.isxander.controlify.controller;
import com.google.common.collect.ImmutableMap;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.hid.HIDIdentifier;
import dev.isxander.controlify.utils.Log;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.Resource;
@ -38,7 +38,7 @@ public record ControllerType(String friendlyName, String mappingId, String theme
readControllerIdFiles(reader);
} catch (Exception e) {
Controlify.LOGGER.error("Failed to load HID DB from source", e);
Log.LOGGER.error("Failed to load HID DB from source", e);
}
}
} catch (Exception e) {
@ -78,7 +78,7 @@ public record ControllerType(String friendlyName, String mappingId, String theme
} else if (productId == -1) {
productId = reader.nextInt();
} else {
Controlify.LOGGER.warn("Too many values in HID array. Skipping...");
Log.LOGGER.warn("Too many values in HID array. Skipping...");
reader.skipValue();
}
}
@ -90,7 +90,7 @@ public record ControllerType(String friendlyName, String mappingId, String theme
case "force_joystick" -> 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;
}

View File

@ -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<HidDevice, HIDIdentifier> 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()));

View File

@ -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<JoystickCo
if (newConfig != null) {
this.config = newConfig;
} else {
Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead.");
Log.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead.");
this.config = defaultConfig();
}
this.config.setup(this);

View File

@ -1,11 +1,11 @@
package dev.isxander.controlify.controller.joystick;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
import dev.isxander.controlify.debug.DebugProperties;
import dev.isxander.controlify.utils.ControllerUtils;
import dev.isxander.controlify.utils.Log;
import dev.isxander.yacl3.api.NameableEnum;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
@ -131,10 +131,10 @@ public class JoystickState implements ControllerState {
List<HatState> 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);

View File

@ -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<JoystickState,
// the duration doesn't matter because we are not updating the joystick state,
// so there is never any SDL check to stop the rumble after the desired time.
if (!SDL.SDL_JoystickRumbleTriggers(ptrJoystick, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 1)) {
Controlify.LOGGER.error("Could not rumble controller " + name() + ": " + SDL.SDL_GetError());
Log.LOGGER.error("Could not rumble controller " + name() + ": " + SDL.SDL_GetError());
return false;
}
return true;

View File

@ -1,11 +1,11 @@
package dev.isxander.controlify.controller.joystick.mapping;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.bindings.JoystickAxisBind;
import dev.isxander.controlify.controller.ControllerType;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.controller.joystick.render.JoystickRenderer;
import dev.isxander.controlify.utils.Log;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
@ -48,7 +48,7 @@ public class RPJoystickMapping implements JoystickMapping {
hats = readHats(reader, type);
}
default -> {
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());
}
}

View File

@ -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;

View File

@ -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) {

View File

@ -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(),

View File

@ -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;

View File

@ -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;

View File

@ -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));

View File

@ -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 {

View File

@ -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);
}

View File

@ -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.<Boolean>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.<Boolean>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.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.notify_low_battery"))
.description(OptionDescription.createBuilder()

View File

@ -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

View File

@ -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<Integer, RumbleState> 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<Integer, RumbleState> stateFunction, int priority, int timeout, int minTime) {
public ContinuousRumbleEffect(Function<Integer, RumbleState> 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<Float, Float> fallofFunction) {
this.inWorldProperties = new InWorldProperties(sourceLocation, minMagnitude, maxMagnitude, minDistance, maxDistance, fallofFunction);
public Builder inWorld(Supplier<Vec3> sourceLocation, float min, float max, float effectRange, Function<Float, Float> 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<Float, Float> fallofFunction) {
private record InWorldProperties(Supplier<Vec3> sourceLocation, float minMagnitude, float maxMagnitude, float effectRange, Function<Float, Float> fallofFunction) {
private Function<Integer, RumbleState> modify(Function<Integer, RumbleState> 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);
};
}
}

View File

@ -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<ResourceLocation, RumbleSource> 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<RumbleSource> values() {

View File

@ -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);
}

View File

@ -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));
});
}
}

View File

@ -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<ControlifyServerConfig> INSTANCE = GsonConfigInstance.createBuilder(ControlifyServerConfig.class)
.setPath(FabricLoader.getInstance().getConfigDir().resolve("controlify.json"))
.build();
public boolean reachAroundPolicy = false;
}

View File

@ -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<EntityVibrationPacket> 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;
}
}

View File

@ -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<OriginVibrationPacket> 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);
}
}

View File

@ -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<ReachAroundPolicyPacket> 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;
}
}

View File

@ -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<CommandSourceStack> 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<CommandSourceStack> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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);
}
}

View File

@ -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<VibrationPacket> 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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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");
}

View File

@ -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",

View File

@ -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": "*"
}
}