macOS Steamworks 1.61 Test MIT

steamworks-swift

一个使用 Swift C++ 导入器的 Steamworks SDK 实用接口。

集成者注意:Swift C++ 导入器是新的且在不断发展;这个包构建于其之上

截至 Swift 6,C++ 在 macOS 上似乎终于稳定了。在 Linux 上很难推荐:仍然倾向于以奇怪的、不可移植的方式崩溃。我还没有尝试 Windows。

当前状态

下面

概念

下一步

API 映射设计

生命周期

// Initialization
let steam = SteamAPI(appID: MyAppId) // or `SteamGameServerAPI`

// Frame loop
steam.runCallbacks() // or `steam.releaseCurrentThreadMemory()`

// Shutdown
// ...when `steam` goes out of scope

回调

C++

STEAM_CALLBACK(MyClass, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived);

...

m_CallbackUserStatsReceived( this, &MyClass::OnUserStatsReceived )

...

void MyClass::OnUserStatsReceived( UserStatsReceived_t *pCallback ) {
  ...
}

Swift

steam.onUserStatsReceived { userStatsReceived in
  ...
}

也有异步版本,例如

for await userStatsReceived in steam.userStatsReceived {
  ...
}

请务必查看 Swift 并发问题

函数

auto handle = SteamInventory()->StartUpdateProperties();
let handle = steam.inventory.startUpdateProperties()

调用返回样式

C++

CCallResult<MyClass, FriendsGetFollowerCount_t> m_GetFollowerCountCallResult;

...

auto hSteamAPICall = SteamFriends.GetFollowerCount(steamID);
m_GetFollowerCountCallResult.Set(hSteamAPICall, this, &MyClass::OnGetFollowerCount);

...

void MyClass::OnGetFollowerCount(FriendsGetFollowerCount_t *pCallback, bool bIOFailure) {
  ...
}

Swift

steam.friends.getFollowerCount(steamID: steamID) { getFollowerCount in
  guard let getFollowerCount = getFollowerCount else {
    // `bIOFailure` case
    ...
  }
  ...
}

有异步版本

let getFollowerCount = await steam.friends.getFollowerCount(steamID: steamID)

……截至 Swift 6 终于安全了,但请检查 Swift 并发问题

数组长度参数

携带输入数组长度的参数将被丢弃,因为 Swift 数组本身就携带了它们的长度。

'Out' 参数

API 填充的 C++ “out” 参数在元组中返回,或者,如果 Steam API 是 void,则作为唯一的返回值。

SteamInventoryResult_t result;
bool rc = SteamInventory()->GrantPromoItems(&result);
let (rc, result) = steamAPI.inventory.grantPromoItems()

可选 'out' 参数

一些 C++ “out”参数是可选的:可以将它们作为 NULL 传递,以表明调用者不需要它们。 在 Swift API 中,这些参数会生成一个额外的布尔参数 return<ParamName>,默认值为 true

auto avail = SteamNetworkingUtils()->GetRelayNetworkStatusAvailability(NULL);
let (avail, _) = steamAPI.networkingUtils.getRelayNetworkStatusAvailability(returnDetails: false)

返回元组仍然会填充一些内容,但其内容是未定义的; 该库保证将 NULL 传递给底层 Steamworks API。

'In-out' 参数

C++ 参数,其值很重要并且其值也会被更新,同时存在于 Swift 函数参数和返回元组中。

uint32 itemDefIDCount = 0;
bool rc1 = SteamInventory()->GetItemDefinitionIDs(NULL, &itemDefIDCount);
auto itemDefIDs = new SteamItemDef_t [itemDefIDCount];
bool rc2 = SteamInventory()->GetItemDefinitions(itemDefIDs, &itemDefIDCount);
let (rc1, _, itemDefIDCount) = steamAPI.inventory.
                                   getItemDefinitionIDs(returnItemDefIDs: false,
                                                        itemDefIDsArraySize: 0)
let (rc2, itemDefIDs, _) = steamAPI.inventory.
                               getItemDefinitionIDs(itemDefIDsArraySize: itemDefIDCount)

默认参数值

在 API 文档建议一个值的地方提供默认值,但仍然存在 API 需要调用者为输出字符串提供最大缓冲区长度 - 这些在 Swift 中看起来非常奇怪,但无法避免。 一些 Steamworks API 支持旧的“传递 NULL 以获取所需长度”的两遍样式,并且这些模式以 Swifty 的方式封装在 SteamworksHelpers 模块中。

