Skip to content

Commit

Permalink
New extension: steamworks (#1622)
Browse files Browse the repository at this point in the history
  • Loading branch information
GarboMuffin authored Jul 26, 2024
1 parent 8541c95 commit d916741
Show file tree
Hide file tree
Showing 3 changed files with 344 additions and 0 deletions.
98 changes: 98 additions & 0 deletions docs/steamworks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Steamworks

The Steamworks extension lets you use these Steam APIs:

- Basic user information (name, id, level, country)
- Achievements
- DLC
- Opening URLs in the Steam Overlay

## Enabling Steamworks

Steamworks support will be automatically enabled when you [package](https://packager.turbowarp.org/) your project using one of these environments:

- Electron Windows application (64-bit)
- Electron macOS application
- Electron Linux application (64-bit)

The blocks will not work in the editor, 32-bit environments, ARM environments, plain HTML files, WKWebView, or NW.js. You can still run the blocks, they just won't interact with Steam at all.

When you package a project that uses the Steamworks extension, the packager willl ask you to enter your game's App ID, which you can find on the Steamworks website. If you don't have an App ID yet, see the demo game section below.

You can just run the executable directly as usual; you don't need to start the game from Steam for Steamworks to function. There are a couple caveats:

- On macOS and Linux, the Steam overlay may not function
- On Linux, Steam needs to be installed as a native package, not as a Flatpak/Snap as the sandbox will prevent the apps from communicating

## Security considerations

Using the Steamworks extension will not prevent people from pirating your game.

The Steamworks extension is also inherently client-side, so a cheater could manipulate all of the Steamworks blocks to return whatever they want. You shouldn't use them for things that are security critical.

## Demo game

For testing the Steamworks extension without paying for a Steamworks Partner Program membership, you can use the free Steamworks demo game. It's called Spacewar and its App ID is `480`. You don't need to install Spacewar; rather you can use its App ID to test various Steamworks APIs.

Spacewar has achievements with the following API Names, which can used for testing the achievement blocks:

- `ACH_WIN_ONE_GAME`
- `ACH_WIN_100_GAMES`
- `ACH_TRAVEL_FAR_ACCUM`
- `ACH_TRAVEL_FAR_SINGLE`

## Basic information

Remember that Steamworks is only properly enabled when your project is packaged in a few specific environments. You can detect if this is the case using:

```scratch
<has steamworks? :: #136C9F>
```

Then you can get basic information about the user using:

```scratch
(get user (name v) :: #136C9F)
```

## Achievements

Achievements are created in the Steamworks website. The **API Name** of each achievement is what you need to provide in your project's code to the Steamworks extension.

This would unlock the `ACH_WIN_ONE_GAME` achievement from Spacewar:

```scratch
when this sprite clicked
set achievement [ACH_WIN_ONE_GAME] unlocked to (true v) :: #136C9F
```

You can also detect if an achievement has already been unlocked:

```scratch
when flag clicked
forever
if <achievement [ACH_WIN_ONE_GAME] unlocked? :: #136C9F> then
say [Unlocked!]
else
say [Not unlocked :(]
end
end
```

## DLC

Each DLC has its own App ID which you can find in the Steamworks website. You can detect if it is installed using:

```scratch
if <(DLC v) [1234] installed? :: #136C9F> then
end
```

## Overlay

The Steamworks extension has a block to open URLs in the Steam Overlay's web browser. If the overlay is not working, it might open in the Steam app instead. If that also doesn't work, it will open in the default web browser. Regardless it won't display the "The project wants to open a new window or tab" security prompt when packaged.

```scratch
open (URL v) [https://example.com/] in overlay :: #136C9F
```
1 change: 1 addition & 0 deletions extensions/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"veggiecan/LongmanDictionary",
"CubesterYT/TurboHook",
"Alestore/nfcwarp",
"steamworks",
"itchio",
"gamejolt",
"obviousAlexC/newgroundsIO",
Expand Down
245 changes: 245 additions & 0 deletions extensions/steamworks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// Name: Steamworks
// ID: steamworks
// Description: Connect your project to Steamworks APIs.
// License: MPL-2.0
// Context: Probably don't translate the word "Steamworks".

(function (Scratch) {
"use strict";

/* globals Steamworks */

const canUseSteamworks = typeof Steamworks !== "undefined" && Steamworks.ok();

class SteamworksExtension {
getInfo() {
return {
id: "steamworks",
name: "Steamworks",
color1: "#136C9F",
color2: "#105e8c",
color3: "#0d486b",
docsURI: "https://extensions.turbowarp.org/steamworks",
blocks: [
{
blockType: Scratch.BlockType.BOOLEAN,
opcode: "hasSteamworks",
text: Scratch.translate("has steamworks?"),
},

{
blockType: Scratch.BlockType.REPORTER,
opcode: "getUserInfo",
text: Scratch.translate({
default: "get user [THING]",
description:
"[THING] is a dropdown with name, steam ID, account level, IP country, etc.",
}),
arguments: {
THING: {
type: Scratch.ArgumentType.STRING,
menu: "userInfo",
},
},
},

"---",

{
blockType: Scratch.BlockType.COMMAND,
opcode: "setAchievement",
text: Scratch.translate({
default: "set achievement [ACHIEVEMENT] unlocked to [STATUS]",
description: "[STATUS] is true/false dropdown",
}),
arguments: {
ACHIEVEMENT: {
type: Scratch.ArgumentType.STRING,
defaultValue: "",
},
STATUS: {
type: Scratch.ArgumentType.STRING,
menu: "achievementUnlocked",
},
},
},
{
blockType: Scratch.BlockType.BOOLEAN,
opcode: "getAchievement",
text: Scratch.translate("achievement [ACHIEVEMENT] unlocked?"),
arguments: {
ACHIEVEMENT: {
type: Scratch.ArgumentType.STRING,
defaultValue: "",
},
},
},

"---",

{
blockType: Scratch.BlockType.BOOLEAN,
opcode: "getInstalled",
text: Scratch.translate({
default: "[TYPE] [ID] installed?",
description: "eg. can be read as 'DLC 1234 installed?'",
}),
arguments: {
TYPE: {
type: Scratch.ArgumentType.STRING,
menu: "installType",
},
ID: {
type: Scratch.ArgumentType.STRING,
defaultValue: "",
},
},
},

"---",

{
blockType: Scratch.BlockType.COMMAND,
opcode: "openInOverlay",
text: Scratch.translate({
default: "open [TYPE] [DATA] in overlay",
description: "eg. 'open URL example.com in overlay'",
}),
arguments: {
TYPE: {
type: Scratch.ArgumentType.STRING,
menu: "overlayType",
},
DATA: {
type: Scratch.ArgumentType.STRING,
defaultValue: "https://example.com/",
},
},
},
],
menus: {
userInfo: {
acceptReporters: true,
items: [
{
value: "name",
text: Scratch.translate("name"),
},
{
value: "level",
text: Scratch.translate({
default: "level",
description: "Steam account level",
}),
},
{
value: "IP country",
text: Scratch.translate("IP country"),
},
{
value: "steam ID",
text: Scratch.translate("steam ID"),
},
],
},

achievementUnlocked: {
acceptReporters: true,
items: [
{
value: "true",
text: Scratch.translate("true"),
},
{
value: "false",
text: Scratch.translate("false"),
},
],
},

installType: {
acceptReporters: true,
items: [
{
value: "DLC",
text: Scratch.translate({
default: "DLC",
description: "Downloadable content",
}),
},
],
},

overlayType: {
acceptReporters: true,
items: [
{
value: "URL",
text: Scratch.translate("URL"),
},
],
},
},
};
}

hasSteamworks() {
return canUseSteamworks;
}

getUserInfo({ THING }) {
if (!canUseSteamworks) return "Steamworks unavailable";
switch (THING) {
case "name":
return Steamworks.localplayer.getName();
case "level":
return Steamworks.localplayer.getLevel();
case "IP country":
return Steamworks.localplayer.getIpCountry();
case "steam ID":
return Steamworks.localplayer.getSteamId().steamId64;
}
return "???";
}

setAchievement({ ACHIEVEMENT, STATUS }) {
if (!canUseSteamworks) return;
if (Scratch.Cast.toBoolean(STATUS)) {
Steamworks.achievement.activate(Scratch.Cast.toString(ACHIEVEMENT));
} else {
Steamworks.achievement.clear(Scratch.Cast.toString(ACHIEVEMENT));
}
}

getAchievement({ ACHIEVEMENT }) {
if (!canUseSteamworks) return false;
return Steamworks.achievement.isActivated(
Scratch.Cast.toString(ACHIEVEMENT)
);
}

getInstalled({ TYPE, ID }) {
if (!canUseSteamworks) return false;
if (TYPE === "DLC") {
return Steamworks.apps.isDlcInstalled(Scratch.Cast.toNumber(ID));
}
return false;
}

openInOverlay({ TYPE, DATA }) {
if (TYPE === "URL") {
const url = Scratch.Cast.toString(DATA);
if (canUseSteamworks) {
// This will always be a packaged environment so don't need to bother
// with canOpenWindow()
Steamworks.overlay.activateToWebPage(DATA);
} else {
// Don't await result, we don't care
Scratch.openWindow(url);
}
}
}
}

Scratch.extensions.register(new SteamworksExtension());
})(Scratch);

0 comments on commit d916741

Please sign in to comment.