// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2025 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file  d_main.c
/// \brief SRB2 main program
///
///        SRB2 main program (D_SRB2Main) and game loop (D_SRB2Loop),
///        plus functions to parse command line parameters, configure game
///        parameters, and call the startup functions.

#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
#include <sys/stat.h>
#include <sys/types.h>
#endif

#ifdef __GNUC__
#include <unistd.h> // for getcwd
#endif

#ifdef _WIN32
#include <direct.h>
#include <malloc.h>
#endif

#include <time.h>

#include "doomdef.h"
#include "am_map.h"
#include "console.h"
#include "netcode/d_net.h"
#include "f_finale.h"
#include "g_game.h"
#include "hu_stuff.h"
#include "i_sound.h"
#include "i_system.h"
#include "i_time.h"
#include "i_threads.h"
#include "i_video.h"
#include "m_argv.h"
#include "m_menu.h"
#include "m_misc.h"
#include "p_setup.h"
#include "p_saveg.h"
#include "r_main.h"
#include "r_local.h"
#include "r_translation.h"
#include "s_sound.h"
#include "st_stuff.h"
#include "v_video.h"
#include "w_wad.h"
#include "z_zone.h"
#include "d_main.h"
#include "netcode/d_netfil.h"
#include "m_cheat.h"
#include "y_inter.h"
#include "p_local.h" // chasecam
#include "netcode/mserv.h" // ms_RoomId
#include "m_misc.h" // screenshot functionality
#include "deh_tables.h" // Dehacked list test
#include "m_cond.h" // condition initialization
#include "fastcmp.h"
#include "r_fps.h" // Frame interpolation/uncapped
#include "keys.h"
#include "filesrch.h" // refreshdirmenu
#include "g_input.h" // tutorial mode control scheming
#include "m_perfstats.h"
#include "m_random.h"
#include "command.h"

#ifdef CMAKECONFIG
#include "config.h"
#else
#include "config.h.in"
#endif

#ifdef HWRENDER
#include "hardware/hw_main.h" // 3D View Rendering
#endif

#ifdef _WINDOWS
#include "win32/win_main.h" // I_DoStartupMouse
#endif

#ifdef HW3SOUND
#include "hardware/hw3sound.h"
#endif

#include "lua_script.h"

// Version numbers for netplay :upside_down_face:
int    VERSION;
int SUBVERSION;

// platform independant focus loss
UINT8 window_notinfocus = false;

static addfilelist_t startupwadfiles;
static addfilelist_t startuppwads;

boolean devparm = false; // started game with -devparm

boolean singletics = false; // timedemo
boolean lastdraw = false;

postimg_t postimgtype = postimg_none;
INT32 postimgparam;
postimg_t postimgtype2 = postimg_none;
INT32 postimgparam2;

// These variables are in effect
// whether the respective sound system is disabled
// or they're init'ed, but the player just toggled them
boolean midi_disabled = false;
boolean sound_disabled = false;
boolean digital_disabled = false;

//
// DEMO LOOP
//
boolean advancedemo;
#ifdef DEBUGFILE
INT32 debugload = 0;
#endif

UINT16 numskincolors;
menucolor_t *menucolorhead, *menucolortail;

char savegamename[256];
char liveeventbackup[256];

char srb2home[256] = ".";
char srb2path[256] = ".";
boolean usehome = true;
const char *pandf = "%s" PATHSEP "%s";
static char addonsdir[MAX_WADPATH];

//
// EVENT HANDLING
//
// Events are asynchronous inputs generally generated by the game user.
// Events can be discarded if no responder claims them
// referenced from i_system.c for I_GetKey()

event_t events[MAXEVENTS];
INT32 eventhead, eventtail;

boolean dedicated = false;

//
// D_PostEvent
// Called by the I/O functions when input is detected
//
void D_PostEvent(const event_t *ev)
{
	events[eventhead] = *ev;
	eventhead = (eventhead+1) & (MAXEVENTS-1);
}

// modifier keys
// Now handled in I_OsPolling
UINT8 shiftdown = 0; // 0x1 left, 0x2 right
UINT8 ctrldown = 0; // 0x1 left, 0x2 right
UINT8 altdown = 0; // 0x1 left, 0x2 right
boolean capslock = 0;	// gee i wonder what this does.

//
// D_ProcessEvents
// Send all the events of the given timestamp down the responder chain
//
void D_ProcessEvents(void)
{
	event_t *ev;

	boolean eaten;

	// Reset possibly stale mouse info
	G_SetMouseDeltas(0, 0, 1);
	G_SetMouseDeltas(0, 0, 2);
	mouse.buttons &= ~(MB_SCROLLUP|MB_SCROLLDOWN);
	mouse2.buttons &= ~(MB_SCROLLUP|MB_SCROLLDOWN);

	for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
	{
		boolean hooked = false;

		ev = &events[eventtail];

		// Set mouse buttons early in case event is eaten later
		if (ev->type == ev_keydown || ev->type == ev_keyup || ev->type == ev_text)
		{
			// Mouse buttons
			if ((UINT32)(ev->key - KEY_MOUSE1) < MOUSEBUTTONS)
			{
				if (ev->type == ev_keydown || ev->type == ev_text)
					mouse.buttons |= 1 << (ev->key - KEY_MOUSE1);
				else
					mouse.buttons &= ~(1 << (ev->key - KEY_MOUSE1));
			}
			else if ((UINT32)(ev->key - KEY_2MOUSE1) < MOUSEBUTTONS)
			{
				if (ev->type == ev_keydown || ev->type == ev_text)
					mouse2.buttons |= 1 << (ev->key - KEY_2MOUSE1);
				else
					mouse2.buttons &= ~(1 << (ev->key - KEY_2MOUSE1));
			}
			// Scroll (has no keyup event)
			else switch (ev->key) {
				case KEY_MOUSEWHEELUP:
					mouse.buttons |= MB_SCROLLUP;
					break;
				case KEY_MOUSEWHEELDOWN:
					mouse.buttons |= MB_SCROLLDOWN;
					break;
				case KEY_2MOUSEWHEELUP:
					mouse2.buttons |= MB_SCROLLUP;
					break;
				case KEY_2MOUSEWHEELDOWN:
					mouse2.buttons |= MB_SCROLLDOWN;
					break;
			}
		}

		// Screenshots over everything so that they can be taken anywhere.
		if (M_ScreenshotResponder(ev))
			continue; // ate the event

		if (gameaction == ga_nothing && gamestate == GS_TITLESCREEN)
		{
			if (cht_Responder(ev))
				continue;
		}

		if (!CON_Ready() && !menuactive) {
			if (G_LuaResponder(ev))
				continue;
			hooked = true;
		}

		// Menu input
#ifdef HAVE_THREADS
		I_lock_mutex(&m_menu_mutex);
#endif
		{
			eaten = M_Responder(ev);
		}
#ifdef HAVE_THREADS
		I_unlock_mutex(m_menu_mutex);
#endif

		if (eaten)
			continue; // menu ate the event

		if (!hooked && !CON_Ready()) {
			if (G_LuaResponder(ev))
				continue;
			hooked = true;
		}

		// console input
#ifdef HAVE_THREADS
		I_lock_mutex(&con_mutex);
#endif
		{
			eaten = CON_Responder(ev);
		}
#ifdef HAVE_THREADS
		I_unlock_mutex(con_mutex);
#endif

		if (eaten)
			continue; // ate the event

		if (!hooked && !CON_Ready() && G_LuaResponder(ev))
			continue;

		G_Responder(ev);
	}

	if (mouse.rdx || mouse.rdy)
		G_SetMouseDeltas(mouse.rdx, mouse.rdy, 1);
	if (mouse2.rdx || mouse2.rdy)
		G_SetMouseDeltas(mouse2.rdx, mouse2.rdy, 2);
}

