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.
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
if (dbg->is_session_active()) {
dbg->_stop_and_notify();
}
dbg->_stop_and_notify();
});
_break_state_changed();
breakpoints.clear();

View File

@@ -2176,7 +2176,7 @@ void EditorNode::try_autosave() {
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.
_save_scene(scene->get_scene_file_path());
_save_scene(scene->get_scene_file_path(), -1, false);
}
}
_menu_option(SCENE_SAVE_ALL_SCENES);

View File

@@ -32,7 +32,8 @@ android_files = [
"rendering_context_driver_vulkan_android.cpp",
"variant/callable_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()

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" {
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
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
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
}
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
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
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
}
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
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
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
}
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
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
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
}
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
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
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
}
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
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
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
}
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
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
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
}
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
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
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
}
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
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
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
}
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
if (EditorInterface::get_singleton()) {
EditorInterface::get_singleton()->play_main_scene();
@@ -134,7 +134,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainSc
#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
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {

View File

@@ -33,15 +33,15 @@
#include <jni.h>
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_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_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_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_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_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_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_resetCamera2DPosition(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_editor_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz);
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 {
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
implementation project(":lib")

View File

@@ -60,6 +60,22 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</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
android:name=".GodotGame"
@@ -101,8 +117,7 @@
android:autoRemoveFromRecents="true"
android:screenOrientation="landscape"
android:resizeableActivity="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
</activity>
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
<!--
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.GodotActivity
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.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.ProcessPhoenix
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
/**
@@ -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_SHORT = "-p"
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.
internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
@@ -236,6 +243,50 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
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
private fun setupGameMenuBar() {
@@ -327,26 +378,41 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
}
}
return if (hasEditor) {
EDITOR_MAIN_INFO
} else {
// Launching a game.
val openxrEnabled = xrMode == XR_MODE_ON ||
(xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())
if (openxrEnabled && isNativeXRDevice(applicationContext)) {
XR_RUN_GAME_INFO
} else {
if (godot?.isProjectManagerHint() == true || isNativeXRDevice(applicationContext)) {
if (hasEditor) {
return EDITOR_MAIN_INFO
}
// Launching a game.
if (isNativeXRDevice(applicationContext)) {
if (xrMode == XR_MODE_ON) {
return XR_RUN_GAME_INFO
}
if ((xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())) {
val hybridLaunchMode = getHybridAppLaunchMode()
return if (hybridLaunchMode == HybridMode.PANEL) {
RUN_GAME_INFO
} else {
val resolvedEmbedMode = resolveGameEmbedModeIfNeeded(gameEmbedMode)
if (resolvedEmbedMode == GameEmbedMode.DISABLED) {
RUN_GAME_INFO
} else {
EMBEDDED_RUN_GAME_INFO
}
XR_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)
}
@CallSuper
override fun supportsFeature(featureTag: String): Boolean {
if (featureTag == "xr_editor") {
return isNativeXRDevice(applicationContext)
@@ -639,11 +706,12 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
return BuildConfig.FLAVOR == "picoos"
}
return false
return super.supportsFeature(featureTag)
}
internal fun onEditorConnected(connectedEditorId: Int) {
when (connectedEditorId) {
internal fun onEditorConnected(editorId: Int) {
Log.d(TAG, "Editor $editorId connected!")
when (editorId) {
EMBEDDED_RUN_GAME_INFO.windowId, RUN_GAME_INFO.windowId -> {
runOnUiThread {
embeddedGameViewContainerWindow?.isVisible = false
@@ -652,12 +720,16 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
XR_RUN_GAME_INFO.windowId -> {
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) {
if (gameRunning) {
embeddedGameStateLabel?.apply {

View File

@@ -35,9 +35,11 @@ import android.util.Log
import androidx.annotation.CallSuper
import org.godotengine.godot.Godot
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.ProcessPhoenix
import org.godotengine.godot.xr.HYBRID_APP_FEATURE
import org.godotengine.godot.xr.isHybridAppEnabled
/**
* Base class for the Godot play windows.
@@ -101,4 +103,14 @@ abstract class BaseGodotGame: GodotEditor() {
}
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 -> {
val editorId = msg.arg1
val messenger = msg.replyTo
registerMessenger(editorId, messenger)
registerMessenger(editorId, messenger) {
editor.onEditorDisconnected(editorId)
}
}
MSG_DISPATCH_GAME_MENU_ACTION -> {
@@ -211,8 +213,8 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
} else if (messenger.binder.isBinderAlive) {
messenger.binder.linkToDeath({
Log.v(TAG, "Removing messenger for $editorId")
cleanEditorConnection(editorId)
messengerDeathCallback?.run()
cleanEditorConnection(editorId)
}, 0)
editorConnectionsInfos[editorId] = EditorConnectionInfo(messenger)
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.
*
* 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
* 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.util.Log
import android.view.View
import androidx.annotation.CallSuper
import androidx.core.view.isVisible
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.isHorizonOSDevice
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.
@@ -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() {
if (hasPiPSystemFeature()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -245,4 +262,19 @@ open class GodotGame : BaseGodotGame() {
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 org.godotengine.editor.GodotGame
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.

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
private val TAG = GodotActivity::class.java.simpleName
@JvmStatic
protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
@JvmStatic
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. */
/**************************************************************************/
package org.godotengine.godot.utils
package org.godotengine.godot.editor.utils
import android.util.Log
import org.godotengine.godot.GodotLib
/**
* Utility class for accessing and using game menu APIs.
*
* This class is only functional on editor builds.
*/
object GameMenuUtils {
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
}
}