mirror of
https://github.com/ddnet/ddnet.git
synced 2024-11-10 10:08:18 +00:00
Added first version of the console.
This commit is contained in:
parent
a9c90d6ac0
commit
75c8b2e9a5
|
@ -7,7 +7,6 @@
|
|||
#include "e_system.h"
|
||||
#include "e_config.h"
|
||||
|
||||
|
||||
/* buffered stream for reading lines, should perhaps be something smaller */
|
||||
typedef struct
|
||||
{
|
||||
|
|
|
@ -17,11 +17,17 @@ typedef struct
|
|||
|
||||
extern CONFIGURATION config;
|
||||
|
||||
void config_init();
|
||||
void config_set(const char *line);
|
||||
void config_reset();
|
||||
void config_load(const char *filename);
|
||||
void config_save(const char *filename);
|
||||
|
||||
typedef int (*config_int_getter)(CONFIGURATION *c);
|
||||
typedef const char *(*config_str_getter)(CONFIGURATION *c);
|
||||
typedef void (*config_int_setter)(CONFIGURATION *c, int val);
|
||||
typedef void (*config_str_setter)(CONFIGURATION *c, const char *str);
|
||||
|
||||
#define MACRO_CONFIG_INT(name,def,min,max) int config_get_ ## name (CONFIGURATION *c);
|
||||
#define MACRO_CONFIG_STR(name,len,def) const char *config_get_ ## name (CONFIGURATION *c);
|
||||
#include "e_config_variables.h"
|
||||
|
|
368
src/engine/e_console.c
Normal file
368
src/engine/e_console.c
Normal file
|
@ -0,0 +1,368 @@
|
|||
#include "e_console.h"
|
||||
#include "e_config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
enum
|
||||
{
|
||||
STATE_START,
|
||||
STATE_INT,
|
||||
STATE_FLOAT,
|
||||
STATE_POT_FLOAT,
|
||||
STATE_STRING,
|
||||
STATE_QUOTED,
|
||||
STATE_ESCAPE
|
||||
};
|
||||
|
||||
static const char *store_string(struct lexer_result *res, const char *str, int len)
|
||||
{
|
||||
const char *ptr = res->next_string;
|
||||
|
||||
memcpy(res->next_string, str, len);
|
||||
res->next_string[len] = 0;
|
||||
res->next_string += len+1;
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
static void save_token(struct lexer_result *res, int *index, const char **start, const char *end, int *state, int type)
|
||||
{
|
||||
/* printf("Saving token with length %d\n", end - *start); */
|
||||
struct token *tok = &res->tokens[*index];
|
||||
tok->stored_string = store_string(res, *start, end - *start);
|
||||
tok->type = type;
|
||||
++res->num_tokens;
|
||||
|
||||
*start = end + 1;
|
||||
*state = STATE_START;
|
||||
++*index;
|
||||
}
|
||||
|
||||
int digit(char c)
|
||||
{
|
||||
return '0' <= c && c <= '9';
|
||||
}
|
||||
|
||||
int lex(const char *line, struct lexer_result *res)
|
||||
{
|
||||
int state = STATE_START, i = 0;
|
||||
int length_left = CONSOLE_MAX_STR_LENGTH;
|
||||
const char *start, *c;
|
||||
res->num_tokens = 0;
|
||||
|
||||
memset(res, 0, sizeof(*res));
|
||||
res->next_string = res->string_storage;
|
||||
|
||||
for (c = start = line; *c != '\0' && res->num_tokens < MAX_TOKENS && length_left; ++c, --length_left)
|
||||
{
|
||||
/* printf("State: %d\n", state); */
|
||||
switch (state)
|
||||
{
|
||||
case STATE_START:
|
||||
if (*c == ' ')
|
||||
start = c + 1;
|
||||
else if (digit(*c))
|
||||
state = STATE_INT;
|
||||
else if (*c == '.')
|
||||
state = STATE_POT_FLOAT;
|
||||
else
|
||||
state = STATE_STRING;
|
||||
break;
|
||||
|
||||
case STATE_INT:
|
||||
if (digit(*c))
|
||||
;
|
||||
else if (*c == '.')
|
||||
state = STATE_FLOAT;
|
||||
else if (*c == ' ')
|
||||
save_token(res, &i, &start, c, &state, TOKEN_INT);
|
||||
else
|
||||
state = STATE_STRING;
|
||||
break;
|
||||
|
||||
case STATE_FLOAT:
|
||||
if (digit(*c))
|
||||
;
|
||||
else if (*c == ' ')
|
||||
save_token(res, &i, &start, c, &state, TOKEN_FLOAT);
|
||||
else
|
||||
state = STATE_STRING;
|
||||
break;
|
||||
|
||||
case STATE_POT_FLOAT:
|
||||
if (digit(*c))
|
||||
state = STATE_FLOAT;
|
||||
else if (*c == ' ')
|
||||
save_token(res, &i, &start, c, &state, TOKEN_STRING);
|
||||
else
|
||||
state = STATE_STRING;
|
||||
break;
|
||||
|
||||
case STATE_STRING:
|
||||
if (*c == ' ')
|
||||
save_token(res, &i, &start, c, &state, TOKEN_STRING);
|
||||
break;
|
||||
|
||||
case STATE_QUOTED:
|
||||
if (*c == '"')
|
||||
save_token(res, &i, &start, c, &state, TOKEN_STRING);
|
||||
else if (*c == '\\')
|
||||
state = STATE_ESCAPE;
|
||||
break;
|
||||
|
||||
case STATE_ESCAPE:
|
||||
if (*c != ' ')
|
||||
state = STATE_QUOTED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case STATE_INT:
|
||||
save_token(res, &i, &start, c, &state, TOKEN_INT);
|
||||
break;
|
||||
case STATE_FLOAT:
|
||||
save_token(res, &i, &start, c, &state, TOKEN_FLOAT);
|
||||
break;
|
||||
case STATE_STRING:
|
||||
case STATE_QUOTED:
|
||||
case STATE_POT_FLOAT:
|
||||
save_token(res, &i, &start, c, &state, TOKEN_STRING);
|
||||
break;
|
||||
case STATE_ESCAPE:
|
||||
puts("LOL MALFORMED");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int extract_result_string(struct lexer_result *result, int index, const char **str)
|
||||
{
|
||||
if (index < 0 || index >= result->num_tokens)
|
||||
return -1;
|
||||
else
|
||||
{
|
||||
struct token *t = &result->tokens[index];
|
||||
|
||||
*str = t->stored_string;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int extract_result_int(struct lexer_result *result, int index, int *i)
|
||||
{
|
||||
if (index < 0 || index >= result->num_tokens)
|
||||
return -1;
|
||||
else
|
||||
{
|
||||
struct token *t = &result->tokens[index];
|
||||
const char *str;
|
||||
|
||||
if (t->type != TOKEN_INT)
|
||||
return -2;
|
||||
|
||||
extract_result_string(result, index, &str);
|
||||
|
||||
*i = atoi(str);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int extract_result_float(struct lexer_result *result, int index, float *f)
|
||||
{
|
||||
if (index < 0 || index >= result->num_tokens)
|
||||
return -1;
|
||||
else
|
||||
{
|
||||
struct token *t = &result->tokens[index];
|
||||
const char *str;
|
||||
|
||||
if (t->type != TOKEN_INT && t->type != TOKEN_FLOAT)
|
||||
return -2;
|
||||
|
||||
extract_result_string(result, index, &str);
|
||||
|
||||
*f = atof(str);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static COMMAND *first_command = 0x0;
|
||||
|
||||
COMMAND *console_find_command(const char *name)
|
||||
{
|
||||
COMMAND *cmd;
|
||||
for (cmd = first_command; cmd; cmd = cmd->next)
|
||||
if (strcmp(cmd->name, name) == 0)
|
||||
return cmd;
|
||||
|
||||
return 0x0;
|
||||
}
|
||||
|
||||
void console_register(COMMAND *cmd)
|
||||
{
|
||||
cmd->next = first_command;
|
||||
first_command = cmd;
|
||||
}
|
||||
|
||||
|
||||
static int console_validate(COMMAND *command, struct lexer_result *result)
|
||||
{
|
||||
const char *c = command->params;
|
||||
int i = 1;
|
||||
|
||||
const char *dummy_s;
|
||||
int dummy_i;
|
||||
float dummy_f;
|
||||
|
||||
while (*c && *c != '?')
|
||||
{
|
||||
switch (*c)
|
||||
{
|
||||
case 's':
|
||||
if (extract_result_string(result, i, &dummy_s))
|
||||
return -1;
|
||||
break;
|
||||
case 'i':
|
||||
if (extract_result_int(result, i, &dummy_i))
|
||||
return -1;
|
||||
break;
|
||||
case 'f':
|
||||
if (extract_result_float(result, i, &dummy_f))
|
||||
return -1;
|
||||
break;
|
||||
default:
|
||||
// unknown char, so just continue...
|
||||
c++;
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
c++;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void (*print_callback)(const char *) = 0x0;
|
||||
|
||||
void console_register_print_callback(void (*callback)(const char *))
|
||||
{
|
||||
print_callback = callback;
|
||||
}
|
||||
|
||||
void console_print(const char *str)
|
||||
{
|
||||
if (print_callback)
|
||||
print_callback(str);
|
||||
}
|
||||
|
||||
void console_execute(const char *str)
|
||||
{
|
||||
struct lexer_result result;
|
||||
int error;
|
||||
|
||||
if ((error = lex(str, &result)))
|
||||
printf("ERROR: %d\n", error);
|
||||
else if (result.num_tokens > 0)
|
||||
{
|
||||
const char *name;
|
||||
extract_result_string(&result, 0, &name);
|
||||
|
||||
COMMAND *command = console_find_command(name);
|
||||
|
||||
if (command)
|
||||
{
|
||||
if (console_validate(command, &result))
|
||||
{
|
||||
char buf[256];
|
||||
sprintf(buf, "Invalid arguments... Usage: %s %s", command->name, command->params);
|
||||
console_print(buf);
|
||||
}
|
||||
else
|
||||
command->callback(&result, command->user_data);
|
||||
}
|
||||
else
|
||||
{
|
||||
char buf[256];
|
||||
sprintf(buf, "No such command: %s.", name);
|
||||
console_print(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void echo_command(struct lexer_result *result, void *user_data)
|
||||
{
|
||||
const char *str;
|
||||
extract_result_string(result, 1, &str);
|
||||
|
||||
console_print(str);
|
||||
}
|
||||
|
||||
|
||||
struct int_variable_data
|
||||
{
|
||||
config_int_getter getter;
|
||||
config_int_setter setter;
|
||||
};
|
||||
|
||||
struct str_variable_data
|
||||
{
|
||||
config_str_getter getter;
|
||||
config_str_setter setter;
|
||||
};
|
||||
|
||||
static void int_variable_command(struct lexer_result *result, void *user_data)
|
||||
{
|
||||
struct int_variable_data *data = (struct int_variable_data *)user_data;
|
||||
int new_val;
|
||||
|
||||
if (extract_result_int(result, 1, &new_val))
|
||||
{
|
||||
char buf[256];
|
||||
sprintf(buf, "Value: %d", data->getter(&config));
|
||||
console_print(buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
data->setter(&config, new_val);
|
||||
}
|
||||
}
|
||||
|
||||
static void str_variable_command(struct lexer_result *result, void *user_data)
|
||||
{
|
||||
struct str_variable_data *data = (struct str_variable_data *)user_data;
|
||||
const char *new_val;
|
||||
|
||||
if (extract_result_string(result, 1, &new_val))
|
||||
{
|
||||
char buf[256];
|
||||
sprintf(buf, "Value: %s", data->getter(&config));
|
||||
console_print(buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
data->setter(&config, new_val);
|
||||
}
|
||||
}
|
||||
|
||||
void console_init()
|
||||
{
|
||||
MACRO_REGISTER_COMMAND("echo", "s", echo_command, 0x0);
|
||||
|
||||
#define MACRO_CONFIG_INT(name,def,min,max) { static struct int_variable_data data = { &config_get_ ## name, &config_set_ ## name }; MACRO_REGISTER_COMMAND(#name, "?i", int_variable_command, &data) }
|
||||
#define MACRO_CONFIG_STR(name,len,def) { static struct str_variable_data data = { &config_get_ ## name, &config_set_ ## name }; MACRO_REGISTER_COMMAND(#name, "?s", str_variable_command, &data) }
|
||||
|
||||
#include "e_config_variables.h"
|
||||
|
||||
#undef MACRO_CONFIG_INT
|
||||
#undef MACRO_CONFIG_STR
|
||||
}
|
64
src/engine/e_console.h
Normal file
64
src/engine/e_console.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
#ifndef _CONSOLE_H
|
||||
#define _CONSOLE_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"{
|
||||
#endif
|
||||
|
||||
#define CONSOLE_MAX_STR_LENGTH 255
|
||||
/* the maximum number of tokens occurs in a string of length CONSOLE_MAX_STR_LENGTH with tokens size 1 separated by single spaces */
|
||||
#define MAX_TOKENS (CONSOLE_MAX_STR_LENGTH+1)/2
|
||||
|
||||
enum
|
||||
{
|
||||
TOKEN_INT,
|
||||
TOKEN_FLOAT,
|
||||
TOKEN_STRING
|
||||
};
|
||||
|
||||
struct token
|
||||
{
|
||||
int type;
|
||||
const char *stored_string;
|
||||
};
|
||||
|
||||
struct lexer_result
|
||||
{
|
||||
char string_storage[CONSOLE_MAX_STR_LENGTH+1];
|
||||
char *next_string;
|
||||
|
||||
struct token tokens[MAX_TOKENS];
|
||||
unsigned int num_tokens;
|
||||
};
|
||||
|
||||
int lex(const char *line, struct lexer_result *result);
|
||||
|
||||
int extract_result_string(struct lexer_result *result, int index, const char **str);
|
||||
int extract_result_int(struct lexer_result *result, int index, int *i);
|
||||
int extract_result_float(struct lexer_result *result, int index, float *f);
|
||||
|
||||
typedef void (*console_callback)(struct lexer_result *result, void *user_data);
|
||||
|
||||
typedef struct COMMAND
|
||||
{
|
||||
const char *name;
|
||||
const char *params;
|
||||
console_callback callback;
|
||||
void *user_data;
|
||||
struct COMMAND *next;
|
||||
|
||||
} COMMAND;
|
||||
|
||||
void console_init();
|
||||
void console_register(COMMAND *cmd);
|
||||
void console_execute(const char *str);
|
||||
void console_print(const char *str);
|
||||
void console_register_print_callback(void (*callback)(const char *));
|
||||
|
||||
#define MACRO_REGISTER_COMMAND(name, params, func, ptr) { static COMMAND cmd = { name, params, func, ptr, 0x0 }; console_register(&cmd); }
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
|
@ -5,6 +5,7 @@
|
|||
#include <engine/e_system.h>
|
||||
#include <engine/e_interface.h>
|
||||
#include <engine/e_config.h>
|
||||
#include <engine/e_console.h>
|
||||
|
||||
static char application_save_path[512] = {0};
|
||||
|
||||
|
@ -39,6 +40,9 @@ void engine_init(const char *appname, int argc, char **argv)
|
|||
fs_makedir(path);
|
||||
}
|
||||
}
|
||||
|
||||
/* init console */
|
||||
console_init();
|
||||
|
||||
/* reset the config */
|
||||
config_reset();
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include "e_detect.h"
|
||||
#include "e_system.h"
|
||||
#include "e_console.h"
|
||||
|
||||
#if defined(CONF_FAMILY_UNIX)
|
||||
#include <sys/time.h>
|
||||
|
@ -88,6 +89,18 @@ void dbg_msg(const char *sys, const char *fmt, ...)
|
|||
va_end(args);
|
||||
printf("\n");
|
||||
fflush(stdout);
|
||||
|
||||
{
|
||||
char str[2048];
|
||||
|
||||
sprintf(str, "[%s]: ", sys);
|
||||
|
||||
va_start(args, fmt);
|
||||
vsprintf(str+strlen(str), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
console_print(str);
|
||||
}
|
||||
}
|
||||
|
||||
int memory_alloced = 0;
|
||||
|
|
|
@ -20,6 +20,7 @@ extern "C" {
|
|||
#include "gc_client.h"
|
||||
#include "gc_render.h"
|
||||
#include "gc_anim.h"
|
||||
#include "gc_console.h"
|
||||
|
||||
struct data_container *data = 0;
|
||||
static int64 debug_firedelay = 0;
|
||||
|
|
136
src/game/client/gc_console.cpp
Normal file
136
src/game/client/gc_console.cpp
Normal file
|
@ -0,0 +1,136 @@
|
|||
#include "gc_console.h"
|
||||
|
||||
extern "C" {
|
||||
#include <engine/e_system.h>
|
||||
#include <engine/e_interface.h>
|
||||
#include <engine/e_config.h>
|
||||
#include <engine/e_console.h>
|
||||
#include <engine/client/ec_font.h>
|
||||
}
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
||||
#include "gc_ui.h"
|
||||
|
||||
static unsigned int console_input_len = 0;
|
||||
static char console_input[256] = {0};
|
||||
static int active = 0;
|
||||
|
||||
static char backlog[256][256] = {0};
|
||||
static int backlog_len;
|
||||
|
||||
static void client_console_print(const char *str)
|
||||
{
|
||||
int len = strlen(str);
|
||||
|
||||
if (len > 255)
|
||||
len = 255;
|
||||
|
||||
if (backlog_len >= 256)
|
||||
{
|
||||
puts("console backlog full");
|
||||
}
|
||||
|
||||
memcpy(backlog[backlog_len], str, len);
|
||||
backlog[backlog_len][len] = 0;
|
||||
|
||||
backlog_len++;
|
||||
|
||||
//dbg_msg("console", "FROM CLIENT!! %s", str);
|
||||
}
|
||||
|
||||
void client_console_init()
|
||||
{
|
||||
console_register_print_callback(client_console_print);
|
||||
}
|
||||
|
||||
void console_handle_input()
|
||||
{
|
||||
for(int i = 0; i < inp_num_events(); i++)
|
||||
{
|
||||
INPUTEVENT e = inp_get_event(i);
|
||||
|
||||
if (e.key == KEY_F3)
|
||||
{
|
||||
console_toggle();
|
||||
}
|
||||
|
||||
if (active)
|
||||
{
|
||||
if (!(e.ch >= 0 && e.ch < 32))
|
||||
{
|
||||
if (console_input_len < sizeof(console_input) - 1)
|
||||
{
|
||||
console_input[console_input_len] = e.ch;
|
||||
console_input[console_input_len+1] = 0;
|
||||
console_input_len++;
|
||||
}
|
||||
}
|
||||
|
||||
if(e.key == KEY_BACKSPACE)
|
||||
{
|
||||
if(console_input_len > 0)
|
||||
{
|
||||
console_input[console_input_len-1] = 0;
|
||||
console_input_len--;
|
||||
}
|
||||
}
|
||||
else if(e.key == KEY_ENTER)
|
||||
{
|
||||
console_execute(console_input);
|
||||
console_input[0] = 0;
|
||||
console_input_len = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (active)
|
||||
inp_clear_events();
|
||||
}
|
||||
|
||||
void console_toggle()
|
||||
{
|
||||
active ^= 1;
|
||||
}
|
||||
|
||||
void console_render()
|
||||
{
|
||||
RECT screen = *ui_screen();
|
||||
float console_height = screen.h*3/5.0f;
|
||||
gfx_mapscreen(screen.x, screen.y, screen.w, screen.h);
|
||||
|
||||
gfx_texture_set(-1);
|
||||
gfx_quads_begin();
|
||||
gfx_setcolor(0.4,0.2,0.2,0.8);
|
||||
gfx_quads_drawTL(0,0,screen.w,console_height);
|
||||
gfx_quads_end();
|
||||
|
||||
{
|
||||
float font_size = 12.0f;
|
||||
float row_spacing = font_size*1.4f;
|
||||
float width = gfx_text_width(0, 12, console_input, -1);
|
||||
float x = 3, y = console_height - row_spacing - 2;
|
||||
int backlog_index = backlog_len-1;
|
||||
|
||||
gfx_text(0, x, y, font_size, console_input, -1);
|
||||
gfx_text(0, x+width+1, y, font_size, "_", -1);
|
||||
|
||||
y -= row_spacing;
|
||||
|
||||
while (y > 0.0f && backlog_index >= 0)
|
||||
{
|
||||
const char *line = backlog[backlog_index];
|
||||
gfx_text(0, x, y, font_size, line, -1);
|
||||
|
||||
backlog_index--;
|
||||
y -= row_spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int console_active()
|
||||
{
|
||||
return active;
|
||||
}
|
||||
|
10
src/game/client/gc_console.h
Normal file
10
src/game/client/gc_console.h
Normal file
|
@ -0,0 +1,10 @@
|
|||
#ifndef _GC_CONSOLE_H
|
||||
#define _GC_CONSOLE_H
|
||||
|
||||
void console_handle_input();
|
||||
void console_toggle();
|
||||
void console_render();
|
||||
int console_active();
|
||||
void client_console_init();
|
||||
|
||||
#endif
|
|
@ -17,6 +17,7 @@ extern "C" {
|
|||
#include "gc_skin.h"
|
||||
#include "gc_render.h"
|
||||
#include "gc_map_image.h"
|
||||
#include "gc_console.h"
|
||||
|
||||
extern unsigned char internal_data[];
|
||||
|
||||
|
@ -36,6 +37,8 @@ extern "C" void modc_init()
|
|||
gfx_text_set_default_font(&default_font);
|
||||
|
||||
menu_init();
|
||||
|
||||
client_console_init();
|
||||
|
||||
// setup sound channels
|
||||
snd_set_channel(CHN_GUI, 1.0f, 0.0f);
|
||||
|
@ -288,10 +291,10 @@ extern "C" void modc_newsnapshot()
|
|||
client_datas[i].update_render_info();
|
||||
}
|
||||
|
||||
|
||||
|
||||
extern "C" void modc_render()
|
||||
{
|
||||
console_handle_input();
|
||||
|
||||
// this should be moved around abit
|
||||
if(client_state() == CLIENTSTATE_ONLINE)
|
||||
{
|
||||
|
@ -310,6 +313,11 @@ extern "C" void modc_render()
|
|||
else // if (client_state() != CLIENTSTATE_CONNECTING && client_state() != CLIENTSTATE_LOADING)
|
||||
{
|
||||
menu_render();
|
||||
if (console_active())
|
||||
{
|
||||
console_render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -23,6 +23,7 @@ MACRO_CONFIG_INT(key_teamchat, 'Y', 32, 512)
|
|||
MACRO_CONFIG_INT(key_console, 256+2, 32, 512)
|
||||
MACRO_CONFIG_INT(key_remoteconsole, 256+3, 32, 512)
|
||||
|
||||
MACRO_CONFIG_INT(key_toggleconsole, 256+4, 32, 512)
|
||||
|
||||
MACRO_CONFIG_INT(dbg_bots, 0, 0, 11)
|
||||
|
||||
|
|
Loading…
Reference in a new issue