//
// D_Display
// draw current display, possibly wiping it from the previous
//

// wipegamestate can be set to -1 to force a wipe on the next draw
// added comment : there is a wipe eatch change of the gamestate
gamestate_t wipegamestate = GS_LEVEL;
// -1: Default; 0-n: Wipe index; INT16_MAX: do not wipe
INT16 wipetypepre = -1;
INT16 wipetypepost = -1;

static void D_Display(void)
{
	boolean forcerefresh = false;
	static boolean wipe = false;
	INT32 wipedefindex = 0;

	if (dedicated)
		return;

	if (nodrawers)
		return; // for comparative timing/profiling

	// Lactozilla: Switching renderers works by checking
	// if the game has to do it right when the frame
	// needs to render. If so, five things will happen:
	// 1. Interface functions will be called so
	//    that switching to OpenGL creates a
	//    GL context, and switching to Software
	//    allocates screen buffers.
	// 2. Software will set drawer functions,
	//    and OpenGL will load textures and
	//    create plane polygons, if necessary.
	// 3. Functions related to switching video
	//    modes (resolution) are called.
	// 4. The frame is ready to be drawn!

	// Check for change of renderer or screen size (video mode)
	if ((setrenderneeded || setmodeneeded) && !wipe)
		SCR_SetMode(); // change video mode

	// Recalc the screen
	if (vid.recalc)
		SCR_Recalc(); // NOTE! setsizeneeded is set by SCR_Recalc()

	// View morph
	if (rendermode == render_soft && !splitscreen)
		R_CheckViewMorph();

	// Change the view size if needed
	// Set by changing video mode or renderer
	if (setsizeneeded)
	{
		R_ExecuteSetViewSize();
		forcerefresh = true; // force background redraw
	}

	// draw buffered stuff to screen
	// Used only by linux GGI version
	I_UpdateNoBlit();

	// save the current screen if about to wipe
	wipe = (gamestate != wipegamestate);
	if (wipe && wipetypepre != INT16_MAX)
	{
		// set for all later
		wipedefindex = gamestate; // wipe_xxx_toblack
		if (gamestate == GS_INTERMISSION)
		{
			if (intertype == int_spec) // Special Stage
				wipedefindex = wipe_specinter_toblack;
			else if (intertype != int_coop) // Multiplayer
				wipedefindex = wipe_multinter_toblack;
		}

		if (wipetypepre < 0 || !F_WipeExists(wipetypepre))
			wipetypepre = wipedefs[wipedefindex];

		if (rendermode != render_none)
		{
			// Fade to black first
			if ((wipegamestate == (gamestate_t)FORCEWIPE ||
			        (wipegamestate != (gamestate_t)FORCEWIPEOFF
						&& !(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
					) // fades to black on its own timing, always
			 && wipetypepre != UINT8_MAX)
			{
				F_WipeStartScreen();
				// Check for Mega Genesis fade
				wipestyleflags = WSF_FADEOUT;
				if (wipegamestate == (gamestate_t)FORCEWIPE)
					F_WipeColorFill(31);
				else if (F_TryColormapFade(31))
					wipetypepost = -1; // Don't run the fade below this one
				F_WipeEndScreen();
				F_RunWipe(wipetypepre, gamestate != GS_TIMEATTACK && gamestate != GS_TITLESCREEN);
			}

			F_WipeStartScreen();
		}

		wipetypepre = -1;
	}
	else
		wipetypepre = -1;

	// do buffered drawing
	switch (gamestate)
	{
		case GS_TITLESCREEN:
			if (!titlemapinaction || !curbghide) {
				F_TitleScreenDrawer();
				break;
			}
			/* FALLTHRU */
		case GS_LEVEL:
			if (!gametic)
				break;
			AM_Drawer();
			break;

		case GS_INTERMISSION:
			Y_IntermissionDrawer();
			HU_Drawer();
			break;

		case GS_TIMEATTACK:
			break;

		case GS_INTRO:
			F_IntroDrawer();
			if (wipegamestate == (gamestate_t)-1)
				wipe = true;
			break;

		case GS_ENDING:
			F_EndingDrawer();
			HU_Drawer();
			break;

		case GS_CUTSCENE:
			F_CutsceneDrawer();
			HU_Drawer();
			break;

		case GS_GAMEEND:
			F_GameEndDrawer();
			break;

		case GS_EVALUATION:
			F_GameEvaluationDrawer();
			HU_Drawer();
			break;

		case GS_CONTINUING:
			F_ContinueDrawer();
			break;

		case GS_CREDITS:
			F_CreditDrawer();
			HU_Drawer();
			break;

		case GS_WAITINGPLAYERS:
			// The clientconnect drawer is independent...
			if (netgame)
			{
				// I don't think HOM from nothing drawing is independent...
				F_WaitingPlayersDrawer();
				HU_Drawer();
			}
		case GS_DEDICATEDSERVER:
		case GS_NULL:
			break;
	}

	// STUPID race condition...
	if (wipegamestate == GS_INTRO && gamestate == GS_TITLESCREEN)
		wipegamestate = FORCEWIPEOFF;
	else
	{
		wipegamestate = gamestate;

		if (gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction && curbghide && (!hidetitlemap)))
		{
			// draw the view directly
			if (cv_debug)
			{
				r_renderwalls = cv_renderwalls.value;
				r_renderfloors = cv_renderfloors.value;
				r_renderthings = cv_renderthings.value;
			}
			else
			{
				r_renderwalls = true;
				r_renderfloors = true;
				r_renderthings = true;
			}

			if (!automapactive && !dedicated && cv_renderview.value)
			{
				R_ApplyLevelInterpolators(R_UsingFrameInterpolation() ? rendertimefrac : FRACUNIT);
				PS_START_TIMING(ps_rendercalltime);
				if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
				{
					topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
					objectsdrawn = 0;
	#ifdef HWRENDER
					if (rendermode != render_soft)
						HWR_RenderPlayerView(0, &players[displayplayer]);
					else
	#endif
					if (rendermode != render_none)
						R_RenderPlayerView(&players[displayplayer]);
				}

				// render the second screen
				if (splitscreen && players[secondarydisplayplayer].mo)
				{
					viewwindowy = vid.height / 2;

#ifdef HWRENDER
					if (rendermode == render_opengl)
						HWR_RenderPlayerView(1, &players[secondarydisplayplayer]);
					else
#endif
					if (rendermode != render_none)
					{
						topleft = screens[0] + viewwindowy*vid.width + viewwindowx;

						R_RenderPlayerView(&players[secondarydisplayplayer]);
					}

					viewwindowy = 0;
				}

				// Image postprocessing effect
				if (rendermode == render_soft)
				{
					if (!splitscreen)
						R_ApplyViewMorph();

					if (postimgtype)
						V_DoPostProcessor(0, postimgtype, postimgparam);
					if (postimgtype2)
						V_DoPostProcessor(1, postimgtype2, postimgparam2);
				}
				PS_STOP_TIMING(ps_rendercalltime);
				R_RestoreLevelInterpolators();
			}

			if (lastdraw)
			{
				if (rendermode == render_soft)
				{
					VID_BlitLinearScreen(screens[0], screens[1], vid.width*vid.bpp, vid.height, vid.width*vid.bpp, vid.rowbytes);
					Y_ConsiderScreenBuffer();
					usebuffer = true;
				}
				lastdraw = false;
			}

			PS_START_TIMING(ps_uitime);

			if (gamestate == GS_LEVEL)
			{
				ST_Drawer();
				F_TextPromptDrawer();
				HU_Drawer();
			}
			else
				F_TitleScreenDrawer();
		}
		else
		{
			PS_START_TIMING(ps_uitime);
		}
	}

	// change gamma if needed
	// (GS_LEVEL handles this already due to level-specific palettes)
	if (forcerefresh && !(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
		V_SetPalette(0);

	// draw pause pic
	if (paused && cv_showhud.value && (!menuactive || netgame))
	{
#if 0
		INT32 py;
		patch_t *patch;
		if (automapactive)
			py = 4;
		else
			py = viewwindowy + 4;
		patch = W_CachePatchName("M_PAUSE", PU_PATCH);
		V_DrawScaledPatch(viewwindowx + (BASEVIDWIDTH - patch->width)/2, py, 0, patch);
#else
		INT32 y = ((automapactive) ? (32) : (BASEVIDHEIGHT/2));
		M_DrawTextBox((BASEVIDWIDTH/2) - (60), y - (16), 13, 2);
		V_DrawCenteredString(BASEVIDWIDTH/2, y - (4), V_YELLOWMAP, "Game Paused");
#endif
	}

	// vid size change is now finished if it was on...
	vid.recalc = 0;

#ifdef HAVE_THREADS
	I_lock_mutex(&m_menu_mutex);
#endif
	M_Drawer(); // menu is drawn even on top of everything
#ifdef HAVE_THREADS
	I_unlock_mutex(m_menu_mutex);
#endif
	// focus lost moved to M_Drawer

	CON_Drawer();

	PS_STOP_TIMING(ps_uitime);

	//
	// wipe update
	//
	if (wipe && wipetypepost != INT16_MAX)
	{
		// note: moved up here because NetUpdate does input changes
		// and input during wipe tends to mess things up
		wipedefindex += WIPEFINALSHIFT;

		if (wipetypepost < 0 || !F_WipeExists(wipetypepost))
			wipetypepost = wipedefs[wipedefindex];

		if (rendermode != render_none)
		{
			F_WipeEndScreen();

			// Funny.
			if (WipeStageTitle && st_overlay)
			{
				lt_ticker--;
				lt_lasttic = lt_ticker;
				ST_preLevelTitleCardDrawer();
				V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, levelfadecol);
				F_WipeStartScreen();
			}

			// Check for Mega Genesis fade
			if (F_ShouldColormapFade())
			{
				wipestyleflags |= WSF_FADEIN;
				wipestyleflags &= ~WSF_FADEOUT;
			}

			F_RunWipe(wipetypepost, gamestate != GS_TIMEATTACK && gamestate != GS_TITLESCREEN);
		}

		// reset counters so timedemo doesn't count the wipe duration
		if (timingdemo)
		{
			framecount = 0;
			demostarttime = I_GetTime();
		}

		wipetypepost = -1;
	}
	else
		wipetypepost = -1;

	NetUpdate(); // send out any new accumulation

	// It's safe to end the game now.
	if (G_GetExitGameFlag())
	{
		Command_ExitGame_f();
		G_ClearExitGameFlag();
	}

	//
	// normal update
	//
	if (!wipe)
	{
		if (cv_netstat.value)
		{
			char s[50];
			Net_GetNetStat();

			s[sizeof s - 1] = '\0';

			snprintf(s, sizeof s - 1, "get %d b/s", getbps);
			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-40, V_YELLOWMAP, s);
			snprintf(s, sizeof s - 1, "send %d b/s", sendbps);
			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-30, V_YELLOWMAP, s);
			snprintf(s, sizeof s - 1, "GameMiss %.2f%%", gamelostpercent);
			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-20, V_YELLOWMAP, s);
			snprintf(s, sizeof s - 1, "SysMiss %.2f%%", lostpercent);
			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-10, V_YELLOWMAP, s);
		}

		if (cv_perfstats.value)
		{
			M_DrawPerfStats();
		}

		PS_START_TIMING(ps_swaptime);
		I_FinishUpdate(); // page flip or blit buffer
		PS_STOP_TIMING(ps_swaptime);
	}
}

