/*
	C-Dogs SDL
	A port of the legendary (and fun) action/arcade cdogs.
	Copyright (C) 1995 Ronny Wester
	Copyright (C) 2003 Jeremy Chin
	Copyright (C) 2003-2007 Lucas Martin-King

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

	This file incorporates work covered by the following copyright and
	permission notice:

	Copyright (c) 2013-2018, 2020-2024 Cong Xu
	All rights reserved.

	Redistribution and use in source and binary forms, with or without
	modification, are permitted provided that the following conditions are met:

	Redistributions of source code must retain the above copyright notice, this
	list of conditions and the following disclaimer.
	Redistributions in binary form must reproduce the above copyright notice,
	this list of conditions and the following disclaimer in the documentation
	and/or other materials provided with the distribution.

	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
	AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
	IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
	ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
	LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
	CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
	SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
	INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
	CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
	ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
	POSSIBILITY OF SUCH DAMAGE.
*/
#include "prep.h"

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <SDL_mouse.h>

#include <cdogs/actors.h>
#include <cdogs/blit.h>
#include <cdogs/config_io.h>
#include <cdogs/draw/draw.h>
#include <cdogs/files.h>
#include <cdogs/font.h>
#include <cdogs/grafx.h>
#include <cdogs/handle_game_events.h>
#include <cdogs/joystick.h>
#include <cdogs/keyboard.h>
#include <cdogs/log.h>
#include <cdogs/map_wolf.h>
#include <cdogs/music.h>
#include <cdogs/net_client.h>
#include <cdogs/net_server.h>
#include <cdogs/pic_manager.h>
#include <cdogs/sounds.h>
#include <cdogs/utils.h>

#include "briefing_screens.h"
#include "game.h"
#include "loading_screens.h"
#include "namegen.h"
#include "password.h"
#include "player_select_menus.h"

typedef struct
{
	MenuSystem ms;
	GameLoopResult (*checkFunc)(void *, LoopRunner *l);
	void *data;
} ScreenWaitData;
static void ScreenWaitTerminate(GameLoopData *data);
static GameLoopResult ScreenWaitUpdate(GameLoopData *data, LoopRunner *l);
static void ScreenWaitDraw(GameLoopData *data);
static GameLoopData *ScreenWait(
	const char *message, GameLoopResult (*checkFunc)(void *, LoopRunner *l),
	void *data)
{
	ScreenWaitData *swData;
	CMALLOC(swData, sizeof *swData);
	MenuSystemInit(
		&swData->ms, &gEventHandlers, &gGraphicsDevice, svec2i_zero(),
		gGraphicsDevice.cachedConfig.Res);
	swData->ms.allowAborts = true;
	swData->ms.root = swData->ms.current =
		MenuCreateNormal("", message, MENU_TYPE_NORMAL, 0);
	MenuAddExitType(&swData->ms, MENU_TYPE_RETURN);
	swData->checkFunc = checkFunc;
	swData->data = data;

	return GameLoopDataNew(
		swData, ScreenWaitTerminate, NULL, NULL, NULL, ScreenWaitUpdate,
		ScreenWaitDraw);
}
static void ScreenWaitTerminate(GameLoopData *data)
{
	ScreenWaitData *swData = data->Data;

	MenuSystemTerminate(&swData->ms);
	CFREE(swData->data);
	CFREE(swData);
}
static GameLoopResult ScreenWaitUpdate(GameLoopData *data, LoopRunner *l)
{
	ScreenWaitData *swData = data->Data;

	const GameLoopResult result = swData->checkFunc(swData->data, l);
	if (result != UPDATE_RESULT_DRAW)
	{
		return result;
	}
	const GameLoopResult menuResult = MenuUpdate(&swData->ms);
	if (menuResult == UPDATE_RESULT_OK)
	{
		LoopRunnerPop(l);
	}
	return menuResult;
}
static void ScreenWaitDraw(GameLoopData *data)
{
	const ScreenWaitData *swData = data->Data;

	MenuDraw(&swData->ms);
}

