Hyperfocus

Hyperfocus game integration: the game bridge and the Scratch extension

Hyperfocus is the launcher for the games on ikelene.net and the community site. When it starts your game it also opens a small local web server that the game can call, so the game can set its Discord status, read who is playing, invite friends, hand out achievements and show launcher notifications. This guide is the overview and the reference for that server (the game bridge) and for the Hyperfocus Scratch extension that wraps it.

What a game can use

How Hyperfocus launches your game

When the player presses Play, Hyperfocus runs the game's executable from its install folder (the executable's folder is the working directory). Only .exe, .cmd and .bat files are started and tracked this way. Anything else (an .html file, for example) is simply opened with its default program, and then the launcher has no running game to attach the bridge or the Velora token to. If you ship a web game, package it as an app first (the community site can package .sb3, .pmp and .html projects for you, see the publishing guide).

Game IDs

Every game has an ID. Official games use the id in the Hyperfocus game list (for example fnaj). Community games use c- plus the 8-character ID shown on the community site, for example c-cyqr2nr2. With Developer Mode on, right-click a game in Hyperfocus and choose Copy game ID. Use the c-<id> form in every call; the bare 8-character ID also works for presence, invites and notifications from Hyperfocus 1.3.33-beta.4 onward (achievement calls have always accepted it). When the game was launched by Hyperfocus the simplest thing is to leave the game ID out and let the launcher use the game that is running.

Links that open Hyperfocus

LinkWhat it does
hyperfocus://<game id>Opens Hyperfocus with that game selected. Does not launch it.
hyperfocus://play/<game id>Launches the game.
hyperfocus://add/<community id>Adds a community game to the player's library and selects it (the 8-character ID, without c-).
hyperfocus://join/<game id>/<code>A game invite. See game invites.
hyperfocus://beta/<CODE>Redeems a beta code.

The game bridge

The bridge listens on http://127.0.0.1:47833, on this PC only. All replies are JSON.

Endpoints

CallPurpose
GET /hyperfocus/statusIs Hyperfocus there, and which games are running.
POST /hyperfocus/presenceSet the Discord line, the Discord application and the status friends see.
POST /hyperfocus/presence/resetBack to the defaults.
GET /hyperfocus/userWho is logged in to the launcher (display only).
POST /hyperfocus/invitesTurn invites on or off and set the join code.
GET /hyperfocus/inviteRead an invite the player accepted.
POST /hyperfocus/invite/clearForget the accepted invite.
POST /hyperfocus/friends/openOpen the launcher's friends list over the game.
GET /hyperfocus/achievementsThe game's achievements and which ones the player has.
POST /hyperfocus/achievements/grantUnlock an achievement.
POST /hyperfocus/achievements/revokeTake an achievement back.
POST /hyperfocus/notifyShow a launcher notification.

GET /hyperfocus/status

curl http://127.0.0.1:47833/hyperfocus/status

{
  "ok": true,
  "hyperfocus": true,
  "version": "1.3.33",
  "running": ["fnaj"],
  "currentGame": "fnaj",
  "devNoGame": false
}

running lists the games Hyperfocus launched that are still open. devNoGame is true when Developer Mode allows connections with no game open. A game is "launched from Hyperfocus" when its ID is in running (or devNoGame is true).

POST /hyperfocus/presence

All fields are optional. Details in the Discord presence guide.

curl -X POST http://127.0.0.1:47833/hyperfocus/presence \
  -H "Content-Type: application/json" \
  -d '{"state":"Night 1 - 5 AM","status":"In the lobby","appId":"123456789012345678"}'

{ "ok": true, "game": "fnaj",
  "presence": { "state": "Night 1 - 5 AM", "status": "In the lobby", "clientId": "123456789012345678" } }

POST /hyperfocus/presence/reset clears all three.

GET /hyperfocus/user

{ "ok": true, "loggedIn": true, "username": "Ikelene", "pfp": "https://...", "color": "#b09bcb", "rank": "owner" }

If nobody is logged in, loggedIn is false and the other fields are empty. This works without a game running. It is for showing the player's name and picture; it is not proof of identity. For anything that matters, use the Velora token and validate it.

Invites

POST /hyperfocus/invites   {"enabled": true, "code": "room123"}
-> { "ok": true, "invites": { "enabled": true, "code": "room123" } }

GET  /hyperfocus/invite
-> { "ok": true, "invite": { "code": "room123", "from": "Ikelene", "seq": 3 }, "invites": { "enabled": true, "code": "room123" } }
   ("invite" is null when nothing was accepted)

POST /hyperfocus/invite/clear    -> { "ok": true }
POST /hyperfocus/friends/open    -> { "ok": true }

The join code may contain letters, numbers, - and _, up to 64 characters (400 bad_code otherwise). See the invites guide for the whole flow.

Achievements

GET /hyperfocus/achievements
-> { "ok": true, "game": "fnaj", "achievements": {
     "night1": { "name": "Night 1", "description": "Survive the first night", "icon": "https://...",
                 "rarity": "common", "hidden": false, "game": "fnaj", "gameName": "Five Nights at JARVIS",
                 "unlocked": true, "unlockedAt": 1790000000000 } } }

POST /hyperfocus/achievements/grant    {"achievement": "night1"}
POST /hyperfocus/achievements/revoke   {"achievement": "night1"}

Hidden achievements the player does not have yet are returned as "Hidden Achievement". Grant answers { "ok": true, "game", "new": true|false, "achievement": {...} } and revoke { "ok": true, "game", "revoked": true|false }; Velora's own errors (404 achievement_not_found, 403 wrong_game) are passed on. See the achievements guide.

POST /hyperfocus/notify

