// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2018 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 (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)
#include <sys/stat.h>
#include <sys/types.h>
#endif

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

#ifdef PC_DOS
#include <stdio.h> // for snprintf
int	snprintf(char *str, size_t n, const char *fmt, ...);
//int	vsnprintf(char *str, size_t n, const char *fmt, va_list ap);
#endif

#if (defined (_WIN32) && !defined (_WIN32_WCE)) && !defined (_XBOX)
#include <direct.h>
#include <malloc.h>
#endif

#if !defined (UNDER_CE)
#include <time.h>
#elif defined (_XBOX)
#define NO_TIME
#endif

#include "doomdef.h"
#include "am_map.h"
#include "console.h"
#include "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_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 "s_sound.h"
#include "st_stuff.h"
#include "v_video.h"
#include "w_wad.h"
#include "z_zone.h"
#include "d_main.h"
#include "d_netfil.h"
#include "m_cheat.h"
#include "y_inter.h"
#include "p_local.h" // chasecam
#include "m_misc.h" // screenshot functionality
#include "dehacked.h" // Dehacked list test
#include "m_cond.h" // condition initialization
#include "fastcmp.h"
#include "keys.h"
#include "filesrch.h" // refreshdirmenu

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

#ifdef _XBOX
#include "sdl12/SRB2XBOX/xboxhelp.h"
#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

#ifdef HAVE_BLUA
#include "lua_script.h"
#endif

#ifdef HAVE_DISCORDRPC
#include "discord.h"
#endif

// platform independant focus loss
UINT8 window_notinfocus = false;

//
// DEMO LOOP
//
//static INT32 demosequence;
static const char *pagename = "MAP1PIC";
static char *startupwadfiles[MAX_WADFILES];
static char *startuppwads[MAX_WADFILES];

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

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

postimg_t postimgtype[MAXSPLITSCREENPLAYERS];
INT32 postimgparam[MAXSPLITSCREENPLAYERS];

// These variables are only true if
// whether the respective sound system is disabled
// or they're init'ed, but the player just toggled them
#ifdef _XBOX
#ifndef NO_MIDI
boolean midi_disabled = true;
#endif
boolean sound_disabled = true;
boolean digital_disabled = true;
#else
#ifndef NO_MIDI
boolean midi_disabled = false;
#endif
boolean sound_disabled = false;
boolean digital_disabled = false;
#endif

boolean advancedemo;
#ifdef DEBUGFILE
INT32 debugload = 0;
#endif

char savegamename[256];

#ifdef _arch_dreamcast
char srb2home[256] = "/cd";
char srb2path[256] = "/cd";
#else
char srb2home[256] = ".";
char srb2path[256] = ".";
#endif
boolean usehome = true;
const char *pandf = "%s" PATHSEP "%s";

