Skip to content

Commit e413244

Browse files
committed
splitscreen: attempts at fixing focus issues (they don't work)
1 parent 7a56978 commit e413244

15 files changed

+200
-20
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package dev.isxander.controlify.splitscreen;
2+
3+
/**
4+
* Facilitates communication between a client and the controller.
5+
* <ul>
6+
* <li>On the host side, this is a thin bridge that calls directly.</li>
7+
* <li>On the remote side, this sends packets to the controller.</li>
8+
* </ul>
9+
*/
10+
public interface ControllerBridge {
11+
void giveFocusToMeIfForeground();
12+
}

splitscreen/src/main/java/dev/isxander/controlify/splitscreen/SplitscreenBootstrapper.java

+5
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ public static boolean isSplitscreen() {
5252
return controller != null || pawnConnectionListener != null;
5353
}
5454

55+
public static Optional<ControllerBridge> getControllerBridge() {
56+
return getController().<ControllerBridge>map(SplitscreenController::getControllerBridge)
57+
.or(() -> getPawn().<ControllerBridge>map(PawnConnectionListener::getControllerBridge));
58+
}
59+
5560
public static Optional<SplitscreenController> getController() {
5661
return Optional.ofNullable(controller);
5762
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.isxander.controlify.splitscreen.client;
2+
3+
import dev.isxander.controlify.splitscreen.ControllerBridge;
4+
import dev.isxander.controlify.splitscreen.protocol.controllerbound.play.ControllerboundGiveMeFocusIfForegroundPacket;
5+
import dev.isxander.controlify.splitscreen.window.manager.WindowManager;
6+
import net.minecraft.client.Minecraft;
7+
import net.minecraft.network.Connection;
8+
9+
public class RemoteControllerBridge implements ControllerBridge {
10+
11+
private final Minecraft minecraft;
12+
private final Connection connection;
13+
14+
public RemoteControllerBridge(Minecraft minecraft, Connection connection) {
15+
this.minecraft = minecraft;
16+
this.connection = connection;
17+
}
18+
19+
@Override
20+
public void giveFocusToMeIfForeground() {
21+
this.connection.send(new ControllerboundGiveMeFocusIfForegroundPacket(
22+
WindowManager.get().getNativeWindowHandle(
23+
this.minecraft.getWindow().getWindow()
24+
)
25+
));
26+
}
27+
}

splitscreen/src/main/java/dev/isxander/controlify/splitscreen/client/protocol/ControllerHandshakePacketListener.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import dev.isxander.controlify.splitscreen.protocol.pawnbound.common.PawnboundDisconnectPacket;
66
import dev.isxander.controlify.splitscreen.server.protocol.ControllerPlayPacketListener;
77
import dev.isxander.controlify.splitscreen.protocol.PlayProtocols;
8+
import net.minecraft.client.Minecraft;
89
import net.minecraft.network.Connection;
910
import net.minecraft.network.ConnectionProtocol;
1011
import net.minecraft.network.DisconnectionDetails;
@@ -14,10 +15,12 @@
1415
public class ControllerHandshakePacketListener implements ControllerboundCommonPacketListener, ServerPacketListener {
1516
private final SplitscreenController controller;
1617
private final Connection connection;
18+
private final Minecraft minecraft;
1719

18-
public ControllerHandshakePacketListener(SplitscreenController controller, Connection connection) {
20+
public ControllerHandshakePacketListener(SplitscreenController controller, Connection connection, Minecraft minecraft) {
1921
this.controller = controller;
2022
this.connection = connection;
23+
this.minecraft = minecraft;
2124
}
2225

2326
public void handleHandshake(ControllerboundHandshakePacket packet) {
@@ -29,7 +32,7 @@ public void handleHandshake(ControllerboundHandshakePacket packet) {
2932
return;
3033
}
3134

32-
this.connection.setupInboundProtocol(PlayProtocols.CONTROLLERBOUND, new ControllerPlayPacketListener(controller, connection));
35+
this.connection.setupInboundProtocol(PlayProtocols.CONTROLLERBOUND, new ControllerPlayPacketListener(controller, connection, minecraft));
3336
}
3437

3538
@Override

splitscreen/src/main/java/dev/isxander/controlify/splitscreen/client/protocol/PawnConnectionListener.java

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.google.common.util.concurrent.ThreadFactoryBuilder;
55
import com.mojang.logging.LogUtils;
66
import dev.isxander.controlify.splitscreen.SocketConnectionMethod;
7+
import dev.isxander.controlify.splitscreen.client.RemoteControllerBridge;
78
import dev.isxander.controlify.splitscreen.protocol.ConnectionUtils;
89
import dev.isxander.controlify.splitscreen.protocol.controllerbound.handshake.ControllerboundHandshakePacket;
910
import dev.isxander.controlify.splitscreen.protocol.HandshakeProtocols;
@@ -41,17 +42,24 @@ public class PawnConnectionListener {
4142

4243
private final Connection controllerConnection;
4344

45+
private final RemoteControllerBridge controllerBridge;
46+
4447
public PawnConnectionListener(Minecraft minecraft, SocketConnectionMethod connectionMethod) {
4548
this.controllerConnection = switch (connectionMethod) {
4649
case SocketConnectionMethod.TCP(int port) -> connectToTcp(port, minecraft);
4750
case SocketConnectionMethod.Unix(String socketPath) -> connectToUnixSocket(socketPath, minecraft);
4851
};
52+
this.controllerBridge = new RemoteControllerBridge(minecraft, this.controllerConnection);
4953
}
5054

5155
public Connection getControllerConnection() {
5256
return controllerConnection;
5357
}
5458

59+
public RemoteControllerBridge getControllerBridge() {
60+
return controllerBridge;
61+
}
62+
5563
private Connection connectToUnixSocket(String socketPath, Minecraft minecraft) {
5664
LOGGER.info("Connecting to controller unix socket at {}", socketPath);
5765

splitscreen/src/main/java/dev/isxander/controlify/splitscreen/mixins/core/WindowMixin.java

+22-10
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
44
import com.mojang.blaze3d.platform.Window;
55
import com.mojang.blaze3d.platform.WindowEventHandler;
6+
import dev.isxander.controlify.splitscreen.ControllerBridge;
67
import dev.isxander.controlify.splitscreen.SplitscreenBootstrapper;
8+
import dev.isxander.controlify.splitscreen.window.manager.WindowManager;
79
import org.lwjgl.glfw.GLFWImage;
810
import org.slf4j.Logger;
911
import org.spongepowered.asm.mixin.Final;
@@ -19,6 +21,9 @@
1921
public class WindowMixin {
2022
@Shadow @Final private static Logger LOGGER;
2123

24+
@Shadow
25+
@Final
26+
private long window;
2227
@Unique private boolean hasDoneInitialSetup = false;
2328

2429
@Inject(method = "<init>", at = @At("RETURN"))
@@ -45,16 +50,23 @@ private void propagateTitleToParent(String title, CallbackInfo ci) {
4550
});
4651
}
4752

48-
/**
49-
* GLFW does not correctly report the window focus state when the window is a child.
50-
* Instead, the controller will propagate this event from the parent window.
51-
* @param instance receiver
52-
* @param glfwReportedFocus the incorrect focus state reported by GLFW
53-
* @return if the focus state should be set by GLFW child window
54-
*/
55-
@WrapWithCondition(method = "onFocus", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/WindowEventHandler;setWindowActive(Z)V"))
56-
private boolean dontListenToChildWindowForFocus(WindowEventHandler instance, boolean glfwReportedFocus) {
57-
return !SplitscreenBootstrapper.isSplitscreen();
53+
// /**
54+
// * GLFW does not correctly report the window focus state when the window is a child.
55+
// * Instead, the controller will propagate this event from the parent window.
56+
// * @param instance receiver
57+
// * @param glfwReportedFocus the incorrect focus state reported by GLFW
58+
// * @return if the focus state should be set by GLFW child window
59+
// */
60+
// @WrapWithCondition(method = "onFocus", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/WindowEventHandler;setWindowActive(Z)V"))
61+
// private boolean dontListenToChildWindowForFocus(WindowEventHandler instance, boolean glfwReportedFocus) {
62+
// return !SplitscreenBootstrapper.isSplitscreen();
63+
// }
64+
65+
@Inject(method = "onEnter", at = @At("HEAD"))
66+
private void giveSelfFocusIfForeground(long window, boolean cursorEntered, CallbackInfo ci) {
67+
if (window == this.window && cursorEntered) {
68+
SplitscreenBootstrapper.getControllerBridge().ifPresent(ControllerBridge::giveFocusToMeIfForeground);
69+
}
5870
}
5971

6072
@Inject(

splitscreen/src/main/java/dev/isxander/controlify/splitscreen/protocol/PlayProtocols.java

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public final class PlayProtocols {
1717
builder -> CommonProtocols.addControllerboundPackets(builder)
1818
.addPacket(ControllerboundHelloPacket.TYPE, ControllerboundHelloPacket.CODEC)
1919
.addPacket(ControllerboundKeepAlivePacket.TYPE, ControllerboundKeepAlivePacket.CODEC)
20+
.addPacket(ControllerboundGiveMeFocusIfForegroundPacket.TYPE, ControllerboundGiveMeFocusIfForegroundPacket.CODEC)
2021
).bind(FriendlyByteBuf::new);
2122

2223
public static final ProtocolInfo<PawnPlayPacketListener> PAWNBOUND =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package dev.isxander.controlify.splitscreen.protocol.controllerbound.play;
2+
3+
import dev.isxander.controlify.splitscreen.server.protocol.ControllerPlayPacketListener;
4+
import dev.isxander.controlify.splitscreen.window.manager.NativeWindowHandle;
5+
import io.netty.buffer.ByteBuf;
6+
import net.minecraft.network.codec.StreamCodec;
7+
import net.minecraft.network.protocol.PacketType;
8+
9+
public record ControllerboundGiveMeFocusIfForegroundPacket(NativeWindowHandle childWindow) implements ControllerboundPlayPacket {
10+
public static final StreamCodec<ByteBuf, ControllerboundGiveMeFocusIfForegroundPacket> CODEC =
11+
NativeWindowHandle.STREAM_CODEC.map(ControllerboundGiveMeFocusIfForegroundPacket::new, ControllerboundGiveMeFocusIfForegroundPacket::childWindow);
12+
public static final PacketType<ControllerboundGiveMeFocusIfForegroundPacket> TYPE =
13+
ControllerboundPlayPacket.createType("give_me_focus_if_foreground");
14+
15+
@Override
16+
public void handle(ControllerPlayPacketListener handler) {
17+
handler.handleGiveChildFocusIfForeground(this);
18+
}
19+
20+
@Override
21+
public PacketType<ControllerboundGiveMeFocusIfForegroundPacket> type() {
22+
return TYPE;
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package dev.isxander.controlify.splitscreen.server;
2+
3+
import dev.isxander.controlify.splitscreen.ControllerBridge;
4+
import dev.isxander.controlify.splitscreen.SplitscreenPawn;
5+
import dev.isxander.controlify.splitscreen.window.manager.NativeWindowHandle;
6+
import dev.isxander.controlify.splitscreen.window.manager.WindowManager;
7+
import net.minecraft.client.Minecraft;
8+
9+
public class LocalControllerBridge implements ControllerBridge {
10+
11+
private final Minecraft minecraft;
12+
private final SplitscreenController controller;
13+
14+
public LocalControllerBridge(Minecraft minecraft, SplitscreenController controller) {
15+
this.minecraft = minecraft;
16+
this.controller = controller;
17+
}
18+
19+
@Override
20+
public void giveFocusToMeIfForeground() {
21+
this.giveFocusToChildIfForeground(
22+
WindowManager.get().getNativeWindowHandle(
23+
minecraft.getWindow().getWindow()
24+
),
25+
controller.getLocalPawn()
26+
);
27+
}
28+
29+
public void giveFocusToChildIfForeground(NativeWindowHandle childWindow, SplitscreenPawn childPawn) {
30+
long glfwParentWindowHandle = controller.getParentWindow().getGlfwWindowHandle();
31+
NativeWindowHandle nativeParentWindowHandle = WindowManager.get().getNativeWindowHandle(glfwParentWindowHandle);
32+
33+
if (WindowManager.get().giveChildFocusIfParentIsForeground(nativeParentWindowHandle, childWindow)) {
34+
this.controller.forEachPawn(pawn -> {
35+
pawn.setWindowFocusState(pawn == childPawn);
36+
});
37+
}
38+
}
39+
}

splitscreen/src/main/java/dev/isxander/controlify/splitscreen/server/SplitscreenController.java

+15-3
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,19 @@ public class SplitscreenController implements ParentWindowEventHandler {
3232

3333
private final List<SplitscreenPawn> pawns = new ArrayList<>();
3434
private final ControllerConnectionListener connectionListener;
35+
private final LocalSplitscreenPawn localPawn;
36+
37+
private final LocalControllerBridge controllerBridge;
3538

3639
private @Nullable ParentWindow parentWindow;
3740
private boolean isWindowReady = false;
3841
private final Queue<Runnable> waitingForWindowTasks = new ArrayDeque<>();
3942

4043
public SplitscreenController(Minecraft minecraft, SocketConnectionMethod connectionMethod) {
4144
this.minecraft = minecraft;
42-
this.connectionListener = new ControllerConnectionListener(connectionMethod, this);
43-
this.addPawn(new LocalSplitscreenPawn(minecraft)); // control ourselves as a pawn
45+
this.controllerBridge = new LocalControllerBridge(minecraft, this);
46+
this.connectionListener = new ControllerConnectionListener(connectionMethod, this, minecraft);
47+
this.addPawn(this.localPawn = new LocalSplitscreenPawn(minecraft)); // control ourselves as a pawn
4448
}
4549

4650
public void forEachPawn(Consumer<SplitscreenPawn> consumer) {
@@ -73,6 +77,14 @@ public void removePawn(SplitscreenPawn pawn) {
7377
this.pawns.remove(pawn);
7478
}
7579

80+
public LocalSplitscreenPawn getLocalPawn() {
81+
return this.localPawn;
82+
}
83+
84+
public LocalControllerBridge getControllerBridge() {
85+
return this.controllerBridge;
86+
}
87+
7688
public @Nullable ParentWindow getParentWindow() {
7789
return this.parentWindow;
7890
}
@@ -104,7 +116,7 @@ public void onResizeParentWindow(int width, int height) {
104116

105117
@Override
106118
public void onFocusParentWindow(boolean focused) {
107-
this.forEachPawn(pawn -> pawn.setWindowFocusState(focused));
119+
108120
}
109121

110122
public void negotiateSplitscreen() {

splitscreen/src/main/java/dev/isxander/controlify/splitscreen/server/protocol/ControllerConnectionListener.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.netty.channel.socket.nio.NioServerSocketChannel;
2121
import io.netty.channel.unix.DomainSocketAddress;
2222
import io.netty.handler.timeout.ReadTimeoutHandler;
23+
import net.minecraft.client.Minecraft;
2324
import net.minecraft.network.Connection;
2425
import net.minecraft.network.PacketSendListener;
2526
import net.minecraft.network.chat.Component;
@@ -48,10 +49,13 @@ public class ControllerConnectionListener {
4849
private final List<ChannelFuture> channels = Collections.synchronizedList(new ArrayList<>());
4950
private final List<Connection> connections = Collections.synchronizedList(new ArrayList<>());
5051

52+
private final Minecraft minecraft;
53+
5154
private volatile boolean running;
5255

53-
public ControllerConnectionListener(SocketConnectionMethod connectionMethod, SplitscreenController controller) {
56+
public ControllerConnectionListener(SocketConnectionMethod connectionMethod, SplitscreenController controller, Minecraft minecraft) {
5457
this.running = true;
58+
this.minecraft = minecraft;
5559
switch (connectionMethod) {
5660
case SocketConnectionMethod.TCP(int port) -> startTcpListener(port, controller);
5761
case SocketConnectionMethod.Unix(String socketPath) -> startUnixListener(socketPath, controller);
@@ -92,7 +96,7 @@ protected void initChannel(Channel ch) {
9296
Connection connection = new Connection(PacketFlow.SERVERBOUND);
9397
ControllerConnectionListener.this.connections.add(connection);
9498
connection.configurePacketHandler(pipeline);
95-
connection.setListenerForServerboundHandshake(new ControllerHandshakePacketListener(controller, connection));
99+
connection.setListenerForServerboundHandshake(new ControllerHandshakePacketListener(controller, connection, minecraft));
96100
LOGGER.info("Established connection with {}", ch.remoteAddress());
97101
}
98102
});

splitscreen/src/main/java/dev/isxander/controlify/splitscreen/server/protocol/ControllerPlayPacketListener.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
import com.mojang.logging.LogUtils;
44
import dev.isxander.controlify.splitscreen.SplitscreenPawn;
5+
import dev.isxander.controlify.splitscreen.protocol.controllerbound.play.ControllerboundGiveMeFocusIfForegroundPacket;
56
import dev.isxander.controlify.splitscreen.protocol.controllerbound.play.ControllerboundHelloPacket;
67
import dev.isxander.controlify.splitscreen.protocol.controllerbound.play.ControllerboundKeepAlivePacket;
8+
import dev.isxander.controlify.splitscreen.server.LocalControllerBridge;
79
import dev.isxander.controlify.splitscreen.server.RemoteSplitscreenPawn;
810
import dev.isxander.controlify.splitscreen.server.SplitscreenController;
911
import dev.isxander.controlify.splitscreen.client.protocol.ControllerboundCommonPacketListener;
1012
import dev.isxander.controlify.splitscreen.protocol.pawnbound.play.PawnboundKeepAlivePacket;
13+
import net.minecraft.client.Minecraft;
1114
import net.minecraft.network.Connection;
1215
import net.minecraft.network.ConnectionProtocol;
1316
import net.minecraft.network.DisconnectionDetails;
@@ -20,10 +23,12 @@ public class ControllerPlayPacketListener implements ControllerboundCommonPacket
2023
private final SplitscreenController controller;
2124
private SplitscreenPawn pawnInstance;
2225
private final Connection connection;
26+
private final Minecraft minecraft;
2327

24-
public ControllerPlayPacketListener(SplitscreenController controller, Connection connection) {
28+
public ControllerPlayPacketListener(SplitscreenController controller, Connection connection, Minecraft minecraft) {
2529
this.controller = controller;
2630
this.connection = connection;
31+
this.minecraft = minecraft;
2732
}
2833

2934
public void handleHello(ControllerboundHelloPacket packet) {
@@ -32,9 +37,12 @@ public void handleHello(ControllerboundHelloPacket packet) {
3237

3338
// initiate keep alive chain
3439
this.connection.send(PawnboundKeepAlivePacket.INSTANCE);
40+
}
3541

36-
// schedule window parenting
37-
42+
public void handleGiveChildFocusIfForeground(ControllerboundGiveMeFocusIfForegroundPacket packet) {
43+
this.minecraft.execute(() -> {
44+
this.controller.getControllerBridge().giveFocusToChildIfForeground(packet.childWindow(), this.pawnInstance);
45+
});
3846
}
3947

4048
public void handleKeepAlive(ControllerboundKeepAlivePacket packet) {

splitscreen/src/main/java/dev/isxander/controlify/splitscreen/window/manager/Win32WindowManager.java

+17
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,21 @@ public void embedThisWindow(NativeWindowHandle parentHandle, int x, int y, int w
3434
USER32.SetWindowLong(childHwnd, GWL_STYLE, style);
3535
USER32.SetWindowPos(childHwnd, null, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
3636
}
37+
38+
@Override
39+
public boolean giveChildFocusIfParentIsForeground(NativeWindowHandle parentHandle, NativeWindowHandle childHandle) {
40+
HWND childHwnd = new HWND(new Pointer(childHandle.handle()));
41+
HWND parentHwnd = new HWND(new Pointer(parentHandle.handle()));
42+
43+
HWND foregroundHwnd = USER32.GetForegroundWindow();
44+
System.out.println("Parent window: " + parentHwnd);
45+
System.out.println("Foreground window: " + foregroundHwnd);
46+
System.out.println("Child window: " + childHwnd);
47+
if (foregroundHwnd.equals(parentHwnd)) {
48+
System.out.println("Parent is foreground, setting focus to child");
49+
USER32.SetFocus(childHwnd);
50+
return true;
51+
}
52+
return false;
53+
}
3754
}

splitscreen/src/main/java/dev/isxander/controlify/splitscreen/window/manager/WindowManager.java

+2
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,6 @@ static WindowManager get() {
4040
* @param height height of the child
4141
*/
4242
void embedThisWindow(NativeWindowHandle parentHandle, int x, int y, int width, int height);
43+
44+
boolean giveChildFocusIfParentIsForeground(NativeWindowHandle parentHandle, NativeWindowHandle childHandle);
4345
}

0 commit comments

Comments
 (0)