Skip to content

Commit f527f21

Browse files
committed
Finish & fix abstract controller manager + optimize hashmaps in codebase + no SDL screen
1 parent b07066e commit f527f21

24 files changed

+271
-133
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ plugins {
1111
}
1212

1313
group = "dev.isxander"
14-
version = "1.7.0-beta.2+1.20"
14+
version = "1.7.0-beta.3+1.20.2"
1515
val isAlpha = "alpha" in version.toString()
1616
val isBeta = "beta" in version.toString()
1717
if (isAlpha) println("Controlify alpha version detected.")

changelogs/1.7.0-beta.3+1.20.2.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Controlify 1.7.0 (Beta 3) for 1.20.2
2+
3+
## New Features
4+
5+
- Added D-Pad snapping in container screens
6+
- Keyboard-like movement whitelist and warning toast when joining new servers
7+
- Added bind to open F3 debug screen
8+
- More snap points on recipe book
9+
- Allow users to define a custom SDL natives path (so you can put them in a common dir if you want)
10+
- Add a reset all binds button to controls tab
11+
12+
## Changes
13+
14+
- Internal changes to the way controllers are discovered, loaded and managed. (this could introduce new bugs)
15+
- `delegate_setup` config option has been renamed to `quiet_mode`.
16+
- Pause screen's disconnect shortcut now focuses the button instead of clicking it.
17+
- Add a donate button to the controller carousel screen.
18+
- Modify how analogue inputs are processed whilst ingame or using the virtual mouse to make it feel more "circular"
19+
- Marginally improve performance of Controlify by using optimized hashmaps.
20+
21+
## Bug Fixes
22+
23+
- Fix hotplugging when using natives.
24+
- Fix SDL download screen progress bar being a missing texture.
25+
- Fix people being unable to write newlines and spaces in signs when using mixed input mode.
26+
- Fix some modded GUIs crashing when attempting to open when Controlify is loaded.
27+
- Fix tridents not causing a vibration.
28+
- Fix rumble not working on joysticks.
29+
- Fix fabric mod json requirement allowing any 1.20 version not 1.20.2 and above.

src/main/java/dev/isxander/controlify/Controlify.java

