#include <time.h>
#include <SDL.h>
#include "private.h"
#include "drawing.h"
#include "world.h"
static int sdlInitialized = 0;
static SDL_Window *window;
static SDL_GLContext context;
static int started;
static int stopped;
static int firstFrame = TRUE;
static unsigned long long frameCount;
static unsigned long long elapsedTimeUs;
static unsigned long long elapsedTimeSinceLastFrameUs;
static unsigned int prevFrameTimeMs;
HeliDialog dialog;
static Callback initCallback;
static Callback drawCallback;
static Callback deinitCallback;
static const int minWidth = 200;
static const int minHeight = 200;
static int width = 512;
static int height = 512;
static int resizable;
static char title[1000];
static int titleSize = (int)(sizeof(title)/sizeof(*title));
static double minFPS = HELI_DEFAULT_FPS;
static double maxFPS = HELI_DEFAULT_FPS;
static double frameTime = 1.0/HELI_DEFAULT_FPS;
static HeliArray keyEvents[6];
static int keyEventsCount = (int)(sizeof(keyEvents)/sizeof(*keyEvents));
static int mouseMovedInFrame;
static double _mouseX;
static double _mouseY;
static int _mouseScrolledX;
static int _mouseScrolledY;
static const char* keyAliases[][2] = {
// as user asked | as internally stored //
{ "enter" , "return" },
{ "any enter" , "any return" },
{ "keypad return" , "keypad enter" } };
static int keyAliasesCount = (int)(sizeof(keyAliases)/sizeof(*keyAliases));
static const char* keyGroups[][2] = {
// as SDL passed | as internally stored //
{ "left shift" , "any shift" },
{ "left ctrl" , "any ctrl" },
{ "left alt" , "any alt" },
{ "left gui" , "any gui" },
{ "right shift" , "any shift" },
{ "right ctrl" , "any ctrl" },
{ "right alt" , "any alt" },
{ "right gui" , "any gui" },
{ "0" , "any 0" },
{ "1" , "any 1" },
{ "2" , "any 2" },
{ "3" , "any 3" },
{ "4" , "any 4" },
{ "5" , "any 5" },
{ "6" , "any 6" },
{ "7" , "any 7" },
{ "8" , "any 8" },
{ "9" , "any 9" },
{ "/" , "any /" },
{ "*" , "any *" },
{ "-" , "any -" },
{ "+" , "any +" },
{ "return" , "any return" },
{ "keypad 0" , "any 0" },
{ "keypad 1" , "any 1" },
{ "keypad 2" , "any 2" },
{ "keypad 3" , "any 3" },
{ "keypad 4" , "any 4" },
{ "keypad 5" , "any 5" },
{ "keypad 6" , "any 6" },
{ "keypad 7" , "any 7" },
{ "keypad 8" , "any 8" },
{ "keypad 9" , "any 9" },
{ "keypad 9" , "any 9" },
{ "keypad /" , "any /" },
{ "keypad *" , "any *" },
{ "keypad -" , "any -" },
{ "keypad +" , "any +" },
{ "keypad enter" , "any return" } };
static int keyGroupsCount = (int)(sizeof(keyGroups)/sizeof(*keyGroups));
int keyEventGetCount(KeyEvent mode)
{ return (int)mode >= 0 && (int)mode <= keyEventsCount ? keyEvents[mode].count : 0; }
const char *keyEventGet(KeyEvent mode, int i)
{ return (int)mode >= 0 && (int)mode <= keyEventsCount ? heliArrayGetValue(&keyEvents[mode], i) : NULL; }
static int keyEventCheck(KeyEvent mode, const char *code) {
int count = keyEventGetCount(mode);
for(int i = 0; i < count; ++i)
if (heliStringCompareCi(keyEventGet(mode, i), code) == 0)
return TRUE;
return FALSE;
}
static int keyEventAliasesCheck(KeyEvent mode, const char *code) {
if (keyEventCheck(mode, code)) return TRUE;
for(int i = 0; i < keyAliasesCount; ++i)
if (heliStringCompareCi(code, keyAliases[i][0]) == 0)
if (keyEventCheck(mode, keyAliases[i][1])) return TRUE;
return FALSE;
}
static int keyEventAdd(KeyEvent mode, const char *code) {
if ((int)mode >= 0 && mode <= (int)keyEventsCount && !keyEventCheck(mode, code)) {
heliArrayInsert(&keyEvents[mode], -1, heliStringCopyLower(code), &free);
return TRUE;
}
return FALSE;
}
static int keyEventRemove(KeyEvent mode, const char *code) {
int removed = FALSE;
if ((int)mode >= 0 && mode <= (int)keyEventsCount)
for(int i = keyEvents[mode].count-1; i >= 0; --i)
if (heliStringCompareCi(keyEvents[mode].items[i].value, code) == 0)
{ heliArrayRemove(&keyEvents[mode], i); removed = TRUE; }
return removed;
}
static void pressKey(int mouse, const char *code) {
keyEventAdd(mouse ? KEYEVENT_MOUSE_DOWN : KEYEVENT_KEY_DOWN, code);
keyEventAdd(mouse ? KEYEVENT_MOUSE_WENTDOWN : KEYEVENT_KEY_WENTDOWN, code);
if (!mouse) {
for(int i = 0; i < keyGroupsCount; ++i) {
if (heliStringCompareCi(code, keyGroups[i][0]) == 0) {
keyEventAdd(KEYEVENT_KEY_DOWN, keyGroups[i][1]);
keyEventAdd(KEYEVENT_KEY_WENTDOWN, keyGroups[i][1]);
}
}
}
}
static void releaseKey(int mouse, const char *code) {
if (!keyEventRemove(mouse ? KEYEVENT_MOUSE_DOWN : KEYEVENT_KEY_DOWN, code))
return;
keyEventAdd(mouse ? KEYEVENT_MOUSE_WENTUP : KEYEVENT_KEY_WENTUP, code);
if (mouse)
return;
for(int i = 0; i < keyGroupsCount; ++i) {
if (heliStringCompareCi(code, keyGroups[i][0]) != 0) continue;
int allRemoved = TRUE;
for(int j = 0; j < keyGroupsCount; ++j)
if ( heliStringCompareCi(keyGroups[i][1], keyGroups[j][1]) == 0
&& keyEventCheck(KEYEVENT_KEY_DOWN, keyGroups[j][0]))
{ allRemoved = FALSE; break; }
if (allRemoved)
if (keyEventRemove(KEYEVENT_KEY_DOWN, keyGroups[i][1]))
keyEventAdd(KEYEVENT_KEY_WENTUP, keyGroups[i][1]);
}
}
static void releaseAllKeys() {
int count = keyEventGetCount(KEYEVENT_KEY_DOWN);
for(int i = count-1; i >= 0; --i)
keyEventAdd(KEYEVENT_KEY_WENTUP, keyEventGet(KEYEVENT_KEY_DOWN, i));
heliArrayClear(&keyEvents[KEYEVENT_KEY_DOWN]);
count = keyEventGetCount(KEYEVENT_MOUSE_DOWN);
for(int i = count-1; i >= 0; --i)
keyEventAdd(KEYEVENT_MOUSE_WENTUP, keyEventGet(KEYEVENT_MOUSE_DOWN, i));
heliArrayClear(&keyEvents[KEYEVENT_MOUSE_DOWN]);
}
int keyDown(const char *code)
{ return keyEventAliasesCheck(KEYEVENT_KEY_DOWN, code); }
int keyWentDown(const char *code)
{ return keyEventAliasesCheck(KEYEVENT_KEY_WENTDOWN, code); }
int keyWentUp(const char *code)
{ return keyEventAliasesCheck(KEYEVENT_KEY_WENTUP, code); }
int mouseDidMove()
{ return mouseMovedInFrame; }
int mouseDown(const char *code)
{ return keyEventCheck(KEYEVENT_MOUSE_DOWN, code); }
int mouseWentDown(const char *code)
{ return keyEventCheck(KEYEVENT_MOUSE_WENTDOWN, code); }
int mouseWentUp(const char *code)
{ return keyEventCheck(KEYEVENT_MOUSE_WENTUP, code); }
int mouseScrolledX()
{ return _mouseScrolledX; }
int mouseScrolledY()
{ return _mouseScrolledY; }
double mouseX()
{ return _mouseX; }
double mouseY()
{ return _mouseY; }
double transformedMouseX() {
double x = mouseX(), y = mouseY();
heliGLBackTransform(&x, &y);
return x;
}
double transformedMouseY() {
double x = mouseX(), y = mouseY();
heliGLBackTransform(&x, &y);
return y;
}
static void resize(int w, int h) {
w = w > minWidth ? w : minWidth;
h = h > minHeight ? h : minHeight;
if (width != w || height != h) {
width = w;
height = h;
if (started && !stopped && window)
SDL_SetWindowSize(window, width, height);
}
}
int worldGetWidth()
{ return width; }
void worldSetWidth(int w)
{ resize(w, height); }
int worldGetHeight()
{ return height; }
void worldSetHeight(int h)
{ resize(width, h); }
void worldSetSize(int w, int h)
{ resize(w, h); }
int worldGetResizable()
{ return resizable; }
void worldSetResizable(int r) {
if (resizable == r) return;
resizable = r ? TRUE : FALSE;
if (started && !stopped && window)
SDL_SetWindowResizable(window, resizable);
}
const char* worldGetTitle()
{ return title; }
void worldSetTitle(const char *t) {
int changed = FALSE;
for(int i = 0; i < titleSize-1; ++i) {
if (title[i] != t[i]) changed = TRUE;
title[i] = t[i];
if (!t[i]) break;
}
if (changed && started && !stopped && window)
SDL_SetWindowTitle(window, title);
}
double worldGetMinFrameRate()
{ return minFPS; }
double worldGetMaxFrameRate()
{ return minFPS; }
void worldSetFrameRateEx(double minFrameRate, double maxFrameRate) {
if (!(minFrameRate > HELI_MIN_FPS)) minFrameRate = HELI_MIN_FPS;
if (!(minFrameRate < HELI_MAX_FPS)) minFrameRate = HELI_MAX_FPS;
if (!(maxFrameRate > HELI_MIN_FPS)) maxFrameRate = HELI_MIN_FPS;
if (!(maxFrameRate < HELI_MAX_FPS)) maxFrameRate = HELI_MAX_FPS;
if (minFrameRate > maxFrameRate) minFrameRate = maxFrameRate;
minFPS = minFrameRate;
maxFPS = maxFrameRate;
}
void worldSetFrameRate(double frameRate)
{ worldSetFrameRateEx(frameRate, frameRate); }
void worldSetVariableFrameRate()
{ worldSetFrameRateEx(HELI_MIN_FPS, HELI_MAX_FPS); }
double worldGetFrameTime()
{ return frameTime; }
int worldGetFrameCount()
{ return (int)frameCount; }
double worldGetSeconds()
{ return started ? elapsedTimeUs*1e-6 : 0.0; }
void worldSetInit(Callback init)
{ initCallback = init; }
void worldSetDraw(Callback draw)
{ drawCallback = draw; }
void worldSetDeinit(Callback deinit)
{ deinitCallback = deinit; }
void worldStop()
{ if (started) stopped = TRUE; }
void messageBox(const char *message) {
SDL_ShowSimpleMessageBox(
SDL_MESSAGEBOX_INFORMATION,
title,
message,
window );
prevFrameTimeMs = SDL_GetTicks();
}
int questionBox(const char *question, const char *answer0, const char *answer1) {
SDL_MessageBoxButtonData buttons[2] = {};
buttons[0].buttonid = 1;
buttons[0].flags = SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT;
buttons[0].text = answer1;
buttons[1].buttonid = 0;
buttons[1].flags = SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT;
buttons[1].text = answer0;
SDL_MessageBoxData data = {};
data.flags = SDL_MESSAGEBOX_INFORMATION;
data.window = window;
data.title = title;
data.message = question;
data.buttons = buttons;
data.numbuttons = 2;
int buttonid = 0;;
SDL_ShowMessageBox(&data, &buttonid);
prevFrameTimeMs = SDL_GetTicks();
return buttonid;
}
int questionBox3(const char* question, const char* answer0, const char* answer1, const char* answer2) {
SDL_MessageBoxButtonData buttons[3] = {};
buttons[0].buttonid = 2;
buttons[0].flags = SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT;
buttons[0].text = answer2;
buttons[1].buttonid = 1;
buttons[1].text = answer1;
buttons[2].buttonid = 0;
buttons[2].flags = SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT;
buttons[2].text = answer0;
SDL_MessageBoxData data = {};
data.flags = SDL_MESSAGEBOX_INFORMATION;
data.window = window;
data.title = title;
data.message = question;
data.buttons = buttons;
data.numbuttons = 3;
int buttonid = 0;;
SDL_ShowMessageBox(&data, &buttonid);
prevFrameTimeMs = SDL_GetTicks();
return buttonid;
}
int askText(const char *question, char *answer, int maxAnswerSize)
{ return askTextEx(question, answer, maxAnswerSize, FALSE, FALSE); }
static void resetEvents() {
dialog.newText[0] = 0;
_mouseScrolledX = _mouseScrolledY = 0;
heliArrayClear(&keyEvents[KEYEVENT_KEY_WENTDOWN]);
heliArrayClear(&keyEvents[KEYEVENT_KEY_WENTUP]);
heliArrayClear(&keyEvents[KEYEVENT_MOUSE_WENTDOWN]);
heliArrayClear(&keyEvents[KEYEVENT_MOUSE_WENTUP]);
}
static void draw() {
unsigned int currentFrameTimeMs = SDL_GetTicks();
unsigned long long deltaUs = firstFrame ? 0 : (currentFrameTimeMs - prevFrameTimeMs)*1000ull;
prevFrameTimeMs = currentFrameTimeMs;
double actualMinFPS = minFPS, actualMaxFPS = maxFPS;
if (dialog.shown) { actualMinFPS = 1, actualMaxFPS = 100; }
unsigned long long minTimeStepUs = (unsigned long long)round(1e6/actualMaxFPS);
unsigned long long maxTimeStepUs = (unsigned long long)round(1e6/actualMinFPS);
elapsedTimeSinceLastFrameUs += deltaUs;
if (elapsedTimeSinceLastFrameUs > 2000000)
elapsedTimeSinceLastFrameUs = 2000000;
if (firstFrame || elapsedTimeSinceLastFrameUs >= minTimeStepUs) {
unsigned long long encountedTimeUs = elapsedTimeSinceLastFrameUs;
if (encountedTimeUs > maxTimeStepUs) encountedTimeUs = maxTimeStepUs;
double dt = encountedTimeUs*1e-6;
if (!firstFrame) elapsedTimeSinceLastFrameUs -= encountedTimeUs;
if (!dialog.shown) {
elapsedTimeUs += encountedTimeUs;
++frameCount;
frameTime = firstFrame ? 1/maxFPS : dt;
heliAnimationUpdate(dt);
heliSpriteUpdate(dt);
}
heliSoundUpdate();
firstFrame = FALSE;
viewportByWindow();
projectionByViewport();
heliDrawingPrepareFrame();
if (dialog.shown) {
heliDialogDraw(&dialog);
} else {
if (drawCallback)
drawCallback();
}
resetEvents();
SDL_GL_SwapWindow(window);
}
unsigned long long addUs = elapsedTimeSinceLastFrameUs + (SDL_GetTicks() - prevFrameTimeMs)*1000ull;
if (addUs < minTimeStepUs) {
unsigned long long waitUs = minTimeStepUs - addUs;
if (waitUs > 2000)
SDL_Delay( (unsigned int)((waitUs + 500)/1000ull) );
}
}
static void deinit() {
heliGLStencilOpSeparatePtr = NULL;
if (context) SDL_GL_DeleteContext(context);
context = NULL;
if (window) SDL_DestroyWindow(window);
window = NULL;
if (sdlInitialized) SDL_Quit();
sdlInitialized = FALSE;
}
static int init() {
if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
fprintf(stderr, "helianthus: SDL_Init failed\n");
deinit();
return FALSE;
}
sdlInitialized = TRUE;
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 8);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetSwapInterval(1);
unsigned int flags = SDL_WINDOW_OPENGL;
if (resizable) flags |= SDL_WINDOW_RESIZABLE;
window = SDL_CreateWindow(
title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
width, height, flags );
if (!window) {
// try to create window without multisampling
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0);
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 0);
window = SDL_CreateWindow(
title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
width, height, flags );
}
if (!window) {
fprintf(stderr, "helianthus: cannot create window: %s\n", SDL_GetError());
SDL_ClearError();
deinit();
return FALSE;
}
SDL_SetWindowMinimumSize(window, minWidth, minHeight);
context = SDL_GL_CreateContext(window);
if (!context) {
fprintf(stderr, "helianthus: cannot create OpenGL context: %s\n", SDL_GetError());
SDL_ClearError();
deinit();
return FALSE;
}
heliGLBlendFuncSeparatePtr = SDL_GL_GetProcAddress("glBlendFuncSeparate");
heliGLStencilOpSeparatePtr = SDL_GL_GetProcAddress("glStencilOpSeparate");
heliGLTexImage2DMultisamplePtr = SDL_GL_GetProcAddress("glTexImage2DMultisample");
heliGLGenFramebuffersPtr = SDL_GL_GetProcAddress("glGenFramebuffers");
heliGLDeleteFramebuffersPtr = SDL_GL_GetProcAddress("glDeleteFramebuffers");
heliGLBindFramebufferPtr = SDL_GL_GetProcAddress("glBindFramebuffer");
heliGLBlitFramebufferPtr = SDL_GL_GetProcAddress("glBlitFramebuffer");
heliGLFramebufferRenderbufferPtr = SDL_GL_GetProcAddress("glFramebufferRenderbuffer");
heliGLFramebufferTexture2DPtr = SDL_GL_GetProcAddress("glFramebufferTexture2D");
heliGLCheckFramebufferStatusPtr = SDL_GL_GetProcAddress("glCheckFramebufferStatus");
heliGLGenRenderbuffersPtr = SDL_GL_GetProcAddress("glGenRenderbuffers");
heliGLDeleteRenderbuffersPtr = SDL_GL_GetProcAddress("glDeleteRenderbuffers");
heliGLBindRenderbufferPtr = SDL_GL_GetProcAddress("glBindRenderbuffer");
heliGLRenderbufferStoragePtr = SDL_GL_GetProcAddress("glRenderbufferStorage");
heliGLRenderbufferStorageMultisamplePtr = SDL_GL_GetProcAddress("glRenderbufferStorageMultisample");
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, (int*)&heliGLWindowFramebufferReadId);
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, (int*)&heliGLWindowFramebufferDrawId);
return TRUE;
}
static void handleEvent(SDL_Event *e) {
if (e->type == SDL_QUIT) {
stopped = TRUE;
} else
if (e->type == SDL_WINDOWEVENT) {
if (e->window.event == SDL_WINDOWEVENT_CLOSE) {
stopped = TRUE;
} else
if (e->window.event == SDL_WINDOWEVENT_RESIZED) {
width = e->window.data1;
height = e->window.data2;
} else
if (e->window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
releaseAllKeys();
}
} else
if (e->type == SDL_KEYDOWN || e->type == SDL_KEYUP) {
const char *keyname = SDL_GetKeyName(e->key.keysym.sym);
if (keyname && *keyname) {
if (e->type == SDL_KEYDOWN)
pressKey(FALSE, keyname); else releaseKey(FALSE, keyname);
}
} else
if (e->type == SDL_MOUSEBUTTONDOWN || e->type == SDL_MOUSEBUTTONUP) {
char *button = NULL;
switch(e->button.button) {
case SDL_BUTTON_LEFT: button = "left"; break;
case SDL_BUTTON_MIDDLE: button = "middle"; break;
case SDL_BUTTON_RIGHT: button = "right"; break;
default: break;
}
if (button) {
if (e->type == SDL_MOUSEBUTTONDOWN)
pressKey(TRUE, button); else releaseKey(TRUE, button);
}
_mouseX = e->button.x;
_mouseY = e->button.y;
} else
if (e->type == SDL_MOUSEMOTION) {
_mouseX = e->motion.x;
_mouseY = e->motion.y;
} else
if (e->type == SDL_MOUSEWHEEL) {
_mouseScrolledX += e->wheel.x;
_mouseScrolledY += e->wheel.y;
} else
if (e->type == SDL_TEXTINPUT) {
if (dialog.shown) {
int len = strlen(dialog.newText);
int newlen = strlen(e->text.text);
int dl = len + newlen + 1 - sizeof(dialog.newText);
if (dl > 0) newlen -= dl;
if (newlen > 0) {
memcpy(dialog.newText + len, e->text.text, newlen);
dialog.newText[len + newlen] = 0;
}
}
}
}
int askTextEx(const char *question, char *answer, int maxAnswerSize, int multiline, int password) {
if (maxAnswerSize < 0 || !answer) maxAnswerSize = 0;
memset(&dialog, 0, sizeof(dialog));
dialog.shown = TRUE;
dialog.question = question ? question : "";
dialog.answer = calloc(1, maxAnswerSize + 1);
dialog.passwordText = calloc(1, maxAnswerSize*4 + 1);
dialog.maxAnswerSize = maxAnswerSize - 1;
if (maxAnswerSize > 0) memcpy(dialog.answer, answer, maxAnswerSize);
dialog.multiline = multiline != 0 && password == 0;
dialog.password = password != 0;
dialog.pos = dialog.selPos = strlen(dialog.answer);
dialog.success = FALSE;
SDL_StartTextInput();
while(dialog.shown && !stopped) {
SDL_Event event;
while (SDL_PollEvent(&event))
handleEvent(&event);
draw();
}
SDL_StopTextInput();
int success = dialog.success;
if (dialog.success && maxAnswerSize > 0) strcpy(answer, dialog.answer);
free(dialog.answer);
free(dialog.passwordText);
memset(&dialog, 0, sizeof(dialog));
resetEvents();
heliArrayClear(&keyEvents[KEYEVENT_KEY_DOWN]);
heliArrayClear(&keyEvents[KEYEVENT_MOUSE_DOWN]);
prevFrameTimeMs = SDL_GetTicks();
heliDrawingPrepareFrame();
return success;
}
static void run() {
while(!stopped) {
SDL_Event event;
while (SDL_PollEvent(&event))
handleEvent(&event);
draw();
}
}
void worldRun() {
if (started) return;
started = TRUE;
stopped = FALSE;
firstFrame = TRUE;
frameCount = 0;
elapsedTimeUs = 0;
elapsedTimeSinceLastFrameUs = 0;
srand(time(NULL));
if (init()) {
heliInitialized = TRUE;
resetState();
heliDoTests();
viewportByWindow();
projectionByViewport();
heliDrawingPrepareFrame();
if (initCallback) initCallback();
run();
if (deinitCallback) deinitCallback();
heliArrayClear(&heliObjectsSet);
heliSpriteFinish();
heliDrawingFinish();
heliFontFinish();
heliAnimationFinish();
heliSoundFinish();
heliArrayDestroy(&heliObjectsSet);
heliInitialized = FALSE;
deinit();
_mouseScrolledX = _mouseScrolledY = 0;
for(int i = 0; i < keyEventsCount; ++i)
heliArrayDestroy(&keyEvents[i]);
}
started = FALSE;
}