From 19cdf5966639b47ab798ca6af5a742c845babafe Mon Sep 17 00:00:00 2001 From: isXander Date: Wed, 2 Aug 2023 23:16:51 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9E=95=20Unknown=20controller=20submission?= =?UTF-8?q?=20screen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/isxander/controlify/Controlify.java | 10 + .../controller/ControllerConfig.java | 2 + .../controlify/controller/ControllerType.java | 7 +- .../gui/screen/CustomWarningTitlePadding.java | 7 + .../screen/SubmitUnknownControllerScreen.java | 195 ++++++++++++++++++ .../controlify/hid/ControllerHIDService.java | 2 - .../feature/gui/WarningScreenMixin.java | 20 ++ .../assets/controlify/lang/en_us.json | 11 +- src/main/resources/controlify.mixins.json | 19 +- 9 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/gui/screen/CustomWarningTitlePadding.java create mode 100644 src/main/java/dev/isxander/controlify/gui/screen/SubmitUnknownControllerScreen.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/gui/WarningScreenMixin.java diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index b7e29d8..3986799 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -11,6 +11,7 @@ import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.gui.screen.ControllerCalibrationScreen; import dev.isxander.controlify.gui.screen.SDLOnboardingScreen; +import dev.isxander.controlify.gui.screen.SubmitUnknownControllerScreen; import dev.isxander.controlify.ingame.ControllerPlayerMovement; import dev.isxander.controlify.reacharound.ReachAroundHandler; import dev.isxander.controlify.reacharound.ReachAroundMode; @@ -43,6 +44,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.libsdl.SDL; import org.lwjgl.glfw.GLFW; import java.util.ArrayDeque; @@ -168,6 +170,10 @@ public class Controlify implements ControlifyApi { config().loadOrCreateControllerData(controller); + if (SubmitUnknownControllerScreen.canSubmit(controller)) { + minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen)); + } + if (controller.uid().equals(config().currentControllerUid())) setCurrentController(controller); @@ -379,6 +385,10 @@ public class Controlify implements ControlifyApi { config().loadOrCreateControllerData(controller); + if (SubmitUnknownControllerScreen.canSubmit(controller)) { + minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen)); + } + canDiscoverControllers = false; if (config().globalSettings().delegateSetup) { config().globalSettings().delegateSetup = false; diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java b/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java index 5220650..cd0328d 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerConfig.java @@ -36,6 +36,8 @@ public abstract class ControllerConfig implements Serializable { public boolean mixedInput = false; + public boolean dontShowControllerSubmission = false; + public abstract void setDeadzone(int axis, float deadzone); public abstract float getDeadzone(int axis); diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerType.java b/src/main/java/dev/isxander/controlify/controller/ControllerType.java index 38f9eca..665bb4d 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerType.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerType.java @@ -18,7 +18,12 @@ public record ControllerType(String friendlyName, String mappingId, String theme private static final ResourceLocation hidDbLocation = new ResourceLocation("controlify", "controllers/controller_identification.json5"); public static ControllerType getTypeForHID(HIDIdentifier hid) { - return getTypeMap().getOrDefault(hid, ControllerType.UNKNOWN); + if (getTypeMap().containsKey(hid)) { + return getTypeMap().get(hid); + } else { + Log.LOGGER.warn("Controller found via USB hardware scan, but it was not found in the controller identification database! (HID: {})", hid); + return ControllerType.UNKNOWN; + } } public static void ensureTypeMapFilled() { diff --git a/src/main/java/dev/isxander/controlify/gui/screen/CustomWarningTitlePadding.java b/src/main/java/dev/isxander/controlify/gui/screen/CustomWarningTitlePadding.java new file mode 100644 index 0000000..5e9acec --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/screen/CustomWarningTitlePadding.java @@ -0,0 +1,7 @@ +package dev.isxander.controlify.gui.screen; + +public interface CustomWarningTitlePadding { + default int getMessageY() { + return 70; + } +} diff --git a/src/main/java/dev/isxander/controlify/gui/screen/SubmitUnknownControllerScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/SubmitUnknownControllerScreen.java new file mode 100644 index 0000000..f515c94 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/screen/SubmitUnknownControllerScreen.java @@ -0,0 +1,195 @@ +package dev.isxander.controlify.gui.screen; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.controller.Controller; +import dev.isxander.controlify.controller.ControllerType; +import dev.isxander.controlify.hid.HIDDevice; +import dev.isxander.controlify.utils.Log; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.multiplayer.WarningScreen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.lwjgl.glfw.GLFW; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.regex.Pattern; + +public class SubmitUnknownControllerScreen extends WarningScreen implements CustomWarningTitlePadding { + public static final String SUBMISSION_URL = "https://api-controlify.isxander.dev/api/v1/submit"; + public static final Pattern NAME_PATTERN = Pattern.compile("^[\\w\\- ]{3,32}$"); + + private final Controller controller; + + private final Screen lastScreen; + + private int invalidNameTicks; + + private Button submitButton; + private EditBox nameField; + + public SubmitUnknownControllerScreen(Controller controller, Screen lastScreen) { + super( + Component.translatable("controlify.controller_submission.title").withStyle(ChatFormatting.BOLD), + Component.translatable("controlify.controller_submission.message"), + Component.translatable("controlify.controller_submission.dont_show_again"), + Component.translatable("controlify.controller_submission.title") + ); + if (!canSubmit(controller)) + throw new IllegalArgumentException("Controller ineligible for submission!"); + this.controller = controller; + this.lastScreen = lastScreen; + } + + @Override + protected void initButtons(int textHeight) { + this.submitButton = this.addRenderableWidget( + Button.builder(Component.translatable("controlify.controller_submission.submit"), this::onSubmitButton) + .pos(this.width / 2 - 155, textHeight + 80) + .width(150) + .build() + ); + this.addRenderableWidget( + Button.builder(CommonComponents.GUI_CANCEL, btn -> onClose()) + .pos(this.width / 2 + 5, textHeight + 80) + .width(150) + .build() + ); + this.nameField = this.addRenderableWidget( + new EditBox( + font, + this.width / 2 - 155, + textHeight + 105, + 310, 20, + Component.translatable("controlify.controller_submission.name_narration") + ) + ); + this.nameField.setHint(Component.translatable("controlify.controller_submission.name_hint")); + this.nameField.setValue(controller.name()); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + renderBackground(graphics); + + super.render(graphics, mouseX, mouseY, delta); + + if (invalidNameTicks > 0) { + graphics.drawCenteredString(font, Component.translatable("controlify.controller_submission.invalid_name").withStyle(ChatFormatting.RED), this.width / 2, nameField.getRectangle().bottom() + 4, -1); + } + } + + @Override + public void tick() { + if (invalidNameTicks > 0) { + invalidNameTicks--; + submitButton.active = invalidNameTicks < 50; + } + } + + protected void onSubmitButton(Button button) { + invalidNameTicks = 0; + + if (!checkValidName()) { + invalidNameTicks = 100; + return; + } + + if (submit()) { + dontShowAgain(); + onClose(); + } else { + // TODO: Show error message + onClose(); + } + } + + protected boolean submit() { + try { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(SUBMISSION_URL)) + .POST(HttpRequest.BodyPublishers.ofString(this.generateRequestBody())) + .header("Content-Type", "application/json") + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() / 100 != 2) { + Log.LOGGER.error("Received non-2xx status code from '{}', got {} with body '{}'", SUBMISSION_URL, response.statusCode(), response.body()); + return false; + } + + Log.LOGGER.info("Successfully sent controller information to '{}'", SUBMISSION_URL); + return true; + } catch (Exception e) { + Log.LOGGER.error("Failed to submit controller to '%s'".formatted(SUBMISSION_URL), e); + return false; + } + } + + private String generateRequestBody() { + HIDDevice hid = controller.hidInfo().orElseThrow().hidDevice().orElseThrow(); + + JsonObject object = new JsonObject(); + object.addProperty("vendorID", hid.vendorID()); + object.addProperty("productID", hid.productID()); + object.addProperty("GUID", GLFW.glfwGetJoystickGUID(controller.joystickId())); + object.addProperty("reportedName", nameField.getValue()); + object.addProperty("controlifyVersion", FabricLoader.getInstance().getModContainer("controlify").orElseThrow().getMetadata().getVersion().getFriendlyString()); + + Gson gson = new Gson(); + return gson.toJson(object); + } + + private boolean checkValidName() { + String name = nameField.getValue().trim(); + return NAME_PATTERN.matcher(name).matches(); + } + + private void dontShowAgain() { + controller.config().dontShowControllerSubmission = true; + Controlify.instance().config().setDirty(); + } + + @Override + public void onClose() { + if (stopShowing.selected()) { + dontShowAgain(); + } + + Controlify.instance().config().saveIfDirty(); + minecraft.setScreen(lastScreen); + } + + @Override + public boolean shouldCloseOnEsc() { + return false; + } + + @Override + protected int getLineHeight() { + return 9; + } + + @Override + public int getMessageY() { + return 50; + } + + public static boolean canSubmit(Controller controller) { + return controller.type() == ControllerType.UNKNOWN + && !controller.config().dontShowControllerSubmission + && controller.hidInfo() + .map(info -> info.hidDevice().isPresent()) + .orElse(false); + } +} diff --git a/src/main/java/dev/isxander/controlify/hid/ControllerHIDService.java b/src/main/java/dev/isxander/controlify/hid/ControllerHIDService.java index ae7c3cf..c65587c 100644 --- a/src/main/java/dev/isxander/controlify/hid/ControllerHIDService.java +++ b/src/main/java/dev/isxander/controlify/hid/ControllerHIDService.java @@ -76,8 +76,6 @@ public class ControllerHIDService { } ControllerType type = ControllerType.getTypeForHID(hid.getSecond()); - if (type == ControllerType.UNKNOWN) - 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/mixins/feature/gui/WarningScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/gui/WarningScreenMixin.java new file mode 100644 index 0000000..23dfc78 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/gui/WarningScreenMixin.java @@ -0,0 +1,20 @@ +package dev.isxander.controlify.mixins.feature.gui; + +import dev.isxander.controlify.gui.screen.CustomWarningTitlePadding; +import net.minecraft.client.gui.screens.multiplayer.WarningScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(WarningScreen.class) +public class WarningScreenMixin implements CustomWarningTitlePadding { + @ModifyConstant(method = "render", constant = @Constant(intValue = 70)) + private int modifyMessageY(int original) { + return this.getMessageY(); + } + + @ModifyConstant(method = "init", constant = @Constant(intValue = 76)) + private int modifyCheckboxY(int original) { + return this.getMessageY() + 6; + } +} diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 6714c78..062df50 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -279,10 +279,13 @@ "controlify.calibration.later": "Maybe Later", "controlify.calibration.later.tooltip": "You must calibrate to use the controller. Pressing this will deactivate the controller and you will have to use it again to calibrate.", - "controlify.beta.title": "Controlify Beta Notice", - "controlify.beta.message": "You are currently using Controlify Beta.\n\nThis mod is a work in progress and will contain many bugs. Please, if you spot a bug in this mod or have a suggestion to make it even better, please create an issue on the %s!\n\nYou can always find the link to the issue tracker in Controlify's settings menu.", - "controlify.beta.message.link": "issue tracker", - "controlify.beta.button": "Open Issue Tracker...", + "controlify.controller_submission.title": "Unknown Controller Submission", + "controlify.controller_submission.message": "Please submit some of your controller info to Controlify's database to get it added in a future update.\n\nControlify sends the following information:\n- Your controller's vendor & product IDs\n- Your controller's GUID\n- The name of your controller (in the box below)\n- The version of Controlify you are currently on\n\nThis is completely anonymous and does store any of your personal or account information.", + "controlify.controller_submission.dont_show_again": "Do not show for this controller again", + "controlify.controller_submission.submit": "Submit Data", + "controlify.controller_submission.name_hint": "Controller name", + "controlify.controller_submission.name_narration": "Controller name input field", + "controlify.controller_submission.invalid_name": "Invalid controller name. Use only letters, numbers and spaces.", "controlify.error.hid": "Controller Detection Disabled", "controlify.error.hid.desc": "Controller identification failed, so any controller config changes will not persist. Check logs for more info.", diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index 4caac03..5d36e7b 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -7,26 +7,20 @@ }, "compatibilityLevel": "JAVA_17", "client": [ + "compat.fapi.KeyBindingRegistryImplAccessor", "compat.iris.BaseOptionElementWidgetMixin", "compat.sodium.CycleControlElementMixin", "compat.sodium.SliderControlElementMixin", - "compat.sodium.TickBoxControlElementMixin", - "core.GLXMixin", - "feature.fixes.boatfix.BoatMixin", - "feature.rumble.explosion.LightningBoltMixin", - "feature.rumble.fishing.FishingHookMixin", - "feature.rumble.itembreak.LivingEntityMixin", - "feature.rumble.levelevents.LevelRendererMixin", - "feature.rumble.useitem.LivingEntityMixin", - "compat.fapi.KeyBindingRegistryImplAccessor", "compat.sodium.SodiumOptionsGUIAccessor", "compat.sodium.SodiumOptionsGUIMixin", + "compat.sodium.TickBoxControlElementMixin", "compat.yacl.CyclingControllerElementMixin", "compat.yacl.SliderControllerElementMixin", "compat.yacl.YACLScreenCategoryTabAccessor", "compat.yacl.YACLScreenCategoryTabMixin", "compat.yacl.YACLScreenMixin", "core.ClientPacketListenerMixin", + "core.GLXMixin", "core.GuiMixin", "core.KeyboardHandlerMixin", "core.MinecraftMixin", @@ -38,7 +32,9 @@ "feature.bind.ToggleKeyMappingAccessor", "feature.chatkbheight.ChatComponentMixin", "feature.chatkbheight.ChatScreenMixin", + "feature.fixes.boatfix.BoatMixin", "feature.fixes.boatfix.LocalPlayerMixin", + "feature.gui.WarningScreenMixin", "feature.guide.ingame.ClientPacketListenerMixin", "feature.guide.ingame.GuiMixin", "feature.guide.screen.AbstractButtonMixin", @@ -50,7 +46,12 @@ "feature.rumble.blockbreak.MultiPlayerGameModeMixin", "feature.rumble.damage.LocalPlayerMixin", "feature.rumble.explosion.ClientPacketListenerMixin", + "feature.rumble.explosion.LightningBoltMixin", + "feature.rumble.fishing.FishingHookMixin", + "feature.rumble.itembreak.LivingEntityMixin", "feature.rumble.itembreak.LocalPlayerMixin", + "feature.rumble.levelevents.LevelRendererMixin", + "feature.rumble.useitem.LivingEntityMixin", "feature.rumble.useitem.LocalPlayerMixin", "feature.screenop.GameRendererMixin", "feature.screenop.MinecraftMixin",