// Copyright 2019 The Ebiten Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // +build ebitenmobilegobind // gobind is a wrapper of the original gobind. This command adds extra files like a view controller. package main import ( "flag" "fmt" "io/ioutil" "log" "os" "os/exec" "path/filepath" "strings" "golang.org/x/tools/go/packages" ) var ( lang = flag.String("lang", "", "") outdir = flag.String("outdir", "", "") javaPkg = flag.String("javapkg", "", "") prefix = flag.String("prefix", "", "") bootclasspath = flag.String("bootclasspath", "", "") classpath = flag.String("classpath", "", "") tags = flag.String("tags", "", "") ) var usage = `The Gobind tool generates Java language bindings for Go. For usage details, see doc.go.` func main() { flag.Parse() if err := run(); err != nil { log.Fatal(err) } } func invokeOriginalGobind(lang string) (pkgName string, err error) { cmd := exec.Command("gobind-original", os.Args[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return "", err } cfgtags := strings.Join(strings.Split(*tags, ","), " ") cfg := &packages.Config{} switch lang { case "java": cfg.Env = append(os.Environ(), "GOOS=android") case "objc": cfg.Env = append(os.Environ(), "GOOS=darwin") if cfgtags != "" { cfgtags += " " } cfgtags += "ios" } cfg.BuildFlags = []string{"-tags", cfgtags} pkgs, err := packages.Load(cfg, flag.Args()[0]) if err != nil { return "", err } return pkgs[0].Name, nil } func forceGL() bool { for _, tag := range strings.Split(*tags, ",") { if tag == "ebitengl" { return true } } return false } func run() error { writeFile := func(filename string, content string) error { if err := ioutil.WriteFile(filepath.Join(*outdir, filename), []byte(content), 0644); err != nil { return err } return nil } // Add additional files. langs := strings.Split(*lang, ",") for _, lang := range langs { pkgName, err := invokeOriginalGobind(lang) if err != nil { return err } prefixLower := *prefix + pkgName prefixUpper := strings.Title(*prefix) + strings.Title(pkgName) replacePrefixes := func(content string) string { content = strings.ReplaceAll(content, "{{.PrefixUpper}}", prefixUpper) content = strings.ReplaceAll(content, "{{.PrefixLower}}", prefixLower) content = strings.ReplaceAll(content, "{{.JavaPkg}}", *javaPkg) f := "0" if forceGL() { f = "1" } content = strings.ReplaceAll(content, "{{.ForceGL}}", f) return content } switch lang { case "objc": // iOS if err := writeFile(filepath.Join("src", "gobind", prefixLower+"ebitenviewcontroller_ios.m"), replacePrefixes(objcM)); err != nil { return err } case "java": // Android dir := filepath.Join(strings.Split(*javaPkg, ".")...) dir = filepath.Join(dir, prefixLower) if err := writeFile(filepath.Join("java", dir, "EbitenView.java"), replacePrefixes(viewJava)); err != nil { return err } if err := writeFile(filepath.Join("java", dir, "EbitenSurfaceView.java"), replacePrefixes(surfaceViewJava)); err != nil { return err } case "go": // Do nothing. default: panic(fmt.Sprintf("unsupported language: %s", lang)) } } return nil } const objcM = `// Code generated by ebitenmobile. DO NOT EDIT. // +build ios #import #if TARGET_IPHONE_SIMULATOR || {{.ForceGL}} #define EBITEN_METAL 0 #else #define EBITEN_METAL 1 #endif #import #import #import #import "Ebitenmobileview.objc.h" @interface {{.PrefixUpper}}EbitenViewController : UIViewController @end @implementation {{.PrefixUpper}}EbitenViewController { UIView* metalView_; GLKView* glkView_; bool started_; bool active_; bool error_; } - (UIView*)metalView { if (!metalView_) { metalView_ = [[UIView alloc] init]; metalView_.multipleTouchEnabled = YES; } return metalView_; } - (GLKView*)glkView { if (!glkView_) { glkView_ = [[GLKView alloc] init]; glkView_.multipleTouchEnabled = YES; } return glkView_; } - (void)viewDidLoad { [super viewDidLoad]; if (!started_) { @synchronized(self) { active_ = true; } started_ = true; } #if EBITEN_METAL [self.view addSubview: self.metalView]; EbitenmobileviewSetUIView((uintptr_t)(self.metalView)); #else self.glkView.delegate = (id)(self); [self.view addSubview: self.glkView]; EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; [self glkView].context = context; [EAGLContext setCurrentContext:context]; #endif CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)]; [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; } - (void)viewWillLayoutSubviews { CGRect viewRect = [[self view] frame]; #if EBITEN_METAL [[self metalView] setFrame:viewRect]; #else [[self glkView] setFrame:viewRect]; #endif } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGRect viewRect = [[self view] frame]; EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height); } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. // TODO: Notify this to Go world? } - (void)drawFrame{ @synchronized(self) { if (!active_) { return; } #if EBITEN_METAL [self updateEbiten]; #else [[self glkView] setNeedsDisplay]; #endif } } - (void)glkView:(GLKView*)view drawInRect:(CGRect)rect { @synchronized(self) { [self updateEbiten]; } } - (void)updateEbiten { if (error_) { return; } NSError* err = nil; EbitenmobileviewUpdate(&err); if (err != nil) { [self performSelectorOnMainThread:@selector(onErrorOnGameUpdate:) withObject:err waitUntilDone:NO]; error_ = true; } } - (void)onErrorOnGameUpdate:(NSError*)err { NSLog(@"Error: %@", err); } - (void)updateTouches:(NSSet*)touches { for (UITouch* touch in touches) { #if EBITEN_METAL if (touch.view != [self metalView]) { continue; } #else if (touch.view != [self glkView]) { continue; } #endif CGPoint location = [touch locationInView:touch.view]; EbitenmobileviewUpdateTouchesOnIOS(touch.phase, (uintptr_t)touch, location.x, location.y); } } - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { [self updateTouches:touches]; } - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { [self updateTouches:touches]; } - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { [self updateTouches:touches]; } - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { [self updateTouches:touches]; } - (void)suspendGame { NSAssert(started_, @"suspendGame must not be called before viewDidLoad is called"); @synchronized(self) { active_ = false; EbitenmobileviewSuspend(); } } - (void)resumeGame { NSAssert(started_, @"resumeGame must not be called before viewDidLoad is called"); @synchronized(self) { active_ = true; EbitenmobileviewResume(); } } @end ` const viewJava = `// Code generated by ebitenmobile. DO NOT EDIT. package {{.JavaPkg}}.{{.PrefixLower}}; import android.content.Context; import android.hardware.input.InputManager; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.InputDevice; import android.view.MotionEvent; import android.view.ViewGroup; import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview; public class EbitenView extends ViewGroup implements InputManager.InputDeviceListener { private double getDeviceScale() { if (this.deviceScale == 0.0) { this.deviceScale = getResources().getDisplayMetrics().density; } return this.deviceScale; } private double pxToDp(double x) { return x / getDeviceScale(); } private double deviceScale = 0.0; public EbitenView(Context context) { super(context); initialize(context); } public EbitenView(Context context, AttributeSet attrs) { super(context, attrs); initialize(context); } private void initialize(Context context) { this.ebitenSurfaceView = new EbitenSurfaceView(getContext()); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); addView(this.ebitenSurfaceView, params); this.inputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); this.inputManager.registerInputDeviceListener(this, null); for (int id : this.inputManager.getInputDeviceIds()) { this.onInputDeviceAdded(id); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { this.ebitenSurfaceView.layout(0, 0, right - left, bottom - top); double widthInDp = pxToDp(right - left); double heightInDp = pxToDp(bottom - top); Ebitenmobileview.layout(widthInDp, heightInDp); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar(), event.getSource(), event.getDeviceId()); return true; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { Ebitenmobileview.onKeyUpOnAndroid(keyCode, event.getSource(), event.getDeviceId()); return true; } @Override public boolean onTouchEvent(MotionEvent e) { for (int i = 0; i < e.getPointerCount(); i++) { int id = e.getPointerId(i); int x = (int)e.getX(i); int y = (int)e.getY(i); Ebitenmobileview.updateTouchesOnAndroid(e.getActionMasked(), id, (int)pxToDp(x), (int)pxToDp(y)); } 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, }; @Override public boolean onGenericMotionEvent(MotionEvent event) { if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) { return super.onGenericMotionEvent(event); } 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.onGamepadAxesChanged(event.getDeviceId(), axis, value); } return true; } @Override public void onInputDeviceAdded(int deviceId) { InputDevice inputDevice = this.inputManager.getInputDevice(deviceId); int sources = inputDevice.getSources(); if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD && (sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) { return; } boolean[] keyExistences = inputDevice.hasKeys(gamepadButtons); int buttonNum = gamepadButtons.length - 1; for (int i = gamepadButtons.length - 1; i >= 0; i--) { if (keyExistences[i]) { break; } buttonNum--; } int axisNum = axes.length - 1; for (int i = axes.length - 1; i >= 0; i--) { if (inputDevice.getMotionRange(axes[i], InputDevice.SOURCE_JOYSTICK) != null) { break; } axisNum--; } 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); int axisMask = getAxisMask(inputDevice); Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), buttonNum, axisNum, descriptor, vendorId, productId, buttonMask, axisMask); } // The implementation is copied from SDL: // https://hg.libsdl.org/SDL/file/bc90ce38f1e2/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#l308 private int getButtonMask(InputDevice joystickDevice) { int button_mask = 0; int[] keys = new int[] { KeyEvent.KEYCODE_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, KeyEvent.KEYCODE_BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y, KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BUTTON_MODE, KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_BUTTON_THUMBL, KeyEvent.KEYCODE_BUTTON_THUMBR, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_DPAD_CENTER, // These don't map into any SDL controller buttons directly KeyEvent.KEYCODE_BUTTON_L2, KeyEvent.KEYCODE_BUTTON_R2, KeyEvent.KEYCODE_BUTTON_C, KeyEvent.KEYCODE_BUTTON_Z, 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, }; int[] masks = new int[] { (1 << 0), // A -> A (1 << 1), // B -> B (1 << 2), // X -> X (1 << 3), // Y -> Y (1 << 4), // BACK -> BACK (1 << 5), // MODE -> GUIDE (1 << 6), // START -> START (1 << 7), // THUMBL -> LEFTSTICK (1 << 8), // THUMBR -> RIGHTSTICK (1 << 9), // L1 -> LEFTSHOULDER (1 << 10), // R1 -> RIGHTSHOULDER (1 << 11), // DPAD_UP -> DPAD_UP (1 << 12), // DPAD_DOWN -> DPAD_DOWN (1 << 13), // DPAD_LEFT -> DPAD_LEFT (1 << 14), // DPAD_RIGHT -> DPAD_RIGHT (1 << 4), // SELECT -> BACK (1 << 0), // DPAD_CENTER -> A (1 << 15), // L2 -> ?? (1 << 16), // R2 -> ?? (1 << 17), // C -> ?? (1 << 18), // Z -> ?? (1 << 20), // 1 -> ?? (1 << 21), // 2 -> ?? (1 << 22), // 3 -> ?? (1 << 23), // 4 -> ?? (1 << 24), // 5 -> ?? (1 << 25), // 6 -> ?? (1 << 26), // 7 -> ?? (1 << 27), // 8 -> ?? (1 << 28), // 9 -> ?? (1 << 29), // 10 -> ?? (1 << 30), // 11 -> ?? (1 << 31), // 12 -> ?? // We're out of room... 0xFFFFFFFF, // 13 -> ?? 0xFFFFFFFF, // 14 -> ?? 0xFFFFFFFF, // 15 -> ?? 0xFFFFFFFF, // 16 -> ?? }; boolean[] has_keys = joystickDevice.hasKeys(keys); for (int i = 0; i < keys.length; ++i) { if (has_keys[i]) { button_mask |= masks[i]; } } return button_mask; } private 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; final int SDL_CONTROLLER_AXIS_RIGHTY = 3; final int SDL_CONTROLLER_AXIS_TRIGGERLEFT = 4; final int SDL_CONTROLLER_AXIS_TRIGGERRIGHT = 5; int naxes = 0; for (InputDevice.MotionRange range : joystickDevice.getMotionRanges()) { if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { if (range.getAxis() != MotionEvent.AXIS_HAT_X && range.getAxis() != MotionEvent.AXIS_HAT_Y) { naxes++; } } } // The variable is_accelerometer seems always false, then skip the checking: // https://hg.libsdl.org/SDL/file/bc90ce38f1e2/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#l207 int axisMask = 0; if (naxes >= 2) { axisMask |= ((1 << SDL_CONTROLLER_AXIS_LEFTX) | (1 << SDL_CONTROLLER_AXIS_LEFTY)); } if (naxes >= 4) { axisMask |= ((1 << SDL_CONTROLLER_AXIS_RIGHTX) | (1 << SDL_CONTROLLER_AXIS_RIGHTY)); } if (naxes >= 6) { axisMask |= ((1 << SDL_CONTROLLER_AXIS_TRIGGERLEFT) | (1 << SDL_CONTROLLER_AXIS_TRIGGERRIGHT)); } return axisMask; } @Override public void onInputDeviceChanged(int deviceId) { // Do nothing. } @Override public void onInputDeviceRemoved(int deviceId) { // Do not call inputManager.getInputDevice(), which returns null (#1185). Ebitenmobileview.onInputDeviceRemoved(deviceId); } // suspendGame suspends the game. // It is recommended to call this when the application is being suspended e.g., // Activity's onPause is called. public void suspendGame() { this.inputManager.unregisterInputDeviceListener(this); this.ebitenSurfaceView.onPause(); Ebitenmobileview.suspend(); } // resumeGame resumes the game. // It is recommended to call this when the application is being resumed e.g., // Activity's onResume is called. public void resumeGame() { this.inputManager.registerInputDeviceListener(this, null); this.ebitenSurfaceView.onResume(); Ebitenmobileview.resume(); } // onErrorOnGameUpdate is called on the main thread when an error happens when updating a game. // You can define your own error handler, e.g., using Crashlytics, by overriding this method. protected void onErrorOnGameUpdate(Exception e) { Log.e("Go", e.toString()); } private EbitenSurfaceView ebitenSurfaceView; private InputManager inputManager; } ` const surfaceViewJava = `// Code generated by ebitenmobile. DO NOT EDIT. package {{.JavaPkg}}.{{.PrefixLower}}; import android.content.Context; import android.opengl.GLSurfaceView; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; import android.util.Log; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview; import {{.JavaPkg}}.{{.PrefixLower}}.EbitenView; class EbitenSurfaceView extends GLSurfaceView { private class EbitenRenderer implements GLSurfaceView.Renderer { private boolean errored_ = false; @Override public void onDrawFrame(GL10 gl) { if (errored_) { return; } try { Ebitenmobileview.update(); } catch (final Exception e) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { onErrorOnGameUpdate(e); } }); errored_ = true; } } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { Ebitenmobileview.onContextLost(); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { } } public EbitenSurfaceView(Context context) { super(context); initialize(); } public EbitenSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); initialize(); } private void initialize() { setEGLContextClientVersion(2); setEGLConfigChooser(8, 8, 8, 8, 0, 0); setRenderer(new EbitenRenderer()); } private void onErrorOnGameUpdate(Exception e) { ((EbitenView)getParent()).onErrorOnGameUpdate(e); } } `