Skip to content

Commit 391c228

Browse files
committed
Add built-in game store and menu system
- Store: browse and download carts from wasm4.org directly in the runtime - Menu: pause menu (Enter key) with Continue, Store, Reset options - No-arg launch: running wasm4 without a cart file opens the store - wasm3 fix: skip duplicate memory section for imported memory - Store UX: wrap-around navigation, cursor position remembered - Safe module loading with memory pointer refresh after load
1 parent 5910784 commit 391c228

File tree

13 files changed

+1127
-33
lines changed

13 files changed

+1127
-33
lines changed

runtimes/native/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ endif ()
120120
add_subdirectory(vendor/cubeb)
121121
endif ()
122122

123+
find_package(CURL REQUIRED)
124+
123125
file(GLOB COMMON_SOURCES RELATIVE "${CMAKE_SOURCE_DIR}" "src/*.c")
124126

125127
# Include a strnlen polyfill for some platforms where it's missing (OSX PPC, maybe others)
@@ -170,7 +172,8 @@ target_link_directories(wasm4 PRIVATE
170172
$<$<BOOL:${TOYWASM}>:${toywasm_tmp_install}/lib>)
171173
endif ()
172174

173-
target_link_libraries(wasm4 cubeb
175+
target_include_directories(wasm4 PRIVATE ${CURL_INCLUDE_DIRS})
176+
target_link_libraries(wasm4 cubeb CURL::libcurl pthread
174177
$<$<BOOL:${MINIFB}>:minifb>
175178
$<$<BOOL:${GLFW}>:glfw>
176179
$<$<BOOL:${TOYWASM}>:toywasm-core>)

runtimes/native/src/backend/main.c

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include "../apu.h"
88
#include "../runtime.h"
9+
#include "../store.h"
910
#include "../wasm.h"
1011
#include "../window.h"
1112
#include "../util.h"
@@ -126,38 +127,56 @@ int main (int argc, const char* argv[]) {
126127
char* diskPath = NULL;
127128

128129
if (argc < 2) {
130+
// Try bundled cart first
129131
FILE* file = fopen(argv[0], "rb");
130-
if (file == NULL) {
131-
goto usage;
132+
if (file != NULL) {
133+
fseek(file, -sizeof(FileFooter), SEEK_END);
134+
FileFooter footer;
135+
if (fread(&footer, 1, sizeof(FileFooter), file) >= sizeof(FileFooter) && footer.magic == 1414676803) {
136+
footer.title[sizeof(footer.title)-1] = '\0';
137+
title = footer.title;
138+
cartBytes = xmalloc(footer.cartLength);
139+
fseek(file, -sizeof(FileFooter) - footer.cartLength, SEEK_END);
140+
cartLength = fread(cartBytes, 1, footer.cartLength, file);
141+
fclose(file);
142+
diskPath = xmalloc(strlen(argv[0]) + sizeof(DISK_FILE_EXT));
143+
strcpy(diskPath, argv[0]);
144+
#ifdef _WIN32
145+
trimFileExtension(diskPath);
146+
#endif
147+
strcat(diskPath, DISK_FILE_EXT);
148+
loadDiskFile(&disk, diskPath);
149+
goto load_cart;
150+
}
151+
fclose(file);
132152
}
133-
fseek(file, -sizeof(FileFooter), SEEK_END);
134153

135-
FileFooter footer;
136-
if (fread(&footer, 1, sizeof(FileFooter), file) < sizeof(FileFooter) || footer.magic != 1414676803) {
137-
usage:
138-
// No bundled cart found
139-
fprintf(stderr, "Usage: wasm4 <cart>\n");
140-
return 1;
154+
// No bundled cart — launch store with minimal dummy cart
155+
{
156+
// Minimal WASM module: imports env.memory, exports empty start+update
157+
static const uint8_t dummyCart[] = {
158+
0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,0x01,0x04,0x01,0x60,0x00,0x00,0x02,0x0f,
159+
0x01,0x03,0x65,0x6e,0x76,0x06,0x6d,0x65,0x6d,0x6f,0x72,0x79,0x02,0x00,0x01,0x03,
160+
0x03,0x02,0x00,0x00,0x07,0x12,0x02,0x05,0x73,0x74,0x61,0x72,0x74,0x00,0x00,0x06,
161+
0x75,0x70,0x64,0x61,0x74,0x65,0x00,0x01,0x0a,0x07,0x02,0x02,0x00,0x0b,0x02,0x00,
162+
0x0b
163+
};
164+
audioInit();
165+
w4_storeInit();
166+
cartBytes = xmalloc(sizeof(dummyCart));
167+
memcpy(cartBytes, dummyCart, sizeof(dummyCart));
168+
cartLength = sizeof(dummyCart);
169+
170+
uint8_t* memory = w4_wasmInit();
171+
w4_runtimeInit(memory, &disk);
172+
w4_wasmLoadModule(cartBytes, cartLength);
173+
w4_storeOpen();
174+
w4_windowSetStoreMode(true);
175+
w4_windowBoot(title);
176+
audioUninit();
177+
return 0;
141178
}
142179

143-
// Make sure the title is null terminated
144-
footer.title[sizeof(footer.title)-1] = '\0';
145-
title = footer.title;
146-
147-
cartBytes = xmalloc(footer.cartLength);
148-
fseek(file, -sizeof(FileFooter) - footer.cartLength, SEEK_END);
149-
cartLength = fread(cartBytes, 1, footer.cartLength, file);
150-
fclose(file);
151-
152-
// Look for disk file
153-
diskPath = xmalloc(strlen(argv[0]) + sizeof(DISK_FILE_EXT));
154-
strcpy(diskPath, argv[0]);
155-
#ifdef _WIN32
156-
trimFileExtension(diskPath); // Trim .exe on Windows
157-
#endif
158-
strcat(diskPath, DISK_FILE_EXT);
159-
loadDiskFile(&disk, diskPath);
160-
161180
} else if (!strcmp(argv[1], "-") || !strcmp(argv[1], "/dev/stdin")) {
162181
size_t bufsize = 1024;
163182
cartBytes = xmalloc(bufsize);
@@ -205,7 +224,9 @@ int main (int argc, const char* argv[]) {
205224
loadDiskFile(&disk, diskPath);
206225
}
207226

227+
load_cart:
208228
audioInit();
229+
w4_storeInit();
209230

210231
uint8_t* memory = w4_wasmInit();
211232
w4_runtimeInit(memory, &disk);

runtimes/native/src/backend/wasm_wasm3.c

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,35 @@ static m3ApiRawFunction (tracef) {
155155
m3ApiSuccess();
156156
}
157157

158+
static bool storeLoaded = false;
159+
static bool cartCrashed = false;
160+
158161
static void check (M3Result result) {
159162
if (result != m3Err_none) {
160163
M3ErrorInfo info;
161164
m3_GetErrorInfo(runtime, &info);
162-
fprintf(stderr, "WASM error: %s (%s)\n", result, info.message);
163-
exit(1);
165+
if (storeLoaded) {
166+
fprintf(stderr, "WASM warning: %s (%s)\n", result, info.message);
167+
update = NULL;
168+
start = NULL;
169+
cartCrashed = true;
170+
} else {
171+
fprintf(stderr, "WASM error: %s (%s)\n", result, info.message);
172+
exit(1);
173+
}
174+
}
175+
}
176+
177+
bool w4_wasmDidCrash(void) {
178+
if (cartCrashed) {
179+
cartCrashed = false;
180+
return true;
164181
}
182+
return false;
183+
}
184+
185+
void w4_wasmSetStoreLoaded(bool value) {
186+
storeLoaded = value;
165187
}
166188

167189
uint8_t* w4_wasmInit () {
@@ -187,6 +209,15 @@ uint8_t* w4_wasmInit () {
187209
void w4_wasmDestroy () {
188210
m3_FreeRuntime(runtime);
189211
m3_FreeEnvironment(env);
212+
env = NULL;
213+
runtime = NULL;
214+
module = NULL;
215+
start = NULL;
216+
update = NULL;
217+
}
218+
219+
uint8_t* w4_wasmGetMemory () {
220+
return m3_GetMemory(runtime, NULL, 0);
190221
}
191222

192223
void w4_wasmLoadModule (const uint8_t* wasmBuffer, int byteLength) {
@@ -245,6 +276,70 @@ void w4_wasmLoadModule (const uint8_t* wasmBuffer, int byteLength) {
245276
}
246277
}
247278

279+
int w4_wasmLoadModuleSafe (const uint8_t* wasmBuffer, int byteLength) {
280+
M3Result result;
281+
storeLoaded = true;
282+
283+
result = m3_ParseModule(env, &module, wasmBuffer, byteLength);
284+
if (result) {
285+
fprintf(stderr, "WASM parse error: %s\n", result);
286+
return -1;
287+
}
288+
289+
module->memoryImported = true;
290+
291+
result = m3_LoadModule(runtime, module);
292+
if (result) {
293+
fprintf(stderr, "WASM load error: %s\n", result);
294+
return -1;
295+
}
296+
297+
m3_LinkRawFunction(module, "env", "blit", "v(iiiiii)", blit);
298+
m3_LinkRawFunction(module, "env", "blitSub", "v(iiiiiiiii)", blitSub);
299+
m3_LinkRawFunction(module, "env", "line", "v(iiii)", line);
300+
m3_LinkRawFunction(module, "env", "hline", "v(iii)", hline);
301+
m3_LinkRawFunction(module, "env", "vline", "v(iii)", vline);
302+
m3_LinkRawFunction(module, "env", "oval", "v(iiii)", oval);
303+
m3_LinkRawFunction(module, "env", "rect", "v(iiii)", rect);
304+
m3_LinkRawFunction(module, "env", "text", "v(iii)", text);
305+
m3_LinkRawFunction(module, "env", "textUtf8", "v(iiii)", textUtf8);
306+
m3_LinkRawFunction(module, "env", "textUtf16", "v(iiii)", textUtf16);
307+
m3_LinkRawFunction(module, "env", "tone", "v(iiii)", tone);
308+
m3_LinkRawFunction(module, "env", "diskr", "i(ii)", diskr);
309+
m3_LinkRawFunction(module, "env", "diskw", "i(ii)", diskw);
310+
m3_LinkRawFunction(module, "env", "trace", "v(i)", trace);
311+
m3_LinkRawFunction(module, "env", "traceUtf8", "v(ii)", traceUtf8);
312+
m3_LinkRawFunction(module, "env", "traceUtf16", "v(ii)", traceUtf16);
313+
m3_LinkRawFunction(module, "env", "tracef", "v(ii)", tracef);
314+
315+
m3_FindFunction(&start, runtime, "start");
316+
m3_FindFunction(&update, runtime, "update");
317+
318+
result = m3_RunStart(module);
319+
if (result) {
320+
fprintf(stderr, "WASM start error: %s\n", result);
321+
return -1;
322+
}
323+
324+
M3Function* func;
325+
m3_FindFunction(&func, runtime, "_start");
326+
if (func) {
327+
result = m3_CallV(func);
328+
if (result) {
329+
fprintf(stderr, "WASM _start warning: %s (ignored)\n", result);
330+
}
331+
}
332+
m3_FindFunction(&func, runtime, "_initialize");
333+
if (func) {
334+
result = m3_CallV(func);
335+
if (result) {
336+
fprintf(stderr, "WASM _initialize warning: %s (ignored)\n", result);
337+
}
338+
}
339+
340+
return 0;
341+
}
342+
248343
void w4_wasmCallStart () {
249344
if (start) {
250345
check(m3_CallV(start));

runtimes/native/src/backend/window_glfw.c

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
#include "../window.h"
1010
#include "../runtime.h"
11+
#include "../wasm.h"
12+
#include "../menu.h"
13+
#include "../store.h"
1114

1215
static uint32_t table[256];
1316
static GLuint paletteLocation;
@@ -24,6 +27,11 @@ static int viewportY;
2427
static int viewportSize;
2528

2629
static bool should_close = false;
30+
static bool storeMode = false; // true when launched without a cart
31+
32+
void w4_windowSetStoreMode (bool enabled) {
33+
storeMode = enabled;
34+
}
2735

2836
static void initLookupTable () {
2937
// Create a lookup table for each byte mapping to 4 bytes:
@@ -183,8 +191,32 @@ static void update (GLFWwindow* window) {
183191
w4_runtimeSetGamepad(0, gamepad);
184192

185193
if (glfwGetKey(window, GLFW_KEY_ESCAPE)) {
186-
should_close = true;
194+
if (w4_storeIsOpen()) {
195+
if (storeMode) {
196+
should_close = true;
197+
} else {
198+
w4_storeClose();
199+
}
200+
} else if (w4_menuIsOpen()) {
201+
w4_menuClose();
202+
} else {
203+
should_close = true;
204+
}
205+
}
206+
207+
// Enter toggles pause menu
208+
static int enterWasPressed = 0;
209+
int enterPressed = glfwGetKey(window, GLFW_KEY_ENTER);
210+
if (enterPressed && !enterWasPressed) {
211+
if (w4_storeIsOpen()) {
212+
// ignore Enter in store
213+
} else if (w4_menuIsOpen()) {
214+
w4_menuClose();
215+
} else {
216+
w4_menuOpen();
217+
}
187218
}
219+
enterWasPressed = enterPressed;
188220

189221
// Mouse handling
190222
double mouseX, mouseY;
@@ -201,7 +233,52 @@ static void update (GLFWwindow* window) {
201233
}
202234
w4_runtimeSetMouse(160*(mouseX-contentX)/contentSizeX, 160*(mouseY-contentY)/contentSizeY, mouseButtons);
203235

204-
w4_runtimeUpdate();
236+
if (w4_storeIsOpen()) {
237+
w4_storeInput(gamepad);
238+
239+
// Store was closed via Z button without selecting a cart
240+
if (!w4_storeIsOpen() && storeMode) {
241+
should_close = true;
242+
return;
243+
}
244+
245+
// Check if a cart was downloaded
246+
int cartLen = 0;
247+
uint8_t* cartData = w4_storeGetSelectedCart(&cartLen);
248+
if (cartData) {
249+
w4_storeJoinThread();
250+
fprintf(stderr, "[store] Loading cart (%d bytes)\n", cartLen);
251+
w4_wasmDestroy();
252+
uint8_t* mem = w4_wasmInit();
253+
static w4_Disk storeDisk = {0};
254+
storeDisk.size = 0;
255+
w4_runtimeInit(mem, &storeDisk);
256+
w4_wasmSetStoreLoaded(true);
257+
w4_wasmLoadModule(cartData, cartLen);
258+
// Note: cartData must NOT be freed — wasm3 holds pointers into it
259+
// Refresh memory pointer — wasm3 may realloc during module load
260+
w4_runtimeSetMemory(w4_wasmGetMemory());
261+
storeMode = false;
262+
}
263+
} else if (w4_menuIsOpen()) {
264+
w4_menuInput(gamepad);
265+
int action = w4_menuGetAction();
266+
switch (action) {
267+
case MENU_ACTION_CONTINUE:
268+
w4_menuClose();
269+
break;
270+
case MENU_ACTION_STORE:
271+
w4_menuClose();
272+
w4_storeOpen();
273+
break;
274+
}
275+
} else {
276+
w4_runtimeUpdate();
277+
if (w4_wasmDidCrash()) {
278+
fprintf(stderr, "[store] Cart crashed, opening store\n");
279+
w4_storeOpen();
280+
}
281+
}
205282
}
206283

207284
void w4_windowBoot (const char* title) {
@@ -242,6 +319,19 @@ void w4_windowBoot (const char* title) {
242319
}
243320

244321
update(window);
322+
323+
if (w4_storeIsOpen()) {
324+
static uint32_t storePalette[4];
325+
static uint8_t storeFb[160*160/4];
326+
w4_storeRender(storePalette, storeFb);
327+
w4_windowComposite(storePalette, storeFb);
328+
} else if (w4_menuIsOpen()) {
329+
static uint32_t menuPalette[4];
330+
static uint8_t menuFb[160*160/4];
331+
w4_menuRender(menuPalette, menuFb);
332+
w4_windowComposite(menuPalette, menuFb);
333+
}
334+
245335
glfwSwapBuffers(window);
246336
glfwPollEvents();
247337

0 commit comments

Comments
 (0)