// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: 021688ad5b11b079ca30151383cc4af67e79c4ac $
//
// Copyright (C) 1998-2006 by Randy Heit (ZDoom).
// Copyright (C) 2006-2026 by The Odamex Team.
//
// 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.
//
// DESCRIPTION:
//	Argument processing (?)
//
//-----------------------------------------------------------------------------


#include "odamex.h"

#include <sstream>
#include <algorithm>
#include <ctime>

#include "cmdlib.h"
#include "c_console.h"
#include "c_dispatch.h"
#include "m_argv.h"
#include "m_fileio.h"
#include "m_alloc.h"
#include "d_player.h"
#include "r_defs.h"
#include "i_system.h"

#include "hashtable.h"
#include "m_ostring.h"
#include "oscanner.h"

IMPLEMENT_CLASS (DConsoleCommand, DObject)
IMPLEMENT_CLASS (DConsoleAlias, DConsoleCommand)

EXTERN_CVAR (lookspring)

typedef std::map<std::string, DConsoleCommand *> command_map_t;
command_map_t &Commands()
{
	static command_map_t _Commands;
	return _Commands;
}

struct ActionBits actionbits[NUM_ACTIONS] =
{
	{ 0x00409, ACTION_USE,				"use" },
	{ 0x0074d, ACTION_BACK,				"back" },
	{ 0x007e4, ACTION_LEFT,				"left" },
	{ 0x00816, ACTION_JUMP,				"jump" },
	{ 0x0106d, ACTION_KLOOK,			"klook" },
	{ 0x0109d, ACTION_MLOOK,			"mlook" },
	{ 0x010d8, ACTION_RIGHT,			"right" },
	{ 0x0110a, ACTION_SPEED,			"speed" },
	{ 0x01fc5, ACTION_ATTACK,			"attack" },
	{ 0x021ae, ACTION_LOOKUP,			"lookup" },
	{ 0x021fe, ACTION_MOVEUP,			"moveup" },
	{ 0x02315, ACTION_STRAFE,			"strafe" },
	{ 0x041c4, ACTION_FORWARD,			"forward" },
	{ 0x07cfa, ACTION_AUTOMAP_PANUP,	"am_panup" },
	{ 0x08126, ACTION_FASTTURN,   		"fastturn"},
    { 0x08788, ACTION_LOOKDOWN,			"lookdown"},
	{ 0x088c4, ACTION_MOVELEFT,			"moveleft" },
	{ 0x088c8, ACTION_MOVEDOWN,			"movedown" },
	{ 0x0fc5c, ACTION_AUTOMAP_ZOOMIN,	"am_zoomin" },
	{ 0x11268, ACTION_MOVERIGHT,		"moveright" },
	{ 0x1f4b4, ACTION_AUTOMAP_PANLEFT,	"am_panleft" },
	{ 0x1f4b8, ACTION_AUTOMAP_PANDOWN,	"am_pandown" },
	{ 0x1f952, ACTION_AUTOMAP_ZOOMOUT,	"am_zoomout" },
	{ 0x2314d, ACTION_SHOWSCORES,		"showscores" },
	{ 0x3ea48, ACTION_AUTOMAP_PANRIGHT, "am_panright" },
};
byte Actions[NUM_ACTIONS];


class ActionKeyTracker
{
public:

	ActionKeyTracker() :
		mTable(NUM_ACTIONS)
	{ }

	void clear()
	{
		mTable.clear();
	}

	bool isActionActivated(const OString action)
	{
		ActionKeyListTable::iterator it = mTable.find(action);
		if (it == mTable.end())
			return false;
		ActionKeyList* action_key_list = &it->second;
		return !action_key_list->empty();
	}

	bool pressKey(uint32_t key, const OString action)
	{
		ActionKeyListTable::iterator it = mTable.find(action);
		if (it == mTable.end())
			it = mTable.emplace(action, ActionKeyList()).first;
		ActionKeyList* action_key_list = &it->second;

		if (std::find(action_key_list->begin(), action_key_list->end(), key) != action_key_list->end())
			return false;

		action_key_list->push_back(key);
		return true;
	}

