Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/godot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ on:
push:
paths:
- 'platforms/godot/**'
- 'src/**'
- 'include/**'
- 'third_party/**'
branches:
- main
pull_request:
paths:
- 'platforms/godot/**'
- 'src/**'
- 'include/**'
- 'third_party/**'
workflow_dispatch:

workflow_call:
jobs:
# Run test suite first (includes integration tests with example-server)
test:
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Swift Platform

on:
push:
paths:
- 'platforms/swift/**'
- 'src/**'
- 'include/**'
- 'third_party/**'
pull_request:
paths:
- 'platforms/swift/**'
- 'src/**'
- 'include/**'
- 'third_party/**'
workflow_call:

jobs:
build:
name: Build xcframework
runs-on: macos-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Cleanup Zig cache
run: |
rm -rf ~/.cache/zig

- name: Install Zig
uses: mlugg/setup-zig@v2
with:
version: 0.15.2

- name: Show tool versions
run: |
zig version
swift --version
xcodebuild -version

- name: Build xcframework (all slices)
working-directory: platforms/swift
run: bash build.sh release

- name: Verify xcframework structure
run: |
echo "=== xcframework contents ==="
find platforms/swift/build/Colyseus.xcframework -type f | sort
echo ""
echo "=== Checksum ==="
cat platforms/swift/build/Colyseus.xcframework.zip.sha256

- name: Build example (macOS)
working-directory: platforms/swift/example
run: swift build -c release

- name: Run Swift package tests
working-directory: platforms/swift
run: swift test

- name: Upload xcframework artifact
uses: actions/upload-artifact@v4
with:
name: Colyseus.xcframework
path: platforms/swift/build/Colyseus.xcframework.zip
6 changes: 6 additions & 0 deletions platforms/swift/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
build/
.build/
zig-out/
zig-cache/
*.xcframework.zip
*.xcframework.zip.sha256
58 changes: 58 additions & 0 deletions platforms/swift/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// swift-tools-version: 5.9
import PackageDescription

// MARK: - Package
//
// This package distributes the Colyseus Swift SDK in two modes:
//
// 1. SOURCE mode (default during development):
// The Swift wrapper sources are compiled directly. The pre-built
// xcframework is linked as a binary dependency.
// Use: .package(path: "../..")
//
// 2. RELEASE mode (tagged GitHub releases):
// Both the Swift sources and the libcolyseus static library are
// bundled in a single xcframework and distributed as a binary target.
// Consumers add:
// .package(url: "https://github.com/colyseus/colyseus-sdk", from: "0.17.0")
//
// For source-mode development the xcframework must be built first:
// cd platforms/swift && ./build.sh

let package = Package(
name: "Colyseus",
platforms: [
.macOS(.v13),
.iOS(.v15),
.tvOS(.v15),
],
products: [
.library(
name: "Colyseus",
targets: ["Colyseus"]
),
],
targets: [
// Swift wrapper layer
.target(
name: "Colyseus",
dependencies: ["CColyseus"],
path: "Sources/Colyseus",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
]
),

// C library xcframework (pre-built via build.sh)
.binaryTarget(
name: "CColyseus",
path: "build/Colyseus.xcframework"
),

.testTarget(
name: "ColyseusTests",
dependencies: ["Colyseus"],
path: "Tests/ColyseusTests"
),
]
)
24 changes: 24 additions & 0 deletions platforms/swift/Sources/Colyseus/ClientContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

import CColyseus
import Foundation

/// Heap-allocated context for client matchmaking callbacks.
/// Holds the success/error closures and is released after the callback fires.
final class ClientContext {
let onSuccess: (ColyseusRoom) -> Void
let onError: (Int32, String) -> Void

init(onSuccess: @escaping (ColyseusRoom) -> Void,
onError: @escaping (Int32, String) -> Void) {
self.onSuccess = onSuccess
self.onError = onError
}

func retain() -> UnsafeMutableRawPointer {
Unmanaged.passRetained(self).toOpaque()
}

static func consume(_ ptr: UnsafeMutableRawPointer) -> ClientContext {
Unmanaged<ClientContext>.fromOpaque(ptr).takeRetainedValue()
}
}
168 changes: 168 additions & 0 deletions platforms/swift/Sources/Colyseus/ColyseusClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@

import CColyseus
import Foundation

