dotframe 0.1.9 → 0.2.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/CHANGELOG.md +15 -0
- package/cli/commands/config.ts +21 -3
- package/cli/commands/docker.ts +36 -19
- package/cli/commands/doctor.ts +24 -3
- package/cli/commands/online.ts +37 -6
- package/cli/commands/ship.ts +57 -1
- package/cli/ios.ts +58 -11
- package/cli/lib.ts +29 -1
- package/cli/main.ts +19 -9
- package/cli/native.ts +21 -12
- package/native/df_audio.c +12 -0
- package/native/df_native.c +67 -10
- package/native/df_ws_apple.m +197 -0
- package/native/df_ws_win.c +231 -0
- package/native/ffi.macos.json +28 -29
- package/native/ffi.windows.json +30 -30
- package/package.json +1 -1
- package/scripts/build-native.sh +6 -2
- package/skill-data/core/SKILL.md +3 -1
- package/skill-data/export-web/SKILL.md +1 -0
- package/skill-data/ios/SKILL.md +6 -1
- package/skill-data/macos/SKILL.md +7 -0
- package/skill-data/netplay/SKILL.md +5 -1
- package/src/draw2d.ts +7 -0
- package/src/gpu.ts +3 -0
- package/src/native/backend.ts +8 -10
- package/src/native/ffi.ts +9 -5
- package/src/native/relay.ts +95 -0
- package/src/native/run.ts +1 -0
- package/src/probe-web.ts +7 -0
- package/src/probe.ts +3 -3
- package/src/ui.ts +191 -0
- package/src/web/run.ts +38 -10
- package/templates/_base/main.ios.ts +1 -1
- package/templates/_base/main.native.ts +27 -2
- package/templates/_base/main.web.ts +3 -1
- package/templates/_base/src/setup.ts +22 -7
- package/tools/gen-library-glue.ts +10 -4
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// WebSocket client for the native relay transport on Windows, over WinHTTP (TLS included). Same surface as
|
|
2
|
+
// df_ws_apple.m: the game polls state, sends text frames, and reads messages one byte at a time. Connecting and
|
|
3
|
+
// receiving run on a worker thread per socket; a critical section guards the inbox.
|
|
4
|
+
#include <stdint.h>
|
|
5
|
+
#include <stdlib.h>
|
|
6
|
+
#include <string.h>
|
|
7
|
+
#include <windows.h>
|
|
8
|
+
#include <winhttp.h>
|
|
9
|
+
#include <SDL3/SDL.h>
|
|
10
|
+
|
|
11
|
+
enum { DF_WS_CONNECTING = 0, DF_WS_OPEN = 1, DF_WS_CLOSED = 2, DF_WS_ERROR = 3 };
|
|
12
|
+
|
|
13
|
+
typedef struct Message {
|
|
14
|
+
uint8_t *data;
|
|
15
|
+
int32_t len;
|
|
16
|
+
struct Message *next;
|
|
17
|
+
} Message;
|
|
18
|
+
|
|
19
|
+
typedef struct {
|
|
20
|
+
wchar_t url[2048];
|
|
21
|
+
HINTERNET session, connection, request, socket;
|
|
22
|
+
volatile LONG state;
|
|
23
|
+
CRITICAL_SECTION lock;
|
|
24
|
+
Message *head, *tail;
|
|
25
|
+
HANDLE thread;
|
|
26
|
+
} Socket;
|
|
27
|
+
|
|
28
|
+
#define DF_MAX_SOCKETS 16
|
|
29
|
+
static Socket *g_sockets[DF_MAX_SOCKETS];
|
|
30
|
+
|
|
31
|
+
static Socket *socket_at(int32_t handle) {
|
|
32
|
+
return handle >= 0 && handle < DF_MAX_SOCKETS ? g_sockets[handle] : NULL;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static void push(Socket *s, const uint8_t *data, int32_t len) {
|
|
36
|
+
Message *m = (Message *)calloc(1, sizeof *m);
|
|
37
|
+
if (!m) return;
|
|
38
|
+
m->data = (uint8_t *)malloc(len > 0 ? (size_t)len : 1);
|
|
39
|
+
if (!m->data) {
|
|
40
|
+
free(m);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
memcpy(m->data, data, (size_t)len);
|
|
44
|
+
m->len = len;
|
|
45
|
+
EnterCriticalSection(&s->lock);
|
|
46
|
+
if (s->tail) s->tail->next = m;
|
|
47
|
+
else s->head = m;
|
|
48
|
+
s->tail = m;
|
|
49
|
+
LeaveCriticalSection(&s->lock);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static DWORD WINAPI run(LPVOID arg) {
|
|
53
|
+
Socket *s = (Socket *)arg;
|
|
54
|
+
URL_COMPONENTS parts;
|
|
55
|
+
memset(&parts, 0, sizeof parts);
|
|
56
|
+
parts.dwStructSize = sizeof parts;
|
|
57
|
+
wchar_t host[256], path[1792];
|
|
58
|
+
parts.lpszHostName = host;
|
|
59
|
+
parts.dwHostNameLength = 256;
|
|
60
|
+
parts.lpszUrlPath = path;
|
|
61
|
+
parts.dwUrlPathLength = 1792;
|
|
62
|
+
wchar_t extra[1024];
|
|
63
|
+
parts.lpszExtraInfo = extra;
|
|
64
|
+
parts.dwExtraInfoLength = 1024;
|
|
65
|
+
if (!WinHttpCrackUrl(s->url, 0, 0, &parts)) goto fail;
|
|
66
|
+
// WinHttpCrackUrl knows ws:// and wss:// only as unknown schemes; decide TLS from the prefix.
|
|
67
|
+
int secure = wcsncmp(s->url, L"wss://", 6) == 0;
|
|
68
|
+
INTERNET_PORT port = parts.nPort ? parts.nPort : (secure ? 443 : 80);
|
|
69
|
+
wchar_t target[2816];
|
|
70
|
+
_snwprintf(target, 2816, L"%ls%ls", parts.dwUrlPathLength ? path : L"/", parts.dwExtraInfoLength ? extra : L"");
|
|
71
|
+
s->session = WinHttpOpen(L"dotframe", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
|
72
|
+
if (!s->session) goto fail;
|
|
73
|
+
s->connection = WinHttpConnect(s->session, host, port, 0);
|
|
74
|
+
if (!s->connection) goto fail;
|
|
75
|
+
s->request = WinHttpOpenRequest(s->connection, L"GET", target, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, secure ? WINHTTP_FLAG_SECURE : 0);
|
|
76
|
+
if (!s->request) goto fail;
|
|
77
|
+
if (!WinHttpSetOption(s->request, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, NULL, 0)) goto fail;
|
|
78
|
+
if (!WinHttpSendRequest(s->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, NULL, 0, 0, 0)) goto fail;
|
|
79
|
+
if (!WinHttpReceiveResponse(s->request, NULL)) goto fail;
|
|
80
|
+
s->socket = WinHttpWebSocketCompleteUpgrade(s->request, 0);
|
|
81
|
+
if (!s->socket) goto fail;
|
|
82
|
+
WinHttpCloseHandle(s->request);
|
|
83
|
+
s->request = NULL;
|
|
84
|
+
InterlockedExchange(&s->state, DF_WS_OPEN);
|
|
85
|
+
// A message can arrive in fragments; collect until a non-fragment buffer type closes it.
|
|
86
|
+
uint8_t chunk[4096];
|
|
87
|
+
uint8_t *message = NULL;
|
|
88
|
+
int32_t length = 0;
|
|
89
|
+
for (;;) {
|
|
90
|
+
DWORD read = 0;
|
|
91
|
+
WINHTTP_WEB_SOCKET_BUFFER_TYPE type;
|
|
92
|
+
if (WinHttpWebSocketReceive(s->socket, chunk, sizeof chunk, &read, &type) != NO_ERROR) break;
|
|
93
|
+
if (type == WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE) break;
|
|
94
|
+
uint8_t *grown = (uint8_t *)realloc(message, (size_t)length + read + 1);
|
|
95
|
+
if (!grown) break;
|
|
96
|
+
message = grown;
|
|
97
|
+
memcpy(message + length, chunk, read);
|
|
98
|
+
length += (int32_t)read;
|
|
99
|
+
if (type == WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE || type == WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE) {
|
|
100
|
+
push(s, message, length);
|
|
101
|
+
length = 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
free(message);
|
|
105
|
+
if (s->state != DF_WS_CLOSED) InterlockedExchange(&s->state, DF_WS_CLOSED);
|
|
106
|
+
return 0;
|
|
107
|
+
fail:
|
|
108
|
+
InterlockedExchange(&s->state, DF_WS_ERROR);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static int32_t df_ws_open(const uint8_t *url, size_t len) {
|
|
113
|
+
int32_t handle = -1;
|
|
114
|
+
for (int32_t i = 0; i < DF_MAX_SOCKETS && handle < 0; i++)
|
|
115
|
+
if (!g_sockets[i]) handle = i;
|
|
116
|
+
if (handle < 0) return -1;
|
|
117
|
+
Socket *s = (Socket *)calloc(1, sizeof *s);
|
|
118
|
+
if (!s) return -1;
|
|
119
|
+
int written = MultiByteToWideChar(CP_UTF8, 0, (const char *)url, (int)len, s->url, 2047);
|
|
120
|
+
if (written <= 0) {
|
|
121
|
+
free(s);
|
|
122
|
+
return -1;
|
|
123
|
+
}
|
|
124
|
+
s->url[written] = 0;
|
|
125
|
+
s->state = DF_WS_CONNECTING;
|
|
126
|
+
InitializeCriticalSection(&s->lock);
|
|
127
|
+
g_sockets[handle] = s;
|
|
128
|
+
s->thread = CreateThread(NULL, 0, run, s, 0, NULL);
|
|
129
|
+
if (!s->thread) s->state = DF_WS_ERROR;
|
|
130
|
+
return handle;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static int32_t df_ws_state(int32_t handle) {
|
|
134
|
+
Socket *s = socket_at(handle);
|
|
135
|
+
return s ? (int32_t)s->state : DF_WS_CLOSED;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static int32_t df_ws_send(int32_t handle, const uint8_t *data, size_t len) {
|
|
139
|
+
Socket *s = socket_at(handle);
|
|
140
|
+
if (!s || s->state != DF_WS_OPEN) return -1;
|
|
141
|
+
return WinHttpWebSocketSend(s->socket, WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, (void *)data, (DWORD)len) == NO_ERROR ? 0 : -2;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static int32_t df_ws_next(int32_t handle) {
|
|
145
|
+
Socket *s = socket_at(handle);
|
|
146
|
+
if (!s) return -1;
|
|
147
|
+
EnterCriticalSection(&s->lock);
|
|
148
|
+
int32_t len = s->head ? s->head->len : -1;
|
|
149
|
+
LeaveCriticalSection(&s->lock);
|
|
150
|
+
return len;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static int32_t df_ws_byte(int32_t handle, int32_t index) {
|
|
154
|
+
Socket *s = socket_at(handle);
|
|
155
|
+
if (!s) return -1;
|
|
156
|
+
EnterCriticalSection(&s->lock);
|
|
157
|
+
int32_t value = s->head && index >= 0 && index < s->head->len ? s->head->data[index] : -1;
|
|
158
|
+
LeaveCriticalSection(&s->lock);
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
static void df_ws_pop(int32_t handle) {
|
|
163
|
+
Socket *s = socket_at(handle);
|
|
164
|
+
if (!s) return;
|
|
165
|
+
EnterCriticalSection(&s->lock);
|
|
166
|
+
Message *m = s->head;
|
|
167
|
+
if (m) {
|
|
168
|
+
s->head = m->next;
|
|
169
|
+
if (!s->head) s->tail = NULL;
|
|
170
|
+
}
|
|
171
|
+
LeaveCriticalSection(&s->lock);
|
|
172
|
+
if (m) {
|
|
173
|
+
free(m->data);
|
|
174
|
+
free(m);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static void df_ws_close(int32_t handle) {
|
|
179
|
+
Socket *s = socket_at(handle);
|
|
180
|
+
if (!s) return;
|
|
181
|
+
InterlockedExchange(&s->state, DF_WS_CLOSED);
|
|
182
|
+
if (s->socket) WinHttpWebSocketClose(s->socket, WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, NULL, 0);
|
|
183
|
+
if (s->thread) {
|
|
184
|
+
WaitForSingleObject(s->thread, 2000);
|
|
185
|
+
CloseHandle(s->thread);
|
|
186
|
+
}
|
|
187
|
+
if (s->socket) WinHttpCloseHandle(s->socket);
|
|
188
|
+
if (s->request) WinHttpCloseHandle(s->request);
|
|
189
|
+
if (s->connection) WinHttpCloseHandle(s->connection);
|
|
190
|
+
if (s->session) WinHttpCloseHandle(s->session);
|
|
191
|
+
while (s->head) df_ws_pop(handle);
|
|
192
|
+
DeleteCriticalSection(&s->lock);
|
|
193
|
+
free(s);
|
|
194
|
+
g_sockets[handle] = NULL;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// No share sheet on Windows: the text goes to the clipboard. Returns 2 (clipboard) or 0.
|
|
198
|
+
static int32_t df_share(const uint8_t *text, size_t len) {
|
|
199
|
+
char *value = (char *)malloc(len + 1);
|
|
200
|
+
if (!value) return 0;
|
|
201
|
+
memcpy(value, text, len);
|
|
202
|
+
value[len] = 0;
|
|
203
|
+
int ok = SDL_SetClipboardText(value);
|
|
204
|
+
free(value);
|
|
205
|
+
return ok ? 2 : 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The exported surface is two calls, because scriptc library mode (iOS) caps a library at 32 host callbacks.
|
|
209
|
+
// op 0 opens text as a URL (returns a socket), 1 sends text on handle, 2 shares text.
|
|
210
|
+
int32_t df_ws_text(int32_t op, int32_t handle, const uint8_t *text, size_t len) {
|
|
211
|
+
if (op == 0) return df_ws_open(text, len);
|
|
212
|
+
if (op == 1) return df_ws_send(handle, text, len);
|
|
213
|
+
if (op == 2) return df_share(text, len);
|
|
214
|
+
return -1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// op 0 state, 1 length of the next message, 2 byte arg of it, 3 drop it, 4 close.
|
|
218
|
+
int32_t df_ws(int32_t op, int32_t handle, int32_t arg) {
|
|
219
|
+
if (op == 0) return df_ws_state(handle);
|
|
220
|
+
if (op == 1) return df_ws_next(handle);
|
|
221
|
+
if (op == 2) return df_ws_byte(handle, arg);
|
|
222
|
+
if (op == 3) {
|
|
223
|
+
df_ws_pop(handle);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
if (op == 4) {
|
|
227
|
+
df_ws_close(handle);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
return -1;
|
|
231
|
+
}
|
package/native/ffi.macos.json
CHANGED
|
@@ -134,6 +134,14 @@
|
|
|
134
134
|
],
|
|
135
135
|
"returns": "i32"
|
|
136
136
|
},
|
|
137
|
+
{
|
|
138
|
+
"name": "dfTextureDestroy",
|
|
139
|
+
"symbol": "df_texture_destroy",
|
|
140
|
+
"params": [
|
|
141
|
+
"i32"
|
|
142
|
+
],
|
|
143
|
+
"returns": "void"
|
|
144
|
+
},
|
|
137
145
|
{
|
|
138
146
|
"name": "dfTextureSize",
|
|
139
147
|
"symbol": "df_texture_size",
|
|
@@ -243,49 +251,40 @@
|
|
|
243
251
|
"returns": "i32"
|
|
244
252
|
},
|
|
245
253
|
{
|
|
246
|
-
"name": "
|
|
247
|
-
"symbol": "
|
|
248
|
-
"params": [
|
|
249
|
-
"i32",
|
|
250
|
-
"bool",
|
|
251
|
-
"f64"
|
|
252
|
-
],
|
|
253
|
-
"returns": "i32"
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
"name": "dfMusicStop",
|
|
257
|
-
"symbol": "df_music_stop",
|
|
254
|
+
"name": "dfAudioActive",
|
|
255
|
+
"symbol": "df_audio_active",
|
|
258
256
|
"params": [],
|
|
259
|
-
"returns": "
|
|
257
|
+
"returns": "i32"
|
|
260
258
|
},
|
|
261
259
|
{
|
|
262
|
-
"name": "
|
|
263
|
-
"symbol": "
|
|
260
|
+
"name": "dfWsText",
|
|
261
|
+
"symbol": "df_ws_text",
|
|
264
262
|
"params": [
|
|
265
|
-
"
|
|
263
|
+
"i32",
|
|
264
|
+
"i32",
|
|
265
|
+
"string"
|
|
266
266
|
],
|
|
267
|
-
"returns": "
|
|
267
|
+
"returns": "i32"
|
|
268
268
|
},
|
|
269
269
|
{
|
|
270
|
-
"name": "
|
|
271
|
-
"symbol": "
|
|
270
|
+
"name": "dfWs",
|
|
271
|
+
"symbol": "df_ws",
|
|
272
272
|
"params": [
|
|
273
|
-
"
|
|
273
|
+
"i32",
|
|
274
|
+
"i32",
|
|
275
|
+
"i32"
|
|
274
276
|
],
|
|
275
|
-
"returns": "
|
|
277
|
+
"returns": "i32"
|
|
276
278
|
},
|
|
277
279
|
{
|
|
278
|
-
"name": "
|
|
279
|
-
"symbol": "
|
|
280
|
+
"name": "dfMusic",
|
|
281
|
+
"symbol": "df_music",
|
|
280
282
|
"params": [
|
|
283
|
+
"i32",
|
|
284
|
+
"f64",
|
|
285
|
+
"f64",
|
|
281
286
|
"f64"
|
|
282
287
|
],
|
|
283
|
-
"returns": "void"
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
"name": "dfAudioActive",
|
|
287
|
-
"symbol": "df_audio_active",
|
|
288
|
-
"params": [],
|
|
289
288
|
"returns": "i32"
|
|
290
289
|
}
|
|
291
290
|
],
|
package/native/ffi.windows.json
CHANGED
|
@@ -134,6 +134,14 @@
|
|
|
134
134
|
],
|
|
135
135
|
"returns": "i32"
|
|
136
136
|
},
|
|
137
|
+
{
|
|
138
|
+
"name": "dfTextureDestroy",
|
|
139
|
+
"symbol": "df_texture_destroy",
|
|
140
|
+
"params": [
|
|
141
|
+
"i32"
|
|
142
|
+
],
|
|
143
|
+
"returns": "void"
|
|
144
|
+
},
|
|
137
145
|
{
|
|
138
146
|
"name": "dfTextureSize",
|
|
139
147
|
"symbol": "df_texture_size",
|
|
@@ -243,49 +251,40 @@
|
|
|
243
251
|
"returns": "i32"
|
|
244
252
|
},
|
|
245
253
|
{
|
|
246
|
-
"name": "
|
|
247
|
-
"symbol": "
|
|
248
|
-
"params": [
|
|
249
|
-
"i32",
|
|
250
|
-
"bool",
|
|
251
|
-
"f64"
|
|
252
|
-
],
|
|
253
|
-
"returns": "i32"
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
"name": "dfMusicStop",
|
|
257
|
-
"symbol": "df_music_stop",
|
|
254
|
+
"name": "dfAudioActive",
|
|
255
|
+
"symbol": "df_audio_active",
|
|
258
256
|
"params": [],
|
|
259
|
-
"returns": "
|
|
257
|
+
"returns": "i32"
|
|
260
258
|
},
|
|
261
259
|
{
|
|
262
|
-
"name": "
|
|
263
|
-
"symbol": "
|
|
260
|
+
"name": "dfWsText",
|
|
261
|
+
"symbol": "df_ws_text",
|
|
264
262
|
"params": [
|
|
265
|
-
"
|
|
263
|
+
"i32",
|
|
264
|
+
"i32",
|
|
265
|
+
"string"
|
|
266
266
|
],
|
|
267
|
-
"returns": "
|
|
267
|
+
"returns": "i32"
|
|
268
268
|
},
|
|
269
269
|
{
|
|
270
|
-
"name": "
|
|
271
|
-
"symbol": "
|
|
270
|
+
"name": "dfWs",
|
|
271
|
+
"symbol": "df_ws",
|
|
272
272
|
"params": [
|
|
273
|
-
"
|
|
273
|
+
"i32",
|
|
274
|
+
"i32",
|
|
275
|
+
"i32"
|
|
274
276
|
],
|
|
275
|
-
"returns": "
|
|
277
|
+
"returns": "i32"
|
|
276
278
|
},
|
|
277
279
|
{
|
|
278
|
-
"name": "
|
|
279
|
-
"symbol": "
|
|
280
|
+
"name": "dfMusic",
|
|
281
|
+
"symbol": "df_music",
|
|
280
282
|
"params": [
|
|
283
|
+
"i32",
|
|
284
|
+
"f64",
|
|
285
|
+
"f64",
|
|
281
286
|
"f64"
|
|
282
287
|
],
|
|
283
|
-
"returns": "void"
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
"name": "dfAudioActive",
|
|
287
|
-
"symbol": "df_audio_active",
|
|
288
|
-
"params": [],
|
|
289
288
|
"returns": "i32"
|
|
290
289
|
}
|
|
291
290
|
],
|
|
@@ -317,6 +316,7 @@
|
|
|
317
316
|
"dxgi",
|
|
318
317
|
"d3dcompiler_47",
|
|
319
318
|
"propsys",
|
|
320
|
-
"unwind"
|
|
319
|
+
"unwind",
|
|
320
|
+
"winhttp"
|
|
321
321
|
]
|
|
322
322
|
}
|
package/package.json
CHANGED
package/scripts/build-native.sh
CHANGED
|
@@ -15,7 +15,9 @@ case $target in
|
|
|
15
15
|
clang -O2 -c "$root/native/$unit.c" -I"$root/vendor/SDL3-3.4.16/include" -I"$root/vendor/wgpu/macos/include" \
|
|
16
16
|
-mmacosx-version-min=14.0 -o "$root/build/macos/$unit.o"
|
|
17
17
|
done
|
|
18
|
-
|
|
18
|
+
clang -O2 -fobjc-arc -c "$root/native/df_ws_apple.m" -I"$root/vendor/SDL3-3.4.16/include" -mmacosx-version-min=14.0 \
|
|
19
|
+
-o "$root/build/macos/df_ws.o"
|
|
20
|
+
ar rcs "$root/build/macos/libdf_native.a" "$root/build/macos/df_native.o" "$root/build/macos/df_audio.o" "$root/build/macos/df_ws.o"
|
|
19
21
|
cd "$(dirname "$entry_file")" && scriptc build "$(basename "$entry_file")" --ffi "$root/native/ffi.macos.json" \
|
|
20
22
|
-o "$root/build/macos/$name"
|
|
21
23
|
;;
|
|
@@ -24,7 +26,9 @@ case $target in
|
|
|
24
26
|
zig cc -target x86_64-windows-gnu -O2 -c "$root/native/$unit.c" -I"$root/vendor/SDL3-3.4.16/include" \
|
|
25
27
|
-I"$root/vendor/wgpu/windows/include" -o "$root/build/windows/$unit.o"
|
|
26
28
|
done
|
|
27
|
-
zig
|
|
29
|
+
zig cc -target x86_64-windows-gnu -O2 -c "$root/native/df_ws_win.c" -I"$root/vendor/SDL3-3.4.16/include" \
|
|
30
|
+
-o "$root/build/windows/df_ws.o"
|
|
31
|
+
zig ar rcs "$root/build/windows/libdf_native.a" "$root/build/windows/df_native.o" "$root/build/windows/df_audio.o" "$root/build/windows/df_ws.o"
|
|
28
32
|
cd "$(dirname "$entry_file")" && SCRIPTC_RUNTIME_PACK="$root/node_modules/@scriptc/runtime-win32-x64-msvc" \
|
|
29
33
|
SCRIPTC_TARGET=x86_64-windows-gnu scriptc build "$(basename "$entry_file")" --ffi "$root/native/ffi.windows.json" \
|
|
30
34
|
--windows-subsystem gui -o "$root/build/windows/$name.exe"
|
package/skill-data/core/SKILL.md
CHANGED
|
@@ -60,7 +60,9 @@ Pass `--json` and read `ok`. Errors look like `{"ok": false, "error": {"code", "
|
|
|
60
60
|
|
|
61
61
|
- `dotframe/src/detmath`: deterministic sin, cos, tan, atan, atan2, exp, log, pow, hypot for simulation code.
|
|
62
62
|
- `dotframe/src/checksum`: a fixed-order checksum over numbers.
|
|
63
|
-
- `dotframe/src/netplay` and `dotframe/src/relay-client`: rollback netplay and the relay connection (see netplay).
|
|
63
|
+
- `dotframe/src/netplay` and `dotframe/src/relay-client`: rollback netplay and the relay connection (see netplay); `dotframe/src/native/relay` on native.
|
|
64
|
+
- `dotframe/src/ui`: buttons and arrow rows laid out once for drawing and hit-testing (`column`, `hit`, `drawUi`, `moveFocus`), taps from mouse and touch (`createTap`), and `createReleaseGate` so the press that starts a match does not reach gameplay.
|
|
65
|
+
- Web: `run(window, setup, { fit: "window" })` fills the browser window; `Draw2D.resize(w, h)` lets the logical canvas follow `gpu.aspect()`.
|
|
64
66
|
|
|
65
67
|
## Other guides
|
|
66
68
|
|
|
@@ -24,6 +24,7 @@ dotframe deploy web --prod --yes # only after the human approved the plan
|
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
26
|
dotframe deploy init web --provider dokploy # deploy/Dockerfile.web, Dockerfile.relay, nginx.conf, compose.yaml
|
|
27
|
+
dotframe deploy init web --provider dokploy --site landing # a static folder at /, the game at /play/ (--path)
|
|
27
28
|
dotframe doctor --docker # builds the web image locally, as the server will
|
|
28
29
|
dotframe config set targets.web.deploy.compose '"<compose id>"' # vps compose list
|
|
29
30
|
dotframe deploy web --dry-run && dotframe deploy web --yes
|
package/skill-data/ios/SKILL.md
CHANGED
|
@@ -46,10 +46,15 @@ Templates ship this as `main.ios.ts` over the same `src/setup.ts` web and macOS
|
|
|
46
46
|
|
|
47
47
|
## Notes
|
|
48
48
|
|
|
49
|
+
- Crash with no message? `dotframe device logs ios` copies the newest crash report from the device and prints the exception and crashed thread. build ios also wraps init and frame so a TypeScript throw prints `dotframe: init threw: <message and stack>` to the device log before scriptc's trap aborts.
|
|
50
|
+
- scriptc library mode caps host callbacks at 32. build ios registers only the df* functions the entry reaches (a game without networking uses 27, with the native relay 29); the glue fails with the list if a game goes over.
|
|
51
|
+
- Textures: native has no fixed cap anymore; free what you replace with `gpu.destroyTexture(texture)` (its id is retired, do not draw it again).
|
|
52
|
+
- Online on iOS: `connectRelayNative` and `shareText` from `dotframe/src/native/relay` (see netplay).
|
|
53
|
+
|
|
49
54
|
- The scriptc runtime pack (`@scriptc/runtime-ios-arm64`) must match `scriptc --version`. Right after a scriptc release, bun's minimum release age blocks it; doctor prints the install command with `--minimum-release-age 0`.
|
|
50
55
|
- Touches never synthesize a mouse (SDL_HINT_TOUCH_MOUSE_EVENTS is off), so read `input.touches()` for fingers; `input.pointer()` stays the mouse.
|
|
51
56
|
- Size the logical canvas from `gpu.aspect()` (templates do): a 1280x720 canvas on a 2.16 phone stretches otherwise.
|
|
52
|
-
- Library mode has no `WebSocket
|
|
57
|
+
- Library mode has no browser `WebSocket`: use `dotframe/src/native/relay`, not `dotframe/src/relay-client`.
|
|
53
58
|
- Signing is automatic with `team`; a locked password manager can fail signing with `failed to fill whole buffer`: ask the human to unlock it and retry.
|
|
54
59
|
- `build ios --release` refuses assets marked local-only (see `assets`).
|
|
55
60
|
- Confirm on the device by asking the human. A successful install is not a working game.
|
|
@@ -60,8 +60,15 @@ Runtime traps (compile clean, fail when run):
|
|
|
60
60
|
|
|
61
61
|
- **Structural coercion copies.** Passing a class instance, or a wider record, to a parameter typed as a narrower structural type passes a copy: the callee's writes to scalar fields are lost (arrays inside still alias). In Craft Ones every projectile froze mid-flight. Pass the exact type, or return the updated record and copy it back. Reported to scriptc.
|
|
62
62
|
- **Out-of-range reads.** `rows[y - 1]?.[x]` typed as `string` but `undefined` at runtime crashes with "undefined is not representable in the target union". Bounds-check before reading.
|
|
63
|
+
- **Assigning `globalThis`** traps at startup ("expected object at $"). Keep web-only code (probe publishing, DOM) in web-only modules.
|
|
64
|
+
- **Passing a function whose parameter type differs** from the slot traps ("function invoked through a ... slot"): wrap it in a function of the exact type, for example `run(WINDOW, (platform: Platform): Frame => setup(platform))`. Passing a wider object as a direct argument works.
|
|
65
|
+
- **`doctor`'s `native:<target>` check compiles but does not run.** Run the binary after changes; CI runs each template binary for a few seconds for this reason.
|
|
63
66
|
- **`JSON.stringify` key order** differed from JavaScript in Craft Ones (not reproduced in a minimal case). Never build checksums or netplay comparisons from it.
|
|
64
67
|
|
|
68
|
+
## Networking
|
|
69
|
+
|
|
70
|
+
`dotframe/src/native/relay` (WebSocket over NSURLSession on macOS and iOS, WinHTTP on Windows). Inside a scriptc executable, hostnames and TLS hang (system services never answer there); IP addresses and localhost connect. `DF_WS_DEBUG=1` logs socket events.
|
|
71
|
+
|
|
65
72
|
## Debugging a native crash
|
|
66
73
|
|
|
67
74
|
`--optimization dev` builds failed to link in Craft Ones (undefined `_main`; a template game links fine). When that happens, debug the release binary with lldb: `lldb ./dist/macos/<name>`, `breakpoint set -n scr_error_new`, `run`, then `bt` shows the TypeScript call site of a runtime error.
|
|
@@ -15,7 +15,11 @@ dotframe relay serve # local relay on :8787, same protocol as p
|
|
|
15
15
|
dotframe play --online --frames 900 # two browsers, scripted match, checksums compared, screenshots saved
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`
|
|
18
|
+
Native builds connect with `dotframe/src/native/relay`: `connectRelayNative(url, room)` returns the same RelayLink, polled from the frame (no promises in library mode). `shareText(text)` opens the share sheet on iOS and copies to the clipboard on macOS and Windows. `play --online --native` makes peer 1 the macOS build (it reads DOTFRAME_ROOM, DOTFRAME_RELAY, DOTFRAME_MASH and writes its probe to DOTFRAME_PROBE; templates do), so a web peer and a native peer must agree on every checksum.
|
|
19
|
+
|
|
20
|
+
Known limit: inside a scriptc executable (macOS), hostnames and TLS never connect (Network.framework waits on system services that never answer there); IP addresses and localhost work, so local relays and LAN play do. Reported to scriptc. iOS runs as a library under SDL and is not affected the same way; verify on a device.
|
|
21
|
+
|
|
22
|
+
`play --online` needs the web entry to honor `?room=`, `?relay=` and `?mash=<seed>` and to publish `globalThis.__dotframe = { frame, confirmed, status, sums }` (sums: checksum per 30th confirmed frame). `dotframe/src/probe` does both: `createProbe()` keeps it (call `probe.update(rollback, link.status())` each step, or `probe.update(null, "local", frame)` offline) and `publishProbe(state)` from `dotframe/src/probe-web` exposes it (web only: assigning globalThis traps in scriptc), and `createMasher(seed, randomInput)` returns a function giving the scripted input for each step. `--seeds 7,42` picks each peer's seed. The templates show the wiring.
|
|
19
23
|
|
|
20
24
|
dotframe online play is rollback netcode: each peer predicts the remote input (repeat the last one), simulates ahead, and when the real input arrives and differs, restores a snapshot and resimulates. It only works if every peer computes bit-identical state from the same inputs.
|
|
21
25
|
|
package/src/draw2d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { BufferUsage, type Color, type Draw, type RenderGpu, type Texture, Verte
|
|
|
6
6
|
|
|
7
7
|
export interface Draw2D {
|
|
8
8
|
begin: () => void;
|
|
9
|
+
// Changes the logical canvas size (for a window that resizes); coordinates map onto the whole surface.
|
|
10
|
+
resize: (width: number, height: number) => void;
|
|
9
11
|
end: (clear: Color) => void;
|
|
10
12
|
save: () => void;
|
|
11
13
|
restore: () => void;
|
|
@@ -652,6 +654,11 @@ export function createDraw2D(gpu: RenderGpu, width: number, height: number): Dra
|
|
|
652
654
|
};
|
|
653
655
|
|
|
654
656
|
return {
|
|
657
|
+
resize: (w: number, h: number): void => {
|
|
658
|
+
uniforms[0] = w;
|
|
659
|
+
uniforms[1] = h;
|
|
660
|
+
gpu.writeBuffer(uniformBuffer, new Uint8Array(uniforms.buffer, uniforms.byteOffset, uniforms.byteLength));
|
|
661
|
+
},
|
|
655
662
|
begin: (): void => {
|
|
656
663
|
vertexCount = 0;
|
|
657
664
|
batches.length = 0;
|
package/src/gpu.ts
CHANGED
|
@@ -62,6 +62,9 @@ export interface RenderGpu {
|
|
|
62
62
|
bind: (pipeline: number, buffer: number, texture: number) => number;
|
|
63
63
|
// smooth: linear filtering (fonts, photos); otherwise nearest (pixel art).
|
|
64
64
|
createTexture: (width: number, height: number, rgba: Uint8Array, smooth: boolean) => Texture;
|
|
65
|
+
// Frees a texture (and, natively, the bind groups sampling it). Its id is never reused; do not draw it again.
|
|
66
|
+
// Optional so stub GPUs in tests need not implement it.
|
|
67
|
+
destroyTexture?: (texture: Texture) => void;
|
|
65
68
|
frame: (clear: Color, draws: Draw[]) => void;
|
|
66
69
|
aspect: () => number;
|
|
67
70
|
}
|
package/src/native/backend.ts
CHANGED
|
@@ -17,16 +17,13 @@ import {
|
|
|
17
17
|
dfHeight,
|
|
18
18
|
dfImage,
|
|
19
19
|
dfKeyDown,
|
|
20
|
-
dfMasterVolume,
|
|
21
20
|
dfMouse,
|
|
22
|
-
dfMusicPause,
|
|
23
|
-
dfMusicPlay,
|
|
24
|
-
dfMusicStop,
|
|
25
|
-
dfMusicVolume,
|
|
26
21
|
dfPipeline,
|
|
22
|
+
dfMusic,
|
|
27
23
|
dfPlay,
|
|
28
24
|
dfSound,
|
|
29
25
|
dfTexture,
|
|
26
|
+
dfTextureDestroy,
|
|
30
27
|
dfTextureSize,
|
|
31
28
|
dfTone,
|
|
32
29
|
dfTouch,
|
|
@@ -73,6 +70,7 @@ export function createNativeRenderGpu(): RenderGpu {
|
|
|
73
70
|
if (id < 0) throw new Error(`dfTexture failed: ${id}`);
|
|
74
71
|
return { id, width, height };
|
|
75
72
|
},
|
|
73
|
+
destroyTexture: (texture: Texture): void => dfTextureDestroy(texture.id),
|
|
76
74
|
frame: (clear: Color, draws: Draw[]): void => {
|
|
77
75
|
// Pipelines without depth cannot run in a pass with a depth attachment.
|
|
78
76
|
let usesDepth = false;
|
|
@@ -129,12 +127,12 @@ export function createNativeAudioPlayer(): AudioPlayer {
|
|
|
129
127
|
play: (sound: number, volume: number, rate: number): void => dfPlay(sound, volume, rate),
|
|
130
128
|
tone: (frequency: number, duration: number, volume: number): void => dfTone(frequency, duration, volume),
|
|
131
129
|
playMusic: (track: number, loop: boolean, volume: number): void => {
|
|
132
|
-
|
|
130
|
+
dfMusic(0, track, loop ? 1 : 0, volume);
|
|
133
131
|
},
|
|
134
|
-
stopMusic: (): void =>
|
|
135
|
-
pauseMusic: (paused: boolean): void =>
|
|
136
|
-
setMusicVolume: (volume: number): void =>
|
|
137
|
-
setMasterVolume: (volume: number): void =>
|
|
132
|
+
stopMusic: (): void => void dfMusic(1, 0, 0, 0),
|
|
133
|
+
pauseMusic: (paused: boolean): void => void dfMusic(2, paused ? 1 : 0, 0, 0),
|
|
134
|
+
setMusicVolume: (volume: number): void => void dfMusic(3, volume, 0, 0),
|
|
135
|
+
setMasterVolume: (volume: number): void => void dfMusic(4, volume, 0, 0),
|
|
138
136
|
};
|
|
139
137
|
}
|
|
140
138
|
|
package/src/native/ffi.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare function dfPipeline(wgsl: string, stride: number, attributes: Uin
|
|
|
14
14
|
export declare function dfBind(pipeline: number, buffer: number, texture: number): number;
|
|
15
15
|
export declare function dfTexture(width: number, height: number, rgba: Uint8Array, smooth: boolean): number;
|
|
16
16
|
export declare function dfImage(png: Uint8Array, smooth: boolean): number;
|
|
17
|
+
export declare function dfTextureDestroy(texture: number): void;
|
|
17
18
|
export declare function dfTextureSize(texture: number, axis: number): number;
|
|
18
19
|
export declare function dfPoll(): boolean;
|
|
19
20
|
export declare function dfTouchCount(): number;
|
|
@@ -34,9 +35,12 @@ export declare function dfSound(mp3: Uint8Array): number;
|
|
|
34
35
|
export declare function dfPlay(sound: number, volume: number, rate: number): void;
|
|
35
36
|
export declare function dfTone(frequency: number, duration: number, volume: number): void;
|
|
36
37
|
export declare function dfTrack(mp3: Uint8Array): number;
|
|
37
|
-
export declare function dfMusicPlay(track: number, loop: boolean, volume: number): number;
|
|
38
|
-
export declare function dfMusicStop(): void;
|
|
39
|
-
export declare function dfMusicPause(paused: boolean): void;
|
|
40
|
-
export declare function dfMusicVolume(volume: number): void;
|
|
41
|
-
export declare function dfMasterVolume(volume: number): void;
|
|
42
38
|
export declare function dfAudioActive(): number;
|
|
39
|
+
// Music and volume in one call (library mode caps host callbacks at 32): op 0 play track a, loop when b != 0, at
|
|
40
|
+
// volume c; 1 stop; 2 pause when a != 0, else resume; 3 music volume a; 4 master volume a.
|
|
41
|
+
export declare function dfMusic(op: number, a: number, b: number, c: number): number;
|
|
42
|
+
// WebSocket client and share (df_ws_apple.m, df_ws_win.c), two calls because library mode caps host callbacks at
|
|
43
|
+
// 32. dfWsText: op 0 open url, 1 send text on socket, 2 share text. dfWs: op 0 state, 1 next length, 2 byte, 3 pop,
|
|
44
|
+
// 4 close. See native/relay.ts.
|
|
45
|
+
export declare function dfWsText(op: number, socket: number, text: string): number;
|
|
46
|
+
export declare function dfWs(op: number, socket: number, arg: number): number;
|