Lines changed: 83 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.mojang.blaze3d.Blaze3D;
44
import dev.isxander.controlify.api.ControlifyApi;
55
import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint;
6+
import dev.isxander.controlify.bindings.ControllerBindings;
67
import dev.isxander.controlify.compatibility.ControlifyCompat;
78
import dev.isxander.controlify.controller.joystick.JoystickController;
89
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
@@ -63,10 +64,12 @@ public class Controlify implements ControlifyApi {
6364
private boolean probeMode = false;
6465

6566
private Controller<?, ?> currentController = null;
67+
private InputMode currentInputMode = InputMode.KEYBOARD_MOUSE;
68+
6669
private InGameInputHandler inGameInputHandler;
6770
public InGameButtonGuide inGameButtonGuide;
6871
private VirtualMouseHandler virtualMouseHandler;
69-
private InputMode currentInputMode = InputMode.KEYBOARD_MOUSE;
72+
7073
private ControllerHIDService controllerHIDService;
7174

7275
private CompletableFuture<Boolean> nativeOnboardingFuture = null;
@@ -81,10 +84,6 @@ public class Controlify implements ControlifyApi {
8184

8285
private int showMouseTicks = 0;
8386

84-
private @Nullable Controller<?, ?> switchableController = null;
85-
private double askSwitchTime = 0;
86-
private ToastUtils.ControlifyToast askSwitchToast = null;
87-
8887
/**
8988
* Called at usual fabric client entrypoint.
9089
* Always runs, even with no controllers detected.
@@ -178,10 +177,6 @@ public void initializeControlify() {
178177
ClientTickEvents.END_CLIENT_TICK.register(client -> this.probeTick());
179178
}
180179

181-
// initialise and compatability modules that controlify implements itself
182-
// this does NOT invoke any entrypoints. this is done in the pre-initialisation phase
183-
ControlifyCompat.init();
184-
185180
// register events
186181
ClientLifecycleEvents.CLIENT_STOPPING.register(minecraft -> {
187182
controllerHIDService().stop();
@@ -211,19 +206,28 @@ public void discoverControllers() {
211206
Log.LOGGER.info("No controllers found.");
212207
}
213208

214-
// if no controller is currently selected, select the first one
209+
// if no controller is currently selected, pick one
215210
if (getCurrentController().isEmpty()) {
216-
Controller<?, ?> controller = controllerManager.getConnectedControllers().stream().findFirst().orElse(null);
217-
if (controller != null && (controller.config().delayedCalibration || !controller.config().deadzonesCalibrated)) {
218-
controller = null;
219-
}
211+
Optional<Controller<?, ?>> lastUsedController = controllerManager.getConnectedControllers()
212+
.stream()
213+
.filter(c -> c.uid().equals(config().currentControllerUid()))
214+
.findAny();
220215

221-
this.setCurrentController(controller, false);
222-
} else {
223-
// setCurrentController saves config so there is no need to set dirty to save
224-
config().saveIfDirty();
216+
if (lastUsedController.isPresent()) {
217+
this.setCurrentController(lastUsedController.get(), false);
218+
} else {
219+
Controller<?, ?> anyController = controllerManager.getConnectedControllers()
220+
.stream()
221+
.filter(c -> !c.config().delayedCalibration && c.config().deadzonesCalibrated)
222+
.findFirst()
223+
.orElse(null);
224+
225+
this.setCurrentController(anyController, false);
226+
}
225227
}
226228

229+
config().saveIfDirty();
230+
227231
FabricLoader.getInstance().getEntrypoints("controlify", ControlifyEntrypoint.class).forEach(entrypoint -> {
228232
try {
229233
entrypoint.onControllersDiscovered(this);
@@ -233,10 +237,16 @@ public void discoverControllers() {
233237
});
234238
}
235239

240+
/**
241+
* Completely finishes controlify initialization.
242+
* This can be run at any point during the game's lifecycle.
243+
* @return the future that completes when controlify has finished initializing
244+
*/
236245
public CompletableFuture<Void> finishControlifyInit() {
237246
if (finishedInit) {
238247
return CompletableFuture.completedFuture(null);
239248
}
249+
probeMode = false;
240250
finishedInit = true;
241251

242252
askNatives().whenComplete((loaded, th) -> {
@@ -248,12 +258,33 @@ public CompletableFuture<Void> finishControlifyInit() {
248258
ConnectServerEvent.EVENT.register((minecraft, address, data) -> {
249259
notifyNewServer(data);
250260
});
261+
262+
// initialise and compatability modules that controlify implements itself
263+
// this does NOT invoke any entrypoints. this is done in the pre-initialisation phase
264+
ControlifyCompat.init();
265+
266+
// make sure people don't someone add binds after controllers could have been created
267+
ControllerBindings.lockRegistry();
268+
269+
if (config().globalSettings().quietMode) {
270+
config().globalSettings().quietMode = false;
271+
config().setDirty();
272+
}
273+
251274
discoverControllers();
252275
});
253276

254277
return askNatives().thenApply(loaded -> null);
255278
}
256279

280+
/**
281+
* Called when a controller is connected. Either from controller
282+
* discovery or hotplugging.
283+
*
284+
* @param controller the new controller
285+
* @param hotplugged if this was a result of hotplugging
286+
* @param newController if this controller has never been seen before
287+
*/
257288
private void onControllerAdded(Controller<?, ?> controller, boolean hotplugged, boolean newController) {
258289
if (SubmitUnknownControllerScreen.canSubmit(controller)) {
259290
minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen));
@@ -264,12 +295,10 @@ private void onControllerAdded(Controller<?, ?> controller, boolean hotplugged,
264295
config().setDirty();
265296
}
266297

267-
if (hotplugged) {
268-
if (controller.config().deadzonesCalibrated) {
269-
setCurrentController(controller, hotplugged);
270-
} else {
271-
calibrationQueue.add(controller);
272-
}
298+
if (!controller.config().deadzonesCalibrated) {
299+
calibrationQueue.add(controller);
300+
} else if (hotplugged) {
301+
setCurrentController(controller, true);
273302
}
274303

275304
if (controller instanceof JoystickController<?> joystick && joystick.mapping() instanceof UnmappedJoystickMapping) {
@@ -278,7 +307,7 @@ private void onControllerAdded(Controller<?, ?> controller, boolean hotplugged,
278307
Component.translatable("controlify.toast.unmapped_joystick.description", controller.name()),
279308
true
280309
);
281-
} else {
310+
} else if (hotplugged) {
282311
ToastUtils.sendToast(
283312
Component.translatable("controlify.toast.controller_connected.title"),
284313
Component.translatable("controlify.toast.controller_connected.description", controller.name()),
@@ -289,8 +318,17 @@ private void onControllerAdded(Controller<?, ?> controller, boolean hotplugged,
289318
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
290319
controllerListScreen.refreshControllers();
291320
}
321+
322+
// saved after discovery
323+
if (hotplugged) {
324+
config().saveIfDirty();
325+
}
292326
}
293327

328+
/**
329+
* Called when a controller is disconnected.
330+
* @param controller controller that has been disconnected
331+
*/
294332
private void onControllerRemoved(Controller<?, ?> controller) {
295333
this.setCurrentController(
296334
controllerManager.getConnectedControllers()
@@ -325,6 +363,14 @@ public CompletableFuture<Boolean> askNatives() {
325363
if (nativeOnboardingFuture != null)
326364
return nativeOnboardingFuture;
327365

366+
// just say no if the platform doesn't support it
367+
if (!SDL2NativesManager.isSupportedOnThisPlatform()) {
368+
Log.LOGGER.warn("SDL is not supported on this platform. Platform: {}", SDL2NativesManager.Target.CURRENT);
369+
nativeOnboardingFuture = new CompletableFuture<>();
370+
minecraft.setScreen(new NoSDLScreen(() -> nativeOnboardingFuture.complete(false), minecraft.screen));
371+
return nativeOnboardingFuture;
372+
}
373+
328374
// the user has already been asked, initialise SDL if necessary
329375
// and return a completed future
330376
if (config().globalSettings().vibrationOnboarded) {
@@ -373,19 +419,10 @@ public void tick(Minecraft client) {
373419

374420
boolean outOfFocus = !config().globalSettings().outOfFocusInput && !client.isWindowActive();
375421

422+
// handles updating state of all controllers
376423
controllerManager.tick(outOfFocus);
377424

378-
if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) {
379-
if (switchableController.state().hasAnyInput()) {
380-
switchableController.clearState();
381-
this.setCurrentController(switchableController, true); // setCurrentController sets switchableController to null
382-
if (askSwitchToast != null) {
383-
askSwitchToast.remove();
384-
askSwitchToast = null;
385-
}
386-
}
387-
}
388-
425+
// handle showing/hiding mouse whilst in mixed input mode
389426
if (minecraft.mouseHandler.isMouseGrabbed())
390427
showMouseTicks = 0;
391428
if (currentInputMode() == InputMode.MIXED && showMouseTicks > 0) {
@@ -400,6 +437,7 @@ public void tick(Minecraft client) {
400437

401438
LowBatteryNotifier.tick();
402439

440+
// if splitscreen ever happens this can tick over every controller
403441
getCurrentController().ifPresent(currentController -> {
404442
wrapControllerError(
405443
() -> tickController(currentController, outOfFocus),
@@ -409,6 +447,12 @@ public void tick(Minecraft client) {
409447
});
410448
}
411449

450+
/**
451+
* Ticks a specific controller.
452+
*
453+
* @param controller controller to tick
454+
* @param outOfFocus if the window is out of focus
455+
*/
412456
private void tickController(Controller<?, ?> controller, boolean outOfFocus) {
413457
ControllerState state = controller.state();
414458

@@ -479,10 +523,6 @@ public void setCurrentController(@Nullable Controller<?, ?> controller, boolean
479523

480524
this.currentController = controller;
481525

482-
if (switchableController == controller) {
483-
switchableController = null;
484-
}
485-
486526
if (controller == null) {
487527
this.setInputMode(InputMode.KEYBOARD_MOUSE);
488528
this.inGameInputHandler = null;
@@ -495,7 +535,7 @@ public void setCurrentController(@Nullable Controller<?, ?> controller, boolean
495535
DebugLog.log("Updated current controller to {}({})", controller.name(), controller.uid());
496536

497537
if (!controller.uid().equals(config().currentControllerUid())) {
498-
config().save();
538+
config().setDirty();
499539
}
500540

501541
this.inGameInputHandler = new InGameInputHandler(controller);
@@ -504,6 +544,8 @@ public void setCurrentController(@Nullable Controller<?, ?> controller, boolean
504544
setInputMode(InputMode.MIXED);
505545
else if (changeInputMode)
506546
setInputMode(InputMode.CONTROLLER);
547+
548+
config().saveIfDirty();
507549
}
508550

509551
public Optional<ControllerManager> getControllerManager() {

src/main/java/dev/isxander/controlify/api/bind/ControllerBinding.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.google.gson.JsonObject;
44
import dev.isxander.controlify.bindings.BindContext;
55
import dev.isxander.controlify.bindings.IBind;
6-
import dev.isxander.controlify.bindings.RadialIcons;
76
import dev.isxander.yacl3.api.Option;
87
import net.minecraft.client.KeyMapping;
98
import net.minecraft.network.chat.Component;

src/main/java/dev/isxander/controlify/bindings/ControllerBindingImpl.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import dev.isxander.controlify.gui.DrawSize;
1818
import dev.isxander.yacl3.api.Option;
1919
import dev.isxander.yacl3.api.OptionDescription;
20+
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
21+
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
2022
import net.minecraft.client.KeyMapping;
2123
import net.minecraft.client.gui.GuiGraphics;
2224
import net.minecraft.locale.Language;
@@ -39,9 +41,9 @@ public class ControllerBindingImpl<T extends ControllerState> implements Control
3941
private final ResourceLocation radialIcon;
4042
private final KeyMappingOverride override;
4143

42-
private static final Map<Controller<?, ?>, Set<IBind<?>>> pressedBinds = new HashMap<>();
44+
private static final Map<Controller<?, ?>, Set<IBind<?>>> pressedBinds = new Object2ObjectOpenHashMap<>();
4345

44-
private int fakePressState = 0;
46+
private byte fakePressState = 0;
4547

4648
private ControllerBindingImpl(Controller<T, ?> controller, IBind<T> defaultBind, ResourceLocation id, KeyMappingOverride vanillaOverride, Component name, Component description, Component category, Set<BindContext> contexts, ResourceLocation icon) {
4749
this.controller = controller;
@@ -219,7 +221,7 @@ private static boolean hasBindPressed(ControllerBindingImpl<?> binding) {
219221
}
220222

221223
private static void addPressedBind(ControllerBindingImpl<?> binding) {
222-
pressedBinds.computeIfAbsent(binding.controller, c -> new HashSet<>()).addAll(getBinds(binding.bind));
224+
pressedBinds.computeIfAbsent(binding.controller, c -> new ObjectOpenHashSet<>()).addAll(getBinds(binding.bind));
223225
}
224226

225227
private static Set<IBind<?>> getBinds(IBind<?> bind) {

0 commit comments

Comments
 (0)