// =========================================================================
// D_SRB2Loop
// =========================================================================

tic_t rendergametic;

void D_SRB2Loop(void)
{
	tic_t entertic = 0, oldentertics = 0, realtics = 0, rendertimeout = INFTICS;
	double deltatics = 0.0;
	double deltasecs = 0.0;
	static lumpnum_t gstartuplumpnum;

	boolean interp = false;
	boolean doDisplay = false;

	if (dedicated)
		server = true;

	// Pushing of + parameters is now done back in D_SRB2Main, not here.

#ifdef _WINDOWS
	CONS_Printf("I_StartupMouse()...\n");
	I_DoStartupMouse();
#endif

	I_UpdateTime(cv_timescale.value);
	oldentertics = I_GetTime();

	// end of loading screen: CONS_Printf() will no more call FinishUpdate()
	con_refresh = false;
	con_startup = false;

	// make sure to do a d_display to init mode _before_ load a level
	SCR_SetMode(); // change video mode
	SCR_Recalc();

	chosenrendermode = render_none;

	// Check and print which version is executed.
	// Use this as the border between setup and the main game loop being entered.
	CONS_Printf(
	"===========================================================================\n"
	"                   We hope you enjoy this game as\n"
	"                     much as we did making it!\n"
	"                            ...wait. =P\n"
	"===========================================================================\n");

	// hack to start on a nice clear console screen.
	COM_ImmedExecute("cls;version");

	I_FinishUpdate(); // page flip or blit buffer
	/*
	LMFAO this was showing garbage under OpenGL
	because I_FinishUpdate was called afterward
	*/
	/* Smells like a hack... Don't fade Sonic's ass into the title screen. */
	if (gamestate != GS_TITLESCREEN)
	{
		gstartuplumpnum = W_CheckNumForPatchName("STARTUP");
		if (gstartuplumpnum == LUMPERROR)
			gstartuplumpnum = W_GetNumForPatchName("MISSING");
		V_DrawScaledPatch(0, 0, 0, W_CachePatchNum(gstartuplumpnum, PU_PATCH));
	}

	for (;;)
	{
		// capbudget is the minimum precise_t duration of a single loop iteration
		precise_t capbudget;
		precise_t enterprecise = I_GetPreciseTime();
		precise_t finishprecise = enterprecise;

		{
			// Casting the return value of a function is bad practice (apparently)
			double budget = round((1.0 / R_GetFramerateCap()) * I_GetPrecisePrecision());
			capbudget = (precise_t) budget;
		}

		I_UpdateTime(cv_timescale.value);

		if (lastwipetic)
		{
			oldentertics = lastwipetic;
			lastwipetic = 0;
		}

		// get real tics
		entertic = I_GetTime();
		realtics = entertic - oldentertics;
		oldentertics = entertic;

		if (demoplayback && gamestate == GS_LEVEL)
		{
			// Nicer place to put this.
			realtics = realtics * cv_playbackspeed.value;
		}

#ifdef DEBUGFILE
		if (!realtics)
			if (debugload)
				debugload--;
#endif

		interp = R_UsingFrameInterpolation() && !dedicated;
		doDisplay = false;

#ifdef HW3SOUND
		HW3S_BeginFrameUpdate();
#endif

		refreshdirmenu = 0; // not sure where to put this, here as good as any?

		if (realtics > 0 || singletics)
		{
			// don't skip more than 10 frames at a time
			// (fadein / fadeout cause massive frame skip!)
			if (realtics > 8)
				realtics = 1;

			// process tics (but maybe not if realtic == 0)
			TryRunTics(realtics);

			if (lastdraw || singletics || gametic > rendergametic)
			{
				rendergametic = gametic;
				rendertimeout = entertic + TICRATE/17;

				doDisplay = true;
			}
			else if (rendertimeout < entertic) // in case the server hang or netsplit
			{
				// Lagless camera! Yay!
				if (gamestate == GS_LEVEL && netgame)
				{
					// Evaluate the chase cam once for every local realtic
					// This might actually be better suited inside G_Ticker or TryRunTics
					for (tic_t chasecamtics = 0; chasecamtics < realtics; chasecamtics++)
					{
						if (splitscreen && camera2.chase)
							P_MoveChaseCamera(&players[secondarydisplayplayer], &camera2, false);
						if (camera.chase)
							P_MoveChaseCamera(&players[displayplayer], &camera, false);
					}
					R_UpdateViewInterpolation();
				}

				doDisplay = true;
			}

			renderisnewtic = true;
		}
		else
		{
			renderisnewtic = false;
		}

		if (interp)
		{
			// I looked at the possibility of putting in a float drawer for
			// perfstats and it's very complicated, so we'll just do this instead...
			ps_interp_frac.value.p = (precise_t)((FIXED_TO_FLOAT(g_time.timefrac)) * 1000.0f);
			ps_interp_lag.value.p = (precise_t)((deltasecs) * 1000.0f);

			renderdeltatics = FLOAT_TO_FIXED(deltatics);

			if (!(paused || P_AutoPause()) && deltatics < 1.0 && !hu_stopped)
			{
				rendertimefrac = g_time.timefrac;
			}
			else
			{
				rendertimefrac = FRACUNIT;
			}
		}
		else
		{
			renderdeltatics = realtics * FRACUNIT;
			rendertimefrac = FRACUNIT;
		}

		if (interp || doDisplay)
		{
			D_Display();
		}

		// Only take screenshots after drawing.
		if (moviemode)
			M_SaveFrame();
		if (takescreenshot)
			M_DoScreenShot();

		// consoleplayer -> displayplayers (hear sounds from viewpoint)
		S_UpdateSounds(); // move positional sounds
		if (realtics > 0 || singletics)
			S_UpdateClosedCaptions();

#ifdef HW3SOUND
		HW3S_EndFrameUpdate();
#endif

		LUA_Step();

		// Fully completed frame made.
		finishprecise = I_GetPreciseTime();
		if (!singletics)
		{
			INT64 elapsed = (INT64)(finishprecise - enterprecise);

			// in the case of "match refresh rate" + vsync, don't sleep at all
			const boolean vsync_with_match_refresh = cv_vidwait.value && cv_fpscap.value == 0;

			if (elapsed > 0 && (INT64)capbudget > elapsed && !vsync_with_match_refresh)
			{
				I_SleepDuration(capbudget - (finishprecise - enterprecise));
			}
		}
		// Capture the time once more to get the real delta time.
		finishprecise = I_GetPreciseTime();
		deltasecs = (double)((INT64)(finishprecise - enterprecise)) / I_GetPrecisePrecision();
		deltatics = deltasecs * NEWTICRATE;
	}
}

