@xgamesfun/xcore 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +573 -2
- package/bin/xcore.mjs +141 -0
- package/commands/add.mjs +55 -0
- package/commands/adopt.mjs +57 -0
- package/commands/backend.mjs +70 -0
- package/commands/check.mjs +60 -0
- package/commands/dev.mjs +74 -0
- package/commands/domain.mjs +89 -0
- package/commands/embed.mjs +57 -0
- package/commands/login.mjs +58 -0
- package/commands/logout.mjs +29 -0
- package/commands/loot.mjs +142 -0
- package/commands/plan.mjs +78 -0
- package/commands/publish.mjs +225 -0
- package/commands/secret.mjs +137 -0
- package/docs/SERVER.md +410 -0
- package/lib/add.mjs +566 -0
- package/lib/browser.mjs +141 -0
- package/lib/check.mjs +711 -0
- package/lib/cli.mjs +37 -0
- package/lib/client/gamesfun.js +1315 -0
- package/lib/client/godot/gamesfun.gd +308 -0
- package/lib/client/godot/gamesfun_peer.gd +217 -0
- package/lib/client/server/database.js +229 -0
- package/lib/client/server/rooms.js +270 -0
- package/lib/client/server/rpc.js +206 -0
- package/lib/client/server/vouchers.js +396 -0
- package/lib/client/unity/GamesFun.cs +323 -0
- package/lib/client/unity/GamesFun.jslib +135 -0
- package/lib/command.mjs +48 -0
- package/lib/config-dir.mjs +13 -0
- package/lib/credentials.mjs +104 -0
- package/lib/dev/gf-local.js +106 -0
- package/lib/dev/index.mjs +306 -0
- package/lib/dev/limits.mjs +73 -0
- package/lib/dev/platform.mjs +492 -0
- package/lib/dev/server.mjs +251 -0
- package/lib/dev/tools.mjs +102 -0
- package/lib/economy.mjs +154 -0
- package/lib/games.mjs +40 -0
- package/lib/open.mjs +34 -0
- package/lib/play-policy.mjs +223 -0
- package/lib/publish/adopt.mjs +1380 -0
- package/lib/publish/client.mjs +204 -0
- package/lib/publish/gate.mjs +63 -0
- package/lib/publish/hosting.mjs +83 -0
- package/lib/publish/index.mjs +552 -0
- package/lib/publish/names.mjs +205 -0
- package/lib/publish/oidc.mjs +108 -0
- package/lib/publish/page.mjs +259 -0
- package/lib/publish/plans.mjs +96 -0
- package/lib/publish/scan.mjs +304 -0
- package/lib/publish/secrets.mjs +51 -0
- package/lib/publish/site.mjs +202 -0
- package/lib/publish/tree.mjs +260 -0
- package/lib/secret-patterns.mjs +39 -0
- package/lib/secret-store.mjs +220 -0
- package/lib/server/bundle.mjs +350 -0
- package/lib/server/contract.mjs +267 -0
- package/lib/server/secrets.mjs +26 -0
- package/lib/solana-rpc.mjs +92 -0
- package/loot-page/wallets.js +54 -0
- package/package.json +70 -4
- package/skills/bring-your-own-game/SKILL.md +185 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 games.fun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,574 @@
|
|
|
1
|
-
#
|
|
1
|
+
# xcore
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The one place to start with games.fun (`@xgamesfun/xcore` on npm, the `xcore` command). What you came for decides where
|
|
4
|
+
it takes you:
|
|
5
|
+
|
|
6
|
+
| You have | xcore | Then |
|
|
7
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
8
|
+
| **A web game you already made** | brings it to games.fun: checks it as the host serves it, gives it a backend, publishes it at a link anyone can play (this page) | |
|
|
9
|
+
| **A game that should pay or sell on chain** | adds a Solana economy on loot: shops, loot boxes, raffles, prize pools, staking, rewards (§4) | the economy tools are [`@xgamesfun/solana`](https://github.com/XGamesFun/solana-kit) (`xsolana`), which `xcore loot` runs |
|
|
10
|
+
| **An idea for a game, no game yet** | hands its commands to the kit | the game-making kit, `@xgamesfun/kit` (coming later) |
|
|
11
|
+
|
|
12
|
+
Nothing about a game you bring changes: it keeps its code, its framework and its build, as long as it fits the hosting
|
|
13
|
+
(below): static files a browser runs on its own, plus, if it needs one, a backend that is public on the internet or a
|
|
14
|
+
server of its own that games.fun runs.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx -y @xgamesfun/xcore adopt ./my-game # its record, and the list of what to fix
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install it once, `npm i -g @xgamesfun/xcore`, and then:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
xcore adopt ./my-game # its record, and the list of what to fix
|
|
24
|
+
xcore check ./my-game # the page in Chrome, as the host serves it
|
|
25
|
+
xcore dev ./my-game # play it here as games.fun hosts it: its server, sign-in, rooms, economy
|
|
26
|
+
xcore add signin ./my-game # players sign in (X or a wallet); saves and leaderboards
|
|
27
|
+
xcore add database ./my-game # a database of its own, put up with the game
|
|
28
|
+
xcore add multiplayer ./my-game # rooms players share (Godot's own multiplayer runs over them)
|
|
29
|
+
xcore backend use custom ./my-game --url https://api.my-game.example # only if it calls a backend you run
|
|
30
|
+
xcore loot add ./my-game # only if it gets an economy (a Solana economy on loot)
|
|
31
|
+
xcore add vouchers ./my-game # its server pays results it checked from a grant, with its own key
|
|
32
|
+
xcore login && xcore publish ./my-game
|
|
33
|
+
xcore embed add https://mygame.com ./my-game # your own site may show it (an <iframe>)
|
|
34
|
+
xcore domain add play.mygame.com ./my-game # Creator or Scale: it plays at your own domain
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
(`xcore` on npm is another package: always run this one as `npx -y @xgamesfun/xcore …`, or install it.) It needs Node 24
|
|
38
|
+
or newer, and Chrome, Edge or Chromium for the check (else it downloads Playwright's Chromium once). With an agent,
|
|
39
|
+
the skill in `skills/bring-your-own-game/` walks the same steps, and `xcore loot connect` adds the economy's tools.
|
|
40
|
+
|
|
41
|
+
**Making a game from an idea** is the kit's (`@xgamesfun/kit`): base worlds, the build loop, the studio page, its six
|
|
42
|
+
gates. The kit brings this package, and `xcore` hands the kit every command it does not have itself (`create`,
|
|
43
|
+
`verify` …) when the kit is in or above the folder you run it in, or `GAMESFUN_KIT` names it. A game the kit made
|
|
44
|
+
publishes, checks and runs (`dev`) through the kit, so its six gates run first. The kit's dev server also serves a
|
|
45
|
+
brought game, with the studio around it; its MCP tools for a brought game are `gamesfun_adopt`,
|
|
46
|
+
`gamesfun_verify`, `gamesfun_backend_use` (provider `custom`), `gamesfun_loot_add`, `gamesfun_publish_check` and
|
|
47
|
+
`gamesfun_publish`.
|
|
48
|
+
|
|
49
|
+
## What the hosting takes
|
|
50
|
+
|
|
51
|
+
| | |
|
|
52
|
+
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
53
|
+
| **The page** | Static files, served as they are (GET and HEAD). `index.html` at the root of what is published is the game's entry. A folder URL ending in `/` serves that folder's `index.html`; any other missing path is a "not found" page, with no fallback to `index.html` (a client-side router works from the entry page; a deep link to `/level/3` reloads to "not found"). |
|
|
54
|
+
| **Where it is served** | At its own origin's root once published (the link `xcore publish` prints). Under a folder in games.fun Studio's preview, in `xcore dev` and in the kit's dev server (`/game/`). A game uses relative URLs (`./assets/x.js`, not `/assets/x.js`), so it works in all of them. |
|
|
55
|
+
| **Its server, if any** | `server/` and `gamesfun.json` → `server`: a Cloudflare Worker that answers the game's `/api/*`, with up to 8 Durable Objects and the platform binding `env.GF` (sign-in, saves, scores, loot reports). [docs/SERVER.md](docs/SERVER.md). Bundled into one file on publish; never a public file. |
|
|
56
|
+
| **A backend of its own** | Any public `https://` or `wss://` origin, listed in `backend.json` → `origins` (at most 8, public names only, no IP address, no localhost). The play host lets the page reach exactly those. |
|
|
57
|
+
| **Solana** | Through the game's own devnet RPC (`xcore add rpc`: its server's `api/rpc`), else the play host's `/loot/v1/rpc` (games.fun's shared devnet relay, counted toward the game's plan), with the page client in the game (`gf/loot/`, from `@xgamesfun/solana`). |
|
|
58
|
+
| **What the page may load** | Scripts, style sheets, fonts and frames from its own files only (inline scripts, `eval`, WebAssembly and Web Workers from the game's own files are allowed; a page is not cross-origin isolated, so `SharedArrayBuffer` is not available: export engines single-threaded). Connections to its own origin and to `backend.json`'s origins; pictures and sounds from its own files and from those `https://` origins. No `<base>`, no form posts, no `<object>`. Camera, microphone, location, payments and USB are off; fullscreen, pointer lock, gamepads, audio and WebGL are not limited by the page's headers. |
|
|
59
|
+
| **Paths the host owns** | `/api/*` (the game's server, when it has one), `/platform/v1/*`, the multiplayer signaling (`/signal/v1`), the live watch (`/watch/v1`), world saves (`/world/v1/`), player keys (`/player/v1/`) and `/loot/v1/*`. A file of the game under one of them is never served. |
|
|
60
|
+
| **Files** | Everything in the published folder goes up except hidden files and folders, and, at its top level, `evidence/`, `node_modules/`, `server/`, `supabase/`, `gamesfun.json` and the makers' notes (`README.md`, `NOTES.md`, `LAUNCH.md`, `FRICTION.md`, `CLAUDE.md`, `AGENTS.md`, `TODO.md`) (`.gitignore` is not read): move development material out of it, or give it a name that starts with a dot. Hidden files and folders never go up. A file whose extension the host does not know is served as `application/octet-stream` with `nosniff` (fine for data read with `fetch()`; a browser will not run it as a script or a style). |
|
|
61
|
+
| **Limits** | One game: 3,000 files, 100 MiB (104,857,600 bytes; one file may be all of it). One account: 30 publishes and 1 GB of new files a day, 100 games. Publish reads the account's own limits from games.fun. The server bundle: 5 MB. What the game's server, saves and scores may use is its plan (§5, `xcore plan`). |
|
|
62
|
+
| **Refused, always** | `.env` files, symbolic links, key and certificate files, wallet keypairs, and anything in a text file that looks like a secret (games.fun runs the same scan again). A browser game carries only keys made to be public: a Supabase anon or publishable key, a Firebase web config's `apiKey`. |
|
|
63
|
+
|
|
64
|
+
## 1. Adopt
|
|
65
|
+
|
|
66
|
+
`xcore adopt [folder]` (default: the folder you are in) reads the folder's shape:
|
|
67
|
+
|
|
68
|
+
- **Plain static files** (an `index.html` at its root, or in `dist/`, `build/`, `public/`, `www/` …): published as
|
|
69
|
+
they are.
|
|
70
|
+
- **A project with a build**: the build runs (install its packages first, `npm ci` or `npm install`: adopt does not), and its output is
|
|
71
|
+
published. Known: Vite (`dist`, its `outDir`; `public/`), Next.js with `output: 'export'` (`out`), Astro (`dist`),
|
|
72
|
+
SvelteKit with `@sveltejs/adapter-static` (`build`), Nuxt with `nuxt generate` (`.output/public`), Create React App
|
|
73
|
+
(`build`), Parcel (`dist`), Gatsby (`public`), React Router in SPA mode (`build/client`). Any other `build` script:
|
|
74
|
+
its output is found after it runs (`dist`, `build`, `out` …), or given: `--build "<cmd>" --out <dir>`.
|
|
75
|
+
- **Refused** (nothing is written): a frontend rendered by a server (Next.js without a static export, SvelteKit
|
|
76
|
+
without the static adapter, `nuxt build`, Astro with `output: 'server'`, Remix, an Express app with no static
|
|
77
|
+
page), a folder with no `index.html` and no build, a build that fails. Each comes with what to do.
|
|
78
|
+
|
|
79
|
+
It writes the kit's record into the game's `gamesfun.json` (never published):
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "page": { "build": "npm run build", "out": "dist", "static": "public", "title": "My Game" } }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Key | |
|
|
86
|
+
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
87
|
+
| `build` | the command that makes the published files, run in the game's folder; none for plain files |
|
|
88
|
+
| `out` | the folder that is published: `"."` for plain files, else the build's output |
|
|
89
|
+
| `static` | the folder the build copies as it is (Vite, Next.js, Astro, CRA: `public`), where xcore puts the files it adds (`backend.json`, `xsolana.json`, `gf/loot/`), so every build carries them; for plain files, the folder itself |
|
|
90
|
+
| `title` | optional: the game's name on games.fun (else `index.html`'s `<title>`, else `package.json`'s name) |
|
|
91
|
+
|
|
92
|
+
`--static <dir>` names that folder when the kit cannot tell; `--no-build` reads what is built already. A `server`
|
|
93
|
+
part of `gamesfun.json` (the game's own server) stays as it is.
|
|
94
|
+
|
|
95
|
+
Then a plain list, each line with its fix:
|
|
96
|
+
|
|
97
|
+
- **Fix before publishing**: what publish refuses (secrets, `.env` files, links, a `backend.json` the host cannot
|
|
98
|
+
open, files under the host's own paths, no `index.html`) and what breaks once hosted (files over the limits, paths
|
|
99
|
+
from the root, a Vite `base` other than `'./'`, scripts, style sheets, web fonts or frames from other sites, a
|
|
100
|
+
`<base>`, an address on this machine).
|
|
101
|
+
- **Worth a look**: other sites the code names that `backend.json` does not list (a library's own documentation links
|
|
102
|
+
need nothing: the page check shows which ones the page really calls), a service worker (allowed; it keeps old files
|
|
103
|
+
after a new version until it updates itself), the camera or location, files served without a type, hidden files
|
|
104
|
+
that do not go up, a tab icon named from the root.
|
|
105
|
+
|
|
106
|
+
Fix the list in the game's own files (the source or the build's config, never the build's output) and adopt again
|
|
107
|
+
until it is empty. Adopt reads the files; `xcore check` then runs the page and finds what only shows at run time (a
|
|
108
|
+
path from the root built in a script, a request to a site `backend.json` does not list).
|
|
109
|
+
|
|
110
|
+
**A game of several pages** (a Next.js export with routes, Astro pages): such builds link their pages from the root
|
|
111
|
+
(`/level/2/`), and Next.js has no relative mode for them. Every page works at the published link, which is the game's
|
|
112
|
+
own origin, but not under a folder, so `xcore check` (which serves the game under one, as Studio's preview and the
|
|
113
|
+
kit's dev server do) fails them and publish saves a private draft. Until games.fun previews games at an origin of their
|
|
114
|
+
own, make the game one page that changes screens itself (or uses a hash router), or keep it a single-page export.
|
|
115
|
+
|
|
116
|
+
**A Nuxt game**: `nuxt generate` with its defaults links every file from the root and routes by path. Make it one
|
|
117
|
+
page that is served from any folder: in `nuxt.config`, `ssr: false`, `app: { cdnURL: './' }` and
|
|
118
|
+
`router: { options: { hashMode: true } }`; in the game's own code, relative paths (`fetch('levels.json')`, and in a
|
|
119
|
+
template `:src="'moth.png'"` for a file in `public/`, since Vue reads `src="./moth.png"` as an import).
|
|
120
|
+
|
|
121
|
+
**React Router's framework mode** (`react-router.config` with `ssr: false`) builds one page for `/` and routes by
|
|
122
|
+
path, with no hash mode and no relative mode (`base: './'` makes it load its routes from `assets/assets/` and reload
|
|
123
|
+
for ever). It works at the published link but not under a folder, so `xcore check` fails it as a game of several
|
|
124
|
+
pages. For a game that runs everywhere, use React Router as a library in a Vite app, with `createHashRouter`.
|
|
125
|
+
|
|
126
|
+
**A monorepo** (npm, Yarn or Bun workspaces, `pnpm-workspace.yaml`): adopt the repo's root when its build script builds
|
|
127
|
+
the game; the output is found in the workspace it built (`packages/game/dist`). Or adopt the game's own package: its
|
|
128
|
+
packages installed at the root count as installed.
|
|
129
|
+
|
|
130
|
+
**A game exported by an engine** (Godot, Unity, Defold, GameMaker): adopt the project, not the export folder the
|
|
131
|
+
engine rewrites, and name the export: `xcore adopt . --out build/web` (or `--build "<export command>" --out …`). A
|
|
132
|
+
Godot project never exported for the Web gets its Web preset from `xcore adopt .` (single-threaded, into `build/web/`),
|
|
133
|
+
with the one command that exports it; export, then adopt again. Export again before each publish: with no build
|
|
134
|
+
command, `check` and `publish` take the export as it is on disk (`check` says when the project changed after it).
|
|
135
|
+
`gamesfun.json` and `evidence/` then stay out of the export. A whole export must fit in 100 MB: a stock Godot 4
|
|
136
|
+
`index.wasm` is about 36 MB, so the game's own `index.pck` has the rest (compressed textures and Ogg audio keep it small).
|
|
137
|
+
|
|
138
|
+
- **Godot 4.3 or later**: export for the Web with **Thread Support off**, and `xcore add signin` for sign-in, saves
|
|
139
|
+
and leaderboards from GDScript (§3). A games.fun page is not cross-origin
|
|
140
|
+
isolated, so a threaded export stops at "missing features" (Godot 4.0 to 4.2 always use threads: upgrade). Godot's
|
|
141
|
+
ENet does not run in a browser: `xcore add multiplayer` and `GamesFun.join_room` put the game's high-level multiplayer
|
|
142
|
+
on games.fun's rooms unchanged (§3), or a `WebSocketMultiplayerPeer` reaches a server of your own (`backend use custom`).
|
|
143
|
+
- **Unity** (`xcore add signin` for sign-in, saves and leaderboards from C#, §3): Player Settings → Publishing
|
|
144
|
+
Settings, **Compression Format Disabled** (or keep it and turn on
|
|
145
|
+
**Decompression Fallback**): the play host sends files as they are, with no `Content-Encoding`, so a Brotli or Gzip
|
|
146
|
+
build without the fallback stops at "Unable to parse". Keep Multithreading off for the same reason as Godot's threads.
|
|
147
|
+
A service that connects to many host names (Photon's regional servers, for example) needs each one listed in
|
|
148
|
+
`backend.json` (at most 8, no wildcards).
|
|
149
|
+
- **Unreal Engine**: Unreal 5 has no web export (Unreal 4.24 moved HTML5 out of the engine, to a community project
|
|
150
|
+
that is no longer kept up), so there are no files to publish. Pixel Streaming runs the game on a GPU server of your
|
|
151
|
+
own and streams it as video; games.fun does not host that server, and a page that shows such a stream is untested.
|
|
152
|
+
|
|
153
|
+
**A game with its own Node server** (Express, Socket.IO, deployed on Render, Fly or similar), whose page is a folder of
|
|
154
|
+
static files (`public/`): adopt publishes that folder, and the server stays where it runs. Declare it with
|
|
155
|
+
`backend use custom`, point the page's connections at its address (`io('https://…')` instead of `io()`), and copy any
|
|
156
|
+
client file the server hands out (`/socket.io/socket.io.js`) into the game. The server answers CORS for
|
|
157
|
+
`https://<slug>-play.xgames.fun`.
|
|
158
|
+
|
|
159
|
+
**What xcore writes in the game's folder**: `gamesfun.json` (adopt's record; commit it), `evidence/` (each check's
|
|
160
|
+
screenshots and record, `evidence/verify-N/`) and `.gamesfun/` (publish's last passing check, and the link once
|
|
161
|
+
published). None of them is published; add `evidence/` and `.gamesfun/` to the game's `.gitignore`.
|
|
162
|
+
|
|
163
|
+
## 2. Check
|
|
164
|
+
|
|
165
|
+
`xcore check <folder>` is the page check (a brought game is never judged by the kit's six gates, which are
|
|
166
|
+
about games made on the kit; the kit's `verify` runs this same check for one):
|
|
167
|
+
|
|
168
|
+
1. **it builds** (skipped for plain files, or with `--no-build`)
|
|
169
|
+
2. **it fits the hosting**: everything publish would refuse, and what breaks once hosted, on exactly what would go up
|
|
170
|
+
3. **the page starts**: served under a folder with the play host's page policy, in Chrome at a phone's size and a
|
|
171
|
+
desktop's: no error thrown, nothing in the console's errors, none of its own files missing, nothing asked for from
|
|
172
|
+
the root, something on screen (screenshots in `evidence/verify-N/`)
|
|
173
|
+
4. **it stays inside its origins**: every request is one the hosted page may make; a script, style, font or frame from
|
|
174
|
+
another site, or a connection to an origin `backend.json` does not list, fails it
|
|
175
|
+
|
|
176
|
+
A backend that does not answer this machine is a note, not a failure. The game's play is its own: the page check says
|
|
177
|
+
it starts clean and stays inside what the hosting allows, nothing more. Publish's gate runs the same check, and a game
|
|
178
|
+
whose check fails goes up as a private draft only.
|
|
179
|
+
|
|
180
|
+
### Play it here: `xcore dev`
|
|
181
|
+
|
|
182
|
+
`xcore dev <folder>` serves the game on this machine the way games.fun will host it, with everything a published game
|
|
183
|
+
has behind it, so the whole game runs before it is published:
|
|
184
|
+
|
|
185
|
+
| On this machine | |
|
|
186
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
187
|
+
| `http://127.0.0.1:8787/game/` | the published files (it runs `page.build` first, unless `--no-build`), with the play host's headers |
|
|
188
|
+
| `/game/api/*` | the game's `server/` under workerd, the runtime games.fun runs it on: Durable Objects with their storage kept, WebSockets, `env.GF`, and its secrets from `.dev.vars`. A change under `server/` or to `.dev.vars` reloads it. Miniflare (with workerd, about 140 MB) is installed once, the first time a game with a server runs |
|
|
189
|
+
| `/platform/v1/*` | sign-in, saves and leaderboards: `gamesfun.js` signs in a local player (each tab its own, so two tabs play a room together), and `env.GF.verifyPlayer` accepts its token. Saves and scores are kept in `.gamesfun/dev-platform.json`, never published |
|
|
190
|
+
| `/loot/v1/rpc` | the economy's Solana devnet RPC: games.fun's shared relay (what a published page talks to unless the game brings its own: `xcore add rpc`), or your own with `XSOLANA_RPC_URL` in `.env` (its URL is never shown; a call the shared relay would refuse is said in the terminal). The economy's calls answer once it is live on devnet (`xcore loot approve`) |
|
|
191
|
+
|
|
192
|
+
Games made with any engine get all of it through `gamesfun.js` (`xcore add signin`): on a plain local server it stands
|
|
193
|
+
in with this browser's storage; under `xcore dev` it uses the real thing. What is not the same as hosted: no CPU or
|
|
194
|
+
request limits, and the server's outbound `fetch` is not limited to `backend.json`'s origins.
|
|
195
|
+
|
|
196
|
+
## 3. Players, a database, a backend
|
|
197
|
+
|
|
198
|
+
### Sign-in, saves and leaderboards: `xcore add signin`
|
|
199
|
+
|
|
200
|
+
games.fun signs players in (with X first, or a Solana wallet) for every game, and keeps each player's saves and the
|
|
201
|
+
game's leaderboards. `xcore add signin <folder>` gives the game the page client that reaches them, in the shape its
|
|
202
|
+
engine takes:
|
|
203
|
+
|
|
204
|
+
| The game | What it gets | From its code |
|
|
205
|
+
| ----------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
206
|
+
| A web game | `gamesfun.js` beside the page (in the folder a build copies as it is), loaded first in `index.html` | `await GamesFun.signIn()`, `GamesFun.saves.put('main', {...})` |
|
|
207
|
+
| A Godot 4 project | `addons/gamesfun/gamesfun.gd`, autoloaded as `GamesFun` | `await GamesFun.sign_in()`, `await GamesFun.saves_put("main", {})` |
|
|
208
|
+
| A Unity project | `Assets/Plugins/GamesFun/` (`GamesFun.jslib`, `GamesFun.cs`) | `await GamesFun.SignIn()`, `await GamesFun.SavesPut("main", json)` |
|
|
209
|
+
|
|
210
|
+
The same calls everywhere: sign in and out, the player (`id`, `name`, `level`, `wallet`, `x`), saves (any JSON, 64 KB
|
|
211
|
+
each, 16 a player), leaderboards (submit, top, the player's own), the game's database (below) and its own server
|
|
212
|
+
(`api`). Every call answers `{data, error, status}` and none throws, so a game plays on as a guest wherever games.fun is
|
|
213
|
+
not there.
|
|
214
|
+
|
|
215
|
+
**A leaderboard is the player's own word until the server takes it.** A signed-in player submits their own scores
|
|
216
|
+
from the page, so anyone can submit any score for themselves from the browser's console. A game with its own server
|
|
217
|
+
makes a board nobody can write that way: `env.GF.scores.setup('ranked', {writer: 'server'})` (or a pattern for boards
|
|
218
|
+
to come, `'daily-*'`), then the page's submits
|
|
219
|
+
to it are refused (their answer's `code` is `'server-only'`, HTTP 403) and the server submits each run it checked; the page reads the board as before
|
|
220
|
+
([docs/SERVER.md](docs/SERVER.md)).
|
|
221
|
+
|
|
222
|
+
Where the sign-in comes from:
|
|
223
|
+
|
|
224
|
+
- **In games.fun's play page**, the page signs the player in and hands the game a token.
|
|
225
|
+
- **On the game's own link, and inside a post on X**, a small window on games.fun signs in; when the browser keeps it
|
|
226
|
+
shut (a phone, X's app), the game is given a code to show ("go to xgames.fun/go and enter ABCD-EFGH") that the player
|
|
227
|
+
types on any device. The code comes with `window`: show it plainly only when `window` is false. When a window did
|
|
228
|
+
open, the player is signing in there (X's login takes a while, and its page hides from the game whether that window
|
|
229
|
+
is still open), so show the code at most as a quiet line for someone who lost the window, never over the game.
|
|
230
|
+
- **On this machine** (localhost, `xcore check`), a local player and the browser's own storage stand in, so the game
|
|
231
|
+
can be made and tested before it is published. Under `xcore dev`, a local sign-in (one player a tab) and the
|
|
232
|
+
platform answer as games.fun does, and the game's `server/` accepts its token.
|
|
233
|
+
|
|
234
|
+
The token lasts an hour and stays in the page's memory. The game's `server/` checks it with `env.GF.verifyPlayer`; a
|
|
235
|
+
backend of your own checks it with any JWT library against `https://<slug>-play.xgames.fun/player/v1/key` (issuer
|
|
236
|
+
`xgames.fun`, audience `game:<slug>`).
|
|
237
|
+
|
|
238
|
+
The page calls the game's own server with `GamesFun.api(path, init)` (Godot `GamesFun.api(path, init)`, Unity
|
|
239
|
+
`GamesFun.Api(path, initJson)`): `path` is relative to the game's `api/` (`'finish'` is `api/finish`, at the
|
|
240
|
+
published origin and under a folder alike), `init` is `fetch`'s (an object `body` goes as JSON), and a signed-in
|
|
241
|
+
player's token goes with it as `Authorization: Bearer …`, which the server checks with `env.GF.verifyPlayer` before
|
|
242
|
+
it trusts who is asking. It answers `{data, error, status}` like every other call.
|
|
243
|
+
|
|
244
|
+
### A database of its own: `xcore add database`
|
|
245
|
+
|
|
246
|
+
`xcore add database <folder>` adds sign-in, and a database the game owns: server code (`server/database.js`, one
|
|
247
|
+
Durable Object with SQLite storage, [docs/SERVER.md](docs/SERVER.md)) that publish puts on games.fun with the game, at
|
|
248
|
+
its own `api/db/`. Nothing to sign up for, no keys, and it is kept between publishes. It holds documents (any JSON up
|
|
249
|
+
to 64 KB) in collections:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
const level = (await GamesFun.db.add('levels', { name: 'Moon', tiles })).data; // {id, owner, value, updatedAt}
|
|
253
|
+
const mine = (await GamesFun.db.list('levels', { owner: 'me' })).data;
|
|
254
|
+
await GamesFun.db.put('levels', level.id, { name: 'Moon 2', tiles });
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Anyone reads; a signed-in player writes only their own documents. `RULES` at the top of `server/database.js` changes
|
|
258
|
+
that per collection (`read: 'owner'` keeps a collection private to each player; `write: 'server'` lets only the game's
|
|
259
|
+
own server code write, through `dataOf(env)`, for anything players must not set themselves). A game with a server of
|
|
260
|
+
its own keeps it: the database is added beside it, with the two lines to route it. On this machine the page keeps
|
|
261
|
+
documents in the browser instead. Its calls count toward the game server's requests (600 a minute from one player, 6,000 a
|
|
262
|
+
minute shared by all its players): read and write when something happens, not every frame.
|
|
263
|
+
|
|
264
|
+
### Multiplayer: `xcore add multiplayer`
|
|
265
|
+
|
|
266
|
+
`xcore add multiplayer <folder>` adds sign-in, and rooms the game owns: server code (`server/rooms.js`, one Durable
|
|
267
|
+
Object a room) that publish puts on games.fun with the game, at its own `api/rooms/<CODE>`. A room is a relay with a
|
|
268
|
+
host: the first player in is the host (id 1, as Godot's server), every other player gets an id of their own, and a
|
|
269
|
+
message goes to one player, to everyone else, or to everyone but one. The room never reads a message: the host's game
|
|
270
|
+
decides what happens. When the host leaves, the player who joined next becomes the host (a Godot game instead sees
|
|
271
|
+
its server leave, `server_disconnected`, as over ENet).
|
|
272
|
+
|
|
273
|
+
| The game | How it joins |
|
|
274
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
275
|
+
| A Godot 4 project | `multiplayer.multiplayer_peer = await GamesFun.join_room(code)`, in place of an `ENetMultiplayerPeer`: RPCs, `MultiplayerSpawner` and `MultiplayerSynchronizer` work unchanged (all reliable and ordered). In the editor the same call makes a local ENet game, for Run Multiple Instances |
|
|
276
|
+
| A Unity project | `var room = await GamesFun.JoinRoom(code)`: `room.Send(json, to)`, `room.SendBytes(bytes, to)`, `OnMessage`, `OnBytes`, `OnJoin`, `OnLeave`, `OnHost` |
|
|
277
|
+
| A web game | `const { data: room } = await GamesFun.rooms.join(code)`: `room.send(data, to)`, `room.sendBytes(bytes, to)`, `room.on('message', …)` |
|
|
278
|
+
|
|
279
|
+
A room's code is 4 to 8 letters and digits (`rooms.code()` makes one). By default the first to a code opens the room
|
|
280
|
+
and anyone after joins it; a Host button passes `host: true` (refused when someone is in the room) and a Join button
|
|
281
|
+
`host: false` (refused when no one is, as joining an address no one serves fails over ENet). `rooms.link(code)` is the link to share:
|
|
282
|
+
`https://xgames.fun/play/<slug>?room=<CODE>`, which opens the game in that room, on games.fun and inside a post on X
|
|
283
|
+
(its card frames the room), and `rooms.fromLink()` reads it back. `ROOM` at the top of `server/rooms.js` sets the most
|
|
284
|
+
players (16), whether only signed-in players may join (their games.fun id and X handle come with them), and the
|
|
285
|
+
message limits (120 messages a second a player; past it messages are dropped and the game told, through the room's
|
|
286
|
+
`error` event, Godot's `room_error` signal and Unity's `OnError`). The page client sends each frame's messages as one,
|
|
287
|
+
so a game at any frame rate sends at most about 60 a second, however many objects it syncs. A player's connection
|
|
288
|
+
counts once toward the game server's requests (600 a minute from one player, 6,000 a game), however many messages it carries.
|
|
289
|
+
|
|
290
|
+
Porting a Godot game from ENet: `create_server` / `create_client` become `join_room` (with `host: true` / `false` for
|
|
291
|
+
Host and Join buttons), a variable typed `ENetMultiplayerPeer` becomes `MultiplayerPeer`, `create_server`'s
|
|
292
|
+
`max_clients` is `ROOM.maxPlayers` (for every room of the game), and a packet sent unreliable arrives reliable. The
|
|
293
|
+
peer leaves its room on `peer.close()` or when nothing holds it any more. The host's game relays every client's
|
|
294
|
+
packets to the others, as Godot's server does over ENet. On this machine, tabs of the same browser share a room, so two tabs play together before the game
|
|
295
|
+
is published; under `xcore dev` they play through the room server itself, as on games.fun. A netcode library with a server of its own (Netcode for GameObjects, Mirror, a Node server) does not
|
|
296
|
+
run on games.fun as it is: its game logic moves to the host's game, or its server runs on a host of your own (below).
|
|
297
|
+
|
|
298
|
+
**When the host must not decide.** In a room the host's game is the referee, and the host's player can read anything
|
|
299
|
+
their page holds. That is fine for most games played with friends, but not for a hidden role or word (the host would
|
|
300
|
+
see the Impostor), or a result that pays tokens or prizes (the host could claim any winner). Such a game keeps the
|
|
301
|
+
match in a Durable Object of its own, as a Node or Socket.IO server would: the room's state, the hidden parts and the
|
|
302
|
+
result live there, players send it their moves over a WebSocket, and it sends each player only what that player may
|
|
303
|
+
see ([docs/SERVER.md](docs/SERVER.md), "A room over a WebSocket"). It sits beside `server/rooms.js` (or in place of
|
|
304
|
+
it), and the server that decided the result is the one that pays it (`env.GF.loot.report` for prizes by place, or a
|
|
305
|
+
voucher it signs with its own key: `xcore add vouchers`).
|
|
306
|
+
|
|
307
|
+
A database you already have (Postgres, MySQL, a hosted one) stays where it is, behind a backend of your own (below):
|
|
308
|
+
the page never holds its address or password.
|
|
309
|
+
|
|
310
|
+
### A backend
|
|
311
|
+
|
|
312
|
+
Three ways, any of them, or none:
|
|
313
|
+
|
|
314
|
+
- **One you already run**: `xcore backend use custom <folder> --url https://api.my-game.example [--origin wss://live.my-game.example]`
|
|
315
|
+
writes `backend.json` with the URL's origin and its `wss://` twin, plus any `--origin` you add (another host, a CDN for its pictures). No client is
|
|
316
|
+
copied: the game's own code calls it. The backend answers CORS for the game's origin (the published link's, and
|
|
317
|
+
`http://127.0.0.1:<port>` in the kit's dev server). No key, unless one made to be public.
|
|
318
|
+
- **Supabase or Railway**: needs the game-making kit (`@xgamesfun/kit`) installed. `xcore backend connect supabase|railway`
|
|
319
|
+
hands the command to the kit, which connects one for you. Without the kit, set one up yourself and declare it with
|
|
320
|
+
`backend use custom`.
|
|
321
|
+
- **A server games.fun runs**: `server/index.js` and `gamesfun.json` → `server` (the `gf-server-v1` contract, [docs/SERVER.md](docs/SERVER.md)): a Worker on the
|
|
322
|
+
game's `/api/*`, with Durable Objects for rooms and state, `env.GF` for sign-in, saves and scores, and your own
|
|
323
|
+
secrets as `env.<NAME>` (`xcore secret set <NAME>`: kept on the server's host only, never shown again, never given
|
|
324
|
+
to the page). `xcore dev` runs it under workerd at `/game/api/*`, with secrets from `.dev.vars`; publish bundles it.
|
|
325
|
+
This is the way to keep server logic without a host of your own.
|
|
326
|
+
|
|
327
|
+
A brought game with a build keeps `backend.json` in its `static` folder, so the build publishes it.
|
|
328
|
+
|
|
329
|
+
## 4. An economy (web3)
|
|
330
|
+
|
|
331
|
+
> **Read [Disclaimer and responsible use](#disclaimer-and-responsible-use) before you build an economy.** Loot boxes,
|
|
332
|
+
> raffles, lotteries, prize pools and tokens are regulated in many places, and on-chain transactions cannot be undone.
|
|
333
|
+
|
|
334
|
+
A game's economy runs on loot, one shared Solana program (devnet only until it is audited): shops, loot boxes, raffles
|
|
335
|
+
and lotteries, prize pools, staking, crafting, perks for holders, rewards a player claims. Its tools come with this
|
|
336
|
+
package (`@xgamesfun/solana`). `xcore loot connect` adds them to Claude Code or Codex: the `xsolana_*` MCP tools
|
|
337
|
+
and the `xsolana-economy` skill, which turn a one-line idea into a checked economy. `xsolana_check` refuses what would
|
|
338
|
+
fail or lose money on chain (a prize a pool cannot pay, a pool nothing fills, a farm that empties a lottery, a claim with
|
|
339
|
+
no limit), `xsolana_simulate` shows where the money goes day by day, and `xsolana_cost` what going live costs.
|
|
340
|
+
|
|
341
|
+
`xcore loot add <folder>` says where the economy goes (`xsolana.json` in the `static` folder) and, once it is
|
|
342
|
+
there, copies the page client beside it (`gf/loot/`, a copy: never edit it; publish keeps it current). The page loads
|
|
343
|
+
it at run time with a page-relative URL, so a bundler leaves it alone:
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
const { solanaApp, walletStandardSigner } = await import(
|
|
347
|
+
/* @vite-ignore */ new URL('gf/loot/play/index.js', document.baseURI).href
|
|
348
|
+
);
|
|
349
|
+
const { solanaWallets, connectWallet, PAGE_RPC } = await import(
|
|
350
|
+
/* @vite-ignore */ new URL('gf/loot/wallets.js', document.baseURI).href
|
|
351
|
+
);
|
|
352
|
+
const spec = await (await fetch('xsolana.json')).json();
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`solanaApp` takes `rpcUrl: PAGE_RPC`, the page's own `/loot/v1/rpc`. `gf/loot/wallets.js` gives the page the
|
|
356
|
+
player's wallet through the Wallet Standard, with no package:
|
|
357
|
+
|
|
358
|
+
| Export | |
|
|
359
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
360
|
+
| `solanaWallets({ waitMs = 300 })` | → the wallets in this browser that can sign Solana devnet transactions (an empty list when there are none: say so, and keep the game playable) |
|
|
361
|
+
| `connectWallet(wallet)` | → the player's account; the wallet shows its own prompt, and it throws if they say no |
|
|
362
|
+
| `PAGE_RPC` | the page's own `/loot/v1/rpc` (games.fun's relay once published) |
|
|
363
|
+
|
|
364
|
+
`walletStandardSigner(wallet, account)` from `gf/loot/play/index.js` turns them into the signer `solanaApp` takes.
|
|
365
|
+
|
|
366
|
+
**From Godot, Unity or any page with `gamesfun.js`** (`xcore add signin`), `GamesFun.loot` does all of that with JSON in
|
|
367
|
+
and out (big numbers as text), so an engine calls it like the rest of games.fun:
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
await GamesFun.loot.ready(); // {live, project, wallets: [names], spec}
|
|
371
|
+
await GamesFun.loot.connect(); // {address, wallet}: the wallet asks the player
|
|
372
|
+
await GamesFun.loot.link(); // {wallet}: it signs that it is this player's (a game server that pays needs it)
|
|
373
|
+
const shop = await GamesFun.loot.call('shop'); // any method of loot's page client, with its arguments
|
|
374
|
+
await GamesFun.loot.call('buy', 'sword', { listing: shop.data[0] }); // a listing goes back as it came
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Godot: `await GamesFun.loot_ready()`, `loot_connect()`, `loot_link()`, `loot_call("shop")`, `loot_call("buy", ["sword", {"listing": item}])`.
|
|
378
|
+
Unity: `GamesFun.LootReady()`, `LootConnect()`, `LootLink()`, `LootCall("shop")`, `LootCall("buy", "\"sword\", {\"listing\": …}")`.
|
|
379
|
+
In a Godot or Unity project, `xcore loot add` keeps `xsolana.json` and `gf/loot/` out of the engine's export, in the
|
|
380
|
+
project's own `gamesfun/` folder (with a `.gdignore`, so Godot leaves it alone): `gamesfun.json` → `page.static`.
|
|
381
|
+
Publish, `xcore check` and `xcore dev` lay that folder over the export, so you export as before, and the file that
|
|
382
|
+
holds the addresses the economy went live at stays in the project (keep it in git).
|
|
383
|
+
|
|
384
|
+
`@xgamesfun/solana`'s own examples use a public devnet RPC and `node_modules/` paths; on games.fun use `PAGE_RPC` and
|
|
385
|
+
`gf/loot/` as above. Where its help says `xsolana <command>`, run `xcore loot <command>`. `xcore loot check|simulate|cost|explain`
|
|
386
|
+
runs the tools from a terminal. Going live is the person's own step, approved in their own wallet: `xcore loot
|
|
387
|
+
approve` in the folder with `xsolana.json`.
|
|
388
|
+
|
|
389
|
+
**When loot is upgraded.** This package pins the tools for one build of loot (0.25.0). loot is upgraded in place on
|
|
390
|
+
devnet, and no version so far has carried accounts over: an economy that went live on an earlier build goes live
|
|
391
|
+
again (`xcore loot approve`), and the next publish brings `gf/loot/` up to the installed package. **Nothing runs by
|
|
392
|
+
itself** on chain: the page client sends what a player needs (settles, refunds, claims, a draw that is due); a
|
|
393
|
+
schedule's runs are the project's own steps, and a run nobody sends is missed.
|
|
394
|
+
|
|
395
|
+
**What a page alone can pay safely.** A game that runs only in the browser can't prove what a player did there: anyone
|
|
396
|
+
can change its score in the console. So from the page alone, offer what the player pays for or the chain decides (a
|
|
397
|
+
shop, boxes, raffles, lotteries, staking, crafting, perks for holders). A reward for a result (a score, a win, a level)
|
|
398
|
+
is a grant claimed with a voucher, and the voucher key must never be in the page (publish refuses a key it finds): the
|
|
399
|
+
game's server checks the result, and then one of two signers vouches for it.
|
|
400
|
+
|
|
401
|
+
**Paying a result: the keeper or your own key.** When the server already decides who won and the prize is fixed by
|
|
402
|
+
place (a room's winner, a match's top three), it reports the winners with `env.GF.loot.report` and games.fun's keeper
|
|
403
|
+
signs, with no key for you to keep. When the amount depends on the result (paid by score), when the server replays or
|
|
404
|
+
bounds runs before paying, when there are entry fees or a prize pool, or when results come faster than 120 an hour,
|
|
405
|
+
give the server its own key: `xcore add vouchers` writes `server/vouchers.js` (its `PAY.check` is what your server
|
|
406
|
+
checks) and a test key in `.dev.vars` for `xcore dev`. In order: `xcore publish`, `xcore secret set VOUCHER_SEED
|
|
407
|
+
--new-key` (the live key, made on games.fun and never on your machine: it prints the address), that address into the
|
|
408
|
+
grant in `xsolana.json`, `xcore loot approve`, `xcore publish`. A voucher pays only a wallet the player proved is
|
|
409
|
+
theirs (signed in with it, or linked with `gf.loot.link()`), and since a wallet is free, pay rewards worth farming
|
|
410
|
+
only to players signed in with X (`VOUCHERS.payOnly`). A grant names up to three keys, so the keeper and your own can
|
|
411
|
+
pay the same grant while you move from one to the other.
|
|
412
|
+
[docs/SERVER.md](docs/SERVER.md), "Paying a result", has both, side by side.
|
|
413
|
+
|
|
414
|
+
## 5. Publish
|
|
415
|
+
|
|
416
|
+
`xcore login` signs this computer in (your browser signs in to games.fun and hands it a publish token for 30
|
|
417
|
+
days; xcore never sees your password). `xcore publish <folder>` then builds the game, checks it (the page
|
|
418
|
+
check on exactly the files built), and uploads its published folder with its `server/` bundle:
|
|
419
|
+
|
|
420
|
+
1. **The files**: the published folder, without hidden files, `evidence/`, `node_modules/`, `server/`, `supabase/` and the makers' notes at its top (`README.md`, `NOTES.md`, `LAUNCH.md` and the like). A `.env` file or a link
|
|
421
|
+
stops it. Two small records go up beside them, as games.fun keeps them with every version:
|
|
422
|
+
`foundation.lock.json` (which tool published it) and `adaptation-plan.json` (the game's title and a one-line
|
|
423
|
+
description). Like every published file, they can be read at the game's link.
|
|
424
|
+
2. **No secrets**: every text file is scanned for anything that looks like a key (private keys, cloud, payment and AI
|
|
425
|
+
keys, a Supabase service-role key, a database address with a password). games.fun scans again on its side.
|
|
426
|
+
3. **Its backend**: `backend.json`'s origins are the only other sites the published page may reach.
|
|
427
|
+
4. **Its server code** (`server/`, a Worker under the `gf-server-v1` contract): bundled into one file and checked (it
|
|
428
|
+
builds, no code made from text, no Node built-in a Worker lacks, no secrets, its Durable Objects exported, at most
|
|
429
|
+
5 MB). `server/` and `gamesfun.json` are never public files.
|
|
430
|
+
5. **The check**: a game whose check fails is saved as a private draft (a version in your games.fun Studio, with no
|
|
431
|
+
public link), and you are told which checks failed. A pass is remembered for exactly these files.
|
|
432
|
+
6. **Upload**: only the files games.fun does not have yet.
|
|
433
|
+
7. **The link**: unlisted, and the same through every update. Publishing the folder again makes the next version. The
|
|
434
|
+
play host caches files for up to 30 seconds, so an update can take that long to show at the link, and for that
|
|
435
|
+
time a visitor can get some files of the new version and some of the old. Files the new version no longer has
|
|
436
|
+
are gone at once: a page someone already has open that loads a file later (a lazily loaded chunk with a hashed
|
|
437
|
+
name) can fail to find it, so have the game reload when that happens. A game installed as an app (a web
|
|
438
|
+
manifest and a service worker) installs from its own link, `https://<slug>-play.xgames.fun/`.
|
|
439
|
+
|
|
440
|
+
`publish --check` runs steps 1 to 4 above without publishing (`--check --verify` adds the check and remembers a pass), `--status`
|
|
441
|
+
shows the link and the account, `--draft` saves a private version, `--new` makes a new game with a new link.
|
|
442
|
+
|
|
443
|
+
**Where the token is kept**: the macOS keychain; on Linux the Secret Service (`secret-tool`) when it runs, else a file
|
|
444
|
+
only you can read under `~/.config/gamesfun/secrets/`; on Windows a file encrypted for your user (DPAPI) under
|
|
445
|
+
`%APPDATA%\gamesfun\secrets\`. `GAMESFUN_CREDENTIALS=file` always uses the file. Without a browser (SSH, a
|
|
446
|
+
container), `xcore login --code` prints a link to open anywhere and asks for the code it shows.
|
|
447
|
+
|
|
448
|
+
**From GitHub Actions, no key stored**: the job's own OIDC token signs the publish in place of a login. games.fun
|
|
449
|
+
accepts it for the repository and branch you linked to the game once in Studio.
|
|
450
|
+
|
|
451
|
+
```yaml
|
|
452
|
+
# .github/workflows/publish.yml
|
|
453
|
+
on: { push: { branches: [main] } }
|
|
454
|
+
permissions: { id-token: write, contents: read }
|
|
455
|
+
jobs:
|
|
456
|
+
publish:
|
|
457
|
+
runs-on: ubuntu-latest # has Chrome, which the check needs
|
|
458
|
+
steps:
|
|
459
|
+
- uses: actions/checkout@v4
|
|
460
|
+
- uses: actions/setup-node@v4
|
|
461
|
+
with: { node-version: 24 }
|
|
462
|
+
- run: npx -y @xgamesfun/xcore@<version> publish ./my-game
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Limits**: one game 3,000 files, 100 MiB (one file may be all of it); one account 30 publishes and 1 GB of new files a day, 100
|
|
466
|
+
games. `GAMESFUN_STUDIO=<origin>` publishes to another games.fun (a Studio on your own machine while developing it).
|
|
467
|
+
|
|
468
|
+
### Its plan: `xcore plan`
|
|
469
|
+
|
|
470
|
+
Hosting the page is free. What the game's server, saves and scores may use is the game's plan:
|
|
471
|
+
|
|
472
|
+
| | Free | Creator | Scale |
|
|
473
|
+
| -------------------- | ------------------------------------------------------------------ | --------------------------------------------------------- | ----------------------- |
|
|
474
|
+
| For | every game, by default | a game in one of your paid game slots (per game, a month) | granted by games.fun |
|
|
475
|
+
| Server | 50 ms of CPU a request; 600 requests a minute, 120 from one player | 200 ms; 6,000 a minute, 600 from one player | 1,000 ms; 30,000, 1,200 |
|
|
476
|
+
| Saves and scores | 10 MB of saves; 8 boards of 10,000 entries | 1 GB; 16 boards of 1,000,000 | 10 GB; 16 of 1,000,000 |
|
|
477
|
+
| Use included a month | $1 | $10 a slot, shared by your Creator games | as granted |
|
|
478
|
+
| Sites that embed it | 3 (`xcore embed`) | 10 | 25 |
|
|
479
|
+
| Domains of your own | none | 1 (`xcore domain`) | 5 |
|
|
480
|
+
|
|
481
|
+
Past what is included, a game is **capped** until the month ends: its server answers at most 60 requests a minute (20
|
|
482
|
+
from one player) and saves and scores slow down, but nothing is lost and the game keeps working. Slots are $15 for 30
|
|
483
|
+
days, prepaid in SOL or USDC in games.fun Studio (Plans; no automatic renewal, 7 days' grace after the period ends).
|
|
484
|
+
Usage credit, bought the same way, lets Creator games go past what their slots include, at twice what games.fun pays,
|
|
485
|
+
instead of slowing down. A slot moves between your games at any time
|
|
486
|
+
(`xcore plan deactivate <one>`, `xcore plan activate <another>`) without changing the bill. `xcore plan [folder]`
|
|
487
|
+
shows a game's plan, its limits and this month's use; `--all` your slots and every game. A game's server sees a 429
|
|
488
|
+
with `scope` (`"player"` or `"game"`) and `plan` when it goes past a rate.
|
|
489
|
+
|
|
490
|
+
### Your own site and your own domain: `xcore embed`, `xcore domain`
|
|
491
|
+
|
|
492
|
+
A published game plays at its xgames.fun link and inside a post on X. Your own website can show it too, and on
|
|
493
|
+
Creator and Scale it can live at an address of yours:
|
|
494
|
+
|
|
495
|
+
```sh
|
|
496
|
+
xcore embed add https://mygame.com # mygame.com may frame the game; prints the <iframe> for its page
|
|
497
|
+
xcore domain add play.mygame.com # Creator or Scale: prints the DNS records to add at your domain's DNS
|
|
498
|
+
xcore domain check play.mygame.com # live once the records are right (games.fun also checks by itself)
|
|
499
|
+
xcore domain # where the game plays, the sites that embed it, its domains
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
- **Embedding** (every plan: 3 sites, 10, 25): a site is its origin (`https://mygame.com`; `http://localhost:<port>`
|
|
503
|
+
while you build it). It applies within about a minute, with no publish. Framed on your site, the game signs players
|
|
504
|
+
in by itself (a small games.fun window, or a code), as in a post on X: only games.fun's own page can hand it a
|
|
505
|
+
sign-in, so a page that embeds it can't sign players in for it or take their sign-in.
|
|
506
|
+
- **A domain of your own** (Creator 1, Scale 5): `domain add` prints two records, a CNAME pointing the domain at
|
|
507
|
+
games.fun and a TXT proving you own it. Once they are right the game is served there with its certificate, and
|
|
508
|
+
sign-in, saves, scores, rooms, the server and the economy work there as on its own link. Sign-in still opens a
|
|
509
|
+
small games.fun window, Studio stays on games.fun, and a post on X still uses the xgames.fun link. A bare domain
|
|
510
|
+
(`mygame.com`) needs your DNS provider's CNAME flattening or ALIAS record; else use `www.` or `play.` and redirect.
|
|
511
|
+
The page must load `gamesfun.js` 1.2.0 or later (it asks the play edge which game it is on a host of its own): an
|
|
512
|
+
older copy can't sign players in there, so `domain add` says when to run `xcore add signin` and publish again. A
|
|
513
|
+
game whose plan drops keeps its domain paused: it sends players to the xgames.fun link until there is room again.
|
|
514
|
+
- Custom domains need games.fun's operator to have set them up (Cloudflare for SaaS): until then `xcore domain` says so.
|
|
515
|
+
|
|
516
|
+
## What is not supported, and why
|
|
517
|
+
|
|
518
|
+
- **Server-side rendering** (a page made per request by Node): the host serves files only. Make the page static and
|
|
519
|
+
put what needs a server in `server/` (a Worker) or a backend of your own.
|
|
520
|
+
- **A server in Python, Ruby, Go or another language**: games.fun runs only `server/` (a Worker, JavaScript or
|
|
521
|
+
WebAssembly). Such a server runs on a host of your own, declared with `backend use custom`; or its logic moves into
|
|
522
|
+
`server/`, or into the page itself (WebAssembly is allowed: Pyodide runs Python in the browser). Each is a change to
|
|
523
|
+
how the game is built: the person who made it decides.
|
|
524
|
+
- **A Node server as the backend on games.fun**: `server/` is a Worker, not Node (Node built-ins only with
|
|
525
|
+
`nodejs_compat`, and no `fs`, `child_process` or listening sockets). A Node backend runs on a host of your own,
|
|
526
|
+
declared with `backend use custom`.
|
|
527
|
+
- **Scripts, style sheets and fonts from a CDN**: the page loads code and styles from its own files only, so a copy goes
|
|
528
|
+
in the game (or the package is bundled).
|
|
529
|
+
- **A fallback to `index.html` for every path**: deep links of a client-side router reload to "not found"; use a hash
|
|
530
|
+
router or links that start at the entry page.
|
|
531
|
+
- **Keys that are public by design but look like secrets**, other than the two the scan knows (a Supabase anon or
|
|
532
|
+
publishable key, and a Firebase web config's `apiKey`): the host's scan refuses them like any other key. Fetch such a
|
|
533
|
+
configuration at run time from your backend.
|
|
534
|
+
- **More than 8 backend origins, IP addresses, localhost, wildcards**: the host opens the page's policy to named
|
|
535
|
+
public origins only.
|
|
536
|
+
|
|
537
|
+
## Documentation
|
|
538
|
+
|
|
539
|
+
- [docs/SERVER.md](docs/SERVER.md): a game's own server code (`server/`, the `gf-server-v1` contract)
|
|
540
|
+
- [skills/bring-your-own-game/SKILL.md](skills/bring-your-own-game/SKILL.md): the same steps, for a coding agent
|
|
541
|
+
- [CHANGELOG.md](https://github.com/XGamesFun/xcore/blob/main/CHANGELOG.md), [CONTRIBUTING.md](https://github.com/XGamesFun/xcore/blob/main/CONTRIBUTING.md), [SECURITY.md](https://github.com/XGamesFun/xcore/blob/main/SECURITY.md)
|
|
542
|
+
|
|
543
|
+
## Disclaimer and responsible use
|
|
544
|
+
|
|
545
|
+
**Use this software at your own risk.** It is provided "as is", without warranty of any kind, express or implied, as
|
|
546
|
+
set out in the [LICENSE](LICENSE). The authors and contributors are not liable for any loss or damage arising from its
|
|
547
|
+
use, including lost funds, lost data, account suspensions or legal claims.
|
|
548
|
+
|
|
549
|
+
**You are responsible for making sure everything you build, publish and run with it is legal** in every jurisdiction
|
|
550
|
+
where you operate and where your players are. Depending on what your game does, that may include laws and rules on:
|
|
551
|
+
|
|
552
|
+
- **Gambling, lotteries, sweepstakes and prize promotions.** Loot boxes, raffles, lotteries, coin flips, prize pools
|
|
553
|
+
and paid chances to win are restricted or banned in many countries and states.
|
|
554
|
+
- **Digital assets and finance.** Tokens, NFTs, staking and rewards may be treated as securities, commodities,
|
|
555
|
+
payment instruments or money transmission, and may require licensing, KYC/AML checks or sanctions screening.
|
|
556
|
+
- **Consumer protection, age limits and advertising**, including rules on minors and on disclosing odds.
|
|
557
|
+
- **Tax** on sales, prizes and digital-asset income, for you and for your players.
|
|
558
|
+
- **Privacy and data protection** (such as GDPR and CCPA) for any player data your game or its backend collects.
|
|
559
|
+
- **Intellectual property.** Publish only code, art, music and names that you own or have a license to use.
|
|
560
|
+
|
|
561
|
+
Nothing in this repository, its documentation or its output is legal, financial, investment or tax advice. Get advice
|
|
562
|
+
from a qualified professional before you offer anything of real value.
|
|
563
|
+
|
|
564
|
+
**On-chain features.** The loot program runs on Solana **devnet** and **has not been audited**: do not use it with
|
|
565
|
+
real value. Blockchain transactions are public and irreversible. You alone control your wallet and keys. Never put a
|
|
566
|
+
private key, seed phrase or secret in a game, and approve only transactions you understand.
|
|
567
|
+
|
|
568
|
+
**Third-party services.** games.fun, Cloudflare, Supabase, Railway, GitHub, npm, Solana RPC providers and wallets are
|
|
569
|
+
run by others under their own terms and policies, which you must follow. Their availability, fees and rules can change
|
|
570
|
+
without notice.
|
|
571
|
+
|
|
572
|
+
## License
|
|
573
|
+
|
|
574
|
+
[MIT](LICENSE) © games.fun
|