mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-12 02:01:29 -10:00
655 lines
16 KiB
C++
655 lines
16 KiB
C++
// TabCommLine.cpp : implementation file
|
|
//
|
|
|
|
#include <io.h>
|
|
#include <direct.h>
|
|
|
|
#include "stdafx.h"
|
|
#include "Launcher.h"
|
|
#include "TabCommLine.h"
|
|
|
|
#include "win32func.h"
|
|
#include "misc.h"
|
|
#include "launcher_settings.h"
|
|
#include "metadata.h"
|
|
#include "mod_settings.h"
|
|
|
|
#include "iniparser/iniparser.h"
|
|
#include "iniparser/dictionary.h"
|
|
|
|
#include "LauncherDlg.h"
|
|
|
|
#ifdef _DEBUG
|
|
#define new DEBUG_NEW
|
|
#undef THIS_FILE
|
|
static char THIS_FILE[] = __FILE__;
|
|
#endif
|
|
|
|
|
|
int num_eflags = 0;
|
|
int num_params = 0;
|
|
|
|
|
|
EasyFlag *easy_flags = NULL;
|
|
Flag *exe_params = NULL;
|
|
bool *flag_states = NULL;
|
|
|
|
#define MAX_CMDLINE_SIZE 3000
|
|
#define MAX_CUSTOM_PARAM_SIZE 1200
|
|
|
|
char command_line[MAX_CMDLINE_SIZE] = "";
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// CTabCommLine dialog
|
|
|
|
|
|
CTabCommLine::CTabCommLine(CWnd* pParent /*=NULL*/)
|
|
: CDialog(CTabCommLine::IDD, pParent)
|
|
{
|
|
m_flag_gen_in_process = false;
|
|
|
|
//{{AFX_DATA_INIT(CTabCommLine)
|
|
//}}AFX_DATA_INIT
|
|
}
|
|
|
|
CTabCommLine::~CTabCommLine()
|
|
{
|
|
if(easy_flags) free(easy_flags);
|
|
if(exe_params) free(exe_params);
|
|
if(flag_states) free(flag_states);
|
|
}
|
|
|
|
void CTabCommLine::DoDataExchange(CDataExchange* pDX)
|
|
{
|
|
CDialog::DoDataExchange(pDX);
|
|
//{{AFX_DATA_MAP(CTabCommLine)
|
|
DDX_Control(pDX, IDC_FLAG_TYPE, m_flag_type_list);
|
|
DDX_Control(pDX, IDC_FLAG_SETUP, m_easy_flag);
|
|
DDX_Control(pDX, IDC_FLAG_LIST, m_flag_list);
|
|
//}}AFX_DATA_MAP
|
|
}
|
|
|
|
|
|
BEGIN_MESSAGE_MAP(CTabCommLine, CDialog)
|
|
//{{AFX_MSG_MAP(CTabCommLine)
|
|
ON_EN_CHANGE(IDC_CUSTOM_PARAM, OnChangeCustomParam)
|
|
ON_NOTIFY(LVN_ITEMCHANGED, IDC_FLAG_LIST, OnItemchangedFlagList)
|
|
ON_CBN_SELCHANGE(IDC_FLAG_SETUP, OnSelchangeFlagSetup)
|
|
ON_NOTIFY(NM_DBLCLK, IDC_FLAG_LIST, OnDblclkFlagList)
|
|
ON_CBN_SELCHANGE(IDC_FLAG_TYPE, OnSelchangeFlagType)
|
|
ON_BN_CLICKED(IDC_SETTINGS_NORMAL, OnSettingsNormal)
|
|
ON_BN_CLICKED(IDC_SETTINGS_MOD, OnSettingsMod)
|
|
//}}AFX_MSG_MAP
|
|
END_MESSAGE_MAP()
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// CTabCommLine message handlers
|
|
|
|
BOOL CTabCommLine::OnInitDialog()
|
|
{
|
|
CDialog::OnInitDialog();
|
|
// Make sure its a checkbox and insert a column
|
|
m_flag_list.SetExtendedStyle(LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT);
|
|
m_flag_list.InsertColumn(0, "Flag", LVCFMT_LEFT, 200);
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* @return CString - string holding command line (may be empty)
|
|
*/
|
|
CString CTabCommLine::GetCommandLine()
|
|
{
|
|
return CString(command_line);
|
|
}
|
|
|
|
/**
|
|
* Populate the command line variable, and update the dialog window.
|
|
*/
|
|
void CTabCommLine::UpdateCommandLine()
|
|
{
|
|
char *mod_params = NULL;
|
|
char *standard_params = NULL;
|
|
char *custom_params = NULL;
|
|
bool enable_reset_to_normal = false; // TODO: what is "normal"?
|
|
bool enable_reset_to_mod = false;
|
|
strcpy(command_line, "");
|
|
|
|
// add the EXE to the command line
|
|
if (LauncherSettings::is_exe_path_valid())
|
|
{
|
|
strcat(command_line, LauncherSettings::get_exe_filepath());
|
|
}
|
|
else
|
|
{
|
|
goto done_with_command_line;
|
|
}
|
|
|
|
// for non-custom EXEs, that's all we can do
|
|
if (LauncherSettings::get_exe_type() != EXE_TYPE_CUSTOM)
|
|
{
|
|
goto done_with_command_line;
|
|
}
|
|
|
|
// add params specified by mod
|
|
mod_params = ModSettings::get_mod_parameters();
|
|
if (strlen(mod_params) > 0)
|
|
{
|
|
strcat(command_line, mod_params);
|
|
strcat(command_line, " ");
|
|
enable_reset_to_mod = true;
|
|
}
|
|
|
|
// add params specified by checkbox
|
|
standard_params = ModSettings::get_standard_parameters();
|
|
if (strlen(standard_params) > 0)
|
|
{
|
|
strcat(command_line, standard_params);
|
|
strcat(command_line, " ");
|
|
}
|
|
|
|
// add params entered by user
|
|
custom_params = ModSettings::get_custom_parameters();
|
|
if (strlen(custom_params) > 0)
|
|
{
|
|
strcat(command_line, custom_params);
|
|
strcat(command_line, " ");
|
|
}
|
|
|
|
trim(command_line);
|
|
|
|
done_with_command_line:
|
|
// draw the dialog window
|
|
GetDlgItem(IDC_COMM_LINE_PATH)->SetWindowText(command_line);
|
|
GetDlgItem(IDC_SETTINGS_NORMAL)->EnableWindow(enable_reset_to_normal);
|
|
GetDlgItem(IDC_SETTINGS_MOD)->EnableWindow(enable_reset_to_mod);
|
|
CLauncherDlg::Redraw();
|
|
}
|
|
|
|
/**
|
|
* When the flag states array has been changed, call this to update the GUI.
|
|
*/
|
|
void CTabCommLine::RefreshFlagList()
|
|
{
|
|
int num_flags = m_flag_list.GetItemCount();
|
|
|
|
for(int i = 0; i < num_flags; i++)
|
|
{
|
|
int index = m_flag_list.GetItemData(i);
|
|
m_flag_list.SetCheck(i, flag_states[index]);
|
|
}
|
|
|
|
CLauncherDlg::Redraw();
|
|
}
|
|
|
|
/**
|
|
* This is called when a tick box is ticked or unticked on the parameter list
|
|
*/
|
|
void CTabCommLine::OnItemchangedFlagList(NMHDR* pNMHDR, LRESULT* pResult)
|
|
{
|
|
NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
|
|
|
|
*pResult = 0;
|
|
|
|
if (pNMListView->iItem < 0) return;
|
|
if (m_flag_gen_in_process) return;
|
|
|
|
int index = m_flag_list.GetItemData(pNMListView->iItem);
|
|
flag_states[index] = (m_flag_list.GetCheck(pNMListView->iItem) != 0);
|
|
|
|
// build list of params
|
|
char standard_params[MAX_CMDLINE_SIZE];
|
|
strcpy(standard_params, "");
|
|
for (int i = 0; i < num_params; i++)
|
|
{
|
|
if (flag_states[i])
|
|
{
|
|
strcat(standard_params, exe_params[i].name);
|
|
strcat(standard_params, " ");
|
|
}
|
|
}
|
|
trim(standard_params);
|
|
ModSettings::set_standard_parameters(standard_params);
|
|
|
|
UpdateCommandLine();
|
|
}
|
|
|
|
/**
|
|
* This is called when something is typed in the custom parameter edit box
|
|
*/
|
|
void CTabCommLine::OnChangeCustomParam()
|
|
{
|
|
// get trimmed list of params
|
|
CString custom_params;
|
|
GetDlgItem(IDC_CUSTOM_PARAM)->GetWindowText(custom_params);
|
|
custom_params.TrimLeft();
|
|
custom_params.TrimRight();
|
|
ModSettings::set_custom_parameters(custom_params);
|
|
|
|
UpdateCommandLine();
|
|
}
|
|
|
|
/**
|
|
* @return - EXE_TYPE_NONE, EXE_TYPE_CUSTOM or if recognised exe it returns the flag list.
|
|
*/
|
|
int CTabCommLine::GetEXEFlags()
|
|
{
|
|
if(LauncherSettings::get_exe_type() == EXE_TYPE_NONE)
|
|
return 0;
|
|
|
|
return exe_types[LauncherSettings::get_exe_type()].flags;
|
|
}
|
|
|
|
/**
|
|
* @return int - type of exe (one of following)
|
|
*
|
|
* EXE_TYPE_NONE
|
|
* EXE_TYPE_CUSTOM
|
|
* An integer to reference the 'exe_types' array with
|
|
*/
|
|
void CTabCommLine::SelectRegPathAndExeType()
|
|
{
|
|
if ( !LauncherSettings::is_exe_path_valid() ||
|
|
!file_exists(LauncherSettings::get_exe_filepath()) )
|
|
{
|
|
LauncherSettings::set_reg_path("", EXE_TYPE_NONE);
|
|
return;
|
|
}
|
|
|
|
int exe_type = EXE_TYPE_CUSTOM;
|
|
|
|
// Use filename and size to determine official builds
|
|
for (int i = 0; i < MAX_EXE_TYPES; i++)
|
|
{
|
|
// Confirm this by name
|
|
if (stricmp(exe_types[i].exe_name, LauncherSettings::get_exe_nameonly()) == 0)
|
|
{
|
|
exe_type = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
char reg_path[MAX_PATH];
|
|
|
|
if (exe_type < MAX_EXE_TYPES)
|
|
{
|
|
sprintf(reg_path, "SOFTWARE\\%s\\%s",
|
|
exe_types[exe_type].company,
|
|
exe_types[exe_type].regname);
|
|
}
|
|
else
|
|
{
|
|
sprintf(reg_path, "SOFTWARE\\%s\\%s",
|
|
exe_types[EXE_TYPE_FS2].company,
|
|
exe_types[EXE_TYPE_FS2].regname);
|
|
}
|
|
|
|
LauncherSettings::set_reg_path(reg_path, exe_type);
|
|
}
|
|
|
|
void CTabCommLine::ConstructFlagList()
|
|
{
|
|
int k;
|
|
|
|
// If this is a retail FS2 exe skip all this
|
|
if (LauncherSettings::get_exe_type() != EXE_TYPE_CUSTOM)
|
|
{
|
|
m_easy_flag.ResetContent();
|
|
m_easy_flag.EnableWindow(FALSE);
|
|
|
|
ConstructFlagListRetail();
|
|
ConstructFlagListInternal();
|
|
|
|
return;
|
|
}
|
|
|
|
m_flag_type_list.EnableWindow(TRUE);
|
|
m_flag_list.EnableWindow(TRUE);
|
|
m_easy_flag.EnableWindow(TRUE);
|
|
|
|
// Delete and generate new flag file
|
|
char flag_file[MAX_PATH];
|
|
sprintf(flag_file, "%s\\flags.lch",LauncherSettings::get_exe_pathonly());
|
|
|
|
DeleteFile(flag_file);
|
|
|
|
if ( !run_file((LPTSTR) LauncherSettings::get_exe_nameonly(), (LPTSTR) LauncherSettings::get_exe_pathonly()," -get_flags", true) )
|
|
{
|
|
MessageBox("Unable to query FreeSpace Open for launcher flag information! Is the EXE present?");
|
|
return;
|
|
}
|
|
|
|
FILE *fp = fopen(flag_file, "r");
|
|
|
|
int focount = 2000;
|
|
while (fp == NULL && focount)
|
|
{
|
|
fp = fopen(flag_file, "r");
|
|
focount--;
|
|
}
|
|
|
|
if (fp == NULL)
|
|
{
|
|
MessageBox("Failed to read launcher flag file", "Fatal Error", MB_OK);
|
|
return;
|
|
}
|
|
|
|
int eflags_struct_size;
|
|
int params_struct_size;
|
|
|
|
fread(&eflags_struct_size, sizeof(int), 1, fp);
|
|
fread(¶ms_struct_size, sizeof(int), 1, fp);
|
|
|
|
if (eflags_struct_size != sizeof(EasyFlag) ||
|
|
params_struct_size != sizeof(Flag))
|
|
{
|
|
MessageBox("Launcher and fs2_open versions do not work with each other", "Fatal Error", MB_OK);
|
|
fclose(fp);
|
|
DeleteFile(flag_file);
|
|
return;
|
|
}
|
|
|
|
if (easy_flags)
|
|
free(easy_flags);
|
|
if (exe_params)
|
|
free(exe_params);
|
|
if (flag_states)
|
|
free(flag_states);
|
|
|
|
fread(&num_eflags, sizeof(int), 1, fp);
|
|
easy_flags = (EasyFlag *) malloc(sizeof(EasyFlag) * num_eflags);
|
|
|
|
if (easy_flags == NULL)
|
|
{
|
|
MessageBox("Failed to allocate enough memory for easy_flags", "Fatal Error", MB_OK);
|
|
fclose(fp);
|
|
DeleteFile(flag_file);
|
|
return;
|
|
}
|
|
|
|
fread(easy_flags, sizeof(EasyFlag) * num_eflags, 1, fp);
|
|
|
|
fread(&num_params, sizeof(int), 1, fp);
|
|
exe_params = (Flag *) malloc(sizeof(Flag) * num_params);
|
|
flag_states = (bool *) malloc(sizeof(bool) * num_params);
|
|
|
|
memset(flag_states, 0, sizeof(bool) * num_params);
|
|
|
|
if (exe_params == NULL || flag_states == NULL)
|
|
{
|
|
MessageBox("Failed to allocate enough memory for exe_params", "Fatal Error", MB_OK);
|
|
fclose(fp);
|
|
DeleteFile(flag_file);
|
|
return;
|
|
}
|
|
|
|
fread(exe_params, sizeof(Flag) * num_params, 1, fp);
|
|
|
|
// hack for OpenAL check
|
|
if ( (filelength(fileno(fp)) - ftell(fp)) == 1 ) {
|
|
LauncherSettings::set_openal_build(true);
|
|
}
|
|
|
|
fclose(fp);
|
|
|
|
// go ahead and delete the file now, it's just taking up space at this point
|
|
DeleteFile(flag_file);
|
|
|
|
// Setup Easy Flags
|
|
m_easy_flag.ResetContent();
|
|
for (k = 0; k < num_eflags; k++)
|
|
{
|
|
m_easy_flag.InsertString(k,easy_flags[k].name);
|
|
}
|
|
|
|
// Setup Flag Types
|
|
m_flag_type_list.ResetContent();
|
|
|
|
int count = 0;
|
|
char last_word[FLAG_TYPE_LEN] = "";
|
|
for (k = 0; k < num_params; k++)
|
|
{
|
|
if (strcmp(exe_params[k].type, last_word) != 0)
|
|
{
|
|
m_flag_type_list.InsertString(count, exe_params[k].type);
|
|
strcpy(last_word, exe_params[k].type);
|
|
count++;
|
|
|
|
if (count > 19)
|
|
{
|
|
MessageBox("Flag type count is 20 or more, please report to coder","Error");
|
|
count = 19;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_flag_type_list.SetCurSel(0);
|
|
ConstructFlagListInternal();
|
|
}
|
|
|
|
void CTabCommLine::ConstructFlagListRetail()
|
|
{
|
|
int i, count = 0;
|
|
char last_word[FLAG_TYPE_LEN] = "";
|
|
|
|
if ( exe_types[LauncherSettings::get_exe_type()].flags & FLAG_FS1 )
|
|
num_params = Num_retail_params_FS1;
|
|
else
|
|
num_params = Num_retail_params_FS2;
|
|
|
|
exe_params = (Flag *) malloc(sizeof(Flag) * num_params);
|
|
flag_states = (bool *) malloc(sizeof(bool) * num_params);
|
|
|
|
if ( (exe_params == NULL) || (flag_states == NULL) )
|
|
{
|
|
MessageBox("Memory allocation failure!");
|
|
return;
|
|
}
|
|
|
|
if ( exe_types[LauncherSettings::get_exe_type()].flags & FLAG_FS1 )
|
|
memcpy( exe_params, retail_params_FS1, sizeof(Flag) * num_params );
|
|
else
|
|
memcpy( exe_params, retail_params_FS2, sizeof(Flag) * num_params );
|
|
|
|
memset( flag_states, 0, sizeof(bool) * num_params );
|
|
|
|
// setup flag types
|
|
m_flag_type_list.ResetContent();
|
|
m_flag_type_list.EnableWindow(TRUE);
|
|
|
|
for (i = 0; i < num_params; i++) {
|
|
if ( strcmp(exe_params[i].type, last_word) ) {
|
|
m_flag_type_list.InsertString(count, exe_params[i].type);
|
|
strcpy(last_word, exe_params[i].type);
|
|
count++;
|
|
}
|
|
}
|
|
|
|
m_flag_type_list.SetCurSel(0);
|
|
}
|
|
|
|
void CTabCommLine::ConstructFlagListInternal()
|
|
{
|
|
int type_index = m_flag_type_list.GetCurSel();
|
|
|
|
char type_text[FLAG_TYPE_LEN];
|
|
m_flag_type_list.GetWindowText(type_text, FLAG_TYPE_LEN);
|
|
|
|
// Setup flags
|
|
m_flag_list.DeleteAllItems();
|
|
m_flag_list.EnableWindow(TRUE);
|
|
|
|
int exe_flags = 0;
|
|
|
|
// If we are not sure what this exe is give access to all parameters
|
|
if(LauncherSettings::get_exe_type() != EXE_TYPE_NONE)
|
|
{
|
|
exe_flags = exe_types[LauncherSettings::get_exe_type()].flags;
|
|
}
|
|
|
|
m_flag_gen_in_process = true;
|
|
|
|
// For each parameter
|
|
for(int i = 0, j = 0; i < num_params; i++)
|
|
{
|
|
if(strcmp(exe_params[i].type, type_text) != 0)
|
|
continue;
|
|
|
|
if(exe_params[i].fso_only && !(exe_types[LauncherSettings::get_exe_type()].flags & FLAG_FS2OPEN))
|
|
continue;
|
|
|
|
// Insert this item but keep a record of the index number
|
|
// The insert item index will not stay as is and will mess up if there are gaps (unshown params)
|
|
if(strlen(exe_params[i].desc) > 0)
|
|
m_flag_list.InsertItem(j, exe_params[i].desc, 0);
|
|
else
|
|
m_flag_list.InsertItem(j, exe_params[i].name, 0);
|
|
|
|
m_flag_list.SetItemData(j, i);
|
|
j++;
|
|
}
|
|
|
|
m_flag_gen_in_process = false;
|
|
}
|
|
|
|
void CTabCommLine::LoadSettings()
|
|
{
|
|
ModSettings::load_user();
|
|
PopulateFlagStates();
|
|
|
|
RefreshFlagList();
|
|
GetDlgItem(IDC_CUSTOM_PARAM)->SetWindowText(ModSettings::get_custom_parameters());
|
|
|
|
UpdateCommandLine();
|
|
}
|
|
|
|
void CTabCommLine::PopulateFlagStates()
|
|
{
|
|
char *new_standard_params = strdup(ModSettings::get_standard_parameters());
|
|
char *new_custom_params = strdup(ModSettings::get_custom_parameters());
|
|
|
|
// tokenize the standard parameters and copy them to
|
|
|
|
for (int i = 0; i < num_params; i++)
|
|
{
|
|
exe_params[i].name,
|
|
|
|
}
|
|
// Seperate custom flags from standard one
|
|
for(int i = 0; i < num_params; i++) {
|
|
// while going through the cmdline make sure to grab only the option that we
|
|
// are looking for, but if one similar then keep searching for the exact match
|
|
do {
|
|
found_str = strstr(&custom_param[0] + get_new_offset, exe_params[i].name);
|
|
|
|
if (found_str && (*(found_str + strlen(exe_params[i].name))) && (*(found_str + strlen(exe_params[i].name)) != ' ') ) {
|
|
// the new offset should be our current location + the length of the current option
|
|
get_new_offset = (strlen(custom_param) - strlen(found_str) + strlen(exe_params[i].name));
|
|
} else {
|
|
get_new_offset = 0;
|
|
}
|
|
} while ( get_new_offset );
|
|
|
|
if (found_str == NULL)
|
|
continue;
|
|
|
|
flag_states[i] = true;
|
|
|
|
if (end_string_here == NULL)
|
|
end_string_here = found_str;
|
|
}
|
|
|
|
// Cut the standard options out of the custom string
|
|
if (end_string_here != NULL) {
|
|
if (end_string_here > custom_param && end_string_here[-1] == ' ') {
|
|
end_string_here[-1] = '\0';
|
|
} else {
|
|
end_string_here[0] = '\0';
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Save command line to settings.ini file.
|
|
*/
|
|
void CTabCommLine::SaveSettings()
|
|
{
|
|
ModSettings::save_user();
|
|
}
|
|
|
|
// User has selected new easy select type
|
|
void CTabCommLine::OnSelchangeFlagSetup()
|
|
{
|
|
int easy_flag = m_easy_flag.GetCurSel();
|
|
|
|
// Custom setting, leave it alone
|
|
if(easy_flag == 0) return;
|
|
|
|
for(int i = 0; i < num_params; i++)
|
|
{
|
|
if(exe_params[i].on_flags & (1 << easy_flag))
|
|
flag_states[i] = true;
|
|
|
|
if(exe_params[i].off_flags & (1 << easy_flag))
|
|
flag_states[i] = false;
|
|
}
|
|
|
|
RefreshFlagList();
|
|
UpdateCommandLine();
|
|
}
|
|
|
|
// User has selected a new flag group type
|
|
void CTabCommLine::OnSelchangeFlagType()
|
|
{
|
|
ConstructFlagListInternal();
|
|
RefreshFlagList();
|
|
}
|
|
|
|
// User has double clicked flag
|
|
void CTabCommLine::OnDblclkFlagList(NMHDR* pNMHDR, LRESULT* pResult)
|
|
{
|
|
NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
|
|
*pResult = 0;
|
|
|
|
if(exe_params == NULL) return;
|
|
if(pNMListView->iItem < 0) return;
|
|
|
|
int index = m_flag_list.GetItemData(pNMListView->iItem);
|
|
|
|
if(index < 0 || index > num_params)
|
|
{
|
|
MessageBox("Bad m_flag_list index value","Error");
|
|
}
|
|
|
|
if(strlen(exe_params[index].web_url) > 0)
|
|
{
|
|
if(exe_params[index].web_url[0] == 'h' &&
|
|
exe_params[index].web_url[1] == 't' &&
|
|
exe_params[index].web_url[2] == 't' &&
|
|
exe_params[index].web_url[3] == 'p')
|
|
{
|
|
open_web_page(exe_params[index].web_url);
|
|
}
|
|
else
|
|
{
|
|
MessageBox(exe_params[index].web_url,"Info");
|
|
}
|
|
}
|
|
else
|
|
MessageBox("No online help for this feature","Sorry");
|
|
}
|
|
|
|
void CTabCommLine::OnSettingsNormal()
|
|
{
|
|
UpdateCommandLine();
|
|
}
|
|
|
|
void CTabCommLine::OnSettingsMod()
|
|
{
|
|
ModSettings::set_standard_parameters("");
|
|
ModSettings::set_custom_parameters("");
|
|
UpdateCommandLine();
|
|
}
|