static GameLoopResult CheckCampaignDefComplete(void *data, LoopRunner *l);
GameLoopData *ScreenWaitForCampaignDef(void)
{
	char buf[256];
	char ipbuf[64];
	enet_address_get_host_ip(&gNetClient.peer->address, ipbuf, sizeof ipbuf);
	sprintf(
		buf, "Connecting to %s:%u...", ipbuf, gNetClient.peer->address.port);
	return ScreenWait(buf, CheckCampaignDefComplete, NULL);
}
static GameLoopResult CheckCampaignDefComplete(void *data, LoopRunner *l)
{
	UNUSED(data);
	if (gCampaign.IsLoaded || gCampaign.IsError)
	{
		if (gCampaign.IsError)
		{
			// Failed to load campaign
			CampaignUnload(&gCampaign);
		}
		else
		{
			CASSERT(gCampaign.IsClient, "campaign is not client");
		}
		LoopRunnerPop(l);
		return UPDATE_RESULT_OK;
	}
	return UPDATE_RESULT_DRAW;
}

static void NumPlayersTerminate(GameLoopData *data);
static void NumPlayersOnEnter(GameLoopData *data);
static GameLoopResult NumPlayersUpdate(GameLoopData *data, LoopRunner *l);
static void NumPlayersDraw(GameLoopData *data);
GameLoopData *NumPlayersSelection(
	GraphicsDevice *graphics, EventHandlers *handlers)
{
	MenuSystem *ms;
	CMALLOC(ms, sizeof *ms);
	MenuSystemInit(
		ms, handlers, graphics, svec2i_zero(), graphics->cachedConfig.Res);
	ms->allowAborts = true;
	ms->root = ms->current =
		MenuCreateNormal("", "Select number of players", MENU_TYPE_NORMAL, 0);
	MenuAddSubmenu(ms->current, MenuCreateReturn("(No local players)", 0));
	for (int i = 0; i < MAX_LOCAL_PLAYERS; i++)
	{
		char buf[2];
		sprintf(buf, "%d", i + 1);
		MenuAddSubmenu(ms->current, MenuCreateReturn(buf, i + 1));
	}
	MenuAddExitType(ms, MENU_TYPE_RETURN);
	// Select 1 player default
	ms->current->u.normal.index = 1;

	return GameLoopDataNew(
		ms, NumPlayersTerminate, NumPlayersOnEnter, NULL, NULL,
		NumPlayersUpdate, NumPlayersDraw);
}
static void NumPlayersTerminate(GameLoopData *data)
{
	MenuSystem *ms = data->Data;
	MenuSystemTerminate(ms);
	CFREE(data->Data);
}
static void NumPlayersOnEnter(GameLoopData *data)
{
	UNUSED(data);
	MusicPlayFromChunk(
		&gSoundDevice.music, MUSIC_BRIEFING,
		&gCampaign.Setting.CustomSongs[MUSIC_BRIEFING]);
}
static GameLoopResult NumPlayersUpdate(GameLoopData *data, LoopRunner *l)
{
	int numPlayers = 0;
	GameLoopResult result = UPDATE_RESULT_DRAW;
	bool hasAbort = false;
	if (gEventHandlers.DemoQuitTimer > 0)
	{
		// Select random number of players for demo
		numPlayers = RAND_INT(1, 5);
		result = UPDATE_RESULT_OK;
	}
	else
	{
		MenuSystem *ms = data->Data;
		result = MenuUpdate(ms);
		numPlayers = ms->current->u.returnCode;
		hasAbort = ms->hasAbort;
	}

	if (result == UPDATE_RESULT_OK)
	{
		if (hasAbort)
		{
			CampaignUnload(&gCampaign);
			LoopRunnerPop(l);
		}
		else
		{
			CA_FOREACH(const PlayerData, p, gPlayerDatas)
			CASSERT(!p->IsLocal, "unexpected local player");
			CA_FOREACH_END()
			// Add the players
			for (int i = 0; i < numPlayers; i++)
			{
				GameEvent e = GameEventNew(GAME_EVENT_PLAYER_DATA);
				e.u.PlayerData = PlayerDataDefault(i);
				e.u.PlayerData.UID = gNetClient.FirstPlayerUID + i;
				GameEventsEnqueue(&gGameEvents, e);
			}
			// Process the events to force add the players
			HandleGameEvents(&gGameEvents, NULL, NULL, NULL, NULL);
			// This also causes the client to send player data to the server

			// Switch to player selection
			LoopRunnerChange(l, PlayerSelection());
		}
	}
	return result;
}
static void NumPlayersDraw(GameLoopData *data)
{
	MenuDraw(data->Data);
}

