@xgamesfun/xcore 0.1.0 → 0.2.1
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 +1 -1
- package/README.md +183 -84
- package/bin/xcore.mjs +13 -8
- package/commands/add.mjs +11 -8
- package/commands/adopt.mjs +9 -7
- package/commands/backend.mjs +2 -2
- package/commands/check.mjs +4 -4
- package/commands/db.mjs +218 -0
- package/commands/dev.mjs +8 -6
- package/commands/domain.mjs +8 -8
- package/commands/embed.mjs +3 -3
- package/commands/files.mjs +106 -0
- package/commands/login.mjs +9 -17
- package/commands/logout.mjs +15 -4
- package/commands/loot.mjs +2 -2
- package/commands/plan.mjs +22 -19
- package/commands/publish.mjs +54 -21
- package/commands/secret.mjs +6 -6
- package/docs/SERVER.md +218 -18
- package/lib/add.mjs +265 -40
- package/lib/browser.mjs +11 -2
- package/lib/check.mjs +10 -9
- package/lib/cli.mjs +24 -9
- package/lib/client/gamesfun.js +242 -122
- package/lib/client/godot/gamesfun.gd +17 -11
- package/lib/client/godot/gamesfun_peer.gd +2 -2
- package/lib/client/server/0001_init.sql +10 -0
- package/lib/client/server/database.js +271 -109
- package/lib/client/server/gf-db.js +55 -0
- package/lib/client/server/gf-docs.js +56 -0
- package/lib/client/server/rooms.js +16 -9
- package/lib/client/server/rpc.js +7 -7
- package/lib/client/server/stock.js +37 -0
- package/lib/client/server/vouchers.js +127 -46
- package/lib/client/unity/GamesFun.cs +15 -12
- package/lib/client/unity/GamesFun.jslib +1 -1
- package/lib/config-dir.mjs +1 -1
- package/lib/credentials.mjs +10 -5
- package/lib/db.mjs +355 -0
- package/lib/dev/d1.mjs +486 -0
- package/lib/dev/files.mjs +247 -0
- package/lib/dev/gf-local.js +69 -3
- package/lib/dev/index.mjs +68 -20
- package/lib/dev/limits.mjs +2 -2
- package/lib/dev/platform.mjs +111 -12
- package/lib/dev/server.mjs +54 -5
- package/lib/dev/tools.mjs +71 -18
- package/lib/files.mjs +208 -0
- package/lib/open.mjs +1 -1
- package/lib/play-policy.mjs +14 -5
- package/lib/publish/adopt.mjs +73 -33
- package/lib/publish/client.mjs +130 -20
- package/lib/publish/gate.mjs +4 -2
- package/lib/publish/hosting.mjs +5 -5
- package/lib/publish/index.mjs +162 -40
- package/lib/publish/oidc.mjs +26 -5
- package/lib/publish/page.mjs +1 -1
- package/lib/publish/plans.mjs +19 -12
- package/lib/publish/scan.mjs +78 -102
- package/lib/publish/secrets.mjs +4 -5
- package/lib/publish/site.mjs +9 -10
- package/lib/publish/tree.mjs +8 -8
- package/lib/secret-patterns.mjs +53 -16
- package/lib/secret-store.mjs +2 -2
- package/lib/server/bundle.mjs +113 -13
- package/lib/server/contract.mjs +440 -16
- package/lib/server/data-calls.mjs +136 -0
- package/lib/server/secrets.mjs +9 -7
- package/lib/solana-rpc.mjs +5 -5
- package/lib/state-folder.mjs +23 -0
- package/package.json +4 -4
- package/skills/bring-your-own-game/SKILL.md +10 -9
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The server's own calls to the older database (server/database.js: dataOf(env)) that 0.2.0 reads differently from
|
|
2
|
+
// 0.1.0. In 0.1.0 a document was found by its collection and id alone, whoever wrote it; now every player has a space
|
|
3
|
+
// of their own and dataOf(env) without an owner is the server's: a get by id alone answers 404 for a player's
|
|
4
|
+
// document, and a put, add or delete writes the server's own copy, which the player never sees. A list without an
|
|
5
|
+
// owner is everyone's, as it was; its `offset` is refused. Read where `xcore add database` brings a 0.1.0
|
|
6
|
+
// database.js up to date, and at publish, so each call is said with the change it needs (CHANGELOG / README
|
|
7
|
+
// "Upgrading from 0.1.0").
|
|
8
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
/** Comments and strings blanked (same length), so a word in them is not code. */
|
|
12
|
+
export function codeOnly(src) {
|
|
13
|
+
// a string keeps its quotes (so `import('./x.js')` still reads as a literal path), its text becomes spaces
|
|
14
|
+
return src.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"/g, m =>
|
|
15
|
+
m[0] === '/' ? m.replace(/[^\n]/g, ' ') : m[0] + ' '.repeat(m.length - 2) + m[0],
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Each call's options: the argument after collection, id and value (get(c, id, opts), list(c, opts), …). */
|
|
20
|
+
const OPTS_AT = { get: 2, put: 3, add: 2, delete: 2, list: 1 };
|
|
21
|
+
const CALL = '(get|put|add|list|delete)';
|
|
22
|
+
const lineAt = (text, i) => text.slice(0, i).split('\n').length;
|
|
23
|
+
|
|
24
|
+
/** The arguments of the call whose ( is at `open` in `code`, as [start, end) spans; null if it never closes. */
|
|
25
|
+
function argsAt(code, open) {
|
|
26
|
+
const spans = [];
|
|
27
|
+
let depth = 0,
|
|
28
|
+
start = open + 1;
|
|
29
|
+
for (let i = open; i < code.length; i++) {
|
|
30
|
+
const c = code[i];
|
|
31
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
32
|
+
else if (c === ')' || c === ']' || c === '}') {
|
|
33
|
+
if (--depth === 0) {
|
|
34
|
+
if (code.slice(start, i).trim()) spans.push([start, i]);
|
|
35
|
+
return { spans, end: i + 1 };
|
|
36
|
+
}
|
|
37
|
+
} else if (c === ',' && depth === 1) {
|
|
38
|
+
spans.push([start, i]);
|
|
39
|
+
start = i + 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What one call needs, or null when it says whose space it means (or its options are not written out here). */
|
|
46
|
+
function needs(op, opts) {
|
|
47
|
+
if (op === 'list') {
|
|
48
|
+
if (opts && opts.startsWith('{') && /\boffset\b/.test(opts) && !/\boffset\s*:\s*0\s*[,}]/.test(opts))
|
|
49
|
+
return "offset is refused now: page with the last answer's after (.list(c, {after})), which the answer gives with more";
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (opts !== null && (!opts.startsWith('{') || /\bowner\b|\.\.\./.test(opts))) return null;
|
|
53
|
+
const mean = {
|
|
54
|
+
get: 'reads the server\'s own space (owner "server"), so a document a player wrote answers 404',
|
|
55
|
+
put: "writes the server's own copy, which the player never sees",
|
|
56
|
+
add: "adds to the server's own space, not a player's",
|
|
57
|
+
delete: "deletes the server's own copy; a player's document stays",
|
|
58
|
+
}[op];
|
|
59
|
+
return `without an owner it ${mean}. For a player's document pass {owner: sub} (their id, from env.GF.verifyPlayer); for the server's own, {owner: 'server'}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The calls to dataOf(env) in one file's text that need a change: dataOf(env).get(…) and a name it was kept under
|
|
64
|
+
* (const data = dataOf(env); data.get(…)). → [{file, line, op, fix}]
|
|
65
|
+
*/
|
|
66
|
+
export function dataCallsIn(file, text) {
|
|
67
|
+
const code = codeOnly(text);
|
|
68
|
+
const found = [];
|
|
69
|
+
const look = (re, from) => {
|
|
70
|
+
for (const m of code.matchAll(re)) {
|
|
71
|
+
const open = m.index + m[0].length - 1;
|
|
72
|
+
const args = argsAt(code, open);
|
|
73
|
+
if (!args) continue;
|
|
74
|
+
const op = m[from];
|
|
75
|
+
const at = OPTS_AT[op];
|
|
76
|
+
const opts = args.spans[at] ? text.slice(...args.spans[at]).trim() : null;
|
|
77
|
+
const fix = needs(op, opts);
|
|
78
|
+
if (fix) found.push({ file, line: lineAt(code, m.index), op, fix });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
// dataOf(env).get(…), dataOf(env)?.put(…)
|
|
82
|
+
for (const m of code.matchAll(/(?<![\w$.])dataOf\s*\(/g)) {
|
|
83
|
+
const args = argsAt(code, m.index + m[0].length - 1);
|
|
84
|
+
if (!args) continue;
|
|
85
|
+
const rest = new RegExp(String.raw`^\s*\??\.\s*${CALL}\s*\(`).exec(code.slice(args.end));
|
|
86
|
+
if (!rest) continue;
|
|
87
|
+
const at = args.end + rest[0].length - 1;
|
|
88
|
+
const call = argsAt(code, at);
|
|
89
|
+
if (!call) continue;
|
|
90
|
+
const opts = call.spans[OPTS_AT[rest[1]]] ? text.slice(...call.spans[OPTS_AT[rest[1]]]).trim() : null;
|
|
91
|
+
const fix = needs(rest[1], opts);
|
|
92
|
+
if (fix) found.push({ file, line: lineAt(code, m.index), op: rest[1], fix });
|
|
93
|
+
}
|
|
94
|
+
// const data = dataOf(env); … data.get(…)
|
|
95
|
+
for (const m of code.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?dataOf\s*\(/g))
|
|
96
|
+
look(new RegExp(String.raw`(?<![\w$.])${m[1].replace(/\$/g, '\\$')}\s*\??\.\s*${CALL}\s*\(`, 'g'), 1);
|
|
97
|
+
return found.sort((a, b) => a.line - b.line);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const SOURCE = /\.(m?js|cjs|ts|mts|jsx|tsx)$/;
|
|
101
|
+
/** xcore's own files, and code of others: not the game's calls. */
|
|
102
|
+
const NOT_OURS = rel =>
|
|
103
|
+
rel === 'server/database.js' || rel.startsWith('server/gf-loot/') || rel.split('/').includes('node_modules');
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The calls in the game's server code that need a change: `files`, the paths (relative to `dir`, with /) to read, or
|
|
107
|
+
* every source file in server/. → [{file, line, op, fix}]
|
|
108
|
+
*/
|
|
109
|
+
export async function ownerlessDataCalls(dir, files = null) {
|
|
110
|
+
if (!files) {
|
|
111
|
+
files = [];
|
|
112
|
+
const walk = async rel => {
|
|
113
|
+
let entries;
|
|
114
|
+
try {
|
|
115
|
+
entries = await readdir(path.join(dir, ...rel.split('/')), { withFileTypes: true });
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const e of entries) {
|
|
120
|
+
const r = `${rel}/${e.name}`;
|
|
121
|
+
if (e.isDirectory()) await walk(r);
|
|
122
|
+
else files.push(r);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
await walk('server');
|
|
126
|
+
}
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const rel of files.filter(f => SOURCE.test(f) && !NOT_OURS(f))) {
|
|
129
|
+
const text = await readFile(path.join(dir, ...rel.split('/')), 'utf8').catch(() => null);
|
|
130
|
+
if (text?.includes('dataOf')) out.push(...dataCallsIn(rel, text));
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** One call, as a line a person reads. */
|
|
136
|
+
export const dataCallLine = c => `${c.file}:${c.line} dataOf(env).${c.op}(…): ${c.fix}`;
|
package/lib/server/secrets.mjs
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
// A game server's own secrets (gf-server-v1,
|
|
2
|
-
// The owner sets a name and a value;
|
|
1
|
+
// A game server's own secrets (gf-server-v1, xgames.fun's hosting contract): `xcore secret set|list|delete`.
|
|
2
|
+
// The owner sets a name and a value; xgames.fun hands the value to Cloudflare as a secret on the game's server script
|
|
3
3
|
// and never stores, logs or answers it; the server reads env.<NAME> from the next request. Never the page: a page's
|
|
4
4
|
// files are public. Locally, `xcore dev` gives the server the same names from the game folder's .dev.vars.
|
|
5
|
-
// This file is the rules (shared with xcore dev's .dev.vars); the calls to
|
|
5
|
+
// This file is the rules (shared with xcore dev's .dev.vars); the calls to xgames.fun are ../publish/secrets.mjs.
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** xgames.fun's limits for a game's secrets. */
|
|
8
8
|
export const SECRET_LIMITS = Object.freeze({ perGame: 32, valueBytes: 5120, nameChars: 64 });
|
|
9
9
|
const NAME = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
10
10
|
|
|
11
|
-
/** The name's problem in plain words, or null (
|
|
11
|
+
/** The name's problem in plain words, or null (xgames.fun's rule). `taken`: the server's bindings (rooms, database). */
|
|
12
12
|
export function secretNameProblem(name, taken = []) {
|
|
13
13
|
if (typeof name !== 'string' || !NAME.test(name))
|
|
14
14
|
return 'a secret’s name is 1–64 of A–Z, 0–9 and _, starting with a letter (for example VOUCHER_KEY)';
|
|
15
|
-
if (name === 'GF' || name.startsWith('GF_'))
|
|
16
|
-
|
|
15
|
+
if (name === 'GF' || name.startsWith('GF_'))
|
|
16
|
+
return `${name} is xgames.fun’s own (env.GF and GF_*): pick another name`;
|
|
17
|
+
if (taken.includes(name))
|
|
18
|
+
return `${name} is already one of the server's bindings (its rooms' Durable Objects, or its database)`;
|
|
17
19
|
return null;
|
|
18
20
|
}
|
|
19
21
|
|
package/lib/solana-rpc.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The Solana RPC a game's economy talks to (devnet only, like loot). One setting, XSOLANA_RPC_URL (@xgamesfun/solana's
|
|
2
2
|
// own name for it; LOOT_RPC_URL, its older name, is read too), from the environment or the workspace's .env: unset, it
|
|
3
|
-
// is
|
|
3
|
+
// is xgames.fun's shared relay (the play host's /loot/v1/rpc, which holds xgames.fun's own RPC key on its side); set,
|
|
4
4
|
// it is the person's own devnet RPC (Helius, Triton, QuickNode, a local validator). The shared one is the default
|
|
5
5
|
// for now; a published game brings its own with `xcore add rpc` (server/rpc.js at api/rpc, the URL a secret of its
|
|
6
6
|
// server), which the page client asks first.
|
|
@@ -12,7 +12,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { parseEnv } from 'node:util';
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/** xgames.fun's shared devnet relay (the play host's /loot/v1/rpc). */
|
|
16
16
|
export const SHARED_RPC = 'https://play.xgames.fun/loot/v1/rpc';
|
|
17
17
|
/** The path a game's page calls, on whatever host serves it. */
|
|
18
18
|
export const PAGE_RPC = '/loot/v1/rpc';
|
|
@@ -34,7 +34,7 @@ function setting(env, file) {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* The RPC in use: {url, shared, problem}. `file`: the .env it reads (the folder the command runs in, or GAMESFUN_WORK; the
|
|
37
|
-
* kit passes its workspace's). `shared` is
|
|
37
|
+
* kit passes its workspace's). `shared` is xgames.fun's relay (nothing set). A setting that is mainnet, not a
|
|
38
38
|
* URL, or plain http to anything but this machine is not used: `problem` says why and `url` is the shared relay.
|
|
39
39
|
*/
|
|
40
40
|
export function solanaRpc(env = process.env, { file = path.join(env.GAMESFUN_WORK || process.cwd(), '.env') } = {}) {
|
|
@@ -43,7 +43,7 @@ export function solanaRpc(env = process.env, { file = path.join(env.GAMESFUN_WOR
|
|
|
43
43
|
const refuse = why => ({
|
|
44
44
|
url: SHARED_RPC,
|
|
45
45
|
shared: true,
|
|
46
|
-
problem: `XSOLANA_RPC_URL ${why}; using
|
|
46
|
+
problem: `XSOLANA_RPC_URL ${why}; using xgames.fun's shared devnet RPC instead`,
|
|
47
47
|
});
|
|
48
48
|
let url;
|
|
49
49
|
try {
|
|
@@ -60,7 +60,7 @@ export function solanaRpc(env = process.env, { file = path.join(env.GAMESFUN_WOR
|
|
|
60
60
|
/** The RPC in words, never its URL when it is the person's own (it usually carries their key). */
|
|
61
61
|
export function rpcWords(rpc) {
|
|
62
62
|
if (rpc.shared)
|
|
63
|
-
return `
|
|
63
|
+
return `xgames.fun's shared devnet RPC (${SHARED_RPC}; set XSOLANA_RPC_URL in .env to use your own)${rpc.problem ? `: ${rpc.problem}` : ''}`;
|
|
64
64
|
let host = 'your own';
|
|
65
65
|
try {
|
|
66
66
|
host = new URL(rpc.url).host;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// <game>/.gamesfun/: xcore's own files for one game (the publish link, the local database and files, the dev
|
|
2
|
+
// platform's saves, exports of the players' data). Hidden, so never published; and kept out of git by a .gitignore of
|
|
3
|
+
// its own (`*`), written whenever xcore writes into the folder and finds none, so an export of players' data is not
|
|
4
|
+
// committed by a `git add .`.
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
export const STATE_FOLDER = '.gamesfun';
|
|
10
|
+
export const STATE_GITIGNORE = `# xcore's own files for this game: the publish link, the local database and files, exports of your players' data.
|
|
11
|
+
# Never published, and kept out of git. xcore writes this file again whenever it is missing, so deleting it does not
|
|
12
|
+
# let anything in: change it instead (it is left as you write it). A line such as !publish.json commits that file.
|
|
13
|
+
*
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
/** <dir>/.gamesfun, made with its .gitignore when either is missing. → the folder */
|
|
17
|
+
export async function stateFolder(dir) {
|
|
18
|
+
const folder = path.join(dir, STATE_FOLDER);
|
|
19
|
+
await mkdir(folder, { recursive: true });
|
|
20
|
+
const ignore = path.join(folder, '.gitignore');
|
|
21
|
+
if (!existsSync(ignore)) await writeFile(ignore, STATE_GITIGNORE, { flag: 'wx' }).catch(() => {});
|
|
22
|
+
return folder;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xgamesfun/xcore",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "xgames.fun from the command line: bring a web game you already made, check it as xgames.fun hosts it, give it a backend and a Solana economy, and publish it at a link anyone can play.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"game",
|
|
7
7
|
"browser-game",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"hosting",
|
|
10
10
|
"web3",
|
|
11
11
|
"solana",
|
|
12
|
-
"
|
|
12
|
+
"xgames.fun"
|
|
13
13
|
],
|
|
14
14
|
"homepage": "https://github.com/XGamesFun/xcore#readme",
|
|
15
15
|
"repository": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"bugs": {
|
|
20
20
|
"url": "https://github.com/XGamesFun/xcore/issues"
|
|
21
21
|
},
|
|
22
|
-
"author": "
|
|
22
|
+
"author": "xgames.fun",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"bin": {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bring-your-own-game
|
|
3
|
-
description: Help someone bring a web game they already made without this kit (plain HTML/JS, or a Vite, Next.js static export, Astro, SvelteKit, Nuxt, Create React App or Parcel project) to
|
|
3
|
+
description: Help someone bring a web game they already made without this kit (plain HTML/JS, or a Vite, Next.js static export, Astro, SvelteKit, Nuxt, Create React App or Parcel project) to xgames.fun - adopt it, fix what does not fit the hosting (server-side rendering, paths from the root, CDN scripts and fonts, undeclared backend origins, secrets, size limits), check it with the page check, connect their own backend or a server/ Worker, add a loot economy, and publish it. Use when the person has an existing game folder or repository and wants web3, xgames.fun hosting, or the kit's checks for it; never rebuild such a game through the creation loop.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Bring your own game
|
|
7
7
|
|
|
8
|
-
The person already has a game. Your job is to make it fit
|
|
8
|
+
The person already has a game. Your job is to make it fit xgames.fun's hosting with the smallest changes to _their_
|
|
9
9
|
files, never to remake it. The whole story, with every rule of the hosting, is xcore's README
|
|
10
10
|
(github.com/XGamesFun/xcore; on npm, @xgamesfun/xcore's page); read its "What the hosting takes" table once. Everything
|
|
11
11
|
here is `@xgamesfun/xcore` (the `xcore` command: `npx -y @xgamesfun/xcore …`, or `xcore …` once installed with `npm i -g
|
|
@@ -67,7 +67,7 @@ Four checks: it builds, it fits the hosting, the page starts (phone and desktop,
|
|
|
67
67
|
console errors, nothing missing, nothing from the root, something on screen), it stays inside its origins. Open the two
|
|
68
68
|
screenshots in `evidence/verify-N/` and look at them. A failing check 4 names the origin and why: declare it in
|
|
69
69
|
`backend.json` if the game really calls it, or remove the call. The page check does not say the game plays well:
|
|
70
|
-
that is the person's own game; ask them what to try, and try it with `xcore dev <folder>` (the game as
|
|
70
|
+
that is the person's own game; ask them what to try, and try it with `xcore dev <folder>` (the game as xgames.fun
|
|
71
71
|
hosts it, with its server, sign-in, saves, rooms and economy RPC) or their own.
|
|
72
72
|
|
|
73
73
|
## 4. Players, a database, a backend (only when the game has or needs them)
|
|
@@ -96,7 +96,7 @@ await GamesFun.join_room(code)` and `multiplayer.multiplayer_peer = peer`; keep
|
|
|
96
96
|
answer CORS for the game's origin. Never put their secret keys in the game.
|
|
97
97
|
- **They want one**: Supabase or Railway, connected by the game-making kit (`@xgamesfun/kit`: `xcore backend connect
|
|
98
98
|
supabase|railway` with the kit installed). Without the kit, they set one up themselves and you declare it as above.
|
|
99
|
-
- **Server logic
|
|
99
|
+
- **Server logic xgames.fun runs for them**: `server/index.js` and `gamesfun.json` → `server`
|
|
100
100
|
([docs/SERVER.md](../../docs/SERVER.md)): a Worker on `/api/*`, Durable Objects for rooms, `env.GF` for sign-in, saves
|
|
101
101
|
and scores. Their page calls it with a relative address (`fetch('api/scores')`): `/api/*` at the published origin,
|
|
102
102
|
`/game/api/*` under `xcore dev`, which runs it under workerd with a local `env.GF`. A Node-only server stays on
|
|
@@ -126,14 +126,14 @@ Unity). A browser with no wallet is told so, with the game still playable. `xcor
|
|
|
126
126
|
SOL: an economy approved to try it is one the person can approve again later). Going live is the person's own
|
|
127
127
|
`xcore loot approve`, in their wallet: never yours.
|
|
128
128
|
|
|
129
|
-
Paying a result has two signers (docs/SERVER.md, "Paying a result", has the rule):
|
|
129
|
+
Paying a result has two signers (docs/SERVER.md, "Paying a result", has the rule): xgames.fun's keeper, when the
|
|
130
130
|
server already decides who won and the prize is fixed by place (the server calls `env.GF.loot.report`; the owner
|
|
131
131
|
certifies the host key in Studio once, and nobody keeps a key), or the server's own key, when the amount depends on
|
|
132
132
|
the result, when the server replays or bounds runs, for entry fees or a prize pool, or past 120 results an hour. For
|
|
133
133
|
the own key, run `xcore add vouchers <folder>`: it writes `server/vouchers.js` and a test key for `xcore dev` (its
|
|
134
134
|
seed in `.dev.vars`, kept out of git). Then, in order: write `PAY.check` with the person (what their server checks
|
|
135
135
|
before it pays; nothing is paid until it is written) and count payouts in `PAY.paid`; they publish; they run `xcore
|
|
136
|
-
secret set VOUCHER_SEED --new-key` (the live key
|
|
136
|
+
secret set VOUCHER_SEED --new-key` (the live key: made here, sent to xgames.fun only and never kept on their machine; it prints the address);
|
|
137
137
|
that address goes in the grant's `vouchers` (`xcore loot check`); they approve and publish again. Never read the seed
|
|
138
138
|
out of `.dev.vars` or repeat it.
|
|
139
139
|
|
|
@@ -165,9 +165,10 @@ nothing checked (their gameplay, their backend's own behaviour).
|
|
|
165
165
|
|
|
166
166
|
A game with a `server/`, saves or scores runs under its **plan** (`xcore plan <folder>`): Free by default (50 ms of CPU
|
|
167
167
|
and 600 requests a minute, $1 of use a month, then capped and slow until the month ends). When the game needs more
|
|
168
|
-
(a server that replays play, many players at once), tell them: a paid
|
|
169
|
-
(`xcore plan activate <folder>` once they have
|
|
170
|
-
|
|
168
|
+
(a server that replays play, many players at once), tell them: a paid slot makes it Creator
|
|
169
|
+
(`xcore plan activate <folder>` once they have one), and the paid slot moves between their games at any time. Everyone
|
|
170
|
+
has 2 free slots: a third game is refused on publish (`no-free-slot`) until they archive one on their console
|
|
171
|
+
(xgames.fun/console) or move one to a paid slot. Never activate, deactivate or archive a game without asking.
|
|
171
172
|
|
|
172
173
|
Their own website can show the game (`xcore embed add https://theirsite.com <folder>` prints the `<iframe>`), and on
|
|
173
174
|
Creator or Scale it can play at their own domain (`xcore domain add play.theirsite.com <folder>` prints two DNS records
|