diff --git a/build.gradle b/build.gradle index 4709e3c0..91b47a9a 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ android { } build { - finalizedBy(':wrapper:launcher:build') + //finalizedBy(':wrapper:launcher:build') } dependencies { @@ -48,6 +48,8 @@ dependencies { implementation("commons-io:commons-io:2.13.0") implementation("commons-codec:commons-codec:1.15") implementation("androidx.annotation:annotation:1.7.1") + implementation("androidx.core:core:1.13.1") implementation("com.microsoft.azure:msal4j:1.14.0") + implementation("com.github.Mathias-Boulay:android_gamepad_remapper:2.0.3") implementation("blank:unity-classes") } diff --git a/settings.gradle b/settings.gradle index 7d680692..855e144e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,7 +20,7 @@ dependencyResolutionManagement { google() mavenCentral() maven { - url = uri("https://repo.u-team.info") + url = uri("https://jitpack.io") } flatDir { dirs("libs") @@ -30,5 +30,5 @@ dependencyResolutionManagement { } rootProject.name = "Pojlib" -include ':wrapper', ':wrapper:launcher', "wrapper:unityLibrary", "wrapper:unityLibrary:xrmanifest.androidlib", ":jre_lwjgl3glfw" +include ":jre_lwjgl3glfw"//, ':wrapper', ':wrapper:launcher', "wrapper:unityLibrary", "wrapper:unityLibrary:xrmanifest.androidlib" diff --git a/src/main/assets/lwjgl/version b/src/main/assets/lwjgl/version index 6bb4e0bd..64a7f3db 100644 --- a/src/main/assets/lwjgl/version +++ b/src/main/assets/lwjgl/version @@ -1 +1 @@ -1725902304131 \ No newline at end of file +1725917740464 \ No newline at end of file diff --git a/src/main/java/pojlib/API.java b/src/main/java/pojlib/API.java index 0bd01921..8297c7db 100644 --- a/src/main/java/pojlib/API.java +++ b/src/main/java/pojlib/API.java @@ -36,6 +36,7 @@ public class API { public static String memoryValue = "1800"; public static boolean developerMods; public static MinecraftAccount currentAcc; + public static MinecraftInstances.Instance currentInstance; public static boolean advancedDebugger; diff --git a/src/main/java/pojlib/InstanceHandler.java b/src/main/java/pojlib/InstanceHandler.java index e3596aab..cd09213a 100644 --- a/src/main/java/pojlib/InstanceHandler.java +++ b/src/main/java/pojlib/InstanceHandler.java @@ -288,6 +288,7 @@ public static boolean delete(MinecraftInstances instances, MinecraftInstances.In public static void launchInstance(Activity activity, MinecraftAccount account, MinecraftInstances.Instance instance) { try { + API.currentInstance = instance; JREUtils.redirectAndPrintJRELog(); VLoader.setAndroidInitInfo(activity); JREUtils.launchJavaVM(activity, instance.generateLaunchArgs(account), instance); diff --git a/src/main/java/pojlib/UnityPlayerActivity.java b/src/main/java/pojlib/UnityPlayerActivity.java index 25c1edb4..a338a497 100644 --- a/src/main/java/pojlib/UnityPlayerActivity.java +++ b/src/main/java/pojlib/UnityPlayerActivity.java @@ -1,11 +1,16 @@ package pojlib; +import static android.os.Build.VERSION.SDK_INT; + +import android.app.Activity; import android.app.ActivityGroup; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Intent; import android.content.res.Configuration; +import android.os.Build; import android.os.Bundle; +import android.util.DisplayMetrics; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Window; @@ -13,6 +18,8 @@ import com.unity3d.player.IUnityPlayerLifecycleEvents; import com.unity3d.player.UnityPlayer; +import org.lwjgl.glfw.CallbackBridge; + import java.io.File; import pojlib.input.AWTInputBridge; @@ -60,10 +67,47 @@ protected String updateUnityCommandLineArguments(String cmdLine) FileUtil.unzipArchiveFromAsset(this, "JRE-22.zip", this.getFilesDir() + "/runtimes/JRE-22"); } + updateWindowSize(this); GLOBAL_CLIPBOARD = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); } + public static DisplayMetrics getDisplayMetrics(Activity activity) { + DisplayMetrics displayMetrics = new DisplayMetrics(); + + if(activity.isInMultiWindowMode() || activity.isInPictureInPictureMode()){ + //For devices with free form/split screen, we need window size, not screen size. + displayMetrics = activity.getResources().getDisplayMetrics(); + }else{ + if (SDK_INT >= Build.VERSION_CODES.R) { + activity.getDisplay().getRealMetrics(displayMetrics); + } else { // Removed the clause for devices with unofficial notch support, since it also ruins all devices with virtual nav bars before P + activity.getWindowManager().getDefaultDisplay().getRealMetrics(displayMetrics); + } + } + currentDisplayMetrics = displayMetrics; + return displayMetrics; + } + + public static DisplayMetrics currentDisplayMetrics; + + public static void updateWindowSize(Activity activity) { + currentDisplayMetrics = getDisplayMetrics(activity); + + CallbackBridge.physicalWidth = currentDisplayMetrics.widthPixels; + CallbackBridge.physicalHeight = currentDisplayMetrics.heightPixels; + } + + public static float dpToPx(float dp) { + //Better hope for the currentDisplayMetrics to be good + return dp * currentDisplayMetrics.density; + } + + public static float pxToDp(float px){ + //Better hope for the currentDisplayMetrics to be good + return px / currentDisplayMetrics.density; + } + public static void querySystemClipboard() { ClipData clipData = GLOBAL_CLIPBOARD.getPrimaryClip(); if(clipData == null) { diff --git a/src/main/java/pojlib/input/gamepad/DefaultDataProvider.java b/src/main/java/pojlib/input/gamepad/DefaultDataProvider.java new file mode 100644 index 00000000..62c18d89 --- /dev/null +++ b/src/main/java/pojlib/input/gamepad/DefaultDataProvider.java @@ -0,0 +1,33 @@ +package pojlib.input.gamepad; + +import pojlib.input.GrabListener; + +import org.lwjgl.glfw.CallbackBridge; + +public class DefaultDataProvider implements GamepadDataProvider { + public static final DefaultDataProvider INSTANCE = new DefaultDataProvider(); + + // Cannot instantiate this class publicly + private DefaultDataProvider() {} + + @Override + public GamepadMap getGameMap() { + return GamepadMapStore.getGameMap(); + } + + + @Override + public GamepadMap getMenuMap() { + return GamepadMapStore.getMenuMap(); + } + + @Override + public boolean isGrabbing() { + return CallbackBridge.isGrabbing(); + } + + @Override + public void attachGrabListener(GrabListener grabListener) { + CallbackBridge.addGrabListener(grabListener); + } +} diff --git a/src/main/java/pojlib/input/gamepad/Gamepad.java b/src/main/java/pojlib/input/gamepad/Gamepad.java new file mode 100644 index 00000000..17410a8e --- /dev/null +++ b/src/main/java/pojlib/input/gamepad/Gamepad.java @@ -0,0 +1,453 @@ +package pojlib.input.gamepad; + +import static android.view.MotionEvent.AXIS_HAT_X; +import static android.view.MotionEvent.AXIS_HAT_Y; +import static android.view.MotionEvent.AXIS_LTRIGGER; +import static android.view.MotionEvent.AXIS_RTRIGGER; +import static android.view.MotionEvent.AXIS_RZ; +import static android.view.MotionEvent.AXIS_X; +import static android.view.MotionEvent.AXIS_Y; +import static android.view.MotionEvent.AXIS_Z; + +import android.content.Context; +import android.view.Choreographer; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import org.lwjgl.glfw.CallbackBridge; + +import static org.lwjgl.glfw.CallbackBridge.sendKeyPress; +import static org.lwjgl.glfw.CallbackBridge.sendMouseButton; + +import static pojlib.UnityPlayerActivity.currentDisplayMetrics; +import static pojlib.input.gamepad.GamepadJoystick.DIRECTION_EAST; +import static pojlib.input.gamepad.GamepadJoystick.DIRECTION_NONE; +import static pojlib.input.gamepad.GamepadJoystick.DIRECTION_NORTH; +import static pojlib.input.gamepad.GamepadJoystick.DIRECTION_NORTH_EAST; +import static pojlib.input.gamepad.GamepadJoystick.DIRECTION_NORTH_WEST; +import static pojlib.input.gamepad.GamepadJoystick.DIRECTION_SOUTH; +import static pojlib.input.gamepad.GamepadJoystick.DIRECTION_SOUTH_EAST; +import static pojlib.input.gamepad.GamepadJoystick.DIRECTION_SOUTH_WEST; +import static pojlib.input.gamepad.GamepadJoystick.DIRECTION_WEST; +import static pojlib.input.gamepad.GamepadJoystick.isJoystickEvent; +import static pojlib.util.MCOptionUtils.getMcScale; + +import androidx.core.content.res.ResourcesCompat; +import androidx.core.math.MathUtils; + +import fr.spse.gamepad_remapper.GamepadHandler; +import fr.spse.gamepad_remapper.Settings; +import pojlib.android.R; +import pojlib.input.GrabListener; +import pojlib.input.LwjglGlfwKeycode; +import pojlib.util.MCOptionUtils; + +public class Gamepad implements GrabListener, GamepadHandler { + + /* Resolution scaler option, allow downsizing a window */ + private final float mScaleFactor = 0; // LauncherPreferences.DEFAULT_PREF.getInt("resolutionRatio",100)/100f; + + /* Sensitivity, adjusted according to screen size */ + private final double mSensitivityFactor = (1.4 * (1080f/ currentDisplayMetrics.heightPixels)); + private final ImageView mPointerImageView; + + private final GamepadJoystick mLeftJoystick; + private int mCurrentJoystickDirection = DIRECTION_NONE; + + private final GamepadJoystick mRightJoystick; + private float mLastHorizontalValue = 0.0f; + private float mLastVerticalValue = 0.0f; + + private static final double MOUSE_MAX_ACCELERATION = 2f; + + private double mMouseMagnitude; + private double mMouseAngle; + private double mMouseSensitivity = 19; + + private GamepadMap mGameMap; + private GamepadMap mMenuMap; + private GamepadMap mCurrentMap; + + private boolean isGrabbing; + + + /* Choreographer with time to compute delta on ticking */ + private final Choreographer mScreenChoreographer; + private long mLastFrameTime; + + /* Listen for change in gui scale */ + @SuppressWarnings("FieldCanBeLocal") //the field is used in a WeakReference + private final MCOptionUtils.MCOptionListener mGuiScaleListener = () -> notifyGUISizeChange(getMcScale()); + + private final GamepadDataProvider mMapProvider; + + public Gamepad(View contextView, InputDevice inputDevice, GamepadDataProvider mapProvider, boolean showCursor){ + Settings.setDeadzoneScale(100f); + + mScreenChoreographer = Choreographer.getInstance(); + Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + tick(frameTimeNanos); + mScreenChoreographer.postFrameCallback(this); + } + }; + mScreenChoreographer.postFrameCallback(frameCallback); + mLastFrameTime = System.nanoTime(); + + /* Add the listener for the cross hair */ + MCOptionUtils.addMCOptionListener(mGuiScaleListener); + + mLeftJoystick = new GamepadJoystick(AXIS_X, AXIS_Y, inputDevice); + mRightJoystick = new GamepadJoystick(AXIS_Z, AXIS_RZ, inputDevice); + + + Context ctx = contextView.getContext(); + mPointerImageView = new ImageView(contextView.getContext()); + mPointerImageView.setImageDrawable(ResourcesCompat.getDrawable(ctx.getResources(), R.drawable.ic_gamepad_pointer, ctx.getTheme())); + mPointerImageView.getDrawable().setFilterBitmap(false); + + int size = (int) ((22 * getMcScale()) / mScaleFactor); + mPointerImageView.setLayoutParams(new FrameLayout.LayoutParams(size, size)); + + mMapProvider = mapProvider; + + CallbackBridge.sendCursorPos(CallbackBridge.windowWidth/2f, CallbackBridge.windowHeight/2f); + + if(showCursor) { + ((ViewGroup)contextView.getParent()).addView(mPointerImageView); + } + + placePointerView(CallbackBridge.physicalWidth/2, CallbackBridge.physicalHeight/2); + + reloadGamepadMaps(); + mMapProvider.attachGrabListener(this); + } + + + public void reloadGamepadMaps() { + if(mGameMap != null) mGameMap.resetPressedState(); + if(mMenuMap != null) mMenuMap.resetPressedState(); + GamepadMapStore.load(); + mGameMap = mMapProvider.getGameMap(); + mMenuMap = mMapProvider.getMenuMap(); + mCurrentMap = mGameMap; + // Force state refresh + boolean currentGrab = CallbackBridge.isGrabbing(); + isGrabbing = !currentGrab; + onGrabState(currentGrab); + } + + public void updateJoysticks(){ + updateDirectionalJoystick(); + updateMouseJoystick(); + } + + public void notifyGUISizeChange(int newSize){ + //Change the pointer size to match UI + int size = (int) ((22 * newSize) / mScaleFactor); + mPointerImageView.post(() -> mPointerImageView.setLayoutParams(new FrameLayout.LayoutParams(size, size))); + + } + + + public static void sendInput(short[] keycodes, boolean isDown){ + for(short keycode : keycodes){ + switch (keycode){ + case GamepadMap.MOUSE_SCROLL_DOWN: + if(isDown) CallbackBridge.sendScroll(0, -1); + break; + case GamepadMap.MOUSE_SCROLL_UP: + if(isDown) CallbackBridge.sendScroll(0, 1); + break; + case GamepadMap.MOUSE_LEFT: + sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, isDown); + break; + case GamepadMap.MOUSE_MIDDLE: + sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_MIDDLE, isDown); + break; + case GamepadMap.MOUSE_RIGHT: + sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, isDown); + break; + case GamepadMap.UNSPECIFIED: + break; + + default: + sendKeyPress(keycode, CallbackBridge.getCurrentMods(), isDown); + CallbackBridge.setModifiers(keycode, isDown); + break; + } + } + + } + + public static boolean isGamepadEvent(MotionEvent event){ + return isJoystickEvent(event); + } + + public static boolean isGamepadEvent(KeyEvent event){ + boolean isGamepad = ((event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) + || ((event.getDevice() != null) && ((event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)); + + return isGamepad && GamepadDpad.isDpadEvent(event); + } + + /** + * Send the new mouse position, computing the delta + * @param frameTimeNanos The time to render the frame, used to compute mouse delta + */ + private void tick(long frameTimeNanos){ + //update mouse position + long newFrameTime = System.nanoTime(); + if(mLastHorizontalValue != 0 || mLastVerticalValue != 0){ + + double acceleration = Math.pow(mMouseMagnitude, MOUSE_MAX_ACCELERATION); + if(acceleration > 1) acceleration = 1; + + // Compute delta since last tick time + float deltaX = (float) (Math.cos(mMouseAngle) * acceleration * mMouseSensitivity); + float deltaY = (float) (Math.sin(mMouseAngle) * acceleration * mMouseSensitivity); + newFrameTime = System.nanoTime(); // More accurate delta + float deltaTimeScale = ((newFrameTime - mLastFrameTime) / 16666666f); // Scale of 1 = 60Hz + deltaX *= deltaTimeScale; + deltaY *= deltaTimeScale; + + CallbackBridge.mouseX += deltaX; + CallbackBridge.mouseY -= deltaY; + + if(!isGrabbing){ + CallbackBridge.mouseX = MathUtils.clamp(CallbackBridge.mouseX, 0, CallbackBridge.windowWidth); + CallbackBridge.mouseY = MathUtils.clamp(CallbackBridge.mouseY, 0, CallbackBridge.windowHeight); + placePointerView((int) (CallbackBridge.mouseX / mScaleFactor), (int) (CallbackBridge.mouseY/ mScaleFactor)); + } + + //Send the mouse to the game + CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY); + } + + // Update last nano time + mLastFrameTime = newFrameTime; + } + + private void updateMouseJoystick(){ + GamepadJoystick currentJoystick = isGrabbing ? mRightJoystick : mLeftJoystick; + float horizontalValue = currentJoystick.getHorizontalAxis(); + float verticalValue = currentJoystick.getVerticalAxis(); + if(horizontalValue != mLastHorizontalValue || verticalValue != mLastVerticalValue){ + mLastHorizontalValue = horizontalValue; + mLastVerticalValue = verticalValue; + + mMouseMagnitude = currentJoystick.getMagnitude(); + mMouseAngle = currentJoystick.getAngleRadian(); + + tick(System.nanoTime()); + return; + } + mLastHorizontalValue = horizontalValue; + mLastVerticalValue = verticalValue; + + mMouseMagnitude = currentJoystick.getMagnitude(); + mMouseAngle = currentJoystick.getAngleRadian(); + + } + + private void updateDirectionalJoystick(){ + GamepadJoystick currentJoystick = isGrabbing ? mLeftJoystick : mRightJoystick; + + int lastJoystickDirection = mCurrentJoystickDirection; + mCurrentJoystickDirection = currentJoystick.getHeightDirection(); + + if(mCurrentJoystickDirection == lastJoystickDirection) return; + + sendDirectionalKeycode(lastJoystickDirection, false, getCurrentMap()); + sendDirectionalKeycode(mCurrentJoystickDirection, true, getCurrentMap()); + } + + + private GamepadMap getCurrentMap(){ + return mCurrentMap; + } + + private static void sendDirectionalKeycode(int direction, boolean isDown, GamepadMap map){ + switch (direction){ + case DIRECTION_NORTH: + map.DIRECTION_FORWARD.update(isDown); + break; + case DIRECTION_NORTH_EAST: + map.DIRECTION_FORWARD.update(isDown); + map.DIRECTION_RIGHT.update(isDown); + break; + case DIRECTION_EAST: + map.DIRECTION_RIGHT.update(isDown); + break; + case DIRECTION_SOUTH_EAST: + map.DIRECTION_RIGHT.update(isDown); + map.DIRECTION_BACKWARD.update(isDown); + break; + case DIRECTION_SOUTH: + map.DIRECTION_BACKWARD.update(isDown); + break; + case DIRECTION_SOUTH_WEST: + map.DIRECTION_BACKWARD.update(isDown); + map.DIRECTION_LEFT.update(isDown); + break; + case DIRECTION_WEST: + map.DIRECTION_LEFT.update(isDown); + break; + case DIRECTION_NORTH_WEST: + map.DIRECTION_FORWARD.update(isDown); + map.DIRECTION_LEFT.update(isDown); + break; + } + } + + /** Place the pointer on the screen, offsetting the image size */ + private void placePointerView(int x, int y){ + mPointerImageView.setX(x - mPointerImageView.getWidth()/2f); + mPointerImageView.setY(y - mPointerImageView.getHeight()/2f); + } + + /** Update the grabbing state, and change the currentMap, mouse position and sensibility */ + @Override + public void onGrabState(boolean isGrabbing) { + boolean lastGrabbingValue = this.isGrabbing; + this.isGrabbing = isGrabbing; + if(lastGrabbingValue == isGrabbing) return; + + // Switch grabbing state then + mCurrentMap.resetPressedState(); + if(isGrabbing){ + mCurrentMap = mGameMap; + mPointerImageView.setVisibility(View.INVISIBLE); + mMouseSensitivity = 18; + return; + } + + mCurrentMap = mMenuMap; + sendDirectionalKeycode(mCurrentJoystickDirection, false, mGameMap); // removing what we were doing + + CallbackBridge.sendCursorPos(CallbackBridge.windowWidth/2f, CallbackBridge.windowHeight/2f); + placePointerView(CallbackBridge.physicalWidth/2, CallbackBridge.physicalHeight/2); + mPointerImageView.setVisibility(View.VISIBLE); + // Sensitivity in menu is MC and HARDWARE resolution dependent + mMouseSensitivity = 19 * mScaleFactor / mSensitivityFactor; + } + + @Override + public void handleGamepadInput(int keycode, float value) { + boolean isKeyEventDown = value == 1f; + switch (keycode){ + case KeyEvent.KEYCODE_BUTTON_A: + getCurrentMap().BUTTON_A.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_BUTTON_B: + getCurrentMap().BUTTON_B.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_BUTTON_X: + getCurrentMap().BUTTON_X.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_BUTTON_Y: + getCurrentMap().BUTTON_Y.update(isKeyEventDown); + break; + + //Shoulders + case KeyEvent.KEYCODE_BUTTON_L1: + getCurrentMap().SHOULDER_LEFT.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_BUTTON_R1: + getCurrentMap().SHOULDER_RIGHT.update(isKeyEventDown); + break; + + //Triggers + case KeyEvent.KEYCODE_BUTTON_L2: + getCurrentMap().TRIGGER_LEFT.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_BUTTON_R2: + getCurrentMap().TRIGGER_RIGHT.update(isKeyEventDown); + break; + + //L3 || R3 + case KeyEvent.KEYCODE_BUTTON_THUMBL: + getCurrentMap().THUMBSTICK_LEFT.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_BUTTON_THUMBR: + getCurrentMap().THUMBSTICK_RIGHT.update(isKeyEventDown); + break; + + //DPAD + case KeyEvent.KEYCODE_DPAD_UP: + getCurrentMap().DPAD_UP.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + getCurrentMap().DPAD_DOWN.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_DPAD_LEFT: + getCurrentMap().DPAD_LEFT.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + getCurrentMap().DPAD_RIGHT.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + getCurrentMap().DPAD_RIGHT.update(false); + getCurrentMap().DPAD_LEFT.update(false); + getCurrentMap().DPAD_UP.update(false); + getCurrentMap().DPAD_DOWN.update(false); + break; + + //Start/select + case KeyEvent.KEYCODE_BUTTON_START: + getCurrentMap().BUTTON_START.update(isKeyEventDown); + break; + case KeyEvent.KEYCODE_BUTTON_SELECT: + getCurrentMap().BUTTON_SELECT.update(isKeyEventDown); + break; + + /* Now, it is time for motionEvents */ + case AXIS_HAT_X: + getCurrentMap().DPAD_RIGHT.update(value > 0.85); + getCurrentMap().DPAD_LEFT.update(value < -0.85); + break; + case AXIS_HAT_Y: + getCurrentMap().DPAD_DOWN.update(value > 0.85); + getCurrentMap().DPAD_UP.update(value < -0.85); + break; + + // Left joystick + case AXIS_X: + mLeftJoystick.setXAxisValue(value); + updateJoysticks(); + break; + case AXIS_Y: + mLeftJoystick.setYAxisValue(value); + updateJoysticks(); + break; + + // Right joystick + case AXIS_Z: + mRightJoystick.setXAxisValue(value); + updateJoysticks(); + break; + case AXIS_RZ: + mRightJoystick.setYAxisValue(value); + updateJoysticks(); + break; + + // Triggers + case AXIS_RTRIGGER: + getCurrentMap().TRIGGER_RIGHT.update(value > 0.5); + break; + case AXIS_LTRIGGER: + getCurrentMap().TRIGGER_LEFT.update(value > 0.5); + break; + + default: + sendKeyPress(LwjglGlfwKeycode.GLFW_KEY_SPACE, CallbackBridge.getCurrentMods(), isKeyEventDown); + break; + } + } +} diff --git a/src/main/java/pojlib/input/gamepad/GamepadButton.java b/src/main/java/pojlib/input/gamepad/GamepadButton.java new file mode 100644 index 00000000..f479affd --- /dev/null +++ b/src/main/java/pojlib/input/gamepad/GamepadButton.java @@ -0,0 +1,30 @@ +package pojlib.input.gamepad; + +/** + * This class corresponds to a button that does exist on the gamepad + */ +public class GamepadButton extends GamepadEmulatedButton { + public boolean isToggleable = false; + private boolean mIsToggled = false; + + + @Override + protected void onDownStateChanged(boolean isDown) { + if(isToggleable && isDown){ + mIsToggled = !mIsToggled; + Gamepad.sendInput(keycodes, mIsToggled); + return; + } + super.onDownStateChanged(isDown); + } + + @Override + public void resetButtonState() { + if(!mIsDown && mIsToggled) { + Gamepad.sendInput(keycodes, false); + mIsToggled = false; + } else { + super.resetButtonState(); + } + } +} diff --git a/src/main/java/pojlib/input/gamepad/GamepadDataProvider.java b/src/main/java/pojlib/input/gamepad/GamepadDataProvider.java new file mode 100644 index 00000000..ecb096b0 --- /dev/null +++ b/src/main/java/pojlib/input/gamepad/GamepadDataProvider.java @@ -0,0 +1,10 @@ +package pojlib.input.gamepad; + +import pojlib.input.GrabListener; + +public interface GamepadDataProvider { + GamepadMap getMenuMap(); + GamepadMap getGameMap(); + boolean isGrabbing(); + void attachGrabListener(GrabListener grabListener); +} diff --git a/src/main/java/pojlib/input/gamepad/GamepadDpad.java b/src/main/java/pojlib/input/gamepad/GamepadDpad.java new file mode 100644 index 00000000..d1b86094 --- /dev/null +++ b/src/main/java/pojlib/input/gamepad/GamepadDpad.java @@ -0,0 +1,62 @@ +package pojlib.input.gamepad; + +import static android.view.InputDevice.KEYBOARD_TYPE_ALPHABETIC; +import static android.view.InputDevice.SOURCE_GAMEPAD; +import static android.view.KeyEvent.KEYCODE_DPAD_CENTER; +import static android.view.KeyEvent.KEYCODE_DPAD_DOWN; +import static android.view.KeyEvent.KEYCODE_DPAD_LEFT; +import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; +import static android.view.KeyEvent.KEYCODE_DPAD_UP; + +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; + + +public class GamepadDpad { + private int mLastKeycode = KEYCODE_DPAD_CENTER; + + /** + * Convert the event to a 2 int array: keycode and keyAction, similar to a keyEvent + * @param event The motion to convert + * @return int[0] keycode, int[1] keyAction + */ + public int[] convertEvent(MotionEvent event){ + // Use the hat axis value to find the D-pad direction + float xaxis = event.getAxisValue(MotionEvent.AXIS_HAT_X); + float yaxis = event.getAxisValue(MotionEvent.AXIS_HAT_Y); + int action = KeyEvent.ACTION_DOWN; + + // Check if the AXIS_HAT_X value is -1 or 1, and set the D-pad + // LEFT and RIGHT direction accordingly. + if (Float.compare(xaxis, -1.0f) == 0) { + mLastKeycode = KEYCODE_DPAD_LEFT; + } else if (Float.compare(xaxis, 1.0f) == 0) { + mLastKeycode = KEYCODE_DPAD_RIGHT; + } + // Check if the AXIS_HAT_Y value is -1 or 1, and set the D-pad + // UP and DOWN direction accordingly. + else if (Float.compare(yaxis, -1.0f) == 0) { + mLastKeycode = KEYCODE_DPAD_UP; + } else if (Float.compare(yaxis, 1.0f) == 0) { + mLastKeycode = KEYCODE_DPAD_DOWN; + }else { + //No keycode change + action = KeyEvent.ACTION_UP; + } + + return new int[]{mLastKeycode, action}; + + } + + @SuppressWarnings("unused") public static boolean isDpadEvent(MotionEvent event) { + // Check that input comes from a device with directional pads. + // And... also the joystick since it declares sometimes as a joystick. + return (event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK; + } + + public static boolean isDpadEvent(KeyEvent event){ + //return ((event.getSource() & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) && (event.getDevice().getKeyboardType() == KEYBOARD_TYPE_NON_ALPHABETIC); + return event.isFromSource(SOURCE_GAMEPAD) && event.getDevice().getKeyboardType() != KEYBOARD_TYPE_ALPHABETIC; + } +} \ No newline at end of file diff --git a/src/main/java/pojlib/input/gamepad/GamepadEmulatedButton.java b/src/main/java/pojlib/input/gamepad/GamepadEmulatedButton.java new file mode 100644 index 00000000..65376031 --- /dev/null +++ b/src/main/java/pojlib/input/gamepad/GamepadEmulatedButton.java @@ -0,0 +1,33 @@ +package pojlib.input.gamepad; + +import android.view.KeyEvent; + +/** + * This class corresponds to a button that does not physically exist on the gamepad, but is + * emulated from other inputs on it (like WASD directional keys) + */ +public class GamepadEmulatedButton { + public short[] keycodes; + protected boolean mIsDown = false; + + public void update(KeyEvent event) { + boolean isKeyDown = (event.getAction() == KeyEvent.ACTION_DOWN); + update(isKeyDown); + } + + public void update(boolean isKeyDown){ + if(isKeyDown != mIsDown){ + mIsDown = isKeyDown; + onDownStateChanged(mIsDown); + } + } + + public void resetButtonState() { + if(mIsDown) Gamepad.sendInput(keycodes, false); + mIsDown = false; + } + + protected void onDownStateChanged(boolean isDown) { + Gamepad.sendInput(keycodes, mIsDown); + } +} diff --git a/src/main/java/pojlib/input/gamepad/GamepadJoystick.java b/src/main/java/pojlib/input/gamepad/GamepadJoystick.java new file mode 100644 index 00000000..6826480a --- /dev/null +++ b/src/main/java/pojlib/input/gamepad/GamepadJoystick.java @@ -0,0 +1,85 @@ +package pojlib.input.gamepad; + +import android.view.InputDevice; +import android.view.MotionEvent; + +import pojlib.util.MathUtils; + +public class GamepadJoystick { + + //Directions + public static final int DIRECTION_NONE = -1; //GamepadJoystick at the center + + public static final int DIRECTION_EAST = 0; + public static final int DIRECTION_NORTH_EAST = 1; + public static final int DIRECTION_NORTH = 2; + public static final int DIRECTION_NORTH_WEST = 3; + public static final int DIRECTION_WEST = 4; + public static final int DIRECTION_SOUTH_WEST = 5; + public static final int DIRECTION_SOUTH = 6; + public static final int DIRECTION_SOUTH_EAST = 7; + + private final InputDevice mInputDevice; + + private final int mHorizontalAxis; + private final int mVerticalAxis; + private float mVerticalAxisValue = 0; + private float mHorizontalAxisValue = 0; + + public GamepadJoystick(int horizontalAxis, int verticalAxis, InputDevice device){ + mHorizontalAxis = horizontalAxis; + mVerticalAxis = verticalAxis; + this.mInputDevice = device; + } + + public double getAngleRadian(){ + //From -PI to PI + // TODO misuse of the deadzone here ! + return -Math.atan2(getVerticalAxis(), getHorizontalAxis()); + } + + + public double getAngleDegree(){ + //From 0 to 360 degrees + double result = Math.toDegrees(getAngleRadian()); + if(result < 0) result += 360; + + return result; + } + + public double getMagnitude(){ + float x = Math.abs(mHorizontalAxisValue); + float y = Math.abs(mVerticalAxisValue); + + return MathUtils.dist(0,0, x, y); + } + + public float getVerticalAxis(){ + return mVerticalAxisValue; + } + + public float getHorizontalAxis(){ + return mHorizontalAxisValue; + } + + public static boolean isJoystickEvent(MotionEvent event){ + return (event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK + && event.getAction() == MotionEvent.ACTION_MOVE; + } + + + public int getHeightDirection(){ + if(getMagnitude() == 0) return DIRECTION_NONE; + return ((int) ((getAngleDegree()+22.5)/45)) % 8; + } + + + /* Setters */ + public void setXAxisValue(float value){ + this.mHorizontalAxisValue = value; + } + + public void setYAxisValue(float value){ + this.mVerticalAxisValue = value; + } +} diff --git a/src/main/java/pojlib/input/gamepad/GamepadMap.java b/src/main/java/pojlib/input/gamepad/GamepadMap.java new file mode 100644 index 00000000..f1ba16c8 --- /dev/null +++ b/src/main/java/pojlib/input/gamepad/GamepadMap.java @@ -0,0 +1,190 @@ +package pojlib.input.gamepad; + +import pojlib.input.LwjglGlfwKeycode; + +public class GamepadMap { + + + public static final short MOUSE_SCROLL_DOWN = -1; + public static final short MOUSE_SCROLL_UP = -2; + // Made mouse keycodes their own specials because managing special keycodes above 0 + // proved to be complicated + public static final short MOUSE_LEFT = -3; + public static final short MOUSE_MIDDLE = -4; + public static final short MOUSE_RIGHT = -5; + // Workaround, because GLFW_KEY_UNKNOWN and GLFW_MOUSE_BUTTON_LEFT are both 0 + public static final short UNSPECIFIED = -6; + + /* + This class is just here to store the mapping + can be modified to create re-mappable controls I guess + + Be warned, you should define ALL keys if you want to avoid a non defined exception + */ + + public GamepadButton BUTTON_A, BUTTON_B, BUTTON_X, BUTTON_Y, BUTTON_START, BUTTON_SELECT, + TRIGGER_RIGHT, TRIGGER_LEFT, SHOULDER_RIGHT, SHOULDER_LEFT, THUMBSTICK_RIGHT, + THUMBSTICK_LEFT, DPAD_UP, DPAD_DOWN, DPAD_RIGHT, DPAD_LEFT; + + public GamepadEmulatedButton DIRECTION_FORWARD, DIRECTION_BACKWARD, DIRECTION_RIGHT, DIRECTION_LEFT; + + /* + * Sets all buttons to a not pressed state, sending an input if needed + */ + public void resetPressedState(){ + BUTTON_A.resetButtonState(); + BUTTON_B.resetButtonState(); + BUTTON_X.resetButtonState(); + BUTTON_Y.resetButtonState(); + + BUTTON_START.resetButtonState(); + BUTTON_SELECT.resetButtonState(); + + TRIGGER_LEFT.resetButtonState(); + TRIGGER_RIGHT.resetButtonState(); + + SHOULDER_LEFT.resetButtonState(); + SHOULDER_RIGHT.resetButtonState(); + + THUMBSTICK_LEFT.resetButtonState(); + THUMBSTICK_RIGHT.resetButtonState(); + + DPAD_UP.resetButtonState(); + DPAD_RIGHT.resetButtonState(); + DPAD_DOWN.resetButtonState(); + DPAD_LEFT.resetButtonState(); + + } + + private static GamepadMap createAndInitializeButtons() { + GamepadMap gamepadMap = new GamepadMap(); + gamepadMap.BUTTON_A = new GamepadButton(); + gamepadMap.BUTTON_B = new GamepadButton(); + gamepadMap.BUTTON_X = new GamepadButton(); + gamepadMap.BUTTON_Y = new GamepadButton(); + + gamepadMap.BUTTON_START = new GamepadButton(); + gamepadMap.BUTTON_SELECT = new GamepadButton(); + + gamepadMap.TRIGGER_RIGHT = new GamepadButton(); + gamepadMap.TRIGGER_LEFT = new GamepadButton(); + + gamepadMap.SHOULDER_RIGHT = new GamepadButton(); + gamepadMap.SHOULDER_LEFT = new GamepadButton(); + + gamepadMap.DIRECTION_FORWARD = new GamepadEmulatedButton(); + gamepadMap.DIRECTION_BACKWARD = new GamepadEmulatedButton(); + gamepadMap.DIRECTION_RIGHT = new GamepadEmulatedButton(); + gamepadMap.DIRECTION_LEFT = new GamepadEmulatedButton(); + + gamepadMap.THUMBSTICK_RIGHT = new GamepadButton(); + gamepadMap.THUMBSTICK_LEFT = new GamepadButton(); + + gamepadMap.DPAD_UP = new GamepadButton(); + gamepadMap.DPAD_RIGHT = new GamepadButton(); + gamepadMap.DPAD_DOWN = new GamepadButton(); + gamepadMap.DPAD_LEFT = new GamepadButton(); + return gamepadMap; + } + + /* + * Returns a pre-done mapping used when the mouse is grabbed by the game. + */ + public static GamepadMap getDefaultGameMap(){ + GamepadMap gameMap = GamepadMap.createEmptyMap(); + + gameMap.BUTTON_A.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_SPACE; + gameMap.BUTTON_B.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_Q; + gameMap.BUTTON_X.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_E; + gameMap.BUTTON_Y.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_F; + + gameMap.DIRECTION_FORWARD.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_W; + gameMap.DIRECTION_BACKWARD.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_S; + gameMap.DIRECTION_RIGHT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_D; + gameMap.DIRECTION_LEFT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_A; + + gameMap.DPAD_UP.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT; + gameMap.DPAD_DOWN.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_O; //For mods ? + gameMap.DPAD_RIGHT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_K; //For mods ? + gameMap.DPAD_LEFT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_J; //For mods ? + + gameMap.SHOULDER_LEFT.keycodes[0] = GamepadMap.MOUSE_SCROLL_UP; + gameMap.SHOULDER_RIGHT.keycodes[0] = GamepadMap.MOUSE_SCROLL_DOWN; + + gameMap.TRIGGER_LEFT.keycodes[0] = GamepadMap.MOUSE_RIGHT; + gameMap.TRIGGER_RIGHT.keycodes[0] = GamepadMap.MOUSE_LEFT; + + gameMap.THUMBSTICK_LEFT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_LEFT_CONTROL; + gameMap.THUMBSTICK_RIGHT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT; + gameMap.THUMBSTICK_RIGHT.isToggleable = true; + + gameMap.BUTTON_START.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_ESCAPE; + gameMap.BUTTON_SELECT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_TAB; + + return gameMap; + } + + /* + * Returns a pre-done mapping used when the mouse is NOT grabbed by the game. + */ + public static GamepadMap getDefaultMenuMap(){ + GamepadMap menuMap = GamepadMap.createEmptyMap(); + + menuMap.BUTTON_A.keycodes[0] = GamepadMap.MOUSE_LEFT; + menuMap.BUTTON_B.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_ESCAPE; + menuMap.BUTTON_X.keycodes[0] = GamepadMap.MOUSE_RIGHT; + { + short[] keycodes = menuMap.BUTTON_Y.keycodes; + keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT; + keycodes[1] = GamepadMap.MOUSE_RIGHT; + } + + { + short[] keycodes = menuMap.DIRECTION_FORWARD.keycodes; + keycodes[0] = keycodes[1] = keycodes[2] = keycodes[3] = GamepadMap.MOUSE_SCROLL_UP; + } + { + short[] keycodes = menuMap.DIRECTION_BACKWARD.keycodes; + keycodes[0] = keycodes[1] = keycodes[2] = keycodes[3] = GamepadMap.MOUSE_SCROLL_DOWN; + } + + menuMap.DPAD_DOWN.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_O; //For mods ? + menuMap.DPAD_RIGHT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_K; //For mods ? + menuMap.DPAD_LEFT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_J; //For mods ? + + menuMap.SHOULDER_LEFT.keycodes[0] = GamepadMap.MOUSE_SCROLL_UP; + menuMap.SHOULDER_RIGHT.keycodes[0] = GamepadMap.MOUSE_SCROLL_DOWN; + + menuMap.BUTTON_SELECT.keycodes[0] = LwjglGlfwKeycode.GLFW_KEY_ESCAPE; + + return menuMap; + } + + /* + * Returns all GamepadEmulatedButtons of the controller key map. + */ + public GamepadEmulatedButton[] getButtons(){ + return new GamepadEmulatedButton[]{ BUTTON_A, BUTTON_B, BUTTON_X, BUTTON_Y, + BUTTON_SELECT, BUTTON_START, + TRIGGER_LEFT, TRIGGER_RIGHT, + SHOULDER_LEFT, SHOULDER_RIGHT, + THUMBSTICK_LEFT, THUMBSTICK_RIGHT, + DPAD_UP, DPAD_RIGHT, DPAD_DOWN, DPAD_LEFT, + DIRECTION_FORWARD, DIRECTION_BACKWARD, + DIRECTION_LEFT, DIRECTION_RIGHT}; + } + + /* + * Returns an pre-initialized GamepadMap with only empty keycodes + */ + @SuppressWarnings("unused") public static GamepadMap createEmptyMap(){ + GamepadMap emptyMap = createAndInitializeButtons(); + for(GamepadEmulatedButton button : emptyMap.getButtons()) + button.keycodes = new short[] {UNSPECIFIED, UNSPECIFIED, UNSPECIFIED, UNSPECIFIED}; + return emptyMap; + } + + public static String[] getSpecialKeycodeNames() { + return new String[] {"UNSPECIFIED", "MOUSE_RIGHT", "MOUSE_MIDDLE", "MOUSE_LEFT", "SCROLL_UP", "SCROLL_DOWN"}; + } +} diff --git a/src/main/java/pojlib/input/gamepad/GamepadMapStore.java b/src/main/java/pojlib/input/gamepad/GamepadMapStore.java new file mode 100644 index 00000000..fd61c10d --- /dev/null +++ b/src/main/java/pojlib/input/gamepad/GamepadMapStore.java @@ -0,0 +1,61 @@ +package pojlib.input.gamepad; + +import android.util.Log; + +import com.google.gson.JsonParseException; + +import java.io.File; +import java.io.IOException; + +import pojlib.util.Constants; +import pojlib.util.FileUtil; +import pojlib.util.GsonUtils; + +public class GamepadMapStore { + private static final File STORE_FILE = new File(Constants.USER_HOME, "gamepad_map.json"); + private static GamepadMapStore sMapStore; + private GamepadMap mInMenuMap; + private GamepadMap mInGameMap; + private static GamepadMapStore createDefault() { + GamepadMapStore mapStore = new GamepadMapStore(); + mapStore.mInGameMap = GamepadMap.getDefaultGameMap(); + mapStore.mInMenuMap = GamepadMap.getDefaultMenuMap(); + return mapStore; + } + + private static void loadIfNecessary() { + if(sMapStore == null) return; + load(); + } + + public static void load() { + GamepadMapStore mapStore = null; + if(STORE_FILE.exists() && STORE_FILE.canRead()) { + try { + String storeFileContent = FileUtil.read(STORE_FILE.getPath()); + mapStore = GsonUtils.GLOBAL_GSON.fromJson(storeFileContent, GamepadMapStore.class); + } catch (JsonParseException | IOException e) { + Log.w("GamepadMapStore", "Map store failed to load!", e); + } + } + if(mapStore == null) mapStore = createDefault(); + sMapStore = mapStore; + } + + public static void save() throws IOException { + if(sMapStore == null) throw new RuntimeException("Must load map store first!"); + FileUtil.ensureParentDirectory(STORE_FILE); + String jsonData = GsonUtils.GLOBAL_GSON.toJson(sMapStore); + FileUtil.write(STORE_FILE.getAbsolutePath(), jsonData.getBytes()); + } + + public static GamepadMap getGameMap() { + loadIfNecessary(); + return sMapStore.mInGameMap; + } + + public static GamepadMap getMenuMap() { + loadIfNecessary(); + return sMapStore.mInMenuMap; + } +} diff --git a/src/main/java/pojlib/util/FileUtil.java b/src/main/java/pojlib/util/FileUtil.java index 89e245cf..553cbcb2 100644 --- a/src/main/java/pojlib/util/FileUtil.java +++ b/src/main/java/pojlib/util/FileUtil.java @@ -133,4 +133,29 @@ public static File newFile(File destinationDir, ZipEntry zipEntry) throws IOExce return destFile; } + + /** + * @author PojavLauncherTeam + * Same as ensureDirectorySilently(), but throws an IOException telling why the check failed. + * @param targetFile the directory to check + * @throws IOException when the checks fail + */ + public static void ensureDirectory(File targetFile) throws IOException{ + if(targetFile.isFile()) throw new IOException("Target directory is a file"); + if(targetFile.exists()) { + if(!targetFile.canWrite()) throw new IOException("Target directory is not writable"); + }else if(!targetFile.mkdirs()) throw new IOException("Unable to create target directory"); + } + + /** + * @author PojavLauncherTeam + * Same as ensureParentDirectorySilently(), but throws an IOException telling why the check failed. + * @param targetFile the File whose parent should be checked + * @throws IOException when the checks fail + */ + public static void ensureParentDirectory(File targetFile) throws IOException{ + File parentFile = targetFile.getParentFile(); + if(parentFile == null) throw new IOException("targetFile does not have a parent"); + ensureDirectory(parentFile); + } } diff --git a/src/main/java/pojlib/util/MCOptionUtils.java b/src/main/java/pojlib/util/MCOptionUtils.java new file mode 100644 index 00000000..44a3baaf --- /dev/null +++ b/src/main/java/pojlib/util/MCOptionUtils.java @@ -0,0 +1,142 @@ +package pojlib.util; + +import static org.lwjgl.glfw.CallbackBridge.windowHeight; +import static org.lwjgl.glfw.CallbackBridge.windowWidth; + +import android.os.Build; +import android.os.FileObserver; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Objects; + +import pojlib.API; + +public class MCOptionUtils { + private static final HashMap sParameterMap = new HashMap<>(); + private static final ArrayList> sOptionListeners = new ArrayList<>(); + private static FileObserver sFileObserver; + private static String sOptionFolderPath = null; + public interface MCOptionListener { + /** Called when an option is changed. Don't know which one though */ + void onOptionChanged(); + } + + + public static void load(){ + load(sOptionFolderPath == null + ? API.currentInstance.gameDir + : sOptionFolderPath); + } + + public static void load(@NonNull String folderPath) { + File optionFile = new File(folderPath + "/options.txt"); + if(!optionFile.exists()) { + try { // Needed for new instances I guess :think: + optionFile.createNewFile(); + } catch (IOException e) { e.printStackTrace(); } + } + + if(sFileObserver == null || !Objects.equals(sOptionFolderPath, folderPath)){ + sOptionFolderPath = folderPath; + setupFileObserver(); + } + sOptionFolderPath = folderPath; // Yeah I know, it may be redundant + + sParameterMap.clear(); + + try { + BufferedReader reader = new BufferedReader(new FileReader(optionFile)); + String line; + while ((line = reader.readLine()) != null) { + int firstColonIndex = line.indexOf(':'); + if(firstColonIndex < 0) { + Log.w("QuestCraft", "No colon on line \""+line+"\", skipping"); + continue; + } + sParameterMap.put(line.substring(0,firstColonIndex), line.substring(firstColonIndex+1)); + } + reader.close(); + } catch (IOException e) { + Log.w("QuestCraft", "Could not load options.txt", e); + } + } + + public static String get(String key) { + return sParameterMap.get(key); + } + + /** @return The stored Minecraft GUI scale, also auto-computed if on auto-mode or improper setting */ + public static int getMcScale() { + String str = MCOptionUtils.get("guiScale"); + int guiScale = (str == null ? 0 :Integer.parseInt(str)); + + int scale = Math.max(Math.min(windowWidth / 320, windowHeight / 240), 1); + if(scale < guiScale || guiScale == 0){ + guiScale = scale; + } + + return guiScale; + } + + /** Add a file observer to reload options on file change + * Listeners get notified of the change */ + private static void setupFileObserver(){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){ + sFileObserver = new FileObserver(new File(sOptionFolderPath + "/options.txt"), FileObserver.MODIFY) { + @Override + public void onEvent(int i, @Nullable String s) { + MCOptionUtils.load(); + notifyListeners(); + } + }; + }else{ + sFileObserver = new FileObserver(sOptionFolderPath + "/options.txt", FileObserver.MODIFY) { + @Override + public void onEvent(int i, @Nullable String s) { + MCOptionUtils.load(); + notifyListeners(); + } + }; + } + + sFileObserver.startWatching(); + } + + /** Notify the option listeners */ + public static void notifyListeners(){ + for(WeakReference weakReference : sOptionListeners){ + MCOptionListener optionListener = weakReference.get(); + if(optionListener == null) continue; + + optionListener.onOptionChanged(); + } + } + + /** Add an option listener, notice how we don't have a reference to it */ + public static void addMCOptionListener(MCOptionListener listener){ + sOptionListeners.add(new WeakReference<>(listener)); + } + + /** Remove a listener from existence, or at least, its reference here */ + public static void removeMCOptionListener(MCOptionListener listener){ + for(WeakReference weakReference : sOptionListeners){ + MCOptionListener optionListener = weakReference.get(); + if(optionListener == null) continue; + if(optionListener == listener){ + sOptionListeners.remove(weakReference); + return; + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/pojlib/util/MathUtils.java b/src/main/java/pojlib/util/MathUtils.java new file mode 100644 index 00000000..04e700ff --- /dev/null +++ b/src/main/java/pojlib/util/MathUtils.java @@ -0,0 +1,11 @@ +package pojlib.util; + +public class MathUtils { + + /** Returns the distance between two points. */ + public static float dist(float x1, float y1, float x2, float y2) { + final float x = (x2 - x1); + final float y = (y2 - y1); + return (float) Math.hypot(x, y); + } +} diff --git a/src/main/res/drawable/ic_gamepad_pointer.png b/src/main/res/drawable/ic_gamepad_pointer.png new file mode 100644 index 00000000..7ad77f98 Binary files /dev/null and b/src/main/res/drawable/ic_gamepad_pointer.png differ