static void AssignPlayerInputDevices(EventHandlers *handlers)
{
	CA_FOREACH(PlayerData, p, gPlayerDatas)
	if (!p->IsLocal)
	{
		continue;
	}

	// Try to assign devices to players
	// For each unassigned player, check if any device has button 1 pressed
	// If so, and that input device wasn't used, assign it to that player
	for (int j = 0; j < MAX_KEYBOARD_CONFIGS; j++)
	{
		char buf[256];
		sprintf(buf, "Input.PlayerCodes%d.button1", j);
		if (KeyIsPressed(&handlers->keyboard, ConfigGetInt(&gConfig, buf)) &&
			PlayerTrySetUnusedInputDevice(p, INPUT_DEVICE_KEYBOARD, j))
		{
			MenuPlaySound(MENU_SOUND_START);
			break;
		}
	}
	for (int j = 0; j < (int)handlers->joysticks.size; j++)
	{
		const Joystick *joy = CArrayGet(&handlers->joysticks, j);
		if (JoyIsPressed(joy->id, CMD_BUTTON1) &&
			PlayerTrySetUnusedInputDevice(p, INPUT_DEVICE_JOYSTICK, joy->id))
		{
			MenuPlaySound(MENU_SOUND_START);
			break;
		}
	}
	CA_FOREACH_END()
}