	bool releaseKey(uint32_t key, const OString action)
	{
		ActionKeyListTable::iterator it = mTable.find(action);
		if (it == mTable.end())
			return false;

		ActionKeyList* action_key_list = &it->second;
		action_key_list->remove(key);
		return action_key_list->empty();
	}

private:
	typedef std::list<uint32_t> ActionKeyList;

	typedef OHashTable<OString, ActionKeyList> ActionKeyListTable;
	ActionKeyListTable mTable;
};

static ActionKeyTracker action_key_tracker;


static int ListActionCommands (void)
{
	int i;

	for (i = 0; i < NUM_ACTIONS; i++)
	{
		PrintFmt(PRINT_HIGH, "+{}\n", actionbits[i].name);
		PrintFmt(PRINT_HIGH, "-{}\n", actionbits[i].name);
	}
	return NUM_ACTIONS * 2;
}

unsigned int MakeKey (const char *s)
{
	unsigned int v = 0;

	if (*s)
		v = tolower(*s++);
	if (*s)
		v = (v*3) + tolower(*s++);
	while (*s)
		v = (v << 1) + tolower(*s++);

	return v;
}

// GetActionBit scans through the actionbits[] array
// for a matching key and returns an index or -1 if
// the key could not be found. This uses binary search,
// actionbits[] must be sorted in ascending order.

int GetActionBit (unsigned int key)
{
	int min = 0;
	int max = NUM_ACTIONS - 1;

	while (min <= max)
	{
		int mid = (min + max) / 2;
		unsigned int seekey = actionbits[mid].key;

		if (seekey == key)
			return actionbits[mid].index;
		else if (seekey < key)
			min = mid + 1;
		else
			max = mid - 1;
	}

	return -1;
}

bool safemode = false;


void C_DoCommand(std::string_view cmd, uint32_t key)
{
	auto parser = ParseString(cmd, true);

	auto [token, rest] = parser();
	if (!token)
		return;

	// Check if this is an action
	if (token->at(0) == '+' || token->at(0) == '-')
	{
		OString action(token->substr(1));
		int check = GetActionBit(MakeKey(action.c_str()));

		if (token->at(0) == '+')
		{
			if (action_key_tracker.pressKey(key, action))
			{
				if (check != -1)
					Actions[check] = 1;
			}
		}
		else if (token->at(0) == '-')
		{
			if (action_key_tracker.releaseKey(key, action))
			{
				if (check != -1)
					Actions[check] = 0;

				if ((check == ACTION_LOOKDOWN || check == ACTION_LOOKUP || check == ACTION_MLOOK) && lookspring)
					AddCommandString("centerview");
			}
		}
		if (check != -1)
			return;
	}

	size_t argc = 0;
	std::vector<char*> argv;
	StringTokens args;
	// for rcon
	const char *realargs = rest.data();
	parser = ParseString(cmd, true);

	while ((token = parser().token))
	{
		args.push_back(*token);
		argc++;
	}

	for (std::string& arg : args)
		argv.push_back(arg.data());

	// Checking for matching commands follows this search order:
	//	1. Check the Commands map
	//	2. Check the CVars list
	command_map_t::iterator c = Commands().find(StdStringToLower(argv[0]));
	DConsoleCommand* com;

	if (c != Commands().end())
	{
		com = c->second;

		if (!safemode || stricmp(argv[0], "if") == 0 || stricmp(argv[0], "exec") == 0)
		{
			com->argc = argc;
			com->argv = argv.data();
			com->args = realargs;
			com->m_Instigator = consoleplayer().mo;
			com->Run(key);
		}
		else
		{
			PrintFmt(PRINT_HIGH, "Not a cvar command \"{}\"\n", argv[0]);
		}
	}
	else
	{
		// Check for any CVars that match the command
		cvar_t *var, *dummy;

		if ((var = cvar_t::FindCVar(argv[0], &dummy)))
		{
			if (argc >= 2)
			{
				c = Commands().find("set");
				if (c != Commands().end())
				{
					com = c->second;
					com->argc = argc + 1;
					com->argv = argv.data() - 1; // Hack
					com->m_Instigator = consoleplayer().mo;
					com->Run(key);
				}
				else
					PrintFmt(PRINT_HIGH, "set command not found\n");
			}
			else
			{
				c = Commands().find("get");
				if (c != Commands().end())
				{
					com = c->second;
					com->argc = argc + 1;
					com->argv = argv.data() - 1; // Hack
					com->m_Instigator = consoleplayer().mo;
					com->Run();
				}
				else
					PrintFmt(PRINT_WARNING, "get command not found\n");
			}
		}
		else
		{
			// We don't know how to handle this command
			PrintFmt(PRINT_WARNING, "Unknown command \"{}\"\n", argv[0]);
		}
	}
}

