ravensight-playtest 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 +380 -0
- package/addons/ravensight_driver/driver.gd +836 -0
- package/addons/ravensight_driver/export_plugin.gd +51 -0
- package/addons/ravensight_driver/plugin.cfg +7 -0
- package/addons/ravensight_driver/plugin.gd +36 -0
- package/bin/ravensight-playtest.js +31 -0
- package/package.json +45 -0
- package/src/api/README.md +500 -0
- package/src/api/client.js +340 -0
- package/src/api/errors.js +115 -0
- package/src/api/http.js +194 -0
- package/src/api/index.js +107 -0
- package/src/auth/deviceCode.js +79 -0
- package/src/auth/keychain.js +159 -0
- package/src/auth/session.js +128 -0
- package/src/cli.js +335 -0
- package/src/commands/brief.js +303 -0
- package/src/commands/check.js +318 -0
- package/src/commands/fakeCore.js +379 -0
- package/src/commands/init.js +120 -0
- package/src/commands/login.js +90 -0
- package/src/commands/logout.js +70 -0
- package/src/commands/open.js +125 -0
- package/src/commands/profile.js +262 -0
- package/src/commands/resume.js +156 -0
- package/src/commands/run.js +1015 -0
- package/src/commands/upload.js +137 -0
- package/src/config.js +100 -0
- package/src/dashboard.js +97 -0
- package/src/detect.js +77 -0
- package/src/errors.js +44 -0
- package/src/fsutil.js +77 -0
- package/src/godot.js +85 -0
- package/src/packs/index.js +191 -0
- package/src/paths.js +129 -0
- package/src/run/aggregate.js +658 -0
- package/src/run/args.js +111 -0
- package/src/run/context.js +181 -0
- package/src/run/deps.js +184 -0
- package/src/run/drivers/driver.js +183 -0
- package/src/run/drivers/godot-observation.js +138 -0
- package/src/run/drivers/godot-project.js +475 -0
- package/src/run/drivers/godot-rpc.js +225 -0
- package/src/run/drivers/godot.js +587 -0
- package/src/run/drivers/index.js +52 -0
- package/src/run/drivers/web.js +385 -0
- package/src/run/exit.js +21 -0
- package/src/run/heartbeat.js +131 -0
- package/src/run/index.js +31 -0
- package/src/run/json.js +56 -0
- package/src/run/model.js +384 -0
- package/src/run/paths.js +88 -0
- package/src/run/personaLoop.js +871 -0
- package/src/run/profile.js +214 -0
- package/src/run/regenerate.js +149 -0
- package/src/run/repoTools.js +286 -0
- package/src/run/report.js +222 -0
- package/src/run/resume.js +272 -0
- package/src/run/secretScan.js +171 -0
- package/src/run/state.js +198 -0
- package/src/run/synthetic.js +206 -0
- package/src/run/tools.js +344 -0
- package/src/run/transcript.js +93 -0
- package/src/run/usage.js +115 -0
- package/src/state/index.js +105 -0
- package/src/states.js +104 -0
- package/src/ui/index.js +195 -0
- package/src/upload/allowlist.js +116 -0
- package/src/upload/index.js +467 -0
- package/src/upload/queue.js +114 -0
- package/src/version.js +63 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Client for the Godot driver's wire protocol: newline delimited JSON-RPC 2.0
|
|
2
|
+
// over a loopback TCP socket (addons/ravensight_driver/driver.gd, spec 07).
|
|
3
|
+
//
|
|
4
|
+
// This is the Node replacement for the proof of concept's client.py. It keeps
|
|
5
|
+
// the same shape (one JSON object per line, ids matched to pending calls) and
|
|
6
|
+
// adds the parts a long persona run needs: per-call timeouts, a queue for the
|
|
7
|
+
// server's interleaved "event" notifications, a hard cap on response size, and
|
|
8
|
+
// a single rejection path so a dead game never leaves a call hanging forever.
|
|
9
|
+
import { EventEmitter } from 'node:events';
|
|
10
|
+
import net from 'node:net';
|
|
11
|
+
|
|
12
|
+
const NEWLINE = 0x0a;
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_PORT = 47800;
|
|
15
|
+
// Screenshots travel as base64 inside a single line, so responses are allowed
|
|
16
|
+
// to be much larger than requests.
|
|
17
|
+
export const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
/** A JSON-RPC error returned by the driver, with its spec 07 numeric code. */
|
|
20
|
+
export class DriverRpcError extends Error {
|
|
21
|
+
constructor(method, { code, message, data }) {
|
|
22
|
+
super(`${method}: ${code} ${message}`);
|
|
23
|
+
this.name = 'DriverRpcError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.rpcMessage = message;
|
|
26
|
+
this.data = data;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Raised when the socket goes away, which usually means the game exited. */
|
|
31
|
+
export class DriverClosedError extends Error {
|
|
32
|
+
constructor(message = 'driver connection closed') {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'DriverClosedError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class DriverTimeoutError extends Error {
|
|
39
|
+
constructor(method, timeoutMs) {
|
|
40
|
+
super(`${method}: no response within ${timeoutMs}ms`);
|
|
41
|
+
this.name = 'DriverTimeoutError';
|
|
42
|
+
this.method = method;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class GodotRpcClient extends EventEmitter {
|
|
47
|
+
constructor({
|
|
48
|
+
host = '127.0.0.1',
|
|
49
|
+
port = DEFAULT_PORT,
|
|
50
|
+
timeoutMs = 15000,
|
|
51
|
+
maxResponseBytes = MAX_RESPONSE_BYTES,
|
|
52
|
+
} = {}) {
|
|
53
|
+
super();
|
|
54
|
+
this.host = host;
|
|
55
|
+
this.port = port;
|
|
56
|
+
this.timeoutMs = timeoutMs;
|
|
57
|
+
this.maxResponseBytes = maxResponseBytes;
|
|
58
|
+
this.socket = null;
|
|
59
|
+
this.closed = false;
|
|
60
|
+
this.events = [];
|
|
61
|
+
this._nextId = 1;
|
|
62
|
+
this._pending = new Map();
|
|
63
|
+
this._buffer = Buffer.alloc(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Dials the driver, retrying while the game boots. The driver only ever
|
|
68
|
+
* listens on loopback, so a non loopback host is refused here too rather
|
|
69
|
+
* than being attempted and failing obscurely.
|
|
70
|
+
*/
|
|
71
|
+
async connect({ attempts = 240, delayMs = 250 } = {}) {
|
|
72
|
+
if (!isLoopbackHost(this.host)) {
|
|
73
|
+
throw new Error(`refusing to dial non loopback host ${this.host}`);
|
|
74
|
+
}
|
|
75
|
+
let lastError;
|
|
76
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
77
|
+
try {
|
|
78
|
+
this.socket = await openSocket(this.host, this.port);
|
|
79
|
+
this._attach(this.socket);
|
|
80
|
+
this.closed = false;
|
|
81
|
+
return this;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
lastError = error;
|
|
84
|
+
await sleep(delayMs);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error(
|
|
88
|
+
`could not connect to the Godot driver at ${this.host}:${this.port}: ${lastError?.message ?? 'unknown error'}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_attach(socket) {
|
|
93
|
+
socket.setNoDelay(true);
|
|
94
|
+
socket.on('data', (chunk) => this._onData(chunk));
|
|
95
|
+
socket.on('error', (error) => this._fail(new DriverClosedError(error.message)));
|
|
96
|
+
socket.on('close', () => this._fail(new DriverClosedError()));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_onData(chunk) {
|
|
100
|
+
this._buffer = Buffer.concat([this._buffer, chunk]);
|
|
101
|
+
for (;;) {
|
|
102
|
+
const index = this._buffer.indexOf(NEWLINE);
|
|
103
|
+
if (index === -1) {
|
|
104
|
+
if (this._buffer.length > this.maxResponseBytes) {
|
|
105
|
+
this._fail(
|
|
106
|
+
new DriverClosedError(`driver response exceeded ${this.maxResponseBytes} bytes`),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const line = this._buffer.subarray(0, index).toString('utf8').trim();
|
|
112
|
+
this._buffer = this._buffer.subarray(index + 1);
|
|
113
|
+
if (line) this._onLine(line);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_onLine(line) {
|
|
118
|
+
let message;
|
|
119
|
+
try {
|
|
120
|
+
message = JSON.parse(line);
|
|
121
|
+
} catch {
|
|
122
|
+
this.emit('protocolError', new Error(`driver sent invalid JSON: ${line.slice(0, 200)}`));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (message.method === 'event') {
|
|
126
|
+
const params = message.params ?? {};
|
|
127
|
+
this.events.push(params);
|
|
128
|
+
this.emit('event', params);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const pending = this._pending.get(message.id);
|
|
132
|
+
if (!pending) {
|
|
133
|
+
// An id we are not waiting for: a late reply after a timeout, or the
|
|
134
|
+
// unsolicited busy error the driver sends to a second client.
|
|
135
|
+
this.emit('orphan', message);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this._pending.delete(message.id);
|
|
139
|
+
clearTimeout(pending.timer);
|
|
140
|
+
if (message.error) {
|
|
141
|
+
pending.reject(new DriverRpcError(pending.method, message.error));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
pending.resolve(message.result ?? {});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_fail(error) {
|
|
148
|
+
if (this.closed) return;
|
|
149
|
+
this.closed = true;
|
|
150
|
+
for (const [id, pending] of this._pending) {
|
|
151
|
+
clearTimeout(pending.timer);
|
|
152
|
+
this._pending.delete(id);
|
|
153
|
+
pending.reject(error);
|
|
154
|
+
}
|
|
155
|
+
if (this.socket) {
|
|
156
|
+
this.socket.destroy();
|
|
157
|
+
this.socket = null;
|
|
158
|
+
}
|
|
159
|
+
this.emit('closed', error);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async call(method, params = {}, { timeoutMs = this.timeoutMs } = {}) {
|
|
163
|
+
if (this.closed || !this.socket) throw new DriverClosedError();
|
|
164
|
+
const id = this._nextId;
|
|
165
|
+
this._nextId += 1;
|
|
166
|
+
const line = `${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`;
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const timer = setTimeout(() => {
|
|
169
|
+
this._pending.delete(id);
|
|
170
|
+
reject(new DriverTimeoutError(method, timeoutMs));
|
|
171
|
+
}, timeoutMs);
|
|
172
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
173
|
+
this._pending.set(id, { method, resolve, reject, timer });
|
|
174
|
+
this.socket.write(line, (error) => {
|
|
175
|
+
if (!error) return;
|
|
176
|
+
this._pending.delete(id);
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
reject(new DriverClosedError(error.message));
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Returns the notifications received since the previous drain. */
|
|
184
|
+
drainEvents() {
|
|
185
|
+
const drained = this.events;
|
|
186
|
+
this.events = [];
|
|
187
|
+
return drained;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
close() {
|
|
191
|
+
this._fail(new DriverClosedError('closed by client'));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Only the two literal loopback addresses. "localhost" is deliberately not
|
|
197
|
+
* accepted: what it resolves to is up to /etc/hosts and the resolver, so it is
|
|
198
|
+
* not a guarantee that the connection stays on the machine, and the driver's
|
|
199
|
+
* one hard promise is that it never leaves it.
|
|
200
|
+
*/
|
|
201
|
+
export function isLoopbackHost(host) {
|
|
202
|
+
return host === '127.0.0.1' || host === '::1';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function openSocket(host, port) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const socket = net.createConnection({ host, port });
|
|
208
|
+
const onError = (error) => {
|
|
209
|
+
socket.destroy();
|
|
210
|
+
reject(error);
|
|
211
|
+
};
|
|
212
|
+
socket.once('error', onError);
|
|
213
|
+
socket.once('connect', () => {
|
|
214
|
+
socket.removeListener('error', onError);
|
|
215
|
+
resolve(socket);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function sleep(ms) {
|
|
221
|
+
return new Promise((resolve) => {
|
|
222
|
+
const timer = setTimeout(resolve, ms);
|
|
223
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
224
|
+
});
|
|
225
|
+
}
|