typedef struct
{
	PlayerSelectMenu menus[MAX_LOCAL_PLAYERS];
	NameGen g;
	char prefixes[CDOGS_PATH_MAX];
	char suffixes[CDOGS_PATH_MAX];
	char suffixnames[CDOGS_PATH_MAX];
	EventWaitResult waitResult;
	int endCounter;
} PlayerSelectionData;
static void PlayerSelectionTerminate(GameLoopData *data);
static void PlayerSelectionOnExit(GameLoopData *data);
static GameLoopResult PlayerSelectionUpdate(GameLoopData *data, LoopRunner *l);
static void PlayerSelectionDraw(GameLoopData *data);
GameLoopData *PlayerSelection(void)
{
	PlayerSelectionData *data;
	CCALLOC(data, sizeof *data);
	data->waitResult = EVENT_WAIT_CONTINUE;
	GetDataFilePath(data->prefixes, "data/prefixes.txt");
	GetDataFilePath(data->suffixes, "data/suffixes.txt");
	GetDataFilePath(data->suffixnames, "data/suffixnames.txt");
	NameGenInit(&data->g, data->prefixes, data->suffixes, data->suffixnames);

	// Create selection menus for each local player
	for (int i = 0, idx = 0; i < (int)gPlayerDatas.size; i++, idx++)
	{
		PlayerData *p = CArrayGet(&gPlayerDatas, i);
		if (!p->IsLocal)
		{
			idx--;
			continue;
		}
		PlayerSelectMenusCreate(
			&data->menus[idx], GetNumPlayers(PLAYER_ANY, false, true), idx,
			p->UID, &gEventHandlers, &gGraphicsDevice, &data->g);
	}

	return GameLoopDataNew(
		data, PlayerSelectionTerminate, NULL, PlayerSelectionOnExit, NULL,
		PlayerSelectionUpdate, PlayerSelectionDraw);
}
static void PlayerSelectionTerminate(GameLoopData *data)
{
	PlayerSelectionData *pData = data->Data;

	for (int i = 0; i < GetNumPlayers(PLAYER_ANY, false, true); i++)
	{
		MenuSystemTerminate(&pData->menus[i].ms);
	}
	NameGenTerminate(&pData->g);
	CFREE(pData);
}
static void PlayerSelectionOnExit(GameLoopData *data)
{
	PlayerSelectionData *pData = data->Data;
	if (pData->waitResult == EVENT_WAIT_OK)
	{
		if (!gCampaign.IsClient)
		{
			gCampaign.MissionIndex = 0;
		}
		MapWolfN3DCheckAndLoadCustomQuiz(
			gCampaign.Setting.Title, &gPlayerDatas);
	}
	else
	{
		CampaignUnload(&gCampaign);
	}
}
static GameLoopResult PlayerSelectionUpdate(GameLoopData *data, LoopRunner *l)
{
	PlayerSelectionData *pData = data->Data;

	if (GetNumPlayers(PLAYER_ANY, false, true) == 0)
	{
		pData->waitResult = EVENT_WAIT_OK;
		LoopRunnerPop(l);
		return UPDATE_RESULT_OK;
	}

	// Check if anyone pressed escape
	int cmds[MAX_LOCAL_PLAYERS];
	memset(cmds, 0, sizeof cmds);
	GetPlayerCmds(&gEventHandlers, &cmds);
	if (EventIsEscape(
			&gEventHandlers, cmds, GetMenuCmd(&gEventHandlers, false)))
	{
		pData->waitResult = EVENT_WAIT_CANCEL;
		LoopRunnerPop(l);
		return UPDATE_RESULT_OK;
	}

	int idx = 0;
	if (pData->endCounter == 0)
	{
		// Menu input
		const int localHumanPlayers = GetNumPlayers(PLAYER_ANY, true, true);
		const bool useMenuCmd = (localHumanPlayers <= 1);
		for (int i = 0; i < (int)gPlayerDatas.size; i++, idx++)
		{
			const PlayerData *p = CArrayGet(&gPlayerDatas, i);
			if (!p->IsLocal)
			{
				idx--;
				continue;
			}
			if (p->inputDevice != INPUT_DEVICE_UNSET)
			{
				MenuSystem *ms = &pData->menus[idx].ms;
				if (ms->current->customPostUpdateFunc)
				{
					ms->current->customPostUpdateFunc(
						ms->current, ms->current->customPostUpdateData);
				}
				MenuUpdateMouse(ms);
				if (useMenuCmd)
				{
					cmds[idx] |=
						GetMenuCmd(&gEventHandlers, ms->current->mouseHover);
				}
				if (!MenuIsExit(ms) && cmds[idx])
				{
					MenuProcessCmd(ms, cmds[idx]);
				}
			}
		}
	}

	// Conditions for exit: at least one player has selected "Done",
	// and no other players, if any, are still selecting their player
	// The "players" with no input device are turned into AIs
	// If in demo mode, all players are AI
	bool hasAtLeastOneInput = gEventHandlers.DemoQuitTimer > 0;
	bool isDone = true;
	idx = 0;
	for (int i = 0; i < (int)gPlayerDatas.size; i++, idx++)
	{
		const PlayerData *p = CArrayGet(&gPlayerDatas, i);
		if (!p->IsLocal)
		{
			idx--;
			continue;
		}
		if (p->inputDevice != INPUT_DEVICE_UNSET)
		{
			hasAtLeastOneInput = true;
			if (strcmp(pData->menus[idx].ms.current->name, "Done") != 0)
			{
				isDone = false;
			}
		}
	}
	if (isDone && hasAtLeastOneInput)
	{
		// For any player slots not picked, turn them into AIs
		bool hasAIPlayers = false;
		idx = 0;
		for (int i = 0; i < (int)gPlayerDatas.size; i++, idx++)
		{
			PlayerData *p = CArrayGet(&gPlayerDatas, i);
			if (!p->IsLocal)
			{
				idx--;
				continue;
			}
			if (p->inputDevice == INPUT_DEVICE_UNSET ||
				p->inputDevice == INPUT_DEVICE_AI)
			{
				hasAIPlayers = true;
				PlayerTrySetInputDevice(p, INPUT_DEVICE_AI, 0);
				pData->menus[idx].ms.current =
					MenuGetSubmenuByName(pData->menus[idx].ms.root, "Done");
			}
		}
		if (hasAIPlayers && pData->endCounter < 70)
		{
			// If we have AI players, wait a bit before continuing
			pData->endCounter++;
			return UPDATE_RESULT_DRAW;
		}
		else
		{
			pData->waitResult = EVENT_WAIT_OK;
			if (!gCampaign.IsClient && CanLevelSelect(gCampaign.Entry.Mode))
			{
				LoopRunnerChange(l, LevelSelection(&gGraphicsDevice));
			}
			else
			{
				LoopRunnerChange(l, GameOptions(gCampaign.Entry.Mode));
			}
			return UPDATE_RESULT_OK;
		}
	}

	AssignPlayerInputDevices(&gEventHandlers);

	return UPDATE_RESULT_DRAW;
}
static void PlayerSelectionDraw(GameLoopData *data)
{
	const PlayerSelectionData *pData = data->Data;

	BlitClearBuf(&gGraphicsDevice);
	const int w = gGraphicsDevice.cachedConfig.Res.x;
	const int h = gGraphicsDevice.cachedConfig.Res.y;
	int idx = 0;
	for (int i = 0; i < (int)gPlayerDatas.size; i++, idx++)
	{
		const PlayerData *p = CArrayGet(&gPlayerDatas, i);
		if (!p->IsLocal)
		{
			idx--;
			continue;
		}
		if (p->inputDevice != INPUT_DEVICE_UNSET)
		{
			MenuDisplay(&pData->menus[idx].ms);
		}
		else
		{
			struct vec2i center = svec2i_zero();
			const char *prompt =
				"Press Fire to choose input device\nand join...";
			const struct vec2i offset =
				svec2i_scale_divide(FontStrSize(prompt), -2);
			switch (GetNumPlayers(false, false, true))
			{
			case 1:
				// Center of screen
				center = svec2i(w / 2, h / 2);
				break;
			case 2:
				// Side by side
				center = svec2i(idx * w / 2 + w / 4, h / 2);
				break;
			case 3:
			case 4:
				// Four corners
				center = svec2i(
					(idx & 1) * w / 2 + w / 4, (idx / 2) * h / 2 + h / 4);
				break;
			default:
				CASSERT(false, "not implemented");
				break;
			}
			FontStr(prompt, svec2i_add(center, offset));
		}
	}

	BlitUpdateFromBuf(&gGraphicsDevice, gGraphicsDevice.screen);
}