/// Top-level Colyseus client. Manages matchmaking and room lifecycle.
public final class ColyseusClient {

private let raw: UnsafeMutablePointer<colyseus_client_t>
private let settings: ColyseusSettings // retain settings

// MARK: - Lifecycle

public init(settings: ColyseusSettings) {
self.settings = settings
guard let ptr = colyseus_client_create(settings.raw) else {
fatalError("colyseus_client_create returned nil")
}
raw = ptr
}

deinit {
colyseus_client_free(raw)
}

// MARK: - Matchmaking

public func joinOrCreate(
_ roomName: String,
options: String = "{}",
onSuccess: @escaping (ColyseusRoom) -> Void,
onError: @escaping (Int32, String) -> Void
) {
let ctx = ClientContext(onSuccess: onSuccess, onError: onError)
colyseus_client_join_or_create(
raw, roomName, options,
Self.roomSuccessCallback, Self.roomErrorCallback,
ctx.retain()
)
}

public func create(
_ roomName: String,
options: String = "{}",
onSuccess: @escaping (ColyseusRoom) -> Void,
onError: @escaping (Int32, String) -> Void
) {
let ctx = ClientContext(onSuccess: onSuccess, onError: onError)
colyseus_client_create_room(
raw, roomName, options,
Self.roomSuccessCallback, Self.roomErrorCallback,
ctx.retain()
)
}

public func join(
_ roomName: String,
options: String = "{}",
onSuccess: @escaping (ColyseusRoom) -> Void,
onError: @escaping (Int32, String) -> Void
) {
let ctx = ClientContext(onSuccess: onSuccess, onError: onError)
colyseus_client_join(
raw, roomName, options,
Self.roomSuccessCallback, Self.roomErrorCallback,
ctx.retain()
)
}

public func joinById(
_ roomId: String,
options: String = "{}",
onSuccess: @escaping (ColyseusRoom) -> Void,
onError: @escaping (Int32, String) -> Void
) {
let ctx = ClientContext(onSuccess: onSuccess, onError: onError)
colyseus_client_join_by_id(
raw, roomId, options,
Self.roomSuccessCallback, Self.roomErrorCallback,
ctx.retain()
)
}

public func reconnect(
token: String,
onSuccess: @escaping (ColyseusRoom) -> Void,
onError: @escaping (Int32, String) -> Void
) {
let ctx = ClientContext(onSuccess: onSuccess, onError: onError)
colyseus_client_reconnect(
raw, token,
Self.roomSuccessCallback, Self.roomErrorCallback,
ctx.retain()
)
}

// MARK: - Async/await wrappers

public func joinOrCreate(_ roomName: String, options: String = "{}") async throws -> ColyseusRoom {
try await withCheckedThrowingContinuation { cont in
joinOrCreate(roomName, options: options,
onSuccess: { cont.resume(returning: $0) },
onError: { code, msg in cont.resume(throwing: ColyseusError(code: code, message: msg)) }
)
}
}

public func create(_ roomName: String, options: String = "{}") async throws -> ColyseusRoom {
try await withCheckedThrowingContinuation { cont in
create(roomName, options: options,
onSuccess: { cont.resume(returning: $0) },
onError: { code, msg in cont.resume(throwing: ColyseusError(code: code, message: msg)) }
)
}
}

public func join(_ roomName: String, options: String = "{}") async throws -> ColyseusRoom {
try await withCheckedThrowingContinuation { cont in
join(roomName, options: options,
onSuccess: { cont.resume(returning: $0) },
onError: { code, msg in cont.resume(throwing: ColyseusError(code: code, message: msg)) }
)
}
}

public func joinById(_ roomId: String, options: String = "{}") async throws -> ColyseusRoom {
try await withCheckedThrowingContinuation { cont in
joinById(roomId, options: options,
onSuccess: { cont.resume(returning: $0) },
onError: { code, msg in cont.resume(throwing: ColyseusError(code: code, message: msg)) }
)
}
}

public func reconnect(token: String) async throws -> ColyseusRoom {
try await withCheckedThrowingContinuation { cont in
reconnect(token: token,
onSuccess: { cont.resume(returning: $0) },
onError: { code, msg in cont.resume(throwing: ColyseusError(code: code, message: msg)) }
)
}
}

// MARK: - C trampolines (must be @convention(c))

private static let roomSuccessCallback: colyseus_client_room_callback_t = { rawRoom, userdata in
guard let rawRoom = rawRoom, let userdata = userdata else { return }
let ctx = ClientContext.consume(userdata)
let room = ColyseusRoom(raw: rawRoom, owns: true)
room.installCallbacks()
DispatchQueue.main.async { ctx.onSuccess(room) }
}

private static let roomErrorCallback: colyseus_client_error_callback_t = { code, message, userdata in
guard let userdata = userdata else { return }
let ctx = ClientContext.consume(userdata)
let msg = message.map { String(cString: $0) } ?? ""
DispatchQueue.main.async { ctx.onError(code, msg) }
}
}

// MARK: - ColyseusError

public struct ColyseusError: Error, LocalizedError {
public let code: Int32
public let message: String

public var errorDescription: String? { "Colyseus error \(code): \(message)" }
}
Loading
Loading