void AddCommandString(const std::string &str, uint32_t key)
{
	size_t totallen = str.length();
	if (!totallen)
		return;

	// pointers to the start and end of the current substring in str.c_str()
	const char* cstart = str.c_str();
	const char* cend;

	// stores a copy of the current substring
	auto command = std::make_unique<char[]>(totallen + 1);

	// scan for a command ending
	while (*cstart)
	{
		const char* cp = cstart;

		// read until the next command (separated by semicolon) or until comment (two slashes)
		while (*cp != ';' && !(cp[0] == '/' && cp[1] == '/') && *cp != 0)
		{
			if (cp[0] == '\\' && cp[1] != 0)
			{
				// [AM] Skip two chars if escaped.
				cp += 2;
			}
			else if (*cp == '"')
			{
				// Ignore ';' if it is inside a pair of quotes.
				while (1)
				{
					cp++;
					if (*cp == 0)
					{
						// End of string.
						break;
					}
					if (cp[0] == '\\' && cp[1] == '"')
					{
						// [AM] Skip over escaped quote.
						cp++;
					}
					else if (*cp == '"')
					{
						// End of quote.  Skip over ending quote.
						cp++;
						break;
					}
				}
			}
			else
			{
				// Advance to next char.
				cp++;
			}
		}

		cend = cp - 1;

		// remove leading and trailing whitespace
		while (cstart < cend && *cstart == ' ')
			cstart++;
		while (cend > cstart && *cend == ' ')
			cend--;

		size_t clength = cend - cstart + 1;
		memcpy(command.get(), cstart, clength);
		command[clength] = '\0';

		C_DoCommand(command.get(), key);

		// don't parse anymore if there's a comment
		if (cp[0] == '/' && cp[1] == '/')
			break;

		// are there more commands following this one?
		if (*cp == ';')
			cstart = cp + 1;
		else
			cstart = cp;
	}
}

#define MAX_EXEC_DEPTH 32

static bool if_command_result;

