#include <time.h>
#include <SDL.h>
#include "private.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;
HeliGlStencilOpSeparatePtr heliGlStencilOpSeparatePtr = NULL;
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;
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 (strcmp(keyEventGet(mode, i), code) == 0)
return TRUE;
return FALSE;
}
static void keyEventAdd(KeyEvent mode, const char *code) {
if ((int)mode >= 0 && mode <= (int)keyEventsCount && !keyEventCheck(mode, code))
heliArrayInsert(&keyEvents[mode], -1, heliStringCopy(code), &free);
}
static void keyEventRemove(KeyEvent mode, const char *code) {
if ((int)mode >= 0 && mode <= (int)keyEventsCount)
for(int i = keyEvents[mode].count-1; i >= 0; --i)
if (strcmp(keyEvents[mode].items[i].value, code) == 0)
heliArrayRemove(&keyEvents[mode], i);
}
int keyDown(const char *code)
{ return keyEventCheck(KEYEVENT_KEY_DOWN, code); }
int keyWentDown(const char *code)
{ return keyEventCheck(KEYEVENT_KEY_WENTDOWN, code); }
int keyWentUp(const char *code)
{ return keyEventCheck(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); }
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); }
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); }
double worldGetFrameTime()
{ return frameTime; }
double worldGetMinFrameRate();
void worldSetMinFrameRate(double minFrameRate);
double worldGetMaxFrameRate();
void worldSetMaxFrameRate(double maxFrameRate);
void worldSetFrameRate(double frameRate);
double worldGetFrameTime();
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;
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;
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, width, height, 0, -1.0, 1.0);
glMatrixMode(GL_MODELVIEW);
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) {
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;
}
heliGlStencilOpSeparatePtr = SDL_GL_GetProcAddress("glStencilOpSeparate");
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) {
int count = keyEventGetCount(KEYEVENT_KEY_DOWN);
for(int i = 0; i < count; ++i)
keyEventAdd(KEYEVENT_KEY_WENTUP, keyEventGet(KEYEVENT_KEY_DOWN, i));
heliArrayClear(&keyEvents[KEYEVENT_KEY_DOWN]);
count = keyEventGetCount(KEYEVENT_MOUSE_DOWN);
for(int i = 0; i < count; ++i)
keyEventAdd(KEYEVENT_MOUSE_WENTUP, keyEventGet(KEYEVENT_MOUSE_DOWN, i));
heliArrayClear(&keyEvents[KEYEVENT_MOUSE_DOWN]);
}
} else
if (e->type == SDL_KEYDOWN || e->type == SDL_KEYUP) {
const char *keynameOrig = SDL_GetKeyName(e->key.keysym.sym);
if (keynameOrig && *keynameOrig) {
char *keyname = heliStringCopy(keynameOrig);
heliLowercase(keyname);
if (e->type == SDL_KEYDOWN) {
keyEventAdd(KEYEVENT_KEY_DOWN, keyname);
keyEventAdd(KEYEVENT_KEY_WENTDOWN, keyname);
} else {
keyEventRemove(KEYEVENT_KEY_DOWN, keyname);
keyEventAdd(KEYEVENT_KEY_WENTUP, keyname);
}
free(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) {
keyEventAdd(KEYEVENT_MOUSE_DOWN, button);
keyEventAdd(KEYEVENT_MOUSE_WENTDOWN, button);
} else {
keyEventRemove(KEYEVENT_MOUSE_DOWN, button);
keyEventAdd(KEYEVENT_MOUSE_WENTUP, 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_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;
heliDoTests();
if (initCallback) initCallback();
run();
if (deinitCallback) deinitCallback();
heliArrayClear(&heliObjectsSet);
heliSpriteFinish();
heliDrawingFinish();
heliFontFinish();
heliAnimationFinish();
heliSoundFinish();
heliArrayDestroy(&heliObjectsSet);
heliInitialized = FALSE;
deinit();
for(int i = 0; i < keyEventsCount; ++i)
heliArrayDestroy(&keyEvents[i]);
}
started = FALSE;
}