2013-07-06 04:37:13 +00:00
|
|
|
/*
|
2015-10-22 05:13:26 +00:00
|
|
|
* Apple // emulator for *ix
|
|
|
|
*
|
|
|
|
* This software package is subject to the GNU General Public License
|
|
|
|
* version 3 or later (your choice) as published by the Free Software
|
|
|
|
* Foundation.
|
2013-06-11 07:08:15 +00:00
|
|
|
*
|
|
|
|
* Copyright 1994 Alexander Jean-Claude Bottema
|
|
|
|
* Copyright 1995 Stephen Lee
|
|
|
|
* Copyright 1997, 1998 Aaron Culliney
|
|
|
|
* Copyright 1998, 1999, 2000 Michael Deutschmann
|
2015-10-22 05:13:26 +00:00
|
|
|
* Copyright 2013-2015 Aaron Culliney
|
2013-06-11 07:08:15 +00:00
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2014-01-23 04:42:34 +00:00
|
|
|
#include "common.h"
|
2013-06-11 07:08:15 +00:00
|
|
|
|
2016-10-09 21:54:11 +00:00
|
|
|
#define SAVE_MAGICK "A2VM"
|
|
|
|
#define SAVE_MAGICK2 "A2V2"
|
2017-07-07 05:36:37 +00:00
|
|
|
#define SAVE_VERSION 2
|
2015-11-23 03:04:46 +00:00
|
|
|
#define SAVE_MAGICK_LEN sizeof(SAVE_MAGICK)
|
|
|
|
|
2016-02-23 05:36:08 +00:00
|
|
|
typedef struct module_ctor_node_s {
|
|
|
|
struct module_ctor_node_s *next;
|
|
|
|
long order;
|
|
|
|
startup_callback_f ctor;
|
|
|
|
} module_ctor_node_s;
|
|
|
|
|
|
|
|
static module_ctor_node_s *head = NULL;
|
2016-04-17 19:08:11 +00:00
|
|
|
static bool emulatorShuttingDown = false;
|
2016-02-23 05:36:08 +00:00
|
|
|
|
2015-02-23 19:19:41 +00:00
|
|
|
const char *data_dir = NULL;
|
2015-09-11 07:00:04 +00:00
|
|
|
char **argv = NULL;
|
|
|
|
int argc = 0;
|
2015-09-26 22:20:54 +00:00
|
|
|
CrashHandler_s *crashHandler = NULL;
|
2015-02-18 00:16:34 +00:00
|
|
|
|
2016-02-23 05:36:08 +00:00
|
|
|
#if defined(CONFIG_DATADIR)
|
2015-11-14 07:13:21 +00:00
|
|
|
static void _init_common(void) {
|
2016-02-26 05:43:54 +00:00
|
|
|
data_dir = STRDUP(CONFIG_DATADIR PATH_SEPARATOR PACKAGE_NAME);
|
2017-07-30 17:12:51 +00:00
|
|
|
log_init();
|
|
|
|
LOG("Initializing common...");
|
2016-02-23 05:36:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static __attribute__((constructor)) void __init_common(void) {
|
|
|
|
emulator_registerStartupCallback(CTOR_PRIORITY_FIRST, &_init_common);
|
|
|
|
}
|
|
|
|
#elif defined(ANDROID) || defined(__APPLE__)
|
|
|
|
// data_dir is set up elsewhere
|
|
|
|
#else
|
2015-09-11 07:00:04 +00:00
|
|
|
# error "Specify a CONFIG_DATADIR and PACKAGE_NAME"
|
2014-01-23 04:42:34 +00:00
|
|
|
#endif
|
2013-06-11 07:08:15 +00:00
|
|
|
|
2015-11-23 03:04:46 +00:00
|
|
|
static bool _save_state(int fd, const uint8_t * outbuf, ssize_t outmax) {
|
|
|
|
ssize_t outlen = 0;
|
|
|
|
do {
|
|
|
|
if (TEMP_FAILURE_RETRY(outlen = write(fd, outbuf, outmax)) == -1) {
|
2017-07-16 00:34:43 +00:00
|
|
|
LOG("OOPS, error writing emulator save-state file");
|
2015-11-23 03:04:46 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
outbuf += outlen;
|
|
|
|
outmax -= outlen;
|
|
|
|
} while (outmax > 0);
|
|
|
|
|
|
|
|
return outmax == 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool _load_state(int fd, uint8_t * inbuf, ssize_t inmax) {
|
|
|
|
ssize_t inlen = 0;
|
2016-10-16 19:52:27 +00:00
|
|
|
|
|
|
|
assert(inmax > 0);
|
|
|
|
|
|
|
|
struct stat stat_buf;
|
|
|
|
if (UNLIKELY(fstat(fd, &stat_buf) < 0)) {
|
2017-07-16 00:34:43 +00:00
|
|
|
LOG("OOPS, could not stat FD");
|
2016-10-16 19:52:27 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
off_t fileSiz = stat_buf.st_size;
|
|
|
|
off_t filePos = lseek(fd, 0, SEEK_CUR);
|
|
|
|
if (UNLIKELY(filePos < 0)) {
|
2017-07-16 00:34:43 +00:00
|
|
|
LOG("OOPS, could not lseek FD");
|
2016-10-16 19:52:27 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (UNLIKELY(filePos + inmax > fileSiz)) {
|
|
|
|
LOG("OOPS, encountered truncated save-state file");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-11-23 03:04:46 +00:00
|
|
|
do {
|
|
|
|
if (TEMP_FAILURE_RETRY(inlen = read(fd, inbuf, inmax)) == -1) {
|
2017-07-16 00:34:43 +00:00
|
|
|
LOG("error reading emulator save-state file");
|
2015-11-23 03:04:46 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (inlen == 0) {
|
2017-07-16 00:34:43 +00:00
|
|
|
LOG("error reading emulator save-state file (truncated)");
|
2015-11-23 03:04:46 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
inbuf += inlen;
|
|
|
|
inmax -= inlen;
|
|
|
|
} while (inmax > 0);
|
|
|
|
|
|
|
|
return inmax == 0;
|
|
|
|
}
|
|
|
|
|
2017-05-21 21:58:55 +00:00
|
|
|
static int _load_magick(int fd) {
|
|
|
|
// load header
|
|
|
|
uint8_t magick[SAVE_MAGICK_LEN] = { 0 };
|
|
|
|
if (!_load_state(fd, magick, SAVE_MAGICK_LEN)) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check header
|
|
|
|
|
2017-07-07 05:36:37 +00:00
|
|
|
if (memcmp(magick, SAVE_MAGICK2, SAVE_MAGICK_LEN) == 0) {
|
2017-05-21 21:58:55 +00:00
|
|
|
return 2;
|
2017-05-29 17:24:30 +00:00
|
|
|
} else if (memcmp(magick, SAVE_MAGICK, SAVE_MAGICK_LEN) == 0) {
|
|
|
|
return 1;
|
2017-05-21 21:58:55 +00:00
|
|
|
}
|
|
|
|
|
2017-07-16 00:34:43 +00:00
|
|
|
LOG("bad header magick in emulator save state file");
|
2017-05-21 21:58:55 +00:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2017-06-30 05:42:28 +00:00
|
|
|
bool emulator_saveState(int fd) {
|
2015-11-23 03:04:46 +00:00
|
|
|
bool saved = false;
|
|
|
|
|
2016-09-11 18:53:59 +00:00
|
|
|
#if !TESTING
|
2015-11-23 03:04:46 +00:00
|
|
|
assert(cpu_isPaused() && "should be paused to save state");
|
2016-09-11 18:53:59 +00:00
|
|
|
#endif
|
2015-11-23 03:04:46 +00:00
|
|
|
|
|
|
|
do {
|
|
|
|
// save header
|
2017-07-07 05:36:37 +00:00
|
|
|
if (!_save_state(fd, (const uint8_t *)SAVE_MAGICK2, SAVE_MAGICK_LEN)) {
|
2015-11-23 03:04:46 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
StateHelper_s helper = {
|
|
|
|
.fd = fd,
|
2016-10-09 21:54:11 +00:00
|
|
|
.version = SAVE_VERSION,
|
2015-11-23 03:04:46 +00:00
|
|
|
.save = &_save_state,
|
|
|
|
.load = &_load_state,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!disk6_saveState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!vm_saveState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!cpu65_saveState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2016-10-13 03:36:16 +00:00
|
|
|
if (!timing_saveState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2015-11-23 03:04:46 +00:00
|
|
|
if (!video_saveState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2016-10-09 21:54:11 +00:00
|
|
|
if (!mb_saveState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2015-11-23 03:04:46 +00:00
|
|
|
TEMP_FAILURE_RETRY(fsync(fd));
|
|
|
|
saved = true;
|
|
|
|
} while (0);
|
|
|
|
|
|
|
|
return saved;
|
|
|
|
}
|
|
|
|
|
2017-06-30 05:42:28 +00:00
|
|
|
bool emulator_loadState(int fd, int fdA, int fdB) {
|
2015-11-23 03:04:46 +00:00
|
|
|
bool loaded = false;
|
|
|
|
|
2016-09-11 18:53:59 +00:00
|
|
|
#if !TESTING
|
2015-11-23 03:04:46 +00:00
|
|
|
assert(cpu_isPaused() && "should be paused to load state");
|
2016-09-11 18:53:59 +00:00
|
|
|
#endif
|
2015-11-23 03:04:46 +00:00
|
|
|
|
2016-02-18 06:38:50 +00:00
|
|
|
video_setDirty(A2_DIRTY_FLAG);
|
|
|
|
|
2015-11-23 03:04:46 +00:00
|
|
|
do {
|
2017-05-21 21:58:55 +00:00
|
|
|
int version = _load_magick(fd);
|
|
|
|
if (version < 0) {
|
2015-11-23 03:04:46 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
StateHelper_s helper = {
|
|
|
|
.fd = fd,
|
2016-10-09 21:54:11 +00:00
|
|
|
.version = version,
|
2017-05-21 21:58:55 +00:00
|
|
|
.diskFdA = fdA,
|
|
|
|
.diskFdB = fdB,
|
2015-11-23 03:04:46 +00:00
|
|
|
.save = &_save_state,
|
|
|
|
.load = &_load_state,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!disk6_loadState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!vm_loadState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!cpu65_loadState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2016-10-16 19:52:27 +00:00
|
|
|
if (version >= 2) {
|
|
|
|
if (!timing_loadState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
2016-10-13 03:36:16 +00:00
|
|
|
}
|
|
|
|
|
2015-11-23 03:04:46 +00:00
|
|
|
if (!video_loadState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2016-10-09 21:54:11 +00:00
|
|
|
if (version >= 2) {
|
|
|
|
if (!mb_loadState(&helper)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-16 19:52:27 +00:00
|
|
|
// sanity-check whole file was read
|
|
|
|
|
|
|
|
struct stat stat_buf;
|
|
|
|
if (fstat(fd, &stat_buf) < 0) {
|
2017-07-16 00:34:43 +00:00
|
|
|
LOG("OOPS, could not stat FD");
|
2016-10-16 19:52:27 +00:00
|
|
|
}
|
|
|
|
off_t fileSiz = stat_buf.st_size;
|
|
|
|
off_t filePos = lseek(fd, 0, SEEK_CUR);
|
|
|
|
if (filePos < 0) {
|
2017-07-16 00:34:43 +00:00
|
|
|
LOG("OOPS, could not lseek FD");
|
2016-10-16 19:52:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (UNLIKELY(filePos != fileSiz)) {
|
|
|
|
LOG("OOPS, state file read: %lu total: %lu", filePos, fileSiz);
|
|
|
|
}
|
|
|
|
|
2015-11-23 03:04:46 +00:00
|
|
|
loaded = true;
|
|
|
|
} while (0);
|
|
|
|
|
|
|
|
if (!loaded) {
|
2016-10-16 19:52:27 +00:00
|
|
|
LOG("OOPS, problem(s) encountered loading emulator save-state file");
|
2015-11-23 03:04:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return loaded;
|
|
|
|
}
|
|
|
|
|
2017-06-30 05:42:28 +00:00
|
|
|
bool emulator_stateExtractDiskPaths(int fd, JSON_ref json) {
|
2017-05-21 21:58:55 +00:00
|
|
|
bool loaded = false;
|
|
|
|
|
|
|
|
do {
|
|
|
|
int version = _load_magick(fd);
|
|
|
|
if (version < 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
StateHelper_s helper = {
|
|
|
|
.fd = fd,
|
|
|
|
.version = version,
|
|
|
|
.diskFdA = -1,
|
|
|
|
.diskFdB = -1,
|
|
|
|
.save = &_save_state,
|
|
|
|
.load = &_load_state,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!disk6_stateExtractDiskPaths(&helper, json)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
loaded = true;
|
|
|
|
} while (0);
|
|
|
|
|
2017-07-04 16:20:55 +00:00
|
|
|
if (fd >= 0) {
|
|
|
|
// Ensure that we leave the file descriptor ready for a call to emulator_loadState()
|
|
|
|
off_t ret = lseek(fd, 0, SEEK_SET);
|
|
|
|
if (ret != 0) {
|
2017-07-16 00:34:43 +00:00
|
|
|
LOG("OOPS : state file lseek() failed!");
|
2017-07-04 16:20:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-21 21:58:55 +00:00
|
|
|
if (!loaded) {
|
|
|
|
LOG("OOPS, problem(s) encountered loading emulator save-state file");
|
|
|
|
}
|
|
|
|
|
|
|
|
return loaded;
|
|
|
|
}
|
|
|
|
|
2015-05-31 19:59:26 +00:00
|
|
|
static void _shutdown_threads(void) {
|
2016-03-26 21:20:57 +00:00
|
|
|
#if defined(__linux__) && !defined(ANDROID)
|
2015-05-31 19:59:26 +00:00
|
|
|
LOG("Emulator waiting for other threads to clean up...");
|
|
|
|
do {
|
|
|
|
DIR *dir = opendir("/proc/self/task");
|
|
|
|
if (!dir) {
|
2017-07-16 00:34:43 +00:00
|
|
|
LOG("Cannot open /proc/self/task !");
|
2015-05-31 19:59:26 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
int thread_count = 0;
|
|
|
|
struct dirent *d = NULL;
|
|
|
|
while ((d = readdir(dir)) != NULL) {
|
|
|
|
if (strncmp(".", d->d_name, 2) == 0) {
|
|
|
|
// ignore
|
|
|
|
} else if (strncmp("..", d->d_name, 3) == 0) {
|
|
|
|
// ignore
|
|
|
|
} else {
|
|
|
|
++thread_count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
closedir(dir);
|
|
|
|
|
|
|
|
assert(thread_count >= 1 && "there must at least be one thread =P");
|
|
|
|
if (thread_count == 1) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
static struct timespec ts = { .tv_sec=0, .tv_nsec=33333333 };
|
|
|
|
nanosleep(&ts, NULL); // 30Hz framerate
|
|
|
|
} while (1);
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2016-02-23 05:36:08 +00:00
|
|
|
void emulator_registerStartupCallback(long order, startup_callback_f ctor) {
|
|
|
|
|
|
|
|
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
pthread_mutex_lock(&mutex);
|
|
|
|
|
|
|
|
module_ctor_node_s *node = MALLOC(sizeof(module_ctor_node_s));
|
|
|
|
assert(node);
|
|
|
|
node->next = NULL;
|
|
|
|
node->order = order;
|
|
|
|
node->ctor = ctor;
|
|
|
|
|
|
|
|
module_ctor_node_s *p0 = NULL;
|
|
|
|
module_ctor_node_s *p = head;
|
|
|
|
while (p && (order > p->order)) {
|
|
|
|
p0 = p;
|
|
|
|
p = p->next;
|
|
|
|
}
|
|
|
|
if (p0) {
|
|
|
|
p0->next = node;
|
|
|
|
} else {
|
|
|
|
head = node;
|
|
|
|
}
|
|
|
|
node->next = p;
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&mutex);
|
|
|
|
}
|
|
|
|
|
2016-04-13 05:24:04 +00:00
|
|
|
void emulator_ctors(void) {
|
2016-02-23 05:36:08 +00:00
|
|
|
module_ctor_node_s *p = head;
|
|
|
|
head = NULL;
|
|
|
|
while (p) {
|
|
|
|
p->ctor();
|
|
|
|
module_ctor_node_s *next = p->next;
|
|
|
|
FREE(p);
|
|
|
|
p = next;
|
|
|
|
}
|
|
|
|
head = NULL;
|
2016-04-13 05:24:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void emulator_start(void) {
|
|
|
|
|
|
|
|
emulator_ctors();
|
2016-02-23 05:36:08 +00:00
|
|
|
|
2016-03-26 05:34:33 +00:00
|
|
|
prefs_load(); // user prefs
|
2016-04-06 05:04:57 +00:00
|
|
|
prefs_sync(NULL);
|
2016-04-13 05:24:04 +00:00
|
|
|
|
|
|
|
#if defined(INTERFACE_CLASSIC) && !TESTING
|
2014-11-22 22:28:38 +00:00
|
|
|
c_keys_set_key(kF8); // show credits before emulation start
|
2015-02-16 02:33:35 +00:00
|
|
|
#endif
|
2016-02-06 21:13:31 +00:00
|
|
|
|
|
|
|
#if !defined(__APPLE__) && !defined(ANDROID)
|
2015-09-13 21:24:17 +00:00
|
|
|
video_init();
|
2016-02-06 21:13:31 +00:00
|
|
|
#endif
|
2016-03-26 21:20:57 +00:00
|
|
|
|
2016-04-13 05:24:04 +00:00
|
|
|
timing_startCPU();
|
2015-09-11 07:00:04 +00:00
|
|
|
}
|
2015-05-31 19:59:26 +00:00
|
|
|
|
2015-09-11 07:00:04 +00:00
|
|
|
void emulator_shutdown(void) {
|
2016-04-17 19:08:11 +00:00
|
|
|
emulatorShuttingDown = true;
|
|
|
|
video_shutdown();
|
|
|
|
prefs_shutdown();
|
2015-09-11 07:00:04 +00:00
|
|
|
timing_stopCPU();
|
|
|
|
_shutdown_threads();
|
|
|
|
}
|
2015-05-31 19:59:26 +00:00
|
|
|
|
2016-04-17 19:08:11 +00:00
|
|
|
bool emulator_isShuttingDown(void) {
|
|
|
|
return emulatorShuttingDown;
|
|
|
|
}
|
|
|
|
|
2016-04-13 05:24:04 +00:00
|
|
|
#if !defined(__APPLE__) && !defined(ANDROID)
|
2015-09-11 07:00:04 +00:00
|
|
|
int main(int _argc, char **_argv) {
|
|
|
|
argc = _argc;
|
|
|
|
argv = _argv;
|
2015-05-31 19:59:26 +00:00
|
|
|
|
2016-04-13 05:24:04 +00:00
|
|
|
#if TESTING
|
|
|
|
# if TEST_CPU
|
|
|
|
// Currently this test is the only one that blocks current thread and runs as a black screen
|
|
|
|
extern int test_cpu(int, char *[]);
|
|
|
|
test_cpu(argc, argv);
|
|
|
|
# elif TEST_DISK
|
|
|
|
extern int test_disk(int, char *[]);
|
|
|
|
test_disk(argc, argv);
|
2016-07-24 00:23:36 +00:00
|
|
|
# elif TEST_DISPLAY
|
|
|
|
extern int test_display(int, char *[]);
|
|
|
|
test_display(argc, argv);
|
2016-04-27 03:57:35 +00:00
|
|
|
# elif TEST_PREFS
|
2016-04-13 05:24:04 +00:00
|
|
|
extern void test_prefs(int, char *[]);
|
|
|
|
test_prefs(argc, argv);
|
2016-04-27 03:57:35 +00:00
|
|
|
# elif TEST_TRACE
|
|
|
|
extern void test_trace(int, char *[]);
|
|
|
|
test_trace(argc, argv);
|
2016-09-11 18:53:59 +00:00
|
|
|
# elif TEST_UI
|
|
|
|
extern int test_ui(int, char *[]);
|
|
|
|
test_ui(argc, argv);
|
2016-07-24 00:23:36 +00:00
|
|
|
# elif TEST_VM
|
|
|
|
extern int test_vm(int, char *[]);
|
|
|
|
test_vm(argc, argv);
|
2016-04-13 05:24:04 +00:00
|
|
|
# else
|
|
|
|
# error "OOPS, no testsuite specified"
|
|
|
|
# endif
|
|
|
|
#endif
|
|
|
|
|
2016-04-16 20:33:50 +00:00
|
|
|
cpu_pause();
|
|
|
|
|
2015-09-11 07:00:04 +00:00
|
|
|
emulator_start();
|
2015-09-12 22:33:22 +00:00
|
|
|
|
2016-04-16 20:33:50 +00:00
|
|
|
cpu_resume();
|
|
|
|
|
2016-04-13 05:24:04 +00:00
|
|
|
video_main_loop();
|
2015-09-12 22:33:22 +00:00
|
|
|
|
2015-09-11 07:00:04 +00:00
|
|
|
emulator_shutdown();
|
2015-05-31 19:59:26 +00:00
|
|
|
|
2015-09-12 22:33:22 +00:00
|
|
|
LOG("Emulator exit ...");
|
|
|
|
|
2015-05-31 19:59:26 +00:00
|
|
|
return 0;
|
2013-06-11 07:08:15 +00:00
|
|
|
}
|
2015-02-18 04:28:23 +00:00
|
|
|
#endif
|
2014-01-25 22:13:38 +00:00
|
|
|
|