//
// 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);
}
// just for lock this function
#if defined (PC_DOS) && !defined (DOXYGEN)
void D_PostEvent_end(void) {};
#endif

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

	for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
	{
		ev = &events[eventtail];

		// 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 (demo.savemode == DSM_TITLEENTRY)
		{
			if (G_DemoTitleResponder(ev))
				continue;
		}

		// 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

		// Demo input:
		if (demo.playback)
			if (M_DemoResponder(ev))
				continue;	// demo ate the event

		// 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

		G_Responder(ev);
	}
}

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

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

	if (!dedicated)
	{
		if (nodrawers)
			return; // for comparative timing/profiling

		// check for change of screen size (video mode)
		if (setmodeneeded && !wipe)
			SCR_SetMode(); // change video mode

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

		// change the view size if needed
		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)
	{
		// set for all later
		wipedefindex = gamestate; // wipe_xxx_toblack
		if (gamestate == GS_TITLESCREEN && wipegamestate != GS_INTRO)
			wipedefindex = wipe_timeattack_toblack;
		else 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 (!dedicated)
		{
			// Fade to black first
			if (gamestate != GS_LEVEL // fades to black on its own timing, always
			 && wipedefs[wipedefindex] != UINT8_MAX)
			{
				F_WipeStartScreen();
				V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
				F_WipeEndScreen();
				F_RunWipe(wipedefs[wipedefindex], gamestate != GS_TIMEATTACK);
			}

			if (gamestate != GS_LEVEL && rendermode != render_none)
			{
				V_SetPaletteLump("PLAYPAL"); // Reset the palette
				R_ReInitColormaps(0, LUMPERROR);
			}

			F_WipeStartScreen();
		}
		else //dedicated servers
		{
			F_RunWipe(wipedefs[wipedefindex], gamestate != GS_TIMEATTACK);
			wipegamestate = gamestate;
		}
	}

	if (dedicated) //bail out after wipe logic
		return;

	// do buffered drawing
	switch (gamestate)
	{
		case GS_LEVEL:
			if (!gametic)
				break;
			HU_Erase();
			AM_Drawer();
			break;

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

		case GS_VOTING:
			Y_VoteDrawer();
			HU_Erase();
			HU_Drawer();
			break;

		case GS_TIMEATTACK:
			break;

		case GS_INTRO:
			F_IntroDrawer();
			if (wipegamestate == (gamestate_t)-1)
			{
				wipe = true;
				wipedefindex = gamestate; // wipe_xxx_toblack
			}
			break;

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

		case GS_GAMEEND:
			F_GameEndDrawer();
			break;

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

		case GS_CONTINUING:
			F_ContinueDrawer();
			break;

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

		case GS_TITLESCREEN:
			F_TitleScreenDrawer();
			if (wipe)
				wipedefindex = wipe_titlescreen_toblack;
			break;

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

	if (gamestate == GS_LEVEL)
	{
		// draw the view directly
		if (cv_renderview.value && !automapactive)
		{
			for (i = 0; i <= splitscreen; i++)
			{
				if (players[displayplayers[i]].mo || players[displayplayers[i]].playerstate == PST_DEAD)
				{
					if (i == 0) // Initialize for P1
					{
						viewwindowy = 0;
						viewwindowx = 0;

						topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
						objectsdrawn = 0;
					}

					viewssnum = i;

#ifdef HWRENDER
					if (rendermode != render_soft)
						HWR_RenderPlayerView(i, &players[displayplayers[i]]);
					else
#endif
					if (rendermode != render_none)
					{
						if (i > 0) // Splitscreen-specific
						{
							switch (i)
							{
								case 1:
									if (splitscreen > 1)
									{
										viewwindowx = viewwidth;
										viewwindowy = 0;
									}
									else
									{
										viewwindowx = 0;
										viewwindowy = viewheight;
									}
									M_Memcpy(ylookup, ylookup2, viewheight*sizeof (ylookup[0]));
									break;
								case 2:
									viewwindowx = 0;
									viewwindowy = viewheight;
									M_Memcpy(ylookup, ylookup3, viewheight*sizeof (ylookup[0]));
									break;
								case 3:
									viewwindowx = viewwidth;
									viewwindowy = viewheight;
									M_Memcpy(ylookup, ylookup4, viewheight*sizeof (ylookup[0]));
								default:
									break;
							}


							topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
						}

						R_RenderPlayerView(&players[displayplayers[i]]);

						if (i > 0)
							M_Memcpy(ylookup, ylookup1, viewheight*sizeof (ylookup[0]));
					}
				}
			}

			if (rendermode == render_soft)
			{
				for (i = 0; i <= splitscreen; i++)
				{
					if (postimgtype[i])
						V_DoPostProcessor(i, postimgtype[i], postimgparam[i]);
				}
			}
		}

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

		ST_Drawer();
		HU_Drawer();
	}

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

	wipegamestate = gamestate;

	// draw pause pic
	if (paused && cv_showhud.value && !demo.playback)
	{
		INT32 py;
		patch_t *patch;
		if (automapactive)
			py = 4;
		else
			py = viewwindowy + 4;
		patch = W_CachePatchName("M_PAUSE", PU_CACHE);
		V_DrawScaledPatch(viewwindowx + (BASEVIDWIDTH - SHORT(patch->width))/2, py, 0, patch);
	}

	if (demo.rewinding)
		V_DrawFadeScreen(TC_RAINBOW, (leveltime & 0x20) ? SKINCOLOR_PASTEL : SKINCOLOR_MOONSLAM);

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

	// FIXME: draw either console or menu, not the two
	if (gamestate != GS_TIMEATTACK)
		CON_Drawer();

#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

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

		if (rendermode != render_none)
		{
			F_WipeEndScreen();
			F_RunWipe(wipedefs[wipedefindex], gamestate != GS_TIMEATTACK);
		}
	}

	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-ST_HEIGHT-40, V_YELLOWMAP, s);
			snprintf(s, sizeof s - 1, "send %d b/s", sendbps);
			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-ST_HEIGHT-30, V_YELLOWMAP, s);
			snprintf(s, sizeof s - 1, "GameMiss %.2f%%", gamelostpercent);
			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-ST_HEIGHT-20, V_YELLOWMAP, s);
			snprintf(s, sizeof s - 1, "SysMiss %.2f%%", lostpercent);
			V_DrawRightAlignedString(BASEVIDWIDTH, BASEVIDHEIGHT-ST_HEIGHT-10, V_YELLOWMAP, s);
		}

		if (cv_shittyscreen.value)
			V_DrawVhsEffect(cv_shittyscreen.value == 2);

		I_FinishUpdate(); // page flip or blit buffer
	}
}

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