curl -X POST http://127.0.0.1:47833/hyperfocus/notify \
  -d '{"title":"Night 1 complete!","text":"Only 4 more to go..."}'

{ "ok": true, "game": "fnaj" }

Shows a toast over the game. The title is trimmed to 80 characters (default is the game's name) and the text to 240. A game can send one every 1.5 seconds (429 too_fast otherwise). Clicking it opens the game in Hyperfocus.

Error replies

StatuserrorMeaning
400bad_json, bad_app_id, bad_code, bad_achievement_id, no_gameThe request body or an ID was not valid, or no game could be worked out.
401not_logged_inAchievements need the player to be logged in to Velora in Hyperfocus.
403forbidden, needs_permission, site_blockedWrong Host header, or a website the player has not allowed.
404not_found, achievement_not_foundUnknown route, or (while Velora is unreachable) an achievement ID the game does not define.
405post_only, use_postWrong HTTP method.
409not_runningThe game was not started by Hyperfocus. The reply includes a hint.
413too_bigBody over 4096 bytes.
429too_fastNotifications: one per 1.5 seconds.
othervelora_errorVelora answered with an error for an achievement call; hint has the message.

Calling the bridge from code

It is plain HTTP, so any language works. Check /status first and keep going quietly when it is not there, because players can also run your game without Hyperfocus.

// JavaScript (browser or Electron renderer)
const HF = 'http://127.0.0.1:47833/hyperfocus';

async function hyperfocus(path, body) {
  try {
    const res = await fetch(HF + path, body === undefined ? {} : {
      method: 'POST',
      headers: { 'Content-Type': 'text/plain' },      // text/plain: no CORS preflight
      body: JSON.stringify(body),
    });
    return await res.json();
  } catch {
    return { ok: false, error: 'hyperfocus_not_reachable' };
  }
}

const status = await hyperfocus('/status');
if (status.hyperfocus && status.running.length) {
  await hyperfocus('/presence', { state: 'Night 1 - 5 AM' });
  await hyperfocus('/achievements/grant', { achievement: 'night1' });
}
# Python
import requests

HF = "http://127.0.0.1:47833/hyperfocus"

def hyperfocus(path, body=None):
    try:
        r = requests.post(HF + path, json=body, timeout=2) if body is not None else requests.get(HF + path, timeout=2)
        return r.json()
    except Exception:
        return {"ok": False, "error": "hyperfocus_not_reachable"}

hyperfocus("/notify", {"title": "Level 2", "text": "New area unlocked"})

Testing without launching from Hyperfocus

In Hyperfocus open Settings, developer, turn on Developer Mode, then turn on Allow game extension connections when no game is open. The bridge then accepts presence, invite, achievement and notification calls even though Hyperfocus did not start the game. Without a running game, name the game in the call (game) - achievements and invites need a game ID; presence falls back to a test ID. Achievements also work for community games that are still waiting for review. The same settings page has the test achievement popups, the bridge log and the data folder.

The Hyperfocus Scratch extension

For TurboWarp and PenguinMod projects (including projects packaged into an app) there is an extension that wraps the bridge. Add it as a custom extension from this URL:

https://ikelene.net/launcher/hyperfocus_extension.js

It works sandboxed or unsandboxed. While the game was not started from Hyperfocus, every command does nothing and the reporters are empty, so the same project can also be played elsewhere. The extension keeps checking for the launcher every 5 seconds, and when the launcher shows up (or comes back after a restart) it sends the Discord line, friends status, invite state and join code again. If you open Hyperfocus after the project started, use attempt reconnection to launcher.

Opening the project inside the editor in a browser needs the one-time Allow popup in Hyperfocus (see above) and Developer Mode, because the editor was not started by the launcher. The extension has a How to test? button that explains this.

Blocks

General

launched from Hyperfocus?attempt reconnection to launchersend launcher notification TITLE TEXTthis game's Hyperfocus ID is IDlast Hyperfocus response

launched from Hyperfocus? is true while the launcher can hear the game. this game's Hyperfocus ID is is only needed when several apps run at once, for achievements while testing, or for Developer Mode. last Hyperfocus response is the raw JSON of the last call, for debugging.

Discord status

set Discord status to TEXTuse Discord application ID ID for this sessionreset Discord status

Player

logged in to Velora?Velora usernameINFO of the player

[INFO] of the player has a menu: profile picture URL, accent colour, rank.

Friends and multiplayer

allow inviting friends STATEset join code to CODEopen friends listset status for friends to TEXTwhen joining a friend's gamejoined from an invite?invite join codeinvited byforget the invite

Achievements

grant achievement IDrevoke achievement IDhas achievement ID?all achievements (JSON)

all achievements (JSON) reports the same object as GET /hyperfocus/achievements.

No Velora token from the extension.The player blocks only report the name, picture, colour and rank for display. There is no block that returns a Velora token, so a Scratch project cannot prove who the player is through Hyperfocus. See the Velora login guide for what packaged Scratch games can do.

Questions

Which games can use the game bridge?

Games that Hyperfocus started itself and that are still running, launched as an .exe, .cmd or .bat file. The bridge ignores calls for games it did not start.

Does the bridge work from a web page?

Yes, but the player has to allow the website once in a Hyperfocus popup for the calls that expose player information (user, invites, achievements, notifications). Presence calls are not gated.

How do I test without launching the game?

Turn on Developer Mode in Hyperfocus settings, then Allow game extension connections when no game is open. The bridge then accepts calls for a game ID you name.

Does the Scratch extension give my game a Velora token?

No. The extension only exposes the player's username, picture, accent colour and rank for display. To get a real token, use the veloraToken setting and the HF_VELORA_TOKEN environment variable.

More Hyperfocus guides