From 47558d20c505edf2a43eabeebbd39cffe3158579 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Thu, 8 Sep 2022 12:29:02 +0900 Subject: [PATCH] internal/gamepaddb: enable the database for Android Before this fix, the button and axis IDs are from the OS. These didn't match with the SDL game controller databaes unfortunately. This fix changes the assignments of the buttons and the axes to match with the database. Closes #2312 --- cmd/ebitenmobile/gobind.go | 176 ++++++++++---------- cmd/ebitenmobile/gobind.src.go | 2 +- internal/gamepad/extern_android.go | 84 +++++----- internal/gamepaddb/gamepaddb.go | 111 ++++--------- internal/gamepaddb/sdl.go | 51 ++++++ mobile/ebitenmobileview/input_android.go | 197 +++++++---------------- 6 files changed, 273 insertions(+), 348 deletions(-) create mode 100644 internal/gamepaddb/sdl.go diff --git a/cmd/ebitenmobile/gobind.go b/cmd/ebitenmobile/gobind.go index c209f4de3..0625c0024 100644 --- a/cmd/ebitenmobile/gobind.go +++ b/cmd/ebitenmobile/gobind.go @@ -354,6 +354,11 @@ const viewJava = `// Code generated by ebitenmobile. DO NOT EDIT. package {{.JavaPkg}}.{{.PrefixLower}}; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + import android.content.Context; import android.hardware.input.InputManager; import android.os.Handler; @@ -371,6 +376,32 @@ import android.view.WindowManager; import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview; public class EbitenView extends ViewGroup implements InputManager.InputDeviceListener { + static class Gamepad { + public int deviceId; + public ArrayList axes; + public ArrayList hats; + } + + // See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L154-L173 + static class RangeComparator implements Comparator { + @Override + public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) { + int arg0Axis = arg0.getAxis(); + int arg1Axis = arg1.getAxis(); + if (arg0Axis == MotionEvent.AXIS_GAS) { + arg0Axis = MotionEvent.AXIS_BRAKE; + } else if (arg0Axis == MotionEvent.AXIS_BRAKE) { + arg0Axis = MotionEvent.AXIS_GAS; + } + if (arg1Axis == MotionEvent.AXIS_GAS) { + arg1Axis = MotionEvent.AXIS_BRAKE; + } else if (arg1Axis == MotionEvent.AXIS_BRAKE) { + arg1Axis = MotionEvent.AXIS_GAS; + } + return arg0Axis - arg1Axis; + } + } + private static double pxToDp(double x) { return x / Ebitenmobileview.deviceScale(); } @@ -386,6 +417,8 @@ public class EbitenView extends ViewGroup implements InputManager.InputDeviceLis } private void initialize(Context context) { + this.gamepads = new ArrayList(); + this.ebitenSurfaceView = new EbitenSurfaceView(getContext()); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); addView(this.ebitenSurfaceView, params); @@ -433,75 +466,14 @@ public class EbitenView extends ViewGroup implements InputManager.InputDeviceLis return true; } - // The order must be the same as mobile/ebitenmobileview/input_android.go. - static int[] gamepadButtons = { - KeyEvent.KEYCODE_BUTTON_A, - KeyEvent.KEYCODE_BUTTON_B, - KeyEvent.KEYCODE_BUTTON_C, - KeyEvent.KEYCODE_BUTTON_X, - KeyEvent.KEYCODE_BUTTON_Y, - KeyEvent.KEYCODE_BUTTON_Z, - KeyEvent.KEYCODE_BUTTON_L1, - KeyEvent.KEYCODE_BUTTON_R1, - KeyEvent.KEYCODE_BUTTON_L2, - KeyEvent.KEYCODE_BUTTON_R2, - KeyEvent.KEYCODE_BUTTON_THUMBL, - KeyEvent.KEYCODE_BUTTON_THUMBR, - KeyEvent.KEYCODE_BUTTON_START, - KeyEvent.KEYCODE_BUTTON_SELECT, - KeyEvent.KEYCODE_BUTTON_MODE, - KeyEvent.KEYCODE_BUTTON_1, - KeyEvent.KEYCODE_BUTTON_2, - KeyEvent.KEYCODE_BUTTON_3, - KeyEvent.KEYCODE_BUTTON_4, - KeyEvent.KEYCODE_BUTTON_5, - KeyEvent.KEYCODE_BUTTON_6, - KeyEvent.KEYCODE_BUTTON_7, - KeyEvent.KEYCODE_BUTTON_8, - KeyEvent.KEYCODE_BUTTON_9, - KeyEvent.KEYCODE_BUTTON_10, - KeyEvent.KEYCODE_BUTTON_11, - KeyEvent.KEYCODE_BUTTON_12, - KeyEvent.KEYCODE_BUTTON_13, - KeyEvent.KEYCODE_BUTTON_14, - KeyEvent.KEYCODE_BUTTON_15, - KeyEvent.KEYCODE_BUTTON_16, - }; - - // The order must be the same as mobile/ebitenmobileview/input_android.go. - static int[] axes = { - MotionEvent.AXIS_X, - MotionEvent.AXIS_Y, - MotionEvent.AXIS_Z, - MotionEvent.AXIS_RX, - MotionEvent.AXIS_RY, - MotionEvent.AXIS_RZ, - MotionEvent.AXIS_HAT_X, - MotionEvent.AXIS_HAT_Y, - MotionEvent.AXIS_LTRIGGER, - MotionEvent.AXIS_RTRIGGER, - MotionEvent.AXIS_THROTTLE, - MotionEvent.AXIS_RUDDER, - MotionEvent.AXIS_WHEEL, - MotionEvent.AXIS_GAS, - MotionEvent.AXIS_BRAKE, - MotionEvent.AXIS_GENERIC_1, - MotionEvent.AXIS_GENERIC_2, - MotionEvent.AXIS_GENERIC_3, - MotionEvent.AXIS_GENERIC_4, - MotionEvent.AXIS_GENERIC_5, - MotionEvent.AXIS_GENERIC_6, - MotionEvent.AXIS_GENERIC_7, - MotionEvent.AXIS_GENERIC_8, - MotionEvent.AXIS_GENERIC_9, - MotionEvent.AXIS_GENERIC_10, - MotionEvent.AXIS_GENERIC_11, - MotionEvent.AXIS_GENERIC_12, - MotionEvent.AXIS_GENERIC_13, - MotionEvent.AXIS_GENERIC_14, - MotionEvent.AXIS_GENERIC_15, - MotionEvent.AXIS_GENERIC_16, - }; + private Gamepad getGamepad(int deviceId) { + for (Gamepad gamepad : this.gamepads) { + if (gamepad.deviceId == deviceId) { + return gamepad; + } + } + return null; + } @Override public boolean onGenericMotionEvent(MotionEvent event) { @@ -511,17 +483,24 @@ public class EbitenView extends ViewGroup implements InputManager.InputDeviceLis if (event.getAction() != MotionEvent.ACTION_MOVE) { return super.onGenericMotionEvent(event); } - InputDevice inputDevice = this.inputManager.getInputDevice(event.getDeviceId()); - for (int axis : axes) { - InputDevice.MotionRange motionRange = inputDevice.getMotionRange(axis, event.getSource()); - float value = 0.0f; - if (motionRange != null) { - value = event.getAxisValue(axis); - if (Math.abs(value) <= motionRange.getFlat()) { - value = 0.0f; - } - } - Ebitenmobileview.onGamepadAxesOrHatsChanged(event.getDeviceId(), axis, value); + + // See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L256-L277 + Gamepad gamepad = this.getGamepad(event.getDeviceId()); + if (gamepad == null) { + return true; + } + + int actionPointerIndex = event.getActionIndex(); + for (int i = 0; i < gamepad.axes.size(); i++) { + InputDevice.MotionRange range = gamepad.axes.get(i); + float axisValue = event.getAxisValue(range.getAxis(), actionPointerIndex); + float value = (axisValue - range.getMin()) / range.getRange() * 2.0f - 1.0f; + Ebitenmobileview.onGamepadAxisChanged(gamepad.deviceId, i, value); + } + for (int i = 0; i < gamepad.hats.size() / 2; i++) { + int hatX = Math.round(event.getAxisValue(gamepad.hats.get(2*i).getAxis(), actionPointerIndex)); + int hatY = Math.round(event.getAxisValue(gamepad.hats.get(2*i+1).getAxis(), actionPointerIndex)); + Ebitenmobileview.onGamepadHatChanged(gamepad.deviceId, i, hatX, hatY); } return true; } @@ -545,34 +524,43 @@ public class EbitenView extends ViewGroup implements InputManager.InputDeviceLis return; } - int naxes = 0; - int nhats2 = 0; - for (int i = 0; i < axes.length; i++) { - InputDevice.MotionRange range = inputDevice.getMotionRange(axes[i], InputDevice.SOURCE_JOYSTICK); - if (range == null) { - continue; - } + // See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L182-L216 + List ranges = inputDevice.getMotionRanges(); + Collections.sort(ranges, new RangeComparator()); + + Gamepad gamepad = new Gamepad(); + gamepad.deviceId = deviceId; + gamepad.axes = new ArrayList(); + gamepad.hats = new ArrayList(); + for (InputDevice.MotionRange range : ranges) { if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) { - nhats2++; + gamepad.hats.add(range); } else { - naxes++; + gamepad.axes.add(range); } } + this.gamepads.add(gamepad); String descriptor = inputDevice.getDescriptor(); int vendorId = inputDevice.getVendorId(); int productId = inputDevice.getProductId(); // These values are required to calculate SDL's GUID. - int buttonMask = getButtonMask(inputDevice, nhats2/2); + int buttonMask = getButtonMask(inputDevice, gamepad.hats.size()/2); int axisMask = getAxisMask(inputDevice); - Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), naxes, nhats2/2, descriptor, vendorId, productId, buttonMask, axisMask); + Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), gamepad.axes.size(), gamepad.hats.size()/2, descriptor, vendorId, productId, buttonMask, axisMask); + + // Initialize the trigger axes values explicitly, or the initial button values would be 0.5 instead of 0. + if (gamepad.axes.size() >= 6) { + Ebitenmobileview.onGamepadAxisChanged(deviceId, 4, -1); + Ebitenmobileview.onGamepadAxisChanged(deviceId, 5, -1); + } } // The implementation is copied from SDL: // https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L308 - private int getButtonMask(InputDevice joystickDevice, int nhats) { + private static int getButtonMask(InputDevice joystickDevice, int nhats) { int buttonMask = 0; int[] keys = new int[] { KeyEvent.KEYCODE_BUTTON_A, @@ -672,7 +660,7 @@ public class EbitenView extends ViewGroup implements InputManager.InputDeviceLis return buttonMask; } - private int getAxisMask(InputDevice joystickDevice) { + private static int getAxisMask(InputDevice joystickDevice) { final int SDL_CONTROLLER_AXIS_LEFTX = 0; final int SDL_CONTROLLER_AXIS_LEFTY = 1; final int SDL_CONTROLLER_AXIS_RIGHTX = 2; @@ -712,6 +700,7 @@ public class EbitenView extends ViewGroup implements InputManager.InputDeviceLis public void onInputDeviceRemoved(int deviceId) { // Do not call inputManager.getInputDevice(), which returns null (#1185). Ebitenmobileview.onInputDeviceRemoved(deviceId); + this.gamepads.remove(this.getGamepad(deviceId)); } // suspendGame suspends the game. @@ -748,6 +737,7 @@ public class EbitenView extends ViewGroup implements InputManager.InputDeviceLis private EbitenSurfaceView ebitenSurfaceView; private InputManager inputManager; + private ArrayList gamepads; } ` diff --git a/cmd/ebitenmobile/gobind.src.go b/cmd/ebitenmobile/gobind.src.go index 8585fb29e..a7622d56b 100644 --- a/cmd/ebitenmobile/gobind.src.go +++ b/cmd/ebitenmobile/gobind.src.go @@ -2,4 +2,4 @@ package main -var gobindsrc = []byte("// Copyright 2019 The Ebiten Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n//go:build ebitenmobilegobind\n// +build ebitenmobilegobind\n\n// gobind is a wrapper of the original gobind. This command adds extra files like a view controller.\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"golang.org/x/tools/go/packages\"\n)\n\nvar (\n\tlang = flag.String(\"lang\", \"\", \"\")\n\toutdir = flag.String(\"outdir\", \"\", \"\")\n\tjavaPkg = flag.String(\"javapkg\", \"\", \"\")\n\tprefix = flag.String(\"prefix\", \"\", \"\")\n\tbootclasspath = flag.String(\"bootclasspath\", \"\", \"\")\n\tclasspath = flag.String(\"classpath\", \"\", \"\")\n\ttags = flag.String(\"tags\", \"\", \"\")\n)\n\nvar usage = `The Gobind tool generates Java language bindings for Go.\n\nFor usage details, see doc.go.`\n\nfunc main() {\n\tflag.Parse()\n\tif err := run(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc invokeOriginalGobind(lang string) (pkgName string, err error) {\n\tcmd := exec.Command(\"gobind-original\", os.Args[1:]...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcfgtags := strings.Join(strings.Split(*tags, \",\"), \" \")\n\tcfg := &packages.Config{}\n\tswitch lang {\n\tcase \"java\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=android\")\n\tcase \"objc\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=darwin\")\n\t\tif cfgtags != \"\" {\n\t\t\tcfgtags += \" \"\n\t\t}\n\t\tcfgtags += \"ios\"\n\t}\n\tcfg.BuildFlags = []string{\"-tags\", cfgtags}\n\tpkgs, err := packages.Load(cfg, flag.Args()[0])\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn pkgs[0].Name, nil\n}\n\nfunc run() error {\n\twriteFile := func(filename string, content string) error {\n\t\tif err := ioutil.WriteFile(filepath.Join(*outdir, filename), []byte(content), 0644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Add additional files.\n\tlangs := strings.Split(*lang, \",\")\n\tfor _, lang := range langs {\n\t\tpkgName, err := invokeOriginalGobind(lang)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprefixLower := *prefix + pkgName\n\t\tprefixUpper := strings.Title(*prefix) + strings.Title(pkgName)\n\t\treplacePrefixes := func(content string) string {\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixUpper}}\", prefixUpper)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixLower}}\", prefixLower)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.JavaPkg}}\", *javaPkg)\n\t\t\treturn content\n\t\t}\n\n\t\tswitch lang {\n\t\tcase \"objc\":\n\t\t\t// iOS\n\t\t\tif err := writeFile(filepath.Join(\"src\", \"gobind\", prefixLower+\"ebitenviewcontroller_ios.m\"), replacePrefixes(objcM)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := writeFile(filepath.Join(\"src\", \"gobind\", prefixLower+\"ebitenviewcontroller_ios.go\"), `package main\n\n// #cgo CFLAGS: -DGLES_SILENCE_DEPRECATION\nimport \"C\"`); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"java\":\n\t\t\t// Android\n\t\t\tdir := filepath.Join(strings.Split(*javaPkg, \".\")...)\n\t\t\tdir = filepath.Join(dir, prefixLower)\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenView.java\"), replacePrefixes(viewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenSurfaceView.java\"), replacePrefixes(surfaceViewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"go\":\n\t\t\t// Do nothing.\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unsupported language: %s\", lang))\n\t\t}\n\t}\n\n\treturn nil\n}\n\nconst objcM = `// Code generated by ebitenmobile. DO NOT EDIT.\n\n//go:build ios\n// +build ios\n\n#import \n\n#import \n#import \n#import \n\n#import \"Ebitenmobileview.objc.h\"\n\n@interface {{.PrefixUpper}}EbitenViewController : UIViewController\n@end\n\n@implementation {{.PrefixUpper}}EbitenViewController {\n UIView* metalView_;\n GLKView* glkView_;\n bool started_;\n bool active_;\n bool error_;\n CADisplayLink* displayLink_;\n bool explicitRendering_;\n}\n\n- (UIView*)metalView {\n if (!metalView_) {\n metalView_ = [[UIView alloc] init];\n metalView_.multipleTouchEnabled = YES;\n }\n return metalView_;\n}\n\n- (GLKView*)glkView {\n if (!glkView_) {\n glkView_ = [[GLKView alloc] init];\n glkView_.multipleTouchEnabled = YES;\n }\n return glkView_;\n}\n\n- (void)viewDidLoad {\n [super viewDidLoad];\n\n if (!started_) {\n @synchronized(self) {\n active_ = true;\n }\n started_ = true;\n }\n\n if (EbitenmobileviewIsGL()) {\n self.glkView.delegate = (id)(self);\n [self.view addSubview: self.glkView];\n\n EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];\n [self glkView].context = context;\n\t\n [EAGLContext setCurrentContext:context];\n } else {\n [self.view addSubview: self.metalView];\n EbitenmobileviewSetUIView((uintptr_t)(self.metalView));\n }\n\n displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];\n [displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];\n EbitenmobileviewSetRenderRequester(self);\n}\n\n- (void)viewWillLayoutSubviews {\n CGRect viewRect = [[self view] frame];\n if (EbitenmobileviewIsGL()) {\n [[self glkView] setFrame:viewRect];\n } else {\n [[self metalView] setFrame:viewRect];\n }\n}\n\n- (void)viewDidLayoutSubviews {\n [super viewDidLayoutSubviews];\n CGRect viewRect = [[self view] frame];\n\n EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height);\n}\n\n- (void)didReceiveMemoryWarning {\n [super didReceiveMemoryWarning];\n // Dispose of any resources that can be recreated.\n // TODO: Notify this to Go world?\n}\n\n- (void)drawFrame{\n @synchronized(self) {\n if (!active_) {\n return;\n }\n\n if (EbitenmobileviewIsGL()) {\n [[self glkView] setNeedsDisplay];\n } else {\n [self updateEbiten];\n }\n\n if (explicitRendering_) {\n [displayLink_ setPaused:YES];\n }\n }\n}\n\n- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {\n @synchronized(self) {\n [self updateEbiten];\n }\n}\n\n- (void)updateEbiten {\n if (error_) {\n return;\n }\n NSError* err = nil;\n EbitenmobileviewUpdate(&err);\n if (err != nil) {\n [self performSelectorOnMainThread:@selector(onErrorOnGameUpdate:)\n withObject:err\n waitUntilDone:NO];\n error_ = true;\n }\n}\n\n- (void)onErrorOnGameUpdate:(NSError*)err {\n NSLog(@\"Error: %@\", err);\n}\n\n- (void)updateTouches:(NSSet*)touches {\n for (UITouch* touch in touches) {\n if (EbitenmobileviewIsGL()) {\n if (touch.view != [self glkView]) {\n continue;\n }\n } else {\n if (touch.view != [self metalView]) {\n continue;\n }\n }\n CGPoint location = [touch locationInView:touch.view];\n EbitenmobileviewUpdateTouchesOnIOS(touch.phase, (uintptr_t)touch, location.x, location.y);\n }\n}\n\n- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)suspendGame {\n NSAssert(started_, @\"suspendGame must not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = false;\n NSError* err = nil;\n EbitenmobileviewSuspend(&err);\n if (err != nil) {\n [self onErrorOnGameUpdate:err];\n }\n }\n}\n\n- (void)resumeGame {\n NSAssert(started_, @\"resumeGame must not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = true;\n NSError* err = nil;\n EbitenmobileviewResume(&err);\n if (err != nil) {\n [self onErrorOnGameUpdate:err];\n }\n }\n}\n\n- (void)setExplicitRenderingMode:(BOOL)explicitRendering {\n @synchronized(self) {\n explicitRendering_ = explicitRendering;\n if (explicitRendering_) {\n [displayLink_ setPaused:YES];\n }\n }\n}\n\n- (void)requestRenderIfNeeded {\n @synchronized(self) {\n if (explicitRendering_) {\n // Resume the callback temporarily.\n // This is paused again soon in drawFrame.\n [displayLink_ setPaused:NO];\n }\n }\n}\n\n@end\n`\n\nconst viewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.hardware.input.InputManager;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.util.DisplayMetrics;\nimport android.util.Log;\nimport android.view.Display;\nimport android.view.KeyEvent;\nimport android.view.InputDevice;\nimport android.view.MotionEvent;\nimport android.view.ViewGroup;\nimport android.view.WindowManager;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\n\npublic class EbitenView extends ViewGroup implements InputManager.InputDeviceListener {\n private static double pxToDp(double x) {\n return x / Ebitenmobileview.deviceScale();\n }\n\n public EbitenView(Context context) {\n super(context);\n initialize(context);\n }\n\n public EbitenView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize(context);\n }\n\n private void initialize(Context context) {\n this.ebitenSurfaceView = new EbitenSurfaceView(getContext());\n LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);\n addView(this.ebitenSurfaceView, params);\n\n this.inputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);\n this.inputManager.registerInputDeviceListener(this, null);\n for (int id : this.inputManager.getInputDeviceIds()) {\n this.onInputDeviceAdded(id);\n }\n }\n\n @Override\n protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n this.ebitenSurfaceView.layout(0, 0, right - left, bottom - top);\n double widthInDp = pxToDp(right - left);\n double heightInDp = pxToDp(bottom - top);\n Ebitenmobileview.layout(widthInDp, heightInDp);\n }\n\n @Override\n public boolean onKeyDown(int keyCode, KeyEvent event) {\n Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar(), event.getSource(), event.getDeviceId());\n return true;\n }\n\n @Override\n public boolean onKeyUp(int keyCode, KeyEvent event) {\n Ebitenmobileview.onKeyUpOnAndroid(keyCode, event.getSource(), event.getDeviceId());\n return true;\n }\n\n @Override\n public boolean onTouchEvent(MotionEvent e) {\n // getActionIndex returns a valid value only for the action whose index is the returned value of getActionIndex (#2220).\n // See https://developer.android.com/reference/android/view/MotionEvent#getActionMasked().\n // For other pointers, treat their actions as MotionEvent.ACTION_MOVE.\n int touchIndex = e.getActionIndex();\n for (int i = 0; i < e.getPointerCount(); i++) {\n int id = e.getPointerId(i);\n int x = (int)e.getX(i);\n int y = (int)e.getY(i);\n int action = (i == touchIndex) ? e.getActionMasked() : MotionEvent.ACTION_MOVE;\n Ebitenmobileview.updateTouchesOnAndroid(action, id, (int)pxToDp(x), (int)pxToDp(y));\n }\n return true;\n }\n\n // The order must be the same as mobile/ebitenmobileview/input_android.go.\n static int[] gamepadButtons = {\n KeyEvent.KEYCODE_BUTTON_A,\n KeyEvent.KEYCODE_BUTTON_B,\n KeyEvent.KEYCODE_BUTTON_C,\n KeyEvent.KEYCODE_BUTTON_X,\n KeyEvent.KEYCODE_BUTTON_Y,\n KeyEvent.KEYCODE_BUTTON_Z,\n KeyEvent.KEYCODE_BUTTON_L1,\n KeyEvent.KEYCODE_BUTTON_R1,\n KeyEvent.KEYCODE_BUTTON_L2,\n KeyEvent.KEYCODE_BUTTON_R2,\n KeyEvent.KEYCODE_BUTTON_THUMBL,\n KeyEvent.KEYCODE_BUTTON_THUMBR,\n KeyEvent.KEYCODE_BUTTON_START,\n KeyEvent.KEYCODE_BUTTON_SELECT,\n KeyEvent.KEYCODE_BUTTON_MODE,\n KeyEvent.KEYCODE_BUTTON_1,\n KeyEvent.KEYCODE_BUTTON_2,\n KeyEvent.KEYCODE_BUTTON_3,\n KeyEvent.KEYCODE_BUTTON_4,\n KeyEvent.KEYCODE_BUTTON_5,\n KeyEvent.KEYCODE_BUTTON_6,\n KeyEvent.KEYCODE_BUTTON_7,\n KeyEvent.KEYCODE_BUTTON_8,\n KeyEvent.KEYCODE_BUTTON_9,\n KeyEvent.KEYCODE_BUTTON_10,\n KeyEvent.KEYCODE_BUTTON_11,\n KeyEvent.KEYCODE_BUTTON_12,\n KeyEvent.KEYCODE_BUTTON_13,\n KeyEvent.KEYCODE_BUTTON_14,\n KeyEvent.KEYCODE_BUTTON_15,\n KeyEvent.KEYCODE_BUTTON_16,\n };\n\n // The order must be the same as mobile/ebitenmobileview/input_android.go.\n static int[] axes = {\n MotionEvent.AXIS_X,\n MotionEvent.AXIS_Y,\n MotionEvent.AXIS_Z,\n MotionEvent.AXIS_RX,\n MotionEvent.AXIS_RY,\n MotionEvent.AXIS_RZ,\n MotionEvent.AXIS_HAT_X,\n MotionEvent.AXIS_HAT_Y,\n MotionEvent.AXIS_LTRIGGER,\n MotionEvent.AXIS_RTRIGGER,\n MotionEvent.AXIS_THROTTLE,\n MotionEvent.AXIS_RUDDER,\n MotionEvent.AXIS_WHEEL,\n MotionEvent.AXIS_GAS,\n MotionEvent.AXIS_BRAKE,\n MotionEvent.AXIS_GENERIC_1,\n MotionEvent.AXIS_GENERIC_2,\n MotionEvent.AXIS_GENERIC_3,\n MotionEvent.AXIS_GENERIC_4,\n MotionEvent.AXIS_GENERIC_5,\n MotionEvent.AXIS_GENERIC_6,\n MotionEvent.AXIS_GENERIC_7,\n MotionEvent.AXIS_GENERIC_8,\n MotionEvent.AXIS_GENERIC_9,\n MotionEvent.AXIS_GENERIC_10,\n MotionEvent.AXIS_GENERIC_11,\n MotionEvent.AXIS_GENERIC_12,\n MotionEvent.AXIS_GENERIC_13,\n MotionEvent.AXIS_GENERIC_14,\n MotionEvent.AXIS_GENERIC_15,\n MotionEvent.AXIS_GENERIC_16,\n };\n\n @Override\n public boolean onGenericMotionEvent(MotionEvent event) {\n if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {\n return super.onGenericMotionEvent(event);\n }\n if (event.getAction() != MotionEvent.ACTION_MOVE) {\n return super.onGenericMotionEvent(event);\n }\n InputDevice inputDevice = this.inputManager.getInputDevice(event.getDeviceId());\n for (int axis : axes) {\n InputDevice.MotionRange motionRange = inputDevice.getMotionRange(axis, event.getSource());\n float value = 0.0f;\n if (motionRange != null) {\n value = event.getAxisValue(axis);\n if (Math.abs(value) <= motionRange.getFlat()) {\n value = 0.0f;\n }\n }\n Ebitenmobileview.onGamepadAxesOrHatsChanged(event.getDeviceId(), axis, value);\n }\n return true;\n }\n\n @Override\n public void onInputDeviceAdded(int deviceId) {\n InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);\n // The InputDevice can be null on some deivces (#1342).\n if (inputDevice == null) {\n return;\n }\n\n // A fingerprint reader is unexpectedly recognized as a joystick. Skip this (#1542).\n if (inputDevice.getName().equals(\"uinput-fpc\")) {\n return;\n }\n\n int sources = inputDevice.getSources();\n if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&\n (sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {\n return;\n }\n\n int naxes = 0;\n int nhats2 = 0;\n for (int i = 0; i < axes.length; i++) {\n InputDevice.MotionRange range = inputDevice.getMotionRange(axes[i], InputDevice.SOURCE_JOYSTICK);\n if (range == null) {\n continue;\n }\n if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {\n nhats2++;\n } else {\n naxes++;\n }\n }\n\n String descriptor = inputDevice.getDescriptor();\n int vendorId = inputDevice.getVendorId();\n int productId = inputDevice.getProductId();\n\n // These values are required to calculate SDL's GUID.\n int buttonMask = getButtonMask(inputDevice, nhats2/2);\n int axisMask = getAxisMask(inputDevice);\n\n Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), naxes, nhats2/2, descriptor, vendorId, productId, buttonMask, axisMask);\n }\n\n // The implementation is copied from SDL:\n // https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L308\n private int getButtonMask(InputDevice joystickDevice, int nhats) {\n int buttonMask = 0;\n int[] keys = new int[] {\n KeyEvent.KEYCODE_BUTTON_A,\n KeyEvent.KEYCODE_BUTTON_B,\n KeyEvent.KEYCODE_BUTTON_X,\n KeyEvent.KEYCODE_BUTTON_Y,\n KeyEvent.KEYCODE_BACK,\n KeyEvent.KEYCODE_BUTTON_MODE,\n KeyEvent.KEYCODE_BUTTON_START,\n KeyEvent.KEYCODE_BUTTON_THUMBL,\n KeyEvent.KEYCODE_BUTTON_THUMBR,\n KeyEvent.KEYCODE_BUTTON_L1,\n KeyEvent.KEYCODE_BUTTON_R1,\n KeyEvent.KEYCODE_DPAD_UP,\n KeyEvent.KEYCODE_DPAD_DOWN,\n KeyEvent.KEYCODE_DPAD_LEFT,\n KeyEvent.KEYCODE_DPAD_RIGHT,\n KeyEvent.KEYCODE_BUTTON_SELECT,\n KeyEvent.KEYCODE_DPAD_CENTER,\n\n // These don't map into any SDL controller buttons directly\n KeyEvent.KEYCODE_BUTTON_L2,\n KeyEvent.KEYCODE_BUTTON_R2,\n KeyEvent.KEYCODE_BUTTON_C,\n KeyEvent.KEYCODE_BUTTON_Z,\n KeyEvent.KEYCODE_BUTTON_1,\n KeyEvent.KEYCODE_BUTTON_2,\n KeyEvent.KEYCODE_BUTTON_3,\n KeyEvent.KEYCODE_BUTTON_4,\n KeyEvent.KEYCODE_BUTTON_5,\n KeyEvent.KEYCODE_BUTTON_6,\n KeyEvent.KEYCODE_BUTTON_7,\n KeyEvent.KEYCODE_BUTTON_8,\n KeyEvent.KEYCODE_BUTTON_9,\n KeyEvent.KEYCODE_BUTTON_10,\n KeyEvent.KEYCODE_BUTTON_11,\n KeyEvent.KEYCODE_BUTTON_12,\n KeyEvent.KEYCODE_BUTTON_13,\n KeyEvent.KEYCODE_BUTTON_14,\n KeyEvent.KEYCODE_BUTTON_15,\n KeyEvent.KEYCODE_BUTTON_16,\n };\n int[] masks = new int[] {\n (1 << 0), // A -> A\n (1 << 1), // B -> B\n (1 << 2), // X -> X\n (1 << 3), // Y -> Y\n (1 << 4), // BACK -> BACK\n (1 << 5), // MODE -> GUIDE\n (1 << 6), // START -> START\n (1 << 7), // THUMBL -> LEFTSTICK\n (1 << 8), // THUMBR -> RIGHTSTICK\n (1 << 9), // L1 -> LEFTSHOULDER\n (1 << 10), // R1 -> RIGHTSHOULDER\n (1 << 11), // DPAD_UP -> DPAD_UP\n (1 << 12), // DPAD_DOWN -> DPAD_DOWN\n (1 << 13), // DPAD_LEFT -> DPAD_LEFT\n (1 << 14), // DPAD_RIGHT -> DPAD_RIGHT\n (1 << 4), // SELECT -> BACK\n (1 << 0), // DPAD_CENTER -> A\n (1 << 15), // L2 -> ??\n (1 << 16), // R2 -> ??\n (1 << 17), // C -> ??\n (1 << 18), // Z -> ??\n (1 << 20), // 1 -> ??\n (1 << 21), // 2 -> ??\n (1 << 22), // 3 -> ??\n (1 << 23), // 4 -> ??\n (1 << 24), // 5 -> ??\n (1 << 25), // 6 -> ??\n (1 << 26), // 7 -> ??\n (1 << 27), // 8 -> ??\n (1 << 28), // 9 -> ??\n (1 << 29), // 10 -> ??\n (1 << 30), // 11 -> ??\n (1 << 31), // 12 -> ??\n // We're out of room...\n 0xFFFFFFFF, // 13 -> ??\n 0xFFFFFFFF, // 14 -> ??\n 0xFFFFFFFF, // 15 -> ??\n 0xFFFFFFFF, // 16 -> ??\n };\n boolean[] hasKeys = joystickDevice.hasKeys(keys);\n for (int i = 0; i < keys.length; ++i) {\n if (hasKeys[i]) {\n buttonMask |= masks[i];\n }\n }\n // https://github.com/libsdl-org/SDL/blob/47f2373dc13b66c48bf4024fcdab53cd0bdd59bb/src/joystick/android/SDL_sysjoystick.c#L360-L367\n if (nhats > 0) {\n // Add Dpad buttons.\n buttonMask |= 1 << 11;\n buttonMask |= 1 << 12;\n buttonMask |= 1 << 13;\n buttonMask |= 1 << 14;\n }\n return buttonMask;\n }\n\n private int getAxisMask(InputDevice joystickDevice) {\n final int SDL_CONTROLLER_AXIS_LEFTX = 0;\n final int SDL_CONTROLLER_AXIS_LEFTY = 1;\n final int SDL_CONTROLLER_AXIS_RIGHTX = 2;\n final int SDL_CONTROLLER_AXIS_RIGHTY = 3;\n final int SDL_CONTROLLER_AXIS_TRIGGERLEFT = 4;\n final int SDL_CONTROLLER_AXIS_TRIGGERRIGHT = 5;\n\n int naxes = 0;\n for (InputDevice.MotionRange range : joystickDevice.getMotionRanges()) {\n if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {\n if (range.getAxis() != MotionEvent.AXIS_HAT_X && range.getAxis() != MotionEvent.AXIS_HAT_Y) {\n naxes++;\n }\n }\n }\n // The variable is_accelerometer seems always false, then skip the checking:\n // https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L207\n int axisMask = 0;\n if (naxes >= 2) {\n axisMask |= ((1 << SDL_CONTROLLER_AXIS_LEFTX) | (1 << SDL_CONTROLLER_AXIS_LEFTY));\n }\n if (naxes >= 4) {\n axisMask |= ((1 << SDL_CONTROLLER_AXIS_RIGHTX) | (1 << SDL_CONTROLLER_AXIS_RIGHTY));\n }\n if (naxes >= 6) {\n axisMask |= ((1 << SDL_CONTROLLER_AXIS_TRIGGERLEFT) | (1 << SDL_CONTROLLER_AXIS_TRIGGERRIGHT));\n }\n return axisMask;\n }\n\n @Override\n public void onInputDeviceChanged(int deviceId) {\n // Do nothing.\n }\n\n @Override\n public void onInputDeviceRemoved(int deviceId) {\n // Do not call inputManager.getInputDevice(), which returns null (#1185).\n Ebitenmobileview.onInputDeviceRemoved(deviceId);\n }\n\n // suspendGame suspends the game.\n // It is recommended to call this when the application is being suspended e.g.,\n // Activity's onPause is called.\n public void suspendGame() {\n this.inputManager.unregisterInputDeviceListener(this);\n this.ebitenSurfaceView.onPause();\n try {\n Ebitenmobileview.suspend();\n } catch (final Exception e) {\n onErrorOnGameUpdate(e);\n }\n }\n\n // resumeGame resumes the game.\n // It is recommended to call this when the application is being resumed e.g.,\n // Activity's onResume is called.\n public void resumeGame() {\n this.inputManager.registerInputDeviceListener(this, null);\n this.ebitenSurfaceView.onResume();\n try {\n Ebitenmobileview.resume();\n } catch (final Exception e) {\n onErrorOnGameUpdate(e);\n }\n }\n\n // onErrorOnGameUpdate is called on the main thread when an error happens when updating a game.\n // You can define your own error handler, e.g., using Crashlytics, by overriding this method.\n protected void onErrorOnGameUpdate(Exception e) {\n Log.e(\"Go\", e.toString());\n }\n\n private EbitenSurfaceView ebitenSurfaceView;\n private InputManager inputManager;\n}\n`\n\nconst surfaceViewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.opengl.GLSurfaceView;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.util.Log;\n\nimport javax.microedition.khronos.egl.EGLConfig;\nimport javax.microedition.khronos.opengles.GL10;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\nimport {{.JavaPkg}}.ebitenmobileview.RenderRequester;\nimport {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;\n\nclass EbitenSurfaceView extends GLSurfaceView implements RenderRequester {\n\n private class EbitenRenderer implements GLSurfaceView.Renderer {\n\n private boolean errored_ = false;\n\n @Override\n public void onDrawFrame(GL10 gl) {\n if (errored_) {\n return;\n }\n try {\n Ebitenmobileview.update();\n } catch (final Exception e) {\n new Handler(Looper.getMainLooper()).post(new Runnable() {\n @Override\n public void run() {\n onErrorOnGameUpdate(e);\n }\n });\n errored_ = true;\n }\n }\n\n @Override\n public void onSurfaceCreated(GL10 gl, EGLConfig config) {\n Ebitenmobileview.onContextLost();\n }\n\n @Override\n public void onSurfaceChanged(GL10 gl, int width, int height) {\n }\n }\n\n public EbitenSurfaceView(Context context) {\n super(context);\n initialize();\n }\n\n public EbitenSurfaceView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize();\n }\n\n private void initialize() {\n setEGLContextClientVersion(2);\n setEGLConfigChooser(8, 8, 8, 8, 0, 0);\n setRenderer(new EbitenRenderer());\n Ebitenmobileview.setRenderRequester(this);\n }\n\n private void onErrorOnGameUpdate(Exception e) {\n ((EbitenView)getParent()).onErrorOnGameUpdate(e);\n }\n\n @Override\n public synchronized void setExplicitRenderingMode(boolean explicitRendering) {\n if (explicitRendering) {\n setRenderMode(RENDERMODE_WHEN_DIRTY);\n } else {\n setRenderMode(RENDERMODE_CONTINUOUSLY);\n }\n }\n\n @Override\n public synchronized void requestRenderIfNeeded() {\n if (getRenderMode() == RENDERMODE_WHEN_DIRTY) {\n requestRender();\n }\n }\n}\n`\n") +var gobindsrc = []byte("// Copyright 2019 The Ebiten Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n//go:build ebitenmobilegobind\n// +build ebitenmobilegobind\n\n// gobind is a wrapper of the original gobind. This command adds extra files like a view controller.\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"golang.org/x/tools/go/packages\"\n)\n\nvar (\n\tlang = flag.String(\"lang\", \"\", \"\")\n\toutdir = flag.String(\"outdir\", \"\", \"\")\n\tjavaPkg = flag.String(\"javapkg\", \"\", \"\")\n\tprefix = flag.String(\"prefix\", \"\", \"\")\n\tbootclasspath = flag.String(\"bootclasspath\", \"\", \"\")\n\tclasspath = flag.String(\"classpath\", \"\", \"\")\n\ttags = flag.String(\"tags\", \"\", \"\")\n)\n\nvar usage = `The Gobind tool generates Java language bindings for Go.\n\nFor usage details, see doc.go.`\n\nfunc main() {\n\tflag.Parse()\n\tif err := run(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc invokeOriginalGobind(lang string) (pkgName string, err error) {\n\tcmd := exec.Command(\"gobind-original\", os.Args[1:]...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcfgtags := strings.Join(strings.Split(*tags, \",\"), \" \")\n\tcfg := &packages.Config{}\n\tswitch lang {\n\tcase \"java\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=android\")\n\tcase \"objc\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=darwin\")\n\t\tif cfgtags != \"\" {\n\t\t\tcfgtags += \" \"\n\t\t}\n\t\tcfgtags += \"ios\"\n\t}\n\tcfg.BuildFlags = []string{\"-tags\", cfgtags}\n\tpkgs, err := packages.Load(cfg, flag.Args()[0])\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn pkgs[0].Name, nil\n}\n\nfunc run() error {\n\twriteFile := func(filename string, content string) error {\n\t\tif err := ioutil.WriteFile(filepath.Join(*outdir, filename), []byte(content), 0644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Add additional files.\n\tlangs := strings.Split(*lang, \",\")\n\tfor _, lang := range langs {\n\t\tpkgName, err := invokeOriginalGobind(lang)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprefixLower := *prefix + pkgName\n\t\tprefixUpper := strings.Title(*prefix) + strings.Title(pkgName)\n\t\treplacePrefixes := func(content string) string {\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixUpper}}\", prefixUpper)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixLower}}\", prefixLower)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.JavaPkg}}\", *javaPkg)\n\t\t\treturn content\n\t\t}\n\n\t\tswitch lang {\n\t\tcase \"objc\":\n\t\t\t// iOS\n\t\t\tif err := writeFile(filepath.Join(\"src\", \"gobind\", prefixLower+\"ebitenviewcontroller_ios.m\"), replacePrefixes(objcM)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := writeFile(filepath.Join(\"src\", \"gobind\", prefixLower+\"ebitenviewcontroller_ios.go\"), `package main\n\n// #cgo CFLAGS: -DGLES_SILENCE_DEPRECATION\nimport \"C\"`); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"java\":\n\t\t\t// Android\n\t\t\tdir := filepath.Join(strings.Split(*javaPkg, \".\")...)\n\t\t\tdir = filepath.Join(dir, prefixLower)\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenView.java\"), replacePrefixes(viewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenSurfaceView.java\"), replacePrefixes(surfaceViewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"go\":\n\t\t\t// Do nothing.\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unsupported language: %s\", lang))\n\t\t}\n\t}\n\n\treturn nil\n}\n\nconst objcM = `// Code generated by ebitenmobile. DO NOT EDIT.\n\n//go:build ios\n// +build ios\n\n#import \n\n#import \n#import \n#import \n\n#import \"Ebitenmobileview.objc.h\"\n\n@interface {{.PrefixUpper}}EbitenViewController : UIViewController\n@end\n\n@implementation {{.PrefixUpper}}EbitenViewController {\n UIView* metalView_;\n GLKView* glkView_;\n bool started_;\n bool active_;\n bool error_;\n CADisplayLink* displayLink_;\n bool explicitRendering_;\n}\n\n- (UIView*)metalView {\n if (!metalView_) {\n metalView_ = [[UIView alloc] init];\n metalView_.multipleTouchEnabled = YES;\n }\n return metalView_;\n}\n\n- (GLKView*)glkView {\n if (!glkView_) {\n glkView_ = [[GLKView alloc] init];\n glkView_.multipleTouchEnabled = YES;\n }\n return glkView_;\n}\n\n- (void)viewDidLoad {\n [super viewDidLoad];\n\n if (!started_) {\n @synchronized(self) {\n active_ = true;\n }\n started_ = true;\n }\n\n if (EbitenmobileviewIsGL()) {\n self.glkView.delegate = (id)(self);\n [self.view addSubview: self.glkView];\n\n EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];\n [self glkView].context = context;\n\t\n [EAGLContext setCurrentContext:context];\n } else {\n [self.view addSubview: self.metalView];\n EbitenmobileviewSetUIView((uintptr_t)(self.metalView));\n }\n\n displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];\n [displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];\n EbitenmobileviewSetRenderRequester(self);\n}\n\n- (void)viewWillLayoutSubviews {\n CGRect viewRect = [[self view] frame];\n if (EbitenmobileviewIsGL()) {\n [[self glkView] setFrame:viewRect];\n } else {\n [[self metalView] setFrame:viewRect];\n }\n}\n\n- (void)viewDidLayoutSubviews {\n [super viewDidLayoutSubviews];\n CGRect viewRect = [[self view] frame];\n\n EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height);\n}\n\n- (void)didReceiveMemoryWarning {\n [super didReceiveMemoryWarning];\n // Dispose of any resources that can be recreated.\n // TODO: Notify this to Go world?\n}\n\n- (void)drawFrame{\n @synchronized(self) {\n if (!active_) {\n return;\n }\n\n if (EbitenmobileviewIsGL()) {\n [[self glkView] setNeedsDisplay];\n } else {\n [self updateEbiten];\n }\n\n if (explicitRendering_) {\n [displayLink_ setPaused:YES];\n }\n }\n}\n\n- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {\n @synchronized(self) {\n [self updateEbiten];\n }\n}\n\n- (void)updateEbiten {\n if (error_) {\n return;\n }\n NSError* err = nil;\n EbitenmobileviewUpdate(&err);\n if (err != nil) {\n [self performSelectorOnMainThread:@selector(onErrorOnGameUpdate:)\n withObject:err\n waitUntilDone:NO];\n error_ = true;\n }\n}\n\n- (void)onErrorOnGameUpdate:(NSError*)err {\n NSLog(@\"Error: %@\", err);\n}\n\n- (void)updateTouches:(NSSet*)touches {\n for (UITouch* touch in touches) {\n if (EbitenmobileviewIsGL()) {\n if (touch.view != [self glkView]) {\n continue;\n }\n } else {\n if (touch.view != [self metalView]) {\n continue;\n }\n }\n CGPoint location = [touch locationInView:touch.view];\n EbitenmobileviewUpdateTouchesOnIOS(touch.phase, (uintptr_t)touch, location.x, location.y);\n }\n}\n\n- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)suspendGame {\n NSAssert(started_, @\"suspendGame must not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = false;\n NSError* err = nil;\n EbitenmobileviewSuspend(&err);\n if (err != nil) {\n [self onErrorOnGameUpdate:err];\n }\n }\n}\n\n- (void)resumeGame {\n NSAssert(started_, @\"resumeGame must not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = true;\n NSError* err = nil;\n EbitenmobileviewResume(&err);\n if (err != nil) {\n [self onErrorOnGameUpdate:err];\n }\n }\n}\n\n- (void)setExplicitRenderingMode:(BOOL)explicitRendering {\n @synchronized(self) {\n explicitRendering_ = explicitRendering;\n if (explicitRendering_) {\n [displayLink_ setPaused:YES];\n }\n }\n}\n\n- (void)requestRenderIfNeeded {\n @synchronized(self) {\n if (explicitRendering_) {\n // Resume the callback temporarily.\n // This is paused again soon in drawFrame.\n [displayLink_ setPaused:NO];\n }\n }\n}\n\n@end\n`\n\nconst viewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport android.content.Context;\nimport android.hardware.input.InputManager;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.util.DisplayMetrics;\nimport android.util.Log;\nimport android.view.Display;\nimport android.view.KeyEvent;\nimport android.view.InputDevice;\nimport android.view.MotionEvent;\nimport android.view.ViewGroup;\nimport android.view.WindowManager;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\n\npublic class EbitenView extends ViewGroup implements InputManager.InputDeviceListener {\n static class Gamepad {\n public int deviceId;\n public ArrayList axes;\n public ArrayList hats;\n }\n\n // See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L154-L173\n static class RangeComparator implements Comparator {\n @Override\n public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {\n int arg0Axis = arg0.getAxis();\n int arg1Axis = arg1.getAxis();\n if (arg0Axis == MotionEvent.AXIS_GAS) {\n arg0Axis = MotionEvent.AXIS_BRAKE;\n } else if (arg0Axis == MotionEvent.AXIS_BRAKE) {\n arg0Axis = MotionEvent.AXIS_GAS;\n }\n if (arg1Axis == MotionEvent.AXIS_GAS) {\n arg1Axis = MotionEvent.AXIS_BRAKE;\n } else if (arg1Axis == MotionEvent.AXIS_BRAKE) {\n arg1Axis = MotionEvent.AXIS_GAS;\n }\n return arg0Axis - arg1Axis;\n }\n }\n\n private static double pxToDp(double x) {\n return x / Ebitenmobileview.deviceScale();\n }\n\n public EbitenView(Context context) {\n super(context);\n initialize(context);\n }\n\n public EbitenView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize(context);\n }\n\n private void initialize(Context context) {\n this.gamepads = new ArrayList();\n\n this.ebitenSurfaceView = new EbitenSurfaceView(getContext());\n LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);\n addView(this.ebitenSurfaceView, params);\n\n this.inputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);\n this.inputManager.registerInputDeviceListener(this, null);\n for (int id : this.inputManager.getInputDeviceIds()) {\n this.onInputDeviceAdded(id);\n }\n }\n\n @Override\n protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n this.ebitenSurfaceView.layout(0, 0, right - left, bottom - top);\n double widthInDp = pxToDp(right - left);\n double heightInDp = pxToDp(bottom - top);\n Ebitenmobileview.layout(widthInDp, heightInDp);\n }\n\n @Override\n public boolean onKeyDown(int keyCode, KeyEvent event) {\n Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar(), event.getSource(), event.getDeviceId());\n return true;\n }\n\n @Override\n public boolean onKeyUp(int keyCode, KeyEvent event) {\n Ebitenmobileview.onKeyUpOnAndroid(keyCode, event.getSource(), event.getDeviceId());\n return true;\n }\n\n @Override\n public boolean onTouchEvent(MotionEvent e) {\n // getActionIndex returns a valid value only for the action whose index is the returned value of getActionIndex (#2220).\n // See https://developer.android.com/reference/android/view/MotionEvent#getActionMasked().\n // For other pointers, treat their actions as MotionEvent.ACTION_MOVE.\n int touchIndex = e.getActionIndex();\n for (int i = 0; i < e.getPointerCount(); i++) {\n int id = e.getPointerId(i);\n int x = (int)e.getX(i);\n int y = (int)e.getY(i);\n int action = (i == touchIndex) ? e.getActionMasked() : MotionEvent.ACTION_MOVE;\n Ebitenmobileview.updateTouchesOnAndroid(action, id, (int)pxToDp(x), (int)pxToDp(y));\n }\n return true;\n }\n\n private Gamepad getGamepad(int deviceId) {\n for (Gamepad gamepad : this.gamepads) {\n if (gamepad.deviceId == deviceId) {\n return gamepad;\n }\n }\n return null;\n }\n\n @Override\n public boolean onGenericMotionEvent(MotionEvent event) {\n if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {\n return super.onGenericMotionEvent(event);\n }\n if (event.getAction() != MotionEvent.ACTION_MOVE) {\n return super.onGenericMotionEvent(event);\n }\n\n // See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L256-L277\n Gamepad gamepad = this.getGamepad(event.getDeviceId());\n if (gamepad == null) {\n return true;\n }\n\n int actionPointerIndex = event.getActionIndex();\n for (int i = 0; i < gamepad.axes.size(); i++) {\n InputDevice.MotionRange range = gamepad.axes.get(i);\n float axisValue = event.getAxisValue(range.getAxis(), actionPointerIndex);\n float value = (axisValue - range.getMin()) / range.getRange() * 2.0f - 1.0f;\n Ebitenmobileview.onGamepadAxisChanged(gamepad.deviceId, i, value);\n }\n for (int i = 0; i < gamepad.hats.size() / 2; i++) {\n int hatX = Math.round(event.getAxisValue(gamepad.hats.get(2*i).getAxis(), actionPointerIndex));\n int hatY = Math.round(event.getAxisValue(gamepad.hats.get(2*i+1).getAxis(), actionPointerIndex));\n Ebitenmobileview.onGamepadHatChanged(gamepad.deviceId, i, hatX, hatY);\n }\n return true;\n }\n\n @Override\n public void onInputDeviceAdded(int deviceId) {\n InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);\n // The InputDevice can be null on some deivces (#1342).\n if (inputDevice == null) {\n return;\n }\n\n // A fingerprint reader is unexpectedly recognized as a joystick. Skip this (#1542).\n if (inputDevice.getName().equals(\"uinput-fpc\")) {\n return;\n }\n\n int sources = inputDevice.getSources();\n if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&\n (sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {\n return;\n }\n\n // See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L182-L216\n List ranges = inputDevice.getMotionRanges();\n Collections.sort(ranges, new RangeComparator());\n\n Gamepad gamepad = new Gamepad();\n gamepad.deviceId = deviceId;\n gamepad.axes = new ArrayList();\n gamepad.hats = new ArrayList();\n for (InputDevice.MotionRange range : ranges) {\n if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {\n gamepad.hats.add(range);\n } else {\n gamepad.axes.add(range);\n }\n }\n this.gamepads.add(gamepad);\n\n String descriptor = inputDevice.getDescriptor();\n int vendorId = inputDevice.getVendorId();\n int productId = inputDevice.getProductId();\n\n // These values are required to calculate SDL's GUID.\n int buttonMask = getButtonMask(inputDevice, gamepad.hats.size()/2);\n int axisMask = getAxisMask(inputDevice);\n\n Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), gamepad.axes.size(), gamepad.hats.size()/2, descriptor, vendorId, productId, buttonMask, axisMask);\n\n // Initialize the trigger axes values explicitly, or the initial button values would be 0.5 instead of 0.\n if (gamepad.axes.size() >= 6) {\n Ebitenmobileview.onGamepadAxisChanged(deviceId, 4, -1);\n Ebitenmobileview.onGamepadAxisChanged(deviceId, 5, -1);\n }\n }\n\n // The implementation is copied from SDL:\n // https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L308\n private static int getButtonMask(InputDevice joystickDevice, int nhats) {\n int buttonMask = 0;\n int[] keys = new int[] {\n KeyEvent.KEYCODE_BUTTON_A,\n KeyEvent.KEYCODE_BUTTON_B,\n KeyEvent.KEYCODE_BUTTON_X,\n KeyEvent.KEYCODE_BUTTON_Y,\n KeyEvent.KEYCODE_BACK,\n KeyEvent.KEYCODE_BUTTON_MODE,\n KeyEvent.KEYCODE_BUTTON_START,\n KeyEvent.KEYCODE_BUTTON_THUMBL,\n KeyEvent.KEYCODE_BUTTON_THUMBR,\n KeyEvent.KEYCODE_BUTTON_L1,\n KeyEvent.KEYCODE_BUTTON_R1,\n KeyEvent.KEYCODE_DPAD_UP,\n KeyEvent.KEYCODE_DPAD_DOWN,\n KeyEvent.KEYCODE_DPAD_LEFT,\n KeyEvent.KEYCODE_DPAD_RIGHT,\n KeyEvent.KEYCODE_BUTTON_SELECT,\n KeyEvent.KEYCODE_DPAD_CENTER,\n\n // These don't map into any SDL controller buttons directly\n KeyEvent.KEYCODE_BUTTON_L2,\n KeyEvent.KEYCODE_BUTTON_R2,\n KeyEvent.KEYCODE_BUTTON_C,\n KeyEvent.KEYCODE_BUTTON_Z,\n KeyEvent.KEYCODE_BUTTON_1,\n KeyEvent.KEYCODE_BUTTON_2,\n KeyEvent.KEYCODE_BUTTON_3,\n KeyEvent.KEYCODE_BUTTON_4,\n KeyEvent.KEYCODE_BUTTON_5,\n KeyEvent.KEYCODE_BUTTON_6,\n KeyEvent.KEYCODE_BUTTON_7,\n KeyEvent.KEYCODE_BUTTON_8,\n KeyEvent.KEYCODE_BUTTON_9,\n KeyEvent.KEYCODE_BUTTON_10,\n KeyEvent.KEYCODE_BUTTON_11,\n KeyEvent.KEYCODE_BUTTON_12,\n KeyEvent.KEYCODE_BUTTON_13,\n KeyEvent.KEYCODE_BUTTON_14,\n KeyEvent.KEYCODE_BUTTON_15,\n KeyEvent.KEYCODE_BUTTON_16,\n };\n int[] masks = new int[] {\n (1 << 0), // A -> A\n (1 << 1), // B -> B\n (1 << 2), // X -> X\n (1 << 3), // Y -> Y\n (1 << 4), // BACK -> BACK\n (1 << 5), // MODE -> GUIDE\n (1 << 6), // START -> START\n (1 << 7), // THUMBL -> LEFTSTICK\n (1 << 8), // THUMBR -> RIGHTSTICK\n (1 << 9), // L1 -> LEFTSHOULDER\n (1 << 10), // R1 -> RIGHTSHOULDER\n (1 << 11), // DPAD_UP -> DPAD_UP\n (1 << 12), // DPAD_DOWN -> DPAD_DOWN\n (1 << 13), // DPAD_LEFT -> DPAD_LEFT\n (1 << 14), // DPAD_RIGHT -> DPAD_RIGHT\n (1 << 4), // SELECT -> BACK\n (1 << 0), // DPAD_CENTER -> A\n (1 << 15), // L2 -> ??\n (1 << 16), // R2 -> ??\n (1 << 17), // C -> ??\n (1 << 18), // Z -> ??\n (1 << 20), // 1 -> ??\n (1 << 21), // 2 -> ??\n (1 << 22), // 3 -> ??\n (1 << 23), // 4 -> ??\n (1 << 24), // 5 -> ??\n (1 << 25), // 6 -> ??\n (1 << 26), // 7 -> ??\n (1 << 27), // 8 -> ??\n (1 << 28), // 9 -> ??\n (1 << 29), // 10 -> ??\n (1 << 30), // 11 -> ??\n (1 << 31), // 12 -> ??\n // We're out of room...\n 0xFFFFFFFF, // 13 -> ??\n 0xFFFFFFFF, // 14 -> ??\n 0xFFFFFFFF, // 15 -> ??\n 0xFFFFFFFF, // 16 -> ??\n };\n boolean[] hasKeys = joystickDevice.hasKeys(keys);\n for (int i = 0; i < keys.length; ++i) {\n if (hasKeys[i]) {\n buttonMask |= masks[i];\n }\n }\n // https://github.com/libsdl-org/SDL/blob/47f2373dc13b66c48bf4024fcdab53cd0bdd59bb/src/joystick/android/SDL_sysjoystick.c#L360-L367\n if (nhats > 0) {\n // Add Dpad buttons.\n buttonMask |= 1 << 11;\n buttonMask |= 1 << 12;\n buttonMask |= 1 << 13;\n buttonMask |= 1 << 14;\n }\n return buttonMask;\n }\n\n private static int getAxisMask(InputDevice joystickDevice) {\n final int SDL_CONTROLLER_AXIS_LEFTX = 0;\n final int SDL_CONTROLLER_AXIS_LEFTY = 1;\n final int SDL_CONTROLLER_AXIS_RIGHTX = 2;\n final int SDL_CONTROLLER_AXIS_RIGHTY = 3;\n final int SDL_CONTROLLER_AXIS_TRIGGERLEFT = 4;\n final int SDL_CONTROLLER_AXIS_TRIGGERRIGHT = 5;\n\n int naxes = 0;\n for (InputDevice.MotionRange range : joystickDevice.getMotionRanges()) {\n if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {\n if (range.getAxis() != MotionEvent.AXIS_HAT_X && range.getAxis() != MotionEvent.AXIS_HAT_Y) {\n naxes++;\n }\n }\n }\n // The variable is_accelerometer seems always false, then skip the checking:\n // https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L207\n int axisMask = 0;\n if (naxes >= 2) {\n axisMask |= ((1 << SDL_CONTROLLER_AXIS_LEFTX) | (1 << SDL_CONTROLLER_AXIS_LEFTY));\n }\n if (naxes >= 4) {\n axisMask |= ((1 << SDL_CONTROLLER_AXIS_RIGHTX) | (1 << SDL_CONTROLLER_AXIS_RIGHTY));\n }\n if (naxes >= 6) {\n axisMask |= ((1 << SDL_CONTROLLER_AXIS_TRIGGERLEFT) | (1 << SDL_CONTROLLER_AXIS_TRIGGERRIGHT));\n }\n return axisMask;\n }\n\n @Override\n public void onInputDeviceChanged(int deviceId) {\n // Do nothing.\n }\n\n @Override\n public void onInputDeviceRemoved(int deviceId) {\n // Do not call inputManager.getInputDevice(), which returns null (#1185).\n Ebitenmobileview.onInputDeviceRemoved(deviceId);\n this.gamepads.remove(this.getGamepad(deviceId));\n }\n\n // suspendGame suspends the game.\n // It is recommended to call this when the application is being suspended e.g.,\n // Activity's onPause is called.\n public void suspendGame() {\n this.inputManager.unregisterInputDeviceListener(this);\n this.ebitenSurfaceView.onPause();\n try {\n Ebitenmobileview.suspend();\n } catch (final Exception e) {\n onErrorOnGameUpdate(e);\n }\n }\n\n // resumeGame resumes the game.\n // It is recommended to call this when the application is being resumed e.g.,\n // Activity's onResume is called.\n public void resumeGame() {\n this.inputManager.registerInputDeviceListener(this, null);\n this.ebitenSurfaceView.onResume();\n try {\n Ebitenmobileview.resume();\n } catch (final Exception e) {\n onErrorOnGameUpdate(e);\n }\n }\n\n // onErrorOnGameUpdate is called on the main thread when an error happens when updating a game.\n // You can define your own error handler, e.g., using Crashlytics, by overriding this method.\n protected void onErrorOnGameUpdate(Exception e) {\n Log.e(\"Go\", e.toString());\n }\n\n private EbitenSurfaceView ebitenSurfaceView;\n private InputManager inputManager;\n private ArrayList gamepads;\n}\n`\n\nconst surfaceViewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.opengl.GLSurfaceView;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.util.Log;\n\nimport javax.microedition.khronos.egl.EGLConfig;\nimport javax.microedition.khronos.opengles.GL10;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\nimport {{.JavaPkg}}.ebitenmobileview.RenderRequester;\nimport {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;\n\nclass EbitenSurfaceView extends GLSurfaceView implements RenderRequester {\n\n private class EbitenRenderer implements GLSurfaceView.Renderer {\n\n private boolean errored_ = false;\n\n @Override\n public void onDrawFrame(GL10 gl) {\n if (errored_) {\n return;\n }\n try {\n Ebitenmobileview.update();\n } catch (final Exception e) {\n new Handler(Looper.getMainLooper()).post(new Runnable() {\n @Override\n public void run() {\n onErrorOnGameUpdate(e);\n }\n });\n errored_ = true;\n }\n }\n\n @Override\n public void onSurfaceCreated(GL10 gl, EGLConfig config) {\n Ebitenmobileview.onContextLost();\n }\n\n @Override\n public void onSurfaceChanged(GL10 gl, int width, int height) {\n }\n }\n\n public EbitenSurfaceView(Context context) {\n super(context);\n initialize();\n }\n\n public EbitenSurfaceView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize();\n }\n\n private void initialize() {\n setEGLContextClientVersion(2);\n setEGLConfigChooser(8, 8, 8, 8, 0, 0);\n setRenderer(new EbitenRenderer());\n Ebitenmobileview.setRenderRequester(this);\n }\n\n private void onErrorOnGameUpdate(Exception e) {\n ((EbitenView)getParent()).onErrorOnGameUpdate(e);\n }\n\n @Override\n public synchronized void setExplicitRenderingMode(boolean explicitRendering) {\n if (explicitRendering) {\n setRenderMode(RENDERMODE_WHEN_DIRTY);\n } else {\n setRenderMode(RENDERMODE_CONTINUOUSLY);\n }\n }\n\n @Override\n public synchronized void requestRenderIfNeeded() {\n if (getRenderMode() == RENDERMODE_WHEN_DIRTY) {\n requestRender();\n }\n }\n}\n`\n") diff --git a/internal/gamepad/extern_android.go b/internal/gamepad/extern_android.go index 10dd1ee2a..9fb8e5ccc 100644 --- a/internal/gamepad/extern_android.go +++ b/internal/gamepad/extern_android.go @@ -18,14 +18,7 @@ package gamepad import ( - "fmt" -) - -type AndroidHatDirection int - -const ( - AndroidHatDirectionX AndroidHatDirection = iota - AndroidHatDirectionY + "github.com/hajimehoshi/ebiten/v2/internal/gamepaddb" ) func AddAndroidGamepad(androidDeviceID int, name, sdlID string, axisCount, hatCount int) { @@ -44,8 +37,8 @@ func UpdateAndroidGamepadButton(androidDeviceID int, button Button, pressed bool theGamepads.updateAndroidGamepadButton(androidDeviceID, button, pressed) } -func UpdateAndroidGamepadHat(androidDeviceID int, hat int, dir AndroidHatDirection, value int) { - theGamepads.updateAndroidGamepadHat(androidDeviceID, hat, dir, value) +func UpdateAndroidGamepadHat(androidDeviceID int, hat int, xValue, yValue int) { + theGamepads.updateAndroidGamepadHat(androidDeviceID, hat, xValue, yValue) } func (g *gamepads) addAndroidGamepad(androidDeviceID int, name, sdlID string, axisCount, hatCount int) { @@ -56,7 +49,7 @@ func (g *gamepads) addAndroidGamepad(androidDeviceID int, name, sdlID string, ax gp.native = &nativeGamepadImpl{ androidDeviceID: androidDeviceID, axes: make([]float64, axisCount), - buttons: make([]bool, ButtonCount), + buttons: make([]bool, gamepaddb.SDLControllerButtonMax+1), hats: make([]int, hatCount), } } @@ -96,7 +89,7 @@ func (g *gamepads) updateAndroidGamepadButton(androidDeviceID int, button Button gp.updateAndroidGamepadButton(button, pressed) } -func (g *gamepads) updateAndroidGamepadHat(androidDeviceID int, hat int, dir AndroidHatDirection, value int) { +func (g *gamepads) updateAndroidGamepadHat(androidDeviceID int, hat int, xValue, yValue int) { g.m.Lock() defer g.m.Unlock() @@ -106,7 +99,7 @@ func (g *gamepads) updateAndroidGamepadHat(androidDeviceID int, hat int, dir And if gp == nil { return } - gp.updateAndroidGamepadHat(hat, dir, value) + gp.updateAndroidGamepadHat(hat, xValue, yValue) } func (g *Gamepad) updateAndroidGamepadAxis(axis int, value float64) { @@ -131,7 +124,7 @@ func (g *Gamepad) updateAndroidGamepadButton(button Button, pressed bool) { n.buttons[button] = pressed } -func (g *Gamepad) updateAndroidGamepadHat(hat int, dir AndroidHatDirection, value int) { +func (g *Gamepad) updateAndroidGamepadHat(hat int, xValue, yValue int) { g.m.Lock() defer g.m.Unlock() @@ -139,32 +132,43 @@ func (g *Gamepad) updateAndroidGamepadHat(hat int, dir AndroidHatDirection, valu if hat < 0 || hat >= len(n.hats) { return } - v := n.hats[hat] - switch dir { - case AndroidHatDirectionX: - switch { - case value < 0: - v |= hatLeft - v &^= hatRight - case value > 0: - v &^= hatLeft - v |= hatRight - default: - v &^= (hatLeft | hatRight) - } - case AndroidHatDirectionY: - switch { - case value < 0: - v |= hatUp - v &^= hatDown - case value > 0: - v &^= hatUp - v |= hatDown - default: - v &^= (hatUp | hatDown) - } - default: - panic(fmt.Sprintf("gamepad: invalid direction: %d", dir)) + var v int + switch { + case xValue < 0: + v |= hatLeft + case xValue > 0: + v |= hatRight + } + switch { + case yValue < 0: + v |= hatUp + case yValue > 0: + v |= hatDown } n.hats[hat] = v + + // Update the gamepad buttons in addition to hats. + // See https://github.com/libsdl-org/SDL/blob/47f2373dc13b66c48bf4024fcdab53cd0bdd59bb/src/joystick/android/SDL_sysjoystick.c#L290-L301 + switch { + case xValue < 0: + n.buttons[gamepaddb.SDLControllerButtonDpadLeft] = true + n.buttons[gamepaddb.SDLControllerButtonDpadRight] = false + case xValue > 0: + n.buttons[gamepaddb.SDLControllerButtonDpadLeft] = false + n.buttons[gamepaddb.SDLControllerButtonDpadRight] = true + default: + n.buttons[gamepaddb.SDLControllerButtonDpadLeft] = false + n.buttons[gamepaddb.SDLControllerButtonDpadRight] = false + } + switch { + case yValue < 0: + n.buttons[gamepaddb.SDLControllerButtonDpadUp] = true + n.buttons[gamepaddb.SDLControllerButtonDpadDown] = false + case yValue > 0: + n.buttons[gamepaddb.SDLControllerButtonDpadUp] = false + n.buttons[gamepaddb.SDLControllerButtonDpadDown] = true + default: + n.buttons[gamepaddb.SDLControllerButtonDpadUp] = false + n.buttons[gamepaddb.SDLControllerButtonDpadDown] = false + } } diff --git a/internal/gamepaddb/gamepaddb.go b/internal/gamepaddb/gamepaddb.go index 1df318cb1..067538aa3 100644 --- a/internal/gamepaddb/gamepaddb.go +++ b/internal/gamepaddb/gamepaddb.go @@ -341,30 +341,26 @@ func toStandardGamepadAxis(str string) (StandardAxis, bool) { } func buttonMappings(id string) map[StandardButton]*mapping { - // TODO: Use the database instead of the original mapping (#2308). - // The buttons and axes assignments should be fixed. + if m, ok := gamepadButtonMappings[id]; ok { + return m + } if currentPlatform == platformAndroid { if addAndroidDefaultMappings(id) { return gamepadButtonMappings[id] } } - if m, ok := gamepadButtonMappings[id]; ok { - return m - } return nil } func axisMappings(id string) map[StandardAxis]*mapping { - // TODO: Use the database instead of the original mapping (#2308). - // The buttons and axes assignments should be fixed. + if m, ok := gamepadAxisMappings[id]; ok { + return m + } if currentPlatform == platformAndroid { if addAndroidDefaultMappings(id) { return gamepadAxisMappings[id] } } - if m, ok := gamepadAxisMappings[id]; ok { - return m - } return nil } @@ -585,35 +581,6 @@ func Update(mappingData []byte) error { } func addAndroidDefaultMappings(id string) bool { - // See https://github.com/libsdl-org/SDL/blob/120c76c84bbce4c1bfed4e9eb74e10678bd83120/include/SDL_gamecontroller.h#L655-L680 - const ( - SDLControllerButtonA = 0 - SDLControllerButtonB = 1 - SDLControllerButtonX = 2 - SDLControllerButtonY = 3 - SDLControllerButtonBack = 4 - SDLControllerButtonGuide = 5 - SDLControllerButtonStart = 6 - SDLControllerButtonLeftStick = 7 - SDLControllerButtonRightStick = 8 - SDLControllerButtonLeftShoulder = 9 - SDLControllerButtonRightShoulder = 10 - SDLControllerButtonDpadUp = 11 - SDLControllerButtonDpadDown = 12 - SDLControllerButtonDpadLeft = 13 - SDLControllerButtonDpadRight = 14 - ) - - // See https://github.com/libsdl-org/SDL/blob/120c76c84bbce4c1bfed4e9eb74e10678bd83120/include/SDL_gamecontroller.h#L550-L560 - const ( - SDLControllerAxisLeftX = 0 - SDLControllerAxisLeftY = 1 - SDLControllerAxisRightX = 2 - SDLControllerAxisRightY = 3 - SDLControllerAxisTriggerLeft = 4 - SDLControllerAxisTriggerRight = 5 - ) - // See https://github.com/libsdl-org/SDL/blob/120c76c84bbce4c1bfed4e9eb74e10678bd83120/src/joystick/SDL_gamecontroller.c#L468-L568 const faceButtonMask = ((1 << SDLControllerButtonA) | @@ -642,38 +609,38 @@ func addAndroidDefaultMappings(id string) bool { if buttonMask&(1< On game pads with two analog joysticks, this axis is often reinterpreted to report the absolute X position of the second joystick instead. gamepadAxisMappings[id][StandardAxisRightStickHorizontal] = &mapping{ Type: mappingTypeAxis, - Index: 2, + Index: SDLControllerAxisRightX, AxisScale: 1, AxisOffset: 0, } } if axisMask&(1< On game pads with two analog joysticks, this axis is often reinterpreted to report the absolute Y position of the second joystick instead. gamepadAxisMappings[id][StandardAxisRightStickVertical] = &mapping{ Type: mappingTypeAxis, - Index: 5, + Index: SDLControllerAxisRightY, AxisScale: 1, AxisOffset: 0, } @@ -783,17 +742,17 @@ func addAndroidDefaultMappings(id string) bool { if axisMask&(1<