//
// D_AdvanceDemo
// Called after each demo or intro demosequence finishes
//
void D_AdvanceDemo(void)
{
	advancedemo = true;
}

// =========================================================================
// D_SRB2Main
// =========================================================================

//
// D_StartTitle
//
void D_StartTitle(void)
{
	INT32 i;

	S_StopMusic();

	if (netgame)
	{
		if (gametyperules & GTR_CAMPAIGN)
		{
			G_SetGamestate(GS_WAITINGPLAYERS); // hack to prevent a command repeat

			if (server)
			{
				char mapname[6];

				strlcpy(mapname, G_BuildMapName(spstage_start), sizeof (mapname));
				strlwr(mapname);
				mapname[5] = '\0';

				COM_BufAddText(va("map %s\n", mapname));
			}
		}

		return;
	}

	// okay, stop now
	// (otherwise the game still thinks we're playing!)
	SV_StopServer();
	SV_ResetServer();

	for (i = 0; i < MAXPLAYERS; i++)
		CL_ClearPlayer(i);

	players[consoleplayer].availabilities = players[1].availabilities = R_GetSkinAvailabilities(); // players[1] is supposed to be for 2p

	splitscreen = false;
	SplitScreen_OnChange();
	botingame = false;
	botskin = 0;
	cv_debug = 0;
	emeralds = 0;
	memset(&luabanks, 0, sizeof(luabanks));
	lastmaploaded = 0;
	pickedchar = R_SkinAvailable(cv_skin.string);

	// In case someone exits out at the same time they start a time attack run,
	// reset modeattacking
	modeattacking = ATTACKING_NONE;
	marathonmode = 0;

	// empty maptol so mario/etc sounds don't play in sound test when they shouldn't
	maptol = 0;

	gameaction = ga_nothing;
	displayplayer = consoleplayer = 0;
	G_SetGametype(GT_COOP);
	paused = false;
	advancedemo = false;
	F_InitMenuPresValues();
	F_StartTitleScreen();

	currentMenu = &MainDef; // reset the current menu ID

	// Reset the palette
	if (rendermode != render_none)
		V_SetPaletteLump("PLAYPAL");

	// The title screen is obviously not a tutorial! (Unless I'm mistaken)
	if (tutorialmode && tutorialgcs)
	{
		G_CopyControls(gamecontrol, gamecontroldefault[gcs_custom], gcl_tutorial_full, num_gcl_tutorial_full); // using gcs_custom as temp storage
		CV_SetValue(&cv_usemouse, tutorialusemouse);
		CV_SetValue(&cv_alwaysfreelook, tutorialfreelook);
		CV_SetValue(&cv_mousemove, tutorialmousemove);
		CV_SetValue(&cv_analog[0], tutorialanalog);
		M_StartMessage("Do you want to \x82save the recommended \x82movement controls?\x80\n\nPress 'Y' or 'Enter' to confirm\nPress 'N' or any key to keep \nyour current controls",
			M_TutorialSaveControlResponse, MM_YESNO);
	}
	tutorialmode = false;
}

