summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVito Caputo <vcaputo@pengaru.com>2020-04-18 20:29:15 -0700
committerVito Caputo <vcaputo@pengaru.com>2020-04-19 03:27:39 -0700
commitb4911c2abfa513e91c82a712ef277f7e6209b502 (patch)
tree4ee41f6fc432d15147f0bf7e18b032d01efe2231
*: initial commit
This is largely ripped out of Eon to try give a reusable scaffolding for accelerating SDL-based simple game development. I expect there to be future commits adding more configurability like a means of influencing which flags are passed to SDL_Init(), e.g. if you have no need for joystick support, don't pass in that flag, and libplay won't pass it to SDL_Init() and do the joystick opening/mapping dance, etc.
-rw-r--r--Makefile.am1
-rw-r--r--README10
-rwxr-xr-xbootstrap5
-rw-r--r--configure.ac26
-rw-r--r--src/Makefile.am2
-rw-r--r--src/macros.h34
-rw-r--r--src/play.c384
-rw-r--r--src/play.h69
8 files changed, 531 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..af437a6
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1 @@
+SUBDIRS = src
diff --git a/README b/README
new file mode 100644
index 0000000..d4c5571
--- /dev/null
+++ b/README
@@ -0,0 +1,10 @@
+libplay is thin veneer over SDL2 and SDL2_Mixer targeting simple game
+development.
+
+It gives a simple API for music playback with asynchronous next song
+queueing, rudimentary game context switching with event routing, and some
+basic timers built around SDL_Ticks.
+
+Note that this library doesn't do graceful error handling, since it
+targets game development errors are treated as fatal and simply exit
+with something printed to stderr.
diff --git a/bootstrap b/bootstrap
new file mode 100755
index 0000000..99f6f06
--- /dev/null
+++ b/bootstrap
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+aclocal \
+&& automake --gnu --add-missing \
+&& autoconf
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..22cabbd
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,26 @@
+AC_INIT([libplay], [1.0], [vcaputo@pengaru.com])
+AM_INIT_AUTOMAKE([-Wall -Werror foreign])
+AC_PROG_CC
+AM_PROG_CC_C_O
+AM_PROG_AR
+AC_PROG_RANLIB
+AM_SILENT_RULES([yes])
+
+CFLAGS="$CFLAGS -Wall"
+
+dnl Check for SDL2
+PKG_CHECK_MODULES(SDL2, sdl2)
+CFLAGS="$CFLAGS $SDL2_CFLAGS"
+LIBS="$LIBS $SDL2_LIBS"
+
+dnl Check for SDL2_mixer
+PKG_CHECK_MODULES(SDL2_MIXER, SDL2_mixer)
+LIBS="$SDL2_MIXER_LIBS $LIBS"
+CFLAGS="$SDL2_MIXER_CFLAGS $CFLAGS"
+
+AC_CONFIG_FILES([
+ Makefile
+ src/Makefile
+])
+
+AC_OUTPUT
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..aff4573
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,2 @@
+noinst_LIBRARIES = libplay.a
+libplay_a_SOURCES = play.c play.h
diff --git a/src/macros.h b/src/macros.h
new file mode 100644
index 0000000..cee2464
--- /dev/null
+++ b/src/macros.h
@@ -0,0 +1,34 @@
+/*
+ * 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/>.
+ */
+
+#ifndef _MACROS_H
+#define _MACROS_H
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#define fatal_if(_cond, _fmt, ...) \
+ if (_cond) { \
+ fprintf(stderr, "Fatal error: " _fmt "\n", ##__VA_ARGS__); \
+ exit(EXIT_FAILURE); \
+ }
+
+#define warn_if(_cond, _fmt, ...) \
+ if (_cond) { \
+ fprintf(stderr, "Warning: " _fmt "\n", ##__VA_ARGS__); \
+ }
+
+#endif
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);
+ }
+ }
+}
diff --git a/src/play.h b/src/play.h
new file mode 100644
index 0000000..0f1d2da
--- /dev/null
+++ b/src/play.h
@@ -0,0 +1,69 @@
+/*
+ * 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/>.
+ */
+
+#ifndef _PLAY_H
+#define _PLAY_H
+
+#include <SDL.h>
+
+typedef enum play_ticks_t {
+ PLAY_TICKS_TIMER0,
+ PLAY_TICKS_TIMER1,
+ PLAY_TICKS_TIMER2,
+ PLAY_TICKS_TIMER3,
+ PLAY_TICKS_TIMER4,
+ PLAY_TICKS_TIMER5,
+ PLAY_TICKS_TIMER6,
+ PLAY_TICKS_TIMER7,
+ PLAY_TICKS_TIMER8,
+ PLAY_TICKS_TIMER9,
+ PLAY_TICKS_CNT
+} play_ticks_t;
+
+#define PLAY_MUSIC_FLAG_LOOP (1L)
+#define PLAY_MUSIC_FLAG_FADEIN (1L << 1)
+#define PLAY_MUSIC_FLAG_OPTIONAL (1L << 2)
+#define PLAY_MUSIC_FLAG_QUEUE (1L << 3)
+#define PLAY_MUSIC_FLAG_IDEMPOTENT (1L << 4)
+
+typedef struct play_t play_t;
+
+typedef struct play_ops_t {
+ void * (*init)(play_t *play, int argc, char *argv[]);
+ void (*destroy)(play_t *play, void *context);
+ void (*enter)(play_t *play, void *context);
+ void (*leave)(play_t *play, void *context);
+ void (*update)(play_t *play, void *context);
+ void (*render)(play_t *play, void *context);
+ void (*dispatch)(play_t *play, void *context, SDL_Event *event);
+} play_ops_t;
+
+play_t * play_startup(int argc, char *argv[], const play_ops_t *ops[]);
+int play_shutdown(play_t *play);
+void play_run(play_t *play);
+
+void play_music_set(play_t *play, unsigned flags, const char *fmt, ...);
+void play_music_pause(play_t *play);
+void play_music_resume(play_t *play);
+void play_context_enter(play_t *play, int context);
+void * play_context(play_t *play, int context);
+unsigned play_ticks(play_t *play, play_ticks_t timer);
+void play_ticks_reset(play_t *play, play_ticks_t timer);
+void play_ticks_pause(play_t *play);
+int play_ticks_elapsed(play_t *play, play_ticks_t timer, unsigned duration);
+void play_quit(play_t *play);
+
+#endif
© All Rights Reserved