Leverage java annotations to simplify the logic used to register the Godot plugin methods.

This commit is contained in:
Fredia Huya-Kouadio 2021-03-16 00:36:31 -07:00
parent a384fac953
commit 2d574bcc85
9 changed files with 243 additions and 64 deletions

View file

@ -44,7 +44,7 @@ import androidx.fragment.app.FragmentActivity;
* It's also a reference implementation for how to setup and use the {@link Godot} fragment
* within an Android app.
*/
public abstract class FullScreenGodotApp extends FragmentActivity {
public abstract class FullScreenGodotApp extends FragmentActivity implements GodotHost {
@Nullable
private Godot godotFragment;
@ -64,6 +64,8 @@ public abstract class FullScreenGodotApp extends FragmentActivity {
public void onNewIntent(Intent intent) {
if (godotFragment != null) {
godotFragment.onNewIntent(intent);
} else {
super.onNewIntent(intent);
}
}

View file

@ -103,6 +103,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@ -131,6 +132,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
private boolean activityResumed;
private int mState;
private GodotHost godotHost;
private GodotPluginRegistry pluginRegistry;
static private Intent mCurrentIntent;
@ -177,6 +179,22 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
}
public ResultCallback result_callback;
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (getParentFragment() instanceof GodotHost) {
godotHost = (GodotHost)getParentFragment();
} else if (getActivity() instanceof GodotHost) {
godotHost = (GodotHost)getActivity();
}
}
@Override
public void onDetach() {
super.onDetach();
godotHost = null;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (result_callback != null) {
@ -200,6 +218,20 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
}
};
/**
* Invoked on the render thread when the Godot setup is complete.
*/
@CallSuper
protected void onGodotSetupCompleted() {
for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
plugin.onGodotSetupCompleted();
}
if (godotHost != null) {
godotHost.onGodotSetupCompleted();
}
}
/**
* Invoked on the render thread when the Godot main loop has started.
*/
@ -208,6 +240,10 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
plugin.onGodotMainLoopStarted();
}
if (godotHost != null) {
godotHost.onGodotMainLoopStarted();
}
}
/**
@ -301,7 +337,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
v.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
//deprecated in API 26
// deprecated in API 26
v.vibrate(durationMs);
}
}
@ -356,6 +392,21 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
@CallSuper
protected String[] getCommandLine() {
String[] original = parseCommandLine();
String[] updated;
List<String> hostCommandLine = godotHost != null ? godotHost.getCommandLine() : null;
if (hostCommandLine == null || hostCommandLine.isEmpty()) {
updated = original;
} else {
updated = Arrays.copyOf(original, original.length + hostCommandLine.size());
for (int i = 0; i < hostCommandLine.size(); i++) {
updated[original.length + i] = hostCommandLine.get(i);
}
}
return updated;
}
private String[] parseCommandLine() {
InputStream is;
try {
is = getActivity().getAssets().open("_cl_");
@ -473,7 +524,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
mClipboard = (ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE);
pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this);
//check for apk expansion API
// check for apk expansion API
boolean md5mismatch = false;
command_line = getCommandLine();
String main_pack_md5 = null;
@ -529,9 +580,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
command_line = new_args.toArray(new String[new_args.size()]);
}
if (use_apk_expansion && main_pack_md5 != null && main_pack_key != null) {
//check that environment is ok!
// check that environment is ok!
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
//show popup and die
// show popup and die
}
// Build the full path to the app's expansion files

View file

@ -0,0 +1,56 @@
/*************************************************************************/
/* GodotHost.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot;
import java.util.Collections;
import java.util.List;
/**
* Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} fragment.
*/
public interface GodotHost {
/**
* Provides a set of command line parameters to setup the engine.
*/
default List<String> getCommandLine() {
return Collections.emptyList();
}
/**
* Invoked on the render thread when the Godot setup is complete.
*/
default void onGodotSetupCompleted() {}
/**
* Invoked on the render thread when the Godot main loop has started.
*/
default void onGodotMainLoopStarted() {}
}

View file

