summary refs log tree commit diff
path: root/src/Game.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/Game.c
initial commit
Diffstat (limited to 'src/Game.c')
-rw-r--r--src/Game.c796
1 files changed, 796 insertions, 0 deletions
diff --git a/src/Game.c b/src/Game.c
new file mode 100644
index 0000000..185eec4
--- /dev/null
+++ b/src/Game.c
@@ -0,0 +1,796 @@
+#include "Game.h"
+#include "Block.h"
+#include "World.h"
+#include "Lighting.h"
+#include "MapRenderer.h"
+#include "Graphics.h"
+#include "Camera.h"
+#include "Options.h"
+#include "Funcs.h"
+#include "ExtMath.h"
+#include "Gui.h"
+#include "Window.h"
+#include "Event.h"
+#include "Utils.h"
+#include "Logger.h"
+#include "Entity.h"
+#include "Chat.h"
+#include "Commands.h"
+#include "Drawer2D.h"
+#include "Model.h"
+#include "Particle.h"
+#include "Http.h"
+#include "Inventory.h"
+#include "Input.h"
+#include "Server.h"
+#include "TexturePack.h"
+#include "Screens.h"
+#include "SelectionBox.h"
+#include "AxisLinesRenderer.h"
+#include "EnvRenderer.h"
+#include "HeldBlockRenderer.h"
+#include "SelOutlineRenderer.h"
+#include "Menus.h"
+#include "Audio.h"
+#include "Stream.h"
+#include "Builder.h"
+#include "Protocol.h"
+#include "Picking.h"
+#include "Animations.h"
+#include "SystemFonts.h"
+#include "Formats.h"
+#include "EntityRenderers.h"
+
+struct _GameData Game;
+cc_uint64 Game_FrameStart;
+cc_bool Game_UseCPEBlocks;
+
+struct RayTracer Game_SelectedPos;
+int Game_ViewDistance     = DEFAULT_VIEWDIST;
+int Game_UserViewDistance = DEFAULT_VIEWDIST;
+int Game_MaxViewDistance  = DEFAULT_MAX_VIEWDIST;
+
+int     Game_FpsLimit, Game_Vertices;
+cc_bool Game_SimpleArmsAnim;
+static cc_bool gameRunning;
+
+cc_bool Game_ClassicMode, Game_ClassicHacks;
+cc_bool Game_AllowCustomBlocks;
+cc_bool Game_AllowServerTextures;
+cc_bool Game_Anaglyph3D;
+
+cc_bool Game_ViewBobbing, Game_HideGui;
+cc_bool Game_BreakableLiquids, Game_ScreenshotRequested;
+struct GameVersion Game_Version;
+
+static char usernameBuffer[STRING_SIZE];
+static char mppassBuffer[STRING_SIZE];
+cc_string Game_Username  = String_FromArray(usernameBuffer);
+cc_string Game_Mppass    = String_FromArray(mppassBuffer);
+#ifdef CC_BUILD_SPLITSCREEN
+int Game_NumLocalPlayers = 1;
+#endif
+
+const char* const FpsLimit_Names[FPS_LIMIT_COUNT] = {
+	"LimitVSync", "Limit30FPS", "Limit60FPS", "Limit120FPS", "Limit144FPS", "LimitNone",
+};
+
+static struct IGameComponent* comps_head;
+static struct IGameComponent* comps_tail;
+void Game_AddComponent(struct IGameComponent* comp) {
+	LinkedList_Append(comp, comps_head, comps_tail);
+}
+
+#define TASKS_DEF_ELEMS 6
+static struct ScheduledTask defaultTasks[TASKS_DEF_ELEMS];
+static int tasksCapacity = TASKS_DEF_ELEMS, tasksCount, entTaskI;
+static struct ScheduledTask* tasks = defaultTasks;
+
+int ScheduledTask_Add(double interval, ScheduledTaskCallback callback) {
+	struct ScheduledTask task;
+	task.accumulator = 0.0;
+	task.interval    = interval;
+	task.Callback    = callback;
+
+	if (tasksCount == tasksCapacity) {
+		Utils_Resize((void**)&tasks, &tasksCapacity,
+			sizeof(struct ScheduledTask), TASKS_DEF_ELEMS, TASKS_DEF_ELEMS);
+	}
+
+	tasks[tasksCount++] = task;
+	return tasksCount - 1;
+}
+
+
+void Game_ToggleFullscreen(void) {
+	int state = Window_GetWindowState();
+	cc_result res;
+
+	if (state == WINDOW_STATE_FULLSCREEN) {
+		res = Window_ExitFullscreen();
+		if (res) Logger_SysWarn(res, "leaving fullscreen");
+	} else {
+		res = Window_EnterFullscreen();
+		if (res) Logger_SysWarn(res, "going fullscreen");
+	}
+}
+
+static void CycleViewDistanceForwards(const short* viewDists, int count) {
+	int i, dist;
+	for (i = 0; i < count; i++) {
+		dist = viewDists[i];
+
+		if (dist > Game_UserViewDistance) {
+			Game_UserSetViewDistance(dist); return;
+		}
+	}
+	Game_UserSetViewDistance(viewDists[0]);
+}
+
+static void CycleViewDistanceBackwards(const short* viewDists, int count) {
+	int i, dist;
+	for (i = count - 1; i >= 0; i--) {
+		dist = viewDists[i];
+
+		if (dist < Game_UserViewDistance) {
+			Game_UserSetViewDistance(dist); return;
+		}
+	}
+	Game_UserSetViewDistance(viewDists[count - 1]);
+}
+
+static const short normalDists[]  = { 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 };
+static const short classicDists[] = { 8, 32, 128, 512 };
+void Game_CycleViewDistance(void) {
+	const short* dists = Gui.ClassicMenu ? classicDists : normalDists;
+	int count = Gui.ClassicMenu ? Array_Elems(classicDists) : Array_Elems(normalDists);
+
+	if (Input_IsShiftPressed()) {
+		CycleViewDistanceBackwards(dists, count);
+	} else {
+		CycleViewDistanceForwards(dists,  count);
+	}
+}
+
+cc_bool Game_ReduceVRAM(void) {
+	if (Game_UserViewDistance <= 16) return false;
+	Game_UserViewDistance /= 2;
+	Game_UserViewDistance = max(16, Game_UserViewDistance);
+
+	MapRenderer_Refresh();
+	Game_SetViewDistance(Game_UserViewDistance);
+	Chat_AddRaw("&cOut of VRAM! Halving view distance..");
+	return true;
+}
+
+
+void Game_SetViewDistance(int distance) {
+	distance = min(distance, Game_MaxViewDistance);
+	if (distance == Game_ViewDistance) return;
+	Game_ViewDistance = distance;
+
+	Event_RaiseVoid(&GfxEvents.ViewDistanceChanged);
+	Camera_UpdateProjection();
+}
+
+void Game_UserSetViewDistance(int distance) {
+	Game_UserViewDistance = distance;
+	Options_SetInt(OPT_VIEW_DISTANCE, distance);
+	Game_SetViewDistance(distance);
+}
+
+void Game_Disconnect(const cc_string* title, const cc_string* reason) {
+	Event_RaiseVoid(&NetEvents.Disconnected);
+	Game_Reset();
+	DisconnectScreen_Show(title, reason);
+}
+
+void Game_Reset(void) {
+	struct IGameComponent* comp;
+	World_NewMap();
+
+	for (comp = comps_head; comp; comp = comp->next) {
+		if (comp->Reset) comp->Reset();
+	}
+}
+
+void Game_UpdateBlock(int x, int y, int z, BlockID block) {
+	BlockID old = World_GetBlock(x, y, z);
+	World_SetBlock(x, y, z, block);
+
+	if (Weather_Heightmap) {
+		EnvRenderer_OnBlockChanged(x, y, z, old, block);
+	}
+	Lighting.OnBlockChanged(x, y, z, old, block);
+	MapRenderer_OnBlockChanged(x, y, z, block);
+}
+
+void Game_ChangeBlock(int x, int y, int z, BlockID block) {
+	BlockID old = World_GetBlock(x, y, z);
+	Game_UpdateBlock(x, y, z, block);
+	Server.SendBlock(x, y, z, old, block);
+}
+
+cc_bool Game_CanPick(BlockID block) {
+	if (Blocks.Draw[block] == DRAW_GAS)    return false;
+	if (Blocks.Draw[block] == DRAW_SPRITE) return true;
+	return Blocks.Collide[block] != COLLIDE_LIQUID || Game_BreakableLiquids;
+}
+
+cc_bool Game_UpdateTexture(GfxResourceID* texId, struct Stream* src, const cc_string* file, 
+							cc_uint8* skinType, int* heightDivisor) {
+	struct Bitmap bmp;
+	cc_bool success;
+	cc_result res;
+	
+	res = Png_Decode(&bmp, src);
+	if (res) { Logger_SysWarn2(res, "decoding", file); }
+	
+	/* E.g. gui.png only need top half of the texture loaded */
+	if (heightDivisor && bmp.height >= *heightDivisor) 
+		bmp.height /= *heightDivisor;
+
+	success = !res && Game_ValidateBitmap(file, &bmp);
+	if (success) {
+		if (skinType) { *skinType = Utils_CalcSkinType(&bmp); }
+		Gfx_RecreateTexture(texId, &bmp, TEXTURE_FLAG_MANAGED, false);
+	}
+
+	Mem_Free(bmp.scan0);
+	return success;
+}
+
+cc_bool Game_ValidateBitmap(const cc_string* file, struct Bitmap* bmp) {
+	int maxWidth = Gfx.MaxTexWidth, maxHeight = Gfx.MaxTexHeight;
+	float texSize, maxSize;
+
+	if (!bmp->scan0) {
+		Chat_Add1("&cError loading %s from the texture pack.", file);
+		return false;
+	}
+	
+	if (bmp->width > maxWidth || bmp->height > maxHeight) {
+		Chat_Add1("&cUnable to use %s from the texture pack.", file);
+
+		Chat_Add4("&c Its size is (%i,%i), your GPU supports (%i,%i) at most.", 
+				&bmp->width, &bmp->height, &maxWidth, &maxHeight);
+		return false;
+	}
+
+	if (Gfx.MaxTexSize && (bmp->width * bmp->height > Gfx.MaxTexSize)) {
+		Chat_Add1("&cUnable to use %s from the texture pack.", file);
+		texSize = (bmp->width * bmp->height) / (1024.0f * 1024.0f);
+		maxSize = Gfx.MaxTexSize             / (1024.0f * 1024.0f);
+
+		Chat_Add2("&c Its size is %f3 MB, your GPU supports %f3 MB at most.", 
+				&texSize, &maxSize);
+		return false;
+	}
+
+	return Game_ValidateBitmapPow2(file, bmp);
+}
+
+cc_bool Game_ValidateBitmapPow2(const cc_string* file, struct Bitmap* bmp) {
+	if (!Math_IsPowOf2(bmp->width) || !Math_IsPowOf2(bmp->height)) {
+		Chat_Add1("&cUnable to use %s from the texture pack.", file);
+
+		Chat_Add2("&c Its size is (%i,%i), which is not a power of two size.", 
+			&bmp->width, &bmp->height);
+		return false;
+	}
+	return true;
+}
+
+void Game_UpdateDimensions(void) {
+	Game.Width  = max(Window_Main.Width,  1);
+	Game.Height = max(Window_Main.Height, 1);
+}
+
+static void Game_OnResize(void* obj) {
+	Game_UpdateDimensions();
+	Gfx_OnWindowResize();
+	Camera_UpdateProjection();
+}
+
+static void HandleOnNewMap(void* obj) {
+	struct IGameComponent* comp;
+	for (comp = comps_head; comp; comp = comp->next) {
+		if (comp->OnNewMap) comp->OnNewMap();
+	}
+}
+
+static void HandleOnNewMapLoaded(void* obj) {
+	struct IGameComponent* comp;
+	for (comp = comps_head; comp; comp = comp->next) {
+		if (comp->OnNewMapLoaded) comp->OnNewMapLoaded();
+	}
+}
+
+static void HandleInactiveChanged(void* obj) {
+	if (Window_Main.Inactive) {
+		Chat_AddOf(&Gfx_LowPerfMessage, MSG_TYPE_EXTRASTATUS_2);
+		Gfx_SetFpsLimit(false, 1000 / 1.0f);
+		Gfx.ReducedPerfMode = true;
+	} else {
+		Chat_AddOf(&String_Empty,       MSG_TYPE_EXTRASTATUS_2);
+		Game_SetFpsLimit(Game_FpsLimit);
+
+		Gfx.ReducedPerfMode         = false;
+		Gfx.ReducedPerfModeCooldown = 2;
+	}
+
+#ifdef CC_BUILD_WEB
+	extern void emscripten_resume_main_loop(void);
+	emscripten_resume_main_loop();
+#endif
+}
+
+static void Game_WarnFunc(const cc_string* msg) {
+	cc_string str = *msg, line;
+	while (str.length) {
+		String_UNSAFE_SplitBy(&str, '\n', &line);
+		Chat_Add1("&c%s", &line);
+	}
+}
+
+static void LoadOptions(void) {
+	Game_ClassicMode  = Options_GetBool(OPT_CLASSIC_MODE,  false);
+	Game_ClassicHacks = Options_GetBool(OPT_CLASSIC_HACKS, false);
+	Game_Anaglyph3D   = Options_GetBool(OPT_ANAGLYPH3D,    false);
+	Game_ViewBobbing  = Options_GetBool(OPT_VIEW_BOBBING,  true);
+	
+	Game_AllowCustomBlocks   = !Game_ClassicMode && Options_GetBool(OPT_CUSTOM_BLOCKS,      true);
+	Game_SimpleArmsAnim      = !Game_ClassicMode && Options_GetBool(OPT_SIMPLE_ARMS_ANIM,   false);
+	Game_BreakableLiquids    = !Game_ClassicMode && Options_GetBool(OPT_MODIFIABLE_LIQUIDS, false);
+	Game_AllowServerTextures = !Game_ClassicMode && Options_GetBool(OPT_SERVER_TEXTURES,    true);
+
+	Game_ViewDistance     = Options_GetInt(OPT_VIEW_DISTANCE, 8, 4096, DEFAULT_VIEWDIST);
+	Game_UserViewDistance = Game_ViewDistance;
+	/* TODO: Do we need to support option to skip SSL */
+	/*cc_bool skipSsl = Options_GetBool("skip-ssl-check", false);
+	if (skipSsl) {
+		ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
+		Options.Set("skip-ssl-check", false);
+	}*/
+}
+
+#ifdef CC_BUILD_PLUGINS
+static void LoadPlugin(const cc_string* path, void* obj) {
+	void* lib;
+	void* verSym;  /* EXPORT int Plugin_ApiVersion = GAME_API_VER; */
+	void* compSym; /* EXPORT struct IGameComponent Plugin_Component = { (whatever) } */
+	int ver;
+
+	/* ignore accepted.txt, deskop.ini, .pdb files, etc */
+	if (!String_CaselessEnds(path, &DynamicLib_Ext)) return;
+	/* don't try to load 32 bit plugins on 64 bit OS or vice versa */
+	if (sizeof(void*) == 4 && String_ContainsConst(path, "_64.")) return;
+	if (sizeof(void*) == 8 && String_ContainsConst(path, "_32.")) return;
+
+	lib = DynamicLib_Load2(path);
+	if (!lib) { Logger_DynamicLibWarn("loading", path); return; }
+
+	verSym  = DynamicLib_Get2(lib, "Plugin_ApiVersion");
+	if (!verSym)  { Logger_DynamicLibWarn("getting version of", path); return; }
+	compSym = DynamicLib_Get2(lib, "Plugin_Component");
+	if (!compSym) { Logger_DynamicLibWarn("initing", path); return; }
+
+	ver = *((int*)verSym);
+	if (ver < GAME_API_VER) {
+		Chat_Add1("&c%s plugin is outdated! Try getting a more recent version.", path);
+		return;
+	} else if (ver > GAME_API_VER) {
+		Chat_Add1("&cYour game is too outdated to use %s plugin! Try updating it.", path);
+		return;
+	}
+
+	Game_AddComponent((struct IGameComponent*)compSym);
+}
+
+static void LoadPlugins(void) {
+	static const cc_string dir = String_FromConst("plugins");
+	cc_result res;
+
+	Utils_EnsureDirectory("plugins");
+	res = Directory_Enum(&dir, NULL, LoadPlugin);
+	if (res) Logger_SysWarn(res, "enumerating plugins directory");
+}
+#else
+static void LoadPlugins(void) { }
+#endif
+
+static void Game_Free(void* obj);
+static void Game_Load(void) {
+	struct IGameComponent* comp;
+	Game_UpdateDimensions();
+	Game_SetFpsLimit(Options_GetEnum(OPT_FPS_LIMIT, 0, FpsLimit_Names, FPS_LIMIT_COUNT));
+	Gfx_Create();
+	
+	Logger_WarnFunc = Game_WarnFunc;
+	LoadOptions();
+	GameVersion_Load();
+	Utils_EnsureDirectory("maps");
+
+	Event_Register_(&WorldEvents.NewMap,           NULL, HandleOnNewMap);
+	Event_Register_(&WorldEvents.MapLoaded,        NULL, HandleOnNewMapLoaded);
+	Event_Register_(&WindowEvents.Resized,         NULL, Game_OnResize);
+	Event_Register_(&WindowEvents.Closing,         NULL, Game_Free);
+	Event_Register_(&WindowEvents.InactiveChanged, NULL, HandleInactiveChanged);
+
+	Game_AddComponent(&World_Component);
+	Game_AddComponent(&Textures_Component);
+	Game_AddComponent(&Input_Component);
+	Game_AddComponent(&Camera_Component);
+	Game_AddComponent(&Gfx_Component);
+	Game_AddComponent(&Blocks_Component);
+	Game_AddComponent(&Drawer2D_Component);
+	Game_AddComponent(&SystemFonts_Component);
+
+	Game_AddComponent(&Chat_Component);
+	Game_AddComponent(&Commands_Component);
+	Game_AddComponent(&Particles_Component);
+	Game_AddComponent(&TabList_Component);
+	Game_AddComponent(&Models_Component);
+	Game_AddComponent(&Entities_Component);
+	Game_AddComponent(&Http_Component);
+	Game_AddComponent(&Lighting_Component);
+
+	Game_AddComponent(&Animations_Component);
+	Game_AddComponent(&Inventory_Component);
+	Game_AddComponent(&Builder_Component);
+	Game_AddComponent(&MapRenderer_Component);
+	Game_AddComponent(&EnvRenderer_Component);
+	Game_AddComponent(&Server_Component);
+	Game_AddComponent(&Protocol_Component);
+
+	Game_AddComponent(&Gui_Component);
+	Game_AddComponent(&Selections_Component);
+	Game_AddComponent(&HeldBlockRenderer_Component);
+	/* Gfx_SetDepthWrite(true) */
+	Game_AddComponent(&SelOutlineRenderer_Component);
+	Game_AddComponent(&Audio_Component);
+	Game_AddComponent(&AxisLinesRenderer_Component);
+	Game_AddComponent(&Formats_Component);
+	Game_AddComponent(&EntityRenderers_Component);
+
+	LoadPlugins();
+	for (comp = comps_head; comp; comp = comp->next) {
+		if (comp->Init) comp->Init();
+	}
+
+	TexturePack_ExtractCurrent(true);
+	if (TexturePack_DefaultMissing) {
+		Window_ShowDialog("Missing file",
+			"Both default.zip and classicube.zip are missing,\n try downloading resources first.\n\nClassiCube will still run, but without any textures.");
+	}
+
+	entTaskI = ScheduledTask_Add(GAME_DEF_TICKS, Entities_Tick);
+	if (Gfx_WarnIfNecessary()) EnvRenderer_SetMode(EnvRenderer_Minimal | ENV_LEGACY);
+	Server.BeginConnect();
+}
+
+void Game_SetFpsLimit(int method) {
+	float minFrameTime = 0;
+	Game_FpsLimit = method;
+
+	switch (method) {
+	case FPS_LIMIT_144: minFrameTime = 1000/144.0f; break;
+	case FPS_LIMIT_120: minFrameTime = 1000/120.0f; break;
+	case FPS_LIMIT_60:  minFrameTime = 1000/60.0f;  break;
+	case FPS_LIMIT_30:  minFrameTime = 1000/30.0f;  break;
+	}
+	Gfx_SetFpsLimit(method == FPS_LIMIT_VSYNC, minFrameTime);
+}
+
+static void UpdateViewMatrix(void) {
+	Camera.Active->GetView(&Gfx.View);
+	FrustumCulling_CalcFrustumEquations(&Gfx.Projection, &Gfx.View);
+}
+
+static void Render3DFrame(float delta, float t) {
+	Vec3 pos;
+	Gfx_LoadMatrix(MATRIX_PROJECTION, &Gfx.Projection);
+	Gfx_LoadMatrix(MATRIX_VIEW,       &Gfx.View);
+	if (EnvRenderer_ShouldRenderSkybox()) EnvRenderer_RenderSkybox();
+
+	AxisLinesRenderer_Render();
+	Entities_RenderModels(delta, t);
+	EntityNames_Render();
+
+	Particles_Render(t);
+	EnvRenderer_RenderSky();
+	EnvRenderer_RenderClouds();
+
+	MapRenderer_Update(delta);
+	MapRenderer_RenderNormal(delta);
+	EnvRenderer_RenderMapSides();
+
+	EntityShadows_Render();
+	if (Game_SelectedPos.valid && !Game_HideGui) {
+		SelOutlineRenderer_Render(&Game_SelectedPos, true);
+	}
+
+	/* Render water over translucent blocks when under the water outside the map for proper alpha blending */
+	pos = Camera.CurrentPos;
+	if (pos.y < Env.EdgeHeight && (pos.x < 0 || pos.z < 0 || pos.x > World.Width || pos.z > World.Length)) {
+		MapRenderer_RenderTranslucent(delta);
+		EnvRenderer_RenderMapEdges();
+	} else {
+		EnvRenderer_RenderMapEdges();
+		MapRenderer_RenderTranslucent(delta);
+	}
+
+	/* Need to render again over top of translucent block, as the selection outline */
+	/* is drawn without writing to the depth buffer */
+	if (Game_SelectedPos.valid && !Game_HideGui && Blocks.Draw[Game_SelectedPos.block] == DRAW_TRANSLUCENT) {
+		SelOutlineRenderer_Render(&Game_SelectedPos, false);
+	}
+
+	Selections_Render();
+	EntityNames_RenderHovered();
+	if (!Game_HideGui) HeldBlockRenderer_Render(delta);
+}
+
+static void Render3D_Anaglyph(float delta, float t) {
+	struct Matrix proj = Gfx.Projection;
+	struct Matrix view = Gfx.View;
+
+	Gfx_Set3DLeft(&proj, &view);
+	Render3DFrame(delta, t);
+
+	Gfx_Set3DRight(&proj, &view);
+	Render3DFrame(delta, t);
+
+	Gfx_End3D(&proj, &view);
+}
+
+static void PerformScheduledTasks(double time) {
+	struct ScheduledTask* task;
+	int i;
+
+	for (i = 0; i < tasksCount; i++) {
+		task = &tasks[i];
+		task->accumulator += time;
+
+		while (task->accumulator >= task->interval) {
+			task->Callback(task);
+			task->accumulator -= task->interval;
+		}
+	}
+}
+
+void Game_TakeScreenshot(void) {
+	cc_string filename; char fileBuffer[STRING_SIZE];
+	cc_string path;     char pathBuffer[FILENAME_SIZE];
+	struct DateTime now;
+	cc_result res;
+#ifdef CC_BUILD_WEB
+	char str[NATIVE_STR_LEN];
+#else
+	struct Stream stream;
+#endif
+	Game_ScreenshotRequested = false;
+	DateTime_CurrentLocal(&now);
+
+	String_InitArray(filename, fileBuffer);
+	String_Format3(&filename, "screenshot_%p4-%p2-%p2", &now.year, &now.month, &now.day);
+	String_Format3(&filename, "-%p2-%p2-%p2.png", &now.hour, &now.minute, &now.second);
+
+#ifdef CC_BUILD_WEB
+	extern void interop_TakeScreenshot(const char* path);
+	String_EncodeUtf8(str, &filename);
+	interop_TakeScreenshot(str);
+#else
+	if (!Utils_EnsureDirectory("screenshots")) return;
+	String_InitArray(path, pathBuffer);
+	String_Format1(&path, "screenshots/%s", &filename);
+
+	res = Stream_CreateFile(&stream, &path);
+	if (res) { Logger_SysWarn2(res, "creating", &path); return; }
+
+	res = Gfx_TakeScreenshot(&stream);
+	if (res) { 
+		Logger_SysWarn2(res, "saving to", &path); stream.Close(&stream); return;
+	}
+
+	res = stream.Close(&stream);
+	if (res) { Logger_SysWarn2(res, "closing", &path); return; }
+	Chat_Add1("&eTaken screenshot as: %s", &filename);
+
+#ifdef CC_BUILD_MOBILE
+	Platform_ShareScreenshot(&filename);
+#endif
+#endif
+}
+
+static CC_INLINE void Game_DrawFrame(float delta, float t) {
+	UpdateViewMatrix();
+
+	if (!Gui_GetBlocksWorld()) {
+		Camera.Active->GetPickedBlock(&Game_SelectedPos); /* TODO: only pick when necessary */
+		Camera_KeyLookUpdate(delta);
+		InputHandler_Tick();
+
+		if (Game_Anaglyph3D) {
+			Render3D_Anaglyph(delta, t);
+		} else {
+			Render3DFrame(delta, t);
+		}
+	} else {
+		RayTracer_SetInvalid(&Game_SelectedPos);
+	}
+
+	Gfx_Begin2D(Game.Width, Game.Height);
+	Gui_RenderGui(delta);
+	OnscreenKeyboard_Draw3D();
+/* TODO find a better solution than this */
+#ifdef CC_BUILD_3DS
+	if (Game_Anaglyph3D) {
+		extern void Gfx_SetTopRight(void);
+		Gfx_SetTopRight();
+		Gui_RenderGui(delta);
+	}
+#endif
+	Gfx_End2D();
+}
+
+#ifdef CC_BUILD_SPLITSCREEN
+static void DrawSplitscreen(float delta, float t, int i, int x, int y, int w, int h) {
+	Gfx_SetViewport(x, y, w, h);
+	
+	Entities.CurPlayer = &LocalPlayer_Instances[i];
+	LocalPlayer_SetInterpPosition(Entities.CurPlayer, t);
+	Camera.CurrentPos = Camera.Active->GetPosition(t);
+	
+	Game_DrawFrame(delta, t);
+}
+#endif
+
+static CC_INLINE void Game_RenderFrame(double delta) {
+	struct ScheduledTask entTask;
+	float t;
+
+	/* TODO: Should other tasks get called back too? */
+	/* Might not be such a good idea for the http_clearcache, */
+	/* don't really want all skins getting lost */
+	if (Gfx.LostContext) {
+		if (Gfx_TryRestoreContext()) {
+			Gfx_RecreateContext();
+			/* all good, context is back */
+		} else {
+			Game.Time += delta; /* TODO: Not set in two places? */
+			Server.Tick(NULL);
+			Thread_Sleep(16);
+			return;
+		}
+	}
+
+	Gfx_BeginFrame();
+	Gfx_BindIb(Gfx_defaultIb);
+	Game.Time += delta;
+	Game_Vertices = 0;
+
+	if (Input.Sources & INPUT_SOURCE_GAMEPAD) Gamepad_Tick(delta);
+	Camera.Active->UpdateMouse(Entities.CurPlayer, delta);
+
+	if (!Window_Main.Focused && !Gui.InputGrab) Gui_ShowPauseMenu();
+
+	if (InputBind_IsPressed(BIND_ZOOM_SCROLL) && !Gui.InputGrab) {
+		InputHandler_SetFOV(Camera.ZoomFov);
+	}
+
+	PerformScheduledTasks(delta);
+	entTask = tasks[entTaskI];
+	t = (float)(entTask.accumulator / entTask.interval);
+	LocalPlayer_SetInterpPosition(Entities.CurPlayer, t);
+
+	Camera.CurrentPos = Camera.Active->GetPosition(t);
+	/* NOTE: EnvRenderer_UpdateFog also also sets clear color */
+	EnvRenderer_UpdateFog();
+	AudioBackend_Tick();
+
+	/* TODO: Not calling Gfx_EndFrame doesn't work with Direct3D9 */
+	if (Window_Main.Inactive) return;
+	Gfx_ClearBuffers(GFX_BUFFER_COLOR | GFX_BUFFER_DEPTH);
+	
+#ifdef CC_BUILD_SPLITSCREEN
+	switch (Game_NumLocalPlayers) {
+		case 1:
+			Game_DrawFrame(delta, t); break;
+		case 2:
+			DrawSplitscreen(delta, t, 0,  0,               0, Game.Width, Game.Height / 2);
+			DrawSplitscreen(delta, t, 1,  0, Game.Height / 2, Game.Width, Game.Height / 2);
+			break;
+		case 3:
+			DrawSplitscreen(delta, t, 0,              0,               0, Game.Width    , Game.Height / 2);
+			DrawSplitscreen(delta, t, 1,              0, Game.Height / 2, Game.Width / 2, Game.Height / 2);
+			DrawSplitscreen(delta, t, 2, Game.Width / 2, Game.Height / 2, Game.Width / 2, Game.Height / 2);
+			break;
+		case 4:
+			DrawSplitscreen(delta, t, 0,              0,               0, Game.Width / 2, Game.Height / 2);
+			DrawSplitscreen(delta, t, 1, Game.Width / 2,               0, Game.Width / 2, Game.Height / 2);
+			DrawSplitscreen(delta, t, 2,              0, Game.Height / 2, Game.Width / 2, Game.Height / 2);
+			DrawSplitscreen(delta, t, 3, Game.Width / 2, Game.Height / 2, Game.Width / 2, Game.Height / 2);
+			break;
+	}
+#else
+	Game_DrawFrame(delta, t);
+#endif
+
+	if (Game_ScreenshotRequested) Game_TakeScreenshot();
+	Gfx_EndFrame();
+}
+
+static void Game_Free(void* obj) {
+	struct IGameComponent* comp;
+	/* Most components will call OnContextLost in their Free functions */
+	/* Set to false so components will always free managed textures too */
+	Gfx.ManagedTextures = false;
+	Event_UnregisterAll();
+	tasksCount = 0;
+
+	for (comp = comps_head; comp; comp = comp->next) {
+		if (comp->Free) comp->Free();
+	}
+
+	gameRunning     = false;
+	Logger_WarnFunc = Logger_DialogWarn;
+	Gfx_Free();
+	Options_SaveIfChanged();
+	Window_DisableRawMouse();
+}
+
+#define Game_DoFrameBody() \
+	render = Stopwatch_Measure();\
+	delta  = Stopwatch_ElapsedMicroseconds(Game_FrameStart, render) / (1000.0 * 1000.0);\
+	\
+	Window_ProcessEvents(delta);\
+	if (!gameRunning) return;\
+	\
+	if (delta > 5.0) delta = 5.0; /* avoid large delta with suspended process */ \
+	if (delta > 0.0) { Game_FrameStart = render; Game_RenderFrame(delta); }
+
+#ifdef CC_BUILD_WEB
+void Game_DoFrame(void) {
+	cc_uint64 render; 
+	double delta;
+	Game_DoFrameBody()
+}
+
+static void Game_RunLoop(void) {
+	Game_FrameStart = Stopwatch_Measure();
+	/* Window_Web.c sets Game_DoFrame as the main loop callback function */
+	/* (i.e. web browser is in charge of calling Game_DoFrame, not us) */
+}
+
+cc_bool Game_ShouldClose(void) {
+	if (Server.IsSinglePlayer) {
+		/* Close if map was saved within last 5 seconds */
+		return World.LastSave + 5 >= Game.Time;
+	}
+
+	/* Try to intercept Ctrl+W or Cmd+W for multiplayer */
+	if (Input_IsCtrlPressed() || Input_IsWinPressed()) return false;
+	/* Also try to intercept mouse back button (Mouse4) */
+	return !Input.Pressed[CCMOUSE_X1];
+}
+#else
+static void Game_RunLoop(void) {
+	cc_uint64 render;
+	double delta;
+
+	Game_FrameStart = Stopwatch_Measure();
+	for (;;) { Game_DoFrameBody() }
+}
+#endif
+
+void Game_Run(int width, int height, const cc_string* title) {
+	Window_Create3D(width, height);
+	Window_SetTitle(title);
+	Window_Show();
+	gameRunning = true;
+
+	Game_Load();
+	Event_RaiseVoid(&WindowEvents.Resized);
+	Game_RunLoop();
+}