diff --git a/.github/workflows/j3o-scan.yml b/.github/workflows/j3o-scan.yml index 33ccbc1314..bcb1b57a71 100644 --- a/.github/workflows/j3o-scan.yml +++ b/.github/workflows/j3o-scan.yml @@ -34,7 +34,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: temurin - java-version: '17' + java-version: '25' - name: Scan J3O assets run: ./gradlew :jme3-desktop:scanJ3O --console=plain diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d93461de56..0da6c76710 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,7 +69,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '21' + java-version: '25' - name: Validate the Gradle wrapper uses: gradle/actions/wrapper-validation@v6.1.0 - name: Run Checkstyle @@ -96,7 +96,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '21' + java-version: '25' - name: Validate the Gradle wrapper uses: gradle/actions/wrapper-validation@v6.1.0 - name: Run SpotBugs @@ -120,6 +120,11 @@ jobs: contents: read steps: - uses: actions/checkout@v6 + - name: Setup the java environment + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '25' - name: Start xvfb run: | Xvfb :99 -ac -screen 0 1024x768x16 & @@ -150,45 +155,6 @@ jobs: **/build/changed-images/** **/build/test-results/** - # Build iOS natives - BuildIosNatives: - name: Build natives for iOS - runs-on: macOS-14 - - steps: - - name: Check default JAVAs - run: echo $JAVA_HOME --- $JAVA_HOME_8_X64 --- $JAVA_HOME_11_X64 --- $JAVA_HOME_17_X64 --- $JAVA_HOME_21_X64 --- - - - name: Setup the java environment - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '17' - - - name: Setup the XCode version to 15.1.0 - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 - with: - xcode-version: '15.1.0' - - - name: Clone the repo - uses: actions/checkout@v6 - with: - fetch-depth: 1 - - - name: Validate the Gradle wrapper - uses: gradle/actions/wrapper-validation@v6.1.0 - - - name: Build - run: | - ./gradlew -PuseCommitHashAsVersionName=true --no-daemon -PbuildNativeProjects=true \ - :jme3-ios-native:build - - - name: Upload natives - uses: actions/upload-artifact@v7.0.1 - with: - name: ios-natives - path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework - # Build the natives on android BuildAndroidNatives: name: Build natives for android @@ -202,11 +168,11 @@ jobs: with: fetch-depth: 1 - - name: Setup Java 17 + - name: Setup Java 25 uses: actions/setup-java@v5 with: distribution: temurin - java-version: '17' + java-version: '25' - name: Check java version run: java -version @@ -234,7 +200,7 @@ jobs: # Build the engine, we only deploy from ubuntu-latest jdk25 BuildJMonkey: - needs: [BuildAndroidNatives, BuildIosNatives] + needs: [BuildAndroidNatives] name: Build on ${{ matrix.osName }} jdk${{ matrix.jdk }} runs-on: ${{ matrix.os }} strategy: @@ -259,18 +225,18 @@ jobs: with: fetch-depth: 1 + - name: Setup the java environment + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: ${{ matrix.jdk }} + - name: Download natives for android uses: actions/download-artifact@v8.0.1 with: name: android-natives path: build/native - - name: Download natives for iOS - uses: actions/download-artifact@v8.0.1 - with: - name: ios-natives - path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework - - name: Validate the Gradle wrapper uses: gradle/actions/wrapper-validation@v6.1.0 - name: Build Engine @@ -453,12 +419,12 @@ jobs: with: fetch-depth: 1 - # Setup jdk 21 used for building Maven-style artifacts + # Setup jdk 25 used for building Maven-style artifacts - name: Setup the java environment uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: distribution: 'temurin' - java-version: '21' + java-version: '25' - name: Download natives for android uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -466,12 +432,6 @@ jobs: name: android-natives path: build/native - - name: Download natives for iOS - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ios-natives - path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework - - name: Rebuild the maven artifacts and upload them to Sonatype's maven-snapshots repo run: | if [ "${{ secrets.CENTRAL_PASSWORD }}" = "" ]; @@ -502,12 +462,12 @@ jobs: with: fetch-depth: 1 - # Setup jdk 21 used for building Sonatype artifacts + # Setup jdk 25 used for building Sonatype artifacts - name: Setup the java environment uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '21' + java-version: '25' # Download all the stuff... - name: Download maven artifacts @@ -528,12 +488,6 @@ jobs: name: android-natives path: build/native - - name: Download natives for iOS - uses: actions/download-artifact@v8.0.1 - with: - name: ios-natives - path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework - - name: Rebuild the maven artifacts and upload them to Sonatype's Central Publisher Portal run: | if [ "${{ secrets.CENTRAL_PASSWORD }}" = "" ]; diff --git a/build.gradle b/build.gradle index 3a1395207c..ebe2df96eb 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,15 @@ apply from: file('version.gradle') allprojects { repositories { + mavenLocal() mavenCentral() + maven { + name = 'CentralSnapshots' + url = uri('https://central.sonatype.com/repository/maven-snapshots/') + mavenContent { + snapshotsOnly() + } + } google() } tasks.withType(Jar) { diff --git a/jme3-core/src/main/java/com/jme3/renderer/Caps.java b/jme3-core/src/main/java/com/jme3/renderer/Caps.java index dfe8a10669..1183002a65 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Caps.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Caps.java @@ -427,6 +427,11 @@ public enum Caps { */ DepthTexture, + /** + * Supports hardware depth texture comparison for shadow maps. + */ + TextureShadowCompare, + /** * Supports 32-bit index buffers. */ diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index e8410ce429..91ad5596e4 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -453,6 +453,10 @@ private void loadCapabilitiesCommon() { caps.add(Caps.DepthTexture); } + if (gl2 != null || caps.contains(Caps.OpenGLES30)) { + caps.add(Caps.TextureShadowCompare); + } + if (caps.contains(Caps.OpenGL20) || caps.contains(Caps.OpenGLES30) || caps.contains(Caps.WebGL) || hasExtension("GL_OES_depth24")) { caps.add(Caps.Depth24); @@ -2674,7 +2678,7 @@ && isMipmapGenerationSupported(image.getFormat(), } ShadowCompareMode texCompareMode = tex.getShadowCompareMode(); - if ( (gl2 != null || caps.contains(Caps.OpenGLES30)) && curState.shadowCompareMode != texCompareMode) { + if (caps.contains(Caps.TextureShadowCompare) && curState.shadowCompareMode != texCompareMode) { bindTextureAndUnit(target, image, unit); if (texCompareMode != ShadowCompareMode.Off) { gl.glTexParameteri(target, GL2.GL_TEXTURE_COMPARE_MODE, GL2.GL_COMPARE_REF_TO_TEXTURE); diff --git a/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java b/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java index 62d4c6c5de..7f0a81d94a 100644 --- a/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/util/MaterialDebugAppState.java @@ -377,16 +377,23 @@ public void init() { file = new File(url.getFile()); fileLastM = file.lastModified(); - } catch (NoSuchFieldException - | SecurityException + } catch (NoSuchFieldException ex) { + Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.FINE, + "Material hot reload disabled for {0}; asset URL is not reflectively available.", + fileName); + } catch (SecurityException | IllegalArgumentException | IllegalAccessException ex) { - Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, null, ex); + Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.FINE, + "Material hot reload disabled for " + fileName, ex); } } } public boolean shouldFire() { + if (file == null || fileLastM == null) { + return false; + } if (file.lastModified() != fileLastM) { fileLastM = file.lastModified(); return true; diff --git a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java index 2447800759..0e1e2af20a 100644 --- a/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java +++ b/jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java @@ -105,7 +105,6 @@ public void simpleInitApp() { updateMaterial(); - } @@ -153,4 +152,5 @@ private void updateMaterial() { "Tank material -> metallic: " + metallic + ", roughness: " + roughness + " (N/P, F toggles SH fast path)"); } + } diff --git a/jme3-ios-examples/build.gradle b/jme3-ios-examples/build.gradle new file mode 100644 index 0000000000..9977512407 --- /dev/null +++ b/jme3-ios-examples/build.gradle @@ -0,0 +1,391 @@ +buildscript { + repositories { + def libJGLIOSPluginRepositories = [] + if ((findProperty('libJGLIOSLocal') ?: 'false').toString().toBoolean()) { + libJGLIOSPluginRepositories << mavenLocal() + } + libJGLIOSPluginRepositories << maven { + name = 'CentralSnapshots' + url = uri('https://central.sonatype.com/repository/maven-snapshots/') + mavenContent { + snapshotsOnly() + } + } + exclusiveContent { + forRepositories(*libJGLIOSPluginRepositories) + filter { + includeModule('org.ngengine', 'libjglios-gradle-plugin') + } + } + gradlePluginPortal() + mavenCentral() + } + dependencies { + classpath 'org.ngengine:libjglios-gradle-plugin:0.1.0-SNAPSHOT' + } +} + +import groovy.json.JsonOutput +import java.io.DataInputStream +import java.util.zip.ZipFile + +apply plugin: 'org.ngengine.libjglios' + +description = 'iOS libJGLIOS launcher for jme3-examples.' + +repositories { + def libJGLIOSRepositories = [] + if ((findProperty('libJGLIOSLocal') ?: 'false').toString().toBoolean()) { + libJGLIOSRepositories << mavenLocal() + } + libJGLIOSRepositories << maven { + name = 'CentralSnapshots' + url = uri('https://central.sonatype.com/repository/maven-snapshots/') + mavenContent { + snapshotsOnly() + } + } + exclusiveContent { + forRepositories(*libJGLIOSRepositories) + filter { + includeModule('org.ngengine', 'libjglios-core-ios') + includeModule('org.ngengine', 'libjglios-gles-ios') + includeModule('org.ngengine', 'libjglios-sdl3-ios') + includeModule('org.ngengine', 'libjglios-openal-ios') + includeModule('org.ngengine', 'libjglios-angle-ios') + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +def examplesJar = project(':jme3-examples').tasks.named('jar') +def generateExamplesTestChooserClassList = project(':jme3-examples').tasks.named('generateTestChooserClassList') +def generatedExamplesTestChooserResourcesDir = project(':jme3-examples').layout.buildDirectory.dir('generated/testchooser/resources') +def examplesTestClassListFile = generatedExamplesTestChooserResourcesDir.map { it.file('jme3test/test-classes.txt') } +def iosChooserLauncherClass = 'jme3test.ios.IosTestChooserLauncher' +def requestedExampleClass = findProperty('example')?.toString()?.trim() +def exampleClass = requestedExampleClass ?: iosChooserLauncherClass +def iosExampleClassesDir = layout.buildDirectory.dir('ios-example-classes') +def iosInitialExampleSourceDir = layout.buildDirectory.dir('generated/ios-initial-example/sources') +def iosNativeImageMetadataDir = layout.buildDirectory.dir('generated/ios-native-image-metadata/resources') +def iosChooserExcludedPrefixes = [ + 'jme3test.awt.', + 'jme3test.bullet.', + 'jme3test.niftygui.', + 'jme3test.opencl.', + 'jme3test.terrain.' +] +def iosChooserExcludedNames = [ + 'jme3test.app.TestChangeAppIcon', + 'jme3test.app.TestContextRestart', + 'jme3test.app.TestMonitorApp', + 'jme3test.app.TestResizableApp', + 'jme3test.asset.TestOnlineJar', + 'jme3test.audio.TestAudioDeviceDisconnect' +] +def isIosChooserClass = { String className -> + !iosChooserExcludedPrefixes.any { className.startsWith(it) } + && !iosChooserExcludedNames.contains(className) + && !className.contains('Jogl') + && !className.contains('Lwjgl') +} +def readExamplesTestClassList = { + def file = examplesTestClassListFile.get().asFile + if (!file.exists()) { + return [] + } + file.readLines('UTF-8') + .collect { it.trim() } + .findAll { it } + .findAll { isIosChooserClass(it) } +} +def selectedIosExampleClasses = { + requestedExampleClass ? [requestedExampleClass] : readExamplesTestClassList() +} +def javaStringLiteral = { String value -> + if (value == null) { + return 'null' + } + '"' + value + .replace('\\', '\\\\') + .replace('"', '\\"') + .replace('\n', '\\n') + .replace('\r', '\\r') + '"' +} +def readClassReferences = { ZipFile zip, String classPath -> + def entry = zip.getEntry("${classPath}.class") + if (entry == null) { + return [] as Set + } + zip.getInputStream(entry).withCloseable { input -> + def data = new DataInputStream(input) + if (data.readInt() != (int) 0xCAFEBABE) { + return [] as Set + } + data.readUnsignedShort() + data.readUnsignedShort() + def constantPool = new Object[data.readUnsignedShort()] + for (int index = 1; index < constantPool.length; index++) { + int tag = data.readUnsignedByte() + switch (tag) { + case 1: + constantPool[index] = data.readUTF() + break + case 3: + case 4: + data.skipBytes(4) + break + case 5: + case 6: + data.skipBytes(8) + index++ + break + case 7: + case 8: + case 16: + case 19: + case 20: + constantPool[index] = [tag: tag, nameIndex: data.readUnsignedShort()] + break + case 9: + case 10: + case 11: + case 12: + case 18: + data.skipBytes(4) + break + case 15: + data.skipBytes(3) + break + case 17: + data.skipBytes(4) + break + default: + throw new GradleException("Unsupported constant-pool tag ${tag} in ${classPath}.class") + } + } + constantPool.findAll { it instanceof Map && it.tag == 7 } + .collect { constantPool[it.nameIndex] } + .findAll { it instanceof String && it.startsWith('jme3test/') && !it.startsWith('jme3test/ios/') } + .collect { it.replace('/', '.') } + .findAll { !it.contains('[') } as Set + } +} +def expandExamplesClassNames = { Collection rootClasses -> + def expanded = new LinkedHashSet(rootClasses) + def jarFile = examplesJar.flatMap { it.archiveFile }.get().asFile + new ZipFile(jarFile).withCloseable { zip -> + def availablePaths = new LinkedHashSet() + zip.entries().each { entry -> + if (entry.directory || !entry.name.endsWith('.class')) { + return + } + def classPath = entry.name.substring(0, entry.name.length() - '.class'.length()) + if (classPath.startsWith('jme3test/')) { + availablePaths.add(classPath) + } + } + + def pending = new ArrayDeque(expanded) + while (!pending.isEmpty()) { + def className = pending.removeFirst() + def rootPath = className.replace('.', '/') + availablePaths.findAll { it.startsWith("${rootPath}\$") }.each { classPath -> + def nestedClass = classPath.replace('/', '.') + if (expanded.add(nestedClass)) { + pending.add(nestedClass) + } + } + readClassReferences(zip, rootPath).each { referencedClass -> + def referencedPath = referencedClass.replace('.', '/') + if (availablePaths.contains(referencedPath) && expanded.add(referencedClass)) { + pending.add(referencedClass) + } + } + } + } + expanded as List +} +def collectRuntimeClassNamesByPrefix = { Collection prefixes -> + def classes = new LinkedHashSet() + sourceSets.main.compileClasspath.files.findAll { it.exists() }.each { file -> + if (file.isDirectory()) { + file.eachFileRecurse { classFile -> + if (!classFile.name.endsWith('.class')) { + return + } + def relativePath = file.toPath().relativize(classFile.toPath()).toString().replace(File.separatorChar, (char) '/') + def className = relativePath.substring(0, relativePath.length() - '.class'.length()).replace('/', '.') + if (prefixes.any { className.startsWith(it) } && !className.endsWith('module-info') && !className.endsWith('package-info')) { + classes.add(className) + } + } + } else if (file.name.endsWith('.jar')) { + new ZipFile(file).withCloseable { zip -> + zip.entries().each { entry -> + if (entry.directory || !entry.name.endsWith('.class')) { + return + } + def className = entry.name.substring(0, entry.name.length() - '.class'.length()).replace('/', '.') + if (prefixes.any { className.startsWith(it) } && !className.endsWith('module-info') && !className.endsWith('package-info')) { + classes.add(className) + } + } + } + } + } + classes as List +} + +def prepareIosExampleClasses = tasks.register('prepareIosExampleClasses', Sync) { + dependsOn examplesJar, generateExamplesTestChooserClassList + inputs.property('exampleClass', exampleClass) + inputs.property('requestedExampleClass', requestedExampleClass ?: '') + inputs.property('iosExampleClassExpansionVersion', 'bytecode-reference-v1') + inputs.property('iosExampleIncludes', findProperty('iosExampleIncludes')?.toString() ?: '') + inputs.file(examplesTestClassListFile) + inputs.file(examplesJar.flatMap { it.archiveFile }) + from(zipTree(examplesJar.flatMap { it.archiveFile })) { + def expandedClasses + include { details -> + expandedClasses = expandedClasses ?: expandExamplesClassNames(selectedIosExampleClasses()) + expandedClasses.any { className -> details.path == "${className.replace('.', '/')}.class" } + } + def extraIncludes = findProperty('iosExampleIncludes')?.toString() + if (extraIncludes) { + extraIncludes.split(',').collect { it.trim() }.findAll { it }.each { include it } + } + } + into iosExampleClassesDir +} + +def generateIosInitialExampleSource = tasks.register('generateIosInitialExampleSource') { + def outputFile = iosInitialExampleSourceDir.map { it.file('jme3test/ios/IosInitialExample.java') } + outputs.file(outputFile) + inputs.property('requestedExampleClass', requestedExampleClass ?: '') + doLast { + def file = outputFile.get().asFile + file.parentFile.mkdirs() + file.text = """package jme3test.ios; + +final class IosInitialExample { + private static final String CLASS_NAME = ${javaStringLiteral(requestedExampleClass)}; + + private IosInitialExample() { + } + + static String className() { + return CLASS_NAME; + } +} +""" + } +} + +def generateIosNativeImageMetadata = tasks.register('generateIosNativeImageMetadata') { + dependsOn examplesJar, generateExamplesTestChooserClassList, tasks.named('prepareGraalHostNik') + def reflectConfig = iosNativeImageMetadataDir.map { + it.file('META-INF/native-image/org.jmonkeyengine/jme3-ios-testchooser/reflect-config.json') + } + outputs.file(reflectConfig) + inputs.property('exampleClass', exampleClass) + inputs.property('requestedExampleClass', requestedExampleClass ?: '') + inputs.property('iosExampleClassExpansionVersion', 'bytecode-reference-v1') + inputs.file(examplesTestClassListFile) + inputs.file(examplesJar.flatMap { it.archiveFile }) + inputs.files({ sourceSets.main.compileClasspath }) + doLast { + def exampleClasses = selectedIosExampleClasses() + def classes = (expandExamplesClassNames(exampleClasses) + + collectRuntimeClassNamesByPrefix(['com.bulletphysics.']) + + [iosChooserLauncherClass, 'jme3test.ios.IosTestChooser', 'jme3test.ios.IosInitialExample']).unique() + def metadata = classes.collect { className -> + [ + name: className, + allDeclaredConstructors: true, + allPublicConstructors: true, + allPublicMethods: true + ] + } + def outputFile = reflectConfig.get().asFile + outputFile.parentFile.mkdirs() + outputFile.text = JsonOutput.prettyPrint(JsonOutput.toJson(metadata)) + System.lineSeparator() + } +} + +libJGLIOS { + mainClass = iosChooserLauncherClass + bundleId = 'org.jmonkeyengine.jme3iosexamples' + appName = 'JmeIosExamples' + simulatorDevice = (findProperty('iosSimulatorDevice') ?: 'iPhone 16').toString() +} + +sourceSets { + main { + java.srcDir iosInitialExampleSourceDir + output.dir(iosExampleClassesDir, builtBy: prepareIosExampleClasses) + resources { + srcDir iosNativeImageMetadataDir + srcDir generatedExamplesTestChooserResourcesDir + srcDir '../jme3-testdata/src/main/resources' + srcDir '../jme3-examples/src/main/resources' + } + } +} + +dependencies { + implementation project(':jme3-core') + implementation project(':jme3-ios') + implementation 'org.ngengine:libjglios-core-ios:0.1.0-SNAPSHOT' + implementation 'org.ngengine:libjglios-gles-ios:0.1.0-SNAPSHOT' + implementation 'org.ngengine:libjglios-sdl3-ios:0.1.0-SNAPSHOT' + implementation 'org.ngengine:libjglios-openal-ios:0.1.0-SNAPSHOT' + implementation 'org.ngengine:libjglios-angle-ios:0.1.0-SNAPSHOT' + implementation project(':jme3-effects') + implementation project(':jme3-jbullet') + implementation project(':jme3-jogg') + implementation project(':jme3-networking') + implementation project(':jme3-plugins') + implementation project(':jme3-plugins-json') + implementation project(':jme3-plugins-json-gson') + + if ((findProperty('iosIncludeAwtUnsafeModules') ?: 'false').toString().toBoolean()) { + implementation project(':jme3-niftygui') + implementation project(':jme3-terrain') + } +} + +tasks.named('compileJava') { + dependsOn prepareIosExampleClasses, generateIosInitialExampleSource + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + options.release = 11 +} + +tasks.named('processResources') { + dependsOn generateIosNativeImageMetadata, generateExamplesTestChooserClassList +} + +tasks.register('runIosExamples') { + group = 'verification' + description = 'Builds, installs, and launches the jme3-examples iOS app.' + dependsOn tasks.named('runIosApp') +} + +tasks.register('printIosExamplesRunHelp') { + group = 'help' + description = 'Prints how to run jme3-examples on iOS through libJGLIOS.' + doLast { + println "Run :jme3-ios-examples:runIosExamples for the iOS test chooser." + println "Run :jme3-ios-examples:runIosExamples -Pexample=jme3test.helloworld.HelloJME3 to open one example automatically through the chooser launcher." + println "runIosExamples uses an available connected device automatically, otherwise it opens the simulator." + println "Use -PiosAppTarget=simulator to force simulator, or -PiosDevice='' to force a device." + println "For device signing, add -PiosSigningIdentity, -PiosProvisioningProfile, and optionally -PiosCodesignEntitlements." + println "For examples with helper classes, add -PiosExampleIncludes='jme3test/path/**'." + println "Terrain and niftygui are excluded by default because they pull java.awt into native-image; opt in with -PiosIncludeAwtUnsafeModules=true." + } +} diff --git a/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java b/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java new file mode 100644 index 0000000000..2e6acd5ad5 --- /dev/null +++ b/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java @@ -0,0 +1,428 @@ +package jme3test.ios; + +import com.jme3.app.SimpleApplication; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.MouseInput; +import com.jme3.input.RawInputListenerAdapter; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.input.event.KeyInputEvent; +import com.jme3.input.event.TouchEvent; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.Quad; +import com.jme3.system.JmeSystem; +import java.util.ArrayList; +import java.util.List; + +public final class IosTestChooser extends SimpleApplication implements ActionListener { + private static final String SELECT_MAPPING = "IosTestChooserSelect"; + private static final float MIN_TEXT_SIZE = 10f; + private static final long TAP_DEBOUNCE_NANOS = 180_000_000L; + private static final ColorRGBA BACKGROUND_COLOR = new ColorRGBA(0.045f, 0.060f, 0.050f, 1f); + private static final ColorRGBA EXAMPLE_BUTTON_COLOR = new ColorRGBA(0.135f, 0.305f, 0.145f, 1f); + private static final ColorRGBA ACTION_BUTTON_COLOR = new ColorRGBA(0.760f, 0.360f, 0.060f, 1f); + private static final ColorRGBA DISABLED_BUTTON_COLOR = new ColorRGBA(0.090f, 0.105f, 0.095f, 1f); + private static final ColorRGBA SEARCH_BUTTON_COLOR = new ColorRGBA(0.070f, 0.085f, 0.075f, 1f); + private static final ColorRGBA PRIMARY_TEXT_COLOR = new ColorRGBA(0.930f, 0.960f, 0.900f, 1f); + private static final ColorRGBA MUTED_TEXT_COLOR = new ColorRGBA(0.500f, 0.560f, 0.480f, 1f); + private static final ColorRGBA SEARCH_TEXT_COLOR = new ColorRGBA(0.765f, 0.910f, 0.555f, 1f); + + private final List