mirror of
https://gitee.com/anod/open_agb_firm.git
synced 2025-05-06 13:54:09 +08:00
612 lines
16 KiB
C
612 lines
16 KiB
C
/*
|
||
* This file is part of open_agb_firm
|
||
* Copyright (C) 2021 derrek, profi200
|
||
*
|
||
* 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 3 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, see <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include "types.h"
|
||
#include "error_codes.h"
|
||
#include "fs.h"
|
||
#include "util.h"
|
||
#include "arm11/drivers/hid.h"
|
||
#include "arm11/console.h"
|
||
#include "arm11/fmt.h"
|
||
#include "drivers/gfx.h"
|
||
#include "arm11/acf.h"
|
||
#include "arm11/atp.h"
|
||
#include "arm11/acl.h"
|
||
#include "arm11/cheat.h"
|
||
|
||
#define screenClean() memset(consoleGet()->frameBuffer, 0, CWIDTH*CHEIGHT*sizeof(uint16_t))
|
||
|
||
// Notes on these settings:
|
||
// MAX_ENT_BUF_SIZE should be big enough to hold the average file/dir name length * MAX_DIR_ENTRIES.
|
||
#define MAX_ENT_BUF_SIZE (1024u * 196) // 196 KiB.
|
||
#define MAX_DIR_ENTRIES (999u)
|
||
#define DIR_READ_BLOCKS (10u)
|
||
|
||
#define CWIDTH 320u
|
||
#define CHEIGHT 240u
|
||
|
||
#define ENT_TYPE_FILE (0)
|
||
#define ENT_TYPE_DIR (1)
|
||
|
||
typedef struct
|
||
{
|
||
u32 num; // Total number of entries.
|
||
char entBuf[MAX_ENT_BUF_SIZE]; // Format: char entryType; char name[X]; // null terminated.
|
||
char *ptrs[MAX_DIR_ENTRIES]; // For fast sorting.
|
||
} DirList;
|
||
|
||
int dlistCompare(const void *a, const void *b)
|
||
{
|
||
const char *entA = *(char**)a;
|
||
const char *entB = *(char**)b;
|
||
|
||
// Compare the entry type. Dirs have priority over files.
|
||
if(*entA != *entB) return (int)*entB - *entA;
|
||
|
||
// Compare the string.
|
||
int res;
|
||
do
|
||
{
|
||
res = *++entA - *++entB;
|
||
} while(res == 0 && *entA != '\0' && *entB != '\0');
|
||
|
||
return res;
|
||
}
|
||
|
||
static Result scanDir(const char *const path, DirList *const dList, const char *const filter, const char *match, int *index)
|
||
{
|
||
FILINFO *const fis = (FILINFO*)malloc(sizeof(FILINFO) * DIR_READ_BLOCKS);
|
||
if(fis == NULL) return RES_OUT_OF_MEM;
|
||
|
||
dList->num = 0;
|
||
|
||
Result res;
|
||
DHandle dh;
|
||
u32 received = 0;
|
||
if((res = fOpenDir(&dh, path)) == RES_OK)
|
||
{
|
||
u32 read; // Number of entries read by fReadDir().
|
||
u32 numEntries = 0; // Total number of processed entries.
|
||
u32 entBufPos = 0; // Entry buffer position/number of bytes used.
|
||
const u32 filterLen = strlen(filter);
|
||
do
|
||
{
|
||
if((res = fReadDir(dh, fis, DIR_READ_BLOCKS, &read)) != RES_OK) break;
|
||
received = numEntries + read;
|
||
read = (read <= MAX_DIR_ENTRIES - numEntries ? read : MAX_DIR_ENTRIES - numEntries);
|
||
|
||
for(u32 i = 0; i < read; i++)
|
||
{
|
||
const char entType = (fis[i].fattrib & AM_DIR ? ENT_TYPE_DIR : ENT_TYPE_FILE);
|
||
const u32 nameLen = strlen(fis[i].fname);
|
||
if(entType == ENT_TYPE_FILE)
|
||
{
|
||
if(nameLen <= filterLen || strcmp(filter, fis[i].fname + nameLen - filterLen) != 0)
|
||
continue;
|
||
}
|
||
|
||
// nameLen does not include the entry type and NULL termination.
|
||
if(entBufPos + nameLen + 2 > MAX_ENT_BUF_SIZE)
|
||
{
|
||
res = RES_PATH_TOO_LONG;
|
||
goto scanEnd;
|
||
}
|
||
|
||
char *const entry = &dList->entBuf[entBufPos];
|
||
*entry = entType;
|
||
safeStrcpy(&entry[1], fis[i].fname, 256);
|
||
dList->ptrs[numEntries++] = entry;
|
||
entBufPos += nameLen + 2;
|
||
}
|
||
} while(read == DIR_READ_BLOCKS);
|
||
|
||
scanEnd:
|
||
dList->num = numEntries;
|
||
|
||
fCloseDir(dh);
|
||
}
|
||
|
||
free(fis);
|
||
|
||
qsort(dList->ptrs, dList->num, sizeof(char*), dlistCompare);
|
||
|
||
if( match != NULL )
|
||
{
|
||
for( u32 i=0; i < dList->num; ++i )
|
||
{
|
||
if( strcmp(match, &dList->ptrs[i][1]) == 0 )
|
||
{
|
||
*index = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return received > MAX_DIR_ENTRIES ? RES_OUT_OF_RANGE : res;
|
||
}
|
||
|
||
static Result rom_get_serial( const char *file, char serial[5] )
|
||
{
|
||
FHandle f;
|
||
Result res;
|
||
u32 readed;
|
||
serial[0] = serial[4] = '\0';
|
||
if( RES_OK != (res=fOpen(&f, file, FA_OPEN_EXISTING | FA_READ)) )
|
||
{
|
||
return res;
|
||
}
|
||
if( RES_OK != (res=fLseek(f, 0xac)) )
|
||
{
|
||
fClose( f );
|
||
return res;
|
||
}
|
||
if( RES_OK != (res=fRead(f, serial, 4, &readed))
|
||
|| readed != 4 )
|
||
{
|
||
fClose(f);
|
||
return res;
|
||
}
|
||
fClose(f);
|
||
return RES_OK;
|
||
}
|
||
|
||
static atp_text_t folder_help[] = {
|
||
"操 作 说 明",
|
||
"~ ~ ~ ~ ~ ~ ~",
|
||
"蓝色 目录",
|
||
"白色 游戏",
|
||
"~ ~ ~ ~ ~ ~ ~",
|
||
"上下方向键 切换选中文件或目录",
|
||
"左右方向键 翻页",
|
||
"A键 查看目录或启动游戏",
|
||
"B键 上层文件夹",
|
||
"X键 金手指",
|
||
"Y键 暂不使用",
|
||
"START 查看说明",
|
||
"SELECT 系统设置"
|
||
};
|
||
|
||
static atp_error_t display_folder( atp_callerdata_t data, atp_counter_t index, atp_itemcfg_t *config )
|
||
{
|
||
void* *dat = (void **)data;
|
||
DirList *const dList = (DirList*)dat[0];
|
||
u8 type = *dList->ptrs[index];
|
||
if( type == ENT_TYPE_DIR )
|
||
config->text_color = ATP_COLOR_BLUE;
|
||
config->text = &dList->ptrs[index][1];
|
||
config->value = index;
|
||
return ATP_SUCCESS;
|
||
}
|
||
|
||
#define DECLARE_ERROR_PAGE(name, message) static atp_error_t name (atp_callerdata_t, atp_counter_t, atp_linecfg_t *c) \
|
||
{ \
|
||
c->text = message; \
|
||
c->text_align = ATP_PLACEMENT_CENTER; \
|
||
c->text_color = ATP_COLOR_RED; \
|
||
return ATP_SUCCESS; \
|
||
}
|
||
|
||
DECLARE_ERROR_PAGE( display_openlib, "打开金手指文件出错" )
|
||
DECLARE_ERROR_PAGE( display_selcht, "查找金手指配置出错" )
|
||
DECLARE_ERROR_PAGE( display_noserial, "提取游戏识别码失败" )
|
||
DECLARE_ERROR_PAGE( display_nocheat, "找不到对应的金手指配置" )
|
||
DECLARE_ERROR_PAGE( display_empty, "没有合适的文件" )
|
||
DECLARE_ERROR_PAGE( display_toolong, "路径过长,改名或移动文件后再试" )
|
||
DECLARE_ERROR_PAGE( display_pathfull, "游戏或目录过量,最多显示999个" )
|
||
DECLARE_ERROR_PAGE( display_longname, "文件名总计过长,只显示前面的文件" )
|
||
|
||
static atp_error_t display_help( atp_callerdata_t table, atp_counter_t index, atp_linecfg_t *config )
|
||
{
|
||
atp_text_t *list = (atp_text_t*)table;
|
||
config->text_align = ATP_PLACEMENT_CENTER;
|
||
config->text_color = index == 0 ? ATP_COLOR_MAGENTA : ATP_COLOR_LIGHT;
|
||
config->text = list[index];
|
||
return ATP_SUCCESS;
|
||
}
|
||
|
||
atp_error_t help_page( atp_text_t *wording, atp_counter_t length )
|
||
{
|
||
atp_tips( NULL, "返回:按A/B" );
|
||
atp_error_t res = atp_show( length, display_help, (atp_callerdata_t)wording );
|
||
atp_tips( NULL, "指引:按START" );
|
||
return res;
|
||
}
|
||
|
||
static atp_error_t disp_str( atp_callerdata_t data, atp_counter_t, atp_linecfg_t *cfg )
|
||
{
|
||
cfg->text = (atp_text_t)data;
|
||
return ATP_SUCCESS;
|
||
}
|
||
|
||
static atp_error_t select_region( atp_callerdata_t, atp_counter_t index, atp_itemcfg_t *config )
|
||
{
|
||
static char text[16];
|
||
|
||
acl_region_t sreg;
|
||
acl_chtid_t sid, id;
|
||
if( ACHTLIB_SUCCESS != acl_query_cheat_set(index, &sid, &sreg) )
|
||
{
|
||
config->text = "无效数据";
|
||
config->value = index;
|
||
return ATP_SUCCESS;
|
||
}
|
||
char *t;
|
||
switch( sreg )
|
||
{
|
||
case 'C': t = "官方中文"; break;
|
||
case 'J': t = "日文版"; break;
|
||
case 'E': t = "英文版"; break;
|
||
case 'F': t = "法语版"; break;
|
||
case 'S': t = "西班牙语"; break;
|
||
case 'I': t = "意大利语"; break;
|
||
case 'D': t = "德语版"; break;
|
||
case 'K': t = "韩文版"; break;
|
||
case 'X': case 'P': t = "欧洲语种"; break;
|
||
default: t = "其他语种"; break;
|
||
}
|
||
ee_sprintf(text, "%c-%s", 'A'+(char)index, t);
|
||
|
||
if( CCHT_NOT_INIT == info_current_cheat( &id, NULL ) )
|
||
id = 0;
|
||
|
||
config->text = text;
|
||
config->value = index;
|
||
|
||
// show saved information
|
||
if( sid == id )
|
||
{
|
||
config->extra_text = "已启用";
|
||
config->extra_text_color = ATP_COLOR_GREEN;
|
||
}
|
||
return ATP_SUCCESS;
|
||
}
|
||
|
||
static atp_error_t select_holes( atp_callerdata_t, atp_counter_t index, atp_itemcfg_t *cfg )
|
||
{
|
||
acl_text_t text;
|
||
acl_error_t res = acl_entry_get_label(index, &text);
|
||
if( res == ACHTLIB_SUCCESS )
|
||
cfg->text = text;
|
||
else cfg->text = "无效数据";
|
||
cfg->value = 1+index;
|
||
|
||
// show saved information
|
||
acl_entryid_t id;
|
||
if( CCHT_OK == get_current_cheat( index, &id ) && ENT_USING(id) )
|
||
{
|
||
cfg->extra_text = "开";
|
||
cfg->extra_text_color = ATP_COLOR_GREEN;
|
||
}
|
||
|
||
return ATP_SUCCESS;
|
||
}
|
||
|
||
static atp_error_t select_onoff( atp_callerdata_t data, atp_counter_t index, atp_itemcfg_t *cfg )
|
||
{
|
||
acl_entryid_t eid = (acl_entryid_t)data;
|
||
cfg->text = index == 0 ? "不开启" : "开启";
|
||
cfg->value = index << 16 | eid;
|
||
|
||
// show saved information
|
||
if( CCHT_OK == include_current_cheat((acl_entryid_t)cfg->value) )
|
||
{
|
||
cfg->extra_text = "选中";
|
||
cfg->extra_text_color = ATP_COLOR_GREEN;
|
||
}
|
||
return ATP_SUCCESS;
|
||
}
|
||
|
||
static atp_error_t select_keys( atp_callerdata_t data, atp_counter_t index, atp_itemcfg_t *cfg )
|
||
{
|
||
acl_text_t text;
|
||
if( index == 0 )
|
||
{
|
||
cfg->text = "不开启";
|
||
}
|
||
else
|
||
{
|
||
acl_error_t res = acl_entry_get_label(index-1, &text);
|
||
if( res == ACHTLIB_SUCCESS )
|
||
cfg->text = text;
|
||
else cfg->text = "无效数据";
|
||
}
|
||
acl_entryid_t eid = (acl_entryid_t)data;
|
||
cfg->value = index<<16 | eid;
|
||
|
||
if( CCHT_OK == include_current_cheat((acl_entryid_t)cfg->value) )
|
||
{
|
||
cfg->extra_text = "选中";
|
||
cfg->extra_text_color = ATP_COLOR_GREEN;
|
||
}
|
||
return ATP_SUCCESS;
|
||
}
|
||
|
||
extern atp_error_t oaf_config_page();
|
||
|
||
#define DIRBUFFSIZE 512
|
||
#define WAIT_ON_ACT( act ) ( ATP_POWER_OFF == (act) ) ? ATP_POWER_OFF : ATP_PAGE_REFRESH
|
||
#define WAIT_ON_ERRPAGE( page ) WAIT_ON_ACT( atp_show(1, (page), NULL) )
|
||
|
||
static atp_pageopt_t serve_on_key( atp_callerdata_t data, atp_counter_t index, atp_boolean_t x, atp_boolean_t y, atp_boolean_t l, atp_boolean_t r, atp_boolean_t start, atp_boolean_t select )
|
||
{
|
||
if( start )
|
||
{
|
||
atp_tips( "", NULL );
|
||
return WAIT_ON_ACT( help_page( folder_help, sizeof(folder_help)/sizeof(atp_text_t) ) );
|
||
}
|
||
else if( select )
|
||
{
|
||
return WAIT_ON_ACT( oaf_config_page() );
|
||
}
|
||
else if( x )
|
||
{
|
||
void* *dat = (void **)data;
|
||
DirList const *dList = (DirList*)dat[0];
|
||
char *path = (char*)dat[1];
|
||
|
||
const char *file = &dList->ptrs[index][1];
|
||
int pathlen = strlen(path);
|
||
int filelen = strlen(file);
|
||
|
||
if( pathlen + filelen > DIRBUFFSIZE - 1)
|
||
{
|
||
return WAIT_ON_ERRPAGE( display_toolong );
|
||
}
|
||
else if( ENT_TYPE_DIR == dList->ptrs[index][0] )
|
||
{
|
||
return WAIT_ON_ERRPAGE( display_empty );
|
||
}
|
||
|
||
path[pathlen] = '/';
|
||
safeStrcpy( path+pathlen+1, file, DIRBUFFSIZE - filelen );
|
||
|
||
#define RECOVER_PATH path[pathlen] = '\0'
|
||
// 确定game serial
|
||
char serial[5];
|
||
if( RES_OK != rom_get_serial(path, serial) )
|
||
{
|
||
RECOVER_PATH;
|
||
return WAIT_ON_ERRPAGE( display_noserial );
|
||
}
|
||
else RECOVER_PATH;
|
||
|
||
acl_count_t len;
|
||
if( ACHTLIB_SUCCESS != acl_open_lib( "gba.acl" ) )
|
||
{
|
||
return WAIT_ON_ERRPAGE( display_openlib );
|
||
}
|
||
if( ACHTLIB_SUCCESS != acl_select_game( serial, 0, &len ) )
|
||
{
|
||
acl_close_lib();
|
||
return WAIT_ON_ERRPAGE( display_selcht );
|
||
}
|
||
if( len == 0 )
|
||
{
|
||
acl_close_lib();
|
||
return WAIT_ON_ERRPAGE( display_nocheat );
|
||
}
|
||
|
||
// 显示配置页面
|
||
atp_error_t res;
|
||
atp_itemval_t item;
|
||
acl_elemlen_t cnt;
|
||
|
||
#define DISP_DONE 0
|
||
#define DISP_REGION 1
|
||
#define DISP_HOLES 2
|
||
#define DISP_KEYS 3
|
||
|
||
uint8_t status = DISP_REGION;
|
||
atp_counter_t defi = 0;
|
||
acl_entryid_t eid = 0;
|
||
|
||
while( status != DISP_DONE )
|
||
{
|
||
if( status == DISP_REGION )
|
||
{
|
||
res = atp_select("请谨慎使用金手指!金手指可能会引起卡顿、死机、损坏存档等现象。"
|
||
"选择一个金手指配置", len, select_region, NULL, NULL, defi, 0, &item );
|
||
if( res == ATP_SUCCESS )
|
||
{
|
||
defi = item;
|
||
acl_chtid_t sid;
|
||
acl_elemlen_t len;
|
||
u32 ccid, cclen;
|
||
acl_query_cheat_set((acl_index_t)item, &sid, NULL );
|
||
acl_select_cheat_set( sid );
|
||
acl_select_entry(0, &len);
|
||
if( CCHT_OK == info_current_cheat(&ccid, &cclen) )
|
||
{
|
||
// SKIP the init process when id and len is the same
|
||
if( ccid != sid || cclen != len )
|
||
init_current_cheat( sid, len );
|
||
}
|
||
else init_current_cheat( sid, len );
|
||
eid = 0;
|
||
status = DISP_HOLES;
|
||
}
|
||
else if( res == ATP_NO_ACTION)
|
||
{
|
||
status = DISP_DONE;
|
||
}
|
||
else break;
|
||
}
|
||
else if( status == DISP_HOLES )
|
||
{
|
||
acl_select_entry(0, &cnt);
|
||
res = atp_select("选择一个金手指项目", cnt, select_holes, NULL, NULL, eid > 0 ? (eid&0xffff)-1 : 0, 0, &item);
|
||
if( res == ATP_SUCCESS )
|
||
{
|
||
eid = (acl_entryid_t)item;
|
||
acl_select_entry(eid, &cnt);
|
||
status = DISP_KEYS;
|
||
}
|
||
else if( res == ATP_NO_ACTION )
|
||
{
|
||
status = DISP_REGION;
|
||
}
|
||
else break;
|
||
}
|
||
else // DISP_KEYS
|
||
{
|
||
res = atp_select("选择此项目对应的设置", cnt==0 ? 2:cnt, cnt==0 ? select_onoff : select_keys, NULL, (atp_callerdata_t)eid, 0, 0, &item);
|
||
if( res == ATP_SUCCESS )
|
||
{
|
||
put_current_cheat( (acl_entryid_t)item );
|
||
status = DISP_HOLES;
|
||
}
|
||
else if( res == ATP_NO_ACTION )
|
||
{
|
||
status = DISP_HOLES;
|
||
}
|
||
else break;
|
||
}
|
||
}
|
||
|
||
acl_close_lib();
|
||
return WAIT_ON_ACT( res );
|
||
}
|
||
return ATP_PAGE_NOOPTION;
|
||
}
|
||
|
||
#define PATH_SAME 0
|
||
#define PATH_PUSH 1
|
||
#define PATH_POP 2
|
||
#define DIRUP { \
|
||
char *tmpPathPtr = curDir + pathLen; \
|
||
while(*--tmpPathPtr != '/'); \
|
||
if(*(tmpPathPtr - 1) == ':') tmpPathPtr++; \
|
||
*tmpPathPtr = '\0'; \
|
||
path_recover = PATH_POP; \
|
||
upFrom = tmpPathPtr + 1; \
|
||
}
|
||
Result browseFiles(const char *const basePath, char selected[512])
|
||
{
|
||
if(basePath == NULL || selected == NULL) return RES_INVALID_ARG;
|
||
// TODO: Check if the base path is empty.
|
||
|
||
char *curDir = (char*)malloc(DIRBUFFSIZE);
|
||
if(curDir == NULL) return RES_OUT_OF_MEM;
|
||
safeStrcpy(curDir, basePath, 512);
|
||
|
||
DirList *const dList = (DirList*)malloc(sizeof(DirList));
|
||
if(dList == NULL) return RES_OUT_OF_MEM;
|
||
|
||
Result res;
|
||
if((res = scanDir(curDir, dList, ".gba", NULL, NULL)) != RES_OK)
|
||
{
|
||
if( res == RES_OUT_OF_RANGE )
|
||
atp_show(1, display_pathfull, NULL);
|
||
else if( res == RES_PATH_TOO_LONG )
|
||
atp_show(1, display_longname, NULL );
|
||
else goto end;
|
||
}
|
||
|
||
const char *upFrom = NULL;
|
||
int selecting = 0;
|
||
while( 1 )
|
||
{
|
||
atp_itemval_t value;
|
||
atp_error_t error;
|
||
int path_recover = PATH_SAME;
|
||
const u32 count = dList->num;
|
||
|
||
u32 pathLen = strlen(curDir);
|
||
|
||
if( count > 0 )
|
||
{
|
||
GFX_waitForVBlank0();
|
||
hidScanInput();
|
||
atp_tips( NULL, "指引:按START" );
|
||
void *cust[2] = {dList, curDir};
|
||
error = atp_select( curDir, count, display_folder, serve_on_key, (atp_callerdata_t)cust, selecting, 0, &value );
|
||
}
|
||
else error = atp_show( 1, display_empty, NULL );
|
||
|
||
if( error == ATP_POWER_OFF )
|
||
{
|
||
res = error;
|
||
break;
|
||
}
|
||
else if( error == ATP_SUCCESS )
|
||
{// 进入目录或文件
|
||
if( count == 0 )
|
||
{
|
||
DIRUP;
|
||
}
|
||
else
|
||
{
|
||
const char *fname = &dList->ptrs[value][1];
|
||
u32 namelen = strlen( fname ) + 1;
|
||
if( namelen + pathLen > DIRBUFFSIZE-1 )
|
||
{
|
||
atp_show( 1, display_toolong, NULL );
|
||
continue;
|
||
}
|
||
curDir[pathLen++] = '/';
|
||
safeStrcpy( curDir+pathLen, fname, DIRBUFFSIZE - namelen );
|
||
if( *dList->ptrs[value] == ENT_TYPE_FILE )
|
||
{
|
||
safeStrcpy( selected, curDir, 512 );
|
||
break;
|
||
}
|
||
else
|
||
{
|
||
path_recover = PATH_PUSH;
|
||
selecting = 0;
|
||
upFrom = NULL;
|
||
}
|
||
}
|
||
}
|
||
else if( error == ATP_NO_ACTION )
|
||
{// 上层目录
|
||
if( strcmp(curDir, FS_DRIVE_NAMES) == 0 )
|
||
{
|
||
atp_tips("没有上层目录", NULL);
|
||
help_page( folder_help, sizeof(folder_help)/sizeof(atp_text_t) );
|
||
atp_tips("", NULL);
|
||
}
|
||
else DIRUP;
|
||
}
|
||
|
||
if( path_recover )
|
||
{
|
||
if(RES_OK != (res = scanDir(curDir, dList, ".gba", upFrom, &selecting)) )
|
||
{
|
||
if( res == RES_OUT_OF_RANGE )
|
||
atp_show( 1, display_pathfull, NULL ); // give a warning for user, and keep show the list
|
||
else if( res == RES_PATH_TOO_LONG )
|
||
atp_show( 1, display_longname, NULL );
|
||
else break;
|
||
}
|
||
path_recover = PATH_SAME;
|
||
}
|
||
}
|
||
|
||
end:
|
||
free(dList);
|
||
free(curDir);
|
||
|
||
// Clear screen.
|
||
screenClean();
|
||
|
||
return res;
|
||
}
|