// Copyright (c) 2023 Alex Diener. All rights reserved. #include "audioplayer/AudioManager.h" #include "audioplayer/AudioPlayer.h" #include "binaryserialization/BinaryDeserializationContext.h" #include "gamemath/Vector2i.h" #include "gamepad/Gamepad.h" #include "imageio/ImageIO.h" #include "inputcontroller/InputRecorder.h" #include "inputcontroller/InputPlayback.h" #include "PROJECT_NAME/Atoms.h" #include "PROJECT_NAME/Events.h" #include "PROJECT_NAME/FileUtilities.h" #include "PROJECT_NAME/GameplayScreen.h" #include "PROJECT_NAME/GameSession.h" #include "PROJECT_NAME/Globals.h" #include "PROJECT_NAME/Music.h" #include "PROJECT_NAME/Shaders.h" #include "PROJECT_NAME/SharedDefinitions.h" #include "PROJECT_NAME/Sounds.h" #include "renderer/Renderer.h" #include "renderer/RenderTarget.h" #include "shadercollection/ShaderCollection.h" #include "shadercollection/ShaderConfiguration2DTexture.h" #include "shell/Shell.h" #include "shell/ShellCallbacks.h" #include "shell/ShellKeyCodes.h" #include "uitoolkit/UIDrawingInterface2DMultitexture.h" #include "uitoolkit/UIToolkitAppearance.h" #include "uitoolkit/UIToolkitCursor.h" #include "uitoolkit/UIToolkitDrawing.h" #include "utilities/AutoFreePool.h" #include "utilities/IOUtilities.h" #include "MetadataKeys.h" #if defined(STEM_PLATFORM_macosx) #include "nsopenglshell/NSOpenGLShell.h" #include "nsopenglshell/NSOpenGLTarget.h" #elif defined(STEM_PLATFORM_windows) #include "wglshell/WGLShell.h" #include "wglshell/WGLTarget.h" #elif defined(STEM_PLATFORM_linux) #include "glxshell/GLXShell.h" #include "glxshell/GLXTarget.h" #elif defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos) #include "eaglshell/EAGLShell.h" #include "eaglshell/EAGLTarget.h" #elif defined(STEM_PLATFORM_android) #include "eglshell/EGLShell.h" #include "eglshell/EGLTarget.h" #include #define printf(format, ...) __android_log_print(ANDROID_LOG_INFO, STEM_HUMAN_READABLE_TARGET_NAME, format, ##__VA_ARGS__); #define fprintf(stderr, format, ...) __android_log_print(ANDROID_LOG_INFO, STEM_HUMAN_READABLE_TARGET_NAME, format, ##__VA_ARGS__); #else #error Unsupported platform #endif #include "stem_core.h" #include #include #include #include #include #include #define AUDIO_TIMER_INTERVAL 0.5 #define GAMEPAD_TIMER_INTERVAL (1.0 / 60.0) #define GAMEPAD_DETECT_DEVICES_INTERVAL (1.0 / 5.0) static ScreenManager * screenManager; static ShellTimer gamepadTimerID, gamepadDetectDevicesTimerID, audioTimerID; static double lastGamepadTime; static bool backgrounded; static bool audioDisabled; static unsigned int maxScaleMultiplier = UINT_MAX; static const char * recordInputFileName; static const char * playbackInputFileName; static const char * videoOutDirectory; static bool headless; static bool noInputDelay; static bool synchronousLoad; static bool noInputRecording; static InputRecorder * inputRecorder; static InputPlayback * inputPlayback; static void initShaderConfiguration(void) { Matrix4x4f projectionMatrix = Matrix4x4f_ortho(MATRIX4x4f_IDENTITY, 0.0f, g_renderTarget->width, 0.0f, g_renderTarget->height, -2000.0f, 2000.0f); ShaderUniformConfiguration_setMat4_Matrix4x4f(&g_projectionUniform, projectionMatrix); g_shaderConfiguration = ShaderConfiguration2DMultitexture_create(ShaderCollection_get2DMultitextureShader()); call_virtual(referenceProjectionMatrix, g_shaderConfiguration, &g_projectionUniform); ShaderConfiguration * renderTargetShaderConfiguration = ShaderCollection_getRenderTargetShaderConfiguration(); call_virtual(setTexture, renderTargetShaderConfiguration, 0, g_renderTarget->colorTexture, false); ShaderConfiguration * renderTargetAreaShaderConfiguration = ShaderCollection_getRenderTargetAreaFilterShaderConfiguration(); call_virtual(setTexture, renderTargetAreaShaderConfiguration, 0, g_renderTarget->colorTexture, false); } static void initTileset(void) { extern const char EMBEDDATA_main_imagecollection[]; extern unsigned int EMBEDSIZE_main_imagecollection; FileBundle * fileBundle = FileBundle_loadData(EMBEDDATA_main_imagecollection, EMBEDSIZE_main_imagecollection); BitmapImage * atlasImage; TextureAtlasData * atlasData; unpackImageCollectionBundle(fileBundle, &g_imageCollection, &atlasImage, &atlasData); FileBundle_dispose(fileBundle); BitmapImage_premultiply(atlasImage); g_imageCollectionTexture = Texture_createWithTexels2D(atlasImage->pixels, atlasImage->width, atlasImage->height, 4, TEXEL_COMPONENT_UINT8_NORM, TEXEL_SWIZZLE_DEFAULT, TEXTURE_OPTION_MAGNIFY_NEAREST); TextureAtlas * imageAtlas = TextureAtlasData_createTextureAtlas(atlasData, atlasImage->width, atlasImage->height); BitmapImage_dispose(atlasImage); TextureAtlasData_dispose(atlasData); ImageCollection_resolveAtlasEntries(g_imageCollection, imageAtlas); TextureAtlas_dispose(imageAtlas); extern const char EMBEDDATA_main_tileset_edit[]; extern unsigned int EMBEDSIZE_main_tileset_edit; g_tileset = readTilesetEditData(EMBEDDATA_main_tileset_edit, EMBEDSIZE_main_tileset_edit); g_tileProperties = HashTable_create(sizeof(TilePropertyBits)); for (unsigned int tileIndex = 0; tileIndex < g_tileset->tileCount; tileIndex++) { TilePropertyBits tileProperties = 0; if (valueGetBoolean(hashGet(g_tileset->tiles[tileIndex].metadata, METADATA_KEY_floor))) { tileProperties |= TILE_STATIC_FLOOR; } if (valueGetBoolean(hashGet(g_tileset->tiles[tileIndex].metadata, METADATA_KEY_wall))) { tileProperties |= TILE_STATIC_WALL; } HashTable_set(g_tileProperties, HashTable_uint32Key(g_tileset->tiles[tileIndex].identifier), &tileProperties); } extern const char EMBEDDATA_main_tileset_adjacency[]; extern unsigned int EMBEDSIZE_main_tileset_adjacency; g_blendMap = readTilesetAdjacencyBlendMapData(EMBEDDATA_main_tileset_adjacency, EMBEDSIZE_main_tileset_adjacency); extern const char EMBEDDATA_main_spritecollection[]; extern unsigned int EMBEDSIZE_main_spritecollection; g_spriteCollection = readSpriteCollectionData(EMBEDDATA_main_spritecollection, EMBEDSIZE_main_spritecollection, g_imageCollection); } static Rect4f getAtlasEntryCallback(uint32_t codepoint, const char * glyphName, void * context) { TextureAtlas * atlas = context; return TextureAtlas_lookup(atlas, HashTable_stringKey(glyphName)); } static void initAppearance(void) { extern const char EMBEDDATA_appearance_atlas[]; extern unsigned int EMBEDSIZE_appearance_atlas; BinaryDeserializationContext * context = BinaryDeserializationContext_createWithBytes(EMBEDDATA_appearance_atlas, EMBEDSIZE_appearance_atlas); TextureAtlasData * atlasData = TextureAtlasData_deserialize(context); BinaryDeserializationContext_dispose(context); extern const char EMBEDDATA_appearance_atlas_png[]; extern unsigned int EMBEDSIZE_appearance_atlas_png; BitmapImage * image = ImageIO_readImageData(EMBEDDATA_appearance_atlas_png, EMBEDSIZE_appearance_atlas_png, BITMAP_PIXEL_FORMAT_RGBA_8888, true); g_texture = Texture_createWithTexels2D(image->pixels, image->width, image->height, 4, TEXEL_COMPONENT_UINT8_NORM, TEXEL_SWIZZLE_DEFAULT, TEXTURE_OPTION_MAGNIFY_NEAREST | TEXTURE_OPTION_MINIFY_NEAREST); TextureAtlas * atlas = TextureAtlasData_createTextureAtlas(atlasData, image->width, image->height); BitmapImage_dispose(image); TextureAtlasData_dispose(atlasData); extern const char EMBEDDATA_17pt_bitmapfont2[]; extern unsigned int EMBEDSIZE_17pt_bitmapfont2; context = BinaryDeserializationContext_createWithBytes(EMBEDDATA_17pt_bitmapfont2, EMBEDSIZE_17pt_bitmapfont2); g_font17pt = BitmapFont2_deserialize(context); BinaryDeserializationContext_dispose(context); BitmapFont2_resolveAtlasEntries(g_font17pt, getAtlasEntryCallback, atlas); g_whiteAtlasEntry = TextureAtlas_lookup(atlas, HashTable_stringKey("white")); g_uiTypeface = UITypeface_BitmapFont2_create(g_font17pt, 1.0f, 0, TEXT_OPTION_PIXEL_SNAPPING); UIDrawingInterface2DMultitexture * drawingInterface = UIDrawingInterface2DMultitexture_create(0); drawingInterface->scaleFactor = g_scaleFactor; g_uiContext = UIToolkitContext_init(default2DRenderPipelineConfiguration(RENDER_BLEND_ALPHA_PREMULTIPLIED), g_shaderConfiguration, drawingInterface); UIToolkit_setContext(&g_uiContext); g_uiAppearance = UIAppearance_init(NULL); UIToolkit_registerAppearanceParameters(&g_uiAppearance, atlas, g_uiTypeface, NULL); TextureAtlas_dispose(atlas); call_virtual(setTexture, g_shaderConfiguration, 0, g_texture, false); call_virtual(setTexture, g_shaderConfiguration, 1, g_imageCollectionTexture, false); } static void audioTimer(ShellTimer timerID, void * context) { AudioPlayer_run(); } static void gamepadDetectDevicesTimer(ShellTimer timerID, void * context) { Gamepad_detectDevices(); } static void gamepadEventTimer(ShellTimer timerID, void * context) { double currentTime = Shell_getCurrentTime(); Gamepad_processEvents(); InputController_processRepeats(g_gameSession->inputController, currentTime, currentTime - lastGamepadTime); lastGamepadTime = currentTime; } static PCMAudio * loadSoundCallback(SoundID soundID, void * context) { return loadSoundEffect(soundID); } static PCMAudio * loadMusicCallback(MusicID musicID, AudioFrameIndex * outLoopDuration, void * context) { return loadMusic(musicID, outLoopDuration); } static void initAudio(void) { if (!audioDisabled) { AudioOut_sampleFormat sampleFormat = {2, AUDIO_DEFAULT_SAMPLE_RATE, 2}; AudioPlayer_init(STEM_HUMAN_READABLE_TARGET_NAME, AUDIO_SOURCE_COUNT_MAX, AUDIO_STREAM_COUNT_MAX, AUDIO_CATEGORY_COUNT, sampleFormat, NULL, NULL, NULL); g_audioSampleRate = AudioOut_getTransportFormat().sampleRate; if (!GameSession_getSoundEnabled(g_gameSession)) { AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_SOUND, 0.0f); } else { AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_SOUND, GameSession_getSoundVolume(g_gameSession)); } if (!GameSession_getMusicEnabled(g_gameSession)) { AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_MUSIC, 0.0f); } else { AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_MUSIC, GameSession_getMusicVolume(g_gameSession)); } AudioPlayer_setPausesAudioDeviceWhenIdle(false); audioTimerID = Shell_setTimer(AUDIO_TIMER_INTERVAL, true, audioTimer, NULL); if (videoOutDirectory != NULL) { AudioPlayer_setSpeakerOutputEnabled(false); } AudioManager_init(AUDIO_MUSIC_LANE_COUNT, 4, 8, loadSoundCallback, loadMusicCallback, NULL); initSoundEffects(); initMusic(); if (synchronousLoad || headless) { AudioManager_loadAllAudioImmediate(); } else { AudioManager_loadAllAudioAsync(); } } else { AudioManager_init(0, 1, 1, NULL, NULL, NULL); } } static void initScreens(void) { GameplayScreen * gameplayScreen = GameplayScreen_create(); ScreenManager_addScreen(screenManager, gameplayScreen); ScreenManager_setScreen(screenManager, gameplayScreen); } static void markInputSessionClean(void) { if (inputRecorder != NULL) { uint8_t cleanExit = 1; InputRecorder_rewriteReplayStartupData(inputRecorder, &cleanExit, sizeof(cleanExit)); } } #ifdef DEBUG static bool inputPlaybackComplete(Atom eventID, void * eventData, void * context) { fprintf(stderr, "Playback complete\n"); return true; } #endif static void applyRunOptions(void) { const char * supportPath = Shell_getSupportPath(SUPPORT_SUBDIRECTORY_NAME); char defaultInputFileName[PATH_MAX]; snprintf_safe(defaultInputFileName, sizeof(defaultInputFileName), "%s/last.inputsession", supportPath); if (recordInputFileName == NULL && !noInputRecording && playbackInputFileName == NULL) { recordInputFileName = defaultInputFileName; } enum InputSessionError error; InputSession * lastInputSession = InputSession_loadFile(defaultInputFileName, &error); if (lastInputSession != NULL && lastInputSession->replayStartupDataSize > 0) { uint8_t * cleanExit = lastInputSession->replayStartupData; if (!*cleanExit) { const char * crashFileName = uniqueFileInDirectory(supportPath, "crash", "inputsession", 9999); if (crashFileName != NULL) { int result = rename(defaultInputFileName, crashFileName); #ifdef DEBUG if (result != 0) { fprintf(stderr, "Warning: Failed to rename crashed input session with errno %d; disabling recording for this session\n", errno); recordInputFileName = NULL; } else { fprintf(stderr, "Saved crash report to %s\n", crashFileName); } } else { fprintf(stderr, "Warning: Couldn't find a unique file name for saving a crash report; disabling recording for this session\n"); #endif } } InputSession_dispose(lastInputSession); } if (recordInputFileName != NULL) { uint8_t cleanExit = 0; inputRecorder = InputRecorder_createWithFileOutput(g_gameSession->inputController, &cleanExit, sizeof(cleanExit), recordInputFileName); atexit(markInputSessionClean); #ifdef DEBUG if (inputRecorder == NULL) { fprintf(stderr, "Couldn't create recording output file \"%s\" (errno = %d)\n", recordInputFileName, errno); } #endif } if (playbackInputFileName != NULL) { enum InputSessionError error; InputSession * inputSession = InputSession_loadFile(playbackInputFileName, &error); if (inputSession == NULL) { #ifdef DEBUG fprintf(stderr, "Couldn't read input session from file \"%s\" (error %d)", playbackInputFileName, error); #endif } else { inputPlayback = InputPlayback_create(g_gameSession->inputController, inputSession); #ifdef DEBUG EventDispatcher_registerForEvent(inputPlayback->eventDispatcher, ATOM(INPUT_PLAYBACK_EVENT_PLAYBACK_COMPLETE), inputPlaybackComplete, NULL); fprintf(stderr, "Playing back input session from file %s\n", playbackInputFileName); #endif } } if (videoOutDirectory != NULL) { int result = mkdir(videoOutDirectory #ifndef WIN32 , 0777 #endif ); if (result != 0 && errno != EEXIST) { #ifdef DEBUG fprintf(stderr, "Couldn't create video output directory \"%s\" (errno = %d)\n", videoOutDirectory, errno); #endif videoOutDirectory = NULL; } } } static void initAfterFirstDraw(void) { #ifdef STEM_PLATFORM_linux // XRRGetScreenInfo (called by Shell_getDisplayRefreshRate() in glxshell) is extremely slow and causes noticeably delayed // application startup time when called here. Even if called from a secondary thread, screen updates stop for a noticeable // amount of time. Applications can choose to accept the default 60hz, accept the slowdown at startup, or provide a // mechanism to manually set the refresh rate on a preferences screen. Shell_setDrawThrottleRate(1.0 / 60.0f); #else Shell_setDrawThrottleRate(1.0 / Shell_getDisplayRefreshRate(Shell_getDisplayIndexFromWindow())); #endif pcg32_seed(&g_pcgState, time(NULL), 0); pcg32_stir(&g_pcgState, 50); lastGamepadTime = Shell_getCurrentTime(); gamepadTimerID = Shell_setTimer(GAMEPAD_TIMER_INTERVAL, true, gamepadEventTimer, NULL); gamepadDetectDevicesTimerID = Shell_setTimer(GAMEPAD_DETECT_DEVICES_INTERVAL, true, gamepadDetectDevicesTimer, NULL); initShaders(); initShaderConfiguration(); initTileset(); initAppearance(); initAudio(); applyRunOptions(); initScreens(); } static void drawFrame(double referenceTime, double activeDrawDelta) { if (inputPlayback != NULL) { if (noInputDelay) { InputPlayback_skipToNextEventFrame(inputPlayback); } InputPlayback_step(inputPlayback, referenceTime); } Screen * currentScreen = screenManager->currentScreen; struct drawEvent event = {referenceTime, activeDrawDelta}; bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_draw, &event); if (!handled && screenManager->currentScreen != currentScreen) { handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_draw, &event); } if (inputRecorder != NULL) { InputRecorder_nextFrame(inputRecorder); } if (videoOutDirectory != NULL) { static unsigned int frameIndex; static BitmapImage * image; char filePath[PATH_MAX]; snprintf_safe(filePath, sizeof(filePath), "%s/frame%05u.png", videoOutDirectory, frameIndex); if (image == NULL) { image = BitmapImage_create(BITMAP_PIXEL_FORMAT_RGBA_8888, DISPLAY_WIDTH, DISPLAY_HEIGHT); } RenderTarget_readPixels(g_renderTarget, image->pixels, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); ImageIO_writePNGFile(image, filePath, BITMAP_PIXEL_FORMAT_RGB_888, true); frameIndex++; } } #if defined(STEM_PLATFORM_linux) #define EXTRA_DRAW_COUNT_MAX 1 #elif defined(STEM_PLATFORM_windows) #define EXTRA_DRAW_COUNT_MAX 4 #endif #define INITIAL_DRAW_COUNT_BEFORE_INIT 1 #define INITIAL_DRAW_COUNT_AFTER_INIT 1 #define INITIAL_DRAW_COUNT_BEFORE_RENDER (INITIAL_DRAW_COUNT_BEFORE_INIT + 1 + INITIAL_DRAW_COUNT_AFTER_INIT) static bool Target_draw(double referenceTime, double activeDrawDelta) { static unsigned int initialDrawCount; if (initialDrawCount == INITIAL_DRAW_COUNT_BEFORE_INIT) { initAfterFirstDraw(); Renderer_clear(g_renderer, COLOR4f(0.0f, 0.0f, 0.0f, 0.0f)); Shell_redisplay(); initialDrawCount++; } else if (initialDrawCount < INITIAL_DRAW_COUNT_BEFORE_RENDER) { Renderer_clear(g_renderer, COLOR4f(0.0f, 0.0f, 0.0f, 0.0f)); Shell_redisplay(); initialDrawCount++; } else { if (initialDrawCount == INITIAL_DRAW_COUNT_BEFORE_RENDER) { initialDrawCount++; } Renderer_setRenderTarget(g_renderer, g_renderTarget); drawFrame(referenceTime, activeDrawDelta); Renderer_setRenderTarget(g_renderer, NULL); Renderer_clear(g_renderer, COLOR4f(0.0f, 0.0f, 0.0f, 0.0f)); drawRenderTarget(g_renderer, g_renderTarget, ShaderCollection_getRenderTargetAdaptiveAreaFilterShaderConfiguration(g_viewWidth, g_viewHeight, g_renderTarget->width, g_renderTarget->height)); } AutoFreePool_empty(); return true; } static void Target_keyDown(unsigned int charCode, unsigned int keyCode, unsigned int modifiers, bool isRepeat, double referenceTime) { if (keyCode == KEY_CODE_ENTER && (modifiers & MODIFIER_ALT_BIT) && !isRepeat) { if (Shell_isFullScreen()) { Shell_exitFullScreen(); } else { Shell_enterFullScreen(Shell_getDisplayIndexFromWindow(), false); } EventDispatcher_dispatchEvent(screenManager->eventDispatcher, Shell_isFullScreen() ? ATOM_event_fullscreened : ATOM_event_windowed, &referenceTime); } else if (keyCode == KEY_CODE_Q && (modifiers & MODIFIER_PLATFORM_MENU_COMMAND_BIT)) { exit(EXIT_SUCCESS); } else if (keyCode == KEY_CODE_EQUAL && (modifiers & MODIFIER_CONTROL_BIT) && (modifiers & MODIFIER_SHIFT_BIT) && !isRepeat) { const char * filePath; const char * pngExtension = "png"; struct ShellFileDialogFilter filter = {"Image files", 1, &pngExtension}; if (Shell_saveFileDialog(NULL, NULL, "screenshot.png", &filePath, 1, &filter)) { BitmapImage * image = BitmapImage_create(BITMAP_PIXEL_FORMAT_RGBA_8888, DISPLAY_WIDTH, DISPLAY_HEIGHT); RenderTarget_readPixels(g_renderTarget, image->pixels, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); ImageIO_writePNGFile(image, filePath, BITMAP_PIXEL_FORMAT_RGB_888, true); BitmapImage_dispose(image); } } else { struct keyEvent event; event.keyCode = keyCode; event.charCode = charCode; event.modifiers = modifiers; event.isRepeat = isRepeat; event.timestamp = referenceTime; bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_key_down, &event); if (!handled) { handled = InputController_keyDown(g_gameSession->inputController, keyCode, modifiers, referenceTime, false); if (handled) { Shell_hideCursorUntilMouseMoves(); } } } } static void Target_keyUp(unsigned int keyCode, unsigned int modifiers, double referenceTime) { struct keyEvent event; event.keyCode = keyCode; event.modifiers = modifiers; event.timestamp = referenceTime; bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_key_up, &event); if (!handled) { handled = InputController_keyUp(g_gameSession->inputController, keyCode, referenceTime, false); } } static void Target_keyModifiersChanged(unsigned int modifiers, unsigned int lastModifiers, double referenceTime) { struct keyEvent event; event.modifiers = modifiers; event.lastModifiers = lastModifiers; event.timestamp = referenceTime; bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_key_modifiers_changed, &event); if (!handled) { handled = InputController_keyModifiersChanged(g_gameSession->inputController, modifiers, lastModifiers, referenceTime, false); } } static void Target_mouseDown(unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, double referenceTime) { struct mouseEvent event; event.buttonNumber = buttonNumber; event.buttonMask = buttonMask; event.position = VECTOR2f(x, y); event.modifiers = modifiers; event.timestamp = referenceTime; g_lastMousePosition = event.position; EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_down, &event); } static void Target_mouseUp(unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, double referenceTime) { struct mouseEvent event; event.buttonNumber = buttonNumber; event.buttonMask = buttonMask; event.position = VECTOR2f(x, y); event.modifiers = modifiers; event.timestamp = referenceTime; g_lastMousePosition = event.position; EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_up, &event); } static void Target_mouseMoved(float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { struct mouseEvent event; event.position = VECTOR2f(x, y); event.delta = VECTOR2f(deltaX, deltaY); event.modifiers = modifiers; event.timestamp = referenceTime; g_lastMousePosition = event.position; EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_moved, &event); } static void Target_mouseDragged(unsigned int buttonMask, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { struct mouseEvent event; event.buttonMask = buttonMask; event.position = VECTOR2f(x, y); event.delta = VECTOR2f(deltaX, deltaY); event.modifiers = modifiers; event.timestamp = referenceTime; g_lastMousePosition = event.position; EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_dragged, &event); } static void Target_mouseLeave(unsigned int modifiers, double referenceTime) { struct mouseEvent event; event.modifiers = modifiers; event.timestamp = referenceTime; EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_leave, &event); } void gamepadAttached(struct Gamepad_device * device, void * context) { EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_gamepad_attached, device); } void gamepadRemoved(struct Gamepad_device * device, void * context) { EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_gamepad_detached, device); } void gamepadButtonDown(struct Gamepad_device * device, unsigned int buttonID, double timestamp, void * context) { struct gamepadButtonEvent event; event.device = device; event.buttonID = buttonID; event.timestamp = timestamp; bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_gamepad_button_down, &event); if (!handled) { handled = InputController_gamepadButtonDown(g_gameSession->inputController, device, buttonID, timestamp, false); if (handled) { Shell_hideCursorUntilMouseMoves(); } } } void gamepadButtonUp(struct Gamepad_device * device, unsigned int buttonID, double timestamp, void * context) { struct gamepadButtonEvent event; event.device = device; event.buttonID = buttonID; event.timestamp = timestamp; bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_gamepad_button_up, &event); if (!handled) { handled = InputController_gamepadButtonUp(g_gameSession->inputController, device, buttonID, timestamp, false); } } void gamepadAxisMoved(struct Gamepad_device * device, unsigned int axisID, float value, float lastValue, double timestamp, void * context) { struct gamepadAxisEvent event; event.device = device; event.axisID = axisID; event.value = value; event.lastValue = lastValue; event.timestamp = timestamp; bool handled = EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_gamepad_axis_move, &event); if (!handled) { handled = InputController_gamepadAxisMoved(g_gameSession->inputController, device, axisID, value, lastValue, timestamp, false); if (handled) { Shell_hideCursorUntilMouseMoves(); } } } static void Target_scrollWheel(float x, float y, int deltaX, int deltaY, unsigned int buttonMask, unsigned int modifiers, double referenceTime) { struct mouseWheelEvent event; event.position = VECTOR2f(x, y); event.deltaX = deltaX; event.deltaY = deltaY; event.buttonMask = buttonMask; event.modifiers = modifiers; event.timestamp = referenceTime; g_lastMousePosition = event.position; EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_mouse_scroll_wheel, &event); } static void Target_resized(unsigned int newWidth, unsigned int newHeight, double referenceTime) { g_viewWidth = newWidth; g_viewHeight = newHeight; g_viewRatio = (float) newWidth / newHeight; g_scaleFactor = Shell_getDisplayScaleFactor(Shell_getDisplayIndexFromWindow()); if (g_renderer != NULL) { if (g_uiContext.drawingInterface != NULL) { g_uiContext.drawingInterface->scaleFactor = g_scaleFactor; } Renderer_setViewport(g_renderer, 0, 0, g_viewWidth, g_viewHeight, NULL); Shell_redisplay(); } if (screenManager != NULL) { EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_resized, &referenceTime); } } static void Target_backgrounded(double referenceTime) { backgrounded = true; EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_backgrounded, &referenceTime); AudioPlayer_pause(); Shell_cancelTimer(audioTimerID); Shell_cancelTimer(gamepadTimerID); Shell_cancelTimer(gamepadDetectDevicesTimerID); } static void Target_foregrounded(double referenceTime) { backgrounded = false; EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_foregrounded, &referenceTime); AudioPlayer_unpause(); audioTimerID = Shell_setTimer(AUDIO_TIMER_INTERVAL, true, audioTimer, NULL); gamepadTimerID = Shell_setTimer(GAMEPAD_TIMER_INTERVAL, true, gamepadEventTimer, NULL); gamepadDetectDevicesTimerID = Shell_setTimer(GAMEPAD_DETECT_DEVICES_INTERVAL, true, gamepadDetectDevicesTimer, NULL); Shell_redisplay(); } static bool Target_confirmQuit(double referenceTime) { return true; } static void runHeadless(void) { initAfterFirstDraw(); Renderer_setRenderTarget(g_renderer, g_renderTarget); int16_t frameAudioBuffer[g_audioSampleRate / 60 * 2]; char wavHeader[] = "RIFF\xB2\x03\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x02\x00\x80\xBB\x00\x00\x00\xEE\x02\x00\x04\x00\x10\x00""data\x00\x00\x00\x00"; FILE * audioFile; unsigned int audioByteCount = 0; char audioPath[PATH_MAX]; snprintf_safe(audioPath, PATH_MAX, "%s/audio.wav", videoOutDirectory); audioFile = fopen(audioPath, "wb"); fseek(audioFile, sizeof(wavHeader) - 1, SEEK_SET); double referenceTime = 0.0; double deltaTime = 1.0 / 60.0; while (inputPlayback->eventIndex < inputPlayback->inputSession->eventCount) { drawFrame(referenceTime, deltaTime); referenceTime += deltaTime; AudioPlayer_readSamples(frameAudioBuffer, g_audioSampleRate / 60); fwrite(frameAudioBuffer, 1, sizeof(frameAudioBuffer), audioFile); audioByteCount += sizeof(frameAudioBuffer); AudioPlayer_run(); AutoFreePool_empty(); } wavHeader[4] = (audioByteCount + 36) & 0xFF; wavHeader[5] = (audioByteCount + 36) >> 8 & 0xFF; wavHeader[6] = (audioByteCount + 36) >> 16 & 0xFF; wavHeader[7] = (audioByteCount + 36) >> 24 & 0xFF; wavHeader[40] = audioByteCount & 0xFF; wavHeader[41] = audioByteCount >> 8 & 0xFF; wavHeader[42] = audioByteCount >> 16 & 0xFF; wavHeader[43] = audioByteCount >> 24 & 0xFF; fseek(audioFile, 0, SEEK_SET); fwrite(wavHeader, 1, sizeof(wavHeader) - 1, audioFile); fclose(audioFile); } static void cursorChangeCallback(void) { EventDispatcher_dispatchEvent(screenManager->eventDispatcher, ATOM_event_cursor_change, NULL); } static void registerShellCallbacks() { Shell_drawFunc(Target_draw); Shell_resizeFunc(Target_resized); Shell_keyDownFunc(Target_keyDown); Shell_keyUpFunc(Target_keyUp); Shell_keyModifiersChangedFunc(Target_keyModifiersChanged); Shell_mouseDownFunc(Target_mouseDown); Shell_mouseUpFunc(Target_mouseUp); Shell_mouseMovedFunc(Target_mouseMoved); Shell_mouseDraggedFunc(Target_mouseDragged); Shell_mouseLeaveFunc(Target_mouseLeave); Shell_scrollWheelFunc(Target_scrollWheel); Shell_backgroundedFunc(Target_backgrounded); Shell_foregroundedFunc(Target_foregrounded); Shell_confirmQuitFunc(Target_confirmQuit); UIToolkit_cursorChangeFunc(cursorChangeCallback); } static void parseArgs(int argc, const char ** argv) { for (int argIndex = 1; argIndex < argc; argIndex++) { if (!strcmp(argv[argIndex], "--disable-audio")) { audioDisabled = true; } else if (!strcmp(argv[argIndex], "--max-pixel-scale") && argIndex < argc - 1) { sscanf(argv[++argIndex], "%u", &maxScaleMultiplier); } else if (!strcmp(argv[argIndex], "--record") && argIndex < argc - 1) { recordInputFileName = argv[++argIndex]; } else if (!strcmp(argv[argIndex], "--playback") && argIndex < argc - 1) { playbackInputFileName = argv[++argIndex]; } else if (!strcmp(argv[argIndex], "--video-out") && argIndex < argc - 1) { videoOutDirectory = argv[++argIndex]; } else if (!strcmp(argv[argIndex], "--headless")) { headless = true; } else if (!strcmp(argv[argIndex], "--no-input-delay")) { noInputDelay = true; } else if (!strcmp(argv[argIndex], "--synchronous-load")) { synchronousLoad = true; } else if (!strcmp(argv[argIndex], "--no-recording")) { noInputRecording = true; } else if (!strcmp(argv[argIndex], "--help")) { fprintf(stderr, "Available command-line arguments:\n" " --disable-audio\n" " --max-pixel-scale \n" " --record \n" " --playback \n" " --video-out \n" " --headless\n"); } } } #if defined(STEM_PLATFORM_macosx) void NSOpenGLTarget_configure(int argc, const char ** argv, struct NSOpenGLShellConfiguration * configuration) { parseArgs(argc, argv); configuration->fullScreenMenuItem = true; configuration->useGLCoreProfile = true; #elif defined(STEM_PLATFORM_windows) void WGLTarget_configure(void * instance, void * prevInstance, char * commandLine, int command, int argc, const char ** argv, struct WGLShellConfiguration * configuration) { parseArgs(argc, argv); configuration->useGLCoreProfile = true; #elif defined(STEM_PLATFORM_linux) void GLXTarget_configure(int argc, const char ** argv, struct GLXShellConfiguration * configuration) { #include "IconData_PROJECT_NAME.h" parseArgs(argc, argv); configuration->icon.data = STATIC_iconData_PROJECT_NAME; configuration->icon.size = sizeof_count(STATIC_iconData_PROJECT_NAME); configuration->applicationName = STEM_HUMAN_READABLE_TARGET_NAME; #elif defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos) void EAGLTarget_configure(int argc, char ** argv, struct EAGLShellConfiguration * configuration) { #elif defined(STEM_PLATFORM_android) void EGLTarget_configure(struct EGLShellConfiguration * configuration) { readResourceFile = EGLShell_readResourceFile; #else #error Unsupported platform #endif #if !defined(STEM_PLATFORM_iphonesimulator) && !defined(STEM_PLATFORM_iphoneos) && !defined(STEM_PLATFORM_android) int displayX, displayY; unsigned int displayWidth, displayHeight, windowWidth, windowHeight; Shell_getSafeWindowRect(0, &displayX, &displayY, &displayWidth, &displayHeight); if (displayWidth / (float) displayHeight > DISPLAY_WIDTH / (float) DISPLAY_HEIGHT) { if (displayHeight < DISPLAY_HEIGHT) { windowWidth = displayWidth; windowHeight = displayHeight; } else { unsigned int scaleMultiplier = displayHeight / DISPLAY_HEIGHT; if (scaleMultiplier > maxScaleMultiplier) { scaleMultiplier = maxScaleMultiplier; } windowHeight = DISPLAY_HEIGHT * scaleMultiplier; windowWidth = windowHeight * DISPLAY_WIDTH / DISPLAY_HEIGHT; } } else { if (displayWidth < DISPLAY_WIDTH) { windowWidth = displayWidth; windowHeight = displayHeight; } else { unsigned int scaleMultiplier = displayWidth / DISPLAY_WIDTH; if (scaleMultiplier > maxScaleMultiplier) { scaleMultiplier = maxScaleMultiplier; } windowWidth = DISPLAY_WIDTH * scaleMultiplier; windowHeight = windowWidth * DISPLAY_HEIGHT / DISPLAY_WIDTH; } } configuration->windowX = displayX + (displayWidth - windowWidth) / 2; configuration->windowY = displayY + (displayHeight - windowHeight) / 2; configuration->windowWidth = g_viewWidth = windowWidth; configuration->windowHeight = g_viewHeight = windowHeight; configuration->windowTitle = STEM_HUMAN_READABLE_TARGET_NAME; #endif registerShellCallbacks(); } void Target_init() { g_renderer = Renderer_create(); Renderer_setViewport(g_renderer, 0, 0, g_viewWidth, g_viewHeight, NULL); Gamepad_init(); Gamepad_deviceAttachFunc(gamepadAttached, NULL); Gamepad_deviceRemoveFunc(gamepadRemoved, NULL); Gamepad_buttonDownFunc(gamepadButtonDown, NULL); Gamepad_buttonUpFunc(gamepadButtonUp, NULL); Gamepad_axisMoveFunc(gamepadAxisMoved, NULL); g_gameSession = GameSession_create(); g_renderTarget = RenderTarget_create(DISPLAY_WIDTH, DISPLAY_HEIGHT, true, false); screenManager = ScreenManager_create(); #if defined(STEM_PLATFORM_android) EGLShell_setOrientation(EGLShellOrientation_landscape); Shell_enterFullScreen(0, true); #else if (GameSession_getFullScreen(g_gameSession)) { Shell_enterFullScreen(Shell_getDisplayIndexFromWindow(), false); } #endif if (headless && playbackInputFileName != NULL) { runHeadless(); return; } Shell_mainLoop(); }