BEGIN_COMMAND (exec)
{
	if (argc < 2)
		return;

	static std::vector<std::string> exec_stack;
	static std::vector<bool>	tag_stack;

	// TODO: clean this up into a clearer ordering of search directories
	// CWD, M_FindUserFileName, -cfgdir, ODAMEX_INSTALL_DATADIR, M_GetBinaryDir
	// the last two are so that config-sample can always be easily exec'd
	// not sure where exactly -cfgdir should be in the priority, it could be argued
	// that it should be above user directory and/or CWD
	std::string found = M_FileExists(argv[1]) ?
		M_CleanPath(argv[1]) : M_FindUserFileName(argv[1], ".cfg");
	if (found.empty())
	{
		const char* cfgdir = Args.CheckValue("-cfgdir");
		if (!cfgdir)
		{
			PrintFmt(PRINT_WARNING, "Could not find \"{}\"\n", argv[1]);
			return;
		}

		found = M_CleanPath(M_JoinPath(cfgdir, argv[1]));
		if (!M_FileExists(found))
		{
			found += ".cfg";
			if (!M_FileExists(found))
			{
				PrintFmt(PRINT_WARNING, "Could not find \"{}\"\n", argv[1]);
				return;
			}
		}
	}

	if(std::find(exec_stack.begin(), exec_stack.end(), found) != exec_stack.end())
	{
		PrintFmt(PRINT_HIGH, "Ignoring recursive exec \"{}\"\n", found);
		return;
	}

	if(exec_stack.size() >= MAX_EXEC_DEPTH)
	{
		PrintFmt(PRINT_HIGH, "Ignoring recursive exec \"{}\"\n", found);
		return;
	}

	std::ifstream ifs(found);
	if(ifs.fail())
	{
		PrintFmt(PRINT_WARNING, "Could not open \"{}\"\n", found);
		return;
	}

	exec_stack.push_back(found);

	while(ifs)
	{
		std::string line;
		std::getline(ifs, line);
		line = TrimString(line);

		if (line.empty())
			continue;

		// start tag
		if(line.substr(0, 3) == "#if")
		{
			AddCommandString(line.c_str() + 1);
			tag_stack.push_back(if_command_result);

			continue;
		}

		// else tag
		if(line.substr(0, 5) == "#else")
		{
			if(tag_stack.empty())
				PrintFmt(PRINT_HIGH, "Ignoring stray #else\n");
			else
				tag_stack.back() = !tag_stack.back();

			continue;
		}

		// end tag
		if(line.substr(0, 6) == "#endif")
		{
			if(tag_stack.empty())
				PrintFmt(PRINT_HIGH, "Ignoring stray #endif\n");
			else
				tag_stack.pop_back();

			continue;
		}

		// inside tag that evaluated false?
		if(!tag_stack.empty() && !tag_stack.back())
			continue;

		AddCommandString(line);
	}

	exec_stack.pop_back();
}
END_COMMAND (exec)

// denis
// if cvar eq blah "command";
BEGIN_COMMAND (if)
{
	if_command_result = false;

	if (argc < 4)
		return;

	cvar_t *var, *dummy;
	var = cvar_t::FindCVar (argv[1], &dummy);

	if (!var)
	{
		PrintFmt(PRINT_HIGH, "if: no cvar named {}\n", argv[1]);
		return;
	}

	std::string op = argv[2];

	if(op == "eq")
	{
		if_command_result = !strcmp(var->cstring(), argv[3]);
	}
	else if(op == "ne")
	{
		if_command_result = ((strcmp(var->cstring(), argv[3])) != 0);
	}
	else
	{
		PrintFmt(PRINT_HIGH, "if: no operator {}\n", argv[2]);
		PrintFmt(PRINT_HIGH, "if: operators are eq, ne\n");
		return;
	}

	if(if_command_result && argc > 4)
	{
		std::string param = C_ArgCombine(argc - 4, (const char **)&argv[4]);
		AddCommandString(param);
	}
}
END_COMMAND (if)

// Returns true if the character is a valid escape char, false otherwise.
bool ValidEscape(char data)
{
	return (data == '"' || data == ';' || data == '\\');
}

