diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index dd818bf4a..2ba3b31ed 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -14,6 +14,8 @@ labels: bug [ ] Windows [ ] Linux [ ] Mac +[ ] Android +[ ] iOS ### Explain your issue ##### Please check first if your issue haven't already been reported yet. diff --git a/.github/ISSUE_TEMPLATE/compiling.md b/.github/ISSUE_TEMPLATE/compiling.md index 4f516469e..cc8d321a2 100644 --- a/.github/ISSUE_TEMPLATE/compiling.md +++ b/.github/ISSUE_TEMPLATE/compiling.md @@ -14,6 +14,8 @@ labels: compiling help [ ] Windows [ ] Linux [ ] Mac +[ ] Android +[ ] iOS ### Explain your issue ##### Please check first if your issue haven't already been reported yet, and make sure you ran the `update.bat` file before building. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..582928bb7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,111 @@ +name: Main +on: workflow_dispatch +jobs: + Desktop: + runs-on: ${{matrix.os}} + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-12, macos-14] + steps: + - name: Checkout + uses: actions/checkout@main + - name: Setup Haxe + uses: krdlab/setup-haxe@master + with: + haxe-version: 4.3.6 + - name: Install libVLC (Linux) + if: startsWith(matrix.os, 'ubuntu') + run: sudo apt install libvlc-dev libvlccore-dev + - name: Install Libraries + run: | + haxelib install hmm --quiet + haxelib run hmm install --quiet + - name: Compile (Windows) + if: startsWith(matrix.os, 'windows') + run: haxelib run lime build windows + - name: Compile (Linux) + if: startsWith(matrix.os, 'ubuntu') + run: haxelib run lime build linux + - name: Compile (macOS) + if: startsWith(matrix.os, 'macos') + run: haxelib run lime build mac + - name: Upload Artifact (Windows) + if: startsWith(matrix.os, 'windows') + uses: actions/upload-artifact@main + with: + name: windowsBuild + path: export\release\windows\bin\* + if-no-files-found: error + - name: Upload Artifact (Linux) + if: startsWith(matrix.os, 'ubuntu') + uses: actions/upload-artifact@main + with: + name: linuxBuild + path: export/release/linux/bin/* + if-no-files-found: error + - name: Upload Artifact (macOS 12) + if: matrix.os == 'macos-12' + uses: actions/upload-artifact@main + with: + name: macOSBuild-x86_64 + path: export/release/macos/bin/* + if-no-files-found: error + - name: Upload Artifact (macOS 14) + if: matrix.os == 'macos-14' + uses: actions/upload-artifact@main + with: + name: macOSBuild-arm64 + path: export/release/macos/bin/* + if-no-files-found: error + Mobile: + runs-on: ${{matrix.os}} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14] + steps: + - name: Checkout + uses: actions/checkout@main + - name: Setup Haxe + uses: krdlab/setup-haxe@master + with: + haxe-version: 4.3.6 + - name: Install Libraries + run: | + haxelib install hmm --quiet + haxelib run hmm install --quiet + - name: Configure Android + if: startsWith(matrix.os, 'ubuntu') + run: | + haxelib run lime config ANDROID_SDK $ANDROID_HOME + haxelib run lime config ANDROID_NDK_ROOT $ANDROID_NDK_LATEST_HOME + haxelib run lime config JAVA_HOME $JAVA_HOME_17_X64 + haxelib run lime config ANDROID_SETUP true + - name: Compile (Android) + if: startsWith(matrix.os, 'ubuntu') + run: haxelib run lime build android -ONLY_ARMV7 + - name: Compile (iOS) + if: startsWith(matrix.os, 'macos') + run: haxelib run lime build ios -nosign + - name: Make Ipa + if: startsWith(matrix.os, 'macos') + run: | + cd export/release/ios/build/*-iphoneos + mkdir Payload + mv *.app Payload + zip -r CodenameEngine.ipa Payload + - name: Upload Artifact (Android) + if: startsWith(matrix.os, 'ubuntu') + uses: actions/upload-artifact@main + with: + name: androidBuild + path: export/release/android/bin/app/build/outputs/apk/release/*.apk + if-no-files-found: error + - name: Upload Artifact (iOS) + if: startsWith(matrix.os, 'macos') + uses: actions/upload-artifact@main + with: + name: iOSBuild + path: export/release/ios/build/Release-iphoneos/*.ipa + if-no-files-found: error diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml new file mode 100644 index 000000000..da7fddff2 --- /dev/null +++ b/.github/workflows/mobile-release.yml @@ -0,0 +1,82 @@ +name: Mobile + Release +on: workflow_dispatch +jobs: + Mobile: + runs-on: ${{matrix.os}} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14] + steps: + - name: Checkout + uses: actions/checkout@main + - name: Setup Haxe + uses: krdlab/setup-haxe@master + with: + haxe-version: 4.3.6 + - name: Install Libraries + run: | + haxelib install hmm --quiet + haxelib run hmm install --quiet + - name: Configure Android + if: startsWith(matrix.os, 'ubuntu') + run: | + haxelib run lime config ANDROID_SDK $ANDROID_HOME + haxelib run lime config ANDROID_NDK_ROOT $ANDROID_NDK_LATEST_HOME + haxelib run lime config JAVA_HOME $JAVA_HOME_17_X64 + haxelib run lime config ANDROID_SETUP true + - name: Compile (Android) + if: startsWith(matrix.os, 'ubuntu') + run: haxelib run lime build android -final + - name: Compile (iOS) + if: startsWith(matrix.os, 'macos') + run: haxelib run lime build ios -final -nosign + - name: Make Ipa And Zip For Release + if: startsWith(matrix.os, 'macos') + run: | + cd export/release/ios/build/*-iphoneos + mkdir Payload + mv *.app Payload + zip -r CodenameEngine.ipa Payload + zip CodenameEngine-iOS.zip CodenameEngine.ipa + - name: Upload Artifact (Android) + if: startsWith(matrix.os, 'ubuntu') + uses: actions/upload-artifact@main + with: + name: androidBuild + path: export/release/android/bin/app/build/outputs/apk/release/*.apk + if-no-files-found: error + - name: Upload Artifact (iOS) + if: startsWith(matrix.os, 'macos') + uses: actions/upload-artifact@main + with: + name: iOSBuild + path: export/release/ios/build/Release-iphoneos/*.zip + if-no-files-found: error + Releaser: + needs: [Mobile] + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Download Android Build + uses: actions/download-artifact@main + with: + name: androidBuild + path: /home/runner + - name: Move Android File + run: mv /home/runner/CodenameEngine-release.apk /home/runner/CodenameEngine-Android.apk + - name: Download iOS Build + uses: actions/download-artifact@main + with: + name: iOSBuild + path: /home/runner + - name: Publish The Release + uses: marvinpinto/action-automatic-releases@latest + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + prerelease: true + automatic_release_tag: "dev-ca565de" + title: "DevBuild ca565de" + files: | + /home/runner/*.apk + /home/runner/*.zip diff --git a/README.md b/README.md index 604cd9c20..8ada852e0 100644 --- a/README.md +++ b/README.md @@ -87,3 +87,11 @@ In the future (when the engine won't be a WIP anymore) we're gonna also publish - Credits to Smokey555 for the backup Animate Atlas to spritesheet code - Credits to MAJigsaw77 for [hxvlc](https://github.com/MAJigsaw77/hxvlc) (video cutscene/mp4 support) and [hxdiscord_rpc](https://github.com/MAJigsaw77/hxdiscord_rpc) (discord rpc integration) + +
+

Mobile Credits

