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 @@
\ 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);