add quest_info tool
This commit is contained in:
parent
e0665ff094
commit
8cde7983a6
|
@ -26,3 +26,7 @@ target_link_libraries(bindat_to_gcdl ${SYLVERANT_LIBRARY})
|
|||
# gci_extract
|
||||
add_executable(gci_extract gci_extract.c quests.c fuzziqer_prs.c utils.c)
|
||||
target_link_libraries(gci_extract ${SYLVERANT_LIBRARY})
|
||||
|
||||
# quest_info
|
||||
add_executable(quest_info quest_info.c quests.c fuzziqer_prs.c utils.c)
|
||||
target_link_libraries(quest_info ${SYLVERANT_LIBRARY})
|
||||
|
|
439
quest_info.c
Normal file
439
quest_info.c
Normal file
|
@ -0,0 +1,439 @@
|
|||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include <sylverant/encryption.h>
|
||||
#include "fuzziqer_prs.h"
|
||||
|
||||
#include "retvals.h"
|
||||
#include "utils.h"
|
||||
#include "quests.h"
|
||||
|
||||
typedef struct _PACKED_ {
|
||||
uint8_t pkt_id;
|
||||
uint8_t pkt_flags;
|
||||
uint16_t pkt_size;
|
||||
} PACKET_HEADER;
|
||||
|
||||
#define PACKET_TYPE_ERROR 0
|
||||
#define PACKET_TYPE_HEADER 1
|
||||
#define PACKET_TYPE_DATA 2
|
||||
#define PACKET_TYPE_EOF 4 // not really a packet type, lol
|
||||
|
||||
#define QST_TYPE_NONE 0
|
||||
#define QST_TYPE_ONLINE 1
|
||||
#define QST_TYPE_DOWNLOAD 2
|
||||
|
||||
const char* get_area_string(int area, int episode) {
|
||||
if (episode == 0) {
|
||||
switch (area) {
|
||||
case 0: return "Pioneer 2";
|
||||
case 1: return "Forest 1";
|
||||
case 2: return "Forest 2";
|
||||
case 3: return "Caves 1";
|
||||
case 4: return "Caves 2";
|
||||
case 5: return "Caves 3";
|
||||
case 6: return "Mines 1";
|
||||
case 7: return "Mines 2";
|
||||
case 8: return "Ruins 1";
|
||||
case 9: return "Ruins 2";
|
||||
case 10: return "Ruins 3";
|
||||
case 11: return "Under the Dome";
|
||||
case 12: return "Underground Channel";
|
||||
case 13: return "Monitor Room";
|
||||
case 14: return "????";
|
||||
case 15: return "Visual Lobby";
|
||||
case 16: return "VR Spaceship Alpha";
|
||||
case 17: return "VR Temple Alpha";
|
||||
default: return "Invalid Area";
|
||||
}
|
||||
} else if (episode == 1) {
|
||||
switch (area) {
|
||||
case 0: return "Lab";
|
||||
case 1: return "VR Temple Alpha";
|
||||
case 2: return "VR Temple Beta";
|
||||
case 3: return "VR Spaceship Alpha";
|
||||
case 4: return "VR Spaceship Beta";
|
||||
case 5: return "Central Control Area";
|
||||
case 6: return "Jungle North";
|
||||
case 7: return "Jungle East";
|
||||
case 8: return "Mountain";
|
||||
case 9: return "Seaside";
|
||||
case 10: return "Seabed Upper";
|
||||
case 11: return "Seabed Lower";
|
||||
case 12: return "Cliffs of Gal Da Val";
|
||||
case 13: return "Test Subject Disposal Area";
|
||||
case 14: return "VR Temple Final";
|
||||
case 15: return "VR Spaceship Final";
|
||||
case 16: return "Seaside Night";
|
||||
case 17: return "Control Tower";
|
||||
default: return "Invalid Area";
|
||||
}
|
||||
} else {
|
||||
return "Invalid Episode";
|
||||
}
|
||||
}
|
||||
|
||||
void display_info(uint8_t *bin_data, size_t bin_length, uint8_t *dat_data, size_t dat_length, int qst_type) {
|
||||
int validation_result;
|
||||
int32_t result;
|
||||
uint8_t *decompressed_bin_data = NULL;
|
||||
uint8_t *decompressed_dat_data = NULL;
|
||||
size_t decompressed_bin_length, decompressed_dat_length;
|
||||
|
||||
printf("Decompressing .bin data ...\n");
|
||||
result = fuzziqer_prs_decompress_buf(bin_data, &decompressed_bin_data, bin_length);
|
||||
if (result < 0) {
|
||||
printf("Error code %d decompressing .bin data.\n", result);
|
||||
goto error;
|
||||
}
|
||||
decompressed_bin_length = result;
|
||||
|
||||
printf("Decompressing .dat data ...\n");
|
||||
result = fuzziqer_prs_decompress_buf(dat_data, &decompressed_dat_data, dat_length);
|
||||
if (result < 0) {
|
||||
printf("Error code %d decompressing .dat data.\n", result);
|
||||
goto error;
|
||||
}
|
||||
decompressed_dat_length = result;
|
||||
|
||||
|
||||
printf("Validating .bin data ...\n");
|
||||
QUEST_BIN_HEADER *bin_header = (QUEST_BIN_HEADER*)decompressed_bin_data;
|
||||
validation_result = validate_quest_bin(bin_header, decompressed_bin_length, true);
|
||||
if (validation_result == QUESTBIN_ERROR_SMALLER_BIN_SIZE) {
|
||||
printf("WARNING: Decompressed .bin data is larger than expected. Proceeding using the smaller .bin header bin_size value ...\n");
|
||||
decompressed_bin_length = bin_header->bin_size;
|
||||
} else if (validation_result == QUESTBIN_ERROR_LARGER_BIN_SIZE) {
|
||||
if ((decompressed_bin_length + 1) == bin_header->bin_size) {
|
||||
printf("WARNING: Decompressed .bin data is 1 byte smaller than the .bin header bin_size specifies. Correcting by adding a null byte ...\n");
|
||||
++decompressed_bin_length;
|
||||
decompressed_bin_data = realloc(decompressed_bin_data, decompressed_bin_length);
|
||||
decompressed_bin_data[decompressed_bin_length - 1] = 0;
|
||||
}
|
||||
} else if (validation_result) {
|
||||
printf("Aborting due to invalid quest .bin data.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
printf("Validating .dat data ...\n");
|
||||
validation_result = validate_quest_dat(decompressed_dat_data, decompressed_dat_length, true);
|
||||
if (validation_result != QUESTDAT_ERROR_EOF_EMPTY_TABLE) {
|
||||
printf("Aborting due to invalid quest .dat data.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
printf("\n\n");
|
||||
|
||||
printf("QUEST FILE FORMAT: ");
|
||||
switch (qst_type) {
|
||||
case QST_TYPE_NONE: printf("raw .bin/.dat\n"); break;
|
||||
case QST_TYPE_DOWNLOAD: printf("download/offline .qst (0x%02X)\n", PACKET_ID_QUEST_INFO_DOWNLOAD); break;
|
||||
case QST_TYPE_ONLINE: printf("online .qst (0x%02X)\n", PACKET_ID_QUEST_INFO_ONLINE); break;
|
||||
default: printf("unknown\n");
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
|
||||
printf("QUEST .BIN FILE\n");
|
||||
printf("======================================================================\n");
|
||||
printf("name: %s\n", bin_header->name);
|
||||
printf("download flag: %d\n", bin_header->download);
|
||||
printf("quest_number: as byte: %d as word: %d\n", bin_header->quest_number_byte, bin_header->quest_number_word);
|
||||
printf("episode: %d (%d)\n", bin_header->episode, bin_header->episode + 1);
|
||||
printf("xffffffff: 0x%08x\n", bin_header->xffffffff);
|
||||
printf("unknown: 0x%02x\n", bin_header->unknown);
|
||||
printf("\n");
|
||||
printf("short_description:\n%s\n\n", bin_header->short_description);
|
||||
printf("long_description:\n%s\n", bin_header->long_description);
|
||||
printf("object_code_offset: %d\n", bin_header->object_code_offset);
|
||||
printf("function_offset_table_offset: %d\n", bin_header->function_offset_table_offset);
|
||||
printf("object_code_size: %d\n", (bin_header->function_offset_table_offset - bin_header->object_code_offset));
|
||||
printf("function_offset_table_size: %d\n", (bin_header->bin_size - bin_header->function_offset_table_offset));
|
||||
|
||||
|
||||
printf("\n\n");
|
||||
printf("QUEST .DAT FILE\n");
|
||||
printf("======================================================================\n");
|
||||
|
||||
int table_index = 0;
|
||||
uint32_t offset = 0;
|
||||
while (offset < decompressed_dat_length) {
|
||||
QUEST_DAT_TABLE_HEADER *table_header = (QUEST_DAT_TABLE_HEADER*)(decompressed_dat_data + offset);
|
||||
|
||||
printf("Table index %d - ", table_index);
|
||||
switch (table_header->type) {
|
||||
case 1:
|
||||
printf("Object\n");
|
||||
printf("table_body_size: %d\n", table_header->table_body_size);
|
||||
printf("area: %s (%d)\n", get_area_string(table_header->area, bin_header->episode), table_header->area);
|
||||
printf("object count: %d\n", table_header->table_body_size / 68);
|
||||
break;
|
||||
case 2:
|
||||
printf("NPC\n");
|
||||
printf("table_body_size: %d\n", table_header->table_body_size);
|
||||
printf("area: %s (%d)\n", get_area_string(table_header->area, bin_header->episode), table_header->area);
|
||||
printf("npc count: %d\n", table_header->table_body_size / 72);
|
||||
break;
|
||||
case 3:
|
||||
printf("Wave\n");
|
||||
printf("table_body_size: %d\n", table_header->table_body_size);
|
||||
printf("area: %s (%d)\n", get_area_string(table_header->area, bin_header->episode), table_header->area);
|
||||
break;
|
||||
case 4:
|
||||
printf("Challenge Mode Spawn Points\n");
|
||||
printf("table_body_size: %d\n", table_header->table_body_size);
|
||||
printf("area: %s (%d)\n", get_area_string(table_header->area, bin_header->episode), table_header->area);
|
||||
break;
|
||||
case 5:
|
||||
printf("Challenge Mode (?)\n");
|
||||
printf("table_body_size: %d\n", table_header->table_body_size);
|
||||
printf("area: %s (%d)\n", get_area_string(table_header->area, bin_header->episode), table_header->area);
|
||||
break;
|
||||
default:
|
||||
if (table_header->type == 0 && table_header->table_size == 0 && table_header->area == 0 && table_header->table_body_size == 0)
|
||||
printf("EOF marker\n");
|
||||
else {
|
||||
printf("Unknown\n");
|
||||
printf("type: %d\n", table_header->type);
|
||||
printf("table_body_size: %d\n", table_header->table_body_size);
|
||||
printf("area: %d\n", table_header->area);
|
||||
}
|
||||
break;
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
offset += sizeof(QUEST_DAT_TABLE_HEADER);
|
||||
offset += table_header->table_body_size;
|
||||
++table_index;
|
||||
}
|
||||
|
||||
error:
|
||||
free(decompressed_bin_data);
|
||||
free(decompressed_dat_data);
|
||||
}
|
||||
|
||||
int read_next_qst_packet(FILE *fp, QST_HEADER *out_header_packet, QST_DATA_CHUNK *out_data_packet) {
|
||||
size_t bytes_read;
|
||||
PACKET_HEADER packet_header;
|
||||
|
||||
bytes_read = fread(&packet_header, 1, sizeof(PACKET_HEADER), fp);
|
||||
if (bytes_read == 0 && feof(fp))
|
||||
return PACKET_TYPE_EOF;
|
||||
if (bytes_read != sizeof(PACKET_HEADER))
|
||||
return PACKET_TYPE_ERROR;
|
||||
|
||||
if (packet_header.pkt_size == sizeof(QST_HEADER) &&
|
||||
(packet_header.pkt_id == PACKET_ID_QUEST_INFO_ONLINE ||
|
||||
packet_header.pkt_id == PACKET_ID_QUEST_INFO_DOWNLOAD)) {
|
||||
memcpy(out_header_packet, &packet_header, sizeof(PACKET_HEADER));
|
||||
size_t remaining_bytes = sizeof(QST_HEADER) - sizeof(PACKET_HEADER);
|
||||
bytes_read = fread((uint8_t*)out_header_packet + sizeof(PACKET_HEADER), 1, remaining_bytes, fp);
|
||||
if (bytes_read != remaining_bytes)
|
||||
return PACKET_TYPE_ERROR;
|
||||
else
|
||||
return PACKET_TYPE_HEADER;
|
||||
|
||||
} else if (packet_header.pkt_size == sizeof(QST_DATA_CHUNK) &&
|
||||
(packet_header.pkt_id == PACKET_ID_QUEST_CHUNK_ONLINE ||
|
||||
packet_header.pkt_id == PACKET_ID_QUEST_CHUNK_DOWNLOAD)) {
|
||||
memcpy(out_data_packet, &packet_header, sizeof(PACKET_HEADER));
|
||||
size_t remaining_bytes = sizeof(QST_DATA_CHUNK) - sizeof(PACKET_HEADER);
|
||||
bytes_read = fread((uint8_t*)out_data_packet + sizeof(PACKET_HEADER), 1, remaining_bytes, fp);
|
||||
if (bytes_read != remaining_bytes)
|
||||
return PACKET_TYPE_ERROR;
|
||||
else
|
||||
return PACKET_TYPE_DATA;
|
||||
|
||||
} else
|
||||
return PACKET_TYPE_ERROR;
|
||||
}
|
||||
|
||||
int load_quest_from_qst(const char *filename, uint8_t **out_bin_data, size_t *out_bin_length, uint8_t **out_dat_data, size_t *out_dat_length, int *out_qst_type) {
|
||||
int returncode;
|
||||
FILE *fp = NULL;
|
||||
uint8_t *bin_data = NULL;
|
||||
uint8_t *dat_data = NULL;
|
||||
int qst_type;
|
||||
|
||||
fp = fopen(filename, "rb");
|
||||
if (!fp) {
|
||||
returncode = ERROR_FILE_NOT_FOUND;
|
||||
goto error;
|
||||
}
|
||||
|
||||
char bin_filename[QUEST_FILENAME_MAX_LENGTH] = "";
|
||||
char dat_filename[QUEST_FILENAME_MAX_LENGTH] = "";
|
||||
size_t bin_data_length, dat_data_length;
|
||||
size_t bin_data_pos, dat_data_pos;
|
||||
|
||||
while (!feof(fp)) {
|
||||
QST_HEADER header;
|
||||
QST_DATA_CHUNK data;
|
||||
int type = read_next_qst_packet(fp, &header, &data);
|
||||
|
||||
if (type == PACKET_TYPE_EOF && bin_data && dat_data)
|
||||
break;
|
||||
|
||||
if (type == PACKET_TYPE_ERROR) {
|
||||
returncode = ERROR_BAD_DATA;
|
||||
goto error;
|
||||
|
||||
} else if (type == PACKET_TYPE_HEADER) {
|
||||
//CRYPT_PrintData(&header, sizeof(QST_HEADER));
|
||||
if (string_ends_with(header.filename, ".bin")) {
|
||||
strncpy(bin_filename, header.filename, QUEST_FILENAME_MAX_LENGTH);
|
||||
bin_data_length = header.size;
|
||||
bin_data_pos = 0;
|
||||
bin_data = malloc(bin_data_length);
|
||||
} else if (string_ends_with(header.filename, ".dat")) {
|
||||
strncpy(dat_filename, header.filename, QUEST_FILENAME_MAX_LENGTH);
|
||||
dat_data_length = header.size;
|
||||
dat_data_pos = 0;
|
||||
dat_data = malloc(dat_data_length);
|
||||
} else {
|
||||
returncode = ERROR_BAD_DATA;
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (header.pkt_id == PACKET_ID_QUEST_INFO_ONLINE)
|
||||
qst_type = QST_TYPE_ONLINE;
|
||||
else
|
||||
qst_type = QST_TYPE_DOWNLOAD;
|
||||
|
||||
} else if (type == PACKET_TYPE_DATA) {
|
||||
//CRYPT_PrintData(&data, sizeof(QST_DATA_CHUNK));
|
||||
if (strncmp(data.filename, bin_filename, QUEST_FILENAME_MAX_LENGTH) == 0) {
|
||||
memcpy(bin_data + bin_data_pos, data.data, data.size);
|
||||
bin_data_pos += data.size;
|
||||
|
||||
} else if (strncmp(data.filename, dat_filename, QUEST_FILENAME_MAX_LENGTH) == 0) {
|
||||
memcpy(dat_data + dat_data_pos, data.data, data.size);
|
||||
dat_data_pos += data.size;
|
||||
|
||||
} else {
|
||||
returncode = ERROR_BAD_DATA;
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
*out_bin_length = bin_data_length;
|
||||
*out_dat_length = dat_data_length;
|
||||
*out_bin_data = bin_data;
|
||||
*out_dat_data = dat_data;
|
||||
*out_qst_type = qst_type;
|
||||
|
||||
return SUCCESS;
|
||||
|
||||
error:
|
||||
fclose(fp);
|
||||
free(bin_data);
|
||||
free(dat_data);
|
||||
return returncode;
|
||||
}
|
||||
|
||||
int decrypt_qst_bindat(uint8_t *bin_data, size_t *bin_length, uint8_t *dat_data, size_t *dat_length) {
|
||||
DOWNLOAD_QUEST_CHUNKS_HEADER *bin_dl_header = (DOWNLOAD_QUEST_CHUNKS_HEADER*)bin_data;
|
||||
DOWNLOAD_QUEST_CHUNKS_HEADER *dat_dl_header = (DOWNLOAD_QUEST_CHUNKS_HEADER*)dat_data;
|
||||
|
||||
CRYPT_SETUP bin_cs, dat_cs;
|
||||
CRYPT_CreateKeys(&bin_cs, &bin_dl_header->crypt_key, CRYPT_PC);
|
||||
CRYPT_CreateKeys(&dat_cs, &dat_dl_header->crypt_key, CRYPT_PC);
|
||||
|
||||
uint8_t *actual_bin_data = bin_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
uint8_t *actual_dat_data = dat_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
size_t decrypted_bin_length = *bin_length - sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
size_t decrypted_dat_length = *dat_length - sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
CRYPT_CryptData(&bin_cs, bin_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER), decrypted_bin_length, 0);
|
||||
CRYPT_CryptData(&dat_cs, dat_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER), decrypted_dat_length, 0);
|
||||
|
||||
memmove(bin_data, actual_bin_data, decrypted_bin_length);
|
||||
memmove(dat_data, actual_dat_data, decrypted_dat_length);
|
||||
|
||||
*bin_length = decrypted_bin_length;
|
||||
*dat_length = decrypted_dat_length;
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int load_quest_from_bindat(const char *bin_filename, const char *dat_filename, uint8_t **out_bin_data, size_t *out_bin_length, uint8_t **out_dat_data, size_t *out_dat_length) {
|
||||
int returncode;
|
||||
uint8_t *bin_data = NULL;
|
||||
uint8_t *dat_data = NULL;
|
||||
uint32_t bin_data_length, dat_data_length;
|
||||
|
||||
returncode = read_file(bin_filename, &bin_data, &bin_data_length);
|
||||
if (returncode)
|
||||
goto error;
|
||||
|
||||
returncode = read_file(dat_filename, &dat_data, &dat_data_length);
|
||||
if (returncode)
|
||||
goto error;
|
||||
|
||||
*out_bin_length = bin_data_length;
|
||||
*out_dat_length = dat_data_length;
|
||||
*out_bin_data = bin_data;
|
||||
*out_dat_data = dat_data;
|
||||
|
||||
return SUCCESS;
|
||||
|
||||
error:
|
||||
free(bin_data);
|
||||
free(dat_data);
|
||||
return returncode;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int returncode;
|
||||
|
||||
if (argc != 2 && argc != 3) {
|
||||
printf("Usage: quest_info quest.bin quest.dat\n");
|
||||
printf(" quest_info quest.qst\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t *bin_data = NULL;
|
||||
uint8_t *dat_data = NULL;
|
||||
size_t bin_data_size, dat_data_size;
|
||||
int qst_type = QST_TYPE_NONE;
|
||||
|
||||
if (argc == 2) {
|
||||
printf("Reading .qst file: %s\n", argv[1]);
|
||||
returncode = load_quest_from_qst(argv[1], &bin_data, &bin_data_size, &dat_data, &dat_data_size, &qst_type);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) loading quest: %s\n", returncode, get_error_message(returncode), argv[1]);
|
||||
goto error;
|
||||
}
|
||||
if (qst_type == QST_TYPE_DOWNLOAD) {
|
||||
printf("Decrypting download .qst data ...\n");
|
||||
returncode = decrypt_qst_bindat(bin_data, &bin_data_size, dat_data, &dat_data_size);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) while decrypting .qst contents", returncode, get_error_message(returncode));
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printf("Reading .bin file %s and .dat file %s ... \n", argv[1], argv[2]);
|
||||
returncode = load_quest_from_bindat(argv[1], argv[2], &bin_data, &bin_data_size, &dat_data, &dat_data_size);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) loading quest files %s and %s\n", returncode, get_error_message(returncode), argv[1], argv[2]);
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
display_info(bin_data, bin_data_size, dat_data, dat_data_size, qst_type);
|
||||
|
||||
returncode = 0;
|
||||
goto quit;
|
||||
error:
|
||||
returncode = 1;
|
||||
quit:
|
||||
free(bin_data);
|
||||
free(dat_data);
|
||||
return returncode;
|
||||
}
|
14
utils.c
14
utils.c
|
@ -101,6 +101,20 @@ char* append_string(const char *a, const char *b) {
|
|||
return result;
|
||||
}
|
||||
|
||||
bool string_ends_with(const char *s, const char *suffix) {
|
||||
if (!s || !suffix)
|
||||
return false;
|
||||
|
||||
size_t s_len = strlen(s);
|
||||
size_t suffix_len = strlen(suffix);
|
||||
if (suffix_len > s_len)
|
||||
return false;
|
||||
else {
|
||||
const char *end_of_s = s + (s_len - suffix_len);
|
||||
return strncmp(end_of_s, suffix, suffix_len) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
const char* get_error_message(int retvals_error_code) {
|
||||
retvals_error_code = abs(retvals_error_code);
|
||||
|
||||
|
|
2
utils.h
2
utils.h
|
@ -2,6 +2,7 @@
|
|||
#define UTILS_H_INCLUDED
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "retvals.h"
|
||||
|
||||
|
@ -10,6 +11,7 @@ int write_file(const char *filename, const void *data, size_t size);
|
|||
int get_filesize(const char *filename, size_t *out_size);
|
||||
const char* path_to_filename(const char *path);
|
||||
char* append_string(const char *a, const char *b);
|
||||
bool string_ends_with(const char *s, const char *suffix);
|
||||
|
||||
const char* get_error_message(int retvals_error_code);
|
||||
|
||||
|
|
Loading…
Reference in a new issue