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<