+ +- Credits to [Lily](ttps://youtube.com/@mcagabe19) to porting the engine +- Credits to [Karim Akra](https://youtube.com/@Karim0690) to helping me to port the engine +- Credits to [MAJigsaw77](https://github.com/MAJigsaw77) for mobile controls +
\ No newline at end of file diff --git a/assets/data/config/options.xml b/assets/data/config/options.xml index 888288d7a..d55f9e401 100644 --- a/assets/data/config/options.xml +++ b/assets/data/config/options.xml @@ -1,6 +1,7 @@ + @@ -11,5 +12,6 @@ + \ No newline at end of file diff --git a/assets/data/scripts/week6-pause.hx b/assets/data/scripts/week6-pause.hx index 6a4455cd8..7f49973d5 100644 --- a/assets/data/scripts/week6-pause.hx +++ b/assets/data/scripts/week6-pause.hx @@ -62,6 +62,9 @@ function create(event) { cameras = [pauseCam]; FlxG.sound.play(Paths.sound(isThorns ? 'pixel/ANGRY' : 'pixel/clickText')); + + addVirtualPad('UP_DOWN', 'A'); + addVirtualPadCamera(false); } function confText(text) { diff --git a/assets/images/mobile/menu/arrows.png b/assets/images/mobile/menu/arrows.png new file mode 100644 index 000000000..96158666b Binary files /dev/null and b/assets/images/mobile/menu/arrows.png differ diff --git a/assets/images/mobile/menu/arrows.xml b/assets/images/mobile/menu/arrows.xml new file mode 100644 index 000000000..df670d955 --- /dev/null +++ b/assets/images/mobile/menu/arrows.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/mobile/virtualpad/a.png b/assets/images/mobile/virtualpad/a.png new file mode 100644 index 000000000..7e0abf3be Binary files /dev/null and b/assets/images/mobile/virtualpad/a.png differ diff --git a/assets/images/mobile/virtualpad/b.png b/assets/images/mobile/virtualpad/b.png new file mode 100644 index 000000000..93cf690e6 Binary files /dev/null and b/assets/images/mobile/virtualpad/b.png differ diff --git a/assets/images/mobile/virtualpad/c.png b/assets/images/mobile/virtualpad/c.png new file mode 100644 index 000000000..ea5eb9900 Binary files /dev/null and b/assets/images/mobile/virtualpad/c.png differ diff --git a/assets/images/mobile/virtualpad/d.png b/assets/images/mobile/virtualpad/d.png new file mode 100644 index 000000000..75fedc606 Binary files /dev/null and b/assets/images/mobile/virtualpad/d.png differ diff --git a/assets/images/mobile/virtualpad/default.png b/assets/images/mobile/virtualpad/default.png new file mode 100644 index 000000000..5d3a11dcb Binary files /dev/null and b/assets/images/mobile/virtualpad/default.png differ diff --git a/assets/images/mobile/virtualpad/down.png b/assets/images/mobile/virtualpad/down.png new file mode 100644 index 000000000..cc802e53f Binary files /dev/null and b/assets/images/mobile/virtualpad/down.png differ diff --git a/assets/images/mobile/virtualpad/e.png b/assets/images/mobile/virtualpad/e.png new file mode 100644 index 000000000..004693125 Binary files /dev/null and b/assets/images/mobile/virtualpad/e.png differ diff --git a/assets/images/mobile/virtualpad/f.png b/assets/images/mobile/virtualpad/f.png new file mode 100644 index 000000000..28df192d6 Binary files /dev/null and b/assets/images/mobile/virtualpad/f.png differ diff --git a/assets/images/mobile/virtualpad/g.png b/assets/images/mobile/virtualpad/g.png new file mode 100644 index 000000000..e3835860b Binary files /dev/null and b/assets/images/mobile/virtualpad/g.png differ diff --git a/assets/images/mobile/virtualpad/h.png b/assets/images/mobile/virtualpad/h.png new file mode 100644 index 000000000..90260779a Binary files /dev/null and b/assets/images/mobile/virtualpad/h.png differ diff --git a/assets/images/mobile/virtualpad/i.png b/assets/images/mobile/virtualpad/i.png new file mode 100644 index 000000000..3c1787f03 Binary files /dev/null and b/assets/images/mobile/virtualpad/i.png differ diff --git a/assets/images/mobile/virtualpad/j.png b/assets/images/mobile/virtualpad/j.png new file mode 100644 index 000000000..5e4086c1c Binary files /dev/null and b/assets/images/mobile/virtualpad/j.png differ diff --git a/assets/images/mobile/virtualpad/k.png b/assets/images/mobile/virtualpad/k.png new file mode 100644 index 000000000..8576c8d69 Binary files /dev/null and b/assets/images/mobile/virtualpad/k.png differ diff --git a/assets/images/mobile/virtualpad/l.png b/assets/images/mobile/virtualpad/l.png new file mode 100644 index 000000000..56209bfca Binary files /dev/null and b/assets/images/mobile/virtualpad/l.png differ diff --git a/assets/images/mobile/virtualpad/left.png b/assets/images/mobile/virtualpad/left.png new file mode 100644 index 000000000..6f3a11b1c Binary files /dev/null and b/assets/images/mobile/virtualpad/left.png differ diff --git a/assets/images/mobile/virtualpad/m.png b/assets/images/mobile/virtualpad/m.png new file mode 100644 index 000000000..a73db43d4 Binary files /dev/null and b/assets/images/mobile/virtualpad/m.png differ diff --git a/assets/images/mobile/virtualpad/n.png b/assets/images/mobile/virtualpad/n.png new file mode 100644 index 000000000..4844e113a Binary files /dev/null and b/assets/images/mobile/virtualpad/n.png differ diff --git a/assets/images/mobile/virtualpad/o.png b/assets/images/mobile/virtualpad/o.png new file mode 100644 index 000000000..d53a7d63b Binary files /dev/null and b/assets/images/mobile/virtualpad/o.png differ diff --git a/assets/images/mobile/virtualpad/p.png b/assets/images/mobile/virtualpad/p.png new file mode 100644 index 000000000..eabcfe131 Binary files /dev/null and b/assets/images/mobile/virtualpad/p.png differ diff --git a/assets/images/mobile/virtualpad/q.png b/assets/images/mobile/virtualpad/q.png new file mode 100644 index 000000000..cb6ae7743 Binary files /dev/null and b/assets/images/mobile/virtualpad/q.png differ diff --git a/assets/images/mobile/virtualpad/r.png b/assets/images/mobile/virtualpad/r.png new file mode 100644 index 000000000..e21377370 Binary files /dev/null and b/assets/images/mobile/virtualpad/r.png differ diff --git a/assets/images/mobile/virtualpad/right.png b/assets/images/mobile/virtualpad/right.png new file mode 100644 index 000000000..95a8a4ff8 Binary files /dev/null and b/assets/images/mobile/virtualpad/right.png differ diff --git a/assets/images/mobile/virtualpad/s.png b/assets/images/mobile/virtualpad/s.png new file mode 100644 index 000000000..02d386f99 Binary files /dev/null and b/assets/images/mobile/virtualpad/s.png differ diff --git a/assets/images/mobile/virtualpad/t.png b/assets/images/mobile/virtualpad/t.png new file mode 100644 index 000000000..af9f5103c Binary files /dev/null and b/assets/images/mobile/virtualpad/t.png differ diff --git a/assets/images/mobile/virtualpad/u.png b/assets/images/mobile/virtualpad/u.png new file mode 100644 index 000000000..273f3723a Binary files /dev/null and b/assets/images/mobile/virtualpad/u.png differ diff --git a/assets/images/mobile/virtualpad/up.png b/assets/images/mobile/virtualpad/up.png new file mode 100644 index 000000000..89a3a28a4 Binary files /dev/null and b/assets/images/mobile/virtualpad/up.png differ diff --git a/assets/images/mobile/virtualpad/v.png b/assets/images/mobile/virtualpad/v.png new file mode 100644 index 000000000..2e9ccf775 Binary files /dev/null and b/assets/images/mobile/virtualpad/v.png differ diff --git a/assets/images/mobile/virtualpad/w.png b/assets/images/mobile/virtualpad/w.png new file mode 100644 index 000000000..3c84a137d Binary files /dev/null and b/assets/images/mobile/virtualpad/w.png differ diff --git a/assets/images/mobile/virtualpad/x.png b/assets/images/mobile/virtualpad/x.png new file mode 100644 index 000000000..231c090b3 Binary files /dev/null and b/assets/images/mobile/virtualpad/x.png differ diff --git a/assets/images/mobile/virtualpad/y.png b/assets/images/mobile/virtualpad/y.png new file mode 100644 index 000000000..4ac65849e Binary files /dev/null and b/assets/images/mobile/virtualpad/y.png differ diff --git a/assets/images/mobile/virtualpad/z.png b/assets/images/mobile/virtualpad/z.png new file mode 100644 index 000000000..99bca74b2 Binary files /dev/null and b/assets/images/mobile/virtualpad/z.png differ diff --git a/hmm.json b/hmm.json new file mode 100644 index 000000000..dbd648284 --- /dev/null +++ b/hmm.json @@ -0,0 +1,87 @@ +{ + "dependencies": [ + { + "name": "lime", + "type": "git", + "dir": null, + "ref": "develop", + "url": "https://github.com/mcagabe19-stuff/lime" + }, + { + "name": "hscript-improved", + "type": "git", + "dir": null, + "ref": "custom-classes", + "url": "https://www.github.com/FNF-CNE-Devs/hscript-improved" + }, + { + "name": "openfl", + "type": "haxelib", + "version": "9.2.2" + }, + { + "name": "away3d", + "type": "git", + "dir": null, + "ref": "master", + "url": "https://github.com/FNF-CNE-Devs/away3d" + }, + { + "name": "format", + "type": "haxelib", + "version": null + }, + { + "name": "markdown", + "type": "haxelib", + "version": null + }, + { + "name": "flixel", + "type": "git", + "dir": null, + "ref": "dev", + "url": "https://github.com/FNF-CNE-Devs/flixel" + }, + { + "name": "flixel-addons", + "type": "git", + "dir": null, + "ref": "dev", + "url": "https://github.com/FNF-CNE-Devs/flixel-addons" + }, + { + "name": "hxcpp", + "type": "git", + "dir": null, + "ref": "master", + "url": "https://github.com/mcagabe19-stuff/hxcpp" + }, + { + "name": "hxvlc", + "type": "haxelib", + "version": null + }, + { + "name": "hxdiscord_rpc", + "type": "git", + "dir": null, + "ref": "main", + "url": "https://github.com/FNF-CNE-Devs/hxdiscord_rpc" + }, + { + "name": "extension-androidtools", + "type": "git", + "dir": null, + "ref": "main", + "url": "https://github.com/MAJigsaw77/extension-androidtools" + }, + { + "name": "flxanimate", + "type": "git", + "dir": null, + "ref": "master", + "url": "https://github.com/FNF-CNE-Devs/flxanimate" + } + ] +} diff --git a/key.keystore b/key.keystore new file mode 100644 index 000000000..e21ca6f9c Binary files /dev/null and b/key.keystore differ diff --git a/project.xml b/project.xml index 9b396fa74..4e77d4626 100644 --- a/project.xml +++ b/project.xml @@ -38,7 +38,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -141,7 +141,7 @@
- +
@@ -180,6 +180,10 @@ + + + +
@@ -189,6 +193,9 @@ + + + @@ -198,4 +205,10 @@ + + + + + + diff --git a/source/funkin/backend/MusicBeatState.hx b/source/funkin/backend/MusicBeatState.hx index 35b684e57..aa79c4453 100644 --- a/source/funkin/backend/MusicBeatState.hx +++ b/source/funkin/backend/MusicBeatState.hx @@ -12,6 +12,12 @@ import funkin.backend.scripting.ScriptPack; import funkin.backend.system.interfaces.IBeatReceiver; import funkin.backend.system.Conductor; import funkin.options.PlayerSettings; +import mobile.objects.MobileControls; +import mobile.flixel.FlxVirtualPad; +import flixel.FlxCamera; +import flixel.input.actions.FlxActionInput; +import flixel.util.FlxDestroyUtil; +import flixel.util.typeLimit.OneOfTwo; class MusicBeatState extends FlxState implements IBeatReceiver { @@ -105,8 +111,110 @@ class MusicBeatState extends FlxState implements IBeatReceiver inline function get_controlsP2():Controls return PlayerSettings.player2.controls; + public var mobileControls:MobileControls; + public var virtualPad:FlxVirtualPad; + public var camControls:FlxCamera; + public var camVPad:FlxCamera; + public static var instance:MusicBeatState; + + var trackedInputsMobileControls:Array = []; + var trackedInputsVirtualPad:Array = []; + + public function addVirtualPad(DPad:OneOfTwo, Action:OneOfTwo):Void + { + if (virtualPad != null) + removeVirtualPad(); + + virtualPad = new FlxVirtualPad(DPad, Action); + add(virtualPad); + + controls.setVirtualPadUI(virtualPad, virtualPad.curDPadMode, virtualPad.curActionMode); + trackedInputsVirtualPad = controls.trackedInputsUI; + controls.trackedInputsUI = []; + } + + public function removeVirtualPad():Void + { + if (trackedInputsVirtualPad.length > 0) + controls.removeVirtualControlsInput(trackedInputsVirtualPad); + + if (virtualPad != null) + remove(virtualPad); + } + + public function addMobileControls(DefaultDrawTarget:Bool = false) { + if (mobileControls != null) + removeMobileControls(); + + mobileControls = new MobileControls(); + + switch (MobileControls.mode) + { + case 0 | 1 | 2: + controls.setVirtualPadNOTES(mobileControls.virtualPad, RIGHT_FULL, NONE); + case 3: + controls.setHitBox(mobileControls.hitbox); + case 4: // do nothing + } + + trackedInputsMobileControls = controls.trackedInputsNOTES; + controls.trackedInputsNOTES = []; + + camControls = new FlxCamera(); + camControls.bgColor.alpha = 0; + FlxG.cameras.add(camControls, DefaultDrawTarget); + + mobileControls.cameras = [camControls]; + add(mobileControls); + } + + public function removeMobileControls() { + if(trackedInputsMobileControls.length > 0) + controls.removeVirtualControlsInput(trackedInputsMobileControls); + + if(mobileControls != null) + remove(mobileControls); + } + + public function addVirtualPadCamera(DefaultDrawTarget:Bool = false) { + if (virtualPad == null) return; + + camVPad = new FlxCamera(); + camVPad.bgColor.alpha = 0; + FlxG.cameras.add(camVPad, DefaultDrawTarget); + virtualPad.cameras = [camVPad]; + } + + override function destroy() { + // Mobile Controls Related + if(trackedInputsMobileControls.length > 0) + controls.removeVirtualControlsInput(trackedInputsMobileControls); + + if(trackedInputsVirtualPad.length > 0) + controls.removeVirtualControlsInput(trackedInputsVirtualPad); + + if(virtualPad != null) + virtualPad = FlxDestroyUtil.destroy(virtualPad); + + if(mobileControls != null) + mobileControls = FlxDestroyUtil.destroy(mobileControls); + + if(camControls != null) + camControls = FlxDestroyUtil.destroy(camControls); + + if(camVPad != null) + camVPad = FlxDestroyUtil.destroy(camVPad); + + // CNE Related + super.destroy(); + graphicCache.destroy(); + call("destroy"); + stateScripts = FlxDestroyUtil.destroy(stateScripts); + } + public function new(scriptsAllowed:Bool = true, ?scriptName:String) { super(); + instance = this; this.scriptsAllowed = #if SOFTCODED_STATES scriptsAllowed #else false #end; this.scriptName = scriptName; } @@ -125,6 +233,13 @@ class MusicBeatState extends FlxState implements IBeatReceiver script.remappedNames.set(script.fileName, '$i:${script.fileName}'); stateScripts.add(script); script.load(); + stateScripts.set('setVirtualPadMode', function(DPadMode:String, ActionMode:String, ?addCamera = false){ + if(virtualPad == null) return; + removeVirtualPad(); + addVirtualPad(DPadMode, ActionMode); + if(addCamera) + addVirtualPadCamera(false); + }); } } else stateScripts.reload(); @@ -236,13 +351,6 @@ class MusicBeatState extends FlxState implements IBeatReceiver event("onResize", EventManager.get(ResizeEvent).recycle(w, h, null, null)); } - public override function destroy() { - super.destroy(); - graphicCache.destroy(); - call("destroy"); - stateScripts = FlxDestroyUtil.destroy(stateScripts); - } - public override function draw() { graphicCache.draw(); var e = event("draw", EventManager.get(DrawEvent).recycle()); diff --git a/source/funkin/backend/MusicBeatSubstate.hx b/source/funkin/backend/MusicBeatSubstate.hx index e2bbb3123..71a6ce20e 100644 --- a/source/funkin/backend/MusicBeatSubstate.hx +++ b/source/funkin/backend/MusicBeatSubstate.hx @@ -10,6 +10,12 @@ import funkin.backend.system.Conductor; import funkin.backend.system.Controls; import funkin.options.PlayerSettings; import flixel.FlxSubState; +import mobile.objects.MobileControls; +import mobile.flixel.FlxVirtualPad; +import flixel.FlxCamera; +import flixel.input.actions.FlxActionInput; +import flixel.util.FlxDestroyUtil; +import flixel.util.typeLimit.OneOfTwo; class MusicBeatSubstate extends FlxSubState implements IBeatReceiver { @@ -91,9 +97,111 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver inline function get_controlsP2():Controls return PlayerSettings.player2.controls; + public var mobileControls:MobileControls; + public var virtualPad:FlxVirtualPad; + public var camControls:FlxCamera; + public var camVPad:FlxCamera; + public static var instance:MusicBeatSubstate; + + var trackedInputsMobileControls:Array = []; + var trackedInputsVirtualPad:Array = []; + + public function addVirtualPad(DPad:OneOfTwo, Action:OneOfTwo) + { + if (virtualPad != null) + removeVirtualPad(); + + virtualPad = new FlxVirtualPad(DPad, Action); + add(virtualPad); + + controls.setVirtualPadUI(virtualPad, virtualPad.curDPadMode, virtualPad.curActionMode); + trackedInputsVirtualPad = controls.trackedInputsUI; + controls.trackedInputsUI = []; + } + + public function removeVirtualPad() + { + if (trackedInputsVirtualPad.length > 0) + controls.removeVirtualControlsInput(trackedInputsVirtualPad); + + if (virtualPad != null) + remove(virtualPad); + } + + public function addMobileControls(DefaultDrawTarget:Bool = false) { + if (mobileControls != null) + removeMobileControls(); + + mobileControls = new MobileControls(); + + switch (MobileControls.mode) + { + case 0 | 1 | 2: + controls.setVirtualPadNOTES(mobileControls.virtualPad, RIGHT_FULL, NONE); + case 3: + controls.setHitBox(mobileControls.hitbox); + case 4: // do nothing + } + + trackedInputsMobileControls = controls.trackedInputsNOTES; + controls.trackedInputsNOTES = []; + + camControls = new FlxCamera(); + camControls.bgColor.alpha = 0; + FlxG.cameras.add(camControls, DefaultDrawTarget); + + mobileControls.cameras = [camControls]; + mobileControls.visible = false; + add(mobileControls); + } + + public function removeMobileControls() { + if(trackedInputsMobileControls.length > 0) + controls.removeVirtualControlsInput(trackedInputsMobileControls); + + if(mobileControls != null) + remove(mobileControls); + } + + public function addVirtualPadCamera(DefaultDrawTarget:Bool = false) { + if (virtualPad == null) return; + + camVPad = new FlxCamera(); + camVPad.bgColor.alpha = 0; + FlxG.cameras.add(camVPad, DefaultDrawTarget); + virtualPad.cameras = [camVPad]; + } + + override function destroy() { + // Mobile Controls Related + if(trackedInputsMobileControls.length > 0) + controls.removeVirtualControlsInput(trackedInputsMobileControls); + + if(trackedInputsVirtualPad.length > 0) + controls.removeVirtualControlsInput(trackedInputsVirtualPad); + + if(virtualPad != null) + virtualPad = FlxDestroyUtil.destroy(virtualPad); + + if(mobileControls != null) + mobileControls = FlxDestroyUtil.destroy(mobileControls); + + if(camControls != null) + camControls = FlxDestroyUtil.destroy(camControls); + + if(camVPad != null) + camVPad = FlxDestroyUtil.destroy(camVPad); + + // CNE Related + super.destroy(); + call("destroy"); + stateScripts = FlxDestroyUtil.destroy(stateScripts); + + } public function new(scriptsAllowed:Bool = true, ?scriptName:String) { super(); + instance = this; this.scriptsAllowed = #if SOFTCODED_STATES scriptsAllowed #else false #end; this.scriptName = scriptName; } @@ -112,6 +220,13 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver script.remappedNames.set(script.fileName, '$i:${script.fileName}'); stateScripts.add(script); script.load(); + stateScripts.set('setVirtualPadMode', function(DPadMode:String, ActionMode:String, ?addCamera = false){ + if(virtualPad == null) return; + removeVirtualPad(); + addVirtualPad(DPadMode, ActionMode); + if(addCamera) + addVirtualPadCamera(false); + }); } } else stateScripts.reload(); @@ -225,12 +340,6 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver event("onResize", EventManager.get(ResizeEvent).recycle(w, h, null, null)); } - public override function destroy() { - super.destroy(); - call("destroy"); - stateScripts = FlxDestroyUtil.destroy(stateScripts); - } - public override function switchTo(nextState:FlxState) { var e = event("onStateSwitch", EventManager.get(StateEvent).recycle(nextState)); if (e.cancelled) diff --git a/source/funkin/backend/assets/AssetsLibraryList.hx b/source/funkin/backend/assets/AssetsLibraryList.hx index 494479970..0b578d842 100644 --- a/source/funkin/backend/assets/AssetsLibraryList.hx +++ b/source/funkin/backend/assets/AssetsLibraryList.hx @@ -1,7 +1,11 @@ package funkin.backend.assets; +import sys.FileSystem; import funkin.backend.assets.IModsAssetLibrary; import lime.utils.AssetLibrary; +import lime.utils.AssetType; + +using StringTools; class AssetsLibraryList extends AssetLibrary { public var libraries:Array = []; @@ -58,14 +62,30 @@ class AssetsLibraryList extends AssetLibrary { l = cast(l, openfl.utils.AssetLibrary).__proxy; } - // TODO: do base folder scanning #if MOD_SUPPORT - if (l is IModsAssetLibrary) { - var lib = cast(l, IModsAssetLibrary); - for(e in lib.getFiles(folder)) - content.push(e); + if (source == MODS || source == BOTH) { + if (l is IModsAssetLibrary) { + var lib = cast(l, IModsAssetLibrary); + for (e in lib.getFiles(folder)) + content.push(e); + } + } + #else + #if sys + if (source == SOURCE || source == BOTH) { + var fileStuffs = FileSystem.readDirectory(folder); + if (fileStuffs != null && fileStuffs.length > 0) { + for (e in fileStuffs) { + if (!FileSystem.isDirectory(folder + e.toString())) { + content.push(e); + } + } + } else { + Logs.trace('No files/folders found in the requested directory \'${folder}\'', WARNING, YELLOW); + } } #end + #end } return content; } @@ -82,14 +102,30 @@ class AssetsLibraryList extends AssetLibrary { l = cast(l, openfl.utils.AssetLibrary).__proxy; } - // TODO: do base folder scanning #if MOD_SUPPORT - if (l is IModsAssetLibrary) { - var lib = cast(l, IModsAssetLibrary); - for(e in lib.getFolders(folder)) - content.push(e); + if (source == MODS || source == BOTH) { + if (l is IModsAssetLibrary) { + var lib = cast(l, IModsAssetLibrary); + for (e in lib.getFolders(folder)) + content.push(e); + } + } + #else + #if sys + if (source == SOURCE || source == BOTH) { + var fileStuffs = FileSystem.readDirectory(folder); + if (fileStuffs != null && fileStuffs.length > 0) { + for (e in fileStuffs) { + if (FileSystem.isDirectory(folder + e.toString())) { + content.push(e); + } + } + } else { + Logs.trace('No files/folders found in the requested directory \'${folder}\'', WARNING, YELLOW); + } } #end + #end } return content; } @@ -162,6 +198,20 @@ class AssetsLibraryList extends AssetLibrary { libraries.insert(0, lib); return lib; } + + override public function list(type:String):Array { + var items = []; + + for(library in libraries) { + var libraryItems = library.list(type); + + if (libraryItems != null) { + items = items.concat(libraryItems); + } + } + + return items; + } } enum abstract AssetSource(Null) from Bool from Null to Null { diff --git a/source/funkin/backend/assets/ModsFolder.hx b/source/funkin/backend/assets/ModsFolder.hx index cd1704ed3..bc3d021bd 100644 --- a/source/funkin/backend/assets/ModsFolder.hx +++ b/source/funkin/backend/assets/ModsFolder.hx @@ -35,11 +35,11 @@ class ModsFolder { /** * Path to the `mods` folder. */ - public static var modsPath:String = "./mods/"; + public static var modsPath:String = SUtil.getStorageDirectory(true) + "mods/"; /** * Path to the `addons` folder. */ - public static var addonsPath:String = "./addons/"; + public static var addonsPath:String = SUtil.getStorageDirectory(true) + "addons/"; /** * If accessing a file as assets/data/global/LIB_mymod.hx should redirect to mymod:assets/data/global.hx @@ -54,6 +54,8 @@ class ModsFolder { * Initialises `mods` folder. */ public static function init() { + if (!FileSystem.exists(modsPath)) FileSystem.createDirectory(modsPath); + if (!FileSystem.exists(addonsPath)) FileSystem.createDirectory(addonsPath); if(!getModsList().contains(Options.lastLoadedMod)) Options.lastLoadedMod = null; } diff --git a/source/funkin/backend/assets/Paths.hx b/source/funkin/backend/assets/Paths.hx index d8c6ba392..361c1a611 100644 --- a/source/funkin/backend/assets/Paths.hx +++ b/source/funkin/backend/assets/Paths.hx @@ -171,7 +171,7 @@ class Paths return FlxAtlasFrames.fromAseprite('$key.png', '$key.json'); inline static public function getAssetsRoot():String - return ModsFolder.currentModFolder != null ? '${ModsFolder.modsPath}${ModsFolder.currentModFolder}' : './assets'; + return ModsFolder.currentModFolder != null ? '${ModsFolder.modsPath}${ModsFolder.currentModFolder}' : 'assets'; /** * Gets frames at specified path. diff --git a/source/funkin/backend/assets/ScriptedAssetLibrary.hx b/source/funkin/backend/assets/ScriptedAssetLibrary.hx index 0fd8d380c..1ca8268f0 100644 --- a/source/funkin/backend/assets/ScriptedAssetLibrary.hx +++ b/source/funkin/backend/assets/ScriptedAssetLibrary.hx @@ -21,7 +21,8 @@ class ScriptedAssetLibrary extends ModsFolderLibrary { public var scriptName:String; private static var nullValue:Dynamic = {}; - public function new(scriptName:String, args:Array = null, folderPath:String="./assets/", libName:String="assets", ?modName:String) { + public function new(scriptName:String, args:Array = null, folderPath:String="", libName:String="assets", ?modName:String) { + if(folderPath == "") folderPath = SUtil.getStorageDirectory() + "assets/"; if(modName == null) modName = scriptName; super(folderPath, libName, modName); this.scriptName = scriptName; diff --git a/source/funkin/backend/shaders/CustomShader.hx b/source/funkin/backend/shaders/CustomShader.hx index 7ea07c7e1..6aaf81fc3 100644 --- a/source/funkin/backend/shaders/CustomShader.hx +++ b/source/funkin/backend/shaders/CustomShader.hx @@ -19,9 +19,9 @@ class CustomShader extends FunkinShader { /** * Creates a new custom shader * @param name Name of the frag and vert files. - * @param glslVersion GLSL version to use. Defaults to `120`. + * @param glslVersion GLSL version to use. Defaults to `100` in mobile, `120` in desktop. */ - public function new(name:String, glslVersion:String = "120") { + public function new(name:String, glslVersion:String = #if mobile "100" #else "120" #end) { var fragShaderPath = Paths.fragShader(name); var vertShaderPath = Paths.vertShader(name); var fragCode = Assets.exists(fragShaderPath) ? Assets.getText(fragShaderPath) : null; diff --git a/source/funkin/backend/shaders/FunkinShader.hx b/source/funkin/backend/shaders/FunkinShader.hx index ef91d1285..9c8bcfb17 100644 --- a/source/funkin/backend/shaders/FunkinShader.hx +++ b/source/funkin/backend/shaders/FunkinShader.hx @@ -25,16 +25,16 @@ import openfl.display.ShaderInput; class FunkinShader extends FlxShader implements IHScriptCustomBehaviour { private static var __instanceFields = Type.getInstanceFields(FunkinShader); - public var glslVer:String = "120"; + public var glslVer:String = #if mobile "100" #else "120" #end; /** * Creates a new shader from the specified fragment and vertex source. * Accepts `#pragma header`. * @param frag Fragment source (pass `null` to use default) * @param vert Vertex source (pass `null` to use default) - * @param glslVer Version of GLSL to use (defaults to 120) + * @param glslVer Version of GLSL to use (defaults to 120 at desktop, 100 at mobile) */ - public override function new(frag:String, vert:String, glslVer:String = "120") { + public override function new(frag:String, vert:String, glslVer:String = #if mobile "100" #else "120" #end) { if (frag == null) frag = ShaderTemplates.defaultFragmentSource; if (vert == null) vert = ShaderTemplates.defaultVertexSource; this.glFragmentSource = frag; diff --git a/source/funkin/backend/system/Controls.hx b/source/funkin/backend/system/Controls.hx index a09da1c74..8e20d8533 100644 --- a/source/funkin/backend/system/Controls.hx +++ b/source/funkin/backend/system/Controls.hx @@ -9,6 +9,9 @@ import flixel.input.actions.FlxActionSet; import flixel.input.gamepad.FlxGamepadButton; import flixel.input.gamepad.FlxGamepadInputID; import flixel.input.keyboard.FlxKey; +import mobile.flixel.FlxButton as Button; +import mobile.objects.Hitbox; +import mobile.flixel.FlxVirtualPad; enum abstract Action(String) to String from String { var UP = "up"; @@ -419,6 +422,129 @@ class Controls extends FlxActionSet { super.update(); } + + public var trackedInputsUI:Array = []; + public var trackedInputsNOTES:Array = []; + + public function addButtonNOTES(action:FlxActionDigital, button:Button, state:FlxInputState):Void + { + if (button == null) + return; + + var input:FlxActionInputDigitalIFlxInput = new FlxActionInputDigitalIFlxInput(button, state); + trackedInputsNOTES.push(input); + action.add(input); + } + + public function addButtonUI(action:FlxActionDigital, button:Button, state:FlxInputState):Void + { + if (button == null) + return; + + var input:FlxActionInputDigitalIFlxInput = new FlxActionInputDigitalIFlxInput(button, state); + trackedInputsUI.push(input); + action.add(input); + } + + public function setHitBox(hitbox:Hitbox):Void + { + if (Hitbox == null) + return; + + inline forEachBound(Control.NOTE_LEFT, (action, state) -> addButtonNOTES(action, hitbox.buttonLeft, state)); + inline forEachBound(Control.NOTE_DOWN, (action, state) -> addButtonNOTES(action, hitbox.buttonDown, state)); + inline forEachBound(Control.NOTE_UP, (action, state) -> addButtonNOTES(action, hitbox.buttonUp, state)); + inline forEachBound(Control.NOTE_RIGHT, (action, state) -> addButtonNOTES(action, hitbox.buttonRight, state)); + } + + public function setVirtualPadUI(vpad:FlxVirtualPad, DPad:FlxDPadMode, Action:FlxActionMode):Void + { + if (vpad == null) + return; + + switch (DPad) + { + case UP_DOWN: + inline forEachBound(Control.UP, (action, state) -> addButtonUI(action, vpad.buttonUp, state)); + inline forEachBound(Control.DOWN, (action, state) -> addButtonUI(action, vpad.buttonDown, state)); + case LEFT_RIGHT: + inline forEachBound(Control.LEFT, (action, state) -> addButtonUI(action, vpad.buttonLeft, state)); + inline forEachBound(Control.RIGHT, (action, state) -> addButtonUI(action, vpad.buttonRight, state)); + case NONE: // do nothing + default: + inline forEachBound(Control.UP, (action, state) -> addButtonUI(action, vpad.buttonUp, state)); + inline forEachBound(Control.DOWN, (action, state) -> addButtonUI(action, vpad.buttonDown, state)); + inline forEachBound(Control.LEFT, (action, state) -> addButtonUI(action, vpad.buttonLeft, state)); + inline forEachBound(Control.RIGHT, (action, state) -> addButtonUI(action, vpad.buttonRight, state)); + } + + switch (Action) + { + case A: + inline forEachBound(Control.ACCEPT, (action, state) -> addButtonUI(action, vpad.buttonA, state)); + case B: + inline forEachBound(Control.BACK, (action, state) -> addButtonUI(action, vpad.buttonB, state)); + case P: + inline forEachBound(Control.PAUSE, (action, state) -> addButtonUI(action, vpad.buttonP, state)); + case NONE: // do nothing + default: + inline forEachBound(Control.ACCEPT, (action, state) -> addButtonUI(action, vpad.buttonA, state)); + inline forEachBound(Control.BACK, (action, state) -> addButtonUI(action, vpad.buttonB, state)); + } + } + + public function setVirtualPadNOTES(VirtualPad:FlxVirtualPad, DPad:FlxDPadMode, Action:FlxActionMode):Void + { + if (VirtualPad == null) + return; + + switch (DPad) + { + case UP_DOWN: + inline forEachBound(Control.NOTE_UP, (action, state) -> addButtonNOTES(action, VirtualPad.buttonUp, state)); + inline forEachBound(Control.NOTE_DOWN, (action, state) -> addButtonNOTES(action, VirtualPad.buttonDown, state)); + case LEFT_RIGHT: + inline forEachBound(Control.NOTE_LEFT, (action, state) -> addButtonNOTES(action, VirtualPad.buttonLeft, state)); + inline forEachBound(Control.NOTE_RIGHT, (action, state) -> addButtonNOTES(action, VirtualPad.buttonRight, state)); + case NONE: // do nothing + default: + inline forEachBound(Control.NOTE_UP, (action, state) -> addButtonNOTES(action, VirtualPad.buttonUp, state)); + inline forEachBound(Control.NOTE_DOWN, (action, state) -> addButtonNOTES(action, VirtualPad.buttonDown, state)); + inline forEachBound(Control.NOTE_LEFT, (action, state) -> addButtonNOTES(action, VirtualPad.buttonLeft, state)); + inline forEachBound(Control.NOTE_RIGHT, (action, state) -> addButtonNOTES(action, VirtualPad.buttonRight, state)); + } + + switch (Action) + { + case A: + inline forEachBound(Control.ACCEPT, (action, state) -> addButtonNOTES(action, VirtualPad.buttonA, state)); + case B: + inline forEachBound(Control.BACK, (action, state) -> addButtonNOTES(action, VirtualPad.buttonB, state)); + case P: + inline forEachBound(Control.PAUSE, (action, state) -> addButtonNOTES(action, VirtualPad.buttonP, state)); + case NONE: // do nothing + default: + inline forEachBound(Control.ACCEPT, (action, state) -> addButtonNOTES(action, VirtualPad.buttonA, state)); + inline forEachBound(Control.BACK, (action, state) -> addButtonNOTES(action, VirtualPad.buttonB, state)); + } + } + + public function removeVirtualControlsInput(Tinputs:Array):Void + { + for (action in this.digitalActions) + { + var i = action.inputs.length; + while (i-- > 0) + { + var x = Tinputs.length; + while (x-- > 0) + { + if (Tinputs[x] == action.inputs[i]) + action.remove(action.inputs[i]); + } + } + } + } // inline public function checkByName(name:Action):Bool diff --git a/source/funkin/backend/system/Main.hx b/source/funkin/backend/system/Main.hx index cad2266ec..85841bb79 100644 --- a/source/funkin/backend/system/Main.hx +++ b/source/funkin/backend/system/Main.hx @@ -16,6 +16,9 @@ import flixel.addons.transition.TransitionData; import flixel.math.FlxPoint; import flixel.math.FlxRect; import funkin.backend.system.modules.*; +#if mobile +import mobile.funkin.backend.system.MobileRatioScaleMode as FunkinRatioScaleMode; +#end #if ALLOW_MULTITHREADING import sys.thread.Thread; @@ -24,6 +27,7 @@ import sys.thread.Thread; import sys.io.File; #end import funkin.backend.assets.ModsFolder; +import lime.system.System as LimeSystem; class Main extends Sprite { @@ -38,9 +42,7 @@ class Main extends Sprite public static var noTerminalColor:Bool = false; public static var scaleMode:FunkinRatioScaleMode; - #if !mobile public static var framerateSprite:funkin.backend.system.framerate.Framerate; - #end var gameWidth:Int = 1280; // Width of the game in pixels (might be less / more in actual pixels). var gameHeight:Int = 720; // Height of the game in pixels (might be less / more in actual pixels). @@ -63,12 +65,24 @@ class Main extends Sprite instance = this; + #if mobile + #if android + SUtil.doPermissionsShit(); + #end + Sys.setCwd(SUtil.getStorageDirectory(false)); + #end + CrashHandler.init(); + #if !html5 framerateSprite = new funkin.backend.system.framerate.Framerate(); #end + addChild(game = new FunkinGame(gameWidth, gameHeight, MainState, Options.framerate, Options.framerate, skipSplash, startFullscreen)); - #if (!mobile && !web) - addChild(framerateSprite = new funkin.backend.system.framerate.Framerate()); + #if android FlxG.android.preventDefaultKeys = [BACK]; #end + + #if !html5 + addChild(framerateSprite); + FlxG.stage.window.onResize.add((w:Int, h:Int) -> framerateSprite.setScale()); SystemInfo.init(); #end } @@ -127,12 +141,12 @@ class Main extends Sprite #if (sys && TEST_BUILD) trace("Used cne test / cne build. Switching into source assets."); #if MOD_SUPPORT - ModsFolder.modsPath = './${pathBack}mods/'; - ModsFolder.addonsPath = './${pathBack}addons/'; + ModsFolder.modsPath = Sys.getCwd() + '${pathBack}mods/'; + ModsFolder.addonsPath = Sys.getCwd() + '${pathBack}addons/'; #end - Paths.assetsTree.__defaultLibraries.push(ModsFolder.loadLibraryFromFolder('assets', './${pathBack}assets/', true)); + Paths.assetsTree.__defaultLibraries.push(ModsFolder.loadLibraryFromFolder('assets', Sys.getCwd() + '${pathBack}assets/', true)); #elseif USE_ADAPTED_ASSETS - Paths.assetsTree.__defaultLibraries.push(ModsFolder.loadLibraryFromFolder('assets', './assets/', true)); + Paths.assetsTree.__defaultLibraries.push(ModsFolder.loadLibraryFromFolder('assets', Sys.getCwd() + 'assets/', true)); #end @@ -152,10 +166,11 @@ class Main extends Sprite Conductor.init(); AudioSwitchFix.init(); EventManager.init(); + FlxG.signals.preStateSwitch.add(onStateSwitch); FlxG.signals.postStateSwitch.add(onStateSwitchPost); - FlxG.mouse.useSystemCursor = true; + FlxG.mouse.useSystemCursor = !MobileControls.mobileC; ModsFolder.init(); #if MOD_SUPPORT @@ -163,6 +178,9 @@ class Main extends Sprite #end initTransition(); + #if mobile + LimeSystem.allowScreenTimeout = Options.screenTimeOut; + #end } public static function refreshAssets() { diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx index 14c40aff8..f5f078f6d 100644 --- a/source/funkin/backend/system/MainState.hx +++ b/source/funkin/backend/system/MainState.hx @@ -8,6 +8,9 @@ import funkin.menus.TitleState; import funkin.menus.BetaWarningState; import funkin.backend.chart.EventsData; import flixel.FlxState; +#if mobile +import mobile.funkin.backend.system.CopyState; +#end /** * Simple state used for loading the game @@ -17,12 +20,22 @@ class MainState extends FlxState { public static var betaWarningShown:Bool = false; public override function create() { super.create(); + funkin.backend.system.Main.framerateSprite.setScale(); if (!initiated) + { Main.loadGameSettings(); + #if mobile + if (!CopyState.checkExistingFiles()) + { + FlxG.switchState(new CopyState()); + return; + } + #end + } initiated = true; #if sys - CoolUtil.deleteFolder('./.temp/'); // delete temp folder + CoolUtil.deleteFolder('.temp/'); // delete temp folder #end Options.save(); diff --git a/source/funkin/backend/system/framerate/Framerate.hx b/source/funkin/backend/system/framerate/Framerate.hx index 88680e89d..20f307e3c 100644 --- a/source/funkin/backend/system/framerate/Framerate.hx +++ b/source/funkin/backend/system/framerate/Framerate.hx @@ -9,6 +9,7 @@ import openfl.display.Sprite; import openfl.text.TextField; import openfl.text.TextFormat; import openfl.ui.Keyboard; +import flixel.util.FlxTimer; class Framerate extends Sprite { public static var instance:Framerate; @@ -43,6 +44,11 @@ class Framerate extends Sprite { return __bitmap; } + #if mobile + #if android public var presses:Int = 0; #end + public var sillyTimer:FlxTimer = new FlxTimer(); + #end + public function new() { super(); if (instance != null) throw "Cannot create another instance"; @@ -101,13 +107,37 @@ class Framerate extends Sprite { public override function __enterFrame(t:Int) { alpha = CoolUtil.fpsLerp(alpha, debugMode > 0 ? 1 : 0, 0.5); debugAlpha = CoolUtil.fpsLerp(debugAlpha, debugMode > 1 ? 1 : 0, 0.5); + #if android + if(FlxG.android.justReleased.BACK){ + sillyTimer.cancel(); + ++presses; + if(presses >= 3){ + debugMode = (debugMode + 1) % 3; + presses = 0; + return; + } + sillyTimer.start(0.3, (tmr:FlxTimer) -> presses = 0); + } + #elseif ios + for(camera in FlxG.cameras.list) { + var pos = FlxG.mouse.getScreenPosition(camera); + if(posx >= 0 && posx <= 80 && posy >= 0 && posy <= 60) { + if(FlxG.mouse.justPressed) + sillyTimer.start(0.4, (tmr:FlxTimer) -> debugMode = (debugMode + 1) % 3); + + if(FlxG.mouse.justReleased) + sillyTimer.cancel(); + } else if(sillyTimer.active && !sillyTimer.finished) + sillyTimer.cancel(); + } + #end if (alpha < 0.05) return; super.__enterFrame(t); bgSprite.alpha = debugAlpha * 0.5; - x = 10 + offset.x; - y = 2 + offset.y; + x = #if mobile FlxG.game.x + #end 10 + offset.x; + y = #if mobile FlxG.game.y + #end 2 + offset.y; var width = Math.max(fpsCounter.width, #if SHOW_BUILD_ON_FPS Math.max(memoryCounter.width, codenameBuildField.width) #else memoryCounter.width #end) + (x*2); var height = #if SHOW_BUILD_ON_FPS codenameBuildField.y + codenameBuildField.height #else memoryCounter.y + memoryCounter.height #end; @@ -132,4 +162,10 @@ class Framerate extends Sprite { y = c.y + c.height + 4; } } + + public inline function setScale(?scale:Float){ + if(scale == null) + scale = Math.min(FlxG.stage.window.width / FlxG.width, FlxG.stage.window.height / FlxG.height); + scaleX = scaleY = #if android (scale > 1 ? scale : 1) #else (scale < 1 ? scale : 1) #end; + } } \ No newline at end of file diff --git a/source/funkin/backend/system/framerate/SystemInfo.hx b/source/funkin/backend/system/framerate/SystemInfo.hx index 0d2bc73a5..8fa383861 100644 --- a/source/funkin/backend/system/framerate/SystemInfo.hx +++ b/source/funkin/backend/system/framerate/SystemInfo.hx @@ -3,9 +3,22 @@ package funkin.backend.system.framerate; import funkin.backend.utils.native.HiddenProcess; import funkin.backend.utils.MemoryUtil; import funkin.backend.system.Logs; +#if android +import android.os.Build; +import android.os.Build.VERSION; +#end using StringTools; +#if cpp +#if windows +@:cppFileCode('#include ') +#elseif (mac || ios) +@:cppFileCode('#include ') +#else +@:headerInclude('sys/utsname.h') +#end +#end class SystemInfo extends FramerateCategory { public static var osInfo:String = "Unknown"; public static var gpuName:String = "Unknown"; @@ -62,7 +75,7 @@ class SystemInfo extends FramerateCategory { if (process.exitCode() != 0) throw 'Could not fetch CPU information'; cpuName = process.stdout.readAll().toString().trim().split("\n")[1].trim(); - #elseif mac + #elseif (mac || ios) var process = new HiddenProcess("sysctl -a | grep brand_string"); // Somehow this isnt able to use the args but it still works if (process.exitCode() != 0) throw 'Could not fetch CPU information'; @@ -77,6 +90,8 @@ class SystemInfo extends FramerateCategory { break; } } + #elseif android + cpuName = (VERSION.SDK_INT >= VERSION_CODES.S) ? Build.SOC_MODEL : Build.HARDWARE; #end } catch (e) { Logs.trace('Unable to grab CPU Name: $e', ERROR, RED); @@ -116,9 +131,9 @@ class SystemInfo extends FramerateCategory { } static function formatSysInfo() { - __formattedSysText = ""; + __formattedSysText = #if android 'Device: ${Build.BRAND.charAt(0).toUpperCase() + Build.BRAND.substring(1)} ${Build.MODEL} (${Build.BOARD})\n' #else "" #end; if (osInfo != "Unknown") __formattedSysText += 'System: $osInfo'; - if (cpuName != "Unknown") __formattedSysText += '\nCPU: $cpuName ${openfl.system.Capabilities.cpuArchitecture} ${(openfl.system.Capabilities.supports64BitProcesses ? '64-Bit' : '32-Bit')}'; + if (cpuName != "Unknown") __formattedSysText += '\nCPU: $cpuName ${getCPUArch()}'; if (gpuName != cpuName || vRAM != "Unknown") { var gpuNameKnown = gpuName != "Unknown" && gpuName != cpuName; var vramKnown = vRAM != "Unknown"; @@ -146,4 +161,44 @@ class SystemInfo extends FramerateCategory { this.text.text = _text; super.__enterFrame(t); } + + #if windows + @:functionCode(' + SYSTEM_INFO osInfo; + + GetSystemInfo(&osInfo); + + switch(osInfo.wProcessorArchitecture) + { + case 9: + return ::String("x86_64"); + case 5: + return ::String("ARM"); + case 12: + return ::String("ARM64"); + case 6: + return ::String("IA-64"); + case 0: + return ::String("x86"); + default: + return ::String("Unknown"); + } + ') + #elseif (mac || ios) + @:functionCode(' + const NXArchInfo *archInfo = NXGetLocalArchInfo(); + return ::String(archInfo == NULL ? "Unknown" : archInfo->name); + ') + #elseif cpp + @:functionCode(' + struct utsname osInfo{}; + uname(&osInfo); + return ::String(osInfo.machine); + ') + #end + @:noCompletion + private static function getCPUArch():String + { + return "Unknown"; + } } \ No newline at end of file diff --git a/source/funkin/backend/system/modules/CrashHandler.hx b/source/funkin/backend/system/modules/CrashHandler.hx index 19c183165..57c44f75a 100644 --- a/source/funkin/backend/system/modules/CrashHandler.hx +++ b/source/funkin/backend/system/modules/CrashHandler.hx @@ -1,17 +1,18 @@ package funkin.backend.system.modules; -import lime.system.System; -import funkin.backend.utils.NativeAPI; -import openfl.Lib; import openfl.events.UncaughtErrorEvent; import openfl.events.ErrorEvent; import openfl.errors.Error; -import openfl.events.UncaughtErrorEvent; -import haxe.CallStack; +import lime.system.System as LimeSystem; +import haxe.io.Path; +#if sys +import sys.FileSystem; +import sys.io.File; +#end class CrashHandler { - public static function init() { - Lib.current.loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, onUncaughtError); + public static function init():Void { + openfl.Lib.current.loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, onUncaughtError); #if cpp untyped __global__.__hxcpp_set_critical_error_handler(onError); #elseif hl @@ -19,7 +20,11 @@ class CrashHandler { #end } - public static function onUncaughtError(e:UncaughtErrorEvent) { + private static function onUncaughtError(e:UncaughtErrorEvent):Void { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + var m:String = e.error; if (Std.isOfType(e.error, Error)) { var err = cast(e.error, Error); @@ -28,40 +33,52 @@ class CrashHandler { var err = cast(e.error, ErrorEvent); m = '${err.text}'; } - var stack = CallStack.exceptionStack(); - var stackLabel:String = ""; + var stack = haxe.CallStack.exceptionStack(); + var stackBuffer = new StringBuf(); for(e in stack) { switch(e) { - case CFunction: stackLabel += "Non-Haxe (C) Function"; - case Module(c): stackLabel += 'Module ${c}'; + case CFunction: stackBuffer.add("Non-Haxe (C) Function\n"); + case Module(c): stackBuffer.add('Module ${c}\n'); case FilePos(parent, file, line, col): switch(parent) { case Method(cla, func): - stackLabel += '(${file}) ${cla.split(".").last()}.$func() - line $line'; + stackBuffer.add('${Path.withoutExtension(file)}.$func() - line $line\n'); case _: - stackLabel += '(${file}) - line $line'; + stackBuffer.add('${file} - line $line\n'); } case LocalFunction(v): - stackLabel += 'Local Function ${v}'; + stackBuffer.add('Local Function ${v}\n'); case Method(cl, m): - stackLabel += '${cl} - ${m}'; + stackBuffer.add('${cl} - ${m}\n'); } - stackLabel += "\r\n"; } + var stackLabel = stackBuffer.toString(); + #if sys + try + { + if (!FileSystem.exists('crash')) + FileSystem.createDirectory('crash'); - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); + File.saveContent('crash/' + Date.now().toString().replace(' ', '-').replace(':', "'") + '.txt', '$m\n$stackLabel'); + } + catch (e:haxe.Exception) + trace('Couldn\'t save error message. (${e.message})'); + #end - NativeAPI.showMessageBox("Codename Engine Crash Handler", 'Uncaught Error:$m\n\n$stackLabel', MSG_ERROR); - #if sys - Sys.exit(1); + NativeAPI.showMessageBox("Error!", '$m\n$stackLabel', MSG_ERROR); + + #if js + if (FlxG.sound.music != null) + FlxG.sound.music.stop(); + + js.Browser.window.location.reload(true); + #else + LimeSystem.exit(1); #end } #if (cpp || hl) - private static function onError(message:Dynamic):Void - { + private static function onError(message:Dynamic):Void { throw Std.string(message); } #end diff --git a/source/funkin/backend/system/updating/AsyncUpdater.hx b/source/funkin/backend/system/updating/AsyncUpdater.hx index af7cfff57..52821a73c 100644 --- a/source/funkin/backend/system/updating/AsyncUpdater.hx +++ b/source/funkin/backend/system/updating/AsyncUpdater.hx @@ -66,7 +66,7 @@ class AsyncUpdater { var reader = ZipUtil.openZip(path); progress.curZipProgress = new ZipProgress(); - ZipUtil.uncompressZip(reader, './', null, progress.curZipProgress); + ZipUtil.uncompressZip(reader, Sys.getCwd(), null, progress.curZipProgress); // FileSystem.deleteFile(path); } if (executableReplaced = FileSystem.exists('$path$executableName')) { diff --git a/source/funkin/backend/system/updating/UpdateScreen.hx b/source/funkin/backend/system/updating/UpdateScreen.hx index 323fda09c..c9555b383 100644 --- a/source/funkin/backend/system/updating/UpdateScreen.hx +++ b/source/funkin/backend/system/updating/UpdateScreen.hx @@ -121,7 +121,7 @@ class UpdateScreen extends MusicBeatState { #if windows // the executable has been replaced, restart the game entirely Sys.command('start /B ${AsyncUpdater.executableName}'); - #else + #elseif !mobile // We have to make the new executable allowed to execute // before we can execute it! Sys.command('chmod +x ./${AsyncUpdater.executableName} && ./${AsyncUpdater.executableName}'); diff --git a/source/funkin/backend/utils/MemoryUtil.hx b/source/funkin/backend/utils/MemoryUtil.hx index 6905d6b20..d40975c83 100644 --- a/source/funkin/backend/utils/MemoryUtil.hx +++ b/source/funkin/backend/utils/MemoryUtil.hx @@ -69,8 +69,12 @@ class MemoryUtil { return funkin.backend.utils.native.Windows.getTotalRam(); #elseif mac return funkin.backend.utils.native.Mac.getTotalRam(); + #elseif ios + return funkin.backend.utils.native.IOS.getTotalRam(); #elseif linux return funkin.backend.utils.native.Linux.getTotalRam(); + #elseif android + return funkin.backend.utils.native.Android.getTotalRam(); #else return 0; #end @@ -124,7 +128,7 @@ class MemoryUtil { var process = new HiddenProcess("wmic", ["memorychip", "get", "SMBIOSMemoryType"]); if (process.exitCode() == 0) memoryOutput = Std.int(Std.parseFloat(process.stdout.readAll().toString().trim().split("\n")[1])); if (memoryOutput != -1) return memoryMap[memoryOutput]; - #elseif mac + #elseif (mac || ios) var process = new HiddenProcess("system_profiler", ["SPMemoryDataType"]); var reg = ~/Type: (.+)/; reg.match(process.stdout.readAll().toString()); @@ -138,6 +142,8 @@ class MemoryUtil { return line.substring("Type:".length).trim(); } } + #elseif android + // MTODO: Do get mem type for android smh? #end return "Unknown"; } diff --git a/source/funkin/backend/utils/NativeAPI.hx b/source/funkin/backend/utils/NativeAPI.hx index 78f0b38de..d5fabb52b 100644 --- a/source/funkin/backend/utils/NativeAPI.hx +++ b/source/funkin/backend/utils/NativeAPI.hx @@ -96,6 +96,10 @@ class NativeAPI { public static function showMessageBox(caption:String, message:String, icon:MessageBoxIcon = MSG_WARNING) { #if windows Windows.showMessageBox(caption, message, icon); + #elseif (ios || iphonesim || web) + trace('$caption - $message'); + #elseif android + android.Tools.showAlertDialog(caption, message, {name: "OK", func: null}, null); #else lime.app.Application.current.window.alert(message, caption); #end diff --git a/source/funkin/backend/utils/ZipUtil.hx b/source/funkin/backend/utils/ZipUtil.hx index eef06a6d7..03358a5ee 100644 --- a/source/funkin/backend/utils/ZipUtil.hx +++ b/source/funkin/backend/utils/ZipUtil.hx @@ -21,7 +21,7 @@ import sys.thread.Thread; using StringTools; // import ZipUtils; ZipUtils.uncompressZip(ZipUtils.openZip("E:\\Desktop\\test\\termination lua.ycemod"), "E:\\Desktop\\test\\uncompressed\\"); -// import ZipUtils; var e = ZipUtils.createZipFile("gjnsdghs.ycemod"); ZipUtils.writeFolderToZip(e, "./mods/Friday Night Funkin'/", "Friday Night Funkin'/"); e.flush(); e.close(); +// import ZipUtils; var e = ZipUtils.createZipFile("gjnsdghs.ycemod"); ZipUtils.writeFolderToZip(e, Sys.getCwd() + "mods/Friday Night Funkin'/", "Friday Night Funkin'/"); e.flush(); e.close(); class ZipUtil { public static var bannedNames:Array = [".git", ".gitignore", ".github", ".vscode", ".gitattributes", "readme.txt"]; diff --git a/source/funkin/backend/utils/native/Android.hx b/source/funkin/backend/utils/native/Android.hx new file mode 100644 index 000000000..4ba180f7e --- /dev/null +++ b/source/funkin/backend/utils/native/Android.hx @@ -0,0 +1,31 @@ +package funkin.backend.utils.native; + +#if android +class Android +{ + @:functionCode(' + FILE *meminfo = fopen("/proc/meminfo", "r"); + + if(meminfo == NULL) + return -1; + + char line[256]; + while(fgets(line, sizeof(line), meminfo)) + { + int ram; + if(sscanf(line, "MemTotal: %d kB", &ram) == 1) + { + fclose(meminfo); + return (ram / 1024); + } + } + + fclose(meminfo); + return -1; + ') + public static function getTotalRam():Float + { + return 0; + } +} +#end diff --git a/source/funkin/backend/utils/native/IOS.hx b/source/funkin/backend/utils/native/IOS.hx new file mode 100644 index 000000000..6f5c84962 --- /dev/null +++ b/source/funkin/backend/utils/native/IOS.hx @@ -0,0 +1,21 @@ +package funkin.backend.utils.native; + +#if ios +@:cppFileCode("#include ") +class IOS { + @:functionCode(' + int mib [] = { CTL_HW, HW_MEMSIZE }; + int64_t value = 0; + size_t length = sizeof(value); + + if(-1 == sysctl(mib, 2, &value, &length, NULL, 0)) + return -1; // An error occurred + + return value / 1024 / 1024; + ') + public static function getTotalRam():Float + { + return 0; + } +} +#end diff --git a/source/funkin/editors/DebugOptions.hx b/source/funkin/editors/DebugOptions.hx index 8dcfeda7d..d7ebdd493 100644 --- a/source/funkin/editors/DebugOptions.hx +++ b/source/funkin/editors/DebugOptions.hx @@ -26,7 +26,7 @@ class DebugOptions extends TreeMenu { class DebugOptionsScreen extends OptionsScreen { public override function new() { - super("Debug Options", "Use this menu to change debug options."); + super("Debug Options", "Use this menu to change debug options.", null, 'LEFT_FULL', 'A_B'); #if windows add(new TextOption( "Show Console", diff --git a/source/funkin/editors/EditorPicker.hx b/source/funkin/editors/EditorPicker.hx index 7056305f2..64a8dc3df 100644 --- a/source/funkin/editors/EditorPicker.hx +++ b/source/funkin/editors/EditorPicker.hx @@ -71,7 +71,9 @@ class EditorPicker extends MusicBeatSubstate { } sprites[0].selected = true; - FlxG.mouse.getScreenPosition(subCam, oldMousePos); + if (!MobileControls.mobileC) FlxG.mouse.getScreenPosition(subCam, oldMousePos); + + addVirtualPad('UP_DOWN', 'A_B'); } public override function update(elapsed:Float) { @@ -86,14 +88,14 @@ class EditorPicker extends MusicBeatSubstate { } changeSelection(-FlxG.mouse.wheel + (controls.UP_P ? -1 : 0) + (controls.DOWN_P ? 1 : 0)); - FlxG.mouse.getScreenPosition(subCam, curMousePos); - if (curMousePos.x != oldMousePos.x || curMousePos.y != oldMousePos.y) { + if (!MobileControls.mobileC) FlxG.mouse.getScreenPosition(subCam, curMousePos); + if (!MobileControls.mobileC && curMousePos.x != oldMousePos.x || curMousePos.y != oldMousePos.y) { oldMousePos.set(curMousePos.x, curMousePos.y); curSelected = -1; changeSelection(Std.int(curMousePos.y / optionHeight)+1); } - if (controls.ACCEPT || FlxG.mouse.justReleased) { + if (controls.ACCEPT || !MobileControls.mobileC && FlxG.mouse.justReleased) { if (options[curSelected].state != null) { selected = true; CoolUtil.playMenuSFX(CONFIRM); diff --git a/source/funkin/editors/SaveSubstate.hx b/source/funkin/editors/SaveSubstate.hx index 7202cf373..219b6a365 100644 --- a/source/funkin/editors/SaveSubstate.hx +++ b/source/funkin/editors/SaveSubstate.hx @@ -26,6 +26,10 @@ class SaveSubstate extends MusicBeatSubstate { public override function create() { super.create(); + #if mobile + mobile.funkin.backend.utils.SUtil.saveContent(options.defaultSaveFile.replace(options.saveExt, ''), options.saveExt, data); + close(); + #else var fileDialog = new FileDialog(); fileDialog.onCancel.add(function() close()); fileDialog.onSelect.add(function(str) { @@ -33,6 +37,7 @@ class SaveSubstate extends MusicBeatSubstate { close(); }); fileDialog.browse(SAVE, options.saveExt.getDefault(Path.extension(options.defaultSaveFile)), options.defaultSaveFile); + #end } public override function update(elapsed:Float) { diff --git a/source/funkin/editors/UIDebugState.hx b/source/funkin/editors/UIDebugState.hx index dd74c90ef..f59611f79 100644 --- a/source/funkin/editors/UIDebugState.hx +++ b/source/funkin/editors/UIDebugState.hx @@ -7,7 +7,8 @@ class UIDebugState extends UIState { public override function create() { super.create(); - FlxG.mouse.useSystemCursor = FlxG.mouse.visible = true; + FlxG.mouse.useSystemCursor = !MobileControls.mobileC; + FlxG.mouse.visible = true; var bg = new FlxSprite().makeSolid(FlxG.width, FlxG.height, 0xFF444444); bg.updateHitbox(); diff --git a/source/funkin/editors/character/CharacterEditor.hx b/source/funkin/editors/character/CharacterEditor.hx index 95da49893..933b9855d 100644 --- a/source/funkin/editors/character/CharacterEditor.hx +++ b/source/funkin/editors/character/CharacterEditor.hx @@ -296,7 +296,7 @@ class CharacterEditor extends UIState { closeCurrentContextMenu(); openContextMenu(topMenu[2].childs); } - if (FlxG.mouse.pressed) { + if (!MobileControls.mobileC && FlxG.mouse.pressed) { nextScroll.set(nextScroll.x - FlxG.mouse.deltaScreenX, nextScroll.y - FlxG.mouse.deltaScreenY); currentCursor = HAND; } else diff --git a/source/funkin/editors/character/CharacterSelection.hx b/source/funkin/editors/character/CharacterSelection.hx index 817acf294..d79b4d150 100644 --- a/source/funkin/editors/character/CharacterSelection.hx +++ b/source/funkin/editors/character/CharacterSelection.hx @@ -18,11 +18,19 @@ class CharacterSelection extends EditorTreeMenu var modsList:Array = Character.getList(true); + final button:String = MobileControls.mobileC ? 'A' : 'ACCEPT'; + var list:Array = [ for (char in (modsList.length == 0 ? Character.getList(false) : modsList)) - new IconOption(char, "Press ACCEPT to edit this character.", Character.getIconFromCharName(char), + new IconOption(char, "Press " + button + " to edit this character.", Character.getIconFromCharName(char), function() { + #if mobile + openSubState(new UIWarningSubstate("CharacterEditor: Not Supported!", "This feature isnt supported on current platform. We are sorry but you need a PC to do that.\n\n\n- Mobile Porting Team", [ + {label: "Ok", color: 0xFFFF0000, onClick: function(t) {}} + ])); + #else FlxG.switchState(new CharacterEditor(char)); + #end }) ]; @@ -32,7 +40,7 @@ class CharacterSelection extends EditorTreeMenu ])); })); - main = new OptionsScreen("Character Editor", "Select a character to edit", list); + main = new OptionsScreen("Character Editor", "Select a character to edit", list, 'UP_DOWN', 'A'); DiscordUtil.call("onEditorTreeLoaded", ["Character Editor"]); } diff --git a/source/funkin/editors/charter/CharterSelection.hx b/source/funkin/editors/charter/CharterSelection.hx index 6fd97bdcd..331539a7d 100644 --- a/source/funkin/editors/charter/CharterSelection.hx +++ b/source/funkin/editors/charter/CharterSelection.hx @@ -16,6 +16,7 @@ using StringTools; class CharterSelection extends EditorTreeMenu { public var freeplayList:FreeplaySonglist; public var curSong:ChartMetaData; + private final button:String = MobileControls.mobileC ? 'A' : 'ACCEPT'; public override function create() { bgType = "charter"; @@ -26,26 +27,44 @@ class CharterSelection extends EditorTreeMenu { freeplayList = FreeplaySonglist.get(false); var list:Array = [ - for(s in freeplayList.songs) new EditorIconOption(s.name, "Press ACCEPT to choose a difficulty to edit.", s.icon, function() { + for(s in freeplayList.songs) new EditorIconOption(s.name, "Press " + button + " to choose a difficulty to edit.", s.icon, function() { curSong = s; var list:Array = [ for(d in s.difficulties) if (d != "") - new TextOption(d, "Press ACCEPT to edit the chart for the selected difficulty", function() { + new TextOption(d, "Press " + button + " to edit the chart for the selected difficulty", function() { + #if mobile + openSubState(new UIWarningSubstate("Charter: Not Supported!", "This feature isnt supported on current platform. We are sorry but you need a PC to do that.\n\n\n- Mobile Porting Team", [ + {label: "Ok", color: 0xFFFF0000, onClick: function(t) {}} + ])); + #else FlxG.switchState(new Charter(s.name, d)); + #end }) ]; list.push(new NewOption("New Difficulty", "New Difficulty", function() { + #if mobile + openSubState(new UIWarningSubstate("New Difficulty: Not Supported!", "This feature isnt supported on current platform. We are sorry but you need a PC to do that.\n\n\n- Mobile Porting Team", [ + {label: "Ok", color: 0xFFFF0000, onClick: function(t) {}} + ])); + #else FlxG.state.openSubState(new ChartCreationScreen(saveChart)); + #end })); - optionsTree.add(new OptionsScreen(s.name, "Select a difficulty to continue.", list)); + optionsTree.add(new OptionsScreen(s.name, "Select a difficulty to continue.", list, 'UP_DOWN', 'A_B')); }, s.parsedColor.getDefault(0xFFFFFFFF)) ]; list.insert(0, new NewOption("New Song", "New Song", function() { + #if mobile + openSubState(new UIWarningSubstate("New Song: Not Supported!", "This feature isnt supported on current platform. We are sorry but you need a PC to do that.\n\n\n- Mobile Porting Team", [ + {label: "Ok", color: 0xFFFF0000, onClick: function(t) {}} + ])); + #else FlxG.state.openSubState(new SongCreationScreen(saveSong)); + #end })); - main = new OptionsScreen("Chart Editor", "Select a song to modify the charts from.", list); + main = new OptionsScreen("Chart Editor", "Select a song to modify the charts from.", list, 'UP_DOWN', 'A'); DiscordUtil.call("onEditorTreeLoaded", ["Chart Editor"]); } @@ -114,16 +133,28 @@ class CharterSelection extends EditorTreeMenu { if (creation.voicesBytes != null) sys.io.File.saveBytes('$songFolder/song/Voices.${Paths.SOUND_EXT}', creation.voicesBytes); #end - var option = new EditorIconOption(creation.meta.name, "Press ACCEPT to choose a difficulty to edit.", creation.meta.icon, function() { + var option = new EditorIconOption(creation.meta.name, "Press " + button + " to choose a difficulty to edit.", creation.meta.icon, function() { curSong = creation.meta; var list:Array = [ for(d in creation.meta.difficulties) - if (d != "") new TextOption(d, "Press ACCEPT to edit the chart for the selected difficulty", function() { + if (d != "") new TextOption(d, "Press " + button + " to edit the chart for the selected difficulty", function() { + #if mobile + openSubState(new UIWarningSubstate("Charter: Not Supported!", "This feature isnt supported on current platform. We are sorry but you need a PC to do that.\n\n\n- Mobile Porting Team", [ + {label: "Ok", color: 0xFFFF0000, onClick: function(t) {}} + ])); + #else FlxG.switchState(new Charter(creation.meta.name, d)); + #end }) ]; list.push(new NewOption("New Difficulty", "New Difficulty", function() { + #if mobile + openSubState(new UIWarningSubstate("New Difficulty: Not Supported!", "This feature isnt supported on current platform. We are sorry but you need a PC to do that.\n\n\n- Mobile Porting Team", [ + {label: "Ok", color: 0xFFFF0000, onClick: function(t) {}} + ])); + #else FlxG.state.openSubState(new ChartCreationScreen(saveChart)); + #end })); optionsTree.insert(1, new OptionsScreen(creation.meta.name, "Select a difficulty to continue.", list)); }, creation.meta.parsedColor.getDefault(0xFFFFFFFF)); @@ -151,8 +182,14 @@ class CharterSelection extends EditorTreeMenu { // Add to List curSong.difficulties.push(name); - var option = new TextOption(name, "Press ACCEPT to edit the chart for the selected difficulty", function() { + var option = new TextOption(name, "Press " + button + " to edit the chart for the selected difficulty", function() { + #if mobile + openSubState(new UIWarningSubstate("Charter: Not Supported!", "This feature isnt supported on current platform. We are sorry but you need a PC to do that.\n\n\n- Mobile Porting Team", [ + {label: "Ok", color: 0xFFFF0000, onClick: function(t) {}} + ])); + #else FlxG.switchState(new Charter(curSong.name, name)); + #end }); optionsTree.members[optionsTree.members.length-1].insert(optionsTree.members[optionsTree.members.length-1].length-1, option); diff --git a/source/funkin/editors/ui/UITextBox.hx b/source/funkin/editors/ui/UITextBox.hx index 3aa97fd27..a7aa609cd 100644 --- a/source/funkin/editors/ui/UITextBox.hx +++ b/source/funkin/editors/ui/UITextBox.hx @@ -72,6 +72,7 @@ class UITextBox extends UISliceSprite implements IUIFocusable { framesOffset = (selected ? 18 : (hovered ? 9 : 0)); @:privateAccess { if (selected) { + FlxG.stage.window.textInputEnabled = true; __wasFocused = true; caretSpr.alpha = (FlxG.game.ticks % 666) >= 333 ? 1 : 0; diff --git a/source/funkin/game/GameOverSubstate.hx b/source/funkin/game/GameOverSubstate.hx index 66b379681..fa44e5e54 100644 --- a/source/funkin/game/GameOverSubstate.hx +++ b/source/funkin/game/GameOverSubstate.hx @@ -89,6 +89,9 @@ class GameOverSubstate extends MusicBeatSubstate DiscordUtil.call("onGameOver", []); gameoverScript.call("postCreate"); + + addVirtualPad('NONE', 'A_B'); + addVirtualPadCamera(false); } override function update(elapsed:Float) diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 4e1257241..042daf43a 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -536,6 +536,7 @@ class PlayState extends MusicBeatState @:dox(hide) override public function create() { + #if mobile lime.system.System.allowScreenTimeout = false; #end Note.__customNoteTypeExists = []; // SCRIPTING & DATA INITIALIZATION #if REGION @@ -759,6 +760,12 @@ class PlayState extends MusicBeatState #end startingSong = true; + addMobileControls(); + mobileControls.visible = true; + #if !android + addVirtualPad('NONE', 'P'); + addVirtualPadCamera(false); + #end super.create(); @@ -958,6 +965,7 @@ class PlayState extends MusicBeatState public override function destroy() { scripts.call("destroy"); + #if mobile lime.system.System.allowScreenTimeout = Options.screenTimeOut; #end for(g in __cachedGraphics) g.useCount--; @:privateAccess @@ -1033,6 +1041,8 @@ class PlayState extends MusicBeatState { var event = scripts.event("onSubstateOpen", EventManager.get(StateEvent).recycle(SubState)); + #if mobile lime.system.System.allowScreenTimeout = Options.screenTimeOut; #end + if (!postCreated) MusicBeatState.skipTransIn = true; @@ -1058,6 +1068,7 @@ class PlayState extends MusicBeatState override function closeSubState() { var event = scripts.event("onSubstateClose", EventManager.get(StateEvent).recycle(subState)); + #if mobile lime.system.System.allowScreenTimeout = false; #end if (event.cancelled) return; if (paused) @@ -1237,7 +1248,7 @@ class PlayState extends MusicBeatState updateRatingStuff(); - if (controls.PAUSE && startedCountdown && canPause) + if (#if android FlxG.android.justReleased.BACK || #else virtualPad.buttonP.justPressed || #end controls.PAUSE && startedCountdown && canPause) pauseGame(); if (canAccessDebugMenus) { @@ -1440,6 +1451,7 @@ class PlayState extends MusicBeatState */ public function endSong():Void { + mobileControls.visible = false; scripts.call("onSongEnd"); canPause = false; inst.volume = 0; @@ -1481,6 +1493,7 @@ class PlayState extends MusicBeatState * Immediately switches to the next song, or goes back to the Story/Freeplay menu. */ public function nextSong() { + mobileControls.visible = false; if (isStoryMode) { campaignScore += songScore; diff --git a/source/funkin/game/cutscenes/DialogueCutscene.hx b/source/funkin/game/cutscenes/DialogueCutscene.hx index 656df6f24..8491946ff 100644 --- a/source/funkin/game/cutscenes/DialogueCutscene.hx +++ b/source/funkin/game/cutscenes/DialogueCutscene.hx @@ -126,7 +126,12 @@ class DialogueCutscene extends Cutscene { super.update(elapsed); dialogueScript.call("update", [elapsed]); - if(controls.ACCEPT) { + var justTouched:Bool = false; + for (touch in FlxG.touches.list) + if (touch.justPressed) + justTouched = true; + + if(justTouched || controls.ACCEPT) { if(dialogueBox.dialogueEnded) next(); else dialogueBox.text.skip(); } diff --git a/source/funkin/game/cutscenes/VideoCutscene.hx b/source/funkin/game/cutscenes/VideoCutscene.hx index de485450e..a6d7ef78b 100644 --- a/source/funkin/game/cutscenes/VideoCutscene.hx +++ b/source/funkin/game/cutscenes/VideoCutscene.hx @@ -92,7 +92,7 @@ class VideoCutscene extends Cutscene { // ZIP PATH: EXPORT // TODO: this but better and more ram friendly - localPath = './.temp/video-${curVideo++}.mp4'; + localPath = '.temp/video-${curVideo++}.mp4'; Main.execAsync(function() { File.saveBytes(localPath, Assets.getBytes(path)); videoReady = true; diff --git a/source/funkin/import.hx b/source/funkin/import.hx index eca238fd6..ae8943ee7 100644 --- a/source/funkin/import.hx +++ b/source/funkin/import.hx @@ -11,6 +11,9 @@ import funkin.options.Options; import funkin.game.PlayState; import funkin.backend.scripting.EventManager; +import mobile.funkin.backend.utils.SUtil; +import mobile.objects.MobileControls; + import openfl.utils.Assets; import flixel.FlxSprite; diff --git a/source/funkin/menus/BetaWarningState.hx b/source/funkin/menus/BetaWarningState.hx index 162014540..b1dafa072 100644 --- a/source/funkin/menus/BetaWarningState.hx +++ b/source/funkin/menus/BetaWarningState.hx @@ -19,7 +19,7 @@ class BetaWarningState extends MusicBeatState { disclaimer = new FunkinText(16, titleAlphabet.y + titleAlphabet.height + 10, FlxG.width - 32, "", 32); disclaimer.alignment = CENTER; - disclaimer.applyMarkup('This engine is still in a *${Main.releaseCycle}* state. That means *majority of the features* are either *buggy* or *non finished*. If you find any bugs, please report them to the Codename Engine GitHub.\n\nPress ENTER to continue', + disclaimer.applyMarkup('This engine is still in a *${Main.releaseCycle}* state. That means *majority of the features* are either *buggy* or *non finished*. If you find any bugs, please report them to the Codename Engine GitHub.\n\n${MobileControls.mobileC ? 'Tap Your Screen' : 'Press ENTER'} to continue', [ new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFFF4444), "*") ] @@ -36,6 +36,22 @@ class BetaWarningState extends MusicBeatState { public override function update(elapsed:Float) { super.update(elapsed); + #if FLX_TOUCH + for (touch in FlxG.touches.list) + { + if (touch.justPressed && transitioning) { + FlxG.camera.stopFX(); FlxG.camera.visible = false; + goToTitle(); + } else if (touch.justPressed && !transitioning) { + transitioning = true; + CoolUtil.playMenuSFX(CONFIRM); + FlxG.camera.flash(FlxColor.WHITE, 1, function() { + FlxG.camera.fade(FlxColor.BLACK, 2.5, false, goToTitle); + }); + } + } + #end + if (controls.ACCEPT && transitioning) { FlxG.camera.stopFX(); FlxG.camera.visible = false; goToTitle(); diff --git a/source/funkin/menus/FreeplayState.hx b/source/funkin/menus/FreeplayState.hx index 7ca66b022..7bc8cfa98 100644 --- a/source/funkin/menus/FreeplayState.hx +++ b/source/funkin/menus/FreeplayState.hx @@ -108,6 +108,9 @@ class FreeplayState extends MusicBeatState curSelected = k; } } + + #if mobile if (funkin.backend.assets.ModsFolder.currentModFolder == null) for (song in songs) song.difficulties = ['EASY', 'NORMAL', 'HARD']; #end // mobile temporary fix + if (songs[curSelected] != null) { for(k=>diff in songs[curSelected].difficulties) { if (diff == Options.freeplayLastDifficulty) { @@ -172,6 +175,8 @@ class FreeplayState extends MusicBeatState changeCoopMode(0, true); interpColor = new FlxInterpolateColor(bg.color); + + addVirtualPad('LEFT_FULL', 'A_B_X_Y'); } #if PRELOAD_ALL @@ -218,7 +223,7 @@ class FreeplayState extends MusicBeatState if (canSelect) { changeSelection((controls.UP_P ? -1 : 0) + (controls.DOWN_P ? 1 : 0)); changeDiff((controls.LEFT_P ? -1 : 0) + (controls.RIGHT_P ? 1 : 0)); - changeCoopMode((FlxG.keys.justPressed.TAB ? 1 : 0)); + changeCoopMode(((virtualPad.buttonX.justPressed || FlxG.keys.justPressed.TAB) ? 1 : 0)); // putting it before so that its actually smooth updateOptionsAlpha(); } @@ -256,7 +261,7 @@ class FreeplayState extends MusicBeatState } #if sys - if (FlxG.keys.justPressed.EIGHT && Sys.args().contains("-livereload")) + if (virtualPad.buttonY.justPressed || FlxG.keys.justPressed.EIGHT && Sys.args().contains("-livereload")) convertChart(); #end @@ -286,7 +291,8 @@ class FreeplayState extends MusicBeatState public function select() { updateCoopModes(); - if (songs[curSelected].difficulties.length <= 0) return; + if (songs[curSelected].difficulties.length <= 0) + return; var event = event("onSelect", EventManager.get(FreeplaySongSelectEvent).recycle(songs[curSelected].name, songs[curSelected].difficulties[curDifficulty], __opponentMode, __coopMode)); @@ -346,11 +352,12 @@ class FreeplayState extends MusicBeatState /** * Array containing all labels for Co-Op / Opponent modes. */ - public var coopLabels:Array = [ - "[TAB] Solo", - "[TAB] Opponent Mode", - "[TAB] Co-Op Mode", - "[TAB] Co-Op Mode (Switched)" + public var coopLabels:Array = MobileControls.mobileC ? ['[X] Solo', '[X] Opponent Mode'] : + [ + '[TAB] Solo', + '[TAB] Opponent Mode', + '[TAB] Co-Op Mode', + '[TAB] Co-Op Mode (Switched)' ]; /** @@ -363,7 +370,13 @@ class FreeplayState extends MusicBeatState if (!songs[curSelected].coopAllowed && !songs[curSelected].opponentModeAllowed) return; var bothEnabled = songs[curSelected].coopAllowed && songs[curSelected].opponentModeAllowed; - var event = event("onChangeCoopMode", EventManager.get(MenuChangeEvent).recycle(curCoopMode, FlxMath.wrap(curCoopMode + change, 0, bothEnabled ? 3 : 1), change)); + var changeThingy:Int = -1; + if(MobileControls.mobileC) + changeThingy = FlxMath.wrap(curCoopMode + change, 0, 1); + else + changeThingy = FlxMath.wrap(curCoopMode + change, 0, bothEnabled ? 3 : 1); + + var event = event("onChangeCoopMode", EventManager.get(MenuChangeEvent).recycle(curCoopMode, changeThingy, change)); if (event.cancelled) return; diff --git a/source/funkin/menus/GitarooPause.hx b/source/funkin/menus/GitarooPause.hx index cff8aec9f..652736572 100644 --- a/source/funkin/menus/GitarooPause.hx +++ b/source/funkin/menus/GitarooPause.hx @@ -46,6 +46,8 @@ class GitarooPause extends MusicBeatState changeThing(); super.create(); + + addVirtualPad('LEFT_RIGHT', 'A'); } override function update(elapsed:Float) diff --git a/source/funkin/menus/MainMenuState.hx b/source/funkin/menus/MainMenuState.hx index 4d30a2621..a581f053c 100644 --- a/source/funkin/menus/MainMenuState.hx +++ b/source/funkin/menus/MainMenuState.hx @@ -12,6 +12,7 @@ import lime.app.Application; import funkin.backend.scripting.events.*; import funkin.options.OptionsMenu; +import mobile.funkin.menus.MobileControlSelectSubState; using StringTools; @@ -75,13 +76,16 @@ class MainMenuState extends MusicBeatState } FlxG.camera.follow(camFollow, null, 0.06); + var modsKey:String = MobileControls.mobileC ? "M" : controls.getKeyName(SWITCHMOD); - versionText = new FunkinText(5, FlxG.height - 2, 0, 'Codename Engine v${Application.current.meta.get('version')}\nCommit ${funkin.backend.system.macros.GitCommitMacro.commitNumber} (${funkin.backend.system.macros.GitCommitMacro.commitHash})\n[${controls.getKeyName(SWITCHMOD)}] Open Mods menu\n'); + versionText = new FunkinText(5, FlxG.height - 2, 0, 'Codename Engine v${Application.current.meta.get('version')}\nCommit ${funkin.backend.system.macros.GitCommitMacro.commitNumber} (${funkin.backend.system.macros.GitCommitMacro.commitHash})\n[$modsKey}] Open Mods menu\n'); versionText.y -= versionText.height; versionText.scrollFactor.set(); add(versionText); changeItem(); + + addVirtualPad('UP_DOWN', 'A_B_M_E'); } var selectedSomethin:Bool = false; @@ -95,7 +99,7 @@ class MainMenuState extends MusicBeatState if (!selectedSomethin) { if (canAccessDebugMenus) { - if (FlxG.keys.justPressed.SEVEN) { + if (FlxG.keys.justPressed.SEVEN || virtualPad.buttonE.justPressed) { persistentUpdate = false; persistentDraw = true; openSubState(new funkin.editors.EditorPicker()); @@ -119,7 +123,7 @@ class MainMenuState extends MusicBeatState FlxG.switchState(new TitleState()); #if MOD_SUPPORT - if (controls.SWITCHMOD) { + if (controls.SWITCHMOD || virtualPad.buttonM.justPressed) { openSubState(new ModSwitchMenu()); persistentUpdate = false; persistentDraw = true; @@ -141,6 +145,12 @@ class MainMenuState extends MusicBeatState }); } + override function closeSubState() { + super.closeSubState(); + removeVirtualPad(); + addVirtualPad('UP_DOWN', 'A_B_M_E'); + } + public override function switchTo(nextState:FlxState):Bool { try { menuItems.forEach(function(spr:FlxSprite) { diff --git a/source/funkin/menus/ModSwitchMenu.hx b/source/funkin/menus/ModSwitchMenu.hx index 1b23b9d2f..fcfc38da4 100644 --- a/source/funkin/menus/ModSwitchMenu.hx +++ b/source/funkin/menus/ModSwitchMenu.hx @@ -34,6 +34,8 @@ class ModSwitchMenu extends MusicBeatSubstate { } add(alphabets); changeSelection(0, true); + + addVirtualPad('UP_DOWN', 'A_B'); } public override function update(elapsed:Float) { diff --git a/source/funkin/menus/PauseSubState.hx b/source/funkin/menus/PauseSubState.hx index fb2326e5c..79e35c044 100644 --- a/source/funkin/menus/PauseSubState.hx +++ b/source/funkin/menus/PauseSubState.hx @@ -14,6 +14,7 @@ import funkin.options.keybinds.KeybindsOptions; import funkin.menus.StoryMenuState; import funkin.backend.system.Conductor; import funkin.backend.utils.FunkinParentDisabler; +import mobile.funkin.menus.MobileControlSelectSubState; class PauseSubState extends MusicBeatSubstate { @@ -108,6 +109,9 @@ class PauseSubState extends MusicBeatSubstate pauseScript.call("postCreate"); game.updateDiscordPresence(); + + addVirtualPad('UP_DOWN', 'A'); + addVirtualPadCamera(); } override function update(elapsed:Float) @@ -150,8 +154,9 @@ class PauseSubState extends MusicBeatSubstate game.registerSmoothTransition(); FlxG.resetState(); case "Change Controls": - persistentDraw = false; - openSubState(new KeybindsOptions()); + persistentUpdate = false; + removeVirtualPad(); + openSubState(MobileControls.mobileC ? new MobileControlSelectSubState() : new KeybindsOptions()); case "Change Options": FlxG.switchState(new OptionsMenu()); case "Exit to charter": @@ -187,6 +192,14 @@ class PauseSubState extends MusicBeatSubstate super.destroy(); } + override function closeSubState() { + persistentUpdate = true; + super.closeSubState(); + removeVirtualPad(); + addVirtualPad('UP_DOWN', 'A'); + addVirtualPadCamera(); + } + function changeSelection(change:Int = 0):Void { var event = EventManager.get(MenuChangeEvent).recycle(curSelected, FlxMath.wrap(curSelected + change, 0, menuItems.length-1), change, change != 0); diff --git a/source/funkin/menus/PlaytestingWarningSubstate.hx b/source/funkin/menus/PlaytestingWarningSubstate.hx index e366bf9f1..f9b5aa469 100644 --- a/source/funkin/menus/PlaytestingWarningSubstate.hx +++ b/source/funkin/menus/PlaytestingWarningSubstate.hx @@ -68,6 +68,8 @@ class PlaytestingWarningSubstate extends MusicBeatSubstate curSelected = options.length-1; changeSelection(0); + + addVirtualPad('LEFT_RIGHT', 'A'); } var sinner:Float = 0; diff --git a/source/funkin/menus/StoryMenuState.hx b/source/funkin/menus/StoryMenuState.hx index 27d700d7a..06d469921 100644 --- a/source/funkin/menus/StoryMenuState.hx +++ b/source/funkin/menus/StoryMenuState.hx @@ -118,6 +118,8 @@ class StoryMenuState extends MusicBeatState { DiscordUtil.call("onMenuLoaded", ["Story Menu"]); CoolUtil.playMenuSong(); + + addVirtualPad('LEFT_FULL', 'A_B'); } var __lastDifficultyTween:FlxTween; diff --git a/source/funkin/menus/TitleState.hx b/source/funkin/menus/TitleState.hx index 4aca30dbf..a6e11f518 100644 --- a/source/funkin/menus/TitleState.hx +++ b/source/funkin/menus/TitleState.hx @@ -119,17 +119,13 @@ class TitleState extends MusicBeatState { if (FlxG.keys.justPressed.F) FlxG.fullscreen = !FlxG.fullscreen; - var pressedEnter:Bool = FlxG.keys.justPressed.ENTER; + var pressedEnter:Bool = controls.ACCEPT; - #if mobile - for (touch in FlxG.touches.list) - { - if (touch.justPressed) - { - pressedEnter = true; - } + if (MobileControls.mobileC) { + for (touch in FlxG.touches.list) + if (touch.justPressed) + pressedEnter = true; } - #end var gamepad:FlxGamepad = FlxG.gamepads.lastActive; diff --git a/source/funkin/menus/credits/CreditsCodename.hx b/source/funkin/menus/credits/CreditsCodename.hx index 579360edf..08da585d7 100644 --- a/source/funkin/menus/credits/CreditsCodename.hx +++ b/source/funkin/menus/credits/CreditsCodename.hx @@ -17,7 +17,7 @@ class CreditsCodename extends funkin.options.OptionsScreen { public override function new() { - super("Codename Engine", "All the contributors of the engine! - Press RESET to update the list (One reset per 2 minutes)."); + super("Codename Engine", "All the contributors of the engine! - Press RESET to update the list (One reset per 2 minutes).", null, 'UP_DOWN', 'A_B'); tryUpdating(true); } diff --git a/source/funkin/menus/credits/CreditsMain.hx b/source/funkin/menus/credits/CreditsMain.hx index 847d2b647..02143bc10 100644 --- a/source/funkin/menus/credits/CreditsMain.hx +++ b/source/funkin/menus/credits/CreditsMain.hx @@ -42,7 +42,8 @@ class CreditsMain extends TreeMenu { CoolUtil.openURL("https://ninja-muffin24.itch.io/funkin"); })); - main = new OptionsScreen('Credits', 'The people who made this possible!', items); + main = new OptionsScreen('Credits', 'The people who made this possible!', items, 'UP_DOWN', 'A_B'); + super.create(); DiscordUtil.call("onMenuLoaded", ["Credits Menu"]); @@ -96,7 +97,7 @@ class CreditsMain extends TreeMenu { case "menu": credsMenus.push(new TextOption(name + " >", desc, function() { - optionsTree.add(new OptionsScreen(name, desc, parseCreditsFromXML(node, source))); + optionsTree.add(new OptionsScreen(name, desc, parseCreditsFromXML(node, source), 'UP_DOWN', 'A_B')); })); } } diff --git a/source/funkin/options/Options.hx b/source/funkin/options/Options.hx index a9bf72ac1..81b50db84 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -33,15 +33,25 @@ class Options public static var splashesEnabled:Bool = true; public static var hitWindow:Float = 250; public static var songOffset:Float = 0; - public static var framerate:Int = 120; - public static var gpuOnlyBitmaps:Bool = #if (mac || web) false #else true #end; // causes issues on mac and web + public static var framerate:Int = #if !mobile 120 #else 60 #end; + public static var gpuOnlyBitmaps:Bool = #if (mac || web || mobile) false #else true #end; // causes issues on mac, web and mobile public static var lastLoadedMod:String = null; + // mobile options + #if mobile + public static var screenTimeOut:Bool = false; + public static var wideScreen:Bool = false; + #end + public static var hideHitbox:Bool = false; + public static var hitboxType:String = 'gradient'; + public static var controlsAlpha:Float = FlxG.onMobile ? 0.6 : 0; + #if android public static var storageType:String = "EXTERNAL_DATA"; #end + /** * EDITORS SETTINGS */ - public static var intensiveBlur:Bool = true; + public static var intensiveBlur:Bool = #if mobile false #else true #end; public static var editorSFX:Bool = true; public static var editorPrettyPrint:Bool = false; public static var maxUndos:Int = 120; diff --git a/source/funkin/options/OptionsMenu.hx b/source/funkin/options/OptionsMenu.hx index 497bfd9b9..d36aef62b 100644 --- a/source/funkin/options/OptionsMenu.hx +++ b/source/funkin/options/OptionsMenu.hx @@ -5,6 +5,8 @@ import haxe.xml.Access; import funkin.options.type.*; import funkin.options.categories.*; import funkin.options.TreeMenu; +import haxe.ds.Map; +import mobile.flixel.FlxVirtualPad; class OptionsMenu extends TreeMenu { public static var mainOptions:Array = [ @@ -24,6 +26,11 @@ class OptionsMenu extends TreeMenu { desc: 'Change Appearance options such as Flashing menus...', state: AppearanceOptions }, + { + name: 'Mobile Options >', + desc: 'Change Options Related To Mobile & Mobile Controls', + state: MobileOptions + }, { name: 'Miscellaneous >', desc: 'Use this menu to reset save data or engine settings.', @@ -80,7 +87,8 @@ class OptionsMenu extends TreeMenu { main.add(o); } } - + addVirtualPad('UP_DOWN', 'A_B'); + addVirtualPadCamera(false); } public override function exit() { @@ -92,6 +100,7 @@ class OptionsMenu extends TreeMenu { /** * XML STUFF */ + var vpadMap:Map> = new Map(); public function parseOptionsFromXML(xml:Access):Array { var options:Array = []; @@ -136,8 +145,14 @@ class OptionsMenu extends TreeMenu { case "menu": options.push(new TextOption(name + " >", desc, function() { - optionsTree.add(new OptionsScreen(name, desc, parseOptionsFromXML(node))); + optionsTree.add(new OptionsScreen(name, desc, parseOptionsFromXML(node), vpadMap.exists(name) ? vpadMap.get(name)[0] : 'NONE', vpadMap.exists(name) ? vpadMap.get(name)[1] : 'NONE')); })); + case "virtualPad": + var arr = [ + node.getAtt("dpadMode") == null ? MusicBeatState.instance.virtualPad.curDPadMode.getName() : node.getAtt("dpadMode"), + node.getAtt("actionMode") == null ? MusicBeatState.instance.virtualPad.curActionMode.getName() : node.getAtt("actionMode") + ]; + vpadMap.set(node.getAtt("menuName"), arr); } } diff --git a/source/funkin/options/OptionsScreen.hx b/source/funkin/options/OptionsScreen.hx index 455808215..b25474792 100644 --- a/source/funkin/options/OptionsScreen.hx +++ b/source/funkin/options/OptionsScreen.hx @@ -1,6 +1,7 @@ package funkin.options; import funkin.options.type.OptionType; +import mobile.objects.MobileControls; class OptionsScreen extends FlxTypedSpriteGroup { public static var optionHeight:Float = 120; @@ -15,19 +16,30 @@ class OptionsScreen extends FlxTypedSpriteGroup { public var name:String; public var desc:String; - public function new(name:String, desc:String, ?options:Array) { + public var dpadMode:String = 'NONE'; + public var actionMode:String = 'NONE'; + public var prevVPadModes:Array = []; + + public function new(name:String, desc:String, ?options:Array, dpadMode:String = 'NONE', actionMode:String = 'NONE') { super(); this.name = name; this.desc = desc; if (options != null) for(o in options) add(o); + if(MusicBeatState.instance.virtualPad != null) + prevVPadModes = [MusicBeatState.instance.virtualPad.curDPadMode.getName(), MusicBeatState.instance.virtualPad.curActionMode.getName()]; + this.dpadMode = dpadMode; + this.actionMode = actionMode; + MusicBeatState.instance.removeVirtualPad(); + MusicBeatState.instance.addVirtualPad(dpadMode, actionMode); + MusicBeatState.instance.addVirtualPadCamera(false); } public override function update(elapsed:Float) { super.update(elapsed); var controls = PlayerSettings.solo.controls; - - changeSelection((controls.UP_P ? -1 : 0) + (controls.DOWN_P ? 1 : 0) - FlxG.mouse.wheel); + var wheel = FlxG.mouse.wheel; + changeSelection((controls.UP_P ? -1 : 0) + (controls.DOWN_P ? 1 : 0) - wheel); x = id * FlxG.width; for(k=>option in members) { if(option == null) continue; @@ -45,19 +57,24 @@ class OptionsScreen extends FlxTypedSpriteGroup { if (members.length > 0) { members[curSelected].selected = true; - if (controls.ACCEPT || FlxG.mouse.justReleased) + if (controls.ACCEPT || (FlxG.mouse.justReleased && !MobileControls.mobileC)) members[curSelected].onSelect(); if (controls.LEFT_P) members[curSelected].onChangeSelection(-1); if (controls.RIGHT_P) members[curSelected].onChangeSelection(1); } - if (controls.BACK || FlxG.mouse.justReleasedRight) + if (controls.BACK || (FlxG.mouse.justReleasedRight && !MobileControls.mobileC)) close(); } public function close() { onClose(this); + if(prevVPadModes.length > 0){ + MusicBeatState.instance.removeVirtualPad(); + MusicBeatState.instance.addVirtualPad(prevVPadModes[0], prevVPadModes[1]); + MusicBeatState.instance.addVirtualPadCamera(false); + } } public function changeSelection(sel:Int, force:Bool = false) { diff --git a/source/funkin/options/categories/AppearanceOptions.hx b/source/funkin/options/categories/AppearanceOptions.hx index 926213431..de9ab5c52 100644 --- a/source/funkin/options/categories/AppearanceOptions.hx +++ b/source/funkin/options/categories/AppearanceOptions.hx @@ -2,7 +2,7 @@ package funkin.options.categories; class AppearanceOptions extends OptionsScreen { public override function new() { - super("Appearance", "Change Appearance options such as Flashing menus..."); + super("Appearance", "Change Appearance options such as Flashing menus...", null, 'LEFT_FULL', 'A_B'); add(new NumOption( "Framerate", "Pretty self explanatory, isn't it?", diff --git a/source/funkin/options/categories/GameplayOptions.hx b/source/funkin/options/categories/GameplayOptions.hx index 15c0ba603..92e2b5fae 100644 --- a/source/funkin/options/categories/GameplayOptions.hx +++ b/source/funkin/options/categories/GameplayOptions.hx @@ -9,7 +9,7 @@ class GameplayOptions extends OptionsScreen { var offsetSetting:NumOption; public override function new() { - super("Gameplay", 'Change Gameplay options such as Downscroll, Scroll Speed, Naughtyness...'); + super("Gameplay", 'Change Gameplay options such as Downscroll, Scroll Speed, Naughtyness...', null, 'LEFT_FULL', 'A_B'); add(new Checkbox( "Downscroll", "If checked, notes will go from up to down instead of down to up, as if they're falling.", diff --git a/source/funkin/options/categories/MiscOptions.hx b/source/funkin/options/categories/MiscOptions.hx index 2aecb20fd..5da7dd85f 100644 --- a/source/funkin/options/categories/MiscOptions.hx +++ b/source/funkin/options/categories/MiscOptions.hx @@ -3,7 +3,9 @@ package funkin.options.categories; class MiscOptions extends OptionsScreen { public override function new() { - super("Miscellaneous", "Use this menu to reset save data or engine settings."); + dpadMode = 'NONE'; + actionMode = 'A_B'; + super("Miscellaneous", "Use this menu to reset save data or engine settings.", null, #if UPDATE_CHECKING 'UP_DOWN' #else 'NONE' #end, 'A_B'); #if UPDATE_CHECKING add(new Checkbox( "Enable Nightly Updates", diff --git a/source/funkin/options/categories/MobileOptions.hx b/source/funkin/options/categories/MobileOptions.hx new file mode 100644 index 000000000..414a12bb7 --- /dev/null +++ b/source/funkin/options/categories/MobileOptions.hx @@ -0,0 +1,134 @@ +package funkin.options.categories; + +import flixel.FlxG; +import flixel.input.keyboard.FlxKey; +import flixel.util.FlxTimer; +import funkin.backend.MusicBeatState; +import funkin.options.Options; +import lime.system.System as LimeSystem; +#if android +import mobile.funkin.backend.utils.SUtil; +#end +#if sys +import sys.io.File; +#end + +class MobileOptions extends OptionsScreen { + var canEnter:Bool = true; + #if android + final lastStorageType:String = Options.storageType; + var externalPaths:Array = SUtil.checkExternalPaths(true); + var typeNames:Array = ['Data', 'Obb', 'Media', 'External']; + var typeVars:Array = ['EXTERNAL_DATA', 'EXTERNAL_OBB', 'EXTERNAL_MEDIA', 'EXTERNAL']; + #end + + public override function new() { + #if android + if (!externalPaths.contains('\n')) + { + typeNames = typeNames.concat(externalPaths); + typeVars = typeVars.concat(externalPaths); + } + #end + dpadMode = 'LEFT_FULL'; + actionMode = 'A_B'; + super("Mobile", 'Change Mobile Related Things such as Controls alpha, screen timeout....', null, 'LEFT_FULL', 'A_B'); + add(new TextOption( + "Mobile Controls", + "Choose which control to play with (hitbox, vpad left, vpad right, custom...).", + openMobileControlsMenu)); + add(new NumOption( + "Controls Alpha", + "Change how transparent the mobile controls should be", + 0.0, // minimum + 1.0, // maximum + 0.1, // change + "controlsAlpha", // save name or smth + changeControlsAlpha)); // callback + add(new ArrayOption( + "Hitbox Design", + "Choose how your hitbox should look like!", + ['gradient', 'noGradient', 'hidden'], + ['Gradient', 'No Gradient', 'Hidden'], + 'hitboxType')); + #if mobile + add(new Checkbox( + "Allow Screen Timeout", + "If checked, The phone will enter sleep mode if the player is inactive.", + "screenTimeOut")); + add(new Checkbox( + "Wide Screen", + "If checked, It'll change aspect ratio of the game.", + "wideScreen")); + #end + #if android + add(new ArrayOption( + "Storage Type", + "Choose which folder Codename Engine should use! (CHANGING THIS MAKES DELETE YOUR OLD FOLDER!!)", + typeVars, + typeNames, + 'storageType')); + #end + } + + override function update(elapsed) { + #if mobile + final lastScreenTimeOut:Bool = Options.screenTimeOut; + final lastWideScreen:Bool = Options.wideScreen; + if (lastScreenTimeOut != Options.screenTimeOut) LimeSystem.allowScreenTimeout = Options.screenTimeOut; + if (lastWideScreen != Options.wideScreen) FlxG.scaleMode = new mobile.funkin.backend.system.MobileRatioScaleMode(); + #end + super.update(elapsed); + } + + override public function destroy() { + #if android + if (lastStorageType != Options.storageType) { + onStorageChange(); + funkin.backend.utils.NativeAPI.showMessageBox('Notice!', 'Storage Type has been changed and you needed restart the game!!\nPress OK to close the game.'); + LimeSystem.exit(0); + } + #end + } + + function changeControlsAlpha(alpha) { + MusicBeatState.instance.virtualPad.alpha = alpha; + if (mobile.objects.MobileControls.mobileC) { + FlxG.sound.volumeUpKeys = []; + FlxG.sound.volumeDownKeys = []; + FlxG.sound.muteKeys = []; + } else { + FlxG.sound.volumeUpKeys = [FlxKey.PLUS, FlxKey.NUMPADPLUS]; + FlxG.sound.volumeDownKeys = [FlxKey.MINUS, FlxKey.NUMPADMINUS]; + FlxG.sound.muteKeys = [FlxKey.ZERO, FlxKey.NUMPADZERO]; + } + } + + function openMobileControlsMenu() { + if(!canEnter) return; + canEnter = false; + FlxG.state.persistentUpdate = false; + MusicBeatState.instance.camVPad.visible = false; + FlxG.state.openSubState(new mobile.funkin.menus.MobileControlSelectSubState(() -> { + MusicBeatState.instance.camVPad.visible = true; + FlxG.state.persistentUpdate = true; + new FlxTimer().start(0.2, (tmr:FlxTimer) -> canEnter = true); + })); + } + + #if android + function onStorageChange():Void + { + File.saveContent(LimeSystem.applicationStorageDirectory + 'storagetype.txt', Options.storageType); + + var lastStoragePath:String = StorageType.fromStrForce(lastStorageType) + '/'; + + try + { + Sys.command('rm', ['-rf', lastStoragePath]); + } + catch (e:haxe.Exception) + trace('Failed to remove last directory. (${e.message})'); + } + #end +} diff --git a/source/funkin/options/keybinds/KeybindsOptions.hx b/source/funkin/options/keybinds/KeybindsOptions.hx index e4e7d26cd..0d5af6c33 100644 --- a/source/funkin/options/keybinds/KeybindsOptions.hx +++ b/source/funkin/options/keybinds/KeybindsOptions.hx @@ -162,6 +162,9 @@ class KeybindsOptions extends MusicBeatSubstate { } add(alphabets); add(camFollow); + + addVirtualPad('LEFT_FULL', 'A_B'); + addVirtualPadCamera(); } public override function destroy() { diff --git a/source/mobile/flixel/FlxButton.hx b/source/mobile/flixel/FlxButton.hx new file mode 100644 index 000000000..f543a2ee8 --- /dev/null +++ b/source/mobile/flixel/FlxButton.hx @@ -0,0 +1,587 @@ +package mobile.flixel; + +import flixel.FlxCamera; +import flixel.FlxG; +import flixel.FlxSprite; +import flixel.graphics.atlas.FlxAtlas; +import flixel.graphics.atlas.FlxNode; +import flixel.graphics.frames.FlxTileFrames; +import flixel.input.FlxInput; +import flixel.input.FlxPointer; +import flixel.input.IFlxInput; +import flixel.input.touch.FlxTouch; +import flixel.math.FlxPoint; +import flixel.sound.FlxSound; +import flixel.text.FlxText; +import flixel.util.FlxDestroyUtil; + +/** + * A simple button class that calls a function when clicked by the touch. + */ +class FlxButton extends FlxTypedButton +{ + /** + * Used with public variable status, means not highlighted or pressed. + */ + public static inline var NORMAL:Int = 0; + + /** + * Used with public variable status, means highlighted (usually from touch over). + */ + public static inline var HIGHLIGHT:Int = 1; + + /** + * Used with public variable status, means pressed (usually from touch click). + */ + public static inline var PRESSED:Int = 2; + + /** + * Shortcut to setting label.text + */ + public var text(get, set):String; + + /** + * Creates a new `FlxButton` object with a gray background + * and a callback function on the UI thread. + * + * @param X The x position of the button. + * @param Y The y position of the button. + * @param Text The text that you want to appear on the button. + * @param OnClick The function to call whenever the button is clicked. + */ + public function new(X:Float = 0, Y:Float = 0, ?Text:String, ?OnClick:Void->Void):Void + { + super(X, Y, OnClick); + + for (point in labelOffsets) + point.set(point.x - 1, point.y + 3); + + initLabel(Text); + } + + /** + * Updates the size of the text field to match the button. + */ + override function resetHelpers():Void + { + super.resetHelpers(); + + if (label != null) + { + label.fieldWidth = label.frameWidth = Std.int(width); + label.size = label.size; // Calls set_size(), don't remove! + } + } + + inline function initLabel(Text:String):Void + { + if (Text != null) + { + label = new FlxText(x + labelOffsets[NORMAL].x, y + labelOffsets[NORMAL].y, 80, Text); + label.setFormat(null, 8, 0x333333, 'center'); + label.alpha = labelAlphas[status]; + label.drawFrame(true); + } + } + + inline function get_text():String + { + return (label != null) ? label.text : null; + } + + inline function set_text(Text:String):String + { + if (label == null) + initLabel(Text); + else + label.text = Text; + return Text; + } +} + +/** + * A simple button class that calls a function when clicked by the touch. + */ +#if !display +@:generic +#end +class FlxTypedButton extends FlxSprite implements IFlxInput +{ + /** + * The label that appears on the button. Can be any `FlxSprite`. + */ + public var label(default, set):T; + + /** + * What offsets the `label` should have for each status. + */ + public var labelOffsets:Array = [FlxPoint.get(), FlxPoint.get(), FlxPoint.get(0, 1)]; + + /** + * What alpha value the label should have for each status. Default is `[0.8, 1.0, 0.5]`. + * Multiplied with the button's `alpha`. + */ + public var labelAlphas:Array = [0.8, 1.0, 0.5]; + + /** + * What animation should be played for each status. + * Default is ['normal', 'highlight', 'pressed']. + */ + public var statusAnimations:Array = ['normal', 'highlight', 'pressed']; + + /** + * Whether you can press the button simply by releasing the touch button over it (default). + * If false, the input has to be pressed while hovering over the button. + */ + public var allowSwiping:Bool = true; + + /** + * Whether the button can use multiple fingers on it. + */ + public var multiTouch:Bool = false; + + /** + * Maximum distance a pointer can move to still trigger event handlers. + * If it moves beyond this limit, onOut is triggered. + * Defaults to `Math.POSITIVE_INFINITY` (i.e. no limit). + */ + public var maxInputMovement:Float = Math.POSITIVE_INFINITY; + + /** + * Shows the current state of the button, either `FlxButton.NORMAL`, + * `FlxButton.HIGHLIGHT` or `FlxButton.PRESSED`. + */ + public var status(default, set):Int; + + /** + * The properties of this button's `onUp` event (callback function, sound). + */ + public var onUp(default, null):FlxButtonEvent; + + /** + * The properties of this button's `onDown` event (callback function, sound). + */ + public var onDown(default, null):FlxButtonEvent; + + /** + * The properties of this button's `onOver` event (callback function, sound). + */ + public var onOver(default, null):FlxButtonEvent; + + /** + * The properties of this button's `onOut` event (callback function, sound). + */ + public var onOut(default, null):FlxButtonEvent; + + public var justReleased(get, never):Bool; + public var released(get, never):Bool; + public var pressed(get, never):Bool; + public var justPressed(get, never):Bool; + + /** + * We cast label to a `FlxSprite` for internal operations to avoid Dynamic casts in C++ + */ + var _spriteLabel:FlxSprite; + + /** + * We don't need an ID here, so let's just use `Int` as the type. + */ + var input:FlxInput; + + /** + * The input currently pressing this button, if none, it's `null`. Needed to check for its release. + */ + var currentInput:IFlxInput; + + var lastStatus = -1; + + /** + * Creates a new `FlxTypedButton` object with a gray background. + * + * @param X The x position of the button. + * @param Y The y position of the button. + * @param OnClick The function to call whenever the button is clicked. + */ + public function new(X:Float = 0, Y:Float = 0, ?OnClick:Void->Void):Void + { + super(X, Y); + + loadDefaultGraphic(); + + onUp = new FlxButtonEvent(OnClick); + onDown = new FlxButtonEvent(); + onOver = new FlxButtonEvent(); + onOut = new FlxButtonEvent(); + + status = multiTouch ? FlxButton.NORMAL : FlxButton.HIGHLIGHT; + + // Since this is a UI element, the default scrollFactor is (0, 0) + scrollFactor.set(); + + statusAnimations[FlxButton.HIGHLIGHT] = 'normal'; + labelAlphas[FlxButton.HIGHLIGHT] = 1; + + input = new FlxInput(0); + } + + override public function graphicLoaded():Void + { + super.graphicLoaded(); + + setupAnimation('normal', FlxButton.NORMAL); + setupAnimation('pressed', FlxButton.PRESSED); + } + + function loadDefaultGraphic():Void + loadGraphic('flixel/images/ui/button.png', true, 80, 20); + + function setupAnimation(animationName:String, frameIndex:Int):Void + { + // make sure the animation doesn't contain an invalid frame + frameIndex = Std.int(Math.min(frameIndex, #if (flixel < "5.3.0") animation.frames #else animation.numFrames #end - 1)); + animation.add(animationName, [frameIndex]); + } + + /** + * Called by the game state when state is changed (if this object belongs to the state) + */ + override public function destroy():Void + { + label = FlxDestroyUtil.destroy(label); + _spriteLabel = null; + + onUp = FlxDestroyUtil.destroy(onUp); + onDown = FlxDestroyUtil.destroy(onDown); + onOver = FlxDestroyUtil.destroy(onOver); + onOut = FlxDestroyUtil.destroy(onOut); + + labelOffsets = FlxDestroyUtil.putArray(labelOffsets); + + labelAlphas = null; + currentInput = null; + input = null; + + super.destroy(); + } + + /** + * Called by the game loop automatically, handles touch over and click detection. + */ + override public function update(elapsed:Float):Void + { + super.update(elapsed); + + if (visible) + { + // Update the button, but only if at least either touches are enabled + #if FLX_POINTER_INPUT + updateButton(); + #end + + // Trigger the animation only if the button's input status changes. + if (lastStatus != status) + { + updateStatusAnimation(); + lastStatus = status; + } + } + + input.update(); + } + + function updateStatusAnimation():Void + animation.play(statusAnimations[status]); + + /** + * Just draws the button graphic and text label to the screen. + */ + override public function draw():Void + { + super.draw(); + + if (_spriteLabel != null && _spriteLabel.visible) + { + _spriteLabel.cameras = cameras; + _spriteLabel.draw(); + } + } + + #if FLX_DEBUG + /** + * Helper function to draw the debug graphic for the label as well. + */ + override public function drawDebug():Void + { + super.drawDebug(); + + if (_spriteLabel != null) + _spriteLabel.drawDebug(); + } + #end + + /** + * Stamps button's graphic and label onto specified atlas object and loads graphic from this atlas. + * This method assumes that you're using whole image for button's graphic and image has no spaces between frames. + * And it assumes that label is a single frame sprite. + * + * @param atlas Atlas to stamp graphic to. + * @return Whether the button's graphic and label's graphic were stamped on the atlas successfully. + */ + public function stampOnAtlas(atlas:FlxAtlas):Bool + { + var buttonNode:FlxNode = atlas.addNode(graphic.bitmap, graphic.key); + var result:Bool = (buttonNode != null); + + if (buttonNode != null) + { + var buttonFrames:FlxTileFrames = cast frames; + var tileSize:FlxPoint = FlxPoint.get(buttonFrames.tileSize.x, buttonFrames.tileSize.y); + var tileFrames:FlxTileFrames = buttonNode.getTileFrames(tileSize); + this.frames = tileFrames; + } + + if (result && label != null) + { + var labelNode:FlxNode = atlas.addNode(label.graphic.bitmap, label.graphic.key); + result = result && (labelNode != null); + + if (labelNode != null) + label.frames = labelNode.getImageFrame(); + } + + return result; + } + + /** + * Basic button update logic - searches for overlaps with touches and + * the touch and calls `updateStatus()`. + */ + function updateButton():Void + { + var overlapFound = checkTouchOverlap(); + + if (currentInput != null && currentInput.justReleased && overlapFound) + onUpHandler(); + + if (status != FlxButton.NORMAL && (!overlapFound || (currentInput != null && currentInput.justReleased))) + onOutHandler(); + } + + function checkTouchOverlap():Bool + { + var overlap = false; + + for (camera in cameras) + for (touch in FlxG.touches.list) + if (checkInput(touch, touch, touch.justPressedPosition, camera)) + overlap = true; + + return overlap; + } + + function checkInput(pointer:FlxPointer, input:IFlxInput, justPressedPosition:FlxPoint, camera:FlxCamera):Bool + { + if (maxInputMovement != Math.POSITIVE_INFINITY + && justPressedPosition.distanceTo(pointer.getScreenPosition(FlxPoint.weak())) > maxInputMovement + && input == currentInput) + { + currentInput = null; + } + else if (overlapsPoint(pointer.getWorldPosition(camera, _point), true, camera)) + { + updateStatus(input); + return true; + } + + return false; + } + + /** + * Updates the button status by calling the respective event handler function. + */ + function updateStatus(input:IFlxInput):Void + { + if (input.justPressed) + { + currentInput = input; + onDownHandler(); + } + else if (status == FlxButton.NORMAL) + { + // Allow 'swiping' to press a button (dragging it over the button while pressed) + if (allowSwiping && input.pressed) + onDownHandler(); + else + onOverHandler(); + } + } + + function updateLabelPosition() + { + if (_spriteLabel != null) // Label positioning + { + _spriteLabel.x = (pixelPerfectPosition ? Math.floor(x) : x) + labelOffsets[status].x; + _spriteLabel.y = (pixelPerfectPosition ? Math.floor(y) : y) + labelOffsets[status].y; + } + } + + function updateLabelAlpha() + { + if (_spriteLabel != null && labelAlphas.length > status) + _spriteLabel.alpha = alpha * labelAlphas[status]; + } + + /** + * Internal function that handles the onUp event. + */ + function onUpHandler():Void + { + status = FlxButton.NORMAL; + input.release(); + currentInput = null; + onUp.fire(); // Order matters here, because onUp.fire() could cause a state change and destroy this object. + } + + /** + * Internal function that handles the onDown event. + */ + function onDownHandler():Void + { + status = FlxButton.PRESSED; + input.press(); + onDown.fire(); // Order matters here, because onDown.fire() could cause a state change and destroy this object. + } + + /** + * Internal function that handles the onOver event. + */ + function onOverHandler():Void + { + status = FlxButton.HIGHLIGHT; + onOver.fire(); // Order matters here, because onOver.fire() could cause a state change and destroy this object. + } + + /** + * Internal function that handles the onOut event. + */ + function onOutHandler():Void + { + status = FlxButton.NORMAL; + input.release(); + onOut.fire(); // Order matters here, because onOut.fire() could cause a state change and destroy this object. + } + + function set_label(Value:T):T + { + if (Value != null) + { + // use the same FlxPoint object for both + Value.scrollFactor.put(); + Value.scrollFactor = scrollFactor; + } + + label = Value; + _spriteLabel = label; + + updateLabelPosition(); + + return Value; + } + + function set_status(Value:Int):Int + { + status = Value; + updateLabelAlpha(); + return status; + } + + override function set_alpha(Value:Float):Float + { + super.set_alpha(Value); + updateLabelAlpha(); + return alpha; + } + + override function set_x(Value:Float):Float + { + super.set_x(Value); + updateLabelPosition(); + return x; + } + + override function set_y(Value:Float):Float + { + super.set_y(Value); + updateLabelPosition(); + return y; + } + + inline function get_justReleased():Bool + return input.justReleased; + + inline function get_released():Bool + return input.released; + + inline function get_pressed():Bool + return input.pressed; + + inline function get_justPressed():Bool + return input.justPressed; +} + +/** + * Helper function for `FlxButton` which handles its events. + */ +private class FlxButtonEvent implements IFlxDestroyable +{ + /** + * The callback function to call when this even fires. + */ + public var callback:Void->Void; + + #if FLX_SOUND_SYSTEM + /** + * The sound to play when this event fires. + */ + public var sound:FlxSound; + #end + + /** + * @param Callback The callback function to call when this even fires. + * @param sound The sound to play when this event fires. + */ + public function new(?Callback:Void->Void, ?sound:FlxSound):Void + { + callback = Callback; + + #if FLX_SOUND_SYSTEM + this.sound = sound; + #end + } + + /** + * Cleans up memory. + */ + public inline function destroy():Void + { + callback = null; + + #if FLX_SOUND_SYSTEM + sound = FlxDestroyUtil.destroy(sound); + #end + } + + /** + * Fires this event (calls the callback and plays the sound) + */ + public inline function fire():Void + { + if (callback != null) + callback(); + + #if FLX_SOUND_SYSTEM + if (sound != null) + sound.play(true); + #end + } +} \ No newline at end of file diff --git a/source/mobile/flixel/FlxVirtualPad.hx b/source/mobile/flixel/FlxVirtualPad.hx new file mode 100644 index 000000000..3a4fea9d4 --- /dev/null +++ b/source/mobile/flixel/FlxVirtualPad.hx @@ -0,0 +1,260 @@ +package mobile.flixel; + +import flixel.FlxG; +#if MOD_SUPPORT +import sys.FileSystem; +#end +import flixel.math.FlxPoint; +import funkin.options.Options; +import mobile.flixel.FlxButton; +import openfl.display.BitmapData; +import flixel.util.FlxDestroyUtil; +import flixel.graphics.FlxGraphic; +import funkin.backend.assets.Paths; +import mobile.objects.FlxButtonGroup; +import flixel.graphics.frames.FlxTileFrames; +import flixel.graphics.frames.FlxAtlasFrames; +import openfl.utils.Assets; +import haxe.ds.Map; +import flixel.util.typeLimit.OneOfTwo; + +enum FlxDPadMode +{ + UP_DOWN; + LEFT_RIGHT; + LEFT_FULL; + RIGHT_FULL; + NONE; +} + +enum FlxActionMode +{ + A; + B; + P; + A_B; + A_B_C; + A_B_E; + A_B_X_Y; + A_B_M_E; + A_B_C_X_Y; + A_B_C_X_Y_Z; + A_B_C_D_V_X_Y_Z; + NONE; +} + +/** + * A highly modified FlxVirtualPad. + * It's easy to customize the layout. + * + * @author Ka Wing Chin + * @author Mihai Alexandru (M.A. Jigsaw) + */ +class FlxVirtualPad extends FlxButtonGroup +{ + public var buttonLeft:FlxButton = new FlxButton(0, 0); + public var buttonUp:FlxButton = new FlxButton(0, 0); + public var buttonRight:FlxButton = new FlxButton(0, 0); + public var buttonDown:FlxButton = new FlxButton(0, 0); + public var buttonLeft2:FlxButton = new FlxButton(0, 0); + public var buttonUp2:FlxButton = new FlxButton(0, 0); + public var buttonRight2:FlxButton = new FlxButton(0, 0); + public var buttonDown2:FlxButton = new FlxButton(0, 0); + public var buttonA:FlxButton = new FlxButton(0, 0); + public var buttonB:FlxButton = new FlxButton(0, 0); + public var buttonC:FlxButton = new FlxButton(0, 0); + public var buttonD:FlxButton = new FlxButton(0, 0); + public var buttonE:FlxButton = new FlxButton(0, 0); + public var buttonF:FlxButton = new FlxButton(0, 0); + public var buttonG:FlxButton = new FlxButton(0, 0); + public var buttonH:FlxButton = new FlxButton(0, 0); + public var buttonI:FlxButton = new FlxButton(0, 0); + public var buttonJ:FlxButton = new FlxButton(0, 0); + public var buttonK:FlxButton = new FlxButton(0, 0); + public var buttonL:FlxButton = new FlxButton(0, 0); + public var buttonM:FlxButton = new FlxButton(0, 0); + public var buttonN:FlxButton = new FlxButton(0, 0); + public var buttonO:FlxButton = new FlxButton(0, 0); + public var buttonP:FlxButton = new FlxButton(0, 0); + public var buttonQ:FlxButton = new FlxButton(0, 0); + public var buttonR:FlxButton = new FlxButton(0, 0); + public var buttonS:FlxButton = new FlxButton(0, 0); + public var buttonT:FlxButton = new FlxButton(0, 0); + public var buttonU:FlxButton = new FlxButton(0, 0); + public var buttonV:FlxButton = new FlxButton(0, 0); + public var buttonW:FlxButton = new FlxButton(0, 0); + public var buttonX:FlxButton = new FlxButton(0, 0); + public var buttonY:FlxButton = new FlxButton(0, 0); + public var buttonZ:FlxButton = new FlxButton(0, 0); + + public var curDPadMode:FlxDPadMode = NONE; + public var curActionMode:FlxActionMode = NONE; + public static var dpadModes:Map; + public static var actionModes:Map; + + /** + * Create a gamepad. + * + * @param FlxDPadMode The D-Pad mode. `LEFT_FULL` for example. + * @param FlxActionMode The action buttons mode. `A_B_C` for example. + */ + public function new(DPad:OneOfTwo, Action:OneOfTwo) + { + super(); + var dpadMode:FlxDPadMode; + var actionMode:FlxActionMode; + + if(DPad is FlxDPadMode) + dpadMode = cast DPad; + else + dpadMode = cast getDPadModeByString(cast DPad); + + if(Action is FlxActionMode) + actionMode = cast DPad; + else + actionMode = cast getActionModeByString(cast Action); + curDPadMode = dpadMode; + curActionMode = actionMode; + switch (dpadMode) + { + case UP_DOWN: + add(buttonUp = createButton(0, FlxG.height - 258, 'up', 0x00FF00)); + add(buttonDown = createButton(0, FlxG.height - 131, 'down', 0x00FFFF)); + case LEFT_RIGHT: + add(buttonLeft = createButton(0, FlxG.height - 131, 'left', 0xFF00FF)); + add(buttonRight = createButton(127, FlxG.height - 131, 'right', 0xFF0000)); + case LEFT_FULL: + add(buttonUp = createButton(105, FlxG.height - 356, 'up', 0x00FF00)); + add(buttonLeft = createButton(0, FlxG.height - 246, 'left', 0xFF00FF)); + add(buttonRight = createButton(207, FlxG.height - 246, 'right', 0xFF0000)); + add(buttonDown = createButton(105, FlxG.height - 131, 'down', 0x00FFFF)); + case RIGHT_FULL: + add(buttonUp = createButton(FlxG.width - 258, FlxG.height - 404, 'up', 0x00FF00)); + add(buttonLeft = createButton(FlxG.width - 384, FlxG.height - 305, 'left', 0xFF00FF)); + add(buttonRight = createButton(FlxG.width - 132, FlxG.height - 305, 'right', 0xFF0000)); + add(buttonDown = createButton(FlxG.width - 258, FlxG.height - 197, 'down', 0x00FFFF)); + case NONE: // do nothing + } + + switch (actionMode) + { + case A: + add(buttonA = createButton(FlxG.width - 132, FlxG.height - 131, 'a', 0xFF0000)); + case B: + add(buttonB = createButton(FlxG.width - 132, FlxG.height - 131, 'b', 0xFFCB00)); + case P: + add(buttonP = createButton(FlxG.width - 132, 0, 'p', 0xFFCB00)); + case A_B: + add(buttonB = createButton(FlxG.width - 262, FlxG.height - 131, 'b', 0xFFCB00)); + add(buttonA = createButton(FlxG.width - 132, FlxG.height - 131, 'a', 0xFF0000)); + case A_B_C: + add(buttonC = createButton(FlxG.width - 392, FlxG.height - 131, 'c', 0x44FF00)); + add(buttonB = createButton(FlxG.width - 262, FlxG.height - 131, 'b', 0xFFCB00)); + add(buttonA = createButton(FlxG.width - 132, FlxG.height - 131, 'a', 0xFF0000)); + case A_B_E: + add(buttonE = createButton(FlxG.width - 392, FlxG.height - 131, 'e', 0xFF7D00)); + add(buttonB = createButton(FlxG.width - 262, FlxG.height - 131, 'b', 0xFFCB00)); + add(buttonA = createButton(FlxG.width - 132, FlxG.height - 131, 'a', 0xFF0000)); + case A_B_X_Y: + add(buttonX = createButton(FlxG.width - 522, FlxG.height - 131, 'x', 0x99062D)); + add(buttonB = createButton(FlxG.width - 262, FlxG.height - 131, 'b', 0xFFCB00)); + add(buttonY = createButton(FlxG.width - 392, FlxG.height - 131, 'y', 0x4A35B9)); + add(buttonA = createButton(FlxG.width - 132, FlxG.height - 131, 'a', 0xFF0000)); + case A_B_C_X_Y: + add(buttonC = createButton(FlxG.width - 392, FlxG.height - 131, 'c', 0x44FF00)); + add(buttonX = createButton(FlxG.width - 262, FlxG.height - 251, 'x', 0x99062D)); + add(buttonB = createButton(FlxG.width - 262, FlxG.height - 131, 'b', 0xFFCB00)); + add(buttonY = createButton(FlxG.width - 132, FlxG.height - 251, 'y', 0x4A35B9)); + add(buttonA = createButton(FlxG.width - 132, FlxG.height - 131, 'a', 0xFF0000)); + case A_B_C_X_Y_Z: + add(buttonX = createButton(FlxG.width - 392, FlxG.height - 251, 'x', 0x99062D)); + add(buttonC = createButton(FlxG.width - 392, FlxG.height - 131, 'c', 0x44FF00)); + add(buttonY = createButton(FlxG.width - 262, FlxG.height - 251, 'y', 0x4A35B9)); + add(buttonB = createButton(FlxG.width - 262, FlxG.height - 131, 'b', 0xFFCB00)); + add(buttonZ = createButton(FlxG.width - 132, FlxG.height - 251, 'z', 0xCCB98E)); + add(buttonA = createButton(FlxG.width - 132, FlxG.height - 131, 'a', 0xFF0000)); + case A_B_C_D_V_X_Y_Z: + add(buttonV = createButton(FlxG.width - 522, FlxG.height - 251, 'v', 0x49A9B2)); + add(buttonD = createButton(FlxG.width - 522, FlxG.height - 131, 'd', 0x0078FF)); + add(buttonX = createButton(FlxG.width - 392, FlxG.height - 251, 'x', 0x99062D)); + add(buttonC = createButton(FlxG.width - 392, FlxG.height - 131, 'c', 0x44FF00)); + add(buttonY = createButton(FlxG.width - 262, FlxG.height - 251, 'y', 0x4A35B9)); + add(buttonB = createButton(FlxG.width - 262, FlxG.height - 131, 'b', 0xFFCB00)); + add(buttonZ = createButton(FlxG.width - 132, FlxG.height - 251, 'z', 0xCCB98E)); + add(buttonA = createButton(FlxG.width - 132, FlxG.height - 131, 'a', 0xFF0000)); + // CNE Releated + case A_B_M_E: + add(buttonM = createButton(FlxG.width - 522, FlxG.height - 131, 'm', 0x00BBFF)); + add(buttonB = createButton(FlxG.width - 262, FlxG.height - 131, 'b', 0xFFCB00)); + add(buttonE = createButton(FlxG.width - 392, FlxG.height - 131, 'e', 0xFF7D00)); + add(buttonA = createButton(FlxG.width - 132, FlxG.height - 131, 'a', 0xFF0000)); + case NONE: // do nothing + } + + scrollFactor.set(); + var guh = Options.controlsAlpha; + if (guh >= 0.9) + guh = guh - 0.07; + alpha = Options.controlsAlpha; + } + + public static function getDPadModeByString(mode:String):FlxDPadMode { + if(dpadModes == null){ + dpadModes = new Map(); + for(enumValue in FlxDPadMode.createAll()) + dpadModes.set(enumValue.getName(), enumValue); + } + return dpadModes.exists(mode) ? dpadModes.get(mode) : NONE; + } + + public static function getActionModeByString(mode:String):FlxActionMode { + if(actionModes == null){ + actionModes = new Map(); + for(enumValue in FlxActionMode.createAll()) + actionModes.set(enumValue.getName(), enumValue); + } + return actionModes.exists(mode) ? actionModes.get(mode) : NONE; + } + + /** + * Clean up memory. + */ + override public function destroy():Void + { + super.destroy(); + + for (field in Reflect.fields(this)) + if (Std.isOfType(Reflect.field(this, field), FlxButton)) + Reflect.setField(this, field, FlxDestroyUtil.destroy(Reflect.field(this, field))); + } + + private function createButton(X:Float, Y:Float, Graphic:String, Color:Int = 0xFFFFFF):FlxButton + { + var graphic:FlxGraphic; + var path:String = Paths.image('mobile/virtualpad/$Graphic'); + #if MOD_SUPPORT + if(FileSystem.exists(path)) + graphic = FlxGraphic.fromBitmapData(BitmapData.fromFile(path)); + else #end if(Assets.exists(path)) + graphic = FlxGraphic.fromBitmapData(Assets.getBitmapData(path)); + else + graphic = FlxGraphic.fromBitmapData(Assets.getBitmapData(Paths.image('mobile/virtualpad/default'))); + + var button:FlxButton = new FlxButton(X, Y); + try { + button.frames = FlxTileFrames.fromGraphic(graphic, FlxPoint.get(Std.int(graphic.width / 2), graphic.height)); + } + catch (e){ + trace("Failed to create button(s) " + e.message); + return null; + } + button.solid = false; + button.immovable = true; + button.scrollFactor.set(); + button.color = Color; + #if FLX_DEBUG + button.ignoreDrawDebug = true; + #end + return button; + } +} \ No newline at end of file diff --git a/source/mobile/funkin/backend/CNEJNI.hx b/source/mobile/funkin/backend/CNEJNI.hx new file mode 100644 index 000000000..99a7141bf --- /dev/null +++ b/source/mobile/funkin/backend/CNEJNI.hx @@ -0,0 +1,65 @@ +// fully stolen from PsychJNI bleh +package mobile.backend; + +/** + * ... + * @author Lily Ross (mcagabe19) + */ +#if android +import lime.system.JNI; + +class CNEJNI #if (lime >= "8.0.0") implements JNISafety #end +{ + public static final SDL_ORIENTATION_UNKNOWN:Int = 0; + public static final SDL_ORIENTATION_LANDSCAPE:Int = 1; + public static final SDL_ORIENTATION_LANDSCAPE_FLIPPED:Int = 2; + public static final SDL_ORIENTATION_PORTRAIT:Int = 3; + public static final SDL_ORIENTATION_PORTRAIT_FLIPPED:Int = 4; + + public static inline function setOrientation(width:Int, height:Int, resizeable:Bool, hint:String):Dynamic + return setOrientation_jni(width, height, resizeable, hint); + + public static inline function getCurrentOrientationAsString():String + { + return switch (getCurrentOrientation_jni()) + { + case SDL_ORIENTATION_PORTRAIT: "Portrait"; + case SDL_ORIENTATION_LANDSCAPE: "LandscapeRight"; + case SDL_ORIENTATION_PORTRAIT_FLIPPED: "PortraitUpsideDown"; + case SDL_ORIENTATION_LANDSCAPE_FLIPPED: "LandscapeLeft"; + default: "Unknown"; + } + } + + public static inline function isScreenKeyboardShown():Dynamic + return isScreenKeyboardShown_jni(); + + public static inline function clipboardHasText():Dynamic + return clipboardHasText_jni(); + + public static inline function clipboardGetText():Dynamic + return clipboardGetText_jni(); + + public static inline function clipboardSetText(string:String):Dynamic + return clipboardSetText_jni(string); + + public static inline function manualBackButton():Dynamic + return manualBackButton_jni(); + + public static inline function setActivityTitle(title:String):Dynamic + return setActivityTitle_jni(title); + + @:noCompletion private static var setOrientation_jni:Dynamic = JNI.createStaticMethod('org/libsdl/app/SDLActivity', 'setOrientation', + '(IIZLjava/lang/String;)V'); + @:noCompletion private static var getCurrentOrientation_jni:Dynamic = JNI.createStaticMethod('org/libsdl/app/SDLActivity', 'getCurrentOrientation', '()I'); + @:noCompletion private static var isScreenKeyboardShown_jni:Dynamic = JNI.createStaticMethod('org/libsdl/app/SDLActivity', 'isScreenKeyboardShown', '()Z'); + @:noCompletion private static var clipboardHasText_jni:Dynamic = JNI.createStaticMethod('org/libsdl/app/SDLActivity', 'clipboardHasText', '()Z'); + @:noCompletion private static var clipboardGetText_jni:Dynamic = JNI.createStaticMethod('org/libsdl/app/SDLActivity', 'clipboardGetText', + '()Ljava/lang/String;'); + @:noCompletion private static var clipboardSetText_jni:Dynamic = JNI.createStaticMethod('org/libsdl/app/SDLActivity', 'clipboardSetText', + '(Ljava/lang/String;)V'); + @:noCompletion private static var manualBackButton_jni:Dynamic = JNI.createStaticMethod('org/libsdl/app/SDLActivity', 'manualBackButton', '()V'); + @:noCompletion private static var setActivityTitle_jni:Dynamic = JNI.createStaticMethod('org/libsdl/app/SDLActivity', 'setActivityTitle', + '(Ljava/lang/String;)Z'); +} +#end diff --git a/source/mobile/funkin/backend/TouchFunctions.hx b/source/mobile/funkin/backend/TouchFunctions.hx new file mode 100644 index 000000000..bde52dd6f --- /dev/null +++ b/source/mobile/funkin/backend/TouchFunctions.hx @@ -0,0 +1,73 @@ +package mobile.funkin.backend; + +import flixel.FlxG; +import flixel.FlxBasic; +import flixel.FlxObject; +import flixel.FlxCamera; +import flixel.input.touch.FlxTouch; + +class TouchFunctions +{ + public static var touchPressed(get, never):Bool; + public static var touchJustPressed(get, never):Bool; + public static var touchJustReleased(get, never):Bool; + public static var touch(get, never):FlxTouch; + + public static function touchOverlapObject(object:FlxBasic, camera:FlxCamera):Bool + { + for (touch in FlxG.touches.list) + if (touch.overlaps(object, camera)) + return true; + return false; + } + + public static function touchOverlapObjectComplex(object:FlxObject):Bool + { + var overlap = false; + for (camera in object.cameras) + { + for (touch in FlxG.touches.list) + { + @:privateAccess + if (object.overlapsPoint(touch.getWorldPosition(camera, object._point), true, camera)) + overlap = true; + } + } + return overlap; + } + + @:noCompletion + private static function get_touchPressed():Bool + { + for (touch in FlxG.touches.list) + if (touch.pressed) + return true; + return false; + } + + @:noCompletion + private static function get_touchJustPressed():Bool + { + for (touch in FlxG.touches.list) + if (touch.justPressed) + return true; + return false; + } + + @:noCompletion + private static function get_touchJustReleased():Bool + { + for (touch in FlxG.touches.list) + if (touch.justReleased) + return true; + return false; + } + + @:noCompletion + private static function get_touch():FlxTouch + { + for (touch in FlxG.touches.list) + return touch; + return FlxG.touches.list[0]; + } +} diff --git a/source/mobile/funkin/backend/system/CopyState.hx b/source/mobile/funkin/backend/system/CopyState.hx new file mode 100644 index 000000000..b82a2e03e --- /dev/null +++ b/source/mobile/funkin/backend/system/CopyState.hx @@ -0,0 +1,216 @@ +package mobile.funkin.backend.system; + +#if mobile +import lime.utils.Assets as LimeAssets; +import openfl.utils.Assets as OpenFLAssets; +import flixel.addons.util.FlxAsyncLoop; +import flixel.FlxG; +import flixel.text.FlxText; +import flixel.FlxSprite; +import flixel.util.FlxColor; +import openfl.utils.ByteArray; +import haxe.io.Path; +import mobile.funkin.backend.utils.SUtil; +import funkin.backend.assets.Paths; +import funkin.backend.utils.NativeAPI; +import funkin.backend.system.MainState; + +#if sys +import sys.io.File; +import sys.FileSystem; +#end + +using StringTools; + +class CopyState extends funkin.backend.MusicBeatState +{ + public static var locatedFiles:Array = []; + public static var maxLoopTimes:Int = 0; + public static final IGNORE_FOLDER_FILE_NAME:String = "ignore.txt"; + + public var loadingImage:FlxSprite; + public var bottomBG:FlxSprite; + public var loadedText:FlxText; + public var copyLoop:FlxAsyncLoop; + + var loopTimes:Int = 0; + var failedFiles:Array = []; + var failedFilesStack:Array = []; + var canUpdate:Bool = true; + var shouldCopy:Bool = false; + + private static final textFilesExtensions:Array = ['ini', 'txt', 'xml', 'hxs', 'hx', 'lua', 'json', 'frag', 'vert']; + + override function create() + { + locatedFiles = []; + maxLoopTimes = 0; + checkExistingFiles(); + if (maxLoopTimes <= 0) + { + FlxG.switchState(new MainState()); + return; + } + + NativeAPI.showMessageBox("Notice", "Seems like you have some missing files that are necessary to run the game\nPress OK to begin the copy process"); + + shouldCopy = true; + + add(new FlxSprite(0, 0).makeGraphic(FlxG.width, FlxG.height, 0xffcaff4d)); + + loadingImage = new FlxSprite(0, 0, Paths.image('funkay')); + loadingImage.setGraphicSize(0, FlxG.height); + loadingImage.updateHitbox(); + loadingImage.screenCenter(); + add(loadingImage); + + bottomBG = new FlxSprite(0, FlxG.height - 26).makeGraphic(FlxG.width, 26, 0xFF000000); + bottomBG.alpha = 0.6; + add(bottomBG); + + loadedText = new FlxText(bottomBG.x, bottomBG.y + 4, FlxG.width, '', 16); + loadedText.setFormat(Paths.font("vcr.ttf"), 16, FlxColor.WHITE, CENTER); + add(loadedText); + + var ticks:Int = 15; + if (maxLoopTimes <= 15) + ticks = 1; + + copyLoop = new FlxAsyncLoop(maxLoopTimes, copyAsset, ticks); + add(copyLoop); + copyLoop.start(); + + super.create(); + } + + override function update(elapsed:Float) + { + if (shouldCopy && copyLoop != null) + { + if (copyLoop.finished && canUpdate) + { + if (failedFiles.length > 0) + { + NativeAPI.showMessageBox('Failed To Copy ${failedFiles.length} File.', failedFiles.join('\n')); + if (!FileSystem.exists('logs')) + FileSystem.createDirectory('logs'); + File.saveContent('logs/' + Date.now().toString().replace(' ', '-').replace(':', "'") + '-CopyState' + '.txt', failedFilesStack.join('\n')); + } + canUpdate = false; + FlxG.sound.play(Paths.sound('menu/confirm')).onComplete = () -> { + FlxG.switchState(new MainState()); + }; + } + + if (maxLoopTimes == 0) + loadedText.text = "Completed!"; + else + loadedText.text = '$loopTimes/$maxLoopTimes'; + } + super.update(elapsed); + } + + public function copyAsset() + { + var file = locatedFiles[loopTimes]; + loopTimes++; + if (!FileSystem.exists(file)) + { + var directory = Path.directory(file); + if (!FileSystem.exists(directory)) + SUtil.mkDirs(directory); + try + { + if (OpenFLAssets.exists(getFile(file))) + { + if (textFilesExtensions.contains(Path.extension(file))) + createContentFromInternal(file); + else + File.saveBytes(file, getFileBytes(getFile(file))); + } + else + { + failedFiles.push(getFile(file) + " (File Dosen't Exist)"); + failedFilesStack.push('Asset ${getFile(file)} does not exist.'); + } + } + catch (e:haxe.Exception) + { + failedFiles.push('${getFile(file)} (${e.message})'); + failedFilesStack.push('${getFile(file)} (${e.stack})'); + } + } + } + + public function createContentFromInternal(file:String) + { + var fileName = Path.withoutDirectory(file); + var directory = Path.directory(file); + try + { + var fileData:String = OpenFLAssets.getText(getFile(file)); + if (fileData == null) + fileData = ''; + if (!FileSystem.exists(directory)) + SUtil.mkDirs(directory); + File.saveContent(Path.join([directory, fileName]), fileData); + } + catch (e:haxe.Exception) + { + failedFiles.push('${getFile(file)} (${e.message})'); + failedFilesStack.push('${getFile(file)} (${e.stack})'); + } + } + + public function getFileBytes(file:String):ByteArray + { + switch (Path.extension(file)) + { + case 'otf' | 'ttf': + return ByteArray.fromFile(file); + default: + return OpenFLAssets.getBytes(file); + } + } + + public static function getFile(file:String):String + { + if(OpenFLAssets.exists(file)) return file; + + @:privateAccess + for(library in LimeAssets.libraries.keys()){ + if(OpenFLAssets.exists('$library:$file') && library != 'default') + return '$library:$file'; + } + + return file; + } + + public static function checkExistingFiles():Bool + { + locatedFiles = OpenFLAssets.list(); + + // removes unwanted assets + var assets = locatedFiles.filter(folder -> folder.startsWith('assets/')); + var mods = locatedFiles.filter(folder -> folder.startsWith('mods/')); + locatedFiles = assets.concat(mods); + + var filesToRemove:Array = []; + + for (file in locatedFiles) + { + if (FileSystem.exists(file) || OpenFLAssets.exists(getFile(Path.join([Path.directory(getFile(file)), IGNORE_FOLDER_FILE_NAME])))) + { + filesToRemove.push(file); + } + } + + for (file in filesToRemove) + locatedFiles.remove(file); + + maxLoopTimes = locatedFiles.length; + + return (maxLoopTimes <= 0); + } +} +#end diff --git a/source/mobile/funkin/backend/system/MobileRatioScaleMode.hx b/source/mobile/funkin/backend/system/MobileRatioScaleMode.hx new file mode 100644 index 000000000..b5fe45a42 --- /dev/null +++ b/source/mobile/funkin/backend/system/MobileRatioScaleMode.hx @@ -0,0 +1,53 @@ +package mobile.funkin.backend.system; + +import flixel.FlxG; +import flixel.system.scaleModes.BaseScaleMode; + +class MobileRatioScaleMode extends BaseScaleMode +{ + public static var allowWideScreen(default, set):Bool = true; + + override function updateGameSize(Width:Int, Height:Int):Void + { + if (funkin.options.Options.wideScreen && allowWideScreen) + { + super.updateGameSize(Width, Height); + } + else + { + var ratio:Float = FlxG.width / FlxG.height; + var realRatio:Float = Width / Height; + + var scaleY:Bool = realRatio < ratio; + + if (scaleY) + { + gameSize.x = Width; + gameSize.y = Math.floor(gameSize.x / ratio); + } + else + { + gameSize.y = Height; + gameSize.x = Math.floor(gameSize.y * ratio); + } + } + } + + override function updateGamePosition():Void + { + if (funkin.options.Options.wideScreen && allowWideScreen) + FlxG.game.x = FlxG.game.y = 0; + else + super.updateGamePosition(); + } + + @:noCompletion + private static function set_allowWideScreen(value:Bool):Bool + { + allowWideScreen = value; + FlxG.scaleMode = new MobileRatioScaleMode(); + return value; + } + + public function resetSize() {} +} diff --git a/source/mobile/funkin/backend/utils/SUtil.hx b/source/mobile/funkin/backend/utils/SUtil.hx new file mode 100644 index 000000000..af089fe6d --- /dev/null +++ b/source/mobile/funkin/backend/utils/SUtil.hx @@ -0,0 +1,188 @@ +package mobile.funkin.backend.utils; + +#if android +import android.content.Context; +import android.os.Environment; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.Permissions; +import android.Settings; +#end +import lime.system.System as LimeSystem; +import funkin.backend.utils.NativeAPI; +#if sys +import sys.io.File; +import sys.FileSystem; +#end + +using StringTools; + +/** + * A storage class for mobile. + * @author Mihai Alexandru (M.A. Jigsaw) + */ +class SUtil +{ + #if sys + public static function getStorageDirectory(?force:Bool = false):String + { + #if mobile + var daPath:String; + #if android + if (!FileSystem.exists(LimeSystem.applicationStorageDirectory + 'storagetype.txt')) + File.saveContent(LimeSystem.applicationStorageDirectory + 'storagetype.txt', funkin.options.Options.storageType); + var curStorageType:String = File.getContent(LimeSystem.applicationStorageDirectory + 'storagetype.txt'); + daPath = force ? StorageType.fromStrForce(curStorageType) : StorageType.fromStr(curStorageType); + daPath = haxe.io.Path.addTrailingSlash(daPath); + #elseif ios + daPath = LimeSystem.documentsDirectory; + #end + + return daPath; + #else + return Sys.getCwd(); + #end + } + + public static function mkDirs(directory:String):Void + { + try { + if (FileSystem.exists(directory) && FileSystem.isDirectory(directory)) + return; + } catch (e:haxe.Exception) { + trace('Something went wrong while looking at folder. (${e.message})'); + } + var total:String = ''; + if (directory.substr(0, 1) == '/') + total = '/'; + + var parts:Array = directory.split('/'); + if (parts.length > 0 && parts[0].indexOf(':') > -1) + parts.shift(); + + for (part in parts) + { + if (part != '.' && part != '') + { + if (total != '' && total != '/') + total += '/'; + + total += part; + + try + { + if (!FileSystem.exists(total)) + FileSystem.createDirectory(total); + } + catch (e:haxe.Exception) + trace('Error while creating folder. (${e.message}'); + } + } + } + + public static function saveContent(fileName:String = 'file', fileExtension:String = '.json', + fileData:String = 'You forgor to add somethin\' in yo code :3'):Void + { + try + { + if (!FileSystem.exists('saves')) + FileSystem.createDirectory('saves'); + + File.saveContent('saves/$fileName', fileData); + NativeAPI.showMessageBox("Success!", '$fileName has been saved.', MSG_INFORMATION); + } + catch (e:haxe.Exception) + trace('File couldn\'t be saved. (${e.message})'); + } + + #if android + public static function doPermissionsShit():Void + { + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) + Permissions.requestPermissions(['READ_MEDIA_IMAGES', 'READ_MEDIA_VIDEO', 'READ_MEDIA_AUDIO']); + else + Permissions.requestPermissions(['READ_EXTERNAL_STORAGE', 'WRITE_EXTERNAL_STORAGE']); + + if (!Environment.isExternalStorageManager()) + { + if (VERSION.SDK_INT >= VERSION_CODES.S) + Settings.requestSetting('REQUEST_MANAGE_MEDIA'); + Settings.requestSetting('MANAGE_APP_ALL_FILES_ACCESS_PERMISSION'); + } + + if ((VERSION.SDK_INT >= VERSION_CODES.TIRAMISU && !Permissions.getGrantedPermissions().contains('android.permission.READ_MEDIA_IMAGES')) || (VERSION.SDK_INT < VERSION_CODES.TIRAMISU && !Permissions.getGrantedPermissions().contains('android.permission.READ_EXTERNAL_STORAGE'))) + NativeAPI.showMessageBox('Notice!', 'If you accepted the permissions you are all good!' + '\nIf you didn\'t then expect a crash' + '\nPress Ok to see what happens', MSG_INFORMATION); + + try + { + if (!FileSystem.exists(SUtil.getStorageDirectory())) + mkDirs(SUtil.getStorageDirectory()); + } + catch (e:Dynamic) + { + NativeAPI.showMessageBox('Error!', 'Please create folder to\n' + SUtil.getStorageDirectory(true) + '\nPress OK to close the game', MSG_ERROR); + LimeSystem.exit(1); + } + } + + public static function checkExternalPaths(?splitStorage = false):Array { + var process = new funkin.backend.utils.native.HiddenProcess('grep -o "/storage/....-...." /proc/mounts | paste -sd \',\''); + var paths:String = process.stdout.readAll().toString(); + if (splitStorage) paths = paths.replace('/storage/', ''); + return paths.split(','); + } + + public static function getExternalDirectory(external:String):String { + var daPath:String = ''; + for (path in checkExternalPaths()) + if (path.contains(external)) daPath = path; + + daPath = haxe.io.Path.addTrailingSlash(daPath.endsWith("\n") ? daPath.substr(0, daPath.length - 1) : daPath); + return daPath; + } + #end + #end +} + +#if android +enum abstract StorageType(String) from String to String +{ + final forcedPath = '/storage/emulated/0/'; + final packageNameLocal = 'com.yoshman29.codenameengine'; + final fileLocal = 'CodenameEngine'; + + public static function fromStr(str:String):StorageType + { + final EXTERNAL_DATA = Context.getExternalFilesDir(); + final EXTERNAL_OBB = Context.getObbDir(); + final EXTERNAL_MEDIA = Environment.getExternalStorageDirectory() + '/Android/media/' + lime.app.Application.current.meta.get('packageName'); + final EXTERNAL = Environment.getExternalStorageDirectory() + '/.' + lime.app.Application.current.meta.get('file'); + + return switch (str) + { + case "EXTERNAL_DATA": EXTERNAL_DATA; + case "EXTERNAL_OBB": EXTERNAL_OBB; + case "EXTERNAL_MEDIA": EXTERNAL_MEDIA; + case "EXTERNAL": EXTERNAL; + default: SUtil.getExternalDirectory(str) + '.' + fileLocal; + } + } + + public static function fromStrForce(str:String):StorageType + { + final EXTERNAL_DATA = forcedPath + 'Android/data/' + packageNameLocal + '/files'; + final EXTERNAL_OBB = forcedPath + 'Android/obb/' + packageNameLocal; + final EXTERNAL_MEDIA = forcedPath + 'Android/media/' + packageNameLocal; + final EXTERNAL = forcedPath + '.' + fileLocal; + + return switch (str) + { + case "EXTERNAL_DATA": EXTERNAL_DATA; + case "EXTERNAL_OBB": EXTERNAL_OBB; + case "EXTERNAL_MEDIA": EXTERNAL_MEDIA; + case "EXTERNAL": EXTERNAL; + default: SUtil.getExternalDirectory(str) + '.' + fileLocal; + } + } +} +#end diff --git a/source/mobile/funkin/menus/MobileControlSelectSubState.hx b/source/mobile/funkin/menus/MobileControlSelectSubState.hx new file mode 100644 index 000000000..7d83e850f --- /dev/null +++ b/source/mobile/funkin/menus/MobileControlSelectSubState.hx @@ -0,0 +1,271 @@ +package mobile.funkin.menus; + +import flixel.addons.display.FlxBackdrop; +import flixel.addons.display.FlxGridOverlay; +import flixel.util.FlxGradient; +import mobile.funkin.backend.TouchFunctions; +import mobile.flixel.FlxButton; +import flixel.input.touch.FlxTouch; +import flixel.ui.FlxButton as UIButton; +import funkin.backend.MusicBeatSubstate; +import mobile.objects.MobileControls; +import flixel.FlxSprite; +import flixel.text.FlxText; +import flixel.FlxCamera; +import flixel.util.FlxColor; +import flixel.tweens.*; +import flixel.FlxG; +import funkin.backend.assets.Paths; +import funkin.backend.utils.CoolUtil; +import funkin.menus.ui.Alphabet; + +using StringTools; + +class MobileControlSelectSubState extends MusicBeatSubstate +{ + var options:Array = ['Pad-Right', 'Pad-Left', 'Pad-Custom', 'Hitbox', 'Keyboard']; + var control:MobileControls; + var leftArrow:FlxSprite; + var rightArrow:FlxSprite; + var itemText:Alphabet; + var positionText:FlxText; + var positionTextBg:FlxSprite; + var bg:FlxBackdrop; + var ui:FlxCamera; + var buttonCamera:FlxCamera; + var curOption:Int = MobileControls.mode; + var buttonBinded:Bool = false; + var bindButton:FlxButton; + var reset:UIButton; + var tweenieShit:Float = 0; + var keyboardText:FlxText; + var closeCallBack:Void->Void; + + public function new(?closeCallBack:Void->Void, ?openCallBack:Void->Void) + { + super(); + + this.closeCallBack = closeCallBack; + if(openCallBack != null) openCallBack(); + + bg = new FlxBackdrop(FlxGridOverlay.createGrid(80, 80, 160, 160, true, + FlxColor.fromRGB(FlxG.random.int(0, 255), FlxG.random.int(0, 255), FlxG.random.int(0, 255)), + FlxColor.fromRGB(FlxG.random.int(0, 255), FlxG.random.int(0, 255), FlxG.random.int(0, 255)))); + bg.velocity.set(40, 40); + bg.alpha = 0; + add(bg); + + ui = new FlxCamera(); + ui.bgColor.alpha = 0; + ui.alpha = 0; + FlxG.cameras.add(ui, false); + + buttonCamera = new FlxCamera(); + buttonCamera.bgColor.alpha = 0; + buttonCamera.alpha = 0; + FlxG.cameras.add(buttonCamera, false); + + itemText = new Alphabet(0, 60, ''); + add(itemText); + + leftArrow = new FlxSprite(0, itemText.y - 25); + leftArrow.frames = Paths.getSparrowAtlas('mobile/menu/arrows'); + leftArrow.animation.addByPrefix('idle', 'arrow left'); + leftArrow.animation.addByPrefix('press', "arrow push left"); + leftArrow.animation.play('idle'); + add(leftArrow); + + itemText.x = leftArrow.width + 70; + leftArrow.x = itemText.x - 60; + + rightArrow = new FlxSprite().loadGraphicFromSprite(leftArrow); + rightArrow.flipX = true; + rightArrow.setPosition(itemText.x + itemText.width + 10, itemText.y - 25); + add(rightArrow); + + positionText = new FlxText(0, FlxG.height, FlxG.width / 4, ''); + positionText.setFormat(Paths.font("vcr.ttf"), 18, FlxColor.WHITE, FlxTextAlign.LEFT); + positionText.visible = false; + + positionTextBg = FlxGradient.createGradientFlxSprite(250, 150, [FlxColor.BLACK, FlxColor.BLACK, FlxColor.BLACK, FlxColor.TRANSPARENT], 1, 360); + positionTextBg.setPosition(0, FlxG.height - positionTextBg.height); + positionTextBg.visible = false; + positionTextBg.alpha = 0.8; + add(positionTextBg); + add(positionText); + + keyboardText = new FlxText(0, 0, FlxG.width, "-- No Controls --", 14); + keyboardText.setFormat(Paths.font("vcr.ttf"), 36, FlxColor.WHITE, FlxTextAlign.CENTER); + keyboardText.screenCenter(); + add(keyboardText); + keyboardText.kill(); + + var exit = new UIButton(0, itemText.y - 25, "Exit & Save", () -> + { + MobileControls.mode = curOption; + if (options[curOption] == 'Pad-Custom') + MobileControls.setCustomMode(control.virtualPad); + CoolUtil.playMenuSFX(CANCEL); + if(closeCallBack != null) closeCallBack(); + close(); + }); + exit.color = FlxColor.LIME; + exit.setGraphicSize(Std.int(exit.width) * 3); + exit.updateHitbox(); + exit.x = FlxG.width - exit.width - 70; + exit.label.setFormat(Paths.font('vcr.ttf'), 28, FlxColor.WHITE, FlxTextAlign.CENTER); + exit.label.fieldWidth = exit.width; + exit.label.x = ((exit.width - exit.label.width) / 2) + exit.x; + exit.label.offset.y = -10; // WHY THE FUCK I CAN'T CHANGE THE LABEL Y + add(exit); + + reset = new UIButton(exit.x, exit.height + exit.y + 20, "Reset", () -> + { + changeOption(0); // realods the current control mode ig? + }); + reset.color = FlxColor.RED; + reset.setGraphicSize(Std.int(reset.width) * 3); + reset.updateHitbox(); + reset.label.setFormat(Paths.font('vcr.ttf'), 28, FlxColor.WHITE, FlxTextAlign.CENTER); + reset.label.fieldWidth = reset.width; + reset.label.x = ((reset.width - reset.label.width) / 2) + reset.x; + reset.label.offset.y = -10; + add(reset); + + cameras = [ui]; + leftArrow.cameras = rightArrow.cameras = reset.cameras = exit.cameras = [buttonCamera]; + FlxTween.tween(bg, {alpha: 0.45}, 0.3, { + ease: FlxEase.quadOut, + onComplete: (twn:FlxTween) -> + { + for (camera in [ui, buttonCamera]) + FlxTween.tween(camera, {alpha: 1}, 0.2, {ease: FlxEase.circOut}); + } + }); + changeOption(0); + setOptionText(); + FlxG.mouse.visible = true; + } + + override function update(elapsed:Float) + { + checkArrowButton(leftArrow, () -> changeOption(-1)); + checkArrowButton(rightArrow, () -> changeOption(1)); + + for(touch in FlxG.touches.list){ + if (options[curOption] == 'Pad-Custom') + { + if (buttonBinded) + { + if (touch.justReleased) + { + bindButton = null; + buttonBinded = false; + } + else + moveButton(touch, bindButton); + } + else + { + control.virtualPad.forEachAlive((button:FlxButton) -> + { + if (button.justPressed) + moveButton(touch, button); + }); + } + } + } + + tweenieShit += 180 * elapsed; + keyboardText.alpha = 1 - Math.sin((Math.PI * tweenieShit) / 180); + + super.update(elapsed); + } + + function changeControls(?type:Int = null) + { + if (type == null) + type = curOption; + if (control != null) + control.destroy(); + if (members.contains(control)) + remove(control); + control = new MobileControls(type); + add(control); + control.cameras = [ui]; + } + + function changeOption(change:Int) + { + CoolUtil.playMenuSFX(); + curOption += change; + + if (curOption < 0) + curOption = options.length - 1; + if (curOption >= options.length) + curOption = 0; + + switch (curOption) + { + case 2: + reset.visible = true; + keyboardText.kill(); + changeControls(); + default: + reset.visible = false; + keyboardText.kill(); + changeControls(); + } + updatePosText(); + setOptionText(); + } + + function setOptionText() + { + itemText.text = options[curOption].replace('-', ' '); + itemText.updateHitbox(); + itemText.offset.set(0, 15); + FlxTween.tween(rightArrow, {x: itemText.x + itemText.width + 10}, 0.1, {ease: FlxEase.quintOut}); + } + + function updatePosText() + { + var optionName = options[curOption]; + if (optionName == 'Pad-Custom') + { + positionText.visible = positionTextBg.visible = true; + positionText.text = 'LEFT X: ${control.virtualPad.buttonLeft.x} - Y: ${control.virtualPad.buttonLeft.y}\nDOWN X: ${control.virtualPad.buttonDown.x} - Y: ${control.virtualPad.buttonDown.y}\n\nUP X: ${control.virtualPad.buttonUp.x} - Y: ${control.virtualPad.buttonUp.y}\nRIGHT X: ${control.virtualPad.buttonRight.x} - Y: ${control.virtualPad.buttonRight.y}'; + positionText.setPosition(0, (((positionTextBg.height - positionText.height) / 2) + positionTextBg.y)); + } + else + positionText.visible = positionTextBg.visible = false; + } + + function checkArrowButton(button:FlxSprite, func:Void->Void) + { + // OVERLAPS WON'T WORK IDFK WHY + for(camera in button.cameras) + if (FlxG.mouse.getScreenPosition(camera).x >= button.x && FlxG.mouse.getScreenPosition(camera).x <= button.x + button.width && + FlxG.mouse.getScreenPosition(camera).y >= button.y && FlxG.mouse.getScreenPosition(camera).y <= button.y + button.height) + { + if (FlxG.mouse.justPressed) + func(); + if (FlxG.mouse.pressed) + button.animation.play('press'); + } + if (FlxG.mouse.justReleased && button.animation.curAnim.name == 'press') + button.animation.play('idle'); + + if (FlxG.keys.justPressed.LEFT && button == leftArrow || FlxG.keys.justPressed.RIGHT && button == rightArrow) + func(); + } + + function moveButton(touch:FlxTouch, button:FlxButton):Void + { + bindButton = button; + buttonBinded = bindButton == null ? false : true; + bindButton.x = touch.getScreenPosition(ui).x - Std.int(bindButton.width / 2); + bindButton.y = touch.getScreenPosition(ui).y - Std.int(bindButton.height / 2); + updatePosText(); + } +} diff --git a/source/mobile/objects/FlxButtonGroup.hx b/source/mobile/objects/FlxButtonGroup.hx new file mode 100644 index 000000000..f18dc5c2b --- /dev/null +++ b/source/mobile/objects/FlxButtonGroup.hx @@ -0,0 +1,6 @@ +package mobile.objects; + +import mobile.flixel.FlxButton; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; + +typedef FlxButtonGroup = FlxTypedSpriteGroup; \ No newline at end of file diff --git a/source/mobile/objects/Hitbox.hx b/source/mobile/objects/Hitbox.hx new file mode 100644 index 000000000..9d66e5dde --- /dev/null +++ b/source/mobile/objects/Hitbox.hx @@ -0,0 +1,170 @@ +package mobile.objects; + +import flixel.FlxG; +import flixel.tweens.*; +import flixel.util.FlxColor; +import openfl.display.Shape; +import funkin.options.Options; +import mobile.flixel.FlxButton; +import openfl.display.BitmapData; +import flixel.util.FlxDestroyUtil; + +/** + * A zone with 4 hint's (A hitbox). + * It's really easy to customize the layout. + * + * @author: Mihai Alexandru and Karim Akra + */ +class Hitbox extends FlxButtonGroup +{ + //final offsetFir:Int = (ClientPrefs.data.hitbox2 ? Std.int(FlxG.height / 4) * 3 : 0); + //final offsetSec:Int = (ClientPrefs.data.hitbox2 ? 0 : Std.int(FlxG.height / 4)); + + public var buttonLeft:FlxButton = new FlxButton(0, 0); + public var buttonDown:FlxButton = new FlxButton(0, 0); + public var buttonUp:FlxButton = new FlxButton(0, 0); + public var buttonRight:FlxButton = new FlxButton(0, 0); + public var buttonExtra:FlxButton = new FlxButton(0, 0); + public var buttonExtra2:FlxButton = new FlxButton(0, 0); + + /** + * Create the zone. + */ + public function new(/*extraMode:ExtraActions*/) + { + super(); + + //switch (extraMode) + //{ + // case NONE: + add(buttonLeft = createHint(0, 0, Std.int(FlxG.width / 4), FlxG.height, 0xFFC24B99)); + add(buttonDown = createHint(FlxG.width / 4, 0, Std.int(FlxG.width / 4), FlxG.height, 0xFF00FFFF)); + add(buttonUp = createHint(FlxG.width / 2, 0, Std.int(FlxG.width / 4), FlxG.height, 0xFF12FA05)); + add(buttonRight = createHint((FlxG.width / 2) + (FlxG.width / 4), 0, Std.int(FlxG.width / 4), FlxG.height, 0xFFF9393F)); + /* case SINGLE: + add(buttonLeft = createHint(0, offsetSec, Std.int(FlxG.width / 4), Std.int(FlxG.height / 4) * 3, 0xFFC24B99)); + add(buttonDown = createHint(FlxG.width / 4, offsetSec, Std.int(FlxG.width / 4), Std.int(FlxG.height / 4) * 3, 0xFF00FFFF)); + add(buttonUp = createHint(FlxG.width / 2, offsetSec, Std.int(FlxG.width / 4), Std.int(FlxG.height / 4) * 3, 0xFF12FA05)); + add(buttonRight = createHint((FlxG.width / 2) + (FlxG.width / 4), offsetSec, Std.int(FlxG.width / 4), Std.int(FlxG.height / 4) * 3, + 0xFFF9393F)); + add(buttonExtra = createHint(0, offsetFir, FlxG.width, Std.int(FlxG.height / 4), 0xFF0066FF)); + case DOUBLE: + add(buttonLeft = createHint(0, offsetSec, Std.int(FlxG.width / 4), Std.int(FlxG.height / 4) * 3, 0xFFC24B99)); + add(buttonDown = createHint(FlxG.width / 4, offsetSec, Std.int(FlxG.width / 4), Std.int(FlxG.height / 4) * 3, 0xFF00FFFF)); + add(buttonUp = createHint(FlxG.width / 2, offsetSec, Std.int(FlxG.width / 4), Std.int(FlxG.height / 4) * 3, 0xFF12FA05)); + add(buttonRight = createHint((FlxG.width / 2) + (FlxG.width / 4), offsetSec, Std.int(FlxG.width / 4), Std.int(FlxG.height / 4) * 3, + 0xFFF9393F)); + add(buttonExtra2 = createHint(Std.int(FlxG.width / 2), offsetFir, Std.int(FlxG.width / 2), Std.int(FlxG.height / 4), 0xA6FF00)); + add(buttonExtra = createHint(0, offsetFir, Std.int(FlxG.width / 2), Std.int(FlxG.height / 4), 0xFF0066FF)); + } + */ + scrollFactor.set(); + var guh = Options.controlsAlpha; + if (guh >= 0.9) + guh = guh - 0.07; + alpha = Options.controlsAlpha; + } + + /** + * Clean up memory. + */ + override function destroy() + { + super.destroy(); + + buttonLeft = FlxDestroyUtil.destroy(buttonLeft); + buttonDown = FlxDestroyUtil.destroy(buttonDown); + buttonUp = FlxDestroyUtil.destroy(buttonUp); + buttonRight = FlxDestroyUtil.destroy(buttonRight); + buttonExtra = FlxDestroyUtil.destroy(buttonExtra); + buttonExtra2 = FlxDestroyUtil.destroy(buttonExtra2); + } + + private function createHint(X:Float, Y:Float, Width:Int, Height:Int, Color:Int = 0xFFFFFF):FlxButton + { + var hintTween:FlxTween = null; + var hint = new FlxButton(X, Y); + hint.loadGraphic(createHintGraphic(Width, Height)); + hint.color = Color; + hint.solid = false; + hint.immovable = true; + hint.multiTouch = true; + hint.moves = false; + hint.scrollFactor.set(); + hint.alpha = 0.00001; + hint.antialiasing = Options.antialiasing; + if (Options.hitboxType != 'hidden') + { + var controlsAlpha = Options.hitboxType == 'gradient' ? Options.controlsAlpha : 0.25; // so it won't cover up your whole screen with a solid color + hint.onDown.callback = function() + { + if (hintTween != null) + hintTween.cancel(); + + hintTween = FlxTween.tween(hint, {alpha: controlsAlpha}, controlsAlpha / 100, { + ease: FlxEase.circInOut, + onComplete: function(twn:FlxTween) + { + hintTween = null; + } + }); + } + hint.onUp.callback = function() + { + if (hintTween != null) + hintTween.cancel(); + + hintTween = FlxTween.tween(hint, {alpha: 0.00001}, controlsAlpha / 10, { + ease: FlxEase.circInOut, + onComplete: function(twn:FlxTween) + { + hintTween = null; + } + }); + } + hint.onOut.callback = function() + { + if (hintTween != null) + hintTween.cancel(); + + hintTween = FlxTween.tween(hint, {alpha: 0.00001}, controlsAlpha / 10, { + ease: FlxEase.circInOut, + onComplete: function(twn:FlxTween) + { + hintTween = null; + } + }); + } + } + #if FLX_DEBUG + hint.ignoreDrawDebug = true; + #end + return hint; + } + + function createHintGraphic(Width:Int, Height:Int):BitmapData + { + var guh = Options.controlsAlpha; + if (guh >= 0.9) + guh = Options.controlsAlpha - 0.07; + var shape:Shape = new Shape(); + shape.graphics.beginFill(0xFFFFFF); + if(Options.hitboxType == 'gradient'){ + shape.graphics.lineStyle(3, 0xFFFFFF, 1); + shape.graphics.drawRect(0, 0, Width, Height); + shape.graphics.lineStyle(0, 0, 0); + shape.graphics.drawRect(3, 3, Width - 6, Height - 6); + shape.graphics.endFill(); + shape.graphics.beginGradientFill(RADIAL, [0xFFFFFF, FlxColor.TRANSPARENT], [guh, 0], [0, 255], null, null, null, 0.5); + shape.graphics.drawRect(3, 3, Width - 6, Height - 6); + shape.graphics.endFill(); + } else { + shape.graphics.lineStyle(10, 0xFFFFFF, 1); + shape.graphics.drawRect(0, 0, Width, Height); + shape.graphics.endFill(); + } + var bitmap:BitmapData = new BitmapData(Width, Height, true, 0); + bitmap.draw(shape); + return bitmap; + } +} \ No newline at end of file diff --git a/source/mobile/objects/MobileControls.hx b/source/mobile/objects/MobileControls.hx new file mode 100644 index 000000000..0fdf5a870 --- /dev/null +++ b/source/mobile/objects/MobileControls.hx @@ -0,0 +1,200 @@ +package mobile.objects; + +import flixel.FlxG; +import flixel.math.FlxPoint; +import mobile.flixel.FlxButton; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import mobile.flixel.FlxVirtualPad; +import flixel.util.FlxDestroyUtil; +import funkin.options.Options; + +class MobileControls extends FlxTypedSpriteGroup +{ + public var virtualPad:FlxVirtualPad = new FlxVirtualPad(FlxDPadMode.NONE, FlxActionMode.NONE); + public var hitbox:Hitbox = new Hitbox(); + // YOU CAN'T CHANGE PROPERTIES USING THIS EXCEPT WHEN IN RUNTIME!! (except for the variables it already has like buttonUp, buttonLeft...) + public var current:CurrentManager; + + public static var mode(get, set):Int; + public static var forcedControl:Null; + public static var mobileC(get, never):Bool; + + public function new(?forceType:Int) + { + super(); + forcedControl = mode; + if (forceType != null) + forcedControl = forceType; + switch (forcedControl) + { + case 0: // RIGHT_FULL + initControler(0); + case 1: // LEFT_FULL + initControler(1); + case 2: // CUSTOM + initControler(2); + case 3: // HITBOX + initControler(3); + case 4: // KEYBOARD + } + current = new CurrentManager(this); + //updateButtonsColors(); + } + + private function initControler(virtualPadMode:Int = 0) + { + switch (virtualPadMode) + { + case 0: + virtualPad = new FlxVirtualPad('RIGHT_FULL', 'NONE'); + add(virtualPad); + case 1: + virtualPad = new FlxVirtualPad('LEFT_FULL', 'NONE'); + add(virtualPad); + case 2: + virtualPad = getCustomMode(new FlxVirtualPad('RIGHT_FULL', 'NONE')); + add(virtualPad); + case 3: + hitbox = new Hitbox(); + add(hitbox); + } + } + + public static function setCustomMode(virtualPad:FlxVirtualPad):Void + { + if (FlxG.save.data.buttons == null) + { + FlxG.save.data.buttons = new Array(); + for (buttons in virtualPad) + FlxG.save.data.buttons.push(FlxPoint.get(buttons.x, buttons.y)); + } + else + { + var tempCount:Int = 0; + for (buttons in virtualPad) + { + FlxG.save.data.buttons[tempCount] = FlxPoint.get(buttons.x, buttons.y); + tempCount++; + } + } + + FlxG.save.flush(); + } + + public static function getCustomMode(virtualPad:FlxVirtualPad):FlxVirtualPad + { + var tempCount:Int = 0; + + if (FlxG.save.data.buttons == null) + return virtualPad; + + for (buttons in virtualPad) + { + if(FlxG.save.data.buttons[tempCount] != null){ + buttons.x = FlxG.save.data.buttons[tempCount].x; + buttons.y = FlxG.save.data.buttons[tempCount].y; + } + tempCount++; + } + + return virtualPad; + } + + override public function destroy():Void + { + super.destroy(); + + if (virtualPad != null) + { + virtualPad = FlxDestroyUtil.destroy(virtualPad); + virtualPad = null; + } + + if (hitbox != null) + { + hitbox = FlxDestroyUtil.destroy(hitbox); + hitbox = null; + } + } + + static function set_mode(mode:Int = 0) + { + FlxG.save.data.mobileControlsMode = mode; + FlxG.save.flush(); + return mode; + } + + static function get_mode():Int + { + if (forcedControl != null) + return forcedControl; + + if (FlxG.save.data.mobileControlsMode == null) + { + FlxG.save.data.mobileControlsMode = 0; + FlxG.save.flush(); + } + + return FlxG.save.data.mobileControlsMode; + } + + @:noCompletion + private static function get_mobileC():Bool return Options.controlsAlpha >= 0.1; + /* + public function updateButtonsColors() { + // Dynamic Controls Color + var buttonsColors:Array = []; + var data:Dynamic; + if (ClientPrefs.data.dynamicColors) + data = ClientPrefs.data; + else + data = ClientPrefs.defaultData; + + buttonsColors.push(data.arrowRGB[0][0]); + buttonsColors.push(data.arrowRGB[1][0]); + buttonsColors.push(data.arrowRGB[2][0]); + buttonsColors.push(data.arrowRGB[3][0]); + if (mode == 3) + { + virtualPad.buttonLeft2.color = buttonsColors[0]; + virtualPad.buttonDown2.color = buttonsColors[1]; + virtualPad.buttonUp2.color = buttonsColors[2]; + virtualPad.buttonRight2.color = buttonsColors[3]; + } + current.buttonLeft.color = buttonsColors[0]; + current.buttonDown.color = buttonsColors[1]; + current.buttonUp.color = buttonsColors[2]; + current.buttonRight.color = buttonsColors[3]; + } + */ +} + +class CurrentManager { + public var buttonLeft:FlxButton; + public var buttonDown:FlxButton; + public var buttonUp:FlxButton; + public var buttonRight:FlxButton; + //public var buttonExtra:FlxButton; + //public var buttonExtra2:FlxButton; + public var target:FlxButtonGroup; + + public function new(control:MobileControls){ + if(MobileControls.mode == 3) { + target = control.hitbox; + buttonLeft = control.hitbox.buttonLeft; + buttonDown = control.hitbox.buttonDown; + buttonUp = control.hitbox.buttonUp; + buttonRight = control.hitbox.buttonRight; + //buttonExtra = control.hitbox.buttonExtra; + //buttonExtra2 = control.hitbox.buttonExtra2; + } else { + target = control.virtualPad; + buttonLeft = control.virtualPad.buttonLeft; + buttonDown = control.virtualPad.buttonDown; + buttonUp = control.virtualPad.buttonUp; + buttonRight = control.virtualPad.buttonRight; + //buttonExtra = control.virtualPad.buttonExtra; + //buttonExtra2 = control.virtualPad.buttonExtra2; + } + } +} diff --git a/source/openfl/display/Shader.hx b/source/openfl/display/Shader.hx new file mode 100644 index 000000000..72b017197 --- /dev/null +++ b/source/openfl/display/Shader.hx @@ -0,0 +1,985 @@ +package openfl.display; + +#if !flash +import openfl.display3D._internal.GLProgram; +import openfl.display3D._internal.GLShader; +import openfl.display._internal.ShaderBuffer; +import openfl.utils._internal.Float32Array; +import openfl.utils._internal.Log; +import openfl.display3D.Context3D; +import openfl.display3D.Program3D; +import openfl.utils.ByteArray; + +/** + // TODO: Document GLSL Shaders + A Shader instance represents a Pixel Bender shader kernel in ActionScript. + To use a shader in your application, you create a Shader instance for it. + You then use that Shader instance in the appropriate way according to the + effect you want to create. For example, to use the shader as a filter, you + assign the Shader instance to the `shader` property of a ShaderFilter + object. + A shader defines a function that executes on all the pixels in an image, + one pixel at a time. The result of each call to the function is the output + color at that pixel coordinate in the image. A shader can specify one or + more input images, which are images whose content can be used in + determining the output of the function. A shader can also specify one or + more parameters, which are input values that can be used in calculating + the function output. In a single shader execution, the input and parameter + values are constant. The only thing that varies is the coordinate of the + pixel whose color is the function result. Shader function calls for + multiple output pixel coordinates execute in parallel to improve shader + execution performance. + + The shader bytecode can be loaded at run time using a URLLoader instance. + The following example demonstrates loading a shader bytecode file at run + time and linking it to a Shader instance. + + ```as3 + var loader:URLLoader = new URLLoader(); + loader.dataFormat = URLLoaderDataFormat.BINARY; + loader.addEventListener(Event.COMPLETE, onLoadComplete); + loader.load(new URLRequest("myShader.pbj")); + var shader:Shader; + + function onLoadComplete(event:Event):void { + // Create a new shader and set the loaded data as its bytecode + shader = new Shader(); + shader.byteCode = loader.data; + + // You can also pass the bytecode to the Shader() constructor like this: + // shader = new Shader(loader.data); + + // do something with the shader + } + ``` + + You can also embed the shader into the SWF at compile time using the + `[Embed]` metadata tag. The `[Embed]` metadata tag is only available if + you use the Flex SDK to compile the SWF. The `[Embed]` tag's `source` + parameter points to the shader file, and its `mimeType` parameter is + `"application/octet-stream"`, as in this example: + + ```as3 + [Embed(source="myShader.pbj", mimeType="application/octet-stream)] var MyShaderClass:Class; + + // ... + + // create a new shader and set the embedded shader as its bytecode var + shaderShader = new Shader(); + shader.byteCode = new MyShaderClass(); + + // You can also pass the bytecode to the Shader() constructor like this: + // var shader:Shader = new Shader(new MyShaderClass()); + + // do something with the shader + ``` + + In either case, you link the raw shader (the `URLLoader.data` property or + an instance of the `[Embed]` data class) to the Shader instance. As the + previous examples demonstrate, you can do this in two ways. You can pass + the shader bytecode as an argument to the `Shader()` constructor. + Alternatively, you can set it as the Shader instance's `byteCode` + property. + + Once a Shader instance is created, it can be used in one of several ways: + + * A shader fill: The output of the shader is used as a fill for content + drawn with the drawing API. Pass the Shader instance as an argument to the + `Graphics.beginShaderFill()` method. + * A shader filter: The output of the shader is used as a graphic filter + applied to a display object. Assign the Shader instance to the `shader` + property of a ShaderFilter instance. + * A blend mode: The output of the shader is rendered as the blending + between two overlapping display objects. Assign the Shader instance to the + `blendShader` property of the upper of the two display objects. + * Background shader processing: The shader executes in the background, + avoiding the possibility of freezing the display, and dispatches an event + when processing is complete. Assign the Shader instance to the `shader` + property of a ShaderJob instance. + + Shader fills, filters, and blends are not supported under GPU rendering. + + **Mobile Browser Support:** This feature is not supported in mobile + browsers. + + _Adobe AIR profile support:_ This feature is supported on all desktop operating + systems, but it is not supported on all mobile devices. It is not + supported on AIR for TV devices. See + [AIR Profile Support](https://help.adobe.com/en_US/air/build/WS144092a96ffef7cc16ddeea2126bb46b82f-8000.html) + for more information regarding API support across multiple profiles. +**/ +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +@:access(openfl.display3D.Context3D) +@:access(openfl.display3D.Program3D) +@:access(openfl.display.ShaderInput) +@:access(openfl.display.ShaderParameter) +// #if (!display && !macro) +#if !macro +@:autoBuild(openfl.utils._internal.ShaderMacro.build()) +#end +class Shader +{ + /** + The raw shader bytecode for this Shader instance. + **/ + public var byteCode(null, default):ByteArray; + + /** + Provides access to parameters, input images, and metadata for the + Shader instance. ShaderParameter objects representing parameters for + the shader, ShaderInput objects representing the input images for the + shader, and other values representing the shader's metadata are + dynamically added as properties of the `data` property object when the + Shader instance is created. Those properties can be used to introspect + the shader and to set parameter and input values. + For information about accessing and manipulating the dynamic + properties of the `data` object, see the ShaderData class description. + **/ + public var data(get, set):ShaderData; + + /** + Get or set the fragment source used when compiling with GLSL. + + This property is not available on the Flash target. + **/ + public var glFragmentSource(get, set):String; + + /** + The compiled GLProgram if available. + + This property is not available on the Flash target. + **/ + @SuppressWarnings("checkstyle:Dynamic") public var glProgram(default, null):GLProgram; + + /** + Get or set the vertex source used when compiling with GLSL. + + This property is not available on the Flash target. + **/ + public var glVertexSource(get, set):String; + + /** + The precision of math operations performed by the shader. + The set of possible values for the `precisionHint` property is defined + by the constants in the ShaderPrecision class. + + The default value is `ShaderPrecision.FULL`. Setting the precision to + `ShaderPrecision.FAST` can speed up math operations at the expense of + precision. + + Full precision mode (`ShaderPrecision.FULL`) computes all math + operations to the full width of the IEEE 32-bit floating standard and + provides consistent behavior on all platforms. In this mode, some math + operations such as trigonometric and exponential functions can be + slow. + + Fast precision mode (`ShaderPrecision.FAST`) is designed for maximum + performance but does not work consistently on different platforms and + individual CPU configurations. In many cases, this level of precision + is sufficient to create graphic effects without visible artifacts. + + The precision mode selection affects the following shader operations. + These operations are faster on an Intel processor with the SSE + instruction set: + + * `sin(x)` + * `cos(x)` + * `tan(x)` + * `asin(x)` + * `acos(x)` + * `atan(x)` + * `atan(x, y)` + * `exp(x)` + * `exp2(x)` + * `log(x)` + * `log2(x)` + * `pow(x, y)` + * `reciprocal(x)` + * `sqrt(x)` + **/ + public var precisionHint:ShaderPrecision; + + /** + The compiled Program3D if available. + + This property is not available on the Flash target. + **/ + public var program:Program3D; + + @:noCompletion private var __alpha:ShaderParameter; + @:noCompletion private var __bitmap:ShaderInput; + @:noCompletion private var __colorMultiplier:ShaderParameter; + @:noCompletion private var __colorOffset:ShaderParameter; + @:noCompletion private var __context:Context3D; + @:noCompletion private var __data:ShaderData; + @:noCompletion private var __glFragmentSource:String; + @:noCompletion private var __glSourceDirty:Bool; + @:noCompletion private var __glVertexSource:String; + @:noCompletion private var __hasColorTransform:ShaderParameter; + @:noCompletion private var __inputBitmapData:Array>; + @:noCompletion private var __isGenerated:Bool; + @:noCompletion private var __matrix:ShaderParameter; + @:noCompletion private var __numPasses:Int; + @:noCompletion private var __paramBool:Array>; + @:noCompletion private var __paramFloat:Array>; + @:noCompletion private var __paramInt:Array>; + @:noCompletion private var __position:ShaderParameter; + @:noCompletion private var __textureCoord:ShaderParameter; + @:noCompletion private var __texture:ShaderInput; + @:noCompletion private var __textureSize:ShaderParameter; + + #if openfljs + @:noCompletion private static function __init__() + { + untyped Object.defineProperties(Shader.prototype, { + "data": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_data (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_data (v); }") + }, + "glFragmentSource": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_glFragmentSource (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_glFragmentSource (v); }") + }, + "glVertexSource": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_glVertexSource (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_glVertexSource (v); }") + }, + }); + } + #end + + /** + Creates a new Shader instance. + + @param code The raw shader bytecode to link to the Shader. + **/ + public function new(code:ByteArray = null) + { + byteCode = code; + precisionHint = FULL; + + __glSourceDirty = true; + __numPasses = 1; + __data = new ShaderData(code); + } + + @:noCompletion private function __clearUseArray():Void + { + for (parameter in __paramBool) + { + parameter.__useArray = false; + } + + for (parameter in __paramFloat) + { + parameter.__useArray = false; + } + + for (parameter in __paramInt) + { + parameter.__useArray = false; + } + } + + // private function __clone ():Shader { + // var classType = Type.getClass (this); + // var shader = Type.createInstance (classType, []); + // for (input in __inputBitmapData) { + // if (input.input != null) { + // var field = Reflect.field (shader.data, input.name); + // field.channels = input.channels; + // field.height = input.height; + // field.input = input.input; + // field.smoothing = input.smoothing; + // field.width = input.width; + // } + // } + // for (param in __paramBool) { + // if (param.value != null) { + // Reflect.field (shader.data, param.name).value = param.value.copy (); + // } + // } + // for (param in __paramFloat) { + // if (param.value != null) { + // Reflect.field (shader.data, param.name).value = param.value.copy (); + // } + // } + // for (param in __paramInt) { + // if (param.value != null) { + // Reflect.field (shader.data, param.name).value = param.value.copy (); + // } + // } + // return shader; + // } + @:noCompletion private function __createGLShader(source:String, type:Int):GLShader + { + var gl = __context.gl; + + var shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + var shaderInfoLog = gl.getShaderInfoLog(shader); + var hasInfoLog = shaderInfoLog != null && StringTools.trim(shaderInfoLog) != ""; + var compileStatus = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + + if (hasInfoLog || compileStatus == 0) + { + final startMessage = '${(compileStatus == 0) ? "Error" : "Info" } ${(type == gl.VERTEX_SHADER) ? "compiling vertex shader" : "compiling fragment shader"}'; + var message = startMessage; + message += "\n" + shaderInfoLog; + message += "\n" + source; + #if sys + if (compileStatus == 0) + { + try + { + if (!sys.FileSystem.exists('logs')) + sys.FileSystem.createDirectory('logs'); + + sys.io.File.saveContent('logs/' + 'ShaderCompileError.txt', '$message'); + } + catch (e:haxe.Exception) + Log.warn('Couldn\'t save error message. (${e.message})', null); + } + #end + if (compileStatus == 0) + #if (android && !macro) + android.Tools.showAlertDialog("Shader Compile Error!", message, null, null) + #elseif !ios + openfl.Lib.application.window.alert('$message', 'Shader Compile Error!') + #else + Log.error(message) + #end; + else if (hasInfoLog) + Log.debug(message); + } + + return shader; + } + + @:noCompletion private function __createGLProgram(vertexSource:String, fragmentSource:String):GLProgram + { + var gl = __context.gl; + + var vertexShader = __createGLShader(vertexSource, gl.VERTEX_SHADER); + var fragmentShader = __createGLShader(fragmentSource, gl.FRAGMENT_SHADER); + + var program = gl.createProgram(); + + // Fix support for drivers that don't draw if attribute 0 is disabled + for (param in __paramFloat) + { + if (param.name.indexOf("Position") > -1 && StringTools.startsWith(param.name, "openfl_")) + { + gl.bindAttribLocation(program, 0, param.name); + break; + } + } + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (gl.getProgramParameter(program, gl.LINK_STATUS) == 0) + { + var message = "Unable to initialize the shader program"; + message += "\n" + gl.getProgramInfoLog(program); + Log.error(message); + } + + return program; + } + + @:noCompletion private function __disable():Void + { + if (program != null) + { + __disableGL(); + } + } + + @:noCompletion private function __disableGL():Void + { + var gl = __context.gl; + + var textureCount = 0; + + for (input in __inputBitmapData) + { + input.__disableGL(__context, textureCount); + textureCount++; + if (textureCount == gl.MAX_TEXTURE_IMAGE_UNITS) + break; + } + + for (parameter in __paramBool) + { + parameter.__disableGL(__context); + } + + for (parameter in __paramFloat) + { + parameter.__disableGL(__context); + } + + for (parameter in __paramInt) + { + parameter.__disableGL(__context); + } + + __context.__bindGLArrayBuffer(null); + + #if lime + if (__context.__context.type == OPENGL) + { + gl.disable(gl.TEXTURE_2D); + } + #end + } + + @:noCompletion private function __enable():Void + { + __init(); + + if (program != null) + { + __enableGL(); + } + } + + @:noCompletion private function __enableGL():Void + { + var textureCount = 0; + + var gl = __context.gl; + + for (input in __inputBitmapData) + { + gl.uniform1i(input.index, textureCount); + textureCount++; + } + + #if lime + if (__context.__context.type == OPENGL && textureCount > 0) + { + gl.enable(gl.TEXTURE_2D); + } + #end + } + + @:noCompletion private function __init():Void + { + if (__data == null) + { + __data = cast new ShaderData(null); + } + + if (__glFragmentSource != null && __glVertexSource != null && (program == null || __glSourceDirty)) + { + __initGL(); + } + } + + @:noCompletion private function __initGL():Void + { + if (__glSourceDirty || __paramBool == null) + { + __glSourceDirty = false; + program = null; + + __inputBitmapData = new Array(); + __paramBool = new Array(); + __paramFloat = new Array(); + __paramInt = new Array(); + + __processGLData(glVertexSource, "attribute"); + __processGLData(glVertexSource, "uniform"); + __processGLData(glFragmentSource, "uniform"); + } + + if (__context != null && program == null) + { + var gl = __context.gl; + + #if (js && html5) + var prefix = (precisionHint == FULL ? "precision mediump float;\n" : "precision lowp float;\n"); + #else + var prefix = "#ifdef GL_ES\n" + + (precisionHint == FULL ? "#ifdef GL_FRAGMENT_PRECISION_HIGH\n" + + "precision highp float;\n" + + "#else\n" + + "precision mediump float;\n" + + "#endif\n" : "precision lowp float;\n") + + "#endif\n\n"; + #end + + var vertex = prefix + glVertexSource; + var fragment = prefix + glFragmentSource; + + var id = vertex + fragment; + + if (__context.__programs.exists(id)) + { + program = __context.__programs.get(id); + } + else + { + program = __context.createProgram(GLSL); + + // TODO + // program.uploadSources (vertex, fragment); + program.__glProgram = __createGLProgram(vertex, fragment); + + __context.__programs.set(id, program); + } + + if (program != null) + { + glProgram = program.__glProgram; + + for (input in __inputBitmapData) + { + if (input.__isUniform) + { + input.index = gl.getUniformLocation(glProgram, input.name); + } + else + { + input.index = gl.getAttribLocation(glProgram, input.name); + } + } + + for (parameter in __paramBool) + { + if (parameter.__isUniform) + { + parameter.index = gl.getUniformLocation(glProgram, parameter.name); + } + else + { + parameter.index = gl.getAttribLocation(glProgram, parameter.name); + } + } + + for (parameter in __paramFloat) + { + if (parameter.__isUniform) + { + parameter.index = gl.getUniformLocation(glProgram, parameter.name); + } + else + { + parameter.index = gl.getAttribLocation(glProgram, parameter.name); + } + } + + for (parameter in __paramInt) + { + if (parameter.__isUniform) + { + parameter.index = gl.getUniformLocation(glProgram, parameter.name); + } + else + { + parameter.index = gl.getAttribLocation(glProgram, parameter.name); + } + } + } + } + } + + @:noCompletion private function __processGLData(source:String, storageType:String):Void + { + var lastMatch = 0, position, regex, name, type; + + if (storageType == "uniform") + { + regex = ~/uniform ([A-Za-z0-9]+) ([A-Za-z0-9_]+)/; + } + else + { + regex = ~/attribute ([A-Za-z0-9]+) ([A-Za-z0-9_]+)/; + } + + while (regex.matchSub(source, lastMatch)) + { + type = regex.matched(1); + name = regex.matched(2); + + if (StringTools.startsWith(name, "gl_")) + { + continue; + } + + var isUniform = (storageType == "uniform"); + + if (StringTools.startsWith(type, "sampler")) + { + var input = new ShaderInput(); + input.name = name; + input.__isUniform = isUniform; + __inputBitmapData.push(input); + + switch (name) + { + case "openfl_Texture": + __texture = input; + case "bitmap": + __bitmap = input; + default: + } + + Reflect.setField(__data, name, input); + if (__isGenerated) + Reflect.setField(this, name, input); + } + else if (!Reflect.hasField(__data, name) || Reflect.field(__data, name) == null) + { + var parameterType:ShaderParameterType = switch (type) + { + case "bool": BOOL; + case "double", "float": FLOAT; + case "int", "uint": INT; + case "bvec2": BOOL2; + case "bvec3": BOOL3; + case "bvec4": BOOL4; + case "ivec2", "uvec2": INT2; + case "ivec3", "uvec3": INT3; + case "ivec4", "uvec4": INT4; + case "vec2", "dvec2": FLOAT2; + case "vec3", "dvec3": FLOAT3; + case "vec4", "dvec4": FLOAT4; + case "mat2", "mat2x2": MATRIX2X2; + case "mat2x3": MATRIX2X3; + case "mat2x4": MATRIX2X4; + case "mat3x2": MATRIX3X2; + case "mat3", "mat3x3": MATRIX3X3; + case "mat3x4": MATRIX3X4; + case "mat4x2": MATRIX4X2; + case "mat4x3": MATRIX4X3; + case "mat4", "mat4x4": MATRIX4X4; + default: null; + } + + var length = switch (parameterType) + { + case BOOL2, INT2, FLOAT2: 2; + case BOOL3, INT3, FLOAT3: 3; + case BOOL4, INT4, FLOAT4, MATRIX2X2: 4; + case MATRIX3X3: 9; + case MATRIX4X4: 16; + default: 1; + } + + var arrayLength = switch (parameterType) + { + case MATRIX2X2: 2; + case MATRIX3X3: 3; + case MATRIX4X4: 4; + default: 1; + } + + switch (parameterType) + { + case BOOL, BOOL2, BOOL3, BOOL4: + var parameter = new ShaderParameter(); + parameter.name = name; + parameter.type = parameterType; + parameter.__arrayLength = arrayLength; + parameter.__isBool = true; + parameter.__isUniform = isUniform; + parameter.__length = length; + __paramBool.push(parameter); + + if (name == "openfl_HasColorTransform") + { + __hasColorTransform = parameter; + } + + Reflect.setField(__data, name, parameter); + if (__isGenerated) + Reflect.setField(this, name, parameter); + + case INT, INT2, INT3, INT4: + var parameter = new ShaderParameter(); + parameter.name = name; + parameter.type = parameterType; + parameter.__arrayLength = arrayLength; + parameter.__isInt = true; + parameter.__isUniform = isUniform; + parameter.__length = length; + __paramInt.push(parameter); + Reflect.setField(__data, name, parameter); + if (__isGenerated) + Reflect.setField(this, name, parameter); + + default: + var parameter = new ShaderParameter(); + parameter.name = name; + parameter.type = parameterType; + parameter.__arrayLength = arrayLength; + #if lime + if (arrayLength > 0) + parameter.__uniformMatrix = new Float32Array(arrayLength * arrayLength); + #end + parameter.__isFloat = true; + parameter.__isUniform = isUniform; + parameter.__length = length; + __paramFloat.push(parameter); + + if (StringTools.startsWith(name, "openfl_")) + { + switch (name) + { + case "openfl_Alpha": __alpha = parameter; + case "openfl_ColorMultiplier": __colorMultiplier = parameter; + case "openfl_ColorOffset": __colorOffset = parameter; + case "openfl_Matrix": __matrix = parameter; + case "openfl_Position": __position = parameter; + case "openfl_TextureCoord": __textureCoord = parameter; + case "openfl_TextureSize": __textureSize = parameter; + default: + } + } + + Reflect.setField(__data, name, parameter); + if (__isGenerated) + Reflect.setField(this, name, parameter); + } + } + + position = regex.matchedPos(); + lastMatch = position.pos + position.len; + } + } + + @:noCompletion private function __update():Void + { + if (program != null) + { + __updateGL(); + } + } + + @:noCompletion private function __updateFromBuffer(shaderBuffer:ShaderBuffer, bufferOffset:Int):Void + { + if (program != null) + { + __updateGLFromBuffer(shaderBuffer, bufferOffset); + } + } + + @:noCompletion private function __updateGL():Void + { + var textureCount = 0; + + for (input in __inputBitmapData) + { + input.__updateGL(__context, textureCount); + textureCount++; + } + + for (parameter in __paramBool) + { + parameter.__updateGL(__context); + } + + for (parameter in __paramFloat) + { + parameter.__updateGL(__context); + } + + for (parameter in __paramInt) + { + parameter.__updateGL(__context); + } + } + + @:noCompletion private function __updateGLFromBuffer(shaderBuffer:ShaderBuffer, bufferOffset:Int):Void + { + var textureCount = 0; + var input, inputData, inputFilter, inputMipFilter, inputWrap; + + for (i in 0...shaderBuffer.inputCount) + { + input = shaderBuffer.inputRefs[i]; + inputData = shaderBuffer.inputs[i]; + inputFilter = shaderBuffer.inputFilter[i]; + inputMipFilter = shaderBuffer.inputMipFilter[i]; + inputWrap = shaderBuffer.inputWrap[i]; + + if (inputData != null) + { + input.__updateGL(__context, textureCount, inputData, inputFilter, inputMipFilter, inputWrap); + textureCount++; + } + } + + var gl = __context.gl; + + if (shaderBuffer.paramDataLength > 0) + { + if (shaderBuffer.paramDataBuffer == null) + { + shaderBuffer.paramDataBuffer = gl.createBuffer(); + } + + // Log.verbose ("bind param data buffer (length: " + shaderBuffer.paramData.length + ") (" + shaderBuffer.paramCount + ")"); + + __context.__bindGLArrayBuffer(shaderBuffer.paramDataBuffer); + gl.bufferData(gl.ARRAY_BUFFER, shaderBuffer.paramData, gl.DYNAMIC_DRAW); + } + else + { + // Log.verbose ("bind buffer null"); + + __context.__bindGLArrayBuffer(null); + } + + var boolIndex = 0; + var floatIndex = 0; + var intIndex = 0; + + var boolCount = shaderBuffer.paramBoolCount; + var floatCount = shaderBuffer.paramFloatCount; + var paramData = shaderBuffer.paramData; + + var boolRef, floatRef, intRef, hasOverride; + var overrideBoolValue:Array = null, + overrideFloatValue:Array = null, + overrideIntValue:Array = null; + + for (i in 0...shaderBuffer.paramCount) + { + hasOverride = false; + + if (i < boolCount) + { + boolRef = shaderBuffer.paramRefs_Bool[boolIndex]; + + for (j in 0...shaderBuffer.overrideBoolCount) + { + if (boolRef.name == shaderBuffer.overrideBoolNames[j]) + { + overrideBoolValue = shaderBuffer.overrideBoolValues[j]; + hasOverride = true; + break; + } + } + + if (hasOverride) + { + boolRef.__updateGL(__context, overrideBoolValue); + } + else + { + boolRef.__updateGLFromBuffer(__context, paramData, shaderBuffer.paramPositions[i], shaderBuffer.paramLengths[i], bufferOffset); + } + + boolIndex++; + } + else if (i < boolCount + floatCount) + { + floatRef = shaderBuffer.paramRefs_Float[floatIndex]; + + for (j in 0...shaderBuffer.overrideFloatCount) + { + if (floatRef.name == shaderBuffer.overrideFloatNames[j]) + { + overrideFloatValue = shaderBuffer.overrideFloatValues[j]; + hasOverride = true; + break; + } + } + + if (hasOverride) + { + floatRef.__updateGL(__context, overrideFloatValue); + } + else + { + floatRef.__updateGLFromBuffer(__context, paramData, shaderBuffer.paramPositions[i], shaderBuffer.paramLengths[i], bufferOffset); + } + + floatIndex++; + } + else + { + intRef = shaderBuffer.paramRefs_Int[intIndex]; + + for (j in 0...shaderBuffer.overrideIntCount) + { + if (intRef.name == shaderBuffer.overrideIntNames[j]) + { + overrideIntValue = cast shaderBuffer.overrideIntValues[j]; + hasOverride = true; + break; + } + } + + if (hasOverride) + { + intRef.__updateGL(__context, overrideIntValue); + } + else + { + intRef.__updateGLFromBuffer(__context, paramData, shaderBuffer.paramPositions[i], shaderBuffer.paramLengths[i], bufferOffset); + } + + intIndex++; + } + } + } + + // Get & Set Methods + @:noCompletion private function get_data():ShaderData + { + if (__glSourceDirty || __data == null) + { + __init(); + } + + return __data; + } + + @:noCompletion private function set_data(value:ShaderData):ShaderData + { + return __data = cast value; + } + + @:noCompletion private function get_glFragmentSource():String + { + return __glFragmentSource; + } + + @:noCompletion private function set_glFragmentSource(value:String):String + { + if (value != __glFragmentSource) + { + __glSourceDirty = true; + } + + return __glFragmentSource = value; + } + + @:noCompletion private function get_glVertexSource():String + { + return __glVertexSource; + } + + @:noCompletion private function set_glVertexSource(value:String):String + { + if (value != __glVertexSource) + { + __glSourceDirty = true; + } + + return __glVertexSource = value; + } +} +#else +typedef Shader = flash.display.Shader; +#end \ No newline at end of file diff --git a/source/openfl/utils/Assets.hx b/source/openfl/utils/Assets.hx index db92e5303..13538288c 100644 --- a/source/openfl/utils/Assets.hx +++ b/source/openfl/utils/Assets.hx @@ -614,6 +614,7 @@ class Assets } else { + @:privateAccess LimeAssets.libraries.remove(name); _library = new AssetLibrary(); _library.__proxy = library; LimeAssets.registerLibrary(name, _library);