Added minigame hook ups.

This commit is contained in:
Justin Marshall
2026-05-22 20:01:49 -07:00
parent e7b269b082
commit d24553b366
9 changed files with 563 additions and 0 deletions
+5
View File
@@ -409,6 +409,8 @@
<ClInclude Include="renderer\stb_image.h" />
<ClInclude Include="renderer\tr_local.h" />
<ClInclude Include="renderer\VertexCache.h" />
<ClInclude Include="minigame\Minigame_public.h" />
<ClInclude Include="minigame\MinigameHost.h" />
<ClInclude Include="sound\snd_local.h" />
<ClInclude Include="sound\sound.h" />
<ClInclude Include="sound\OggVorbis\vorbissrc\backends.h" />
@@ -483,6 +485,7 @@
<ClInclude Include="ui\ListGUILocal.h" />
<ClInclude Include="ui\ListWindow.h" />
<ClInclude Include="ui\MarkerWindow.h" />
<ClInclude Include="ui\MinigameWindow.h" />
<ClInclude Include="ui\RegExp.h" />
<ClInclude Include="ui\RegExp_old.h" />
<ClInclude Include="ui\RenderWindow.h" />
@@ -1616,6 +1619,7 @@
<ClCompile Include="renderer\tr_trace.cpp" />
<ClCompile Include="renderer\tr_trisurf.cpp" />
<ClCompile Include="renderer\VertexCache.cpp" />
<ClCompile Include="minigame\MinigameHost.cpp" />
<ClCompile Include="sound\snd_cache.cpp" />
<ClCompile Include="sound\snd_decoder.cpp" />
<ClCompile Include="sound\snd_efxfile.cpp" />
@@ -2857,6 +2861,7 @@
<ClCompile Include="ui\ListGUI.cpp" />
<ClCompile Include="ui\ListWindow.cpp" />
<ClCompile Include="ui\MarkerWindow.cpp" />
<ClCompile Include="ui\MinigameWindow.cpp" />
<ClCompile Include="ui\RegExp.cpp" />
<ClCompile Include="ui\RenderWindow.cpp" />
<ClCompile Include="ui\SimpleWindow.cpp" />
+18
View File
@@ -13,6 +13,9 @@
<Filter Include="Renderer">
<UniqueIdentifier>{19b363f5-76a7-4256-b7f0-7a6ec012d09d}</UniqueIdentifier>
</Filter>
<Filter Include="Minigame">
<UniqueIdentifier>{a48802f5-5084-4d4a-bd3d-7161c1885b64}</UniqueIdentifier>
</Filter>
<Filter Include="Sound">
<UniqueIdentifier>{2b6b512d-2388-41b9-8880-402e156ec20b}</UniqueIdentifier>
</Filter>
@@ -255,6 +258,12 @@
<ClInclude Include="renderer\VertexCache.h">
<Filter>Renderer</Filter>
</ClInclude>
<ClInclude Include="minigame\Minigame_public.h">
<Filter>Minigame</Filter>
</ClInclude>
<ClInclude Include="minigame\MinigameHost.h">
<Filter>Minigame</Filter>
</ClInclude>
<ClInclude Include="sound\snd_local.h">
<Filter>Sound</Filter>
</ClInclude>
@@ -519,6 +528,9 @@
<ClInclude Include="ui\MarkerWindow.h">
<Filter>Ui</Filter>
</ClInclude>
<ClInclude Include="ui\MinigameWindow.h">
<Filter>Ui</Filter>
</ClInclude>
<ClInclude Include="ui\RegExp.h">
<Filter>Ui</Filter>
</ClInclude>
@@ -812,6 +824,9 @@
<ClCompile Include="renderer\VertexCache.cpp">
<Filter>Renderer</Filter>
</ClCompile>
<ClCompile Include="minigame\MinigameHost.cpp">
<Filter>Minigame</Filter>
</ClCompile>
<ClCompile Include="sound\snd_cache.cpp">
<Filter>Sound</Filter>
</ClCompile>
@@ -1571,6 +1586,9 @@
<ClCompile Include="ui\MarkerWindow.cpp">
<Filter>Ui</Filter>
</ClCompile>
<ClCompile Include="ui\MinigameWindow.cpp">
<Filter>Ui</Filter>
</ClCompile>
<ClCompile Include="ui\RegExp.cpp">
<Filter>Ui</Filter>
</ClCompile>
+315
View File
@@ -0,0 +1,315 @@
/*
===========================================================================
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;
}
+73
View File
@@ -0,0 +1,73 @@
/*
===========================================================================
IceTech GPL Source Code
Copyright (C) 2026 Justin Marshall
This file is part of the IceTech GPL Source Code.
===========================================================================
*/
#ifndef __MINIGAME_HOST_H__
#define __MINIGAME_HOST_H__
#include "Minigame_public.h"
class idMinigameInstance {
public:
idMinigameInstance();
~idMinigameInstance();
bool Start( const char *moduleName, const char *imageName, const char *commandLine );
void Shutdown();
void SetActive( bool active );
void QueueEvent( const sysEvent_t *event, int time );
bool UploadFramebuffer();
const char * GetImageName() const { return imageName.c_str(); }
bool IsStarted() const { return instance != NULL; }
bool HasFailed() const { return failed; }
private:
static unsigned int ThreadProc( void *parm );
unsigned int RunThread();
void CopyFramebuffer( const iceMinigameFramebuffer_t &source );
void ClearFramebuffer();
void Lock();
void Unlock();
static void PrintCallback( void *userData, const char *text );
static void SubmitAudioCallback( void *userData, const iceMinigameAudioBuffer_t *audio );
INT_PTR moduleHandle;
iceMinigameExport_t exports;
iceMinigameImport_t imports;
iceMinigameInstance_t * instance;
idStr moduleName;
idStr imageName;
idStr commandLine;
idStr resolvedPath;
idStr importBasePath;
idStr importSavePath;
xthreadInfo threadInfo;
CRITICAL_SECTION criticalSection;
bool threadStarted;
volatile bool shutdownRequested;
bool active;
bool activeSent;
bool failed;
bool frameReady;
byte * framebuffer;
int framebufferWidth;
int framebufferHeight;
int framebufferBytes;
idList<iceMinigameInputEvent_t> pendingEvents;
};
#endif // __MINIGAME_HOST_H__
+108
View File
@@ -0,0 +1,108 @@
/*
===========================================================================
IceTech GPL Source Code
Copyright (C) 2026 Justin Marshall
This file is part of the IceTech GPL Source Code.
===========================================================================
*/
#ifndef __MINIGAME_PUBLIC_H__
#define __MINIGAME_PUBLIC_H__
#include <stdint.h>
#define ICE_MINIGAME_API_VERSION 1
#define ICE_MINIGAME_ENTRY_POINT "GetMinigameAPI"
#ifdef _WIN32
#ifdef __cplusplus
#define ICE_MINIGAME_EXPORT extern "C" __declspec(dllexport)
#else
#define ICE_MINIGAME_EXPORT __declspec(dllexport)
#endif
#else
#ifdef __cplusplus
#define ICE_MINIGAME_EXPORT extern "C"
#else
#define ICE_MINIGAME_EXPORT
#endif
#endif
typedef enum {
ICE_MINIGAME_EVENT_NONE = 0,
ICE_MINIGAME_EVENT_KEY,
ICE_MINIGAME_EVENT_CHAR,
ICE_MINIGAME_EVENT_MOUSE
} iceMinigameEventType_t;
typedef enum {
ICE_MINIGAME_PIXEL_RGBA8 = 1
} iceMinigamePixelFormat_t;
typedef struct iceMinigameInputEvent_s {
int type;
int value;
int value2;
int time;
} iceMinigameInputEvent_t;
typedef struct iceMinigameFramebuffer_s {
const uint8_t *pixels;
int width;
int height;
int pitchBytes;
int pixelFormat;
} iceMinigameFramebuffer_t;
typedef struct iceMinigameAudioBuffer_s {
const float *samples;
int frames;
int channels;
int sampleRate;
} iceMinigameAudioBuffer_t;
typedef void (*iceMinigamePrint_t)( void *userData, const char *text );
typedef void (*iceMinigameSubmitAudio_t)( void *userData, const iceMinigameAudioBuffer_t *audio );
typedef struct iceMinigameImport_s {
int version;
void *userData;
const char *basePath;
const char *savePath;
iceMinigamePrint_t Print;
iceMinigameSubmitAudio_t SubmitAudio;
} iceMinigameImport_t;
typedef struct iceMinigameInstance_s iceMinigameInstance_t;
typedef int (*iceMinigameCreateInstance_t)(
const iceMinigameImport_t *imports,
const char *commandLine,
iceMinigameInstance_t **instance );
typedef void (*iceMinigameDestroyInstance_t)( iceMinigameInstance_t *instance );
typedef void (*iceMinigameSetActive_t)( iceMinigameInstance_t *instance, int active );
typedef void (*iceMinigameQueueEvent_t)( iceMinigameInstance_t *instance, const iceMinigameInputEvent_t *event );
typedef void (*iceMinigameRunFrame_t)( iceMinigameInstance_t *instance, int elapsedMilliseconds );
typedef int (*iceMinigameGetFramebuffer_t)( iceMinigameInstance_t *instance, iceMinigameFramebuffer_t *framebuffer );
typedef struct iceMinigameExport_s {
int version;
const char *gameName;
int framebufferWidth;
int framebufferHeight;
int preferredSampleRate;
iceMinigameCreateInstance_t CreateInstance;
iceMinigameDestroyInstance_t DestroyInstance;
iceMinigameSetActive_t SetActive;
iceMinigameQueueEvent_t QueueEvent;
iceMinigameRunFrame_t RunFrame;
iceMinigameGetFramebuffer_t GetFramebuffer;
} iceMinigameExport_t;
typedef int (*iceMinigameGetAPI_t)( const iceMinigameImport_t *imports, iceMinigameExport_t *exports );
#endif // __MINIGAME_PUBLIC_H__
+30
View File
@@ -1048,6 +1048,36 @@ bool idRenderSystemLocal::UploadImage( const char *imageName, const byte *data,
return true;
}
/*
===============
idRenderSystemLocal::CreateScratchImage
===============
*/
bool idRenderSystemLocal::CreateScratchImage( const char *imageName, int width, int height ) {
if ( !imageName || !imageName[0] || width <= 0 || height <= 0 ) {
return false;
}
idImage *image = globalImages->GetImage( imageName );
if ( !image ) {
image = globalImages->AllocImage( imageName );
}
if ( !image ) {
return false;
}
const int bytes = width * height * 4;
byte *data = (byte *)Mem_Alloc( bytes );
memset( data, 0, bytes );
for ( int i = 3; i < bytes; i += 4 ) {
data[i] = 255;
}
image->GenerateImage( data, width, height, TF_LINEAR, false, TR_CLAMP, TD_DEFAULT );
Mem_Free( data );
return true;
}
/*
===============
idRenderSystemLocal::LoadImage
+1
View File
@@ -278,6 +278,7 @@ public:
// texture filter / mipmapping / repeat won't be modified by the upload
// returns false if the image wasn't found
virtual bool UploadImage( const char *imageName, const byte *data, int width, int height ) = 0;
virtual bool CreateScratchImage( const char *imageName, int width, int height ) = 0;
// Loads in a image.
virtual void LoadImage(const char* cname, byte** pic, int* width, int* height, ID_TIME_T* timestamp, bool makePowerOf2) = 0;
+1
View File
@@ -731,6 +731,7 @@ public:
virtual void UnCrop();
virtual void GetCardCaps( bool &oldCard, bool &nv10or20 );
virtual bool UploadImage( const char *imageName, const byte *data, int width, int height );
virtual bool CreateScratchImage( const char *imageName, int width, int height );
// Loads in a image.
virtual void LoadImage(const char* cname, byte** pic, int* width, int* height, ID_TIME_T* timestamp, bool makePowerOf2);
+12
View File
@@ -37,6 +37,7 @@ If you have questions concerning this license or the applicable additional terms
#include "BindWindow.h"
#include "ListWindow.h"
#include "RenderWindow.h"
#include "MinigameWindow.h"
#include "MarkerWindow.h"
#include "FieldWindow.h"
@@ -2534,6 +2535,17 @@ bool idWindow::Parse(idParser* src, bool rebuild) {
dwt.win = win;
drawWindows.Append(dwt);
}
else if (token == "minigameDef") {
idMinigameWindow* win = new idMinigameWindow(dc, gui);
SaveExpressionParseState();
win->Parse(src, rebuild);
RestoreExpressionParseState();
AddChild(win);
win->SetParent(this);
dwt.simp = NULL;
dwt.win = win;
drawWindows.Append(dwt);
}
//
// added new onEvent
else if (token == "onNamedEvent") {