tic_t rendergametic;

void D_SRB2Loop(void)
{
	tic_t oldentertics = 0, entertic = 0, realtics = 0, rendertimeout = INFTICS;

	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

	oldentertics = I_GetTime();

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

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

	// 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"
	"===========================================================================\n");

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

	if (rendermode == render_soft)
		V_DrawFixedPatch(0, 0, FRACUNIT/2, 0, (patch_t *)W_CacheLumpNum(W_GetNumForName("KARTKREW"), PU_CACHE), NULL);
	I_FinishUpdate(); // page flip or blit buffer

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

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

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

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

		if (!realtics && !singletics)
		{
			I_Sleep();
			continue;
		}

#ifdef HW3SOUND
		HW3S_BeginFrameUpdate();
#endif

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

			// Update display, next frame, with current state.
			D_Display();

			if (moviemode)
				M_SaveFrame();
			if (takescreenshot) // Only take screenshots after drawing.
				M_DoScreenShot();
		}
		else if (rendertimeout < entertic) // in case the server hang or netsplit
		{
			D_Display();

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

		// consoleplayer -> displayplayers (hear sounds from viewpoint)
		S_UpdateSounds(); // move positional sounds

		// check for media change, loop music..
		I_UpdateCD();

#ifdef HW3SOUND
		HW3S_EndFrameUpdate();
#endif

#ifdef HAVE_BLUA
		LUA_Step();
#endif

#ifdef HAVE_DISCORDRPC
		if (! dedicated)
		{
			Discord_RunCallbacks();
		}
#endif
	}
}

//
// 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;
	if (netgame)
	{
		if (gametype == GT_RACE) // SRB2kart
		{
			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);

	splitscreen = 0;
	SplitScreen_OnChange();
	botingame = false;
	botskin = 0;
	cv_debug = 0;
	emeralds = 0;

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

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

	gameaction = ga_nothing;
	memset(displayplayers, 0, sizeof(displayplayers));
	consoleplayer = 0;
	//demosequence = -1;
	gametype = GT_RACE; // SRB2kart
	paused = false;
	advancedemo = false;
	F_StartTitleScreen();

	// Reset the palette -- SRB2Kart: actually never mind let's do this in the middle of every fade
	/*if (rendermode != render_none)
		V_SetPaletteLump("PLAYPAL");*/
}

//
// D_AddFile
//
static void D_AddFile(const char *file, char **filearray)
{
	size_t pnumwadfiles;
	char *newfile;

	for (pnumwadfiles = 0; filearray[pnumwadfiles]; pnumwadfiles++)
		;

	newfile = malloc(strlen(file) + 1);
	if (!newfile)
	{
		I_Error("No more free memory to AddFile %s",file);
	}
	strcpy(newfile, file);

	filearray[pnumwadfiles] = newfile;
}