#define REALLOC_FILE_LIST \
	if (list->files == NULL) \
	{ \
		list->files = calloc(2, sizeof(list->files)); \
		list->numfiles = 1; \
	} \
	else \
	{ \
		index = list->numfiles; \
		list->files = realloc(list->files, sizeof(list->files) * ((++list->numfiles) + 1)); \
		if (list->files == NULL) \
			I_Error("%s: No more free memory to add file %s", __FUNCTION__, file); \
	}

static void D_AddFile(addfilelist_t *list, const char *file)
{
	char *newfile;
	size_t index = 0;

	REALLOC_FILE_LIST

	newfile = malloc(strlen(file) + 1);
	if (!newfile)
		I_Error("D_AddFile: No more free memory to add file %s", file);

	strcpy(newfile, file);
	list->files[index] = newfile;
}

static void D_AddFolder(addfilelist_t *list, const char *file)
{
	char *newfile;
	size_t index = 0;

	REALLOC_FILE_LIST

	newfile = malloc(strlen(file) + 2); // Path delimiter + NULL terminator
	if (!newfile)
		I_Error("D_AddFolder: No more free memory to add folder %s", file);

	strcpy(newfile, file);
	strcat(newfile, PATHSEP);

	list->files[index] = newfile;
}

#undef REALLOC_FILE_LIST

static inline void D_CleanFile(addfilelist_t *list)
{
	if (list->files)
	{
		size_t pnumwadfiles = 0;

		for (; pnumwadfiles < list->numfiles; pnumwadfiles++)
			free(list->files[pnumwadfiles]);

		free(list->files);
		list->files = NULL;
	}

	list->numfiles = 0;
}

///\brief Checks if a netgame URL is being handled, and changes working directory to the EXE's if so.
///       Done because browsers (at least, Firefox on Windows) launch the game from the browser's directory, which causes problems.
static void ChangeDirForUrlHandler(void)
{
	// URL handlers are opened by web browsers (at least Firefox) from the browser's working directory, not the game's stored directory,
	// so chdir to that directory unless overridden.
	if (M_GetUrlProtocolArg() != NULL && !M_CheckParm("-nochdir"))
	{
		size_t i;

		CONS_Printf("%s connect links load game files from the SRB2 application's stored directory. Switching to ", SERVER_URL_PROTOCOL);
		strlcpy(srb2path, myargv[0], sizeof(srb2path));

		// Get just the directory, minus the EXE name
		for (i = strlen(srb2path)-1; i > 0; i--)
		{
			if (srb2path[i] == '/' || srb2path[i] == '\\')
			{
				srb2path[i] = '\0';
				break;
			}
		}

		CONS_Printf("%s\n", srb2path);

#if defined (_WIN32)
		SetCurrentDirectoryA(srb2path);
#else
		if (chdir(srb2path) == -1)
			I_OutputMsg("Couldn't change working directory\n");
#endif
	}
}

// ==========================================================================
// Identify the SRB2 version, and IWAD file to use.
// ==========================================================================

static void IdentifyVersion(void)
{
	char *srb2wad;
	const char *srb2waddir = NULL;

#if defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)
	// change to the directory where 'srb2.pk3' is found
	srb2waddir = I_LocateWad();
#endif

	// get the current directory (possible problem on NT with "." as current dir)
	if (srb2waddir)
	{
		strlcpy(srb2path,srb2waddir,sizeof (srb2path));
	}
	else
	{
		if (getcwd(srb2path, 256) != NULL)
			srb2waddir = srb2path;
		else
		{
			srb2waddir = ".";
		}
	}

#if defined (macintosh) && !defined (HAVE_SDL)
	// cwd is always "/" when app is dbl-clicked
	if (!stricmp(srb2waddir, "/"))
		srb2waddir = I_GetWadDir();
#endif
	// Commercial.
	srb2wad = malloc(strlen(srb2waddir)+1+8+1);
	if (srb2wad == NULL)
		I_Error("No more free memory to look in %s", srb2waddir);
	else
		sprintf(srb2wad, pandf, srb2waddir, "srb2.pk3");

	// will be overwritten in case of -cdrom or unix/win home
	snprintf(configfile, sizeof configfile, "%s" PATHSEP CONFIGFILENAME, srb2waddir);
	configfile[sizeof configfile - 1] = '\0';

	// Load the IWAD
	if (srb2wad != NULL && FIL_ReadFileOK(srb2wad))
		D_AddFile(&startupwadfiles, srb2wad);
	else
		I_Error("srb2.pk3 not found! Expected in %s, ss file: %s\n", srb2waddir, srb2wad);

	if (srb2wad)
		free(srb2wad);

	// if you change the ordering of this or add/remove a file, be sure to update the md5
	// checking in D_SRB2Main

	// Add the maps
	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "zones.pk3"));

	// Add the characters
	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "characters.pk3"));

#ifdef USE_PATCH_DTA
	// Add our crappy patches to fix our bugs
	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "patch.pk3"));
#endif

#if !defined (HAVE_SDL) || defined (HAVE_MIXER)
	{
#define MUSICTEST(str) \
		{\
			const char *musicpath = va(pandf,srb2waddir,str);\
			int ms = W_VerifyNMUSlumps(musicpath, false); \
			if (ms == 1) \
				D_AddFile(&startupwadfiles, musicpath); \
			else if (ms == 0) \
				I_Error("File "str" has been modified with non-music/sound lumps"); \
		}

		MUSICTEST("music.pk3")
		//MUSICTEST("patch_music.pk3")
	}
#endif
}

static void
D_ConvertVersionNumbers (void)
{
	/* leave at defaults (0) under DEVELOP */
#ifndef DEVELOP
	int major;
	int minor;

	sscanf(SRB2VERSION, "%d.%d.%d", &major, &minor, &SUBVERSION);

	/* this is stupid */
	VERSION = ( major * 100 ) + minor;
#endif
}

static void Command_assert(void)
{
#if !defined(NDEBUG) || defined(PARANOIA)
	CONS_Printf("Yes, assertions are enabled.\n");
#else
	CONS_Printf("No, assertions are NOT enabled.\n");
#endif
}

