/* =========================================================================== IceTech GPL Source Code Copyright (C) 2026 Justin Marshall This file is part of the IceTech GPL Source Code. =========================================================================== */ #include "precompiled.h" #pragma hdrstop #include "MinigameHost.h" #include "../renderer/RenderSystem.h" idMinigameInstance::idMinigameInstance() { moduleHandle = 0; memset( &exports, 0, sizeof( exports ) ); memset( &imports, 0, sizeof( imports ) ); instance = NULL; memset( &threadInfo, 0, sizeof( threadInfo ) ); InitializeCriticalSection( &criticalSection ); threadStarted = false; shutdownRequested = false; active = false; activeSent = false; failed = false; frameReady = false; framebuffer = NULL; framebufferWidth = 0; framebufferHeight = 0; framebufferBytes = 0; } idMinigameInstance::~idMinigameInstance() { Shutdown(); DeleteCriticalSection( &criticalSection ); } void idMinigameInstance::Lock() { EnterCriticalSection( &criticalSection ); } void idMinigameInstance::Unlock() { LeaveCriticalSection( &criticalSection ); } void idMinigameInstance::PrintCallback( void *userData, const char *text ) { if ( text && text[0] ) { common->Printf( "minigame: %s", text ); } } void idMinigameInstance::SubmitAudioCallback( void *userData, const iceMinigameAudioBuffer_t *audio ) { idMinigameInstance *self = reinterpret_cast( userData ); if ( !self || !audio || !audio->samples || audio->frames <= 0 ) { return; } // The ABI already carries raw PCM across the DLL boundary. The engine mixer // bridge will consume this callback once the minigame sound emitter lands. } bool idMinigameInstance::Start( const char *module, const char *image, const char *cmdLine ) { Shutdown(); if ( !module || !module[0] || !image || !image[0] ) { failed = true; return false; } moduleName = module; imageName = image; commandLine = cmdLine ? cmdLine : ""; failed = false; shutdownRequested = false; idStr relativePath; if ( moduleName.Find( "/" ) >= 0 || moduleName.Find( "\\" ) >= 0 ) { relativePath = moduleName; } else { relativePath = "minigames/"; relativePath += moduleName; } if ( relativePath.Find( "." ) < 0 ) { relativePath += ".minigame"; } resolvedPath = fileSystem->RelativePathToOSPath( relativePath.c_str(), "fs_basepath" ); moduleHandle = Sys_DLL_Load( resolvedPath.c_str() ); if ( !moduleHandle ) { common->Warning( "Could not load minigame '%s' from '%s'", moduleName.c_str(), resolvedPath.c_str() ); failed = true; return false; } iceMinigameGetAPI_t GetMinigameAPI = (iceMinigameGetAPI_t)Sys_DLL_GetProcAddress( moduleHandle, ICE_MINIGAME_ENTRY_POINT ); if ( !GetMinigameAPI ) { common->Warning( "Minigame '%s' does not export %s", moduleName.c_str(), ICE_MINIGAME_ENTRY_POINT ); Shutdown(); failed = true; return false; } imports.version = ICE_MINIGAME_API_VERSION; imports.userData = this; importBasePath = fileSystem->RelativePathToOSPath( "", "fs_basepath" ); importSavePath = fileSystem->RelativePathToOSPath( "", "fs_savepath" ); imports.basePath = importBasePath.c_str(); imports.savePath = importSavePath.c_str(); imports.Print = PrintCallback; imports.SubmitAudio = SubmitAudioCallback; if ( !GetMinigameAPI( &imports, &exports ) || exports.version != ICE_MINIGAME_API_VERSION || !exports.CreateInstance || !exports.DestroyInstance || !exports.RunFrame || !exports.GetFramebuffer ) { common->Warning( "Minigame '%s' has an incompatible API", moduleName.c_str() ); Shutdown(); failed = true; return false; } if ( !exports.CreateInstance( &imports, commandLine.c_str(), &instance ) || !instance ) { common->Warning( "Minigame '%s' failed to create an instance", moduleName.c_str() ); Shutdown(); failed = true; return false; } if ( renderSystem && exports.framebufferWidth > 0 && exports.framebufferHeight > 0 ) { renderSystem->CreateScratchImage( imageName.c_str(), exports.framebufferWidth, exports.framebufferHeight ); } Sys_CreateThread( ThreadProc, this, THREAD_NORMAL, threadInfo, "minigame", g_threads, &g_thread_count ); threadStarted = true; return true; } void idMinigameInstance::Shutdown() { shutdownRequested = true; if ( threadStarted && threadInfo.threadHandle ) { Sys_DestroyThread( threadInfo ); } threadStarted = false; shutdownRequested = false; if ( exports.DestroyInstance && instance ) { exports.DestroyInstance( instance ); } instance = NULL; if ( moduleHandle ) { Sys_DLL_Unload( moduleHandle ); } moduleHandle = 0; memset( &exports, 0, sizeof( exports ) ); Lock(); pendingEvents.Clear(); ClearFramebuffer(); frameReady = false; active = false; activeSent = false; Unlock(); } void idMinigameInstance::SetActive( bool setActive ) { Lock(); active = setActive; Unlock(); } void idMinigameInstance::QueueEvent( const sysEvent_t *event, int time ) { if ( !event || !instance ) { return; } iceMinigameInputEvent_t minigameEvent; minigameEvent.type = ICE_MINIGAME_EVENT_NONE; minigameEvent.value = event->evValue; minigameEvent.value2 = event->evValue2; minigameEvent.time = time; switch ( event->evType ) { case SE_KEY: minigameEvent.type = ICE_MINIGAME_EVENT_KEY; break; case SE_CHAR: minigameEvent.type = ICE_MINIGAME_EVENT_CHAR; break; case SE_MOUSE: minigameEvent.type = ICE_MINIGAME_EVENT_MOUSE; break; default: return; } Lock(); if ( pendingEvents.Num() < 256 ) { pendingEvents.Append( minigameEvent ); } Unlock(); } bool idMinigameInstance::UploadFramebuffer() { if ( !renderSystem || !instance ) { return false; } Lock(); if ( !frameReady || !framebuffer || framebufferWidth <= 0 || framebufferHeight <= 0 ) { Unlock(); return false; } const bool uploaded = renderSystem->UploadImage( imageName.c_str(), framebuffer, framebufferWidth, framebufferHeight ); frameReady = false; Unlock(); return uploaded; } unsigned int idMinigameInstance::ThreadProc( void *parm ) { idMinigameInstance *self = reinterpret_cast( parm ); return self ? self->RunThread() : 0; } unsigned int idMinigameInstance::RunThread() { int lastTime = Sys_Milliseconds(); while ( !shutdownRequested ) { iceMinigameInputEvent_t events[256]; int numEvents = 0; bool nowActive; bool sendActive; Lock(); nowActive = active; sendActive = ( activeSent != active ); activeSent = active; numEvents = pendingEvents.Num(); if ( numEvents > 256 ) { numEvents = 256; } for ( int i = 0; i < numEvents; i++ ) { events[i] = pendingEvents[i]; } pendingEvents.Clear(); Unlock(); if ( sendActive && exports.SetActive ) { exports.SetActive( instance, nowActive ? 1 : 0 ); } for ( int i = 0; i < numEvents; i++ ) { if ( exports.QueueEvent ) { exports.QueueEvent( instance, &events[i] ); } } const int now = Sys_Milliseconds(); const int elapsed = Max( 1, now - lastTime ); lastTime = now; if ( nowActive || numEvents > 0 ) { exports.RunFrame( instance, elapsed ); iceMinigameFramebuffer_t framebufferInfo; memset( &framebufferInfo, 0, sizeof( framebufferInfo ) ); if ( exports.GetFramebuffer( instance, &framebufferInfo ) ) { CopyFramebuffer( framebufferInfo ); } } Sys_Sleep( 1 ); } return 0; } void idMinigameInstance::CopyFramebuffer( const iceMinigameFramebuffer_t &source ) { if ( !source.pixels || source.width <= 0 || source.height <= 0 || source.pitchBytes <= 0 || source.pixelFormat != ICE_MINIGAME_PIXEL_RGBA8 ) { return; } const int rowBytes = source.width * 4; const int requiredBytes = rowBytes * source.height; Lock(); if ( requiredBytes != framebufferBytes ) { ClearFramebuffer(); framebuffer = (byte *)Mem_Alloc( requiredBytes ); framebufferBytes = requiredBytes; } if ( framebuffer ) { for ( int y = 0; y < source.height; y++ ) { memcpy( framebuffer + y * rowBytes, source.pixels + y * source.pitchBytes, rowBytes ); } framebufferWidth = source.width; framebufferHeight = source.height; frameReady = true; } Unlock(); } void idMinigameInstance::ClearFramebuffer() { if ( framebuffer ) { Mem_Free( framebuffer ); } framebuffer = NULL; framebufferWidth = 0; framebufferHeight = 0; framebufferBytes = 0; }