1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-04 12:00:25 +00:00

Merge pull request #103972 from m4gr3d/xr_editor_hybrid_support

Add support for running hybrid apps from the XR editor
This commit is contained in:
Thaddeus Crews
2025-06-20 08:38:55 -05:00
20 changed files with 438 additions and 62 deletions

View File

@@ -311,9 +311,7 @@ void EditorDebuggerNode::stop(bool p_force) {
// Also close all debugging sessions. // Also close all debugging sessions.
_for_all(tabs, [&](ScriptEditorDebugger *dbg) { _for_all(tabs, [&](ScriptEditorDebugger *dbg) {
if (dbg->is_session_active()) { dbg->_stop_and_notify();
dbg->_stop_and_notify();
}
}); });
_break_state_changed(); _break_state_changed();
breakpoints.clear(); breakpoints.clear();

View File

@@ -2176,7 +2176,7 @@ void EditorNode::try_autosave() {
Node *scene = editor_data.get_edited_scene_root(); Node *scene = editor_data.get_edited_scene_root();
if (scene && !scene->get_scene_file_path().is_empty()) { // Only autosave if there is a scene and if it has a path. if (scene && !scene->get_scene_file_path().is_empty()) { // Only autosave if there is a scene and if it has a path.
_save_scene(scene->get_scene_file_path()); _save_scene(scene->get_scene_file_path(), -1, false);
} }
} }
_menu_option(SCENE_SAVE_ALL_SCENES); _menu_option(SCENE_SAVE_ALL_SCENES);

View File

@@ -32,7 +32,8 @@ android_files = [
"rendering_context_driver_vulkan_android.cpp", "rendering_context_driver_vulkan_android.cpp",
"variant/callable_jni.cpp", "variant/callable_jni.cpp",
"dialog_utils_jni.cpp", "dialog_utils_jni.cpp",
"game_menu_utils_jni.cpp", "editor/game_menu_utils_jni.cpp",
"editor/editor_utils_jni.cpp",
] ]
env_android = env.Clone() env_android = env.Clone()

View File

@@ -0,0 +1,82 @@
/**************************************************************************/
/* editor_utils_jni.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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. */
/**************************************************************************/
#include "editor_utils_jni.h"
#include "jni_utils.h"
#ifdef TOOLS_ENABLED
#include "editor/gui/editor_run_bar.h"
#include "main/main.h"
#endif
extern "C" {
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_EditorUtils_runScene(JNIEnv *p_env, jclass, jstring p_scene, jobjectArray p_scene_args) {
#ifdef TOOLS_ENABLED
Vector<String> scene_args;
jint length = p_env->GetArrayLength(p_scene_args);
for (jint i = 0; i < length; ++i) {
jstring j_arg = (jstring)p_env->GetObjectArrayElement(p_scene_args, i);
String arg = jstring_to_string(j_arg, p_env);
scene_args.push_back(arg);
p_env->DeleteLocalRef(j_arg);
}
String scene = jstring_to_string(p_scene, p_env);
EditorRunBar *editor_run_bar = EditorRunBar::get_singleton();
if (editor_run_bar != nullptr) {
if (scene.is_empty()) {
editor_run_bar->play_main_scene(false);
} else {
editor_run_bar->play_custom_scene(scene, scene_args);
}
} else {
List<String> args;
for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_PROJECT)) {
args.push_back(a);
}
for (const String &arg : scene_args) {
args.push_back(arg);
}
if (!scene.is_empty()) {
args.push_back("--scene");
args.push_back(scene);
}
Error err = OS::get_singleton()->create_instance(args);
ERR_FAIL_COND(err);
}
#endif
}
}

View File

@@ -0,0 +1,37 @@
/**************************************************************************/
/* editor_utils_jni.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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. */
/**************************************************************************/
#pragma once
#include <jni.h>
extern "C" {
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_EditorUtils_runScene(JNIEnv *p_env, jclass, jstring p_scene, jobjectArray p_scene_args);
}

View File