static inline void D_CleanFile(char **filearray)
{
	size_t pnumwadfiles;
	for (pnumwadfiles = 0; filearray[pnumwadfiles]; pnumwadfiles++)
	{
		free(filearray[pnumwadfiles]);
		filearray[pnumwadfiles] = NULL;
	}
}

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

static boolean AddIWAD(void)
{
	char * path = va(pandf,srb2path,"srb2.srb");

	if (FIL_ReadFileOK(path))
	{
		D_AddFile(path, startupwadfiles);
		return true;
	}
	else
	{
		return false;
	}
}

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

#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
	// change to the directory where 'srb2.srb' 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 !defined(_WIN32_WCE) && !defined(_PS3)
		if (getcwd(srb2path, 256) != NULL)
			srb2waddir = srb2path;
		else
#endif
		{
#ifdef _arch_dreamcast
			srb2waddir = "/cd";
#else
			srb2waddir = srb2path;
#endif
		}
	}

	// Load the IWAD
	if (! AddIWAD())
	{
		I_Error("SRB2.SRB not found! Expected in %s\n", srb2waddir);
	}

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

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

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

	D_AddFile(va(pandf,srb2waddir,"gfx.kart"), startupwadfiles);
	D_AddFile(va(pandf,srb2waddir,"textures.kart"), startupwadfiles);
	D_AddFile(va(pandf,srb2waddir,"chars.kart"), startupwadfiles);
	D_AddFile(va(pandf,srb2waddir,"maps.kart"), startupwadfiles);
#ifdef USE_PATCH_KART
	D_AddFile(va(pandf,srb2waddir,"patch.kart"), startupwadfiles);
#endif

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

/* ======================================================================== */
// Just print the nice red titlebar like the original SRB2 for DOS.
/* ======================================================================== */
#ifdef PC_DOS
static inline void D_Titlebar(char *title1, char *title2)
{
	// SRB2 banner
	clrscr();
	textattr((BLUE<<4)+WHITE);
	clreol();
	cputs(title1);

	// standard srb2 banner
	textattr((RED<<4)+WHITE);
	clreol();
	gotoxy((80-strlen(title2))/2, 2);
	cputs(title2);
	normvideo();
	gotoxy(1,3);
}
#endif

//
// Center the title string, then add the date and time of compilation.
//
static inline void D_MakeTitleString(char *s)
{
	char temp[82];
	char *t;
	const char *u;
	INT32 i;

	for (i = 0, t = temp; i < 82; i++)
		*t++=' ';

	for (t = temp + (80-strlen(s))/2, u = s; *u != '\0' ;)
		*t++ = *u++;

	u = compdate;
	for (t = temp + 1, i = 11; i-- ;)
		*t++ = *u++;
	u = comptime;
	for (t = temp + 71, i = 8; i-- ;)
		*t++ = *u++;

	temp[80] = '\0';
	strcpy(s, temp);
}


