Zig NEWS

Cover image for Implementing Steamworks API in Zig
Samarth Hattangady
Samarth Hattangady

Posted on

Implementing Steamworks API in Zig

Disclaimer: This is not the correct way to implement the Steamworks API. It's just something that I found to work. Since there was no other documentation regarding this, I thought it might be useful to share.

Steam is a video game digital distribution service and storefront. The Steamworks API allows your game to access the various features that Steam provides.

The API officially supports C++. It has some support for other languages. Regarding C:

steam_api_flat.h declares a set of "flat" functions that mirror the interface functions in the SDK. This is not pure C code, but it does use plain C linkage and calling conventions, so it is easy to interop with other languages. These functions are exported by steam_api[64][.dll/.so/dylib].

Up until now I had only used pure C from zig, so I tried the same set of things.

// in build.zig
exe.addCSourceFile("dependencies/steam/steam_api_impl.c", &[_][]const u8{"-std=c99"});
Enter fullscreen mode Exit fullscreen mode
// where steam_api_impl.c has
#define STEAM_FLAG_1
#define STEAM_FLAG_2
#include "steam_api_flat.h"
Enter fullscreen mode Exit fullscreen mode

or directly in my c.zig

pub usingnamespace @cImport({
    // other includes...
    @cInclude("steam_api_flat.h");
});
Enter fullscreen mode Exit fullscreen mode

Neither of these worked, which was because steam_api_flat.h has an #include steam_api.h which #includes a bunch of other header files, one of which eventually has some class or template or something else which causes the compile to fail.

Basically, I was not able to import C++ code into zig.


I needed just the following functionality

  1. Connect to Steam Client
  2. Trigger Steam Achievements

What I did instead

Convince Zig that the API that I need to use exists.

// core
pub extern fn SteamAPI_Init() bool;
pub extern fn SteamAPI_Shutdown() void;
pub extern fn SteamAPI_RestartAppIfNecessary(app_id: u32) bool;

// achievements
// the steam_api_flat.h is not publicly available code, so I won't share the API openly here. The C++ API is openly available, and you should be able to refer that and understand most of the details.
// basically, to translate, most of the class pointers can be cast to *anyopaque.
// for achievements, I had to translate the following commands.
pub extern fn get_steam_user(..);
pub extern fn get_steam_id(..);
pub extern fn get_steam_user_stats(..);
pub extern fn steam_request_user_stats(..);
pub extern fn steam_set_achievement(..);
pub extern fn steam_store_stats(..);
Enter fullscreen mode Exit fullscreen mode

I don't like that so much of it is anyopaque. There may be better ways to do this, but I'm not sure how to.

Convince the linker that these extern APIs exist.

// in build.zig
// for the linker
exe.addObjectFile("dependencies/steamworks_sdk_155/win64/steam_api64.lib");
// this just adds the dll file to zig-out/bin dir
b.installBinFile("dependencies/steamworks_sdk_155/win64/steam_api64.dll", "steam_api64.dll");
Enter fullscreen mode Exit fullscreen mode

Use the code in game

In main.zig, at startup

var steam_user_stats: *anyopaque = undefined;
if (c.SteamAPI_RestartAppIfNecessary(constants.STEAM_APP_ID)) {
    return;  // steam will relaunch the game from the steam client.
}
if (c.SteamAPI_Init()) {
    var user = c.get_steam_user();
    const steam_id = c.get_steam_id(user);
    steam_user_stats = c.get_steam_user_stats();
    _ = c.steam_request_user_stats(steam_user_stats, steam_id);
    if (constants.BUILDER_MODE) helpers.debug_print("steam init done: user stats {d}\n", .{steam_id});
} else {
    if (constants.BUILDER_MODE) helpers.debug_print("steam init failed\n", .{});
}
defer c.SteamAPI_Shutdown();
// send steam_user_stats to the game.
Enter fullscreen mode Exit fullscreen mode

In game,

// Achievement is an enum
fn trigger_achievement(self: *Self, achievement: Achievement) void {
    // mark achievement as completed
    const triggered = c.steam_set_achievement(self.steam_user_stats, &@tagName(achievement)[0]);
    if (triggered) {
        // update the steam client, so that the overlay can popup.
        _ = c.steam_store_stats(self.steam_user_stats);
    }
}
Enter fullscreen mode Exit fullscreen mode

And we're done.


Other notes

  • Some APIs have callbacks. I don't know how this method would work in that case.
  • There is a json file, steam_api.json that describes (almost all of) the interfaces, types, and functions in the SDK. It should be possible to autogenerate the extern APIs with this.

As mentioned, this is not the ideal way. I am sure there are better ways to accomplish the same. But this was just something that worked for me.

This was all discovered when working on my game, Konkan Coast Pirate Solutions.

It is a puzzle game about helping pirate ships do pirate things. Set up the simulations. Watch how the ships behave. Explore how the systems interact.

Wishlist on Steam now.

Top comments (0)