//
// D_SRB2Main
//
void D_SRB2Main(void)
{
	INT32 p;

	INT32 pstartmap = 1;
	boolean autostart = false;

	/* break the version string into version numbers, for netplay */
	D_ConvertVersionNumbers();

	if (!strcmp(compbranch, ""))
	{
		compbranch = "detached HEAD";
	}

	// Print GPL notice for our console users (Linux)
	CONS_Printf(
	"\n\nSonic Robo Blast 2\n"
	"Copyright (C) 1998-2025 by Sonic Team Junior\n\n"
	"This program comes with ABSOLUTELY NO WARRANTY.\n\n"
	"This is free software, and you are welcome to redistribute it\n"
	"and/or modify it under the terms of the GNU General Public License\n"
	"as published by the Free Software Foundation; either version 2 of\n"
	"the License, or (at your option) any later version.\n"
	"See the 'LICENSE.txt' file for details.\n\n"
	"Sonic the Hedgehog and related characters are trademarks of SEGA.\n"
	"We do not claim ownership of SEGA's intellectual property used\n"
	"in this program.\n\n");

	// keep error messages until the final flush(stderr)
#if !defined(NOTERMIOS)
	if (setvbuf(stderr, NULL, _IOFBF, 1000))
		I_OutputMsg("setvbuf didnt work\n");
#endif

	// initialise locale code
	M_StartupLocale();

	// get parameters from a response file (eg: srb2 @parms.txt)
	M_FindResponseFile();

	// MAINCFG is now taken care of where "OBJCTCFG" is handled
	G_LoadGameSettings();

	// Test Dehacked lists
	DEH_TableCheck();

	// Netgame URL special case: change working dir to EXE folder.
	ChangeDirForUrlHandler();

	// identify the main IWAD file to use
	IdentifyVersion();

#if !defined(NOTERMIOS)
	setbuf(stdout, NULL); // non-buffered output
#endif

#if 0 //defined (_DEBUG)
	devparm = M_CheckParm("-nodebug") == 0;
#else
	devparm = M_CheckParm("-debug") != 0;
#endif

	// for dedicated server
#if !defined (_WINDOWS) && !defined (DEDICATED) //already check in win_main.c
	dedicated = M_CheckParm("-dedicated") != 0;
#endif

	if (devparm)
		CONS_Printf(M_GetText("Development mode ON.\n"));

	// default savegame
	strcpy(savegamename, SAVEGAMENAME"%u.ssg");
	strcpy(liveeventbackup, "live"SAVEGAMENAME".bkp"); // intentionally not ending with .ssg

	{
		const char *userhome = D_Home(); //Alam: path to home

		if (!userhome)
		{
#if (defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)) && !defined (__CYGWIN__)
			I_Error("Please set $HOME to your home directory\n");
#else
			if (dedicated)
				snprintf(configfile, sizeof configfile, "d"CONFIGFILENAME);
			else
				snprintf(configfile, sizeof configfile, CONFIGFILENAME);
#endif
		}
		else
		{
			// use user specific config file
#ifdef DEFAULTDIR
			snprintf(srb2home, sizeof srb2home, "%s" PATHSEP DEFAULTDIR, userhome);
			snprintf(downloaddir, sizeof downloaddir, "%s" PATHSEP "DOWNLOAD", srb2home);
			if (dedicated)
				snprintf(configfile, sizeof configfile, "%s" PATHSEP "d"CONFIGFILENAME, srb2home);
			else
				snprintf(configfile, sizeof configfile, "%s" PATHSEP CONFIGFILENAME, srb2home);

			// can't use sprintf since there is %u in savegamename
			strcatbf(savegamename, srb2home, PATHSEP);
			strcatbf(liveeventbackup, srb2home, PATHSEP);

			snprintf(luafiledir, sizeof luafiledir, "%s" PATHSEP "luafiles", srb2home);
#else // DEFAULTDIR
			snprintf(srb2home, sizeof srb2home, "%s", userhome);
			snprintf(downloaddir, sizeof downloaddir, "%s", userhome);
			if (dedicated)
				snprintf(configfile, sizeof configfile, "%s" PATHSEP "d"CONFIGFILENAME, userhome);
			else
				snprintf(configfile, sizeof configfile, "%s" PATHSEP CONFIGFILENAME, userhome);

			// can't use sprintf since there is %u in savegamename
			strcatbf(savegamename, userhome, PATHSEP);
			strcatbf(liveeventbackup, userhome, PATHSEP);

			snprintf(luafiledir, sizeof luafiledir, "%s" PATHSEP "luafiles", userhome);
#endif // DEFAULTDIR
		}

		configfile[sizeof configfile - 1] = '\0';
	}

	// Create addons dir
	snprintf(addonsdir, sizeof addonsdir, "%s%s%s", srb2home, PATHSEP, "addons");
	I_mkdir(addonsdir, 0755);

	// seed M_Random because it is necessary; seed P_Random for scripts that
	// might want to use random numbers immediately at start
	if (!M_RandomSeedFromOS())
		M_RandomSeed((UINT32)time(NULL)); // less good but serviceable

	P_SetRandSeed(M_RandomizedSeed());

	if (M_CheckParm("-password") && M_IsNextParm())
		D_SetPassword(M_GetNextParm());

	// player setup menu colors must be initialized before
	// any wad file is added, as they may contain colors themselves
	M_InitPlayerSetupColors();

	CONS_Printf("Z_Init(): Init zone memory allocation daemon. \n");
	Z_Init();

	clientGamedata = M_NewGameDataStruct();
	serverGamedata = M_NewGameDataStruct();

	// Do this up here so that WADs loaded through the command line can use ExecCfg
	COM_Init();

	COM_AddCommand("assert", Command_assert, COM_LUA);

	// Add any files specified on the command line with
	// "-file <file>" or "-folder <folder>" to the add-on list
	if (!((M_GetUrlProtocolArg() || M_CheckParm("-connect")) && !M_CheckParm("-server")))
	{
		INT32 addontype = 0;
		INT32 i;

		for (i = 1; i < myargc; i++)
		{
			if (!strcasecmp(myargv[i], "-file"))
				addontype = 1;
			else if (!strcasecmp(myargv[i], "-folder"))
				addontype = 2;
			else if (myargv[i][0] == '-' || myargv[i][0] == '+')
				addontype = 0;
			else if (addontype == 1)
				D_AddFile(&startuppwads, myargv[i]);
			else if (addontype == 2)
				D_AddFolder(&startuppwads, myargv[i]);
		}
	}

	// get map from parms

	if (M_CheckParm("-server") || dedicated)
		netgame = server = true;

	// adapt tables to SRB2's needs, including extra slots for dehacked file support
	P_PatchInfoTables();

	// initiate menu metadata before SOCcing them
	M_InitMenuPresTables();

	// init title screen display params
	if (M_GetUrlProtocolArg() || M_CheckParm("-connect"))
		F_InitMenuPresValues();

	//---------------------------------------------------- READY TIME
	// we need to check for dedicated before initialization of some subsystems

	CONS_Printf("I_InitializeTime()...\n");
	I_InitializeTime();

	// Make backups of some SOCcable tables.
	P_BackupTables();

	mainwads = 3; // doesn't include music.pk3
#ifdef USE_PATCH_DTA
	mainwads++;
#endif

	// load wad, including the main wad file
	CONS_Printf("W_InitMultipleFiles(): Adding IWAD and main PWADs.\n");
	W_InitMultipleFiles(&startupwadfiles);
	D_CleanFile(&startupwadfiles);

#ifndef DEVELOP // md5s last updated 22/02/20 (ddmmyy)

	// Check MD5s of autoloaded files
	W_VerifyFileMD5(0, ASSET_HASH_SRB2_PK3); // srb2.pk3
	W_VerifyFileMD5(1, ASSET_HASH_ZONES_PK3); // zones.pk3
	W_VerifyFileMD5(2, ASSET_HASH_CHARACTERS_PK3); // characters.pk3
#ifdef USE_PATCH_DTA
	W_VerifyFileMD5(3, ASSET_HASH_PATCH_PK3); // patch.pk3
#endif
	// don't check music.pk3 because people like to modify it, and it doesn't matter if they do
	// ...except it does if they slip maps in there, and that's what W_VerifyNMUSlumps is for.
#endif //ifndef DEVELOP

	cht_Init();

	//---------------------------------------------------- READY SCREEN
	// we need to check for dedicated before initialization of some subsystems

	CONS_Printf("I_StartupGraphics()...\n");
	I_StartupGraphics();

#ifdef HWRENDER
	// Lactozilla: Add every hardware mode CVAR and CCMD.
	// Has to be done before the configuration file loads,
	// but after the OpenGL library loads.
	HWR_AddCommands();
#endif

	//--------------------------------------------------------- CONSOLE
	// setup loading screen
	SCR_Startup();

	PaletteRemap_Init();

	HU_Init();

	CON_Init();

	D_RegisterServerCommands();
	D_RegisterClientCommands(); // be sure that this is called before D_CheckNetGame
	R_RegisterEngineStuff();
	S_RegisterSoundStuff();

	I_RegisterSysCommands();

	CON_StopRefresh(); // Temporarily stop refreshing the screen for wad loading

	if (startuppwads.numfiles)
	{
		CONS_Printf("W_InitMultipleFiles(): Adding extra PWADs.\n");
		W_InitMultipleFiles(&startuppwads);
		D_CleanFile(&startuppwads);
	}

	CON_StartRefresh(); // Restart the refresh!

	CONS_Printf("HU_LoadGraphics()...\n");
	HU_LoadGraphics();

	//--------------------------------------------------------- CONFIG.CFG
	M_FirstLoadConfig(); // WARNING : this do a "COM_BufExecute()"

	if (M_CheckParm("-gamedata") && M_IsNextParm())
	{
		// Moved from G_LoadGameData itself, as it would cause some crazy
		// confusion issues when loading mods.
		strlcpy(gamedatafilename, M_GetNextParm(), sizeof gamedatafilename);
	}

	G_LoadGameData(clientGamedata);
	M_CopyGameData(serverGamedata, clientGamedata);

	VID_PrepareModeList(); // Regenerate Modelist according to cv_fullscreen

	// set user default mode or mode set at cmdline
	SCR_CheckDefaultMode();

	wipegamestate = gamestate;

	savedata.lives = 0; // flag this as not-used

	//------------------------------------------------ COMMAND LINE PARAMS

	// this must be done after loading gamedata,
	// to avoid setting off the corrupted gamedata code in G_LoadGameData if a SOC with custom gamedata is added
	// -- Monster Iestyn 20/02/20
	if (M_CheckParm("-warp") && M_IsNextParm())
	{
		const char *word = M_GetNextParm();
		pstartmap = G_FindMapByNameOrCode(word, 0);
		if (! pstartmap)
			I_Error("Cannot find a map remotely named '%s'\n", word);
		else
		{
			if (!(M_CheckParm("-server") || dedicated))
				G_SetUsedCheats(true);
			autostart = true;
		}
	}

	if (M_CheckParm("-noupload"))
		COM_BufAddText("downloading 0\n");

	CONS_Printf("M_Init(): Init miscellaneous info.\n");
	M_Init();

	CONS_Printf("R_Init(): Init SRB2 refresh daemon.\n");
	R_Init();

	// setting up sound
	if (dedicated)
	{
		sound_disabled = true;
		midi_disabled = digital_disabled = true;
	}
	if (M_CheckParm("-noaudio")) // combines -nosound and -nomusic
	{
		sound_disabled = true;
		digital_disabled = true;
		midi_disabled = true;
	}
	else
	{
		if (M_CheckParm("-nosound"))
			sound_disabled = true;
		if (M_CheckParm("-nomusic")) // combines -nomidimusic and -nodigmusic
		{
			digital_disabled = true;
			midi_disabled = true;
		}
		else
		{
			if (M_CheckParm("-nomidimusic"))
				midi_disabled = true; // WARNING: DOS version initmusic in I_StartupSound
			if (M_CheckParm("-nodigmusic"))
				digital_disabled = true; // WARNING: DOS version initmusic in I_StartupSound
		}
	}
	if (!( sound_disabled && digital_disabled
#ifndef NO_MIDI
				&& midi_disabled
#endif
	 ))
	{
		CONS_Printf("S_InitSfxChannels(): Setting up sound channels.\n");
		I_StartupSound();
		I_InitMusic();
		S_InitSfxChannels(cv_soundvolume.value);
	}

	S_InitMusicDefs();

	CONS_Printf("ST_Init(): Init status bar.\n");
	ST_Init();

	if (M_CheckParm("-room"))
	{
		if (!M_IsNextParm())
			I_Error("usage: -room <room_id>\nCheck the Master Server's webpage for room ID numbers.\n");
		CV_SetValue(&cv_masterserver_room_id, atoi(M_GetNextParm()));

#ifdef UPDATE_ALERT
		GetMODVersion_Console();
#endif
	}

	// init all NETWORK
	CONS_Printf("D_CheckNetGame(): Checking network game status.\n");
	if (D_CheckNetGame())
		autostart = true;

	if (!dedicated)
		pickedchar = R_SkinAvailable(cv_defaultskin.string);

	// check for a driver that wants intermission stats
	// start the apropriate game based on parms
	if (M_CheckParm("-metal"))
	{
		G_RecordMetal();
		autostart = true;
	}
	else if (M_CheckParm("-record") && M_IsNextParm())
	{
		G_RecordDemo(M_GetNextParm());
		autostart = true;
	}

	// user settings come before "+" parameters.
	if (dedicated)
		COM_ImmedExecute(va("exec \"%s"PATHSEP"adedserv.cfg\"\n", srb2home));
	else
		COM_ImmedExecute(va("exec \"%s"PATHSEP"autoexec.cfg\" -noerror\n", srb2home));

	if (!autostart)
		M_PushSpecialParameters(); // push all "+" parameters at the command buffer

	// demo doesn't need anymore to be added with D_AddFile()
	p = M_CheckParm("-playdemo");
	if (!p)
		p = M_CheckParm("-timedemo");
	if (p && M_IsNextParm())
	{
		char tmp[MAX_WADPATH];
		// add .lmp to identify the EXTERNAL demo file
		// it is NOT possible to play an internal demo using -playdemo,
		// rather push a playdemo command.. to do.

		strcpy(tmp, M_GetNextParm());
		// get spaced filename or directory
		while (M_IsNextParm())
		{
			strcat(tmp, " ");
			strcat(tmp, M_GetNextParm());
		}

		FIL_DefaultExtension(tmp, ".lmp");

		CONS_Printf(M_GetText("Playing demo %s.\n"), tmp);

		if (M_CheckParm("-playdemo"))
		{
			singledemo = true; // quit after one demo
			G_DeferedPlayDemo(tmp);
		}
		else
			G_TimeDemo(tmp);

		G_SetGamestate(GS_NULL);
		wipegamestate = GS_NULL;
		return;
	}

	if (M_CheckParm("-ultimatemode"))
	{
		autostart = true;
		ultimatemode = true;
	}

	if (M_CheckParm("-splitscreen"))
	{
		autostart = true;
		splitscreen = true;
	}

	// rei/miru: bootmap (Idea: starts the game on a predefined map)
	if (bootmap && !(M_CheckParm("-warp") && M_IsNextParm()))
	{
		pstartmap = bootmap;

		if (pstartmap < 1 || pstartmap > NUMMAPS)
			I_Error("Cannot warp to map %d (out of range)\n", pstartmap);
		else
		{
			autostart = true;
		}
	}

	if (autostart || netgame)
	{
		gameaction = ga_nothing;

		CV_ClearChangedFlags();

		// Do this here so if you run SRB2 with eg +timelimit 5, the time limit counts
		// as having been modified for the first game.
		M_PushSpecialParameters(); // push all "+" parameter at the command buffer

		COM_BufExecute(); // ensure the command buffer gets executed before the map starts (+skin)

		if (M_CheckParm("-gametype") && M_IsNextParm())
		{
			// from Command_Map_f
			INT32 j;
			INT16 newgametype = -1;
			const char *sgametype = M_GetNextParm();

			newgametype = G_GetGametypeByName(sgametype);

			if (newgametype == -1) // reached end of the list with no match
			{
				j = atoi(sgametype); // assume they gave us a gametype number, which is okay too
				if (j >= 0 && j < gametypecount)
					newgametype = (INT16)j;
			}

			if (newgametype != -1)
			{
				j = gametype;
				G_SetGametype(newgametype);
				D_GameTypeChanged(j);
			}
		}

		if (server && !M_CheckParm("+map"))
		{
			// Prevent warping to nonexistent levels
			if (W_CheckNumForName(G_BuildMapName(pstartmap)) == LUMPERROR)
				I_Error("Could not warp to %s (map not found)\n", G_BuildMapName(pstartmap));
			else
			{
				if (M_CampaignWarpIsCheat(gametype, pstartmap, serverGamedata))
				{
					// If you're warping via command line, you know what you're doing.
					// No need to I_Error over this.
					G_SetUsedCheats(false);
				}

				D_MapChange(pstartmap, gametype, ultimatemode, true, 0, false, false);
			}
		}
	}
	else if (M_CheckParm("-skipintro"))
	{
		F_InitMenuPresValues();
		F_StartTitleScreen();
	}
	else
		F_StartIntro(); // Tails 03-03-2002

	CON_ToggleOff();

	if (dedicated && server)
	{
		levelstarttic = gametic;
		G_SetGamestate(GS_LEVEL);
		if (!P_LoadLevel(false, false))
			I_Quit(); // fail so reset game stuff
	}
}

