2017-09-12 03:28:15 +02:00
|
|
|
/*
|
2019-08-03 19:43:48 +02:00
|
|
|
* This software is licensed under the terms of the MIT License.
|
2017-09-12 03:28:15 +02:00
|
|
|
* See COPYING for further information.
|
|
|
|
* ---
|
2024-05-16 23:30:41 +02:00
|
|
|
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
|
|
|
|
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
|
2017-09-12 03:28:15 +02:00
|
|
|
*/
|
|
|
|
|
2019-03-18 05:41:12 +01:00
|
|
|
#include "cli.h"
|
2024-05-17 04:41:28 +02:00
|
|
|
|
|
|
|
#include "cutscenes/cutscene.h"
|
|
|
|
#include "cutscenes/scenes.h"
|
2017-04-02 10:16:52 +02:00
|
|
|
#include "difficulty.h"
|
|
|
|
#include "log.h"
|
|
|
|
#include "plrmodes.h"
|
2024-05-17 04:41:28 +02:00
|
|
|
#include "stageinfo.h"
|
|
|
|
#include "util.h"
|
|
|
|
#include "util/env.h"
|
|
|
|
#include "util/io.h"
|
2020-04-23 16:47:42 +02:00
|
|
|
#include "version.h"
|
2024-05-17 04:41:28 +02:00
|
|
|
|
|
|
|
#include <getopt.h>
|
2017-04-02 10:16:52 +02:00
|
|
|
|
2021-06-16 00:39:48 +02:00
|
|
|
struct TsOption { struct option opt; const char *help; const char *argname; };
|
2017-04-02 10:16:52 +02:00
|
|
|
|
2019-09-21 15:39:05 +02:00
|
|
|
enum {
|
|
|
|
OPT_RENDERER = INT_MIN,
|
2020-11-28 11:11:10 +01:00
|
|
|
OPT_CUTSCENE,
|
|
|
|
OPT_CUTSCENE_LIST,
|
|
|
|
OPT_FORCE_INTRO,
|
2021-06-16 00:39:48 +02:00
|
|
|
OPT_REREPLAY,
|
2023-05-08 04:23:08 +02:00
|
|
|
OPT_POPCACHE,
|
2023-05-29 00:48:50 +02:00
|
|
|
OPT_UNLOCKALL,
|
2019-09-21 15:39:05 +02:00
|
|
|
};
|
|
|
|
|
2017-04-02 10:16:52 +02:00
|
|
|
static void print_help(struct TsOption* opts) {
|
2019-04-22 22:14:50 +02:00
|
|
|
tsfprintf(stdout, "Usage: taisei [OPTIONS]\nTaisei is an open source Tōhō Project fangame.\n\nOptions:\n");
|
2017-04-02 10:16:52 +02:00
|
|
|
int margin = 20;
|
|
|
|
for(struct TsOption *opt = opts; opt->opt.name; opt++) {
|
2019-09-21 15:39:05 +02:00
|
|
|
if(opt->opt.val > 0) {
|
|
|
|
tsfprintf(stdout, " -%c, --%s ", opt->opt.val, opt->opt.name);
|
|
|
|
} else {
|
|
|
|
tsfprintf(stdout, " --%s ", opt->opt.name);
|
|
|
|
}
|
|
|
|
|
2017-04-02 10:16:52 +02:00
|
|
|
int length = margin-(int)strlen(opt->opt.name);
|
2019-09-21 15:39:05 +02:00
|
|
|
|
2017-04-02 10:16:52 +02:00
|
|
|
if(opt->argname) {
|
2017-04-03 01:10:16 +02:00
|
|
|
tsfprintf(stdout, "%s", opt->argname);
|
2017-04-02 10:16:52 +02:00
|
|
|
length -= (int)strlen(opt->argname);
|
|
|
|
}
|
2019-09-21 15:39:05 +02:00
|
|
|
|
|
|
|
for(int i = 0; i < length; i++) {
|
|
|
|
fputc(' ', stdout);
|
|
|
|
}
|
|
|
|
|
|
|
|
fputs(" ", stdout);
|
|
|
|
|
|
|
|
if(opt->argname && strchr(opt->help, '%')) {
|
2017-04-02 10:16:52 +02:00
|
|
|
tsfprintf(stdout, opt->help, opt->argname);
|
2019-09-21 15:39:05 +02:00
|
|
|
} else {
|
2017-04-03 01:10:16 +02:00
|
|
|
tsfprintf(stdout, "%s", opt->help);
|
2019-09-21 15:39:05 +02:00
|
|
|
}
|
|
|
|
|
2017-04-03 01:10:16 +02:00
|
|
|
tsfprintf(stdout, "\n");
|
2017-04-02 10:16:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int cli_args(int argc, char **argv, CLIAction *a) {
|
2019-09-21 15:39:05 +02:00
|
|
|
const char *const _renderer_list =
|
|
|
|
#define R(r) ","#r
|
|
|
|
TAISEI_BUILDCONF_RENDERER_BACKENDS
|
|
|
|
#undef R
|
|
|
|
;
|
|
|
|
|
|
|
|
char renderer_list[strlen(_renderer_list) + 2];
|
|
|
|
snprintf(renderer_list, sizeof(renderer_list), "{%s}", _renderer_list+1);
|
|
|
|
|
2018-04-18 01:17:28 +02:00
|
|
|
struct TsOption taisei_opts[] = {
|
2020-04-23 16:47:42 +02:00
|
|
|
{{"replay", required_argument, 0, 'r'}, "Play a replay from %s", "FILE"},
|
2021-06-16 00:39:48 +02:00
|
|
|
{{"verify-replay", required_argument, 0, 'R'}, "Play a replay from %s in headless mode, crash as soon as it desyncs unless --rereplay is used", "FILE"},
|
|
|
|
{{"rereplay", required_argument, 0, OPT_REREPLAY}, "Re-record replay into %s; specify input with -r or -R", "OUTFILE"},
|
2017-04-02 10:16:52 +02:00
|
|
|
#ifdef DEBUG
|
2020-04-23 16:47:42 +02:00
|
|
|
{{"play", no_argument, 0, 'p'}, "Play a specific stage"},
|
|
|
|
{{"sid", required_argument, 0, 'i'}, "Select stage by %s", "ID"},
|
|
|
|
{{"diff", required_argument, 0, 'd'}, "Select a difficulty (Easy/Normal/Hard/Lunatic)", "DIFF"},
|
|
|
|
{{"shotmode", required_argument, 0, 's'}, "Select a shotmode (marisaA/youmuA/marisaB/youmuB)", "SMODE"},
|
|
|
|
{{"dumpstages", no_argument, 0, 'u'}, "Print a list of all stages in the game"},
|
|
|
|
{{"vfs-tree", required_argument, 0, 't'}, "Print the virtual filesystem tree starting from %s", "PATH"},
|
2020-11-28 11:11:10 +01:00
|
|
|
{{"cutscene", required_argument, 0, OPT_CUTSCENE}, "Play cutscene by numeric %s and exit", "ID"},
|
|
|
|
{{"list-cutscenes", no_argument, 0, OPT_CUTSCENE_LIST}, "List all registered cutscenes with their numeric IDs and names, then exit" },
|
|
|
|
{{"intro", no_argument, 0, OPT_FORCE_INTRO}, "Play the intro cutscene even if already seen"},
|
2021-05-06 20:13:29 +02:00
|
|
|
{{"skip-to-bookmark", required_argument, 0, 'b'}, "Fast-forward stage to a specific STAGE_BOOKMARK call"},
|
2023-05-29 00:48:50 +02:00
|
|
|
{{"unlock-all", no_argument, 0, OPT_UNLOCKALL}, "Unlock all content"},
|
2017-04-02 10:16:52 +02:00
|
|
|
#endif
|
2020-04-23 16:47:42 +02:00
|
|
|
{{"frameskip", optional_argument, 0, 'f'}, "Disable FPS limiter, render only every %s frame", "FRAME"},
|
|
|
|
{{"credits", no_argument, 0, 'c'}, "Show the credits scene and exit"},
|
|
|
|
{{"renderer", required_argument, 0, OPT_RENDERER}, "Choose the rendering backend", renderer_list},
|
2023-05-08 04:23:08 +02:00
|
|
|
{{"populate-cache", no_argument, 0, OPT_POPCACHE}, "Attempt to load all available resources, populating the cache, then exit"},
|
2024-05-02 00:24:06 +02:00
|
|
|
{{"width", required_argument, 0, 'W'}, "Set window width", "WIDTH"},
|
|
|
|
{{"height", required_argument, 0, 'H'}, "Set window height", "HEIGHT"},
|
2020-04-23 16:47:42 +02:00
|
|
|
{{"help", no_argument, 0, 'h'}, "Print help and exit"},
|
|
|
|
{{"version", no_argument, 0, 'v'}, "Print version and exit"},
|
|
|
|
{ 0 }
|
2017-04-02 10:16:52 +02:00
|
|
|
};
|
|
|
|
|
2020-11-28 11:11:10 +01:00
|
|
|
memset(a, 0, sizeof(*a));
|
2017-04-02 10:16:52 +02:00
|
|
|
|
2021-06-16 00:39:48 +02:00
|
|
|
int nopts = ARRAY_SIZE(taisei_opts);
|
2017-04-02 10:16:52 +02:00
|
|
|
struct option opts[nopts];
|
|
|
|
char optc[2*nopts+1];
|
|
|
|
char *ptr = optc;
|
|
|
|
|
|
|
|
for(int i = 0; i < nopts; i++) {
|
|
|
|
opts[i] = taisei_opts[i].opt;
|
2019-09-21 15:39:05 +02:00
|
|
|
|
|
|
|
if(opts[i].val <= 0) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-04-02 10:16:52 +02:00
|
|
|
*ptr = opts[i].val;
|
|
|
|
ptr++;
|
2017-04-03 01:30:12 +02:00
|
|
|
|
2017-04-02 10:16:52 +02:00
|
|
|
if(opts[i].has_arg != no_argument) {
|
|
|
|
*ptr = ':';
|
|
|
|
ptr++;
|
2017-04-03 01:30:12 +02:00
|
|
|
|
|
|
|
if(opts[i].has_arg == optional_argument) {
|
|
|
|
*ptr = ':';
|
|
|
|
ptr++;
|
|
|
|
}
|
2017-04-02 10:16:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
*ptr = 0;
|
|
|
|
|
2017-04-02 16:53:27 +02:00
|
|
|
// on OS X, programs get passed some strange parameter when they are run from bundles.
|
|
|
|
for(int i = 0; i < argc; i++) {
|
|
|
|
if(strstartswith(argv[i],"-psn_"))
|
|
|
|
argv[i][0] = 0;
|
|
|
|
}
|
2017-04-02 10:16:52 +02:00
|
|
|
|
|
|
|
int c;
|
2017-04-04 11:10:54 +02:00
|
|
|
uint16_t stageid = 0;
|
2017-10-08 13:30:51 +02:00
|
|
|
PlayerMode *plrmode = NULL;
|
2017-04-03 01:10:16 +02:00
|
|
|
|
|
|
|
while((c = getopt_long(argc, argv, optc, opts, 0)) != -1) {
|
2017-04-03 01:30:12 +02:00
|
|
|
char *endptr = NULL;
|
|
|
|
|
2017-04-02 10:16:52 +02:00
|
|
|
switch(c) {
|
|
|
|
case 'h':
|
|
|
|
case '?':
|
|
|
|
print_help(taisei_opts);
|
2017-04-03 01:10:16 +02:00
|
|
|
// a->type = CLI_Quit;
|
2020-04-23 16:47:42 +02:00
|
|
|
exit(0);
|
2017-04-02 10:16:52 +02:00
|
|
|
break;
|
|
|
|
case 'r':
|
|
|
|
a->type = CLI_PlayReplay;
|
2021-06-16 00:45:13 +02:00
|
|
|
stralloc(&a->filename, optarg);
|
2017-04-02 10:16:52 +02:00
|
|
|
break;
|
2018-04-18 01:17:28 +02:00
|
|
|
case 'R':
|
|
|
|
a->type = CLI_VerifyReplay;
|
2021-06-16 00:45:13 +02:00
|
|
|
stralloc(&a->filename, optarg);
|
2018-04-18 01:17:28 +02:00
|
|
|
break;
|
2021-06-16 00:39:48 +02:00
|
|
|
case OPT_REREPLAY:
|
2021-06-16 00:45:13 +02:00
|
|
|
stralloc(&a->out_replay, optarg);
|
2021-06-16 00:39:48 +02:00
|
|
|
env_set("TAISEI_REPLAY_DESYNC_CHECK_FREQUENCY", 1, false);
|
|
|
|
break;
|
2017-04-02 10:16:52 +02:00
|
|
|
case 'p':
|
|
|
|
a->type = CLI_SelectStage;
|
|
|
|
break;
|
|
|
|
case 'i':
|
2017-04-03 01:30:12 +02:00
|
|
|
stageid = strtol(optarg, &endptr, 16);
|
|
|
|
if(!*optarg || endptr == optarg)
|
|
|
|
log_fatal("Stage id '%s' is not a number", optarg);
|
2017-04-02 10:16:52 +02:00
|
|
|
break;
|
|
|
|
case 'u':
|
|
|
|
a->type = CLI_DumpStages;
|
|
|
|
break;
|
|
|
|
case 'd':
|
2017-04-03 01:10:16 +02:00
|
|
|
a->diff = D_Any;
|
2021-05-06 20:18:42 +02:00
|
|
|
|
2017-04-02 10:16:52 +02:00
|
|
|
for(int i = D_Easy ; i <= NUM_SELECTABLE_DIFFICULTIES; i++) {
|
2017-04-03 01:10:16 +02:00
|
|
|
if(strcasecmp(optarg, difficulty_name(i)) == 0) {
|
2017-04-02 10:16:52 +02:00
|
|
|
a->diff = i;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2017-04-03 01:10:16 +02:00
|
|
|
|
2021-05-06 20:18:42 +02:00
|
|
|
if(a->diff == D_Any) {
|
|
|
|
char *end;
|
|
|
|
int dval = strtol(optarg, &end, 10);
|
|
|
|
if(dval >= D_Easy && dval <= D_Lunatic && end == optarg + strlen(optarg)) {
|
|
|
|
a->diff = dval;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-03 01:10:16 +02:00
|
|
|
if(a->diff == D_Any) {
|
|
|
|
log_fatal("Invalid difficulty '%s'", optarg);
|
|
|
|
}
|
|
|
|
|
2017-04-02 10:16:52 +02:00
|
|
|
break;
|
|
|
|
case 's':
|
2017-10-08 13:30:51 +02:00
|
|
|
if(!(plrmode = plrmode_parse(optarg)))
|
|
|
|
log_fatal("Invalid shotmode '%s'", optarg);
|
2017-04-02 10:16:52 +02:00
|
|
|
break;
|
2017-04-03 01:30:12 +02:00
|
|
|
case 'f':
|
|
|
|
a->frameskip = 1;
|
|
|
|
|
|
|
|
if(optarg) {
|
|
|
|
a->frameskip = strtol(optarg, &endptr, 10);
|
|
|
|
|
|
|
|
if(endptr == optarg) {
|
|
|
|
log_fatal("Frameskip value '%s' is not a number", optarg);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(a->frameskip < 0) {
|
|
|
|
a->frameskip = INT_MAX;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
2017-04-18 21:48:18 +02:00
|
|
|
case 't':
|
|
|
|
a->type = CLI_DumpVFSTree,
|
2021-06-16 00:45:13 +02:00
|
|
|
stralloc(&a->filename, optarg ? optarg : "");
|
2017-04-18 21:48:18 +02:00
|
|
|
break;
|
2017-10-23 12:48:30 +02:00
|
|
|
case 'c':
|
|
|
|
a->type = CLI_Credits;
|
|
|
|
break;
|
2019-09-21 15:39:05 +02:00
|
|
|
case OPT_RENDERER:
|
|
|
|
env_set("TAISEI_RENDERER", optarg, true);
|
|
|
|
break;
|
2020-11-28 11:11:10 +01:00
|
|
|
case OPT_CUTSCENE:
|
|
|
|
a->type = CLI_Cutscene;
|
|
|
|
a->cutscene = strtol(optarg, &endptr, 16);
|
|
|
|
|
|
|
|
if(!*optarg || endptr == optarg || (uint)a->cutscene >= NUM_CUTSCENE_IDS) {
|
|
|
|
log_fatal("Invalid cutscene ID '%s'", optarg);
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
case OPT_CUTSCENE_LIST:
|
|
|
|
for(CutsceneID i = 0; i < NUM_CUTSCENE_IDS; ++i) {
|
|
|
|
const Cutscene *cs = g_cutscenes + i;
|
|
|
|
tsfprintf(stdout, "%2d : %s\n", i, cs->phases ? cs->name : "!! UNIMPLEMENTED !!");
|
|
|
|
}
|
|
|
|
|
|
|
|
exit(0);
|
|
|
|
case OPT_FORCE_INTRO:
|
|
|
|
a->force_intro = true;
|
|
|
|
break;
|
2021-05-06 20:13:29 +02:00
|
|
|
case 'b':
|
|
|
|
env_set("TAISEI_SKIP_TO_BOOKMARK", optarg, true);
|
|
|
|
break;
|
2020-04-23 16:47:42 +02:00
|
|
|
case 'v':
|
|
|
|
tsfprintf(stdout, "%s %s\n", TAISEI_VERSION_FULL, TAISEI_VERSION_BUILD_TYPE);
|
|
|
|
exit(0);
|
2023-05-08 04:23:08 +02:00
|
|
|
case OPT_POPCACHE:
|
|
|
|
env_set("TAISEI_AGGRESSIVE_PRELOAD", 1, true);
|
|
|
|
a->type = CLI_QuitLate;
|
|
|
|
break;
|
2023-05-29 00:48:50 +02:00
|
|
|
case OPT_UNLOCKALL:
|
|
|
|
a->unlock_all = true;
|
|
|
|
break;
|
2024-05-02 00:24:06 +02:00
|
|
|
case 'W':
|
|
|
|
a->width = strtol(optarg, NULL, 10);
|
|
|
|
break;
|
|
|
|
case 'H':
|
|
|
|
a->height = strtol(optarg, NULL, 10);
|
|
|
|
break;
|
2017-04-02 10:16:52 +02:00
|
|
|
default:
|
2019-09-21 15:39:05 +02:00
|
|
|
UNREACHABLE;
|
2017-04-02 10:16:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-04 11:10:54 +02:00
|
|
|
if(stageid) {
|
2018-04-18 01:17:28 +02:00
|
|
|
switch(a->type) {
|
|
|
|
case CLI_PlayReplay:
|
|
|
|
case CLI_VerifyReplay:
|
|
|
|
case CLI_SelectStage:
|
2020-05-16 22:41:54 +02:00
|
|
|
if(stageinfo_get_by_id(stageid) == NULL) {
|
2018-04-18 01:17:28 +02:00
|
|
|
log_fatal("Invalid stage id: %X", stageid);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
log_warn("--sid was ignored");
|
|
|
|
break;
|
2017-04-03 01:50:48 +02:00
|
|
|
}
|
|
|
|
}
|
2017-04-02 10:16:52 +02:00
|
|
|
|
2017-10-08 13:30:51 +02:00
|
|
|
if(plrmode) {
|
|
|
|
if(a->type == CLI_SelectStage) {
|
|
|
|
a->plrmode = plrmode;
|
|
|
|
} else {
|
|
|
|
log_warn("--shotmode was ignored");
|
|
|
|
}
|
2017-04-02 10:16:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
a->stageid = stageid;
|
|
|
|
|
2021-06-16 00:39:48 +02:00
|
|
|
if(a->type == CLI_SelectStage && !stageid) {
|
2017-04-02 10:16:52 +02:00
|
|
|
log_fatal("StageSelect mode, but no stage id was given");
|
2021-06-16 00:39:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if(a->out_replay && a->type != CLI_PlayReplay && a->type != CLI_VerifyReplay) {
|
|
|
|
log_fatal("--rereplay requires --replay or --verify-replay");
|
|
|
|
}
|
2017-04-02 10:16:52 +02:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void free_cli_action(CLIAction *a) {
|
2023-01-09 04:19:31 +01:00
|
|
|
mem_free(a->filename);
|
Emscripten compatibility (#161)
* Major refactoring of the main loop(s) and control flow (WIP)
run_at_fps() is gone 🦀
Instead of nested blocking event loops, there is now an eventloop API
that manages an explicit stack of scenes. This makes Taisei a lot more
portable to async environments where spinning a loop forever without
yielding control simply is not an option, and that is the entire point
of this change.
A prime example of such an environment is the Web (via emscripten).
Taisei was able to run there through a terrible hack: inserting
emscripten_sleep calls into the loop, which would yield to the browser.
This has several major drawbacks: first of all, every function that
could possibly call emscripten_sleep must be compiled into a special
kind of bytecode, which then has to be interpreted at runtime, *much*
slower than JITed WebAssembly. And that includes *everything* down the
call stack, too! For more information, see
https://emscripten.org/docs/porting/emterpreter.html
Even though that method worked well enough for experimenting, despite
suboptimal performance, there is another obvious drawback:
emscripten_sleep is implemented via setTimeout(), which can be very
imprecise and is generally not reliable for fluid animation. Browsers
actually have an API specifically for that use case:
window.requestAnimationFrame(), but Taisei's original blocking control
flow style is simply not compatible with it. Emscripten exposes this API
with its emscripten_set_main_loop(), which the eventloop backend now
uses on that platform.
Unfortunately, C is still C, with no fancy closures or coroutines.
With blocking calls into menu/scene loops gone, the control flow is
reimplemented via so-called (pun intended) "call chains". That is
basically an euphemism for callback hell. With manual memory management
and zero type-safety. Not that the menu system wasn't shitty enough
already. I'll just keep telling myself that this is all temporary and
will be replaced with scripts in v1.4.
* improve build system for emscripten + various fixes
* squish menu bugs
* improve emscripten event loop; disable EMULATE_FUNCTION_POINTER_CASTS
Note that stock freetype does not work without
EMULATE_FUNCTION_POINTER_CASTS; use a patched version from the
"emscripten" branch here:
https://github.com/taisei-project/freetype2/tree/emscripten
* Enable -Wcast-function-type
Calling functions through incompatible pointers is nasal demons and
doesn't work in WASM.
* webgl: workaround a crash on some browsers
* emscripten improvements:
* Persist state (config, progress, replays, ...) in local IndexDB
* Simpler HTML shell (temporary)
* Enable more optimizations
* fix build if validate_glsl=false
* emscripten: improve asset packaging, with local cache
Note that even though there are rules to build audio bundles, audio
does *not* work yet. It looks like SDL2_mixer can not work without
threads, which is a problem. Yet another reason to write an OpenAL
backend - emscripten supports that natively.
* emscripten: customize the html shell
* emscripten: force "show log" checkbox unchecked initially
* emscripten: remove quit shortcut from main menu (since there's no quit)
* emscripten: log area fixes
* emscripten/webgl: workaround for fullscreen viewport issue
* emscripten: implement frameskip
* emscripter: improve framerate limiter
* align List to at least 8 bytes (shut up warnings)
* fix non-emscripten builds
* improve fullscreen handling, mainly for emscripten
* Workaround to make audio work in chromium
emscripten-core/emscripten#6511
* emscripten: better vsync handling; enable vsync & disable fxaa by default
2019-03-09 20:32:32 +01:00
|
|
|
a->filename = NULL;
|
2023-01-09 04:19:31 +01:00
|
|
|
mem_free(a->out_replay);
|
2021-06-16 00:39:48 +02:00
|
|
|
a->out_replay = NULL;
|
2017-04-02 10:16:52 +02:00
|
|
|
}
|