/* * Copyright (C) 2020 - Vito Caputo - * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3 as published * by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include /* exit, atexit */ #include "config.h" #include "macros.h" #include "play.h" /* libplay: SDL2 and SDL2_Mixer glued together a bit */ typedef struct play_t { #ifdef WITH_AUDIO Mix_Music *music; char *music_file, *queued_music_file; unsigned music_flags, queued_music_flags; #endif Uint32 tick_offsets[PLAY_TICKS_CNT]; Uint32 ticks_paused_at; unsigned ticks_paused:1; unsigned update_needed:1; unsigned has_audio:1; unsigned pointer_owned:1; const play_ops_t **ops; int n_ops; int context; void *contexts[]; } play_t; void play_music_set(play_t *play, unsigned flags, const char *fmt, ...) { #ifdef WITH_AUDIO int fade = (flags & PLAY_MUSIC_FLAG_FADEIN); int optional = (flags & PLAY_MUSIC_FLAG_OPTIONAL); int queue = (flags & PLAY_MUSIC_FLAG_QUEUE); int idempotent = (flags & PLAY_MUSIC_FLAG_IDEMPOTENT); char file[256] = {}; Mix_Music *new; va_list ap; #endif assert(play); assert(play->has_audio); #ifdef WITH_AUDIO va_start(ap, fmt); vsnprintf(file, sizeof(file), fmt, ap); va_end(ap); assert(!file[sizeof(file) - 1]); if (queue && play->music) { free(play->queued_music_file); play->queued_music_file = strdup(file); play->queued_music_flags = (flags & ~PLAY_MUSIC_FLAG_QUEUE); return; } if (idempotent && play->music && !strcmp(file, play->music_file)) return; new = Mix_LoadMUS(file); if (!new && optional) return; fatal_if(!new, "Unable to load music \"%s\", reason: \"%s\"", file, Mix_GetError()); if (play->music) { if (play->queued_music_file) { free(play->queued_music_file); play->queued_music_file = NULL; play->queued_music_flags = 0; } Mix_HaltMusic(); Mix_FreeMusic(play->music); free(play->music_file); play->music = NULL; play->music_file = NULL; play->music_flags = 0; } play->music = new; fatal_if(!(play->music_file = strdup(file)), "Unable to dup \"%s\"", file); play->music_flags = flags; Mix_VolumeMusic(100); /* XXX: just hard-coding music volume for now */ if (fade) { fatal_if(Mix_FadeInMusic(play->music, 0, 1000), "Unable to play music"); } else { fatal_if(Mix_PlayMusic(play->music, 0), "Unable to play music"); } Mix_ResumeMusic(); // XXX KLUDGE ALERT: on windows paused music doesn't seem to resume // automatically via the above playmusic/fadeinmusic stuff. } /* XXX KLUDGE ALERT: this is necessary because Mix_GetMusicHookData() doesn't * work unless we hook the mixing. The postmix register doesn't seem to set * the pointer returned by GetMusicHookData() * So we use a global, stow the pointer for music_finished() to access :(. */ static play_t *__play_ptr_for_music_fixme; static void music_finished(void) { play_t *play = __play_ptr_for_music_fixme; if (play->queued_music_file) { char *file = play->queued_music_file; unsigned flags = play->queued_music_flags; play->queued_music_file = NULL; play->queued_music_flags = 0; play_music_set(play, flags, "%s", file); free(file); } if (!Mix_PlayingMusic() && (play->music_flags & PLAY_MUSIC_FLAG_LOOP)) fatal_if(Mix_PlayMusic(play->music, 0), "Unable to loop music"); #endif } void play_music_pause(play_t *play) { assert(play); assert(play->has_audio); #ifdef WITH_AUDIO Mix_PauseMusic(); #endif } void play_music_resume(play_t *play) { assert(play); assert(play->has_audio); #ifdef WITH_AUDIO Mix_ResumeMusic(); #endif } void play_context_enter(play_t *play, int context) { assert(play); assert(context >= 0 && context < play->n_ops); if (play->context == context) return; if (play->ops[play->context]->leave) play->ops[play->context]->leave(play, play->contexts[play->context]); play->context = context; if (play->ops[play->context]->enter) play->ops[play->context]->enter(play, play->contexts[play->context]); /* whenever a context switch occurs, this flag triggers immediately calling of update() hooks. */ play->update_needed = 1; } /* return context pointer of specified context, * a negative context requests the current context. */ void * play_context(const play_t *play, int context) { assert(play); if (context < 0) context = play->context; assert(context < play->n_ops); return play->contexts[context]; } /* unpause ticks if necessary, see play_ticks_pause() */ static inline void ticks_active(play_t *play) { Uint32 delta; if (!play->ticks_paused) return; delta = SDL_GetTicks() - play->ticks_paused_at; for (int i = 0; i < PLAY_TICKS_CNT; i++) play->tick_offsets[i] += delta; play->ticks_paused = 0; } /* return ticks counter */ unsigned play_ticks(play_t *play, play_ticks_t timer) { assert(play); assert(timer < PLAY_TICKS_CNT); ticks_active(play); return SDL_GetTicks() - play->tick_offsets[timer]; } /* reset ticks counter to begin counting from now * always returns 0 for convenience/ergonomic reasons to use as * a new post-reset ticks value. */ unsigned play_ticks_reset(play_t *play, play_ticks_t timer) { assert(play); assert(timer < PLAY_TICKS_CNT); ticks_active(play); play->tick_offsets[timer] = SDL_GetTicks(); return 0; } /* "pause" timers - this basically just notes the current time, * and on any subsequent timer use the difference is added to * all timer offsets as an implicit "resume" before discarding * the paused state. */ void play_ticks_pause(play_t *play) { if (play->ticks_paused) return; play->ticks_paused = 1; play->ticks_paused_at = SDL_GetTicks(); } /* returns true if specified duration elapsed, resets timer if so. */ int play_ticks_elapsed(play_t *play, play_ticks_t timer, unsigned duration) { assert(play); assert(timer < PLAY_TICKS_CNT); if (play_ticks(play, timer) >= duration) { (void) play_ticks_reset(play, timer); return 1; } return 0; } void play_pointer_own(play_t *play) { assert(play); if (play->pointer_owned) return; SDL_SetRelativeMouseMode(SDL_TRUE); play->pointer_owned = 1; } void play_pointer_disown(play_t *play) { assert(play); if (!play->pointer_owned) return; SDL_SetRelativeMouseMode(SDL_FALSE); play->pointer_owned = 0; } int play_pointer_owned(const play_t *play) { assert(play); return !!play->pointer_owned; } void play_quit(play_t *play) { SDL_Event ev = { .type = SDL_QUIT }; SDL_PushEvent(&ev); } /* This initializes a bunch of SDL stuff and calls all the ops.init() hooks * serially, in-order, before returning a handle to it all. * * Note this doesn't create an SDL window or OpenGL context, as that's all * quite game-specific so it's left for presumably the first init hook. * * Flags may be used to disable audio/video/gamepad support, simply use empty flags * to get all three. */ play_t * play_startup(int argc, char *argv[], unsigned flags, const play_ops_t *ops[]) { Uint32 sdl_flags = 0; size_t n_ops; play_t *play; assert(ops); for (n_ops = 0; ops[n_ops]; n_ops++); assert(n_ops > 0); play = calloc(1, sizeof(play_t) + sizeof(void *) * n_ops); fatal_if(!play, "Unable to allocate play_t"); /* since it's expected that the vast majority of callers will want * sound+video+gamecontroller this is inverted so those may pass * empty flags, and instead the exceptions turn off which things * they don't want. */ if (!(flags & PLAY_FLAG_NOAUDIO)) { #ifndef WITH_AUDIO fatal_if(1, "libplay built without audio support, audio/music API unavailable"); #endif sdl_flags |= SDL_INIT_AUDIO; play->has_audio = 1; } if (!(flags & PLAY_FLAG_NOVIDEO)) sdl_flags |= SDL_INIT_VIDEO; if (!(flags & PLAY_FLAG_NOGAMEPAD)) sdl_flags |= SDL_INIT_GAMECONTROLLER; fatal_if(SDL_Init(sdl_flags) != 0, "Unable to initialize SDL"); fatal_if(atexit(SDL_Quit), "Unable to set exit handler"); #ifdef WITH_AUDIO if (play->has_audio) { fatal_if(Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) == -1, "Unable to open audio"); __play_ptr_for_music_fixme = play; Mix_HookMusicFinished(music_finished); Mix_AllocateChannels(32); } #endif if (!(flags & PLAY_FLAG_NOGAMEPAD)) { /* Just in case the user has dropped a game controller mapping */ SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt"); /* I don't want to get too crazy over here with controller handling, just open all * the attached controllers at startup and get on with it. */ for (int i = 0; i < SDL_NumJoysticks(); ++i) { SDL_GameController *controller; char guid[64] = {}; SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i), guid, sizeof(guid)); if (!SDL_IsGameController(i)) { fprintf(stderr, "Ignoring unrecognized joystick \"%s\",\n" "add a \"gamecontrollerdb.txt\" to use it.\n", guid); continue; } controller = SDL_GameControllerOpen(i); warn_if(!controller, "Could not open game controller \"%s\": %s", guid, SDL_GetError()); /* XXX: For now I'm just opening all of them and losing their handles, * it seems benign enough, the events will be delivered just the same. * If the controller handle is needed for things then it gets more tricky.. */ } } for (size_t i = 0; i < n_ops; i++) { play->n_ops++; if (ops[i]->init) play->contexts[i] = ops[i]->init(play, argc, argv, flags); } play->ops = ops; return play; } int play_shutdown(play_t *play) { #if 0 /* XXX: for now just rely on atexit(SDL_Quit()) doing the SDL stuff, * and don't bother cleaning up anything play-specific, we're exiting. */ /* TODO: cleanup all the contexts */ stage_free(play->contexts[play->context].stage); SDL_DestroyWindow(play->window); Mix_CloseAudio(); #endif return EXIT_SUCCESS; } int play_run_slice(play_t *play) { SDL_Event ev; do { play->update_needed = 0; if (play->ops[play->context]->update) play->ops[play->context]->update(play, play->contexts[play->context]); /* see comment in play_context_enter() for why play->update_needed exists */ } while (play->update_needed); if (play->ops[play->context]->render) play->ops[play->context]->render(play, play->contexts[play->context]); while (!play->update_needed && SDL_PollEvent(&ev)) { if (ev.type == SDL_APP_TERMINATING || ev.type == SDL_QUIT) return 0; if (play->ops[play->context]->dispatch) play->ops[play->context]->dispatch(play, play->contexts[play->context], &ev); } return 1; } void play_run(play_t *play) { for (;;) { if (!play_run_slice(play)) return; } }