Create Hello world for macOS (#1)
This commit is contained in:
1 parent
7597b3abfa
commit
184454cf59
32 files changed
+767
No files matched your search
+10
@@ -0,0 +1,10 @@
|
|||||||
|
build
|
||||||
|
.build
|
||||||
|
.DS_Store
|
||||||
|
.gradle
|
||||||
|
gradle-daemon-jvm.properties
|
||||||
|
.idea
|
||||||
|
.kotlin
|
||||||
|
local.properties
|
||||||
|
Package.resolved
|
||||||
|
*.xcodeproj
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# OGS Mahjong (`mj`)
|
||||||
|
|
||||||
|
A macOS SwiftUI app for **OGS Mahjong**, built on the **Dialect v4** declarative
|
||||||
|
reactive engine (see the sibling `../dialect` repo — this project is a
|
||||||
|
downstream consumer of its code generator).
|
||||||
|
|
||||||
|
Business logic lives in `dialect.yml` (YAML). A generator turns it into
|
||||||
|
`game/ver-mac/src/dialect.swift` (the runtime engine + `struct F` field
|
||||||
|
constants + component/should/effect registration). The Swift app shell
|
||||||
|
(`GameApp`/`AppDelegate`) and the SwiftUI views live outside the generated file
|
||||||
|
and talk to the engine through generated functions like `rootSet(...)`.
|
||||||
|
|
||||||
|
Remote: `https://git.opengamestudio.org/mahjong/mahjong` · Version: `2.0.0`
|
||||||
|
|
||||||
|
> Current `dialect.yml` is still the "Root / counter" template inherited from the
|
||||||
|
> dialect example — the Mahjong game logic has not been written yet. The app
|
||||||
|
> shell, packaging and symlink wiring are in place.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
dialect.yml Single source of truth for the reactive logic.
|
||||||
|
Generates game/ver-mac/src/dialect.swift
|
||||||
|
|
||||||
|
components/ SHARED source-of-truth for all components.
|
||||||
|
other/other.swift Shared helpers (otherPrintStderr, otherWriteFile,
|
||||||
|
otherSetupConsoleLogging, ...)
|
||||||
|
root/root.swift RootComponent (owns DialectController, registers
|
||||||
|
effects + shoulds, sets didSetup on setup())
|
||||||
|
root/rootVM.swift RootVM (ObservableObject / @Published countText)
|
||||||
|
root/mac/view/RootView.swift Per-platform SwiftUI view
|
||||||
|
|
||||||
|
game/ver-mac/ macOS app (SPM executable target "ogs-mahjong")
|
||||||
|
Package.swift SPM manifest (macOS 13+, swift-tools 5.9)
|
||||||
|
Info.plist Bundle id org.opengamestudio.Mahjong, exec ogs-mahjong
|
||||||
|
Resources/AppIcon.icns App icon
|
||||||
|
src/
|
||||||
|
GameApp.swift @main SwiftUI App entry
|
||||||
|
AppDelegate.swift Creates RootComponent + RootVM, calls root.setup()
|
||||||
|
dialect.swift GENERATED — do not hand-edit (see chicken-egg below)
|
||||||
|
other/other.swift -> symlink components/other/other.swift
|
||||||
|
root/root.swift -> symlink components/root/root.swift
|
||||||
|
root/rootVM.swift -> symlink components/root/rootVM.swift
|
||||||
|
root/view/RootView.swift-> symlink components/root/mac/view/RootView.swift
|
||||||
|
|
||||||
|
ref/swiftraylib Symlink to ../../swiftraylib — the raylib/SwiftUI
|
||||||
|
renderer sample the game will use for 3D rendering.
|
||||||
|
|
||||||
|
util/ Build/run scripts (see Build & Run)
|
||||||
|
paths APP_MAC = .build/OGS Mahjong.app
|
||||||
|
build-mac generateDialect -> buildMac -> packageMac
|
||||||
|
run-mac runMac
|
||||||
|
rm-mac-geom Resets saved window geometry
|
||||||
|
step/ Sourced steps: generateDialect, buildMac,
|
||||||
|
packageMac, runMac
|
||||||
|
|
||||||
|
history/gitjs/ GitJS "Glavred" history + the original JS source of the
|
||||||
|
web Mahjong, kept for reference — not part of the build.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Symlink parity rule
|
||||||
|
|
||||||
|
`components/` is the source of truth; `game/ver-mac/src/` contains real
|
||||||
|
directories whose files are **relative symlinks** back into `components/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/other/other.swift -> ../../../../components/other/other.swift (4 up)
|
||||||
|
src/root/root.swift -> ../../../../components/root/root.swift (4 up)
|
||||||
|
src/root/rootVM.swift -> ../../../../components/root/rootVM.swift (4 up)
|
||||||
|
src/root/view/RootView.swift-> ../../../../../components/root/mac/view/RootView.swift (5 up)
|
||||||
|
```
|
||||||
|
|
||||||
|
SPM silently **skips** a dangling symlink (no error), so after moving
|
||||||
|
`components/` or a subdir, audit for broken links:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find . -type l ! -exec test -e {} \; -not -path './.build/*'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisite: the generator
|
||||||
|
|
||||||
|
`util/step/generateDialect` calls the **sibling** dialect repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$SDIR/../../dialect/util/run-generator --file=$SDIR/../dialect.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
So `/Volumes/MOE/p/dialect` must exist and its generator must be built before
|
||||||
|
`util/build-mac` can regenerate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(cd ../dialect && util/build-generator) # once, or after dialect core changes
|
||||||
|
```
|
||||||
|
|
||||||
|
The generator resolves the `output:` path **relative to `dialect.yml`'s
|
||||||
|
directory**, which is why `dialect.yml` says `game/ver-mac/src/dialect.swift`.
|
||||||
|
|
||||||
|
## Build & Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
util/build-mac # generateDialect + swift build + package OGS Mahjong.app
|
||||||
|
util/run-mac # runs .build/OGS Mahjong.app/Contents/MacOS/ogs-mahjong
|
||||||
|
util/rm-mac-geom # reset the app's saved window geometry
|
||||||
|
```
|
||||||
|
|
||||||
|
`build-mac` sources `util/paths` then the `util/step/*` scripts in order:
|
||||||
|
|
||||||
|
1. `generateDialect` — runs the sibling generator, writes `src/dialect.swift`
|
||||||
|
2. `buildMac` — `cd game/ver-mac && swift build`
|
||||||
|
3. `packageMac` — assembles `.build/OGS Mahjong.app` (binary `ogs-mahjong`,
|
||||||
|
`Info.plist`, `Resources/AppIcon.icns`)
|
||||||
|
|
||||||
|
## Editing the dialect
|
||||||
|
|
||||||
|
- Add/modify contexts and components in `dialect.yml`, then run
|
||||||
|
`util/build-mac` to regenerate `dialect.swift`.
|
||||||
|
- **Chicken-egg**: `struct F` (field-name constants) lives only in the
|
||||||
|
generated `dialect.swift`. Adding a new field to a context in `dialect.yml`
|
||||||
|
requires hand-patching its `static let <field> = "<field>"` into `struct F`
|
||||||
|
in `dialect.swift` (ASCII-alphabetical order) if you cannot regenerate first,
|
||||||
|
otherwise the build fails on `F.<field>` being unresolved.
|
||||||
|
- `dialect.swift` is generated — edits there are overwritten on the next
|
||||||
|
`util/build-mac`. Change the source (`dialect.yml` / `components/`) instead.
|
||||||
|
- **Validate `dialect.yml` after edits** (e.g. `ruby -ryaml -e 'YAML.load_file("dialect.yml")'`).
|
||||||
|
Every should-branch label (`1. Upon each ...`) MUST end with a colon — the
|
||||||
|
parser (`components/yml/ymlFun.swift`) blindly `dropLast()`s it, so a missing
|
||||||
|
colon silently eats the label's last character in the generated `about`
|
||||||
|
comment. Note the file can never pass a *strict* YAML parser: condition and
|
||||||
|
reaction lines (under `if:` / `then:`) contain unquoted `: ` (e.g.
|
||||||
|
`c.countText = "Count: '\(c.count)'"`), which standard YAML rejects. So treat
|
||||||
|
a parser complaint pointing *inside* an `if:`/`then:` block as expected; any
|
||||||
|
complaint elsewhere (like a missing branch-label colon) is a real bug.
|
||||||
|
|
||||||
|
## Swift 6
|
||||||
|
|
||||||
|
The project currently builds in Swift 5 language mode
|
||||||
|
(`util/step/buildMac` has the Swift 6 flag commented out). Enabling
|
||||||
|
`-Xswiftc -swift-version -Xswiftc 6` fails today on the singleton/`@MainActor`
|
||||||
|
concurrency errors (`RootComponent.singleton`, `RootVM.shared` in
|
||||||
|
`components/root/`, and the `RootVM.shared!.countText = ...` oneliner in the
|
||||||
|
generated `dialect.swift`). Do not flip it on without addressing those.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- `game/ver-mac/.build/`, `build/`, `.DS_Store`, Xcode/Gradle artifacts are
|
||||||
|
git-ignored.
|
||||||
|
- Prefer editing files in `components/` (the real files); the `src/` entries are
|
||||||
|
symlinks.
|
||||||
|
- The generated UI callback plumbing is nonisolated; UI singletons are weak
|
||||||
|
(`RootComponent.singleton`, `RootVM.shared`) and are accessed from the
|
||||||
|
generated oneliners with force-unwrap (`RootVM.shared!.countText = ...`).
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Copyright (c) 2026 OpenGameStudio
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
===
|
||||||
|
Third parties
|
||||||
|
===
|
||||||
|
|
||||||
|
raylib - Copyright (c) 2013-2026 Ramon Santamaria (@raysan5)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Decode a base64 string back into a regular string
|
||||||
|
func otherBase64ToString(_ encoded: String) -> String {
|
||||||
|
guard
|
||||||
|
let data = Data(base64Encoded: encoded),
|
||||||
|
let str = String(data: data, encoding: .utf8)
|
||||||
|
else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Capitalize the first letter of a string
|
||||||
|
func otherCapitalize(_ s: String) -> String {
|
||||||
|
guard let first = s.first else {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return String(first).uppercased() + String(s.dropFirst())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Launch to wait for asynchrnous actions
|
||||||
|
func otherLaunch() {
|
||||||
|
RunLoop.current.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count leading spaces in a line
|
||||||
|
func otherLineIndent(_ line: String) -> Int {
|
||||||
|
var count = 0
|
||||||
|
for ch in line {
|
||||||
|
if ch == " " {
|
||||||
|
count += 1
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print to stderr
|
||||||
|
func otherPrintStderr(_ txt: String) {
|
||||||
|
if let dat = "\(txt)\n".data(using: .utf8) {
|
||||||
|
FileHandle.standardError.write(dat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print each key/value processed by a controller into stderr
|
||||||
|
func otherSetupConsoleLogging(
|
||||||
|
_ ctrl: DialectController,
|
||||||
|
_ prefix: String
|
||||||
|
) {
|
||||||
|
ctrl.registerCallback { c -> Void in
|
||||||
|
let value = c.fieldAny(c.recentField)
|
||||||
|
let line = "ИГР \(prefix) k/v: '\(c.recentField)'/'\(value)'"
|
||||||
|
otherPrintStderr(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write output text to a file
|
||||||
|
func otherWriteFile(
|
||||||
|
_ path: String,
|
||||||
|
_ content: String
|
||||||
|
) {
|
||||||
|
try? content.write(toFile: path, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terminate the main RunLoop
|
||||||
|
func otherShutdown() {
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct RootView: View {
|
||||||
|
@ObservedObject var vm: RootVM
|
||||||
|
|
||||||
|
init(_ vm: RootVM) {
|
||||||
|
self.vm = vm
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Text("Hello, World!")
|
||||||
|
.font(.largeTitle)
|
||||||
|
|
||||||
|
Text(vm.countText)
|
||||||
|
.font(.title3.monospacedDigit())
|
||||||
|
|
||||||
|
Button("Increment") {
|
||||||
|
rootSet(F.didClickIncrement, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(40)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
class RootComponent {
|
||||||
|
let ctrl = DialectController(RootContext())
|
||||||
|
static private(set) weak var singleton: RootComponent?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Self.singleton = self
|
||||||
|
rootRegisterEffects(ctrl)
|
||||||
|
rootRegisterShoulds(ctrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
rootSet(F.didSetup, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import Combine
|
||||||
|
|
||||||
|
final class RootVM: ObservableObject {
|
||||||
|
@Published var countText = ""
|
||||||
|
|
||||||
|
static private(set) weak var shared: RootVM?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Self.shared = self
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
version: 4
|
||||||
|
|
||||||
|
output:
|
||||||
|
game/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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>OGS Mahjong</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>OGS Mahjong</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.opengamestudio.Mahjong</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>ogs-mahjong</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>2.0.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>11.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
Binary file not shown.
@@ -0,0 +1,22 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
otherSetupConsoleLogging(root.ctrl, "Root")
|
||||||
|
root.setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<T>(_ 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<T>(
|
||||||
|
_ ctrl: DialectController,
|
||||||
|
_ items: [Any]
|
||||||
|
) -> T? {
|
||||||
|
let halfCount = items.count / 2
|
||||||
|
for i in 0..<halfCount {
|
||||||
|
let field = items[i * 2] as! String
|
||||||
|
let callback = items[i * 2 + 1] as! (T) -> 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
|
||||||
|
public struct F {
|
||||||
|
public static let count = "count"
|
||||||
|
public static let countText = "countText"
|
||||||
|
public static let didClickIncrement = "didClickIncrement"
|
||||||
|
public static let didLaunch = "didLaunch"
|
||||||
|
public static let didSetup = "didSetup"
|
||||||
|
|
||||||
|
}
|
||||||
|
public struct RootContext: DialectContext {
|
||||||
|
public var count = Int()
|
||||||
|
public var countText = String()
|
||||||
|
public var didClickIncrement = Bool()
|
||||||
|
public var didLaunch = Bool()
|
||||||
|
public var didSetup = Bool()
|
||||||
|
|
||||||
|
public var recentField = ""
|
||||||
|
|
||||||
|
public func field<T>(_ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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 change */
|
||||||
|
if
|
||||||
|
c.recentField == F.count
|
||||||
|
{
|
||||||
|
c.countText = "Count: '\(c.count)'"
|
||||||
|
c.recentField = F.countText
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Upon launch */
|
||||||
|
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 },
|
||||||
|
|
||||||
|
])
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../../components/other/other.swift
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../../components/root/root.swift
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../../components/root/rootVM.swift
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../components/root/mac/view/RootView.swift
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../swiftraylib
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
SDIR=$(cd "$(dirname "$0")" ; pwd -P)
|
||||||
|
|
||||||
|
T=$SDIR/step
|
||||||
|
source $SDIR/paths
|
||||||
|
|
||||||
|
STEP=0
|
||||||
|
source $T/generateDialect
|
||||||
|
source $T/buildMac
|
||||||
|
source $T/packageMac
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
APP_MAC="$SDIR/../game/ver-mac/.build/OGS Mahjong.app"
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
killall cfprefsd 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
rm -f $HOME/Library/Preferences/OGS\ Mahjong.plist \
|
||||||
|
$HOME/Library/Preferences/org.opengamestudio.Mahjong.plist
|
||||||
|
|
||||||
|
echo "removed app preferences (window geometry)"
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
SDIR=$(cd "$(dirname "$0")" ; pwd -P)
|
||||||
|
|
||||||
|
T=$SDIR/step
|
||||||
|
source $SDIR/paths
|
||||||
|
|
||||||
|
STEP=0
|
||||||
|
source $T/runMac
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
STEP=$((STEP+1))
|
||||||
|
echo -e "\n> > > > Step №$STEP. Build for macOS"
|
||||||
|
|
||||||
|
cd $SDIR/../game/ver-mac
|
||||||
|
swift build # -Xswiftc -swift-version -Xswiftc 6
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
STEP=$((STEP+1))
|
||||||
|
echo -e "\n> > > > Step №$STEP. Generate Dialect"
|
||||||
|
|
||||||
|
$SDIR/../../dialect/util/run-generator --file=$SDIR/../dialect.yml
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
STEP=$((STEP+1))
|
||||||
|
echo -e "\n> > > > Step №$STEP. Make an app package for macOS"
|
||||||
|
|
||||||
|
rm -rf "$APP_MAC"
|
||||||
|
|
||||||
|
APP_DAT="$APP_MAC/Contents"
|
||||||
|
|
||||||
|
mkdir -p "$APP_DAT/MacOS"
|
||||||
|
mkdir -p "$APP_DAT/Resources"
|
||||||
|
|
||||||
|
cp "$SDIR/../game/ver-mac/.build/debug/ogs-mahjong" "$APP_DAT/MacOS/ogs-mahjong"
|
||||||
|
cp "$SDIR/../game/ver-mac/Info.plist" "$APP_DAT/Info.plist"
|
||||||
|
cp "$SDIR/../game/ver-mac/Resources/AppIcon.icns" "$APP_DAT/Resources/AppIcon.icns"
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
STEP=$((STEP+1))
|
||||||
|
echo -e "\n> > > > Step №$STEP. Run macOS app"
|
||||||
|
|
||||||
|
"$APP_MAC/Contents/MacOS/ogs-mahjong"
|
||||||
Reference in new issue
Block a user