const char *D_Home(void)
{
	const char *userhome = NULL;

#ifdef ANDROID
	return "/data/data/org.srb2/";
#endif

	if (M_CheckParm("-home") && M_IsNextParm())
		userhome = M_GetNextParm();
	else
	{
#if !(defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON))
		if (FIL_FileOK(CONFIGFILENAME))
			usehome = false; // Let's NOT use home
		else
#endif
			userhome = I_GetEnv("HOME"); //Alam: my new HOME for srb2
	}
#ifdef _WIN32 //Alam: only Win32 have APPDATA and USERPROFILE
	if (!userhome && usehome) //Alam: Still not?
	{
		char *testhome = NULL;
		testhome = I_GetEnv("APPDATA");
		if (testhome != NULL
			&& (FIL_FileOK(va("%s" PATHSEP "%s" PATHSEP CONFIGFILENAME, testhome, DEFAULTDIR))))
		{
			userhome = testhome;
		}
	}
#ifndef __CYGWIN__
	if (!userhome && usehome) //Alam: All else fails?
	{
		char *testhome = NULL;
		testhome = I_GetEnv("USERPROFILE");
		if (testhome != NULL
			&& (FIL_FileOK(va("%s" PATHSEP "%s" PATHSEP CONFIGFILENAME, testhome, DEFAULTDIR))))
		{
			userhome = testhome;
		}
	}
#endif// !__CYGWIN__
#endif// _WIN32
	if (usehome) return userhome;
	else return NULL;
}

