diff options
Diffstat (limited to 'src/play.c')
-rw-r--r-- | src/play.c | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/src/play.c b/src/play.c new file mode 100644 index 0000000..ca51c2e --- /dev/null +++ b/src/play.c @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include <SDL.h> +#include <SDL_mixer.h> +#include <assert.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdlib.h> /* exit, atexit */ + +#include "macros.h" +#include "play.h" + +/* libplay: SDL2 and SDL2_Mixer glued together a bit */ + +typedef struct play_t { + Mix_Music *music; + char *music_file, *queued_music_file; + unsigned music_flags, queued_music_flags; + Uint32 tick_offsets[PLAY_TICKS_CNT]; + Uint32 ticks_paused_at; + unsigned ticks_paused:1; + unsigned update_needed: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, ...) +{ + 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; + + 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"); +} + + +void play_music_pause(play_t *play) +{ + Mix_PauseMusic(); +} + + +void play_music_resume(play_t *play) +{ + Mix_ResumeMusic(); +} + + +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(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 */ +void 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(); +} + + +/* "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) { + play_ticks_reset(play, timer); + return 1; + } + + return 0; +} + + +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. + */ +play_t * play_startup(int argc, char *argv[], const play_ops_t *ops[]) +{ + 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"); + + fatal_if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_GAMECONTROLLER) != 0, + "Unable to initialize SDL"); + + fatal_if(atexit(SDL_Quit), + "Unable to set exit handler"); + + 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); + + /* 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); + } + 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; +} + + +void play_run(play_t *play) +{ + for (;;) { + 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; + + if (play->ops[play->context]->dispatch) + play->ops[play->context]->dispatch(play, play->contexts[play->context], &ev); + } + } +} |