diff -ruN a/mingin_web.h b/mingin_web.h --- a/mingin_web.h 1969-12-31 21:00:00.000000000 -0300 +++ b/mingin_web.h 2026-11-09 16:53:39.363836615 -0300 @@ -0,0 +1,915 @@ +/* + Mingin platform for the browser (Emscripten + SDL2), written for fredygames.com. + + Mingin is by Jason Rohrer and not copyrighted ("I place it into the public domain"); this file is + also placed in the public domain. + + Included by mingin.h (see mingin-web.patch) in the slot of the "dummy example implementation", in + the same translation unit as the rest of Mingin, so it can use Mingin's internal static helpers. + + - video: the game's RGB pixels go to a streaming texture of an SDL2 renderer (WebGL) on the canvas + - input: SDL2 keyboard (by physical key) and mouse; no gamepads, no fullscreen toggle + - audio: SDL2 audio callback (signed 16-bit stereo, 44100 Hz) + - time: 60 steps per second, paced by requestAnimationFrame + - persistent data in "settings/" and bulk data in "data/" of the Emscripten file system (the page + mounts IndexedDB on settings/; data/ is preloaded) +*/ + +#include +#include +#include +#include +#include +#include + +#define MINGIN_WEB_STEPS_PER_SECOND 60 +#define MINGIN_WEB_MAX_FILES 64 +#define MINGIN_WEB_AUDIO_RATE 44100 +#define MINGIN_WEB_AUDIO_FRAMES 2048 + +static SDL_Window *mn_webWindow = NULL; +static SDL_Renderer *mn_webRenderer = NULL; +static SDL_Texture *mn_webTexture = NULL; +static SDL_AudioDeviceID mn_webAudio = 0; +static int mn_webW = 0; +static int mn_webH = 0; +static char mn_shouldQuit = 0; +static char mn_areWeInStepFunction = 0; +static char mn_webFirstStepDone = 0; +static double mn_webStartTime = 0; +static double mn_webNextStepTime = 0; + +/* game's expected buffer is RGB */ +static unsigned char mn_gameScreenBuffer[ MINGIN_MAX_SCREEN_W * + MINGIN_MAX_SCREEN_H * 3 ]; + +/* button state, same scheme as the other platforms: a button pressed and released between two + steps still counts as down during the next step */ +static MinginButton mn_lastButtonPressed = MGN_BUTTON_NONE; +static char mn_buttonDown[ MGN_NUM_BUTTONS ]; +static char mn_buttonWasDownSinceLastStep[ MGN_NUM_BUTTONS ]; + +static int mn_mouseX = 0; +static int mn_mouseY = 0; +static char mn_mouseInside = 0; + + + +static void mn_setButtonState( MinginButton inButton, + char inDown ) { + + if( inButton <= MGN_BUTTON_NONE + || + inButton >= MGN_DUMMY_LAST_BUTTON ) { + return; + } + + mn_buttonDown[ inButton ] = inDown; + + if( inDown ) { + mn_buttonWasDownSinceLastStep[ inButton ] = 1; + mn_lastButtonPressed = inButton; + } + } + + + +static void mn_releaseAllButtons( void ) { + int b; + + for( b = 0; + b < MGN_NUM_BUTTONS; + b ++ ) { + mn_buttonDown[ b ] = 0; + } + } + + + +/* the physical keys (like X11's unshifted keysyms: the raw key, not the character it types) */ +static MinginButton mn_mapScancode( SDL_Scancode inScancode ) { + + if( inScancode >= SDL_SCANCODE_A + && + inScancode <= SDL_SCANCODE_Z ) { + return (MinginButton)( MGN_KEY_A + ( inScancode - SDL_SCANCODE_A ) ); + } + if( inScancode >= SDL_SCANCODE_1 + && + inScancode <= SDL_SCANCODE_9 ) { + return (MinginButton)( MGN_KEY_1 + ( inScancode - SDL_SCANCODE_1 ) ); + } + if( inScancode >= SDL_SCANCODE_F1 + && + inScancode <= SDL_SCANCODE_F12 ) { + return (MinginButton)( MGN_KEY_F1 + ( inScancode - SDL_SCANCODE_F1 ) ); + } + + switch( inScancode ) { + case SDL_SCANCODE_0: return MGN_KEY_0; + case SDL_SCANCODE_BACKSPACE: return MGN_KEY_BACKSPACE; + case SDL_SCANCODE_TAB: return MGN_KEY_TAB; + case SDL_SCANCODE_RETURN: return MGN_KEY_RETURN; + case SDL_SCANCODE_KP_ENTER: return MGN_KEY_RETURN; + case SDL_SCANCODE_ESCAPE: return MGN_KEY_ESCAPE; + case SDL_SCANCODE_DELETE: return MGN_KEY_DELETE; + case SDL_SCANCODE_HOME: return MGN_KEY_HOME; + case SDL_SCANCODE_LEFT: return MGN_KEY_LEFT; + case SDL_SCANCODE_UP: return MGN_KEY_UP; + case SDL_SCANCODE_RIGHT: return MGN_KEY_RIGHT; + case SDL_SCANCODE_DOWN: return MGN_KEY_DOWN; + case SDL_SCANCODE_PAGEUP: return MGN_KEY_PAGE_UP; + case SDL_SCANCODE_PAGEDOWN: return MGN_KEY_PAGE_DOWN; + case SDL_SCANCODE_END: return MGN_KEY_END; + case SDL_SCANCODE_NUMLOCKCLEAR: return MGN_KEY_NUM_LOCK; + case SDL_SCANCODE_LSHIFT: return MGN_KEY_SHIFT_L; + case SDL_SCANCODE_RSHIFT: return MGN_KEY_SHIFT_R; + case SDL_SCANCODE_LCTRL: return MGN_KEY_CONTROL_L; + case SDL_SCANCODE_RCTRL: return MGN_KEY_CONTROL_R; + case SDL_SCANCODE_CAPSLOCK: return MGN_KEY_CAPS_LOCK; + case SDL_SCANCODE_LALT: return MGN_KEY_ALT_L; + case SDL_SCANCODE_RALT: return MGN_KEY_ALT_R; + case SDL_SCANCODE_LGUI: return MGN_KEY_SUPER_L; + case SDL_SCANCODE_RGUI: return MGN_KEY_SUPER_R; + case SDL_SCANCODE_SPACE: return MGN_KEY_SPACE; + case SDL_SCANCODE_MINUS: return MGN_KEY_MINUS; + case SDL_SCANCODE_EQUALS: return MGN_KEY_EQUAL; + case SDL_SCANCODE_LEFTBRACKET: return MGN_KEY_BRACKET_L; + case SDL_SCANCODE_RIGHTBRACKET: return MGN_KEY_BRACKET_R; + case SDL_SCANCODE_BACKSLASH: return MGN_KEY_BACKSLASH; + case SDL_SCANCODE_SEMICOLON: return MGN_KEY_SEMICOLON; + case SDL_SCANCODE_APOSTROPHE: return MGN_KEY_APOSTROPHE; + case SDL_SCANCODE_GRAVE: return MGN_KEY_BACK_TICK; + case SDL_SCANCODE_COMMA: return MGN_KEY_COMMA; + case SDL_SCANCODE_PERIOD: return MGN_KEY_PERIOD; + case SDL_SCANCODE_SLASH: return MGN_KEY_SLASH; + default: return MGN_BUTTON_NONE; + } + } + + + +static void mn_webPollEvents( void ) { + + SDL_Event e; + + while( SDL_PollEvent( &e ) ) { + switch( e.type ) { + case SDL_QUIT: + mn_shouldQuit = 1; + break; + case SDL_KEYDOWN: + case SDL_KEYUP: + if( e.key.repeat == 0 ) { + mn_setButtonState( mn_mapScancode( e.key.keysym.scancode ), + e.type == SDL_KEYDOWN ); + } + break; + case SDL_MOUSEMOTION: + mn_mouseX = e.motion.x; + mn_mouseY = e.motion.y; + mn_mouseInside = 1; + break; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: { + MinginButton b = MGN_BUTTON_NONE; + + mn_mouseX = e.button.x; + mn_mouseY = e.button.y; + mn_mouseInside = 1; + + if( e.button.button == SDL_BUTTON_LEFT ) { + b = MGN_BUTTON_MOUSE_LEFT; + } + else if( e.button.button == SDL_BUTTON_MIDDLE ) { + b = MGN_BUTTON_MOUSE_MIDDLE; + } + else if( e.button.button == SDL_BUTTON_RIGHT ) { + b = MGN_BUTTON_MOUSE_RIGHT; + } + mn_setButtonState( b, + e.type == SDL_MOUSEBUTTONDOWN ); + break; + } + case SDL_WINDOWEVENT: + if( e.window.event == SDL_WINDOWEVENT_LEAVE ) { + mn_mouseInside = 0; + } + else if( e.window.event == SDL_WINDOWEVENT_FOCUS_LOST ) { + mn_releaseAllButtons(); + } + break; + default: + break; + } + } + } + + + +static void mn_webAudioCallback( void *inUserData, + Uint8 *inStream, + int inLength ) { + (void)inUserData; + + if( ! mn_webFirstStepDone ) { + memset( inStream, 0, (size_t)inLength ); + return; + } + + minginGame_getAudioSamples( inLength / 4, + MINGIN_WEB_AUDIO_RATE, + inStream ); + } + + + +static void mn_webOpenAudio( void ) { + + SDL_AudioSpec want; + SDL_AudioSpec have; + + if( SDL_InitSubSystem( SDL_INIT_AUDIO ) != 0 ) { + return; + } + + SDL_zero( want ); + want.freq = MINGIN_WEB_AUDIO_RATE; + want.format = AUDIO_S16LSB; + want.channels = 2; + want.samples = MINGIN_WEB_AUDIO_FRAMES; + want.callback = mn_webAudioCallback; + + mn_webAudio = SDL_OpenAudioDevice( NULL, 0, &want, &have, 0 ); + + if( mn_webAudio != 0 ) { + SDL_PauseAudioDevice( mn_webAudio, 0 ); + } + else { + mingin_log( "Failed to open audio: " ); + mingin_log( SDL_GetError() ); + mingin_log( "\n" ); + } + } + + + +/* one browser frame: run the steps that are due (60 per second, whatever the display's rate), then + draw if the game stepped */ +static void mn_webFrame( void ) { + + double now = emscripten_get_now(); + double stepMs = 1000.0 / MINGIN_WEB_STEPS_PER_SECOND; + int stepped = 0; + int b; + + mn_webPollEvents(); + + /* a long pause (hidden tab) must not make the game run a pile of steps */ + if( now > mn_webNextStepTime + 4 * stepMs ) { + mn_webNextStepTime = now; + } + + while( now >= mn_webNextStepTime + && + ! mn_shouldQuit ) { + + mn_areWeInStepFunction = 1; + minginGame_step( 0 ); + mn_areWeInStepFunction = 0; + mn_webFirstStepDone = 1; + stepped ++; + + mn_webNextStepTime += stepMs; + + for( b = 0; + b < MGN_NUM_BUTTONS; + b ++ ) { + mn_buttonWasDownSinceLastStep[ b ] = 0; + } + } + + if( stepped > 0 ) { + minginGame_getScreenPixels( mn_webW, + mn_webH, + mn_gameScreenBuffer ); + SDL_UpdateTexture( mn_webTexture, + NULL, + mn_gameScreenBuffer, + mn_webW * 3 ); + SDL_RenderClear( mn_webRenderer ); + SDL_RenderCopy( mn_webRenderer, + mn_webTexture, + NULL, + NULL ); + SDL_RenderPresent( mn_webRenderer ); + } + + if( mn_shouldQuit ) { + emscripten_cancel_main_loop(); + exit( 0 ); + } + } + + + +int main( void ) { + + int gameW; + int gameH; + int scale; + int availW; + int availH; + + minginInternal_init(); + + mkdir( "settings", 0777 ); + + minginGame_getMinimumViableScreenSize( &gameW, + &gameH ); + + /* the largest whole multiple of the game's size that fits the page (the game scales its own + pixels by whole factors), but no more than 3: the game draws every pixel in software */ + availW = EM_ASM_INT( { return window.innerWidth * ( window.devicePixelRatio || 1 ); } ); + availH = EM_ASM_INT( { return window.innerHeight * ( window.devicePixelRatio || 1 ); } ); + + scale = availW / gameW; + if( availH / gameH < scale ) { + scale = availH / gameH; + } + if( scale < 1 ) { + scale = 1; + } + if( scale > 3 ) { + scale = 3; + } + + mn_webW = scale * gameW; + mn_webH = scale * gameH; + + if( mn_webW > MINGIN_MAX_SCREEN_W + || + mn_webH > MINGIN_MAX_SCREEN_H ) { + mn_webW = gameW; + mn_webH = gameH; + } + + SDL_SetHint( SDL_HINT_RENDER_SCALE_QUALITY, + "nearest" ); + + if( SDL_Init( SDL_INIT_VIDEO | SDL_INIT_EVENTS ) != 0 ) { + mingin_log( "Failed to init SDL\n" ); + return 1; + } + + mn_webWindow = SDL_CreateWindow( mn_getWindowTitle(), + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + mn_webW, + mn_webH, + SDL_WINDOW_SHOWN ); + mn_webRenderer = SDL_CreateRenderer( mn_webWindow, + -1, + 0 ); + mn_webTexture = SDL_CreateTexture( mn_webRenderer, + SDL_PIXELFORMAT_RGB24, + SDL_TEXTUREACCESS_STREAMING, + mn_webW, + mn_webH ); + + if( mn_webWindow == NULL + || + mn_webRenderer == NULL + || + mn_webTexture == NULL ) { + mingin_log( "Failed to open the window\n" ); + return 1; + } + + mn_webStartTime = emscripten_get_now(); + mn_webNextStepTime = mn_webStartTime; + + mn_webOpenAudio(); + + emscripten_set_main_loop( mn_webFrame, + 0, + 1 ); + + /* we don't use these static mingin all-platform internal functions + suppress warnings */ + (void)mn_getFlagSetting; + (void)mn_saveFlagSetting; + (void)mn_stringStartsWith; + (void)mn_stringsEqual; + (void)mn_stringLength; + (void)mn_intToString; + (void)mn_stringsEqualLimit; + + return 0; + } + + + +int mingin_getStepsPerSecond( void ) { + return MINGIN_WEB_STEPS_PER_SECOND; + } + + + +int mingin_getMillisecondsLeftInStep( void ) { + double left = mn_webNextStepTime - emscripten_get_now(); + + if( left < 0 ) { + return 0; + } + return (int)left; + } + + + +void mingin_getRunningTime( long *outSeconds, + long *outMilliseconds ) { + long ms = (long)( emscripten_get_now() - mn_webStartTime ); + + *outSeconds = ms / 1000; + *outMilliseconds = ms % 1000; + } + + + +unsigned long mingin_getEntropySeed( void ) { + return (unsigned long)emscripten_random() * 0x7FFFFFFFUL + ^ (unsigned long)( emscripten_get_now() * 1000 ); + } + + + +/* the audio callback runs on the same (only) thread as the game, never in the middle of a step */ +void mingin_lockAudio( void ) { + } + + + +void mingin_unlockAudio( void ) { + } + + + +char mingin_isSoundPlaying( void ) { + return mn_webAudio != 0; + } + + + +void mingin_quit( void ) { + if( ! mn_areWeInStepFunction ) { + mingin_log( "Error: calling mingin_quit from " + "outside minginGame_step function\n" ); + return; + } + mn_shouldQuit = 1; + } + + + +static char minginPlatform_isButtonDown( MinginButton inButton ) { + + if( inButton <= MGN_BUTTON_NONE + || + inButton >= MGN_DUMMY_LAST_BUTTON ) { + return 0; + } + + if( mn_buttonDown[ inButton ] + || + mn_buttonWasDownSinceLastStep[ inButton ] ) { + return 1; + } + + if( inButton == MGN_ANY_KEY_OR_BUTTON ) { + int i; + + for( i = 0; + i < MGN_NUM_BUTTONS; + i ++ ) { + if( mn_buttonDown[i] + || + mn_buttonWasDownSinceLastStep[i] ) { + return 1; + } + } + } + if( inButton == MGN_ANY_KEY ) { + int i; + + for( i = MGN_FIRST_KEYBOARD_KEY; + i <= MGN_LAST_KEYBOARD_KEY; + i ++ ) { + if( mn_buttonDown[i] + || + mn_buttonWasDownSinceLastStep[i] ) { + return 1; + } + } + } + + return 0; + } + + + +MinginButton mingin_getPlatformPrimaryButton( int inButtonHandle ) { + int i = 0; + MinginButton *mapping = mingin_getButtonMapping( inButtonHandle ); + + if( mapping == 0 ) { + return MGN_BUTTON_NONE; + } + + /* no gamepads here: the first mapped keyboard key or mouse button */ + while( mapping[ i ] != MGN_MAP_END ) { + MinginButton b = mapping[ i ]; + + if( ( b >= MGN_FIRST_KEYBOARD_KEY + && + b <= MGN_LAST_KEYBOARD_KEY ) + || + ( b >= MGN_FIRST_MOUSE_BUTTON + && + b <= MGN_LAST_MOUSE_BUTTON ) ) { + return b; + } + i ++; + } + return MGN_BUTTON_NONE; + } + + + +char mingin_getPointerLocation( int *outX, + int *outY, + int *outMaxX, + int *outMaxY ) { + if( ! mn_mouseInside ) { + return 0; + } + + *outX = mn_mouseX; + *outY = mn_mouseY; + *outMaxX = mn_webW; + *outMaxY = mn_webH; + + return 1; + } + + + +char minginPlatform_getStickPosition( MinginStick inStick, + int *outPosition, + int *outLowerLimit, + int *outUpperLimit ) { + (void)inStick; + (void)outPosition; + (void)outUpperLimit; + (void)outLowerLimit; + + return 0; + } + + + +char mingin_hasAnyGamepadBeenTouched( void ) { + return 0; + } + + + +void mingin_clearGamepadTouchedStatus( void ) { + } + + + +void mingin_log( const char *inString ) { + fputs( inString, + stdout ); + fflush( stdout ); + } + + + +/* the page is the window: no fullscreen of our own */ +char mingin_toggleFullscreen( char inFullscreen ) { + (void)inFullscreen; + return 0; + } + + + +char mingin_isFullscreen( void ) { + return 0; + } + + + +MinginButton mingin_getLastButtonPressed( void ) { + MinginButton last = mn_lastButtonPressed; + + mn_lastButtonPressed = MGN_BUTTON_NONE; + return last; + } + + + +/* + Persistent data and bulk data are files: settings/ and data/. + Handles are slots in a table, numbered from 1. +*/ + +static FILE *mn_persistFiles[ MINGIN_WEB_MAX_FILES ]; +static FILE *mn_bulkFiles[ MINGIN_WEB_MAX_FILES ]; + + + +static char mn_webMakePath( char *outPath, + const char *inDir, + const char *inName ) { + /* names are plain file names */ + if( strlen( inDir ) + strlen( inName ) + 2 > 512 + || + strchr( inName, '/' ) != NULL + || + strstr( inName, ".." ) != NULL ) { + return 0; + } + sprintf( outPath, "%s/%s", inDir, inName ); + return 1; + } + + + +static int mn_webOpen( FILE **inTable, + const char *inDir, + const char *inName, + const char *inMode, + int *outTotalBytes ) { + char path[ 520 ]; + FILE *f; + int i; + + if( ! mn_webMakePath( path, inDir, inName ) ) { + return -1; + } + + for( i = 0; + i < MINGIN_WEB_MAX_FILES; + i ++ ) { + if( inTable[i] == NULL ) { + break; + } + } + if( i == MINGIN_WEB_MAX_FILES ) { + return -1; + } + + f = fopen( path, + inMode ); + if( f == NULL ) { + return -1; + } + + if( outTotalBytes != NULL ) { + fseek( f, 0, SEEK_END ); + *outTotalBytes = (int)ftell( f ); + fseek( f, 0, SEEK_SET ); + } + + inTable[i] = f; + return i + 1; + } + + + +static FILE *mn_webGet( FILE **inTable, + int inHandle ) { + if( inHandle < 1 + || + inHandle > MINGIN_WEB_MAX_FILES ) { + return NULL; + } + return inTable[ inHandle - 1 ]; + } + + + +static void mn_webClose( FILE **inTable, + int inHandle ) { + FILE *f = mn_webGet( inTable, + inHandle ); + if( f != NULL ) { + fclose( f ); + inTable[ inHandle - 1 ] = NULL; + } + } + + + +static int mn_webRead( FILE *inFile, + int inNumBytes, + unsigned char *inBuffer ) { + size_t n; + + if( inFile == NULL + || + inNumBytes < 0 ) { + return -1; + } + n = fread( inBuffer, + 1, + (size_t)inNumBytes, + inFile ); + if( n < (size_t)inNumBytes + && + ferror( inFile ) ) { + return -1; + } + return (int)n; + } + + + +int mingin_startWritePersistData( const char *inStoreName ) { + return mn_webOpen( mn_persistFiles, "settings", inStoreName, "wb", NULL ); + } + + + +int mingin_startAppendPersistData( const char *inStoreName ) { + return mn_webOpen( mn_persistFiles, "settings", inStoreName, "ab", NULL ); + } + + + +int mingin_startReadPersistData( const char *inStoreName, + int *outTotalBytes ) { + return mn_webOpen( mn_persistFiles, "settings", inStoreName, "rb", outTotalBytes ); + } + + + +char mingin_writePersistData( int inStoreWriteHandle, + int inNumBytesToWrite, + const unsigned char *inByteBuffer ) { + FILE *f = mn_webGet( mn_persistFiles, + inStoreWriteHandle ); + + if( f == NULL + || + inNumBytesToWrite < 0 ) { + return 0; + } + return fwrite( inByteBuffer, + 1, + (size_t)inNumBytesToWrite, + f ) == (size_t)inNumBytesToWrite; + } + + + +int mingin_readPersistData( int inStoreReadHandle, + int inNumBytesToRead, + unsigned char *inByteBuffer ) { + return mn_webRead( mn_webGet( mn_persistFiles, inStoreReadHandle ), + inNumBytesToRead, + inByteBuffer ); + } + + + +char mingin_seekPersistData( int inStoreReadHandle, + int inAbsoluteBytePosition ) { + FILE *f = mn_webGet( mn_persistFiles, + inStoreReadHandle ); + + return f != NULL + && + fseek( f, inAbsoluteBytePosition, SEEK_SET ) == 0; + } + + + +int mingin_getPersistDataPosition( int inStoreReadHandle ) { + FILE *f = mn_webGet( mn_persistFiles, + inStoreReadHandle ); + + if( f == NULL ) { + return -1; + } + return (int)ftell( f ); + } + + + +void mingin_endWritePersistData( int inStoreWriteHandle ) { + mn_webClose( mn_persistFiles, + inStoreWriteHandle ); + } + + + +void mingin_endReadPersistData( int inStoreReadHandle ) { + mn_webClose( mn_persistFiles, + inStoreReadHandle ); + } + + + +void mingin_deletePersistData( const char *inStoreName ) { + char path[ 520 ]; + + if( mn_webMakePath( path, "settings", inStoreName ) ) { + remove( path ); + } + } + + + +char mingin_renamePersistData( const char *inStoreName, + const char *inStoreNewName ) { + char oldPath[ 520 ]; + char newPath[ 520 ]; + + if( ! mn_webMakePath( oldPath, "settings", inStoreName ) + || + ! mn_webMakePath( newPath, "settings", inStoreNewName ) ) { + return 0; + } + return rename( oldPath, + newPath ) == 0; + } + + + +int mingin_startReadBulkData( const char *inBulkName, + int *outTotalBytes ) { + return mn_webOpen( mn_bulkFiles, "data", inBulkName, "rb", outTotalBytes ); + } + + + +/* reads come from memory (the data/ folder is preloaded), so there is nothing to prefetch */ +void mingin_setBulkDataReadBuffer( int inBulkDataHandle, + int inBufferSize, + unsigned char *inBuffer ) { + (void)inBulkDataHandle; + (void)inBufferSize; + (void)inBuffer; + } + + + +int mingin_readBulkData( int inBulkDataHandle, + int inNumBytesToRead, + unsigned char *inByteBuffer ) { + return mn_webRead( mn_webGet( mn_bulkFiles, inBulkDataHandle ), + inNumBytesToRead, + inByteBuffer ); + } + + + +char mingin_seekBulkData( int inBulkDataHandle, + int inAbsoluteBytePosition ) { + FILE *f = mn_webGet( mn_bulkFiles, + inBulkDataHandle ); + + return f != NULL + && + fseek( f, inAbsoluteBytePosition, SEEK_SET ) == 0; + } + + + +int mingin_getBulkDataPosition( int inBulkDataHandle ) { + FILE *f = mn_webGet( mn_bulkFiles, + inBulkDataHandle ); + + if( f == NULL ) { + return -1; + } + return (int)ftell( f ); + } + + + +void mingin_endReadBulkData( int inBulkDataHandle ) { + mn_webClose( mn_bulkFiles, + inBulkDataHandle ); + } + + + +/* the data does not change while the game runs */ +char mingin_getBulkDataChanged( const char *inBulkName ) { + (void)inBulkName; + return 0; + } diff -ruN a/mingin.h b/mingin.h --- a/mingin.h 2026-11-09 16:53:39.348742701 -0300 +++ b/mingin.h 2026-11-09 16:53:39.362086956 -0300 @@ -10847,6 +10847,13 @@ /* end of _WIN32 case */ +#elif defined(__EMSCRIPTEN__) + +/* + The browser (FredyGames): see mingin_web.h +*/ +#include "mingin_web.h" + #else