// ParseString is adapted from COM_Parse
// found in the Quake2 source distribution
// If expandVars == true, any token of
// the form $<cvar>, will be replaced by the
// contents of <cvar>.
std::function<parse_string_result_t()> ParseString(std::string_view data, bool expandVars)
{
	auto base = [data]() mutable -> parse_string_result_t {
		std::string token;

		while (!data.empty() && data[0] <= ' ')
			data.remove_prefix(1);

		if (data.empty())
			return {std::nullopt, data};

		// Ch0wW : If having a comment, break immediately the line!
		if (data.length() >= 2 && data[0] == '/' && data[1] == '/') {
			return {std::nullopt, data};
		}

		if (data.length() >= 2 && data[0] == '\\' && ValidEscape(data[1]))
		{
			// [AM] Handle escaped chars.
			token += data[1];
			data.remove_prefix(2);
		}
		else if (data[0] == '"')
		{
			// Quoted strings count as one large token.
			while (true) {
				data.remove_prefix(1);
				if (data.empty())
				{
					// [AM] Unclosed quote, show no mercy.
					return {std::nullopt, data};
				}
				if (data.length() >= 2 && data[0] == '\\' && ValidEscape(data[1]))
				{
					// [AM] Handle escaped chars.
					token += data[1];
					data.remove_prefix(1); // Skip one _additional_ char.
					continue;
				}
				else if (data[0] == '"')
				{
					// Closing quote, that's the entire token.
					data.remove_prefix(1); // Skip the closing quote.
					return {std::optional(token), data};
				}
				// None of the above, copy the char and continue.
				token += data[0];
			}
		}

		while (true) {
			// Parse a regular word.
			if (data.empty() || data[0] <= ' ')
			{
				// End of word.
				break;
			}
			if (data.length() >= 2 && data[0] == '\\' && ValidEscape(data[1]))
			{
				// [AM] Handle escaped chars.
				token += data[1];
				data.remove_prefix(2); // Skip two chars.
				continue;
			}
			else if (data[0] == '"')
			{
				// End of word.
				break;
			}
			// None of the above, copy the char and continue.
			token += data[0];
			data.remove_prefix(1);
		}
		// We're done
		// return the remaining data to parse.
		return {std::optional(token), data};
	};

	if (!expandVars)
		return base;

	return [base = std::move(base)]() mutable -> parse_string_result_t {
		auto result = base();
		if (!result || result.token->empty() || result.token->at(0) != '$')
			return result;

		cvar_t *dummy;
		if (const cvar_t* var = cvar_t::FindCVar(std::string_view(*result.token).substr(1), &dummy))
			return {std::optional(var->str()), result.rest};

		return result;
	};
}

DConsoleCommand::DConsoleCommand (const char *name)
{
	static bool firstTime = true;

	if (firstTime)
	{
		firstTime = false;

		// Add all the action commands for tab completion
		for (const auto& bit : actionbits)
		{
			std::string tname = fmt::format("+{}", bit.name);
			C_AddTabCommand(tname.c_str());
			tname[0] = '-';
			C_AddTabCommand(tname.c_str());
		}
	}

	m_Name = name;

	Commands()[name] = this;
	C_AddTabCommand(name);
}

DConsoleCommand::~DConsoleCommand ()
{
	C_RemoveTabCommand (m_Name.c_str());
}

DConsoleAlias::DConsoleAlias (const char *name, const char *command)
	:	DConsoleCommand(StdStringToLower(name).c_str()),  state_lock(false),
		m_Command(command)
{
}

DConsoleAlias::~DConsoleAlias ()
{
}

void DConsoleAlias::Run(uint32_t key)
{
	if(!state_lock)
	{
		state_lock = true;

        m_CommandParam = m_Command;

		// [Russell] - Allows for aliases with parameters
		if (argc > 1)
        {
            for (size_t i = 1; i < argc; i++)
            {
                m_CommandParam += " ";
                m_CommandParam += argv[i];
            }
        }

        AddCommandString(m_CommandParam, key);

		state_lock = false;
	}
	else
	{
		PrintFmt(PRINT_HIGH, "warning: ignored recursive alias");
	}
}

// [AM] Combine many arguments into one valid argument.  Since this
//      function is called after we parse arguments, we don't need to
//      escape the output.
std::string C_ArgCombine(size_t argc, const char **argv)
{
	std::ostringstream buffer;
	for (size_t i = 0;i < argc;i++)
	{
		buffer << argv[i];
		if (i + 1 < argc)
			buffer << " ";
	}
	return buffer.str();
}

std::string BuildString (size_t argc, std::vector<std::string> args)
{
	std::string out;

	for(size_t i = 0; i < argc; i++)
	{
		if(args[i].find_first_of(' ') != std::string::npos)
		{
			out += "\"";
			out += args[i];
			out += "\"";
		}
		else
		{
			out += args[i];
		}

		if(i + 1 < argc)
		{
			out += " ";
		}
	}

	return out;
}

// [AM] Take a string, quote it, and escape it, making it suitable for parsing
//      as an argument.
std::string C_QuoteString(const std::string &argstr)
{
	std::ostringstream buffer;
	buffer << "\"";
	for (const auto c : argstr)
	{
		if (ValidEscape(c))
		{
			// Escape this char.
			buffer << '\\' << c;
		}
		else
		{
			buffer << c;
		}
	}
	buffer << "\"";
	return buffer.str();
}

