Blob Blame Raw

#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 = 24;
static double maxFPS = 24;
static double frameTime = 1/24;

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;
}

int mousePressedOver(Sprite sprite)
	{ return keyEventGetCount(KEYEVENT_MOUSE_DOWN) && mouseIsOver(sprite); }

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 > 1)) minFrameRate = 1;
	if (!(minFrameRate < 100)) minFrameRate = 100;
	if (!(maxFrameRate > 1)) maxFrameRate = 1;
	if (!(maxFrameRate < 100)) maxFrameRate = 100;
	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();
}

void askText(const char *question, char *answer, int maxAnswerSize)
	{ 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;
			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;
			}
		}
	}
}


void 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();
	
	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();
}


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;
}