@@ -45,7 +45,7 @@ static GameViewPlugin *_get_game_view_plugin() {
extern "C" { extern "C" {
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin(); GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) { if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -54,7 +54,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend
#endif #endif
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin(); GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) { if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -63,7 +63,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(
#endif #endif
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin(); GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) { if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -72,7 +72,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeTyp
#endif #endif
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin(); GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) { if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -81,7 +81,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectM
#endif #endif
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin(); GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) { if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -90,7 +90,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelecti
#endif #endif
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin(); GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) { if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -99,7 +99,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraO
#endif #endif
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin(); GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) { if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -108,7 +108,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraM
#endif #endif
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin(); GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) { if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -117,7 +117,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamer
#endif #endif
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin(); GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) { if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -126,7 +126,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamer
#endif #endif
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
if (EditorInterface::get_singleton()) { if (EditorInterface::get_singleton()) {
EditorInterface::get_singleton()->play_main_scene(); EditorInterface::get_singleton()->play_main_scene();
@@ -134,7 +134,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainSc
#endif #endif
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setDebugMuteAudio(JNIEnv *env, jclass clazz, jboolean enabled) { JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setDebugMuteAudio(JNIEnv *env, jclass clazz, jboolean enabled) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin(); GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) { if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {

View File

@@ -33,15 +33,15 @@
#include <jni.h> #include <jni.h>
extern "C" { extern "C" {
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled);
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type);
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode);
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible);
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled);
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode);
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setDebugMuteAudio(JNIEnv *env, jclass clazz, jboolean enabled); JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setDebugMuteAudio(JNIEnv *env, jclass clazz, jboolean enabled);
} }

View File

@@ -180,6 +180,8 @@ android {
} }
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "androidx.fragment:fragment:$versions.fragmentVersion" implementation "androidx.fragment:fragment:$versions.fragmentVersion"
implementation project(":lib") implementation project(":lib")

View File

@@ -60,6 +60,22 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- Intent filter used to intercept hybrid PANEL launch for the current editor project, and route it
properly through the editor 'run' logic (e.g: debugger setup) -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="org.godotengine.xr.hybrid.PANEL" />
</intent-filter>
<!-- Intent filter used to intercept hybrid IMMERSIVE launch for the current editor project, and route it
properly through the editor 'run' logic (e.g: debugger setup) -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="org.godotengine.xr.hybrid.IMMERSIVE" />
</intent-filter>
</activity> </activity>
<activity <activity
android:name=".GodotGame" android:name=".GodotGame"
@@ -101,8 +117,7 @@
android:autoRemoveFromRecents="true" android:autoRemoveFromRecents="true"
android:screenOrientation="landscape" android:screenOrientation="landscape"
android:resizeableActivity="false" android:resizeableActivity="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"> android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
</activity>
<!-- <!--
We remove this meta-data originating from the vendors plugin as we only need the loader for We remove this meta-data originating from the vendors plugin as we only need the loader for

View File

@@ -60,14 +60,19 @@ import org.godotengine.editor.utils.verifyApk
import org.godotengine.godot.BuildConfig import org.godotengine.godot.BuildConfig
import org.godotengine.godot.GodotActivity import org.godotengine.godot.GodotActivity
import org.godotengine.godot.GodotLib import org.godotengine.godot.GodotLib
import org.godotengine.godot.editor.utils.EditorUtils
import org.godotengine.godot.editor.utils.GameMenuUtils
import org.godotengine.godot.editor.utils.GameMenuUtils.GameEmbedMode
import org.godotengine.godot.editor.utils.GameMenuUtils.fetchGameEmbedMode
import org.godotengine.godot.error.Error import org.godotengine.godot.error.Error
import org.godotengine.godot.utils.DialogUtils import org.godotengine.godot.utils.DialogUtils
import org.godotengine.godot.utils.GameMenuUtils
import org.godotengine.godot.utils.GameMenuUtils.GameEmbedMode
import org.godotengine.godot.utils.GameMenuUtils.fetchGameEmbedMode
import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix import org.godotengine.godot.utils.ProcessPhoenix
import org.godotengine.godot.utils.isNativeXRDevice import org.godotengine.godot.utils.isNativeXRDevice
import org.godotengine.godot.xr.HybridMode
import org.godotengine.godot.xr.getHybridAppLaunchMode
import org.godotengine.godot.xr.HYBRID_APP_PANEL_CATEGORY
import org.godotengine.godot.xr.HYBRID_APP_IMMERSIVE_CATEGORY
import kotlin.math.min import kotlin.math.min
/** /**
@@ -98,6 +103,8 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager" private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p" private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
internal const val XR_MODE_ARG = "--xr-mode" internal const val XR_MODE_ARG = "--xr-mode"
private const val SCENE_ARG = "--scene"
private const val PATH_ARG = "--path"
// Info for the various classes used by the editor. // Info for the various classes used by the editor.
internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "") internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
@@ -236,6 +243,50 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
setupGameMenuBar() setupGameMenuBar()
} }
override fun onNewIntent(newIntent: Intent) {
if (newIntent.hasCategory(HYBRID_APP_PANEL_CATEGORY) || newIntent.hasCategory(HYBRID_APP_IMMERSIVE_CATEGORY)) {
val params = newIntent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
Log.d(TAG, "Received hybrid transition intent $newIntent with parameters ${params.contentToString()}")
// Override EXTRA_NEW_LAUNCH so the editor is not restarted
newIntent.putExtra(EXTRA_NEW_LAUNCH, false)
godot?.runOnRenderThread {
// Look for the scene and xr-mode arguments
var scene = ""
var xrMode = XR_MODE_DEFAULT
var path = ""
if (params != null) {
val sceneIndex = params.indexOf(SCENE_ARG)
if (sceneIndex != -1 && sceneIndex + 1 < params.size) {
scene = params[sceneIndex +1]
}
val xrModeIndex = params.indexOf(XR_MODE_ARG)
if (xrModeIndex != -1 && xrModeIndex + 1 < params.size) {
xrMode = params[xrModeIndex + 1]
}
val pathIndex = params.indexOf(PATH_ARG)
if (pathIndex != -1 && pathIndex + 1 < params.size) {
path = params[pathIndex + 1]
}
}
val sceneArgs = mutableSetOf(XR_MODE_ARG, xrMode).apply {
if (path.isNotEmpty() && scene.isEmpty()) {
add(PATH_ARG)
add(path)
}
}
Log.d(TAG, "Running scene $scene with arguments: $sceneArgs")
EditorUtils.runScene(scene, sceneArgs.toTypedArray())
}
}
super.onNewIntent(newIntent)
}
protected open fun shouldShowGameMenuBar() = gameMenuContainer != null protected open fun shouldShowGameMenuBar() = gameMenuContainer != null
private fun setupGameMenuBar() { private fun setupGameMenuBar() {
@@ -327,26 +378,41 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
} }
} }
return if (hasEditor) { if (hasEditor) {
EDITOR_MAIN_INFO return EDITOR_MAIN_INFO
} else { }
// Launching a game.
val openxrEnabled = xrMode == XR_MODE_ON || // Launching a game.
(xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean()) if (isNativeXRDevice(applicationContext)) {
if (openxrEnabled && isNativeXRDevice(applicationContext)) { if (xrMode == XR_MODE_ON) {
XR_RUN_GAME_INFO return XR_RUN_GAME_INFO
} else { }
if (godot?.isProjectManagerHint() == true || isNativeXRDevice(applicationContext)) {
if ((xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())) {
val hybridLaunchMode = getHybridAppLaunchMode()
return if (hybridLaunchMode == HybridMode.PANEL) {
RUN_GAME_INFO RUN_GAME_INFO
} else { } else {
val resolvedEmbedMode = resolveGameEmbedModeIfNeeded(gameEmbedMode) XR_RUN_GAME_INFO
if (resolvedEmbedMode == GameEmbedMode.DISABLED) {
RUN_GAME_INFO
} else {
EMBEDDED_RUN_GAME_INFO
}
} }
} }
// Native XR devices don't support embed mode yet.
return RUN_GAME_INFO
}
// Project manager doesn't support embed mode.
if (godot?.isProjectManagerHint() == true) {
return RUN_GAME_INFO
}
// Check for embed mode launch.
val resolvedEmbedMode = resolveGameEmbedModeIfNeeded(gameEmbedMode)
return if (resolvedEmbedMode == GameEmbedMode.DISABLED) {
RUN_GAME_INFO
} else {
EMBEDDED_RUN_GAME_INFO
} }
} }
@@ -626,6 +692,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
return verifyApk(godot.fileAccessHandler, apkPath) return verifyApk(godot.fileAccessHandler, apkPath)
} }
@CallSuper
override fun supportsFeature(featureTag: String): Boolean { override fun supportsFeature(featureTag: String): Boolean {
if (featureTag == "xr_editor") { if (featureTag == "xr_editor") {
return isNativeXRDevice(applicationContext) return isNativeXRDevice(applicationContext)
@@ -639,11 +706,12 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
return BuildConfig.FLAVOR == "picoos" return BuildConfig.FLAVOR == "picoos"
} }
return false return super.supportsFeature(featureTag)
} }
internal fun onEditorConnected(connectedEditorId: Int) { internal fun onEditorConnected(editorId: Int) {
when (connectedEditorId) { Log.d(TAG, "Editor $editorId connected!")
when (editorId) {
EMBEDDED_RUN_GAME_INFO.windowId, RUN_GAME_INFO.windowId -> { EMBEDDED_RUN_GAME_INFO.windowId, RUN_GAME_INFO.windowId -> {
runOnUiThread { runOnUiThread {
embeddedGameViewContainerWindow?.isVisible = false embeddedGameViewContainerWindow?.isVisible = false
@@ -652,12 +720,16 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
XR_RUN_GAME_INFO.windowId -> { XR_RUN_GAME_INFO.windowId -> {
runOnUiThread { runOnUiThread {
updateEmbeddedGameView(true, false) updateEmbeddedGameView(gameRunning = true, gameEmbedded = false)
} }
} }
} }
} }
internal fun onEditorDisconnected(editorId: Int) {
Log.d(TAG, "Editor $editorId disconnected!")
}
private fun updateEmbeddedGameView(gameRunning: Boolean, gameEmbedded: Boolean) { private fun updateEmbeddedGameView(gameRunning: Boolean, gameEmbedded: Boolean) {
if (gameRunning) { if (gameRunning) {
embeddedGameStateLabel?.apply { embeddedGameStateLabel?.apply {

View File

@@ -35,9 +35,11 @@ import android.util.Log
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import org.godotengine.godot.Godot import org.godotengine.godot.Godot
import org.godotengine.godot.GodotLib import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.GameMenuUtils import org.godotengine.godot.editor.utils.GameMenuUtils
import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix import org.godotengine.godot.utils.ProcessPhoenix
import org.godotengine.godot.xr.HYBRID_APP_FEATURE
import org.godotengine.godot.xr.isHybridAppEnabled
/** /**
* Base class for the Godot play windows. * Base class for the Godot play windows.
@@ -101,4 +103,14 @@ abstract class BaseGodotGame: GodotEditor() {
} }
protected open fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.AUTO protected open fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.AUTO
@CallSuper
override fun supportsFeature(featureTag: String): Boolean {
if (HYBRID_APP_FEATURE == featureTag) {
// Check if hybrid is enabled
return isHybridAppEnabled()
}
return super.supportsFeature(featureTag)
}
} }

View File

@@ -104,7 +104,9 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
MSG_REGISTER_MESSENGER -> { MSG_REGISTER_MESSENGER -> {
val editorId = msg.arg1 val editorId = msg.arg1
val messenger = msg.replyTo val messenger = msg.replyTo
registerMessenger(editorId, messenger) registerMessenger(editorId, messenger) {
editor.onEditorDisconnected(editorId)
}
} }
MSG_DISPATCH_GAME_MENU_ACTION -> { MSG_DISPATCH_GAME_MENU_ACTION -> {
@@ -211,8 +213,8 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
} else if (messenger.binder.isBinderAlive) { } else if (messenger.binder.isBinderAlive) {
messenger.binder.linkToDeath({ messenger.binder.linkToDeath({
Log.v(TAG, "Removing messenger for $editorId") Log.v(TAG, "Removing messenger for $editorId")
cleanEditorConnection(editorId)
messengerDeathCallback?.run() messengerDeathCallback?.run()
cleanEditorConnection(editorId)
}, 0) }, 0)
editorConnectionsInfos[editorId] = EditorConnectionInfo(messenger) editorConnectionsInfos[editorId] = EditorConnectionInfo(messenger)
editor.onEditorConnected(editorId) editor.onEditorConnected(editorId)
@@ -234,7 +236,8 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
/** /**
* Utility method to register a [Messenger] attached to this handler with a host. * Utility method to register a [Messenger] attached to this handler with a host.
* *
* This is done so that the host can send request to the editor instance attached to this handle. * This is done so that the host can send request (e.g: force-quit when the host exits) to the editor instance
* attached to this handle.
* *
* Note that this is only done when the editor instance is internal (not exported) to prevent * Note that this is only done when the editor instance is internal (not exported) to prevent
* arbitrary apps from having the ability to send requests. * arbitrary apps from having the ability to send requests.

View File

@@ -37,12 +37,17 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import androidx.annotation.CallSuper
import androidx.core.view.isVisible import androidx.core.view.isVisible
import org.godotengine.editor.embed.GameMenuFragment import org.godotengine.editor.embed.GameMenuFragment
import org.godotengine.godot.utils.GameMenuUtils import org.godotengine.godot.GodotLib
import org.godotengine.godot.editor.utils.GameMenuUtils
import org.godotengine.godot.utils.ProcessPhoenix import org.godotengine.godot.utils.ProcessPhoenix
import org.godotengine.godot.utils.isHorizonOSDevice import org.godotengine.godot.utils.isHorizonOSDevice
import org.godotengine.godot.utils.isNativeXRDevice import org.godotengine.godot.utils.isNativeXRDevice
import org.godotengine.godot.xr.HYBRID_APP_PANEL_FEATURE
import org.godotengine.godot.xr.XRMode
import org.godotengine.godot.xr.isHybridAppEnabled
/** /**
* Drives the 'run project' window of the Godot Editor. * Drives the 'run project' window of the Godot Editor.
@@ -82,6 +87,18 @@ open class GodotGame : BaseGodotGame() {
} }
} }
override fun getCommandLine(): MutableList<String> {
val updatedArgs = super.getCommandLine()
if (!updatedArgs.contains(XRMode.REGULAR.cmdLineArg)) {
updatedArgs.add(XRMode.REGULAR.cmdLineArg)
}
if (!updatedArgs.contains(XR_MODE_ARG)) {
updatedArgs.add(XR_MODE_ARG)
updatedArgs.add("off")
}
return updatedArgs
}
override fun enterPiPMode() { override fun enterPiPMode() {
if (hasPiPSystemFeature()) { if (hasPiPSystemFeature()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -245,4 +262,19 @@ open class GodotGame : BaseGodotGame() {
expandGameMenuButton?.isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable() && collapsed expandGameMenuButton?.isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable() && collapsed
} }
@CallSuper
override fun supportsFeature(featureTag: String): Boolean {
if (HYBRID_APP_PANEL_FEATURE == featureTag) {
// Check if openxr is enabled
if (!GodotLib.getGlobal("xr/openxr/enabled").toBoolean()) {
return false
}
// Check if hybrid is enabled
return isHybridAppEnabled()
}
return super.supportsFeature(featureTag)
}
} }

View File

@@ -40,7 +40,7 @@ import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
import org.godotengine.editor.GodotGame import org.godotengine.editor.GodotGame
import org.godotengine.editor.R import org.godotengine.editor.R
import org.godotengine.godot.utils.GameMenuUtils import org.godotengine.godot.editor.utils.GameMenuUtils
/** /**
* Host the Godot game from the editor when the embedded mode is enabled. * Host the Godot game from the editor when the embedded mode is enabled.

View File

@@ -105,7 +105,7 @@ android {
} }
boolean devBuild = buildType == "dev" boolean devBuild = buildType == "dev"
boolean debugSymbols = devBuild boolean debugSymbols = devBuild || (buildType == "debug" && isAndroidStudio())
boolean runTests = devBuild boolean runTests = devBuild
boolean storeRelease = buildType == "release" boolean storeRelease = buildType == "release"
boolean productionBuild = storeRelease boolean productionBuild = storeRelease

View File

@@ -1026,7 +1026,7 @@ class Godot private constructor(val context: Context) {
*/ */
@Keep @Keep
private fun hasFeature(feature: String): Boolean { private fun hasFeature(feature: String): Boolean {
if (primaryHost?.supportsFeature(feature) ?: false) { if (primaryHost?.supportsFeature(feature) == true) {
return true; return true;
} }

View File

@@ -55,7 +55,7 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
private val TAG = GodotActivity::class.java.simpleName private val TAG = GodotActivity::class.java.simpleName
@JvmStatic @JvmStatic
protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
@JvmStatic @JvmStatic
protected val EXTRA_NEW_LAUNCH = "new_launch_requested" protected val EXTRA_NEW_LAUNCH = "new_launch_requested"

View File

@@ -0,0 +1,41 @@
/**************************************************************************/
/* EditorUtils.kt */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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.editor.utils
/**
* Utility class for accessing and using editor specific capabilities.
*
* This class is only functional on editor builds.
*/
object EditorUtils {
@JvmStatic
external fun runScene(scene: String, sceneArgs: Array<String>)
}

View File

@@ -28,13 +28,15 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/ /**************************************************************************/
package org.godotengine.godot.utils package org.godotengine.godot.editor.utils
import android.util.Log import android.util.Log
import org.godotengine.godot.GodotLib import org.godotengine.godot.GodotLib
/** /**
* Utility class for accessing and using game menu APIs. * Utility class for accessing and using game menu APIs.
*
* This class is only functional on editor builds.
*/ */
object GameMenuUtils { object GameMenuUtils {
private val TAG = GameMenuUtils::class.java.simpleName private val TAG = GameMenuUtils::class.java.simpleName

View File

@@ -0,0 +1,79 @@
/**************************************************************************/
/* HybridAppUtils.kt */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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. */
/**************************************************************************/
/**
* Contains utility methods and constants for hybrid apps.
*/
@file:JvmName("HybridAppUtils")
package org.godotengine.godot.xr
import android.util.Log
import org.godotengine.godot.GodotLib
private const val TAG = "HybridAppUtils"
enum class HybridMode(private val nativeValue: Int) {
NONE( -1),
IMMERSIVE(0),
PANEL(1);
companion object {
fun fromNative(nativeValue: Int): HybridMode {
for (mode in HybridMode.entries) {
if (mode.nativeValue == nativeValue) {
return mode
}
}
return NONE
}
}
}
const val HYBRID_APP_FEATURE = "godot_openxr_hybrid_app"
const val HYBRID_APP_PANEL_FEATURE = "godot_openxr_panel_app"
const val HYBRID_APP_PANEL_CATEGORY = "org.godotengine.xr.hybrid.PANEL"
const val HYBRID_APP_IMMERSIVE_CATEGORY = "org.godotengine.xr.hybrid.IMMERSIVE"
fun isHybridAppEnabled() = GodotLib.getGlobal("xr/hybrid_app/enabled").toBoolean()
fun getHybridAppLaunchMode(): HybridMode {
if (!isHybridAppEnabled()) {
return HybridMode.NONE
}
try {
val launchModeValue = GodotLib.getGlobal("xr/hybrid_app/launch_mode").toInt()
return HybridMode.fromNative(launchModeValue)
} catch (e: Exception) {
Log.w(TAG, "Unable to retrieve 'xr/hybrid_app/launch_mode' project setting", e)
return HybridMode.NONE
}
}