Skip to content

menduz/zig-steamworks

Repository files navigation

zig-steamworks

Zig bindings for Steamworks, compatible with v1.57 (March 2023).

The bindings are autogenerated from the C bindings and are compatible with

  • macOS (both Apple Silicon and Intel)
  • linux (x86_64)
  • windows (x86_64)

Main branch is compatible with Zig 0.14.0

Including the Steamworks SDK

Due to licence agreements, you must bring your own SDK. This project assumes it is located in the ./steamworks folder. But you can have it anywhere else.

Linking the Steamworks libraries.

// build.zig
const zig_steamworks = @import("zig_steamworks");

pub fn build(b: *std.Build) !void {
  // create a module
  const exe_mod = b.createModule( ... );

  // add the library as a dependency
  const steamworks_lib = b.dependency("zig_steamworks", .{
        .target = target,
        .optimize = optimize,
    });

  // add zig import and link static library
  exe_mod.addImport("steamworks", steamworks_lib.module("steamworks"));
  exe_mod.linkLibrary(steamworks_lib.artifact("steamworks"));

  // ...
  const exe = b.addExecutable( ... );

  zig_steamworks.addLibraryPath(steamworks_lib.builder, exe);
}

Build it

For convenience, this repository offers already built bindings for Zig in the src folder. Until Zig modules and package manager are mature, the recommended course of action is to copy these files into your project. In this case, you'll need to manually link the steamapi dlls and headers in your own build.zig file. Alternatively, you can use zig fetch --save git+https://github.com/menduz/zig-steamworks or zig fetch --save /path/to/local/zig-steamworks, and include it as a dependency as outlined above

To re-build the files run the following command

node generate.js path_to_steamworks/public/steam/steam_api.json

Example

const std = @import("std");
const steam = @import("steamworks");

/// callback hook for debug text emitted from the Steam API
pub fn SteamAPIDebugTextHook(nSeverity: c_int, pchDebugText: [*c]const u8) callconv(.C) void {
    // if you're running in the debugger, only warnings (nSeverity >= 1) will be sent
    // if you add -debug_steamapi to the command-line, a lot of extra informational messages will also be sent
    std.debug.print("SteamAPIDebugTextHook sev:{} msg: {s}\n", .{ nSeverity, pchDebugText });
}

/// get an authentication ticket for our user
fn authTicket(identity: *steam.SteamNetworkingIdentity) !void {
    var rgchToken: *[2048]u8 = try std.heap.c_allocator.create([2048]u8);
    var unTokenLen: c_uint = 0;
    var m_hAuthTicket = steam.SteamUser().GetAuthSessionTicket(rgchToken, 2048, &unTokenLen, identity);
    std.debug.print("GetAuthSessionTicket={} len={} token={s}", .{ m_hAuthTicket, unTokenLen, rgchToken });
}

pub fn main() !void {
    if (steam.SteamAPI_RestartAppIfNecessary(480)) {
        // if Steam is not running or the game wasn't started through Steam, SteamAPI_RestartAppIfNecessary starts the
        // local Steam client and also launches this game again.

        // Once you get a public Steam AppID assigned for this game, you need to replace k_uAppIdInvalid with it and
        // removed steam_appid.txt from the game depot.
        @panic("SteamAPI_RestartAppIfNecessary");
    }

    if (steam.SteamAPI_Init()) {
        std.debug.print("Steam initialized correctly. \n", .{});
    } else {
        @panic("Steam did not init\n");
    }

    steam.SteamClient().SetWarningMessageHook(SteamAPIDebugTextHook);

    defer steam.SteamAPI_Shutdown();

    std.debug.print("User {?}\n", .{steam.SteamUser().GetSteamID()});

    var sock = steam.SteamNetworkingSockets_SteamAPI();

    var pInfo: *steam.SteamNetworkingFakeIPResult_t = try std.heap.c_allocator.create(steam.SteamNetworkingFakeIPResult_t);
    defer std.heap.c_allocator.destroy(pInfo);

    sock.GetFakeIP(0, pInfo);
    std.debug.print("GetFakeIP: {}\n", .{pInfo});
    if (!pInfo.m_identity.IsEqualTo(pInfo.m_identity)) @panic("not equal");

    var pDetails: *steam.SteamNetAuthenticationStatus_t = try std.heap.c_allocator.create(steam.SteamNetAuthenticationStatus_t);
    defer std.heap.c_allocator.destroy(pDetails);

    var connectionStatus = sock.GetAuthenticationStatus(pDetails);
    std.debug.print("GetAuthenticationStatus: {} {}\n", .{ connectionStatus, pDetails });

    var pIdentity: *steam.SteamNetworkingIdentity = try std.heap.c_allocator.create(steam.SteamNetworkingIdentity);
    std.heap.c_allocator.destroy(pIdentity);
    var r = sock.GetIdentity(pIdentity);
    std.debug.print("GetIdentity={} {}\n", .{ r, pIdentity });

    try authTicket(pIdentity);
}

Alignment

Steamworks is packed with pragma packs, I've tried dozens of approaches even brute forcing all structs, but getting the right alignment for every struct was impossible. So I took the good-enough-and-not-so-elegant path of delegating the alignment to the C compiler entirely and then forcing Zig to copy everything with the generated code. In effect, you will see a lot of align(...) in the generated code.

In practice both Mac and Linux alignments are the same.

Please do the following steps for each steamworks release that is added to this repo. Mind the diff in the steamworks headers to remove the private fields (search for // ZIG:).

To generate the linux alignment file from aarch64 Mac hosts

# open a docker container using x86_64
docker run -v $(pwd):/mnt -w /mnt --rm -it --platform linux/amd64 mcr.microsoft.com/devcontainers/typescript-node:0-18 zsh
# install zig
sudo bash .devcontainer/install-zig.sh
# run the script and grab a coffee
bash ./create-align-info-linux.sh

To generate the mac alignment file from Mac

# run the script
bash ./create-align-info-mac.sh

To generate the mac alignment file from Windows

# run the script
.\create-align-info-mac.bat