33import com .mojang .blaze3d .Blaze3D ;
44import dev .isxander .controlify .api .ControlifyApi ;
55import dev .isxander .controlify .api .entrypoint .ControlifyEntrypoint ;
6+ import dev .isxander .controlify .bindings .ControllerBindings ;
67import dev .isxander .controlify .compatibility .ControlifyCompat ;
78import dev .isxander .controlify .controller .joystick .JoystickController ;
89import 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 () {
0 commit comments