Swift 并发问题

Steamworks 架构是基于线程的。 对于要调用 Steam API 的每个线程,必须定期调用 SteamAPI.runCallbacks()SteamAPI.releaseCurrentThreadMemory()。 前者同步回调到您的代码以完成回调; 它们都执行内部的线程特定内务处理。

Swift 并发及其内置的基于 libdispatch 的执行器完全反对用户考虑线程,只是勉强例外地考虑“主线程”。

为了将 async-await 与 Steamworks 一起使用,我认为有两种方法

  1. 将 Steam 交互保留在主线程上。 使用 @MainActor 和相关工具将您的代码保留在那里(MainActor.assumeIsolated() 可以成为救命稻草)。 如果需要从另一个隔离域调用 Steam,则必须跳过去 - 就像使用 AppKit 和 friends 一样。

    作为帧循环或类似循环的一部分调用 SteamAPI.runCallbacks()

  2. 使用 Swift 自定义执行器来管理线程以运行您的代码并执行所需的 Steam 轮询。 将这些执行器的实例分配给 actor 以托管您的程序,巧妙地选择线程的数量和分布。

测试中的几个 (1) 示例,请参阅 TestApiSimple.testCallReturnAsync()TestApiSimple.testCallbackAsync() 以及它们基于回调的版本。

SteamworksConcurrency 模块中的 (2) 的原型执行器在 SteamExecutor 中,以及 TestExecutor.testExecutorSteam() 中的使用示例。

我认为一个实际的解决方案是将它们混合起来:对一般的事情使用 @MainActor 绑定的代码,使用帧循环来触发频繁的回调,然后使用一个或多个执行器来照顾游戏服务器或较低优先级的工作。

如何使用此项目

先决条件

安装 Steamworks SDK

示例 Package.swift

// swift-tools-version: 6.0

import PackageDescription

let package = Package(
  name: "MySteamApp",
  platforms: [
    .macOS("15.0"),
  ],
  dependencies: [
    .package(url: "https://github.com/johnfairh/steamworks-swift", from: "1.0.0"),
  ],
  targets: [
    .executableTarget(
      name: "MySteamApp",
      dependencies: [
        .product(name: "Steamworks", package: "steamworks-swift")
      ],
      swiftSettings: [.interoperabilityMode(.Cxx)]
    )
  ]
)

请注意,您必须在依赖 Steamworks 的所有目标,以及依赖于它们的所有目标中设置 .interoperabilityMode(.Cxx),直到最后一个依赖项。 这种传染性是当前 Swift 设计的一部分,目前无法避免。

示例骨架程序

import Steamworks

@main
public struct MySteamApp {
  public static func main() {
    guard let steam = SteamAPI(appID: .spaceWar, fakeAppIdTxtFile: true) else {
      print("SteamInit failed")
      return
    }
    print("Hello world with Steam name \(steam.friends.getPersonaName())")
  }
}

API 文档 这里

功能齐全的 AppKit/Metal 演示 这里

实现说明

Swift C++ 错误

在 Swift 6 中已基本修复。Linux 仍然有点痛苦。

技术限制,基于 6.0 Xcode 16.b3

非 Swift 问题

奇怪的 Steam 消息

SteamAPI_ManualDispatch_GetNextCallback() 中获取意外的 SteamAPICallCompleteds - 怀疑 steamworks 的某些部分试图在内部使用回调,而不了解手动分发模式。 或者我错过了一个 API 来分发它们。

似乎是由使用 steamnetworking 触发的。

Facepunch 也记录并丢弃了这些,所以,嗯,我想耸耸肩。

使用 steamnetworkingmessages 时获取 src/steamnetworkingsockets/clientlib/csteamnetworkingmessages.cpp (229) : Assertion Failed: [#40725897 pipe] Unlinking connection in state 1; 可能它不希望从 Steam ID 向自身发送消息。

JSON 注释

捕获一些关于将 json 反射到模块中的问题的注释。

贡献

欢迎:提出问题 / johnfairh@gmail.com / @johnfairh@mastodon.social

许可协议

根据 MIT 许可协议发布。Steamworks SDK 部分除外。