static boolean check_top_dir(const char **path, const char *top)
{
	// empty string does NOT match
	if (!strcmp(top, ""))
		return false;

	if (!startswith(*path, top))
		return false;

	*path += strlen(top);

	// if it doesn't already end with a path separator,
	// check if a separator follows
	if (!endswith(top, PATHSEP))
	{
		if (startswith(*path, PATHSEP))
			*path += strlen(PATHSEP);
		else
			return false;
	}

	return true;
}

static int cmp_strlen_desc(const void *A, const void *B)
{
	const char *pA = A;
	const char *pB = B;
	size_t As = strlen(pA);
	size_t Bs = strlen(pB);
	return ((int)Bs - (int)As);
}

boolean D_IsPathAllowed(const char *path)
{
	char *paths[] = {
		srb2home,
		srb2path,
		cv_addons_folder.zstring
	};

	const size_t n_paths = sizeof paths / sizeof *paths;

	size_t i;

	// Sort folder paths by longest to shortest so
	// overlapping paths work. E.g.:
	// Path 1: /home/james/.srb2/addons
	// Path 2: /home/james/.srb2
	qsort(paths, n_paths, sizeof *paths, cmp_strlen_desc);

	// These paths are allowed to be absolute
	// path is offset so ".." can be checked only in the
	// rest of the path
	for (i = 0; i < n_paths; ++i)
	{
		if (check_top_dir(&path, paths[i]))
			break;
	}

	// Only if none of the presets matched
	if (i == n_paths)
	{
		// Cannot be an absolute path
		if (M_IsPathAbsolute(path))
			return false;
	}

	// Cannot traverse upwards
	if (strstr(path, ".."))
		return false;

	return true;
}

boolean D_CheckPathAllowed(const char *path, const char *why)
{
	if (!D_IsPathAllowed(path))
	{
		CONS_Alert(CONS_WARNING, "%s: %s, location is not allowed\n", why, path);
		return false;
	}

	return true;
}
