603 lines
17 KiB
C++
603 lines
17 KiB
C++
/*
|
|
This file is part of Warzone 2100.
|
|
Copyright (C) 1999-2004 Eidos Interactive
|
|
Copyright (C) 2005-2013 Warzone 2100 Project
|
|
|
|
Warzone 2100 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.
|
|
|
|
Warzone 2100 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 Warzone 2100; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
/*
|
|
* clParse.c
|
|
*
|
|
* Parse command line arguments
|
|
*
|
|
*/
|
|
|
|
#include "lib/framework/frame.h"
|
|
#include "lib/framework/opengl.h"
|
|
#include "lib/ivis_opengl/screen.h"
|
|
#include "lib/netplay/netplay.h"
|
|
|
|
#include "clparse.h"
|
|
#include "display3d.h"
|
|
#include "frontend.h"
|
|
#include "keybind.h"
|
|
#include "loadsave.h"
|
|
#include "main.h"
|
|
#include "multiplay.h"
|
|
#include "version.h"
|
|
#include "warzoneconfig.h"
|
|
#include "wrappers.h"
|
|
|
|
//////
|
|
// Our fine replacement for the popt abomination follows
|
|
|
|
#define POPT_ARG_STRING true
|
|
#define POPT_ARG_NONE false
|
|
#define POPT_ERROR_BADOPT -1
|
|
#define POPT_SKIP_MAC_PSN 666
|
|
|
|
struct poptOption
|
|
{
|
|
const char *string;
|
|
char short_form;
|
|
bool argument;
|
|
void *nullpointer; // unused
|
|
int enumeration;
|
|
const char *descrip;
|
|
const char *argDescrip;
|
|
};
|
|
|
|
typedef struct _poptContext
|
|
{
|
|
int argc, current, size;
|
|
const char **argv;
|
|
const char *parameter;
|
|
const char *bad;
|
|
const struct poptOption *table;
|
|
} *poptContext;
|
|
|
|
/// TODO: Find a way to use the real qFatal from Qt
|
|
#define qFatal(...) { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); }
|
|
|
|
static void poptPrintHelp(poptContext ctx, FILE *output, WZ_DECL_UNUSED int unused)
|
|
{
|
|
int i;
|
|
|
|
fprintf(output, "Usage: %s [OPTION...]\n", ctx->argv[0]);
|
|
for (i = 0; i < ctx->size; i++)
|
|
{
|
|
char txt[128];
|
|
|
|
if (ctx->table[i].short_form != '\0')
|
|
{
|
|
ssprintf(txt, " -%c, --%s", ctx->table[i].short_form, ctx->table[i].string);
|
|
}
|
|
else
|
|
{
|
|
ssprintf(txt, " --%s", ctx->table[i].string);
|
|
}
|
|
|
|
if (ctx->table[i].argument)
|
|
{
|
|
sstrcat(txt, "=");
|
|
sstrcat(txt, ctx->table[i].argDescrip);
|
|
}
|
|
|
|
fprintf(output, "%-40s", txt);
|
|
if (ctx->table[i].descrip)
|
|
{
|
|
fprintf(output, "%s", ctx->table[i].descrip);
|
|
}
|
|
fprintf(output, "\n");
|
|
}
|
|
}
|
|
|
|
static const char *poptBadOption(poptContext ctx, WZ_DECL_UNUSED int unused)
|
|
{
|
|
return ctx->bad;
|
|
}
|
|
|
|
static const char *poptGetOptArg(poptContext ctx)
|
|
{
|
|
return ctx->parameter;
|
|
}
|
|
|
|
static int poptGetNextOpt(poptContext ctx)
|
|
{
|
|
static char match[PATH_MAX]; // static for bad function
|
|
static char parameter[PATH_MAX]; // static for arg function
|
|
char *pparam;
|
|
int i;
|
|
|
|
ctx->bad = NULL;
|
|
ctx->parameter = NULL;
|
|
parameter[0] = '\0';
|
|
match[0] = '\0';
|
|
|
|
if (ctx->current >= ctx->argc) // counts from 1
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (strstr(ctx->argv[ctx->current], "-psn_"))
|
|
{
|
|
ctx->current++; // skip mac -psn_* Yum!
|
|
return POPT_SKIP_MAC_PSN;
|
|
}
|
|
|
|
sstrcpy(match, ctx->argv[ctx->current]);
|
|
ctx->current++;
|
|
pparam = strrchr(match, '=');
|
|
if (pparam) // option's got a parameter
|
|
{
|
|
*pparam++ = '\0'; // split option from parameter and increment past '='
|
|
if (pparam[0] == '"') // found scary quotes
|
|
{
|
|
pparam++; // skip start quote
|
|
sstrcpy(parameter, pparam); // copy first parameter
|
|
if (!strrchr(pparam, '"')) // if no end quote, then find it
|
|
{
|
|
while (!strrchr(parameter, '"') && ctx->current < ctx->argc)
|
|
{
|
|
sstrcat(parameter, " "); // insert space
|
|
sstrcat(parameter, ctx->argv[ctx->current]);
|
|
ctx->current++; // next part, please!
|
|
}
|
|
}
|
|
if (strrchr(parameter, '"')) // its not an else for above!
|
|
{
|
|
*strrchr(parameter, '"') = '\0'; // remove end qoute
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sstrcpy(parameter, pparam); // copy parameter
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < ctx->size; i++)
|
|
{
|
|
char sshort[3];
|
|
char slong[64];
|
|
|
|
ssprintf(sshort, "-%c", ctx->table[i].short_form);
|
|
ssprintf(slong, "--%s", ctx->table[i].string);
|
|
if ((strcmp(sshort, match) == 0 && ctx->table[i].short_form != '\0') || strcmp(slong, match) == 0)
|
|
{
|
|
if (ctx->table[i].argument && pparam)
|
|
{
|
|
ctx->parameter = parameter;
|
|
}
|
|
return ctx->table[i].enumeration;
|
|
}
|
|
}
|
|
ctx->bad = match;
|
|
ctx->current++;
|
|
return POPT_ERROR_BADOPT;
|
|
}
|
|
|
|
static poptContext poptGetContext(WZ_DECL_UNUSED void *unused, int argc, const char **argv, const struct poptOption *table, WZ_DECL_UNUSED int none)
|
|
{
|
|
static struct _poptContext ctx;
|
|
|
|
ctx.argc = argc;
|
|
ctx.argv = argv;
|
|
ctx.table = table;
|
|
ctx.current = 1;
|
|
ctx.parameter = NULL;
|
|
|
|
for (ctx.size = 0; table[ctx.size].string; ctx.size++) ; // count table size
|
|
|
|
return &ctx;
|
|
}
|
|
|
|
|
|
typedef enum
|
|
{
|
|
// We don't want to use zero, so start at one (1)
|
|
CLI_CONFIGDIR = 1,
|
|
CLI_DATADIR,
|
|
CLI_DEBUG,
|
|
CLI_DEBUGFILE,
|
|
CLI_FLUSHDEBUGSTDERR,
|
|
CLI_FULLSCREEN,
|
|
CLI_GAME,
|
|
CLI_HELP,
|
|
CLI_MOD_GLOB,
|
|
CLI_MOD_CA,
|
|
CLI_MOD_MP,
|
|
CLI_SAVEGAME,
|
|
CLI_WINDOW,
|
|
CLI_VERSION,
|
|
CLI_RESOLUTION,
|
|
CLI_SHADOWS,
|
|
CLI_NOSHADOWS,
|
|
CLI_SOUND,
|
|
CLI_NOSOUND,
|
|
CLI_CONNECTTOIP,
|
|
CLI_HOSTLAUNCH,
|
|
CLI_NOASSERT,
|
|
CLI_CRASH,
|
|
CLI_TEXTURECOMPRESSION,
|
|
CLI_NOTEXTURECOMPRESSION,
|
|
} CLI_OPTIONS;
|
|
|
|
static const struct poptOption* getOptionsTable(void)
|
|
{
|
|
static const struct poptOption optionsTable[] =
|
|
{
|
|
{ "configdir", '\0', POPT_ARG_STRING, NULL, CLI_CONFIGDIR, N_("Set configuration directory"), N_("configuration directory") },
|
|
{ "datadir", '\0', POPT_ARG_STRING, NULL, CLI_DATADIR, N_("Set default data directory"), N_("data directory") },
|
|
{ "debug", '\0', POPT_ARG_STRING, NULL, CLI_DEBUG, N_("Show debug for given level"), N_("debug level") },
|
|
{ "debugfile", '\0', POPT_ARG_STRING, NULL, CLI_DEBUGFILE, N_("Log debug output to file"), N_("file") },
|
|
{ "flush-debug-stderr", '\0', POPT_ARG_NONE, NULL, CLI_FLUSHDEBUGSTDERR, N_("Flush all debug output written to stderr"), NULL },
|
|
{ "fullscreen", '\0', POPT_ARG_NONE, NULL, CLI_FULLSCREEN, N_("Play in fullscreen mode"), NULL },
|
|
{ "game", '\0', POPT_ARG_STRING, NULL, CLI_GAME, N_("Load a specific game"), N_("game-name") },
|
|
{ "help", 'h', POPT_ARG_NONE, NULL, CLI_HELP, N_("Show this help message and exit"), NULL },
|
|
{ "mod", '\0', POPT_ARG_STRING, NULL, CLI_MOD_GLOB, N_("Enable a global mod"), N_("mod") },
|
|
{ "mod_ca", '\0', POPT_ARG_STRING, NULL, CLI_MOD_CA, N_("Enable a campaign only mod"), N_("mod") },
|
|
{ "mod_mp", '\0', POPT_ARG_STRING, NULL, CLI_MOD_MP, N_("Enable a multiplay only mod"), N_("mod") },
|
|
{ "noassert", '\0', POPT_ARG_NONE, NULL, CLI_NOASSERT, N_("Disable asserts"), NULL },
|
|
{ "crash", '\0', POPT_ARG_NONE, NULL, CLI_CRASH, N_("Causes a crash to test the crash handler"), NULL },
|
|
{ "savegame", '\0', POPT_ARG_STRING, NULL, CLI_SAVEGAME, N_("Load a saved game"), N_("savegame") },
|
|
{ "window", '\0', POPT_ARG_NONE, NULL, CLI_WINDOW, N_("Play in windowed mode"), NULL },
|
|
{ "version", '\0', POPT_ARG_NONE, NULL, CLI_VERSION, N_("Show version information and exit"), NULL },
|
|
{ "resolution", '\0', POPT_ARG_STRING, NULL, CLI_RESOLUTION, N_("Set the resolution to use"), N_("WIDTHxHEIGHT") },
|
|
{ "shadows", '\0', POPT_ARG_NONE, NULL, CLI_SHADOWS, N_("Enable shadows"), NULL },
|
|
{ "noshadows", '\0', POPT_ARG_NONE, NULL, CLI_NOSHADOWS, N_("Disable shadows"), NULL },
|
|
{ "sound", '\0', POPT_ARG_NONE, NULL, CLI_SOUND, N_("Enable sound"), NULL },
|
|
{ "nosound", '\0', POPT_ARG_NONE, NULL, CLI_NOSOUND, N_("Disable sound"), NULL },
|
|
{ "join", '\0', POPT_ARG_STRING, NULL, CLI_CONNECTTOIP,N_("connect directly to IP/hostname"), N_("host") },
|
|
{ "host", '\0', POPT_ARG_NONE, NULL, CLI_HOSTLAUNCH, N_("go directly to host screen"), NULL },
|
|
{ "texturecompression", '\0', POPT_ARG_NONE, NULL, CLI_TEXTURECOMPRESSION, N_("Enable texture compression"), NULL },
|
|
{ "notexturecompression", '\0', POPT_ARG_NONE, NULL, CLI_NOTEXTURECOMPRESSION, N_("Disable texture compression"), NULL },
|
|
// Terminating entry
|
|
{ NULL, '\0', 0, NULL, 0, NULL, NULL },
|
|
};
|
|
|
|
static struct poptOption TranslatedOptionsTable[sizeof(optionsTable) / sizeof(struct poptOption)];
|
|
static bool translated = false;
|
|
|
|
if (translated == false)
|
|
{
|
|
unsigned int table_size = sizeof(optionsTable) / sizeof(struct poptOption) - 1;
|
|
unsigned int i;
|
|
|
|
for (i = 0; i < table_size; ++i)
|
|
{
|
|
TranslatedOptionsTable[i] = optionsTable[i];
|
|
|
|
// If there is a description, make sure to translate it with gettext
|
|
if (TranslatedOptionsTable[i].descrip != NULL)
|
|
TranslatedOptionsTable[i].descrip = gettext(TranslatedOptionsTable[i].descrip);
|
|
|
|
if (TranslatedOptionsTable[i].argDescrip != NULL)
|
|
TranslatedOptionsTable[i].argDescrip = gettext(TranslatedOptionsTable[i].argDescrip);
|
|
}
|
|
|
|
translated = true;
|
|
}
|
|
|
|
return TranslatedOptionsTable;
|
|
}
|
|
|
|
//! Early parsing of the commandline
|
|
/**
|
|
* First half of the command line parsing. Also see ParseCommandLine()
|
|
* below. The parameters here are needed early in the boot process,
|
|
* while the ones in ParseCommandLine can benefit from debugging being
|
|
* set up first.
|
|
* \param argc number of arguments given
|
|
* \param argv string array of the arguments
|
|
* \return Returns true on success, false on error */
|
|
bool ParseCommandLineEarly(int argc, const char** argv)
|
|
{
|
|
poptContext poptCon = poptGetContext(NULL, argc, argv, getOptionsTable(), 0);
|
|
int iOption;
|
|
|
|
#if defined(WZ_OS_MAC) && defined(DEBUG)
|
|
debug_enable_switch( "all" );
|
|
#endif /* WZ_OS_MAC && DEBUG */
|
|
|
|
/* loop through command line */
|
|
while ((iOption = poptGetNextOpt(poptCon)) > 0 || iOption == POPT_ERROR_BADOPT)
|
|
{
|
|
CLI_OPTIONS option = (CLI_OPTIONS)iOption;
|
|
const char* token;
|
|
|
|
if (iOption == POPT_ERROR_BADOPT)
|
|
{
|
|
qFatal("Unrecognized option: %s", poptBadOption(poptCon, 0));
|
|
}
|
|
|
|
switch (option)
|
|
{
|
|
case CLI_DEBUG:
|
|
// retrieve the debug section name
|
|
token = poptGetOptArg(poptCon);
|
|
if (token == NULL)
|
|
{
|
|
qFatal("Usage: --debug=<flag>");
|
|
}
|
|
|
|
// Attempt to enable the given debug section
|
|
if (!debug_enable_switch(token))
|
|
{
|
|
qFatal("Debug flag \"%s\" not found!", token);
|
|
}
|
|
break;
|
|
|
|
case CLI_DEBUGFILE:
|
|
// find the file name
|
|
token = poptGetOptArg(poptCon);
|
|
if (token == NULL)
|
|
{
|
|
qFatal("Missing debugfile filename?");
|
|
}
|
|
debug_register_callback( debug_callback_file, debug_callback_file_init, debug_callback_file_exit, (void*)token );
|
|
customDebugfile = true;
|
|
break;
|
|
|
|
case CLI_FLUSHDEBUGSTDERR:
|
|
// Tell the debug stderr output callback to always flush its output
|
|
debugFlushStderr();
|
|
break;
|
|
|
|
case CLI_CONFIGDIR:
|
|
// retrieve the configuration directory
|
|
token = poptGetOptArg(poptCon);
|
|
if (token == NULL)
|
|
{
|
|
qFatal("Unrecognised configuration directory");
|
|
}
|
|
sstrcpy(configdir, token);
|
|
break;
|
|
|
|
case CLI_HELP:
|
|
poptPrintHelp(poptCon, stdout, 0);
|
|
return false;
|
|
|
|
case CLI_VERSION:
|
|
printf("Warzone 2100 - %s\n", version_getFormattedVersionString());
|
|
return false;
|
|
|
|
default:
|
|
break;
|
|
};
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//! second half of parsing the commandline
|
|
/**
|
|
* Second half of command line parsing. See ParseCommandLineEarly() for
|
|
* the first half. Note that render mode must come before resolution flag.
|
|
* \param argc number of arguments given
|
|
* \param argv string array of the arguments
|
|
* \return Returns true on success, false on error */
|
|
bool ParseCommandLine(int argc, const char** argv)
|
|
{
|
|
poptContext poptCon = poptGetContext(NULL, argc, argv, getOptionsTable(), 0);
|
|
int iOption;
|
|
|
|
/* loop through command line */
|
|
while ((iOption = poptGetNextOpt(poptCon)) > 0)
|
|
{
|
|
const char* token;
|
|
CLI_OPTIONS option = (CLI_OPTIONS)iOption;
|
|
|
|
switch (option)
|
|
{
|
|
case CLI_DEBUG:
|
|
case CLI_DEBUGFILE:
|
|
case CLI_FLUSHDEBUGSTDERR:
|
|
case CLI_CONFIGDIR:
|
|
case CLI_HELP:
|
|
case CLI_VERSION:
|
|
// These options are parsed in ParseCommandLineEarly() already, so ignore them
|
|
break;
|
|
|
|
case CLI_NOASSERT:
|
|
kf_NoAssert();
|
|
break;
|
|
|
|
// NOTE: The sole purpose of this is to test the crash handler.
|
|
case CLI_CRASH:
|
|
CauseCrash = true;
|
|
NetPlay.bComms = false;
|
|
sstrcpy(aLevelName, "CAM_3A");
|
|
SetGameMode(GS_NORMAL);
|
|
break;
|
|
|
|
case CLI_DATADIR:
|
|
// retrieve the quoted path name
|
|
token = poptGetOptArg(poptCon);
|
|
if (token == NULL)
|
|
{
|
|
qFatal("Unrecognised datadir");
|
|
}
|
|
sstrcpy(datadir, token);
|
|
break;
|
|
|
|
case CLI_FULLSCREEN:
|
|
war_setFullscreen(true);
|
|
break;
|
|
case CLI_CONNECTTOIP:
|
|
//get the ip we want to connect with, and go directly to join screen.
|
|
token = poptGetOptArg(poptCon);
|
|
if (token == NULL)
|
|
{
|
|
qFatal("No IP/hostname given");
|
|
}
|
|
sstrcpy(iptoconnect, token);
|
|
break;
|
|
case CLI_HOSTLAUNCH:
|
|
// go directly to host screen, bypass all others.
|
|
hostlaunch = true;
|
|
break;
|
|
case CLI_GAME:
|
|
// retrieve the game name
|
|
token = poptGetOptArg(poptCon);
|
|
if (token == NULL
|
|
|| (strcmp(token, "CAM_1A") && strcmp(token, "CAM_2A") && strcmp(token, "CAM_3A")
|
|
&& strcmp(token, "TUTORIAL3") && strcmp(token, "FASTPLAY")))
|
|
{
|
|
qFatal("The game parameter requires one of the following keywords:"
|
|
"CAM_1A, CAM_2A, CAM_3A, TUTORIAL3, or FASTPLAY.");
|
|
}
|
|
NetPlay.bComms = false;
|
|
bMultiPlayer = false;
|
|
bMultiMessages = false;
|
|
NetPlay.players[0].allocated = true;
|
|
if (!strcmp(token, "CAM_1A") || !strcmp(token, "CAM_2A") || !strcmp(token, "CAM_3A"))
|
|
{
|
|
game.type = CAMPAIGN;
|
|
}
|
|
else
|
|
{
|
|
game.type = SKIRMISH; // tutorial is skirmish for some reason
|
|
}
|
|
sstrcpy(aLevelName, token);
|
|
SetGameMode(GS_NORMAL);
|
|
break;
|
|
case CLI_MOD_GLOB:
|
|
{
|
|
unsigned int i;
|
|
|
|
// retrieve the file name
|
|
token = poptGetOptArg(poptCon);
|
|
if (token == NULL)
|
|
{
|
|
qFatal("Missing mod name?");
|
|
}
|
|
|
|
// Find an empty place in the global_mods list
|
|
for (i = 0; i < 100 && global_mods[i] != NULL; ++i) {}
|
|
if (i >= 100 || global_mods[i] != NULL)
|
|
{
|
|
qFatal("Too many mods registered! Aborting!");
|
|
}
|
|
global_mods[i] = strdup(token);
|
|
break;
|
|
}
|
|
case CLI_MOD_CA:
|
|
{
|
|
unsigned int i;
|
|
|
|
// retrieve the file name
|
|
token = poptGetOptArg(poptCon);
|
|
if (token == NULL)
|
|
{
|
|
qFatal("Missing mod name?");
|
|
}
|
|
|
|
// Find an empty place in the campaign_mods list
|
|
for (i = 0; i < 100 && campaign_mods[i] != NULL; ++i) {}
|
|
if (i >= 100 || campaign_mods[i] != NULL)
|
|
{
|
|
qFatal("Too many mods registered! Aborting!");
|
|
}
|
|
campaign_mods[i] = strdup(token);
|
|
break;
|
|
}
|
|
case CLI_MOD_MP:
|
|
{
|
|
unsigned int i;
|
|
|
|
// retrieve the file name
|
|
token = poptGetOptArg(poptCon);
|
|
if (token == NULL)
|
|
{
|
|
qFatal("Missing mod name?");
|
|
}
|
|
|
|
for (i = 0; i < 100 && multiplay_mods[i] != NULL; ++i) {}
|
|
if (i >= 100 || multiplay_mods[i] != NULL)
|
|
{
|
|
qFatal("Too many mods registered! Aborting!");
|
|
}
|
|
multiplay_mods[i] = strdup(token);
|
|
break;
|
|
}
|
|
case CLI_RESOLUTION:
|
|
{
|
|
unsigned int width, height;
|
|
|
|
token = poptGetOptArg(poptCon);
|
|
if (sscanf(token, "%ix%i", &width, &height ) != 2 )
|
|
{
|
|
qFatal("Invalid parameter specified (format is WIDTHxHEIGHT, e.g. 800x600)");
|
|
}
|
|
if (width < 640) {
|
|
debug(LOG_ERROR, "Screen width < 640 unsupported, using 640");
|
|
width = 640;
|
|
}
|
|
if (height < 480) {
|
|
debug(LOG_ERROR, "Screen height < 480 unsupported, using 480");
|
|
height = 480;
|
|
}
|
|
// tell the display system of the desired resolution
|
|
pie_SetVideoBufferWidth(width);
|
|
pie_SetVideoBufferHeight(height);
|
|
// and update the configuration
|
|
war_SetWidth(width);
|
|
war_SetHeight(height);
|
|
break;
|
|
}
|
|
case CLI_SAVEGAME:
|
|
// retrieve the game name
|
|
token = poptGetOptArg(poptCon);
|
|
if (token == NULL)
|
|
{
|
|
qFatal("Unrecognised savegame name");
|
|
}
|
|
snprintf(saveGameName, sizeof(saveGameName), "%s/%s", SaveGamePath, token);
|
|
SetGameMode(GS_SAVEGAMELOAD);
|
|
break;
|
|
|
|
case CLI_WINDOW:
|
|
war_setFullscreen(false);
|
|
break;
|
|
|
|
case CLI_SHADOWS:
|
|
setDrawShadows(true);
|
|
break;
|
|
|
|
case CLI_NOSHADOWS:
|
|
setDrawShadows(false);
|
|
break;
|
|
|
|
case CLI_SOUND:
|
|
war_setSoundEnabled(true);
|
|
break;
|
|
|
|
case CLI_NOSOUND:
|
|
war_setSoundEnabled(false);
|
|
break;
|
|
|
|
case CLI_TEXTURECOMPRESSION:
|
|
wz_texture_compression = GL_COMPRESSED_RGBA_ARB;
|
|
break;
|
|
|
|
case CLI_NOTEXTURECOMPRESSION:
|
|
wz_texture_compression = GL_RGBA;
|
|
break;
|
|
};
|
|
}
|
|
|
|
return true;
|
|
}
|