diff --git a/.gitignore b/.gitignore
index 9bb0631d3..4df45b480 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,8 @@ DerivedData
*.perspectivev3
!default.perspectivev3
xcuserdata
+*.xcodeproj
+*.xcworkspace
## Other
*.xccheckout
diff --git a/.gitmodules b/.gitmodules
index 6bf44d67e..f854cfd2a 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,30 +1,36 @@
-[submodule "Cores/DeltaCore"]
- path = Cores/DeltaCore
- url = git@github.com:rileytestut/DeltaCore.git
[submodule "Cores/SNESDeltaCore"]
path = Cores/SNESDeltaCore
url = git@github.com:rileytestut/SNESDeltaCore.git
-[submodule "External/Roxas"]
- path = External/Roxas
- url = git@github.com:rileytestut/Roxas.git
[submodule "Cores/GBADeltaCore"]
path = Cores/GBADeltaCore
url = git@github.com:rileytestut/GBADeltaCore.git
[submodule "Cores/GBCDeltaCore"]
path = Cores/GBCDeltaCore
url = git@github.com:rileytestut/GBCDeltaCore.git
-[submodule "External/Harmony"]
- path = External/Harmony
- url = https://github.com/rileytestut/Harmony.git
[submodule "Cores/NESDeltaCore"]
path = Cores/NESDeltaCore
url = git@github.com:rileytestut/NESDeltaCore.git
-[submodule "Cores/N64DeltaCore"]
- path = Cores/N64DeltaCore
- url = git@github.com:rileytestut/N64DeltaCore.git
-[submodule "Cores/DSDeltaCore"]
- path = Cores/DSDeltaCore
- url = https://github.com/rileytestut/DSDeltaCore.git
-[submodule "Cores/MelonDSDeltaCore"]
- path = Cores/MelonDSDeltaCore
- url = https://github.com/rileytestut/MelonDSDeltaCore.git
+[submodule "Cores/MelonDSDeltaCore/melonDS"]
+ path = Cores/MelonDSDeltaCore/melonDS
+ url = git@github.com:rileytestut/melonDS.git
+[submodule "Cores/Mupen64PlusDeltaCore/Mupen64Plus/mupen64plus-core"]
+ path = Cores/Mupen64PlusDeltaCore/Mupen64Plus/mupen64plus-core
+ url = git@github.com:rileytestutrileytestut/mupen64plus-core.git
+[submodule "Cores/Mupen64PlusDeltaCore/Mupen64Plus/GLideN64"]
+ path = Cores/Mupen64PlusDeltaCore/Mupen64Plus/GLideN64
+ url = git@github.com:rileytestut/GLideN64.git
+[submodule "Cores/Mupen64PlusDeltaCore/Mupen64Plus/libpng"]
+ path = Cores/Mupen64PlusDeltaCore/Mupen64Plus/libpng
+ url = git://git.code.sf.net/p/libpng/code
+[submodule "Cores/Mupen64PlusDeltaCore/Mupen64Plus/mupen64plus-rsp-hle"]
+ path = Cores/Mupen64PlusDeltaCore/Mupen64Plus/mupen64plus-rsp-hle
+ url = git@github.com:mupen64plus/mupen64plus-rsp-hle.git
+[submodule "Cores/DeSmuMEDeltaCore/desmume"]
+ path = Cores/DeSmuMEDeltaCore/desmume
+ url = git@github.com:TASVideos/desmume.git
+[submodule "External/Harmony"]
+ path = External/Harmony
+ url = git@github.com:rileytestut/Harmony.git
+[submodule "External/Roxas"]
+ path = External/Roxas
+ url = git@github.com:rileytestut/Roxas.git
diff --git a/.package.resolved b/.package.resolved
new file mode 100644
index 000000000..3fd8853da
--- /dev/null
+++ b/.package.resolved
@@ -0,0 +1,16 @@
+{
+ "object": {
+ "pins": [
+ {
+ "package": "ZIPFoundation",
+ "repositoryURL": "https://github.com/rileytestut/ZIPFoundation",
+ "state": {
+ "branch": "development",
+ "revision": "9ea4da96aae5ae4842f81aed684e10cff057d7b9",
+ "version": null
+ }
+ }
+ ]
+ },
+ "version": 1
+}
diff --git a/.tuist-version b/.tuist-version
new file mode 100644
index 000000000..83cf0d951
--- /dev/null
+++ b/.tuist-version
@@ -0,0 +1 @@
+1.29.1
diff --git a/Cores/DSDeltaCore b/Cores/DSDeltaCore
deleted file mode 160000
index b59ca1970..000000000
--- a/Cores/DSDeltaCore
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit b59ca197068be7cf8ca66ff52b24e686154a1c15
diff --git a/Cores/DeSmuMEDeltaCore/Derived/InfoPlists/DeSmuMEDeltaCore.plist b/Cores/DeSmuMEDeltaCore/Derived/InfoPlists/DeSmuMEDeltaCore.plist
new file mode 100644
index 000000000..323e5ecfc
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/Derived/InfoPlists/DeSmuMEDeltaCore.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+
+
diff --git a/Cores/DeSmuMEDeltaCore/Derived/Sources/Bundle+DeSmuMEDeltaCore.swift b/Cores/DeSmuMEDeltaCore/Derived/Sources/Bundle+DeSmuMEDeltaCore.swift
new file mode 100644
index 000000000..aac745f22
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/Derived/Sources/Bundle+DeSmuMEDeltaCore.swift
@@ -0,0 +1,23 @@
+// swiftlint:disable all
+import Foundation
+
+// MARK: - Swift Bundle Accessor
+
+private class BundleFinder {}
+
+extension Foundation.Bundle {
+ /// Since DeSmuMEDeltaCore is a framework, the bundle containing the resources is copied into the final product.
+ static var module: Bundle = {
+ return Bundle(for: BundleFinder.self)
+ }()
+}
+
+// MARK: - Objective-C Bundle Accessor
+
+@objc
+public class DeSmuMEDeltaCoreResources: NSObject {
+ @objc public class var bundle: Bundle {
+ return .module
+ }
+}
+// swiftlint:enable all
\ No newline at end of file
diff --git a/Cores/DeSmuMEDeltaCore/Project.swift b/Cores/DeSmuMEDeltaCore/Project.swift
new file mode 100644
index 000000000..e8ac1610c
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/Project.swift
@@ -0,0 +1,85 @@
+import ProjectDescription
+
+let project = Project(name: "DeSmuMEDeltaCore",
+ packages: [],
+ targets: [
+ Target(name: "DeSmuMEDeltaCore",
+ platform: .iOS,
+ product: .framework,
+ bundleId: "com.rileytestut.desmumeDeltaCore",
+ deploymentTarget: .iOS(targetVersion: "12.2", devices: [.iphone, .ipad]),
+ infoPlist: .extendingDefault(with: [:]),
+ sources: ["Sources/**/*.swift", "Sources/Bridge/DeSmuMEEmulatorBridge.{h,mm}"],
+ resources: ["Resources/**/*.{deltamapping,deltaskin}"],
+ headers: Headers(public: ["Sources/DeSmuMEDeltaCore.h", "Sources/Bridge/DeSmuMEEmulatorBridge.h"], project: "Sources/Types/DeSmuMETypes.h"),
+ dependencies: [
+ .project(target: "DeltaCore", path: "../DeltaCore"),
+ .target(name: "libDeSMuME"),
+ .sdk(name: "libz.tbd"),
+ ],
+ settings: Settings(base: [
+ "HEADER_SEARCH_PATHS": "\"$(SRCROOT)/desmume/desmume/src/libretro-common/include\"",
+ "OTHER_CFLAGS": "-DHOST_DARWIN -DDESMUME_COCOA -DHAVE_OPENGL -DHAVE_LIBZ -DANDROID -fexceptions -ftree-vectorize -DCOMPRESS_MT -DIOS -DOBJ_C -marm -fvisibility=hidden"
+ ])),
+ Target(name: "libDeSMuME",
+ platform: .iOS,
+ product: .staticLibrary,
+ bundleId: "com.rileytestut.libDeSMuME",
+ deploymentTarget: .iOS(targetVersion: "12.2", devices: [.iphone, .ipad]),
+ infoPlist: .extendingDefault(with: [:]),
+ sources: [
+ SourceFileGlob("desmume/desmume/src/**/*.{c,cpp}", excluding: [
+ "desmume/desmume/src/OGLRender*.{c,cpp}",
+ "desmume/desmume/src/lua-engine.{c,cpp}",
+ "desmume/desmume/src/frontend/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/algorithms/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/compat/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/conversion/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/crt/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/dynamic/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/file/nbio/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/file/archive_*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/file/config_file.{c,cpp}",
+ "desmume/desmume/src/libretro-common/file/config_file.{c,cpp}",
+ "desmume/desmume/src/libretro-common/formats/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/gfx/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/glsm/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/glsym/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/hash/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/include/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/libco/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/lists/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/memmap/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/net/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/queues/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/rthreads/async_job.{c,cpp}",
+ "desmume/desmume/src/libretro-common/rthreads/rsemaphore.{c,cpp}",
+ "desmume/desmume/src/libretro-common/rthreads/xenon_sdl_threads.{c,cpp}",
+ "desmume/desmume/src/libretro-common/streams/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/string/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/utils/**/*.{c,cpp}",
+ "desmume/desmume/src/libretro-common/vulkan/**/*.{c,cpp}",
+ "desmume/desmume/src/metaspu/SoundTouch/*_win.{c,cpp}",
+ "desmume/desmume/src/metaspu/win32/**/*.{c,cpp}",
+ "desmume/desmume/src/utils/arm_arm/**/*.{c,cpp}",
+ "desmume/desmume/src/utils/AsmJit/**/*.{c,cpp}",
+ "desmume/desmume/src/utils/colorspacehandler/*_*.{c,cpp}"
+ ])
+ ],
+ headers: Headers(
+ project: [
+ "desmume/desmume/src/*.{h,hpp}",
+ "desmume/desmume/src/libretro-common/include/*.{h,hpp}",
+ "desmume/desmume/src/libretro-common/include/math/*.{h,hpp}",
+ "desmume/desmume/src/metaspu/**/*.{h,hpp}",
+ "libDeSmuME/*.{h,hpp}"
+ ]
+ ),
+ settings: Settings(base: [
+ "OTHER_LDFLAGS": "-ObjC",
+ "CLANG_CXX_LANGUAGE_STANDARD": "compiler-default",
+ "CLANG_CXX_LIBRARY": "compiler-default",
+ "HEADER_SEARCH_PATHS": "\"$(SRCROOT)/desmume/desmume/src/libretro-common/include\"",
+ "OTHER_CFLAGS": "-DHOST_DARWIN -DDESMUME_COCOA -DHAVE_OPENGL -DHAVE_LIBZ -DANDROID -fexceptions -ftree-vectorize -DCOMPRESS_MT -DIOS -DOBJ_C -marm -fvisibility=hidden"
+ ])),
+ ])
diff --git a/Cores/DeSmuMEDeltaCore/Resources/Standard.deltamapping b/Cores/DeSmuMEDeltaCore/Resources/Standard.deltamapping
new file mode 100644
index 000000000..4f87c0f39
Binary files /dev/null and b/Cores/DeSmuMEDeltaCore/Resources/Standard.deltamapping differ
diff --git a/Cores/DeSmuMEDeltaCore/Resources/Standard.deltaskin b/Cores/DeSmuMEDeltaCore/Resources/Standard.deltaskin
new file mode 100644
index 000000000..8f5adc4dc
Binary files /dev/null and b/Cores/DeSmuMEDeltaCore/Resources/Standard.deltaskin differ
diff --git a/Cores/DeSmuMEDeltaCore/Resources/info.json b/Cores/DeSmuMEDeltaCore/Resources/info.json
new file mode 100644
index 000000000..0ed555b18
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/Resources/info.json
@@ -0,0 +1,921 @@
+{
+ "name" : "Standard DS",
+ "identifier" : "com.delta.ds.standard",
+ "gameTypeIdentifier" : "com.rileytestut.delta.game.ds",
+ "debug" : false,
+ "representations" : {
+ "iphone" : {
+ "standard" : {
+ "portrait" : {
+ "assets" : {
+ "resizable" : "iphone_portrait.pdf"
+ },
+ "items": [
+ {
+ "inputs": {
+ "up": "up",
+ "down": "down",
+ "left": "left",
+ "right": "right"
+ },
+ "frame": {
+ "x": 15,
+ "y": 500,
+ "width": 128,
+ "height": 128
+ },
+ "extendedEdges": {
+ "top": 15,
+ "bottom": 15,
+ "left": 17,
+ "right": 10
+ }
+ },
+ {
+ "inputs": [
+ "a"
+ ],
+ "frame": {
+ "x": 313,
+ "y": 540,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 16
+ }
+ },
+ {
+ "inputs": [
+ "b"
+ ],
+ "frame": {
+ "x": 267,
+ "y": 586,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "l"
+ ],
+ "frame": {
+ "x": 18,
+ "y": 366,
+ "width": 32,
+ "height": 112
+ },
+ "extendedEdges": {
+ "right": 0,
+ "left": 18
+ }
+ },
+ {
+ "inputs": [
+ "r"
+ ],
+ "frame": {
+ "x": 325,
+ "y": 366,
+ "width": 32,
+ "height": 112
+ },
+ "extendedEdges": {
+ "left": 0,
+ "right": 18
+ }
+ },
+ {
+ "inputs": [
+ "start"
+ ],
+ "frame": {
+ "x": 154,
+ "y": 600,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "top": 15,
+ "bottom": 0,
+ "left": 0,
+ "right": 45
+ }
+ },
+ {
+ "inputs": [
+ "select"
+ ],
+ "frame": {
+ "x": 154,
+ "y": 631,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 15,
+ "left": 0,
+ "right": 45
+ }
+ },
+ {
+ "inputs": [
+ "menu"
+ ],
+ "frame": {
+ "x": 154,
+ "y": 498,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "left": 0,
+ "right": 45
+ }
+ },
+ {
+ "inputs": [
+ "y"
+ ],
+ "frame": {
+ "x": 221,
+ "y": 540,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "x"
+ ],
+ "frame": {
+ "x": 267,
+ "y": 494,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": {
+ "x": "touchScreenX",
+ "y": "touchScreenY"
+ },
+ "frame": {
+ "x": 50,
+ "y": 252,
+ "width": 275,
+ "height": 206
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ }
+ ],
+ "screens": [
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 0,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 50,
+ "y": 18,
+ "width": 275,
+ "height": 206
+ }
+ },
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 192,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 50,
+ "y": 252,
+ "width": 275,
+ "height": 206
+ }
+ }
+ ],
+ "mappingSize" : {
+ "width" : 375,
+ "height" : 667
+ },
+ "extendedEdges" : {
+ "top" : 7,
+ "bottom" : 7,
+ "left" : 7,
+ "right" : 7
+ }
+ },
+ "landscape" : {
+ "assets" : {
+ "resizable" : "iphone_landscape.pdf"
+ },
+ "items": [
+ {
+ "inputs": {
+ "up": "up",
+ "down": "down",
+ "left": "left",
+ "right": "right"
+ },
+ "frame": {
+ "x": 15,
+ "y": 235,
+ "width": 123,
+ "height": 123
+ },
+ "extendedEdges": {
+ "top": 15,
+ "bottom": 20,
+ "left": 20,
+ "right": 20
+ }
+ },
+ {
+ "inputs": [
+ "a"
+ ],
+ "frame": {
+ "x": 606,
+ "y": 273,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 30
+ }
+ },
+ {
+ "inputs": [
+ "b"
+ ],
+ "frame": {
+ "x": 562,
+ "y": 318,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "l"
+ ],
+ "frame": {
+ "x": 173,
+ "y": 219,
+ "width": 135,
+ "height": 33
+ },
+ "extendedEdges": {
+ "top": 0
+ }
+ },
+ {
+ "inputs": [
+ "r"
+ ],
+ "frame": {
+ "x": 359,
+ "y": 219,
+ "width": 135,
+ "height": 33
+ },
+ "extendedEdges": {
+ "top": 0
+ }
+ },
+ {
+ "inputs": [
+ "start"
+ ],
+ "frame": {
+ "x": 304,
+ "y": 287,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "right": 50
+ }
+ },
+ {
+ "inputs": [
+ "select"
+ ],
+ "frame": {
+ "x": 410,
+ "y": 288,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "right": 50
+ }
+ },
+ {
+ "inputs": [
+ "menu"
+ ],
+ "frame": {
+ "x": 195,
+ "y": 288,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "right": 50
+ }
+ },
+ {
+ "inputs": [
+ "x"
+ ],
+ "frame": {
+ "x": 562,
+ "y": 229,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 8,
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "y"
+ ],
+ "frame": {
+ "x": 517,
+ "y": 273,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": {
+ "x": "touchScreenX",
+ "y": "touchScreenY"
+ },
+ "frame": {
+ "x": 361,
+ "y": 24,
+ "width": 259,
+ "height": 194
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ }
+ ],
+ "mappingSize" : {
+ "width" : 667,
+ "height" : 375
+ },
+ "screens": [
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 0,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 47,
+ "y": 24,
+ "width": 259,
+ "height": 194
+ }
+ },
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 192,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 361,
+ "y": 24,
+ "width": 259,
+ "height": 194
+ }
+ }
+ ],
+ "extendedEdges" : {
+ "top" : 15,
+ "bottom" : 15,
+ "left" : 15,
+ "right" : 15
+ }
+ }
+ },
+ "edgeToEdge" : {
+ "portrait" : {
+ "assets" : {
+ "resizable" : "iphone_edgetoedge_portrait.pdf"
+ },
+ "items" : [
+ {
+ "inputs" : {
+ "up" : "up",
+ "down" : "down",
+ "left" : "left",
+ "right" : "right"
+ },
+ "frame" : {
+ "x" : 17,
+ "y" : 698,
+ "width" : 141,
+ "height" : 141
+ },
+ "extendedEdges" : {
+ "top" : 15,
+ "bottom" : 15,
+ "left" : 17,
+ "right" : 10
+ }
+ },
+ {
+ "inputs" : [
+ "a"
+ ],
+ "frame" : {
+ "x" : 346,
+ "y" : 743,
+ "width" : 52,
+ "height" : 52
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "bottom" : 0,
+ "left" : 0,
+ "right" : 16
+ }
+ },
+ {
+ "inputs" : [
+ "b"
+ ],
+ "frame" : {
+ "x" : 295,
+ "y" : 794,
+ "width" : 52,
+ "height" : 52
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "left" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs" : [
+ "l"
+ ],
+ "frame" : {
+ "x" : 23,
+ "y" : 630,
+ "width" : 127,
+ "height" : 35
+ }
+ ,
+ "extendedEdges": {
+ "top": 0,
+ "left": 22
+ }
+ },
+ {
+ "inputs" : [
+ "r"
+ ],
+ "frame" : {
+ "x" : 263,
+ "y" : 630,
+ "width" : 127,
+ "height" : 35
+ }
+ ,
+ "extendedEdges": {
+ "top": 0,
+ "right": 22
+ }
+ },
+ {
+ "inputs" : [
+ "start"
+ ],
+ "frame" : {
+ "x" : 171,
+ "y" : 808,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges" : {
+ "top": 15,
+ "bottom": 0,
+ "left": 0,
+ "right" : 45
+ }
+ },
+ {
+ "inputs" : [
+ "select"
+ ],
+ "frame" : {
+ "x" : 171,
+ "y" : 844,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 15,
+ "left": 0,
+ "right": 45
+ }
+ },
+ {
+ "inputs" : [
+ "menu"
+ ],
+ "frame" : {
+ "x" : 170,
+ "y" : 697,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges": {
+ "left": 0,
+ "right": 45
+ }
+ },
+ {
+ "inputs" : [
+ "y"
+ ],
+ "frame" : {
+ "x" : 244,
+ "y" : 743,
+ "width" : 52,
+ "height" : 52
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "bottom" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs" : [
+ "x"
+ ],
+ "frame" : {
+ "x" : 295,
+ "y" : 692,
+ "width" : 52,
+ "height" : 52
+ },
+ "extendedEdges" : {
+ "bottom" : 0,
+ "left" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs": {
+ "x": "touchScreenX",
+ "y": "touchScreenY"
+ },
+ "frame": {
+ "x": 25,
+ "y": 366,
+ "width": 363,
+ "height": 273
+ }
+ ,
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ }
+ ],
+ "screens": [
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 0,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 25,
+ "y": 54,
+ "width": 363,
+ "height": 273
+ }
+ },
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 192,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 25,
+ "y": 366,
+ "width": 363,
+ "height": 273
+ }
+ }
+ ],
+ "mappingSize" : {
+ "width" : 414,
+ "height" : 896
+ },
+ "extendedEdges" : {
+ "top" : 7,
+ "bottom" : 7,
+ "left" : 7,
+ "right" : 7
+ }
+ },
+ "landscape" : {
+ "assets" : {
+ "resizable" : "iphone_edgetoedge_landscape.pdf"
+ },
+ "items" : [
+ {
+ "inputs" : {
+ "up" : "up",
+ "down" : "down",
+ "left" : "left",
+ "right" : "right"
+ },
+ "frame" : {
+ "x" : 69,
+ "y" : 260,
+ "width" : 136,
+ "height" : 136
+ },
+ "extendedEdges" : {
+ "left" : 30,
+ "top" : 20,
+ "right" : 20,
+ "bottom" : 20
+ }
+ },
+ {
+ "inputs" : [
+ "a"
+ ],
+ "frame" : {
+ "x" : 777,
+ "y" : 302,
+ "width" : 51,
+ "height" : 51
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "bottom" : 0,
+ "left" : 0,
+ "right" : 30
+ }
+ },
+ {
+ "inputs" : [
+ "b"
+ ],
+ "frame" : {
+ "x" : 728,
+ "y" : 351,
+ "width" : 51,
+ "height" : 51
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "left" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs" : [
+ "l"
+ ],
+ "frame" : {
+ "x" : 69,
+ "y" : 65,
+ "width" : 40,
+ "height" : 148
+ }
+ ,
+ "extendedEdges": {
+ "left": 50,
+ "right": 0
+ }
+ },
+ {
+ "inputs" : [
+ "r"
+ ],
+ "frame" : {
+ "x" : 788,
+ "y" : 65,
+ "width" : 40,
+ "height" : 148
+ }
+ ,
+ "extendedEdges": {
+ "left": 0,
+ "right": 50
+ }
+ },
+ {
+ "inputs" : [
+ "start"
+ ],
+ "frame" : {
+ "x" : 415,
+ "y" : 315,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges": {
+ "right": 50
+ }
+ },
+ {
+ "inputs" : [
+ "select"
+ ],
+ "frame" : {
+ "x" : 533,
+ "y" : 315,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges" : {
+ "right" : 50
+ }
+ },
+ {
+ "inputs" : [
+ "menu"
+ ],
+ "frame" : {
+ "x" : 295,
+ "y" : 315,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges": {
+ "right": 50
+ }
+ },
+ {
+ "inputs" : [
+ "x"
+ ],
+ "frame" : {
+ "x" : 728,
+ "y" : 253,
+ "width" : 51,
+ "height" : 51
+ },
+ "extendedEdges" : {
+ "bottom" : 0,
+ "left" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs" : [
+ "y"
+ ],
+ "frame" : {
+ "x" : 679,
+ "y" : 302,
+ "width" : 51,
+ "height" : 51
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "bottom" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs": {
+ "x": "touchScreenX",
+ "y": "touchScreenY"
+ },
+ "frame": {
+ "x": 500,
+ "y": 27,
+ "width": 286,
+ "height": 214
+ }
+ ,
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ }
+ ],
+ "mappingSize" : {
+ "width" : 896,
+ "height" : 414
+ },
+ "screens": [
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 0,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 110,
+ "y": 27,
+ "width": 286,
+ "height": 214
+ }
+ },
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 192,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 500,
+ "y": 27,
+ "width": 286,
+ "height": 214
+ }
+ }
+ ],
+ "extendedEdges" : {
+ "top" : 15,
+ "bottom" : 15,
+ "left" : 15,
+ "right" : 15
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cores/DeSmuMEDeltaCore/Resources/iphone_edgetoedge_landscape.pdf b/Cores/DeSmuMEDeltaCore/Resources/iphone_edgetoedge_landscape.pdf
new file mode 100644
index 000000000..949dc7b64
Binary files /dev/null and b/Cores/DeSmuMEDeltaCore/Resources/iphone_edgetoedge_landscape.pdf differ
diff --git a/Cores/DeSmuMEDeltaCore/Resources/iphone_edgetoedge_portrait.pdf b/Cores/DeSmuMEDeltaCore/Resources/iphone_edgetoedge_portrait.pdf
new file mode 100644
index 000000000..41e48bf4d
Binary files /dev/null and b/Cores/DeSmuMEDeltaCore/Resources/iphone_edgetoedge_portrait.pdf differ
diff --git a/Cores/DeSmuMEDeltaCore/Resources/iphone_landscape.pdf b/Cores/DeSmuMEDeltaCore/Resources/iphone_landscape.pdf
new file mode 100644
index 000000000..d33daba9b
Binary files /dev/null and b/Cores/DeSmuMEDeltaCore/Resources/iphone_landscape.pdf differ
diff --git a/Cores/DeSmuMEDeltaCore/Resources/iphone_portrait.pdf b/Cores/DeSmuMEDeltaCore/Resources/iphone_portrait.pdf
new file mode 100644
index 000000000..cc5bf29d3
Binary files /dev/null and b/Cores/DeSmuMEDeltaCore/Resources/iphone_portrait.pdf differ
diff --git a/Cores/DeSmuMEDeltaCore/Sources/Bridge/DeSmuMEEmulatorBridge.h b/Cores/DeSmuMEDeltaCore/Sources/Bridge/DeSmuMEEmulatorBridge.h
new file mode 100644
index 000000000..f33ab9940
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/Sources/Bridge/DeSmuMEEmulatorBridge.h
@@ -0,0 +1,24 @@
+//
+// DeSmuMEEmulatorBridge.h
+// DeSmuMEDeltaCore
+//
+// Created by Riley Testut on 8/2/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import
+
+@protocol DLTAEmulatorBridging;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Weverything" // Silence "Cannot find protocol definition" warning due to forward declaration.
+@interface DeSmuMEEmulatorBridge : NSObject
+#pragma clang diagnostic pop
+
+@property (class, nonatomic, readonly) DeSmuMEEmulatorBridge *sharedBridge;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Cores/DeSmuMEDeltaCore/Sources/Bridge/DeSmuMEEmulatorBridge.mm b/Cores/DeSmuMEDeltaCore/Sources/Bridge/DeSmuMEEmulatorBridge.mm
new file mode 100644
index 000000000..53dd5eb57
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/Sources/Bridge/DeSmuMEEmulatorBridge.mm
@@ -0,0 +1,357 @@
+//
+// DeSmuMEEmulatorBridge.m
+// DeSmuMEDeltaCore
+//
+// Created by Riley Testut on 8/2/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import "DeSmuMEEmulatorBridge.h"
+
+#import
+#import
+
+#if STATIC_LIBRARY
+#import "DeSmuMEDeltaCore-Swift.h"
+#else
+#import
+#endif
+
+// DeSmuME
+#include "types.h"
+#include "render3D.h"
+#include "rasterize.h"
+#include "SPU.h"
+#include "debug.h"
+#include "NDSSystem.h"
+#include "path.h"
+#include "slot1.h"
+#include "saves.h"
+#include "cheatSystem.h"
+#include "slot1.h"
+#include "version.h"
+#include "metaspu.h"
+#include "GPU.h"
+
+#undef BOOL
+
+#define SNDCORE_DELTA 1
+
+void DLTAUpdateAudio(s16 *buffer, u32 num_samples);
+u32 DLTAGetAudioSpace();
+
+SoundInterface_struct DeltaAudio = {
+ SNDCORE_DELTA,
+ "CoreAudio Sound Interface",
+ SNDDummy.Init,
+ SNDDummy.DeInit,
+ DLTAUpdateAudio,
+ DLTAGetAudioSpace,
+ SNDDummy.MuteAudio,
+ SNDDummy.UnMuteAudio,
+ SNDDummy.SetVolume,
+};
+
+volatile bool execute = true;
+
+GPU3DInterface *core3DList[] = {
+ &gpu3DNull,
+ &gpu3DRasterize,
+ NULL
+};
+
+SoundInterface_struct *SNDCoreList[] = {
+ &SNDDummy,
+ &DeltaAudio,
+ NULL
+};
+
+@interface DeSmuMEEmulatorBridge ()
+{
+ BOOL _isPrepared;
+}
+
+@property (nonatomic, copy, nullable, readwrite) NSURL *gameURL;
+
+@property (nonatomic) uint32_t activatedInputs;
+@property (nonatomic) CGPoint touchScreenPoint;
+
+@end
+
+@implementation DeSmuMEEmulatorBridge
+@synthesize audioRenderer = _audioRenderer;
+@synthesize videoRenderer = _videoRenderer;
+@synthesize saveUpdateHandler = _saveUpdateHandler;
+
++ (instancetype)sharedBridge
+{
+ static DeSmuMEEmulatorBridge *_emulatorBridge = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ _emulatorBridge = [[self alloc] init];
+ });
+
+ return _emulatorBridge;
+}
+
+#pragma mark - Emulation State -
+
+- (void)startWithGameURL:(NSURL *)gameURL
+{
+ self.gameURL = gameURL;
+
+ path.ReadPathSettings();
+
+ // General
+ CommonSettings.num_cores = (int)sysconf( _SC_NPROCESSORS_ONLN );
+ CommonSettings.advanced_timing = false;
+ CommonSettings.cheatsDisable = true;
+ CommonSettings.autodetectBackupMethod = 1;
+ CommonSettings.use_jit = false;
+ CommonSettings.micMode = TCommonSettings::Physical;
+ CommonSettings.showGpu.main = 1;
+ CommonSettings.showGpu.sub = 1;
+
+ // HUD
+ CommonSettings.hud.FpsDisplay = false;
+ CommonSettings.hud.FrameCounterDisplay = false;
+ CommonSettings.hud.ShowInputDisplay = false;
+ CommonSettings.hud.ShowGraphicalInputDisplay = false;
+ CommonSettings.hud.ShowLagFrameCounter = false;
+ CommonSettings.hud.ShowMicrophone = false;
+ CommonSettings.hud.ShowRTC = false;
+
+ // Graphics
+ CommonSettings.GFX3D_HighResolutionInterpolateColor = 0;
+ CommonSettings.GFX3D_EdgeMark = 0;
+ CommonSettings.GFX3D_Fog = 1;
+ CommonSettings.GFX3D_Texture = 1;
+ CommonSettings.GFX3D_LineHack = 0;
+
+ // Sound
+ CommonSettings.spuInterpolationMode = SPUInterpolation_Cosine;
+ CommonSettings.spu_advanced = false;
+
+ // Firmware
+ CommonSettings.fwConfig.language = NDS_FW_LANG_ENG;
+ CommonSettings.fwConfig.favoriteColor = 15;
+ CommonSettings.fwConfig.birthdayMonth = 10;
+ CommonSettings.fwConfig.birthdayDay = 7;
+ CommonSettings.fwConfig.consoleType = NDS_CONSOLE_TYPE_LITE;
+
+ static const char *nickname = "Delta";
+ CommonSettings.fwConfig.nicknameLength = strlen(nickname);
+ for(int i = 0 ; i < CommonSettings.fwConfig.nicknameLength ; ++i)
+ {
+ CommonSettings.fwConfig.nickname[i] = nickname[i];
+ }
+
+ static const char *message = "Delta is the best!";
+ CommonSettings.fwConfig.messageLength = strlen(message);
+ for(int i = 0 ; i < CommonSettings.fwConfig.messageLength ; ++i)
+ {
+ CommonSettings.fwConfig.message[i] = message[i];
+ }
+
+ if (!_isPrepared)
+ {
+ Desmume_InitOnce();
+
+ NDS_Init();
+ cur3DCore = 1;
+
+ GPU->Change3DRendererByID(1);
+ GPU->SetColorFormat(NDSColorFormat_BGR888_Rev);
+
+ SPU_ChangeSoundCore(SNDCORE_DELTA, DESMUME_SAMPLE_RATE * 8/60);
+
+ _isPrepared = true;
+ }
+
+ NSURL *gameDirectory = [NSURL URLWithString:@"/dev/null"];
+ path.setpath(PathInfo::BATTERY, gameDirectory.fileSystemRepresentation);
+
+ if (!NDS_LoadROM(gameURL.relativePath.UTF8String))
+ {
+ NSLog(@"Error loading ROM: %@", gameURL);
+ }
+}
+
+- (void)stop
+{
+ NDS_FreeROM();
+}
+
+- (void)pause
+{
+}
+
+- (void)resume
+{
+}
+
+#pragma mark - Game Loop -
+
+- (void)runFrameAndProcessVideo:(BOOL)processVideo
+{
+ // Inputs
+ NDS_setPad(self.activatedInputs & DeSmuMEGameInputRight,
+ self.activatedInputs & DeSmuMEGameInputLeft,
+ self.activatedInputs & DeSmuMEGameInputDown,
+ self.activatedInputs & DeSmuMEGameInputUp,
+ self.activatedInputs & DeSmuMEGameInputSelect,
+ self.activatedInputs & DeSmuMEGameInputStart,
+ self.activatedInputs & DeSmuMEGameInputB,
+ self.activatedInputs & DeSmuMEGameInputA,
+ self.activatedInputs & DeSmuMEGameInputY,
+ self.activatedInputs & DeSmuMEGameInputX,
+ self.activatedInputs & DeSmuMEGameInputL,
+ self.activatedInputs & DeSmuMEGameInputR,
+ false,
+ false);
+
+ if (self.activatedInputs & DeSmuMEGameInputTouchScreenX || self.activatedInputs & DeSmuMEGameInputTouchScreenY)
+ {
+ NDS_setTouchPos(self.touchScreenPoint.x, self.touchScreenPoint.y);
+ }
+ else
+ {
+ NDS_releaseTouch();
+ }
+
+ NDS_beginProcessingInput();
+ NDS_endProcessingInput();
+
+ if (!processVideo)
+ {
+ NDS_SkipNextFrame();
+ }
+
+ NDS_exec();
+
+ if (processVideo)
+ {
+ memcpy(self.videoRenderer.videoBuffer, GPU->GetDisplayInfo().masterNativeBuffer, 256 * 384 * 4);
+ [self.videoRenderer processFrame];
+ }
+
+ SPU_Emulate_user();
+}
+
+#pragma mark - Inputs -
+
+- (void)activateInput:(NSInteger)input value:(double)value
+{
+ self.activatedInputs |= (uint32_t)input;
+
+ CGPoint touchPoint = self.touchScreenPoint;
+
+ switch ((DeSmuMEGameInput)input)
+ {
+ case DeSmuMEGameInputTouchScreenX:
+ touchPoint.x = value * 256;
+ break;
+
+ case DeSmuMEGameInputTouchScreenY:
+ touchPoint.y = value * 192;
+ break;
+
+ default: break;
+ }
+
+ self.touchScreenPoint = touchPoint;
+}
+
+- (void)deactivateInput:(NSInteger)input
+{
+ self.activatedInputs &= ~((uint32_t)input);
+
+ CGPoint touchPoint = self.touchScreenPoint;
+
+ switch ((DeSmuMEGameInput)input)
+ {
+ case DeSmuMEGameInputTouchScreenX:
+ touchPoint.x = 0;
+ break;
+
+ case DeSmuMEGameInputTouchScreenY:
+ touchPoint.y = 0;
+ break;
+
+ default: break;
+ }
+
+ self.touchScreenPoint = touchPoint;
+}
+
+- (void)resetInputs
+{
+ self.activatedInputs = 0;
+ self.touchScreenPoint = CGPointZero;
+}
+
+#pragma mark - Game Saves -
+
+- (void)saveGameSaveToURL:(NSURL *)URL
+{
+ MMU_new.backupDevice.export_raw(URL.fileSystemRepresentation);
+}
+
+- (void)loadGameSaveFromURL:(NSURL *)URL
+{
+ if ([[NSFileManager defaultManager] fileExistsAtPath:URL.path])
+ {
+ MMU_new.backupDevice.import_raw(URL.fileSystemRepresentation);
+ }
+}
+
+#pragma mark - Save States -
+
+- (void)saveSaveStateToURL:(NSURL *)URL
+{
+ savestate_save(URL.fileSystemRepresentation);
+}
+
+- (void)loadSaveStateFromURL:(NSURL *)URL
+{
+ savestate_load(URL.fileSystemRepresentation);
+}
+
+#pragma mark - Cheats -
+
+- (BOOL)addCheatCode:(NSString *)cheatCode type:(NSString *)type
+{
+ return NO;
+}
+
+- (void)resetCheats
+{
+}
+
+- (void)updateCheats
+{
+}
+
+#pragma mark - Audio -
+
+void DLTAUpdateAudio(s16 *buffer, u32 num_samples)
+{
+ [DeSmuMEEmulatorBridge.sharedBridge.audioRenderer.audioBuffer writeBuffer:(uint8_t *)buffer size:num_samples * 4];
+}
+
+u32 DLTAGetAudioSpace()
+{
+ NSInteger availableBytes = DeSmuMEEmulatorBridge.sharedBridge.audioRenderer.audioBuffer.availableBytesForWriting;
+
+ u32 availableFrames = (u32)availableBytes / 4;
+ return availableFrames;
+}
+
+#pragma mark - Getters/Setters -
+
+- (NSTimeInterval)frameDuration
+{
+ return (1.0 / 60.0);
+}
+
+@end
diff --git a/Cores/DeSmuMEDeltaCore/Sources/Bridge/texcache.cpp b/Cores/DeSmuMEDeltaCore/Sources/Bridge/texcache.cpp
new file mode 100644
index 000000000..d5af6a91c
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/Sources/Bridge/texcache.cpp
@@ -0,0 +1,19 @@
+//
+// texcache.cpp
+// DSDeltaCore
+//
+// Created by Riley Testut on 2/3/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+// Rename TextureCache to prevent static library collision with N64's TextureCache.
+#define TextureCache TextureCacheDS
+
+#include "../../desmume/desmume/src/texcache.cpp"
+
+// Include files that reference texcache.h.
+#include "../../desmume/desmume/src/driver.cpp"
+#include "../../desmume/desmume/src/render3D.cpp"
+#include "../../desmume/desmume/src/rasterize.cpp"
+
+#undef TextureCache
diff --git a/Cores/DeSmuMEDeltaCore/Sources/DeSmuME.swift b/Cores/DeSmuMEDeltaCore/Sources/DeSmuME.swift
new file mode 100644
index 000000000..bec36d3c6
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/Sources/DeSmuME.swift
@@ -0,0 +1,75 @@
+//
+// DeSmuMEDeltaCore.swift
+// DeSmuMEDeltaCore
+//
+// Created by Riley Testut on 8/2/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import Foundation
+import AVFoundation
+
+import DeltaCore
+
+#if !STATIC_LIBRARY
+public extension GameType
+{
+ static let ds = GameType("com.rileytestut.delta.game.ds")
+}
+#endif
+
+@objc public enum DeSmuMEGameInput: Int, Input
+{
+ case up = 1
+ case down = 2
+ case left = 4
+ case right = 8
+ case a = 16
+ case b = 32
+ case x = 64
+ case y = 128
+ case l = 256
+ case r = 512
+ case start = 1024
+ case select = 2048
+
+ case touchScreenX = 4096
+ case touchScreenY = 8192
+
+ public var type: InputType {
+ return .game(.ds)
+ }
+
+ public var isContinuous: Bool {
+ switch self
+ {
+ case .touchScreenX, .touchScreenY: return true
+ default: return false
+ }
+ }
+}
+
+public struct DeSmuME: DeltaCoreProtocol
+{
+ public static let core = DeSmuME()
+
+ public var name: String { "DeSmuMEDeltaCore" }
+ public var identifier: String { "com.rileytestut.DeSmuMEDeltaCore" }
+
+ public var gameType: GameType { GameType.ds }
+ public var gameInputType: Input.Type { DeSmuMEGameInput.self }
+ public var gameSaveFileExtension: String { "dsv" }
+
+ public let audioFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 44100, channels: 2, interleaved: true)!
+ public let videoFormat = VideoFormat(format: .bitmap(.rgba8), dimensions: CGSize(width: 256, height: 384))
+
+ public var supportedCheatFormats: Set {
+ return []
+ }
+
+ public var emulatorBridge: EmulatorBridging { DeSmuMEEmulatorBridge.shared }
+
+ private init()
+ {
+ }
+}
diff --git a/Cores/DeSmuMEDeltaCore/Sources/DeSmuMEDeltaCore.h b/Cores/DeSmuMEDeltaCore/Sources/DeSmuMEDeltaCore.h
new file mode 100644
index 000000000..830d0498b
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/Sources/DeSmuMEDeltaCore.h
@@ -0,0 +1,19 @@
+//
+// DeSmuMEDeltaCore.h
+// DeSmuMEDeltaCore
+//
+// Created by Riley Testut on 8/2/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import
+
+//! Project version number for DeSmuMEDeltaCore.
+FOUNDATION_EXPORT double DeSmuMEDeltaCoreVersionNumber;
+
+//! Project version string for DeSmuMEDeltaCore.
+FOUNDATION_EXPORT const unsigned char DeSmuMEDeltaCoreVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+#import
+
diff --git a/Cores/DeSmuMEDeltaCore/Sources/Types/DeSmuMETypes.h b/Cores/DeSmuMEDeltaCore/Sources/Types/DeSmuMETypes.h
new file mode 100644
index 000000000..f2204dff2
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/Sources/Types/DeSmuMETypes.h
@@ -0,0 +1,12 @@
+//
+// DeSmuMETypes.h
+// DeSmuMEDeltaCore
+//
+// Created by Riley Testut on 2/1/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+#import
+
+// Extensible Enums
+FOUNDATION_EXPORT GameType const GameTypeDS NS_SWIFT_NAME(ds);
diff --git a/Cores/DeSmuMEDeltaCore/desmume b/Cores/DeSmuMEDeltaCore/desmume
new file mode 160000
index 000000000..aeaf40417
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/desmume
@@ -0,0 +1 @@
+Subproject commit aeaf404177e3d5dc8f4f19a0eaa44096dff3df69
diff --git a/Cores/DeSmuMEDeltaCore/libDeSmuME/pcap.h b/Cores/DeSmuMEDeltaCore/libDeSmuME/pcap.h
new file mode 100644
index 000000000..87647bd87
--- /dev/null
+++ b/Cores/DeSmuMEDeltaCore/libDeSmuME/pcap.h
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 1993, 1994, 1995, 1996, 1997
+ * The Regents of the University of California. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ * must display the following acknowledgement:
+ * This product includes software developed by the Computer Systems
+ * Engineering Group at Lawrence Berkeley Laboratory.
+ * 4. Neither the name of the University nor of the Laboratory may be used
+ * to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ */
+
+#ifndef lib_pcap_h
+#define lib_pcap_h
+
+#include
+#include
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HOST_WINDOWS // Allow us to compile wifi.cpp without modifications.
+
+#define PCAP_VERSION_MAJOR 2
+#define PCAP_VERSION_MINOR 4
+
+#define PCAP_ERRBUF_SIZE 256
+
+ /*
+ * Compatibility for systems that have a bpf.h that
+ * predates the bpf typedefs for 64-bit support.
+ */
+#if BPF_RELEASE - 0 < 199406
+ typedef int bpf_int32;
+ typedef u_int bpf_u_int32;
+#endif
+
+ typedef struct pcap pcap_t;
+ typedef struct pcap_dumper pcap_dumper_t;
+ typedef struct pcap_if pcap_if_t;
+ typedef struct pcap_addr pcap_addr_t;
+
+ /*
+ * The first record in the file contains saved values for some
+ * of the flags used in the printout phases of tcpdump.
+ * Many fields here are 32 bit ints so compilers won't insert unwanted
+ * padding; these files need to be interchangeable across architectures.
+ *
+ * Do not change the layout of this structure, in any way (this includes
+ * changes that only affect the length of fields in this structure).
+ *
+ * Also, do not change the interpretation of any of the members of this
+ * structure, in any way (this includes using values other than
+ * LINKTYPE_ values, as defined in "savefile.c", in the "linktype"
+ * field).
+ *
+ * Instead:
+ *
+ * introduce a new structure for the new format, if the layout
+ * of the structure changed;
+ *
+ * send mail to "tcpdump-workers@tcpdump.org", requesting a new
+ * magic number for your new capture file format, and, when
+ * you get the new magic number, put it in "savefile.c";
+ *
+ * use that magic number for save files with the changed file
+ * header;
+ *
+ * make the code in "savefile.c" capable of reading files with
+ * the old file header as well as files with the new file header
+ * (using the magic number to determine the header format).
+ *
+ * Then supply the changes to "patches@tcpdump.org", so that future
+ * versions of libpcap and programs that use it (such as tcpdump) will
+ * be able to read your new capture file format.
+ */
+ struct pcap_file_header {
+ bpf_u_int32 magic;
+ u_short version_major;
+ u_short version_minor;
+ bpf_int32 thiszone; /* gmt to local correction */
+ bpf_u_int32 sigfigs; /* accuracy of timestamps */
+ bpf_u_int32 snaplen; /* max length saved portion of each pkt */
+ bpf_u_int32 linktype; /* data link type (LINKTYPE_*) */
+ };
+
+ /*
+ * Each packet in the dump file is prepended with this generic header.
+ * This gets around the problem of different headers for different
+ * packet interfaces.
+ */
+ struct pcap_pkthdr {
+ struct timeval ts; /* time stamp */
+ bpf_u_int32 caplen; /* length of portion present */
+ bpf_u_int32 len; /* length this packet (off wire) */
+ };
+
+ /*
+ * As returned by the pcap_stats()
+ */
+ struct pcap_stat {
+ u_int ps_recv; /* number of packets received */
+ u_int ps_drop; /* number of packets dropped */
+ u_int ps_ifdrop; /* drops by interface XXX not yet supported */
+ };
+
+ /*
+ * Item in a list of interfaces.
+ */
+ struct pcap_if {
+ struct pcap_if *next;
+ char *name; /* name to hand to "pcap_open_live()" */
+ char *description; /* textual description of interface, or NULL */
+ struct pcap_addr *addresses;
+ u_int flags; /* PCAP_IF_ interface flags */
+ };
+
+#define PCAP_IF_LOOPBACK 0x00000001 /* interface is loopback */
+
+ /*
+ * Representation of an interface address.
+ */
+ struct pcap_addr {
+ struct pcap_addr *next;
+ struct sockaddr *addr; /* address */
+ struct sockaddr *netmask; /* netmask for that address */
+ struct sockaddr *broadaddr; /* broadcast address for that address */
+ struct sockaddr *dstaddr; /* P2P destination address for that address */
+ };
+
+ typedef void (*pcap_handler)(u_char *, const struct pcap_pkthdr *,
+ const u_char *);
+
+ char *pcap_lookupdev(char *);
+ int pcap_lookupnet(char *, bpf_u_int32 *, bpf_u_int32 *, char *);
+ pcap_t *pcap_open_live(char *, int, int, int, char *);
+ pcap_t *pcap_open_dead(int, int);
+ pcap_t *pcap_open_offline(const char *, char *);
+ void pcap_close(pcap_t *);
+ int pcap_loop(pcap_t *, int, pcap_handler, u_char *);
+ int pcap_dispatch(pcap_t *, int, pcap_handler, u_char *);
+ const u_char*
+ pcap_next(pcap_t *, struct pcap_pkthdr *);
+ int pcap_stats(pcap_t *, struct pcap_stat *);
+ int pcap_setfilter(pcap_t *, struct bpf_program *);
+ int pcap_getnonblock(pcap_t *, char *);
+ int pcap_setnonblock(pcap_t *, int, char *);
+ void pcap_perror(pcap_t *, char *);
+ char *pcap_strerror(int);
+ char *pcap_geterr(pcap_t *);
+ int pcap_compile(pcap_t *, struct bpf_program *, char *, int,
+ bpf_u_int32);
+ int pcap_compile_nopcap(int, int, struct bpf_program *,
+ char *, int, bpf_u_int32);
+ void pcap_freecode(struct bpf_program *);
+ int pcap_datalink(pcap_t *);
+ int pcap_snapshot(pcap_t *);
+ int pcap_is_swapped(pcap_t *);
+ int pcap_major_version(pcap_t *);
+ int pcap_minor_version(pcap_t *);
+
+ /* XXX */
+ FILE *pcap_file(pcap_t *);
+ int pcap_fileno(pcap_t *);
+
+ pcap_dumper_t *pcap_dump_open(pcap_t *, const char *);
+ void pcap_dump_close(pcap_dumper_t *);
+ void pcap_dump(u_char *, const struct pcap_pkthdr *, const u_char *);
+
+ int pcap_findalldevs(pcap_if_t **, char *);
+ void pcap_freealldevs(pcap_if_t *);
+
+ /* XXX this guy lives in the bpf tree */
+ u_int bpf_filter(struct bpf_insn *, u_char *, u_int, u_int);
+ int bpf_validate(struct bpf_insn *f, int len);
+ char *bpf_image(struct bpf_insn *, int);
+ void bpf_dump(struct bpf_program *, int);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/Cores/DeltaCore b/Cores/DeltaCore
deleted file mode 160000
index a07abad32..000000000
--- a/Cores/DeltaCore
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit a07abad3214a310785055eddf86bed36ae7612ca
diff --git a/Cores/DeltaCore/Derived/InfoPlists/DeltaCore.plist b/Cores/DeltaCore/Derived/InfoPlists/DeltaCore.plist
new file mode 100644
index 000000000..323e5ecfc
--- /dev/null
+++ b/Cores/DeltaCore/Derived/InfoPlists/DeltaCore.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+
+
diff --git a/Cores/DeltaCore/Derived/Sources/Bundle+DeltaCore.swift b/Cores/DeltaCore/Derived/Sources/Bundle+DeltaCore.swift
new file mode 100644
index 000000000..65c8442a1
--- /dev/null
+++ b/Cores/DeltaCore/Derived/Sources/Bundle+DeltaCore.swift
@@ -0,0 +1,23 @@
+// swiftlint:disable all
+import Foundation
+
+// MARK: - Swift Bundle Accessor
+
+private class BundleFinder {}
+
+extension Foundation.Bundle {
+ /// Since DeltaCore is a framework, the bundle containing the resources is copied into the final product.
+ static var module: Bundle = {
+ return Bundle(for: BundleFinder.self)
+ }()
+}
+
+// MARK: - Objective-C Bundle Accessor
+
+@objc
+public class DeltaCoreResources: NSObject {
+ @objc public class var bundle: Bundle {
+ return .module
+ }
+}
+// swiftlint:enable all
\ No newline at end of file
diff --git a/Cores/DeltaCore/Project.swift b/Cores/DeltaCore/Project.swift
new file mode 100644
index 000000000..b7cd83dd2
--- /dev/null
+++ b/Cores/DeltaCore/Project.swift
@@ -0,0 +1,20 @@
+import ProjectDescription
+
+let project = Project(name: "DeltaCore",
+ packages: [
+ .remote(url: "https://github.com/rileytestut/ZIPFoundation", requirement: .branch("development")),
+ ],
+ targets: [
+ Target(name: "DeltaCore",
+ platform: .iOS,
+ product: .framework,
+ bundleId: "com.rileytestut.DeltaCore",
+ deploymentTarget: .iOS(targetVersion: "12.0", devices: [.iphone, .ipad]),
+ infoPlist: .extendingDefault(with: [:]),
+ sources: ["Sources/**"],
+ resources: ["Resources/**"],
+ headers: Headers(public: ["Sources/DeltaCore.h", "Sources/DeltaTypes.h", "Sources/Emulator Core/Audio/DLTAMuteSwitchMonitor.h"]),
+ dependencies: [
+ .package(product: "ZIPFoundation"),
+ ]),
+ ])
diff --git a/Cores/DeltaCore/Resources/KeyboardGameController.deltamapping b/Cores/DeltaCore/Resources/KeyboardGameController.deltamapping
new file mode 100644
index 000000000..ced86f15b
Binary files /dev/null and b/Cores/DeltaCore/Resources/KeyboardGameController.deltamapping differ
diff --git a/Cores/DeltaCore/Resources/MFiGameController.deltamapping b/Cores/DeltaCore/Resources/MFiGameController.deltamapping
new file mode 100644
index 000000000..3983eb8d4
Binary files /dev/null and b/Cores/DeltaCore/Resources/MFiGameController.deltamapping differ
diff --git a/Cores/DeltaCore/Sources/Cores/DeltaCoreProtocol.swift b/Cores/DeltaCore/Sources/Cores/DeltaCoreProtocol.swift
new file mode 100644
index 000000000..8a441f7cb
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Cores/DeltaCoreProtocol.swift
@@ -0,0 +1,97 @@
+//
+// DeltaCoreProtocol.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/29/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import AVFoundation
+
+public protocol DeltaCoreProtocol: CustomStringConvertible
+{
+ /* General */
+ var name: String { get }
+ var identifier: String { get }
+
+ var gameType: GameType { get }
+ var gameSaveFileExtension: String { get }
+
+ // Should be associated type, but Swift type system makes this difficult, so ¯\_(ツ)_/¯
+ var gameInputType: Input.Type { get }
+
+ /* Rendering */
+ var audioFormat: AVAudioFormat { get }
+ var videoFormat: VideoFormat { get }
+
+ /* Cheats */
+ var supportedCheatFormats: Set { get }
+
+ /* Emulation */
+ var emulatorBridge: EmulatorBridging { get }
+
+ var resourceBundle: Bundle { get }
+}
+
+public extension DeltaCoreProtocol
+{
+ var bundle: Bundle {
+ #if FRAMEWORK
+ let bundle = Bundle(for: type(of: self.emulatorBridge))
+ #else
+ let bundle = Bundle.main
+ #endif
+ return bundle
+ }
+
+ var resourceBundle: Bundle {
+ #if FRAMEWORK
+ let bundle = Bundle(for: type(of: self.emulatorBridge))
+ #elseif STATIC_LIBRARY
+ let bundle: Bundle
+ if let bundleURL = Bundle.main.url(forResource: self.name, withExtension: "bundle")
+ {
+ bundle = Bundle(url: bundleURL)!
+ }
+ else
+ {
+ bundle = .main
+ }
+ #else
+ let bundle = Bundle.main
+ #endif
+
+ return bundle
+ }
+
+ var directoryURL: URL {
+ let directoryURL = Delta.coresDirectoryURL.appendingPathComponent(self.name, isDirectory: true)
+
+ try? FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
+
+ return directoryURL
+ }
+}
+
+public extension DeltaCoreProtocol
+{
+ var description: String {
+ let description = "\(self.name) (\(self.identifier))"
+ return description
+ }
+}
+
+public func ==(lhs: DeltaCoreProtocol?, rhs: DeltaCoreProtocol?) -> Bool
+{
+ return lhs?.identifier == rhs?.identifier
+}
+
+public func !=(lhs: DeltaCoreProtocol?, rhs: DeltaCoreProtocol?) -> Bool
+{
+ return !(lhs == rhs)
+}
+
+public func ~=(lhs: DeltaCoreProtocol?, rhs: DeltaCoreProtocol?) -> Bool
+{
+ return lhs == rhs
+}
diff --git a/Cores/DeltaCore/Sources/Cores/EmulatorBridging.swift b/Cores/DeltaCore/Sources/Cores/EmulatorBridging.swift
new file mode 100644
index 000000000..590b5d17f
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Cores/EmulatorBridging.swift
@@ -0,0 +1,56 @@
+//
+// EmulatorBridging.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/29/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+@objc(DLTAEmulatorBridging)
+public protocol EmulatorBridging: NSObjectProtocol
+{
+ /// State
+ var gameURL: URL? { get }
+
+ /// System
+ var frameDuration: TimeInterval { get }
+
+ /// Audio
+ var audioRenderer: AudioRendering? { get set }
+
+ /// Video
+ var videoRenderer: VideoRendering? { get set }
+
+ /// Saves
+ var saveUpdateHandler: (() -> Void)? { get set }
+
+
+ /// Emulation State
+ func start(withGameURL gameURL: URL)
+ func stop()
+ func pause()
+ func resume()
+
+ /// Game Loop
+ @objc(runFrameAndProcessVideo:) func runFrame(processVideo: Bool)
+
+ /// Inputs
+ func activateInput(_ input: Int, value: Double)
+ func deactivateInput(_ input: Int)
+ func resetInputs()
+
+ /// Save States
+ @objc(saveSaveStateToURL:) func saveSaveState(to url: URL)
+ @objc(loadSaveStateFromURL:) func loadSaveState(from url: URL)
+
+ /// Game Games
+ @objc(saveGameSaveToURL:) func saveGameSave(to url: URL)
+ @objc(loadGameSaveFromURL:) func loadGameSave(from url: URL)
+
+ /// Cheats
+ @discardableResult func addCheatCode(_ cheatCode: String, type: String) -> Bool
+ func resetCheats()
+ func updateCheats()
+}
diff --git a/Cores/DeltaCore/Sources/Delta.swift b/Cores/DeltaCore/Sources/Delta.swift
new file mode 100644
index 000000000..2b2c165fc
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Delta.swift
@@ -0,0 +1,56 @@
+//
+// Delta.swift
+// DeltaCore
+//
+// Created by Riley Testut on 7/22/15.
+// Copyright © 2015 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+extension GameType: CustomStringConvertible
+{
+ public var description: String {
+ return self.rawValue
+ }
+}
+
+public extension GameType
+{
+ static let unknown = GameType("com.rileytestut.delta.game.unknown")
+}
+
+public struct Delta
+{
+ public private(set) static var registeredCores = [GameType: DeltaCoreProtocol]()
+
+ private init()
+ {
+ }
+
+ public static func register(_ core: DeltaCoreProtocol)
+ {
+ self.registeredCores[core.gameType] = core
+ }
+
+ public static func unregister(_ core: DeltaCoreProtocol)
+ {
+ // Ensure another core has not been registered for core.gameType.
+ guard let registeredCore = self.registeredCores[core.gameType], registeredCore == core else { return }
+ self.registeredCores[core.gameType] = nil
+ }
+
+ public static func core(for gameType: GameType) -> DeltaCoreProtocol?
+ {
+ return self.registeredCores[gameType]
+ }
+
+ public static var coresDirectoryURL: URL = {
+ let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
+ let coresDirectoryURL = documentsDirectoryURL.appendingPathComponent("Cores", isDirectory: true)
+
+ try? FileManager.default.createDirectory(at: coresDirectoryURL, withIntermediateDirectories: true, attributes: nil)
+
+ return coresDirectoryURL
+ }()
+}
diff --git a/Cores/DeltaCore/Sources/DeltaCore.h b/Cores/DeltaCore/Sources/DeltaCore.h
new file mode 100644
index 000000000..7c7a55764
--- /dev/null
+++ b/Cores/DeltaCore/Sources/DeltaCore.h
@@ -0,0 +1,23 @@
+//
+// DeltaCore.h
+// DeltaCore
+//
+// Created by Riley Testut on 3/8/15.
+// Copyright (c) 2015 Riley Testut. All rights reserved.
+//
+
+#import
+
+//! Project version number for DeltaCore.
+FOUNDATION_EXPORT double DeltaCoreVersionNumber;
+
+//! Project version string for DeltaCore.
+FOUNDATION_EXPORT const unsigned char DeltaCoreVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+#import
+#import
+
+// HACK: Needed because the generated DeltaCore-Swift header file uses @import syntax, which isn't supported in Objective-C++ code.
+#import
+#import
diff --git a/Cores/DeltaCore/Sources/DeltaTypes.h b/Cores/DeltaCore/Sources/DeltaTypes.h
new file mode 100644
index 000000000..8f7869062
--- /dev/null
+++ b/Cores/DeltaCore/Sources/DeltaTypes.h
@@ -0,0 +1,16 @@
+//
+// DeltaTypes.h
+// DeltaCore
+//
+// Created by Riley Testut on 1/30/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+#import
+
+// Extensible Enums
+typedef NSString *GameType NS_TYPED_EXTENSIBLE_ENUM;
+typedef NSString *CheatType NS_TYPED_EXTENSIBLE_ENUM;
+typedef NSString *GameControllerInputType NS_TYPED_EXTENSIBLE_ENUM;
+
+extern NSNotificationName const DeltaRegistrationRequestNotification;
diff --git a/Cores/DeltaCore/Sources/DeltaTypes.m b/Cores/DeltaCore/Sources/DeltaTypes.m
new file mode 100644
index 000000000..eb7d87df0
--- /dev/null
+++ b/Cores/DeltaCore/Sources/DeltaTypes.m
@@ -0,0 +1,11 @@
+//
+// DeltaTypes.m
+// DeltaCore
+//
+// Created by Riley Testut on 6/30/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+@import Foundation;
+
+NSNotificationName const DeltaRegistrationRequestNotification = @"DeltaRegistrationRequestNotification";
diff --git a/Cores/DeltaCore/Sources/Emulator Core/Audio/AudioManager.swift b/Cores/DeltaCore/Sources/Emulator Core/Audio/AudioManager.swift
new file mode 100644
index 000000000..7e754b813
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Emulator Core/Audio/AudioManager.swift
@@ -0,0 +1,397 @@
+//
+// AudioManager.swift
+// DeltaCore
+//
+// Created by Riley Testut on 1/12/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import AVFoundation
+
+internal extension AVAudioFormat
+{
+ var frameSize: Int {
+ return Int(self.streamDescription.pointee.mBytesPerFrame)
+ }
+}
+
+private extension AVAudioSession
+{
+ func setDeltaCategory() throws
+ {
+ try AVAudioSession.sharedInstance().setCategory(.playAndRecord,
+ options: [.mixWithOthers, .allowBluetoothA2DP, .allowAirPlay])
+ }
+}
+
+private extension AVAudioSessionRouteDescription
+{
+ var isHeadsetPluggedIn: Bool
+ {
+ let isHeadsetPluggedIn = self.outputs.contains { $0.portType == .headphones || $0.portType == .bluetoothA2DP }
+ return isHeadsetPluggedIn
+ }
+
+ var isOutputtingToReceiver: Bool
+ {
+ let isOutputtingToReceiver = self.outputs.contains { $0.portType == .builtInReceiver }
+ return isOutputtingToReceiver
+ }
+
+ var isOutputtingToExternalDevice: Bool
+ {
+ let isOutputtingToExternalDevice = self.outputs.contains { $0.portType != .builtInSpeaker && $0.portType != .builtInReceiver }
+ return isOutputtingToExternalDevice
+ }
+}
+
+public class AudioManager: NSObject, AudioRendering
+{
+ /// Currently only supports 16-bit interleaved Linear PCM.
+ public internal(set) var audioFormat: AVAudioFormat {
+ didSet {
+ self.resetAudioEngine()
+ }
+ }
+
+ public var isEnabled = true {
+ didSet
+ {
+ self.audioBuffer.isEnabled = self.isEnabled
+
+ self.updateOutputVolume()
+
+ do
+ {
+ if self.isEnabled
+ {
+ try self.audioEngine.start()
+ }
+ else
+ {
+ self.audioEngine.pause()
+ }
+ }
+ catch
+ {
+ print(error)
+ }
+
+ self.audioBuffer.reset()
+ }
+ }
+
+ public private(set) var audioBuffer: RingBuffer
+
+ public internal(set) var rate = 1.0 {
+ didSet {
+ self.timePitchEffect.rate = Float(self.rate)
+ }
+ }
+
+ var frameDuration: Double = (1.0 / 60.0) {
+ didSet {
+ guard self.audioEngine.isRunning else { return }
+ self.resetAudioEngine()
+ }
+ }
+
+ private let audioEngine: AVAudioEngine
+ private let audioPlayerNode: AVAudioPlayerNode
+ private let timePitchEffect: AVAudioUnitTimePitch
+
+ @available(iOS 13.0, *)
+ private lazy var sourceNode = self.makeSourceNode()
+
+ private var audioConverter: AVAudioConverter?
+ private var audioConverterRequiredFrameCount: AVAudioFrameCount?
+
+ private let audioBufferCount = 3
+
+ // Used to synchronize access to self.audioPlayerNode without causing deadlocks.
+ private let renderingQueue = DispatchQueue(label: "com.rileytestut.Delta.AudioManager.renderingQueue")
+
+ private var isMuted: Bool = false {
+ didSet {
+ self.updateOutputVolume()
+ }
+ }
+
+ private let muteSwitchMonitor = DLTAMuteSwitchMonitor()
+
+ public init(audioFormat: AVAudioFormat)
+ {
+ self.audioFormat = audioFormat
+
+ // Temporary. Will be replaced with more accurate RingBuffer in resetAudioEngine().
+ self.audioBuffer = RingBuffer(preferredBufferSize: 4096)!
+
+ do
+ {
+ // Set category before configuring AVAudioEngine to prevent pausing any currently playing audio from another app.
+ try AVAudioSession.sharedInstance().setDeltaCategory()
+ }
+ catch
+ {
+ print(error)
+ }
+
+ self.audioEngine = AVAudioEngine()
+
+ self.audioPlayerNode = AVAudioPlayerNode()
+ self.audioEngine.attach(self.audioPlayerNode)
+
+ self.timePitchEffect = AVAudioUnitTimePitch()
+ self.audioEngine.attach(self.timePitchEffect)
+
+ super.init()
+
+ if #available(iOS 13.0, *)
+ {
+ self.audioEngine.attach(self.sourceNode)
+ }
+
+ self.updateOutputVolume()
+
+ NotificationCenter.default.addObserver(self, selector: #selector(AudioManager.resetAudioEngine), name: .AVAudioEngineConfigurationChange, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(AudioManager.resetAudioEngine), name: AVAudioSession.routeChangeNotification, object: nil)
+ }
+}
+
+public extension AudioManager
+{
+ func start()
+ {
+ self.muteSwitchMonitor.startMonitoring { [weak self] (isMuted) in
+ self?.isMuted = isMuted
+ }
+
+ do
+ {
+ try AVAudioSession.sharedInstance().setDeltaCategory()
+ try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005)
+
+ if #available(iOS 13.0, *)
+ {
+ try AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true)
+ }
+
+ try AVAudioSession.sharedInstance().setActive(true)
+ }
+ catch
+ {
+ print(error)
+ }
+
+ self.resetAudioEngine()
+ }
+
+ func stop()
+ {
+ self.muteSwitchMonitor.stopMonitoring()
+
+ self.renderingQueue.sync {
+ self.audioPlayerNode.stop()
+ self.audioEngine.stop()
+ }
+
+ self.audioBuffer.isEnabled = false
+ }
+}
+
+private extension AudioManager
+{
+ func render(_ inputBuffer: AVAudioPCMBuffer, into outputBuffer: AVAudioPCMBuffer)
+ {
+ guard let buffer = inputBuffer.int16ChannelData, let audioConverter = self.audioConverter else { return }
+
+ // Ensure any buffers from previous audio route configurations are no longer processed.
+ guard inputBuffer.format == audioConverter.inputFormat && outputBuffer.format == audioConverter.outputFormat else { return }
+
+ if self.audioConverterRequiredFrameCount == nil
+ {
+ // Determine the minimum number of input frames needed to perform a conversion.
+ audioConverter.convert(to: outputBuffer, error: nil) { (requiredPacketCount, outStatus) -> AVAudioBuffer? in
+ // In Linear PCM, one packet = one frame.
+ self.audioConverterRequiredFrameCount = requiredPacketCount
+
+ // Setting to ".noDataNow" sometimes results in crash, so we set to ".endOfStream" and reset audioConverter afterwards.
+ outStatus.pointee = .endOfStream
+ return nil
+ }
+
+ audioConverter.reset()
+ }
+
+ guard let audioConverterRequiredFrameCount = self.audioConverterRequiredFrameCount else { return }
+
+ let availableFrameCount = AVAudioFrameCount(self.audioBuffer.availableBytesForReading / self.audioFormat.frameSize)
+ if self.audioEngine.isRunning && availableFrameCount >= audioConverterRequiredFrameCount
+ {
+ var conversionError: NSError?
+ let status = audioConverter.convert(to: outputBuffer, error: &conversionError) { (requiredPacketCount, outStatus) -> AVAudioBuffer? in
+
+ // Copy requiredPacketCount frames into inputBuffer's first channel (since audio is interleaved, no need to modify other channels).
+ let preferredSize = min(Int(requiredPacketCount) * self.audioFormat.frameSize, Int(inputBuffer.frameCapacity) * self.audioFormat.frameSize)
+ buffer[0].withMemoryRebound(to: UInt8.self, capacity: preferredSize) { (uint8Buffer) in
+ let readBytes = self.audioBuffer.read(into: uint8Buffer, preferredSize: preferredSize)
+
+ let frameLength = AVAudioFrameCount(readBytes / self.audioFormat.frameSize)
+ inputBuffer.frameLength = frameLength
+ }
+
+ if inputBuffer.frameLength == 0
+ {
+ outStatus.pointee = .noDataNow
+ return nil
+ }
+ else
+ {
+ outStatus.pointee = .haveData
+ return inputBuffer
+ }
+ }
+
+ if status == .error
+ {
+ if let error = conversionError
+ {
+ print(error, error.userInfo)
+ }
+ }
+ }
+ else
+ {
+ // If not running or not enough input frames, set frameLength to 0 to minimize time until we check again.
+ inputBuffer.frameLength = 0
+ }
+
+ self.audioPlayerNode.scheduleBuffer(outputBuffer) { [weak self, weak node = audioPlayerNode] in
+ guard let self = self else { return }
+
+ self.renderingQueue.async {
+ if node?.isPlaying == true
+ {
+ self.render(inputBuffer, into: outputBuffer)
+ }
+ }
+ }
+ }
+
+ @objc func resetAudioEngine()
+ {
+ self.renderingQueue.sync {
+ self.audioPlayerNode.reset()
+
+ guard let outputAudioFormat = AVAudioFormat(standardFormatWithSampleRate: AVAudioSession.sharedInstance().sampleRate, channels: self.audioFormat.channelCount) else { return }
+
+ let inputAudioBufferFrameCount = Int(self.audioFormat.sampleRate * self.frameDuration)
+ let outputAudioBufferFrameCount = Int(outputAudioFormat.sampleRate * self.frameDuration)
+
+ // Allocate enough space to prevent us from overwriting data before we've used it.
+ let ringBufferAudioBufferCount = Int((self.audioFormat.sampleRate / outputAudioFormat.sampleRate).rounded(.up) + 10.0)
+
+ let preferredBufferSize = inputAudioBufferFrameCount * self.audioFormat.frameSize * ringBufferAudioBufferCount
+ guard let ringBuffer = RingBuffer(preferredBufferSize: preferredBufferSize) else {
+ fatalError("Cannot initialize RingBuffer with preferredBufferSize of \(preferredBufferSize)")
+ }
+ self.audioBuffer = ringBuffer
+
+ let audioConverter = AVAudioConverter(from: self.audioFormat, to: outputAudioFormat)
+ self.audioConverter = audioConverter
+
+ self.audioConverterRequiredFrameCount = nil
+
+ self.audioEngine.disconnectNodeOutput(self.timePitchEffect)
+ self.audioEngine.connect(self.timePitchEffect, to: self.audioEngine.mainMixerNode, format: outputAudioFormat)
+
+ if #available(iOS 13.0, *)
+ {
+ self.audioEngine.detach(self.sourceNode)
+
+ self.sourceNode = self.makeSourceNode()
+ self.audioEngine.attach(self.sourceNode)
+
+ self.audioEngine.connect(self.sourceNode, to: self.timePitchEffect, format: outputAudioFormat)
+ }
+ else
+ {
+ self.audioEngine.disconnectNodeOutput(self.audioPlayerNode)
+ self.audioEngine.connect(self.audioPlayerNode, to: self.timePitchEffect, format: outputAudioFormat)
+
+ for _ in 0 ..< self.audioBufferCount
+ {
+ let inputAudioBufferFrameCapacity = max(inputAudioBufferFrameCount, outputAudioBufferFrameCount)
+
+ if let inputBuffer = AVAudioPCMBuffer(pcmFormat: self.audioFormat, frameCapacity: AVAudioFrameCount(inputAudioBufferFrameCapacity)),
+ let outputBuffer = AVAudioPCMBuffer(pcmFormat: outputAudioFormat, frameCapacity: AVAudioFrameCount(outputAudioBufferFrameCount))
+ {
+ self.render(inputBuffer, into: outputBuffer)
+ }
+ }
+ }
+
+ do
+ {
+ // Explicitly set output port since .defaultToSpeaker option pauses external audio.
+ if AVAudioSession.sharedInstance().currentRoute.isOutputtingToReceiver
+ {
+ try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
+ }
+
+ try self.audioEngine.start()
+
+ if #available(iOS 13.0, *) {}
+ else
+ {
+ self.audioPlayerNode.play()
+ }
+ }
+ catch
+ {
+ print(error)
+ }
+ }
+ }
+
+ @objc func updateOutputVolume()
+ {
+ if !self.isEnabled
+ {
+ self.audioEngine.mainMixerNode.outputVolume = 0.0
+ }
+ else
+ {
+ let route = AVAudioSession.sharedInstance().currentRoute
+ if self.isMuted && (route.isHeadsetPluggedIn || !route.isOutputtingToExternalDevice)
+ {
+ // Mute if playing through speaker or headphones.
+ self.audioEngine.mainMixerNode.outputVolume = 0.0
+ }
+ else
+ {
+ // Ignore mute switch for other audio routes (e.g. AirPlay).
+ self.audioEngine.mainMixerNode.outputVolume = 1.0
+ }
+ }
+ }
+
+ @available(iOS 13.0, *)
+ func makeSourceNode() -> AVAudioSourceNode
+ {
+ let sourceNode = AVAudioSourceNode(format: self.audioFormat) { [frameSize = audioFormat.frameSize, audioBuffer] (_, _, frameCount, audioBufferList) -> OSStatus in
+ let unsafeAudioBufferList = UnsafeMutableAudioBufferListPointer(audioBufferList)
+ guard let buffer = unsafeAudioBufferList[0].mData else { return kAudioFileStreamError_UnspecifiedError }
+
+ let requestedBytes = Int(frameCount) * frameSize
+ guard audioBuffer.availableBytesForReading >= requestedBytes else { return kAudioFileStreamError_DataUnavailable }
+
+ let readBytes = audioBuffer.read(into: buffer, preferredSize: requestedBytes)
+ unsafeAudioBufferList[0].mDataByteSize = UInt32(readBytes)
+
+ return noErr
+ }
+
+ return sourceNode
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Emulator Core/Audio/DLTAMuteSwitchMonitor.h b/Cores/DeltaCore/Sources/Emulator Core/Audio/DLTAMuteSwitchMonitor.h
new file mode 100644
index 000000000..372fe9874
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Emulator Core/Audio/DLTAMuteSwitchMonitor.h
@@ -0,0 +1,23 @@
+//
+// DLTAMuteSwitchMonitor.h
+// DeltaCore
+//
+// Created by Riley Testut on 11/19/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface DLTAMuteSwitchMonitor : NSObject
+
+@property (nonatomic, readonly) BOOL isMonitoring;
+@property (nonatomic, readonly) BOOL isMuted;
+
+- (void)startMonitoring:(void (^)(BOOL isMuted))muteHandler;
+- (void)stopMonitoring;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Cores/DeltaCore/Sources/Emulator Core/Audio/DLTAMuteSwitchMonitor.m b/Cores/DeltaCore/Sources/Emulator Core/Audio/DLTAMuteSwitchMonitor.m
new file mode 100644
index 000000000..cce55d2e4
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Emulator Core/Audio/DLTAMuteSwitchMonitor.m
@@ -0,0 +1,85 @@
+//
+// DLTAMuteSwitchMonitor.m
+// DeltaCore
+//
+// Created by Riley Testut on 11/19/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+#import "DLTAMuteSwitchMonitor.h"
+
+#import
+
+#if STATIC_LIBRARY
+#import "DeltaCore-Swift.h"
+#else
+#import
+#endif
+
+@import AudioToolbox;
+
+@interface DLTAMuteSwitchMonitor ()
+
+@property (nonatomic, readwrite) BOOL isMonitoring;
+@property (nonatomic, readwrite) BOOL isMuted;
+
+@property (nonatomic) int notifyToken;
+
+@end
+
+@implementation DLTAMuteSwitchMonitor
+
+- (instancetype)init
+{
+ self = [super init];
+ if (self)
+ {
+ _isMuted = YES;
+ }
+
+ return self;
+}
+
+- (void)startMonitoring:(void (^)(BOOL isMuted))muteHandler
+{
+ if ([self isMonitoring])
+ {
+ return;
+ }
+
+ self.isMonitoring = YES;
+
+ void (^updateMutedState)(void) = ^{
+ uint64_t state;
+ uint32_t result = notify_get_state(_notifyToken, &state);
+ if (result == NOTIFY_STATUS_OK)
+ {
+ self.isMuted = (state == 0);
+ muteHandler(self.isMuted);
+ }
+ else
+ {
+ NSLog(@"Failed to get mute state. Error: %@", @(result));
+ }
+ };
+
+ notify_register_dispatch("com.apple.springboard.ringerstate", &_notifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(int token) {
+ updateMutedState();
+ });
+
+ updateMutedState();
+}
+
+- (void)stopMonitoring
+{
+ if (![self isMonitoring])
+ {
+ return;
+ }
+
+ self.isMonitoring = NO;
+
+ notify_cancel(self.notifyToken);
+}
+
+@end
diff --git a/Cores/DeltaCore/Sources/Emulator Core/Audio/RingBuffer.swift b/Cores/DeltaCore/Sources/Emulator Core/Audio/RingBuffer.swift
new file mode 100644
index 000000000..d0795e132
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Emulator Core/Audio/RingBuffer.swift
@@ -0,0 +1,168 @@
+//
+// RingBuffer.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/29/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+// Heavily based on Michael Tyson's TPCircularBuffer (https://github.com/michaeltyson/TPCircularBuffer)
+//
+
+import Foundation
+import Darwin.Mach.machine.vm_types
+
+private func trunc_page(_ x: vm_size_t) -> vm_size_t
+{
+ return x & ~(vm_page_size - 1)
+}
+
+private func round_page(_ x: vm_size_t) -> vm_size_t
+{
+ return trunc_page(x + (vm_size_t(vm_page_size) - 1))
+}
+
+@objc(DLTARingBuffer) @objcMembers
+public class RingBuffer: NSObject
+{
+ public var isEnabled: Bool = true
+
+ public var availableBytesForWriting: Int {
+ return Int(self.bufferLength - Int(self.usedBytesCount))
+ }
+
+ public var availableBytesForReading: Int {
+ return Int(self.usedBytesCount)
+ }
+
+ private var head: UnsafeMutableRawPointer {
+ let head = self.buffer.advanced(by: self.headOffset)
+ return head
+ }
+
+ private var tail: UnsafeMutableRawPointer {
+ let head = self.buffer.advanced(by: self.tailOffset)
+ return head
+ }
+
+ private let buffer: UnsafeMutableRawPointer
+ private var bufferLength = 0
+ private var tailOffset = 0
+ private var headOffset = 0
+ private var usedBytesCount: Int32 = 0
+
+ public init?(preferredBufferSize: Int)
+ {
+ assert(preferredBufferSize > 0)
+
+ // To handle race conditions, repeat initialization process up to 3 times before failing.
+ for _ in 1...3
+ {
+ let length = round_page(vm_size_t(preferredBufferSize))
+ self.bufferLength = Int(length)
+
+ var bufferAddress: vm_address_t = 0
+ guard vm_allocate(mach_task_self_, &bufferAddress, vm_size_t(length * 2), VM_FLAGS_ANYWHERE) == ERR_SUCCESS else { continue }
+
+ guard vm_deallocate(mach_task_self_, bufferAddress + length, length) == ERR_SUCCESS else {
+ vm_deallocate(mach_task_self_, bufferAddress, length)
+ continue
+ }
+
+ var virtualAddress: vm_address_t = bufferAddress + length
+ var current_protection: vm_prot_t = 0
+ var max_protection: vm_prot_t = 0
+
+ guard vm_remap(mach_task_self_, &virtualAddress, length, 0, 0, mach_task_self_, bufferAddress, 0, ¤t_protection, &max_protection, VM_INHERIT_DEFAULT) == ERR_SUCCESS else {
+ vm_deallocate(mach_task_self_, bufferAddress, length)
+ continue
+ }
+
+ guard virtualAddress == bufferAddress + length else {
+ vm_deallocate(mach_task_self_, virtualAddress, length)
+ vm_deallocate(mach_task_self_, bufferAddress, length)
+
+ continue
+ }
+
+ self.buffer = UnsafeMutableRawPointer(bitPattern: UInt(bufferAddress))!
+
+ return
+ }
+
+ return nil
+ }
+
+ deinit
+ {
+ let address = UInt(bitPattern: self.buffer)
+ vm_deallocate(mach_task_self_, vm_address_t(address), vm_size_t(self.bufferLength * 2))
+ }
+}
+
+public extension RingBuffer
+{
+ /// Writes `size` bytes from `buffer` to ring buffer if possible. Otherwise, writes as many as possible.
+ @objc(writeBuffer:size:)
+ @discardableResult func write(_ buffer: UnsafeRawPointer, size: Int) -> Int
+ {
+ guard self.isEnabled else { return 0 }
+ guard self.availableBytesForWriting > 0 else { return 0 }
+
+ if size > self.availableBytesForWriting
+ {
+ print("Ring Buffer Capacity reached. Available: \(self.availableBytesForWriting). Requested: \(size) Max: \(self.bufferLength). Filled: \(self.usedBytesCount).")
+
+ self.reset()
+ }
+
+ let size = min(size, self.availableBytesForWriting)
+ memcpy(self.head, buffer, size)
+
+ self.decrementAvailableBytes(by: size)
+
+ return size
+ }
+
+ /// Copies `size` bytes from ring buffer to `buffer` if possible. Otherwise, copies as many as possible.
+ @objc(readIntoBuffer:preferredSize:)
+ @discardableResult func read(into buffer: UnsafeMutableRawPointer, preferredSize: Int) -> Int
+ {
+ guard self.isEnabled else { return 0 }
+ guard self.availableBytesForReading > 0 else { return 0 }
+
+ if preferredSize > self.availableBytesForReading
+ {
+ print("Ring Buffer Empty. Available: \(self.availableBytesForReading). Requested: \(preferredSize) Max: \(self.bufferLength). Filled: \(self.usedBytesCount).")
+
+ self.reset()
+ }
+
+ let size = min(preferredSize, self.availableBytesForReading)
+ memcpy(buffer, self.tail, size)
+
+ self.incrementAvailableBytes(by: size)
+
+ return size
+ }
+
+ func reset()
+ {
+ let size = self.availableBytesForReading
+ self.incrementAvailableBytes(by: size)
+ }
+}
+
+private extension RingBuffer
+{
+ func incrementAvailableBytes(by size: Int)
+ {
+ self.tailOffset = (self.tailOffset + size) % self.bufferLength
+ OSAtomicAdd32(-Int32(size), &self.usedBytesCount)
+ }
+
+ func decrementAvailableBytes(by size: Int)
+ {
+ self.headOffset = (self.headOffset + size) % self.bufferLength
+ OSAtomicAdd32(Int32(size), &self.usedBytesCount)
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Emulator Core/EmulatorCore.swift b/Cores/DeltaCore/Sources/Emulator Core/EmulatorCore.swift
new file mode 100644
index 000000000..f854899bf
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Emulator Core/EmulatorCore.swift
@@ -0,0 +1,528 @@
+//
+// EmulatorCore.swift
+// DeltaCore
+//
+// Created by Riley Testut on 3/11/15.
+// Copyright (c) 2015 Riley Testut. All rights reserved.
+//
+
+import AVFoundation
+
+extension EmulatorCore
+{
+ @objc public static let emulationDidQuitNotification = Notification.Name("com.rileytestut.DeltaCore.emulationDidQuit")
+
+ private static let didUpdateFrameNotification = Notification.Name("com.rileytestut.DeltaCore.didUpdateFrame")
+}
+
+public extension EmulatorCore
+{
+ @objc enum State: Int
+ {
+ case stopped
+ case running
+ case paused
+ }
+
+ enum CheatError: Error
+ {
+ case invalid
+ }
+
+ enum SaveStateError: Error
+ {
+ case doesNotExist
+ }
+}
+
+@objc(DLTAEmulatorCore)
+public final class EmulatorCore: NSObject
+{
+ //MARK: - Properties -
+ /** Properties **/
+ public let game: GameProtocol
+ public private(set) var gameViews: [GameView] = []
+
+ public var updateHandler: ((EmulatorCore) -> Void)?
+ public var saveHandler: ((EmulatorCore) -> Void)?
+
+ public let audioManager: AudioManager
+ public let videoManager: VideoManager
+
+ // KVO-Compliant
+ @objc public private(set) dynamic var state = State.stopped
+ @objc public dynamic var rate = 1.0 {
+ didSet {
+ self.audioManager.rate = self.rate
+ }
+ }
+
+ public let deltaCore: DeltaCoreProtocol
+ public var preferredRenderingSize: CGSize { return self.deltaCore.videoFormat.dimensions }
+
+ //MARK: - Private Properties
+
+ // We privately set this first to clean up before setting self.state, which notifies KVO observers
+ private var _state = State.stopped
+
+ private let gameType: GameType
+ private let gameSaveURL: URL
+
+ private var cheatCodes = [String: CheatType]()
+
+ private var gameControllers = NSHashTable.weakObjects()
+
+ private var previousState = State.stopped
+ private var previousFrameDuration: TimeInterval? = nil
+
+ private var reactivateInputsDispatchGroup: DispatchGroup?
+ private let reactivateInputsQueue = DispatchQueue(label: "com.rileytestut.DeltaCore.EmulatorCore.reactivateInputsQueue", attributes: [.concurrent])
+
+ private let emulationLock = NSLock()
+
+ //MARK: - Initializers -
+ /** Initializers **/
+ public required init?(game: GameProtocol)
+ {
+ // These MUST be set in start(), because it's possible the same emulator core might be stopped, another one started, and then resumed back to this one
+ // AKA, these need to always be set at start to ensure it points to the correct managers
+ // self.configuration.bridge.audioRenderer = self.audioManager
+ // self.configuration.bridge.videoRenderer = self.videoManager
+
+ guard let deltaCore = Delta.core(for: game.type) else {
+ print(game.type.rawValue + " is not a supported game type.")
+ return nil
+ }
+
+ self.deltaCore = deltaCore
+
+ self.game = game
+
+ // Store separately in case self.game is an NSManagedObject subclass, and we need to access .type or .gameSaveURL on a different thread than its NSManagedObjectContext
+ self.gameType = self.game.type
+ self.gameSaveURL = self.game.gameSaveURL
+
+ // These were previously lazy variables, but turns out Swift lazy variables are not thread-safe.
+ // Since they don't actually need to be lazy, we now explicitly initialize them in the initializer.
+ self.audioManager = AudioManager(audioFormat: deltaCore.audioFormat)
+ self.videoManager = VideoManager(videoFormat: deltaCore.videoFormat)
+
+ super.init()
+
+ NotificationCenter.default.addObserver(self, selector: #selector(EmulatorCore.emulationDidQuit), name: EmulatorCore.emulationDidQuitNotification, object: nil)
+ }
+}
+
+//MARK: - Emulation -
+/// Emulation
+public extension EmulatorCore
+{
+ @discardableResult func start() -> Bool
+ {
+ guard self._state == .stopped else { return false }
+
+ self.emulationLock.lock()
+
+ self._state = .running
+ defer { self.state = self._state }
+
+ self.deltaCore.emulatorBridge.audioRenderer = self.audioManager
+ self.deltaCore.emulatorBridge.videoRenderer = self.videoManager
+ self.deltaCore.emulatorBridge.saveUpdateHandler = { [unowned self] in
+ self.save()
+ }
+
+ self.audioManager.start()
+ self.deltaCore.emulatorBridge.start(withGameURL: self.game.fileURL)
+
+ let videoFormat = self.deltaCore.videoFormat
+ if videoFormat != self.videoManager.videoFormat
+ {
+ self.videoManager.videoFormat = videoFormat
+ }
+
+ self.deltaCore.emulatorBridge.loadGameSave(from: self.gameSaveURL)
+
+ self.runGameLoop()
+ self.waitForFrameUpdate()
+
+ self.emulationLock.unlock()
+
+ return true
+ }
+
+ @discardableResult func stop() -> Bool
+ {
+ guard self._state != .stopped else { return false }
+
+ self.emulationLock.lock()
+
+ let isRunning = self.state == .running
+
+ self._state = .stopped
+ defer { self.state = self._state }
+
+ if isRunning
+ {
+ self.waitForFrameUpdate()
+ }
+
+ self.save()
+
+ self.audioManager.stop()
+ self.deltaCore.emulatorBridge.stop()
+
+ self.emulationLock.unlock()
+
+ return true
+ }
+
+ @discardableResult func pause() -> Bool
+ {
+ guard self._state == .running else { return false }
+
+ self.emulationLock.lock()
+
+ self._state = .paused
+ defer { self.state = self._state }
+
+ self.waitForFrameUpdate()
+
+ self.save()
+
+ self.audioManager.isEnabled = false
+ self.deltaCore.emulatorBridge.pause()
+
+ self.emulationLock.unlock()
+
+ return true
+ }
+
+ @discardableResult func resume() -> Bool
+ {
+ guard self._state == .paused else { return false }
+
+ self.emulationLock.lock()
+
+ self._state = .running
+ defer { self.state = self._state }
+
+ self.audioManager.isEnabled = true
+ self.deltaCore.emulatorBridge.resume()
+
+ self.runGameLoop()
+ self.waitForFrameUpdate()
+
+ self.emulationLock.unlock()
+
+ return true
+ }
+
+ private func waitForFrameUpdate()
+ {
+ let semaphore = DispatchSemaphore(value: 0)
+
+ let token = NotificationCenter.default.addObserver(forName: EmulatorCore.didUpdateFrameNotification, object: self, queue: nil) { (notification) in
+ semaphore.signal()
+ }
+
+ semaphore.wait()
+
+ NotificationCenter.default.removeObserver(token, name: EmulatorCore.didUpdateFrameNotification, object: self)
+ }
+}
+
+//MARK: - Game Views -
+/// Game Views
+public extension EmulatorCore
+{
+ func add(_ gameView: GameView)
+ {
+ self.gameViews.append(gameView)
+
+ self.videoManager.add(gameView)
+ }
+
+ func remove(_ gameView: GameView)
+ {
+ if let index = self.gameViews.firstIndex(of: gameView)
+ {
+ self.gameViews.remove(at: index)
+ }
+
+ self.videoManager.remove(gameView)
+ }
+}
+
+//MARK: - Game Saves -
+/// Game Saves
+public extension EmulatorCore
+{
+ func save()
+ {
+ self.deltaCore.emulatorBridge.saveGameSave(to: self.gameSaveURL)
+ self.saveHandler?(self)
+ }
+}
+
+//MARK: - Save States -
+/// Save States
+public extension EmulatorCore
+{
+ @discardableResult func saveSaveState(to url: URL) -> SaveStateProtocol
+ {
+ self.deltaCore.emulatorBridge.saveSaveState(to: url)
+
+ let saveState = SaveState(fileURL: url, gameType: self.gameType)
+ return saveState
+ }
+
+ func load(_ saveState: SaveStateProtocol) throws
+ {
+ guard FileManager.default.fileExists(atPath: saveState.fileURL.path) else { throw SaveStateError.doesNotExist }
+
+ self.deltaCore.emulatorBridge.loadSaveState(from: saveState.fileURL)
+
+ self.updateCheats()
+ self.deltaCore.emulatorBridge.resetInputs()
+
+ // Reactivate activated inputs.
+ for gameController in self.gameControllers.allObjects as! [GameController]
+ {
+ for (input, value) in gameController.activatedInputs
+ {
+ gameController.activate(input, value: value)
+ }
+ }
+ }
+}
+
+//MARK: - Cheats -
+/// Cheats
+public extension EmulatorCore
+{
+ func activate(_ cheat: CheatProtocol) throws
+ {
+ let success = self.deltaCore.emulatorBridge.addCheatCode(String(cheat.code), type: cheat.type.rawValue)
+ if success
+ {
+ self.cheatCodes[cheat.code] = cheat.type
+ }
+
+ // Ensures correct state, especially if attempted cheat was invalid
+ self.updateCheats()
+
+ if !success
+ {
+ throw CheatError.invalid
+ }
+ }
+
+ func deactivate(_ cheat: CheatProtocol)
+ {
+ guard self.cheatCodes[cheat.code] != nil else { return }
+
+ self.cheatCodes[cheat.code] = nil
+
+ self.updateCheats()
+ }
+
+ private func updateCheats()
+ {
+ self.deltaCore.emulatorBridge.resetCheats()
+
+ for (cheatCode, type) in self.cheatCodes
+ {
+ self.deltaCore.emulatorBridge.addCheatCode(String(cheatCode), type: type.rawValue)
+ }
+
+ self.deltaCore.emulatorBridge.updateCheats()
+ }
+}
+
+extension EmulatorCore: GameControllerReceiver
+{
+ public func gameController(_ gameController: GameController, didActivate input: Input, value: Double)
+ {
+ self.gameControllers.add(gameController)
+
+ guard let input = self.mappedInput(for: input), input.type == .game(self.gameType) else { return }
+
+ // If any of game controller's sustained inputs map to input, treat input as sustained.
+ let isSustainedInput = gameController.sustainedInputs.keys.contains(where: {
+ guard let mappedInput = gameController.mappedInput(for: $0, receiver: self) else { return false }
+ return self.mappedInput(for: mappedInput) == input
+ })
+
+ if isSustainedInput && !input.isContinuous
+ {
+ self.reactivateInputsQueue.async {
+
+ self.deltaCore.emulatorBridge.deactivateInput(input.intValue!)
+
+ self.reactivateInputsDispatchGroup = DispatchGroup()
+
+ // To ensure the emulator core recognizes us activating an input that is currently active, we need to first deactivate it, wait at least two frames, then activate it again.
+ self.reactivateInputsDispatchGroup?.enter()
+ self.reactivateInputsDispatchGroup?.enter()
+ self.reactivateInputsDispatchGroup?.wait()
+
+ self.reactivateInputsDispatchGroup = nil
+
+ self.deltaCore.emulatorBridge.activateInput(input.intValue!, value: value)
+ }
+ }
+ else
+ {
+ self.deltaCore.emulatorBridge.activateInput(input.intValue!, value: value)
+ }
+ }
+
+ public func gameController(_ gameController: GameController, didDeactivate input: Input)
+ {
+ guard let input = self.mappedInput(for: input), input.type == .game(self.gameType) else { return }
+
+ self.deltaCore.emulatorBridge.deactivateInput(input.intValue!)
+ }
+
+ private func mappedInput(for input: Input) -> Input?
+ {
+ guard let standardInput = StandardGameControllerInput(input: input) else { return input }
+
+ let mappedInput = standardInput.input(for: self.gameType)
+ return mappedInput
+ }
+}
+
+private extension EmulatorCore
+{
+ func runGameLoop()
+ {
+ let emulationQueue = DispatchQueue(label: "com.rileytestut.DeltaCore.emulationQueue", qos: .userInitiated)
+ emulationQueue.async {
+
+ let screenRefreshRate = 1.0 / 60.0
+
+ var emulationTime = Thread.absoluteSystemTime
+ var counter = 0.0
+
+ while true
+ {
+ let frameDuration = self.deltaCore.emulatorBridge.frameDuration / self.rate
+ if frameDuration != self.previousFrameDuration
+ {
+ Thread.setRealTimePriority(withPeriod: frameDuration)
+
+ self.previousFrameDuration = frameDuration
+
+ // Reset counter
+ counter = 0
+ }
+
+ // Update audio/video configurations if necessary.
+
+ let internalFrameDuration = self.deltaCore.emulatorBridge.frameDuration
+ if internalFrameDuration != self.audioManager.frameDuration
+ {
+ self.audioManager.frameDuration = internalFrameDuration
+ }
+
+ let audioFormat = self.deltaCore.audioFormat
+ if audioFormat != self.audioManager.audioFormat
+ {
+ self.audioManager.audioFormat = audioFormat
+ }
+
+ let videoFormat = self.deltaCore.videoFormat
+ if videoFormat != self.videoManager.videoFormat
+ {
+ self.videoManager.videoFormat = videoFormat
+ }
+
+ if counter >= screenRefreshRate
+ {
+ self.runFrame(renderGraphics: true)
+
+ // Reset counter
+ counter = 0
+ }
+ else
+ {
+ // No need to render graphics more than once per screen refresh rate
+ self.runFrame(renderGraphics: false)
+ }
+
+ counter += frameDuration
+ emulationTime += frameDuration
+
+ let currentTime = Thread.absoluteSystemTime
+
+ // The number of frames we need to skip to keep in sync
+ var framesToSkip = Int((currentTime - emulationTime) / frameDuration)
+ framesToSkip = min(framesToSkip, 5) // Prevent unbounding frame skipping resulting in frozen game.
+
+ if framesToSkip > 0
+ {
+ // Only actually skip frames if we're running at normal speed
+ if self.rate == 1.0
+ {
+ for _ in 0 ..< framesToSkip
+ {
+ // "Skip" frames by running them without rendering graphics
+ self.runFrame(renderGraphics: false)
+ }
+ }
+
+ emulationTime = currentTime
+ }
+
+ // Prevent race conditions
+ let state = self._state
+
+ defer
+ {
+ if self.previousState != state
+ {
+ NotificationCenter.default.post(name: EmulatorCore.didUpdateFrameNotification, object: self)
+ self.previousState = state
+ }
+ }
+
+ if state != .running
+ {
+ break
+ }
+
+ Thread.realTimeWait(until: emulationTime)
+ }
+ }
+ }
+
+ func runFrame(renderGraphics: Bool)
+ {
+ self.deltaCore.emulatorBridge.runFrame(processVideo: renderGraphics)
+
+ if renderGraphics
+ {
+ self.videoManager.render()
+ }
+
+ if let dispatchGroup = self.reactivateInputsDispatchGroup
+ {
+ dispatchGroup.leave()
+ }
+
+ self.updateHandler?(self)
+ }
+}
+
+private extension EmulatorCore
+{
+ @objc func emulationDidQuit(_ notification: Notification)
+ {
+ DispatchQueue.global(qos: .userInitiated).async {
+ // Dispatch onto global queue to prevent deadlock.
+ self.stop()
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Emulator Core/Video/BitmapProcessor.swift b/Cores/DeltaCore/Sources/Emulator Core/Video/BitmapProcessor.swift
new file mode 100644
index 000000000..5a6ce4cff
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Emulator Core/Video/BitmapProcessor.swift
@@ -0,0 +1,101 @@
+//
+// BitmapProcessor.swift
+// DeltaCore
+//
+// Created by Riley Testut on 4/8/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import CoreImage
+import Accelerate
+
+fileprivate extension VideoFormat.PixelFormat
+{
+ var nativeCIFormat: CIFormat? {
+ switch self
+ {
+ case .rgb565: return nil
+ case .bgra8: return .BGRA8
+ case .rgba8: return .RGBA8
+ }
+ }
+}
+
+fileprivate extension VideoFormat
+{
+ var pixelFormat: PixelFormat {
+ switch self.format
+ {
+ case .bitmap(let format): return format
+ case .openGLES: fatalError("Should not be using VideoFormat.Format.openGLES with BitmapProcessor.")
+ }
+ }
+
+ var bufferSize: Int {
+ let bufferSize = Int(self.dimensions.width * self.dimensions.height) * self.pixelFormat.bytesPerPixel
+ return bufferSize
+ }
+}
+
+class BitmapProcessor: VideoProcessor
+{
+ let videoFormat: VideoFormat
+ let videoBuffer: UnsafeMutablePointer?
+
+ private let outputVideoFormat: VideoFormat
+ private let outputVideoBuffer: UnsafeMutablePointer
+
+ init(videoFormat: VideoFormat)
+ {
+ self.videoFormat = videoFormat
+
+ switch self.videoFormat.pixelFormat
+ {
+ case .rgb565: self.outputVideoFormat = VideoFormat(format: .bitmap(.bgra8), dimensions: self.videoFormat.dimensions)
+ case .bgra8, .rgba8: self.outputVideoFormat = self.videoFormat
+ }
+
+ self.videoBuffer = UnsafeMutablePointer.allocate(capacity: self.videoFormat.bufferSize)
+ self.outputVideoBuffer = UnsafeMutablePointer.allocate(capacity: self.outputVideoFormat.bufferSize)
+ }
+
+ deinit
+ {
+ self.videoBuffer?.deallocate()
+ self.outputVideoBuffer.deallocate()
+ }
+}
+
+extension BitmapProcessor
+{
+ func prepare()
+ {
+ }
+
+ func processFrame() -> CIImage?
+ {
+ guard let ciFormat = self.outputVideoFormat.pixelFormat.nativeCIFormat else {
+ print("VideoManager output format is not supported.")
+ return nil
+ }
+
+ return autoreleasepool {
+ var inputVImageBuffer = vImage_Buffer(data: self.videoBuffer, height: vImagePixelCount(self.videoFormat.dimensions.height), width: vImagePixelCount(self.videoFormat.dimensions.width), rowBytes: self.videoFormat.pixelFormat.bytesPerPixel * Int(self.videoFormat.dimensions.width))
+ var outputVImageBuffer = vImage_Buffer(data: self.outputVideoBuffer, height: vImagePixelCount(self.outputVideoFormat.dimensions.height), width: vImagePixelCount(self.outputVideoFormat.dimensions.width), rowBytes: self.outputVideoFormat.pixelFormat.bytesPerPixel * Int(self.outputVideoFormat.dimensions.width))
+
+ switch self.videoFormat.pixelFormat
+ {
+ case .rgb565: vImageConvert_RGB565toBGRA8888(255, &inputVImageBuffer, &outputVImageBuffer, 0)
+ case .bgra8, .rgba8:
+ // Ensure alpha value is 255, not 0.
+ // 0x1 refers to the Blue channel in ARGB, which corresponds to the Alpha channel in BGRA and RGBA.
+ vImageOverwriteChannelsWithScalar_ARGB8888(255, &inputVImageBuffer, &outputVImageBuffer, 0x1, vImage_Flags(kvImageNoFlags))
+ }
+
+ let bitmapData = Data(bytes: self.outputVideoBuffer, count: self.outputVideoFormat.bufferSize)
+
+ let image = CIImage(bitmapData: bitmapData, bytesPerRow: self.outputVideoFormat.pixelFormat.bytesPerPixel * Int(self.outputVideoFormat.dimensions.width), size: self.outputVideoFormat.dimensions, format: ciFormat, colorSpace: nil)
+ return image
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Emulator Core/Video/OpenGLESProcessor.swift b/Cores/DeltaCore/Sources/Emulator Core/Video/OpenGLESProcessor.swift
new file mode 100644
index 000000000..a01a34481
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Emulator Core/Video/OpenGLESProcessor.swift
@@ -0,0 +1,149 @@
+//
+// OpenGLESProcessor.swift
+// DeltaCore
+//
+// Created by Riley Testut on 4/8/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import CoreImage
+import GLKit
+
+class OpenGLESProcessor: VideoProcessor
+{
+ var videoFormat: VideoFormat {
+ didSet {
+ self.resizeVideoBuffers()
+ }
+ }
+
+ private let context: EAGLContext
+
+ private var framebuffer: GLuint = 0
+ private var texture: GLuint = 0
+ private var renderbuffer: GLuint = 0
+
+ private var indexBuffer: GLuint = 0
+ private var vertexBuffer: GLuint = 0
+
+ init(videoFormat: VideoFormat, context: EAGLContext)
+ {
+ self.videoFormat = videoFormat
+ self.context = EAGLContext(api: .openGLES2, sharegroup: context.sharegroup)!
+ }
+
+ deinit
+ {
+ if self.renderbuffer > 0
+ {
+ glDeleteRenderbuffers(1, &self.renderbuffer)
+ }
+
+ if self.texture > 0
+ {
+ glDeleteTextures(1, &self.texture)
+ }
+
+ if self.framebuffer > 0
+ {
+ glDeleteFramebuffers(1, &self.framebuffer)
+ }
+
+ if self.indexBuffer > 0
+ {
+ glDeleteBuffers(1, &self.indexBuffer)
+ }
+
+ if self.vertexBuffer > 0
+ {
+ glDeleteBuffers(1, &self.vertexBuffer)
+ }
+ }
+}
+
+extension OpenGLESProcessor
+{
+ var videoBuffer: UnsafeMutablePointer? {
+ return nil
+ }
+
+ func prepare()
+ {
+ struct Vertex
+ {
+ var x: GLfloat
+ var y: GLfloat
+ var z: GLfloat
+
+ var u: GLfloat
+ var v: GLfloat
+ }
+
+ EAGLContext.setCurrent(self.context)
+
+ // Vertex buffer
+ let vertices = [Vertex(x: -1.0, y: -1.0, z: 1.0, u: 0.0, v: 0.0),
+ Vertex(x: 1.0, y: -1.0, z: 1.0, u: 1.0, v: 0.0),
+ Vertex(x: 1.0, y: 1.0, z: 1.0, u: 1.0, v: 1.0),
+ Vertex(x: -1.0, y: 1.0, z: 1.0, u: 0.0, v: 1.0)]
+ glGenBuffers(1, &self.vertexBuffer)
+ glBindBuffer(GLenum(GL_ARRAY_BUFFER), self.vertexBuffer)
+ glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout.size * vertices.count, vertices, GLenum(GL_DYNAMIC_DRAW))
+ glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
+
+ // Index buffer
+ let indices: [GLushort] = [0, 1, 2, 0, 2, 3]
+ glGenBuffers(1, &self.indexBuffer)
+ glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), self.indexBuffer)
+ glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER), MemoryLayout.size * indices.count, indices, GLenum(GL_STATIC_DRAW))
+ glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), 0)
+
+ // Framebuffer
+ glGenFramebuffers(1, &self.framebuffer)
+ glBindFramebuffer(GLenum(GL_FRAMEBUFFER), self.framebuffer)
+
+ // Texture
+ glGenTextures(1, &self.texture)
+ glBindTexture(GLenum(GL_TEXTURE_2D), self.texture)
+ glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GLint(GL_LINEAR))
+ glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GLint(GL_LINEAR))
+ glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GLint(GL_CLAMP_TO_EDGE))
+ glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GLint(GL_CLAMP_TO_EDGE))
+ glBindTexture(GLenum(GL_TEXTURE_2D), 0)
+ glFramebufferTexture2D(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_TEXTURE_2D), self.texture, 0)
+
+ // Renderbuffer
+ glGenRenderbuffers(1, &self.renderbuffer)
+ glBindRenderbuffer(GLenum(GL_RENDERBUFFER), self.renderbuffer)
+ glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_DEPTH_ATTACHMENT), GLenum(GL_RENDERBUFFER), self.renderbuffer)
+
+ self.resizeVideoBuffers()
+ }
+
+ func processFrame() -> CIImage?
+ {
+ glFlush()
+
+ let image = CIImage(texture: self.texture, size: self.videoFormat.dimensions, flipped: false, colorSpace: nil)
+ return image
+ }
+}
+
+private extension OpenGLESProcessor
+{
+ func resizeVideoBuffers()
+ {
+ guard self.texture > 0 && self.renderbuffer > 0 else { return }
+
+ EAGLContext.setCurrent(self.context)
+
+ glBindTexture(GLenum(GL_TEXTURE_2D), self.texture)
+ glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(self.videoFormat.dimensions.width), GLsizei(self.videoFormat.dimensions.height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), nil)
+ glBindTexture(GLenum(GL_TEXTURE_2D), 0)
+
+ glBindRenderbuffer(GLenum(GL_RENDERBUFFER), self.renderbuffer)
+ glRenderbufferStorage(GLenum(GL_RENDERBUFFER), GLenum(GL_DEPTH_COMPONENT16), GLsizei(self.videoFormat.dimensions.width), GLsizei(self.videoFormat.dimensions.height))
+
+ glViewport(0, 0, GLsizei(self.videoFormat.dimensions.width), GLsizei(self.videoFormat.dimensions.height))
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Emulator Core/Video/VideoFormat.swift b/Cores/DeltaCore/Sources/Emulator Core/Video/VideoFormat.swift
new file mode 100644
index 000000000..6bedc72e0
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Emulator Core/Video/VideoFormat.swift
@@ -0,0 +1,47 @@
+//
+// VideoBufferInfo.swift
+// DeltaCore
+//
+// Created by Riley Testut on 4/18/17.
+// Copyright © 2017 Riley Testut. All rights reserved.
+//
+
+import CoreGraphics
+import CoreImage
+
+extension VideoFormat
+{
+ public enum Format: Equatable
+ {
+ case bitmap(PixelFormat)
+ case openGLES
+ }
+
+ public enum PixelFormat: Equatable
+ {
+ case rgb565
+ case bgra8
+ case rgba8
+
+ public var bytesPerPixel: Int {
+ switch self
+ {
+ case .rgb565: return 2
+ case .bgra8: return 4
+ case .rgba8: return 4
+ }
+ }
+ }
+}
+
+public struct VideoFormat: Equatable
+{
+ public var format: Format
+ public var dimensions: CGSize
+
+ public init(format: Format, dimensions: CGSize)
+ {
+ self.format = format
+ self.dimensions = dimensions
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Emulator Core/Video/VideoManager.swift b/Cores/DeltaCore/Sources/Emulator Core/Video/VideoManager.swift
new file mode 100644
index 000000000..f1f857945
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Emulator Core/Video/VideoManager.swift
@@ -0,0 +1,152 @@
+//
+// VideoManager.swift
+// DeltaCore
+//
+// Created by Riley Testut on 3/16/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+import Accelerate
+import CoreImage
+import GLKit
+
+protocol VideoProcessor
+{
+ var videoBuffer: UnsafeMutablePointer? { get }
+
+ func prepare()
+ func processFrame() -> CIImage?
+}
+
+public class VideoManager: NSObject, VideoRendering
+{
+ public internal(set) var videoFormat: VideoFormat {
+ didSet {
+ self.updateProcessor()
+ }
+ }
+
+ public private(set) var gameViews = [GameView]()
+
+ public var isEnabled = true
+
+ private let context: EAGLContext
+ private let ciContext: CIContext
+
+ private var processor: VideoProcessor
+ @NSCopying private var processedImage: CIImage?
+ @NSCopying private var displayedImage: CIImage? // Can only accurately snapshot rendered images.
+
+ public init(videoFormat: VideoFormat)
+ {
+ self.videoFormat = videoFormat
+ self.context = EAGLContext(api: .openGLES2)!
+ self.ciContext = CIContext(eaglContext: self.context, options: [.workingColorSpace: NSNull()])
+
+ switch videoFormat.format
+ {
+ case .bitmap: self.processor = BitmapProcessor(videoFormat: videoFormat)
+ case .openGLES: self.processor = OpenGLESProcessor(videoFormat: videoFormat, context: self.context)
+ }
+
+ super.init()
+ }
+
+ private func updateProcessor()
+ {
+ switch self.videoFormat.format
+ {
+ case .bitmap:
+ self.processor = BitmapProcessor(videoFormat: self.videoFormat)
+
+ case .openGLES:
+ guard let processor = self.processor as? OpenGLESProcessor else { return }
+ processor.videoFormat = self.videoFormat
+ }
+ }
+}
+
+public extension VideoManager
+{
+ func add(_ gameView: GameView)
+ {
+ gameView.eaglContext = self.context
+ self.gameViews.append(gameView)
+ }
+
+ func remove(_ gameView: GameView)
+ {
+ if let index = self.gameViews.firstIndex(of: gameView)
+ {
+ self.gameViews.remove(at: index)
+ }
+ }
+}
+
+public extension VideoManager
+{
+ var videoBuffer: UnsafeMutablePointer? {
+ return self.processor.videoBuffer
+ }
+
+ func prepare()
+ {
+ self.processor.prepare()
+ }
+
+ func processFrame()
+ {
+ guard self.isEnabled else { return }
+
+ autoreleasepool {
+ self.processedImage = self.processor.processFrame()
+ }
+ }
+
+ func render()
+ {
+ guard self.isEnabled else { return }
+
+ guard let image = self.processedImage else { return }
+
+ // Autoreleasepool necessary to prevent leaking CIImages.
+ autoreleasepool {
+ for gameView in self.gameViews
+ {
+ gameView.inputImage = image
+ }
+
+ self.displayedImage = image
+ }
+ }
+
+ func snapshot() -> UIImage?
+ {
+ guard let displayedImage = self.displayedImage else { return nil }
+
+ let imageWidth = Int(self.videoFormat.dimensions.width)
+ let imageHeight = Int(self.videoFormat.dimensions.height)
+ let capacity = imageWidth * imageHeight * 4
+
+ let imageBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: capacity, alignment: 1)
+ defer { imageBuffer.deallocate() }
+
+ guard let baseAddress = imageBuffer.baseAddress, let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) else { return nil }
+
+ // Must render to raw buffer first so we can set CGImageAlphaInfo.noneSkipLast flag when creating CGImage.
+ // Otherwise, some parts of images may incorrectly be transparent.
+ self.ciContext.render(displayedImage, toBitmap: baseAddress, rowBytes: imageWidth * 4, bounds: displayedImage.extent, format: .RGBA8, colorSpace: colorSpace)
+
+ let data = Data(bytes: baseAddress, count: imageBuffer.count)
+ let bitmapInfo: CGBitmapInfo = [CGBitmapInfo.byteOrder32Big, CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipLast.rawValue)]
+
+ guard
+ let dataProvider = CGDataProvider(data: data as CFData),
+ let cgImage = CGImage(width: imageWidth, height: imageHeight, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: imageWidth * 4, space: colorSpace, bitmapInfo: bitmapInfo, provider: dataProvider, decode: nil, shouldInterpolate: true, intent: .defaultIntent)
+ else { return nil }
+
+ let image = UIImage(cgImage: cgImage)
+ return image
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Extensions/Bundle+Resources.swift b/Cores/DeltaCore/Sources/Extensions/Bundle+Resources.swift
new file mode 100644
index 000000000..63cbfdade
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Extensions/Bundle+Resources.swift
@@ -0,0 +1,32 @@
+//
+// Bundle+Resources.swift
+// DeltaCore
+//
+// Created by Riley Testut on 2/3/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+extension Bundle
+{
+ class var resources: Bundle {
+ #if FRAMEWORK
+ let bundle = Bundle(for: RingBuffer.self)
+ #elseif STATIC_LIBRARY
+ let bundle: Bundle
+ if let bundleURL = Bundle.main.url(forResource: "DeltaCore", withExtension: "bundle")
+ {
+ bundle = Bundle(url: bundleURL)!
+ }
+ else
+ {
+ bundle = .main
+ }
+ #else
+ let bundle = Bundle.main
+ #endif
+
+ return bundle
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Extensions/CGGeometry+Dictionary.swift b/Cores/DeltaCore/Sources/Extensions/CGGeometry+Dictionary.swift
new file mode 100644
index 000000000..fd27b1c4f
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Extensions/CGGeometry+Dictionary.swift
@@ -0,0 +1,53 @@
+//
+// CGGeometry+Dictionary.swift
+// DeltaCore
+//
+// Created by Riley Testut on 12/19/15.
+// Copyright © 2015 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+internal extension CGRect
+{
+ init?(dictionary: [String: CGFloat])
+ {
+ guard
+ let x = dictionary["x"],
+ let y = dictionary["y"],
+ let width = dictionary["width"],
+ let height = dictionary["height"]
+ else { return nil }
+
+ self = CGRect(x: x, y: y, width: width, height: height)
+ }
+}
+
+internal extension CGSize
+{
+ init?(dictionary: [String: CGFloat])
+ {
+ guard
+ let width = dictionary["width"],
+ let height = dictionary["height"]
+ else { return nil }
+
+ self = CGSize(width: width, height: height)
+ }
+}
+
+internal extension UIEdgeInsets
+{
+ init?(dictionary: [String: CGFloat])
+ {
+ guard
+ let top = dictionary["top"],
+ let bottom = dictionary["bottom"],
+ let left = dictionary["left"],
+ let right = dictionary["right"]
+ else { return nil }
+
+ self = UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)
+ }
+}
+
diff --git a/Cores/DeltaCore/Sources/Extensions/CharacterSet+Hexadecimals.swift b/Cores/DeltaCore/Sources/Extensions/CharacterSet+Hexadecimals.swift
new file mode 100644
index 000000000..feb2038d0
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Extensions/CharacterSet+Hexadecimals.swift
@@ -0,0 +1,38 @@
+//
+// CharacterSet+Hexadecimals.swift
+// DeltaCore
+//
+// Created by Riley Testut on 4/30/17.
+// Copyright © 2017 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+// Extend NSCharacterSet for Objective-C interopability.
+public extension NSCharacterSet
+{
+ @objc(hexadecimalCharacterSet)
+ class var hexadecimals: NSCharacterSet
+ {
+ let characterSet = NSCharacterSet(charactersIn: "0123456789ABCDEFabcdef")
+ return characterSet
+ }
+}
+
+public extension NSMutableCharacterSet
+{
+ @objc(hexadecimalCharacterSet)
+ override class var hexadecimals: NSMutableCharacterSet
+ {
+ let characterSet = NSCharacterSet.hexadecimals.mutableCopy() as! NSMutableCharacterSet
+ return characterSet
+ }
+}
+
+public extension CharacterSet
+{
+ static var hexadecimals: CharacterSet
+ {
+ return NSCharacterSet.hexadecimals as CharacterSet
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Extensions/Thread+RealTime.swift b/Cores/DeltaCore/Sources/Extensions/Thread+RealTime.swift
new file mode 100644
index 000000000..8b2598867
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Extensions/Thread+RealTime.swift
@@ -0,0 +1,57 @@
+//
+// Thread+RealTime.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/29/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+import Darwin.Mach
+
+private let machToSeconds: Double = {
+ var base = mach_timebase_info()
+ mach_timebase_info(&base)
+ return 1e-9 * Double(base.numer) / Double(base.denom)
+}()
+
+internal extension Thread
+{
+ class var absoluteSystemTime: TimeInterval {
+ return Double(mach_absolute_time()) * machToSeconds;
+ }
+
+ @discardableResult class func setRealTimePriority(withPeriod period: TimeInterval) -> Bool
+ {
+ var policy = thread_time_constraint_policy()
+ policy.period = UInt32(period / machToSeconds)
+ policy.computation = UInt32(0.007 / machToSeconds)
+ policy.constraint = UInt32(0.03 / machToSeconds)
+ policy.preemptible = 0
+
+ let threadport = pthread_mach_thread_np(pthread_self())
+ let count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size)
+
+ var result = KERN_SUCCESS
+
+ withUnsafePointer(to: &policy) { (pointer) in
+ pointer.withMemoryRebound(to: integer_t.self, capacity: 1) { (policyPointer) in
+ let mutablePolicyPointer = UnsafeMutablePointer(mutating: policyPointer)
+ result = thread_policy_set(threadport, UInt32(THREAD_TIME_CONSTRAINT_POLICY), mutablePolicyPointer, count)
+ }
+ }
+
+ if result != KERN_SUCCESS
+ {
+ print("Thread.setRealTimePriority(withPeriod:) failed.")
+ return false
+ }
+
+ return true
+ }
+
+ class func realTimeWait(until targetTime: TimeInterval)
+ {
+ mach_wait_until(UInt64(targetTime / machToSeconds))
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Extensions/UIApplication+AppExtension.swift b/Cores/DeltaCore/Sources/Extensions/UIApplication+AppExtension.swift
new file mode 100644
index 000000000..7b56d2fa1
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Extensions/UIApplication+AppExtension.swift
@@ -0,0 +1,18 @@
+//
+// UIApplication+AppExtension.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/14/18.
+// Copyright © 2018 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+public extension UIApplication
+{
+ // Cannot normally use UIApplication.shared from extensions, so we get around this by calling value(forKey:).
+ class var delta_shared: UIApplication? {
+ return UIApplication.value(forKey: "sharedApplication") as? UIApplication
+ }
+}
+
diff --git a/Cores/DeltaCore/Sources/Extensions/UIDevice+Vibration.swift b/Cores/DeltaCore/Sources/Extensions/UIDevice+Vibration.swift
new file mode 100644
index 000000000..b0ff27282
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Extensions/UIDevice+Vibration.swift
@@ -0,0 +1,93 @@
+//
+// UIDevice+Vibration.swift
+// DeltaCore
+//
+// Created by Riley Testut on 11/28/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import UIKit
+import AudioToolbox
+
+@_silgen_name("AudioServicesStopSystemSound")
+func AudioServicesStopSystemSound(_ soundID: SystemSoundID)
+
+// vibrationPattern parameter must be NSDictionary to prevent crash when bridging from Swift.Dictionary.
+@_silgen_name("AudioServicesPlaySystemSoundWithVibration")
+func AudioServicesPlaySystemSoundWithVibration(_ soundID: SystemSoundID, _ idk: Any?, _ vibrationPattern: NSDictionary)
+
+public extension UIDevice
+{
+ enum FeedbackSupportLevel: Int
+ {
+ case unsupported
+ case basic
+ case feedbackGenerator
+ }
+}
+
+public extension UIDevice
+{
+ var feedbackSupportLevel: FeedbackSupportLevel
+ {
+ guard let rawValue = self.value(forKey: "_feedbackSupportLevel") as? Int else { return .unsupported }
+
+ let feedbackSupportLevel = FeedbackSupportLevel(rawValue: rawValue)
+ return feedbackSupportLevel ?? .feedbackGenerator // We'll assume raw values greater than 2 still support UIFeedbackGenerator ¯\_(ツ)_/¯
+ }
+
+ var isVibrationSupported: Bool {
+ #if (arch(i386) || arch(x86_64))
+ // Return false for iOS simulator
+ return false
+ #else
+ // All iPhones support some form of vibration, and potentially future non-iPhone devices will support taptic feedback
+ return (self.model.hasPrefix("iPhone")) || self.feedbackSupportLevel != .unsupported
+ #endif
+ }
+
+ func vibrate()
+ {
+ guard self.isVibrationSupported else { return }
+
+ switch self.feedbackSupportLevel
+ {
+ case .unsupported:
+ AudioServicesStopSystemSound(kSystemSoundID_Vibrate)
+
+ var vibrationLength = 30
+
+ if self.modelGeneration.hasPrefix("iPhone6")
+ {
+ // iPhone 5S has a weaker vibration motor, so we vibrate for 10ms longer to compensate
+ vibrationLength = 40;
+ }
+
+ // Must use NSArray/NSDictionary to prevent crash.
+ let pattern: [Any] = [false, 0, true, vibrationLength]
+ let dictionary: [String: Any] = ["VibePattern": pattern, "Intensity": 1]
+
+ AudioServicesPlaySystemSoundWithVibration(kSystemSoundID_Vibrate, nil, dictionary as NSDictionary)
+
+ case .basic, .feedbackGenerator: AudioServicesPlaySystemSound(1519) // "peek" vibration
+ }
+ }
+}
+
+private extension UIDevice
+{
+ var modelGeneration: String {
+ var sysinfo = utsname()
+ uname(&sysinfo)
+
+ var modelGeneration: String!
+
+ withUnsafePointer(to: &sysinfo.machine) { pointer in
+ pointer.withMemoryRebound(to: UInt8.self, capacity: Int(Mirror(reflecting: pointer.pointee).children.count), { (pointer) in
+ modelGeneration = String(cString: pointer)
+ })
+ }
+
+ return modelGeneration
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Extensions/UIImage+PDF.swift b/Cores/DeltaCore/Sources/Extensions/UIImage+PDF.swift
new file mode 100644
index 000000000..cf0763201
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Extensions/UIImage+PDF.swift
@@ -0,0 +1,70 @@
+//
+// UIImage+PDF.swift
+// DeltaCore
+//
+// Created by Riley Testut on 12/21/15.
+// Copyright © 2015 Riley Testut. All rights reserved.
+//
+// Based on Erica Sadun's UIImage+PDFUtility ( https://github.com/erica/useful-things/blob/master/useful%20pack/UIImage%2BPDF/UIImage%2BPDFUtility.m )
+//
+
+import UIKit
+import CoreGraphics
+import AVFoundation
+
+internal extension UIImage
+{
+ class func image(withPDFData data: Data, targetSize: CGSize) -> UIImage?
+ {
+ guard targetSize.width > 0 && targetSize.height > 0 else { return nil }
+
+ guard
+ let dataProvider = CGDataProvider(data: data as CFData),
+ let document = CGPDFDocument(dataProvider),
+ let page = document.page(at: 1)
+ else { return nil }
+
+ let pageFrame = page.getBoxRect(.cropBox)
+
+ var destinationFrame = AVMakeRect(aspectRatio: pageFrame.size, insideRect: CGRect(origin: CGPoint.zero, size: targetSize))
+ destinationFrame.origin = CGPoint.zero
+
+ let format = UIGraphicsImageRendererFormat()
+ format.scale = UIScreen.main.scale
+ let imageRenderer = UIGraphicsImageRenderer(bounds: destinationFrame, format: format)
+
+ let image = imageRenderer.image { (imageRendererContext) in
+
+ let context = imageRendererContext.cgContext
+
+ // Save state
+ context.saveGState()
+
+ // Flip coordinate system to match Quartz system
+ let transform = CGAffineTransform.identity.scaledBy(x: 1.0, y: -1.0).translatedBy(x: 0.0, y: -targetSize.height)
+ context.concatenate(transform)
+
+ // Calculate rendering frames
+ destinationFrame = destinationFrame.applying(transform)
+
+ let aspectScale = min(destinationFrame.width / pageFrame.width, destinationFrame.height / pageFrame.height)
+
+ // Ensure aspect ratio is preserved
+ var drawingFrame = pageFrame.applying(CGAffineTransform(scaleX: aspectScale, y: aspectScale))
+ drawingFrame.origin.x = destinationFrame.midX - (drawingFrame.width / 2.0)
+ drawingFrame.origin.y = destinationFrame.midY - (drawingFrame.height / 2.0)
+
+ // Scale the context
+ context.translateBy(x: destinationFrame.minX, y: destinationFrame.minY)
+ context.scaleBy(x: aspectScale, y: aspectScale)
+
+ // Render the PDF
+ context.drawPDFPage(page)
+
+ // Restore state
+ context.restoreGState()
+ }
+
+ return image
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Extensions/UIResponder+FirstResponder.swift b/Cores/DeltaCore/Sources/Extensions/UIResponder+FirstResponder.swift
new file mode 100644
index 000000000..bdd445f66
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Extensions/UIResponder+FirstResponder.swift
@@ -0,0 +1,30 @@
+//
+// UIResponder+FirstResponder.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/14/18.
+// Copyright © 2018 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+private class FirstResponderEvent: UIEvent
+{
+ var firstResponder: UIResponder?
+}
+
+extension UIResponder
+{
+ @objc(delta_firstResponder)
+ class var firstResponder: UIResponder? {
+ let event = FirstResponderEvent()
+ UIApplication.delta_shared?.sendAction(#selector(UIResponder.findFirstResponder(sender:event:)), to: nil, from: nil, for: event)
+ return event.firstResponder
+ }
+
+ @objc(delta_findFirstResponderWithSender:event:)
+ private func findFirstResponder(sender: Any?, event: FirstResponderEvent)
+ {
+ event.firstResponder = self
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Extensions/UIScreen+ControllerSkin.swift b/Cores/DeltaCore/Sources/Extensions/UIScreen+ControllerSkin.swift
new file mode 100644
index 000000000..9b0c2ccf5
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Extensions/UIScreen+ControllerSkin.swift
@@ -0,0 +1,36 @@
+//
+// UIScreen+ControllerSkin.swift
+// DeltaCore
+//
+// Created by Riley Testut on 7/4/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+public extension UIScreen
+{
+ var defaultControllerSkinSize: ControllerSkin.Size
+ {
+ let fixedBounds = self.fixedCoordinateSpace.convert(self.bounds, from: self.coordinateSpace)
+
+ if UIDevice.current.userInterfaceIdiom == .pad
+ {
+ switch fixedBounds.width
+ {
+ case 768: return .small
+ case 834: return .medium
+ default: return .large
+ }
+ }
+ else
+ {
+ switch fixedBounds.width
+ {
+ case 320: return .small
+ case 375: return .medium
+ default: return .large
+ }
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Filters/FilterChain.swift b/Cores/DeltaCore/Sources/Filters/FilterChain.swift
new file mode 100644
index 000000000..334a41f83
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Filters/FilterChain.swift
@@ -0,0 +1,89 @@
+//
+// FilterChain.swift
+// DeltaCore
+//
+// Created by Riley Testut on 4/13/17.
+// Copyright © 2017 Riley Testut. All rights reserved.
+//
+
+import CoreImage
+
+private extension CIImage
+{
+ func flippingYAxis() -> CIImage
+ {
+ let transform = CGAffineTransform(scaleX: 1, y: -1)
+ let flippedImage = self.applyingFilter("CIAffineTransform", parameters: ["inputTransform": NSValue(cgAffineTransform: transform)])
+
+ let translation = CGAffineTransform(translationX: 0, y: self.extent.height)
+ let translatedImage = flippedImage.applyingFilter("CIAffineTransform", parameters: ["inputTransform": NSValue(cgAffineTransform: translation)])
+
+ return translatedImage
+ }
+}
+
+@objcMembers
+public class FilterChain: CIFilter
+{
+ public var inputFilters = [CIFilter]()
+
+ public var inputImage: CIImage?
+
+ public override var outputImage: CIImage? {
+ return self.inputFilters.reduce(self.inputImage, { (image, filter) -> CIImage? in
+ guard var image = image else { return nil }
+
+ let flippedImage = image.flippingYAxis()
+
+ var outputImage: CIImage?
+
+ if filter.inputKeys.contains(kCIInputImageKey)
+ {
+ filter.setValue(flippedImage, forKey: kCIInputImageKey)
+ outputImage = filter.outputImage
+ }
+ else
+ {
+ guard var filterImage = filter.outputImage else { return image }
+
+ if filterImage.extent.isInfinite
+ {
+ filterImage = filterImage.cropped(to: flippedImage.extent)
+ }
+
+ // Filter is already "flipped", so no need to flip it again.
+ // filterImage = filterImage.flippingYAxis()
+
+ outputImage = filterImage.composited(over: flippedImage)
+ }
+
+ outputImage = outputImage?.flippingYAxis()
+
+ if let image = outputImage, image.extent.origin != .zero
+ {
+ // Always translate CIImage back to origin so later calculations are correct.
+ let translation = CGAffineTransform(translationX: -image.extent.origin.x, y: -image.extent.origin.y)
+ outputImage = image.applyingFilter("CIAffineTransform", parameters: ["inputTransform": NSValue(cgAffineTransform: translation)])
+ }
+
+ return outputImage
+ })
+ }
+
+ public override init()
+ {
+ // Must be declared or else we'll get "Use of unimplemented initializer FilterChain.init()" runtime exception.
+ super.init()
+ }
+
+ public init(filters: [CIFilter])
+ {
+ self.inputFilters = filters
+ super.init()
+ }
+
+ public required init?(coder aDecoder: NSCoder)
+ {
+ super.init(coder: aDecoder)
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Game Controllers/ExternalGameControllerManager.swift b/Cores/DeltaCore/Sources/Game Controllers/ExternalGameControllerManager.swift
new file mode 100644
index 000000000..c6d402593
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Game Controllers/ExternalGameControllerManager.swift
@@ -0,0 +1,237 @@
+//
+// ExternalGameControllerManager.swift
+// DeltaCore
+//
+// Created by Riley Testut on 8/20/15.
+// Copyright © 2015 Riley Testut. All rights reserved.
+//
+
+import Foundation
+import GameController
+
+private let ExternalKeyboardStatusDidChange: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = {
+ (notificationCenter, observer, name, object, userInfo) in
+
+ if ExternalGameControllerManager.shared.isKeyboardConnected
+ {
+ NotificationCenter.default.post(name: .externalKeyboardDidConnect, object: nil)
+ }
+ else
+ {
+ NotificationCenter.default.post(name: .externalKeyboardDidDisconnect, object: nil)
+ }
+}
+
+public extension Notification.Name
+{
+ static let externalGameControllerDidConnect = Notification.Name("ExternalGameControllerDidConnectNotification")
+ static let externalGameControllerDidDisconnect = Notification.Name("ExternalGameControllerDidDisconnectNotification")
+
+ static let externalKeyboardDidConnect = Notification.Name("ExternalKeyboardDidConnect")
+ static let externalKeyboardDidDisconnect = Notification.Name("ExternalKeyboardDidDisconnect")
+}
+
+public class ExternalGameControllerManager: UIResponder
+{
+ public static let shared = ExternalGameControllerManager()
+
+ //MARK: - Properties -
+ /** Properties **/
+ public private(set) var connectedControllers: [GameController] = []
+
+ public var automaticallyAssignsPlayerIndexes: Bool
+
+ private var nextAvailablePlayerIndex: Int {
+ var nextPlayerIndex = -1
+
+ let sortedGameControllers = self.connectedControllers.sorted { ($0.playerIndex ?? -1) < ($1.playerIndex ?? -1) }
+ for controller in sortedGameControllers
+ {
+ let playerIndex = controller.playerIndex ?? -1
+
+ if abs(playerIndex - nextPlayerIndex) > 1
+ {
+ break
+ }
+ else
+ {
+ nextPlayerIndex = playerIndex
+ }
+ }
+
+ nextPlayerIndex += 1
+
+ return nextPlayerIndex
+ }
+
+ private override init()
+ {
+#if targetEnvironment(simulator)
+ self.automaticallyAssignsPlayerIndexes = false
+#else
+ self.automaticallyAssignsPlayerIndexes = true
+#endif
+
+ super.init()
+ }
+}
+
+//MARK: - Discovery -
+/** Discovery **/
+public extension ExternalGameControllerManager
+{
+ func startMonitoring()
+ {
+ for controller in GCController.controllers()
+ {
+ let externalController = MFiGameController(controller: controller)
+ self.add(externalController)
+ }
+
+ if self.isKeyboardConnected
+ {
+ let keyboardController = KeyboardGameController()
+ self.add(keyboardController)
+ }
+
+ NotificationCenter.default.addObserver(self, selector: #selector(ExternalGameControllerManager.mfiGameControllerDidConnect(_:)), name: .GCControllerDidConnect, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(ExternalGameControllerManager.mfiGameControllerDidDisconnect(_:)), name: .GCControllerDidDisconnect, object: nil)
+
+ NotificationCenter.default.addObserver(self, selector: #selector(ExternalGameControllerManager.keyboardDidConnect(_:)), name: .externalKeyboardDidConnect, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(ExternalGameControllerManager.keyboardDidDisconnect(_:)), name: .externalKeyboardDidDisconnect, object: nil)
+
+ if let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
+ {
+ CFNotificationCenterAddObserver(notificationCenter, nil, ExternalKeyboardStatusDidChange, "GSEventHardwareKeyboardAttached" as CFString, nil, .deliverImmediately)
+ }
+ }
+
+ func stopMonitoring()
+ {
+ NotificationCenter.default.removeObserver(self, name: .GCControllerDidConnect, object: nil)
+ NotificationCenter.default.removeObserver(self, name: .GCControllerDidDisconnect, object: nil)
+
+ NotificationCenter.default.removeObserver(self, name: .externalKeyboardDidConnect, object: nil)
+ NotificationCenter.default.removeObserver(self, name: .externalKeyboardDidDisconnect, object: nil)
+
+ self.connectedControllers.removeAll()
+ }
+
+ func startWirelessControllerDiscovery(withCompletionHandler completionHandler: (() -> Void)?)
+ {
+ GCController.startWirelessControllerDiscovery(completionHandler: completionHandler)
+ }
+
+ func stopWirelessControllerDiscovery()
+ {
+ GCController.stopWirelessControllerDiscovery()
+ }
+}
+
+//MARK: - External Keyboard -
+public extension ExternalGameControllerManager
+{
+ // Implementation based on Ian McDowell's tweet: https://twitter.com/ian_mcdowell/status/844572113759547392
+ var isKeyboardConnected: Bool {
+ guard let uiKeyboardClass: AnyObject = NSClassFromString("UIKeyboard") else { return false }
+
+ let selector = NSSelectorFromString("shouldMinimizeForHardwareKeyboard")
+ guard uiKeyboardClass.responds(to: selector) else { return false }
+
+ if let _ = uiKeyboardClass.perform(selector)
+ {
+ // Returns non-nil value when true, so return true ourselves.
+ return true
+ }
+
+ return false
+ }
+
+ override func keyPressesBegan(_ presses: Set, with event: UIEvent)
+ {
+ for case let keyboardController as KeyboardGameController in self.connectedControllers
+ {
+ keyboardController.keyPressesBegan(presses, with: event)
+ }
+ }
+
+ override func keyPressesEnded(_ presses: Set, with event: UIEvent)
+ {
+ for case let keyboardController as KeyboardGameController in self.connectedControllers
+ {
+ keyboardController.keyPressesEnded(presses, with: event)
+ }
+ }
+}
+
+//MARK: - Managing Controllers -
+private extension ExternalGameControllerManager
+{
+ func add(_ controller: GameController)
+ {
+ if self.automaticallyAssignsPlayerIndexes
+ {
+ let playerIndex = self.nextAvailablePlayerIndex
+ controller.playerIndex = playerIndex
+ }
+
+ self.connectedControllers.append(controller)
+
+ NotificationCenter.default.post(name: .externalGameControllerDidConnect, object: controller)
+ }
+
+ func remove(_ controller: GameController)
+ {
+ guard let index = self.connectedControllers.firstIndex(where: { $0.isEqual(controller) }) else { return }
+
+ self.connectedControllers.remove(at: index)
+
+ NotificationCenter.default.post(name: .externalGameControllerDidDisconnect, object: controller)
+ }
+}
+
+//MARK: - MFi Game Controllers -
+private extension ExternalGameControllerManager
+{
+ @objc func mfiGameControllerDidConnect(_ notification: Notification)
+ {
+ guard let controller = notification.object as? GCController else { return }
+
+ let externalController = MFiGameController(controller: controller)
+ self.add(externalController)
+ }
+
+ @objc func mfiGameControllerDidDisconnect(_ notification: Notification)
+ {
+ guard let controller = notification.object as? GCController else { return }
+
+ for externalController in self.connectedControllers
+ {
+ guard let mfiController = externalController as? MFiGameController else { continue }
+
+ if mfiController.controller == controller
+ {
+ self.remove(externalController)
+ }
+ }
+ }
+}
+
+//MARK: - Keyboard Game Controllers -
+private extension ExternalGameControllerManager
+{
+ @objc func keyboardDidConnect(_ notification: Notification)
+ {
+ guard !self.connectedControllers.contains(where: { $0 is KeyboardGameController }) else { return }
+
+ let keyboardController = KeyboardGameController()
+ self.add(keyboardController)
+ }
+
+ @objc func keyboardDidDisconnect(_ notification: Notification)
+ {
+ guard let keyboardController = self.connectedControllers.first(where: { $0 is KeyboardGameController }) else { return }
+
+ self.remove(keyboardController)
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Game Controllers/Keyboard/KeyboardGameController.swift b/Cores/DeltaCore/Sources/Game Controllers/Keyboard/KeyboardGameController.swift
new file mode 100644
index 000000000..8d1781c89
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Game Controllers/Keyboard/KeyboardGameController.swift
@@ -0,0 +1,114 @@
+//
+// KeyboardGameController.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/14/18.
+// Copyright © 2018 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+public extension GameControllerInputType
+{
+ static let keyboard = GameControllerInputType("keyboard")
+}
+
+extension KeyboardGameController
+{
+ public struct Input: Hashable, RawRepresentable, Codable
+ {
+ public let rawValue: String
+
+ public init(rawValue: String)
+ {
+ self.rawValue = rawValue
+ }
+
+ public init(_ rawValue: String)
+ {
+ self.rawValue = rawValue
+ }
+ }
+}
+
+extension KeyboardGameController.Input: Input
+{
+ public var type: InputType {
+ return .controller(.keyboard)
+ }
+
+ public init(stringValue: String)
+ {
+ self.init(rawValue: stringValue)
+ }
+}
+
+public extension KeyboardGameController.Input
+{
+ static let up = KeyboardGameController.Input("up")
+ static let down = KeyboardGameController.Input("down")
+ static let left = KeyboardGameController.Input("left")
+ static let right = KeyboardGameController.Input("right")
+
+ static let escape = KeyboardGameController.Input("escape")
+
+ static let shift = KeyboardGameController.Input("shift")
+ static let command = KeyboardGameController.Input("command")
+ static let option = KeyboardGameController.Input("option")
+ static let control = KeyboardGameController.Input("control")
+ static let capsLock = KeyboardGameController.Input("capsLock")
+
+ static let space = KeyboardGameController.Input("space")
+ static let `return` = KeyboardGameController.Input("return")
+ static let tab = KeyboardGameController.Input("tab")
+}
+
+public class KeyboardGameController: UIResponder, GameController
+{
+ public var name: String {
+ return NSLocalizedString("Keyboard", comment: "")
+ }
+
+ public var playerIndex: Int?
+
+ public let inputType: GameControllerInputType = .keyboard
+
+ public private(set) lazy var defaultInputMapping: GameControllerInputMappingProtocol? = {
+ guard let fileURL = Bundle.resources.url(forResource: "KeyboardGameController", withExtension: "deltamapping") else {
+ fatalError("KeyboardGameController.deltamapping does not exist.")
+ }
+
+ do
+ {
+ let inputMapping = try GameControllerInputMapping(fileURL: fileURL)
+ return inputMapping
+ }
+ catch
+ {
+ print(error)
+
+ fatalError("KeyboardGameController.deltamapping does not exist.")
+ }
+ }()
+}
+
+public extension KeyboardGameController
+{
+ override func keyPressesBegan(_ presses: Set, with event: UIEvent)
+ {
+ for press in presses
+ {
+ let input = Input(press.key)
+ self.activate(input)
+ }
+ }
+
+ override func keyPressesEnded(_ presses: Set, with event: UIEvent)
+ {
+ for press in presses
+ {
+ let input = Input(press.key)
+ self.deactivate(input)
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Game Controllers/Keyboard/KeyboardResponder.swift b/Cores/DeltaCore/Sources/Game Controllers/Keyboard/KeyboardResponder.swift
new file mode 100644
index 000000000..d38b2270b
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Game Controllers/Keyboard/KeyboardResponder.swift
@@ -0,0 +1,169 @@
+//
+// KeyboardResponder.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/14/18.
+// Copyright © 2018 Riley Testut. All rights reserved.
+//
+
+import UIKit
+import ObjectiveC
+
+public extension UIResponder
+{
+ @objc func keyPressesBegan(_ presses: Set, with event: UIEvent)
+ {
+ self.next?.keyPressesBegan(presses, with: event)
+ }
+
+ @objc func keyPressesEnded(_ presses: Set, with event: UIEvent)
+ {
+ self.next?.keyPressesEnded(presses, with: event)
+ }
+}
+
+private extension UIResponder
+{
+ @objc(_keyCommandForEvent:target:)
+ @NSManaged func _keyCommand(for event: UIEvent, target: UnsafeMutablePointer) -> UIKeyCommand?
+}
+
+@objc public class KeyPress: NSObject
+{
+ public fileprivate(set) var key: String
+ public fileprivate(set) var keyCode: Int
+
+ public fileprivate(set) var modifierFlags: UIKeyModifierFlags
+
+ public fileprivate(set) var isActive: Bool = true
+
+ fileprivate init(key: String, keyCode: Int, modifierFlags: UIKeyModifierFlags)
+ {
+ self.key = key
+ self.keyCode = keyCode
+ self.modifierFlags = modifierFlags
+ }
+}
+
+public class KeyboardResponder: UIResponder
+{
+ private let _nextResponder: UIResponder?
+
+ public override var next: UIResponder? {
+ return self._nextResponder
+ }
+
+ // Use KeyPress.keyCode as dictionary key because KeyPress.key may be invalid for keyUp events.
+ private static var activeKeyPresses = [Int: KeyPress]()
+ private static var activeModifierFlags = UIKeyModifierFlags(rawValue: 0)
+
+ public init(nextResponder: UIResponder?)
+ {
+ self._nextResponder = nextResponder
+ }
+}
+
+private extension KeyboardResponder
+{
+ // Implementation based on Steve Troughton-Smith's gist: https://gist.github.com/steventroughtonsmith/7515380
+ override func _keyCommand(for event: UIEvent, target: UnsafeMutablePointer) -> UIKeyCommand?
+ {
+ // Retrieve information from event.
+ guard
+ let key = event.value(forKey: "_unmodifiedInput") as? String,
+ let keyCode = event.value(forKey: "_keyCode") as? Int,
+ let rawModifierFlags = event.value(forKey: "_modifierFlags") as? Int,
+ let isActive = event.value(forKey: "_isKeyDown") as? Bool
+ else { return nil }
+
+ let modifierFlags = UIKeyModifierFlags(rawValue: rawModifierFlags)
+ defer { KeyboardResponder.activeModifierFlags = modifierFlags }
+
+ let previousKeyPress = KeyboardResponder.activeKeyPresses[keyCode]
+
+ // Ignore key presses that haven't changed activate state to filter out duplicate key press events.
+ guard previousKeyPress?.isActive != isActive else { return nil }
+
+ // Attempt to use previousKeyPress.key because key may be invalid for keyUp events.
+ var pressedKey = previousKeyPress?.key ?? key
+
+ // Check if pressedKey is a whitespace or newline character.
+ if pressedKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ {
+ if pressedKey.isEmpty
+ {
+ if isActive
+ {
+ // Determine the newly activated modifier key.
+ let activatedModifierFlags = modifierFlags.subtracting(KeyboardResponder.activeModifierFlags)
+
+ guard let key = self.key(for: activatedModifierFlags) else { return nil }
+ pressedKey = key
+ }
+ else
+ {
+ // Determine the newly deactivated modifier key.
+ let deactivatedModifierFlags = KeyboardResponder.activeModifierFlags.subtracting(modifierFlags)
+
+ guard let key = self.key(for: deactivatedModifierFlags) else { return nil }
+ pressedKey = key
+ }
+ }
+ else
+ {
+ switch pressedKey
+ {
+ case " ": pressedKey = KeyboardGameController.Input.space.rawValue
+ case "\r", "\n": pressedKey = KeyboardGameController.Input.return.rawValue
+ case "\t": pressedKey = KeyboardGameController.Input.tab.rawValue
+ default: break
+ }
+ }
+ }
+ else
+ {
+ switch pressedKey
+ {
+ case UIKeyCommand.inputUpArrow: pressedKey = KeyboardGameController.Input.up.rawValue
+ case UIKeyCommand.inputDownArrow: pressedKey = KeyboardGameController.Input.down.rawValue
+ case UIKeyCommand.inputLeftArrow: pressedKey = KeyboardGameController.Input.left.rawValue
+ case UIKeyCommand.inputRightArrow: pressedKey = KeyboardGameController.Input.right.rawValue
+ case UIKeyCommand.inputEscape: pressedKey = KeyboardGameController.Input.escape.rawValue
+ default: break
+ }
+ }
+
+ let keyPress = previousKeyPress ?? KeyPress(key: pressedKey, keyCode: keyCode, modifierFlags: modifierFlags)
+ keyPress.isActive = isActive
+
+ if keyPress.isActive
+ {
+ KeyboardResponder.activeKeyPresses[keyCode] = keyPress
+
+ UIResponder.firstResponder?.keyPressesBegan([keyPress], with: event)
+ ExternalGameControllerManager.shared.keyPressesBegan([keyPress], with: event)
+ }
+ else
+ {
+ UIResponder.firstResponder?.keyPressesEnded([keyPress], with: event)
+ ExternalGameControllerManager.shared.keyPressesEnded([keyPress], with: event)
+
+ KeyboardResponder.activeKeyPresses[keyCode] = nil
+ }
+
+ return nil
+ }
+
+ func key(for modifierFlags: UIKeyModifierFlags) -> String?
+ {
+ switch modifierFlags
+ {
+ case .shift: return KeyboardGameController.Input.shift.rawValue
+ case .control: return KeyboardGameController.Input.control.rawValue
+ case .alternate: return KeyboardGameController.Input.option.rawValue
+ case .command: return KeyboardGameController.Input.command.rawValue
+ case .alphaShift: return KeyboardGameController.Input.capsLock.rawValue
+ default: return nil
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Game Controllers/MFi/MFiGameController.swift b/Cores/DeltaCore/Sources/Game Controllers/MFi/MFiGameController.swift
new file mode 100644
index 000000000..b95cb7017
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Game Controllers/MFi/MFiGameController.swift
@@ -0,0 +1,195 @@
+//
+// MFiGameController.swift
+// DeltaCore
+//
+// Created by Riley Testut on 7/22/15.
+// Copyright © 2015 Riley Testut. All rights reserved.
+//
+
+import GameController
+
+public extension GameControllerInputType
+{
+ static let mfi = GameControllerInputType("mfi")
+}
+
+extension MFiGameController
+{
+ public enum Input: String, Codable
+ {
+ case menu
+
+ case up
+ case down
+ case left
+ case right
+
+ case leftThumbstickUp
+ case leftThumbstickDown
+ case leftThumbstickLeft
+ case leftThumbstickRight
+
+ case rightThumbstickUp
+ case rightThumbstickDown
+ case rightThumbstickLeft
+ case rightThumbstickRight
+
+ case a
+ case b
+ case x
+ case y
+
+ case leftShoulder
+ case leftTrigger
+
+ case rightShoulder
+ case rightTrigger
+ }
+}
+
+extension MFiGameController.Input: Input
+{
+ public var type: InputType {
+ return .controller(.mfi)
+ }
+
+ public var isContinuous: Bool {
+ switch self
+ {
+ case .leftThumbstickUp, .leftThumbstickDown, .leftThumbstickLeft, .leftThumbstickRight: return true
+ case .rightThumbstickUp, .rightThumbstickDown, .rightThumbstickLeft, .rightThumbstickRight: return true
+ default: return false
+ }
+ }
+}
+
+public class MFiGameController: NSObject, GameController
+{
+ //MARK: - Properties -
+ /** Properties **/
+ public let controller: GCController
+
+ public var name: String {
+ return self.controller.vendorName ?? NSLocalizedString("MFi Controller", comment: "")
+ }
+
+ public var playerIndex: Int? {
+ get {
+ switch self.controller.playerIndex
+ {
+ case .indexUnset: return nil
+ case .index1: return 0
+ case .index2: return 1
+ case .index3: return 2
+ case .index4: return 3
+ @unknown default: return nil
+ }
+ }
+ set {
+ switch newValue
+ {
+ case .some(0): self.controller.playerIndex = .index1
+ case .some(1): self.controller.playerIndex = .index2
+ case .some(2): self.controller.playerIndex = .index3
+ case .some(3): self.controller.playerIndex = .index4
+ default: self.controller.playerIndex = .indexUnset
+ }
+ }
+ }
+
+ public let inputType: GameControllerInputType = .mfi
+
+ public private(set) lazy var defaultInputMapping: GameControllerInputMappingProtocol? = {
+ guard let fileURL = Bundle.resources.url(forResource: "MFiGameController", withExtension: "deltamapping") else {
+ fatalError("MFiGameController.deltamapping does not exist.")
+ }
+
+ do
+ {
+ let inputMapping = try GameControllerInputMapping(fileURL: fileURL)
+ return inputMapping
+ }
+ catch
+ {
+ print(error)
+ fatalError("MFiGameController.deltamapping does not exist.")
+ }
+ }()
+
+ //MARK: - Initializers -
+ /** Initializers **/
+ public init(controller: GCController)
+ {
+ self.controller = controller
+
+ super.init()
+
+ self.controller.controllerPausedHandler = { [unowned self] controller in
+ self.activate(Input.menu)
+ self.deactivate(Input.menu)
+ }
+
+ let inputChangedHandler: (_ input: MFiGameController.Input, _ pressed: Bool) -> Void = { [unowned self] (input, pressed) in
+ if pressed
+ {
+ self.activate(input)
+ }
+ else
+ {
+ self.deactivate(input)
+ }
+ }
+
+ let thumbstickChangedHandler: (_ input1: MFiGameController.Input, _ input2: MFiGameController.Input, _ value: Float) -> Void = { [unowned self] (input1, input2, value) in
+
+ switch value
+ {
+ case ..<0:
+ self.activate(input1, value: Double(-value))
+ self.deactivate(input2)
+
+ case 0:
+ self.deactivate(input1)
+ self.deactivate(input2)
+
+ default:
+ self.deactivate(input1)
+ self.activate(input2, value: Double(value))
+ }
+ }
+
+ if let gamepad = self.controller.gamepad
+ {
+ gamepad.buttonA.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.a, pressed) }
+ gamepad.buttonB.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.b, pressed) }
+ gamepad.buttonX.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.x, pressed) }
+ gamepad.buttonY.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.y, pressed) }
+ gamepad.leftShoulder.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.leftShoulder, pressed) }
+ gamepad.rightShoulder.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.rightShoulder, pressed) }
+
+ gamepad.dpad.up.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.up, pressed) }
+ gamepad.dpad.down.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.down, pressed) }
+ gamepad.dpad.left.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.left, pressed) }
+ gamepad.dpad.right.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.right, pressed) }
+ }
+
+ if let extendedGamepad = self.controller.extendedGamepad
+ {
+ extendedGamepad.leftTrigger.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.leftTrigger, pressed) }
+ extendedGamepad.rightTrigger.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.rightTrigger, pressed) }
+
+ extendedGamepad.leftThumbstick.xAxis.valueChangedHandler = { (axis, value) in
+ thumbstickChangedHandler(.leftThumbstickLeft, .leftThumbstickRight, value)
+ }
+ extendedGamepad.leftThumbstick.yAxis.valueChangedHandler = { (axis, value) in
+ thumbstickChangedHandler(.leftThumbstickDown, .leftThumbstickUp, value)
+ }
+ extendedGamepad.rightThumbstick.xAxis.valueChangedHandler = { (axis, value) in
+ thumbstickChangedHandler(.rightThumbstickLeft, .rightThumbstickRight, value)
+ }
+ extendedGamepad.rightThumbstick.yAxis.valueChangedHandler = { (axis, value) in
+ thumbstickChangedHandler(.rightThumbstickDown, .rightThumbstickUp, value)
+ }
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Model/Cheat.swift b/Cores/DeltaCore/Sources/Model/Cheat.swift
new file mode 100644
index 000000000..7171a60b9
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Model/Cheat.swift
@@ -0,0 +1,21 @@
+//
+// Cheat.swift
+// DeltaCore
+//
+// Created by Riley Testut on 5/19/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public struct Cheat: CheatProtocol
+{
+ public var code: String
+ public var type: CheatType
+
+ public init(code: String, type: CheatType)
+ {
+ self.code = code
+ self.type = type
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Model/CheatFormat.swift b/Cores/DeltaCore/Sources/Model/CheatFormat.swift
new file mode 100644
index 000000000..fde970e4a
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Model/CheatFormat.swift
@@ -0,0 +1,108 @@
+//
+// CheatFormat.swift
+// DeltaCore
+//
+// Created by Riley Testut on 5/22/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public struct CheatFormat: Hashable
+{
+ public let name: String
+
+ // Must begin and end with an alphanumberic character. Besides these, non-alphanumberic characters will be treated as special formatting characters.
+ // Ex: XXXX-YYYY. The "-" is a special formatting character, and should be automatically inserted between alphanumeric characters by a code editor
+ public let format: String
+
+ public let type: CheatType
+
+ public let allowedCodeCharacters: CharacterSet
+
+ public init(name: String, format: String, type: CheatType, allowedCodeCharacters: CharacterSet = CharacterSet.hexadecimals)
+ {
+ self.name = name
+ self.format = format
+ self.type = type
+ self.allowedCodeCharacters = allowedCodeCharacters
+ }
+}
+
+public extension String
+{
+ func sanitized(with characterSet: CharacterSet) -> String
+ {
+ let sanitizedString = (self as NSString).components(separatedBy: characterSet.inverted).joined(separator: "")
+ return sanitizedString
+ }
+
+ func formatted(with cheatFormat: CheatFormat) -> String
+ {
+ // NOTE: We do not use cheatFormat.allowedCodeCharacters because the code format typically includes non-legal characters.
+ // Ex: Using "XXXX-YYYY" for the code format despite the actual code format being strictly hexadecial characters.
+ // This is okay because this function's job is not to validate the input, but simply to format it
+ let characterSet = CharacterSet.alphanumerics
+
+ // Remove all characters not in characterSet
+ let sanitizedFormat = cheatFormat.format.sanitized(with: characterSet)
+
+ // We need to repeat the format enough times so it is greater than or equal to the length of self
+ // This prevents us from having to account for wrapping around the cheat format
+ let repetitions = Int(ceil((Float(self.count) / Float(sanitizedFormat.count))))
+
+ var format = ""
+ for i in 0 ..< repetitions
+ {
+ if i > 0
+ {
+ format += "\n"
+ }
+
+ format += cheatFormat.format
+ }
+
+
+ var formattedString = ""
+
+ // Serves as a sort of stack buffer for us to draw characters from
+ let codeBuffer = NSMutableString(string: self)
+
+ let scanner = Scanner(string: format)
+ scanner.charactersToBeSkipped = nil
+
+ while !scanner.isAtEnd
+ {
+ // Scan up until the first separator character
+ var string: NSString? = nil
+ scanner.scanCharacters(from: characterSet, into: &string)
+
+ // Might start with separator characters, in which case scannedString would be nil/empty
+ if let scannedString = string, scannedString.length > 0
+ {
+ let range = NSRange(location: 0, length: min(scannedString.length, codeBuffer.length))
+
+ // "Pop off" characters from the front of codeBuffer
+ let code = codeBuffer.substring(with: range)
+ codeBuffer.replaceCharacters(in: range, with: "")
+
+ formattedString += code
+
+ // No characters left in buffer means we've finished formatting
+ guard codeBuffer.length > 0 else { break }
+ }
+
+ // Scan all separator characters
+ var separatorString: NSString? = nil
+ scanner.scanUpToCharacters(from: characterSet, into: &separatorString)
+
+ // If no separator characters, we're done!
+ guard let tempString = separatorString as String?, separatorString?.length ?? 0 > 0 else { break }
+
+ formattedString += tempString
+ }
+
+ // Ensure it is all uppercase
+ return formattedString.uppercased()
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Model/ControllerSkin.swift b/Cores/DeltaCore/Sources/Model/ControllerSkin.swift
new file mode 100644
index 000000000..58a722cb3
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Model/ControllerSkin.swift
@@ -0,0 +1,905 @@
+//
+// ControllerSkin.swift
+// DeltaCore
+//
+// Created by Riley Testut on 5/5/15.
+// Copyright © 2015 Riley Testut. All rights reserved.
+//
+
+import UIKit
+import ZIPFoundation
+
+public let kUTTypeDeltaControllerSkin: CFString = "com.rileytestut.delta.skin" as CFString
+
+private typealias RepresentationDictionary = [String: [String: AnyObject]]
+
+public extension GameControllerInputType
+{
+ static let controllerSkin = GameControllerInputType("controllerSkin")
+}
+
+private extension Archive
+{
+ func extract(_ entry: Entry) throws -> Data
+ {
+ var data = Data()
+ _ = try self.extract(entry) { data.append($0) }
+
+ return data
+ }
+}
+
+extension ControllerSkin
+{
+ public struct Screen
+ {
+ public var inputFrame: CGRect?
+ public var outputFrame: CGRect
+
+ public var filters: [CIFilter]?
+ }
+}
+
+public struct ControllerSkin: ControllerSkinProtocol
+{
+ public let name: String
+ public let identifier: String
+ public let gameType: GameType
+ public let isDebugModeEnabled: Bool
+
+ public let fileURL: URL
+
+ private let representations: [Traits: Representation]
+ private let imageCache = NSCache()
+
+ private let archive: Archive
+
+ public init?(fileURL: URL)
+ {
+ self.fileURL = fileURL
+
+ guard let archive = Archive(url: fileURL, accessMode: .read) else { return nil }
+ self.archive = archive
+
+ guard let infoEntry = archive["info.json"] else { return nil }
+
+ do
+ {
+ let infoData = try archive.extract(infoEntry)
+
+ guard let info = try JSONSerialization.jsonObject(with: infoData) as? [String: AnyObject] else { return nil }
+
+ guard
+ let name = info["name"] as? String,
+ let identifier = info["identifier"] as? String,
+ let isDebugModeEnabled = info["debug"] as? Bool,
+ let representationsDictionary = info["representations"] as? RepresentationDictionary
+ else { return nil }
+
+ #if FRAMEWORK
+ guard let gameType = info["gameTypeIdentifier"] as? GameType else { return nil }
+ #else
+ guard let gameTypeString = info["gameTypeIdentifier"] as? String else { return nil }
+ let gameType = GameType(gameTypeString)
+ #endif
+
+ self.name = name
+ self.identifier = identifier
+ self.gameType = gameType
+ self.isDebugModeEnabled = isDebugModeEnabled
+
+ let representationsSet = ControllerSkin.parsedRepresentations(from: representationsDictionary)
+
+ var representations = [Traits: Representation]()
+ for representation in representationsSet
+ {
+ representations[representation.traits] = representation
+ }
+ self.representations = representations
+
+ guard self.representations.count > 0 else { return nil }
+ }
+ catch let error as NSError
+ {
+ print("\(error) \(error.userInfo)")
+
+ return nil
+ }
+ }
+
+ // Sometimes, recursion really is the best solution ¯\_(ツ)_/¯
+ private static func parsedRepresentations(from representationsDictionary: RepresentationDictionary, device: Device? = nil, displayType: DisplayType? = nil, orientation: Orientation? = nil) -> Set
+ {
+ var representations = Set()
+
+ for (key, dictionary) in representationsDictionary
+ {
+ if device == nil
+ {
+ guard let device = Device(rawValue: key), let dictionary = dictionary as? RepresentationDictionary else { continue }
+
+ representations.formUnion(self.parsedRepresentations(from: dictionary, device: device))
+ }
+ else if displayType == nil
+ {
+ if let displayType = DisplayType(rawValue: key), let dictionary = dictionary as? RepresentationDictionary
+ {
+ representations.formUnion(self.parsedRepresentations(from: dictionary, device: device, displayType: displayType))
+ }
+ else
+ {
+ // Key doesn't exist, so we continue with the same dictionary we're currently iterating, but pass in .standard for displayMode
+ representations.formUnion(self.parsedRepresentations(from: representationsDictionary, device: device, displayType: .standard))
+
+ // Return early to prevent us from repeating the above step multiple times
+ return representations
+ }
+ }
+ else if orientation == nil
+ {
+ guard
+ let device = device,
+ let displayType = displayType,
+ let orientation = Orientation(rawValue: key)
+ else { continue }
+
+ let traits = Traits(device: device, displayType: displayType, orientation: orientation)
+ if let representation = Representation(traits: traits, dictionary: dictionary)
+ {
+ representations.insert(representation)
+ }
+ }
+ }
+
+ return representations
+ }
+}
+
+public extension ControllerSkin
+{
+ static func standardControllerSkin(for gameType: GameType) -> ControllerSkin?
+ {
+ guard
+ let deltaCore = Delta.core(for: gameType),
+ let fileURL = deltaCore.resourceBundle.url(forResource: "Standard", withExtension: "deltaskin")
+ else { return nil }
+
+ let controllerSkin = ControllerSkin(fileURL: fileURL)
+ return controllerSkin
+ }
+}
+
+public extension ControllerSkin
+{
+ func supports(_ traits: Traits) -> Bool
+ {
+ let representation = self.representations[traits]
+ return representation != nil
+ }
+
+ func thumbstick(for item: ControllerSkin.Item, traits: Traits, preferredSize: Size) -> (UIImage, CGSize)?
+ {
+ guard let representation = self.representation(for: traits) else { return nil }
+ guard let imageName = item.thumbstickImageName, let size = item.thumbstickSize else { return nil }
+ guard let entry = self.archive[imageName] else { return nil }
+
+ let cacheKey = imageName + self.cacheKey(for: traits, size: preferredSize)
+
+ if let image = self.imageCache.object(forKey: cacheKey as NSString)
+ {
+ return (image, size)
+ }
+
+ let thumbstickImage: UIImage?
+
+ do
+ {
+ let data = try self.archive.extract(entry)
+
+ switch (imageName as NSString).pathExtension.lowercased()
+ {
+ case "pdf":
+ let assetSize = AssetSize(size: preferredSize)
+ guard let targetSize = assetSize.targetSize(for: representation.traits) else { return nil }
+
+ let thumbstickSize = CGSize(width: size.width * targetSize.width, height: size.height * targetSize.height)
+ thumbstickImage = UIImage.image(withPDFData: data, targetSize: thumbstickSize)
+
+ default:
+ thumbstickImage = UIImage(data: data, scale: 1.0)
+ }
+ }
+ catch
+ {
+ print(error)
+
+ return nil
+ }
+
+ guard let image = thumbstickImage else { return nil }
+
+ self.imageCache.setObject(image, forKey: cacheKey as NSString)
+
+ return (image, size)
+ }
+
+ func image(for traits: Traits, preferredSize: Size) -> UIImage?
+ {
+ guard let representation = self.representation(for: traits) else { return nil }
+
+ let cacheKey = self.cacheKey(for: traits, size: preferredSize)
+
+ if let image = self.imageCache.object(forKey: cacheKey as NSString)
+ {
+ return image
+ }
+
+ var returnedImage: UIImage? = nil
+
+ switch preferredSize
+ {
+ case .small:
+ if let image = self.image(for: representation, assetSize: AssetSize(size: .small)) { returnedImage = image }
+ else if let image = self.image(for: representation, assetSize: AssetSize(size: .small, resizable: true)) { returnedImage = image }
+ else if let image = self.image(for: representation, assetSize: AssetSize(size: .medium)) { returnedImage = image }
+ else if let image = self.image(for: representation, assetSize: AssetSize(size: .large)) { returnedImage = image }
+
+ case .medium:
+ // First, attempt to load a medium image
+ if let image = self.image(for: representation, assetSize: AssetSize(size: .medium)) { returnedImage = image }
+
+ // If a medium image doesn't exist, fallback to trying to load a medium resizable image
+ else if let image = self.image(for: representation, assetSize: AssetSize(size: .medium, resizable: true)) { returnedImage = image }
+
+ // If neither medium nor resizable exists, check for a large image (because downscaling large is better than upscaling small)
+ else if let image = self.image(for: representation, assetSize: AssetSize(size: .large)) { returnedImage = image }
+
+ // If still no images exist, finally check the small image size
+ else if let image = self.image(for: representation, assetSize: AssetSize(size: .small)) { returnedImage = image }
+
+ case .large:
+ if let image = self.image(for: representation, assetSize: AssetSize(size: .large)) { returnedImage = image }
+ else if let image = self.image(for: representation, assetSize: AssetSize(size: .large, resizable: true)) { returnedImage = image }
+ else if let image = self.image(for: representation, assetSize: AssetSize(size: .medium)) { returnedImage = image }
+ else if let image = self.image(for: representation, assetSize: AssetSize(size: .small)) { returnedImage = image }
+
+ }
+
+ if let image = returnedImage
+ {
+ self.imageCache.setObject(image, forKey: cacheKey as NSString)
+ }
+
+ return returnedImage
+ }
+
+ func inputs(for traits: Traits, at point: CGPoint) -> [Input]?
+ {
+ guard let representation = self.representation(for: traits) else { return nil }
+
+ var inputs: [Input] = []
+
+ for item in representation.items
+ {
+ guard item.extendedFrame.contains(point) else { continue }
+
+ switch item.inputs
+ {
+ // Don't return inputs for thumbsticks or touch screens since they're handled separately.
+ case .directional where item.kind == .thumbstick: break
+ case .touch: break
+
+ case .standard(let itemInputs):
+ inputs.append(contentsOf: itemInputs)
+
+ case let .directional(up, down, left, right):
+
+ let divisor: CGFloat
+ if case .thumbstick = item.kind
+ {
+ divisor = 2.0
+ }
+ else
+ {
+ divisor = 3.0
+ }
+
+ let topRect = CGRect(x: item.extendedFrame.minX, y: item.extendedFrame.minY, width: item.extendedFrame.width, height: (item.frame.height / divisor) + (item.frame.minY - item.extendedFrame.minY))
+ let bottomRect = CGRect(x: item.extendedFrame.minX, y: item.frame.maxY - item.frame.height / divisor, width: item.extendedFrame.width, height: (item.frame.height / divisor) + (item.extendedFrame.maxY - item.frame.maxY))
+ let leftRect = CGRect(x: item.extendedFrame.minX, y: item.extendedFrame.minY, width: (item.frame.width / divisor) + (item.frame.minX - item.extendedFrame.minX), height: item.extendedFrame.height)
+ let rightRect = CGRect(x: item.frame.maxX - item.frame.width / divisor, y: item.extendedFrame.minY, width: (item.frame.width / divisor) + (item.extendedFrame.maxX - item.frame.maxX), height: item.extendedFrame.height)
+
+ if topRect.contains(point)
+ {
+ inputs.append(up)
+ }
+
+ if bottomRect.contains(point)
+ {
+ inputs.append(down)
+ }
+
+ if leftRect.contains(point)
+ {
+ inputs.append(left)
+ }
+
+ if rightRect.contains(point)
+ {
+ inputs.append(right)
+ }
+ }
+ }
+
+ return inputs
+ }
+
+ func items(for traits: Traits) -> [Item]?
+ {
+ guard let representation = self.representation(for: traits) else { return nil }
+ return representation.items
+ }
+
+ func isTranslucent(for traits: Traits) -> Bool?
+ {
+ guard let representation = self.representation(for: traits) else { return nil }
+ return representation.isTranslucent
+ }
+
+ func gameScreenFrame(for traits: Traits) -> CGRect?
+ {
+ guard let representation = self.representation(for: traits) else { return nil }
+ return representation.screens?.first?.outputFrame
+ }
+
+ func screens(for traits: Traits) -> [ControllerSkin.Screen]?
+ {
+ guard let representation = self.representation(for: traits) else { return nil }
+ return representation.screens
+ }
+
+ func aspectRatio(for traits: ControllerSkin.Traits) -> CGSize?
+ {
+ guard let representation = self.representation(for: traits) else { return nil }
+ return representation.aspectRatio
+ }
+}
+
+private extension ControllerSkin
+{
+ func image(for representation: Representation, assetSize: AssetSize) -> UIImage?
+ {
+ guard let filename = representation.assets[assetSize], let entry = self.archive[filename] else { return nil }
+
+ do
+ {
+ let data = try self.archive.extract(entry)
+
+ let image: UIImage?
+
+ switch assetSize
+ {
+ case .small, .medium, .large:
+ guard let imageScale = assetSize.imageScale(for: representation.traits) else { return nil }
+ image = UIImage(data: data, scale: imageScale)
+
+ case .resizable:
+ guard let targetSize = assetSize.targetSize(for: representation.traits) else { return nil }
+ image = UIImage.image(withPDFData: data, targetSize: targetSize)
+ }
+
+ return image
+ }
+ catch
+ {
+ print(error)
+
+ return nil
+ }
+ }
+
+ func cacheKey(for traits: Traits, size: Size) -> String
+ {
+ return String(describing: traits) + "-" + String(describing: size)
+ }
+
+ func representation(for traits: Traits) -> Representation?
+ {
+ let representation = self.representations[traits]
+ guard representation == nil else {
+ return representation
+ }
+
+ guard let fallbackTraits = self.supportedTraits(for: traits) else {
+ return nil
+ }
+
+ let fallbackRepresentation = self.representations[fallbackTraits]
+ return fallbackRepresentation
+ }
+}
+
+extension ControllerSkin
+{
+ public struct Item
+ {
+ public enum Kind: Equatable
+ {
+ case button
+ case dPad
+ case thumbstick
+ case touchScreen
+ }
+
+ public enum Inputs
+ {
+ case standard([Input])
+ case directional(up: Input, down: Input, left: Input, right: Input)
+ case touch(x: Input, y: Input)
+
+ public var allInputs: [Input] {
+ switch self
+ {
+ case .standard(let inputs): return inputs
+ case let .directional(up, down, left, right): return [up, down, left, right]
+ case let .touch(x, y): return [x, y]
+ }
+ }
+ }
+
+ public var kind: Kind
+ public var inputs: Inputs
+
+ public var frame: CGRect
+ public var extendedFrame: CGRect
+
+ fileprivate var thumbstickImageName: String?
+ fileprivate var thumbstickSize: CGSize?
+
+ fileprivate init?(dictionary: [String: AnyObject], extendedEdges: ExtendedEdges, mappingSize: CGSize)
+ {
+ guard
+ let frameDictionary = dictionary["frame"] as? [String: CGFloat], let frame = CGRect(dictionary: frameDictionary)
+ else { return nil }
+
+ if let inputs = dictionary["inputs"] as? [String]
+ {
+ self.kind = .button
+ self.inputs = .standard(inputs.map { AnyInput(stringValue: $0, intValue: nil, type: .controller(.controllerSkin)) })
+ }
+ else if let inputs = dictionary["inputs"] as? [String: String]
+ {
+ if let up = inputs["up"], let down = inputs["down"], let left = inputs["left"], let right = inputs["right"]
+ {
+ let isContinuous: Bool
+
+ if
+ let thumbstickDictionary = dictionary["thumbstick"] as? [String: Any],
+ let imageName = thumbstickDictionary["name"] as? String,
+ let width = thumbstickDictionary["width"] as? CGFloat,
+ let height = thumbstickDictionary["height"] as? CGFloat
+ {
+ self.thumbstickImageName = imageName
+ self.thumbstickSize = CGSize(width: CGFloat(width) / mappingSize.width, height: CGFloat(height) / mappingSize.height)
+
+ self.kind = .thumbstick
+ isContinuous = true
+ }
+ else
+ {
+ self.kind = .dPad
+ isContinuous = false
+ }
+
+ self.inputs = .directional(up: AnyInput(stringValue: up, intValue: nil, type: .controller(.controllerSkin), isContinuous: isContinuous),
+ down: AnyInput(stringValue: down, intValue: nil, type: .controller(.controllerSkin), isContinuous: isContinuous),
+ left: AnyInput(stringValue: left, intValue: nil, type: .controller(.controllerSkin), isContinuous: isContinuous),
+ right: AnyInput(stringValue: right, intValue: nil, type: .controller(.controllerSkin), isContinuous: isContinuous))
+ }
+ else if let x = inputs["x"], let y = inputs["y"]
+ {
+ self.kind = .touchScreen
+ self.inputs = .touch(x: AnyInput(stringValue: x, intValue: nil, type: .controller(.controllerSkin), isContinuous: true),
+ y: AnyInput(stringValue: y, intValue: nil, type: .controller(.controllerSkin), isContinuous: true))
+ }
+ else
+ {
+ return nil
+ }
+ }
+ else
+ {
+ return nil
+ }
+
+ let overrideExtendedEdges = ExtendedEdges(dictionary: dictionary["extendedEdges"] as? [String: CGFloat])
+
+ var extendedEdges = extendedEdges
+ extendedEdges.top = overrideExtendedEdges.top ?? extendedEdges.top
+ extendedEdges.bottom = overrideExtendedEdges.bottom ?? extendedEdges.bottom
+ extendedEdges.left = overrideExtendedEdges.left ?? extendedEdges.left
+ extendedEdges.right = overrideExtendedEdges.right ?? extendedEdges.right
+
+ var extendedFrame = frame
+ extendedFrame.origin.x -= extendedEdges.left ?? 0
+ extendedFrame.origin.y -= extendedEdges.top ?? 0
+ extendedFrame.size.width += (extendedEdges.left ?? 0) + (extendedEdges.right ?? 0)
+ extendedFrame.size.height += (extendedEdges.top ?? 0) + (extendedEdges.bottom ?? 0)
+
+ // Convert frames to relative values.
+ let scaleTransform = CGAffineTransform(scaleX: 1.0 / mappingSize.width, y: 1.0 / mappingSize.height)
+ self.frame = frame.applying(scaleTransform)
+ self.extendedFrame = extendedFrame.applying(scaleTransform)
+ }
+ }
+}
+
+extension ControllerSkin.Item: Hashable
+{
+ public static func ==(lhs: ControllerSkin.Item, rhs: ControllerSkin.Item) -> Bool
+ {
+ guard
+ lhs.kind == rhs.kind,
+ lhs.thumbstickImageName == rhs.thumbstickImageName, lhs.thumbstickSize == rhs.thumbstickSize,
+ lhs.inputs.allInputs.map({ $0.stringValue }) == rhs.inputs.allInputs.map({ $0.stringValue }),
+ lhs.frame == rhs.frame && lhs.extendedFrame == rhs.extendedFrame
+ else { return false }
+
+ return true
+ }
+
+ public func hash(into hasher: inout Hasher)
+ {
+ switch self.kind
+ {
+ case .button: hasher.combine(0)
+ case .dPad: hasher.combine(1)
+ case .thumbstick: hasher.combine(2)
+ case .touchScreen: hasher.combine(3)
+ }
+
+ hasher.combine(self.thumbstickImageName)
+ hasher.combine(self.thumbstickSize?.width)
+ hasher.combine(self.thumbstickSize?.height)
+
+ for input in self.inputs.allInputs
+ {
+ hasher.combine(input.stringValue)
+ }
+
+ for frame in [self.frame, self.extendedFrame]
+ {
+ hasher.combine(frame.origin.x)
+ hasher.combine(frame.origin.y)
+ hasher.combine(frame.width)
+ hasher.combine(frame.height)
+ }
+ }
+}
+
+private extension ControllerSkin
+{
+ struct ExtendedEdges
+ {
+ var top: CGFloat?
+ var bottom: CGFloat?
+ var left: CGFloat?
+ var right: CGFloat?
+
+ init(dictionary: [String: CGFloat]?)
+ {
+ self.top = dictionary?["top"]
+ self.bottom = dictionary?["bottom"]
+ self.left = dictionary?["left"]
+ self.right = dictionary?["right"]
+ }
+ }
+
+ enum AssetSize: RawRepresentable, Hashable
+ {
+ case small
+ case medium
+ case large
+ indirect case resizable(assetSize: AssetSize?)
+
+ // If we're resizable, return our associated AssetSize
+ // Otherwise, we just return self
+ var unwrapped: AssetSize?
+ {
+ if case .resizable(let size) = self
+ {
+ if let size = size
+ {
+ return size
+ }
+ else
+ {
+ return nil
+ }
+ }
+ else
+ {
+ return self
+ }
+ }
+
+ /// Hashable
+ var hashValue: Int {
+ return self.rawValue.hashValue
+ }
+
+ /// RawRepresentable
+ typealias RawValue = String
+
+ var rawValue: String {
+ switch self
+ {
+ case .small: return "small"
+ case .medium: return "medium"
+ case .large: return "large"
+ case .resizable: return "resizable"
+ }
+ }
+
+ init?(rawValue: String)
+ {
+ switch rawValue
+ {
+ case "small": self = .small
+ case "medium": self = .medium
+ case "large": self = .large
+ case "resizable": self = .resizable(assetSize: nil)
+ default: return nil
+ }
+ }
+
+ init(size: Size, resizable: Bool = false)
+ {
+ switch size
+ {
+ case .small: self = .small
+ case .medium: self = .medium
+ case .large: self = .large
+ }
+
+ if resizable
+ {
+ self = .resizable(assetSize: self)
+ }
+ }
+
+ // Should always be used over the associated value for .resizable because it handles orientation
+ func targetSize(for traits: ControllerSkin.Traits) -> CGSize?
+ {
+ guard let assetSize = self.unwrapped else { return nil }
+
+ var targetSize: CGSize
+
+ switch (traits.device, traits.displayType, assetSize)
+ {
+ case (.iphone, .standard, .small): targetSize = CGSize(width: 320, height: 568)
+ case (.iphone, .standard, .medium): targetSize = CGSize(width: 375, height: 667)
+ case (.iphone, .standard, .large): targetSize = CGSize(width: 414, height: 736)
+
+ case (.iphone, .edgeToEdge, _): targetSize = CGSize(width: 375, height: 812)
+ case (.iphone, .splitView, _): return nil
+
+ case (.ipad, _, .small): targetSize = CGSize(width: 768, height: 1024)
+ case (.ipad, _, .medium): targetSize = CGSize(width: 834, height: 1112)
+ case (.ipad, _, .large): targetSize = CGSize(width: 1024, height: 1366)
+
+ case (_, _, .resizable): return nil
+ }
+
+ switch traits.orientation
+ {
+ case .portrait: break
+ case .landscape: targetSize = CGSize(width: targetSize.height, height: targetSize.width)
+ }
+
+ return targetSize
+ }
+
+ func imageScale(for traits: ControllerSkin.Traits) -> CGFloat?
+ {
+ guard let assetSize = self.unwrapped else { return nil }
+
+ switch (traits.device, traits.displayType, assetSize)
+ {
+ case (.iphone, .standard, .small): return 2.0
+ case (.iphone, .standard, .medium): return 2.0
+ case (.iphone, .standard, .large): return 3.0
+
+ case (.iphone, .edgeToEdge, _): return 3.0
+ case (.iphone, .splitView, _): return nil
+
+ case (.ipad, .standard, _): return 2.0
+ case (.ipad, .edgeToEdge, _): return nil
+ case (.ipad, .splitView, _): return 2.0
+
+ case (_, _, .resizable): return nil
+ }
+ }
+ }
+
+ struct Representation: Hashable, CustomStringConvertible
+ {
+ let traits: Traits
+
+ let assets: [AssetSize: String]
+ let isTranslucent: Bool
+ let screens: [Screen]?
+ let aspectRatio: CGSize
+
+ let items: [Item]
+
+ /// CustomStringConvertible
+ var description: String {
+ return self.traits.description
+ }
+
+ init?(traits: Traits, dictionary: [String: AnyObject])
+ {
+ guard
+ let mappingSizeDictionary = dictionary["mappingSize"] as? [String: CGFloat], let mappingSize = CGSize(dictionary: mappingSizeDictionary),
+ let itemsArray = dictionary["items"] as? [[String: AnyObject]],
+ let assetsDictionary = dictionary["assets"] as? [String: String]
+ else { return nil }
+
+ self.aspectRatio = mappingSize
+
+ self.traits = traits
+
+ let extendedEdges = ExtendedEdges(dictionary: dictionary["extendedEdges"] as? [String: CGFloat])
+
+ var items = [Item]()
+ for dictionary in itemsArray
+ {
+ if let item = Item(dictionary: dictionary, extendedEdges: extendedEdges, mappingSize: mappingSize)
+ {
+ items.append(item)
+ }
+ }
+ self.items = items
+
+ var assets = [AssetSize: String]()
+ for (key, value) in assetsDictionary
+ {
+ if let size = AssetSize(rawValue: key)
+ {
+ assets[size] = value
+ }
+ }
+ self.assets = assets
+
+ guard self.assets.count > 0 else { return nil }
+
+ self.isTranslucent = dictionary["translucent"] as? Bool ?? false
+
+ if
+ let gameScreenFrameDictionary = dictionary["gameScreenFrame"] as? [String: CGFloat],
+ let gameScreenFrame = CGRect(dictionary: gameScreenFrameDictionary)
+ {
+ let scaleTransform = CGAffineTransform(scaleX: 1.0 / mappingSize.width, y: 1.0 / mappingSize.height)
+ let frame = gameScreenFrame.applying(scaleTransform)
+
+ self.screens = [Screen(inputFrame: nil, outputFrame: frame)]
+ }
+ else if let screensArray = dictionary["screens"] as? [[String: Any]]
+ {
+ let scaleTransform = CGAffineTransform(scaleX: 1.0 / mappingSize.width, y: 1.0 / mappingSize.height)
+
+ let screens = screensArray.compactMap { (screenDictionary) -> Screen? in
+ guard
+ let outputFrameDictionary = screenDictionary["outputFrame"] as? [String: CGFloat],
+ let outputFrame = CGRect(dictionary: outputFrameDictionary)
+ else { return nil }
+
+ let normalizedOutputFrame = outputFrame.applying(scaleTransform)
+
+ var inputFrame: CGRect?
+ if let dictionary = screenDictionary["inputFrame"] as? [String: CGFloat], let frame = CGRect(dictionary: dictionary)
+ {
+ inputFrame = frame
+ }
+
+ var filters: [CIFilter]?
+ if let filtersArray = screenDictionary["filters"] as? [[String: Any]]
+ {
+ filters = filtersArray.compactMap { (dictionary) -> CIFilter? in
+ guard let name = dictionary["name"] as? String else { return nil }
+ let parameters = dictionary["parameters"] as? [String: Any]
+
+ guard let filter = CIFilter(name: name) else { return nil }
+
+ var filterParameters = [String: Any]()
+
+ for (parameter, value) in parameters ?? [:]
+ {
+ guard let attribute = filter.attributes[parameter] as? [String: Any] else { continue }
+ guard let className = attribute[kCIAttributeClass] as? String else { continue }
+ guard let attributeType = attribute[kCIAttributeType] as? String else { continue }
+
+ let mappedValue: Any
+
+ switch (className, value)
+ {
+ case (NSStringFromClass(NSNumber.self), let value as NSNumber):
+ mappedValue = value
+
+ case (NSStringFromClass(CIVector.self), let value as [String: CGFloat]):
+ guard let x = value["x"], let y = value["y"] else { continue }
+
+ if let width = value["width"], let height = value["height"]
+ {
+ let vector = CIVector(cgRect: CGRect(x: x, y: y, width: width, height: height))
+ mappedValue = vector
+ }
+ else
+ {
+ let vector = CIVector(x: x, y: y)
+ mappedValue = vector
+ }
+
+ case (NSStringFromClass(CIColor.self), let value as [String: CGFloat]):
+ guard let red = value["r"], let green = value["g"], let blue = value["b"] else { continue }
+
+ let alpha = value["a"] ?? 255.0
+
+ let color = CIColor(red: red / 255.0, green: green / 255.0, blue: blue / 255.0, alpha: alpha / 255.0)
+ mappedValue = color
+
+ case (NSStringFromClass(NSValue.self), let value as [String: CGFloat]) where attributeType == kCIAttributeTypeTransform:
+ let transform: CGAffineTransform
+
+ if let angle = value["rotation"]
+ {
+ let radians = angle * .pi / 180
+ transform = CGAffineTransform.identity.rotated(by: radians)
+ }
+ else
+ {
+ let x = value["scaleX"] ?? 1
+ let y = value["scaleY"] ?? 1
+
+ transform = CGAffineTransform(scaleX: x, y: y)
+ }
+
+ let value = NSValue(cgAffineTransform: transform)
+ mappedValue = value
+
+ default: continue
+ }
+
+ filter.setValue(mappedValue, forKey: parameter)
+ }
+
+ return filter
+ }
+ }
+
+ let screen = Screen(inputFrame: inputFrame, outputFrame: normalizedOutputFrame, filters: filters)
+ return screen
+ }
+
+ self.screens = screens
+ }
+ else
+ {
+ self.screens = nil
+ }
+ }
+
+ /// Equatable
+ static func ==(lhs: ControllerSkin.Representation, rhs: ControllerSkin.Representation) -> Bool
+ {
+ return lhs.traits == rhs.traits
+ }
+
+ /// Hashable
+ func hash(into hasher: inout Hasher)
+ {
+ hasher.combine(self.traits)
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Model/ControllerSkinTraits.swift b/Cores/DeltaCore/Sources/Model/ControllerSkinTraits.swift
new file mode 100644
index 000000000..43fb34c0d
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Model/ControllerSkinTraits.swift
@@ -0,0 +1,94 @@
+//
+// ControllerSkinTraits.swift
+// DeltaCore
+//
+// Created by Riley Testut on 7/4/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+extension ControllerSkin
+{
+ public enum Device: String
+ {
+ // Naming conventions? I treat the "P" as the capital letter, so since it's a value (not a type) I've opted to lowercase it
+ case iphone
+ case ipad
+ }
+
+ public enum DisplayType: String
+ {
+ case standard
+ case edgeToEdge
+ case splitView
+ }
+
+ public enum Orientation: String
+ {
+ case portrait
+ case landscape
+ }
+
+ public enum Size: String
+ {
+ case small
+ case medium
+ case large
+ }
+
+ public struct Traits: Hashable, CustomStringConvertible
+ {
+ public var device: Device
+ public var displayType: DisplayType
+ public var orientation: Orientation
+
+ /// CustomStringConvertible
+ public var description: String {
+ return self.device.rawValue + "-" + self.displayType.rawValue + "-" + self.orientation.rawValue
+ }
+
+ public init(device: Device, displayType: DisplayType, orientation: Orientation)
+ {
+ self.device = device
+ self.displayType = displayType
+ self.orientation = orientation
+ }
+
+ public static func defaults(for window: UIWindow) -> ControllerSkin.Traits
+ {
+ let device: Device
+ let displayType: DisplayType
+ let orientation: Orientation
+
+ // Use trait collection to determine device because our container app may be containing us in an "iPhone" trait collection despite being on iPad
+ // 99% of the time, won't make a difference ¯\_(ツ)_/¯
+ if window.traitCollection.userInterfaceIdiom == .pad
+ {
+ device = .ipad
+
+ if !window.bounds.equalTo(window.screen.bounds)
+ {
+ displayType = .splitView
+
+ // Use screen bounds because in split view window bounds might be portrait, but device is actually landscape (and we want landscape skin)
+ orientation = (window.screen.bounds.width > window.screen.bounds.height) ? .landscape : .portrait
+ }
+ else
+ {
+ displayType = .standard
+ orientation = (window.bounds.width > window.bounds.height) ? .landscape : .portrait
+ }
+ }
+ else
+ {
+ device = .iphone
+ displayType = (window.safeAreaInsets.bottom != 0) ? .edgeToEdge : .standard
+ orientation = (window.bounds.width > window.bounds.height) ? .landscape : .portrait
+ }
+
+ let traits = ControllerSkin.Traits(device: device, displayType: displayType, orientation: orientation)
+ return traits
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Model/Game.swift b/Cores/DeltaCore/Sources/Model/Game.swift
new file mode 100644
index 000000000..4368bddd4
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Model/Game.swift
@@ -0,0 +1,21 @@
+//
+// Game.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/20/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public struct Game: GameProtocol
+{
+ public var fileURL: URL
+ public var type: GameType
+
+ public init(fileURL: URL, type: GameType)
+ {
+ self.fileURL = fileURL
+ self.type = type
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Model/GameControllerInputMapping.swift b/Cores/DeltaCore/Sources/Model/GameControllerInputMapping.swift
new file mode 100644
index 000000000..bb2ed204c
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Model/GameControllerInputMapping.swift
@@ -0,0 +1,72 @@
+//
+// GameControllerInputMapping.swift
+// DeltaCore
+//
+// Created by Riley Testut on 7/22/17.
+// Copyright © 2017 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public struct GameControllerInputMapping: GameControllerInputMappingProtocol, Codable
+{
+ public var name: String?
+ public var gameControllerInputType: GameControllerInputType
+
+ public var supportedControllerInputs: [Input] {
+ return self.inputMappings.keys.map { AnyInput(stringValue: $0, intValue: nil, type: .controller(self.gameControllerInputType)) }
+ }
+
+ private var inputMappings: [String: AnyInput]
+
+ public init(gameControllerInputType: GameControllerInputType)
+ {
+ self.gameControllerInputType = gameControllerInputType
+
+ self.inputMappings = [:]
+ }
+
+ public func input(forControllerInput controllerInput: Input) -> Input?
+ {
+ precondition(controllerInput.type == .controller(self.gameControllerInputType), "controllerInput.type must match GameControllerInputMapping.gameControllerInputType")
+
+ let input = self.inputMappings[controllerInput.stringValue]
+ return input
+ }
+}
+
+public extension GameControllerInputMapping
+{
+ init(fileURL: URL) throws
+ {
+ let data = try Data(contentsOf: fileURL)
+
+ let decoder = PropertyListDecoder()
+ self = try decoder.decode(GameControllerInputMapping.self, from: data)
+ }
+
+ func write(to url: URL) throws
+ {
+ let encoder = PropertyListEncoder()
+
+ let data = try encoder.encode(self)
+ try data.write(to: url)
+ }
+}
+
+public extension GameControllerInputMapping
+{
+ mutating func set(_ input: Input?, forControllerInput controllerInput: Input)
+ {
+ precondition(controllerInput.type == .controller(self.gameControllerInputType), "controllerInput.type must match GameControllerInputMapping.gameControllerInputType")
+
+ if let input = input
+ {
+ self.inputMappings[controllerInput.stringValue] = AnyInput(input)
+ }
+ else
+ {
+ self.inputMappings[controllerInput.stringValue] = nil
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Model/GameControllerStateManager.swift b/Cores/DeltaCore/Sources/Model/GameControllerStateManager.swift
new file mode 100644
index 000000000..aea635da0
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Model/GameControllerStateManager.swift
@@ -0,0 +1,149 @@
+//
+// GameControllerStateManager.swift
+// DeltaCore
+//
+// Created by Riley Testut on 5/29/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+internal class GameControllerStateManager
+{
+ let gameController: GameController
+
+ private(set) var activatedInputs = [AnyInput: Double]()
+ private(set) var sustainedInputs = [AnyInput: Double]()
+
+ var receivers: [GameControllerReceiver] {
+ var objects: [GameControllerReceiver]!
+
+ self.dispatchQueue.sync {
+ objects = self._receivers.keyEnumerator().allObjects as? [GameControllerReceiver]
+ }
+
+ return objects
+ }
+
+ private let _receivers = NSMapTable.weakToStrongObjects()
+
+ // Used to synchronize access to _receivers to prevent race conditions (yay ObjC)
+ private let dispatchQueue = DispatchQueue(label: "com.rileytestut.Delta.GameControllerStateManager.dispatchQueue")
+
+
+ init(gameController: GameController)
+ {
+ self.gameController = gameController
+ }
+}
+
+extension GameControllerStateManager
+{
+ func addReceiver(_ receiver: GameControllerReceiver, inputMapping: GameControllerInputMappingProtocol?)
+ {
+ self.dispatchQueue.sync {
+ self._receivers.setObject(inputMapping as AnyObject, forKey: receiver)
+ }
+ }
+
+ func removeReceiver(_ receiver: GameControllerReceiver)
+ {
+ self.dispatchQueue.sync {
+ self._receivers.removeObject(forKey: receiver)
+ }
+ }
+}
+
+extension GameControllerStateManager
+{
+ func activate(_ input: Input, value: Double)
+ {
+ precondition(input.type == .controller(self.gameController.inputType), "input.type must match self.gameController.inputType")
+
+ // An input may be "activated" multiple times, such as by pressing different buttons that map to same input, or moving an analog stick.
+ self.activatedInputs[AnyInput(input)] = value
+
+ for receiver in self.receivers
+ {
+ if let mappedInput = self.mappedInput(for: input, receiver: receiver)
+ {
+ receiver.gameController(self.gameController, didActivate: mappedInput, value: value)
+ }
+ }
+ }
+
+ func deactivate(_ input: Input)
+ {
+ precondition(input.type == .controller(self.gameController.inputType), "input.type must match self.gameController.inputType")
+
+ // Unlike activate(_:), we don't allow an input to be deactivated multiple times.
+ guard self.activatedInputs.keys.contains(AnyInput(input)) else { return }
+
+ if let sustainedValue = self.sustainedInputs[AnyInput(input)]
+ {
+ // Currently sustained, so reset value to sustained value.
+ self.activate(input, value: sustainedValue)
+ }
+ else
+ {
+ // Not sustained, so simply deactivate it.
+ self.activatedInputs[AnyInput(input)] = nil
+
+ for receiver in self.receivers
+ {
+ if let mappedInput = self.mappedInput(for: input, receiver: receiver)
+ {
+ let hasActivatedMappedControllerInputs = self.activatedInputs.keys.contains {
+ guard let input = self.mappedInput(for: $0, receiver: receiver) else { return false }
+ return input == mappedInput
+ }
+
+ if !hasActivatedMappedControllerInputs
+ {
+ // All controller inputs that map to this input have been deactivated, so we can deactivate the mapped input.
+ receiver.gameController(self.gameController, didDeactivate: mappedInput)
+ }
+ }
+ }
+ }
+ }
+
+ func sustain(_ input: Input, value: Double)
+ {
+ precondition(input.type == .controller(self.gameController.inputType), "input.type must match self.gameController.inputType")
+
+ if self.activatedInputs[AnyInput(input)] != value
+ {
+ self.activate(input, value: value)
+ }
+
+ self.sustainedInputs[AnyInput(input)] = value
+ }
+
+ // Technically not a word, but no good alternative, so ¯\_(ツ)_/¯
+ func unsustain(_ input: Input)
+ {
+ precondition(input.type == .controller(self.gameController.inputType), "input.type must match self.gameController.inputType")
+
+ self.sustainedInputs[AnyInput(input)] = nil
+
+ self.deactivate(AnyInput(input))
+ }
+}
+
+extension GameControllerStateManager
+{
+ func inputMapping(for receiver: GameControllerReceiver) -> GameControllerInputMappingProtocol?
+ {
+ let inputMapping = self._receivers.object(forKey: receiver) as? GameControllerInputMappingProtocol
+ return inputMapping
+ }
+
+ func mappedInput(for input: Input, receiver: GameControllerReceiver) -> Input?
+ {
+ guard let inputMapping = self.inputMapping(for: receiver) else { return input }
+
+ let mappedInput = inputMapping.input(forControllerInput: input)
+ return mappedInput
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Model/Inputs/AnyInput.swift b/Cores/DeltaCore/Sources/Model/Inputs/AnyInput.swift
new file mode 100644
index 000000000..2eeae1d9f
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Model/Inputs/AnyInput.swift
@@ -0,0 +1,111 @@
+//
+// AnyInput.swift
+// DeltaCore
+//
+// Created by Riley Testut on 7/24/17.
+// Copyright © 2017 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public struct AnyInput: Input, Codable, Hashable
+{
+ public let stringValue: String
+ public let intValue: Int?
+
+ public var type: InputType
+ public var isContinuous: Bool
+
+ public init(_ input: Input)
+ {
+ self.init(stringValue: input.stringValue, intValue: input.intValue, type: input.type, isContinuous: input.isContinuous)
+ }
+
+ public init(stringValue: String, intValue: Int?, type: InputType, isContinuous: Bool? = nil)
+ {
+ self.stringValue = stringValue
+ self.intValue = intValue
+
+ self.type = type
+ self.isContinuous = false
+
+ if let isContinuous = isContinuous
+ {
+ self.isContinuous = isContinuous
+ }
+ else
+ {
+ switch type
+ {
+ case .game(let gameType):
+ guard let deltaCore = Delta.core(for: gameType), let input = deltaCore.gameInputType.init(stringValue: self.stringValue) else { break }
+ self.isContinuous = input.isContinuous
+
+ case .controller(.standard):
+ guard let standardInput = StandardGameControllerInput(input: self) else { break }
+ self.isContinuous = standardInput.isContinuous
+
+ case .controller(.mfi):
+ guard let mfiInput = MFiGameController.Input(input: self) else { break }
+ self.isContinuous = mfiInput.isContinuous
+
+ case .controller:
+ // FIXME: We have no way to look up arbitrary controller inputs at runtime, so just leave isContinuous as false for now.
+ // In practice this is not too bad, since it's very uncommon to map from an input to a non-standard controller input.
+ break
+ }
+ }
+ }
+}
+
+public extension AnyInput
+{
+ init?(stringValue: String)
+ {
+ return nil
+ }
+
+ init?(intValue: Int)
+ {
+ return nil
+ }
+}
+
+public extension AnyInput
+{
+ private enum CodingKeys: String, CodingKey
+ {
+ case stringValue = "identifier"
+ case type
+ }
+
+ init(from decoder: Decoder) throws
+ {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ let stringValue = try container.decode(String.self, forKey: .stringValue)
+ let type = try container.decode(InputType.self, forKey: .type)
+
+ let intValue: Int?
+
+ switch type
+ {
+ case .controller: intValue = nil
+ case .game(let gameType):
+ guard let deltaCore = Delta.core(for: gameType), let input = deltaCore.gameInputType.init(stringValue: stringValue) else {
+ throw DecodingError.dataCorruptedError(forKey: .stringValue, in: container, debugDescription: "The Input game type \(gameType) is unsupported.")
+ }
+
+ intValue = input.intValue
+ }
+
+ self.init(stringValue: stringValue, intValue: intValue, type: type)
+ }
+
+ func encode(to encoder: Encoder) throws
+ {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(self.stringValue, forKey: .stringValue)
+ try container.encode(self.type, forKey: .type)
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Model/Inputs/StandardGameControllerInput.swift b/Cores/DeltaCore/Sources/Model/Inputs/StandardGameControllerInput.swift
new file mode 100644
index 000000000..0fb0a976a
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Model/Inputs/StandardGameControllerInput.swift
@@ -0,0 +1,98 @@
+//
+// StandardGameControllerInput.swift
+// DeltaCore
+//
+// Created by Riley Testut on 7/20/17.
+// Copyright © 2017 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public extension GameControllerInputType
+{
+ static let standard = GameControllerInputType("standard")
+}
+
+public enum StandardGameControllerInput: String, Codable
+{
+ case menu
+
+ case up
+ case down
+ case left
+ case right
+
+ case leftThumbstickUp
+ case leftThumbstickDown
+ case leftThumbstickLeft
+ case leftThumbstickRight
+
+ case rightThumbstickUp
+ case rightThumbstickDown
+ case rightThumbstickLeft
+ case rightThumbstickRight
+
+ case a
+ case b
+ case x
+ case y
+
+ case start
+ case select
+
+ case l1
+ case l2
+ case l3
+
+ case r1
+ case r2
+ case r3
+}
+
+extension StandardGameControllerInput: Input
+{
+ public var type: InputType {
+ return .controller(.standard)
+ }
+
+ public var isContinuous: Bool {
+ switch self
+ {
+ case .leftThumbstickUp, .leftThumbstickDown, .leftThumbstickLeft, .leftThumbstickRight: return true
+ case .rightThumbstickUp, .rightThumbstickDown, .rightThumbstickLeft, .rightThumbstickRight: return true
+ default: return false
+ }
+ }
+}
+
+public extension StandardGameControllerInput
+{
+ private static var inputMappings = [GameType: GameControllerInputMapping]()
+
+ func input(for gameType: GameType) -> Input?
+ {
+ if let inputMapping = StandardGameControllerInput.inputMappings[gameType]
+ {
+ let input = inputMapping.input(forControllerInput: self)
+ return input
+ }
+
+ guard
+ let deltaCore = Delta.core(for: gameType),
+ let fileURL = deltaCore.resourceBundle.url(forResource: "Standard", withExtension: "deltamapping")
+ else { fatalError("Cannot find Standard.deltamapping for game type \(gameType)") }
+
+ do
+ {
+ let inputMapping = try GameControllerInputMapping(fileURL: fileURL)
+ StandardGameControllerInput.inputMappings[gameType] = inputMapping
+
+ let input = inputMapping.input(forControllerInput: self)
+ return input
+ }
+ catch
+ {
+ fatalError(String(describing: error))
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Model/SaveState.swift b/Cores/DeltaCore/Sources/Model/SaveState.swift
new file mode 100644
index 000000000..974dcf313
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Model/SaveState.swift
@@ -0,0 +1,21 @@
+//
+// SaveState.swift
+// DeltaCore
+//
+// Created by Riley Testut on 1/31/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public struct SaveState: SaveStateProtocol
+{
+ public var fileURL: URL
+ public var gameType: GameType
+
+ public init(fileURL: URL, gameType: GameType)
+ {
+ self.fileURL = fileURL
+ self.gameType = gameType
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Protocols/Inputs/GameController.swift b/Cores/DeltaCore/Sources/Protocols/Inputs/GameController.swift
new file mode 100644
index 000000000..843648c32
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Protocols/Inputs/GameController.swift
@@ -0,0 +1,132 @@
+//
+// GameController.swift
+// DeltaCore
+//
+// Created by Riley Testut on 5/3/15.
+// Copyright (c) 2015 Riley Testut. All rights reserved.
+//
+
+import ObjectiveC
+
+private var gameControllerStateManagerKey = 0
+
+//MARK: - GameControllerReceiver -
+public protocol GameControllerReceiver: class
+{
+ /// Equivalent to pressing a button, or moving an analog stick
+ func gameController(_ gameController: GameController, didActivate input: Input, value: Double)
+
+ /// Equivalent to releasing a button or an analog stick
+ func gameController(_ gameController: GameController, didDeactivate input: Input)
+}
+
+//MARK: - GameController -
+public protocol GameController: NSObjectProtocol
+{
+ var name: String { get }
+
+ var playerIndex: Int? { get set }
+
+ var inputType: GameControllerInputType { get }
+
+ var defaultInputMapping: GameControllerInputMappingProtocol? { get }
+}
+
+public extension GameController
+{
+ private var stateManager: GameControllerStateManager {
+ var stateManager = objc_getAssociatedObject(self, &gameControllerStateManagerKey) as? GameControllerStateManager
+
+ if stateManager == nil
+ {
+ stateManager = GameControllerStateManager(gameController: self)
+ objc_setAssociatedObject(self, &gameControllerStateManagerKey, stateManager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ }
+
+ return stateManager!
+ }
+
+ var receivers: [GameControllerReceiver] {
+ return self.stateManager.receivers
+ }
+
+ var activatedInputs: [AnyInput: Double] {
+ return self.stateManager.activatedInputs
+ }
+
+ var sustainedInputs: [AnyInput: Double] {
+ return self.stateManager.sustainedInputs
+ }
+}
+
+public extension GameController
+{
+ func addReceiver(_ receiver: GameControllerReceiver)
+ {
+ self.addReceiver(receiver, inputMapping: self.defaultInputMapping)
+ }
+
+ func addReceiver(_ receiver: GameControllerReceiver, inputMapping: GameControllerInputMappingProtocol?)
+ {
+ self.stateManager.addReceiver(receiver, inputMapping: inputMapping)
+ }
+
+ func removeReceiver(_ receiver: GameControllerReceiver)
+ {
+ self.stateManager.removeReceiver(receiver)
+ }
+
+ func activate(_ input: Input, value: Double = 1.0)
+ {
+ self.stateManager.activate(input, value: value)
+ }
+
+ func deactivate(_ input: Input)
+ {
+ self.stateManager.deactivate(input)
+ }
+
+ func sustain(_ input: Input, value: Double = 1.0)
+ {
+ self.stateManager.sustain(input, value: value)
+ }
+
+ func unsustain(_ input: Input)
+ {
+ self.stateManager.unsustain(input)
+ }
+}
+
+public extension GameController
+{
+ func inputMapping(for receiver: GameControllerReceiver) -> GameControllerInputMappingProtocol?
+ {
+ return self.stateManager.inputMapping(for: receiver)
+ }
+
+ func mappedInput(for input: Input, receiver: GameControllerReceiver) -> Input?
+ {
+ return self.stateManager.mappedInput(for: input, receiver: receiver)
+ }
+}
+
+public func ==(lhs: GameController?, rhs: GameController?) -> Bool
+{
+ switch (lhs, rhs)
+ {
+ case (nil, nil): return true
+ case (_?, nil): return false
+ case (nil, _?): return false
+ case (let lhs?, let rhs?): return lhs.isEqual(rhs)
+ }
+}
+
+public func !=(lhs: GameController?, rhs: GameController?) -> Bool
+{
+ return !(lhs == rhs)
+}
+
+public func ~=(pattern: GameController?, value: GameController?) -> Bool
+{
+ return pattern == value
+}
diff --git a/Cores/DeltaCore/Sources/Protocols/Inputs/Input.swift b/Cores/DeltaCore/Sources/Protocols/Inputs/Input.swift
new file mode 100644
index 000000000..71d2dd71b
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Protocols/Inputs/Input.swift
@@ -0,0 +1,105 @@
+//
+// Input.swift
+// DeltaCore
+//
+// Created by Riley Testut on 7/4/15.
+// Copyright © 2015 Riley Testut. All rights reserved.
+//
+
+public enum InputType: Codable
+{
+ case controller(GameControllerInputType)
+ case game(GameType)
+}
+
+extension InputType: RawRepresentable
+{
+ public var rawValue: String {
+ switch self
+ {
+ case .controller(let inputType): return inputType.rawValue
+ case .game(let gameType): return gameType.rawValue
+ }
+ }
+
+ public init(rawValue: String)
+ {
+ let gameType = GameType(rawValue)
+
+ if Delta.core(for: gameType) != nil
+ {
+ self = .game(gameType)
+ }
+ else
+ {
+ let inputType = GameControllerInputType(rawValue)
+ self = .controller(inputType)
+ }
+ }
+}
+
+extension InputType: Hashable
+{
+ public func hash(into hasher: inout Hasher)
+ {
+ hasher.combine(self.rawValue)
+ }
+}
+
+// Conformance to CodingKey allows compiler to automatically generate intValue/stringValue logic for enums.
+public protocol Input: CodingKey
+{
+ var type: InputType { get }
+
+ var isContinuous: Bool { get }
+}
+
+public extension RawRepresentable where Self: Input, RawValue == String
+{
+ var stringValue: String {
+ return self.rawValue
+ }
+
+ var intValue: Int? {
+ return nil
+ }
+
+ init?(stringValue: String)
+ {
+ self.init(rawValue: stringValue)
+ }
+
+ init?(intValue: Int)
+ {
+ return nil
+ }
+}
+
+public extension Input
+{
+ var isContinuous: Bool {
+ return false
+ }
+
+ init?(input: Input)
+ {
+ self.init(stringValue: input.stringValue)
+
+ guard self.type == input.type else { return nil }
+ }
+}
+
+public func ==(lhs: Input?, rhs: Input?) -> Bool
+{
+ return lhs?.type == rhs?.type && lhs?.stringValue == rhs?.stringValue
+}
+
+public func !=(lhs: Input?, rhs: Input?) -> Bool
+{
+ return !(lhs == rhs)
+}
+
+public func ~=(pattern: Input?, value: Input?) -> Bool
+{
+ return pattern == value
+}
diff --git a/Cores/DeltaCore/Sources/Protocols/Model/CheatProtocol.swift b/Cores/DeltaCore/Sources/Protocols/Model/CheatProtocol.swift
new file mode 100644
index 000000000..b9eeff7b8
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Protocols/Model/CheatProtocol.swift
@@ -0,0 +1,15 @@
+//
+// CheatProtocol.swift
+// DeltaCore
+//
+// Created by Riley Testut on 5/19/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public protocol CheatProtocol
+{
+ var code: String { get }
+ var type: CheatType { get }
+}
diff --git a/Cores/DeltaCore/Sources/Protocols/Model/ControllerSkinProtocol.swift b/Cores/DeltaCore/Sources/Protocols/Model/ControllerSkinProtocol.swift
new file mode 100644
index 000000000..88dca012a
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Protocols/Model/ControllerSkinProtocol.swift
@@ -0,0 +1,73 @@
+//
+// ControllerSkinProtocol.swift
+// DeltaCore
+//
+// Created by Riley Testut on 10/13/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+public protocol ControllerSkinProtocol
+{
+ var name: String { get }
+ var identifier: String { get }
+ var gameType: GameType { get }
+ var isDebugModeEnabled: Bool { get }
+
+ func supports(_ traits: ControllerSkin.Traits) -> Bool
+
+ func image(for traits: ControllerSkin.Traits, preferredSize: ControllerSkin.Size) -> UIImage?
+ func thumbstick(for item: ControllerSkin.Item, traits: ControllerSkin.Traits, preferredSize: ControllerSkin.Size) -> (UIImage, CGSize)?
+
+ /// Provided point should be normalized [0,1] for both axies.
+ func inputs(for traits: ControllerSkin.Traits, at point: CGPoint) -> [Input]?
+
+ func items(for traits: ControllerSkin.Traits) -> [ControllerSkin.Item]?
+
+ func isTranslucent(for traits: ControllerSkin.Traits) -> Bool?
+
+ func gameScreenFrame(for traits: ControllerSkin.Traits) -> CGRect?
+ func screens(for traits: ControllerSkin.Traits) -> [ControllerSkin.Screen]?
+
+ func aspectRatio(for traits: ControllerSkin.Traits) -> CGSize?
+
+ func supportedTraits(for traits: ControllerSkin.Traits) -> ControllerSkin.Traits?
+}
+
+public extension ControllerSkinProtocol
+{
+ func supportedTraits(for traits: ControllerSkin.Traits) -> ControllerSkin.Traits?
+ {
+ var traits = traits
+
+ while !self.supports(traits)
+ {
+ guard traits.device == .iphone, traits.displayType == .edgeToEdge else { return nil }
+
+ traits.displayType = .standard
+ }
+
+ return traits
+ }
+
+ func gameScreenFrame(for traits: DeltaCore.ControllerSkin.Traits) -> CGRect?
+ {
+ return self.screens(for: traits)?.first?.outputFrame
+ }
+}
+
+public func ==(lhs: ControllerSkinProtocol?, rhs: ControllerSkinProtocol?) -> Bool
+{
+ return lhs?.identifier == rhs?.identifier
+}
+
+public func !=(lhs: ControllerSkinProtocol?, rhs: ControllerSkinProtocol?) -> Bool
+{
+ return !(lhs == rhs)
+}
+
+public func ~=(pattern: ControllerSkinProtocol?, value: ControllerSkinProtocol?) -> Bool
+{
+ return pattern == value
+}
diff --git a/Cores/DeltaCore/Sources/Protocols/Model/GameControllerInputMappingProtocol.swift b/Cores/DeltaCore/Sources/Protocols/Model/GameControllerInputMappingProtocol.swift
new file mode 100644
index 000000000..88a462f08
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Protocols/Model/GameControllerInputMappingProtocol.swift
@@ -0,0 +1,16 @@
+//
+// GameControllerInputMappingProtocol.swift
+// DeltaCore
+//
+// Created by Riley Testut on 8/14/17.
+// Copyright © 2017 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public protocol GameControllerInputMappingProtocol
+{
+ var gameControllerInputType: GameControllerInputType { get }
+
+ func input(forControllerInput controllerInput: Input) -> Input?
+}
diff --git a/Cores/DeltaCore/Sources/Protocols/Model/GameProtocol.swift b/Cores/DeltaCore/Sources/Protocols/Model/GameProtocol.swift
new file mode 100644
index 000000000..b0d9dc03c
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Protocols/Model/GameProtocol.swift
@@ -0,0 +1,28 @@
+//
+// GameProtocol.swift
+// DeltaCore
+//
+// Created by Riley Testut on 3/8/15.
+// Copyright (c) 2015 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public protocol GameProtocol
+{
+ var fileURL: URL { get }
+ var gameSaveURL: URL { get }
+
+ var type: GameType { get }
+}
+
+public extension GameProtocol
+{
+ var gameSaveURL: URL {
+ let fileExtension = Delta.core(for: self.type)?.gameSaveFileExtension ?? "sav"
+
+ let gameURL = self.fileURL.deletingPathExtension()
+ let gameSaveURL = gameURL.appendingPathExtension(fileExtension)
+ return gameSaveURL
+ }
+}
diff --git a/Cores/DeltaCore/Sources/Protocols/Model/SaveStateProtocol.swift b/Cores/DeltaCore/Sources/Protocols/Model/SaveStateProtocol.swift
new file mode 100644
index 000000000..c40407eb3
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Protocols/Model/SaveStateProtocol.swift
@@ -0,0 +1,15 @@
+//
+// SaveStateProtocol.swift
+// DeltaCore
+//
+// Created by Riley Testut on 1/31/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public protocol SaveStateProtocol
+{
+ var fileURL: URL { get }
+ var gameType: GameType { get }
+}
diff --git a/Cores/DeltaCore/Sources/Protocols/Rendering/AudioRendering.swift b/Cores/DeltaCore/Sources/Protocols/Rendering/AudioRendering.swift
new file mode 100644
index 000000000..5c9bb861c
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Protocols/Rendering/AudioRendering.swift
@@ -0,0 +1,15 @@
+//
+// AudioRendering.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/29/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+@objc(DLTAAudioRendering)
+public protocol AudioRendering: NSObjectProtocol
+{
+ var audioBuffer: RingBuffer { get }
+}
diff --git a/Cores/DeltaCore/Sources/Protocols/Rendering/VideoRendering.swift b/Cores/DeltaCore/Sources/Protocols/Rendering/VideoRendering.swift
new file mode 100644
index 000000000..6dc87e974
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Protocols/Rendering/VideoRendering.swift
@@ -0,0 +1,18 @@
+//
+// VideoRendering.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/29/16.
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+@objc(DLTAVideoRendering)
+public protocol VideoRendering: NSObjectProtocol
+{
+ var videoBuffer: UnsafeMutablePointer? { get }
+
+ func prepare()
+ func processFrame()
+}
diff --git a/Cores/DeltaCore/Sources/Types/ExtensibleEnums.swift b/Cores/DeltaCore/Sources/Types/ExtensibleEnums.swift
new file mode 100644
index 000000000..140dc071e
--- /dev/null
+++ b/Cores/DeltaCore/Sources/Types/ExtensibleEnums.swift
@@ -0,0 +1,38 @@
+//
+// ExtensibleEnum.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/9/18.
+// Copyright © 2018 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+public protocol ExtensibleEnum: Hashable, Codable, RawRepresentable where RawValue == String {}
+
+public extension ExtensibleEnum
+{
+ init(_ rawValue: String)
+ {
+ self.init(rawValue: rawValue)!
+ }
+
+ init(from decoder: Decoder) throws
+ {
+ let container = try decoder.singleValueContainer()
+
+ let rawValue = try container.decode(String.self)
+ self.init(rawValue: rawValue)!
+ }
+
+ func encode(to encoder: Encoder) throws
+ {
+ var container = encoder.singleValueContainer()
+ try container.encode(self.rawValue)
+ }
+}
+
+// Conform types to ExtensibleEnum to receive automatic Codable conformance + implementation.
+extension GameType: ExtensibleEnum {}
+extension CheatType: ExtensibleEnum {}
+extension GameControllerInputType: ExtensibleEnum {}
diff --git a/Cores/DeltaCore/Sources/UI/Controller/ButtonsInputView.swift b/Cores/DeltaCore/Sources/UI/Controller/ButtonsInputView.swift
new file mode 100644
index 000000000..c2a8f71d8
--- /dev/null
+++ b/Cores/DeltaCore/Sources/UI/Controller/ButtonsInputView.swift
@@ -0,0 +1,154 @@
+//
+// ButtonsInputView.swift
+// DeltaCore
+//
+// Created by Riley Testut on 8/4/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+class ButtonsInputView: UIView
+{
+ var isHapticFeedbackEnabled = true
+
+ var controllerSkin: ControllerSkinProtocol?
+ var controllerSkinTraits: ControllerSkin.Traits?
+
+ var activateInputsHandler: ((Set) -> Void)?
+ var deactivateInputsHandler: ((Set) -> Void)?
+
+ var image: UIImage? {
+ get {
+ return self.imageView.image
+ }
+ set {
+ self.imageView.image = newValue
+ }
+ }
+
+ private let imageView = UIImageView(frame: .zero)
+
+ private let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
+
+ private var touchInputsMappingDictionary: [UITouch: Set] = [:]
+ private var previousTouchInputs = Set()
+ private var touchInputs: Set {
+ return self.touchInputsMappingDictionary.values.reduce(Set(), { $0.union($1) })
+ }
+
+ override var intrinsicContentSize: CGSize {
+ return self.imageView.intrinsicContentSize
+ }
+
+ override init(frame: CGRect)
+ {
+ super.init(frame: frame)
+
+ self.isMultipleTouchEnabled = true
+
+ self.feedbackGenerator.prepare()
+
+ self.imageView.translatesAutoresizingMaskIntoConstraints = false
+ self.addSubview(self.imageView)
+
+ NSLayoutConstraint.activate([self.imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
+ self.imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
+ self.imageView.topAnchor.constraint(equalTo: self.topAnchor),
+ self.imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor)])
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ public override func touchesBegan(_ touches: Set, with event: UIEvent?)
+ {
+ for touch in touches
+ {
+ self.touchInputsMappingDictionary[touch] = []
+ }
+
+ self.updateInputs(for: touches)
+ }
+
+ public override func touchesMoved(_ touches: Set, with event: UIEvent?)
+ {
+ self.updateInputs(for: touches)
+ }
+
+ public override func touchesEnded(_ touches: Set, with event: UIEvent?)
+ {
+ for touch in touches
+ {
+ self.touchInputsMappingDictionary[touch] = nil
+ }
+
+ self.updateInputs(for: touches)
+ }
+
+ public override func touchesCancelled(_ touches: Set, with event: UIEvent?)
+ {
+ return self.touchesEnded(touches, with: event)
+ }
+}
+
+private extension ButtonsInputView
+{
+ func updateInputs(for touches: Set)
+ {
+ guard let controllerSkin = self.controllerSkin else { return }
+
+ // Don't add the touches if it has been removed in touchesEnded:/touchesCancelled:
+ for touch in touches where self.touchInputsMappingDictionary[touch] != nil
+ {
+ guard touch.view == self else { continue }
+
+ var point = touch.location(in: self)
+ point.x /= self.bounds.width
+ point.y /= self.bounds.height
+
+ if let traits = self.controllerSkinTraits
+ {
+ let inputs = Set((controllerSkin.inputs(for: traits, at: point) ?? []).map { AnyInput($0) })
+
+ let menuInput = AnyInput(stringValue: StandardGameControllerInput.menu.stringValue, intValue: nil, type: .controller(.controllerSkin))
+ if inputs.contains(menuInput)
+ {
+ // If the menu button is located at this position, ignore all other inputs that might be overlapping.
+ self.touchInputsMappingDictionary[touch] = [menuInput]
+ }
+ else
+ {
+ self.touchInputsMappingDictionary[touch] = Set(inputs)
+ }
+ }
+ }
+
+ let activatedInputs = self.touchInputs.subtracting(self.previousTouchInputs)
+ let deactivatedInputs = self.previousTouchInputs.subtracting(self.touchInputs)
+
+ // We must update previousTouchInputs *before* calling activate() and deactivate().
+ // Otherwise, race conditions that cause duplicate touches from activate() or deactivate() calls can result in various bugs.
+ self.previousTouchInputs = self.touchInputs
+
+ if !activatedInputs.isEmpty
+ {
+ self.activateInputsHandler?(activatedInputs)
+
+ if self.isHapticFeedbackEnabled
+ {
+ switch UIDevice.current.feedbackSupportLevel
+ {
+ case .feedbackGenerator: self.feedbackGenerator.impactOccurred()
+ case .basic, .unsupported: UIDevice.current.vibrate()
+ }
+ }
+ }
+
+ if !deactivatedInputs.isEmpty
+ {
+ self.deactivateInputsHandler?(deactivatedInputs)
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/UI/Controller/ControllerDebugView.swift b/Cores/DeltaCore/Sources/UI/Controller/ControllerDebugView.swift
new file mode 100644
index 000000000..2248c98dd
--- /dev/null
+++ b/Cores/DeltaCore/Sources/UI/Controller/ControllerDebugView.swift
@@ -0,0 +1,125 @@
+//
+// ControllerDebugView.swift
+// DeltaCore
+//
+// Created by Riley Testut on 12/20/15.
+// Copyright © 2015 Riley Testut. All rights reserved.
+//
+
+import UIKit
+import Foundation
+
+internal class ControllerDebugView: UIView
+{
+ var items: [ControllerSkin.Item]? {
+ didSet {
+ self.updateItems()
+ }
+ }
+
+ private var itemViews = [ItemView]()
+
+ override init(frame: CGRect)
+ {
+ super.init(frame: frame)
+
+ self.initialize()
+ }
+
+ required init?(coder aDecoder: NSCoder)
+ {
+ super.init(coder: aDecoder)
+
+ self.initialize()
+ }
+
+ private func initialize()
+ {
+ self.backgroundColor = UIColor.clear
+ self.isUserInteractionEnabled = false
+ }
+
+ override func layoutSubviews()
+ {
+ super.layoutSubviews()
+
+ for view in self.itemViews
+ {
+ var frame = view.item.extendedFrame
+ frame.origin.x *= self.bounds.width
+ frame.origin.y *= self.bounds.height
+ frame.size.width *= self.bounds.width
+ frame.size.height *= self.bounds.height
+
+ view.frame = frame
+ }
+ }
+
+ private func updateItems()
+ {
+ self.itemViews.forEach { $0.removeFromSuperview() }
+
+ var itemViews = [ItemView]()
+
+ for item in (self.items ?? [])
+ {
+ let itemView = ItemView(item: item)
+ self.addSubview(itemView)
+
+ itemViews.append(itemView)
+ }
+
+ self.itemViews = itemViews
+
+ self.setNeedsLayout()
+ }
+}
+
+private class ItemView: UIView
+{
+ let item: ControllerSkin.Item
+
+ private let label: UILabel
+
+ init(item: ControllerSkin.Item)
+ {
+ self.item = item
+
+ self.label = UILabel()
+ self.label.translatesAutoresizingMaskIntoConstraints = false
+ self.label.textColor = UIColor.white
+ self.label.font = UIFont.boldSystemFont(ofSize: 16)
+
+ var text = ""
+
+ for input in item.inputs.allInputs
+ {
+ if text.isEmpty
+ {
+ text = input.stringValue
+ }
+ else
+ {
+ text = text + "," + input.stringValue
+ }
+ }
+
+ self.label.text = text
+
+ self.label.sizeToFit()
+
+ super.init(frame: CGRect.zero)
+
+ self.addSubview(self.label)
+
+ self.label.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
+ self.label.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
+
+ self.backgroundColor = UIColor.red.withAlphaComponent(0.75)
+ }
+
+ required init?(coder aDecoder: NSCoder)
+ {
+ fatalError()
+ }
+}
diff --git a/Cores/DeltaCore/Sources/UI/Controller/ControllerInputView.swift b/Cores/DeltaCore/Sources/UI/Controller/ControllerInputView.swift
new file mode 100644
index 000000000..ceb5c2a7f
--- /dev/null
+++ b/Cores/DeltaCore/Sources/UI/Controller/ControllerInputView.swift
@@ -0,0 +1,57 @@
+//
+// ControllerInputView.swift
+// DeltaCore
+//
+// Created by Riley Testut on 6/17/18.
+// Copyright © 2018 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+class ControllerInputView: UIInputView
+{
+ let controllerView: ControllerView
+
+ private var aspectRatioConstraint: NSLayoutConstraint? {
+ didSet {
+ oldValue?.isActive = false
+ }
+ }
+
+ init(frame: CGRect)
+ {
+ self.controllerView = ControllerView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height))
+ self.controllerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ self.controllerView.isControllerInputView = true
+
+ super.init(frame: frame, inputViewStyle: .keyboard)
+
+ self.addSubview(self.controllerView)
+
+ self.translatesAutoresizingMaskIntoConstraints = false
+ self.allowsSelfSizing = true
+
+ self.setNeedsUpdateConstraints()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func layoutSubviews()
+ {
+ super.layoutSubviews()
+
+ guard
+ let controllerSkin = self.controllerView.controllerSkin,
+ let traits = self.controllerView.controllerSkinTraits,
+ let aspectRatio = controllerSkin.aspectRatio(for: traits)
+ else { return }
+
+ let multiplier = aspectRatio.height / aspectRatio.width
+ guard self.aspectRatioConstraint?.multiplier != multiplier else { return }
+
+ self.aspectRatioConstraint = self.heightAnchor.constraint(equalTo: self.widthAnchor, multiplier: multiplier)
+ self.aspectRatioConstraint?.isActive = true
+ }
+}
diff --git a/Cores/DeltaCore/Sources/UI/Controller/ControllerView.swift b/Cores/DeltaCore/Sources/UI/Controller/ControllerView.swift
new file mode 100644
index 000000000..0d1f328bb
--- /dev/null
+++ b/Cores/DeltaCore/Sources/UI/Controller/ControllerView.swift
@@ -0,0 +1,647 @@
+//
+// ControllerView.swift
+// DeltaCore
+//
+// Created by Riley Testut on 5/3/15.
+// Copyright (c) 2015 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+private struct ControllerViewInputMapping: GameControllerInputMappingProtocol
+{
+ let controllerView: ControllerView
+
+ var name: String {
+ return self.controllerView.name
+ }
+
+ var gameControllerInputType: GameControllerInputType {
+ return self.controllerView.inputType
+ }
+
+ func input(forControllerInput controllerInput: Input) -> Input?
+ {
+ guard let gameType = self.controllerView.controllerSkin?.gameType, let deltaCore = Delta.core(for: gameType) else { return nil }
+
+ if let gameInput = deltaCore.gameInputType.init(stringValue: controllerInput.stringValue)
+ {
+ return gameInput
+ }
+
+ if let standardInput = StandardGameControllerInput(stringValue: controllerInput.stringValue)
+ {
+ return standardInput
+ }
+
+ return nil
+ }
+}
+
+extension ControllerView
+{
+ public static let controllerViewDidChangeControllerSkinNotification = Notification.Name("controllerViewDidChangeControllerSkinNotification")
+}
+
+public class ControllerView: UIView, GameController
+{
+ //MARK: - Properties -
+ /** Properties **/
+ public var controllerSkin: ControllerSkinProtocol? {
+ didSet {
+ self.updateControllerSkin()
+ NotificationCenter.default.post(name: ControllerView.controllerViewDidChangeControllerSkinNotification, object: self)
+ }
+ }
+
+ public var controllerSkinTraits: ControllerSkin.Traits? {
+ if let traits = self.overrideControllerSkinTraits
+ {
+ return traits
+ }
+
+ guard let window = self.window else { return nil }
+
+ let traits = ControllerSkin.Traits.defaults(for: window)
+
+ guard let controllerSkin = self.controllerSkin else { return traits }
+
+ guard let supportedTraits = controllerSkin.supportedTraits(for: traits) else { return traits }
+ return supportedTraits
+ }
+
+ public var controllerSkinSize: ControllerSkin.Size! {
+ let size = self.overrideControllerSkinSize ?? UIScreen.main.defaultControllerSkinSize
+ return size
+ }
+
+ public var overrideControllerSkinTraits: ControllerSkin.Traits?
+ public var overrideControllerSkinSize: ControllerSkin.Size?
+
+ public var translucentControllerSkinOpacity: CGFloat = 0.7
+
+ public var isButtonHapticFeedbackEnabled = true {
+ didSet {
+ self.buttonsView.isHapticFeedbackEnabled = self.isButtonHapticFeedbackEnabled
+ }
+ }
+
+ public var isThumbstickHapticFeedbackEnabled = true {
+ didSet {
+ self.thumbstickViews.values.forEach { $0.isHapticFeedbackEnabled = self.isThumbstickHapticFeedbackEnabled }
+ }
+ }
+
+ //MARK: -
+ ///
+ public var name: String {
+ return self.controllerSkin?.name ?? NSLocalizedString("Game Controller", comment: "")
+ }
+
+ public var playerIndex: Int? {
+ didSet {
+ self.reloadInputViews()
+ }
+ }
+
+ public let inputType: GameControllerInputType = .controllerSkin
+
+ public lazy var defaultInputMapping: GameControllerInputMappingProtocol? = ControllerViewInputMapping(controllerView: self)
+
+ internal var isControllerInputView = false
+
+ //MARK: - Private Properties
+ private let contentView = UIView(frame: .zero)
+ private var transitionSnapshotView: UIView? = nil
+ private let controllerDebugView = ControllerDebugView()
+
+ private let buttonsView = ButtonsInputView(frame: CGRect.zero)
+ private var thumbstickViews = [ControllerSkin.Item: ThumbstickInputView]()
+ private var touchViews = [ControllerSkin.Item: TouchInputView]()
+
+ private var _performedInitialLayout = false
+
+ private var controllerInputView: ControllerInputView?
+
+ private(set) var imageCache = NSCache>()
+
+ public override var intrinsicContentSize: CGSize {
+ return self.buttonsView.intrinsicContentSize
+ }
+
+ //MARK: - Initializers -
+ /** Initializers **/
+ public override init(frame: CGRect)
+ {
+ super.init(frame: frame)
+
+ self.initialize()
+ }
+
+ public required init?(coder aDecoder: NSCoder)
+ {
+ super.init(coder: aDecoder)
+
+ self.initialize()
+ }
+
+ private func initialize()
+ {
+ self.backgroundColor = UIColor.clear
+
+ self.contentView.translatesAutoresizingMaskIntoConstraints = false
+ self.addSubview(self.contentView)
+
+ self.buttonsView.translatesAutoresizingMaskIntoConstraints = false
+ self.buttonsView.activateInputsHandler = { [weak self] (inputs) in
+ self?.activateButtonInputs(inputs)
+ }
+ self.buttonsView.deactivateInputsHandler = { [weak self] (inputs) in
+ self?.deactivateButtonInputs(inputs)
+ }
+ self.contentView.addSubview(self.buttonsView)
+
+ self.controllerDebugView.translatesAutoresizingMaskIntoConstraints = false
+ self.contentView.addSubview(self.controllerDebugView)
+
+ self.isMultipleTouchEnabled = true
+
+ // Remove shortcuts from shortcuts bar so it doesn't appear when using external keyboard as input.
+ self.inputAssistantItem.leadingBarButtonGroups = []
+ self.inputAssistantItem.trailingBarButtonGroups = []
+
+ NotificationCenter.default.addObserver(self, selector: #selector(ControllerView.keyboardDidDisconnect(_:)), name: .externalKeyboardDidDisconnect, object: nil)
+
+ NSLayoutConstraint.activate([self.contentView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
+ self.contentView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
+ self.contentView.topAnchor.constraint(equalTo: self.topAnchor),
+ self.contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor)])
+
+ NSLayoutConstraint.activate([self.buttonsView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
+ self.buttonsView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
+ self.buttonsView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
+ self.buttonsView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor)])
+
+ NSLayoutConstraint.activate([self.controllerDebugView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
+ self.controllerDebugView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
+ self.controllerDebugView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
+ self.controllerDebugView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor)])
+ }
+
+ //MARK: - UIView
+ /// UIView
+ public override func layoutSubviews()
+ {
+ super.layoutSubviews()
+
+ self._performedInitialLayout = true
+
+ self.updateControllerSkin()
+ }
+
+ public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
+ {
+ guard self.bounds.contains(point) else { return super.hitTest(point, with: event) }
+
+ let adjustedPoint = CGPoint(x: point.x / self.bounds.width, y: point.y / self.bounds.height)
+
+ for (item, thumbstickView) in self.thumbstickViews
+ {
+ guard item.extendedFrame.contains(adjustedPoint) else { continue }
+ return thumbstickView
+ }
+
+ for (item, touchView) in self.touchViews
+ {
+ guard item.frame.contains(adjustedPoint) else { continue }
+
+ if let traits = self.controllerSkinTraits, let inputs = self.controllerSkin?.inputs(for: traits, at: adjustedPoint)
+ {
+ // No other inputs at this position, so return touchView.
+ if inputs.isEmpty
+ {
+ return touchView
+ }
+ }
+ }
+
+ return self.buttonsView
+ }
+
+ //MARK: -
+ ///
+ public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
+ {
+ super.traitCollectionDidChange(previousTraitCollection)
+
+ self.setNeedsLayout()
+ }
+}
+
+//MARK: - UIResponder -
+/// UIResponder
+extension ControllerView
+{
+ public override var canBecomeFirstResponder: Bool {
+ let canBecomeFirstResponder = (self.controllerSkinTraits?.displayType == .splitView || ExternalGameControllerManager.shared.isKeyboardConnected)
+ return canBecomeFirstResponder
+ }
+
+ public override var next: UIResponder? {
+ return KeyboardResponder(nextResponder: super.next)
+ }
+
+ public override var inputView: UIView? {
+ guard self.playerIndex != nil else { return nil }
+ return self.controllerInputView
+ }
+
+ @discardableResult public override func becomeFirstResponder() -> Bool
+ {
+ guard super.becomeFirstResponder() else { return false }
+
+ self.reloadInputViews()
+
+ return self.isFirstResponder
+ }
+}
+
+//MARK: - Update Skins -
+/// Update Skins
+public extension ControllerView
+{
+ func beginAnimatingUpdateControllerSkin()
+ {
+ guard self.transitionSnapshotView == nil else { return }
+
+ guard let transitionSnapshotView = self.contentView.snapshotView(afterScreenUpdates: false) else { return }
+ transitionSnapshotView.frame = self.contentView.frame
+ transitionSnapshotView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ transitionSnapshotView.alpha = self.contentView.alpha
+ self.addSubview(transitionSnapshotView)
+
+ self.transitionSnapshotView = transitionSnapshotView
+
+ self.contentView.alpha = 0.0
+ }
+
+ func updateControllerSkin()
+ {
+ guard self._performedInitialLayout else { return }
+
+ self.buttonsView.controllerSkin = self.controllerSkin
+ self.buttonsView.controllerSkinTraits = self.controllerSkinTraits
+
+ if let isDebugModeEnabled = self.controllerSkin?.isDebugModeEnabled
+ {
+ self.controllerDebugView.isHidden = !isDebugModeEnabled
+ }
+
+ var isTranslucent = false
+
+ if let traits = self.controllerSkinTraits
+ {
+ let items = self.controllerSkin?.items(for: traits)
+ self.controllerDebugView.items = items
+
+ if traits.displayType == .splitView && !self.isControllerInputView
+ {
+ self.buttonsView.image = nil
+
+ self.isUserInteractionEnabled = false
+ self.controllerDebugView.alpha = 0.0
+ }
+ else
+ {
+ let image: UIImage?
+
+ if let controllerSkin = self.controllerSkin
+ {
+ let cacheKey = String(describing: traits) + "-" + String(describing: self.controllerSkinSize)
+
+ if
+ let cache = self.imageCache.object(forKey: controllerSkin.identifier as NSString),
+ let cachedImage = cache.object(forKey: cacheKey as NSString)
+ {
+ image = cachedImage
+ }
+ else
+ {
+ image = controllerSkin.image(for: traits, preferredSize: self.controllerSkinSize)
+ }
+
+ if let image = image
+ {
+ let cache = self.imageCache.object(forKey: controllerSkin.identifier as NSString) ?? NSCache()
+ cache.setObject(image, forKey: cacheKey as NSString)
+ self.imageCache.setObject(cache, forKey: controllerSkin.identifier as NSString)
+ }
+ }
+ else
+ {
+ image = nil
+ }
+
+ self.buttonsView.image = image
+
+ self.isUserInteractionEnabled = true
+ self.controllerDebugView.alpha = 1.0
+ }
+
+ isTranslucent = self.controllerSkin?.isTranslucent(for: traits) ?? false
+
+ var thumbstickViews = [ControllerSkin.Item: ThumbstickInputView]()
+ var previousThumbstickViews = self.thumbstickViews
+
+ var touchViews = [ControllerSkin.Item: TouchInputView]()
+ var previousTouchViews = self.touchViews
+
+ for item in items ?? []
+ {
+ var frame = item.frame
+ frame.origin.x *= self.bounds.width
+ frame.origin.y *= self.bounds.height
+ frame.size.width *= self.bounds.width
+ frame.size.height *= self.bounds.height
+
+ var extendedFrame = item.extendedFrame
+ extendedFrame.origin.x *= self.bounds.width
+ extendedFrame.origin.y *= self.bounds.height
+ extendedFrame.size.width *= self.bounds.width
+ extendedFrame.size.height *= self.bounds.height
+
+ switch item.kind
+ {
+ case .button, .dPad: break
+ case .thumbstick:
+ let thumbstickView: ThumbstickInputView
+
+ if let previousThumbstickView = previousThumbstickViews[item]
+ {
+ thumbstickView = previousThumbstickView
+ previousThumbstickViews[item] = nil
+ }
+ else
+ {
+ thumbstickView = ThumbstickInputView(frame: frame)
+ self.contentView.addSubview(thumbstickView)
+ }
+
+ thumbstickView.frame = frame
+ thumbstickView.valueChangedHandler = { [weak self] (xAxis, yAxis) in
+ self?.updateThumbstickValues(item: item, xAxis: xAxis, yAxis: yAxis)
+ }
+
+ if let (image, size) = self.controllerSkin?.thumbstick(for: item, traits: traits, preferredSize: self.controllerSkinSize)
+ {
+ let size = CGSize(width: size.width * self.bounds.width, height: size.height * self.bounds.height)
+ thumbstickView.thumbstickImage = image
+ thumbstickView.thumbstickSize = size
+ }
+
+ thumbstickView.isHapticFeedbackEnabled = self.isThumbstickHapticFeedbackEnabled
+
+ thumbstickViews[item] = thumbstickView
+
+ case .touchScreen:
+ let touchView: TouchInputView
+
+ if let previousTouchView = previousTouchViews[item]
+ {
+ touchView = previousTouchView
+ previousTouchViews[item] = nil
+ }
+ else
+ {
+ touchView = TouchInputView(frame: frame)
+ self.contentView.addSubview(touchView)
+ }
+
+ touchView.frame = frame
+ touchView.valueChangedHandler = { [weak self] (point) in
+ self?.updateTouchValues(item: item, point: point)
+ }
+
+ touchViews[item] = touchView
+ }
+ }
+
+ previousThumbstickViews.values.forEach { $0.removeFromSuperview() }
+ self.thumbstickViews = thumbstickViews
+
+ previousTouchViews.values.forEach { $0.removeFromSuperview() }
+ self.touchViews = touchViews
+ }
+ else
+ {
+ self.thumbstickViews.values.forEach { $0.removeFromSuperview() }
+ self.thumbstickViews = [:]
+
+ self.touchViews.values.forEach { $0.removeFromSuperview() }
+ self.touchViews = [:]
+ }
+
+ if self.transitionSnapshotView != nil
+ {
+ // Wrap in an animation closure to ensure it actually animates correctly
+ // As of iOS 8.3, calling this within transition coordinator animation closure without wrapping
+ // in this animation closure causes the change to be instantaneous
+ UIView.animate(withDuration: 0.0) {
+ self.contentView.alpha = isTranslucent ? self.translucentControllerSkinOpacity : 1.0
+ }
+ }
+ else
+ {
+ self.contentView.alpha = isTranslucent ? self.translucentControllerSkinOpacity : 1.0
+ }
+
+ self.transitionSnapshotView?.alpha = 0.0
+
+ if self.controllerSkinTraits?.displayType == .splitView
+ {
+ self.presentInputControllerView()
+ }
+ else
+ {
+ self.dismissInputControllerView()
+ }
+
+ self.controllerInputView?.controllerView.overrideControllerSkinTraits = self.controllerSkinTraits
+
+ self.invalidateIntrinsicContentSize()
+ self.setNeedsUpdateConstraints()
+
+ self.reloadInputViews()
+ }
+
+ func finishAnimatingUpdateControllerSkin()
+ {
+ if let transitionImageView = self.transitionSnapshotView
+ {
+ transitionImageView.removeFromSuperview()
+ self.transitionSnapshotView = nil
+ }
+
+ self.contentView.alpha = 1.0
+ }
+}
+
+private extension ControllerView
+{
+ func presentInputControllerView()
+ {
+ guard !self.isControllerInputView else { return }
+
+ guard let controllerSkin = self.controllerSkin, let traits = self.controllerSkinTraits else { return }
+
+ if self.controllerInputView == nil
+ {
+ let inputControllerView = ControllerInputView(frame: CGRect(x: 0, y: 0, width: 1024, height: 300))
+ inputControllerView.controllerView.addReceiver(self, inputMapping: nil)
+ self.controllerInputView = inputControllerView
+ }
+
+ if controllerSkin.supports(traits)
+ {
+ self.controllerInputView?.controllerView.controllerSkin = controllerSkin
+ }
+ else
+ {
+ self.controllerInputView?.controllerView.controllerSkin = ControllerSkin.standardControllerSkin(for: controllerSkin.gameType)
+ }
+ }
+
+ func dismissInputControllerView()
+ {
+ guard !self.isControllerInputView else { return }
+
+ guard self.controllerInputView != nil else { return }
+
+ self.controllerInputView = nil
+ }
+}
+
+//MARK: - Activating/Deactivating Inputs -
+/// Activating/Deactivating Inputs
+private extension ControllerView
+{
+ func activateButtonInputs(_ inputs: Set)
+ {
+ for input in inputs
+ {
+ self.activate(input)
+ }
+ }
+
+ func deactivateButtonInputs(_ inputs: Set)
+ {
+ for input in inputs
+ {
+ self.deactivate(input)
+ }
+ }
+
+ func updateThumbstickValues(item: ControllerSkin.Item, xAxis: Double, yAxis: Double)
+ {
+ guard case .directional(let up, let down, let left, let right) = item.inputs else { return }
+
+ switch xAxis
+ {
+ case ..<0:
+ self.activate(left, value: -xAxis)
+ self.deactivate(right)
+
+ case 0:
+ self.deactivate(left)
+ self.deactivate(right)
+
+ default:
+ self.deactivate(left)
+ self.activate(right, value: xAxis)
+ }
+
+ switch yAxis
+ {
+ case ..<0:
+ self.activate(down, value: -yAxis)
+ self.deactivate(up)
+
+ case 0:
+ self.deactivate(down)
+ self.deactivate(up)
+
+ default:
+ self.deactivate(down)
+ self.activate(up, value: yAxis)
+ }
+ }
+
+ func updateTouchValues(item: ControllerSkin.Item, point: CGPoint?)
+ {
+ guard case .touch(let x, let y) = item.inputs else { return }
+
+ if let point = point
+ {
+ self.activate(x, value: Double(point.x))
+ self.activate(y, value: Double(point.y))
+ }
+ else
+ {
+ self.deactivate(x)
+ self.deactivate(y)
+ }
+ }
+}
+
+private extension ControllerView
+{
+ @objc func keyboardDidDisconnect(_ notification: Notification)
+ {
+ guard self.isFirstResponder else { return }
+
+ self.resignFirstResponder()
+
+ if self.canBecomeFirstResponder
+ {
+ self.becomeFirstResponder()
+ }
+ }
+}
+
+//MARK: - GameControllerReceiver -
+/// GameControllerReceiver
+extension ControllerView: GameControllerReceiver
+{
+ public func gameController(_ gameController: GameController, didActivate input: Input, value: Double)
+ {
+ guard gameController == self.controllerInputView?.controllerView else { return }
+
+ self.activate(input)
+ }
+
+ public func gameController(_ gameController: GameController, didDeactivate input: Input)
+ {
+ guard gameController == self.controllerInputView?.controllerView else { return }
+
+ self.deactivate(input)
+ }
+}
+
+//MARK: - UIKeyInput
+/// UIKeyInput
+// Becoming first responder doesn't steal keyboard focus from other apps in split view unless the first responder conforms to UIKeyInput.
+// So, we conform ControllerView to UIKeyInput and provide stub method implementations.
+extension ControllerView: UIKeyInput
+{
+ public var hasText: Bool {
+ return false
+ }
+
+ public func insertText(_ text: String)
+ {
+ }
+
+ public func deleteBackward()
+ {
+ }
+}
diff --git a/Cores/DeltaCore/Sources/UI/Controller/ImmediatePanGestureRecognizer.swift b/Cores/DeltaCore/Sources/UI/Controller/ImmediatePanGestureRecognizer.swift
new file mode 100644
index 000000000..13511dff2
--- /dev/null
+++ b/Cores/DeltaCore/Sources/UI/Controller/ImmediatePanGestureRecognizer.swift
@@ -0,0 +1,21 @@
+//
+// ImmediatePanGestureRecognizer.swift
+// DeltaCore
+//
+// Created by Riley Testut on 8/5/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import UIKit.UIGestureRecognizerSubclass
+
+class ImmediatePanGestureRecognizer: UIPanGestureRecognizer
+{
+ override func touchesBegan(_ touches: Set, with event: UIEvent)
+ {
+ guard self.state != .began else { return }
+
+ super.touchesBegan(touches, with: event)
+
+ self.state = .began
+ }
+}
diff --git a/Cores/DeltaCore/Sources/UI/Controller/ThumbstickInputView.swift b/Cores/DeltaCore/Sources/UI/Controller/ThumbstickInputView.swift
new file mode 100644
index 000000000..4a99042bd
--- /dev/null
+++ b/Cores/DeltaCore/Sources/UI/Controller/ThumbstickInputView.swift
@@ -0,0 +1,257 @@
+//
+// ThumbstickInputView.swift
+// DeltaCore
+//
+// Created by Riley Testut on 4/18/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import UIKit
+import simd
+
+extension ThumbstickInputView
+{
+ private enum Direction
+ {
+ case up
+ case down
+ case left
+ case right
+
+ init?(xAxis: Double, yAxis: Double, threshold: Double)
+ {
+ let deadzone = -threshold...threshold
+ switch (xAxis, yAxis)
+ {
+ case (deadzone, deadzone): return nil
+ case (...0, deadzone): self = .left
+ case (0..., deadzone): self = .right
+ case (deadzone, ...0): self = .down
+ case (deadzone, 0...): self = .up
+ default: return nil
+ }
+ }
+ }
+}
+
+class ThumbstickInputView: UIView
+{
+ var isHapticFeedbackEnabled = true
+
+ var valueChangedHandler: ((Double, Double) -> Void)?
+
+ var thumbstickImage: UIImage? {
+ didSet {
+ self.update()
+ }
+ }
+
+ var thumbstickSize: CGSize? {
+ didSet {
+ self.update()
+ }
+ }
+
+ private let imageView = UIImageView(image: nil)
+ private let panGestureRecognizer = ImmediatePanGestureRecognizer(target: nil, action: nil)
+
+ private let lightFeedbackGenerator = UISelectionFeedbackGenerator()
+ private let mediumFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
+
+ private var isActivated = false
+
+ private var trackingOrigin: CGPoint?
+ private var previousDirection: Direction?
+
+ private var isTracking: Bool {
+ return self.trackingOrigin != nil
+ }
+
+ override init(frame: CGRect)
+ {
+ super.init(frame: frame)
+
+ self.panGestureRecognizer.addTarget(self, action: #selector(ThumbstickInputView.handlePanGesture(_:)))
+ self.panGestureRecognizer.delaysTouchesBegan = true
+ self.panGestureRecognizer.cancelsTouchesInView = true
+ self.addGestureRecognizer(self.panGestureRecognizer)
+
+ self.addSubview(self.imageView)
+
+ self.update()
+ }
+
+ required init?(coder aDecoder: NSCoder)
+ {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func layoutSubviews()
+ {
+ super.layoutSubviews()
+
+ self.update()
+ }
+}
+
+private extension ThumbstickInputView
+{
+ @objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer)
+ {
+ switch gestureRecognizer.state
+ {
+ case .began:
+ let location = gestureRecognizer.location(in: self)
+ self.trackingOrigin = location
+
+ if self.isHapticFeedbackEnabled
+ {
+ self.lightFeedbackGenerator.prepare()
+ self.mediumFeedbackGenerator.prepare()
+ }
+
+ self.update()
+
+ case .changed:
+ // When initially tracking the gesture, we calculate the translation
+ // relative to where the user began the pan gesture.
+ // This works well, but becomes weird once we leave the bounds then return later,
+ // since it's more obvious at that point if the thumbstick position doesn't match the user's finger.
+ //
+ // To compensate, once we've left the bounds (and have reached maximum translation),
+ // we reset the origin we're using for calculation to 0.
+ // This won't change the visual position of the thumbstick since it's snapped to the edge,
+ // but will correctly track user's finger upon re-entering the bounds.
+
+ guard var origin = self.trackingOrigin else { break }
+
+ let location = gestureRecognizer.location(in: self)
+ let translationX = location.x - origin.x
+ let translationY = location.y - origin.y
+
+ let x = origin.x + translationX
+ let y = origin.y + translationY
+
+ let horizontalRange = self.bounds.minX...self.bounds.maxX
+ let verticalRange = self.bounds.minY...self.bounds.maxY
+
+ if !horizontalRange.contains(x) && abs(translationX) >= self.bounds.midX
+ {
+ origin.x = self.bounds.midX
+ }
+
+ if !verticalRange.contains(y) && abs(translationY) >= self.bounds.midY
+ {
+ origin.y = self.bounds.midY
+ }
+
+ let translation = CGPoint(x: translationX, y: translationY)
+ self.update(translation)
+
+ self.trackingOrigin = origin
+
+ case .ended, .cancelled:
+
+ if self.isHapticFeedbackEnabled
+ {
+ self.mediumFeedbackGenerator.impactOccurred()
+ }
+
+ self.update()
+
+ self.trackingOrigin = nil
+ self.isActivated = false
+ self.previousDirection = nil
+
+ default: break
+ }
+ }
+
+ func update(_ translation: CGPoint = CGPoint(x: 0, y: 0))
+ {
+ let center = SIMD2(Double(self.bounds.midX), Double(self.bounds.midY))
+ let point = SIMD2(Double(translation.x), Double(translation.y))
+
+ self.imageView.image = self.thumbstickImage
+
+ if let size = self.thumbstickSize
+ {
+ self.imageView.bounds.size = CGSize(width: size.width, height: size.height)
+ }
+ else
+ {
+ self.imageView.sizeToFit()
+ }
+
+ guard !self.bounds.isEmpty, self.isTracking else {
+ self.imageView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
+ return
+ }
+
+ let maximumDistance = Double(self.bounds.midX)
+ let distance = min(simd_length(point), maximumDistance)
+
+ let angle = atan2(point.y, point.x)
+
+ var adjustedX = distance * cos(angle)
+ adjustedX += center.x
+
+ var adjustedY = distance * sin(angle)
+ adjustedY += center.y
+
+ let insetSideLength = maximumDistance / sqrt(2)
+ let insetFrame = CGRect(x: center.x - insetSideLength / 2,
+ y: center.y - insetSideLength / 2,
+ width: insetSideLength,
+ height: insetSideLength)
+
+ let threshold = 0.1
+
+ var xAxis = Double((CGFloat(adjustedX) - insetFrame.minX) / insetFrame.width)
+ xAxis = max(xAxis, 0)
+ xAxis = min(xAxis, 1)
+ xAxis = (xAxis * 2) - 1 // Convert range from [0, 1] to [-1, 1].
+
+ if abs(xAxis) < threshold
+ {
+ xAxis = 0
+ }
+
+ var yAxis = Double((CGFloat(adjustedY) - insetFrame.minY) / insetFrame.height)
+ yAxis = max(yAxis, 0)
+ yAxis = min(yAxis, 1)
+ yAxis = -((yAxis * 2) - 1) // Convert range from [0, 1] to [-1, 1], then invert it (due to flipped coordinates).
+
+ if abs(yAxis) < threshold
+ {
+ yAxis = 0
+ }
+
+ let magnitude = simd_length(SIMD2(xAxis, yAxis))
+ let isActivated = (magnitude > 0.1)
+
+ if let direction = Direction(xAxis: xAxis, yAxis: yAxis, threshold: threshold)
+ {
+ if self.previousDirection != direction && self.isHapticFeedbackEnabled
+ {
+ self.mediumFeedbackGenerator.impactOccurred()
+ }
+
+ self.previousDirection = direction
+ }
+ else
+ {
+ if isActivated && !self.isActivated && self.isHapticFeedbackEnabled
+ {
+ self.lightFeedbackGenerator.selectionChanged()
+ }
+
+ self.previousDirection = nil
+ }
+
+ self.isActivated = isActivated
+
+ self.imageView.center = CGPoint(x: adjustedX, y: adjustedY)
+ self.valueChangedHandler?(xAxis, yAxis)
+ }
+}
diff --git a/Cores/DeltaCore/Sources/UI/Controller/TouchControllerSkin.swift b/Cores/DeltaCore/Sources/UI/Controller/TouchControllerSkin.swift
new file mode 100644
index 000000000..7736b3ed0
--- /dev/null
+++ b/Cores/DeltaCore/Sources/UI/Controller/TouchControllerSkin.swift
@@ -0,0 +1,204 @@
+//
+// TouchControllerSkin.swift
+// DeltaCore
+//
+// Created by Riley Testut on 12/1/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+import AVFoundation
+import UIKit
+
+extension TouchControllerSkin
+{
+ public enum LayoutAxis
+ {
+ case vertical
+ case horizontal
+ }
+}
+
+public struct TouchControllerSkin
+{
+ public var name: String { "TouchControllerSkin" }
+ public var identifier: String { "com.delta.TouchControllerSkin" }
+ public var gameType: GameType { self.controllerSkin.gameType }
+ public var isDebugModeEnabled: Bool { false }
+
+ public var screenLayoutAxis: LayoutAxis = .vertical
+ public var layoutGuide: UILayoutGuide?
+
+ private let controllerSkin: ControllerSkin
+
+ public init(controllerSkin: ControllerSkin)
+ {
+ self.controllerSkin = controllerSkin
+ }
+}
+
+extension TouchControllerSkin: ControllerSkinProtocol
+{
+ public func supports(_ traits: ControllerSkin.Traits) -> Bool
+ {
+ return true
+ }
+
+ public func image(for traits: ControllerSkin.Traits, preferredSize: ControllerSkin.Size) -> UIImage?
+ {
+ return nil
+ }
+
+ public func thumbstick(for item: ControllerSkin.Item, traits: ControllerSkin.Traits, preferredSize: ControllerSkin.Size) -> (UIImage, CGSize)?
+ {
+ return nil
+ }
+
+ public func inputs(for traits: ControllerSkin.Traits, at point: CGPoint) -> [Input]?
+ {
+ // Return empty array since touch inputs are normally filtered out.
+ return []
+ }
+
+ public func items(for traits: ControllerSkin.Traits) -> [ControllerSkin.Item]?
+ {
+ guard
+ let fullScreenFrame = self.fullScreenFrame(for: traits),
+ var touchScreenItem = self.controllerSkin.items(for: traits)?.first(where: { $0.kind == .touchScreen })
+ else { return nil }
+
+ let touchScreenFrame: CGRect
+
+ if let touchScreenIndex = self.controllerSkin.screens(for: traits)?.firstIndex(where: { touchScreenItem.frame.contains($0.outputFrame) }),
+ let updatedScreen = self.screens(for: traits)?[touchScreenIndex]
+ {
+ // This (original) screen is completely covered by a touch screen input,
+ // therefore we assume it's a touch screen and use its (updated) frame.
+ touchScreenFrame = updatedScreen.outputFrame
+ }
+ else
+ {
+ // The touch screen (if one exists) is partially covering the main screen,
+ // so calculate the intersection to determine where to place touch screen.
+
+ var screenFrame = self.controllerSkin.gameScreenFrame(for: traits) ?? fullScreenFrame
+ var itemFrame = touchScreenItem.frame
+
+ // Align itemFrame relative to screenFrame's origin.
+ itemFrame.origin.x -= screenFrame.minX
+ itemFrame.origin.y -= screenFrame.minY
+ screenFrame.origin = .zero
+
+ var intersection = screenFrame.intersection(itemFrame)
+ intersection.origin.x /= screenFrame.width
+ intersection.origin.y /= screenFrame.height
+ intersection.size.width /= screenFrame.width
+ intersection.size.height /= screenFrame.height
+
+ intersection.origin.x *= fullScreenFrame.width
+ intersection.origin.y *= fullScreenFrame.height
+ intersection.size.width *= fullScreenFrame.width
+ intersection.size.height *= fullScreenFrame.height
+
+ // Translate intersection back to correct location.
+ intersection.origin.x += fullScreenFrame.minX
+ intersection.origin.y += fullScreenFrame.minY
+
+ touchScreenFrame = intersection
+ }
+
+ touchScreenItem.frame = touchScreenFrame
+ touchScreenItem.extendedFrame = touchScreenFrame
+ return [touchScreenItem]
+ }
+
+ public func isTranslucent(for traits: ControllerSkin.Traits) -> Bool?
+ {
+ return false
+ }
+
+ public func screens(for traits: ControllerSkin.Traits) -> [ControllerSkin.Screen]?
+ {
+ guard let fullScreenFrame = self.fullScreenFrame(for: traits) else { return nil }
+
+ let screensCount = CGFloat(self.controllerSkin.screens(for: traits)?.count ?? 0)
+
+ let screens = self.controllerSkin.screens(for: traits)?.enumerated().map { (index, screen) -> ControllerSkin.Screen in
+ var screen = screen
+
+ switch self.screenLayoutAxis
+ {
+ case .horizontal:
+ let length = fullScreenFrame.width / screensCount
+ screen.outputFrame = CGRect(x: fullScreenFrame.minX + length * CGFloat(index), y: fullScreenFrame.minY, width: length, height: fullScreenFrame.height)
+
+ case .vertical:
+ let length = fullScreenFrame.height / screensCount
+ screen.outputFrame = CGRect(x: fullScreenFrame.minX, y: fullScreenFrame.minY + length * CGFloat(index), width: fullScreenFrame.width, height: length)
+ }
+
+ return screen
+ }
+
+ return screens
+ }
+
+ public func aspectRatio(for traits: ControllerSkin.Traits) -> CGSize?
+ {
+ return self.controllerSkin.aspectRatio(for: traits)
+ }
+}
+
+private extension TouchControllerSkin
+{
+ func fullScreenFrame(for traits: ControllerSkin.Traits) -> CGRect?
+ {
+ // Assumes skin is a full screen skin.
+
+ guard let mappingSize = self.aspectRatio(for: traits), let core = Delta.core(for: self.gameType) else { return nil }
+
+ let fullScreenSize: CGSize
+
+ if let screens = self.controllerSkin.screens(for: traits), let outputFrame = screens.first?.outputFrame
+ {
+ var size = CGSize(width: outputFrame.width * mappingSize.width, height: outputFrame.height * mappingSize.height)
+
+ // Assume screens are the same size.
+ switch self.screenLayoutAxis
+ {
+ case .horizontal: size.width *= CGFloat(screens.count)
+ case .vertical: size.height *= CGFloat(screens.count)
+ }
+
+ fullScreenSize = size
+ }
+ else
+ {
+ fullScreenSize = core.videoFormat.dimensions
+ }
+
+ let layoutFrame: CGRect
+ let containingSize: CGSize
+
+ if let layoutGuide = self.layoutGuide, let bounds = layoutGuide.owningView?.bounds
+ {
+ var frame = layoutGuide.layoutFrame
+ frame.size.height = bounds.height - frame.minY // HACK: Ignore bottom insets to prevent home indicator messing up calculations.
+
+ layoutFrame = frame
+ containingSize = bounds.size
+ }
+ else
+ {
+ layoutFrame = CGRect(origin: .zero, size: mappingSize)
+ containingSize = layoutFrame.size
+ }
+
+ var screenFrame = AVMakeRect(aspectRatio: fullScreenSize, insideRect: layoutFrame)
+ screenFrame.origin.x /= containingSize.width
+ screenFrame.origin.y /= containingSize.height
+ screenFrame.size.width /= containingSize.width
+ screenFrame.size.height /= containingSize.height
+
+ return screenFrame
+ }
+}
diff --git a/Cores/DeltaCore/Sources/UI/Controller/TouchInputView.swift b/Cores/DeltaCore/Sources/UI/Controller/TouchInputView.swift
new file mode 100644
index 000000000..142bab58d
--- /dev/null
+++ b/Cores/DeltaCore/Sources/UI/Controller/TouchInputView.swift
@@ -0,0 +1,55 @@
+//
+// TouchInputView.swift
+// DeltaCore
+//
+// Created by Riley Testut on 8/4/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import UIKit
+
+class TouchInputView: UIView
+{
+ var valueChangedHandler: ((CGPoint?) -> Void)?
+
+ override init(frame: CGRect)
+ {
+ super.init(frame: frame)
+
+ self.isMultipleTouchEnabled = false
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func touchesBegan(_ touches: Set, with event: UIEvent?)
+ {
+ // Manually track touches because UIPanGestureRecognizer delays touches near screen edges,
+ // even if we've opted to de-prioritize system gestures.
+ self.touchesMoved(touches, with: event)
+ }
+
+ override func touchesMoved(_ touches: Set, with event: UIEvent?)
+ {
+ guard let touch = touches.first else { return }
+
+ let location = touch.location(in: self)
+
+ var adjustedLocation = CGPoint(x: location.x / self.bounds.width, y: location.y / self.bounds.height)
+ adjustedLocation.x = min(max(adjustedLocation.x, 0), 1)
+ adjustedLocation.y = min(max(adjustedLocation.y, 0), 1)
+
+ self.valueChangedHandler?(adjustedLocation)
+ }
+
+ override func touchesCancelled(_ touches: Set, with event: UIEvent?)
+ {
+ self.touchesEnded(touches, with: event)
+ }
+
+ override func touchesEnded(_ touches: Set, with event: UIEvent?)
+ {
+ self.valueChangedHandler?(nil)
+ }
+}
diff --git a/Cores/DeltaCore/Sources/UI/Game/GameView.swift b/Cores/DeltaCore/Sources/UI/Game/GameView.swift
new file mode 100644
index 000000000..04b127cb5
--- /dev/null
+++ b/Cores/DeltaCore/Sources/UI/Game/GameView.swift
@@ -0,0 +1,209 @@
+//
+// GameView.swift
+// DeltaCore
+//
+// Created by Riley Testut on 3/16/15.
+// Copyright (c) 2015 Riley Testut. All rights reserved.
+//
+
+import UIKit
+import CoreImage
+import GLKit
+import AVFoundation
+
+// Create wrapper class to prevent exposing GLKView (and its annoying deprecation warnings) to clients.
+private class GameViewGLKViewDelegate: NSObject, GLKViewDelegate
+{
+ weak var gameView: GameView?
+
+ init(gameView: GameView)
+ {
+ self.gameView = gameView
+ }
+
+ func glkView(_ view: GLKView, drawIn rect: CGRect)
+ {
+ self.gameView?.glkView(view, drawIn: rect)
+ }
+}
+
+public enum SamplerMode
+{
+ case linear
+ case nearestNeighbor
+}
+
+public class GameView: UIView
+{
+ @NSCopying public var inputImage: CIImage? {
+ didSet {
+ if self.inputImage?.extent != oldValue?.extent
+ {
+ DispatchQueue.main.async {
+ self.setNeedsLayout()
+ }
+ }
+
+ self.update()
+ }
+ }
+
+ @NSCopying public var filter: CIFilter? {
+ didSet {
+ self.update()
+ }
+ }
+
+ public var samplerMode: SamplerMode = .nearestNeighbor {
+ didSet {
+ self.update()
+ }
+ }
+
+ public var outputImage: CIImage? {
+ guard let inputImage = self.inputImage else { return nil }
+
+ var image: CIImage?
+
+ switch self.samplerMode
+ {
+ case .linear: image = inputImage.samplingLinear()
+ case .nearestNeighbor: image = inputImage.samplingNearest()
+ }
+
+ if let filter = self.filter
+ {
+ filter.setValue(image, forKey: kCIInputImageKey)
+ image = filter.outputImage
+ }
+
+ return image
+ }
+
+ internal var eaglContext: EAGLContext {
+ get { return self.glkView.context }
+ set {
+ self.renderQueue.sync {
+ // For some reason, if we don't explicitly set current EAGLContext to nil, assigning
+ // to self.glkView may crash if we've already rendered to a game view.
+ EAGLContext.setCurrent(nil)
+
+ self.glkView.context = EAGLContext(api: .openGLES2, sharegroup: newValue.sharegroup)!
+ self.context = self.makeContext()
+ }
+ }
+ }
+ private lazy var context: CIContext = self.makeContext()
+
+ private let glkView: GLKView
+ private lazy var glkViewDelegate = GameViewGLKViewDelegate(gameView: self)
+
+ private let renderQueue = DispatchQueue(label: "DeltaCore.GameView.renderQueue")
+
+ public override init(frame: CGRect)
+ {
+ let eaglContext = EAGLContext(api: .openGLES2)!
+ self.glkView = GLKView(frame: CGRect.zero, context: eaglContext)
+
+ super.init(frame: frame)
+
+ self.initialize()
+ }
+
+ public required init?(coder aDecoder: NSCoder)
+ {
+ let eaglContext = EAGLContext(api: .openGLES2)!
+ self.glkView = GLKView(frame: CGRect.zero, context: eaglContext)
+
+ super.init(coder: aDecoder)
+
+ self.initialize()
+ }
+
+ private func initialize()
+ {
+ self.glkView.frame = self.bounds
+ self.glkView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ self.glkView.delegate = self.glkViewDelegate
+ self.glkView.enableSetNeedsDisplay = false
+ self.addSubview(self.glkView)
+ }
+
+ public override func didMoveToWindow()
+ {
+ if let window = self.window
+ {
+ self.glkView.contentScaleFactor = window.screen.scale
+ self.update()
+ }
+ }
+
+ public override func layoutSubviews()
+ {
+ super.layoutSubviews()
+
+ self.glkView.isHidden = (self.outputImage == nil)
+ }
+}
+
+public extension GameView
+{
+ func snapshot() -> UIImage?
+ {
+ // Unfortunately, rendering CIImages doesn't always work when backed by an OpenGLES texture.
+ // As a workaround, we simply render the view itself into a graphics context the same size
+ // as our output image.
+ //
+ // let cgImage = self.context.createCGImage(outputImage, from: outputImage.extent)
+
+ guard let outputImage = self.outputImage else { return nil }
+
+ let rect = CGRect(origin: .zero, size: outputImage.extent.size)
+
+ let format = UIGraphicsImageRendererFormat()
+ format.scale = 1.0
+ format.opaque = true
+
+ let renderer = UIGraphicsImageRenderer(size: rect.size, format: format)
+
+ let snapshot = renderer.image { (context) in
+ self.glkView.drawHierarchy(in: rect, afterScreenUpdates: false)
+ }
+
+ return snapshot
+ }
+}
+
+private extension GameView
+{
+ func makeContext() -> CIContext
+ {
+ let context = CIContext(eaglContext: self.glkView.context, options: [.workingColorSpace: NSNull()])
+ return context
+ }
+
+ func update()
+ {
+ // Calling display when outputImage is nil may crash for OpenGLES-based rendering.
+ guard self.outputImage != nil else { return }
+
+ self.renderQueue.sync {
+ self.glkView.display()
+ }
+ }
+}
+
+private extension GameView
+{
+ func glkView(_ view: GLKView, drawIn rect: CGRect)
+ {
+ glClearColor(0.0, 0.0, 0.0, 1.0)
+ glClear(UInt32(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
+
+ if let outputImage = self.outputImage
+ {
+ let bounds = CGRect(x: 0, y: 0, width: self.glkView.drawableWidth, height: self.glkView.drawableHeight)
+ self.context.draw(outputImage, in: bounds, from: outputImage.extent)
+ }
+ }
+}
diff --git a/Cores/DeltaCore/Sources/UI/Game/GameViewController.swift b/Cores/DeltaCore/Sources/UI/Game/GameViewController.swift
new file mode 100644
index 000000000..1af87a2c8
--- /dev/null
+++ b/Cores/DeltaCore/Sources/UI/Game/GameViewController.swift
@@ -0,0 +1,589 @@
+//
+// GameViewController.swift
+// DeltaCore
+//
+// Created by Riley Testut on 7/4/16.
+// Happy 4th of July, Everyone! 🎉
+// Copyright © 2016 Riley Testut. All rights reserved.
+//
+
+import UIKit
+import AVFoundation
+
+fileprivate extension NSLayoutConstraint
+{
+ class func constraints(aspectFitting view1: UIView, to view2: UIView) -> [NSLayoutConstraint]
+ {
+ let boundingWidthConstraint = view1.widthAnchor.constraint(lessThanOrEqualTo: view2.widthAnchor, multiplier: 1.0)
+ let boundingHeightConstraint = view1.heightAnchor.constraint(lessThanOrEqualTo: view2.heightAnchor, multiplier: 1.0)
+
+ let widthConstraint = view1.widthAnchor.constraint(equalTo: view2.widthAnchor)
+ widthConstraint.priority = .defaultHigh
+
+ let heightConstraint = view1.heightAnchor.constraint(equalTo: view2.heightAnchor)
+ heightConstraint.priority = .defaultHigh
+
+ return [boundingWidthConstraint, boundingHeightConstraint, widthConstraint, heightConstraint]
+ }
+}
+
+public protocol GameViewControllerDelegate: class
+{
+ func gameViewControllerShouldPauseEmulation(_ gameViewController: GameViewController) -> Bool
+ func gameViewControllerShouldResumeEmulation(_ gameViewController: GameViewController) -> Bool
+
+ func gameViewController(_ gameViewController: GameViewController, handleMenuInputFrom gameController: GameController)
+
+ func gameViewControllerDidUpdate(_ gameViewController: GameViewController)
+}
+
+public extension GameViewControllerDelegate
+{
+ func gameViewControllerShouldPauseEmulation(_ gameViewController: GameViewController) -> Bool { return true }
+ func gameViewControllerShouldResumeEmulation(_ gameViewController: GameViewController) -> Bool { return true }
+
+ func gameViewController(_ gameViewController: GameViewController, handleMenuInputFrom gameController: GameController) {}
+
+ func gameViewControllerDidUpdate(_ gameViewController: GameViewController) {}
+}
+
+private var kvoContext = 0
+
+open class GameViewController: UIViewController, GameControllerReceiver
+{
+ open var game: GameProtocol?
+ {
+ didSet
+ {
+ if let game = self.game
+ {
+ self.emulatorCore = EmulatorCore(game: game)
+ }
+ else
+ {
+ self.emulatorCore = nil
+ }
+ }
+ }
+
+ open private(set) var emulatorCore: EmulatorCore?
+ {
+ didSet
+ {
+ oldValue?.stop()
+
+ self.emulatorCore?.updateHandler = { [weak self] core in
+ guard let strongSelf = self else { return }
+ strongSelf.delegate?.gameViewControllerDidUpdate(strongSelf)
+ }
+
+ self.prepareForGame()
+ }
+ }
+
+ open weak var delegate: GameViewControllerDelegate?
+
+ public var gameView: GameView! {
+ return self.gameViews.first
+ }
+ public private(set) var gameViews: [GameView] = []
+
+ open private(set) var controllerView: ControllerView!
+ private var splitViewInputViewHeight: CGFloat = 0
+
+ private let emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.DeltaCore.GameViewController.emulatorCoreQueue", qos: .userInitiated)
+
+ private var _previousControllerSkin: ControllerSkinProtocol?
+ private var _previousControllerSkinTraits: ControllerSkin.Traits?
+
+ /// UIViewController
+ open override var prefersStatusBarHidden: Bool {
+ return true
+ }
+
+ public required init()
+ {
+ super.init(nibName: nil, bundle: nil)
+
+ self.initialize()
+ }
+
+ public required init?(coder aDecoder: NSCoder)
+ {
+ super.init(coder: aDecoder)
+
+ self.initialize()
+ }
+
+ private func initialize()
+ {
+ NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.willResignActive(with:)), name: UIApplication.willResignActiveNotification, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didBecomeActive(with:)), name: UIApplication.didBecomeActiveNotification, object: nil)
+
+ NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.keyboardWillShow(with:)), name: UIResponder.keyboardWillShowNotification, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.keyboardWillChangeFrame(with:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.keyboardWillHide(with:)), name: UIResponder.keyboardWillHideNotification, object: nil)
+ }
+
+ deinit
+ {
+ self.controllerView.removeObserver(self, forKeyPath: #keyPath(ControllerView.isHidden), context: &kvoContext)
+ self.emulatorCore?.stop()
+ }
+
+ // MARK: - UIViewController -
+ /// UIViewController
+ // These would normally be overridden in a public extension, but overriding these methods in subclasses of GameViewController segfaults compiler if so
+
+ open override var prefersHomeIndicatorAutoHidden: Bool
+ {
+ let prefersHomeIndicatorAutoHidden = self.view.bounds.width > self.view.bounds.height
+ return prefersHomeIndicatorAutoHidden
+ }
+
+ open dynamic override func viewDidLoad()
+ {
+ super.viewDidLoad()
+
+ self.view.backgroundColor = UIColor.black
+
+ let gameView = GameView(frame: CGRect.zero)
+ self.view.addSubview(gameView)
+ self.gameViews.append(gameView)
+
+ self.controllerView = ControllerView(frame: CGRect.zero)
+ self.view.addSubview(self.controllerView)
+
+ self.controllerView.addObserver(self, forKeyPath: #keyPath(ControllerView.isHidden), options: [.old, .new], context: &kvoContext)
+ NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateGameViews), name: ControllerView.controllerViewDidChangeControllerSkinNotification, object: self.controllerView)
+
+ let tapGestureRecognizer = UITapGestureRecognizer(target: self.controllerView, action: #selector(ControllerView.becomeFirstResponder))
+ self.view.addGestureRecognizer(tapGestureRecognizer)
+
+ self.prepareForGame()
+ }
+
+ open dynamic override func viewWillAppear(_ animated: Bool)
+ {
+ super.viewWillAppear(animated)
+
+ self.emulatorCoreQueue.async {
+ _ = self._startEmulation()
+ }
+ }
+
+ open dynamic override func viewDidAppear(_ animated: Bool)
+ {
+ super.viewDidAppear(animated)
+
+ UIApplication.delta_shared?.isIdleTimerDisabled = true
+
+ self.controllerView.becomeFirstResponder()
+ }
+
+ open dynamic override func viewDidDisappear(_ animated: Bool)
+ {
+ super.viewDidDisappear(animated)
+
+ UIApplication.delta_shared?.isIdleTimerDisabled = false
+
+ self.emulatorCoreQueue.async {
+ _ = self._pauseEmulation()
+ }
+ }
+
+ override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
+ {
+ super.viewWillTransition(to: size, with: coordinator)
+
+ self.controllerView.beginAnimatingUpdateControllerSkin()
+
+ // Disable VideoManager temporarily to prevent random Metal crashes due to rendering while adjusting layout.
+ let isVideoManagerEnabled = self.emulatorCore?.videoManager.isEnabled ?? true
+ self.emulatorCore?.videoManager.isEnabled = false
+
+ // As of iOS 11, the keyboard NSNotifications may return incorrect values for split view controller input view when rotating device.
+ // As a workaround, we explicitly resign controllerView as first responder, then restore first responder status after rotation.
+ let isControllerViewFirstResponder = self.controllerView.isFirstResponder
+ self.controllerView.resignFirstResponder()
+
+ self.view.setNeedsUpdateConstraints()
+
+ coordinator.animate(alongsideTransition: { (context) in
+ self.updateGameViews()
+ }) { (context) in
+ self.controllerView.finishAnimatingUpdateControllerSkin()
+
+ if isControllerViewFirstResponder
+ {
+ self.controllerView.becomeFirstResponder()
+ }
+
+ // Re-enable VideoManager if necessary.
+ self.emulatorCore?.videoManager.isEnabled = isVideoManagerEnabled
+ }
+ }
+
+ open override func viewDidLayoutSubviews()
+ {
+ super.viewDidLayoutSubviews()
+
+ let screenAspectRatio = self.emulatorCore?.preferredRenderingSize ?? CGSize(width: 1, height: 1)
+
+ let controllerViewFrame: CGRect
+ let availableGameFrame: CGRect
+
+ /* Controller View */
+ switch self.controllerView.controllerSkinTraits
+ {
+ case let traits? where traits.displayType == .splitView:
+ // Split-View:
+ // - Controller View is pinned to bottom and spans width of device as keyboard input view.
+ // - Game View should be vertically centered between top of screen and input view.
+
+ controllerViewFrame = CGRect(x: 0, y: self.view.bounds.maxY, width: self.view.bounds.width, height: 0)
+ (_, availableGameFrame) = self.view.bounds.divided(atDistance: self.splitViewInputViewHeight, from: .maxYEdge)
+
+ case .none: fallthrough
+ case _? where self.controllerView.isHidden:
+ // Controller View Hidden:
+ // - Controller View should have a height of 0.
+ // - Game View should be centered in self.view.
+
+ (controllerViewFrame, availableGameFrame) = self.view.bounds.divided(atDistance: 0, from: .maxYEdge)
+
+ case let traits? where traits.orientation == .portrait:
+ // Portrait:
+ // - Controller View should be pinned to bottom of self.view and centered horizontally.
+ // - Game View should be vertically centered between top of screen and controller view.
+
+ let intrinsicContentSize = self.controllerView.intrinsicContentSize
+ if intrinsicContentSize.height != UIView.noIntrinsicMetric && intrinsicContentSize.width != UIView.noIntrinsicMetric
+ {
+ let controllerViewHeight = (self.view.bounds.width / intrinsicContentSize.width) * intrinsicContentSize.height
+ (controllerViewFrame, availableGameFrame) = self.view.bounds.divided(atDistance: controllerViewHeight, from: .maxYEdge)
+ }
+ else
+ {
+ controllerViewFrame = self.view.bounds
+ availableGameFrame = self.view.bounds
+ }
+
+ case _?:
+ // Landscape:
+ // - Controller View should be centered vertically in view (though most of the time its height will == self.view height).
+ // - Game View should be centered in self.view.
+
+ let intrinsicContentSize = self.controllerView.intrinsicContentSize
+ if intrinsicContentSize.height != UIView.noIntrinsicMetric && intrinsicContentSize.width != UIView.noIntrinsicMetric
+ {
+ controllerViewFrame = AVMakeRect(aspectRatio: intrinsicContentSize, insideRect: self.view.bounds)
+ }
+ else
+ {
+ controllerViewFrame = self.view.bounds
+ }
+
+ availableGameFrame = self.view.bounds
+ }
+
+ self.controllerView.frame = controllerViewFrame
+
+ /* Game View */
+ if
+ let controllerSkin = self.controllerView.controllerSkin,
+ let traits = self.controllerView.controllerSkinTraits,
+ let screens = controllerSkin.screens(for: traits),
+ !self.controllerView.isHidden
+ {
+ for (screen, gameView) in zip(screens, self.gameViews)
+ {
+ let outputFrame = screen.outputFrame.applying(.init(scaleX: self.view.bounds.width, y: self.view.bounds.height))
+ gameView.frame = outputFrame
+ }
+ }
+ else
+ {
+ let gameViewFrame = AVMakeRect(aspectRatio: screenAspectRatio, insideRect: availableGameFrame)
+ self.gameView.frame = gameViewFrame
+ }
+
+ if self.emulatorCore?.state != .running
+ {
+ // WORKAROUND
+ // Sometimes, iOS will cache the rendered image (such as when covered by a UIVisualEffectView), and as a result the game view might appear skewed
+ // To compensate, we manually "refresh" the game screen
+ self.gameView.inputImage = self.gameView.outputImage
+ }
+
+ self.setNeedsUpdateOfHomeIndicatorAutoHidden()
+ }
+
+ // MARK: - KVO -
+ /// KVO
+ open dynamic override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
+ {
+ guard context == &kvoContext else { return super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) }
+
+ // Ensures the value is actually different, or else we might potentially run into an infinite loop if subclasses hide/show controllerView in viewDidLayoutSubviews()
+ guard (change?[.newKey] as? Bool) != (change?[.oldKey] as? Bool) else { return }
+
+ self.view.setNeedsLayout()
+ self.view.layoutIfNeeded()
+ }
+
+ // MARK: - GameControllerReceiver -
+ /// GameControllerReceiver
+ // These would normally be declared in an extension, but non-ObjC compatible methods cannot be overridden if declared in extension :(
+ open func gameController(_ gameController: GameController, didActivate input: Input, value: Double)
+ {
+ guard let standardInput = StandardGameControllerInput(input: input), standardInput == .menu else { return }
+ self.delegate?.gameViewController(self, handleMenuInputFrom: gameController)
+ }
+
+ open func gameController(_ gameController: GameController, didDeactivate input: Input)
+ {
+ // This method intentionally left blank
+ }
+}
+
+// MARK: - Layout -
+/// Layout
+extension GameViewController
+{
+ @objc func updateGameViews()
+ {
+ var previousGameViews = Array(self.gameViews.reversed())
+ var gameViews = [GameView]()
+
+ if
+ let controllerSkin = self.controllerView.controllerSkin,
+ let traits = self.controllerView.controllerSkinTraits,
+ let screens = controllerSkin.screens(for: traits),
+ !self.controllerView.isHidden
+ {
+ for screen in screens
+ {
+ let gameView = previousGameViews.popLast() ?? GameView(frame: .zero)
+
+ var filters = [CIFilter]()
+
+ if let inputFrame = screen.inputFrame
+ {
+ let cropFilter = CIFilter(name: "CICrop", parameters: ["inputRectangle": CIVector(cgRect: inputFrame)])!
+ filters.append(cropFilter)
+ }
+
+ if let screenFilters = screen.filters
+ {
+ filters.append(contentsOf: screenFilters)
+ }
+
+ if filters.isEmpty
+ {
+ gameView.filter = nil
+ }
+ else
+ {
+ // Always use FilterChain since it has additional logic for chained filters.
+ let filterChain = FilterChain(filters: filters)
+ gameView.filter = filterChain
+ }
+
+ let outputFrame = screen.outputFrame.applying(.init(scaleX: self.view.bounds.width, y: self.view.bounds.height))
+ gameView.frame = outputFrame
+
+ gameViews.append(gameView)
+ }
+ }
+ else
+ {
+ for gameView in self.gameViews
+ {
+ gameView.filter = nil
+ }
+
+ gameViews.append(self.gameView)
+ }
+
+ for gameView in gameViews
+ {
+ guard !self.gameViews.contains(gameView) else { continue }
+
+ self.view.insertSubview(gameView, aboveSubview: self.gameView)
+ self.emulatorCore?.add(gameView)
+ }
+
+ for gameView in previousGameViews
+ {
+ guard !gameViews.contains(gameView) else { continue }
+
+ gameView.removeFromSuperview()
+ self.emulatorCore?.remove(gameView)
+ }
+
+ self.gameViews = gameViews
+ self.view.setNeedsLayout()
+ }
+}
+
+// MARK: - Emulation -
+/// Emulation
+public extension GameViewController
+{
+ @discardableResult func startEmulation() -> Bool
+ {
+ return self.emulatorCoreQueue.sync {
+ return self._startEmulation()
+ }
+ }
+
+ @discardableResult func pauseEmulation() -> Bool
+ {
+ return self.emulatorCoreQueue.sync {
+ return self._pauseEmulation()
+ }
+ }
+
+ @discardableResult func resumeEmulation() -> Bool
+ {
+ return self.emulatorCoreQueue.sync {
+ self._resumeEmulation()
+ }
+ }
+}
+
+private extension GameViewController
+{
+ func _startEmulation() -> Bool
+ {
+ guard let emulatorCore = self.emulatorCore else { return false }
+
+ // Toggle audioManager.enabled to reset the audio buffer and ensure the audio isn't delayed from the beginning
+ // This is especially noticeable when peeking a game
+ emulatorCore.audioManager.isEnabled = false
+ emulatorCore.audioManager.isEnabled = true
+
+ return self._resumeEmulation()
+ }
+
+ private func _pauseEmulation() -> Bool
+ {
+ guard let emulatorCore = self.emulatorCore, self.delegate?.gameViewControllerShouldPauseEmulation(self) ?? true else { return false }
+
+ let result = emulatorCore.pause()
+ return result
+ }
+
+ private func _resumeEmulation() -> Bool
+ {
+ guard let emulatorCore = self.emulatorCore, self.delegate?.gameViewControllerShouldResumeEmulation(self) ?? true else { return false }
+
+ DispatchQueue.main.async {
+ if self.view.window != nil
+ {
+ self.controllerView.becomeFirstResponder()
+ }
+ }
+
+ let result: Bool
+
+ switch emulatorCore.state
+ {
+ case .stopped: result = emulatorCore.start()
+ case .paused: result = emulatorCore.resume()
+ case .running: result = true
+ }
+
+ return result
+ }
+}
+
+// MARK: - Preparation -
+private extension GameViewController
+{
+ func prepareForGame()
+ {
+ guard
+ let controllerView = self.controllerView,
+ let emulatorCore = self.emulatorCore,
+ let game = self.game
+ else { return }
+
+ for gameView in self.gameViews
+ {
+ emulatorCore.add(gameView)
+ }
+
+ controllerView.addReceiver(self)
+ controllerView.addReceiver(emulatorCore)
+
+ let controllerSkin = ControllerSkin.standardControllerSkin(for: game.type)
+ controllerView.controllerSkin = controllerSkin
+
+ self.view.setNeedsUpdateConstraints()
+ }
+}
+
+// MARK: - Notifications -
+private extension GameViewController
+{
+ @objc func willResignActive(with notification: Notification)
+ {
+ self.emulatorCoreQueue.async {
+ _ = self._pauseEmulation()
+ }
+ }
+
+ @objc func didBecomeActive(with notification: Notification)
+ {
+ self.emulatorCoreQueue.async {
+ _ = self._resumeEmulation()
+ }
+ }
+
+ @objc func keyboardWillShow(with notification: Notification)
+ {
+ guard let traits = self.controllerView.controllerSkinTraits, traits.displayType == .splitView else { return }
+
+ let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
+ guard keyboardFrame.height > 0 else { return }
+
+ self.splitViewInputViewHeight = keyboardFrame.height
+
+ let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval
+
+ let rawAnimationCurve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int
+ let animationCurve = UIView.AnimationCurve(rawValue: rawAnimationCurve)!
+
+ let animator = UIViewPropertyAnimator(duration: duration, curve: animationCurve) {
+ self.view.setNeedsLayout()
+ self.view.layoutIfNeeded()
+ }
+ animator.startAnimation()
+ }
+
+ @objc func keyboardWillChangeFrame(with notification: Notification)
+ {
+ self.keyboardWillShow(with: notification)
+ }
+
+ @objc func keyboardWillHide(with notification: Notification)
+ {
+ let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
+ guard keyboardFrame.height > 0 else { return }
+
+ let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval
+
+ let rawAnimationCurve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int
+ let animationCurve = UIView.AnimationCurve(rawValue: rawAnimationCurve)!
+
+ self.splitViewInputViewHeight = 0
+
+ let animator = UIViewPropertyAnimator(duration: duration, curve: animationCurve) {
+ self.view.setNeedsLayout()
+ self.view.layoutIfNeeded()
+ }
+ animator.startAnimation()
+ }
+}
diff --git a/Cores/MelonDSDeltaCore b/Cores/MelonDSDeltaCore
deleted file mode 160000
index 92fd10895..000000000
--- a/Cores/MelonDSDeltaCore
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 92fd10895a0821ad9455ea5a2d0494c5df8ebe54
diff --git a/Cores/MelonDSDeltaCore/Derived/InfoPlists/MelonDSDeltaCore.plist b/Cores/MelonDSDeltaCore/Derived/InfoPlists/MelonDSDeltaCore.plist
new file mode 100644
index 000000000..323e5ecfc
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/Derived/InfoPlists/MelonDSDeltaCore.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+
+
diff --git a/Cores/MelonDSDeltaCore/Derived/Sources/Bundle+MelonDSDeltaCore.swift b/Cores/MelonDSDeltaCore/Derived/Sources/Bundle+MelonDSDeltaCore.swift
new file mode 100644
index 000000000..88b687b53
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/Derived/Sources/Bundle+MelonDSDeltaCore.swift
@@ -0,0 +1,23 @@
+// swiftlint:disable all
+import Foundation
+
+// MARK: - Swift Bundle Accessor
+
+private class BundleFinder {}
+
+extension Foundation.Bundle {
+ /// Since MelonDSDeltaCore is a framework, the bundle containing the resources is copied into the final product.
+ static var module: Bundle = {
+ return Bundle(for: BundleFinder.self)
+ }()
+}
+
+// MARK: - Objective-C Bundle Accessor
+
+@objc
+public class MelonDSDeltaCoreResources: NSObject {
+ @objc public class var bundle: Bundle {
+ return .module
+ }
+}
+// swiftlint:enable all
\ No newline at end of file
diff --git a/Cores/MelonDSDeltaCore/Project.swift b/Cores/MelonDSDeltaCore/Project.swift
new file mode 100644
index 000000000..b0c9f4eab
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/Project.swift
@@ -0,0 +1,52 @@
+import ProjectDescription
+
+let project = Project(name: "MelonDSDeltaCore",
+ packages: [],
+ targets: [
+ Target(name: "MelonDSDeltaCore",
+ platform: .iOS,
+ product: .framework,
+ bundleId: "com.rileytestut.melonDSDeltaCore",
+ deploymentTarget: .iOS(targetVersion: "12.2", devices: [.iphone, .ipad]),
+ infoPlist: .extendingDefault(with: [:]),
+ sources: ["Sources/**/*.swift", "Sources/Bridge/MelonDSEmulatorBridge.{h,mm}"],
+ resources: ["Resources/**/*.{deltamapping,deltaskin}"],
+ headers: Headers(public: ["Sources/MelonDSDeltaCore.h", "Sources/Bridge/MelonDSEmulatorBridge.h"], project: "Sources/Types/MelonDSTypes.h"),
+ dependencies: [
+ .project(target: "DeltaCore", path: "../DeltaCore"),
+ .target(name: "libMelonDS")
+ ],
+ settings: Settings(base: [
+ "GCC_PREPROCESSOR_DEFINITIONS": "$(inherited) JIT_ENABLED=1",
+ "USER_HEADER_SEARCH_PATHS": "\"$(SRCROOT)\"",
+ ])),
+ Target(name: "libMelonDS",
+ platform: .iOS,
+ product: .staticLibrary,
+ bundleId: "com.rileytestut.libMelonDS",
+ deploymentTarget: .iOS(targetVersion: "13.4", devices: [.iphone, .ipad]),
+ infoPlist: .extendingDefault(with: [:]),
+ sources: [
+ "melonDS/src/frontend/qt_sdl/PlatformConfig.{h,cpp}",
+ "melonDS/src/tiny-AES-c/*.{h,hpp,c}",
+ "melonDS/src/ARMJIT_A64/*.{h,cpp,s}",
+ "melonDS/src/dolphin/Arm64Emitter.{h,cpp}",
+ "melonDS/src/xxhash/*.{h,c}",
+ SourceFileGlob("melonDS/src/*.{h,hpp,cpp}", excluding: [
+ "melonDS/src/GPU3D_OpenGL.cpp",
+ "melonDS/src/OpenGLSupport.cpp",
+ "melonDS/src/GPU_OpenGL.cpp"
+ ])
+ ],
+ headers: Headers(project: [
+ "melonDS/src/*.h",
+ "melonDS/src/frontend/qt_sdl/PlatformConfig.h",
+ "melonDS/src/tiny-AES-c/*.{h,hpp}",
+ "melonDS/src/ARMJIT_A64/*.h",
+ "melonDS/src/dolphin/Arm64Emitter.h",
+ "melonDS/src/xxhash/*.h"
+ ]),
+ settings: Settings(base: [
+ "GCC_PREPROCESSOR_DEFINITIONS": "$(inherited) JIT_ENABLED=1",
+ ])),
+ ])
diff --git a/Cores/MelonDSDeltaCore/Resources/Standard.deltamapping b/Cores/MelonDSDeltaCore/Resources/Standard.deltamapping
new file mode 100644
index 000000000..4f87c0f39
Binary files /dev/null and b/Cores/MelonDSDeltaCore/Resources/Standard.deltamapping differ
diff --git a/Cores/MelonDSDeltaCore/Resources/Standard.deltaskin b/Cores/MelonDSDeltaCore/Resources/Standard.deltaskin
new file mode 100644
index 000000000..8f5adc4dc
Binary files /dev/null and b/Cores/MelonDSDeltaCore/Resources/Standard.deltaskin differ
diff --git a/Cores/MelonDSDeltaCore/Resources/info.json b/Cores/MelonDSDeltaCore/Resources/info.json
new file mode 100644
index 000000000..0ed555b18
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/Resources/info.json
@@ -0,0 +1,921 @@
+{
+ "name" : "Standard DS",
+ "identifier" : "com.delta.ds.standard",
+ "gameTypeIdentifier" : "com.rileytestut.delta.game.ds",
+ "debug" : false,
+ "representations" : {
+ "iphone" : {
+ "standard" : {
+ "portrait" : {
+ "assets" : {
+ "resizable" : "iphone_portrait.pdf"
+ },
+ "items": [
+ {
+ "inputs": {
+ "up": "up",
+ "down": "down",
+ "left": "left",
+ "right": "right"
+ },
+ "frame": {
+ "x": 15,
+ "y": 500,
+ "width": 128,
+ "height": 128
+ },
+ "extendedEdges": {
+ "top": 15,
+ "bottom": 15,
+ "left": 17,
+ "right": 10
+ }
+ },
+ {
+ "inputs": [
+ "a"
+ ],
+ "frame": {
+ "x": 313,
+ "y": 540,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 16
+ }
+ },
+ {
+ "inputs": [
+ "b"
+ ],
+ "frame": {
+ "x": 267,
+ "y": 586,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "l"
+ ],
+ "frame": {
+ "x": 18,
+ "y": 366,
+ "width": 32,
+ "height": 112
+ },
+ "extendedEdges": {
+ "right": 0,
+ "left": 18
+ }
+ },
+ {
+ "inputs": [
+ "r"
+ ],
+ "frame": {
+ "x": 325,
+ "y": 366,
+ "width": 32,
+ "height": 112
+ },
+ "extendedEdges": {
+ "left": 0,
+ "right": 18
+ }
+ },
+ {
+ "inputs": [
+ "start"
+ ],
+ "frame": {
+ "x": 154,
+ "y": 600,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "top": 15,
+ "bottom": 0,
+ "left": 0,
+ "right": 45
+ }
+ },
+ {
+ "inputs": [
+ "select"
+ ],
+ "frame": {
+ "x": 154,
+ "y": 631,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 15,
+ "left": 0,
+ "right": 45
+ }
+ },
+ {
+ "inputs": [
+ "menu"
+ ],
+ "frame": {
+ "x": 154,
+ "y": 498,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "left": 0,
+ "right": 45
+ }
+ },
+ {
+ "inputs": [
+ "y"
+ ],
+ "frame": {
+ "x": 221,
+ "y": 540,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "x"
+ ],
+ "frame": {
+ "x": 267,
+ "y": 494,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": {
+ "x": "touchScreenX",
+ "y": "touchScreenY"
+ },
+ "frame": {
+ "x": 50,
+ "y": 252,
+ "width": 275,
+ "height": 206
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ }
+ ],
+ "screens": [
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 0,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 50,
+ "y": 18,
+ "width": 275,
+ "height": 206
+ }
+ },
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 192,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 50,
+ "y": 252,
+ "width": 275,
+ "height": 206
+ }
+ }
+ ],
+ "mappingSize" : {
+ "width" : 375,
+ "height" : 667
+ },
+ "extendedEdges" : {
+ "top" : 7,
+ "bottom" : 7,
+ "left" : 7,
+ "right" : 7
+ }
+ },
+ "landscape" : {
+ "assets" : {
+ "resizable" : "iphone_landscape.pdf"
+ },
+ "items": [
+ {
+ "inputs": {
+ "up": "up",
+ "down": "down",
+ "left": "left",
+ "right": "right"
+ },
+ "frame": {
+ "x": 15,
+ "y": 235,
+ "width": 123,
+ "height": 123
+ },
+ "extendedEdges": {
+ "top": 15,
+ "bottom": 20,
+ "left": 20,
+ "right": 20
+ }
+ },
+ {
+ "inputs": [
+ "a"
+ ],
+ "frame": {
+ "x": 606,
+ "y": 273,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 30
+ }
+ },
+ {
+ "inputs": [
+ "b"
+ ],
+ "frame": {
+ "x": 562,
+ "y": 318,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "l"
+ ],
+ "frame": {
+ "x": 173,
+ "y": 219,
+ "width": 135,
+ "height": 33
+ },
+ "extendedEdges": {
+ "top": 0
+ }
+ },
+ {
+ "inputs": [
+ "r"
+ ],
+ "frame": {
+ "x": 359,
+ "y": 219,
+ "width": 135,
+ "height": 33
+ },
+ "extendedEdges": {
+ "top": 0
+ }
+ },
+ {
+ "inputs": [
+ "start"
+ ],
+ "frame": {
+ "x": 304,
+ "y": 287,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "right": 50
+ }
+ },
+ {
+ "inputs": [
+ "select"
+ ],
+ "frame": {
+ "x": 410,
+ "y": 288,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "right": 50
+ }
+ },
+ {
+ "inputs": [
+ "menu"
+ ],
+ "frame": {
+ "x": 195,
+ "y": 288,
+ "width": 18,
+ "height": 18
+ },
+ "extendedEdges": {
+ "right": 50
+ }
+ },
+ {
+ "inputs": [
+ "x"
+ ],
+ "frame": {
+ "x": 562,
+ "y": 229,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 8,
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "y"
+ ],
+ "frame": {
+ "x": 517,
+ "y": 273,
+ "width": 47,
+ "height": 47
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": {
+ "x": "touchScreenX",
+ "y": "touchScreenY"
+ },
+ "frame": {
+ "x": 361,
+ "y": 24,
+ "width": 259,
+ "height": 194
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ }
+ ],
+ "mappingSize" : {
+ "width" : 667,
+ "height" : 375
+ },
+ "screens": [
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 0,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 47,
+ "y": 24,
+ "width": 259,
+ "height": 194
+ }
+ },
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 192,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 361,
+ "y": 24,
+ "width": 259,
+ "height": 194
+ }
+ }
+ ],
+ "extendedEdges" : {
+ "top" : 15,
+ "bottom" : 15,
+ "left" : 15,
+ "right" : 15
+ }
+ }
+ },
+ "edgeToEdge" : {
+ "portrait" : {
+ "assets" : {
+ "resizable" : "iphone_edgetoedge_portrait.pdf"
+ },
+ "items" : [
+ {
+ "inputs" : {
+ "up" : "up",
+ "down" : "down",
+ "left" : "left",
+ "right" : "right"
+ },
+ "frame" : {
+ "x" : 17,
+ "y" : 698,
+ "width" : 141,
+ "height" : 141
+ },
+ "extendedEdges" : {
+ "top" : 15,
+ "bottom" : 15,
+ "left" : 17,
+ "right" : 10
+ }
+ },
+ {
+ "inputs" : [
+ "a"
+ ],
+ "frame" : {
+ "x" : 346,
+ "y" : 743,
+ "width" : 52,
+ "height" : 52
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "bottom" : 0,
+ "left" : 0,
+ "right" : 16
+ }
+ },
+ {
+ "inputs" : [
+ "b"
+ ],
+ "frame" : {
+ "x" : 295,
+ "y" : 794,
+ "width" : 52,
+ "height" : 52
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "left" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs" : [
+ "l"
+ ],
+ "frame" : {
+ "x" : 23,
+ "y" : 630,
+ "width" : 127,
+ "height" : 35
+ }
+ ,
+ "extendedEdges": {
+ "top": 0,
+ "left": 22
+ }
+ },
+ {
+ "inputs" : [
+ "r"
+ ],
+ "frame" : {
+ "x" : 263,
+ "y" : 630,
+ "width" : 127,
+ "height" : 35
+ }
+ ,
+ "extendedEdges": {
+ "top": 0,
+ "right": 22
+ }
+ },
+ {
+ "inputs" : [
+ "start"
+ ],
+ "frame" : {
+ "x" : 171,
+ "y" : 808,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges" : {
+ "top": 15,
+ "bottom": 0,
+ "left": 0,
+ "right" : 45
+ }
+ },
+ {
+ "inputs" : [
+ "select"
+ ],
+ "frame" : {
+ "x" : 171,
+ "y" : 844,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 15,
+ "left": 0,
+ "right": 45
+ }
+ },
+ {
+ "inputs" : [
+ "menu"
+ ],
+ "frame" : {
+ "x" : 170,
+ "y" : 697,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges": {
+ "left": 0,
+ "right": 45
+ }
+ },
+ {
+ "inputs" : [
+ "y"
+ ],
+ "frame" : {
+ "x" : 244,
+ "y" : 743,
+ "width" : 52,
+ "height" : 52
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "bottom" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs" : [
+ "x"
+ ],
+ "frame" : {
+ "x" : 295,
+ "y" : 692,
+ "width" : 52,
+ "height" : 52
+ },
+ "extendedEdges" : {
+ "bottom" : 0,
+ "left" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs": {
+ "x": "touchScreenX",
+ "y": "touchScreenY"
+ },
+ "frame": {
+ "x": 25,
+ "y": 366,
+ "width": 363,
+ "height": 273
+ }
+ ,
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ }
+ ],
+ "screens": [
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 0,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 25,
+ "y": 54,
+ "width": 363,
+ "height": 273
+ }
+ },
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 192,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 25,
+ "y": 366,
+ "width": 363,
+ "height": 273
+ }
+ }
+ ],
+ "mappingSize" : {
+ "width" : 414,
+ "height" : 896
+ },
+ "extendedEdges" : {
+ "top" : 7,
+ "bottom" : 7,
+ "left" : 7,
+ "right" : 7
+ }
+ },
+ "landscape" : {
+ "assets" : {
+ "resizable" : "iphone_edgetoedge_landscape.pdf"
+ },
+ "items" : [
+ {
+ "inputs" : {
+ "up" : "up",
+ "down" : "down",
+ "left" : "left",
+ "right" : "right"
+ },
+ "frame" : {
+ "x" : 69,
+ "y" : 260,
+ "width" : 136,
+ "height" : 136
+ },
+ "extendedEdges" : {
+ "left" : 30,
+ "top" : 20,
+ "right" : 20,
+ "bottom" : 20
+ }
+ },
+ {
+ "inputs" : [
+ "a"
+ ],
+ "frame" : {
+ "x" : 777,
+ "y" : 302,
+ "width" : 51,
+ "height" : 51
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "bottom" : 0,
+ "left" : 0,
+ "right" : 30
+ }
+ },
+ {
+ "inputs" : [
+ "b"
+ ],
+ "frame" : {
+ "x" : 728,
+ "y" : 351,
+ "width" : 51,
+ "height" : 51
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "left" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs" : [
+ "l"
+ ],
+ "frame" : {
+ "x" : 69,
+ "y" : 65,
+ "width" : 40,
+ "height" : 148
+ }
+ ,
+ "extendedEdges": {
+ "left": 50,
+ "right": 0
+ }
+ },
+ {
+ "inputs" : [
+ "r"
+ ],
+ "frame" : {
+ "x" : 788,
+ "y" : 65,
+ "width" : 40,
+ "height" : 148
+ }
+ ,
+ "extendedEdges": {
+ "left": 0,
+ "right": 50
+ }
+ },
+ {
+ "inputs" : [
+ "start"
+ ],
+ "frame" : {
+ "x" : 415,
+ "y" : 315,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges": {
+ "right": 50
+ }
+ },
+ {
+ "inputs" : [
+ "select"
+ ],
+ "frame" : {
+ "x" : 533,
+ "y" : 315,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges" : {
+ "right" : 50
+ }
+ },
+ {
+ "inputs" : [
+ "menu"
+ ],
+ "frame" : {
+ "x" : 295,
+ "y" : 315,
+ "width" : 20,
+ "height" : 20
+ },
+ "extendedEdges": {
+ "right": 50
+ }
+ },
+ {
+ "inputs" : [
+ "x"
+ ],
+ "frame" : {
+ "x" : 728,
+ "y" : 253,
+ "width" : 51,
+ "height" : 51
+ },
+ "extendedEdges" : {
+ "bottom" : 0,
+ "left" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs" : [
+ "y"
+ ],
+ "frame" : {
+ "x" : 679,
+ "y" : 302,
+ "width" : 51,
+ "height" : 51
+ },
+ "extendedEdges" : {
+ "top" : 0,
+ "bottom" : 0,
+ "right" : 0
+ }
+ },
+ {
+ "inputs": {
+ "x": "touchScreenX",
+ "y": "touchScreenY"
+ },
+ "frame": {
+ "x": 500,
+ "y": 27,
+ "width": 286,
+ "height": 214
+ }
+ ,
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ }
+ ],
+ "mappingSize" : {
+ "width" : 896,
+ "height" : 414
+ },
+ "screens": [
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 0,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 110,
+ "y": 27,
+ "width": 286,
+ "height": 214
+ }
+ },
+ {
+ "inputFrame": {
+ "x": 0,
+ "y": 192,
+ "width": 256,
+ "height": 192
+ },
+ "outputFrame": {
+ "x": 500,
+ "y": 27,
+ "width": 286,
+ "height": 214
+ }
+ }
+ ],
+ "extendedEdges" : {
+ "top" : 15,
+ "bottom" : 15,
+ "left" : 15,
+ "right" : 15
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cores/MelonDSDeltaCore/Resources/iphone_edgetoedge_landscape.pdf b/Cores/MelonDSDeltaCore/Resources/iphone_edgetoedge_landscape.pdf
new file mode 100644
index 000000000..949dc7b64
Binary files /dev/null and b/Cores/MelonDSDeltaCore/Resources/iphone_edgetoedge_landscape.pdf differ
diff --git a/Cores/MelonDSDeltaCore/Resources/iphone_edgetoedge_portrait.pdf b/Cores/MelonDSDeltaCore/Resources/iphone_edgetoedge_portrait.pdf
new file mode 100644
index 000000000..41e48bf4d
Binary files /dev/null and b/Cores/MelonDSDeltaCore/Resources/iphone_edgetoedge_portrait.pdf differ
diff --git a/Cores/MelonDSDeltaCore/Resources/iphone_landscape.pdf b/Cores/MelonDSDeltaCore/Resources/iphone_landscape.pdf
new file mode 100644
index 000000000..d33daba9b
Binary files /dev/null and b/Cores/MelonDSDeltaCore/Resources/iphone_landscape.pdf differ
diff --git a/Cores/MelonDSDeltaCore/Resources/iphone_portrait.pdf b/Cores/MelonDSDeltaCore/Resources/iphone_portrait.pdf
new file mode 100644
index 000000000..cc5bf29d3
Binary files /dev/null and b/Cores/MelonDSDeltaCore/Resources/iphone_portrait.pdf differ
diff --git a/Cores/MelonDSDeltaCore/Sources/Bridge/MelonDSEmulatorBridge.h b/Cores/MelonDSDeltaCore/Sources/Bridge/MelonDSEmulatorBridge.h
new file mode 100644
index 000000000..c8e405ae3
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/Sources/Bridge/MelonDSEmulatorBridge.h
@@ -0,0 +1,42 @@
+//
+// MelonDSEmulatorBridge.h
+// MelonDSDeltaCore
+//
+// Created by Riley Testut on 10/31/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import
+
+@protocol DLTAEmulatorBridging;
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSInteger, MelonDSSystemType)
+{
+ MelonDSSystemTypeDS NS_SWIFT_NAME(ds) = 0,
+ MelonDSSystemTypeDSi NS_SWIFT_NAME(dsi) = 1
+};
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Weverything" // Silence "Cannot find protocol definition" warning due to forward declaration.
+@interface MelonDSEmulatorBridge : NSObject
+#pragma clang diagnostic pop
+
+@property (class, nonatomic, readonly) MelonDSEmulatorBridge *sharedBridge;
+
+@property (nonatomic) MelonDSSystemType systemType;
+@property (nonatomic, getter=isJITEnabled) BOOL jitEnabled;
+
+@property (nonatomic, readonly) NSURL *bios7URL;
+@property (nonatomic, readonly) NSURL *bios9URL;
+@property (nonatomic, readonly) NSURL *firmwareURL;
+
+@property (nonatomic, readonly) NSURL *dsiBIOS7URL;
+@property (nonatomic, readonly) NSURL *dsiBIOS9URL;
+@property (nonatomic, readonly) NSURL *dsiFirmwareURL;
+@property (nonatomic, readonly) NSURL *dsiNANDURL;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Cores/MelonDSDeltaCore/Sources/Bridge/MelonDSEmulatorBridge.mm b/Cores/MelonDSDeltaCore/Sources/Bridge/MelonDSEmulatorBridge.mm
new file mode 100644
index 000000000..16b47c1e3
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/Sources/Bridge/MelonDSEmulatorBridge.mm
@@ -0,0 +1,740 @@
+//
+// MelonDSEmulatorBridge.m
+// MelonDSDeltaCore
+//
+// Created by Riley Testut on 10/31/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import "MelonDSEmulatorBridge.h"
+
+#import // Prevent undeclared symbols in below headers
+
+#import
+#import
+
+#if STATIC_LIBRARY
+#import "MelonDSDeltaCore-Swift.h"
+#else
+#import
+#endif
+
+#include "melonDS/src/Platform.h"
+#include "melonDS/src/NDS.h"
+#include "melonDS/src/SPU.h"
+#include "melonDS/src/GPU.h"
+#include "melonDS/src/AREngine.h"
+
+#include "melonDS/src/Config.h"
+
+#include
+
+#import
+
+// Copied from melonDS source (no longer exists in HEAD)
+void ParseTextCode(char* text, int tlen, u32* code, int clen) // or whatever this should be named?
+{
+ u32 cur_word = 0;
+ u32 ndigits = 0;
+ u32 nin = 0;
+ u32 nout = 0;
+
+ char c;
+ while ((c = *text++) != '\0')
+ {
+ u32 val;
+ if (c >= '0' && c <= '9')
+ val = c - '0';
+ else if (c >= 'a' && c <= 'f')
+ val = c - 'a' + 0xA;
+ else if (c >= 'A' && c <= 'F')
+ val = c - 'A' + 0xA;
+ else
+ continue;
+
+ cur_word <<= 4;
+ cur_word |= val;
+
+ ndigits++;
+ if (ndigits >= 8)
+ {
+ if (nout >= clen)
+ {
+ printf("AR: code too long!\n");
+ return;
+ }
+
+ *code++ = cur_word;
+ nout++;
+
+ ndigits = 0;
+ cur_word = 0;
+ }
+
+ nin++;
+ if (nin >= tlen) break;
+ }
+
+ if (nout & 1)
+ {
+ printf("AR: code was missing one word\n");
+ if (nout >= clen)
+ {
+ printf("AR: code too long!\n");
+ return;
+ }
+ *code++ = 0;
+ }
+}
+
+@interface MelonDSEmulatorBridge ()
+
+@property (nonatomic, copy, nullable, readwrite) NSURL *gameURL;
+
+@property (nonatomic) uint32_t activatedInputs;
+@property (nonatomic) CGPoint touchScreenPoint;
+
+@property (nonatomic, readonly) std::shared_ptr cheatCodes;
+@property (nonatomic, readonly) int notifyToken;
+
+@property (nonatomic, getter=isInitialized) BOOL initialized;
+@property (nonatomic, getter=isStopping) BOOL stopping;
+
+@property (nonatomic, readonly) AVAudioEngine *audioEngine;
+@property (nonatomic, readonly) AVAudioUnitEQ *audioEQEffect;
+@property (nonatomic, readonly) AVAudioConverter *audioConverter;
+@property (nonatomic, readonly) DLTARingBuffer *microphoneBuffer;
+@property (nonatomic, readonly) dispatch_queue_t microphoneQueue;
+
+@end
+
+@implementation MelonDSEmulatorBridge
+@synthesize audioRenderer = _audioRenderer;
+@synthesize videoRenderer = _videoRenderer;
+@synthesize saveUpdateHandler = _saveUpdateHandler;
+@synthesize audioConverter = _audioConverter;
+
++ (instancetype)sharedBridge
+{
+ static MelonDSEmulatorBridge *_emulatorBridge = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ _emulatorBridge = [[self alloc] init];
+ });
+
+ return _emulatorBridge;
+}
+
+- (instancetype)init
+{
+ self = [super init];
+ if (self)
+ {
+ _cheatCodes = std::make_shared("");
+ _activatedInputs = 0;
+
+ _audioEngine = [[AVAudioEngine alloc] init];
+ _audioEQEffect = [[AVAudioUnitEQ alloc] initWithNumberOfBands:2];
+
+ _microphoneBuffer = [[DLTARingBuffer alloc] initWithPreferredBufferSize:100 * 1024];
+ _microphoneQueue = dispatch_queue_create("com.rileytestut.MelonDSDeltaCore.Microphone", DISPATCH_QUEUE_SERIAL);
+ }
+
+ return self;
+}
+
+#pragma mark - Emulation State -
+
+- (void)startWithGameURL:(NSURL *)gameURL
+{
+ self.gameURL = gameURL;
+
+ if ([self isInitialized])
+ {
+ NDS::DeInit();
+ }
+ else
+ {
+ // DS paths
+ strncpy(Config::BIOS7Path, self.bios7URL.lastPathComponent.UTF8String, self.bios7URL.lastPathComponent.length);
+ strncpy(Config::BIOS9Path, self.bios9URL.lastPathComponent.UTF8String, self.bios9URL.lastPathComponent.length);
+ strncpy(Config::FirmwarePath, self.firmwareURL.lastPathComponent.UTF8String, self.firmwareURL.lastPathComponent.length);
+
+ // DSi paths
+ strncpy(Config::DSiBIOS7Path, self.dsiBIOS7URL.lastPathComponent.UTF8String, self.dsiBIOS7URL.lastPathComponent.length);
+ strncpy(Config::DSiBIOS9Path, self.dsiBIOS9URL.lastPathComponent.UTF8String, self.dsiBIOS9URL.lastPathComponent.length);
+ strncpy(Config::DSiFirmwarePath, self.dsiFirmwareURL.lastPathComponent.UTF8String, self.dsiFirmwareURL.lastPathComponent.length);
+ strncpy(Config::DSiNANDPath, self.dsiNANDURL.lastPathComponent.UTF8String, self.dsiNANDURL.lastPathComponent.length);
+
+ [self registerForNotifications];
+ [self prepareAudioEngine];
+ }
+
+ NDS::SetConsoleType((int)self.systemType);
+
+ Config::JIT_Enable = [self isJITEnabled];
+ Config::JIT_FastMemory = NO;
+
+ NDS::Init();
+ self.initialized = YES;
+
+ GPU::RenderSettings settings;
+ settings.Soft_Threaded = NO;
+
+ GPU::InitRenderer(0);
+ GPU::SetRenderSettings(0, settings);
+
+ BOOL isDirectory = NO;
+ if ([[NSFileManager defaultManager] fileExistsAtPath:gameURL.path isDirectory:&isDirectory] && !isDirectory)
+ {
+ if (!NDS::LoadROM(gameURL.fileSystemRepresentation, "", YES))
+ {
+ NSLog(@"Failed to load Nintendo DS ROM.");
+ }
+ }
+ else
+ {
+ NDS::LoadBIOS();
+ }
+
+ self.stopping = NO;
+}
+
+- (void)stop
+{
+ self.stopping = YES;
+
+ NDS::Stop();
+
+ [self.audioEngine stop];
+}
+
+- (void)pause
+{
+ [self.audioEngine pause];
+}
+
+- (void)resume
+{
+}
+
+#pragma mark - Game Loop -
+
+- (void)runFrameAndProcessVideo:(BOOL)processVideo
+{
+ if ([self isStopping])
+ {
+ return;
+ }
+
+ uint32_t inputs = self.activatedInputs;
+ uint32_t inputsMask = 0xFFF; // 0b000000111111111111;
+
+ uint16_t sanitizedInputs = inputsMask ^ inputs;
+ NDS::SetKeyMask(sanitizedInputs);
+
+ if (self.activatedInputs & MelonDSGameInputTouchScreenX || self.activatedInputs & MelonDSGameInputTouchScreenY)
+ {
+ NDS::TouchScreen(self.touchScreenPoint.x, self.touchScreenPoint.y);
+ }
+ else
+ {
+ NDS::ReleaseScreen();
+ }
+
+ if (self.activatedInputs & MelonDSGameInputLid)
+ {
+ NDS::SetLidClosed(true);
+ }
+ else if (NDS::IsLidClosed())
+ {
+ NDS::SetLidClosed(false);
+ }
+
+ static int16_t micBuffer[735];
+ NSInteger readBytes = (NSInteger)[self.microphoneBuffer readIntoBuffer:micBuffer preferredSize:735 * sizeof(int16_t)];
+ NSInteger readFrames = readBytes / sizeof(int16_t);
+
+ if (readFrames > 0)
+ {
+ NDS::MicInputFrame(micBuffer, (int)readFrames);
+ }
+
+ NDS::RunFrame();
+
+ static int16_t buffer[0x1000];
+ u32 availableBytes = SPU::GetOutputSize();
+ availableBytes = MAX(availableBytes, (u32)(sizeof(buffer) / (2 * sizeof(int16_t))));
+
+ int samples = SPU::ReadOutput(buffer, availableBytes);
+ [self.audioRenderer.audioBuffer writeBuffer:buffer size:samples * 4];
+
+ if (processVideo)
+ {
+ int screenBufferSize = 256 * 192 * 4;
+
+ memcpy(self.videoRenderer.videoBuffer, GPU::Framebuffer[GPU::FrontBuffer][0], screenBufferSize);
+ memcpy(self.videoRenderer.videoBuffer + screenBufferSize, GPU::Framebuffer[GPU::FrontBuffer][1], screenBufferSize);
+
+ [self.videoRenderer processFrame];
+ }
+}
+
+#pragma mark - Inputs -
+
+- (void)activateInput:(NSInteger)input value:(double)value
+{
+ self.activatedInputs |= (uint32_t)input;
+
+ CGPoint touchPoint = self.touchScreenPoint;
+
+ switch ((MelonDSGameInput)input)
+ {
+ case MelonDSGameInputTouchScreenX:
+ touchPoint.x = value * (256 - 1);
+ break;
+
+ case MelonDSGameInputTouchScreenY:
+ touchPoint.y = value * (192 - 1);
+ break;
+
+ default: break;
+ }
+
+ self.touchScreenPoint = touchPoint;
+}
+
+- (void)deactivateInput:(NSInteger)input
+{
+ self.activatedInputs &= ~((uint32_t)input);
+
+ CGPoint touchPoint = self.touchScreenPoint;
+
+ switch ((MelonDSGameInput)input)
+ {
+ case MelonDSGameInputTouchScreenX:
+ touchPoint.x = 0;
+ break;
+
+ case MelonDSGameInputTouchScreenY:
+ touchPoint.y = 0;
+ break;
+
+ default: break;
+ }
+
+ self.touchScreenPoint = touchPoint;
+}
+
+- (void)resetInputs
+{
+ self.activatedInputs = 0;
+ self.touchScreenPoint = CGPointZero;
+}
+
+#pragma mark - Game Saves -
+
+- (void)saveGameSaveToURL:(NSURL *)URL
+{
+ NDS::RelocateSave(URL.fileSystemRepresentation, true);
+}
+
+- (void)loadGameSaveFromURL:(NSURL *)URL
+{
+ if (![[NSFileManager defaultManager] fileExistsAtPath:URL.path])
+ {
+ return;
+ }
+
+ NDS::RelocateSave(URL.fileSystemRepresentation, false);
+}
+
+#pragma mark - Save States -
+
+- (void)saveSaveStateToURL:(NSURL *)URL
+{
+ Savestate *savestate = new Savestate(URL.fileSystemRepresentation, true);
+ NDS::DoSavestate(savestate);
+ delete savestate;
+}
+
+- (void)loadSaveStateFromURL:(NSURL *)URL
+{
+ Savestate *savestate = new Savestate(URL.fileSystemRepresentation, false);
+ NDS::DoSavestate(savestate);
+ delete savestate;
+}
+
+#pragma mark - Cheats -
+
+- (BOOL)addCheatCode:(NSString *)cheatCode type:(NSString *)type
+{
+ NSArray *codes = [cheatCode componentsSeparatedByString:@"\n"];
+ for (NSString *code in codes)
+ {
+ if (code.length != 17)
+ {
+ return NO;
+ }
+
+ NSMutableCharacterSet *legalCharactersSet = [NSMutableCharacterSet hexadecimalCharacterSet];
+ [legalCharactersSet addCharactersInString:@" "];
+
+ if ([code rangeOfCharacterFromSet:legalCharactersSet.invertedSet].location != NSNotFound)
+ {
+ return NO;
+ }
+ }
+
+ NSString *sanitizedCode = [[cheatCode componentsSeparatedByCharactersInSet:NSCharacterSet.hexadecimalCharacterSet.invertedSet] componentsJoinedByString:@""];
+ int codeLength = (sanitizedCode.length / 8);
+
+ ARCode code;
+ memset(code.Name, 0, 128);
+ memset(code.Code, 0, 128);
+ memcpy(code.Name, sanitizedCode.UTF8String, MIN(128, sanitizedCode.length));
+ ParseTextCode((char *)sanitizedCode.UTF8String, (int)[sanitizedCode lengthOfBytesUsingEncoding:NSUTF8StringEncoding], &code.Code[0], 128);
+ code.Enabled = YES;
+ code.CodeLen = codeLength;
+
+ ARCodeCat category;
+ memcpy(category.Name, sanitizedCode.UTF8String, MIN(128, sanitizedCode.length));
+ category.Codes.push_back(code);
+
+ self.cheatCodes->Categories.push_back(category);
+
+ return YES;
+}
+
+- (void)resetCheats
+{
+ self.cheatCodes->Categories.clear();
+ AREngine::Reset();
+}
+
+- (void)updateCheats
+{
+ AREngine::SetCodeFile(self.cheatCodes.get());
+}
+
+#pragma mark - Notifications -
+
+- (void)registerForNotifications
+{
+ int status = notify_register_dispatch("com.apple.springboard.hasBlankedScreen", &_notifyToken, dispatch_get_main_queue(), ^(int t) {
+ uint64_t state;
+ int result = notify_get_state(self.notifyToken, &state);
+ NSLog(@"Lock screen state = %llu", state);
+
+ if (state == 0)
+ {
+ [self deactivateInput:MelonDSGameInputLid];
+ }
+ else
+ {
+ [self activateInput:MelonDSGameInputLid value:1];
+ }
+
+ if (result != NOTIFY_STATUS_OK)
+ {
+ NSLog(@"Lock screen notification returned: %d", result);
+ }
+ });
+
+ if (status != NOTIFY_STATUS_OK)
+ {
+ NSLog(@"Lock screen notification registration returned: %d", status);
+ }
+}
+
+#pragma mark - Microphone -
+
+- (void)prepareAudioEngine
+{
+ self.audioEQEffect.globalGain = 3;
+
+ // Experimentally-determined values. Focuses on ensuring blows are registered correctly.
+ self.audioEQEffect.bands[0].filterType = AVAudioUnitEQFilterTypeLowShelf;
+ self.audioEQEffect.bands[0].frequency = 100;
+ self.audioEQEffect.bands[0].gain = 20;
+ self.audioEQEffect.bands[0].bypass = NO;
+
+ self.audioEQEffect.bands[1].filterType = AVAudioUnitEQFilterTypeHighShelf;
+ self.audioEQEffect.bands[1].frequency = 10000;
+ self.audioEQEffect.bands[1].gain = -30;
+ self.audioEQEffect.bands[1].bypass = NO;
+
+ [self.audioEngine attachNode:self.audioEQEffect];
+ [self.audioEngine connect:self.audioEngine.inputNode to:self.audioEQEffect format:self.audioConverter.inputFormat];
+
+ NSInteger bufferSize = 1024 * self.audioConverter.inputFormat.streamDescription->mBytesPerFrame;
+ [self.audioEQEffect installTapOnBus:0 bufferSize:bufferSize format:self.audioConverter.inputFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
+ dispatch_async(self.microphoneQueue, ^{
+ [self processMicrophoneBuffer:buffer];
+ });
+ }];
+}
+
+- (void)processMicrophoneBuffer:(AVAudioPCMBuffer *)inputBuffer
+{
+ static AVAudioPCMBuffer *outputBuffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:self.audioConverter.outputFormat frameCapacity:5000];
+ outputBuffer.frameLength = 5000;
+
+ __block BOOL didReturnBuffer = NO;
+
+ NSError *error = nil;
+ AVAudioConverterOutputStatus status = [self.audioConverter convertToBuffer:outputBuffer error:&error
+ withInputFromBlock:^AVAudioBuffer * _Nullable(AVAudioPacketCount packetCount, AVAudioConverterInputStatus * _Nonnull outStatus) {
+ if (didReturnBuffer)
+ {
+ *outStatus = AVAudioConverterInputStatus_NoDataNow;
+ return nil;
+ }
+ else
+ {
+ didReturnBuffer = YES;
+ *outStatus = AVAudioConverterInputStatus_HaveData;
+ return inputBuffer;
+ }
+ }];
+
+ if (status == AVAudioConverterOutputStatus_Error)
+ {
+ NSLog(@"Conversion error: %@", error);
+ }
+
+ NSInteger outputSize = outputBuffer.frameLength * outputBuffer.format.streamDescription->mBytesPerFrame;
+ [self.microphoneBuffer writeBuffer:outputBuffer.int16ChannelData[0] size:outputSize];
+}
+#pragma mark - Getters/Setters -
+
+- (NSTimeInterval)frameDuration
+{
+ return (1.0 / 60.0);
+}
+
+- (NSURL *)bios7URL
+{
+ return [MelonDSEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:@"bios7.bin"];
+}
+
+- (NSURL *)bios9URL
+{
+ return [MelonDSEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:@"bios9.bin"];
+}
+
+- (NSURL *)firmwareURL
+{
+ return [MelonDSEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:@"firmware.bin"];
+}
+
+- (NSURL *)dsiBIOS7URL
+{
+ return [MelonDSEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:@"dsibios7.bin"];
+}
+
+- (NSURL *)dsiBIOS9URL
+{
+ return [MelonDSEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:@"dsibios9.bin"];
+}
+
+- (NSURL *)dsiFirmwareURL
+{
+ return [MelonDSEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:@"dsifirmware.bin"];
+}
+
+- (NSURL *)dsiNANDURL
+{
+ return [MelonDSEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:@"dsinand.bin"];
+}
+
+- (AVAudioConverter *)audioConverter
+{
+ if (_audioConverter == nil)
+ {
+ // Lazily initialize so we don't cause microphone permission alert to appear prematurely.
+ AVAudioFormat *inputFormat = [_audioEngine.inputNode inputFormatForBus:0];
+ AVAudioFormat *outputFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16 sampleRate:44100 channels:1 interleaved:NO];
+ _audioConverter = [[AVAudioConverter alloc] initFromFormat:inputFormat toFormat:outputFormat];
+ }
+
+ return _audioConverter;
+}
+
+@end
+
+namespace Platform
+{
+ void StopEmu()
+ {
+ if ([MelonDSEmulatorBridge.sharedBridge isStopping])
+ {
+ return;
+ }
+
+ MelonDSEmulatorBridge.sharedBridge.stopping = YES;
+ [[NSNotificationCenter defaultCenter] postNotificationName:DLTAEmulatorCore.emulationDidQuitNotification object:nil];
+ }
+
+ FILE* OpenFile(const char* path, const char* mode, bool mustexist)
+ {
+ FILE* ret;
+
+ if (mustexist)
+ {
+ ret = fopen(path, "rb");
+ if (ret) ret = freopen(path, mode, ret);
+ }
+ else
+ ret = fopen(path, mode);
+
+ return ret;
+ }
+
+ FILE* OpenLocalFile(const char* path, const char* mode)
+ {
+ NSURL *fileURL = [MelonDSEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:@(path)];
+ return OpenFile(fileURL.fileSystemRepresentation, mode);
+ }
+
+ FILE* OpenDataFile(const char* path)
+ {
+ NSString *resourceName = [@(path) stringByDeletingPathExtension];
+ NSString *extension = [@(path) pathExtension];
+
+ NSURL *fileURL = [MelonDSEmulatorBridge.dsResources URLForResource:resourceName withExtension:extension];
+ return OpenFile(fileURL.fileSystemRepresentation, "rb");
+ }
+
+ void *Thread_Create(void (*func)())
+ {
+ return NULL;
+ }
+
+ void Thread_Free(void* thread)
+ {
+ }
+
+ void Thread_Wait(void* thread)
+ {
+ }
+
+ void *Semaphore_Create()
+ {
+ return NULL;
+ }
+
+ void Semaphore_Free(void *semaphore)
+ {
+ }
+
+ void Semaphore_Reset(void *semaphore)
+ {
+ }
+
+ void Semaphore_Wait(void *semaphore)
+ {
+ }
+
+ void Semaphore_Post(void *semaphore)
+ {
+ }
+
+ void *GL_GetProcAddress(const char* proc)
+ {
+ return NULL;
+ }
+
+ bool MP_Init()
+ {
+ return false;
+ }
+
+ void MP_DeInit()
+ {
+ }
+
+ int MP_SendPacket(u8* bytes, int len)
+ {
+ return 0;
+ }
+
+ int MP_RecvPacket(u8* bytes, bool block)
+ {
+ return 0;
+ }
+
+ bool LAN_Init()
+ {
+ return false;
+ }
+
+ void LAN_DeInit()
+ {
+ }
+
+ int LAN_SendPacket(u8* data, int len)
+ {
+ return 0;
+ }
+
+ int LAN_RecvPacket(u8* data)
+ {
+ return 0;
+ }
+
+ void Mic_Prepare()
+ {
+ if ([MelonDSEmulatorBridge.sharedBridge.audioEngine isRunning])
+ {
+ return;
+ }
+
+ NSError *error = nil;
+ if (![MelonDSEmulatorBridge.sharedBridge.audioEngine startAndReturnError:&error])
+ {
+ NSLog(@"Failed to start listening to microphone. %@", error);
+ }
+ }
+}
+
+namespace GPU3D
+{
+namespace GLRenderer
+{
+ bool Init()
+ {
+ return false;
+ }
+
+ void DeInit()
+ {
+ }
+
+ void Reset()
+ {
+ }
+
+ void UpdateDisplaySettings()
+ {
+ }
+
+ void RenderFrame()
+ {
+ }
+
+ void PrepareCaptureFrame()
+ {
+ }
+
+ u32* GetLine(int line)
+ {
+ return NULL;
+ }
+
+ void SetupAccelFrame()
+ {
+ return;
+ }
+}
+}
diff --git a/Cores/MelonDSDeltaCore/Sources/MelonDS.swift b/Cores/MelonDSDeltaCore/Sources/MelonDS.swift
new file mode 100644
index 000000000..6cd994614
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/Sources/MelonDS.swift
@@ -0,0 +1,97 @@
+//
+// MelonDS.swift
+// MelonDSDeltaCore
+//
+// Created by Riley Testut on 10/31/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import Foundation
+import AVFoundation
+
+import DeltaCore
+
+#if !STATIC_LIBRARY
+public extension GameType
+{
+ static let ds = GameType("com.rileytestut.delta.game.ds")
+}
+
+public extension CheatType
+{
+ static let actionReplay = CheatType("ActionReplay")
+}
+#endif
+
+@objc public enum MelonDSGameInput: Int, Input
+{
+ case a = 1
+ case b = 2
+ case select = 4
+ case start = 8
+ case right = 16
+ case left = 32
+ case up = 64
+ case down = 128
+ case r = 256
+ case l = 512
+ case x = 1024
+ case y = 2048
+
+ case touchScreenX = 4096
+ case touchScreenY = 8192
+
+ case lid = 16_384
+
+ public var type: InputType {
+ return .game(.ds)
+ }
+
+ public var isContinuous: Bool {
+ switch self
+ {
+ case .touchScreenX, .touchScreenY: return true
+ default: return false
+ }
+ }
+}
+
+public struct MelonDS: DeltaCoreProtocol
+{
+ public static let core = MelonDS()
+
+ public var name: String { "melonDS" }
+ public var identifier: String { "com.rileytestut.MelonDSDeltaCore" }
+
+ public var gameType: GameType { GameType.ds }
+ public var gameInputType: Input.Type { MelonDSGameInput.self }
+ public var gameSaveFileExtension: String { "dsv" }
+
+ public let audioFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 32768, channels: 2, interleaved: true)!
+ public let videoFormat = VideoFormat(format: .bitmap(.bgra8), dimensions: CGSize(width: 256, height: 384))
+
+ public var supportedCheatFormats: Set {
+ let actionReplayFormat = CheatFormat(name: NSLocalizedString("Action Replay", comment: ""), format: "XXXXXXXX YYYYYYYY", type: .actionReplay)
+ return [actionReplayFormat]
+ }
+
+ public var emulatorBridge: EmulatorBridging { MelonDSEmulatorBridge.shared }
+
+ private init()
+ {
+ }
+}
+
+// Expose DeltaCore properties to Objective-C.
+public extension MelonDSEmulatorBridge
+{
+ @objc(dsResources) class var __dsResources: Bundle {
+ return MelonDS.core.resourceBundle
+ }
+
+ @objc(coreDirectoryURL) class var __coreDirectoryURL: URL {
+ return _coreDirectoryURL
+ }
+}
+
+private let _coreDirectoryURL = MelonDS.core.directoryURL
diff --git a/Cores/MelonDSDeltaCore/Sources/MelonDSDeltaCore.h b/Cores/MelonDSDeltaCore/Sources/MelonDSDeltaCore.h
new file mode 100644
index 000000000..3d800caed
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/Sources/MelonDSDeltaCore.h
@@ -0,0 +1,20 @@
+//
+// MelonDSDeltaCore.h
+// MelonDSDeltaCore
+//
+// Created by Riley Testut on 10/15/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import
+
+//! Project version number for MelonDSDeltaCore.
+FOUNDATION_EXPORT double MelonDSDeltaCoreVersionNumber;
+
+//! Project version string for MelonDSDeltaCore.
+FOUNDATION_EXPORT const unsigned char MelonDSDeltaCoreVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+#if !STATIC_LIBRARY
+#import
+#endif
diff --git a/Cores/MelonDSDeltaCore/Sources/Types/MelonDSTypes.h b/Cores/MelonDSDeltaCore/Sources/Types/MelonDSTypes.h
new file mode 100644
index 000000000..2d3574e83
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/Sources/Types/MelonDSTypes.h
@@ -0,0 +1,14 @@
+//
+// MelonDSTypes.h
+// MelonDSDeltaCore
+//
+// Created by Riley Testut on 4/2/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+#import
+
+// Extensible Enums
+FOUNDATION_EXPORT GameType const GameTypeDS NS_SWIFT_NAME(ds);
+
+FOUNDATION_EXPORT CheatType const CheatTypeActionReplay;
diff --git a/Cores/MelonDSDeltaCore/Sources/Types/MelonDSTypes.m b/Cores/MelonDSDeltaCore/Sources/Types/MelonDSTypes.m
new file mode 100644
index 000000000..359759a29
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/Sources/Types/MelonDSTypes.m
@@ -0,0 +1,13 @@
+//
+// MelonDSTypes.m
+// MelonDSDeltaCore
+//
+// Created by Riley Testut on 4/2/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+#import "MelonDSTypes.h"
+
+GameType const GameTypeDS = @"com.rileytestut.delta.game.ds";
+
+CheatType const CheatTypeActionReplay = @"ActionReplay";
diff --git a/Cores/MelonDSDeltaCore/melonDS b/Cores/MelonDSDeltaCore/melonDS
new file mode 160000
index 000000000..c353e7760
--- /dev/null
+++ b/Cores/MelonDSDeltaCore/melonDS
@@ -0,0 +1 @@
+Subproject commit c353e7760be6a61f6dc34551d5b10637aafaa04b
diff --git a/Cores/Mupen64PlusDeltaCore/Derived/InfoPlists/Mupen64PlusDeltaCore.plist b/Cores/Mupen64PlusDeltaCore/Derived/InfoPlists/Mupen64PlusDeltaCore.plist
new file mode 100644
index 000000000..323e5ecfc
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Derived/InfoPlists/Mupen64PlusDeltaCore.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+
+
diff --git a/Cores/Mupen64PlusDeltaCore/Derived/InfoPlists/mupen64plus-rsp-hle.plist b/Cores/Mupen64PlusDeltaCore/Derived/InfoPlists/mupen64plus-rsp-hle.plist
new file mode 100644
index 000000000..323e5ecfc
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Derived/InfoPlists/mupen64plus-rsp-hle.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+
+
diff --git a/Cores/Mupen64PlusDeltaCore/Derived/InfoPlists/mupen64plus-video-GLideN64.plist b/Cores/Mupen64PlusDeltaCore/Derived/InfoPlists/mupen64plus-video-GLideN64.plist
new file mode 100644
index 000000000..323e5ecfc
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Derived/InfoPlists/mupen64plus-video-GLideN64.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+
+
diff --git a/Cores/Mupen64PlusDeltaCore/Derived/Sources/Bundle+Mupen64PlusDeltaCore.swift b/Cores/Mupen64PlusDeltaCore/Derived/Sources/Bundle+Mupen64PlusDeltaCore.swift
new file mode 100644
index 000000000..f1557a7ac
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Derived/Sources/Bundle+Mupen64PlusDeltaCore.swift
@@ -0,0 +1,23 @@
+// swiftlint:disable all
+import Foundation
+
+// MARK: - Swift Bundle Accessor
+
+private class BundleFinder {}
+
+extension Foundation.Bundle {
+ /// Since Mupen64PlusDeltaCore is a framework, the bundle containing the resources is copied into the final product.
+ static var module: Bundle = {
+ return Bundle(for: BundleFinder.self)
+ }()
+}
+
+// MARK: - Objective-C Bundle Accessor
+
+@objc
+public class Mupen64PlusDeltaCoreResources: NSObject {
+ @objc public class var bundle: Bundle {
+ return .module
+ }
+}
+// swiftlint:enable all
\ No newline at end of file
diff --git a/Cores/Mupen64PlusDeltaCore/Mupen64Plus/GLideN64 b/Cores/Mupen64PlusDeltaCore/Mupen64Plus/GLideN64
new file mode 160000
index 000000000..86738998d
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Mupen64Plus/GLideN64
@@ -0,0 +1 @@
+Subproject commit 86738998d393ad211604c6701358227ee1de6940
diff --git a/Cores/Mupen64PlusDeltaCore/Mupen64Plus/libpng b/Cores/Mupen64PlusDeltaCore/Mupen64Plus/libpng
new file mode 160000
index 000000000..dbe3e0c43
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Mupen64Plus/libpng
@@ -0,0 +1 @@
+Subproject commit dbe3e0c43e549a1602286144d94b0666549b18e6
diff --git a/Cores/Mupen64PlusDeltaCore/Mupen64Plus/mupen64plus-core b/Cores/Mupen64PlusDeltaCore/Mupen64Plus/mupen64plus-core
new file mode 160000
index 000000000..aa9903b54
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Mupen64Plus/mupen64plus-core
@@ -0,0 +1 @@
+Subproject commit aa9903b5446a9b50b8f5a31f927ca98e5a33a230
diff --git a/Cores/Mupen64PlusDeltaCore/Mupen64Plus/mupen64plus-rsp-hle b/Cores/Mupen64PlusDeltaCore/Mupen64Plus/mupen64plus-rsp-hle
new file mode 160000
index 000000000..e653930d7
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Mupen64Plus/mupen64plus-rsp-hle
@@ -0,0 +1 @@
+Subproject commit e653930d75019f88dd386a3d534008d89dbc12ff
diff --git a/Cores/Mupen64PlusDeltaCore/Project.swift b/Cores/Mupen64PlusDeltaCore/Project.swift
new file mode 100644
index 000000000..5a00ba928
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Project.swift
@@ -0,0 +1,235 @@
+import ProjectDescription
+
+let project = Project(name: "Mupen64PlusDeltaCore",
+ packages: [],
+ targets: [
+ Target(name: "Mupen64PlusDeltaCore",
+ platform: .iOS,
+ product: .framework,
+ bundleId: "com.rileytestut.mupen64PlusDeltaCore",
+ deploymentTarget: .iOS(targetVersion: "12.0", devices: [.iphone, .ipad]),
+ infoPlist: .extendingDefault(with: [:]),
+ sources: [
+ "Sources/**/*.{m,swift}",
+ "Mupen64Plus/mupen64plus-core/src/device/dd/dd_controller.c",
+ "Mupen64Plus/mupen64plus-core/src/backends/api/video_capture_backend.c",
+ "Mupen64Plus/mupen64plus-core/src/device/controllers/paks/biopak.c",
+ "Mupen64Plus/mupen64plus-core/src/backends/dummy_video_capture.c",
+ ],
+ resources: ["Resources/**/*.{deltamapping,deltaskin}", "Mupen64Plus/**/*.ini"],
+ headers: Headers(public: ["Sources/Mupen64PlusDeltaCore.h", "Sources/Bridge/Mupen64PlusEmulatorBridge.h"], project: "Sources/Types/Mupen64PlusTypes.h"),
+ dependencies: [
+ .project(target: "DeltaCore", path: "../DeltaCore"),
+ .target(name: "libMupen64Plus")
+ ],
+ settings: Settings(base: [
+ "HEADER_SEARCH_PATHS": "\"$(SRCROOT)/Mupen64Plus/mupen64plus-core/subprojects\"/** \"$(SRCROOT)/libMupen64Plus/SDL\" \"$(SRCROOT)/Mupen64Plus/mupen64plus-core/src\" \"$(SRCROOT)/Mupen64Plus/mupen64plus-core/src/api\" \"$(SRCROOT)/Mupen64Plus/GLideN64/src\"",
+ "USER_HEADER_SEARCH_PATHS": "\"$(SRCROOT)/Mupen64Plus/mupen64plus-core/src\" \"$(SRCROOT)/Mupen64Plus/mupen64plus-core/src/api\" \"$(SRCROOT)/Mupen64Plus/GLideN64/src\"",
+ ])),
+ Target(name: "libMupen64Plus",
+ platform: .iOS,
+ product: .staticLibrary,
+ bundleId: "com.rileytestut.libMupen64Plus",
+ deploymentTarget: .iOS(targetVersion: "12.0", devices: [.iphone, .ipad]),
+ infoPlist: .extendingDefault(with: [:]),
+ sources: [
+ "libMupen64Plus/**/*.{h,m,mm,c,cpp}",
+ SourceFileGlob("Mupen64Plus/mupen64plus-core/**/*.{h,m,mm,c,cpp}", excluding: [
+ "Mupen64Plus/mupen64plus-core/src/api/vidext.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/asm_defines/*.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/backends/api/*.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/backends/dummy_video_capture.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/backends/opencv_video_capture.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/debugger/*.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/device/controllers/paks/biopak.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/device/dd/*.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/device/r4300/new_dynarec/**/*.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/device/r4300/x86/*.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/device/r4300/x86_64/*.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/device/r4300/instr_counters.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/device/r4300/recomp.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/main/eventloop.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/main/screenshot.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/osal/dynamiclib_*.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/osal/files_unix.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/osal/files_win32.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/src/osd/*.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/subprojects/oglft/*.{c,cpp}",
+ "Mupen64Plus/mupen64plus-core/tools/*.{c,cpp}"
+ ]),
+ ],
+ settings: Settings(base: [
+ "OTHER_CFLAGS": "$(inherited) -DM64P_PARALLEL=1 -DIN_OPENEMU=1 -DNO_ASM=1 -DM64P_CORE_PROTOTYPES=1 -DNDEBUG=1 -DPIC=1 -flto -fomit-frame-pointer -DUSE_GLES=1",
+ "HEADER_SEARCH_PATHS": "\"$(SRCROOT)/Mupen64Plus/mupen64plus-core/src\" \"$(SRCROOT)/libMupen64Plus/SDL\" \"$(SRCROOT)/Mupen64Plus/mupen64plus-core/subprojects\"/**",
+ ])),
+ Target(name: "mupen64plus-rsp-hle",
+ platform: .iOS,
+ product: .framework,
+ bundleId: "com.rileytestut.mupen64PlusRSPHLE",
+ deploymentTarget: .iOS(targetVersion: "12.0", devices: [.iphone, .ipad]),
+ infoPlist: .extendingDefault(with: [:]),
+ sources: [
+ "Sources/mupen64plus-rsp-hle-plugin.c",
+ SourceFileGlob("Mupen64Plus/mupen64plus-rsp-hle/src/*.{h,c}", excluding: [
+ "Mupen64Plus/mupen64plus-rsp-hle/src/osal_dynamiclib_unix.c",
+ "Mupen64Plus/mupen64plus-rsp-hle/src/osal_dynamiclib_win32.c",
+ "Mupen64Plus/mupen64plus-rsp-hle/src/plugin.c",
+ "Mupen64Plus/mupen64plus-rsp-hle/src/memory.h"
+ ]),
+ ],
+ headers: Headers(project: ["Mupen64Plus/mupen64plus-rsp-hle/src/**/*"]),
+ settings: Settings(base: [
+ "HEADER_SEARCH_PATHS": "\"$(SRCROOT)/Mupen64Plus/mupen64plus-core/src/api\"",
+ "OTHER_CFLAGS": "-fno-strict-aliasing -DGCC -pthread -fPIC -D__unix__ -ffast-math",
+ ])),
+ Target(name: "mupen64plus-video-GLideN64",
+ platform: .iOS,
+ product: .framework,
+ bundleId: "com.rileytestut.mupen64PlusVideoGlideN64",
+ deploymentTarget: .iOS(targetVersion: "12.0", devices: [.iphone, .ipad]),
+ infoPlist: .extendingDefault(with: [:]),
+ sources: [
+ "Sources/mupen64plus-video-GLideN64-plugin.cpp",
+ "**/3DMath.cpp",
+ "**/ClipPolygon.cpp",
+ "**/ColorBufferReader.cpp",
+ "**/ColorBufferToRDRAM.cpp",
+ "**/Combiner.cpp",
+ "**/CombinerKey.cpp",
+ "**/CombinerProgram.cpp",
+ "**/CommonAPIImpl_common.cpp",
+ "**/Config.cpp",
+ "**/Context.cpp",
+ "**/convert.cpp",
+ "**/CRC_OPT.cpp",
+ "**/DebugDump.cpp",
+ "**/Debugger.cpp",
+ "**/DepthBuffer.cpp",
+ "**/DepthBufferRender.cpp",
+ "**/DepthBufferToRDRAM.cpp",
+ "**/DisplayLoadProgress.cpp",
+ "**/DisplayWindow.cpp",
+ "**/F3D.cpp",
+ "**/F3DAM.cpp",
+ "**/F3DBETA.cpp",
+ "**/F3DDKR.cpp",
+ "**/F3DEX.cpp",
+ "**/F3DEX2.cpp",
+ "**/F3DEX2ACCLAIM.cpp",
+ "**/F3DEX2CBFD.cpp",
+ "**/F3DFLX2.cpp",
+ "**/F3DGOLDEN.cpp",
+ "**/F3DPD.cpp",
+ "**/F3DSETA.cpp",
+ "**/F3DTEXA.cpp",
+ "**/F3DZEX2.cpp",
+ "**/F5Indi_Naboo.cpp",
+ "**/F5Rogue.cpp",
+ "**/FrameBuffer.cpp",
+ "**/FrameBufferInfo.cpp",
+ "**/GBI.cpp",
+ "**/gDP.cpp",
+ "**/GLFunctions.cpp",
+ "**/GLideN64.cpp",
+ "**/glsl_CombinerInputs.cpp",
+ "**/glsl_CombinerProgramBuilder.cpp",
+ "**/glsl_CombinerProgramImpl.cpp",
+ "**/glsl_CombinerProgramUniformFactory.cpp",
+ "**/glsl_FXAA.cpp",
+ "**/glsl_ShaderStorage.cpp",
+ "**/glsl_SpecialShadersFactory.cpp",
+ "**/glsl_Utils.cpp",
+ "**/GraphicsDrawer.cpp",
+ "**/gSP.cpp",
+ "**/Keys.cpp",
+ "**/L3D.cpp",
+ "**/L3DEX.cpp",
+ "**/L3DEX2.cpp",
+ "**/Log_ios.mm",
+ "**/MemoryStatus_mupenplus.cpp",
+ "**/mupen64plus_DisplayWindow.cpp",
+ "**/N64.cpp",
+ "**/NoiseTexture.cpp",
+ "**/ObjectHandle.cpp",
+ "**/opengl_Attributes.cpp",
+ "**/opengl_BufferedDrawer.cpp",
+ "**/opengl_BufferManipulationObjectFactory.cpp",
+ "**/opengl_CachedFunctions.cpp",
+ "**/opengl_ColorBufferReaderWithBufferStorage.cpp",
+ "**/opengl_ColorBufferReaderWithPixelBuffer.cpp",
+ "**/opengl_ColorBufferReaderWithReadPixels.cpp",
+ "**/opengl_ContextImpl.cpp",
+ "**/opengl_GLInfo.cpp",
+ "**/opengl_Parameters.cpp",
+ "**/opengl_TextureManipulationObjectFactory.cpp",
+ "**/opengl_UnbufferedDrawer.cpp",
+ "**/opengl_Utils.cpp",
+ "**/osal_files_ios.mm",
+ "**/PaletteTexture.cpp",
+ "**/Performance.cpp",
+ "**/png.c",
+ "**/pngerror.c",
+ "**/pngget.c",
+ "**/pngmem.c",
+ "**/pngpread.c",
+ "**/pngread.c",
+ "**/pngrio.c",
+ "**/pngrtran.c",
+ "**/pngrutil.c",
+ "**/pngset.c",
+ "**/pngtest.c",
+ "**/pngtrans.c",
+ "**/pngwio.c",
+ "**/pngwrite.c",
+ "**/pngwtran.c",
+ "**/pngwutil.c",
+ "**/PostProcessor.cpp",
+ "**/RDP.cpp",
+ "**/RDRAMtoColorBuffer.cpp",
+ "**/RSP_LoadMatrix.cpp",
+ "**/RSP.cpp",
+ "**/S2DEX.cpp",
+ "**/S2DEX2.cpp",
+ "**/SoftwareRender.cpp",
+ "**/T3DUX.cpp",
+ "**/TexrectDrawer.cpp",
+ "**/TextDrawerStub.cpp",
+ "**/TextureFilterHandler.cpp",
+ "**/TextureFilters_2xsai.cpp",
+ "**/TextureFilters_hq2x.cpp",
+ "**/TextureFilters_hq4x.cpp",
+ "**/TextureFilters_xbrz.cpp",
+ "**/TextureFilters.cpp",
+ "**/Textures.cpp",
+ "**/Turbo3D.cpp",
+ "**/TxCache.cpp",
+ "**/TxDbg_ios.mm",
+ "**/TxFilter.cpp",
+ "**/TxFilterExport.cpp",
+ "**/TxHiResCache.cpp",
+ "**/TxImage.cpp",
+ "**/TxQuantize.cpp",
+ "**/TxReSample.cpp",
+ "**/TxTexCache.cpp",
+ "**/TxUtil.cpp",
+ "**/txWidestringWrapper.cpp",
+ "**/VI.cpp",
+ "**/xxhash.c",
+ "**/ZlutTexture.cpp",
+ "**/ZSort.cpp",
+ "**/ZSortBOSS.cpp",
+ ],
+ headers: Headers(private: ["**/*"]),
+ dependencies: [
+ .sdk(name: "OpenGLES.framework", status: .required),
+ .sdk(name: "libz.tbd"),
+ ],
+ settings: Settings(base: [
+ "HEADER_SEARCH_PATHS": "\"$(SRCROOT)/Mupen64Plus/GLideN64/src/\" \"$(SRCROOT)/Mupen64Plus/GLideN64/src/inc/\"",
+ "USER_HEADER_SEARCH_PATHS": "\"$(SRCROOT)/Mupen64Plus/GLideN64/src/\" \"$(SRCROOT)/Mupen64Plus/mupen64plus-core/src\"",
+ "ALWAYS_SEARCH_USER_PATHS": "YES",
+ "OTHER_CFLAGS": "-fno-strict-aliasing -DGCC -pthread -fPIC -D__unix__ -ffast-math -D__VEC4_OPT -fvisibility=hidden",
+ "OTHER_LDFLAGS": "-Wl,-exported_symbol,_Video_PluginStartup,-exported_symbol,_Video_PluginShutdown,-exported_symbol,_Video_PluginGetVersion,-exported_symbol,_Video_RomOpen,-exported_symbol,_Video_RomClosed,-exported_symbol,_ConfigGetSharedDataFilepath,-exported_symbol,_ConfigGetUserConfigPath,-exported_symbol,_ConfigGetUserCachePath,-exported_symbol,_ConfigGetUserDataPath,-exported_symbol,_ConfigOpenSection,-exported_symbol,_ConfigDeleteSection,-exported_symbol,_ConfigSaveSection,-exported_symbol,_ConfigSaveFile,-exported_symbol,_ConfigSetDefaultInt,-exported_symbol,_ConfigSetDefaultFloat,-exported_symbol,_ConfigSetDefaultBool,-exported_symbol,_ConfigSetDefaultString,-exported_symbol,_ConfigGetParamInt,-exported_symbol,_ConfigGetParamFloat,-exported_symbol,_ConfigGetParamBool,-exported_symbol,_ConfigGetParamString,-exported_symbol,_ConfigExternalGetParameter,-exported_symbol,_ConfigExternalOpen,-exported_symbol,_ConfigExternalClose,-exported_symbol,_VidExt_Init,-exported_symbol,_VidExt_Quit,-exported_symbol,_VidExt_ListFullscreenModes,-exported_symbol,_VidExt_SetVideoMode,-exported_symbol,_VidExt_SetCaption,-exported_symbol,_VidExt_ToggleFullScreen,-exported_symbol,_VidExt_ResizeWindow,-exported_symbol,_VidExt_GL_GetProcAddress,-exported_symbol,_VidExt_GL_SetAttribute,-exported_symbol,_VidExt_GL_GetAttribute,-exported_symbol,_VidExt_GL_SwapBuffers,-exported_symbol,_ChangeWindow,-exported_symbol,_InitiateGFX,-exported_symbol,_MoveScreen,-exported_symbol,_ProcessDList,-exported_symbol,_ProcessRDPList,-exported_symbol,_ShowCFB,-exported_symbol,_UpdateScreen,-exported_symbol,_ViStatusChanged,-exported_symbol,_ViWidthChanged,-exported_symbol,_ReadScreen2,-exported_symbol,_SetRenderingCallback,-exported_symbol,_FBRead,-exported_symbol,_FBWrite,-exported_symbol,_FBGetFrameBufferInfo,-exported_symbol,_ResizeVideoOutput,-exported_symbol,_RSP_PluginStartup,-exported_symbol,_RSP_PluginShutdown,-exported_symbol,_RSP_PluginGetVersion,-exported_symbol,_DoRspCycles,-exported_symbol,_InitiateRSP,-exported_symbol,_RSP_RomClosed,-exported_symbol,_CoreGetAPIVersions,-exported_symbol,_ConfigGetParameter,-exported_symbol,_ConfigSetParameter,-exported_symbol,_CoreDoCommand",
+ "GCC_PREPROCESSOR_DEFINITIONS": "MUPENPLUSAPI TXFILTER_LIB OS_IOS GLESX GL_ERROR_DEBUG GL_DEBUG GLESX PNG_ARM_NEON_OPT=0",
+ ])),
+ ])
diff --git a/Cores/Mupen64PlusDeltaCore/Resources/Standard.deltamapping b/Cores/Mupen64PlusDeltaCore/Resources/Standard.deltamapping
new file mode 100644
index 000000000..1d0d4c8d9
Binary files /dev/null and b/Cores/Mupen64PlusDeltaCore/Resources/Standard.deltamapping differ
diff --git a/Cores/Mupen64PlusDeltaCore/Resources/Standard.deltaskin b/Cores/Mupen64PlusDeltaCore/Resources/Standard.deltaskin
new file mode 100644
index 000000000..c003b6ef3
Binary files /dev/null and b/Cores/Mupen64PlusDeltaCore/Resources/Standard.deltaskin differ
diff --git a/Cores/Mupen64PlusDeltaCore/Resources/info.json b/Cores/Mupen64PlusDeltaCore/Resources/info.json
new file mode 100644
index 000000000..1399a95d6
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Resources/info.json
@@ -0,0 +1,848 @@
+{
+ "name" : "Standard N64",
+ "identifier" : "com.delta.n64.standard",
+ "gameTypeIdentifier" : "com.rileytestut.delta.game.n64",
+ "debug" : false,
+ "representations" : {
+ "iphone" : {
+ "standard": {
+ "portrait": {
+ "assets": {
+ "resizable": "iphone_portrait.pdf"
+ },
+ "items": [
+ {
+ "thumbstick": {
+ "name": "portrait_thumbstick.pdf",
+ "width": 85,
+ "height": 87
+ },
+ "inputs": {
+ "up": "analogStickUp",
+ "down": "analogStickDown",
+ "left": "analogStickLeft",
+ "right": "analogStickRight"
+ },
+ "frame": {
+ "x": 39,
+ "y": 77,
+ "width": 71,
+ "height": 71
+ },
+ "extendedEdges": {
+ "top": 20,
+ "bottom": 20,
+ "left": 20,
+ "right": 20
+ }
+ },
+ {
+ "inputs": {
+ "up": "up",
+ "down": "down",
+ "left": "left",
+ "right": "right"
+ },
+ "frame": {
+ "x": 17,
+ "y": 195,
+ "width": 120,
+ "height": 120
+ },
+ "extendedEdges": {
+ "top": 15,
+ "bottom": 15,
+ "left": 17,
+ "right": 7
+ }
+ },
+ {
+ "inputs": [
+ "a"
+ ],
+ "frame": {
+ "x": 247,
+ "y": 122,
+ "width": 56,
+ "height": 56
+ },
+ "extendedEdges": {
+ "right": 17,
+ "top": 15
+ }
+ },
+ {
+ "inputs": [
+ "b"
+ ],
+ "frame": {
+ "x": 171,
+ "y": 98,
+ "width": 56,
+ "height": 56
+ }
+ },
+ {
+ "inputs": [
+ "z"
+ ],
+ "frame": {
+ "x": 229,
+ "y": 50,
+ "width": 56,
+ "height": 56
+ },
+ "extendedEdges": {
+ "bottom": 15
+ }
+ },
+ {
+ "inputs": [
+ "l"
+ ],
+ "frame": {
+ "x": 0,
+ "y": 0,
+ "width": 95,
+ "height": 30
+ }
+ },
+ {
+ "inputs": [
+ "r"
+ ],
+ "frame": {
+ "x": 225,
+ "y": 0,
+ "width": 95,
+ "height": 30
+ }
+ },
+ {
+ "inputs": [
+ "start"
+ ],
+ "frame": {
+ "x": 144,
+ "y": 179,
+ "width": 32,
+ "height": 32
+ },
+ "extendedEdges": {
+ "left": 0
+ }
+ },
+ {
+ "inputs": [
+ "menu"
+ ],
+ "frame": {
+ "x": 150,
+ "y": 34,
+ "width": 18,
+ "height": 18
+ }
+ },
+ {
+ "inputs": [
+ "cUp"
+ ],
+ "frame": {
+ "x": 215,
+ "y": 197,
+ "width": 42,
+ "height": 42
+ },
+ "extendedEdges": {
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cDown"
+ ],
+ "frame": {
+ "x": 215,
+ "y": 276,
+ "width": 42,
+ "height": 42
+ },
+ "extendedEdges": {
+ "top": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cLeft"
+ ],
+ "frame": {
+ "x": 177,
+ "y": 236,
+ "width": 42,
+ "height": 42
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cRight"
+ ],
+ "frame": {
+ "x": 255,
+ "y": 237,
+ "width": 42,
+ "height": 42
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 23
+ }
+ }
+ ],
+ "mappingSize": {
+ "width": 320,
+ "height": 328
+ },
+ "extendedEdges": {
+ "top": 7,
+ "bottom": 7,
+ "left": 7,
+ "right": 7
+ }
+ },
+ "landscape": {
+ "assets": {
+ "resizable": "iphone_landscape.pdf"
+ },
+ "items": [
+ {
+ "thumbstick": {
+ "name": "thumbstick_landscape.pdf",
+ "width": 78,
+ "height": 78
+ },
+ "inputs": {
+ "up": "analogStickUp",
+ "down": "analogStickDown",
+ "left": "analogStickLeft",
+ "right": "analogStickRight"
+ },
+ "frame": {
+ "x": 34,
+ "y": 103,
+ "width": 86,
+ "height": 86
+ },
+ "extendedEdges": {
+ "top": 30,
+ "bottom": 30,
+ "left": 34,
+ "right": 30
+ }
+ },
+ {
+ "inputs": {
+ "up": "up",
+ "down": "down",
+ "left": "left",
+ "right": "right"
+ },
+ "frame": {
+ "x": 15,
+ "y": 237,
+ "width": 123,
+ "height": 123
+ }
+ },
+ {
+ "inputs": [
+ "a"
+ ],
+ "frame": {
+ "x": 597,
+ "y": 158,
+ "width": 56,
+ "height": 56
+ }
+ },
+ {
+ "inputs": [
+ "b"
+ ],
+ "frame": {
+ "x": 522,
+ "y": 134,
+ "width": 56,
+ "height": 56
+ }
+ },
+ {
+ "inputs": [
+ "z"
+ ],
+ "frame": {
+ "x": 580,
+ "y": 86,
+ "width": 56,
+ "height": 56
+ }
+ },
+ {
+ "inputs": [
+ "l"
+ ],
+ "frame": {
+ "x": 15,
+ "y": 14,
+ "width": 100,
+ "height": 41
+ }
+ },
+ {
+ "inputs": [
+ "r"
+ ],
+ "frame": {
+ "x": 552,
+ "y": 14,
+ "width": 100,
+ "height": 41
+ }
+ },
+ {
+ "inputs": [
+ "start"
+ ],
+ "frame": {
+ "x": 297,
+ "y": 334,
+ "width": 73,
+ "height": 26
+ }
+ },
+ {
+ "inputs": [
+ "menu"
+ ],
+ "frame": {
+ "x": 299,
+ "y": 15,
+ "width": 73,
+ "height": 26
+ }
+ },
+ {
+ "inputs": [
+ "cUp"
+ ],
+ "frame": {
+ "x": 575,
+ "y": 246,
+ "width": 40,
+ "height": 40
+ },
+ "extendedEdges": {
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cDown"
+ ],
+ "frame": {
+ "x": 575,
+ "y": 320,
+ "width": 40,
+ "height": 40
+ },
+ "extendedEdges": {
+ "top": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cLeft"
+ ],
+ "frame": {
+ "x": 538,
+ "y": 283,
+ "width": 40,
+ "height": 40
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cRight"
+ ],
+ "frame": {
+ "x": 612,
+ "y": 283,
+ "width": 40,
+ "height": 40
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0
+ }
+ }
+ ],
+ "mappingSize": {
+ "width": 667,
+ "height": 375
+ },
+ "extendedEdges": {
+ "top": 15,
+ "bottom": 15,
+ "left": 15,
+ "right": 15
+ },
+ "translucent": true
+ }
+ },
+ "edgeToEdge" : {
+ "portrait": {
+ "assets": {
+ "resizable": "iphone_edgetoedge_portrait.pdf"
+ },
+ "items": [
+ {
+ "thumbstick": {
+ "name": "portrait_thumbstick.pdf",
+ "width": 85,
+ "height": 87
+ },
+ "inputs": {
+ "up": "analogStickUp",
+ "down": "analogStickDown",
+ "left": "analogStickLeft",
+ "right": "analogStickRight"
+ },
+ "frame": {
+ "x": 39,
+ "y": 77,
+ "width": 71,
+ "height": 71
+ },
+ "extendedEdges": {
+ "top": 20,
+ "bottom": 20,
+ "left": 20,
+ "right": 20
+ }
+ },
+ {
+ "inputs": {
+ "up": "up",
+ "down": "down",
+ "left": "left",
+ "right": "right"
+ },
+ "frame": {
+ "x": 17,
+ "y": 195,
+ "width": 120,
+ "height": 120
+ },
+ "extendedEdges": {
+ "top": 15,
+ "bottom": 15,
+ "left": 17,
+ "right": 7
+ }
+ },
+ {
+ "inputs": [
+ "a"
+ ],
+ "frame": {
+ "x": 247,
+ "y": 122,
+ "width": 56,
+ "height": 56
+ },
+ "extendedEdges": {
+ "right": 17,
+ "top": 15
+ }
+ },
+ {
+ "inputs": [
+ "b"
+ ],
+ "frame": {
+ "x": 171,
+ "y": 98,
+ "width": 56,
+ "height": 56
+ }
+ },
+ {
+ "inputs": [
+ "z"
+ ],
+ "frame": {
+ "x": 229,
+ "y": 50,
+ "width": 56,
+ "height": 56
+ },
+ "extendedEdges": {
+ "bottom": 15
+ }
+ },
+ {
+ "inputs": [
+ "l"
+ ],
+ "frame": {
+ "x": 0,
+ "y": 0,
+ "width": 95,
+ "height": 30
+ }
+ },
+ {
+ "inputs": [
+ "r"
+ ],
+ "frame": {
+ "x": 225,
+ "y": 0,
+ "width": 95,
+ "height": 30
+ }
+ },
+ {
+ "inputs": [
+ "start"
+ ],
+ "frame": {
+ "x": 144,
+ "y": 179,
+ "width": 32,
+ "height": 32
+ },
+ "extendedEdges": {
+ "left": 0
+ }
+ },
+ {
+ "inputs": [
+ "menu"
+ ],
+ "frame": {
+ "x": 150,
+ "y": 34,
+ "width": 18,
+ "height": 18
+ }
+ },
+ {
+ "inputs": [
+ "cUp"
+ ],
+ "frame": {
+ "x": 215,
+ "y": 197,
+ "width": 42,
+ "height": 42
+ },
+ "extendedEdges": {
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cDown"
+ ],
+ "frame": {
+ "x": 215,
+ "y": 276,
+ "width": 42,
+ "height": 42
+ },
+ "extendedEdges": {
+ "top": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cLeft"
+ ],
+ "frame": {
+ "x": 177,
+ "y": 236,
+ "width": 42,
+ "height": 42
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cRight"
+ ],
+ "frame": {
+ "x": 255,
+ "y": 237,
+ "width": 42,
+ "height": 42
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 23
+ }
+ }
+ ],
+ "mappingSize": {
+ "width": 320,
+ "height": 362
+ },
+ "extendedEdges": {
+ "top": 7,
+ "bottom": 7,
+ "left": 7,
+ "right": 7
+ }
+ },
+ "landscape": {
+ "assets": {
+ "resizable": "iphone_edgetoedge_landscape.pdf"
+ },
+ "items": [
+ {
+ "thumbstick": {
+ "name": "thumbstick_landscape.pdf",
+ "width": 78,
+ "height": 78
+ },
+ "inputs": {
+ "up": "analogStickUp",
+ "down": "analogStickDown",
+ "left": "analogStickLeft",
+ "right": "analogStickRight"
+ },
+ "frame": {
+ "x": 49,
+ "y": 103,
+ "width": 86,
+ "height": 86
+ },
+ "extendedEdges": {
+ "top": 30,
+ "bottom": 30,
+ "left": 49,
+ "right": 30
+ }
+ },
+ {
+ "inputs": {
+ "up": "up",
+ "down": "down",
+ "left": "left",
+ "right": "right"
+ },
+ "frame": {
+ "x": 30,
+ "y": 237,
+ "width": 123,
+ "height": 123
+ },
+ "extendedEdges": {
+ "left": 30
+ }
+ },
+ {
+ "inputs": [
+ "a"
+ ],
+ "frame": {
+ "x": 727,
+ "y": 158,
+ "width": 56,
+ "height": 56
+ },
+ "extendedEdges": {
+ "right": 30
+ }
+ },
+ {
+ "inputs": [
+ "b"
+ ],
+ "frame": {
+ "x": 652,
+ "y": 134,
+ "width": 56,
+ "height": 56
+ }
+ },
+ {
+ "inputs": [
+ "z"
+ ],
+ "frame": {
+ "x": 710,
+ "y": 86,
+ "width": 56,
+ "height": 56
+ },
+ "extendedEdges": {
+ "right": 46
+ }
+ },
+ {
+ "inputs": [
+ "l"
+ ],
+ "frame": {
+ "x": 15,
+ "y": 14,
+ "width": 100,
+ "height": 41
+ }
+ },
+ {
+ "inputs": [
+ "r"
+ ],
+ "frame": {
+ "x": 697,
+ "y": 14,
+ "width": 100,
+ "height": 41
+ }
+ },
+ {
+ "inputs": [
+ "start"
+ ],
+ "frame": {
+ "x": 370,
+ "y": 334,
+ "width": 73,
+ "height": 26
+ }
+ },
+ {
+ "inputs": [
+ "menu"
+ ],
+ "frame": {
+ "x": 370,
+ "y": 15,
+ "width": 73,
+ "height": 26
+ }
+ },
+ {
+ "inputs": [
+ "cUp"
+ ],
+ "frame": {
+ "x": 705,
+ "y": 246,
+ "width": 40,
+ "height": 40
+ },
+ "extendedEdges": {
+ "bottom": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cDown"
+ ],
+ "frame": {
+ "x": 705,
+ "y": 320,
+ "width": 40,
+ "height": 40
+ },
+ "extendedEdges": {
+ "top": 0,
+ "left": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cLeft"
+ ],
+ "frame": {
+ "x": 668,
+ "y": 283,
+ "width": 40,
+ "height": 40
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "right": 0
+ }
+ },
+ {
+ "inputs": [
+ "cRight"
+ ],
+ "frame": {
+ "x": 742,
+ "y": 283,
+ "width": 40,
+ "height": 40
+ },
+ "extendedEdges": {
+ "top": 0,
+ "bottom": 0,
+ "left": 0,
+ "right": 30
+ }
+ }
+ ],
+ "mappingSize": {
+ "width": 812,
+ "height": 375
+ },
+ "extendedEdges": {
+ "top": 15,
+ "bottom": 15,
+ "left": 15,
+ "right": 15
+ },
+ "translucent": true
+ }
+ }
+ }
+ }
+}
diff --git a/Cores/Mupen64PlusDeltaCore/Resources/iphone_edgetoedge_landscape.pdf b/Cores/Mupen64PlusDeltaCore/Resources/iphone_edgetoedge_landscape.pdf
new file mode 100644
index 000000000..20e606599
Binary files /dev/null and b/Cores/Mupen64PlusDeltaCore/Resources/iphone_edgetoedge_landscape.pdf differ
diff --git a/Cores/Mupen64PlusDeltaCore/Resources/iphone_edgetoedge_portrait.pdf b/Cores/Mupen64PlusDeltaCore/Resources/iphone_edgetoedge_portrait.pdf
new file mode 100644
index 000000000..cebe51cd7
Binary files /dev/null and b/Cores/Mupen64PlusDeltaCore/Resources/iphone_edgetoedge_portrait.pdf differ
diff --git a/Cores/Mupen64PlusDeltaCore/Resources/iphone_landscape.pdf b/Cores/Mupen64PlusDeltaCore/Resources/iphone_landscape.pdf
new file mode 100644
index 000000000..f5eeedf29
Binary files /dev/null and b/Cores/Mupen64PlusDeltaCore/Resources/iphone_landscape.pdf differ
diff --git a/Cores/Mupen64PlusDeltaCore/Resources/iphone_portrait.pdf b/Cores/Mupen64PlusDeltaCore/Resources/iphone_portrait.pdf
new file mode 100644
index 000000000..77cf7fc7b
Binary files /dev/null and b/Cores/Mupen64PlusDeltaCore/Resources/iphone_portrait.pdf differ
diff --git a/Cores/Mupen64PlusDeltaCore/Resources/portrait_thumbstick.pdf b/Cores/Mupen64PlusDeltaCore/Resources/portrait_thumbstick.pdf
new file mode 100644
index 000000000..44c4e3ea9
Binary files /dev/null and b/Cores/Mupen64PlusDeltaCore/Resources/portrait_thumbstick.pdf differ
diff --git a/Cores/Mupen64PlusDeltaCore/Resources/thumbstick_landscape.pdf b/Cores/Mupen64PlusDeltaCore/Resources/thumbstick_landscape.pdf
new file mode 100644
index 000000000..d6352ee48
Binary files /dev/null and b/Cores/Mupen64PlusDeltaCore/Resources/thumbstick_landscape.pdf differ
diff --git a/Cores/Mupen64PlusDeltaCore/Sources/Bridge/Mupen64PlusEmulatorBridge.h b/Cores/Mupen64PlusDeltaCore/Sources/Bridge/Mupen64PlusEmulatorBridge.h
new file mode 100644
index 000000000..edc32ced9
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Sources/Bridge/Mupen64PlusEmulatorBridge.h
@@ -0,0 +1,29 @@
+//
+// Mupen64PlusEmulatorBridge.h
+// Mupen64PlusDeltaCore
+//
+// Created by Riley Testut on 3/27/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import
+#import
+
+@protocol DLTAEmulatorBridging;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Weverything" // Silence "Cannot find protocol definition" warning due to forward declaration.
+__attribute__((visibility("default")))
+@interface Mupen64PlusEmulatorBridge : NSObject
+#pragma clang diagnostic pop
+
+@property (class, nonatomic, readonly) Mupen64PlusEmulatorBridge *sharedBridge;
+
+@property (nonatomic, readonly) AVAudioFormat *preferredAudioFormat;
+@property (nonatomic, readonly) CGSize preferredVideoDimensions;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Cores/Mupen64PlusDeltaCore/Sources/Bridge/Mupen64PlusEmulatorBridge.m b/Cores/Mupen64PlusDeltaCore/Sources/Bridge/Mupen64PlusEmulatorBridge.m
new file mode 100644
index 000000000..2d8a67b55
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Sources/Bridge/Mupen64PlusEmulatorBridge.m
@@ -0,0 +1,953 @@
+//
+// Mupen64PlusEmulatorBridge.m
+// Mupen64PlusDeltaCore
+//
+// Created by Riley Testut on 3/27/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import "Mupen64PlusEmulatorBridge.h"
+
+#import
+
+#if STATIC_LIBRARY
+#import "Mupen64PlusDeltaCore-Swift.h"
+#else
+#import
+#endif
+
+#define M64P_CORE_PROTOTYPES
+#define N64_ANALOG_MAX 80
+
+#include "api/m64p_common.h"
+#include "api/m64p_config.h"
+#include "api/m64p_frontend.h"
+#include "api/m64p_vidext.h"
+#include "api/callbacks.h"
+#include "main/rom.h"
+#include "main/savestates.h"
+#include "main/cheat.h"
+#include "osal/dynamiclib.h"
+#include "main/version.h"
+#include "main/main.h"
+#include "osd.h"
+#include "backends/api/storage_backend.h"
+#include "backends/file_storage.h"
+
+#include "plugin/plugin.h"
+
+#import
+#import
+
+m64p_error CALL Video_PluginStartup(m64p_dynlib_handle CoreLibHandle, void *Context, void (*DebugCallback)(void *, int, const char *));
+m64p_error CALL Video_PluginShutdown(void);
+m64p_error CALL Video_PluginGetVersion(m64p_plugin_type *PluginType, int *PluginVersion, int *APIVersion, const char **PluginNamePtr, int *Capabilities);
+int CALL Video_RomOpen(void);
+void CALL Video_RomClosed(void);
+
+m64p_error CALL RSP_PluginStartup(m64p_dynlib_handle CoreLibHandle, void *Context, void (*DebugCallback)(void *, int, const char *));
+m64p_error CALL RSP_PluginShutdown(void);
+m64p_error CALL RSP_PluginGetVersion(m64p_plugin_type *PluginType, int *PluginVersion, int *APIVersion, const char **PluginNamePtr, int *Capabilities);
+void CALL RSP_RomClosed(void);
+
+@interface Mupen64PlusEmulatorBridge ()
+{
+@public
+ double inputs[20];
+}
+
+@property (nonatomic, copy, nullable, readwrite) NSURL *gameURL;
+
+@property (nonatomic, readonly) NSURL *gameSaveDirectoryURL;
+@property (nonatomic, readonly) NSURL *configDirectoryURL;
+
+@property (nonatomic, assign) BOOL isNTSC;
+@property (nonatomic, assign) double sampleRate;
+
+@property (nonatomic, strong) dispatch_semaphore_t beginFrameSemaphore;
+@property (nonatomic, strong) dispatch_semaphore_t endFrameSemaphore;
+@property (nonatomic, strong) dispatch_semaphore_t stopEmulationSemaphore;
+
+@property (nonatomic) BOOL didLoadPlugins;
+@property (nonatomic, assign, getter=isRunning) BOOL running;
+
+@property (nonatomic, strong, readonly) NSMutableDictionary *stateCallbacks;
+
+@property (nonatomic, strong, readwrite) AVAudioFormat *preferredAudioFormat;
+@property (nonatomic, readwrite) CGSize preferredVideoDimensions;
+
+@property (nonatomic, strong) NSMutableSet *activeCheats;
+@property (nonatomic) m64p_plugin_type activePluginType;
+
+@end
+
+@implementation Mupen64PlusEmulatorBridge
+@synthesize audioRenderer = _audioRenderer;
+@synthesize videoRenderer = _videoRenderer;
+@synthesize saveUpdateHandler = _saveUpdateHandler;
+
+static void MupenDebugCallback(void *context, int level, const char *message)
+{
+ NSLog(@"Mupen (%d): %s", level, message);
+}
+
+static void MupenStateCallback(void *context, m64p_core_param paramType, int newValue)
+{
+ NSLog(@"Mupen: param %d -> %d", paramType, newValue);
+
+ void (^callback)(void) = Mupen64PlusEmulatorBridge.sharedBridge.stateCallbacks[@(paramType)];
+
+ if (callback)
+ {
+ callback();
+ }
+
+ Mupen64PlusEmulatorBridge.sharedBridge.stateCallbacks[@(paramType)] = nil;
+}
+
+static void *dlopen_Mupen64PlusDeltaCore()
+{
+ Dl_info info;
+
+ dladdr((void *)dlopen_Mupen64PlusDeltaCore, &info);
+
+ return dlopen(info.dli_fname, RTLD_LAZY | RTLD_NOLOAD);
+}
+
+static void MupenGetKeys(int Control, BUTTONS *Keys)
+{
+ Keys->R_DPAD = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputRight];
+ Keys->L_DPAD = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputLeft];
+ Keys->D_DPAD = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputDown];
+ Keys->U_DPAD = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputUp];
+ Keys->START_BUTTON = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputStart];
+ Keys->Z_TRIG = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputZ];
+ Keys->B_BUTTON = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputB];
+ Keys->A_BUTTON = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputA];
+ Keys->R_CBUTTON = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputCRight];
+ Keys->L_CBUTTON = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputCLeft];
+ Keys->D_CBUTTON = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputCDown];
+ Keys->U_CBUTTON = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputCUp];
+ Keys->R_TRIG = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputR];
+ Keys->L_TRIG = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputL];
+
+ if (Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputAnalogStickLeft])
+ {
+ Keys->X_AXIS = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputAnalogStickLeft] * -N64_ANALOG_MAX;
+ }
+ else if (Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputAnalogStickRight])
+ {
+ Keys->X_AXIS = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputAnalogStickRight] * N64_ANALOG_MAX;
+ }
+ else
+ {
+ Keys->X_AXIS = 0.0;
+ }
+
+ if (Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputAnalogStickUp])
+ {
+ Keys->Y_AXIS = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputAnalogStickUp] * N64_ANALOG_MAX;
+ }
+ else if (Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputAnalogStickDown])
+ {
+ Keys->Y_AXIS = Mupen64PlusEmulatorBridge.sharedBridge->inputs[Mupen64PlusGameInputAnalogStickDown] * -N64_ANALOG_MAX;
+ }
+ else
+ {
+ Keys->Y_AXIS = 0.0;
+ }
+}
+
+static void MupenInitiateControllers (CONTROL_INFO ControlInfo)
+{
+ ControlInfo.Controls[0].Present = 1;
+ ControlInfo.Controls[0].Plugin = PLUGIN_RAW;
+ ControlInfo.Controls[1].Present = 0;
+ ControlInfo.Controls[1].Plugin = PLUGIN_MEMPAK;
+ ControlInfo.Controls[2].Present = 0;
+ ControlInfo.Controls[2].Plugin = PLUGIN_MEMPAK;
+ ControlInfo.Controls[3].Present = 0;
+ ControlInfo.Controls[3].Plugin = PLUGIN_MEMPAK;
+}
+
+static void MupenControllerCommand(int Control, unsigned char *Command)
+{
+}
+
+static AUDIO_INFO AudioInfo;
+
+static void MupenAudioSampleRateChanged(int SystemType)
+{
+ double previousSampleRate = Mupen64PlusEmulatorBridge.sharedBridge.preferredAudioFormat.sampleRate;
+ double sampleRate = 0.0;
+
+ switch (SystemType)
+ {
+ default:
+ case SYSTEM_NTSC:
+ sampleRate = 48681812 / (*AudioInfo.AI_DACRATE_REG + 1);
+ break;
+ case SYSTEM_PAL:
+ sampleRate = 49656530 / (*AudioInfo.AI_DACRATE_REG + 1);
+ break;
+ }
+
+ NSLog(@"Mupen rate changed %f -> %f\n", previousSampleRate, sampleRate);
+
+ Mupen64PlusEmulatorBridge.sharedBridge.preferredAudioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16 sampleRate:sampleRate channels:2 interleaved:YES];
+}
+
+static void MupenAudioLenChanged()
+{
+ int LenReg = *AudioInfo.AI_LEN_REG;
+ uint8_t *ptr = (uint8_t*)(AudioInfo.RDRAM + (*AudioInfo.AI_DRAM_ADDR_REG & 0xFFFFFF));
+
+ // Swap channels
+ for (uint32_t i = 0; i < LenReg; i += 4)
+ {
+ ptr[i] ^= ptr[i + 2];
+ ptr[i + 2] ^= ptr[i];
+ ptr[i] ^= ptr[i + 2];
+ ptr[i + 1] ^= ptr[i + 3];
+ ptr[i + 3] ^= ptr[i + 1];
+ ptr[i + 1] ^= ptr[i + 3];
+ }
+
+ [Mupen64PlusEmulatorBridge.sharedBridge.audioRenderer.audioBuffer writeBuffer:ptr size:LenReg];
+}
+
+static void SetIsNTSC()
+{
+ switch (ROM_HEADER.Country_code & 0xFF)
+ {
+ case 0x44:
+ case 0x46:
+ case 0x49:
+ case 0x50:
+ case 0x53:
+ case 0x55:
+ case 0x58:
+ case 0x59:
+ Mupen64PlusEmulatorBridge.sharedBridge.isNTSC = NO;
+ break;
+
+ case 0x37:
+ case 0x41:
+ case 0x45:
+ case 0x4a:
+ Mupen64PlusEmulatorBridge.sharedBridge.isNTSC = YES;
+ break;
+ }
+}
+
+static int MupenOpenAudio(AUDIO_INFO info)
+{
+ AudioInfo = info;
+
+ SetIsNTSC();
+
+ return M64ERR_SUCCESS;
+}
+
+static void MupenSetAudioSpeed(int percent)
+{
+}
+
++ (instancetype)sharedBridge
+{
+ static Mupen64PlusEmulatorBridge *_emulatorBridge = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ _emulatorBridge = [[self alloc] init];
+ });
+
+ return _emulatorBridge;
+}
+
+- (instancetype)init
+{
+ self = [super init];
+ if (self)
+ {
+ _beginFrameSemaphore = dispatch_semaphore_create(0);
+ _endFrameSemaphore = dispatch_semaphore_create(0);
+ _stopEmulationSemaphore = dispatch_semaphore_create(0);
+
+ _stateCallbacks = [NSMutableDictionary dictionary];
+
+ _preferredAudioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16 sampleRate:44100 channels:2 interleaved:YES];
+ _preferredVideoDimensions = CGSizeMake(640, 480);
+
+ _activeCheats = [NSMutableSet set];
+ }
+
+ return self;
+}
+
+#pragma mark - Emulation State -
+
+- (void)startWithGameURL:(NSURL *)gameURL
+{
+ self.gameURL = gameURL;
+
+ /* Copy .ini files */
+ NSArray *iniFiles = @[@"GLideN64", @"GLideN64.custom", @"mupen64plus"];
+ for (NSString *filename in iniFiles)
+ {
+ NSURL *sourceURL = [Mupen64PlusEmulatorBridge.n64Resources URLForResource:filename withExtension:@"ini"];
+ NSURL *destinationURL = [[Mupen64PlusEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:filename] URLByAppendingPathExtension:@"ini"];
+
+ if ([[NSFileManager defaultManager] fileExistsAtPath:destinationURL.path isDirectory:nil])
+ {
+ continue;
+ }
+
+ NSError *error = nil;
+ if (![[NSFileManager defaultManager] copyItemAtURL:sourceURL toURL:destinationURL error:&error])
+ {
+ NSLog(@"Error copying %@. %@", filename, error);
+ }
+ }
+
+ /* Prepare Emulation */
+ CoreStartup(FRONTEND_API_VERSION, self.configDirectoryURL.fileSystemRepresentation, Mupen64PlusEmulatorBridge.coreDirectoryURL.fileSystemRepresentation, (__bridge void *)self, MupenDebugCallback, (__bridge void *)self, MupenStateCallback);
+
+ /* Configure Core */
+ m64p_handle config;
+ ConfigOpenSection("Core", &config);
+
+ ConfigSetParameter(config, "SaveSRAMPath", M64TYPE_STRING, self.gameSaveDirectoryURL.fileSystemRepresentation);
+ ConfigSetParameter(config, "SharedDataPath", M64TYPE_STRING, Mupen64PlusEmulatorBridge.coreDirectoryURL.fileSystemRepresentation);
+
+ // Pure Interpreter = 0, Cached Interpreter = 1, Dynamic Recompiler = 2
+ int emulationMode = 1;
+ ConfigSetParameter(config, "R4300Emulator", M64TYPE_INT, &emulationMode);
+
+ ConfigSaveSection("Core");
+
+
+ /* Configure Video */
+ m64p_handle video;
+ ConfigOpenSection("Video-General", &video);
+
+ int useFullscreen = 1;
+ ConfigSetParameter(video, "Fullscreen", M64TYPE_BOOL, &useFullscreen);
+
+ int screenWidth = 640;
+ ConfigSetParameter(video, "ScreenWidth", M64TYPE_INT, &screenWidth);
+
+ int screenHeight = 480;
+ ConfigSetParameter(video, "ScreenHeight", M64TYPE_INT, &screenHeight);
+
+ ConfigSaveSection("Video-General");
+
+
+ /* Configure GLideN64 */
+ m64p_handle gliden64;
+ ConfigOpenSection("Video-GLideN64", &gliden64);
+
+ // 0 = stretch, 1 = 4:3, 2 = 16:9, 3 = adjust
+ int aspectRatio = 1;
+ ConfigSetParameter(gliden64, "AspectRatio", M64TYPE_INT, &aspectRatio);
+
+ int enablePerPixelLighting = 1;
+ ConfigSetParameter(gliden64, "EnableHWLighting", M64TYPE_BOOL, &enablePerPixelLighting);
+
+ int osd = 0;
+ ConfigSetParameter(gliden64, "OnScreenDisplay", M64TYPE_BOOL, &osd);
+ ConfigSetParameter(gliden64, "ShowFPS", M64TYPE_BOOL, &osd);
+ ConfigSetParameter(gliden64, "ShowVIS", M64TYPE_BOOL, &osd);
+ ConfigSetParameter(gliden64, "ShowPercent", M64TYPE_BOOL, &osd);
+ ConfigSetParameter(gliden64, "ShowInternalResolution", M64TYPE_BOOL, &osd);
+ ConfigSetParameter(gliden64, "ShowRenderingResolution", M64TYPE_BOOL, &osd);
+
+ ConfigSaveSection("Video-GLideN64");
+
+ NSData *romData = [NSData dataWithContentsOfURL:gameURL options:NSDataReadingMappedAlways error:nil];
+ if (romData.length == 0)
+ {
+ NSLog(@"Error loading ROM at path: %@\n File does not exist.", gameURL);
+ return;
+ }
+
+ m64p_error openStatus = CoreDoCommand(M64CMD_ROM_OPEN, (int)[romData length], (void *)[romData bytes]);
+ if (openStatus != M64ERR_SUCCESS)
+ {
+ NSLog(@"Error loading ROM at path: %@\n Error code was: %i", gameURL, openStatus);
+ return;
+ }
+
+
+ /* Prepare Audio */
+ audio.aiDacrateChanged = MupenAudioSampleRateChanged;
+ audio.aiLenChanged = MupenAudioLenChanged;
+ audio.initiateAudio = MupenOpenAudio;
+ audio.setSpeedFactor = MupenSetAudioSpeed;
+ plugin_start(M64PLUGIN_AUDIO);
+
+ /* Prepare Input */
+ input.getKeys = MupenGetKeys;
+ input.initiateControllers = MupenInitiateControllers;
+ input.controllerCommand = MupenControllerCommand;
+ plugin_start(M64PLUGIN_INPUT);
+
+ if (![self didLoadPlugins])
+ {
+ /* Prepare Plugins */
+
+#if STATIC_LIBRARY
+ // Ensure symbols are _not_ stripped by referencing them from log statements.
+ NSLog(@"Address of Video_PluginStartup: %p", Video_PluginStartup);
+ NSLog(@"Address of Video_PluginShutdown: %p", Video_PluginShutdown);
+ NSLog(@"Address of Video_PluginGetVersion: %p", Video_PluginGetVersion);
+ NSLog(@"Address of Video_RomOpen: %p", Video_RomOpen);
+ NSLog(@"Address of Video_RomClosed: %p", Video_RomClosed);
+
+ NSLog(@"Address of RSP_PluginStartup: %p", RSP_PluginStartup);
+ NSLog(@"Address of RSP_PluginShutdown: %p", RSP_PluginShutdown);
+ NSLog(@"Address of RSP_PluginGetVersion: %p", RSP_PluginGetVersion);
+ NSLog(@"Address of RSP_RomClosed: %p", RSP_RomClosed);
+#endif
+
+ BOOL didLoadVideoPlugin = [self loadPlugin:@"mupen64plus_video_GLideN64" type:M64PLUGIN_GFX];
+ NSAssert(didLoadVideoPlugin, @"Failed to load video plugin.");
+
+ BOOL didLoadRSPPlugin = [self loadPlugin:@"mupen64plus_rsp_hle" type:M64PLUGIN_RSP];
+ NSAssert(didLoadRSPPlugin, @"Failed to load RSP plugin.");
+
+ self.didLoadPlugins = YES;
+ }
+
+ self.running = YES;
+
+ [NSThread detachNewThreadSelector:@selector(startEmulationLoop) toTarget:self withObject:nil];
+
+ dispatch_semaphore_wait(self.endFrameSemaphore, DISPATCH_TIME_FOREVER);
+}
+
+- (void)startEmulationLoop
+{
+ @autoreleasepool
+ {
+ [self.videoRenderer prepare];
+
+ CoreDoCommand(M64CMD_EXECUTE, 0, NULL);
+
+ dispatch_semaphore_signal(self.stopEmulationSemaphore);
+ }
+}
+
+- (void)stop
+{
+ CoreDoCommand(M64CMD_STOP, 0, NULL);
+
+ dispatch_semaphore_signal(self.beginFrameSemaphore);
+ dispatch_semaphore_wait(self.stopEmulationSemaphore, DISPATCH_TIME_FOREVER);
+
+ CoreDoCommand(M64CMD_ROM_CLOSE, 0, NULL);
+
+ [self.activeCheats removeAllObjects];
+
+ self.running = NO;
+}
+
+- (void)pause
+{
+ self.running = NO;
+}
+
+- (void)resume
+{
+ self.running = YES;
+}
+
+#pragma mark - Game Loop -
+
+- (void)runFrameAndProcessVideo:(BOOL)processVideo
+{
+ dispatch_semaphore_signal(self.beginFrameSemaphore);
+
+ dispatch_semaphore_wait(self.endFrameSemaphore, DISPATCH_TIME_FOREVER);
+}
+
+#pragma mark - Inputs -
+
+- (void)activateInput:(NSInteger)input
+{
+ inputs[input] = 1;
+}
+
+- (void)activateInput:(NSInteger)input value:(double)value
+{
+ inputs[input] = value;
+}
+
+- (void)deactivateInput:(NSInteger)input
+{
+ inputs[input] = 0;
+}
+
+- (void)resetInputs
+{
+ for (NSInteger input = 0; input < 18; input++)
+ {
+ [self deactivateInput:input];
+ }
+}
+
+#pragma mark - Save States -
+
+- (void)saveSaveStateToURL:(NSURL *)url
+{
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
+
+ [self registerCallbackForType:M64CORE_STATE_SAVECOMPLETE callback:^{
+ dispatch_semaphore_signal(semaphore);
+ }];
+
+ CoreDoCommand(M64CMD_STATE_SAVE, 1, (void *)[url fileSystemRepresentation]);
+
+ if (![self isRunning])
+ {
+ [self runFrameAndProcessVideo:YES];
+ }
+
+ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
+}
+
+- (void)loadSaveStateFromURL:(NSURL *)url
+{
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
+
+ [self registerCallbackForType:M64CORE_STATE_LOADCOMPLETE callback:^{
+ dispatch_semaphore_signal(semaphore);
+ }];
+
+ CoreDoCommand(M64CMD_STATE_LOAD, 1, (void *)[url fileSystemRepresentation]);
+
+ if (![self isRunning])
+ {
+ [self runFrameAndProcessVideo:YES];
+ }
+
+ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
+}
+
+#pragma mark - Game Saves -
+
+- (void)saveGameSaveToURL:(NSURL *)url
+{
+ struct file_storage *storage = NULL;
+
+ if (g_dev.cart.use_flashram == -1)
+ {
+ storage = (struct file_storage *)g_dev.cart.sram.storage;
+ }
+ else if (g_dev.cart.use_flashram == 0)
+ {
+ storage = (struct file_storage *)g_dev.cart.eeprom.storage;
+ }
+ else if (g_dev.cart.use_flashram == 1)
+ {
+ storage = (struct file_storage *)g_dev.cart.flashram.storage;
+ }
+
+ NSData *data = [NSData dataWithBytes:storage->data length:storage->size];
+ [data writeToURL:url atomically:YES];
+}
+
+- (void)loadGameSaveFromURL:(NSURL *)url
+{
+ struct file_storage *storage = NULL;
+
+ if (g_dev.cart.use_flashram == -1)
+ {
+ storage = (struct file_storage *)g_dev.cart.sram.storage;
+ }
+ else if (g_dev.cart.use_flashram == 0)
+ {
+ storage = (struct file_storage *)g_dev.cart.eeprom.storage;
+ }
+ else if (g_dev.cart.use_flashram == 1)
+ {
+ storage = (struct file_storage *)g_dev.cart.flashram.storage;
+ }
+
+ NSData *saveData = [NSData dataWithContentsOfURL:url];
+ if (saveData == nil)
+ {
+ memset(storage->data, 0xFF, storage->size);
+ }
+ else
+ {
+ memcpy(storage->data, saveData.bytes, storage->size);
+ }
+}
+
+#pragma mark - Cheats -
+
+- (BOOL)addCheatCode:(NSString *)cheatCode type:(NSString *)type
+{
+ if ([self.activeCheats containsObject:cheatCode])
+ {
+ CoreCheatEnabled([cheatCode UTF8String], 1);
+ return YES;
+ }
+
+ NSArray *codes = [cheatCode componentsSeparatedByString:@"\n"];
+ m64p_cheat_code *codeList = (m64p_cheat_code *)calloc(codes.count, sizeof(m64p_cheat_code));
+
+ for (int i = 0; i < codes.count; i++)
+ {
+ NSString *code = codes[i];
+ code = [code stringByReplacingOccurrencesOfString:@" " withString:@""];
+
+ if (code.length != 12)
+ {
+ return NO;
+ }
+
+ m64p_cheat_code *gsCode = codeList + i;
+
+ NSString *address = [code substringWithRange:NSMakeRange(0, 8)];
+ NSString *value = [code substringWithRange:NSMakeRange(8, 4)];
+
+ unsigned int outAddress = 0;
+ [[NSScanner scannerWithString:address] scanHexInt:&outAddress];
+
+ unsigned int outValue = 0;
+ [[NSScanner scannerWithString:value] scanHexInt:&outValue];
+
+ gsCode->address = outAddress;
+ gsCode->value = outValue;
+ }
+
+ if (CoreAddCheat([cheatCode UTF8String], codeList, codes.count) != M64ERR_SUCCESS)
+ {
+ return NO;
+ }
+
+ [self.activeCheats addObject:cheatCode];
+
+ return YES;
+}
+
+- (void)resetCheats
+{
+ for (NSString *code in self.activeCheats)
+ {
+ CoreCheatEnabled([code UTF8String], 0);
+ }
+}
+
+- (void)updateCheats
+{
+}
+
+#pragma mark - Helper Methods -
+
+- (void)processFrame
+{
+ [self.videoRenderer processFrame];
+}
+
+- (void)videoInterrupt
+{
+ dispatch_semaphore_signal(self.endFrameSemaphore);
+
+ dispatch_semaphore_wait(self.beginFrameSemaphore, DISPATCH_TIME_FOREVER);
+}
+
+- (BOOL)loadPlugin:(NSString *)pluginName type:(m64p_plugin_type)type
+{
+ self.activePluginType = type;
+
+ m64p_dynlib_handle mupen64PlusDeltaCoreHandle = dlopen_Mupen64PlusDeltaCore();
+
+#if STATIC_LIBRARY
+ m64p_dynlib_handle pluginHandle = mupen64PlusDeltaCoreHandle;
+ ptr_PluginStartup pluginStart = (ptr_PluginStartup)osal_dynlib_getproc(pluginHandle, "PluginStartup");
+#else
+ NSString *frameworkPath = [NSString stringWithFormat:@"%@.framework/%@", pluginName, pluginName];
+ NSString *pluginPath = [[[NSBundle mainBundle] privateFrameworksPath] stringByAppendingPathComponent:frameworkPath];
+
+ m64p_dynlib_handle pluginHandle = dlopen([pluginPath fileSystemRepresentation], RTLD_LAZY | RTLD_LOCAL);
+ ptr_PluginStartup pluginStart = dlsym(pluginHandle, "PluginStartup");
+#endif
+
+ m64p_error error = pluginStart(mupen64PlusDeltaCoreHandle, (__bridge void *)self, MupenDebugCallback);
+ if (error != M64ERR_SUCCESS)
+ {
+ NSLog(@"Error code %@ loading plugin of type %@, name: %@", @(error), @(type), pluginName);
+ self.activePluginType = M64PLUGIN_NULL;
+ return NO;
+ }
+
+ error = CoreAttachPlugin(type, pluginHandle);
+ self.activePluginType = M64PLUGIN_NULL;
+
+ if (error != M64ERR_SUCCESS)
+ {
+ NSLog(@"Error code %@ attaching plugin of type %@, name: %@", @(error), @(type), pluginName);
+ return NO;
+ }
+
+ return YES;
+}
+
+- (void)registerCallbackForType:(m64p_core_param)callbackType callback:(void (^)(void))callback
+{
+ self.stateCallbacks[@(callbackType)] = callback;
+}
+
+#pragma mark - Getters/Setters -
+
+- (NSTimeInterval)frameDuration
+{
+ return [self isNTSC] ? (1.0 / 60.0) : (1.0 / 50.0);
+}
+
+- (NSURL *)gameSaveDirectoryURL
+{
+ NSURL *gameSaveDirectoryURL = [Mupen64PlusEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:@"Saves" isDirectory:YES];
+
+ NSError *error = nil;
+ if (![[NSFileManager defaultManager] createDirectoryAtURL:gameSaveDirectoryURL withIntermediateDirectories:YES attributes:nil error:nil])
+ {
+ NSLog(@"Unable to create Game Save Directory. %@", error);
+ }
+
+ return gameSaveDirectoryURL;
+}
+
+- (NSURL *)configDirectoryURL
+{
+ NSURL *configDirectoryURL = [Mupen64PlusEmulatorBridge.coreDirectoryURL URLByAppendingPathComponent:@"Config" isDirectory:YES];
+
+ NSError *error = nil;
+ if (![[NSFileManager defaultManager] createDirectoryAtURL:configDirectoryURL withIntermediateDirectories:YES attributes:nil error:nil])
+ {
+ NSLog(@"Unable to create Config Directory. %@", error);
+ }
+
+ return configDirectoryURL;
+}
+
+@end
+
+#pragma mark - Mupen64Plus Callbacks -
+
+EXPORT m64p_error CALL osal_dynlib_open(m64p_dynlib_handle *pLibHandle, const char *pccLibraryPath)
+{
+ if (pLibHandle == NULL || pccLibraryPath == NULL)
+ {
+ return M64ERR_INPUT_ASSERT;
+ }
+
+ *pLibHandle = dlopen(pccLibraryPath, RTLD_NOW);
+
+ if (*pLibHandle == NULL)
+ {
+ return M64ERR_INPUT_NOT_FOUND;
+ }
+
+ return M64ERR_SUCCESS;
+}
+
+EXPORT m64p_function CALL osal_dynlib_getproc(m64p_dynlib_handle LibHandle, const char *pccProcedureName)
+{
+ if (pccProcedureName == NULL)
+ {
+ return NULL;
+ }
+
+#if STATIC_LIBRARY
+ const char *getVersion = "PluginGetVersion";
+ const char *romOpen = "RomOpen";
+ const char *romClosed = "RomClosed";
+ const char *pluginStartup = "PluginStartup";
+ const char *pluginShutdown = "PluginShutdown";
+
+ if (strncmp(pccProcedureName, getVersion, strlen(getVersion)) == 0 ||
+ strncmp(pccProcedureName, pluginStartup, strlen(pluginStartup)) == 0 ||
+ strncmp(pccProcedureName, pluginShutdown, strlen(pluginShutdown)) == 0 ||
+ strncmp(pccProcedureName, romOpen, strlen(romOpen)) == 0 ||
+ strncmp(pccProcedureName, romClosed, strlen(romClosed)) == 0)
+ {
+ const char *prefix = "";
+
+ switch (Mupen64PlusEmulatorBridge.sharedBridge.activePluginType)
+ {
+ case M64PLUGIN_GFX:
+ prefix = "Video_";
+ break;
+
+ case M64PLUGIN_RSP:
+ prefix = "RSP_";
+ break;
+
+ default:
+ break;
+ }
+
+ char prefixedName[64];
+ prefixedName[0] = '\0';
+
+ strcat(prefixedName, prefix);
+ strcat(prefixedName, pccProcedureName);
+
+ m64p_function address = (m64p_function)dlsym(LibHandle, prefixedName);
+ return address;
+ }
+#endif
+
+ m64p_function address = (m64p_function)dlsym(LibHandle, pccProcedureName);
+ return address;
+}
+
+EXPORT m64p_error CALL osal_dynlib_close(m64p_dynlib_handle LibHandle)
+{
+ int result = dlclose(LibHandle);
+ if (result != 0)
+ {
+ return M64ERR_INTERNAL;
+ }
+
+ return M64ERR_SUCCESS;
+}
+
+
+EXPORT m64p_error CALL VidExt_Init(void)
+{
+ return M64ERR_SUCCESS;
+}
+
+EXPORT m64p_error CALL VidExt_Quit(void)
+{
+ return M64ERR_SUCCESS;
+}
+
+EXPORT m64p_error CALL VidExt_ListFullscreenModes(m64p_2d_size *SizeArray, int *NumSizes)
+{
+ *NumSizes = 0;
+ return M64ERR_SUCCESS;
+}
+
+EXPORT m64p_error CALL VidExt_SetVideoMode(int Width, int Height, int BitsPerPixel, m64p_video_mode ScreenMode, m64p_video_flags Flags)
+{
+ Mupen64PlusEmulatorBridge.sharedBridge.preferredVideoDimensions = CGSizeMake(Width, Height);
+ return M64ERR_SUCCESS;
+}
+
+EXPORT m64p_error CALL VidExt_SetCaption(const char *Title)
+{
+ NSLog(@"Mupen caption: %s", Title);
+ return M64ERR_SUCCESS;
+}
+
+EXPORT m64p_error CALL VidExt_ToggleFullScreen(void)
+{
+ return M64ERR_UNSUPPORTED;
+}
+
+EXPORT m64p_function CALL VidExt_GL_GetProcAddress(const char* Proc)
+{
+ return (m64p_function)dlsym(RTLD_NEXT, Proc);
+}
+
+EXPORT m64p_error CALL VidExt_GL_SetAttribute(m64p_GLattr Attr, int Value)
+{
+ return M64ERR_UNSUPPORTED;
+}
+
+EXPORT m64p_error CALL VidExt_GL_GetAttribute(m64p_GLattr Attr, int *pValue)
+{
+ return M64ERR_UNSUPPORTED;
+}
+
+EXPORT m64p_error CALL VidExt_GL_SwapBuffers(void)
+{
+ [Mupen64PlusEmulatorBridge.sharedBridge.videoRenderer processFrame];
+
+ return M64ERR_SUCCESS;
+}
+
+EXPORT m64p_error OverrideVideoFunctions(m64p_video_extension_functions *VideoFunctionStruct)
+{
+ return M64ERR_SUCCESS;
+}
+
+EXPORT m64p_error CALL VidExt_ResizeWindow(int width, int height)
+{
+ return M64ERR_SUCCESS;
+}
+
+EXPORT int VidExt_InFullscreenMode(void)
+{
+ return 1;
+}
+
+EXPORT int VidExt_VideoRunning(void)
+{
+ return Mupen64PlusEmulatorBridge.sharedBridge.isRunning;
+}
+
+EXPORT void new_vi(void)
+{
+ struct r4300_core* r4300 = &g_dev.r4300;
+
+ if (g_gs_vi_counter < 60)
+ {
+ if (g_gs_vi_counter == 0)
+ {
+ cheat_apply_cheats(&g_cheat_ctx, r4300, ENTRY_BOOT);
+ }
+
+ g_gs_vi_counter = 60;
+ }
+ else
+ {
+ cheat_apply_cheats(&g_cheat_ctx, r4300, ENTRY_VI);
+ }
+
+ [Mupen64PlusEmulatorBridge.sharedBridge videoInterrupt];
+}
+
+EXPORT void ScreenshotRomOpen(void)
+{
+}
+
+EXPORT void TakeScreenshot(int iFrameNumber)
+{
+}
+
+EXPORT osd_message_t * osd_message_valid(osd_message_t *m)
+{
+ return NULL;
+}
+
+EXPORT int event_set_core_defaults(void)
+{
+ return 1;
+}
+
+EXPORT void event_initialize(void)
+{
+}
+
+EXPORT void event_sdl_keydown(int keysym, int keymod)
+{
+}
+
+EXPORT void event_sdl_keyup(int keysym, int keymod)
+{
+}
+
+EXPORT int event_gameshark_active(void)
+{
+ return 1;
+}
+
+EXPORT void event_set_gameshark(int active)
+{
+}
diff --git a/Cores/Mupen64PlusDeltaCore/Sources/Mupen64Plus.swift b/Cores/Mupen64PlusDeltaCore/Sources/Mupen64Plus.swift
new file mode 100644
index 000000000..f1538dd7f
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Sources/Mupen64Plus.swift
@@ -0,0 +1,106 @@
+//
+// Mupen64Plus.swift
+// Mupen64PlusDeltaCore
+//
+// Created by Riley Testut on 3/27/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+import Foundation
+import AVFoundation
+
+import DeltaCore
+
+#if !STATIC_LIBRARY
+public extension GameType
+{
+ static let n64 = GameType("com.rileytestut.delta.game.n64")
+}
+
+public extension CheatType
+{
+ static let gameShark = CheatType("GameShark")
+}
+#endif
+
+@objc public enum Mupen64PlusGameInput: Int, Input
+{
+ // D-Pad
+ case up = 0
+ case down = 1
+ case left = 2
+ case right = 3
+
+ // Analog-Stick
+ case analogStickUp = 4
+ case analogStickDown = 5
+ case analogStickLeft = 6
+ case analogStickRight = 7
+
+ // C-Buttons
+ case cUp = 8
+ case cDown = 9
+ case cLeft = 10
+ case cRight = 11
+
+ // Other
+ case a = 12
+ case b = 13
+ case l = 14
+ case r = 15
+ case z = 16
+ case start = 17
+
+ public var type: InputType {
+ return .game(.n64)
+ }
+
+ public var isContinuous: Bool {
+ switch self
+ {
+ case .analogStickUp, .analogStickDown, .analogStickLeft, .analogStickRight: return true
+ default: return false
+ }
+ }
+}
+
+public struct Mupen64Plus: DeltaCoreProtocol
+{
+ public static let core = Mupen64Plus()
+
+ public var name: String { "Mupen64Plus" }
+ public var identifier: String { "com.rileytestut.N64DeltaCore" }
+
+ public var gameType: GameType { GameType.n64 }
+ public var gameInputType: Input.Type { Mupen64PlusGameInput.self }
+ public var gameSaveFileExtension: String { "sav" }
+
+ public var audioFormat: AVAudioFormat { Mupen64PlusEmulatorBridge.shared.preferredAudioFormat }
+ public var videoFormat: VideoFormat { VideoFormat(format: .openGLES, dimensions: Mupen64PlusEmulatorBridge.shared.preferredVideoDimensions) }
+
+ public var supportedCheatFormats: Set {
+ let gameSharkFormat = CheatFormat(name: NSLocalizedString("GameShark", comment: ""), format: "XXXXXXXX YYYY", type: .gameShark)
+ return [gameSharkFormat]
+ }
+
+ public var emulatorBridge: EmulatorBridging { Mupen64PlusEmulatorBridge.shared }
+
+ private init()
+ {
+ }
+}
+
+// Expose DeltaCore properties to Objective-C.
+public extension Mupen64PlusEmulatorBridge
+{
+ @objc(n64Resources) class var __n64Resources: Bundle {
+ return Mupen64Plus.core.resourceBundle
+ }
+
+ @objc(coreDirectoryURL) class var __coreDirectoryURL: URL {
+ return _coreDirectoryURL
+ }
+}
+
+private let _coreDirectoryURL = Mupen64Plus.core.directoryURL
+
diff --git a/Cores/Mupen64PlusDeltaCore/Sources/Mupen64PlusDeltaCore.h b/Cores/Mupen64PlusDeltaCore/Sources/Mupen64PlusDeltaCore.h
new file mode 100644
index 000000000..3c8133b09
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Sources/Mupen64PlusDeltaCore.h
@@ -0,0 +1,21 @@
+//
+// Mupen64PlusDeltaCore.h
+// Mupen64PlusDeltaCore
+//
+// Created by Riley Testut on 3/27/19.
+// Copyright © 2019 Riley Testut. All rights reserved.
+//
+
+#import
+
+//! Project version number for N64DeltaCore.
+FOUNDATION_EXPORT double Mupen64PlusDeltaCoreVersionNumber;
+
+//! Project version string for N64DeltaCore.
+FOUNDATION_EXPORT const unsigned char Mupen64PlusDeltaCoreVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+
+#if !STATIC_LIBRARY
+#import
+#endif
diff --git a/Cores/Mupen64PlusDeltaCore/Sources/TxDbg_ios.mm b/Cores/Mupen64PlusDeltaCore/Sources/TxDbg_ios.mm
new file mode 100644
index 000000000..9dc721002
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Sources/TxDbg_ios.mm
@@ -0,0 +1,57 @@
+/*
+ * Texture Filtering
+ * Version: 1.0
+ *
+ * Copyright (C) 2007 Hiroshi Morii All Rights Reserved.
+ * Email koolsmoky(at)users.sourceforge.net
+ * Web http://www.3dfxzone.it/koolsmoky
+ *
+ * this is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * this is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GNU Make; see the file COPYING. If not, write to
+ * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#define DBG_LEVEL 80
+
+#include "TxDbg.h"
+#include
+#include
+
+#include
+
+TxDbg::TxDbg()
+{
+ _level = DBG_LEVEL;
+}
+
+TxDbg::~TxDbg()
+{}
+
+void
+TxDbg::output(const int level, const wchar_t *format, ...)
+{
+ if (level > _level)
+ return;
+
+ va_list va;
+
+ NSString *nsformat = [[NSString alloc] initWithBytes:format
+ length:wcslen(format)*sizeof(*format)
+ encoding:NSUTF32LittleEndianStringEncoding];
+
+ nsformat = [@"GLideN64: " stringByAppendingString:nsformat];
+
+ va_start(va, format);
+ NSLogv(nsformat, va);
+ va_end(va);
+}
diff --git a/Cores/Mupen64PlusDeltaCore/Sources/Types/Mupen64PlusTypes.h b/Cores/Mupen64PlusDeltaCore/Sources/Types/Mupen64PlusTypes.h
new file mode 100644
index 000000000..460d974e0
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Sources/Types/Mupen64PlusTypes.h
@@ -0,0 +1,14 @@
+//
+// Mupen64PlusTypes.h
+// Mupen64PlusDeltaCore
+//
+// Created by Riley Testut on 1/30/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+#import
+
+// Extensible Enums
+FOUNDATION_EXPORT GameType const GameTypeN64 NS_SWIFT_NAME(n64);
+
+FOUNDATION_EXPORT CheatType const CheatTypeGameShark;
diff --git a/Cores/Mupen64PlusDeltaCore/Sources/mupen64plus-rsp-hle-plugin.c b/Cores/Mupen64PlusDeltaCore/Sources/mupen64plus-rsp-hle-plugin.c
new file mode 100644
index 000000000..da404c5e3
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Sources/mupen64plus-rsp-hle-plugin.c
@@ -0,0 +1,20 @@
+//
+// mupen64plus-rsp-hle-plugin.c
+// Mupen64PlusDeltaCore
+//
+// Created by Riley Testut on 2/1/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+// Add RSP prefix to function names so they don't collide with Video plug-in functions.
+#define PluginStartup(A, B, C) RSP_PluginStartup(A, B, C)
+#define PluginShutdown(A) RSP_PluginShutdown(A)
+#define PluginGetVersion(A, B, C, D, E) RSP_PluginGetVersion(A, B, C, D, E)
+#define RomClosed(A) RSP_RomClosed(A)
+
+#include "../Mupen64Plus/mupen64plus-rsp-hle/src/plugin.c"
+
+#undef PluginStartup
+#undef PluginShutdown
+#undef PluginGetVersion
+#undef RomClosed
diff --git a/Cores/Mupen64PlusDeltaCore/Sources/mupen64plus-video-GLideN64-plugin.cpp b/Cores/Mupen64PlusDeltaCore/Sources/mupen64plus-video-GLideN64-plugin.cpp
new file mode 100644
index 000000000..283c5a541
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/Sources/mupen64plus-video-GLideN64-plugin.cpp
@@ -0,0 +1,85 @@
+//
+// plugin_delta.c
+// N64DeltaCore-Video
+//
+// Created by Riley Testut on 2/1/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+// Add Video prefix to function names so they don't collide with RSP plug-in functions.
+#define PluginStartup(A, ...) Video_PluginStartup(A, ## __VA_ARGS__)
+#define PluginShutdown(A) Video_PluginShutdown(A)
+#define PluginGetVersion(A, B, C, D, E) Video_PluginGetVersion(A, B, C, D, E)
+#define RomOpen(A) Video_RomOpen(A)
+#define RomClosed(A) Video_RomClosed(A)
+
+// Explicitly include m64p_config.h _before_ renaming all Config functions so their header declarations remain the same.
+#define M64P_CORE_PROTOTYPES
+#include "../Mupen64Plus/mupen64plus-core/src/api/m64p_config.h"
+#undef M64P_CORE_PROTOTYPES
+
+#define ConfigGetSharedDataFilepath Video_ConfigGetSharedDataFilepath
+#define ConfigGetUserConfigPath Video_ConfigGetUserConfigPath
+#define ConfigGetUserDataPath Video_ConfigGetUserDataPath
+#define ConfigGetUserCachePath Video_ConfigGetUserCachePath
+#define ConfigOpenSection Video_ConfigOpenSection
+#define ConfigDeleteSection Video_ConfigDeleteSection
+#define ConfigSaveSection Video_ConfigSaveSection
+#define ConfigSaveFile Video_ConfigSaveFile
+#define ConfigSetParameter Video_ConfigSetParameter
+#define ConfigGetParameter Video_ConfigGetParameter
+#define ConfigGetParameterHelp Video_ConfigGetParameterHelp
+#define ConfigSetDefaultInt Video_ConfigSetDefaultInt
+#define ConfigSetDefaultFloat Video_ConfigSetDefaultFloat
+#define ConfigSetDefaultBool Video_ConfigSetDefaultBool
+#define ConfigSetDefaultString Video_ConfigSetDefaultString
+#define ConfigGetParamInt Video_ConfigGetParamInt
+#define ConfigGetParamFloat Video_ConfigGetParamFloat
+#define ConfigGetParamBool Video_ConfigGetParamBool
+#define ConfigGetParamString Video_ConfigGetParamString
+#define ConfigExternalGetParameter Video_ConfigExternalGetParameter
+#define ConfigExternalOpen Video_ConfigExternalOpen
+#define ConfigExternalClose Video_ConfigExternalClose
+
+#include "../Mupen64Plus/GLideN64/src/CommonPluginAPI.cpp"
+#include "../Mupen64Plus/GLideN64/src/MupenPlusPluginAPI.cpp"
+#include "../Mupen64Plus/GLideN64/src/common/CommonAPIImpl_common.cpp"
+
+extern "C" m64p_function CALL osal_dynlib_getproc(m64p_dynlib_handle LibHandle, const char *pccProcedureName);
+
+// Replace dlsym calls with our own osal_dynlib_getproc implementation.
+#define dlsym(A, B) osal_dynlib_getproc(A, B)
+#include "../Mupen64Plus/GLideN64/src/mupenplus/MupenPlusAPIImpl.cpp"
+#undef dlsym
+
+#undef ConfigGetSharedDataFilepath
+#undef ConfigGetUserConfigPath
+#undef ConfigGetUserDataPath
+#undef ConfigGetUserCachePath
+#undef ConfigOpenSection
+#undef ConfigDeleteSection
+#undef ConfigSaveSection
+#undef ConfigSaveFile
+#undef ConfigSetParameter
+#undef ConfigGetParameter
+#undef ConfigGetParameterHelp
+#undef ConfigSetDefaultInt
+#undef ConfigSetDefaultFloat
+#undef ConfigSetDefaultBool
+#undef ConfigSetDefaultString
+#undef ConfigGetParamInt
+#undef ConfigGetParamFloat
+#undef ConfigGetParamBool
+#undef ConfigGetParamString
+#undef ConfigExternalGetParameter
+#undef ConfigExternalOpen
+#undef ConfigExternalClose
+
+#undef PluginStartup
+#undef PluginShutdown
+#undef PluginGetVersion
+#undef RomOpen
+#undef RomClosed
+
+#include "../Mupen64Plus/GLideN64/src/mupenplus/Config_mupenplus.cpp"
+#include "../Mupen64Plus/GLideN64/src/mupenplus/CommonAPIImpl_mupenplus.cpp"
diff --git a/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL.h b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL.h
new file mode 100644
index 000000000..30c7f8584
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL.h
@@ -0,0 +1,54 @@
+/*
+ Copyright (c) 2010 OpenEmu Team
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the OpenEmu Team nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SDL_H
+#define SDL_H
+
+#include
+#include
+#include
+#include
+#include
+
+#include "SDL_thread.h"
+
+typedef uint32_t Uint32;
+
+__BEGIN_DECLS
+
+Uint32 SDL_GetTicks(void);
+
+void SDL_Quit(void);
+
+void SDL_Delay(Uint32 ms);
+
+void SDL_PumpEvents(void);
+
+void SDL_GL_SwapBuffers(void);
+
+__END_DECLS
+
+#endif
\ No newline at end of file
diff --git a/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDLStubs.m b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDLStubs.m
new file mode 100644
index 000000000..d023ca889
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDLStubs.m
@@ -0,0 +1,196 @@
+/*
+ Copyright (c) 2010 OpenEmu Team
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the OpenEmu Team nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SDL.h"
+#include "SDL_thread.h"
+#import
+#include
+#include
+#include
+#import
+
+static double mach_to_sec = 0;
+
+static void init_mach_time(void)
+{
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ if (mach_to_sec == 0.0)
+ {
+ struct mach_timebase_info base;
+ mach_timebase_info(&base);
+ mach_to_sec = 1e-9 * (base.numer / (double)base.denom);
+ }
+ });
+}
+
+NSTimeInterval MupenOEMonotonicTime(void)
+{
+ init_mach_time();
+
+ return mach_absolute_time() * mach_to_sec;
+}
+
+SDL_mutex *SDL_CreateMutex(void)
+{
+ pthread_mutex_t *m = malloc(sizeof(pthread_mutex_t));
+ pthread_mutex_init(m, NULL);
+
+ return (SDL_mutex*)m;
+}
+
+int SDL_LockMutex(SDL_mutex *m)
+{
+ return pthread_mutex_lock((pthread_mutex_t*)m);
+}
+
+int SDL_UnlockMutex(SDL_mutex *m)
+{
+ return pthread_mutex_unlock((pthread_mutex_t*)m);
+}
+
+void SDL_DestroyMutex(SDL_mutex *m)
+{
+ pthread_mutex_destroy((pthread_mutex_t*)m);
+ free(m);
+}
+
+Uint32 SDL_GetTicks(void)
+{
+ return MupenOEMonotonicTime();
+}
+
+void SDL_Quit(void)
+{
+}
+
+void SDL_Delay(Uint32 ms)
+{
+ usleep(ms * 1000);
+}
+
+void SDL_PumpEvents(void)
+{
+}
+
+void SDL_GL_SwapBuffers(void)
+{
+ NSLog(@"Mupen warning: Should not reach here");
+}
+
+SDL_cond *SDL_CreateCond(void)
+{
+ pthread_cond_t *cond = malloc(sizeof(pthread_cond_t));
+ pthread_cond_init(cond, NULL);
+
+ return (SDL_cond*)cond;
+}
+
+int SDL_CondWait(SDL_cond *cond, SDL_mutex *mut)
+{
+ return pthread_cond_wait((pthread_cond_t*)cond, (pthread_mutex_t*)mut);
+}
+
+int SDL_CondSignal(SDL_cond *cond)
+{
+ return pthread_cond_signal((pthread_cond_t*)cond);
+}
+
+void SDL_DestroyCond(SDL_cond *cond)
+{
+ pthread_cond_destroy((pthread_cond_t*)cond);
+ free(cond);
+}
+
+static struct {
+ const char *thread_name;
+ void *thread_context;
+} sContext;
+
+void *Fake_SDL_New_Thread(void *p)
+{
+ pthread_setname_np(sContext.thread_name);
+ int (*fn)(void *) = p;
+ return (void*)fn(sContext.thread_context);
+}
+
+SDL_Thread *SDL_CreateThread(int (*fn)(void *), const char *name, void *context)
+{
+ pthread_t *thread = malloc(sizeof(pthread_t));
+
+ sContext.thread_name = name;
+ sContext.thread_context = context;
+ pthread_create(thread, NULL, Fake_SDL_New_Thread, fn);
+ return (SDL_Thread*)thread;
+}
+
+void SDL_WaitThread(SDL_Thread *thread, int *status)
+{
+ void *_status;
+
+ pthread_join(*((pthread_t*)thread), &_status);
+ *status = (int)_status;
+ free(thread);
+}
+
+SDL_sem *SDL_CreateSemaphore(int initial_value)
+{
+ sem_t *semaphore = (sem_t *)malloc(sizeof(sem_t));
+ sem_init(semaphore, 0, initial_value);
+
+ return (SDL_sem*)semaphore;
+}
+
+int SDL_SemPost(SDL_sem *sem)
+{
+ int retval;
+
+ if ( ! sem ) {
+ // Passed a NULL semaphore
+ return -1;
+ }
+
+ retval = sem_post((sem_t *)sem);
+
+ return retval;
+}
+
+int SDL_SemTryWait(SDL_sem *sem)
+{
+ int retval;
+
+ if ( ! sem ) {
+ // Passed a NULL semaphore
+ return -1;
+ }
+
+ retval = 1;
+
+ if ( sem_trywait((sem_t *)sem) == 0 ) {
+ retval = 0;
+ }
+ return retval;
+}
diff --git a/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_config.h b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_config.h
new file mode 100644
index 000000000..9f5bf01b5
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_config.h
@@ -0,0 +1,30 @@
+/*
+ Copyright (c) 2015 OpenEmu Team
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the OpenEmu Team nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SDL_CONFIG_H
+#define SDL_CONFIG_H
+
+#endif
diff --git a/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_opengl.h b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_opengl.h
new file mode 100644
index 000000000..3050b27bf
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_opengl.h
@@ -0,0 +1,42 @@
+/*
+ Copyright (c) 2010 OpenEmu Team
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the OpenEmu Team nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include
+#import
+
+#ifndef APIENTRY
+#define APIENTRY
+#endif
+#ifndef APIENTRYP
+#define APIENTRYP APIENTRY *
+#endif
+#ifndef GLAPI
+#define GLAPI extern
+#endif
+
+// Stuff that doesn't exist
+
+#define GL_MIRRORED_REPEAT_IBM 0
diff --git a/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_opengles2.h b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_opengles2.h
new file mode 100644
index 000000000..d3630ad04
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_opengles2.h
@@ -0,0 +1,50 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2016 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * \file SDL_opengles2.h
+ *
+ * This is a simple file to encapsulate the OpenGL ES 2.0 API headers.
+ */
+#ifndef _MSC_VER
+
+//#ifdef __IPHONEOS__
+#include
+#include
+//#else
+//#include
+//#include
+//#include
+//#endif
+
+#else /* _MSC_VER */
+
+/* OpenGL ES2 headers for Visual Studio */
+#include "SDL_opengles2_khrplatform.h"
+#include "SDL_opengles2_gl2platform.h"
+#include "SDL_opengles2_gl2.h"
+#include "SDL_opengles2_gl2ext.h"
+
+#endif /* _MSC_VER */
+
+#ifndef APIENTRY
+#define APIENTRY GL_APIENTRY
+#endif
diff --git a/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_thread.h b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_thread.h
new file mode 100644
index 000000000..53d8fd71a
--- /dev/null
+++ b/Cores/Mupen64PlusDeltaCore/libMupen64Plus/SDL/SDL_thread.h
@@ -0,0 +1,58 @@
+/*
+ Copyright (c) 2010 OpenEmu Team
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the OpenEmu Team nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SDL_THREAD_H
+#define SDL_THREAD_H
+
+#define SDL_VERSION_ATLEAST(x,y,z) 1
+
+typedef void SDL_mutex;
+typedef void SDL_cond;
+typedef void SDL_Thread;
+typedef void SDL_sem;
+
+__BEGIN_DECLS
+
+SDL_mutex *SDL_CreateMutex(void);
+int SDL_LockMutex(SDL_mutex *m);
+int SDL_UnlockMutex(SDL_mutex *m);
+void SDL_DestroyMutex(SDL_mutex *m);
+
+SDL_cond *SDL_CreateCond(void);
+int SDL_CondWait(SDL_cond *cond, SDL_mutex *mut);
+int SDL_CondSignal(SDL_cond *cond);
+void SDL_DestroyCond(SDL_cond *cond);
+
+SDL_Thread *SDL_CreateThread(int (*fn)(void *), const char *name, void *context);
+void SDL_WaitThread(SDL_Thread *thread, int *status);
+
+SDL_sem *SDL_CreateSemaphore(int initial_value);
+int SDL_SemPost(SDL_sem *sem);
+int SDL_SemTryWait(SDL_sem *sem);
+
+__END_DECLS
+
+#endif
\ No newline at end of file
diff --git a/Cores/N64DeltaCore b/Cores/N64DeltaCore
deleted file mode 160000
index d92746745..000000000
--- a/Cores/N64DeltaCore
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit d92746745d52ea255579c707e005cfd57c4c85b7
diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj
deleted file mode 100644
index 68c0cd783..000000000
--- a/Delta.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,1438 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 46;
- objects = {
-
-/* Begin PBXAggregateTarget section */
- BF14D8941DE7A512002CA1BE /* mogenerator */ = {
- isa = PBXAggregateTarget;
- buildConfigurationList = BF14D8971DE7A512002CA1BE /* Build configuration list for PBXAggregateTarget "mogenerator" */;
- buildPhases = (
- BF14D8981DE7A519002CA1BE /* mogenerator */,
- );
- dependencies = (
- );
- name = mogenerator;
- productName = mogenerator;
- };
-/* End PBXAggregateTarget section */
-
-/* Begin PBXBuildFile section */
- 1FA4ABA79AB72914FE414A61 /* libPods-Delta.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */; };
- BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */; };
- BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */; };
- BF1020E31F95B05B00313182 /* DeltaToDelta2.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF1020E21F95B05B00313182 /* DeltaToDelta2.xcmappingmodel */; };
- BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF107EC31BF413F000E0C32C /* GamesViewController.swift */; };
- BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */; };
- BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */; };
- BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */; };
- BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF15AF831F54B43B009B6AAB /* ActionInput.swift */; };
- BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */; };
- BF1DAD5D1D9F576000E752A7 /* PreferredControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1DAD5C1D9F576000E752A7 /* PreferredControllerSkinsViewController.swift */; };
- BF1F45A421AF274D00EF9895 /* SyncResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */; };
- BF1F45AB21AF4B5800EF9895 /* SyncResultsViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */; };
- BF1F45AD21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */; };
- BF1F45BF21AF676F00EF9895 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45BE21AF676F00EF9895 /* Box.swift */; };
- BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2451BB73FE800CCF94A /* Assets.xcassets */; };
- BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */; };
- BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF31878A1D489AAA00BD020D /* CheatValidator.swift */; };
- BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */; };
- BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34FA101CF1899D006624C7 /* CheatTextView.swift */; };
- BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF11C5D7FB000C1184C /* PauseViewController.swift */; };
- BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF353FF41C5D837600C1184C /* PauseMenu.storyboard */; };
- BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF81C5D870B00C1184C /* MenuItem.swift */; };
- BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FFD1C5DA3C500C1184C /* PausePresentationController.swift */; };
- BF3540001C5DA3C500C1184C /* PausePresentationControllerContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF353FFE1C5DA3C500C1184C /* PausePresentationControllerContentView.xib */; };
- BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */; };
- BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540071C5DAFAD00C1184C /* PauseTransitionCoordinator.swift */; };
- BF3D6C512202865F0083E05A /* Delta2ToDelta3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF3D6C502202865F0083E05A /* Delta2ToDelta3.xcmappingmodel */; };
- BF3D6C53220286750083E05A /* Delta3ToDelta4.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF3D6C52220286750083E05A /* Delta3ToDelta4.xcmappingmodel */; };
- BF4828841F9027B600028B97 /* Delta.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BF4828811F9027B600028B97 /* Delta.xcdatamodeld */; };
- BF4828861F9028F500028B97 /* System.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF4828851F9028F500028B97 /* System.swift */; };
- BF4828881F90290F00028B97 /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF4828871F90290F00028B97 /* Action.swift */; };
- BF48F74E219A16DA00BC2FC1 /* SyncingServicesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF48F74D219A16DA00BC2FC1 /* SyncingServicesViewController.swift */; };
- BF525EE81FF5F370004AA849 /* DeepLinkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF525EE71FF5F370004AA849 /* DeepLinkController.swift */; };
- BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF525EE91FF6CD12004AA849 /* DeepLink.swift */; };
- BF56450D220239B800A8EA26 /* GameControllerInputMappingMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56450C220239B800A8EA26 /* GameControllerInputMappingMigrationPolicy.swift */; };
- BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942611E09BBB10051894B /* LoadControllerSkinImageOperation.swift */; };
- BF5942661E09BBB10051894B /* LoadImageURLOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942631E09BBB10051894B /* LoadImageURLOperation.swift */; };
- BF59426A1E09BBD00051894B /* GridCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942681E09BBD00051894B /* GridCollectionViewCell.swift */; };
- BF59426B1E09BBD00051894B /* GridCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942691E09BBD00051894B /* GridCollectionViewLayout.swift */; };
- BF59426F1E09BC5D0051894B /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF59426D1E09BC5D0051894B /* DatabaseManager.swift */; };
- BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF59426E1E09BC5D0051894B /* GamesDatabase.swift */; };
- BF59427C1E09BC830051894B /* Cheat.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942771E09BC830051894B /* Cheat.swift */; };
- BF59427D1E09BC830051894B /* ControllerSkin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942781E09BC830051894B /* ControllerSkin.swift */; };
- BF59427E1E09BC830051894B /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942791E09BC830051894B /* Game.swift */; };
- BF59427F1E09BC830051894B /* GameCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF59427A1E09BC830051894B /* GameCollection.swift */; };
- BF5942801E09BC830051894B /* SaveState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF59427B1E09BC830051894B /* SaveState.swift */; };
- BF5942861E09BC8B0051894B /* _Cheat.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942811E09BC8B0051894B /* _Cheat.swift */; };
- BF5942871E09BC8B0051894B /* _ControllerSkin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942821E09BC8B0051894B /* _ControllerSkin.swift */; };
- BF5942881E09BC8B0051894B /* _Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942831E09BC8B0051894B /* _Game.swift */; };
- BF5942891E09BC8B0051894B /* _GameCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942841E09BC8B0051894B /* _GameCollection.swift */; };
- BF59428A1E09BC8B0051894B /* _SaveState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942851E09BC8B0051894B /* _SaveState.swift */; };
- BF59428E1E09BCFB0051894B /* ImportController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF59428D1E09BCFB0051894B /* ImportController.swift */; };
- BF5942931E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5942901E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m */; };
- BF5942941E09BD1A0051894B /* NSManagedObject+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942911E09BD1A0051894B /* NSManagedObject+Conveniences.swift */; };
- BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */; };
- BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */; };
- BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF5E7F451B9A652600AE44F8 /* Settings.storyboard */; };
- BF63A1A321A4AAAE00EE8F61 /* RecordSyncStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63A1A221A4AAAE00EE8F61 /* RecordSyncStatusViewController.swift */; };
- BF63A1B521A4B76E00EE8F61 /* RecordVersionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63A1B421A4B76E00EE8F61 /* RecordVersionsViewController.swift */; };
- BF6424831F5B8F3F00D6AB44 /* ListMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6424821F5B8F3F00D6AB44 /* ListMenuViewController.swift */; };
- BF6424851F5CBDC900D6AB44 /* UIView+ParentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */; };
- BF647A6A22FB8FCE0061D76D /* Bundle+SwizzleBundleID.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF647A6922FB8FCE0061D76D /* Bundle+SwizzleBundleID.swift */; };
- BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */; };
- BF696B801D9B2B02009639E0 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF696B7F1D9B2B02009639E0 /* Theme.swift */; };
- BF69FBA223E375A20051BEEA /* libVBA-M.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF69FBA123E375A20051BEEA /* libVBA-M.a */; };
- BF69FBA823E396860051BEEA /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF69FBA723E3967B0051BEEA /* libz.tbd */; };
- BF69FBAA23E399AA0051BEEA /* CoreMotion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF69FBA923E399AA0051BEEA /* CoreMotion.framework */; };
- BF69FBC923E3A8380051BEEA /* libNestopia.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF69FBC823E3A8380051BEEA /* libNestopia.a */; };
- BF6BF3131EB7E47F008E83CD /* ImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3121EB7E47F008E83CD /* ImportOption.swift */; };
- BF6BF3181EB82111008E83CD /* iTunesImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3171EB82111008E83CD /* iTunesImportOption.swift */; };
- BF6BF31A1EB82146008E83CD /* ClipboardImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3191EB82146008E83CD /* ClipboardImportOption.swift */; };
- BF6BF31C1EB821A0008E83CD /* GamesDatabaseImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF31B1EB821A0008E83CD /* GamesDatabaseImportOption.swift */; };
- BF6BF3211EB82362008E83CD /* GamesDatabase.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF6BF31F1EB82362008E83CD /* GamesDatabase.storyboard */; };
- BF6BF3271EB87EB8008E83CD /* PhotoLibraryImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */; };
- BF6EE5E91F7C5F860051AD6C /* _GameControllerInputMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EE5E81F7C5F860051AD6C /* _GameControllerInputMapping.swift */; };
- BF6EE5EB1F7C5F8F0051AD6C /* GameControllerInputMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EE5EA1F7C5F8F0051AD6C /* GameControllerInputMapping.swift */; };
- BF713C0822499ED4004A1A2B /* PreviousHarmony.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BF713C0622499ED3004A1A2B /* PreviousHarmony.xcdatamodeld */; };
- BF71CF871FE90006001F1613 /* AppIconShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF71CF861FE90006001F1613 /* AppIconShortcutsViewController.swift */; };
- BF71CF8A1FE904B1001F1613 /* GameTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF71CF891FE904B1001F1613 /* GameTableViewCell.xib */; };
- BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */; };
- BF7AE8081C2E858400B1B5BC /* GridMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8041C2E858400B1B5BC /* GridMenuViewController.swift */; };
- BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */; };
- BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */; };
- BF8A333421A484A000A42FD4 /* BadgedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8A333321A484A000A42FD4 /* BadgedTableViewCell.swift */; };
- BF8A334621A4926F00A42FD4 /* GameSyncStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8A334521A4926F00A42FD4 /* GameSyncStatusViewController.swift */; };
- BF8CA9361F5F651900499FDD /* PopoverMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CA9351F5F651900499FDD /* PopoverMenuController.swift */; };
- BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */; };
- BF95E2771E4977BF0030E7AD /* GameMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF95E2761E4977BF0030E7AD /* GameMetadata.swift */; };
- BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */; };
- BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */; };
- BFA0D1271D3AE1F600565894 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63BDE91D389EEB00FCB040 /* GameViewController.swift */; };
- BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAA1FEC1B8AA4FA00495943 /* Settings.swift */; };
- BFAB9F7D219A43380080EC7D /* SyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAB9F7C219A43380080EC7D /* SyncManager.swift */; };
- BFAB9F88219A4B670080EC7D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = BFAB9F87219A4B670080EC7D /* GoogleService-Info.plist */; };
- BFB3645823245A6000CD0EB1 /* LicensesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB3645723245A6000CD0EB1 /* LicensesViewController.swift */; };
- BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */; };
- BFC1F2CC22F9515F00606A45 /* CopyDeepLinkActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F2CB22F9515F00606A45 /* CopyDeepLinkActivity.swift */; };
- BFC3628021ADE2BA00EF2BE6 /* UIAlertController+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC3627F21ADE2BA00EF2BE6 /* UIAlertController+Error.swift */; };
- BFC6F7B81F435BC500221B96 /* Input+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC6F7B71F435BC500221B96 /* Input+Display.swift */; };
- BFC9B7391CEFCD34008629BB /* CheatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */; };
- BFCEA67E1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */; };
- BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */; };
- BFD1EF402336BD8800D197CF /* UIDevice+Processor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD1EF3F2336BD8800D197CF /* UIDevice+Processor.swift */; };
- BFDB0FEC24464758001C727C /* DS.png in Resources */ = {isa = PBXBuildFile; fileRef = BFDB0FEB24464757001C727C /* DS.png */; };
- BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB3417219E4B1700595A62 /* SyncStatusViewController.swift */; };
- BFDCA1E6244EBAA900B8FBDB /* liblibDeSmuME.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFDCA1E5244EBAA900B8FBDB /* liblibDeSmuME.a */; };
- BFDCA1E9244F7E1000B8FBDB /* Delta5ToDelta6.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BFDCA1E8244F7E1000B8FBDB /* Delta5ToDelta6.xcmappingmodel */; };
- BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */; };
- BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */; };
- BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */; };
- BFE56E1923EB7BE00014FECD /* UIImage+SymbolFallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */; };
- BFE593CA21F3F8B7003412A6 /* GameSave.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE593C921F3F8B7003412A6 /* GameSave.swift */; };
- BFE593CC21F3F8C2003412A6 /* _GameSave.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE593CB21F3F8C2003412A6 /* _GameSave.swift */; };
- BFE9908024451E15006409A7 /* MelonDSCoreSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE9907F24451E15006409A7 /* MelonDSCoreSettingsViewController.swift */; };
- BFEE943D23F2180200CDA07D /* Delta4ToDelta5.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BFEE943C23F2180200CDA07D /* Delta4ToDelta5.xcmappingmodel */; };
- BFEF24F31F7DD4FD00454C62 /* SaveStateMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF24F21F7DD4FB00454C62 /* SaveStateMigrationPolicy.swift */; };
- BFF6452E1F7CC5060056533E /* GameControllerInputMappingTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6B82A41F7CC2A300042BFB /* GameControllerInputMappingTransformer.swift */; };
- BFFA4C091E8A24D600D87934 /* GameTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA4C081E8A24D600D87934 /* GameTableViewCell.swift */; };
- BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */; };
- BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFFA71E01AAC406100EE9DD1 /* Main.storyboard */; };
- BFFBD3D9224A0756002EFC79 /* URL+ExtendedAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFBD3D8224A0756002EFC79 /* URL+ExtendedAttributes.swift */; };
- BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */; };
- BFFC461F1D59823500AF2CC6 /* GamesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461C1D59823500AF2CC6 /* GamesStoryboardSegue.swift */; };
- BFFC46201D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461D1D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift */; };
- BFFC46231D5984A000AF2CC6 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC46221D5984A000AF2CC6 /* LaunchViewController.swift */; };
- BFFC46461D59861000AF2CC6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFFC46441D59861000AF2CC6 /* LaunchScreen.storyboard */; };
- BFFC464C1D5998D600AF2CC6 /* CheatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC464B1D5998D600AF2CC6 /* CheatTableViewCell.swift */; };
- BFFDF03723E3BB2600931B96 /* libSnes9x.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF03623E3BB2600931B96 /* libSnes9x.a */; };
- BFFDF03F23E3C28A00931B96 /* libGambatte.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF03D23E3C0F000931B96 /* libGambatte.a */; };
- BFFDF04623E3D3A600931B96 /* libMupen64Plus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF04523E3D3A600931B96 /* libMupen64Plus.a */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXFileReference section */
- 22506DA00971C4300AF90A35 /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- A19FF50F55441BC2B2248241 /* Pods-Delta.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.release.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.release.xcconfig"; sourceTree = ""; };
- BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = openvgdb.sqlite; sourceTree = ""; };
- BF0418131D01E93400E85BCF /* GBADeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBADeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerSkinsViewController.swift; sourceTree = ""; };
- BF07200E219A3A9500F05DA4 /* ZIPFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ZIPFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BF0758DE2202827C005110F2 /* Delta 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Delta 4.xcdatamodel"; sourceTree = ""; };
- BF090CF11B490D8300DCAB45 /* Delta-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Delta-Bridging-Header.h"; sourceTree = ""; };
- BF1020E21F95B05B00313182 /* DeltaToDelta2.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = DeltaToDelta2.xcmappingmodel; sourceTree = ""; };
- BF107EC31BF413F000E0C32C /* GamesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesViewController.swift; sourceTree = ""; };
- BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllersSettingsViewController.swift; sourceTree = ""; };
- BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewGameViewController.swift; sourceTree = ""; };
- BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EmulatorCore+Cheats.swift"; sourceTree = ""; };
- BF15AF831F54B43B009B6AAB /* ActionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionInput.swift; sourceTree = ""; };
- BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Importing.swift"; sourceTree = ""; };
- BF1DAD5C1D9F576000E752A7 /* PreferredControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferredControllerSkinsViewController.swift; sourceTree = ""; };
- BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncResultViewController.swift; sourceTree = ""; };
- BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SyncResultsViewController.storyboard; sourceTree = ""; };
- BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HarmonyMetadataKey+Keys.swift"; sourceTree = ""; };
- BF1F45BE21AF676F00EF9895 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; };
- BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = ""; };
- BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods.framework; path = "Pods/../build/Debug-appletvos/Pods.framework"; sourceTree = ""; };
- BF27CC941BCB7B7A00A20D89 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.0.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; };
- BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesCollectionHeaderView.swift; sourceTree = ""; };
- BF30AC25244E88BE00F0C744 /* libMelonDSDeltaCore.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libMelonDSDeltaCore.a; sourceTree = BUILT_PRODUCTS_DIR; };
- BF31878A1D489AAA00BD020D /* CheatValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatValidator.swift; sourceTree = ""; };
- BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditCheatViewController.swift; sourceTree = ""; };
- BF34FA101CF1899D006624C7 /* CheatTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatTextView.swift; sourceTree = ""; };
- BF353FF11C5D7FB000C1184C /* PauseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PauseViewController.swift; sourceTree = ""; };
- BF353FF51C5D837600C1184C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PauseMenu.storyboard; sourceTree = ""; };
- BF353FF81C5D870B00C1184C /* MenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuItem.swift; sourceTree = ""; };
- BF353FFD1C5DA3C500C1184C /* PausePresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PausePresentationController.swift; sourceTree = ""; };
- BF353FFE1C5DA3C500C1184C /* PausePresentationControllerContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = PausePresentationControllerContentView.xib; path = Delta/Base.lproj/PausePresentationControllerContentView.xib; sourceTree = SOURCE_ROOT; };
- BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PauseStoryboardSegue.swift; sourceTree = ""; };
- BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesViewController.swift; sourceTree = ""; };
- BF3540071C5DAFAD00C1184C /* PauseTransitionCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PauseTransitionCoordinator.swift; sourceTree = ""; };
- BF3D6C502202865F0083E05A /* Delta2ToDelta3.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = Delta2ToDelta3.xcmappingmodel; sourceTree = ""; };
- BF3D6C52220286750083E05A /* Delta3ToDelta4.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = Delta3ToDelta4.xcmappingmodel; sourceTree = ""; };
- BF4828821F9027B600028B97 /* Delta 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Delta 2.xcdatamodel"; sourceTree = ""; };
- BF4828831F9027B600028B97 /* Delta.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Delta.xcdatamodel; sourceTree = ""; };
- BF4828851F9028F500028B97 /* System.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = System.swift; sourceTree = ""; };
- BF4828871F90290F00028B97 /* Action.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = ""; };
- BF48F74D219A16DA00BC2FC1 /* SyncingServicesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncingServicesViewController.swift; sourceTree = ""; };
- BF48F754219A1EEB00BC2FC1 /* Harmony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Harmony.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BF48F75A219A1F8300BC2FC1 /* Harmony_Drive.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Harmony_Drive.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BF525EE71FF5F370004AA849 /* DeepLinkController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkController.swift; sourceTree = ""; };
- BF525EE91FF6CD12004AA849 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; };
- BF5645092202381000A8EA26 /* Delta 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Delta 3.xcdatamodel"; sourceTree = ""; };
- BF56450C220239B800A8EA26 /* GameControllerInputMappingMigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerInputMappingMigrationPolicy.swift; sourceTree = ""; };
- BF5942611E09BBB10051894B /* LoadControllerSkinImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadControllerSkinImageOperation.swift; sourceTree = ""; };
- BF5942631E09BBB10051894B /* LoadImageURLOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadImageURLOperation.swift; sourceTree = ""; };
- BF5942681E09BBD00051894B /* GridCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridCollectionViewCell.swift; sourceTree = ""; };
- BF5942691E09BBD00051894B /* GridCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridCollectionViewLayout.swift; sourceTree = ""; };
- BF59426D1E09BC5D0051894B /* DatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; };
- BF59426E1E09BC5D0051894B /* GamesDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesDatabase.swift; sourceTree = ""; };
- BF5942771E09BC830051894B /* Cheat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cheat.swift; sourceTree = ""; };
- BF5942781E09BC830051894B /* ControllerSkin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerSkin.swift; sourceTree = ""; };
- BF5942791E09BC830051894B /* Game.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Game.swift; sourceTree = ""; };
- BF59427A1E09BC830051894B /* GameCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameCollection.swift; sourceTree = ""; };
- BF59427B1E09BC830051894B /* SaveState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveState.swift; sourceTree = ""; };
- BF5942811E09BC8B0051894B /* _Cheat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _Cheat.swift; sourceTree = ""; };
- BF5942821E09BC8B0051894B /* _ControllerSkin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _ControllerSkin.swift; sourceTree = ""; };
- BF5942831E09BC8B0051894B /* _Game.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _Game.swift; sourceTree = ""; };
- BF5942841E09BC8B0051894B /* _GameCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _GameCollection.swift; sourceTree = ""; };
- BF5942851E09BC8B0051894B /* _SaveState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _SaveState.swift; sourceTree = ""; };
- BF59428B1E09BC930051894B /* ControllerSkinConfigurations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ControllerSkinConfigurations.h; sourceTree = ""; };
- BF59428D1E09BCFB0051894B /* ImportController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportController.swift; sourceTree = ""; };
- BF59428F1E09BD1A0051894B /* NSFetchedResultsController+Conveniences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSFetchedResultsController+Conveniences.h"; sourceTree = ""; };
- BF5942901E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSFetchedResultsController+Conveniences.m"; sourceTree = ""; };
- BF5942911E09BD1A0051894B /* NSManagedObject+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Conveniences.swift"; sourceTree = ""; };
- BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = ""; };
- BF5ACE3823E23D6500BD0F20 /* libVBA-M.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libVBA-M.a"; sourceTree = BUILT_PRODUCTS_DIR; };
- BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; };
- BF5E7F451B9A652600AE44F8 /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Settings.storyboard; path = Delta/Base.lproj/Settings.storyboard; sourceTree = SOURCE_ROOT; };
- BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerInputsViewController.swift; sourceTree = ""; };
- BF63A1A221A4AAAE00EE8F61 /* RecordSyncStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordSyncStatusViewController.swift; sourceTree = ""; };
- BF63A1B421A4B76E00EE8F61 /* RecordVersionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordVersionsViewController.swift; sourceTree = ""; };
- BF63BDE91D389EEB00FCB040 /* GameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = ""; };
- BF6424821F5B8F3F00D6AB44 /* ListMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListMenuViewController.swift; sourceTree = ""; };
- BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+ParentViewController.swift"; sourceTree = ""; };
- BF647A6922FB8FCE0061D76D /* Bundle+SwizzleBundleID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+SwizzleBundleID.swift"; sourceTree = ""; };
- BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ControllerSkin+Configuring.swift"; sourceTree = ""; };
- BF696B7F1D9B2B02009639E0 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; };
- BF69FBA123E375A20051BEEA /* libVBA-M.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libVBA-M.a"; sourceTree = BUILT_PRODUCTS_DIR; };
- BF69FBA723E3967B0051BEEA /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
- BF69FBA923E399AA0051BEEA /* CoreMotion.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMotion.framework; path = System/Library/Frameworks/CoreMotion.framework; sourceTree = SDKROOT; };
- BF69FBC823E3A8380051BEEA /* libNestopia.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libNestopia.a; sourceTree = BUILT_PRODUCTS_DIR; };
- BF6B82A41F7CC2A300042BFB /* GameControllerInputMappingTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerInputMappingTransformer.swift; sourceTree = ""; };
- BF6BB2451BB73FE800CCF94A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- BF6BF3121EB7E47F008E83CD /* ImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportOption.swift; sourceTree = ""; };
- BF6BF3171EB82111008E83CD /* iTunesImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iTunesImportOption.swift; sourceTree = ""; };
- BF6BF3191EB82146008E83CD /* ClipboardImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClipboardImportOption.swift; sourceTree = ""; };
- BF6BF31B1EB821A0008E83CD /* GamesDatabaseImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesDatabaseImportOption.swift; sourceTree = ""; };
- BF6BF3201EB82362008E83CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/GamesDatabase.storyboard; sourceTree = ""; };
- BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoLibraryImportOption.swift; sourceTree = ""; };
- BF6EE5E81F7C5F860051AD6C /* _GameControllerInputMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _GameControllerInputMapping.swift; sourceTree = ""; };
- BF6EE5EA1F7C5F8F0051AD6C /* GameControllerInputMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerInputMapping.swift; sourceTree = ""; };
- BF70798B1B6B464B0019077C /* ZipZap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ZipZap.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BF713C0722499ED3004A1A2B /* Harmony.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Harmony.xcdatamodel; sourceTree = ""; };
- BF71CF861FE90006001F1613 /* AppIconShortcutsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconShortcutsViewController.swift; sourceTree = ""; };
- BF71CF891FE904B1001F1613 /* GameTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = GameTableViewCell.xib; sourceTree = ""; };
- BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+FontSize.swift"; sourceTree = ""; };
- BF79966C224C075A009B094F /* N64DeltaCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = N64DeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BF7AE8041C2E858400B1B5BC /* GridMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMenuViewController.swift; sourceTree = ""; };
- BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Delta.swift"; sourceTree = ""; };
- BF8A333321A484A000A42FD4 /* BadgedTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgedTableViewCell.swift; sourceTree = ""; };
- BF8A334521A4926F00A42FD4 /* GameSyncStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameSyncStatusViewController.swift; sourceTree = ""; };
- BF8CA9351F5F651900499FDD /* PopoverMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverMenuController.swift; sourceTree = ""; };
- BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputCalloutView.swift; sourceTree = ""; };
- BF95E2761E4977BF0030E7AD /* GameMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameMetadata.swift; sourceTree = ""; };
- BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesDatabaseBrowserViewController.swift; sourceTree = ""; };
- BF98C9812204D9A1006B95AC /* NESDeltaCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = NESDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerSkinTableViewCell.swift; sourceTree = ""; };
- BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BFAA1FEC1B8AA4FA00495943 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; };
- BFAB9F7C219A43380080EC7D /* SyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncManager.swift; sourceTree = ""; };
- BFAB9F87219A4B670080EC7D /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
- BFB359412278FD6700CFD920 /* N64DeltaCore_Video.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = N64DeltaCore_Video.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BFB359422278FD6800CFD920 /* N64DeltaCore_RSP.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = N64DeltaCore_RSP.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BFB3645723245A6000CD0EB1 /* LicensesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LicensesViewController.swift; sourceTree = ""; };
- BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DeltaCoreProtocol+Delta.swift"; sourceTree = ""; };
- BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SNESDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BFC1F2CB22F9515F00606A45 /* CopyDeepLinkActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyDeepLinkActivity.swift; sourceTree = ""; };
- BFC3627F21ADE2BA00EF2BE6 /* UIAlertController+Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Error.swift"; sourceTree = ""; };
- BFC6F7B71F435BC500221B96 /* Input+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Input+Display.swift"; sourceTree = ""; };
- BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatsViewController.swift; sourceTree = ""; };
- BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewControllerContextTransitioning+Conveniences.swift"; sourceTree = ""; };
- BFD1EF3F2336BD8800D197CF /* UIDevice+Processor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Processor.swift"; sourceTree = ""; };
- BFDB0FEB24464757001C727C /* DS.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = DS.png; sourceTree = ""; };
- BFDB3417219E4B1700595A62 /* SyncStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatusViewController.swift; sourceTree = ""; };
- BFDCA1E5244EBAA900B8FBDB /* liblibDeSmuME.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblibDeSmuME.a; sourceTree = BUILT_PRODUCTS_DIR; };
- BFDCA1E7244F7DB100B8FBDB /* Delta 6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Delta 6.xcdatamodel"; sourceTree = ""; };
- BFDCA1E8244F7E1000B8FBDB /* Delta5ToDelta6.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = Delta5ToDelta6.xcmappingmodel; sourceTree = ""; };
- BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameCollectionViewController.swift; sourceTree = ""; };
- BFDE2CC6222DF345008038E0 /* Harmony_Dropbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Harmony_Dropbox.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BFDE2CC7222DF345008038E0 /* Alamofire.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BFDE2CC8222DF345008038E0 /* SwiftyDropbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SwiftyDropbox.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BFDF71DA22F94CDF0074D92E /* DSDeltaCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = DSDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverMenuButton.swift; sourceTree = ""; };
- BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesStoryboardSegue.swift; sourceTree = ""; };
- BFE4275223EDF75300E6B417 /* Delta 5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Delta 5.xcdatamodel"; sourceTree = ""; };
- BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SymbolFallback.swift"; sourceTree = ""; };
- BFE593C921F3F8B7003412A6 /* GameSave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameSave.swift; sourceTree = ""; };
- BFE593CB21F3F8C2003412A6 /* _GameSave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _GameSave.swift; sourceTree = ""; };
- BFE9907F24451E15006409A7 /* MelonDSCoreSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MelonDSCoreSettingsViewController.swift; sourceTree = ""; };
- BFEC732C1AAECC4A00650035 /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BFEE943C23F2180200CDA07D /* Delta4ToDelta5.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = Delta4ToDelta5.xcmappingmodel; sourceTree = ""; };
- BFEF24F21F7DD4FB00454C62 /* SaveStateMigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveStateMigrationPolicy.swift; sourceTree = ""; };
- BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBCDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- BFFA4C081E8A24D600D87934 /* GameTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameTableViewCell.swift; sourceTree = ""; };
- BFFA71D71AAC406100EE9DD1 /* Delta.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Delta.app; sourceTree = BUILT_PRODUCTS_DIR; };
- BFFA71DB1AAC406100EE9DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- BFFA71E11AAC406100EE9DD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
- BFFBD3D8224A0756002EFC79 /* URL+ExtendedAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+ExtendedAttributes.swift"; sourceTree = ""; };
- BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesPresentationController.swift; sourceTree = "