Files
2026-05-22 20:01:49 -07:00

316 lines
8.1 KiB
C++

/*
===========================================================================
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<idMinigameInstance *>( 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<idMinigameInstance *>( 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;
}