//
// D_SRB2Main
//
void D_SRB2Main(void)
{
	INT32 p, i;
	char srb2[82]; // srb2 title banner
	char title[82];
	lumpinfo_t *lumpinfo;
	UINT16 wadnum;
	char *name;

	INT32 pstartmap = 1;
	boolean autostart = false;

	// Print GPL notice for our console users (Linux)
	CONS_Printf(
	"\n\nSonic Robo Blast 2 Kart\n"
	"Copyright (C) 1998-2018 by Kart Krew & STJr\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 (PC_DOS) && !defined (_WIN32_WCE) && !defined(NOTERMIOS)
	if (setvbuf(stderr, NULL, _IOFBF, 1000))
		I_OutputMsg("setvbuf didnt work\n");
#endif

#ifdef GETTEXT
	// initialise locale code
	M_StartupLocale();
#endif

	// 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_Check();

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

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

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

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

	strcpy(title, "SRB2Kart");
	strcpy(srb2, "SRB2Kart");
	D_MakeTitleString(srb2);

#ifdef PC_DOS
	D_Titlebar(srb2, title);
#endif

#if defined (__OS2__) && !defined (HAVE_SDL)
	// set PM window title
	snprintf(pmData->title, sizeof (pmData->title),
		"SRB2Kart" VERSIONSTRING ": %s",
		title);
	pmData->title[sizeof (pmData->title) - 1] = '\0';
#endif

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

	// default savegame
	strcpy(savegamename, SAVEGAMENAME"%u.ssg");

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

		if (!userhome)
		{
#if ((defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)) && !defined (__CYGWIN__) && !defined (DC) && !defined (PSP) && !defined(GP2X)
			I_Error("Please set $HOME to your home directory\n");
#elif defined (_WIN32_WCE) && 0
			if (dedicated)
				snprintf(configfile, sizeof configfile, "/Storage Card/SRB2DEMO/d"CONFIGFILENAME);
			else
				snprintf(configfile, sizeof configfile, "/Storage Card/SRB2DEMO/"CONFIGFILENAME);
#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);

			I_mkdir(srb2home, 0700);
#else
			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);
#endif
		}

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

#ifdef _arch_dreamcast
	strcpy(downloaddir, "/ram"); // the dreamcast's TMP
#endif
	}

	// rand() needs seeded regardless of password
	srand((unsigned int)time(NULL));

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

	// add any files specified on the command line with -file wadfile
	// to the wad list
	if (!(M_CheckParm("-connect") && !M_CheckParm("-server")))
	{
		if (M_CheckParm("-file"))
		{
			// the parms after p are wadfile/lump names,
			// until end of parms or another - preceded parm
			while (M_IsNextParm())
			{
				const char *s = M_GetNextParm();

				if (s) // Check for NULL?
					D_AddFile(s, startuppwads);
			}
		}
	}

	// get map from parms

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

	if (M_CheckParm("-warp") && M_IsNextParm())
	{
		const char *word = M_GetNextParm();
		char ch; // use this with sscanf to catch non-digits with
		if (fastncmp(word, "MAP", 3)) // MAPxx name
			pstartmap = M_MapNumber(word[3], word[4]);
		else if (sscanf(word, "%d%c", &pstartmap, &ch) != 1) // a plain number
			I_Error("Cannot warp to map %s (invalid map name)\n", word);
		// Don't check if lump exists just yet because the wads haven't been loaded!
		// Just do a basic range check here.
		if (pstartmap < 1 || pstartmap > NUMMAPS)
			I_Error("Cannot warp to map %d (out of range)\n", pstartmap);
		else
		{
			if (!M_CheckParm("-server"))
				G_SetGameModified(true, true);
			autostart = true;
		}
	}

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

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

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

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

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

	// Setup default unlockable conditions
	M_SetupDefaultConditionSets();

	// Setup character tables
	// Have to be done here before files are loaded
	M_InitCharacterTables();

	// load wad, including the main wad file
	CONS_Printf("W_InitMultipleFiles(): Adding IWAD and main PWADs.\n");
	if (!W_InitMultipleFiles(startupwadfiles, false))
#ifdef _DEBUG
		CONS_Error("A WAD file was not found or not valid.\nCheck the log to see which ones.\n");
#else
		I_Error("A WAD file was not found or not valid.\nCheck the log to see which ones.\n");
#endif
	D_CleanFile(startupwadfiles);

	mainwads = 0;

#ifndef DEVELOP
	// Check MD5s of autoloaded files
	// Note: Do not add any files that ignore MD5!
	W_VerifyFileMD5(mainwads, ASSET_HASH_SRB2_SRB);						// srb2.srb/srb2.wad
#ifdef USE_PATCH_DTA
	mainwads++; W_VerifyFileMD5(mainwads, ASSET_HASH_PATCH_DTA);		// patch.dta
#endif
	mainwads++; W_VerifyFileMD5(mainwads, ASSET_HASH_GFX_KART);			// gfx.kart
	mainwads++; W_VerifyFileMD5(mainwads, ASSET_HASH_TEXTURES_KART);	// textures.kart
	mainwads++; W_VerifyFileMD5(mainwads, ASSET_HASH_CHARS_KART);		// chars.kart
	mainwads++; W_VerifyFileMD5(mainwads, ASSET_HASH_MAPS_KART);		// maps.kart -- 4 - If you touch this, make sure to touch up the majormods stuff below.
