-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathMinecraftGLSurface.java
424 lines (361 loc) · 17 KB
/
MinecraftGLSurface.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
package net.kdt.pojavlaunch;
import static net.kdt.pojavlaunch.MainActivity.touchCharInput;
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_GAMEPAD_PASSTHRU;
import static net.kdt.pojavlaunch.utils.MCOptionUtils.getMcScale;
import static org.lwjgl.glfw.CallbackBridge.sendMouseButton;
import static org.lwjgl.glfw.CallbackBridge.windowHeight;
import static org.lwjgl.glfw.CallbackBridge.windowWidth;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import net.kdt.pojavlaunch.customcontrols.ControlLayout;
import net.kdt.pojavlaunch.customcontrols.gamepad.DefaultDataProvider;
import net.kdt.pojavlaunch.customcontrols.gamepad.Gamepad;
import net.kdt.pojavlaunch.customcontrols.mouse.AbstractTouchpad;
import net.kdt.pojavlaunch.customcontrols.mouse.AndroidPointerCapture;
import net.kdt.pojavlaunch.customcontrols.mouse.InGUIEventProcessor;
import net.kdt.pojavlaunch.customcontrols.mouse.InGameEventProcessor;
import net.kdt.pojavlaunch.customcontrols.mouse.TouchEventProcessor;
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
import net.kdt.pojavlaunch.utils.JREUtils;
import net.kdt.pojavlaunch.utils.MCOptionUtils;
import org.lwjgl.glfw.CallbackBridge;
import fr.spse.gamepad_remapper.RemapperManager;
import fr.spse.gamepad_remapper.RemapperView;
/**
* Class dealing with showing minecraft surface and taking inputs to dispatch them to minecraft
*/
public class MinecraftGLSurface extends View implements GrabListener {
/* Gamepad object for gamepad inputs, instantiated on need */
private Gamepad mGamepad = null;
/* The RemapperView.Builder object allows you to set which buttons to remap */
private final RemapperManager mInputManager = new RemapperManager(getContext(), new RemapperView.Builder(null)
.remapA(true)
.remapB(true)
.remapX(true)
.remapY(true)
.remapLeftJoystick(true)
.remapRightJoystick(true)
.remapStart(true)
.remapSelect(true)
.remapLeftShoulder(true)
.remapRightShoulder(true)
.remapLeftTrigger(true)
.remapRightTrigger(true)
.remapDpad(true));
/* Sensitivity, adjusted according to screen size */
private final double mSensitivityFactor = (1.4 * (1080f/ Tools.getDisplayMetrics((Activity) getContext()).heightPixels));
/* Surface ready listener, used by the activity to launch minecraft */
SurfaceReadyListener mSurfaceReadyListener = null;
final Object mSurfaceReadyListenerLock = new Object();
/* View holding the surface, either a SurfaceView or a TextureView */
View mSurface;
private final InGameEventProcessor mIngameProcessor = new InGameEventProcessor(mSensitivityFactor);
private final InGUIEventProcessor mInGUIProcessor = new InGUIEventProcessor();
private TouchEventProcessor mCurrentTouchProcessor = mInGUIProcessor;
private AndroidPointerCapture mPointerCapture;
private boolean mLastGrabState = false;
public MinecraftGLSurface(Context context) {
this(context, null);
}
public MinecraftGLSurface(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
setFocusable(true);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void setUpPointerCapture(AbstractTouchpad touchpad) {
if(mPointerCapture != null) mPointerCapture.detach();
mPointerCapture = new AndroidPointerCapture(touchpad, this);
}
/** Initialize the view and all its settings
* @param isAlreadyRunning set to true to tell the view that the game is already running
* (only updates the window without calling the start listener)
* @param touchpad the optional cursor-emulating touchpad, used for touch event processing
* when the cursor is not grabbed
*/
public void start(boolean isAlreadyRunning, AbstractTouchpad touchpad){
if(Tools.isAndroid8OrHigher()) setUpPointerCapture(touchpad);
mInGUIProcessor.setAbstractTouchpad(touchpad);
if(LauncherPreferences.PREF_USE_ALTERNATE_SURFACE){
SurfaceView surfaceView = new SurfaceView(getContext());
mSurface = surfaceView;
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
private boolean isCalled = isAlreadyRunning;
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
if(isCalled) {
JREUtils.setupBridgeWindow(surfaceView.getHolder().getSurface());
return;
}
isCalled = true;
realStart(surfaceView.getHolder().getSurface());
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
refreshSize();
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {}
});
((ViewGroup)getParent()).addView(surfaceView);
}else{
TextureView textureView = new TextureView(getContext());
textureView.setOpaque(true);
textureView.setAlpha(1.0f);
mSurface = textureView;
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
private boolean isCalled = isAlreadyRunning;
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
Surface tSurface = new Surface(surface);
if(isCalled) {
JREUtils.setupBridgeWindow(tSurface);
return;
}
isCalled = true;
realStart(tSurface);
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {
refreshSize();
}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
return true;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {}
});
((ViewGroup)getParent()).addView(textureView);
}
}
/**
* The touch event for both grabbed an non-grabbed mouse state on the touch screen
* Does not cover the virtual mouse touchpad
*/
@Override
@SuppressWarnings("accessibility")
public boolean onTouchEvent(MotionEvent e) {
// Kinda need to send this back to the layout
if(((ControlLayout)getParent()).getModifiable()) return false;
// Looking for a mouse to handle, won't have an effect if no mouse exists.
for (int i = 0; i < e.getPointerCount(); i++) {
int toolType = e.getToolType(i);
if(toolType == MotionEvent.TOOL_TYPE_MOUSE) {
if(Tools.isAndroid8OrHigher() &&
mPointerCapture != null) {
mPointerCapture.handleAutomaticCapture();
return true;
}
}else if(toolType != MotionEvent.TOOL_TYPE_STYLUS) continue;
// Mouse found
if(CallbackBridge.isGrabbing()) return false;
CallbackBridge.sendCursorPos( e.getX(i) * LauncherPreferences.PREF_SCALE_FACTOR, e.getY(i) * LauncherPreferences.PREF_SCALE_FACTOR);
return true; //mouse event handled successfully
}
if (mIngameProcessor == null || mInGUIProcessor == null) return true;
return mCurrentTouchProcessor.processTouchEvent(e);
}
private void createGamepad(View contextView, InputDevice inputDevice) {
mGamepad = new Gamepad(contextView, inputDevice, DefaultDataProvider.INSTANCE, true);
}
/**
* The event for mouse/joystick movements
*/
@SuppressLint("NewApi")
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event) {
int mouseCursorIndex = -1;
if(Gamepad.isGamepadEvent(event) && !PREF_GAMEPAD_PASSTHRU){
if(mGamepad == null) createGamepad(this, event.getDevice());
mInputManager.handleMotionEventInput(getContext(), event, mGamepad);
return true;
}
for(int i = 0; i < event.getPointerCount(); i++) {
if(event.getToolType(i) != MotionEvent.TOOL_TYPE_MOUSE && event.getToolType(i) != MotionEvent.TOOL_TYPE_STYLUS ) continue;
// Mouse found
mouseCursorIndex = i;
break;
}
if(mouseCursorIndex == -1) return false; // we cant consoom that, theres no mice!
// Make sure we grabbed the mouse if necessary
updateGrabState(CallbackBridge.isGrabbing());
switch(event.getActionMasked()) {
case MotionEvent.ACTION_HOVER_MOVE:
CallbackBridge.mouseX = (event.getX(mouseCursorIndex) * LauncherPreferences.PREF_SCALE_FACTOR);
CallbackBridge.mouseY = (event.getY(mouseCursorIndex) * LauncherPreferences.PREF_SCALE_FACTOR);
CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY);
return true;
case MotionEvent.ACTION_SCROLL:
CallbackBridge.sendScroll((double) event.getAxisValue(MotionEvent.AXIS_HSCROLL), (double) event.getAxisValue(MotionEvent.AXIS_VSCROLL));
return true;
case MotionEvent.ACTION_BUTTON_PRESS:
return sendMouseButtonUnconverted(event.getActionButton(),true);
case MotionEvent.ACTION_BUTTON_RELEASE:
return sendMouseButtonUnconverted(event.getActionButton(),false);
default:
return false;
}
}
/** The event for keyboard/ gamepad button inputs */
public boolean processKeyEvent(KeyEvent event) {
if (PREF_GAMEPAD_PASSTHRU) {
return false;
}
//Log.i("KeyEvent", event.toString());
//Filtering useless events by order of probability
int eventKeycode = event.getKeyCode();
if(eventKeycode == KeyEvent.KEYCODE_UNKNOWN) return true;
if(eventKeycode == KeyEvent.KEYCODE_VOLUME_DOWN) return false;
if(eventKeycode == KeyEvent.KEYCODE_VOLUME_UP) return false;
if(event.getRepeatCount() != 0) return true;
int action = event.getAction();
if(action == KeyEvent.ACTION_MULTIPLE) return true;
// Ignore the cancelled up events. They occur when the user switches layouts.
// In accordance with https://developer.android.com/reference/android/view/KeyEvent#FLAG_CANCELED
if(action == KeyEvent.ACTION_UP &&
(event.getFlags() & KeyEvent.FLAG_CANCELED) != 0) return true;
//Sometimes, key events comes from SOME keys of the software keyboard
//Even weirder, is is unknown why a key or another is selected to trigger a keyEvent
if((event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) == KeyEvent.FLAG_SOFT_KEYBOARD){
if(eventKeycode == KeyEvent.KEYCODE_ENTER) return true; //We already listen to it.
touchCharInput.dispatchKeyEvent(event);
return true;
}
//Sometimes, key events may come from the mouse
if(event.getDevice() != null
&& ( (event.getSource() & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE
|| (event.getSource() & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) ){
if(eventKeycode == KeyEvent.KEYCODE_BACK){
sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, event.getAction() == KeyEvent.ACTION_DOWN);
return true;
}
}
if(Gamepad.isGamepadEvent(event)){
if(mGamepad == null) createGamepad(this, event.getDevice());
mInputManager.handleKeyEventInput(getContext(), event, mGamepad);
return true;
}
int index = EfficientAndroidLWJGLKeycode.getIndexByKey(eventKeycode);
if(EfficientAndroidLWJGLKeycode.containsIndex(index)) {
EfficientAndroidLWJGLKeycode.execKey(event, index);
return true;
}
// Some events will be generated an infinite number of times when no consumed
return (event.getFlags() & KeyEvent.FLAG_FALLBACK) == KeyEvent.FLAG_FALLBACK;
}
/** Convert the mouse button, then send it
* @return Whether the event was processed
*/
public static boolean sendMouseButtonUnconverted(int button, boolean status) {
int glfwButton = -256;
switch (button) {
case MotionEvent.BUTTON_PRIMARY:
glfwButton = LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT;
break;
case MotionEvent.BUTTON_TERTIARY:
glfwButton = LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_MIDDLE;
break;
case MotionEvent.BUTTON_SECONDARY:
glfwButton = LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT;
break;
}
if(glfwButton == -256) return false;
sendMouseButton(glfwButton, status);
return true;
}
/** Called when the size need to be set at any point during the surface lifecycle **/
public void refreshSize(){
refreshSize(false);
}
/** Same as refreshSize, but allows you to force an immediate size update **/
public void refreshSize(boolean immediate) {
if(isInLayout() && !immediate) {
post(this::refreshSize);
return;
}
// Use the width and height of the View instead of display dimensions to avoid
// getting squiched/stretched due to inconsistencies between the layout and
// screen dimensions.
windowWidth = Tools.getDisplayFriendlyRes(getWidth(), LauncherPreferences.PREF_SCALE_FACTOR);
windowHeight = Tools.getDisplayFriendlyRes(getHeight(), LauncherPreferences.PREF_SCALE_FACTOR);
if(mSurface == null){
Log.w("MGLSurface", "Attempt to refresh size on null surface");
return;
}
if(LauncherPreferences.PREF_USE_ALTERNATE_SURFACE){
SurfaceView view = (SurfaceView) mSurface;
if(view.getHolder() != null){
view.getHolder().setFixedSize(windowWidth, windowHeight);
}
}else{
TextureView view = (TextureView)mSurface;
if(view.getSurfaceTexture() != null){
view.getSurfaceTexture().setDefaultBufferSize(windowWidth, windowHeight);
}
}
CallbackBridge.sendUpdateWindowSize(windowWidth, windowHeight);
}
private void realStart(Surface surface){
// Initial size set. Request immedate refresh, otherwise the initial width and height for the game
// may be broken/unknown.
refreshSize(true);
//Load Minecraft options:
MCOptionUtils.set("fullscreen", "off");
MCOptionUtils.set("overrideWidth", String.valueOf(windowWidth));
MCOptionUtils.set("overrideHeight", String.valueOf(windowHeight));
MCOptionUtils.save();
getMcScale();
JREUtils.setupBridgeWindow(surface);
new Thread(() -> {
try {
// Wait until the listener is attached
synchronized(mSurfaceReadyListenerLock) {
if(mSurfaceReadyListener == null) mSurfaceReadyListenerLock.wait();
}
mSurfaceReadyListener.isReady();
} catch (Throwable e) {
Tools.showError(getContext(), e, true);
}
}, "JVM Main thread").start();
}
@Override
public void onGrabState(boolean isGrabbing) {
post(()->updateGrabState(isGrabbing));
}
private TouchEventProcessor pickEventProcessor(boolean isGrabbing) {
return isGrabbing ? mIngameProcessor : mInGUIProcessor;
}
private void updateGrabState(boolean isGrabbing) {
if(mLastGrabState != isGrabbing) {
mCurrentTouchProcessor.cancelPendingActions();
mCurrentTouchProcessor = pickEventProcessor(isGrabbing);
mLastGrabState = isGrabbing;
}
}
/** A small interface called when the listener is ready for the first time */
public interface SurfaceReadyListener {
void isReady();
}
public void setSurfaceReadyListener(SurfaceReadyListener listener){
synchronized (mSurfaceReadyListenerLock) {
mSurfaceReadyListener = listener;
mSurfaceReadyListenerLock.notifyAll();
}
}
}