From eecb6012e51be6afd2c0acce5e16f09fe9352c81 Mon Sep 17 00:00:00 2001 From: John Fairhurst Date: Thu, 2 May 2024 11:09:09 +0100 Subject: [PATCH] Swift 5.10 updates --- Package.resolved | 15 +++++----- Package.swift | 13 +++++---- README.md | 2 +- Sources/SpaceWar/App.swift | 16 ++++++---- Sources/SpaceWar/BaseMenu.swift | 2 ++ Sources/SpaceWar/Controller.swift | 2 +- Sources/SpaceWar/FakeNet.swift | 4 +++ Sources/SpaceWar/Inventory.swift | 1 + Sources/SpaceWar/Lobby.swift | 1 + Sources/SpaceWar/Misc.swift | 1 + Sources/SpaceWar/SpaceWarClient.swift | 7 +++-- .../SpaceWar/SpaceWarClientConnection.swift | 7 +++-- Sources/SpaceWar/SpaceWarClientLayout.swift | 1 + Sources/SpaceWar/SpaceWarEntity.swift | 29 ++----------------- Sources/SpaceWar/SpaceWarMain.swift | 15 ++++++++-- Sources/SpaceWar/SpaceWarServer.swift | 5 +++- .../SpaceWar/SpaceWarServerConnection.swift | 10 +++++-- Sources/SpaceWar/StarField.swift | 1 + Sources/SpaceWar/Sun.swift | 1 + Sources/SpaceWar/TextureCache.swift | 1 + Sources/SpaceWar/VectorEntity.swift | 10 +++---- 21 files changed, 84 insertions(+), 60 deletions(-) diff --git a/Package.resolved b/Package.resolved index 116beaa..f506ee3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "0e835b31f6c99de6fcc976f46a22a3f635ba60b860700b86b6104956fd572882", "pins" : [ { "identity" : "steamworks-swift", "kind" : "remoteSourceControl", "location" : "https://github.com/johnfairh/steamworks-swift", "state" : { - "revision" : "56ef22fc7c1cd98fc2e04acf4d45bcb4bc9db389", - "version" : "0.5.1" + "revision" : "a55227dc4f155977147d7cb66c0d164c6139a870", + "version" : "0.5.2" } }, { @@ -14,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version" : "1.5.3" + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" } }, { @@ -23,10 +24,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/johnfairh/TMLEngines", "state" : { - "revision" : "6ba327419697ce9e150f6c782a59fa7b4ea89124", - "version" : "1.3.1" + "revision" : "3a728fea123425746bea5ec0b07ee6c80eb6c53d", + "version" : "1.3.3" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index e5179ff..aef30eb 100644 --- a/Package.swift +++ b/Package.swift @@ -1,17 +1,17 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 5.10 import PackageDescription let package = Package( name: "spacewar-swift", platforms: [ - .macOS("13.0"), + .macOS("14.0"), ], dependencies: [ .package(url: "https://github.com/johnfairh/steamworks-swift", - from: "0.5.1"), + from: "0.5.2"), .package(url: "https://github.com/johnfairh/TMLEngines", - from: "1.2.0") + from: "1.3.3") ], targets: [ .executableTarget( @@ -28,7 +28,10 @@ let package = Package( .process("Resources/xbox_controller.vdf"), .process("Resources/ps5_controller.vdf") ], - swiftSettings: [.interoperabilityMode(.Cxx)] + swiftSettings: [ + .interoperabilityMode(.Cxx), + .enableExperimentalFeature("StrictConcurrency") + ] ), .systemLibrary(name: "CSpaceWar") ] diff --git a/README.md b/README.md index b62ec89..459105a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Educational port of Steamworks demo to Swift for macOS with Metal backend. -Needs Xcode 15 / Swift 5.9 +Needs Xcode 15.3 / Swift 5.10 Needs Steam up and logged in; best run from CLI `swift run` - see CI for pre-reqs. diff --git a/Sources/SpaceWar/App.swift b/Sources/SpaceWar/App.swift index 41019d7..c38fceb 100644 --- a/Sources/SpaceWar/App.swift +++ b/Sources/SpaceWar/App.swift @@ -14,6 +14,7 @@ import Dispatch /// Don't actually create the client or set up Steam until the engine starts up and gives /// us a context to create objects and hang stuff on. @main +@MainActor struct SpaceWarApp: App { init() { // Some nonsense to simulate a library that probably doesn't exist @@ -53,12 +54,13 @@ struct SpaceWarApp: App { } /// Might need to reach around here from random places, not sure + @MainActor static private(set) var instance: SpaceWarMain? /// Steam API initialization dance private func initSteam() -> (SteamAPI, Controller) { // Set our log handler before SteamAPI creates a logger - SWLogHandler.setup() + SWLogHandler.setup(level: .trace) LoggingSystem.bootstrap(SWLogHandler.init) guard let steam = SteamAPI(appID: .spaceWar, fakeAppIdTxtFile: true) else { @@ -69,7 +71,6 @@ struct SpaceWarApp: App { // Debug handlers steam.useLoggerForSteamworksWarnings() steam.networkingUtils.useLoggerForDebug(detailLevel: .everything) - SteamAPI.logger.logLevel = .debug // Ensure that the user has logged into Steam. This will always return true if the game is launched // from Steam, but if Steam is at the login prompt when you run your game from the debugger, it @@ -122,8 +123,9 @@ struct SpaceWarApp: App { } /// Top-level debug logging +let logger = Logger(label: "SpaceWar") func OutputDebugString(_ msg: String) { - SteamAPI.logger.debug(.init(stringLiteral: msg)) + logger.debug(.init(stringLiteral: msg)) } /// CEG -- don't think this exists on macOS, we don't have it at any rate @@ -137,20 +139,24 @@ import Logging /// A `LogHandler` which logs to stdout and a file -- big hack yikes need to find a proper logger struct SWLogHandler: LogHandler { - static func setup() { + static func setup(level: Logger.Level = .info) { unlink(Self.LOGFILE) + defaultLevel = level } + private nonisolated(unsafe) static var defaultLevel: Logger.Level = .info + static let LOGFILE = "/Users/johnf/project/swift-spacewar/latest-log" /// Create a `SyslogLogHandler`. public init(label: String) { self.label = label + self.logLevel = SWLogHandler.defaultLevel } public let label: String - public var logLevel: Logger.Level = .info + public var logLevel: Logger.Level public func log(level: Logger.Level, message: Logger.Message, diff --git a/Sources/SpaceWar/BaseMenu.swift b/Sources/SpaceWar/BaseMenu.swift index 77515df..bef7ccf 100644 --- a/Sources/SpaceWar/BaseMenu.swift +++ b/Sources/SpaceWar/BaseMenu.swift @@ -6,6 +6,7 @@ import MetalEngine /// Shared things and constants +@MainActor private enum Menu { static var font: Font2D! @@ -18,6 +19,7 @@ private enum Menu { } /// General menu class that can draw itself, scroll, and report selection to a callback +@MainActor class BaseMenu { private let engine: Engine2D private let controller: Controller diff --git a/Sources/SpaceWar/Controller.swift b/Sources/SpaceWar/Controller.swift index c1ed926..91c62d9 100644 --- a/Sources/SpaceWar/Controller.swift +++ b/Sources/SpaceWar/Controller.swift @@ -8,7 +8,7 @@ import struct MetalEngine.Color2D // The platform-independent part of 'engine' to do with wrangling SteamInput -final class Controller { +final class Controller: @unchecked Sendable { let steam: SteamAPI // MARK: Button bindings diff --git a/Sources/SpaceWar/FakeNet.swift b/Sources/SpaceWar/FakeNet.swift index df40271..dd41471 100644 --- a/Sources/SpaceWar/FakeNet.swift +++ b/Sources/SpaceWar/FakeNet.swift @@ -58,7 +58,9 @@ final class FakeMsgEndpoint: CustomStringConvertible { } } +@MainActor class FakeNet { + @MainActor private static var endpoints: [SteamID : FakeMsgEndpoint] = [:] static var enableReporting = 0 @@ -171,6 +173,7 @@ extension SteamNetworkingMessage : SteamMsgProtocol { /// Versions of send/receive that are `FAKE_NET` aware extension SteamNetworkingSockets { /// `FAKE_NET_USE`-aware send-msg function + @MainActor func sendMessageToConnection(conn: HSteamNetConnection?, from: SteamID, to: SteamID, data: UnsafeRawPointer, dataSize: Int, sendFlags: SteamNetworkingSendFlags) -> (rc: Result, messageNumber: Int) { if FAKE_NET_USE { FakeNet.send(from: from, to: to, data: data, size: dataSize) @@ -184,6 +187,7 @@ extension SteamNetworkingSockets { } /// `FAKE_NET_USE`-aware recv-msgs function + @MainActor func receiveMessagesOnConnection(conn: HSteamNetConnection?, steamID: SteamID, maxMessages: Int) -> (rc: Int, messages: [SteamMsgProtocol]) { if FAKE_NET_USE { var msgs = [FakeClientMsg]() diff --git a/Sources/SpaceWar/Inventory.swift b/Sources/SpaceWar/Inventory.swift index 83087c0..3428452 100644 --- a/Sources/SpaceWar/Inventory.swift +++ b/Sources/SpaceWar/Inventory.swift @@ -419,6 +419,7 @@ final class SpaceWarLocalInventory { /// This gets accessed from all over the place so we mimic the global getter extension SpaceWarLocalInventory { + @MainActor static var instance: SpaceWarLocalInventory { SpaceWarApp.instance!.inventory } diff --git a/Sources/SpaceWar/Lobby.swift b/Sources/SpaceWar/Lobby.swift index be91aa8..94dbc6b 100644 --- a/Sources/SpaceWar/Lobby.swift +++ b/Sources/SpaceWar/Lobby.swift @@ -14,6 +14,7 @@ import MetalEngine /// 1) Create Lobby -> start server locally and join it /// 2) Browse for existing lobby -> join it -> join server remotely /// +@MainActor class Lobbies { private let steam: SteamAPI private let engine: Engine2D diff --git a/Sources/SpaceWar/Misc.swift b/Sources/SpaceWar/Misc.swift index 5d4bcda..4dd819c 100644 --- a/Sources/SpaceWar/Misc.swift +++ b/Sources/SpaceWar/Misc.swift @@ -86,6 +86,7 @@ struct Debounced { /// Record time of state change /// Provide call to execute code first time made in new state /// Provide setter to nop if already there and execute code if not +@MainActor final class MonitoredState { let tickSource: TickSource let name: String diff --git a/Sources/SpaceWar/SpaceWarClient.swift b/Sources/SpaceWar/SpaceWarClient.swift index de40c73..4401bc2 100644 --- a/Sources/SpaceWar/SpaceWarClient.swift +++ b/Sources/SpaceWar/SpaceWarClient.swift @@ -11,6 +11,7 @@ import Foundation /// /// SpaceWarMain is in charge of this, initializes it and passes it the baton of /// running a game, with either a local server or connecting to one. +@MainActor final class SpaceWarClient { private let steam: SteamAPI private let engine: Engine2D @@ -102,8 +103,10 @@ final class SpaceWarClient { } deinit { - clientConnection.disconnect(reason: "Client object deletion") - disconnect() + MainActor.assumeIsolated { // sure... + clientConnection.disconnect(reason: "Client object deletion") + disconnect() + } } // MARK: Kick-off entrypoints diff --git a/Sources/SpaceWar/SpaceWarClientConnection.swift b/Sources/SpaceWar/SpaceWarClientConnection.swift index 8b90070..b37968b 100644 --- a/Sources/SpaceWar/SpaceWarClientConnection.swift +++ b/Sources/SpaceWar/SpaceWarClientConnection.swift @@ -28,6 +28,7 @@ import MetalEngine /// notice, its watchdog at the top of `runFrame()` will trigger. /// /// Factored out of SpaceWarClient to save my sanity. +@MainActor final class SpaceWarClientConnection { let steam: SteamAPI let tickSource: TickSource @@ -422,7 +423,7 @@ final class SpaceWarClientConnection { // MARK: ServerPing /// Helper to ping/query a server from an IP address -private final class ServerPing: SteamMatchmakingPingResponse { +private final class ServerPing: SteamMatchmakingPingResponse, @unchecked Sendable { private let steam: SteamAPI private weak var connection: SpaceWarClientConnection? private var serverQuery: HServerQuery? @@ -441,7 +442,9 @@ private final class ServerPing: SteamMatchmakingPingResponse { func serverResponded(server: GameServerItem) { if let connection { OutputDebugString("ClientConnection ping response, connecting to Steam ID") - connection.connect(steamID: server.steamID) + MainActor.assumeIsolated { + connection.connect(steamID: server.steamID) + } } } diff --git a/Sources/SpaceWar/SpaceWarClientLayout.swift b/Sources/SpaceWar/SpaceWarClientLayout.swift index 63ed320..a591322 100644 --- a/Sources/SpaceWar/SpaceWarClientLayout.swift +++ b/Sources/SpaceWar/SpaceWarClientLayout.swift @@ -7,6 +7,7 @@ import Steamworks import MetalEngine /// Routines from spacewarclient to do with drawing graphics on screen +@MainActor final class SpaceWarClientLayout { let steam: SteamAPI let controller: Controller diff --git a/Sources/SpaceWar/SpaceWarEntity.swift b/Sources/SpaceWar/SpaceWarEntity.swift index f38ac65..67d9b09 100644 --- a/Sources/SpaceWar/SpaceWarEntity.swift +++ b/Sources/SpaceWar/SpaceWarEntity.swift @@ -8,6 +8,7 @@ import simd /// A `SpaceWarEntity` is just like a `VectorEntity`, except it knows how /// to apply gravity from the SpaceWar Sun +@MainActor class SpaceWarEntity: VectorEntity { private let affectedByGravity: Bool @@ -16,22 +17,18 @@ class SpaceWarEntity: VectorEntity { super.init(engine: engine, collisionRadius: collisionRadius, maximumVelocity: maximumVelocity) } - static let MIN_GRAVITY = Float(15) // pixels per second per second + static nonisolated let MIN_GRAVITY = Float(15) // pixels per second per second override func runFrame() { if affectedByGravity { // The suns gravity, compute that here, sun is always at the center of the screen [JF: !!!] let posSun = engine.viewportSize / 2 - #if true // XXX CxxInterop - let distancePower = max(my_distance_squared(posSun, pos), 1) - #else let distancePower = max(simd_distance_squared(posSun, pos), 1) // gravity power falls off exponentially; guard div0 - #endif let factor = min(100000.0 / distancePower, SpaceWarEntity.MIN_GRAVITY) // arbitrary value for power of gravity - let direction = my_normalize(pos - posSun) // XXX CxxInterop simd_normalize(pos - posSun) + let direction = simd_normalize(pos - posSun) // Set updated acceleration acceleration -= factor * direction @@ -40,23 +37,3 @@ class SpaceWarEntity: VectorEntity { super.runFrame() } } - -// Swift C++ interop makes simd_vector_add() not link, which loads depends on ... baffling -func my_distance_squared(_ a: SIMD2, _ b: SIMD2) -> Float { - let xs = pow(a.x - b.x, 2) - let ys = pow(a.y - b.y, 2) - return xs + ys -} - -func my_distance(_ a: SIMD2, _ b: SIMD2) -> Float { - sqrt(my_distance_squared(a, b)) -} - -func my_length(_ v: SIMD2) -> Float { - sqrt(pow(v.x, 2) + pow(v.y, 2)) -} - -private func my_normalize(_ v: SIMD2) -> SIMD2 { - let len = my_length(v) - return [v.x / len, v.y / len] -} diff --git a/Sources/SpaceWar/SpaceWarMain.swift b/Sources/SpaceWar/SpaceWarMain.swift index 56b8a90..7e3d836 100644 --- a/Sources/SpaceWar/SpaceWarMain.swift +++ b/Sources/SpaceWar/SpaceWarMain.swift @@ -18,6 +18,7 @@ import Foundation /// Though I have modularized slightly rather than having one massive state enum. /// /// No PS3 accommodations. +@MainActor final class SpaceWarMain { private let engine: Engine2D private let steam: SteamAPI @@ -47,6 +48,7 @@ final class SpaceWarMain { private(set) var gameState: MonitoredState private var cancelInput: Debounced private var infrequent: Debounced + private var networkRcvTask: Task? init(engine: Engine2D, steam: SteamAPI, controller: Controller) { self.engine = engine @@ -105,14 +107,23 @@ final class SpaceWarMain { steam.networkingUtils.initRelayNetworkAccess() } - Timer.scheduledTimer(withTimeInterval: 0.005, repeats: true) { [weak self] _ in - self?.receiveNetworkData() + networkRcvTask = Task { [weak self] in + MainActor.assertIsolated() // does isolation inheritance work? + while !Task.isCancelled { + try? await Task.sleep(for: .milliseconds(5)) + self?.receiveNetworkData() + } + OutputDebugString("NetworkRcvTask exitting") } initSteamNotifications() initCommandLine() } + deinit { + networkRcvTask?.cancel() + } + // MARK: General Steam Infrastructure Interlocks /// Connect to general Steam notifications, roughly all lifecycle-related diff --git a/Sources/SpaceWar/SpaceWarServer.swift b/Sources/SpaceWar/SpaceWarServer.swift index d48d6b0..70207a1 100644 --- a/Sources/SpaceWar/SpaceWarServer.swift +++ b/Sources/SpaceWar/SpaceWarServer.swift @@ -9,6 +9,7 @@ import simd typealias PlayerIndex = Int /* 0...3 */ +@MainActor final class SpaceWarServer { let engine: Engine2D let controller: Controller @@ -150,7 +151,9 @@ final class SpaceWarServer { deinit { OutputDebugString("SpaceWarServer deinit") // Tell clients we are exiting - serverConnection.shutdownAllClients() + MainActor.assumeIsolated { // jeepers creepers... + serverConnection.shutdownAllClients() + } // Disconnect from the steam servers steam.gameServer.logOff() } diff --git a/Sources/SpaceWar/SpaceWarServerConnection.swift b/Sources/SpaceWar/SpaceWarServerConnection.swift index bbb7b07..e2777e8 100644 --- a/Sources/SpaceWar/SpaceWarServerConnection.swift +++ b/Sources/SpaceWar/SpaceWarServerConnection.swift @@ -10,6 +10,7 @@ typealias ClientToken = FakeNetToken /// Component of SpaceWarServer to manage a set of connected clients, abstracting /// network access and `FAKE_NET` stuff. +@MainActor final class SpaceWarServerConnection { let steam: SteamGameServerAPI let tickSource: TickSource @@ -93,9 +94,12 @@ final class SpaceWarServerConnection { if let pollGroup { steam.networkingSockets.destroyPollGroup(pollGroup: pollGroup) } - if FAKE_NET_USE && steamID.isValid { - FakeNet.stopListening(at: steamID) - FakeNet.freeEndpoint(for: steamID) + let sID = steamID + if FAKE_NET_USE && sID.isValid { + MainActor.assumeIsolated { + FakeNet.stopListening(at: steamID) + FakeNet.freeEndpoint(for: steamID) + } } } diff --git a/Sources/SpaceWar/StarField.swift b/Sources/SpaceWar/StarField.swift index 6b9fc29..a8af8e3 100644 --- a/Sources/SpaceWar/StarField.swift +++ b/Sources/SpaceWar/StarField.swift @@ -6,6 +6,7 @@ import MetalEngine +@MainActor final class StarField { private static let STAR_COUNT = 600 diff --git a/Sources/SpaceWar/Sun.swift b/Sources/SpaceWar/Sun.swift index 220bbac..2d2a87c 100644 --- a/Sources/SpaceWar/Sun.swift +++ b/Sources/SpaceWar/Sun.swift @@ -5,6 +5,7 @@ import MetalEngine +@MainActor final class Sun: SpaceWarEntity { static let VECTOR_SCALE_FACTOR: Float = 14 diff --git a/Sources/SpaceWar/TextureCache.swift b/Sources/SpaceWar/TextureCache.swift index d7a8324..463d01b 100644 --- a/Sources/SpaceWar/TextureCache.swift +++ b/Sources/SpaceWar/TextureCache.swift @@ -11,6 +11,7 @@ import MetalEngine /// Get a specific Steam image RGBA as a game texture /// /// `SpaceWarClient::GetSteamImageAsTexture()` +@MainActor struct TextureCache { private let steam: SteamAPI private let engine: Engine2D diff --git a/Sources/SpaceWar/VectorEntity.swift b/Sources/SpaceWar/VectorEntity.swift index 023436c..bb6760e 100644 --- a/Sources/SpaceWar/VectorEntity.swift +++ b/Sources/SpaceWar/VectorEntity.swift @@ -10,6 +10,7 @@ typealias Radians = Float /// An entity with fixed geometry that has position and acceleration in space, updating itself by frame /// Subclasses expected to tweak parameters +@MainActor class VectorEntity { let engine: Engine2D let collisionRadius: Float @@ -31,7 +32,7 @@ class VectorEntity { /// The distance travelled since the last frame var distanceTraveledLastFrame: Float { - my_distance(pos, posLastFrame) // XXX CxxInterop simd_distance(pos, posLastFrame) + simd_distance(pos, posLastFrame) } /// Current velocity - normally computed from acceleration @@ -49,7 +50,7 @@ class VectorEntity { private(set) var rotationDeltaLastFrame: Radians /// Max velocity in pixels per second - static let DEFAULT_MAXIMUM_VELOCITY: Float = 450 + static nonisolated let DEFAULT_MAXIMUM_VELOCITY: Float = 450 init(engine: Engine2D, collisionRadius: Float, maximumVelocity: Float = VectorEntity.DEFAULT_MAXIMUM_VELOCITY) { self.engine = engine @@ -100,7 +101,7 @@ class VectorEntity { // Make sure velocity does not exceed maximum allowed - this scales it while // keeping the aspect ratio consistent - let linearVelocity = my_length(velocity) // XXX CxxInterop simd_length(velocity) + let linearVelocity = simd_length(velocity) if linearVelocity > maximumVelocity { let ratio = maximumVelocity / linearVelocity velocity *= ratio @@ -143,8 +144,7 @@ class VectorEntity { return false } -// return simd_distance(pos, target.pos) < collisionRadius + target.collisionRadius XXX CxxInterop - return my_distance(pos, target.pos) < collisionRadius + target.collisionRadius + return simd_distance(pos, target.pos) < collisionRadius + target.collisionRadius } }