#ifdef USE_PATCH_KART
	mainwads++; W_VerifyFileMD5(mainwads, ASSET_HASH_PATCH_KART);		// patch.kart
#endif
#else
#ifdef USE_PATCH_DTA
	mainwads++;	// patch.dta
#endif
	mainwads++;	// gfx.kart
	mainwads++;	// textures.kart
	mainwads++;	// chars.kart
	mainwads++;	// maps.kart
#ifdef USE_PATCH_KART
	mainwads++;	// patch.kart
#endif

#endif //ifndef DEVELOP

	//
	// search for maps
	//
	for (wadnum = 4; wadnum < 6; wadnum++) // fucking arbitrary numbers
	{
		lumpinfo = wadfiles[wadnum]->lumpinfo;
		for (i = 0; i < wadfiles[wadnum]->numlumps; i++, lumpinfo++)
		{
			name = lumpinfo->name;

			if (name[0] == 'M' && name[1] == 'A' && name[2] == 'P') // Ignore the headers
			{
				INT16 num;
				if (name[5] != '\0')
					continue;
				num = (INT16)M_MapNumber(name[3], name[4]);

				// we want to record whether this map exists. if it doesn't have a header, we can assume it's not relephant
				if (num <= NUMMAPS && mapheaderinfo[num - 1])
				{
					mapheaderinfo[num - 1]->menuflags |= LF2_EXISTSHACK;
				}
			}
		}
	}

	if (!W_InitMultipleFiles(startuppwads, true))
		CONS_Error("A PWAD file was not found or not valid.\nCheck the log to see which ones.\n");
	D_CleanFile(startuppwads);

	//
	// search for maps... again.
	//
	for (wadnum = mainwads+1; wadnum < numwadfiles; wadnum++)
	{
		lumpinfo = wadfiles[wadnum]->lumpinfo;
		for (i = 0; i < wadfiles[wadnum]->numlumps; i++, lumpinfo++)
		{
			name = lumpinfo->name;

			if (name[0] == 'M' && name[1] == 'A' && name[2] == 'P') // Ignore the headers
			{
				INT16 num;
				if (name[5] != '\0')
					continue;
				num = (INT16)M_MapNumber(name[3], name[4]);

				// we want to record whether this map exists. if it doesn't have a header, we can assume it's not relephant
				if (num <= NUMMAPS && mapheaderinfo[num - 1])
				{
					if (mapheaderinfo[num - 1]->menuflags & LF2_EXISTSHACK)
						G_SetGameModified(multiplayer, true); // oops, double-defined - no record attack privileges for you
					mapheaderinfo[num - 1]->menuflags |= LF2_EXISTSHACK;
				}

				CONS_Printf("%s\n", name);
			}
		}
	}

	cht_Init();

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

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

#ifdef HWRENDER
	if (rendermode == render_opengl)
	{
		for (i = 0; i < numwadfiles; i++)
			HWR_LoadShaders(i, (wadfiles[i]->type == RET_PK3));
	}
#endif

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

	// we need the font of the console
	CONS_Printf("HU_Init(): Setting up heads up display.\n");
	HU_Init();

	COM_Init();
	// libogc has a CON_Init function, we must rename SRB2's CON_Init in WII/libogc
#ifndef _WII
	CON_Init();
#else
	CON_InitWii();
#endif

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

	I_RegisterSysCommands();

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

	G_LoadGameData();

#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
	VID_PrepareModeList(); // Regenerate Modelist according to cv_fullscreen
#endif

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

	wipegamestate = gamestate;

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

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

	// Initialize CD-Audio
	if (M_CheckParm("-usecd") && !dedicated)
		I_InitCD();

	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;
		digital_disabled = true;
#ifndef NO_MIDI
		midi_disabled = true;