typedef struct
{
	MenuSystem ms;
	const CArray *weapons;
	CArray allowed; // of bool
} GameOptionsData;
static menu_t *MenuCreateAllowedWeapons(
	const char *name, GameOptionsData *data);
static void GameOptionsTerminate(GameLoopData *data);
static void GameOptionsOnEnter(GameLoopData *data);
static GameLoopResult GameOptionsUpdate(GameLoopData *data, LoopRunner *l);
static void GameOptionsDraw(GameLoopData *data);
GameLoopData *GameOptions(const GameMode gm)
{
	// Create selection menus
	const int w = gGraphicsDevice.cachedConfig.Res.x;
	const int h = gGraphicsDevice.cachedConfig.Res.y;
	GameOptionsData *data;
	CMALLOC(data, sizeof *data);
	MenuSystem *ms = &data->ms;
	data->weapons = &gMission.Weapons;
	CArrayInit(&data->allowed, sizeof(bool));
	for (int i = 0; i < (int)data->weapons->size; i++)
	{
		const bool f = true;
		CArrayPushBack(&data->allowed, &f);
	}
	MenuSystemInit(
		ms, &gEventHandlers, &gGraphicsDevice, svec2i_zero(), svec2i(w, h));
	ms->align = MENU_ALIGN_CENTER;
	ms->allowAborts = true;
	ms->root = MenuCreateNormal("", "", MENU_TYPE_OPTIONS, 0);
#define I(_config)                                                            \
	MenuAddConfigOptionsItem(ms->root, ConfigGet(&gConfig, _config));
	switch (gm)
	{
	case GAME_MODE_NORMAL:
		I("Game.Difficulty");
		I("Game.EnemyDensity");
		I("Game.NonPlayerHP");
		if (gCampaign.Setting.PlayerMaxHP == 0)
		{
			I("Game.PlayerHP");
		}
		if (gCampaign.Setting.Lives == 0)
		{
			I("Game.Lives");
		}
		I("Game.HealthPickups");
		I("Game.RandomSeed");
		MenuAddSubmenu(ms->root, MenuCreateSeparator(""));
		I("StartServer");
		break;
	case GAME_MODE_DOGFIGHT:
		I("Dogfight.PlayerHP");
		I("Dogfight.FirstTo");
		I("Game.HealthPickups");
		I("Game.RandomSeed");
		MenuAddSubmenu(ms->root, MenuCreateAllowedWeapons("Weapons...", data));
		MenuAddSubmenu(ms->root, MenuCreateSeparator(""));
		I("StartServer");
		break;
	case GAME_MODE_DEATHMATCH:
		if (gCampaign.Setting.PlayerMaxHP == 0)
		{
			I("Game.PlayerHP");
		}
		I("Deathmatch.Lives");
		I("Game.HealthPickups");
		I("Game.RandomSeed");
		MenuAddSubmenu(ms->root, MenuCreateAllowedWeapons("Weapons...", data));
		MenuAddSubmenu(ms->root, MenuCreateSeparator(""));
		I("StartServer");
		break;
	case GAME_MODE_QUICK_PLAY:
		I("QuickPlay.WallCount");
		I("QuickPlay.WallLength");
		I("QuickPlay.RoomCount");
		I("QuickPlay.SquareCount");
		I("QuickPlay.EnemyCount");
		I("QuickPlay.EnemySpeed");
		I("QuickPlay.EnemyHealth");
		I("QuickPlay.EnemiesWithExplosives");
		I("QuickPlay.ItemCount");
		// Force start server to be false - we don't support multiplayer with
		// this game mode
		ConfigGet(&gConfig, "StartServer")->u.Bool.Value = false;
		break;
	default:
		CASSERT(false, "unknown game mode");
		break;
	}
#undef I
	MenuAddSubmenu(ms->root, MenuCreateSeparator(""));
	MenuAddSubmenu(ms->root, MenuCreateReturn("Done", 0));
	MenuAddExitType(ms, MENU_TYPE_RETURN);

	return GameLoopDataNew(
		ms, GameOptionsTerminate, GameOptionsOnEnter, NULL, NULL,
		GameOptionsUpdate, GameOptionsDraw);
}
static void GameOptionsTerminate(GameLoopData *data)
{
	GameOptionsData *gData = data->Data;

	MenuSystemTerminate(&gData->ms);
	CArrayTerminate(&gData->allowed);
	CFREE(gData);
}
static void GameOptionsOnEnter(GameLoopData *data)
{
	GameOptionsData *gData = data->Data;

	if (!gCampaign.IsQuit)
	{
		MissionOptionsTerminate(&gMission);
		CampaignAndMissionSetup(&gCampaign, &gMission);
	}

	gData->ms.current = gData->ms.root;
	// Select "Done"
	gData->ms.root->u.normal.index =
		(int)gData->ms.root->u.normal.subMenus.size - 1;
}
static GameLoopResult GameOptionsUpdate(GameLoopData *data, LoopRunner *l)
{
	GameOptionsData *gData = data->Data;

	// Check end conditions:
	// - Campaign not loaded
	// - Campaign complete
	// - Mission quit
	// - No options needed
	// - Demo mode
	// - Menu complete
	const GameLoopResult result = MenuUpdate(&gData->ms);
	const bool isQuit = !gCampaign.IsLoaded || gCampaign.IsComplete ||
						gCampaign.IsQuit || gData->ms.hasAbort ||
						gMission.missionData == NULL;
	const bool isDone = !IsGameOptionsNeeded(gCampaign.Entry.Mode) ||
						result == UPDATE_RESULT_OK ||
						gEventHandlers.DemoQuitTimer > 0;
	if (isQuit || isDone)
	{
		if (isQuit)
		{
			MissionOptionsTerminate(&gMission);
			CampaignUnload(&gCampaign);
			LoopRunnerPop(l);
			return UPDATE_RESULT_OK;
		}
		if (!ConfigApply(&gConfig, NULL))
		{
			LOG(LM_MAIN, LL_ERROR,
				"Failed to apply config; reset to last used");
			ConfigResetChanged(&gConfig);
		}
		else
		{
			// Save options for later
			ConfigSave(&gConfig, GetConfigFilePath(CONFIG_FILE));
		}

		// Set allowed weapons
		// First check if the player has unwittingly disabled all weapons
		// if so, enable all weapons
		bool allDisabled = true;
		for (int i = 0, j = 0; i < (int)gData->allowed.size; i++, j++)
		{
			const bool *allowed = CArrayGet(&gData->allowed, i);
			if (*allowed)
			{
				allDisabled = false;
				break;
			}
		}
		if (!allDisabled)
		{
			for (int i = 0, j = 0; i < (int)gData->allowed.size; i++, j++)
			{
				const bool *allowed = CArrayGet(&gData->allowed, i);
				if (!*allowed)
				{
					CArrayDelete(&gMission.Weapons, j);
					j--;
				}
			}
		}

		gCampaign.OptionsSet = true;

		// If enabled, start net server
		if (!gCampaign.IsClient && ConfigGetBool(&gConfig, "StartServer"))
		{
			NetServerOpen(
				&gNetServer, (uint16_t)ConfigGetInt(&gConfig, "ListenPort"));
		}
		LoopRunnerPush(
			l, ScreenMissionBriefing(&gCampaign.Setting, &gMission));
		return UPDATE_RESULT_OK;
	}
	return UPDATE_RESULT_DRAW;
}
static void GameOptionsDraw(GameLoopData *data)
{
	const GameOptionsData *gData = data->Data;

	MenuDraw(&gData->ms);
}
static menu_t *MenuCreateAllowedWeapons(
	const char *name, GameOptionsData *data)
{
	// Create a menu to choose allowable weapons for this map
	// The weapons will be chosen from the available weapons
	menu_t *m =
		MenuCreateNormal(name, "Available Weapons:", MENU_TYPE_OPTIONS, 0);
	m->customPostInputData = data;
	for (int i = 0; i < (int)data->weapons->size; i++)
	{
		const WeaponClass **wc = CArrayGet(data->weapons, i);
		MenuAddSubmenu(
			m,
			MenuCreateOptionToggle((*wc)->name, CArrayGet(&data->allowed, i)));
	}
	MenuAddSubmenu(m, MenuCreateSeparator(""));
	MenuAddSubmenu(m, MenuCreateBack("Done"));
	return m;
}

static GameLoopResult CheckGameStart(void *data, LoopRunner *l);
GameLoopData *ScreenWaitForGameStart(void)
{
	return ScreenWait("Waiting for game start...", CheckGameStart, NULL);
}
static GameLoopResult CheckGameStart(void *data, LoopRunner *l)
{
	UNUSED(data);
	if (!gNetClient.Ready)
	{
		// Tell server we're ready
		NetClientSendMsg(&gNetClient, GAME_EVENT_CLIENT_READY, NULL);
	}
	gNetClient.Ready = true;
	if (!gCampaign.IsClient || gMission.HasStarted)
	{
		goto bail;
	}
	// Check disconnections
	if (!NetClientIsConnected(&gNetClient))
	{
		CampaignUnload(&gCampaign);
		goto bail;
	}
	return UPDATE_RESULT_DRAW;

bail:
	if (!gCampaign.IsLoaded)
	{
		LoopRunnerPop(l);
		return UPDATE_RESULT_OK;
	}
	LoopRunnerPush(
		l, ScreenLoading(
			   "Starting game...", true, RunGame(&gCampaign, &gMission, &gMap),
			   true));
	return UPDATE_RESULT_OK;
}