// Take a string of inputted WADs and escape them indvidually
// and add a space before loading them into the system.
std::string C_EscapeWadList(const std::vector<std::string> wadlist)
{
	std::string wadstr;
	for (size_t i = 0; i < wadlist.size(); i++)
	{
		if (i != 0)
		{
			wadstr += " ";
		}
		wadstr += C_QuoteString(wadlist.at(i));
	}
	return wadstr;
}

static int DumpHash (bool aliases)
{
	int count = 0;

	for (const auto& [_, cmd] : Commands())
	{
		count++;
		if (cmd->IsAlias())
		{
			if (aliases)
				static_cast<DConsoleAlias *>(cmd)->PrintAlias ();
		}
		else if (!aliases)
			cmd->PrintCommand ();
	}

	return count;
}

void DConsoleAlias::Archive(FILE *f)
{
	fmt::print(f, "alias {} {}\n", C_QuoteString(m_Name), C_QuoteString(m_Command));
}

void DConsoleAlias::C_ArchiveAliases (FILE *f)
{
	for (const auto& [_, alias] : Commands())
	{
		if (alias->IsAlias())
			static_cast<DConsoleAlias *>(alias)->Archive (f);
	}
}

void DConsoleAlias::DestroyAll()
{
	for (const auto& [_, alias] : Commands())
	{
		if (alias->IsAlias())
			delete alias;
	}
}

BEGIN_COMMAND (alias)
{
	if (argc == 1)
	{
		PrintFmt(PRINT_HIGH, "Current alias commands:\n");
		DumpHash(true);
	}
	else
	{
		command_map_t::iterator i = Commands().find(StdStringToLower(argv[1]));

		if(i != Commands().end())
		{
			if(i->second->IsAlias())
			{
				// Remove the old alias
				delete i->second;
				Commands().erase(i);
			}
			else
			{
				PrintFmt(PRINT_HIGH, "{}: is a command, can not become an alias\n", argv[1]);
				return;
			}
		}
		else if(argc == 2)
		{
			PrintFmt(PRINT_HIGH, "{}: not an alias\n", argv[1]);
			return;
		}

		if(argc > 2)
		{
			// Build the new alias
			std::string param = C_ArgCombine(argc - 2, (const char **)&argv[2]);
			new DConsoleAlias (argv[1], param.c_str());
		}
	}
}
END_COMMAND (alias)

BEGIN_COMMAND (cmdlist)
{
	int count;

	count = ListActionCommands();
	count += DumpHash(false);
	PrintFmt(PRINT_HIGH, "{} commands\n", count);
}
END_COMMAND (cmdlist)


// Execute any console commands specified on the command line.
// These all begin with '+' as opposed to '-'.
// If onlyset is true, only "set" commands will be executed,
// otherwise only non-"set" commands are executed.
// If onlylogfile is true... well, you get the point.
void C_ExecCmdLineParams (bool onlyset, bool onlylogfile)
{
	size_t cmdlen, argstart;
	int didlogfile = 0;

	for (size_t currArg = 1; currArg < Args.NumArgs(); )
	{
		if (*Args.GetArg (currArg++) == '+')
		{
			int setComp = stricmp (Args.GetArg (currArg - 1) + 1, "set");
			int logfileComp = stricmp (Args.GetArg (currArg - 1) + 1, "logfile");
			if ((onlyset && setComp) || (onlylogfile && logfileComp) ||
                (!onlyset && !setComp) || (!onlylogfile && !logfileComp))
			{
				continue;
			}

			cmdlen = 1;
			argstart = currArg - 1;

			while (currArg < Args.NumArgs())
			{
				if (*Args.GetArg (currArg) == '-' || *Args.GetArg (currArg) == '+')
					break;
				currArg++;
				cmdlen++;
			}

			std::string cmdString = BuildString (cmdlen, Args.GetArgList(argstart));
			if (cmdString.length()) {
				C_DoCommand(std::string_view(cmdString).substr(1), 0);
				if (onlylogfile) didlogfile = 1;
			}
		}
	}

    // [Nes] - Calls version at startup if no logfile.
	if (onlylogfile && !didlogfile) AddCommandString("version");
}