@ -47,6 +47,7 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -109,31 +110,9 @@ public abstract class GodotPlugin {
* This method is invoked on the render thread.
*/
public final void onRegisterPluginWithGodotNative() {
registeredSignals.putAll(registerPluginWithGodotNative(this, new GodotPluginInfoProvider() {
@NonNull
@Override
public String getPluginName() {
return GodotPlugin.this.getPluginName();
}
@NonNull
@Override
public List<String> getPluginMethods() {
return GodotPlugin.this.getPluginMethods();
}
@NonNull
@Override
public Set<SignalInfo> getPluginSignals() {
return GodotPlugin.this.getPluginSignals();
}
@NonNull
@Override
public Set<String> getPluginGDNativeLibrariesPaths() {
return GodotPlugin.this.getPluginGDNativeLibrariesPaths();
}
}));
registeredSignals.putAll(
registerPluginWithGodotNative(this, getPluginName(), getPluginMethods(), getPluginSignals(),
getPluginGDNativeLibrariesPaths()));
}
/**
@ -141,23 +120,41 @@ public abstract class GodotPlugin {
*
* This method must be invoked on the render thread.
*/
public static Map<String, SignalInfo> registerPluginWithGodotNative(Object pluginObject, GodotPluginInfoProvider pluginInfoProvider) {
nativeRegisterSingleton(pluginInfoProvider.getPluginName(), pluginObject);
public static void registerPluginWithGodotNative(Object pluginObject,
GodotPluginInfoProvider pluginInfoProvider) {
registerPluginWithGodotNative(pluginObject, pluginInfoProvider.getPluginName(),
Collections.emptyList(), pluginInfoProvider.getPluginSignals(),
pluginInfoProvider.getPluginGDNativeLibrariesPaths());
// Notify that registration is complete.
pluginInfoProvider.onPluginRegistered();
}
private static Map<String, SignalInfo> registerPluginWithGodotNative(Object pluginObject,
String pluginName, List<String> pluginMethods, Set<SignalInfo> pluginSignals,
Set<String> pluginGDNativeLibrariesPaths) {
nativeRegisterSingleton(pluginName, pluginObject);
Set<Method> filteredMethods = new HashSet<>();
Class clazz = pluginObject.getClass();
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
boolean found = false;
for (String s : pluginInfoProvider.getPluginMethods()) {
if (s.equals(method.getName())) {
found = true;
break;
// Check if the method is annotated with {@link UsedByGodot}.
if (method.getAnnotation(UsedByGodot.class) != null) {
filteredMethods.add(method);
} else {
// For backward compatibility, process the methods from the given <pluginMethods> argument.
for (String methodName : pluginMethods) {
if (methodName.equals(method.getName())) {
filteredMethods.add(method);
break;
}
}
}
if (!found)
continue;
}
for (Method method : filteredMethods) {
List<String> ptr = new ArrayList<>();
Class[] paramTypes = method.getParameterTypes();
@ -168,21 +165,20 @@ public abstract class GodotPlugin {
String[] pt = new String[ptr.size()];
ptr.toArray(pt);
nativeRegisterMethod(pluginInfoProvider.getPluginName(), method.getName(), method.getReturnType().getName(), pt);
nativeRegisterMethod(pluginName, method.getName(), method.getReturnType().getName(), pt);
}
// Register the signals for this plugin.
Map<String, SignalInfo> registeredSignals = new HashMap<>();
for (SignalInfo signalInfo : pluginInfoProvider.getPluginSignals()) {
for (SignalInfo signalInfo : pluginSignals) {
String signalName = signalInfo.getName();
nativeRegisterSignal(pluginInfoProvider.getPluginName(), signalName, signalInfo.getParamTypesNames());
nativeRegisterSignal(pluginName, signalName, signalInfo.getParamTypesNames());
registeredSignals.put(signalName, signalInfo);
}
// Get the list of gdnative libraries to register.
Set<String> gdnativeLibrariesPaths = pluginInfoProvider.getPluginGDNativeLibrariesPaths();
if (!gdnativeLibrariesPaths.isEmpty()) {
nativeRegisterGDNativeLibraries(gdnativeLibrariesPaths.toArray(new String[0]));
if (!pluginGDNativeLibrariesPaths.isEmpty()) {
nativeRegisterGDNativeLibraries(pluginGDNativeLibrariesPaths.toArray(new String[0]));
}
return registeredSignals;
@ -235,6 +231,11 @@ public abstract class GodotPlugin {
*/
public boolean onMainBackPressed() { return false; }
/**
* Invoked on the render thread when the Godot setup is complete.
*/
public void onGodotSetupCompleted() {}
/**
* Invoked on the render thread when the Godot main loop has started.
*/
@ -282,8 +283,11 @@ public abstract class GodotPlugin {
/**
* Returns the list of methods to be exposed to Godot.
*
* @deprecated Used the {@link UsedByGodot} annotation instead.
*/
@NonNull
@Deprecated
public List<String> getPluginMethods() {
return Collections.emptyList();
}

View file

@ -32,7 +32,7 @@ package org.godotengine.godot.plugin;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.Collections;
import java.util.Set;
/**
@ -45,17 +45,13 @@ public interface GodotPluginInfoProvider {
@NonNull
String getPluginName();
/**
* Returns the list of methods to be exposed to Godot.
*/
@NonNull
List<String> getPluginMethods();
/**
* Returns the list of signals to be exposed to Godot.
*/
@NonNull
Set<SignalInfo> getPluginSignals();
default Set<SignalInfo> getPluginSignals() {
return Collections.emptySet();
}
/**
* Returns the paths for the plugin's gdnative libraries (if any).
@ -63,5 +59,14 @@ public interface GodotPluginInfoProvider {
* The paths must be relative to the 'assets' directory and point to a '*.gdnlib' file.
*/
@NonNull
Set<String> getPluginGDNativeLibrariesPaths();
default Set<String> getPluginGDNativeLibrariesPaths() {
return Collections.emptySet();
}
/**
* This is invoked on the render thread when the plugin described by this instance has been
* registered.
*/
default void onPluginRegistered() {
}
}

View file

@ -0,0 +1,45 @@
/*************************************************************************/
/* UsedByGodot.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.plugin;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to indicate a method is being invoked from the Godot game logic.
*
* At runtime, annotated plugin methods are detected and automatically registered.
*/
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface UsedByGodot {}

View file

@ -155,7 +155,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc
}
if (err != OK) {
return; //should exit instead and print the error
return; // should exit instead and print the error
}
java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity()));
@ -217,9 +217,10 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jcl
if (step == 1) {
if (!Main::start()) {
return; //should exit instead and print the error
return; // should exit instead and print the error
}
godot_java->on_godot_setup_completed(env);
os_android->main_loop_begin();
godot_java->on_godot_main_loop_started(env);
++step;

View file

@ -74,6 +74,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_is_activity_resumed = p_env->GetMethodID(godot_class, "isActivityResumed", "()Z");
_vibrate = p_env->GetMethodID(godot_class, "vibrate", "(I)V");
_get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;");
_on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V");
_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
// get some Activity method pointers...
@ -120,11 +121,21 @@ GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() {
}
void GodotJavaWrapper::on_video_init(JNIEnv *p_env) {
if (_on_video_init)
if (_on_video_init) {
if (p_env == nullptr)
p_env = get_jni_env();
p_env->CallVoidMethod(godot_instance, _on_video_init);
p_env->CallVoidMethod(godot_instance, _on_video_init);
}
}
void GodotJavaWrapper::on_godot_setup_completed(JNIEnv *p_env) {
if (_on_godot_setup_completed) {
if (p_env == nullptr) {
p_env = get_jni_env();
}
p_env->CallVoidMethod(godot_instance, _on_godot_setup_completed);
}
}
void GodotJavaWrapper::on_godot_main_loop_started(JNIEnv *p_env) {
@ -132,24 +143,26 @@ void GodotJavaWrapper::on_godot_main_loop_started(JNIEnv *p_env) {
if (p_env == nullptr) {
p_env = get_jni_env();
}
p_env->CallVoidMethod(godot_instance, _on_godot_main_loop_started);
}
p_env->CallVoidMethod(godot_instance, _on_godot_main_loop_started);
}
void GodotJavaWrapper::restart(JNIEnv *p_env) {
if (_restart)
if (_restart) {
if (p_env == nullptr)
p_env = get_jni_env();
p_env->CallVoidMethod(godot_instance, _restart);
p_env->CallVoidMethod(godot_instance, _restart);
}
}
void GodotJavaWrapper::force_quit(JNIEnv *p_env) {
if (_finish)
if (_finish) {
if (p_env == nullptr)
p_env = get_jni_env();
p_env->CallVoidMethod(godot_instance, _finish);
p_env->CallVoidMethod(godot_instance, _finish);
}
}
void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) {

View file

@ -66,6 +66,7 @@ private:
jmethodID _is_activity_resumed = 0;
jmethodID _vibrate = 0;
jmethodID _get_input_fallback_mapping = 0;
jmethodID _on_godot_setup_completed = 0;
jmethodID _on_godot_main_loop_started = 0;
jmethodID _get_class_loader = 0;
@ -80,6 +81,7 @@ public:
GodotJavaViewWrapper *get_godot_view();
void on_video_init(JNIEnv *p_env = nullptr);
void on_godot_setup_completed(JNIEnv *p_env = nullptr);
void on_godot_main_loop_started(JNIEnv *p_env = nullptr);
void restart(JNIEnv *p_env = nullptr);
void force_quit(JNIEnv *p_env = nullptr);