diff --git a/.conform.yaml b/.conform.yaml
index 824927c9f..38ac78b5e 100644
--- a/.conform.yaml
+++ b/.conform.yaml
@@ -36,4 +36,5 @@ policies:
- "docs"
- "cli"
- "deps"
+ - "jni"
descriptionLength: 72
diff --git a/README.md b/README.md
index eb847074e..8aade6a01 100644
--- a/README.md
+++ b/README.md
@@ -39,3 +39,4 @@ The template offers the following feature switches which can be enabled during c
* `monitor`: create a thin decorator class which can be used to log traffic going through API layer
* `olink`: create the adaption layer for the [OLink](https://docs.apigear.io/docs/advanced/protocols/objectlink/intro) protocol. This can be used to connect to the simulation or other technologies like python, pure c++ etc..
* `msgbus`: create the adaption layer for the [UE MsgBus](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Messaging) protocol. This can be used for communication inside the same UE app or between different instances, including across network interfaces.
+* `jni`: create the jni adaption layer for the android messenger communication provided by apigear java template. Please read more on a separate document readme_jni.
diff --git a/readme_jni.md b/readme_jni.md
new file mode 100644
index 000000000..1bf5f7bb9
--- /dev/null
+++ b/readme_jni.md
@@ -0,0 +1,102 @@
+# Unreal Jni Adapter
+
+The feature "jni" provides the jni adaption layer for the android messenger communication, which for each interface must be generated with ApiGear java template.
+The android build requires the modules provided with the java template. They are compiled within an unreal application with its build system.
+Make also sure that your unreal app has proper settings selected to make it available to run on android devices (like allowing large obb files or some android permissions enabled).
+
+## Code generation
+
+Java template provides features:
+**api, stub, android, jnibridge** - which a required for the unreal jni feature to work.
+You can also take a look at the **testclientapp** and **testserviceapp** features which generates a simple example of client or service android app. Those apps only serve first defined interface, and require user to fill partially code for buttons - you need to provide the arguments values. You may use those apps as a starting point to test your unreal side app.
+
+The code generated by java template starting form the top-level folder for the module should be placed in your modules Jni plugin under android folder.
+For example for the module named `"HelloWorld"` the generated folder `helloworld` the path should be:
+```
+YourProject/Plugins/HelloWorld/Source/HelloWorldJni/android
+```
+You can achieve this by configuring your `*.solution.yaml` file e.g. like that
+
+```
+schema: "apigear.solution/1.0"
+name: helloworld
+version: "0.1.0"
+
+layers:
+ - name: unreal
+ inputs:
+ - myhelloworld.module.yaml
+ output: ../relative-path-to/YourProject
+ template: apigear-io/template-unreal //TODO MINIMAL VERSION
+ features: ["api","jni","stubs"]
+ - name: java
+ inputs:
+ - myhelloworld.module.yaml
+ output: ../relative-path-to/YourProject/Plugins/HelloWorld/Source/HelloWorldJni/android
+ template: apigear-io/template-java
+ features: ["api","android","stubs","jnibridge"]
+```
+Remember that re-generation may overwrite all your manual changes to generated files.
+
+### Versions
+Currently minimum supported sdk version for android is 33.
+Currently tested UE version is 5.5.
+
+## How to use
+
+Generated code provides:
+* Jni Service Adapter
+* Jni Client
+
+Have in mind the most probably you'll need only one side in your app. The generated objects are a *game subssystems* and they are always started. Moreover the android service starts during the Service Adapter initialization. It will not collide with your client trying to connect other implementation of the service as you need to specify package from which you'd like to use the service, but still may be not wanted overhead in your app.
+
+### Jni Service Adapter
+
+The Jni Service Adapter treats exposes your Local Implementation of the api (requires setting a backend) as an android service. It is generated per interface described in your `*.module.yaml`
+When the Jni Service Adapter is initialized it creates an android service, with a jni backend implemented by that Jni Service Adapter.
+Notice that there will be as many `andorid services` as the interfaces declared in the `*.module.yaml` file. When it comes to threads - no extra thread is spawned for each service, they are all in the applications thread.
+All the services are already exposed in the unreal app `AndroidManifest.xml`.
+
+### Jni Client
+Jni Client adapter is generated per interface. It uses the same interfaces as other ApiGear generated versions (OlinkClient, MessageBusClient or the local implementation).
+It is recommended to use it through that interface, which allows you to easy change of the "data input" and test your application first on your desktop e.g. using OLink simulation.
+To make your client work you need to bind to a **running service**.
+The client does not start any service while trying to bind. The re-connection is not automated neither when service died nor when it wasn't started at all.
+The binding may be performed from the blueprints. It requires the package name of the app that provides (and starts) the requested android service and a unique connection Id (among the this service).
+The client unbinds automatically on deinitialization.
+All the necessary queries and permissions are configured in the unreal app` AndroidManifest.xml`.
+
+## Application permissions
+
+Here is a suggested configuration of the unreal project for automotive.
+You can use settings in unreal editor or you can change your DefaultEngine.ini file.
+
+/***
+
+[/Script/AndroidRuntimeSettings.AndroidRuntimeSettings]
+PackageName=your.pacakge.[PROJECT]
+...
+MinSDKVersion=33
+TargetSDKVersion=33
+...
+bDisableVerifyOBBOnStartUp=False
+bForceSmallOBBFiles=False
+bAllowLargeOBBFiles=True
+...
+ExtraApplicationNodeTags=android:requestLegacyExternalStorage="true"
+ExtraPermissions=android.permission.READ_MEDIA_IMAGES
+ExtraPermissions=android.permission.WRITE_EXTERNAL_STORAGE
+ExtraPermissions=android.permission.READ_MEDIA_VIDEO
+ExtraPermissions=android.permission.READ_MEDIA_AUDIO
+ExtraPermissions=android.permission.READ_EXTERNAL_STORAGE
+ExtraPermissions=android.permission.MANAGE_EXTERNAL_STORAGE
+
+***/
+
+Remember to fill the `PackageName` field with `your.pacakge.[PROJECT]` especially when providing the service side. `your.package` is the package name that all the client should provide on binding to bind this service.
+
+The minimum target version should not be lower than the one that is required for java template.
+
+The OBB settings allow the unreal app to start on android device. By default android does not allow large OBB files, but unreal apps usually have them large.
+
+Unreal app also requires some extra permissions.
diff --git a/rules.yaml b/rules.yaml
index 252f4bc14..a54c6f280 100644
--- a/rules.yaml
+++ b/rules.yaml
@@ -473,3 +473,59 @@ features:
- source: "module/Source/modulemsgbus/Private/Tests/msgbusfixture.cpp.tpl"
target: "Private/Tests/{{Camel .Module.Name}}{{Camel .Interface.Name}}MsgBusFixture.cpp"
preserve: true
+ - name: jni
+ requires:
+ - api
+ - plugin
+ scopes:
+ - match: module
+ prefix: "Plugins/{{Camel .Module.Name}}/"
+ documents:
+ - source: "module/Source/modulejni/Private/modulejni.cpp.tpl"
+ target: "Source/{{Camel .Module.Name}}Jni/Private/Generated/{{Camel .Module.Name}}Jni.cpp"
+ - source: "module/Source/modulejni/Public/modulejni.h.tpl"
+ target: "Source/{{Camel .Module.Name}}Jni/Public/{{Camel .Module.Name}}/{{Camel .Module.Name}}Jni.h"
+ - source: "module/Source/modulejni/modulejni.Build.cs.tpl"
+ target: "Source/{{Camel .Module.Name}}Jni/{{Camel .Module.Name}}Jni.Build.cs"
+ - source: "module/Source/modulejni/jni_UPL.xml.tpl"
+ target: "Source/{{Camel .Module.Name}}Jni/{{Camel .Module.Name}}_JNI_UPL.xml"
+ - source: "module/Source/modulejni/android/buildsystem/config.gradle"
+ target: "android/buildsystem/config.gradle"
+ raw: true
+ - source: "module/Source/modulejni/android/buildsystem/dependencies.gradle"
+ target: "android/buildsystem/dependencies.gradle"
+ raw: true
+ - source: "module/Source/modulejni/Private/Generated/jni/datajavaconverter.cpp.tpl"
+ target: "Source/{{Camel .Module.Name}}Jni/Private/Generated/Jni/{{Camel .Module.Name}}DataJavaConverter.cpp"
+ - source: "module/Source/modulejni/Public/Generated/jni/datajavaconverter.h.tpl"
+ target: "Source/{{Camel .Module.Name}}Jni/Public/{{Camel .Module.Name}}/Generated/Jni/{{Camel .Module.Name}}DataJavaConverter.h"
+ - source: "module/Source/modulejni/Public/Generated/jni/jniconnectionstatus.h.tpl"
+ target: "Source/{{Camel .Module.Name}}Jni/Public/{{Camel .Module.Name}}//Generated/Jni/{{Camel .Module.Name}}JniConnectionStatus.h"
+ - match: interface
+ prefix: "Plugins/{{Camel .Module.Name}}/Source/{{Camel .Module.Name}}Jni/"
+ documents:
+ - source: "module/Source/modulejni/Public/Generated/jni/jniadapter.h.tpl"
+ target: "Public/{{Camel .Module.Name}}/Generated/Jni/{{Camel .Module.Name}}{{Camel .Interface.Name}}JniAdapter.h"
+ - source: "module/Source/modulejni/Private/Generated/jni/jniadapter.cpp.tpl"
+ target: "Private/Generated/Jni/{{Camel .Module.Name}}{{Camel .Interface.Name}}JniAdapter.cpp"
+ - source: "module/Source/modulejni/Public/Generated/jni/jniclient.h.tpl"
+ target: "Public/{{Camel .Module.Name}}/Generated/Jni/{{Camel .Module.Name}}{{Camel .Interface.Name}}JniClient.h"
+ - source: "module/Source/modulejni/Private/Generated/jni/jniclient.cpp.tpl"
+ target: "Private/Generated/Jni/{{Camel .Module.Name}}{{Camel .Interface.Name}}JniClient.cpp"
+ - name: jni_tests
+ requires:
+ - jni
+ - stubs
+ scopes:
+ - match: interface
+ prefix: "Plugins/{{Camel .Module.Name}}/Source/{{Camel .Module.Name}}Jni/"
+ documents:
+ - source: "module/Source/modulejni/Private/Tests/jni.spec.cpp.tpl"
+ target: "Private/Tests/{{Camel .Module.Name}}{{Camel .Interface.Name}}Jni.spec.cpp"
+ preserve: true
+ - source: "module/Source/modulejni/Private/Tests/jnifixture.h.tpl"
+ target: "Private/Tests/{{Camel .Module.Name}}{{Camel .Interface.Name}}JniFixture.h"
+ preserve: true
+ - source: "module/Source/modulejni/Private/Tests/jnifixture.cpp.tpl"
+ target: "Private/Tests/{{Camel .Module.Name}}{{Camel .Interface.Name}}JniFixture.cpp"
+ preserve: true
\ No newline at end of file
diff --git a/templates/buildTestPlugins.bat.tpl b/templates/buildTestPlugins.bat.tpl
index 5dd251a23..31b7eb554 100644
--- a/templates/buildTestPlugins.bat.tpl
+++ b/templates/buildTestPlugins.bat.tpl
@@ -74,7 +74,7 @@ if %ERRORLEVEL% GEQ 1 exit /b %ERRORLEVEL%
{{ end }}
@REM run build and tests
-call :buildTestPlugins "%ProjectTarget_path%/TP_Blank.uproject" %script_path% ".Impl.{{ if .Features.olink -}}+.OLink.{{ end }}{{ if .Features.msgbus_tests -}}+.MsgBus.{{ end }}"
+call :buildTestPlugins "%ProjectTarget_path%/TP_Blank.uproject" %script_path% ".Impl.{{ if .Features.olink -}}+.OLink.{{ end }}{{ if .Features.msgbus_tests -}}+.MsgBus.{{ end }}{{ if .Features.jni_tests -}}+.Jni.{{ end }}"
exit /b 0
@REM function implementations
diff --git a/templates/buildTestPlugins.sh.tpl b/templates/buildTestPlugins.sh.tpl
index d5547a2e0..1f20bf588 100644
--- a/templates/buildTestPlugins.sh.tpl
+++ b/templates/buildTestPlugins.sh.tpl
@@ -75,5 +75,5 @@ mkdir -p "$ProjectTarget_path/Plugins/{{Camel .Name}}" && cp -rf "$script_path/P
if [ $? -ne 0 ]; then exit 1; fi;
{{ end }}
-buildTestPlugins "$ProjectTarget_path/TP_Blank.uproject" "$script_path" ".Impl.{{ if .Features.olink -}}+.OLink.{{ end }}{{ if .Features.msgbus_tests -}}+.MsgBus.{{ end }}"
+buildTestPlugins "$ProjectTarget_path/TP_Blank.uproject" "$script_path" ".Impl.{{ if .Features.olink -}}+.OLink.{{ end }}{{ if .Features.msgbus_tests -}}+.MsgBus.{{ end }}{{ if .Features.jni_tests -}}+.Jni.{{ end }}"
if [ $buildresult -ne 0 ]; then exit 1; fi;
diff --git a/templates/module/Source/modulecore/Private/Tests/tests_common.cpp.tpl b/templates/module/Source/modulecore/Private/Tests/tests_common.cpp.tpl
index b8b13d238..afd991186 100644
--- a/templates/module/Source/modulecore/Private/Tests/tests_common.cpp.tpl
+++ b/templates/module/Source/modulecore/Private/Tests/tests_common.cpp.tpl
@@ -39,4 +39,22 @@ TArray<{{$class}}> createTest{{$class }}Array()
return TestValueArray;
}
{{ end }}
+
+{{- range .Module.Enums }}
+{{- $class := printf "E%s%s" $ModuleName .Name }}
+{{- $quickFixclass := printf "F%s%s" $ModuleName .Name }}
+TArray<{{$class}}> createTest{{$quickFixclass }}Array()
+{
+ TArray<{{$class}}> TestValueArray;
+ {{- $moduleEnumName := printf "%s%s" $ModuleName .Name }}
+{{if len .Members }}
+ {{- $member:= (index .Members 0) }}
+ {{$class}} val = {{$class}}::{{ abbreviate $moduleEnumName }}_{{ Camel $member.Name }};
+ TestValueArray.Add(val);
+{{- end }}
+ return TestValueArray;
+}
+
+{{ end }}
+
#endif // WITH_DEV_AUTOMATION_TESTS
diff --git a/templates/module/Source/modulecore/Public/Tests/tests_common.h.tpl b/templates/module/Source/modulecore/Public/Tests/tests_common.h.tpl
index 758602674..d582b35b1 100644
--- a/templates/module/Source/modulecore/Public/Tests/tests_common.h.tpl
+++ b/templates/module/Source/modulecore/Public/Tests/tests_common.h.tpl
@@ -26,4 +26,10 @@ constexpr int {{$ModuleName}}TestFilterMask = EAutomationTestFlags::ApplicationC
TArray<{{$class}}> {{$API_MACRO}} createTest{{$class }}Array();
{{ end }}
+
+{{- range .Module.Enums }}
+{{- $class := printf "E%s%s" $ModuleName .Name }}
+{{- $quickFixclass := printf "F%s%s" $ModuleName .Name }}
+TArray<{{$class}}> {{$API_MACRO}} createTest{{$quickFixclass }}Array();
+{{ end }}
#endif // WITH_DEV_AUTOMATION_TESTS
diff --git a/templates/module/Source/modulejni/Private/Generated/jni/datajavaconverter.cpp.tpl b/templates/module/Source/modulejni/Private/Generated/jni/datajavaconverter.cpp.tpl
new file mode 100644
index 000000000..b35cc7c45
--- /dev/null
+++ b/templates/module/Source/modulejni/Private/Generated/jni/datajavaconverter.cpp.tpl
@@ -0,0 +1,304 @@
+{{/* Copyright Epic Games, Inc. All Rights Reserved */}}
+{{- $API_MACRO := printf "%sAPI_API" (CAMEL .Module.Name) }}
+{{- $ModuleName := Camel .Module.Name -}}
+{{- $moduleName := camel .Module.Name -}}
+{{- $Category := printf "ApiGear|%s" $ModuleName }}
+/**
+Copyright 2021 ApiGear UG
+Copyright 2021 Epic Games, Inc.
+
+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.
+*/
+
+#include "{{$ModuleName}}/Generated/Jni/{{$ModuleName}}DataJavaConverter.h"
+
+{{- $includes := getEmptyStringList}}
+{{- range .Module.Externs }}
+{{- $class := ueExtern . }}
+{{- if $class.Include }}
+{{- $includeName := printf "\"%s\"" $class.Include }}
+{{- $includes = (appendList $includes $includeName) }}
+{{- end }}
+{{- end }}
+{{- range .Module.Imports }}
+{{- $importModuleName := Camel .Name }}
+{{- $includeName := printf "\"%s/Generated/api/%s_data.h\"" $importModuleName $importModuleName }}
+{{- $includeName := printf "\"%s/Generated/Jni/%sDataJavaConverter.h\"" $importModuleName $importModuleName }}
+{{- $includes = (appendList $includes $includeName) }}
+{{- end }}
+{{- $includes = unique $includes }}
+{{ range $includes }}
+#include {{ .}}
+{{- end }}
+{{ if or (len .Module.Enums) (len .Module.Structs) -}}
+#include "{{$ModuleName}}/Generated/api/{{ $ModuleName }}_data.h"
+{{ end }}
+
+#if PLATFORM_ANDROID
+
+#include "Engine/Engine.h"
+#include "Android/AndroidJNI.h"
+#include "Android/AndroidApplication.h"
+
+#if USE_ANDROID_JNI
+#include
+#endif
+#endif
+
+
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+
+{{$className := printf "%sDataJavaConverter" $ModuleName}}
+
+{{- range .Module.Structs }}
+
+{{- $structType := printf "F%s%s" $ModuleName .Name }}
+{{- $structName := printf "out_%s" (snake .Name)}}
+
+void {{$className }}::fill{{Camel .Name }}(JNIEnv* env, jobject input, {{$structType}}& {{$structName}})
+{
+ jclass cls = env->GetObjectClass(input);
+
+{{- range .Fields }}
+ {{- $cppFieldName := .Name}}
+ {{- $javaFieldName := camel .Name}}
+
+ jfieldID jFieldId_{{snake .Name}} = env->GetFieldID(cls, "{{$javaFieldName}}", "{{jniSignatureType . }}");
+
+{{- if .IsArray }}
+ {{ jniToReturnType . }} {{snake .Name}}_value = ({{jniToReturnType . }})env->GetObjectField(input, jFieldId_{{snake .Name}});
+{{- if (eq .KindType "enum") }}
+ {{- $otherModuleClassName := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+ {{if not (eq $otherModuleClassName "DataJavaConverter" )}}{{$otherModuleClassName}}::{{end -}}
+ fill{{Camel .Type }}Array(env, {{snake .Name}}_value, {{$structName}}.{{$cppFieldName}});
+{{- else if (eq .KindType "string")}}
+ {{$structName}}.{{$cppFieldName}} = FJavaHelper::ObjectArrayToFStringTArray(env, {{snake .Name}}_value);
+
+ {{- else if (eq .KindType "bool")}}
+ jsize len{{snake .Name}} = env->GetArrayLength({{snake .Name}}_value);
+ {{$structName}}.{{$cppFieldName}}.Reserve(len{{snake .Name}});
+ TArray Temp;
+ Temp.SetNumUninitialized(len{{snake .Name}});
+ env->GetBooleanArrayRegion({{snake .Name}}_value, 0, len{{snake .Name}}, Temp.GetData());
+ for (int i = 0; i < len{{snake .Name}}; i++)
+ {
+ {{$structName}}.{{$cppFieldName}}.Add(Temp[i] == JNI_TRUE);
+ }
+ env->DeleteLocalRef({{snake .Name}}_value);
+
+{{- else if .IsPrimitive }}
+ jsize len{{snake .Name}} = env->GetArrayLength({{snake .Name}}_value);
+ {{$structName}}.{{$cppFieldName}}.AddUninitialized(len{{snake .Name}});
+ env->Get{{jniToEnvNameType .}}ArrayRegion({{snake .Name}}_value, 0, len{{snake .Name}}, {{- if (eq .KindType "int64") -}}
+ reinterpret_cast({{$structName}}.{{$cppFieldName}}.GetData()));
+ {{- else -}}
+ {{$structName}}.{{$cppFieldName}}.GetData());
+ {{- end }}
+{{- else if not (eq .KindType "extern")}}
+ {{- $otherModuleClassName := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+ {{if not (eq $otherModuleClassName "DataJavaConverter" )}}{{$otherModuleClassName}}::{{end -}}
+ fill{{Camel .Type }}Array(env, {{snake .Name}}_value, {{$structName}}.{{$cppFieldName}});
+{{- end }}
+ env->DeleteLocalRef({{snake .Name}}_value);
+{{- else if eq .KindType "enum"}}
+ {{ jniToReturnType . }} {{snake .Name}}_value = env->GetObjectField(input, jFieldId_{{snake .Name}});
+ {{- $otherModuleClassName := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+ {{$structName}}.{{$cppFieldName}} = {{if not (eq $otherModuleClassName "DataJavaConverter" )}}{{$otherModuleClassName}}::{{end -}}
+ get{{Camel .Type }}Value(env, {{snake .Name}}_value);
+{{- else if eq .KindType "string"}}
+ jstring {{snake .Name}}_value = (jstring)env->GetObjectField(input, jFieldId_{{snake .Name}});
+ {{$structName}}.{{$cppFieldName}} = FJavaHelper::FStringFromLocalRef(env,{{snake .Name}}_value);
+{{- else if .IsPrimitive }}
+ {{$structName}}.{{$cppFieldName}} = env->Get{{jniToEnvNameType .}}Field(input, jFieldId_{{snake .Name}});
+{{- else if not (eq .KindType "extern")}}
+ {{ jniToReturnType . }} {{snake .Name}}_value = env->GetObjectField(input, jFieldId_{{snake .Name}});
+ {{- $otherModuleClassName := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+ {{if not (eq $otherModuleClassName "DataJavaConverter" )}}{{$otherModuleClassName}}::{{end -}}
+ fill{{Camel .Type }}(env, {{snake .Name}}_value,{{$structName}}.{{$cppFieldName }});
+ env->DeleteLocalRef({{snake .Name}}_value);
+{{- end }}
+
+{{- end }}
+
+}
+
+void {{$className }}::fill{{Camel .Name }}Array(JNIEnv* env, jobjectArray input, TArray<{{$structType}}>& out_array)
+{
+ jsize len = env->GetArrayLength(input);
+ out_array.Reserve(len);
+ out_array.AddDefaulted(len);
+ for (jsize i = 0; i < len; ++i)
+ {
+ jobject element = env->GetObjectArrayElement(input, i);
+ fill{{Camel .Name }}(env, element, out_array[i]);
+ env->DeleteLocalRef(element);
+ }
+}
+
+{{- $in_cppStructName := printf "out_%s" (snake .Name)}}
+{{- $api_package_name := printf "%s_api" (camel $moduleName) }}
+{{- $packageName := ( join "/" (strSlice $moduleName $api_package_name) ) }}
+{{- $javaClassTypeName := .Name}}
+
+jobject {{$className }}::makeJava{{Camel .Name }}(JNIEnv* env, const {{$structType}}& {{$in_cppStructName}})
+{
+ jclass javaClass = FAndroidApplication::FindJavaClassGlobalRef("{{$packageName}}/{{$javaClassTypeName}}");
+ jmethodID ctor = env->GetMethodID(javaClass, "", "()V");
+ jobject javaObjInstance = env->NewObject(javaClass, ctor);
+
+{{- range .Fields }}
+ {{- $cppFieldName := .Name}}
+ {{- $javaFieldName := camel .Name}}
+ {{- $tmpObjName := printf "l_%s" $javaFieldName }}
+
+ jfieldID jFieldId_{{snake .Name}} = env->GetFieldID(javaClass, "{{$javaFieldName}}", "{{jniSignatureType . }}");
+
+{{- if .IsArray }}
+ {{ jniToReturnType . }} {{snake .Name}}_value = ({{ jniToReturnType . }})env->GetObjectField(javaObjInstance,jFieldId_{{snake .Name}});
+{{- if (eq .KindType "enum") }}
+ {{- $otherModuleClassName := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+ jobjectArray {{$tmpObjName}} = {{if not (eq $otherModuleClassName "DataJavaConverter" )}}{{$otherModuleClassName}}::{{end -}}
+ makeJava{{Camel .Type }}Array(env, {{$structName}}.{{$cppFieldName}});
+{{- else if (eq .KindType "string")}}
+ TArray {{$cppFieldName}}StringViews;
+ {{$cppFieldName}}StringViews.Reserve({{$structName}}.{{$cppFieldName}}.Num());
+ for (const FString& Str : {{$structName}}.{{$cppFieldName}})
+ {
+ {{$cppFieldName}}StringViews.Add(FStringView(Str));
+ }
+ auto {{$tmpObjName}}Wrapper = FJavaHelper::ToJavaStringArray(env,{{$cppFieldName}}StringViews);
+ jobjectArray {{$tmpObjName}} = static_cast(env->NewLocalRef(*{{$tmpObjName}}Wrapper));
+{{- else if (eq .KindType "bool")}}
+ auto len{{snake .Name}} = {{$structName}}.{{$cppFieldName}}.Num();
+ TArray Temp;
+ Temp.SetNumUninitialized(len{{snake .Name}});
+ for (int i = 0; i < len{{snake .Name}}; i++)
+ {
+ Temp[i] = {{$structName}}.{{$cppFieldName}}[i] ? JNI_TRUE : JNI_FALSE;
+ }
+ env->SetBooleanArrayRegion({{snake .Name}}_value, 0, len{{snake .Name}}, Temp.GetData());
+{{- else if .IsPrimitive }}
+ auto len{{snake .Name}} = {{$structName}}.{{$cppFieldName}}.Num();
+ jFieldId_{{snake .Name}} = (*env)->New{{jniToEnvNameType .}}Array(len{{snake .Name}});
+ if (jFieldId_{{snake .Name}} == NULL){/*Log error, skip?*/};
+ env->Set{{jniToEnvNameType .}}ArrayRegion(jFieldId_{{snake .Name}}, 0, len{{snake .Name}}, {{- if (eq .KindType "int64") -}}
+ reinterpret_cast({{$structName}}.{{$cppFieldName}}.GetData()));
+ {{- else -}}
+ {{$structName}}.{{$cppFieldName}}.GetData());
+ {{- end }}
+{{- else if not (eq .KindType "extern")}}
+ {{- $otherModuleClassName := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+ jobjectArray {{$tmpObjName}} = {{if not (eq $otherModuleClassName "DataJavaConverter" )}}{{$otherModuleClassName}}::{{end -}}
+ makeJava{{Camel .Type }}Array(env, {{$structName}}.{{$cppFieldName}});
+{{- end }}
+ {{- if or (not .IsPrimitive) (eq .KindType "string") }}
+ env->SetObjectField(javaObjInstance, jFieldId_{{snake .Name}}, {{$tmpObjName}});
+ env->DeleteLocalRef({{$tmpObjName}});
+ {{- end }}
+{{- else if eq .KindType "enum"}}
+ {{- $otherModuleClassName := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+ jobject {{$tmpObjName}} = {{if not (eq $otherModuleClassName "DataJavaConverter" )}}{{$otherModuleClassName}}::{{end -}}
+ makeJava{{Camel .Type }}(env, {{$structName}}.{{$cppFieldName}});
+ env->SetObjectField(javaObjInstance, jFieldId_{{snake .Name}}, {{$tmpObjName}});
+ env->DeleteLocalRef({{$tmpObjName}});
+{{- else if eq .KindType "string"}}
+ auto {{$tmpObjName}}Wrapper = FJavaHelper::ToJavaString(env,{{$in_cppStructName}}.{{$cppFieldName}});
+ jstring {{$tmpObjName}} = static_cast(env->NewLocalRef(*{{$tmpObjName}}Wrapper));
+ env->SetObjectField(javaObjInstance, jFieldId_{{snake .Name}}, {{$tmpObjName}});
+ // in UE 5.6 no need for new local ref
+{{- else if .IsPrimitive }}
+ env->Set{{jniToEnvNameType .}}Field(javaObjInstance, jFieldId_{{snake .Name}}, {{$in_cppStructName}}.{{$cppFieldName}});
+{{- else if not (eq .KindType "extern")}}
+ {{- $otherModuleClassName := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+ jobject {{$tmpObjName}} = {{if not (eq $otherModuleClassName "DataJavaConverter" )}}{{$otherModuleClassName}}::{{end -}}
+ makeJava{{Camel .Type }}(env,{{$structName}}.{{$cppFieldName }});
+ env->SetObjectField(javaObjInstance, jFieldId_{{snake .Name}}, {{$tmpObjName}});
+ env->DeleteLocalRef({{$tmpObjName}});
+{{- end }}
+
+{{- end }}
+ return javaObjInstance;
+}
+
+jobjectArray {{$className }}::makeJava{{Camel .Name }}Array(JNIEnv* env, const TArray<{{$structType}}>& cppArray)
+{
+ jclass javaStruct = FAndroidApplication::FindJavaClassGlobalRef("{{$packageName}}/{{$javaClassTypeName}}");
+ auto arraySize = cppArray.Num();
+ jobjectArray javaArray = env->NewObjectArray( arraySize, javaStruct, nullptr);
+ for (jsize i = 0; i < arraySize; ++i) {
+ jobject element = makeJava{{Camel .Name }}(env, cppArray[i]);
+ env->SetObjectArrayElement(javaArray, i, element);
+ env->DeleteLocalRef(element);
+ }
+ return javaArray;
+}
+{{- end }}
+
+{{- range .Module.Enums }}
+{{- $moduleEnumName := printf "%s%s" $ModuleName .Name }}
+{{- $cpp_class := printf "E%s%s" $ModuleName .Name }}
+
+{{- $in_cppStructName := printf "out_%s" (snake .Name)}}
+{{- $api_package_name := printf "%s_api" (camel $moduleName) }}
+{{- $packageName := ( join "/" (strSlice $moduleName $api_package_name) ) }}
+{{- $javaClassTypeName := Camel .Name}}
+
+void {{$className }}::fill{{Camel .Name }}Array(JNIEnv* env, jobjectArray input, TArray<{{$cpp_class}}>& out_array)
+{
+ jclass javaStruct = FAndroidApplication::FindJavaClassGlobalRef("{{$packageName}}/{{$javaClassTypeName}}");
+ out_array.Empty();
+ jsize len = env->GetArrayLength(input);
+ for (jsize i = 0; i < len; ++i)
+ {
+ jobject element = env->GetObjectArrayElement(input, i);
+ out_array.Add(get{{Camel .Name }}Value(env, element));
+ env->DeleteLocalRef(element);
+ }
+}
+
+{{$cpp_class}} {{$className }}::get{{Camel .Name }}Value(JNIEnv* env, jobject input)
+{
+ {{$cpp_class}} cppEnumValue;
+ jclass javaStruct = FAndroidApplication::FindJavaClassGlobalRef("{{$packageName}}/{{$javaClassTypeName}}");
+ jmethodID getValueMethod = env->GetMethodID(javaStruct, "getValue", "()I");
+ int int_value = env->CallIntMethod(input, getValueMethod);
+ {{- $toEnumFuncName := printf "U%sLibrary::to%s%s" $ModuleName $ModuleName .Name }}
+ {{$toEnumFuncName}}(cppEnumValue, int_value);
+ return cppEnumValue;
+}
+
+
+jobjectArray {{$className }}::makeJava{{Camel .Name }}Array(JNIEnv* env, const TArray<{{$cpp_class}}>& cppArray)
+{
+ jclass javaStruct = FAndroidApplication::FindJavaClassGlobalRef("{{$packageName}}/{{$javaClassTypeName}}");
+ auto arraySize = cppArray.Num();
+ jobjectArray javaArray = env->NewObjectArray( arraySize, javaStruct, nullptr);
+ for (jsize i = 0; i < arraySize; ++i) {
+ jobject element = makeJava{{Camel .Name }}(env, cppArray[i]);
+ env->SetObjectArrayElement(javaArray, i, element);
+ env->DeleteLocalRef(element);
+ }
+ return javaArray;
+}
+
+jobject {{$className }}::makeJava{{Camel .Name }}(JNIEnv* env, {{$cpp_class}} value)
+{
+ jclass javaStruct = FAndroidApplication::FindJavaClassGlobalRef("{{$packageName}}/{{$javaClassTypeName}}");
+ jmethodID fromValueMethod = env->GetStaticMethodID(javaStruct, "fromValue", "(I)L{{$packageName}}/{{$javaClassTypeName}};");
+ if (!fromValueMethod) return nullptr;
+ int int_value = (uint8)value;
+ jobject javaObj = env->CallStaticObjectMethod(javaStruct, fromValueMethod, int_value);
+ return javaObj;
+}
+{{- end }}
+#endif
diff --git a/templates/module/Source/modulejni/Private/Generated/jni/jniadapter.cpp.tpl b/templates/module/Source/modulejni/Private/Generated/jni/jniadapter.cpp.tpl
new file mode 100644
index 000000000..b987f54a4
--- /dev/null
+++ b/templates/module/Source/modulejni/Private/Generated/jni/jniadapter.cpp.tpl
@@ -0,0 +1,564 @@
+{{- /* Copyright Epic Games, Inc. All Rights Reserved */ -}}
+/**{{ template "copyright" }}*/
+{{- $ModuleName := Camel .Module.Name}}
+{{- $ModuleNameRaw := .Module.Name}}
+{{- $IfaceName := Camel .Interface.Name }}
+{{- $Category := printf "ApiGear|%s|%s" $ModuleName $IfaceName }}
+{{- $DisplayName := printf "%s%s" $ModuleName $IfaceName }}
+{{- $Class := printf "U%sJniAdapter" $DisplayName}}
+{{- $Iface := printf "%s%s" $ModuleName $IfaceName }}
+{{- $jniservice_name:= printf "%sjniservice" ( camel $ModuleNameRaw) }}
+{{- $javaClassPath := ( join "/" (strSlice ( camel $ModuleNameRaw) $jniservice_name) ) }}
+{{- $javaClassName := printf "%sJniService" $IfaceName }}
+{{- $jniFullFuncPrefix := ( join "_" (strSlice "Java" ( camel $ModuleNameRaw) $jniservice_name $javaClassName ) ) }}
+{{- $api_module_name:= printf "%s_api" ( camel $ModuleNameRaw) }}
+{{- $javaIfClassName := printf "I%s" $IfaceName }}
+{{- $javaIfClassFull := ( join "/" (strSlice (camel $ModuleNameRaw) $api_module_name $javaIfClassName ) ) }}
+
+
+{{- define "convert_to_java_type"}}
+ {{- $localName := printf "jlocal_%s" (Camel .Name) }}
+ {{- $cppropName := ueVar "" .}}
+ {{- $javaClassConverter := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+ {{- if (eq $javaClassConverter "DataJavaConverter" )}}{{- $javaClassConverter = printf "%sDataJavaConverter" (Camel .Schema.Module.Name) }}{{ end }}
+ {{- if .IsArray }}
+ {{- if (eq .KindType "string")}}
+ TArray {{$cppropName}}StringViews;
+ {{$cppropName}}StringViews.Reserve({{$cppropName}}.Num());
+ for (const FString& Str : {{$cppropName}})
+ {
+ {{$cppropName}}StringViews.Add(FStringView(Str));
+ }
+ auto {{$localName}}Wrapped = FJavaHelper::ToJavaStringArray(Env,{{$cppropName}}StringViews);
+ jobjectArray {{$localName}} = static_cast(Env->NewLocalRef(*{{$localName}}Wrapped));
+ {{- else if (eq .KindType "bool")}}
+ auto len{{snake .Name}} = {{$cppropName}}.Num();
+ {{jniToReturnType .}} {{$localName}} = Env->New{{jniToEnvNameType .}}Array(len{{snake .Name}});
+ TArray Temp{{$localName}};
+ Temp{{$localName}}.SetNumUninitialized(len{{snake .Name}});
+ for (int i = 0; i < len{{snake .Name}}; i++)
+ {
+ Temp{{$localName}}[i] = {{$cppropName}}[i] ? JNI_TRUE : JNI_FALSE;
+ }
+ Env->SetBooleanArrayRegion({{$localName}}, 0, len{{snake .Name}}, Temp{{$localName}}.GetData());
+ {{- else if and (.IsPrimitive ) (not (eq .KindType "enum")) }}
+ auto len{{snake .Name}} = {{$cppropName}}.Num();
+ {{jniToReturnType .}} {{$localName}} = Env->New{{jniToEnvNameType .}}Array(len{{snake .Name}});
+ if ({{$localName}} == NULL){/*Log error, skip?*/};
+ Env->Set{{jniToEnvNameType .}}ArrayRegion({{$localName}}, 0, len{{snake .Name}}, {{- if (eq .KindType "int64") -}}
+ reinterpret_cast({{$cppropName}}.GetData()));
+ {{- else -}}
+ {{$cppropName}}.GetData());
+ {{- end }}
+ {{- else if not (eq .KindType "extern")}}
+ {{jniToReturnType .}} {{$localName}} = {{$javaClassConverter}}::makeJava{{Camel .Type }}Array(Env, {{$cppropName}});
+ {{- end }}
+ {{- else if (eq .KindType "string")}}
+ auto {{$localName}}Wrapped = FJavaHelper::ToJavaString(Env, {{$cppropName}});
+ jstring {{$localName}} = static_cast(Env->NewLocalRef(*{{$localName}}Wrapped));
+ {{- else if ( or (not .IsPrimitive ) (eq .KindType "enum" ) ) }}
+ {{jniToReturnType .}} {{$localName}} = {{$javaClassConverter}}::makeJava{{Camel .Type }}(Env, {{$cppropName}});
+ {{- end }}
+{{- end}}
+
+{{- define "convert_to_local_cpp_value_java_param"}}
+{{- $javaPropName := .Name}}
+{{- $javaClassConverter := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+{{- $local_value := printf "local_%s" (snake .Name) }}
+{{- if (eq $javaClassConverter "DataJavaConverter" )}}{{- $javaClassConverter = printf "%sDataJavaConverter" (Camel .Schema.Module.Name)}}{{ end }}
+{{- if .IsArray }}
+ {{ueReturn "" .}} {{$local_value}} = {{ ueDefault "" . }};
+ {{- if (eq .KindType "string")}}
+ {{$local_value}} = FJavaHelper::ObjectArrayToFStringTArray(Env, {{$javaPropName}});
+ {{- else if (eq .KindType "bool")}}
+ jbooleanArray l_java{{Camel .Name}}Array = (jbooleanArray){{$javaPropName}};
+ jsize len{{snake .Name}} = Env->GetArrayLength(l_java{{Camel .Name}}Array);
+ {{$local_value}}.Reserve(len{{snake .Name}});
+ TArray Temp{{Camel .Name}};
+ Temp{{Camel .Name}}.SetNumUninitialized(len{{snake .Name}});
+ Env->GetBooleanArrayRegion(l_java{{Camel .Name}}Array, 0, len{{snake .Name}}, Temp{{Camel .Name}}.GetData());
+ for (int i = 0; i < len{{snake .Name}}; i++)
+ {
+ {{$local_value}}.Add(Temp{{Camel .Name}}[i] == JNI_TRUE);
+ }
+ Env->DeleteLocalRef(l_java{{Camel .Name}}Array);
+ {{- else if .IsPrimitive }}
+ {{ jniToReturnType . }} l_java{{Camel .Name}}Array = ({{ jniToReturnType . }}){{$javaPropName}};
+ jsize len{{snake .Name}} = Env->GetArrayLength(l_java{{Camel .Name}}Array);
+ {{$local_value}}.AddUninitialized(len{{snake .Name}});
+ Env->Get{{jniToEnvNameType .}}ArrayRegion({{$javaPropName}}, 0, len{{snake .Name}}, {{- if (eq .KindType "int64") -}}
+ reinterpret_cast({{$local_value}}.GetData()));
+ {{- else -}}
+ {{$local_value}}.GetData());
+ {{- end }}
+ Env->DeleteLocalRef(l_java{{Camel .Name}}Array);
+ {{- else if not (eq .KindType "extern")}}
+ {{$javaClassConverter}}::fill{{Camel .Type }}Array(Env, {{$javaPropName}}, {{$local_value}});
+ {{- end }}
+{{- else if eq .KindType "enum" }}
+ {{ueReturn "" .}} {{$local_value}} = {{$javaClassConverter}}::get{{Camel .Type }}Value(Env, {{$javaPropName}});
+{{- else if (eq .KindType "string")}}
+ FString {{$local_value}} = FJavaHelper::FStringFromParam(Env, {{$javaPropName}});
+{{- else if not (ueIsStdSimpleType . )}}
+ {{ueReturn "" .}} {{$local_value}} = {{ ueDefault "" . }};
+ {{$javaClassConverter}}::fill{{Camel .Type }}(Env, {{$javaPropName}}, {{$local_value}});
+{{- end }}
+{{- end }}
+
+////////////////////////////////
+// WARNING AUTOGENERATED
+// DO NOT MODIFY
+///////////////////////////////
+
+#include "{{$ModuleName}}/Generated/Jni/{{$Iface}}JniAdapter.h"
+#include "{{$ModuleName}}/Generated/Jni/{{$ModuleName}}DataJavaConverter.h"
+#include "Async/Future.h"
+#include "Async/Async.h"
+#include "Engine/Engine.h"
+#include "Misc/DateTime.h"
+#include "HAL/Platform.h"
+
+{{- $includes := getEmptyStringList}}
+{{- range .Module.Externs }}
+{{- $class := ueExtern . }}
+{{- if $class.Include }}
+{{- $includeName := printf "\"%s\"" $class.Include }}
+{{- $includes = (appendList $includes $includeName) }}
+{{- end }}
+{{- end }}
+{{- range .Module.Imports }}
+{{- $importModuleName := Camel .Name }}
+{{- $includeName := printf "\"%s/Generated/api/%s_data.h\"" $importModuleName $importModuleName }}
+{{- $includeName := printf "\"%s/Generated/Jni/%sDataJavaConverter.h\"" $importModuleName $importModuleName }}
+{{- $includes = (appendList $includes $includeName) }}
+{{- end }}
+{{- $includes = unique $includes }}
+{{ range $includes }}
+#include {{ .}}
+{{- end }}
+{{ if or (len .Module.Enums) (len .Module.Structs) -}}
+#include "{{$ModuleName}}/Generated/api/{{ $ModuleName }}_data.h"
+{{ end }}
+
+#if PLATFORM_ANDROID
+
+#include "Engine/Engine.h"
+#include "Android/AndroidJNI.h"
+#include "Android/AndroidApplication.h"
+
+#if USE_ANDROID_JNI
+#include
+#endif
+#endif
+
+DEFINE_LOG_CATEGORY(Log{{$Iface}}_JNI);
+
+
+namespace
+{
+ {{$Class}}* g{{$Class}}Handle = nullptr;
+}
+
+
+{{- if .Interface.Description }}
+/**
+ \brief {{.Interface.Description}}
+*/
+{{- end }}
+{{$Class}}::{{$Class}}()
+{
+}
+
+void {{$Class}}::Initialize(FSubsystemCollectionBase& Collection)
+{
+ Super::Initialize(Collection);
+ g{{$Class}}Handle = this;
+#if PLATFORM_ANDROID
+#if USE_ANDROID_JNI
+ m_javaJniServiceClass = FAndroidApplication::FindJavaClassGlobalRef("{{$javaClassPath}}/{{$javaClassName}}");
+ auto Env = FAndroidApplication::GetJavaEnv();
+ jclass BridgeClass = FAndroidApplication::FindJavaClassGlobalRef("{{$javaClassPath}}/{{$javaClassName}}Starter");
+ if (BridgeClass == nullptr)
+ {
+ UE_LOG(LogTemp, Warning, TEXT("{{Camel .Module.Name}}JavaServiceStarter:start; CLASS not found"));
+ return;
+ }
+ auto functionSignature = "(Landroid/content/Context;)L{{$javaIfClassFull}};";
+ jmethodID StartMethod = Env->GetStaticMethodID(BridgeClass, "start", functionSignature);
+ if (StartMethod == nullptr)
+ {
+ UE_LOG(LogTemp, Warning, TEXT( "{{Camel .Module.Name}}JavaServiceStarter:start; method not found"));
+ return;
+ }
+ jobject Activity = FJavaWrapper::GameActivityThis;
+ jobject localRef = FJavaWrapper::CallStaticObjectMethod(Env, BridgeClass, StartMethod, Activity);
+
+ m_javaJniServiceInstance = Env->NewGlobalRef(localRef);
+ Env->DeleteLocalRef(localRef);
+#endif
+#endif
+}
+
+void {{$Class}}::Deinitialize()
+{
+ callJniServiceReady(false);
+ g{{$Class}}Handle = nullptr;
+#if PLATFORM_ANDROID
+#if USE_ANDROID_JNI
+ m_javaJniServiceClass = nullptr;
+ if (m_javaJniServiceInstance)
+ {
+ FAndroidApplication::GetJavaEnv()->DeleteGlobalRef(m_javaJniServiceInstance);
+ m_javaJniServiceInstance = nullptr;
+ }
+ JNIEnv* Env = FAndroidApplication::GetJavaEnv();
+
+ jclass BridgeClass = FAndroidApplication::FindJavaClassGlobalRef("{{$javaClassPath}}/{{$javaClassName}}Starter");
+ if (BridgeClass != nullptr)
+ {
+ jmethodID StopMethod = Env->GetStaticMethodID(BridgeClass, "stop", "(Landroid/content/Context;)V");
+ if (StopMethod != nullptr)
+ {
+ jobject Activity = FJavaWrapper::GameActivityThis; // Unreal’s activity
+ FJavaWrapper::CallStaticVoidMethod(Env, BridgeClass, StopMethod, Activity);
+ }
+ else
+ {
+ UE_LOG(LogTemp, Warning, TEXT("{{Camel .Module.Name}}JavaServiceStarter:stop; method not found, failed to stop service"));
+ return;
+ }
+ }
+ else
+ {
+ UE_LOG(LogTemp, Warning, TEXT( "{{Camel .Module.Name}}JavaServiceStarter:stop; CLASS not found, failed to stop service"));
+ }
+#endif
+#endif
+ Super::Deinitialize();
+}
+
+void {{$Class}}::setBackendService(TScriptInterface InService)
+{
+ // unsubscribe from old backend
+ if (BackendService != nullptr)
+ {
+{{- if or (len .Interface.Properties) (.Interface.Signals) }}
+ U{{$Iface}}Signals* BackendSignals = BackendService->_GetSignals();
+ checkf(BackendSignals, TEXT("Cannot unsubscribe from delegates from backend service {{$Iface}}"));
+{{- end }}
+{{- range .Interface.Properties }}
+ if (On{{Camel .Name}}ChangedHandle.IsValid())
+ {
+ BackendSignals->On{{Camel .Name}}Changed.Remove(On{{Camel .Name}}ChangedHandle);
+ On{{Camel .Name}}ChangedHandle.Reset();
+ }
+{{- end }}
+{{- range .Interface.Signals }}
+ if (On{{Camel .Name}}SignalHandle.IsValid())
+ {
+ BackendSignals->On{{Camel .Name}}Signal.Remove(On{{Camel .Name}}SignalHandle);
+ On{{Camel .Name}}SignalHandle.Reset();
+ }
+{{- end }}
+ }
+
+ // only set if interface is implemented
+ checkf(InService.GetInterface() != nullptr, TEXT("Cannot set backend service - interface {{$Iface}} is not fully implemented"));
+
+ // subscribe to new backend
+{{- $Service := printf "I%sInterface" $Iface }}
+ BackendService = InService;
+{{- if or (len .Interface.Properties) (.Interface.Signals) }}
+ U{{$Iface}}Signals* BackendSignals = BackendService->_GetSignals();
+ checkf(BackendSignals, TEXT("Cannot subscribe to delegates from backend service {{$Iface}}"));
+{{- end }}
+ // connect property changed signals or simple events
+{{- range .Interface.Properties }}
+ On{{Camel .Name}}ChangedHandle = BackendSignals->On{{Camel .Name}}Changed.AddUObject(this, &{{$Class}}::On{{Camel .Name}}Changed);
+{{- end }}
+{{- range .Interface.Signals }}
+ On{{Camel .Name}}SignalHandle = BackendSignals->On{{Camel .Name}}Signal.AddUObject(this, &{{$Class}}::On{{Camel .Name}});
+{{- end }}
+
+ callJniServiceReady(true);
+}
+
+TScriptInterface {{$Class}}::getBackendService()
+{
+ return BackendService;
+}
+
+void {{$Class}}::callJniServiceReady(bool isServiceReady)
+{
+ UE_LOG(Log{{$Iface}}_JNI, Verbose, TEXT("{{$Class}} call nativeServiceReady the service function "));
+
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
+ {
+ if (!m_javaJniServiceClass || !m_javaJniServiceInstance )
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:nativeServiceReady(Z)V CLASS not found"));
+ return;
+ }
+
+ static const jmethodID MethodID = Env->GetMethodID(m_javaJniServiceClass, "nativeServiceReady", "(Z)V");
+
+ if (MethodID != nullptr)
+ {
+ FJavaWrapper::CallVoidMethod(Env, m_javaJniServiceInstance, MethodID, isServiceReady);
+ }
+ else
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:nativeServiceReady(Z)V not found "));
+ }
+ }
+#endif
+}
+
+{{- range .Interface.Signals }}
+
+void {{$Class}}::On{{Camel .Name}}({{ueParams "" .Params}})
+{
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ UE_LOG(Log{{$Iface}}_JNI, Verbose, TEXT("Notify java jni {{$Class}}::on{{Camel .Name}} "));
+ if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
+ {
+ {{- $signatureParams:= jniJavaSignatureParams .Params}}
+ if (m_javaJniServiceClass == nullptr || m_javaJniServiceInstance == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:on{{Camel .Name}} ({{$signatureParams}})V CLASS not found"));
+ return;
+ }
+ static const jmethodID MethodID = Env->GetMethodID(m_javaJniServiceClass, "on{{Camel .Name}}", "({{$signatureParams}})V");
+ if (MethodID == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:on{{Camel .Name}} ({{$signatureParams}})V not found"));
+ return;
+ }
+
+ {{- range .Params -}}
+ {{template "convert_to_java_type" .}}
+ {{- end }}
+
+ FJavaWrapper::CallVoidMethod(Env, m_javaJniServiceInstance, MethodID{{- if len (.Params) }},{{- end}}
+ {{- range $idx, $p := .Params -}} {{- if $idx}}, {{ end -}}
+ {{- $javaPropName := Camel .Name}}
+ {{- $cppropName := ueVar "" .}}
+ {{- $localName := printf "jlocal_%s" $javaPropName }}
+ {{- if .IsArray }} {{$localName}}
+ {{- else if or ( or (eq .KindType "enum") (eq .KindType "string") ) (not .IsPrimitive ) }} {{$localName}}
+ {{- else }} {{$cppropName}}
+ {{- end -}}
+ {{- end -}});
+
+ {{- range $idx, $p := .Params -}}
+ {{- $javaPropName := Camel .Name}}
+ {{- $localName := printf "jlocal_%s" $javaPropName }}
+ {{- if or ( or .IsArray (eq .KindType "enum" ) ) }}
+ Env->DeleteLocalRef({{$localName}});
+ {{- else if not ( or (eq .KindType "extern") (ueIsStdSimpleType .) ) }}
+ Env->DeleteLocalRef({{$localName}});
+ {{- end }}
+ {{- end }}
+ }
+#endif
+}
+{{- end }}
+
+
+{{- range .Interface.Properties }}
+
+void {{$Class}}::On{{Camel .Name}}Changed({{ueParam "" .}})
+{
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ UE_LOG(Log{{$Iface}}_JNI, Verbose, TEXT("Notify java jni {{$Class}}::On{{Camel .Name}} "));
+ {{- $signature := printf "(%s)V" (jniJavaSignatureParam .)}}
+ if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
+ {
+ if (m_javaJniServiceClass == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}::on{{Camel .Name}}Changed{{$signature}} CLASS not found"));
+ return;
+ }
+
+ static const jmethodID MethodID = Env->GetMethodID(m_javaJniServiceClass, "on{{Camel .Name}}Changed","{{$signature}}");
+ if (MethodID == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:on{{Camel .Name}}Changed{{$signature}} not found"));
+ return;
+ }
+
+ {{- $cppropName := ueVar "" .}}
+ {{template "convert_to_java_type" .}}
+ {{- $javaLocalName := printf "jlocal_%s" (Camel .Name) }}
+ {{- if or ( or ( .IsArray ) (eq .KindType "string")) ( or (eq .KindType "enum") (not (ueIsStdSimpleType .)) ) }}
+ FJavaWrapper::CallVoidMethod(Env, m_javaJniServiceInstance, MethodID, {{$javaLocalName}});
+
+ {{- if and .IsArray }}
+ Env->DeleteLocalRef({{$javaLocalName}});
+ {{- else if not ( or (eq .KindType "extern") (ueIsStdSimpleType . ) ) }}
+ Env->DeleteLocalRef({{$javaLocalName}});
+ {{- end}}
+ {{- else }}
+ FJavaWrapper::CallVoidMethod(Env, m_javaJniServiceInstance, MethodID, {{$cppropName}});
+ {{- end }}
+
+ }
+#endif
+}
+{{- end }}
+
+
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+
+{{- range .Interface.Operations }}
+JNI_METHOD {{ jniToReturnType .Return}} {{$jniFullFuncPrefix}}_native{{ Camel .Name }}(JNIEnv* Env, jclass Clazz{{- if len (.Params) }},{{end}} {{jniJavaParams "" .Params }})
+{
+ UE_LOG(Log{{$Iface}}_JNI, Verbose, TEXT("{{$jniFullFuncPrefix}}_native{{Camel .Name}}"));
+ if (g{{$Class}}Handle == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("{{$jniFullFuncPrefix}}_native{{ Camel .Name }}: JNI SERVICE ADAPTER NOT FOUND "));
+ return {{ jniEmptyReturn .Return }};
+ }
+
+{{- range .Params -}}
+ {{- template "convert_to_local_cpp_value_java_param" . }}
+{{- end }}
+
+ auto service = g{{$Class}}Handle->getBackendService();
+ if (service != nullptr)
+ {
+ {{- $cppropName := "result"}}
+ {{ if not .Return.IsVoid }}auto {{$cppropName}} = {{ end -}}
+ service->{{Camel .Name}}(
+ {{- range $idx, $p := .Params -}}
+ {{- if $idx}}, {{ end -}}
+ {{- $local_value := printf "local_%s" (snake .Name) -}}
+ {{- if or .IsArray ( or (eq .KindType "enum") (not (ueIsStdSimpleType .)) ) }} {{$local_value -}}
+ {{- else }} {{.Name}}
+ {{- end -}}
+ {{- end -}}
+ );
+
+ {{- if .Return.IsVoid }}
+ return;
+ {{- else if or .Return.IsArray ( or (eq .Return.KindType "enum") (not (ueIsStdSimpleType .Return)) ) }}
+ {{- $localName := "jresult" }}
+ {{- $javaClassConverter := printf "%sDataJavaConverter" ( Camel .Return.Schema.Import ) }}
+ {{- if (eq $javaClassConverter "DataJavaConverter" )}}{{- $javaClassConverter = printf "%sDataJavaConverter" $ModuleName }}{{ end }}
+ {{- if .Return.IsArray }}
+ {{- if (eq .Return.KindType "string")}}
+ TArray {{$cppropName}}StringViews;
+ {{$cppropName}}StringViews.Reserve({{$cppropName}}.Num());
+ for (const FString& Str : {{$cppropName}})
+ {
+ {{$cppropName}}StringViews.Add(FStringView(Str));
+ }
+ auto {{$localName}}Wrapped = FJavaHelper::ToJavaStringArray(Env,{{$cppropName}}StringViews);
+ auto {{$localName}} = static_cast(Env->NewLocalRef(*{{$localName}}Wrapped));
+ {{- else if (eq .Return.KindType "bool")}}
+ auto len = {{$cppropName}}.Num();
+ {{jniToReturnType .Return}} {{$localName}} = Env->New{{jniToEnvNameType .Return}}Array(len);
+ TArray Temp;
+ Temp.SetNumUninitialized(len);
+ for (int i = 0; i < len; i++)
+ {
+ Temp[i] = {{$cppropName}}[i] ? JNI_TRUE : JNI_FALSE;
+ }
+ Env->SetBooleanArrayRegion({{$localName}}, 0, len, Temp.GetData());
+ {{- else if and (.Return.IsPrimitive ) (not (eq .Return.KindType "enum")) }}
+ auto len = {{$cppropName}}.Num();
+ {{jniToReturnType .Return}} {{$localName}} = Env->New{{jniToEnvNameType .Return}}Array(len);
+ if ({{$localName}} == NULL){/*Log error, skip?*/};
+ Env->Set{{jniToEnvNameType .Return}}ArrayRegion({{$localName}}, 0, len, {{- if (eq .Return.KindType "int64") -}}
+ reinterpret_cast({{$cppropName}}.GetData()));
+ {{- else -}}
+ {{$cppropName}}.GetData());
+ {{- end }}
+ {{- else if not (eq .Return.KindType "extern")}}
+ {{jniToReturnType .Return}} {{$localName}} = {{$javaClassConverter}}::makeJava{{Camel .Return.Type }}Array(Env, {{$cppropName}});
+ {{- end }}
+ {{- else if (eq .Return.KindType "string")}}
+ auto {{$localName}}Wrapped = FJavaHelper::ToJavaString(Env, {{$cppropName}});
+ jstring {{$localName}} = static_cast(Env->NewLocalRef(*{{$localName}}Wrapped));
+ {{- else if ( or (not (ueIsStdSimpleType .Return)) (eq .Return.KindType "enum" ) ) }}
+ {{jniToReturnType .Return}} {{$localName}} = {{$javaClassConverter}}::makeJava{{Camel .Return.Type }}(Env, {{$cppropName}});
+ {{- end }}
+ return {{$localName}};
+ {{- else }}
+ return {{$cppropName}};
+ {{- end }}
+ }
+ else
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("service not valid"));
+ return {{ jniEmptyReturn .Return }};
+ }
+}
+{{- end}}
+
+
+
+{{- range .Interface.Properties }}
+{{- if not .IsReadOnly }}
+{{- $javaPropName := .Name}}
+JNI_METHOD void {{$jniFullFuncPrefix}}_nativeSet{{ Camel .Name }}(JNIEnv* Env, jclass Clazz, {{jniJavaParam "" . }})
+{
+ UE_LOG(Log{{$Iface}}_JNI, Verbose, TEXT("{{$jniFullFuncPrefix}}_nativeSet{{Camel .Name}}"));
+ if (g{{$Class}}Handle == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("{{$jniFullFuncPrefix}}_nativeSet{{ Camel .Name }}: JNI SERVICE ADAPTER NOT FOUND "));
+ return;
+ }
+ {{ template "convert_to_local_cpp_value_java_param" . }}
+ {{- $hasLocalVar := or .IsArray ( or (eq .KindType "enum") (not (ueIsStdSimpleType .)) ) }}
+ {{- $local_value := printf "local_%s" (snake .Name) }}
+
+ AsyncTask(ENamedThreads::GameThread, [{{- if $hasLocalVar }}p{{$local_value -}} = MoveTemp({{$local_value -}}){{- else}}{{$javaPropName}}{{- end}}]()
+ {
+ auto service = g{{$Class}}Handle->getBackendService();
+ if (service != nullptr)
+ {
+ {{- if $hasLocalVar }}
+ service->Set{{Camel .Name}}(p{{$local_value}});
+ {{- else}}
+ service->Set{{Camel .Name}}({{$javaPropName}});
+ {{- end}}
+ }
+ else
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("service not valid, cannot set value for {{.Name}}"));
+ }
+ });
+}
+{{- end}}
+
+JNI_METHOD {{jniToReturnType .}} {{$jniFullFuncPrefix}}_nativeGet{{ Camel .Name }}(JNIEnv* Env, jclass Clazz)
+{
+ UE_LOG(Log{{$Iface}}_JNI, Verbose, TEXT("{{$jniFullFuncPrefix}}_nativeGet{{Camel .Name}}"));
+ if (g{{$Class}}Handle == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("{{$jniFullFuncPrefix}}_nativeGet{{Camel .Name }}: JNI SERVICE ADAPTER NOT FOUND "));
+ return {{ jniEmptyReturn . }};
+ }
+ auto service = g{{$Class}}Handle->getBackendService();
+ if (service != nullptr)
+ {
+ {{- $cppropName := ueVar "" .}}
+ auto {{$cppropName}} = service->Get{{Camel .Name}}();
+ {{- if or .IsArray ( or (eq .KindType "enum") (not (ueIsStdSimpleType .)) ) }}
+ {{template "convert_to_java_type" .}}
+ {{- $localName := printf "jlocal_%s" (Camel .Name) }}
+ return {{$localName}};
+ {{- else }}
+ return {{$cppropName}};
+ {{- end }}
+ }
+ else
+ {
+ UE_LOG(Log{{$Iface}}_JNI, Warning, TEXT("service not available, try setting a backend service "));
+ return {{ jniEmptyReturn . }};
+ }
+}
+{{- end }}
+#endif
diff --git a/templates/module/Source/modulejni/Private/Generated/jni/jniclient.cpp.tpl b/templates/module/Source/modulejni/Private/Generated/jni/jniclient.cpp.tpl
new file mode 100644
index 000000000..f1f3feaea
--- /dev/null
+++ b/templates/module/Source/modulejni/Private/Generated/jni/jniclient.cpp.tpl
@@ -0,0 +1,604 @@
+/**
+Copyright 2021 ApiGear UG
+Copyright 2021 Epic Games, Inc.
+
+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.
+*/
+
+////////////////////////////////
+// WARNING AUTOGENERATED
+// DO NOT MODIFY
+///////////////////////////////
+
+{{- /* Copyright Epic Games, Inc. All Rights Reserved */}}
+
+/**{{ template "copyright" }} */
+
+{{ $ModuleName := Camel .Module.Name}}
+
+{{- $IfaceName := Camel .Interface.Name }}
+{{- $DisplayName := printf "%s%s" $ModuleName $IfaceName }}
+{{- $Class := printf "U%sJniClient" $DisplayName}}
+{{- $Iface := printf "%s%s" $ModuleName $IfaceName }}
+{{- $ModuleNameRaw := .Module.Name}}
+{{- $jniclient_name:= printf "%sjniclient" ( camel $ModuleNameRaw) }}
+{{- $javaClassPath := ( join "/" (strSlice (camel $ModuleNameRaw) $jniclient_name ) ) }}
+{{- $javaClassName := printf "%sJniClient" $IfaceName }}
+{{- $jniFullFuncPrefix := ( join "_" (strSlice "Java" ( camel $ModuleNameRaw) $jniclient_name $javaClassName ) ) }}
+{{- $javaClassFull := ( join "/" (strSlice (camel $ModuleNameRaw) $jniclient_name $javaClassName ) ) }}
+
+
+{{- define "convert_to_java_type_in_param"}}
+ {{- $localName := printf "jlocal_%s" (Camel .Name) }}
+ {{- $cppropName := ueVar "In" .}}
+ {{- $javaClassConverter := printf "%sDataJavaConverter" ( Camel .Schema.Import ) }}
+ {{- if (eq $javaClassConverter "DataJavaConverter" )}}{{- $javaClassConverter = printf "%sDataJavaConverter" (Camel .Schema.Module.Name) }}{{ end }}
+ {{- if .IsArray }}
+ {{- if (eq .KindType "string")}}
+ TArray {{$cppropName}}StringViews;
+ {{$cppropName}}StringViews.Reserve({{$cppropName}}.Num());
+ for (const FString& Str : {{$cppropName}})
+ {
+ {{$cppropName}}StringViews.Add(FStringView(Str));
+ }
+ auto {{$localName}}Wrapped = FJavaHelper::ToJavaStringArray(Env,{{$cppropName}}StringViews);
+ jobjectArray {{$localName}} = static_cast(Env->NewLocalRef(*{{$localName}}Wrapped));
+ {{- else if (eq .KindType "bool")}}
+ auto len{{snake .Name}} = {{$cppropName}}.Num();
+ {{jniToReturnType .}} {{$localName}} = Env->New{{jniToEnvNameType .}}Array(len{{snake .Name}});
+ TArray Temp;
+ Temp.SetNumUninitialized(len{{snake .Name}});
+ for (int i = 0; i < len{{snake .Name}}; i++)
+ {
+ Temp[i] = {{$cppropName}}[i] ? JNI_TRUE : JNI_FALSE;
+ }
+ Env->SetBooleanArrayRegion({{$localName}}, 0, len{{snake .Name}}, Temp.GetData());
+ {{- else if and (.IsPrimitive ) (not (eq .KindType "enum")) }}
+ auto len{{snake .Name}} = {{$cppropName}}.Num();
+ {{jniToReturnType .}} {{$localName}} = Env->New{{jniToEnvNameType .}}Array(len{{snake .Name}});
+ if ({{$localName}} == NULL){/*Log error, skip?*/};
+ Env->Set{{jniToEnvNameType .}}ArrayRegion({{$localName}}, 0, len{{snake .Name}}, {{- if (eq .KindType "int64") -}}
+ reinterpret_cast({{$cppropName}}.GetData()));
+ {{- else -}}
+ {{$cppropName}}.GetData());
+ {{- end }}
+ {{- else if not (eq .KindType "extern")}}
+ {{jniToReturnType .}} {{$localName}} = {{$javaClassConverter}}::makeJava{{Camel .Type }}Array(Env, {{$cppropName}});
+ {{- end }}
+ {{- else if (eq .KindType "string")}}
+ auto {{$localName}}Wrapped = FJavaHelper::ToJavaString(Env, {{$cppropName}});
+ jstring {{$localName}} = static_cast(Env->NewLocalRef(*{{$localName}}Wrapped));
+ {{- else if ( or (not .IsPrimitive ) (eq .KindType "enum" ) ) }}
+ {{jniToReturnType .}} {{$localName}} = {{$javaClassConverter}}::makeJava{{Camel .Type }}(Env, {{$cppropName}});
+ {{- end }}
+{{- end}}
+
+
+#include "{{$ModuleName}}/Generated/Jni/{{$Iface}}JniClient.h"
+#include "{{$ModuleName}}/Generated/Jni/{{$ModuleName}}DataJavaConverter.h"
+{{ if or (len .Module.Enums) (len .Module.Structs) -}}
+#include "{{$ModuleName}}/Generated/api/{{ $ModuleName }}_data.h"
+{{ end }}
+#include "Async/Async.h"
+#include "Engine/Engine.h"
+
+#if PLATFORM_ANDROID
+
+#include "Engine/Engine.h"
+ #include "Android/AndroidJNI.h"
+ #include "Android/AndroidApplication.h"
+
+ #if USE_ANDROID_JNI
+ #include
+ #endif
+#endif
+
+#include
+#include "HAL/CriticalSection.h"
+#include "GenericPlatform/GenericPlatformMisc.h"
+
+/**
+ \brief data structure to hold the last sent property values
+*/
+
+
+class {{$Class}}MethodHelper
+{
+public:
+ template
+ FGuid StorePromise(TPromise& Promise);
+
+ template
+ bool FulfillPromise(const FGuid& Id, const ResultType& Value);
+private:
+ TMap ReplyPromisesMap;
+ FCriticalSection ReplyPromisesMapCS;
+
+};
+
+namespace {
+
+ {{$Class}}* g{{$Class}}Handle = nullptr;
+ TFunction g{{$Class}}notifyIsReady = [](bool value) { (void)value; UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("notifyIsReady used but not set ")); };
+ {{- range .Interface.Properties }}
+ TFunction g{{$Class}}On{{Camel .Name}}ChangedEmpty = []({{ueReturn "" .}} value) { (void)value; UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("on{{Camel .Name}}Changed used but not set ")); };
+ TFunction g{{$Class}}On{{Camel .Name}}Changed = g{{$Class}}On{{Camel .Name}}ChangedEmpty;
+ {{- end}}
+
+ {{$Class}}MethodHelper g{{$Class}}methodHelper;
+
+}
+
+
+DEFINE_LOG_CATEGORY(Log{{$Iface}}Client_JNI);
+
+{{$Class}}::{{$Class}}()
+{
+#if !(PLATFORM_ANDROID && USE_ANDROID_JNI)
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("This is class that adapts the usage with android and jni, but seems to be used on different target. Make sure you are using it with Android"));
+#endif
+}
+
+{{$Class}}::{{$Class}}(FVTableHelper& Helper)
+ : Super(Helper)
+{
+}
+{{$Class}}::~{{$Class}}() = default;
+
+void {{$Class}}::Initialize(FSubsystemCollectionBase& Collection)
+{
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("Init"));
+ Super::Initialize(Collection);
+
+ g{{$Class}}Handle = this;
+ g{{$Class}}notifyIsReady = [this](bool value) {
+ b_isReady = value;
+ AsyncTask(ENamedThreads::GameThread, [this]()
+ {
+ _ConnectionStatusChangedBP.Broadcast(b_isReady);
+ _ConnectionStatusChanged.Broadcast(b_isReady);
+ });
+ };
+
+ {{- range .Interface.Properties }}
+ g{{$Class}}On{{Camel .Name}}Changed = [this]({{ueParam "In" . }})
+ {
+ {{ueVar "" .}} = {{ueVar "In" .}};
+ _GetSignals()->Broadcast{{Camel .Name}}Changed({{ueVar "" .}});
+ };
+ {{- end}}
+
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ JNIEnv* Env = FAndroidApplication::GetJavaEnv();
+ m_javaJniClientClass = FAndroidApplication::FindJavaClassGlobalRef("{{$javaClassFull}}");
+ jmethodID constructor = Env->GetMethodID(m_javaJniClientClass, "", "()V");
+ jobject localRef = Env->NewObject(m_javaJniClientClass, constructor);
+ m_javaJniClientInstance = Env->NewGlobalRef(localRef);
+ FAndroidApplication::GetJavaEnv()->DeleteLocalRef(localRef);
+#endif
+}
+
+void {{$Class}}::Deinitialize()
+{
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("deinit"));
+ _unbind();
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ JNIEnv* Env = FAndroidApplication::GetJavaEnv();
+ Env->DeleteGlobalRef(m_javaJniClientInstance);
+ m_javaJniClientClass = nullptr;
+ m_javaJniClientInstance = nullptr;
+#endif
+
+ g{{$Class}}notifyIsReady = [](bool value){(void)value; UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("notifyIsReady used but not set "));};
+ {{- range .Interface.Properties}}
+ g{{$Class}}On{{Camel .Name}}Changed = g{{$Class}}On{{Camel .Name}}ChangedEmpty;
+ {{- end}}
+
+ g{{$Class}}Handle = nullptr;
+ Super::Deinitialize();
+}
+
+{{- range .Interface.Properties }}
+{{ueReturn "" .}} {{$Class}}::Get{{Camel .Name}}() const
+{
+ return {{ueVar "" . }};
+}
+
+{{- if not .IsReadOnly }}
+void {{$Class}}::Set{{Camel .Name}}({{ueParam "In" .}})
+{
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("{{$javaClassPath}}/{{$javaClassName}}:set{{Camel .Name}}"));
+ if (!b_isReady)
+ {
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("No valid connection to service. Check that android service is set up correctly"));
+#else
+ UE_LOG(Log{{$Iface}}Client_JNI, Log, TEXT("No valid connection to service. Check that android service is set up correctly"));
+#endif
+ return;
+ }
+
+ // only send change requests if the value changed -> reduce network load
+ if (Get{{Camel .Name}}() == {{ueVar "In" . }} )
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("Property {{.Name}} to set is same as current, new value won't be sent"));
+ return;
+ }
+
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
+ {
+ {{- $signatureParam := jniJavaSignatureParam . }}
+ if (m_javaJniClientClass == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:set{{Camel .Name}} ({{$signatureParam}})V CLASS not found"));
+ return;
+ }
+ static jmethodID MethodID = Env->GetMethodID(m_javaJniClientClass, "set{{Camel .Name}}", "({{$signatureParam}})V");
+ if (MethodID == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:set{{Camel .Name}} ({{$signatureParam}})V not found"));
+ return;
+ }
+ {{- $cppropName := ueVar "In" .}}
+ {{- if or ( or .IsArray (eq .KindType "string")) ( or (eq .KindType "enum") (not (ueIsStdSimpleType .)) ) }}
+ {{template "convert_to_java_type_in_param" .}}
+ {{- $javaLocalName := printf "jlocal_%s" (Camel .Name) }}
+ FJavaWrapper::CallVoidMethod(Env, m_javaJniClientInstance, MethodID, {{$javaLocalName}});
+ Env->DeleteLocalRef({{$javaLocalName}});
+ {{- else }}
+ FJavaWrapper::CallVoidMethod(Env, m_javaJniClientInstance, MethodID, {{$cppropName}});
+ {{- end }}
+ }
+#endif
+
+}
+
+{{- end }}
+
+{{- end }}
+
+{{- range .Interface.Operations}}
+{{ueReturn "" .Return }} {{$Class}}::{{Camel .Name}}({{ueParams "In" .Params}})
+{
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("{{$javaClassPath}}/{{$javaClassName}}:{{.Name}} "));
+ if (!b_isReady)
+ {
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("No valid connection to service. Check that android service is set up correctly"));
+#else
+ UE_LOG(Log{{$Iface}}Client_JNI, Log, TEXT("No valid connection to service. Check that android service is set up correctly"));
+#endif
+ return{{ if not .Return.IsVoid }} {{ueDefault "" .Return }}{{ end}};
+ }
+ {{- if not .Return.IsVoid }}
+ TPromise<{{ueReturn "" .Return}}> Promise;
+ {{- end}}
+
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ if (m_javaJniClientClass == nullptr)
+ {
+ {{- $signatureParams:= jniJavaSignatureParams .Params}}
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:{{camel .Name}}Async:(Ljava/lang/String;{{$signatureParams}})V CLASS not found"));
+ return{{ if not .Return.IsVoid }} {{ueDefault "" .Return }}{{ end}};
+ }
+ JNIEnv* Env = FAndroidApplication::GetJavaEnv();
+ static jmethodID MethodID = Env->GetMethodID(m_javaJniClientClass, "{{camel .Name}}Async", "(Ljava/lang/String;{{$signatureParams}})V");
+ if (MethodID != nullptr)
+ {
+ {{- if not .Return.IsVoid }}
+ auto id = g{{$Class}}methodHelper.StorePromise(Promise);
+ {{- else}}
+ FGuid id = FGuid::NewGuid();
+ {{- end}}
+ auto idString = FJavaHelper::ToJavaString(Env, id.ToString(EGuidFormats::Digits));
+
+ {{- range .Params -}}
+ {{template "convert_to_java_type_in_param" .}}
+ {{- end }};
+
+ FJavaWrapper::CallVoidMethod(Env, m_javaJniClientInstance, MethodID, *idString{{- if len (.Params) }},{{end}} {{- range $idx, $p := .Params -}} {{- if $idx}}, {{ end -}}
+ {{- $javaPropName := Camel .Name}}
+ {{- $cppropName := ueVar "In" .}}
+ {{- $localName := printf "jlocal_%s" $javaPropName }}
+ {{- if .IsArray }} {{$localName}}
+ {{- else if or ( or (eq .KindType "enum") (eq .KindType "string") ) (not .IsPrimitive ) }} {{$localName}}
+ {{- else }} {{$cppropName}}
+ {{- end -}}
+ {{- end -}});
+
+ {{- range $idx, $p := .Params -}}
+ {{- $javaPropName := Camel .Name}}
+ {{- $localName := printf "jlocal_%s" $javaPropName }}
+ {{- if or .IsArray (eq .KindType "enum" ) }}
+ Env->DeleteLocalRef({{$localName}});
+ {{- else if not ( or (eq .KindType "extern") (ueIsStdSimpleType .) ) }}
+ Env->DeleteLocalRef({{$localName}});
+ {{- end }}
+ {{- end }}
+ }
+ else
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:{{camel .Name}}Async (Ljava/lang/String;{{$signatureParams}})V not found"));
+ }
+#endif
+ //TODO probalby #elsif set some default on promise as a result.
+ return{{ if not .Return.IsVoid }} Promise.GetFuture().Get() {{- end}};
+
+}
+
+{{- end }}
+
+bool {{$Class}}::_bindToService(FString servicePackage, FString connectionId)
+{
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("Request JNI connection to %s"), *servicePackage);
+ if (b_isReady)
+ {
+ if (servicePackage == m_lastBoundServicePackage && connectionId == m_lastConnectionId)
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("Already bound"));
+ return true;
+ }
+ else
+ {
+ _unbind();
+ }
+ }
+ m_lastBoundServicePackage = servicePackage;
+ m_lastConnectionId = connectionId;
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ JNIEnv* Env = FAndroidApplication::GetJavaEnv();
+ if (m_javaJniClientClass == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:bind:(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Z CLASS not found"));
+ return false;
+ }
+ static jmethodID MethodID = Env->GetMethodID(m_javaJniClientClass, "bind", "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Z");
+ if (MethodID != nullptr)
+ {
+ jobject Activity = FJavaWrapper::GameActivityThis;
+ auto jPackage = FJavaHelper::ToJavaString(Env, servicePackage);
+ auto jConnId = FJavaHelper::ToJavaString(Env, connectionId);
+ auto res = FJavaWrapper::CallBooleanMethod(Env, m_javaJniClientInstance, MethodID, Activity, *jPackage, *jConnId);
+ return res;
+ }
+ else
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:bind (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Z not found"));
+ }
+#endif
+ return false;
+}
+
+void {{$Class}}::_unbind()
+{
+
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("Request JNI unbind"));
+
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ JNIEnv* Env = FAndroidApplication::GetJavaEnv();
+ if (m_javaJniClientClass == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:unbind:()V CLASS not found"));
+ return;
+ }
+ static jmethodID MethodID = Env->GetMethodID(m_javaJniClientClass, "unbind", "()V");
+ if (MethodID != nullptr)
+ {
+ FJavaWrapper::CallVoidMethod(Env, m_javaJniClientInstance, MethodID);
+ }
+ else
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$javaClassPath}}/{{$javaClassName}}:unbind ()V not found"));
+ }
+#endif
+}
+
+bool {{$Class}}::_IsReady() const
+{
+ return b_isReady;
+}
+
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+
+{{- range .Interface.Properties }}
+{{- $javaPropName := .Name}}
+JNI_METHOD void {{$jniFullFuncPrefix}}_nativeOn{{Camel .Name}}Changed(JNIEnv* Env, jclass Clazz,{{jniJavaParam "" . }})
+{
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("{{$jniFullFuncPrefix}}_nativeOn{{Camel .Name}}Changed"));
+ if (g{{$Class}}Handle == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$jniFullFuncPrefix}}_nativeOn{{Camel .Name}}Changed: JNI SERVICE ADAPTER NOT FOUND "));
+ return;
+ }
+
+ {{- template "convert_to_local_cpp_value_java_param" . }}
+ {{- $hasLocalVar := or .IsArray ( or (eq .KindType "enum") (not (ueIsStdSimpleType .)) ) }}
+ {{- $local_value := printf "local_%s" (snake .Name) }}
+
+ AsyncTask(ENamedThreads::GameThread, [{{- if $hasLocalVar }}p{{$local_value -}} = MoveTemp({{$local_value -}}){{- else}}{{$javaPropName}}{{- end}}]()
+ {
+ {{- if $hasLocalVar }}
+ g{{$Class}}On{{Camel .Name}}Changed(p{{$local_value}});
+ {{- else}}
+ g{{$Class}}On{{Camel .Name}}Changed({{$javaPropName}});
+ {{- end}}
+ });
+}
+{{- end}}
+
+{{- range .Interface.Signals }}
+
+JNI_METHOD void {{$jniFullFuncPrefix}}_nativeOn{{Camel .Name}}(JNIEnv* Env, jclass Clazz{{if len (.Params)}}, {{end}}{{jniJavaParams "" .Params }})
+{
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("{{$jniFullFuncPrefix}}_nativeOn{{Camel .Name}}"));
+ if (g{{$Class}}Handle == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$jniFullFuncPrefix}}_nativeOn{{Camel .Name}}: JNI SERVICE ADAPTER NOT FOUND "));
+ return;
+ }
+
+{{- range .Params -}}
+ {{- template "convert_to_local_cpp_value_java_param" . }}
+{{- end }}
+
+ AsyncTask(ENamedThreads::GameThread, [{{- range $idx, $p := .Params -}}
+ {{- if $idx}}, {{ end -}}
+ {{- $local_value := printf "local_%s" (snake .Name) -}}
+ {{- if or .IsArray ( or (eq .KindType "enum") (not (ueIsStdSimpleType .)) ) }} p{{$local_value -}} = MoveTemp({{$local_value -}})
+ {{- else }} {{.Name}}
+ {{- end -}}
+ {{- end -}}]()
+ {
+ if (g{{$Class}}Handle == nullptr)
+ {
+ UE_LOG(Log{{$Iface}}Client_JNI, Warning, TEXT("{{$jniFullFuncPrefix}}_nativeOn{{Camel .Name}}: JNI SERVICE ADAPTER NOT FOUND "));
+ return;
+ }
+ g{{$Class}}Handle->_GetSignals()->Broadcast{{Camel .Name}}Signal({{- range $idx, $p := .Params -}}
+ {{- if $idx}}, {{ end -}}
+ {{- $local_value := printf "local_%s" (snake .Name) -}}
+ {{- if or .IsArray ( or (eq .KindType "enum") (not (ueIsStdSimpleType .)) ) }} p{{$local_value -}}
+ {{- else }} {{ .Name}}
+ {{- end -}}
+ {{- end -}});
+ });
+}
+
+{{- end }}
+
+{{- range .Interface.Operations}}
+
+JNI_METHOD void {{$jniFullFuncPrefix}}_nativeOn{{Camel .Name}}Result(JNIEnv* Env, jclass Clazz, {{if not .Return.IsVoid}}{{jniToReturnType .Return }} result, {{end }}jstring callId)
+{
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("{{$jniFullFuncPrefix}}_nativeOn{{Camel .Name}}Result"));
+ FString callIdString = FJavaHelper::FStringFromParam(Env, callId);
+ FGuid guid;
+
+{{- if not .Return.IsVoid }}
+{{- $javaClassConverter := printf "%sDataJavaConverter" ( Camel .Return.Schema.Import ) }}
+{{- $hasLocalVar := 1 }}
+{{- if (eq $javaClassConverter "DataJavaConverter" )}}{{- $javaClassConverter = printf "%sDataJavaConverter" $ModuleName}}{{ end }}
+{{- if .Return.IsArray }}
+ {{ueReturn "" .Return}} cpp_result = {{ ueDefault "" .Return }};
+ {{- if (eq .Return.KindType "string")}}
+ cpp_result = FJavaHelper::ObjectArrayToFStringTArray(Env, result);
+ {{- else if (eq .Return.KindType "bool")}}
+ jbooleanArray localArray = (jbooleanArray)result;
+ jsize len = Env->GetArrayLength(localArray);
+ cpp_result.AddUninitialized(len);
+ TArray Temp;
+ Temp.SetNumUninitialized(len);
+ Env->GetBooleanArrayRegion(localArray, 0, len, Temp.GetData());
+ for (int i = 0; i < len; i++)
+ {
+ cpp_result[i] = (Temp[i] == JNI_TRUE);
+ }
+ Env->DeleteLocalRef(localArray);
+ {{- else if .Return.IsPrimitive }}
+ {{ jniToReturnType .Return }} localArray = ({{ jniToReturnType .Return }})result;
+ jsize len = Env->GetArrayLength(localArray);
+ cpp_result.AddUninitialized(len);
+ Env->Get{{jniToEnvNameType .Return}}ArrayRegion(result, 0, len, {{- if (eq .Return.KindType "int64") -}}
+ reinterpret_cast(cpp_result.GetData()));
+ {{- else -}}
+ cpp_result.GetData());
+ {{- end }}
+ Env->DeleteLocalRef(localArray);
+ {{- else if not (eq .Return.KindType "extern")}}
+ {{$javaClassConverter}}::fill{{Camel .Return.Type }}Array(Env, result, cpp_result);
+ {{- end }}
+{{- else if eq .Return.KindType "enum" }}
+ {{ueReturn "" .Return}} cpp_result = {{$javaClassConverter}}::get{{Camel .Return.Type }}Value(Env, result);
+{{- else if (eq .Return.KindType "string")}}
+ FString cpp_result = FJavaHelper::FStringFromParam(Env, result);
+{{- else if not (ueIsStdSimpleType .Return )}}
+ {{ueReturn "" .Return}} cpp_result = {{ ueDefault "" .Return }};
+ {{$javaClassConverter}}::fill{{Camel .Return.Type }}(Env, result,cpp_result);
+{{- else }}
+{{- $hasLocalVar = 0 }}
+{{- end }}
+
+ FGuid::Parse(callIdString, guid);
+ AsyncTask(ENamedThreads::GameThread, [guid, {{if $hasLocalVar}}local_result = MoveTemp(cpp_result){{else}}result{{end}}]()
+ {
+ g{{$Class}}methodHelper.FulfillPromise(guid, {{if $hasLocalVar}}local_{{end}}result);
+ });
+ {{ else }}
+ FGuid::Parse(callIdString, guid);
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT("{{$jniFullFuncPrefix}}_nativeOn{{Camel .Name}}Result for id %s"), *(guid.ToString(EGuidFormats::Digits)));
+ {{- end }}
+}
+
+{{- end }}
+
+JNI_METHOD void {{$jniFullFuncPrefix}}_nativeIsReady(JNIEnv* Env, jclass Clazz, jboolean value)
+{
+ AsyncTask(ENamedThreads::GameThread, [value]()
+ {
+ g{{$Class}}notifyIsReady(value);
+ });
+}
+#endif
+
+
+template
+FGuid {{$Class}}MethodHelper::StorePromise(TPromise& Promise)
+{
+ FGuid Id = FGuid::NewGuid();
+ FScopeLock Lock(&ReplyPromisesMapCS);
+ ReplyPromisesMap.Add(Id, &Promise);
+ //TODO invalid id if sth goes wrong + log + checking
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT(" method store id %s"), *(Id.ToString(EGuidFormats::Digits)));
+ return Id;
+}
+
+template
+bool {{$Class}}MethodHelper::FulfillPromise(const FGuid& Id, const ResultType& Value)
+{
+ UE_LOG(Log{{$Iface}}Client_JNI, Verbose, TEXT(" method resolving id %s"), *(Id.ToString(EGuidFormats::Digits)));
+ TPromise* PromisePtr = nullptr;
+
+ {
+ FScopeLock Lock(&ReplyPromisesMapCS);
+ if (auto** Found = ReplyPromisesMap.Find(Id))
+ {
+ PromisePtr = static_cast*>(*Found);
+ ReplyPromisesMap.Remove(Id);
+ }
+ }
+
+ if (PromisePtr)
+ {
+ AsyncTask(ENamedThreads::GameThread, [Value, PromisePtr]()
+ {
+ PromisePtr->SetValue(Value);
+ });
+ return true;
+
+ }
+ return false;
+}
+{{- $returnTypes := getEmptyStringList}}
+{{- range .Interface.Operations }}
+{{- if not .Return.IsVoid }}
+{{- $type := ueReturn "" .Return }}
+{{- $returnTypes = (appendList $returnTypes $type) }}
+{{- end }}
+{{- end }}
+{{- $returnTypes = unique $returnTypes }}
+{{- range $returnTypes}}
+template FGuid {{$Class}}MethodHelper::StorePromise<{{.}}>(TPromise<{{.}}>& Promise);
+template bool {{$Class}}MethodHelper::FulfillPromise<{{.}}>(const FGuid& Id, const {{.}}& Value);
+{{- end}}
+
diff --git a/templates/module/Source/modulejni/Private/Tests/jni.spec.cpp.tpl b/templates/module/Source/modulejni/Private/Tests/jni.spec.cpp.tpl
new file mode 100644
index 000000000..415ce3629
--- /dev/null
+++ b/templates/module/Source/modulejni/Private/Tests/jni.spec.cpp.tpl
@@ -0,0 +1,422 @@
+{{- /* Copyright Epic Games, Inc. All Rights Reserved */ -}}
+/**{{ template "copyright" }}*/
+{{- $ModuleName := Camel .Module.Name}}
+{{- $DisplayName := printf "%s%s" $ModuleName (Camel .Interface.Name) }}
+{{- $IfaceName := Camel .Interface.Name }}
+{{- $Class := printf "U%s" $DisplayName}}
+{{- $Iface := printf "%s%s" $ModuleName $IfaceName }}
+
+#include "Misc/AutomationTest.h"
+#if WITH_DEV_AUTOMATION_TESTS
+
+#include "{{$ModuleName}}/Tests/{{$ModuleName}}TestsCommon.h"
+#include "{{$ModuleName}}/Implementation/{{$Iface}}.h"
+#include "{{$DisplayName}}JniFixture.h"
+#include "{{$ModuleName}}/Generated/Jni/{{$DisplayName}}JniClient.h"
+#include "{{$ModuleName}}/Generated/Jni/{{$DisplayName}}JniAdapter.h"
+{{- range .Module.Imports }}
+#include "{{Camel .Name}}/Tests/{{Camel .Name}}TestsCommon.h"
+{{- end }}
+
+
+#if PLATFORM_ANDROID
+
+#include "Engine/Engine.h"
+ #include "Android/AndroidJNI.h"
+ #include "Android/AndroidApplication.h"
+
+ #if USE_ANDROID_JNI
+ #include
+ #endif
+#endif
+
+// nested namespaces do not work with UE4.27 MSVC due to old C++ standard
+namespace {{$ModuleName}}
+{
+namespace {{$IfaceName}}
+{
+namespace Jni
+{
+namespace Tests
+{
+BEGIN_DEFINE_SPEC({{$Class}}JniSpec, "{{$ModuleName}}.{{$IfaceName}}.Jni", {{$ModuleName}}TestFilterMask);
+
+TUniquePtr ImplFixture;
+
+END_DEFINE_SPEC({{$Class}}JniSpec);
+
+void {{$Class}}JniSpec::Define()
+{
+ LatentBeforeEach([this](const FDoneDelegate& TestDone)
+ {
+ ImplFixture = MakeUnique();
+ TestTrue("Check for valid ImplFixture", ImplFixture.IsValid());
+
+ TestTrue("Check for valid testImplementation", ImplFixture->GetClient() != nullptr);
+
+ // set up service and adapter
+ auto service =ImplFixture->GetLocalImplementation();
+ ImplFixture->GetAdapter()->setBackendService(service);
+
+ // setup client
+ U{{$DisplayName}}JniClient* JniClient = ImplFixture->GetClient();
+ TestTrue("Check for valid Jni client", JniClient != nullptr);
+
+ #if PLATFORM_ANDROID && USE_ANDROID_JNI
+ JniClient->_ConnectionStatusChanged.AddLambda([this, TestDone](bool bConnected)
+ {
+ if (bConnected)
+ {
+ TestDone.Execute();
+ }
+ });
+ //Test packge name should start with name of the pacakge declared by the test application in e.g. defaultEngine.ini in [/Script/AndroidRuntimeSettings.AndroidRuntimeSettings] section.
+ {{- $projectName := .System.Name }}
+ {{- $service_package_name := printf "com.%s" (camel $projectName) }}
+ FString servicePackage = "{{$service_package_name}}";
+ JniClient->_bindToService(servicePackage, "TestConnectionId");
+ #else
+ TestDone.Execute();
+ #endif
+ });
+
+ AfterEach([this]()
+ {
+ #if PLATFORM_ANDROID && USE_ANDROID_JNI
+ U{{$DisplayName}}JniClient* JniClient =ImplFixture->GetClient();
+ #endif
+ ImplFixture.Reset();
+ });
+{{- range .Interface.Properties }}
+
+ It("Property.{{ Camel .Name }}.Default", [this]()
+ {
+ // Do implement test here
+ {{ueType "" .}} TestValue = {{ueDefault "" .}}; // default value
+ {{- if not .IsReadOnly }}
+ TestEqual(TEXT("Getter should return the default value"), ImplFixture->GetClient()->Get{{Camel .Name}}(), TestValue);
+ {{- else }}
+ TestEqual(TEXT("Getter should return the default value"), ImplFixture->GetClient()->Get{{Camel .Name}}(), {{ueDefault "" .}});
+ {{- end }}
+ });
+
+ {{- if and (not .IsReadOnly) (not (eq .KindType "extern")) (not (eq .KindType "interface"))}}
+
+ LatentIt("Property.{{ Camel .Name }}.Change", EAsyncExecution::ThreadPool, [this](const FDoneDelegate TestDone)
+ {
+ // Do implement test here
+ {{ueType "" .}} TestValue = {{ueDefault "" .}}; // default value
+ TestEqual(TEXT("Getter should return the default value"), ImplFixture->GetClient()->Get{{Camel .Name}}(), TestValue);
+
+ {{$Class}}Signals* {{$Iface}}Signals = ImplFixture->GetClient()->_GetSignals();
+ {{$Iface}}Signals->On{{Camel .Name}}Changed.AddLambda([this, TestDone]({{ueParam "In" .}})
+ {
+ {{ueType "" .}} TestValue = {{ueDefault "" .}};
+ // use different test value
+ {{- if .IsArray }}
+ {{- if or .IsPrimitive (eq .KindType "enum") }}
+ TestValue.Add({{ ueTestValue "" .}});
+ {{- else }}
+ {{- $type := ""}}
+ {{- if not (eq .Import "") }}
+ {{- $type = printf "F%s%s" (Camel .Import) .Type }}
+ {{- else }}
+ {{- $type = printf "F%s%s" $ModuleName .Type }}
+ {{- end }}
+ TestValue = createTest{{ $type }}Array();
+ {{- end }}
+ {{- else if and (not .IsPrimitive) (not (eq .KindType "enum")) (not (eq .KindType "interface"))}}
+ TestValue = createTest{{ ueType "" . }}();
+ {{- else }}
+ TestValue = {{ ueTestValue "" . }};
+ {{- end }}
+ TestEqual(TEXT("Delegate parameter should be the same value as set by the setter"), {{ueVar "In" .}}, TestValue);
+ TestEqual(TEXT("Getter should return the same value as set by the setter"), ImplFixture->GetClient()->Get{{Camel .Name}}(), TestValue);
+ TestDone.Execute();
+ });
+
+ // use different test value
+ {{- if .IsArray }}
+ {{- if or .IsPrimitive (eq .KindType "enum") }}
+ TestValue.Add({{ ueTestValue "" .}});
+ {{- else }}
+ {{- $type := ""}}
+ {{- if not (eq .Import "") }}
+ {{- $type = printf "F%s%s" (Camel .Import) .Type }}
+ {{- else }}
+ {{- $type = printf "F%s%s" $ModuleName .Type }}
+ {{- end }}
+ TestValue = createTest{{ $type }}Array();
+ {{- end }}
+ {{- else if and (not .IsPrimitive) (not (eq .KindType "enum")) (not (eq .KindType "interface"))}}
+ TestValue = createTest{{ ueType "" . }}();
+ {{- else }}
+ TestValue = {{ ueTestValue "" . }};
+ {{- end }}
+ ImplFixture->GetClient()->Set{{Camel .Name}}(TestValue);
+ #if ! (PLATFORM_ANDROID && USE_ANDROID_JNI)
+ TestDone.Execute();
+ #endif
+ });
+
+ LatentIt("Property.{{ Camel .Name }}.ChangeLocalCheckRemote", EAsyncExecution::ThreadPool, [this](const FDoneDelegate TestDone)
+ {
+ // Do implement test here
+ {{ueType "" .}} DefaultValue = {{ueDefault "" .}}; // default value
+ TestEqual(TEXT("Getter should return the default value"), ImplFixture->GetClient()->Get{{Camel .Name}}(), DefaultValue);
+
+ #if PLATFORM_ANDROID && USE_ANDROID_JNI
+ {{$Class}}Signals* {{$Iface}}Signals = ImplFixture->GetClient()->_GetSignals();
+ #else
+ {{$Class}}Signals* {{$Iface}}Signals = ImplFixture->GetLocalImplementation()->_GetSignals();
+ #endif
+ {{$Iface}}Signals->On{{Camel .Name}}Changed.AddLambda([this, DefaultValue, TestDone]({{ueParam "In" .}})
+ {
+ {{ueType "" .}} TestValue = {{ueDefault "" .}};
+ // use different test value
+ {{- if .IsArray }}
+ {{- if or .IsPrimitive (eq .KindType "enum") }}
+ TestValue.Add({{ ueTestValue "" .}});
+ {{- else }}
+ {{- $type := ""}}
+ {{- if not (eq .Import "") }}
+ {{- $type = printf "F%s%s" (Camel .Import) .Type }}
+ {{- else }}
+ {{- $type = printf "F%s%s" $ModuleName .Type }}
+ {{- end }}
+ TestValue = createTest{{ $type }}Array();
+ {{- end }}
+ {{- else if and (not .IsPrimitive) (not (eq .KindType "enum")) (not (eq .KindType "interface"))}}
+ TestValue = createTest{{ ueType "" . }}();
+ {{- else }}
+ TestValue = {{ ueTestValue "" . }};
+ {{- end }}
+ TestEqual(TEXT("Delegate parameter should be the same value as set by the setter"), {{ueVar "In" .}}, TestValue);
+ TestEqual(TEXT("Getter should return the same value as set by the setter"), ImplFixture->GetLocalImplementation()->Get{{Camel .Name}}(), TestValue);
+ #if PLATFORM_ANDROID && USE_ANDROID_JNI
+ //TODO CHANGE THE IMPLEMENTATION TO CLIENT this is confusing
+ TestEqual(TEXT("Getter should return the same value as set by the setter"), ImplFixture->GetClient()->Get{{Camel .Name}}(), TestValue);
+ #else
+ TestEqual(TEXT("No connection, client has same value as at the start"), ImplFixture->GetClient()->Get{{Camel .Name}}(), DefaultValue);
+ #endif
+ TestDone.Execute();
+ });
+ // use different test value, but init it first.
+ {{ueType "" .}} TestValue = DefaultValue;
+ {{- if .IsArray }}
+ {{- if or .IsPrimitive (eq .KindType "enum") }}
+ TestValue.Add({{ ueTestValue "" .}});
+ {{- else }}
+ {{- $type := ""}}
+ {{- if not (eq .Import "") }}
+ {{- $type = printf "F%s%s" (Camel .Import) .Type }}
+ {{- else }}
+ {{- $type = printf "F%s%s" $ModuleName .Type }}
+ {{- end }}
+ TestValue = createTest{{ $type }}Array();
+ {{- end }}
+ {{- else if and (not .IsPrimitive) (not (eq .KindType "enum")) (not (eq .KindType "interface"))}}
+ TestValue = createTest{{ ueType "" . }}();
+ {{- else }}
+ TestValue = {{ ueTestValue "" . }};
+ {{- end }}
+ auto service = ImplFixture->GetLocalImplementation();
+ service->Set{{Camel .Name}}(TestValue);
+ });
+
+ LatentIt("Property.{{ Camel .Name }}.ChangeLocalChangeBackRemote", EAsyncExecution::ThreadPool, [this](const FDoneDelegate TestDone)
+ {
+ // Do implement test here
+ {{ueType "" .}} StartValue = ImplFixture->GetLocalImplementation()->Get{{Camel .Name}}();
+ TestEqual(TEXT("Getter should return the default value"), ImplFixture->GetClient()->Get{{Camel .Name}}(), StartValue);
+
+ #if PLATFORM_ANDROID && USE_ANDROID_JNI
+ {{$Class}}Signals* {{$Iface}}Signals = ImplFixture->GetClient()->_GetSignals();
+ {{$Iface}}Signals->On{{Camel .Name}}Changed.AddLambda([this, TestDone]({{ueParam "In" .}})
+ #else
+ {{$Class}}Signals* {{$Iface}}Signals = ImplFixture->GetLocalImplementation()->_GetSignals();
+ {{$Iface}}Signals->On{{Camel .Name}}Changed.AddLambda([this, TestDone, StartValue]({{ueParam "In" .}})
+ #endif
+ {
+ // this function must be called twice before we can successfully pass this test.
+ // first call it should have the test value of the parameter
+ // second call it should have the default value of the parameter again
+ static int count = 0;
+ count++;
+
+ if (count % 2 != 0)
+ {
+ {{ueType "" .}} TestValue = {{ueDefault "" .}};
+ // use different test value
+ {{- if .IsArray }}
+ {{- if or .IsPrimitive (eq .KindType "enum") }}
+ TestValue.Add({{ ueTestValue "" .}});
+ {{- else }}
+ {{- $type := ""}}
+ {{- if not (eq .Import "") }}
+ {{- $type = printf "F%s%s" (Camel .Import) .Type }}
+ {{- else }}
+ {{- $type = printf "F%s%s" $ModuleName .Type }}
+ {{- end }}
+ TestValue = createTest{{ $type }}Array();
+ {{- end }}
+ {{- else if and (not .IsPrimitive) (not (eq .KindType "enum")) (not (eq .KindType "interface"))}}
+ TestValue = createTest{{ ueType "" . }}();
+ {{- else }}
+ TestValue = {{ ueTestValue "" . }};
+ {{- end }}
+ TestEqual(TEXT("Delegate parameter should be the same value as set by the setter"), {{ueVar "In" .}}, TestValue);
+ TestEqual(TEXT("Getter should return the same value as set by the setter"), ImplFixture->GetLocalImplementation()->Get{{Camel .Name}}(), TestValue);
+ #if PLATFORM_ANDROID && USE_ANDROID_JNI
+ TestEqual(TEXT("Getter should return the same value as set by the setter"), ImplFixture->GetClient()->Get{{Camel .Name}}(), TestValue);
+ #else
+ TestEqual(TEXT("Getter should return the same value as set by the setter"), ImplFixture->GetClient()->Get{{Camel .Name}}(), StartValue);
+ #endif
+ // now set it to the default value
+ TestValue = {{ueDefault "" .}}; // default value
+ #if PLATFORM_ANDROID && USE_ANDROID_JNI
+ ImplFixture->GetClient()->Set{{Camel .Name}}(TestValue);
+ #else
+ ImplFixture->GetLocalImplementation()->Set{{Camel .Name}}(TestValue);
+ #endif
+ }
+ else
+ {
+ {{ueType "" .}} TestValue = {{ueDefault "" .}}; // default value
+ TestEqual(TEXT("Delegate parameter should be the same value as set by the setter"), {{ueVar "In" .}}, TestValue);
+ TestEqual(TEXT("Getter should return the same value as set by the setter"), ImplFixture->GetLocalImplementation()->Get{{Camel .Name}}(), TestValue);
+ TestEqual(TEXT("Getter should return the same value as set by the setter"), ImplFixture->GetClient()->Get{{Camel .Name}}(), TestValue);
+ TestDone.Execute();
+ }
+ });
+ // use different test value
+ {{ueType "" .}} TestValue = StartValue;
+ {{- if .IsArray }}
+ {{- if or .IsPrimitive (eq .KindType "enum") }}
+ TestValue.Add({{ ueTestValue "" .}});
+ {{- else }}
+ {{- $type := ""}}
+ {{- if not (eq .Import "") }}
+ {{- $type = printf "F%s%s" (Camel .Import) .Type }}
+ {{- else }}
+ {{- $type = printf "F%s%s" $ModuleName .Type }}
+ {{- end }}
+ TestValue = createTest{{ $type }}Array();
+ {{- end }}
+ {{- else if and (not .IsPrimitive) (not (eq .KindType "enum")) (not (eq .KindType "interface"))}}
+ TestValue = createTest{{ ueType "" . }}();
+ {{- else }}
+ TestValue = {{ ueTestValue "" . }};
+ {{- end }}
+ auto service = ImplFixture->GetLocalImplementation();
+ service->Set{{Camel .Name}}(TestValue);
+ });
+ {{- end }}
+
+{{- end }}
+
+{{- range .Interface.Operations }}
+{{- nl }}
+ {{- if (eq .Return.KindType "extern")}}
+ // Please implement serialization for {{ueType "" .Return}} before enabling the test.
+ {{- end }}
+ {{if (eq .Return.KindType "extern")}}x{{end}}LatentIt("Operation.{{ Camel .Name }}", EAsyncExecution::ThreadPool, [this](const FDoneDelegate TestDone)
+ {
+ {{- range $i, $e := .Params }}
+ {{- if (eq .KindType "extern")}}
+ // Please implement serialization for {{ueType "" .}} before testing.
+ {{- end }}
+ {{- end }}
+ // Do implement test here
+ AsyncTask(ENamedThreads::AnyThread, [this, TestDone]()
+ {
+ ImplFixture->GetClient()->{{Camel .Name}}(
+ {{- range $i, $e := .Params -}}
+ {{ if $i }}, {{end}}{{ueDefault "" .}}
+ {{- end -}}
+ );
+ //Test will work also without connection, we always return default value. real check should test for custom, which is not possible for generated tests.
+ TestDone.Execute();
+ });
+ });
+{{- end }}
+
+{{- range .Interface.Signals }}
+
+ LatentIt("Signal.{{ Camel .Name }}", EAsyncExecution::ThreadPool, [this](const FDoneDelegate TestDone)
+ {
+
+ {{$Class}}Signals* Source{{$Iface}}Signals = ImplFixture->GetLocalImplementation()->_GetSignals();
+ #if PLATFORM_ANDROID && USE_ANDROID_JNI
+ {{$Class}}Signals* {{$Iface}}Signals = ImplFixture->GetClient()->_GetSignals();
+ #else
+ {{$Class}}Signals* {{$Iface}}Signals = ImplFixture->GetLocalImplementation()->_GetSignals();
+ #endif
+ {{$Iface}}Signals->On{{Camel .Name}}Signal.AddLambda([this, TestDone]({{ueParams "In" .Params}})
+ {
+ // known test value
+ {{- range $i, $e := .Params -}}
+ {{- if not (eq .KindType "extern") }}
+ {{- if .IsArray }}
+ {{- if or .IsPrimitive (eq .KindType "enum") (eq .KindType "interface") }}
+ {{ueType "" .}} {{ueVar "" .}}TestValue = {{ueDefault "" .}}; // default value
+ {{ueVar "" .}}TestValue.Add({{ ueTestValue "" .}});
+ {{- else }}
+ {{- $type := ""}}
+ {{- if not (eq .Import "") }}
+ {{- $type = printf "F%s%s" (Camel .Import) .Type }}
+ {{- else }}
+ {{- $type = printf "F%s%s" $ModuleName .Type }}
+ {{- end }}
+ {{ueType "" .}} {{ueVar "" .}}TestValue = createTest{{ $type }}Array();
+ {{- end }}
+ {{- else if and (not .IsPrimitive) (not (eq .KindType "enum")) (not (eq .KindType "interface"))}}
+ {{ueType "" .}} {{ueVar "" .}}TestValue = createTest{{ ueType "" . }}();
+ {{- else }}
+ {{ueType "" .}} {{ueVar "" .}}TestValue = {{ ueTestValue "" . }};
+ {{- end }}
+ TestEqual(TEXT("Parameter should be the same value as sent by the signal"), {{ueVar "In" .}}, {{ueVar "" .}}TestValue);
+ {{- end }}
+ {{- end }}
+ TestDone.Execute();
+ });
+
+ // use different test value
+ {{- range $i, $e := .Params -}}
+ {{- if not (eq .KindType "extern") }}
+ {{- if .IsArray }}
+ {{- if or .IsPrimitive (eq .KindType "enum") (eq .KindType "interface") }}
+ {{ueType "" .}} {{ueVar "" .}}TestValue = {{ueDefault "" .}}; // default value
+ {{ueVar "" .}}TestValue.Add({{ ueTestValue "" .}});
+ {{- else }}
+ {{- $type := ""}}
+ {{- if not (eq .Import "") }}
+ {{- $type = printf "F%s%s" (Camel .Import) .Type }}
+ {{- else }}
+ {{- $type = printf "F%s%s" $ModuleName .Type }}
+ {{- end }}
+ {{ ueType "" . }} {{ueVar "" .}}TestValue = createTest{{ $type }}Array();
+ {{- end }}
+ {{- else if and (not .IsPrimitive) (not (eq .KindType "enum")) (not (eq .KindType "interface"))}}
+ {{ ueType "" . }} {{ueVar "" .}}TestValue = createTest{{ ueType "" . }}();
+ {{- else }}
+ {{ ueType "" . }} {{ueVar "" .}}TestValue = {{ ueTestValue "" . }};
+ {{- end }}
+ {{- else }}
+ {{ ueType "" . }} {{ueVar "" .}}TestValue = {{ ueDefault "" . }};
+ {{- end }}
+ {{- end }}
+ Source{{$Iface}}Signals->Broadcast{{Camel .Name}}Signal(
+ {{- range $i, $e := .Params -}}
+ {{- if $i }}, {{ end }}{{ueVar "" .}}TestValue
+ {{- end -}});
+ });
+
+{{- end }}
+}
+} // namespace Tests
+} // namespace Jni
+} // namespace {{$IfaceName}}
+} // namespace {{$ModuleName}}
+
+#endif // WITH_DEV_AUTOMATION_TESTS
diff --git a/templates/module/Source/modulejni/Private/Tests/jnifixture.cpp.tpl b/templates/module/Source/modulejni/Private/Tests/jnifixture.cpp.tpl
new file mode 100644
index 000000000..789698cec
--- /dev/null
+++ b/templates/module/Source/modulejni/Private/Tests/jnifixture.cpp.tpl
@@ -0,0 +1,77 @@
+{{- /* Copyright Epic Games, Inc. All Rights Reserved */ -}}
+/**{{ template "copyright" }}*/
+{{- $ModuleName := Camel .Module.Name}}
+{{- $DisplayName := printf "%s%s" $ModuleName (Camel .Interface.Name) }}
+{{- $IfaceName := Camel .Interface.Name }}
+{{- $Class := printf "U%s" $DisplayName}}
+#include "{{$DisplayName}}JniFixture.h"
+#include "Misc/AutomationTest.h"
+
+
+#if WITH_DEV_AUTOMATION_TESTS
+
+#include "{{$ModuleName}}/Generated/Jni/{{$DisplayName}}JniClient.h"
+#include "{{$ModuleName}}/Generated/Jni/{{$DisplayName}}JniAdapter.h"
+#include "{{$ModuleName}}/Implementation/{{$DisplayName}}.h"
+#include "Engine/GameInstance.h"
+
+// nested namespaces do not work with UE4.27 MSVC due to old C++ standard
+namespace {{$ModuleName}}
+{
+namespace {{$IfaceName}}
+{
+namespace Jni
+{
+namespace Tests
+{
+F{{ $DisplayName }}JniFixture::F{{ $DisplayName }}JniFixture()
+{
+ source = GetGameInstance()->GetSubsystem();
+}
+
+F{{ $DisplayName }}JniFixture::~F{{ $DisplayName }}JniFixture()
+{
+ CleanUp();
+}
+
+{{$Class}}JniClient* F{{ $DisplayName }}JniFixture::GetClient()
+{
+ return GetGameInstance()->GetSubsystem();
+}
+
+TScriptInterface F{{ $DisplayName }}JniFixture::GetLocalImplementation()
+{
+ return source;
+}
+
+U{{ $DisplayName }}JniAdapter* F{{ $DisplayName }}JniFixture::GetAdapter()
+{
+ return GetGameInstance()->GetSubsystem();
+}
+
+UGameInstance* F{{ $DisplayName }}JniFixture::GetGameInstance()
+{
+ if (!GameInstance.IsValid())
+ {
+ GameInstance = NewObject(GetTransientPackage());
+ GameInstance->Init();
+ // needed to prevent garbage collection and we can't use UPROPERTY on raw c++ objects
+ GameInstance->AddToRoot();
+ }
+
+ return GameInstance.Get();
+}
+
+void F{{ $DisplayName }}JniFixture::CleanUp()
+{
+ if (GameInstance.IsValid())
+ {
+ GameInstance->Shutdown();
+ GameInstance->RemoveFromRoot();
+ }
+}
+} // namespace Tests
+} // namespace Jni
+} // namespace {{$IfaceName}}
+} // namespace {{$ModuleName}}
+#endif // WITH_DEV_AUTOMATION_TESTS
diff --git a/templates/module/Source/modulejni/Private/Tests/jnifixture.h.tpl b/templates/module/Source/modulejni/Private/Tests/jnifixture.h.tpl
new file mode 100644
index 000000000..fa8126d0f
--- /dev/null
+++ b/templates/module/Source/modulejni/Private/Tests/jnifixture.h.tpl
@@ -0,0 +1,54 @@
+{{- /* Copyright Epic Games, Inc. All Rights Reserved */ -}}
+{{- $ModuleName := Camel .Module.Name}}
+{{- $DisplayName := printf "%s%s" $ModuleName (Camel .Interface.Name) }}
+{{- $IfaceName := Camel .Interface.Name }}
+{{- $Class := printf "U%s" $DisplayName}}
+#pragma once
+
+#include "Misc/AutomationTest.h"
+#if WITH_DEV_AUTOMATION_TESTS
+
+#include "CoreMinimal.h"
+#include "Engine/GameInstance.h"
+#include "UObject/Object.h"
+#include "UObject/NoExportTypes.h"
+#include "{{$ModuleName}}/Generated/api/{{$ModuleName}}_data.h"
+
+class {{$Class}}JniSpec;
+class I{{$DisplayName}}Interface;
+class U{{$DisplayName}}JniAdapter;
+class U{{$DisplayName}}JniClient;
+class IApiGearConnection;
+
+// nested namespaces do not work with UE4.27 MSVC due to old C++ standard
+namespace {{$ModuleName}}
+{
+namespace {{$IfaceName}}
+{
+namespace Jni
+{
+namespace Tests
+{
+class F{{ $DisplayName }}JniFixture
+{
+public:
+ F{{ $DisplayName }}JniFixture();
+ ~F{{ $DisplayName }}JniFixture();
+
+ UGameInstance* GetGameInstance();
+ {{$Class}}JniClient* GetClient();
+ TScriptInterface GetLocalImplementation();
+ {{$Class}}JniAdapter* GetAdapter();
+
+private:
+ void CleanUp();
+
+ TSoftObjectPtr GameInstance;
+ TScriptInterface source;
+};
+} // namespace Tests
+} // namespace Jni
+} // namespace {{$IfaceName}}
+} // namespace {{$ModuleName}}
+
+#endif // WITH_DEV_AUTOMATION_TESTS
diff --git a/templates/module/Source/modulejni/Private/modulejni.cpp.tpl b/templates/module/Source/modulejni/Private/modulejni.cpp.tpl
new file mode 100644
index 000000000..2192716cc
--- /dev/null
+++ b/templates/module/Source/modulejni/Private/modulejni.cpp.tpl
@@ -0,0 +1,43 @@
+{{/* Copyright Epic Games, Inc. All Rights Reserved */}}
+{{- $ModuleName := Camel .Module.Name}}
+{{- $Category := printf "ApiGear%s" $ModuleName -}}
+/**
+Copyright 2021 ApiGear UG
+Copyright 2021 Epic Games, Inc.
+
+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.
+*/
+
+{{- $class := printf "F%sJniModule" $ModuleName}}
+
+#include "{{$ModuleName}}/{{$ModuleName}}Jni.h"
+#include "{{$ModuleName}}/Generated/{{$ModuleName}}Factory.h"
+#include "Engine/Engine.h"
+#include "{{$ModuleName}}Settings.h"
+#include "Modules/ModuleManager.h"
+
+#define LOCTEXT_NAMESPACE "{{$ModuleName}}Jni"
+
+void {{$class}}::StartupModule()
+{
+}
+
+void {{$class}}::ShutdownModule()
+{
+ // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
+ // we call this function before unloading the module.
+}
+
+IMPLEMENT_MODULE({{$class}}, {{$ModuleName}}Jni)
+
+#undef LOCTEXT_NAMESPACE
diff --git a/templates/module/Source/modulejni/Public/Generated/jni/datajavaconverter.h.tpl b/templates/module/Source/modulejni/Public/Generated/jni/datajavaconverter.h.tpl
new file mode 100644
index 000000000..861725397
--- /dev/null
+++ b/templates/module/Source/modulejni/Public/Generated/jni/datajavaconverter.h.tpl
@@ -0,0 +1,83 @@
+{{/* Copyright Epic Games, Inc. All Rights Reserved */}}
+{{- $API_MACRO := printf "%sAPI_API" (CAMEL .Module.Name) }}
+{{- $ModuleName := Camel .Module.Name -}}
+{{- $Category := printf "ApiGear|%s" $ModuleName }}
+/**
+Copyright 2021 ApiGear UG
+Copyright 2021 Epic Games, Inc.
+
+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.
+*/
+#pragma once
+{{- $includes := getEmptyStringList}}
+{{- range .Module.Externs }}
+{{- $class := ueExtern . }}
+{{- if $class.Include }}
+{{- $includeName := printf "\"%s\"" $class.Include }}
+{{- $includes = (appendList $includes $includeName) }}
+{{- end }}
+{{- end }}
+{{- range .Module.Imports }}
+{{- $importModuleName := Camel .Name }}
+{{- $includeName := printf "\"%s/Generated/api/%s_data.h\"" $importModuleName $importModuleName }}
+{{- $includeName := printf "\"%s/Generated/Jni/%sDataJavaConverter.h\"" $importModuleName $importModuleName }}
+{{- $includes = (appendList $includes $includeName) }}
+{{- end }}
+{{- $includes = unique $includes }}
+{{ range $includes }}
+#include {{ .}}
+{{- end }}
+{{ if or (len .Module.Enums) (len .Module.Structs) -}}
+#include "{{ $ModuleName }}/Generated/api/{{ $ModuleName }}_data.h"
+{{ end }}
+
+#if PLATFORM_ANDROID
+
+#include "Engine/Engine.h"
+#include "Android/AndroidJNI.h"
+#include "Android/AndroidApplication.h"
+
+#if USE_ANDROID_JNI
+#include
+#endif
+#endif
+
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+
+#include "Engine/Engine.h"
+{{$className := printf "%sDataJavaConverter" $ModuleName}}
+
+class {{ $API_MACRO }} {{$className }}{
+public:
+{{- range .Module.Structs }}
+
+{{- $structType := printf "F%s%s" $ModuleName .Name }}
+{{- $structName := printf "out_%s" (snake .Name)}}
+
+ static void fill{{Camel .Name }}(JNIEnv* env, jobject input, {{$structType}}& {{$structName}});
+ static void fill{{Camel .Name }}Array(JNIEnv* env, jobjectArray input, TArray<{{$structType}}>& out_array);
+{{- $in_cppStructName := printf "out_%s" (snake .Name)}}
+ static jobject makeJava{{Camel .Name }}(JNIEnv* env, const {{$structType}}& {{$in_cppStructName}});
+ static jobjectArray makeJava{{Camel .Name }}Array(JNIEnv* env, const TArray<{{$structType}}>& cppArray);
+{{- end }}
+
+{{- range .Module.Enums }}
+{{- $cpp_class := printf "E%s%s" $ModuleName .Name }}
+
+ static void fill{{Camel .Name }}Array(JNIEnv* env, jobjectArray input, TArray<{{$cpp_class}}>& out_array);
+ static {{$cpp_class}} get{{Camel .Name }}Value(JNIEnv* env, jobject input);
+ static jobjectArray makeJava{{Camel .Name }}Array(JNIEnv* env, const TArray<{{$cpp_class}}>& cppArray);
+ static jobject makeJava{{Camel .Name }}(JNIEnv* env, {{$cpp_class}} value);
+{{- end }}
+};
+#endif
diff --git a/templates/module/Source/modulejni/Public/Generated/jni/jniadapter.h.tpl b/templates/module/Source/modulejni/Public/Generated/jni/jniadapter.h.tpl
new file mode 100644
index 000000000..0eda857db
--- /dev/null
+++ b/templates/module/Source/modulejni/Public/Generated/jni/jniadapter.h.tpl
@@ -0,0 +1,93 @@
+
+{{- /* Copyright Epic Games, Inc. All Rights Reserved */ -}}
+/**{{ template "copyright" }}*/
+{{- $ModuleName := Camel .Module.Name}}
+
+{{- $IfaceName := Camel .Interface.Name }}
+{{- $API_MACRO := printf "%sJNI_API" (CAMEL .Module.Name) }}
+{{- $Category := printf "ApiGear|%s|%s" $ModuleName $IfaceName }}
+{{- $DisplayName := printf "%s%s" $ModuleName $IfaceName }}
+{{- $Class := printf "U%sJniAdapter" $DisplayName}}
+{{- $Iface := printf "%s%s" $ModuleName $IfaceName }}
+#pragma once
+
+#include "{{$ModuleName}}/Generated/api/{{$Iface}}Interface.h"
+#include "Subsystems/GameInstanceSubsystem.h"
+#include
+
+#if PLATFORM_ANDROID
+
+#include "Engine/Engine.h"
+#include "Android/AndroidJNI.h"
+#include "Android/AndroidApplication.h"
+
+#if USE_ANDROID_JNI
+#include
+#endif
+#endif
+
+
+#include "{{$Iface}}JniAdapter.generated.h"
+
+DECLARE_LOG_CATEGORY_EXTERN(Log{{$Iface}}_JNI, Log, All);
+
+
+/** @brief handles the adaption between the service implementation and the java android Service Backend
+ * takes an object of the type I{{Camel .Module.Name}}{{Camel .Interface.Name}}Interface
+ */
+UCLASS(BlueprintType)
+class {{ $API_MACRO }} {{$Class}} : public UGameInstanceSubsystem
+{
+ GENERATED_BODY()
+public:
+ explicit {{$Class}}();
+ virtual ~{{$Class}}() = default;
+
+ // subsystem
+ void Initialize(FSubsystemCollectionBase& Collection) override;
+ void Deinitialize() override;
+
+ UFUNCTION(BlueprintCallable, Category = "ApiGear|{{Camel .Module.Name}}|{{Camel .Interface.Name}}")
+ void setBackendService(TScriptInterface InService);
+
+ UFUNCTION(BlueprintCallable, Category = "ApiGear|{{Camel .Module.Name}}|{{Camel .Interface.Name}}")
+ TScriptInterface getBackendService();
+
+private:
+
+ //helper function, wraps calling java service side
+ void callJniServiceReady(bool isServiceReady);
+
+ // helper member;
+#if PLATFORM_ANDROID
+#if USE_ANDROID_JNI
+ jclass m_javaJniServiceClass = nullptr;
+ jobject m_javaJniServiceInstance = nullptr;
+#endif
+#endif
+
+ // signals
+{{- range $i, $e := .Interface.Signals }}
+{{- if $i }}{{nl}}{{ end }}
+ UFUNCTION(Category = "{{$Category}}", BlueprintInternalUseOnly)
+ void On{{Camel .Name}}({{ueParams "" .Params}});
+{{- end }}
+{{- if len .Interface.Properties }}{{ nl }}{{ end }}
+{{- range $i, $e := .Interface.Properties }}
+{{- if $i }}{{nl}}{{ end }}
+ UFUNCTION(Category = "{{$Category}}", BlueprintInternalUseOnly)
+ void On{{Camel .Name}}Changed({{ueParam "" .}});
+{{- end }}
+
+ // delegate handles
+{{- range .Interface.Properties }}
+ FDelegateHandle On{{Camel .Name}}ChangedHandle;
+{{- end }}
+{{- range .Interface.Signals }}
+ FDelegateHandle On{{Camel .Name}}SignalHandle;
+{{- end }}
+
+ /** Holds the service backend, can be exchanged with different implementation during runtime */
+ UPROPERTY(VisibleAnywhere, Category = "{{$Category}}")
+ TScriptInterface BackendService;
+};
diff --git a/templates/module/Source/modulejni/Public/Generated/jni/jniclient.h.tpl b/templates/module/Source/modulejni/Public/Generated/jni/jniclient.h.tpl
new file mode 100644
index 000000000..4349d4269
--- /dev/null
+++ b/templates/module/Source/modulejni/Public/Generated/jni/jniclient.h.tpl
@@ -0,0 +1,90 @@
+
+{{- /* Copyright Epic Games, Inc. All Rights Reserved */ -}}
+/**{{ template "copyright" }}*/
+{{- $ModuleName := Camel .Module.Name}}
+
+{{- $IfaceName := Camel .Interface.Name }}
+{{- $API_MACRO := printf "%sJNI_API" (CAMEL .Module.Name) }}
+{{- $Category := printf "ApiGear|%s|%s" $ModuleName $IfaceName }}
+{{- $DisplayName := printf "%s%s" $ModuleName $IfaceName }}
+{{- $Class := printf "U%sJniClient" $DisplayName}}
+{{- $Iface := printf "%s%s" $ModuleName $IfaceName }}
+#pragma once
+
+#include "{{$ModuleName}}/Generated/api/{{$Iface}}Interface.h"
+#include "{{ $ModuleName }}/Generated/api/Abstract{{$Iface}}.h"
+#include "Subsystems/GameInstanceSubsystem.h"
+#include "{{$ModuleName}}/Generated/Jni/{{$ModuleName}}JniConnectionStatus.h"
+#include
+
+#if PLATFORM_ANDROID
+
+#include "Engine/Engine.h"
+#include "Android/AndroidJNI.h"
+#include "Android/AndroidApplication.h"
+
+#if USE_ANDROID_JNI
+#include
+#endif
+#endif
+
+#include "{{$Iface}}JniClient.generated.h"
+
+DECLARE_LOG_CATEGORY_EXTERN(Log{{$Iface}}Client_JNI, Log, All);
+
+UCLASS(NotBlueprintable, BlueprintType)
+class {{ $API_MACRO }} {{$Class}} : public UAbstract{{Camel .Module.Name}}{{Camel .Interface.Name }}
+{
+ GENERATED_BODY()
+public:
+ explicit {{$Class}}();
+
+ {{$Class}}(FVTableHelper& Helper);
+ virtual ~{{$Class}}();
+
+ // subsystem
+ void Initialize(FSubsystemCollectionBase& Collection) override;
+ void Deinitialize() override;
+
+ {{- range .Interface.Properties }}
+ {{ueReturn "" .}} Get{{Camel .Name}}() const override;
+ {{- if not .IsReadOnly }}
+ void Set{{Camel .Name}}({{ueParam "In" .}}) override;
+ {{- end }}
+ {{- end}}
+
+ // operations
+{{- range $i, $e := .Interface.Operations }}
+ virtual {{ueReturn "" .Return}} {{Camel .Name}}({{ueParams "" .Params}}) override;
+{{- end }}
+
+ UPROPERTY(BlueprintAssignable, Category = "ApiGear|{{Camel .Module.Name}}|{{$IfaceName}}|Jni|Remote", DisplayName = "Connection Status Changed")
+ F{{$ModuleName}}JniConnectionStatusChangedDelegateBP _ConnectionStatusChangedBP;
+ F{{$ModuleName}}JniConnectionStatusChangedDelegate _ConnectionStatusChanged;
+
+ /**
+ * Informs about the subscription state of the interface client.
+ * @return true if the client is connected to a service and ready to send and receive messages or false if the server cannot be reached.
+ */
+ UFUNCTION(BlueprintCallable, Category = "ApiGear|{{Camel .Module.Name}}|{{$IfaceName}}|JNI")
+ bool _IsReady() const;
+
+ /**
+ * @param package. This client will connect only to a {{$IfaceName}} Service, that myb be exposed by the application that runs it.
+ * To make successful binding the package of that application must be passed here.
+ * @return true if successfully bound, false it binding failed.
+ */
+ UFUNCTION(BlueprintCallable, Category = "ApiGear|{{Camel .Module.Name}}|{{$IfaceName}}|JNI|Connection")
+ bool _bindToService(FString servicePackage, FString connectionId);
+
+ UFUNCTION(BlueprintCallable, Category = "ApiGear|{{Camel .Module.Name}}|{{$IfaceName}}|JNI|Connection")
+ void _unbind();
+private:
+ bool b_isReady = false;
+ FString m_lastBoundServicePackage;
+ FString m_lastConnectionId;
+#if PLATFORM_ANDROID && USE_ANDROID_JNI
+ jclass m_javaJniClientClass = nullptr;
+ jobject m_javaJniClientInstance = nullptr;
+#endif
+};
diff --git a/templates/module/Source/modulejni/Public/Generated/jni/jniconnectionstatus.h.tpl b/templates/module/Source/modulejni/Public/Generated/jni/jniconnectionstatus.h.tpl
new file mode 100644
index 000000000..f39e5aaa0
--- /dev/null
+++ b/templates/module/Source/modulejni/Public/Generated/jni/jniconnectionstatus.h.tpl
@@ -0,0 +1,21 @@
+#pragma once
+{{- $ModuleName := Camel .Module.Name}}
+{{- $API_MACRO := printf "%sJNI_API" (CAMEL .Module.Name) }}
+#include "CoreMinimal.h"
+#include "Delegates/DelegateCombinations.h"
+#include "{{$ModuleName}}JniConnectionStatus.generated.h"
+
+
+/**
+ * @brief Used when the interface client changes its subscription status:
+ * either is connected with the service side (true) or connection couldn't be established or ended (false).
+ */
+DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(F{{$ModuleName}}JniConnectionStatusChangedDelegateBP, bool, IsConnected);
+DECLARE_MULTICAST_DELEGATE_OneParam(F{{$ModuleName}}JniConnectionStatusChangedDelegate, bool /* IsSubscribed */);
+
+// Dummy class required for getting the .generated.h file without which the delegates are properly compiled.
+UCLASS()
+class {{ $API_MACRO }} U{{$ModuleName}}DelegatesDummy : public UObject
+{
+ GENERATED_BODY()
+};
\ No newline at end of file
diff --git a/templates/module/Source/modulejni/Public/modulejni.h.tpl b/templates/module/Source/modulejni/Public/modulejni.h.tpl
new file mode 100644
index 000000000..d72cb563a
--- /dev/null
+++ b/templates/module/Source/modulejni/Public/modulejni.h.tpl
@@ -0,0 +1,35 @@
+{{- /* Copyright Epic Games, Inc. All Rights Reserved */ -}}
+{{- $ModuleName := Camel .Module.Name}}
+{{- $Category := printf "ApiGear%s" $ModuleName -}}
+/**
+Copyright 2021 ApiGear UG
+Copyright 2021 Epic Games, Inc.
+
+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.
+*/
+{{- $class := printf "F%sJniModule" $ModuleName}}
+#pragma once
+
+#include "Modules/ModuleManager.h"
+
+class {{$class}} : public IModuleInterface
+{
+public:
+ /** IModuleInterface implementation */
+ virtual void StartupModule() override;
+ virtual void ShutdownModule() override;
+
+private:
+ /** Handle to the test dll we will load */
+ void* {{$class}};
+};
diff --git a/templates/module/Source/modulejni/android/buildsystem/config.gradle b/templates/module/Source/modulejni/android/buildsystem/config.gradle
new file mode 100644
index 000000000..8101a8c22
--- /dev/null
+++ b/templates/module/Source/modulejni/android/buildsystem/config.gradle
@@ -0,0 +1,51 @@
+ext {
+ proAAPTConfigs = System.getenv("GRADLE_PRODUCT_AAPT_CONFIG")
+ androidHome = System.getenv("ANDROID_HOME")
+ targetBuildConfigs = System.getenv("TARGET_BUILD_VARIANT")
+ repoRoot = rootProject.projectDir.path + "../"
+ UnrealEngineAARVersion = '1.0.0'
+
+ config = [
+ versionCode : 1,
+ versionName : "1.0",
+ javaJvmVersion : JavaVersion.VERSION_1_8,
+ sdkVersion : 33,
+// toolsVersion : '29.0.3',
+ minSdkVersion : 31,
+ ndkVersion : '25.1.8937393',
+ localLibsDir : repoRoot + "SharedWithPartners/MakeAAR/aars",
+ AAR_VERSION : '"05.0.0"',
+ obbCommandLine : '"-nosound"',
+ obbModuleName : '""',
+ obbFileLocation : '""'
+ ]
+
+
+ if (file("../buildsystem/UnrealAARVersion.xml").exists()) {
+ println "UnrealAARVersion.xml found"
+ def unrealVersion = (new XmlParser()).parse('buildsystem/UnrealAARVersion.xml')
+ unrealVersion.each { values ->
+ project.ext.set("UnrealEngineAARVersion", values.value().toString().replace('[', '').replace(']', ''))
+ project.config.replace("AAR_VERSION", '"' + project.ext.get("UnrealEngineAARVersion") + '"')
+ }
+ }
+ else {
+ println "UnrealAARVersion.xml NOT found"
+ }
+
+ println "UnrealEngineAARVersion = ${UnrealEngineAARVersion}"
+ println "config.AAR_VERSION = ${config.AAR_VERSION}"
+}
+
+gradle.projectsEvaluated {
+ tasks.withType(JavaCompile) {
+ if ("${project.proAAPTConfigs}" != "null") {
+ def framework_jar_file = "${config.localLibsDir}" + "/framework_intermediates/classes.jar"
+ options.compilerArgs.add("-Xbootclasspath/p:${framework_jar_file}")
+ } else {
+ def framework_jar_file = "${project.androidHome}" + "/platforms/android-29/framework.jar"
+
+ options.compilerArgs.add("-Xbootclasspath/p:${framework_jar_file}")
+ }
+ }
+}
\ No newline at end of file
diff --git a/templates/module/Source/modulejni/android/buildsystem/dependencies.gradle b/templates/module/Source/modulejni/android/buildsystem/dependencies.gradle
new file mode 100644
index 000000000..f7c5313b5
--- /dev/null
+++ b/templates/module/Source/modulejni/android/buildsystem/dependencies.gradle
@@ -0,0 +1,35 @@
+ext {
+
+ androidXVersion = "23.1.1"
+ androidXTestVersion = "1.2.0"
+ androidXArchTestVersion = "2.1.0"
+ androidXLifecycleExtVersion = "2.1.0"
+ androidMaterialVersion = "1.1.0"
+ rxjavaVersion = "2.2.13"
+ rxandroidVersion = "2.1.1"
+ kotlinversion = "1.6.10"
+ kotlinPluginVersion = "1.4.0"
+ androidPluginVersion = "3.6.1"
+ javaJUnitVersion = "4.12"
+ espressoVersion = "3.2.0"
+ constraintLayoutVersion = "1.1.3"
+ lottieVersion = "5.2.0"
+
+ lib = [
+ //plugins
+ kotlin_plugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinPluginVersion}",
+ android_plugin : "com.android.tools.build:gradle:${androidPluginVersion}",
+
+ //java/kotlin
+ rxjava : "io.reactivex.rxjava2:rxjava:${rxjavaVersion}",
+ kotlin_stdlib : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinversion}",
+ kotlin_reflect : "org.jetbrains.kotlin:kotlin-reflect:${kotlinversion}",
+
+ //android
+ rxandroid : "io.reactivex.rxjava2:rxandroid:${rxandroidVersion}",
+ android_material : "com.google.android.material:material:${androidMaterialVersion}",
+ androidx_lifecycle : "androidx.lifecycle:lifecycle-extensions:${androidXLifecycleExtVersion}",
+ constraint_layout : "androidx.constraintlayout:constraintlayout:${constraintLayoutVersion}",
+ ]
+
+}
\ No newline at end of file
diff --git a/templates/module/Source/modulejni/android/buildsystem/ue4shared.keystore b/templates/module/Source/modulejni/android/buildsystem/ue4shared.keystore
new file mode 100644
index 000000000..140b62011
Binary files /dev/null and b/templates/module/Source/modulejni/android/buildsystem/ue4shared.keystore differ
diff --git a/templates/module/Source/modulejni/jni_UPL.xml.tpl b/templates/module/Source/modulejni/jni_UPL.xml.tpl
new file mode 100644
index 000000000..44e49dc10
--- /dev/null
+++ b/templates/module/Source/modulejni/jni_UPL.xml.tpl
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{- $moduleName := .Module.Name }}
+ {{- $service_package_name := printf "%s_android_service" (camel $moduleName) }}
+
+
+
+
+
+
+
+ {{- range .Module.Interfaces -}}
+
+
+ {{- end }}
+
+
+
+
+
+
+ {{- $service_package_name := printf "%s_android_service" (camel $moduleName) }}
+ {{- $client_package_name := printf "%s_android_client" (camel $moduleName) }}
+ {{- $impl_package_name := printf "%s_impl" (camel $moduleName) }}
+ {{- $messenger_package_name := printf "%s_android_messenger" (camel $moduleName) }}
+ {{- $api_package_name := printf "%s_api" (camel $moduleName) }}
+
+
+
+
+
+ {{- $jniservice_name:= printf "%sjniservice" (camel $moduleName ) }}
+
+ {{- $jniclient_name:= printf "%sjniclient" (camel $moduleName ) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ if (task.name == 'assembleDebug') {
+ task.dependsOn ':{{camel $moduleName}}_api:assembleDebug'
+ task.dependsOn ':{{camel $moduleName}}_impl:assembleDebug'
+ task.dependsOn ':{{camel $moduleName}}_android_service:assembleDebug'
+ task.dependsOn ':{{camel $moduleName}}_android_client:assembleDebug'
+ task.dependsOn ':{{camel $moduleName}}_android_messenger:assembleDebug'
+ }
+ if (task.name == 'assembleRelease') {
+ task.dependsOn ':{{camel $moduleName}}_api:assembleRelease'
+ task.dependsOn ':{{camel $moduleName}}_impl:assembleRelease'
+ task.dependsOn ':{{camel $moduleName}}_android_service:assembleRelease'
+ task.dependsOn ':{{camel $moduleName}}_android_client:assembleRelease'
+ task.dependsOn ':{{camel $moduleName}}_android_messenger:assembleRelease'
+ }
+}
+
+]]>
+
+
+
+
+
+
+ {{- range .Module.Interfaces -}}
+ {{- $jniservice_name:= printf "%sjniservice" (camel $moduleName) }}
+ -keep class {{camel $moduleName}}.{{$jniservice_name}}.{{Camel .Name}}JniService {
+ public *;
+ }
+ -keep class {{camel $moduleName}}.{{$jniservice_name}}.{{Camel .Name}}JniServiceStarter {
+ public *;
+ }
+ {{- $jniclient_name:= printf "%sjniclient" (camel $moduleName) }}
+ -keep class {{camel $moduleName}}.{{$jniclient_name}}.{{Camel .Name}}JniClient {
+ public *;
+ }
+ {{- end }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/module/Source/modulejni/modulejni.Build.cs.tpl b/templates/module/Source/modulejni/modulejni.Build.cs.tpl
new file mode 100644
index 000000000..4e48402ce
--- /dev/null
+++ b/templates/module/Source/modulejni/modulejni.Build.cs.tpl
@@ -0,0 +1,88 @@
+{{- /* Copyright Epic Games, Inc. All Rights Reserved */ -}}
+{{- $ModuleName := Camel .Module.Name}}
+{{- $ModulePath := path .Module.Name}}
+{{- $Category := printf "ApiGear%s" $ModuleName -}}
+using UnrealBuildTool;
+using System.IO;
+
+public class {{$ModuleName}}Jni : ModuleRules
+{
+ public {{$ModuleName}}Jni(ReadOnlyTargetRules Target) : base(Target)
+ {
+ PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
+ #if UE_5_2_OR_LATER
+ IWYUSupport = IWYUSupport.Full;
+ #else
+ bEnforceIWYU = true;
+ #endif
+
+ // Disable nlohmann::json exception handling
+ PublicDefinitions.Add("JSON_NOEXCEPTION=1");
+
+ PublicIncludePaths.AddRange(
+ new string[] {
+ // ... add public include paths required here ...
+ }
+ );
+
+
+ PrivateIncludePaths.AddRange(
+ new string[] {
+ // ... add other private include paths required here ...
+ }
+ );
+
+
+ PublicDependencyModuleNames.AddRange(
+ new string[]
+ {
+{{- if .Features.api }}
+ "{{Camel .Module.Name}}API",
+{{- end }}
+ "{{Camel .Module.Name}}Core",
+ "Projects",
+ "Engine",
+ "JsonUtilities"
+{{- if .Module.Imports }}, {{- end}}
+{{- range $idx, $elem := .Module.Imports }}
+{{- if $idx}}, {{ end }}
+ "{{Camel .Name}}Core"
+{{- end }}
+ }
+ );
+
+
+ PrivateDependencyModuleNames.AddRange(
+ new string[]
+ {
+ "Core",
+ "CoreUObject",
+ "Engine",
+{{- if .Features.stubs }}
+ "{{Camel .Module.Name}}Core",
+ "{{Camel .Module.Name}}Implementation",
+{{- end }}
+{{- range $idx, $elem := .Module.Imports }}
+ "{{Camel .Name}}API",
+{{- end }}
+ }
+ );
+
+ if (Target.Platform == UnrealTargetPlatform.Android)
+ {
+ PrivateDefinitions.Add("WITH_JNI=1");
+ string PluginPath = Utils.MakePathRelativeTo(ModuleDirectory, Target.RelativeEnginePath);
+ AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(PluginPath, "{{Camel .Module.Name}}_JNI_UPL.xml"));
+
+ PrivateDependencyModuleNames.AddRange(new string[] { "Launch" });
+ }
+
+
+ DynamicallyLoadedModuleNames.AddRange(
+ new string[]
+ {
+ // ... add any modules that your module loads dynamically here ...
+ }
+ );
+ }
+}
diff --git a/templates/module/Source/pluginname.uplugin.tpl b/templates/module/Source/pluginname.uplugin.tpl
index 3c33fbf56..47be26f92 100644
--- a/templates/module/Source/pluginname.uplugin.tpl
+++ b/templates/module/Source/pluginname.uplugin.tpl
@@ -59,6 +59,13 @@
"Type": "Runtime",
"LoadingPhase": "default"
},
+{{- end }}
+{{- if and .Features.jni (len .Module.Interfaces) }}
+ {
+ "Name": "{{$ModuleName}}Jni",
+ "Type": "Runtime",
+ "LoadingPhase": "default"
+ },
{{- end }}
{
"Name" : "{{$ModuleName}}Editor",