diff --git a/dialect.yml b/dialect.yml new file mode 100644 index 0000000..ba619a4 --- /dev/null +++ b/dialect.yml @@ -0,0 +1,51 @@ +version: 4 + +output: + ver-mac/src/dialect.swift: + type: swift + +RootComponent: + type: component + oneliners: + - countText: print(c.countText) + - countText: RootVM.shared!.countText = c.countText + shoulds: + count: + 1. Upon hitting 10 the first time: + if: + c.recentField == F.didClickIncrement && + c.count == 7 + then: + c.count += 10 + 2. Upon each button click: + if: + c.recentField == F.didClickIncrement + then: + c.count += 1 + countText: + 1. Upon each count change + if: + c.recentField == F.count + then: + c.countText = "Count: '\(c.count)'" + 2. Upon launch + if: + c.recentField == F.didLaunch + then: + c.countText = "Press the button to count" + didLaunch: + 1. Only once during the first setup: + if: + c.recentField == F.didSetup && + c.didLaunch == false + then: + c.didLaunch = true + +RootContext: + type: context + fields: + count: Int + countText: String + didClickIncrement: Bool + didLaunch: Bool + didSetup: Bool diff --git a/game/ver-mac/Info.plist b/game/ver-mac/Info.plist new file mode 100644 index 0000000..dc46284 --- /dev/null +++ b/game/ver-mac/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleName + OGS Mahjong + CFBundleDisplayName + OGS Mahjong + CFBundleIdentifier + org.opengamestudio.Mahjong + CFBundleExecutable + ogs-mahjong + CFBundleIconFile + AppIcon + CFBundlePackageType + APPL + CFBundleShortVersionString + 2.0.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 11.0 + NSHighResolutionCapable + + + diff --git a/game/ver-mac/Package.swift b/game/ver-mac/Package.swift new file mode 100644 index 0000000..e2b4221 --- /dev/null +++ b/game/ver-mac/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "OGSMahjong", + platforms: [ + .macOS(.v13) + ], + targets: [ + .executableTarget( + name: "ogs-mahjong", + path: "src" + ) + ] +) diff --git a/game/ver-mac/Resources/AppIcon.icns b/game/ver-mac/Resources/AppIcon.icns new file mode 100644 index 0000000..0fb5686 Binary files /dev/null and b/game/ver-mac/Resources/AppIcon.icns differ diff --git a/game/ver-mac/src/AppDelegate.swift b/game/ver-mac/src/AppDelegate.swift new file mode 100644 index 0000000..73db6af --- /dev/null +++ b/game/ver-mac/src/AppDelegate.swift @@ -0,0 +1,21 @@ +import AppKit + +final class AppDelegate: + NSObject, + NSApplicationDelegate +{ + let root = RootComponent() + let rootVM = RootVM() + + func applicationDidFinishLaunching(_ notification: Notification) { + // Bring the app to the foreground + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + + root.setup() + } + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + true + } +} diff --git a/game/ver-mac/src/GameApp.swift b/game/ver-mac/src/GameApp.swift new file mode 100644 index 0000000..65825ae --- /dev/null +++ b/game/ver-mac/src/GameApp.swift @@ -0,0 +1,14 @@ +import AppKit +import SwiftUI + +@main +struct GameApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + + var body: some Scene { + WindowGroup { + RootView(appDelegate.rootVM) + } + .defaultSize(width: 640, height: 480) + } +} diff --git a/game/ver-mac/src/dialect.swift b/game/ver-mac/src/dialect.swift new file mode 100644 index 0000000..93af8f7 --- /dev/null +++ b/game/ver-mac/src/dialect.swift @@ -0,0 +1,276 @@ + +// Reusable "none" constant +public let DIALECT_CONTEXT_RECENT_FIELD_NONE = "none" + +// Base protocol for each component's state +public protocol DialectContext { + var recentField: String { get set } + + func field(_ name: String) -> T + func fieldAny(_ name: String) -> Any + mutating func setField(_ name: String, _ value: Any) +} + +// Default implementation of `fieldAny` method for the protocol +public extension DialectContext { + func fieldAny(_ name: String) -> Any { + return field(name) + } +} + +// The engine of the dialect components +public class DialectController { + var callbacks = [(DialectContext) -> Void]() + var context: DialectContext + var functions = [(DialectContext) -> DialectContext]() + var isProcessingQueue = false + var queue = [DialectContext]() + + public init(_ context: DialectContext) { + self.context = context + } + + func executeFunctions() { + let c = queue.removeFirst() + // Keep SSOT: Only allow single field change per should-function + context.recentField = c.recentField + context.setField(c.recentField, c.fieldAny(c.recentField)) + + for f in functions { + let ctx = f(context) + if ctx.recentField != DIALECT_CONTEXT_RECENT_FIELD_NONE { + queue.append(ctx) + } + } + + reportContext() + } + + func processQueue() { + // Prevent recursion. + if isProcessingQueue { + return + } + + isProcessingQueue = true + + while (queue.count > 0) { + executeFunctions() + } + + isProcessingQueue = false + } + + public func registerCallback(_ cb: @escaping (DialectContext) -> Void) { + callbacks.append(cb) + } + + public func registerFieldCallback( + _ fieldName: String, + _ cb: @escaping (DialectContext) -> Void + ) { + callbacks.append({ c in + if c.recentField == fieldName { + cb(c) + } + }) + } + + public func registerFunction(_ f: @escaping (DialectContext) -> DialectContext) { + functions.append(f) + } + + func reportContext() { + for cb in callbacks { + cb(context) + } + } + + public func set( + _ fieldName: String, + _ value: Any + ) { + var c = context + c.setField(fieldName, value) + c.recentField = fieldName + queue.append(c) + processQueue() + } +} + +// Register several oneliner callbacks to a controller +func registerOneliners( + _ ctrl: DialectController, + _ items: [Any] +) -> T? { + let halfCount = items.count / 2 + for i in 0.. Void + ctrl.registerFieldCallback(field) { cc in + let c = cc as! T + callback(c) + } + } + + // A hack for generics to operate + return nil +} + +// Context field names for static type check +struct F { + static let count = "count" + static let countText = "countText" + static let didClickIncrement = "didClickIncrement" + static let didLaunch = "didLaunch" + static let didSetup = "didSetup" + +} +struct RootContext: DialectContext { + var count = Int() + var countText = String() + var didClickIncrement = Bool() + var didLaunch = Bool() + var didSetup = Bool() + + var recentField = "" + + func field(_ name: String) -> T { + if (name == "count") { + return count as! T + } + else if (name == "countText") { + return countText as! T + } + else if (name == "didClickIncrement") { + return didClickIncrement as! T + } + else if (name == "didLaunch") { + return didLaunch as! T + } + else if (name == "didSetup") { + return didSetup as! T + } + + return "unknown-field-name" as! T + } + + mutating func setField( + _ name: String, + _ value: Any + ) { + if (name == "count") { + count = value as! Int + } + else if (name == "countText") { + countText = value as! String + } + else if (name == "didClickIncrement") { + didClickIncrement = value as! Bool + } + else if (name == "didLaunch") { + didLaunch = value as! Bool + } + else if (name == "didSetup") { + didSetup = value as! Bool + } + + } +} + +func rootSet( + _ key: String, + _ value: Any +) { + RootComponent.singleton!.ctrl.set(key, value) +} + +func rootShouldResetCount(_ c: RootContext) -> RootContext { + var c = c + + /* 1. Upon hitting 10 the first time */ + if + c.recentField == F.didClickIncrement && + c.count == 7 + { + c.count += 10 + c.recentField = F.count + return c + } + + /* 2. Upon each button click */ + if + c.recentField == F.didClickIncrement + { + c.count += 1 + c.recentField = F.count + return c + } + + + c.recentField = DIALECT_CONTEXT_RECENT_FIELD_NONE + return c +} + +func rootShouldResetCountText(_ c: RootContext) -> RootContext { + var c = c + + /* 1. Upon each count chang */ + if + c.recentField == F.count + { + c.countText = "Count: '\(c.count)'" + c.recentField = F.countText + return c + } + + /* 2. Upon launc */ + if + c.recentField == F.didLaunch + { + c.countText = "Press the button to count" + c.recentField = F.countText + return c + } + + + c.recentField = DIALECT_CONTEXT_RECENT_FIELD_NONE + return c +} + +func rootShouldResetDidLaunch(_ c: RootContext) -> RootContext { + var c = c + + /* 1. Only once during the first setup */ + if + c.recentField == F.didSetup && + c.didLaunch == false + { + c.didLaunch = true + c.recentField = F.didLaunch + return c + } + + + c.recentField = DIALECT_CONTEXT_RECENT_FIELD_NONE + return c +} + +func rootRegisterShoulds(_ ctrl: DialectController) { + [ + rootShouldResetCount, + rootShouldResetCountText, + rootShouldResetDidLaunch, + + ].forEach { f in + ctrl.registerFunction { c in f(c as! RootContext) } + } +} + +func rootRegisterEffects(_ ctrl: DialectController) { + let _: RootContext? = registerOneliners(ctrl, [ + F.countText, { (c: RootContext) in print(c.countText) }, + F.countText, { (c: RootContext) in RootVM.shared!.countText = c.countText }, + + ]) +}