BEGIN_COMMAND (actorlist)
{
	AActor *mo;
	TThinkerIterator<AActor> iterator;
	PrintFmt(PRINT_HIGH, "Actors at level.time == {}:\n", level.time);
	while ( (mo = iterator.Next ()) )
	{
		PrintFmt(PRINT_HIGH, "{} ({:x}, {:x}, {:x} | {:x}) state: {} tics: {} tid: {}\n", mobjinfo[mo->type].name,
			static_cast<uint32_t>(mo->x), static_cast<uint32_t>(mo->y), static_cast<uint32_t>(mo->z),
			static_cast<uint32_t>(mo->angle), mo->state->statenum, mo->tics, mo->tid);
	}
}
END_COMMAND(actorlist)

BEGIN_COMMAND(logfile)
{
	time_t rawtime;
	struct tm* timeinfo;
	const std::string default_logname =
	    M_GetUserFileName(::serverside ? "odasrv.log" : "odamex.log");

	if (::LOG.is_open())
	{
		if ((argc == 1 && ::LOG_FILE == default_logname) ||
		    (argc > 1 && ::LOG_FILE == argv[1]))
		{
			PrintFmt("Log file {} already in use\n", ::LOG_FILE.c_str());
			return;
		}

		time(&rawtime);
		timeinfo = localtime(&rawtime);
		PrintFmt("Log file {} closed on {}\n", ::LOG_FILE, asctime(timeinfo));
		::LOG.close();
	}

	::LOG_FILE = (argc > 1 ? argv[1] : default_logname);
	::LOG.open(::LOG_FILE.c_str(), std::ios::app);

	if (!::LOG.is_open())
	{
		PrintFmt(PRINT_HIGH, "Unable to create logfile: {}\n", ::LOG_FILE);
	}
	else
	{
		time(&rawtime);
		timeinfo = localtime(&rawtime);
		::LOG.flush();
		::LOG << std::endl;
		PrintFmt(PRINT_HIGH, "Logging in file {} started {}\n", ::LOG_FILE,
		         asctime(timeinfo));
	}
}
END_COMMAND(logfile)

BEGIN_COMMAND (stoplog)
{
	time_t rawtime;
	struct tm * timeinfo;

	if (LOG.is_open()) {
		time (&rawtime);
    	timeinfo = localtime (&rawtime);
		PrintFmt(PRINT_HIGH, "Logging to file {} stopped {}\n", LOG_FILE, asctime (timeinfo));
		LOG.close();
	}
}
END_COMMAND (stoplog)

bool P_StartScript (AActor *who, line_t *where, int script, const char *map, int lineSide,
					int arg0, int arg1, int arg2, int always);

BEGIN_COMMAND (puke)
{
	if (argc < 2 || argc > 5) {
		PrintFmt(PRINT_HIGH, " puke <script> [arg1] [arg2] [arg3]\n");
	} else {
		int script = atoi (argv[1]);
		int arg0=0, arg1=0, arg2=0;

		if (argc > 2) {
			arg0 = atoi (argv[2]);
			if (argc > 3) {
				arg1 = atoi (argv[3]);
				if (argc > 4) {
					arg2 = atoi (argv[4]);
				}
			}
		}
		P_StartScript (m_Instigator, NULL, script, level.mapname.c_str(), 0, arg0, arg1, arg2, false);
	}
}
END_COMMAND (puke)

BEGIN_COMMAND (error)
{
	std::string text = C_ArgCombine(argc - 1, (const char **)(argv + 1));
	I_Error ("{}", text);
}
END_COMMAND (error)

VERSION_CONTROL (c_dispatch_cpp, "$Id: 021688ad5b11b079ca30151383cc4af67e79c4ac $")