#endif
	}
	if (M_CheckParm("-noaudio")) // combines -nosound and -nomusic
	{
		sound_disabled = true;
		digital_disabled = true;
#ifndef NO_MIDI
		midi_disabled = true;
#endif
	}
	else
	{
		if (M_CheckParm("-nosound"))
			sound_disabled = true;
		if (M_CheckParm("-nomusic")) // combines -nomidimusic and -nodigmusic
		{
			digital_disabled = true;
#ifndef NO_MIDI
			midi_disabled = true;
#endif
		}
		else
		{
#ifndef NO_MIDI
			if (M_CheckParm("-nomidimusic"))
				midi_disabled = true; // WARNING: DOS version initmusic in I_StartupSound
#endif
			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();

	// Set up splitscreen players before joining!
	if (!dedicated && (M_CheckParm("-splitscreen") && M_IsNextParm()))
	{
		UINT8 num = atoi(M_GetNextParm());
		if (num >= 1 && num <= 4)
		{
			CV_StealthSetValue(&cv_splitplayers, num);
			splitscreen = num-1;
			SplitScreen_OnChange();
		}
	}

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

	if (splitscreen && !M_CheckParm("-connect")) // Make sure multiplayer & autostart is set if you have splitscreen, even after D_CheckNetGame
		multiplayer = autostart = true;

	// 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"kartserv.cfg\"\n", srb2home));
	else
		COM_ImmedExecute(va("exec \"%s"PATHSEP"kartexec.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"))
		{
			demo.quitafterplaying = 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 (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

		strncpy(connectedservername, cv_servername.string, MAXSERVERNAME);

		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 < NUMGAMETYPES)
					newgametype = (INT16)j;
			}

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

		if (M_CheckParm("-skill") && M_IsNextParm())
		{
			INT32 j;
			INT16 newskill = -1;
			const char *sskill = M_GetNextParm();

			for (j = 0; kartspeed_cons_t[j].strvalue; j++)
				if (!strcasecmp(kartspeed_cons_t[j].strvalue, sskill))
				{
					newskill = (INT16)kartspeed_cons_t[j].value;
					break;
				}
			if (!kartspeed_cons_t[j].strvalue) // reached end of the list with no match
			{
				j = atoi(sskill); // assume they gave us a skill number, which is okay too
				if (j >= 0 && j <= 2)
					newskill = (INT16)j;
			}

			if (newskill != -1)
				CV_SetValue(&cv_kartspeed, newskill);
		}

		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));
			// Prevent warping to locked levels
			// ... unless you're in a dedicated server.  Yes, technically this means you can view any level by
			// running a dedicated server and joining it yourself, but that's better than making dedicated server's
			// lives hell.
			else if (!dedicated && M_MapLocked(pstartmap))
				I_Error("You need to unlock this level before you can warp to it!\n");
			else
				D_MapChange(pstartmap, gametype, (boolean)cv_kartencore.value, true, 0, false, false);
		}
	}
	else if (M_CheckParm("-skipintro"))
	{
		F_StartTitleScreen();
	}
	else
		F_StartIntro(); // Tails 03-03-2002

	CON_ToggleOff();

	if (dedicated && server)
	{
		pagename = "TITLESKY";
		levelstarttic = gametic;
		G_SetGamestate(GS_LEVEL);
		if (!P_SetupLevel(false))
			I_Quit(); // fail so reset game stuff
	}

#ifdef HAVE_DISCORDRPC
	if (! dedicated)
	{
		DRPC_Init();
	}
#endif
}

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

#ifdef ANDROID
	return "/data/data/org.srb2/";
#endif
#ifdef _arch_dreamcast
	char VMUHOME[] = "HOME=/vmu/a1";
	putenv(VMUHOME); //don't use I_PutEnv
#endif

	if (M_CheckParm("-home") && M_IsNextParm())
		userhome = M_GetNextParm();
	else
	{
#if defined (GP2X)
		usehome = false; //let use the CWD
		return NULL;
#elif !((defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)) && !defined (__APPLE__) && !defined(_WIN32_WCE)
		if (FIL_FileOK(CONFIGFILENAME))
			usehome = false; // Let's NOT use home
		else
#endif
			userhome = I_GetEnv("HOME"); //Alam: my new HOME for srb2
	}
#if defined (_WIN32) && !defined(_WIN32_WCE) //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;
}
